summaryrefslogtreecommitdiffstats
path: root/src/ui
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/CMakeLists.txt561
-rw-r--r--src/ui/README21
-rw-r--r--src/ui/builder-utils.cpp20
-rw-r--r--src/ui/builder-utils.h68
-rw-r--r--src/ui/cache/README3
-rw-r--r--src/ui/cache/svg_preview_cache.cpp173
-rw-r--r--src/ui/cache/svg_preview_cache.h69
-rw-r--r--src/ui/clipboard.cpp2024
-rw-r--r--src/ui/clipboard.h80
-rw-r--r--src/ui/column-menu-builder.h108
-rw-r--r--src/ui/contextmenu.cpp364
-rw-r--r--src/ui/contextmenu.h55
-rw-r--r--src/ui/control-types.h58
-rw-r--r--src/ui/cursor-utils.cpp244
-rw-r--r--src/ui/cursor-utils.h40
-rw-r--r--src/ui/desktop/README27
-rw-r--r--src/ui/desktop/document-check.cpp140
-rw-r--r--src/ui/desktop/document-check.h28
-rw-r--r--src/ui/desktop/menu-icon-shift.cpp109
-rw-r--r--src/ui/desktop/menu-icon-shift.h41
-rw-r--r--src/ui/desktop/menubar.cpp332
-rw-r--r--src/ui/desktop/menubar.h49
-rw-r--r--src/ui/dialog-events.cpp102
-rw-r--r--src/ui/dialog-events.h41
-rw-r--r--src/ui/dialog/README.md46
-rw-r--r--src/ui/dialog/about.cpp206
-rw-r--r--src/ui/dialog/about.h43
-rw-r--r--src/ui/dialog/align-and-distribute.cpp315
-rw-r--r--src/ui/dialog/align-and-distribute.h97
-rw-r--r--src/ui/dialog/arrange-tab.h55
-rw-r--r--src/ui/dialog/attrdialog.cpp848
-rw-r--r--src/ui/dialog/attrdialog.h153
-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.cpp2821
-rw-r--r--src/ui/dialog/clonetiler.h208
-rw-r--r--src/ui/dialog/color-item.cpp506
-rw-r--r--src/ui/dialog/color-item.h127
-rw-r--r--src/ui/dialog/command-palette.cpp1659
-rw-r--r--src/ui/dialog/command-palette.h271
-rw-r--r--src/ui/dialog/debug.cpp259
-rw-r--r--src/ui/dialog/debug.h101
-rw-r--r--src/ui/dialog/dialog-base.cpp318
-rw-r--r--src/ui/dialog/dialog-base.h139
-rw-r--r--src/ui/dialog/dialog-container.cpp1153
-rw-r--r--src/ui/dialog/dialog-container.h130
-rw-r--r--src/ui/dialog/dialog-data.cpp81
-rw-r--r--src/ui/dialog/dialog-data.h54
-rw-r--r--src/ui/dialog/dialog-manager.cpp323
-rw-r--r--src/ui/dialog/dialog-manager.h97
-rw-r--r--src/ui/dialog/dialog-multipaned.cpp1312
-rw-r--r--src/ui/dialog/dialog-multipaned.h203
-rw-r--r--src/ui/dialog/dialog-notebook.cpp1023
-rw-r--r--src/ui/dialog/dialog-notebook.h128
-rw-r--r--src/ui/dialog/dialog-window.cpp281
-rw-r--r--src/ui/dialog/dialog-window.h78
-rw-r--r--src/ui/dialog/document-properties.cpp1863
-rw-r--r--src/ui/dialog/document-properties.h275
-rw-r--r--src/ui/dialog/document-resources.cpp1170
-rw-r--r--src/ui/dialog/document-resources.h105
-rw-r--r--src/ui/dialog/export-batch.cpp830
-rw-r--r--src/ui/dialog/export-batch.h182
-rw-r--r--src/ui/dialog/export-single.cpp1058
-rw-r--r--src/ui/dialog/export-single.h211
-rw-r--r--src/ui/dialog/export.cpp531
-rw-r--r--src/ui/dialog/export.h112
-rw-r--r--src/ui/dialog/filedialog.cpp186
-rw-r--r--src/ui/dialog/filedialog.h200
-rw-r--r--src/ui/dialog/filedialogimpl-gtkmm.cpp676
-rw-r--r--src/ui/dialog/filedialogimpl-gtkmm.h274
-rw-r--r--src/ui/dialog/filedialogimpl-win32.cpp1923
-rw-r--r--src/ui/dialog/filedialogimpl-win32.h371
-rw-r--r--src/ui/dialog/fill-and-stroke.cpp215
-rw-r--r--src/ui/dialog/fill-and-stroke.h92
-rw-r--r--src/ui/dialog/filter-effects-dialog.cpp3376
-rw-r--r--src/ui/dialog/filter-effects-dialog.h355
-rw-r--r--src/ui/dialog/find.cpp1145
-rw-r--r--src/ui/dialog/find.h313
-rw-r--r--src/ui/dialog/font-collections-manager.cpp172
-rw-r--r--src/ui/dialog/font-collections-manager.h90
-rw-r--r--src/ui/dialog/font-substitution.cpp241
-rw-r--r--src/ui/dialog/font-substitution.h39
-rw-r--r--src/ui/dialog/global-palettes.cpp99
-rw-r--r--src/ui/dialog/global-palettes.h66
-rw-r--r--src/ui/dialog/glyphs.cpp777
-rw-r--r--src/ui/dialog/glyphs.h84
-rw-r--r--src/ui/dialog/grid-arrange-tab.cpp660
-rw-r--r--src/ui/dialog/grid-arrange-tab.h152
-rw-r--r--src/ui/dialog/guides.cpp387
-rw-r--r--src/ui/dialog/guides.h106
-rw-r--r--src/ui/dialog/icon-preview.cpp637
-rw-r--r--src/ui/dialog/icon-preview.h109
-rw-r--r--src/ui/dialog/inkscape-preferences.cpp3886
-rw-r--r--src/ui/dialog/inkscape-preferences.h738
-rw-r--r--src/ui/dialog/input.cpp1792
-rw-r--r--src/ui/dialog/input.h46
-rw-r--r--src/ui/dialog/knot-properties.cpp189
-rw-r--r--src/ui/dialog/knot-properties.h96
-rw-r--r--src/ui/dialog/layer-properties.cpp435
-rw-r--r--src/ui/dialog/layer-properties.h158
-rw-r--r--src/ui/dialog/livepatheffect-add.cpp1000
-rw-r--r--src/ui/dialog/livepatheffect-add.h147
-rw-r--r--src/ui/dialog/livepatheffect-editor.cpp1258
-rw-r--r--src/ui/dialog/livepatheffect-editor.h127
-rw-r--r--src/ui/dialog/lpe-fillet-chamfer-properties.cpp255
-rw-r--r--src/ui/dialog/lpe-fillet-chamfer-properties.h110
-rw-r--r--src/ui/dialog/lpe-powerstroke-properties.cpp184
-rw-r--r--src/ui/dialog/lpe-powerstroke-properties.h90
-rw-r--r--src/ui/dialog/memory.cpp212
-rw-r--r--src/ui/dialog/memory.h51
-rw-r--r--src/ui/dialog/messages.cpp214
-rw-r--r--src/ui/dialog/messages.h96
-rw-r--r--src/ui/dialog/new-from-template.cpp90
-rw-r--r--src/ui/dialog/new-from-template.h43
-rw-r--r--src/ui/dialog/object-attributes.cpp760
-rw-r--r--src/ui/dialog/object-attributes.h118
-rw-r--r--src/ui/dialog/object-properties.cpp581
-rw-r--r--src/ui/dialog/object-properties.h143
-rw-r--r--src/ui/dialog/objects.cpp1860
-rw-r--r--src/ui/dialog/objects.h204
-rw-r--r--src/ui/dialog/paint-servers.cpp657
-rw-r--r--src/ui/dialog/paint-servers.h144
-rw-r--r--src/ui/dialog/polar-arrange-tab.cpp409
-rw-r--r--src/ui/dialog/polar-arrange-tab.h104
-rw-r--r--src/ui/dialog/print.cpp308
-rw-r--r--src/ui/dialog/print.h80
-rw-r--r--src/ui/dialog/prototype.cpp101
-rw-r--r--src/ui/dialog/prototype.h67
-rw-r--r--src/ui/dialog/save-template-dialog.cpp86
-rw-r--r--src/ui/dialog/save-template-dialog.h56
-rw-r--r--src/ui/dialog/selectorsdialog.cpp1340
-rw-r--r--src/ui/dialog/selectorsdialog.h194
-rw-r--r--src/ui/dialog/spellcheck.cpp751
-rw-r--r--src/ui/dialog/spellcheck.h281
-rw-r--r--src/ui/dialog/startup.cpp789
-rw-r--r--src/ui/dialog/startup.h87
-rw-r--r--src/ui/dialog/styledialog.cpp1601
-rw-r--r--src/ui/dialog/styledialog.h195
-rw-r--r--src/ui/dialog/svg-fonts-dialog.cpp1794
-rw-r--r--src/ui/dialog/svg-fonts-dialog.h384
-rw-r--r--src/ui/dialog/svg-preview.cpp448
-rw-r--r--src/ui/dialog/svg-preview.h123
-rw-r--r--src/ui/dialog/swatches.cpp447
-rw-r--r--src/ui/dialog/swatches.h114
-rw-r--r--src/ui/dialog/symbols.cpp1361
-rw-r--r--src/ui/dialog/symbols.h192
-rw-r--r--src/ui/dialog/text-edit.cpp647
-rw-r--r--src/ui/dialog/text-edit.h222
-rw-r--r--src/ui/dialog/tile.cpp136
-rw-r--r--src/ui/dialog/tile.h81
-rw-r--r--src/ui/dialog/tracedialog.cpp553
-rw-r--r--src/ui/dialog/tracedialog.h47
-rw-r--r--src/ui/dialog/transformation.cpp1216
-rw-r--r--src/ui/dialog/transformation.h231
-rw-r--r--src/ui/dialog/undo-history.cpp356
-rw-r--r--src/ui/dialog/undo-history.h161
-rw-r--r--src/ui/dialog/xml-tree.cpp928
-rw-r--r--src/ui/dialog/xml-tree.h200
-rw-r--r--src/ui/drag-and-drop.cpp429
-rw-r--r--src/ui/drag-and-drop.h32
-rw-r--r--src/ui/draw-anchor.cpp81
-rw-r--r--src/ui/draw-anchor.h71
-rw-r--r--src/ui/event-debug.h125
-rw-r--r--src/ui/filtered-store.h99
-rw-r--r--src/ui/icon-loader.cpp144
-rw-r--r--src/ui/icon-loader.h29
-rw-r--r--src/ui/icon-names.h32
-rw-r--r--src/ui/interface.cpp189
-rw-r--r--src/ui/interface.h63
-rw-r--r--src/ui/knot/README62
-rw-r--r--src/ui/knot/knot-enums.h49
-rw-r--r--src/ui/knot/knot-holder-entity.cpp633
-rw-r--r--src/ui/knot/knot-holder-entity.h254
-rw-r--r--src/ui/knot/knot-holder.cpp512
-rw-r--r--src/ui/knot/knot-holder.h123
-rw-r--r--src/ui/knot/knot-ptr.cpp34
-rw-r--r--src/ui/knot/knot-ptr.h17
-rw-r--r--src/ui/knot/knot.cpp515
-rw-r--r--src/ui/knot/knot.h208
-rw-r--r--src/ui/modifiers.cpp295
-rw-r--r--src/ui/modifiers.h256
-rw-r--r--src/ui/monitor.cpp68
-rw-r--r--src/ui/monitor.h38
-rw-r--r--src/ui/operation-blocker.h38
-rw-r--r--src/ui/selected-color.cpp155
-rw-r--r--src/ui/selected-color.h100
-rw-r--r--src/ui/shape-editor-knotholders.cpp2611
-rw-r--r--src/ui/shape-editor.cpp210
-rw-r--r--src/ui/shape-editor.h78
-rw-r--r--src/ui/shortcuts.cpp1024
-rw-r--r--src/ui/shortcuts.h140
-rw-r--r--src/ui/simple-pref-pusher.cpp49
-rw-r--r--src/ui/simple-pref-pusher.h65
-rw-r--r--src/ui/svg-renderer.cpp120
-rw-r--r--src/ui/svg-renderer.h54
-rw-r--r--src/ui/syntax.cpp399
-rw-r--r--src/ui/syntax.h134
-rw-r--r--src/ui/themes.cpp687
-rw-r--r--src/ui/themes.h107
-rw-r--r--src/ui/tool-factory.cpp113
-rw-r--r--src/ui/tool-factory.h42
-rw-r--r--src/ui/tool/README29
-rw-r--r--src/ui/tool/commit-events.h52
-rw-r--r--src/ui/tool/control-point-selection.cpp784
-rw-r--r--src/ui/tool/control-point-selection.h179
-rw-r--r--src/ui/tool/control-point.cpp587
-rw-r--r--src/ui/tool/control-point.h413
-rw-r--r--src/ui/tool/curve-drag-point.cpp247
-rw-r--r--src/ui/tool/curve-drag-point.h77
-rw-r--r--src/ui/tool/event-utils.cpp93
-rw-r--r--src/ui/tool/event-utils.h129
-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.cpp907
-rw-r--r--src/ui/tool/multi-path-manipulator.h159
-rw-r--r--src/ui/tool/node-types.h57
-rw-r--r--src/ui/tool/node.cpp1915
-rw-r--r--src/ui/tool/node.h513
-rw-r--r--src/ui/tool/path-manipulator.cpp1847
-rw-r--r--src/ui/tool/path-manipulator.h195
-rw-r--r--src/ui/tool/selectable-control-point.cpp150
-rw-r--r--src/ui/tool/selectable-control-point.h80
-rw-r--r--src/ui/tool/shape-record.h65
-rw-r--r--src/ui/tool/transform-handle-set.cpp827
-rw-r--r--src/ui/tool/transform-handle-set.h147
-rw-r--r--src/ui/toolbar/arc-toolbar.cpp542
-rw-r--r--src/ui/toolbar/arc-toolbar.h120
-rw-r--r--src/ui/toolbar/booleans-toolbar.cpp53
-rw-r--r--src/ui/toolbar/booleans-toolbar.h43
-rw-r--r--src/ui/toolbar/box3d-toolbar.cpp408
-rw-r--r--src/ui/toolbar/box3d-toolbar.h110
-rw-r--r--src/ui/toolbar/calligraphy-toolbar.cpp625
-rw-r--r--src/ui/toolbar/calligraphy-toolbar.h105
-rw-r--r--src/ui/toolbar/connector-toolbar.cpp412
-rw-r--r--src/ui/toolbar/connector-toolbar.h102
-rw-r--r--src/ui/toolbar/dropper-toolbar.cpp117
-rw-r--r--src/ui/toolbar/dropper-toolbar.h70
-rw-r--r--src/ui/toolbar/eraser-toolbar.cpp352
-rw-r--r--src/ui/toolbar/eraser-toolbar.h100
-rw-r--r--src/ui/toolbar/gradient-toolbar.cpp1189
-rw-r--r--src/ui/toolbar/gradient-toolbar.h106
-rw-r--r--src/ui/toolbar/lpe-toolbar.cpp417
-rw-r--r--src/ui/toolbar/lpe-toolbar.h101
-rw-r--r--src/ui/toolbar/marker-toolbar.cpp34
-rw-r--r--src/ui/toolbar/marker-toolbar.h31
-rw-r--r--src/ui/toolbar/measure-toolbar.cpp448
-rw-r--r--src/ui/toolbar/measure-toolbar.h91
-rw-r--r--src/ui/toolbar/mesh-toolbar.cpp613
-rw-r--r--src/ui/toolbar/mesh-toolbar.h97
-rw-r--r--src/ui/toolbar/node-toolbar.cpp691
-rw-r--r--src/ui/toolbar/node-toolbar.h116
-rw-r--r--src/ui/toolbar/page-toolbar.cpp530
-rw-r--r--src/ui/toolbar/page-toolbar.h118
-rw-r--r--src/ui/toolbar/paintbucket-toolbar.cpp220
-rw-r--r--src/ui/toolbar/paintbucket-toolbar.h72
-rw-r--r--src/ui/toolbar/pencil-toolbar.cpp691
-rw-r--r--src/ui/toolbar/pencil-toolbar.h111
-rw-r--r--src/ui/toolbar/rect-toolbar.cpp383
-rw-r--r--src/ui/toolbar/rect-toolbar.h116
-rw-r--r--src/ui/toolbar/select-toolbar.cpp654
-rw-r--r--src/ui/toolbar/select-toolbar.h93
-rw-r--r--src/ui/toolbar/spiral-toolbar.cpp277
-rw-r--r--src/ui/toolbar/spiral-toolbar.h101
-rw-r--r--src/ui/toolbar/spray-toolbar.cpp541
-rw-r--r--src/ui/toolbar/spray-toolbar.h107
-rw-r--r--src/ui/toolbar/star-toolbar.cpp553
-rw-r--r--src/ui/toolbar/star-toolbar.h111
-rw-r--r--src/ui/toolbar/text-toolbar.cpp2647
-rw-r--r--src/ui/toolbar/text-toolbar.h158
-rw-r--r--src/ui/toolbar/toolbar.cpp84
-rw-r--r--src/ui/toolbar/toolbar.h66
-rw-r--r--src/ui/toolbar/tweak-toolbar.cpp346
-rw-r--r--src/ui/toolbar/tweak-toolbar.h89
-rw-r--r--src/ui/toolbar/zoom-toolbar.cpp67
-rw-r--r--src/ui/toolbar/zoom-toolbar.h62
-rw-r--r--src/ui/tools/arc-tool.cpp454
-rw-r--r--src/ui/tools/arc-tool.h76
-rw-r--r--src/ui/tools/booleans-builder.cpp271
-rw-r--r--src/ui/tools/booleans-builder.h90
-rw-r--r--src/ui/tools/booleans-subitems.cpp356
-rw-r--r--src/ui/tools/booleans-subitems.h71
-rw-r--r--src/ui/tools/booleans-tool.cpp255
-rw-r--r--src/ui/tools/booleans-tool.h70
-rw-r--r--src/ui/tools/box3d-tool.cpp570
-rw-r--r--src/ui/tools/box3d-tool.h100
-rw-r--r--src/ui/tools/calligraphic-tool.cpp1162
-rw-r--r--src/ui/tools/calligraphic-tool.h101
-rw-r--r--src/ui/tools/connector-tool.cpp1324
-rw-r--r--src/ui/tools/connector-tool.h191
-rw-r--r--src/ui/tools/dropper-tool.cpp394
-rw-r--r--src/ui/tools/dropper-tool.h94
-rw-r--r--src/ui/tools/dynamic-base.cpp141
-rw-r--r--src/ui/tools/dynamic-base.h136
-rw-r--r--src/ui/tools/eraser-tool.cpp1413
-rw-r--r--src/ui/tools/eraser-tool.h155
-rw-r--r--src/ui/tools/flood-tool.cpp1230
-rw-r--r--src/ui/tools/flood-tool.h67
-rw-r--r--src/ui/tools/freehand-base.cpp1007
-rw-r--r--src/ui/tools/freehand-base.h161
-rw-r--r--src/ui/tools/gradient-tool.cpp822
-rw-r--r--src/ui/tools/gradient-tool.h77
-rw-r--r--src/ui/tools/lpe-tool.cpp460
-rw-r--r--src/ui/tools/lpe-tool.h98
-rw-r--r--src/ui/tools/marker-tool.cpp302
-rw-r--r--src/ui/tools/marker-tool.h50
-rw-r--r--src/ui/tools/measure-tool.cpp1445
-rw-r--r--src/ui/tools/measure-tool.h127
-rw-r--r--src/ui/tools/mesh-tool.cpp970
-rw-r--r--src/ui/tools/mesh-tool.h86
-rw-r--r--src/ui/tools/node-tool.cpp861
-rw-r--r--src/ui/tools/node-tool.h115
-rw-r--r--src/ui/tools/pages-tool.cpp668
-rw-r--r--src/ui/tools/pages-tool.h99
-rw-r--r--src/ui/tools/pen-tool.cpp2043
-rw-r--r--src/ui/tools/pen-tool.h175
-rw-r--r--src/ui/tools/pencil-tool.cpp1177
-rw-r--r--src/ui/tools/pencil-tool.h101
-rw-r--r--src/ui/tools/rect-tool.cpp464
-rw-r--r--src/ui/tools/rect-tool.h60
-rw-r--r--src/ui/tools/select-tool.cpp1148
-rw-r--r--src/ui/tools/select-tool.h81
-rw-r--r--src/ui/tools/spiral-tool.cpp409
-rw-r--r--src/ui/tools/spiral-tool.h61
-rw-r--r--src/ui/tools/spray-tool.cpp1528
-rw-r--r--src/ui/tools/spray-tool.h148
-rw-r--r--src/ui/tools/star-tool.cpp428
-rw-r--r--src/ui/tools/star-tool.h72
-rw-r--r--src/ui/tools/text-tool.cpp1905
-rw-r--r--src/ui/tools/text-tool.h122
-rw-r--r--src/ui/tools/tool-base.cpp1712
-rw-r--r--src/ui/tools/tool-base.h262
-rw-r--r--src/ui/tools/tweak-tool.cpp1482
-rw-r--r--src/ui/tools/tweak-tool.h107
-rw-r--r--src/ui/tools/zoom-tool.cpp214
-rw-r--r--src/ui/tools/zoom-tool.h41
-rw-r--r--src/ui/util.cpp291
-rw-r--r--src/ui/util.h126
-rw-r--r--src/ui/view/README51
-rw-r--r--src/ui/view/svg-view-widget.cpp259
-rw-r--r--src/ui/view/svg-view-widget.h97
-rw-r--r--src/ui/view/view-widget.cpp58
-rw-r--r--src/ui/view/view-widget.h56
-rw-r--r--src/ui/view/view.cpp95
-rw-r--r--src/ui/view/view.h141
-rw-r--r--src/ui/widget/alignment-selector.cpp80
-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/canvas-grid.cpp419
-rw-r--r--src/ui/widget/canvas-grid.h131
-rw-r--r--src/ui/widget/canvas-notice.cpp52
-rw-r--r--src/ui/widget/canvas-notice.h39
-rw-r--r--src/ui/widget/canvas.cpp2426
-rw-r--r--src/ui/widget/canvas.h224
-rw-r--r--src/ui/widget/canvas/cairographics.cpp423
-rw-r--r--src/ui/widget/canvas/cairographics.h80
-rw-r--r--src/ui/widget/canvas/fragment.h33
-rw-r--r--src/ui/widget/canvas/framecheck.cpp24
-rw-r--r--src/ui/widget/canvas/framecheck.h58
-rw-r--r--src/ui/widget/canvas/glgraphics.cpp873
-rw-r--r--src/ui/widget/canvas/glgraphics.h144
-rw-r--r--src/ui/widget/canvas/graphics.cpp166
-rw-r--r--src/ui/widget/canvas/graphics.h91
-rw-r--r--src/ui/widget/canvas/pixelstreamer.cpp501
-rw-r--r--src/ui/widget/canvas/pixelstreamer.h75
-rw-r--r--src/ui/widget/canvas/prefs.h102
-rw-r--r--src/ui/widget/canvas/stores.cpp371
-rw-r--r--src/ui/widget/canvas/stores.h106
-rw-r--r--src/ui/widget/canvas/synchronizer.cpp103
-rw-r--r--src/ui/widget/canvas/synchronizer.h72
-rw-r--r--src/ui/widget/canvas/texture.cpp66
-rw-r--r--src/ui/widget/canvas/texture.h62
-rw-r--r--src/ui/widget/canvas/texturecache.cpp115
-rw-r--r--src/ui/widget/canvas/texturecache.h52
-rw-r--r--src/ui/widget/canvas/updaters.cpp235
-rw-r--r--src/ui/widget/canvas/updaters.h78
-rw-r--r--src/ui/widget/canvas/util.cpp70
-rw-r--r--src/ui/widget/canvas/util.h75
-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.cpp987
-rw-r--r--src/ui/widget/color-icc-selector.h75
-rw-r--r--src/ui/widget/color-notebook.cpp382
-rw-r--r--src/ui/widget/color-notebook.h108
-rw-r--r--src/ui/widget/color-palette.cpp724
-rw-r--r--src/ui/widget/color-palette.h130
-rw-r--r--src/ui/widget/color-picker.cpp173
-rw-r--r--src/ui/widget/color-picker.h119
-rw-r--r--src/ui/widget/color-preview.cpp172
-rw-r--r--src/ui/widget/color-preview.h57
-rw-r--r--src/ui/widget/color-scales.cpp1247
-rw-r--r--src/ui/widget/color-scales.h141
-rw-r--r--src/ui/widget/color-slider.cpp546
-rw-r--r--src/ui/widget/color-slider.h92
-rw-r--r--src/ui/widget/combo-box-entry-tool-item.cpp725
-rw-r--r--src/ui/widget/combo-box-entry-tool-item.h153
-rw-r--r--src/ui/widget/combo-enums.h232
-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/completion-popup.cpp108
-rw-r--r--src/ui/widget/completion-popup.h51
-rw-r--r--src/ui/widget/custom-tooltip.cpp61
-rw-r--r--src/ui/widget/custom-tooltip.h22
-rw-r--r--src/ui/widget/dash-selector.cpp260
-rw-r--r--src/ui/widget/dash-selector.h116
-rw-r--r--src/ui/widget/entity-entry.cpp220
-rw-r--r--src/ui/widget/entity-entry.h89
-rw-r--r--src/ui/widget/entry.cpp30
-rw-r--r--src/ui/widget/entry.h45
-rw-r--r--src/ui/widget/export-lists.cpp321
-rw-r--r--src/ui/widget/export-lists.h114
-rw-r--r--src/ui/widget/export-preview.cpp208
-rw-r--r--src/ui/widget/export-preview.h99
-rw-r--r--src/ui/widget/fill-style.cpp738
-rw-r--r--src/ui/widget/fill-style.h85
-rw-r--r--src/ui/widget/filter-effect-chooser.cpp211
-rw-r--r--src/ui/widget/filter-effect-chooser.h97
-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-collection-selector.cpp674
-rw-r--r--src/ui/widget/font-collection-selector.h139
-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.cpp562
-rw-r--r--src/ui/widget/font-selector.h177
-rw-r--r--src/ui/widget/font-variants.cpp1461
-rw-r--r--src/ui/widget/font-variants.h223
-rw-r--r--src/ui/widget/font-variations.cpp182
-rw-r--r--src/ui/widget/font-variations.h128
-rw-r--r--src/ui/widget/frame.cpp80
-rw-r--r--src/ui/widget/frame.h75
-rw-r--r--src/ui/widget/gradient-editor.cpp653
-rw-r--r--src/ui/widget/gradient-editor.h114
-rw-r--r--src/ui/widget/gradient-image.cpp247
-rw-r--r--src/ui/widget/gradient-image.h76
-rw-r--r--src/ui/widget/gradient-selector-interface.h32
-rw-r--r--src/ui/widget/gradient-selector.cpp611
-rw-r--r--src/ui/widget/gradient-selector.h160
-rw-r--r--src/ui/widget/gradient-vector-selector.cpp326
-rw-r--r--src/ui/widget/gradient-vector-selector.h97
-rw-r--r--src/ui/widget/gradient-with-stops.cpp552
-rw-r--r--src/ui/widget/gradient-with-stops.h117
-rw-r--r--src/ui/widget/icon-combobox.h101
-rw-r--r--src/ui/widget/iconrenderer.cpp119
-rw-r--r--src/ui/widget/iconrenderer.h84
-rw-r--r--src/ui/widget/image-properties.cpp295
-rw-r--r--src/ui/widget/image-properties.h47
-rw-r--r--src/ui/widget/imagetoggler.cpp148
-rw-r--r--src/ui/widget/imagetoggler.h97
-rw-r--r--src/ui/widget/ink-color-wheel.cpp1356
-rw-r--r--src/ui/widget/ink-color-wheel.h167
-rw-r--r--src/ui/widget/ink-ruler.cpp645
-rw-r--r--src/ui/widget/ink-ruler.h112
-rw-r--r--src/ui/widget/ink-spinscale.cpp288
-rw-r--r--src/ui/widget/ink-spinscale.h100
-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.cpp106
-rw-r--r--src/ui/widget/labelled.h88
-rw-r--r--src/ui/widget/layer-selector.cpp219
-rw-r--r--src/ui/widget/layer-selector.h82
-rw-r--r--src/ui/widget/licensor.cpp157
-rw-r--r--src/ui/widget/licensor.h57
-rw-r--r--src/ui/widget/marker-combo-box.cpp814
-rw-r--r--src/ui/widget/marker-combo-box.h175
-rw-r--r--src/ui/widget/notebook-page.cpp48
-rw-r--r--src/ui/widget/notebook-page.h57
-rw-r--r--src/ui/widget/object-composite-settings.cpp302
-rw-r--r--src/ui/widget/object-composite-settings.h78
-rw-r--r--src/ui/widget/objects-dialog-cells.cpp90
-rw-r--r--src/ui/widget/objects-dialog-cells.h75
-rw-r--r--src/ui/widget/oklab-color-wheel.cpp309
-rw-r--r--src/ui/widget/oklab-color-wheel.h83
-rw-r--r--src/ui/widget/optglarea.cpp127
-rw-r--r--src/ui/widget/optglarea.h80
-rw-r--r--src/ui/widget/page-properties.cpp525
-rw-r--r--src/ui/widget/page-properties.h59
-rw-r--r--src/ui/widget/page-selector.cpp198
-rw-r--r--src/ui/widget/page-selector.h91
-rw-r--r--src/ui/widget/page-size-preview.cpp186
-rw-r--r--src/ui/widget/page-size-preview.h50
-rw-r--r--src/ui/widget/paint-selector.cpp1267
-rw-r--r--src/ui/widget/paint-selector.h236
-rw-r--r--src/ui/widget/pattern-editor.cpp685
-rw-r--r--src/ui/widget/pattern-editor.h134
-rw-r--r--src/ui/widget/pattern-store.h61
-rw-r--r--src/ui/widget/point.cpp186
-rw-r--r--src/ui/widget/point.h196
-rw-r--r--src/ui/widget/preferences-widget.cpp1117
-rw-r--r--src/ui/widget/preferences-widget.h357
-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.cpp830
-rw-r--r--src/ui/widget/registered-widget.h452
-rw-r--r--src/ui/widget/registry.cpp58
-rw-r--r--src/ui/widget/registry.h51
-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.cpp271
-rw-r--r--src/ui/widget/scalar-unit.h196
-rw-r--r--src/ui/widget/scalar.cpp212
-rw-r--r--src/ui/widget/scalar.h202
-rw-r--r--src/ui/widget/scroll-utils.cpp53
-rw-r--r--src/ui/widget/scroll-utils.h32
-rw-r--r--src/ui/widget/scrollprotected.h117
-rw-r--r--src/ui/widget/selected-style.cpp1416
-rw-r--r--src/ui/widget/selected-style.h302
-rw-r--r--src/ui/widget/shapeicon.cpp148
-rw-r--r--src/ui/widget/shapeicon.h115
-rw-r--r--src/ui/widget/spin-button-tool-item.cpp607
-rw-r--r--src/ui/widget/spin-button-tool-item.h130
-rw-r--r--src/ui/widget/spin-scale.cpp247
-rw-r--r--src/ui/widget/spin-scale.h116
-rw-r--r--src/ui/widget/spinbutton.cpp144
-rw-r--r--src/ui/widget/spinbutton.h124
-rw-r--r--src/ui/widget/stroke-style.cpp1223
-rw-r--r--src/ui/widget/stroke-style.h213
-rw-r--r--src/ui/widget/style-subject.cpp113
-rw-r--r--src/ui/widget/style-subject.h114
-rw-r--r--src/ui/widget/style-swatch.cpp405
-rw-r--r--src/ui/widget/style-swatch.h107
-rw-r--r--src/ui/widget/swatch-selector.cpp101
-rw-r--r--src/ui/widget/swatch-selector.h58
-rw-r--r--src/ui/widget/template-list.cpp224
-rw-r--r--src/ui/widget/template-list.h62
-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.h88
-rw-r--r--src/ui/widget/unit-menu.cpp152
-rw-r--r--src/ui/widget/unit-menu.h154
-rw-r--r--src/ui/widget/unit-tracker.cpp315
-rw-r--r--src/ui/widget/unit-tracker.h89
538 files changed, 182293 insertions, 0 deletions
diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt
new file mode 100644
index 0000000..fc18109
--- /dev/null
+++ b/src/ui/CMakeLists.txt
@@ -0,0 +1,561 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+set(ui_SRC
+ builder-utils.cpp
+ clipboard.cpp
+ contextmenu.cpp
+ cursor-utils.cpp
+ dialog-events.cpp
+ draw-anchor.cpp
+ drag-and-drop.cpp
+ icon-loader.cpp
+ interface.cpp
+ monitor.cpp
+ selected-color.cpp
+ shape-editor.cpp
+ shape-editor-knotholders.cpp
+ simple-pref-pusher.cpp
+ shortcuts.cpp
+ svg-renderer.cpp
+ syntax.cpp
+ themes.cpp
+ tool-factory.cpp
+ util.cpp
+ modifiers.cpp
+
+ cache/svg_preview_cache.cpp
+
+ desktop/document-check.cpp
+ desktop/menubar.cpp
+ desktop/menu-icon-shift.cpp
+
+ knot/knot.cpp
+ knot/knot-holder.cpp
+ knot/knot-holder-entity.cpp
+ knot/knot-ptr.cpp
+
+ tool/control-point-selection.cpp
+ tool/control-point.cpp
+ tool/curve-drag-point.cpp
+ tool/event-utils.cpp
+ tool/modifier-tracker.cpp
+ tool/multi-path-manipulator.cpp
+ tool/node.cpp
+ tool/path-manipulator.cpp
+ tool/selectable-control-point.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/marker-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/page-toolbar.cpp
+ toolbar/paintbucket-toolbar.cpp
+ toolbar/pencil-toolbar.cpp
+ toolbar/rect-toolbar.cpp
+ toolbar/select-toolbar.cpp
+ toolbar/booleans-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/pages-tool.cpp
+ tools/pencil-tool.cpp
+ tools/pen-tool.cpp
+ tools/rect-tool.cpp
+ tools/marker-tool.cpp
+ tools/select-tool.cpp
+ tools/booleans-builder.cpp
+ tools/booleans-tool.cpp
+ tools/booleans-subitems.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/about.cpp
+ dialog/align-and-distribute.cpp
+ dialog/calligraphic-profile-rename.cpp
+ dialog/clonetiler.cpp
+ dialog/color-item.cpp
+ dialog/command-palette.cpp
+ dialog/attrdialog.cpp
+ dialog/debug.cpp
+ dialog/dialog-base.cpp
+ dialog/dialog-container.cpp
+ dialog/dialog-data.cpp
+ dialog/dialog-manager.cpp
+ dialog/dialog-multipaned.cpp
+ dialog/dialog-notebook.cpp
+ dialog/dialog-window.cpp
+ dialog/document-properties.cpp
+ dialog/document-resources.cpp
+ dialog/export.cpp
+ dialog/export-batch.cpp
+ dialog/export-single.cpp
+ dialog/filedialog.cpp
+ dialog/filedialogimpl-gtkmm.cpp
+ dialog/fill-and-stroke.cpp
+ dialog/filter-effects-dialog.cpp
+ dialog/find.cpp
+ dialog/font-collections-manager.cpp
+ widget/font-collection-selector.cpp
+ dialog/font-substitution.cpp
+ dialog/global-palettes.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/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.cpp
+ dialog/prototype.cpp
+ dialog/selectorsdialog.cpp
+ dialog/startup.cpp
+ dialog/styledialog.cpp
+ dialog/svg-fonts-dialog.cpp
+ dialog/svg-preview.cpp
+ dialog/swatches.cpp
+ dialog/symbols.cpp
+ dialog/paint-servers.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/completion-popup.cpp
+ widget/canvas.cpp
+ widget/canvas/stores.cpp
+ widget/canvas/synchronizer.cpp
+ widget/canvas/util.cpp
+ widget/canvas/texture.cpp
+ widget/canvas/texturecache.cpp
+ widget/canvas/pixelstreamer.cpp
+ widget/canvas/updaters.cpp
+ widget/canvas/framecheck.cpp
+ widget/canvas/glgraphics.cpp
+ widget/canvas/cairographics.cpp
+ widget/canvas/graphics.cpp
+ widget/canvas-grid.cpp
+ widget/canvas-notice.cpp
+ widget/color-entry.cpp
+ widget/color-icc-selector.cpp
+ widget/color-notebook.cpp
+ widget/color-palette.cpp
+ widget/color-picker.cpp
+ widget/color-preview.cpp
+ icon-loader.cpp
+ widget/color-scales.cpp
+ widget/color-slider.cpp
+ widget/combo-box-entry-tool-item.cpp
+ widget/combo-tool-item.cpp
+ widget/custom-tooltip.cpp
+ widget/dash-selector.cpp
+ widget/entity-entry.cpp
+ widget/entry.cpp
+ widget/export-lists.cpp
+ widget/export-preview.cpp
+ widget/filter-effect-chooser.cpp
+ widget/fill-style.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/gradient-image.cpp
+ widget/gradient-editor.cpp
+ widget/gradient-selector.cpp
+ widget/gradient-vector-selector.cpp
+ widget/gradient-with-stops.cpp
+ widget/image-properties.cpp
+ widget/imagetoggler.cpp
+ widget/ink-color-wheel.cpp
+ widget/ink-ruler.cpp
+ widget/ink-spinscale.cpp
+ widget/label-tool-item.cpp
+ widget/labelled.cpp
+ widget/layer-selector.cpp
+ widget/licensor.cpp
+ widget/marker-combo-box.cpp
+ widget/notebook-page.cpp
+ widget/object-composite-settings.cpp
+ widget/objects-dialog-cells.cpp
+ widget/oklab-color-wheel.cpp
+ widget/optglarea.cpp
+ widget/page-properties.cpp
+ widget/page-size-preview.cpp
+ widget/page-selector.cpp
+ widget/paint-selector.cpp
+ widget/pattern-editor.cpp
+ widget/point.cpp
+ widget/preferences-widget.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/scroll-utils.cpp
+ widget/selected-style.cpp
+ widget/shapeicon.cpp
+ widget/spin-button-tool-item.cpp
+ widget/spin-scale.cpp
+ widget/spinbutton.cpp
+ widget/stroke-style.cpp
+ widget/style-subject.cpp
+ widget/style-swatch.cpp
+ widget/swatch-selector.cpp
+ widget/template-list.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
+ builder-utils.h
+ clipboard.h
+ contextmenu.h
+ cursor-utils.h
+ control-types.h
+ dialog-events.h
+ drag-and-drop.h
+ draw-anchor.h
+ event-debug.h
+ filtered-store.h
+ icon-names.h
+ icon-loader.h
+ interface.h
+ monitor.h
+ selected-color.h
+ shape-editor.h
+ simple-pref-pusher.h
+ shortcuts.h
+ syntax.h
+ themes.h
+ tool-factory.h
+ util.h
+ modifiers.h
+
+ cache/svg_preview_cache.h
+
+ desktop/document-check.h
+ desktop/menubar.h
+ desktop/menu-icon-shift.h
+
+ dialog/about.h
+ dialog/align-and-distribute.h
+ dialog/arrange-tab.h
+ dialog/calligraphic-profile-rename.h
+ dialog/clonetiler.h
+ dialog/color-item.h
+ dialog/command-palette.h
+ dialog/attrdialog.h
+ dialog/debug.h
+ dialog/dialog-base.h
+ dialog/dialog-container.h
+ dialog/dialog-data.h
+ dialog/dialog-manager.h
+ dialog/dialog-multipaned.h
+ dialog/dialog-notebook.h
+ dialog/dialog-window.h
+ dialog/document-properties.h
+ dialog/export.h
+ dialog/export-batch.h
+ dialog/export-single.h
+ dialog/filedialog.h
+ dialog/filedialogimpl-gtkmm.h
+ dialog/filedialogimpl-win32.h
+ dialog/fill-and-stroke.h
+ dialog/filter-effects-dialog.h
+ dialog/find.h
+ dialog/font-collections-manager.h
+ dialog/font-substitution.h
+ dialog/global-palettes.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/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/polar-arrange-tab.h
+ dialog/print.h
+ dialog/prototype.h
+ dialog/selectorsdialog.h
+ dialog/startup.h
+ dialog/styledialog.h
+ dialog/svg-fonts-dialog.h
+ dialog/svg-preview.h
+ dialog/swatches.h
+ dialog/symbols.h
+ dialog/paint-servers.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
+
+ knot/knot.h
+ knot/knot-enums.h
+ knot/knot-holder.h
+ knot/knot-holder-entity.h
+ knot/knot-ptr.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/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/marker-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/page-toolbar.h
+ toolbar/paintbucket-toolbar.h
+ toolbar/pencil-toolbar.h
+ toolbar/rect-toolbar.h
+ toolbar/select-toolbar.h
+ toolbar/booleans-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/pages-tool.h
+ tools/pen-tool.h
+ tools/pencil-tool.h
+ tools/rect-tool.h
+ tools/marker-tool.h
+ tools/select-tool.h
+ tools/booleans-builder.h
+ tools/booleans-tool.h
+ tools/booleans-subitems.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/canvas.h
+ widget/canvas/fragment.h
+ widget/canvas/prefs.h
+ widget/canvas/stores.h
+ widget/canvas/synchronizer.h
+ widget/canvas/util.h
+ widget/canvas/texture.h
+ widget/canvas/texturecache.h
+ widget/canvas/graphics.h
+ widget/canvas/pixelstreamer.h
+ widget/canvas/updaters.h
+ widget/canvas/framecheck.h
+ widget/canvas/glgraphics.h
+ widget/canvas/cairographics.h
+ widget/canvas-grid.h
+ widget/canvas-notice.h
+ widget/completion-popup.h
+ widget/color-entry.h
+ widget/color-icc-selector.h
+ widget/color-notebook.h
+ widget/color-palette.h
+ widget/color-picker.h
+ widget/color-preview.h
+ widget/color-scales.h
+ widget/color-slider.h
+ widget/combo-enums.h
+ widget/combo-box-entry-tool-item.h
+ widget/combo-tool-item.h
+ widget/custom-tooltip.h
+ widget/dash-selector.h
+ widget/entity-entry.h
+ widget/entry.h
+ widget/export-lists.h
+ widget/export-preview.h
+ widget/fill-style.h
+ widget/filter-effect-chooser.h
+ widget/font-button.h
+ widget/font-collection-selector.h
+ widget/font-selector.h
+ widget/font-selector-toolbar.h
+ widget/font-variants.h
+ widget/font-variations.h
+ widget/frame.h
+ widget/gradient-image.h
+ widget/gradient-editor.h
+ widget/gradient-selector.h
+ widget/gradient-vector-selector.h
+ widget/gradient-with-stops.h
+ widget/image-properties.h
+ widget/imagetoggler.h
+ widget/ink-color-wheel.h
+ widget/ink-ruler.h
+ widget/ink-spinscale.h
+ widget/label-tool-item.h
+ widget/labelled.h
+ widget/layer-selector.h
+ widget/licensor.h
+ widget/marker-combo-box.h
+ widget/notebook-page.h
+ widget/object-composite-settings.h
+ widget/objects-dialog-cells.h
+ widget/oklab-color-wheel.h
+ widget/optglarea.h
+ widget/page-selector.h
+ widget/paint-selector.h
+ widget/pattern-editor.h
+ widget/point.h
+ widget/preferences-widget.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/scroll-utils.h
+ widget/scrollprotected.h
+ widget/selected-style.h
+ widget/shapeicon.h
+ widget/spin-button-tool-item.h
+ widget/spin-scale.h
+ widget/spinbutton.h
+ widget/stroke-style.h
+ widget/style-subject.h
+ widget/style-swatch.h
+ widget/swatch-selector.h
+ widget/template-list.h
+ widget/text.h
+ widget/tolerance-slider.h
+ widget/unit-menu.h
+ widget/unit-tracker.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 ("${WITH_GSPELL}")
+ 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/builder-utils.cpp b/src/ui/builder-utils.cpp
new file mode 100644
index 0000000..b555ea2
--- /dev/null
+++ b/src/ui/builder-utils.cpp
@@ -0,0 +1,20 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include "builder-utils.h"
+#include "io/resource.h"
+
+namespace Inkscape {
+namespace UI {
+
+Glib::RefPtr<Gtk::Builder> create_builder(const char* filename) {
+ auto glade = Inkscape::IO::Resource::get_filename(Inkscape::IO::Resource::UIS, filename);
+ Glib::RefPtr<Gtk::Builder> builder;
+ try {
+ return Gtk::Builder::create_from_file(glade);
+ }
+ catch (Glib::Error& ex) {
+ g_error("Cannot load glade file: %s", ex.what().c_str());
+ throw;
+ }
+}
+
+}} // namespace
diff --git a/src/ui/builder-utils.h b/src/ui/builder-utils.h
new file mode 100644
index 0000000..83fd059
--- /dev/null
+++ b/src/ui/builder-utils.h
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Gtk builder utilities
+ */
+/* Authors:
+ * Michael Kowalski
+ *
+ * Copyright (C) 2021 Michael Kowalski
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_BUILDER_UTILS_H
+#define SEEN_BUILDER_UTILS_H
+
+#include <glibmm/refptr.h>
+#include <gtkmm/builder.h>
+
+namespace Inkscape {
+namespace UI {
+
+// get widget from builder or throw
+template<class W> W& get_widget(const Glib::RefPtr<Gtk::Builder>& builder, const char* id) {
+ W* widget;
+ builder->get_widget(id, widget);
+ if (!widget) {
+ throw std::runtime_error("Missing widget in a glade resource file");
+ }
+ return *widget;
+}
+
+template<class W, typename... Args>
+W& get_derived_widget(const Glib::RefPtr<Gtk::Builder>& builder, const char* id, Args&&... args) {
+ W* widget;
+ builder->get_widget_derived(id, widget, std::forward<Args>(args)...);
+ if (!widget) {
+ throw std::runtime_error("Missing widget in a glade resource file");
+ }
+ return *widget;
+}
+
+template<class Ob> Glib::RefPtr<Ob> get_object(Glib::RefPtr<Gtk::Builder>& builder, const char* id) {
+ auto object = Glib::RefPtr<Ob>::cast_dynamic(builder->get_object(id));
+ if (!object) {
+ throw std::runtime_error("Missing object in a glade resource file");
+ }
+ return object;
+}
+
+/**
+ * This version of get_object is needed for Gtk::CellRenderer objects which can not be
+ * put into Glib::RefPtr by the compiler, but are somehow passed to us as RefPtrs anyway.
+ */
+template <class Ob>
+Ob &get_object_raw(Glib::RefPtr<Gtk::Builder> &builder, const char *id)
+{
+ auto object = dynamic_cast<Ob *>(builder->get_object(id).get());
+ if (!object) {
+ throw std::runtime_error("Missing object in a glade resource file");
+ }
+ return *object;
+}
+
+// load glade file from share/ui folder and return builder; throws on errors
+Glib::RefPtr<Gtk::Builder> create_builder(const char* filename);
+
+} } // namespace
+
+#endif // SEEN_BUILDER_UTILS_H
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..10bf3cf
--- /dev/null
+++ b/src/ui/cache/svg_preview_cache.cpp
@@ -0,0 +1,173 @@
+// 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"
+
+cairo_surface_t* render_surface(Inkscape::Drawing &drawing, double scale_factor, Geom::Rect const &dbox,
+ Geom::IntPoint pixsize, double device_scale, const guint32* checkerboard_color, bool no_clip)
+{
+ scale_factor *= device_scale;
+ pixsize.x() *= device_scale;
+ pixsize.y() *= device_scale;
+
+ Geom::IntRect ibox = (dbox * Geom::Scale(scale_factor)).roundOutwards();
+
+ if (no_clip) {
+ // check if object fits in the surface
+ if (ibox.width() > pixsize.x() || ibox.height() > pixsize.y()) {
+ auto sx = static_cast<double>(ibox.width()) / pixsize.x();
+ auto sy = static_cast<double>(ibox.height()) / pixsize.y();
+ // reduce scale
+ scale_factor /= std::max(sx, sy);
+ ibox = (dbox * Geom::Scale(scale_factor)).roundOutwards();
+ }
+ }
+
+ drawing.root()->setTransform(Geom::Scale(scale_factor));
+
+ drawing.update(ibox);
+
+ /* Find visible area */
+ int width = ibox.width();
+ int height = ibox.height();
+ int dx = pixsize.x();
+ int dy = pixsize.y();
+ 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), pixsize);
+
+ /* Render */
+ cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, pixsize.x(), pixsize.y());
+ Inkscape::DrawingContext dc(s, area.min());
+
+ if (checkerboard_color) {
+ guint rgba = *checkerboard_color;
+ auto pattern = ink_cairo_pattern_create_checkerboard(rgba);
+ dc.save();
+ dc.transform(Geom::Scale(device_scale));
+ dc.setOperator(CAIRO_OPERATOR_SOURCE);
+ dc.setSource(pattern);
+ dc.paint();
+ dc.restore();
+ cairo_pattern_destroy(pattern);
+ }
+
+ drawing.render(dc, area, Inkscape::DrawingItem::RENDER_BYPASS_CACHE);
+ cairo_surface_flush(s);
+
+ return s;
+}
+
+GdkPixbuf* render_pixbuf(Inkscape::Drawing &drawing, double scale_factor, Geom::Rect const &dbox, unsigned psize) {
+ auto s = render_surface(drawing, scale_factor, dbox, Geom::IntPoint(psize, psize), 1, nullptr, true);
+ 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..9006eb4
--- /dev/null
+++ b/src/ui/cache/svg_preview_cache.h
@@ -0,0 +1,69 @@
+// 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>
+#include <2geom/int-point.h>
+
+namespace Inkscape {
+
+class Drawing;
+class DrawingItem;
+
+} // namespace Inkscape
+
+
+cairo_surface_t* render_surface(Inkscape::Drawing &drawing, double scale_factor, const Geom::Rect& dbox,
+ Geom::IntPoint pixsize, double device_scale, const guint32* checkerboard_color, bool no_clip);
+
+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..eb13ff5
--- /dev/null
+++ b/src/ui/clipboard.cpp
@@ -0,0 +1,2024 @@
+// 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 "clipboard.h"
+
+#include <giomm/application.h>
+#include <glib/gstdio.h> // for g_file_set_contents etc., used in _onGet and paste
+#include <glibmm/i18n.h>
+#include <gtkmm/clipboard.h>
+
+#include <2geom/transforms.h>
+#include <2geom/path-sink.h>
+#include <iostream>
+#include <string>
+#include <boost/bimap.hpp>
+
+// TODO: reduce header bloat if possible
+
+#include "context-fns.h"
+#include "desktop-style.h" // for sp_desktop_set_style, used in _pasteStyle
+#include "desktop.h"
+#include "display/curve.h"
+#include "document.h"
+#include "extension/db.h" // extension database
+#include "extension/find_extension_by_mime.h"
+#include "extension/input.h"
+#include "extension/output.h"
+#include "file.h" // for file_import, used in _pasteImage
+#include "filter-chemistry.h"
+#include "gradient-drag.h"
+#include "helper/png-write.h"
+#include "id-clash.h"
+#include "inkgc/gc-core.h"
+#include "inkscape.h"
+#include "live_effects/lpe-bspline.h"
+#include "live_effects/lpe-spiro.h"
+#include "live_effects/lpeobject-reference.h"
+#include "live_effects/lpeobject.h"
+#include "live_effects/parameter/path.h"
+#include "message-stack.h"
+#include "object/box3d.h"
+#include "object/persp3d.h"
+#include "object/sp-clippath.h"
+#include "object/sp-defs.h"
+#include "object/sp-flowtext.h"
+#include "object/sp-gradient-reference.h"
+#include "object/sp-hatch.h"
+#include "object/sp-item-transform.h"
+#include "object/sp-linear-gradient.h"
+#include "object/sp-marker.h"
+#include "object/sp-mask.h"
+#include "object/sp-mesh-gradient.h"
+#include "object/sp-path.h"
+#include "object/sp-page.h"
+#include "object/sp-pattern.h"
+#include "object/sp-radial-gradient.h"
+#include "object/sp-rect.h"
+#include "object/sp-root.h"
+#include "object/sp-shape.h"
+#include "object/sp-symbol.h"
+#include "object/sp-textpath.h"
+#include "object/sp-use.h"
+#include "page-manager.h"
+#include "path-chemistry.h"
+#include "selection-chemistry.h"
+#include "style.h"
+#include "svg/css-ostringstream.h" // used in copy
+#include "svg/svg-color.h"
+#include "svg/svg.h" // for sp_svg_transform_write, used in _copySelection
+#include "text-chemistry.h"
+#include "text-editing.h"
+#include "ui/tool/control-point-selection.h"
+#include "ui/tool/multi-path-manipulator.h"
+#include "ui/tools/dropper-tool.h" // used in copy()
+#include "ui/tools/node-tool.h"
+#include "ui/tools/text-tool.h"
+#include "util/units.h"
+#include "xml/repr.h"
+#include "xml/sp-css-attr.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, SPDocument *source, Geom::Rect const &bbox) override;
+ void insertSymbol(SPDesktop *desktop, Geom::Point const &shift_dt) override;
+ bool paste(SPDesktop *desktop, bool in_place, bool on_page) 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;
+ Glib::ustring getFirstObjectID() override;
+
+ ClipboardManagerImpl();
+ ~ClipboardManagerImpl() override;
+
+private:
+ void _cleanStyle(SPCSSAttr *);
+ void _copySelection(ObjectSet *);
+ void _copyCompleteStyle(SPItem *item, Inkscape::XML::Node *target, bool child = false);
+ void _copyUsedDefs(SPItem *);
+ void _copyGradient(SPGradient *);
+ void _copyPattern(SPPattern *);
+ void _copyHatch(SPHatch *);
+ void _copyTextPath(SPTextPath *);
+ bool _copyNodes(SPDesktop *desktop, ObjectSet *set);
+ 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);
+ bool _pasteNodes(SPDesktop *desktop, SPDocument *clipdoc, bool in_place, bool on_page);
+ void _applyPathEffect(SPItem *, gchar const *);
+ std::unique_ptr<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(SPDesktop *desktop = nullptr);
+ void _setClipboardTargets();
+ void _setClipboardColor(guint32);
+ void _userWarn(SPDesktop *, char const *);
+
+ // private properties
+ std::unique_ptr<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;
+
+
+ // 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
+ auto dt = dynamic_cast<Inkscape::UI::Tools::DropperTool *>(desktop->event_context);
+ if (dt) {
+ _setClipboardColor(SP_DROPPER_CONTEXT(desktop->event_context)->get_color(false, true));
+ _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
+ auto tt = dynamic_cast<Inkscape::UI::Tools::TextTool *>(desktop->event_context);
+ if (tt) {
+ _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;
+ }
+
+ // Special case for copying part of a path instead of the whole selected object.
+ if (_copyNodes(desktop, set)) {
+ 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.get());
+
+ _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;
+ }
+ SPItem * item = SP_ACTIVE_DESKTOP->getSelection()->singleItem();
+ Geom::PathVector pv = pp->get_pathvector();
+ if (item != nullptr) {
+ pv *= item->i2doc_affine();
+ }
+ auto svgd = sp_svg_write_path(pv);
+
+ if (svgd.empty()) {
+ return;
+ }
+
+ _discardInternalClipboard();
+ _createInternalClipboard();
+
+ Inkscape::XML::Node *pathnode = _doc->createElement("svg:path");
+ pathnode->setAttribute("d", svgd);
+ _root->appendChild(pathnode);
+ Inkscape::GC::release(pathnode);
+
+ fit_canvas_to_drawing(_clipboardSPDoc.get());
+ _setClipboardTargets();
+}
+
+/**
+ * Copy a symbol from the symbol dialog.
+ *
+ * @param symbol The Inkscape::XML::Node for the symbol.
+ * @param style The style to be applied to the symbol.
+ * @param source The source document of the symbol.
+ * @param bbox The bounding box of the symbol, in desktop coordinates.
+ */
+void ClipboardManagerImpl::copySymbol(Inkscape::XML::Node* symbol, gchar const* style, SPDocument *source,
+ Geom::Rect const &bbox)
+{
+ if (!symbol)
+ 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.
+ auto original = cast<SPItem>(source->getObjectByRepr(symbol));
+ _copyUsedDefs(original);
+ 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);
+ auto nsymbol = cast<SPSymbol>(_clipboardSPDoc->getObjectById(symbol_name));
+ if (nsymbol) {
+ _copyCompleteStyle(original, repr, true);
+ auto scale = _clipboardSPDoc->getDocumentScale();
+ // Convert scale from source to clipboard user units
+ nsymbol->scaleChildItemsRec(scale, Geom::Point(0, 0), false);
+ if (!nsymbol->title()) {
+ nsymbol->setTitle(nsymbol->label() ? nsymbol->label() : nsymbol->getId());
+ }
+ auto href = Glib::ustring("#") + symbol_name;
+ size_t pos = href.find( "_inkscape_duplicate" );
+ // while ffix rename id we do this hack
+ href.erase( pos );
+ Inkscape::XML::Node *use_repr = _doc->createElement("svg:use");
+ use_repr->setAttribute("xlink:href", href);
+
+ /**
+ * If the symbol has a viewBox but no width or height, then take width and
+ * height from the viewBox and set them on the use element. Otherwise, the
+ * use element will have 100% document width and height!
+ */
+ {
+ auto widthAttr = symbol->attribute("width");
+ auto heightAttr = symbol->attribute("height");
+ auto viewBoxAttr = symbol->attribute("viewBox");
+ if (viewBoxAttr && !(heightAttr || widthAttr)) {
+ SPViewBox vb;
+ vb.set_viewBox(viewBoxAttr);
+ if (vb.viewBox_set) {
+ use_repr->setAttributeSvgDouble("width", vb.viewBox.width());
+ use_repr->setAttributeSvgDouble("height", vb.viewBox.height());
+ }
+ }
+ }
+ // Set a default style in <use> rather than <symbol> so it can be changed.
+ use_repr->setAttribute("style", style);
+ _root->appendChild(use_repr);
+ // because a extrange reason on append use getObjectsByElement("symbol") return 2 elements,
+ // it not give errrost by the moment;
+ if (auto use = cast<SPUse>(_clipboardSPDoc->getObjectByRepr(use_repr))) {
+ Geom::Affine affine = source->getDocumentScale();
+ use->doWriteTransform(affine, &affine, false);
+ }
+ // Set min and max offsets based on the bounding rectangle.
+ _clipnode->setAttributePoint("min", bbox.min());
+ _clipnode->setAttributePoint("max", bbox.max());
+ fit_canvas_to_drawing(_clipboardSPDoc.get());
+ }
+ _setClipboardTargets();
+}
+
+/**
+ * Insert a symbol into the document at the prescribed position (at the end of a drag).
+ *
+ * @param desktop The desktop onto which the symbol has been dropped.
+ * @param shift_dt The vector by which the symbol position should be shifted, in desktop coordinates.
+ */
+void ClipboardManagerImpl::insertSymbol(SPDesktop *desktop, Geom::Point const &shift_dt)
+{
+ if (!desktop || !Inkscape::have_viable_layer(desktop, desktop->getMessageStack())) {
+ return;
+ }
+ auto symbol = _retrieveClipboard("image/x-inkscape-svg");
+ if (!symbol) {
+ return;
+ }
+
+ prevent_id_clashes(symbol.get(), desktop->getDocument(), true);
+ auto *root = symbol->getRoot();
+
+ // Synthesize a clipboard position in order to paste the symbol where it got dropped.
+ if (auto *clipnode = sp_repr_lookup_name(root->getRepr(), "inkscape:clipboard", 1)) {
+ clipnode->setAttributePoint("min", clipnode->getAttributePoint("min") + shift_dt);
+ clipnode->setAttributePoint("max", clipnode->getAttributePoint("max") + shift_dt);
+ }
+
+ sp_import_document(desktop, symbol.get(), true);
+}
+
+/**
+ * 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, bool on_page)
+{
+ // 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(desktop);
+
+ // 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 ( !on_page && target == CLIPBOARD_GDK_PIXBUF_TARGET ) {
+ return _pasteImage(desktop->doc());
+ }
+ if ( !on_page && target == CLIPBOARD_TEXT_TARGET ) {
+ // It was text, and we did paste it. If not, continue on.
+ if (_pasteText(desktop)) {
+ return true;
+ }
+ // If the clipboard contains text/plain, but is an svg document
+ // then we'll try and detect it and then paste it if possible.
+ }
+
+ auto tempdoc = _retrieveClipboard(target);
+
+ if ( tempdoc == nullptr ) {
+ if (target == CLIPBOARD_TEXT_TARGET ) {
+ _userWarn(desktop, _("Can't paste text outside of the text tool."));
+ return false;
+ } else {
+ _userWarn(desktop, _("Nothing on the clipboard."));
+ return false;
+ }
+ }
+
+ if (_pasteNodes(desktop, tempdoc.get(), in_place, on_page)) {
+ return true;
+ }
+
+ // copy definitions
+ prevent_id_clashes(tempdoc.get(), desktop->getDocument(), true);
+ sp_import_document(desktop, tempdoc.get(), in_place, on_page);
+
+ // _copySelection() has put all items in groups, now ungroup them (preserves transform
+ // relationships of clones, text-on-path, etc.)
+ if (target == "image/x-inkscape-svg") {
+ SPDocument *doc = nullptr;
+ desktop->getSelection()->ungroup(true);
+ std::vector<SPItem *> vec2(desktop->getSelection()->items().begin(), desktop->getSelection()->items().end());
+ for (auto item : vec2) {
+ // just a bit beauty on paste hidden items unselect
+ doc = item->document;
+ if (vec2.size() > 1 && item->isHidden()) {
+ desktop->getSelection()->remove(item);
+ }
+ auto pasted_lpe_item = cast<SPLPEItem>(item);
+ if (pasted_lpe_item) {
+ remove_hidder_filter(pasted_lpe_item);
+ }
+ }
+ if (doc) {
+ doc->fix_lpe_data();
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Copy any selected nodes and return true if there were nodes.
+ */
+bool ClipboardManagerImpl::_copyNodes(SPDesktop *desktop, ObjectSet *set)
+{
+ auto node_tool = dynamic_cast<Inkscape::UI::Tools::NodeTool *>(desktop->event_context);
+ if (!node_tool || !node_tool->_selected_nodes)
+ return false;
+
+ SPObject *first_path = nullptr;
+ for (auto obj : set->items()) {
+ if(is<SPPath>(obj)) {
+ first_path = obj;
+ break;
+ }
+ }
+
+ auto builder = new Geom::PathBuilder();
+ node_tool->_multipath->copySelectedPath(builder);
+ Geom::PathVector pathv = builder->peek();
+
+ // discardInternalClipboard done after copy, as deleting clipboard
+ // document may trigger tool switch (as in PathParam::~PathParam)
+ _discardInternalClipboard();
+ _createInternalClipboard();
+
+ // Were any nodes actually copied?
+ if (pathv.empty() || !first_path)
+ return false;
+
+ Inkscape::XML::Node *pathRepr = _doc->createElement("svg:path");
+
+ // Remove the source document's scale from path as clipboard is 1:1
+ auto source_scale = first_path->document->getDocumentScale();
+ pathRepr->setAttribute("d", sp_svg_write_path(pathv * source_scale.inverse()));
+
+ // Group the path to make it consistant with other copy processes
+ auto group = _doc->createElement("svg:g");
+ _root->appendChild(group);
+ Inkscape::GC::release(group);
+
+ // Store the style for paste-as-object operations. Ignored if pasting into an other path.
+ pathRepr->setAttribute("style", first_path->style->write(SP_STYLE_FLAG_IFSET) );
+ group->appendChild(pathRepr);
+ Inkscape::GC::release(pathRepr);
+
+ // Store the parent transformation, and scaling factor of the copied object
+ if (auto parent = cast<SPItem>(first_path->parent)) {
+ auto transform_str = sp_svg_transform_write(parent->i2doc_affine());
+ group->setAttributeOrRemoveIfEmpty("transform", transform_str);
+ }
+
+ // Set the translation for paste-in-place operation, must be done after repr appends
+ if (auto path_obj = cast<SPPath>(_clipboardSPDoc->getObjectByRepr(pathRepr))) {
+ // we could use pathv.boundsFast here, but that box doesn't include stroke width
+ // so we must take the value from the visualBox of the new shape instead.
+ auto bbox = *(path_obj->visualBounds()) * source_scale;
+ _clipnode->setAttributePoint("min", bbox.min());
+ _clipnode->setAttributePoint("max", bbox.max());
+ }
+ _setClipboardTargets();
+ return true;
+}
+
+/**
+ * Paste nodes into a selected path and return true if it's possible.
+ * if the node tool selected
+ * and one path selected in target
+ * and one path in source
+ */
+bool ClipboardManagerImpl::_pasteNodes(SPDesktop *desktop, SPDocument *clipdoc, bool in_place, bool on_page)
+{
+ auto node_tool = dynamic_cast<Inkscape::UI::Tools::NodeTool *>(desktop->event_context);
+ if (!node_tool || desktop->getSelection()->objects().size() != 1)
+ return false;
+
+ SPObject *obj = desktop->getSelection()->objects().back();
+ auto target_path = cast<SPPath>(obj);
+ if (!target_path)
+ return false;
+
+ auto source_scale = clipdoc->getDocumentScale();
+ auto target_trans = target_path->i2doc_affine();
+ // Select all nodes prior to pasting in, for later inversion.
+ node_tool->_selected_nodes->selectAll();
+
+ for (auto node = clipdoc->getReprRoot()->firstChild(); node ;node = node->next()) {
+
+ auto source_obj = clipdoc->getObjectByRepr(node);
+ auto group_affine = Geom::Affine();
+
+ // Unpack group that may have a transformation inside it.
+ if (auto source_group = cast<SPGroup>(source_obj)) {
+ if (source_group->children.size() == 1) {
+ source_obj = source_group->firstChild();
+ group_affine = source_group->i2doc_affine();
+ }
+ }
+
+ if (auto source_path = cast<SPPath>(source_obj)) {
+ auto source_curve = *source_path->curveForEdit();
+ auto target_curve = *target_path->curveForEdit();
+
+ // Apply group transformation which is usually the old translation plus document scaling factor
+ source_curve.transform(group_affine);
+ // Convert curve from source units (usually px so 1:1)
+ source_curve.transform(source_scale);
+
+ if (!in_place) {
+ // Move the source curve to the mouse pointer, units are px so do before target_trans
+ auto bbox = *(source_path->geometricBounds()) * group_affine;
+ auto to_mouse = Geom::Translate(desktop->point() - bbox.midpoint());
+ source_curve.transform(to_mouse);
+ } else if (auto clipnode = sp_repr_lookup_name(clipdoc->getReprRoot(), "inkscape:clipboard", 1)) {
+ // Force translation so a foreign path will end up in the right place.
+ auto bbox = *(source_path->visualBounds()) * group_affine;
+ auto to_origin = Geom::Translate(clipnode->getAttributePoint("min") - bbox.min());
+ source_curve.transform(to_origin);
+ }
+
+ // Finally convert the curve into path item's coordinate system
+ source_curve.transform(target_trans.inverse());
+
+ // Add the source curve to the target copy
+ target_curve.append(std::move(source_curve));
+
+ // Set the attribute to keep the document up to date (fixes undo)
+ auto str = sp_svg_write_path(target_curve.get_pathvector());
+ target_path->setAttribute("d", str);
+
+ if (on_page) {
+ g_warning("Node paste on page not Implemented");
+ }
+ }
+ }
+ // Finally we invert the selection, this selects all newly added nodes.
+ node_tool->_selected_nodes->invertSelection();
+ return true;
+}
+
+/**
+ * Returns the id of the first visible copied object.
+ */
+Glib::ustring ClipboardManagerImpl::getFirstObjectID()
+{
+ auto tempdoc = _retrieveClipboard("image/x-inkscape-svg");
+ if ( tempdoc == nullptr ) {
+ return {};
+ }
+
+ Inkscape::XML::Node *root = tempdoc->getReprRoot();
+
+ if (!root) {
+ return {};
+ }
+
+ Inkscape::XML::Node *ch = root->firstChild();
+ Inkscape::XML::Node *child = nullptr;
+ // now clipboard is wrapped on copy since 202d57ea fix
+ while (ch != nullptr &&
+ g_strcmp0(ch->name(), "svg:g") &&
+ g_strcmp0(child?child->name():nullptr, "svg:g") &&
+ g_strcmp0(child?child->name():nullptr, "svg:path") &&
+ g_strcmp0(child?child->name():nullptr, "svg:use") &&
+ g_strcmp0(child?child->name():nullptr, "svg:text") &&
+ g_strcmp0(child?child->name():nullptr, "svg:image") &&
+ g_strcmp0(child?child->name():nullptr, "svg:rect") &&
+ g_strcmp0(child?child->name():nullptr, "svg:ellipse") &&
+ g_strcmp0(child?child->name():nullptr, "svg:circle")
+ ) {
+ ch = ch->next();
+ child = ch ? ch->firstChild(): nullptr;
+ }
+
+ if (child) {
+ char const *id = child->attribute("id");
+ if (id) {
+ return id;
+ }
+ }
+
+ return {};
+}
+
+/**
+ * Remove certain css elements which are not useful for pasteStyle
+ */
+void ClipboardManagerImpl::_cleanStyle(SPCSSAttr *style)
+{
+ if (style) {
+ /* Clean text 'position' properties */
+ sp_repr_css_unset_property(style, "text-anchor");
+ sp_repr_css_unset_property(style, "shape-inside");
+ sp_repr_css_unset_property(style, "shape-subtract");
+ sp_repr_css_unset_property(style, "shape-padding");
+ sp_repr_css_unset_property(style, "shape-margin");
+ sp_repr_css_unset_property(style, "inline-size");
+ }
+}
+
+/**
+ * Implements the Paste Style action.
+ */
+bool ClipboardManagerImpl::pasteStyle(ObjectSet *set)
+{
+ auto dt = set->desktop();
+ if (dt == 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;
+ }
+
+ auto tempdoc = _retrieveClipboard("image/x-inkscape-svg");
+ if ( tempdoc == nullptr ) {
+ // no document, but we can try _text_style
+ if (_text_style) {
+ _cleanStyle(_text_style);
+ sp_desktop_set_style(set, set->desktop(), _text_style);
+ return true;
+ } else {
+ _userWarn(set->desktop(), _("No style on the clipboard."));
+ return false;
+ }
+ }
+
+ static auto *const prefs = Inkscape::Preferences::get();
+ auto const copy_computed = prefs->getBool("/options/copycomputedstyle/value", true);
+
+ Inkscape::XML::Node *root = tempdoc->getReprRoot();
+ Inkscape::XML::Node *clipnode = sp_repr_lookup_name(root, "inkscape:clipboard", 1);
+
+ bool pasted = false;
+
+ if (clipnode) {
+ if (copy_computed) {
+ SPCSSAttr *style = sp_repr_css_attr(clipnode, "style");
+ sp_desktop_set_style(set, set->desktop(), style);
+ pasted = true;
+ } else {
+ for (auto node : set->xmlNodes()) {
+ pasted = node->copyAttribute("class", clipnode, true) || pasted;
+ pasted = node->copyAttribute("style", clipnode, true) || pasted;
+ }
+ }
+ if (pasted) {
+ // pasted style might depend on defs from the source
+ set->document()->importDefs(tempdoc.get());
+ }
+ }
+ else {
+ _userWarn(set->desktop(), _("No style on the clipboard."));
+ }
+
+ 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
+ auto 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;
+ bool visual_bbox = !Inkscape::Preferences::get()->getInt("/tools/bounding_box");
+ min = clipnode->getAttributePoint((visual_bbox ? "min" : "geom-min"), min);
+ max = clipnode->getAttributePoint((visual_bbox ? "max" : "geom-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->desktopPreferredBounds();
+ 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->preferredBounds();
+ if ( sel_size ) {
+ set->setScaleRelative(sel_size->midpoint(),
+ _getScale(set->desktop(), min, max, *sel_size, apply_x, apply_y));
+ }
+ }
+ pasted = true;
+ }
+ 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;
+ }
+
+ auto 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.get());
+ // 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);
+ item->doWriteTransform(item->transform);
+ }
+
+ 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)
+{
+ auto doc = _retrieveClipboard(); // any target will do here
+ if (!doc) {
+ _userWarn(desktop, _("Nothing on the clipboard."));
+ return "";
+ }
+
+ // unlimited search depth
+ auto repr = sp_repr_lookup_name(doc->getReprRoot(), "svg:path", -1);
+ auto item = cast<SPItem>(doc->getObjectByRepr(repr));
+
+ if (!item) {
+ _userWarn(desktop, _("Clipboard does not contain a path."));
+ return "";
+ }
+
+ // Adjust any copied path into the target document transform.
+ auto tr_p = item->i2doc_affine();
+ auto tr_s = doc->getDocumentScale().inverse();
+ auto pathv = sp_svg_read_pathv(repr->attribute("d"));
+ return sp_svg_write_path(pathv * tr_s * tr_p);
+}
+
+
+/**
+ * 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!
+
+ auto 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."));
+ return "";
+ }
+ gchar const *svgd = repr->attribute("id");
+ return svgd ? 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;
+ auto 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::compose(_("Clipboard does not contain any objects of type \"%1\"."), type)).c_str());
+ return result;
+ }
+ return result;
+}
+
+/**
+ * Iterate over a list of items and copy them to the clipboard.
+ */
+void ClipboardManagerImpl::_copySelection(ObjectSet *selection)
+{
+ static auto *const prefs = Inkscape::Preferences::get();
+ auto const copy_computed = prefs->getBool("/options/copycomputedstyle/value", true);
+ SPPage *page = nullptr;
+
+ // copy the defs used by all items
+ auto itemlist = selection->items();
+ cloned_elements.clear();
+ std::vector<SPItem *> items(itemlist.begin(), itemlist.end());
+ for (auto item : itemlist) {
+ if (!page) {
+ page = item->document->getPageManager().getPageFor(item, false);
+ }
+ auto lpeitem = cast<SPLPEItem>(item);
+ if (lpeitem) {
+ for (auto satellite : lpeitem->get_satellites(false, true)) {
+ if (satellite) {
+ auto item2 = cast<SPItem>(satellite);
+ if (item2 && std::find(items.begin(), items.end(), item2) == items.end()) {
+ items.push_back(item2);
+ }
+ }
+ }
+ }
+ }
+ cloned_elements.clear();
+ for (auto item : items) {
+ if (item) {
+ _copyUsedDefs(item);
+ } else {
+ g_assert_not_reached();
+ }
+ }
+
+ // copy the representation of the items
+ std::vector<SPObject *> sorted_items(items.begin(), items.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){
+ auto item = 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 = cast<SPItem>(item->parent)) {
+ auto transform_str = sp_svg_transform_write(parent->i2doc_affine());
+ group->setAttributeOrRemoveIfEmpty("transform", 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);
+
+ if (copy_computed) {
+ // copy complete inherited style
+ _copyCompleteStyle(item, obj_copy);
+ }
+ }
+ }
+ // copy style for Paste Style action
+ if (auto item = selection->singleItem()) {
+ if (copy_computed) {
+ SPCSSAttr *style = take_style_from_item(item);
+ _cleanStyle(style);
+ sp_repr_css_set(_clipnode, style, "style");
+ sp_repr_css_attr_unref(style);
+ } else {
+ _clipnode->copyAttribute("class", item->getRepr(), true);
+ _clipnode->copyAttribute("style", item->getRepr(), true);
+ }
+
+ // copy path effect from the first path
+ if (gchar const *effect = item->getRepr()->attribute("inkscape:path-effect")) {
+ _clipnode->setAttribute("inkscape:path-effect", effect);
+ }
+ }
+
+ if (Geom::OptRect size = selection->visualBounds()) {
+ _clipnode->setAttributePoint("min", size->min());
+ _clipnode->setAttributePoint("max", size->max());
+ }
+ if (Geom::OptRect geom_size = selection->geometricBounds()) {
+ _clipnode->setAttributePoint("geom-min", geom_size->min());
+ _clipnode->setAttributePoint("geom-max", geom_size->max());
+ }
+ if (page) {
+ auto page_rect = page->getDesktopRect();
+ _clipnode->setAttributePoint("page-min", page_rect.min());
+ _clipnode->setAttributePoint("page-max", page_rect.max());
+ }
+}
+
+/**
+ * Copies the style from the stylesheet to preserve it.
+ *
+ * @param item - The source item (connected to it's document)
+ * @param target - The target xml node to store the style in.
+ * @param child - Flag to indicate a recursive call, do not use.
+ */
+void ClipboardManagerImpl::_copyCompleteStyle(SPItem *item, Inkscape::XML::Node *target, bool child)
+{
+ auto source = item->getRepr();
+ SPCSSAttr *css;
+ if (child) {
+ // Child styles shouldn't copy their parent's existing cascaded style.
+ css = sp_repr_css_attr(source, "style");
+ } else {
+ css = sp_repr_css_attr_inherited(source, "style");
+ }
+ for (auto iter : item->style->properties()) {
+ if (iter->style_src == SPStyleSrc::STYLE_SHEET) {
+ css->setAttributeOrRemoveIfEmpty(iter->name(), iter->get_value());
+ }
+ }
+ sp_repr_css_set(target, css, "style");
+ sp_repr_css_attr_unref(css);
+
+ if (is<SPGroup>(item)) {
+ // Recursively go through chldren too
+ auto source_child = source->firstChild();
+ auto target_child = target->firstChild();
+ while (source_child && target_child) {
+ if (auto child_item = cast<SPItem>(item->document->getObjectByRepr(source_child))) {
+ _copyCompleteStyle(child_item, target_child, true);
+ }
+ source_child = source_child->next();
+ target_child = target_child->next();
+ }
+ }
+}
+
+/**
+ * Recursively copy all the definitions used by a given item to the clipboard defs.
+ */
+void ClipboardManagerImpl::_copyUsedDefs(SPItem *item)
+{
+ auto use = 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 (is<SPLinearGradient>(server) || is<SPRadialGradient>(server) || is<SPMeshGradient>(server) ) {
+ _copyGradient(cast<SPGradient>(server));
+ }
+ auto pattern = cast<SPPattern>(server);
+ if (pattern) {
+ _copyPattern(pattern);
+ }
+ auto hatch = cast<SPHatch>(server);
+ if (hatch) {
+ _copyHatch(hatch);
+ }
+ }
+ if (style && (style->stroke.isPaintserver())) {
+ SPPaintServer *server = item->style->getStrokePaintServer();
+ if (is<SPLinearGradient>(server) || is<SPRadialGradient>(server) || is<SPMeshGradient>(server) ) {
+ _copyGradient(cast<SPGradient>(server));
+ }
+ auto pattern = cast<SPPattern>(server);
+ if (pattern) {
+ _copyPattern(pattern);
+ }
+ auto hatch = cast<SPHatch>(server);
+ if (hatch) {
+ _copyHatch(hatch);
+ }
+ }
+
+ // For shapes, copy all of the shape's markers
+ auto shape = cast<SPShape>(item);
+ if (shape) {
+ for (auto & i : shape->_marker) {
+ if (i) {
+ _copyNode(i->getRepr(), _doc, _defs);
+ }
+ }
+ }
+
+ // For 3D boxes, copy perspectives
+ if (auto box = cast<SPBox3D>(item)) {
+ if (auto perspective = box->get_perspective()) {
+ _copyNode(perspective->getRepr(), _doc, _defs);
+ }
+ }
+
+ // Copy text paths
+ {
+ auto text = cast<SPText>(item);
+ SPTextPath *textpath = text ? cast<SPTextPath>(text->firstChild()) : nullptr;
+ if (textpath) {
+ _copyTextPath(textpath);
+ }
+ if (text) {
+ for (auto &&shape_prop_ptr : {
+ reinterpret_cast<SPIShapes SPStyle::*>(&SPStyle::shape_inside),
+ reinterpret_cast<SPIShapes SPStyle::*>(&SPStyle::shape_subtract) }) {
+ for (auto *href : (text->style->*shape_prop_ptr).hrefs) {
+ auto shape_obj = href->getObject();
+ if (!shape_obj)
+ continue;
+ auto shape_repr = shape_obj->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);
+ // recurse
+ for (auto &o : clip->children) {
+ if (auto childItem = cast<SPItem>(&o)) {
+ _copyUsedDefs(childItem);
+ }
+ }
+ }
+ // 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) {
+ auto childItem = cast<SPItem>(&o);
+ if (childItem) {
+ _copyUsedDefs(childItem);
+ }
+ }
+ }
+
+ // Copy filters
+ if (style->getFilter()) {
+ SPObject *filter = style->getFilter();
+ if (is<SPFilter>(filter)) {
+ _copyNode(filter->getRepr(), _doc, _defs);
+ }
+ }
+
+ // For lpe items, copy lpe stack if applicable
+ auto lpeitem = 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) {
+ auto childItem = 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) {
+ auto childItem = cast<SPItem>(&child);
+ if (childItem) {
+ _copyUsedDefs(childItem);
+ }
+ }
+ pattern = pattern->ref.getObject();
+ }
+}
+
+/**
+ * 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) {
+ auto childItem = 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 && is<SPDefs>(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();
+ 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", nullptr );
+ 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 (dynamic_cast<Inkscape::UI::Tools::TextTool *>(desktop->event_context)) {
+ return Inkscape::UI::Tools::sp_text_paste_inline(desktop->event_context);
+ }
+
+ // Parse the clipboard text as if it was a color string.
+ Glib::RefPtr<Gtk::Clipboard> clipboard = Gtk::Clipboard::get();
+ Glib::ustring const clip_text = clipboard->wait_for_text();
+ if (clip_text.length() < 30) {
+ // Zero makes it impossible to paste a 100% transparent black, but it's useful.
+ guint32 const rgb0 = sp_svg_read_color(clip_text.c_str(), 0x0);
+ if (rgb0) {
+ SPCSSAttr *color_css = sp_repr_css_attr_new();
+ sp_repr_css_set_property(color_css, "fill", SPColor(rgb0).toString().c_str());
+ // In the future this could parse opacity, but sp_svg_read_color lacks this.
+ sp_repr_css_set_property(color_css, "fill-opacity", "1.0");
+ sp_desktop_set_style(desktop, color_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;
+ }
+
+ auto lpeitem = 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.get(), href.c_str());
+ if (!obj) {
+ return;
+ }
+ auto lpeobj = cast<LivePathEffectObject>(obj);
+ if (lpeobj) {
+ Inkscape::LivePathEffect::LPESpiro *spiroto = dynamic_cast<Inkscape::LivePathEffect::LPESpiro *>(lpeobj->get_lpe());
+ bool has_spiro = lpeitem->hasPathEffectOfType(Inkscape::LivePathEffect::SPIRO);
+ Inkscape::LivePathEffect::LPEBSpline *bsplineto = dynamic_cast<Inkscape::LivePathEffect::LPEBSpline *>(lpeobj->get_lpe());
+ bool has_bspline = lpeitem->hasPathEffectOfType(Inkscape::LivePathEffect::BSPLINE);
+ if ((!spiroto || !has_spiro) && (!bsplineto || !has_bspline)) {
+ 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
+ */
+std::unique_ptr<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", nullptr );
+
+ 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 == "text/plain") {
+ 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);
+ } catch (...) {
+ }
+ g_unlink(filename);
+ g_free(filename);
+
+ return std::unique_ptr<SPDocument>(tempdoc);
+}
+
+#ifdef __APPLE__
+
+// MIME types: image/x-inkscape-svg, image/png, image/svg+xml, image/x-inkscape-svg-compressed, image/svg+xml-compressed, application/pdf, image/svg+xml, application/x-zip, application/tar, image/dxf, image/dxf, image/x-e-postscript, image/x-emf, text/xml+fxg, text/plain, image/hpgl, text/html, image/jpeg, application/x-zip, text/x-tex, text/xml+xaml, text/x-povray-script, image/x-postscript, text/x-povray-script, image/sif, image/tiff, image/webp, image/x-wmf,
+
+template <typename L, typename R>
+boost::bimap<L, R> make_bimap(std::initializer_list<typename boost::bimap<L, R>::value_type> list) {
+ return boost::bimap<L, R>(list.begin(), list.end());
+}
+
+// MIME type to Universal Type Identifiers
+static auto mime_uti = make_bimap<std::string, std::string>({
+ {"image/x-inkscape-svg", "org.inkscape.svg"},
+ {"image/svg+xml", "public.svg-image"},
+ {"image/png", "public.png"},
+ {"image/webp", "public.webp"},
+ {"image/tiff", "public.tiff"},
+ {"image/jpeg", "public.jpeg"},
+ {"image/x-e-postscript", "com.adobe.encapsulated-postscript"},
+ {"image/x-postscript", "com.adobe.postscript"},
+ // {"text/plain", "public.plain-text"}, - GIMP color palette
+ {"text/html", "public.html"},
+ {"application/pdf", "com.adobe.pdf"},
+ {"application/tar", "public.tar-archive"},
+ {"application/x-zip", "public.zip-archive"},
+});
+
+#endif
+
+/**
+ * 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();
+ g_info("Clipboard _onGet target: %s", target.c_str());
+
+ if (target == "") {
+ return; // this shouldn't happen
+ }
+
+ if (target == CLIPBOARD_TEXT_TARGET) {
+ target = "image/x-inkscape-svg";
+ }
+
+#ifdef __APPLE__
+ // translate UTI back to MIME
+ auto mime = mime_uti.right.find(target);
+ if (mime != mime_uti.right.end()) {
+ target = mime->get_left();
+ }
+#endif
+
+ // 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", nullptr );
+ 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 {
+ 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)->loaded()) {
+ // Need to load the extension.
+ (*out)->set_state(Inkscape::Extension::Extension::STATE_LOADED);
+ }
+
+ if ((*out)->is_raster())
+ {
+ 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 = nv->getAttributeDouble("inkscape:pageopacity", 1.0);
+ bgcolor |= SP_COLOR_F_TO_U(opacity);
+ }
+ std::vector<SPItem*> x;
+ gchar *raster_file = g_build_filename( g_get_user_cache_dir(), "inkscape-clipboard-export-raster", nullptr );
+ sp_export_png_file(_clipboardSPDoc.get(), raster_file, area, width, height, dpi, dpi, bgcolor, nullptr,
+ nullptr, true, x);
+ (*out)->export_raster(_clipboardSPDoc.get(), raster_file, filename, true);
+ unlink(raster_file);
+ g_free(raster_file);
+ }
+ else
+ {
+ (*out)->save(_clipboardSPDoc.get(), 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.reset(SPDocument::createNewDoc(nullptr, false, true));
+ //g_assert( _clipboardSPDoc != NULL );
+ _defs = _clipboardSPDoc->getDefs()->getRepr();
+ _doc = _clipboardSPDoc->getReprDoc();
+ _root = _clipboardSPDoc->getReprRoot();
+
+ // Preserve ANY copied text kerning
+ _root->setAttribute("xml:space", "preserve");
+
+ 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) {
+ _clipboardSPDoc.reset();
+ _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(SPDesktop *desktop)
+{
+ 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");
+ //*/
+
+ // Prioritise text when the text tool is active
+ if (desktop && dynamic_cast<Inkscape::UI::Tools::TextTool *>(desktop->event_context)) {
+ if (_clipboard->wait_is_text_available()) {
+ return CLIPBOARD_TEXT_TARGET;
+ }
+ }
+
+ 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();
+#ifdef __APPLE__
+ auto uti = mime_uti.left.find(mime);
+ if (uti != mime_uti.left.end()) {
+ target_list.emplace_back(uti->get_right());
+ }
+#endif
+ 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", nullptr );
+
+ try {
+ (*out)->save(_clipboardSPDoc.get(), 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..b2537ed
--- /dev/null
+++ b/src/ui/clipboard.h
@@ -0,0 +1,80 @@
+// 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>
+#include <vector>
+#include <2geom/point.h>
+#include <2geom/rect.h>
+
+// forward declarations
+class SPDesktop;
+class SPDocument;
+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, SPDocument *source, Geom::Rect const &bbox) = 0;
+ virtual void insertSymbol(SPDesktop *desktop, Geom::Point const &shift_dt) = 0;
+ virtual bool paste(SPDesktop *desktop, bool in_place = false, bool on_page = 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 Glib::ustring 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/column-menu-builder.h b/src/ui/column-menu-builder.h
new file mode 100644
index 0000000..a575c97
--- /dev/null
+++ b/src/ui/column-menu-builder.h
@@ -0,0 +1,108 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <functional>
+#include <gtkmm/enums.h>
+#include <optional>
+
+#include <glibmm/ustring.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/image.h>
+#include <gtkmm/label.h>
+#include <gtkmm/menu.h>
+#include <gtkmm/menuitem.h>
+#include <gtkmm/separatormenuitem.h>
+#include <utility>
+
+#ifndef COLUMN_MENU_BUILDER_INCLUDED
+#define COLUMN_MENU_BUILDER_INCLUDED
+
+namespace Inkscape {
+namespace UI {
+
+template<typename T>
+class ColumnMenuBuilder {
+public:
+ ColumnMenuBuilder(Gtk::Menu& menu, int columns, Gtk::IconSize icon_size = Gtk::ICON_SIZE_MENU)
+ : _menu(menu), _columns(columns), _icon_size(static_cast<int>(icon_size)) {}
+
+ Gtk::MenuItem* add_item(Glib::ustring label, T section, Glib::ustring tooltip, Glib::ustring icon_name, bool sensitive, bool customtooltip, std::function<void ()> callback) {
+ _new_section = false;
+ _section = nullptr;
+ if (!_last_section || *_last_section != section) {
+ _new_section = true;
+ }
+
+ if (_new_section) {
+ if (_col > 0) _row++;
+
+ // add separator
+ if (_row > 0) {
+ auto separator = Gtk::make_managed<Gtk::SeparatorMenuItem>();
+ separator->show();
+ _menu.attach(*separator, 0, _columns, _row, _row + 1);
+ _row++;
+ }
+
+ _last_section = section;
+
+ auto sep = Gtk::make_managed<Gtk::MenuItem>();
+ sep->get_style_context()->add_class("menu-category");
+ sep->set_sensitive(false);
+ sep->show();
+ _menu.attach(*sep, 0, _columns, _row, _row + 1);
+ _section = sep;
+ _col = 0;
+ _row++;
+ }
+
+ auto item = Gtk::make_managed<Gtk::MenuItem>();
+ auto grid = Gtk::make_managed<Gtk::Grid>();
+ grid->set_column_spacing(8);
+ grid->insert_row(0);
+ grid->insert_column(0);
+ grid->insert_column(1);
+ grid->attach(*Gtk::make_managed<Gtk::Image>(std::move(icon_name), _icon_size), 0, 0);
+ grid->attach(*Gtk::make_managed<Gtk::Label>(std::move(label), Gtk::ALIGN_START, Gtk::ALIGN_CENTER, true), 1, 0);
+ grid->set_sensitive(sensitive);
+ item->add(*grid);
+ if (!customtooltip) {
+ item->set_tooltip_markup(std::move(tooltip));
+ }
+ item->set_sensitive(sensitive);
+ item->signal_activate().connect(callback);
+ item->show_all();
+ _menu.attach(*item, _col, _col + 1, _row, _row + 1);
+ _col++;
+ if (_col >= _columns) {
+ _col = 0;
+ _row++;
+ }
+
+ return item;
+ }
+
+ bool new_section() {
+ return _new_section;
+ }
+
+ void set_section(Glib::ustring name) {
+ // name lastest section
+ if (_section) {
+ _section->set_label(name.uppercase());
+ }
+ }
+
+private:
+ int _row = 0;
+ int _col = 0;
+ int _columns;
+ Gtk::Menu& _menu;
+ bool _new_section = false;
+ std::optional<T> _last_section;
+ Gtk::MenuItem* _section = nullptr;
+ Gtk::IconSize _icon_size;
+};
+
+}} // namespace
+
+#endif // COLUMN_MENU_BUILDER_INCLUDED \ No newline at end of file
diff --git a/src/ui/contextmenu.cpp b/src/ui/contextmenu.cpp
new file mode 100644
index 0000000..707923e
--- /dev/null
+++ b/src/ui/contextmenu.cpp
@@ -0,0 +1,364 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Context menu
+ */
+/* Authors:
+ * Tavmjong Bah
+ *
+ * Rewrite of code authored by:
+ * 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) 2022 Tavmjong Bah
+ * 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 "desktop.h"
+#include "document.h"
+#include "layer-manager.h"
+#include "page-manager.h"
+#include "selection.h"
+
+#include "object/sp-anchor.h"
+#include "object/sp-image.h"
+#include "object/sp-page.h"
+#include "object/sp-shape.h"
+#include "object/sp-text.h"
+
+#include "ui/desktop/menu-icon-shift.h"
+#include "ui/util.h"
+
+ContextMenu::ContextMenu(SPDesktop *desktop, SPObject *object, bool hide_layers_and_objects_menu_item)
+{
+ set_name("ContextMenu");
+
+ auto item = cast<SPItem>(object);
+
+ // std::cout << "ContextMenu::ContextMenu: " << (item ? item->getId() : "no item") << std::endl;
+ action_group = Gio::SimpleActionGroup::create();
+ insert_action_group("ctx", action_group);
+ auto document = desktop->getDocument();
+ action_group->add_action("unhide-objects-below-cursor", sigc::bind<SPDocument*, bool>(sigc::mem_fun(*this, &ContextMenu::unhide_or_unlock), document, true));
+ action_group->add_action("unlock-objects-below-cursor", sigc::bind<SPDocument*, bool>(sigc::mem_fun(*this, &ContextMenu::unhide_or_unlock), document, false));
+
+ auto gmenu = Gio::Menu::create(); // Main menu
+ auto gmenu_section = Gio::Menu::create(); // Section (used multiple times)
+
+ auto layer = Inkscape::LayerManager::asLayer(item); // Layers have their own context menu in the Object and Layers dialog.
+ auto root = desktop->layerManager().currentRoot();
+
+ // Get a list of items under the cursor, used for unhiding and unlocking.
+ auto point_document = desktop->point() * desktop->dt2doc();
+ Geom::Rect b(point_document, point_document + Geom::Point(1, 1)); // Seems strange to use a rect!
+ items_under_cursor = document->getItemsPartiallyInBox(desktop->dkey, b, true, true, true, true);
+ bool has_hidden_below_cursor = false;
+ bool has_locked_below_cursor = false;
+ for (auto item : items_under_cursor) {
+ if (item->isHidden()) {
+ has_hidden_below_cursor = true;
+ }
+ if (item->isLocked()) {
+ has_locked_below_cursor = true;
+ }
+ }
+ // std::cout << "Items below cursor: " << items_under_cursor.size()
+ // << " hidden: " << std::boolalpha << has_hidden_below_cursor
+ // << " locked: " << std::boolalpha << has_locked_below_cursor
+ // << std::endl;
+
+ // clang-tidy off
+
+ // Undo/redo
+ // gmenu_section = Gio::Menu::create();
+ // AppendItemFromAction(gmenu_section, "doc.undo", _("Undo"), "edit-undo");
+ // AppendItemFromAction(gmenu_section, "doc.redo", _("Redo"), "edit-redo");
+ // gmenu->append_section(gmenu_section);
+
+ if (auto page = cast<SPPage>(object)) {
+ auto &page_manager = document->getPageManager();
+ page_manager.selectPage(page);
+
+ gmenu_section = Gio::Menu::create();
+ AppendItemFromAction(gmenu_section, "doc.page-new", _("_New Page"), "pages-add");
+ gmenu->append_section(gmenu_section);
+
+ gmenu_section = Gio::Menu::create();
+ AppendItemFromAction(gmenu_section, "doc.page-delete", _("_Delete Page"), "pages-remove");
+ AppendItemFromAction(gmenu_section, "doc.page-move-backward", _("Move Page _Backward"), "pages-order-backwards");
+ AppendItemFromAction(gmenu_section, "doc.page-move-forward", _("Move Page _Forward"), "pages-order-forwards");
+ gmenu->append_section(gmenu_section);
+
+ } else if (!layer) {
+ // "item" is the object that was under the mouse when right-clicked. It determines what is shown
+ // in the menu thus it makes the most sense that it is either selected or part of the current
+ // selection.
+ auto selection = desktop->getSelection();
+ if (object && !selection->includes(object)) {
+ selection->set(object);
+ }
+
+ if (!item) {
+ // Even when there's no item, we should still have the Paste action on top
+ // (see https://gitlab.com/inkscape/inkscape/-/issues/4150)
+ gmenu->append_section(create_clipboard_actions(true));
+
+ gmenu_section = Gio::Menu::create();
+ AppendItemFromAction(gmenu_section, "win.dialog-open('DocumentProperties')", _("Document Properties..."), "document-properties");
+ gmenu->append_section(gmenu_section);
+ } else {
+ // When an item is selected, show all three of Cut, Copy and Paste.
+ gmenu->append_section(create_clipboard_actions());
+
+ gmenu_section = Gio::Menu::create();
+ AppendItemFromAction(gmenu_section, "app.duplicate", _("Duplic_ate"), "edit-duplicate");
+ AppendItemFromAction(gmenu_section, "app.clone", _("_Clone"), "edit-clone");
+ AppendItemFromAction(gmenu_section, "app.delete-selection", _("_Delete"), "edit-delete");
+ gmenu->append_section(gmenu_section);
+
+ // Dialogs
+ auto gmenu_dialogs = Gio::Menu::create();
+ if (!hide_layers_and_objects_menu_item) { // Hidden when context menu is popped up in Layers and Objects dialog!
+ AppendItemFromAction(gmenu_dialogs, "win.dialog-open('Objects')", _("Layers and Objects..."), "dialog-objects" );
+ }
+ AppendItemFromAction(gmenu_dialogs, "win.dialog-open('ObjectProperties')", _("_Object Properties..."), "dialog-object-properties" );
+
+ if (is<SPShape>(item) || is<SPText>(item) || is<SPGroup>(item)) {
+ AppendItemFromAction(gmenu_dialogs, "win.dialog-open('FillStroke')", _("_Fill and Stroke..."), "dialog-fill-and-stroke" );
+ }
+
+ // Image dialogs (mostly).
+ if (auto image = cast<SPImage>(item)) {
+ AppendItemFromAction( gmenu_dialogs, "win.dialog-open('ObjectAttributes')", _("Image _Properties..."), "dialog-fill-and-stroke");
+ AppendItemFromAction( gmenu_dialogs, "win.dialog-open('Trace')", _("_Trace Bitmap..."), "bitmap-trace" );
+
+ if (image->getClipObject()) {
+ AppendItemFromAction( gmenu_dialogs, "app.element-image-crop", _("Crop Image to Clip"), "" );
+ }
+ if (strncmp(image->href, "data", 4) == 0) {
+ // Image is embedded.
+ AppendItemFromAction( gmenu_dialogs, "app.org.inkscape.filter.extract-image", _("Extract Image..."), "" );
+ } else {
+ // Image is linked.
+ AppendItemFromAction( gmenu_dialogs, "app.org.inkscape.filter.selected.embed-image", _("Embed Image"), "" );
+ AppendItemFromAction( gmenu_dialogs, "app.element-image-edit", _("Edit Externally..."), "" );
+ }
+ }
+
+ // Text dialogs.
+ if (is<SPText>(item)) {
+ AppendItemFromAction( gmenu_dialogs, "win.dialog-open('Text')", _("_Text and Font..."), "dialog-text-and-font" );
+ AppendItemFromAction( gmenu_dialogs, "win.dialog-open('Spellcheck')", _("Check Spellin_g..."), "tools-check-spelling" );
+ }
+ gmenu->append_section(gmenu_dialogs); // We might add to it later...
+
+ if (!is<SPAnchor>(item)) {
+ // Item menu
+
+ // Selection
+ gmenu_section = Gio::Menu::create();
+ auto gmenu_submenu = Gio::Menu::create();
+ AppendItemFromAction( gmenu_submenu, "win.select-same-fill-and-stroke", _("Fill _and Stroke"), "edit-select-same-fill-and-stroke");
+ AppendItemFromAction( gmenu_submenu, "win.select-same-fill", _("_Fill Color"), "edit-select-same-fill" );
+ AppendItemFromAction( gmenu_submenu, "win.select-same-stroke-color", _("_Stroke Color"), "edit-select-same-stroke-color" );
+ AppendItemFromAction( gmenu_submenu, "win.select-same-stroke-style", _("Stroke St_yle"), "edit-select-same-stroke-style" );
+ AppendItemFromAction( gmenu_submenu, "win.select-same-object-type", _("_Object Type"), "edit-select-same-object-type" );
+ gmenu_section->append_submenu(_("Select Sa_me"), gmenu_submenu);
+ gmenu->append_section(gmenu_section);
+
+ // Groups and Layers
+ gmenu_section = Gio::Menu::create();
+ AppendItemFromAction( gmenu_section, "win.selection-move-to-layer", _("_Move to Layer..."), "" );
+ AppendItemFromAction( gmenu_section, "app.selection-link", _("Create anchor (hyperlink)"), "" );
+ AppendItemFromAction( gmenu_section, "app.selection-group", _("_Group"), "" );
+ if (is<SPGroup>(item)) {
+ AppendItemFromAction( gmenu_section, "app.selection-ungroup", _("_Ungroup"), "" );
+ Glib::ustring label = Glib::ustring::compose(_("Enter group %1"), item->defaultLabel());
+ AppendItemFromAction( gmenu_section, "win.selection-group-enter", label, "" );
+ if (item->getParentGroup()->isLayer() || item->getParentGroup() == root) {
+ // A layer should be a child of root or another layer.
+ AppendItemFromAction( gmenu_section, "win.layer-from-group", _("Group to Layer"), "" );
+ }
+ }
+ auto group = cast<SPGroup>(item->parent);
+ if (group && !group->isLayer()) {
+ AppendItemFromAction( gmenu_section, "win.selection-group-exit", _("Exit group"), "" );
+ AppendItemFromAction( gmenu_section, "app.selection-ungroup-pop", _("_Pop selection out of group"), "" );
+ }
+ gmenu->append_section(gmenu_section);
+
+ // Clipping and Masking
+ gmenu_section = Gio::Menu::create();
+ if (selection->size() > 1) {
+ AppendItemFromAction( gmenu_section, "app.object-set-clip", _("Set Cl_ip"), "" );
+ }
+ if (item->getClipObject()) {
+ AppendItemFromAction( gmenu_section, "app.object-release-clip", _("Release C_lip"), "" );
+ } else {
+ AppendItemFromAction( gmenu_section, "app.object-set-clip-group", _("Set Clip G_roup"), "" );
+ }
+ if (selection->size() > 1) {
+ AppendItemFromAction( gmenu_section, "app.object-set-mask", _("Set Mask"), "" );
+ }
+ if (item->getMaskObject()) {
+ AppendItemFromAction( gmenu_section, "app.object-release-mask", _("Release Mask"), "" );
+ }
+ gmenu->append_section(gmenu_section);
+
+ // Hide and Lock
+ gmenu_section = Gio::Menu::create();
+ AppendItemFromAction( gmenu_section, "app.selection-hide", _("Hide Selected Objects"), "" );
+ AppendItemFromAction( gmenu_section, "app.selection-lock", _("Lock Selected Objects"), "" );
+ gmenu->append_section(gmenu_section);
+
+ } else {
+ // Anchor menu
+ gmenu_section = Gio::Menu::create();
+ AppendItemFromAction( gmenu_section, "win.dialog-open('ObjectAttributes')", _("Link _Properties..."), "" );
+ AppendItemFromAction( gmenu_section, "app.element-a-open-link", _("_Open link in browser"), "" );
+ AppendItemFromAction( gmenu_section, "app.selection-ungroup", _("_Remove Link"), "" );
+ AppendItemFromAction( gmenu_section, "win.selection-group-enter", _("Enter Group"), "" );
+ gmenu->append_section(gmenu_section);
+ }
+ }
+
+ // Hidden or locked beneath cursor
+ gmenu_section = Gio::Menu::create();
+ if (has_hidden_below_cursor) {
+ AppendItemFromAction( gmenu_section, "ctx.unhide-objects-below-cursor", _("Unhide Objects Below Cursor"), "" );
+ }
+ if (has_locked_below_cursor) {
+ AppendItemFromAction( gmenu_section, "ctx.unlock-objects-below-cursor", _("Unlock Objects Below Cursor"), "" );
+ }
+ gmenu->append_section(gmenu_section);
+
+ } else {
+ // Layers: Only used in "Layers and Objects" dialog.
+
+ gmenu_section = Gio::Menu::create();
+ AppendItemFromAction(gmenu_section, "win.layer-new", _("_Add Layer..."), "layer-new");
+ AppendItemFromAction(gmenu_section, "win.layer-duplicate", _("D_uplicate Layer"), "layer-duplicate");
+ AppendItemFromAction(gmenu_section, "win.layer-delete", _("_Delete Layer"), "layer-delete");
+ AppendItemFromAction(gmenu_section, "win.layer-rename", _("Re_name Layer..."), "layer-rename");
+ AppendItemFromAction(gmenu_section, "win.layer-to-group", _("Layer to _Group"), "dialog-objects");
+ gmenu->append_section(gmenu_section);
+
+ gmenu_section = Gio::Menu::create();
+ AppendItemFromAction(gmenu_section, "win.layer-raise", _("_Raise Layer"), "layer-raise");
+ AppendItemFromAction(gmenu_section, "win.layer-lower", _("_Lower Layer"), "layer-lower");
+ gmenu->append_section(gmenu_section);
+
+ gmenu_section = Gio::Menu::create();
+ AppendItemFromAction(gmenu_section, "win.layer-hide-toggle-others", _("_Hide/show other layers"), "");
+ AppendItemFromAction(gmenu_section, "win.layer-hide-all", _("_Hide all layers"), "");
+ AppendItemFromAction(gmenu_section, "win.layer-unhide-all", _("_Show all layers"), "");
+ gmenu->append_section(gmenu_section);
+
+ gmenu_section = Gio::Menu::create();
+ AppendItemFromAction(gmenu_section, "win.layer-lock-toggle-others", _("_Lock/unlock other layers"), "");
+ AppendItemFromAction(gmenu_section, "win.layer-lock-all", _("_Lock all layers"), "");
+ AppendItemFromAction(gmenu_section, "win.layer-unlock-all", _("_Unlock all layers"), "");
+ gmenu->append_section(gmenu_section);
+
+ }
+ // clang-tidy on
+
+ bind_model(gmenu, true);
+
+ // Do not install this CSS provider; it messes up menus with icons (like popup menu with all dialogs).
+ // It doesn't work well with context menu either, introducing disturbing visual glitch
+ // where menu shifts upon opening.
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getInt("/theme/shiftIcons", true)) {
+ get_style_context()->add_class("shifticonmenu");
+ shift_icons(this);
+ }
+ // Set the style and icon theme of the new menu based on the desktop
+ if (Gtk::Window *window = desktop->getToplevel()) {
+ if (window->get_style_context()->has_class("dark")) {
+ get_style_context()->add_class("dark");
+ } else {
+ get_style_context()->add_class("bright");
+ }
+ if (prefs->getBool("/theme/symbolicIcons", false)) {
+ get_style_context()->add_class("symbolic");
+ } else {
+ get_style_context()->add_class("regular");
+ }
+ }
+}
+
+/** @brief Create a menu section containing the standard editing actions:
+ * Cut, Copy, Paste.
+ *
+ * @param paste_only If true, only the Paste action will be included.
+ * @return A new menu containing the requested actions.
+ */
+Glib::RefPtr<Gio::Menu> ContextMenu::create_clipboard_actions(bool paste_only)
+{
+ auto result = Gio::Menu::create();
+ if (!paste_only) {
+ AppendItemFromAction(result, "app.cut", _("Cu_t"), "edit-cut");
+ AppendItemFromAction(result, "app.copy", _("_Copy"), "edit-copy");
+ }
+ AppendItemFromAction(result, "win.paste", _("_Paste"), "edit-paste");
+ return result;
+}
+
+void
+ContextMenu::AppendItemFromAction(Glib::RefPtr<Gio::Menu> gmenu, Glib::ustring action, Glib::ustring label, Glib::ustring icon)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool show_icons = prefs->getInt("/theme/menuIcons", true);
+
+ auto menu_item = Gio::MenuItem::create(label, action);
+ if (icon != "" && show_icons) {
+ auto _icon = Gio::Icon::create(icon);
+ menu_item->set_icon(_icon);
+ }
+ gmenu->append_item(menu_item);
+}
+
+void
+ContextMenu::unhide_or_unlock(SPDocument* document, bool unhide)
+{
+ for (auto item : items_under_cursor) {
+ if (unhide) {
+ if (item->isHidden()) {
+ item->setHidden(false);
+ }
+ } else {
+ if (item->isLocked()) {
+ item->setLocked(false);
+ }
+ }
+ }
+
+ // We wouldn't be here if we didn't make a change.
+ Inkscape::DocumentUndo::done(document, (unhide ? _("Unhid objects") : _("Unlocked objects")), "");
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ 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..a2ae29b
--- /dev/null
+++ b/src/ui/contextmenu.h
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CONTEXTMENU_H
+#define SEEN_CONTEXTMENU_H
+
+/*
+ * Context menu
+ *
+ * Authors:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2022 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <vector>
+
+#include <gtkmm/menu.h>
+#include <giomm.h>
+
+class SPDesktop;
+class SPDocument;
+class SPObject;
+class SPItem;
+
+/**
+ * Implements the Inkscape context menu.
+ */
+class ContextMenu : public Gtk::Menu
+{
+public:
+ ContextMenu(SPDesktop *desktop, SPObject *object, bool hide_layers_and_objects_menu_item = false);
+ ~ContextMenu() override = default;
+
+private:
+ static void AppendItemFromAction(Glib::RefPtr<Gio::Menu> gmenu, Glib::ustring action,
+ Glib::ustring label, Glib::ustring icon = "");
+ static Glib::RefPtr<Gio::Menu> create_clipboard_actions(bool paste_only = false);
+ // Used for unlock and unhide actions
+ Glib::RefPtr<Gio::SimpleActionGroup> action_group;
+ std::vector<SPItem *> items_under_cursor;
+ void unhide_or_unlock(SPDocument* document, bool unhide);
+};
+#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-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/cursor-utils.cpp b/src/ui/cursor-utils.cpp
new file mode 100644
index 0000000..8fbbd8b
--- /dev/null
+++ b/src/ui/cursor-utils.cpp
@@ -0,0 +1,244 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Cursor utilities
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * The contents of this file may be used under the GNU General Public License Version 2 or later.
+ *
+ */
+
+#include <iomanip>
+#include <sstream>
+#include <unordered_map>
+#include <boost/functional/hash.hpp>
+
+#include "cursor-utils.h"
+
+#include "document.h"
+#include "preferences.h"
+
+#include "display/cairo-utils.h"
+
+#include "helper/pixbuf-ops.h"
+
+#include "io/file.h"
+#include "io/resource.h"
+
+#include "object/sp-object.h"
+#include "object/sp-root.h"
+
+#include "util/units.h"
+
+using Inkscape::IO::Resource::SYSTEM;
+using Inkscape::IO::Resource::ICONS;
+
+namespace Inkscape {
+
+// SVG cursor unique ID/key
+typedef std::tuple<std::string, std::string, std::string, guint32, guint32, double, double, bool, int> Key;
+
+struct KeyHasher {
+ std::size_t operator () (const Key& k) const { return boost::hash_value(k); }
+};
+
+/**
+ * Loads an SVG cursor from the specified file name.
+ *
+ * Returns pointer to cursor (or null cursor if we could not load a cursor).
+ */
+Glib::RefPtr<Gdk::Cursor>
+load_svg_cursor(Glib::RefPtr<Gdk::Display> display,
+ Glib::RefPtr<Gdk::Window> window,
+ std::string const &file_name,
+ guint32 fill,
+ guint32 stroke,
+ double fill_opacity,
+ double stroke_opacity)
+{
+ // GTK puts cursors in a "cursors" subdirectory of icon themes. We'll do the same... but
+ // note that we cannot use the normal GTK method for loading cursors as GTK knows nothing
+ // about scalable SVG cursors. We must locate and load the files ourselves. (Even if
+ // GTK could handle scalable cursors, we would need to load the files ourselves inorder
+ // to modify CSS 'fill' and 'stroke' properties.)
+
+ Glib::RefPtr<Gdk::Cursor> cursor;
+
+ // Make list of icon themes, highest priority first.
+ std::vector<std::string> theme_names;
+
+ // Set in preferences
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring theme_name = prefs->getString("/theme/iconTheme", prefs->getString("/theme/defaultIconTheme", ""));
+ if (!theme_name.empty()) {
+ theme_names.push_back(theme_name);
+ }
+
+ // System
+ theme_name = Gtk::Settings::get_default()->property_gtk_icon_theme_name();
+ theme_names.push_back(theme_name);
+
+ // Our default
+ theme_names.emplace_back("hicolor");
+
+ // quantize opacity to limit number of cursor variations we generate
+ fill_opacity = std::floor(std::clamp(fill_opacity, 0.0, 1.0) * 100) / 100;
+ stroke_opacity = std::floor(std::clamp(stroke_opacity, 0.0, 1.0) * 100) / 100;
+
+ const auto enable_drop_shadow = prefs->getBool("/options/cursor-drop-shadow", true);
+
+ // Find the rendered size of the icon.
+ int scale = 1;
+ // cursor scaling? note: true by default - this has to be in sync with inkscape-preferences where it is true
+ bool cursor_scaling = prefs->getBool("/options/cursorscaling", true); // Fractional scaling is broken but we can't detect it.
+ if (cursor_scaling) {
+ scale = window->get_scale_factor(); // Adjust for HiDPI screens.
+ }
+
+ static std::unordered_map<Key, Glib::RefPtr<Gdk::Cursor>, KeyHasher> cursor_cache;
+ Key cursor_key;
+
+ const auto cache_enabled = prefs->getBool("/options/cache_svg_cursors", true);
+ if (cache_enabled) {
+ // construct a key
+ cursor_key = std::make_tuple(std::string(theme_names[0]), std::string(theme_names[1]), file_name, fill, stroke, fill_opacity, stroke_opacity, enable_drop_shadow, scale);
+ if (auto cursor = cursor_cache[cursor_key]) {
+ return cursor;
+ }
+ }
+
+ // Find theme paths.
+ auto screen = display->get_default_screen();
+ auto icon_theme = Gtk::IconTheme::get_for_screen(screen);
+ auto theme_paths = icon_theme->get_search_path();
+
+ // Loop over theme names and paths, looking for file.
+ Glib::RefPtr<Gio::File> file;
+ std::string full_file_path;
+ for (auto const &theme_name : theme_names) {
+ for (auto const &theme_path : theme_paths) {
+ full_file_path = Glib::build_filename(theme_path, theme_name, "cursors", file_name);
+ // std::cout << "Checking: " << full_file_path << std::endl;
+ file = Gio::File::create_for_path(full_file_path);
+ if (file->query_exists()) break;
+ }
+ if (file->query_exists()) break;
+ }
+
+ if (!file->query_exists()) {
+ std::cerr << "load_svg_cursor: Cannot locate cursor file: " << file_name << std::endl;
+ return cursor;
+ }
+
+ bool cancelled = false;
+ std::unique_ptr<SPDocument> document;
+ document.reset(ink_file_open(file, &cancelled));
+
+ if (!document) {
+ std::cerr << "load_svg_cursor: Could not open document: " << full_file_path << std::endl;
+ return cursor;
+ }
+
+ SPRoot *root = document->getRoot();
+ if (!root) {
+ std::cerr << "load_svg_cursor: Could not find SVG element: " << full_file_path << std::endl;
+ return cursor;
+ }
+
+ // Set the CSS 'fill' and 'stroke' properties on the SVG element (for cascading).
+ SPCSSAttr *css = sp_repr_css_attr(root->getRepr(), "style");
+
+ std::stringstream fill_stream;
+ fill_stream << "#"
+ << std::setfill ('0') << std::setw(6)
+ << std::hex << (fill >> 8);
+ std::stringstream stroke_stream;
+ stroke_stream << "#"
+ << std::setfill ('0') << std::setw(6)
+ << std::hex << (stroke >> 8);
+
+ sp_repr_css_set_property(css, "fill", fill_stream.str().c_str());
+ sp_repr_css_set_property(css, "stroke", stroke_stream.str().c_str());
+ sp_repr_css_set_property_double(css, "fill-opacity", fill_opacity);
+ sp_repr_css_set_property_double(css, "stroke-opacity", stroke_opacity);
+ root->changeCSS(css, "style");
+ sp_repr_css_attr_unref(css);
+
+ if (!enable_drop_shadow) {
+ // turn off drop shadow, if any
+ Glib::ustring shadow("drop-shadow");
+ auto objects = document->getObjectsByClass(shadow);
+ for (auto&& el : objects) {
+ if (auto val = el->getAttribute("class")) {
+ Glib::ustring cls = val;
+ auto pos = cls.find(shadow);
+ if (pos != Glib::ustring::npos) {
+ cls.erase(pos, shadow.length());
+ }
+ el->setAttribute("class", cls);
+ }
+ }
+ }
+
+ // Check for maximum size
+ // int mwidth = 0;
+ // int mheight = 0;
+ // display->get_maximal_cursor_size(mwidth, mheight);
+ // int normal_size = display->get_default_cursor_size();
+
+ auto w = document->getWidth().value("px");
+ auto h = document->getHeight().value("px");
+ // Calculate the hotspot.
+ int hotspot_x = root->getIntAttribute("inkscape:hotspot_x", 0); // Do not include window scale factor!
+ int hotspot_y = root->getIntAttribute("inkscape:hotspot_y", 0);
+
+ Geom::Rect area(0, 0, cursor_scaling ? w * scale : w, cursor_scaling ? h * scale : h);
+ int dpi = 96 * scale;
+ // render document into internal bitmap; returns null on failure
+ if (auto ink_pixbuf = std::unique_ptr<Inkscape::Pixbuf>(sp_generate_internal_bitmap(document.get(), area, dpi))) {
+ if (cursor_scaling) {
+ // creating cursor from Cairo surface rather than pixbuf gives us opportunity to set device scaling;
+ // what that means in practice is we can prepare high-res image and it will be used as-is on
+ // a high-res display; cursors created from pixbuf are up-scaled to device pixels (blurry)
+ auto surface = ink_pixbuf->getSurface();
+ if (surface && surface->cobj()) {
+ cairo_surface_set_device_scale(surface->cobj(), scale, scale);
+ cursor = Gdk::Cursor::create(display, surface, hotspot_x, hotspot_y);
+ }
+ else {
+ std::cerr << "load_svg_cursor: failed to get surface for: " << full_file_path << std::endl;
+ }
+ }
+ else {
+ // original code path when cursor scaling is turned off in preferences
+ auto pixbuf = Glib::wrap(ink_pixbuf->getPixbufRaw(), true);
+
+ if (pixbuf) {
+ cursor = Gdk::Cursor::create(display, pixbuf, hotspot_x, hotspot_y);
+ }
+ }
+ } else {
+ std::cerr << "load_svg_cursor: failed to create pixbuf for: " << full_file_path << std::endl;
+ }
+
+ document.reset();
+
+ if (cache_enabled) {
+ cursor_cache[cursor_key] = cursor;
+ }
+
+ return cursor;
+}
+
+} // 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 :
diff --git a/src/ui/cursor-utils.h b/src/ui/cursor-utils.h
new file mode 100644
index 0000000..02c9fad
--- /dev/null
+++ b/src/ui/cursor-utils.h
@@ -0,0 +1,40 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Cursor utilities
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * The contents of this file may be used under the GNU General Public License Version 2 or later.
+ *
+ */
+
+#ifndef INK_CURSOR_UTILITIES_H
+#define INK_CURSOR_UTILITIES_H
+
+#include <gtkmm.h>
+#include <string>
+
+namespace Inkscape {
+
+Glib::RefPtr<Gdk::Cursor> load_svg_cursor(Glib::RefPtr<Gdk::Display> display,
+ Glib::RefPtr<Gdk::Window> window,
+ std::string const &file_name,
+ guint32 fill = 0xffffffff,
+ guint32 stroke = 0x000000ff,
+ double fill_opacity = 1.0,
+ double stroke_opacity = 1.0);
+
+} // Namespace Inkscape
+
+#endif // INK_CURSOR_UTILITIES_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/document-check.cpp b/src/ui/desktop/document-check.cpp
new file mode 100644
index 0000000..6a8a809
--- /dev/null
+++ b/src/ui/desktop/document-check.cpp
@@ -0,0 +1,140 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Check for data loss when closing a document window.
+ *
+ * Copyright (C) 2004-2021 Authors
+ *
+ * The contents of this file may be used under the GNU General Public License Version 2 or later.
+ *
+ */
+
+/* Authors:
+ * MenTaLguY
+ * Link Mauve
+ * Thomas Holder
+ * Tavmjong Bah
+ */
+
+#include "document-check.h"
+
+#include <glibmm/i18n.h> // Internationalization
+#include <gtkmm.h>
+
+#include "inkscape-window.h"
+#include "object/sp-namedview.h"
+
+#include "file.h"
+#include "extension/system.h" // Inkscape::Extension::FILE...
+
+/** Check if closing document associated with window will cause data loss, and if so opens a dialog
+ * that gives user options to save or ignore.
+ *
+ * Returns true if document should remain open.
+ */
+bool
+document_check_for_data_loss(InkscapeWindow* window)
+{
+ auto document = window->get_document();
+
+ if (document->isModifiedSinceSave()) {
+ // Document has been modified!
+
+ Glib::ustring message = g_markup_printf_escaped(
+ _("<span weight=\"bold\" size=\"larger\">Save changes to document \"%s\" before closing?</span>\n\n"
+ "If you close without saving, your changes will be discarded."),
+ document->getDocumentName());
+
+ Gtk::MessageDialog dialog =
+ Gtk::MessageDialog(*window, message, true, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_NONE);
+ dialog.property_destroy_with_parent() = true;
+
+ // Don't allow text to be selected (via tabbing).
+ Gtk::Container *ma = dialog.get_message_area();
+ std::vector<Gtk::Widget*> ma_labels = ma->get_children();
+ ma_labels[0]->set_can_focus(false);
+
+ dialog.add_button(_("Close _without saving"), Gtk::RESPONSE_NO);
+ dialog.add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL);
+ dialog.add_button(_("_Save"), Gtk::RESPONSE_YES);
+ dialog.set_default_response(Gtk::RESPONSE_YES);
+
+ int response = dialog.run();
+
+ switch (response) {
+ case GTK_RESPONSE_YES:
+ {
+ // Save document
+ sp_namedview_document_from_window(window->get_desktop()); // Save window geometry in document.
+ if (!sp_file_save_document(*window, document)) {
+ // Save dialog cancelled or save failed.
+ return true;
+ }
+ break;
+ }
+ case GTK_RESPONSE_NO:
+ break;
+ default: // cancel pressed, or dialog was closed
+ return true;
+ break;
+ }
+ }
+
+ // Check for data loss due to saving in lossy format.
+ bool allow_data_loss = false;
+ while (document->getReprRoot()->attribute("inkscape:dataloss") != nullptr && allow_data_loss == false) {
+ // This loop catches if the user saves to a lossy format when in the loop.
+
+ Glib::ustring message = g_markup_printf_escaped(
+ _("<span weight=\"bold\" size=\"larger\">The file \"%s\" was saved with a format that may cause data loss!</span>\n\n"
+ "Do you want to save this file as Inkscape SVG?"),
+ document->getDocumentName() ? document->getDocumentName() : "Unnamed");
+
+ Gtk::MessageDialog dialog =
+ Gtk::MessageDialog(*window, message, true, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_NONE);
+ dialog.property_destroy_with_parent() = true;
+
+ // Don't allow text to be selected (via tabbing).
+ Gtk::Container *ma = dialog.get_message_area();
+ std::vector<Gtk::Widget*> ma_labels = ma->get_children();
+ ma_labels[0]->set_can_focus(false);
+
+ dialog.add_button(_("Close _without saving"), Gtk::RESPONSE_NO);
+ dialog.add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL);
+ dialog.add_button(_("_Save as Inkscape SVG"), Gtk::RESPONSE_YES);
+ dialog.set_default_response(Gtk::RESPONSE_YES);
+
+ int response = dialog.run();
+
+ switch (response) {
+ case GTK_RESPONSE_YES:
+ {
+ if (!sp_file_save_dialog(*window, document, Inkscape::Extension::FILE_SAVE_METHOD_INKSCAPE_SVG)) {
+ // Save dialog cancelled or save failed.
+ return TRUE;
+ }
+
+ break;
+ }
+ case GTK_RESPONSE_NO:
+ allow_data_loss = true;
+ break;
+ default: // cancel pressed, or dialog was closed
+ return true;
+ break;
+ }
+ }
+
+ 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/desktop/document-check.h b/src/ui/desktop/document-check.h
new file mode 100644
index 0000000..3fa1e5d
--- /dev/null
+++ b/src/ui/desktop/document-check.h
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Check for data loss when closing a document window.
+ *
+ * Copyright (C) 2021 Tavmjong Bah
+ *
+ * The contents of this file may be used under the GNU General Public License Version 2 or later.
+ *
+ */
+
+#ifndef DOCUMENT_CHECK_H
+
+class InkscapeWindow;
+
+bool document_check_for_data_loss(InkscapeWindow* window);
+
+#endif // DOCUMENT_CHECK_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/menu-icon-shift.cpp b/src/ui/desktop/menu-icon-shift.cpp
new file mode 100644
index 0000000..c0b9afe
--- /dev/null
+++ b/src/ui/desktop/menu-icon-shift.cpp
@@ -0,0 +1,109 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Shift Gtk::MenuItems with icons to align with Toggle and Radio buttons.
+ */
+/*
+ * Authors:
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Patrick Storz <eduard.braun2@gmx.de>
+ *
+ * Copyright (C) 2020 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 "menu-icon-shift.h"
+
+#include <iostream>
+#include <gtkmm.h>
+
+#include "inkscape-application.h" // Action extra data
+
+// Could be used to update status bar.
+// bool on_enter_notify(GdkEventCrossing* crossing_event, Gtk::MenuItem* menuitem)
+// {
+// return false;
+// }
+
+/*
+ * Install CSS to shift icons into the space reserved for toggles (i.e. check and radio items).
+ * The CSS will apply to all menu icons but is updated as each menu is shown.
+ */
+
+bool
+shift_icons(Gtk::MenuShell* menu)
+{
+ gint width, height;
+ gtk_icon_size_lookup(GTK_ICON_SIZE_MENU, &width, &height);
+ bool shifted = false;
+ // Calculate required shift. We need an example!
+ // Search for Gtk::MenuItem -> Gtk::Box -> Gtk::Image
+ static auto app = InkscapeApplication::instance();
+ auto &label_to_tooltip_map = app->get_menu_label_to_tooltip_map();
+
+ for (auto child : menu->get_children()) {
+ auto menuitem = dynamic_cast<Gtk::MenuItem *>(child);
+ if (menuitem) { //we need to go here to know we are in RTL maybe we can check in otehr way and simplify
+ auto submenu = menuitem->get_submenu();
+ if (submenu) {
+ shifted = shift_icons(submenu);
+ }
+ Gtk::Box *box = nullptr;
+ auto label = menuitem->get_label();
+ if (label.empty()) {
+ box = dynamic_cast<Gtk::Box *>(menuitem->get_child());
+ if (!box) {
+ continue;
+ }
+ std::vector<Gtk::Widget *> children = box->get_children();
+ if (children.size() == 2) {
+ auto label_widget = dynamic_cast<Gtk::Label *>(children[1]);
+ if (!label_widget) {
+ label_widget = dynamic_cast<Gtk::Label *>(children[0]);
+ }
+ if (label_widget) {
+ label = label_widget->get_label();
+ }
+ }
+ }
+ if (label.empty()) {
+ continue;
+ }
+ auto it = label_to_tooltip_map.find(label);
+ if (it != label_to_tooltip_map.end()) {
+ menuitem->set_tooltip_text(it->second);
+ }
+ if (shifted || !box) {
+ continue;
+ }
+ width += box->get_spacing() * 1.5; //2 elements 3 halfs to measure
+ std::string css_str;
+ Glib::RefPtr<Gtk::CssProvider> provider = Gtk::CssProvider::create();
+ auto const screen = Gdk::Screen::get_default();
+ Gtk::StyleContext::add_provider_for_screen(screen, provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ if (menuitem->get_direction() == Gtk::TEXT_DIR_RTL) {
+ css_str = ".shifticonmenu box {margin-right:-" + std::to_string(width) + "px;}";
+ } else {
+ css_str = ".shifticonmenu box {margin-left:-" + std::to_string(width) + "px;}";
+ }
+ provider->load_from_data(css_str);
+ shifted = true;
+ }
+ }
+ return shifted;
+}
+
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ 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/menu-icon-shift.h b/src/ui/desktop/menu-icon-shift.h
new file mode 100644
index 0000000..9b55f0d
--- /dev/null
+++ b/src/ui/desktop/menu-icon-shift.h
@@ -0,0 +1,41 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_DESKTOP_MENU_ITEM_SHIFT_H
+#define SEEN_DESKTOP_MENU_ITEM_SHIFT_H
+
+/**
+ * @file
+ * Shift Gtk::MenuItems with icons to align with Toggle and Radio buttons.
+ */
+/*
+ * Authors:
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Patrick Storz <eduard.braun2@gmx.de>
+ *
+ * Copyright (C) 2020 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 MenuShell;
+}
+
+/**
+ * Call back to shift icons into place reserved for toggles (i.e. check and radio items).
+ */
+bool shift_icons(Gtk::MenuShell *menu);
+
+#endif // SEEN_DESKTOP_MENU_ITEM_SHIFT_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/desktop/menubar.cpp b/src/ui/desktop/menubar.cpp
new file mode 100644
index 0000000..76cc3bf
--- /dev/null
+++ b/src/ui/desktop/menubar.cpp
@@ -0,0 +1,332 @@
+// 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>
+ * Sushant A.A. <sushant.co19@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 "menubar.h"
+
+#include <iostream>
+#include <iomanip>
+#include <map>
+#include <regex>
+
+#include <glibmm/i18n.h>
+
+#include "actions/actions-effect.h"
+#include "inkscape-application.h" // Open recent
+#include "preferences.h" // Use icons or not
+#include "io/resource.h" // UI File location
+
+// =================== Main Menu ================
+void
+build_menu()
+{
+ std::string filename = Inkscape::IO::Resource::get_filename(Inkscape::IO::Resource::UIS, "menus.ui");
+ auto refBuilder = Gtk::Builder::create();
+
+ try
+ {
+ refBuilder->add_from_file(filename);
+ }
+ catch (const Glib::Error& err)
+ {
+ std::cerr << "build_menu: failed to load Main menu from: "
+ << filename <<": "
+ << err.what().raw() << std::endl;
+ }
+
+ const auto object = refBuilder->get_object("menus");
+#if GTK_CHECK_VERSION(4, 0 ,0)
+ const auto gmenu = std::dynamic_pointer_cast<Gio::Menu>(object);
+#else
+ const auto gmenu = Glib::RefPtr<Gio::Menu>::cast_dynamic(object);
+#endif
+
+ if (!gmenu) {
+ std::cerr << "build_menu: failed to build Main menu!" << std::endl;
+ } else {
+
+ static auto app = InkscapeApplication::instance();
+ enable_effect_actions(app, false);
+ std::map<Glib::ustring, Glib::ustring>& label_to_tooltip_map = app->get_menu_label_to_tooltip_map();
+ label_to_tooltip_map.clear();
+
+ { // Filters and Extensions
+
+ auto effects_object = refBuilder->get_object("effect-menu-effects");
+ auto filters_object = refBuilder->get_object("filter-menu-filters");
+ auto effects_menu = Glib::RefPtr<Gio::Menu>::cast_dynamic(effects_object);
+ auto filters_menu = Glib::RefPtr<Gio::Menu>::cast_dynamic(filters_object);
+
+ if (!filters_menu) {
+ std::cerr << "build_menu(): Couldn't find Filters menu entry!" << std::endl;
+ }
+ if (!effects_menu) {
+ std::cerr << "build_menu(): Couldn't find Extensions menu entry!" << std::endl;
+ }
+
+ std::map<Glib::ustring, Glib::RefPtr<Gio::Menu>> submenus;
+
+ for (auto &[ entry_id, submenu_name_list, entry_name ] : app->get_action_effect_data().give_all_data())
+ {
+ if (submenu_name_list.size() > 0) {
+
+ // Effect data is used for both filters menu and extensions menu... we need to
+ // add to correct menu. 'submenu_name_list' either starts with 'Effects' or 'Filters'.
+ // Note "Filters" is translated!
+ Glib::ustring path; // Only used as index to map of submenus.
+ auto top_menu = filters_menu;
+ if (submenu_name_list.front() == "Effects") {
+ top_menu = effects_menu;
+ path += "Effects";
+ } else {
+ path += "Filters";
+ }
+ submenu_name_list.pop_front();
+
+ if (top_menu) { // It's possible that the menu doesn't exist (Kid's Inkscape?)
+ auto current_menu = top_menu;
+ for (auto &submenu_name : submenu_name_list) {
+ path += submenu_name + "-";
+ auto it = submenus.find(path);
+ if (it == submenus.end()) {
+ auto new_gsubmenu = Gio::Menu::create();
+ submenus[path] = new_gsubmenu;
+ current_menu->append_submenu(submenu_name, new_gsubmenu);
+ current_menu = new_gsubmenu;
+ } else {
+ current_menu = it->second;
+ }
+ }
+ current_menu->append(entry_name, "app." + entry_id);
+ } else {
+ std::cerr << "build_menu(): menu doesn't exist!" << std::endl; // Warn for now.
+ }
+ }
+ }
+ }
+
+ // Recent file
+ auto recent_manager = Gtk::RecentManager::get_default();
+
+ auto sub_object = refBuilder->get_object("recent-files");
+ auto sub_gmenu = Glib::RefPtr<Gio::Menu>::cast_dynamic(sub_object);
+ auto recent_menu_quark = Glib::Quark("recent-manager");
+ sub_gmenu->set_data(recent_menu_quark, recent_manager.get()); // mark submenu, so we can find it
+
+ auto rebuild = [](Glib::RefPtr<Gio::Menu> submenu) {
+ auto recent_manager = Gtk::RecentManager::get_default();
+ submenu->remove_all();
+ int max_files = Inkscape::Preferences::get()->getInt("/options/maxrecentdocuments/value");
+ if (max_files <= 0) {
+ return;
+ }
+
+ auto recent_files = recent_manager->get_items(); // all recent files not necessarily inkscape only
+ // sort by "last modified" time, which puts the most recently opened files first
+ std::sort(begin(recent_files), end(recent_files),
+ [](Glib::RefPtr<Gtk::RecentInfo> a, Glib::RefPtr<Gtk::RecentInfo> b) -> bool {
+ return a->get_modified() > b->get_modified();
+ }
+ );
+
+ unsigned inserted_entries = 0;
+ for (auto const &recent_file : recent_files) {
+ // check if given was generated by inkscape
+ bool valid_file = recent_file->has_application(g_get_prgname()) ||
+ recent_file->has_application("org.inkscape.Inkscape") ||
+ recent_file->has_application("inkscape")
+#ifdef _WIN32
+ || recent_file->has_application("inkscape.exe")
+#endif
+ ;
+
+ // this is potentially expensive: local FS access (remote files are not checked)
+ valid_file = valid_file && recent_file->exists();
+
+ if (!valid_file) {
+ continue;
+ }
+
+ // Escape underscores to prevent them from being interpreted as accelerator mnemonics
+ std::regex underscore{"_"};
+ std::string const dunder{"__"};
+ std::string raw_name = recent_file->get_short_name();
+ Glib::ustring escaped = std::regex_replace(raw_name, underscore, dunder);
+
+ auto item { Gio::MenuItem::create(std::move(escaped), Glib::ustring()) };
+ auto target { Glib::Variant<Glib::ustring>::create(recent_file->get_uri_display()) };
+ // note: setting action and target separately rather than using convenience menu method append
+ // since some filename characters can result in invalid "direct action" string
+ item->set_action_and_target(Glib::ustring("app.file-open-window"), target);
+ submenu->append_item(item);
+ inserted_entries++;
+
+ if (--max_files == 0) {
+ break;
+ }
+ }
+
+ if (!inserted_entries) { // Create a placeholder with a non-existent action
+ auto nothing2c = Gio::MenuItem::create(_("No items found"), "app.nop");
+ submenu->append_item(nothing2c);
+ }
+ };
+
+ rebuild(sub_gmenu);
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ auto useicons = static_cast<UseIcons>(prefs->getInt("/theme/menuIcons", 0));
+
+ // Remove all or some icons. Also create label to tooltip map.
+ auto gmenu_copy = Gio::Menu::create();
+ // menu gets recreated; keep track of new recent items submenu
+ rebuild_menu(gmenu, gmenu_copy, useicons, recent_menu_quark, sub_gmenu);
+ app->gtk_app()->set_menubar(gmenu_copy);
+
+ // rebuild recent items submenu when the list changes
+ recent_manager->signal_changed().connect([=](){ rebuild(sub_gmenu); });
+ }
+}
+
+
+/*
+ * Disable all or some menu icons.
+ *
+ * This is quite nasty:
+ *
+ * We must disable icons in the Gio::Menu as there is no way to pass
+ * the needed information to the children of Gtk::PopoverMenu and no
+ * way to set visibility via CSS.
+ *
+ * MenuItems are immutable and not copyable so you have to recreate
+ * the menu tree. The format for accessing MenuItem data is not the
+ * same as what you need to create a new MenuItem.
+ *
+ * NOTE: Input is a Gio::MenuModel, Output is a Gio::Menu!!
+ */
+#if GTK_CHECK_VERSION(4, 0, 0)
+void rebuild_menu (std::shared_ptr<Gio::MenuModel> menu, std::shared_ptr<Gio::Menu> menu_copy, UseIcons useIcons, Glib::Quark quark, Glib::RefPtr<Gio::Menu>& recent_files) {
+#else
+void rebuild_menu (Glib::RefPtr<Gio::MenuModel> menu, Glib::RefPtr<Gio::Menu> menu_copy, UseIcons useIcons, Glib::Quark quark, Glib::RefPtr<Gio::Menu>& recent_files) {
+#endif
+
+ static auto app = InkscapeApplication::instance();
+ auto& extra_data = app->get_action_extra_data();
+ auto& label_to_tooltip_map = app->get_menu_label_to_tooltip_map();
+
+ for (int i = 0; i < menu->get_n_items(); ++i) {
+
+ Glib::ustring label;
+ Glib::ustring action;
+ Glib::ustring target;
+ Glib::VariantBase icon;
+ Glib::ustring use_icon;
+ std::map<Glib::ustring, Glib::VariantBase> attributes;
+
+ auto attribute_iter = menu->iterate_item_attributes(i);
+ while (attribute_iter->next()) {
+
+ // Attributes we need to create MenuItem or set icon.
+ if (attribute_iter->get_name() == "label") {
+ // Convert label while preserving unicode translations
+ label = Glib::VariantBase::cast_dynamic<Glib::Variant<std::string> >(attribute_iter->get_value()).get();
+ } else if (attribute_iter->get_name() == "action") {
+ action = attribute_iter->get_value().print();
+ action.erase(0, 1);
+ action.erase(action.size()-1, 1);
+ } else if (attribute_iter->get_name() == "target") {
+ target = attribute_iter->get_value().print();
+ } else if (attribute_iter->get_name() == "icon") {
+ icon = attribute_iter->get_value();
+ } else if (attribute_iter->get_name() == "use-icon") {
+ use_icon = attribute_iter->get_value().print();
+ } else {
+ // All the remaining attributes.
+ attributes[attribute_iter->get_name()] = attribute_iter->get_value();
+ }
+ }
+ Glib::ustring detailed_action = action;
+ if (target.size() > 0) {
+ detailed_action += "(" + target + ")";
+ }
+
+ auto tooltip = extra_data.get_tooltip_for_action(detailed_action);
+ label_to_tooltip_map[label] = tooltip;
+
+ // std::cout << " " << std::setw(30) << detailed_action
+ // << " label: " << std::setw(30) << label.c_str()
+ // << " use_icon (.ui): " << std::setw(6) << use_icon
+ // << " icon: " << (icon ? "yes" : "no ")
+ // << " useIcons: " << (int)useIcons
+ // << " use_icon.size(): " << use_icon.size()
+ // << " tooltip: " << tooltip.c_str()
+ // << std::endl;
+
+#ifdef __APPLE__
+ // Workaround for https://gitlab.gnome.org/GNOME/gtk/-/issues/5667
+ // Convert document actions to window actions
+ if (strncmp(detailed_action.c_str(), "doc.", 4) == 0) {
+ detailed_action = "win." + detailed_action.raw().substr(4);
+ }
+#endif
+
+ auto menu_item = Gio::MenuItem::create(label, detailed_action);
+ if (icon &&
+ (useIcons == UseIcons::always ||
+ (useIcons == UseIcons::as_requested && use_icon.size() > 0))) {
+ menu_item->set_attribute_value("icon", icon);
+ }
+
+ // Add remaining attributes
+ for (auto const& [key, value] : attributes) {
+ menu_item->set_attribute_value(key, value);
+ }
+
+ // Add submenus
+ auto link_iter = menu->iterate_item_links(i);
+ while (link_iter->next()) {
+ auto submenu = Gio::Menu::create();
+ if (link_iter->get_name() == "submenu") {
+ menu_item->set_submenu(submenu);
+ if (link_iter->get_value()->get_data(quark)) {
+ recent_files = submenu;
+ }
+ } else if (link_iter->get_name() == "section") {
+ menu_item->set_section(submenu);
+ } else {
+ std::cerr << "rebuild_menu: Unknown link type: " << link_iter->get_name().raw() << std::endl;
+ }
+ rebuild_menu (link_iter->get_value(), submenu, useIcons, quark, recent_files);
+ }
+
+ menu_copy->append_item(menu_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/desktop/menubar.h b/src/ui/desktop/menubar.h
new file mode 100644
index 0000000..704f26f
--- /dev/null
+++ b/src/ui/desktop/menubar.h
@@ -0,0 +1,49 @@
+// 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
+ * Sushant A.A.
+ *
+ * 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> // GTK_CHECK_VERSION
+
+void build_menu();
+
+enum class UseIcons {
+ never = -1, // Match existing preference numbering.
+ as_requested,
+ always,
+};
+
+// Rebuild menu with icons enabled or disabled. Recursive.
+#if GTK_CHECK_VERSION(4, 0, 0)
+void rebuild_menu (std::shared_ptr<Gio::MenuModel> menu, std::shared_ptr<Gio::Menu> menu_copy, UseIcons useIcons, Glib::Quark quark, Glib::RefPtr<Gio::Menu>& recent_files);
+#else
+void rebuild_menu (Glib::RefPtr<Gio::MenuModel> menu, Glib::RefPtr<Gio::Menu> menu_copy, UseIcons useIcons, Glib::Quark quark, Glib::RefPtr<Gio::Menu>& recent_files);
+#endif
+
+#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..68416d8
--- /dev/null
+++ b/src/ui/dialog-events.cpp
@@ -0,0 +1,102 @@
+// 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 "enums.h"
+#include "ui/dialog-events.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
+ if (auto w = win->get_transient_for()) {
+ // switch to it
+ w->present();
+ }
+}
+
+void sp_dialog_defocus(GtkWindow *win)
+{
+ // find out the document window we're transient for
+ if (auto w = gtk_window_get_transient_for(GTK_WINDOW(win))) {
+ // switch to it
+ gtk_window_present(w);
+ }
+}
+
+void sp_dialog_defocus_on_enter_cpp(Gtk::Entry *e)
+{
+ e->signal_activate().connect([e] {
+ sp_dialog_defocus_cpp(dynamic_cast<Gtk::Window*>(e->get_toplevel()));
+ });
+}
+
+static void sp_dialog_defocus_callback(GtkWindow*, gpointer data)
+{
+ sp_dialog_defocus(GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(data))));
+}
+
+void sp_dialog_defocus_on_enter(GtkWidget *w)
+{
+ g_signal_connect(G_OBJECT(w), "activate", G_CALLBACK(sp_dialog_defocus_callback), w);
+}
+
+/**
+ * Make the argument dialog transient to the currently active document window.
+ */
+void sp_transientize(GtkWidget *dialog)
+{
+ auto 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", PREFS_DIALOGS_WINDOWS_NORMAL,
+ PREFS_DIALOGS_WINDOWS_NONE, PREFS_DIALOGS_WINDOWS_AGGRESSIVE);
+
+#ifdef _WIN32 // Win32 special code to enable transient dialogs
+ transient_policy = PREFS_DIALOGS_WINDOWS_AGGRESSIVE;
+#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);
+ }
+ }
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ 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..3c13053
--- /dev/null
+++ b/src/ui/dialog-events.h
@@ -0,0 +1,41 @@
+// 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>
+
+namespace Gtk {
+class Window;
+class Entry;
+}
+
+void sp_dialog_defocus_cpp (Gtk::Window *win);
+void sp_dialog_defocus_on_enter_cpp(Gtk::Entry *e);
+
+void sp_dialog_defocus (GtkWindow *win);
+void sp_dialog_defocus_on_enter(GtkWidget *w);
+void sp_transientize (GtkWidget *win);
+
+#endif // SEEN_DIALOG_EVENTS_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/README.md b/src/ui/dialog/README.md
new file mode 100644
index 0000000..17217d2
--- /dev/null
+++ b/src/ui/dialog/README.md
@@ -0,0 +1,46 @@
+# Dialog System
+
+Author: vanntile
+
+The dialog system used for dialog-type widgets is made out of the classes
+defined in the following header files (each explained later on):
+
+- dialog-container.h
+- dialog-multipaned.h
+- dialog-notebook.h
+- dialog-window.h
+- dialog-base.h
+
+This is a Gtk::Notebook based dialog manager with a GIMP style multipane.
+Dialogs can live only inside DialogNotebooks, as pages in the inner
+Gtk::Notebook, with their own tabs. A DialogNotebook itself is one of the
+children of a DialogMultipaned. There can be several levels of DialogMultipaned,
+but the top parent is a DialogContainer, which manages the existence of such
+dialogs.
+
+DialogMultipaned is a paned-type container which supports resizing the children
+by dragging the separator "handle" widgets. More than this, it supports the
+addition of new children (in DialogNotebook) by drag-and-dropping them at the
+extremeties of a multipane, where you can find dropzones (left and right for
+horizontal ones, or top and bottom for vertical ones).
+
+Dialogs can also live independently in their own DialogWindow. In this floating
+state, they track the last active window, while in the attached (docked) state,
+they track the InkscapeWindow they are in. You can drag tabs from DialogNotebook
+to move a DialogBase (or child class instance) between windows. In Wayland,
+if you drag the tab to an invalid position, it will create automatically a
+DialogWindow to live in.
+
+DialogContainers are instantiated with a horizontal DialogMultipaned.
+
+## Initialisation
+
+The initial DialogContainer is created inside a DesktopWidget, then inserting
+toolbars and an empty vertical DialogMultipaned where new dialogs will be added.
+
+## Adding a new dialog
+
+In order to add a new dialog to a window, you have to call the method
+`new_dialog(const Glib::ustring& dialog_type)` on the container inside
+the desktop of the window. Allowed dialog types can be found in the
+dialog-container.cpp file.
diff --git a/src/ui/dialog/about.cpp b/src/ui/dialog/about.cpp
new file mode 100644
index 0000000..fec809a
--- /dev/null
+++ b/src/ui/dialog/about.cpp
@@ -0,0 +1,206 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief A dialog for the about screen
+ *
+ * Copyright (C) Martin Owens 2019 <doctormo@gmail.com>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "about.h"
+
+#include <algorithm>
+#include <fstream>
+#include <random>
+#include <regex>
+#include <streambuf>
+#include <string>
+
+#include "document.h"
+#include "inkscape-version-info.h"
+#include "io/resource.h"
+#include "ui/util.h"
+#include "ui/view/svg-view-widget.h"
+#include "util/units.h"
+
+using namespace Inkscape::IO;
+using namespace Inkscape::UI::View;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+static Gtk::Window *window = nullptr;
+static Gtk::Notebook *tabs = nullptr;
+
+void close_about_screen() {
+ window->hide();
+}
+bool show_copy_button(Gtk::Button *button, Gtk::Label *label) {
+ reveal_widget(button, true);
+ reveal_widget(label, false);
+ return false;
+}
+void copy_version(Gtk::Button *button, Gtk::Label *label) {
+ auto clipboard = Gtk::Clipboard::get();
+ clipboard->set_text(Inkscape::inkscape_version());
+ if (label) {
+ reveal_widget(button, false);
+ reveal_widget(label, true);
+ Glib::signal_timeout().connect_seconds(
+ sigc::bind(sigc::ptr_fun(&show_copy_button), button, label), 2);
+ }
+}
+void copy_debug_info(Gtk::Button *button, Gtk::Label *label) {
+ auto clipboard = Gtk::Clipboard::get();
+ clipboard->set_text(Inkscape::debug_info());
+ if (label) {
+ reveal_widget(button, false);
+ reveal_widget(label, true);
+ Glib::signal_timeout().connect_seconds(
+ sigc::bind(sigc::ptr_fun(&show_copy_button), button, label), 2);
+ }
+}
+
+void AboutDialog::show_about() {
+
+ if(!window) {
+ // Load glade file here
+ Glib::ustring gladefile = Resource::get_filename(Resource::UIS, "inkscape-about.glade");
+ Glib::RefPtr<Gtk::Builder> builder;
+ try {
+ builder = Gtk::Builder::create_from_file(gladefile);
+ } catch (const Glib::Error &ex) {
+ g_error("Glade file loading failed for about screen dialog");
+ return;
+ }
+ builder->get_widget("about-screen-window", window);
+ builder->get_widget("tabs", tabs);
+ if(!tabs || !window) {
+ g_error("Window or tabs in glade file are missing or do not have the right ids.");
+ return;
+ }
+ // Automatic signal handling (requires -rdynamic compile flag)
+ //gtk_builder_connect_signals(builder->gobj(), NULL);
+
+ // When automatic handling fails
+ Gtk::Button *version;
+ Gtk::Label *label;
+ builder->get_widget("version", version);
+ builder->get_widget("version-copied", label);
+ if(version) {
+ version->set_label(Inkscape::inkscape_version());
+ version->signal_clicked().connect(
+ sigc::bind(sigc::ptr_fun(&copy_version), version, label));
+ }
+
+ Gtk::Button *debug_info;
+ Gtk::Label *label2;
+ builder->get_widget("debug_info", debug_info);
+ builder->get_widget("debug-info-copied", label2);
+ if (debug_info) {
+ debug_info->signal_clicked().connect(
+ sigc::bind(sigc::ptr_fun(&copy_debug_info), version, label2));
+ }
+
+ Gtk::Label *copyright;
+ builder->get_widget("copyright", copyright);
+ if (copyright) {
+ copyright->set_label(
+ Glib::ustring::compose(copyright->get_label(), Inkscape::inkscape_build_year()));
+ }
+
+ // Render the about screen image via inkscape SPDocument
+ auto filename = Resource::get_filename(Resource::SCREENS, "about.svg", true, false);
+ SPDocument *doc = SPDocument::createNewDoc(filename.c_str(), TRUE);
+
+ // Bind glade's container to our SVGViewWidget class
+ if(doc) {
+ //SVGViewWidget *viewer;
+ //builder->get_widget_derived("image-container", viewer, doc);
+ //Gtk::manage(viewer);
+ auto viewer = Gtk::manage(new Inkscape::UI::View::SVGViewWidget(doc));
+ double width = doc->getWidth().value("px");
+ double height = doc->getHeight().value("px");
+ viewer->setResize(width, height);
+
+ Gtk::AspectFrame *splash_widget;
+ builder->get_widget("aspect-frame", splash_widget);
+ splash_widget->unset_label();
+ splash_widget->set_shadow_type(Gtk::SHADOW_NONE);
+ splash_widget->property_ratio() = width / height;
+ splash_widget->add(*viewer);
+ splash_widget->show_all();
+ } else {
+ g_error("Error loading about screen SVG.");
+ }
+
+ Gtk::TextView *authors;
+ builder->get_widget("credits-authors", authors);
+ std::random_device rd;
+ std::mt19937 g(rd());
+
+ if(authors) {
+ std::ifstream fn(Resource::get_filename(Resource::DOCS, "AUTHORS"));
+ std::vector<std::string> authors_data;
+ std::string line;
+ while (getline(fn, line)) {
+ authors_data.push_back(line);
+ }
+ std::shuffle(std::begin(authors_data), std::end(authors_data), g);
+ std::string str = "";
+ for (auto author : authors_data) {
+ str += author + "\n";
+ }
+ authors->get_buffer()->set_text(str.c_str());
+ }
+
+ Gtk::TextView *translators;
+ builder->get_widget("credits-translators", translators);
+ if(translators) {
+ std::ifstream fn(Resource::get_filename(Resource::DOCS, "TRANSLATORS"));
+ std::vector<std::string> translators_data;
+ std::string line;
+ while (getline(fn, line)) {
+ translators_data.push_back(line);
+ }
+ std::string str = "";
+ std::regex e("(.*?)(<.*|)");
+ std::shuffle(std::begin(translators_data), std::end(translators_data), g);
+ for (auto translator : translators_data) {
+ str += std::regex_replace(translator, e, "$1") + "\n";
+ }
+ translators->get_buffer()->set_text(str.c_str());
+ }
+
+ Gtk::Label *license;
+ builder->get_widget("license-text", license);
+ if(license) {
+ std::ifstream fn(Resource::get_filename(Resource::DOCS, "LICENSE"));
+ std::string str((std::istreambuf_iterator<char>(fn)),
+ std::istreambuf_iterator<char>());
+ license->set_markup(str.c_str());
+ }
+ }
+ if(window) {
+ window->show();
+ tabs->set_current_page(0);
+ } else {
+ g_error("About screen window couldn't be loaded. Missing window id in glade file.");
+ }
+}
+
+} // 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/about.h b/src/ui/dialog/about.h
new file mode 100644
index 0000000..b6d7923
--- /dev/null
+++ b/src/ui/dialog/about.h
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief A dialog for the about screen
+ *
+ * Copyright (C) Martin Owens 2019 <doctormo@gmail.com>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef ABOUTDIALOG_H
+#define ABOUTDIALOG_H
+
+#include <gtkmm.h>
+#include <gtkmm/builder.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class AboutDialog {
+
+ public:
+ static void show_about();
+
+ private:
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // ABOUTDIALOG_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..709f221
--- /dev/null
+++ b/src/ui/dialog/align-and-distribute.cpp
@@ -0,0 +1,315 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Align and Distribute widget
+ */
+/* Authors:
+ * Tavmjong Bah
+ *
+ * Based on dialog by:
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ * Aubanel MONNIER <aubi@libertysurf.fr>
+ * Frank Felfe <innerspace@iname.com>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ *
+ * Copyright (C) 2021 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "align-and-distribute.h" // widget
+
+#include <iostream>
+
+#include <giomm.h>
+
+#include "desktop.h" // Tool switching.
+#include "inkscape-window.h" // Activate window action.
+#include "actions/actions-tools.h" // Tool switching.
+#include "io/resource.h"
+#include "ui/dialog/dialog-base.h" // Tool switching.
+#include "ui/util.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+using Inkscape::IO::Resource::get_filename;
+using Inkscape::IO::Resource::UIS;
+
+AlignAndDistribute::AlignAndDistribute(Inkscape::UI::Dialog::DialogBase* dlg)
+ : Gtk::Box(Gtk::ORIENTATION_VERTICAL)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ Glib::ustring builder_file = get_filename(UIS, "align-and-distribute.ui");
+ auto builder = Gtk::Builder::create();
+ try
+ {
+ builder->add_from_file(builder_file);
+ }
+ catch (const Glib::Error& ex)
+ {
+ std::cerr << "AlignAndDistribute::AlignAndDistribute: " << builder_file.raw() << " file not read! " << ex.what().raw() << std::endl;
+ }
+
+ builder->get_widget("align-and-distribute-box", align_and_distribute_box);
+ if (!align_and_distribute_box) {
+ std::cerr << "AlignAndDistribute::AlignAndDistribute: failed to load widget (box)!" << std::endl;
+ } else {
+ add(*align_and_distribute_box);
+ }
+
+ builder->get_widget("align-and-distribute-object", align_and_distribute_object);
+ if (!align_and_distribute_object) {
+ std::cerr << "AlignAndDistribute::AlignAndDistribute: failed to load widget (object)!" << std::endl;
+ } else {
+ align_and_distribute_object->show();
+ }
+
+ builder->get_widget("align-and-distribute-node", align_and_distribute_node);
+ if (!align_and_distribute_node) {
+ std::cerr << "AlignAndDistribute::AlignAndDistribute: failed to load widget (node)!" << std::endl;
+ } else {
+ align_and_distribute_node->hide();
+ }
+
+ // ------------ Object Align -------------
+
+ builder->get_widget("align-relative-object", align_relative_object);
+ if (!align_relative_object) {
+ std::cerr << "AlignAndDistribute::AlignAndDistribute: failed to load widget (combobox)!" << std::endl;
+ } else {
+ std::string align_to = prefs->getString("/dialogs/align/objects-align-to", "selection");
+ align_relative_object->set_active_id(align_to);
+ align_relative_object->signal_changed().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_align_relative_object_changed));
+ }
+
+ builder->get_widget("align-move-as-group", align_move_as_group);
+ if (!align_move_as_group) {
+ std::cerr << "AlignAndDistribute::AlignAndDistribute: failed to load widget (group button)!" << std::endl;
+ } else {
+ bool sel_as_group = prefs->getBool("/dialogs/align/sel-as-groups");
+ align_move_as_group->set_active(sel_as_group);
+
+ align_move_as_group->signal_clicked().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_align_as_group_clicked));
+ }
+
+ // clang-format off
+ std::vector<std::pair<std::string, std::string>> align_buttons = {
+ {"align-horizontal-right-to-anchor", "right anchor" },
+ {"align-horizontal-left", "left" },
+ {"align-horizontal-center", "hcenter" },
+ {"align-horizontal-right", "right" },
+ {"align-horizontal-left-to-anchor", "left anchor" },
+ {"align-horizontal-baseline", "horizontal" },
+ {"align-vertical-bottom-to-anchor", "bottom anchor" },
+ {"align-vertical-top", "top" },
+ {"align-vertical-center", "vcenter" },
+ {"align-vertical-bottom", "bottom" },
+ {"align-vertical-top-to-anchor", "top anchor" },
+ {"align-vertical-baseline", "vertical" }
+ };
+ // clang-format on
+
+ for (auto align_button: align_buttons) {
+ Gtk::Button* button;
+ builder->get_widget(align_button.first, button);
+ if (!button) {
+ std::cerr << "AlignAndDistribute::AlignAndDisribute: failed to get button: "
+ << align_button.first << " " << align_button.second << std::endl;
+ } else {
+ button->signal_button_press_event().connect(
+ sigc::bind<std::string>(sigc::mem_fun(*this, &AlignAndDistribute::on_align_button_press_event), align_button.second), false);
+ }
+ }
+
+
+ // ------------ Remove overlap -------------
+
+ builder->get_widget("remove-overlap-button", remove_overlap_button);
+ if (!remove_overlap_button) {
+ std::cerr << "AlignAndDistribute::AlignAndDistribute: failed to load widget!" << std::endl;
+ } else {
+ remove_overlap_button->signal_button_press_event().connect(
+ sigc::mem_fun(*this, &AlignAndDistribute::on_remove_overlap_button_press_event), false); // false => run first.
+ }
+
+ builder->get_widget("remove-overlap-hgap", remove_overlap_hgap);
+ if (!remove_overlap_hgap) {
+ std::cerr << "AlignAndDistribute::AlignAndDistribute: failed to load widget!" << std::endl;
+ }
+
+ builder->get_widget("remove-overlap-vgap", remove_overlap_vgap);
+ if (!remove_overlap_vgap) {
+ std::cerr << "AlignAndDistribute::AlignAndDistribute: failed to load widget!" << std::endl;
+ }
+
+ // ------------ Node Align -------------
+
+ builder->get_widget("align-relative-node", align_relative_node);
+ if (!align_relative_node) {
+ std::cerr << "AlignAndDistribute::AlignAndDistribute: failed to load widget (combobox)!" << std::endl;
+ } else {
+ std::string align_nodes_to = prefs->getString("/dialogs/align/nodes-align-to", "first");
+ align_relative_node->set_active_id(align_nodes_to);
+ align_relative_node->signal_changed().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_align_relative_node_changed));
+ }
+
+ std::vector<std::pair<std::string, std::string>> align_node_buttons = {
+ {"align-node-horizontal", "horizontal"},
+ {"align-node-vertical", "vertical" }
+ };
+
+ for (auto align_button: align_node_buttons) {
+ Gtk::Button* button;
+ builder->get_widget(align_button.first, button);
+ if (!button) {
+ std::cerr << "AlignAndDistribute::AlignAndDisribute: failed to get button: "
+ << align_button.first << " " << align_button.second << std::endl;
+ } else {
+ button->signal_button_press_event().connect(
+ sigc::bind<std::string>(sigc::mem_fun(*this, &AlignAndDistribute::on_align_node_button_press_event), align_button.second), false);
+ }
+ }
+
+
+ // ------------ Set initial values ------------
+
+ // Normal or node alignment?
+ auto desktop = dlg->getDesktop();
+ if (desktop) {
+ desktop_changed(desktop);
+ }
+
+ auto set_icon_size_prefs = [=]() {
+ int size = prefs->getIntLimited("/toolbox/tools/iconsize", -1, 16, 48);
+ Inkscape::UI::set_icon_sizes(this, size);
+ };
+
+ // For now we are going to track the toolbox icon size, in the future we will have our own
+ // dialog based icon sizes, perhaps done via css instead.
+ _icon_sizes_changed = prefs->createObserver("/toolbox/tools/iconsize", set_icon_size_prefs);
+ set_icon_size_prefs();
+}
+
+void
+AlignAndDistribute::desktop_changed(SPDesktop* desktop)
+{
+ tool_connection.disconnect();
+ if (desktop) {
+ tool_connection =
+ desktop->connectEventContextChanged(sigc::mem_fun(*this, &AlignAndDistribute::tool_changed_callback));
+ tool_changed(desktop);
+ }
+}
+
+void
+AlignAndDistribute::tool_changed(SPDesktop* desktop)
+{
+ bool node = get_active_tool(desktop) == "Node";
+ if (node) {
+ align_and_distribute_object->hide();
+ align_and_distribute_node->show();
+ } else {
+ align_and_distribute_object->show();
+ align_and_distribute_node->hide();
+ }
+}
+
+void
+AlignAndDistribute::tool_changed_callback(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec)
+{
+ tool_changed(desktop);
+}
+
+
+void
+AlignAndDistribute::on_align_as_group_clicked()
+{
+ bool state = align_move_as_group->get_active();
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/dialogs/align/sel-as-groups", state);
+}
+
+void
+AlignAndDistribute::on_align_relative_object_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setString("/dialogs/align/objects-align-to", align_relative_object->get_active_id());
+}
+
+void
+AlignAndDistribute::on_align_relative_node_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setString("/dialogs/align/nodes-align-to", align_relative_node->get_active_id());
+}
+
+bool
+AlignAndDistribute::on_align_button_press_event(GdkEventButton* button_event, const std::string& align_to)
+{
+ Glib::ustring argument = align_to;
+
+ argument += " " + align_relative_object->get_active_id();
+
+ if (align_move_as_group->get_active()) {
+ argument += " group";
+ }
+
+ auto variant = Glib::Variant<Glib::ustring>::create(argument);
+ auto app = Gio::Application::get_default();
+
+ if (align_to.find("vertical") != Glib::ustring::npos or align_to.find("horizontal") != Glib::ustring::npos) {
+ app->activate_action("object-align-text", variant);
+ } else {
+ app->activate_action("object-align", variant);
+ }
+
+ return true;
+}
+
+bool
+AlignAndDistribute::on_remove_overlap_button_press_event(GdkEventButton* button_event)
+{
+ double hgap = remove_overlap_hgap->get_value();
+ double vgap = remove_overlap_vgap->get_value();
+
+ auto variant = Glib::Variant<std::tuple<double, double>>::create(std::tuple<double, double>(hgap, vgap));
+ auto app = Gio::Application::get_default();
+ app->activate_action("object-remove-overlaps", variant);
+ return true;
+}
+
+bool
+AlignAndDistribute::on_align_node_button_press_event(GdkEventButton* button_event, const std::string& direction)
+{
+ Glib::ustring argument = align_relative_node->get_active_id();
+
+ auto variant = Glib::Variant<Glib::ustring>::create(argument);
+ InkscapeWindow* win = InkscapeApplication::instance()->get_active_window();
+ if (!win) {
+ return true;
+ }
+ if (direction == "horizontal") {
+ win->activate_action("node-align-horizontal", variant);
+ } else {
+ win->activate_action("node-align-vertical", variant);
+ }
+
+ return 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/align-and-distribute.h b/src/ui/dialog/align-and-distribute.h
new file mode 100644
index 0000000..1e94e1a
--- /dev/null
+++ b/src/ui/dialog/align-and-distribute.h
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Align and Distribute widget
+ */
+/* Authors:
+ * Tavmjong Bah
+ *
+ * Based on dialog by:
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ * Aubanel MONNIER <aubi@libertysurf.fr>
+ * Frank Felfe <innerspace@iname.com>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ *
+ * Copyright (C) 2021 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_ALIGN_AND_DISTRIBUTE_H
+#define INKSCAPE_UI_WIDGET_ALIGN_AND_DISTRIBUTE_H
+
+#include <sigc++/connection.h>
+#include <gtkmm.h>
+
+#include "preferences.h"
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace UI {
+
+namespace Tools {
+class ToolBase;
+}
+
+namespace Dialog {
+class DialogBase;
+
+class AlignAndDistribute : public Gtk::Box
+{
+public:
+ AlignAndDistribute(Inkscape::UI::Dialog::DialogBase* dlg);
+ ~AlignAndDistribute() override = default;
+
+ void desktop_changed(SPDesktop* desktop);
+ void tool_changed(SPDesktop* desktop); // Need to show different widgets for node vs. other tools.
+ void tool_changed_callback(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec);
+
+private:
+
+ // ********* Widgets ********** //
+
+ Gtk::Box* align_and_distribute_box = nullptr;
+ Gtk::Box* align_and_distribute_object = nullptr; // Hidden when node tool active.
+ Gtk::Box* align_and_distribute_node = nullptr; // Visible when node tool active.
+
+ // Align
+ Gtk::ToggleButton* align_move_as_group = nullptr;
+ Gtk::ComboBox* align_relative_object = nullptr;
+ Gtk::ComboBox* align_relative_node = nullptr;
+
+ // Remove overlap
+ Gtk::Button* remove_overlap_button = nullptr;
+ Gtk::SpinButton* remove_overlap_hgap = nullptr;
+ Gtk::SpinButton* remove_overlap_vgap = nullptr;
+
+
+ // ********* Signal handlers ********** //
+
+ void on_align_as_group_clicked();
+ void on_align_relative_object_changed();
+ void on_align_relative_node_changed();
+
+ bool on_align_button_press_event(GdkEventButton* button_event, const std::string& align_to);
+ bool on_remove_overlap_button_press_event(GdkEventButton* button_event);
+ bool on_align_node_button_press_event(GdkEventButton* button_event, const std::string& align_to);
+
+ sigc::connection tool_connection;
+ Inkscape::PrefObserver _icon_sizes_changed;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_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..6f464b9
--- /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::Box
+{
+public:
+ ArrangeTab() : Gtk::Box(Gtk::ORIENTATION_VERTICAL) {}
+ ~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..88b411d
--- /dev/null
+++ b/src/ui/dialog/attrdialog.cpp
@@ -0,0 +1,848 @@
+// 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 "preferences.h"
+#include "selection.h"
+#include "document-undo.h"
+#include "message-context.h"
+#include "message-stack.h"
+#include "style.h"
+
+#include "io/resource.h"
+
+#include "ui/builder-utils.h"
+#include "ui/dialog/inkscape-preferences.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "ui/syntax.h"
+#include "ui/util.h"
+#include "ui/widget/shapeicon.h"
+#include "util/numeric/converters.h"
+#include "util/trim.h"
+#include "xml/attribute-record.h"
+
+#include <cstddef>
+#include <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.h>
+#include <glibmm/main.h>
+#include <glibmm/regex.h>
+#include <glibmm/timer.h>
+#include <glibmm/ustring.h>
+#include <gtkmm/box.h>
+#include <gtkmm/button.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/label.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/menuitem.h>
+#include <gtkmm/object.h>
+#include <gtkmm/popover.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/targetlist.h>
+#include <gtkmm/textview.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/widget.h>
+#include <memory>
+#include <string>
+
+#include "config.h"
+#if WITH_GSOURCEVIEW
+# include <gtksourceview/gtksource.h>
+#endif
+
+/**
+ * Return true if `node` is a text or comment node
+ */
+static bool is_text_or_comment_node(Inkscape::XML::Node const &node)
+{
+ switch (node.type()) {
+ case Inkscape::XML::NodeType::TEXT_NODE:
+ case Inkscape::XML::NodeType::COMMENT_NODE:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static Glib::ustring get_syntax_theme()
+{
+ return Inkscape::Preferences::get()->getString("/theme/syntax-color-theme", "-none-");
+}
+
+namespace Inkscape::UI::Dialog {
+
+// arbitrarily selected size limits
+constexpr int MAX_POPOVER_HEIGHT = 450;
+constexpr int MAX_POPOVER_WIDTH = 520;
+constexpr int TEXT_MARGIN = 3;
+
+std::unique_ptr<Syntax::TextEditView> AttrDialog::init_text_view(AttrDialog* owner, Syntax::SyntaxMode coloring, bool map)
+{
+ auto edit = Syntax::TextEditView::create(coloring);
+ auto& textview = edit->getTextView();
+ textview.set_wrap_mode(Gtk::WrapMode::WRAP_WORD);
+
+ // this actually sets padding rather than margin and extends textview's background color to the sides
+ textview.set_top_margin(TEXT_MARGIN);
+ textview.set_left_margin(TEXT_MARGIN);
+ textview.set_right_margin(TEXT_MARGIN);
+ textview.set_bottom_margin(TEXT_MARGIN);
+
+ if (map) {
+ textview.signal_map().connect([owner](){
+ // this is not effective: text view recalculates its size on idle, so it's too early to call on 'map';
+ // (note: there's no signal on a TextView to tell us that formatting has been done)
+ // delay adjustment; this will work if UI is fast enough, but at the cost of popup jumping,
+ // but at least it will be sized properly
+ owner->_adjust_size = Glib::signal_timeout().connect([=](){ owner->adjust_popup_edit_size(); return false; }, 50);
+ });
+ }
+
+ return edit;
+}
+
+/**
+ * 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()
+ : DialogBase("/dialogs/attr", "AttrDialog")
+ , _builder(create_builder("attribute-edit-component.glade"))
+ , _scrolled_text_view(get_widget<Gtk::ScrolledWindow>(_builder, "scroll-wnd"))
+ , _content_sw(get_widget<Gtk::ScrolledWindow>(_builder, "content-sw"))
+ , _scrolled_window(get_widget<Gtk::ScrolledWindow>(_builder, "scrolled-wnd"))
+ , _treeView(get_widget<Gtk::TreeView>(_builder, "tree-view"))
+ , _popover(&get_widget<Gtk::Popover>(_builder, "popup"))
+ , _status_box(get_widget<Gtk::Box>(_builder, "status-box"))
+ , _status(get_widget<Gtk::Label>(_builder, "status-label"))
+{
+ // Attribute value editing (with syntax highlighting).
+ using namespace Syntax;
+ _css_edit = init_text_view(this, SyntaxMode::InlineCss, true);
+ _svgd_edit = init_text_view(this, SyntaxMode::SvgPathData, true);
+ _points_edit = init_text_view(this, SyntaxMode::SvgPolyPoints, true);
+ _attr_edit = init_text_view(this, SyntaxMode::PlainText, true);
+
+ // string content editing
+ _text_edit = init_text_view(this, SyntaxMode::PlainText, false);
+ _style_edit = init_text_view(this, SyntaxMode::CssStyle, false);
+
+ set_size_request(20, 15);
+
+ // For text and comment nodes: update XML on the fly, as users type
+ for (auto tv : {&_text_edit->getTextView(), &_style_edit->getTextView()}) {
+ tv->get_buffer()->signal_end_user_action().connect([=]() {
+ if (_repr) {
+ _repr->setContent(tv->get_buffer()->get_text().c_str());
+ setUndo(_("Type text"));
+ }
+ });
+ }
+
+ _store = Gtk::ListStore::create(_attrColumns);
+ _treeView.set_model(_store);
+
+ // high-res aware icon renderer for a trash can
+ auto delete_renderer = manage(new Inkscape::UI::Widget::CellRendererItemIcon());
+ delete_renderer->property_shape_type().set_value("edit-delete");
+ _treeView.append_column("", *delete_renderer);
+ 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);
+ }
+ delete_renderer->signal_activated().connect(sigc::mem_fun(*this, &AttrDialog::onAttrDelete));
+ _treeView.signal_key_press_event().connect(sigc::mem_fun(*this, &AttrDialog::onKeyPressed));
+
+ _nameRenderer = Gtk::make_managed<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);
+ }
+
+ _message_stack = std::make_shared<Inkscape::MessageStack>();
+ _message_context = std::make_unique<Inkscape::MessageContext>(_message_stack);
+ _message_changed_connection = _message_stack->connectChanged([=](MessageType, const char* message) {
+ _status.set_markup(message ? message : "");
+ });
+
+ _valueRenderer = Gtk::make_managed<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), true);
+ _treeView.append_column(_("Value"), *_valueRenderer);
+ _valueCol = _treeView.get_column(2);
+ if (_valueCol) {
+ _valueCol->add_attribute(_valueRenderer->property_text(), _attrColumns._attributeValueRender);
+ }
+
+ set_current_textedit(_attr_edit.get());
+ _scrolled_text_view.set_max_content_height(MAX_POPOVER_HEIGHT);
+
+ auto& apply = get_widget<Gtk::Button>(_builder, "btn-ok");
+ apply.signal_clicked().connect([=]() { valueEditedPop(); });
+
+ auto& cancel = get_widget<Gtk::Button>(_builder, "btn-cancel");
+ cancel.signal_clicked().connect([=](){
+ if (!_value_editing.empty()) {
+ _activeTextView().get_buffer()->set_text(_value_editing);
+ }
+ _popover->popdown();
+ });
+
+ _popover->signal_closed().connect([=]() { popClosed(); });
+ _popover->signal_key_press_event().connect([=](GdkEventKey* ev) { return key_callback(ev); }, false);
+ _popover->hide();
+
+ get_widget<Gtk::Button>(_builder, "btn-truncate").signal_clicked().connect([=](){ truncateDigits(); });
+
+ const int N = 5;
+ _rounding_precision = Inkscape::Preferences::get()->getIntLimited("/dialogs/attrib/precision", 2, 0, N);
+ for (int n = 0; n <= N; ++n) {
+ auto id = '_' + std::to_string(n);
+ auto item = &get_widget<Gtk::MenuItem>(_builder, id.c_str());
+ auto action = [=](){
+ _rounding_precision = n;
+ get_widget<Gtk::Label>(_builder, "precision").set_label(' ' + item->get_label());
+ Inkscape::Preferences::get()->setInt("/dialogs/attrib/precision", n);
+ };
+ item->signal_activate().connect(action);
+
+ if (n == _rounding_precision) {
+ action();
+ }
+ }
+
+ attr_reset_context(0);
+ pack_start(get_widget<Gtk::Box>(_builder, "main-box"), Gtk::PACK_EXPAND_WIDGET);
+ _updating = false;
+}
+
+AttrDialog::~AttrDialog()
+{
+ _current_text_edit = nullptr;
+ _popover->hide();
+
+ // remove itself from the list of node observers
+ setRepr(nullptr);
+}
+
+static int fmt_number(_GMatchInfo const *match, _GString *ret, void *prec)
+{
+ auto number = g_match_info_fetch(match, 1);
+
+ char *end;
+ double val = g_ascii_strtod(number, &end);
+ if (*number && (end == nullptr || end > number)) {
+ auto precision = *static_cast<int*>(prec);
+ auto fmt = Util::format_number(val, precision);
+ g_string_append(ret, fmt.c_str());
+ } else {
+ g_string_append(ret, number);
+ }
+
+ auto text = g_match_info_fetch(match, 2);
+ g_string_append(ret, text);
+
+ g_free(number);
+ g_free(text);
+
+ return false;
+}
+
+Glib::ustring AttrDialog::round_numbers(const Glib::ustring& text, int precision)
+{
+ // match floating point number followed by something else (not a number); repeat
+ static const auto numbers = Glib::Regex::create("([-+]?(?:(?:\\d+\\.?\\d*)|(?:\\.\\d+))(?:[eE][-+]?\\d*)?)([^+\\-0-9]*)", Glib::REGEX_MULTILINE);
+
+ return numbers->replace_eval(text, text.size(), 0, Glib::RegexMatchFlags::REGEX_MATCH_NOTEMPTY, &fmt_number, &precision);
+}
+
+/** Round the selected floating point numbers in the attribute edit popover. */
+void AttrDialog::truncateDigits() const
+{
+ if (!_current_text_edit) {
+ return;
+ }
+
+ auto buffer = _current_text_edit->getTextView().get_buffer();
+ auto start = buffer->begin();
+ auto end = buffer->end();
+
+ bool const had_selection = buffer->get_has_selection();
+ int start_idx = 0, end_idx = 0;
+ if (had_selection) {
+ buffer->get_selection_bounds(start, end);
+ start_idx = start.get_offset();
+ end_idx = end.get_offset();
+ }
+
+ auto text = buffer->get_text(start, end);
+ auto ret = round_numbers(text, _rounding_precision);
+ buffer->erase(start, end);
+ buffer->insert_at_cursor(ret);
+
+ if (had_selection) {
+ // Restore selection but note that its length may have decreased.
+ end_idx -= text.size() - ret.size();
+ if (end_idx < start_idx) {
+ end_idx = start_idx;
+ }
+ buffer->select_range(buffer->get_iter_at_offset(start_idx), buffer->get_iter_at_offset(end_idx));
+ }
+}
+
+void AttrDialog::set_current_textedit(Syntax::TextEditView* edit)
+{
+ _current_text_edit = edit ? edit : _attr_edit.get();
+ _scrolled_text_view.remove();
+ _scrolled_text_view.add(_current_text_edit->getTextView());
+ _scrolled_text_view.show_all();
+}
+
+void AttrDialog::adjust_popup_edit_size()
+{
+ auto vscroll = _scrolled_text_view.get_vadjustment();
+ int height = vscroll->get_upper() + 2 * TEXT_MARGIN;
+ if (height < MAX_POPOVER_HEIGHT) {
+ _scrolled_text_view.set_min_content_height(height);
+ vscroll->set_value(vscroll->get_lower());
+ } else {
+ _scrolled_text_view.set_min_content_height(MAX_POPOVER_HEIGHT);
+ }
+}
+
+bool AttrDialog::key_callback(GdkEventKey* event) {
+ switch (event->keyval) {
+ case GDK_KEY_Return:
+ case GDK_KEY_KP_Enter:
+ if (_popover->is_visible()) {
+ if (event->state & GDK_SHIFT_MASK) {
+ valueEditedPop();
+ return true;
+ }
+ else {
+ // as we type and content grows, resize the popup to accommodate it
+ _adjust_size = Glib::signal_timeout().connect([=](){ adjust_popup_edit_size(); return false; }, 50);
+ }
+ }
+ 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;
+}
+
+void set_mono_class(Gtk::Widget* widget, bool mono)
+{
+ if (!widget) {
+ return;
+ }
+ Glib::ustring class_name = "mono-font";
+ auto style = widget->get_style_context();
+ auto has_class = style->has_class(class_name);
+
+ if (mono && !has_class) {
+ style->add_class(class_name);
+ } else if (!mono && has_class) {
+ style->remove_class(class_name);
+ }
+}
+
+void AttrDialog::set_mono_font(bool mono)
+{
+ set_mono_class(&_treeView, mono);
+}
+
+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));
+}
+
+Gtk::TextView &AttrDialog::_activeTextView() const
+{
+ return _current_text_edit->getTextView();
+}
+
+void AttrDialog::startValueEdit(Gtk::CellEditable *cell, const Glib::ustring &path)
+{
+ _value_path = path;
+ Gtk::TreeIter iter = *_store->get_iter(path);
+ Gtk::TreeModel::Row row = *iter;
+ if (!row || !_repr || !cell) {
+ return;
+ }
+
+ // popover in GTK3 is clipped to dialog window (in a floating dialog); limit size:
+ const int dlg_width = get_allocated_width() - 10;
+ _popover->set_size_request(std::min(MAX_POPOVER_WIDTH, dlg_width), -1);
+
+ auto const attribute = row[_attrColumns._attributeName];
+ bool edit_in_popup =
+#if WITH_GSOURCEVIEW
+ true;
+#else
+ false;
+#endif
+ bool enable_rouding = false;
+
+ if (attribute == "style") {
+ set_current_textedit(_css_edit.get());
+ } else if (attribute == "d" || attribute == "inkscape:original-d") {
+ enable_rouding = true;
+ set_current_textedit(_svgd_edit.get());
+ } else if (attribute == "points") {
+ enable_rouding = true;
+ set_current_textedit(_points_edit.get());
+ } else {
+ set_current_textedit(_attr_edit.get());
+ edit_in_popup = false;
+ }
+
+ // number rounding functionality
+ widget_show(get_widget<Gtk::Box>(_builder, "rounding-box"), enable_rouding);
+
+ _activeTextView().set_size_request(std::min(MAX_POPOVER_WIDTH - 10, dlg_width), -1);
+
+ auto theme = get_syntax_theme();
+
+ auto entry = dynamic_cast<Gtk::Entry*>(cell);
+ int width, height;
+ entry->get_layout()->get_pixel_size(width, height);
+ int colwidth = _valueCol->get_width();
+
+ if (row[_attrColumns._attributeValue] != row[_attrColumns._attributeValueRender] ||
+ edit_in_popup || colwidth - 10 < width)
+ {
+ _value_editing = 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);
+ }
+ if (rect.get_x() >= dlg_width) {
+ rect.set_x(dlg_width - 1);
+ }
+ _popover->set_pointing_to(rect);
+
+ auto current_value = row[_attrColumns._attributeValue];
+ _current_text_edit->setStyle(theme);
+ _current_text_edit->setText(current_value);
+
+ // close in-line entry
+ cell->property_editing_canceled() = true;
+ cell->remove_widget();
+ // cannot dismiss it right away without warning from GTK, so delay it
+ Glib::signal_timeout().connect_once([=](){
+ cell->editing_done(); // only this call will actually remove in-line edit widget
+ cell->remove_widget();
+ }, 0);
+ // and show popup edit instead
+ Glib::signal_timeout().connect_once([=](){ _popover->popup(); }, 10);
+ } else {
+ entry->signal_key_press_event().connect(
+ sigc::bind(sigc::mem_fun(*this, &AttrDialog::onValueKeyPressed), entry));
+ }
+}
+
+void AttrDialog::popClosed()
+{
+ if (!_current_text_edit) {
+ return;
+ }
+ _activeTextView().get_buffer()->set_text("");
+ // delay this resizing, so it is not visible as popover fades out
+ _close_popup = Glib::signal_timeout().connect([=](){ _scrolled_text_view.set_min_content_height(20); return false; }, 250);
+}
+
+/**
+ * @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->removeObserver(*this);
+ Inkscape::GC::release(_repr);
+ _repr = nullptr;
+ }
+ _repr = repr;
+ if (repr) {
+ Inkscape::GC::anchor(_repr);
+ _repr->addObserver(*this);
+
+ // show either attributes or content
+ bool show_content = is_text_or_comment_node(*_repr);
+ if (show_content) {
+ _content_sw.remove();
+ auto type = repr->name();
+ auto elem = repr->parent();
+ if (type && strcmp(type, "string") == 0 && elem && elem->name() && strcmp(elem->name(), "svg:style") == 0) {
+ // editing embedded CSS style
+ _style_edit->setStyle(get_syntax_theme());
+ _content_sw.add(_style_edit->getTextView());
+ } else {
+ _content_sw.add(_text_edit->getTextView());
+ }
+ }
+
+ _repr->synthesizeEvents(*this);
+ _scrolled_window.set_visible(!show_content);
+ _content_sw.set_visible(show_content);
+ }
+}
+
+void AttrDialog::setUndo(Glib::ustring const &event_description)
+{
+ DocumentUndo::done(getDocument(), event_description, INKSCAPE_ICON("dialog-xml-editor"));
+}
+
+/**
+ * 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::notifyAttributeChanged
+ * This is called when the XML has an updated attribute
+ */
+void AttrDialog::notifyAttributeChanged(XML::Node&, GQuark name_, Util::ptr_shared, Util::ptr_shared new_value)
+{
+ if (_updating) {
+ return;
+ }
+
+ auto const name = g_quark_to_string(name_);
+
+ Glib::ustring renderval;
+ if (new_value) {
+ renderval = prepare_rendervalue(new_value.pointer());
+ }
+ for (auto&& iter : _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.pointer();
+ row[_attrColumns._attributeValueRender] = renderval;
+ new_value = Util::ptr_shared(); // 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.pointer();
+ 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];
+ {
+ this->_store->erase(row);
+ this->_repr->removeAttribute(name);
+ this->setUndo(_("Delete attribute"));
+ }
+ }
+}
+
+void AttrDialog::notifyContentChanged(XML::Node &,
+ Util::ptr_shared,
+ Util::ptr_shared new_content)
+{
+ auto textview = dynamic_cast<Gtk::TextView *>(_content_sw.get_child());
+ if (!textview) {
+ return;
+ }
+ auto buffer = textview->get_buffer();
+ if (!buffer->get_modified()) {
+ auto str = new_content.pointer();
+ buffer->set_text(str ? str : "");
+ }
+ buffer->set_modified(false);
+}
+
+
+/**
+ * @brief AttrDialog::onKeyPressed
+ * @param event
+ * @return true
+ * Delete or create elements based on key presses
+ */
+bool AttrDialog::onKeyPressed(GdkEventKey *event)
+{
+ bool ret = false;
+ if (!_repr) {
+ return ret;
+ }
+ auto selection = _treeView.get_selection();
+ auto row = *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];
+ _store->erase(row);
+ _repr->removeAttribute(name);
+ 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 = _store->prepend();
+ Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iter;
+ _treeView.set_cursor(path, *_nameCol, true);
+ grab_focus();
+ ret = true;
+ } break;
+
+ case GDK_KEY_Return:
+ case GDK_KEY_KP_Enter:
+ if (_popover->is_visible() && (event->state & GDK_SHIFT_MASK)) {
+ valueEditedPop();
+ 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_Return:
+ case GDK_KEY_KP_Enter:
+ if (event->state & GDK_SHIFT_MASK) {
+ int pos = entry->get_position();
+ entry->insert_text("\n", 1, pos);
+ entry->set_position(pos + 1);
+ ret = true;
+ }
+ break;
+ case GDK_KEY_Tab:
+ case GDK_KEY_KP_Tab:
+ entry->editing_done();
+ ret = true;
+ break;
+ }
+ return ret;
+}
+
+void AttrDialog::storeMoveToNext(Gtk::TreeModel::Path modelpath)
+{
+ auto selection = _treeView.get_selection();
+ auto iter = *(selection->get_selected());
+ auto path = static_cast<Gtk::TreeModel::Path>(iter);
+ Gtk::TreeViewColumn *focus_column;
+ _treeView.get_cursor(path, focus_column);
+ if (path == modelpath && focus_column == _treeView.get_column(1)) {
+ _treeView.set_cursor(modelpath, *_valueCol, true);
+ }
+}
+
+/**
+ * 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);
+ auto modelpath = static_cast<Gtk::TreeModel::Path>(iter);
+ Gtk::TreeModel::Row row = *iter;
+ if(row && this->_repr) {
+ Glib::ustring old_name = row[_attrColumns._attributeName];
+ if (old_name == name) {
+ Glib::signal_timeout().connect_once([=](){ storeMoveToNext(modelpath); }, 50);
+ grab_focus();
+ 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;
+ Glib::signal_timeout().connect_once([=](){ storeMoveToNext(modelpath); }, 50);
+ setUndo(_("Rename attribute"));
+ }
+}
+
+void AttrDialog::valueEditedPop()
+{
+ valueEdited(_value_path, _current_text_edit->getText());
+ _value_editing.clear();
+ _popover->popdown();
+}
+
+/**
+ * @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)
+{
+ if (!getDesktop()) {
+ return;
+ }
+
+ Gtk::TreeModel::Row row = *_store->get_iter(path);
+ if (row && _repr) {
+ Glib::ustring name = row[_attrColumns._attributeName];
+ Glib::ustring old_value = row[_attrColumns._attributeValue];
+ if (old_value == value || name.empty()) {
+ return;
+ }
+
+ _repr->setAttributeOrRemoveIfEmpty(name, value);
+
+ if (!value.empty()) {
+ row[_attrColumns._attributeValue] = value;
+ Glib::ustring renderval = prepare_rendervalue(value.c_str());
+ row[_attrColumns._attributeValueRender] = renderval;
+ }
+ setUndo(_("Change attribute value"));
+ }
+}
+
+} // namespace Inkscape::UI::Dialog
diff --git a/src/ui/dialog/attrdialog.h b/src/ui/dialog/attrdialog.h
new file mode 100644
index 0000000..e85d42d
--- /dev/null
+++ b/src/ui/dialog/attrdialog.h
@@ -0,0 +1,153 @@
+// 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 <gtkmm/builder.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 <memory>
+
+#include "helper/auto-connection.h"
+#include "inkscape-application.h"
+#include "message.h"
+#include "ui/dialog/dialog-base.h"
+#include "ui/syntax.h"
+#include "xml/node-observer.h"
+
+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 DialogBase
+ , private XML::NodeObserver
+{
+public:
+ AttrDialog();
+ ~AttrDialog() override;
+
+ void setRepr(Inkscape::XML::Node * repr);
+ Gtk::ScrolledWindow& get_scrolled_window() { return _scrolled_window; }
+ Gtk::Box& get_status_box() { return _status_box; }
+ void adjust_popup_edit_size();
+ void set_mono_font(bool mono);
+
+private:
+ // builder comes first, so it is initialized before other data members
+ Glib::RefPtr<Gtk::Builder> _builder;
+
+ // 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::Popover *_popover;
+ Glib::ustring _value_path;
+ Glib::ustring _value_editing;
+ // Status bar
+ std::shared_ptr<Inkscape::MessageStack> _message_stack;
+ std::unique_ptr<Inkscape::MessageContext> _message_context;
+ // Widgets
+ Gtk::ScrolledWindow& _scrolled_window;
+ Gtk::ScrolledWindow& _scrolled_text_view;
+ // Variables - Inkscape
+ Inkscape::XML::Node* _repr{nullptr};
+ Gtk::Box& _status_box;
+ Gtk::Label& _status;
+ bool _updating = true;
+
+ // Helper functions
+ void setUndo(Glib::ustring const &event_description);
+ /**
+ * Sets the XML status bar, depending on which attr is selected.
+ */
+ void attr_reset_context(gint attr);
+
+ /**
+ * Signal handlers
+ */
+ auto_connection _message_changed_connection;
+ 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 truncateDigits() const;
+ 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 valueEditedPop();
+ void storeMoveToNext(Gtk::TreeModel::Path modelpath);
+
+private:
+ // Text/comment nodes
+ Gtk::ScrolledWindow& _content_sw;
+ std::unique_ptr<Syntax::TextEditView> _text_edit; // text content editing (plain text)
+ std::unique_ptr<Syntax::TextEditView> _style_edit; // embedded CSS style (with syntax coloring)
+
+ // Attribute value editing
+ std::unique_ptr<Syntax::TextEditView> _css_edit; // in-line CSS style
+ std::unique_ptr<Syntax::TextEditView> _svgd_edit; // SVG path data
+ std::unique_ptr<Syntax::TextEditView> _points_edit; // points in a <polygon> or <polyline>
+ std::unique_ptr<Syntax::TextEditView> _attr_edit; // all other attributes (plain text)
+ Syntax::TextEditView* _current_text_edit = nullptr; // current text edit for attribute value editing
+ auto_connection _adjust_size;
+ auto_connection _close_popup;
+ int _rounding_precision = 0;
+
+ bool key_callback(GdkEventKey* event);
+ void notifyAttributeChanged(XML::Node &repr, GQuark name, Util::ptr_shared old_value, Util::ptr_shared new_value) final;
+ void notifyContentChanged(XML::Node &node, Util::ptr_shared old_content, Util::ptr_shared new_content) final;
+ static Glib::ustring round_numbers(const Glib::ustring& text, int precision);
+ Gtk::TextView &_activeTextView() const;
+ void set_current_textedit(Syntax::TextEditView* edit);
+ static std::unique_ptr<Syntax::TextEditView> init_text_view(AttrDialog* owner, Syntax::SyntaxMode coloring, bool map);
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // SEEN_UI_DIALOGS_ATTRDIALOG_H
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..79e272e
--- /dev/null
+++ b/src/ui/dialog/clonetiler.cpp
@@ -0,0 +1,2821 @@
+// 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/grid.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/sizegroup.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 "display/cairo-utils.h"
+#include "display/drawing-context.h"
+#include "display/drawing.h"
+
+#include "ui/icon-loader.h"
+
+#include "object/algorithms/unclump.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"
+#include "xml/href-attribute-helper.h"
+
+using Inkscape::DocumentUndo;
+using Inkscape::Util::unit_table;
+
+namespace Inkscape {
+namespace UI {
+
+namespace Widget {
+/**
+ * Simple extension of Gtk::CheckButton, which adds a flag
+ * to indicate whether the box should be unticked when reset
+ */
+class CheckButtonInternal : public Gtk::CheckButton {
+ private:
+ bool _uncheckable = false;
+ public:
+ CheckButtonInternal() = default;
+
+ CheckButtonInternal(const Glib::ustring &label)
+ : Gtk::CheckButton(label)
+ {}
+
+ void set_uncheckable(const bool val = true) { _uncheckable = val; }
+ bool get_uncheckable() const { return _uncheckable; }
+};
+}
+
+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()
+ : DialogBase("/dialogs/clonetiler/", "CloneTiler")
+ , table_row_labels(nullptr)
+{
+ set_spacing(0);
+
+ {
+ auto prefs = Inkscape::Preferences::get();
+
+ auto mainbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4));
+ mainbox->set_homogeneous(false);
+ mainbox->set_border_width(6);
+
+ pack_start(*mainbox, true, true, 0);
+
+ nb = Gtk::manage(new Gtk::Notebook());
+ mainbox->pack_start(*nb, false, false, 0);
+
+
+ // Symmetry
+ {
+ auto 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, nullptr);
+
+ for (const auto & sg : sym_groups) {
+ // Add the description of the symgroup to a new row
+ combo->append(sg.label);
+ }
+
+ vb->pack_start(*combo, 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::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL);
+
+ // Shift
+ {
+ auto vb = new_tab(nb, _("S_hift"));
+
+ auto table = table_x_y_rand (3);
+ vb->pack_start(*table, false, false, 0);
+
+ // X
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ // TRANSLATORS: "shift" means: the tiles will be shifted (offset) horizontally by this amount
+ // xgettext:no-c-format
+ l->set_markup(_("<b>Shift X:</b>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*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
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ // TRANSLATORS: "shift" means: the tiles will be shifted (offset) vertically by this amount
+ // xgettext:no-c-format
+ l->set_markup(_("<b>Shift Y:</b>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*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
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<b>Exponent:</b>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*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
+ auto l = Gtk::manage(new Gtk::Label(""));
+ // TRANSLATORS: "Alternate" is a verb here
+ l->set_markup(_("<small>Alternate:</small>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*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
+ auto l = Gtk::manage(new Gtk::Label(""));
+ // TRANSLATORS: "Cumulate" is a verb here
+ l->set_markup(_("<small>Cumulate:</small>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*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
+ auto l = Gtk::manage(new Gtk::Label(""));
+ // TRANSLATORS: "Cumulate" is a verb here
+ l->set_markup(_("<small>Exclude tile:</small>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*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
+ {
+ auto vb = new_tab(nb, _("Sc_ale"));
+
+ auto table = table_x_y_rand(2);
+ vb->pack_start(*table, false, false, 0);
+
+ // X
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<b>Scale X:</b>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*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
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<b>Scale Y:</b>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*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
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<b>Exponent:</b>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*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)
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<b>Base:</b>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*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
+ auto l = Gtk::manage(new Gtk::Label(""));
+ // TRANSLATORS: "Alternate" is a verb here
+ l->set_markup(_("<small>Alternate:</small>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*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
+ auto l = Gtk::manage(new Gtk::Label(""));
+ // TRANSLATORS: "Cumulate" is a verb here
+ l->set_markup(_("<small>Cumulate:</small>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*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
+ {
+ auto vb = new_tab(nb, _("_Rotation"));
+
+ auto table = table_x_y_rand (1);
+ vb->pack_start(*table, false, false, 0);
+
+ // Angle
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<b>Angle:</b>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*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
+ auto l = Gtk::manage(new Gtk::Label(""));
+ // TRANSLATORS: "Alternate" is a verb here
+ l->set_markup(_("<small>Alternate:</small>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*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
+ auto l = Gtk::manage(new Gtk::Label(""));
+ // TRANSLATORS: "Cumulate" is a verb here
+ l->set_markup(_("<small>Cumulate:</small>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*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
+ {
+ auto vb = new_tab(nb, _("_Blur & opacity"));
+
+ auto table = table_x_y_rand(1);
+ vb->pack_start(*table, false, false, 0);
+
+
+ // Blur
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<b>Blur:</b>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*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
+ auto l = Gtk::manage(new Gtk::Label(""));
+ // TRANSLATORS: "Alternate" is a verb here
+ l->set_markup(_("<small>Alternate:</small>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*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
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<b>Opacity:</b>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*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
+ auto l = Gtk::manage(new Gtk::Label(""));
+ // TRANSLATORS: "Alternate" is a verb here
+ l->set_markup(_("<small>Alternate:</small>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*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
+ {
+ auto vb = new_tab(nb, _("Co_lor"));
+
+ {
+ auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0));
+ hb->set_homogeneous(false);
+
+ auto l = Gtk::manage(new Gtk::Label(_("Initial color: ")));
+ hb->pack_start(*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));
+
+ hb->pack_start(*color_picker, false, false, 0);
+
+ vb->pack_start(*hb, false, false, 0);
+ }
+
+
+ auto table = table_x_y_rand(3);
+ vb->pack_start(*table, false, false, 0);
+
+ // Hue
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<b>H:</b>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*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
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<b>S:</b>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*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
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<b>L:</b>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*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
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<small>Alternate:</small>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*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
+ {
+ auto vb = new_tab(nb, _("_Trace"));
+ {
+ auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN));
+ hb->set_border_width(4);
+ hb->set_homogeneous(false);
+ vb->pack_start(*hb, false, false, 0);
+
+ _b = Gtk::manage(new UI::Widget::CheckButtonInternal(_("Trace the drawing under the clones/sprayed items")));
+ _b->set_uncheckable();
+ 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"));
+ hb->pack_start(*_b, false, false, 0);
+ _b->signal_toggled().connect(sigc::mem_fun(*this, &CloneTiler::do_pick_toggled));
+ }
+
+ {
+ auto vvb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 0));
+ vvb->set_homogeneous(false);
+ vb->pack_start(*vvb, false, false, 0);
+ _dotrace = vvb;
+
+ {
+ auto frame = Gtk::manage(new Gtk::Frame(_("1. Pick from the drawing:")));
+ frame->set_shadow_type(Gtk::SHADOW_NONE);
+ vvb->pack_start(*frame, false, false, 0);
+
+ auto table = Gtk::manage(new Gtk::Grid());
+ table->set_row_spacing(4);
+ table->set_column_spacing(6);
+ table->set_border_width(4);
+ frame->add(*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);
+ }
+
+ }
+
+ {
+ auto frame = Gtk::manage(new Gtk::Frame(_("2. Tweak the picked value:")));
+ frame->set_shadow_type(Gtk::SHADOW_NONE);
+ vvb->pack_start(*frame, false, false, VB_MARGIN);
+
+ auto table = Gtk::manage(new Gtk::Grid());
+ table->set_row_spacing(4);
+ table->set_column_spacing(6);
+ table->set_border_width(4);
+ frame->add(*table);
+
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("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);
+ }
+
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("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);
+ }
+
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("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);
+ }
+ }
+
+ {
+ auto frame = Gtk::manage(new Gtk::Frame(_("3. Apply the value to the clones':")));
+ frame->set_shadow_type(Gtk::SHADOW_NONE);
+ vvb->pack_start(*frame, false, false, 0);
+
+ auto table = Gtk::manage(new Gtk::Grid());
+ table->set_row_spacing(4);
+ table->set_column_spacing(6);
+ table->set_border_width(4);
+ frame->add(*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"));
+ }
+ }
+ vvb->set_sensitive(prefs->getBool(prefs_path + "dotrace"));
+ }
+ }
+
+ {
+ auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN));
+ hb->set_homogeneous(false);
+ mainbox->pack_start(*hb, false, false, 0);
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("Apply to tiled clones:"));
+ hb->pack_start(*l, false, false, 0);
+ }
+ // Rows/columns, width/height
+ {
+ auto table = Gtk::manage(new Gtk::Grid());
+ table->set_row_spacing(4);
+ table->set_column_spacing(6);
+
+ table->set_border_width(VB_MARGIN);
+ mainbox->pack_start(*table, false, false, 0);
+
+ {
+ {
+ 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);
+ sb->set_name("row");
+ table_attach(table, sb, 0.0f, 1, 2);
+ _rowscols.push_back(sb);
+
+ 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;");
+ table_attach(table, l, 0.0f, 1, 3);
+ _rowscols.push_back(l);
+ }
+
+ {
+ 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);
+ table_attach(table, sb, 0.0f, 1, 4);
+ _rowscols.push_back(sb);
+
+ a->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::xy_changed), a, "imax"));
+ }
+ }
+
+ {
+ // 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);
+ table_attach(table, e, 0.0f, 2, 2);
+ _widthheight.push_back(e);
+ 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;");
+ table_attach(table, l, 0.0f, 2, 3);
+ _widthheight.push_back(l);
+ }
+
+ {
+ // 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);
+ table_attach(table, e, 0.0f, 2, 4);
+ _widthheight.push_back(e);
+ fill_height->signal_value_changed().connect(sigc::mem_fun(*this, &CloneTiler::fill_height_changed));
+ }
+
+ table_attach(table, unit_menu, 0.0f, 2, 5);
+ _widthheight.push_back(unit_menu);
+ }
+
+ // 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, radio, 0.0, 1, 1);
+
+ if (!prefs->getBool(prefs_path + "fillrect")) {
+ radio->set_active(true);
+ switch_to_create();
+ }
+ radio->signal_toggled().connect(sigc::mem_fun(*this, &CloneTiler::switch_to_create));
+ }
+ {
+ 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, radio, 0.0, 2, 1);
+
+ if (prefs->getBool(prefs_path + "fillrect")) {
+ radio->set_active(true);
+ switch_to_fill();
+ }
+ radio->signal_toggled().connect(sigc::mem_fun(*this, &CloneTiler::switch_to_fill));
+ }
+ }
+
+
+ // Use saved pos
+ {
+ auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN));
+ mainbox->pack_start(*hb, false, false, 0);
+
+ _cb_keep_bbox = Gtk::manage(new UI::Widget::CheckButtonInternal(_("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::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN));
+ hb->set_homogeneous(false);
+ mainbox->pack_end(*hb, false, false, 0);
+ auto l = Gtk::manage(new Gtk::Label(""));
+ _status = l;
+ hb->pack_start(*l, false, false, 0);
+ }
+
+ // Buttons
+ {
+ auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN));
+ hb->set_homogeneous(false);
+ mainbox->pack_start(*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));
+ hb->pack_end(*b, false, false, 0);
+ }
+
+ { // buttons which are enabled only when there are tiled clones
+ auto sb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4));
+ sb->set_homogeneous(false);
+ hb->pack_end(*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));
+ sb->pack_end(*b, 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));
+ sb->pack_end(*b, 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));
+ hb->pack_start(*b, false, false, 0);
+ }
+ }
+
+ mainbox->show_all();
+ }
+
+ show_all();
+}
+
+CloneTiler::~CloneTiler ()
+{
+ selectChangedConn.disconnect();
+ externChangedConn.disconnect();
+ color_changed_connection.disconnect();
+}
+
+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()) {
+ _buttons_on_tiles->set_sensitive(false);
+ _status->set_markup(_("<small>Nothing selected.</small>"));
+ return;
+ }
+
+ if (boost::distance(selection->items()) > 1) {
+ _buttons_on_tiles->set_sensitive(false);
+ _status->set_markup(_("<small>More than one object selected.</small>"));
+ return;
+ }
+
+ guint n = number_of_clones(selection->singleItem());
+ if (n > 0) {
+ _buttons_on_tiles->set_sensitive(true);
+ gchar *sta = g_strdup_printf (_("<small>Object has <b>%d</b> tiled clones.</small>"), n);
+ _status->set_markup(sta);
+ g_free (sta);
+ } else {
+ _buttons_on_tiles->set_sensitive(false);
+ _status->set_markup(_("<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"));
+ }
+
+ auto href = Inkscape::getHrefAttribute(*tile->getRepr()).second;
+
+ if (is<SPUse>(tile) &&
+ href && (!id_href || !strcmp(id_href, 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) {
+ auto item = 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 selection = getSelection();
+ if (!selection)
+ return;
+
+ // check if something is selected
+ if (selection->isEmpty() || boost::distance(selection->items()) > 1) {
+ getDesktop()->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);
+ }
+ }
+
+ getDocument()->ensureUpToDate();
+ reverse(to_unclump.begin(),to_unclump.end());
+ ::unclump (to_unclump);
+
+ DocumentUndo::done(getDocument(), _("Unclump tiled clones"), INKSCAPE_ICON("dialog-tile-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*/)
+{
+ auto selection = getSelection();
+ if (!selection)
+ return;
+
+ // check if something is selected
+ if (selection->isEmpty() || boost::distance(selection->items()) > 1) {
+ getDesktop()->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(getDocument(), _("Delete tiled clones"), INKSCAPE_ICON("dialog-tile-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()
+{
+ auto desktop = getDesktop();
+ auto selection = getSelection();
+ if (!selection)
+ return;
+
+ // 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
+ _status->set_markup(_("<small>Creating tiled clones...</small>"));
+ _status->queue_draw();
+
+ 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 = getDocument()->getDocumentScale().inverse();
+ double scale_units = scale[Geom::X]; // Use just x direction....
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ 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);
+
+ auto item = cast<SPItem>(obj);
+ if (dotrace) {
+ trace_setup(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 = obj_repr->getAttributeDouble("inkscape:tile-cx", 0);
+ double cy = obj_repr->getAttributeDouble("inkscape:tile-cy", 0);
+ center = Geom::Point (cx, cy);
+
+ w = obj_repr->getAttributeDouble("inkscape:tile-w", w);
+ h = obj_repr->getAttributeDouble("inkscape:tile-h", h);
+ x0 = obj_repr->getAttributeDouble("inkscape:tile-x0", x0);
+ y0 = obj_repr->getAttributeDouble("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());
+
+ obj_repr->setAttributeSvgDouble("inkscape:tile-cx", center[Geom::X]);
+ obj_repr->setAttributeSvgDouble("inkscape:tile-cy", center[Geom::Y]);
+ obj_repr->setAttributeSvgDouble("inkscape:tile-w", w);
+ obj_repr->setAttributeSvgDouble("inkscape:tile-h", h);
+ obj_repr->setAttributeSvgDouble("inkscape:tile-x0", x0);
+ obj_repr->setAttributeSvgDouble("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;
+ }
+
+ clone->setAttributeOrRemoveIfEmpty("transform", sp_svg_transform_write(t));
+
+ if (opacity < 1.0) {
+ clone->setAttributeCssDouble("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);
+ auto item = cast<SPItem>(clone_object);
+ double radius = blur * perimeter_original * t.descrim();
+ // this is necessary for all newly added clones to have correct bboxes,
+ // otherwise filters won't work:
+ desktop->getDocument()->ensureUpToDate();
+ SPFilter *constructed = new_filter_gaussian_blur(desktop->getDocument(), radius, t.descrim());
+ constructed->update_filter_region(item);
+ sp_style_set_property_url (clone_object, "filter", constructed, false);
+ }
+
+ if (center_set) {
+ SPObject *clone_object = desktop->getDocument()->getObjectByRepr(clone);
+ auto item = 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(getDocument(), _("Create tiled clones"), INKSCAPE_ICON("dialog-tile-clones"));
+}
+
+Gtk::Box * CloneTiler::new_tab(Gtk::Notebook *nb, const gchar *label)
+{
+ auto l = Gtk::manage(new Gtk::Label(label, true));
+ auto vb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, VB_MARGIN));
+ vb->set_homogeneous(false);
+ vb->set_border_width(VB_MARGIN);
+ nb->append_page(*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 UI::Widget::CheckButtonInternal());
+ 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_uncheckable();
+
+ 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_oneable();
+ } else {
+ sb->set_zeroable();
+ }
+ }
+
+ {
+ 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(Gtk::Widget *w)
+{
+ if (w) {
+ auto sb = dynamic_cast<Inkscape::UI::Widget::SpinButton *>(w);
+ auto tb = dynamic_cast<Inkscape::UI::Widget::CheckButtonInternal *>(w);
+
+ {
+ if (sb && sb->get_zeroable()) { // spinbutton
+ auto a = sb->get_adjustment();
+ a->set_value(0);
+ }
+ }
+ {
+ if (sb && sb->get_oneable()) { // spinbutton
+ auto a = sb->get_adjustment();
+ a->set_value(1);
+ }
+ }
+ {
+ if (tb && tb->get_uncheckable()) { // checkbox
+ tb->set_active(false);
+ }
+ }
+ }
+
+ auto container = dynamic_cast<Gtk::Container *>(w);
+
+ if (container) {
+ auto c = container->get_children();
+ for (auto i : c) {
+ reset_recursive(i);
+ }
+ }
+}
+
+void CloneTiler::reset()
+{
+ reset_recursive(this);
+}
+
+void CloneTiler::table_attach(Gtk::Grid *table, Gtk::Widget *widget, float align, int row, int col)
+{
+ widget->set_halign(Gtk::ALIGN_FILL);
+ widget->set_valign(Gtk::ALIGN_CENTER);
+ table->attach(*widget, col, row, 1, 1);
+}
+
+Gtk::Grid * CloneTiler::table_x_y_rand(int values)
+{
+ auto table = Gtk::manage(new Gtk::Grid());
+ table->set_row_spacing(6);
+ table->set_column_spacing(8);
+
+ table->set_border_width(VB_MARGIN);
+
+ {
+ auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0));
+ hb->set_homogeneous(false);
+
+ auto i = Glib::wrap(sp_get_icon_image("object-rows", GTK_ICON_SIZE_MENU));
+ hb->pack_start(*i, false, false, 2);
+
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<small>Per row:</small>"));
+ hb->pack_start(*l, false, false, 2);
+
+ table_attach(table, hb, 0, 1, 2);
+ }
+
+ {
+ auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0));
+ hb->set_homogeneous(false);
+
+ auto i = Glib::wrap(sp_get_icon_image("object-columns", GTK_ICON_SIZE_MENU));
+ hb->pack_start(*i, false, false, 2);
+
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<small>Per column:</small>"));
+ hb->pack_start(*l, false, false, 2);
+
+ table_attach(table, hb, 0, 1, 3);
+ }
+
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<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()
+{
+ for (auto w : _rowscols) {
+ w->set_sensitive(true);
+ }
+ for (auto w : _widthheight) {
+ w->set_sensitive(false);
+ }
+
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool(prefs_path + "fillrect", false);
+}
+
+
+void CloneTiler::switch_to_fill()
+{
+ for (auto w : _rowscols) {
+ w->set_sensitive(false);
+ }
+ for (auto w : _widthheight) {
+ w->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);
+ fill_width->set_value(width_value);
+ fill_height->set_value(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) {
+ _dotrace->set_sensitive(active);
+ }
+}
+
+void CloneTiler::show_page_trace()
+{
+ nb->set_current_page(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..f6ef6e0
--- /dev/null
+++ b/src/ui/dialog/clonetiler.h
@@ -0,0 +1,208 @@
+// 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/dialog/dialog-base.h"
+#include "ui/widget/color-picker.h"
+
+namespace Gtk {
+ class Box;
+ class ComboBox;
+ class Grid;
+ class Notebook;
+ class SizeGroup;
+ class ToggleButton;
+}
+
+class SPItem;
+class SPObject;
+
+namespace Geom {
+ class Rect;
+ class Affine;
+}
+
+namespace Inkscape {
+namespace UI {
+
+namespace Widget {
+ class CheckButtonInternal;
+ class UnitMenu;
+}
+
+namespace Dialog {
+
+class CloneTiler : public DialogBase
+{
+public:
+ CloneTiler();
+ ~CloneTiler() override;
+
+ void show_page_trace();
+
+protected:
+ enum PickType
+ {
+ PICK_COLOR,
+ PICK_OPACITY,
+ PICK_R,
+ PICK_G,
+ PICK_B,
+ PICK_H,
+ PICK_S,
+ PICK_L
+ };
+
+ Gtk::Box * new_tab(Gtk::Notebook *nb, const gchar *label);
+ Gtk::Grid * 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(Gtk::Grid *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(Gtk::Widget *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:
+ UI::Widget::CheckButtonInternal *_b;
+ UI::Widget::CheckButtonInternal *_cb_keep_bbox;
+ Gtk::Notebook *nb = nullptr;
+ Inkscape::UI::Widget::ColorPicker *color_picker;
+ Glib::RefPtr<Gtk::SizeGroup> table_row_labels;
+ Inkscape::UI::Widget::UnitMenu *unit_menu;
+
+ Glib::RefPtr<Gtk::Adjustment> fill_width;
+ Glib::RefPtr<Gtk::Adjustment> fill_height;
+
+ sigc::connection selectChangedConn;
+ sigc::connection externChangedConn;
+ sigc::connection color_changed_connection;
+ sigc::connection unitChangedConn;
+
+ Gtk::Box *_buttons_on_tiles;
+ Gtk::Box *_dotrace;
+ Gtk::Label *_status;
+ std::vector<Gtk::Widget*> _rowscols;
+ std::vector<Gtk::Widget*> _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..6ed95c4
--- /dev/null
+++ b/src/ui/dialog/color-item.cpp
@@ -0,0 +1,506 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include "color-item.h"
+
+#include <cstdint>
+#include <cairomm/cairomm.h>
+#include <glibmm/convert.h>
+#include <glibmm/i18n.h>
+#include <gdkmm/general.h>
+
+#include "helper/sigc-track-obj.h"
+#include "inkscape-preferences.h"
+#include "io/resource.h"
+#include "io/sys.h"
+#include "object/sp-gradient.h"
+#include "svg/svg-color.h"
+#include "hsluv.h"
+#include "display/cairo-utils.h"
+#include "desktop-style.h"
+#include "actions/actions-tools.h"
+#include "message-context.h"
+#include "ui/dialog/dialog-base.h"
+#include "ui/dialog/dialog-container.h"
+#include "ui/icon-names.h"
+
+namespace {
+
+class Globals
+{
+ Globals()
+ {
+ load_removecolor();
+ load_mimetargets();
+ }
+
+ void load_removecolor()
+ {
+ auto path_utf8 = (Glib::ustring)Inkscape::IO::Resource::get_path(Inkscape::IO::Resource::SYSTEM, Inkscape::IO::Resource::PIXMAPS, "remove-color.png");
+ auto path = Glib::filename_from_utf8(path_utf8);
+ auto pixbuf = Gdk::Pixbuf::create_from_file(path);
+ if (!pixbuf) {
+ g_warning("Null pixbuf for %p [%s]", path.c_str(), path.c_str());
+ }
+ removecolor = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, 1);
+ }
+
+ void load_mimetargets()
+ {
+ auto &mimetypes = PaintDef::getMIMETypes();
+ mimetargets.reserve(mimetypes.size());
+ for (int i = 0; i < mimetypes.size(); i++) {
+ mimetargets.emplace_back(mimetypes[i], (Gtk::TargetFlags)0, i);
+ }
+ }
+
+public:
+ static Globals &get()
+ {
+ static Globals instance;
+ return instance;
+ }
+
+ // The "remove-color" image.
+ Cairo::RefPtr<Cairo::ImageSurface> removecolor;
+
+ // The MIME targets for drag and drop, in the format expected by GTK.
+ std::vector<Gtk::TargetEntry> mimetargets;
+};
+
+} // namespace
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+ColorItem::ColorItem(PaintDef const &paintdef, DialogBase *dialog)
+ : dialog(dialog)
+{
+ if (paintdef.get_type() == PaintDef::RGB) {
+ pinned_default = false;
+ data = RGBData{paintdef.get_rgb()};
+ } else {
+ pinned_default = true;
+ data = NoneData{};
+ }
+ description = paintdef.get_description();
+ color_id = paintdef.get_color_id();
+
+ common_setup();
+}
+
+ColorItem::ColorItem(SPGradient *gradient, DialogBase *dialog)
+ : dialog(dialog)
+{
+ data = GradientData{gradient};
+ description = gradient->defaultLabel();
+ color_id = gradient->getId();
+
+ gradient->connectRelease(SIGC_TRACKING_ADAPTOR([this] (SPObject*) {
+ boost::get<GradientData>(data).gradient = nullptr;
+ }, *this));
+
+ gradient->connectModified(SIGC_TRACKING_ADAPTOR([this] (SPObject *obj, unsigned flags) {
+ if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) {
+ cache_dirty = true;
+ queue_draw();
+ }
+ description = obj->defaultLabel();
+ _signal_modified.emit();
+ if (is_pinned() != was_grad_pinned) {
+ was_grad_pinned = is_pinned();
+ _signal_pinned.emit();
+ }
+ }, *this));
+
+ was_grad_pinned = is_pinned();
+ common_setup();
+}
+
+void ColorItem::common_setup()
+{
+ set_name("ColorItem");
+ set_tooltip_text(description);
+ add_events(Gdk::ENTER_NOTIFY_MASK |
+ Gdk::LEAVE_NOTIFY_MASK |
+ Gdk::BUTTON_PRESS_MASK |
+ Gdk::BUTTON_RELEASE_MASK);
+ drag_source_set(Globals::get().mimetargets, Gdk::BUTTON1_MASK, Gdk::ACTION_MOVE | Gdk::ACTION_COPY);
+}
+
+void ColorItem::set_pinned_pref(const std::string &path)
+{
+ pinned_pref = path + "/pinned/" + color_id;
+}
+
+void ColorItem::draw_color(Cairo::RefPtr<Cairo::Context> const &cr, int w, int h) const
+{
+ if (boost::get<NoneData>(&data)) {
+ if (auto surface = Globals::get().removecolor) {
+ const auto device_scale = get_scale_factor();
+ cr->save();
+ cr->scale((double)w / surface->get_width() / device_scale, (double)h / surface->get_height() / device_scale);
+ cr->set_source(surface, 0, 0);
+ cr->paint();
+ cr->restore();
+ }
+ } else if (auto rgbdata = boost::get<RGBData>(&data)) {
+ auto [r, g, b] = rgbdata->rgb;
+ cr->set_source_rgb(r / 255.0, g / 255.0, b / 255.0);
+ cr->paint();
+ } else if (auto graddata = boost::get<GradientData>(&data)) {
+ // Gradient pointer may be null if the gradient was destroyed.
+ auto grad = graddata->gradient;
+ if (!grad) return;
+
+ auto pat_checkerboard = Cairo::RefPtr<Cairo::Pattern>(new Cairo::Pattern(ink_cairo_pattern_create_checkerboard(), true));
+ auto pat_gradient = Cairo::RefPtr<Cairo::Pattern>(new Cairo::Pattern(grad->create_preview_pattern(w), true));
+
+ cr->set_source(pat_checkerboard);
+ cr->paint();
+ cr->set_source(pat_gradient);
+ cr->paint();
+ }
+}
+
+bool ColorItem::on_draw(Cairo::RefPtr<Cairo::Context> const &cr)
+{
+ auto w = get_width();
+ auto h = get_height();
+
+ // Only using caching for none and gradients. None is included because the image is huge.
+ bool use_cache = boost::get<NoneData>(&data) || boost::get<GradientData>(&data);
+
+ if (use_cache) {
+ auto scale = get_scale_factor();
+ // Ensure cache exists and has correct size.
+ if (!cache || cache->get_width() != w * scale || cache->get_height() != h * scale) {
+ cache = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, w * scale, h * scale);
+ cairo_surface_set_device_scale(cache->cobj(), scale, scale);
+ cache_dirty = true;
+ }
+ // Ensure cache contents is up-to-date.
+ if (cache_dirty) {
+ draw_color(Cairo::Context::create(cache), w * scale, h * scale);
+ cache_dirty = false;
+ }
+ // Paint from cache.
+ cr->set_source(cache, 0, 0);
+ cr->paint();
+ } else {
+ // Paint directly.
+ draw_color(cr, w, h);
+ }
+
+ // Draw fill/stroke indicators.
+ if (is_fill || is_stroke) {
+ double const lightness = Hsluv::rgb_to_perceptual_lightness(average_color());
+ auto [gray, alpha] = Hsluv::get_contrasting_color(lightness);
+ cr->set_source_rgba(gray, gray, gray, alpha);
+
+ // Scale so that the square -1...1 is the biggest possible square centred in the widget.
+ auto minwh = std::min(w, h);
+ cr->translate((w - minwh) / 2.0, (h - minwh) / 2.0);
+ cr->scale(minwh / 2.0, minwh / 2.0);
+ cr->translate(1.0, 1.0);
+
+ if (is_fill) {
+ cr->arc(0.0, 0.0, 0.35, 0.0, 2 * M_PI);
+ cr->fill();
+ }
+
+ if (is_stroke) {
+ cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD);
+ cr->arc(0.0, 0.0, 0.65, 0.0, 2 * M_PI);
+ cr->arc(0.0, 0.0, 0.5, 0.0, 2 * M_PI);
+ cr->fill();
+ }
+ }
+
+ return true;
+}
+
+void ColorItem::on_size_allocate(Gtk::Allocation &allocation)
+{
+ Gtk::DrawingArea::on_size_allocate(allocation);
+ cache_dirty = true;
+}
+
+bool ColorItem::on_enter_notify_event(GdkEventCrossing*)
+{
+ mouse_inside = true;
+ if (auto desktop = dialog->getDesktop()) {
+ auto msg = Glib::ustring::compose(_("Color: <b>%1</b>; <b>Click</b> to set fill, <b>Shift+click</b> to set stroke"), description);
+ desktop->tipsMessageContext()->set(Inkscape::INFORMATION_MESSAGE, msg.c_str());
+ }
+ return false;
+}
+
+bool ColorItem::on_leave_notify_event(GdkEventCrossing*)
+{
+ mouse_inside = false;
+ if (auto desktop = dialog->getDesktop()) {
+ desktop->tipsMessageContext()->clear();
+ }
+ return false;
+}
+
+bool ColorItem::on_button_press_event(GdkEventButton *event)
+{
+ if (event->button == 3) {
+ on_rightclick(event);
+ return true;
+ }
+ // Return true necessary to avoid stealing the canvas focus.
+ return true;
+}
+
+bool ColorItem::on_button_release_event(GdkEventButton* event)
+{
+ if (mouse_inside && (event->button == 1 || event->button == 2)) {
+ bool stroke = event->button == 2 || (event->state & GDK_SHIFT_MASK);
+ on_click(stroke);
+ return true;
+ }
+ return false;
+}
+
+void ColorItem::on_click(bool stroke)
+{
+ auto desktop = dialog->getDesktop();
+ if (!desktop) return;
+
+ auto attr_name = stroke ? "stroke" : "fill";
+ auto css = std::unique_ptr<SPCSSAttr, void(*)(SPCSSAttr*)>(sp_repr_css_attr_new(), [] (auto p) {sp_repr_css_attr_unref(p);});
+
+ Glib::ustring descr;
+ if (boost::get<NoneData>(&data)) {
+ sp_repr_css_set_property(css.get(), attr_name, "none");
+ descr = stroke ? _("Set stroke color to none") : _("Set fill color to none");
+ } else if (auto rgbdata = boost::get<RGBData>(&data)) {
+ auto [r, g, b] = rgbdata->rgb;
+ uint32_t rgba = (r << 24) | (g << 16) | (b << 8) | 0xff;
+ char buf[64];
+ sp_svg_write_color(buf, sizeof(buf), rgba);
+ sp_repr_css_set_property(css.get(), attr_name, buf);
+ descr = stroke ? _("Set stroke color from swatch") : _("Set fill color from swatch");
+ } else if (auto graddata = boost::get<GradientData>(&data)) {
+ auto grad = graddata->gradient;
+ if (!grad) return;
+ auto colorspec = "url(#" + Glib::ustring(grad->getId()) + ")";
+ sp_repr_css_set_property(css.get(), attr_name, colorspec.c_str());
+ descr = stroke ? _("Set stroke color from swatch") : _("Set fill color from swatch");
+ }
+
+ sp_desktop_set_style(desktop, css.get());
+
+ DocumentUndo::done(desktop->getDocument(), descr.c_str(), INKSCAPE_ICON("swatches"));
+}
+
+void ColorItem::on_rightclick(GdkEventButton *event)
+{
+ auto menu_gobj = gtk_menu_new(); /* C */
+ auto menu = Glib::wrap(GTK_MENU(menu_gobj)); /* C */
+
+ auto additem = [&, this] (Glib::ustring const &name, sigc::slot<void()> slot) {
+ auto item = Gtk::make_managed<Gtk::MenuItem>(name);
+ menu->append(*item);
+ item->signal_activate().connect(SIGC_TRACKING_ADAPTOR(slot, *this));
+ };
+
+ // TRANSLATORS: An item in context menu on a colour in the swatches
+ additem(_("Set fill"), [this] { on_click(false); });
+ additem(_("Set stroke"), [this] { on_click(true); });
+
+ if (boost::get<GradientData>(&data)) {
+ menu->append(*Gtk::make_managed<Gtk::SeparatorMenuItem>());
+
+ additem(_("Delete"), [this] {
+ auto grad = boost::get<GradientData>(data).gradient;
+ if (!grad) return;
+
+ grad->setSwatch(false);
+ DocumentUndo::done(grad->document, _("Delete swatch"), INKSCAPE_ICON("color-gradient"));
+ });
+
+ additem(_("Edit..."), [this] {
+ auto grad = boost::get<GradientData>(data).gradient;
+ if (!grad) return;
+
+ auto desktop = dialog->getDesktop();
+ auto selection = desktop->getSelection();
+ auto items = std::vector<SPItem*>(selection->items().begin(), selection->items().end());
+
+ if (!items.empty()) {
+ auto query = SPStyle(desktop->doc());
+ int result = objects_query_fillstroke(items, &query, true);
+ if (result == QUERY_STYLE_MULTIPLE_SAME || result == QUERY_STYLE_SINGLE) {
+ if (query.fill.isPaintserver()) {
+ if (cast<SPGradient>(query.getFillPaintServer()) == grad) {
+ desktop->getContainer()->new_dialog("FillStroke");
+ return;
+ }
+ }
+ }
+ }
+
+ // Otherwise, invoke the gradient tool.
+ set_active_tool(desktop, "Gradient");
+ });
+ }
+
+ additem(is_pinned() ? _("Unpin Color") : _("Pin Color"), [this] {
+ if (boost::get<GradientData>(&data)) {
+ auto grad = boost::get<GradientData>(data).gradient;
+ if (!grad) return;
+
+ grad->setPinned(!is_pinned());
+ DocumentUndo::done(grad->document, is_pinned() ? _("Pin swatch") : _("Unpin swatch"), INKSCAPE_ICON("color-gradient"));
+ } else {
+ Inkscape::Preferences::get()->setBool(pinned_pref, !is_pinned());
+ }
+ });
+
+ Gtk::Menu *convert_submenu = nullptr;
+
+ auto create_convert_submenu = [&] {
+ menu->append(*Gtk::make_managed<Gtk::SeparatorMenuItem>());
+
+ auto convert_item = Gtk::make_managed<Gtk::MenuItem>(_("Convert"));
+ menu->append(*convert_item);
+
+ convert_submenu = Gtk::make_managed<Gtk::Menu>();
+ convert_item->set_submenu(*convert_submenu);
+ };
+
+ auto add_convert_subitem = [&, this] (Glib::ustring const &name, sigc::slot<void()> slot) {
+ if (!convert_submenu) {
+ create_convert_submenu();
+ }
+
+ auto item = Gtk::make_managed<Gtk::MenuItem>(name);
+ convert_submenu->append(*item);
+ item->signal_activate().connect(slot);
+ };
+
+ auto grads = dialog->getDesktop()->getDocument()->getResourceList("gradient");
+ for (auto obj : grads) {
+ auto grad = static_cast<SPGradient*>(obj);
+ if (grad->hasStops() && !grad->isSwatch()) {
+ add_convert_subitem(grad->getId(), [name = grad->getId(), this] {
+ auto doc = dialog->getDesktop()->getDocument();
+ auto grads = doc->getResourceList("gradient");
+ for (auto obj : grads) {
+ auto grad = static_cast<SPGradient*>(obj);
+ if (grad->getId() == name) {
+ grad->setSwatch();
+ DocumentUndo::done(doc, _("Add gradient stop"), INKSCAPE_ICON("color-gradient"));
+ }
+ }
+ });
+ }
+ }
+
+ menu->show_all();
+ menu->popup_at_pointer(reinterpret_cast<GdkEvent*>(event));
+
+ // Todo: All lines marked /* C */ in this function are required in order for the menu to
+ // self-destruct after it has finished. Please replace upon discovery of a better method.
+ g_object_ref_sink(menu_gobj); /* C */
+ g_object_unref(menu_gobj); /* C */
+}
+
+PaintDef ColorItem::to_paintdef() const
+{
+ if (boost::get<NoneData>(&data)) {
+ return PaintDef();
+ } else if (auto rgbdata = boost::get<RGBData>(&data)) {
+ return PaintDef(rgbdata->rgb, description);
+ } else if (boost::get<GradientData>(&data)) {
+ auto grad = boost::get<GradientData>(data).gradient;
+ return PaintDef({0, 0, 0}, grad->getId());
+ }
+
+ // unreachable
+ return {};
+}
+
+void ColorItem::on_drag_data_get(Glib::RefPtr<Gdk::DragContext> const &context, Gtk::SelectionData &selection_data, guint info, guint time)
+{
+ auto &mimetypes = PaintDef::getMIMETypes();
+ if (info < 0 || info >= mimetypes.size()) {
+ g_warning("ERROR: unknown value (%d)", info);
+ return;
+ }
+ auto &key = mimetypes[info];
+
+ auto def = to_paintdef();
+ auto [vec, format] = def.getMIMEData(key);
+ if (vec.empty()) return;
+
+ selection_data.set(key, format, reinterpret_cast<guint8 const*>(vec.data()), vec.size());
+}
+
+void ColorItem::on_drag_begin(Glib::RefPtr<Gdk::DragContext> const &context)
+{
+ constexpr int w = 32;
+ constexpr int h = 24;
+
+ auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, w, h);
+ draw_color(Cairo::Context::create(surface), w, h);
+
+ context->set_icon(Gdk::Pixbuf::create(surface, 0, 0, w, h), 0, 0);
+}
+
+void ColorItem::set_fill(bool b)
+{
+ is_fill = b;
+ queue_draw();
+}
+
+void ColorItem::set_stroke(bool b)
+{
+ is_stroke = b;
+ queue_draw();
+}
+
+bool ColorItem::is_pinned() const
+{
+ if (boost::get<GradientData>(&data)) {
+ auto grad = boost::get<GradientData>(data).gradient;
+ if (!grad) {
+ return false;
+ }
+ return grad->isPinned();
+ } else {
+ return Inkscape::Preferences::get()->getBool(pinned_pref, pinned_default);
+ }
+}
+
+std::array<double, 3> ColorItem::average_color() const
+{
+ if (boost::get<NoneData>(&data)) {
+ return {1.0, 1.0, 1.0};
+ } else if (auto rgbdata = boost::get<RGBData>(&data)) {
+ auto [r, g, b] = rgbdata->rgb;
+ return {r / 255.0, g / 255.0, b / 255.0};
+ } else if (auto graddata = boost::get<GradientData>(&data)) {
+ auto grad = graddata->gradient;
+ auto pat = Cairo::RefPtr<Cairo::Pattern>(new Cairo::Pattern(grad->create_preview_pattern(1), true));
+ auto img = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, 1, 1);
+ auto cr = Cairo::Context::create(img);
+ cr->set_source_rgb(196.0 / 255.0, 196.0 / 255.0, 196.0 / 255.0);
+ cr->paint();
+ cr->set_source(pat);
+ cr->paint();
+ cr.clear();
+ auto rgb = img->get_data();
+ return {rgb[0] / 255.0, rgb[1] / 255.0, rgb[2] / 255.0};
+ }
+
+ // unreachable
+ return {1.0, 1.0, 1.0};
+}
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
diff --git a/src/ui/dialog/color-item.h b/src/ui/dialog/color-item.h
new file mode 100644
index 0000000..a0609c1
--- /dev/null
+++ b/src/ui/dialog/color-item.h
@@ -0,0 +1,127 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Color item used in palettes and swatches UI.
+ */
+/* Authors: PBS <pbs3141@gmail.com>
+ * Copyright (C) 2022 PBS
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_COLOR_ITEM_H
+#define INKSCAPE_UI_DIALOG_COLOR_ITEM_H
+
+#include <boost/variant.hpp> // TODO: Upgrade to boost::variant2 or std::variant when possible.
+#include <boost/noncopyable.hpp>
+#include <cairomm/cairomm.h>
+#include <gtkmm/drawingarea.h>
+
+#include "inkscape-preferences.h"
+#include "widgets/paintdef.h"
+
+class SPGradient;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class DialogBase;
+
+/**
+ * The color item you see on-screen as a clickable box.
+ *
+ * Note: This widget must be outlived by its parent dialog, passed in the constructor.
+ */
+class ColorItem final : public Gtk::DrawingArea, boost::noncopyable
+{
+public:
+ /// Create a static color from a paintdef.
+ ColorItem(PaintDef const&, DialogBase*);
+
+ /**
+ * Create a dynamically-updating color from a gradient, to which it remains linked.
+ * If the gradient is destroyed, the widget will go into an inactive state.
+ */
+ ColorItem(SPGradient*, DialogBase*);
+
+ /// Update the fill indicator, showing this widget is the fill of the current selection.
+ void set_fill(bool);
+
+ /// Update the stroke indicator, showing this widget is the stroke of the current selection.
+ void set_stroke(bool);
+
+ /// Update whether this item is pinned.
+ bool is_pinned() const;
+ void set_pinned_pref(const std::string &path);
+
+ const Glib::ustring &get_description() const { return description; }
+
+ sigc::signal<void ()>& signal_modified() { return _signal_modified; };
+ sigc::signal<void ()>& signal_pinned() { return _signal_pinned; };
+
+protected:
+ bool on_draw(Cairo::RefPtr<Cairo::Context> const&) override;
+ void on_size_allocate(Gtk::Allocation&) override;
+ bool on_enter_notify_event(GdkEventCrossing*) override;
+ bool on_leave_notify_event(GdkEventCrossing*) override;
+ bool on_button_press_event(GdkEventButton*) override;
+ bool on_button_release_event(GdkEventButton*) override;
+ void on_drag_data_get(Glib::RefPtr<Gdk::DragContext> const &context, Gtk::SelectionData &selection_data, guint info, guint time) override;
+ void on_drag_begin(Glib::RefPtr<Gdk::DragContext> const&) override;
+
+private:
+ // Common post-construction setup.
+ void common_setup();
+
+ // Perform the on-click action of setting the fill or stroke.
+ void on_click(bool stroke);
+
+ // Perform the right-click action of showing the context menu.
+ void on_rightclick(GdkEventButton *event);
+
+ // Draw the color only (i.e. no indicators) to a Cairo context. Used for drawing both the widget and the drag/drop icon.
+ void draw_color(Cairo::RefPtr<Cairo::Context> const &cr, int w, int h) const;
+
+ // Construct an equivalent paintdef for use during drag/drop.
+ PaintDef to_paintdef() const;
+
+ // Return the color (or average if a gradient), for choosing the color of the fill/stroke indicators.
+ std::array<double, 3> average_color() const;
+
+ // Description of the color, shown in help text.
+ Glib::ustring description;
+ Glib::ustring color_id;
+
+ /// The pinned preference path
+ Glib::ustring pinned_pref;
+ bool pinned_default = false;
+
+ // The color.
+ struct NoneData {};
+ struct RGBData { std::array<unsigned, 3> rgb; };
+ struct GradientData { SPGradient *gradient; };
+ boost::variant<NoneData, RGBData, GradientData> data;
+
+ // The dialog this widget belongs to. Used for determining what desktop to take action on.
+ DialogBase *dialog;
+
+ // Whether this color is in use as the fill or stroke of the current selection.
+ bool is_fill = false;
+ bool is_stroke = false;
+
+ // A cache of the widget contents, if necessary.
+ Cairo::RefPtr<Cairo::ImageSurface> cache;
+ bool cache_dirty = true;
+ bool was_grad_pinned = false;
+
+ // For ensuring that clicks that release outside the widget don't count.
+ bool mouse_inside = false;
+
+ sigc::signal<void ()> _signal_modified;
+ sigc::signal<void ()> _signal_pinned;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_COLOR_ITEM_H
diff --git a/src/ui/dialog/command-palette.cpp b/src/ui/dialog/command-palette.cpp
new file mode 100644
index 0000000..bf82ba7
--- /dev/null
+++ b/src/ui/dialog/command-palette.cpp
@@ -0,0 +1,1659 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Dialog for adding a live path effect.
+ *
+ * Author:
+ * Abhay Raj Singh <abhayonlyone@gmail.com>
+ *
+ * Copyright (C) 2020 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "command-palette.h"
+
+#include <cstddef>
+#include <cstring>
+#include <ctime>
+#include <gdk/gdkkeysyms.h>
+#include <giomm/action.h>
+#include <giomm/application.h>
+#include <giomm/file.h>
+#include <giomm/fileinfo.h>
+#include <glib/gi18n.h>
+#include <glibconfig.h>
+#include <glibmm/convert.h>
+#include <glibmm/date.h>
+#include <glibmm/error.h>
+#include <glibmm/i18n.h>
+#include <glibmm/markup.h>
+#include <glibmm/ustring.h>
+#include <gtkmm/application.h>
+#include <gtkmm/box.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/eventbox.h>
+#include <gtkmm/label.h>
+#include <gtkmm/messagedialog.h>
+#include <gtkmm/recentinfo.h>
+#include <iostream>
+#include <iterator>
+#include <memory>
+#include <optional>
+#include <ostream>
+#include <sigc++/adaptors/bind.h>
+#include <sigc++/functors/mem_fun.h>
+#include <string>
+
+#include "actions/actions-extra-data.h"
+#include "file.h"
+#include "gc-anchored.h"
+#include "include/glibmm_version.h"
+#include "inkscape-application.h"
+#include "inkscape-window.h"
+#include "inkscape.h"
+#include "io/resource.h"
+#include "message-context.h"
+#include "message-stack.h"
+#include "object/uri.h"
+#include "preferences.h"
+#include "ui/interface.h"
+#include "xml/repr.h"
+
+namespace Inkscape {
+class MessageStack;
+namespace UI {
+namespace Dialog {
+
+namespace {
+template <typename T>
+void debug_print(T variable)
+{
+ std::cerr << variable << std::endl;
+}
+} // namespace
+
+// constructor
+CommandPalette::CommandPalette()
+{
+ // setup _builder
+ {
+ auto gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "command-palette-main.glade");
+ try {
+ _builder = Gtk::Builder::create_from_file(gladefile);
+ } catch (const Glib::Error &ex) {
+ g_warning("Glade file loading failed for command palette dialog");
+ return;
+ }
+ }
+
+ // Setup Base UI Components
+ _builder->get_widget("CPBase", _CPBase);
+ _builder->get_widget("CPHeader", _CPHeader);
+ _builder->get_widget("CPListBase", _CPListBase);
+
+ _builder->get_widget("CPSearchBar", _CPSearchBar);
+ _builder->get_widget("CPFilter", _CPFilter);
+
+ _builder->get_widget("CPSuggestions", _CPSuggestions);
+ _builder->get_widget("CPHistory", _CPHistory);
+
+ _builder->get_widget("CPSuggestionsScroll", _CPSuggestionsScroll);
+ _builder->get_widget("CPHistoryScroll", _CPHistoryScroll);
+
+ _CPBase->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);
+
+ // TODO: Customise on user language RTL, LTR or better user preference
+ _CPBase->set_halign(Gtk::ALIGN_CENTER);
+ _CPBase->set_valign(Gtk::ALIGN_START);
+
+ auto esc_func = sigc::mem_fun(*this, &CommandPalette::on_key_press_cpfilter_escape);
+ _CPFilter->signal_key_press_event().connect(esc_func, false);
+ _CPSuggestions->signal_key_release_event().connect(esc_func, false);
+ _CPHistory->signal_key_press_event().connect(esc_func, false);
+ set_mode(CPMode::SEARCH);
+
+ _CPSuggestions->set_activate_on_single_click();
+ _CPSuggestions->set_selection_mode(Gtk::SELECTION_SINGLE);
+
+ // Setup operations [actions, extensions]
+ {
+ // setup recent files
+ {
+ //TODO: refactor this ==============================
+ // this code is repeated in menubar.cpp
+ auto recent_manager = Gtk::RecentManager::get_default();
+ auto recent_files = recent_manager->get_items(); // all recent files not necessarily inkscape only
+
+ int max_files = Inkscape::Preferences::get()->getInt("/options/maxrecentdocuments/value");
+
+ for (auto const &recent_file : recent_files) {
+ // check if given was generated by inkscape
+ bool valid_file = recent_file->has_application(g_get_prgname()) or
+ recent_file->has_application("org.inkscape.Inkscape") or
+ recent_file->has_application("inkscape") or
+ recent_file->has_application("inkscape.exe");
+
+ valid_file = valid_file and recent_file->exists();
+
+ if (not valid_file) {
+ continue;
+ }
+
+ if (max_files-- <= 0) {
+ break;
+ }
+
+ append_recent_file_operation(recent_file->get_uri_display(), true,
+ false); // open - second param true to append in _CPSuggestions
+ append_recent_file_operation(recent_file->get_uri_display(), true,
+ true); // import - last param true for import operation
+ }
+ // ==================================================
+ }
+ }
+
+ // History management
+ {
+ const auto history = _history_xml.get_operation_history();
+
+ for (const auto &page : history) {
+ // second params false to append in history
+ switch (page.history_type) {
+ case HistoryType::ACTION:
+ generate_action_operation(get_action_ptr_name(page.data), false);
+ break;
+ case HistoryType::IMPORT_FILE:
+ append_recent_file_operation(page.data, false, true);
+ break;
+ case HistoryType::OPEN_FILE:
+ append_recent_file_operation(page.data, false, false);
+ break;
+ default:
+ continue;
+ }
+ }
+ }
+ // for `enter to execute` feature
+ _CPSuggestions->signal_row_activated().connect(sigc::mem_fun(*this, &CommandPalette::on_row_activated));
+}
+
+void CommandPalette::open()
+{
+ if (not _win_doc_actions_loaded) {
+ // loading actions can be very slow
+ load_app_actions();
+ // win doc don't exist at construction so loading at first time opening Command Palette
+ load_win_doc_actions();
+ _win_doc_actions_loaded = true;
+ }
+ _CPBase->show_all();
+ _CPFilter->grab_focus();
+ _is_open = true;
+}
+
+void CommandPalette::close()
+{
+ _CPBase->hide();
+
+ // Reset filtering - show all suggestions
+ _CPFilter->set_text("");
+ _CPSuggestions->invalidate_filter();
+
+ set_mode(CPMode::SEARCH);
+
+ _is_open = false;
+}
+
+void CommandPalette::toggle()
+{
+ if (not _is_open) {
+ open();
+ return;
+ }
+ close();
+}
+
+void CommandPalette::append_recent_file_operation(const Glib::ustring &path, bool is_suggestion, bool is_import)
+{
+ static const auto gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "command-palette-operation.glade");
+ Glib::RefPtr<Gtk::Builder> operation_builder;
+ try {
+ operation_builder = Gtk::Builder::create_from_file(gladefile);
+ } catch (const Glib::Error &ex) {
+ g_warning("Glade file loading failed for Command Palette operation dialog");
+ }
+
+ // declaring required widgets pointers
+ Gtk::EventBox *CPOperation;
+ Gtk::Box *CPSynapseBox;
+
+ Gtk::Label *CPGroup;
+ Gtk::Label *CPName;
+ Gtk::Label *CPShortcut;
+ Gtk::Button *CPActionFullButton;
+ Gtk::Label *CPActionFullLabel;
+ Gtk::Label *CPDescription;
+
+ // Reading widgets
+ operation_builder->get_widget("CPOperation", CPOperation);
+ operation_builder->get_widget("CPSynapseBox", CPSynapseBox);
+
+ operation_builder->get_widget("CPGroup", CPGroup);
+ operation_builder->get_widget("CPName", CPName);
+ operation_builder->get_widget("CPShortcut", CPShortcut);
+ operation_builder->get_widget("CPActionFullButton", CPActionFullButton);
+ operation_builder->get_widget("CPActionFullLabel", CPActionFullLabel);
+ operation_builder->get_widget("CPDescription", CPDescription);
+
+ const auto file = Gio::File::create_for_path(path);
+ if (file->query_exists()) {
+ const Glib::ustring file_name = file->get_basename();
+
+ if (is_import) {
+ // Used for Activate row signal of listbox and not
+ CPGroup->set_text("import");
+ CPActionFullLabel->set_text("import"); // For filtering only
+
+ } else {
+ CPGroup->set_text("open");
+ CPActionFullLabel->set_text("open"); // For filtering only
+ }
+
+ // Hide for recent_file, not required
+ CPActionFullButton->set_no_show_all();
+ CPActionFullButton->hide();
+
+ CPName->set_text((is_import ? _("Import") : _("Open")) + (": " + file_name));
+ CPName->set_tooltip_text((is_import ? ("Import") : ("Open")) + (": " + file_name)); // Tooltip_text are not translatable
+ CPDescription->set_text(path);
+ CPDescription->set_tooltip_text(path);
+
+ {
+ Glib::DateTime mod_time;
+#if GLIBMM_CHECK_VERSION(2, 62, 0)
+ mod_time = file->query_info()->get_modification_date_time();
+ // Using this to reduce instead of ActionFullName widget because fullname is searched
+#else
+ mod_time.create_now_local(file->query_info()->modification_time());
+#endif
+ CPShortcut->set_text(mod_time.format("%d %b %R"));
+ }
+ // Add to suggestions
+ if (is_suggestion) {
+ _CPSuggestions->append(*CPOperation);
+ } else {
+ _CPHistory->append(*CPOperation);
+ }
+ }
+}
+
+bool CommandPalette::generate_action_operation(const ActionPtrName &action_ptr_name, bool is_suggestion)
+{
+ static const auto app = InkscapeApplication::instance();
+ static const auto gapp = app->gtk_app();
+ static InkActionExtraData &action_data = app->get_action_extra_data();
+ static const bool show_full_action_name =
+ Inkscape::Preferences::get()->getBool("/options/commandpalette/showfullactionname/value");
+ static const auto gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "command-palette-operation.glade");
+
+ Glib::RefPtr<Gtk::Builder> operation_builder;
+ try {
+ operation_builder = Gtk::Builder::create_from_file(gladefile);
+ } catch (const Glib::Error &ex) {
+ g_warning("Glade file loading failed for Command Palette operation dialog");
+ return false;
+ }
+
+ // declaring required widgets pointers
+ Gtk::EventBox *CPOperation;
+ Gtk::Box *CPSynapseBox;
+
+ Gtk::Label *CPGroup;
+ Gtk::Label *CPName;
+ Gtk::Label *CPShortcut;
+ Gtk::Label *CPDescription;
+ Gtk::Button *CPActionFullButton;
+ Gtk::Label *CPActionFullLabel;
+
+ // Reading widgets
+ operation_builder->get_widget("CPOperation", CPOperation);
+ operation_builder->get_widget("CPSynapseBox", CPSynapseBox);
+
+ operation_builder->get_widget("CPGroup", CPGroup);
+ operation_builder->get_widget("CPName", CPName);
+ operation_builder->get_widget("CPShortcut", CPShortcut);
+ operation_builder->get_widget("CPActionFullButton", CPActionFullButton);
+ operation_builder->get_widget("CPActionFullLabel", CPActionFullLabel);
+ operation_builder->get_widget("CPDescription", CPDescription);
+
+ CPGroup->set_text(action_data.get_section_for_action(Glib::ustring(action_ptr_name.second)));
+
+ // Setting CPName
+ {
+ auto name = action_data.get_label_for_action(action_ptr_name.second);
+ auto untranslated_name = action_data.get_label_for_action(action_ptr_name.second, false);
+ if (name.empty()) {
+ // If action doesn't have a label, set the name = full action name
+ name = action_ptr_name.second;
+ untranslated_name = action_ptr_name.second;
+ }
+
+ CPName->set_text(name);
+ CPName->set_tooltip_text(untranslated_name);
+ }
+
+ {
+ CPActionFullLabel->set_text(action_ptr_name.second);
+
+ if (not show_full_action_name) {
+ CPActionFullButton->set_no_show_all();
+ CPActionFullButton->hide();
+ } else {
+ CPActionFullButton->signal_clicked().connect(
+ sigc::bind<Glib::ustring>(sigc::mem_fun(*this, &CommandPalette::on_action_fullname_clicked),
+ action_ptr_name.second),
+ false);
+ }
+ }
+
+ {
+ std::vector<Glib::ustring> accels = gapp->get_accels_for_action(action_ptr_name.second);
+ std::stringstream ss;
+ for (const auto &accel : accels) {
+ guint key = 0;
+ Gdk::ModifierType mods;
+ Gtk::AccelGroup::parse(accel, key, mods);
+ Glib::ustring label = Gtk::AccelGroup::get_label(key, mods);
+ ss << label.raw() << ' ';
+ }
+ std::string accel_label = ss.str();
+
+ if (not accel_label.empty()) {
+ accel_label.pop_back();
+ CPShortcut->set_text(accel_label);
+ } else {
+ CPShortcut->set_no_show_all();
+ CPShortcut->hide();
+ }
+ }
+
+ CPDescription->set_text(action_data.get_tooltip_for_action(action_ptr_name.second));
+ CPDescription->set_tooltip_text(action_data.get_tooltip_for_action(action_ptr_name.second, false));
+
+ // Add to suggestions
+ if (is_suggestion) {
+ _CPSuggestions->append(*CPOperation);
+ } else {
+ _CPHistory->append(*CPOperation);
+ }
+
+ return true;
+}
+
+void CommandPalette::on_search()
+{
+ _CPSuggestions->unset_sort_func();
+ _CPSuggestions->set_sort_func(sigc::mem_fun(*this, &CommandPalette::on_sort));
+ _search_text = _CPFilter->get_text();
+ _CPSuggestions->invalidate_filter(); // Remove old filter constraint and apply new one
+ if (auto top_row = _CPSuggestions->get_row_at_y(0); top_row) {
+ _CPSuggestions->select_row(*top_row); // select top row
+ }
+ _CPSuggestionsScroll->get_vadjustment()->set_value(0);
+}
+
+bool CommandPalette::on_filter_full_action_name(Gtk::ListBoxRow *child)
+{
+ if (auto CPActionFullLabel = get_full_action_name(child);
+ CPActionFullLabel and _search_text == CPActionFullLabel->get_text()) {
+ return true;
+ }
+ return false;
+}
+
+bool CommandPalette::on_filter_recent_file(Gtk::ListBoxRow *child, bool const is_import)
+{
+ auto CPActionFullLabel = get_full_action_name(child);
+ if (is_import) {
+ if (CPActionFullLabel and CPActionFullLabel->get_text() == "import") {
+ auto [CPName, CPDescription] = get_name_desc(child);
+ if (CPDescription && CPDescription->get_text() == _search_text) {
+ return true;
+ }
+ }
+ return false;
+ }
+ if (CPActionFullLabel and CPActionFullLabel->get_text() == "open") {
+ auto [CPName, CPDescription] = get_name_desc(child);
+ if (CPDescription && CPDescription->get_text() == _search_text) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool CommandPalette::on_key_press_cpfilter_escape(GdkEventKey *evt)
+{
+ if (evt->keyval == GDK_KEY_Escape || evt->keyval == GDK_KEY_question) {
+ close();
+ return true; // stop propagation of key press, not needed anymore
+ }
+ return false; // Pass the key event which are not used
+}
+
+bool CommandPalette::on_key_press_cpfilter_search_mode(GdkEventKey *evt)
+{
+ auto key = evt->keyval;
+ if (key == GDK_KEY_Return or key == GDK_KEY_Linefeed) {
+ if (auto selected_row = _CPSuggestions->get_selected_row(); selected_row) {
+ selected_row->activate();
+ }
+ return true;
+ } else if (key == GDK_KEY_Up) {
+ if (!_CPHistory->get_children().empty()) {
+ set_mode(CPMode::HISTORY);
+ return true;
+ }
+ } else if (key == GDK_KEY_Down) {
+ if (!_CPSuggestions->get_children().empty()) {
+ _CPSuggestions->unselect_all();
+ }
+ }
+ return false;
+}
+
+bool CommandPalette::on_key_press_cpfilter_history_mode(GdkEventKey *evt)
+{
+ if (evt->keyval == GDK_KEY_BackSpace) {
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Executes action when enter pressed
+ */
+bool CommandPalette::on_key_press_cpfilter_input_mode(GdkEventKey *evt, const ActionPtrName &action_ptr_name)
+{
+ switch (evt->keyval) {
+ case GDK_KEY_Return:
+ [[fallthrough]];
+ case GDK_KEY_Linefeed:
+ execute_action(action_ptr_name, _CPFilter->get_text());
+ close();
+ return true;
+ }
+ return false;
+}
+
+void CommandPalette::hide_suggestions()
+{
+ _CPBase->set_size_request(-1, 10);
+ _CPListBase->hide();
+}
+void CommandPalette::show_suggestions()
+{
+ _CPBase->set_size_request(-1, _max_height_requestable);
+ _CPListBase->show_all();
+}
+
+void CommandPalette::on_action_fullname_clicked(const Glib::ustring &action_fullname)
+{
+ static auto clipboard = Gtk::Clipboard::get();
+ clipboard->set_text(action_fullname);
+ clipboard->store();
+}
+
+void CommandPalette::on_row_activated(Gtk::ListBoxRow *activated_row)
+{
+ // this is set to import/export or full action name
+ const auto full_action_name = get_full_action_name(activated_row)->get_label();
+ if (full_action_name == "import" or full_action_name == "open") {
+ const auto [name, description] = get_name_desc(activated_row);
+ operate_recent_file(description->get_text(), full_action_name == "import");
+ } else {
+ ask_action_parameter(get_action_ptr_name(full_action_name));
+ // this is an action
+ }
+}
+
+void CommandPalette::on_history_selection_changed(Gtk::ListBoxRow *lb)
+{
+ // set the search box text to current selection
+ if (const auto name_label = get_name_desc(lb).first; name_label) {
+ _CPFilter->set_text(name_label->get_text());
+ }
+}
+
+bool CommandPalette::operate_recent_file(Glib::ustring const &uri, bool const import)
+{
+ static auto prefs = Inkscape::Preferences::get();
+
+ bool write_to_history = true;
+
+ // if the last element in CPHistory is already this, don't update history file
+ if (not _CPHistory->get_children().empty()) {
+ if (const auto last_operation = _history_xml.get_last_operation(); last_operation.has_value()) {
+ if (uri == last_operation->data) {
+ bool last_operation_was_import = last_operation->history_type == HistoryType::IMPORT_FILE;
+ // As previous uri is verfied to be the same as current uri we can write to history if current and
+ // previous operation are not the same.
+ // For example: if we want to import and previous operation was import (with same uri) we should not
+ // write ot history, similarly if current is open and previous was open to then dont WTH.
+ // But in case previous operation was open and current is import and vice-versa we should write to
+ // history.
+ if (not(import xor last_operation_was_import)) {
+ write_to_history = false;
+ }
+ }
+ }
+ }
+
+ if (import) {
+ prefs->setBool("/options/onimport", true);
+ file_import(SP_ACTIVE_DOCUMENT, uri, nullptr);
+ prefs->setBool("/options/onimport", true);
+
+ if (write_to_history) {
+ _history_xml.add_import(uri);
+ }
+
+ close();
+ return true;
+ }
+
+ // open
+ {
+ get_action_ptr_name("app.file-open").first->activate(uri);
+ if (write_to_history) {
+ _history_xml.add_open(uri);
+ }
+ }
+
+ close();
+ return true;
+} // namespace Dialog
+
+/**
+ * Maybe replaced by: Temporary arrangement may be replaced by snippets
+ * This can help us provide parameters for multiple argument function
+ * whose actions take a string as param
+ */
+bool CommandPalette::ask_action_parameter(const ActionPtrName &action_ptr_name)
+{
+ // Avoid writing same last action again
+ // TODO: Merge the if else parts
+ if (const auto last_of_history = _history_xml.get_last_operation(); last_of_history.has_value()) {
+ // operation history is not empty
+ const auto last_full_action_name = last_of_history->data;
+ if (last_full_action_name != action_ptr_name.second) {
+ // last action is not the same so write this one
+ _history_xml.add_action(action_ptr_name.second); // to history file
+ generate_action_operation(action_ptr_name, false); // to _CPHistory
+ }
+ } else {
+ // History is empty so no need to check
+ _history_xml.add_action(action_ptr_name.second); // to history file
+ generate_action_operation(action_ptr_name, false); // to _CPHistory
+ }
+
+ // Checking if action has handleable parameter type
+ TypeOfVariant action_param_type = get_action_variant_type(action_ptr_name.first);
+ if (action_param_type == TypeOfVariant::UNKNOWN) {
+ std::cerr << "CommandPalette::ask_action_parameter: unhandled action value type (Unknown Type) "
+ << action_ptr_name.second.raw() << std::endl;
+ return false;
+ }
+
+ if (action_param_type != TypeOfVariant::NONE) {
+ set_mode(CPMode::INPUT);
+
+ _cpfilter_key_press_connection = _CPFilter->signal_key_press_event().connect(
+ sigc::bind<ActionPtrName>(sigc::mem_fun(*this, &CommandPalette::on_key_press_cpfilter_input_mode),
+ action_ptr_name),
+ false);
+
+ // get type string NOTE: Temporary should be replaced by adding some data to InkActionExtraDataj
+ Glib::ustring type_string;
+ switch (action_param_type) {
+ case TypeOfVariant::BOOL:
+ type_string = "bool";
+ break;
+ case TypeOfVariant::INT:
+ type_string = "integer";
+ break;
+ case TypeOfVariant::DOUBLE:
+ type_string = "double";
+ break;
+ case TypeOfVariant::STRING:
+ type_string = "string";
+ break;
+ case TypeOfVariant::TUPLE_DD:
+ type_string = "pair of doubles";
+ break;
+ default:
+ break;
+ }
+
+ const auto app = InkscapeApplication::instance();
+ InkActionHintData &action_hint_data = app->get_action_hint_data();
+ auto action_hint = action_hint_data.get_tooltip_hint_for_action(action_ptr_name.second, false);
+
+
+ // Indicate user about what to enter FIXME Dialog generation
+ if (action_hint.length()) {
+ _CPFilter->set_placeholder_text(action_hint);
+ _CPFilter->set_tooltip_text(action_hint);
+ } else {
+ _CPFilter->set_placeholder_text("Enter a " + type_string + "...");
+ _CPFilter->set_tooltip_text("Enter a " + type_string + "...");
+ }
+
+
+ return true;
+ }
+
+ execute_action(action_ptr_name, "");
+ close();
+
+ return true;
+}
+
+/**
+ * Color removal
+ */
+void CommandPalette::remove_color(Gtk::Label *label, const Glib::ustring &subject, bool tooltip)
+{
+ /* if (tooltip) {
+ label->set_tooltip_text(subject);
+ } else if (label->get_use_markup()) {
+ label->set_text(subject);
+ } */
+}
+
+/**
+ * Color addition
+ */
+Glib::ustring make_bold(const Glib::ustring &search)
+{
+ // TODO: Add a CSS class that changes the color of the search
+ return "<span weight=\"bold\">" + search + "</span>";
+}
+
+void CommandPalette::add_color(Gtk::Label *label, const Glib::ustring &search, const Glib::ustring &subject, bool tooltip)
+{
+ //is no working on master fill all chars so I comment to speedup
+ /* Glib::ustring text = "";
+ Glib::ustring subject_string = subject.lowercase();
+ Glib::ustring search_string = search.lowercase();
+ int j = 0;
+
+ if (search_string.length() > 7) {
+ for (gunichar i : search_string) {
+ if (i == ' ') {
+ continue;
+ }
+ while (j < subject_string.length()) {
+ if (i == subject_string[j]) {
+ text += make_bold(Glib::Markup::escape_text(subject.substr(j, 1)));
+ j++;
+ break;
+ } else {
+ text += Glib::Markup::escape_text(subject.substr(j, 1));
+ }
+ j++;
+ }
+ }
+ if (j < subject.length()) {
+ text += Glib::Markup::escape_text(subject.substr(j));
+ }
+ } else {
+ std::map<gunichar, int> search_string_character;
+
+ for (const auto &character : search_string) {
+ search_string_character[character]++;
+ }
+
+ int subject_length = subject_string.length();
+
+ for (int i = 0; i < subject_length; i++) {
+ if (search_string_character[subject_string[i]]--) {
+ text += make_bold(Glib::Markup::escape_text(subject.substr(i, 1)));
+ } else {
+ text += Glib::Markup::escape_text(subject.substr(i, 1));
+ }
+ }
+ }
+
+ if (tooltip) {
+ label->set_tooltip_markup(text);
+ } else {
+ label->set_markup(text);
+ } */
+}
+
+/**
+ * Color addition for description text
+ * Coloring complete consecutive search text in the description text
+ */
+void CommandPalette::add_color_description(Gtk::Label *label, const Glib::ustring &search)
+{
+ /* Glib::ustring subject = label->get_text();
+
+ Glib::ustring const subject_normalize = subject.lowercase().normalize();
+ Glib::ustring const search_normalize = search.lowercase().normalize();
+
+ auto const position = subject_normalize.find(search_normalize);
+ auto const search_length = search_normalize.size();
+
+ subject = Glib::Markup::escape_text(subject.substr(0, position)) +
+ make_bold(Glib::Markup::escape_text(subject.substr(position, search_length))) +
+ Glib::Markup::escape_text(subject.substr(position + search_length));
+
+ label->set_markup(subject); */
+}
+
+/**
+ * The Searching algorithm consists of fuzzy search and fuzzy points.
+ *
+ * Ever search of the label can contain up to three subjects to search
+ * CPName text,CPName tooltip text,CPDescription text
+ *
+ * Fuzzy search searches the search text in these subjects and returns a boolean
+ * Searching of a search text as a subsequence of the subject
+ *
+ * Fuzzy points give an integer of a particular search text concerning a particular subject.
+ * Less the fuzzy point more is the precedence.
+ *
+ * Special case for CPDescription text search by searching text as a substring of the subject
+ *
+ * TODO: Adding more conditions in fuzzy points and fuzzy search for creating better user experience
+ */
+
+bool CommandPalette::fuzzy_tolerance_search(const Glib::ustring &subject, const Glib::ustring &search)
+{
+ Glib::ustring subject_string = subject.lowercase();
+ Glib::ustring search_string = search.lowercase();
+ std::map<gunichar, int> subject_string_character, search_string_character;
+ for (const auto &character : subject_string) {
+ subject_string_character[character]++;
+ }
+ for (const auto &character : search_string) {
+ search_string_character[character]++;
+ }
+ for (const auto &character : search_string_character) {
+ auto [alphabet, occurrence] = character;
+ if (subject_string_character[alphabet] < occurrence) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool CommandPalette::fuzzy_search(const Glib::ustring &subject, const Glib::ustring &search)
+{
+ Glib::ustring subject_string = subject.lowercase();
+ Glib::ustring search_string = search.lowercase();
+
+ for (int j = 0, i = 0; i < search_string.length(); i++) {
+ bool alphabet_present = false;
+
+ while (j < subject_string.length()) {
+ if (search_string[i] == subject_string[j]) {
+ alphabet_present = true;
+ j++;
+ break;
+ }
+ j++;
+ }
+
+ if (!alphabet_present) {
+ return false; // If not present
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Searching the full search_text in the subject string
+ * used for CPDescription text
+ */
+bool CommandPalette::normal_search(const Glib::ustring &subject, const Glib::ustring &search)
+{
+ if (subject.lowercase().find(search.lowercase()) != -1) {
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Calculates the fuzzy_point
+ */
+int CommandPalette::fuzzy_points(const Glib::ustring &subject, const Glib::ustring &search)
+{
+ int fuzzy_cost = 100; // Taking initial fuzzy_cost as 100
+
+ constexpr int SEQUENTIAL_BONUS = -15; // bonus for adjacent matches
+ constexpr int SEPARATOR_BONUS = -30; // bonus if search occurs after a separator
+ constexpr int CAMEL_BONUS = -30; // bonus if search is uppercase and subject is lower
+ constexpr int FIRST_LETTET_BONUS = -15; // bonus if the first letter is matched
+ constexpr int LEADING_LETTER_PENALTY = +5; // penalty applied for every letter in subject before the first match
+ constexpr int MAX_LEADING_LETTER_PENALTY = +15; // maximum penalty for leading letters
+ constexpr int UNMATCHED_LETTER_PENALTY = +1; // penalty for every letter that doesn't matter
+
+ Glib::ustring subject_string = subject.lowercase();
+ Glib::ustring search_string = search.lowercase();
+
+ bool sequential_compare = false;
+ bool leading_letter = true;
+ int total_leading_letter_penalty = 0;
+ int j = 0, i = 0;
+
+ while (i < search_string.length() && j < subject_string.length()) {
+ if (search_string[i] != subject_string[j]) {
+ j++;
+ sequential_compare = false;
+ fuzzy_cost += UNMATCHED_LETTER_PENALTY;
+
+ if (leading_letter) {
+ if (total_leading_letter_penalty < MAX_LEADING_LETTER_PENALTY) {
+ fuzzy_cost += LEADING_LETTER_PENALTY;
+ total_leading_letter_penalty += LEADING_LETTER_PENALTY;
+ }
+ }
+
+ continue;
+ }
+
+ if (search_string[i] == subject_string[j]) {
+ leading_letter = false;
+
+ if (j > 0 && subject_string[j - 1] == ' ') {
+ fuzzy_cost += SEPARATOR_BONUS;
+ }
+
+ if (i == 0 && j == 0) {
+ fuzzy_cost += FIRST_LETTET_BONUS;
+ }
+
+ if (search[i] == subject_string[j]) {
+ fuzzy_cost += CAMEL_BONUS;
+ }
+
+ if (sequential_compare) {
+ fuzzy_cost += SEQUENTIAL_BONUS;
+ }
+
+ sequential_compare = true;
+ i++;
+ }
+ }
+
+ return fuzzy_cost;
+}
+
+int CommandPalette::fuzzy_tolerance_points(const Glib::ustring &subject, const Glib::ustring &search)
+{
+ int fuzzy_cost = 200; // Taking initial fuzzy_cost as 200
+ constexpr int FIRST_LETTET_BONUS = -15; // bonus if the first letter is matched
+
+ Glib::ustring subject_string = subject.lowercase();
+ Glib::ustring search_string = search.lowercase();
+
+ std::map<gunichar, int> search_string_character;
+
+ for (const auto &character : search_string) {
+ search_string_character[character]++;
+ }
+
+ for (const auto &character : search_string_character) {
+ auto [alphabet, occurrence] = character;
+ for (int i = 0; i < subject_string.length() && occurrence; i++) {
+ if (subject_string[i] == alphabet) {
+ if (i == 0)
+ fuzzy_cost += FIRST_LETTET_BONUS;
+ fuzzy_cost += i;
+ occurrence--;
+ }
+ }
+ }
+
+ return fuzzy_cost;
+}
+
+int CommandPalette::on_filter_general(Gtk::ListBoxRow *child)
+{
+ auto [CPName, CPDescription] = get_name_desc(child);
+ if (CPName) {
+ remove_color(CPName, CPName->get_text());
+ remove_color(CPName, CPName->get_tooltip_text(), true);
+ }
+ if (CPDescription) {
+ remove_color(CPDescription, CPDescription->get_text());
+ }
+
+ if (_search_text.empty()) {
+ return 1;
+ } // Every operation is visible if search text is empty
+
+ if (CPName) {
+ if (fuzzy_search(CPName->get_text(), _search_text)) {
+ add_color(CPName, _search_text, CPName->get_text());
+ return fuzzy_points(CPName->get_text(), _search_text);
+ }
+
+ if (fuzzy_search(CPName->get_tooltip_text(), _search_text)) {
+ add_color(CPName, _search_text, CPName->get_tooltip_text(), true);
+ return fuzzy_points(CPName->get_tooltip_text(), _search_text);
+ }
+
+ if (fuzzy_tolerance_search(CPName->get_text(), _search_text)) {
+ add_color(CPName, _search_text, CPName->get_text());
+ return fuzzy_tolerance_points(CPName->get_text(), _search_text);
+ }
+
+ if (fuzzy_tolerance_search(CPName->get_tooltip_text(), _search_text)) {
+ add_color(CPName, _search_text, CPName->get_tooltip_text(), true);
+ return fuzzy_tolerance_points(CPName->get_tooltip_text(), _search_text);
+ }
+ }
+ if (CPDescription && normal_search(CPDescription->get_text(), _search_text)) {
+ add_color_description(CPDescription, _search_text);
+ return fuzzy_points(CPDescription->get_text(), _search_text);
+ }
+
+ return 0;
+}
+
+int CommandPalette::fuzzy_points_compare(int fuzzy_points_count_1, int fuzzy_points_count_2, int text_len_1,
+ int text_len_2)
+{
+ if (fuzzy_points_count_1 && fuzzy_points_count_2) {
+ if (fuzzy_points_count_1 < fuzzy_points_count_2) {
+ return -1;
+ } else if (fuzzy_points_count_1 == fuzzy_points_count_2) {
+ if (text_len_1 > text_len_2) {
+ return 1;
+ } else {
+ return -1;
+ }
+ } else {
+ return 1;
+ }
+ }
+
+ if (fuzzy_points_count_1 == 0 && fuzzy_points_count_2) {
+ return 1;
+ }
+ if (fuzzy_points_count_2 == 0 && fuzzy_points_count_1) {
+ return -1;
+ }
+
+ return 0;
+}
+
+/**
+ * compare different rows for order of display
+ * priority of comparison
+ * 1) CPName->get_text()
+ * 2) CPName->get_tooltip_text()
+ * 3) CPDescription->get_text()
+ */
+int CommandPalette::on_sort(Gtk::ListBoxRow *row1, Gtk::ListBoxRow *row2)
+{
+ // tests for fuzzy_search
+ assert(fuzzy_search("Export background", "ebo") == true);
+ assert(fuzzy_search("Query y", "qyy") == true);
+ assert(fuzzy_search("window close", "qt") == false);
+
+ // tests for fuzzy_points
+ assert(fuzzy_points("Export background", "ebo") == -22);
+ assert(fuzzy_points("Query y", "qyy") == -16);
+ assert(fuzzy_points("window close", "wc") == 2);
+
+ // tests for fuzzy_tolerance_search
+ assert(fuzzy_tolerance_search("object to path", "ebo") == true);
+ assert(fuzzy_tolerance_search("execute verb", "qyy") == false);
+ assert(fuzzy_tolerance_search("color mode", "moco") == true);
+
+ // tests for fuzzy_tolerance_points
+ assert(fuzzy_tolerance_points("object to path", "ebo") == 189);
+ assert(fuzzy_tolerance_points("execute verb", "vec") == 196);
+ assert(fuzzy_tolerance_points("color mode", "moco") == 195);
+
+ if (_search_text.empty()) {
+ return -1;
+ } // No change in the order
+
+ auto [cp_name_1, cp_description_1] = get_name_desc(row1);
+ auto [cp_name_2, cp_description_2] = get_name_desc(row2);
+
+ int fuzzy_points_count_1 = 0, fuzzy_points_count_2 = 0;
+ int text_len_1 = 0, text_len_2 = 0;
+ int points_compare = 0;
+
+ constexpr int TOOLTIP_PENALTY = 100;
+ constexpr int DESCRIPTION_PENALTY = 500;
+
+ if (cp_name_1 && cp_name_2) {
+ if (fuzzy_search(cp_name_1->get_text(), _search_text)) {
+ text_len_1 = cp_name_1->get_text().length();
+ fuzzy_points_count_1 = fuzzy_points(cp_name_1->get_text(), _search_text);
+ }
+ if (fuzzy_search(cp_name_2->get_text(), _search_text)) {
+ text_len_2 = cp_name_2->get_text().length();
+ fuzzy_points_count_2 = fuzzy_points(cp_name_2->get_text(), _search_text);
+ }
+
+ points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2);
+ if (points_compare != 0) {
+ return points_compare;
+ }
+
+ if (fuzzy_tolerance_search(cp_name_1->get_text(), _search_text)) {
+ text_len_1 = cp_name_1->get_text().length();
+ fuzzy_points_count_1 = fuzzy_tolerance_points(cp_name_1->get_text(), _search_text);
+ }
+ if (fuzzy_tolerance_search(cp_name_2->get_text(), _search_text)) {
+ text_len_2 = cp_name_2->get_text().length();
+ fuzzy_points_count_2 = fuzzy_tolerance_points(cp_name_2->get_text(), _search_text);
+ }
+
+ points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2);
+ if (points_compare != 0) {
+ return points_compare;
+ }
+
+ if (fuzzy_search(cp_name_1->get_tooltip_text(), _search_text)) {
+ text_len_1 = cp_name_1->get_tooltip_text().length();
+ fuzzy_points_count_1 = fuzzy_points(cp_name_1->get_tooltip_text(), _search_text) + TOOLTIP_PENALTY;
+ }
+ if (fuzzy_search(cp_name_2->get_tooltip_text(), _search_text)) {
+ text_len_2 = cp_name_2->get_tooltip_text().length();
+ fuzzy_points_count_2 = fuzzy_points(cp_name_2->get_tooltip_text(), _search_text) + TOOLTIP_PENALTY;
+ }
+
+ points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2);
+ if (points_compare != 0) {
+ return points_compare;
+ }
+
+ if (fuzzy_tolerance_search(cp_name_1->get_tooltip_text(), _search_text)) {
+ text_len_1 = cp_name_1->get_tooltip_text().length();
+ fuzzy_points_count_1 = fuzzy_tolerance_points(cp_name_1->get_tooltip_text(), _search_text) +
+ TOOLTIP_PENALTY; // Adding a constant integer to decrease the prefrence
+ }
+ if (fuzzy_tolerance_search(cp_name_2->get_tooltip_text(), _search_text)) {
+ text_len_2 = cp_name_2->get_tooltip_text().length();
+ fuzzy_points_count_2 = fuzzy_tolerance_points(cp_name_2->get_tooltip_text(), _search_text) +
+ TOOLTIP_PENALTY; // Adding a constant integer to decrease the prefrence
+ }
+ points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2);
+ if (points_compare != 0) {
+ return points_compare;
+ }
+ }
+
+ if (cp_description_1 && normal_search(cp_description_1->get_text(), _search_text)) {
+ text_len_1 = cp_description_1->get_text().length();
+ fuzzy_points_count_1 = fuzzy_points(cp_description_1->get_text(), _search_text) +
+ DESCRIPTION_PENALTY; // Adding a constant integer to decrease the prefrence
+ }
+ if (cp_description_2 && normal_search(cp_description_2->get_text(), _search_text)) {
+ text_len_2 = cp_description_2->get_text().length();
+ fuzzy_points_count_2 = fuzzy_points(cp_description_2->get_text(), _search_text) +
+ DESCRIPTION_PENALTY; // Adding a constant integer to decrease the prefrence
+ }
+
+ points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2);
+ if (points_compare != 0) {
+ return points_compare;
+ }
+ return 0;
+}
+
+void CommandPalette::set_mode(CPMode mode)
+{
+ switch (mode) {
+ case CPMode::SEARCH:
+ if (_mode == CPMode::SEARCH) {
+ return;
+ }
+
+ _CPFilter->set_text("");
+ _CPFilter->set_icon_from_icon_name("edit-find-symbolic");
+ _CPFilter->set_placeholder_text("Search operation...");
+ _CPFilter->set_tooltip_text("Search operation...");
+ show_suggestions();
+
+ // Show Suggestions instead of history
+ _CPHistoryScroll->set_no_show_all();
+ _CPHistoryScroll->hide();
+
+ _CPSuggestionsScroll->set_no_show_all(false);
+ _CPSuggestionsScroll->show_all();
+
+ _CPSuggestions->unset_filter_func();
+ _CPSuggestions->set_filter_func(sigc::mem_fun(*this, &CommandPalette::on_filter_general));
+
+ _cpfilter_search_connection.disconnect(); // to be sure
+ _cpfilter_key_press_connection.disconnect();
+
+ _cpfilter_search_connection =
+ _CPFilter->signal_search_changed().connect(sigc::mem_fun(*this, &CommandPalette::on_search));
+ _cpfilter_key_press_connection = _CPFilter->signal_key_press_event().connect(
+ sigc::mem_fun(*this, &CommandPalette::on_key_press_cpfilter_search_mode), false);
+
+ _search_text = "";
+ _CPSuggestions->invalidate_filter();
+ break;
+
+ case CPMode::INPUT:
+ if (_mode == CPMode::INPUT) {
+ return;
+ }
+ _cpfilter_search_connection.disconnect();
+ _cpfilter_key_press_connection.disconnect();
+
+ hide_suggestions();
+ _CPFilter->set_text("");
+ _CPFilter->grab_focus();
+
+ _CPFilter->set_icon_from_icon_name("input-keyboard");
+ _CPFilter->set_placeholder_text("Enter action argument");
+ _CPFilter->set_tooltip_text("Enter action argument");
+
+ break;
+
+ case CPMode::SHELL:
+ if (_mode == CPMode::SHELL) {
+ return;
+ }
+
+ hide_suggestions();
+ _CPFilter->set_icon_from_icon_name("gtk-search");
+ _cpfilter_search_connection.disconnect();
+ _cpfilter_key_press_connection.disconnect();
+
+ break;
+
+ case CPMode::HISTORY:
+ if (_mode == CPMode::HISTORY) {
+ return;
+ }
+
+ if (_CPHistory->get_children().empty()) {
+ return;
+ }
+
+ // Show history instead of suggestions
+ _CPSuggestionsScroll->set_no_show_all();
+ _CPHistoryScroll->set_no_show_all(false);
+
+ _CPSuggestionsScroll->hide();
+ _CPHistoryScroll->show_all();
+
+ _CPFilter->set_icon_from_icon_name("format-justify-fill");
+ _CPFilter->set_icon_tooltip_text(N_("History mode"));
+ _cpfilter_search_connection.disconnect();
+ _cpfilter_key_press_connection.disconnect();
+
+ _cpfilter_key_press_connection = _CPFilter->signal_key_press_event().connect(
+ sigc::mem_fun(*this, &CommandPalette::on_key_press_cpfilter_history_mode), false);
+
+ _CPHistory->signal_row_selected().connect(
+ sigc::mem_fun(*this, &CommandPalette::on_history_selection_changed));
+ _CPHistory->signal_row_activated().connect(sigc::mem_fun(*this, &CommandPalette::on_row_activated));
+
+ {
+ // select last row
+ const auto last_row = _CPHistory->get_row_at_index(_CPHistory->get_children().size() - 1);
+ _CPHistory->select_row(*last_row);
+ last_row->grab_focus();
+ }
+
+ {
+ // FIXME: scroll to bottom
+ const auto adjustment = _CPHistoryScroll->get_vadjustment();
+ adjustment->set_value(adjustment->get_upper());
+ }
+
+ break;
+ }
+ _mode = mode;
+}
+
+/**
+ * Calls actions with parameters
+ */
+CommandPalette::ActionPtrName CommandPalette::get_action_ptr_name(const Glib::ustring &full_action_name)
+{
+ static auto gapp = InkscapeApplication::instance()->gtk_app();
+ // TODO: Optimisation: only try to assign if null, make static
+ const auto win = InkscapeApplication::instance()->get_active_window();
+ const auto doc = InkscapeApplication::instance()->get_active_document();
+ auto action_domain_string = full_action_name.substr(0, full_action_name.find('.')); // app, win, doc
+ auto action_name = full_action_name.substr(full_action_name.find('.') + 1);
+
+ ActionPtr action_ptr;
+ if (action_domain_string == "app") {
+ action_ptr = gapp->lookup_action(action_name);
+ } else if (action_domain_string == "win" and win) {
+ action_ptr = win->lookup_action(action_name);
+ } else if (action_domain_string == "doc" and doc) {
+ if (const auto map = doc->getActionGroup(); map) {
+ action_ptr = map->lookup_action(action_name);
+ }
+ }
+
+ return {action_ptr, full_action_name};
+}
+
+bool CommandPalette::execute_action(const ActionPtrName &action_ptr_name, const Glib::ustring &value)
+{
+ if (not value.empty()) {
+ _history_xml.add_action_parameter(action_ptr_name.second, value);
+ }
+ auto [action_ptr, action_name] = action_ptr_name;
+
+ switch (get_action_variant_type(action_ptr)) {
+ case TypeOfVariant::BOOL:
+ if (value == "1" || value == "t" || value == "true" || value.empty()) {
+ action_ptr->activate(Glib::Variant<bool>::create(true));
+ } else if (value == "0" || value == "f" || value == "false") {
+ action_ptr->activate(Glib::Variant<bool>::create(false));
+ } else {
+ std::cerr << "CommandPalette::execute_action: Invalid boolean value: " << action_name.raw() << ":" << value
+ << std::endl;
+ }
+ break;
+ case TypeOfVariant::INT:
+ try {
+ action_ptr->activate(Glib::Variant<int>::create(std::stoi(value)));
+ } catch (...) {
+ if (SPDesktop *dt = SP_ACTIVE_DESKTOP; dt) {
+ dt->messageStack()->flash(ERROR_MESSAGE, _("Invalid input! Enter an integer number."));
+ }
+ }
+ break;
+ case TypeOfVariant::DOUBLE:
+ try {
+ action_ptr->activate(Glib::Variant<double>::create(std::stod(value)));
+ } catch (...) {
+ if (SPDesktop *dt = SP_ACTIVE_DESKTOP; dt) {
+ dt->messageStack()->flash(ERROR_MESSAGE, _("Invalid input! Enter a decimal number."));
+ }
+ }
+ break;
+ case TypeOfVariant::STRING:
+ action_ptr->activate(Glib::Variant<Glib::ustring>::create(value));
+ break;
+ case TypeOfVariant::TUPLE_DD:
+ try {
+ double d0 = 0;
+ double d1 = 0;
+ std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("\\s*,\\s*", value);
+
+ try {
+ if (tokens.size() != 2) {
+ throw std::invalid_argument("requires two numbers");
+ }
+ } catch (...) {
+ throw;
+ }
+
+ try {
+ d0 = std::stod(tokens[0]);
+ d1 = std::stod(tokens[1]);
+ } catch (...) {
+ throw;
+ }
+
+ auto variant = Glib::Variant<std::tuple<double, double>>::create(std::tuple<double, double>(d0, d1));
+ action_ptr->activate(variant);
+ } catch (...) {
+ if (SPDesktop *dt = SP_ACTIVE_DESKTOP; dt) {
+ dt->messageStack()->flash(ERROR_MESSAGE, _("Invalid input! Enter two comma separated numbers."));
+ }
+ }
+ break;
+ case TypeOfVariant::UNKNOWN:
+ std::cerr << "CommandPalette::execute_action: unhandled action value type (Unknown Type) " << action_name.raw()
+ << std::endl;
+ break;
+ case TypeOfVariant::NONE:
+ default:
+ action_ptr->activate();
+ break;
+ }
+ return false;
+}
+
+TypeOfVariant CommandPalette::get_action_variant_type(const ActionPtr &action_ptr)
+{
+ const GVariantType *gtype = g_action_get_parameter_type(action_ptr->gobj());
+ if (gtype) {
+ Glib::VariantType type = action_ptr->get_parameter_type();
+ if (type.get_string() == "b") {
+ return TypeOfVariant::BOOL;
+ } else if (type.get_string() == "i") {
+ return TypeOfVariant::INT;
+ } else if (type.get_string() == "d") {
+ return TypeOfVariant::DOUBLE;
+ } else if (type.get_string() == "s") {
+ return TypeOfVariant::STRING;
+ } else if (type.get_string() == "(dd)") {
+ return TypeOfVariant::TUPLE_DD;
+ } else {
+ std::cerr << "CommandPalette::get_action_variant_type: unknown variant type: " << type.get_string() << std::endl;
+ return TypeOfVariant::UNKNOWN;
+ }
+ }
+ // With value.
+ return TypeOfVariant::NONE;
+}
+
+std::pair<Gtk::Label *, Gtk::Label *> CommandPalette::get_name_desc(Gtk::ListBoxRow *child)
+{
+ auto event_box = dynamic_cast<Gtk::EventBox *>(child->get_child());
+ if (event_box) {
+ // NOTE: These variables have same name as in the glade file command-palette-operation.glade
+ // FIXME: When structure of Gladefile of CPOperation changes, refactor this
+ auto CPSynapseBox = dynamic_cast<Gtk::Box *>(event_box->get_child());
+ if (CPSynapseBox) {
+ auto synapse_children = CPSynapseBox->get_children();
+ auto CPNameBox = dynamic_cast<Gtk::Box *>(synapse_children[0]);
+ if (CPNameBox) {
+ auto name_children = CPNameBox->get_children();
+ auto CPName = dynamic_cast<Gtk::Label *>(name_children[0]);
+ auto CPDescription = dynamic_cast<Gtk::Label *>(name_children[1]);
+ return std::pair(CPName, CPDescription);
+ }
+ }
+ }
+ return std::pair(nullptr, nullptr);
+}
+
+Gtk::Label *CommandPalette::get_full_action_name(Gtk::ListBoxRow *child)
+{
+ auto event_box = dynamic_cast<Gtk::EventBox *>(child->get_child());
+ if (event_box) {
+ auto CPSynapseBox = dynamic_cast<Gtk::Box *>(event_box->get_child());
+ if (CPSynapseBox) {
+ auto synapse_children = CPSynapseBox->get_children();
+ auto CPActionFullButton = dynamic_cast<Gtk::Button *>(synapse_children[1]);
+ if (CPActionFullButton) {
+ auto synapse_button = CPActionFullButton->get_children();
+ auto CPSinapseButtonBox = dynamic_cast<Gtk::Box *>(synapse_button[0]);
+ if (CPSinapseButtonBox) {
+ auto synapse_button_content = CPSinapseButtonBox->get_children();
+ return dynamic_cast<Gtk::Label *>(synapse_button_content[1]);
+ }
+ }
+ }
+ }
+
+ return nullptr;
+}
+
+void CommandPalette::load_app_actions()
+{
+ auto gapp = InkscapeApplication::instance()->gtk_app();
+ std::vector<ActionPtrName> all_actions_info;
+
+ std::vector<Glib::ustring> actions = gapp->list_actions();
+ for (const auto &action : actions) {
+ generate_action_operation(get_action_ptr_name("app." + action), true);
+ }
+}
+
+void CommandPalette::load_win_doc_actions()
+{
+ if (auto window = InkscapeApplication::instance()->get_active_window(); window) {
+ std::vector<Glib::ustring> actions = window->list_actions();
+ for (auto action : actions) {
+ generate_action_operation(get_action_ptr_name("win." + action), true);
+ }
+
+ if (auto document = window->get_document(); document) {
+ auto map = document->getActionGroup();
+ if (map) {
+ std::vector<Glib::ustring> actions = map->list_actions();
+ for (auto action : actions) {
+ generate_action_operation(get_action_ptr_name("doc." + action), true);
+ }
+ } else {
+ std::cerr << "CommandPalette::load_win_doc_actions: No document map!" << std::endl;
+ }
+ }
+ }
+}
+
+Gtk::Box *CommandPalette::get_base_widget()
+{
+ return _CPBase;
+}
+
+// CPHistoryXML ---------------------------------------------------------------
+CPHistoryXML::CPHistoryXML()
+ : _file_path(IO::Resource::profile_path("cphistory.xml"))
+{
+ _xml_doc = sp_repr_read_file(_file_path.c_str(), nullptr);
+ if (not _xml_doc) {
+ _xml_doc = sp_repr_document_new("cphistory");
+
+ /* STRUCTURE EXAMPLE ------------------ Illustration 1
+ <cphistory>
+ <operations>
+ <action> full.action_name </action>
+ <import> uri </import>
+ <export> uri </export>
+ </operations>
+ <params>
+ <action name="app.transfor-rotate">
+ <param> 30 </param>
+ <param> 23.5 </param>
+ </action>
+ </params>
+ </cphistory>
+ */
+
+ // Just a pointer, we don't own it, don't free/release/delete
+ auto root = _xml_doc->root();
+
+ // add operation history in this element
+ auto operations = _xml_doc->createElement("operations");
+ root->appendChild(operations);
+
+ // add param history in this element
+ auto params = _xml_doc->createElement("params");
+ root->appendChild(params);
+
+ // This was created by allocated
+ Inkscape::GC::release(operations);
+ Inkscape::GC::release(params);
+
+ // only save if created new
+ save();
+ }
+
+ // Only two children :) check and ensure Illustration 1
+ _operations = _xml_doc->root()->firstChild();
+ _params = _xml_doc->root()->lastChild();
+}
+
+CPHistoryXML::~CPHistoryXML()
+{
+ Inkscape::GC::release(_xml_doc);
+}
+void CPHistoryXML::add_action(const std::string &full_action_name)
+{
+ add_operation(HistoryType::ACTION, full_action_name);
+}
+
+void CPHistoryXML::add_import(const std::string &uri)
+{
+ add_operation(HistoryType::IMPORT_FILE, uri);
+}
+void CPHistoryXML::add_open(const std::string &uri)
+{
+ add_operation(HistoryType::OPEN_FILE, uri);
+}
+
+void CPHistoryXML::add_action_parameter(const std::string &full_action_name, const std::string &param)
+{
+ /* Creates
+ * <params>
+ * +1 <action name="full.action-name">
+ * + <param>30</param>
+ * + <param>60</param>
+ * + <param>90</param>
+ * +1 <action name="full.action-name">
+ * <params>
+ *
+ * + : generally creates
+ * +1: creates once
+ */
+ const auto parameter_node = _xml_doc->createElement("param");
+ const auto parameter_text = _xml_doc->createTextNode(param.c_str());
+
+ parameter_node->appendChild(parameter_text);
+ Inkscape::GC::release(parameter_text);
+
+ for (auto action_iter = _params->firstChild(); action_iter; action_iter = action_iter->next()) {
+ // If this action's node already exists
+ if (full_action_name == action_iter->attribute("name")) {
+ // If the last parameter was the same don't do anything, inner text is also a node hence 2 times last
+ // child
+ if (action_iter->lastChild()->lastChild() && action_iter->lastChild()->lastChild()->content() == param) {
+ Inkscape::GC::release(parameter_node);
+ return;
+ }
+
+ // If last current than parameter is different, add current
+ action_iter->appendChild(parameter_node);
+ Inkscape::GC::release(parameter_node);
+
+ save();
+ return;
+ }
+ }
+
+ // only encountered when the actions element doesn't already exists,so we create that action's element
+ const auto action_node = _xml_doc->createElement("action");
+ action_node->setAttribute("name", full_action_name.c_str());
+ action_node->appendChild(parameter_node);
+
+ _params->appendChild(action_node);
+ save();
+
+ Inkscape::GC::release(action_node);
+ Inkscape::GC::release(parameter_node);
+}
+
+std::optional<History> CPHistoryXML::get_last_operation()
+{
+ auto last_child = _operations->lastChild();
+ if (last_child) {
+ if (const auto operation_type = _get_operation_type(last_child); operation_type.has_value()) {
+ // inner text is a text Node thus last child
+ return History{*operation_type, last_child->lastChild()->content()};
+ }
+ }
+ return std::nullopt;
+}
+std::vector<History> CPHistoryXML::get_operation_history() const
+{
+ // TODO: add max items in history
+ std::vector<History> history;
+ for (auto operation_iter = _operations->firstChild(); operation_iter; operation_iter = operation_iter->next()) {
+ if (const auto operation_type = _get_operation_type(operation_iter); operation_type.has_value()) {
+ history.emplace_back(*operation_type, operation_iter->firstChild()->content());
+ }
+ }
+ return history;
+}
+
+std::vector<std::string> CPHistoryXML::get_action_parameter_history(const std::string &full_action_name) const
+{
+ std::vector<std::string> params;
+ for (auto action_iter = _params->firstChild(); action_iter; action_iter = action_iter->prev()) {
+ // If this action's node already exists
+ if (full_action_name == action_iter->attribute("name")) {
+ // lastChild and prev for LIFO order
+ for (auto param_iter = _params->lastChild(); param_iter; param_iter = param_iter->prev()) {
+ params.emplace_back(param_iter->content());
+ }
+ return params;
+ }
+ }
+ // action not used previously so no params;
+ return {};
+}
+
+void CPHistoryXML::save() const
+{
+ sp_repr_save_file(_xml_doc, _file_path.c_str());
+}
+
+void CPHistoryXML::add_operation(const HistoryType history_type, const std::string &data)
+{
+ std::string operation_type_name;
+ switch (history_type) {
+ // see Illustration 1
+ case HistoryType::ACTION:
+ operation_type_name = "action";
+ break;
+ case HistoryType::IMPORT_FILE:
+ operation_type_name = "import";
+ break;
+ case HistoryType::OPEN_FILE:
+ operation_type_name = "open";
+ break;
+ default:
+ return;
+ }
+ auto operation_to_add = _xml_doc->createElement(operation_type_name.c_str()); // action, import, open
+ auto operation_data = _xml_doc->createTextNode(data.c_str());
+ operation_data->setContent(data.c_str());
+
+ operation_to_add->appendChild(operation_data);
+ _operations->appendChild(operation_to_add);
+
+ Inkscape::GC::release(operation_data);
+ Inkscape::GC::release(operation_to_add);
+
+ save();
+}
+std::optional<HistoryType> CPHistoryXML::_get_operation_type(Inkscape::XML::Node *operation)
+{
+ const std::string operation_type_name = operation->name();
+
+ if (operation_type_name == "action") {
+ return HistoryType::ACTION;
+ } else if (operation_type_name == "import") {
+ return HistoryType::IMPORT_FILE;
+ } else if (operation_type_name == "open") {
+ return HistoryType::OPEN_FILE;
+ } else {
+ return std::nullopt;
+ // unknown HistoryType
+ }
+}
+
+} // 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/command-palette.h b/src/ui/dialog/command-palette.h
new file mode 100644
index 0000000..4b848fa
--- /dev/null
+++ b/src/ui/dialog/command-palette.h
@@ -0,0 +1,271 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** \file
+ * CommandPalette: Class providing Command Palette feature
+ *
+ * Authors:
+ * Abhay Raj Singh <abhayonlyone@gmail.com>
+ *
+ * Copyright (C) 2020 Autors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_DIALOG_COMMAND_PALETTE_H
+#define INKSCAPE_DIALOG_COMMAND_PALETTE_H
+
+#include <utility>
+#include <vector>
+
+#include <giomm/action.h>
+#include <giomm/application.h>
+#include <glibmm/refptr.h>
+#include <glibmm/ustring.h>
+#include <gtkmm/box.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/button.h>
+#include <gtkmm/eventbox.h>
+#include <gtkmm/label.h>
+#include <gtkmm/listbox.h>
+#include <gtkmm/listboxrow.h>
+#include <gtkmm/recentinfo.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/searchbar.h>
+#include <gtkmm/searchentry.h>
+#include <gtkmm/viewport.h>
+
+#include "inkscape.h"
+#include "ui/dialog/align-and-distribute.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+// Enables using switch case
+enum class TypeOfVariant
+{
+ NONE,
+ UNKNOWN,
+ BOOL,
+ INT,
+ DOUBLE,
+ STRING,
+ TUPLE_DD
+};
+
+enum class CPMode
+{
+ SEARCH,
+ INPUT, // Input arguments
+ SHELL,
+ HISTORY
+};
+
+enum class HistoryType
+{
+ LPE,
+ ACTION,
+ OPEN_FILE,
+ IMPORT_FILE,
+};
+
+struct History
+{
+ HistoryType history_type;
+ std::string data;
+
+ History(HistoryType ht, std::string &&data)
+ : history_type(ht)
+ , data(data)
+ {}
+};
+
+class CPHistoryXML
+{
+public:
+ // constructors, asssignment, destructor
+ CPHistoryXML();
+ ~CPHistoryXML();
+
+ // Handy wrappers for code clearity
+ void add_action(const std::string &full_action_name);
+
+ void add_import(const std::string &uri);
+ void add_open(const std::string &uri);
+
+ // Remember parameter for action
+ void add_action_parameter(const std::string &full_action_name, const std::string &param);
+
+ std::optional<History> get_last_operation();
+
+ // To construct _CPHistory
+ std::vector<History> get_operation_history() const;
+ // To get parameter history when an action is selected, LIFO stack like so more recent first
+ std::vector<std::string> get_action_parameter_history(const std::string &full_action_name) const;
+
+private:
+ void save() const;
+
+ void add_operation(const HistoryType history_type, const std::string &data);
+
+ static std::optional<HistoryType> _get_operation_type(Inkscape::XML::Node *operation);
+
+ const std::string _file_path;
+
+ Inkscape::XML::Document *_xml_doc;
+ // handy for xml doc child
+ Inkscape::XML::Node *_operations;
+ Inkscape::XML::Node *_params;
+};
+
+class CommandPalette
+{
+public: // API
+ CommandPalette();
+ ~CommandPalette() = default;
+
+ CommandPalette(CommandPalette const &) = delete; // no copy
+ CommandPalette &operator=(CommandPalette const &) = delete; // no assignment
+
+ void open();
+ void close();
+ void toggle();
+
+ Gtk::Box *get_base_widget();
+
+private: // Helpers
+ using ActionPtr = Glib::RefPtr<Gio::Action>;
+ using ActionPtrName = std::pair<ActionPtr, Glib::ustring>;
+
+ /**
+ * Insert actions in _CPSuggestions
+ */
+ void load_app_actions();
+ void load_win_doc_actions();
+
+ void append_recent_file_operation(const Glib::ustring &path, bool is_suggestion, bool is_import = true);
+ bool generate_action_operation(const ActionPtrName &action_ptr_name, const bool is_suggestion);
+
+private: // Signal handlers
+ void on_search();
+
+ int on_filter_general(Gtk::ListBoxRow *child);
+ bool on_filter_full_action_name(Gtk::ListBoxRow *child);
+ bool on_filter_recent_file(Gtk::ListBoxRow *child, bool const is_import);
+
+ bool on_key_press_cpfilter_escape(GdkEventKey *evt);
+ bool on_key_press_cpfilter_search_mode(GdkEventKey *evt);
+ bool on_key_press_cpfilter_input_mode(GdkEventKey *evt, const ActionPtrName &action_ptr_name);
+ bool on_key_press_cpfilter_history_mode(GdkEventKey *evt);
+
+ /**
+ * when search bar is empty
+ */
+ void hide_suggestions();
+
+ /**
+ * when search bar isn't empty
+ */
+ void show_suggestions();
+
+ void on_row_activated(Gtk::ListBoxRow *activated_row);
+ void on_history_selection_changed(Gtk::ListBoxRow *lb);
+
+ bool operate_recent_file(Glib::ustring const &uri, bool const import);
+
+ void on_action_fullname_clicked(const Glib::ustring &action_fullname);
+
+ /**
+ * Implements text matching logic
+ */
+ static bool fuzzy_search(const Glib::ustring &subject, const Glib::ustring &search);
+ static bool normal_search(const Glib::ustring &subject, const Glib::ustring &search);
+ static bool fuzzy_tolerance_search(const Glib::ustring &subject, const Glib::ustring &search);
+ static int fuzzy_points(const Glib::ustring &subject, const Glib::ustring &search);
+ static int fuzzy_tolerance_points(const Glib::ustring &subject, const Glib::ustring &search);
+ static int fuzzy_points_compare(int fuzzy_points_count_1, int fuzzy_points_count_2, int text_len_1, int text_len_2);
+ int on_sort(Gtk::ListBoxRow *row1, Gtk::ListBoxRow *row2);
+ void set_mode(CPMode mode);
+
+ /**
+ * Color addition in searched character
+ */
+ void add_color(Gtk::Label *label, const Glib::ustring &search, const Glib::ustring &subject, bool tooltip=false);
+ void remove_color(Gtk::Label *label, const Glib::ustring &subject, bool tooltip=false);
+ static void add_color_description(Gtk::Label *label, const Glib::ustring &search);
+
+ /**
+ * Executes Action
+ */
+ bool ask_action_parameter(const ActionPtrName &action);
+ static ActionPtrName get_action_ptr_name(const Glib::ustring &full_action_name);
+ bool execute_action(const ActionPtrName &action, const Glib::ustring &value);
+
+ static TypeOfVariant get_action_variant_type(const ActionPtr &action_ptr);
+
+ static std::pair<Gtk::Label *, Gtk::Label *> get_name_desc(Gtk::ListBoxRow *child);
+ Gtk::Label *get_full_action_name(Gtk::ListBoxRow *child);
+
+private: // variables
+ // Widgets
+ Glib::RefPtr<Gtk::Builder> _builder;
+
+ Gtk::Box *_CPBase;
+ Gtk::Box *_CPHeader;
+ Gtk::Box *_CPListBase;
+
+ Gtk::SearchBar *_CPSearchBar;
+ Gtk::SearchEntry *_CPFilter;
+
+ Gtk::ListBox *_CPSuggestions;
+ Gtk::ListBox *_CPHistory;
+
+ Gtk::ScrolledWindow *_CPSuggestionsScroll;
+ Gtk::ScrolledWindow *_CPHistoryScroll;
+
+ // Data
+ const int _max_height_requestable = 360;
+ Glib::ustring _search_text;
+
+ // States
+ bool _is_open = false;
+ bool _win_doc_actions_loaded = false;
+
+ // History
+ CPHistoryXML _history_xml;
+ /**
+ * Remember the mode we are in helps in unnecessary signal disconnection and reconnection
+ * Used by set_mode()
+ */
+ CPMode _mode = CPMode::SHELL;
+ // Default value other than SEARCH required
+ // set_mode() switches between mode hence checks if it already in the target mode.
+ // Constructed value is sometimes SEARCH being the first Item for now
+ // set_mode() never attaches the on search listener then
+ // This initialising value can be any thing other than the initial required mode
+ // Example currently it's open in search mode
+
+ /**
+ * Stores the search connection to deactivate when not needed
+ */
+ sigc::connection _cpfilter_search_connection;
+ /**
+ * Stores the key_press connection to deactivate when not needed
+ */
+ sigc::connection _cpfilter_key_press_connection;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_DIALOG_COMMAND_PALETTE_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..b9e7f28
--- /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, -1);
+ 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/dialog-base.cpp b/src/ui/dialog/dialog-base.cpp
new file mode 100644
index 0000000..154c811
--- /dev/null
+++ b/src/ui/dialog/dialog-base.cpp
@@ -0,0 +1,318 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/** @file
+ * @brief A base class for all dialogs.
+ *
+ * Authors: see git history
+ * Tavmjong Bah
+ *
+ * Copyright (c) 2018 Tavmjong Bah, Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "dialog-base.h"
+
+#include <glibmm/i18n.h>
+#include <glibmm/main.h>
+#include <glibmm/refptr.h>
+#include <gtkmm/cssprovider.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/scrolledwindow.h>
+#include <iostream>
+
+#include "inkscape.h"
+#include "desktop.h"
+#include "ui/dialog/dialog-data.h"
+#include "ui/dialog/dialog-notebook.h"
+#include "ui/dialog-events.h"
+// get_latin_keyval
+#include "ui/tools/tool-base.h"
+#include "widgets/spw-utilities.h"
+#include "ui/widget/canvas.h"
+#include "ui/util.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/**
+ * DialogBase constructor.
+ *
+ * @param prefs_path characteristic path to load/save dialog position.
+ * @param dialog_type is the "type" string for the dialog.
+ */
+DialogBase::DialogBase(gchar const *prefs_path, Glib::ustring dialog_type)
+ : Gtk::Box(Gtk::ORIENTATION_VERTICAL)
+ , desktop(nullptr)
+ , document(nullptr)
+ , selection(nullptr)
+ , _name("DialogBase")
+ , _prefs_path(prefs_path)
+ , _dialog_type(dialog_type)
+ , _app(InkscapeApplication::instance())
+{
+ auto const &dialog_data = get_dialog_data();
+
+ // Derive a pretty display name for the dialog.
+ auto it = dialog_data.find(dialog_type);
+ if (it != dialog_data.end()) {
+
+ _name = it->second.label; // Already translated
+
+ // remove ellipsis and mnemonics
+ int pos = _name.find("...", 0);
+ if (pos >= 0 && pos < _name.length() - 2) {
+ _name.erase(pos, 3);
+ }
+ pos = _name.find("…", 0);
+ if (pos >= 0 && pos < _name.length()) {
+ _name.erase(pos, 1);
+ }
+ pos = _name.find("_", 0);
+ if (pos >= 0 && pos < _name.length()) {
+ _name.erase(pos, 1);
+ }
+ }
+
+ set_name(_dialog_type); // Essential for dialog functionality
+ property_margin().set_value(1); // Essential for dialog UI
+}
+
+DialogBase::~DialogBase() {
+#ifdef _WIN32
+ // this is bad, but it supposedly fixes some resizng problem on Windows
+ ensure_size();
+#endif
+
+ unsetDesktop();
+};
+
+void DialogBase::ensure_size() {
+ if (desktop) {
+ resize_widget_children(desktop->getToplevel());
+ resize_widget_children(this);
+ }
+}
+
+void DialogBase::on_map() {
+ // Update asks the dialogs if they need their Gtk widgets updated.
+ update();
+ // Set the desktop on_map, although we might want to be smarter about this.
+ // Note: Inkscape::Application::instance().active_desktop() is used here, as it contains current desktop at
+ // the time of dialog creation. Formerly used _app.get_active_view() did not at application start-up.
+ setDesktop(Inkscape::Application::instance().active_desktop());
+ parent_type::on_map();
+ ensure_size();
+}
+
+bool DialogBase::on_key_press_event(GdkEventKey* key_event) {
+ switch (Inkscape::UI::Tools::get_latin_keyval(key_event)) {
+ case GDK_KEY_Escape:
+ defocus_dialog();
+ return true;
+ }
+
+ return parent_type::on_key_press_event(key_event);
+}
+
+
+/**
+ * Highlight notebook where dialog already exists.
+ */
+void DialogBase::blink()
+{
+ Gtk::Notebook *notebook = dynamic_cast<Gtk::Notebook *>(get_parent());
+ if (notebook && notebook->get_is_drawable()) {
+ // Switch notebook to this dialog.
+ notebook->set_current_page(notebook->page_num(*this));
+ notebook->get_style_context()->add_class("blink");
+
+ // Add timer to turn off blink.
+ sigc::slot<bool ()> slot = sigc::mem_fun(*this, &DialogBase::blink_off);
+ sigc::connection connection = Glib::signal_timeout().connect(slot, 1000); // msec
+ }
+}
+
+void DialogBase::focus_dialog() {
+ if (auto window = dynamic_cast<Gtk::Window*>(get_toplevel())) {
+ window->present();
+ }
+
+ // widget that had focus, if any
+ if (auto child = get_focus_child()) {
+ child->grab_focus();
+ }
+ else {
+ // find first focusable widget
+ if (auto child = sp_find_focusable_widget(this)) {
+ child->grab_focus();
+ }
+ }
+}
+
+void DialogBase::defocus_dialog() {
+ if (auto wnd = dynamic_cast<Gtk::Window*>(get_toplevel())) {
+ // defocus floating dialog:
+ sp_dialog_defocus_cpp(wnd);
+
+ // for docked dialogs, move focus to canvas
+ if (auto desktop = getDesktop()) {
+ desktop->getCanvas()->grab_focus();
+ }
+ }
+}
+
+/**
+ * Callback to reset the dialog highlight.
+ */
+bool DialogBase::blink_off()
+{
+ Gtk::Notebook *notebook = dynamic_cast<Gtk::Notebook *>(get_parent());
+ if (notebook && notebook->get_is_drawable()) {
+ notebook->get_style_context()->remove_class("blink");
+ }
+ return false;
+}
+
+/**
+ * Called when the desktop might have changed for this dialog.
+ */
+void DialogBase::setDesktop(SPDesktop *new_desktop)
+{
+ if (desktop == new_desktop) {
+ return;
+ }
+
+ unsetDesktop();
+
+ if (new_desktop) {
+ desktop = new_desktop;
+
+ if (auto sel = desktop->getSelection()) {
+ selection = sel;
+ _select_changed = selection->connectChanged([this](Inkscape::Selection *selection) {
+ _changed_while_hidden = !_showing;
+ if (_showing)
+ selectionChanged(selection);
+ });
+ _select_modified = selection->connectModified([this](Inkscape::Selection *selection, guint flags) {
+ _modified_while_hidden = !_showing;
+ _modified_flags = flags;
+ if (_showing)
+ selectionModified(selection, flags);
+ });
+ }
+
+ _doc_replaced = desktop->connectDocumentReplaced(sigc::hide<0>(sigc::mem_fun(*this, &DialogBase::setDocument)));
+ _desktop_destroyed = desktop->connectDestroy(sigc::mem_fun(*this, &DialogBase::desktopDestroyed));
+ this->setDocument(desktop->getDocument());
+
+ if (desktop->getSelection()) {
+ selectionChanged(selection);
+ }
+ set_sensitive(true);
+ }
+
+ desktopReplaced();
+}
+
+//
+void DialogBase::fix_inner_scroll(Gtk::Widget *scrollwindow)
+{
+ auto scrollwin = dynamic_cast<Gtk::ScrolledWindow *>(scrollwindow);
+ auto viewport = dynamic_cast<Gtk::ScrolledWindow *>(scrollwin->get_child());
+ Gtk::Widget *child = nullptr;
+ if (viewport) { //some widgets has viewportother not
+ child = viewport->get_child();
+ } else {
+ child = scrollwin->get_child();
+ }
+ if (child && scrollwin) {
+ Glib::RefPtr<Gtk::Adjustment> adjustment = scrollwin->get_vadjustment();
+ child->signal_scroll_event().connect([=](GdkEventScroll* event) {
+ auto container = dynamic_cast<Gtk::Container *>(this);
+ if (container) {
+ std::vector<Gtk::Widget*> widgets = container->get_children();
+ if (widgets.size()) {
+ auto parentscroll = dynamic_cast<Gtk::ScrolledWindow *>(widgets[0]);
+ if (parentscroll) {
+ if (event->delta_y > 0 && (adjustment->get_value() + adjustment->get_page_size()) == adjustment->get_upper()) {
+ parentscroll->event((GdkEvent*)event);
+ return true;
+ } else if (event->delta_y < 0 && adjustment->get_value() == adjustment->get_lower()) {
+ parentscroll->event((GdkEvent*)event);
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ });
+ }
+}
+
+/**
+ * function called from notebook dialog that performs an update of the dialog and sets the dialog showing state true
+ */
+void
+DialogBase::setShowing(bool showing) {
+ _showing = showing;
+ if (showing && _changed_while_hidden) {
+ selectionChanged(getSelection());
+ _changed_while_hidden = false;
+ }
+ if (showing && _modified_while_hidden) {
+ selectionModified(getSelection(), _modified_flags);
+ _modified_while_hidden = false;
+ }
+}
+
+/**
+ * Called to destruct desktops, must not call virtuals
+ */
+void DialogBase::unsetDesktop()
+{
+ desktop = nullptr;
+ document = nullptr;
+ selection = nullptr;
+ _desktop_destroyed.disconnect();
+ _doc_replaced.disconnect();
+ _select_changed.disconnect();
+ _select_modified.disconnect();
+}
+
+void DialogBase::desktopDestroyed(SPDesktop* old_desktop)
+{
+ if (old_desktop == desktop && desktop) {
+ unsetDesktop();
+ desktopReplaced();
+ set_sensitive(false);
+ }
+}
+
+/**
+ * Called when the document might have changed, called from setDesktop too.
+ */
+void DialogBase::setDocument(SPDocument *new_document)
+{
+ if (document != new_document) {
+ document = new_document;
+ documentReplaced();
+ }
+}
+
+} // 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-base.h b/src/ui/dialog/dialog-base.h
new file mode 100644
index 0000000..a0f44ea
--- /dev/null
+++ b/src/ui/dialog/dialog-base.h
@@ -0,0 +1,139 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifndef INK_DIALOG_BASE_H
+#define INK_DIALOG_BASE_H
+
+/** @file
+ * @brief A base class for all dialogs.
+ *
+ * Authors: see git history
+ * Tavmjong Bah
+ *
+ * Copyright (c) 2018 Tavmjong Bah, Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/ustring.h>
+#include <gtkmm/box.h>
+
+#include "inkscape-application.h"
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/**
+ * DialogBase is the base class for the dialog system.
+ *
+ * Each dialog has a reference to the application, in order to update its inner focus
+ * (be it of the active desktop, document, selection, etc.) in the update() method.
+ *
+ * DialogBase derived classes' instances live in DialogNotebook classes and are managed by
+ * DialogContainer classes. DialogContainer instances can have at most one type of dialog,
+ * differentiated by the associated type.
+ */
+class DialogBase : public Gtk::Box
+{
+ using parent_type = Gtk::Box;
+
+public:
+ DialogBase(char const *prefs_path = nullptr, Glib::ustring dialog_type = "");
+ DialogBase(DialogBase const &) = delete;
+ DialogBase &operator=(DialogBase const &) = delete;
+ ~DialogBase() override;
+
+ /**
+ * The update() method is essential to Gtk state management. DialogBase implementations get updated whenever
+ * a new focus event happens if they are in a DialogWindow or if they are in the currently focused window.
+ *
+ * DO NOT use update to keep SPDesktop, SPDocument or Selection states, use the virtual functions below.
+ */
+ virtual void update() {}
+
+ // Public for future use, say if the desktop is smartly set when docking dialogs.
+ void setDesktop(SPDesktop *new_desktop);
+
+ void on_map() override;
+
+ /*
+ * Often the dialog won't request the right size until the window has
+ * been pushed to resize all it's children. We do this on dialog creation
+ * and destruction.
+ */
+ void ensure_size();
+
+ // Getters and setters
+ Glib::ustring get_name() { return _name; };
+ const Glib::ustring& getPrefsPath() const { return _prefs_path; }
+ Glib::ustring const &get_type() const { return _dialog_type; }
+
+ void blink();
+ // find focusable widget to grab focus
+ void focus_dialog();
+ // return focus back to canvas
+ void defocus_dialog();
+ bool getShowing() { return _showing; }
+ // fix children scrolled windows to send outer scroll when his own reach limits
+ void fix_inner_scroll(Gtk::Widget *child);
+ // Too many dialogs have unprotected calls to ask for this data
+ SPDesktop *getDesktop() const { return desktop; }
+protected:
+ InkscapeApplication *getApp() const { return _app; }
+ SPDocument *getDocument() const { return document; }
+ Selection *getSelection() const { return selection; }
+ friend class DialogNotebook;
+ void setShowing(bool showing);
+ Glib::ustring _name; // Gtk widget name (must be set!)
+ Glib::ustring const _prefs_path; // Stores characteristic path for loading/saving the dialog position.
+ Glib::ustring const _dialog_type; // Type of dialog (we could just use _pref_path?).
+private:
+ bool blink_off(); // timer callback
+ bool on_key_press_event(GdkEventKey* key_event) override;
+ // return if dialog is on visible tab
+ bool _showing = true;
+ void unsetDesktop();
+ void desktopDestroyed(SPDesktop* old_desktop);
+ void setDocument(SPDocument *new_document);
+ /**
+ * Called when the desktop has certainly changed. It may have changed to nullptr
+ * when destructing the dialog, so the override should expect nullptr too.
+ */
+ virtual void desktopReplaced() {}
+ virtual void documentReplaced() {}
+ virtual void selectionChanged(Inkscape::Selection *selection) {};
+ virtual void selectionModified(Inkscape::Selection *selection, guint flags) {};
+
+ sigc::connection _desktop_destroyed;
+ sigc::connection _doc_replaced;
+ sigc::connection _select_changed;
+ sigc::connection _select_modified;
+
+ int _modified_flags = 0;
+ bool _modified_while_hidden = false;
+ bool _changed_while_hidden = false;
+
+ InkscapeApplication *_app; // Used for state management
+ SPDesktop *desktop;
+ SPDocument *document;
+ Selection *selection;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INK_DIALOG_BASE_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-container.cpp b/src/ui/dialog/dialog-container.cpp
new file mode 100644
index 0000000..be5e587
--- /dev/null
+++ b/src/ui/dialog/dialog-container.cpp
@@ -0,0 +1,1153 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/** @file
+ * @brief A widget that manages DialogNotebook's and other widgets inside a horizontal DialogMultipaned.
+ *
+ * Authors: see git history
+ * Tavmjong Bah
+ *
+ * Copyright (c) 2018 Tavmjong Bah, Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "dialog-container.h"
+
+#include <glibmm/i18n.h>
+#include <giomm/file.h>
+#include <glibmm/keyfile.h>
+#include <gtkmm/box.h>
+#include <gtkmm/eventbox.h>
+#include <gtkmm/image.h>
+
+#include "enums.h"
+#include "inkscape-application.h"
+#include "inkscape-window.h"
+// #include "ui/dialog/align-and-distribute.h"
+#include "ui/dialog/clonetiler.h"
+#include "ui/dialog/dialog-data.h"
+#include "ui/dialog/dialog-multipaned.h"
+#include "ui/dialog/dialog-notebook.h"
+#include "ui/dialog/dialog-window.h"
+#include "ui/dialog/document-properties.h"
+#include "ui/dialog/document-resources.h"
+#include "ui/dialog/export.h"
+#include "ui/dialog/fill-and-stroke.h"
+#include "ui/dialog/filter-effects-dialog.h"
+#include "ui/dialog/find.h"
+#include "ui/dialog/font-collections-manager.h"
+#include "ui/dialog/glyphs.h"
+#include "ui/dialog/icon-preview.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/object-attributes.h"
+#include "ui/dialog/object-properties.h"
+#include "ui/dialog/objects.h"
+#include "ui/dialog/paint-servers.h"
+#include "ui/dialog/selectorsdialog.h"
+#if WITH_GSPELL
+#include "ui/dialog/spellcheck.h"
+#endif
+#include "ui/dialog/svg-fonts-dialog.h"
+#include "ui/dialog/swatches.h"
+#include "ui/dialog/symbols.h"
+#include "ui/dialog/text-edit.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/xml-tree.h"
+#include "ui/icon-names.h"
+#include "ui/themes.h"
+#include "ui/widget/canvas-grid.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+DialogContainer::~DialogContainer() {
+ // delete columns; desktop widget deletes dialog container before it get "unrealized",
+ // so it doesn't get a chance to remove them
+ delete columns;
+}
+
+DialogContainer::DialogContainer(InkscapeWindow* inkscape_window)
+ : _inkscape_window(inkscape_window)
+{
+ g_assert(_inkscape_window != nullptr);
+
+ get_style_context()->add_class("DialogContainer");
+
+ // Setup main column
+ columns = Gtk::manage(new DialogMultipaned(Gtk::ORIENTATION_HORIZONTAL));
+
+ connections.emplace_back(columns->signal_prepend_drag_data().connect(
+ sigc::bind<DialogMultipaned *>(sigc::mem_fun(*this, &DialogContainer::prepend_drop), columns)));
+
+ connections.emplace_back(columns->signal_append_drag_data().connect(
+ sigc::bind<DialogMultipaned *>(sigc::mem_fun(*this, &DialogContainer::append_drop), columns)));
+
+ // Setup drop targets.
+ target_entries.emplace_back(Gtk::TargetEntry("GTK_NOTEBOOK_TAB"));
+ columns->set_target_entries(target_entries);
+
+ add(*columns);
+
+ // Should probably be moved to window.
+ // connections.emplace_back(signal_unmap().connect(sigc::mem_fun(*this, &DialogContainer::cb_on_unmap)));
+
+ show_all_children();
+}
+
+DialogMultipaned *DialogContainer::create_column()
+{
+ DialogMultipaned *column = Gtk::manage(new DialogMultipaned(Gtk::ORIENTATION_VERTICAL));
+
+ connections.emplace_back(column->signal_prepend_drag_data().connect(
+ sigc::bind<DialogMultipaned *>(sigc::mem_fun(*this, &DialogContainer::prepend_drop), column)));
+
+ connections.emplace_back(column->signal_append_drag_data().connect(
+ sigc::bind<DialogMultipaned *>(sigc::mem_fun(*this, &DialogContainer::append_drop), column)));
+
+ connections.emplace_back(column->signal_now_empty().connect(
+ sigc::bind<DialogMultipaned *>(sigc::mem_fun(*this, &DialogContainer::column_empty), column)));
+
+ column->set_target_entries(target_entries);
+
+ return column;
+}
+
+/**
+ * Get an instance of a DialogBase dialog using the associated dialog name.
+ */
+std::unique_ptr<DialogBase> DialogContainer::dialog_factory(Glib::ustring const &dialog_type)
+{
+ // clang-format off
+ if (dialog_type == "AlignDistribute") return std::make_unique<ArrangeDialog>();
+ else if (dialog_type == "CloneTiler") return std::make_unique<CloneTiler>();
+ else if (dialog_type == "DocumentProperties") return std::make_unique<DocumentProperties>();
+ else if (dialog_type == "DocumentResources") return std::make_unique<DocumentResources>();
+ else if (dialog_type == "Export") return std::make_unique<Export>();
+ else if (dialog_type == "FillStroke") return std::make_unique<FillAndStroke>();
+ else if (dialog_type == "FilterEffects") return std::make_unique<FilterEffectsDialog>();
+ else if (dialog_type == "Find") return std::make_unique<Find>();
+ else if (dialog_type == "FontCollections") return std::make_unique<FontCollectionsManager>();
+ else if (dialog_type == "Glyphs") return std::make_unique<GlyphsPanel>();
+ else if (dialog_type == "IconPreview") return std::make_unique<IconPreviewPanel>();
+ else if (dialog_type == "Input") return InputDialog::create();
+ else if (dialog_type == "LivePathEffect") return std::make_unique<LivePathEffectEditor>();
+ else if (dialog_type == "Memory") return std::make_unique<Memory>();
+ else if (dialog_type == "Messages") return std::make_unique<Messages>();
+ else if (dialog_type == "ObjectAttributes") return std::make_unique<ObjectAttributes>();
+ else if (dialog_type == "ObjectProperties") return std::make_unique<ObjectProperties>();
+ else if (dialog_type == "Objects") return std::make_unique<ObjectsPanel>();
+ else if (dialog_type == "PaintServers") return std::make_unique<PaintServersDialog>();
+ else if (dialog_type == "Preferences") return std::make_unique<InkscapePreferences>();
+ else if (dialog_type == "Selectors") return std::make_unique<SelectorsDialog>();
+ else if (dialog_type == "SVGFonts") return std::make_unique<SvgFontsDialog>();
+ else if (dialog_type == "Swatches") return std::make_unique<SwatchesPanel>();
+ else if (dialog_type == "Symbols") return std::make_unique<SymbolsDialog>();
+ else if (dialog_type == "Text") return std::make_unique<TextEdit>();
+ else if (dialog_type == "Trace") return TraceDialog::create();
+ else if (dialog_type == "Transform") return std::make_unique<Transformation>();
+ else if (dialog_type == "UndoHistory") return std::make_unique<UndoHistory>();
+ else if (dialog_type == "XMLEditor") return std::make_unique<XmlTree>();
+#if WITH_GSPELL
+ else if (dialog_type == "Spellcheck") return std::make_unique<SpellCheck>();
+#endif
+#ifdef DEBUG
+ else if (dialog_type == "Prototype") return std::make_unique<Prototype>();
+#endif
+ else {
+ std::cerr << "DialogContainer::dialog_factory: Unhandled dialog: " << dialog_type.raw() << std::endl;
+ return nullptr;
+ }
+ // clang-format on
+}
+
+// Create the notebook tab
+Gtk::Widget *DialogContainer::create_notebook_tab(Glib::ustring label_str, Glib::ustring image_str, const Glib::ustring shortcut)
+{
+ Gtk::Label *label = Gtk::manage(new Gtk::Label(label_str));
+ Gtk::Image *image = Gtk::manage(new Gtk::Image());
+ Gtk::Button *close = Gtk::manage(new Gtk::Button());
+ image->set_from_icon_name(image_str, Gtk::ICON_SIZE_MENU);
+ Gtk::Box *tab = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 2));
+ close->set_image_from_icon_name("window-close");
+ close->set_halign(Gtk::ALIGN_END);
+ close->set_tooltip_text(_("Close Tab"));
+ close->get_style_context()->add_class("close-button");
+ Glib::ustring label_str_fix = label_str;
+ label_str_fix = Glib::Regex::create("\\W")->replace_literal(label_str_fix, 0, "-", (Glib::RegexMatchFlags)0);
+ tab->get_style_context()->add_class(label_str_fix);
+ tab->pack_start(*image);
+ tab->pack_end(*close);
+ tab->pack_end(*label);
+ tab->show_all();
+
+ // Workaround to the fact that Gtk::Box doesn't receive on_button_press event
+ Gtk::EventBox *cover = Gtk::manage(new Gtk::EventBox());
+ cover->add(*tab);
+
+ // Add shortcut tooltip
+ if (shortcut.size() > 0) {
+ auto tlabel = shortcut;
+ int pos = tlabel.find("&", 0);
+ if (pos >= 0 && pos < tlabel.length()) {
+ tlabel.replace(pos, 1, "&amp;");
+ }
+ tab->set_tooltip_markup(label_str + " (<b>" + tlabel + "</b>)");
+ } else {
+ tab->set_tooltip_text(label_str);
+ }
+
+ return cover;
+}
+
+// find dialog's multipaned parent; is there a better way?
+DialogMultipaned* get_dialog_parent(DialogBase* dialog) {
+ if (!dialog) return nullptr;
+
+ // dialogs are nested inside Gtk::Notebook
+ if (auto notebook = dynamic_cast<Gtk::Notebook*>(dialog->get_parent())) {
+ // notebooks are inside viewport, inside scrolled window
+ if (auto viewport = dynamic_cast<Gtk::Viewport*>(notebook->get_parent())) {
+ if (auto scroll = dynamic_cast<Gtk::ScrolledWindow*>(viewport->get_parent())) {
+ // finally get the panel
+ if (auto panel = dynamic_cast<DialogMultipaned*>(scroll->get_parent())) {
+ return panel;
+ }
+ }
+ }
+ }
+
+ return nullptr;
+}
+
+/**
+ * Add new dialog to the current container or in a floating window, based on preferences.
+ */
+void DialogContainer::new_dialog(const Glib::ustring& dialog_type )
+{
+ // Open all dialogs as floating, if set in preferences
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs == nullptr) {
+ return;
+ }
+
+ int dockable = prefs->getInt("/options/dialogtype/value", PREFS_DIALOGS_BEHAVIOR_DOCKABLE);
+ bool floating = DialogManager::singleton().should_open_floating(dialog_type);
+ if (dockable == PREFS_DIALOGS_BEHAVIOR_FLOATING || floating) {
+ new_floating_dialog(dialog_type);
+ } else {
+ new_dialog(dialog_type, nullptr);
+ }
+
+ if (DialogBase* dialog = find_existing_dialog(dialog_type)) {
+ dialog->focus_dialog();
+ }
+}
+
+
+DialogBase* DialogContainer::find_existing_dialog(const Glib::ustring& dialog_type) {
+ DialogBase *existing_dialog = get_dialog(dialog_type);
+ if (!existing_dialog) {
+ existing_dialog = DialogManager::singleton().find_floating_dialog(dialog_type);
+ }
+ return existing_dialog;
+}
+
+/**
+ * Overloaded new_dialog
+ */
+void DialogContainer::new_dialog(const Glib::ustring& dialog_type, DialogNotebook *notebook)
+{
+ columns->ensure_multipaned_children();
+
+ // Limit each container to containing one of any type of dialog.
+ if (DialogBase* existing_dialog = find_existing_dialog(dialog_type)) {
+ // make sure parent window is not hidden/collapsed
+ if (auto panel = get_dialog_parent(existing_dialog)) {
+ panel->show();
+ }
+ // found existing dialog; blink & exit
+ existing_dialog->blink();
+ return;
+ }
+
+ // Create the dialog widget
+ DialogBase *dialog = dialog_factory(dialog_type).release(); // Evil, but necessitated by GTK.
+
+ if (!dialog) {
+ std::cerr << "DialogContainer::new_dialog(): couldn't find dialog for: " << dialog_type.raw() << std::endl;
+ return;
+ }
+
+ // manage the dialog instance
+ dialog = Gtk::manage(dialog);
+
+ // Create the notebook tab
+ auto const &dialog_data = get_dialog_data();
+ Glib::ustring image("inkscape-logo");
+ auto it = dialog_data.find(dialog_type);
+ if (it != dialog_data.end()) {
+ image = it->second.icon_name;
+ }
+
+ Glib::ustring label;
+ Glib::ustring action_name = "win.dialog-open('" + dialog_type + "')";
+ auto app = InkscapeApplication::instance();
+ std::vector<Glib::ustring> accels = app->gtk_app()->get_accels_for_action(action_name);
+ if (accels.size() > 0) {
+ guint key = 0;
+ Gdk::ModifierType mods;
+ Gtk::AccelGroup::parse(accels[0], key, mods);
+ label = Gtk::AccelGroup::get_label(key, mods);
+ }
+
+ Gtk::Widget *tab = create_notebook_tab(dialog->get_name(), image, label);
+
+ // If not from notebook menu add at top of last column.
+ if (!notebook) {
+ // Look to see if last column contains a multipane. If not, add one.
+ DialogMultipaned *last_column = dynamic_cast<DialogMultipaned *>(columns->get_last_widget());
+ if (!last_column) {
+ last_column = create_column();
+ columns->append(last_column);
+ }
+
+ // Look to see if first widget in column is notebook, if not add one.
+ notebook = dynamic_cast<DialogNotebook *>(last_column->get_first_widget());
+ if (!notebook) {
+ notebook = Gtk::manage(new DialogNotebook(this));
+ last_column->prepend(notebook);
+ }
+ }
+
+ // Add dialog
+ notebook->add_page(*dialog, *tab, dialog->get_name());
+
+ if (auto panel = dynamic_cast<DialogMultipaned*>(notebook->get_parent())) {
+ // if panel is collapsed, show it now, or else new dialog will be mysteriously missing
+ panel->show_all();
+ }
+}
+
+// recreate dialogs hosted (docked) in a floating DialogWindow; window will be created
+bool DialogContainer::recreate_dialogs_from_state(InkscapeWindow* inkscape_window, const Glib::KeyFile* keyfile)
+{
+ g_assert(inkscape_window != nullptr);
+
+ bool restored = false;
+ // Step 1: check if we want to load the state
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int save_state = prefs->getInt("/options/savedialogposition/value", PREFS_DIALOGS_STATE_SAVE);
+ if (save_state == PREFS_DIALOGS_STATE_NONE) {
+ return restored; // User has turned off this feature in Preferences
+ }
+
+ // if it isn't dockable, all saved docked dialogs are made floating
+ bool is_dockable =
+ prefs->getInt("/options/dialogtype/value", PREFS_DIALOGS_BEHAVIOR_DOCKABLE) != PREFS_DIALOGS_BEHAVIOR_FLOATING;
+
+ if (!is_dockable)
+ return false; // not applicable if docking is off
+
+ // Step 2: get the number of windows; should be 1
+ int windows_count = 0;
+ try {
+ // we may have no 'Windows' initially when recreating floating dialog state (state is empty)
+ if (keyfile->has_group("Windows") && keyfile->has_key("Windows", "Count")) {
+ windows_count = keyfile->get_integer("Windows", "Count");
+ }
+ } catch (Glib::Error &error) {
+ std::cerr << G_STRFUNC << ": " << error.what().raw() << std::endl;
+ }
+
+ // Step 3: for each window, load its state.
+ for (int window_idx = 0; window_idx < windows_count; ++window_idx) {
+ Glib::ustring group_name = "Window" + std::to_string(window_idx);
+
+ bool has_position = keyfile->has_key(group_name, "Position") && keyfile->get_boolean(group_name, "Position");
+ window_position_t pos;
+ if (has_position) { // floating window position recorded?
+ pos.x = keyfile->get_integer(group_name, "x");
+ pos.y = keyfile->get_integer(group_name, "y");
+ pos.width = keyfile->get_integer(group_name, "width");
+ pos.height = keyfile->get_integer(group_name, "height");
+ }
+ // Step 3.0: read the window parameters
+ int column_count = 0;
+ try {
+ column_count = keyfile->get_integer(group_name, "ColumnCount");
+ } catch (Glib::Error &error) {
+ std::cerr << G_STRFUNC << ": " << error.what().raw() << std::endl;
+ }
+
+ // Step 3.1: get the window's container columns where we want to create the dialogs
+ DialogWindow *dialog_window = new DialogWindow(inkscape_window, nullptr);
+ DialogContainer *active_container = dialog_window->get_container();
+ DialogMultipaned *active_columns = active_container ? active_container->get_columns() : nullptr;
+
+ if (!active_container || !active_columns) {
+ continue;
+ }
+
+ // Step 3.2: for each column, load its state
+ for (int column_idx = 0; column_idx < column_count; ++column_idx) {
+ Glib::ustring column_group_name = group_name + "Column" + std::to_string(column_idx);
+
+ // Step 3.2.0: read the column parameters
+ int notebook_count = 0;
+ bool before_canvas = false;
+ try {
+ notebook_count = keyfile->get_integer(column_group_name, "NotebookCount");
+ if (keyfile->has_key(column_group_name, "BeforeCanvas")) {
+ before_canvas = keyfile->get_boolean(column_group_name, "BeforeCanvas");
+ }
+ } catch (Glib::Error &error) {
+ std::cerr << G_STRFUNC << ": " << error.what().raw() << std::endl;
+ }
+
+ // Step 3.2.1: create the column
+ DialogMultipaned *column = active_container->create_column();
+
+ before_canvas ? active_columns->prepend(column) : active_columns->append(column);
+
+ // Step 3.2.2: for each noteboook, load its dialogs
+ for (int notebook_idx = 0; notebook_idx < notebook_count; ++notebook_idx) {
+ Glib::ustring key = "Notebook" + std::to_string(notebook_idx) + "Dialogs";
+
+ // Step 3.2.2.0 read the list of dialogs in the current notebook
+ std::vector<Glib::ustring> dialogs;
+ try {
+ dialogs = keyfile->get_string_list(column_group_name, key);
+ } catch (Glib::Error &error) {
+ std::cerr << G_STRFUNC << ": " << error.what().raw() << std::endl;
+ }
+
+ if (!dialogs.size()) {
+ continue;
+ }
+
+ DialogNotebook *notebook = nullptr;
+ auto const &dialog_data = get_dialog_data();
+
+ // Step 3.2.2.1 create each dialog in the current notebook
+ for (auto type : dialogs) {
+ if (DialogManager::singleton().find_floating_dialog(type)) {
+ // avoid duplicates
+ continue;
+ }
+
+ if (dialog_data.find(type) != dialog_data.end()) {
+ if (!notebook) {
+ notebook = Gtk::manage(new DialogNotebook(active_container));
+ column->append(notebook);
+ }
+ active_container->new_dialog(type, notebook);
+ } else {
+ std::cerr << "recreate_dialogs_from_state: invalid dialog type: " << type.raw() << std::endl;
+ }
+ }
+ }
+ }
+
+ if (has_position) {
+ dm_restore_window_position(*dialog_window, pos);
+ }
+ else {
+ dialog_window->update_window_size_to_fit_children();
+ }
+ dialog_window->show_all();
+ // Set the style and icon theme of the new menu based on the desktop
+ INKSCAPE.themecontext->getChangeThemeSignal().emit();
+ INKSCAPE.themecontext->add_gtk_css(true);
+ restored = true;
+ }
+
+ return restored;
+}
+
+/**
+ * Add a new floating dialog (or reuse existing one if it's already up)
+ */
+DialogWindow *DialogContainer::new_floating_dialog(const Glib::ustring& dialog_type)
+{
+ return create_new_floating_dialog(dialog_type, true);
+}
+
+DialogWindow *DialogContainer::create_new_floating_dialog(const Glib::ustring& dialog_type, bool blink)
+{
+ // check if this dialog is already open
+ if (DialogBase* existing_dialog = find_existing_dialog(dialog_type)) {
+ // found existing dialog; blink & exit
+ if (blink) {
+ existing_dialog->blink();
+ // show its window if it is hidden
+ if (auto window = DialogManager::singleton().find_floating_dialog_window(dialog_type)) {
+ DialogManager::singleton().set_floating_dialog_visibility(window, true);
+ }
+ }
+ return nullptr;
+ }
+
+ // check if this dialog *was* open and floating; if so recreate its window
+ if (auto state = DialogManager::singleton().find_dialog_state(dialog_type)) {
+ if (recreate_dialogs_from_state(_inkscape_window, state.get())) {
+ return nullptr;
+ }
+ }
+
+ // Create the dialog widget
+ DialogBase *dialog = dialog_factory(dialog_type).release(); // Evil, but necessitated by GTK.
+
+ if (!dialog) {
+ std::cerr << "DialogContainer::new_dialog(): couldn't find dialog for: " << dialog_type.raw() << std::endl;
+ return nullptr;
+ }
+
+ // manage the dialog instance
+ dialog = Gtk::manage(dialog);
+
+ // Create the notebook tab
+ gchar* image = nullptr;
+
+ Glib::ustring label;
+ Glib::ustring action_name = "win.dialog-open('" + dialog_type + "')";
+ auto app = InkscapeApplication::instance();
+ std::vector<Glib::ustring> accels = app->gtk_app()->get_accels_for_action(action_name);
+ if (accels.size() > 0) {
+ guint key = 0;
+ Gdk::ModifierType mods;
+ Gtk::AccelGroup::parse(accels[0], key, mods);
+ label = Gtk::AccelGroup::get_label(key, mods);
+ }
+
+ Gtk::Widget *tab =
+ create_notebook_tab(dialog->get_name(), image ? Glib::ustring(image) : INKSCAPE_ICON("inkscape-logo"), label);
+
+ // New temporary noteboook
+ DialogNotebook *notebook = Gtk::manage(new DialogNotebook(this));
+ notebook->add_page(*dialog, *tab, dialog->get_name());
+
+ return notebook->pop_tab_callback();
+}
+
+// toggle dialogs (visibility) is invoked on a top container embedded in Inkscape window
+void DialogContainer::toggle_dialogs()
+{
+ // check how many dialog panels are visible and how many are hidden
+ // we use this info to decide what it means to toggle visibility
+ int visible = 0;
+ int hidden = 0;
+ for (auto child : columns->get_children()) {
+ // only examine panels, skip drop zones and handles
+ if (auto panel = dynamic_cast<DialogMultipaned*>(child)) {
+ if (panel->is_visible()) {
+ ++visible;
+ }
+ else {
+ ++hidden;
+ }
+ }
+ }
+
+ // next examine floating dialogs
+ auto windows = DialogManager::singleton().get_all_floating_dialog_windows();
+ for (auto wnd : windows) {
+ if (wnd->is_visible()) {
+ ++visible;
+ }
+ else {
+ ++hidden;
+ }
+ }
+
+ bool show_dialogs = true;
+ // if some dialogs are hidden, toggle will first show them;
+ // another option could be to hide all if some dialogs are visible
+ if (hidden > 0) {
+ show_dialogs = true;
+ }
+ else {
+ // if everything's visible, hide them
+ show_dialogs = false;
+ }
+
+ // set visibility of floating dialogs
+ for (auto wnd : windows) {
+ DialogManager::singleton().set_floating_dialog_visibility(wnd, show_dialogs);
+ }
+
+ // set visibility of docked dialogs
+ columns->toggle_multipaned_children(show_dialogs);
+}
+
+// Update dialogs
+void DialogContainer::update_dialogs()
+{
+ for_each(dialogs.begin(), dialogs.end(), [&](auto dialog) { dialog.second->update(); });
+}
+
+void DialogContainer::set_inkscape_window(InkscapeWindow* inkscape_window)
+{
+ g_assert(inkscape_window != nullptr);
+ _inkscape_window = inkscape_window;
+ auto desktop = _inkscape_window->get_desktop();
+ for_each(dialogs.begin(), dialogs.end(), [&](auto dialog) { dialog.second->setDesktop(desktop); });
+}
+
+bool DialogContainer::has_dialog_of_type(DialogBase *dialog)
+{
+ return (dialogs.find(dialog->get_type()) != dialogs.end());
+}
+
+DialogBase *DialogContainer::get_dialog(const Glib::ustring& dialog_type)
+{
+ auto found = dialogs.find(dialog_type);
+ if (found != dialogs.end()) {
+ return found->second;
+ }
+ return nullptr;
+}
+
+// Add dialog to list.
+void DialogContainer::link_dialog(DialogBase *dialog)
+{
+ dialogs.insert(std::pair<Glib::ustring, DialogBase *>(dialog->get_type(), dialog));
+
+ DialogWindow *window = dynamic_cast<DialogWindow *>(get_toplevel());
+ if (window) {
+ window->update_dialogs();
+ }
+ else {
+ // dialog without DialogWindow has been docked; remove it's floating state
+ // so if user closes and reopens it, it shows up docked again, not floating
+ DialogManager::singleton().remove_dialog_floating_state(dialog->get_type());
+ }
+}
+
+// Remove dialog from list.
+void DialogContainer::unlink_dialog(DialogBase *dialog)
+{
+ if (!dialog) {
+ return;
+ }
+
+ auto found = dialogs.find(dialog->get_type());
+ if (found != dialogs.end()) {
+ dialogs.erase(found);
+ }
+
+ DialogWindow *window = dynamic_cast<DialogWindow *>(get_toplevel());
+ if (window) {
+ window->update_dialogs();
+ }
+}
+
+/**
+ * Load last open window's dialog configuration state.
+ *
+ * For the keyfile format, check `save_container_state()`.
+ */
+void DialogContainer::load_container_state(Glib::KeyFile *keyfile, bool include_floating)
+{
+ // Step 1: check if we want to load the state
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ // if it isn't dockable, all saved docked dialogs are made floating
+ bool is_dockable =
+ prefs->getInt("/options/dialogtype/value", PREFS_DIALOGS_BEHAVIOR_DOCKABLE) != PREFS_DIALOGS_BEHAVIOR_FLOATING;
+
+ // Step 2: get the number of windows
+ int windows_count = keyfile->get_integer("Windows", "Count");
+
+ // Step 3: for each window, load its state. Only the first window is not floating (the others are DialogWindow)
+ for (int window_idx = 0; window_idx < windows_count; ++window_idx) {
+ if (window_idx > 0 && !include_floating)
+ break;
+
+ Glib::ustring group_name = "Window" + std::to_string(window_idx);
+
+ // Step 3.0: read the window parameters
+ int column_count = 0;
+ bool floating = window_idx != 0;
+ window_position_t pos;
+ bool has_position = false;
+ try {
+ column_count = keyfile->get_integer(group_name, "ColumnCount");
+ floating = keyfile->get_boolean(group_name, "Floating");
+ if (keyfile->has_key(group_name, "Position") && keyfile->get_boolean(group_name, "Position")) {
+ pos.x = keyfile->get_integer(group_name, "x");
+ pos.y = keyfile->get_integer(group_name, "y");
+ pos.width = keyfile->get_integer(group_name, "width");
+ pos.height = keyfile->get_integer(group_name, "height");
+ has_position = true;
+ }
+ } catch (Glib::Error &error) {
+ std::cerr << "DialogContainer::load_container_state: " << error.what().raw() << std::endl;
+ }
+
+ // Step 3.1: get the window's container columns where we want to create the dialogs
+ DialogContainer *active_container = nullptr;
+ DialogMultipaned *active_columns = nullptr;
+ DialogWindow *dialog_window = nullptr;
+
+ if (is_dockable) {
+ if (floating) {
+ dialog_window = new DialogWindow(_inkscape_window, nullptr);
+ if (dialog_window) {
+ active_container = dialog_window->get_container();
+ active_columns = dialog_window->get_container()->get_columns();
+ }
+ } else {
+ active_container = this;
+ active_columns = columns;
+ }
+
+ if (!active_container || !active_columns) {
+ continue;
+ }
+ }
+
+ // Step 3.2: for each column, load its state
+ for (int column_idx = 0; column_idx < column_count; ++column_idx) {
+ Glib::ustring column_group_name = group_name + "Column" + std::to_string(column_idx);
+
+ // Step 3.2.0: read the column parameters
+ int notebook_count = 0;
+ bool before_canvas = false;
+ try {
+ notebook_count = keyfile->get_integer(column_group_name, "NotebookCount");
+ before_canvas = keyfile->get_boolean(column_group_name, "BeforeCanvas");
+ } catch (Glib::Error &error) {
+ std::cerr << "DialogContainer::load_container_state: " << error.what().raw() << std::endl;
+ }
+
+ // Step 3.2.1: create the column
+ DialogMultipaned *column = nullptr;
+ if (is_dockable) {
+ column = active_container->create_column();
+ if (!column) {
+ continue;
+ }
+
+ if (keyfile->has_key(column_group_name, "ColumnWidth")) {
+ auto width = keyfile->get_integer(column_group_name, "ColumnWidth");
+ column->set_restored_width(width);
+ }
+
+ before_canvas ? active_columns->prepend(column) : active_columns->append(column);
+ }
+
+ // Step 3.2.2: for each noteboook, load its dialogs
+ for (int notebook_idx = 0; notebook_idx < notebook_count; ++notebook_idx) {
+ Glib::ustring key = "Notebook" + std::to_string(notebook_idx) + "Dialogs";
+
+ // Step 3.2.2.0 read the list of dialogs in the current notebook
+ std::vector<Glib::ustring> dialogs;
+ try {
+ dialogs = keyfile->get_string_list(column_group_name, key);
+ } catch (Glib::Error &error) {
+ std::cerr << "DialogContainer::load_container_state: " << error.what().raw() << std::endl;
+ }
+
+ if (!dialogs.size()) {
+ continue;
+ }
+
+ DialogNotebook *notebook = nullptr;
+ if (is_dockable) {
+ notebook = Gtk::manage(new DialogNotebook(active_container));
+ column->append(notebook);
+ }
+
+ auto const &dialog_data = get_dialog_data();
+ // Step 3.2.2.1 create each dialog in the current notebook
+ for (auto type : dialogs) {
+
+ if (dialog_data.find(type) != dialog_data.end()) {
+ if (is_dockable) {
+ active_container->new_dialog(type, notebook);
+ } else {
+ dialog_window = create_new_floating_dialog(type, false);
+ }
+ } else {
+ std::cerr << "load_container_state: invalid dialog type: " << type.raw() << std::endl;
+ }
+ }
+
+ if (notebook) {
+ Glib::ustring row = "Notebook" + std::to_string(notebook_idx) + "Height";
+ if (keyfile->has_key(column_group_name, row)) {
+ auto height = keyfile->get_integer(column_group_name, row);
+ notebook->set_requested_height(height);
+ }
+ Glib::ustring tab = "Notebook" + std::to_string(notebook_idx) + "ActiveTab";
+ if (keyfile->has_key(column_group_name, tab)) {
+ if (auto nb = notebook->get_notebook()) {
+ auto page = keyfile->get_integer(column_group_name, tab);
+ nb->set_current_page(page);
+ }
+ }
+ }
+ }
+ }
+
+ if (dialog_window) {
+ if (has_position) {
+ dm_restore_window_position(*dialog_window, pos);
+ }
+ else {
+ dialog_window->update_window_size_to_fit_children();
+ }
+ dialog_window->show_all();
+ // Set the style and icon theme of the new menu based on the desktop
+ }
+ }
+ INKSCAPE.themecontext->getChangeThemeSignal().emit();
+ INKSCAPE.themecontext->add_gtk_css(true);
+}
+
+void save_wnd_position(Glib::KeyFile *keyfile, const Glib::ustring &group_name, const window_position_t *position)
+{
+ keyfile->set_boolean(group_name, "Position", position != nullptr);
+ if (position) { // floating window position?
+ keyfile->set_integer(group_name, "x", position->x);
+ keyfile->set_integer(group_name, "y", position->y);
+ keyfile->set_integer(group_name, "width", position->width);
+ keyfile->set_integer(group_name, "height", position->height);
+ }
+}
+
+// get *this* container's state only; store window 'position' in the state if given
+std::shared_ptr<Glib::KeyFile> DialogContainer::get_container_state(const window_position_t *position) const
+{
+ std::shared_ptr<Glib::KeyFile> keyfile = std::make_shared<Glib::KeyFile>();
+
+ DialogMultipaned *window = columns;
+ const int window_idx = 0;
+
+ // Step 2: save the number of windows
+ keyfile->set_integer("Windows", "Count", 1);
+
+ // Step 3.0: get all the multipanes of the window
+ std::vector<DialogMultipaned *> multipanes;
+
+ for (auto const &column : window->get_children()) {
+ if (auto paned = dynamic_cast<DialogMultipaned *>(column)) {
+ multipanes.push_back(paned);
+ }
+ }
+
+ // Step 3.1: for each non-empty column, save its data.
+ int column_count = 0; // non-empty columns count
+ for (size_t column_idx = 0; column_idx < multipanes.size(); ++column_idx) {
+ Glib::ustring group_name = "Window" + std::to_string(window_idx) + "Column" + std::to_string(column_idx);
+ int notebook_count = 0; // non-empty notebooks count
+
+ // Step 3.1.0: for each notebook, get its dialogs
+ for (auto const &columns_widget : multipanes[column_idx]->get_children()) {
+ if (auto dialog_notebook = dynamic_cast<DialogNotebook *>(columns_widget)) {
+ std::vector<Glib::ustring> dialogs;
+
+ for (auto const &widget : dialog_notebook->get_notebook()->get_children()) {
+ if (DialogBase *dialog = dynamic_cast<DialogBase *>(widget)) {
+ dialogs.push_back(dialog->get_type());
+ }
+ }
+
+ // save the dialogs type
+ Glib::ustring key = "Notebook" + std::to_string(notebook_count) + "Dialogs";
+ keyfile->set_string_list(group_name, key, dialogs);
+
+ // increase the notebook count
+ notebook_count++;
+ }
+ }
+
+ // Step 3.1.1: increase the column count
+ if (notebook_count != 0) {
+ column_count++;
+ }
+
+ // Step 3.1.2: Save the column's data
+ keyfile->set_integer(group_name, "NotebookCount", notebook_count);
+ }
+
+ // Step 3.2: save the window group
+ Glib::ustring group_name = "Window" + std::to_string(window_idx);
+ keyfile->set_integer(group_name, "ColumnCount", column_count);
+ save_wnd_position(keyfile.get(), group_name, position);
+
+ return keyfile;
+}
+
+/**
+ * Save container state. The configuration of open dialogs and the relative positions of the notebooks are saved.
+ *
+ * The structure of such a KeyFile is:
+ *
+ * There is a "Windows" group that records the number of the windows:
+ * [Windows]
+ * Count=1
+ *
+ * A "WindowX" group saves the number of columns the window's container has and whether the window is floating:
+ *
+ * [Window0]
+ * ColumnCount=1
+ * Floating=false
+ *
+ * For each column, we have a "WindowWColumnX" group, where X is the index of the column. "BeforeCanvas" checks
+ * if the column is before the canvas or not. "NotebookCount" records how many notebooks are in each column and
+ * "NotebookXDialogs" records a list of the types for the dialogs in notebook X.
+ *
+ * [Window0Column0]
+ * Notebook0Dialogs=Text;
+ * NotebookCount=2
+ * BeforeCanvas=false
+ *
+ */
+std::unique_ptr<Glib::KeyFile> DialogContainer::save_container_state()
+{
+ std::unique_ptr<Glib::KeyFile> keyfile = std::make_unique<Glib::KeyFile>();
+ auto app = InkscapeApplication::instance();
+
+ // Step 1: get all the container columns (in order, from the current container and all DialogWindow containers)
+ std::vector<DialogMultipaned *> windows(1, columns);
+ std::vector<DialogWindow *> dialog_windows(1, nullptr);
+
+ for (auto const &window : app->gtk_app()->get_windows()) {
+ DialogWindow *dialog_window = dynamic_cast<DialogWindow *>(window);
+ if (dialog_window) {
+ windows.push_back(dialog_window->get_container()->get_columns());
+ dialog_windows.push_back(dialog_window);
+ }
+ }
+
+ // Step 2: save the number of windows
+ keyfile->set_integer("Windows", "Count", windows.size());
+
+ // Step 3: for each window, save its data. Only the first window is not floating (the others are DialogWindow)
+ for (int window_idx = 0; window_idx < (int)windows.size(); ++window_idx) {
+ // Step 3.0: get all the multipanes of the window
+ std::vector<DialogMultipaned *> multipanes;
+
+ // used to check if the column is before or after canvas
+ std::vector<DialogMultipaned *>::iterator multipanes_it = multipanes.begin();
+ bool canvas_seen = window_idx != 0; // no floating windows (window_idx > 0) have a canvas
+ int before_canvas_columns_count = 0;
+
+ for (auto const &column : windows[window_idx]->get_children()) {
+ if (!canvas_seen) {
+ UI::Widget::CanvasGrid *canvas = dynamic_cast<UI::Widget::CanvasGrid *>(column);
+ if (canvas) {
+ canvas_seen = true;
+ } else {
+ DialogMultipaned *paned = dynamic_cast<DialogMultipaned *>(column);
+ if (paned) {
+ multipanes_it = multipanes.insert(multipanes_it, paned);
+ before_canvas_columns_count++;
+ }
+ }
+ } else {
+ DialogMultipaned *paned = dynamic_cast<DialogMultipaned *>(column);
+ if (paned) {
+ multipanes.push_back(paned);
+ }
+ }
+ }
+
+ // Step 3.1: for each non-empty column, save its data.
+ int column_count = 0; // non-empty columns count
+ for (int column_idx = 0; column_idx < (int)multipanes.size(); ++column_idx) {
+ Glib::ustring group_name = "Window" + std::to_string(window_idx) + "Column" + std::to_string(column_idx);
+ int notebook_count = 0; // non-empty notebooks count
+ int width = multipanes[column_idx]->get_allocated_width();
+
+ // Step 3.1.0: for each notebook, get its dialogs' types
+ for (auto const &columns_widget : multipanes[column_idx]->get_children()) {
+ DialogNotebook *dialog_notebook = dynamic_cast<DialogNotebook *>(columns_widget);
+
+ if (dialog_notebook) {
+ std::vector<Glib::ustring> dialogs;
+
+ for (auto const &widget : dialog_notebook->get_notebook()->get_children()) {
+ DialogBase *dialog = dynamic_cast<DialogBase *>(widget);
+ if (dialog) {
+ dialogs.push_back(dialog->get_type());
+ }
+ }
+
+ // save the dialogs type
+ Glib::ustring key = "Notebook" + std::to_string(notebook_count) + "Dialogs";
+ keyfile->set_string_list(group_name, key, dialogs);
+ // save height; useful when there are multiple "rows" of docked dialogs
+ Glib::ustring row = "Notebook" + std::to_string(notebook_count) + "Height";
+ keyfile->set_integer(group_name, row, dialog_notebook->get_allocated_height());
+ if (auto notebook = dialog_notebook->get_notebook()) {
+ Glib::ustring row = "Notebook" + std::to_string(notebook_count) + "ActiveTab";
+ keyfile->set_integer(group_name, row, notebook->get_current_page());
+ }
+
+ // increase the notebook count
+ notebook_count++;
+ }
+ }
+
+ // Step 3.1.1: increase the column count
+ if (notebook_count != 0) {
+ column_count++;
+ }
+
+ keyfile->set_integer(group_name, "ColumnWidth", width);
+
+ // Step 3.1.2: Save the column's data
+ keyfile->set_integer(group_name, "NotebookCount", notebook_count);
+ keyfile->set_boolean(group_name, "BeforeCanvas", (column_idx < before_canvas_columns_count));
+ }
+
+ // Step 3.2: save the window group
+ Glib::ustring group_name = "Window" + std::to_string(window_idx);
+ keyfile->set_integer(group_name, "ColumnCount", column_count);
+ keyfile->set_boolean(group_name, "Floating", window_idx != 0);
+ if (window_idx != 0) { // floating?
+ if (auto wnd = dynamic_cast<DialogWindow *>(dialog_windows.at(window_idx))) {
+ // store window position
+ auto pos = dm_get_window_position(*wnd);
+ save_wnd_position(keyfile.get(), group_name, pos ? &*pos : nullptr);
+ }
+ }
+ }
+
+ return keyfile;
+}
+
+// Signals -----------------------------------------------------
+
+/**
+ * No zombie windows. TODO: Need to work on this as it still leaves Gtk::Window! (?)
+ */
+void DialogContainer::on_unrealize() {
+ // Disconnect all signals
+ for_each(connections.begin(), connections.end(), [&](auto c) { c.disconnect(); });
+
+ delete columns;
+ columns = nullptr;
+
+ parent_type::on_unrealize();
+}
+
+// Create a new notebook and move page.
+DialogNotebook *DialogContainer::prepare_drop(const Glib::RefPtr<Gdk::DragContext> context)
+{
+ Gtk::Widget *source = Gtk::Widget::drag_get_source_widget(context);
+
+ // Find source notebook and page
+ Gtk::Notebook *old_notebook = dynamic_cast<Gtk::Notebook *>(source);
+ if (!old_notebook) {
+ std::cerr << "DialogContainer::prepare_drop: notebook not found!" << std::endl;
+ return nullptr;
+ }
+
+ // Find page
+ Gtk::Widget *page = old_notebook->get_nth_page(old_notebook->get_current_page());
+ if (!page) {
+ std::cerr << "DialogContainer::prepare_drop: page not found!" << std::endl;
+ return nullptr;
+ }
+
+ // Create new notebook and move page.
+ DialogNotebook *new_notebook = Gtk::manage(new DialogNotebook(this));
+ new_notebook->move_page(*page);
+
+ // move_page() takes care of updating dialog lists.
+ INKSCAPE.themecontext->getChangeThemeSignal().emit();
+ INKSCAPE.themecontext->add_gtk_css(true);
+ return new_notebook;
+}
+
+// Notebook page dropped on prepend target. Call function to create new notebook and then insert.
+void DialogContainer::prepend_drop(const Glib::RefPtr<Gdk::DragContext> context, DialogMultipaned *multipane)
+{
+ DialogNotebook *new_notebook = prepare_drop(context); // Creates notebook, moves page.
+ if (!new_notebook) {
+ std::cerr << "DialogContainer::prepend_drop: no new notebook!" << std::endl;
+ return;
+ }
+
+ if (multipane->get_orientation() == Gtk::ORIENTATION_HORIZONTAL) {
+ // Columns
+ // Create column
+ DialogMultipaned *column = create_column();
+ column->prepend(new_notebook);
+ columns->prepend(column);
+ } else {
+ // Column
+ multipane->prepend(new_notebook);
+ }
+
+ update_dialogs(); // Always update dialogs on Notebook change
+}
+
+// Notebook page dropped on append target. Call function to create new notebook and then insert.
+void DialogContainer::append_drop(const Glib::RefPtr<Gdk::DragContext> context, DialogMultipaned *multipane)
+{
+ DialogNotebook *new_notebook = prepare_drop(context); // Creates notebook, moves page.
+ if (!new_notebook) {
+ std::cerr << "DialogContainer::append_drop: no new notebook!" << std::endl;
+ return;
+ }
+
+ if (multipane->get_orientation() == Gtk::ORIENTATION_HORIZONTAL) {
+ // Columns
+ // Create column
+ DialogMultipaned *column = create_column();
+ column->append(new_notebook);
+ columns->append(column);
+ } else {
+ // Column
+ multipane->append(new_notebook);
+ }
+
+ update_dialogs(); // Always update dialogs on Notebook change
+}
+
+/**
+ * If a DialogMultipaned column is empty and it can be removed, remove it
+ */
+void DialogContainer::column_empty(DialogMultipaned *column)
+{
+ DialogMultipaned *parent = dynamic_cast<DialogMultipaned *>(column->get_parent());
+ if (parent) {
+ parent->remove(*column);
+ }
+
+ DialogWindow *window = dynamic_cast<DialogWindow *>(get_toplevel());
+ if (window && parent) {
+ auto children = parent->get_children();
+ // Close the DialogWindow if you're in an empty one
+ if (children.size() == 3 && parent->has_empty_widget()) {
+ window->close();
+ }
+ }
+}
+
+} // 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-container.h b/src/ui/dialog/dialog-container.h
new file mode 100644
index 0000000..cb6e5c6
--- /dev/null
+++ b/src/ui/dialog/dialog-container.h
@@ -0,0 +1,130 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifndef INKSCAPE_UI_DIALOG_CONTAINER_H
+#define INKSCAPE_UI_DIALOG_CONTAINER_H
+
+/** @file
+ * @brief A widget that manages DialogNotebook's and other widgets inside a horizontal DialogMultipaned.
+ *
+ * Authors: see git history
+ * Tavmjong Bah
+ *
+ * Copyright (c) 2018 Tavmjong Bah, Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gdkmm/dragcontext.h>
+#include <glibmm/refptr.h>
+#include <glibmm/ustring.h>
+#include <glibmm/keyfile.h>
+#include <gtkmm/accelkey.h>
+#include <gtkmm/box.h>
+#include <gtkmm/targetentry.h>
+#include <iostream>
+#include <map>
+#include <set>
+#include <memory>
+#include "dialog-manager.h"
+#include "desktop.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class DialogBase;
+class DialogNotebook;
+class DialogMultipaned;
+class DialogWindow;
+
+/**
+ * A widget that manages DialogNotebook's and other widgets inside a
+ * horizontal DialogMultipaned containing vertical DialogMultipaned's or other widgets.
+ */
+class DialogContainer : public Gtk::Box
+{
+ using parent_type = Gtk::Box;
+
+public:
+ DialogContainer(InkscapeWindow* inkscape_window);
+ ~DialogContainer() override;
+
+ // Columns-related functions
+ DialogMultipaned *get_columns() { return columns; }
+ DialogMultipaned *create_column();
+
+ // Dialog-related functions
+ void new_dialog(const Glib::ustring& dialog_type);
+
+ DialogWindow* new_floating_dialog(const Glib::ustring& dialog_type);
+ bool has_dialog_of_type(DialogBase *dialog);
+ DialogBase *get_dialog(const Glib::ustring& dialog_type);
+ void link_dialog(DialogBase *dialog);
+ void unlink_dialog(DialogBase *dialog);
+ const std::multimap<Glib::ustring, DialogBase *> *get_dialogs() { return &dialogs; };
+ void toggle_dialogs();
+ void update_dialogs(); // Update all linked dialogs
+ void set_inkscape_window(InkscapeWindow *inkscape_window);
+ InkscapeWindow* get_inkscape_window() { return _inkscape_window; }
+
+ // State saving functionality
+ std::unique_ptr<Glib::KeyFile> save_container_state();
+ void load_container_state(Glib::KeyFile* keyfile, bool include_floating);
+
+ void restore_window_position(DialogWindow* window);
+ void store_window_position(DialogWindow* window);
+
+ // get this container's state; provide window position for container embedded in DialogWindow
+ std::shared_ptr<Glib::KeyFile> get_container_state(const window_position_t* position) const;
+ void load_container_state(Glib::KeyFile& state, const std::string& window_id);
+
+private:
+ InkscapeWindow *_inkscape_window = nullptr; // Every container is attached to an InkscapeWindow.
+ DialogMultipaned *columns = nullptr; // The main widget inside which other children are kept.
+ std::vector<Gtk::TargetEntry> target_entries; // What kind of object can be dropped.
+
+ /**
+ * Due to the way Gtk handles dragging between notebooks, one can
+ * either allow multiple instances of the same dialog in a notebook
+ * or restrict dialogs to docks tied to a particular document
+ * window. (More explicitly, use one group name for all notebooks or
+ * use a unique group name for each document window with related
+ * floating docks.) For the moment we choose the former which
+ * requires a multimap here as we use the dialog type as a key.
+ */
+ std::multimap<Glib::ustring, DialogBase *>dialogs;
+
+ void new_dialog(const Glib::ustring& dialog_type, DialogNotebook* notebook);
+ std::unique_ptr<DialogBase> dialog_factory(Glib::ustring const &dialog_type);
+ Gtk::Widget *create_notebook_tab(Glib::ustring label, Glib::ustring image, const Glib::ustring shortcut);
+ DialogWindow* create_new_floating_dialog(const Glib::ustring& dialog_type, bool blink);
+
+ // Signal connections
+ std::vector<sigc::connection> connections;
+
+ // Handlers
+ void on_unrealize() override;
+ DialogNotebook *prepare_drop(const Glib::RefPtr<Gdk::DragContext> context);
+ void prepend_drop(const Glib::RefPtr<Gdk::DragContext> context, DialogMultipaned *column);
+ void append_drop(const Glib::RefPtr<Gdk::DragContext> context, DialogMultipaned *column);
+ void column_empty(DialogMultipaned *column);
+ DialogBase* find_existing_dialog(const Glib::ustring& dialog_type);
+ static bool recreate_dialogs_from_state(InkscapeWindow* inkscape_window, const Glib::KeyFile* keyfile);
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_CONTAINER_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-data.cpp b/src/ui/dialog/dialog-data.cpp
new file mode 100644
index 0000000..b8acdb1
--- /dev/null
+++ b/src/ui/dialog/dialog-data.cpp
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <map>
+
+#include <glibmm/i18n.h>
+#include <glibmm/ustring.h>
+
+#include "config.h" // Needed for WITH_GSPELL
+
+#include "ui/dialog/dialog-data.h"
+#include "ui/icon-names.h" // INKSCAPE_ICON macro
+
+/*
+ * In an ideal world, this information would be in .ui files for each
+ * dialog (the .ui file would describe a dialog wrapped by a notebook
+ * tab). At the moment we create each dialog notebook tab on the fly
+ * so we need a place to keep this information.
+ */
+
+std::map<std::string, DialogData> const &get_dialog_data()
+{
+ static std::map<std::string, DialogData> dialog_data;
+
+ // Note the "AttrDialog" is now part of the "XMLDialog" and the "Style" dialog is part of the
+ // "Selectors" dialog. Also note that the "AttrDialog" does not correspond to SP_VERB_DIALOG_ATTR!!!
+ // (That would be the "ObjectAttributes" dialog.)
+
+ if (dialog_data.empty()) {
+ dialog_data = {
+ // clang-format off
+ {"AlignDistribute", {_("_Align and Distribute"), INKSCAPE_ICON("dialog-align-and-distribute"), DialogData::Basic, ScrollProvider::NOPROVIDE }},
+ {"CloneTiler", {_("Create Tiled Clones"), INKSCAPE_ICON("dialog-tile-clones"), DialogData::Basic, ScrollProvider::NOPROVIDE }},
+ {"DocumentProperties", {_("_Document Properties"), INKSCAPE_ICON("document-properties"), DialogData::Settings, ScrollProvider::NOPROVIDE }},
+ {"DocumentResources", {_("_Document Resources"), INKSCAPE_ICON("document-resources"), DialogData::Advanced, ScrollProvider::NOPROVIDE }},
+ {"Export", {_("_Export"), INKSCAPE_ICON("document-export"), DialogData::Basic, ScrollProvider::PROVIDE }},
+ {"FillStroke", {_("_Fill and Stroke"), INKSCAPE_ICON("dialog-fill-and-stroke"), DialogData::Basic, ScrollProvider::NOPROVIDE }},
+ {"FilterEffects", {_("Filter _Editor"), INKSCAPE_ICON("dialog-filters"), DialogData::Advanced, ScrollProvider::NOPROVIDE }},
+ {"Find", {_("_Find/Replace"), INKSCAPE_ICON("edit-find"), DialogData::Basic, ScrollProvider::NOPROVIDE }},
+ {"FontCollections", {_("_Font Collections"), INKSCAPE_ICON("font_collections"), DialogData::Advanced, ScrollProvider::NOPROVIDE }},
+ {"Glyphs", {_("_Unicode Characters"), INKSCAPE_ICON("accessories-character-map"), DialogData::Basic, ScrollProvider::NOPROVIDE }},
+ {"IconPreview", {_("Icon Preview"), INKSCAPE_ICON("dialog-icon-preview"), DialogData::Basic, ScrollProvider::NOPROVIDE }},
+ {"Input", {_("_Input Devices"), INKSCAPE_ICON("dialog-input-devices"), DialogData::Settings, ScrollProvider::NOPROVIDE }},
+ {"LivePathEffect", {_("Path E_ffects"), INKSCAPE_ICON("dialog-path-effects"), DialogData::Advanced, ScrollProvider::NOPROVIDE }},
+ {"Memory", {_("About _Memory"), INKSCAPE_ICON("dialog-memory"), DialogData::Diagnostics, ScrollProvider::PROVIDE }},
+ {"Messages", {_("_Messages"), INKSCAPE_ICON("dialog-messages"), DialogData::Diagnostics, ScrollProvider::NOPROVIDE }},
+ {"ObjectAttributes", {_("_Object Attributes"), INKSCAPE_ICON("dialog-object-properties"), DialogData::Settings, ScrollProvider::NOPROVIDE }},
+ {"ObjectProperties", {_("_Object Properties"), INKSCAPE_ICON("dialog-object-properties"), DialogData::Settings, ScrollProvider::NOPROVIDE }},
+ {"Objects", {_("Layers and Object_s"), INKSCAPE_ICON("dialog-objects"), DialogData::Basic, ScrollProvider::PROVIDE }},
+ {"PaintServers", {_("_Paint Servers"), INKSCAPE_ICON("dialog-paint-server"), DialogData::Advanced, ScrollProvider::PROVIDE }},
+ {"Preferences", {_("P_references"), INKSCAPE_ICON("preferences-system"), DialogData::Settings, ScrollProvider::PROVIDE }},
+ {"Selectors", {_("_Selectors and CSS"), INKSCAPE_ICON("dialog-selectors"), DialogData::Advanced, ScrollProvider::PROVIDE }},
+ {"SVGFonts", {_("SVG Font Editor"), INKSCAPE_ICON("dialog-svg-font"), DialogData::Advanced, ScrollProvider::NOPROVIDE }},
+ {"Swatches", {_("S_watches"), INKSCAPE_ICON("swatches"), DialogData::Basic, ScrollProvider::PROVIDE }},
+ {"Symbols", {_("S_ymbols"), INKSCAPE_ICON("symbols"), DialogData::Basic, ScrollProvider::PROVIDE }},
+ {"Text", {_("_Text and Font"), INKSCAPE_ICON("dialog-text-and-font"), DialogData::Basic, ScrollProvider::NOPROVIDE }},
+ {"Trace", {_("_Trace Bitmap"), INKSCAPE_ICON("bitmap-trace"), DialogData::Basic, ScrollProvider::NOPROVIDE }},
+ {"Transform", {_("Transfor_m"), INKSCAPE_ICON("dialog-transform"), DialogData::Basic, ScrollProvider::NOPROVIDE }},
+ {"UndoHistory", {_("Undo _History"), INKSCAPE_ICON("edit-undo-history"), DialogData::Basic, ScrollProvider::NOPROVIDE }},
+ {"XMLEditor", {_("_XML Editor"), INKSCAPE_ICON("dialog-xml-editor"), DialogData::Advanced, ScrollProvider::NOPROVIDE }},
+#if WITH_GSPELL
+ {"Spellcheck", {_("Check Spellin_g"), INKSCAPE_ICON("tools-check-spelling"), DialogData::Basic, ScrollProvider::NOPROVIDE }},
+#endif
+#if DEBUG
+ {"Prototype", {_("Prototype"), INKSCAPE_ICON("document-properties"), DialogData::Other, ScrollProvider::NOPROVIDE }},
+#endif
+ // clang-format on
+ };
+ }
+ return dialog_data;
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ 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-data.h b/src/ui/dialog/dialog-data.h
new file mode 100644
index 0000000..1816600
--- /dev/null
+++ b/src/ui/dialog/dialog-data.h
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/** @file
+ * @brief Basic dialog info.
+ *
+ * Authors: see git history
+ * Tavmjong Bah
+ *
+ * Copyright (c) 2021 Tavmjong Bah, Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <map>
+
+#include <glibmm/i18n.h>
+#include <glibmm/ustring.h>
+
+enum class ScrollProvider {
+ PROVIDE = 0,
+ NOPROVIDE
+};
+
+class DialogData {
+public:
+ Glib::ustring label;
+ Glib::ustring icon_name;
+ enum Category { Basic = 0, Advanced, Settings, Diagnostics, Other, _num_categories };
+ Category category;
+ ScrollProvider provide_scroll;
+};
+
+// dialog categories (used to group them in a dialog submenu)
+char const *const dialog_categories[DialogData::_num_categories] = {
+ N_("Basic"),
+ N_("Advanced"),
+ N_("Settings"),
+ N_("Diagnostic"),
+ N_("Other")
+};
+
+/** Get the data about all existing dialogs. */
+std::map<std::string, DialogData> const &get_dialog_data();
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ 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..f12c9be
--- /dev/null
+++ b/src/ui/dialog/dialog-manager.cpp
@@ -0,0 +1,323 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "dialog-manager.h"
+
+#include <gdkmm/monitor.h>
+#include <limits>
+#ifdef G_OS_WIN32
+#include <filesystem>
+namespace filesystem = std::filesystem;
+#else
+// Waiting for compiler on MacOS to catch up to C++x17
+#include <boost/filesystem.hpp>
+namespace filesystem = boost::filesystem;
+#endif
+
+#include "io/resource.h"
+#include "inkscape-application.h"
+#include "dialog-base.h"
+#include "dialog-container.h"
+#include "dialog-window.h"
+#include "enums.h"
+#include "preferences.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+std::optional<window_position_t> dm_get_window_position(Gtk::Window &window)
+{
+ std::optional<window_position_t> position = std::nullopt;
+
+ const int max = std::numeric_limits<int>::max();
+ int x = max;
+ int y = max;
+ int width = 0;
+ int height = 0;
+ // gravity NW to include window decorations
+ window.property_gravity() = Gdk::GRAVITY_NORTH_WEST;
+ window.get_position(x, y);
+ window.get_size(width, height);
+
+ if (x != max && y != max && width > 0 && height > 0) {
+ position = window_position_t{x, y, width, height};
+ }
+
+ return position;
+}
+
+void dm_restore_window_position(Gtk::Window &window, const window_position_t &position)
+{
+ // note: Gtk window methods are recommended over low-level Gdk ones to resize and position window
+ window.property_gravity() = Gdk::GRAVITY_NORTH_WEST;
+ window.set_default_size(position.width, position.height);
+ // move & resize positions window on the screen making sure it is not clipped
+ // (meaning it is visible; this works with two monitors too)
+ window.move(position.x, position.y);
+ window.resize(position.width, position.height);
+}
+
+DialogManager &DialogManager::singleton()
+{
+ static DialogManager dm;
+ return dm;
+}
+
+// store complete dialog window state (including its container state)
+void DialogManager::store_state(DialogWindow &wnd)
+{
+ // get window's size and position
+ if (auto pos = dm_get_window_position(wnd)) {
+ if (auto container = wnd.get_container()) {
+ // get container's state
+ auto state = container->get_container_state(&*pos);
+
+ // find dialogs hosted in this window
+ for (auto dlg : *container->get_dialogs()) {
+ _floating_dialogs[dlg.first] = state;
+ }
+ }
+ }
+}
+
+//
+bool DialogManager::should_open_floating(const Glib::ustring& dialog_type)
+{
+ return _floating_dialogs.count(dialog_type) > 0;
+}
+
+void DialogManager::set_floating_dialog_visibility(DialogWindow* wnd, bool show) {
+ if (!wnd) return;
+
+ if (show) {
+ if (wnd->is_visible()) return;
+
+ // wnd->present(); - not sure which one is better, show or present...
+ wnd->show();
+ _hidden_dlg_windows.erase(wnd);
+ // re-add it to application; hiding removed it
+ if (auto app = InkscapeApplication::instance()) {
+ app->gtk_app()->add_window(*wnd);
+ }
+ }
+ else {
+ if (!wnd->is_visible()) return;
+
+ _hidden_dlg_windows.insert(wnd);
+ wnd->hide();
+ }
+}
+
+std::vector<DialogWindow*> DialogManager::get_all_floating_dialog_windows() {
+ std::vector<Gtk::Window*> windows = InkscapeApplication::instance()->gtk_app()->get_windows();
+
+ std::vector<DialogWindow*> result(_hidden_dlg_windows.begin(), _hidden_dlg_windows.end());
+ for (auto wnd : windows) {
+ if (auto dlg_wnd = dynamic_cast<DialogWindow*>(wnd)) {
+ result.push_back(dlg_wnd);
+ }
+ }
+
+ return result;
+}
+
+DialogWindow* DialogManager::find_floating_dialog_window(const Glib::ustring& dialog_type) {
+ auto windows = get_all_floating_dialog_windows();
+
+ for (auto dlg_wnd : windows) {
+ if (auto container = dlg_wnd->get_container()) {
+ if (container->get_dialog(dialog_type)) {
+ return dlg_wnd;
+ }
+ }
+ }
+
+ return nullptr;
+}
+
+DialogBase *DialogManager::find_floating_dialog(const Glib::ustring& dialog_type)
+{
+ auto windows = get_all_floating_dialog_windows();
+
+ for (auto dlg_wnd : windows) {
+ if (auto container = dlg_wnd->get_container()) {
+ if (auto dlg = container->get_dialog(dialog_type)) {
+ return dlg;
+ }
+ }
+ }
+
+ return nullptr;
+}
+
+std::shared_ptr<Glib::KeyFile> DialogManager::find_dialog_state(const Glib::ustring& dialog_type)
+{
+ auto it = _floating_dialogs.find(dialog_type);
+ if (it != _floating_dialogs.end()) {
+ return it->second;
+ }
+ return nullptr;
+}
+
+const char dialogs_state[] = "dialogs-state-ex.ini";
+const char save_dialog_position[] = "/options/savedialogposition/value";
+const char transient_group[] = "transient";
+
+// list of dialogs sharing the same state
+std::vector<Glib::ustring> DialogManager::count_dialogs(const Glib::KeyFile *state) const
+{
+ std::vector<Glib::ustring> dialogs;
+ if (!state) return dialogs;
+
+ for (auto dlg : _floating_dialogs) {
+ if (dlg.second.get() == state) {
+ dialogs.emplace_back(dlg.first);
+ }
+ }
+ return dialogs;
+}
+
+void DialogManager::save_dialogs_state(DialogContainer *docking_container)
+{
+ if (!docking_container) return;
+
+ // check if we want to save the state
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int save_state = prefs->getInt(save_dialog_position, PREFS_DIALOGS_STATE_SAVE);
+ if (save_state == PREFS_DIALOGS_STATE_NONE) return;
+
+ // save state of docked dialogs and currently open floating ones
+ auto keyfile = docking_container->save_container_state();
+
+ // save transient state of floating dialogs that user might have opened interacting with the app
+ int idx = 1;
+ for (auto dlg : _floating_dialogs) {
+ auto state = dlg.second.get();
+ auto&& type = dlg.first;
+ auto index = std::to_string(idx++);
+ // state may be empty; all that means it that dialog hasn't been opened yet,
+ // but when it is, then it should be open in a floating state
+ keyfile->set_string(transient_group, "state" + index, state ? state->to_data() : "");
+ auto dialogs = count_dialogs(state);
+ if (!state) {
+ dialogs.emplace_back(type);
+ }
+ keyfile->set_string_list(transient_group, "dialogs" + index, dialogs);
+ }
+ keyfile->set_integer(transient_group, "count", _floating_dialogs.size());
+
+ std::string filename = Glib::build_filename(Inkscape::IO::Resource::profile_path(), dialogs_state);
+ try {
+ keyfile->save_to_file(filename);
+ } catch (Glib::FileError &error) {
+ std::cerr << G_STRFUNC << ": " << error.what().raw() << std::endl;
+ }
+}
+
+// load transient dialog state - it includes state of floating dialogs that may or may not be open
+void DialogManager::load_transient_state(Glib::KeyFile *file)
+{
+ int count = file->get_integer(transient_group, "count");
+ for (int i = 0; i < count; ++i) {
+ auto index = std::to_string(i + 1);
+ auto dialogs = file->get_string_list(transient_group, "dialogs" + index);
+ auto state = file->get_string(transient_group, "state" + index);
+
+ auto keyfile = std::make_shared<Glib::KeyFile>();
+ if (!state.empty()) {
+ keyfile->load_from_data(state);
+ }
+ for (auto type : dialogs) {
+ _floating_dialogs[type] = keyfile;
+ }
+ }
+}
+
+bool file_exists(const std::string& filepath) {
+#ifdef G_OS_WIN32
+ bool exists = filesystem::exists(filesystem::u8path(filepath));
+#else
+ bool exists = filesystem::exists(filesystem::path(filepath));
+#endif
+ return exists;
+}
+
+// restore state of dialogs; populate docking container and open visible floating dialogs
+void DialogManager::restore_dialogs_state(DialogContainer *docking_container, bool include_floating)
+{
+ if (!docking_container) return;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int save_state = prefs->getInt(save_dialog_position, PREFS_DIALOGS_STATE_SAVE);
+ if (save_state == PREFS_DIALOGS_STATE_NONE) return;
+
+ try {
+ auto keyfile = std::make_unique<Glib::KeyFile>();
+ std::string filename = Glib::build_filename(Inkscape::IO::Resource::profile_path(), dialogs_state);
+
+ bool exists = file_exists(filename);
+
+ if (exists && keyfile->load_from_file(filename)) {
+ // restore visible dialogs first; that state is up-to-date
+ docking_container->load_container_state(keyfile.get(), include_floating);
+
+ // then load transient data too; it may be older than above
+ if (include_floating) {
+ try {
+ load_transient_state(keyfile.get());
+ } catch (Glib::Error &error) {
+ std::cerr << G_STRFUNC << ": transient state not loaded - " << error.what().raw() << std::endl;
+ }
+ }
+ }
+ else {
+ // state not available or not valid; prepare defaults
+ dialog_defaults(docking_container);
+ }
+ } catch (Glib::Error &error) {
+ std::cerr << G_STRFUNC << ": dialogs state not loaded - " << error.what().raw() << std::endl;
+ }
+}
+
+void DialogManager::remove_dialog_floating_state(const Glib::ustring& dialog_type) {
+ auto it = _floating_dialogs.find(dialog_type);
+ if (it != _floating_dialogs.end()) {
+ _floating_dialogs.erase(it);
+ }
+}
+
+// apply defaults when dialog state cannot be loaded / doesn't exist:
+// here we load defaults from dedicated ini file
+void DialogManager::dialog_defaults(DialogContainer* docking_container) {
+ auto keyfile = std::make_unique<Glib::KeyFile>();
+ // default/initial state used when running Inkscape for the first time
+ std::string filename = Inkscape::IO::Resource::get_filename(Inkscape::IO::Resource::UIS, "default-dialog-state.ini");
+
+ bool exists = file_exists(filename);
+
+ if (exists && keyfile->load_from_file(filename)) {
+ // populate info about floating dialogs, so when users try opening them,
+ // they will pop up in a window, not docked
+ load_transient_state(keyfile.get());
+ // create docked dialogs only, if any
+ docking_container->load_container_state(keyfile.get(), false);
+ }
+ else {
+ g_warning("Cannot load default dialog state %s", filename.c_str());
+ }
+}
+
+} // 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..b9b95dc
--- /dev/null
+++ b/src/ui/dialog/dialog-manager.h
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifndef INKSCAPE_UI_DIALOG_MANAGER_H
+#define INKSCAPE_UI_DIALOG_MANAGER_H
+
+#include <glibmm/keyfile.h>
+#include <gtkmm/window.h>
+#include <map>
+#include <set>
+#include <memory>
+#include <optional>
+#include <vector>
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class DialogWindow;
+class DialogBase;
+class DialogContainer;
+
+struct window_position_t
+{
+ int x, y, width, height;
+};
+
+// try to read window's geometry
+std::optional<window_position_t> dm_get_window_position(Gtk::Window &window);
+
+// restore window's geometry
+void dm_restore_window_position(Gtk::Window &window, const window_position_t &position);
+
+class DialogManager
+{
+public:
+ static DialogManager &singleton();
+
+ // store complete dialog window state (including its container state)
+ void store_state(DialogWindow &wnd);
+
+ // return true if dialog 'type' should be opened as floating
+ bool should_open_floating(const Glib::ustring& dialog_type);
+
+ // find instance of dialog 'type' in one of currently open floating dialog windows
+ DialogBase *find_floating_dialog(const Glib::ustring& dialog_type);
+
+ // find window hosting floating dialog
+ DialogWindow* find_floating_dialog_window(const Glib::ustring& dialog_type);
+
+ // find floating window state hosting dialog 'code', if there was one
+ std::shared_ptr<Glib::KeyFile> find_dialog_state(const Glib::ustring& dialog_type);
+
+ // remove dialog floating state
+ void remove_dialog_floating_state(const Glib::ustring& dialog_type);
+
+ // save configuration of docked and floating dialogs
+ void save_dialogs_state(DialogContainer *docking_container);
+
+ // restore state of dialogs
+ void restore_dialogs_state(DialogContainer *docking_container, bool include_floating);
+
+ // find all floating dialog windows
+ std::vector<DialogWindow*> get_all_floating_dialog_windows();
+
+ // show/hide dialog window and keep track of it
+ void set_floating_dialog_visibility(DialogWindow* wnd, bool show);
+
+private:
+ DialogManager() = default;
+ ~DialogManager() = default;
+
+ std::vector<Glib::ustring> count_dialogs(const Glib::KeyFile *state) const;
+ void load_transient_state(Glib::KeyFile *keyfile);
+ void dialog_defaults(DialogContainer* docking_container);
+
+ // transient dialog state for floating windows user closes
+ std::map<std::string, std::shared_ptr<Glib::KeyFile>> _floating_dialogs;
+ // temp set used when dialogs are hidden (F12 toggle)
+ std::set<DialogWindow*> _hidden_dlg_windows;
+};
+
+} // 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/dialog-multipaned.cpp b/src/ui/dialog/dialog-multipaned.cpp
new file mode 100644
index 0000000..5483f46
--- /dev/null
+++ b/src/ui/dialog/dialog-multipaned.cpp
@@ -0,0 +1,1312 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/** @file
+ * @brief A widget with multiple panes. Agnostic to type what kind of widgets panes contain.
+ *
+ * Authors: see git history
+ * Tavmjong Bah
+ *
+ * Copyright (c) 2020 Tavmjong Bah, Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "dialog-multipaned.h"
+
+#include <glibmm/i18n.h>
+#include <glibmm/objectbase.h>
+#include <gtkmm/container.h>
+#include <gtkmm/image.h>
+#include <gtkmm/label.h>
+#include <iostream>
+#include <numeric>
+
+#include "ui/dialog/dialog-notebook.h"
+#include "ui/util.h"
+#include "ui/widget/canvas-grid.h"
+#include "dialog-window.h"
+
+#define DROPZONE_SIZE 5
+#define DROPZONE_EXPANSION 15
+#define HANDLE_SIZE 12
+#define HANDLE_CROSS_SIZE 25
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/*
+ * References:
+ * https://blog.gtk.org/2017/06/
+ * https://developer.gnome.org/gtkmm-tutorial/stable/sec-custom-containers.html.en
+ * https://wiki.gnome.org/HowDoI/Gestures
+ *
+ * The children widget sizes are "sticky". They change a minimal
+ * amount when the parent widget is resized or a child is added or
+ * removed.
+ *
+ * A gesture is used to track handle movement. This must be attached
+ * to the parent widget (the offset_x/offset_y values are relative to
+ * the widget allocation which changes for the handles as they are
+ * moved).
+ */
+
+int get_handle_size() {
+ return HANDLE_SIZE;
+}
+
+/* ============ MyDropZone ============ */
+
+std::list<MyDropZone *> MyDropZone::_instances_list;
+
+MyDropZone::MyDropZone(Gtk::Orientation orientation)
+ : Glib::ObjectBase("MultipanedDropZone")
+ , Gtk::Orientable()
+ , Gtk::EventBox()
+{
+ set_name("MultipanedDropZone");
+ set_orientation(orientation);
+ set_size(DROPZONE_SIZE);
+
+ get_style_context()->add_class("backgnd-passive");
+
+ signal_drag_motion().connect([=](const Glib::RefPtr<Gdk::DragContext>& ctx, int x, int y, guint time) {
+ if (!_active) {
+ _active = true;
+ add_highlight();
+ set_size(DROPZONE_SIZE + DROPZONE_EXPANSION);
+ }
+ return true;
+ });
+
+ signal_drag_leave().connect([=](const Glib::RefPtr<Gdk::DragContext>&, guint time) {
+ if (_active) {
+ _active = false;
+ set_size(DROPZONE_SIZE);
+ }
+ });
+
+ _instances_list.push_back(this);
+}
+
+MyDropZone::~MyDropZone()
+{
+ _instances_list.remove(this);
+}
+
+void MyDropZone::add_highlight_instances()
+{
+ for (auto *instance : _instances_list) {
+ instance->add_highlight();
+ }
+}
+
+void MyDropZone::remove_highlight_instances()
+{
+ for (auto *instance : _instances_list) {
+ instance->remove_highlight();
+ // instance->set_size(DROPZONE_SIZE);
+ }
+}
+
+void MyDropZone::add_highlight()
+{
+ const auto &style = get_style_context();
+ style->remove_class("backgnd-passive");
+ style->add_class("backgnd-active");
+}
+
+void MyDropZone::remove_highlight()
+{
+ const auto &style = get_style_context();
+ style->remove_class("backgnd-active");
+ style->add_class("backgnd-passive");
+}
+
+void MyDropZone::set_size(int size)
+{
+ if (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) {
+ set_size_request(size, -1);
+ } else {
+ set_size_request(-1, size);
+ }
+}
+
+/* ============ MyHandle ============ */
+
+MyHandle::MyHandle(Gtk::Orientation orientation, int size = get_handle_size())
+ : Glib::ObjectBase("MultipanedHandle")
+ , Gtk::Orientable()
+ , Gtk::EventBox()
+ , _cross_size(0)
+ , _child(nullptr)
+{
+ set_name("MultipanedHandle");
+ set_orientation(orientation);
+
+ add_events(Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK | Gdk::POINTER_MOTION_MASK);
+
+ Gtk::Image *image = Gtk::manage(new Gtk::Image());
+ if (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) {
+ image->set_from_icon_name("view-more-symbolic", Gtk::ICON_SIZE_SMALL_TOOLBAR);
+ set_size_request(size, -1);
+ } else {
+ image->set_from_icon_name("view-more-horizontal-symbolic", Gtk::ICON_SIZE_SMALL_TOOLBAR);
+ set_size_request(-1, size);
+ }
+ image->set_pixel_size(size);
+ add(*image);
+
+ // Signal
+ signal_size_allocate().connect(sigc::mem_fun(*this, &MyHandle::resize_handler));
+
+ show_all();
+}
+
+// draw rectangle with rounded corners
+void rounded_rectangle(const Cairo::RefPtr<Cairo::Context>& cr, double x, double y, double w, double h, double r) {
+ cr->begin_new_sub_path();
+ cr->arc(x + r, y + r, r, M_PI, 3 * M_PI / 2);
+ cr->arc(x + w - r, y + r, r, 3 * M_PI / 2, 2 * M_PI);
+ cr->arc(x + w - r, y + h - r, r, 0, M_PI / 2);
+ cr->arc(x + r, y + h - r, r, M_PI / 2, M_PI);
+ cr->close_path();
+}
+
+// part of the handle where clicking makes it automatically collapse/expand docked dialogs
+Cairo::Rectangle MyHandle::get_active_click_zone() {
+ const Gtk::Allocation& allocation = get_allocation();
+ double width = allocation.get_width();
+ double height = allocation.get_height();
+ double h = height / 5;
+
+ Cairo::Rectangle rect = { .x = 0, .y = (height - h) / 2, .width = width, .height = h };
+ return rect;
+}
+
+bool MyHandle::on_draw(const Cairo::RefPtr<Cairo::Context>& cr) {
+ bool ret = EventBox::on_draw(cr);
+
+ // show click indicator/highlight?
+ if (_click_indicator && is_click_resize_active() && !_dragging) {
+ auto rect = get_active_click_zone();
+
+ if (rect.width > 4 && rect.height > 0) {
+ Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context();
+ Gdk::RGBA fg = style_context->get_color(get_state_flags());
+ rounded_rectangle(cr, rect.x + 2, rect.y, rect.width - 4, rect.height, 3);
+ cr->set_source_rgba(fg.get_red(), fg.get_green(), fg.get_blue(), 0.26);
+ cr->fill();
+ }
+ }
+
+ return ret;
+}
+
+void MyHandle::set_dragging(bool dragging) {
+ if (_dragging != dragging) {
+ _dragging = dragging;
+ if (_click_indicator) {
+ queue_draw();
+ }
+ }
+}
+
+/**
+ * Change the mouse pointer into a resize icon to show you can drag.
+ */
+bool MyHandle::on_enter_notify_event(GdkEventCrossing *crossing_event)
+{
+ auto window = get_window();
+ auto display = get_display();
+
+ if (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) {
+ auto cursor = Gdk::Cursor::create(display, "col-resize");
+ window->set_cursor(cursor);
+ } else {
+ auto cursor = Gdk::Cursor::create(display, "row-resize");
+ window->set_cursor(cursor);
+ }
+
+ update_click_indicator(crossing_event->x, crossing_event->y);
+
+ return false;
+}
+
+bool MyHandle::on_leave_notify_event(GdkEventCrossing* crossing_event) {
+ show_click_indicator(false);
+ return false;
+}
+
+void MyHandle::show_click_indicator(bool show) {
+ if (!is_click_resize_active()) return;
+
+ if (show != _click_indicator) {
+ _click_indicator = show;
+ this->queue_draw();
+ }
+}
+
+void MyHandle::update_click_indicator(double x, double y) {
+ if (!is_click_resize_active()) return;
+
+ auto rect = get_active_click_zone();
+ bool inside =
+ x >= rect.x && x < rect.x + rect.width &&
+ y >= rect.y && y < rect.y + rect.height;
+
+ show_click_indicator(inside);
+}
+
+bool MyHandle::is_click_resize_active() const {
+ return get_orientation() == Gtk::ORIENTATION_HORIZONTAL;
+}
+
+bool MyHandle::on_button_press_event(GdkEventButton* button_event) {
+ // detect single-clicks
+ _click = button_event->button == 1 && button_event->type == GDK_BUTTON_PRESS;
+ return false;
+}
+
+bool MyHandle::on_button_release_event(GdkEventButton* event) {
+ // single-click on active zone?
+ if (_click && event->type == GDK_BUTTON_RELEASE && event->button == 1 && _click_indicator) {
+ _click = false;
+ _dragging = false;
+ // handle clicked
+ if (is_click_resize_active()) {
+ toggle_multipaned();
+ return true;
+ }
+ }
+
+ _click = false;
+ return false;
+}
+
+void MyHandle::toggle_multipaned() {
+ // visibility toggle of multipaned in a floating dialog window doesn't make sense; skip
+ if (dynamic_cast<DialogWindow*>(get_toplevel())) return;
+
+ auto panel = dynamic_cast<DialogMultipaned*>(get_parent());
+ if (!panel) return;
+
+ auto children = panel->get_children();
+ Gtk::Widget* multi = nullptr; // multipaned widget to toggle
+ bool left_side = true; // panels to the left of canvas
+ size_t i = 0;
+
+ // find multipaned widget to resize; it is adjacent (sibling) to 'this' handle in widget hierarchy
+ for (auto widget : children) {
+ if (dynamic_cast<Inkscape::UI::Widget::CanvasGrid*>(widget)) {
+ // widget past canvas are on the right side (of canvas)
+ left_side = false;
+ }
+
+ if (widget == this) {
+ if (left_side && i > 0) {
+ // handle to the left of canvas toggles preceeding panel
+ multi = dynamic_cast<DialogMultipaned*>(children[i - 1]);
+ }
+ else if (!left_side && i + 1 < children.size()) {
+ // handle to the right of canvas toggles next panel
+ multi = dynamic_cast<DialogMultipaned*>(children[i + 1]);
+ }
+
+ if (multi) {
+ if (multi->is_visible()) {
+ multi->hide();
+ }
+ else {
+ multi->show();
+ }
+ // resize parent
+ panel->children_toggled();
+ }
+ break;
+ }
+
+ ++i;
+ }
+}
+
+bool MyHandle::on_motion_notify_event(GdkEventMotion* motion_event) {
+ // motion invalidates click; it activates resizing
+ _click = false;
+ // show_click_indicator(false);
+ update_click_indicator(motion_event->x, motion_event->y);
+ return false;
+}
+
+/**
+ * This allocation handler function is used to add/remove handle icons in order to be able
+ * to hide completely a transversal handle into the sides of a DialogMultipaned.
+ *
+ * The image has a specific size set up in the constructor and will not naturally shrink/hide.
+ * In conclusion, we remove it from the handle and save it into an internal reference.
+ */
+void MyHandle::resize_handler(Gtk::Allocation &allocation)
+{
+ int size = (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) ? allocation.get_height() : allocation.get_width();
+
+ if (_cross_size > size && HANDLE_CROSS_SIZE > size && !_child) {
+ _child = get_child();
+ remove();
+ } else if (_cross_size < size && HANDLE_CROSS_SIZE < size && _child) {
+ add(*_child);
+ _child = nullptr;
+ }
+
+ _cross_size = size;
+}
+
+/* ============ DialogMultipaned ============= */
+
+DialogMultipaned::DialogMultipaned(Gtk::Orientation orientation)
+ : Glib::ObjectBase("DialogMultipaned")
+ , Gtk::Orientable()
+ , Gtk::Container()
+ , _empty_widget(nullptr)
+{
+ set_name("DialogMultipaned");
+ set_orientation(orientation);
+ set_has_window(false);
+ set_redraw_on_allocate(false);
+
+ // ============= Add dropzones ==============
+ MyDropZone *dropzone_s = Gtk::manage(new MyDropZone(orientation));
+ MyDropZone *dropzone_e = Gtk::manage(new MyDropZone(orientation));
+
+ dropzone_s->set_parent(*this);
+ dropzone_e->set_parent(*this);
+
+ children.push_back(dropzone_s);
+ children.push_back(dropzone_e);
+
+ // ============ Connect signals =============
+ gesture = Gtk::GestureDrag::create(*this);
+
+ _connections.emplace_back(
+ gesture->signal_drag_begin().connect(sigc::mem_fun(*this, &DialogMultipaned::on_drag_begin)));
+ _connections.emplace_back(gesture->signal_drag_end().connect(sigc::mem_fun(*this, &DialogMultipaned::on_drag_end)));
+ _connections.emplace_back(
+ gesture->signal_drag_update().connect(sigc::mem_fun(*this, &DialogMultipaned::on_drag_update)));
+
+ _connections.emplace_back(
+ signal_drag_data_received().connect(sigc::mem_fun(*this, &DialogMultipaned::on_drag_data)));
+ _connections.emplace_back(
+ dropzone_s->signal_drag_data_received().connect(sigc::mem_fun(*this, &DialogMultipaned::on_prepend_drag_data)));
+ _connections.emplace_back(
+ dropzone_e->signal_drag_data_received().connect(sigc::mem_fun(*this, &DialogMultipaned::on_append_drag_data)));
+
+ // add empty widget to initiate the container
+ add_empty_widget();
+
+ show_all();
+}
+
+DialogMultipaned::~DialogMultipaned()
+{
+ // Disconnect all signals
+ for_each(_connections.begin(), _connections.end(), [&](auto c) { c.disconnect(); });
+ /*
+ for (std::vector<Gtk::Widget *>::iterator it = children.begin(); it != children.end();) {
+ if (dynamic_cast<DialogMultipaned *>(*it) || dynamic_cast<DialogNotebook *>(*it)) {
+ delete *it;
+ } else {
+ it++;
+ }
+ }
+ */
+
+ for (;;) {
+ auto it = std::find_if(children.begin(), children.end(), [](auto w) {
+ return dynamic_cast<DialogMultipaned *>(w) || dynamic_cast<DialogNotebook *>(w);
+ });
+ if (it != children.end()) {
+ // delete dialog multipanel or notebook; this action results in its removal from 'children'!
+ delete *it;
+ } else {
+ // no more dialog panels
+ break;
+ }
+ }
+
+ // need to remove CanvasGrid from this container to avoid on idle repainting and crash:
+ // Gtk:ERROR:../gtk/gtkwidget.c:5871:gtk_widget_get_frame_clock: assertion failed: (window != NULL)
+ // Bail out! Gtk:ERROR:../gtk/gtkwidget.c:5871:gtk_widget_get_frame_clock: assertion failed: (window != NULL)
+ for (auto child : children) {
+ if (dynamic_cast<Inkscape::UI::Widget::CanvasGrid*>(child)) {
+ remove(*child);
+ }
+ }
+
+ children.clear();
+}
+
+void DialogMultipaned::prepend(Gtk::Widget *child)
+{
+ remove_empty_widget(); // Will remove extra widget if existing
+
+ // If there are MyMultipane children that are empty, they will be removed
+ for (auto const &child1 : children) {
+ DialogMultipaned *paned = dynamic_cast<DialogMultipaned *>(child1);
+ if (paned && paned->has_empty_widget()) {
+ remove(*child1);
+ remove_empty_widget();
+ }
+ }
+
+ if (child) {
+ // Add handle
+ if (children.size() > 2) {
+ MyHandle *my_handle = Gtk::manage(new MyHandle(get_orientation()));
+ my_handle->set_parent(*this);
+ children.insert(children.begin() + 1, my_handle); // After start dropzone
+ }
+
+ // Add child
+ children.insert(children.begin() + 1, child);
+ if (!child->get_parent())
+ child->set_parent(*this);
+
+ // Ideally, we would only call child->show() here and assume that the
+ // child has already configured visibility of all its own children.
+ child->show_all();
+ }
+}
+
+void DialogMultipaned::append(Gtk::Widget *child)
+{
+ remove_empty_widget(); // Will remove extra widget if existing
+
+ // If there are MyMultipane children that are empty, they will be removed
+ for (auto const &child1 : children) {
+ DialogMultipaned *paned = dynamic_cast<DialogMultipaned *>(child1);
+ if (paned && paned->has_empty_widget()) {
+ remove(*child1);
+ remove_empty_widget();
+ }
+ }
+
+ if (child) {
+ // Add handle
+ if (children.size() > 2) {
+ MyHandle *my_handle = Gtk::manage(new MyHandle(get_orientation()));
+ my_handle->set_parent(*this);
+ children.insert(children.end() - 1, my_handle); // Before end dropzone
+ }
+
+ // Add child
+ children.insert(children.end() - 1, child);
+ if (!child->get_parent())
+ child->set_parent(*this);
+
+ // See comment in DialogMultipaned::prepend
+ child->show_all();
+ }
+}
+
+void DialogMultipaned::add_empty_widget()
+{
+ const int EMPTY_WIDGET_SIZE = 60; // magic number
+
+ // The empty widget is a label
+ auto label = Gtk::manage(new Gtk::Label(_("You can drop dockable dialogs here.")));
+ label->set_line_wrap();
+ label->set_justify(Gtk::JUSTIFY_CENTER);
+ label->set_valign(Gtk::ALIGN_CENTER);
+ label->set_vexpand();
+
+ append(label);
+ _empty_widget = label;
+
+ if (get_orientation() == Gtk::ORIENTATION_VERTICAL) {
+ int dropzone_size = (get_height() - EMPTY_WIDGET_SIZE) / 2;
+ if (dropzone_size > DROPZONE_SIZE) {
+ set_dropzone_sizes(dropzone_size, dropzone_size);
+ }
+ }
+}
+
+void DialogMultipaned::remove_empty_widget()
+{
+ if (_empty_widget) {
+ auto it = std::find(children.begin(), children.end(), _empty_widget);
+ if (it != children.end()) {
+ children.erase(it);
+ }
+ _empty_widget->unparent();
+ _empty_widget = nullptr;
+ }
+
+ if (get_orientation() == Gtk::ORIENTATION_VERTICAL) {
+ set_dropzone_sizes(DROPZONE_SIZE, DROPZONE_SIZE);
+ }
+}
+
+Gtk::Widget *DialogMultipaned::get_first_widget()
+{
+ if (children.size() > 2) {
+ return children[1];
+ } else {
+ return nullptr;
+ }
+}
+
+Gtk::Widget *DialogMultipaned::get_last_widget()
+{
+ if (children.size() > 2) {
+ return children[children.size() - 2];
+ } else {
+ return nullptr;
+ }
+}
+
+/**
+ * Set the sizes of the DialogMultipaned dropzones.
+ * @param start, the size you want or -1 for the default `DROPZONE_SIZE`
+ * @param end, the size you want or -1 for the default `DROPZONE_SIZE`
+ */
+void DialogMultipaned::set_dropzone_sizes(int start, int end)
+{
+ bool orientation = get_orientation() == Gtk::ORIENTATION_HORIZONTAL;
+
+ if (start == -1) {
+ start = DROPZONE_SIZE;
+ }
+
+ MyDropZone *dropzone_s = dynamic_cast<MyDropZone *>(children[0]);
+
+ if (dropzone_s) {
+ if (orientation) {
+ dropzone_s->set_size_request(start, -1);
+ } else {
+ dropzone_s->set_size_request(-1, start);
+ }
+ }
+
+ if (end == -1) {
+ end = DROPZONE_SIZE;
+ }
+
+ MyDropZone *dropzone_e = dynamic_cast<MyDropZone *>(children[children.size() - 1]);
+
+ if (dropzone_e) {
+ if (orientation) {
+ dropzone_e->set_size_request(end, -1);
+ } else {
+ dropzone_e->set_size_request(-1, end);
+ }
+ }
+}
+
+/**
+ * Show/hide as requested all children of this container that are of type multipaned
+ */
+void DialogMultipaned::toggle_multipaned_children(bool show)
+{
+ _handle = -1;
+ _drag_handle = -1;
+
+ for (auto child : children) {
+ if (auto panel = dynamic_cast<DialogMultipaned*>(child)) {
+ if (show) {
+ panel->show();
+ }
+ else {
+ panel->hide();
+ }
+ }
+ }
+}
+
+/**
+ * Ensure that this dialog container is visible.
+ */
+void DialogMultipaned::ensure_multipaned_children()
+{
+ toggle_multipaned_children(true);
+ // hide_multipaned = false;
+ // queue_allocate();
+}
+
+// ****************** OVERRIDES ******************
+
+// The following functions are here to define the behavior of our custom container
+
+Gtk::SizeRequestMode DialogMultipaned::get_request_mode_vfunc() const
+{
+ if (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) {
+ return Gtk::SIZE_REQUEST_WIDTH_FOR_HEIGHT;
+ } else {
+ return Gtk::SIZE_REQUEST_HEIGHT_FOR_WIDTH;
+ }
+}
+
+void DialogMultipaned::get_preferred_width_vfunc(int &minimum_width, int &natural_width) const
+{
+ minimum_width = 0;
+ natural_width = 0;
+ for (auto const &child : children) {
+ if (child && child->is_visible()) {
+ int child_minimum_width = 0;
+ int child_natural_width = 0;
+ child->get_preferred_width(child_minimum_width, child_natural_width);
+ if (get_orientation() == Gtk::ORIENTATION_VERTICAL) {
+ minimum_width = std::max(minimum_width, child_minimum_width);
+ natural_width = std::max(natural_width, child_natural_width);
+ } else {
+ minimum_width += child_minimum_width;
+ natural_width += child_natural_width;
+ }
+ }
+ }
+ if (_natural_width > natural_width) {
+ natural_width = _natural_width;
+ }
+}
+
+void DialogMultipaned::get_preferred_height_vfunc(int &minimum_height, int &natural_height) const
+{
+ minimum_height = 0;
+ natural_height = 0;
+ for (auto const &child : children) {
+ if (child && child->is_visible()) {
+ int child_minimum_height = 0;
+ int child_natural_height = 0;
+ child->get_preferred_height(child_minimum_height, child_natural_height);
+ if (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) {
+ minimum_height = std::max(minimum_height, child_minimum_height);
+ natural_height = std::max(natural_height, child_natural_height);
+ } else {
+ minimum_height += child_minimum_height;
+ natural_height += child_natural_height;
+ }
+ }
+ }
+}
+
+void DialogMultipaned::get_preferred_width_for_height_vfunc(int height, int &minimum_width, int &natural_width) const
+{
+ minimum_width = 0;
+ natural_width = 0;
+ for (auto const &child : children) {
+ if (child && child->is_visible()) {
+ int child_minimum_width = 0;
+ int child_natural_width = 0;
+ child->get_preferred_width_for_height(height, child_minimum_width, child_natural_width);
+ if (get_orientation() == Gtk::ORIENTATION_VERTICAL) {
+ minimum_width = std::max(minimum_width, child_minimum_width);
+ natural_width = std::max(natural_width, child_natural_width);
+ } else {
+ minimum_width += child_minimum_width;
+ natural_width += child_natural_width;
+ }
+ }
+ }
+
+ if (_natural_width > natural_width) {
+ natural_width = _natural_width;
+ }
+}
+
+void DialogMultipaned::get_preferred_height_for_width_vfunc(int width, int &minimum_height, int &natural_height) const
+{
+ minimum_height = 0;
+ natural_height = 0;
+ for (auto const &child : children) {
+ if (child && child->is_visible()) {
+ int child_minimum_height = 0;
+ int child_natural_height = 0;
+ child->get_preferred_height_for_width(width, child_minimum_height, child_natural_height);
+ if (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) {
+ minimum_height = std::max(minimum_height, child_minimum_height);
+ natural_height = std::max(natural_height, child_natural_height);
+ } else {
+ minimum_height += child_minimum_height;
+ natural_height += child_natural_height;
+ }
+ }
+ }
+}
+
+
+void DialogMultipaned::children_toggled() {
+ _handle = -1;
+ _drag_handle = -1;
+ queue_allocate();
+}
+
+/**
+ * This function allocates the sizes of the children widgets (be them internal or not) from
+ * the container's allocated size.
+ *
+ * Natural width: The width the widget really wants.
+ * Minimum width: The minimum width for a widget to be useful.
+ * Minimum <= Natural.
+ */
+void DialogMultipaned::on_size_allocate(Gtk::Allocation &allocation)
+{
+ set_allocation(allocation);
+ bool horizontal = get_orientation() == Gtk::ORIENTATION_HORIZONTAL;
+
+ if (_drag_handle != -1) { // Exchange allocation between the widgets on either side of moved handle
+ // Allocation values calculated in on_drag_update();
+ children[_drag_handle - 1]->size_allocate(allocation1);
+ children[_drag_handle]->size_allocate(allocationh);
+ children[_drag_handle + 1]->size_allocate(allocation2);
+ _drag_handle = -1;
+ }
+ // initially widgets get created with a 1x1 size; ignore it and wait for the final resize
+ else if (allocation.get_width() > 1 && allocation.get_height() > 1) {
+ _natural_width = allocation.get_width();
+ }
+
+ std::vector<bool> expandables; // Is child expandable?
+ std::vector<int> sizes_minimums; // Difference between allocated space and minimum space.
+ std::vector<int> sizes_naturals; // Difference between allocated space and natural space.
+ std::vector<int> sizes_current; // The current sizes along main axis
+ int left = horizontal ? allocation.get_width() : allocation.get_height();
+
+ int index = 0;
+ bool force_resize = false; // initially panels are not sized yet, so we will apply their natural sizes
+ int canvas_index = -1;
+ for (auto child : children) {
+ bool visible = child->get_visible();
+
+ if (dynamic_cast<Inkscape::UI::Widget::CanvasGrid*>(child)) {
+ canvas_index = index;
+ }
+
+ {
+ expandables.push_back(child->compute_expand(get_orientation()));
+
+ Gtk::Requisition req_minimum;
+ Gtk::Requisition req_natural;
+ child->get_preferred_size(req_minimum, req_natural);
+ if (child == _resizing_widget1 || child == _resizing_widget2) {
+ // ignore limits for widget being resized interactively and use their current size
+ req_minimum.width = req_minimum.height = 0;
+ auto alloc = child->get_allocation();
+ req_natural.width = alloc.get_width();
+ req_natural.height = alloc.get_height();
+ }
+
+ sizes_minimums.push_back(visible ? horizontal ? req_minimum.width : req_minimum.height : 0);
+ sizes_naturals.push_back(visible ? horizontal ? req_natural.width : req_natural.height : 0);
+ }
+
+ Gtk::Allocation child_allocation = child->get_allocation();
+ sizes_current.push_back(visible ? horizontal ? child_allocation.get_width() : child_allocation.get_height()
+ : 0);
+ index++;
+
+ if (sizes_current.back() < sizes_minimums.back()) force_resize = true;
+ }
+
+ std::vector<int> sizes = sizes_current; // The new allocation sizes
+
+ const int sum_current = std::accumulate(sizes_current.begin(), sizes_current.end(), 0);
+ {
+ // Precalculate the minimum, natural and current totals
+ const int sum_minimums = std::accumulate(sizes_minimums.begin(), sizes_minimums.end(), 0);
+ const int sum_naturals = std::accumulate(sizes_naturals.begin(), sizes_naturals.end(), 0);
+
+ // initial resize requested?
+ if (force_resize && sum_naturals <= left) {
+ sizes = sizes_naturals;
+ left -= sum_naturals;
+ } else if (sum_minimums <= left && left < sum_current) {
+ // requested size exeeds available space; try shrinking it by starting from the last element
+ sizes = sizes_current;
+ auto excess = sum_current - left;
+ for (int i = static_cast<int>(sizes.size() - 1); excess > 0 && i >= 0; --i) {
+ auto extra = sizes_current[i] - sizes_minimums[i];
+ if (extra > 0) {
+ if (extra >= excess) {
+ // we are done, enough space found
+ sizes[i] -= excess;
+ excess = 0;
+ }
+ else {
+ // shrink as far as possible, then continue to the next panel
+ sizes[i] -= extra;
+ excess -= extra;
+ }
+ }
+ }
+
+ if (excess > 0) {
+ sizes = sizes_minimums;
+ left -= sum_minimums;
+ }
+ else {
+ left = 0;
+ }
+ }
+ else {
+ left = std::max(0, left - sum_current);
+ }
+ }
+
+ if (canvas_index >= 0) { // give remaining space to canvas element
+ sizes[canvas_index] += left;
+ } else { // or, if in a sub-dialogmultipaned, give it to the last panel
+
+ for (int i = static_cast<int>(children.size()) - 1; i >= 0; --i) {
+ if (expandables[i]) {
+ sizes[i] += left;
+ break;
+ }
+ }
+ }
+
+ // Check if we actually need to change the sizes on the main axis
+ left = horizontal ? allocation.get_width() : allocation.get_height();
+ if (left == sum_current) {
+ bool valid = true;
+ for (size_t i = 0; i < children.size(); ++i) {
+ valid = sizes_minimums[i] <= sizes_current[i] && // is it over the minimums?
+ (expandables[i] || sizes_current[i] <= sizes_naturals[i]); // but does it want to be expanded?
+ if (!valid)
+ break;
+ }
+ if (valid) {
+ sizes = sizes_current; // The current sizes are good, don't change anything;
+ }
+ }
+
+ // Set x and y values of allocations (widths should be correct).
+ int current_x = allocation.get_x();
+ int current_y = allocation.get_y();
+
+ // Allocate
+ for (size_t i = 0; i < children.size(); ++i) {
+ Gtk::Allocation child_allocation = children[i]->get_allocation();
+ child_allocation.set_x(current_x);
+ child_allocation.set_y(current_y);
+
+ int size = sizes[i];
+
+ if (horizontal) {
+ child_allocation.set_width(size);
+ current_x += size;
+ child_allocation.set_height(allocation.get_height());
+ } else {
+ child_allocation.set_height(size);
+ current_y += size;
+ child_allocation.set_width(allocation.get_width());
+ }
+
+ children[i]->size_allocate(child_allocation);
+ }
+}
+
+void DialogMultipaned::forall_vfunc(gboolean, GtkCallback callback, gpointer callback_data)
+{
+ for (auto const &child : children) {
+ if (child) {
+ callback(child->gobj(), callback_data);
+ }
+ }
+}
+
+void DialogMultipaned::on_add(Gtk::Widget *child)
+{
+ if (child) {
+ append(child);
+ }
+}
+
+/**
+ * Callback when a widget is removed from DialogMultipaned and executes the removal.
+ * It does not remove handles or dropzones.
+ */
+void DialogMultipaned::on_remove(Gtk::Widget *child)
+{
+ if (child) {
+ MyDropZone *dropzone = dynamic_cast<MyDropZone *>(child);
+ if (dropzone) {
+ return;
+ }
+ MyHandle *my_handle = dynamic_cast<MyHandle *>(child);
+ if (my_handle) {
+ return;
+ }
+
+ const bool visible = child->get_visible();
+ if (children.size() > 2) {
+ auto it = std::find(children.begin(), children.end(), child);
+ if (it != children.end()) { // child found
+ if (it + 2 != children.end()) { // not last widget
+ my_handle = dynamic_cast<MyHandle *>(*(it + 1));
+ my_handle->unparent();
+ child->unparent();
+ children.erase(it, it + 2);
+ } else { // last widget
+ if (children.size() == 3) { // only widget
+ child->unparent();
+ children.erase(it);
+ } else { // not only widget, delete preceding handle
+ my_handle = dynamic_cast<MyHandle *>(*(it - 1));
+ my_handle->unparent();
+ child->unparent();
+ children.erase(it - 1, it + 1);
+ }
+ }
+ }
+ }
+ if (visible) {
+ queue_resize();
+ }
+
+ if (children.size() == 2) {
+ add_empty_widget();
+ _empty_widget->set_size_request(300, -1);
+ _signal_now_empty.emit();
+ }
+ }
+}
+
+void DialogMultipaned::on_drag_begin(double start_x, double start_y)
+{
+ _hide_widget1 = _hide_widget2 = nullptr;
+ _resizing_widget1 = _resizing_widget2 = nullptr;
+ // We clicked on handle.
+ bool found = false;
+ int child_number = 0;
+ Gtk::Allocation allocation = get_allocation();
+ for (auto const &child : children) {
+ MyHandle *my_handle = dynamic_cast<MyHandle *>(child);
+ if (my_handle) {
+ Gtk::Allocation child_allocation = my_handle->get_allocation();
+
+ // Did drag start in handle?
+ int x = child_allocation.get_x() - allocation.get_x();
+ int y = child_allocation.get_y() - allocation.get_y();
+ if (x < start_x && start_x < x + child_allocation.get_width() && y < start_y &&
+ start_y < y + child_allocation.get_height()) {
+ found = true;
+ my_handle->set_dragging(true);
+ break;
+ }
+ }
+ ++child_number;
+ }
+
+ if (!found) {
+ gesture->set_state(Gtk::EVENT_SEQUENCE_DENIED);
+ return;
+ }
+
+ if (child_number < 1 || child_number > (int)(children.size() - 2)) {
+ std::cerr << "DialogMultipaned::on_drag_begin: Invalid child (" << child_number << "!!" << std::endl;
+ gesture->set_state(Gtk::EVENT_SEQUENCE_DENIED);
+ return;
+ }
+
+ gesture->set_state(Gtk::EVENT_SEQUENCE_CLAIMED);
+
+ // Save for use in on_drag_update().
+ _handle = child_number;
+ start_allocation1 = children[_handle - 1]->get_allocation();
+ if (!children[_handle - 1]->is_visible()) {
+ start_allocation1.set_width(0);
+ start_allocation1.set_height(0);
+ }
+ start_allocationh = children[_handle]->get_allocation();
+ start_allocation2 = children[_handle + 1]->get_allocation();
+ if (!children[_handle + 1]->is_visible()) {
+ start_allocation2.set_width(0);
+ start_allocation2.set_height(0);
+ }
+}
+
+void DialogMultipaned::on_drag_end(double offset_x, double offset_y)
+{
+ if (_handle >= 0 && _handle < children.size()) {
+ if (auto my_handle = dynamic_cast<MyHandle*>(children[_handle])) {
+ my_handle->set_dragging(false);
+ }
+ }
+
+ gesture->set_state(Gtk::EVENT_SEQUENCE_DENIED);
+ _handle = -1;
+ _drag_handle = -1;
+ if (_hide_widget1) {
+ _hide_widget1->hide();
+ }
+ if (_hide_widget2) {
+ _hide_widget2->hide();
+ }
+ _hide_widget1 = nullptr;
+ _hide_widget2 = nullptr;
+ _resizing_widget1 = nullptr;
+ _resizing_widget2 = nullptr;
+
+ queue_allocate(); // reimpose limits if any were bent during interactive resizing
+}
+
+// docking panels in application window can be collapsed (to left or right side) to make more
+// room for canvas; this functionality is only meaningful in app window, not in floating dialogs
+bool can_collapse(Gtk::Widget* widget, Gtk::Widget* handle) {
+ // can only collapse DialogMultipaned widgets
+ if (!widget || dynamic_cast<DialogMultipaned*>(widget) == nullptr) return false;
+
+ // collapsing is not supported in floating dialogs
+ if (dynamic_cast<DialogWindow*>(widget->get_toplevel())) return false;
+
+ auto parent = handle->get_parent();
+ if (!parent) return false;
+
+ // find where the resizing handle is in relation to canvas area: left or right side;
+ // next, find where the panel is in relation to the handle: on its left or right
+ bool left_side = true;
+ bool left_handle = false;
+ size_t panel_index = 0;
+ size_t handle_index = 0;
+ size_t i = 0;
+ for (auto child : parent->get_children()) {
+ if (dynamic_cast<Inkscape::UI::Widget::CanvasGrid*>(child)) {
+ left_side = false;
+ }
+ else if (child == handle) {
+ left_handle = left_side;
+ handle_index = i;
+ }
+ else if (child == widget) {
+ panel_index = i;
+ }
+ ++i;
+ }
+
+ if (left_handle && panel_index < handle_index) {
+ return true;
+ }
+ if (!left_handle && panel_index > handle_index) {
+ return true;
+ }
+
+ return false;
+}
+
+// return minimum widget size; this fn works for hidden widgets too
+int get_min_width(Gtk::Widget* widget) {
+ bool hidden = !widget->is_visible();
+ if (hidden) widget->show();
+ int minimum_size = 0;
+ int natural_size = 0;
+ widget->get_preferred_width(minimum_size, natural_size);
+ if (hidden) widget->hide();
+ return minimum_size;
+}
+
+// Different docking resizing activities use easing functions to speed up or slow down certain phases of resizing
+// Below are two picewise linear functions used for that purpose
+
+// easing function for revealing collapsed panels
+double reveal_curve(double val, double size) {
+ if (size > 0 && val <= size && val >= 0) {
+ // slow start (resistance to opening) and then quick reveal
+ auto x = val / size;
+ auto pos = x;
+ if (x <= 0.2) {
+ pos = x * 0.25;
+ }
+ else {
+ pos = x * 9.5 - 1.85;
+ if (pos > 1) pos = 1;
+ }
+ return size * pos;
+ }
+
+ return val;
+}
+
+// easing function for collapsing panels
+// note: factors for x dictate how fast resizing happens when moving mouse (with 1 being at the same speed);
+// other constants are to make this fn produce values in 0..1 range and seamlessly connect three segments
+double collapse_curve(double val, double size) {
+ if (size > 0 && val <= size && val >= 0) {
+ // slow start (resistance), short pause and then quick collapse
+ auto x = val / size;
+ auto pos = x;
+ if (x < 0.5) {
+ // fast collapsing
+ pos = x * 10 - 5 + 0.92;
+ if (pos < 0) {
+ // panel collapsed
+ pos = 0;
+ }
+ }
+ else if (x < 0.6) {
+ // pause
+ pos = 0.2 * 0.6 + 0.8; // = 0.92;
+ }
+ else {
+ // resistance to collapsing (move slow, x 0.2 decrease)
+ pos = x * 0.2 + 0.8;
+ }
+ return size * pos;
+ }
+
+ return val;
+}
+
+void DialogMultipaned::on_drag_update(double offset_x, double offset_y)
+{
+ if (_handle < 0) return;
+
+ auto child1 = children[_handle - 1];
+ auto child2 = children[_handle + 1];
+ allocation1 = children[_handle - 1]->get_allocation();
+ allocationh = children[_handle]->get_allocation();
+ allocation2 = children[_handle + 1]->get_allocation();
+
+ // HACK: The bias prevents erratic resizing when dragging the handle fast, outside the bounds of the app.
+ const int BIAS = 1;
+
+ if (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) {
+
+ auto handle = children[_handle];
+
+ // function to resize panel
+ auto resize_fn = [](Gtk::Widget* handle, Gtk::Widget* child, int start_width, double& offset_x) {
+ int minimum_size = get_min_width(child);
+ auto width = start_width + offset_x;
+ bool resizing = false;
+ Gtk::Widget* hide = nullptr;
+
+ if (!child->is_visible() && can_collapse(child, handle)) {
+ child->show();
+ resizing = true;
+ }
+
+ if (width < minimum_size) {
+ if (can_collapse(child, handle)) {
+ resizing = true;
+ auto w = start_width == 0 ? reveal_curve(width, minimum_size) : collapse_curve(width, minimum_size);
+ offset_x = w - start_width;
+ // facilitate closing/opening panels: users don't have to drag handle all the
+ // way to collapse/expand a panel, they just need to move it fraction of the way;
+ // note: those thresholds correspond to the easing functions used
+ auto threshold = start_width == 0 ? minimum_size * 0.20 : minimum_size * 0.42;
+ hide = width <= threshold ? child : nullptr;
+ }
+ else {
+ offset_x = -(start_width - minimum_size) + BIAS;
+ }
+ }
+
+ return std::make_pair(resizing, hide);
+ };
+
+ /*
+ TODO NOTE:
+ Resizing should ideally take into account all columns, not just adjacent ones (left and right here).
+ Without it, expanding second collapsed column does not work, since first one may already have min width,
+ and cannot be shrunk anymore. Instead it should be pushed out of the way (canvas should be shrunk).
+ */
+
+ // panel on the left
+ auto action1 = resize_fn(handle, child1, start_allocation1.get_width(), offset_x);
+ _resizing_widget1 = action1.first ? child1 : nullptr;
+ _hide_widget1 = action1.second ? child1 : nullptr;
+
+ // panel on the right (needs reversing offset_x, so it can use the same logic)
+ offset_x = -offset_x;
+ auto action2 = resize_fn(handle, child2, start_allocation2.get_width(), offset_x);
+ _resizing_widget2 = action2.first ? child2 : nullptr;
+ _hide_widget2 = action2.second ? child2 : nullptr;
+ offset_x = -offset_x;
+
+ // set new sizes; they may temporarily violate min size panel requirements
+ // GTK is not happy about 0-size allocations
+ allocation1.set_width(start_allocation1.get_width() + offset_x);
+ allocationh.set_x(start_allocationh.get_x() + offset_x);
+ allocation2.set_x(start_allocation2.get_x() + offset_x);
+ allocation2.set_width(start_allocation2.get_width() - offset_x);
+ } else {
+ // nothing fancy about resizing in vertical direction; no panel collapsing happens here
+ int minimum_size;
+ int natural_size;
+ children[_handle - 1]->get_preferred_height(minimum_size, natural_size);
+ if (start_allocation1.get_height() + offset_y < minimum_size)
+ offset_y = -(start_allocation1.get_height() - minimum_size) + BIAS;
+ children[_handle + 1]->get_preferred_height(minimum_size, natural_size);
+ if (start_allocation2.get_height() - offset_y < minimum_size)
+ offset_y = start_allocation2.get_height() - minimum_size - BIAS;
+
+ allocation1.set_height(start_allocation1.get_height() + offset_y);
+ allocationh.set_y(start_allocationh.get_y() + offset_y);
+ allocation2.set_y(start_allocation2.get_y() + offset_y);
+ allocation2.set_height(start_allocation2.get_height() - offset_y);
+ }
+
+ _drag_handle = _handle;
+ queue_allocate(); // Relayout DialogMultipaned content.
+}
+
+void DialogMultipaned::set_target_entries(const std::vector<Gtk::TargetEntry> &target_entries)
+{
+ drag_dest_set(target_entries);
+ ((MyDropZone *)children[0])->drag_dest_set(target_entries, Gtk::DEST_DEFAULT_ALL, Gdk::ACTION_MOVE);
+ ((MyDropZone *)children[children.size() - 1])
+ ->drag_dest_set(target_entries, Gtk::DEST_DEFAULT_ALL, Gdk::ACTION_MOVE);
+}
+
+void DialogMultipaned::on_drag_data(const Glib::RefPtr<Gdk::DragContext> context, int x, int y,
+ const Gtk::SelectionData &selection_data, guint info, guint time)
+{
+ _signal_prepend_drag_data.emit(context);
+}
+
+void DialogMultipaned::on_prepend_drag_data(const Glib::RefPtr<Gdk::DragContext> context, int x, int y,
+ const Gtk::SelectionData &selection_data, guint info, guint time)
+{
+ _signal_prepend_drag_data.emit(context);
+}
+
+void DialogMultipaned::on_append_drag_data(const Glib::RefPtr<Gdk::DragContext> context, int x, int y,
+ const Gtk::SelectionData &selection_data, guint info, guint time)
+{
+ _signal_append_drag_data.emit(context);
+}
+
+// Signals
+sigc::signal<void (const Glib::RefPtr<Gdk::DragContext>)> DialogMultipaned::signal_prepend_drag_data()
+{
+ resize_widget_children(this);
+ return _signal_prepend_drag_data;
+}
+
+sigc::signal<void (const Glib::RefPtr<Gdk::DragContext>)> DialogMultipaned::signal_append_drag_data()
+{
+ resize_widget_children(this);
+ return _signal_append_drag_data;
+}
+
+sigc::signal<void ()> DialogMultipaned::signal_now_empty()
+{
+ return _signal_now_empty;
+}
+
+void DialogMultipaned::set_restored_width(int width) {
+ _natural_width = width;
+}
+
+} // 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-multipaned.h b/src/ui/dialog/dialog-multipaned.h
new file mode 100644
index 0000000..c7014af
--- /dev/null
+++ b/src/ui/dialog/dialog-multipaned.h
@@ -0,0 +1,203 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifndef INKSCAPE_UI_DIALOG_MULTIPANED_H
+#define INKSCAPE_UI_DIALOG_MULTIPANED_H
+
+/** @file
+ * @brief A widget with multiple panes. Agnostic to type what kind of widgets panes contain.
+ *
+ * Authors: see git history
+ * Tavmjong Bah
+ *
+ * Copyright (c) 2020 Tavmjong Bah, Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/refptr.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/eventbox.h>
+#include <gtkmm/gesturedrag.h>
+#include <gtkmm/orientable.h>
+#include <gtkmm/widget.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/** A widget with multiple panes */
+
+class MyDropZone;
+class MyHandle;
+class DialogMultipaned;
+
+/* ============ DROPZONE ============ */
+
+/**
+ * Dropzones are eventboxes at the ends of a DialogMultipaned where you can drop dialogs.
+ */
+class MyDropZone
+ : public Gtk::Orientable
+ , public Gtk::EventBox
+{
+public:
+ MyDropZone(Gtk::Orientation orientation);
+ ~MyDropZone() override;
+
+ static void add_highlight_instances();
+ static void remove_highlight_instances();
+
+private:
+ void set_size(int size);
+ bool _active = false;
+ void add_highlight();
+ void remove_highlight();
+
+ static std::list<MyDropZone *> _instances_list;
+};
+
+/* ============ HANDLE ============ */
+
+/**
+ * Handles are event boxes that help with resizing DialogMultipaned' children.
+ */
+class MyHandle
+ : public Gtk::Orientable
+ , public Gtk::EventBox
+{
+public:
+ MyHandle(Gtk::Orientation orientation, int size);
+ ~MyHandle() override = default;
+
+ bool on_enter_notify_event(GdkEventCrossing *crossing_event) override;
+ void set_dragging(bool dragging);
+private:
+ bool on_leave_notify_event(GdkEventCrossing* crossing_event) override;
+ bool on_button_press_event(GdkEventButton* button_event) override;
+ bool on_button_release_event(GdkEventButton *event) override;
+ bool on_motion_notify_event(GdkEventMotion* motion_event) override;
+ void toggle_multipaned();
+ void update_click_indicator(double x, double y);
+ void show_click_indicator(bool show);
+ bool on_draw(const Cairo::RefPtr<Cairo::Context>& cr) override;
+ Cairo::Rectangle get_active_click_zone();
+ int _cross_size;
+ Gtk::Widget *_child;
+ void resize_handler(Gtk::Allocation &allocation);
+ bool is_click_resize_active() const;
+ bool _click = false;
+ bool _click_indicator = false;
+ bool _dragging = false;
+};
+
+/* ============ MULTIPANE ============ */
+
+/*
+ * A widget with multiple panes. Agnostic to type what kind of widgets panes contain.
+ * Handles allow a user to resize children widgets. Drop zones allow adding widgets
+ * at either end.
+ */
+class DialogMultipaned
+ : public Gtk::Orientable
+ , public Gtk::Container
+{
+public:
+ DialogMultipaned(Gtk::Orientation orientation = Gtk::ORIENTATION_HORIZONTAL);
+ ~DialogMultipaned() override;
+
+ void prepend(Gtk::Widget *new_widget);
+ void append(Gtk::Widget *new_widget);
+
+ // Getters and setters
+ Gtk::Widget *get_first_widget();
+ Gtk::Widget *get_last_widget();
+ std::vector<Gtk::Widget *> get_children() { return children; }
+ void set_target_entries(const std::vector<Gtk::TargetEntry> &target_entries);
+ bool has_empty_widget() { return (bool)_empty_widget; }
+
+ // Signals
+ sigc::signal<void (const Glib::RefPtr<Gdk::DragContext>)> signal_prepend_drag_data();
+ sigc::signal<void (const Glib::RefPtr<Gdk::DragContext>)> signal_append_drag_data();
+ sigc::signal<void ()> signal_now_empty();
+
+ // UI functions
+ void set_dropzone_sizes(int start, int end);
+ void toggle_multipaned_children(bool show);
+ void children_toggled();
+ void ensure_multipaned_children();
+ void set_restored_width(int width);
+
+protected:
+ // Overrides
+ Gtk::SizeRequestMode get_request_mode_vfunc() const override;
+ 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 get_preferred_width_for_height_vfunc(int height, int &minimum_width, int &natural_width) const override;
+ void get_preferred_height_for_width_vfunc(int width, int &minimum_height, int &natural_height) const override;
+ void on_size_allocate(Gtk::Allocation &allocation) override;
+
+ // Allow us to keep track of our widgets ourselves.
+ void forall_vfunc(gboolean include_internals, GtkCallback callback, gpointer callback_data) override;
+
+ void on_add(Gtk::Widget *child) override;
+ void on_remove(Gtk::Widget *child) override;
+
+ // Signals
+ sigc::signal<void (const Glib::RefPtr<Gdk::DragContext>)> _signal_prepend_drag_data;
+ sigc::signal<void (const Glib::RefPtr<Gdk::DragContext>)> _signal_append_drag_data;
+ sigc::signal<void ()> _signal_now_empty;
+
+private:
+ // We must manage children ourselves.
+ std::vector<Gtk::Widget *> children;
+
+ // Values used when dragging handle.
+ int _handle = -1; // Child number of active handle
+ int _drag_handle = -1;
+ Gtk::Widget* _resizing_widget1 = nullptr;
+ Gtk::Widget* _resizing_widget2 = nullptr;
+ Gtk::Widget* _hide_widget1 = nullptr;
+ Gtk::Widget* _hide_widget2 = nullptr;
+ Gtk::Allocation start_allocation1;
+ Gtk::Allocation start_allocationh;
+ Gtk::Allocation start_allocation2;
+ Gtk::Allocation allocation1;
+ Gtk::Allocation allocationh;
+ Gtk::Allocation allocation2;
+
+ Glib::RefPtr<Gtk::GestureDrag> gesture;
+ // Signal callbacks
+ void on_drag_begin(double start_x, double start_y);
+ void on_drag_end(double offset_x, double offset_y);
+ void on_drag_update(double offset_x, double offset_y);
+ void on_drag_data(const Glib::RefPtr<Gdk::DragContext> context, int x, int y,
+ const Gtk::SelectionData &selection_data, guint info, guint time);
+ void on_prepend_drag_data(const Glib::RefPtr<Gdk::DragContext> context, int x, int y,
+ const Gtk::SelectionData &selection_data, guint info, guint time);
+ void on_append_drag_data(const Glib::RefPtr<Gdk::DragContext> context, int x, int y,
+ const Gtk::SelectionData &selection_data, guint info, guint time);
+
+ // Others
+ Gtk::Widget *_empty_widget; // placeholder in an empty container
+ void add_empty_widget();
+ void remove_empty_widget();
+ std::vector<sigc::connection> _connections;
+ int _natural_width = 0;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_MULTIPANED_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-notebook.cpp b/src/ui/dialog/dialog-notebook.cpp
new file mode 100644
index 0000000..e8960e9
--- /dev/null
+++ b/src/ui/dialog/dialog-notebook.cpp
@@ -0,0 +1,1023 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/** @file
+ * @brief A wrapper for Gtk::Notebook.
+ *
+ * Authors: see git history
+ * Tavmjong Bah
+ *
+ * Copyright (c) 2018 Tavmjong Bah, Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "dialog-notebook.h"
+
+#include <vector>
+#include <glibmm/i18n.h>
+#include <gtkmm/eventbox.h>
+#include <gtkmm/scrollbar.h>
+#include <gtkmm/separatormenuitem.h>
+#include <gtkmm/menu.h>
+
+#include "enums.h"
+#include "inkscape.h"
+#include "inkscape-window.h"
+#include "ui/dialog/dialog-base.h"
+#include "ui/dialog/dialog-data.h"
+#include "ui/dialog/dialog-container.h"
+#include "ui/dialog/dialog-multipaned.h"
+#include "ui/dialog/dialog-window.h"
+#include "ui/icon-loader.h"
+#include "ui/util.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+std::list<DialogNotebook *> DialogNotebook::_instances;
+
+/**
+ * DialogNotebook constructor.
+ *
+ * @param container the parent DialogContainer of the notebook.
+ */
+DialogNotebook::DialogNotebook(DialogContainer *container)
+ : Gtk::ScrolledWindow()
+ , _container(container)
+ , _labels_auto(true)
+ , _detaching_duplicate(false)
+ , _selected_page(nullptr)
+ , _label_visible(true)
+{
+ set_name("DialogNotebook");
+ set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_NEVER);
+ set_shadow_type(Gtk::SHADOW_NONE);
+ set_vexpand(true);
+ set_hexpand(true);
+
+ // =========== Getting preferences ==========
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs == nullptr) {
+ return;
+ }
+ gint labelstautus = prefs->getInt("/options/notebooklabels/value", PREFS_NOTEBOOK_LABELS_AUTO);
+ _labels_auto = labelstautus == PREFS_NOTEBOOK_LABELS_AUTO;
+ _labels_off = labelstautus == PREFS_NOTEBOOK_LABELS_OFF;
+
+ // ============= Notebook menu ==============
+ _notebook.set_name("DockedDialogNotebook");
+ _notebook.set_show_border(false);
+ _notebook.set_group_name("InkscapeDialogGroup");
+ _notebook.set_scrollable(true);
+
+ Gtk::MenuItem *new_menu_item = nullptr;
+
+ int row = 0;
+ // Close tab
+ new_menu_item = Gtk::manage(new Gtk::MenuItem(_("Close Current Tab")));
+ _conn.emplace_back(
+ new_menu_item->signal_activate().connect(sigc::mem_fun(*this, &DialogNotebook::close_tab_callback)));
+ _menu.attach(*new_menu_item, 0, 2, row, row + 1);
+ row++;
+
+ // Close notebook
+ new_menu_item = Gtk::manage(new Gtk::MenuItem(_("Close Panel")));
+ _conn.emplace_back(
+ new_menu_item->signal_activate().connect(sigc::mem_fun(*this, &DialogNotebook::close_notebook_callback)));
+ _menu.attach(*new_menu_item, 0, 2, row, row + 1);
+ row++;
+
+ // Move to new window
+ new_menu_item = Gtk::manage(new Gtk::MenuItem(_("Move Tab to New Window")));
+ _conn.emplace_back(
+ new_menu_item->signal_activate().connect([=]() { pop_tab_callback(); }));
+ _menu.attach(*new_menu_item, 0, 2, row, row + 1);
+ row++;
+
+ // Separator menu item
+ // new_menu_item = Gtk::manage(new Gtk::SeparatorMenuItem());
+ // _menu.attach(*new_menu_item, 0, 2, row, row + 1);
+ // row++;
+
+ struct Dialog {
+ Glib::ustring key;
+ Glib::ustring label;
+ Glib::ustring order;
+ Glib::ustring icon_name;
+ DialogData::Category category;
+ ScrollProvider provide_scroll;
+ };
+ std::vector<Dialog> all_dialogs;
+ auto const &dialog_data = get_dialog_data();
+ all_dialogs.reserve(dialog_data.size());
+ for (auto&& kv : dialog_data) {
+ const auto& key = kv.first;
+ const auto& data = kv.second;
+ if (data.category == DialogData::Other) {
+ continue;
+ }
+ // for sorting dialogs alphabetically, remove '_' (used for accelerators)
+ Glib::ustring order = data.label; // Already translated
+ auto underscore = order.find('_');
+ if (underscore != Glib::ustring::npos) {
+ order = order.erase(underscore, 1);
+ }
+ all_dialogs.emplace_back(Dialog {
+ .key = key,
+ .label = data.label,
+ .order = order,
+ .icon_name = data.icon_name,
+ .category = data.category,
+ .provide_scroll = data.provide_scroll
+ });
+ }
+ // sort by categories and then by names
+ std::sort(all_dialogs.begin(), all_dialogs.end(), [](const Dialog& a, const Dialog& b){
+ if (a.category != b.category) return a.category < b.category;
+ return a.order < b.order;
+ });
+
+ int col = 0;
+ DialogData::Category category = DialogData::Other;
+ for (auto&& data : all_dialogs) {
+ if (data.category != category) {
+ if (col > 0) row++;
+
+ auto separator = Gtk::make_managed<Gtk::SeparatorMenuItem>();
+ _menu.attach(*separator, 0, 2, row, row + 1);
+ row++;
+
+ category = data.category;
+ auto sep = Gtk::make_managed<Gtk::MenuItem>();
+ sep->set_label(Glib::ustring(gettext(dialog_categories[category])).uppercase());
+ sep->get_style_context()->add_class("menu-category");
+ sep->set_sensitive(false);
+ _menu.attach(*sep, 0, 2, row, row + 1);
+ col = 0;
+ row++;
+ }
+ auto key = data.key;
+ auto dlg = Gtk::make_managed<Gtk::MenuItem>();
+ auto *grid = Gtk::make_managed<Gtk::Grid>();
+ grid->set_row_spacing(10);
+ grid->set_column_spacing(8);
+ grid->insert_row(0);
+ grid->insert_column(0);
+ grid->insert_column(1);
+ grid->attach(*Gtk::make_managed<Gtk::Image>(data.icon_name, Gtk::ICON_SIZE_MENU),0,0);
+ grid->attach(*Gtk::make_managed<Gtk::Label>(data.label, Gtk::ALIGN_START, Gtk::ALIGN_CENTER, true),1,0);
+ dlg->add(*grid);
+ dlg->signal_activate().connect([=](){
+ // get desktop's container, it may be different than current '_container'!
+ if (auto desktop = SP_ACTIVE_DESKTOP) {
+ if (auto container = desktop->getContainer()) {
+ container->new_dialog(key);
+ }
+ }
+ });
+ _menu.attach(*dlg, col, col + 1, row, row + 1);
+ col++;
+ if (col > 1) {
+ col = 0;
+ row++;
+ }
+ }
+ if (prefs->getBool("/theme/symbolicIcons", true)) {
+ _menu.get_style_context()->add_class("symbolic");
+ }
+
+ _menu.show_all_children();
+
+ Gtk::Button* menubtn = Gtk::manage(new Gtk::Button());
+ menubtn->set_image_from_icon_name("go-down-symbolic");
+ menubtn->signal_clicked().connect([=](){ _menu.popup_at_widget(menubtn, Gdk::GRAVITY_SOUTH, Gdk::GRAVITY_NORTH, nullptr); });
+ _notebook.set_action_widget(menubtn, Gtk::PACK_END);
+ menubtn->show();
+ menubtn->set_relief(Gtk::RELIEF_NORMAL);
+ menubtn->set_valign(Gtk::ALIGN_CENTER);
+ menubtn->set_halign(Gtk::ALIGN_CENTER);
+ menubtn->set_can_focus(false);
+ menubtn->set_name("DialogMenuButton");
+
+ // =============== Signals ==================
+ _conn.emplace_back(signal_size_allocate().connect(sigc::mem_fun(*this, &DialogNotebook::on_size_allocate_scroll)));
+ _conn.emplace_back(_notebook.signal_drag_begin().connect(sigc::mem_fun(*this, &DialogNotebook::on_drag_begin)));
+ _conn.emplace_back(_notebook.signal_drag_end().connect(sigc::mem_fun(*this, &DialogNotebook::on_drag_end)));
+ _conn.emplace_back(_notebook.signal_page_added().connect(sigc::mem_fun(*this, &DialogNotebook::on_page_added)));
+ _conn.emplace_back(_notebook.signal_page_removed().connect(sigc::mem_fun(*this, &DialogNotebook::on_page_removed)));
+ _conn.emplace_back(_notebook.signal_switch_page().connect(sigc::mem_fun(*this, &DialogNotebook::on_page_switch)));
+
+ // ============= Finish setup ===============
+ _reload_context = true;
+ add(_notebook);
+ show_all();
+
+ _instances.push_back(this);
+}
+
+DialogNotebook::~DialogNotebook()
+{
+ // disconnect signals first, so no handlers are invoked when removing pages
+ for_each(_conn.begin(), _conn.end(), [&](auto c) { c.disconnect(); });
+ for_each(_connmenu.begin(), _connmenu.end(), [&](auto c) { c.disconnect(); });
+ for_each(_tab_connections.begin(), _tab_connections.end(), [&](auto it) { it.second.disconnect(); });
+
+ // Unlink and remove pages
+ for (int i = _notebook.get_n_pages(); i >= 0; --i) {
+ DialogBase *dialog = dynamic_cast<DialogBase *>(_notebook.get_nth_page(i));
+ _container->unlink_dialog(dialog);
+ _notebook.remove_page(i);
+ }
+
+ _conn.clear();
+ _connmenu.clear();
+ _tab_connections.clear();
+
+ _instances.remove(this);
+}
+
+void DialogNotebook::add_highlight_header()
+{
+ const auto &style = _notebook.get_style_context();
+ style->add_class("nb-highlight");
+}
+
+void DialogNotebook::remove_highlight_header()
+{
+ const auto &style = _notebook.get_style_context();
+ style->remove_class("nb-highlight");
+}
+
+/**
+ * get provide scroll
+ */
+bool
+DialogNotebook::provide_scroll(Gtk::Widget &page) {
+ auto const &dialog_data = get_dialog_data();
+ auto dialogbase = dynamic_cast<DialogBase*>(&page);
+ if (dialogbase) {
+ auto data = dialog_data.find(dialogbase->get_type());
+ if ((*data).second.provide_scroll == ScrollProvider::PROVIDE) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Set provide scroll
+ */
+Gtk::ScrolledWindow *
+DialogNotebook::get_current_scrolledwindow(bool skip_scroll_provider) {
+
+ gint pagenum = _notebook.get_current_page();
+ Gtk::Widget *page = _notebook.get_nth_page(pagenum);
+ if (page) {
+ if (skip_scroll_provider && provide_scroll(*page)) {
+ return nullptr;
+ }
+ auto container = dynamic_cast<Gtk::Container *>(page);
+ if (container) {
+ std::vector<Gtk::Widget *> widgs = container->get_children();
+ if (widgs.size()) {
+ auto scrolledwindow = dynamic_cast<Gtk::ScrolledWindow *>(widgs[0]);
+ if (scrolledwindow) {
+ return scrolledwindow;
+ }
+ }
+ }
+ }
+ return nullptr;
+}
+
+/**
+ * Adds a widget as a new page with a tab.
+ */
+void DialogNotebook::add_page(Gtk::Widget &page, Gtk::Widget &tab, Glib::ustring)
+{
+ _reload_context = true;
+ page.set_vexpand();
+
+ auto container = dynamic_cast<Gtk::Box *>(&page);
+ if (container) {
+ auto *wrapper = Gtk::manage(new Gtk::ScrolledWindow());
+ wrapper->set_vexpand(true);
+ wrapper->set_propagate_natural_height(true);
+ wrapper->set_valign(Gtk::ALIGN_FILL);
+ wrapper->set_overlay_scrolling(false);
+ wrapper->set_can_focus(false);
+ wrapper->get_style_context()->add_class("noborder");
+ auto *wrapperbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL,0));
+ wrapperbox->set_valign(Gtk::ALIGN_FILL);
+ wrapperbox->set_vexpand(true);
+ std::vector<Gtk::Widget *> widgs = container->get_children();
+ for (auto widg : widgs) {
+ bool expand = container->child_property_expand(*widg);
+ bool fill = container->child_property_fill(*widg);
+ guint padding = container->child_property_expand(*widg);
+ Gtk::PackType pack_type = container->child_property_pack_type(*widg);
+ container->remove(*widg);
+ if (pack_type == Gtk::PACK_START) {
+ wrapperbox->pack_start(*widg, expand, fill, padding);
+ } else {
+ wrapperbox->pack_end (*widg, expand, fill, padding);
+ }
+ }
+ wrapper->add(*wrapperbox);
+ container->add(*wrapper);
+ if (provide_scroll(page)) {
+ wrapper->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_EXTERNAL);
+ } else {
+ wrapper->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
+ }
+ }
+
+ int page_number = _notebook.append_page(page, tab);
+ _notebook.set_tab_reorderable(page);
+ _notebook.set_tab_detachable(page);
+ _notebook.show_all();
+ _notebook.set_current_page(page_number);
+}
+
+/**
+ * Moves a page from a different notebook to this one.
+ */
+void DialogNotebook::move_page(Gtk::Widget &page)
+{
+ // Find old notebook
+ Gtk::Notebook *old_notebook = dynamic_cast<Gtk::Notebook *>(page.get_parent());
+ if (!old_notebook) {
+ std::cerr << "DialogNotebook::move_page: page not in notebook!" << std::endl;
+ return;
+ }
+
+ Gtk::Widget *tab = old_notebook->get_tab_label(page);
+ Glib::ustring text = old_notebook->get_menu_label_text(page);
+
+ // Keep references until re-attachment
+ tab->reference();
+ page.reference();
+
+ old_notebook->detach_tab(page);
+ _notebook.append_page(page, *tab);
+ // Remove unnecessary references
+ tab->unreference();
+ page.unreference();
+
+ // Set default settings for a new page
+ _notebook.set_tab_reorderable(page);
+ _notebook.set_tab_detachable(page);
+ _notebook.show_all();
+ _reload_context = true;
+}
+
+// ============ Notebook callbacks ==============
+
+/**
+ * Callback to close the current active tab.
+ */
+void DialogNotebook::close_tab_callback()
+{
+ int page_number = _notebook.get_current_page();
+
+ if (_selected_page) {
+ page_number = _notebook.page_num(*_selected_page);
+ _selected_page = nullptr;
+ }
+
+ if (dynamic_cast<DialogBase*>(_notebook.get_nth_page(page_number))) {
+ // is this a dialog in a floating window?
+ if (auto window = dynamic_cast<DialogWindow*>(_container->get_toplevel())) {
+ // store state of floating dialog before it gets deleted
+ DialogManager::singleton().store_state(*window);
+ }
+ }
+
+ // Remove page from notebook
+ _notebook.remove_page(page_number);
+
+ // Delete the signal connection
+ remove_close_tab_callback(_selected_page);
+
+ if (_notebook.get_n_pages() == 0) {
+ close_notebook_callback();
+ return;
+ }
+
+ // Update tab labels by comparing the sum of their widths to the allocation
+ Gtk::Allocation allocation = get_allocation();
+ on_size_allocate_scroll(allocation);
+ _reload_context = true;
+}
+
+/**
+ * Shutdown callback - delete the parent DialogMultipaned before destructing.
+ */
+void DialogNotebook::close_notebook_callback()
+{
+ // Search for DialogMultipaned
+ DialogMultipaned *multipaned = dynamic_cast<DialogMultipaned *>(get_parent());
+ if (multipaned) {
+ multipaned->remove(*this);
+ } else if (get_parent()) {
+ std::cerr << "DialogNotebook::close_notebook_callback: Unexpected parent!" << std::endl;
+ get_parent()->remove(*this);
+ }
+ delete this;
+}
+
+/**
+ * Callback to move the current active tab.
+ */
+DialogWindow* DialogNotebook::pop_tab_callback()
+{
+ // Find page.
+ Gtk::Widget *page = _notebook.get_nth_page(_notebook.get_current_page());
+
+ if (_selected_page) {
+ page = _selected_page;
+ _selected_page = nullptr;
+ }
+
+ if (!page) {
+ std::cerr << "DialogNotebook::pop_tab_callback: page not found!" << std::endl;
+ return nullptr;
+ }
+
+ // Move page to notebook in new dialog window (attached to active InkscapeWindow).
+ auto inkscape_window = _container->get_inkscape_window();
+ auto window = new DialogWindow(inkscape_window, page);
+ window->show_all();
+
+ if (_notebook.get_n_pages() == 0) {
+ close_notebook_callback();
+ return window;
+ }
+
+ // Update tab labels by comparing the sum of their widths to the allocation
+ Gtk::Allocation allocation = get_allocation();
+ on_size_allocate_scroll(allocation);
+
+ return window;
+}
+
+// ========= Signal handlers - notebook =========
+
+/**
+ * Signal handler to pop a dragged tab into its own DialogWindow.
+ *
+ * A failed drag means that the page was not dropped on an existing notebook.
+ * Thus create a new window with notebook to move page to.
+ *
+ * BUG: this has inconsistent behavior on Wayland.
+ */
+void DialogNotebook::on_drag_end(const Glib::RefPtr<Gdk::DragContext> &context)
+{
+ // Remove dropzone highlights
+ MyDropZone::remove_highlight_instances();
+ for (auto instance : _instances) {
+ instance->remove_highlight_header();
+ }
+
+ bool set_floating = !context->get_dest_window();
+ if (!set_floating && context->get_dest_window()->get_window_type() == Gdk::WINDOW_FOREIGN) {
+ set_floating = true;
+ }
+
+ if (set_floating) {
+ Gtk::Widget *source = Gtk::Widget::drag_get_source_widget(context);
+
+ // Find source notebook and page
+ Gtk::Notebook *old_notebook = dynamic_cast<Gtk::Notebook *>(source);
+ if (!old_notebook) {
+ std::cerr << "DialogNotebook::on_drag_end: notebook not found!" << std::endl;
+ } else {
+ // Find page
+ Gtk::Widget *page = old_notebook->get_nth_page(old_notebook->get_current_page());
+ if (page) {
+ // Move page to notebook in new dialog window
+
+ auto inkscape_window = _container->get_inkscape_window();
+ auto window = new DialogWindow(inkscape_window, page);
+
+ // Move window to mouse pointer
+ if (auto device = context->get_device()) {
+ int x = 0, y = 0;
+ device->get_position(x, y);
+ window->move(std::max(0, x - 50), std::max(0, y - 50));
+ }
+
+ window->show_all();
+ }
+ }
+ }
+
+ // Closes the notebook if empty.
+ if (_notebook.get_n_pages() == 0) {
+ close_notebook_callback();
+ return;
+ }
+
+ // Update tab labels by comparing the sum of their widths to the allocation
+ Gtk::Allocation allocation = get_allocation();
+ on_size_allocate_scroll(allocation);
+}
+
+void DialogNotebook::on_drag_begin(const Glib::RefPtr<Gdk::DragContext> &context)
+{
+ MyDropZone::add_highlight_instances();
+ for (auto instance : _instances) {
+ instance->add_highlight_header();
+ }
+}
+
+/**
+ * Signal handler to update dialog list when adding a page.
+ */
+void DialogNotebook::on_page_added(Gtk::Widget *page, int page_num)
+{
+ DialogBase *dialog = dynamic_cast<DialogBase *>(page);
+
+ // Does current container/window already have such a dialog?
+ if (dialog && _container->has_dialog_of_type(dialog)) {
+ // We already have a dialog of the same type
+
+ // Highlight first dialog
+ DialogBase *other_dialog = _container->get_dialog(dialog->get_type());
+ other_dialog->blink();
+
+ // Remove page from notebook
+ _detaching_duplicate = true; // HACK: prevent removing the initial dialog of the same type
+ _notebook.detach_tab(*page);
+ return;
+ } else if (dialog) {
+ // We don't have a dialog of this type
+
+ // Add to dialog list
+ _container->link_dialog(dialog);
+ } else {
+ // This is not a dialog
+ return;
+ }
+
+ // add close tab signal
+ add_close_tab_callback(page);
+
+ // Switch tab labels if needed
+ if (!_labels_auto) {
+ toggle_tab_labels_callback(false);
+ }
+
+ // Update tab labels by comparing the sum of their widths to the allocation
+ Gtk::Allocation allocation = get_allocation();
+ on_size_allocate_scroll(allocation);
+}
+
+/**
+ * Signal handler to update dialog list when removing a page.
+ */
+void DialogNotebook::on_page_removed(Gtk::Widget *page, int page_num)
+{
+ /**
+ * When adding a dialog in a notebooks header zone of the same type as an existing one,
+ * we remove it immediately, which triggers a call to this method. We use `_detaching_duplicate`
+ * to prevent reemoving the initial dialog.
+ */
+ if (_detaching_duplicate) {
+ _detaching_duplicate = false;
+ return;
+ }
+
+ // Remove from dialog list
+ DialogBase *dialog = dynamic_cast<DialogBase *>(page);
+ if (dialog) {
+ _container->unlink_dialog(dialog);
+ }
+
+ // remove old close tab signal
+ remove_close_tab_callback(page);
+}
+
+/**
+ * We need to remove the scrollbar to snap a whole DialogNotebook to width 0.
+ *
+ */
+void DialogNotebook::on_size_allocate_scroll(Gtk::Allocation &a)
+{
+ // magic number
+ const int MIN_HEIGHT = 60;
+ // set or unset scrollbars to completely hide a notebook
+ // because we have a "blocking" scroll per tab we need to loop to aboid
+ // other page stop out scroll
+ for (auto const &page : _notebook.get_children()) {
+ auto *container = dynamic_cast<Gtk::Container *>(page);
+ if (container && !provide_scroll(*page)) {
+ std::vector<Gtk::Widget *> widgs = container->get_children();
+ if (widgs.size()) {
+ auto scrolledwindow = dynamic_cast<Gtk::ScrolledWindow *>(widgs[0]);
+ if (scrolledwindow) {
+ double height = scrolledwindow->get_allocation().get_height();
+ if (height > 1) {
+ Gtk::PolicyType policy = scrolledwindow->property_vscrollbar_policy().get_value();
+ if (height >= MIN_HEIGHT && policy != Gtk::POLICY_AUTOMATIC) {
+ scrolledwindow->property_vscrollbar_policy().set_value(Gtk::POLICY_AUTOMATIC);
+ } else if(height < MIN_HEIGHT && policy != Gtk::POLICY_EXTERNAL) {
+ scrolledwindow->property_vscrollbar_policy().set_value(Gtk::POLICY_EXTERNAL);
+ } else {
+ // we don't need to update; break
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+
+ set_allocation(a);
+ // only update notebook tabs on horizontal changes
+ if (a.get_width() != _prev_alloc_width) {
+ on_size_allocate_notebook(a);
+ }
+}
+
+/**
+ * This function hides the tab labels if necessary (and _labels_auto == true)
+ */
+void DialogNotebook::on_size_allocate_notebook(Gtk::Allocation &a)
+{
+
+ // we unset scrollable when FULL mode on to prevent overflow with
+ // container at full size that makes an unmaximized desktop freeze
+ _notebook.set_scrollable(false);
+ if (!_labels_set_off && !_labels_auto) {
+ toggle_tab_labels_callback(false);
+ }
+ if (!_labels_auto) {
+ return;
+ }
+
+ int alloc_width = get_allocation().get_width();
+ // Don't update on closed dialog container, prevent console errors
+ if (alloc_width < 2) {
+ _notebook.set_scrollable(true);
+ return;
+ }
+ int nat_width = 0;
+ int initial_width = 0;
+ int total_width = 0;
+ _notebook.get_preferred_width(initial_width, nat_width); // get current notebook allocation
+ for (auto const &page : _notebook.get_children()) {
+ Gtk::EventBox *cover = dynamic_cast<Gtk::EventBox *>(_notebook.get_tab_label(*page));
+ if (!cover) {
+ continue;
+ }
+ cover->show_all();
+ }
+ _notebook.get_preferred_width(total_width, nat_width); // get full notebook allocation (all open)
+ prev_tabstatus = tabstatus;
+ if (_single_tab_width != _none_tab_width &&
+ ((_none_tab_width && _none_tab_width > alloc_width) ||
+ (_single_tab_width > alloc_width && _single_tab_width < total_width)))
+ {
+ tabstatus = TabsStatus::NONE;
+ if (_single_tab_width != initial_width || prev_tabstatus == TabsStatus::NONE) {
+ _none_tab_width = initial_width;
+ }
+ } else {
+ tabstatus = (alloc_width <= total_width) ? TabsStatus::SINGLE : TabsStatus::ALL;
+ if (total_width != initial_width &&
+ prev_tabstatus == TabsStatus::SINGLE &&
+ tabstatus == TabsStatus::SINGLE)
+ {
+ _single_tab_width = initial_width;
+ }
+ }
+ if ((_single_tab_width && !_none_tab_width) ||
+ (_single_tab_width && _single_tab_width == _none_tab_width))
+ {
+ _none_tab_width = _single_tab_width - 1;
+ }
+
+ /*
+ std::cout << "::::::::::tabstatus::" << (int)tabstatus << std::endl;
+ std::cout << ":::::prev_tabstatus::" << (int)prev_tabstatus << std::endl;
+ std::cout << "::::::::alloc_width::" << alloc_width << std::endl;
+ std::cout << "::_prev_alloc_width::" << _prev_alloc_width << std::endl;
+ std::cout << "::::::initial_width::" << initial_width << std::endl;
+ std::cout << "::::::::::nat_width::" << nat_width << std::endl;
+ std::cout << "::::::::total_width::" << total_width << std::endl;
+ std::cout << "::::_none_tab_width::" << _none_tab_width << std::endl;
+ std::cout << "::_single_tab_width::" << _single_tab_width << std::endl;
+ std::cout << ":::::::::::::::::::::" << std::endl;
+ */
+
+ _prev_alloc_width = alloc_width;
+ bool show = tabstatus == TabsStatus::ALL;
+ toggle_tab_labels_callback(show);
+}
+
+/**
+ * Signal handler to close a tab when middle-clicking.
+ */
+bool DialogNotebook::on_tab_click_event(GdkEventButton *event, Gtk::Widget *page)
+{
+ if (event->type == GDK_BUTTON_PRESS) {
+ if (event->button == 2) { // Close tab
+ _selected_page = page;
+ close_tab_callback();
+ } else if (event->button == 3) { // Show menu
+ _selected_page = page;
+ reload_tab_menu();
+ _menutabs.popup_at_pointer((GdkEvent *)event);
+ }
+ }
+
+ return false;
+}
+
+void DialogNotebook::on_close_button_click_event(Gtk::Widget *page)
+{
+ _selected_page = page;
+ close_tab_callback();
+}
+
+// ================== Helpers ===================
+
+/**
+ * Reload tab menu
+ */
+void DialogNotebook::reload_tab_menu()
+{
+ if (_reload_context) {
+ _reload_context = false;
+ Gtk::MenuItem* menuitem = nullptr;
+ for_each(_connmenu.begin(), _connmenu.end(), [&](auto c) { c.disconnect(); });
+ _connmenu.clear();
+ for (auto widget : _menutabs.get_children()) {
+ delete widget;
+ }
+ auto prefs = Inkscape::Preferences::get();
+ bool symbolic = false;
+ if (prefs->getBool("/theme/symbolicIcons", false)) {
+ symbolic = true;
+ }
+
+ for (auto const &page : _notebook.get_children()) {
+ Gtk::EventBox *cover = dynamic_cast<Gtk::EventBox *>(_notebook.get_tab_label(*page));
+ if (!cover) {
+ continue;
+ }
+
+ Gtk::Box *box = dynamic_cast<Gtk::Box *>(cover->get_child());
+
+ if (!box) {
+ continue;
+ }
+ auto childs = box->get_children();
+ if (childs.size() < 2) {
+ continue;
+ }
+ // Create a box to hold icon and label as Gtk::MenuItem derives from GtkBin and can
+ // only hold one child
+ Gtk::Box *boxmenu = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
+ boxmenu->set_halign(Gtk::ALIGN_START);
+ menuitem = Gtk::manage(new Gtk::MenuItem());
+ menuitem->add(*boxmenu);
+
+ Gtk::Label *label = dynamic_cast<Gtk::Label *>(childs[1]);
+ Gtk::Label *labelto = Gtk::manage(new Gtk::Label(label->get_text()));
+
+ Gtk::Image *icon = dynamic_cast<Gtk::Image *>(childs[0]);
+ if (icon) {
+ int min_width, nat_width;
+ icon->get_preferred_width(min_width, nat_width);
+ _icon_width = min_width;
+ auto name = icon->get_icon_name();
+ if (!name.empty()) {
+ if (symbolic && name.find("-symbolic") == Glib::ustring::npos) {
+ name += Glib::ustring("-symbolic");
+ }
+ Gtk::Image *iconend = sp_get_icon_image(name, Gtk::ICON_SIZE_MENU);
+ boxmenu->pack_start(*iconend, false, false, 0);
+ }
+ }
+ boxmenu->pack_start(*labelto, true, true, 0);
+ size_t pagenum = _notebook.page_num(*page);
+ _connmenu.emplace_back(
+ menuitem->signal_activate().connect(sigc::bind(sigc::mem_fun(*this, &DialogNotebook::change_page),pagenum)));
+
+ _menutabs.append(*Gtk::manage(menuitem));
+ }
+ }
+ _menutabs.show_all();
+}
+/**
+ * Callback to toggle all tab labels to the selected state.
+ * @param show: whether you want the labels to show or not
+ */
+void DialogNotebook::toggle_tab_labels_callback(bool show)
+{
+ _label_visible = show;
+ for (auto const &page : _notebook.get_children()) {
+ Gtk::EventBox *cover = dynamic_cast<Gtk::EventBox *>(_notebook.get_tab_label(*page));
+ if (!cover) {
+ continue;
+ }
+
+ Gtk::Box *box = dynamic_cast<Gtk::Box *>(cover->get_child());
+ if (!box) {
+ continue;
+ }
+
+ Gtk::Label *label = dynamic_cast<Gtk::Label *>(box->get_children()[1]);
+ Gtk::Button *close = dynamic_cast<Gtk::Button *>(*box->get_children().rbegin());
+ int n = _notebook.get_current_page();
+ if (close && label) {
+ if (page != _notebook.get_nth_page(n)) {
+ show ? close->show() : close->hide();
+ show ? label->show() : label->hide();
+ } else if (tabstatus == TabsStatus::NONE || _labels_off) {
+ if (page != _notebook.get_nth_page(n)) {
+ close->hide();
+ } else {
+ close->show();
+ }
+ label->hide();
+ } else {
+ close->show();
+ label->show();
+ }
+ }
+ }
+ _labels_set_off = _labels_off;
+ if (_prev_alloc_width && prev_tabstatus != tabstatus && (show || tabstatus != TabsStatus::NONE || !_labels_off)) {
+ resize_widget_children(&_notebook);
+ }
+ if (show && _single_tab_width) {
+ _notebook.set_scrollable(true);
+ }
+}
+
+void DialogNotebook::on_page_switch(Gtk::Widget *curr_page, guint)
+{
+ if (auto container = dynamic_cast<Gtk::Container *>(curr_page)) {
+ container->show_all_children();
+ }
+ for (auto const &page : _notebook.get_children()) {
+ auto dialogbase = dynamic_cast<DialogBase*>(page);
+ if (dialogbase) {
+ std::vector<Gtk::Widget *> widgs = dialogbase->get_children();
+ if (widgs.size()) {
+ if (curr_page == page) {
+ widgs[0]->show_now();
+ } else {
+ widgs[0]->hide();
+ }
+ }
+ if (_prev_alloc_width) {
+ dialogbase->setShowing(curr_page == page);
+ }
+ }
+ if (_label_visible) {
+ continue;
+ }
+ Gtk::EventBox *cover = dynamic_cast<Gtk::EventBox *>(_notebook.get_tab_label(*page));
+ if (!cover) {
+ continue;
+ }
+
+ if (cover == dynamic_cast<Gtk::EventBox *>(_notebook.get_tab_label(*curr_page))) {
+ Gtk::Box *box = dynamic_cast<Gtk::Box *>(cover->get_child());
+ Gtk::Label *label = dynamic_cast<Gtk::Label *>(box->get_children()[1]);
+ Gtk::Button *close = dynamic_cast<Gtk::Button *>(*box->get_children().rbegin());
+
+ if (label) {
+ if (tabstatus == TabsStatus::NONE) {
+ label->hide();
+ } else {
+ label->show();
+ }
+ }
+
+ if (close) {
+ if (tabstatus == TabsStatus::NONE && curr_page != page) {
+ close->hide();
+ } else {
+ close->show();
+ }
+ }
+ continue;
+ }
+
+ Gtk::Box *box = dynamic_cast<Gtk::Box *>(cover->get_child());
+ if (!box) {
+ continue;
+ }
+
+ Gtk::Label *label = dynamic_cast<Gtk::Label *>(box->get_children()[1]);
+ Gtk::Button *close = dynamic_cast<Gtk::Button *>(*box->get_children().rbegin());
+
+
+ close->hide();
+ label->hide();
+ }
+ if (_prev_alloc_width) {
+ if (!_label_visible) {
+ queue_allocate();
+ }
+ auto window = dynamic_cast<DialogWindow*>(_container->get_toplevel());
+ if (window) {
+ resize_widget_children(window->get_container());
+ } else {
+ if (auto desktop = SP_ACTIVE_DESKTOP) {
+ if (auto container = desktop->getContainer()) {
+ resize_widget_children(container);
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Helper method that change the page
+ */
+void DialogNotebook::change_page(size_t pagenum)
+{
+ _notebook.set_current_page(pagenum);
+}
+
+/**
+ * Helper method that adds the close tab signal connection for the page given.
+ */
+void DialogNotebook::add_close_tab_callback(Gtk::Widget *page)
+{
+ Gtk::Widget *tab = _notebook.get_tab_label(*page);
+ auto *eventbox = static_cast<Gtk::EventBox *>(tab);
+ auto *box = static_cast<Gtk::Box *>(*eventbox->get_children().begin());
+ auto children = box->get_children();
+ auto *close = static_cast<Gtk::Button *>(*children.crbegin());
+
+ sigc::connection close_connection = close->signal_clicked().connect(
+ sigc::bind<Gtk::Widget *>(sigc::mem_fun(*this, &DialogNotebook::on_close_button_click_event), page), true);
+
+ sigc::connection tab_connection = tab->signal_button_press_event().connect(
+ sigc::bind<Gtk::Widget *>(sigc::mem_fun(*this, &DialogNotebook::on_tab_click_event), page), true);
+
+ _tab_connections.insert(std::pair<Gtk::Widget *, sigc::connection>(page, tab_connection));
+ _tab_connections.insert(std::pair<Gtk::Widget *, sigc::connection>(page, close_connection));
+}
+
+/**
+ * Helper method that removes the close tab signal connection for the page given.
+ */
+void DialogNotebook::remove_close_tab_callback(Gtk::Widget *page)
+{
+ auto tab_connection_it = _tab_connections.find(page);
+
+ while (tab_connection_it != _tab_connections.end()) {
+ (*tab_connection_it).second.disconnect();
+ _tab_connections.erase(tab_connection_it);
+ tab_connection_it = _tab_connections.find(page);
+ }
+}
+
+void DialogNotebook::get_preferred_height_for_width_vfunc(int width, int& minimum_height, int& natural_height) const {
+ Gtk::ScrolledWindow::get_preferred_height_for_width_vfunc(width, minimum_height, natural_height);
+ if (_natural_height > 0) {
+ natural_height = _natural_height;
+ if (minimum_height > _natural_height) {
+ minimum_height = _natural_height;
+ }
+ }
+}
+
+void DialogNotebook::get_preferred_height_vfunc(int& minimum_height, int& natural_height) const {
+ Gtk::ScrolledWindow::get_preferred_height_vfunc(minimum_height, natural_height);
+ if (_natural_height > 0) {
+ natural_height = _natural_height;
+ if (minimum_height > _natural_height) {
+ minimum_height = _natural_height;
+ }
+ }
+}
+
+void DialogNotebook::set_requested_height(int height) {
+ _natural_height = height;
+}
+
+} // 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-notebook.h b/src/ui/dialog/dialog-notebook.h
new file mode 100644
index 0000000..a4505a6
--- /dev/null
+++ b/src/ui/dialog/dialog-notebook.h
@@ -0,0 +1,128 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifndef INKSCAPE_UI_DIALOG_NOTEBOOK_H
+#define INKSCAPE_UI_DIALOG_NOTEBOOK_H
+
+/** @file
+ * @brief A wrapper for Gtk::Notebook.
+ *
+ * Authors: see git history
+ * Tavmjong Bah
+ *
+ * Copyright (c) 2018 Tavmjong Bah, Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gdkmm/dragcontext.h>
+#include <gtkmm/menu.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/radiomenuitem.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/widget.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+enum class TabsStatus {
+ NONE,
+ SINGLE,
+ ALL
+};
+
+class DialogContainer;
+class DialogWindow;
+
+/**
+ * A widget that wraps a Gtk::Notebook with dialogs as pages.
+ *
+ * A notebook is fixed to a specific DialogContainer which manages the dialogs inside the notebook.
+ */
+class DialogNotebook : public Gtk::ScrolledWindow
+{
+public:
+ DialogNotebook(DialogContainer *container);
+ ~DialogNotebook() override;
+
+ void add_page(Gtk::Widget &page, Gtk::Widget &tab, Glib::ustring label);
+ void move_page(Gtk::Widget &page);
+
+ // Getters
+ Gtk::Notebook *get_notebook() { return &_notebook; }
+ DialogContainer *get_container() { return _container; }
+
+ // Notebook callbacks
+ void close_tab_callback();
+ void close_notebook_callback();
+ DialogWindow* pop_tab_callback();
+ Gtk::ScrolledWindow * get_current_scrolledwindow(bool skip_scroll_provider);
+ void set_requested_height(int height);
+private:
+ // Widgets
+ DialogContainer *_container;
+ Gtk::Menu _menu;
+ Gtk::Menu _menutabs;
+ Gtk::Notebook _notebook;
+
+ // State variables
+ bool _label_visible;
+ bool _labels_auto;
+ bool _labels_off;
+ bool _labels_set_off = false;
+ bool _detaching_duplicate;
+ bool _reload_context = true;
+ gint _prev_alloc_width = 0;
+ gint _none_tab_width = 0;
+ gint _single_tab_width = 0;
+ gint _icon_width = 0;
+ TabsStatus tabstatus = TabsStatus::NONE;
+ TabsStatus prev_tabstatus = TabsStatus::NONE;
+ Gtk::Widget *_selected_page;
+ std::vector<sigc::connection> _conn;
+ std::vector<sigc::connection> _connmenu;
+ std::multimap<Gtk::Widget *, sigc::connection> _tab_connections;
+
+ static std::list<DialogNotebook *> _instances;
+ void add_highlight_header();
+ void remove_highlight_header();
+
+ // Signal handlers - notebook
+ void on_drag_begin(const Glib::RefPtr<Gdk::DragContext> &context) override;
+ void on_drag_end(const Glib::RefPtr<Gdk::DragContext> &context) override;
+ void on_page_added(Gtk::Widget *page, int page_num);
+ void on_page_removed(Gtk::Widget *page, int page_num);
+ void on_size_allocate_scroll(Gtk::Allocation &allocation);
+ void on_size_allocate_notebook(Gtk::Allocation &allocation);
+ bool on_tab_click_event(GdkEventButton *event, Gtk::Widget *page);
+ void on_close_button_click_event(Gtk::Widget *page);
+ void on_page_switch(Gtk::Widget *page, guint page_number);
+ // Helpers
+ bool provide_scroll(Gtk::Widget &page);
+ void preventOverflow();
+ void change_page(size_t pagenum);
+ void reload_tab_menu();
+ void toggle_tab_labels_callback(bool show);
+ void add_close_tab_callback(Gtk::Widget *page);
+ void remove_close_tab_callback(Gtk::Widget *page);
+ void get_preferred_height_for_width_vfunc(int width, int& minimum_height, int& natural_height) const override;
+ void get_preferred_height_vfunc(int& minimum_height, int& natural_height) const override;
+ int _natural_height = 0;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_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/dialog/dialog-window.cpp b/src/ui/dialog/dialog-window.cpp
new file mode 100644
index 0000000..9ec49a7
--- /dev/null
+++ b/src/ui/dialog/dialog-window.cpp
@@ -0,0 +1,281 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/** @file
+ * @brief A window for floating dialogs.
+ *
+ * Authors: see git history
+ * Tavmjong Bah
+ *
+ * Copyright (c) 2018 Tavmjong Bah, Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/dialog/dialog-window.h"
+
+#include <glibmm/i18n.h>
+#include <gtkmm/application.h>
+#include <gtkmm/box.h>
+#include <gtkmm/label.h>
+#include <iostream>
+
+#include "enums.h"
+#include "inkscape-application.h"
+#include "inkscape-window.h"
+#include "inkscape.h"
+#include "preferences.h"
+#include "ui/dialog/dialog-base.h"
+#include "ui/dialog/dialog-container.h"
+#include "ui/dialog/dialog-manager.h"
+#include "ui/dialog/dialog-multipaned.h"
+#include "ui/dialog/dialog-notebook.h"
+#include "ui/shortcuts.h"
+#include "ui/util.h"
+
+// Sizing constants
+const int MINIMUM_WINDOW_WIDTH = 210;
+const int MINIMUM_WINDOW_HEIGHT = 320;
+const int INITIAL_WINDOW_WIDTH = 360;
+const int INITIAL_WINDOW_HEIGHT = 520;
+const int WINDOW_DROPZONE_SIZE = 10;
+const int WINDOW_DROPZONE_SIZE_LARGE = 16;
+const int NOTEBOOK_TAB_HEIGHT = 36;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class DialogNotebook;
+class DialogContainer;
+
+DialogWindow::~DialogWindow() {}
+
+// Create a dialog window and move page from old notebook.
+DialogWindow::DialogWindow(InkscapeWindow *inkscape_window, Gtk::Widget *page)
+ : Gtk::Window()
+ , _app(InkscapeApplication::instance())
+ , _inkscape_window(inkscape_window)
+ , _title(_("Dialog Window"))
+{
+ g_assert(_app != nullptr);
+ g_assert(_inkscape_window != nullptr);
+
+ // ============ Initialization ===============
+ // Setting the window type
+ set_type_hint(Gdk::WINDOW_TYPE_HINT_DIALOG);
+ set_transient_for(*inkscape_window);
+
+ // Add the dialog window to our app
+ _app->gtk_app()->add_window(*this);
+
+ this->signal_delete_event().connect([=](GdkEventAny *) {
+ DialogManager::singleton().store_state(*this);
+ delete this;
+ return true;
+ });
+
+ auto win_action_group = dynamic_cast<Gio::ActionGroup *>(inkscape_window);
+ if (win_action_group) {
+ // Must use C API as C++ API takes a RefPtr which we can't get (easily).
+ gtk_widget_insert_action_group(GTK_WIDGET(this->gobj()), "win", win_action_group->gobj());
+ } else {
+ std::cerr << "DialogWindow::DialogWindow: Can't find InkscapeWindow Gio:ActionGroup!" << std::endl;
+ }
+
+ insert_action_group("doc", inkscape_window->get_document()->getActionGroup());
+
+ // ============ Theming: icons ==============
+
+
+ // ================ Window ==================
+ set_title(_title);
+ set_name(_title);
+ int window_width = INITIAL_WINDOW_WIDTH;
+ int window_height = INITIAL_WINDOW_HEIGHT;
+
+ // =============== Outer Box ================
+ Gtk::Box *box_outer = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
+ add(*box_outer);
+
+ // =============== Container ================
+ _container = Gtk::manage(new DialogContainer(inkscape_window));
+ DialogMultipaned *columns = _container->get_columns();
+ auto drop_size = Inkscape::Preferences::get()->getBool("/options/dockingzone/value", true) ? WINDOW_DROPZONE_SIZE / 2 : WINDOW_DROPZONE_SIZE;
+ columns->set_dropzone_sizes(drop_size, drop_size);
+ box_outer->pack_end(*_container);
+
+ // If there is no page, create an empty Dialogwindow to be populated later
+ if (page) {
+ // ============= Initial Column =============
+ DialogMultipaned *column = _container->create_column();
+ columns->append(column);
+
+ // ============== New Notebook ==============
+ DialogNotebook *dialog_notebook = Gtk::manage(new DialogNotebook(_container));
+ column->append(dialog_notebook);
+ column->set_dropzone_sizes(drop_size, drop_size);
+ dialog_notebook->move_page(*page);
+
+ // Set window title
+ DialogBase *dialog = dynamic_cast<DialogBase *>(page);
+ if (dialog) {
+ _title = dialog->get_name();
+ set_title(_title);
+ }
+
+ // Set window size considering what the dialog needs
+ Gtk::Requisition minimum_size, natural_size;
+ dialog->get_preferred_size(minimum_size, natural_size);
+ int overhead = 2 * (drop_size + dialog->property_margin().get_value());
+ int width = natural_size.width + overhead;
+ int height = natural_size.height + overhead + NOTEBOOK_TAB_HEIGHT;
+ window_width = std::max(width, window_width);
+ window_height = std::max(height, window_height);
+ }
+
+ // Set window sizing
+ set_size_request(MINIMUM_WINDOW_WIDTH, MINIMUM_WINDOW_HEIGHT);
+ set_default_size(window_width, window_height);
+
+ if (page) {
+ update_dialogs();
+ }
+
+ // window is created hidden; don't show it now, its size needs to be restored
+}
+
+/**
+ * Change InkscapeWindow that DialogWindow is linked to.
+ */
+void DialogWindow::set_inkscape_window(InkscapeWindow* inkscape_window)
+{
+ if (!inkscape_window) {
+ std::cerr << "DialogWindow::set_inkscape_window: no inkscape_window!" << std::endl;
+ return;
+ }
+
+ _inkscape_window = inkscape_window;
+ update_dialogs();
+}
+
+/**
+ * Update all dialogs that are owned by the DialogWindow's _container.
+ */
+void DialogWindow::update_dialogs()
+{
+ g_assert(_app != nullptr);
+ g_assert(_container != nullptr);
+ g_assert(_inkscape_window != nullptr);
+
+ _container->set_inkscape_window(_inkscape_window);
+ _container->update_dialogs(); // Updating dialogs is not using the _app reference here.
+
+ // Set window title.
+ const std::multimap<Glib::ustring, DialogBase *> *dialogs = _container->get_dialogs();
+ if (dialogs->size() > 1) {
+ _title = "Multiple dialogs";
+ } else if (dialogs->size() == 1) {
+ _title = dialogs->begin()->second->get_name();
+ } else {
+ // Should not happen... but does on closing a window!
+ // std::cerr << "DialogWindow::update_dialogs(): No dialogs!" << std::endl;
+ _title = "";
+ }
+
+ auto document_name = _inkscape_window->get_document()->getDocumentName();
+ if (document_name) {
+ set_title(_title + " - " + Glib::ustring(document_name));
+ }
+}
+
+/**
+ * Update window width and height in order to fit all dialogs inisde its container.
+ *
+ * The intended use of this function is at initialization.
+ */
+void DialogWindow::update_window_size_to_fit_children()
+{
+ // Declare variables
+ int pos_x = 0, pos_y = 0;
+ int width = 0, height = 0;
+ int overhead = 0, baseline;
+ Gtk::Allocation allocation;
+ Gtk::Requisition minimum_size, natural_size;
+
+ // Read needed data
+ get_position(pos_x, pos_y);
+ get_allocated_size(allocation, baseline);
+ const std::multimap<Glib::ustring, DialogBase *> *dialogs = _container->get_dialogs();
+
+ // Get largest sizes for dialogs
+ for (auto dialog : *dialogs) {
+ dialog.second->get_preferred_size(minimum_size, natural_size);
+ width = std::max(natural_size.width, width);
+ height = std::max(natural_size.height, height);
+ overhead = std::max(overhead, dialog.second->property_margin().get_value());
+ }
+
+ // Compute sizes including overhead
+ overhead = 2 * (WINDOW_DROPZONE_SIZE_LARGE + overhead);
+ width = width + overhead;
+ height = height + overhead + NOTEBOOK_TAB_HEIGHT;
+
+ // If sizes are lower then current, don't change them
+ if (allocation.get_width() >= width && allocation.get_height() >= height) {
+ return;
+ }
+
+ // Compute largest sizes on both axis
+ width = std::max(width, allocation.get_width());
+ height = std::max(height, allocation.get_height());
+
+ // Compute new positions to keep window centered
+ pos_x = pos_x - (width - allocation.get_width()) / 2;
+ pos_y = pos_y - (height - allocation.get_height()) / 2;
+
+ // Keep window inside the screen
+ pos_x = std::max(pos_x, 0);
+ pos_y = std::max(pos_y, 0);
+
+ // Resize window
+ move(pos_x, pos_y);
+ resize(width, height);
+}
+
+// mimic InkscapeWindow handling of shortcuts to make them work with active floating dialog window
+bool DialogWindow::on_key_press_event(GdkEventKey *key_event)
+{
+ auto focus = get_focus();
+ if (focus) {
+ if (focus->event(reinterpret_cast<GdkEvent *>(key_event))) {
+ return true;
+ }
+ }
+
+ // Pass key event to this window or to app (via this window).
+ if (Gtk::Window::on_key_press_event(key_event)) {
+ return true;
+ }
+
+ // Pass key event to active InkscapeWindow to handle win (and app) level shortcuts.
+ if (auto win = _app->get_active_window(); win && win->on_key_press_event(key_event)) {
+ return true;
+ }
+
+ return 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/dialog/dialog-window.h b/src/ui/dialog/dialog-window.h
new file mode 100644
index 0000000..56d9247
--- /dev/null
+++ b/src/ui/dialog/dialog-window.h
@@ -0,0 +1,78 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifndef INKSCAPE_UI_DIALOG_WINDOW_H
+#define INKSCAPE_UI_DIALOG_WINDOW_H
+
+/** @file
+ * @brief A window for floating docks.
+ *
+ * Authors: see git history
+ * Tavmjong Bah
+ *
+ * Copyright (c) 2018 Tavmjong Bah, Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/refptr.h>
+#include <glibmm/ustring.h>
+#include <gtkmm/applicationwindow.h>
+
+#include "inkscape-application.h"
+
+using Gtk::Label;
+using Gtk::Widget;
+
+class InkscapeWindow;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class DialogContainer;
+class DialogMultipaned;
+
+/**
+ * DialogWindow holds DialogContainer instances for undocked dialogs.
+ *
+ * It watches the last active InkscapeWindow and updates its inner dialogs, if any.
+ */
+class DialogWindow : public Gtk::Window
+{
+public:
+ DialogWindow(InkscapeWindow* window, Gtk::Widget *page = nullptr);
+ ~DialogWindow() override;
+
+ void set_inkscape_window(InkscapeWindow *window);
+ InkscapeWindow* get_inkscape_window() { return _inkscape_window; }
+ void update_dialogs();
+ void update_window_size_to_fit_children();
+
+ // Getters
+ DialogContainer *get_container() { return _container; }
+
+private:
+ bool on_key_press_event(GdkEventKey* key_event) override;
+
+ InkscapeApplication *_app = nullptr;
+ InkscapeWindow *_inkscape_window = nullptr; // The Inkscape window that dialog window is attached to, changes when mouse moves into new Inkscape window.
+ DialogContainer *_container = nullptr;
+ Glib::ustring _title;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_WINDOW_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..5bf5976
--- /dev/null
+++ b/src/ui/dialog/document-properties.cpp
@@ -0,0 +1,1863 @@
+// 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 "actions/actions-tools.h"
+#include "document-properties.h"
+#include "include/gtkmm_version.h"
+#include "io/sys.h"
+#include "object/color-profile.h"
+#include "object/sp-root.h"
+#include "object/sp-grid.h"
+#include "object/sp-script.h"
+#include "page-manager.h"
+#include "rdf.h"
+#include "style.h"
+#include "svg/svg-color.h"
+#include "ui/dialog/filedialog.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "ui/shape-editor.h"
+#include "ui/widget/alignment-selector.h"
+#include "ui/widget/entity-entry.h"
+#include "ui/widget/notebook-page.h"
+#include "ui/widget/page-properties.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+#define SPACE_SIZE_X 15
+#define SPACE_SIZE_Y 10
+
+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()
+ : DialogBase("/dialogs/documentoptions", "DocumentProperties")
+ , _page_page(Gtk::manage(new UI::Widget::NotebookPage(1, 1, false, true)))
+ , _page_guides(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)))
+ //---------------------------------------------------------------
+ // General guide 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 current page"))
+ , _delete_guides_btn(_("Delete all guides"))
+ //---------------------------------------------------------------
+ , _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)
+ , _grids_vbox(Gtk::ORIENTATION_VERTICAL)
+ , _grids_hbox_crea(Gtk::ORIENTATION_HORIZONTAL)
+ , _grids_space(Gtk::ORIENTATION_HORIZONTAL)
+ // Attach nodeobservers to this document
+ , _namedview_connection(this)
+ , _root_connection(this)
+{
+ set_spacing (0);
+ pack_start(_notebook, true, true);
+
+ _notebook.append_page(*_page_page, _("Display"));
+ _notebook.append_page(*_page_guides, _("Guides"));
+ _notebook.append_page(_grids_vbox, _("Grids"));
+ _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_cms();
+ 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));
+
+ show_all_children();
+ _grids_button_remove.hide();
+}
+
+DocumentProperties::~DocumentProperties()
+{
+ 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;
+ 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 set_namedview_bool(SPDesktop* desktop, const Glib::ustring& operation, SPAttr key, bool on) {
+ if (!desktop || !desktop->getDocument()) return;
+
+ desktop->getNamedView()->change_bool_setting(key, on);
+
+ desktop->getDocument()->setModifiedSinceSave();
+ DocumentUndo::done(desktop->getDocument(), operation, "");
+}
+
+void set_color(SPDesktop* desktop, Glib::ustring operation, unsigned int rgba, SPAttr color_key, SPAttr opacity_key = SPAttr::INVALID) {
+ if (!desktop || !desktop->getDocument()) return;
+
+ desktop->getNamedView()->change_color(rgba, color_key, opacity_key);
+
+ desktop->getDocument()->setModifiedSinceSave();
+ DocumentUndo::maybeDone(desktop->getDocument(), ("document-color-" + operation).c_str(), operation, "");
+}
+
+void set_document_dimensions(SPDesktop* desktop, double width, double height, const Inkscape::Util::Unit* unit) {
+ if (!desktop) return;
+
+ Inkscape::Util::Quantity width_quantity = Inkscape::Util::Quantity(width, unit);
+ Inkscape::Util::Quantity height_quantity = Inkscape::Util::Quantity(height, unit);
+ SPDocument* doc = desktop->getDocument();
+ Inkscape::Util::Quantity const old_height = doc->getHeight();
+ auto rect = Geom::Rect(Geom::Point(0, 0), Geom::Point(width_quantity.value("px"), height_quantity.value("px")));
+ doc->fitToRect(rect, false);
+
+ // 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 (!doc->is_yaxisdown()) {
+ Geom::Translate const vert_offset(Geom::Point(0, (old_height.value("px") - height_quantity.value("px"))));
+ doc->getRoot()->translateChildItems(vert_offset);
+ }
+ // units: this is most likely not needed, units are part of document size attributes
+ // if (unit) {
+ // set_namedview_value(desktop, "", SPAttr::UNITS)
+ // write_str_to_xml(desktop, _("Set document unit"), "unit", unit->abbr.c_str());
+ // }
+ doc->setWidthAndHeight(width_quantity, height_quantity, true);
+
+ DocumentUndo::done(doc, _("Set page size"), "");
+}
+
+void DocumentProperties::set_viewbox_pos(SPDesktop* desktop, double x, double y) {
+ if (!desktop) return;
+
+ auto document = desktop->getDocument();
+ if (!document) return;
+
+ auto box = document->getViewBox();
+ document->setViewBox(Geom::Rect::from_xywh(x, y, box.width(), box.height()));
+ DocumentUndo::done(document, _("Set viewbox position"), "");
+ update_scale_ui(desktop);
+}
+
+void DocumentProperties::set_viewbox_size(SPDesktop* desktop, double width, double height) {
+ if (!desktop) return;
+
+ auto document = desktop->getDocument();
+ if (!document) return;
+
+ auto box = document->getViewBox();
+ document->setViewBox(Geom::Rect::from_xywh(box.min()[Geom::X], box.min()[Geom::Y], width, height));
+ DocumentUndo::done(document, _("Set viewbox size"), "");
+ update_scale_ui(desktop);
+}
+
+// helper function to set document scale; uses magnitude of document width/height only, not computed (pixel) values
+void set_document_scale_helper(SPDocument& document, double scale) {
+ if (scale <= 0) return;
+
+ auto root = document.getRoot();
+ auto box = document.getViewBox();
+ document.setViewBox(Geom::Rect::from_xywh(
+ box.min()[Geom::X], box.min()[Geom::Y],
+ root->width.value / scale, root->height.value / scale)
+ );
+}
+
+void DocumentProperties::set_document_scale(SPDesktop* desktop, double scale) {
+ if (!desktop) return;
+
+ auto document = desktop->getDocument();
+ if (!document) return;
+
+ if (scale > 0) {
+ set_document_scale_helper(*document, scale);
+ update_viewbox_ui(desktop);
+ update_scale_ui(desktop);
+ DocumentUndo::done(document, _("Set page scale"), "");
+ }
+}
+
+// document scale as a ratio of document size and viewbox size
+// as described in Wiki: https://wiki.inkscape.org/wiki/index.php/Units_In_Inkscape
+// for example: <svg width="100mm" height="100mm" viewBox="0 0 100 100"> will report 1:1 scale
+std::optional<Geom::Scale> get_document_scale_helper(SPDocument& doc) {
+ auto root = doc.getRoot();
+ if (root &&
+ root->width._set && root->width.unit != SVGLength::PERCENT &&
+ root->height._set && root->height.unit != SVGLength::PERCENT) {
+ if (root->viewBox_set) {
+ // viewbox and document size present
+ auto vw = root->viewBox.width();
+ auto vh = root->viewBox.height();
+ if (vw > 0 && vh > 0) {
+ return Geom::Scale(root->width.value / vw, root->height.value / vh);
+ }
+ } else {
+ // no viewbox, use SVG size in pixels
+ auto w = root->width.computed;
+ auto h = root->height.computed;
+ if (w > 0 && h > 0) {
+ return Geom::Scale(root->width.value / w, root->height.value / h);
+ }
+ }
+ }
+
+ // there is no scale concept applicable in the current state
+ return std::optional<Geom::Scale>();
+}
+
+void DocumentProperties::update_scale_ui(SPDesktop* desktop) {
+ if (!desktop) return;
+
+ auto document = desktop->getDocument();
+ if (!document) return;
+
+ using UI::Widget::PageProperties;
+ if (auto scale = get_document_scale_helper(*document)) {
+ auto sx = (*scale)[Geom::X];
+ auto sy = (*scale)[Geom::Y];
+ double eps = 0.0001; // TODO: tweak this value
+ bool uniform = fabs(sx - sy) < eps;
+ _page->set_dimension(PageProperties::Dimension::Scale, sx, sx); // only report one, only one "scale" is used
+ _page->set_check(PageProperties::Check::NonuniformScale, !uniform);
+ _page->set_check(PageProperties::Check::DisabledScale, false);
+ } else {
+ // no scale
+ _page->set_dimension(PageProperties::Dimension::Scale, 1, 1);
+ _page->set_check(PageProperties::Check::NonuniformScale, false);
+ _page->set_check(PageProperties::Check::DisabledScale, true);
+ }
+}
+
+void DocumentProperties::update_viewbox_ui(SPDesktop* desktop) {
+ if (!desktop) return;
+
+ auto document = desktop->getDocument();
+ if (!document) return;
+
+ using UI::Widget::PageProperties;
+ Geom::Rect viewBox = document->getViewBox();
+ _page->set_dimension(PageProperties::Dimension::ViewboxPosition, viewBox.min()[Geom::X], viewBox.min()[Geom::Y]);
+ _page->set_dimension(PageProperties::Dimension::ViewboxSize, viewBox.width(), viewBox.height());
+}
+
+void DocumentProperties::build_page()
+{
+ using UI::Widget::PageProperties;
+ _page = Gtk::manage(PageProperties::create());
+ _page_page->table().attach(*_page, 0, 0);
+ _page_page->show();
+
+ _page->signal_color_changed().connect([=](unsigned int color, PageProperties::Color element){
+ if (_wr.isUpdating() || !_wr.desktop()) return;
+
+ _wr.setUpdating(true);
+ switch (element) {
+ case PageProperties::Color::Desk:
+ set_color(_wr.desktop(), _("Desk color"), color, SPAttr::INKSCAPE_DESK_COLOR);
+ break;
+ case PageProperties::Color::Background:
+ set_color(_wr.desktop(), _("Background color"), color, SPAttr::PAGECOLOR);
+ break;
+ case PageProperties::Color::Border:
+ set_color(_wr.desktop(), _("Border color"), color, SPAttr::BORDERCOLOR, SPAttr::BORDEROPACITY);
+ break;
+ }
+ _wr.setUpdating(false);
+ });
+
+ _page->signal_dimmension_changed().connect([=](double x, double y, const Inkscape::Util::Unit* unit, PageProperties::Dimension element){
+ if (_wr.isUpdating() || !_wr.desktop()) return;
+
+ _wr.setUpdating(true);
+ switch (element) {
+ case PageProperties::Dimension::PageTemplate:
+ case PageProperties::Dimension::PageSize:
+ set_document_dimensions(_wr.desktop(), x, y, unit);
+ update_viewbox(_wr.desktop());
+ break;
+
+ case PageProperties::Dimension::ViewboxSize:
+ set_viewbox_size(_wr.desktop(), x, y);
+ break;
+
+ case PageProperties::Dimension::ViewboxPosition:
+ set_viewbox_pos(_wr.desktop(), x, y);
+ break;
+
+ case PageProperties::Dimension::Scale:
+ set_document_scale(_wr.desktop(), x); // only uniform scale; there's no 'y' in the dialog
+ }
+ _wr.setUpdating(false);
+ });
+
+ _page->signal_check_toggled().connect([=](bool checked, PageProperties::Check element){
+ if (_wr.isUpdating() || !_wr.desktop()) return;
+
+ _wr.setUpdating(true);
+ switch (element) {
+ case PageProperties::Check::Checkerboard:
+ set_namedview_bool(_wr.desktop(), _("Toggle checkerboard"), SPAttr::INKSCAPE_DESK_CHECKERBOARD, checked);
+ break;
+ case PageProperties::Check::Border:
+ set_namedview_bool(_wr.desktop(), _("Toggle page border"), SPAttr::SHOWBORDER, checked);
+ break;
+ case PageProperties::Check::BorderOnTop:
+ set_namedview_bool(_wr.desktop(), _("Toggle border on top"), SPAttr::BORDERLAYER, checked);
+ break;
+ case PageProperties::Check::Shadow:
+ set_namedview_bool(_wr.desktop(), _("Toggle page shadow"), SPAttr::SHOWPAGESHADOW, checked);
+ break;
+ case PageProperties::Check::AntiAlias:
+ set_namedview_bool(_wr.desktop(), _("Toggle anti-aliasing"), SPAttr::SHAPE_RENDERING, checked);
+ break;
+ case PageProperties::Check::ClipToPage:
+ set_namedview_bool(_wr.desktop(), _("Toggle clip to page mode"), SPAttr::INKSCAPE_CLIP_TO_PAGE_RENDERING, checked);
+ break;
+ case PageProperties::Check::PageLabelStyle:
+ set_namedview_bool(_wr.desktop(), _("Toggle page label style"), SPAttr::PAGELABELSTYLE, checked);
+ }
+ _wr.setUpdating(false);
+ });
+
+ _page->signal_unit_changed().connect([=](const Inkscape::Util::Unit* unit, PageProperties::Units element){
+ if (_wr.isUpdating() || !_wr.desktop()) return;
+
+ if (element == PageProperties::Units::Display) {
+ // display only units
+ display_unit_change(unit);
+ }
+ else if (element == PageProperties::Units::Document) {
+ // not used, fired with page size
+ }
+ });
+
+ _page->signal_resize_to_fit().connect([=](){
+ if (_wr.isUpdating() || !_wr.desktop()) return;
+
+ if (auto document = getDocument()) {
+ auto &page_manager = document->getPageManager();
+ page_manager.selectPage(0);
+ // fit page to selection or content, if there's no selection
+ page_manager.fitToSelection(_wr.desktop()->getSelection());
+ DocumentUndo::done(document, _("Resize page to fit"), INKSCAPE_ICON("tool-pages"));
+ update_widgets();
+ }
+ });
+}
+
+void DocumentProperties::build_guides()
+{
+ _page_guides->show();
+
+ Gtk::Label *label_gui = Gtk::manage (new Gtk::Label);
+ label_gui->set_markup (_("<b>Guides</b>"));
+
+ _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);
+
+ // Must use C API until GTK4.
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(_create_guides_btn.gobj()), "doc.create-guides-around-page");
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(_delete_guides_btn.gobj()), "doc.delete-all-guides");
+}
+
+/// 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;
+ }
+}
+
+
+/// 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)
+ if (auto document = getDocument()){
+ // Find the index of the currently-selected row in the color profiles combobox
+ Gtk::TreeModel::iterator iter = _AvailableProfilesList.get_active();
+ if (!iter)
+ 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 = 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 = document->getReprDoc();
+ Inkscape::XML::Node *cprofRepr = xml_doc->createElement("svg:color-profile");
+ gchar* tmp = g_strdup(name.c_str());
+ std::string nameStr = tmp ? tmp : "profile"; // TODO add some auto-numbering to avoid collisions
+ ColorProfile::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(document->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(document, _("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();
+ if (auto document = getDocument()) {
+ std::vector<SPObject *> current = document->getResourceList( "iccprofile" );
+ if (! current.empty()) {
+ _emb_profiles_observer.set((*(current.begin()))->parent);
+ }
+
+ std::set<Inkscape::ColorProfile *> _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;
+ }
+ }
+ if (auto document = getDocument()) {
+ std::vector<SPObject *> current = 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(document, _("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>"));
+
+ _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::Box* spacer = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
+ 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);
+
+ _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));
+ _AvailableProfilesList.signal_changed().connect( sigc::mem_fun(*this, &DocumentProperties::linkSelectedProfile) );
+
+ 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);
+
+ _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));
+
+ if (auto document = getDocument()) {
+ std::vector<SPObject *> current = 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();
+ }
+}
+
+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::Box* spacer_external = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
+ 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::Box* spacer_embedded = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
+ 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));
+
+ _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));
+
+//TODO: review this observers code:
+ if (auto document = getDocument()) {
+ std::vector<SPObject *> current = 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();
+ }
+}
+
+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(){
+
+ auto document = getDocument();
+ if (!document)
+ 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 = document->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(document, _("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 = getDesktop();
+ if (desktop && !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(){
+ if(auto document = getDocument()) {
+ Inkscape::XML::Document *xml_doc = document->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(document, _("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;
+ }
+ }
+
+ auto document = getDocument();
+ if (!document)
+ return;
+ std::vector<SPObject *> current = document->getResourceList( "script" );
+ for (auto obj : current) {
+ if (obj) {
+ auto script = 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(document, _("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;
+ }
+ }
+
+ if (auto document = getDocument()) {
+ if (auto obj = document->getObjectById(id)) {
+ //XML Tree being used directly here while it shouldn't be.
+ if (auto repr = obj->getRepr()){
+ sp_repr_unparent(repr);
+
+ // inform the document, so we can undo
+ DocumentUndo::done(document, _("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;
+ }
+ }
+
+ auto document = getDocument();
+ if (!document)
+ return;
+
+ bool voidscript=true;
+ std::vector<SPObject *> current = 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;
+ }
+ }
+
+ auto document = getDocument();
+ if (!document)
+ return;
+
+ for (auto obj : document->getResourceList("script")) {
+ 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(document->getReprDoc()->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(document, _("Edit embedded script"), "");
+ }
+ }
+ }
+}
+
+void DocumentProperties::populate_script_lists(){
+ _ExternalScriptsListStore->clear();
+ _EmbeddedScriptsListStore->clear();
+ auto document = getDocument();
+ if (!document)
+ return;
+
+ std::vector<SPObject *> current = getDocument()->getResourceList( "script" );
+ if (!current.empty()) {
+ SPObject *obj = *(current.begin());
+ g_assert(obj != nullptr);
+ _scripts_observer.set(obj->parent);
+ }
+ for (auto obj : current) {
+ auto script = 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. DO NOT call this a lot. It's expensive!
+* Will need to probably create a GridManager with signals to each Grid attribute
+*/
+void DocumentProperties::update_gridspage()
+{
+ SPNamedView *nv = getDesktop()->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->getRepr()->attribute("id")) continue; // update_gridspage is called again when "id" is added
+ Glib::ustring name(grid->getRepr()->attribute("id"));
+ const char *icon = grid->typeName();
+ _grids_notebook.append_page(*createNewGridWidget(grid), _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);
+ }
+}
+
+void *DocumentProperties::notifyGridWidgetsDestroyed(void *data)
+{
+ if (auto prop = reinterpret_cast<DocumentProperties *>(data)) {
+ prop->_grid_rcb_enabled = nullptr;
+ prop->_grid_rcb_snap_visible_only = nullptr;
+ prop->_grid_rcb_visible = nullptr;
+ prop->_grid_rcb_dotted = nullptr;
+ prop->_grid_as_alignment = nullptr;
+ }
+ return nullptr;
+}
+
+Gtk::Widget *DocumentProperties::createNewGridWidget(SPGrid *grid)
+{
+ auto vbox = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_VERTICAL);
+ auto namelabel = Gtk::make_managed<Gtk::Label>("", Gtk::ALIGN_CENTER);
+
+ Inkscape::XML::Node *repr = grid->getRepr();
+ auto doc = getDocument();
+
+ namelabel->set_markup(Glib::ustring("<b>") + grid->displayName() + "</b>");
+ vbox->pack_start(*namelabel, false, false);
+
+ _grid_rcb_enabled = Gtk::make_managed<Inkscape::UI::Widget::RegisteredCheckButton>(
+ _("_Enabled"),
+ _("Makes the grid available for working with on the canvas."),
+ "enabled", _wr, false, repr, doc);
+ // grid_rcb_enabled serves as a canary that tells us that the widgets have been destroyed
+ _grid_rcb_enabled->add_destroy_notify_callback(this, notifyGridWidgetsDestroyed);
+
+ _grid_rcb_snap_visible_only = Gtk::make_managed<Inkscape::UI::Widget::RegisteredCheckButton>(
+ _("Snap to visible _grid lines only"),
+ _("When zoomed out, not all grid lines will be displayed. Only the visible ones will be snapped to"),
+ "snapvisiblegridlinesonly", _wr, false, repr, doc);
+
+ _grid_rcb_visible = Gtk::make_managed<Inkscape::UI::Widget::RegisteredCheckButton>(
+ _("_Visible"),
+ _("Determines whether the grid is displayed or not. Objects are still snapped to invisible grids."),
+ "visible", _wr, false, repr, doc);
+
+ _grid_as_alignment = Gtk::make_managed<Inkscape::UI::Widget::AlignmentSelector>();
+ _grid_as_alignment->on_alignmentClicked().connect([this, grid](int align) {
+ auto doc = getDocument();
+ Geom::Point dimensions = doc->getDimensions();
+ dimensions[Geom::X] *= align % 3 * 0.5;
+ dimensions[Geom::Y] *= align / 3 * 0.5;
+ dimensions *= doc->doc2dt();
+ grid->setOrigin(dimensions);
+ });
+
+ auto left = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_VERTICAL, 4);
+ left->pack_start(*_grid_rcb_enabled, false, false);
+ left->pack_start(*_grid_rcb_visible, false, false);
+ left->pack_start(*_grid_rcb_snap_visible_only, false, false);
+
+ if (grid->getType() == GridType::RECTANGULAR) {
+ _grid_rcb_dotted = Gtk::make_managed<Inkscape::UI::Widget::RegisteredCheckButton>(
+ _("_Show dots instead of lines"), _("If set, displays dots at gridpoints instead of gridlines"),
+ "dotted", _wr, false, repr, doc );
+ left->pack_start(*_grid_rcb_dotted, false, false);
+ }
+
+ left->pack_start(*Gtk::make_managed<Gtk::Label>(_("Align to page:")), false, false);
+ left->pack_start(*_grid_as_alignment, false, false);
+
+ auto right = createRightGridColumn(grid);
+ right->set_hexpand(false);
+
+ auto inner = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL, 4);
+ inner->pack_start(*left, true, true);
+ inner->pack_start(*right, false, false);
+ vbox->pack_start(*inner, false, false);
+ vbox->set_border_width(4);
+
+ std::list<Gtk::Widget*> slaves;
+ for (auto &item : left->get_children()) {
+ if (item != _grid_rcb_enabled) {
+ slaves.push_back(item);
+ }
+ }
+ slaves.push_back(right);
+ _grid_rcb_enabled->setSlaveWidgets(slaves);
+
+ // set widget values
+ _wr.setUpdating (true);
+ _grid_rcb_enabled->setActive(grid->isEnabled());
+ _grid_rcb_visible->setActive(grid->isVisible());
+
+ if (_grid_rcb_dotted)
+ _grid_rcb_dotted->setActive(grid->isDotted());
+
+ _grid_rcb_snap_visible_only->setActive(grid->getSnapToVisibleOnly());
+ _grid_rcb_enabled->setActive(grid->snapper()->getEnabled());
+ _grid_rcb_snap_visible_only->setActive(grid->snapper()->getSnapVisibleOnly());
+ _wr.setUpdating (false);
+
+ return vbox;
+}
+
+// needs to switch based on grid type, need to find a better way
+Gtk::Widget *DocumentProperties::createRightGridColumn(SPGrid *grid)
+{
+ using namespace Inkscape::UI::Widget;
+ Inkscape::XML::Node *repr = grid->getRepr();
+ auto doc = getDocument();
+
+ auto rumg = Gtk::make_managed<RegisteredUnitMenu>(
+ _("Grid _units:"), "units", _wr, repr, doc);
+ auto rsu_ox = Gtk::make_managed<RegisteredScalarUnit>(
+ _("_Origin X:"), _("X coordinate of grid origin"), "originx",
+ *rumg, _wr, repr, doc, RSU_x);
+ auto rsu_oy = Gtk::make_managed<RegisteredScalarUnit>(
+ _("O_rigin Y:"), _("Y coordinate of grid origin"), "originy",
+ *rumg, _wr, repr, doc, RSU_y);
+ auto rsu_sx = Gtk::make_managed<RegisteredScalarUnit>(
+ _("Spacing _X:"), _("Distance between vertical grid lines"), "spacingx",
+ *rumg, _wr, repr, doc, RSU_x);
+ auto rsu_sy = Gtk::make_managed<RegisteredScalarUnit>(
+ _("Spacing _Y:"), _("Base length of z-axis"), "spacingy",
+ *rumg, _wr, repr, doc, RSU_y);
+ auto rsu_ax = Gtk::make_managed<RegisteredScalar>(
+ _("Angle X:"), _("Angle of x-axis"), "gridanglex", _wr, repr, doc);
+ auto rsu_az = Gtk::make_managed<RegisteredScalar>(
+ _("Angle Z:"), _("Angle of z-axis"), "gridanglez", _wr, repr, doc);
+ auto rcp_gcol = Gtk::make_managed<RegisteredColorPicker>(
+ _("Minor grid line _color:"), _("Minor grid line color"), _("Color of the minor grid lines"),
+ "color", "opacity", _wr, repr, doc);
+ auto rcp_gmcol = Gtk::make_managed<RegisteredColorPicker>(
+ _("Ma_jor grid line color:"), _("Major grid line color"),
+ _("Color of the major (highlighted) grid lines"),
+ "empcolor", "empopacity", _wr, repr, doc);
+ auto rsi = Gtk::make_managed<RegisteredSuffixedInteger>(
+ _("_Major grid line every:"), "", _("lines"), "empspacing", _wr, repr, doc);
+
+ rumg->set_hexpand();
+ rsu_ox->set_hexpand();
+ rsu_oy->set_hexpand();
+ rsu_sx->set_hexpand();
+ rsu_sy->set_hexpand();
+ rsu_ax->set_hexpand();
+ rsu_az->set_hexpand();
+ rcp_gcol->set_hexpand();
+ rcp_gmcol->set_hexpand();
+ rsi->set_hexpand();
+
+ // set widget values
+ _wr.setUpdating (true);
+
+ rsu_ox->setDigits(5);
+ rsu_ox->setIncrements(0.1, 1.0);
+
+ rsu_oy->setDigits(5);
+ rsu_oy->setIncrements(0.1, 1.0);
+
+ rsu_sx->setDigits(5);
+ rsu_sx->setIncrements(0.1, 1.0);
+
+ rsu_sy->setDigits(5);
+ rsu_sy->setIncrements(0.1, 1.0);
+
+ rumg->setUnit(grid->getUnit()->abbr);
+
+ // Doc to px so unit is conserved in RegisteredScalerUnit
+ auto origin = grid->getOrigin() * doc->getDocumentScale();
+ rsu_ox->setValueKeepUnit(origin[Geom::X], "px");
+ rsu_oy->setValueKeepUnit(origin[Geom::Y], "px");
+
+ auto spacing = grid->getSpacing() * doc->getDocumentScale();
+ rsu_sx->setValueKeepUnit(spacing[Geom::X], "px");
+ rsu_sy->setValueKeepUnit(spacing[Geom::Y], "px");
+
+ rsu_ax->setValue(grid->getAngleX());
+ rsu_az->setValue(grid->getAngleZ());
+
+ rcp_gcol->setRgba32 (grid->getMinorColor());
+ rcp_gmcol->setRgba32 (grid->getMajorColor());
+ rsi->setValue (grid->getMajorLineInterval());
+
+ _wr.setUpdating (false);
+
+ rsu_ox->setProgrammatically = false;
+ rsu_oy->setProgrammatically = false;
+
+ auto column = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_VERTICAL, 4);
+ column->pack_start(*rumg, true, false);
+ column->pack_start(*rsu_ox, true, false);
+ column->pack_start(*rsu_oy, true, false);
+
+ if (grid->getType() == GridType::RECTANGULAR) {
+ column->pack_start(*rsu_sx, true, false);
+ }
+
+ column->pack_start(*rsu_sy, true, false);
+
+ if (grid->getType() == GridType::AXONOMETRIC) {
+ column->pack_start(*rsu_ax, true, false);
+ column->pack_start(*rsu_az, true, false);
+ }
+
+ column->pack_start(*rcp_gcol, true, false);
+ column->pack_start(*rcp_gmcol, true, false);
+ column->pack_start(*rsi, true, false);
+
+ return column;
+}
+
+/**
+ * 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.
+
+ _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);
+
+ _grids_combo_gridtype.append( _("Rectangular Grid") );
+ _grids_combo_gridtype.append( _("Axonometric Grid") );
+
+ _grids_combo_gridtype.set_active_text( _("Rectangular Grid") );
+
+ _grids_space.set_size_request (SPACE_SIZE_X, SPACE_SIZE_Y);
+
+ _grids_vbox.set_name("NotebookPage");
+ _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);
+}
+
+void DocumentProperties::update_viewbox(SPDesktop* desktop) {
+ if (!desktop) return;
+
+ auto* document = desktop->getDocument();
+ if (!document) return;
+
+ using UI::Widget::PageProperties;
+ SPRoot* root = document->getRoot();
+ if (root->viewBox_set) {
+ auto& vb = root->viewBox;
+ _page->set_dimension(PageProperties::Dimension::ViewboxPosition, vb.min()[Geom::X], vb.min()[Geom::Y]);
+ _page->set_dimension(PageProperties::Dimension::ViewboxSize, vb.width(), vb.height());
+ }
+
+ update_scale_ui(desktop);
+}
+
+/**
+ * Update dialog widgets from desktop. Also call updateWidget routines of the grids.
+ */
+void DocumentProperties::update_widgets()
+{
+ auto desktop = getDesktop();
+ auto document = getDocument();
+ if (_wr.isUpdating() || !document) return;
+
+ auto nv = desktop->getNamedView();
+ auto &page_manager = document->getPageManager();
+
+ _wr.setUpdating(true);
+
+ SPRoot *root = document->getRoot();
+
+ double doc_w = root->width.value;
+ Glib::ustring doc_w_unit = unit_table.getUnit(root->width.unit)->abbr;
+ bool percent = doc_w_unit == "%";
+ if (doc_w_unit == "") {
+ doc_w_unit = "px";
+ } else if (doc_w_unit == "%" && root->viewBox_set) {
+ doc_w_unit = "px";
+ doc_w = root->viewBox.width();
+ }
+ double doc_h = root->height.value;
+ Glib::ustring doc_h_unit = unit_table.getUnit(root->height.unit)->abbr;
+ percent = percent || doc_h_unit == "%";
+ if (doc_h_unit == "") {
+ doc_h_unit = "px";
+ } else if (doc_h_unit == "%" && root->viewBox_set) {
+ doc_h_unit = "px";
+ doc_h = root->viewBox.height();
+ }
+ using UI::Widget::PageProperties;
+ // dialog's behavior is not entirely correct when document sizes are expressed in '%', so put up a disclaimer
+ _page->set_check(PageProperties::Check::UnsupportedSize, percent);
+
+ _page->set_dimension(PageProperties::Dimension::PageSize, doc_w, doc_h);
+ _page->set_unit(PageProperties::Units::Document, doc_w_unit);
+
+ update_viewbox_ui(desktop);
+ update_scale_ui(desktop);
+
+ if (nv->display_units) {
+ _page->set_unit(PageProperties::Units::Display, nv->display_units->abbr);
+ }
+ _page->set_check(PageProperties::Check::Checkerboard, nv->desk_checkerboard);
+ _page->set_color(PageProperties::Color::Desk, nv->desk_color);
+ _page->set_color(PageProperties::Color::Background, page_manager.background_color);
+ _page->set_check(PageProperties::Check::Border, page_manager.border_show);
+ _page->set_check(PageProperties::Check::BorderOnTop, page_manager.border_on_top);
+ _page->set_color(PageProperties::Color::Border, page_manager.border_color);
+ _page->set_check(PageProperties::Check::Shadow, page_manager.shadow_show);
+ _page->set_check(PageProperties::Check::PageLabelStyle, page_manager.label_style != "default");
+
+ _page->set_check(PageProperties::Check::AntiAlias, root->style->shape_rendering.computed != SP_CSS_SHAPE_RENDERING_CRISPEDGES);
+ _page->set_check(PageProperties::Check::ClipToPage, nv->clip_to_page);
+
+ //-----------------------------------------------------------guide page
+
+ _rcb_sgui.setActive (nv->getShowGuides());
+ _rcb_lgui.setActive (nv->getLockGuides());
+ _rcp_gui.setRgba32 (nv->guidecolor);
+ _rcp_hgui.setRgba32 (nv->guidehicolor);
+
+ //-----------------------------------------------------------grids page
+
+ update_gridspage();
+
+ //------------------------------------------------Color Management page
+
+ populate_linked_profiles_box();
+ populate_available_profiles();
+
+ //-----------------------------------------------------------meta pages
+ // update the RDF entities; note that this may modify document, maybe doc-undo should be called?
+ if (auto document = getDocument()) {
+ for (auto &it : _rdflist) {
+ bool read_only = false;
+ it->update(document, read_only);
+ }
+ _licensor.update(document);
+ }
+ _wr.setUpdating (false);
+}
+
+// TODO: copied from fill-and-stroke.cpp factor out into new ui/widget file?
+Gtk::Box&
+DocumentProperties::_createPageTabLabel(const Glib::ustring& label, const char *label_image)
+{
+ Gtk::Box *_tab_label_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 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_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*/
+ if (auto document = getDocument()) {
+ for (auto &it : _rdflist) {
+ it->save_to_preferences(document);
+ }
+ }
+}
+
+void DocumentProperties::WatchConnection::connect(Inkscape::XML::Node *node)
+{
+ disconnect();
+ if (!node) return;
+
+ _node = node;
+ _node->addObserver(*this);
+}
+
+void DocumentProperties::WatchConnection::disconnect() {
+ if (_node) {
+ _node->removeObserver(*this);
+ _node = nullptr;
+ }
+}
+
+void DocumentProperties::WatchConnection::notifyChildAdded(XML::Node&, XML::Node&, XML::Node*)
+{
+ _dialog->update_gridspage();
+}
+
+void DocumentProperties::WatchConnection::notifyChildRemoved(XML::Node&, XML::Node&, XML::Node*)
+{
+ _dialog->update_gridspage();
+}
+
+void DocumentProperties::WatchConnection::notifyAttributeChanged(XML::Node&, GQuark, Util::ptr_shared, Util::ptr_shared)
+{
+ _dialog->update_widgets();
+}
+
+void DocumentProperties::documentReplaced()
+{
+ _root_connection.disconnect();
+ _namedview_connection.disconnect();
+
+ if (auto desktop = getDesktop()) {
+ _wr.setDesktop(desktop);
+ _namedview_connection.connect(desktop->getNamedView()->getRepr());
+ if (auto document = desktop->getDocument()) {
+ _root_connection.connect(document->getRoot()->getRepr());
+ }
+ populate_linked_profiles_box();
+ update_widgets();
+ }
+}
+
+void DocumentProperties::update()
+{
+ update_widgets();
+}
+
+/*########################################################################
+# BUTTON CLICK HANDLERS (callbacks)
+########################################################################*/
+
+void DocumentProperties::onNewGrid()
+{
+ auto desktop = getDesktop();
+ auto document = getDocument();
+ if (!desktop || !document) return;
+
+ auto selected_grid_type = _grids_combo_gridtype.get_active_row_number();
+ GridType grid_type;
+ switch (selected_grid_type) {
+ case 0: grid_type = GridType::RECTANGULAR; break;
+ case 1: grid_type = GridType::AXONOMETRIC; break;
+ default: g_assert_not_reached(); return;
+ }
+
+ auto repr = desktop->getNamedView()->getRepr();
+ SPGrid::create_new(document, repr, grid_type);
+
+ // toggle grid showing to ON:
+ // side effect: any pre-existing grids set to invisible will be set to visible
+ desktop->getNamedView()->setShowGrids(true);
+ DocumentUndo::done(document, _("Create new grid"), INKSCAPE_ICON("document-properties"));
+}
+
+
+void DocumentProperties::onRemoveGrid()
+{
+ gint pagenum = _grids_notebook.get_current_page();
+ if (pagenum == -1) // no pages
+ return;
+
+ SPNamedView *nv = getDesktop()->getNamedView();
+ SPGrid *found_grid = nullptr;
+ if( pagenum < (gint)nv->grids.size())
+ found_grid = nv->grids[pagenum];
+
+ if (auto document = getDocument()) {
+ 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->getRepr()->parent()->removeChild(found_grid->getRepr());
+ DocumentUndo::done(document, _("Remove grid"), INKSCAPE_ICON("document-properties"));
+ }
+ }
+}
+
+/* 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::display_unit_change(const Inkscape::Util::Unit* doc_unit)
+{
+ SPDocument *document = getDocument();
+ // Don't execute when change is being undone
+ if (!document || !DocumentUndo::getUndoSensitive(document)) {
+ return;
+ }
+ // Don't execute when initializing widgets
+ if (_wr.isUpdating()) {
+ return;
+ }
+
+ auto action = document->getActionGroup()->lookup_action("set-display-unit");
+ action->activate(doc_unit->abbr);
+}
+
+} // 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..0240652
--- /dev/null
+++ b/src/ui/dialog/document-properties.h
@@ -0,0 +1,275 @@
+// 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 <gtkmm/buttonbox.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/textview.h>
+#include <sigc++/sigc++.h>
+
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/licensor.h"
+#include "ui/widget/registered-widget.h"
+#include "ui/widget/registry.h"
+#include "ui/widget/tolerance-slider.h"
+#include "xml/helper-observer.h"
+#include "xml/node-observer.h"
+
+namespace Inkscape {
+namespace XML { class Node; }
+namespace UI {
+
+namespace Widget {
+class AlignmentSelector;
+class EntityEntry;
+class NotebookPage;
+class PageProperties;
+} // namespace Widget
+
+namespace Dialog {
+
+using RDEList = std::vector<UI::Widget::EntityEntry *>;
+
+class DocumentProperties : public DialogBase
+{
+public:
+ DocumentProperties();
+ ~DocumentProperties() override;
+
+ void update_widgets();
+ static DocumentProperties &getInstance();
+ static void destroy();
+
+ void documentReplaced() override;
+
+ void update() override;
+ void update_gridspage();
+
+protected:
+ void build_page();
+ void build_grid();
+ void build_guides();
+ void build_snap();
+ void build_gridspage();
+
+ void build_cms();
+ void build_scripting();
+ void build_metadata();
+
+ virtual void on_response (int);
+ 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);
+
+ 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 update_viewbox(SPDesktop* desktop);
+ void update_scale_ui(SPDesktop* desktop);
+ void update_viewbox_ui(SPDesktop* desktop);
+ void set_document_scale(SPDesktop* desktop, double scale_x);
+ void set_viewbox_pos(SPDesktop* desktop, double x, double y);
+ void set_viewbox_size(SPDesktop* desktop, double width, double height);
+
+ 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_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::Box _grids_vbox;
+
+ UI::Widget::Registry _wr;
+ //---------------------------------------------------------------
+ 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::PageProperties* _page;
+ //---------------------------------------------------------------
+ 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::Box _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::Box _grids_space;
+ //---------------------------------------------------------------
+
+ RDEList _rdflist;
+ UI::Widget::Licensor _licensor;
+
+ Gtk::Box& _createPageTabLabel(const Glib::ustring& label, const char *label_image);
+
+private:
+ // callback methods for buttons on grids page.
+ void onNewGrid();
+ void onRemoveGrid();
+
+ // callback for display unit change
+ void display_unit_change(const Inkscape::Util::Unit* unit);
+
+ Gtk::Widget *createNewGridWidget(SPGrid *grid);
+ Gtk::Widget *createRightGridColumn(SPGrid *grid);
+ static void *notifyGridWidgetsDestroyed(void *data);
+
+ UI::Widget::RegisteredCheckButton *_grid_rcb_enabled = nullptr;
+ UI::Widget::RegisteredCheckButton *_grid_rcb_snap_visible_only = nullptr;
+ UI::Widget::RegisteredCheckButton *_grid_rcb_visible = nullptr;
+ UI::Widget::RegisteredCheckButton *_grid_rcb_dotted = nullptr;
+ UI::Widget::AlignmentSelector *_grid_as_alignment = nullptr;
+
+ class WatchConnection : private XML::NodeObserver
+ {
+ public:
+ WatchConnection(DocumentProperties *dialog)
+ : _dialog(dialog)
+ {}
+ ~WatchConnection() override { disconnect(); }
+ void connect(Inkscape::XML::Node *node);
+ void disconnect();
+
+ private:
+ void notifyChildAdded(XML::Node &node, XML::Node &child, XML::Node *prev) final;
+ void notifyChildRemoved(XML::Node &node, XML::Node &child, XML::Node *prev) final;
+ void notifyAttributeChanged(XML::Node &node, GQuark name, Util::ptr_shared old_value,
+ Util::ptr_shared new_value) final;
+
+ Inkscape::XML::Node *_node{nullptr};
+ DocumentProperties *_dialog;
+ };
+ // nodes connected to listeners
+ WatchConnection _namedview_connection;
+ WatchConnection _root_connection;
+};
+
+} // 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/document-resources.cpp b/src/ui/dialog/document-resources.cpp
new file mode 100644
index 0000000..7cc4aae
--- /dev/null
+++ b/src/ui/dialog/document-resources.cpp
@@ -0,0 +1,1170 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "document-resources.h"
+#include <cairo.h>
+#include <cairomm/enums.h>
+#include <cairomm/refptr.h>
+#include <cairomm/surface.h>
+#include <cassert>
+#include <cstddef>
+#include <gdkmm/pixbuf.h>
+#include <gdkmm/rgba.h>
+#include <glib/gi18n.h>
+#include <glibmm/exception.h>
+#include <glibmm/fileutils.h>
+#include <glibmm/main.h>
+#include <glibmm/markup.h>
+#include <glibmm/miscutils.h>
+#include <glibmm/refptr.h>
+#include <glibmm/ustring.h>
+#include <gtkmm/button.h>
+#include <gtkmm/cellrendererpixbuf.h>
+#include <gtkmm/cellrenderertext.h>
+#include <gtkmm/dialog.h>
+#include <gtkmm/drawingarea.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/filechooser.h>
+#include <gtkmm/filechooserdialog.h>
+#include <gtkmm/filefilter.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/iconview.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/object.h>
+#include <gtkmm/paned.h>
+#include <gtkmm/searchentry.h>
+#include <gtkmm/stack.h>
+#include <gtkmm/treemodelfilter.h>
+#include <gtkmm/treemodelsort.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/window.h>
+#include <memory>
+#include <optional>
+#include <sstream>
+#include <string>
+#include <typeindex>
+#include <unordered_map>
+#include <vector>
+#include "color.h"
+#include "display/cairo-utils.h"
+#include "document.h"
+#include "extension/system.h"
+#include "helper/choose-file.h"
+#include "helper/save-image.h"
+#include "inkscape.h"
+#include "object/sp-filter.h"
+#include "object/filters/sp-filter-primitive.h"
+#include "object/color-profile.h"
+#include "object/sp-gradient.h"
+#include "object/sp-font.h"
+#include "object/sp-image.h"
+#include "object/sp-item-group.h"
+#include "object/sp-marker.h"
+#include "object/sp-mesh-gradient.h"
+#include "object/sp-object.h"
+#include "object/sp-offset.h"
+#include "object/sp-path.h"
+#include "object/sp-pattern.h"
+#include "object/tags.h"
+#include "pattern-manipulation.h"
+#include "rdf.h"
+#include "selection.h"
+#include "ui/builder-utils.h"
+#include "object/sp-defs.h"
+#include "object/sp-root.h"
+#include "object/sp-symbol.h"
+#include "object/sp-use.h"
+#include "style.h"
+#include "ui/dialog/filedialog.h"
+#include "ui/icon-names.h"
+#include "ui/themes.h"
+#include "ui/util.h"
+#include "ui/widget/shapeicon.h"
+#include "util/object-renderer.h"
+#include "util/trim.h"
+#include "xml/href-attribute-helper.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+struct ItemColumns : public Gtk::TreeModel::ColumnRecord {
+ Gtk::TreeModelColumn<Glib::ustring> id;
+ Gtk::TreeModelColumn<Glib::ustring> label;
+ Gtk::TreeModelColumn<Cairo::RefPtr<Cairo::Surface>> image;
+ Gtk::TreeModelColumn<bool> editable;
+ Gtk::TreeModelColumn<SPObject*> object;
+ Gtk::TreeModelColumn<int> color;
+
+ ItemColumns() {
+ add(id);
+ add(label);
+ add(image);
+ add(editable);
+ add(object);
+ add(color);
+ }
+} g_item_columns;
+
+struct InfoColumns : public Gtk::TreeModel::ColumnRecord {
+ Gtk::TreeModelColumn<Glib::ustring> item;
+ Gtk::TreeModelColumn<Glib::ustring> value;
+ Gtk::TreeModelColumn<uint32_t> count;
+ Gtk::TreeModelColumn<SPObject*> object;
+
+ InfoColumns() {
+ add(item);
+ add(value);
+ add(count);
+ add(object);
+ }
+} g_info_columns;
+
+enum Resources : int {
+ Stats, Colors, Fonts, Styles, Patterns, Symbols, Markers, Gradients, Swatches, Images, Filters, External, Metadata
+};
+
+const std::unordered_map<std::string, Resources> g_id_to_resource = {
+ {"colors", Colors},
+ {"swatches", Swatches},
+ {"fonts", Fonts},
+ {"stats", Stats},
+ {"styles", Styles},
+ {"patterns", Patterns},
+ {"symbols", Symbols},
+ {"markers", Markers},
+ {"gradients", Gradients},
+ {"images", Images},
+ {"filters", Filters},
+ {"external", External},
+ {"metadata", Metadata},
+ // to do: SVG fonts
+ // other resources
+};
+
+size_t get_resource_count(const details::Statistics& stats, Resources rsrc) {
+ switch (rsrc) {
+ case Colors: return stats.colors;
+ case Swatches: return stats.swatches;
+ case Fonts: return stats.fonts;
+ case Symbols: return stats.symbols;
+ case Gradients: return stats.gradients;
+ case Patterns: return stats.patterns;
+ case Images: return stats.images;
+ case Filters: return stats.filters;
+ case Markers: return stats.markers;
+ case Metadata: return stats.metadata;
+ case Styles: return stats.styles;
+ case External: return stats.external_uris;
+
+ case Stats: return 1;
+
+ default:
+ break;
+ }
+ return 0;
+}
+
+Resources id_to_resource(const std::string& id) {
+ auto it = g_id_to_resource.find(id);
+ if (it == end(g_id_to_resource)) return Stats;
+
+ return it->second;
+}
+
+size_t get_resource_count(const std::string& id, const details::Statistics& stats) {
+ auto it = g_id_to_resource.find(id);
+ if (it == end(g_id_to_resource)) return 0;
+
+ return get_resource_count(stats, it->second);
+}
+
+bool is_resource_present(const std::string& id, const details::Statistics& stats) {
+ return get_resource_count(id, stats) > 0;
+}
+
+std::string choose_file(Glib::ustring title, Gtk::Window* parent, Glib::ustring mime_type, Glib::ustring file_name) {
+ static std::string current_folder;
+ return Inkscape::choose_file_save(title, parent, mime_type, file_name, current_folder);
+}
+
+void save_gimp_palette(std::string fname, const std::vector<int>& colors, const char* name) {
+ try {
+ std::ostringstream ost;
+ ost << "GIMP Palette\n";
+ if (name && *name) {
+ ost << "Name: " << name << "\n";
+ }
+ ost << "#\n";
+ for (auto c : colors) {
+ auto r = (c >> 16) & 0xff;
+ auto g = (c >> 8) & 0xff;
+ auto b = c & 0xff;
+ ost << r << ' ' << g << ' ' << b << '\n';
+ }
+ Glib::file_set_contents(fname, ost.str());
+ }
+ catch (Glib::Exception& ex) {
+ g_warning("Error saving color palette: %s", ex.what().c_str());
+ }
+ catch (...) {
+ g_warning("Error saving color palette.");
+ }
+}
+
+void extract_colors(Gtk::Window* parent, const std::vector<int>& colors, const char* name) {
+ if (colors.empty() || !parent) return;
+
+ auto fname = choose_file(_("Export Color Palette"), parent, "application/color-palette", "color-palette.gpl");
+ if (fname.empty()) return;
+
+ // export palette
+ save_gimp_palette(fname, colors, name);
+}
+
+void delete_object(SPObject* object, Inkscape::Selection* selection) {
+ if (!object || !selection) return;
+
+ auto document = object->document;
+
+ if (auto pattern = cast<SPPattern>(object)) {
+ // delete action fails for patterns; remove them by deleting their nodes
+ sp_repr_unparent(pattern->getRepr());
+ DocumentUndo::done(document, _("Delete pattern"), INKSCAPE_ICON("document-resources"));
+ }
+ else if (auto gradient = cast<SPGradient>(object)) {
+ // delete action fails for gradients; remove them by deleting their nodes
+ sp_repr_unparent(gradient->getRepr());
+ DocumentUndo::done(document, _("Delete gradient"), INKSCAPE_ICON("document-resources"));
+ }
+ else {
+ selection->set(object);
+ selection->deleteItems();
+ }
+}
+
+namespace details {
+ // editing "inkscape:label"
+ Glib::ustring get_inkscape_label(const SPObject& object) {
+ auto label = object.getAttribute("inkscape:label");
+ return Glib::ustring(label ? label : "");
+ }
+ void set_inkscape_label(SPObject& object, const Glib::ustring& label) {
+ object.setAttribute("inkscape:label", label.c_str());
+ }
+
+ // editing title element
+ Glib::ustring get_title(const SPObject& object) {
+ auto title = object.title();
+ Glib::ustring str(title ? title : "");
+ g_free(title);
+ return str;
+ }
+ void set_title(SPObject& object, const Glib::ustring& title) {
+ object.setTitle(title.c_str());
+ }
+}
+
+// label editing: get/set functions for various object types;
+// by default "inkscape:label" will be used (expressed as SPObject);
+// if some types need exceptions to this ruke, they can provide their own edit functions;
+// note: all most-derived types need to be listed to specify overrides
+std::map<std::type_index, std::function<Glib::ustring (const SPObject&)>> g_get_label = {
+ // default: editing "inkscape:label" as a description;
+ // patterns use Inkscape-specific "inkscape:label" attribute;
+ // gradients can also use labels instead of IDs;
+ // filters; to do - editing in a tree view;
+ // images can use both, label & title; defaulting to label for consistency
+ {typeid(SPObject), details::get_inkscape_label},
+ // exception: symbols use <title> element for description
+ {typeid(SPSymbol), details::get_title},
+ // markers use stockid for some reason - label: to do
+ {typeid(SPMarker), details::get_inkscape_label},
+};
+
+std::map<std::type_index, std::function<void (SPObject&, const Glib::ustring&)>> g_set_label = {
+ {typeid(SPObject), details::set_inkscape_label},
+ {typeid(SPSymbol), details::set_title},
+ {typeid(SPMarker), details::set_inkscape_label},
+};
+
+// liststore columns from glade file
+constexpr int COL_ID = 1;
+constexpr int COL_ICON = 2;
+constexpr int COL_COUNT = 3;
+
+DocumentResources::DocumentResources()
+ : DialogBase("/dialogs/document-resources", "DocumentResources"),
+ _builder(create_builder("dialog-document-resources.glade")),
+ _iconview(get_widget<Gtk::IconView>(_builder, "iconview")),
+ _treeview(get_widget<Gtk::TreeView>(_builder, "treeview")),
+ _selector(get_widget<Gtk::TreeView>(_builder, "tree")),
+ _edit(get_widget<Gtk::Button>(_builder, "edit")),
+ _select(get_widget<Gtk::Button>(_builder, "select")),
+ _delete(get_widget<Gtk::Button>(_builder, "delete")),
+ _extract(get_widget<Gtk::Button>(_builder, "extract")),
+ _search(get_widget<Gtk::SearchEntry>(_builder, "search")) {
+
+ _info_store = Gtk::ListStore::create(g_info_columns);
+ _item_store = Gtk::ListStore::create(g_item_columns);
+ auto filtered_info = Gtk::TreeModelFilter::create(_info_store);
+ auto filtered_items = Gtk::TreeModelFilter::create(_item_store);
+ auto model = Gtk::TreeModelSort::create(filtered_items);
+ model->set_sort_column(g_item_columns.label.index(), Gtk::SORT_ASCENDING);
+
+ add(get_widget<Gtk::Box>(_builder, "main"));
+
+ _iconview.set_model(model);
+ _iconview.set_text_column(g_item_columns.label);
+ _label_renderer = dynamic_cast<Gtk::CellRendererText*>(_iconview.get_first_cell());
+ assert(_label_renderer);
+ _label_renderer->property_editable() = true;
+ _label_renderer->signal_editing_started().connect([=](Gtk::CellEditable* cell, const Glib::ustring& path){
+ start_editing(cell, path);
+ });
+ _label_renderer->signal_edited().connect([=](const Glib::ustring& path, const Glib::ustring& new_text){
+ end_editing(path, new_text);
+ });
+
+ _iconview.pack_start(_image_renderer);
+ _iconview.add_attribute(_image_renderer, "surface", g_item_columns.image);
+
+ _treeview.set_model(filtered_info);
+
+ auto treestore = get_object<Gtk::ListStore>(_builder, "liststore");
+ _selector.set_row_separator_func([=](const Glib::RefPtr<Gtk::TreeModel>&, const Gtk::TreeModel::iterator& it){
+ Glib::ustring id;
+ it->get_value(COL_ID, id);
+ return id == "-";
+ });
+ _categories = Gtk::TreeModelFilter::create(treestore);
+ _categories->set_visible_func([=](const Gtk::TreeModel::const_iterator& it){
+ Glib::ustring id;
+ it->get_value(COL_ID, id);
+ return id == "-" || is_resource_present(id, _stats);
+ });
+ _selector.set_model(_categories);
+ auto icon_renderer = manage(new Inkscape::UI::Widget::CellRendererItemIcon());
+ _selector.insert_column("", *icon_renderer, 0);
+ auto column = _selector.get_column(0);
+ column->add_attribute(*icon_renderer, icon_renderer->property_shape_type().get_name(), COL_ICON);
+ auto count_renderer = Gtk::make_managed<Gtk::CellRendererText>();
+ auto count_column = _selector.get_column(_selector.append_column("", *count_renderer) - 1);
+ count_column->add_attribute(*count_renderer, "text", COL_COUNT);
+ count_column->set_cell_data_func(*count_renderer, [=](Gtk::CellRenderer* r, const Gtk::TreeModel::iterator& it){
+ uint64_t count;
+ it->get_value(COL_COUNT, count);
+ count_renderer->property_text().set_value(count > 0 ? std::to_string(count) : "");
+ });
+ count_renderer->set_padding(3, 4);
+
+ _wr.setUpdating(true); // set permanently
+
+ for (auto entity = rdf_work_entities; entity && entity->name; ++entity) {
+ if (entity->editable != RDF_EDIT_GENERIC) continue;
+
+ auto w = Inkscape::UI::Widget::EntityEntry::create(entity, _wr);
+ _rdf_list.push_back(w);
+ }
+
+ _page_selection = _selector.get_selection();
+ _selection_change = _page_selection->signal_changed().connect([=](){
+ if (auto it = _page_selection->get_selected()) {
+ Glib::ustring id;
+ it->get_value(COL_ID, id);
+ select_page(id);
+ }
+ });
+
+ auto paned = &get_widget<Gtk::Paned>(_builder, "paned");
+ auto move = [=](){
+ auto pos = paned->get_position();
+ get_widget<Gtk::Label>(_builder, "spacer").set_size_request(pos);
+ };
+ paned->property_position().signal_changed().connect([=](){ move(); });
+ move();
+
+ _edit.signal_clicked().connect([=](){
+ auto sel = _iconview.get_selected_items();
+ if (sel.size() == 1) {
+ // todo: investigate why this doesn't work initially:
+ _iconview.set_cursor(sel.front(), true);
+ }
+ else {
+ // treeview todo if needed
+ }
+ });
+
+ // selectable elements can be selected on the canvas;
+ // even elements in <defs> can be selected (same as in XML dialog)
+ _select.signal_clicked().connect([=](){
+ auto document = getDocument();
+ auto desktop = getDesktop();
+ if (!document || !desktop) return;
+
+ if (auto row = selected_item()) {
+ Glib::ustring id = row[g_item_columns.id];
+ if (auto object = document->getObjectById(id)) {
+ // select object
+ desktop->getSelection()->set(object);
+ }
+ }
+ else {
+ // to do: select from treeview if needed
+ }
+ });
+
+ _search.signal_search_changed().connect([=](){
+ filtered_items->freeze_notify();
+ filtered_items->refilter();
+ filtered_items->thaw_notify();
+
+ filtered_info->freeze_notify();
+ filtered_info->refilter();
+ filtered_info->thaw_notify();
+ });
+
+ // filter gridview
+ filtered_items->set_visible_func([=](const Gtk::TreeModel::const_iterator& it){
+ if (_search.get_text_length() == 0) return true;
+
+ auto str = _search.get_text().lowercase();
+ Glib::ustring label = (*it)[g_item_columns.label];
+ return label.lowercase().find(str) != Glib::ustring::npos;
+ });
+ // filter treeview too
+ filtered_info->set_visible_func([=](const Gtk::TreeModel::const_iterator& it){
+ if (_search.get_text_length() == 0) return true;
+
+ auto str = _search.get_text().lowercase();
+ Glib::ustring value = (*it)[g_info_columns.value];
+ return value.lowercase().find(str) != Glib::ustring::npos;
+ });
+
+ _delete.signal_clicked().connect([=](){
+ // delete selected object
+ if (auto row = selected_item()) {
+ SPObject* object = row[g_item_columns.object];
+ delete_object(object, getDesktop()->getSelection());
+ }
+ });
+
+ _extract.signal_clicked().connect([=](){
+ auto window = dynamic_cast<Gtk::Window*>(get_toplevel());
+
+ switch (_showing_resource) {
+ case Images:
+ // extract selected image
+ if (auto row = selected_item()) {
+ SPObject* object = row[g_item_columns.object];
+ extract_image(window, cast<SPImage>(object));
+ }
+ break;
+ case Colors:
+ // export colors into a GIMP palette
+ if (_document) {
+ std::vector<int> colors;
+ _item_store->foreach_iter([&](const Gtk::TreeModel::iterator& it){
+ int c;
+ it->get_value(g_item_columns.color.index(), c);
+ colors.push_back(c);
+ return false; // false means continue
+ });
+ extract_colors(window, colors, _document->getDocumentName());
+ }
+ break;
+ default:
+ // nothing else so far
+ break;
+ }
+ });
+
+ _iconview.signal_selection_changed().connect([=](){
+ update_buttons();
+ });
+
+}
+
+Gtk::TreeModel::Row DocumentResources::selected_item() {
+ auto sel = _iconview.get_selected_items();
+ auto model = _iconview.get_model();
+ Gtk::TreeModel::Row row;
+ if (sel.size() == 1 && model) {
+ row = *model->get_iter(sel.front());
+ }
+ return row;
+}
+
+void DocumentResources::update_buttons() {
+ if (!_iconview.get_visible()) return;
+
+ auto single_sel = !!selected_item();
+
+ _edit.set_sensitive(single_sel);
+ _extract.set_sensitive(single_sel || _showing_resource == Colors);
+ _delete.set_sensitive(single_sel);
+ _select.set_sensitive(single_sel);
+}
+
+Cairo::RefPtr<Cairo::Surface> render_color(uint32_t rgb, double size, double radius, int device_scale) {
+ Cairo::RefPtr<Cairo::Surface> nul;
+ return add_background_to_image(nul, rgb, size / 2, radius, device_scale, 0x7f7f7f00);
+}
+
+void collect_object_colors(SPObject& obj, std::map<std::string, SPColor>& colors) {
+ auto style = obj.style;
+
+ if (style->stroke.set && style->stroke.colorSet) {
+ const auto& c = style->stroke.value.color;
+ colors[c.toString()] = c;
+ }
+
+ if (style->color.set) {
+ const auto& c = style->color.value.color;
+ colors[c.toString()] = c;
+ }
+
+ if (style->fill.set) {
+ const auto& c = style->fill.value.color;
+ colors[c.toString()] = c;
+ }
+
+ if (style->solid_color.set) {
+ const auto& c = style->solid_color.value.color;
+ colors[c.toString()] = c;
+ }
+}
+
+// traverse all nodes starting from given 'object'
+template<typename V>
+void apply_visitor(SPObject& object, V&& visitor) {
+ visitor(object);
+
+ // SPUse inserts referenced object as a child; skip it
+ if (is<SPUse>(&object)) return;
+
+ for (auto&& child : object.children) {
+ apply_visitor(child, visitor);
+ }
+}
+
+std::map<std::string, SPColor> collect_colors(SPObject* object) {
+ std::map<std::string, SPColor> colors;
+ if (object) {
+ apply_visitor(*object, [&](SPObject& obj){ collect_object_colors(obj, colors); });
+ }
+ return colors;
+}
+
+void collect_used_fonts(SPObject& object, std::set<std::string>& fonts) {
+ auto style = object.style;
+
+ if (style->font_specification.set) {
+ auto fspec = style->font_specification.value();
+ if (fspec && *fspec) {
+ fonts.insert(fspec);
+ }
+ }
+ else if (style->font.set) {
+ // some SVG files won't have Inkscape-specific fontspec; read font settings instead
+ auto font = style->font.get_value();
+ if (style->font_style.set) {
+ font += ' ' + style->font_style.get_value();
+ }
+ fonts.insert(font);
+ }
+}
+
+std::set<std::string> collect_fontspecs(SPObject* object) {
+ std::set<std::string> fonts;
+ if (object) {
+ apply_visitor(*object, [&](SPObject& obj){ collect_used_fonts(obj, fonts); });
+ }
+ return fonts;
+}
+
+template<typename T>
+bool filter_element(T& object) { return true; }
+
+template<>
+bool filter_element<SPPattern>(SPPattern& object) { return object.hasChildren(); }
+
+template<>
+bool filter_element<SPGradient>(SPGradient& object) { return object.hasStops(); }
+
+template<typename T>
+std::vector<T*> collect_items(SPObject* object, bool (*filter)(T&) = filter_element<T>) {
+ std::vector<T*> items;
+ if (object) {
+ apply_visitor(*object, [&](SPObject& obj){
+ if (auto t = cast<T>(&obj)) {
+ if (filter(*t)) items.push_back(t);
+ }
+ });
+ }
+ return items;
+}
+
+std::unordered_map<std::string, size_t> collect_styles(SPObject* root) {
+ std::unordered_map<std::string, size_t> map;
+ if (!root) return map;
+
+ apply_visitor(*root, [&](SPObject& obj){
+ if (auto style = obj.getAttribute("style")) {
+ map[style]++;
+ }
+ });
+
+ return map;
+}
+
+bool has_external_ref(SPObject& obj) {
+ bool present = false;
+ if (auto href = Inkscape::getHrefAttribute(*obj.getRepr()).second) {
+ if (*href && *href != '#' && *href != '?') {
+ auto scheme = Glib::uri_parse_scheme(href);
+ // There are tens of schemes: https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
+ // TODO: Which ones to collect as external resources?
+ if (scheme == "file" || scheme == "http" || scheme == "https" || scheme.empty()) {
+ present = true;
+ }
+ }
+ }
+ return present;
+}
+
+details::Statistics collect_statistics(SPObject* root) {
+ details::Statistics stats;
+
+ if (!root) {
+ return stats;
+ }
+
+ std::map<std::string, SPColor> colors;
+ std::set<std::string> fonts;
+
+ apply_visitor(*root, [&](SPObject& obj){
+ // order of tests is important; derived classes first, before base,
+ // so meshgradient first, gradient next
+
+ if (auto pattern = cast<SPPattern>(&obj)) {
+ if (filter_element(*pattern)) {
+ stats.patterns++;
+ }
+ }
+ else if (is<SPMeshGradient>(&obj)) {
+ stats.meshgradients++;
+ }
+ else if (auto gradient = cast<SPGradient>(&obj)) {
+ if (filter_element(*gradient)) {
+ if (gradient->isSwatch()) {
+ stats.swatches++;
+ }
+ else {
+ stats.gradients++;
+ }
+ }
+ }
+ else if (auto marker = cast<SPMarker>(&obj)) {
+ if (filter_element(*marker)) {
+ stats.markers++;
+ }
+ }
+ else if (auto symbol = cast<SPSymbol>(&obj)) {
+ if (filter_element(*symbol)) {
+ stats.symbols++;
+ }
+ }
+ else if (is<SPFont>(&obj)) { // SVG font
+ stats.svg_fonts++;
+ }
+ else if (is<SPImage>(&obj)) {
+ stats.images++;
+ }
+ else if (auto group = cast<SPGroup>(&obj)) {
+ if (strcmp(group->getRepr()->name(), "svg:g") == 0) {
+ switch (group->layerMode()) {
+ case SPGroup::GROUP:
+ stats.groups++;
+ break;
+ case SPGroup::LAYER:
+ stats.layers++;
+ break;
+ }
+ }
+ }
+ else if (is<SPPath>(&obj)) {
+ stats.paths++;
+ }
+ else if (is<SPFilter>(&obj)) {
+ stats.filters++;
+ }
+ else if (is<ColorProfile>(&obj)) {
+ stats.colorprofiles++;
+ }
+
+ if (auto style = obj.getAttribute("style")) {
+ if (*style) stats.styles++;
+ }
+
+ if (has_external_ref(obj)) {
+ stats.external_uris++;
+ }
+
+ collect_object_colors(obj, colors);
+ collect_used_fonts(obj, fonts);
+
+ // verify:
+ stats.nodes++;
+ });
+
+ stats.colors = colors.size();
+ stats.fonts = fonts.size();
+
+ return stats;
+}
+
+details::Statistics DocumentResources::collect_statistics() {
+
+ auto root = _document ? _document->getRoot() : nullptr;
+ auto stats = ::Inkscape::UI::Dialog::collect_statistics(root);
+
+ if (_document) {
+ for (auto& el : _rdf_list) {
+ bool read_only = true;
+ el.update(_document, read_only);
+ if (!el.content().empty()) stats.metadata++;
+ }
+ }
+
+ return stats;
+}
+
+void DocumentResources::rebuild_stats() {
+ _stats = collect_statistics();
+
+ if (auto desktop = getDesktop()) {
+ _wr.setDesktop(desktop);
+ }
+
+ _categories->refilter();
+ _categories->foreach_iter([=](const Gtk::TreeModel::iterator& it){
+ Glib::ustring id;
+ it->get_value(COL_ID, id);
+ auto count = get_resource_count(id, _stats);
+ if (id == "stats") count = 0; // don't show count 1 for "overview"
+ it->set_value(COL_COUNT, count);
+ return false; // false means continue
+ });
+ _selector.columns_autosize();
+}
+
+void DocumentResources::documentReplaced() {
+ _document = getDocument();
+ if (_document) {
+ _document_modified = _document->connectModified([=](unsigned){
+ // brute force refresh, but throttled
+ _idle_refresh = Glib::signal_timeout().connect([=](){
+ rebuild_stats();
+ refresh_current_page();
+ return false;
+ }, 200);
+ });
+ }
+ else {
+ _document_modified.disconnect();
+ }
+
+ rebuild_stats();
+ refresh_current_page();
+}
+
+void DocumentResources::refresh_current_page() {
+ auto page = _cur_page_id;
+ if (!is_resource_present(page, _stats)) {
+ page = "stats";
+ }
+ auto model = _selector.get_model();
+
+ model->foreach([=](const Gtk::TreeModel::Path& path, const Gtk::TreeModel::iterator& it) {
+ Glib::ustring id;
+ it->get_value(COL_ID, id);
+
+ if (id == page) {
+ _page_selection->select(path);
+ refresh_page(id);
+ return true;
+ }
+ return false;
+ });
+}
+
+void DocumentResources::selectionModified(Inkscape::Selection* selection, guint flags) {
+ // no op so far
+}
+
+auto get_id = [](const SPObject* object) { auto id = object->getId(); return id ? id : ""; };
+auto label_fmt = [](const char* label, const Glib::ustring& id) { return label && *label ? label : '#' + id; };
+
+void add_colors(Glib::RefPtr<Gtk::ListStore> item_store, const std::map<std::string, SPColor>& colors, int device_scale) {
+ for (auto&& it : colors) {
+ const auto& color = it.second;
+
+ auto row = *item_store->append();
+ auto name = color.toString();
+ auto rgba32 = color.toRGBA32(0xff);
+ auto rgb24 = rgba32 >> 8;
+
+ row[g_item_columns.id] = name;
+ row[g_item_columns.label] = name;
+ row[g_item_columns.color] = rgb24;
+ int size = 20;
+ double radius = 2.0;
+ row[g_item_columns.image] = render_color(rgba32, size, radius, device_scale);
+ row[g_item_columns.object] = nullptr;
+ }
+}
+
+void _add_items_with_images(Glib::RefPtr<Gtk::ListStore> item_store, const std::vector<SPObject*>& items, double width, double height, int device_scale, bool use_title, object_renderer::options opt) {
+ object_renderer renderer;
+ item_store->freeze_notify();
+
+ for (auto item : items) {
+ auto row = *item_store->append();
+
+ auto id = get_id(item);
+ row[g_item_columns.id] = id;
+
+ if (use_title) {
+ auto title = item->title();
+ row[g_item_columns.label] = label_fmt(title, id);
+ g_free(title);
+ }
+ else {
+ auto label = item->getAttribute("inkscape:label");
+ row[g_item_columns.label] = label_fmt(label, id);
+ }
+ row[g_item_columns.image] = renderer.render(*item, width, height, device_scale, opt);
+ row[g_item_columns.object] = item;
+ }
+
+ item_store->thaw_notify();
+}
+
+template<typename T>
+void add_items_with_images(Glib::RefPtr<Gtk::ListStore> item_store, const std::vector<T*>& items, double width, double height, int device_scale, bool use_title = false, object_renderer::options opt = {}) {
+ static_assert(std::is_base_of<SPObject, T>::value);
+ _add_items_with_images(item_store, reinterpret_cast<const std::vector<SPObject*>&>(items), width, height, device_scale, use_title, opt);
+}
+
+void add_fonts(Glib::RefPtr<Gtk::ListStore> store, const std::set<std::string>& fontspecs) {
+ size_t i = 1;
+ for (auto&& fs : fontspecs) {
+ auto row = *store->append();
+ row[g_info_columns.item] = Glib::ustring::compose("%1 %2", _("Font"), i++);
+ auto name = Glib::Markup::escape_text(fs);
+ row[g_info_columns.value] = Glib::ustring::format(
+ "<span allow_breaks='false' size='xx-large' font='", fs, "'>", name, "</span>\n",
+ "<span allow_breaks='false' size='small' alpha='60%'>", name, "</span>"
+ );
+ }
+}
+
+void add_stats(Glib::RefPtr<Gtk::ListStore> info_store, SPDocument* document, const details::Statistics& stats) {
+ auto read_only = true;
+ auto license = document ? rdf_get_license(document, read_only) : nullptr;
+
+ std::pair<const char*, std::string> str[] = {
+ {_("Document"), document && document->getDocumentFilename() ? document->getDocumentFilename() : "-"},
+ {_("License"), license && license->name ? license->name : "-"},
+ {_("Metadata"), stats.metadata > 0 ? C_("Adjective for Metadata status", "Present") : "-"},
+ };
+ for (auto& pair : str) {
+ auto row = *info_store->append();
+ row[g_info_columns.item] = pair.first;
+ row[g_info_columns.value] = Glib::Markup::escape_text(pair.second);
+ }
+
+ std::pair<const char*, size_t> kv[] = {
+ {_("Colors"), stats.colors},
+ {_("Color profiles"), stats.colorprofiles},
+ {_("Swatches"), stats.swatches},
+ {_("Fonts"), stats.fonts},
+ {_("Gradients"), stats.gradients},
+ {_("Mesh gradients"), stats.meshgradients},
+ {_("Patterns"), stats.patterns},
+ {_("Symbols"), stats.symbols},
+ {_("Markers"), stats.markers},
+ {_("Filters"), stats.filters},
+ {_("Images"), stats.images},
+ {_("SVG fonts"), stats.svg_fonts},
+ {_("Layers"), stats.layers},
+ {_("Total elements"), stats.nodes},
+ {_("Groups"), stats.groups},
+ {_("Paths"), stats.paths},
+ {_("External URIs"), stats.external_uris},
+ };
+ for (auto& pair : kv) {
+ auto row = *info_store->append();
+ row[g_info_columns.item] = pair.first;
+ row[g_info_columns.value] = pair.second ? std::to_string(pair.second) : "-";
+ }
+}
+
+void add_metadata(Glib::RefPtr<Gtk::ListStore> info_store, SPDocument* document,
+ const boost::ptr_vector<Inkscape::UI::Widget::EntityEntry>& rdf_list) {
+
+ for (auto& entry : rdf_list) {
+ auto row = *info_store->append();
+ auto label = entry._label.get_label();
+ Util::trim(label, ":");
+ row[g_info_columns.item] = label;
+ row[g_info_columns.value] = Glib::Markup::escape_text(entry.content());
+ }
+}
+
+void add_filters(Glib::RefPtr<Gtk::ListStore> info_store, const std::vector<SPFilter*>& filters) {
+ for (auto& filter : filters) {
+ auto row = *info_store->append();
+ auto label = filter->getAttribute("inkscape:label");
+ auto name = Glib::ustring(label ? label : filter->getId());
+ row[g_info_columns.item] = name;
+ std::ostringstream ost;
+ bool first = true;
+ for (auto& obj : filter->children) {
+ if (auto primitive = cast<SPFilterPrimitive>(&obj)) {
+ if (!first) ost << ", ";
+ Glib::ustring name = primitive->getRepr()->name();
+ if (name.find("svg:") != std::string::npos) {
+ name.erase(name.find("svg:"), 4);
+ }
+ ost << name;
+ first = false;
+ }
+ }
+ row[g_info_columns.value] = Glib::Markup::escape_text(ost.str());
+ }
+}
+
+void add_styles(Glib::RefPtr<Gtk::ListStore> info_store, const std::unordered_map<std::string, size_t>& map) {
+ std::vector<std::string> vect;
+ vect.reserve(map.size());
+ for (auto style : map) {
+ vect.emplace_back(style.first);
+ }
+ std::sort(vect.begin(), vect.end());
+ info_store->freeze_notify();
+ int n = 1;
+ for (auto& style : vect) {
+ auto row = *info_store->append();
+ row[g_info_columns.item] = _("Style ") + std::to_string(n++);
+ row[g_info_columns.count] = map.find(style)->second;
+ row[g_info_columns.value] = Glib::Markup::escape_text(style);
+ }
+ info_store->thaw_notify();
+}
+
+void add_refs(Glib::RefPtr<Gtk::ListStore> info_store, const std::vector<SPObject*>& objects) {
+ info_store->freeze_notify();
+ for (auto& obj : objects) {
+ auto href = Inkscape::getHrefAttribute(*obj->getRepr()).second;
+ if (!href) continue;
+
+ auto row = *info_store->append();
+ row[g_info_columns.item] = label_fmt(nullptr, get_id(obj));
+ row[g_info_columns.value] = href;
+ row[g_info_columns.object] = obj;
+ }
+ info_store->thaw_notify();
+}
+
+void DocumentResources::select_page(const Glib::ustring& id) {
+ if (_cur_page_id == id) return;
+
+ _cur_page_id = id;
+ refresh_page(id);
+}
+
+void DocumentResources::clear_stores() {
+ _item_store->freeze_notify();
+ _item_store->clear();
+ _item_store->thaw_notify();
+
+ _info_store->freeze_notify();
+ _info_store->clear();
+ _info_store->thaw_notify();
+}
+
+void DocumentResources::refresh_page(const Glib::ustring& id) {
+ auto rsrc = id_to_resource(id);
+
+ // GTK spits out a lot of warnings and errors from filtered model.
+ // I don't know how to fix them.
+ // https://gitlab.gnome.org/GNOME/gtk/-/issues/1150
+ // Clear sorting? Remove filtering?
+ // GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID
+
+ clear_stores();
+
+ auto root = _document ? _document->getRoot() : nullptr;
+ auto defs = _document ? _document->getDefs() : nullptr;
+
+ int device_scale = get_scale_factor();
+ auto tab = "iconview";
+ auto has_count = false;
+ auto item_width = 90;
+ auto context = get_style_context();
+ Gdk::RGBA color = context->get_color(get_state_flags());
+ auto label_editable = false;
+ auto items_selectable = true;
+ auto can_delete = false; // enable where supported
+ auto can_extract = false;
+
+ switch (rsrc) {
+ case Colors:
+ add_colors(_item_store, collect_colors(root), device_scale);
+ item_width = 70;
+ items_selectable = false; // to do: make selectable?
+ can_extract = true;
+ break;
+
+ case Symbols:
+ {
+ auto opt = object_renderer::options();
+ if (INKSCAPE.themecontext->isCurrentThemeDark(dynamic_cast<Gtk::Container*>(this))) {
+ // white background for typically black symbols, so they don't disappear in a dark theme
+ opt.solid_background(0xf0f0f0ff, 3, 3);
+ }
+ opt.symbol_style_from_use();
+ add_items_with_images(_item_store, collect_items<SPSymbol>(defs), 70, 60, device_scale, true, opt);
+ }
+ label_editable = true;
+ can_delete = true;
+ break;
+
+ case Patterns:
+ add_items_with_images(_item_store, collect_items<SPPattern>(defs), 80, 70, device_scale);
+ label_editable = true;
+ can_delete = true;
+ break;
+
+ case Markers:
+ add_items_with_images(_item_store, collect_items<SPMarker>(defs), 70, 60, device_scale, false,
+ object_renderer::options().foreground(color));
+ label_editable = true;
+ can_delete = true;
+ break;
+
+ case Gradients:
+ add_items_with_images(_item_store,
+ collect_items<SPGradient>(defs, [](auto& g){ return filter_element(g) && !g.isSwatch(); }),
+ 180, 22, device_scale);
+ label_editable = true;
+ can_delete = true;
+ break;
+
+ case Swatches:
+ add_items_with_images(_item_store,
+ collect_items<SPGradient>(defs, [](auto& g){ return filter_element(g) && g.isSwatch(); }),
+ 100, 22, device_scale);
+ label_editable = true;
+ can_delete = true;
+ break;
+
+ case Fonts:
+ add_fonts(_info_store, collect_fontspecs(root));
+ tab = "treeview";
+ items_selectable = false;
+ break;
+
+ case Filters:
+ add_filters(_info_store, collect_items<SPFilter>(defs));
+ label_editable = true;
+ tab = "treeview";
+ items_selectable = false; // to do: make selectable
+ break;
+
+ case Styles:
+ add_styles(_info_store, collect_styles(root));
+ tab = "treeview";
+ has_count = true;
+ items_selectable = false; // to do: make selectable?
+ break;
+
+ case Images:
+ add_items_with_images(_item_store, collect_items<SPImage>(root), 110, 110, device_scale);
+ label_editable = true;
+ can_extract = true;
+ can_delete = true;
+ break;
+
+ case External:
+ add_refs(_info_store, collect_items<SPObject>(root, [](auto& obj){ return has_external_ref(obj); }));
+ tab = "treeview";
+ items_selectable = false; // to do: make selectable
+ break;
+
+ case Stats:
+ add_stats(_info_store, _document, _stats);
+ tab = "treeview";
+ items_selectable = false;
+ break;
+
+ case Metadata:
+ add_metadata(_info_store, _document, _rdf_list);
+ tab = "treeview";
+ items_selectable = false;
+ break;
+ }
+
+ _showing_resource = rsrc;
+
+ _treeview.get_column(1)->set_visible(has_count);
+ _label_renderer->property_editable() = label_editable;
+ widget_show(_edit, label_editable);
+ widget_show(_select, items_selectable);
+ widget_show(_delete, can_delete);
+ widget_show(_extract, can_extract);
+
+ _iconview.set_item_width(item_width);
+ get_widget<Gtk::Stack>(_builder, "stack").set_visible_child(tab);
+ update_buttons();
+}
+
+void DocumentResources::start_editing(Gtk::CellEditable* cell, const Glib::ustring& path) {
+ auto entry = dynamic_cast<Gtk::Entry*>(cell);
+ entry->set_has_frame();
+}
+
+void DocumentResources::end_editing(const Glib::ustring& path, const Glib::ustring& new_text) {
+ auto model = _iconview.get_model();
+ Gtk::TreeModel::Row row = *model->get_iter(path);
+ if (!row) return;
+
+ SPObject* object = row[g_item_columns.object];
+ if (!object) {
+ g_warning("Missing object ptr, cannot edit object's name.");
+ return;
+ }
+
+ // try object-specific edit functions first; if not present fall back to generic
+ auto getter = g_get_label[typeid(*object)];
+ auto setter = g_set_label[typeid(*object)];
+ if (!getter || !setter) {
+ getter = g_get_label[typeid(SPObject)];
+ setter = g_set_label[typeid(SPObject)];
+ }
+
+ auto name = getter(*object);
+ if (new_text == name) return;
+
+ setter(*object, new_text);
+
+ auto id = get_id(object);
+ row[g_item_columns.label] = label_fmt(new_text.c_str(), id);
+
+ if (auto document = object->document) {
+ DocumentUndo::done(document, _("Edit object title"), INKSCAPE_ICON("document-resources"));
+ }
+}
+
+} } } // namespaces
diff --git a/src/ui/dialog/document-resources.h b/src/ui/dialog/document-resources.h
new file mode 100644
index 0000000..bf670cd
--- /dev/null
+++ b/src/ui/dialog/document-resources.h
@@ -0,0 +1,105 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief A simple dialog for previewing document resources
+ *
+ * Copyright (C) 2023 Michael Kowalski
+ */
+
+#ifndef SEEN_DOC_RESOURCES_H
+#define SEEN_DOC_RESOURCES_H
+
+#include "document.h"
+#include "helper/auto-connection.h"
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/entity-entry.h"
+#include "ui/widget/registry.h"
+#include <cstddef>
+#include <glibmm/refptr.h>
+#include <glibmm/ustring.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/button.h>
+#include <gtkmm/cellrendererpixbuf.h>
+#include <gtkmm/cellrenderertext.h>
+#include <gtkmm/iconview.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/searchentry.h>
+#include <gtkmm/treeview.h>
+#include <memory>
+#include <string>
+#include <boost/ptr_container/ptr_vector.hpp>
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+namespace details {
+ struct Statistics {
+ size_t nodes = 0;
+ size_t groups = 0;
+ size_t layers = 0;
+ size_t paths = 0;
+ size_t images = 0;
+ size_t patterns = 0;
+ size_t symbols = 0;
+ size_t markers = 0;
+ size_t fonts = 0;
+ size_t filters = 0;
+ size_t svg_fonts = 0;
+ size_t colors = 0;
+ size_t gradients = 0;
+ size_t swatches = 0;
+ size_t metadata = 0;
+ size_t styles = 0;
+ size_t meshgradients = 0;
+ size_t colorprofiles = 0;
+ size_t external_uris = 0;
+ };
+}
+
+class DocumentResources : public DialogBase {
+public:
+ DocumentResources();
+
+private:
+ void documentReplaced() override;
+ void select_page(const Glib::ustring& id);
+ void refresh_page(const Glib::ustring& id);
+ void refresh_current_page();
+ void rebuild_stats();
+ details::Statistics collect_statistics();
+ void start_editing(Gtk::CellEditable* cell, const Glib::ustring& path);
+ void end_editing(const Glib::ustring& path, const Glib::ustring& new_text);
+ void selectionModified(Inkscape::Selection *selection, guint flags) override;
+ void update_buttons();
+ Gtk::TreeModel::Row selected_item();
+ void clear_stores();
+
+ Glib::RefPtr<Gtk::Builder> _builder;
+ Glib::RefPtr<Gtk::ListStore> _item_store;
+ Glib::RefPtr<Gtk::TreeModelFilter> _categories;
+ Glib::RefPtr<Gtk::ListStore> _info_store;
+ Gtk::CellRendererPixbuf _image_renderer;
+ SPDocument* _document = nullptr;
+ auto_connection _selection_change;
+ details::Statistics _stats;
+ std::string _cur_page_id; // the last category that user selected
+ int _showing_resource = -1; // ID of the resource that's currently presented
+ Glib::RefPtr<Gtk::TreeSelection> _page_selection;
+ Gtk::IconView& _iconview;
+ Gtk::TreeView& _treeview;
+ Gtk::TreeView& _selector;
+ Gtk::Button& _edit;
+ Gtk::Button& _select;
+ Gtk::Button& _delete;
+ Gtk::Button& _extract;
+ Gtk::SearchEntry& _search;
+ boost::ptr_vector<Inkscape::UI::Widget::EntityEntry> _rdf_list;
+ UI::Widget::Registry _wr;
+ Gtk::CellRendererText* _label_renderer;
+ auto_connection _document_modified;
+ auto_connection _idle_refresh;
+};
+
+} } } // namespaces
+
+#endif // SEEN_DOC_RESOURCES_H
diff --git a/src/ui/dialog/export-batch.cpp b/src/ui/dialog/export-batch.cpp
new file mode 100644
index 0000000..43f2b96
--- /dev/null
+++ b/src/ui/dialog/export-batch.cpp
@@ -0,0 +1,830 @@
+// 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>
+ * Anshudhar Kumar Singh <anshudhar2001@gmail.com>
+ *
+ * Copyright (C) 1999-2007, 2021 Authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/convert.h>
+#include <glibmm/i18n.h>
+#include <glibmm/miscutils.h>
+#include <gtkmm.h>
+#include <png.h>
+#include <regex>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "extension/db.h"
+#include "extension/output.h"
+#include "file.h"
+#include "helper/auto-connection.h"
+#include "helper/png-write.h"
+#include "inkscape-window.h"
+#include "inkscape.h"
+#include "io/resource.h"
+#include "io/sys.h"
+#include "layer-manager.h"
+#include "message-stack.h"
+#include "object/object-set.h"
+#include "object/sp-namedview.h"
+#include "object/sp-page.h"
+#include "object/sp-root.h"
+#include "page-manager.h"
+#include "preferences.h"
+#include "selection-chemistry.h"
+#include "ui/dialog-events.h"
+#include "ui/dialog/export.h"
+#include "ui/dialog/export-batch.h"
+#include "ui/dialog/dialog-notebook.h"
+#include "ui/dialog/filedialog.h"
+#include "ui/interface.h"
+#include "ui/widget/color-picker.h"
+#include "ui/widget/export-lists.h"
+#include "ui/widget/export-preview.h"
+#include "ui/widget/scrollprotected.h"
+#include "ui/widget/unit-menu.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+BatchItem::BatchItem(SPItem *item, std::shared_ptr<PreviewDrawing> drawing)
+{
+ _item = item;
+ init(drawing);
+ _object_modified_conn = _item->connectModified([=](SPObject *obj, unsigned int flags) {
+ update_label();
+ });
+ update_label();
+}
+
+BatchItem::BatchItem(SPPage *page, std::shared_ptr<PreviewDrawing> drawing)
+{
+ _page = page;
+ init(drawing);
+ _object_modified_conn = _page->connectModified([=](SPObject *obj, unsigned int flags) {
+ update_label();
+ });
+ update_label();
+}
+
+void BatchItem::update_label()
+{
+ Glib::ustring label = "no-name";
+ if (_page) {
+ label = _page->getDefaultLabel();
+ if (auto id = _page->label()) {
+ label = id;
+ }
+ } else if (_item) {
+ label = _item->defaultLabel();
+ if (label.empty()) {
+ if (auto _id = _item->getId()) {
+ label = _id;
+ } else {
+ label = "no-id";
+ }
+ }
+ }
+ _label_str = label;
+ _label.set_text(label);
+ set_tooltip_text(label);
+}
+
+void BatchItem::init(std::shared_ptr<PreviewDrawing> drawing) {
+
+
+ _grid.set_row_spacing(5);
+ _grid.set_column_spacing(5);
+ _grid.set_valign(Gtk::Align::ALIGN_CENTER);
+
+ _selector.set_active(true);
+ _selector.set_can_focus(false);
+ _selector.set_margin_start(2);
+ _selector.set_margin_bottom(2);
+ _selector.set_valign(Gtk::ALIGN_END);
+
+ _option.set_active(false);
+ _option.set_can_focus(false);
+ _option.set_margin_start(2);
+ _option.set_margin_bottom(2);
+ _option.set_valign(Gtk::ALIGN_END);
+
+ _preview.set_name("export_preview_batch");
+ _preview.setItem(_item);
+ _preview.setDrawing(drawing);
+ _preview.setSize(64);
+ _preview.set_halign(Gtk::ALIGN_CENTER);
+ _preview.set_valign(Gtk::ALIGN_CENTER);
+
+ _label.set_width_chars(10);
+ _label.set_ellipsize(Pango::ELLIPSIZE_END);
+ _label.set_halign(Gtk::Align::ALIGN_CENTER);
+
+ set_valign(Gtk::Align::ALIGN_START);
+ set_halign(Gtk::Align::ALIGN_START);
+ add(_grid);
+ show();
+ this->set_can_focus(false);
+
+ _selector.signal_toggled().connect([=]() {
+ set_selected(_selector.get_active());
+ });
+ _option.signal_toggled().connect([=]() {
+ set_selected(_option.get_active());
+ });
+
+ // This initially packs the widgets with a hidden preview.
+ refresh(!is_hide, 0);
+}
+
+/**
+ * Syncronise the FlowBox selection to the active widget activity.
+ */
+void BatchItem::set_selected(bool selected)
+{
+ auto box = dynamic_cast<Gtk::FlowBox *>(get_parent());
+ if (box && selected != is_selected()) {
+ if (selected) {
+ box->select_child(*this);
+ } else {
+ box->unselect_child(*this);
+ }
+ }
+}
+
+/**
+ * Syncronise the FlowBox selection to the existing active widget state.
+ */
+void BatchItem::update_selected()
+{
+ if (auto parent = dynamic_cast<Gtk::FlowBox *>(get_parent()))
+ on_mode_changed(parent->get_selection_mode());
+ if (_selector.get_visible()) {
+ set_selected(_selector.get_active());
+ } else if (_option.get_visible()) {
+ set_selected(_option.get_active());
+ }
+}
+
+/**
+ * A change in the selection mode for the flow box.
+ */
+void BatchItem::on_mode_changed(Gtk::SelectionMode mode)
+{
+ _selector.set_visible(mode == Gtk::SELECTION_MULTIPLE);
+ _option.set_visible(mode == Gtk::SELECTION_SINGLE);
+}
+
+/**
+ * Update the connection to the parent FlowBox
+ */
+void BatchItem::on_parent_changed(Gtk::Widget *previous) {
+ auto parent = dynamic_cast<Gtk::FlowBox *>(get_parent());
+ if (!parent)
+ return;
+
+ _selection_widget_changed_conn = parent->signal_selected_children_changed().connect([=]() {
+ // Syncronise the active widget state to the Flowbox selection.
+ if (_selector.get_visible()) {
+ _selector.set_active(is_selected());
+ } else if (_option.get_visible()) {
+ _option.set_active(is_selected());
+ }
+ });
+ update_selected();
+
+ if (auto first = dynamic_cast<BatchItem *>(parent->get_child_at_index(0))) {
+ auto group = first->get_radio_group();
+ _option.set_group(group);
+ }
+}
+
+
+void BatchItem::refresh(bool hide, guint32 bg_color)
+{
+ if (_page) {
+ _preview.setBox(_page->getDocumentRect());
+ }
+
+ _preview.setBackgroundColor(bg_color);
+
+ // When hiding the preview, we show the items as a checklist
+ // So all items must be packed differently on refresh.
+ if (hide != is_hide) {
+ is_hide = hide;
+ _grid.remove(_selector);
+ _grid.remove(_option);
+ _grid.remove(_label);
+ _grid.remove(_preview);
+
+ if (hide) {
+ _selector.set_valign(Gtk::Align::ALIGN_BASELINE);
+ _label.set_xalign(0.0);
+ _grid.attach(_selector, 0, 1, 1, 1);
+ _grid.attach(_option, 0, 1, 1, 1);
+ _grid.attach(_label, 1, 1, 1, 1);
+ } else {
+ _selector.set_valign(Gtk::Align::ALIGN_END);
+ _label.set_xalign(0.5);
+ _grid.attach(_selector, 0, 1, 1, 1);
+ _grid.attach(_option, 0, 1, 1, 1);
+ _grid.attach(_label, 0, 2, 2, 1);
+ _grid.attach(_preview, 0, 0, 2, 2);
+ }
+ show_all_children();
+ update_selected();
+ }
+
+ if (!hide) {
+ _preview.queueRefresh();
+ }
+}
+
+
+BatchExport::BatchExport(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder>& builder)
+ : Gtk::Box(cobject) {
+ prefs = Inkscape::Preferences::get();
+
+ builder->get_widget("b_s_selection", selection_buttons[SELECTION_SELECTION]);
+ selection_names[SELECTION_SELECTION] = "selection";
+ builder->get_widget("b_s_layers", selection_buttons[SELECTION_LAYER]);
+ selection_names[SELECTION_LAYER] = "layer";
+ builder->get_widget("b_s_pages", selection_buttons[SELECTION_PAGE]);
+ selection_names[SELECTION_PAGE] = "page";
+
+ builder->get_widget("b_preview_box", preview_container);
+ builder->get_widget("b_show_preview", show_preview);
+ builder->get_widget("b_num_elements", num_elements);
+ builder->get_widget("b_hide_all", hide_all);
+ builder->get_widget("b_filename", filename_entry);
+ builder->get_widget("b_export", export_btn);
+ builder->get_widget("b_cancel", cancel_btn);
+ builder->get_widget("b_inprogress", progress_box);
+
+ builder->get_widget("b_progress", _prog);
+ builder->get_widget("b_progress_batch", _prog_batch);
+ builder->get_widget_derived("b_export_list", export_list);
+
+ Gtk::Button* button = nullptr;
+ builder->get_widget("b_backgnd", button);
+ assert(button);
+ _bgnd_color_picker = std::make_unique<Inkscape::UI::Widget::ColorPicker>(
+ _("Background color"), _("Color used to fill the image background"), 0xffffff00, true, button);
+ setup();
+}
+
+void BatchExport::selectionModified(Inkscape::Selection *selection, guint flags)
+{
+ if (!_desktop || _desktop->getSelection() != selection) {
+ return;
+ }
+ if (!(flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_PARENT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) {
+ return;
+ }
+ queueRefreshItems();
+}
+
+void BatchExport::selectionChanged(Inkscape::Selection *selection)
+{
+ if (!_desktop || _desktop->getSelection() != selection) {
+ return;
+ }
+ selection_buttons[SELECTION_SELECTION]->set_sensitive(!selection->isEmpty());
+ if (selection->isEmpty()) {
+ if (current_key == SELECTION_SELECTION) {
+ selection_buttons[SELECTION_LAYER]->set_active(true); // This causes refresh area
+ // return otherwise refreshArea will be called again
+ // even though we are at default key, selection is the one which was original key.
+ prefs->setString("/dialogs/export/batchexportarea/value", selection_names[SELECTION_SELECTION]);
+ return;
+ }
+ } else {
+ Glib::ustring pref_key_name = prefs->getString("/dialogs/export/batchexportarea/value");
+ if (selection_names[SELECTION_SELECTION] == pref_key_name && current_key != SELECTION_SELECTION) {
+ selection_buttons[SELECTION_SELECTION]->set_active();
+ return;
+ }
+ }
+ queueRefresh();
+}
+
+void BatchExport::pagesChanged()
+{
+ if (!_desktop || !_document) return;
+
+ bool has_pages = _document->getPageManager().hasPages();
+ selection_buttons[SELECTION_PAGE]->set_sensitive(has_pages);
+
+ if (current_key == SELECTION_PAGE && !has_pages) {
+ current_key = SELECTION_LAYER;
+ selection_buttons[SELECTION_LAYER]->set_active();
+ }
+
+ queueRefresh();
+}
+
+// Setup Single Export.Called by export on realize
+void BatchExport::setup()
+{
+ if (setupDone) {
+ return;
+ }
+ setupDone = true;
+
+ export_list->setup();
+
+ // set them before connecting to signals
+ setDefaultSelectionMode();
+ setExporting(false);
+ queueRefresh();
+
+ // Connect Signals
+ for (auto [key, button] : selection_buttons) {
+ button->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &BatchExport::onAreaTypeToggle), key));
+ }
+ show_preview->signal_toggled().connect(sigc::mem_fun(*this, &BatchExport::refreshPreview));
+ filename_conn = filename_entry->signal_changed().connect(sigc::mem_fun(*this, &BatchExport::onFilenameModified));
+ export_conn = export_btn->signal_clicked().connect(sigc::mem_fun(*this, &BatchExport::onExport));
+ cancel_conn = cancel_btn->signal_clicked().connect(sigc::mem_fun(*this, &BatchExport::onCancel));
+ browse_conn = filename_entry->signal_icon_release().connect(sigc::mem_fun(*this, &BatchExport::onBrowse));
+ hide_all->signal_toggled().connect(sigc::mem_fun(*this, &BatchExport::refreshPreview));
+ _bgnd_color_picker->connectChanged([=](guint32 color){
+ if (_desktop) {
+ Inkscape::UI::Dialog::set_export_bg_color(_desktop->getNamedView(), color);
+ }
+ refreshPreview();
+ });
+}
+
+void BatchExport::refreshItems()
+{
+ if (!_desktop || !_document) return;
+
+ // Create New List of Items
+ std::set<SPItem *> itemsList;
+ std::set<SPPage *, SPPage::PageIndexOrder> pageList;
+ std::set<SPPage *> pageUnsorted;
+
+ char *num_str = nullptr;
+ switch (current_key) {
+ case SELECTION_SELECTION: {
+ auto items = _desktop->getSelection()->items();
+ for (auto i = items.begin(); i != items.end(); ++i) {
+ if (SPItem *item = *i) {
+ // Ignore empty items (empty groups, other bad items)
+ if (item->visualBounds()) {
+ itemsList.insert(item);
+ }
+ }
+ }
+ num_str = g_strdup_printf(ngettext("%d Item", "%d Items", itemsList.size()), (int)itemsList.size());
+ break;
+ }
+ case SELECTION_LAYER: {
+ for (auto layer : _desktop->layerManager().getAllLayers()) {
+ // Ignore empty layers, they have no size.
+ if (layer->geometricBounds()) {
+ itemsList.insert(layer);
+ }
+ }
+ num_str = g_strdup_printf(ngettext("%d Layer", "%d Layers", itemsList.size()), (int)itemsList.size());
+ break;
+ }
+ case SELECTION_PAGE: {
+ for (auto page : _desktop->getDocument()->getPageManager().getPages()) {
+ pageList.insert(page);
+ pageUnsorted.insert(page);
+ }
+ num_str = g_strdup_printf(ngettext("%d Page", "%d Pages", pageList.size()), (int)pageList.size());
+ break;
+ }
+ default:
+ break;
+ }
+ if (num_str) {
+ num_elements->set_text(num_str);
+ g_free(num_str);
+ }
+
+ // Create a list of items which are already present but will be removed as they are not present anymore
+ std::vector<std::string> toRemove;
+ for (auto &[key, val] : current_items) {
+ if (SPItem *item = val->getItem()) {
+ // if item is not present in itemList add it to remove list so that we can remove it
+ auto itemItr = itemsList.find(item);
+ if (itemItr == itemsList.end() || !(*itemItr)->getId() || (*itemItr)->getId() != key) {
+ toRemove.push_back(key);
+ }
+ }
+ if (SPPage *page = val->getPage()) {
+ auto pageItr = pageUnsorted.find(page);
+ if (pageItr == pageUnsorted.end() || !(*pageItr)->getId() || (*pageItr)->getId() != key) {
+ toRemove.push_back(key);
+ }
+ }
+ }
+
+ // now remove all the items
+ for (auto const &key : toRemove) {
+ if (current_items[key]) {
+ preview_container->remove(*current_items[key]);
+ current_items.erase(key);
+ }
+ }
+
+ // now add which were are new
+ for (auto &item : itemsList) {
+ if (auto id = item->getId()) {
+ // If an Item with same Id is already present, Skip
+ if (current_items[id] && current_items[id]->getItem() == item) {
+ continue;
+ }
+ // Add new item to the end of list
+ current_items[id] = std::make_unique<BatchItem>(item, _preview_drawing);
+ preview_container->insert(*current_items[id], -1);
+ current_items[id]->set_selected(true);
+ }
+ }
+ for (auto &page : pageList) {
+ if (auto id = page->getId()) {
+ if (current_items[id] && current_items[id]->getPage() == page) {
+ continue;
+ }
+ current_items[id] = std::make_unique<BatchItem>(page, _preview_drawing);
+ preview_container->insert(*current_items[id], -1);
+ current_items[id]->set_selected(true);
+ }
+ }
+
+ refreshPreview();
+}
+
+void BatchExport::refreshPreview()
+{
+ if (!_desktop) return;
+
+ // For Batch Export we are now hiding all object except current object
+ bool hide = hide_all->get_active();
+ bool preview = show_preview->get_active();
+ preview_container->set_orientation(preview ? Gtk::ORIENTATION_HORIZONTAL : Gtk::ORIENTATION_VERTICAL);
+
+ if (preview) {
+ std::vector<SPItem *> selected;
+ for (auto &[key, val] : current_items) {
+ if (hide) {
+ // Assumption: This will never alternate between these branches in the same
+ // list of current_items. Either it's a selection, layers xor pages.
+ if (auto item = val->getItem()) {
+ selected.push_back(item);
+ } else if (val->getPage()) {
+ auto sels = _desktop->getSelection()->items();
+ selected = std::vector<SPItem *>(sels.begin(), sels.end());
+ break;
+ }
+ }
+ }
+ _preview_drawing->set_shown_items(std::move(selected));
+
+ for (auto &[key, val] : current_items) {
+ val->refresh(!preview, _bgnd_color_picker->get_current_color());
+ }
+ }
+}
+
+void BatchExport::loadExportHints()
+{
+ if (!_desktop) return;
+
+ SPDocument *doc = _desktop->getDocument();
+ auto old_filename = filename_entry->get_text();
+ if (old_filename.empty()) {
+ Glib::ustring filename = doc->getRoot()->getExportFilename();
+ if (filename.empty()) {
+ Glib::ustring filename_entry_text = filename_entry->get_text();
+ Glib::ustring extension = ".png";
+ filename = Export::defaultFilename(doc, original_name, extension);
+ }
+ filename_entry->set_text(filename);
+ filename_entry->set_position(filename.length());
+ doc_export_name = filename;
+ }
+}
+
+// Signals CallBack
+
+void BatchExport::onAreaTypeToggle(selection_mode key)
+{
+ // Prevent executing function twice
+ if (!selection_buttons[key]->get_active()) {
+ return;
+ }
+ // If you have reached here means the current key is active one ( not sure if multiple transitions happen but
+ // last call will change values)
+ current_key = key;
+ prefs->setString("/dialogs/export/batchexportarea/value", selection_names[current_key]);
+
+ queueRefresh();
+}
+
+void BatchExport::onFilenameModified()
+{
+}
+
+void BatchExport::onCancel()
+{
+ interrupted = true;
+ setExporting(false);
+}
+
+void BatchExport::onExport()
+{
+ interrupted = false;
+ if (!_desktop)
+ return;
+
+ // If there are no selected button, simply flash message in status bar
+ int num = current_items.size();
+ if (current_items.size() == 0) {
+ _desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("No items selected."));
+ return;
+ }
+
+ setExporting(true);
+
+ // Find and remove any extension from filename so that we can add suffix to it.
+ Glib::ustring filename = filename_entry->get_text();
+ export_list->removeExtension(filename);
+
+ bool hide = hide_all->get_active();
+ auto sels = _desktop->getSelection()->items();
+ std::vector<SPItem *> selected_items(sels.begin(), sels.end());
+
+ // Start Exporting Each Item
+ int num_rows = export_list->get_rows();
+ for (int j = 0; j < num_rows && !interrupted; j++) {
+
+ auto suffix = export_list->get_suffix(j);
+ auto ext = export_list->getExtension(j);
+ float dpi = export_list->get_dpi(j);
+
+ if (!ext || ext->deactivated()) {
+ continue;
+ }
+
+ int count = 0;
+ for (auto i = current_items.begin(); i != current_items.end() && !interrupted; ++i) {
+ count++;
+
+ auto &batchItem = i->second;
+ if (!batchItem->is_selected()) {
+ continue;
+ }
+
+ SPItem *item = batchItem->getItem();
+ SPPage *page = batchItem->getPage();
+
+ std::vector<SPItem *> show_only;
+ Geom::Rect area;
+ if (item) {
+ if (auto bounds = item->documentVisualBounds()) {
+ area = *bounds;
+ } else {
+ continue;
+ }
+ show_only.emplace_back(item);
+ } else if (page) {
+ area = page->getDesktopRect();
+ show_only = selected_items; // Maybe stuff here
+ } else {
+ continue;
+ }
+
+ Glib::ustring id = batchItem->getLabel();
+ if (id.empty()) {
+ continue;
+ }
+
+ Glib::ustring item_filename = filename;
+ if (!filename.empty()) {
+ Glib::ustring::value_type last_char = filename.at(filename.length() - 1);
+ if (last_char != '/' && last_char != '\\') {
+ item_filename += "_";
+ }
+ }
+ if (id.at(0) == '#' && batchItem->getItem() && !batchItem->getItem()->label()) {
+ item_filename += id.substr(1);
+ } else {
+ item_filename += id;
+ }
+
+ if (!suffix.empty()) {
+ if (ext->is_raster()) {
+ // Put the dpi in at the user's requested location.
+ suffix = std::regex_replace(suffix.c_str(), std::regex("\\{dpi\\}"), std::to_string((int)dpi));
+ }
+ item_filename = item_filename + "_" + suffix;
+ }
+
+ bool found = Export::unConflictFilename(_document, item_filename, ext->get_extension());
+ if (!found) {
+ continue;
+ }
+
+ // Set the progress bar with our updated information
+ double progress = (((double)count / num) + j) / num_rows;
+ _prog_batch->set_fraction(progress);
+
+ setExporting(true,
+ Glib::ustring::compose(_("Exporting %1"), item_filename),
+ Glib::ustring::compose(_("Format %1, Selection %2"), j + 1, count));
+
+
+ if (ext->is_raster()) {
+ unsigned long int width = (int)(area.width() * dpi / DPI_BASE + 0.5);
+ unsigned long int height = (int)(area.height() * dpi / DPI_BASE + 0.5);
+
+ Export::exportRaster(
+ area, width, height, dpi, _bgnd_color_picker->get_current_color(),
+ item_filename, true, onProgressCallback, this, ext, hide ? &show_only : nullptr);
+ } else {
+ auto copy_doc = _document->copy();
+ Export::exportVector(ext, copy_doc.get(), item_filename, true, show_only, page);
+ }
+ }
+ }
+ // Do this right at the end to finish up
+ setExporting(false);
+}
+
+void BatchExport::onBrowse(Gtk::EntryIconPosition pos, const GdkEventButton *ev)
+{
+ if (!_app || !_app->get_active_window()) {
+ return;
+ }
+ Gtk::Window *window = _app->get_active_window();
+ browse_conn.block();
+ Glib::ustring filename = Glib::filename_from_utf8(filename_entry->get_text());
+
+ if (filename.empty()) {
+ filename = Export::defaultFilename(_document, filename, ".png");
+ }
+
+ Inkscape::UI::Dialog::FileSaveDialog *dialog = Inkscape::UI::Dialog::FileSaveDialog::create(
+ *window, filename, Inkscape::UI::Dialog::EXPORT_TYPES, _("Select a filename for exporting"), "", "",
+ Inkscape::Extension::FILE_SAVE_METHOD_EXPORT);
+
+ if (dialog->show()) {
+ filename = dialog->getFilename();
+ // Remove extension and don't add a new one, for obvious reasons.
+ export_list->removeExtension(filename);
+
+ filename_entry->set_text(filename);
+ filename_entry->set_position(filename.length());
+
+ // deleting dialog before exporting is important
+ // proper delete function should be made for dialog IMO
+ delete dialog;
+ } else {
+ delete dialog;
+ }
+ browse_conn.unblock();
+}
+
+void BatchExport::setDefaultSelectionMode()
+{
+ current_key = (selection_mode)0; // default key
+ bool found = false;
+ Glib::ustring pref_key_name = prefs->getString("/dialogs/export/batchexportarea/value");
+ for (auto [key, name] : selection_names) {
+ if (pref_key_name == name) {
+ current_key = key;
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ pref_key_name = selection_names[current_key];
+ }
+ if (_desktop) {
+ if (auto _sel = _desktop->getSelection()) {
+ selection_buttons[SELECTION_SELECTION]->set_sensitive(!_sel->isEmpty());
+ }
+ selection_buttons[SELECTION_PAGE]->set_sensitive(_document->getPageManager().hasPages());
+ }
+ if (!selection_buttons[current_key]->get_sensitive()) {
+ current_key = SELECTION_LAYER;
+ }
+ selection_buttons[current_key]->set_active(true);
+
+ // we need to set pref key because signals above will set set pref == current key but we sometimes change
+ // current key like selection key
+ prefs->setString("/dialogs/export/batchexportarea/value", pref_key_name);
+}
+
+void BatchExport::setExporting(bool exporting, Glib::ustring const &text, Glib::ustring const &text_batch)
+{
+ if (exporting) {
+ set_sensitive(false);
+ set_opacity(0.2);
+ progress_box->show();
+ _prog->set_text(text);
+ _prog->set_fraction(0.0);
+ _prog_batch->set_text(text_batch);
+ } else {
+ set_sensitive(true);
+ set_opacity(1.0);
+ progress_box->hide();
+ _prog->set_text("");
+ _prog->set_fraction(0.0);
+ _prog_batch->set_text("");
+ }
+}
+
+unsigned int BatchExport::onProgressCallback(float value, void *data)
+{
+ if (auto bi = static_cast<BatchExport *>(data)) {
+ bi->_prog->set_fraction(value);
+ Gtk::Main::iteration(false);
+ return !bi->interrupted;
+ }
+ return false;
+}
+
+void BatchExport::setDesktop(SPDesktop *desktop)
+{
+ if (desktop != _desktop) {
+ _pages_changed_connection.disconnect();
+ _desktop = desktop;
+ }
+}
+
+void BatchExport::setDocument(SPDocument *document)
+{
+ if (!_desktop) {
+ document = nullptr;
+ }
+ if (_document == document)
+ return;
+
+ _document = document;
+ _pages_changed_connection.disconnect();
+ if (document) {
+ // when the page selected is changed, update the export area
+ _pages_changed_connection = document->getPageManager().connectPagesChanged([=]() { pagesChanged(); });
+
+ auto bg_color = get_export_bg_color(document->getNamedView(), 0xffffff00);
+ _bgnd_color_picker->setRgba32(bg_color);
+ _preview_drawing = std::make_shared<PreviewDrawing>(document);
+ } else {
+ _preview_drawing.reset();
+ }
+
+ refreshItems();
+}
+
+void BatchExport::queueRefreshItems()
+{
+ if (refresh_items_conn) {
+ return;
+ }
+ // Asynchronously refresh the preview
+ refresh_items_conn = Glib::signal_idle().connect([this] {
+ refreshItems();
+ return false;
+ }, Glib::PRIORITY_HIGH);
+}
+
+void BatchExport::queueRefresh()
+{
+ if (refresh_conn) {
+ return;
+ }
+ refresh_conn = Glib::signal_idle().connect([this] {
+ refreshItems();
+ loadExportHints();
+ return false;
+ }, Glib::PRIORITY_HIGH);
+}
+
+} // 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/export-batch.h b/src/ui/dialog/export-batch.h
new file mode 100644
index 0000000..78c6d80
--- /dev/null
+++ b/src/ui/dialog/export-batch.h
@@ -0,0 +1,182 @@
+// 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>
+ * Anshudhar Kumar Singh <anshudhar2001@gmail.com>
+ *
+ * Copyright (C) 1999-2007, 2021 Authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SP_EXPORT_BATCH_H
+#define SP_EXPORT_BATCH_H
+
+#include <gtkmm.h>
+
+#include "helper/auto-connection.h"
+#include "ui/widget/export-preview.h"
+#include "ui/widget/scrollprotected.h"
+
+
+class ExportProgressDialog;
+class InkscapeApplication;
+class SPDesktop;
+class SPDocument;
+class SPItem;
+class SPPage;
+
+namespace Inkscape {
+class Preferences;
+class Selection;
+
+namespace UI {
+
+namespace Widget {
+class ColorPicker;
+} // namespace Widget
+
+namespace Dialog {
+
+class ExportList;
+
+class BatchItem : public Gtk::FlowBoxChild
+{
+public:
+ BatchItem(SPItem *item, std::shared_ptr<PreviewDrawing> drawing);
+ BatchItem(SPPage *page, std::shared_ptr<PreviewDrawing> drawing);
+ ~BatchItem() override = default;
+
+ Glib::ustring getLabel() { return _label_str; }
+ SPItem *getItem() { return _item; }
+ SPPage *getPage() { return _page; }
+ void refresh(bool hide, guint32 bg_color);
+ void setDrawing(std::shared_ptr<PreviewDrawing> drawing) { _preview.setDrawing(drawing); }
+
+ auto get_radio_group() { return _option.get_group(); }
+ void on_parent_changed(Gtk::Widget *) override;
+ void on_mode_changed(Gtk::SelectionMode mode);
+ void set_selected(bool selected);
+ void update_selected();
+
+private:
+ void init(std::shared_ptr<PreviewDrawing> drawing);
+ void update_label();
+
+ Glib::ustring _label_str;
+ Gtk::Grid _grid;
+ Gtk::Label _label;
+ Gtk::CheckButton _selector;
+ Gtk::RadioButton _option;
+ ExportPreview _preview;
+ SPItem *_item = nullptr;
+ SPPage *_page = nullptr;
+ bool is_hide = false;
+
+ auto_connection _selection_widget_changed_conn;
+ auto_connection _object_modified_conn;
+};
+
+class BatchExport : public Gtk::Box
+{
+public:
+ BatchExport(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder>& builder);
+ ~BatchExport() override = default;
+
+ void setApp(InkscapeApplication *app) { _app = app; }
+ void setDocument(SPDocument *document);
+ void setDesktop(SPDesktop *desktop);
+ void selectionChanged(Inkscape::Selection *selection);
+ void selectionModified(Inkscape::Selection *selection, guint flags);
+ void pagesChanged();
+ void queueRefreshItems();
+ void queueRefresh();
+
+private:
+ enum selection_mode
+ {
+ SELECTION_LAYER = 0, // Default is alaways placed first
+ SELECTION_SELECTION,
+ SELECTION_PAGE,
+ };
+
+ typedef Inkscape::UI::Widget::ScrollProtected<Gtk::SpinButton> SpinButton;
+
+ InkscapeApplication *_app;
+ SPDesktop *_desktop = nullptr;
+ SPDocument *_document = nullptr;
+ std::shared_ptr<PreviewDrawing> _preview_drawing;
+ bool setupDone = false; // To prevent setup() call add connections again.
+
+ std::map<selection_mode, Gtk::RadioButton *> selection_buttons;
+ Gtk::FlowBox *preview_container = nullptr;
+ Gtk::CheckButton *show_preview = nullptr;
+ Gtk::Label *num_elements = nullptr;
+ Gtk::CheckButton *hide_all = nullptr;
+ Gtk::Entry *filename_entry = nullptr;
+ Gtk::Button *export_btn = nullptr;
+ Gtk::Button *cancel_btn = nullptr;
+ Gtk::ProgressBar *_prog = nullptr;
+ Gtk::ProgressBar *_prog_batch = nullptr;
+ ExportList *export_list = nullptr;
+ Gtk::Widget *progress_box = nullptr;
+
+ // Store all items to be displayed in flowbox
+ std::map<std::string, std::unique_ptr<BatchItem>> current_items;
+
+ Glib::ustring original_name;
+ Glib::ustring doc_export_name;
+
+ Inkscape::Preferences *prefs = nullptr;
+ std::map<selection_mode, Glib::ustring> selection_names;
+ selection_mode current_key;
+
+ // initialise variables from builder
+ void initialise(const Glib::RefPtr<Gtk::Builder> &builder);
+ void setup();
+ void setDefaultSelectionMode();
+ void onFilenameModified();
+ void onAreaTypeToggle(selection_mode key);
+ void onExport();
+ void onCancel();
+ void onBrowse(Gtk::EntryIconPosition pos, const GdkEventButton *ev);
+
+ void refreshPreview();
+ void refreshItems();
+ void loadExportHints();
+
+ void setExporting(bool exporting, Glib::ustring const &text = "", Glib::ustring const &test_batch = "");
+
+ static unsigned int onProgressCallback(float value, void *);
+
+ bool interrupted;
+
+ // Gtk Signals
+ auto_connection filename_conn;
+ auto_connection export_conn;
+ auto_connection cancel_conn;
+ auto_connection browse_conn;
+ auto_connection refresh_conn;
+ auto_connection refresh_items_conn;
+ // SVG Signals
+ auto_connection _pages_changed_connection;
+
+ std::unique_ptr<Inkscape::UI::Widget::ColorPicker> _bgnd_color_picker;
+};
+} // 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/export-single.cpp b/src/ui/dialog/export-single.cpp
new file mode 100644
index 0000000..c37548c
--- /dev/null
+++ b/src/ui/dialog/export-single.cpp
@@ -0,0 +1,1058 @@
+// 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>
+ * Anshudhar Kumar Singh <anshudhar2001@gmail.com>
+ *
+ * Copyright (C) 1999-2007, 2021 Authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "export-single.h"
+
+#include <glibmm/convert.h>
+#include <glibmm/i18n.h>
+#include <glibmm/miscutils.h>
+#include <gtkmm.h>
+#include <png.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "extension/db.h"
+#include "extension/output.h"
+#include "file.h"
+#include "helper/png-write.h"
+#include "inkscape-window.h"
+#include "inkscape.h"
+#include "io/resource.h"
+#include "io/sys.h"
+#include "message-stack.h"
+#include "object/object-set.h"
+#include "object/sp-namedview.h"
+#include "object/sp-root.h"
+#include "object/sp-page.h"
+#include "page-manager.h"
+#include "preferences.h"
+#include "selection-chemistry.h"
+#include "ui/dialog-events.h"
+#include "ui/dialog/dialog-notebook.h"
+#include "ui/dialog/export.h"
+#include "ui/dialog/filedialog.h"
+#include "ui/icon-names.h"
+#include "ui/interface.h"
+#include "ui/widget/color-picker.h"
+#include "ui/widget/export-lists.h"
+#include "ui/widget/export-preview.h"
+#include "ui/dialog/export-batch.h"
+#include "ui/widget/scrollprotected.h"
+#include "ui/widget/unit-menu.h"
+#ifdef _WIN32
+
+#endif
+
+using Inkscape::Util::unit_table;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+
+SingleExport::SingleExport(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder>& builder)
+ : Gtk::Box(cobject) {
+ prefs = Inkscape::Preferences::get();
+
+ builder->get_widget("si_s_document", selection_buttons[SELECTION_DRAWING]);
+ selection_names[SELECTION_DRAWING] = "drawing";
+ builder->get_widget("si_s_page", selection_buttons[SELECTION_PAGE]);
+ selection_names[SELECTION_PAGE] = "page";
+ builder->get_widget("si_s_selection", selection_buttons[SELECTION_SELECTION]);
+ selection_names[SELECTION_SELECTION] = "selection";
+ builder->get_widget("si_s_custom", selection_buttons[SELECTION_CUSTOM]);
+ selection_names[SELECTION_CUSTOM] = "custom";
+
+ builder->get_widget_derived("si_left_sb", spin_buttons[SPIN_X0]);
+ builder->get_widget_derived("si_right_sb", spin_buttons[SPIN_X1]);
+ builder->get_widget_derived("si_top_sb", spin_buttons[SPIN_Y0]);
+ builder->get_widget_derived("si_bottom_sb", spin_buttons[SPIN_Y1]);
+ builder->get_widget_derived("si_height_sb", spin_buttons[SPIN_HEIGHT]);
+ builder->get_widget_derived("si_width_sb", spin_buttons[SPIN_WIDTH]);
+
+ builder->get_widget("si_label_left", spin_labels[SPIN_X0]);
+ builder->get_widget("si_label_right", spin_labels[SPIN_X1]);
+ builder->get_widget("si_label_top", spin_labels[SPIN_Y0]);
+ builder->get_widget("si_label_bottom", spin_labels[SPIN_Y1]);
+ builder->get_widget("si_label_height", spin_labels[SPIN_HEIGHT]);
+ builder->get_widget("si_label_width", spin_labels[SPIN_WIDTH]);
+
+ builder->get_widget_derived("si_img_height_sb", spin_buttons[SPIN_BMHEIGHT]);
+ builder->get_widget_derived("si_img_width_sb", spin_buttons[SPIN_BMWIDTH]);
+ builder->get_widget_derived("si_dpi_sb", spin_buttons[SPIN_DPI]);
+
+ builder->get_widget("si_pages", pages_list);
+ builder->get_widget("si_pages_box", pages_list_box);
+ builder->get_widget("si_sizes", size_box);
+
+ builder->get_widget_derived("si_units", units);
+ builder->get_widget("si_units_row", si_units_row);
+
+ builder->get_widget("si_hide_all", si_hide_all);
+ builder->get_widget("si_show_preview", si_show_preview);
+ builder->get_widget_derived("si_preview", preview);
+ builder->get_widget("si_preview_box", preview_box);
+
+ builder->get_widget_derived("si_extention", si_extension_cb);
+ Gtk::Box *pref_button_box = nullptr;
+ builder->get_widget("si_prefs", pref_button_box);
+ pref_button_box->add(*si_extension_cb->getPrefButton());
+
+ builder->get_widget("si_filename", si_filename_entry);
+ builder->get_widget("si_export", si_export);
+
+ builder->get_widget("si_progress", _prog);
+ builder->get_widget("si_cancel", cancel_button);
+ builder->get_widget("si_inprogress", progress_box);
+
+ Gtk::Button* button = nullptr;
+ builder->get_widget("si_backgnd", button);
+ _bgnd_color_picker = std::make_unique<Inkscape::UI::Widget::ColorPicker>(
+ _("Background color"), _("Color used to fill background"), 0xffffff00, true, button);
+
+ setup();
+}
+
+// Inkscape Selection Modified CallBack
+void SingleExport::selectionModified(Inkscape::Selection *selection, guint flags)
+{
+ if (!_desktop || _desktop->getSelection() != selection) {
+ return;
+ }
+ if (!(flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_PARENT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) {
+ return;
+ }
+ refreshArea();
+ // Do not load export hits for modifications
+}
+
+void SingleExport::selectionChanged(Inkscape::Selection *selection)
+{
+ if (!_desktop || _desktop->getSelection() != selection) {
+ return;
+ }
+
+ Glib::ustring pref_key_name = prefs->getString("/dialogs/export/exportarea/value");
+ for (auto [key, name] : selection_names) {
+ if (name == pref_key_name && current_key != key && key != SELECTION_SELECTION) {
+ selection_buttons[key]->set_active(true);
+ current_key = key;
+ break;
+ }
+ }
+ if (selection->isEmpty()) {
+ selection_buttons[SELECTION_SELECTION]->set_sensitive(false);
+ if (current_key == SELECTION_SELECTION) {
+ selection_buttons[(selection_mode)0]->set_active(true); // This causes refresh area
+ // even though we are at default key, selection is the one which was original key.
+ prefs->setString("/dialogs/export/exportarea/value", selection_names[SELECTION_SELECTION]);
+ // return otherwise refreshArea will be called again
+ return;
+ }
+ } else {
+ selection_buttons[SELECTION_SELECTION]->set_sensitive(true);
+ if (selection_names[SELECTION_SELECTION] == pref_key_name && current_key != SELECTION_SELECTION) {
+ selection_buttons[SELECTION_SELECTION]->set_active();
+ return;
+ }
+ }
+
+ refreshArea();
+ loadExportHints();
+}
+
+// Setup Single Export.Called by export on realize
+void SingleExport::setup()
+{
+ if (setupDone) {
+ // We need to setup only once
+ return;
+ }
+ setupDone = true;
+
+ si_extension_cb->setup();
+
+ setupUnits();
+ setupSpinButtons();
+
+ // set them before connecting to signals
+ setDefaultSelectionMode();
+ setPagesMode(false);
+ setExporting(false);
+
+ // Refresh the filename when the user selects a different page
+ _pages_list_changed = pages_list->signal_selected_children_changed().connect([=]() {
+ loadExportHints();
+ refreshArea();
+ });
+
+ // Connect Signals Here
+ for (auto [key, button] : selection_buttons) {
+ button->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SingleExport::onAreaTypeToggle), key));
+ }
+ units->signal_changed().connect(sigc::mem_fun(*this, &SingleExport::onUnitChanged));
+ extensionConn = si_extension_cb->signal_changed().connect(sigc::mem_fun(*this, &SingleExport::onExtensionChanged));
+ exportConn = si_export->signal_clicked().connect(sigc::mem_fun(*this, &SingleExport::onExport));
+ filenameConn = si_filename_entry->signal_changed().connect(sigc::mem_fun(*this, &SingleExport::onFilenameModified));
+ browseConn = si_filename_entry->signal_icon_release().connect(sigc::mem_fun(*this, &SingleExport::onBrowse));
+ cancelConn = cancel_button->signal_clicked().connect(sigc::mem_fun(*this, &SingleExport::onCancel));
+ si_filename_entry->signal_activate().connect(sigc::mem_fun(*this, &SingleExport::onExport));
+ si_show_preview->signal_toggled().connect(sigc::mem_fun(*this, &SingleExport::refreshPreview));
+ si_hide_all->signal_toggled().connect(sigc::mem_fun(*this, &SingleExport::refreshPreview));
+ _bgnd_color_picker->connectChanged([=](guint32 color){
+ if (_desktop) {
+ Inkscape::UI::Dialog::set_export_bg_color(_desktop->getNamedView(), color);
+ }
+ refreshPreview();
+ });
+}
+
+// Setup units combobox
+void SingleExport::setupUnits()
+{
+ units->setUnitType(Inkscape::Util::UNIT_TYPE_LINEAR);
+ if (_desktop) {
+ units->setUnit(_desktop->getNamedView()->display_units->abbr);
+ }
+}
+
+// Create all spin buttons
+void SingleExport::setupSpinButtons()
+{
+ setupSpinButton<sb_type>(spin_buttons[SPIN_X0], 0.0, -1000000.0, 1000000.0, 0.1, 1.0, EXPORT_COORD_PRECISION, true,
+ &SingleExport::onAreaXChange, SPIN_X0);
+ setupSpinButton<sb_type>(spin_buttons[SPIN_X1], 0.0, -1000000.0, 1000000.0, 0.1, 1.0, EXPORT_COORD_PRECISION, true,
+ &SingleExport::onAreaXChange, SPIN_X1);
+ setupSpinButton<sb_type>(spin_buttons[SPIN_Y0], 0.0, -1000000.0, 1000000.0, 0.1, 1.0, EXPORT_COORD_PRECISION, true,
+ &SingleExport::onAreaYChange, SPIN_Y0);
+ setupSpinButton<sb_type>(spin_buttons[SPIN_Y1], 0.0, -1000000.0, 1000000.0, 0.1, 1.0, EXPORT_COORD_PRECISION, true,
+ &SingleExport::onAreaYChange, SPIN_Y1);
+
+ setupSpinButton<sb_type>(spin_buttons[SPIN_HEIGHT], 0.0, 0.0, PNG_UINT_31_MAX, 0.1, 1.0, EXPORT_COORD_PRECISION,
+ true, &SingleExport::onAreaYChange, SPIN_HEIGHT);
+ setupSpinButton<sb_type>(spin_buttons[SPIN_WIDTH], 0.0, 0.0, PNG_UINT_31_MAX, 0.1, 1.0, EXPORT_COORD_PRECISION,
+ true, &SingleExport::onAreaXChange, SPIN_WIDTH);
+
+ setupSpinButton<sb_type>(spin_buttons[SPIN_BMHEIGHT], 1.0, 1.0, 1000000.0, 1.0, 10.0, 0, true,
+ &SingleExport::onDpiChange, SPIN_BMHEIGHT);
+ setupSpinButton<sb_type>(spin_buttons[SPIN_BMWIDTH], 1.0, 1.0, 1000000.0, 1.0, 10.0, 0, true,
+ &SingleExport::onDpiChange, SPIN_BMWIDTH);
+ setupSpinButton<sb_type>(spin_buttons[SPIN_DPI], prefs->getDouble("/dialogs/export/defaultxdpi/value", DPI_BASE),
+ 1.0, 100000.0, 0.1, 1.0, 2, true, &SingleExport::onDpiChange, SPIN_DPI);
+}
+
+template <typename T>
+void SingleExport::setupSpinButton(Gtk::SpinButton *sb, double val, double min, double max, double step, double page,
+ int digits, bool sensitive, void (SingleExport::*cb)(T), T param)
+{
+ if (sb) {
+ sb->set_digits(digits);
+ sb->set_increments(step, page);
+ sb->set_range(min, max);
+ sb->set_value(val);
+ sb->set_sensitive(sensitive);
+ sb->set_width_chars(0);
+ sb->set_max_width_chars(0);
+ if (cb) {
+ auto signal = sb->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, cb), param));
+ // add signals to list to block all easily
+ spinButtonConns.push_back(signal);
+ }
+ }
+}
+
+void SingleExport::refreshArea()
+{
+ if (_document) {
+ Geom::OptRect bbox;
+ auto sel = getSelectedPages();
+
+ switch (current_key) {
+ case SELECTION_SELECTION:
+ if ((_desktop->getSelection())->isEmpty() == false) {
+ bbox = _desktop->getSelection()->visualBounds();
+ break;
+ }
+ case SELECTION_DRAWING:
+ bbox = _document->getRoot()->desktopVisualBounds();
+ if (bbox) {
+ break;
+ }
+ case SELECTION_PAGE:
+ // If the page is set in the multi-selection use that.
+ if (sel.size() == 1) {
+ bbox = sel[0]->getDesktopRect();
+ } else {
+ bbox = _document->getPageManager().getSelectedPageRect();
+ }
+ break;
+ case SELECTION_CUSTOM:
+ break;
+ default:
+ break;
+ }
+ if (current_key != SELECTION_CUSTOM && bbox) {
+ setArea(bbox->min()[Geom::X], bbox->min()[Geom::Y], bbox->max()[Geom::X], bbox->max()[Geom::Y]);
+ }
+ }
+ refreshPreview();
+}
+
+void SingleExport::refreshPage()
+{
+ if (!_document)
+ return;
+
+ bool multi = pages_list->get_selection_mode() == Gtk::SELECTION_MULTIPLE;
+ auto &pm = _document->getPageManager();
+ bool has_pages = current_key == SELECTION_PAGE && pm.getPageCount() > 1;
+ pages_list_box->set_visible(has_pages);
+ preview_box->set_visible(!has_pages);
+ size_box->set_visible(!has_pages || !multi);
+}
+
+void SingleExport::setPagesMode(bool multi)
+{
+ // Set set the internal mode to NONE to preserve selections while changing
+ pages_list->foreach([=](Gtk::Widget& widget) {
+ if (auto item = dynamic_cast<BatchItem *>(&widget))
+ item->on_mode_changed(Gtk::SELECTION_NONE);
+ });
+ pages_list->set_selection_mode(multi ? Gtk::SELECTION_MULTIPLE : Gtk::SELECTION_SINGLE);
+ // A second call it needed in it's own loop because of how updates happen in the FlowBox
+ pages_list->foreach([=](Gtk::Widget& widget) {
+ if (auto item = dynamic_cast<BatchItem *>(&widget))
+ item->update_selected();
+ });
+ refreshPage();
+}
+
+void SingleExport::selectPage(SPPage *page)
+{
+ pages_list->foreach([=](Gtk::Widget& widget) {
+ if (auto item = dynamic_cast<BatchItem *>(&widget)) {
+ if (item->getPage() == page) {
+ item->set_selected(true);
+ }
+ }
+ });
+}
+
+std::vector<SPPage *> SingleExport::getSelectedPages()
+{
+ std::vector<SPPage *> pages;
+ pages_list->selected_foreach([&pages](Gtk::FlowBox *box, Gtk::FlowBoxChild *child) {
+ if (auto item = dynamic_cast<BatchItem *>(child))
+ pages.push_back(item->getPage());
+ });
+ return pages;
+}
+
+/**
+ * Clear all page preview widgets and halting any in-progress updates.
+ */
+void SingleExport::clearPagePreviews()
+{
+ _pages_list_changed.block();
+ while (auto widget = pages_list->get_child_at_index(0)) {
+ pages_list->remove(*widget);
+ }
+ _pages_list_changed.unblock();
+}
+
+void SingleExport::onPagesChanged()
+{
+ clearPagePreviews();
+ if (!_document)
+ return;
+ _pages_list_changed.block();
+ auto &pm = _document->getPageManager();
+ if (pm.getPageCount() > 1) {
+ for (auto page : pm.getPages()) {
+ auto item = Gtk::manage(new BatchItem(page, _preview_drawing));
+ pages_list->insert(*item, -1);
+ }
+ }
+ refreshPage();
+ if (auto ext = si_extension_cb->getExtension()) {
+ setPagesMode(!ext->is_raster());
+ }
+ _pages_list_changed.unblock();
+}
+
+void SingleExport::onPagesModified(SPPage *page)
+{
+ refreshArea();
+}
+
+void SingleExport::onPagesSelected(SPPage *page) {
+ if (pages_list->get_selection_mode() != Gtk::SELECTION_MULTIPLE) {
+ selectPage(page);
+ }
+ refreshArea();
+}
+
+void SingleExport::loadExportHints()
+{
+ if (filename_modified || !_document || !_desktop) return;
+
+ Glib::ustring old_filename = si_filename_entry->get_text();
+ Glib::ustring filename;
+ Geom::Point dpi;
+ switch (current_key) {
+ case SELECTION_PAGE:
+ {
+ auto pages = getSelectedPages();
+ if (pages.size() == 1) {
+ dpi = pages[0]->getExportDpi();
+ filename = pages[0]->getExportFilename();
+ if (filename.empty()) {
+ filename = Export::filePathFromId(_document, pages[0]->getLabel(), old_filename);
+ }
+ break;
+ }
+ // No or many pages means output is drawing, continue.
+ }
+ case SELECTION_CUSTOM:
+ case SELECTION_DRAWING:
+ {
+ dpi = _document->getRoot()->getExportDpi();
+ filename = _document->getRoot()->getExportFilename();
+ break;
+ }
+ case SELECTION_SELECTION:
+ {
+ auto selection = _desktop->getSelection();
+ if (selection->isEmpty()) break;
+
+ // Get filename and dpi from selected items
+ for (auto item : selection->items()) {
+ if (!dpi.x()) {
+ dpi = item->getExportDpi();
+ }
+ if (filename.empty()) {
+ filename = item->getExportFilename();
+ }
+ }
+
+ if (filename.empty()) {
+ filename = Export::filePathFromObject(_document, selection->firstItem(), old_filename);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ if (filename.empty()) {
+ filename = Export::defaultFilename(_document, old_filename, ".png");
+ }
+ if (auto ext = si_extension_cb->getExtension()) {
+ si_extension_cb->removeExtension(filename);
+ ext->add_extension(filename);
+ }
+
+ original_name = filename;
+ si_filename_entry->set_text(filename);
+ si_filename_entry->set_position(filename.length());
+
+ if (dpi.x() != 0.0) { // XXX Should this deal with dpi.y() ?
+ spin_buttons[SPIN_DPI]->set_value(dpi.x());
+ }
+}
+
+void SingleExport::saveExportHints(SPObject *target)
+{
+ if (target) {
+ target->setExportFilename(si_filename_entry->get_text());
+ target->setExportDpi(Geom::Point(
+ spin_buttons[SPIN_DPI]->get_value(),
+ spin_buttons[SPIN_DPI]->get_value()
+ ));
+ }
+}
+
+void SingleExport::setArea(double x0, double y0, double x1, double y1)
+{
+ blockSpinConns(true);
+
+ Unit const *unit = units->getUnit();
+ auto px = unit_table.getUnit("px");
+ spin_buttons[SPIN_X0]->get_adjustment()->set_value(px->convert(x0, unit));
+ spin_buttons[SPIN_X1]->get_adjustment()->set_value(px->convert(x1, unit));
+ spin_buttons[SPIN_Y0]->get_adjustment()->set_value(px->convert(y0, unit));
+ spin_buttons[SPIN_Y1]->get_adjustment()->set_value(px->convert(y1, unit));
+
+ areaXChange(SPIN_X1);
+ areaYChange(SPIN_Y1);
+
+ blockSpinConns(false);
+}
+
+// Signals CallBack
+
+void SingleExport::onUnitChanged()
+{
+ refreshArea();
+}
+
+void SingleExport::onAreaTypeToggle(selection_mode key)
+{
+ // Prevent executing function twice
+ if (!selection_buttons[key]->get_active()) {
+ return;
+ }
+ // If you have reached here means the current key is active one ( not sure if multiple transitions happen but
+ // last call will change values)
+ current_key = key;
+ prefs->setString("/dialogs/export/exportarea/value", selection_names[current_key]);
+
+ refreshArea();
+ loadExportHints();
+ toggleSpinButtonVisibility();
+ refreshPage();
+}
+
+void SingleExport::toggleSpinButtonVisibility()
+{
+ bool show = current_key == SELECTION_CUSTOM;
+ spin_buttons[SPIN_X0]->set_visible(show);
+ spin_buttons[SPIN_X1]->set_visible(show);
+ spin_buttons[SPIN_Y0]->set_visible(show);
+ spin_buttons[SPIN_Y1]->set_visible(show);
+ spin_buttons[SPIN_WIDTH]->set_visible(show);
+ spin_buttons[SPIN_HEIGHT]->set_visible(show);
+
+ spin_labels[SPIN_X0]->set_visible(show);
+ spin_labels[SPIN_X1]->set_visible(show);
+ spin_labels[SPIN_Y0]->set_visible(show);
+ spin_labels[SPIN_Y1]->set_visible(show);
+ spin_labels[SPIN_WIDTH]->set_visible(show);
+ spin_labels[SPIN_HEIGHT]->set_visible(show);
+
+ si_units_row->set_visible(show);
+}
+
+void SingleExport::onAreaXChange(sb_type type)
+{
+ blockSpinConns(true);
+ areaXChange(type);
+ selection_buttons[SELECTION_CUSTOM]->set_active(true);
+ refreshPreview();
+ blockSpinConns(false);
+}
+void SingleExport::onAreaYChange(sb_type type)
+{
+ blockSpinConns(true);
+ areaYChange(type);
+ selection_buttons[SELECTION_CUSTOM]->set_active(true);
+ refreshPreview();
+ blockSpinConns(false);
+}
+void SingleExport::onDpiChange(sb_type type)
+{
+ blockSpinConns(true);
+ dpiChange(type);
+ blockSpinConns(false);
+}
+
+void SingleExport::onFilenameModified()
+{
+ extensionConn.block();
+ Glib::ustring filename = si_filename_entry->get_text();
+
+ if (original_name == filename) {
+ filename_modified = false;
+ } else {
+ filename_modified = true;
+ }
+
+ si_extension_cb->setExtensionFromFilename(filename);
+
+ extensionConn.unblock();
+}
+
+void SingleExport::onExtensionChanged()
+{
+ if (auto ext = si_extension_cb->getExtension()) {
+ setPagesMode(!ext->is_raster());
+ loadExportHints();
+ }
+}
+
+void SingleExport::onCancel()
+{
+ interrupted = true;
+ setExporting(false);
+}
+
+void SingleExport::onExport()
+{
+ interrupted = false;
+ if (!_desktop || !_document)
+ return;
+
+ auto &page_manager = _document->getPageManager();
+ auto selection = _desktop->getSelection();
+ bool exportSuccessful = false;
+ auto omod = si_extension_cb->getExtension();
+ if (!omod) {
+ return;
+ }
+
+ setExporting(true, _("Exporting"));
+
+ bool selected_only = si_hide_all->get_active();
+ Unit const *unit = units->getUnit();
+ Glib::ustring filename = si_filename_entry->get_text();
+
+ float x0 = unit->convert(spin_buttons[SPIN_X0]->get_value(), "px");
+ float x1 = unit->convert(spin_buttons[SPIN_X1]->get_value(), "px");
+ float y0 = unit->convert(spin_buttons[SPIN_Y0]->get_value(), "px");
+ float y1 = unit->convert(spin_buttons[SPIN_Y1]->get_value(), "px");
+ auto area = Geom::Rect(Geom::Point(x0, y0), Geom::Point(x1, y1));
+
+ if (omod->is_raster()) {
+ area *= _desktop->dt2doc();
+ unsigned long int width = int(spin_buttons[SPIN_BMWIDTH]->get_value() + 0.5);
+ unsigned long int height = int(spin_buttons[SPIN_BMHEIGHT]->get_value() + 0.5);
+
+ float dpi = spin_buttons[SPIN_DPI]->get_value();
+
+ setExporting(true, Glib::ustring::compose(_("Exporting %1 (%2 x %3)"), filename, width, height));
+
+ std::vector<SPItem *> selected(selection->items().begin(), selection->items().end());
+
+ exportSuccessful = Export::exportRaster(
+ area, width, height, dpi, _bgnd_color_picker->get_current_color(),
+ filename, false, onProgressCallback, this,
+ omod, selected_only ? &selected : nullptr);
+
+ } else {
+ setExporting(true, Glib::ustring::compose(_("Exporting %1"), filename));
+
+ auto copy_doc = _document->copy();
+
+ std::vector<SPItem *> items;
+ if (selected_only) {
+ auto itemlist = selection->items();
+ for (auto i = itemlist.begin(); i != itemlist.end(); ++i) {
+ SPItem *item = *i;
+ items.push_back(item);
+ }
+ }
+
+ if (current_key == SELECTION_PAGE && page_manager.getPageCount() > 1) {
+ auto pages = getSelectedPages();
+ exportSuccessful = Export::exportVector(omod, copy_doc.get(), filename, false, items, pages);
+ } else {
+ // To get the right kind of export, we're going to make a page
+ // This allows all the same raster options to work for vectors
+ auto page = copy_doc->getPageManager().newDocumentPage(area);
+ exportSuccessful = Export::exportVector(omod, copy_doc.get(), filename, false, items, page);
+ }
+ }
+ // Save the export hints back to the svg document
+ if (exportSuccessful) {
+
+ std::string path = Export::absolutizePath(_document, Glib::filename_from_utf8(filename));
+ auto recentmanager = Gtk::RecentManager::get_default();
+ if (recentmanager && Glib::path_is_absolute(path)) {
+ Glib::ustring uri = Glib::filename_to_uri(path);
+ recentmanager->add_item(uri);
+ }
+
+ SPObject *target;
+ switch (current_key) {
+ case SELECTION_CUSTOM:
+ case SELECTION_DRAWING:
+ target = _document->getRoot();
+ break;
+ case SELECTION_PAGE:
+ target = page_manager.getSelected();
+ if (!target)
+ target = _document->getRoot();
+ break;
+ case SELECTION_SELECTION:
+ target = _desktop->getSelection()->firstItem();
+ break;
+ default:
+ break;
+ }
+ if (target) {
+ saveExportHints(target);
+ DocumentUndo::done(_document, _("Set Export Options"), INKSCAPE_ICON("export"));
+ }
+ }
+ setExporting(false);
+ original_name = filename;
+ filename_modified = false;
+ interrupted = false;
+}
+
+void SingleExport::onBrowse(Gtk::EntryIconPosition pos, const GdkEventButton *ev)
+{
+ if (!_app || !_app->get_active_window() || !_document) {
+ return;
+ }
+ Gtk::Window *window = _app->get_active_window();
+ browseConn.block();
+ Glib::ustring filename = Glib::filename_from_utf8(si_filename_entry->get_text());
+
+ if (filename.empty()) {
+ filename = Export::defaultFilename(_document, filename, ".png");
+ }
+
+ Inkscape::UI::Dialog::FileSaveDialog *dialog = Inkscape::UI::Dialog::FileSaveDialog::create(
+ *window, filename, Inkscape::UI::Dialog::EXPORT_TYPES, _("Select a filename for exporting"), "", "",
+ Inkscape::Extension::FILE_SAVE_METHOD_EXPORT);
+
+ // Tell the browse dialog what extension to start with
+ if (auto omod = si_extension_cb->getExtension()) {
+ dialog->setExtension(omod);
+ }
+
+ if (dialog->show()) {
+ filename = dialog->getFilename();
+ // Once complete, we use the extension selected to save the file
+ if (auto ext = dialog->getExtension()) {
+ si_extension_cb->set_active_id(ext->get_id());
+ } else {
+ si_extension_cb->setExtensionFromFilename(filename);
+ }
+
+ si_filename_entry->set_text(filename);
+ si_filename_entry->set_position(filename.length());
+
+ // deleting dialog before exporting is important
+ delete dialog;
+ onExport();
+ } else {
+ delete dialog;
+ }
+ browseConn.unblock();
+}
+
+// Utils Functions
+
+void SingleExport::blockSpinConns(bool status = true)
+{
+ for (auto signal : spinButtonConns) {
+ if (status) {
+ signal.block();
+ } else {
+ signal.unblock();
+ }
+ }
+}
+
+void SingleExport::areaXChange(sb_type type)
+{
+ auto x0_adj = spin_buttons[SPIN_X0]->get_adjustment();
+ auto x1_adj = spin_buttons[SPIN_X1]->get_adjustment();
+ auto width_adj = spin_buttons[SPIN_WIDTH]->get_adjustment();
+
+ float x0, x1, dpi, width, bmwidth;
+
+ // Get all values in px
+ Unit const *unit = units->getUnit();
+ x0 = unit->convert(x0_adj->get_value(), "px");
+ x1 = unit->convert(x1_adj->get_value(), "px");
+ width = unit->convert(width_adj->get_value(), "px");
+ bmwidth = spin_buttons[SPIN_BMWIDTH]->get_value();
+ dpi = spin_buttons[SPIN_DPI]->get_value();
+
+ switch (type) {
+ case SPIN_X0:
+ bmwidth = (x1 - x0) * dpi / DPI_BASE;
+ if (bmwidth < SP_EXPORT_MIN_SIZE) {
+ x0 = x1 - (SP_EXPORT_MIN_SIZE * DPI_BASE) / dpi;
+ }
+ break;
+ case SPIN_X1:
+ bmwidth = (x1 - x0) * dpi / DPI_BASE;
+ if (bmwidth < SP_EXPORT_MIN_SIZE) {
+ x1 = x0 + (SP_EXPORT_MIN_SIZE * DPI_BASE) / dpi;
+ }
+ break;
+ case SPIN_WIDTH:
+ bmwidth = width * dpi / DPI_BASE;
+ if (bmwidth < SP_EXPORT_MIN_SIZE) {
+ width = (SP_EXPORT_MIN_SIZE * DPI_BASE) / dpi;
+ }
+ x1 = x0 + width;
+ break;
+ default:
+ break;
+ }
+
+ width = x1 - x0;
+ bmwidth = floor(width * dpi / DPI_BASE + 0.5);
+
+ auto px = unit_table.getUnit("px");
+ x0_adj->set_value(px->convert(x0, unit));
+ x1_adj->set_value(px->convert(x1, unit));
+ width_adj->set_value(px->convert(width, unit));
+ spin_buttons[SPIN_BMWIDTH]->set_value(bmwidth);
+}
+
+void SingleExport::areaYChange(sb_type type)
+{
+ auto y0_adj = spin_buttons[SPIN_Y0]->get_adjustment();
+ auto y1_adj = spin_buttons[SPIN_Y1]->get_adjustment();
+ auto height_adj = spin_buttons[SPIN_HEIGHT]->get_adjustment();
+
+ float y0, y1, dpi, height, bmheight;
+
+ // Get all values in px
+ Unit const *unit = units->getUnit();
+ y0 = unit->convert(y0_adj->get_value(), "px");
+ y1 = unit->convert(y1_adj->get_value(), "px");
+ height = unit->convert(height_adj->get_value(), "px");
+ bmheight = spin_buttons[SPIN_BMHEIGHT]->get_value();
+ dpi = spin_buttons[SPIN_DPI]->get_value();
+
+ switch (type) {
+ case SPIN_Y0:
+ bmheight = (y1 - y0) * dpi / DPI_BASE;
+ if (bmheight < SP_EXPORT_MIN_SIZE) {
+ y0 = y1 - (SP_EXPORT_MIN_SIZE * DPI_BASE) / dpi;
+ }
+ break;
+ case SPIN_Y1:
+ bmheight = (y1 - y0) * dpi / DPI_BASE;
+ if (bmheight < SP_EXPORT_MIN_SIZE) {
+ y1 = y0 + (SP_EXPORT_MIN_SIZE * DPI_BASE) / dpi;
+ }
+ break;
+ case SPIN_HEIGHT:
+ bmheight = height * dpi / DPI_BASE;
+ if (bmheight < SP_EXPORT_MIN_SIZE) {
+ height = (SP_EXPORT_MIN_SIZE * DPI_BASE) / dpi;
+ }
+ y1 = y0 + height;
+ break;
+ default:
+ break;
+ }
+
+ height = y1 - y0;
+ bmheight = floor(height * dpi / DPI_BASE + 0.5);
+
+ auto px = unit_table.getUnit("px");
+ y0_adj->set_value(px->convert(y0, unit));
+ y1_adj->set_value(px->convert(y1, unit));
+ height_adj->set_value(px->convert(height, unit));
+ spin_buttons[SPIN_BMHEIGHT]->set_value(bmheight);
+}
+
+void SingleExport::dpiChange(sb_type type)
+{
+ float dpi, height, width, bmheight, bmwidth;
+
+ // Get all values in px
+ Unit const *unit = units->getUnit();
+ height = unit->convert(spin_buttons[SPIN_HEIGHT]->get_value(), "px");
+ width = unit->convert(spin_buttons[SPIN_WIDTH]->get_value(), "px");
+ bmheight = spin_buttons[SPIN_BMHEIGHT]->get_value();
+ bmwidth = spin_buttons[SPIN_BMWIDTH]->get_value();
+ dpi = spin_buttons[SPIN_DPI]->get_value();
+
+ switch (type) {
+ case SPIN_BMHEIGHT:
+ if (bmheight < SP_EXPORT_MIN_SIZE) {
+ bmheight = SP_EXPORT_MIN_SIZE;
+ }
+ dpi = bmheight * DPI_BASE / height;
+ break;
+ case SPIN_BMWIDTH:
+ if (bmwidth < SP_EXPORT_MIN_SIZE) {
+ bmwidth = SP_EXPORT_MIN_SIZE;
+ }
+ dpi = bmwidth * DPI_BASE / width;
+ break;
+ case SPIN_DPI:
+ prefs->setDouble("/dialogs/export/defaultdpi/value", dpi);
+ break;
+ default:
+ break;
+ }
+
+ bmwidth = floor(width * dpi / DPI_BASE + 0.5);
+ bmheight = floor(height * dpi / DPI_BASE + 0.5);
+
+ spin_buttons[SPIN_BMHEIGHT]->set_value(bmheight);
+ spin_buttons[SPIN_BMWIDTH]->set_value(bmwidth);
+ spin_buttons[SPIN_DPI]->set_value(dpi);
+}
+
+void SingleExport::setDefaultSelectionMode()
+{
+ current_key = (selection_mode)0; // default key
+ bool found = false;
+ Glib::ustring pref_key_name = prefs->getString("/dialogs/export/exportarea/value");
+ for (auto [key, name] : selection_names) {
+ if (pref_key_name == name) {
+ current_key = key;
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ pref_key_name = selection_names[current_key];
+ }
+
+ if (_desktop) {
+ if (current_key == SELECTION_SELECTION && (_desktop->getSelection())->isEmpty()) {
+ current_key = (selection_mode)0;
+ }
+ if ((_desktop->getSelection())->isEmpty()) {
+ selection_buttons[SELECTION_SELECTION]->set_sensitive(false);
+ }
+ if (current_key == SELECTION_CUSTOM &&
+ (spin_buttons[SPIN_HEIGHT]->get_value() == 0 || spin_buttons[SPIN_WIDTH]->get_value() == 0)) {
+ Geom::OptRect bbox = _document->preferredBounds();
+ setArea(bbox->min()[Geom::X], bbox->min()[Geom::Y], bbox->max()[Geom::X], bbox->max()[Geom::Y]);
+ }
+ } else {
+ current_key = (selection_mode)0;
+ }
+ selection_buttons[current_key]->set_active(true);
+ prefs->setString("/dialogs/export/exportarea/value", pref_key_name);
+
+ toggleSpinButtonVisibility();
+ refreshPage();
+}
+
+void SingleExport::setExporting(bool exporting, Glib::ustring const &text)
+{
+ if (exporting) {
+ set_sensitive(false);
+ set_opacity(0.2);
+ progress_box->show();
+ _prog->set_text(text);
+ _prog->set_fraction(0.0);
+ } else {
+ set_sensitive(true);
+ set_opacity(1.0);
+ progress_box->hide();
+ _prog->set_text("");
+ _prog->set_fraction(0.0);
+ }
+ Gtk::Main::iteration(false);
+}
+
+// Called for every progress iteration
+unsigned int SingleExport::onProgressCallback(float value, void *data)
+{
+ if (auto si = static_cast<SingleExport *>(data)) {
+ si->_prog->set_fraction(value);
+ Gtk::Main::iteration(false);
+ return !si->interrupted;
+ }
+ return false;
+}
+
+void SingleExport::refreshPreview()
+{
+ if (!_desktop) {
+ preview->resetPixels();
+ return;
+ }
+
+ std::vector<SPItem *> selected;
+ if (si_hide_all->get_active()) {
+ // This is because selection items is not a std::vector yet. FIXME.
+ selected =
+ std::vector<SPItem *>(_desktop->getSelection()->items().begin(), _desktop->getSelection()->items().end());
+ }
+ _preview_drawing->set_shown_items(std::move(selected));
+
+ bool show = si_show_preview->get_active();
+ if (!show || current_key == SELECTION_PAGE) {
+ bool have_pages = false;
+ for (auto child : pages_list->get_children()) {
+ if (auto bi = dynamic_cast<BatchItem *>(child)) {
+ bi->refresh(!show, _bgnd_color_picker->get_current_color());
+ have_pages = true;
+ }
+ }
+ if (have_pages) {
+ // We don't want to update the main preview for pages, it's hidden
+ preview->resetPixels();
+ return;
+ }
+ }
+
+ Unit const *unit = units->getUnit();
+ float x0 = unit->convert(spin_buttons[SPIN_X0]->get_value(), "px");
+ float x1 = unit->convert(spin_buttons[SPIN_X1]->get_value(), "px");
+ float y0 = unit->convert(spin_buttons[SPIN_Y0]->get_value(), "px");
+ float y1 = unit->convert(spin_buttons[SPIN_Y1]->get_value(), "px");
+ preview->setBox(Geom::Rect(x0, y0, x1, y1) * _document->dt2doc());
+ preview->setBackgroundColor(_bgnd_color_picker->get_current_color());
+ preview->queueRefresh();
+}
+
+void SingleExport::setDesktop(SPDesktop *desktop)
+{
+ if (desktop != _desktop) {
+ _page_selected_connection.disconnect();
+ _desktop = desktop;
+ }
+}
+
+void SingleExport::setDocument(SPDocument *document)
+{
+ if (_document == document || !_desktop)
+ return;
+
+ _document = document;
+ _page_changed_connection.disconnect();
+ _page_selected_connection.disconnect();
+ if (document) {
+ auto &pm = document->getPageManager();
+ _page_selected_connection = pm.connectPageSelected(sigc::mem_fun(*this, &SingleExport::onPagesSelected));
+ _page_modified_connection = pm.connectPageModified(sigc::mem_fun(*this, &SingleExport::onPagesModified));
+ _page_changed_connection = pm.connectPagesChanged(sigc::mem_fun(*this, &SingleExport::onPagesChanged));
+ auto bg_color = get_export_bg_color(document->getNamedView(), 0xffffff00);
+ _bgnd_color_picker->setRgba32(bg_color);
+ _preview_drawing = std::make_shared<PreviewDrawing>(document);
+ preview->setDrawing(_preview_drawing);
+
+ // Refresh values to sync them with defaults.
+ onPagesChanged();
+ refreshArea();
+ loadExportHints();
+ } else {
+ _preview_drawing.reset();
+ clearPagePreviews();
+ }
+}
+
+SingleExport::~SingleExport() { _page_selected_connection.disconnect(); }
+
+} // 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/export-single.h b/src/ui/dialog/export-single.h
new file mode 100644
index 0000000..814cea0
--- /dev/null
+++ b/src/ui/dialog/export-single.h
@@ -0,0 +1,211 @@
+// 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>
+ * Anshudhar Kumar Singh <anshudhar2001@gmail.com>
+ *
+ * Copyright (C) 1999-2007, 2021 Authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SP_EXPORT_SINGLE_H
+#define SP_EXPORT_SINGLE_H
+
+#include "ui/widget/scrollprotected.h"
+
+class InkscapeApplication;
+class SPDesktop;
+class SPDocument;
+class SPObject;
+class SPPage;
+
+namespace Inkscape {
+ class Selection;
+ class Preferences;
+
+namespace Util {
+ class Unit;
+}
+namespace UI {
+ namespace Widget {
+ class UnitMenu;
+ class ColorPicker;
+ }
+namespace Dialog {
+ class PreviewDrawing;
+ class ExportPreview;
+ class ExtensionList;
+
+class SingleExport : public Gtk::Box
+{
+public:
+ SingleExport(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade);
+ ~SingleExport() override;
+
+ void setApp(InkscapeApplication *app) { _app = app; }
+ void setDocument(SPDocument *document);
+ void setDesktop(SPDesktop *desktop);
+ void selectionChanged(Inkscape::Selection *selection);
+ void selectionModified(Inkscape::Selection *selection, guint flags);
+ void refresh()
+ {
+ refreshArea();
+ refreshPage();
+ loadExportHints();
+ };
+
+private:
+ enum sb_type
+ {
+ SPIN_X0 = 0,
+ SPIN_X1,
+ SPIN_Y0,
+ SPIN_Y1,
+ SPIN_WIDTH,
+ SPIN_HEIGHT,
+ SPIN_BMWIDTH,
+ SPIN_BMHEIGHT,
+ SPIN_DPI
+ };
+
+ enum selection_mode
+ {
+ SELECTION_PAGE = 0, // Default is alaways placed first
+ SELECTION_SELECTION,
+ SELECTION_DRAWING,
+ SELECTION_CUSTOM,
+ };
+
+ InkscapeApplication *_app = nullptr;
+ SPDesktop *_desktop = nullptr;
+ SPDocument *_document = nullptr;
+ std::shared_ptr<PreviewDrawing> _preview_drawing;
+
+ bool setupDone = false; // To prevent setup() call add connections again.
+
+ typedef Inkscape::UI::Widget::ScrollProtected<Gtk::SpinButton> SpinButton;
+
+ std::map<sb_type, SpinButton *> spin_buttons;
+ std::map<sb_type, Gtk::Label *> spin_labels;
+ std::map<selection_mode, Gtk::RadioButton *> selection_buttons;
+
+ Gtk::Box *si_units_row = nullptr;
+ Gtk::CheckButton *show_export_area = nullptr;
+ Inkscape::UI::Widget::UnitMenu *units = nullptr;
+ Gtk::FlowBox *pages_list = nullptr;
+
+ Gtk::CheckButton *si_hide_all = nullptr;
+ Gtk::CheckButton *si_show_preview = nullptr;
+
+ ExportPreview *preview = nullptr;
+
+ ExtensionList *si_extension_cb = nullptr;
+ Gtk::Entry *si_filename_entry = nullptr;
+ Gtk::Button *si_export = nullptr;
+ Gtk::Box *adv_box = nullptr;
+ Gtk::Grid *size_box = nullptr;
+ Gtk::ProgressBar *_prog = nullptr;
+ Gtk::Widget *pages_list_box = nullptr;
+ Gtk::Widget *preview_box = nullptr;
+ Gtk::Widget *progress_box = nullptr;
+ Gtk::Button *cancel_button = nullptr;
+
+ bool filename_modified = false;
+ Glib::ustring original_name;
+ Glib::ustring doc_export_name;
+
+ Inkscape::Preferences *prefs = nullptr;
+ std::map<selection_mode, Glib::ustring> selection_names;
+ selection_mode current_key = (selection_mode)0;
+
+ void setup();
+ void setupUnits();
+ void setupExtensionList();
+ void setupSpinButtons();
+ void toggleSpinButtonVisibility();
+ void refreshPreview();
+
+ // change range and callbacks to spinbuttons
+ template <typename T>
+ void setupSpinButton(Gtk::SpinButton *sb, double val, double min, double max, double step, double page, int digits,
+ bool sensitive, void (SingleExport::*cb)(T), T param);
+
+ void setDefaultSelectionMode();
+ void onAreaXChange(sb_type type);
+ void onAreaYChange(sb_type type);
+ void onDpiChange(sb_type type);
+ void onAreaTypeToggle(selection_mode key);
+ void onUnitChanged();
+ void onFilenameModified();
+ void onExtensionChanged();
+ void onExport();
+ void onCancel();
+ void onBrowse(Gtk::EntryIconPosition pos, const GdkEventButton *ev);
+ void on_inkscape_selection_modified(Inkscape::Selection *selection, guint flags);
+ void on_inkscape_selection_changed(Inkscape::Selection *selection);
+
+ void refreshArea();
+ void refreshPage();
+ void loadExportHints();
+ void saveExportHints(SPObject *target);
+ void areaXChange(sb_type type);
+ void areaYChange(sb_type type);
+ void dpiChange(sb_type type);
+ void setArea(double x0, double y0, double x1, double y1);
+ void blockSpinConns(bool status);
+
+ void setExporting(bool exporting, Glib::ustring const &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)
+ */
+ static unsigned int onProgressCallback(float value, void *data);
+
+ /**
+ * Page functions
+ */
+ void clearPagePreviews();
+ void onPagesChanged();
+ void onPagesModified(SPPage *page);
+ void onPagesSelected(SPPage *page);
+ void setPagesMode(bool multi);
+ void selectPage(SPPage *page);
+ std::vector<SPPage *> getSelectedPages();
+
+ bool interrupted;
+
+ // Gtk Signals
+ std::vector<sigc::connection> spinButtonConns;
+ sigc::connection filenameConn;
+ sigc::connection extensionConn;
+ sigc::connection exportConn;
+ sigc::connection cancelConn;
+ sigc::connection browseConn;
+ sigc::connection prefsConn;
+ sigc::connection _pages_list_changed;
+ // Document Signals
+ sigc::connection _page_selected_connection;
+ sigc::connection _page_modified_connection;
+ sigc::connection _page_changed_connection;
+
+ std::unique_ptr<Inkscape::UI::Widget::ColorPicker> _bgnd_color_picker;
+};
+} // 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/export.cpp b/src/ui/dialog/export.cpp
new file mode 100644
index 0000000..522acf0
--- /dev/null
+++ b/src/ui/dialog/export.cpp
@@ -0,0 +1,531 @@
+// 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>
+ * Anshudhar Kumar Singh <anshudhar2001@gmail.com>
+ *
+ * Copyright (C) 1999-2007, 2012, 2021 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 "export.h"
+
+#include <glibmm/convert.h>
+#include <glibmm/i18n.h>
+#include <glibmm/miscutils.h>
+#include <gtkmm.h>
+#include <png.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "color/color-conv.h"
+#include "extension/db.h"
+#include "extension/output.h"
+#include "file.h"
+#include "helper/png-write.h"
+#include "inkscape-window.h"
+#include "inkscape.h"
+#include "io/resource.h"
+#include "io/sys.h"
+#include "message-stack.h"
+#include "object/object-set.h"
+#include "object/sp-namedview.h"
+#include "object/sp-root.h"
+#include "object/sp-page.h"
+#include "object/weakptr.h"
+#include "preferences.h"
+#include "selection-chemistry.h"
+#include "ui/dialog-events.h"
+#include "ui/dialog/export-single.h"
+#include "ui/dialog/export-batch.h"
+#include "ui/dialog/dialog-notebook.h"
+#include "ui/dialog/filedialog.h"
+#include "ui/interface.h"
+#include "ui/widget/color-picker.h"
+#include "ui/widget/scrollprotected.h"
+#include "ui/widget/unit-menu.h"
+
+#ifdef _WIN32
+
+#endif
+
+using Inkscape::Util::unit_table;
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+Export::Export()
+ : DialogBase("/dialogs/export/", "Export")
+{
+ std::string gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "dialog-export.glade");
+
+ try {
+ builder = Gtk::Builder::create_from_file(gladefile);
+ } catch (const Glib::Error &ex) {
+ g_error("Glade file loading failed for export screen");
+ return;
+ }
+
+ prefs = Inkscape::Preferences::get();
+
+ builder->get_widget("export-box", container);
+ add(*container);
+ show_all_children();
+
+ builder->get_widget("export-notebook", export_notebook);
+
+ // Initialise Single Export and its objects
+ builder->get_widget_derived("single-image", single_image);
+
+ // Initialise Batch Export and its objects
+ builder->get_widget_derived("batch-export", batch_export);
+
+ container->signal_realize().connect([=]() {
+ setDefaultNotebookPage();
+ notebook_signal = export_notebook->signal_switch_page().connect(sigc::mem_fun(*this, &Export::onNotebookPageSwitch));
+ });
+ container->signal_unrealize().connect([=]() {
+ notebook_signal.disconnect();
+ });
+}
+
+// Set current page based on preference/last visited page
+void Export::setDefaultNotebookPage()
+{
+ pages[BATCH_EXPORT] = export_notebook->page_num(*batch_export->get_parent());
+ pages[SINGLE_IMAGE] = export_notebook->page_num(*single_image->get_parent());
+ export_notebook->set_current_page(pages[SINGLE_IMAGE]);
+}
+
+void Export::documentReplaced()
+{
+ single_image->setDocument(getDocument());
+ batch_export->setDocument(getDocument());
+}
+
+void Export::desktopReplaced()
+{
+ single_image->setDesktop(getDesktop());
+ single_image->setApp(getApp());
+ batch_export->setDesktop(getDesktop());
+ batch_export->setApp(getApp());
+ // Called previously, but we need post-desktop call too
+ documentReplaced();
+}
+
+void Export::selectionChanged(Inkscape::Selection *selection)
+{
+ auto current_page = export_notebook->get_current_page();
+ if (current_page == pages[SINGLE_IMAGE]) {
+ single_image->selectionChanged(selection);
+ }
+ if (current_page == pages[BATCH_EXPORT]) {
+ batch_export->selectionChanged(selection);
+ }
+}
+void Export::selectionModified(Inkscape::Selection *selection, guint flags)
+{
+ auto current_page = export_notebook->get_current_page();
+ if (current_page == pages[SINGLE_IMAGE]) {
+ single_image->selectionModified(selection, flags);
+ }
+ if (current_page == pages[BATCH_EXPORT]) {
+ batch_export->selectionModified(selection, flags);
+ }
+}
+
+void Export::onNotebookPageSwitch(Widget *page, guint page_number)
+{
+ auto desktop = getDesktop();
+ if (desktop) {
+ auto selection = desktop->getSelection();
+
+ if (page_number == pages[SINGLE_IMAGE]) {
+ single_image->selectionChanged(selection);
+ }
+ if (page_number == pages[BATCH_EXPORT]) {
+ batch_export->selectionChanged(selection);
+ }
+ }
+}
+
+std::string Export::absolutizePath(SPDocument *doc, const std::string &filename)
+{
+ std::string path;
+ // Make relative paths go from the document location, if possible:
+ if (!Glib::path_is_absolute(filename) && doc->getDocumentFilename()) {
+ auto dirname = Glib::path_get_dirname(doc->getDocumentFilename());
+ if (!dirname.empty()) {
+ path = Glib::build_filename(dirname, filename);
+ }
+ }
+ if (path.empty()) {
+ path = filename;
+ }
+ return path;
+}
+
+bool Export::unConflictFilename(SPDocument *doc, Glib::ustring &filename, Glib::ustring const extension)
+{
+ std::string path = absolutizePath(doc, Glib::filename_from_utf8(filename));
+ Glib::ustring test_filename = path + extension;
+ if (!Inkscape::IO::file_test(test_filename.c_str(), G_FILE_TEST_EXISTS)) {
+ filename = test_filename;
+ return true;
+ }
+ for (int i = 1; i <= 100; i++) {
+ test_filename = path + "_copy_" + std::to_string(i) + extension;
+ if (!Inkscape::IO::file_test(test_filename.c_str(), G_FILE_TEST_EXISTS)) {
+ filename = test_filename;
+ return true;
+ }
+ }
+ return false;
+}
+
+bool Export::exportRaster(
+ Geom::Rect const &area, unsigned long int const &width, unsigned long int const &height,
+ float const &dpi, guint32 bg_color, Glib::ustring const &filename, bool overwrite,
+ unsigned (*callback)(float, void *), void *data,
+ Inkscape::Extension::Output *extension, std::vector<SPItem *> *items)
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if (!desktop)
+ return false;
+ SPDocument *doc = desktop->getDocument();
+
+ if (area.hasZeroArea() || 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 false;
+ }
+ 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 false;
+ }
+
+ if (!extension || !extension->is_raster()) {
+ desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("Raster Export Error"));
+ sp_ui_error_dialog(_("Raster export Method is used for NON RASTER EXTENSION"));
+ return false;
+ }
+
+ float pHYs = extension->get_param_float("png_phys", dpi);
+ if (pHYs < 0.01) pHYs = dpi;
+
+ bool use_interlacing = extension->get_param_bool("png_interlacing", false);
+ int antialiasing = extension->get_param_int("png_antialias", 2); // Cairo anti aliasing
+ int zlib = extension->get_param_int("png_compression", 1); // Default is 6 for png, but 1 for non-png
+ auto val = extension->get_param_int("png_bitdepth", 99); // corresponds to RGBA 8
+
+ int bit_depth = pow(2, (val & 0x0F));
+ int color_type = (val & 0xF0) >> 4;
+
+ std::string path = absolutizePath(doc, Glib::filename_from_utf8(filename));
+ 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))) {
+ Glib::ustring safeDir = Inkscape::IO::sanitizeString(dirname.c_str());
+ Glib::ustring error =
+ g_strdup_printf(_("Directory <b>%s</b> does not exist or is not a directory.\n"), safeDir.c_str());
+
+ desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, error.c_str());
+ sp_ui_error_dialog(error.c_str());
+ return false;
+ }
+
+ // Do the over-write protection now, since the png is just a temp file.
+ if (!overwrite && !sp_ui_overwrite_file(path.c_str())) {
+ return false;
+ }
+
+ auto fn = Glib::path_get_basename(path);
+ auto png_filename = path;
+ {
+ // Select the extension and set the filename to a temporary file
+ int tempfd_out = Glib::file_open_tmp(png_filename, "ink_ext_");
+ close(tempfd_out);
+ }
+
+ // Export Start Here
+ std::vector<SPItem *> selected;
+ if (items && items->size() > 0) {
+ selected = *items;
+ }
+
+ ExportResult result = sp_export_png_file(desktop->getDocument(), png_filename.c_str(), area, width, height, pHYs,
+ pHYs, // previously xdpi, ydpi.
+ bg_color, callback, data, true, selected,
+ use_interlacing, color_type, bit_depth, zlib, antialiasing);
+
+ bool failed = result == EXPORT_ERROR; // || prog_dialog->get_stopped();
+
+ if (failed) {
+ Glib::ustring safeFile = Inkscape::IO::sanitizeString(path.c_str());
+ Glib::ustring error = g_strdup_printf(_("Could not export to filename <b>%s</b>.\n"), safeFile.c_str());
+
+ desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, error.c_str());
+ sp_ui_error_dialog(error.c_str());
+ return false;
+ } else if (result == EXPORT_OK) {
+ // Don't ask for preferences on every run.
+ try {
+ extension->export_raster(doc, png_filename, path.c_str(), false);
+ } catch (Inkscape::Extension::Output::save_failed &e) {
+ return false;
+ }
+ } else {
+ // Extensions have their own error popup, so this only tracks failures in the png step
+ desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Export aborted."));
+ return false;
+ }
+
+ Glib::ustring safeFile = Inkscape::IO::sanitizeString(path.c_str());
+ desktop->messageStack()->flashF(Inkscape::INFORMATION_MESSAGE, _("Drawing exported to <b>%s</b>."),
+ safeFile.c_str());
+
+ unlink(png_filename.c_str());
+ return true;
+}
+
+bool Export::exportVector(
+ Inkscape::Extension::Output *extension, SPDocument *doc,
+ Glib::ustring const &filename,
+ bool overwrite, const std::vector<SPItem *> &items, SPPage *page)
+{
+ std::vector<SPPage *> pages;
+ if (page)
+ pages.push_back(page);
+ return exportVector(extension, doc, filename, overwrite, items, pages);
+}
+
+bool Export::exportVector(
+ Inkscape::Extension::Output *extension, SPDocument *copy_doc,
+ Glib::ustring const &filename,
+ bool overwrite, const std::vector<SPItem *> &items, const std::vector<SPPage *> &pages)
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if (!desktop)
+ return false;
+
+ 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 false;
+ }
+
+ if (!extension || extension->is_raster()) {
+ desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("Vector Export Error"));
+ sp_ui_error_dialog(_("Vector export Method is used for RASTER EXTENSION"));
+ return false;
+ }
+
+ std::string path = absolutizePath(copy_doc, Glib::filename_from_utf8(filename));
+ Glib::ustring dirname = Glib::path_get_dirname(path);
+ Glib::ustring safeFile = Inkscape::IO::sanitizeString(path.c_str());
+ Glib::ustring safeDir = Inkscape::IO::sanitizeString(dirname.c_str());
+
+ if (dirname.empty() ||
+ !Inkscape::IO::file_test(dirname.c_str(), (GFileTest)(G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR))) {
+ Glib::ustring error =
+ g_strdup_printf(_("Directory <b>%s</b> does not exist or is not a directory.\n"), safeDir.c_str());
+
+ desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, error.c_str());
+ sp_ui_error_dialog(error.c_str());
+
+ return false;
+ }
+
+ // Do the over-write protection now
+ if (!overwrite && !sp_ui_overwrite_file(path.c_str())) {
+ return false;
+ }
+ copy_doc->ensureUpToDate();
+
+ std::vector<SPItem *> objects = items;
+ std::set<std::string> obj_ids;
+ std::set<std::string> page_ids;
+ Geom::OptRect page_rect;
+ for (auto page : pages) {
+ // Save the first page rect, must be done before everything else
+ if (!page_rect)
+ page_rect = page->getDesktopRect();
+
+ if (auto _id = page->getId()) {
+ page_ids.insert(std::string(_id));
+ }
+ // If page then our item set is limited to the overlapping items
+ auto page_items = page->getOverlappingItems(true, true);
+
+ if (items.empty()) {
+ // Items is page_items, remove all items not in this page.
+ objects.insert(objects.end(), page_items.begin(), page_items.end());
+ } else {
+ for (auto &item : page_items) {
+ item->getIds(obj_ids);
+ }
+ }
+ }
+
+ // Delete any pages not specified, delete all pages if none specified
+ auto &pm = copy_doc->getPageManager();
+
+ // Make weak pointers to pages, since deletePage() can delete more than just the requested page.
+ std::vector<SPWeakPtr<SPPage>> copy_pages;
+ copy_pages.reserve(pm.getPageCount());
+ for (auto *page : pm.getPages()) {
+ copy_pages.emplace_back(page);
+ }
+
+ // We refuse to delete anything if everything would be deleted.
+ for (auto &page : copy_pages) {
+ if (page) {
+ auto _id = page->getId();
+ if (_id && page_ids.find(_id) == page_ids.end()) {
+ pm.deletePage(page.get(), false);
+ }
+ }
+ }
+
+ // Page export ALWAYS restricts, even if nothing would be on the page.
+ if (!objects.empty() || !pages.empty()) {
+ std::vector<SPObject *> objects_to_export;
+ Inkscape::ObjectSet object_set(copy_doc);
+ for (auto &object : objects) {
+ auto _id = object->getId();
+ if (!_id || (!obj_ids.empty() && obj_ids.find(_id) == obj_ids.end())) {
+ // This item is off the page so can be ignored for export
+ continue;
+ }
+
+ SPObject *obj = copy_doc->getObjectById(_id);
+ if (!obj) {
+ Glib::ustring error = g_strdup_printf(_("Could not export to filename <b>%s</b>. (missing object)\n"), safeFile.c_str());
+
+ desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, error.c_str());
+ sp_ui_error_dialog(error.c_str());
+
+ return false;
+ }
+ copy_doc->ensureUpToDate();
+
+ object_set.add(obj, true);
+ objects_to_export.push_back(obj);
+ }
+
+ copy_doc->getRoot()->cropToObjects(objects_to_export);
+
+ if (pages.empty()) {
+ object_set.fitCanvas(true, true);
+ }
+ }
+
+ // Remove all unused definitions
+ copy_doc->vacuumDocument();
+
+ try {
+ extension->save(copy_doc, path.c_str());
+ } catch (Inkscape::Extension::Output::save_failed &e) {
+ Glib::ustring safeFile = Inkscape::IO::sanitizeString(path.c_str());
+ Glib::ustring error = g_strdup_printf(_("Could not export to filename <b>%s</b>.\n"), safeFile.c_str());
+
+ desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, error.c_str());
+ sp_ui_error_dialog(error.c_str());
+
+ return false;
+ }
+
+ desktop->messageStack()->flashF(Inkscape::INFORMATION_MESSAGE, _("Drawing exported to <b>%s</b>."),
+ safeFile.c_str());
+ return true;
+}
+
+std::string Export::filePathFromObject(SPDocument *doc, SPObject *obj, const Glib::ustring &file_entry_text)
+{
+ Glib::ustring id = _("bitmap");
+ if (obj && obj->getId()) {
+ id = obj->getId();
+ }
+ return filePathFromId(doc, id, file_entry_text);
+}
+
+std::string Export::filePathFromId(SPDocument *doc, Glib::ustring id, const Glib::ustring &file_entry_text)
+{
+ g_assert(!id.empty());
+
+ std::string directory;
+
+ if (!file_entry_text.empty()) {
+ directory = Glib::path_get_dirname(Glib::filename_from_utf8(file_entry_text));
+ }
+
+ if (directory.empty()) {
+ /* Grab document directory */
+ const gchar *docFilename = doc->getDocumentFilename();
+ if (docFilename) {
+ directory = Glib::path_get_dirname(docFilename);
+ }
+ }
+
+ if (directory.empty()) {
+ directory = Inkscape::IO::Resource::homedir_path();
+ }
+
+ return Glib::build_filename(directory, Glib::filename_from_utf8(id));
+}
+
+Glib::ustring Export::defaultFilename(SPDocument *doc, Glib::ustring &filename_entry_text, Glib::ustring extension)
+{
+ Glib::ustring filename;
+ if (doc && doc->getDocumentFilename()) {
+ filename = doc->getDocumentFilename();
+ //appendExtensionToFilename(filename, extension);
+ } else if (doc) {
+ filename = filePathFromId(doc, _("bitmap"), filename_entry_text);
+ filename = filename + extension;
+ }
+ return filename;
+}
+
+void set_export_bg_color(SPObject* object, guint32 color) {
+ if (object) {
+ object->setAttribute("inkscape:export-bgcolor", Inkscape::Util::rgba_color_to_string(color).c_str());
+ }
+}
+
+guint32 get_export_bg_color(SPObject* object, guint32 default_color) {
+ if (object) {
+ if (auto color = Inkscape::Util::string_to_rgba_color(object->getAttribute("inkscape:export-bgcolor"))) {
+ return *color;
+ }
+ }
+ return default_color;
+}
+
+
+} // 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 :
+// 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..4ce4da3
--- /dev/null
+++ b/src/ui/dialog/export.h
@@ -0,0 +1,112 @@
+// 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>
+ * Anshudhar Kumar Singh <anshudhar2001@gmail.com>
+ *
+ * Copyright (C) 1999-2007, 2021 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.h>
+
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/scrollprotected.h"
+
+class SPPage;
+
+namespace Inkscape {
+ class Preferences;
+ namespace Util {
+ class Unit;
+ }
+ namespace Extension {
+ class Output;
+ }
+
+namespace UI {
+namespace Dialog {
+ class SingleExport;
+ class BatchExport;
+
+enum notebook_page
+{
+ SINGLE_IMAGE = 0,
+ BATCH_EXPORT
+};
+
+void set_export_bg_color(SPObject* object, guint32 color);
+guint32 get_export_bg_color(SPObject* object, guint32 default_color);
+
+class Export : public DialogBase
+{
+public:
+ Export();
+ ~Export() override = default;
+
+private:
+ Glib::RefPtr<Gtk::Builder> builder;
+ Gtk::Box *container = nullptr; // Main Container
+ Gtk::Notebook *export_notebook = nullptr; // Notebook Container for single and batch export
+
+ SingleExport *single_image = nullptr;
+ BatchExport *batch_export = nullptr;
+
+ Inkscape::Preferences *prefs = nullptr;
+
+ // setup default values of widgets
+ void setDefaultNotebookPage();
+ std::map<notebook_page, int> pages;
+
+ sigc::connection notebook_signal;
+
+ // signals callback
+ void onNotebookPageSwitch(Widget *page, guint page_number);
+ void documentReplaced() override;
+ void desktopReplaced() override;
+ void selectionChanged(Inkscape::Selection *selection) override;
+ void selectionModified(Inkscape::Selection *selection, guint flags) override;
+
+public:
+ static std::string absolutizePath(SPDocument *doc, const std::string &filename);
+ static bool unConflictFilename(SPDocument *doc, Glib::ustring &filename, Glib::ustring const extension);
+ static std::string filePathFromObject(SPDocument *doc, SPObject *obj, const Glib::ustring &file_entry_text);
+ static std::string filePathFromId(SPDocument *doc, Glib::ustring id, const Glib::ustring &file_entry_text);
+ static Glib::ustring defaultFilename(SPDocument *doc, Glib::ustring &filename_entry_text, Glib::ustring extension);
+
+ static bool exportRaster(
+ Geom::Rect const &area, unsigned long int const &width, unsigned long int const &height,
+ float const &dpi, guint32 bg_color, Glib::ustring const &filename, bool overwrite,
+ unsigned (*callback)(float, void *), void *data,
+ Inkscape::Extension::Output *extension, std::vector<SPItem *> *items = nullptr);
+
+ static bool exportVector(
+ Inkscape::Extension::Output *extension, SPDocument *doc, Glib::ustring const &filename,
+ bool overwrite, const std::vector<SPItem *> &items, SPPage *page);
+ static bool exportVector(
+ Inkscape::Extension::Output *extension, SPDocument *doc, Glib::ustring const &filename,
+ bool overwrite, const std::vector<SPItem *> &items, const std::vector<SPPage *> &pages);
+
+};
+
+} // 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/filedialog.cpp b/src/ui/dialog/filedialog.cpp
new file mode 100644
index 0000000..d56d0da
--- /dev/null
+++ b/src/ui/dialog/filedialog.cpp
@@ -0,0 +1,186 @@
+// 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;
+}
+
+//########################################################################
+//# 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::getDocTitle()
+{
+ return myDocTitle;
+}
+
+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();
+ _filename = 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..49cbcd8
--- /dev/null
+++ b/src/ui/dialog/filedialog.h
@@ -0,0 +1,200 @@
+// 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);
+
+class FileDialog
+{
+public:
+ /**
+ * 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 *getExtension() { return _extension; }
+ virtual void setExtension(Inkscape::Extension::Extension *key) { _extension = key; }
+
+ const Glib::ustring &getFilename() { return _filename; }
+ void setFilename(const Glib::ustring &path) { _filename = path; }
+
+ /**
+ * Show file selector.
+ * @return the selected path if user selected one, else NULL
+ */
+ virtual bool show() = 0;
+
+ /**
+ * Add a filter menu to the file dialog.
+ */
+ virtual void addFilterMenu(const Glib::ustring &name, Glib::ustring pattern = "",
+ Inkscape::Extension::Extension *mod = nullptr) = 0;
+
+ /**
+ * Get the current directory of the file dialog.
+ */
+ virtual Glib::ustring getCurrentDirectory() = 0;
+
+protected:
+ // The selected extension
+ Inkscape::Extension::Extension *_extension;
+
+ // Filename that was given
+ 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 FileDialog
+{
+public:
+ // Constructor. Do not call directly. Use the factory.
+ FileOpenDialog() = default;
+ virtual ~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);
+
+ virtual std::vector<Glib::ustring> getFilenames() = 0;
+
+protected:
+
+}; //FileOpenDialog
+
+
+/**
+ * This class provides an implementation-independent API for
+ * file "Save" dialogs.
+ */
+class FileSaveDialog : public FileDialog
+{
+public:
+ // Constructor. Do not call directly. Use the factory.
+ FileSaveDialog() = default;
+ virtual ~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);
+
+ /**
+ * Get the document title chosen by the user.
+ * Valid after an [OK]
+ */
+ Glib::ustring getDocTitle ();
+
+protected:
+ // 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..44667b6
--- /dev/null
+++ b/src/ui/dialog/filedialogimpl-gtkmm.cpp
@@ -0,0 +1,676 @@
+// 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 "filedialogimpl-gtkmm.h"
+
+#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 <iostream>
+
+#include "document.h"
+#include "extension/db.h"
+#include "extension/input.h"
+#include "extension/output.h"
+#include "inkscape.h"
+#include "io/resource.h"
+#include "io/sys.h"
+#include "path-prefix.h"
+#include "preferences.h"
+#include "ui/dialog-events.h"
+#include "ui/util.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
+
+/**
+ * Information stored about all save and open filters applied to the dialog.
+ */
+struct FilterListClass : public Gtk::TreeModelColumnRecord
+{
+ Gtk::TreeModelColumn<Glib::ustring> label;
+ Gtk::TreeModelColumn<Inkscape::Extension::Extension *> extension;
+ Gtk::TreeModelColumn<bool> enabled;
+
+ FilterListClass()
+ {
+ add(label);
+ add(extension);
+ add(enabled);
+ }
+};
+FilterListClass FilterList;
+
+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
+#########################################################################*/
+
+void FileDialogBaseGtk::internalSetup()
+{
+ filterComboBox = dynamic_cast<Gtk::ComboBoxText *>(get_widget_by_name(this, "GtkComboBoxText"));
+ g_assert(filterComboBox);
+
+ filterStore = Gtk::ListStore::create(FilterList);
+ filterComboBox->set_model(filterStore);
+ filterComboBox->signal_changed().connect(sigc::mem_fun(*this, &FileDialogBaseGtk::filterChangedCallback));
+
+ auto cell_renderer = filterComboBox->get_first_cell();
+ if (cell_renderer) {
+ // Add enabled column to cell_renderer property
+ filterComboBox->add_attribute(cell_renderer->property_sensitive(), FilterList.enabled);
+ }
+
+ // 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::_updatePreviewCallback));
+
+ svgexportCheckbox.set_label(Glib::ustring(_("Export as SVG 1.1 per settings in Preferences 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::_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()
+{
+ bool enabled = previewCheckbox.get_active();
+
+ set_preview_widget_active(enabled);
+
+ if (!enabled)
+ return;
+
+ Glib::ustring fileName = get_preview_filename();
+ if (fileName.empty()) {
+ fileName = get_preview_uri();
+ }
+
+ if (!fileName.empty()) {
+ svgPreview.set(fileName, _dialogType);
+ } else {
+ svgPreview.showNoPreview();
+ }
+}
+
+Glib::RefPtr<Gtk::FileFilter> FileDialogBaseGtk::addFilter(const Glib::ustring &name, Glib::ustring ext,
+ Inkscape::Extension::Extension *extension)
+{
+ auto filter = Gtk::FileFilter::create();
+ filter->set_name(name);
+ add_filter(filter);
+
+ if (!ext.empty()) {
+ filter->add_pattern(extToPattern(ext));
+ }
+
+ // ListStore is populated by add_filter, so get the last row to add the rest
+ Gtk::TreeRow row;
+ for (auto child : filterStore->children()) {
+ row = child;
+ }
+ if (row) {
+ row[FilterList.extension] = extension;
+ row[FilterList.enabled] = !extension || !extension->deactivated();
+ }
+ return filter;
+}
+
+// Replace this with add_suffix in Gtk4
+Glib::ustring FileDialogBaseGtk::extToPattern(const Glib::ustring &extension) const
+{
+ Glib::ustring pattern = "*";
+ 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;
+ }
+ }
+ return pattern;
+}
+
+/*#########################################################################
+### 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);
+
+ /* 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
+
+ using namespace Inkscape::IO::Resource;
+ auto examplesdir = get_path_string(SYSTEM, EXAMPLES);
+ if (Glib::file_test(examplesdir, Glib::FILE_TEST_IS_DIR) && //
+ Glib::path_is_absolute(examplesdir)) {
+ add_shortcut_folder(examplesdir);
+ }
+}
+
+void FileOpenDialogImplGtk::createFilterMenu()
+{
+ if (_dialogType == CUSTOM_TYPE) {
+ return;
+ }
+
+ addFilter(_("All Files"), "*");
+
+ if (_dialogType != EXE_TYPES) {
+ auto allInkscapeFilter = addFilter(_("All Inkscape Files"));
+ auto allImageFilter = addFilter(_("All Images"));
+ auto allVectorFilter = addFilter(_("All Vectors"));
+ auto allBitmapFilter = addFilter(_("All Bitmaps"));
+
+ // patterns added dynamically below
+ Inkscape::Extension::DB::InputList extension_list;
+ Inkscape::Extension::db.get_input_list(extension_list);
+
+ for (auto imod : extension_list)
+ {
+ addFilter(imod->get_filetypename(true), imod->get_extension(), imod);
+
+ auto upattern = extToPattern(imod->get_extension());
+ allInkscapeFilter->add_pattern(upattern);
+ if (strncmp("image", imod->get_mimetype(), 5) == 0)
+ allImageFilter->add_pattern(upattern);
+
+ // 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) {
+ if (auto iter = filterComboBox->get_active()) {
+ setExtension((*iter)[FilterList.extension]);
+ }
+
+ auto fn = get_filename();
+ setFilename(fn.empty() ? get_uri() : Glib::ustring(fn));
+
+ cleanup(true);
+ return true;
+ } else {
+ cleanup(false);
+ return false;
+ }
+}
+
+
+/**
+ * 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)
+ , checksBox(Gtk::ORIENTATION_VERTICAL)
+ , childBox(Gtk::ORIENTATION_HORIZONTAL)
+{
+ FileSaveDialog::myDocTitle = docTitle;
+
+ /* One file at a time */
+ set_select_multiple(false);
+
+ set_local_only(false);
+
+ /* 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);
+ }
+ setFilename(udir);
+ }
+
+ //###### 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)
+ createFilterMenu();
+
+ childBox.pack_start(checksBox);
+ checksBox.pack_start(fileTypeCheckbox);
+ checksBox.pack_start(previewCheckbox);
+ checksBox.pack_start(svgexportCheckbox);
+
+ set_extra_widget(childBox);
+
+ // Let's do some customization
+ fileNameEntry = dynamic_cast<Gtk::Entry *>(get_widget_by_name(this, "GtkEntry"));
+ if (fileNameEntry) {
+ // Catch when user hits [return] on the text field
+ fileNameEntry->signal_activate().connect(
+ sigc::mem_fun(*this, &FileSaveDialogImplGtk::fileNameEntryChangedCallback));
+ }
+ if (auto expander = dynamic_cast<Gtk::Expander *>(get_widget_by_name(this, "GtkExpander"))) {
+ // Always show the file list
+ expander->set_expanded(true);
+ }
+
+ signal_selection_changed().connect(sigc::mem_fun(*this, &FileSaveDialogImplGtk::fileNameChanged));
+
+ // 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);
+ }
+
+ add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL);
+ set_default(*add_button(_("_Save"), Gtk::RESPONSE_OK));
+
+ show_all_children();
+}
+
+/**
+ * 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::filterChangedCallback()
+{
+ if (auto iter = filterComboBox->get_active())
+ setExtension((*iter)[FilterList.extension]);
+ if (!fromCB)
+ 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 (auto output = dynamic_cast<Inkscape::Extension::Output *>(_extension))
+ if (Glib::ustring(output->get_extension()).casefold() == ext)
+ return;
+ if (knownExtensions.find(ext) == knownExtensions.end()) return;
+ fromCB = true;
+ filterComboBox->set_active_text(knownExtensions[ext]->get_filetypename(true));
+}
+
+void FileSaveDialogImplGtk::createFilterMenu()
+{
+ Inkscape::Extension::DB::OutputList extension_list;
+ Inkscape::Extension::db.get_output_list(extension_list);
+ knownExtensions.clear();
+
+ addFilter(_("Guess from extension"), "*");
+
+ for (auto omod : extension_list) {
+ // Export types are either exported vector types, or any raster type.
+ if (!omod->is_exported() && omod->is_raster() != (_dialogType == EXPORT_TYPES))
+ continue;
+
+ // This extension is limited to save copy only.
+ if (omod->savecopy_only() && save_method != Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY)
+ continue;
+
+ Glib::ustring extension = omod->get_extension();
+ addFilter(omod->get_filetypename(true), extension, omod);
+ knownExtensions.insert(std::pair<Glib::ustring, Inkscape::Extension::Output*>(extension.casefold(), omod));
+ }
+
+ filterComboBox->set_active(0);
+ filterChangedCallback(); // call at least once to set the filter
+}
+
+/**
+ * Show this dialog modally. Return true if user hits [OK]
+ */
+bool FileSaveDialogImplGtk::show()
+{
+ change_path(getFilename());
+ 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());
+ }
+
+ auto extension = getExtension();
+ Inkscape::Extension::store_file_extension_in_prefs((extension != nullptr ? extension->get_id() : ""), save_method);
+
+ cleanup(true);
+
+ return true;
+ } else {
+ cleanup(false);
+ return false;
+ }
+}
+
+void FileSaveDialogImplGtk::setExtension(Inkscape::Extension::Extension *key)
+{
+ // If no pointer to extension is passed in, look up based on filename extension.
+ if (!key) {
+ auto fn = getFilename().casefold();
+
+ for (auto const &iter : knownExtensions) {
+ auto ext = Glib::ustring(iter.second->get_extension()).casefold();
+ if (Glib::str_has_suffix(fn, ext))
+ key = iter.second;
+ }
+ }
+
+ FileDialog::setExtension(key);
+
+ // Ensure the proper entry in the combo box is selected.
+ if (auto omod = dynamic_cast<Inkscape::Extension::Output *>(key)) {
+ filterComboBox->set_active_text(omod->get_filetypename(true));
+ }
+}
+
+Glib::ustring FileSaveDialogImplGtk::getCurrentDirectory()
+{
+ return get_current_folder();
+}
+
+
+/**
+ * Change the default save path location.
+ */
+void FileSaveDialogImplGtk::change_path(const Glib::ustring &path)
+{
+ setFilename(path);
+
+ if (Glib::file_test(_filename, Glib::FILE_TEST_IS_DIR)) {
+ // fprintf(stderr,"set_current_folder(%s)\n",_filename.c_str());
+ set_current_folder(_filename);
+ } else {
+ // fprintf(stderr,"set_filename(%s)\n",_filename.c_str());
+ if (Glib::file_test(_filename, Glib::FILE_TEST_EXISTS)) {
+ set_filename(_filename);
+ } else {
+ std::string dirName = Glib::path_get_dirname(_filename);
+ if (dirName != get_current_folder()) {
+ set_current_folder(dirName);
+ }
+ }
+ Glib::ustring basename = Glib::path_get_basename(_filename);
+ // 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()) {
+ setFilename(tmp);
+ }
+
+ auto output = dynamic_cast<Inkscape::Extension::Output *>(getExtension());
+ if (fileTypeCheckbox.get_active() && output) {
+ // Append the file extension if it's not already present and display it in the file name entry field
+ appendExtension(_filename, output);
+ change_path(_filename);
+ }
+}
+
+
+} // 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..9f3d085
--- /dev/null
+++ b/src/ui/dialog/filedialogimpl-gtkmm.h
@@ -0,0 +1,274 @@
+// 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
+findEntryWidgets(Gtk::Container *parent,
+ std::vector<Gtk::Entry *> &result);
+
+void
+findExpanderWidgets(Gtk::Container *parent,
+ std::vector<Gtk::Expander *> &result);
+
+/*#########################################################################
+### 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;
+
+ /**
+ * Add a Gtk filter to our specially controlled filter dropdown.
+ */
+ Glib::RefPtr<Gtk::FileFilter> addFilter(const Glib::ustring &name, Glib::ustring pattern = "",
+ Inkscape::Extension::Extension *mod = nullptr);
+
+ Glib::ustring extToPattern(const Glib::ustring &extension) const;
+
+ virtual void filterChangedCallback() {}
+
+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;
+
+ /**
+ * Aquired Widgets
+ */
+ Gtk::ComboBoxText *filterComboBox;
+
+private:
+ void internalSetup();
+
+ /**
+ * Callback for seeing if the preview needs to be drawn
+ */
+ void _updatePreviewCallback();
+
+ /**
+ * Callback to for SVG 2 to SVG 1.1 export.
+ */
+ void _svgexportEnabledCB();
+
+ /**
+ * Overriden filter store.
+ */
+ Glib::RefPtr<Gtk::ListStore> filterStore;
+};
+
+
+
+
+/*#########################################################################
+### 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 = default;
+
+ bool show() override;
+
+ Glib::ustring getFilename();
+
+ std::vector<Glib::ustring> getFilenames() override;
+
+ Glib::ustring getCurrentDirectory() override;
+
+ void addFilterMenu(const Glib::ustring &name, Glib::ustring pattern = "",
+ Inkscape::Extension::Extension *mod = nullptr) override
+ {
+ addFilter(name, pattern, mod);
+ }
+
+private:
+
+ /**
+ * Create a filter menu for this type of dialog
+ */
+ void createFilterMenu();
+};
+
+
+
+//########################################################################
+//# 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 = default;
+
+ bool show() override;
+
+ Glib::ustring getCurrentDirectory() override;
+
+ void setExtension(Inkscape::Extension::Extension *key) override;
+
+ void addFilterMenu(const Glib::ustring &name, Glib::ustring pattern = "",
+ Inkscape::Extension::Extension *mod = nullptr) override
+ {
+ addFilter(name, pattern, mod);
+ }
+
+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;
+ Gtk::Box childBox;
+ Gtk::Box checksBox;
+ Gtk::CheckButton fileTypeCheckbox;
+
+ /**
+ * Callback for user input into fileNameEntry
+ */
+ void filterChangedCallback() override;
+
+ /**
+ * Create a filter menu for this type of dialog
+ */
+ void createFilterMenu();
+
+ /**
+ * 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..a75eb98
--- /dev/null
+++ b/src/ui/dialog/filedialogimpl-win32.cpp
@@ -0,0 +1,1923 @@
+// 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 <thread>
+#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);
+}
+
+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(const Glib::ustring &name, Glib::ustring pattern, Inkscape::Extension::Extension *mod)
+{
+ 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(
+ getFilename().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
+ setFilename(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);
+ std::unique_ptr<SPDocument> svgDoc(SPDocument::createNewDoc(utf8string, 0));
+ g_free(utf8string);
+
+ // Check the document loaded properly
+ if (svgDoc == NULL) {
+ return false;
+ }
+ if (svgDoc->getRoot() == NULL)
+ {
+ 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;
+
+ const double dpi = 96 * scaleFactor;
+ Geom::Rect area(0, 0, svgWidth_px, svgHeight_px);
+ Inkscape::Pixbuf *pixbuf =
+ sp_generate_internal_bitmap(svgDoc.get(), area, dpi);
+
+ // Tidy up
+ if (pixbuf == NULL) {
+ return false;
+ }
+
+ // Create the GDK pixbuf
+ _mutex->lock();
+ _preview_bitmap_image = Glib::wrap(pixbuf->getPixbufRaw(), /* ref */ true);
+ delete pixbuf;
+ _preview_document_width = svgWidth_px;
+ _preview_document_height = svgHeight_px;
+ _preview_image_width = round(scaleFactor * svgWidth_px);
+ _preview_image_height = round(scaleFactor * svgHeight_px);
+ _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;
+
+ _result = false;
+ _finished = false;
+ _file_selected = false;
+ _main_loop = g_main_loop_new(g_main_context_default(), FALSE);
+
+ _mutex = std::make_unique<std::mutex>();
+
+ std::thread thethread([this] { GetOpenFileName_thread(); });
+
+ {
+ while(1)
+ {
+ g_main_context_iteration(g_main_context_default(), FALSE);
+
+ if(_mutex->try_lock())
+ {
+ // 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);
+ }
+ }
+
+ thethread.join();
+
+ // Tidy up
+ _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")
+ , save_method(save_method)
+ , _title_label(NULL)
+ , _title_edit(NULL)
+{
+ FileSaveDialog::myDocTitle = docTitle;
+
+ if (dialogType != CUSTOM_TYPE)
+ createFilterMenu();
+
+ /* The code below sets the default file name */
+ setFilename("");
+ 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) {
+ setFilename(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) {
+ setFilename(udir.substr(0, last_period_index ));
+ }
+ }
+
+ // remove one slash if double
+ auto myFilename = getFilename();
+ if (1 + myFilename.find("\\\\",2)) {
+ myFilename.replace(myFilename.find("\\\\",2), 1, "");
+ }
+ setFilename(myFilename);
+ }
+}
+
+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 (auto omod : extension_list) {
+ // Windows OFN dialog does not allow disabled entries in the dialog
+ // So we just remove them. This is a regression from the Gtk dialog.
+ if (omod->deactivated())
+ continue;
+
+ // Export types are either exported vector types, or any raster type.
+ if (!omod->is_exported() && omod->is_raster() != (dialogType == EXPORT_TYPES))
+ continue;
+
+ // This extension is limited to save copy only.
+ if (omod->savecopy_only() && save_method != Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY)
+ 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(
+ getFilename().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
+ setFilename(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()
+{
+ _result = false;
+ _main_loop = g_main_loop_new(g_main_context_default(), FALSE);
+
+ if(_main_loop != NULL)
+ {
+ std::thread thethread([this] { GetSaveFileName_thread(); });
+ g_main_loop_run(_main_loop);
+
+ if(_result && _extension)
+ appendExtension(_filename, (Inkscape::Extension::Output*)_extension);
+
+ thethread.join();
+ }
+
+ return _result;
+}
+
+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..3b12f7c
--- /dev/null
+++ b/src/ui/dialog/filedialogimpl-win32.h
@@ -0,0 +1,371 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Implementation of native file dialogs for Win32
+ */
+/* Authors:
+ * Joel Holdsworth
+ * Inkscape Authors
+ *
+ * Copyright (C) 2004-2008 Inkscape Authors
+ *
+ * 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 <memory>
+#include <mutex>
+#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 browsing 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:
+
+ /// 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;
+};
+
+
+/*#########################################################################
+### 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 browsing 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(); }
+
+ /// Add a custom file filter menu item
+ /// @param name - Name of the filter (such as "Javscript")
+ /// @param pattern - File filtering pattern (such as "*.js")
+ /// Use the FileDialogType::CUSTOM_TYPE in constructor to not include other file types
+ void addFilterMenu(const Glib::ustring &name, Glib::ustring pattern = "", Inkscape::Extension::Extension *mod = nullptr) override;
+
+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
+ std::unique_ptr<std::mutex> _mutex;
+
+ /// 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(); }
+
+ void addFileType(Glib::ustring name, Glib::ustring pattern);
+ void addFilterMenu(const Glib::ustring &name, Glib::ustring pattern = "", Inkscape::Extension::Extension *mod = nullptr) override
+ {
+ }
+
+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();
+
+ // SaveAs or SaveAsCopy
+ Inkscape::Extension::FileSaveMethod save_method;
+
+ /// 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..5f24214
--- /dev/null
+++ b/src/ui/dialog/fill-and-stroke.cpp
@@ -0,0 +1,215 @@
+// 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 "svg/css-ostringstream.h"
+
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+
+#include "ui/widget/fill-style.h"
+#include "ui/widget/stroke-style.h"
+#include "ui/widget/notebook-page.h"
+
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+FillAndStroke::FillAndStroke()
+ : DialogBase("/dialogs/fillstroke", "FillStroke")
+ , _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(INKSCAPE_ICON("dialog-fill-and-stroke"),
+ "fillstroke",
+ UI::Widget::SimpleFilterModifier::ISOLATION |
+ UI::Widget::SimpleFilterModifier::BLEND |
+ UI::Widget::SimpleFilterModifier::BLUR |
+ UI::Widget::SimpleFilterModifier::OPACITY)
+ , fillWdgt(nullptr)
+ , strokeWdgt(nullptr)
+{
+ set_spacing(2);
+ 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();
+
+ pack_end(_composite_settings, Gtk::PACK_SHRINK);
+
+ show_all_children();
+
+ _composite_settings.setSubject(&_subject);
+}
+
+FillAndStroke::~FillAndStroke()
+{
+ // Disconnect signals from composite settings
+ _composite_settings.setSubject(nullptr);
+ fillWdgt->setDesktop(nullptr);
+ strokeWdgt->setDesktop(nullptr);
+ strokeStyleWdgt->setDesktop(nullptr);
+ _subject.setDesktop(nullptr);
+}
+
+void FillAndStroke::selectionChanged(Selection *selection)
+{
+ if (fillWdgt) {
+ fillWdgt->performUpdate();
+ }
+ if (strokeWdgt) {
+ strokeWdgt->performUpdate();
+ }
+ if (strokeStyleWdgt) {
+ strokeStyleWdgt->selectionChangedCB();
+ }
+}
+void FillAndStroke::selectionModified(Selection *selection, guint flags)
+{
+ if (fillWdgt) {
+ fillWdgt->selectionModifiedCB(flags);
+ }
+ if (strokeWdgt) {
+ strokeWdgt->selectionModifiedCB(flags);
+ }
+ if (strokeStyleWdgt) {
+ strokeStyleWdgt->selectionModifiedCB(flags);
+ }
+}
+
+void FillAndStroke::desktopReplaced()
+{
+ if (fillWdgt) {
+ fillWdgt->setDesktop(getDesktop());
+ }
+ if (strokeWdgt) {
+ strokeWdgt->setDesktop(getDesktop());
+ }
+ if (strokeStyleWdgt) {
+ strokeStyleWdgt->setDesktop(getDesktop());
+ }
+ _subject.setDesktop(getDesktop());
+}
+
+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(new UI::Widget::FillNStroke(FILL));
+ _page_fill->table().attach(*fillWdgt, 0, 0, 1, 1);
+}
+
+void
+FillAndStroke::_layoutPageStrokePaint()
+{
+ strokeWdgt = Gtk::manage(new UI::Widget::FillNStroke(STROKE));
+ _page_stroke_paint->table().attach(*strokeWdgt, 0, 0, 1, 1);
+}
+
+void
+FillAndStroke::_layoutPageStrokeStyle()
+{
+ strokeStyleWdgt = Gtk::manage(new UI::Widget::StrokeStyle());
+ strokeStyleWdgt->set_hexpand();
+ strokeStyleWdgt->set_halign(Gtk::ALIGN_START);
+ _page_stroke_style->table().attach(*strokeStyleWdgt, 0, 0, 1, 1);
+}
+
+void
+FillAndStroke::showPageFill()
+{
+ blink();
+ _notebook.set_current_page(0);
+ _savePagePref(0);
+
+}
+
+void
+FillAndStroke::showPageStrokePaint()
+{
+ blink();
+ _notebook.set_current_page(1);
+ _savePagePref(1);
+}
+
+void
+FillAndStroke::showPageStrokeStyle()
+{
+ blink();
+ _notebook.set_current_page(2);
+ _savePagePref(2);
+
+}
+
+Gtk::Box&
+FillAndStroke::_createPageTabLabel(const Glib::ustring& label, const char *label_image)
+{
+ Gtk::Box *_tab_label_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 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..9780f33
--- /dev/null
+++ b/src/ui/dialog/fill-and-stroke.h
@@ -0,0 +1,92 @@
+// 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 <gtkmm/notebook.h>
+
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/object-composite-settings.h"
+#include "ui/widget/style-subject.h"
+
+namespace Inkscape {
+namespace UI {
+
+namespace Widget {
+class FillNStroke;
+class NotebookPage;
+class StrokeStyle;
+}
+
+namespace Dialog {
+
+class FillAndStroke : public DialogBase
+{
+public:
+ FillAndStroke();
+ ~FillAndStroke() override;
+
+ void desktopReplaced() override;
+
+ 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::Box &_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:
+ void selectionChanged(Selection *selection) override;
+ void selectionModified(Selection *selection, guint flags) override;
+
+ UI::Widget::FillNStroke *fillWdgt;
+ UI::Widget::FillNStroke *strokeWdgt;
+ UI::Widget::StrokeStyle *strokeStyleWdgt;
+};
+
+} // 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-effects-dialog.cpp b/src/ui/dialog/filter-effects-dialog.cpp
new file mode 100644
index 0000000..7429398
--- /dev/null
+++ b/src/ui/dialog/filter-effects-dialog.cpp
@@ -0,0 +1,3376 @@
+// 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 <cmath>
+#include <gdkmm/pixbufanimation.h>
+#include <gdkmm/rgba.h>
+#include <glibmm/refptr.h>
+#include <glibmm/ustring.h>
+#include <gtkmm/box.h>
+#include <gtkmm/button.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/image.h>
+#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/label.h>
+#include <gtkmm/menuitem.h>
+#include <gtkmm/paned.h>
+#include <gtkmm/popover.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/sizegroup.h>
+
+#include <glibmm/i18n.h>
+#include <glibmm/stringutils.h>
+#include <glibmm/main.h>
+#include <glibmm/convert.h>
+
+#include <gtkmm/textview.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/treeviewcolumn.h>
+#include <gtkmm/widget.h>
+#include <pangomm/layout.h>
+#include <utility>
+#include <vector>
+
+#include "desktop.h"
+#include "display/nr-filter-types.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 "layer-manager.h"
+#include "selection-chemistry.h"
+#include "style.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 "svg/svg-color.h"
+
+#include "ui/builder-utils.h"
+#include "ui/column-menu-builder.h"
+#include "ui/dialog/filedialog.h"
+#include "ui/icon-names.h"
+#include "ui/util.h"
+#include "ui/widget/color-picker.h"
+#include "ui/widget/completion-popup.h"
+#include "ui/widget/custom-tooltip.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;
+
+constexpr int max_convolution_kernel_size = 10;
+
+// Returns the number of inputs available for the filter primitive type
+static int input_count(const SPFilterPrimitive* prim)
+{
+ if(!prim)
+ return 0;
+ else if(is<SPFeBlend>(prim) || is<SPFeComposite>(prim) || is<SPFeDisplacementMap>(prim))
+ return 2;
+ else if(is<SPFeMerge>(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 SPAttr 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 SPAttr 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 SPAttr a = SPAttr::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::Box
+{
+public:
+ MultiSpinButton(double lower, double upper, double step_inc,
+ double climb_rate, int digits, std::vector<SPAttr> attrs, std::vector<double> default_values, std::vector<char*> tip_text)
+ : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)
+ {
+ 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(), true, true);
+ _spins.back()->set_width_chars(3); // allow spin buttons to shrink to save space
+ }
+ }
+
+ ~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::Box, public AttrWidget
+{
+public:
+ DualSpinButton(char* def, double lower, double upper, double step_inc,
+ double climb_rate, int digits, const SPAttr a, char* tt1, char* tt2)
+ : AttrWidget(a, def), //TO-DO: receive default num-opt-num as parameter in the constructor
+ Gtk::Box(Gtk::ORIENTATION_HORIZONTAL),
+ _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, true, true);
+ pack_end(_s1, true, true);
+ }
+
+ 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 Widget::ColorPicker, public AttrWidget
+{
+public:
+ ColorButton(unsigned int def, const SPAttr a, char* tip_text)
+ : ColorPicker(_("Select color"), tip_text ? tip_text : "", 0x000000ff, false),
+ AttrWidget(a, def)
+ {
+ use_transparency(false);
+ connectChanged([=](guint rgba){ signal_attr_changed().emit(); });
+ if (tip_text) {
+ set_tooltip_text(tip_text);
+ }
+ setRgba32(0xffffffff);
+ }
+
+ // 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_current_color();
+ int r = SP_RGBA32_R_U(c);
+ int g = SP_RGBA32_G_U(c);
+ int b = SP_RGBA32_B_U(c);
+ 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();
+ }
+ setRgba32(i);
+ }
+};
+
+// Used for tableValue in feComponentTransfer
+class EntryAttr : public Gtk::Entry, public AttrWidget
+{
+public:
+ EntryAttr(const SPAttr a, char* tip_text)
+ : AttrWidget(a)
+ {
+ set_width_chars(3); // let it get narrow
+ 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 SPAttr 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(is<SPFeConvolveMatrix>(o)) {
+ auto conv = cast<SPFeConvolveMatrix>(o);
+ int cols, rows;
+ cols = (int)conv->get_order().getNumber();
+ if (cols > max_convolution_kernel_size)
+ cols = max_convolution_kernel_size;
+ rows = conv->get_order().optNumIsSet() ? (int)conv->get_order().getOptNumber() : cols;
+ update(o, rows, cols);
+ }
+ else if(is<SPFeColorMatrix>(o))
+ update(o, 4, 5);
+ }
+ }
+private:
+ class MatrixColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ MatrixColumns()
+ {
+ cols.resize(max_convolution_kernel_size);
+ 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> const *values = nullptr;
+ if(is<SPFeColorMatrix>(o))
+ values = &cast<SPFeColorMatrix>(o)->get_values();
+ else if(is<SPFeConvolveMatrix>(o))
+ values = &cast<SPFeConvolveMatrix>(o)->get_kernel_matrix();
+ else
+ return;
+
+ if(o) {
+ int ndx = 0;
+
+ for(int i = 0; i < cols; ++i) {
+ _tree.append_column_numeric_editable("", _columns.cols[i], "%.2f");
+ static_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(SPAttr::VALUES),
+ // TRANSLATORS: this dialog is accessible via menu Filters - Filter editor
+ _matrix(SPAttr::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("", 1, 0, 1, 0.1, 0.01, 2, SPAttr::VALUES),
+ _angle("", 0, 0, 360, 0.1, 0.01, 1, SPAttr::VALUES),
+ _label(C_("Label", "None"), Gtk::ALIGN_START),
+ _use_stored(false),
+ _saturation_store(1.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
+ {
+ std::string values_string;
+ if(is<SPFeColorMatrix>(o)) {
+ auto col = cast<SPFeColorMatrix>(o);
+ remove();
+ switch(col->get_type()) {
+ case COLORMATRIX_SATURATE:
+ add(_saturation);
+ if(_use_stored)
+ _saturation.set_value(_saturation_store);
+ else
+ _saturation.set_from_attribute(o);
+ values_string = Glib::Ascii::dtostr(_saturation.get_value());
+ break;
+ case COLORMATRIX_HUEROTATE:
+ add(_angle);
+ if(_use_stored)
+ _angle.set_value(_angle_store);
+ else
+ _angle.set_from_attribute(o);
+ values_string = Glib::Ascii::dtostr(_angle.get_value());
+ 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);
+ for (auto v : _matrix.get_values()) {
+ values_string += Glib::Ascii::dtostr(v) + " ";
+ }
+ values_string.pop_back();
+ break;
+ }
+
+ // The filter effects widgets derived from AttrWidget automatically update the
+ // attribute on use. In this case, however, we must also update "values" whenever
+ // "type" is changed.
+ auto repr = o->getRepr();
+ if (values_string.empty()) {
+ repr->removeAttribute("values");
+ } else {
+ repr->setAttribute("values", values_string);
+ }
+
+ _use_stored = true;
+ }
+ }
+
+ Glib::ustring get_as_attribute() const override
+ {
+ const Widget* w = get_child();
+ if(w == &_label)
+ return "";
+ if (auto attrw = dynamic_cast<const AttrWidget *>(w))
+ return attrw->get_as_attribute();
+ g_assert_not_reached();
+ return "";
+ }
+
+ 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::Box, public AttrWidget
+{
+public:
+ FileOrElementChooser(FilterEffectsDialog& d, const SPAttr a)
+ : AttrWidget(a)
+ , _dialog(d)
+ , Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)
+ {
+ set_spacing(3);
+ pack_start(_entry, true, true);
+ pack_start(_fromFile, false, false);
+ pack_start(_fromSVGElement, false, false);
+
+ _fromFile.set_image_from_icon_name("document-open");
+ _fromFile.set_tooltip_text(_("Choose image file"));
+ _fromFile.signal_clicked().connect(sigc::mem_fun(*this, &FileOrElementChooser::select_file));
+
+ _fromSVGElement.set_label(_("SVG Element"));
+ _fromSVGElement.set_tooltip_text(_("Use selected SVG element"));
+ _fromSVGElement.signal_clicked().connect(sigc::mem_fun(*this, &FileOrElementChooser::select_svg_element));
+
+ _entry.set_width_chars(1);
+ _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("");
+ }
+ }
+
+private:
+ void select_svg_element() {
+ Inkscape::Selection* sel = _dialog.getDesktop()->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(
+ *_dialog.getDesktop()->getToplevel(),
+ open_path,
+ Inkscape::UI::Dialog::SVG_TYPES,/*TODO: any image, not just svg*/
+ (char const *)_("Select an image to be used as 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;
+ FilterEffectsDialog &_dialog;
+};
+
+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::Box(Gtk::ORIENTATION_VERTICAL, 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;
+ }
+ }
+
+ void show_current_only() {
+ for (auto& group : _groups) {
+ group->hide();
+ }
+ auto t = get_current_type();
+ if (t >= 0) {
+ _groups[t]->show();
+ }
+ }
+
+ // 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.")));
+ lbl->set_line_wrap();
+ lbl->set_line_wrap_mode(Pango::WRAP_WORD);
+ 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 SPAttr 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 SPAttr 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 SPAttr 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 SPAttr attr, const Glib::ustring& label,
+ const double lo, const double hi, const double step_inc, const double page_inc, 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, page_inc, digits, attr, tip_text2);
+ add_widget(spinslider, label);
+ add_attr_widget(spinslider);
+ return spinslider;
+ }
+
+ // DualSpinScale
+ DualSpinScale* add_dualspinscale(const SPAttr 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 SPAttr 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 SPAttr 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 SPAttr attr1, const SPAttr 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<SPAttr> 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 SPAttr attr1, const SPAttr attr2,
+ const SPAttr 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<SPAttr> 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 SPAttr attr, const Glib::ustring& label)
+ {
+ FileOrElementChooser* foech = new FileOrElementChooser(_dialog, attr);
+ add_widget(foech, label);
+ add_attr_widget(foech);
+ return foech;
+ }
+
+ // ComboBoxEnum
+ template<typename T> ComboBoxEnum<T>* add_combo(T default_value, const SPAttr 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 SPAttr 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::Box *hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
+ hb->set_spacing(6);
+
+ 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::Box*> _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(SPAttr::INVALID),
+ _dialog(d),
+ _settings(d, _box, sigc::mem_fun(*this, &ComponentTransferValues::set_func_attr), COMPONENTTRANSFER_TYPE_ERROR),
+ _type(ComponentTransferTypeConverter, SPAttr::TYPE, false),
+ _channel(channel),
+ _funcNode(nullptr),
+ _box(Gtk::ORIENTATION_VERTICAL)
+ {
+ set_shadow_type(Gtk::SHADOW_NONE);
+ 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, SPAttr::SLOPE, _("Slope"), -10, 10, 0.1, 0.01, 2);
+ _settings.add_spinscale(0, SPAttr::INTERCEPT, _("Intercept"), -10, 10, 0.1, 0.01, 2);
+
+ _settings.type(COMPONENTTRANSFER_TYPE_GAMMA);
+ _settings.add_spinscale(1, SPAttr::AMPLITUDE, _("Amplitude"), 0, 10, 0.1, 0.01, 2);
+ _settings.add_spinscale(1, SPAttr::EXPONENT, _("Exponent"), 0, 10, 0.1, 0.01, 2);
+ _settings.add_spinscale(0, SPAttr::OFFSET, _("Offset"), -10, 10, 0.1, 0.01, 2);
+
+ _settings.type(COMPONENTTRANSFER_TYPE_TABLE);
+ _settings.add_entry(SPAttr::TABLEVALUES, _("Values"), _("List of stops with interpolated output"));
+
+ _settings.type(COMPONENTTRANSFER_TYPE_DISCRETE);
+ _settings.add_entry(SPAttr::TABLEVALUES, _("Values"), _("List of discrete values for a step function"));
+
+ //_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 = cast<SPFeFuncNode>(&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(is<SPFeComponentTransfer>(o)) {
+ auto ct = cast<SPFeComponentTransfer>(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::cerr << "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, _("New transfer function type"), INKSCAPE_ICON("dialog-filters"));
+ 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::Box _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(SPAttr::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),
+ _box(Gtk::ORIENTATION_VERTICAL),
+ _light_box(Gtk::ORIENTATION_HORIZONTAL)
+ {
+ _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(6);
+
+ _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, SPAttr::AZIMUTH, _("Azimuth:"), 0, 360, 1, 1, 0, _("Direction angle for the light source on the XY plane, in degrees"));
+ _settings.add_spinscale(0, SPAttr::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, SPAttr::X, SPAttr::Y, SPAttr::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, SPAttr::X, SPAttr::Y, SPAttr::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,
+ SPAttr::POINTSATX, SPAttr::POINTSATY, SPAttr::POINTSATZ,
+ _("Points at:"), -99999, 99999, 1, 100, 0, _("X coordinate"), _("Y coordinate"), _("Z coordinate"));
+ _settings.add_spinscale(1, SPAttr::SPECULAREXPONENT, _("Specular Exponent:"), 0.1, 100, 0.1, 1, 1, _("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, SPAttr::LIMITINGCONEANGLE, _("Cone Angle:"), 0, 180, 1, 5, 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::Box& 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(is<SPFeDistantLight>(child))
+ _light_source.set_active(0);
+ else if(is<SPFePointLight>(child))
+ _light_source.set_active(1);
+ else if(is<SPFeSpotLight>(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 && is<SPFeDistantLight>(child)) &&
+ !(ls == 1 && is<SPFePointLight>(child)) &&
+ !(ls == 2 && is<SPFeSpotLight>(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, _("New light source"), INKSCAPE_ICON("dialog-filters"));
+ update();
+ }
+
+ _locked = false;
+ }
+ }
+
+ void update()
+ {
+ _box.show();
+
+ SPFilterPrimitive* prim = _dialog._primitive_list.get_selected();
+ if (prim && prim->firstChild()) {
+ _settings.show_and_update(_light_source.get_active_data()->id, prim->firstChild());
+ }
+ else {
+ _settings.show_current_only();
+ }
+ }
+
+ FilterEffectsDialog& _dialog;
+ Gtk::Box _box;
+ Settings _settings;
+ Gtk::Box _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);
+ ct->set_margin_top(4);
+ ct->set_margin_bottom(4);
+ 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, Glib::RefPtr<Gtk::Builder> builder)
+ : Gtk::Box(Gtk::ORIENTATION_VERTICAL),
+ _builder(std::move(builder)),
+ _list(get_widget<Gtk::TreeView>(_builder, "filter-list")),
+ _dialog(d),
+ _add(get_widget<Gtk::Button>(_builder, "btn-new")),
+ _dup(get_widget<Gtk::Button>(_builder, "btn-dup")),
+ _del(get_widget<Gtk::Button>(_builder, "btn-del")),
+ _select(get_widget<Gtk::Button>(_builder, "btn-select")),
+ _menu(get_widget<Gtk::Menu>(_builder, "filters-ctx-menu")),
+ _observer(new Inkscape::XML::SignalObserver)
+{
+ _filters_model = Gtk::ListStore::create(_columns);
+ _list.set_model(_filters_model);
+ _cell_toggle.set_radio();
+ _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);
+ static_cast<Gtk::CellRendererText*>(_list.get_column(1)->get_first_cell())->
+ signal_edited().connect(sigc::mem_fun(*this, &FilterEffectsDialog::FilterModifier::on_name_edited));
+
+ _list.append_column(_("Used"), _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);
+
+ _list.get_column(1)->set_resizable(true);
+ _list.get_column(1)->set_sizing(Gtk::TREE_VIEW_COLUMN_FIXED);
+ _list.get_column(1)->set_expand(true);
+
+ _list.set_reorderable(false);
+ _list.enable_model_drag_dest(Gdk::ACTION_MOVE);
+
+ _list.signal_drag_drop().connect( sigc::mem_fun(*this, &FilterModifier::on_filter_move), false );
+
+ _add.signal_clicked().connect([=]() { add_filter(); });
+ _del.signal_clicked().connect([=]() { remove_filter(); });
+ _dup.signal_clicked().connect([=]() { duplicate_filter(); });
+ _select.signal_clicked().connect([=]() { select_filter_elements(); });
+
+ _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));
+
+ // connect handlers to context menu items
+ auto&& items = _menu.get_children();
+ auto funcs = { &FilterModifier::duplicate_filter, &FilterModifier::remove_filter, &FilterModifier::rename_filter, &FilterModifier::select_filter_elements };
+ int index = 0;
+ for (auto fn : funcs) {
+ static_cast<Gtk::MenuItem*>(items.at(index++))->signal_activate().connect([=](){
+ (this->*fn)();
+ });
+ }
+
+ _list.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &FilterModifier::on_filter_selection_changed));
+ _observer->signal_changed().connect(signal_filter_changed().make_slot());
+}
+
+// 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<SPFilter*> used;
+
+ for (auto obj : sel->items()) {
+ SPStyle *style = obj->style;
+ if (!style || !obj) {
+ continue;
+ }
+
+ if (style->filter.set && style->getFilter()) {
+ //TODO: why is this needed?
+ obj->bbox_valid = FALSE;
+ used.insert(style->getFilter());
+ }
+ }
+
+ const int size = used.size();
+
+ for (auto&& item : _filters_model->children()) {
+ if (used.count(item[_columns.filter])) {
+ // If only one filter is in use by the selection, select it
+ if (size == 1) {
+ _list.get_selection()->select(item);
+ }
+ item[_columns.sel] = size;
+ } else {
+ item[_columns.sel] = 0;
+ }
+ }
+ update_counts();
+ _signal_filters_updated.emit();
+}
+
+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)
+{
+ if (auto iter = _filters_model->get_iter(path)) {
+ SPFilter* filter = (*iter)[_columns.filter];
+ filter->setLabel(text.c_str());
+ DocumentUndo::done(filter->document, _("Rename filter"), INKSCAPE_ICON("dialog-filters"));
+ 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 = _filters_model->get_iter(path);
+ selection_toggled(iter, false);
+}
+
+void FilterEffectsDialog::FilterModifier::selection_toggled(Gtk::TreeIter iter, bool toggle) {
+ if (!iter) return;
+
+ SPDesktop *desktop = _dialog.getDesktop();
+ SPDocument *doc = desktop->getDocument();
+ Inkscape::Selection *sel = desktop->getSelection();
+ SPFilter* filter = (*iter)[_columns.filter];
+
+ /* If this filter is the only one used in the selection, unset it */
+ if ((*iter)[_columns.sel] == 1 && toggle) {
+ filter = nullptr;
+ }
+
+ for (auto item : sel->items()) {
+ SPStyle *style = item->style;
+ g_assert(style != nullptr);
+
+ if (filter && filter->valid_for(item)) {
+ 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, _("Apply filter"), INKSCAPE_ICON("dialog-filters"));
+}
+
+void FilterEffectsDialog::FilterModifier::update_counts()
+{
+ for (auto&& item : _filters_model->children()) {
+ SPFilter* f = item[_columns.filter];
+ item[_columns.count] = f->getRefCount();
+ }
+}
+
+static Glib::ustring get_filter_name(SPFilter* filter) {
+ if (!filter) return Glib::ustring();
+
+ if (auto label = filter->label()) {
+ return label;
+ }
+ else if (auto id = filter->getId()) {
+ return id;
+ }
+ else {
+ return _("filter");
+ }
+}
+
+/* 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()
+{
+ auto document = _dialog.getDocument();
+ if (!document) return; // no document at shut down
+
+ std::vector<SPObject *> filters = document->getResourceList("filter");
+
+ _filters_model->clear();
+ SPFilter* first = nullptr;
+
+ for (auto filter : filters) {
+ Gtk::TreeModel::Row row = *_filters_model->append();
+ auto f = cast<SPFilter>(filter);
+ row[_columns.filter] = f;
+ row[_columns.label] = get_filter_name(f);
+ if (!first) {
+ first = f;
+ }
+ }
+
+ update_selection(_dialog.getSelection());
+ if (first) {
+ select_filter(first);
+ }
+ _dialog.update_filter_general_settings_view();
+ _dialog.update_settings_view();
+}
+
+bool FilterEffectsDialog::FilterModifier::is_selected_filter_active() {
+ if (auto&& sel = _list.get_selection()) {
+ if (Gtk::TreeModel::iterator it = sel->get_selected()) {
+ return (*it)[_columns.sel] > 0;
+ }
+ }
+
+ return false;
+}
+
+bool FilterEffectsDialog::FilterModifier::filters_present() const {
+ return !_filters_model->children().empty();
+}
+
+void FilterEffectsDialog::FilterModifier::toggle_current_filter() {
+ if (auto&& sel = _list.get_selection()) {
+ selection_toggled(sel->get_selected(), true);
+ }
+}
+
+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) return;
+
+ for (auto&& item : _filters_model->children()) {
+ if (item[_columns.filter] == filter) {
+ _list.get_selection()->select(item);
+ 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);
+ items[3]->set_sensitive(sensitive);
+
+ _menu.popup_at_pointer(reinterpret_cast<GdkEvent*>(event));
+ }
+}
+
+void FilterEffectsDialog::FilterModifier::add_filter()
+{
+ SPDocument* doc = _dialog.getDocument();
+ SPFilter* filter = new_filter(doc);
+
+ const int count = _filters_model->children().size();
+ std::ostringstream os;
+ os << _("filter") << count;
+ filter->setLabel(os.str().c_str());
+
+ update_filters();
+
+ select_filter(filter);
+
+ DocumentUndo::done(doc, _("Add filter"), INKSCAPE_ICON("dialog-filters"));
+}
+
+void FilterEffectsDialog::FilterModifier::remove_filter()
+{
+ SPFilter *filter = get_selected_filter();
+
+ if(filter) {
+ auto desktop = _dialog.getDesktop();
+ SPDocument* doc = filter->document;
+
+ // Delete all references to this filter
+ auto all = get_all_items(desktop->layerManager().currentRoot(), desktop, false, false, true);
+ for (auto item : all) {
+ if (!item) {
+ continue;
+ }
+ 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, _("Remove filter"), INKSCAPE_ICON("dialog-filters"));
+
+ update_filters();
+
+ // select first filter to avoid empty dialog after filter deletion
+ const auto& filters = _filters_model->children();
+ if (!filters.empty()) {
+ _list.get_selection()->select(filters[0]);
+ }
+ }
+}
+
+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, _("Duplicate filter"), INKSCAPE_ICON("dialog-filters"));
+
+ update_filters();
+ }
+}
+
+void FilterEffectsDialog::FilterModifier::rename_filter()
+{
+ _list.set_cursor(_filters_model->get_path(_list.get_selection()->get_selected()), *_list.get_column(1), true);
+}
+
+void FilterEffectsDialog::FilterModifier::select_filter_elements()
+{
+ SPFilter *filter = get_selected_filter();
+ auto desktop = _dialog.getDesktop();
+
+ if(!filter)
+ return;
+
+ std::vector<SPItem*> items;
+ auto all = get_all_items(desktop->layerManager().currentRoot(), desktop, false, false, true);
+ for(SPItem *item: all) {
+ if (!item->style) {
+ continue;
+ }
+
+ SPIFilter const &ifilter = item->style->filter;
+ if (ifilter.href) {
+ const SPObject *obj = ifilter.href->getObject();
+ if (obj && obj == (SPObject *)filter) {
+ items.push_back(item);
+ }
+ }
+ }
+ desktop->getSelection()->setList(items);
+}
+
+FilterEffectsDialog::CellRendererConnection::CellRendererConnection()
+ : Glib::ObjectBase(typeid(CellRendererConnection))
+ , _primitive(*this, "primitive", nullptr)
+{}
+
+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
+{
+ auto& primlist = dynamic_cast<PrimitiveList&>(widget);
+ int count = primlist.get_inputs_count();
+ minimum_width = natural_width = size_w * primlist.primitive_count() + primlist.get_input_type_width() * count;
+}
+
+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.
+ auto prim = reinterpret_cast<SPFilterPrimitive*>(_primitive.get_value());
+ minimum_height = natural_height = size_h * 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)
+{
+ _inputs_count = FPInputConverter._length;
+
+ 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(false);
+
+ _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) {
+ auto prim = cast<SPFilterPrimitive>(&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 (auto&& item : _model->children()) {
+ if (item[_columns.primitive] == prim) {
+ get_selection()->select(item);
+ break;
+ }
+ }
+}
+
+void FilterEffectsDialog::PrimitiveList::remove_selected()
+{
+ if (SPFilterPrimitive* prim = get_selected()) {
+ _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.getDocument(), _("Remove filter primitive"), INKSCAPE_ICON("dialog-filters"));
+
+ update();
+ }
+}
+
+void draw_connection_node(const Cairo::RefPtr<Cairo::Context>& cr,
+ const std::vector<Gdk::Point>& points,
+ Gdk::RGBA fill, Gdk::RGBA stroke);
+
+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 = get_style_context();
+
+ // TODO: In Gtk+ 4, the state is not used in get_color
+ auto state = sc->get_state();
+ auto bg_color = get_background_color(sc, state);
+ auto orig_color = sc->get_color(state);
+ Gdk::RGBA fg_color = orig_color;
+ auto bar_color = mix_colors(bg_color, orig_color, 0.06);
+ // color of connector arrow heads and effect separator lines
+ auto mid_color = mix_colors(bg_color, fg_color, 0.16);
+
+ SPFilterPrimitive* prim = get_selected();
+ int row_count = get_model()->children().size();
+
+ int fwidth = CellRendererConnection::size_w;
+ 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() * _inputs_count + 1;
+
+ auto w = get_input_type_width();
+ auto h = vis.get_height();
+ cr->save();
+ // erase selection color from selected item
+ Gdk::Cairo::set_source_rgba(cr, bg_color);
+ cr->rectangle(text_start_x + 1, 0, w * _inputs_count, h);
+ cr->fill();
+ auto text_color = fg_color;
+ text_color.set_alpha(0.7);
+
+ // draw vertical bars corresponding to possible filter inputs
+ for(unsigned int i = 0; i < _inputs_count; ++i) {
+ _vertical_layout->set_text(_(FPInputConverter.get_label((FilterPrimitiveInput)i).c_str()));
+ const int x = text_start_x + w * i;
+ cr->save();
+
+ Gdk::Cairo::set_source_rgba(cr, bar_color);
+ cr->rectangle(x + 1, 0, w - 2, h);
+ cr->fill();
+
+ Gdk::Cairo::set_source_rgba(cr, text_color);
+ cr->move_to(x + w, 5);
+ cr->rotate_degrees(90);
+ _vertical_layout->show_in_cairo_context(cr);
+
+ cr->restore();
+ }
+
+ cr->restore();
+ cr->rectangle(vis.get_x(), 0, vis.get_width(), vis.get_height());
+ cr->clip();
+ }
+
+ 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();
+ cr->set_line_width(1);
+ get_bin_window()->get_device_position(device, mx, my, mask);
+
+ // Outline the bottom of the connection area
+ const int outline_x = x + fwidth * (row_count - row_index);
+ cr->save();
+
+ Gdk::Cairo::set_source_rgba(cr, 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(is<SPFeMerge>(row_prim)) {
+ for(int i = 0; i < inputs; ++i) {
+ inside = do_connection_node(row, i, con_poly, mx, my);
+
+ draw_connection_node(cr, con_poly, inside ? fg_color : mid_color, fg_color);
+
+ 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, SPAttr::INVALID, text_start_x, outline_x, con_poly[2].get_y(), row_count, i, 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();
+
+ draw_connection_node(cr, con_poly, inside ? fg_color : mid_color, fg_color);
+
+ // Draw "in" connection
+ if(_in_drag != 1 || row_prim != prim) {
+ draw_connection(cr, row, SPAttr::IN_, text_start_x, outline_x, con_poly[2].get_y(), row_count, -1, 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();
+ }
+
+ draw_connection_node(cr, con_poly, inside ? fg_color : mid_color, fg_color);
+
+ // Draw "in2" connection
+ if(_in_drag != 2 || row_prim != prim) {
+ draw_connection(cr, row, SPAttr::IN2, text_start_x, outline_x, con_poly[2].get_y(), row_count, -1, fg_color, mid_color);
+ }
+ }
+ }
+
+ // Draw drag connection
+ if(row_prim == prim && _in_drag) {
+ cr->save();
+ Gdk::Cairo::set_source_rgba(cr, 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 SPAttr attr,
+ const int text_start_x, const int x1, const int y1,
+ const int row_count, const int pos,
+ const Gdk::RGBA fg_color, const Gdk::RGBA mid_color)
+{
+ cr->save();
+
+ int src_id = 0;
+ Gtk::TreeIter res = find_result(input, attr, src_id, pos);
+
+ const bool is_first = input == get_model()->children().begin();
+ const bool is_merge = is<SPFeMerge>((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 + 1;
+
+ if(use_default && is_first) {
+ Gdk::Cairo::set_source_rgba(cr, fg_color);
+ cr->set_dash(std::vector<double> {1.0, 1.0}, 0);
+ } else {
+ Gdk::Cairo::set_source_rgba(cr, fg_color);
+ }
+
+ // draw a half-circle touching destination band
+ cr->move_to(x1, y1);
+ cr->line_to(end_x, y1);
+ cr->stroke();
+ cr->arc(end_x, y1, 4, M_PI / 2, M_PI * 1.5);
+ cr->fill();
+ }
+ 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_h;
+ const int fwidth = CellRendererConnection::size_w;
+
+ 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() + fwidth * (row_count - row_index) - fwidth / 2;
+ const int y2 = rct.get_y() + rct.get_height();
+
+ // Draw a bevelled 'L'-shaped connection
+ Gdk::Cairo::set_source_rgba(cr, fg_color);
+ cr->move_to(x1, y1);
+ cr->line_to(x2 - fwidth/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 draw_connection_node(const Cairo::RefPtr<Cairo::Context>& cr,
+ const std::vector<Gdk::Point>& points,
+ Gdk::RGBA fill, Gdk::RGBA stroke)
+{
+ 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);
+ cr->close_path();
+
+ Gdk::Cairo::set_source_rgba(cr, fill);
+ cr->fill_preserve();
+ cr->set_line_width(1);
+ Gdk::Cairo::set_source_rgba(cr, stroke);
+ 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_h;
+ const int fwidth = CellRendererConnection::size_w;
+
+ get_cell_area(_model->get_path(row), *get_column(1), rct);
+ const float h = rct.get_height() / icnt;
+
+ const int x = rct.get_x() + fwidth * (_model->children().size() - find_index(row));
+ // this is how big arrowhead appears:
+ const int con_w = (int)(fwidth * 0.70f);
+ const int con_h = (int)(fheight * 0.35f);
+ const int con_y = (int)(rct.get_y() + (h / 2) - con_h + (input * h));
+ points.clear();
+ points.emplace_back(x, con_y);
+ points.emplace_back(x, con_y + con_h * 2);
+ points.emplace_back(x - con_w, con_y + con_h);
+
+ 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 SPAttr attr, int& src_id,
+ const int pos)
+{
+ SPFilterPrimitive* prim = (*start)[_columns.primitive];
+ Gtk::TreeIter target = _model->children().end();
+ int image = 0;
+
+ if(is<SPFeMerge>(prim)) {
+ int c = 0;
+ bool found = false;
+ for (auto& o: prim->children) {
+ if(c == pos && is<SPFeMergeNode>(&o)) {
+ image = cast<SPFeMergeNode>(&o)->get_in();
+ found = true;
+ }
+ ++c;
+ }
+ if(!found)
+ return target;
+ }
+ else {
+ if(attr == SPAttr::IN_)
+ image = prim->get_in();
+ else if(attr == SPAttr::IN2) {
+ if(is<SPFeBlend>(prim))
+ image = cast<SPFeBlend>(prim)->get_in2();
+ else if(is<SPFeComposite>(prim))
+ image = cast<SPFeComposite>(prim)->get_in2();
+ else if(is<SPFeDisplacementMap>(prim))
+ image = cast<SPFeDisplacementMap>(prim)->get_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])->get_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 * _inputs_count;
+ if(cx > sources_x) {
+ int src = (cx - sources_x) / twidth;
+ if (src < 0) {
+ src = 0;
+ } else if(src >= static_cast<int>(_inputs_count)) {
+ src = _inputs_count - 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 = cast<SPFilter>(prim->parent)->get_new_result_name();
+ repr->setAttributeOrRemoveIfEmpty("result", result);
+ in_val = result.c_str();
+ }
+ else
+ in_val = gres;
+ break;
+ }
+ }
+ }
+
+ if(is<SPFeMerge>(prim)) {
+ int c = 1;
+ bool handled = false;
+ for (auto& o: prim->children) {
+ if(c == _in_drag && is<SPFeMergeNode>(&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, _("Remove merge node"), INKSCAPE_ICON("dialog-filters"));
+ (*get_selection()->get_selected())[_columns.primitive] = prim;
+ } else {
+ _dialog.set_attr(&o, SPAttr::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);
+ auto node = cast<SPFeMergeNode>(prim->document->getObjectByRepr(repr));
+ Inkscape::GC::release(repr);
+ _dialog.set_attr(node, SPAttr::IN_, in_val);
+ (*get_selection()->get_selected())[_columns.primitive] = prim;
+ }
+ }
+ else {
+ if(_in_drag == 1)
+ _dialog.set_attr(prim, SPAttr::IN_, in_val);
+ else if(_in_drag == 2)
+ _dialog.set_attr(prim, SPAttr::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->get_in() == result) {
+ prim->removeAttribute("in");
+ }
+
+ if (auto blend = cast<SPFeBlend>(prim)) {
+ if (blend->get_in2() == result) {
+ prim->removeAttribute("in2");
+ }
+ } else if (auto comp = cast<SPFeComposite>(prim)) {
+ if (comp->get_in2() == result) {
+ prim->removeAttribute("in2");
+ }
+ } else if (auto disp = cast<SPFeDisplacementMap>(prim)) {
+ if (disp->get_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->get_out());
+ else
+ check_single_connection(prim, cur_prim->get_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, _("Reorder filter primitive"), INKSCAPE_ICON("dialog-filters"));
+}
+
+// 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 = static_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 = static_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;
+}
+
+int FilterEffectsDialog::PrimitiveList::get_inputs_count() const {
+ return _inputs_count;
+}
+
+void FilterEffectsDialog::PrimitiveList::set_inputs_count(int count) {
+ _inputs_count = count;
+ queue_allocate();
+ queue_draw();
+}
+
+enum class EffectCategory { Effect, Compose, Colors, Generation };
+
+const Glib::ustring& get_category_name(EffectCategory category) {
+ static const std::map<EffectCategory, Glib::ustring> category_names = {
+ { EffectCategory::Effect, _("Effect") },
+ { EffectCategory::Compose, _("Compositing") },
+ { EffectCategory::Colors, _("Color editing") },
+ { EffectCategory::Generation, _("Generating") },
+ };
+ return category_names.at(category);
+}
+
+struct EffectMetadata {
+ EffectCategory category;
+ Glib::ustring icon_name;
+ Glib::ustring tooltip;
+};
+
+static const std::map<Inkscape::Filters::FilterPrimitiveType, EffectMetadata>& get_effects() {
+ static std::map<Inkscape::Filters::FilterPrimitiveType, EffectMetadata> effects = {
+ { NR_FILTER_GAUSSIANBLUR, { EffectCategory::Effect, "feGaussianBlur-icon",
+ _("Uniformly blurs its input. Commonly used together with Offset to create a drop shadow effect.") }},
+ { NR_FILTER_MORPHOLOGY, { EffectCategory::Effect, "feMorphology-icon",
+ _("Provides erode and dilate effects. For single-color objects erode makes the object thinner and dilate makes it thicker.") }},
+ { NR_FILTER_OFFSET, { EffectCategory::Effect, "feOffset-icon",
+ _("Offsets the input by an user-defined amount. Commonly used for drop shadow effects.") }},
+ { NR_FILTER_CONVOLVEMATRIX, { EffectCategory::Effect, "feConvolveMatrix-icon",
+ _("Performs a convolution on the input image enabling effects like blur, sharpening, embossing and edge detection.") }},
+ { NR_FILTER_DISPLACEMENTMAP, { EffectCategory::Effect, "feDisplacementMap-icon",
+ _("Displaces pixels from the first input using the second as a map of displacement intensity. Classical examples are whirl and pinch effects.") }},
+ { NR_FILTER_TILE, { EffectCategory::Effect, "feTile-icon",
+ _("Tiles a region with an input graphic. The source tile is defined by the filter primitive subregion of the input.") }},
+ { NR_FILTER_COMPOSITE, { EffectCategory::Compose, "feComposite-icon",
+ _("Composites two images using one of the Porter-Duff blending modes or the arithmetic mode described in SVG standard.") }},
+ { NR_FILTER_BLEND, { EffectCategory::Compose, "feBlend-icon",
+ _("Provides image blending modes, such as screen, multiply, darken and lighten.") }},
+ { NR_FILTER_MERGE, { EffectCategory::Compose, "feMerge-icon",
+ _("Merges multiple inputs using normal alpha compositing. Equivalent to using several Blend primitives in 'normal' mode or several Composite primitives in 'over' mode.") }},
+ { NR_FILTER_COLORMATRIX, { EffectCategory::Colors, "feColorMatrix-icon",
+ _("Modifies pixel colors based on a transformation matrix. Useful for adjusting color hue and saturation.") }},
+ { NR_FILTER_COMPONENTTRANSFER, { EffectCategory::Colors, "feComponentTransfer-icon",
+ _("Manipulates color components according to particular transfer functions. Useful for brightness and contrast adjustment, color balance, and thresholding.") }},
+ { NR_FILTER_DIFFUSELIGHTING, { EffectCategory::Colors, "feDiffuseLighting-icon",
+ _("Creates \"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.") }},
+ { NR_FILTER_SPECULARLIGHTING, { EffectCategory::Colors, "feSpecularLighting-icon",
+ _("Creates \"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.") }},
+ { NR_FILTER_FLOOD, { EffectCategory::Generation, "feFlood-icon",
+ _("Fills the region with a given color and opacity. Often used as input to other filters to apply color to a graphic.") }},
+ { NR_FILTER_IMAGE, { EffectCategory::Generation, "feImage-icon",
+ _("Fills the region with graphics from an external file or from another portion of the document.") }},
+ { NR_FILTER_TURBULENCE, { EffectCategory::Generation, "feTurbulence-icon",
+ _("Renders Perlin noise, which is useful to generate textures such as clouds, fire, smoke, marble or granite.") }},
+ };
+ return effects;
+}
+
+// populate popup with filter effects and completion list for a search box
+void FilterEffectsDialog::add_effects(Inkscape::UI::Widget::CompletionPopup& popup, bool symbolic) {
+ auto& menu = popup.get_menu();
+
+ struct Effect {
+ Inkscape::Filters::FilterPrimitiveType type;
+ Glib::ustring label;
+ EffectCategory category;
+ Glib::ustring icon_name;
+ Glib::ustring tooltip;
+ };
+ std::vector<Effect> effects;
+ effects.reserve(get_effects().size());
+ for (auto&& effect : get_effects()) {
+ effects.push_back({
+ effect.first,
+ _(FPConverter.get_label(effect.first).c_str()),
+ effect.second.category,
+ effect.second.icon_name,
+ effect.second.tooltip
+ });
+ }
+ std::sort(begin(effects), end(effects), [=](auto&& a, auto&& b) {
+ if (a.category != b.category) {
+ return a.category < b.category;
+ }
+ return a.label < b.label;
+ });
+
+ popup.clear_completion_list();
+
+ // 2-column menu
+ Inkscape::UI::ColumnMenuBuilder<EffectCategory> builder(menu, 2, Gtk::ICON_SIZE_LARGE_TOOLBAR);
+
+ for (auto& effect : effects) {
+ // build popup menu
+ auto type = effect.type;
+ auto * menuitem = builder.add_item(effect.label, effect.category, effect.tooltip, effect.icon_name, true, true, [=](){ add_filter_primitive(type); });
+ gint id = (gint)type;
+ menuitem->property_has_tooltip() = true;
+ menuitem->signal_query_tooltip().connect([=](int x, int y, bool kbd, const Glib::RefPtr<Gtk::Tooltip>& tooltipw){
+ return sp_query_custom_tooltip(x, y, kbd, tooltipw, id, effect.tooltip, effect.icon_name);
+ });
+ if (builder.new_section()) {
+ builder.set_section(get_category_name(effect.category));
+ }
+
+ // build completion list
+ popup.add_to_completion_list(static_cast<int>(effect.type), effect.label, effect.icon_name + (symbolic ? "-symbolic" : ""));
+ }
+
+ if (symbolic) {
+ menu.get_style_context()->add_class("symbolic");
+ }
+}
+
+/*** FilterEffectsDialog ***/
+
+FilterEffectsDialog::FilterEffectsDialog()
+ : DialogBase("/dialogs/filtereffects", "FilterEffects"),
+ _builder(create_builder("dialog-filter-editor.glade")),
+ _paned(get_widget<Gtk::Paned>(_builder, "paned")),
+ _main_grid(get_widget<Gtk::Grid>(_builder, "main")),
+ _params_box(get_widget<Gtk::Box>(_builder, "params")),
+ _search_box(get_widget<Gtk::Box>(_builder, "search")),
+ _search_wide_box(get_widget<Gtk::Box>(_builder, "search-wide")),
+ _filter_wnd(get_widget<Gtk::ScrolledWindow>(_builder, "filter")),
+ _cur_filter_btn(get_widget<Gtk::CheckButton>(_builder, "label"))
+ , _add_primitive_type(FPConverter)
+ , _add_primitive(_("Add Effect:"))
+ , _empty_settings("", Gtk::ALIGN_CENTER)
+ , _no_filter_selected(_("No filter selected"), Gtk::ALIGN_START)
+ , _settings_initialized(false)
+ , _locked(false)
+ , _attr_lock(false)
+ , _filter_modifier(*this, _builder)
+ , _primitive_list(*this)
+ , _settings_effect(Gtk::ORIENTATION_VERTICAL)
+ , _settings_filter(Gtk::ORIENTATION_VERTICAL)
+{
+ _settings = new Settings(*this, _settings_effect, [=](auto a){ set_attr_direct(a); }, NR_FILTER_ENDPRIMITIVETYPE);
+ _cur_effect_name = &get_widget<Gtk::Label>(_builder, "cur-effect");
+ _settings->_size_group->add_widget(*_cur_effect_name);
+ _filter_general_settings = new Settings(*this, _settings_filter, [=](auto a){ set_filternode_attr(a); }, 1);
+
+ // Initialize widget hierarchy
+ _primitive_box = &get_widget<Gtk::ScrolledWindow>(_builder, "filter");
+ _primitive_list.set_enable_search(false);
+ _primitive_list.show_all();
+ _primitive_box->add(_primitive_list);
+
+ auto symbolic = Inkscape::Preferences::get()->getBool("/theme/symbolicIcons", true);
+ add_effects(_effects_popup, symbolic);
+ _effects_popup.get_entry().set_placeholder_text(_("Add effect"));
+ _effects_popup.on_match_selected().connect([=](int id){ add_filter_primitive(static_cast<FilterPrimitiveType>(id)); });
+ _search_box.pack_start(_effects_popup);
+
+ _filter_modifier.show_all();
+
+ _settings_effect.show_all();
+ _params_box.pack_end(_settings_effect);
+
+ _settings_filter.show_all();
+ get_widget<Gtk::Popover>(_builder, "gen-settings").add(_settings_filter);
+
+ get_widget<Gtk::Popover>(_builder, "info-popover").signal_show().connect([=](){
+ if (auto prim = _primitive_list.get_selected()) {
+ if (prim->getRepr()) {
+ auto id = FPConverter.get_id_from_key(prim->getRepr()->name());
+ const auto& effect = get_effects().at(id);
+ get_widget<Gtk::Image>(_builder, "effect-icon").set_from_icon_name(effect.icon_name, Gtk::ICON_SIZE_DND);
+ auto buffer = get_widget<Gtk::TextView>(_builder, "effect-info").get_buffer();
+ buffer->set_text("");
+ buffer->insert_markup(buffer->begin(), effect.tooltip);
+ get_widget<Gtk::TextView>(_builder, "effect-desc").get_buffer()->set_text("");
+ }
+ }
+ });
+
+ _primitive_list.signal_primitive_changed().connect([=](){
+ update_settings_view();
+ });
+
+ _cur_filter_toggle = _cur_filter_btn.signal_toggled().connect([=](){
+ _filter_modifier.toggle_current_filter();
+ });
+
+ auto update_checkbox = [=](){
+ auto active = _filter_modifier.is_selected_filter_active();
+ _cur_filter_toggle.block();
+ _cur_filter_btn.set_active(active);
+ _cur_filter_toggle.unblock();
+ };
+
+ auto update_widgets = [=](){
+ auto& opt = get_widget<Gtk::MenuButton>(_builder, "filter-opt");
+ _primitive_list.update();
+ Glib::ustring name = "-";
+ if (auto filter = _filter_modifier.get_selected_filter()) {
+ name = get_filter_name(filter);
+ _effects_popup.set_sensitive();
+ _cur_filter_btn.set_sensitive(); // ideally this should also be selection-dependent
+ opt.set_sensitive();
+ }
+ else {
+ _effects_popup.set_sensitive(false);
+ _cur_filter_btn.set_sensitive(false);
+ opt.set_sensitive(false);
+ }
+ get_widget<Gtk::Label>(_builder, "filter-name").set_label(name);
+ update_checkbox();
+ update_settings_view();
+ };
+
+ //TODO: adding animated GIFs to the info popup once they are ready:
+ // auto a = Gdk::PixbufAnimation::create_from_file("/Users/mike/blur-effect.gif");
+ // get_widget<Gtk::Image>(_builder, "effect-image").property_pixbuf_animation().set_value(a);
+
+ init_settings_widgets();
+
+ _filter_modifier.signal_filter_changed().connect([=](){
+ update_widgets();
+ });
+
+ _filter_modifier.signal_filters_updated().connect([=](){
+ update_checkbox();
+ });
+
+ _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));
+
+ get_widget<Gtk::Button>(_builder, "new-filter").signal_clicked().connect([=](){ _filter_modifier.add_filter(); });
+ pack_start(_main_grid);
+
+ get_widget<Gtk::Button>(_builder, "dup-btn").signal_clicked().connect([=](){ duplicate_primitive(); });
+ get_widget<Gtk::Button>(_builder, "del-btn").signal_clicked().connect([=](){ _primitive_list.remove_selected(); });
+ get_widget<Gtk::Button>(_builder, "info-btn").signal_clicked().connect([=](){ /* todo */ });
+
+ auto* show_sources = &get_widget<Gtk::ToggleButton>(_builder, "btn-connect");
+ auto set_inputs = [=](bool all){
+ int count = all ? FPInputConverter._length : 2;
+ _primitive_list.set_inputs_count(count);
+ // full rebuild: this is what it takes to make cell renderer new min width into account to adjust scrollbar
+ _primitive_list.update();
+ };
+ auto show_all_sources = Inkscape::Preferences::get()->getBool(_prefs + "/dialogs/filters/showAllSources", false);
+ show_sources->set_active(show_all_sources);
+ set_inputs(show_all_sources);
+ show_sources->signal_toggled().connect([=](){
+ bool show_all = show_sources->get_active();
+ set_inputs(show_all);
+ Inkscape::Preferences::get()->setBool(_prefs + "/dialogs/filters/showAllSources", show_all);
+ });
+
+ _paned.set_position(Inkscape::Preferences::get()->getIntLimited(_prefs + "/handlePos", 200, 10, 9999));
+ _paned.property_position().signal_changed().connect([=](){
+ Inkscape::Preferences::get()->setInt(_prefs + "/handlePos", _paned.get_position());
+ });
+
+ _primitive_list.update();
+
+ show();
+
+ // reading minimal width at this point should reflect space needed for fitting effect parameters panel
+ int min_width = 0, dummy = 0;
+ get_preferred_width(min_width, dummy);
+ int min_effects = 0;
+ _effects_popup.get_preferred_width(min_effects, dummy);
+ // calculate threshold/minimum width of filters dialog in horizontal layout;
+ // use this size to decide where transition from vertical to horizontal layout is;
+ // if this size is too small dialog can get stuck in horizontal layout - users won't be able
+ // to make it narrow again, due to min dialog size enforced by GTK
+ int thresold_width = min_width + min_effects * 3;
+
+ // two alternative layout arrangements depending on the dialog size;
+ // one is tall and narrow with widgets in one column, while the other
+ // is for wide dialogs with filter parameters and effects side by side
+ signal_size_allocate().connect([=] (const Gtk::Allocation& alloc) {
+ if (alloc.get_width() < 10 || alloc.get_height() < 10) return;
+
+ double const ratio = alloc.get_width() / static_cast<double>(alloc.get_height());
+
+ double constexpr hysteresis = 0.01;
+ if (ratio < 1 - hysteresis || alloc.get_width() <= thresold_width) {
+ // make narrow/tall
+ if (!_narrow_dialog) {
+ _main_grid.remove(_filter_wnd);
+ _search_wide_box.remove(_effects_popup);
+ _paned.add1(_filter_wnd);
+ _search_box.pack_start(_effects_popup);
+ _paned.set_size_request();
+ get_widget<Gtk::Box>(_builder, "connect-box-wide").remove(*show_sources);
+ get_widget<Gtk::Box>(_builder, "connect-box").add(*show_sources);
+ _narrow_dialog = true;
+ ensure_size();
+ }
+ }
+ else if (ratio > 1 + hysteresis && alloc.get_width() > thresold_width) {
+ // make wide/short
+ if (_narrow_dialog) {
+ _paned.remove(_filter_wnd);
+ _search_box.remove(_effects_popup);
+ _main_grid.attach(_filter_wnd, 2, 1, 1, 2);
+ _search_wide_box.pack_start(_effects_popup);
+ _paned.set_size_request(min_width);
+ get_widget<Gtk::Box>(_builder, "connect-box").remove(*show_sources);
+ get_widget<Gtk::Box>(_builder, "connect-box-wide").add(*show_sources);
+ _narrow_dialog = false;
+ ensure_size();
+ }
+ }
+ });
+
+ update_widgets();
+ show_all_children();
+ update();
+ update_settings_view();
+}
+
+FilterEffectsDialog::~FilterEffectsDialog()
+{
+ delete _settings;
+ delete _filter_general_settings;
+}
+
+void FilterEffectsDialog::documentReplaced()
+{
+ _resource_changed.disconnect();
+ if (auto document = getDocument()) {
+ _resource_changed = document->connectResourcesChanged("filter", [=](){ _filter_modifier.update_filters(); });
+
+ _filter_modifier.update_filters();
+ }
+}
+
+void FilterEffectsDialog::selectionChanged(Inkscape::Selection *selection)
+{
+ if (selection) {
+ _filter_modifier.update_selection(selection);
+ }
+}
+
+void FilterEffectsDialog::selectionModified(Inkscape::Selection *selection, guint flags)
+{
+ if (flags & ( SP_OBJECT_MODIFIED_FLAG |
+ SP_OBJECT_PARENT_MODIFIED_FLAG |
+ SP_OBJECT_STYLE_MODIFIED_FLAG) ) {
+ _filter_modifier.update_selection(selection);
+ }
+}
+
+void FilterEffectsDialog::set_attrs_locked(const bool l)
+{
+ _locked = l;
+}
+
+void FilterEffectsDialog::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!
+
+ _empty_settings.set_sensitive(false);
+ _settings_effect.pack_start(_empty_settings);
+
+ _no_filter_selected.set_sensitive(false);
+ _settings_filter.pack_start(_no_filter_selected);
+ _settings_initialized = true;
+
+ _filter_general_settings->type(0);
+ auto _region_auto = _filter_general_settings->add_checkbutton(true, SPAttr::AUTO_REGION, _("Automatic Region"), "true", "false", _("If unset, the coordinates and dimensions won't be updated automatically."));
+ _region_pos = _filter_general_settings->add_multispinbutton(/*default x:*/ (double) -0.1, /*default y:*/ (double) -0.1, SPAttr::X, SPAttr::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"));
+ _region_size = _filter_general_settings->add_multispinbutton(/*default width:*/ (double) 1.2, /*default height:*/ (double) 1.2, SPAttr::WIDTH, SPAttr::HEIGHT, _("Dimensions:"), 0, 1000, 0.01, 0.1, 2, _("Width of filter effects region"), _("Height of filter effects region"));
+ _region_auto->signal_attr_changed().connect( sigc::bind(sigc::mem_fun(*this, &FilterEffectsDialog::update_automatic_region), _region_auto));
+
+ _settings->type(NR_FILTER_BLEND);
+ _settings->add_combo(SP_CSS_BLEND_NORMAL, SPAttr::MODE, _("Mode:"), SPBlendModeConverter);
+
+ _settings->type(NR_FILTER_COLORMATRIX);
+ ComboBoxEnum<FilterColorMatrixType>* colmat = _settings->add_combo(COLORMATRIX_MATRIX, SPAttr::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);
+ // TRANSLATORS: Abbreviation for red color channel in RGBA
+ _settings->add_componenttransfervalues(C_("color", "R:"), SPFeFuncNode::R);
+ // TRANSLATORS: Abbreviation for green color channel in RGBA
+ _settings->add_componenttransfervalues(C_("color", "G:"), SPFeFuncNode::G);
+ // TRANSLATORS: Abbreviation for blue color channel in RGBA
+ _settings->add_componenttransfervalues(C_("color", "B:"), SPFeFuncNode::B);
+ // TRANSLATORS: Abbreviation for alpha channel in RGBA
+ _settings->add_componenttransfervalues(C_("color", "A:"), SPFeFuncNode::A);
+
+ _settings->type(NR_FILTER_COMPOSITE);
+ _settings->add_combo(COMPOSITE_OVER, SPAttr::OPERATOR, _("Operator:"), CompositeOperatorConverter);
+ _k1 = _settings->add_spinscale(0, SPAttr::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, SPAttr::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, SPAttr::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, SPAttr::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", SPAttr::ORDER, _("Size:"), 1, max_convolution_kernel_size, 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, SPAttr::TARGETX, SPAttr::TARGETY, _("Target:"), 0, max_convolution_kernel_size - 1, 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(SPAttr::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, SPAttr::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, SPAttr::BIAS, _("Bias:"), -10, 10, 0.1, 0.5, 2, _("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_NONE, SPAttr::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, SPAttr::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, SPAttr::LIGHTING_COLOR, _("Diffuse Color:"), _("Defines the color of the light source"));
+ _settings->add_spinscale(1, SPAttr::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, SPAttr::DIFFUSECONSTANT, _("Constant:"), 0, 5, 0.1, 0.01, 2, _("This constant affects the Phong lighting model."));
+ // deprecated (https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/kernelUnitLength)
+ // _settings->add_dualspinscale(SPAttr::KERNELUNITLENGTH, _("Kernel Unit Length:"), 0.01, 10, 1, 0.01, 1);
+ _settings->add_lightsource();
+
+ _settings->type(NR_FILTER_DISPLACEMENTMAP);
+ _settings->add_spinscale(0, SPAttr::SCALE, _("Scale:"), 0, 100, 1, 0.01, 1, _("This defines the intensity of the displacement effect."));
+ _settings->add_combo(DISPLACEMENTMAP_CHANNEL_ALPHA, SPAttr::XCHANNELSELECTOR, _("X displacement:"), DisplacementMapChannelConverter, _("Color component that controls the displacement in the X direction"));
+ _settings->add_combo(DISPLACEMENTMAP_CHANNEL_ALPHA, SPAttr::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, SPAttr::FLOOD_COLOR, _("Color:"), _("The whole filter region will be filled with this color."));
+ _settings->add_spinscale(1, SPAttr::FLOOD_OPACITY, _("Opacity:"), 0, 1, 0.1, 0.01, 2);
+
+ _settings->type(NR_FILTER_GAUSSIANBLUR);
+ _settings->add_dualspinscale(SPAttr::STDDEVIATION, _("Size:"), 0, 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, SPAttr::OPERATOR, _("Operator:"), MorphologyOperatorConverter, _("Erode: performs \"thinning\" of input image.\nDilate: performs \"fattening\" of input image."));
+ _settings->add_dualspinscale(SPAttr::RADIUS, _("Radius:"), 0, 100, 1, 0.01, 1);
+
+ _settings->type(NR_FILTER_IMAGE);
+ _settings->add_fileorelement(SPAttr::XLINK_HREF, _("Source of Image:"));
+ _image_x = _settings->add_entry(SPAttr::X, _("Position X:"), _("Position X"));
+ _image_x->signal_attr_changed().connect(sigc::mem_fun(*this, &FilterEffectsDialog::image_x_changed));
+ //This is commented out because we want the default empty value of X or Y and couldn't get it from SpinButton
+ //_image_y = _settings->add_spinbutton(0, SPAttr::Y, _("Y:"), -DBL_MAX, DBL_MAX, 1, 1, 5, _("Y"));
+ _image_y = _settings->add_entry(SPAttr::Y, _("Position Y:"), _("Position Y"));
+ _image_y->signal_attr_changed().connect(sigc::mem_fun(*this, &FilterEffectsDialog::image_y_changed));
+ _settings->add_entry(SPAttr::WIDTH, _("Width:"), _("Width"));
+ _settings->add_entry(SPAttr::HEIGHT, _("Height:"), _("Height"));
+
+ _settings->type(NR_FILTER_OFFSET);
+ _settings->add_checkbutton(false, SPAttr::PRESERVEALPHA, _("Preserve Alpha"), "true", "false", _("If set, the alpha channel won't be altered by this filter primitive."));
+ _settings->add_spinscale(0, SPAttr::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, SPAttr::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, SPAttr::LIGHTING_COLOR, _("Specular Color:"), _("Defines the color of the light source"));
+ _settings->add_spinscale(1, SPAttr::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, SPAttr::SPECULARCONSTANT, _("Constant:"), 0, 5, 0.1, 0.01, 2, _("This constant affects the Phong lighting model."));
+ _settings->add_spinscale(1, SPAttr::SPECULAREXPONENT, _("Exponent:"), 1, 50, 1, 0.01, 1, _("Exponent for specular term, larger is more \"shiny\"."));
+ // deprecated (https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/kernelUnitLength)
+ // _settings->add_dualspinscale(SPAttr::KERNELUNITLENGTH, _("Kernel Unit Length:"), 0.01, 10, 1, 0.01, 1);
+ _settings->add_lightsource();
+
+ _settings->type(NR_FILTER_TILE);
+ // add some filter primitive attributes: https://drafts.fxtf.org/filter-effects/#feTileElement
+ // issue: https://gitlab.com/inkscape/inkscape/-/issues/1417
+ _settings->add_entry(SPAttr::X, _("Position X:"), _("Position X"));
+ _settings->add_entry(SPAttr::Y, _("Position Y:"), _("Position Y"));
+ _settings->add_entry(SPAttr::WIDTH, _("Width:"), _("Width"));
+ _settings->add_entry(SPAttr::HEIGHT, _("Height:"), _("Height"));
+
+ _settings->type(NR_FILTER_TURBULENCE);
+// _settings->add_checkbutton(false, SPAttr::STITCHTILES, _("Stitch Tiles"), "stitch", "noStitch");
+ _settings->add_combo(TURBULENCE_TURBULENCE, SPAttr::TYPE, _("Type:"), TurbulenceTypeConverter, _("Indicates whether the filter primitive should perform a noise or turbulence function."));
+ _settings->add_dualspinscale(SPAttr::BASEFREQUENCY, _("Size:"), 0.001, 10, 0.001, 0.1, 3);
+ _settings->add_spinscale(1, SPAttr::NUMOCTAVES, _("Detail:"), 1, 10, 1, 1, 0);
+ _settings->add_spinscale(0, SPAttr::SEED, _("Seed:"), 0, 1000, 1, 1, 0, _("The starting number for the pseudo random number generator."));
+}
+
+void FilterEffectsDialog::add_filter_primitive(Filters::FilterPrimitiveType type) {
+ if (auto filter = _filter_modifier.get_selected_filter()) {
+ SPFilterPrimitive* prim = filter_add_primitive(filter, type);
+ _primitive_list.select(prim);
+ DocumentUndo::done(filter->document, _("Add filter primitive"), INKSCAPE_ICON("dialog-filters"));
+ }
+}
+
+void FilterEffectsDialog::add_primitive()
+{
+ add_filter_primitive(_add_primitive_type.get_active_data()->id);
+}
+
+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, _("Duplicate filter primitive"), INKSCAPE_ICON("dialog-filters"));
+
+ _primitive_list.update();
+ }
+}
+
+void FilterEffectsDialog::convolve_order_changed()
+{
+ _convolve_matrix->set_from_attribute(_primitive_list.get_selected());
+ // MultiSpinButtons orders widgets backwards: so use index 1 and 0
+ _convolve_target->get_spinbuttons()[1]->get_adjustment()->set_upper(_convolve_order->get_spinbutton1().get_value() - 1);
+ _convolve_target->get_spinbuttons()[0]->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 = g_strtod(text.c_str(), nullptr);
+ 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 SPAttr 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(), _("Set filter primitive attribute"), INKSCAPE_ICON("dialog-filters"));
+ }
+
+ _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_filter.get_children();
+ vect[0]->hide();
+ _no_filter_selected.show();
+ }
+
+ _attr_lock = false;
+ }
+}
+
+void FilterEffectsDialog::update_settings_view()
+{
+ update_settings_sensitivity();
+
+ if (_attr_lock)
+ return;
+
+ // selected effect parameters
+
+ for (auto& i : _settings_effect.get_children()) {
+ i->hide();
+ }
+
+ SPFilterPrimitive* prim = _primitive_list.get_selected();
+ auto& header = get_widget<Gtk::Box>(_builder, "effect-header");
+ SPFilter* filter = _filter_modifier.get_selected_filter();
+ bool present = _filter_modifier.filters_present();
+
+ if (prim && prim->getRepr()) {
+ //XML Tree being used directly here while it shouldn't be.
+ auto id = FPConverter.get_id_from_key(prim->getRepr()->name());
+ _settings->show_and_update(id, prim);
+ _empty_settings.hide();
+ _cur_effect_name->set_text(_(FPConverter.get_label(id).c_str()));
+ header.show();
+ }
+ else {
+ if (filter) {
+ _empty_settings.set_text(_("Add effect from the search bar"));
+ }
+ else if (present) {
+ _empty_settings.set_text(_("Select a filter"));
+ }
+ else {
+ _empty_settings.set_text(_("No filters in the document"));
+ }
+ _empty_settings.show();
+ _cur_effect_name->set_text(Glib::ustring());
+ header.hide();
+ }
+
+ // current filter parameters (area size)
+
+ std::vector<Gtk::Widget*> vect2 = _settings_filter.get_children();
+ vect2[0]->hide();
+ _no_filter_selected.show();
+
+ if (filter) {
+ _filter_general_settings->show_and_update(0, filter);
+ _no_filter_selected.hide();
+ }
+
+ ensure_size();
+}
+
+void FilterEffectsDialog::update_settings_sensitivity()
+{
+ SPFilterPrimitive* prim = _primitive_list.get_selected();
+ const bool use_k = is<SPFeComposite>(prim) && cast<SPFeComposite>(prim)->get_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());
+}
+
+void FilterEffectsDialog::update_automatic_region(Gtk::CheckButton *btn)
+{
+ bool automatic = btn->get_active();
+ _region_pos->set_sensitive(!automatic);
+ _region_size->set_sensitive(!automatic);
+
+}
+
+} // 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..7965283
--- /dev/null
+++ b/src/ui/dialog/filter-effects-dialog.h
@@ -0,0 +1,355 @@
+// 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 <glibmm/property.h>
+#include <glibmm/propertyproxy.h>
+#include <glibmm/refptr.h>
+#include <glibmm/ustring.h>
+#include <gtkmm/box.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/paned.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/treeiter.h>
+#include <memory>
+#include <sigc++/connection.h>
+
+#include "attributes.h"
+#include "display/nr-filter-types.h"
+#include "helper/auto-connection.h"
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/combo-enums.h"
+#include "ui/widget/completion-popup.h"
+#include "ui/widget/spin-scale.h"
+#include "xml/helper-observer.h"
+
+class SPFilter;
+class SPFilterPrimitive;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class EntryAttr;
+class FileOrElementChooser;
+class DualSpinButton;
+class MultiSpinButton;
+
+class FilterEffectsDialog : public DialogBase
+{
+public:
+ FilterEffectsDialog();
+ ~FilterEffectsDialog() override;
+
+ void set_attrs_locked(const bool);
+protected:
+ void show_all_vfunc() override;
+private:
+
+ void documentReplaced() override;
+ void selectionChanged(Inkscape::Selection *selection) override;
+ void selectionModified(Inkscape::Selection *selection, guint flags) override;
+
+ Inkscape::auto_connection _resource_changed;
+
+ friend class FileOrElementChooser;
+
+ class FilterModifier : public Gtk::Box
+ {
+ public:
+ FilterModifier(FilterEffectsDialog& d, Glib::RefPtr<Gtk::Builder> builder);
+
+ void update_filters();
+ void update_selection(Selection *);
+
+ SPFilter* get_selected_filter();
+ void select_filter(const SPFilter*);
+ void add_filter();
+ bool is_selected_filter_active();
+ void toggle_current_filter();
+ bool filters_present() const;
+
+ sigc::signal<void ()>& signal_filter_changed()
+ {
+ return _signal_filter_changed;
+ }
+ sigc::signal<void ()>& signal_filters_updated() {
+ return _signal_filters_updated;
+ }
+
+ 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 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 selection_toggled(Gtk::TreeIter iter, bool toggle);
+
+ void update_counts();
+ void filter_list_button_release(GdkEventButton*);
+ void remove_filter();
+ void duplicate_filter();
+ void rename_filter();
+ void select_filter_elements();
+
+ Glib::RefPtr<Gtk::Builder> _builder;
+ FilterEffectsDialog& _dialog;
+ Gtk::TreeView& _list;
+ Glib::RefPtr<Gtk::ListStore> _filters_model;
+ Columns _columns;
+ Gtk::CellRendererToggle _cell_toggle;
+ Gtk::Button& _add;
+ Gtk::Button& _dup;
+ Gtk::Button& _del;
+ Gtk::Button& _select;
+ Gtk::Menu& _menu;
+ sigc::signal<void ()> _signal_filter_changed;
+ std::unique_ptr<Inkscape::XML::SignalObserver> _observer;
+ sigc::signal<void ()> _signal_filters_updated;
+ };
+
+ 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_w = 16;
+ static const int size_h = 21;
+
+ 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;
+ };
+
+ 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;
+ void set_inputs_count(int count);
+ int get_inputs_count() 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();
+
+ 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 SPAttr attr, int& src_id, const int pos);
+ int find_index(const Gtk::TreeIter& target);
+ void draw_connection(const Cairo::RefPtr<Cairo::Context>& cr,
+ const Gtk::TreeIter&, const SPAttr attr, const int text_start_x,
+ const int x1, const int y1, const int row_count, const int pos,
+ const Gdk::RGBA fg_color, const Gdk::RGBA 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;
+ int _inputs_count;
+ };
+
+ 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 add_filter_primitive(Filters::FilterPrimitiveType type);
+
+ 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 SPAttr, const gchar* val);
+ void update_settings_view();
+ void update_filter_general_settings_view();
+ void update_settings_sensitivity();
+ void update_color_matrix();
+ void update_automatic_region(Gtk::CheckButton *btn);
+ void add_effects(Inkscape::UI::Widget::CompletionPopup& popup, bool symbolic);
+
+ Glib::RefPtr<Gtk::Builder> _builder;
+ Glib::ustring _prefs = "/dialogs/filters";
+ Gtk::Paned& _paned;
+ Gtk::Grid& _main_grid;
+ Gtk::Box& _params_box;
+ Gtk::Box& _search_box;
+ Gtk::Box& _search_wide_box;
+ Gtk::ScrolledWindow& _filter_wnd;
+ bool _narrow_dialog = true;
+ Gtk::CheckButton& _cur_filter_btn;
+ sigc::connection _cur_filter_toggle;
+ // View/add primitives
+ Gtk::ScrolledWindow* _primitive_box;
+
+ UI::Widget::ComboBoxEnum<Inkscape::Filters::FilterPrimitiveType> _add_primitive_type;
+ Gtk::Button _add_primitive;
+
+ // Bottom pane (filter effect primitive settings)
+ Gtk::Box _settings_filter;
+ Gtk::Box _settings_effect;
+ Gtk::Label _empty_settings;
+ Gtk::Label _no_filter_selected;
+ Gtk::Label* _cur_effect_name;
+ bool _settings_initialized;
+
+ class Settings;
+ class MatrixAttr;
+ class ColorMatrixValues;
+ class ComponentTransferValues;
+ class LightSourceControl;
+ Settings* _settings;
+ Settings* _filter_general_settings;
+
+ // General settings
+ MultiSpinButton *_region_pos, *_region_size;
+
+ // 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;
+ Inkscape::UI::Widget::CompletionPopup _effects_popup;
+};
+
+} // 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..300ab4a
--- /dev/null
+++ b/src/ui/dialog/find.cpp
@@ -0,0 +1,1145 @@
+// 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 <gtkmm/enums.h>
+#include <gtkmm/label.h>
+#include <gtkmm/sizegroup.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "layer-manager.h"
+#include "message-stack.h"
+#include "selection-chemistry.h"
+#include "text-editing.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/icon-names.h"
+#include "ui/dialog-events.h"
+
+#include "xml/attribute-record.h"
+#include "xml/node-iterators.h"
+
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+Find::Find()
+ : DialogBase("/dialogs/find", "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")),
+ 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")),
+ check_desc(_("_Desc")),
+ check_title(_("Title")),
+ 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")),
+
+ _left_size_group(Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL)),
+ _right_size_group(Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL)),
+
+ status(""),
+ button_find(_("_Find")),
+ button_replace(_("_Replace All")),
+ _action_replace(false),
+ blocked(false),
+
+ hbox_searchin(Gtk::ORIENTATION_HORIZONTAL),
+ vbox_scope(Gtk::ORIENTATION_VERTICAL),
+ vbox_searchin(Gtk::ORIENTATION_VERTICAL),
+ vbox_options1(Gtk::ORIENTATION_VERTICAL),
+ vbox_options2(Gtk::ORIENTATION_VERTICAL),
+ hbox_options(Gtk::ORIENTATION_HORIZONTAL),
+ vbox_expander(Gtk::ORIENTATION_VERTICAL),
+ hbox_properties(Gtk::ORIENTATION_HORIZONTAL),
+ vbox_properties1(Gtk::ORIENTATION_VERTICAL),
+ vbox_properties2(Gtk::ORIENTATION_VERTICAL),
+ vbox_types1(Gtk::ORIENTATION_VERTICAL),
+ vbox_types2(Gtk::ORIENTATION_VERTICAL),
+ hbox_types(Gtk::ORIENTATION_HORIZONTAL),
+ hboxbutton_row(Gtk::ORIENTATION_HORIZONTAL)
+
+{
+ label_group = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL);
+ auto label1 = const_cast<Gtk::Label*>(entry_find.getLabel());
+ label_group->add_widget(*label1);
+ label1->set_xalign(0);
+ auto label2 = const_cast<Gtk::Label*>(entry_replace.getLabel());
+ label_group->add_widget(*label2);
+ label2->set_xalign(0);
+ const int MARGIN = 4;
+ set_margin_start(MARGIN);
+ set_margin_end(MARGIN);
+ entry_find.set_margin_top(MARGIN);
+ entry_replace.set_margin_top(MARGIN);
+ frame_searchin.set_margin_top(MARGIN);
+ frame_scope.set_margin_top(MARGIN);
+ 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_desc.set_use_underline();
+ check_desc.set_tooltip_text(_("Search description"));
+ check_desc.set_active(false);
+ check_title.set_use_underline();
+ check_title.set_tooltip_text(_("Search title"));
+ check_title.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_properties1.pack_start(check_desc, Gtk::PACK_SHRINK);
+ vbox_properties1.pack_start(check_title, 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);
+ _left_size_group->add_widget(check_desc);
+ _left_size_group->add_widget(check_title);
+ _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);
+
+ set_spacing(6);
+ pack_start(entry_find, false, false);
+ pack_start(entry_replace, false, false);
+ pack_start(hbox_searchin, false, false);
+ pack_start(expander_options, false, false);
+ 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_desc);
+ checkProperties.push_back(&check_title);
+ 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();
+
+ show_all_children();
+
+ button_find.set_can_default();
+ //button_find.grab_default(); // activatable by Enter
+ entry_find.getEntry()->grab_focus();
+}
+
+void Find::desktopReplaced()
+{
+ if (auto selection = getSelection()) {
+ SPItem *item = selection->singleItem();
+ if (item && entry_find.getEntry()->get_text_length() == 0) {
+ Glib::ustring str = sp_te_get_string_multiline(item);
+ if (!str.empty()) {
+ entry_find.getEntry()->set_text(str);
+ }
+ }
+ }
+}
+
+void Find::selectionChanged(Selection *selection)
+{
+ 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;
+ gsize replace_length = Glib::ustring(replace).length();
+ 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 + replace_length);
+ }
+ 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_desc_match (SPItem *item, const gchar *text, bool exact, bool casematch, bool replace)
+{
+ gchar* desc = item->desc();
+ bool found = find_strcmp(desc, text, exact, casematch);
+ if (found && replace) {
+ Glib::ustring r = find_replace(desc, text, entry_replace.getEntry()->get_text().c_str(), exact, casematch, replace);
+ item->setDesc(r.c_str());
+ }
+ g_free(desc);
+ return found;
+}
+
+bool Find::item_title_match (SPItem *item, const gchar *text, bool exact, bool casematch, bool replace)
+{
+ gchar* title = item->title();
+ bool found = find_strcmp(title, text, exact, casematch);
+ if (found && replace) {
+ Glib::ustring r = find_replace(title, text, entry_replace.getEntry()->get_text().c_str(), exact, casematch, replace);
+ item->setTitle(r.c_str());
+ }
+ g_free(title);
+ return found;
+}
+
+bool Find::item_text_match (SPItem *item, const gchar *find, bool exact, bool casematch, bool replace/*=false*/)
+{
+ if (item->getRepr() == nullptr) {
+ return false;
+ }
+
+ Glib::ustring item_text = sp_te_get_string_multiline(item);
+
+ if (!item_text.empty()) {
+ bool found = find_strcmp(item_text.c_str(), 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;
+ }
+
+ Glib::ustring replace = entry_replace.getEntry()->get_text();
+ gsize n = find_strcmp_pos(item_text.c_str(), 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 + ufind.length());
+ sp_te_replace(item, _begin_w, _end_w, replace.c_str());
+ item_text = sp_te_get_string_multiline (item);
+ n = find_strcmp_pos(item_text.c_str(), ufind.c_str(), exact, casematch, n + replace.length());
+ }
+ }
+
+ return found;
+ }
+ return false;
+}
+
+
+bool Find::item_id_match (SPItem *item, const gchar *id, bool exact, bool casematch, bool replace/*=false*/)
+{
+ if (!item->getRepr()) {
+ return false;
+ }
+
+ const gchar *item_id = item->getRepr()->attribute("id");
+ if (!item_id) {
+ 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;
+ }
+
+ for (const auto & iter:item->getRepr()->attributeList()) {
+ 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;
+ auto item = 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 desc = check_desc.get_active();
+ bool title = check_title.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;
+ auto item = 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;
+ auto item = 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;
+ auto item = 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;
+ auto item = 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;
+ auto item = 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);
+ }
+ }
+ }
+ }
+ }
+ if (desc) {
+ for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) {
+ SPObject *obj = *i;
+ auto item = cast<SPItem>(obj);
+ g_assert(item != nullptr);
+ if (item_desc_match(item, text, exact, casematch)) {
+ if (out.end()==find(out.begin(),out.end(),*i)) {
+ out.push_back(*i);
+ if (_action_replace) {
+ item_desc_match(item, text, exact, casematch, _action_replace);
+ }
+ }
+ }
+ }
+ }
+ if (title) {
+ for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) {
+ SPObject *obj = *i;
+ auto item = cast<SPItem>(obj);
+ g_assert(item != nullptr);
+ if (item_title_match(item, text, exact, casematch)) {
+ if (out.end()==find(out.begin(),out.end(),*i)) {
+ out.push_back(*i);
+ if (_action_replace) {
+ item_title_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 (is<SPRect>(item)) {
+ return ( all ||check_rects.get_active());
+
+ } else if (is<SPGenericEllipse>(item)) {
+ return ( all || check_ellipses.get_active());
+
+ } else if (is<SPStar>(item) || is<SPPolygon>(item)) {
+ return ( all || check_stars.get_active());
+
+ } else if (is<SPSpiral>(item)) {
+ return ( all || check_spirals.get_active());
+
+ } else if (is<SPPath>(item) || is<SPLine>(item) || is<SPPolyLine>(item)) {
+ return (all || check_paths.get_active());
+
+ } else if (is<SPText>(item) || is<SPTSpan>(item) ||
+ is<SPTRef>(item) ||
+ is<SPFlowtext>(item) || is<SPFlowdiv>(item) ||
+ is<SPFlowtspan>(item) || is<SPFlowpara>(item)) {
+ return (all || check_texts.get_active());
+
+ } else if (is<SPGroup>(item) &&
+ !getDesktop()->layerManager().isLayer(item)) { // never select layers!
+ return (all || check_groups.get_active());
+
+ } else if (is<SPUse>(item)) {
+ return (all || check_clones.get_active());
+
+ } else if (is<SPImage>(item)) {
+ return (all || check_images.get_active());
+
+ } else if (is<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;
+ auto item = 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 (is<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
+ }
+
+ auto desktop = getDesktop();
+ for (auto& child: r->children) {
+ auto item = cast<SPItem>(&child);
+ if (item && !child.cloned && !desktop->layerManager().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 desktop = getDesktop();
+ auto itemlist = s->items();
+ for (auto i=boost::rbegin(itemlist); boost::rend(itemlist) != i; ++i) {
+ SPObject *obj = *i;
+ auto item = cast<SPItem>(obj);
+ g_assert(item != nullptr);
+ if (item && !item->cloned && !desktop->layerManager().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()
+{
+ auto desktop = getDesktop();
+ 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->getSelection(), l, desktop->layerManager().currentLayer(), hidden, locked);
+ } else {
+ l = all_selection_items (desktop->getSelection(), l, nullptr, hidden, locked);
+ }
+ } else {
+ if (check_scope_layer.get_active()) {
+ l = all_items (desktop->layerManager().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];
+ auto item = cast<SPItem>(obj);
+ g_assert(item != nullptr);
+ scroll_to_show_item(desktop, item);
+
+ if (_action_replace) {
+ DocumentUndo::done(desktop->getDocument(), _("Replace text or property"), INKSCAPE_ICON("draw-text"));
+ }
+
+ } 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..fd2dc89
--- /dev/null
+++ b/src/ui/dialog/find.h
@@ -0,0 +1,313 @@
+// 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 <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/dialog-base.h"
+#include "ui/widget/entry.h"
+#include "ui/widget/frame.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 DialogBase
+{
+public:
+ Find();
+ ~Find() override {};
+
+ void desktopReplaced() override;
+ void selectionChanged(Selection *selection) override;
+
+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 the SPItem 'item' has a <title> or <desc> child that
+ * matches
+ *
+ * @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_desc_match (SPItem *item, const gchar *text, bool exact, bool casematch, bool replace=false);
+ bool item_title_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();
+
+private:
+ /*
+ * Find and replace combo box widgets
+ */
+ UI::Widget::Entry entry_find;
+ UI::Widget::Entry entry_replace;
+ Glib::RefPtr<Gtk::SizeGroup> label_group;
+
+ /**
+ * 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::Box hbox_searchin;
+ Gtk::Box vbox_scope;
+ Gtk::Box 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::Box vbox_options1;
+ Gtk::Box vbox_options2;
+ Gtk::Box hbox_options;
+ Gtk::Box 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::CheckButton check_desc;
+ Gtk::CheckButton check_title;
+ Gtk::Box hbox_properties;
+ Gtk::Box vbox_properties1;
+ Gtk::Box 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::Box vbox_types1;
+ Gtk::Box vbox_types2;
+ Gtk::Box 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::Box hbox_text;
+
+ /**
+ * Action Buttons and status
+ */
+ Gtk::Label status;
+ Gtk::Button button_find;
+ Gtk::Button button_replace;
+ Gtk::ButtonBox box_buttons;
+ Gtk::Box hboxbutton_row;
+
+ /**
+ * Finding or replacing
+ */
+ bool _action_replace;
+ bool blocked;
+
+ 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/font-collections-manager.cpp b/src/ui/dialog/font-collections-manager.cpp
new file mode 100644
index 0000000..9e35831
--- /dev/null
+++ b/src/ui/dialog/font-collections-manager.cpp
@@ -0,0 +1,172 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief A dialog to manage the font collections.
+ */
+/* Authors:
+ * Vaibhav Malik
+ *
+ * Released under GNU GPLv2 or later, read the file 'COPYING' for more information
+ */
+
+#include "font-collections-manager.h"
+
+#include "io/resource.h"
+#include "ui/icon-names.h"
+#include "util/font-collections.h"
+
+#include <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.h>
+#include "libnrtype/font-lister.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+FontCollectionsManager::FontCollectionsManager()
+ : DialogBase("/dialogs/fontcollections", "FontCollections")
+{
+ std::string gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "dialog-font-collections.glade");
+ Glib::RefPtr<Gtk::Builder> builder;
+ try {
+ builder = Gtk::Builder::create_from_file(gladefile);
+ } catch (const Glib::Error &ex) {
+ g_error("Cannot load glade file: %s", ex.what().c_str());
+ throw;
+ }
+
+ builder->get_widget("contents", _contents);
+ builder->get_widget("paned", _paned);
+ builder->get_widget("collections_box", _collections_box);
+ builder->get_widget("buttons_box", _buttons_box);
+ builder->get_widget("font_list_box", _font_list_box);
+ builder->get_widget("font_count_label", _font_count_label);
+ builder->get_widget("font_list_filter_box", _font_list_filter_box);
+ builder->get_widget("search_entry", _search_entry);
+ builder->get_widget("reset_button", _reset_button);
+ builder->get_widget("create_button", _create_button);
+ builder->get_widget("edit_button", _edit_button);
+ builder->get_widget("delete_button", _delete_button);
+
+ _font_list_box->pack_start(_font_selector, true, true);
+ _font_list_box->reorder_child(_font_selector, 1);
+
+ _collections_box->pack_start(_user_font_collections, true, true);
+ _collections_box->reorder_child(_user_font_collections, 0);
+
+ _user_font_collections.populate_system_collections();
+ _user_font_collections.populate_user_collections();
+ _user_font_collections.change_frame_name(_("Font Collections"));
+
+ add(*_contents);
+
+ // Set the button images.
+ _create_button->set_image_from_icon_name(INKSCAPE_ICON("list-add"));
+ _edit_button->set_image_from_icon_name(INKSCAPE_ICON("document-edit"));
+ _delete_button->set_image_from_icon_name(INKSCAPE_ICON("edit-delete"));
+
+ // Paned settings.
+ _paned->child_property_resize(*_paned->get_child1()) = false;
+ _paned->child_property_resize(*_paned->get_child2()) = true;
+
+ change_font_count_label();
+ _font_selector.hide_others();
+ show_all_children();
+
+ // Setup the signals.
+ _font_count_changed_connection = Inkscape::FontLister::get_instance()->connectUpdate(sigc::mem_fun(*this, &FontCollectionsManager::change_font_count_label));
+ _search_entry->signal_search_changed().connect([=](){ on_search_entry_changed(); });
+ _user_font_collections.connect_signal_changed([=](int s){ on_selection_changed(s); });
+ _create_button->signal_clicked().connect([=](){ on_create_button_pressed(); });
+ _edit_button->signal_clicked().connect([=](){ on_edit_button_pressed(); });
+ _delete_button->signal_clicked().connect([=](){ on_delete_button_pressed(); });
+ _reset_button->signal_clicked().connect([=](){ on_reset_button_pressed(); });
+
+ // Edit and delete are initially insensitive because nothing is selected.
+ _edit_button->set_sensitive(false);
+ _delete_button->set_sensitive(false);
+}
+
+void FontCollectionsManager::on_search_entry_changed()
+{
+ auto search_txt = _search_entry->get_text();
+ _font_selector.unset_model();
+ Inkscape::FontLister *font_lister = Inkscape::FontLister::get_instance();
+ font_lister->show_results(search_txt);
+ _font_selector.set_model();
+ change_font_count_label();
+}
+
+void FontCollectionsManager::on_create_button_pressed()
+{
+ _user_font_collections.on_create_collection();
+}
+
+void FontCollectionsManager::on_delete_button_pressed()
+{
+ _user_font_collections.on_delete_button_pressed();
+}
+
+void FontCollectionsManager::on_edit_button_pressed()
+{
+ _user_font_collections.on_edit_button_pressed();
+}
+
+void FontCollectionsManager::on_reset_button_pressed()
+{
+ _search_entry->set_text("");
+ Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+
+ if(font_lister->get_font_families_size() == font_lister->get_font_list()->children().size()) {
+ // _user_font_collections.populate_collections();
+ return;
+ }
+
+ Inkscape::FontCollections::get()->clear_selected_collections();
+ font_lister->init_font_families();
+ font_lister->init_default_styles();
+ SPDocument *document = getDesktop()->getDocument();
+ font_lister->add_document_fonts_at_top(document);
+}
+
+void FontCollectionsManager::change_font_count_label()
+{
+ auto label = Inkscape::FontLister::get_instance()->get_font_count_label();
+ _font_count_label->set_label(label);
+}
+
+// This function will set the sensitivity of the edit and delete buttons
+// Whenever the selection changes.
+void FontCollectionsManager::on_selection_changed(int state)
+{
+ bool edit = false, del = false;
+ switch(state) {
+ case SYSTEM_COLLECTION:
+ break;
+ case USER_COLLECTION:
+ edit = true;
+ del = true;
+ break;
+ case USER_COLLECTION_FONT:
+ edit = false;
+ del = true;
+ default:
+ break;
+ }
+ _edit_button->set_sensitive(edit);
+ _delete_button->set_sensitive(del);
+}
+
+} // 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-collections-manager.h b/src/ui/dialog/font-collections-manager.h
new file mode 100644
index 0000000..d974e58
--- /dev/null
+++ b/src/ui/dialog/font-collections-manager.h
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Text-edit
+ */
+/* Authors:
+ * Vaihav Malik <vaibhavmalik2018@gmail.com>
+ *
+ * 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_FONT_COLLECTIONS_MANAGER_H
+#define INKSCAPE_UI_DIALOG_FONT_COLLECTIONS_MANAGER_H
+
+#include "helper/auto-connection.h"
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/font-selector.h"
+#include "ui/widget/font-collection-selector.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/**
+ * The font collections manager dialog allows the user to:
+ * 1. Create
+ * 2. Read
+ * 3. Update
+ * 4. Delete
+ * the font collections and the fonts associated with each collection.
+ *
+ * User can add new fonts in font collections by dragging the fonts from the
+ * font list and dropping them a user font collection.
+ */
+class FontCollectionsManager : public DialogBase
+{
+public:
+ enum SelectionStates {SYSTEM_COLLECTION = -1, USER_COLLECTION, USER_COLLECTION_FONT};
+
+ FontCollectionsManager();
+
+private:
+ void on_search_entry_changed();
+ void on_create_button_pressed();
+ void on_edit_button_pressed();
+ void on_delete_button_pressed();
+ void on_reset_button_pressed();
+ void change_font_count_label();
+ void on_selection_changed(int state);
+
+ /*
+ * All the dialogs widgets
+ */
+ Gtk::Box *_contents;
+ Gtk::Paned *_paned;
+ Gtk::Box *_collections_box;
+ Gtk::Box *_buttons_box;
+ Gtk::Box *_font_list_box;
+ Gtk::Label *_font_count_label;
+ Gtk::Box *_font_list_filter_box;
+ Gtk::SearchEntry *_search_entry;
+ Gtk::Button *_reset_button;
+ Gtk::Button *_create_button;
+ Gtk::Button *_edit_button;
+ Gtk::Button *_delete_button;
+ Inkscape::UI::Widget::FontSelector _font_selector;
+ Inkscape::UI::Widget::FontCollectionSelector _user_font_collections;
+
+ // Signals
+ auto_connection _font_count_changed_connection;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_FONT_COLLECTIONS_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/font-substitution.cpp b/src/ui/dialog/font-substitution.cpp
new file mode 100644
index 0000000..0e34b84
--- /dev/null
+++ b/src/ui/dialog/font-substitution.cpp
@@ -0,0 +1,241 @@
+// 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 <vector>
+
+#include <glibmm/i18n.h>
+#include <glibmm/regex.h>
+#include <glibmm/ustring.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-item.h"
+#include "object/sp-root.h"
+#include "object/sp-text.h"
+#include "object/sp-textpath.h"
+#include "object/sp-flowdiv.h"
+#include "object/sp-tspan.h"
+
+#include "libnrtype/font-factory.h"
+#include "libnrtype/font-instance.h"
+
+#include "ui/dialog-events.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+namespace {
+
+void show(std::vector<SPItem*> const &list, Glib::ustring const &out)
+{
+ Gtk::MessageDialog warning(_("Some 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"));
+
+ sp_transientize(GTK_WIDGET(warning.gobj()));
+
+ Gtk::TextView 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;
+ 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;
+ cbSelect.set_label(_("Select all the affected items"));
+ cbSelect.set_active(true);
+ cbSelect.show();
+
+ Gtk::CheckButton cbWarning;
+ cbWarning.set_label(_("Don't show this warning again"));
+ cbWarning.show();
+
+ auto box = warning.get_content_area();
+ box->set_border_width(5);
+ 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::get()->setBool("/options/font/substitutedlg", false);
+ }
+
+ if (cbSelect.get_active()) {
+ auto desktop = SP_ACTIVE_DESKTOP;
+ auto selection = desktop->getSelection();
+ selection->clear();
+ selection->setList(list);
+ }
+}
+
+/*
+ * Find all the fonts that are in the document but not available on the user's 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::pair<std::vector<SPItem*>, Glib::ustring> getFontReplacedItems(SPDocument *doc)
+{
+ std::vector<SPItem*> outList;
+ std::set<Glib::ustring> setErrors;
+ std::set<Glib::ustring> setFontSpans;
+ std::map<SPItem*, Glib::ustring> mapFontStyles;
+ Glib::ustring out;
+
+ auto const allList = get_all_items(doc->getRoot(), SP_ACTIVE_DESKTOP, false, false, true);
+ for (auto item : allList) {
+ auto 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 (auto textpath = cast<SPTextPath>(item)) {
+ if (textpath->originalPath) {
+ family = cast<SPText>(item->parent)->layout.getFontFamily(0);
+ setFontSpans.insert(family);
+ }
+ }
+ else if (is<SPTSpan>(item) || is<SPFlowtspan>(item)) {
+ // is_part_of_text_subtree (item)
+ // TSPAN layout comes from the parent->layout->_spans
+ SPObject *parent_text = item;
+ while (parent_text && !is<SPText>(parent_text)) {
+ parent_text = parent_text->parent;
+ }
+ if (parent_text) {
+ family = cast<SPText>(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 = cast<SPText>(parent_text)->layout.getFontFamily(f);
+ setFontSpans.insert(family);
+ }
+ }
+ }
+
+ if (style) {
+ char 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
+ for (auto 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 const &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 (startpos == std::string::npos || endpos == std::string::npos) {
+ continue; // empty font name
+ }
+ auto const trimmed = font.substr(startpos, endpos - startpos + 1);
+ if (setFontSpans.find(trimmed) != setFontSpans.end() ||
+ trimmed == Glib::ustring("sans-serif") ||
+ trimmed == Glib::ustring("Sans") ||
+ trimmed == Glib::ustring("serif") ||
+ trimmed == Glib::ustring("Serif") ||
+ trimmed == Glib::ustring("monospace") ||
+ trimmed == Glib::ustring("Monospace"))
+ {
+ fontFound = true;
+ break;
+ }
+ }
+ if (!fontFound) {
+ 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.emplace_back(item);
+ }
+ }
+
+ for (auto const &err : setErrors) {
+ out.append(err + "\n");
+ g_warning("%s", err.c_str());
+ }
+
+ return std::make_pair(std::move(outList), std::move(out));
+}
+
+} // namespace
+
+void checkFontSubstitutions(SPDocument *doc)
+{
+ bool show_dlg = Inkscape::Preferences::get()->getBool("/options/font/substitutedlg");
+ if (!show_dlg) {
+ return;
+ }
+
+ auto [list, msg] = getFontReplacedItems(doc);
+ if (!msg.empty()) {
+ show(list, msg);
+ }
+}
+
+} // 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..b13de12
--- /dev/null
+++ b/src/ui/dialog/font-substitution.h
@@ -0,0 +1,39 @@
+// 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_DIALOG_FONT_SUBSTITUTION_H
+#define INKSCAPE_UI_DIALOG_FONT_SUBSTITUTION_H
+
+class SPDocument;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+void checkFontSubstitutions(SPDocument *doc);
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_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/global-palettes.cpp b/src/ui/dialog/global-palettes.cpp
new file mode 100644
index 0000000..27b337e
--- /dev/null
+++ b/src/ui/dialog/global-palettes.cpp
@@ -0,0 +1,99 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include "global-palettes.h"
+
+#include <iomanip>
+
+// Using Glib::regex because
+// - std::regex is too slow in debug mode.
+// - boost::regex requires a library not present in the CI image.
+#include <glibmm/i18n.h>
+#include <glibmm/miscutils.h>
+#include <glibmm/regex.h>
+
+#include "io/resource.h"
+#include "io/sys.h"
+
+Inkscape::UI::Dialog::PaletteFileData::PaletteFileData(Glib::ustring const &path)
+{
+ name = Glib::path_get_basename(path);
+ columns = 1;
+ user = Inkscape::IO::file_is_writable(path.c_str());
+
+ auto f = std::unique_ptr<FILE, void(*)(FILE*)>(Inkscape::IO::fopen_utf8name(path.c_str(), "r"), [] (FILE *f) {if (f) std::fclose(f);});
+ if (!f) throw std::runtime_error("Failed to open file");
+
+ char buf[1024];
+ if (!std::fgets(buf, sizeof(buf), f.get())) throw std::runtime_error("File is empty");
+ if (std::strncmp("GIMP Palette", buf, 12) != 0) throw std::runtime_error("First line is wrong");
+
+ static auto const regex_rgb = Glib::Regex::create("\\s*(\\d+)\\s+(\\d+)\\s+(\\d+)\\s*(?:\\s(.*\\S)\\s*)?$", Glib::REGEX_OPTIMIZE | Glib::REGEX_ANCHORED);
+ static auto const regex_name = Glib::Regex::create("\\s*Name:\\s*(.*\\S)", Glib::REGEX_OPTIMIZE | Glib::REGEX_ANCHORED);
+ static auto const regex_cols = Glib::Regex::create("\\s*Columns:\\s*(.*\\S)", Glib::REGEX_OPTIMIZE | Glib::REGEX_ANCHORED);
+ static auto const regex_blank = Glib::Regex::create("\\s*(?:$|#)", Glib::REGEX_OPTIMIZE | Glib::REGEX_ANCHORED);
+
+ while (std::fgets(buf, sizeof(buf), f.get())) {
+ auto line = Glib::ustring(buf); // Unnecessary copy required until using a glibmm with support for string views. TODO: Fix when possible.
+ Glib::MatchInfo match;
+ if (regex_rgb->match(line, match)) { // ::regex_match(line, match, boost::regex(), boost::regex_constants::match_continuous)) {
+ // RGB color, followed by an optional name.
+ Color color;
+ for (int i = 0; i < 3; i++) {
+ color.rgb[i] = std::clamp(std::stoi(match.fetch(i + 1)), 0, 255);
+ }
+ color.name = match.fetch(4);
+
+ if (!color.name.empty()) {
+ // Translate the name if present.
+ color.name = g_dpgettext2(nullptr, "Palette", color.name.c_str());
+ } else {
+ // Otherwise, set the name to be the hex value.
+ color.name = Glib::ustring::compose("#%1%2%3",
+ Glib::ustring::format(std::hex, std::setw(2), std::setfill(L'0'), color.rgb[0]),
+ Glib::ustring::format(std::hex, std::setw(2), std::setfill(L'0'), color.rgb[1]),
+ Glib::ustring::format(std::hex, std::setw(2), std::setfill(L'0'), color.rgb[2])
+ ).uppercase();
+ }
+
+ colors.emplace_back(std::move(color));
+ } else if (regex_name->match(line, match)) {
+ // Header entry for name.
+ name = match.fetch(1);
+ } else if (regex_cols->match(line, match)) {
+ // Header entry for columns.
+ columns = std::clamp(std::stoi(match.fetch(1)), 1, 1000);
+ } else if (regex_blank->match(line, match)) {
+ // Comment or blank line.
+ } else {
+ // Unrecognised.
+ throw std::runtime_error("Invalid line " + std::string(line));
+ }
+ }
+}
+
+Inkscape::UI::Dialog::GlobalPalettes::GlobalPalettes()
+{
+ // Load the palettes.
+ for (auto &path : Inkscape::IO::Resource::get_filenames(Inkscape::IO::Resource::PALETTES, {".gpl"})) {
+ try {
+ palettes.emplace_back(path);
+ } catch (std::runtime_error const &e) {
+ g_warning("Error loading palette %s: %s", path.c_str(), e.what());
+ } catch (std::logic_error const &e) {
+ g_warning("Error loading palette %s: %s", path.c_str(), e.what());
+ }
+ }
+
+ std::sort(palettes.begin(), palettes.end(), [] (decltype(palettes)::const_reference a, decltype(palettes)::const_reference b) {
+ // Sort by user/system first...
+ if (a.user > b.user) return true;
+ if (b.user > a.user) return false;
+ // ... then by name.
+ return a.name.compare(b.name) < 0;
+ });
+}
+
+Inkscape::UI::Dialog::GlobalPalettes const &Inkscape::UI::Dialog::GlobalPalettes::get()
+{
+ static GlobalPalettes instance;
+ return instance;
+}
diff --git a/src/ui/dialog/global-palettes.h b/src/ui/dialog/global-palettes.h
new file mode 100644
index 0000000..f0a1fe0
--- /dev/null
+++ b/src/ui/dialog/global-palettes.h
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Global color palette information.
+ */
+/* Authors: PBS <pbs3141@gmail.com>
+ * Copyright (C) 2022 PBS
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_GLOBAL_PALETTES_H
+#define INKSCAPE_UI_DIALOG_GLOBAL_PALETTES_H
+
+#include <array>
+#include <vector>
+#include <glibmm/ustring.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/**
+ * The data loaded from a palette file.
+ */
+struct PaletteFileData
+{
+ /// Name of the palette, either specified in the file or taken from the filename.
+ Glib::ustring name;
+
+ /// The preferred number of columns (unused).
+ int columns;
+
+ /// Whether this is a user or system palette.
+ bool user;
+
+ struct Color
+ {
+ /// RGB color.
+ std::array<unsigned, 3> rgb;
+
+ /// Name of the color, either specified in the file or generated from the rgb.
+ Glib::ustring name;
+ };
+
+ /// The list of colors in the palette.
+ std::vector<Color> colors;
+
+ /// Load from the given file, throwing std::runtime_error on fail.
+ PaletteFileData(Glib::ustring const &path);
+};
+
+/**
+ * Singleton class that manages the static list of global palettes.
+ */
+class GlobalPalettes
+{
+ GlobalPalettes();
+public:
+ static GlobalPalettes const &get();
+ std::vector<PaletteFileData> palettes;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_GLOBAL_PALETTES_H
diff --git a/src/ui/dialog/glyphs.cpp b/src/ui/dialog/glyphs.cpp
new file mode 100644
index 0000000..620f385
--- /dev/null
+++ b/src/ui/dialog/glyphs.cpp
@@ -0,0 +1,777 @@
+// 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 "libnrtype/font-instance.h"
+#include "libnrtype/font-lister.h"
+#include "libnrtype/font-factory.h"
+
+#include "object/sp-flowtext.h"
+#include "object/sp-text.h"
+
+#include "ui/icon-names.h"
+#include "ui/widget/font-selector.h"
+#include "ui/widget/scrollprotected.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+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()
+ : DialogBase("/dialogs/glyphs", "Glyphs")
+ , store(Gtk::ListStore::create(*getColumns()))
+ , instanceConns()
+{
+ set_orientation(Gtk::ORIENTATION_VERTICAL);
+ auto table = new Gtk::Grid();
+ table->set_row_spacing(4);
+ table->set_column_spacing(4);
+ 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 Inkscape::UI::Widget::ScrollProtected<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 Inkscape::UI::Widget::ScrollProtected<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::Box *box = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL);
+
+ entry = std::make_shared<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(*entry.get(), Gtk::PACK_SHRINK);
+
+ Gtk::Label *pad = new Gtk::Label(" ");
+ box->pack_start(*Gtk::manage(pad), Gtk::PACK_SHRINK);
+
+ label = std::make_shared<Gtk::Label>(" ");
+ box->pack_start(*label.get(), Gtk::PACK_SHRINK);
+
+ pad = new Gtk::Label("");
+ box->pack_start(*Gtk::manage(pad), Gtk::PACK_EXPAND_WIDGET);
+
+ insertBtn = std::make_shared<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(*insertBtn.get(), Gtk::PACK_SHRINK);
+ box->set_hexpand();
+ table->attach( *Gtk::manage(box), 0, row, 3, 1);
+
+ row++;
+
+// -------------------------------
+
+
+ show_all_children();
+}
+
+GlyphsPanel::~GlyphsPanel()
+{
+ for (auto & instanceConn : instanceConns) {
+ instanceConn.disconnect();
+ }
+ instanceConns.clear();
+}
+
+void GlyphsPanel::selectionChanged(Selection *selection)
+{
+ readSelection(true, true);
+}
+void GlyphsPanel::selectionModified(Selection *selection, 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);
+}
+
+// Append selected glyphs to selected text
+void GlyphsPanel::insertText()
+{
+ auto selection = getSelection();
+ if (!selection)
+ return;
+
+ SPItem *textItem = nullptr;
+ auto itemlist = selection->items();
+ for(auto i=itemlist.begin(); itemlist.end() != i; ++i) {
+ if (is<SPText>(*i) || is<SPFlowtext>(*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 = sp_te_get_string_multiline(textItem);
+ combined += glyphs;
+ sp_te_set_repr_text_multiline(textItem, combined.c_str());
+ DocumentUndo::done(getDocument(), _("Append text"), INKSCAPE_ICON("draw-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::calcCanInsert()
+{
+ auto selection = getSelection();
+ if (!selection)
+ return;
+
+ int items = 0;
+ auto itemlist = selection->items();
+ for(auto i=itemlist.begin(); itemlist.end() != i; ++i) {
+ if (is<SPText>(*i) || is<SPFlowtext>(*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();
+
+ std::shared_ptr<FontInstance> font;
+ if (!fontspec.empty()) {
+ font = FontFactory::get().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..2720436
--- /dev/null
+++ b/src/ui/dialog/glyphs.h
@@ -0,0 +1,84 @@
+// 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 <gtkmm/treemodel.h>
+
+#include "ui/dialog/dialog-base.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 DialogBase
+{
+public:
+ GlyphsPanel();
+ ~GlyphsPanel() override;
+
+ void selectionChanged(Selection *selection) override;
+ void selectionModified(Selection *selection, guint flags) override;
+
+private:
+ static GlyphColumns *getColumns();
+
+ void rebuild();
+
+ void glyphActivated(Gtk::TreeModel::Path const & path);
+ void glyphSelectionChanged();
+ void readSelection( bool updateStyle, bool updateContent );
+ void calcCanInsert();
+ void insertText();
+
+ Glib::RefPtr<Gtk::ListStore> store;
+ Gtk::IconView *iconView;
+ std::shared_ptr<Gtk::Entry> entry;
+ std::shared_ptr<Gtk::Label> label;
+ std::shared_ptr<Gtk::Button> insertBtn;
+ Gtk::ComboBoxText *scriptCombo;
+ Gtk::ComboBoxText *rangeCombo;
+ Inkscape::UI::Widget::FontSelector *fontSelector;
+
+ std::vector<sigc::connection> instanceConns;
+};
+
+
+} // 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..2003367
--- /dev/null
+++ b/src/ui/dialog/grid-arrange-tab.cpp
@@ -0,0 +1,660 @@
+// 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 <numeric>
+#include <glibmm/i18n.h>
+
+#include <gtkmm/grid.h>
+#include <gtkmm/sizegroup.h>
+
+#include <2geom/transforms.h>
+
+#include "preferences.h"
+#include "inkscape.h"
+
+#include "document.h"
+#include "document-undo.h"
+#include "desktop.h"
+
+#include "ui/icon-names.h"
+#include "ui/dialog/tile.h" // for Inkscape::UI::Dialog::ArrangeDialog
+
+
+/**
+ * Sort ObjectSet by an existing grid arrangement
+ *
+ * This is based on this paper here: DOI:10.1049/iet-ipr.2015.0126
+ *
+ * @param items - The unsorted object set to sort.
+ * @returns a new grid-sorted std::vector of SPItems.
+ */
+static std::vector<SPItem *> grid_item_sort(Inkscape::ObjectSet *items)
+{
+ std::vector<SPItem *> results;
+ Inkscape::ObjectSet rest;
+
+ // 1. Find middle Y position of the largest top object.
+ double box_top = items->visualBounds()->min()[Geom::Y];
+ double last_height = 0.0;
+ double target = box_top;
+ for (auto item : items->items()) {
+ if (auto item_box = item->desktopVisualBounds()) {
+ if (Geom::are_near(item_box->min()[Geom::Y], box_top, 2.0)) {
+ if (item_box->height() > last_height) {
+ last_height = item_box->height();
+ target = item_box->midpoint()[Geom::Y];
+ }
+ }
+ }
+ }
+
+ // 2. Loop through all remaining items
+ for (auto item : items->items()) {
+ // Items without visual bounds are completely ignored.
+ if (auto item_box = item->desktopVisualBounds()) {
+ auto radius = item_box->height() / 2;
+ auto min = item_box->midpoint()[Geom::Y] - radius;
+ auto max = item_box->midpoint()[Geom::Y] + radius;
+
+ if (max > target && min < target) {
+ // 2a. if the item's radius falls on the Y position above
+ results.push_back(item);
+ } else {
+ // 2b. Save items not in this row for later
+ rest.add(item);
+ }
+ }
+ }
+
+ // 3. Sort this single row according to the X position
+ std::sort(results.begin(), results.end(), [](SPItem *a, SPItem *b) {
+ // These boxes always exist because of the above filtering.
+ return (a->desktopVisualBounds()->min()[Geom::X] < b->desktopVisualBounds()->min()[Geom::X]);
+ });
+
+ if (results.size() == 0) {
+ g_warning("Bad grid detection when sorting items!");
+ } else if (!rest.isEmpty()) {
+ // 4. If there's any remaining, run this function again.
+ auto sorted_rest = grid_item_sort(&rest);
+ results.reserve(items->size());
+ results.insert(results.end(), sorted_rest.begin(), sorted_rest.end());
+ }
+ return results;
+}
+
+ namespace Inkscape {
+ namespace UI {
+ namespace Dialog {
+
+
+ //#########################################################################
+ //## E V E N T S
+ //#########################################################################
+
+ /*
+ *
+ * This arranges the selection in a grid pattern.
+ *
+ */
+
+ void GridArrangeTab::arrange()
+ {
+ // check for correct numbers in the row- and col-spinners
+ on_col_spinbutton_changed();
+ on_row_spinbutton_changed();
+
+ // set padding to manual values
+ double paddingx = XPadding.getValue("px");
+ double paddingy = YPadding.getValue("px");
+ int NoOfCols = NoOfColsSpinner.get_value_as_int();
+ int NoOfRows = NoOfRowsSpinner.get_value_as_int();
+
+ SPDesktop *desktop = Parent->getDesktop();
+ desktop->getDocument()->ensureUpToDate();
+ Inkscape::Selection *selection = desktop->getSelection();
+ if (!selection || selection->isEmpty()) return;
+
+ auto sel_box = selection->documentBounds(SPItem::VISUAL_BBOX);
+ if (sel_box.empty()) return;
+ double grid_left = sel_box->min()[Geom::X];
+ double grid_top = sel_box->min()[Geom::Y];
+
+ // require the sorting done before we can calculate row heights etc.
+ auto sorted = grid_item_sort(selection);
+
+ // Calculate individual Row and Column sizes if necessary
+ auto row_heights = std::vector<double>(NoOfRows, 0.0);
+ auto col_widths = std::vector<double>(NoOfCols, 0.0);
+ for(int i = 0; i < sorted.size(); i++) {
+ if (Geom::OptRect box = sorted[i]->documentVisualBounds()) {
+ double width = box->dimensions()[Geom::X];
+ double height = box->dimensions()[Geom::Y];
+ if (width > col_widths[(i % NoOfCols)]) {
+ col_widths[(i % NoOfCols)] = width;
+ }
+ if (height > row_heights[(i / NoOfCols)]) {
+ row_heights[(i / NoOfCols)] = height;
+ }
+ }
+ }
+
+ double col_width = *std::max_element(std::begin(col_widths), std::end(col_widths));
+ double row_height = *std::max_element(std::begin(row_heights), std::end(row_heights));
+
+ /// Make sure the top and left of the grid don't move by compensating for align values.
+ if (RowHeightButton.get_active()){
+ grid_top = grid_top - (((row_height - row_heights[0]) / 2)*(VertAlign));
+ }
+ if (ColumnWidthButton.get_active()){
+ grid_left = grid_left - (((col_width - col_widths[0]) /2)*(HorizAlign));
+ }
+
+ // Calculate total widths and heights, allowing for columns and rows non uniformly sized.
+ double last_col_padding = 0.0;
+ double total_col_width = 0.0;
+ if (ColumnWidthButton.get_active()){
+ total_col_width = col_width * NoOfCols;
+ // Remember the amount of padding the box will lose on the right side
+ last_col_padding = (col_width - col_widths[NoOfCols-1]) / 2;
+ std::fill(col_widths.begin(), col_widths.end(), col_width);
+ } else {
+ total_col_width = std::accumulate(col_widths.begin(), col_widths.end(), 0);
+ }
+
+ double last_row_padding = 0.0;
+ double total_row_height = 0.0;
+ if (RowHeightButton.get_active()){
+ total_row_height = row_height * NoOfRows;
+ // Remember the amount of padding the box will lose on the bottom side
+ last_row_padding = (row_height - row_heights[NoOfRows-1]) / 2;
+ std::fill(row_heights.begin(), row_heights.end(), row_height);
+ } else {
+ total_row_height = std::accumulate(row_heights.begin(), row_heights.end(), 0);
+ }
+
+ // Fit to bbox, calculate padding between rows accordingly.
+ if (SpaceByBBoxRadioButton.get_active()) {
+ paddingx = (sel_box->width() - total_col_width + last_col_padding) / (NoOfCols -1);
+ paddingy = (sel_box->height() - total_row_height + last_row_padding) / (NoOfRows -1);
+ }
+
+/*
+ Horizontal align - Left = 0
+ Centre = 1
+ Right = 2
+
+ Vertical align - Top = 0
+ Middle = 1
+ Bottom = 2
+
+ X position is calculated by taking the grids left co-ord, adding the distance to the column,
+ then adding 1/2 the spacing multiplied by the align variable above,
+ Y position likewise, takes the top of the grid, adds the y to the current row then adds the padding in to align it.
+
+*/
+
+ // Calculate row and column x and y coords required to allow for columns and rows which are non uniformly sized.
+ std::vector<double> col_xs = {0.0};
+ for (int col=1; col < NoOfCols; col++) {
+ col_xs.push_back(col_widths[col-1] + paddingx + col_xs[col-1]);
+ }
+
+ std::vector<double> row_ys = {0.0};
+ for (int row=1; row < NoOfRows; row++) {
+ row_ys.push_back(row_heights[row-1] + paddingy + row_ys[row-1]);
+ }
+
+ int cnt = 0;
+ std::vector<SPItem*>::iterator it = sorted.begin();
+ for (int row_cnt=0; ((it != sorted.end()) && (row_cnt<NoOfRows)); ++row_cnt) {
+
+ std::vector<SPItem *> current_row;
+ int col_cnt = 0;
+ for(;it!=sorted.end()&&col_cnt<NoOfCols;++it) {
+ current_row.push_back(*it);
+ col_cnt++;
+ }
+
+ for (auto item:current_row) {
+ auto min = Geom::Point(0, 0);
+ double width = 0, height = 0;
+ if (auto vbox = item->documentVisualBounds()) {
+ width = vbox->dimensions()[Geom::X];
+ height = vbox->dimensions()[Geom::Y];
+ min = vbox->min();
+ }
+
+ int row = cnt / NoOfCols;
+ int col = cnt % NoOfCols;
+
+ double new_x = grid_left + (((col_widths[col] - width)/2)*HorizAlign) + col_xs[col];
+ double new_y = grid_top + (((row_heights[row] - height)/2)*VertAlign) + row_ys[row];
+
+ Geom::Point move = Geom::Point(new_x, new_y) - min;
+ Geom::Affine const affine = Geom::Affine(Geom::Translate(move));
+ item->set_i2d_affine(item->i2doc_affine() * affine * item->document->doc2dt());
+ item->doWriteTransform(item->transform);
+ item->updateRepr();
+ cnt +=1;
+ }
+ }
+
+ DocumentUndo::done(desktop->getDocument(), _("Arrange in a grid"), INKSCAPE_ICON("dialog-align-and-distribute"));
+
+}
+
+
+//#########################################################################
+//## E V E N T S
+//#########################################################################
+
+/**
+ * changed value in # of columns spinbox.
+ */
+void GridArrangeTab::on_row_spinbutton_changed()
+{
+ SPDesktop *desktop = Parent->getDesktop();
+ Inkscape::Selection *selection = desktop ? desktop->getSelection() : nullptr;
+ if (!selection) return;
+
+ int selcount = (int) boost::distance(selection->items());
+
+ double NoOfRows = ceil(selcount / NoOfColsSpinner.get_value());
+ NoOfRowsSpinner.set_value(NoOfRows);
+}
+
+/**
+ * changed value in # of rows spinbox.
+ */
+void GridArrangeTab::on_col_spinbutton_changed()
+{
+ SPDesktop *desktop = Parent->getDesktop();
+ Inkscape::Selection *selection = desktop ? desktop->getSelection() : nullptr;
+ if (!selection) return;
+
+ int selcount = (int) boost::distance(selection->items());
+
+ double NoOfCols = ceil(selcount / NoOfRowsSpinner.get_value());
+ NoOfColsSpinner.set_value(NoOfCols);
+}
+
+/**
+ * changed value in x padding spinbox.
+ */
+void GridArrangeTab::on_xpad_spinbutton_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble("/dialogs/gridtiler/XPad", XPadding.getValue("px"));
+
+}
+
+/**
+ * changed value in y padding spinbox.
+ */
+void GridArrangeTab::on_ypad_spinbutton_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble("/dialogs/gridtiler/YPad", YPadding.getValue("px"));
+}
+
+
+/**
+ * checked/unchecked autosize Rows button.
+ */
+void GridArrangeTab::on_RowSize_checkbutton_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (RowHeightButton.get_active()) {
+ prefs->setDouble("/dialogs/gridtiler/AutoRowSize", 20);
+ } else {
+ prefs->setDouble("/dialogs/gridtiler/AutoRowSize", -20);
+ }
+ RowHeightBox.set_sensitive ( !RowHeightButton.get_active());
+}
+
+/**
+ * checked/unchecked autosize Rows button.
+ */
+void GridArrangeTab::on_ColSize_checkbutton_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (ColumnWidthButton.get_active()) {
+ prefs->setDouble("/dialogs/gridtiler/AutoColSize", 20);
+ } else {
+ prefs->setDouble("/dialogs/gridtiler/AutoColSize", -20);
+ }
+ ColumnWidthBox.set_sensitive ( !ColumnWidthButton.get_active());
+}
+
+/**
+ * changed value in columns spinbox.
+ */
+void GridArrangeTab::on_rowSize_spinbutton_changed()
+{
+ // quit if run by the attr_changed listener
+ if (updating) {
+ return;
+ }
+
+ // in turn, prevent listener from responding
+ updating = true;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble("/dialogs/gridtiler/RowHeight", RowHeightSpinner.get_value());
+ updating=false;
+
+}
+
+/**
+ * changed value in rows spinbox.
+ */
+void GridArrangeTab::on_colSize_spinbutton_changed()
+{
+ // quit if run by the attr_changed listener
+ if (updating) {
+ return;
+ }
+
+ // in turn, prevent listener from responding
+ updating = true;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble("/dialogs/gridtiler/ColWidth", ColumnWidthSpinner.get_value());
+ updating=false;
+
+}
+
+/**
+ * changed Radio button in Spacing group.
+ */
+void GridArrangeTab::Spacing_button_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (SpaceManualRadioButton.get_active()) {
+ prefs->setDouble("/dialogs/gridtiler/SpacingType", 20);
+ } else {
+ prefs->setDouble("/dialogs/gridtiler/SpacingType", -20);
+ }
+
+ XPadding.set_sensitive ( SpaceManualRadioButton.get_active());
+ YPadding.set_sensitive ( SpaceManualRadioButton.get_active());
+}
+
+/**
+ * changed Anchor selection widget.
+ */
+void GridArrangeTab::Align_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ VertAlign = AlignmentSelector.getVerticalAlignment();
+ prefs->setInt("/dialogs/gridtiler/VertAlign", VertAlign);
+ HorizAlign = AlignmentSelector.getHorizontalAlignment();
+ prefs->setInt("/dialogs/gridtiler/HorizAlign", HorizAlign);
+}
+
+/**
+ * Desktop selection changed
+ */
+void GridArrangeTab::updateSelection()
+{
+ // quit if run by the attr_changed listener
+ if (updating) {
+ return;
+ }
+
+ // in turn, prevent listener from responding
+ updating = true;
+ SPDesktop *desktop = Parent->getDesktop();
+ Inkscape::Selection *selection = desktop ? desktop->getSelection() : nullptr;
+ std::vector<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);
+ }
+ } else {
+ double PerRow = ceil(sqrt(selcount));
+ double PerCol = ceil(sqrt(selcount));
+ NoOfRowsSpinner.set_value(PerRow);
+ NoOfColsSpinner.set_value(PerCol);
+ }
+ }
+
+ updating = false;
+}
+
+void GridArrangeTab::setDesktop(SPDesktop *desktop)
+{
+ _selection_changed_connection.disconnect();
+
+ if (desktop) {
+ updateSelection();
+
+ _selection_changed_connection = INKSCAPE.signal_selection_changed.connect(
+ sigc::hide<0>(sigc::mem_fun(*this, &GridArrangeTab::updateSelection)));
+ }
+}
+
+
+//#########################################################################
+//## C O N S T R U C T O R / D E S T R U C T O R
+//#########################################################################
+/**
+ * Constructor
+ */
+GridArrangeTab::GridArrangeTab(ArrangeDialog *parent)
+ : Parent(parent),
+ XPadding(_("X:"), _("Horizontal spacing between columns."), UNIT_TYPE_LINEAR, "", "object-columns", &PaddingUnitMenu),
+ YPadding(_("Y:"), _("Vertical spacing between rows."), XPadding, "", "object-rows"),
+ PaddingTable(Gtk::manage(new Gtk::Grid()))
+{
+ // bool used by spin button callbacks to stop loops where they change each other.
+ updating = false;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ auto _col1 = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL);
+ auto _col2 = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL);
+ auto _col3 = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL);
+
+ Gtk::Box *contents = this;
+ set_valign(Gtk::ALIGN_START);
+
+#define MARGIN 2
+
+ //##Set up the panel
+
+ NoOfRowsLabel.set_text_with_mnemonic(_("_Rows:"));
+ NoOfRowsLabel.set_mnemonic_widget(NoOfRowsSpinner);
+ NoOfRowsBox.set_orientation(Gtk::ORIENTATION_VERTICAL);
+ NoOfRowsBox.pack_start(NoOfRowsLabel, false, false, MARGIN);
+
+ NoOfRowsSpinner.set_digits(0);
+ NoOfRowsSpinner.set_increments(1, 0);
+ NoOfRowsSpinner.set_range(1.0, 10000.0);
+ NoOfRowsSpinner.signal_changed().connect(sigc::mem_fun(*this, &GridArrangeTab::on_col_spinbutton_changed));
+ NoOfRowsSpinner.set_tooltip_text(_("Number of rows"));
+ NoOfRowsBox.pack_start(NoOfRowsSpinner, false, false, MARGIN);
+ _col1->add_widget(NoOfRowsBox);
+
+ RowHeightButton.set_label(_("Equal _height"));
+ RowHeightButton.set_use_underline(true);
+ double AutoRow = prefs->getDouble("/dialogs/gridtiler/AutoRowSize", 15);
+ if (AutoRow>0)
+ AutoRowSize=true;
+ else
+ AutoRowSize=false;
+ RowHeightButton.set_active(AutoRowSize);
+
+ NoOfRowsBox.pack_start(RowHeightButton, false, false, MARGIN);
+
+ RowHeightButton.set_tooltip_text(_("If not set, each row has the height of the tallest object in it"));
+ RowHeightButton.signal_toggled().connect(sigc::mem_fun(*this, &GridArrangeTab::on_RowSize_checkbutton_changed));
+
+ SpinsHBox.pack_start(NoOfRowsBox, false, false, MARGIN);
+
+
+ /*#### Label for X ####*/
+ padXByYLabel.set_label(" ");
+ XByYLabelVBox.set_orientation(Gtk::ORIENTATION_VERTICAL);
+ XByYLabelVBox.pack_start(padXByYLabel, false, false, MARGIN);
+ XByYLabel.set_markup(" &#215; ");
+ XByYLabelVBox.pack_start(XByYLabel, false, false, MARGIN);
+ SpinsHBox.pack_start(XByYLabelVBox, false, false, MARGIN);
+ _col2->add_widget(XByYLabelVBox);
+
+ /*#### Number of columns ####*/
+
+ NoOfColsLabel.set_text_with_mnemonic(_("_Columns:"));
+ NoOfColsLabel.set_mnemonic_widget(NoOfColsSpinner);
+ NoOfColsBox.set_orientation(Gtk::ORIENTATION_VERTICAL);
+ NoOfColsBox.pack_start(NoOfColsLabel, false, false, MARGIN);
+
+ NoOfColsSpinner.set_digits(0);
+ NoOfColsSpinner.set_increments(1, 0);
+ NoOfColsSpinner.set_range(1.0, 10000.0);
+ NoOfColsSpinner.signal_changed().connect(sigc::mem_fun(*this, &GridArrangeTab::on_row_spinbutton_changed));
+ NoOfColsSpinner.set_tooltip_text(_("Number of columns"));
+ NoOfColsBox.pack_start(NoOfColsSpinner, false, false, MARGIN);
+ _col3->add_widget(NoOfColsBox);
+
+ ColumnWidthButton.set_label(_("Equal _width"));
+ ColumnWidthButton.set_use_underline(true);
+ double AutoCol = prefs->getDouble("/dialogs/gridtiler/AutoColSize", 15);
+ if (AutoCol>0)
+ AutoColSize=true;
+ else
+ AutoColSize=false;
+ ColumnWidthButton.set_active(AutoColSize);
+ NoOfColsBox.pack_start(ColumnWidthButton, false, false, MARGIN);
+
+ ColumnWidthButton.set_tooltip_text(_("If not set, each column has the width of the widest object in it"));
+ ColumnWidthButton.signal_toggled().connect(sigc::mem_fun(*this, &GridArrangeTab::on_ColSize_checkbutton_changed));
+
+ SpinsHBox.pack_start(NoOfColsBox, false, false, MARGIN);
+
+ TileBox.set_orientation(Gtk::ORIENTATION_VERTICAL);
+ TileBox.pack_start(SpinsHBox, false, false, MARGIN);
+
+ VertAlign = prefs->getInt("/dialogs/gridtiler/VertAlign", 1);
+ HorizAlign = prefs->getInt("/dialogs/gridtiler/HorizAlign", 1);
+
+ // Anchor selection widget
+ AlignLabel.set_label(_("Alignment:"));
+ AlignLabel.set_halign(Gtk::ALIGN_START);
+ AlignLabel.set_valign(Gtk::ALIGN_CENTER);
+ AlignmentSelector.setAlignment(HorizAlign, VertAlign);
+ AlignmentSelector.on_selectionChanged().connect(sigc::mem_fun(*this, &GridArrangeTab::Align_changed));
+ TileBox.pack_start(AlignLabel, false, false, MARGIN);
+ TileBox.pack_start(AlignmentSelector, true, false, MARGIN);
+
+ {
+ /*#### Radio buttons to control spacing manually or to fit selection bbox ####*/
+ SpaceByBBoxRadioButton.set_label(_("_Fit into selection box"));
+ SpaceByBBoxRadioButton.set_use_underline (true);
+ SpaceByBBoxRadioButton.signal_toggled().connect(sigc::mem_fun(*this, &GridArrangeTab::Spacing_button_changed));
+ SpacingGroup = SpaceByBBoxRadioButton.get_group();
+
+ SpacingVBox.pack_start(SpaceByBBoxRadioButton, false, false, MARGIN);
+
+ SpaceManualRadioButton.set_label(_("_Set spacing:"));
+ SpaceManualRadioButton.set_use_underline (true);
+ SpaceManualRadioButton.set_group(SpacingGroup);
+ SpaceManualRadioButton.signal_toggled().connect(sigc::mem_fun(*this, &GridArrangeTab::Spacing_button_changed));
+ SpacingVBox.pack_start(SpaceManualRadioButton, false, false, MARGIN);
+
+ TileBox.pack_start(SpacingVBox, false, false, MARGIN);
+ }
+
+ {
+ /*#### Padding ####*/
+ PaddingUnitMenu.setUnitType(UNIT_TYPE_LINEAR);
+ PaddingUnitMenu.setUnit("px");
+
+ YPadding.setDigits(5);
+ YPadding.setIncrements(0.2, 0);
+ YPadding.setRange(-10000, 10000);
+ double yPad = prefs->getDouble("/dialogs/gridtiler/YPad", 15);
+ YPadding.setValue(yPad, "px");
+ YPadding.signal_value_changed().connect(sigc::mem_fun(*this, &GridArrangeTab::on_ypad_spinbutton_changed));
+
+ XPadding.setDigits(5);
+ XPadding.setIncrements(0.2, 0);
+ XPadding.setRange(-10000, 10000);
+ double xPad = prefs->getDouble("/dialogs/gridtiler/XPad", 15);
+ XPadding.setValue(xPad, "px");
+
+ XPadding.signal_value_changed().connect(sigc::mem_fun(*this, &GridArrangeTab::on_xpad_spinbutton_changed));
+ }
+
+ PaddingTable->set_border_width(MARGIN);
+ PaddingTable->set_row_spacing(MARGIN);
+ PaddingTable->set_column_spacing(MARGIN);
+ PaddingTable->attach(XPadding, 0, 0, 1, 1);
+ PaddingTable->attach(PaddingUnitMenu, 1, 0, 1, 1);
+ PaddingTable->attach(YPadding, 0, 1, 1, 1);
+
+ TileBox.pack_start(*PaddingTable, false, false, MARGIN);
+
+ contents->set_border_width(4);
+ contents->pack_start(TileBox);
+
+ double SpacingType = prefs->getDouble("/dialogs/gridtiler/SpacingType", 15);
+ if (SpacingType>0) {
+ ManualSpacing=true;
+ } else {
+ ManualSpacing=false;
+ }
+ SpaceManualRadioButton.set_active(ManualSpacing);
+ SpaceByBBoxRadioButton.set_active(!ManualSpacing);
+ XPadding.set_sensitive (ManualSpacing);
+ YPadding.set_sensitive (ManualSpacing);
+
+ show_all_children();
+}
+
+
+GridArrangeTab::~GridArrangeTab() {
+ setDesktop(nullptr);
+}
+
+} //namespace Dialog
+} //namespace UI
+} //namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/grid-arrange-tab.h b/src/ui/dialog/grid-arrange-tab.h
new file mode 100644
index 0000000..6c244b3
--- /dev/null
+++ b/src/ui/dialog/grid-arrange-tab.h
@@ -0,0 +1,152 @@
+// 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>
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class ArrangeDialog;
+
+/**
+ * Dialog for tiling an object
+ */
+class GridArrangeTab : public ArrangeTab {
+public:
+ GridArrangeTab(ArrangeDialog *parent);
+ ~GridArrangeTab() override;
+
+ /**
+ * 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 updating;
+
+ Gtk::Box TileBox;
+
+ // 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::Box 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;
+
+ sigc::connection _selection_changed_connection;
+
+ public:
+ void setDesktop(SPDesktop *);
+};
+
+} //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..dcb1d40
--- /dev/null
+++ b/src/ui/dialog/guides.cpp
@@ -0,0 +1,387 @@
+// 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 "include/gtkmm_version.h"
+
+#include "object/sp-guide.h"
+#include "object/sp-namedview.h"
+
+#include "page-manager.h"
+
+#include "ui/dialog-events.h"
+#include "ui/tools/tool-base.h"
+#include "ui/widget/spinbutton.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);
+
+ auto pos = _oldpos;
+
+ // Adjust position by the page position
+ auto prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/options/origincorrection/page", true)) {
+ auto &pm = _guide->document->getPageManager();
+ pos *= pm.getSelectedPageAffine().inverse();
+ }
+
+ _spin_button_x.setValueKeepUnit(pos[Geom::X], "px");
+ _spin_button_y.setValueKeepUnit(pos[Geom::Y], "px");
+ }
+}
+
+void GuidelinePropertiesDialog::_onOK()
+{
+ this->_onOKimpl();
+ DocumentUndo::done(_guide->document, _("Set guide properties"), "");
+
+}
+
+void GuidelinePropertiesDialog::_onOKimpl()
+{
+ 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);
+
+ // Adjust position by either the relative position, or the page offset
+ auto prefs = Inkscape::Preferences::get();
+ if (!_mode) {
+ newpos += _oldpos;
+ } else if (prefs->getBool("/options/origincorrection/page", true)) {
+ auto &pm = _guide->document->getPageManager();
+ newpos *= pm.getSelectedPageAffine();
+ }
+
+ _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);
+}
+
+void GuidelinePropertiesDialog::_onDelete()
+{
+ SPDocument *doc = _guide->document;
+ if (_guide->remove(true))
+ DocumentUndo::done(doc, _("Delete guide"), "");
+}
+
+void GuidelinePropertiesDialog::_onDuplicate()
+{
+ _guide = _guide->duplicate();
+ this->_onOKimpl();
+ DocumentUndo::done(_guide->document, _("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
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ size_t minimumexponent = std::min(std::abs(prefs->getInt("/options/svgoutput/minimumexponent", -8)), 5); //we limit to 5 to minimize rounding errors
+ _spin_button_x.setDigits(minimumexponent);
+ _spin_button_x.setAlignment(1.0);
+ _spin_button_x.setIncrements(1.0, 10.0);
+ _spin_button_x.setRange(-1e6, 1e6);
+ _spin_button_y.setDigits(minimumexponent);
+
+ _spin_button_y.setAlignment(1.0);
+ _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.setDigits(minimumexponent);
+ _spin_angle.setAlignment(1.0);
+ _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->getLockGuides();
+ if(global_guides_lock){
+ _locked_toggle.set_sensitive(false);
+ }
+ _locked_toggle.set_active(_guide->getLocked());
+
+ // This results in the dialog closing when entering a value in one of the spinbuttons and pressing enter (see LP bug 484187)
+ auto sbx = dynamic_cast<UI::Widget::SpinButton *>(_spin_button_x.getWidget());
+ auto sby = dynamic_cast<UI::Widget::SpinButton *>(_spin_button_y.getWidget());
+ auto sba = dynamic_cast<UI::Widget::SpinButton *>(_spin_button_y.getWidget());
+
+ if(sbx) sbx->signal_activate().connect(sigc::mem_fun(*this, &GuidelinePropertiesDialog::on_sb_activate));
+ if(sby) sby->signal_activate().connect(sigc::mem_fun(*this, &GuidelinePropertiesDialog::on_sb_activate));
+ if(sba) sba->signal_activate().connect(sigc::mem_fun(*this, &GuidelinePropertiesDialog::on_sb_activate));
+
+ // 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] ) );
+ }
+
+ {
+ gchar *label = g_strdup_printf(_("Guideline ID: %s"), _guide->getId());
+ _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;
+}
+
+void
+GuidelinePropertiesDialog::on_sb_activate()
+{
+ activate_default();
+}
+}
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ 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..9c449ce
--- /dev/null
+++ b/src/ui/dialog/guides.h
@@ -0,0 +1,106 @@
+// 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 _onOKimpl();
+ 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;
+
+ void on_sb_activate();
+};
+
+} // 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..d62937c
--- /dev/null
+++ b/src/ui/dialog/icon-preview.cpp
@@ -0,0 +1,637 @@
+// 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 "page-manager.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 {
+
+//#########################################################################
+//## E V E N T S
+//#########################################################################
+
+void IconPreviewPanel::on_button_clicked(int which)
+{
+ if ( hot != which ) {
+ buttons[hot]->set_active( false );
+
+ hot = which;
+ updateMagnify();
+ 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()
+ : DialogBase("/dialogs/iconpreview", "IconPreview")
+ , drawing(nullptr)
+ , drawing_doc(nullptr)
+ , visionkey(0)
+ , timer(nullptr)
+ , renderTimer(nullptr)
+ , pending(false)
+ , minDelay(0.1)
+ , targetId()
+ , hot(1)
+ , selectionButton(nullptr)
+ , docModConn()
+ , iconBox(Gtk::ORIENTATION_VERTICAL)
+{
+ 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::Box* magBox = new Gtk::Box(Gtk::ORIENTATION_VERTICAL);
+
+ 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::Box *verts = new Gtk::Box(Gtk::ORIENTATION_VERTICAL);
+ Gtk::Box *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 );
+
+ auto pb = Gdk::Pixbuf::create_from_data(pixMem[i], Gdk::COLORSPACE_RGB, true, 8, sizes[i], sizes[i], stride);
+ images[i] = Gtk::make_managed<Gtk::Image>(pb);
+ 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::Box(Gtk::ORIENTATION_HORIZONTAL));
+ 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 );
+
+ pack_start(iconBox, Gtk::PACK_SHRINK);
+
+ show_all_children();
+
+ refreshPreview();
+}
+
+IconPreviewPanel::~IconPreviewPanel()
+{
+ removeDrawing();
+ if (timer) {
+ timer->stop();
+ delete timer;
+ timer = nullptr;
+ }
+ if ( renderTimer ) {
+ renderTimer->stop();
+ delete renderTimer;
+ renderTimer = nullptr;
+ }
+
+ docModConn.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::selectionModified(Selection *selection, guint flags)
+{
+ if (getDesktop() && Inkscape::Preferences::get()->getBool("/iconpreview/autoRefresh", true)) {
+ queueRefresh();
+ }
+}
+
+void IconPreviewPanel::documentReplaced()
+{
+ removeDrawing();
+ drawing_doc = getDocument();
+ if (drawing_doc) {
+ drawing = new Inkscape::Drawing();
+ visionkey = SPItem::display_key_new(1);
+ drawing->setRoot(drawing_doc->getRoot()->invoke_show(*drawing, visionkey, SP_ITEM_SHOW_DISPLAY));
+ docDesConn = drawing_doc->connectDestroy([=]() { removeDrawing(); });
+ queueRefresh();
+ }
+}
+
+/// Safely delete the Inkscape::Drawing and references to it.
+void IconPreviewPanel::removeDrawing()
+{
+ docDesConn.disconnect();
+ if (!drawing) {
+ return;
+ }
+ drawing_doc->getRoot()->invoke_hide(visionkey);
+ delete drawing;
+ drawing = nullptr;
+ drawing_doc = nullptr;
+}
+
+void IconPreviewPanel::refreshPreview()
+{
+ auto document = getDocument();
+ 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 (document) {
+#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()) ? document->getObjectById( targetId.c_str() ) : nullptr;
+ if ( !target ) {
+ targetId.clear();
+ if (auto selection = getSelection()) {
+ for (auto item : selection->items()) {
+ if (gchar const *id = item->getId()) {
+ targetId = id;
+ target = item;
+ }
+ }
+ }
+ }
+ } else {
+ target = getDesktop()->getDocument()->getRoot();
+ }
+ 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 && is<SPItem>(object)) {
+ auto item = cast<SPItem>(object);
+ // Find bbox in document
+ Geom::OptRect dbox = item->documentVisualBounds();
+
+ if ( object->parent == nullptr )
+ {
+ dbox = *(doc->preferredBounds());
+ }
+
+ /* 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());
+
+ auto bg = doc->getPageManager().getDefaultBackgroundColor();
+
+ cairo_t *cr = cairo_create(s);
+ cairo_set_source_rgba(cr, bg[0], bg[1], bg[2], bg[3]);
+ 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..e65229b
--- /dev/null
+++ b/src/ui/dialog/icon-preview.h
@@ -0,0 +1,109 @@
+// 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/image.h>
+#include <gtkmm/label.h>
+#include <gtkmm/paned.h>
+#include <gtkmm/togglebutton.h>
+#include <gtkmm/toggletoolbutton.h>
+
+#include "ui/dialog/dialog-base.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 DialogBase
+{
+public:
+ IconPreviewPanel();
+ ~IconPreviewPanel() override;
+
+ void selectionModified(Selection *selection, guint flags) override;
+ void documentReplaced() override;
+
+ void refreshPreview();
+ void modeToggled();
+
+private:
+ Drawing *drawing;
+ SPDocument *drawing_doc;
+ unsigned int visionkey;
+ Glib::Timer *timer;
+ Glib::Timer *renderTimer;
+ bool pending;
+ gdouble minDelay;
+
+ Gtk::Box 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 docModConn;
+ sigc::connection docDesConn;
+
+ void setDocument( SPDocument *document );
+ void removeDrawing();
+ 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..d18aaaf
--- /dev/null
+++ b/src/ui/dialog/inkscape-preferences.cpp
@@ -0,0 +1,3886 @@
+// 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.
+ */
+
+#include <glibmm/ustring.h>
+#include <gtkmm/dialog.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/fontbutton.h>
+#include <gtkmm/fontchooserdialog.h>
+#include <gtkmm/widget.h>
+#ifdef HAVE_CONFIG_H
+# include "config.h" // only include where actually required!
+#endif
+
+#include <gtkmm/box.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/image.h>
+#include <gtkmm/label.h>
+#include <gtkmm/object.h>
+#include <gtkmm/togglebutton.h>
+#include "ui/widget/color-scales.h"
+
+#include "inkscape-preferences.h"
+
+#include <fstream>
+#include <strings.h>
+#include <sstream>
+#include <iomanip>
+#include <algorithm>
+
+#include <gio/gio.h>
+#include <gtk/gtk.h>
+#include <glibmm/i18n.h>
+#include <gtkmm.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 "style.h"
+
+#include "display/control/canvas-item-grid.h"
+#include "display/nr-filter-gaussian.h"
+
+#include "extension/internal/gdkpixbuf-input.h"
+
+#include "io/resource.h"
+
+#include "object/color-profile.h"
+
+#include "svg/svg-color.h"
+
+#include "ui/desktop/menubar.h"
+#include "ui/interface.h"
+#include "ui/shortcuts.h"
+#include "ui/modifiers.h"
+#include "ui/util.h"
+#include "ui/widget/style-swatch.h"
+#include "ui/widget/canvas.h"
+#include "ui/themes.h"
+#include "ui/builder-utils.h"
+
+#include "util/trim.h"
+#include "util/recently-used-fonts.h"
+
+#include "widgets/desktop-widget.h"
+#include "widgets/toolbox.h"
+#include "widgets/spw-utilities.h"
+
+#include <gtkmm/accelgroup.h>
+
+#if WITH_GSPELL
+# include "ui/dialog/spellcheck.h" // for get_available_langs
+# ifdef _WIN32
+# include <windows.h>
+# endif
+#endif
+
+#if WITH_GSOURCEVIEW
+# include <gtksourceview/gtksource.h>
+#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::PrefItem;
+using Inkscape::UI::Widget::PrefRadioButtons;
+using Inkscape::UI::Widget::PrefSpinButton;
+using Inkscape::UI::Widget::StyleSwatch;
+using Inkscape::CMSSystem;
+using Inkscape::IO::Resource::get_filename;
+using Inkscape::IO::Resource::UIS;
+
+std::function<Gtk::Image*()> reset_icon = []() {
+ auto image = Gtk::make_managed<Gtk::Image>();
+ image->set_from_icon_name("reset", Gtk::ICON_SIZE_BUTTON);
+ image->set_opacity(0.6);
+ image->set_tooltip_text(_("Requires restart to take effect"));
+ return image;
+};
+
+/**
+ * Case-insensitive and unicode normalized search of `pattern` in `string`.
+ *
+ * @param pattern Text to find
+ * @param string Text to search in
+ * @param[out] score Match score between 0.0 (not found) and 1.0 (pattern == string)
+ * @return True if `pattern` is a substring of `string`.
+ */
+static bool fuzzy_search(Glib::ustring const &pattern, Glib::ustring const &string, float &score)
+{
+ Glib::ustring norm_patt = pattern.lowercase().normalize();
+ Glib::ustring norm_str = string.lowercase().normalize();
+ bool found = (norm_str.find(norm_patt) != Glib::ustring::npos);
+ score = found ? (float)pattern.size() / (float)string.size() : 0;
+ return score > 0.0 ? true : false;
+}
+
+/**
+ * Case-insensitive and unicode normalized search of `pattern` in `string`.
+ *
+ * @param pattern Text to find
+ * @param string Text to search in
+ * @return True if `pattern` is a substring of `string`.
+ */
+static bool fuzzy_search(Glib::ustring const &pattern, Glib::ustring const &string)
+{
+ float score;
+ return fuzzy_search(pattern, string, score);
+}
+
+/**
+ * Get number of child Labels that match a key in a widget
+ *
+ * @param key Text to find
+ * @param widget Gtk::Widget to search in
+ * @return Number of matches found
+ */
+static int get_num_matches(Glib::ustring const &key, Gtk::Widget *widget)
+{
+ int matches = 0;
+ if (auto label = dynamic_cast<Gtk::Label *>(widget)) {
+ if (fuzzy_search(key, label->get_text().lowercase())) {
+ // set score
+ ++matches;
+ }
+ }
+ std::vector<Gtk::Widget *> children;
+ if (auto container = dynamic_cast<Gtk::Container *>(widget)) {
+ children = container->get_children();
+ } else {
+ children = widget->list_mnemonic_labels();
+ }
+ for (auto *child : children) {
+ if (int child_matches = get_num_matches(key, child)) {
+ matches += child_matches;
+ }
+ }
+ return matches;
+}
+
+// Shortcuts model =============
+
+class ModelColumns: public Gtk::TreeModel::ColumnRecord {
+public:
+ ModelColumns() {
+ add(name);
+ add(id);
+ add(shortcut);
+ add(description);
+ add(shortcutkey);
+ 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<Gtk::AccelKey> shortcutkey;
+ Gtk::TreeModelColumn<unsigned int> user_set;
+};
+ModelColumns _kb_columns;
+static ModelColumns& onKBGetCols() {
+ return _kb_columns;
+}
+
+/**
+ * Add CSS-based highlight-class and pango highlight to a Gtk::Label
+ *
+ * @param label Label to add highlight to
+ * @param key Text to add pango highlight
+ */
+void InkscapePreferences::add_highlight(Gtk::Label *label, Glib::ustring const &key)
+{
+ Glib::ustring text = label->get_text();
+ Glib::ustring const n_text = text.lowercase().normalize();
+ Glib::ustring const n_key = key.lowercase().normalize();
+ label->get_style_context()->add_class("highlight");
+ auto const pos = n_text.find(n_key);
+ auto const len = n_key.size();
+ text = Glib::Markup::escape_text(text.substr(0, pos)) + "<span weight=\"bold\" underline=\"single\">" +
+ Glib::Markup::escape_text(text.substr(pos, len)) + "</span>" +
+ Glib::Markup::escape_text(text.substr(pos + len));
+ label->set_markup(text);
+}
+
+/**
+ * Remove CSS-based highlight-class and pango highlight from a Gtk::Label
+ *
+ * @param label Label to remove highlight from
+ * @param key Text to remove pango highlight from
+ */
+void InkscapePreferences::remove_highlight(Gtk::Label *label)
+{
+ if (label->get_use_markup()) {
+ Glib::ustring text = label->get_text();
+ label->set_text(text);
+ label->get_style_context()->remove_class("highlight");
+ }
+}
+
+InkscapePreferences::InkscapePreferences()
+ : DialogBase("/dialogs/preferences", "Preferences"),
+ _minimum_width(0),
+ _minimum_height(0),
+ _natural_width(900),
+ _natural_height(700),
+ _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);
+ add(*sb);
+ show_all_children();
+ Gtk::Requisition sreq;
+ Gtk::Requisition sreq_natural;
+ sb->get_preferred_size(sreq_natural, sreq);
+ _sb_width = sreq.width;
+ 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);
+ add(*hbox_list_page);
+
+ //Pagelist
+ auto list_box = Gtk::manage(new Gtk::Box(Gtk::Orientation::ORIENTATION_VERTICAL, 3));
+ Gtk::ScrolledWindow* scrolled_window = Gtk::manage(new Gtk::ScrolledWindow());
+ _search.set_valign(Gtk::Align::ALIGN_START);
+ list_box->pack_start(_search, false, true, 0);
+ list_box->pack_start(*scrolled_window, false, true, 0);
+ hbox_list_page->pack_start(*list_box, false, true, 0);
+ _page_list.set_headers_visible(false);
+ scrolled_window->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
+ scrolled_window->set_valign(Gtk::Align::ALIGN_FILL);
+ scrolled_window->set_propagate_natural_width();
+ scrolled_window->set_propagate_natural_height();
+ scrolled_window->add(_page_list);
+ scrolled_window->set_vexpand_set(true);
+ scrolled_window->set_vexpand(true);
+ scrolled_window->set_shadow_type(Gtk::SHADOW_IN);
+ _page_list_model = Gtk::TreeStore::create(_page_list_columns);
+ _page_list_model_filter = Gtk::TreeModelFilter::create(_page_list_model);
+ _page_list_model_sort = Gtk::TreeModelSort::create(_page_list_model_filter);
+ _page_list_model_sort->set_sort_column(_page_list_columns._col_name, Gtk::SortType::SORT_ASCENDING);
+ _page_list.set_enable_search(false);
+ _page_list.set_model(_page_list_model_sort);
+ _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);
+
+ // Search
+ _page_list.set_search_column(-1); // this disables pop-up search!
+ _search.signal_search_changed().connect(sigc::mem_fun(*this, &InkscapePreferences::on_search_changed));
+ _search.set_tooltip_text("Search");
+ _page_list_model_sort->set_sort_func(
+ _page_list_columns._col_name, [=](Gtk::TreeModel::iterator const &a, Gtk::TreeModel::iterator const &b) -> int {
+ float score_a, score_b;
+ Glib::ustring key = _search.get_text().lowercase();
+ if (key == "") {
+ return -1;
+ }
+ Glib::ustring label_a = a->get_value(_page_list_columns._col_name).lowercase();
+ Glib::ustring label_b = b->get_value(_page_list_columns._col_name).lowercase();
+ auto *grid_a = a->get_value(_page_list_columns._col_page);
+ auto *grid_b = b->get_value(_page_list_columns._col_page);
+ int num_res_a = num_widgets_in_grid(key, grid_a);
+ int num_res_b = num_widgets_in_grid(key, grid_b);
+ fuzzy_search(key, label_a, score_a);
+ fuzzy_search(key, label_b, score_b);
+ if (score_a > score_b) {
+ return -1;
+ } else if (score_a < score_b) {
+ return 1;
+ } else if (num_res_a >= num_res_b) {
+ return -1;
+ } else if (num_res_a < num_res_b) {
+ return 1;
+ } else {
+ return a->get_value(_page_list_columns._col_id) > b->get_value(_page_list_columns._col_id) ? -1 : 1;
+ }
+ });
+
+ _search.signal_next_match().connect([=]() {
+ if (_search_results.size() > 0) {
+ Gtk::TreeIter curr = _page_list.get_selection()->get_selected();
+ auto _page_list_selection = _page_list.get_selection();
+ auto next = get_next_result(curr);
+ if (next) {
+ _page_list.get_model()->get_iter(next);
+ _page_list.scroll_to_cell(next, *_page_list.get_column(0));
+ _page_list.set_cursor(next);
+ }
+ }
+ });
+
+ _search.signal_previous_match().connect([=]() {
+ if (_search_results.size() > 0) {
+ Gtk::TreeIter curr = _page_list.get_selection()->get_selected();
+ auto _page_list_selection = _page_list.get_selection();
+ auto prev = get_prev_result(curr);
+ if (prev) {
+ _page_list.get_model()->get_iter(prev);
+ _page_list.scroll_to_cell(prev, *_page_list.get_column(0));
+ _page_list.set_cursor(prev);
+ }
+ }
+ });
+
+ _search.signal_key_press_event().connect(sigc::mem_fun(*this, &InkscapePreferences::on_navigate_key_press));
+
+ _page_list_model_filter->set_visible_func([=](Gtk::TreeModel::const_iterator const &row) -> bool {
+ auto key_lower = _search.get_text().lowercase();
+ return recursive_filter(key_lower, row);
+ });
+
+ //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_NONE);
+ title_frame->set_shadow_type(Gtk::SHADOW_NONE);
+
+ initPageTools();
+ initPageUI();
+ initPageBehavior();
+ initPageIO();
+
+ initPageSystem();
+ initPageBitmaps();
+ initPageRendering();
+ initPageSpellcheck();
+
+ signal_map().connect(sigc::mem_fun(*this, &InkscapePreferences::showPage));
+
+ //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();
+
+ // Set Custom theme
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ _theme_oberver = prefs->createObserver("/theme/", [=]() {
+ prefs->setString("/options/boot/theme", "custom");
+ });
+}
+
+InkscapePreferences::~InkscapePreferences()
+= default;
+
+/**
+ * Get child Labels that match a key in a widget grid
+ * and add the results to global _search_results vector
+ *
+ * @param key Text to find
+ * @param widget Gtk::Widget to search in
+ */
+void InkscapePreferences::get_widgets_in_grid(Glib::ustring const &key, Gtk::Widget *widget)
+{
+ if (auto label = dynamic_cast<Gtk::Label *>(widget)) {
+ if (fuzzy_search(key, label->get_text())) {
+ _search_results.push_back(widget);
+ }
+ }
+ std::vector<Gtk::Widget *> children;
+ if (auto container = dynamic_cast<Gtk::Container *>(widget)) {
+ children = container->get_children();
+ } else {
+ children = widget->list_mnemonic_labels();
+ }
+ for (auto *child : children) {
+ get_widgets_in_grid(key, child);
+ }
+}
+
+/**
+ * Get number of child Labels that match a key in a widget grid
+ * and add the results to global _search_results vector
+ *
+ * @param key Text to find
+ * @param widget Gtk::Widget to search in
+ * @return Number of results found
+ */
+int InkscapePreferences::num_widgets_in_grid(Glib::ustring const &key, Gtk::Widget *widget)
+{
+ int results = 0;
+ if (auto label = dynamic_cast<Gtk::Label *>(widget)) {
+ float score;
+ if (fuzzy_search(key, label->get_text(), score)) {
+ ++results;
+ }
+ }
+ std::vector<Gtk::Widget *> children;
+ if (auto container = dynamic_cast<Gtk::Container *>(widget)) {
+ children = container->get_children();
+ } else {
+ children = widget->list_mnemonic_labels();
+ }
+ for (auto *child : children) {
+ int num_child = num_widgets_in_grid(key, child);
+ results += num_child;
+ }
+ return results;
+}
+
+/**
+ * Implementation of the search functionality executes each time
+ * search entry is changed
+ */
+void InkscapePreferences::on_search_changed()
+{
+ _num_results = 0;
+ if (_search_results.size() > 0) {
+ for (auto *result : _search_results) {
+ remove_highlight(static_cast<Gtk::Label *>(result));
+ }
+ _search_results.clear();
+ }
+ auto key = _search.get_text();
+ _page_list_model_filter->refilter();
+ // get first iter
+ Gtk::TreeModel::Children children = _page_list.get_model()->children();
+ Gtk::TreeModel::iterator iter = children.begin();
+
+ highlight_results(key, iter);
+ goto_first_result();
+ if (key == "") {
+ Gtk::TreeModel::Children children = _page_list.get_model()->children();
+ Gtk::TreeModel::iterator iter = children.begin();
+ _page_list.scroll_to_cell(Gtk::TreePath(iter), *_page_list.get_column(0));
+ _page_list.set_cursor(Gtk::TreePath(iter));
+ } else if (_num_results == 0 && key != "") {
+ _page_list.set_has_tooltip(false);
+ _show_all = true;
+ _page_list_model_filter->refilter();
+ _show_all = false;
+ show_not_found();
+ } else {
+ _page_list.expand_all();
+ }
+}
+
+/**
+ * Select the first row in the tree that has a result
+ */
+void InkscapePreferences::goto_first_result()
+{
+ auto key = _search.get_text();
+ if (_num_results > 0) {
+ Gtk::TreeIter curr = _page_list.get_model()->children().begin();
+ if (fuzzy_search(key, curr->get_value(_page_list_columns._col_name)) ||
+ get_num_matches(key, curr->get_value(_page_list_columns._col_page)) > 0) {
+ _page_list.scroll_to_cell(Gtk::TreePath(curr), *_page_list.get_column(0));
+ _page_list.set_cursor(Gtk::TreePath(curr));
+ } else {
+ auto next = get_next_result(curr);
+ if (next) {
+ _page_list.scroll_to_cell(next, *_page_list.get_column(0));
+ _page_list.set_cursor(next);
+ }
+ }
+ }
+}
+
+/**
+ * Look for the immediate next row in the tree that contains a search result
+ *
+ * @param iter Current iter the that is selected in the tree
+ * @param check_children Bool whether to check if the children of the iter
+ * contain search result
+ * @return Immediate next row than contains a search result
+ */
+Gtk::TreePath InkscapePreferences::get_next_result(Gtk::TreeIter &iter, bool check_children)
+{
+ auto key = _search.get_text();
+ Gtk::TreePath path = Gtk::TreePath(iter);
+ if (iter->children().begin() && check_children) { // check for search results in children
+ auto child = iter->children().begin();
+ _page_list.expand_row(path, false);
+ if (fuzzy_search(key, child->get_value(_page_list_columns._col_name)) ||
+ get_num_matches(key, child->get_value(_page_list_columns._col_page)) > 0) {
+ return Gtk::TreePath(child);
+ } else {
+ return get_next_result(child);
+ }
+ } else {
+ ++iter; // go to next row
+ if (iter) { // if row exists
+ if (fuzzy_search(key, iter->get_value(_page_list_columns._col_name)) ||
+ get_num_matches(key, iter->get_value(_page_list_columns._col_page))) {
+ path.next();
+ return path;
+ } else {
+ return get_next_result(iter);
+ }
+ } else if (path.up() && path) {
+ path.next();
+ iter = _page_list.get_model()->get_iter(path);
+ if (iter) {
+ if (fuzzy_search(key, iter->get_value(_page_list_columns._col_name)) ||
+ get_num_matches(key, iter->get_value(_page_list_columns._col_page))) {
+ return Gtk::TreePath(iter);
+ } else {
+ return get_next_result(iter);
+ }
+ } else {
+ path.up();
+ if (path) {
+ iter = _page_list.get_model()->get_iter(path);
+ return get_next_result(iter, false); // dont check for children
+ } else {
+ return Gtk::TreePath(_page_list.get_model()->children().begin());
+ }
+ }
+ }
+ }
+ assert(!iter);
+ return Gtk::TreePath();
+}
+
+/**
+ * Look for the immediate previous row in the tree that contains a search result
+ *
+ * @param iter Current iter that is selected in the tree
+ * @param check_children Bool whether to check if the children of the iter
+ * contain search result
+ * @return Immediate previous row than contains a search result
+ */
+Gtk::TreePath InkscapePreferences::get_prev_result(Gtk::TreeIter &iter, bool iterate)
+{
+ auto key = _search.get_text();
+ Gtk::TreePath path = Gtk::TreePath(iter);
+ if (iterate) {
+ --iter;
+ }
+ if (iter) {
+ if (iter->children().begin()) {
+ auto child = iter->children().end();
+ --child;
+ Gtk::TreePath path = Gtk::TreePath(iter);
+ _page_list.expand_row(path, false);
+ return get_prev_result(child, false);
+ } else if (fuzzy_search(key, iter->get_value(_page_list_columns._col_name)) ||
+ get_num_matches(key, iter->get_value(_page_list_columns._col_page))) {
+ return (Gtk::TreePath(iter));
+ } else {
+ return get_prev_result(iter);
+ }
+ } else if (path.up()) {
+ if (path) {
+ iter = _page_list.get_model()->get_iter(path);
+ if (fuzzy_search(key, iter->get_value(_page_list_columns._col_name)) ||
+ get_num_matches(key, iter->get_value(_page_list_columns._col_page))) {
+ return path;
+ } else {
+ return get_prev_result(iter);
+ }
+ } else {
+ auto lastIter = _page_list.get_model()->children().end();
+ --lastIter;
+ return get_prev_result(lastIter, false);
+ }
+ } else {
+ return Gtk::TreePath(iter);
+ }
+}
+
+/**
+ * Handle key F3 and Shift+F3 key press events to navigate to the next search
+ * result
+ *
+ * @param evt event object
+ * @return Always returns False to label the key press event as handled, this
+ * prevents the search bar from retaining focus for other keyboard event.
+ */
+bool InkscapePreferences::on_navigate_key_press(GdkEventKey *evt)
+{
+ if (evt->keyval == GDK_KEY_F3) {
+ if (_search_results.size() > 0) {
+ GdkModifierType modmask = gtk_accelerator_get_default_mod_mask();
+ if ((evt->state & modmask) == Gdk::SHIFT_MASK) {
+ Gtk::TreeIter curr = _page_list.get_selection()->get_selected();
+ auto _page_list_selection = _page_list.get_selection();
+ auto prev = get_prev_result(curr);
+ if (prev) {
+ _page_list.scroll_to_cell(prev, *_page_list.get_column(0));
+ _page_list.set_cursor(prev);
+ }
+ } else {
+ Gtk::TreeIter curr = _page_list.get_selection()->get_selected();
+ auto _page_list_selection = _page_list.get_selection();
+ auto next = get_next_result(curr);
+ if (next) {
+ _page_list.scroll_to_cell(next, *_page_list.get_column(0));
+ _page_list.set_cursor(next);
+ }
+ }
+ }
+ }
+ return false;
+}
+
+/**
+ * Add highlight to all the search results
+ *
+ * @param key Text to search
+ * @param iter pointing to the first row of the tree
+ */
+void InkscapePreferences::highlight_results(Glib::ustring const &key, Gtk::TreeModel::iterator &iter)
+{
+ Gtk::TreeModel::Path path;
+ Glib::ustring Txt;
+ while (iter) {
+ Gtk::TreeModel::Row row = *iter;
+ auto *grid = row->get_value(_page_list_columns._col_page);
+ get_widgets_in_grid(key, grid);
+ if (_search_results.size() > 0) {
+ for (auto *result : _search_results) {
+ // underline and highlight
+ add_highlight(static_cast<Gtk::Label *>(result), key);
+ }
+ }
+ if (iter->children()) {
+ auto children = iter->children();
+ auto child_iter = children.begin();
+ highlight_results(key, child_iter);
+ }
+ iter++;
+ }
+}
+
+/**
+ * Filter function for the search functionality to show only matching
+ * rows or rows with matching search resuls in the tree view
+ *
+ * @param key Text to search for
+ * @param iter iter pointing to the first row of the tree view
+ * @return True if the row is to be shown else return False
+ */
+bool InkscapePreferences::recursive_filter(Glib::ustring &key, Gtk::TreeModel::const_iterator const &iter)
+{
+ if(_show_all)
+ return true;
+ auto row_label = iter->get_value(_page_list_columns._col_name).lowercase();
+ if (key == "") {
+ return true;
+ }
+ if (fuzzy_search(key, row_label)) {
+ ++_num_results;
+ return true;
+ }
+ auto *grid = iter->get_value(_page_list_columns._col_page);
+ int matches = get_num_matches(key, grid);
+ _num_results += matches;
+ if (matches) {
+ return true;
+ }
+ auto child = iter->children().begin();
+ if (child) {
+ for (auto inner = child; inner; ++inner) {
+ if (recursive_filter(key, inner)) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+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"));
+}
+#define get_tool_action(toolname) ("win.tool-switch('" + toolname + "')")
+Glib::ustring get_tool_action_name(Glib::ustring toolname)
+{
+ auto *iapp = InkscapeApplication::instance();
+ if (iapp)
+ return iapp->get_action_extra_data().get_label_for_action(get_tool_action(toolname));
+ return "";
+}
+void InkscapePreferences::initPageTools()
+{
+ Gtk::TreeModel::iterator iter_tools = this->AddPage(_page_tools, _("Tools"), PREFS_PAGE_TOOLS);
+ this->AddPage(_page_selector, get_tool_action_name("Select"), iter_tools, PREFS_PAGE_TOOLS_SELECTOR);
+ this->AddPage(_page_node, get_tool_action_name("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, get_tool_action_name("Rect"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_RECT);
+ this->AddPage(_page_ellipse, get_tool_action_name("Arc"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_ELLIPSE);
+ this->AddPage(_page_star, get_tool_action_name("Star"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_STAR);
+ this->AddPage(_page_3dbox, get_tool_action_name("3DBox"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_3DBOX);
+ this->AddPage(_page_spiral, get_tool_action_name("Spiral"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_SPIRAL);
+
+ this->AddPage(_page_pen, get_tool_action_name("Pen"), iter_tools, PREFS_PAGE_TOOLS_PEN);
+ this->AddPage(_page_pencil, get_tool_action_name("Pencil"), iter_tools, PREFS_PAGE_TOOLS_PENCIL);
+ this->AddPage(_page_calligraphy, get_tool_action_name("Calligraphic"), iter_tools, PREFS_PAGE_TOOLS_CALLIGRAPHY);
+ this->AddPage(_page_text, get_tool_action_name("Text"), iter_tools, PREFS_PAGE_TOOLS_TEXT);
+
+ this->AddPage(_page_gradient, get_tool_action_name("Gradient"), iter_tools, PREFS_PAGE_TOOLS_GRADIENT);
+ this->AddPage(_page_dropper, get_tool_action_name("Dropper"), iter_tools, PREFS_PAGE_TOOLS_DROPPER);
+ this->AddPage(_page_paintbucket, get_tool_action_name("PaintBucket"), iter_tools, PREFS_PAGE_TOOLS_PAINTBUCKET);
+
+ this->AddPage(_page_tweak, get_tool_action_name("Tweak"), iter_tools, PREFS_PAGE_TOOLS_TWEAK);
+ this->AddPage(_page_spray, get_tool_action_name("Spray"), iter_tools, PREFS_PAGE_TOOLS_SPRAY);
+ this->AddPage(_page_eraser, get_tool_action_name("Eraser"), iter_tools, PREFS_PAGE_TOOLS_ERASER);
+ this->AddPage(_page_connector, get_tool_action_name("Connector"), iter_tools, PREFS_PAGE_TOOLS_CONNECTOR);
+#ifdef WITH_LPETOOL
+ this->AddPage(_page_lpetool, get_tool_action_name("LPETool"), iter_tools, PREFS_PAGE_TOOLS_LPETOOL);
+#endif // WITH_LPETOOL
+ this->AddPage(_page_measure, get_tool_action_name("Measure"), iter_tools, PREFS_PAGE_TOOLS_MEASURE);
+ this->AddPage(_page_zoom, get_tool_action_name("Zoom"), iter_tools, PREFS_PAGE_TOOLS_ZOOM);
+ _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_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
+ 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_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"));
+ _font_sample.init("/tools/text/font_sample", true);
+ _page_text.add_line( false, _("Font sample"), _font_sample, "", _("Change font preview sample text"), true);
+
+ 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)"));
+
+ _recently_used_fonts_size.init("/tools/text/recently_used_fonts_size", 0, 100, 1, 10, 10, true, false);
+ _page_text.add_line( false, _("Fonts in 'Recently used' collection:"), _recently_used_fonts_size, "",
+ _("Maximum number of fonts in the 'Recently used' font collection"), false);
+ _recently_used_fonts_size.changed_signal.connect([](double new_size) {
+ Inkscape::RecentlyUsedFonts* recently_used_fonts = Inkscape::RecentlyUsedFonts::get();
+ recently_used_fonts->change_max_list_size(new_size); });
+ }
+
+ //_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_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);
+
+ _misc_gradient_collect.init(_("Auto-delete unused gradients"), "/option/gradient/auto_collect", true);
+ _page_gradient.add_line(
+ false, "", _misc_gradient_collect, "",
+ _("When enabled, gradients that are not used will be deleted (auto-collected) automatically "
+ "from the SVG file. When disabled, unused gradients will be preserved in "
+ "the file for later use. (Note: This setting only affects new gradients.)"),
+ true);
+
+ //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
+}
+
+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", prefs->getString("/theme/defaultIconTheme", ""));
+ Glib::ustring prefix = "";
+ if (prefs->getBool("/theme/darkTheme", false)) {
+ prefix = ".dark ";
+ }
+ auto higlight = get_filename_string(ICONS, (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));
+ Util::trim(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));
+ Util::trim(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));
+ Util::trim(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));
+ Util::trim(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", prefs->getString("/theme/defaultIconTheme", ""));
+ if (!prefs->getBool("/theme/symbolicIcons", false)) {
+ _symbolic_base_colors.set_sensitive(false);
+ _symbolic_highlight_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/symbolicDefaultBaseColors", true) ||
+ !prefs->getEntry("/theme/" + themeiconname + "/symbolicBaseColor").isValid()) {
+ auto const screen = Gdk::Screen::get_default();
+ if (INKSCAPE.themecontext->getColorizeProvider()) {
+ Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.themecontext->getColorizeProvider());
+ }
+ // This colors are set on style.css of inkscape
+ Gdk::RGBA base_color = _symbolic_base_color.get_style_context()->get_color();
+ // This is a hack to fix a problematic style which isn't updated fast enough on
+ // change from dark to bright themes
+ if (themechange) {
+ auto sc = _symbolic_base_color.get_style_context();
+ base_color = get_background_color(sc);
+ }
+ SPColor base_color_sp(base_color.get_red(), base_color.get_green(), base_color.get_blue());
+ //we copy highlight to not use
+ guint32 colorsetbase = base_color_sp.toRGBA32(base_color.get_alpha());
+ guint32 colorsetsuccess = colorsetbase;
+ guint32 colorsetwarning = colorsetbase;
+ guint32 colorseterror = colorsetbase;
+ get_highlight_colors(colorsetbase, colorsetsuccess, colorsetwarning, colorseterror);
+ _symbolic_base_color.setRgba32(colorsetbase);
+ prefs->setUInt("/theme/" + themeiconname + "/symbolicBaseColor", colorsetbase);
+ _symbolic_base_color.setSensitive(false);
+ changeIconsColors();
+ } else {
+ _symbolic_base_color.setSensitive(true);
+ }
+ if (prefs->getBool("/theme/symbolicDefaultHighColors", true)) {
+ auto const screen = Gdk::Screen::get_default();
+ if (INKSCAPE.themecontext->getColorizeProvider()) {
+ Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.themecontext->getColorizeProvider());
+ }
+ 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 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());
+ //we copy base to not use
+ guint32 colorsetbase = success_color_sp.toRGBA32(success_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_success_color.setRgba32(colorsetsuccess);
+ _symbolic_warning_color.setRgba32(colorsetwarning);
+ _symbolic_error_color.setRgba32(colorseterror);
+ prefs->setUInt("/theme/" + themeiconname + "/symbolicSuccessColor", colorsetsuccess);
+ prefs->setUInt("/theme/" + themeiconname + "/symbolicWarningColor", colorsetwarning);
+ prefs->setUInt("/theme/" + themeiconname + "/symbolicErrorColor", colorseterror);
+ _symbolic_success_color.setSensitive(false);
+ _symbolic_warning_color.setSensitive(false);
+ _symbolic_error_color.setSensitive(false);
+ changeIconsColors();
+ } else {
+ _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", prefs->getString("/theme/defaultIconTheme", ""));
+ 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.themecontext->getColorizeProvider()) {
+ Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.themecontext->getColorizeProvider());
+ }
+ Gtk::CssProvider::create();
+ Glib::ustring css_str = "";
+ if (prefs->getBool("/theme/symbolicIcons", false)) {
+ css_str = INKSCAPE.themecontext->get_symbolic_colors();
+ }
+ try {
+ INKSCAPE.themecontext->getColorizeProvider()->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.themecontext->getColorizeProvider(),
+ 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);
+ _symbolic_highlight_colors.set_sensitive(true);
+ Glib::ustring themeiconname = prefs->getString("/theme/iconTheme", prefs->getString("/theme/defaultIconTheme", ""));
+ 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.themecontext->getColorizeProvider()) {
+ Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.themecontext->getColorizeProvider());
+ }
+ _symbolic_base_colors.set_sensitive(false);
+ _symbolic_highlight_colors.set_sensitive(false);
+ }
+ INKSCAPE.themecontext->getChangeThemeSignal().emit();
+ INKSCAPE.themecontext->add_gtk_css(true);
+}
+
+bool InkscapePreferences::contrastChange(GdkEventButton *button_event)
+{
+ themeChange();
+ return true;
+}
+
+void InkscapePreferences::comboThemeChange()
+{
+ //we reset theming on combo change
+ _dark_theme.set_active(false);
+ _symbolic_base_colors.set_active(true);
+ if (_contrast_theme.getSpinButton()->get_value() != 10.0){
+ _contrast_theme.getSpinButton()->set_value(10.0);
+ } else {
+ themeChange();
+ }
+}
+void InkscapePreferences::contrastThemeChange()
+{
+ //we reset theming on combo change
+ themeChange(true);
+}
+
+void InkscapePreferences::themeChange(bool contrastslider)
+{
+ Gtk::Window *window = SP_ACTIVE_DESKTOP->getToplevel();
+ if (window) {
+ auto const screen = Gdk::Screen::get_default();
+ if (INKSCAPE.themecontext->getContrastThemeProvider()) {
+ Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.themecontext->getContrastThemeProvider());
+ }
+ if (INKSCAPE.themecontext->getThemeProvider() ) {
+ Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.themecontext->getThemeProvider() );
+ }
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring current_theme = prefs->getString("/theme/gtkTheme", prefs->getString("/theme/defaultGtkTheme", ""));
+ _dark_theme.get_parent()->set_no_show_all(false);
+ if (dark_themes[current_theme]) {
+ _dark_theme.get_parent()->show_all();
+ } else {
+ _dark_theme.get_parent()->hide();
+ }
+ auto settings = Gtk::Settings::get_default();
+ settings->property_gtk_theme_name() = current_theme;
+ bool dark = INKSCAPE.themecontext->isCurrentThemeDark(dynamic_cast<Gtk::Container *>(window));
+ bool toggled = prefs->getBool("/theme/darkTheme", false) != dark;
+ prefs->setBool("/theme/darkTheme", dark);
+ INKSCAPE.themecontext->getChangeThemeSignal().emit();
+ INKSCAPE.themecontext->add_gtk_css(true, contrastslider);
+ resetIconsColors(toggled);
+ }
+}
+
+void InkscapePreferences::preferDarkThemeChange()
+{
+ Gtk::Window *window = SP_ACTIVE_DESKTOP->getToplevel();
+ if (window) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool dark = INKSCAPE.themecontext->isCurrentThemeDark(dynamic_cast<Gtk::Container *>(window));
+ bool toggled = prefs->getBool("/theme/darkTheme", false) != dark;
+ prefs->setBool("/theme/darkTheme", dark);
+ INKSCAPE.themecontext->getChangeThemeSignal().emit();
+ INKSCAPE.themecontext->add_gtk_css(true);
+ // we avoid switched base colors
+ if (!_symbolic_base_colors.get_active()) {
+ prefs->setBool("/theme/symbolicDefaultBaseColors", true);
+ resetIconsColors(false);
+ _symbolic_base_colors.set_sensitive(true);
+ prefs->setBool("/theme/symbolicDefaultBaseColors", false);
+ } else {
+ resetIconsColors(toggled);
+ }
+ }
+}
+
+void InkscapePreferences::symbolicThemeCheck()
+{
+ using namespace Inkscape::IO::Resource;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring themeiconname = prefs->getString("/theme/iconTheme", prefs->getString("/theme/defaultIconTheme", ""));
+ bool symbolic = false;
+ auto settings = Gtk::Settings::get_default();
+ if (settings) {
+ if (themeiconname != "") {
+ settings->property_gtk_icon_theme_name() = themeiconname;
+ }
+ }
+ // we always show symbolic in default theme (relays in hicolor theme)
+ if (themeiconname != prefs->getString("/theme/defaultIconTheme", "")) {
+
+ 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 == themeiconname) {
+#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_highlight_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_highlight_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/symbolicDefaultHighColors", true) ||
+ prefs->getBool("/theme/symbolicDefaultBaseColors", 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);
+
+ 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:"), _ui_languages, "",
+ _("Set the language for menus and number formats"), false, reset_icon());
+
+ 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);
+
+ _page_ui.add_group_header(_("_Zoom correction factor (in %)"));
+ _page_ui.add_group_note(_("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"));
+ _ui_zoom_correction.init(300, 30, 0.01, 500.0, 1.0, 10.0, 1.0);
+ _page_ui.add_line( true, "", _ui_zoom_correction, "", "", true);
+
+ _ui_realworldzoom.init( _("Show zoom percentage corrected by factor"), "/options/zoomcorrection/shown", true);
+ _page_ui.add_line( false, "", _ui_realworldzoom, "", _("Zoom percentage can be either by the physical units or by pixels."));
+
+ _ui_pageorigin.init( _("Origin always on current page"), "/options/origincorrection/page", true);
+ _page_ui.add_line( false, "", _ui_pageorigin, "", _("Rulers and tools will display position information relative to the current page, instead of the position on the canvas (corresponding to the first page's position)."));
+
+ _ui_yaxisdown.init( _("Origin at upper left with y-axis pointing down"), "/options/yaxisdown", true);
+ _page_ui.add_line( false, "", _ui_yaxisdown, "",
+ _("When off, origin is at lower left corner and y-axis points up"), false, reset_icon());
+
+ _ui_rotationlock.init(_("Lock canvas rotation by default"), "/options/rotationlock", false);
+ _page_ui.add_line(false, "", _ui_rotationlock, "",
+ _("Prevent accidental canvas rotation by disabling on-canvas keyboard and mouse actions for rotation"), true);
+
+ _ui_rulersel.init( _("Show selection in ruler"), "/options/ruler/show_bbox", true);
+ _page_ui.add_line( false, "", _ui_rulersel, "", _("Shows a blue line in the ruler where the selection is."));
+
+ _page_ui.add_group_header(_("User Interface"));
+ // _page_ui.add_group_header(_("Handle size"));
+ _mouse_grabsize.init("/options/grabsize/value", 1, 15, 1, 2, 3, 0);
+ _page_ui.add_line(true, _("Handle size"), _mouse_grabsize, "", _("Set the relative size of node handles"), true);
+ _narrow_spinbutton.init(_("Use narrow number entry boxes"), "/theme/narrowSpinButton", false);
+ _page_ui.add_line(false, "", _narrow_spinbutton, "", _("Make number editing boxes smaller by limiting padding"), false);
+
+ _page_ui.add_group_header(_("Status bar"));
+ auto sb_style = Gtk::make_managed<UI::Widget::PrefCheckButton>();
+ sb_style->init(_("Show current style"), "/statusbar/visibility/style", true);
+ _page_ui.add_line(false, "", *sb_style, "", _("Control visibility of current fill, stroke and opacity in status bar."), true);
+ auto sb_layer = Gtk::make_managed<UI::Widget::PrefCheckButton>();
+ sb_layer->init(_("Show layer selector"), "/statusbar/visibility/layer", true);
+ _page_ui.add_line(false, "", *sb_layer, "", _("Control visibility of layer selection menu in status bar."), true);
+ auto sb_coords = Gtk::make_managed<UI::Widget::PrefCheckButton>();
+ sb_coords->init(_("Show mouse coordinates"), "/statusbar/visibility/coordinates", true);
+ _page_ui.add_line(false, "", *sb_coords, "", _("Control visibility of mouse coordinates X & Y in status bar."), true);
+ auto sb_rotate = Gtk::make_managed<UI::Widget::PrefCheckButton>();
+ sb_rotate->init(_("Show canvas rotation"), "/statusbar/visibility/rotation", true);
+ _page_ui.add_line(false, "", *sb_rotate, "", _("Control visibility of canvas rotation in status bar."), true);
+
+ _page_ui.add_group_header(_("Mouse cursors"));
+ _ui_cursorscaling.init(_("Enable scaling"), "/options/cursorscaling", true);
+ _page_ui.add_line(false, "", _ui_cursorscaling, "", _("When off, cursor scaling is disabled. Cursor scaling may be broken when fractional scaling is enabled."), true);
+ _ui_cursor_shadow.init(_("Show drop shadow"), "/options/cursor-drop-shadow", true);
+ _page_ui.add_line(false, "", _ui_cursor_shadow, "", _("Control visibility of drop shadow for Inkscape cursors."), true);
+
+ // Theme
+ _page_theme.add_group_header(_("Theme"));
+ _dark_theme.init(_("Use dark theme"), "/theme/preferDarkTheme", false);
+ Glib::ustring current_theme = prefs->getString("/theme/gtkTheme", prefs->getString("/theme/defaultGtkTheme", ""));
+ Glib::ustring default_theme = prefs->getString("/theme/defaultGtkTheme");
+ Glib::ustring theme = "";
+ {
+ dark_themes = INKSCAPE.themecontext->get_available_themes();
+ std::vector<Glib::ustring> labels;
+ std::vector<Glib::ustring> values;
+ std::map<Glib::ustring, bool>::iterator it = dark_themes.begin();
+ // Iterate over the map using Iterator till end.
+ for (std::pair<std::string, int> element : dark_themes) {
+ Glib::ustring theme = element.first;
+ ++it;
+ if (theme == default_theme) {
+ continue;
+ }
+ values.emplace_back(theme);
+ labels.emplace_back(theme);
+ }
+ 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());
+ values.emplace_back("");
+ Glib::ustring default_theme_label = _("Use system theme");
+ default_theme_label += " (" + default_theme + ")";
+ labels.emplace_back(default_theme_label);
+
+ _gtk_theme.init("/theme/gtkTheme", labels, values, "");
+ _page_theme.add_line(false, _("Change GTK theme:"), _gtk_theme, "", "", false);
+ _gtk_theme.signal_changed().connect(sigc::mem_fun(*this, &InkscapePreferences::comboThemeChange));
+ }
+
+ _sys_user_themes_dir_copy.init(g_build_filename(g_get_user_data_dir(), "themes", nullptr), _("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()));
+ _contrast_theme.init("/theme/contrast", 1, 10, 1, 2, 10, 1);
+
+ _page_theme.add_line(true, "", _dark_theme, "", _("Use dark theme"), true);
+ {
+ auto font_scale = new Inkscape::UI::Widget::PrefSlider();
+ font_scale = Gtk::manage(font_scale);
+ font_scale->init(ThemeContext::get_font_scale_pref_path(), 50, 150, 5, 5, 100, 0); // 50% to 150%
+ font_scale->getSlider()->signal_format_value().connect([=](double val) {
+ return Glib::ustring::format(std::fixed, std::setprecision(0), val) + "%";
+ });
+ // Live updates commented out; too disruptive
+ // font_scale->getSlider()->signal_value_changed().connect([=](){
+ // INKSCAPE.themecontext->adjust_global_font_scale(font_scale->getSlider()->get_value() / 100.0);
+ // });
+ auto space = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL);
+ space->set_valign(Gtk::ALIGN_CENTER);
+ auto reset = Gtk::make_managed<Gtk::Button>();
+ reset->set_tooltip_text(_("Reset font size to 100%"));
+ reset->set_image_from_icon_name("reset-settings-symbolic");
+ reset->set_size_request(30, -1);
+ auto apply = Gtk::make_managed<Gtk::Button>(_("Apply"));
+ apply->set_tooltip_text(_("Apply font size changes to the UI"));
+ apply->set_valign(Gtk::ALIGN_FILL);
+ apply->set_margin_end(5);
+ reset->set_valign(Gtk::ALIGN_FILL);
+ space->add(*apply);
+ space->add(*reset);
+ reset->signal_clicked().connect([=](){
+ font_scale->getSlider()->set_value(100);
+ INKSCAPE.themecontext->adjustGlobalFontScale(1.0);
+ });
+ apply->signal_clicked().connect([=](){
+ INKSCAPE.themecontext->adjustGlobalFontScale(font_scale->getSlider()->get_value() / 100.0);
+ });
+ _page_theme.add_line(false, _("_Font scale:"), *font_scale, "", _("Adjust size of UI fonts"), true, space);
+ }
+ auto space = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL);
+ space->set_size_request(_sb_width / 3, -1);
+ _page_theme.add_line(false, _("_Contrast:"), _contrast_theme, "",
+ _("Make background brighter or darker to adjust contrast"), true, space);
+ _contrast_theme.getSlider()->signal_value_changed().connect(sigc::mem_fun(*this, &InkscapePreferences::contrastThemeChange));
+
+ if (dark_themes[current_theme]) {
+ _dark_theme.get_parent()->set_no_show_all(false);
+ _dark_theme.get_parent()->show_all();
+ } else {
+ _dark_theme.get_parent()->set_no_show_all(true);
+ _dark_theme.get_parent()->hide();
+ }
+ _dark_theme.signal_clicked().connect(sigc::mem_fun(*this, &InkscapePreferences::preferDarkThemeChange));
+ // Icons
+ _page_theme.add_group_header(_("Icons"));
+ {
+ using namespace Inkscape::IO::Resource;
+ auto folders = get_foldernames(ICONS, { "application" });
+ std::vector<Glib::ustring> labels;
+ std::vector<Glib::ustring> values;
+ Glib::ustring default_icon_theme = prefs->getString("/theme/defaultIconTheme");
+ 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 in case the 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);
+ }
+ // we want use Adwaita instead fallback hicolor theme
+ if (folder == default_icon_theme) {
+ continue;
+ }
+ labels.emplace_back(folder);
+ values.emplace_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());
+ values.emplace_back("");
+ Glib::ustring default_icon_label = _("Use system icons");
+ default_icon_label += " (" + default_icon_theme + ")";
+ labels.emplace_back(default_icon_label);
+
+ _icon_theme.init("/theme/iconTheme", labels, values, "");
+ _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", prefs->getString("/theme/defaultIconTheme", ""));
+ _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 base color for icons"), "/theme/symbolicDefaultBaseColors", true);
+ _symbolic_base_colors.signal_clicked().connect(sigc::mem_fun(*this, &InkscapePreferences::resetIconsColorsWrapper));
+ _page_theme.add_line(true, "", _symbolic_base_colors, "", "", true);
+ _symbolic_highlight_colors.init(_("Use default highlight colors for icons"), "/theme/symbolicDefaultHighColors", true);
+ _symbolic_highlight_colors.signal_clicked().connect(sigc::mem_fun(*this, &InkscapePreferences::resetIconsColorsWrapper));
+ _page_theme.add_line(true, "", _symbolic_highlight_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"),
+ _("Base color for icons"), 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, _("Icon color highlights"),
+ _("Highlight colors supported by some symbolic icon themes"),
+ 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 for some symbolic icon themes"),
+ 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 setting for the 'use-icon' attribute in the 'menus.ui' file determines whether to display icons in menus."), false, reset_icon());
+ _shift_icons.init(_("Shift icons in menus"), "/theme/shiftIcons", true);
+ _page_theme.add_line(true, "", _shift_icons, "",
+ _("This preference fixes icon positions in menus."), false, reset_icon());
+
+ _page_theme.add_group_header(_("XML Editor"));
+#if WITH_GSOURCEVIEW
+ {
+ auto manager = gtk_source_style_scheme_manager_get_default();
+ auto ids = gtk_source_style_scheme_manager_get_scheme_ids(manager);
+
+ auto syntax = Gtk::make_managed<UI::Widget::PrefCombo>();
+ std::vector<Glib::ustring> labels;
+ std::vector<Glib::ustring> values;
+ for (const char* style = *ids; style; style = *++ids) {
+ if (auto scheme = gtk_source_style_scheme_manager_get_scheme(manager, style)) {
+ auto name = gtk_source_style_scheme_get_name(scheme);
+ labels.emplace_back(name);
+ }
+ else {
+ labels.emplace_back(style);
+ }
+ values.emplace_back(style);
+ }
+ syntax->init("/theme/syntax-color-theme", labels, values, "");
+ _page_theme.add_line(false, _("Color theme:"), *syntax, "", _("Syntax coloring for XML Editor"), false);
+ }
+#endif
+ {
+ auto font_button = Gtk::make_managed<Gtk::Button>("...");
+ font_button->set_halign(Gtk::ALIGN_START);
+ auto font_box = Gtk::make_managed<Gtk::Entry>();
+ font_box->set_editable(false);
+ font_box->set_sensitive(false);
+ auto theme = INKSCAPE.themecontext;
+ font_box->set_text(theme->getMonospacedFont().to_string());
+ font_button->signal_clicked().connect([=](){
+ Gtk::FontChooserDialog dlg;
+ // show fixed-size fonts only
+ dlg.set_filter_func([](const Glib::RefPtr<const Pango::FontFamily>& family, const Glib::RefPtr<const Pango::FontFace>& face) {
+ return family && family->is_monospace();
+ });
+ dlg.set_font_desc(theme->getMonospacedFont());
+ dlg.set_position(Gtk::WIN_POS_MOUSE);
+ dlg.set_modal();
+ if (dlg.run() == Gtk::RESPONSE_OK) {
+ auto desc = dlg.get_font_desc();
+ theme->saveMonospacedFont(desc);
+ theme->adjustGlobalFontScale(theme->getFontScale() / 100);
+ font_box->set_text(desc.to_string());
+ }
+ });
+ _page_theme.add_line(false, _("Monospaced font:"), *font_box, "", _("Select fixed-width font"), true, font_button);
+
+ auto mono_font = Gtk::make_managed<UI::Widget::PrefCheckButton>();
+ mono_font->init( _("Use monospaced font"), "/dialogs/xml/mono-font", false);
+ _page_theme.add_line(false, _("XML tree:"), *mono_font, "", _("Use fixed-width font in XML Editor"), false);
+ }
+
+ //=======================================================================================================
+
+ this->AddPage(_page_theme, _("Theming"), iter_ui, PREFS_PAGE_UI_THEME);
+ symbolicThemeCheck();
+
+ // Toolbars
+ _page_toolbars.add_group_header(_("Toolbars"));
+ try {
+ auto builder = Inkscape::UI::create_builder("toolbar-tool-prefs.ui");
+ Gtk::Widget* toolbox = nullptr;
+ builder->get_widget("tool-toolbar-prefs", toolbox);
+
+ sp_traverse_widget_tree(toolbox, [=](Gtk::Widget* widget){
+ if (auto button = dynamic_cast<Gtk::ToggleButton*>(widget)) {
+ assert(GTK_IS_ACTIONABLE(widget->gobj()));
+ // do not execute any action:
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(widget->gobj()), "");
+
+ button->set_sensitive();
+ auto action_name = sp_get_action_target(button);
+ auto path = ToolboxFactory::get_tool_visible_buttons_path(action_name);
+ auto visible = Inkscape::Preferences::get()->getBool(path, true);
+ button->set_active(visible);
+ button->signal_toggled().connect([=](){
+ Inkscape::Preferences::get()->setBool(path, button->get_active());
+ });
+ auto *iapp = InkscapeApplication::instance();
+ if (iapp) {
+ auto tooltip =
+ iapp->get_action_extra_data().get_tooltip_for_action(get_tool_action(action_name), true, true);
+ button->set_tooltip_markup(tooltip);
+ }
+ }
+ return false;
+ });
+ _page_toolbars.add_line(false, "", *toolbox, "", _("Select visible tool buttons"), true);
+
+ struct tbar_info {const char* label; const char* prefs;} toolbars[] = {
+ {_("Toolbox icon size:"), ToolboxFactory::tools_icon_size},
+ {_("Control bar icon size:"), ToolboxFactory::ctrlbars_icon_size},
+ };
+ for (auto&& tbox : toolbars) {
+ auto slider = Gtk::manage(new UI::Widget::PrefSlider(false));
+ const int min = ToolboxFactory::min_pixel_size;
+ const int max = ToolboxFactory::max_pixel_size;
+ slider->init(tbox.prefs, min, max, 1, 4, min, 0);
+ slider->getSlider()->signal_format_value().connect([](double val){
+ return Glib::ustring::format(std::fixed, std::setprecision(0), val * 100.0 / min) + "%";
+ });
+ slider->getSlider()->get_style_context()->add_class("small-marks");
+ for (int i = min; i <= max; i += 8) {
+ slider->getSlider()->add_mark(i, Gtk::POS_BOTTOM, i % min ? "" : (std::to_string(100 * i / min) + "%").c_str());
+ }
+ _page_toolbars.add_line(false, tbox.label, *slider, "", _("Adjust toolbar icon size"));
+ }
+
+ std::vector<PrefItem> snap = {
+ { _("Simple"), 1, _("Present simplified snapping options that manage all advanced settings"), true },
+ { _("Advanced"), 0, _("Expose all snapping options for manual control") },
+ { _("Permanent"), 2, _("All advanced snap options appear in a permanent bar") },
+ };
+ _page_toolbars.add_line(false, _("Snap controls bar:"), *Gtk::make_managed<PrefRadioButtons>(snap, "/toolbox/simplesnap"), "", "");
+ } catch (const Glib::Error &ex) {
+ g_error("Couldn't load toolbar-tool-prefs user interface file.");
+ }
+
+ this->AddPage(_page_toolbars, _("Toolbars"), iter_ui, PREFS_PAGE_UI_TOOLBARS);
+
+ // 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_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_show_boot.init ( _("Show Welcome dialog"), "/options/boot/enabled", true);
+ _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", PREFS_DIALOGS_WINDOWS_NONE, false, nullptr);
+ _win_ontop_normal.init ( _("Normal"), "/options/transientpolicy/value", PREFS_DIALOGS_WINDOWS_NORMAL, true, &_win_ontop_none);
+ _win_ontop_agressive.init ( _("Aggressive"), "/options/transientpolicy/value", PREFS_DIALOGS_WINDOWS_AGGRESSIVE, false, &_win_ontop_none);
+
+ _win_dialogs_labels_auto.init( _("Automatic"), "/options/notebooklabels/value", PREFS_NOTEBOOK_LABELS_AUTO, true, nullptr);
+ _win_dialogs_labels_active.init( _("Active"), "/options/notebooklabels/value", PREFS_NOTEBOOK_LABELS_ACTIVE, true, nullptr);
+ _win_dialogs_labels_off.init( _("Off"), "/options/notebooklabels/value", PREFS_NOTEBOOK_LABELS_OFF, false, &_win_dialogs_labels_auto);
+
+ {
+ 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 size and position"), 4);
+ _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)"));
+
+#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
+ _page_windows.add_group_header(_("Dialogs settings"), 4);
+
+ std::vector<PrefItem> dock = {
+ { _("Docked"), PREFS_DIALOGS_BEHAVIOR_DOCKABLE, _("Allow dialog docking"), true },
+ { _("Floating"), PREFS_DIALOGS_BEHAVIOR_FLOATING, _("Disable dialog docking") }
+ };
+ _page_windows.add_line(true, _("Dialog behavior"), *Gtk::make_managed<PrefRadioButtons>(dock, "/options/dialogtype/value"), "", "", false, reset_icon());
+
+#ifndef _WIN32 // non-Win32 special code to enable transient dialogs
+ std::vector<PrefItem> on_top = {
+ { C_("Dialog on top", "None"), PREFS_DIALOGS_WINDOWS_NONE, _("Dialogs are treated as regular windows") },
+ { _("Normal"), PREFS_DIALOGS_WINDOWS_NORMAL, _("Dialogs stay on top of document windows"), true },
+ { _("Aggressive"), PREFS_DIALOGS_WINDOWS_AGGRESSIVE, _("Same as Normal but may work better with some window managers") }
+ };
+ _page_windows.add_line(true, _("Dialog on top"), *Gtk::make_managed<PrefRadioButtons>(on_top, "/options/transientpolicy/value"), "", "");
+#endif
+ std::vector<PrefItem> labels = {
+ { _("Automatic"), PREFS_NOTEBOOK_LABELS_AUTO, _("Dialog names will be displayed when there is enough space"), true },
+ { _("Active"), PREFS_NOTEBOOK_LABELS_ACTIVE, _("Only show label on active") },
+ { _("Off"), PREFS_NOTEBOOK_LABELS_OFF, _("Only show dialog icons") }
+ };
+ _page_windows.add_line(true, _("Labels behavior"), *Gtk::make_managed<PrefRadioButtons>(labels, "/options/notebooklabels/value"), "", "", false, reset_icon());
+
+ auto save_dlg = Gtk::make_managed<PrefCheckButton>();
+ save_dlg->init(_("Save and restore dialogs' status"), "/options/savedialogposition/value", true);
+ _page_windows.add_line(true, "", *save_dlg, "", _("Save and restore dialogs' status (the last open windows dialogs are saved when it closes)"));
+
+#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_group_header( _("Miscellaneous"));
+
+ _page_windows.add_line( true, "", _win_show_boot, "",
+ _("Whether the Welcome dialog will be shown when Inkscape starts."));
+ _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);
+
+ // Color pickers
+ _compact_colorselector.init(_("Use compact color selector mode switch"), "/colorselector/switcher", true);
+ _page_color_pickers.add_line(false, "", _compact_colorselector, "", _("Use compact combo box for selecting color modes"), false);
+
+ _page_color_pickers.add_group_header(_("Visible color pickers"));
+ {
+ auto container = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL);
+ auto prefs = Inkscape::Preferences::get();
+ for (auto&& picker : Inkscape::UI::Widget::get_color_pickers()) {
+ auto btn = Gtk::make_managed<Gtk::ToggleButton>();
+ auto box = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL);
+ auto label = Gtk::make_managed<Gtk::Label>(picker.label);
+ label->set_valign(Gtk::ALIGN_CENTER);
+ box->pack_start(*label);
+ box->pack_start(*Gtk::make_managed<Gtk::Image>(picker.icon, Gtk::ICON_SIZE_BUTTON));
+ box->set_spacing(3);
+ auto path = picker.visibility_path;
+ btn->set_active(prefs->getBool(path));
+ btn->add(*box);
+ btn->signal_toggled().connect([=]() {
+ prefs->setBool(path, btn->get_active());
+ auto buttons = container->get_children();
+ if (std::find_if(begin(buttons), end(buttons), [](Gtk::Widget* b) { return static_cast<Gtk::ToggleButton*>(b)->get_active(); }) == end(buttons)) {
+ // all pickers hidden; not a good combination; select first one
+ static_cast<Gtk::ToggleButton*>(buttons.front())->set_active();
+ }
+ });
+ container->pack_start(*btn);
+ }
+ container->show_all();
+ container->set_spacing(5);
+ _page_color_pickers.add_line(true, "", *container, "", _("Select color pickers"), false);
+ }
+
+ AddPage(_page_color_pickers, _("Color Selector"), iter_ui, PREFS_PAGE_UI_COLOR_PICKERS);
+ // end of Color pickers
+
+ // 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, N_("Rectangular Grid"));
+ _grids_notebook.append_page(_grids_axonom, N_("Axonometric Grid"));
+ // Rectangular SPGrid properties
+ _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(3);
+ _grids_xy_origin_y.set_digits(3);
+ _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(3);
+ _grids_xy_spacing_y.set_digits(3);
+ _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_MINOR_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_MAJOR_COLOR);
+ _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);
+
+ // Axonometric SPGrid 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(3);
+ _grids_axonom_origin_y.set_digits(3);
+ _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(3);
+ _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_MINOR_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_MAJOR_COLOR);
+ _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);
+
+ // Command Palette
+ _page_command_palette.add_group_header(_("Display Options"));
+
+ _cp_show_full_action_name.init(_("Show command line argument names"), "/options/commandpalette/showfullactionname/value", false);
+ _page_command_palette.add_line(true, "", _cp_show_full_action_name, "", _("Show action argument names in the command palette suggestions, most useful for using them on the command line"));
+
+ _cp_show_untranslated_name.init(_("Show untranslated (English) names"), "/options/commandpalette/showuntranslatedname/value", true);
+ _page_command_palette.add_line(true, "", _cp_show_untranslated_name, "", _("Also show the English names of the command"));
+
+ this->AddPage(_page_command_palette, _("Command Palette"), iter_ui, PREFS_PAGE_COMMAND_PALETTE);
+ // /Command Palette
+
+
+ initKeyboardShortcuts(iter_ui);
+}
+
+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);
+}
+
+void InkscapePreferences::initPageIO()
+{
+ Gtk::TreeModel::iterator iter_io = this->AddPage(_page_io, _("Input/Output"), PREFS_PAGE_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_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);
+
+ _export_all_extensions.init( _("Show all outputs in Export Dialog"), "/dialogs/export/show_all_extensions", false);
+ _page_io.add_line( false, "", _export_all_extensions, "",
+ _("Will list all possible output extensions in the Export Dialog selection."), 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"),
+ _("How close on the screen you need to be to an object to be able to grab it with mouse (in screen pixels)"), false, reset_icon());
+ _mouse_thres.init ( "/options/dragtolerance/value", 0.0, 100.0, 1.0, 1.0, 8.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"), "/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)"), false, reset_icon());
+
+ _mouse_switch_on_ext_input.init( _("Switch tool based on tablet device"), "/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)"), false, reset_icon());
+ 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 JavaScript code for mesh gradients"), "/options/svgexport/mesh_insertpolyfill", true );
+ _svgexport_insert_hatch_polyfill.init( _("Insert JavaScript code for SVG2 hatches"), "/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 a JavaScript polyfill for rendering meshes in web browsers."), false);
+ _page_svgexport.add_line( false, "", _svgexport_insert_hatch_polyfill, "", _("Adds a JavaScript polyfill for rendering hatches in web browsers."), 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( _("Use correct marker direction in SVG 1.1 renderers"), "/options/svgexport/marker_autostartreverse", false);
+ _svgexport_remove_marker_context_paint.init( _("Use correct marker colors in SVG 1.1 renderers"), "/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 the start of a path with 'auto_start_reverse'. This adds a rotated duplicate of the marker's definition."), false);
+ _page_svgexport.add_line( false, "", _svgexport_remove_marker_context_paint, "", _("SVG 2 allows markers to automatically match the stroke color by using 'context_paint' or 'context_fill'. This adjusts the markers own colors."), 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};
+
+ _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);
+
+ {
+ 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) );
+
+ this->AddPage(_page_cms, _("Color management"), iter_io, PREFS_PAGE_IO_CMS);
+
+ // Autosave options
+ _save_autosave_enable.init( _("Enable autosave"), "/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, 10000.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
+ _save_autosave_enable.changed_signal.connect([](bool) { Inkscape::AutoSave::restart(); });
+ _save_autosave_interval.changed_signal.connect([](double) { Inkscape::AutoSave::restart(); });
+
+ this->AddPage(_page_autosave, _("Autosave"), iter_io, PREFS_PAGE_IO_AUTOSAVE);
+
+ // No Result
+ _page_notfound.add_group_header(_("No matches were found, try another search!"));
+}
+
+void InkscapePreferences::initPageBehavior()
+{
+ Gtk::TreeModel::iterator iter_behavior = this->AddPage(_page_behavior, _("Behavior"), PREFS_PAGE_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_inlayer_same.init ( _("Select same behaves like select all"), "/options/selection/samelikeall", false);
+
+ _sel_layer_deselects.init ( _("Deselect upon layer change"), "/options/selection/layerdeselect", true);
+
+ _sel_touch_topmost_only.init ( _("Select the topmost items only when in touch selection mode"), "/options/selection/touchsel_topmost_only", true);
+ _sel_zero_opacity.init(_("Select transparent objects, strokes, and fills"), "/options/selection/zeroopacity", false);
+
+ _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_line(
+ false, "", _sel_zero_opacity, "",
+ _("Check to make objects, strokes, and fills which are completely transparent selectable even if not in outline mode"));
+
+ _page_select.add_line( false, "", _sel_touch_topmost_only, "",
+ _("In touch selection mode, if multiple items overlap at a point, select only the topmost item"));
+
+ _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)"));
+ _page_select.add_line( true, "", _sel_inlayer_same, "",
+ _("Check this to make the 'select same' functions work like the select all functions, restricting to current layer only."));
+
+ _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"));
+
+ auto paste_above_selected = Gtk::manage(new UI::Widget::PrefCheckButton());
+ paste_above_selected->init(_("Paste above selection instead of layer-top"), "/options/paste/aboveselected", true);
+ _page_select.add_line(false, "", *paste_above_selected, "",
+ _("If checked, pasted items and imported documents will be placed immediately above the "
+ "current selection (z-order). Otherwise, insertion happens on top of all objects in the current layer."));
+
+ 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_dash_scale.init(_("Scale dashes with stroke"), "/options/dash/scale", true);
+ _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_line(false, "", _trans_dash_scale, "", _("When changing stroke width, scale dash array"));
+ _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);
+
+ // 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"));
+ this->AddPage(_page_scrolling, _("Scrolling"), iter_behavior, PREFS_PAGE_BEHAVIOR_SCROLLING);
+
+ // Snapping options
+ _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);
+
+ _snap_indicator_distance.init( _("Show snap distance in case of alignment or distribution snap"), "/options/snapindicatordistance/value", false);
+ _page_snapping.add_line( true, "", _snap_indicator_distance, "",
+ _("Show snap distance in case of alignment or distribution snap"));
+
+ _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 activated, clicking the middle mouse button (usually the mouse wheel) zooms."));
+ _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"));
+ _clone_ignore_to_curves.init ( _("'Object to Path' only unlinks (keeps LPEs, shapes)"), "/options/clonestocurvesjustunlink/value", true);
+ _page_clones.add_line(true, "", _clone_ignore_to_curves, "",
+ _("'Object to path' only unlinks clones when they are converted to paths, but preserves any LPEs and shapes within the clones."));
+ //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_on_ungroup.init ( _("When ungrouping, clips/masks are preserved in children"), "/options/maskobject/maskonungroup", true);
+ _page_mask.add_line(false, "", _mask_mask_on_ungroup, "",
+ _("Uncheck this to remove clip/mask on ungroup"));
+ _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);
+
+ // Markers options
+ _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);
+
+ // Clipboard options
+ _clipboard_style_computed.init(_("Copy computed style"), "/options/copycomputedstyle/value", 1, true, nullptr);
+ _clipboard_style_verbatim.init(_("Copy class and style attributes verbatim"), "/options/copycomputedstyle/value", 0,
+ false, &_clipboard_style_computed);
+
+ _page_clipboard.add_group_header(_("Copying objects to the clipboard"));
+ _page_clipboard.add_line(true, "", _clipboard_style_computed, "",
+ _("The object's 'style' attribute will be set to the computed style, "
+ "preserving the object's appearance as in previous Inkscape versions"));
+ _page_clipboard.add_line(
+ true, "", _clipboard_style_verbatim, "",
+ _("The object's 'style' and 'class' values will be copied verbatim, and will replace those of the target object when using 'Paste style'"));
+
+ this->AddPage(_page_clipboard, _("Clipboard"), iter_behavior, PREFS_PAGE_BEHAVIOR_CLIPBOARD);
+
+ // Document cleanup options
+ _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);
+ _page_lpe.add_group_header( _("General"));
+ _lpe_show_experimental.init ( _("Show experimental effects"), "/dialogs/livepatheffect/showexperimental", false); // text label
+ _page_lpe.add_line( true, "", _lpe_show_experimental, "",
+ _("Show experimental effects")); // tooltip
+ _lpe_show_gallery.init ( _("Show deprecated LPE gallery"), "/dialogs/livepatheffect/showgallery", false); // text label
+ _page_lpe.add_line( true, "", _lpe_show_gallery, "",
+ _("Adds a button to the LPE dialog that opens the old-style LPE selection dialog")); // tooltip
+ _page_lpe.add_group_header( _("Tiling"));
+ _lpe_copy_mirroricons.init ( _("Add advanced tiling options"), "/live_effects/copy/mirroricons", true); // text label
+ _page_lpe.add_line( true, "", _lpe_copy_mirroricons, "",
+ _("Enables using 16 advanced mirror options between the copies (so there can be copies that are mirrored differently between the rows and the columns) for Tiling LPE")); // tooltip
+ this->AddPage(_page_lpe, _("Live Path Effects (LPE)"), iter_behavior, PREFS_PAGE_BEHAVIOR_LPE);
+}
+
+void InkscapePreferences::initPageRendering()
+{
+ // render threads
+ _filter_multi_threaded.init("/options/threading/numthreads", 0.0, 32.0, 1.0, 2.0, 0.0, true, false);
+ _page_rendering.add_line(false, _("Number of _Threads:"), _filter_multi_threaded, "", _("Configure number of threads to use when rendering. The default value of zero means choose automatically."), 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 x-ray 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, _("X-ray radius:"), _rendering_xray_radius, "", _("Radius of the circular area around the mouse cursor in X-ray mode"), false);
+
+ // rendering outline overlay opacity
+ _rendering_outline_overlay_opacity.init("/options/rendering/outline-overlay-opacity", 0.0, 100.0, 1.0, 5.0, 50.0, true, false);
+ _page_rendering.add_line( false, _("Outline overlay opacity:"), _rendering_outline_overlay_opacity, _("%"), _("Opacity of the overlay in outline overlay view mode"), false);
+
+ // update strategy
+ {
+ constexpr int values[] = { 1, 2, 3 };
+ Glib::ustring const labels[] = { _("Responsive"), _("Full redraw"), _("Multiscale") };
+ _canvas_update_strategy.init("/options/rendering/update_strategy", labels, values, 3, 3);
+ _page_rendering.add_line(false, _("Update strategy:"), _canvas_update_strategy, "", _("How to update continually changing content when it can't be redrawn fast enough"), false);
+ }
+
+ // opengl
+ _canvas_request_opengl.init(_("Enable OpenGL"), "/options/rendering/request_opengl", false);
+ _page_rendering.add_line(false, "", _canvas_request_opengl, "", _("Request that the canvas should be painted with OpenGL rather than Cairo. If OpenGL is unsupported, it will fall back to Cairo."), 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"));
+
+#ifdef CAIRO_HAS_DITHER
+ _cairo_dithering.init(_("Use dithering"), "/options/dithering/value", true);
+ _page_rendering.add_line(false, "", _cairo_dithering, "", _("Makes gradients smoother. This can significantly impact the size of generated PNG files."));
+#endif
+
+ auto grid = Gtk::make_managed<Gtk::Grid>();
+ grid->set_border_width(12);
+ grid->set_orientation(Gtk::ORIENTATION_VERTICAL);
+ grid->set_column_spacing(12);
+ grid->set_row_spacing(6);
+ auto revealer = Gtk::make_managed<Gtk::Revealer>();
+ revealer->add(*grid);
+ revealer->set_reveal_child(Inkscape::Preferences::get()->getBool("/options/rendering/devmode"));
+ _canvas_developer_mode_enabled.init(_("Enable developer mode"), "/options/rendering/devmode", false);
+ _canvas_developer_mode_enabled.signal_toggled().connect([revealer, this] { revealer->set_reveal_child(_canvas_developer_mode_enabled.get_active()); });
+ _page_rendering.add_group_header(_("Developer mode"));
+ _page_rendering.add_line(true, "", _canvas_developer_mode_enabled, "", _("Enable additional debugging options"), false);
+ _page_rendering.add(*revealer);
+
+ auto add_devmode_line = [&] (Glib::ustring const &label, Gtk::Widget &widget, Glib::ustring const &suffix, Glib::ustring const &tip) {
+ widget.set_tooltip_text(tip);
+
+ auto hb = Gtk::make_managed<Gtk::Box>();
+ hb->set_spacing(12);
+ hb->set_hexpand(true);
+ hb->pack_start(widget, false, false);
+ hb->set_valign(Gtk::ALIGN_CENTER);
+
+ auto label_widget = Gtk::make_managed<Gtk::Label>(label, Gtk::ALIGN_START, Gtk::ALIGN_CENTER, true);
+ label_widget->set_mnemonic_widget(widget);
+ label_widget->set_markup(label_widget->get_text());
+ label_widget->set_margin_start(12);
+
+ label_widget->set_valign(Gtk::ALIGN_CENTER);
+ grid->add(*label_widget);
+ grid->attach_next_to(*hb, *label_widget, Gtk::POS_RIGHT, 1, 1);
+
+ if (!suffix.empty()) {
+ auto suffix_widget = Gtk::make_managed<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);
+ }
+ };
+
+ auto add_devmode_group_header = [&] (Glib::ustring const &name) {
+ auto label_widget = Gtk::make_managed<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);
+ grid->add(*label_widget);
+ };
+
+ //TRANSLATORS: The following are options for fine-tuning rendering, meant to be used by developers,
+ //find more explanations at https://gitlab.com/inkscape/inbox/-/issues/6544#note_886540227
+ add_devmode_group_header(_("Low-level tuning options"));
+ _canvas_tile_size.init("/options/rendering/tile_size", 1.0, 10000.0, 1.0, 0.0, 300.0, true, false);
+ add_devmode_line(_("Tile size"), _canvas_tile_size, "", _("Halve rendering tile rectangles until their largest dimension is this small"));
+ _canvas_render_time_limit.init("/options/rendering/render_time_limit", 1.0, 5000.0, 1.0, 0.0, 80.0, true, false);
+ add_devmode_line(_("Render time limit"), _canvas_render_time_limit, C_("millisecond abbreviation", "ms"), _("The maximum time allowed for a rendering time slice"));
+ _canvas_block_updates.init("", "/options/rendering/block_updates", true);
+ add_devmode_line(_("Use block updates"), _canvas_block_updates, "", _("Update the dragged region as a single block"));
+ {
+ constexpr int values[] = { 1, 2, 3, 4 };
+ Glib::ustring const labels[] = { _("Auto"), _("Persistent"), _("Asynchronous"), _("Synchronous") };
+ _canvas_pixelstreamer_method.init("/options/rendering/pixelstreamer_method", labels, values, 4, 1);
+ add_devmode_line(_("Pixel streaming method"), _canvas_pixelstreamer_method, "", _("Change the method used for streaming pixel data to the GPU. The default is Auto, which picks the best method available at runtime. As for the other options, higher up is better."));
+ }
+ _canvas_padding.init("/options/rendering/padding", 0.0, 1000.0, 1.0, 0.0, 350.0, true, false);
+ add_devmode_line(_("Buffer padding"), _canvas_padding, C_("pixel abbreviation", "px"), _("Use buffers bigger than the window by this amount"));
+ _canvas_prerender.init("/options/rendering/prerender", 0.0, 1000.0, 1.0, 0.0, 100.0, true, false);
+ add_devmode_line(_("Prerender margin"), _canvas_prerender, "", _("Pre-render a margin around the visible region."));
+ _canvas_preempt.init("/options/rendering/preempt", 0.0, 1000.0, 1.0, 0.0, 250.0, true, false);
+ add_devmode_line(_("Preempt size"), _canvas_preempt, "", _("Prevent thin tiles at the rendering edge by making them at least this size."));
+ _canvas_coarsener_min_size.init("/options/rendering/coarsener_min_size", 0.0, 1000.0, 1.0, 0.0, 200.0, true, false);
+ add_devmode_line(_("Min size for coarsener algorithm"), _canvas_coarsener_min_size, C_("pixel abbreviation", "px"), _("Coarsener algorithm only processes rectangles smaller/thinner than this."));
+ _canvas_coarsener_glue_size.init("/options/rendering/coarsener_glue_size", 0.0, 1000.0, 1.0, 0.0, 80.0, true, false);
+ add_devmode_line(_("Glue size for coarsener algorithm"), _canvas_coarsener_glue_size, C_("pixel abbreviation", "px"), _("Coarsener algorithm absorbs nearby rectangles within this distance."));
+ _canvas_coarsener_min_fullness.init("/options/rendering/coarsener_min_fullness", 0.0, 1.0, 0.0, 0.0, 0.3, false, false);
+ add_devmode_line(_("Min fullness for coarsener algorithm"), _canvas_coarsener_min_fullness, "", _("Refuse coarsening algorithm's attempt if the result would be more empty than this."));
+
+ add_devmode_group_header(_("Debugging, profiling and experiments"));
+ _canvas_debug_framecheck.init("", "/options/rendering/debug_framecheck", false);
+ add_devmode_line(_("Framecheck"), _canvas_debug_framecheck, "", _("Print profiling data of selected operations to a file"));
+ _canvas_debug_logging.init("", "/options/rendering/debug_logging", false);
+ add_devmode_line(_("Logging"), _canvas_debug_logging, "", _("Log certain events to the console"));
+ _canvas_debug_delay_redraw.init("", "/options/rendering/debug_delay_redraw", false);
+ add_devmode_line(_("Delay redraw"), _canvas_debug_delay_redraw, "", _("Introduce a fixed delay for each tile"));
+ _canvas_debug_delay_redraw_time.init("/options/rendering/debug_delay_redraw_time", 0.0, 1000000.0, 1.0, 0.0, 50.0, true, false);
+ add_devmode_line(_("Delay redraw time"), _canvas_debug_delay_redraw_time, C_("microsecond abbreviation", "μs"), _("The delay to introduce for each tile"));
+ _canvas_debug_show_redraw.init("", "/options/rendering/debug_show_redraw", false);
+ add_devmode_line(_("Show redraw"), _canvas_debug_show_redraw, "", _("Paint a translucent random colour over each newly drawn tile"));
+ _canvas_debug_show_unclean.init("", "/options/rendering/debug_show_unclean", false);
+ add_devmode_line(_("Show unclean region"), _canvas_debug_show_unclean, "", _("Show the region that needs to be redrawn in red (only in Cairo mode)"));
+ _canvas_debug_show_snapshot.init("", "/options/rendering/debug_show_snapshot", false);
+ add_devmode_line(_("Show snapshot region"), _canvas_debug_show_snapshot, "", _("Show the region that still contains a saved copy of previously rendered content in blue (only in Cairo mode)"));
+ _canvas_debug_show_clean.init("", "/options/rendering/debug_show_clean", false);
+ add_devmode_line(_("Show clean region's fragmentation"), _canvas_debug_show_clean, "", _("Show the outlines of the rectangles in the region where rendering is complete in green (only in Cairo mode)"));
+ _canvas_debug_disable_redraw.init("", "/options/rendering/debug_disable_redraw", false);
+ add_devmode_line(_("Disable redraw"), _canvas_debug_disable_redraw, "", _("Temporarily disable the idle redraw process completely"));
+ _canvas_debug_sticky_decoupled.init("", "/options/rendering/debug_sticky_decoupled", false);
+ add_devmode_line(_("Sticky decoupled mode"), _canvas_debug_sticky_decoupled, "", _("Stay in decoupled mode even after rendering is complete"));
+ _canvas_debug_animate.init("", "/options/rendering/debug_animate", false);
+ add_devmode_line(_("Animate"), _canvas_debug_animate, "", _("Continuously adjust viewing parameters in an animation loop."));
+
+ 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."));
+
+ _svgoutput_usesodipodiabsref.init(_("Store absolute file path for linked images"),
+ "/options/svgoutput/usesodipodiabsref", false);
+ _page_bitmaps.add_line(
+ true, "", _svgoutput_usesodipodiabsref, "",
+ _("By default, image links are stored as relative paths whenever possible. If this option is enabled, Inkscape "
+ "will additionally add an absolute path ('sodipodi:absref' attribute) to the image. This is used as a "
+ "fall-back for locating the linked image, for example if the SVG document has been moved on disk. Note that this "
+ "will expose your directory structure in the file's source code, which can include personal information like your username."),
+ false);
+
+ {
+ 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"), _("Pages"), _("Embed"), _("Link"), _("New")};
+ Glib::ustring values[] = {"include", "pages", "embed", "link", "new"};
+ _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)
+{
+ // ------- Shortcut file --------
+ auto labels_and_names = Inkscape::Shortcuts::get_file_names();
+ _kb_filelist.init( "/options/kbshortcuts/shortcutfile", labels_and_names, labels_and_names[0].second);
+
+ auto tooltip =
+ Glib::ustring::compose(_("Select a file of predefined shortcuts and modifiers to use. Any customizations you "
+ "create will be added separately to %1"),
+ IO::Resource::get_path_string(IO::Resource::USER, IO::Resource::KEYS, "default.xml"));
+
+ _page_keyshortcuts.add_line( false, _("Keyboard file:"), _kb_filelist, "", tooltip.c_str(), false);
+
+
+ // ---------- Tree --------
+ _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));
+
+ // Name
+ _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);
+
+ // Shortcut
+ _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));
+
+ // Description
+ auto desc_renderer = dynamic_cast<Gtk::CellRendererText*>(_kb_tree.get_column_cell_renderer(2));
+ desc_renderer->property_wrap_mode() = Pango::WRAP_WORD;
+ desc_renderer->property_wrap_width() = 600;
+ _kb_tree.get_column(2)->set_resizable(true);
+ _kb_tree.get_column(2)->set_clickable(true);
+ _kb_tree.get_column(2)->set_expand(true);
+
+ // ID
+ _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) );
+
+ _kb_notebook.append_page(_kb_page_shortcuts, _("Shortcuts"));
+ Gtk::ScrolledWindow* shortcut_scroller = new Gtk::ScrolledWindow();
+ shortcut_scroller->add(_kb_tree);
+ shortcut_scroller->set_hexpand();
+ shortcut_scroller->set_vexpand();
+ // -------- Search --------
+ _kb_search.init("/options/kbshortcuts/value", true);
+ // clear filter initially to show all shortcuts;
+ // this entry will typically be stale and long forgotten and not what user is looking for
+ _kb_search.set_text({});
+ _kb_page_shortcuts.add_line( false, _("Search:"), _kb_search, "", "", true);
+ _kb_page_shortcuts.attach(*shortcut_scroller, 0, 3, 2, 1);
+
+ _mod_store = Gtk::TreeStore::create( _mod_columns );
+ _mod_tree.set_model(_mod_store);
+ _mod_tree.append_column(_("Name"), _mod_columns.name);
+ _mod_tree.append_column("hot", _mod_columns.and_modifiers);
+ _mod_tree.append_column(_("ID"), _mod_columns.id);
+ _mod_tree.set_tooltip_column(2);
+
+ // In order to get tooltips on header, we must create our own label.
+ auto and_keys_header = Gtk::manage(new Gtk::Label(_("Modifier")));
+ and_keys_header->set_tooltip_text(_("All keys specified must be held down to activate this functionality."));
+ and_keys_header->show();
+ auto and_keys_column = _mod_tree.get_column(1);
+ and_keys_column->set_widget(*and_keys_header);
+
+ auto edit_bar = Gtk::manage(new Gtk::Box());
+ _kb_mod_ctrl.set_label("Ctrl");
+ _kb_mod_shift.set_label("Shift");
+ _kb_mod_alt.set_label("Alt");
+ _kb_mod_meta.set_label("Meta");
+ _kb_mod_enabled.set_label(_("Enabled"));
+ edit_bar->add(_kb_mod_ctrl);
+ edit_bar->add(_kb_mod_shift);
+ edit_bar->add(_kb_mod_alt);
+ edit_bar->add(_kb_mod_meta);
+ edit_bar->add(_kb_mod_enabled);
+ _kb_mod_ctrl.signal_toggled().connect(sigc::mem_fun(*this, &InkscapePreferences::on_modifier_edited));
+ _kb_mod_shift.signal_toggled().connect(sigc::mem_fun(*this, &InkscapePreferences::on_modifier_edited));
+ _kb_mod_alt.signal_toggled().connect(sigc::mem_fun(*this, &InkscapePreferences::on_modifier_edited));
+ _kb_mod_meta.signal_toggled().connect(sigc::mem_fun(*this, &InkscapePreferences::on_modifier_edited));
+ _kb_mod_enabled.signal_toggled().connect(sigc::mem_fun(*this, &InkscapePreferences::on_modifier_enabled));
+ _kb_page_modifiers.add_line(false, _("Change:"), *edit_bar, "", "", true);
+
+ _mod_tree.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &InkscapePreferences::on_modifier_selection_changed));
+ on_modifier_selection_changed();
+
+ _kb_notebook.append_page(_kb_page_modifiers, _("Modifiers"));
+ Gtk::ScrolledWindow* mod_scroller = new Gtk::ScrolledWindow();
+ mod_scroller->add(_mod_tree);
+ mod_scroller->set_hexpand();
+ mod_scroller->set_vexpand();
+ //_kb_page_modifiers.add(*mod_scroller);
+ _kb_page_modifiers.attach(*mod_scroller, 0, 1, 2, 1);
+
+ int row = 2;
+ _page_keyshortcuts.attach(_kb_notebook, 0, row, 2, 1);
+
+ row++;
+
+ // ------ Reset/Import/Export -------
+ 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"), 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.shortcutkey] = Gtk::AccelKey();
+ (*iter_group)[_kb_columns.user_set] = 0;
+
+ Gtk::TreeStore::iterator iter_mods = _mod_store->append();
+ (*iter_mods)[_mod_columns.name] = _("Loading ...");
+ (*iter_group)[_mod_columns.id] = "";
+ (*iter_group)[_mod_columns.description] = _("Unable to load keyboard modifier list.");
+ (*iter_group)[_mod_columns.and_modifiers] = "";
+}
+
+void InkscapePreferences::onKBList()
+{
+ // New file path already stored in preferences.
+ Inkscape::Shortcuts::getInstance().init();
+ onKBListKeyboardShortcuts();
+}
+
+void InkscapePreferences::onKBReset()
+{
+ Inkscape::Shortcuts::getInstance().clear_user_shortcuts();
+ onKBListKeyboardShortcuts();
+}
+
+void InkscapePreferences::onKBImport()
+{
+ if (Inkscape::Shortcuts::getInstance().import_shortcuts()) {
+ onKBListKeyboardShortcuts();
+ }
+}
+
+void InkscapePreferences::onKBExport()
+{
+ Inkscape::Shortcuts::getInstance().export_shortcuts();
+}
+
+bool InkscapePreferences::onKBSearchKeyEvent(GdkEventKey * /*event*/)
+{
+ _kb_filter->refilter();
+ auto search = _kb_search.get_text();
+ if (search.length() > 2) {
+ _kb_tree.expand_all();
+ }
+ else {
+ _kb_tree.collapse_all();
+ }
+ return FALSE;
+}
+
+void InkscapePreferences::onKBTreeCleared(const Glib::ustring& path)
+{
+ // Triggered by "Back" key.
+ Gtk::TreeModel::iterator iter = _kb_filter->get_iter(path);
+ Glib::ustring id = (*iter)[_kb_columns.id];
+ // Gtk::AccelKey current_shortcut_key = (*iter)[_kb_columns.shortcutkey];
+
+ // Remove current shortcut from user file (won't remove from other files).
+ Inkscape::Shortcuts::getInstance().remove_user_shortcut(id);
+
+ onKBListKeyboardShortcuts();
+}
+
+void InkscapePreferences::onKBTreeEdited (const Glib::ustring& path, guint accel_key, Gdk::ModifierType accel_mods, guint hardware_keycode)
+{
+ Inkscape::Shortcuts& shortcuts = Inkscape::Shortcuts::getInstance();
+
+ Gtk::TreeModel::iterator iter = _kb_filter->get_iter(path);
+
+ Glib::ustring id = (*iter)[_kb_columns.id];
+ Glib::ustring current_shortcut = (*iter)[_kb_columns.shortcut];
+ Gtk::AccelKey const current_shortcut_key = (*iter)[_kb_columns.shortcutkey];
+
+ GdkEventKey event;
+ event.keyval = accel_key;
+ event.state = accel_mods;
+ event.hardware_keycode = hardware_keycode;
+ Gtk::AccelKey const new_shortcut_key = shortcuts.get_from_event(&event, true);
+
+ if (!new_shortcut_key.is_null() &&
+ (new_shortcut_key.get_key() != current_shortcut_key.get_key() ||
+ new_shortcut_key.get_mod() != current_shortcut_key.get_mod())
+ ) {
+ // Check if there is currently an actions assigned to this shortcut; if yes ask if the shortcut should be reassigned
+ Glib::ustring action_name;
+ Glib::ustring accel = Gtk::AccelGroup::name(accel_key, accel_mods);
+ auto *app = InkscapeApplication::instance()->gtk_app();
+ std::vector<Glib::ustring> actions = app->get_actions_for_accel(accel);
+ if (!actions.empty()) {
+ action_name = actions[0];
+ }
+
+ if (!action_name.empty()) {
+ // Warn user about duplicated shortcuts.
+ Glib::ustring message =
+ Glib::ustring::compose(_("Keyboard shortcut \"%1\"\nis already assigned to \"%2\""),
+ shortcuts.get_label(new_shortcut_key), action_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;
+ }
+ }
+
+ // Add the new shortcut.
+ shortcuts.add_user_shortcut(id, new_shortcut_key);
+
+ onKBListKeyboardShortcuts();
+ }
+}
+
+static bool is_leaf_visible(const Gtk::TreeModel::const_iterator& iter, const Glib::ustring& search) {
+ 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 (name.lowercase().find(search) != name.npos
+ || shortcut.lowercase().find(search) != name.npos
+ || desc.lowercase().find(search) != name.npos
+ || id.lowercase().find(search) != name.npos) {
+ return true;
+ }
+
+ for (auto& child : iter->children()) {
+ if (is_leaf_visible(child, search)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool InkscapePreferences::onKBSearchFilter(const Gtk::TreeModel::const_iterator& iter)
+{
+ Glib::ustring search = _kb_search.get_text().lowercase();
+ if (search.empty()) {
+ return true;
+ }
+
+ return is_leaf_visible(iter, search);
+}
+
+void InkscapePreferences::onKBRealize()
+{
+ if (!_kb_shortcuts_loaded /*&& _current_page == &_page_keyshortcuts*/) {
+ _kb_shortcuts_loaded = true;
+ onKBListKeyboardShortcuts();
+ }
+}
+
+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 font-weight='bold'> " + shortcut + " </span>").c_str();
+ } else {
+ accel->property_markup() = Glib::ustring("<span> " + shortcut + " </span>").c_str();
+ }
+}
+
+void InkscapePreferences::on_modifier_selection_changed()
+{
+ _kb_is_updated = true;
+ Gtk::TreeStore::iterator iter = _mod_tree.get_selection()->get_selected();
+ auto selected = static_cast<bool>(iter);
+
+ _kb_mod_ctrl.set_sensitive(selected);
+ _kb_mod_shift.set_sensitive(selected);
+ _kb_mod_alt.set_sensitive(selected);
+ _kb_mod_meta.set_sensitive(selected);
+ _kb_mod_enabled.set_sensitive(selected);
+
+ _kb_mod_ctrl.set_active(false);
+ _kb_mod_shift.set_active(false);
+ _kb_mod_alt.set_active(false);
+ _kb_mod_meta.set_active(false);
+ _kb_mod_enabled.set_active(false);
+
+ if (selected) {
+ Glib::ustring modifier_id = (*iter)[_mod_columns.id];
+ auto modifier = Modifiers::Modifier::get(modifier_id.c_str());
+ Inkscape::Modifiers::KeyMask mask = Inkscape::Modifiers::NEVER;
+ if(modifier != nullptr) {
+ mask = modifier->get_and_mask();
+ } else {
+ _kb_mod_enabled.set_sensitive(false);
+ }
+ if(mask != Inkscape::Modifiers::NEVER) {
+ _kb_mod_enabled.set_active(true);
+ _kb_mod_ctrl.set_active(mask & Inkscape::Modifiers::CTRL);
+ _kb_mod_shift.set_active(mask & Inkscape::Modifiers::SHIFT);
+ _kb_mod_alt.set_active(mask & Inkscape::Modifiers::ALT);
+ _kb_mod_meta.set_active(mask & Inkscape::Modifiers::META);
+ } else {
+ _kb_mod_ctrl.set_sensitive(false);
+ _kb_mod_shift.set_sensitive(false);
+ _kb_mod_alt.set_sensitive(false);
+ _kb_mod_meta.set_sensitive(false);
+ }
+ }
+ _kb_is_updated = false;
+}
+
+void InkscapePreferences::on_modifier_enabled()
+{
+ auto active = _kb_mod_enabled.get_active();
+ _kb_mod_ctrl.set_sensitive(active);
+ _kb_mod_shift.set_sensitive(active);
+ _kb_mod_alt.set_sensitive(active);
+ _kb_mod_meta.set_sensitive(active);
+ on_modifier_edited();
+}
+
+void InkscapePreferences::on_modifier_edited()
+{
+ Gtk::TreeStore::iterator iter = _mod_tree.get_selection()->get_selected();
+ if (!iter || _kb_is_updated) return;
+
+ Glib::ustring modifier_id = (*iter)[_mod_columns.id];
+ auto modifier = Modifiers::Modifier::get(modifier_id.c_str());
+ if(!_kb_mod_enabled.get_active()) {
+ modifier->set_user(Inkscape::Modifiers::NEVER, Inkscape::Modifiers::NOT_SET);
+ } else {
+ Inkscape::Modifiers::KeyMask mask = 0;
+ if(_kb_mod_ctrl.get_active()) mask |= Inkscape::Modifiers::CTRL;
+ if(_kb_mod_shift.get_active()) mask |= Inkscape::Modifiers::SHIFT;
+ if(_kb_mod_alt.get_active()) mask |= Inkscape::Modifiers::ALT;
+ if(_kb_mod_meta.get_active()) mask |= Inkscape::Modifiers::META;
+ modifier->set_user(mask, Inkscape::Modifiers::NOT_SET);
+ }
+ Inkscape::Shortcuts& shortcuts = Inkscape::Shortcuts::getInstance();
+ shortcuts.write_user();
+ (*iter)[_mod_columns.and_modifiers] = modifier->get_label();
+}
+
+void InkscapePreferences::onKBListKeyboardShortcuts()
+{
+ Inkscape::Shortcuts& shortcuts = Inkscape::Shortcuts::getInstance();
+
+ // 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();
+ _mod_store->clear();
+
+ // Gio::Actions
+
+ auto iapp = InkscapeApplication::instance();
+ auto gapp = iapp->gtk_app();
+
+ // std::vector<Glib::ustring> actions = shortcuts.list_all_actions(); // All actions (app, win, doc)
+
+ // Simpler and better to get action list from extra data (contains "detailed action names").
+ InkActionExtraData& action_data = iapp->get_action_extra_data();
+ std::vector<Glib::ustring> actions = action_data.get_actions();
+
+ // Sort actions by section
+ auto action_sort =
+ [&](Glib::ustring &a, Glib::ustring &b) {
+ return action_data.get_section_for_action(a) < action_data.get_section_for_action(b);
+ };
+ std::sort (actions.begin(), actions.end(), action_sort);
+
+ Glib::ustring old_section;
+ Gtk::TreeStore::iterator iter_group;
+
+ // Fill sections
+ for (auto action : actions) {
+
+ Glib::ustring section = action_data.get_section_for_action(action);
+ if (section.empty()) section = "Misc";
+ if (section != old_section) {
+ iter_group = _kb_store->append();
+ (*iter_group)[_kb_columns.name] = section;
+ (*iter_group)[_kb_columns.shortcut] = "";
+ (*iter_group)[_kb_columns.description] = "";
+ (*iter_group)[_kb_columns.shortcutkey] = Gtk::AccelKey();
+ (*iter_group)[_kb_columns.id] = "";
+ (*iter_group)[_kb_columns.user_set] = 0;
+ old_section = section;
+ }
+
+ // Find accelerators
+ std::vector<Glib::ustring> accels = gapp->get_accels_for_action(action);
+ Glib::ustring shortcut_label;
+ for (auto accel : accels) {
+ // Convert to more user friendly notation.
+
+ // ::get_label shows key pad and numeric keys identically.
+ // TODO: Results in labels like "Numpad Alt+5"
+ if (accel.find("KP") != Glib::ustring::npos) {
+ shortcut_label += _("Numpad");
+ shortcut_label += " ";
+ }
+ unsigned int key = 0;
+ Gdk::ModifierType mod = Gdk::ModifierType(0);
+ Gtk::AccelGroup::parse(accel, key, mod);
+ shortcut_label += Gtk::AccelGroup::get_label(key, mod) + ", ";
+ }
+
+ if (shortcut_label.size() > 1) {
+ shortcut_label.erase(shortcut_label.size()-2);
+ }
+
+ // Find primary (i.e. first) shortcut.
+ Gtk::AccelKey shortcut_key;
+ if (accels.size() > 0) {
+ unsigned int key = 0;
+ Gdk::ModifierType mod = Gdk::ModifierType(0);
+ Gtk::AccelGroup::parse(accels[0], key, mod);
+ shortcut_key = Gtk::AccelKey(key, mod);
+ }
+
+ // Add the action to the group
+ Gtk::TreeStore::iterator row = _kb_store->append(iter_group->children());
+ (*row)[_kb_columns.name] = action_data.get_label_for_action(action);
+ (*row)[_kb_columns.shortcut] = shortcut_label;
+ (*row)[_kb_columns.description] = action_data.get_tooltip_for_action(action);
+ (*row)[_kb_columns.shortcutkey] = shortcut_key;
+ (*row)[_kb_columns.id] = action;
+ (*row)[_kb_columns.user_set] = shortcuts.is_user_set(action);
+
+ if (selected_id == action) {
+ 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);
+ }
+ }
+
+ std::string old_mod_group;
+ Gtk::TreeStore::iterator iter_mod_group;
+
+ // Modifiers (mouse specific keys)
+ for(auto modifier: Inkscape::Modifiers::Modifier::getList()) {
+ auto cat_name = modifier->get_category();
+ if (cat_name != old_mod_group) {
+ iter_mod_group = _mod_store->append();
+ (*iter_mod_group)[_mod_columns.name] = cat_name.empty() ? "" : _(cat_name.c_str());
+ (*iter_mod_group)[_mod_columns.id] = "";
+ (*iter_mod_group)[_mod_columns.description] = "";
+ (*iter_mod_group)[_mod_columns.and_modifiers] = "";
+ (*iter_mod_group)[_mod_columns.user_set] = 0;
+ old_mod_group = cat_name;
+ }
+
+ // Find accelerators
+ Gtk::TreeStore::iterator iter_modifier = _mod_store->append(iter_mod_group->children());
+ (*iter_modifier)[_mod_columns.name] = (modifier->get_name() && strlen(modifier->get_name())) ? _(modifier->get_name()) : "";
+ (*iter_modifier)[_mod_columns.id] = modifier->get_id();
+ (*iter_modifier)[_mod_columns.description] = (modifier->get_description() && strlen(modifier->get_description())) ? _(modifier->get_description()) : "";
+ (*iter_modifier)[_mod_columns.and_modifiers] = modifier->get_label();
+ (*iter_modifier)[_mod_columns.user_set] = modifier->is_set_user();
+ }
+
+ // 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")));
+ }
+
+ // Update all GUI text that includes shortcuts.
+ for (auto win : gapp->get_windows()) {
+ shortcuts.update_gui_text_recursive(win);
+ }
+
+ // Update all GUI text
+ std::list< SPDesktop* > listbuf;
+ // Get list of all available desktops
+ INKSCAPE.get_all_desktops(listbuf);
+
+ // Update text of each desktop to correct Shortcuts
+ for(SPDesktop *desktop: listbuf) {
+ // Caution: Checking if pointers are not NULL
+ if(desktop) {
+ InkscapeWindow *window = desktop->getInkscapeWindow();
+ if(window) {
+ SPDesktopWidget *dtw = window->get_desktop_widget();
+ if(dtw)
+ build_menu();
+ }
+ }
+ }
+}
+
+void InkscapePreferences::initPageSpellcheck()
+{
+#if WITH_GSPELL
+
+ _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
+}
+
+template <typename string_type>
+static void appendList(Glib::ustring& tmp, const std::vector<string_type> &listing)
+{
+ for (auto const & str : listing) {
+ tmp += str;
+ tmp += "\n";
+ }
+}
+
+
+void InkscapePreferences::initPageSystem()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ _sys_shared_path.init("/options/resources/sharedpath", true);
+ auto box = new Gtk::Box();
+ box->pack_start(_sys_shared_path);
+ box->set_size_request(300, -1);
+ _page_system.add_line( false, _("Shared default resources folder:"), *box, "",
+ _("A folder structured like a user's Inkscape preferences directory. This makes it possible to share a set of resources, such as extensions, fonts, icon sets, keyboard shortcuts, patterns/hatches, palettes, symbols, templates, themes and user interface definition files, between multiple users who have access to that folder (on the same computer or in the network). Requires a restart of Inkscape to work when changed."), false, reset_icon());
+ _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);
+ auto profilefolder = Inkscape::IO::Resource::profile_path();
+ _sys_user_config.init(profilefolder.c_str(), _("Open preferences folder"));
+ _page_system.add_line(true, _("User config:"), _sys_user_config, "", _("Location of users configuration"), true);
+
+ auto extensions_folder = IO::Resource::get_path_string(IO::Resource::USER, IO::Resource::EXTENSIONS);
+ _sys_user_extension_dir.init(extensions_folder,
+ _("Open extensions folder"));
+ _page_system.add_line(true, _("User extensions:"), _sys_user_extension_dir, "",
+ _("Location of the user’s extensions"), true);
+
+ _sys_user_fonts_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::FONTS, ""),
+ _("Open fonts folder"));
+ _page_system.add_line(true, _("User fonts:"), _sys_user_fonts_dir, "", _("Location of the user’s fonts"), true);
+
+ _sys_user_themes_dir.init(g_build_filename(g_get_user_data_dir(), "themes", nullptr), _("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(get_inkscape_datadir());
+ _sys_data.set_editable(false);
+ _page_system.add_line(true, _("Inkscape data:"), _sys_data, "", _("Location of Inkscape data"), true);
+
+ extensions_folder = IO::Resource::get_path_string(IO::Resource::SYSTEM, IO::Resource::EXTENSIONS);
+ _sys_extension_dir.set_text(extensions_folder);
+ _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;
+ auto system_data_dirs = Glib::get_system_data_dirs();
+ appendList(tmp, 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);
+
+ _sys_fontdirs_custom.init("/options/font/custom_fontdirs", 50);
+ _page_system.add_line(true, _("Custom Font directories"), _sys_fontdirs_custom, "", _("Load additional fonts from custom locations (one path per line)"), true);
+
+ tmp = "";
+ auto icon_theme = Gtk::IconTheme::get_default();
+ auto paths = icon_theme->get_search_path();
+ appendList( tmp, 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;
+ 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;
+}
+
+// Check if iter points to page indicated in preferences.
+bool InkscapePreferences::matchPage(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])
+ {
+ auto const path = _page_list.get_model()->get_path(*iter);
+ _page_list.expand_to_path(path);
+ _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::show_not_found()
+{
+ if (_current_page)
+ _page_frame.remove();
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ _current_page = &_page_notfound;
+ _page_title.set_markup(_("<span size='large'><b>No Results</b></span>"));
+ _page_frame.add(*_current_page);
+ _current_page->show();
+ this->show_all_children();
+ if (prefs->getInt("/dialogs/preferences/page", 0) == PREFS_PAGE_UI_THEME) {
+ symbolicThemeCheck();
+ }
+}
+
+void InkscapePreferences::show_nothing_on_page()
+{
+ _page_frame.remove();
+ _page_title.set_text("");
+}
+
+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();
+ this->show_all_children();
+ if (prefs->getInt("/dialogs/preferences/page", 0) == PREFS_PAGE_UI_THEME) {
+ symbolicThemeCheck();
+ }
+ }
+}
+
+// Show page indicated in preferences file.
+void InkscapePreferences::showPage()
+{
+ _search.set_text("");
+ _page_list.get_model()->foreach_iter(sigc::mem_fun(*this, &InkscapePreferences::matchPage));
+}
+
+} // 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..5a1d2fc
--- /dev/null
+++ b/src/ui/dialog/inkscape-preferences.h
@@ -0,0 +1,738 @@
+// 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
+
+// checking if cairo supports dithering
+#include <gtkmm/sizegroup.h>
+#ifdef WITH_PATCHED_CAIRO
+#include "3rdparty/cairo/src/cairo.h"
+#else
+#include <cairo.h>
+#endif
+
+
+
+#include <gtkmm/treerowreference.h>
+#include <iostream>
+#include <iterator>
+#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/treemodelfilter.h>
+#include <gtkmm/treemodelsort.h>
+#include <gtkmm/frame.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/textview.h>
+#include <gtkmm/searchentry.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/treemodel.h>
+#include <gtkmm/treemodelfilter.h>
+#include <glibmm/regex.h>
+
+#include "ui/dialog/dialog-base.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_TOOLBARS,
+ PREFS_PAGE_UI_WINDOWS,
+ PREFS_PAGE_UI_COLOR_PICKERS,
+ PREFS_PAGE_UI_GRIDS,
+ PREFS_PAGE_COMMAND_PALETTE,
+ PREFS_PAGE_UI_KEYBOARD_SHORTCUTS,
+ PREFS_PAGE_BEHAVIOR,
+ PREFS_PAGE_BEHAVIOR_SELECTING,
+ PREFS_PAGE_BEHAVIOR_TRANSFORMS,
+ 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_CLIPBOARD,
+ PREFS_PAGE_BEHAVIOR_CLEANUP,
+ PREFS_PAGE_BEHAVIOR_LPE,
+ 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,
+ PREFS_PAGE_NOTFOUND
+};
+
+namespace Gtk {
+class Scale;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class InkscapePreferences : public DialogBase
+{
+public:
+ InkscapePreferences();
+ ~InkscapePreferences() override;
+
+ void showPage(); // Show page indicated by "/dialogs/preferences/page".
+
+protected:
+ Gtk::Frame _page_frame;
+ Gtk::Label _page_title;
+ Gtk::TreeView _page_list;
+ Gtk::SearchEntry _search;
+ Glib::RefPtr<Gtk::TreeStore> _page_list_model;
+ Gtk::Widget *_highlighted_widget = nullptr;
+ Glib::RefPtr<Gtk::TreeModelFilter> _page_list_model_filter;
+ Glib::RefPtr<Gtk::TreeModelSort> _page_list_model_sort;
+ std::vector<Gtk::Widget *> _search_results;
+ Glib::RefPtr<Glib::Regex> _rx;
+ int _num_results = 0;
+ bool _show_all = false;
+
+ //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;
+
+ 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_notfound;
+ UI::Widget::DialogPage _page_theme;
+ UI::Widget::DialogPage _page_toolbars;
+ UI::Widget::DialogPage _page_windows;
+ UI::Widget::DialogPage _page_grids;
+ UI::Widget::DialogPage _page_command_palette;
+ UI::Widget::DialogPage _page_color_pickers;
+
+ UI::Widget::DialogPage _page_behavior;
+ UI::Widget::DialogPage _page_select;
+ UI::Widget::DialogPage _page_transforms;
+ 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_clipboard;
+ UI::Widget::DialogPage _page_cleanup;
+ UI::Widget::DialogPage _page_lpe;
+
+ 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;
+
+ 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::PrefCheckButton _snap_indicator_distance;
+
+ 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;
+
+ // Command Palette
+ UI::Widget::PrefCheckButton _cp_show_full_action_name;
+ UI::Widget::PrefCheckButton _cp_show_untranslated_name;
+
+ 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::PrefSlider _contrast_theme;
+ UI::Widget::PrefCheckButton _narrow_spinbutton;
+ UI::Widget::PrefCheckButton _compact_colorselector;
+ UI::Widget::PrefCheckButton _symbolic_icons;
+ UI::Widget::PrefCheckButton _symbolic_base_colors;
+ UI::Widget::PrefCheckButton _symbolic_highlight_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;
+ UI::Widget::PrefCheckButton _shift_icons;
+
+ 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_dialogs_labels_auto;
+ UI::Widget::PrefRadioButton _win_dialogs_labels_active;
+ UI::Widget::PrefRadioButton _win_dialogs_labels_off;
+ UI::Widget::PrefRadioButton _win_save_geom_off;
+ UI::Widget::PrefRadioButton _win_save_geom;
+ UI::Widget::PrefRadioButton _win_save_geom_prefs;
+ UI::Widget::PrefCheckButton _win_show_boot;
+ 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_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 _clone_ignore_to_curves;
+
+ UI::Widget::PrefCheckButton _mask_mask_on_top;
+ UI::Widget::PrefCheckButton _mask_mask_remove;
+ UI::Widget::PrefCheckButton _mask_mask_on_ungroup;
+ 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::PrefSpinButton _filter_multi_threaded;
+ UI::Widget::PrefSpinButton _rendering_cache_size;
+ UI::Widget::PrefSpinButton _rendering_xray_radius;
+ UI::Widget::PrefSpinButton _rendering_outline_overlay_opacity;
+ UI::Widget::PrefCombo _canvas_update_strategy;
+ UI::Widget::PrefCheckButton _canvas_request_opengl;
+ 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;
+#ifdef CAIRO_HAS_DITHER
+ UI::Widget::PrefCheckButton _cairo_dithering;
+#endif
+
+ UI::Widget::PrefCheckButton _canvas_developer_mode_enabled;
+ UI::Widget::PrefSpinButton _canvas_tile_size;
+ UI::Widget::PrefSpinButton _canvas_render_time_limit;
+ UI::Widget::PrefCheckButton _canvas_block_updates;
+ UI::Widget::PrefCombo _canvas_pixelstreamer_method;
+ UI::Widget::PrefSpinButton _canvas_padding;
+ UI::Widget::PrefSpinButton _canvas_prerender;
+ UI::Widget::PrefSpinButton _canvas_preempt;
+ UI::Widget::PrefSpinButton _canvas_coarsener_min_size;
+ UI::Widget::PrefSpinButton _canvas_coarsener_glue_size;
+ UI::Widget::PrefSpinButton _canvas_coarsener_min_fullness;
+ UI::Widget::PrefCheckButton _canvas_debug_framecheck;
+ UI::Widget::PrefCheckButton _canvas_debug_logging;
+ UI::Widget::PrefCheckButton _canvas_debug_delay_redraw;
+ UI::Widget::PrefSpinButton _canvas_debug_delay_redraw_time;
+ UI::Widget::PrefCheckButton _canvas_debug_show_redraw;
+ UI::Widget::PrefCheckButton _canvas_debug_show_unclean;
+ UI::Widget::PrefCheckButton _canvas_debug_show_snapshot;
+ UI::Widget::PrefCheckButton _canvas_debug_show_clean;
+ UI::Widget::PrefCheckButton _canvas_debug_disable_redraw;
+ UI::Widget::PrefCheckButton _canvas_debug_sticky_decoupled;
+ UI::Widget::PrefCheckButton _canvas_debug_animate;
+
+ UI::Widget::PrefCheckButton _trans_scale_stroke;
+ UI::Widget::PrefCheckButton _trans_scale_corner;
+ UI::Widget::PrefCheckButton _trans_gradient;
+ UI::Widget::PrefCheckButton _trans_pattern;
+ UI::Widget::PrefCheckButton _trans_dash_scale;
+ UI::Widget::PrefRadioButton _trans_optimized;
+ UI::Widget::PrefRadioButton _trans_preserved;
+
+ 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_inlayer_same;
+ UI::Widget::PrefCheckButton _sel_touch_topmost_only;
+ UI::Widget::PrefCheckButton _sel_layer_deselects;
+ UI::Widget::PrefCheckButton _sel_cycle;
+ UI::Widget::PrefCheckButton _sel_zero_opacity;
+
+ UI::Widget::PrefCheckButton _markers_color_stock;
+ UI::Widget::PrefCheckButton _markers_color_custom;
+ UI::Widget::PrefCheckButton _markers_color_update;
+
+ UI::Widget::PrefRadioButton _clipboard_style_computed;
+ UI::Widget::PrefRadioButton _clipboard_style_verbatim;
+
+ UI::Widget::PrefCheckButton _cleanup_swatches;
+
+ UI::Widget::PrefCheckButton _lpe_copy_mirroricons;
+ UI::Widget::PrefCheckButton _lpe_show_experimental;
+ UI::Widget::PrefCheckButton _lpe_show_gallery;
+
+ UI::Widget::PrefSpinButton _importexport_export_res;
+ UI::Widget::PrefSpinButton _importexport_import_res;
+ UI::Widget::PrefCheckButton _importexport_import_res_override;
+ UI::Widget::PrefCheckButton _rendering_image_outline;
+ UI::Widget::PrefSlider _snap_delay;
+ UI::Widget::PrefSlider _snap_weight;
+ UI::Widget::PrefSlider _snap_persistence;
+ UI::Widget::PrefEntry _font_sample;
+ 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 _export_all_extensions;
+ UI::Widget::PrefCheckButton _misc_forkvectors;
+ UI::Widget::PrefSpinButton _misc_gradientangle;
+ UI::Widget::PrefSpinButton _recently_used_fonts_size;
+ UI::Widget::PrefCheckButton _misc_gradient_collect;
+ UI::Widget::PrefCheckButton _misc_scripts;
+
+ // System page
+ 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_fonts_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;
+ UI::Widget::PrefMultiEntry _sys_fontdirs_custom;
+ UI::Widget::PrefEntryFile _sys_shared_path;
+ 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_rulersel;
+ UI::Widget::PrefCheckButton _ui_realworldzoom;
+ UI::Widget::PrefCheckButton _ui_pageorigin;
+ UI::Widget::PrefCheckButton _ui_partialdynamic;
+ UI::Widget::ZoomCorrRulerSlider _ui_zoom_correction;
+ UI::Widget::PrefCheckButton _ui_yaxisdown;
+ UI::Widget::PrefCheckButton _ui_rotationlock;
+ UI::Widget::PrefCheckButton _ui_cursorscaling;
+ UI::Widget::PrefCheckButton _ui_cursor_shadow;
+
+ //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;
+
+ 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::PrefCheckButton _svgoutput_usesodipodiabsref;
+ 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;
+
+
+ Gtk::Notebook _kb_notebook;
+ UI::Widget::DialogPage _kb_page_shortcuts;
+ UI::Widget::DialogPage _kb_page_modifiers;
+ gboolean _kb_shortcuts_loaded;
+ /*
+ * Keyboard shortcut members
+ */
+ Glib::RefPtr<Gtk::TreeStore> _kb_store;
+ Gtk::TreeView _kb_tree;
+ Gtk::CellRendererAccel _kb_shortcut_renderer;
+ Glib::RefPtr<Gtk::TreeModelFilter> _kb_filter;
+
+ /*
+ * Keyboard modifiers interface
+ */
+ class ModifierColumns: public Gtk::TreeModel::ColumnRecord {
+ public:
+ ModifierColumns() {
+ add(name);
+ add(id);
+ add(description);
+ add(and_modifiers);
+ add(user_set);
+ }
+ ~ModifierColumns() override = default;
+
+ Gtk::TreeModelColumn<Glib::ustring> name;
+ Gtk::TreeModelColumn<Glib::ustring> id;
+ Gtk::TreeModelColumn<Glib::ustring> description;
+ Gtk::TreeModelColumn<Glib::ustring> and_modifiers;
+ Gtk::TreeModelColumn<unsigned int> user_set;
+ Gtk::TreeModelColumn<unsigned int> is_enabled;
+ };
+ ModifierColumns _mod_columns;
+ Glib::RefPtr<Gtk::TreeStore> _mod_store;
+ Gtk::TreeView _mod_tree;
+ Gtk::ToggleButton _kb_mod_ctrl;
+ Gtk::ToggleButton _kb_mod_shift;
+ Gtk::ToggleButton _kb_mod_alt;
+ Gtk::ToggleButton _kb_mod_meta;
+ Gtk::CheckButton _kb_mod_enabled;
+ bool _kb_is_updated;
+
+ 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);
+ Gtk::TreePath get_next_result(Gtk::TreeIter& iter, bool check_children = true);
+ Gtk::TreePath get_prev_result(Gtk::TreeIter& iter, bool iterate = true);
+ bool matchPage(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 show_not_found();
+ void show_nothing_on_page();
+ void show_try_search();
+ void on_reset_open_recent_clicked();
+ void on_reset_prefs_clicked();
+ void on_search_changed();
+ void highlight_results(Glib::ustring const &key, Gtk::TreeModel::iterator &iter);
+ void goto_first_result();
+
+ void get_widgets_in_grid(Glib::ustring const &key, Gtk::Widget *widget);
+ int num_widgets_in_grid(Glib::ustring const &key, Gtk::Widget *widget);
+ void remove_highlight(Gtk::Label *label);
+ void add_highlight(Gtk::Label *label, Glib::ustring const &key);
+
+ bool recursive_filter(Glib::ustring &key, Gtk::TreeModel::const_iterator const &row);
+ bool on_navigate_key_press(GdkEventKey *evt);
+
+ 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);
+
+ /*
+ * 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);
+ void on_modifier_selection_changed();
+ void on_modifier_enabled();
+ void on_modifier_edited();
+
+private:
+ Gtk::TreeModel::iterator searchRows(char const* srch, Gtk::TreeModel::iterator& iter, Gtk::TreeModel::Children list_model_childern);
+ void themeChange(bool contrastslider = false);
+ void comboThemeChange();
+ void contrastThemeChange();
+ void preferDarkThemeChange();
+ bool contrastChange(GdkEventButton* button_event);
+ 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);
+
+ std::map<Glib::ustring, bool> dark_themes;
+ bool _init;
+ Inkscape::PrefObserver _theme_oberver;
+};
+
+} // 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..494843a
--- /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/frame.h"
+#include "ui/widget/scrollprotected.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"
+
+// clang-format off
+/* 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",
+" ",
+" .................... ",
+" ..++++++++++++++++++.. ",
+" .++++++++++++++++++++. ",
+" .++++++++++++++++++++. ",
+" ..++++++++++++++++++.. ",
+" .................... ",
+" "};
+// clang-format on
+
+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::Box
+ {
+ 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::Box detailsBox;
+ Gtk::Box titleFrame;
+ Gtk::Label titleLabel;
+ Inkscape::UI::Widget::Frame axisFrame;
+ Inkscape::UI::Widget::Frame keysFrame;
+ Gtk::Box axisVBox;
+ Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBoxText> modeCombo;
+ Gtk::Label modeLabel;
+ Gtk::Box 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;
+
+ Gdk::InputSource 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;
+}
+
+std::unique_ptr<InputDialog> InputDialog::create()
+{
+ return std::make_unique<InputDialogImpl>();
+}
+
+InputDialogImpl::InputDialogImpl() :
+ InputDialog(),
+ lastSourceSeen(static_cast<Gdk::InputSource>(-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()
+{
+ 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);
+ pack_start(topHolder);
+ } else {
+ 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::Box(Gtk::ORIENTATION_VERTICAL),
+ confDeviceStore(Gtk::TreeStore::create(getCols())),
+ confDeviceIter(),
+ confDeviceTree(confDeviceStore),
+ confDeviceScroller(),
+ watcher(*this),
+ useExt(_("_Use pressure-sensitive tablet (requires restart)"), true),
+ save(_("_Save"), true),
+ detailsBox(Gtk::ORIENTATION_VERTICAL, 4),
+ titleFrame(Gtk::ORIENTATION_HORIZONTAL, 4),
+ titleLabel(""),
+ axisFrame(_("Axes")),
+ keysFrame(_("Keys")),
+ modeLabel(_("Mode:")),
+ modeBox(Gtk::ORIENTATION_HORIZONTAL, 4),
+ axisVBox(Gtk::ORIENTATION_VERTICAL)
+
+{
+
+
+ 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 coordinates 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 )
+{
+ auto device = Glib::wrap(dev);
+ auto numAxes = device->get_n_axes();
+
+ 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;
+ auto devicemm = Glib::wrap(device);
+
+ auto source = devicemm->get_source();
+ const auto name = devicemm->get_name();
+
+ 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;
+
+ auto source = lastSourceSeen;
+ Glib::ustring devName = lastDevnameSeen;
+ Glib::ustring key;
+ gint hotButton = -1;
+
+ /* Code for determining which input device caused the event fails with GTK3
+ * because event->device gives a "generic" input device, not the one that
+ * actually caused the event. See snoop_extended in desktop-events.cpp
+ */
+
+ switch ( event->type ) {
+ case GDK_KEY_PRESS:
+ case GDK_KEY_RELEASE:
+ {
+ auto keyEvt = reinterpret_cast<GdkEventKey*>(event);
+ auto name = Gtk::AccelGroup::name(keyEvt->keyval, static_cast<Gdk::ModifierType>(keyEvt->state));
+ keyVal.set_label(name);
+// g_message("%d KEY state:0x%08x 0x%04x [%s]", keyEvt->type, keyEvt->state, keyEvt->keyval, name);
+ }
+ break;
+ case GDK_BUTTON_PRESS:
+ modmod = 1;
+ // fallthrough
+ case GDK_BUTTON_RELEASE:
+ {
+ auto btnEvt = reinterpret_cast<GdkEventButton*>(event);
+ auto device = Glib::wrap(btnEvt->device);
+ if (device) {
+ key = getKeyFor(btnEvt->device);
+ source = device->get_source();
+ devName = device->get_name();
+ 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);
+ }
+ auto name = Gtk::AccelGroup::name(0, static_cast<Gdk::ModifierType>(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")
+
+// );
+ }
+ break;
+ case GDK_MOTION_NOTIFY:
+ {
+ GdkEventMotion* btnMtn = reinterpret_cast<GdkEventMotion*>(event);
+ auto device = Glib::wrap(btnMtn->device);
+ if (device) {
+ key = getKeyFor(btnMtn->device);
+ source = device->get_source();
+ devName = device->get_name();
+ mapAxesValues(key, btnMtn->axes, btnMtn->device);
+ }
+ auto name = Gtk::AccelGroup::name(0, static_cast<Gdk::ModifierType>(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)
+// );
+ }
+ 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..d4e7477
--- /dev/null
+++ b/src/ui/dialog/input.h
@@ -0,0 +1,46 @@
+// 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 <memory>
+#include "ui/dialog/dialog-base.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class InputDialog : public DialogBase
+{
+public:
+ static std::unique_ptr<InputDialog> create();
+
+protected:
+ InputDialog() : DialogBase("/dialogs/inputdevices", "Input") {}
+};
+
+} // 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..048cd33
--- /dev/null
+++ b/src/ui/dialog/knot-properties.cpp
@@ -0,0 +1,189 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Dialog for moving knots. Only used by Measure Tool.
+ */
+
+/* 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 "knot-properties.h"
+
+#include <boost/lexical_cast.hpp>
+
+#include <glibmm/i18n.h>
+#include <glibmm/main.h>
+
+#include "desktop.h"
+#include "ui/knot/knot.h"
+#include "util/units.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialogs {
+
+KnotPropertiesDialog::KnotPropertiesDialog()
+ : _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() {
+}
+
+void KnotPropertiesDialog::showDialog(SPDesktop *desktop, const SPKnot *pt, Glib::ustring const unit_name)
+{
+ KnotPropertiesDialog *dialog = new KnotPropertiesDialog();
+ 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()
+{
+ 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);
+}
+
+} // 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..18f6745
--- /dev/null
+++ b/src/ui/dialog/knot-properties.h
@@ -0,0 +1,96 @@
+// 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 "ui/tools/measure-tool.h"
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialogs {
+
+// Used in Measure tool to set ends of "ruler" (via Shift-click)."
+
+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:
+
+ 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 _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..1149f04
--- /dev/null
+++ b/src/ui/dialog/layer-properties.cpp
@@ -0,0 +1,435 @@
+// 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 "preferences.h"
+#include "selection-chemistry.h"
+
+#include "ui/icon-names.h"
+#include "ui/widget/imagetoggler.h"
+#include "ui/tools/tool-base.h"
+#include "object/sp-root.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialogs {
+
+LayerPropertiesDialog::LayerPropertiesDialog(LayerPropertiesDialogType type)
+ : _type{type}
+ , _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([=]() {_close();});
+ _apply_button.signal_clicked().connect([=]() {_apply();});
+
+ signal_delete_event().connect([=](GdkEventAny*) -> bool {
+ _close();
+ return 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() = default;
+
+/** Static member function which displays a modal dialog of the given type */
+void LayerPropertiesDialog::_showDialog(LayerPropertiesDialogType type, SPDesktop *desktop, SPObject *layer)
+{
+ auto dialog = new LayerPropertiesDialog(type); // Will be destroyed on idle - see _close()
+
+ dialog->_setDesktop(desktop);
+ dialog->_setLayer(layer);
+
+ dialog->_setup();
+
+ dialog->set_modal(true);
+ desktop->setWindowTransient(dialog->gobj());
+ dialog->property_destroy_with_parent() = true;
+
+ dialog->show();
+ dialog->present();
+}
+
+/** Performs an action depending on the type of the dialog */
+void LayerPropertiesDialog::_apply()
+{
+ switch (_type) {
+ case LayerPropertiesDialogType::CREATE:
+ _doCreate();
+ break;
+
+ case LayerPropertiesDialogType::MOVE:
+ _doMove();
+ break;
+
+ case LayerPropertiesDialogType::RENAME:
+ _doRename();
+ break;
+
+ case LayerPropertiesDialogType::NONE:
+ default:
+ break;
+ }
+ _close();
+}
+
+/** Closes the dialog and asks the idle thread to destroy it */
+void LayerPropertiesDialog::_close()
+{
+ _setLayer(nullptr);
+ _setDesktop(nullptr);
+ destroy_();
+ Glib::signal_idle().connect_once([=]() {delete this;});
+}
+
+/** Creates a new layer based on the input entered in the dialog window */
+void LayerPropertiesDialog::_doCreate()
+{
+ LayerRelativePosition position = LPOS_ABOVE;
+ if (_position_visible) {
+ Gtk::ListStore::iterator activeRow(_layer_position_combo.get_active());
+ position = activeRow->get_value(_dropdown_columns.position);
+ int index = _layer_position_combo.get_active_row_number();
+ Preferences::get()->setInt("/dialogs/layerProp/addLayerPosition", index);
+ }
+ Glib::ustring name(_layer_name_entry.get_text());
+ if (name.empty()) {
+ return;
+ }
+
+ auto root = _desktop->getDocument()->getRoot();
+ SPObject *new_layer = Inkscape::create_layer(root, _layer, position);
+
+ if (!name.empty()) {
+ _desktop->layerManager().renameLayer(new_layer, name.c_str(), true);
+ }
+ _desktop->getSelection()->clear();
+ _desktop->layerManager().setCurrentLayer(new_layer);
+ DocumentUndo::done(_desktop->getDocument(), _("Add layer"), INKSCAPE_ICON("layer-new"));
+ _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("New layer created."));
+}
+
+/** Moves selection to the chosen layer */
+void LayerPropertiesDialog::_doMove()
+{
+ if (auto moveto = _selectedLayer()) {
+ _desktop->getSelection()->toLayer(moveto);
+ DocumentUndo::done(_desktop->getDocument(), _("Move selection to layer"), INKSCAPE_ICON("selection-move-to-layer"));
+ }
+}
+
+/** Renames a layer based on the user input in the dialog window */
+void LayerPropertiesDialog::_doRename()
+{
+ Glib::ustring name(_layer_name_entry.get_text());
+ if (name.empty()) {
+ return;
+ }
+ LayerManager &layman = _desktop->layerManager();
+ layman.renameLayer(layman.currentLayer(), name.c_str(), false);
+
+ DocumentUndo::done(_desktop->getDocument(), _("Rename layer"), INKSCAPE_ICON("layer-rename"));
+ // TRANSLATORS: This means "The layer has been renamed"
+ _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Renamed layer"));
+}
+
+/** Sets up the dialog depending on its type */
+void LayerPropertiesDialog::_setup()
+{
+ g_assert(_desktop != nullptr);
+ LayerManager &layman = _desktop->layerManager();
+
+ switch (_type) {
+ case LayerPropertiesDialogType::CREATE: {
+ set_title(_("Add Layer"));
+ Glib::ustring new_name = layman.getNextLayerName(nullptr, layman.currentLayer()->label());
+ _layer_name_entry.set_text(new_name);
+ _apply_button.set_label(_("_Add"));
+ _setup_position_controls();
+ break;
+ }
+
+ case LayerPropertiesDialogType::MOVE: {
+ set_title(_("Move to Layer"));
+ _layer_name_entry.set_text(_("Layer"));
+ _apply_button.set_label(_("_Move"));
+ _apply_button.set_sensitive(layman.getLayerCount());
+ _setup_layers_controls();
+ break;
+ }
+
+ case LayerPropertiesDialogType::RENAME: {
+ set_title(_("Rename Layer"));
+ gchar const *name = layman.currentLayer()->label();
+ _layer_name_entry.set_text(name ? name : _("Layer"));
+ _apply_button.set_label(_("_Rename"));
+ break;
+ }
+
+ case LayerPropertiesDialogType::NONE:
+ default:
+ break;
+ }
+}
+
+/** Sets up the combo box for choosing the relative position of the new layer */
+void LayerPropertiesDialog::_setup_position_controls()
+{
+ if (!_layer || _desktop->getDocument()->getRoot() == _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,
+ [=](Gtk::TreeModel::const_iterator const &row) {
+ _prepareLabelRenderer(row);
+ });
+
+ 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")));
+
+ int position = Preferences::get()->getIntLimited("/dialogs/layerProp/addLayerPosition", 0, 0, 2);
+ _layer_position_combo.set_active(position);
+
+ _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();
+}
+
+/** Sets up the tree view of current layers */
+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);
+
+ auto *eyeRenderer = Gtk::manage(new 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);
+ }
+
+ auto *renderer = Gtk::manage(new 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([=](GdkEventKey *ev) {return _handleKeyEvent(ev);}, false);
+ _tree.signal_button_press_event().connect_notify([=](GdkEventButton *b) {_handleButtonEvent(b);});
+
+ _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->layerManager().currentLayer();
+ _store->clear();
+ _addLayer(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();
+ _scroller.set_propagate_natural_width(true);
+ _scroller.set_propagate_natural_height(true);
+ _layout_table.attach(_scroller, 0, 1, 2, 1);
+
+ show_all_children();
+}
+
+/** Inserts the new layer into the document */
+void LayerPropertiesDialog::_addLayer(SPObject* layer, Gtk::TreeModel::Row* parentRow, SPObject* target,
+ int level)
+{
+ int const max_nest_depth = 20;
+ if (!_desktop || !layer || level >= max_nest_depth) {
+ g_warn_message("Inkscape", __FILE__, __LINE__, __func__, "Maximum layer nesting reached.");
+ return;
+ }
+ LayerManager &layman = _desktop->layerManager();
+ unsigned int counter = layman.childCount(layer);
+ for (unsigned int i = 0; i < counter; i++) {
+ SPObject *child = _desktop->layerManager().nthChildOf(layer, i);
+ if (!child) {
+ continue;
+ }
+#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] = is<SPItem>(child) ? !cast_unsafe<SPItem>(child)->isHidden() : false;
+ row[_model->_colLocked] = is<SPItem>(child) ? cast_unsafe<SPItem>(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);
+ }
+
+ _addLayer(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: {
+ _apply();
+ return true;
+ }
+ }
+ return false;
+}
+
+void LayerPropertiesDialog::_handleButtonEvent(GdkEventButton* event)
+{
+ if ((event->type == GDK_2BUTTON_PRESS) && (event->button == 1)) {
+ _apply();
+ }
+}
+
+/** 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;
+}
+
+
+void LayerPropertiesDialog::_setLayer(SPObject *layer) {
+ if (layer) {
+ sp_object_ref(layer, nullptr);
+ }
+ if (_layer) {
+ sp_object_unref(_layer, nullptr);
+ }
+ _layer = layer;
+}
+
+} // 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/layer-properties.h b/src/ui/dialog/layer-properties.h
new file mode 100644
index 0000000..6ebbc07
--- /dev/null
+++ b/src/ui/dialog/layer-properties.h
@@ -0,0 +1,158 @@
+// 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-manager.h"
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialogs {
+
+/* FIXME: split the LayerPropertiesDialog class into three separate dialogs */
+enum class LayerPropertiesDialogType
+{
+ NONE,
+ CREATE,
+ MOVE,
+ RENAME
+};
+
+class LayerPropertiesDialog : public Gtk::Dialog {
+public:
+ LayerPropertiesDialog(LayerPropertiesDialogType type);
+ ~LayerPropertiesDialog() override;
+ LayerPropertiesDialog(LayerPropertiesDialog const &) = delete; // no copy
+ LayerPropertiesDialog &operator=(LayerPropertiesDialog const &) = delete; // no assign
+
+ Glib::ustring getName() const { return "LayerPropertiesDialog"; }
+
+ static void showRename(SPDesktop *desktop, SPObject *layer) {
+ _showDialog(LayerPropertiesDialogType::RENAME, desktop, layer);
+ }
+ static void showCreate(SPDesktop *desktop, SPObject *layer) {
+ _showDialog(LayerPropertiesDialogType::CREATE, desktop, layer);
+ }
+ static void showMove(SPDesktop *desktop, SPObject *layer) {
+ _showDialog(LayerPropertiesDialogType::MOVE, desktop, layer);
+ }
+
+private:
+ LayerPropertiesDialogType _type = LayerPropertiesDialogType::NONE;
+ SPDesktop *_desktop = nullptr;
+ SPObject *_layer = nullptr;
+
+ 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 = false;
+
+ 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;
+
+ void _setDesktop(SPDesktop *desktop) { _desktop = desktop; };
+ void _setLayer(SPObject *layer);
+
+ static void _showDialog(LayerPropertiesDialogType type, 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(SPObject* layer, Gtk::TreeModel::Row* parentRow, SPObject* target, int level);
+ SPObject* _selectedLayer();
+ bool _handleKeyEvent(GdkEventKey *event);
+ void _handleButtonEvent(GdkEventButton* event);
+
+ void _doCreate();
+ void _doMove();
+ void _doRename();
+ void _setup();
+};
+
+} // 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/livepatheffect-add.cpp b/src/ui/dialog/livepatheffect-add.cpp
new file mode 100644
index 0000000..150aada
--- /dev/null
+++ b/src/ui/dialog/livepatheffect-add.cpp
@@ -0,0 +1,1000 @@
+// 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 <cmath>
+#include <glibmm/i18n.h>
+
+#include "desktop.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 "object/sp-use.h"
+#include "preferences.h"
+#include "ui/widget/canvas.h"
+#include "ui/themes.h"
+#include "ui/util.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+bool sp_has_fav_dialog(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_dialog(Glib::ustring effect)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring favlist = prefs->getString("/dialogs/livepatheffect/favs");
+ if (!sp_has_fav_dialog(effect)) {
+ prefs->setString("/dialogs/livepatheffect/favs", favlist + effect + ";");
+ }
+}
+
+void sp_remove_fav_dialog(Glib::ustring effect)
+{
+ if (sp_has_fav_dialog(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);
+ }
+ }
+}
+
+void sp_add_top_window_classes_callback(Gtk::Widget *widg)
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if (desktop) {
+ Gtk::Widget *canvas = 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);
+ }
+}
+
+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)
+{
+ auto gladefile = get_filename_string(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 path 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_string(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);
+ Gtk::Label *LPEUntranslatedName;
+ builder_effect->get_widget("LPEUntranslatedName", LPEUntranslatedName);
+ const Glib::ustring label = _(converter.get_label(data->id).c_str());
+ const Glib::ustring untranslated_label = converter.get_label(data->id);
+ LPEUntranslatedName->set_text(untranslated_label);
+ 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_dialog(untranslated_label)) {
+ 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_dialog(untranslated_label)) {
+ 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));
+ _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.themecontext->getChangeThemeSignal().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::Label *LPEUntranslatedName;
+ builder_effect->get_widget("LPEUntranslatedName", LPEUntranslatedName);
+ 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_dialog(LPEUntranslatedName->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_dialog(LPEUntranslatedName->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_dialog(LPEUntranslatedName->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.raw());
+ }
+ }
+ 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]);
+ Gtk::Label *untranslatedname = dynamic_cast<Gtk::Label *>(contents[6]);
+ if (!sp_has_fav_dialog(untranslatedname->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;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/dialogs/livepatheffect/showexperimental", _LPEExperimental->get_active());
+ _LPESelectorFlowBox->invalidate_filter();
+ if (_showfavs) {
+ if (_visiblelpe == 0) {
+ _LPEInfo->set_text(_("You don't have any favorites yet. Click on the favorites star again to see all LPEs."));
+ _LPEInfo->set_visible(true);
+ _LPEInfo->get_style_context()->add_class("lpeinfowarn");
+ } else {
+ _LPEInfo->set_text(_("These are your favorite effects"));
+ _LPEInfo->set_visible(true);
+ _LPEInfo->get_style_context()->add_class("lpeinfowarn");
+ }
+ } else {
+ _LPEInfo->set_text(_("Nothing found! Please try again with different search terms."));
+ _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(_("Nothing found! Please try again with different search terms."));
+ _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(_("Nothing found! Please try again with different search terms."));
+ _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 uname1 = "";
+ Glib::ustring name2 = "";
+ Glib::ustring uname2 = "";
+ 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]);
+ Gtk::Label *ulpename = dynamic_cast<Gtk::Label *>(contents[6]);
+ name1 = lpename->get_text();
+ uname1 = ulpename->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_end(25);
+ overlay->set_margin_end(5);
+ } else {
+ icon->set_pixel_size(60);
+ icon->set_margin_end(0);
+ overlay->set_margin_end(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_dialog(uname1)) {
+ 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_dialog(uname1)) {
+ 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();
+ Gtk::Label *ulpename = dynamic_cast<Gtk::Label *>(contents[6]);
+ uname2 = ulpename->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_end(40);
+ } else {
+ icon->set_pixel_size(60);
+ icon->set_margin_end(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_dialog(uname2)) {
+ 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_dialog(uname2)) {
+ 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_dialog(name1) && sp_has_fav_dialog(name2)) {
+ return effect[0] == name1?-1:1;
+ }
+ if (sp_has_fav_dialog(name1)) {
+ return -1;
+ } */
+ if (effect[0] == name1) { //&& !sp_has_fav_dialog(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) {
+ auto use = cast<SPUse>(item);
+ if (use) {
+ item = use->get_original();
+ }
+ auto shape = cast<SPShape>(item);
+ auto path = cast<SPPath>(item);
+ auto group = 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());
+ Gtk::Window *window = desktop->getToplevel();
+ dial._LPEDialogSelector->set_transient_for(*window);
+ 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..51e7437
--- /dev/null
+++ b/src/ui/dialog/livepatheffect-editor.cpp
@@ -0,0 +1,1258 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief A dialog for Live Path Effects (LPE)
+ */
+/* Authors:
+ * Jabiertxof
+ * Adam Belis (UX/Design)
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "livepatheffect-editor.h"
+#include "live_effects/effect-enum.h"
+#include "livepatheffect-add.h"
+#include "live_effects/effect.h"
+#include "live_effects/lpeobject-reference.h"
+#include "live_effects/lpeobject.h"
+
+#include "object/sp-lpe-item.h"
+#include "svg/svg.h"
+#include "ui/icon-names.h"
+#include "ui/icon-loader.h"
+#include "ui/builder-utils.h"
+#include "io/resource.h"
+#include "object/sp-use.h"
+#include "object/sp-shape.h"
+#include "object/sp-path.h"
+#include "object/sp-flowtext.h"
+#include "object/sp-tspan.h"
+#include "object/sp-item-group.h"
+#include "object/sp-text.h"
+#include "ui/tools/node-tool.h"
+#include "ui/widget/custom-tooltip.h"
+#include "util/optstr.h"
+#include <cstddef>
+#include <glibmm/i18n.h>
+
+namespace Inkscape {
+namespace UI {
+
+
+bool sp_can_apply_lpeffect(SPLPEItem* item, LivePathEffect::EffectType etype) {
+ if (!item) return false;
+
+ auto shape = cast<SPShape>(item);
+ auto path = cast<SPPath>(item);
+ auto group = cast<SPGroup>(item);
+ Glib::ustring item_type;
+ if (group) {
+ item_type = "group";
+ } else if (path) {
+ item_type = "path";
+ } else if (shape) {
+ item_type = "shape";
+ }
+ bool has_clip = item->getClipObject() != nullptr;
+ bool has_mask = item->getMaskObject() != nullptr;
+ bool applicable = true;
+ if (!has_clip && etype == LivePathEffect::POWERCLIP) {
+ applicable = false;
+ }
+ if (!has_mask && etype == LivePathEffect::POWERMASK) {
+ applicable = false;
+ }
+ if (item_type == "group" && !Inkscape::LivePathEffect::LPETypeConverter.get_on_group(etype)) {
+ applicable = false;
+ } else if (item_type == "shape" && !Inkscape::LivePathEffect::LPETypeConverter.get_on_shape(etype)) {
+ applicable = false;
+ } else if (item_type == "path" && !Inkscape::LivePathEffect::LPETypeConverter.get_on_path(etype)) {
+ applicable = false;
+ }
+ return applicable;
+}
+
+void sp_apply_lpeffect(SPDesktop* desktop, SPLPEItem* item, LivePathEffect::EffectType etype) {
+ if (!sp_can_apply_lpeffect(item, etype)) return;
+
+ Glib::ustring key = Inkscape::LivePathEffect::LPETypeConverter.get_key(etype);
+ LivePathEffect::Effect::createAndApply(key.c_str(), item->document, item);
+ item->getCurrentLPE()->refresh_widgets = true;
+ DocumentUndo::done(item->document, _("Create and apply path effect"), INKSCAPE_ICON("dialog-path-effects"));
+
+ if (desktop) {
+ // this is rotten - UI LPE knots refresh
+ // force selection change
+ desktop->getSelection()->clear();
+ desktop->getSelection()->add(item);
+ Inkscape::UI::Tools::sp_update_helperpath(desktop);
+ }
+}
+
+namespace Dialog {
+
+/*####################
+ * Callback functions
+ */
+
+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 != Glib::ustring::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 != Glib::ustring::npos) {
+ favlist.erase(pos, effect.length());
+ prefs->setString("/dialogs/livepatheffect/favs", favlist);
+ }
+ }
+}
+
+void sp_toggle_fav(Glib::ustring effect, Gtk::MenuItem *LPEtoggleFavorite)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring favlist = prefs->getString("/dialogs/livepatheffect/favs");
+ if (sp_has_fav(effect)) {
+ sp_remove_fav(effect);
+ LPEtoggleFavorite->set_label(_("Set Favorite"));
+ } else {
+ sp_add_fav(effect);
+ LPEtoggleFavorite->set_label(_("Unset Favorite"));
+ }
+}
+
+void LivePathEffectEditor::selectionChanged(Inkscape::Selection * selection)
+{
+ if (selection_changed_lock) {
+ return;
+ }
+ onSelectionChanged(selection);
+ clearMenu();
+}
+void LivePathEffectEditor::selectionModified(Inkscape::Selection * selection, guint flags)
+{
+ current_lpeitem = cast<SPLPEItem>(selection->singleItem());
+ if (!selection_changed_lock && current_lpeitem && effectlist != current_lpeitem->getEffectList()) {
+ onSelectionChanged(selection);
+ } else if (current_lpeitem && current_lperef.first) {
+ showParams(current_lperef, false);
+ }
+ clearMenu();
+}
+
+/**
+ * Constructor
+ */
+LivePathEffectEditor::LivePathEffectEditor()
+ : DialogBase("/dialogs/livepatheffect", "LivePathEffect"),
+ _builder(create_builder("dialog-livepatheffect.glade")),
+ LPEListBox(get_widget<Gtk::ListBox>(_builder, "LPEListBox")),
+ _LPEContainer(get_widget<Gtk::Box>(_builder, "LPEContainer")),
+ _LPEAddContainer(get_widget<Gtk::Box>(_builder, "LPEAddContainer")),
+ _LPEParentBox(get_widget<Gtk::ListBox>(_builder, "LPEParentBox")),
+ _LPECurrentItem(get_widget<Gtk::Box>(_builder, "LPECurrentItem")),
+ _LPESelectionInfo(get_widget<Gtk::Label>(_builder, "LPESelectionInfo")),
+ _LPEGallery(get_widget<Gtk::Button>(_builder, "LPEGallery")),
+ _showgallery_observer(Preferences::PreferencesObserver::create(
+ "/dialogs/livepatheffect/showgallery", sigc::mem_fun(*this, &LivePathEffectEditor::on_showgallery_notify))),
+ converter(Inkscape::LivePathEffect::LPETypeConverter)
+{
+ _LPEGallery.signal_clicked().connect(sigc::mem_fun(*this, &LivePathEffectEditor::onAddGallery));
+ _showgallery_observer->call(); // Set initial visibility per Preference (widget is :no-show-all)
+
+ Glib::RefPtr<Gtk::EntryCompletion> LPECompletionList = Glib::RefPtr<Gtk::EntryCompletion>::cast_dynamic(_builder->get_object("LPECompletionList"));
+
+ _LPEContainer.signal_map().connect(sigc::mem_fun(*this, &LivePathEffectEditor::map_handler) );
+ _LPEContainer.signal_button_press_event().connect([=](GdkEventButton* const evt){dnd = false; /*hack to fix dnd freze expander*/ return false; }, false);
+ setMenu();
+ add(_LPEContainer);
+ selection_info();
+ _lpes_popup.get_entry().set_placeholder_text(_("Add Live Path Effect"));
+ _lpes_popup.on_match_selected().connect([=](int id){ onAdd((LivePathEffect::EffectType)id); });
+ _lpes_popup.on_button_press().connect([=](){ setMenu(); });
+ _lpes_popup.on_focus().connect([=](){ setMenu(); return true; });
+ _LPEAddContainer.pack_start(_lpes_popup);
+ show_all();
+}
+
+LivePathEffectEditor::~LivePathEffectEditor()
+{
+ sp_clear_custom_tooltip();
+}
+
+bool separator_func(const Glib::RefPtr<Gtk::TreeModel>& model,
+ const Gtk::TreeModel::iterator& iter) {
+ Gtk::TreeModel::Row row = *iter;
+ bool *separator;
+ row->get_value(3, separator);
+ return separator;
+}
+
+bool
+LivePathEffectEditor::is_appliable(LivePathEffect::EffectType etype, Glib::ustring item_type, bool has_clip, bool has_mask) {
+ bool appliable = true;
+
+ if (!has_clip && etype == LivePathEffect::POWERCLIP) {
+ appliable = false;
+ }
+ if (!has_mask && etype == LivePathEffect::POWERMASK) {
+ appliable = false;
+ }
+ if (item_type == "group" && !converter.get_on_group(etype)) {
+ appliable = false;
+ } else if (item_type == "shape" && !converter.get_on_shape(etype)) {
+ appliable = false;
+ } else if (item_type == "path" && !converter.get_on_path(etype)) {
+ appliable = false;
+ }
+ return appliable;
+}
+
+void align(Gtk::Widget* top, gint spinbutton_width_chars) {
+ auto box = dynamic_cast<Gtk::Box*>(top);
+ if (!box) return;
+ box->set_spacing(2);
+
+ // traverse container, locate n-th child in each row
+ auto for_child_n = [=](int child_index, const std::function<void (Gtk::Widget*)>& action) {
+ for (auto child : box->get_children()) {
+ auto container = dynamic_cast<Gtk::Box*>(child);
+ if (!container) continue;
+ container->set_spacing(2);
+ const auto& children = container->get_children();
+ if (children.size() > child_index) {
+ action(children[child_index]);
+ }
+ }
+ };
+
+ // column 0 - labels
+ int max_width = 0;
+ for_child_n(0, [&](Gtk::Widget* child){
+ if (auto label = dynamic_cast<Gtk::Label*>(child)) {
+ label->set_xalign(0); // left-align
+ int label_width = 0, dummy = 0;
+ label->get_preferred_width(dummy, label_width);
+ if (label_width > max_width) {
+ max_width = label_width;
+ }
+ }
+ });
+ // align
+ for_child_n(0, [=](Gtk::Widget* child) {
+ if (auto label = dynamic_cast<Gtk::Label*>(child)) {
+ label->set_size_request(max_width);
+ }
+ });
+
+ // column 1 - align spin buttons, if any
+ int button_width = 0;
+ for_child_n(1, [&](Gtk::Widget* child) {
+ if (auto spin = dynamic_cast<Gtk::SpinButton*>(child)) {
+ // selected spinbutton size by each LPE default 7
+ spin->set_width_chars(spinbutton_width_chars);
+ int dummy = 0;
+ spin->get_preferred_width(dummy, button_width);
+ }
+ });
+ // set min size for comboboxes, if any
+ int combo_size = button_width > 0 ? button_width : 50; // match with spinbuttons, or just min of 50px
+ for_child_n(1, [=](Gtk::Widget* child) {
+ if (auto combo = dynamic_cast<Gtk::ComboBox*>(child)) {
+ combo->set_size_request(combo_size);
+ }
+ });
+}
+
+void
+LivePathEffectEditor::clearMenu()
+{
+ sp_clear_custom_tooltip();
+ _reload_menu = true;
+}
+
+void
+LivePathEffectEditor::toggleVisible(Inkscape::LivePathEffect::Effect *lpe , Gtk::EventBox *visbutton) {
+ auto *visimage = dynamic_cast<Gtk::Image *>(dynamic_cast<Gtk::Button *>(visbutton->get_children()[0])->get_image());
+ bool hide = false;
+ if (!g_strcmp0(lpe->getRepr()->attribute("is_visible"),"true")) {
+ visimage->set_from_icon_name("object-hidden-symbolic", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ lpe->getRepr()->setAttribute("is_visible", "false");
+ hide = true;
+ } else {
+ visimage->set_from_icon_name("object-visible-symbolic", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ lpe->getRepr()->setAttribute("is_visible", "true");
+ }
+ lpe->doOnVisibilityToggled(current_lpeitem);
+ DocumentUndo::done(getDocument(), hide ? _("Deactivate path effect") : _("Activate path effect"), INKSCAPE_ICON("dialog-path-effects"));
+}
+
+const Glib::ustring& get_category_name(Inkscape::LivePathEffect::LPECategory category) {
+ static const std::map<Inkscape::LivePathEffect::LPECategory, Glib::ustring> category_names = {
+ { Inkscape::LivePathEffect::LPECategory::Favorites, _("Favorites") },
+ { Inkscape::LivePathEffect::LPECategory::EditTools, _("Edit/Tools") },
+ { Inkscape::LivePathEffect::LPECategory::Distort, _("Distort") },
+ { Inkscape::LivePathEffect::LPECategory::Generate, _("Generate") },
+ { Inkscape::LivePathEffect::LPECategory::Convert, _("Convert") },
+ { Inkscape::LivePathEffect::LPECategory::Experimental, _("Experimental") },
+ };
+ return category_names.at(category);
+}
+
+struct LPEMetadata {
+ Inkscape::LivePathEffect::LPECategory category;
+ Glib::ustring icon_name;
+ Glib::ustring tooltip;
+ bool sensitive;
+};
+
+static std::map<Inkscape::LivePathEffect::EffectType, LPEMetadata> g_lpes;
+// populate popup with lpes and completion list for a search box
+void LivePathEffectEditor::add_lpes(Inkscape::UI::Widget::CompletionPopup& popup, bool symbolic) {
+ auto& menu = popup.get_menu();
+ struct LPE {
+ Inkscape::LivePathEffect::EffectType type;
+ Glib::ustring label;
+ Inkscape::LivePathEffect::LPECategory category;
+ Glib::ustring icon_name;
+ Glib::ustring tooltip;
+ bool sensitive;
+ };
+ std::vector<LPE> lpes;
+ lpes.reserve(g_lpes.size());
+ for (auto&& lpe : g_lpes) {
+ lpes.push_back({
+ lpe.first,
+ g_dpgettext2(0, "path effect", converter.get_label(lpe.first).c_str()),
+ lpe.second.category,
+ lpe.second.icon_name,
+ lpe.second.tooltip,
+ lpe.second.sensitive
+ });
+ }
+ std::sort(begin(lpes), end(lpes), [=](auto&& a, auto&& b) {
+ if (a.category != b.category) {
+ return a.category < b.category;
+ }
+ return a.label < b.label;
+ });
+
+ popup.clear_completion_list();
+
+ // 2-column menu
+ for (auto w:menu.get_children()) {
+ menu.remove(*w);
+ }
+ Inkscape::UI::ColumnMenuBuilder<Inkscape::LivePathEffect::LPECategory> builder(menu, 3, Gtk::ICON_SIZE_LARGE_TOOLBAR);
+
+ for (auto& lpe : lpes) {
+ // build popup menu
+ auto type = lpe.type;
+ auto *menuitem = builder.add_item(lpe.label, lpe.category, lpe.tooltip, lpe.icon_name, lpe.sensitive, true, [=](){ onAdd((LivePathEffect::EffectType)type); });
+ gint id = (gint)type;
+ menuitem->property_has_tooltip() = true;
+ menuitem->signal_query_tooltip().connect([=](int x, int y, bool kbd, const Glib::RefPtr<Gtk::Tooltip>& tooltipw){
+ return sp_query_custom_tooltip(x, y, kbd, tooltipw, id, lpe.tooltip, lpe.icon_name);
+ });
+ if (builder.new_section()) {
+ builder.set_section(get_category_name(lpe.category));
+ }
+
+ // build completion list
+ if (lpe.sensitive) {
+ popup.add_to_completion_list(static_cast<int>(lpe.type), lpe.label, lpe.icon_name + (symbolic ? "-symbolic" : ""));
+ }
+ }
+
+ if (symbolic) {
+ menu.get_style_context()->add_class("symbolic");
+ }
+}
+
+
+void
+LivePathEffectEditor::setMenu()
+{
+ if (!_reload_menu) {
+ return;
+ }
+ auto shape = cast<SPShape>(current_lpeitem);
+ auto path = cast<SPPath>(current_lpeitem);
+ auto group = cast<SPGroup>(current_lpeitem);
+ bool has_clip = current_lpeitem && (current_lpeitem->getClipObject() != nullptr);
+ bool has_mask = current_lpeitem && (current_lpeitem->getMaskObject() != nullptr);
+ Glib::ustring item_type = "";
+ if (group) {
+ item_type = "group";
+ } else if (path) {
+ item_type = "path";
+ } else if (shape) {
+ item_type = "shape";
+ }
+ if (_item_type != item_type || has_clip != _has_clip || has_mask != _has_mask) {
+ _item_type = item_type;
+ _has_clip = has_clip;
+ _has_mask = has_mask;
+ g_lpes.clear();
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool experimental = prefs->getBool("/dialogs/livepatheffect/showexperimental", false);
+ std::map<Inkscape::LivePathEffect::LPECategory, std::map< Glib::ustring, const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *> > lpesorted;
+ for (int i = 0; i < static_cast<int>(converter._length); ++i) {
+ const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *data = &converter.data(i);
+ const Glib::ustring label = _(converter.get_label(data->id).c_str());
+ const Glib::ustring untranslated_label = converter.get_label(data->id);
+ Glib::ustring name = label;
+ if (untranslated_label != label) {
+ name =+ "\n<span size='x-small'>" + untranslated_label + "</span>";
+ }
+ Inkscape::LivePathEffect::LPECategory category = converter.get_category(data->id);
+ if (sp_has_fav(untranslated_label)) {
+ //category = 0;
+ category = Inkscape::LivePathEffect::LPECategory::Favorites;
+ }
+ if (!experimental && category == Inkscape::LivePathEffect::LPECategory::Experimental) {
+ continue;
+ }
+ lpesorted[category][name] = data;
+ }
+ for (auto e : lpesorted) {
+ for (auto e2 : e.second) {
+ const Glib::ustring label = _(converter.get_label(e2.second->id).c_str());
+ const Glib::ustring untranslated_label = converter.get_label(e2.second->id);
+ Glib::ustring tooltip = _(converter.get_description(e2.second->id).c_str());
+ if (untranslated_label != label) {
+ tooltip = "[" + untranslated_label + "] " + _(converter.get_description(e2.second->id).c_str());
+ }
+ Glib::ustring name = label;
+ Glib::ustring icon = converter.get_icon(e2.second->id);
+ LPEMetadata mdata;
+ mdata.category = e.first;
+ mdata.icon_name = icon;
+ mdata.tooltip = tooltip;
+ mdata.sensitive = is_appliable(e2.second->id, item_type, has_clip, has_mask);
+ g_lpes[e2.second->id] = mdata;
+ }
+ }
+ auto symbolic = Inkscape::Preferences::get()->getBool("/theme/symbolicIcons", true);
+ add_lpes(_lpes_popup, symbolic);
+ }
+}
+
+void LivePathEffectEditor::onAdd(LivePathEffect::EffectType etype)
+{
+ selection_changed_lock = true;
+ Glib::ustring key = converter.get_key(etype);
+ SPLPEItem *fromclone = clonetolpeitem();
+ if (fromclone) {
+ current_lpeitem = fromclone;
+ if (key == "clone_original") {
+ current_lpeitem->getCurrentLPE()->refresh_widgets = true;
+ selection_changed_lock = false;
+ DocumentUndo::done(getDocument(), _("Create and apply path effect"), INKSCAPE_ICON("dialog-path-effects"));
+ return;
+ }
+ }
+ selection_changed_lock = false;
+ if (current_lpeitem) {
+ LivePathEffect::Effect::createAndApply(key.c_str(), getDocument(), current_lpeitem);
+ current_lpeitem->getCurrentLPE()->refresh_widgets = true;
+ DocumentUndo::done(getDocument(), _("Create and apply path effect"), INKSCAPE_ICON("dialog-path-effects"));
+ }
+}
+
+void
+LivePathEffectEditor::map_handler()
+{
+ ensure_size();
+}
+
+void
+LivePathEffectEditor::selection_info()
+{
+ auto selection = getSelection();
+ SPItem * selected = nullptr;
+ _LPESelectionInfo.hide();
+ if (selection && (selected = selection->singleItem()) ) {
+ if (is<SPText>(selected) || is<SPFlowtext>(selected)) {
+ _LPESelectionInfo.set_text(_("Text objects do not support Live Path Effects"));
+ _LPESelectionInfo.show();
+ Glib::ustring labeltext = _("Convert text to paths");
+ Gtk::Button *selectbutton = Gtk::manage(new Gtk::Button());
+ Gtk::Box *boxc = Gtk::manage(new Gtk::Box());
+ Gtk::Label *lbl = Gtk::manage(new Gtk::Label(labeltext));
+ std::string shape_type = "group";
+ std::string highlight = SPColor(selected->highlight_color()).toString();
+ Gtk::Image *type = Gtk::manage(new Gtk::Image(sp_get_shape_icon(shape_type, Gdk::RGBA(highlight),20, 1)));
+ boxc->pack_start(*type, false, false);
+ boxc->pack_start(*lbl, false, false);
+ type->set_margin_start(4);
+ type->set_margin_end(4);
+ selectbutton->add(*boxc);
+ selectbutton->signal_clicked().connect([=](){
+ selection->toCurves();
+ });
+ _LPEParentBox.add(*selectbutton);
+ Glib::ustring labeltext2 = _("Clone");
+ Gtk::Button *selectbutton2 = Gtk::manage(new Gtk::Button());
+ Gtk::Box *boxc2 = Gtk::manage(new Gtk::Box());
+ Gtk::Label *lbl2 = Gtk::manage(new Gtk::Label(labeltext2));
+ std::string shape_type2 = "clone";
+ std::string highlight2 = SPColor(selected->highlight_color()).toString();
+ Gtk::Image *type2 = Gtk::manage(new Gtk::Image(sp_get_shape_icon(shape_type2, Gdk::RGBA(highlight2),20, 1)));
+ boxc2->pack_start(*type2, false, false);
+ boxc2->pack_start(*lbl2, false, false);
+ type2->set_margin_start(4);
+ type2->set_margin_end(4);
+ selectbutton2->add(*boxc2);
+ selectbutton2->signal_clicked().connect([=](){
+ selection->clone();;
+ });
+ _LPEParentBox.add(*selectbutton2);
+ _LPEParentBox.show_all();
+ } else if (!is<SPLPEItem>(selected) && !is<SPUse>(selected)) {
+ _LPESelectionInfo.set_text(_("Select a path, shape, clone or group"));
+ _LPESelectionInfo.show();
+ } else {
+ if (selected->getId()) {
+ Glib::ustring labeltext = selected->label() ? selected->label() : selected->getId();
+ Gtk::Box *boxc = Gtk::manage(new Gtk::Box());
+ Gtk::Label *lbl = Gtk::manage(new Gtk::Label(labeltext));
+ lbl->set_ellipsize(Pango::ELLIPSIZE_END);
+ std::string shape_type = selected->typeName();
+ std::string highlight = SPColor(selected->highlight_color()).toString();
+ Gtk::Image *type = Gtk::manage(new Gtk::Image(sp_get_shape_icon(shape_type, Gdk::RGBA(highlight),20, 1)));
+ boxc->pack_start(*type, false, false);
+ boxc->pack_start(*lbl, false, false);
+ _LPECurrentItem.add(*boxc);
+ _LPECurrentItem.get_children()[0]->set_halign(Gtk::ALIGN_CENTER);
+ _LPESelectionInfo.hide();
+ }
+ std::vector<std::pair <Glib::ustring, Glib::ustring> > newrootsatellites;
+ for (auto root : selected->rootsatellites) {
+ auto lpeobj = cast<LivePathEffectObject>(selected->document->getObjectById(root.second));
+ Inkscape::LivePathEffect::Effect *lpe = nullptr;
+ if (lpeobj) {
+ lpe = lpeobj->get_lpe();
+ }
+ if (lpe) {
+ const Glib::ustring label = _(converter.get_label(lpe->effectType()).c_str());
+ Glib::ustring labeltext = Glib::ustring::compose(_("Select %1 with %2 LPE"), root.first, label);
+ auto lpeitem = cast<SPLPEItem>(selected->document->getObjectById(root.first));
+ if (lpeitem && lpeitem->getLPEIndex(lpe) != Glib::ustring::npos) {
+ newrootsatellites.emplace_back(root.first, root.second);
+ Gtk::Button *selectbutton = Gtk::manage(new Gtk::Button());
+ Gtk::Box *boxc = Gtk::manage(new Gtk::Box());
+ Gtk::Label *lbl = Gtk::manage(new Gtk::Label(labeltext));
+ std::string shape_type = selected->typeName();
+ std::string highlight = SPColor(selected->highlight_color()).toString();
+ Gtk::Image *type = Gtk::manage(new Gtk::Image(sp_get_shape_icon(shape_type, Gdk::RGBA(highlight),20, 1)));
+ boxc->pack_start(*type, false, false);
+ boxc->pack_start(*lbl, false, false);
+ type->set_margin_start(4);
+ type->set_margin_end(4);
+ selectbutton->add(*boxc);
+ selectbutton->signal_clicked().connect([=](){
+ selection->set(lpeitem);
+ });
+ _LPEParentBox.add(*selectbutton);
+ }
+ }
+ }
+ selected->rootsatellites = newrootsatellites;
+ _LPEParentBox.show_all();
+ _LPEParentBox.drag_dest_unset();
+ _LPECurrentItem.show_all();
+ }
+ } else if (!selection || selection->isEmpty()) {
+ _LPESelectionInfo.set_text(_("Select a path, shape, clone or group"));
+ _LPESelectionInfo.show();
+ } else if (selection->size() > 1) {
+ _LPESelectionInfo.set_text(_("Select only one path, shape, clone or group"));
+ _LPESelectionInfo.show();
+ }
+}
+
+void
+LivePathEffectEditor::onSelectionChanged(Inkscape::Selection *sel)
+{
+ SPUse *use = nullptr;
+ _reload_menu = true;
+ if ( sel && !sel->isEmpty() ) {
+ SPItem *item = sel->singleItem();
+ if ( item ) {
+ auto lpeitem = cast<SPLPEItem>(item);
+ use = cast<SPUse>(item);
+ if (lpeitem) {
+ lpeitem->update_satellites();
+ current_lpeitem = lpeitem;
+ _LPEAddContainer.set_sensitive(true);
+ effect_list_reload(lpeitem);
+ return;
+ }
+ }
+ }
+ current_lpeitem = nullptr;
+ _LPEAddContainer.set_sensitive(use != nullptr);
+ clear_lpe_list();
+ selection_info();
+}
+
+void
+LivePathEffectEditor::move_list(gint origin, gint dest)
+{
+ Inkscape::Selection *sel = getDesktop()->getSelection();
+
+ if ( sel && !sel->isEmpty() ) {
+ SPItem *item = sel->singleItem();
+ if ( item ) {
+ auto lpeitem = cast<SPLPEItem>(item);
+ if ( lpeitem ) {
+ lpeitem->movePathEffect(origin, dest);
+ }
+ }
+ }
+}
+
+static const std::vector<Gtk::TargetEntry> entries = {Gtk::TargetEntry("GTK_LIST_BOX_ROW", Gtk::TARGET_SAME_APP, 0 )};
+
+void
+LivePathEffectEditor::showParams(std::pair<Gtk::Expander *, std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> > expanderdata, bool changed)
+{
+ LivePathEffectObject *lpeobj = expanderdata.second->lpeobject;
+
+ if (lpeobj) {
+ Inkscape::LivePathEffect::Effect *lpe = lpeobj->get_lpe();
+ if (lpe) {
+ if (effectwidget && !lpe->refresh_widgets && expanderdata == current_lperef && !changed) {
+ return;
+ }
+ if (effectwidget) {
+ effectwidget->get_parent()->remove(*effectwidget);
+ delete effectwidget;
+ effectwidget = nullptr;
+ }
+ effectwidget = lpe->newWidget();
+ if (!dynamic_cast<Gtk::Container *>(effectwidget)->get_children().size()) {
+ auto * label = new Gtk::Label("", Gtk::ALIGN_START, Gtk::ALIGN_CENTER);
+ label->set_markup(_("<small>Without parameters</small>"));
+ label->set_margin_top(5);
+ label->set_margin_bottom(5);
+ label->set_margin_start(5);
+ effectwidget = label;
+ }
+ expanderdata.first->add(*effectwidget);
+ expanderdata.first->show_all_children();
+ align(effectwidget, lpe->spinbutton_width_chars);
+ // fixme: add resizing of dialog
+ lpe->refresh_widgets = false;
+ ensure_size();
+ } else {
+ current_lperef = std::make_pair(nullptr, nullptr);
+ }
+ } else {
+ current_lperef = std::make_pair(nullptr, nullptr);
+ }
+
+ // effectwidget = effect.newWidget();
+ // effectcontrol_frame.set_label(effect.getName());
+ // effectcontrol_vbox.pack_start(*effectwidget, true, true);
+
+ // button_remove.show();
+ // status_label.hide();
+ // effectcontrol_vbox.show_all_children();
+ // align(effectwidget);
+ // effectcontrol_frame.show();
+ // // fixme: add resizing of dialog
+ // effect.refresh_widgets = false;
+}
+
+bool
+LivePathEffectEditor::closeExpander(GdkEventButton * evt) {
+ current_lperef.first->set_expanded(false);
+ return false;
+}
+
+/*
+ * First clears the effectlist_store, then appends all effects from the effectlist.
+ */
+void
+LivePathEffectEditor::effect_list_reload(SPLPEItem *lpeitem)
+{
+ clear_lpe_list();
+ _LPEExpanders.clear();
+ auto gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "dialog-livepatheffect-item.glade");
+ gint counter = -1;
+ Gtk::Expander *LPEExpanderCurrent = nullptr;
+ effectlist = lpeitem->getEffectList();
+ gint total = effectlist.size();
+ if (total > 1) {
+ _LPECurrentItem.drag_dest_unset();
+ _lpes_popup.drag_dest_unset();
+ _lpes_popup.get_entry().drag_dest_unset();
+ _LPEAddContainer.drag_dest_unset();
+ _LPEContainer.drag_dest_set(entries, Gtk::DEST_DEFAULT_ALL, Gdk::ACTION_MOVE);
+ _LPEContainer.signal_drag_data_received().connect([=](const Glib::RefPtr<Gdk::DragContext>& context, int x, int y, const Gtk::SelectionData& selection_data, guint info, guint time)
+ {
+ if (dnd) {
+ unsigned int pos_target, pos_source;
+ Gtk::Widget *target = &_LPEContainer;
+ pos_source = atoi(reinterpret_cast<char const*>(selection_data.get_data()));
+ pos_target = LPEListBox.get_children().size()-1;
+ if (y < 90) {
+ pos_target = 0;
+ }
+ if (pos_target == pos_source) {
+ gtk_drag_finish(context->gobj(), FALSE, FALSE, time);
+ dnd = false;
+ return;
+ }
+ Glib::RefPtr<Gtk::StyleContext> stylec = target->get_style_context();
+ if (pos_source > pos_target) {
+ if (stylec->has_class("after")) {
+ pos_target ++;
+ }
+ } else if (pos_source < pos_target) {
+ if (stylec->has_class("before")) {
+ pos_target --;
+ }
+ }
+ Gtk::Widget *source = LPEListBox.get_row_at_index(pos_source);
+ g_object_ref(source->gobj());
+ LPEListBox.remove(*source);
+ LPEListBox.insert(*source, pos_target);
+ g_object_unref(source->gobj());
+ move_list(pos_source,pos_target);
+ gtk_drag_finish(context->gobj(), TRUE, TRUE, time);
+ dnd = false;
+ }
+ });
+ _LPEContainer.signal_drag_motion().connect([=](const Glib::RefPtr<Gdk::DragContext>& context, int x, int y, guint time)
+ {
+ Glib::RefPtr<Gtk::StyleContext> stylec = _LPEContainer.get_style_context();
+ if (y < 90) {
+ stylec->add_class("before");
+ stylec->remove_class("after");
+ } else {
+ stylec->remove_class("before");
+ stylec->add_class("after");
+ }
+ return true;
+ }, true);
+ }
+ PathEffectList::iterator it;
+ Gtk::MenuItem *LPEMoveUpExtrem = nullptr;
+ Gtk::MenuItem *LPEMoveDownExtrem = nullptr;
+ Gtk::EventBox *LPEDrag = nullptr;
+ for( it = effectlist.begin() ; it!=effectlist.end(); ++it)
+ {
+ if ( !(*it)->lpeobject ) {
+ continue;
+ }
+ auto lpe = (*it)->lpeobject->get_lpe();
+ bool current = lpeitem->getCurrentLPE() == lpe;
+ counter++;
+ Glib::RefPtr<Gtk::Builder> builder;
+ if (lpe) {
+ try {
+ builder = Gtk::Builder::create_from_file(gladefile);
+ } catch (const Glib::Error &ex) {
+ g_warning("Glade file loading failed for path effect dialog");
+ return;
+ }
+ Gtk::Box *LPEEffect;
+ Gtk::Box *LPEExpanderBox;
+ Gtk::Box *LPEActionButtons;
+ Gtk::EventBox *LPEOpenExpander;
+ Gtk::Expander *LPEExpander;
+ Gtk::Image *LPEIconImage;
+ Gtk::EventBox *LPEErase;
+ Gtk::EventBox *LPEHide;
+ Gtk::MenuItem *LPEtoggleFavorite;
+ Gtk::Label *LPENameLabel;
+ Gtk::Menu *LPEEffectMenu;
+ Gtk::MenuItem *LPEMoveUp;
+ Gtk::MenuItem *LPEMoveDown;
+ Gtk::MenuItem *LPEResetDefault;
+ Gtk::MenuItem *LPESetDefault;
+ builder->get_widget("LPEMoveUp", LPEMoveUp);
+ builder->get_widget("LPEMoveDown", LPEMoveDown);
+ builder->get_widget("LPEResetDefault", LPEResetDefault);
+ builder->get_widget("LPESetDefault", LPESetDefault);
+ builder->get_widget("LPENameLabel", LPENameLabel);
+ builder->get_widget("LPEEffectMenu", LPEEffectMenu);
+ builder->get_widget("LPEHide", LPEHide);
+ builder->get_widget("LPEIconImage", LPEIconImage);
+ builder->get_widget("LPEExpanderBox", LPEExpanderBox);
+ builder->get_widget("LPEEffect", LPEEffect);
+ builder->get_widget("LPEExpander", LPEExpander);
+ builder->get_widget("LPEOpenExpander", LPEOpenExpander);
+ builder->get_widget("LPEErase", LPEErase);
+ builder->get_widget("LPEDrag", LPEDrag);
+ builder->get_widget("LPEActionButtons", LPEActionButtons);
+ builder->get_widget("LPEtoggleFavorite", LPEtoggleFavorite);
+ LPEExpander->drag_dest_unset();
+ LPEActionButtons->drag_dest_unset();
+ LPEMoveUp->show();
+ LPEMoveDown->show();
+ LPEDrag->get_children()[0]->show();
+ LPEDrag->set_tooltip_text(_("Drag to change position in path effects stack"));
+ if (current) {
+ LPEExpanderCurrent = LPEExpander;
+ }
+ if (counter == 0) {
+ LPEMoveUpExtrem = LPEMoveUp;
+ }
+ LPEMoveDownExtrem = LPEMoveDown;
+ auto effectype = lpe->effectType();
+ const Glib::ustring label = _(converter.get_label(effectype).c_str());
+ const Glib::ustring untranslated_label = converter.get_label(effectype);
+ const Glib::ustring icon = converter.get_icon(effectype);
+ LPEIconImage->set_from_icon_name(icon, Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ Glib::ustring lpename = "";
+ if (untranslated_label == label) {
+ lpename = label;
+ } else {
+ lpename = (label + "\n<span size='x-small'>" + untranslated_label + "</span>");
+ }
+ auto *visimage = dynamic_cast<Gtk::Image *>(dynamic_cast<Gtk::Button *>(LPEHide->get_children()[0])->get_image());
+ if (!g_strcmp0(lpe->getRepr()->attribute("is_visible"),"true")) {
+ visimage->set_from_icon_name("object-visible-symbolic", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ } else {
+ visimage->set_from_icon_name("object-hidden-symbolic", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ }
+ _LPEExpanders.emplace_back(LPEExpander, (*it));
+ LPEListBox.add(*LPEEffect);
+
+ Glib::ustring name = "drag_";
+ name += Glib::ustring::format(counter);
+ LPEDrag->set_name(name);
+ if (total > 1) {
+ //DnD
+ LPEDrag->drag_source_set(entries, Gdk::BUTTON1_MASK, Gdk::ACTION_MOVE);
+ }
+ Glib::ustring tooltip = _(converter.get_description(effectype).c_str());
+ if (untranslated_label != label) {
+ tooltip = "[" + untranslated_label + "] " + _(converter.get_description(effectype).c_str());
+ }
+ gint id = (gint)effectype;
+ LPEExpanderBox->property_has_tooltip() = true;
+ LPEExpanderBox->signal_query_tooltip().connect([=](int x, int y, bool kbd, const Glib::RefPtr<Gtk::Tooltip>& tooltipw){
+ return sp_query_custom_tooltip(x, y, kbd, tooltipw, id, tooltip, icon);
+ });
+ size_t pos = 0;
+ std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> lperef = (*it);
+ for (auto w : LPEEffectMenu->get_children()) {
+ auto * mitem = dynamic_cast<Gtk::MenuItem *>(w);
+ if (mitem) {
+ mitem->signal_activate().connect([=](){
+ if (pos == 0) {
+ current_lpeitem->setCurrentPathEffect(lperef);
+ current_lpeitem->duplicateCurrentPathEffect();
+ effect_list_reload(current_lpeitem);
+ DocumentUndo::done(getDocument(), _("Duplicate path effect"), INKSCAPE_ICON("dialog-path-effects"));
+ } else if (pos == 1) {
+ current_lpeitem->setCurrentPathEffect(lperef);
+ current_lpeitem->upCurrentPathEffect();
+ effect_list_reload(current_lpeitem);
+ DocumentUndo::done(getDocument(), _("Move path effect up"), INKSCAPE_ICON("dialog-path-effects"));
+ } else if (pos == 2) {
+ current_lpeitem->setCurrentPathEffect(lperef);
+ current_lpeitem->downCurrentPathEffect();
+ effect_list_reload(current_lpeitem);
+ DocumentUndo::done(getDocument(), _("Move path effect down"), INKSCAPE_ICON("dialog-path-effects"));
+ } else if (pos == 3) {
+ lpeFlatten(lperef);
+ } else if (pos == 4) {
+ lpe->setDefaultParameters();
+ effect_list_reload(current_lpeitem);
+ } else if (pos == 5) {
+ lpe->resetDefaultParameters();
+ effect_list_reload(current_lpeitem);
+ } else if (pos == 6) {
+ sp_toggle_fav(untranslated_label, LPEtoggleFavorite);
+ _reload_menu = true;
+ _item_type = ""; // here we force reload even with the same tipe item selected
+ }
+
+ });
+ if (pos == 6) {
+ if (sp_has_fav(untranslated_label)) {
+ LPEtoggleFavorite->set_label(_("Unset Favorite"));
+ } else {
+ LPEtoggleFavorite->set_label(_("Set Favorite"));
+ }
+ }
+ }
+ pos ++;
+ }
+ if (total > 1) {
+ LPEDrag->signal_drag_begin().connect([=](const Glib::RefPtr<Gdk::DragContext> context){
+ cairo_surface_t *surface;
+ cairo_t *cr;
+ int x, y;
+ double sx = 1;
+ double sy = 1;
+ dnd = true;
+ Gtk::Allocation alloc = LPEEffect->get_allocation ();
+ auto device_scale = get_scale_factor();
+ surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, alloc.get_width() * device_scale, alloc.get_height() * device_scale);
+ cairo_surface_set_device_scale(surface, device_scale, device_scale);
+ cr = cairo_create (surface);
+ LPEEffect->get_style_context()->add_class("drag-icon");
+ gtk_widget_draw (GTK_WIDGET(LPEEffect->gobj()), cr);
+ LPEEffect->get_style_context()->remove_class("drag-icon");
+ LPEDrag->translate_coordinates(*LPEEffect, dndx, dndy, x, y);
+ #ifndef __APPLE__
+ cairo_surface_get_device_scale (surface, &sx, &sy);
+ #endif
+ cairo_surface_set_device_offset (surface, -x * sx, -y * sy);
+ gtk_drag_set_icon_surface (context->gobj(), surface);
+ cairo_destroy (cr);
+ cairo_surface_destroy (surface);
+ });
+ auto row = dynamic_cast<Gtk::ListBoxRow *>(LPEEffect->get_parent());
+ LPEDrag->signal_drag_data_get().connect([=](const Glib::RefPtr<Gdk::DragContext>& context, Gtk::SelectionData& selection_data, guint info, guint time)
+ {
+ selection_data.set("GTK_LIST_BOX_ROW", Glib::ustring::format(row->get_index()));
+ });
+ LPEDrag->signal_drag_end().connect([=](const Glib::RefPtr<Gdk::DragContext>& context)
+ {
+ dnd = false;
+ });
+ row->signal_drag_data_received().connect([=](const Glib::RefPtr<Gdk::DragContext>& context, int x, int y, const Gtk::SelectionData& selection_data, guint info, guint time)
+ {
+ if (dnd) {
+ unsigned int pos_target, pos_source;
+ Gtk::Widget *target = row;
+ pos_target = row->get_index();
+ pos_source = atoi(reinterpret_cast<char const*>(selection_data.get_data()));
+ Glib::RefPtr<Gtk::StyleContext> stylec = target->get_style_context();
+ if (pos_source > pos_target) {
+ if (stylec->has_class("after")) {
+ pos_target ++;
+ }
+ } else if (pos_source < pos_target) {
+ if (stylec->has_class("before")) {
+ pos_target --;
+ }
+ }
+ Gtk::Widget *source = LPEListBox.get_row_at_index(pos_source);
+ if (source == target) {
+ gtk_drag_finish(context->gobj(), FALSE, FALSE, time);
+ dnd = false;
+ return;
+ }
+ g_object_ref(source->gobj());
+ LPEListBox.remove(*source);
+ LPEListBox.insert(*source, pos_target);
+ g_object_unref(source->gobj());
+ move_list(pos_source,pos_target);
+ gtk_drag_finish(context->gobj(), TRUE, TRUE, time);
+ dnd = false;
+ }
+ });
+ row->drag_dest_set(entries, Gtk::DEST_DEFAULT_ALL, Gdk::ACTION_MOVE);
+ row->signal_drag_motion().connect([=](const Glib::RefPtr<Gdk::DragContext>& context, int x, int y, guint time)
+ {
+ gint half = row->get_allocated_height()/2;
+ Glib::RefPtr<Gtk::StyleContext> stylec = row->get_style_context();
+ if (y < half) {
+ stylec->add_class("before");
+ stylec->remove_class("after");
+ } else {
+ stylec->remove_class("before");
+ stylec->add_class("after");
+ }
+ return true;
+ }, true);
+ }
+ // other
+ LPEEffect->set_name("LPEEffectItem");
+ LPENameLabel->set_label(g_dpgettext2(nullptr, "path effect", (*it)->lpeobject->get_lpe()->getName().c_str()));
+ LPEExpander->property_expanded().signal_changed().connect(sigc::bind(sigc::mem_fun(*this, &LivePathEffectEditor::expanded_notify),LPEExpander));
+ LPEOpenExpander->signal_button_press_event().connect([=](GdkEventButton* const evt){
+ LPEExpander->set_expanded(!LPEExpander->property_expanded());
+ return false;
+ }, false);
+ dynamic_cast<Gtk::Button *>(LPEHide->get_children()[0])->signal_clicked().connect(sigc::bind<Inkscape::LivePathEffect::Effect *, Gtk::EventBox *>(sigc::mem_fun(*this, &LivePathEffectEditor::toggleVisible), lpe, LPEHide));
+ LPEDrag->signal_button_press_event().connect([=](GdkEventButton* const evt){dndx = evt->x; dndy = evt->y; return false; }, false);
+ dynamic_cast<Gtk::Button *>(LPEErase->get_children()[0])->signal_clicked().connect([=](){ removeEffect(LPEExpander);});
+ if (total > 1) {
+ LPEDrag->signal_enter_notify_event().connect([=](GdkEventCrossing*){
+ auto window = get_window();
+ auto display = get_display();
+ auto cursor = Gdk::Cursor::create(display, "grab");
+ window->set_cursor(cursor);
+ return false;
+ }, false);
+ LPEDrag->signal_leave_notify_event().connect([=](GdkEventCrossing*){
+ auto window = get_window();
+ auto display = get_display();
+ auto cursor = Gdk::Cursor::create(display, "default");
+ window->set_cursor(cursor);
+ return false;
+ }, false);
+ }
+ if (lpe->hasDefaultParameters()) {
+ LPEResetDefault->show();
+ LPESetDefault->hide();
+
+ } else {
+ LPEResetDefault->hide();
+ LPESetDefault->show();
+ }
+ }
+ }
+ if (counter == 0 && LPEDrag) {
+ LPEDrag->get_children()[0]->hide();
+ LPEDrag->set_tooltip_text("");
+ }
+ if (LPEMoveUpExtrem) {
+ LPEMoveUpExtrem->hide();
+ LPEMoveDownExtrem->hide();
+ }
+ if (LPEExpanderCurrent) {
+ _LPESelectionInfo.hide();
+ LPEExpanderCurrent->set_expanded(true);
+ Gtk::Window *current_window = dynamic_cast<Gtk::Window *>(LPEExpanderCurrent->get_toplevel());
+ if (current_window) {
+ current_window->set_focus(*LPEExpanderCurrent);
+ }
+ }
+ selection_info();
+ LPEListBox.show_all_children();
+ ensure_size();
+}
+
+void LivePathEffectEditor::expanded_notify(Gtk::Expander *expander) {
+
+ if (updating) {
+ return;
+ }
+ if (!dnd) {
+ _freezeexpander = false;
+ }
+ if (_freezeexpander) {
+ _freezeexpander = false;
+ return;
+ }
+ if (dnd) {
+ _freezeexpander = true;
+ expander->set_expanded(!expander->get_expanded());
+ return;
+ };
+ updating = true;
+ if (expander->get_expanded()) {
+ for (auto &w : _LPEExpanders){
+ if (w.first == expander) {
+ w.first->set_expanded(true);
+ w.first->get_parent()->get_parent()->get_parent()->set_name("currentlpe");
+ current_lperef = w;
+ current_lpeitem->setCurrentPathEffect(w.second);
+ showParams(w, true);
+ } else {
+ w.first->set_expanded(false);
+ w.first->get_parent()->get_parent()->get_parent()->set_name("unactive_lpe");
+ }
+ }
+ }
+ auto selection = SP_ACTIVE_DESKTOP->getSelection();
+ if (selection && current_lpeitem && !selection->isEmpty()) {
+ selection_changed_lock = true;
+ selection->clear();
+ selection->add(current_lpeitem);
+ Inkscape::UI::Tools::sp_update_helperpath(getDesktop());
+ selection_changed_lock = false;
+ }
+ updating = false;
+}
+
+bool
+LivePathEffectEditor::lpeFlatten(std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> lperef)
+{
+ current_lpeitem->setCurrentPathEffect(lperef);
+ current_lpeitem = current_lpeitem->flattenCurrentPathEffect();
+ auto selection = getSelection();
+ if (selection && selection->isEmpty() ) {
+ selection->add(current_lpeitem);
+ }
+ DocumentUndo::done(getDocument(), _("Flatten path effect(s)"), INKSCAPE_ICON("dialog-path-effects"));
+ return false;
+}
+
+void
+LivePathEffectEditor::removeEffect(Gtk::Expander * expander) {
+ bool reload = current_lperef.first != expander;
+ auto current_lperef_tmp = current_lperef;
+ for (auto &w : _LPEExpanders){
+ if (w.first == expander) {
+ current_lpeitem->setCurrentPathEffect(w.second);
+ current_lpeitem = current_lpeitem->removeCurrentPathEffect(false);
+ }
+ }
+ if (reload) {
+ current_lpeitem->setCurrentPathEffect(current_lperef_tmp.second);
+ }
+ effect_list_reload(current_lpeitem);
+ DocumentUndo::done(getDocument(), _("Remove path effect"), INKSCAPE_ICON("dialog-path-effects"));
+}
+
+bool
+LivePathEffectEditor::toggleFavInLpe(GdkEventButton * evt, Glib::ustring name, Gtk::Button *favbutton) {
+ auto *favimage = dynamic_cast<Gtk::Image *>(favbutton->get_image());
+ if (favimage->get_icon_name() == "draw-star") {
+ favbutton->set_image_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ sp_remove_fav(name);
+ } else {
+ favbutton->set_image_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ sp_add_fav(name);
+ }
+ clearMenu();
+ return false;
+}
+
+
+
+
+
+/*
+ * Clears the effectlist
+ */
+void
+LivePathEffectEditor::clear_lpe_list()
+{
+ for (auto &w : LPEListBox.get_children()) {
+ LPEListBox.remove(*w);
+ }
+ for (auto &w : _LPEParentBox.get_children()) {
+ _LPEParentBox.remove(*w);
+ }
+ for (auto &w : _LPECurrentItem.get_children()) {
+ _LPECurrentItem.remove(*w);
+ }
+}
+
+SPLPEItem * LivePathEffectEditor::clonetolpeitem()
+{
+ auto selection = getSelection();
+ if (selection && !selection->isEmpty() ) {
+ auto use = cast<SPUse>(selection->singleItem());
+ if ( use ) {
+ DocumentUndo::ScopedInsensitive tmp(getDocument());
+ // 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 ( is<SPShape>(orig) || is<SPGroup>(orig) || is<SPText>(orig) ) {
+ // select original
+ selection->set(orig);
+
+ // delete clone but remember its id and transform
+ auto id_copy = Util::to_opt(use->getAttribute("id"));
+ auto transform_copy = Util::to_opt(use->getAttribute("transform"));
+ use->deleteObject(false);
+ use = nullptr;
+
+ // run sp_selection_clone_original_path_lpe
+ selection->cloneOriginalPathLPE(true, true, true);
+
+ SPItem *new_item = selection->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", Util::to_cstr(id_copy));
+ if (Util::to_cstr(transform_copy)) {
+ Geom::Affine item_t(Geom::identity());
+ sp_svg_transform_read(Util::to_cstr(transform_copy), &item_t);
+ new_item->transform *= item_t;
+ new_item->doWriteTransform(new_item->transform);
+ new_item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ }
+ new_item->setAttribute("class", "fromclone");
+ }
+
+ auto *lpeitem = cast<SPLPEItem>(new_item);
+ if (lpeitem) {
+ sp_lpe_item_update_patheffect(lpeitem, true, true);
+ return lpeitem;
+ }
+ }
+ }
+ }
+ return nullptr;
+}
+
+void LivePathEffectEditor::onAddGallery()
+{
+ // show effectlist dialog
+ using Inkscape::UI::Dialog::LivePathEffectAdd;
+ LivePathEffectAdd::show(getDesktop());
+ clearMenu();
+ if ( !LivePathEffectAdd::isApplied()) {
+ return;
+ }
+
+ const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *data = LivePathEffectAdd::getActiveData();;
+ if (!data) {
+ return;
+ }
+ selection_changed_lock = true;
+ SPLPEItem *fromclone = clonetolpeitem();
+ if (fromclone) {
+ current_lpeitem = fromclone;
+ if (data->key == "clone_original") {
+ current_lpeitem->getCurrentLPE()->refresh_widgets = true;
+ selection_changed_lock = false;
+ DocumentUndo::done(getDocument(), _("Create and apply path effect"), INKSCAPE_ICON("dialog-path-effects"));
+ return;
+ }
+
+ }
+ selection_changed_lock = false;
+ if (current_lpeitem) {
+ LivePathEffect::Effect::createAndApply(data->key.c_str(), getDocument(), current_lpeitem);
+ current_lpeitem->getCurrentLPE()->refresh_widgets = true;
+ DocumentUndo::done(getDocument(), _("Create and apply path effect"), INKSCAPE_ICON("dialog-path-effects"));
+ }
+}
+
+void LivePathEffectEditor::on_showgallery_notify(Preferences::Entry const &new_val)
+{
+ _LPEGallery.set_visible(new_val.getBool());
+}
+
+} // 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..8169270
--- /dev/null
+++ b/src/ui/dialog/livepatheffect-editor.h
@@ -0,0 +1,127 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief A dialog for Live Path Effects (LPE)
+ */
+/* Authors:
+ * Jabiertxof
+ * Adam Belis (UX/Design)
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef LIVEPATHEFFECTEDITOR_H
+#define LIVEPATHEFFECTEDITOR_H
+
+#include <memory>
+#include <gtkmm/builder.h>
+#include "live_effects/effect-enum.h"
+#include "preferences.h"
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/completion-popup.h"
+#include "ui/column-menu-builder.h"
+
+namespace Gtk {
+class Button;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/*
+ * @brief The LivePathEffectEditor class
+ */
+class LivePathEffectEditor : public DialogBase
+{
+public:
+ // No default constructor, noncopyable, nonassignable
+ LivePathEffectEditor();
+ ~LivePathEffectEditor() override;
+ LivePathEffectEditor(LivePathEffectEditor const &d) = delete;
+ LivePathEffectEditor operator=(LivePathEffectEditor const &d) = delete;
+ static LivePathEffectEditor &getInstance() { return *new LivePathEffectEditor(); }
+ void move_list(gint origin, gint dest);
+ std::vector<std::pair<Gtk::Expander *, std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> > > _LPEExpanders;
+ void showParams(std::pair<Gtk::Expander *, std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> > expanderdata, bool changed);
+ bool updating = false;
+ SPLPEItem *current_lpeitem = nullptr;
+ std::pair<Gtk::Expander *, std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> > current_lperef = std::make_pair(nullptr, nullptr);
+ static const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *getActiveData();
+ bool selection_changed_lock = false;
+ bool dnd = false;
+private:
+ Glib::RefPtr<Gtk::Builder> _builder;
+public:
+ Gtk::ListBox& LPEListBox;
+ gint dndx = 0;
+ gint dndy = 0;
+protected:
+ bool apply(GdkEventButton *evt, Glib::RefPtr<Gtk::Builder> builder_effect,
+ const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *to_add);
+ void reload_effect_list();
+ void onButtonEvent(GdkEventButton* evt);
+
+private:
+ void add_lpes(Inkscape::UI::Widget::CompletionPopup& popup, bool symbolic);
+ void clear_lpe_list();
+ void selectionChanged(Inkscape::Selection *selection) override;
+ void selectionModified(Inkscape::Selection *selection, guint flags) override;
+ void onSelectionChanged(Inkscape::Selection *selection);
+ bool toggleFavInLpe(GdkEventButton * evt, Glib::ustring name, Gtk::Button *favbutton);
+ bool closeExpander(GdkEventButton * evt);
+ void onAddGallery();
+ void expanded_notify(Gtk::Expander *expander);
+ void onAdd(Inkscape::LivePathEffect::EffectType etype);
+ void toggleVisible(Inkscape::LivePathEffect::Effect *lpe , Gtk::EventBox *visbutton);
+ bool is_appliable(LivePathEffect::EffectType etypen, Glib::ustring item_type, bool has_clip, bool has_mask);
+ void removeEffect(Gtk::Expander * expander);
+ void effect_list_reload(SPLPEItem *lpeitem);
+
+ SPLPEItem * clonetolpeitem();
+ void selection_info();
+ Inkscape::UI::Widget::CompletionPopup _lpes_popup;
+ void map_handler();
+ void clearMenu();
+ void setMenu();
+ bool lpeFlatten(std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> lperef);
+ Gtk::Box& _LPEContainer;
+ Gtk::Box& _LPEAddContainer;
+ Gtk::Label&_LPESelectionInfo;
+ Gtk::ListBox&_LPEParentBox;
+ Gtk::Box&_LPECurrentItem;
+ PathEffectList effectlist;
+ Glib::RefPtr<Gtk::ListStore> _LPEList;
+ Glib::RefPtr<Gtk::ListStore> _LPEListFilter;
+ const LivePathEffect::EnumEffectDataConverter<LivePathEffect::EffectType> &converter;
+ Gtk::Widget *effectwidget = nullptr;
+ Gtk::Widget *popupwidg = nullptr;
+ GtkWidget *currentdrag = nullptr;
+ bool _reload_menu = false;
+ gint _buttons_width = 0;
+ bool _freezeexpander = false;
+ Glib::ustring _item_type;
+ bool _has_clip;
+ bool _has_mask;
+ bool _frezee = false;
+
+ Gtk::Button &_LPEGallery;
+ std::unique_ptr<Preferences::PreferencesObserver> const _showgallery_observer;
+ void on_showgallery_notify(Preferences::Entry const &new_val);
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // LIVEPATHEFFECTEDITOR_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..f2a2655
--- /dev/null
+++ b/src/ui/dialog/lpe-fillet-chamfer-properties.cpp
@@ -0,0 +1,255 @@
+// 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()
+ : _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 the max allowable 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 the max allowable 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()
+{
+}
+
+void FilletChamferPropertiesDialog::showDialog(SPDesktop *desktop, double _amount,
+ const Inkscape::LivePathEffect::FilletChamferKnotHolderEntity *pt,
+ bool _use_distance, bool _aprox_radius, NodeSatellite _nodesatellite)
+{
+ FilletChamferPropertiesDialog *dialog = new FilletChamferPropertiesDialog();
+
+ dialog->_setUseDistance(_use_distance);
+ dialog->_setAprox(_aprox_radius);
+ dialog->_setAmount(_amount);
+ dialog->_setNodeSatellite(_nodesatellite);
+ 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) {
+ _nodesatellite.nodesatellite_type = FILLET;
+ } else if (_fillet_chamfer_type_inverse_fillet.get_active() == true) {
+ _nodesatellite.nodesatellite_type = INVERSE_FILLET;
+ } else if (_fillet_chamfer_type_inverse_chamfer.get_active() == true) {
+ _nodesatellite.nodesatellite_type = INVERSE_CHAMFER;
+ } else {
+ _nodesatellite.nodesatellite_type = CHAMFER;
+ }
+ if (_flexible) {
+ if (d_pos > 99.99999 || d_pos < 0) {
+ d_pos = 0;
+ }
+ d_pos = d_pos / 100;
+ }
+ _nodesatellite.amount = d_pos;
+ size_t steps = (size_t)_fillet_chamfer_chamfer_subdivisions.get_value();
+ if (steps < 1) {
+ steps = 1;
+ }
+ _nodesatellite.steps = steps;
+ _knotpoint->knot_set_offset(_nodesatellite);
+ }
+ _close();
+}
+
+void FilletChamferPropertiesDialog::_close()
+{
+ 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::_setNodeSatellite(NodeSatellite nodesatellite)
+{
+ 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 (nodesatellite.is_time) {
+ position = _amount * 100;
+ _flexible = true;
+ _fillet_chamfer_position_label.set_label(_("Position (%):"));
+ } else {
+ _flexible = false;
+ auto 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(nodesatellite.steps);
+ if (nodesatellite.nodesatellite_type == FILLET) {
+ _fillet_chamfer_type_fillet.set_active(true);
+ } else if (nodesatellite.nodesatellite_type == INVERSE_FILLET) {
+ _fillet_chamfer_type_inverse_fillet.set_active(true);
+ } else if (nodesatellite.nodesatellite_type == CHAMFER) {
+ _fillet_chamfer_type_chamfer.set_active(true);
+ } else if (nodesatellite.nodesatellite_type == INVERSE_CHAMFER) {
+ _fillet_chamfer_type_inverse_chamfer.set_active(true);
+ }
+ _nodesatellite = nodesatellite;
+}
+
+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;
+}
+
+} // 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..99e446f
--- /dev/null
+++ b/src/ui/dialog/lpe-fillet-chamfer-properties.h
@@ -0,0 +1,110 @@
+// 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/nodesatellitesarray.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, NodeSatellite _nodesatellite);
+
+protected:
+
+ 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 _setPt(const Inkscape::LivePathEffect::
+ FilletChamferKnotHolderEntity *pt);
+ void _setUseDistance(bool use_knot_distance);
+ void _setAprox(bool aprox_radius);
+ void _setAmount(double amount);
+ void _setNodeSatellite(NodeSatellite nodesatellite);
+ void _prepareLabelRenderer(Gtk::TreeModel::const_iterator const &row);
+
+ bool _handleKeyEvent(GdkEventKey *event);
+ void _handleButtonEvent(GdkEventButton *event);
+
+ void _apply();
+ void _close();
+ bool _flexible;
+ NodeSatellite _nodesatellite;
+ 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..cd5fa28
--- /dev/null
+++ b/src/ui/dialog/lpe-powerstroke-properties.cpp
@@ -0,0 +1,184 @@
+// 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()
+ : _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() {
+}
+
+void PowerstrokePropertiesDialog::showDialog(SPDesktop *desktop, Geom::Point knotpoint, const Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity *pt)
+{
+ PowerstrokePropertiesDialog *dialog = new PowerstrokePropertiesDialog();
+
+ 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()
+{
+ 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);
+}
+
+} // 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..161d1fc
--- /dev/null
+++ b/src/ui/dialog/lpe-powerstroke-properties.h
@@ -0,0 +1,90 @@
+// 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:
+
+ 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 _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..a28c065
--- /dev/null
+++ b/src/ui/dialog/memory.cpp
@@ -0,0 +1,212 @@
+// 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 "util/format_size.h"
+
+using Inkscape::Util::format_size;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+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()
+ : DialogBase("/dialogs/memory", "Memory")
+ , _private(std::make_unique<Private>())
+{
+ // Private conf
+ pack_start(_private->view);
+
+ _private->update();
+
+ signal_show().connect(sigc::mem_fun(*_private, &Private::start_update_task));
+ signal_hide().connect(sigc::mem_fun(*_private, &Private::stop_update_task));
+
+ // Add button
+ auto button = Gtk::make_managed<Gtk::Button>(_("Recalculate"));
+ button->signal_button_press_event().connect(sigc::mem_fun(*this, &Memory::_apply));
+
+ auto button_box = Gtk::make_managed<Gtk::ButtonBox>();
+ button_box->set_layout(Gtk::BUTTONBOX_END);
+ button_box->set_spacing(6);
+ button_box->set_border_width(4);
+ button_box->pack_end(*button);
+ pack_end(*button_box, Gtk::PACK_SHRINK, 0);
+
+ // Start
+ _private->start_update_task();
+
+ show_all_children();
+}
+
+Memory::~Memory()
+{
+ _private->stop_update_task();
+}
+
+bool Memory::_apply(GdkEventButton*)
+{
+ GC::Core::gcollect();
+ _private->update();
+ return 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/dialog/memory.h b/src/ui/dialog/memory.h
new file mode 100644
index 0000000..5975fc2
--- /dev/null
+++ b/src/ui/dialog/memory.h
@@ -0,0 +1,51 @@
+// 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 <memory>
+#include "ui/dialog/dialog-base.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class Memory : public DialogBase
+{
+public:
+ Memory();
+ ~Memory() override;
+
+protected:
+ bool _apply(GdkEventButton *);
+
+private:
+ struct Private;
+ std::unique_ptr<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..3e29501
--- /dev/null
+++ b/src/ui/dialog/messages.cpp
@@ -0,0 +1,214 @@
+// 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"
+
+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()
+ : DialogBase("/dialogs/messages", "Messages")
+ , buttonClear(_("_Clear"), _("Clear log messages"))
+ , checkCapture(_("Capture log messages"), _("Capture log messages"))
+ , buttonBox(Gtk::ORIENTATION_HORIZONTAL)
+{
+ /*
+ * 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);
+ pack_start(textScroll);
+
+ buttonBox.set_spacing(6);
+ buttonBox.pack_start(checkCapture, true, true, 6);
+ buttonBox.pack_end(buttonClear, false, false, 10);
+ pack_start(buttonBox, Gtk::PACK_SHRINK);
+
+ // sick of this thing shrinking too much
+ set_size_request(400, -1);
+
+ 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..78b5f33
--- /dev/null
+++ b/src/ui/dialog/messages.h
@@ -0,0 +1,96 @@
+// 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 <glibmm/i18n.h>
+#include <gtkmm/box.h>
+#include <gtkmm/button.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/menu.h>
+#include <gtkmm/menubar.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/textview.h>
+
+#include "ui/dialog/dialog-base.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class Messages : public DialogBase
+{
+public:
+ Messages();
+ ~Messages() override;
+
+ /**
+ * 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::Box buttonBox;
+ Gtk::Button buttonClear;
+ Gtk::CheckButton checkCapture;
+
+ //Handler ID's
+ guint handlerDefault;
+ guint handlerGlibmm;
+ guint handlerAtkmm;
+ guint handlerPangomm;
+ guint handlerGdkmm;
+ guint handlerGtkmm;
+};
+
+} //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..d5fa843
--- /dev/null
+++ b/src/ui/dialog/new-from-template.cpp
@@ -0,0 +1,90 @@
+// 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 <glibmm/i18n.h>
+
+#include "desktop.h"
+#include "file.h"
+#include "inkscape-application.h"
+#include "inkscape-window.h"
+#include "inkscape.h"
+#include "object/sp-namedview.h"
+#include "ui/widget/template-list.h"
+
+namespace Inkscape {
+namespace UI {
+
+
+NewFromTemplate::NewFromTemplate()
+ : _create_template_button(_("Create from template"))
+{
+ set_title(_("New From Template"));
+ resize(750, 500);
+
+ templates = Gtk::manage(new Inkscape::UI::Widget::TemplateList());
+ get_content_area()->pack_start(*templates);
+ templates->init(Inkscape::Extension::TEMPLATE_NEW_FROM);
+
+ _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);
+
+ templates->connectItemSelected([=]() { _create_template_button.set_sensitive(true); });
+ templates->connectItemActivated(sigc::mem_fun(*this, &NewFromTemplate::_createFromTemplate));
+ templates->signal_switch_page().connect([=](Gtk::Widget *const widget, int num) {
+ _create_template_button.set_sensitive(templates->has_selected_preset());
+ });
+
+ show_all();
+}
+
+void NewFromTemplate::_createFromTemplate()
+{
+ SPDesktop *old_desktop = SP_ACTIVE_DESKTOP;
+
+ auto doc = templates->new_document();
+
+ // Cancel button was pressed.
+ if (!doc)
+ return;
+
+ auto app = InkscapeApplication::instance();
+ InkscapeWindow *win = app->window_open(doc);
+ SPDesktop *new_desktop = win->get_desktop();
+ sp_namedview_window_from_document(new_desktop);
+
+ if (old_desktop)
+ old_desktop->clearWaitingCursor();
+
+ _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..f286c21
--- /dev/null
+++ b/src/ui/dialog/new-from-template.h
@@ -0,0 +1,43 @@
+// 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>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+class TemplateList;
+}
+
+class NewFromTemplate : public Gtk::Dialog
+{
+
+friend class TemplateLoadTab;
+public:
+ static void load_new_from_template();
+ ~NewFromTemplate() override{};
+
+private:
+ NewFromTemplate();
+ Gtk::Button _create_template_button;
+ Inkscape::UI::Widget::TemplateList *templates;
+
+ 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..dd5d8e4
--- /dev/null
+++ b/src/ui/dialog/object-attributes.cpp
@@ -0,0 +1,760 @@
+// 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 <2geom/rect.h>
+#include <cmath>
+#include <cstddef>
+#include <glibmm/i18n.h>
+#include <glibmm/markup.h>
+#include <glibmm/ustring.h>
+#include <gtkmm/button.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/label.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/spinbutton.h>
+#include <gtkmm/widget.h>
+#include <memory>
+#include <optional>
+#include <string>
+#include <tuple>
+#include "actions/actions-tools.h"
+#include "desktop.h"
+#include "live_effects/effect-enum.h"
+#include "mod360.h"
+#include "object/sp-anchor.h"
+#include "object/sp-ellipse.h"
+#include "object/sp-image.h"
+#include "object/sp-lpe-item.h"
+#include "object/sp-namedview.h"
+#include "object/sp-object.h"
+#include "object/sp-path.h"
+#include "object/sp-rect.h"
+#include "object/sp-star.h"
+#include "object/tags.h"
+#include "streq.h"
+#include "ui/builder-utils.h"
+#include "ui/dialog/object-attributes.h"
+#include "ui/icon-names.h"
+#include "ui/tools/node-tool.h"
+#include "ui/util.h"
+#include "ui/widget/image-properties.h"
+#include "ui/widget/spinbutton.h"
+#include "ui/widget/style-swatch.h"
+#include "widgets/sp-attribute-widget.h"
+#include "xml/href-attribute-helper.h"
+#include "live_effects/lpeobject-reference.h"
+#include "live_effects/lpeobject.h"
+#include "live_effects/effect.h"
+
+namespace Inkscape {
+namespace UI {
+
+void sp_apply_lpeffect(SPDesktop* desktop, SPLPEItem* item, LivePathEffect::EffectType etype);
+
+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}
+};
+
+ObjectAttributes::ObjectAttributes()
+ : DialogBase("/dialogs/objectattr/", "ObjectAttributes"),
+ _builder(create_builder("object-attributes.glade")),
+ _main_panel(get_widget<Gtk::Box>(_builder, "main-panel")),
+ _obj_title(get_widget<Gtk::Label>(_builder, "main-obj-name")),
+ _style_swatch(nullptr, _("Item's fill, stroke and opacity"), Gtk::ORIENTATION_HORIZONTAL)
+{
+ auto& main = get_widget<Gtk::Box>(_builder, "main-widget");
+ _obj_title.set_text("");
+ _style_swatch.set_hexpand(false);
+ _style_swatch.set_valign(Gtk::ALIGN_CENTER);
+ get_widget<Gtk::Box>(_builder, "main-header").pack_end(_style_swatch, false, true);
+ add(main);
+ create_panels();
+ _style_swatch.hide();
+}
+
+void ObjectAttributes::widget_setup() {
+ if (_update.pending() || !getDesktop()) return;
+
+ auto selection = getDesktop()->getSelection();
+ auto item = selection->singleItem();
+
+ auto scoped(_update.block());
+
+ auto panel = get_panel(item);
+ if (panel != _current_panel && _current_panel) {
+ _current_panel->update_panel(nullptr, nullptr);
+ _main_panel.remove(_current_panel->widget());
+ _obj_title.set_text("");
+ }
+
+ _current_panel = panel;
+ _current_item = nullptr;
+
+ Glib::ustring title = panel ? panel->get_title() : "";
+ if (!panel) {
+ if (item) {
+ if (auto name = item->displayName()) {
+ title = name;
+ }
+ }
+ else if (selection->size() > 1) {
+ title = _("Multiple objects selected");
+ }
+ }
+ _obj_title.set_markup("<b>" + Glib::Markup::escape_text(title) + "</b>");
+
+ if (!panel) {
+ _style_swatch.hide();
+ return;
+ }
+
+ _main_panel.pack_start(panel->widget(), true, true);
+ bool show_style = false;
+ if (panel->supports_fill_stroke()) {
+ if (auto style = item ? item->style : nullptr) {
+ _style_swatch.setStyle(style);
+ show_style = true;
+ }
+ }
+ widget_show(_style_swatch, show_style);
+ panel->update_panel(item, getDesktop());
+ panel->widget().show();
+ _current_item = item;
+
+ // TODO
+ // show no of LPEs?
+ // show locked status?
+}
+
+void ObjectAttributes::update_panel(SPObject* item) {
+ if (!_current_panel) return;
+
+ if (_current_panel->supports_fill_stroke()) {
+ if (auto style = item ? item->style : nullptr) {
+ _style_swatch.setStyle(style);
+ }
+ }
+ _current_panel->update_panel(item, getDesktop());
+}
+
+void ObjectAttributes::desktopReplaced() {
+ //todo; if anything
+}
+
+void ObjectAttributes::selectionChanged(Selection* selection) {
+ widget_setup();
+}
+
+void ObjectAttributes::selectionModified(Selection* _selection, guint flags) {
+ if (_update.pending() || !getDesktop() || !_current_panel) return;
+
+ auto selection = getDesktop()->getSelection();
+ if (flags & (SP_OBJECT_MODIFIED_FLAG |
+ SP_OBJECT_PARENT_MODIFIED_FLAG |
+ SP_OBJECT_STYLE_MODIFIED_FLAG)) {
+
+ auto item = selection->singleItem();
+ if (item == _current_item) {
+ update_panel(item);
+ }
+ else {
+ g_warning("ObjectAttributes: missed selection change?");
+ }
+ }
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
+std::tuple<bool, double, double> round_values(double x, double y) {
+ auto a = std::round(x);
+ auto b = std::round(y);
+ return std::make_tuple(a != x || b != y, a, b);
+}
+
+std::tuple<bool, double, double> round_values(Gtk::SpinButton& x, Gtk::SpinButton& y) {
+ return round_values(x.get_adjustment()->get_value(), y.get_adjustment()->get_value());
+}
+
+const LivePathEffectObject* find_lpeffect(SPLPEItem* item, LivePathEffect::EffectType etype) {
+ if (!item) return nullptr;
+
+ auto lpe = item->getFirstPathEffectOfType(Inkscape::LivePathEffect::FILLET_CHAMFER);
+ if (!lpe) return nullptr;
+ return lpe->getLPEObj();
+}
+
+void remove_lpeffect(SPLPEItem* item, LivePathEffect::EffectType type) {
+ if (auto effect = find_lpeffect(item, type)) {
+ item->setCurrentPathEffect(effect);
+ item->removeCurrentPathEffect(false);
+ DocumentUndo::done(item->document, _("Removed live path effect"), INKSCAPE_ICON("dialog-path-effects"));
+ }
+}
+
+std::optional<double> get_number(SPItem* item, const char* attribute) {
+ if (!item) return {};
+
+ auto val = item->getAttribute(attribute);
+ if (!val) return {};
+
+ return item->getRepr()->getAttributeDouble(attribute);
+}
+
+void align_star_shape(SPStar* path) {
+ if (!path || !path->sides) return;
+
+ auto arg1 = path->arg[0];
+ auto arg2 = path->arg[1];
+ auto delta = arg2 - arg1;
+ auto top = -M_PI / 2;
+ auto odd = path->sides & 1;
+ if (odd) {
+ arg1 = top;
+ }
+ else {
+ arg1 = top - M_PI / path->sides;
+ }
+ arg2 = arg1 + delta;
+
+ path->setAttributeDouble("sodipodi:arg1", arg1);
+ path->setAttributeDouble("sodipodi:arg2", arg2);
+ path->updateRepr();
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
+details::AttributesPanel::AttributesPanel() {
+ _tracker = std::make_unique<UI::Widget::UnitTracker>(Inkscape::Util::UNIT_TYPE_LINEAR);
+ //todo:
+ // auto init_units = desktop->getNamedView()->display_units;
+ // _tracker->setActiveUnit(init_units);
+}
+
+void details::AttributesPanel::update_panel(SPObject* object, SPDesktop* desktop) {
+ if (object) {
+ auto scoped(_update.block());
+ auto units = object->document->getNamedView() ? object->document->getNamedView()->display_units : nullptr;
+ // auto init_units = desktop->getNamedView()->display_units;
+ if (units) _tracker->setActiveUnit(units);
+ }
+
+ _desktop = desktop;
+
+ if (!_update.pending()) {
+ update(object);
+ }
+}
+
+void details::AttributesPanel::change_value_px(SPObject* object, const Glib::RefPtr<Gtk::Adjustment>& adj, const char* attr, std::function<void (double)>&& setter) {
+ if (_update.pending() || !object) return;
+
+ auto scoped(_update.block());
+
+ const auto unit = _tracker->getActiveUnit();
+ auto value = Util::Quantity::convert(adj->get_value(), unit, "px");
+ if (value != 0 || attr == nullptr) {
+ setter(value);
+ }
+ else if (attr) {
+ object->removeAttribute(attr);
+ }
+
+ DocumentUndo::done(object->document, _("Change object attribute"), ""); //TODO INKSCAPE_ICON("draw-rectangle"));
+}
+
+void details::AttributesPanel::change_angle(SPObject* object, const Glib::RefPtr<Gtk::Adjustment>& adj, std::function<void (double)>&& setter) {
+ if (_update.pending() || !object) return;
+
+ auto scoped(_update.block());
+
+ auto value = degree_to_radians_mod2pi(adj->get_value());
+ setter(value);
+
+ DocumentUndo::done(object->document, _("Change object attribute"), ""); //TODO INKSCAPE_ICON("draw-rectangle"));
+}
+
+void details::AttributesPanel::change_value(SPObject* object, const Glib::RefPtr<Gtk::Adjustment>& adj, std::function<void (double)>&& setter) {
+ if (_update.pending() || !object) return;
+
+ auto scoped(_update.block());
+
+ auto value = adj ? adj->get_value() : 0;
+ setter(value);
+
+ DocumentUndo::done(object->document, _("Change object attribute"), ""); //TODO INKSCAPE_ICON("draw-rectangle"));
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
+class ImagePanel : public details::AttributesPanel {
+public:
+ ImagePanel() {
+ _title = _("Image");
+ _show_fill_stroke = false;
+ _panel = std::make_unique<Inkscape::UI::Widget::ImageProperties>();
+ _widget = _panel.get();
+ }
+ ~ImagePanel() override = default;
+
+ void update(SPObject* object) override { _panel->update(cast<SPImage>(object)); }
+
+private:
+ std::unique_ptr<Inkscape::UI::Widget::ImageProperties> _panel;
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+class AnchorPanel : public details::AttributesPanel {
+public:
+ AnchorPanel() {
+ _title = _("Anchor");
+ _show_fill_stroke = false;
+ _table = std::make_unique<SPAttributeTable>();
+ _table->show();
+ _table->set_hexpand();
+ _table->set_vexpand(false);
+ _widget = _table.get();
+ }
+ ~AnchorPanel() override = default;
+
+ void update(SPObject* object) override {
+ auto anchor = cast<SPAnchor>(object);
+ auto changed = _anchor != anchor;
+ _anchor = anchor;
+ if (!anchor) return;
+
+ std::vector<Glib::ustring> labels;
+ std::vector<Glib::ustring> attrs;
+ if (changed) {
+ int len = 0;
+ while (anchor_desc[len].label) {
+ labels.emplace_back(anchor_desc[len].label);
+ attrs.emplace_back(anchor_desc[len].attribute);
+ len += 1;
+ }
+ _table->set_object(anchor, labels, attrs, (GtkWidget*)_table->gobj());
+ }
+ else {
+ _table->reread_properties();
+ }
+ }
+
+private:
+ std::unique_ptr<SPAttributeTable> _table;
+ SPAnchor* _anchor = nullptr;
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+class RectPanel : public details::AttributesPanel {
+public:
+ RectPanel(Glib::RefPtr<Gtk::Builder> builder) :
+ _main(get_widget<Gtk::Grid>(builder, "rect-main")),
+ _width(get_derived_widget<Inkscape::UI::Widget::SpinButton>(builder, "rect-width")),
+ _height(get_derived_widget<Inkscape::UI::Widget::SpinButton>(builder, "rect-height")),
+ _rx(get_derived_widget<Inkscape::UI::Widget::SpinButton>(builder, "rect-rx")),
+ _ry(get_derived_widget<Inkscape::UI::Widget::SpinButton>(builder, "rect-ry")),
+ _sharp(get_widget<Gtk::Button>(builder, "rect-sharp")),
+ _round(get_widget<Gtk::Button>(builder, "rect-corners"))
+ {
+ _title = _("Rectangle");
+ _widget = &_main;
+
+ _width.get_adjustment()->signal_value_changed().connect([=](){
+ change_value_px(_rect, _width.get_adjustment(), "width", [=](double w){ _rect->setVisibleWidth(w); });
+ });
+ _height.get_adjustment()->signal_value_changed().connect([=](){
+ change_value_px(_rect, _height.get_adjustment(), "height", [=](double h){ _rect->setVisibleHeight(h); });
+ });
+ _rx.get_adjustment()->signal_value_changed().connect([=](){
+ change_value_px(_rect, _rx.get_adjustment(), "rx", [=](double rx){ _rect->setVisibleRx(rx); });
+ });
+ _ry.get_adjustment()->signal_value_changed().connect([=](){
+ change_value_px(_rect, _ry.get_adjustment(), "ry", [=](double ry){ _rect->setVisibleRy(ry); });
+ });
+ get_widget<Gtk::Button>(builder, "rect-round").signal_clicked().connect([=](){
+ auto [changed, x, y] = round_values(_width, _height);
+ if (changed) {
+ _width.get_adjustment()->set_value(x);
+ _height.get_adjustment()->set_value(y);
+ }
+ });
+ _sharp.signal_clicked().connect([=](){
+ if (!_rect) return;
+
+ // remove rounded corners if LPE is there (first one found)
+ remove_lpeffect(_rect, LivePathEffect::FILLET_CHAMFER);
+ _rx.get_adjustment()->set_value(0);
+ _ry.get_adjustment()->set_value(0);
+ });
+ _round.signal_clicked().connect([=](){
+ if (!_rect || !_desktop) return;
+
+ // switch to node tool to show handles
+ set_active_tool(_desktop, "Node");
+ // rx/ry need to be reset first, LPE doesn't handle them too well
+ _rx.get_adjustment()->set_value(0);
+ _ry.get_adjustment()->set_value(0);
+ // add flexible corners effect if not yet present
+ if (!find_lpeffect(_rect, LivePathEffect::FILLET_CHAMFER)) {
+ sp_apply_lpeffect(_desktop, _rect, LivePathEffect::FILLET_CHAMFER);
+ }
+ });
+ }
+
+ ~RectPanel() override = default;
+
+ void update(SPObject* object) override {
+ _rect = cast<SPRect>(object);
+ if (!_rect) return;
+
+ auto scoped(_update.block());
+ _width.set_value(_rect->width.value);
+ _height.set_value(_rect->height.value);
+ _rx.set_value(_rect->rx.value);
+ _ry.set_value(_rect->ry.value);
+ auto lpe = find_lpeffect(_rect, LivePathEffect::FILLET_CHAMFER);
+ _sharp.set_sensitive(_rect->rx.value > 0 || _rect->ry.value > 0 || lpe);
+ _round.set_sensitive(!lpe);
+ }
+
+private:
+ SPRect* _rect = nullptr;
+ Gtk::Widget& _main;
+ Inkscape::UI::Widget::SpinButton& _width;
+ Inkscape::UI::Widget::SpinButton& _height;
+ Inkscape::UI::Widget::SpinButton& _rx;
+ Inkscape::UI::Widget::SpinButton& _ry;
+ Gtk::Button& _sharp;
+ Gtk::Button& _round;
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+class EllipsePanel : public details::AttributesPanel {
+public:
+ EllipsePanel(Glib::RefPtr<Gtk::Builder> builder) :
+ _main(get_widget<Gtk::Grid>(builder, "ellipse-main")),
+ _rx(get_derived_widget<Inkscape::UI::Widget::SpinButton>(builder, "el-rx")),
+ _ry(get_derived_widget<Inkscape::UI::Widget::SpinButton>(builder, "el-ry")),
+ _start(get_derived_widget<Inkscape::UI::Widget::SpinButton>(builder, "el-start")),
+ _end(get_derived_widget<Inkscape::UI::Widget::SpinButton>(builder, "el-end")),
+ _slice(get_widget<Gtk::RadioButton>(builder, "el-slice")),
+ _arc(get_widget<Gtk::RadioButton>(builder, "el-arc")),
+ _chord(get_widget<Gtk::RadioButton>(builder, "el-chord")),
+ _whole(get_widget<Gtk::Button>(builder, "el-whole"))
+ {
+ _title = _("Ellipse");
+ _widget = &_main;
+
+ _type[0] = &_slice;
+ _type[1] = &_arc;
+ _type[2] = &_chord;
+
+ int type = 0;
+ for (auto btn : _type) {
+ btn->signal_toggled().connect([=](){ set_type(type); });
+ type++;
+ }
+
+ _whole.signal_clicked().connect([=](){
+ _start.get_adjustment()->set_value(0);
+ _end.get_adjustment()->set_value(0);
+ });
+
+ auto normalize = [=](){
+ _ellipse->normalize();
+ _ellipse->updateRepr();
+ _ellipse->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ };
+
+ _rx.get_adjustment()->signal_value_changed().connect([=](){
+ change_value_px(_ellipse, _rx.get_adjustment(), nullptr, [=](double rx){ _ellipse->setVisibleRx(rx); normalize(); });
+ });
+ _ry.get_adjustment()->signal_value_changed().connect([=](){
+ change_value_px(_ellipse, _ry.get_adjustment(), nullptr, [=](double ry){ _ellipse->setVisibleRy(ry); normalize(); });
+ });
+ _start.get_adjustment()->signal_value_changed().connect([=](){
+ change_angle(_ellipse, _start.get_adjustment(), [=](double s){ _ellipse->start = s; normalize(); });
+ });
+ _end.get_adjustment()->signal_value_changed().connect([=](){
+ change_angle(_ellipse, _end.get_adjustment(), [=](double e){ _ellipse->end = e; normalize(); });
+ });
+
+ get_widget<Gtk::Button>(builder, "el-round").signal_clicked().connect([=](){
+ auto [changed, x, y] = round_values(_rx, _ry);
+ if (changed && x > 0 && y > 0) {
+ _rx.get_adjustment()->set_value(x);
+ _ry.get_adjustment()->set_value(y);
+ }
+ });
+ }
+
+ ~EllipsePanel() override = default;
+
+ void update(SPObject* object) override {
+ _ellipse = cast<SPGenericEllipse>(object);
+ if (!_ellipse) return;
+
+ auto scoped(_update.block());
+ _rx.set_value(_ellipse->rx.value);
+ _ry.set_value(_ellipse->ry.value);
+ _start.set_value(radians_to_degree_mod360(_ellipse->start));
+ _end.set_value(radians_to_degree_mod360(_ellipse->end));
+
+ _slice.set_active(_ellipse->arc_type == SP_GENERIC_ELLIPSE_ARC_TYPE_SLICE);
+ _arc.set_active(_ellipse->arc_type == SP_GENERIC_ELLIPSE_ARC_TYPE_ARC);
+ _chord.set_active(_ellipse->arc_type == SP_GENERIC_ELLIPSE_ARC_TYPE_CHORD);
+
+ auto slice = !_ellipse->is_whole();
+ _whole.set_sensitive(slice);
+ for (auto btn : _type) {
+ btn->set_sensitive(slice);
+ }
+ }
+
+ void set_type(int type) {
+ if (!_ellipse) return;
+
+ auto scoped(_update.block());
+
+ 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 << "Ellipse type change - bad arc type: " << type << std::endl;
+ break;
+ }
+ _ellipse->setAttribute("sodipodi:open", open ? "true" : nullptr);
+ _ellipse->setAttribute("sodipodi:arc-type", arc_type.c_str());
+ _ellipse->updateRepr();
+ DocumentUndo::done(_ellipse->document, _("Change arc type"), INKSCAPE_ICON("draw-ellipse"));
+ }
+
+private:
+ SPGenericEllipse* _ellipse = nullptr;
+ Gtk::Widget& _main;
+ Inkscape::UI::Widget::SpinButton& _rx;
+ Inkscape::UI::Widget::SpinButton& _ry;
+ Inkscape::UI::Widget::SpinButton& _start;
+ Inkscape::UI::Widget::SpinButton& _end;
+ Gtk::RadioButton& _slice;
+ Gtk::RadioButton& _arc;
+ Gtk::RadioButton& _chord;
+ Gtk::Button& _whole;
+ Gtk::RadioButton* _type[3];
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+class StarPanel : public details::AttributesPanel {
+public:
+ StarPanel(Glib::RefPtr<Gtk::Builder> builder) :
+ _main(get_widget<Gtk::Grid>(builder, "star-main")),
+ _corners(get_derived_widget<Inkscape::UI::Widget::SpinButton>(builder, "star-corners")),
+ _ratio(get_derived_widget<Inkscape::UI::Widget::SpinButton>(builder, "star-ratio")),
+ _rounded(get_derived_widget<Inkscape::UI::Widget::SpinButton>(builder, "star-rounded")),
+ _rand(get_derived_widget<Inkscape::UI::Widget::SpinButton>(builder, "star-rand")),
+ _poly(get_widget<Gtk::RadioButton>(builder, "star-poly")),
+ _star(get_widget<Gtk::RadioButton>(builder, "star-star")),
+ _align(get_widget<Gtk::Button>(builder, "star-align")),
+ _clear_rnd(get_widget<Gtk::Button>(builder, "star-rnd-clear")),
+ _clear_round(get_widget<Gtk::Button>(builder, "star-round-clear")),
+ _clear_ratio(get_widget<Gtk::Button>(builder, "star-ratio-clear"))
+ {
+ _title = _("Star");
+ _widget = &_main;
+
+ _corners.get_adjustment()->signal_value_changed().connect([=](){
+ change_value(_path, _corners.get_adjustment(), [=](double sides) {
+ _path->setAttributeDouble("sodipodi:sides", (int)sides);
+ auto arg1 = get_number(_path, "sodipodi:arg1").value_or(0.5);
+ _path->setAttributeDouble("sodipodi:arg2", arg1 + M_PI / sides);
+ _path->updateRepr();
+ });
+ });
+ _rounded.get_adjustment()->signal_value_changed().connect([=](){
+ change_value(_path, _rounded.get_adjustment(), [=](double rounded) {
+ _path->setAttributeDouble("inkscape:rounded", rounded);
+ _path->updateRepr();
+ });
+ });
+ _ratio.get_adjustment()->signal_value_changed().connect([=](){
+ change_value(_path, _ratio.get_adjustment(), [=](double ratio){
+ auto r1 = get_number(_path, "sodipodi:r1").value_or(1.0);
+ auto r2 = get_number(_path, "sodipodi:r2").value_or(1.0);
+ if (r2 < r1) {
+ _path->setAttributeDouble("sodipodi:r2", r1 * ratio);
+ } else {
+ _path->setAttributeDouble("sodipodi:r1", r2 * ratio);
+ }
+ _path->updateRepr();
+ });
+ });
+ _rand.get_adjustment()->signal_value_changed().connect([=](){
+ change_value(_path, _rand.get_adjustment(), [=](double rnd){
+ _path->setAttributeDouble("inkscape:randomized", rnd);
+ _path->updateRepr();
+ });
+ });
+ _clear_rnd.signal_clicked().connect([=](){ _rand.get_adjustment()->set_value(0); });
+ _clear_round.signal_clicked().connect([=](){ _rounded.get_adjustment()->set_value(0); });
+ _clear_ratio.signal_clicked().connect([=](){ _ratio.get_adjustment()->set_value(0.5); });
+
+ _poly.signal_toggled().connect([=](){ set_flat(true); });
+ _star.signal_toggled().connect([=](){ set_flat(false); });
+
+ _align.signal_clicked().connect([=](){
+ change_value(_path, {}, [=](double) { align_star_shape(_path); });
+ });
+ }
+
+ ~StarPanel() override = default;
+
+ void update(SPObject* object) override {
+ _path = cast<SPStar>(object);
+ if (!_path) return;
+
+ auto scoped(_update.block());
+ _corners.set_value(_path->sides);
+ double r1 = get_number(_path, "sodipodi:r1").value_or(0.5);
+ double r2 = get_number(_path, "sodipodi:r2").value_or(0.5);
+ if (r2 < r1) {
+ _ratio.set_value(r1 > 0 ? r2 / r1 : 0.5);
+ } else {
+ _ratio.set_value(r2 > 0 ? r1 / r2 : 0.5);
+ }
+ _rounded.set_value(_path->rounded);
+ _rand.set_value(_path->randomized);
+ widget_show(_clear_rnd, _path->randomized != 0);
+ widget_show(_clear_round, _path->rounded != 0);
+ widget_show(_clear_ratio, std::abs(_ratio.get_value() - 0.5) > 0.0005);
+
+ _poly.set_active(_path->flatsided);
+ _star.set_active(!_path->flatsided);
+ }
+
+ void set_flat(bool flat) {
+ change_value(_path, {}, [=](double){
+ _path->setAttribute("inkscape:flatsided", flat ? "true" : "false");
+ _path->updateRepr();
+ });
+ // adjust corners/sides
+ _corners.get_adjustment()->set_lower(flat ? 3 : 2);
+ if (flat && _corners.get_value() < 3) {
+ _corners.get_adjustment()->set_value(3);
+ }
+ }
+
+private:
+ SPStar* _path = nullptr;
+ Gtk::Widget& _main;
+ Inkscape::UI::Widget::SpinButton& _corners;
+ Inkscape::UI::Widget::SpinButton& _ratio;
+ Inkscape::UI::Widget::SpinButton& _rounded;
+ Inkscape::UI::Widget::SpinButton& _rand;
+ Gtk::Button& _clear_rnd;
+ Gtk::Button& _clear_round;
+ Gtk::Button& _clear_ratio;
+ Gtk::Button& _align;
+ Gtk::RadioButton& _poly;
+ Gtk::RadioButton& _star;
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+class TextPanel : public details::AttributesPanel {
+public:
+ TextPanel(Glib::RefPtr<Gtk::Builder> builder) :
+ _main(get_widget<Gtk::Grid>(builder, "text-main"))
+ {
+ // TODO - text panel
+ }
+
+private:
+ Gtk::Widget& _main;
+
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+std::string get_key(SPObject* object) {
+ if (!object) return {};
+
+ return typeid(*object).name();
+}
+
+details::AttributesPanel* ObjectAttributes::get_panel(SPObject* object) {
+ if (!object) return nullptr;
+
+ std::string name = get_key(object);
+ auto it = _panels.find(name);
+ return it == _panels.end() ? nullptr : it->second.get();
+}
+
+void ObjectAttributes::create_panels() {
+ _panels[typeid(SPImage).name()] = std::make_unique<ImagePanel>();
+ _panels[typeid(SPRect).name()] = std::make_unique<RectPanel>(_builder);
+ _panels[typeid(SPGenericEllipse).name()] = std::make_unique<EllipsePanel>(_builder);
+ _panels[typeid(SPStar).name()] = std::make_unique<StarPanel>(_builder);
+ _panels[typeid(SPAnchor).name()] = std::make_unique<AnchorPanel>();
+}
+
+}
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ 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..1136e85
--- /dev/null
+++ b/src/ui/dialog/object-attributes.h
@@ -0,0 +1,118 @@
+// 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 "desktop.h"
+#include "object/sp-object.h"
+#include "ui/dialog/dialog-base.h"
+#include "ui/operation-blocker.h"
+#include "ui/widget/spinbutton.h"
+#include "ui/widget/style-swatch.h"
+#include "ui/widget/unit-tracker.h"
+#include <glibmm/ustring.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/label.h>
+#include <gtkmm/widget.h>
+#include <memory>
+#include <string>
+#include <map>
+
+class SPAttributeTable;
+class SPItem;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+namespace details {
+ class AttributesPanel {
+ public:
+ AttributesPanel();
+ virtual ~AttributesPanel() = default;
+
+ void update_panel(SPObject* object, SPDesktop* desktop);
+ Gtk::Widget& widget() { if(!_widget) throw "crap"; return *_widget; }
+ Glib::ustring get_title() const { return _title; }
+ bool supports_fill_stroke() const {return _show_fill_stroke; }
+
+ protected:
+ virtual void update(SPObject* object) = 0;
+ // value with units changed by the user; modify current object
+ void change_value_px(SPObject* object, const Glib::RefPtr<Gtk::Adjustment>& adj, const char* attr, std::function<void (double)>&& setter);
+ // angle in degrees changed by the user; modify current object
+ void change_angle(SPObject* object, const Glib::RefPtr<Gtk::Adjustment>& adj, std::function<void (double)>&& setter);
+ // modify current object
+ void change_value(SPObject* object, const Glib::RefPtr<Gtk::Adjustment>& adj, std::function<void (double)>&& setter);
+
+ SPDesktop* _desktop = nullptr;
+ OperationBlocker _update;
+ bool _show_fill_stroke = true;
+ Glib::ustring _title;
+ Gtk::Widget* _widget = nullptr;
+ std::unique_ptr<UI::Widget::UnitTracker> _tracker;
+ };
+}
+
+/**
+ * A dialog widget to show object attributes (currently for images and links).
+ */
+class ObjectAttributes : public DialogBase
+{
+public:
+ ObjectAttributes();
+ ~ObjectAttributes() override = default;
+
+ void selectionChanged(Selection *selection) override;
+ void selectionModified(Selection *selection, guint flags) override;
+
+ void desktopReplaced() override;
+
+ /**
+ * Updates entries and other child widgets on selection change, object modification, etc.
+ */
+ void widget_setup();
+
+private:
+ Glib::RefPtr<Gtk::Builder> _builder;
+
+ void create_panels();
+ std::map<std::string, std::unique_ptr<details::AttributesPanel>> _panels;
+ details::AttributesPanel* get_panel(SPObject* object);
+ void update_panel(SPObject* object);
+
+ details::AttributesPanel* _current_panel = nullptr;
+ OperationBlocker _update;
+ Gtk::Box& _main_panel;
+ Gtk::Label& _obj_title;
+ // Contains a pointer to the currently selected item (NULL in case nothing is or multiple objects are selected).
+ SPItem* _current_item = nullptr;
+ Inkscape::UI::Widget::StyleSwatch _style_swatch;
+};
+
+}
+}
+}
+
+#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..6476b40
--- /dev/null
+++ b/src/ui/dialog/object-properties.cpp
@@ -0,0 +1,581 @@
+// 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 "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "style.h"
+#include "style-enums.h"
+
+#include "object/sp-image.h"
+#include "ui/icon-names.h"
+#include "widgets/sp-attribute-widget.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+ObjectProperties::ObjectProperties()
+ : DialogBase("/dialogs/object/", "ObjectProperties")
+ , _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)
+ , _label_color(_("Highlight Color:"), true)
+ , _highlight_color(_("Highlight Color"), "", 0xff0000ff, 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()))
+{
+ //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:");
+
+ _init();
+}
+
+void ObjectProperties::_init()
+{
+ 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);
+
+ 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);
+
+ /* 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);
+
+ _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);
+
+ /* 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);
+
+ _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);
+
+ /* 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);
+
+ _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));
+
+ _label_color.set_mnemonic_widget(_highlight_color);
+ _label_color.set_halign(Gtk::ALIGN_START);
+ _highlight_color.connectChanged(sigc::mem_fun(*this, &ObjectProperties::_highlightChanged));
+
+ /* 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);
+ 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);
+
+ /* Create the entry box for the SVG DPI */
+ _spin_dpi.set_digits(2);
+ _spin_dpi.set_range(1, 1200);
+
+ _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);
+
+ /* 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 (scaled image is 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);
+
+ _label_image_rendering.set_mnemonic_widget(_combo_image_rendering);
+
+ _combo_image_rendering.signal_changed().connect(
+ sigc::mem_fun(*this, &ObjectProperties::_imageRenderingChanged)
+ );
+
+
+ grid_top->attach(_label_id, 0, 0, 1, 1);
+ grid_top->attach(_entry_id, 1, 0, 1, 1);
+ grid_top->attach(_label_label, 0, 1, 1, 1);
+ grid_top->attach(_entry_label, 1, 1, 1, 1);
+ grid_top->attach(_label_title, 0, 2, 1, 1);
+ grid_top->attach(_entry_title, 1, 2, 1, 1);
+ grid_top->attach(_label_color, 0, 3, 1, 1);
+ grid_top->attach(_highlight_color, 1, 3, 1, 1);
+ grid_top->attach(_label_dpi, 0, 4, 1, 1);
+ grid_top->attach(_spin_dpi, 1, 4, 1, 1);
+ grid_top->attach(_label_image_rendering, 0, 5, 1, 1);
+ grid_top->attach(_combo_image_rendering, 1, 5, 1, 1);
+
+ /* Check boxes */
+ Gtk::Box *hb_checkboxes = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
+ 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);
+ pack_start(_exp_interactivity, Gtk::PACK_SHRINK);
+ show_all();
+}
+
+void ObjectProperties::update_entries()
+{
+ if (_blocked || !getDesktop()) {
+ return;
+ }
+
+ auto selection = getSelection();
+ if (!selection) return;
+
+ if (!selection->singleItem()) {
+ 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();
+ _highlight_color.setRgba32(0x0);
+ return;
+ } else {
+ 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 */
+ _highlight_color.setRgba32(item->highlight_color());
+ _highlight_color.closeWindow();
+
+ 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 (is<SPImage>(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 = 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 (getDocument()->getObjectById(id) != nullptr) {
+ _label_id.set_text(_("Id exists! "));
+ } else {
+ _label_id.set_markup_with_mnemonic(_("_ID:") + Glib::ustring(" "));
+ item->setAttribute("id", id);
+ DocumentUndo::done(getDocument(), _("Set object ID"), INKSCAPE_ICON("dialog-object-properties"));
+ }
+ 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(getDocument(), _("Set object label"), INKSCAPE_ICON("dialog-object-properties"));
+ }
+
+ /* Retrieve the title */
+ if (obj->setTitle(_entry_title.get_text().c_str())) {
+ DocumentUndo::done(getDocument(), _("Set object title"), INKSCAPE_ICON("dialog-object-properties"));
+ }
+
+ /* Retrieve the DPI */
+ if (is<SPImage>(obj)) {
+ Glib::ustring dpi_value = Glib::ustring::format(_spin_dpi.get_value());
+ obj->setAttribute("inkscape:svg-dpi", dpi_value);
+ DocumentUndo::done(getDocument(), _("Set image DPI"), INKSCAPE_ICON("dialog-object-properties"));
+ }
+
+ /* 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(getDocument(), _("Set object description"), INKSCAPE_ICON("dialog-object-properties"));
+ }
+
+ _blocked = false;
+}
+
+void ObjectProperties::_highlightChanged(guint rgba)
+{
+ if (_blocked)
+ return;
+
+ if (auto item = getSelection()->singleItem()) {
+ item->setHighlight(rgba);
+ DocumentUndo::done(getDocument(), _("Set item highlight color"), INKSCAPE_ICON("dialog-object-properties"));
+ }
+}
+
+void ObjectProperties::_imageRenderingChanged()
+{
+ if (_blocked) {
+ return;
+ }
+
+ SPItem *item = 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(getDocument(), _("Set image rendering option"), INKSCAPE_ICON("dialog-object-properties"));
+ }
+ sp_repr_css_attr_unref(css);
+
+ _blocked = false;
+}
+
+void ObjectProperties::_sensitivityToggled()
+{
+ if (_blocked) {
+ return;
+ }
+
+ SPItem *item = getSelection()->singleItem();
+ g_return_if_fail(item != nullptr);
+
+ _blocked = true;
+ item->setLocked(_cb_lock.get_active());
+ DocumentUndo::done(getDocument(), _cb_lock.get_active() ? _("Lock object") : _("Unlock object"), INKSCAPE_ICON("dialog-object-properties"));
+ _blocked = false;
+}
+
+void ObjectProperties::_aspectRatioToggled()
+{
+ if (_blocked) {
+ return;
+ }
+
+ SPItem *item = 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 (is<SPImage>(item)) {
+ Glib::ustring dpi_value = Glib::ustring::format(_spin_dpi.get_value());
+ item->setAttribute("preserveAspectRatio", active);
+ DocumentUndo::done(getDocument(), _("Set preserve ratio"), INKSCAPE_ICON("dialog-object-properties"));
+ }
+ _blocked = false;
+}
+
+void ObjectProperties::_hiddenToggled()
+{
+ if (_blocked) {
+ return;
+ }
+
+ SPItem *item = getSelection()->singleItem();
+ g_return_if_fail(item != nullptr);
+
+ _blocked = true;
+ item->setExplicitlyHidden(_cb_hide.get_active());
+ DocumentUndo::done(getDocument(), _cb_hide.get_active() ? _("Hide object") : _("Unhide object"), INKSCAPE_ICON("dialog-object-properties"));
+ _blocked = false;
+}
+
+void ObjectProperties::selectionChanged(Selection *selection)
+{
+ update_entries();
+}
+
+void ObjectProperties::desktopReplaced()
+{
+ update_entries();
+}
+
+}
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ 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..f52f019
--- /dev/null
+++ b/src/ui/dialog/object-properties.h
@@ -0,0 +1,143 @@
+// 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 <gtkmm/checkbutton.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/expander.h>
+#include <gtkmm/frame.h>
+#include <gtkmm/spinbutton.h>
+#include <gtkmm/textview.h>
+
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/scrollprotected.h"
+#include "ui/widget/color-picker.h"
+#include "ui/widget/frame.h"
+
+class SPAttributeTable;
+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 DialogBase
+{
+public:
+ ObjectProperties();
+ ~ObjectProperties() override {};
+
+ /// Updates entries and other child widgets on selection change, object modification, etc.
+ void update_entries();
+ void selectionChanged(Selection *selection) override;
+
+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_color; //the label for the object highlight
+ Inkscape::UI::Widget::ColorPicker _highlight_color; // color picker for the object highlight
+
+ Gtk::Label _label_image_rendering; // the label for 'image-rendering'
+ Inkscape::UI::Widget::ScrollProtected<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
+
+ /// Constructor auxiliary function creating the child widgets.
+ void _init();
+
+ /// Sets object properties (ID, label, title, description) on user input.
+ void _labelChanged();
+
+ // Callback for highlight color
+ void _highlightChanged(guint rgba);
+
+ /// 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();
+
+ void desktopReplaced() override;
+};
+}
+}
+}
+
+#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..92f0892
--- /dev/null
+++ b/src/ui/dialog/objects.cpp
@@ -0,0 +1,1860 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A panel for listing objects in a document.
+ *
+ * Authors:
+ * Martin Owens
+ * Mike Kowalski
+ * Adam Belis (UX/Design)
+ *
+ * Copyright (C) Authors 2020-2022
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "objects.h"
+
+#include <glibmm/ustring.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/button.h>
+#include <gtkmm/cellrenderer.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/icontheme.h>
+#include <gtkmm/imagemenuitem.h>
+#include <gtkmm/modelbutton.h>
+#include <gtkmm/object.h>
+#include <gtkmm/popover.h>
+#include <gtkmm/scale.h>
+#include <gtkmm/searchentry.h>
+#include <gtkmm/separatormenuitem.h>
+#include <glibmm/main.h>
+#include <glibmm/i18n.h>
+#include <iomanip>
+#include <pango/pango-utils.h>
+#include <string>
+
+#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 "message-stack.h"
+
+#include "actions/actions-tools.h"
+
+#include "include/gtkmm_version.h"
+
+#include "display/drawing-group.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-enums.h"
+#include "style.h"
+#include "svg/css-ostringstream.h"
+#include "ui/builder-utils.h"
+#include "ui/dialog-events.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "ui/selected-color.h"
+#include "ui/shortcuts.h"
+#include "ui/tools/node-tool.h"
+
+#include "ui/contextmenu.h"
+#include "ui/util.h"
+#include "ui/widget/canvas.h"
+#include "ui/widget/filter-effect-chooser.h"
+#include "ui/widget/imagetoggler.h"
+#include "ui/widget/shapeicon.h"
+#include "ui/widget/objects-dialog-cells.h"
+#include "util/numeric/converters.h"
+
+// alpha (transparency) multipliers corresponding to item selection state combinations (SelectionState)
+// when 0 - do not color item's background
+static double const SELECTED_ALPHA[8] = {
+ 0.00, //0 not selected
+ 0.90, //1 selected
+ 0.50, //2 layer focused
+ 0.20, //3 layer focused & selected
+ 0.00, //4 child of focused layer
+ 0.90, //5 selected child of focused layer
+ 0.50, //6 2 and 4
+ 0.90 //7 1, 2 and 4
+};
+
+//#define DUMP_LAYERS 1
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class ObjectWatcher : public Inkscape::XML::NodeObserver
+{
+public:
+ ObjectWatcher() = delete;
+ ObjectWatcher(ObjectsPanel *panel, SPItem *, Gtk::TreeRow *row, bool is_filtered);
+ ~ObjectWatcher() override;
+
+ void initRowInfo();
+ void updateRowInfo();
+ void updateRowHighlight();
+ void updateRowAncestorState(bool invisible, bool locked);
+ void updateRowBg(guint32 rgba = 0.0);
+
+ ObjectWatcher *findChild(Node *node);
+ void addDummyChild();
+ bool addChild(SPItem *, bool dummy = true);
+ void addChildren(SPItem *, bool dummy = false);
+ void setSelectedBit(SelectionState mask, bool enabled);
+ void setSelectedBitRecursive(SelectionState mask, bool enabled);
+ void setSelectedBitChildren(SelectionState mask, bool enabled);
+ void rememberExtendedItems();
+ void moveChild(Node &child, Node *sibling);
+ bool isFiltered() const { return is_filtered; }
+
+ Gtk::TreeNodeChildren getChildren() const;
+ Gtk::TreeIter getChildIter(Node *) const;
+
+ void notifyChildAdded(Node &, Node &, Node *) override;
+ void notifyChildRemoved(Node &, Node &, Node *) override;
+ void notifyChildOrderChanged(Node &, Node &child, Node *, Node *) override;
+ void notifyAttributeChanged(Node &, GQuark, Util::ptr_shared, Util::ptr_shared) override;
+
+ /// Associate this watcher with a tree row
+ void setRow(const Gtk::TreeModel::Path &path)
+ {
+ assert(path);
+ row_ref = Gtk::TreeModel::RowReference(panel->_store, path);
+ }
+ void setRow(const Gtk::TreeModel::Row &row)
+ {
+ setRow(panel->_store->get_path(row));
+ }
+
+ // Get the path out of this watcher
+ Gtk::TreeModel::Path getTreePath() const {
+ return row_ref.get_path();
+ }
+
+ /// True if this watchr has a valid row reference.
+ bool hasRow() const { return bool(row_ref); }
+
+ /// Transfer a child watcher to its new parent
+ void transferChild(Node *childnode)
+ {
+ auto *target = panel->getWatcher(childnode->parent());
+ assert(target != this);
+ auto nh = child_watchers.extract(childnode);
+ assert(nh);
+ bool inserted = target->child_watchers.insert(std::move(nh)).inserted;
+ assert(inserted);
+ }
+
+ /// The XML node associated with this watcher.
+ Node *getRepr() const { return node; }
+ std::optional<Gtk::TreeRow> getRow() const {
+ if (auto path = row_ref.get_path()) {
+ if(auto iter = panel->_store->get_iter(path)) {
+ return *iter;
+ }
+ }
+ return std::nullopt;
+ }
+
+ std::unordered_map<Node const *, std::unique_ptr<ObjectWatcher>> child_watchers;
+
+private:
+ Node *node;
+ Gtk::TreeModel::RowReference row_ref;
+ ObjectsPanel *panel;
+ SelectionState selection_state;
+ bool is_filtered;
+};
+
+class ObjectsPanel::ModelColumns : public Gtk::TreeModel::ColumnRecord
+{
+public:
+ ModelColumns()
+ {
+ add(_colNode);
+ add(_colLabel);
+ add(_colType);
+ add(_colIconColor);
+ add(_colClipMask);
+ add(_colBgColor);
+ add(_colInvisible);
+ add(_colLocked);
+ add(_colAncestorInvisible);
+ add(_colAncestorLocked);
+ add(_colHover);
+ add(_colItemStateSet);
+ add(_colBlendMode);
+ add(_colOpacity);
+ add(_colItemState);
+ add(_colHoverColor);
+ }
+ ~ModelColumns() override = default;
+ Gtk::TreeModelColumn<Node*> _colNode;
+ Gtk::TreeModelColumn<Glib::ustring> _colLabel;
+ Gtk::TreeModelColumn<Glib::ustring> _colType;
+ Gtk::TreeModelColumn<unsigned int> _colIconColor;
+ Gtk::TreeModelColumn<unsigned int> _colClipMask;
+ Gtk::TreeModelColumn<Gdk::RGBA> _colBgColor;
+ Gtk::TreeModelColumn<bool> _colInvisible;
+ Gtk::TreeModelColumn<bool> _colLocked;
+ Gtk::TreeModelColumn<bool> _colAncestorInvisible;
+ Gtk::TreeModelColumn<bool> _colAncestorLocked;
+ Gtk::TreeModelColumn<bool> _colHover;
+ Gtk::TreeModelColumn<bool> _colItemStateSet;
+ Gtk::TreeModelColumn<SPBlendMode> _colBlendMode;
+ Gtk::TreeModelColumn<double> _colOpacity;
+ Gtk::TreeModelColumn<Glib::ustring> _colItemState;
+ // Set when hovering over the color tag cell
+ Gtk::TreeModelColumn<bool> _colHoverColor;
+};
+
+/**
+ * Creates a new ObjectWatcher, a gtk TreeView iterated watching device.
+ *
+ * @param panel The panel to which the object watcher belongs
+ * @param obj The SPItem to watch in the document
+ * @param row The optional list store tree row for the item,
+ if not provided, assumes this is the root 'document' object.
+ * @param filtered, if true this watcher will filter all chldren using the panel filtering function on each item to decide if it should be shown.
+ */
+ObjectWatcher::ObjectWatcher(ObjectsPanel* panel, SPItem* obj, Gtk::TreeRow *row, bool filtered)
+ : panel(panel)
+ , row_ref()
+ , selection_state(0)
+ , is_filtered(filtered)
+ , node(obj->getRepr())
+{
+ if(row != nullptr) {
+ assert(row->children().empty());
+ setRow(*row);
+ initRowInfo();
+ updateRowInfo();
+ }
+ node->addObserver(*this);
+
+ // Only show children for groups (and their subclasses like SPAnchor or SPRoot)
+ if (!is<SPGroup>(obj)) {
+ return;
+ }
+
+ // Add children as a dummy row to avoid excensive execution when
+ // the tree is really large, but not in layers mode.
+ addChildren(obj, (bool)row && !obj->isExpanded());
+}
+ObjectWatcher::~ObjectWatcher()
+{
+ node->removeObserver(*this);
+ Gtk::TreeModel::Path path;
+ if (bool(row_ref) && (path = row_ref.get_path())) {
+ if (auto iter = panel->_store->get_iter(path)) {
+ panel->_store->erase(iter);
+ }
+ }
+ child_watchers.clear();
+}
+
+void ObjectWatcher::initRowInfo()
+{
+ auto _model = panel->_model;
+ auto row = *panel->_store->get_iter(row_ref.get_path());
+ row[_model->_colHover] = false;
+}
+
+/**
+ * Update the information in the row from the stored node
+ */
+void ObjectWatcher::updateRowInfo()
+{
+ if (auto item = cast<SPItem>(panel->getObject(node))) {
+ assert(row_ref);
+ assert(row_ref.get_path());
+
+ auto _model = panel->_model;
+ auto row = *panel->_store->get_iter(row_ref.get_path());
+ row[_model->_colNode] = node;
+
+ // show ids without "#"
+ char const *id = item->getId();
+ row[_model->_colLabel] = (id && !item->label()) ? id : item->defaultLabel();
+
+ row[_model->_colType] = item->typeName();
+ row[_model->_colClipMask] =
+ (item->getClipObject() ? Inkscape::UI::Widget::OVERLAY_CLIP : 0) |
+ (item->getMaskObject() ? Inkscape::UI::Widget::OVERLAY_MASK : 0);
+ row[_model->_colInvisible] = item->isHidden();
+ row[_model->_colLocked] = !item->isSensitive();
+ auto blend = item->style && item->style->mix_blend_mode.set ? item->style->mix_blend_mode.value : SP_CSS_BLEND_NORMAL;
+ row[_model->_colBlendMode] = blend;
+ auto opacity = 1.0;
+ if (item->style && item->style->opacity.set) {
+ opacity = SP_SCALE24_TO_FLOAT(item->style->opacity.value);
+ }
+ row[_model->_colOpacity] = opacity;
+ std::string item_state;
+ if (opacity == 0.0) {
+ item_state = "object-transparent";
+ }
+ else if (blend != SP_CSS_BLEND_NORMAL) {
+ item_state = opacity == 1.0 ? "object-blend-mode" : "object-translucent-blend-mode";
+ }
+ else if (opacity < 1.0) {
+ item_state = "object-translucent";
+ }
+ row[_model->_colItemState] = item_state;
+ row[_model->_colItemStateSet] = !item_state.empty();
+
+ updateRowHighlight();
+ updateRowAncestorState(row[_model->_colAncestorInvisible], row[_model->_colAncestorLocked]);
+ }
+}
+
+/**
+ * Propagate changes to the highlight color to all children.
+ */
+void ObjectWatcher::updateRowHighlight() {
+ if (auto item = cast<SPItem>(panel->getObject(node))) {
+ auto row = *panel->_store->get_iter(row_ref.get_path());
+ auto new_color = item->highlight_color();
+ if (new_color != row[panel->_model->_colIconColor]) {
+ row[panel->_model->_colIconColor] = new_color;
+ updateRowBg(new_color);
+ for (auto &watcher : child_watchers) {
+ watcher.second->updateRowHighlight();
+ }
+ }
+ }
+}
+
+/**
+ * Propagate a change in visibility or locked state to all children
+ */
+void ObjectWatcher::updateRowAncestorState(bool invisible, bool locked) {
+ auto _model = panel->_model;
+ auto row = *panel->_store->get_iter(row_ref.get_path());
+ row[_model->_colAncestorInvisible] = invisible;
+ row[_model->_colAncestorLocked] = locked;
+ for (auto &watcher : child_watchers) {
+ watcher.second->updateRowAncestorState(
+ invisible || row[_model->_colInvisible],
+ locked || row[_model->_colLocked]);
+ }
+}
+
+Gdk::RGBA selection_color;
+
+/**
+ * Updates the row's background colour as indicated by its selection.
+ */
+void ObjectWatcher::updateRowBg(guint32 rgba)
+{
+ assert(row_ref);
+ if (auto row = *panel->_store->get_iter(row_ref.get_path())) {
+ auto alpha = SELECTED_ALPHA[selection_state];
+ if (alpha == 0.0) {
+ row[panel->_model->_colBgColor] = Gdk::RGBA();
+ return;
+ }
+
+ const auto& sel = selection_color;
+ auto gdk_color = Gdk::RGBA();
+ gdk_color.set_red(sel.get_red());
+ gdk_color.set_green(sel.get_green());
+ gdk_color.set_blue(sel.get_blue());
+ gdk_color.set_alpha(sel.get_alpha() * alpha);
+ row[panel->_model->_colBgColor] = gdk_color;
+ }
+}
+
+/**
+ * Flip the selected state bit on or off as needed, calls updateRowBg if changed.
+ *
+ * @param mask - The selection bit to set or unset
+ * @param enabled - If the bit should be set or unset
+ */
+void ObjectWatcher::setSelectedBit(SelectionState mask, bool enabled) {
+ if (!row_ref) return;
+ SelectionState value = selection_state;
+ SelectionState original = value;
+ if (enabled) {
+ value |= mask;
+ } else {
+ value &= ~mask;
+ }
+ if (value != original) {
+ selection_state = value;
+ updateRowBg();
+ }
+}
+
+/**
+ * Flip the selected state bit on or off as needed, on this watcher and all
+ * its direct and indirect children.
+ */
+void ObjectWatcher::setSelectedBitRecursive(SelectionState mask, bool enabled)
+{
+ setSelectedBit(mask, enabled);
+ setSelectedBitChildren(mask, enabled);
+}
+void ObjectWatcher::setSelectedBitChildren(SelectionState mask, bool enabled)
+{
+ for (auto &pair : child_watchers) {
+ pair.second->setSelectedBitRecursive(mask, enabled);
+ }
+}
+
+/**
+ * Keep expanded rows expanded and recurse through all children.
+ */
+void ObjectWatcher::rememberExtendedItems()
+{
+ if (auto item = cast<SPItem>(panel->getObject(node))) {
+ if (item->isExpanded())
+ panel->_tree.expand_row(row_ref.get_path(), false);
+ }
+ for (auto &pair : child_watchers) {
+ pair.second->rememberExtendedItems();
+ }
+}
+
+/**
+ * Find the child watcher for the given node.
+ */
+ObjectWatcher *ObjectWatcher::findChild(Node *node)
+{
+ auto it = child_watchers.find(node);
+ if (it != child_watchers.end()) {
+ return it->second.get();
+ }
+ return nullptr;
+}
+
+/**
+ * Add the child object to this node.
+ *
+ * @param child - SPObject to be added
+ * @param dummy - Add a dummy objects (hidden) instead
+ *
+ * @returns true if child added was a dummy objects
+ */
+bool ObjectWatcher::addChild(SPItem *child, bool dummy)
+{
+ if (is_filtered && !panel->showChildInTree(child)) {
+ return false;
+ }
+
+ auto const children = getChildren();
+ if (!is_filtered && dummy && row_ref) {
+ if (children.empty()) {
+ auto const iter = panel->_store->append(children);
+ assert(panel->isDummy(*iter));
+ return true;
+ } else if (panel->isDummy(children[0])) {
+ return false;
+ }
+ }
+
+ auto *node = child->getRepr();
+ assert(node);
+ Gtk::TreeModel::Row row = *(panel->_store->prepend(children));
+
+ // Ancestor states are handled inside the list store (so we don't have to re-ask every update)
+ auto _model = panel->_model;
+ if (row_ref) {
+ auto parent_row = *panel->_store->get_iter(row_ref.get_path());
+ row[_model->_colAncestorInvisible] = parent_row[_model->_colAncestorInvisible] || parent_row[_model->_colInvisible];
+ row[_model->_colAncestorLocked] = parent_row[_model->_colAncestorLocked] || parent_row[_model->_colLocked];
+ } else {
+ row[_model->_colAncestorInvisible] = false;
+ row[_model->_colAncestorLocked] = false;
+ }
+
+ auto &watcher = child_watchers[node];
+ assert(!watcher);
+ watcher.reset(new ObjectWatcher(panel, child, &row, is_filtered));
+
+ // Make sure new children have the right focus set.
+ if ((selection_state & LAYER_FOCUSED) != 0) {
+ watcher->setSelectedBit(LAYER_FOCUS_CHILD, true);
+ }
+ return false;
+}
+
+/**
+ * Add all SPItem children as child rows.
+ */
+void ObjectWatcher::addChildren(SPItem *obj, bool dummy)
+{
+ assert(child_watchers.empty());
+
+ for (auto &child : obj->children) {
+ if (auto item = cast<SPItem>(&child)) {
+ if (addChild(item, dummy) && dummy) {
+ // one dummy child is enough to make the group expandable
+ break;
+ }
+ }
+ }
+}
+
+/**
+ * Move the child to just after the given sibling
+ *
+ * @param child - SPObject to be moved
+ * @param sibling - Optional sibling Object to add next to, if nullptr the
+ * object is moved to BEFORE the first item.
+ */
+void ObjectWatcher::moveChild(Node &child, Node *sibling)
+{
+ auto child_iter = getChildIter(&child);
+ if (!child_iter)
+ return; // This means the child was never added, probably not an SPItem.
+
+ // sibling might not be an SPItem and thus not be represented in the
+ // TreeView. Find the closest SPItem and use that for the reordering.
+ while (sibling && !is<SPItem>(panel->getObject(sibling))) {
+ sibling = sibling->prev();
+ }
+
+ auto sibling_iter = getChildIter(sibling);
+ panel->_store->move(child_iter, sibling_iter);
+}
+
+/**
+ * Get the TreeRow's children iterator
+ *
+ * @returns Gtk Tree Node Children iterator
+ */
+Gtk::TreeNodeChildren ObjectWatcher::getChildren() const
+{
+ Gtk::TreeModel::Path path;
+ if (row_ref && (path = row_ref.get_path())) {
+ return panel->_store->get_iter(path)->children();
+ }
+ assert(!row_ref);
+ return panel->_store->children();
+}
+
+/**
+ * Convert SPObject to TreeView Row, assuming the object is a child.
+ *
+ * @param child - The child object to find in this branch
+ * @returns Gtk TreeRow for the child, or end() if not found
+ */
+Gtk::TreeIter ObjectWatcher::getChildIter(Node *node) const
+{
+ auto childrows = getChildren();
+
+ if (!node) {
+ return childrows.end();
+ }
+
+ // Note: TreeRow inherits from TreeIter, so this `row` variable is
+ // also an iterator and a valid return value.
+ for (auto &row : childrows) {
+ if (panel->getRepr(row) == node) {
+ return row;
+ }
+ }
+ // In layer mode, we will come here for all non-layers
+ return childrows.begin();
+}
+
+void ObjectWatcher::notifyChildAdded( Node &node, Node &child, Node *prev )
+{
+ assert(this->node == &node);
+ // Ignore XML nodes which are not displayable items
+ if (auto item = cast<SPItem>(panel->getObject(&child))) {
+ addChild(item);
+ moveChild(child, prev);
+ }
+}
+void ObjectWatcher::notifyChildRemoved( Node &node, Node &child, Node* /*prev*/ )
+{
+ assert(this->node == &node);
+
+ if (child_watchers.erase(&child) > 0) {
+ return;
+ }
+
+ if (node.firstChild() == nullptr) {
+ assert(row_ref);
+ auto iter = panel->_store->get_iter(row_ref.get_path());
+ panel->removeDummyChildren(*iter);
+ }
+}
+void ObjectWatcher::notifyChildOrderChanged( Node &parent, Node &child, Node */*old_prev*/, Node *new_prev )
+{
+ assert(this->node == &parent);
+
+ moveChild(child, new_prev);
+}
+void ObjectWatcher::notifyAttributeChanged( Node &node, GQuark name, Util::ptr_shared /*old_value*/, Util::ptr_shared /*new_value*/ )
+{
+ assert(this->node == &node);
+
+ // The root <svg> node doesn't have a row
+ if (this == panel->getRootWatcher()) {
+ return;
+ }
+
+ // Almost anything could change the icon, so update upon any change, defer for lots of updates.
+
+ // examples of not-so-obvious cases:
+ // - width/height: Can change type "circle" to an "ellipse"
+
+ static std::set<GQuark> const excluded{
+ g_quark_from_static_string("transform"),
+ g_quark_from_static_string("x"),
+ g_quark_from_static_string("y"),
+ g_quark_from_static_string("d"),
+ g_quark_from_static_string("sodipodi:nodetypes"),
+ };
+
+ if (excluded.count(name)) {
+ return;
+ }
+
+ updateRowInfo();
+}
+
+
+/**
+ * Get the object from the node.
+ *
+ * @param node - XML Node involved in the signal.
+ * @returns SPObject matching the node, returns nullptr if not found.
+ */
+SPObject *ObjectsPanel::getObject(Node *node) {
+ if (node != nullptr && getDocument())
+ return getDocument()->getObjectByRepr(node);
+ return nullptr;
+}
+
+/**
+ * Get the object watcher from the xml node (reverse lookup), it uses a ancesstor
+ * recursive pattern to match up with the root_watcher.
+ *
+ * @param node - The node to look up.
+ * @return the ObjectWatcher object if it's possible to find.
+ */
+ObjectWatcher* ObjectsPanel::getWatcher(Node *node)
+{
+ assert(node);
+ if (root_watcher->getRepr() == node) {
+ return root_watcher;
+ } else if (node->parent()) {
+ if (auto parent_watcher = getWatcher(node->parent())) {
+ return parent_watcher->findChild(node);
+ }
+ }
+ return nullptr;
+}
+
+/**
+ * Constructor
+ */
+ObjectsPanel::ObjectsPanel()
+ : DialogBase("/dialogs/objects", "Objects")
+ , root_watcher(nullptr)
+ , _model(new ModelColumns())
+ , _layer(nullptr)
+ , _is_editing(false)
+ , _page(Gtk::ORIENTATION_VERTICAL)
+ , _color_picker(_("Highlight color"), "", 0, true)
+ , _builder(create_builder("dialog-objects.glade"))
+ , _settings_menu(get_widget<Gtk::Popover>(_builder, "settings-menu"))
+ , _object_menu(get_widget<Gtk::Popover>(_builder, "object-menu"))
+ , _searchBox(get_widget<Gtk::SearchEntry>(_builder, "search"))
+ , _opacity_slider(get_widget<Gtk::Scale>(_builder, "opacity-slider"))
+ , _setting_layers(get_derived_widget<PrefCheckButton, Glib::ustring, bool>(_builder, "setting-layers", "/dialogs/objects/layers_only", false))
+ , _setting_track(get_derived_widget<PrefCheckButton, Glib::ustring, bool>(_builder, "setting-track", "/dialogs/objects/expand_to_layer", true))
+{
+ _store = Gtk::TreeStore::create(*_model);
+ _color_picker.hide();
+
+ //Set up the tree
+ _tree.set_model(_store);
+ _tree.set_headers_visible(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);
+ _tree.set_name("ObjectsTreeView");
+
+ auto& header = get_widget<Gtk::Box>(_builder, "header");
+ // Search
+ _searchBox.signal_activate().connect(sigc::mem_fun(*this, &ObjectsPanel::_searchActivated));
+ _searchBox.signal_search_changed().connect(sigc::mem_fun(*this, &ObjectsPanel::_searchChanged));
+
+ //Label
+ _name_column = Gtk::manage(new Gtk::TreeViewColumn());
+ _text_renderer = Gtk::manage(new Gtk::CellRendererText());
+ _text_renderer->property_editable() = true;
+ _text_renderer->property_ellipsize().set_value(Pango::ELLIPSIZE_END);
+ _text_renderer->signal_editing_started().connect([=](Gtk::CellEditable*,const Glib::ustring&){
+ _is_editing = true;
+ });
+ _text_renderer->signal_editing_canceled().connect([=](){
+ _is_editing = false;
+ });
+ _text_renderer->signal_edited().connect([=](const Glib::ustring&,const Glib::ustring&){
+ _is_editing = false;
+ });
+
+ const int icon_col_width = 24;
+ auto icon_renderer = Gtk::manage(new Inkscape::UI::Widget::CellRendererItemIcon());
+ icon_renderer->property_xpad() = 2;
+ icon_renderer->property_width() = icon_col_width;
+ _tree.append_column(*_name_column);
+ _name_column->set_expand(true);
+ _name_column->pack_start(*icon_renderer, false);
+ _name_column->pack_start(*_text_renderer, true);
+ _name_column->add_attribute(_text_renderer->property_text(), _model->_colLabel);
+ _name_column->add_attribute(_text_renderer->property_cell_background_rgba(), _model->_colBgColor);
+ _name_column->add_attribute(icon_renderer->property_shape_type(), _model->_colType);
+ _name_column->add_attribute(icon_renderer->property_color(), _model->_colIconColor);
+ _name_column->add_attribute(icon_renderer->property_clipmask(), _model->_colClipMask);
+ _name_column->add_attribute(icon_renderer->property_cell_background_rgba(), _model->_colBgColor);
+
+ // blend mode and opacity icon(s)
+ _item_state_toggler = Gtk::manage(new Inkscape::UI::Widget::ImageToggler(
+ INKSCAPE_ICON("object-blend-mode"), INKSCAPE_ICON("object-opaque")));
+ int modeColNum = _tree.append_column("mode", *_item_state_toggler) - 1;
+ if (auto col = _tree.get_column(modeColNum)) {
+ col->add_attribute(_item_state_toggler->property_active(), _model->_colItemStateSet);
+ col->add_attribute(_item_state_toggler->property_active_icon(), _model->_colItemState);
+ col->add_attribute(_item_state_toggler->property_cell_background_rgba(), _model->_colBgColor);
+ col->add_attribute(_item_state_toggler->property_activatable(), _model->_colHover);
+ col->set_fixed_width(icon_col_width);
+ _blend_mode_column = col;
+ }
+
+ _tree.set_has_tooltip();
+ _tree.signal_query_tooltip().connect([=](int x, int y, bool kbd, const Glib::RefPtr<Gtk::Tooltip>& tooltip){
+ Gtk::TreeModel::iterator iter;
+ if (!_tree.get_tooltip_context_iter(x, y, kbd, iter) || !iter) {
+ return false;
+ }
+ auto blend = (*iter)[_model->_colBlendMode];
+ auto opacity = (*iter)[_model->_colOpacity];
+ auto templt = !pango_version_check(1, 50, 0) ?
+ "<span>%1 %2%%\n</span><span line_height=\"0.5\">\n</span><span>%3\n<i>%4</i></span>" :
+ "<span>%1 %2%%\n</span><span>\n</span><span>%3\n<i>%4</i></span>";
+ auto label = Glib::ustring::compose(templt,
+ _("Opacity:"), Util::format_number(opacity * 100.0, 1),
+ _("Blend mode:"), _blend_mode_names[blend]
+ );
+ tooltip->set_markup(label);
+ _tree.set_tooltip_cell(tooltip, nullptr, _blend_mode_column, _item_state_toggler);
+ return true;
+ });
+
+ _object_menu.set_relative_to(_tree);
+ _object_menu.signal_closed().connect([=](){ _item_state_toggler->set_active(false); _tree.queue_draw(); });
+ auto& modes = get_widget<Gtk::Grid>(_builder, "modes");
+ _opacity_slider.signal_format_value().connect([](double val){
+ return Util::format_number(val, 1) + "%";
+ });
+ const int min = 0, max = 100;
+ for (int i = min; i <= max; i += 50) {
+ _opacity_slider.add_mark(i, Gtk::POS_BOTTOM, "");
+ }
+ _opacity_slider.signal_value_changed().connect([=](){
+ if (current_item) {
+ auto value = _opacity_slider.get_value() / 100.0;
+ Inkscape::CSSOStringStream os;
+ os << CLAMP(value, 0.0, 1.0);
+ auto css = sp_repr_css_attr_new();
+ sp_repr_css_set_property(css, "opacity", os.str().c_str());
+ current_item->changeCSS(css, "style");
+ sp_repr_css_attr_unref(css);
+ DocumentUndo::maybeDone(current_item->document, ":opacity", _("Change opacity"), INKSCAPE_ICON("dialog-object-properties"));
+ }
+ });
+
+ // object blend mode and opacity popup
+ int top = 0;
+ int left = 0;
+ int width = 2;
+ for (size_t i = 0; i < Inkscape::SPBlendModeConverter._length; ++i) {
+ auto& data = Inkscape::SPBlendModeConverter.data(i);
+ auto label = _blend_mode_names[data.id] = g_dpgettext2(nullptr, "BlendMode", data.label.c_str());
+ if (Inkscape::SPBlendModeConverter.get_key(data.id) == "-") {
+ if (top >= (Inkscape::SPBlendModeConverter._length + 1) / 2) {
+ ++left;
+ top = 2;
+ } else if (!left) {
+ auto sep = Gtk::make_managed<Gtk::Separator>();
+ sep->show();
+ modes.attach(*sep, left, top, 2, 1);
+ }
+ } else {
+ // Manual correction that indicates this should all be done in glade
+ if (left == 1 && top == 9)
+ top++;
+
+ auto check = Gtk::make_managed<Gtk::ModelButton>();
+ check->set_label(label);
+ check->property_role().set_value(Gtk::BUTTON_ROLE_RADIO);
+ check->property_inverted().set_value(true);
+ check->property_centered().set_value(false);
+ check->set_halign(Gtk::ALIGN_START);
+ check->signal_clicked().connect([=](){
+ // set blending mode
+ if (set_blend_mode(current_item, data.id)) {
+ for (auto btn : _blend_items) {
+ btn.second->property_active().set_value(btn.first == data.id);
+ }
+ DocumentUndo::done(getDocument(), "set-blend-mode", _("Change blend mode"));
+ }
+ });
+ _blend_items[data.id] = check;
+ _blend_mode_names[data.id] = label;
+ check->show();
+ modes.attach(*check, left, top, width, 1);
+ width = 1; // First element takes whole width
+ }
+ top++;
+ }
+
+ // Visible icon
+ auto *eyeRenderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler(
+ INKSCAPE_ICON("object-hidden"), INKSCAPE_ICON("object-visible")));
+ int visibleColNum = _tree.append_column("vis", *eyeRenderer) - 1;
+ if (auto eye = _tree.get_column(visibleColNum)) {
+ eye->add_attribute(eyeRenderer->property_active(), _model->_colInvisible);
+ eye->add_attribute(eyeRenderer->property_cell_background_rgba(), _model->_colBgColor);
+ eye->add_attribute(eyeRenderer->property_activatable(), _model->_colHover);
+ eye->add_attribute(eyeRenderer->property_gossamer(), _model->_colAncestorInvisible);
+ eye->set_fixed_width(icon_col_width);
+ _eye_column = eye;
+ }
+
+ // Unlocked icon
+ Inkscape::UI::Widget::ImageToggler * lockRenderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler(
+ INKSCAPE_ICON("object-locked"), INKSCAPE_ICON("object-unlocked")));
+ int lockedColNum = _tree.append_column("lock", *lockRenderer) - 1;
+ if (auto lock = _tree.get_column(lockedColNum)) {
+ lock->add_attribute(lockRenderer->property_active(), _model->_colLocked);
+ lock->add_attribute(lockRenderer->property_cell_background_rgba(), _model->_colBgColor);
+ lock->add_attribute(lockRenderer->property_activatable(), _model->_colHover);
+ lock->add_attribute(lockRenderer->property_gossamer(), _model->_colAncestorLocked);
+ lock->set_fixed_width(icon_col_width);
+ _lock_column = lock;
+ }
+
+ // hierarchy indicator - using item's layer highlight color
+ auto tag_renderer = Gtk::manage(new Inkscape::UI::Widget::ColorTagRenderer());
+ int tag_column = _tree.append_column("tag", *tag_renderer) - 1;
+ if (auto tag = _tree.get_column(tag_column)) {
+ tag->add_attribute(tag_renderer->property_color(), _model->_colIconColor);
+ tag->add_attribute(tag_renderer->property_hover(), _model->_colHoverColor);
+ tag->set_fixed_width(tag_renderer->get_width());
+ _color_tag_column = tag;
+ }
+ tag_renderer->signal_clicked().connect([=](const Glib::ustring& path) {
+ // object's color indicator clicked - open color picker
+ _clicked_item_row = *_store->get_iter(path);
+ if (auto item = getItem(_clicked_item_row)) {
+ // find object's color
+ _color_picker.setRgba32(item->highlight_color());
+ _color_picker.open();
+ }
+ });
+
+ _color_picker.connectChanged([=](guint rgba) {
+ if (auto item = getItem(_clicked_item_row)) {
+ item->setHighlight(rgba);
+ DocumentUndo::maybeDone(getDocument(), "highlight-color", _("Set item highlight color"), INKSCAPE_ICON("dialog-object-properties"));
+ }
+ });
+
+ //Set the expander columns and search columns
+ _tree.set_expander_column(*_name_column);
+ _tree.set_search_column(-1);
+ _tree.set_enable_search(false);
+ _tree.get_selection()->set_mode(Gtk::SELECTION_NONE);
+
+ //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::_handleKeyPress), false);
+ _tree.signal_key_release_event().connect(sigc::mem_fun(*this, &ObjectsPanel::_handleKeyEvent), false);
+ _tree.signal_motion_notify_event().connect(sigc::mem_fun(*this, &ObjectsPanel::_handleMotionEvent), false);
+
+ // Set a status bar text when entering the widget
+ _tree.signal_enter_notify_event().connect([=](GdkEventCrossing*){
+ _msg_id = getDesktop()->messageStack()->push(Inkscape::NORMAL_MESSAGE,
+ _("<b>Hold ALT</b> while hovering over item to highlight, <b>hold SHIFT</b> and click to hide/lock all."));
+ return false;
+ }, false);
+ // watch mouse leave too to clear any state.
+ _tree.signal_leave_notify_event().connect([=](GdkEventCrossing*){
+ getDesktop()->messageStack()->cancel(_msg_id);
+ return _handleMotionEvent(nullptr);
+ }, false);
+
+ // Before expanding a row, replace the dummy child with the actual children
+ _tree.signal_test_expand_row().connect([this](const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &) {
+ if (cleanDummyChildren(*iter)) {
+ if (getSelection()) {
+ _selectionChanged();
+ }
+ }
+ return false;
+ });
+ _tree.signal_row_expanded().connect([=](const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &) {
+ if (auto item = getItem(*iter)) {
+ item->setExpanded(true);
+ }
+ });
+ _tree.signal_row_collapsed().connect([=](const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &) {
+ if (auto item = getItem(*iter)) {
+ item->setExpanded(false);
+ }
+ });
+
+ _tree.signal_drag_motion().connect(sigc::mem_fun(*this, &ObjectsPanel::on_drag_motion), false);
+ _tree.signal_drag_drop().connect(sigc::mem_fun(*this, &ObjectsPanel::on_drag_drop), false);
+ _tree.signal_drag_begin().connect(sigc::mem_fun(*this, &ObjectsPanel::on_drag_start), false);
+ _tree.signal_drag_end().connect(sigc::mem_fun(*this, &ObjectsPanel::on_drag_end), false);
+
+ //Set up the label editing signals
+ _text_renderer->signal_edited().connect(sigc::mem_fun(*this, &ObjectsPanel::_handleEdited));
+
+ //Set up the scroller window and pack the page
+ // turn off overlay scrollbars - they block access to the 'lock' icon
+ _scroller.set_overlay_scrolling(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);
+ }
+
+ _page.pack_start(header, false, true);
+ _page.pack_end(_scroller, Gtk::PACK_EXPAND_WIDGET);
+ pack_start(_page, Gtk::PACK_EXPAND_WIDGET);
+
+ selection_color = get_background_color(_tree.get_style_context(), Gtk::STATE_FLAG_SELECTED);
+ _tree_style = _tree.signal_style_updated().connect([=](){
+ selection_color = get_background_color(_tree.get_style_context(), Gtk::STATE_FLAG_SELECTED);
+
+ if (!root_watcher) return;
+ for (auto&& kv : root_watcher->child_watchers) {
+ if (kv.second) {
+ kv.second->updateRowHighlight();
+ }
+ }
+ });
+ // Clear and update entire tree (do not use this in changed/modified signals)
+ auto prefs = Inkscape::Preferences::get();
+ _watch_object_mode = prefs->createObserver("/dialogs/objects/layers_only", [=]() { setRootWatcher(); });
+
+ update();
+ show_all_children();
+}
+
+/**
+ * Destructor
+ */
+ObjectsPanel::~ObjectsPanel()
+{
+ if (root_watcher) {
+ delete root_watcher;
+ }
+ root_watcher = nullptr;
+
+ if (_model) {
+ delete _model;
+ _model = nullptr;
+ }
+}
+
+void ObjectsPanel::desktopReplaced()
+{
+ layer_changed.disconnect();
+
+ if (auto desktop = getDesktop()) {
+ layer_changed = desktop->layerManager().connectCurrentLayerChanged(sigc::mem_fun(*this, &ObjectsPanel::layerChanged));
+ }
+}
+
+void ObjectsPanel::documentReplaced()
+{
+ setRootWatcher();
+}
+
+void ObjectsPanel::setRootWatcher()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (root_watcher) {
+ delete root_watcher;
+ }
+ root_watcher = nullptr;
+
+ if (auto document = getDocument()) {
+ bool filtered = prefs->getBool("/dialogs/objects/layers_only", false) || _searchBox.get_text_length();
+
+ // A filtered object watcher behaves differently to an unfiltered one.
+ // Filtering disables creating dummy children and instead processes entire trees.
+ root_watcher = new ObjectWatcher(this, document->getRoot(), nullptr, filtered);
+ root_watcher->rememberExtendedItems();
+ layerChanged(getDesktop()->layerManager().currentLayer());
+ _selectionChanged();
+ }
+}
+
+/**
+ * Apply any ongoing filters to the items.
+ */
+bool ObjectsPanel::showChildInTree(SPItem *item) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ bool show_child = true;
+
+ // Filter by object type, the layers dialog here.
+ if (prefs->getBool("/dialogs/objects/layers_only", false)) {
+ auto group = cast<SPGroup>(item);
+ if (!group || group->layerMode() != SPGroup::LAYER) {
+ show_child = false;
+ }
+ }
+
+ // Filter by text search, if the search text box has any contents
+ auto term = _searchBox.get_text().lowercase();
+ if (show_child && term.length()) {
+ // A source document allows search for different pieces of metadata
+ std::stringstream source;
+ source << "#" << item->getId();
+ if (auto label = item->label())
+ source << " " << label;
+ source << " @" << item->getTagName();
+ // Might want to add class names here as ".class"
+
+ auto doc = source.str();
+ transform(doc.begin(), doc.end(), doc.begin(), ::tolower);
+ show_child = doc.find(term) != std::string::npos;
+ }
+
+ // Now the terrible bit, searching all the children causing a
+ // duplication of work as it must re-scan up the tree multiple times
+ // when the tree is very deep.
+ for (auto child_obj : item->childList(false)) {
+ if (show_child)
+ break;
+ if (auto child = cast<SPItem>(child_obj)) {
+ show_child = showChildInTree(child);
+ }
+ }
+
+ return show_child;
+}
+
+/**
+ * This both unpacks the tree, and populates lazy loading
+ */
+ObjectWatcher *ObjectsPanel::unpackToObject(SPObject *item)
+{
+ ObjectWatcher *watcher = nullptr;
+ for (auto &parent : item->ancestorList(true)) {
+ if (parent->getRepr() == root_watcher->getRepr()) {
+ watcher = root_watcher;
+ } else if (watcher) {
+ if ((watcher = watcher->findChild(parent->getRepr()))) {
+ if (auto row = watcher->getRow()) {
+ cleanDummyChildren(*row);
+ }
+ }
+ }
+ }
+ return watcher;
+}
+
+// Same definition as in 'document.cpp'
+#define SP_DOCUMENT_UPDATE_PRIORITY (G_PRIORITY_HIGH_IDLE - 2)
+
+void ObjectsPanel::selectionChanged(Selection *selected /* not used */)
+{
+ if (!_idle_connection.connected()) {
+ auto handler = sigc::mem_fun(*this, &ObjectsPanel::_selectionChanged);
+ int priority = SP_DOCUMENT_UPDATE_PRIORITY + 1;
+ _idle_connection = Glib::signal_idle().connect(handler, priority);
+ }
+}
+
+bool ObjectsPanel::_selectionChanged()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ root_watcher->setSelectedBitRecursive(SELECTED_OBJECT, false);
+ bool keep_current_item = false;
+
+ for (auto item : getSelection()->items()) {
+ keep_current_item |= (item == current_item);
+ // Failing to find the watchers here means the object is filtered out
+ // of the current object view and can be safely skipped.
+ if (auto watcher = unpackToObject(item)) {
+ if (auto child_watcher = watcher->findChild(item->getRepr())) {
+ // Expand layers themselves, but do not expand groups.
+ auto group = cast<SPGroup>(item);
+ auto focus_watcher = (group && group->isLayer()) ? child_watcher : watcher;
+ child_watcher->setSelectedBit(SELECTED_OBJECT, true);
+
+ if (prefs->getBool("/dialogs/objects/expand_to_layer", true)) {
+ _tree.expand_to_path(focus_watcher->getTreePath());
+ if (!_scroll_lock) {
+ _tree.scroll_to_row(child_watcher->getTreePath(), 0.5);
+ }
+ }
+ }
+ }
+ }
+ if (!keep_current_item) {
+ current_item = nullptr;
+ }
+ _scroll_lock = false;
+
+ // Returning 'false' disconnects idle signal handler
+ return false;
+}
+
+/**
+ * Happens when the layer selected is changed.
+ *
+ * @param layer - The layer now selected
+ */
+void ObjectsPanel::layerChanged(SPObject *layer)
+{
+ root_watcher->setSelectedBitRecursive(LAYER_FOCUS_CHILD | LAYER_FOCUSED, false);
+
+ if (!layer || !layer->getRepr()) return;
+ auto watcher = getWatcher(layer->getRepr());
+ if (watcher && watcher != root_watcher) {
+ watcher->setSelectedBitChildren(LAYER_FOCUS_CHILD, true);
+ watcher->setSelectedBit(LAYER_FOCUSED, true);
+ }
+ _layer = layer;
+}
+
+
+/**
+ * Stylizes a button using the given icon name and tooltip
+ */
+Gtk::Button* ObjectsPanel::_addBarButton(char const* iconName, char const* tooltip, char const *action_name)
+{
+ Gtk::Button* btn = Gtk::manage(new Gtk::Button());
+ auto child = Glib::wrap(sp_get_icon_image(iconName, GTK_ICON_SIZE_SMALL_TOOLBAR));
+ child->show();
+ btn->add(*child);
+ btn->set_relief(Gtk::RELIEF_NONE);
+ btn->set_tooltip_text(tooltip);
+ // Must use C API until GTK4.
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(btn->gobj()), action_name);
+ return btn;
+}
+
+/**
+ * Sets visibility of items in the tree
+ * @param iter Current item in the tree
+ */
+bool ObjectsPanel::toggleVisible(unsigned int state, Gtk::TreeModel::Row row)
+{
+ auto desktop = getDesktop();
+ auto selection = getSelection();
+
+ if (SPItem* item = getItem(row)) {
+ if (state & GDK_SHIFT_MASK) {
+ // Toggle Visible for layers (hide all other layers)
+ if (desktop->layerManager().isLayer(item)) {
+ desktop->layerManager().toggleLayerSolo(item);
+ DocumentUndo::done(getDocument(), _("Hide other layers"), "");
+ }
+ return true;
+ }
+ bool visible = !row[_model->_colInvisible];
+ if (state & GDK_CONTROL_MASK || !selection->includes(item)) {
+ item->setHidden(visible);
+ } else {
+ for (auto sitem : selection->items()) {
+ sitem->setHidden(visible);
+ }
+ }
+ // Use maybeDone so user can flip back and forth without making loads of undo items
+ DocumentUndo::maybeDone(getDocument(), "toggle-vis", _("Toggle item visibility"), "");
+ return visible;
+ }
+ return false;
+}
+
+// show blend mode popup menu for current item
+bool ObjectsPanel::blendModePopup(GdkEventButton* event, Gtk::TreeModel::Row row) {
+ if (SPItem* item = getItem(row)) {
+ current_item = nullptr;
+ auto blend = SP_CSS_BLEND_NORMAL;
+ if (item->style && item->style->mix_blend_mode.set) {
+ blend = item->style->mix_blend_mode.value;
+ }
+ auto opacity = 1.0;
+ if (item->style && item->style->opacity.set) {
+ opacity = SP_SCALE24_TO_FLOAT(item->style->opacity.value);
+ }
+ for (auto btn : _blend_items) {
+ btn.second->property_active().set_value(btn.first == blend);
+ }
+ _opacity_slider.set_value(opacity * 100);
+ current_item = item;
+
+ Gdk::Rectangle rect(event->x, event->y, 1, 1);
+ _object_menu.set_pointing_to(rect);
+ _item_state_toggler->set_active();
+ _object_menu.popup();
+ }
+
+ return true;
+}
+
+/**
+ * Sets sensitivity of items in the tree
+ * @param iter Current item in the tree
+ * @param locked Whether the item should be locked
+ */
+bool ObjectsPanel::toggleLocked(unsigned int state, Gtk::TreeModel::Row row)
+{
+ auto desktop = getDesktop();
+ auto selection = getSelection();
+
+ if (SPItem* item = getItem(row)) {
+ if (state & GDK_SHIFT_MASK) {
+ // Toggle lock for layers (lock all other layers)
+ if (desktop->layerManager().isLayer(item)) {
+ desktop->layerManager().toggleLockOtherLayers(item);
+ DocumentUndo::done(getDocument(), _("Lock other layers"), "");
+ }
+ return true;
+ }
+ bool locked = !row[_model->_colLocked];
+ if (state & GDK_CONTROL_MASK || !selection->includes(item)) {
+ item->setLocked(locked);
+ } else {
+ for (auto sitem : selection->items()) {
+ sitem->setLocked(locked);
+ }
+ }
+ // Use maybeDone so user can flip back and forth without making loads of undo items
+ DocumentUndo::maybeDone(getDocument(), "toggle-lock", _("Toggle item locking"), "");
+ return locked;
+ }
+ return false;
+}
+
+bool ObjectsPanel::_handleKeyPress(GdkEventKey *event)
+{
+ auto desktop = getDesktop();
+ if (!desktop)
+ return false;
+
+ // This isn't needed in Gtk4, use expand_collapse_cursor_row instead.
+ Gtk::TreeModel::Path path;
+ Gtk::TreeViewColumn *column;
+ _tree.get_cursor(path, column);
+
+ auto selection = getSelection();
+ bool shift = event->state & GDK_SHIFT_MASK;
+ Gtk::AccelKey shortcut = Inkscape::Shortcuts::get_from_event(event);
+ switch (shortcut.get_key()) {
+ case GDK_KEY_Escape:
+ if (desktop->canvas) {
+ desktop->canvas->grab_focus();
+ return true;
+ }
+ break;
+ case GDK_KEY_Left:
+ case GDK_KEY_KP_Left:
+ if (path && shift) {
+ _tree.collapse_row(path);
+ return true;
+ }
+ break;
+ case GDK_KEY_Right:
+ case GDK_KEY_KP_Right:
+ if (path && shift) {
+ _tree.expand_row(path, false);
+ return true;
+ }
+ break;
+ case GDK_KEY_space:
+ selectCursorItem(event->state);
+ return true;
+ // Depending on the action to cover this causes it's special
+ // text and node handling to block deletion of objects. DIY
+ case GDK_KEY_Delete:
+ case GDK_KEY_KP_Delete:
+ case GDK_KEY_BackSpace:
+ getSelection()->deleteItems();
+ // NOTE: We could select a sibling object here to make deleting many objects easier.
+ return true;
+ case GDK_KEY_Page_Up:
+ case GDK_KEY_KP_Page_Up:
+ if (shift) {
+ selection->raiseToTop();
+ return true;
+ }
+ break;
+ case GDK_KEY_Page_Down:
+ case GDK_KEY_KP_Page_Down:
+ if (shift) {
+ selection->lowerToBottom();
+ return true;
+ }
+ break;
+ case GDK_KEY_Up:
+ case GDK_KEY_KP_Up:
+ if (shift) {
+ selection->stackUp();
+ return true;
+ }
+ break;
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Down:
+ if (shift) {
+ selection->stackDown();
+ return true;
+ }
+ break;
+
+ }
+ return _handleKeyEvent(event);
+}
+
+/**
+ * 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)
+{
+ auto desktop = getDesktop();
+ if (!desktop)
+ return false;
+
+ bool press = event->type == GDK_KEY_PRESS;
+ Gtk::AccelKey shortcut = Inkscape::Shortcuts::get_from_event(event);
+ switch (shortcut.get_key()) {
+ // space and return enter label editing mode; leave them for the tree to handle
+ case GDK_KEY_space:
+ case GDK_KEY_Return:
+ return false;
+ case GDK_KEY_Alt_L:
+ case GDK_KEY_Alt_R:
+ _handleTransparentHover(press);
+ return false;
+ }
+ return false;
+}
+
+/**
+ * Handles mouse movements
+ * @param event Motion event passed in from GDK
+ * @returns Whether the event should be eaten.
+ */
+bool ObjectsPanel::_handleMotionEvent(GdkEventMotion* motion_event)
+{
+ if (_is_editing) return false;
+
+ // Unhover any existing hovered row.
+ if (_hovered_row_ref) {
+ if (auto row = *_store->get_iter(_hovered_row_ref.get_path())) {
+ row[_model->_colHover] = false;
+ row[_model->_colHoverColor] = false;
+ }
+ }
+ // Allow this function to be called by LEAVE motion
+ if (!motion_event) {
+ _hovered_row_ref = Gtk::TreeModel::RowReference();
+ _handleTransparentHover(false);
+ return false;
+ }
+
+ Gtk::TreeModel::Path path;
+ Gtk::TreeViewColumn* col = nullptr;
+ int x, y;
+ if (_tree.get_path_at_pos((int)motion_event->x, (int)motion_event->y, path, col, x, y)) {
+ // Only allow drag and drop from the name column, not any others
+ if (col == _name_column) {
+ _drag_column = nullptr;
+ }
+ // Only allow drag and drop when not filtering. Otherwise bad things happen
+ _tree.set_reorderable(col == _name_column);
+ if (auto row = *_store->get_iter(path)) {
+ row[_model->_colHover] = true;
+ _hovered_row_ref = Gtk::TreeModel::RowReference(_store, path);
+ _tree.set_cursor(path);
+
+ if (col == _color_tag_column) {
+ row[_model->_colHoverColor] = true;
+ }
+
+ // Dragging over the eye or locks will set them all
+ auto item = getItem(row);
+ if (item && _drag_column && col == _drag_column) {
+ if (col == _eye_column) {
+ // Defer visibility to th idle thread (it's expensive)
+ Glib::signal_idle().connect_once([=]() {
+ item->setHidden(_drag_flip);
+ DocumentUndo::maybeDone(getDocument(), "toggle-vis", _("Toggle item visibility"), "");
+ }, Glib::PRIORITY_DEFAULT_IDLE);
+ } else if (col == _lock_column) {
+ item->setLocked(_drag_flip);
+ DocumentUndo::maybeDone(getDocument(), "toggle-lock", _("Toggle item locking"), "");
+ }
+ }
+ }
+ }
+
+ _handleTransparentHover(motion_event->state & GDK_MOD1_MASK);
+ return false;
+}
+
+void ObjectsPanel::_handleTransparentHover(bool enabled)
+{
+ SPItem *item = nullptr;
+ if (enabled && _hovered_row_ref) {
+ if (auto row = *_store->get_iter(_hovered_row_ref.get_path())) {
+ item = getItem(row);
+ }
+ }
+
+ if (item == _solid_item)
+ return;
+
+ // Set the target item, this prevents rerunning too.
+ _solid_item = item;
+ auto desktop = getDesktop();
+
+ // Reset all the items in the list.
+ for (auto &item : _translucent_items) {
+ Inkscape::DrawingItem *arenaitem = item->get_arenaitem(desktop->dkey);
+ arenaitem->setOpacity(SP_SCALE24_TO_FLOAT(item->style->opacity.value));
+ }
+ _translucent_items.clear();
+
+ if (item) {
+ _generateTranslucentItems(getDocument()->getRoot());
+
+ for (auto &item : _translucent_items) {
+ Inkscape::DrawingItem *arenaitem = item->get_arenaitem(desktop->dkey);
+ arenaitem->setOpacity(0.2);
+ }
+ }
+}
+
+/**
+ * Generate a new list of sibling items (recursive)
+ */
+void ObjectsPanel::_generateTranslucentItems(SPItem *parent)
+{
+ if (parent == _solid_item)
+ return;
+ if (parent->isAncestorOf(_solid_item)) {
+ for (auto &child: parent->children) {
+ if (auto item = cast<SPItem>(&child)) {
+ _generateTranslucentItems(item);
+ }
+ }
+ } else {
+ _translucent_items.push_back(parent);
+ }
+}
+
+/**
+ * Handles mouse up events
+ * @param event Mouse event from GDK
+ * @return whether to eat the event (om nom nom)
+ */
+bool ObjectsPanel::_handleButtonEvent(GdkEventButton* event)
+{
+ auto selection = getSelection();
+ if (!selection)
+ return false;
+
+ if (event->type == GDK_BUTTON_RELEASE) {
+ _drag_column = nullptr;
+ }
+
+ Gtk::TreeModel::Path path;
+ Gtk::TreeViewColumn* col = nullptr;
+ int x, y;
+ if (_tree.get_path_at_pos((int)event->x, (int)event->y, path, col, x, y)) {
+ if (auto row = *_store->get_iter(path)) {
+ if (event->type == GDK_BUTTON_PRESS) {
+ // Remember column for dragging feature
+ _drag_column = col;
+ if (col == _eye_column) {
+ _drag_flip = toggleVisible(event->state, row);
+ } else if (col == _lock_column) {
+ _drag_flip = toggleLocked(event->state, row);
+ }
+ else if (col == _blend_mode_column) {
+ return blendModePopup(event, row);
+ }
+ }
+ }
+
+ // Gtk lacks the ability to detect if the user is clicking on the
+ // expander icon. So we must detect it using the cell_area check.
+ Gdk::Rectangle r;
+ _tree.get_cell_area(path, *_name_column, r);
+ bool is_expander = x < r.get_x();
+
+ if (col != _name_column || is_expander)
+ return false;
+
+ // This doesn't work, it might be being eaten.
+ if (event->type == GDK_2BUTTON_PRESS) {
+ _tree.set_cursor(path, *col, true);
+ _is_editing = true;
+ return true;
+ }
+ _is_editing = _is_editing && event->type == GDK_BUTTON_RELEASE;
+ auto row = *_store->get_iter(path);
+ if (!row) return false;
+ SPItem *item = getItem(row);
+
+ if (!item) return false;
+ auto group = cast<SPGroup>(item);
+
+ // Load the right click menu
+ const bool context_menu = event->type == GDK_BUTTON_PRESS && event->button == 3;
+
+ // Select items on button release to not confuse drag (unless it's a right-click)
+ // Right-click selects too to set up the stage for context menu which frequently relies on current selection!
+ if (!_is_editing && (event->type == GDK_BUTTON_RELEASE || context_menu)) {
+ if (context_menu) {
+ // if right-clicking on a layer, make it current for context menu actions to work correctly
+ if (group && group->layerMode() == SPGroup::LAYER && getDesktop()->layerManager().currentLayer() != item) {
+ getDesktop()->layerManager().setCurrentLayer(item, true);
+ }
+ ContextMenu *menu = new ContextMenu(getDesktop(), item, true); // true == hide menu item for opening this dialog!
+ menu->attach_to_widget(*this); // So actions work!
+ menu->show();
+ menu->popup_at_pointer(nullptr);
+ } else {
+ selectCursorItem(event->state);
+ }
+ return true;
+ } else {
+ // Remember the item for we are about to drag it!
+ current_item = item;
+ }
+ }
+ return 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)
+{
+ _is_editing = false;
+ if (auto row = *_store->get_iter(path)) {
+ if (auto item = getItem(row)) {
+ if (!new_text.empty() && (!item->label() || new_text != item->label())) {
+ item->setLabel(new_text.c_str());
+ DocumentUndo::done(getDocument(), _("Rename object"), "");
+ }
+ }
+ }
+}
+
+/**
+ * Take over the select row functionality from the TreeView, this is because
+ * we have two selections (layer and object selection) and require a custom
+ * method of rendering the result to the treeview.
+ */
+bool ObjectsPanel::select_row( Glib::RefPtr<Gtk::TreeModel> const & /*model*/, Gtk::TreeModel::Path const &path, bool /*sel*/ )
+{
+ return true;
+}
+
+/**
+ * Get the XML node which is associated with a row. Can be NULL for dummy children.
+ */
+Node *ObjectsPanel::getRepr(Gtk::TreeModel::Row const &row) const
+{
+ return row[_model->_colNode];
+}
+
+/**
+ * Get the item which is associated with a row. If getRepr(row) is not NULL,
+ * then this call is expected to also not be NULL.
+ */
+SPItem *ObjectsPanel::getItem(Gtk::TreeModel::Row const &row) const
+{
+ auto const this_const = const_cast<ObjectsPanel *>(this);
+ return cast<SPItem>(this_const->getObject(getRepr(row)));
+}
+
+/**
+ * Return true if this row has dummy children.
+ */
+bool ObjectsPanel::hasDummyChildren(Gtk::TreeModel::Row const &row) const
+{
+ for (auto &c : row.children()) {
+ if (isDummy(c)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * If the given row has dummy children, remove them.
+ * @pre Eiter all, or no children are dummies
+ * @post If the function returns true, the row has no children
+ * @return False if there are children and they are not dummies
+ */
+bool ObjectsPanel::removeDummyChildren(Gtk::TreeModel::Row const &row)
+{
+ auto &children = row.children();
+ if (!children.empty()) {
+ Gtk::TreeStore::iterator child = children[0];
+ if (!isDummy(*child)) {
+ assert(!hasDummyChildren(row));
+ return false;
+ }
+
+ do {
+ assert(child->parent() == row);
+ assert(isDummy(*child));
+ child = _store->erase(child);
+ } while (child && child->parent() == row);
+ }
+ return true;
+}
+
+bool ObjectsPanel::cleanDummyChildren(Gtk::TreeModel::Row const &row)
+{
+ if (removeDummyChildren(row)) {
+ assert(row);
+ if (auto watcher = getWatcher(getRepr(row))) {
+ watcher->addChildren(getItem(row));
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Signal handler for "drag-motion"
+ *
+ * Refuses drops into non-group items.
+ */
+bool ObjectsPanel::on_drag_motion(const Glib::RefPtr<Gdk::DragContext> &context, int x, int y, guint time)
+{
+ Gtk::TreeModel::Path path;
+ Gtk::TreeViewDropPosition pos;
+
+ auto selection = getSelection();
+ auto document = getDocument();
+
+ if (!selection || !document)
+ goto finally;
+
+ _tree.get_dest_row_at_pos(x, y, path, pos);
+
+ if (path) {
+ auto item = getItem(*_store->get_iter(path));
+
+ // don't drop on self
+ if (selection->includes(item)) {
+ goto finally;
+ }
+
+ context->drag_status(Gdk::ACTION_MOVE, time);
+ return false;
+ }
+
+finally:
+ // remove drop highlight
+ _tree.unset_drag_dest_row();
+ context->drag_refuse(time);
+ return true;
+}
+
+/**
+ * Signal handler for "drag-drop".
+ *
+ * Do the actual work of drag-and-drop.
+ */
+bool ObjectsPanel::on_drag_drop(const Glib::RefPtr<Gdk::DragContext> &context, int x, int y, guint time)
+{
+ Gtk::TreeModel::Path path;
+ Gtk::TreeViewDropPosition pos;
+ _tree.get_dest_row_at_pos(x, y, path, pos);
+
+ if (!path) {
+ return true;
+ }
+
+ auto drop_repr = getRepr(*_store->get_iter(path));
+ bool const drop_into = pos != Gtk::TREE_VIEW_DROP_BEFORE && //
+ pos != Gtk::TREE_VIEW_DROP_AFTER;
+
+ auto selection = getSelection();
+ auto document = getDocument();
+ if (selection && document) {
+ auto item = document->getObjectByRepr(drop_repr);
+ // We always try to drop the item, even if we end up dropping it after the non-group item
+ if (drop_into && is<SPGroup>(item)) {
+ selection->toLayer(item);
+ } else {
+ Node *after = (pos == Gtk::TREE_VIEW_DROP_BEFORE) ? drop_repr : drop_repr->prev();
+ selection->toLayer(item->parent, after);
+ }
+ DocumentUndo::done(document, _("Move items"), INKSCAPE_ICON("selection-move-to-layer"));
+ }
+
+ on_drag_end(context);
+ return true;
+}
+
+void ObjectsPanel::on_drag_start(const Glib::RefPtr<Gdk::DragContext> &context)
+{
+ _scroll_lock = true;
+
+ auto selection = _tree.get_selection();
+ selection->set_mode(Gtk::SELECTION_MULTIPLE);
+ selection->unselect_all();
+
+ auto obj_selection = getSelection();
+ if (!obj_selection)
+ return;
+
+ if (current_item && !obj_selection->includes(current_item)) {
+ // This means the item the user started to drag is not one that is selected
+ // So we'll deselect everything and start dragging this item instead.
+ auto watcher = getWatcher(current_item->getRepr());
+ if (watcher) {
+ auto path = watcher->getTreePath();
+ selection->select(path);
+ obj_selection->set(current_item);
+ }
+ } else {
+ // Drag all the items currently selected (multi-row)
+ for (auto item : obj_selection->items()) {
+ auto watcher = getWatcher(item->getRepr());
+ if (watcher) {
+ auto path = watcher->getTreePath();
+ selection->select(path);
+ }
+ }
+ }
+}
+
+void ObjectsPanel::on_drag_end(const Glib::RefPtr<Gdk::DragContext> &context)
+{
+ auto selection = _tree.get_selection();
+ selection->unselect_all();
+ selection->set_mode(Gtk::SELECTION_NONE);
+ current_item = nullptr;
+}
+
+/**
+ * Select the object currently under the list-cursor (keyboard or mouse)
+ */
+bool ObjectsPanel::selectCursorItem(unsigned int state)
+{
+ auto &layers = getDesktop()->layerManager();
+ auto selection = getSelection();
+ if (!selection)
+ return false;
+
+ Gtk::TreeModel::Path path;
+ Gtk::TreeViewColumn *column;
+ _tree.get_cursor(path, column);
+ if (!path || !column)
+ return false;
+
+ auto row = *_store->get_iter(path);
+ if (!row)
+ return false;
+
+ if (column == _eye_column) {
+ toggleVisible(state, row);
+ } else if (column == _lock_column) {
+ toggleLocked(state, row);
+ } else if (column == _name_column) {
+ auto item = getItem(row);
+ auto group = cast<SPGroup>(item);
+ _scroll_lock = true; // Clicking to select shouldn't scroll the treeview.
+ if (state & GDK_SHIFT_MASK && !selection->isEmpty()) {
+ // Select everything between this row and the last selected item
+ selection->setBetween(item);
+ } else if (state & GDK_CONTROL_MASK) {
+ selection->toggle(item);
+ } else if (group && selection->includes(item) && !group->isLayer()) {
+ // Clicking off a group (second click) will enter the group
+ layers.setCurrentLayer(item, true);
+ } else {
+ if (layers.currentLayer() == item) {
+ layers.setCurrentLayer(item->parent);
+ }
+ selection->set(item);
+ }
+ return true;
+ }
+ return false;
+}
+
+
+/**
+ * User pressed return in search box, process search query.
+ */
+void ObjectsPanel::_searchActivated()
+{
+ // The root watcher and watcher tree handles the search operations
+ setRootWatcher();
+}
+
+/**
+ * User has typed more into the search box
+ */
+void ObjectsPanel::_searchChanged()
+{
+ if (root_watcher->isFiltered() && !_searchBox.get_text_length()) {
+ _searchActivated();
+ }
+}
+
+} //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/objects.h b/src/ui/dialog/objects.h
new file mode 100644
index 0000000..5a19a53
--- /dev/null
+++ b/src/ui/dialog/objects.h
@@ -0,0 +1,204 @@
+// 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/builder.h>
+#include <gtkmm/dialog.h>
+#include <gtkmm/modelbutton.h>
+#include <gtkmm/popover.h>
+#include <gtkmm/scale.h>
+
+#include "helper/auto-connection.h"
+#include "xml/node-observer.h"
+
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/color-picker.h"
+#include "ui/widget/preferences-widget.h"
+
+#include "selection.h"
+#include "style-enums.h"
+#include "color-rgba.h"
+
+using Inkscape::XML::Node;
+using namespace Inkscape::UI::Widget;
+
+class SPObject;
+class SPGroup;
+// struct SPColorSelector;
+
+namespace Inkscape {
+namespace UI {
+
+namespace Widget { class ImageToggler; }
+namespace Dialog {
+
+class ObjectsPanel;
+class ObjectWatcher;
+
+enum {COL_LABEL, COL_VISIBLE, COL_LOCKED};
+
+using SelectionState = int;
+enum SelectionStates : SelectionState {
+ SELECTED_NOT = 0, // Object is NOT in desktop's selection
+ SELECTED_OBJECT = 1, // Object is in the desktop's selection
+ LAYER_FOCUSED = 2, // This layer is the desktop's focused layer
+ LAYER_FOCUS_CHILD = 4 // This object is a child of the focused layer
+};
+
+/**
+ * A panel that displays objects.
+ */
+class ObjectsPanel : public DialogBase
+{
+public:
+ ObjectsPanel();
+ ~ObjectsPanel() override;
+
+ class ModelColumns;
+
+protected:
+ void desktopReplaced() override;
+ void documentReplaced() override;
+ void layerChanged(SPObject *obj);
+ void selectionChanged(Selection *selected) override;
+ ObjectWatcher *unpackToObject(SPObject *item);
+
+ // Accessed by ObjectWatcher directly (friend class)
+ SPObject* getObject(Node *node);
+ ObjectWatcher* getWatcher(Node *node);
+ ObjectWatcher *getRootWatcher() const { return root_watcher; };
+ bool showChildInTree(SPItem *item);
+
+ Node *getRepr(Gtk::TreeModel::Row const &row) const;
+ SPItem *getItem(Gtk::TreeModel::Row const &row) const;
+ std::optional<Gtk::TreeRow> getRow(SPItem *item) const;
+
+ bool isDummy(Gtk::TreeModel::Row const &row) const { return getRepr(row) == nullptr; }
+ bool hasDummyChildren(Gtk::TreeModel::Row const &row) const;
+ bool removeDummyChildren(Gtk::TreeModel::Row const &row);
+ bool cleanDummyChildren(Gtk::TreeModel::Row const &row);
+
+ Glib::RefPtr<Gtk::TreeStore> _store;
+ ModelColumns* _model;
+
+ void setRootWatcher();
+private:
+
+ Glib::RefPtr<Gtk::Builder> _builder;
+ Inkscape::PrefObserver _watch_object_mode;
+ ObjectWatcher* root_watcher;
+ SPItem *current_item = nullptr;
+
+ Inkscape::auto_connection layer_changed;
+ SPObject *_layer;
+ Gtk::TreeModel::RowReference _hovered_row_ref;
+
+ //Show icons in the context menu
+ bool _show_contextmenu_icons;
+ bool _is_editing;
+ bool _scroll_lock = false;
+
+ 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::TreeView::Column *_blend_mode_column = nullptr;
+ Gtk::TreeView::Column *_eye_column = nullptr;
+ Gtk::TreeView::Column *_lock_column = nullptr;
+ Gtk::TreeView::Column *_color_tag_column = nullptr;
+ Gtk::Box _buttonsRow;
+ Gtk::Box _buttonsPrimary;
+ Gtk::Box _buttonsSecondary;
+ Gtk::SearchEntry& _searchBox;
+ Gtk::ScrolledWindow _scroller;
+ Gtk::Menu _popupMenu;
+ Gtk::Box _page;
+ Inkscape::auto_connection _tree_style;
+ Inkscape::UI::Widget::ColorPicker _color_picker;
+ Gtk::TreeRow _clicked_item_row;
+
+ Gtk::Button *_addBarButton(char const* iconName, char const* tooltip, char const *action_name);
+
+ bool blendModePopup(GdkEventButton* event, Gtk::TreeModel::Row row);
+ bool toggleVisible(unsigned int state, Gtk::TreeModel::Row row);
+ bool toggleLocked(unsigned int state, Gtk::TreeModel::Row row);
+
+ bool _handleButtonEvent(GdkEventButton *event);
+ bool _handleKeyPress(GdkEventKey *event);
+ bool _handleKeyEvent(GdkEventKey *event);
+ bool _handleMotionEvent(GdkEventMotion* motion_event);
+ void _searchActivated();
+ void _searchChanged();
+
+ void _handleEdited(const Glib::ustring& path, const Glib::ustring& new_text);
+ void _handleTransparentHover(bool enabled);
+ void _generateTranslucentItems(SPItem *parent);
+
+ bool select_row( Glib::RefPtr<Gtk::TreeModel> const & model, Gtk::TreeModel::Path const & path, bool b );
+
+ bool on_drag_motion(const Glib::RefPtr<Gdk::DragContext> &, int, int, guint) override;
+ bool on_drag_drop(const Glib::RefPtr<Gdk::DragContext> &, int, int, guint) override;
+ void on_drag_start(const Glib::RefPtr<Gdk::DragContext> &);
+ void on_drag_end(const Glib::RefPtr<Gdk::DragContext> &) override;
+
+ bool selectCursorItem(unsigned int state);
+ SPItem *_getCursorItem(Gtk::TreeViewColumn *column);
+
+ friend class ObjectWatcher;
+
+ SPItem *_solid_item;
+ std::list<SPItem *> _translucent_items;
+ int _msg_id;
+ Gtk::Popover& _settings_menu;
+ Gtk::Popover& _object_menu;
+ Gtk::Scale& _opacity_slider;
+ std::map<SPBlendMode, Gtk::ModelButton*> _blend_items;
+ std::map<SPBlendMode, Glib::ustring> _blend_mode_names;
+ Inkscape::UI::Widget::ImageToggler* _item_state_toggler;
+ // Special column dragging mode
+ Gtk::TreeViewColumn* _drag_column = nullptr;
+ PrefCheckButton& _setting_layers;
+ PrefCheckButton& _setting_track;
+ bool _drag_flip;
+
+ bool _selectionChanged();
+ auto_connection _idle_connection;
+};
+
+
+
+} //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..42f437c
--- /dev/null
+++ b/src/ui/dialog/paint-servers.cpp
@@ -0,0 +1,657 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Paint Servers dialog
+ */
+/* Authors:
+ * Valentin Ionita
+ * Rafael Siejakowski <rs@rs-math.net>
+ *
+ * Copyright (C) 2019 Valentin Ionita
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <algorithm>
+#include <map>
+
+#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 "io/resource.h"
+#include "object/sp-defs.h"
+#include "object/sp-hatch.h"
+#include "object/sp-pattern.h"
+#include "object/sp-root.h"
+#include "ui/cache/svg_preview_cache.h"
+#include "ui/widget/scrollprotected.h"
+#include "xml/href-attribute-helper.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>
+)=====";
+
+const char *ALLDOCS = N_("All paint servers");
+const char *CURRENTDOC = N_("Current document");
+
+PaintServersDialog::PaintServersDialog()
+ : DialogBase("/dialogs/paint", "PaintServers")
+ , _targetting_fill(true)
+ , columns()
+{
+ current_store = ALLDOCS;
+ store[ALLDOCS] = Gtk::ListStore::create(columns);
+
+ // 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) {
+ g_warn_message("Inkscape", __FILE__, __LINE__, __func__,
+ "Failed to get wrapper defs or rectangle for 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));
+
+ _buildDialogWindow("dialog-paint-servers.glade");
+ _loadStockPaints();
+}
+
+
+PaintServersDialog::~PaintServersDialog()
+{
+ _defs_changed.disconnect();
+ _document_closed.disconnect();
+}
+
+/** Handles the replacement of the document that we edit */
+void PaintServersDialog::documentReplaced()
+{
+ _defs_changed.disconnect();
+ _document_closed.disconnect();
+
+ auto document = getDocument();
+ if (!document) {
+ return;
+ }
+ document_map[CURRENTDOC] = document;
+ _loadFromCurrentDocument();
+ _regenerateAll();
+
+ if (auto const defs = document->getDefs()) {
+ _defs_changed = defs->connectModified([=](SPObject *, unsigned) -> void {
+ _loadFromCurrentDocument();
+ _regenerateAll();
+ });
+ }
+ _document_closed = document->connectDestroy([=]() { _documentClosed(); });
+}
+
+/** Builds the dialog window from a Glade file and attaches event handlers */
+void PaintServersDialog::_buildDialogWindow(char const *const glade_file)
+{
+ // Load the dialog from the Glade file
+ auto file_path = get_filename_string(IO::Resource::UIS, glade_file);
+ Glib::RefPtr<Gtk::Builder> builder;
+ try {
+ builder = Gtk::Builder::create_from_file(file_path);
+ } catch (Glib::Error const &e) {
+ Glib::ustring message{"Could not load the Glade file for the Paint Servers dialog: "};
+ message += file_path + "\n" + e.what();
+ g_warn_message("Inkscape", __FILE__, __LINE__, __func__, message.c_str());
+ return;
+ }
+
+ // Place top-level grid container in the window
+ Gtk::Grid *container = nullptr;
+ builder->get_widget("PaintServersContainerGrid", container);
+ if (container) {
+ pack_start(*container, Gtk::PACK_EXPAND_WIDGET);
+ } else {
+ return;
+ }
+
+ builder->get_widget("ServersDropdown", dropdown);
+ dropdown->append(ALLDOCS, _(ALLDOCS));
+ dropdown->set_active_id(ALLDOCS);
+ dropdown->signal_changed().connect([=]() { onPaintSourceDocumentChanged(); });
+
+ builder->get_widget("PaintIcons", icon_view);
+ icon_view->set_model(static_cast<Glib::RefPtr<Gtk::TreeModel>>(store[current_store]));
+ icon_view->set_tooltip_column(columns.id.index());
+ icon_view->set_pixbuf_column(columns.pixbuf.index());
+ _item_activated = icon_view->signal_item_activated().connect([=](Gtk::TreeModel::Path const &p) {
+ onPaintClicked(p);
+ });
+
+ Gtk::RadioButton *fill_radio = nullptr;
+ builder->get_widget("TargetRadioFill", fill_radio);
+ fill_radio->signal_toggled().connect([=]() {
+ _targetting_fill = fill_radio->get_active();
+ _updateActiveItem();
+ });
+}
+
+/** Handles the destruction of the current document */
+void PaintServersDialog::_documentClosed()
+{
+ _defs_changed.disconnect();
+ _document_closed.disconnect();
+
+ document_map.erase(CURRENTDOC);
+ store[CURRENTDOC]->clear();
+ _regenerateAll();
+}
+
+/**
+ * @brief Returns the Fill and Stroke, in this order, common to a list of objects
+ * @param objects - a vector of pointers to objects whose fill and stroke will be analysed
+ * @return a tuple of optionals which are empty when the vector of objects is empty or
+ * when either fill or stroke are not common to all of them. Otherwise, the first
+ * element of the tuple is the common fill, as a Glib::ustring, and the second one
+ * is the common stroke.
+ */
+std::tuple<std::optional<Glib::ustring>, std::optional<Glib::ustring>>
+PaintServersDialog::_findCommonFillAndStroke(std::vector<SPObject *> const &objects) const
+{
+ MaybeString common_fill, common_stroke;
+ if (!objects.empty()) {
+ Glib::ustring candidate_fill = objects[0]->style->fill.get_value();
+ Glib::ustring candidate_stroke = objects[0]->style->stroke.get_value();
+ bool fills_agree = true;
+ bool strokes_agree = true;
+ size_t const count = objects.size();
+ for (size_t i = 1; i < count; i++) {
+ if (fills_agree && candidate_fill != objects[i]->style->fill.get_value()) {
+ fills_agree = false;
+ }
+ if (strokes_agree && candidate_stroke != objects[i]->style->stroke.get_value()) {
+ strokes_agree = false;
+ }
+ }
+ if (fills_agree) {
+ common_fill = candidate_fill;
+ }
+ if (strokes_agree) {
+ common_stroke = candidate_stroke;
+ }
+ }
+ return std::tuple(common_fill, common_stroke);
+}
+
+/** Finds paints used by an object and (recursively) by its descendants
+ * @param in - the object whose paints to grab
+ * @param list - the paints will be added to this vector as strings usable in the `fill` CSS property
+ */
+void PaintServersDialog::_findPaints(SPObject *in, std::vector<Glib::ustring> &list)
+{
+ g_return_if_fail(in != nullptr);
+
+ // Add paint servers in <defs> section.
+ if (is<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 (is<SPShape>(in)) {
+ auto const style = in->style;
+ list.push_back(style->fill.get_value());
+ list.push_back(style->stroke.get_value());
+ }
+
+ for (auto child: in->childList(false)) {
+ PaintServersDialog::_findPaints(child, list);
+ }
+}
+
+/** Load stock paints from files in share/paint */
+void PaintServersDialog::_loadStockPaints()
+{
+ std::vector<PaintDescription> paints;
+
+ // Extract out paints from files in share/paint.
+ for (auto const &path : get_filenames(Inkscape::IO::Resource::PAINT, {".svg"})) {
+ try { // createNewDoc throws
+ auto doc = std::unique_ptr<SPDocument>(SPDocument::createNewDoc(path.c_str(), false));
+ if (!doc) {
+ throw std::exception();
+ }
+ _loadPaintsFromDocument(doc.get(), paints);
+ _stock_documents.push_back(std::move(doc)); // Ensures eventual destruction in our dtor
+ } catch (std::exception &e) {
+ auto message = Glib::ustring{"Cannot open paint server resource file '"} + path + "'!";
+ g_warn_message("Inkscape", __FILE__, __LINE__, __func__, message.c_str());
+ continue;
+ }
+ }
+
+ _createPaints(paints);
+}
+
+/** Load paint servers from the <defs> of the current document */
+void PaintServersDialog::_loadFromCurrentDocument()
+{
+ auto document = getDocument();
+ if (!document) {
+ return;
+ }
+
+ std::vector<PaintDescription> paints;
+ _loadPaintsFromDocument(document, paints);
+
+ // There can only be one current document, so we clear the corresponding store
+ store[CURRENTDOC]->clear();
+ _createPaints(paints);
+}
+
+/** Creates a collection of paints from the given vector of descriptions */
+void PaintServersDialog::_createPaints(std::vector<PaintDescription> &collection)
+{
+ // Sort and remove duplicates.
+ auto paints_cmp = [](PaintDescription const &a, PaintDescription const &b) -> bool {
+ return a.url < b.url;
+ };
+ std::sort(collection.begin(), collection.end(), paints_cmp);
+ collection.erase(std::unique(collection.begin(), collection.end()), collection.end());
+
+ for (auto &paint : collection) {
+ _instantiatePaint(paint);
+ }
+}
+
+/** Create a paint from a description and generate its bitmap preview */
+void PaintServersDialog::_instantiatePaint(PaintDescription &paint)
+{
+ if (!paint.has_preview()) {
+ _generateBitmapPreview(paint);
+ }
+ if (paint.has_preview()) { // don't add the paint if preview generation failed.
+ _addToStore(paint);
+ }
+}
+
+/** Adds a paint to store */
+void PaintServersDialog::_addToStore(PaintDescription &paint)
+{
+ if (store.find(paint.doc_title) == store.end()) {
+ store[paint.doc_title] = Gtk::ListStore::create(columns);
+ }
+
+ auto iter = store[paint.doc_title]->append();
+ paint.write_to_iterator(iter, &columns);
+
+ if (document_map.find(paint.doc_title) == document_map.end()) {
+ document_map[paint.doc_title] = paint.source_document;
+ dropdown->append(paint.doc_title, _(paint.doc_title.c_str()));
+ }
+}
+
+/** Returns a PaintDescription for a paint already present in the store */
+PaintDescription PaintServersDialog::_descriptionFromIterator(Gtk::ListStore::iterator const &iter) const
+{
+ Glib::ustring doc_title = (*iter)[columns.document];
+ SPDocument *doc_ptr;
+ try {
+ doc_ptr = document_map.at(doc_title);
+ } catch (std::out_of_range &exception) {
+ doc_ptr = nullptr;
+ }
+ Glib::ustring paint_url = (*iter)[columns.paint];
+ PaintDescription result(doc_ptr, doc_title, std::move(paint_url));
+
+ // Fill in fields that are set only on instantiation
+ result.id = (*iter)[columns.id];
+ result.bitmap = (*iter)[columns.pixbuf];
+ return result;
+}
+
+/** Regenerates the list of all paint servers from the already loaded paints */
+void PaintServersDialog::_regenerateAll()
+{
+ bool showing_all = (current_store == ALLDOCS);
+ std::vector<PaintDescription> all_paints;
+
+ for (auto const &[doc, paint_list] : store) {
+ if (doc == ALLDOCS) {
+ continue; // ignore the target store
+ }
+ paint_list->foreach_iter([&](Gtk::ListStore::iterator const &paint) -> bool
+ {
+ all_paints.push_back(_descriptionFromIterator(paint));
+ return false;
+ });
+ }
+
+ // Sort and remove duplicates. When the duplicate entry is from the current document,
+ // we remove it preferentially, keeping the stock paint if available.
+ std::sort(all_paints.begin(), all_paints.end(),
+ [=](PaintDescription const &a, PaintDescription const &b) -> bool
+ {
+ int cmp = a.url.compare(b.url);
+ if (cmp < 0) return true;
+ if (cmp > 0) return false;
+
+ auto external = [=] (PaintDescription const &p) { return p.doc_title != CURRENTDOC; };
+ return external(a) && !external(b);
+ });
+ all_paints.erase(std::unique(all_paints.begin(), all_paints.end()), all_paints.end());
+
+ store[ALLDOCS]->clear();
+
+ // Add paints from the cleaned up list to the store
+ for (auto &&paint : all_paints) {
+ auto iter = store[ALLDOCS]->append();
+ paint.write_to_iterator(iter, &columns);
+ }
+
+ if (showing_all) {
+ selectionChanged(getSelection());
+ }
+}
+
+/** Generates the bitmap preview for the given paint */
+void PaintServersDialog::_generateBitmapPreview(PaintDescription &paint)
+{
+ SPObject *rect = preview_document->getObjectById("Rect");
+ SPObject *defs = preview_document->getObjectById("Defs");
+
+ paint.bitmap = Glib::RefPtr<Gdk::Pixbuf>(nullptr);
+ if (paint.url.empty()) {
+ return;
+ }
+
+ // Set style on the preview rectangle
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_set_property(css, "fill", paint.url.c_str());
+ rect->changeCSS(css, "style");
+ sp_repr_css_attr_unref(css);
+
+ // Insert paint into the defs of the preview document if required
+ Glib::MatchInfo matchInfo;
+ static Glib::RefPtr<Glib::Regex> regex = Glib::Regex::create("url\\(#([A-Za-z0-9#._-]*)\\)");
+
+ regex->match(paint.url, matchInfo);
+ if (!matchInfo.matches()) {
+ // Currently we only show previews for hatches/patterns of the form url(#some-id)
+ // TODO: handle colors, gradients, etc.
+ // See https://wiki.inkscape.org/wiki/Google_Summer_of_Code#P11._Improvements_to_Paint_Server_Dialog
+ return;
+ }
+ paint.id = matchInfo.fetch(1);
+
+ // Delete old paints if necessary
+ std::vector<SPObject *> old_paints = preview_document->getObjectsBySelector("defs > *");
+ for (auto paint : old_paints) {
+ paint->deleteObject(false);
+ }
+
+ // Find the new paint
+ SPObject *new_paint = paint.source_document->getObjectById(paint.id);
+ if (!new_paint) {
+ Glib::ustring error_message = Glib::ustring{"Cannot find paint server: "} + paint.id;
+ g_warn_message("Inkscape", __FILE__, __LINE__, __func__, error_message.c_str());
+ return;
+ }
+
+ // Add the new paint along with all paints it refers to
+ XML::Document *xml_doc = preview_document->getReprDoc();
+ std::vector<SPObject *> encountered{new_paint}; ///< For the prevention of cyclic refs
+
+ while (new_paint) {
+ auto const *new_repr = new_paint->getRepr();
+ if (!new_repr) {
+ break;
+ }
+
+ // Create a copy repr of the paint
+ defs->appendChild(new_repr->duplicate(xml_doc));
+
+ // Check for cross-references in the paint
+ auto ref = Inkscape::getHrefAttribute(*new_repr).second;
+ if (ref) {
+ // Paint is cross-referencing another object (probably another paint);
+ // we must copy the referenced object as well
+ new_paint = paint.source_document->getObjectByHref(ref);
+ using namespace std;
+ if (find(begin(encountered), end(encountered), new_paint) == end(encountered)) {
+ encountered.push_back(new_paint);
+ } else {
+ break; // Break reference cycle
+ }
+ } else { // No more hrefs
+ break;
+ }
+ }
+
+ preview_document->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ preview_document->ensureUpToDate();
+
+ if (Geom::OptRect dbox = static_cast<SPItem *>(rect)->visualBounds())
+ {
+ unsigned size = std::ceil(std::max(dbox->width(), dbox->height()));
+ paint.bitmap = Glib::wrap(render_pixbuf(renderDrawing, 1, *dbox, size));
+ }
+}
+
+/** @brief Load paint servers from the given source document
+ * @param document - the source document
+ * @param output - the paint descriptions will be added to this vector */
+void PaintServersDialog::_loadPaintsFromDocument(SPDocument *document, std::vector<PaintDescription> &output)
+{
+ Glib::ustring document_title;
+ if (!document->getRoot()->title()) {
+ document_title = CURRENTDOC;
+ } else {
+ document_title = Glib::ustring(document->getRoot()->title());
+ }
+
+ // Find all paints
+ std::vector<Glib::ustring> urls;
+ _findPaints(document->getRoot(), urls);
+
+ for (auto const &url : urls) {
+ output.emplace_back(document, document_title, std::move(url));
+ }
+}
+
+/** Handles the change of the dropdown for selecting paint sources */
+void PaintServersDialog::onPaintSourceDocumentChanged()
+{
+ current_store = dropdown->get_active_id();
+ icon_view->set_model(store[current_store]);
+ _updateActiveItem();
+}
+
+/** Event handler for when a paint entry in the dialog has been activated */
+void PaintServersDialog::onPaintClicked(Gtk::TreeModel::Path const &path)
+{
+ // Get the current selected elements
+ Selection *selection = getSelection();
+ std::vector<SPObject *> items = _unpackSelection(selection);
+
+ if (items.empty()) {
+ return;
+ }
+
+ 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 hatches_document_title = (*iter)[columns.document];
+ SPDocument *hatches_document = document_map[hatches_document_title];
+ SPObject *paint_server = hatches_document->getObjectById(id);
+
+ bool paint_server_exists = false;
+ for (auto const &server : store[CURRENTDOC]->children()) {
+ if (server[columns.id] == id) {
+ paint_server_exists = true;
+ break;
+ }
+ }
+
+ SPDocument *document = getDocument();
+ if (!paint_server_exists) {
+ // Add the paint server to the current document definition
+ Inkscape::XML::Document *xml_doc = document->getReprDoc();
+ Inkscape::XML::Node *repr = paint_server->getRepr()->duplicate(xml_doc);
+ document->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] = CURRENTDOC;
+ }
+
+ for (auto item : items) {
+ item->style->getFillOrStroke(_targetting_fill)->read(paint.c_str());
+ item->updateRepr();
+ }
+
+ document->collectOrphans();
+}
+
+/**
+ * @brief Handles the change in the selection, finding common fill or stroke for selected objects
+ * @param selection - the new selection
+ */
+void PaintServersDialog::selectionChanged(Selection* selection)
+{
+ if (!selection || selection->isEmpty()) {
+ _common_fill.reset();
+ _common_stroke.reset();
+ } else {
+ auto const selected_items = _unpackSelection(selection);
+ auto const &[fill, stroke] = _findCommonFillAndStroke(selected_items);
+ _common_fill = std::move(fill);
+ _common_stroke = std::move(stroke);
+ }
+ _updateActiveItem();
+}
+
+/**
+ * Recursively extracts non-group elements from groups, if any
+ * @param parent - the parent object which will be unpacked recursively
+ * @param output - the resulting SPObject pointers will be added to this vector
+ */
+void PaintServersDialog::_unpackGroups(SPObject *parent, std::vector<SPObject *> &output) const
+{
+ std::vector<SPObject *> children = parent->childList(false);
+ if (children.empty()) {
+ output.push_back(parent);
+ } else {
+ for (auto child : children) {
+ _unpackGroups(child, output);
+ }
+ }
+}
+
+/**
+ * @brief Recursively unpacks groups in the given selection
+ * @param selection - a pointer to an Inkscape::Selection object to be unpacked
+ * @return a vector of SPObject pointers to the unpacked selected items.
+ */
+std::vector<SPObject *> PaintServersDialog::_unpackSelection(Selection *selection) const
+{
+ std::vector<SPObject *> result;
+ if (!selection) {
+ return result;
+ }
+
+ auto const &selected_range = selection->items();
+ for (auto const item : selected_range) {
+ _unpackGroups(static_cast<SPObject *>(item), result);
+ }
+ return result;
+}
+
+/**
+ * @brief Updates the active item in the icon view to reflect the common paint of the
+ * fill or stroke of selected objects
+ */
+void PaintServersDialog::_updateActiveItem()
+{
+ _item_activated.block();
+ MaybeString &common = (_targetting_fill ? _common_fill : _common_stroke);
+ if (common) {
+ bool found = false;
+ store[current_store]->foreach(
+ [&](Gtk::ListStore::Path const &path, Gtk::ListStore::iterator const &icon) -> bool {
+ if ((*icon)[columns.paint] == *common) {
+ icon_view->select_path(path);
+ found = true;
+ return true; // Finish iterating
+ }
+ return false;
+ });
+ if (!found) {
+ icon_view->unselect_all();
+ }
+ } else {
+ icon_view->unselect_all();
+ }
+ _item_activated.unblock();
+}
+
+//----------------------------------------------------------------------------------------------------
+
+PaintDescription::PaintDescription(SPDocument *source_doc, Glib::ustring title, Glib::ustring const &&paint_url)
+ : source_document{source_doc}
+ , doc_title{std::move(title)}
+ , id{} // id will be filled in when generating the bitmap
+ , url{paint_url}
+ , bitmap{nullptr}
+{}
+
+/** Write the data stored in this struct to a list store
+ * @param it - the iterator to the ListStore to write to
+ */
+void PaintDescription::write_to_iterator(Gtk::ListStore::iterator &it, PaintServersColumns const *cols) const {
+ (*it)[cols->id] = id;
+ (*it)[cols->paint] = url;
+ (*it)[cols->pixbuf] = bitmap;
+ (*it)[cols->document] = doc_title;
+}
+
+} // 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..c170318
--- /dev/null
+++ b/src/ui/dialog/paint-servers.h
@@ -0,0 +1,144 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Paint Servers dialog
+ */
+/* Authors:
+ * Valentin Ionita
+ * Rafael Siejakowski <rs@rs-math.net>
+ *
+ * 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 <sigc++/sigc++.h>
+
+#include "display/drawing.h"
+#include "ui/dialog/dialog-base.h"
+
+class SPObject;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+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);
+ }
+};
+
+struct PaintDescription
+{
+ /** Pointer to the document from which the paint originates */
+ SPDocument *source_document = nullptr;
+
+ /** Title of the document from which the paint originates, or "Current document" */
+ Glib::ustring doc_title;
+
+ /** ID of the the paint server within the document */
+ Glib::ustring id;
+
+ /** URL of the paint within the document */
+ Glib::ustring url;
+
+ /** Bitmap preview of the paint */
+ Glib::RefPtr<Gdk::Pixbuf> bitmap;
+
+ PaintDescription(SPDocument *source_doc, Glib::ustring title, Glib::ustring const &&paint_url);
+ void write_to_iterator(Gtk::ListStore::iterator &it, PaintServersColumns const *cols) const;
+
+ /** Whether the paint with this description has a valid pixbuf preview */
+ inline bool has_preview() const { return (bool)bitmap; }
+
+ /** Two paints are considered the same if they have the same urls */
+ inline bool operator==(PaintDescription const &other) const { return url == other.url; }
+};
+
+/**
+ * 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 DialogBase
+{
+ using MaybeString = std::optional<Glib::ustring>;
+
+public:
+ PaintServersDialog();
+ ~PaintServersDialog() override;
+
+ void documentReplaced() override;
+
+private:
+ void _addToStore(PaintDescription &paint);
+ void _buildDialogWindow(char const *const glade_file);
+ void _createPaints(std::vector<PaintDescription> &collection);
+ PaintDescription _descriptionFromIterator(Gtk::ListStore::iterator const &iter) const;
+ void _documentClosed();
+ std::tuple<MaybeString, MaybeString> _findCommonFillAndStroke(std::vector<SPObject *> const &objects) const;
+ static void _findPaints(SPObject *in, std::vector<Glib::ustring> &list);
+ void _generateBitmapPreview(PaintDescription& paint);
+ void _instantiatePaint(PaintDescription &paint);
+ void _loadFromCurrentDocument();
+ void _loadPaintsFromDocument(SPDocument *document, std::vector<PaintDescription> &output);
+ void _loadStockPaints();
+ void _regenerateAll();
+ void _unpackGroups(SPObject *parent, std::vector<SPObject *> &output) const;
+ std::vector<SPObject *> _unpackSelection(Selection *selection) const;
+ void _updateActiveItem();
+ void onPaintClicked(const Gtk::TreeModel::Path &path);
+ void onPaintSourceDocumentChanged();
+ void selectionChanged(Selection *selection) final;
+
+ bool _targetting_fill; ///< whether setting fill (true) or stroke (false)
+ std::map<Glib::ustring, Glib::RefPtr<Gtk::ListStore>> store;
+ Glib::ustring current_store;
+ std::vector<std::unique_ptr<SPDocument>> _stock_documents;
+ std::map<Glib::ustring, SPDocument *> document_map;
+ SPDocument *preview_document = nullptr;
+ Inkscape::Drawing renderDrawing;
+ Gtk::ComboBoxText *dropdown = nullptr;
+ Gtk::IconView *icon_view = nullptr;
+ PaintServersColumns const columns;
+ sigc::connection _defs_changed, _document_closed;
+ MaybeString _common_stroke, _common_fill; ///< Common fill/stroke to all selected elements
+ sigc::connection _item_activated;
+};
+
+} // 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/polar-arrange-tab.cpp b/src/ui/dialog/polar-arrange-tab.cpp
new file mode 100644
index 0000000..960048e
--- /dev/null
+++ b/src/ui/dialog/polar-arrange-tab.cpp
@@ -0,0 +1,409 @@
+// 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 "object/sp-ellipse.h"
+#include "object/sp-item-transform.h"
+
+#include "ui/dialog/polar-arrange-tab.h"
+#include "ui/dialog/tile.h"
+#include "ui/icon-names.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);
+
+ parametersTable.show_all();
+ parametersTable.set_no_show_all();
+ parametersTable.hide();
+}
+
+/**
+ * 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)
+ {
+ if(is<SPGenericEllipse>(item))
+ referenceEllipse = cast<SPGenericEllipse>(item);
+ } else {
+ if(is<SPGenericEllipse>(item) && referenceEllipse == nullptr)
+ referenceEllipse = cast<SPGenericEllipse>(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(), _("Arrange on ellipse"), INKSCAPE_ICON("dialog-align-and-distribute"));
+}
+
+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.cpp b/src/ui/dialog/print.cpp
new file mode 100644
index 0000000..da35219
--- /dev/null
+++ b/src/ui/dialog/print.cpp
@@ -0,0 +1,308 @@
+// 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 "object/sp-page.h"
+
+#include "util/units.h"
+#include "helper/png-write.h"
+#include "page-manager.h"
+#include "svg/svg-color.h"
+
+#include <glibmm/i18n.h>
+
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+Glib::RefPtr<Gtk::PrintSettings> &get_printer_settings()
+{
+ static Glib::RefPtr<Gtk::PrintSettings> printer_settings;
+ return printer_settings;
+}
+
+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
+ set_paper_size(page_setup, _doc->getWidth().value("pt"), _doc->getHeight().value("pt"));
+ _printop->set_default_page_setup(page_setup);
+ _printop->set_use_full_page(true);
+ _printop->set_n_pages(1);
+
+ // Now process actual multi-page setup.
+ auto &pm = _doc->getPageManager();
+ if (pm.hasPages()) {
+ // This appears to be limiting which pages get rendered
+ _printop->set_n_pages(pm.getPageCount());
+ _printop->set_current_page(pm.getSelectedPageIndex());
+ _printop->signal_request_page_setup().connect(sigc::mem_fun(*this, &Print::setup_page));
+ }
+
+ // 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"));
+}
+
+/**
+ * Return the required page setup, only connected for multi-page documents
+ * and only required where there are pages of different sizes.
+ */
+void Print::setup_page(const Glib::RefPtr<Gtk::PrintContext>& context, int page_nr,
+ const Glib::RefPtr<Gtk::PageSetup> &setup)
+{
+ auto &pm = _workaround._doc->getPageManager();
+ if (auto page = pm.getPage(page_nr)) {
+ auto rect = page->getDesktopRect();
+ auto width = Inkscape::Util::Quantity::convert(rect.width(), "px", "pt");
+ auto height = Inkscape::Util::Quantity::convert(rect.height(), "px", "pt");
+ set_paper_size(setup, width, height);
+ }
+}
+
+/**
+ * Set the paper size with correct orientation.
+ */
+void Print::set_paper_size(const Glib::RefPtr<Gtk::PageSetup> &page_setup, double page_width, double page_height)
+{
+ auto p_size = Gtk::PaperSize("custom", "custom", page_width, page_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 (page_width > page_height) {
+ orientation = Gtk::PAGE_ORIENTATION_REVERSE_LANDSCAPE;
+ std::swap(page_width, page_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) - page_width) >= 1.0) {
+ // width (short edge) doesn't match
+ continue;
+ }
+ if (fabs(size.get_height(Gtk::UNIT_POINTS) - page_height) >= 1.0) {
+ // height (short edge) doesn't match
+ continue;
+ }
+ // size matches
+ p_size = size;
+ break;
+ }
+ page_setup->set_paper_size(p_size);
+ page_setup->set_orientation(orientation);
+}
+
+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);
+
+ auto &pm = _workaround._doc->getPageManager();
+ auto page = pm.getPage(page_nr); // nullptr when no pages.
+
+ if (_workaround._tab->as_bitmap()) {
+ // Render as exported PNG
+ prefs->setBool("/dialogs/printing/asbitmap", true);
+ gdouble dpi = _workaround._tab->bitmap_dpi();
+ prefs->setDouble("/dialogs/printing/dpi", dpi);
+
+ auto rect = *(_workaround._doc->preferredBounds());
+ if (page) {
+ rect = page->getDesktopRect();
+ }
+
+ 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 = nv->getAttributeDouble("inkscape:pageopacity", 1.0);
+ bgcolor |= SP_COLOR_F_TO_U(opacity);
+ }
+
+ sp_export_png_file(_workaround._doc, tmp_png.c_str(), rect,
+ (unsigned long)(Inkscape::Util::Quantity::convert(rect.width(), "px", "in") * dpi),
+ (unsigned long)(Inkscape::Util::Quantity::convert(rect.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:
+ {
+ auto png = Cairo::ImageSurface::create_from_png(tmp_png);
+ auto pattern = Cairo::SurfacePattern::create(png);
+ auto cr = context->get_cairo_context();
+ auto m = cr->get_matrix();
+ cr->scale(Inkscape::Util::Quantity::convert(1, "in", "pt") / dpi,
+ Inkscape::Util::Quantity::convert(1, "in", "pt") / dpi);
+ // FIXME: why is the origin offset??
+ cr->set_source(pattern);
+ cr->paint();
+ cr->set_matrix(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);
+
+ auto cr = context->get_cairo_context();
+ auto surface = cr->get_target();
+ auto ctm = cr->get_matrix();
+
+ bool ret = ctx->setSurfaceTarget(surface->cobj(), true, &ctm);
+ if (ret) {
+ ret = renderer.setupDocument (ctx, _workaround._doc);
+ if (ret) {
+ if (auto page = pm.getPage(page_nr)) {
+ renderer.renderPage(ctx, _workaround._doc, page, false);
+ } else {
+ 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()
+{
+ return &_tab;
+}
+
+void Print::begin_print(const Glib::RefPtr<Gtk::PrintContext>&)
+{
+ // Could change which pages get printed here, but nothing to do.
+}
+
+Gtk::PrintOperationResult Print::run(Gtk::PrintOperationAction, Gtk::Window &parent_window)
+{
+ // Remember to restore the previous print settings
+ _printop->set_print_settings(get_printer_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) {
+ get_printer_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..cbbee41
--- /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 {
+
+class Print {
+public:
+ Print(SPDocument *doc, SPItem *base);
+ Gtk::PrintOperationResult run(Gtk::PrintOperationAction, Gtk::Window &parent_window);
+
+protected:
+
+private:
+ void set_paper_size(const Glib::RefPtr<Gtk::PageSetup> &, double width, double height);
+
+ Glib::RefPtr<Gtk::PrintOperation> _printop;
+ SPDocument *_doc;
+ SPItem *_base;
+ Inkscape::UI::Widget::RenderingOptions _tab;
+
+ struct workaround_gtkmm _workaround;
+
+ void setup_page(const Glib::RefPtr<Gtk::PrintContext>& context, int page_nr,
+ const Glib::RefPtr<Gtk::PageSetup> &setup);
+ 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/prototype.cpp b/src/ui/dialog/prototype.cpp
new file mode 100644
index 0000000..d0ee467
--- /dev/null
+++ b/src/ui/dialog/prototype.cpp
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A bare minimum example of deriving from Inkscape::UI:Widget::Panel.
+ *
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifdef DEBUG
+
+#include "prototype.h"
+
+#include "document.h"
+#include "inkscape-application.h"
+
+// Only for use in demonstration widget.
+#include "object/sp-root.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+Prototype::Prototype()
+ : DialogBase("/dialogs/prototype", "Prototype")
+{
+ // A widget for demonstration that displays the current SVG's id.
+ _label = Gtk::make_managed<Gtk::Label>(_name);
+ _label->set_line_wrap();
+
+ _debug_button.set_name("PrototypeDebugButton");
+ _debug_button.set_hexpand();
+ _debug_button.signal_clicked().connect(sigc::mem_fun(*this, &Prototype::on_click));
+
+ _debug_button.add(*_label);
+ add(_debug_button);
+}
+
+void Prototype::documentReplaced()
+{
+ if (document && document->getRoot()) {
+ const gchar *root_id = document->getRoot()->getId();
+ Glib::ustring label_string("Document's SVG id: ");
+ label_string += (root_id ? root_id : "null");
+ _label->set_label(label_string);
+ }
+}
+
+void Prototype::selectionChanged(Inkscape::Selection *selection)
+{
+ if (!selection) {
+ return;
+ }
+
+ // Update demonstration widget.
+ Glib::ustring label = _label->get_text() + "\nSelection changed to ";
+ SPObject* object = selection->single();
+ if (object) {
+ label = label + object->getId();
+ } else {
+ object = selection->activeContext();
+
+ if (object) {
+ label = label + object->getId();
+ } else {
+ label = label + "unknown";
+ }
+ }
+
+ _label->set_label(label);
+}
+
+void Prototype::on_click()
+{
+ auto window = dynamic_cast<Gtk::Window*>(get_toplevel());
+ if (window) {
+ std::cerr << "Dialog is part of: " << window->get_name() << " (" << window->get_title() << ")" << std::endl;
+ } else {
+ std::cerr << "Prototype::on_click(): Dialog not attached to window!" << std::endl;
+ }
+}
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // DEBUG
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ 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/prototype.h b/src/ui/dialog/prototype.h
new file mode 100644
index 0000000..1f36a73
--- /dev/null
+++ b/src/ui/dialog/prototype.h
@@ -0,0 +1,67 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A bare minimum example of deriving from Inkscape::UI:Widget::Panel.
+ *
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_PROTOTYPE_PANEL_H
+#define SEEN_PROTOTYPE_PANEL_H
+
+#ifdef DEBUG
+
+#include <iostream>
+
+#include "selection.h"
+#include "ui/dialog/dialog-base.h"
+
+// Only to display status.
+#include <gtkmm/label.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/**
+ * A panel that does almost nothing!
+ */
+class Prototype : public DialogBase
+{
+public:
+ Prototype();
+ ~Prototype() override { std::cerr << "Prototype::~Prototype()" << std::endl; }
+
+ void documentReplaced(SPDocument *document) override;
+ void selectionChanged(Inkscape::Selection *selection) override;
+
+private:
+ // Just for example
+ Gtk::Label *_label;
+ Gtk::Button _debug_button; // For printing to console.
+
+ virtual void on_click();
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // DEBUG
+
+#endif // SEEN_PROTOTYPE_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/save-template-dialog.cpp b/src/ui/dialog/save-template-dialog.cpp
new file mode 100644
index 0000000..a240f61
--- /dev/null
+++ b/src/ui/dialog/save-template-dialog.cpp
@@ -0,0 +1,86 @@
+// 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>
+#include <gtkmm/builder.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/dialog.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/window.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+SaveTemplate::SaveTemplate(Gtk::Window &parent) {
+
+ std::string gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "dialog-save-template.glade");
+ Glib::RefPtr<Gtk::Builder> builder;
+ try {
+ builder = Gtk::Builder::create_from_file(gladefile);
+ } catch (const Glib::Error &ex) {
+ g_warning("GtkBuilder file loading failed for save template dialog");
+ return;
+ }
+
+ builder->get_widget("dialog", dialog);
+ builder->get_widget("name", name);
+ builder->get_widget("author", author);
+ builder->get_widget("description", description);
+ builder->get_widget("keywords", keywords);
+ builder->get_widget("set-default", set_default_template);
+
+ name->signal_changed().connect(sigc::mem_fun(*this, &SaveTemplate::on_name_changed));
+
+ dialog->add_button(_("Cancel"), Gtk::RESPONSE_CANCEL);
+ dialog->add_button(_("Save"), Gtk::RESPONSE_OK);
+
+ dialog->set_response_sensitive(Gtk::RESPONSE_OK, false);
+ dialog->set_default_response(Gtk::RESPONSE_CANCEL);
+
+ dialog->set_transient_for(parent);
+ dialog->show_all();
+}
+
+void SaveTemplate::on_name_changed() {
+
+ bool has_text = name->get_text_length() != 0;
+ dialog->set_response_sensitive(Gtk::RESPONSE_OK, has_text);
+}
+
+void SaveTemplate::save_template(Gtk::Window &parent) {
+
+ sp_file_save_template(parent, name->get_text(), author->get_text(), description->get_text(),
+ keywords->get_text(), set_default_template->get_active());
+}
+
+void SaveTemplate::save_document_as_template(Gtk::Window &parent) {
+
+ SaveTemplate dialog(parent);
+ int response = dialog.dialog->run();
+
+ switch (response) {
+ case Gtk::RESPONSE_OK:
+ dialog.save_template(parent);
+ break;
+ default:
+ break;
+ }
+
+ dialog.dialog->close();
+}
+
+}
+}
+}
diff --git a/src/ui/dialog/save-template-dialog.h b/src/ui/dialog/save-template-dialog.h
new file mode 100644
index 0000000..eb702ca
--- /dev/null
+++ b/src/ui/dialog/save-template-dialog.h
@@ -0,0 +1,56 @@
+// 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 <glibmm/refptr.h>
+
+namespace Gtk {
+class Builder;
+class CheckButton;
+class Dialog;
+class Entry;
+class Window;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class SaveTemplate
+{
+
+public:
+
+ static void save_document_as_template(Gtk::Window &parentWindow);
+
+protected:
+
+ void on_name_changed();
+
+private:
+
+ Gtk::Dialog *dialog;
+
+ Gtk::Entry *name;
+ Gtk::Entry *author;
+ Gtk::Entry *description;
+ Gtk::Entry *keywords;
+
+ Gtk::CheckButton *set_default_template;
+
+ SaveTemplate(Gtk::Window &parent);
+ void save_template(Gtk::Window &parent);
+
+};
+}
+}
+}
+#endif
diff --git a/src/ui/dialog/selectorsdialog.cpp b/src/ui/dialog/selectorsdialog.cpp
new file mode 100644
index 0000000..fd448a9
--- /dev/null
+++ b/src/ui/dialog/selectorsdialog.cpp
@@ -0,0 +1,1340 @@
+// 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 <map>
+#include <regex>
+#include <utility>
+
+#include <glibmm/i18n.h>
+#include <glibmm/regex.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 "util/trim.h"
+
+#include "xml/attribute-record.h"
+#include "xml/node-observer.h"
+#include "xml/sp-css-attr.h"
+
+
+// G_MESSAGES_DEBUG=DEBUG_SELECTORSDIALOG gdb ./inkscape
+// #define DEBUG_SELECTORSDIALOG
+// #define G_LOG_DOMAIN "SELECTORSDIALOG"
+
+using Inkscape::DocumentUndo;
+
+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 &,
+ Inkscape::Util::ptr_shared,
+ Inkscape::Util::ptr_shared)
+{
+ g_debug("SelectorsDialog::NodeObserver::notifyContentChanged");
+ _selectorsdialog->_scrollock = 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 &,
+ Inkscape::XML::Node &child,
+ Inkscape::XML::Node *) override
+ {
+ _selectorsdialog->_nodeAdded(child);
+ }
+
+ void notifyChildRemoved(Inkscape::XML::Node &,
+ Inkscape::XML::Node &child,
+ Inkscape::XML::Node *) override
+ {
+ _selectorsdialog->_nodeRemoved(child);
+ }
+
+ void notifyAttributeChanged(Inkscape::XML::Node &node,
+ GQuark qname,
+ Util::ptr_shared,
+ Util::ptr_shared) 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");
+
+ _scrollock = 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()
+ : DialogBase("/dialogs/selectors", "Selectors")
+{
+ g_debug("SelectorsDialog::SelectorsDialog");
+
+ m_nodewatcher = std::make_unique<NodeWatcher>(this);
+ m_styletextwatcher = std::make_unique<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);
+
+ // ALWAYS be a single selection widget
+ _treeView.get_selection()->set_mode(Gtk::SELECTION_SINGLE);
+
+ _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();
+
+ show_all();
+}
+
+
+void SelectorsDialog::_vscroll()
+{
+ if (!_scrollock) {
+ _scrollpos = _vadj->get_value();
+ } else {
+ _vadj->set_value(_scrollpos);
+ _scrollock = 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);
+ _scrolled_window_selectors.set_overlay_scrolling(false);
+ _vadj = _scrolled_window_selectors.get_vadjustment();
+ _vadj->signal_value_changed().connect(sigc::mem_fun(*this, &SelectorsDialog::_vscroll));
+ _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 = Gtk::make_managed<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);
+ pack_start(*contents, Gtk::PACK_EXPAND_WIDGET);
+ show_all();
+ _updating = true;
+ _paned.property_position() = 200;
+ _updating = false;
+ set_size_request(320, -1);
+ set_name("SelectorsAndStyleDialog");
+}
+
+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;
+}
+
+/**
+ * @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;
+ _scrollock = 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];
+ Util::trim(selector, ","); // Remove leading/trailing spaces and commas
+ 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;
+
+
+ for (unsigned i = 0; i < tokens.size()-1; i += 2) {
+ Glib::ustring selector = tokens[i];
+ Util::trim(selector, ","); // Remove leading/trailing spaces and commas
+ 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] = nullptr;
+ 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;
+
+ 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;
+ }
+ Util::trim(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] = nullptr;
+ row[_mColumns._colProperties] = properties;
+ row[_mColumns._colVisible] = true;
+ row[_mColumns._colSelected] = 400;
+ // Add as children, objects that match selector.
+ for (auto &obj : _getObjVec(selector)) {
+ 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] = obj;
+ childrow[_mColumns._colProperties] = ""; // Unused
+ childrow[_mColumns._colVisible] = true; // Unused
+ childrow[_mColumns._colSelected] = 400;
+ }
+ }
+
+
+ _updating = false;
+ if (rewrite) {
+ _writeStyleElement();
+ }
+ _scrollock = false;
+ _vadj->set_value(std::min(_scrollpos, _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");
+
+ _scrollock = true;
+ _updating = true;
+ Glib::ustring styleContent = "";
+ for (auto& row: _store->children()) {
+ Glib::ustring selector = row[_mColumns._colSelector];
+#if 0
+ Util::trim(selector, ",");
+ 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());
+ if (empty) {
+ styleContent = "";
+ textNode->setContent(styleContent.c_str());
+ }
+ textNode->setContent(styleContent.c_str());
+ DocumentUndo::done(SP_ACTIVE_DOCUMENT, _("Edited style element."), INKSCAPE_ICON("dialog-selectors"));
+
+ _updating = false;
+ _scrollock = false;
+ _vadj->set_value(std::min(_scrollpos, _vadj->get_upper()));
+ g_debug("SelectorsDialog::_writeStyleElement(): | %s |", styleContent.c_str());
+}
+
+Glib::ustring SelectorsDialog::_getSelectorClasses(Glib::ustring selector)
+{
+ g_debug("SelectorsDialog::_getSelectorClasses");
+
+ 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
+ Util::trim(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];
+ 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 = _getSelectorClasses(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] = obj;
+ childrow[_mColumns._colProperties] = ""; // Unused
+ childrow[_mColumns._colVisible] = true; // Unused
+ childrow[_mColumns._colSelected] = 400;
+ }
+ row[_mColumns._colSelector] = multiselector;
+ _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 (const auto & iter : css_selector->attributeList()) {
+ gchar const *key = g_quark_to_string(iter.key);
+ css->removeAttribute(key);
+ }
+ 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) {
+ _scrollock = 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];
+ Util::trim(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 = _getSelectorClasses(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;
+ }
+ }
+ Util::trim(selector);
+ if (selector.empty()) {
+ _store->erase(parent);
+
+ } else {
+ _store->erase(row);
+ parent[_mColumns._colSelector] = selector;
+ parent[_mColumns._colExpand] = true;
+ parent[_mColumns._colObj] = nullptr;
+ }
+ }
+ _updating = false;
+
+ // Add entry to style element
+ _writeStyleElement();
+ obj->style->readFromObject(obj);
+ obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
+ _scrollock = false;
+ _vadj->set_value(std::min(_scrollpos, _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;
+ }
+ Util::trim(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()->getSelection()->clear();
+ Gtk::TreeModel::iterator iter = _store->get_iter(path);
+ if (iter) {
+ Gtk::TreeModel::Row row = *iter;
+ if (row[_mColumns._colObj]) {
+ getDesktop()->getSelection()->add(row[_mColumns._colObj]);
+ }
+ Gtk::TreeModel::Children children = row.children();
+ if (children.empty() || children.size() == 1) {
+ _del.show();
+ }
+ for (auto child : row.children()) {
+ Gtk::TreeModel::Row child_row = *child;
+ if (child[_mColumns._colObj]) {
+ getDesktop()->getSelection()->add(child[_mColumns._colObj]);
+ }
+ }
+ }
+ _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");
+ _scrollock = 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
+ Util::trim(selectorValue, ",");
+ if (originalValue.find("@import ") != std::string::npos) {
+ Gtk::TreeModel::Row row = *(_store->prepend());
+ row[_mColumns._colSelector] = originalValue;
+ row[_mColumns._colExpand] = false;
+ row[_mColumns._colType] = OTHER;
+ row[_mColumns._colObj] = nullptr;
+ 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 = _getSelectorClasses(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);
+ }
+ }
+ }
+ Gtk::TreeModel::Row row = *(_store->prepend());
+ row[_mColumns._colExpand] = true;
+ row[_mColumns._colType] = SELECTOR;
+ row[_mColumns._colSelector] = selectorValue;
+ row[_mColumns._colObj] = nullptr;
+ row[_mColumns._colProperties] = "";
+ row[_mColumns._colVisible] = true;
+ row[_mColumns._colSelected] = 400;
+ for (auto &obj : _getObjVec(selectorValue)) {
+ 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] = obj;
+ childrow[_mColumns._colProperties] = ""; // Unused
+ childrow[_mColumns._colVisible] = true; // Unused
+ childrow[_mColumns._colSelected] = 400;
+ }
+ }
+ // Add entry to style element
+ _writeStyleElement();
+ _scrollock = false;
+ _vadj->set_value(std::min(_scrollpos, _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");
+
+ _scrollock = true;
+ Glib::RefPtr<Gtk::TreeSelection> refTreeSelection = _treeView.get_selection();
+ Gtk::TreeModel::iterator iter = refTreeSelection->get_selected();
+ if (iter) {
+ _vscroll();
+ Gtk::TreeModel::Row row = *iter;
+ if (row.children().size() > 2) {
+ return;
+ }
+ _updating = true;
+ _store->erase(iter);
+ _updating = false;
+ _writeStyleElement();
+ _del.hide();
+ _scrollock = false;
+ _vadj->set_value(std::min(_scrollpos, _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) {
+ _scrollock = 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)) {
+ _vscroll();
+ 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(_scrollpos, _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;
+};
+
+// -------------------------------------------------------------------
+
+SelectorsDialog::~SelectorsDialog()
+{
+ removeObservers();
+ _style_dialog->setDesktop(nullptr);
+}
+
+void SelectorsDialog::update()
+{
+ _style_dialog->update();
+}
+
+void SelectorsDialog::desktopReplaced()
+{
+ _style_dialog->setDesktop(getDesktop());
+}
+
+void SelectorsDialog::removeObservers()
+{
+ if (_textNode) {
+ _textNode->removeObserver(*m_styletextwatcher);
+ _textNode = nullptr;
+ }
+ if (m_root) {
+ m_root->removeSubtreeObserver(*m_nodewatcher);
+ m_root = nullptr;
+ }
+}
+
+void SelectorsDialog::documentReplaced()
+{
+ removeObservers();
+ if (auto document = getDocument()) {
+ m_root = document->getReprRoot();
+ m_root->addSubtreeObserver(*m_nodewatcher);
+ }
+ selectionChanged(getSelection());
+}
+
+void SelectorsDialog::selectionChanged(Selection *selection)
+{
+ _lastpath.clear();
+ _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");
+ if (event->type == GDK_BUTTON_RELEASE && event->button == 1) {
+ _updating = true;
+ _del.show();
+ int x = static_cast<int>(event->x);
+ int y = static_cast<int>(event->y);
+ _selectObjects(x, y);
+ _updating = false;
+ _selectRow();
+ }
+}
+
+
+/**
+ * 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()
+{
+ _scrollock = 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) {
+ _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.
+
+ Gtk::TreeModel::Children children = _store->children();
+ Inkscape::Selection* selection = getDesktop()->getSelection();
+ if (selection->isEmpty()) {
+ _style_dialog->setCurrentSelector("");
+ }
+ for (auto row : children) {
+ row[_mColumns._colSelected] = 400;
+ Gtk::TreeModel::Children subchildren = row->children();
+ for (auto subrow : subchildren) {
+ subrow[_mColumns._colSelected] = 400;
+ }
+ }
+
+ // Sort selection for matching.
+ std::vector<SPObject *> selected_objs(
+ selection->objects().begin(), selection->objects().end());
+ std::sort(selected_objs.begin(), selected_objs.end());
+
+ for (auto row : children) {
+ // Recalculate the selector, in real time.
+ auto row_children = _getObjVec(row[_mColumns._colSelector]);
+ std::sort(row_children.begin(), row_children.end());
+
+ // If all selected objects are in the css-selector, select it.
+ if (row_children == selected_objs) {
+ row[_mColumns._colSelected] = 700;
+ }
+
+ Gtk::TreeModel::Children subchildren = row->children();
+
+ for (auto subrow : subchildren) {
+ if (subrow[_mColumns._colObj] && selection->includes(subrow[_mColumns._colObj])) {
+ subrow[_mColumns._colSelected] = 700;
+ }
+ if (row[_mColumns._colExpand]) {
+ _treeView.expand_to_path(Gtk::TreePath(row));
+ }
+ }
+ if (row[_mColumns._colExpand]) {
+ _treeView.expand_to_path(Gtk::TreePath(row));
+ }
+ }
+ _vadj->set_value(std::min(_scrollpos, _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..674d6aa
--- /dev/null
+++ b/src/ui/dialog/selectorsdialog.h
@@ -0,0 +1,194 @@
+// 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 <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 <memory>
+#include <vector>
+
+#include "ui/dialog/dialog-base.h"
+#include "ui/dialog/styledialog.h"
+#include "xml/helper-observer.h"
+
+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 DialogBase
+{
+public:
+ SelectorsDialog();
+ ~SelectorsDialog() override;
+
+ void update() override;
+ void desktopReplaced() override;
+ void documentReplaced() override;
+ void selectionChanged(Selection *selection) override;
+
+ private:
+ // Monitor <style> element for changes.
+ class NodeObserver;
+
+ void removeObservers();
+
+ // 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<SPObject *> _colObj; // Matching object (if any).
+ 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;
+
+ // 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 _selectObjects(int, int);
+ // Variables
+ double _scrollpos{0.0};
+ bool _scrollock{false};
+ bool _updating{false}; // Prevent cyclic actions: read <-> write, select via dialog <-> via desktop
+ Inkscape::XML::Node *m_root{nullptr};
+ Inkscape::XML::Node *_textNode{nullptr}; // Track so we know when to add a NodeObserver.
+
+ 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);
+
+ Inkscape::XML::SignalObserver _objObserver; // Track object in selected row (for style change).
+
+ // Signal and handlers - Internal
+ void _addSelector();
+ void _delSelector();
+ static Glib::ustring _getSelectorClasses(Glib::ustring selector);
+ bool _handleButtonEvent(GdkEventButton *event);
+ void _buttonEventsSelectObjs(GdkEventButton *event);
+ void _selectRow(); // Select row in tree when selection changed.
+ void _vscroll();
+
+ // GUI
+ void _styleButton(Gtk::Button& btn, char const* iconName, char const* tooltip);
+};
+
+} // namespace Dialog
+} // 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..51560a7
--- /dev/null
+++ b/src/ui/dialog/spellcheck.cpp
@@ -0,0 +1,751 @@
+// 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 "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "message-stack.h"
+#include "layer-manager.h"
+#include "selection-chemistry.h"
+#include "text-editing.h"
+
+#include "display/control/canvas-item-rect.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 "ui/dialog/dialog-container.h"
+#include "ui/dialog/inkscape-preferences.h" // for PREFS_PAGE_SPELLCHECK
+#include "ui/icon-names.h"
+#include "ui/tools/text-tool.h"
+
+#include <glibmm/i18n.h>
+
+#ifdef _WIN32
+#include <windows.h>
+#endif
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/**
+ * Get the list of installed dictionaries/languages
+ */
+std::vector<LanguagePair> SpellCheck::get_available_langs()
+{
+ std::vector<LanguagePair> langs;
+
+#if WITH_GSPELL
+ // TODO: write a gspellmm library.
+ // TODO: why is this not const?
+ GList *list = const_cast<GList *>(gspell_language_get_available());
+ g_list_foreach(list, [](gpointer data, gpointer user_data) {
+ GspellLanguage *language = reinterpret_cast<GspellLanguage*>(data);
+ std::vector<LanguagePair> *langs = reinterpret_cast<std::vector<LanguagePair>*>(user_data);
+ const gchar *name = gspell_language_get_name(language);
+ const gchar *code = gspell_language_get_code(language);
+ langs->emplace_back(name, code);
+ }, &langs);
+#endif
+
+ return langs;
+}
+
+static void show_spellcheck_preferences_dialog()
+{
+ Inkscape::Preferences::get()->setInt("/dialogs/preferences/page", PREFS_PAGE_SPELLCHECK);
+ SP_ACTIVE_DESKTOP->getContainer()->new_dialog("Spellcheck");
+}
+
+SpellCheck::SpellCheck()
+ : DialogBase("/dialogs/spellcheck/", "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(Gtk::ORIENTATION_HORIZONTAL, 0)
+ , stop_button(_("_Stop"), true)
+ , start_button(_("_Start"), true)
+ , suggestion_hbox(Gtk::ORIENTATION_HORIZONTAL)
+ , changebutton_vbox(Gtk::ORIENTATION_VERTICAL)
+{
+ _prefs = Inkscape::Preferences::get();
+
+ 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(Glib::ustring::compose("<i>%1</i>", _("No dictionaries installed")));
+ }
+ }
+
+ 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 (const LanguagePair &pair : _langs) {
+ dictionary_combo.append(pair.second, pair.first);
+ }
+ // Set previously set language (or the first item)
+ if(!dictionary_combo.set_active_id(_prefs->getString("/dialogs/spellcheck/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
+ */
+ set_spacing(6);
+ pack_start (banner_hbox, false, false, 0);
+ pack_start (suggestion_hbox, true, true, 0);
+ pack_start (dictionary_hbox, false, false, 0);
+ pack_start (action_sep, false, false, 6);
+ 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));
+
+ 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()
+{
+ disconnect();
+}
+
+void SpellCheck::documentReplaced()
+{
+ if (_working) {
+ // Stop and start on the new desktop
+ finished();
+ onStart();
+ }
+}
+
+void SpellCheck::clearRects()
+{
+ _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 (is<SPDefs>(r))
+ return; // we're not interested in items in defs
+
+ if (!strcmp(r->getRepr()->name(), "svg:metadata")) {
+ return; // we're not interested in metadata
+ }
+
+ if (auto desktop = getDesktop()) {
+ for (auto& child: r->children) {
+ if (auto item = cast<SPItem>(&child)) {
+ if (!child.cloned && !desktop->layerManager().isLayer(item)) {
+ if ((hidden || !desktop->itemIsHidden(item)) && (locked || !item->isLocked())) {
+ if (is<SPText>(item) || is<SPFlowtext>(item))
+ l.push_back(item);
+ }
+ }
+ }
+ 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(SPItem const *i1, SPItem const *i2)//returns a<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 = _text->connectModified(sigc::mem_fun(*this, &SpellCheck::onObjModified));
+ _release_connection = _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() {
+}
+
+bool SpellCheck::updateSpeller() {
+#if WITH_GSPELL
+ auto lang = dictionary_combo.get_active_id();
+ if (!lang.empty()) {
+ const GspellLanguage *language = gspell_language_lookup(lang.c_str());
+ _checker = gspell_checker_new(language);
+ }
+
+ return _checker != nullptr;
+#else
+ return false;
+#endif
+}
+
+void SpellCheck::onStart()
+{
+ if (!getDocument())
+ return;
+
+ start_button.set_sensitive(false);
+
+ _stops = 0;
+ _adds = 0;
+ clearRects();
+
+ if (!updateSpeller())
+ return;
+
+ _root = getDocument()->getRoot();
+
+ // empty the list of objects we've checked
+ _seen_objects.clear();
+
+ // grab first text
+ nextText();
+
+ _working = true;
+
+ doSpellcheck();
+}
+
+void
+SpellCheck::finished ()
+{
+ deleteSpeller();
+
+ clearRects();
+ disconnect();
+
+ 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();
+
+ _root = nullptr;
+
+ _working = false;
+}
+
+bool
+SpellCheck::nextWord()
+{
+ auto desktop = getDesktop();
+ if (!_working || !desktop)
+ 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 (is<SPString>(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 (is<SPString>(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 WITH_GSPELL
+ if (_checker) {
+ GError *error = nullptr;
+ have += gspell_checker_check_word(_checker, _word.c_str(), -1, &error);
+ }
+#endif /* WITH_GSPELL */
+
+ if (have == 0) { // not found in any!
+ _stops ++;
+
+ // 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 item rect with red stroke. (TODO: a quad could allow non-axis aligned rects.)
+ auto rect = new Inkscape::CanvasItemRect(desktop->getCanvasSketch(), area);
+ rect->set_stroke(0xff0000ff);
+ rect->show();
+ _rects.emplace_back(rect);
+
+ // scroll to make it all visible
+ Geom::Point const center = desktop->current_center();
+ 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);
+ }
+
+ // select text; if in Text tool, position cursor to the beginning of word
+ // unless it is already in the word
+ if (desktop->getSelection()->singleItem() != _text) {
+ desktop->getSelection()->set (_text);
+ }
+
+ if (dynamic_cast<Inkscape::UI::Tools::TextTool *>(desktop->event_context)) {
+ 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->getSelection()->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 WITH_GSPELL
+
+ // get suggestions
+ model = Gtk::ListStore::create(tree_columns);
+ tree_view.set_model(model);
+ unsigned n_sugg = 0;
+
+ if (_checker) {
+ GSList *list = gspell_checker_get_suggestions(_checker, _word.c_str(), -1);
+ std::vector<std::string> suggs;
+
+ // TODO: use a better API for that, or figure out how to make gspellmm.
+ g_slist_foreach(list, [](gpointer data, gpointer user_data) {
+ const gchar *suggestion = reinterpret_cast<const gchar*>(data);
+ std::vector<std::string> *suggs = reinterpret_cast<std::vector<std::string>*>(user_data);
+ suggs->push_back(suggestion);
+ }, &suggs);
+ g_slist_free_full(list, g_free);
+
+ Gtk::TreeModel::iterator iter;
+ for (std::string sugg : suggs) {
+ 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);
+ }
+ }
+ }
+
+ accept_button.set_sensitive(n_sugg > 0);
+
+#endif /* WITH_GSPELL */
+
+ return true;
+
+ }
+ return false;
+}
+
+void SpellCheck::deleteLastRect()
+{
+ if (!_rects.empty()) {
+ _rects.pop_back();
+ }
+}
+
+void SpellCheck::doSpellcheck ()
+{
+ if (_langs.empty()) {
+ return;
+ }
+
+ banner_label.set_markup(_("<i>Checking...</i>"));
+
+ 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(getDocument(), _("Fix spelling"), INKSCAPE_ICON("draw-text"));
+ }
+ }
+
+ deleteLastRect();
+ doSpellcheck();
+}
+
+void
+SpellCheck::onIgnore ()
+{
+#if WITH_GSPELL
+ if (_checker) {
+ gspell_checker_add_word_to_session(_checker, _word.c_str(), -1);
+ }
+#endif /* WITH_GSPELL */
+
+ deleteLastRect();
+ doSpellcheck();
+}
+
+void
+SpellCheck::onIgnoreOnce ()
+{
+ deleteLastRect();
+ doSpellcheck();
+}
+
+void
+SpellCheck::onAdd ()
+{
+ _adds++;
+
+#if WITH_GSPELL
+ if (_checker) {
+ gspell_checker_add_word_to_personal(_checker, _word.c_str(), -1);
+ }
+#endif /* WITH_GSPELL */
+
+ deleteLastRect();
+ doSpellcheck();
+}
+
+void
+SpellCheck::onStop ()
+{
+ finished();
+}
+
+void SpellCheck::onLanguageChanged()
+{
+ // First, save language for next load
+ auto lang = dictionary_combo.get_active_id();
+ _prefs->setString("/dialogs/spellcheck/lang", lang);
+
+ 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..753eb80
--- /dev/null
+++ b/src/ui/dialog/spellcheck.h
@@ -0,0 +1,281 @@
+// 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 <gtkmm/box.h>
+#include <gtkmm/button.h>
+#include <gtkmm/buttonbox.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/separator.h>
+#include <gtkmm/treeview.h>
+#include <set>
+#include <vector>
+
+#include "text-editing.h"
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/scrollprotected.h"
+#include "display/control/canvas-item-ptr.h"
+
+#if WITH_GSPELL
+#include <gspell/gspell.h>
+#endif /* WITH_GSPELL */
+
+class SPObject;
+class SPItem;
+class SPCanvasItem;
+
+namespace Inkscape {
+class Preferences;
+class CanvasItemRect;
+
+namespace UI {
+namespace Dialog {
+
+using LanguagePair = std::pair<std::string, std::string>;
+
+/**
+ *
+ * A dialog widget to checking spelling of text elements in the document
+ * Uses gspell and one of the languages set in the users preference file
+ *
+ */
+class SpellCheck : public DialogBase
+{
+public:
+ SpellCheck();
+ ~SpellCheck() override;
+
+ static std::vector<LanguagePair> get_available_langs();
+
+private:
+ void documentReplaced() override;
+
+ /**
+ * 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(SPItem const *i1, SPItem const *i2);
+ SPItem *getText (SPObject *root);
+ void nextText ();
+
+ /**
+ * 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();
+
+ SPObject *_root;
+
+#if WITH_GSPELL
+ GspellChecker *_checker = nullptr;
+#endif /* WITH_GSPELL */
+
+ /**
+ * list of canvasitems (currently just rects) that mark misspelled things on canvas
+ */
+ std::vector<CanvasItemPtr<CanvasItemRect>> _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<LanguagePair> _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::Box suggestion_hbox;
+ Gtk::Box 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;
+ Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBoxText> dictionary_combo;
+ Gtk::Box dictionary_hbox;
+ Gtk::Separator action_sep;
+ Gtk::Button stop_button;
+ Gtk::Button start_button;
+ Gtk::ButtonBox actionbutton_hbox;
+
+ 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/startup.cpp b/src/ui/dialog/startup.cpp
new file mode 100644
index 0000000..b459bc1
--- /dev/null
+++ b/src/ui/dialog/startup.cpp
@@ -0,0 +1,789 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief A dialog for the about screen
+ *
+ * Copyright (C) Martin Owens 2019 <doctormo@gmail.com>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "startup.h"
+
+#include <fstream>
+#include <glibmm/i18n.h>
+#include <streambuf>
+#include <string>
+#include <limits>
+
+#include "color-rgba.h"
+#include "file.h"
+#include "inkscape-application.h"
+#include "inkscape-version-info.h"
+#include "inkscape-version.h"
+#include "inkscape.h"
+#include "io/resource.h"
+#include "object/sp-namedview.h"
+#include "preferences.h"
+#include "ui/dialog/filedialog.h"
+#include "ui/shortcuts.h"
+#include "ui/themes.h"
+#include "ui/util.h"
+#include "ui/widget/template-list.h"
+#include "util/units.h"
+
+using namespace Inkscape::IO;
+using namespace Inkscape::UI::View;
+using Inkscape::Util::unit_table;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class NameIdCols: public Gtk::TreeModel::ColumnRecord {
+ public:
+ // These types must match those for the model in the .glade file
+ NameIdCols() {
+ this->add(this->col_name);
+ this->add(this->col_id);
+ }
+ Gtk::TreeModelColumn<Glib::ustring> col_name;
+ Gtk::TreeModelColumn<Glib::ustring> col_id;
+};
+
+class RecentCols: public Gtk::TreeModel::ColumnRecord {
+ public:
+ // These types must match those for the model in the .glade file
+ RecentCols() {
+ this->add(this->col_name);
+ this->add(this->col_id);
+ this->add(this->col_dt);
+ this->add(this->col_crash);
+ }
+ Gtk::TreeModelColumn<Glib::ustring> col_name;
+ Gtk::TreeModelColumn<Glib::ustring> col_id;
+ Gtk::TreeModelColumn<gint64> col_dt;
+ Gtk::TreeModelColumn<bool> col_crash;
+};
+
+class CanvasCols: public Gtk::TreeModel::ColumnRecord {
+ public:
+ // These types must match those for the model in the .glade file
+ CanvasCols() {
+ this->add(this->id);
+ this->add(this->name);
+ this->add(this->icon_filename);
+ this->add(this->pagecolor);
+ this->add(this->checkered);
+ this->add(this->bordercolor);
+ this->add(this->shadow);
+ this->add(this->deskcolor);
+ }
+ Gtk::TreeModelColumn<Glib::ustring> id;
+ Gtk::TreeModelColumn<Glib::ustring> name;
+ Gtk::TreeModelColumn<Glib::ustring> icon_filename;
+ Gtk::TreeModelColumn<Glib::ustring> pagecolor;
+ Gtk::TreeModelColumn<bool> checkered;
+ Gtk::TreeModelColumn<Glib::ustring> bordercolor;
+ Gtk::TreeModelColumn<bool> shadow;
+ Gtk::TreeModelColumn<Glib::ustring> deskcolor;
+};
+
+class ThemeCols: public Gtk::TreeModel::ColumnRecord {
+ public:
+ // These types must match those for the model in the .glade file
+ ThemeCols() {
+ this->add(this->id);
+ this->add(this->name);
+ this->add(this->theme);
+ this->add(this->icons);
+ this->add(this->base);
+ this->add(this->base_dark);
+ this->add(this->success);
+ this->add(this->warn);
+ this->add(this->error);
+ this->add(this->symbolic);
+ this->add(this->smallicons);
+ this->add(this->enabled);
+ }
+ Gtk::TreeModelColumn<Glib::ustring> id;
+ Gtk::TreeModelColumn<Glib::ustring> name;
+ Gtk::TreeModelColumn<Glib::ustring> theme;
+ Gtk::TreeModelColumn<Glib::ustring> icons;
+ Gtk::TreeModelColumn<Glib::ustring> base;
+ Gtk::TreeModelColumn<Glib::ustring> base_dark;
+ Gtk::TreeModelColumn<Glib::ustring> success;
+ Gtk::TreeModelColumn<Glib::ustring> warn;
+ Gtk::TreeModelColumn<Glib::ustring> error;
+ Gtk::TreeModelColumn<bool> symbolic;
+ Gtk::TreeModelColumn<bool> smallicons;
+ Gtk::TreeModelColumn<bool> enabled;
+};
+
+/**
+ * Color is store as a string in the form #RRGGBBAA, '0' means "unset"
+ *
+ * @param color - The string color from glade.
+ */
+unsigned int get_color_value(const Glib::ustring color)
+{
+ Gdk::RGBA gdk_color = Gdk::RGBA(color);
+ ColorRGBA sp_color(gdk_color.get_red(), gdk_color.get_green(),
+ gdk_color.get_blue(), gdk_color.get_alpha());
+ return sp_color.getIntValue();
+}
+
+StartScreen::StartScreen()
+ : Gtk::Dialog()
+{
+ set_can_focus(true);
+ grab_focus();
+ set_can_default(true);
+ grab_default();
+ set_urgency_hint(true); // Draw user's attention to this window!
+ set_modal(true);
+ set_position(Gtk::WIN_POS_CENTER_ALWAYS);
+ set_default_size(700, 360);
+
+ Glib::ustring gladefile = Resource::get_filename(Resource::UIS, "inkscape-start.glade");
+
+ try {
+ builder = Gtk::Builder::create_from_file(gladefile);
+ } catch (const Glib::Error &ex) {
+ g_error("Glade file loading failed for boot screen");
+ // cleanup?
+ }
+
+ // Get window from Glade file.
+ builder->get_widget("start-screen-window", window);
+ set_name("start-screen-window");
+ set_title(Inkscape::inkscape_version());
+
+ // Get references to various widgets used globally.
+ builder->get_widget("tabs", tabs);
+ builder->get_widget_derived("kinds", templates);
+ builder->get_widget("banner", banners);
+ builder->get_widget("themes", themes);
+ builder->get_widget("recent_treeview", recent_treeview);
+
+ // Populate with template extensions
+ templates->init(Inkscape::Extension::TEMPLATE_NEW_WELCOME);
+
+ // Get references to various widget used locally. (In order of appearance.)
+ Gtk::ComboBox* canvas = nullptr;
+ Gtk::ComboBox* keys = nullptr;
+ Gtk::Button* save = nullptr;
+ Gtk::Button* thanks = nullptr;
+ Gtk::Button* close_btn = nullptr;
+ Gtk::Button* new_btn = nullptr;
+ Gtk::Button* show_toggle = nullptr;
+ Gtk::Switch* dark_toggle = nullptr;
+ builder->get_widget("canvas", canvas);
+ builder->get_widget("keys", keys);
+ builder->get_widget("save", save);
+ builder->get_widget("thanks", thanks);
+ builder->get_widget("show_toggle", show_toggle);
+ builder->get_widget("dark_toggle", dark_toggle);
+ builder->get_widget("load", load_btn);
+ builder->get_widget("new", new_btn);
+ builder->get_widget("close_window", close_btn);
+
+ // Unparent to move to our dialog window.
+ auto parent = banners->get_parent();
+ parent->remove(*banners);
+ parent->remove(*tabs);
+
+ // Add signals and setup things.
+ auto prefs = Inkscape::Preferences::get();
+
+ tabs->signal_switch_page().connect(sigc::mem_fun(*this, &StartScreen::notebook_switch));
+
+ // Setup the lists of items
+ enlist_recent_files();
+ enlist_keys();
+ filter_themes();
+ set_active_combo("themes", prefs->getString("/options/boot/theme"));
+ set_active_combo("canvas", prefs->getString("/options/boot/canvas"));
+
+ // initialise dark depending on prefs and background
+ refresh_dark_switch();
+
+ // Welcome! tab
+ std::string welcome_text_file = Resource::get_filename_string(Resource::SCREENS, "start-welcome-text.svg", true);
+ Gtk::Image *welcome_text;
+ builder->get_widget("welcome_text", welcome_text);
+ welcome_text->set(welcome_text_file);
+
+ canvas->signal_changed().connect(sigc::mem_fun(*this, &StartScreen::canvas_changed));
+ keys->signal_changed().connect(sigc::mem_fun(*this, &StartScreen::keyboard_changed));
+ themes->signal_changed().connect(sigc::mem_fun(*this, &StartScreen::theme_changed));
+ dark_toggle->property_active().signal_changed().connect(sigc::mem_fun(*this, &StartScreen::theme_changed));
+ save->signal_clicked().connect(sigc::bind<Gtk::Button *>(sigc::mem_fun(*this, &StartScreen::notebook_next), save));
+
+ // "Supported by You" tab
+ thanks->signal_clicked().connect(sigc::bind<Gtk::Button *>(sigc::mem_fun(*this, &StartScreen::notebook_next), thanks));
+
+ // "Time to Draw" tab
+ recent_treeview->signal_row_activated().connect(sigc::hide(sigc::hide((sigc::mem_fun(*this, &StartScreen::load_document)))));
+ recent_treeview->get_selection()->signal_changed().connect(sigc::mem_fun(*this, &StartScreen::on_recent_changed));
+ templates->signal_switch_page().connect(sigc::mem_fun(*this, &StartScreen::on_kind_changed));
+ load_btn->set_sensitive(true);
+
+ show_toggle->signal_clicked().connect(sigc::mem_fun(*this, &StartScreen::show_toggle));
+ load_btn->signal_clicked().connect(sigc::mem_fun(*this, &StartScreen::load_document));
+ templates->connectItemSelected(sigc::mem_fun(*this, &StartScreen::new_document));
+ new_btn->signal_clicked().connect(sigc::mem_fun(*this, &StartScreen::new_document));
+ close_btn->signal_clicked().connect([=] { response(GTK_RESPONSE_CANCEL); });
+
+ // Reparent to our dialog window
+ set_titlebar(*banners);
+ Gtk::Box* box = get_content_area();
+ box->add(*tabs);
+
+ // Show the first tab ONLY on the first run for this version
+ std::string opt_shown = "/options/boot/shown/ver";
+ opt_shown += Inkscape::version_string_without_revision;
+ if (!prefs->getBool(opt_shown, false)) {
+ theme_changed();
+ tabs->set_current_page(0);
+ prefs->setBool(opt_shown, true);
+ } else {
+ tabs->set_current_page(2);
+ notebook_switch(nullptr, 2);
+ }
+
+ set_modal(true);
+ set_position(Gtk::WIN_POS_CENTER_ALWAYS);
+ property_resizable() = false;
+ set_default_size(700, 360);
+ show();
+}
+
+StartScreen::~StartScreen()
+{
+ // These are "owned" by builder... don't delete them!
+ banners->get_parent()->remove(*banners);
+ tabs->get_parent()->remove(*tabs);
+}
+
+/**
+ * Return the active row of the named combo box.
+ *
+ * @param widget_name - The name of the widget in the glade file
+ * @return Gtk Row object ready for use.
+ * @throws Three errors depending on where it failed.
+ */
+Gtk::TreeModel::Row
+StartScreen::active_combo(std::string widget_name)
+{
+ Gtk::ComboBox *combo;
+ builder->get_widget(widget_name, combo);
+ if (!combo) throw 1;
+ Gtk::TreeModel::iterator iter = combo->get_active();
+ if (!iter) throw 2;
+ Gtk::TreeModel::Row row = *iter;
+ if (!row) throw 3;
+ return row;
+}
+
+/**
+ * Set the active item in the combo based on the unique_id (column set in glade)
+ *
+ * @param widget_name - The name of the widget in the glade file
+ * @param unique_id - The column id to activate, sets to first item if blank.
+ */
+void
+StartScreen::set_active_combo(std::string widget_name, std::string unique_id)
+{
+ Gtk::ComboBox *combo;
+ builder->get_widget(widget_name, combo);
+ if (combo) {
+ if (unique_id.empty()) {
+ combo->set_active(0); // Select the first
+ } else if(!combo->set_active_id(unique_id)) {
+ combo->set_active(-1); // Select nothing
+ }
+ }
+}
+
+/**
+ * When a notbook is switched, reveal the right banner image (gtk signal).
+ */
+void
+StartScreen::notebook_switch(Gtk::Widget *tab, guint page_num)
+{
+ int page = 0;
+ for (auto banner : banners->get_children()) {
+ if (auto revealer = dynamic_cast<Gtk::Revealer *>(banner)) {
+ revealer->set_reveal_child(page == page_num);
+ page++;
+ }
+ }
+}
+
+void
+StartScreen::enlist_recent_files()
+{
+ RecentCols cols;
+ if (!recent_treeview) return;
+ // We're not sure why we have to ask C for the TreeStore object
+ auto store = Glib::wrap(GTK_LIST_STORE(gtk_tree_view_get_model(recent_treeview->gobj())));
+ store->clear();
+ // Now sort the result by visited time
+ store->set_sort_column(cols.col_dt, Gtk::SORT_DESCENDING);
+
+ // Open [other]
+ Gtk::TreeModel::Row first_row = *(store->append());
+ first_row[cols.col_name] = _("Browse for other files...");
+ first_row[cols.col_id] = "";
+ first_row[cols.col_dt] = std::numeric_limits<gint64>::max();
+ recent_treeview->get_selection()->select(store->get_path(first_row));
+
+ Glib::RefPtr<Gtk::RecentManager> manager = Gtk::RecentManager::get_default();
+ for (auto item : manager->get_items()) {
+ if (item->has_application(g_get_prgname())
+ || item->has_application("org.inkscape.Inkscape")
+ || item->has_application("inkscape")
+ || item->has_application("inkscape.exe")
+ ) {
+ // This uri is a GVFS uri, so parse it with that or it will fail.
+ auto file = Gio::File::create_for_uri(item->get_uri());
+ std::string path = file->get_path();
+ if (!path.empty() && Glib::file_test(path, Glib::FILE_TEST_IS_REGULAR)
+ && item->get_mime_type() == "image/svg+xml") {
+ Gtk::TreeModel::Row row = *(store->append());
+ row[cols.col_name] = item->get_display_name();
+ row[cols.col_id] = item->get_uri();
+ row[cols.col_dt] = item->get_modified();
+ row[cols.col_crash] = item->has_group("Crash");
+ }
+ }
+ }
+
+}
+
+/**
+ * Called when a new recent document is selected.
+ */
+void
+StartScreen::on_recent_changed()
+{
+ // TODO: In the future this is where previews and other information can be loaded.
+}
+
+/**
+ * Called when the left side tabs are changed.
+ */
+void
+StartScreen::on_kind_changed(Gtk::Widget *tab, guint page_num)
+{
+ if (page_num == 0) {
+ load_btn->show();
+ } else {
+ load_btn->hide();
+ }
+}
+
+/**
+ * Called when new button clicked or template is double clicked, or escape pressed.
+ */
+void
+StartScreen::new_document()
+{
+ // Generate a new document from the selected template.
+ _document = templates->new_document();
+ if (_document) {
+ // Quit welcome screen if options not 'canceled'
+ response(GTK_RESPONSE_APPLY);
+ }
+}
+
+/**
+ * Called when load button clicked.
+ */
+void
+StartScreen::load_document()
+{
+ RecentCols cols;
+ auto prefs = Inkscape::Preferences::get();
+ auto app = InkscapeApplication::instance();
+
+ if (!recent_treeview)
+ return;
+
+ auto iter = recent_treeview->get_selection()->get_selected();
+ if (iter) {
+ Gtk::TreeModel::Row row = *iter;
+ if (row) {
+ Glib::ustring _file = row[cols.col_id];
+ Glib::RefPtr<Gio::File> file;
+
+ if (!_file.empty()) {
+ file = Gio::File::create_for_uri(_file);
+ } else {
+ Glib::ustring open_path = prefs->getString("/dialogs/open/path");
+ if (open_path.empty()) {
+ open_path = g_get_home_dir();
+ open_path.append(G_DIR_SEPARATOR_S);
+ }
+
+ // Browse for file instead
+ auto browser = Inkscape::UI::Dialog::FileOpenDialog::create(
+ *this, open_path, Inkscape::UI::Dialog::SVG_TYPES, _("Open a different file"));
+
+ if (browser->show()) {
+ prefs->setString("/dialogs/open/path", browser->getCurrentDirectory());
+ file = Gio::File::create_for_path(browser->getFilename());
+ delete browser;
+ } else {
+ delete browser;
+ return; // Cancel
+ }
+ }
+
+ // Now we have filename, open document.
+ bool canceled = false;
+ _document = app->document_open(file, &canceled);
+
+ if (!canceled && _document) {
+ // We're done, hand back to app.
+ response(GTK_RESPONSE_OK);
+ }
+ }
+ }
+}
+
+/**
+ * When a button needs to go to the next notebook page.
+ */
+void
+StartScreen::notebook_next(Gtk::Widget *button)
+{
+ int page = tabs->get_current_page();
+ if (page == 2) {
+ response(GTK_RESPONSE_CANCEL); // Only occurs from keypress.
+ } else {
+ tabs->set_current_page(page + 1);
+ }
+}
+
+/**
+ * When a key is pressed in the main window.
+ */
+bool
+StartScreen::on_key_press_event(GdkEventKey* event)
+{
+#ifdef GDK_WINDOWING_QUARTZ
+ // On macOS only, if user press Cmd+Q => exit
+ if (event->keyval == 'q' && event->state == (GDK_MOD2_MASK | GDK_META_MASK)) {
+ close();
+ return false;
+ }
+#endif
+ switch (event->keyval) {
+ case GDK_KEY_Escape:
+ // Prevent loading any selected items
+ response(GTK_RESPONSE_CANCEL);
+ return true;
+ case GDK_KEY_Return:
+ notebook_next(nullptr);
+ return true;
+ }
+
+ return false;
+}
+
+void
+StartScreen::on_response(int response_id)
+{
+ if (response_id == GTK_RESPONSE_DELETE_EVENT) {
+ // Don't open a window for force closing.
+ return;
+ }
+ if (response_id == GTK_RESPONSE_CANCEL) {
+ templates->reset_selection();
+ }
+ if (response_id != GTK_RESPONSE_OK && !_document) {
+ // Last ditch attempt to generate a new document while exiting.
+ _document = templates->new_document();
+ }
+}
+
+void
+StartScreen::show_toggle()
+{
+ Gtk::ToggleButton *button;
+ builder->get_widget("show_toggle", button);
+ if (button) {
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool("/options/boot/enabled", button->get_active());
+ } else {
+ g_warning("Can't find toggle button widget.");
+ }
+}
+
+/**
+ * Refresh theme in-place so user can see a semi-preview. This theme selection
+ * is not meant to be perfect, but hint to the user that they can set the
+ * theme if they want.
+ *
+ * @param theme_name - The name of the theme to load.
+ */
+void
+StartScreen::refresh_theme(Glib::ustring theme_name)
+{
+ auto const screen = Gdk::Screen::get_default();
+ if (INKSCAPE.themecontext->getContrastThemeProvider()) {
+ Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.themecontext->getContrastThemeProvider());
+ }
+ auto settings = Gtk::Settings::get_default();
+
+ auto prefs = Inkscape::Preferences::get();
+
+ settings->property_gtk_theme_name() = theme_name;
+ settings->property_gtk_application_prefer_dark_theme() = prefs->getBool("/theme/preferDarkTheme", true);
+ settings->property_gtk_icon_theme_name() = prefs->getString("/theme/iconTheme", prefs->getString("/theme/defaultIconTheme", ""));
+
+ if (prefs->getBool("/theme/symbolicIcons", false)) {
+ get_style_context()->add_class("symbolic");
+ get_style_context()->remove_class("regular");
+ } else {
+ get_style_context()->add_class("regular");
+ get_style_context()->remove_class("symbolic");
+ }
+
+ if (INKSCAPE.themecontext->getColorizeProvider()) {
+ Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.themecontext->getColorizeProvider());
+ }
+ if (!prefs->getBool("/theme/symbolicDefaultHighColors", false)) {
+ Gtk::CssProvider::create();
+ Glib::ustring css_str = INKSCAPE.themecontext->get_symbolic_colors();
+ try {
+ INKSCAPE.themecontext->getColorizeProvider()->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.themecontext->getColorizeProvider(),
+ GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ }
+ // set dark switch and disable if there is no prefer option for dark
+ refresh_dark_switch();
+
+ INKSCAPE.themecontext->getChangeThemeSignal().emit();
+}
+
+/**
+ * Set the theme, icon pack and other theme options from a set defined
+ * in the glade file. The combo box has a number of columns with the needed
+ * data describing how to set up the theme.
+ */
+void
+StartScreen::theme_changed()
+{
+ auto prefs = Inkscape::Preferences::get();
+
+ ThemeCols cols;
+ try {
+ auto row = active_combo("themes");
+ Glib::ustring theme_id = row[cols.id];
+ if (theme_id == "custom") return;
+ prefs->setString("/options/boot/theme", row[cols.id]);
+
+ // Update theme from combo.
+ Glib::ustring icons = row[cols.icons];
+ prefs->setBool("/toolbox/tools/small", row[cols.smallicons]);
+ prefs->setString("/theme/gtkTheme", row[cols.theme]);
+ prefs->setString("/theme/iconTheme", icons);
+ prefs->setBool("/theme/symbolicIcons", row[cols.symbolic]);
+
+ Gtk::Switch* dark_toggle = nullptr;
+ builder->get_widget("dark_toggle", dark_toggle);
+ bool is_dark = dark_toggle->get_active();
+ prefs->setBool("/theme/preferDarkTheme", is_dark);
+ prefs->setBool("/theme/darkTheme", is_dark);
+ // Symbolic icon colours
+ if (get_color_value(row[cols.base]) == 0) {
+ prefs->setBool("/theme/symbolicDefaultBaseColors", true);
+ prefs->setBool("/theme/symbolicDefaultHighColors", true);
+ } else {
+ Glib::ustring prefix = "/theme/" + icons;
+ prefs->setBool("/theme/symbolicDefaultBaseColors", false);
+ prefs->setBool("/theme/symbolicDefaultHighColors", false);
+ if (is_dark) {
+ prefs->setUInt(prefix + "/symbolicBaseColor", get_color_value(row[cols.base_dark]));
+ } else {
+ prefs->setUInt(prefix + "/symbolicBaseColor", get_color_value(row[cols.base]));
+ }
+ prefs->setUInt(prefix + "/symbolicSuccessColor", get_color_value(row[cols.success]));
+ prefs->setUInt(prefix + "/symbolicWarningColor", get_color_value(row[cols.warn]));
+ prefs->setUInt(prefix + "/symbolicErrorColor", get_color_value(row[cols.error]));
+ }
+
+ refresh_theme(prefs->getString("/theme/gtkTheme", prefs->getString("/theme/defaultGtkTheme", "")));
+ } catch(int e) {
+ g_warning("Couldn't find theme value.");
+ }
+}
+
+/**
+ * Called when the canvas dropdown changes.
+ */
+void
+StartScreen::canvas_changed()
+{
+ CanvasCols cols;
+ try {
+ auto row = active_combo("canvas");
+
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setString("/options/boot/canvas", row[cols.id]);
+
+ Gdk::RGBA gdk_color = Gdk::RGBA(row[cols.pagecolor]);
+ SPColor sp_color(gdk_color.get_red(), gdk_color.get_green(), gdk_color.get_blue());
+ prefs->setString("/template/base/pagecolor", sp_color.toString());
+ prefs->setDouble("/template/base/pageopacity", gdk_color.get_alpha());
+
+ Gdk::RGBA gdk_border = Gdk::RGBA(row[cols.bordercolor]);
+ SPColor sp_border(gdk_border.get_red(), gdk_border.get_green(), gdk_border.get_blue());
+ prefs->setString("/template/base/bordercolor", sp_border.toString());
+ prefs->setDouble("/template/base/borderopacity", gdk_border.get_alpha());
+
+ prefs->setBool("/template/base/pagecheckerboard", row[cols.checkered]);
+ prefs->setInt("/template/base/pageshadow", row[cols.shadow] ? 2 : 0);
+
+ Gdk::RGBA gdk_desk = Gdk::RGBA(row[cols.deskcolor]);
+ SPColor sp_desk(gdk_desk.get_red(), gdk_desk.get_green(), gdk_desk.get_blue());
+ prefs->setString("/template/base/deskcolor", sp_desk.toString());
+ } catch(int e) {
+ g_warning("Couldn't find canvas value.");
+ }
+}
+
+void
+StartScreen::filter_themes()
+{
+ ThemeCols cols;
+ // We need to disable themes which aren't available.
+ auto store = Glib::wrap(GTK_LIST_STORE(gtk_combo_box_get_model(themes->gobj())));
+ auto available = INKSCAPE.themecontext->get_available_themes();
+
+ // Detect use of custom theme here, detect defaults used in many systems.
+ auto settings = Gtk::Settings::get_default();
+ Glib::ustring theme_name = settings->property_gtk_theme_name();
+ Glib::ustring icons_name = settings->property_gtk_icon_theme_name();
+
+ bool has_system_theme = false;
+ if (theme_name != "Adwaita" || icons_name != "hicolor") {
+ has_system_theme = true;
+ /* Enable if/when we want custom to be the default.
+ if (prefs->getString("/options/boot/theme").empty()) {
+ prefs->setString("/options/boot/theme", "system")
+ theme_changed();
+ }*/
+ }
+
+ for(auto row : store->children()) {
+ Glib::ustring theme = row[cols.theme];
+ if (!row[cols.enabled]) {
+ // Available themes; We only "enable" them, we don't disable them.
+ row[cols.enabled] = available.find(theme) != available.end();
+ } else if(row[cols.id] == "system" && !has_system_theme) {
+ // Disable system theme option if not available.
+ row[cols.enabled] = false;
+ }
+ }
+}
+
+void
+StartScreen::enlist_keys()
+{
+ NameIdCols cols;
+ Gtk::ComboBox *keys;
+ builder->get_widget("keys", keys);
+ if (!keys) return;
+
+ auto store = Glib::wrap(GTK_LIST_STORE(gtk_combo_box_get_model(keys->gobj())));
+ store->clear();
+
+ for(auto item: Inkscape::Shortcuts::get_file_names()){
+ Gtk::TreeModel::Row row = *(store->append());
+ row[cols.col_name] = item.first;
+ row[cols.col_id] = item.second;
+ }
+
+ auto prefs = Inkscape::Preferences::get();
+ auto current = prefs->getString("/options/kbshortcuts/shortcutfile");
+ if (current.empty()) {
+ current = "inkscape.xml";
+ }
+ keys->set_active_id(current);
+}
+
+/**
+ * Set the keys file based on the keys set in the enlist above
+ */
+void
+StartScreen::keyboard_changed()
+{
+ NameIdCols cols;
+ try {
+ auto row = active_combo("keys");
+ auto prefs = Inkscape::Preferences::get();
+ Glib::ustring set_to = row[cols.col_id];
+ prefs->setString("/options/kbshortcuts/shortcutfile", set_to);
+ Inkscape::Shortcuts::getInstance().init();
+
+ Gtk::InfoBar* keys_warning;
+ builder->get_widget("keys_warning", keys_warning);
+ if (set_to != "inkscape.xml" && set_to != "default.xml") {
+ keys_warning->set_message_type(Gtk::MessageType::MESSAGE_WARNING);
+ keys_warning->show();
+ } else {
+ keys_warning->hide();
+ }
+ } catch(int e) {
+ g_warning("Couldn't find keys value.");
+ }
+}
+
+/**
+ * Set Dark Switch based on current selected theme.
+ * We will disable switch if current theme doesn't have prefer dark theme option.
+ */
+
+void StartScreen::refresh_dark_switch()
+{
+ auto prefs = Inkscape::Preferences::get();
+
+ Gtk::Container *window = dynamic_cast<Gtk::Container *>(get_toplevel());
+ bool dark = INKSCAPE.themecontext->isCurrentThemeDark(window);
+ prefs->setBool("/theme/preferDarkTheme", dark);
+ prefs->setBool("/theme/darkTheme", dark);
+
+ auto themes = INKSCAPE.themecontext->get_available_themes();
+ Glib::ustring current_theme = prefs->getString("/theme/gtkTheme", prefs->getString("/theme/defaultGtkTheme", ""));
+
+ Gtk::Switch *dark_toggle = nullptr;
+ builder->get_widget("dark_toggle", dark_toggle);
+
+ if (!themes[current_theme]) {
+ dark_toggle->set_sensitive(false);
+ } else {
+ dark_toggle->set_sensitive(true);
+ }
+ dark_toggle->set_active(dark);
+}
+
+} // 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/startup.h b/src/ui/dialog/startup.h
new file mode 100644
index 0000000..ca0fcf3
--- /dev/null
+++ b/src/ui/dialog/startup.h
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief A dialog for the start screen
+ *
+ * Copyright (C) Martin Owens 2020 <doctormo@gmail.com>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef STARTSCREEN_H
+#define STARTSCREEN_H
+
+#include <gtkmm.h>
+
+class SPDocument;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+class TemplateList;
+}
+
+namespace Dialog {
+
+class StartScreen : public Gtk::Dialog {
+
+public:
+ StartScreen();
+ ~StartScreen() override;
+
+ SPDocument* get_document() { return _document; }
+
+protected:
+ bool on_key_press_event(GdkEventKey* event) override;
+ void on_response(int response_id) override;
+
+private:
+ void notebook_next(Gtk::Widget *button);
+ Gtk::TreeModel::Row active_combo(std::string widget_name);
+ void set_active_combo(std::string widget_name, std::string unique_id);
+ void show_toggle();
+ void enlist_recent_files();
+ void enlist_keys();
+ void filter_themes();
+ void keyboard_changed();
+ void notebook_switch(Gtk::Widget *tab, guint page_num);
+
+ void theme_changed();
+ void canvas_changed();
+ void refresh_theme(Glib::ustring theme_name);
+ void refresh_dark_switch();
+
+ void new_document();
+ void load_document();
+ void on_recent_changed();
+ void on_kind_changed(Gtk::Widget *tab, guint page_num);
+
+
+private:
+ Glib::RefPtr<Gtk::Builder> builder;
+ Gtk::Window *window = nullptr;
+ Gtk::Notebook *tabs = nullptr;
+ Gtk::Fixed *banners = nullptr;
+ Gtk::ComboBox *themes = nullptr;
+ Gtk::TreeView *recent_treeview = nullptr;
+ Gtk::Button *load_btn = nullptr;
+ Inkscape::UI::Widget::TemplateList *templates = nullptr;
+
+ SPDocument* _document = nullptr;
+};
+
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // STARTSCREEN_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/styledialog.cpp b/src/ui/dialog/styledialog.cpp
new file mode 100644
index 0000000..efb7199
--- /dev/null
+++ b/src/ui/dialog/styledialog.cpp
@@ -0,0 +1,1601 @@
+// 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 <map>
+#include <regex>
+#include <utility>
+
+#include <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.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 "util/trim.h"
+#include "xml/attribute-record.h"
+#include "xml/node-observer.h"
+#include "xml/sp-css-attr.h"
+
+// G_MESSAGES_DEBUG=DEBUG_STYLEDIALOG gdb ./inkscape
+// #define DEBUG_STYLEDIALOG
+// #define G_LOG_DOMAIN "STYLEDIALOG"
+
+using Inkscape::DocumentUndo;
+
+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;
+
+ if (!root) {
+ return 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::NodeType::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 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)
+{
+ if (!getShowing()) {
+ return;
+ }
+ readStyleElement();
+}
+
+void StyleDialog::_nodeRemoved(Inkscape::XML::Node &repr)
+{
+ if (!getShowing()) {
+ return;
+ }
+ if (_textNode == &repr) {
+ _textNode = nullptr;
+ }
+
+ readStyleElement();
+}
+
+void StyleDialog::_nodeChanged(Inkscape::XML::Node &object)
+{
+ if (!getShowing()) {
+ return;
+ }
+ g_debug("StyleDialog::_nodeChanged");
+ 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()
+ : DialogBase("/dialogs/style", "Style")
+{
+ 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);
+ _scrolledWindow.set_overlay_scrolling(false);
+ _vadj = _scrolledWindow.get_vadjustment();
+ _vadj->signal_value_changed().connect(sigc::mem_fun(*this, &StyleDialog::_vscroll));
+ _mainBox.set_orientation(Gtk::ORIENTATION_VERTICAL);
+
+ pack_start(_mainBox, Gtk::PACK_EXPAND_WIDGET);
+}
+
+StyleDialog::~StyleDialog()
+{
+ removeObservers();
+}
+
+void StyleDialog::_vscroll()
+{
+ if (!_scrollock) {
+ _scrollpos = _vadj->get_value();
+ } else {
+ _vadj->set_value(_scrollpos);
+ _scrollock = false;
+ }
+}
+
+Glib::ustring StyleDialog::fixCSSSelectors(Glib::ustring selector)
+{
+ g_debug("SelectorsDialog::fixCSSSelectors");
+ Util::trim(selector);
+ std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[,]+", selector);
+ CRSelector *cr_selector = cr_selector_parse_from_buf((guchar const *)selector.c_str(), CR_UTF_8);
+ for (auto token : tokens) {
+ Util::trim(token);
+ std::vector<Glib::ustring> subtokens = Glib::Regex::split_simple("[ ]+", token);
+ for (auto subtoken : subtokens) {
+ Util::trim(subtoken);
+ CRSelector *cr_selector = cr_selector_parse_from_buf((guchar const *)subtoken.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 "";
+}
+
+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");
+
+ auto document = getDocument();
+ if (_updating || !document || _deletion)
+ return; // Don't read if we wrote style element.
+ _updating = true;
+ _scrollock = 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 */)
+
+ 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 = getSelection();
+ SPObject *obj = nullptr;
+ if (selection->objects().size() == 1) {
+ obj = selection->objects().back();
+ }
+ if (!obj) {
+ obj = document->getXMLDialogSelectedObject();
+ if (obj && !obj->getRepr()) {
+ obj = nullptr; // treat detached object as no selection
+ }
+ }
+
+ auto gladefile = get_filename_string(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())) {
+ auto value = attr_prop[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] = value;
+ row[_mColumns._colStrike] = false;
+ row[_mColumns._colOwner] = Glib::ustring("Current value");
+ row[_mColumns._colHref] = nullptr;
+ row[_mColumns._colLinked] = false;
+ if (is_url(value.c_str())) {
+ auto id = value.substr(5, value.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];
+ Util::trim(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 understand
+ // 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 != SPStyleSrc::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 out.");
+ 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 != SPStyleSrc::UNSET) {
+ auto key = iter->id();
+ if (key != SPAttr::FONT && key != SPAttr::D && key != SPAttr::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(_("Current value"));
+ if (!value.empty()) {
+ tooltiptext = Glib::ustring::compose(_("Used in %1"), _owner_style[row[_mColumns._colName]]);
+ row[_mColumns._colStrike] = true;
+ } else {
+ row[_mColumns._colStrike] = false;
+ }
+ 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) {
+ auto selection = getSelection();
+ 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);
+ _deletion = false;
+ }
+}
+
+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;
+
+ Util::trim(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) {
+ Util::trim(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");
+ auto selection = getSelection();
+ if (_updating && selection)
+ return;
+ _scrollock = true;
+ SPObject *obj = nullptr;
+ if (selection->objects().size() == 1) {
+ obj = selection->objects().back();
+ }
+ if (!obj) {
+ obj = 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.append("\n").append(selector.raw()).append(" { \n");
+ }
+ selectorpos = _deleted_pos;
+ for (auto &row : store->children()) {
+ selector = row[_mColumns._colSelector];
+ selectorpos = row[_mColumns._colSelectorPos];
+ const char *opencomment = "";
+ const char *closecomment = "";
+ if (selector != "style_properties" && selector != "attributes") {
+ opencomment = row[_mColumns._colActive] ? " " : " /*";
+ closecomment = row[_mColumns._colActive] ? "\n" : "*/\n";
+ }
+ Glib::ustring const &name = row[_mColumns._colName];
+ Glib::ustring const &value = row[_mColumns._colValue];
+ if (!(name.empty() && value.empty())) {
+ styleContent = styleContent + opencomment + name.raw() + ":" + value.raw() + ";" + closecomment;
+ }
+ }
+ if (selector != "style_properties" && selector != "attributes") {
+ styleContent = styleContent + "}";
+ }
+ if (selector == "style_properties") {
+ _updating = true;
+ obj->getRepr()->setAttribute("style", styleContent);
+ _updating = false;
+ } else if (selector == "attributes") {
+ for (auto iter : obj->style->properties()) {
+ auto key = iter->id();
+ if (key != SPAttr::FONT && key != SPAttr::D && key != SPAttr::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 const &name = row[_mColumns._colName];
+ Glib::ustring const &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());
+ if (empty) {
+ textNode->setContent("");
+ }
+ }
+ _updating = false;
+ readStyleElement();
+ for (auto iter : getDocument()->getObjectsBySelector(selector)) {
+ iter->style->readFromObject(iter);
+ iter->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
+ }
+ DocumentUndo::done(SP_ACTIVE_DOCUMENT, _("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);
+}
+/*Hardcode 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");
+ _scrollock = 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 == "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)
+{
+ g_debug("StyleDialog::_startNameEdit");
+ _scrollock = 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);
+ auto selection = styledialog->_current_css_tree->get_selection();
+ Gtk::TreeIter iter = *(selection->get_selected());
+ if (!iter) {
+ return FALSE;
+ }
+ 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");
+
+ _scrollock = 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 = finalname.find_first_of(";:=");
+ 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 enough 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->removeAttribute(name);
+ sp_repr_css_write_string(css, css_str);
+ obj->getRepr()->setAttributeOrRemoveIfEmpty("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");
+
+ _scrollock = 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");
+
+ _scrollock = 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;
+}
+
+/**
+ * @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 getDocument()->getObjectsBySelector(selector);
+}
+
+void StyleDialog::_closeDialog(Gtk::Dialog *textDialogPtr) { textDialogPtr->response(Gtk::RESPONSE_OK); }
+
+
+void StyleDialog::removeObservers()
+{
+ if (_textNode) {
+ _textNode->removeObserver(*m_styletextwatcher);
+ _textNode = nullptr;
+ }
+ if (m_root) {
+ m_root->removeSubtreeObserver(*m_nodewatcher);
+ m_root = nullptr;
+ }
+}
+
+/**
+ * Handle document replaced. (Happens when a default document is immediately replaced by another
+ * document in a new window.)
+ */
+void StyleDialog::documentReplaced()
+{
+ removeObservers();
+ if (auto document = getDocument()) {
+ m_root = document->getReprRoot();
+ m_root->addSubtreeObserver(*m_nodewatcher);
+ }
+ readStyleElement();
+}
+
+/*
+ * Handle a change in which objects are selected in a document.
+ */
+void StyleDialog::selectionChanged(Selection * /*selection*/)
+{
+ _scrollpos = 0;
+ _vadj->set_value(0);
+ // Sometimes the selection changes because inkscape is closing.
+ if (getDesktop()) {
+ 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..21292af
--- /dev/null
+++ b/src/ui/dialog/styledialog.h
@@ -0,0 +1,195 @@
+// 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 <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 <memory>
+#include <vector>
+
+#include "style-enums.h"
+#include "ui/dialog/dialog-base.h"
+#include "xml/helper-observer.h"
+
+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 DialogBase
+{
+public:
+ StyleDialog();
+ ~StyleDialog() override;
+
+ void documentReplaced() override;
+ void selectionChanged(Selection *selection) override;
+
+ void setCurrentSelector(Glib::ustring current_selector);
+ Gtk::TreeView *_current_css_tree;
+ Gtk::TreeViewColumn *_current_value_col;
+ Gtk::TreeModel::Path _current_path;
+ bool _deletion{false};
+ 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 removeObservers();
+ /* 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{0};
+ // 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 _vscroll();
+ bool _scrollock;
+ double _scrollpos{0};
+ Glib::ustring _current_selector;
+
+ // Update watchers
+ std::unique_ptr<Inkscape::XML::NodeObserver> m_nodewatcher;
+ std::unique_ptr<Inkscape::XML::NodeObserver> m_styletextwatcher;
+
+ // 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{nullptr}; // Track so we know when to add a NodeObserver.
+ bool _updating{false}; // Prevent cyclic actions: read <-> write, select via dialog <-> via desktop
+
+ void _closeDialog(Gtk::Dialog *textDialogPtr);
+};
+
+} // 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..049653c
--- /dev/null
+++ b/src/ui/dialog/svg-fonts-dialog.cpp
@@ -0,0 +1,1794 @@
+// 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 <iomanip>
+
+#include <gtkmm/scale.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/expander.h>
+#include <gtkmm/imagemenuitem.h>
+#include <glibmm/stringutils.h>
+#include <glibmm/i18n.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "layer-manager.h"
+#include "selection.h"
+#include "svg-fonts-dialog.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-guide.h"
+#include "object/sp-missing-glyph.h"
+#include "object/sp-path.h"
+#include "svg/svg.h"
+#include "util/units.h"
+#include "xml/repr.h"
+#include "document.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);
+ auto context = get_style_context();
+ Gdk::RGBA fg = context->get_color(get_state_flags());
+ cr->set_source_rgb(fg.get_red(), fg.get_green(), fg.get_blue());
+ // crash on macos: https://gitlab.com/inkscape/inkscape/-/issues/266
+ try {
+ cr->show_text(_text.c_str());
+ }
+ catch (std::exception& ex) {
+ g_warning("Error drawing custom SVG font text: %s", ex.what());
+ }
+ }
+ return true;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+
+void SvgGlyphRenderer::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 (!_font || !_tree) return;
+
+ cr->set_font_face(Cairo::RefPtr<Cairo::FontFace>(new Cairo::FontFace(_font->get_font_face(), false /* does not have reference */)));
+ cr->set_font_size(_font_size);
+ Glib::ustring glyph = _property_glyph.get_value();
+ Cairo::TextExtents ext;
+ cr->get_text_extents(glyph, ext);
+ cr->move_to(cell_area.get_x() + (_width - ext.width) / 2, cell_area.get_y() + 1);
+ auto context = _tree->get_style_context();
+ Gtk::StateFlags sflags = _tree->get_state_flags();
+ if (flags & Gtk::CELL_RENDERER_SELECTED) {
+ sflags |= Gtk::STATE_FLAG_SELECTED;
+ }
+ Gdk::RGBA fg = context->get_color(sflags);
+ cr->set_source_rgb(fg.get_red(), fg.get_green(), fg.get_blue());
+ // crash on macos: https://gitlab.com/inkscape/inkscape/-/issues/266
+ try {
+ cr->show_text(glyph);
+ }
+ catch (std::exception& ex) {
+ g_warning("Error drawing custom SVG font glyphs: %s", ex.what());
+ }
+}
+
+bool SvgGlyphRenderer::activate_vfunc(
+ GdkEvent* event, Gtk::Widget& widget, const Glib::ustring& path, const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area, Gtk::CellRendererState flags) {
+
+ Glib::ustring glyph = _property_glyph.get_value();
+ _signal_clicked.emit(event, glyph);
+ return false;
+}
+
+SvgFontsDialog::AttrEntry::AttrEntry(SvgFontsDialog* d, gchar* lbl, Glib::ustring tooltip, const SPAttr attr)
+{
+ this->dialog = d;
+ this->attr = attr;
+ entry.set_tooltip_text(tooltip);
+ _label = Gtk::make_managed<Gtk::Label>(lbl);
+ _label->show();
+ _label->set_halign(Gtk::ALIGN_START);
+ entry.signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::AttrEntry::on_attr_changed));
+}
+
+void SvgFontsDialog::AttrEntry::set_text(const 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(){
+ if (dialog->_update.pending()) return;
+
+ SPObject* o = nullptr;
+ for (auto& node: dialog->get_selected_spfont()->children) {
+ switch(this->attr){
+ case SPAttr::FONT_FAMILY:
+ if (is<SPFontFace>(&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(), _("Set SVG Font attribute"), "");
+ }
+
+}
+
+SvgFontsDialog::AttrSpin::AttrSpin(SvgFontsDialog* d, gchar* lbl, Glib::ustring tooltip, const SPAttr attr)
+{
+ this->dialog = d;
+ this->attr = attr;
+ spin.set_tooltip_text(tooltip);
+ spin.show();
+ _label = Gtk::make_managed<Gtk::Label>(lbl);
+ _label->show();
+ _label->set_halign(Gtk::ALIGN_START);
+ spin.set_range(0, 4096);
+ spin.set_increments(10, 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(){
+ if (dialog->_update.pending()) return;
+
+ SPObject* o = nullptr;
+ switch (this->attr) {
+
+ // <font> attributes
+ case SPAttr::HORIZ_ORIGIN_X:
+ case SPAttr::HORIZ_ORIGIN_Y:
+ case SPAttr::HORIZ_ADV_X:
+ case SPAttr::VERT_ORIGIN_X:
+ case SPAttr::VERT_ORIGIN_Y:
+ case SPAttr::VERT_ADV_Y:
+ o = this->dialog->get_selected_spfont();
+ break;
+
+ // <font-face> attributes
+ case SPAttr::UNITS_PER_EM:
+ case SPAttr::ASCENT:
+ case SPAttr::DESCENT:
+ case SPAttr::CAP_HEIGHT:
+ case SPAttr::X_HEIGHT:
+ for (auto& node: dialog->get_selected_spfont()->children){
+ if (is<SPFontFace>(&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(), _("Set SVG Font attribute"), "");
+ }
+
+}
+
+Gtk::Box* SvgFontsDialog::AttrCombo(gchar* lbl, const SPAttr /*attr*/){
+ Gtk::Box* hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
+ hbox->add(* Gtk::manage(new Gtk::Label(lbl)) );
+ hbox->add(* Gtk::manage(new Gtk::ComboBox()) );
+ hbox->show_all();
+ return hbox;
+}
+
+/*** SvgFontsDialog ***/
+
+GlyphComboBox::GlyphComboBox() {
+}
+
+void GlyphComboBox::update(SPFont* spfont){
+ if (!spfont) return;
+
+ // remove wrapping - it has severe performance penalty for appending items
+ set_wrap_width(0);
+
+ this->remove_all();
+
+ for (auto& node: spfont->children) {
+ if (is<SPGlyph>(&node)){
+ this->append((static_cast<SPGlyph*>(&node))->unicode);
+ }
+ }
+
+ // set desired wrpping now
+ set_wrap_width(4);
+}
+
+void SvgFontsDialog::on_kerning_value_changed(){
+ if (!get_selected_kerning_pair()) {
+ return;
+ }
+
+ //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(getDocument(), undokey.c_str(), _("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::sort_glyphs(SPFont* font) {
+ if (!font) return;
+
+ {
+ auto scoped(_update.block());
+ font->sort_glyphs();
+ }
+ update_glyphs();
+}
+
+// return U+<code> ... string
+Glib::ustring create_unicode_name(const Glib::ustring& unicode, int max_chars) {
+ std::ostringstream ost;
+ if (unicode.empty()) {
+ ost << "-";
+ }
+ else {
+ auto it = unicode.begin();
+ for (int i = 0; i < max_chars && it != unicode.end(); ++i) {
+ if (i > 0) {
+ ost << " ";
+ }
+ unsigned int code = *it++;
+ ost << "U+" << std::hex << std::uppercase << std::setw(6) << std::setfill('0') << code;
+ }
+ if (it != unicode.end()) {
+ ost << "..."; // there's more, but we skip them
+ }
+ }
+ return ost.str();
+}
+
+// synthetic name consists for unicode hex numbers derived from glyph's "unicode" attribute
+Glib::ustring get_glyph_synthetic_name(const SPGlyph& glyph) {
+ auto unicode_name = create_unicode_name(glyph.unicode, 3);
+ // U+<code> plus character
+ return unicode_name + " " + glyph.unicode;
+}
+
+// full name consists of user-defined name combined with synthetic one
+Glib::ustring get_glyph_full_name(const SPGlyph& glyph) {
+ auto name = get_glyph_synthetic_name(glyph);
+ if (!glyph.glyph_name.empty()) {
+ // unicode name first, followed by user name - for sorting layers
+ return name + " " + glyph.glyph_name;
+ }
+ else {
+ return name;
+ }
+}
+
+// look for a layer by its label; looking only in direct sublayers of 'root_layer'
+SPItem* find_layer(SPDesktop* desktop, SPObject* root_layer, const Glib::ustring& name) {
+ if (!desktop) return nullptr;
+
+ const auto& layers = desktop->layerManager();
+ auto root = root_layer == nullptr ? layers.currentRoot() : root_layer;
+ if (!root) return nullptr;
+
+ // check only direct child layers
+ auto it = std::find_if(root->children.begin(), root->children.end(), [&](SPObject& obj) {
+ return layers.isLayer(&obj) && obj.label() && strcmp(obj.label(), name.c_str()) == 0;
+ });
+ if (it != root->children.end()) {
+ return static_cast<SPItem*>(&*it);
+ }
+
+ return nullptr; // not found
+}
+
+std::vector<SPGroup*> get_direct_sublayers(SPObject* layer) {
+ std::vector<SPGroup*> layers;
+ if (!layer) return layers;
+
+ for (auto&& item : layer->children) {
+ if (auto l = LayerManager::asLayer(&item)) {
+ layers.push_back(l);
+ }
+ }
+
+ return layers;
+}
+
+void rename_glyph_layer(SPDesktop* desktop, SPItem* layer, const Glib::ustring& font, const Glib::ustring& name) {
+ if (!desktop || !layer || font.empty() || name.empty()) return;
+
+ auto parent_layer = find_layer(desktop, desktop->layerManager().currentRoot(), font);
+ if (!parent_layer) return;
+
+ // before renaming the layer find new place to move it into to keep sorted order intact
+ auto glyph_layers = get_direct_sublayers(parent_layer);
+
+ auto it = std::lower_bound(glyph_layers.rbegin(), glyph_layers.rend(), name, [&](auto&& layer, const Glib::ustring n) {
+ auto label = layer->label();
+ if (!label) return false;
+
+ Glib::ustring temp(label);
+ return std::lexicographical_compare(temp.begin(), temp.end(), n.begin(), n.end());
+ });
+ SPObject* after = nullptr;
+ if (it != glyph_layers.rend()) {
+ after = *it;
+ }
+
+ // SPItem changeOrder messes up inserting into first position, so dropping to Node level
+ if (layer != after && parent_layer->getRepr() && layer->getRepr()) {
+ parent_layer->getRepr()->changeOrder(layer->getRepr(), after ? after->getRepr() : nullptr);
+ }
+
+ desktop->layerManager().renameLayer(layer, name.c_str(), false);
+}
+
+SPItem* get_layer_for_glyph(SPDesktop* desktop, const Glib::ustring& font, const Glib::ustring& name) {
+ if (!desktop || name.empty() || font.empty()) return nullptr;
+
+ auto parent_layer = find_layer(desktop, desktop->layerManager().currentRoot(), font);
+ if (!parent_layer) return nullptr;
+
+ return find_layer(desktop, parent_layer, name);
+}
+
+SPItem* get_or_create_layer_for_glyph(SPDesktop* desktop, const Glib::ustring& font, const Glib::ustring& name) {
+ if (!desktop || name.empty() || font.empty()) return nullptr;
+
+ auto& layers = desktop->layerManager();
+ auto parent_layer = find_layer(desktop, layers.currentRoot(), font);
+ if (!parent_layer) {
+ // create a new layer for a font
+ parent_layer = static_cast<SPItem*>(create_layer(layers.currentRoot(), layers.currentRoot(), Inkscape::LayerRelativePosition::LPOS_CHILD));
+ if (!parent_layer) return nullptr;
+
+ layers.renameLayer(parent_layer, font.c_str(), false);
+ }
+
+ if (auto layer = find_layer(desktop, parent_layer, name)) {
+ return layer;
+ }
+
+ // find the right place for a new layer, so they appear sorted
+ auto glyph_layers = get_direct_sublayers(parent_layer);
+ // auto& glyph_layers = parent_layer->children;
+ auto it = std::lower_bound(glyph_layers.rbegin(), glyph_layers.rend(), name, [&](auto&& layer, const Glib::ustring n) {
+ auto label = layer->label();
+ if (!label) return false;
+
+ Glib::ustring temp(label);
+ return std::lexicographical_compare(temp.begin(), temp.end(), n.begin(), n.end());
+ });
+ SPObject* insert = parent_layer;
+ Inkscape::LayerRelativePosition pos = Inkscape::LayerRelativePosition::LPOS_ABOVE;
+ if (it != glyph_layers.rend()) {
+ insert = *it;
+ }
+ else {
+ // auto first = std::find_if(glyph_layers.begin(), glyph_layers.end(), [&](auto&& obj) {
+ // return layers.isLayer(&obj);
+ // });
+ if (!glyph_layers.empty()) {
+ insert = glyph_layers.front();
+ pos = Inkscape::LayerRelativePosition::LPOS_BELOW;
+ }
+ }
+
+ // create a new layer for a glyph
+ auto layer = create_layer(parent_layer, insert, pos);
+ if (!layer) return nullptr;
+
+ layers.renameLayer(layer, name.c_str(), false);
+
+ DocumentUndo::done(desktop->getDocument(), _("Add layer"), "");
+ return cast<SPItem>(layer);
+}
+
+void SvgFontsDialog::create_glyphs_popup_menu(Gtk::Widget& parent, sigc::slot<void ()> rem)
+{
+ // - edit glyph (show its layer)
+ // - sort glyphs and their layers
+ // - remove current glyph
+ auto mi = Gtk::make_managed<Gtk::MenuItem>(_("_Edit current glyph"), true);
+ mi->show();
+ mi->signal_activate().connect([=](){
+ edit_glyph(get_selected_glyph());
+ });
+ _GlyphsContextMenu.append(*mi);
+
+ mi = Gtk::make_managed<Gtk::SeparatorMenuItem>();
+ mi->show();
+ _GlyphsContextMenu.append(*mi);
+
+ mi = Gtk::make_managed<Gtk::MenuItem>(_("_Sort glyphs"), true);
+ mi->show();
+ mi->signal_activate().connect([=](){
+ sort_glyphs(get_selected_spfont());
+ });
+ _GlyphsContextMenu.append(*mi);
+
+ mi = Gtk::make_managed<Gtk::SeparatorMenuItem>();
+ mi->show();
+ _GlyphsContextMenu.append(*mi);
+
+ mi = Gtk::make_managed<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()){
+ _grid.set_sensitive(true);
+ glyphs_vbox.set_sensitive(true);
+ kerning_vbox.set_sensitive(true);
+ } else {
+ _grid.set_sensitive(false);
+ glyphs_vbox.set_sensitive(false);
+ kerning_vbox.set_sensitive(false);
+ }
+}
+
+Glib::ustring get_font_label(SPFont* font) {
+ if (!font) return Glib::ustring();
+
+ const gchar* label = font->label();
+ const gchar* id = font->getId();
+ return Glib::ustring(label ? label : (id ? id : "font"));
+};
+
+/** Add all fonts in the getDocument() to the combobox.
+ * This function is called when new document is selected as well as when SVG "definition" section changes.
+ * Try to detect if font(s) have actually been modified to eliminate some expensive refreshes.
+ */
+void SvgFontsDialog::update_fonts(bool document_replaced)
+{
+ std::vector<SPObject*> fonts;
+ if (auto document = getDocument()) {
+ fonts = document->getResourceList( "font" );
+ }
+
+ auto children = _model->children();
+ bool equal = false;
+ bool selected_font = false;
+
+ // compare model and resources
+ if (!document_replaced && children.size() == fonts.size()) {
+ equal = true; // assume they are the same
+ auto it = fonts.begin();
+ for (auto&& node : children) {
+ SPFont* sp_font = node[_columns.spfont];
+ if (it == fonts.end() || *it != sp_font) {
+ // difference detected; update model
+ equal = false;
+ break;
+ }
+ ++it;
+ }
+ }
+
+ // rebuild model if list of fonts is different
+ if (!equal) {
+ _model->clear();
+ for (auto font : fonts) {
+ Gtk::TreeModel::Row row = *_model->append();
+ auto f = cast<SPFont>(font);
+ row[_columns.spfont] = f;
+ row[_columns.svgfont] = new SvgFont(f);
+ row[_columns.label] = get_font_label(f);
+ }
+ if (!fonts.empty()) {
+ // select a font, this dialog is disabled without a font
+ auto selection = _FontsList.get_selection();
+ if (selection) {
+ selection->select(_model->get_iter("0"));
+ selected_font = true;
+ }
+ }
+ }
+ else {
+ // list of fonts is the same, but attributes may have changed
+ auto it = fonts.begin();
+ for (auto&& node : children) {
+ if (auto font = cast<SPFont>(*it++)) {
+ node[_columns.label] = get_font_label(font);
+ }
+ }
+ }
+
+ if (document_replaced && !selected_font) {
+ // replace fonts, they are stale
+ font_selected(nullptr, nullptr);
+ }
+ else {
+ 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) {
+ //TODO: perhaps reset all values when there's no font
+ _familyname_entry->set_text("");
+ 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 (is<SPFontFace>(&obj)){
+ _familyname_entry->set_text((cast<SPFontFace>(&obj))->font_family);
+ _units_per_em_spin->set_value((cast<SPFontFace>(&obj))->units_per_em);
+ _ascent_spin->set_value((cast<SPFontFace>(&obj))->ascent);
+ _descent_spin->set_value((cast<SPFontFace>(&obj))->descent);
+ _x_height_spin->set_value((cast<SPFontFace>(&obj))->x_height);
+ _cap_height_spin->set_value((cast<SPFontFace>(&obj))->cap_height);
+ }
+ }
+}
+
+void SvgFontsDialog::font_selected(SvgFont* svgfont, SPFont* spfont) {
+ // in update
+ auto scoped(_update.block());
+
+ first_glyph.update(spfont);
+ second_glyph.update(spfont);
+ kerning_preview.set_svgfont(svgfont);
+ _font_da.set_svgfont(svgfont);
+ _font_da.redraw();
+ _glyph_renderer->set_svg_font(svgfont);
+ _glyph_cell_renderer->set_svg_font(svgfont);
+
+ kerning_slider->set_range(0, spfont ? spfont->horiz_adv_x : 0);
+ kerning_slider->set_draw_value(false);
+ kerning_slider->set_value(0);
+
+ update_global_settings_tab();
+ populate_glyphs_box();
+ populate_kerning_pairs_box();
+ update_sensitiveness();
+}
+
+void SvgFontsDialog::on_font_selection_changed(){
+ SPFont* spfont = get_selected_spfont();
+ SvgFont* svgfont = get_selected_svgfont();
+ font_selected(svgfont, spfont);
+}
+
+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;
+}
+
+Gtk::TreeModel::iterator SvgFontsDialog::get_selected_glyph_iter() {
+ if (_GlyphsListScroller.get_visible()) {
+ if (auto selection = _GlyphsList.get_selection()) {
+ Gtk::TreeModel::iterator it = selection->get_selected();
+ return it;
+ }
+ }
+ else {
+ std::vector<Gtk::TreePath> selected = _glyphs_grid.get_selected_items();
+ if (selected.size() == 1) {
+ Gtk::ListStore::iterator it = _GlyphsListStore->get_iter(selected.front());
+ return it;
+ }
+ }
+ return Gtk::TreeModel::iterator();
+}
+
+SPGlyph* SvgFontsDialog::get_selected_glyph()
+{
+ if (auto it = get_selected_glyph_iter()) {
+ return (*it)[_GlyphsListColumns.glyph_node];
+ }
+ return nullptr;
+}
+
+void SvgFontsDialog::set_selected_glyph(SPGlyph* glyph) {
+ if (!glyph) return;
+
+ _GlyphsListStore->foreach_iter([=](const Gtk::TreeModel::iterator& it) {
+ if (it->get_value(_GlyphsListColumns.glyph_node) == glyph) {
+ if (auto selection = _GlyphsList.get_selection()) {
+ selection->select(it);
+ }
+ auto selected_item = _GlyphsListStore->get_path(it);
+ _glyphs_grid.select_path(selected_item);
+ return true; // stop
+ }
+ return false; // continue
+ });
+}
+
+SPGuide* get_guide(SPDocument& doc, const Glib::ustring& id) {
+ auto object = doc.getObjectById(id);
+ if (!object) return nullptr;
+
+ // get guide line
+ if (auto guide = cast<SPGuide>(object)) {
+ return guide;
+ }
+ // remove colliding object
+ object->deleteObject();
+ return nullptr;
+}
+
+SPGuide* create_guide(SPDocument& doc, double x0, double y0, double x1, double y1) {
+ return SPGuide::createSPGuide(&doc, Geom::Point(x0, y1), Geom::Point(x1, y1));
+}
+
+void set_up_typography_canvas(SPDocument* document, double em, double asc, double cap, double xheight, double des) {
+ if (!document || em <= 0) return;
+
+ // set size and viewbox
+ auto size = Inkscape::Util::Quantity(em, "px");
+ bool change_size = false;
+ document->setWidthAndHeight(size, size, change_size);
+ document->setViewBox(Geom::Rect::from_xywh(0, 0, em, em));
+
+ // baseline
+ double base = des;
+ double ascPos = base + asc;
+ double capPos = base + cap;
+ double xPos = base + xheight;
+ double desPos = base - des;
+
+ if (!document->is_yaxisdown()) {
+ base = size.quantity - des;
+ ascPos = base - asc;
+ capPos = base - cap;
+ xPos = base - xheight;
+ desPos = base + des;
+ }
+
+ // add/move guide lines
+ struct { double pos; const char* name; const char* id; } guides[5] = {
+ {ascPos, _("ascender"), "ink-font-guide-ascender"},
+ {capPos, _("caps"), "ink-font-guide-caps"},
+ {xPos, _("x-height"), "ink-font-guide-x-height"},
+ {base, _("baseline"), "ink-font-guide-baseline"},
+ {desPos, _("descender"), "ink-font-guide-descender"},
+ };
+
+ double left = 0;
+ double right = em;
+
+ for (auto&& g : guides) {
+ double y = em - g.pos;
+ auto guide = get_guide(*document, g.id);
+ if (guide) {
+ guide->set_locked(false, true);
+ guide->moveto(Geom::Point(left, y), true);
+ }
+ else {
+ guide = create_guide(*document, left, y, right, y);
+ guide->getRepr()->setAttributeOrRemoveIfEmpty("id", g.id);
+ }
+ guide->set_label(g.name, true);
+ guide->set_locked(true, true);
+ }
+
+ DocumentUndo::done(document, _("Set up typography canvas"), "");
+}
+
+const int MARGIN_SPACE = 4;
+
+Gtk::Box* SvgFontsDialog::global_settings_tab(){
+
+ _fonts_scroller.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
+ _fonts_scroller.add(_FontsList);
+ _fonts_scroller.set_hexpand();
+ _fonts_scroller.show();
+ _header_box.set_column_spacing(MARGIN_SPACE);
+ _header_box.set_row_spacing(MARGIN_SPACE);
+ _header_box.attach(_fonts_scroller, 0, 0, 1, 3);
+ _header_box.attach(*Gtk::make_managed<Gtk::Label>(), 1, 0);
+ _header_box.attach(_add, 1, 1);
+ _header_box.attach(_remove, 1, 2);
+ _header_box.set_margin_bottom(MARGIN_SPACE);
+ _header_box.set_margin_end(MARGIN_SPACE);
+ _add.set_valign(Gtk::ALIGN_CENTER);
+ _remove.set_valign(Gtk::ALIGN_CENTER);
+ _remove.set_halign(Gtk::ALIGN_CENTER);
+ _add.set_image_from_icon_name("list-add", Gtk::ICON_SIZE_BUTTON);
+ _remove.set_image_from_icon_name("list-remove", Gtk::ICON_SIZE_BUTTON);
+
+ global_vbox.pack_start(_header_box, false, false);
+
+ _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*) _("Horizontal advance X:"), _("Default glyph width for horizontal text"), SPAttr::HORIZ_ADV_X);
+ _horiz_origin_x_spin = new AttrSpin( this, (gchar*) _("Horizontal origin X:"), _("Default X-coordinate of the origin of a glyph (for horizontal text)"), SPAttr::HORIZ_ORIGIN_X);
+ _horiz_origin_y_spin = new AttrSpin( this, (gchar*) _("Horizontal origin Y:"), _("Default Y-coordinate of the origin of a glyph (for horizontal text)"), SPAttr::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"), SPAttr::FONT_FAMILY);
+ _units_per_em_spin = new AttrSpin( this, (gchar*) _("Em-size:"), _("Display units per <italic>em</italic> (nominally width of 'M' character)"), SPAttr::UNITS_PER_EM);
+ _ascent_spin = new AttrSpin( this, (gchar*) _("Ascender:"), _("Amount of space taken up by ascenders like the tall line on the letter 'h'"), SPAttr::ASCENT);
+ _cap_height_spin = new AttrSpin( this, (gchar*) _("Caps height:"), _("The height of a capital letter above the baseline like the letter 'H' or 'I'"), SPAttr::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'"), SPAttr::X_HEIGHT);
+ _descent_spin = new AttrSpin( this, (gchar*) _("Descender:"), _("Amount of space taken up by descenders like the tail on the letter 'g'"), SPAttr::DESCENT);
+
+ //_descent_spin->set_range(-4096,0);
+ _font_label->set_use_markup();
+ _font_face_label->set_use_markup();
+
+ _grid.set_column_spacing(MARGIN_SPACE);
+ _grid.set_row_spacing(MARGIN_SPACE);
+ _grid.set_margin_start(MARGIN_SPACE);
+ _grid.set_margin_bottom(MARGIN_SPACE);
+ const int indent = 2 * MARGIN_SPACE;
+ int row = 0;
+
+ _grid.attach(*_font_label, 0, row++, 2);
+ SvgFontsDialog::AttrSpin* font[] = {_horiz_adv_x_spin, _horiz_origin_x_spin, _horiz_origin_y_spin};
+ for (auto spin : font) {
+ spin->get_label()->set_margin_start(indent);
+ _grid.attach(*spin->get_label(), 0, row);
+ _grid.attach(*spin->getSpin(), 1, row++);
+ }
+
+ _grid.attach(*_font_face_label, 0, row++, 2);
+ _familyname_entry->get_label()->set_margin_start(indent);
+ _familyname_entry->get_entry()->set_margin_end(MARGIN_SPACE);
+ _grid.attach(*_familyname_entry->get_label(), 0, row);
+ _grid.attach(*_familyname_entry->get_entry(), 1, row++, 2);
+
+ SvgFontsDialog::AttrSpin* face[] = {_units_per_em_spin, _ascent_spin, _cap_height_spin, _x_height_spin, _descent_spin};
+ for (auto spin : face) {
+ spin->get_label()->set_margin_start(indent);
+ _grid.attach(*spin->get_label(), 0, row);
+ _grid.attach(*spin->getSpin(), 1, row++);
+ }
+ auto setup = Gtk::make_managed<Gtk::Button>(_("Set up canvas"));
+ _grid.attach(*setup, 0, row++, 2);
+ setup->set_halign(Gtk::ALIGN_START);
+ setup->signal_clicked().connect([=](){
+ // set up typography canvas
+ set_up_typography_canvas(
+ getDocument(),
+ _units_per_em_spin->getSpin()->get_value(),
+ _ascent_spin->getSpin()->get_value(),
+ _cap_height_spin->getSpin()->get_value(),
+ _x_height_spin->getSpin()->get_value(),
+ _descent_spin->getSpin()->get_value()
+ );
+ });
+
+ global_vbox.set_border_width(2);
+ global_vbox.pack_start(_grid, false, true);
+
+/* global_vbox->add(*AttrCombo((gchar*) _("Style:"), SPAttr::FONT_STYLE));
+ global_vbox->add(*AttrCombo((gchar*) _("Variant:"), SPAttr::FONT_VARIANT));
+ global_vbox->add(*AttrCombo((gchar*) _("Weight:"), SPAttr::FONT_WEIGHT));
+*/
+ return &global_vbox;
+}
+
+void SvgFontsDialog::set_glyph_row(const Gtk::TreeRow& row, SPGlyph& glyph) {
+ auto unicode_name = create_unicode_name(glyph.unicode, 3);
+ row[_GlyphsListColumns.glyph_node] = &glyph;
+ row[_GlyphsListColumns.glyph_name] = glyph.glyph_name;
+ row[_GlyphsListColumns.unicode] = glyph.unicode;
+ row[_GlyphsListColumns.UplusCode] = unicode_name;
+ row[_GlyphsListColumns.advance] = glyph.horiz_adv_x;
+ row[_GlyphsListColumns.name_markup] = "<small>" + Glib::Markup::escape_text(get_glyph_synthetic_name(glyph)) + "</small>";
+}
+
+void
+SvgFontsDialog::populate_glyphs_box()
+{
+ if (!_GlyphsListStore) return;
+
+ _GlyphsListStore->freeze_notify();
+
+ // try to keep selected glyph
+ Gtk::TreeModel::Path selected_item;
+ if (auto selected = get_selected_glyph_iter()) {
+ selected_item = _GlyphsListStore->get_path(selected);
+ }
+ _GlyphsListStore->clear();
+
+ SPFont* spfont = get_selected_spfont();
+ _glyphs_observer.set(spfont);
+
+ if (spfont) {
+ for (auto& node: spfont->children) {
+ if (is<SPGlyph>(&node)) {
+ auto& glyph = static_cast<SPGlyph&>(node);
+ Gtk::TreeModel::Row row = *_GlyphsListStore->append();
+ set_glyph_row(row, glyph);
+ }
+ }
+
+ if (!selected_item.empty()) {
+ if (auto selection = _GlyphsList.get_selection()) {
+ selection->select(selected_item);
+ _GlyphsList.scroll_to_row(selected_item);
+ }
+ _glyphs_grid.select_path(selected_item);
+ }
+ }
+
+ _GlyphsListStore->thaw_notify();
+}
+
+void
+SvgFontsDialog::populate_kerning_pairs_box()
+{
+ if (!_KerningPairsListStore) return;
+
+ _KerningPairsListStore->clear();
+
+ if (SPFont* spfont = get_selected_spfont()) {
+ for (auto& node: spfont->children) {
+ if (is<SPHkern>(&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);
+ }
+ }
+ }
+}
+
+// update existing glyph in the tree model
+void SvgFontsDialog::update_glyph(SPGlyph* glyph) {
+ if (_update.pending() || !glyph) return;
+
+ _GlyphsListStore->foreach_iter([&](const Gtk::TreeModel::iterator& it) {
+ if (it->get_value(_GlyphsListColumns.glyph_node) == glyph) {
+ const Gtk::TreeRow& row = *it;
+ set_glyph_row(row, *glyph);
+ return true; // stop
+ }
+ return false; // continue
+ });
+}
+
+void SvgFontsDialog::update_glyphs(SPGlyph* changed_glyph) {
+ if (_update.pending()) return;
+
+ SPFont* font = get_selected_spfont();
+ if (!font) return;
+
+ if (changed_glyph) {
+ update_glyph(changed_glyph);
+ }
+ else {
+ populate_glyphs_box();
+ }
+
+ populate_kerning_pairs_box();
+ refresh_svgfont();
+}
+
+void SvgFontsDialog::refresh_svgfont() {
+ if (auto font = get_selected_svgfont()) {
+ font->refresh();
+ }
+ _font_da.redraw();
+}
+
+void SvgFontsDialog::add_glyph(){
+ auto document = getDocument();
+ if (!document) return;
+ auto font = get_selected_spfont();
+ if (!font) return;
+
+ auto glyphs = _GlyphsListStore->children();
+ // initialize "unicode" field; if there are glyphs look for the last one and take next unicode
+ gunichar unicode = ' ';
+ if (!glyphs.empty()) {
+ const auto& last = glyphs[glyphs.size() - 1];
+ if (SPGlyph* last_glyph = last[_GlyphsListColumns.glyph_node]) {
+ const Glib::ustring& code = last_glyph->unicode;
+ if (!code.empty()) {
+ auto value = code[0];
+ // skip control chars 7f-9f
+ if (value == 0x7e) value = 0x9f;
+ // wrap around
+ if (value == 0x10ffff) value = 0x1f;
+ unicode = value + 1;
+ }
+ }
+ }
+ auto str = Glib::ustring(1, unicode);
+
+ // empty name to begin with
+ SPGlyph* glyph = font->create_new_glyph("", str.c_str());
+ DocumentUndo::done(document, _("Add glyph"), "");
+
+ // select newly added glyph
+ set_selected_glyph(glyph);
+}
+
+double get_font_units_per_em(const SPFont* font) {
+ double units_per_em = 0.0;
+ if (font) {
+ for (auto& obj: font->children) {
+ if (is<SPFontFace>(&obj)){
+ //XML Tree being directly used here while it shouldn't be.
+ units_per_em = obj.getRepr()->getAttributeDouble("units-per-em", units_per_em);
+ break;
+ }
+ }
+ }
+ return units_per_em;
+}
+
+Geom::PathVector flip_coordinate_system(Geom::PathVector pathv, const SPFont* font, double units_per_em) {
+ if (!font) return pathv;
+
+ if (units_per_em <= 0) {
+ g_warning("Units per em not defined, path will be misplaced.");
+ }
+
+ double baseline_offset = units_per_em - font->horiz_origin_y;
+ // This matrix flips y-axis and places the origin at baseline
+ Geom::Affine m(1, 0, 0, -1, 0, baseline_offset);
+ return pathv * m;
+}
+
+void SvgFontsDialog::set_glyph_description_from_selected_path() {
+ auto font = get_selected_spfont();
+ if (!font) return;
+
+ auto selection = getSelection();
+ if (!selection)
+ return;
+
+ Inkscape::MessageStack *msgStack = getDesktop()->getMessageStack();
+ if (selection->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 = selection->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"));
+
+ auto units_per_em = get_font_units_per_em(font);
+ //XML Tree being directly used here while it shouldn't be.
+ glyph->setAttribute("d", sp_svg_write_path(flip_coordinate_system(pathv, font, units_per_em)));
+ DocumentUndo::done(getDocument(), _("Set glyph curves"), "");
+
+ update_glyphs(glyph);
+}
+
+void SvgFontsDialog::missing_glyph_description_from_selected_path(){
+ auto font = get_selected_spfont();
+ if (!font) return;
+
+ auto selection = getSelection();
+ if (!selection)
+ return;
+
+ Inkscape::MessageStack *msgStack = getDesktop()->getMessageStack();
+ if (selection->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 = selection->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 the user?
+
+ Geom::PathVector pathv = sp_svg_read_pathv(node->attribute("d"));
+
+ auto units_per_em = get_font_units_per_em(font);
+ for (auto& obj: font->children) {
+ if (is<SPMissingGlyph>(&obj)){
+ //XML Tree being directly used here while it shouldn't be.
+ obj.setAttribute("d", sp_svg_write_path(flip_coordinate_system(pathv, font, units_per_em)));
+ DocumentUndo::done(getDocument(), _("Set glyph curves"), "");
+ }
+ }
+
+ refresh_svgfont();
+}
+
+void SvgFontsDialog::reset_missing_glyph_description(){
+ for (auto& obj: get_selected_spfont()->children) {
+ if (is<SPMissingGlyph>(&obj)){
+ //XML Tree being directly used here while it shouldn't be.
+ obj.setAttribute("d", "M0,0h1000v1024h-1000z");
+ DocumentUndo::done(getDocument(), _("Reset missing-glyph"), "");
+ }
+ }
+ refresh_svgfont();
+}
+
+void change_glyph_attribute(SPDesktop* desktop, SPGlyph& glyph, std::function<void ()> change) {
+ assert(glyph.parent);
+
+ auto name = get_glyph_full_name(glyph);
+ auto font_label = glyph.parent->label();
+ auto layer = get_layer_for_glyph(desktop, font_label, name);
+
+ change();
+
+ if (!layer) return;
+
+ name = get_glyph_full_name(glyph);
+ font_label = glyph.parent->label();
+ rename_glyph_layer(desktop, layer, font_label, name);
+}
+
+void SvgFontsDialog::glyph_name_edit(const Glib::ustring&, const Glib::ustring& str){
+ SPGlyph* glyph = get_selected_glyph();
+ if (!glyph) return;
+
+ if (glyph->glyph_name == str) return; // no change
+
+ change_glyph_attribute(getDesktop(), *glyph, [=](){
+ //XML Tree being directly used here while it shouldn't be.
+ glyph->setAttribute("glyph-name", str);
+
+ DocumentUndo::done(getDocument(), _("Edit glyph name"), "");
+ update_glyphs(glyph);
+ });
+}
+
+void SvgFontsDialog::glyph_unicode_edit(const Glib::ustring&, const Glib::ustring& str){
+ SPGlyph* glyph = get_selected_glyph();
+ if (!glyph) return;
+
+ if (glyph->unicode == str) return; // no change
+
+ change_glyph_attribute(getDesktop(), *glyph, [=]() {
+ // XML Tree being directly used here while it shouldn't be.
+ glyph->setAttribute("unicode", str);
+
+ DocumentUndo::done(getDocument(), _("Set glyph unicode"), "");
+ update_glyphs(glyph);
+ });
+}
+
+void SvgFontsDialog::glyph_advance_edit(const Glib::ustring&, const Glib::ustring& str){
+ SPGlyph* glyph = get_selected_glyph();
+ if (!glyph) return;
+
+ if (auto val = glyph->getAttribute("horiz-adv-x")) {
+ if (str == val) return; // no change
+ }
+
+ //XML Tree being directly used here while it shouldn't be.
+ std::istringstream is(str.raw());
+ double value;
+ // Check if input valid
+ if ((is >> value)) {
+ glyph->setAttribute("horiz-adv-x", str);
+ DocumentUndo::done(getDocument(), _("Set glyph advance"), "");
+
+ update_glyphs(glyph);
+ } else {
+ std::cerr << "SvgFontDialog::glyph_advance_edit: Error in input: " << str.raw() << 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());
+ DocumentUndo::done(getDocument(), _("Remove font"), "");
+
+ update_fonts(false);
+}
+
+void SvgFontsDialog::remove_selected_glyph(){
+ SPGlyph* glyph = get_selected_glyph();
+ if (!glyph) return;
+
+ //XML Tree being directly used here while it shouldn't be.
+ sp_repr_unparent(glyph->getRepr());
+ DocumentUndo::done(getDocument(), _("Remove glyph"), "");
+
+ update_glyphs();
+}
+
+void SvgFontsDialog::remove_selected_kerning_pair() {
+ SPGlyphKerning* pair = get_selected_kerning_pair();
+ if (!pair) return;
+
+ //XML Tree being directly used here while it shouldn't be.
+ sp_repr_unparent(pair->getRepr());
+ DocumentUndo::done(getDocument(), _("Remove kerning pair"), "");
+
+ update_glyphs();
+}
+
+Inkscape::XML::Node* create_path_from_glyph(const SPGlyph& glyph) {
+ Geom::PathVector pathv = sp_svg_read_pathv(glyph.getAttribute("d"));
+ auto path = glyph.document->getReprDoc()->createElement("svg:path");
+ // auto path = new SPPath();
+ auto font = cast<SPFont>(glyph.parent);
+ auto units_per_em = get_font_units_per_em(font);
+ path->setAttribute("d", sp_svg_write_path(flip_coordinate_system(pathv, font, units_per_em)));
+ return path;
+}
+
+// switch to a glyph layer (and create this dedicated layer if necessary)
+void SvgFontsDialog::edit_glyph(SPGlyph* glyph) {
+ if (!glyph || !glyph->parent) return;
+
+ auto desktop = getDesktop();
+ if (!desktop) return;
+ auto document = getDocument();
+ if (!document) return;
+
+ // glyph's full name to match layer name
+ auto name = get_glyph_full_name(*glyph);
+ if (name.empty()) return;
+ // font's name to match parent layer name
+ auto font_label = get_font_label(cast<SPFont>(glyph->parent));
+ if (font_label.empty()) return;
+
+ auto layer = get_or_create_layer_for_glyph(desktop, font_label, name);
+ if (!layer) return;
+
+ // is layer empty?
+ if (!layer->hasChildren()) {
+ // since layer is empty try to initialize it by copying font glyph into it
+ auto path = create_path_from_glyph(*glyph);
+ if (path) {
+ // layer->attach(path, nullptr);
+ layer->addChild(path);
+ }
+ }
+
+ auto& layers = desktop->layerManager();
+ // set layer as "solo" - only one visible and unlocked
+ if (layers.isLayer(layer) && layer != layers.currentRoot()) {
+ layers.setCurrentLayer(layer, true);
+ layers.toggleLayerSolo(layer, true);
+ layers.toggleLockOtherLayers(layer, true);
+ DocumentUndo::done(document, _("Toggle layer solo"), "");
+ }
+}
+
+void SvgFontsDialog::set_glyphs_view_mode(bool list) {
+ if (list) {
+ _glyphs_icon_scroller.hide();
+ _GlyphsListScroller.show();
+ }
+ else {
+ _GlyphsListScroller.hide();
+ _glyphs_icon_scroller.show();
+ }
+}
+
+Gtk::Box* SvgFontsDialog::glyphs_tab() {
+ _GlyphsList.signal_button_release_event().connect_notify(sigc::mem_fun(*this, &SvgFontsDialog::glyphs_list_button_release));
+ _glyphs_grid.signal_button_release_event().connect_notify([=](GdkEventButton* event){ glyphs_list_button_release(event); });
+ create_glyphs_popup_menu(_GlyphsList, sigc::mem_fun(*this, &SvgFontsDialog::remove_selected_glyph));
+
+ auto missing_glyph = Gtk::make_managed<Gtk::Expander>();
+ missing_glyph->set_label(_("Missing glyph"));
+ Gtk::Box* missing_glyph_hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4));
+ missing_glyph->add(*missing_glyph_hbox);
+ missing_glyph->set_valign(Gtk::ALIGN_CENTER);
+
+ missing_glyph_hbox->set_hexpand(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.set_margin_top(MARGIN_SPACE);
+ 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.set_margin_top(MARGIN_SPACE);
+ 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);
+
+ _GlyphsListScroller.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS);
+ _GlyphsListScroller.add(_GlyphsList);
+ fix_inner_scroll(&_GlyphsListScroller);
+ _GlyphsList.set_model(_GlyphsListStore);
+ _GlyphsList.set_enable_search(false);
+
+ _glyph_renderer = Gtk::manage(new SvgGlyphRenderer());
+ const int size = 20; // arbitrarily chosen to keep glyphs small but still legible
+ _glyph_renderer->set_font_size(size * 9 / 10);
+ _glyph_renderer->set_cell_size(size * 3 / 2, size);
+ _glyph_renderer->set_tree(&_GlyphsList);
+ _glyph_renderer->signal_clicked().connect([=](const GdkEvent*, const Glib::ustring& unicodes) {
+ // set preview: show clicked glyph only
+ _preview_entry.set_text(unicodes);
+ });
+ auto col_index = _GlyphsList.append_column(_("Glyph"), *_glyph_renderer) - 1;
+ if (auto column = _GlyphsList.get_column(col_index)) {
+ column->add_attribute(_glyph_renderer->property_glyph(), _GlyphsListColumns.unicode);
+ }
+ _GlyphsList.append_column_editable(_("Name"), _GlyphsListColumns.glyph_name);
+ _GlyphsList.append_column_editable(_("Characters"), _GlyphsListColumns.unicode);
+ _GlyphsList.append_column(_("Unicode"), _GlyphsListColumns.UplusCode);
+ _GlyphsList.append_column_numeric_editable(_("Advance"), _GlyphsListColumns.advance, "%.2f");
+ _GlyphsList.show();
+ _GlyphsList.signal_row_activated().connect([=](const Gtk::TreeModel::Path& path, Gtk::TreeViewColumn*) {
+ edit_glyph(get_selected_glyph());
+ });
+
+ Gtk::Box* hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4));
+ add_glyph_button.set_image_from_icon_name("list-add");
+ add_glyph_button.set_tooltip_text(_("Add new glyph"));
+ add_glyph_button.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::add_glyph));
+ remove_glyph_button.set_image_from_icon_name("list-remove");
+ remove_glyph_button.set_tooltip_text(_("Delete current glyph"));
+ remove_glyph_button.signal_clicked().connect([=](){ remove_selected_glyph(); });
+
+ glyph_from_path_button.set_label(_("Get curves"));
+ glyph_from_path_button.set_always_show_image();
+ glyph_from_path_button.set_image_from_icon_name("glyph-copy-from");
+ glyph_from_path_button.set_tooltip_text(_("Get curves from selection to replace current glyph"));
+ glyph_from_path_button.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::set_glyph_description_from_selected_path));
+
+ auto edit = Gtk::make_managed<Gtk::Button>();
+ edit->set_label(_("Edit"));
+ edit->set_always_show_image();
+ edit->set_image_from_icon_name("edit");
+ edit->set_tooltip_text(_("Switch to a layer with the same name as current glyph"));
+ edit->signal_clicked().connect([=]() {
+ edit_glyph(get_selected_glyph());
+ });
+
+ hb->pack_start(glyph_from_path_button, false, false);
+ hb->pack_start(*edit, false, false);
+ hb->pack_end(remove_glyph_button, false, false);
+ hb->pack_end(add_glyph_button, false, false);
+
+ _glyph_cell_renderer = Gtk::manage(new SvgGlyphRenderer());
+ _glyph_cell_renderer->set_tree(&_glyphs_grid);
+ const int cell_width = 70;
+ const int cell_height = 50;
+ _glyph_cell_renderer->set_cell_size(cell_width, cell_height);
+ _glyph_cell_renderer->set_font_size(cell_height * 8 / 10); // font size: 80% of height
+ _glyphs_icon_scroller.add(_glyphs_grid);
+ _glyphs_icon_scroller.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ _glyphs_grid.set_name("GlyphsGrid");
+ _glyphs_grid.set_model(_GlyphsListStore);
+ _glyphs_grid.set_item_width(cell_width);
+ _glyphs_grid.set_selection_mode(Gtk::SELECTION_SINGLE);
+ _glyphs_grid.show_all_children();
+ _glyphs_grid.set_margin(0);
+ _glyphs_grid.set_item_padding(0);
+ _glyphs_grid.set_row_spacing(0);
+ _glyphs_grid.set_column_spacing(0);
+ _glyphs_grid.set_columns(-1);
+ _glyphs_grid.set_markup_column(_GlyphsListColumns.name_markup);
+ _glyphs_grid.pack_start(*_glyph_cell_renderer);
+ _glyphs_grid.add_attribute(*_glyph_cell_renderer, "glyph", _GlyphsListColumns.unicode);
+ _glyphs_grid.show();
+ _glyphs_grid.signal_item_activated().connect([=](const Gtk::TreeModel::Path& path) {
+ edit_glyph(get_selected_glyph());
+ });
+
+ // keep selection in sync between the two views: list and grid
+ _glyphs_grid.signal_selection_changed().connect([=]() {
+ if (_glyphs_icon_scroller.get_visible()) {
+ if (auto selected = get_selected_glyph_iter()) {
+ if (auto selection = _GlyphsList.get_selection()) {
+ selection->select(selected);
+ }
+ }
+ }
+ });
+ if (auto selection = _GlyphsList.get_selection()) {
+ selection->signal_changed().connect([=]() {
+ if (_GlyphsListScroller.get_visible()) {
+ if (auto selected = get_selected_glyph_iter()) {
+ auto selected_item = _GlyphsListStore->get_path(selected);
+ _glyphs_grid.select_path(selected_item);
+ }
+ }
+ });
+ }
+
+ // display mode switching buttons
+ auto hbox = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL, 4);
+ Gtk::RadioButtonGroup group;
+ auto list = Gtk::make_managed<Gtk::RadioButton>(group);
+ list->set_mode(false);
+ list->set_image_from_icon_name("glyph-list");
+ list->set_tooltip_text(_("Glyph list view"));
+ list->set_valign(Gtk::ALIGN_START);
+ list->signal_toggled().connect([=]() { set_glyphs_view_mode(true); });
+ auto grid = Gtk::make_managed<Gtk::RadioButton>(group);
+ grid->set_mode(false);
+ grid->set_image_from_icon_name("glyph-grid");
+ grid->set_tooltip_text(_("Glyph grid view"));
+ grid->set_valign(Gtk::ALIGN_START);
+ grid->signal_toggled().connect([=]() { set_glyphs_view_mode(false); });
+ hbox->pack_start(*missing_glyph);
+ hbox->pack_end(*grid, false, false);
+ hbox->pack_end(*list, false, false);
+
+ glyphs_vbox.pack_start(*hb, false, false);
+ glyphs_vbox.pack_start(_GlyphsListScroller, true, true);
+ glyphs_vbox.pack_start(_glyphs_icon_scroller, true, true);
+ glyphs_vbox.pack_start(*hbox, false,false);
+
+ _GlyphsListScroller.set_no_show_all();
+ _glyphs_icon_scroller.set_no_show_all();
+ (_show_glyph_list ? list : grid)->set_active();
+ set_glyphs_view_mode(_show_glyph_list);
+
+ for (auto&& col : _GlyphsList.get_columns()) {
+ col->set_resizable();
+ }
+
+ static_cast<Gtk::CellRendererText*>(_GlyphsList.get_column_cell_renderer(ColName))->signal_edited().connect(
+ sigc::mem_fun(*this, &SvgFontsDialog::glyph_name_edit));
+
+ static_cast<Gtk::CellRendererText*>(_GlyphsList.get_column_cell_renderer(ColString))->signal_edited().connect(
+ sigc::mem_fun(*this, &SvgFontsDialog::glyph_unicode_edit));
+
+ static_cast<Gtk::CellRendererText*>(_GlyphsList.get_column_cell_renderer(ColAdvance))->signal_edited().connect(
+ sigc::mem_fun(*this, &SvgFontsDialog::glyph_advance_edit));
+
+ _glyphs_observer.signal_changed().connect([=]() { 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 (is<SPHkern>(&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
+
+ Inkscape::XML::Document *xml_doc = getDocument()->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
+ kerning_pair = cast<SPHkern>(getDocument()->getObjectByRepr(repr));
+
+ // select newly added pair
+ if (auto selection = _KerningPairsList.get_selection()) {
+ _KerningPairsListStore->foreach_iter([=](const Gtk::TreeModel::iterator& it) {
+ if (it->get_value(_KerningPairsListColumns.spnode) == kerning_pair) {
+ selection->select(it);
+ return true; // stop
+ }
+ return false; // continue
+ });
+ }
+
+ DocumentUndo::done(getDocument(), _("Add kerning pair"), "");
+}
+
+Gtk::Box* 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::Box* kerning_selector = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
+ kerning_selector->pack_start(*Gtk::manage(new Gtk::Label(_("Select glyphs:"))), false, false);
+ kerning_selector->pack_start(first_glyph, false, false, MARGIN_SPACE / 2);
+ kerning_selector->pack_start(second_glyph, false, false, MARGIN_SPACE / 2);
+ kerning_selector->pack_start(add_kernpair_button, false, false, MARGIN_SPACE / 2);
+ 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);
+ _KerningPairsList.set_model(_KerningPairsListStore);
+ _KerningPairsList.append_column(_("First glyph"), _KerningPairsListColumns.first_glyph);
+ _KerningPairsList.append_column(_("Second glyph"), _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::Box* kerning_amount_hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 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(-1, 150 + 20);
+ _font_da.set_size(-1, 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 1000 units
+ repr->setAttribute("horiz-adv-x", "1000");
+
+ // Append the new font node to defs
+ defs->getRepr()->appendChild(repr);
+
+ // add some default values
+ Inkscape::XML::Node *fontface;
+ fontface = xml_doc->createElement("svg:font-face");
+ fontface->setAttribute("units-per-em", "1000");
+ fontface->setAttribute("ascent", "750");
+ fontface->setAttribute("cap-height", "600");
+ fontface->setAttribute("x-height", "400");
+ fontface->setAttribute("descent", "200");
+ repr->appendChild(fontface);
+
+ //create a missing glyph
+ Inkscape::XML::Node *mg;
+ mg = xml_doc->createElement("svg:missing-glyph");
+ mg->setAttribute("d", "M0,0h1000v1000h-1000z");
+ repr->appendChild(mg);
+
+ // get corresponding object
+ auto f = cast<SPFont>( document->getObjectByRepr(repr) );
+
+ g_assert(f != nullptr);
+ 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 (is<SPFontFace>(&obj)){
+ //XML Tree being directly used here while it shouldn't be.
+ obj.setAttribute("font-family", str);
+ }
+ }
+
+ DocumentUndo::done(font->document, _("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 (is<SPFontFace>(&obj)){
+ //XML Tree being directly used here while it shouldn't be.
+ obj.setAttribute("font-family", os2.str());
+ }
+ }
+
+ update_fonts(false);
+ on_font_selection_changed();
+
+ DocumentUndo::done(doc, _("Add font"), "");
+}
+
+SvgFontsDialog::SvgFontsDialog()
+ : DialogBase("/dialogs/svgfonts", "SVGFonts")
+ , global_vbox(Gtk::ORIENTATION_VERTICAL)
+ , glyphs_vbox(Gtk::ORIENTATION_VERTICAL)
+ , kerning_vbox(Gtk::ORIENTATION_VERTICAL)
+{
+ kerning_slider = Gtk::manage(new Gtk::Scale(Gtk::ORIENTATION_HORIZONTAL));
+
+ // kerning pairs store
+ _KerningPairsListStore = Gtk::ListStore::create(_KerningPairsListColumns);
+
+ // list of glyphs in a current font; this store is reused if there are multiple fonts
+ _GlyphsListStore = Gtk::ListStore::create(_GlyphsListColumns);
+
+ // List of SVGFonts declared in a document:
+ _model = Gtk::ListStore::create(_columns);
+ _FontsList.set_model(_model);
+ _FontsList.set_enable_search(false);
+ _FontsList.append_column_editable(_("_Fonts"), _columns.label);
+ _FontsList.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::on_font_selection_changed));
+ // connect to the cell renderer's edit signal; there's also model's row_changed, but it is less specific
+ if (auto renderer = dynamic_cast<Gtk::CellRendererText*>(_FontsList.get_column_cell_renderer(0))) {
+ // commit font names when user edits them
+ renderer->signal_edited().connect([=](const Glib::ustring& path, const Glib::ustring& new_name) {
+ if (auto it = _model->get_iter(path)) {
+ auto font = it->get_value(_columns.spfont);
+ font->setLabel(new_name.c_str());
+ Glib::ustring undokey = "svgfonts:fontName";
+ DocumentUndo::maybeDone(font->document, undokey.c_str(), _("Set SVG font name"), "");
+ }
+ });
+ }
+
+ _add.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::add_font));
+ _remove.signal_clicked().connect([=](){ remove_selected_font(); });
+
+ 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);
+ tabs->signal_switch_page().connect([=](Gtk::Widget*, guint page) {
+ if (page == 2) {
+ // update kerning glyph combos
+ if (SPFont* font = get_selected_spfont()) {
+ first_glyph.update(font);
+ second_glyph.update(font);
+ }
+ }
+ });
+
+ pack_start(*tabs, true, true, 0);
+
+ // Text Preview:
+ _preview_entry.signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::on_preview_text_changed));
+ pack_start((Gtk::Widget&) _font_da, false, false);
+ _preview_entry.set_text(_("Sample text"));
+ _font_da.set_text(_("Sample text"));
+
+ Gtk::Box* preview_entry_hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, MARGIN_SPACE));
+ 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);
+ preview_entry_hbox->set_margin_bottom(MARGIN_SPACE);
+ preview_entry_hbox->set_margin_start(MARGIN_SPACE);
+ preview_entry_hbox->set_margin_end(MARGIN_SPACE);
+
+ _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));
+
+ show_all();
+}
+
+void SvgFontsDialog::documentReplaced()
+{
+ _defs_observer_connection.disconnect();
+ if (auto document = getDocument()) {
+ _defs_observer.set(document->getDefs());
+ _defs_observer_connection = _defs_observer.signal_changed().connect([=](){ update_fonts(false); });
+ }
+ update_fonts(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/svg-fonts-dialog.h b/src/ui/dialog/svg-fonts-dialog.h
new file mode 100644
index 0000000..4095029
--- /dev/null
+++ b/src/ui/dialog/svg-fonts-dialog.h
@@ -0,0 +1,384 @@
+// 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 <2geom/pathvector.h>
+#include <gtkmm/box.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/drawingarea.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/iconview.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/treeview.h>
+
+#include "attributes.h"
+#include "helper/auto-connection.h"
+#include "ui/operation-blocker.h"
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/spinbutton.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*);
+};
+
+// cell text renderer for SVG font glyps (relying on Cairo "user font");
+// it can accept mouse clicks and report them via signal_clicked()
+class SvgGlyphRenderer : public Gtk::CellRenderer {
+public:
+ SvgGlyphRenderer() :
+ Glib::ObjectBase(typeid(CellRenderer)),
+ Gtk::CellRenderer(),
+ _property_active(*this, "active", true),
+ _property_activatable(*this, "activatable", true),
+ _property_glyph(*this, "glyph", "") {
+
+ property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE;
+ }
+
+ ~SvgGlyphRenderer() override = default;
+
+ Glib::PropertyProxy<Glib::ustring> property_glyph() { return _property_glyph.get_proxy(); }
+ Glib::PropertyProxy<bool> property_active() { return _property_active.get_proxy(); }
+ Glib::PropertyProxy<bool> property_activatable() { return _property_activatable.get_proxy(); }
+
+ sigc::signal<void (const GdkEvent*, const Glib::ustring&)>& signal_clicked() {
+ return _signal_clicked;
+ }
+
+ void set_svg_font(SvgFont* font) {
+ _font = font;
+ }
+
+ void set_font_size(int size) {
+ _font_size = size;
+ }
+
+ void set_tree(Gtk::Widget* tree) {
+ _tree = tree;
+ }
+
+ void set_cell_size(int w, int h) {
+ _width = w;
+ _height = h;
+ }
+
+ int get_width() const {
+ return _width;
+ }
+
+ 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;
+
+ 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;
+
+ void get_preferred_width_vfunc(Gtk::Widget& widget, int& min_w, int& nat_w) const override {
+ min_w = nat_w = _width;
+ }
+
+ void get_preferred_height_vfunc(Gtk::Widget& widget, int& min_h, int& nat_h) const override {
+ min_h = nat_h = _height;
+ }
+
+private:
+ int _width = 0;
+ int _height = 0;
+ int _font_size = 0;
+ Glib::Property<Glib::ustring> _property_glyph;
+ Glib::Property<bool> _property_active;
+ Glib::Property<bool> _property_activatable;
+ SvgFont* _font = nullptr;
+ Gtk::Widget* _tree = nullptr;
+ sigc::signal<void (const GdkEvent*, const Glib::ustring&)> _signal_clicked;
+};
+
+
+class SvgFontsDialog : public DialogBase
+{
+public:
+ SvgFontsDialog();
+ ~SvgFontsDialog() override = default;
+
+ void documentReplaced() override;
+
+ void update_fonts(bool document_replaced);
+ 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();
+
+ // Used for font-family
+ class AttrEntry
+ {
+ public:
+ AttrEntry(SvgFontsDialog* d, gchar* lbl, Glib::ustring tooltip, const SPAttr attr);
+ void set_text(const char*);
+ Gtk::Entry* get_entry() { return &entry; }
+ Gtk::Label* get_label() { return _label; }
+ private:
+ SvgFontsDialog* dialog;
+ void on_attr_changed();
+ Gtk::Entry entry;
+ SPAttr attr;
+ Gtk::Label* _label;
+ };
+
+ class AttrSpin
+ {
+ public:
+ AttrSpin(SvgFontsDialog* d, gchar* lbl, Glib::ustring tooltip, const SPAttr attr);
+ void set_value(double v);
+ void set_range(double low, double high);
+ Inkscape::UI::Widget::SpinButton* getSpin() { return &spin; }
+ Gtk::Label* get_label() { return _label; }
+ private:
+ SvgFontsDialog* dialog;
+ void on_attr_changed();
+ Inkscape::UI::Widget::SpinButton spin;
+ SPAttr attr;
+ Gtk::Label* _label;
+ };
+
+ OperationBlocker _update;
+
+private:
+ void update_glyphs(SPGlyph* changed_glyph = nullptr);
+ void update_glyph(SPGlyph* glyph);
+ void set_glyph_row(const Gtk::TreeRow& row, SPGlyph& glyph);
+ void refresh_svgfont();
+ 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 font_selected(SvgFont* svgfont, SPFont* spfont);
+
+ 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);
+
+ Gtk::TreeModel::iterator get_selected_glyph_iter();
+ void set_selected_glyph(SPGlyph* glyph);
+ void edit_glyph(SPGlyph* glyph);
+ void sort_glyphs(SPFont* font);
+
+ Inkscape::XML::SignalObserver _defs_observer; //in order to update fonts
+ Inkscape::XML::SignalObserver _glyphs_observer;
+ Inkscape::auto_connection _defs_observer_connection;
+
+ Gtk::Box* AttrCombo(gchar* lbl, const SPAttr attr);
+ Gtk::Box* 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::Box* kerning_tab();
+ Gtk::Box* glyphs_tab();
+ Gtk::Button _add;
+ Gtk::Button _remove;
+ Gtk::Button add_glyph_button;
+ Gtk::Button remove_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;
+ Gtk::ScrolledWindow _fonts_scroller;
+
+ class GlyphsColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ GlyphsColumns() {
+ add(glyph_node);
+ add(glyph_name);
+ add(unicode);
+ add(UplusCode);
+ add(advance);
+ add(name_markup);
+ }
+
+ Gtk::TreeModelColumn<SPGlyph*> glyph_node;
+ Gtk::TreeModelColumn<Glib::ustring> glyph_name;
+ Gtk::TreeModelColumn<Glib::ustring> unicode;
+ Gtk::TreeModelColumn<Glib::ustring> UplusCode;
+ Gtk::TreeModelColumn<double> advance;
+ Gtk::TreeModelColumn<Glib::ustring> name_markup;
+ };
+ enum GlyphColumnIndex { ColGlyph, ColName, ColString, ColUplusCode, ColAdvance };
+ GlyphsColumns _GlyphsListColumns;
+ Glib::RefPtr<Gtk::ListStore> _GlyphsListStore;
+ Gtk::TreeView _GlyphsList;
+ Gtk::ScrolledWindow _GlyphsListScroller;
+ Gtk::ScrolledWindow _glyphs_icon_scroller;
+ Gtk::IconView _glyphs_grid;
+ SvgGlyphRenderer* _glyph_renderer = nullptr;
+ SvgGlyphRenderer* _glyph_cell_renderer = nullptr;
+
+ 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::Grid _header_box;
+ Gtk::Grid _grid;
+ Gtk::Box global_vbox;
+ Gtk::Box glyphs_vbox;
+ Gtk::Box kerning_vbox;
+ Gtk::Entry _preview_entry;
+ bool _show_glyph_list = true;
+ void set_glyphs_view_mode(bool list);
+
+ 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::Box
+ {
+ public:
+ EntryWidget()
+ : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) {
+ 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..1474d99
--- /dev/null
+++ b/src/ui/dialog/svg-preview.cpp
@@ -0,0 +1,448 @@
+// 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 "ui/view/svg-view-widget.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+bool SVGPreview::setDocument(SPDocument *doc)
+{
+ if (viewer) {
+ viewer->setDocument(doc);
+ } else {
+ viewer = std::make_unique<Inkscape::UI::View::SVGViewWidget>(doc);
+ pack_start(*viewer, true, true);
+ }
+
+ document.reset(doc);
+
+ show_all();
+
+ return true;
+}
+
+bool SVGPreview::setFileName(Glib::ustring const &theFileName)
+{
+ auto fileName = Glib::filename_to_utf8(theFileName);
+
+ /**
+ * 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 const &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.raw() << 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);
+ if( !input ) {
+ std::cerr << "SVGPreview::showImage: Failed to open file: " << theFileName.raw() << std::endl;
+ } else {
+
+ Glib::ustring 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();
+ }
+
+ }
+ }
+ }
+
+ if (height.empty() || width.empty()) {
+ width = std::to_string(imgWidth);
+ height = std::to_string(imgHeight);
+ }
+
+ // 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 const &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()
+ : Gtk::Box(Gtk::ORIENTATION_VERTICAL)
+ , showingNoPreview(false)
+{
+ set_size_request(200, 300);
+}
+
+SVGPreview::~SVGPreview()
+{
+ // Ensure correct destruction order: viewer before document.
+ viewer.reset();
+ document.reset();
+}
+
+} // 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..0df897f
--- /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"
+#include "document.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::Box
+{
+public:
+
+ SVGPreview();
+
+ ~SVGPreview() override;
+
+ bool setDocument(SPDocument *doc);
+
+ bool setFileName(Glib::ustring const &fileName);
+
+ bool setFromMem(char const *xmlBuffer);
+
+ bool set(Glib::ustring const &fileName, int dialogType);
+
+ bool setURI(URI &uri);
+
+ /**
+ * Show image embedded in SVG
+ */
+ void showImage(Glib::ustring const &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
+ */
+ std::unique_ptr<SPDocument> document;
+
+ /**
+ * The sp_svg_view widget
+ */
+ std::unique_ptr<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..a059796
--- /dev/null
+++ b/src/ui/dialog/swatches.cpp
@@ -0,0 +1,447 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Jon A. Cruz
+ * John Bintz
+ * Abhishek Sharma
+ * PBS <pbs3141@gmail.com>
+ *
+ * Copyright (C) 2005 Jon A. Cruz
+ * Copyright (C) 2008 John Bintz
+ * Copyright (C) 2022 PBS
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "swatches.h"
+
+#include <algorithm>
+#include <glibmm/i18n.h>
+
+#include "document.h"
+#include "object/sp-defs.h"
+#include "style.h"
+#include "desktop-style.h"
+#include "object/sp-gradient-reference.h"
+
+#include "inkscape-preferences.h"
+#include "widgets/paintdef.h"
+#include "ui/widget/color-palette.h"
+#include "ui/dialog/global-palettes.h"
+#include "ui/dialog/color-item.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/*
+ * Lifecycle
+ */
+
+SwatchesPanel::SwatchesPanel(char const *prefsPath)
+ : DialogBase(prefsPath, "Swatches")
+{
+ _palette = Gtk::make_managed<Inkscape::UI::Widget::ColorPalette>();
+ pack_start(*_palette);
+ update_palettes();
+
+ bool embedded = _prefs_path != "/dialogs/swatches";
+ _palette->set_compact(embedded);
+
+ Inkscape::Preferences* prefs = Inkscape::Preferences::get();
+
+ index = name_to_index(prefs->getString(_prefs_path + "/palette"));
+
+ // restore palette settings
+ _palette->set_tile_size(prefs->getInt(_prefs_path + "/tile_size", 16));
+ _palette->set_aspect(prefs->getDoubleLimited(_prefs_path + "/tile_aspect", 0.0, -2, 2));
+ _palette->set_tile_border(prefs->getInt(_prefs_path + "/tile_border", 1));
+ _palette->set_rows(prefs->getInt(_prefs_path + "/rows", 1));
+ _palette->enable_stretch(prefs->getBool(_prefs_path + "/tile_stretch", false));
+ _palette->set_large_pinned_panel(embedded && prefs->getBool(_prefs_path + "/enlarge_pinned", true));
+ _palette->enable_labels(!embedded && prefs->getBool(_prefs_path + "/show_labels", true));
+
+ // save settings when they change
+ _palette->get_settings_changed_signal().connect([=] {
+ prefs->setInt(_prefs_path + "/tile_size", _palette->get_tile_size());
+ prefs->setDouble(_prefs_path + "/tile_aspect", _palette->get_aspect());
+ prefs->setInt(_prefs_path + "/tile_border", _palette->get_tile_border());
+ prefs->setInt(_prefs_path + "/rows", _palette->get_rows());
+ prefs->setBool(_prefs_path + "/tile_stretch", _palette->is_stretch_enabled());
+ prefs->setBool(_prefs_path + "/enlarge_pinned", _palette->is_pinned_panel_large());
+ prefs->setBool(_prefs_path + "/show_labels", !embedded && _palette->are_labels_enabled());
+ });
+
+ // Respond to requests from the palette widget to change palettes.
+ _palette->get_palette_selected_signal().connect([this] (Glib::ustring name) {
+ Preferences::get()->setString(_prefs_path + "/palette", name);
+ set_index(name_to_index(name));
+ });
+
+ // Watch for pinned palette options.
+ _pinned_observer = prefs->createObserver(_prefs_path + "/pinned/", [this]() {
+ rebuild();
+ });
+
+ rebuild();
+}
+
+SwatchesPanel::~SwatchesPanel()
+{
+ untrack_gradients();
+}
+
+/*
+ * Activation
+ */
+
+// Note: The "Auto" palette shows the list of gradients that are swatches. When this palette is
+// shown (and we have a document), we therefore need to track both addition/removal of gradients
+// and changes to the isSwatch() status to keep the palette up-to-date.
+
+void SwatchesPanel::documentReplaced()
+{
+ if (getDocument()) {
+ if (index == PALETTE_AUTO) {
+ track_gradients();
+ }
+ } else {
+ untrack_gradients();
+ }
+
+ if (index == PALETTE_AUTO) {
+ rebuild();
+ }
+}
+
+void SwatchesPanel::desktopReplaced()
+{
+ documentReplaced();
+}
+
+void SwatchesPanel::set_index(PaletteIndex new_index)
+{
+ if (index == new_index) return;
+ index = new_index;
+
+ if (index == PALETTE_AUTO) {
+ if (getDocument()) {
+ track_gradients();
+ }
+ } else {
+ untrack_gradients();
+ }
+
+ rebuild();
+}
+
+void SwatchesPanel::track_gradients()
+{
+ auto doc = getDocument();
+
+ // Subscribe to the addition and removal of gradients.
+ conn_gradients.disconnect();
+ conn_gradients = doc->connectResourcesChanged("gradient", [this] {
+ gradients_changed = true;
+ queue_resize();
+ });
+
+ // Subscribe to child modifications of the defs section. We will use this to monitor
+ // each gradient for whether its isSwatch() status changes.
+ conn_defs.disconnect();
+ conn_defs = doc->getDefs()->connectModified([this] (SPObject*, unsigned flags) {
+ if (flags & SP_OBJECT_CHILD_MODIFIED_FLAG) {
+ defs_changed = true;
+ queue_resize();
+ }
+ });
+
+ gradients_changed = false;
+ defs_changed = false;
+ rebuild_isswatch();
+}
+
+void SwatchesPanel::untrack_gradients()
+{
+ conn_gradients.disconnect();
+ conn_defs.disconnect();
+ gradients_changed = false;
+ defs_changed = false;
+}
+
+/*
+ * Updating
+ */
+
+void SwatchesPanel::selectionChanged(Selection*)
+{
+ selection_changed = true;
+ queue_resize();
+}
+
+void SwatchesPanel::selectionModified(Selection*, guint flags)
+{
+ if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) {
+ selection_changed = true;
+ queue_resize();
+ }
+}
+
+// Document updates are handled asynchronously by setting a flag and queuing a resize. This results in
+// the following function being run at the last possible moment before the widget will be repainted.
+// This ensures that multiple document updates only result in a single UI update.
+void SwatchesPanel::on_size_allocate(Gtk::Allocation &alloc)
+{
+ if (gradients_changed) {
+ assert(index = PALETTE_AUTO);
+ // We are in the "Auto" palette, and a gradient was added or removed.
+ // The list of widgets has therefore changed, and must be completely rebuilt.
+ // We must also rebuild the tracking information for each gradient's isSwatch() status.
+ rebuild_isswatch();
+ rebuild();
+ } else if (defs_changed) {
+ assert(index = PALETTE_AUTO);
+ // We are in the "Auto" palette, and a gradient's isSwatch() status was possibly modified.
+ // Check if it has; if so, then the list of widgets has changed, and must be rebuilt.
+ if (update_isswatch()) {
+ rebuild();
+ }
+ }
+
+ if (selection_changed) {
+ update_fillstroke_indicators();
+ }
+
+ selection_changed = false;
+ gradients_changed = false;
+ defs_changed = false;
+
+ // Necessary to perform *after* the above widget modifications, so GTK can process the new layout.
+ DialogBase::on_size_allocate(alloc);
+}
+
+// TODO: The following two functions can made much nicer using C++20 ranges.
+
+void SwatchesPanel::rebuild_isswatch()
+{
+ auto grads = getDocument()->getResourceList("gradient");
+
+ isswatch.resize(grads.size());
+
+ for (int i = 0; i < grads.size(); i++) {
+ isswatch[i] = static_cast<SPGradient*>(grads[i])->isSwatch();
+ }
+}
+
+bool SwatchesPanel::update_isswatch()
+{
+ auto grads = getDocument()->getResourceList("gradient");
+
+ // Should be guaranteed because we catch all size changes and call rebuild_isswatch() instead.
+ assert(isswatch.size() == grads.size());
+
+ bool modified = false;
+
+ for (int i = 0; i < grads.size(); i++) {
+ if (isswatch[i] != static_cast<SPGradient*>(grads[i])->isSwatch()) {
+ isswatch[i].flip();
+ modified = true;
+ }
+ }
+
+ return modified;
+}
+
+static auto spcolor_to_rgb(SPColor const &color)
+{
+ float rgbf[3];
+ color.get_rgb_floatv(rgbf);
+
+ std::array<unsigned, 3> rgb;
+ for (int i = 0; i < 3; i++) {
+ rgb[i] = SP_COLOR_F_TO_U(rgbf[i]);
+ };
+
+ return rgb;
+}
+
+void SwatchesPanel::update_fillstroke_indicators()
+{
+ auto doc = getDocument();
+ auto style = SPStyle(doc);
+
+ // Get the current fill or stroke as a ColorKey.
+ auto current_color = [&, this] (bool fill) -> std::optional<ColorKey> {
+ switch (sp_desktop_query_style(getDesktop(), &style, fill ? QUERY_STYLE_PROPERTY_FILL : QUERY_STYLE_PROPERTY_STROKE))
+ {
+ case QUERY_STYLE_SINGLE:
+ case QUERY_STYLE_MULTIPLE_AVERAGED:
+ case QUERY_STYLE_MULTIPLE_SAME:
+ break;
+ default:
+ return {};
+ }
+
+ auto attr = style.getFillOrStroke(fill);
+ if (!attr->set) {
+ return {};
+ }
+
+ if (attr->isNone()) {
+ return std::monostate{};
+ } else if (attr->isColor()) {
+ return spcolor_to_rgb(attr->value.color);
+ } else if (attr->isPaintserver()) {
+ if (auto grad = cast<SPGradient>(fill ? style.getFillPaintServer() : style.getStrokePaintServer())) {
+ if (grad->isSwatch()) {
+ return grad;
+ } else if (grad->ref) {
+ if (auto ref = grad->ref->getObject(); ref && ref->isSwatch()) {
+ return ref;
+ }
+ }
+ }
+ }
+
+ return {};
+ };
+
+ for (auto w : current_fill) w->set_fill(false);
+ for (auto w : current_stroke) w->set_stroke(false);
+
+ current_fill.clear();
+ current_stroke.clear();
+
+ if (auto fill = current_color(true)) {
+ auto range = widgetmap.equal_range(*fill);
+ for (auto it = range.first; it != range.second; ++it) {
+ current_fill.emplace_back(it->second);
+ }
+ }
+
+ if (auto stroke = current_color(false)) {
+ auto range = widgetmap.equal_range(*stroke);
+ for (auto it = range.first; it != range.second; ++it) {
+ current_stroke.emplace_back(it->second);
+ }
+ }
+
+ for (auto w : current_fill) w->set_fill(true);
+ for (auto w : current_stroke) w->set_stroke(true);
+}
+
+SwatchesPanel::PaletteIndex SwatchesPanel::name_to_index(Glib::ustring const &name)
+{
+ auto &palettes = GlobalPalettes::get().palettes;
+ if (name == "Auto") {
+ return PALETTE_AUTO;
+ } else if (auto it = std::find_if(palettes.begin(), palettes.end(), [&] (PaletteFileData const &p) {return p.name == name;}); it != palettes.end()) {
+ return (PaletteIndex)(PALETTE_GLOBAL + std::distance(palettes.begin(), it));
+ } else {
+ return PALETTE_NONE;
+ }
+}
+
+Glib::ustring SwatchesPanel::index_to_name(PaletteIndex index)
+{
+ auto &palettes = GlobalPalettes::get().palettes;
+ if (index == PALETTE_AUTO) {
+ return "Auto";
+ } else if (auto n = index - PALETTE_GLOBAL; n >= 0 && n < palettes.size()) {
+ return palettes[n].name;
+ } else {
+ return "";
+ }
+}
+
+/**
+ * Process the list of available palettes and update the list in the _palette widget.
+ */
+void SwatchesPanel::update_palettes()
+{
+ std::vector<Inkscape::UI::Widget::ColorPalette::palette_t> palettes;
+ palettes.reserve(1 + GlobalPalettes::get().palettes.size());
+
+ // The first palette in the list is always the "Auto" palette. Although this
+ // will contain colors when selected, the preview we show for it is empty.
+ palettes.push_back({"Auto", {}});
+
+ // The remaining palettes in the list are the global palettes.
+ for (auto &p : GlobalPalettes::get().palettes) {
+ Inkscape::UI::Widget::ColorPalette::palette_t palette;
+ palette.name = p.name;
+ for (auto const &c : p.colors) {
+ auto [r, g, b] = c.rgb;
+ palette.colors.push_back({r / 255.0, g / 255.0, b / 255.0});
+ }
+ palettes.emplace_back(std::move(palette));
+ }
+
+ _palette->set_palettes(palettes);
+}
+
+/**
+ * Rebuild the list of color items shown by the palette.
+ */
+void SwatchesPanel::rebuild()
+{
+ std::vector<ColorItem*> palette;
+
+ // The pointers in widgetmap are to widgets owned by the ColorPalette. It is assumed it will not
+ // delete them unless we ask, via the call to set_colors() later in this function.
+ widgetmap.clear();
+ current_fill.clear();
+ current_stroke.clear();
+
+ // Add the "remove-color" color.
+ auto w = Gtk::make_managed<ColorItem>(PaintDef(), this);
+ w->set_pinned_pref(_prefs_path);
+ palette.emplace_back(w);
+ widgetmap.emplace(std::monostate{}, w);
+
+ if (index >= PALETTE_GLOBAL) {
+ auto &pal = GlobalPalettes::get().palettes[index - PALETTE_GLOBAL];
+ palette.reserve(palette.size() + pal.colors.size());
+ for (auto &c : pal.colors) {
+ auto w = Gtk::make_managed<ColorItem>(PaintDef(c.rgb, c.name), this);
+ w->set_pinned_pref(_prefs_path);
+ palette.emplace_back(w);
+ widgetmap.emplace(c.rgb, w);
+ }
+ } else if (index == PALETTE_AUTO && getDocument()) {
+ auto grads = getDocument()->getResourceList("gradient");
+ for (auto obj : grads) {
+ auto grad = static_cast<SPGradient*>(obj);
+ if (grad->isSwatch()) {
+ auto w = Gtk::make_managed<ColorItem>(grad, this);
+ palette.emplace_back(w);
+ widgetmap.emplace(grad, w);
+ // Rebuild if the gradient gets pinned or unpinned
+ w->signal_pinned().connect([=]() {
+ rebuild();
+ });
+ }
+ }
+ }
+
+ if (getDocument()) {
+ update_fillstroke_indicators();
+ }
+
+ _palette->set_colors(palette);
+ _palette->set_selected(index_to_name(index));
+}
+
+} //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..7cd8e50
--- /dev/null
+++ b/src/ui/dialog/swatches.h
@@ -0,0 +1,114 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Color swatches dialog
+ */
+/* Authors:
+ * Jon A. Cruz
+ * PBS <pbs3141@gmail.com>
+ *
+ * Copyright (C) 2022 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef UI_DIALOG_SWATCHES_H
+#define UI_DIALOG_SWATCHES_H
+
+#include <array>
+#include <vector>
+#include <boost/unordered_map.hpp>
+#include <boost/variant.hpp>
+#include <glibmm/ustring.h>
+
+#include "ui/dialog/dialog-base.h"
+
+namespace Inkscape {
+namespace UI {
+
+namespace Widget {
+class ColorPalette;
+}
+
+namespace Dialog {
+class ColorItem;
+
+/**
+ * A dialog 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 dialog;
+ * the "/embedded/swatches" is the horizontal color palette at the bottom
+ * of the window.
+ */
+class SwatchesPanel : public DialogBase
+{
+public:
+ SwatchesPanel(char const *prefsPath = "/dialogs/swatches");
+ ~SwatchesPanel() override;
+
+protected:
+ void documentReplaced() override;
+ void desktopReplaced() override;
+ void selectionChanged(Selection *selection) override;
+ void selectionModified(Selection *selection, guint flags) override;
+
+ void on_size_allocate(Gtk::Allocation&) override;
+
+private:
+ void update_palettes();
+ void rebuild();
+
+ Inkscape::UI::Widget::ColorPalette *_palette;
+
+ // Mapping between palette names and indexes.
+ enum PaletteIndex
+ {
+ PALETTE_NONE = -2,
+ PALETTE_AUTO = -1,
+ PALETTE_GLOBAL = 0,
+ };
+ PaletteIndex index;
+ static PaletteIndex name_to_index(Glib::ustring const&);
+ static Glib::ustring index_to_name(PaletteIndex);
+ void set_index(PaletteIndex new_index);
+
+ // Asynchronous update mechanism.
+ sigc::connection conn_gradients;
+ sigc::connection conn_defs;
+ bool gradients_changed = false;
+ bool defs_changed = false;
+ bool selection_changed = false;
+ void track_gradients();
+ void untrack_gradients();
+
+ // For each gradient, whether or not it is a swatch. Used to track when isSwatch() changes.
+ std::vector<bool> isswatch;
+ void rebuild_isswatch();
+ bool update_isswatch();
+
+ // A map from colors to their respective widgets. Used to quickly find the widgets corresponding
+ // to the current fill/stroke color, in order to update their fill/stroke indicators.
+ // TODO: Upgrade to boost::variant2 or std::variant when possible.
+ using ColorKey = boost::variant<std::monostate, std::array<unsigned, 3>, SPGradient*>;
+ boost::unordered_multimap<ColorKey, ColorItem*> widgetmap; // need boost for array hash
+ std::vector<ColorItem*> current_fill;
+ std::vector<ColorItem*> current_stroke;
+ void update_fillstroke_indicators();
+
+ Inkscape::PrefObserver _pinned_observer;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // UI_DIALOG_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..aa1e228
--- /dev/null
+++ b/src/ui/dialog/symbols.cpp
@@ -0,0 +1,1361 @@
+// 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.
+ */
+
+#include <2geom/point.h>
+#include <cairo.h>
+#include <cairomm/refptr.h>
+#include <cairomm/surface.h>
+#include <cassert>
+#include <cmath>
+#include <cstddef>
+#include <gdkmm/pixbuf.h>
+#include <gdkmm/rgba.h>
+#include <glibmm/main.h>
+#include <glibmm/priorities.h>
+#include <glibmm/refptr.h>
+#include <glibmm/ustring.h>
+#include <gtkmm/box.h>
+#include <gtkmm/cellrendererpixbuf.h>
+#include <gtkmm/cellrenderertext.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/iconview.h>
+#include <gtkmm/label.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/menubutton.h>
+#include <gtkmm/popover.h>
+#include <gtkmm/scale.h>
+#include <gtkmm/searchentry.h>
+#include <gtkmm/treeiter.h>
+#include <gtkmm/treemodel.h>
+#include <gtkmm/treemodelfilter.h>
+#include <gtkmm/treemodelsort.h>
+#include <gtkmm/treepath.h>
+#include <pangomm/layout.h>
+#include <string>
+#include <vector>
+#include "preferences.h"
+#include "ui/builder-utils.h"
+#include "ui/dialog/messages.h"
+#ifdef HAVE_CONFIG_H
+# include "config.h" // only include where actually required!
+#endif
+
+#include "symbols.h"
+
+#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 "document.h"
+#include "inkscape.h"
+#include "path-prefix.h"
+#include "selection.h"
+#include "display/cairo-utils.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"
+#include "ui/widget/scrollprotected.h"
+#include "xml/href-attribute-helper.h"
+
+#ifdef WITH_LIBVISIO
+ #include <libvisio/libvisio.h>
+ #include <librevenge-stream/librevenge-stream.h>
+
+ using librevenge::RVNGFileStream;
+ using librevenge::RVNGString;
+ using librevenge::RVNGStringVector;
+ using librevenge::RVNGPropertyList;
+ using librevenge::RVNGSVGDrawingGenerator;
+#endif
+
+
+namespace Inkscape {
+namespace UI {
+
+namespace Dialog {
+
+constexpr int SIZES = 51;
+int SYMBOL_ICON_SIZES[SIZES];
+
+struct SymbolSet {
+ std::vector<SPSymbol*> symbols;
+ SPDocument* document = nullptr;
+ Glib::ustring title;
+};
+
+SPDocument* load_symbol_set(std::string filename);
+void scan_all_symbol_sets(std::map<std::string, SymbolSet>& symbol_sets);
+
+// key: symbol set full file name
+// value: symbol set
+static std::map<std::string, SymbolSet> symbol_sets;
+
+struct SymbolColumns : public Gtk::TreeModel::ColumnRecord {
+ Gtk::TreeModelColumn<std::string> cache_key;
+ Gtk::TreeModelColumn<Glib::ustring> symbol_id;
+ Gtk::TreeModelColumn<Glib::ustring> symbol_title;
+ Gtk::TreeModelColumn<Glib::ustring> symbol_short_title;
+ Gtk::TreeModelColumn<Glib::ustring> symbol_search_title;
+ Gtk::TreeModelColumn<Cairo::RefPtr<Cairo::Surface>> symbol_image;
+ Gtk::TreeModelColumn<Geom::Point> doc_dimensions;
+ Gtk::TreeModelColumn<SPDocument*> symbol_document;
+
+ SymbolColumns() {
+ add(cache_key);
+ add(symbol_id);
+ add(symbol_title);
+ add(symbol_short_title);
+ add(symbol_search_title);
+ add(symbol_image);
+ add(doc_dimensions);
+ add(symbol_document);
+ }
+} const g_columns;
+
+static Cairo::RefPtr<Cairo::ImageSurface> g_dummy;
+
+struct SymbolSetsColumns : public Gtk::TreeModel::ColumnRecord {
+ Gtk::TreeModelColumn<Glib::ustring> set_id;
+ Gtk::TreeModelColumn<Glib::ustring> translated_title;
+ Gtk::TreeModelColumn<std::string> set_filename;
+ Gtk::TreeModelColumn<SPDocument*> set_document;
+ Gtk::TreeModelColumn<Cairo::RefPtr<Cairo::Surface>> set_image;
+
+ SymbolSetsColumns() {
+ add(set_id);
+ add(translated_title);
+ add(set_filename);
+ add(set_document);
+ add(set_image);
+ }
+} const g_set_columns;
+
+const Glib::ustring CURRENT_DOC_ID = "{?cur-doc?}";
+const Glib::ustring ALL_SETS_ID = "{?all-sets?}";
+const char *CURRENT_DOC = N_("Current document");
+const char *ALL_SETS = N_("All symbol sets");
+
+SymbolsDialog::SymbolsDialog(const char* prefsPath)
+ : DialogBase(prefsPath, "Symbols"),
+ _builder(create_builder("dialog-symbols.glade")),
+ _zoom(get_widget<Gtk::Scale>(_builder, "zoom")),
+ _symbols_popup(get_widget<Gtk::MenuButton>(_builder, "symbol-set-popup")),
+ _set_search(get_widget<Gtk::SearchEntry>(_builder, "set-search")),
+ _search(get_widget<Gtk::SearchEntry>(_builder, "search")),
+ _symbol_sets_view(get_widget<Gtk::IconView>(_builder, "symbol-sets")),
+ _cur_set_name(get_widget<Gtk::Label>(_builder, "cur-set")),
+ _store(Gtk::ListStore::create(g_columns)),
+ _image_cache(1000) // arbitrary limit for how many rendered symbols to keep around
+{
+ auto prefs = Inkscape::Preferences::get();
+ Glib::ustring path = prefsPath;
+ path += '/';
+
+ _symbols._filtered = Gtk::TreeModelFilter::create(_store);
+ _symbols._store = _store;
+
+ _symbol_sets = Gtk::ListStore::create(g_set_columns);
+ _sets._store = _symbol_sets;
+ _sets._filtered = Gtk::TreeModelFilter::create(_symbol_sets);
+ _sets._filtered->set_visible_func([=](const Gtk::TreeModel::const_iterator& it){
+ if (_set_search.get_text_length() == 0) return true;
+
+ Glib::ustring id = (*it)[g_set_columns.set_id];
+ if (id == CURRENT_DOC_ID || id == ALL_SETS_ID) return true;
+
+ auto text = _set_search.get_text().lowercase();
+ Glib::ustring title = (*it)[g_set_columns.translated_title];
+ return title.lowercase().find(text) != Glib::ustring::npos;
+ });
+ _sets._sorted = Gtk::TreeModelSort::create(_sets._filtered);
+ _sets._sorted->set_sort_func(g_set_columns.translated_title, [=](const Gtk::TreeModel::iterator& a, const Gtk::TreeModel::iterator& b){
+ Glib::ustring ida = (*a)[g_set_columns.set_id];
+ Glib::ustring idb = (*b)[g_set_columns.set_id];
+ // current doc and all docs up front
+ if (ida == idb) return 0;
+ if (ida == CURRENT_DOC_ID) return -1;
+ if (idb == CURRENT_DOC_ID) return 1;
+ if (ida == ALL_SETS_ID) return -1;
+ if (idb == ALL_SETS_ID) return 1;
+ Glib::ustring ttl_a = (*a)[g_set_columns.translated_title];
+ Glib::ustring ttl_b = (*b)[g_set_columns.translated_title];
+ return ttl_a.compare(ttl_b);
+ });
+ _symbol_sets_view.set_model(_sets._sorted);
+ _symbol_sets_view.set_text_column(g_set_columns.translated_title.index());
+ _symbol_sets_view.pack_start(_renderer2);
+ _symbol_sets_view.add_attribute(_renderer2, "surface", g_set_columns.set_image);
+
+ auto row = _symbol_sets->append();
+ (*row)[g_set_columns.set_id] = CURRENT_DOC_ID;
+ (*row)[g_set_columns.translated_title] = _(CURRENT_DOC);
+ row = _symbol_sets->append();
+ (*row)[g_set_columns.set_id] = ALL_SETS_ID;
+ (*row)[g_set_columns.translated_title] = _(ALL_SETS);
+
+ _set_search.signal_search_changed().connect([=](){
+ auto scoped(_update.block());
+ _sets.refilter();
+ });
+
+ auto select_set = [=](const Gtk::TreeModel::Path& set_path) {
+ if (!set_path.empty()) {
+ // drive selection
+ _symbol_sets_view.select_path(set_path);
+ }
+ else if (auto set = get_current_set()) {
+ // populate icon view
+ rebuild(*set);
+ _cur_set_name.set_text((**set)[g_set_columns.translated_title]);
+ update_tool_buttons();
+ Glib::ustring id = (**set)[g_set_columns.set_id];
+ prefs->setString(path + "current-set", id);
+ return true;
+ }
+ return false;
+ };
+
+// _symbol_sets_view.signal_item_activated().connect([=](const Gtk::TreeModel::Path& path){
+// select_set(path);
+// get_widget<Gtk::Popover>(_builder, "set-popover").hide();
+// });
+ _symbol_sets_view.signal_selection_changed().connect([=](){
+ if (select_set({})) {
+ get_widget<Gtk::Popover>(_builder, "set-popover").hide();
+ }
+ });
+
+ const double factor = std::pow(2.0, 1.0 / 12.0);
+ for (int i = 0; i < SIZES; ++i) {
+ SYMBOL_ICON_SIZES[i] = std::round(std::pow(factor, i) * 16);
+ }
+
+ preview_document = symbolsPreviewDoc(); /* Template to render symbols in */
+ key = SPItem::display_key_new(1);
+ renderDrawing.setRoot(preview_document->getRoot()->invoke_show(renderDrawing, key, SP_ITEM_SHOW_DISPLAY));
+
+ auto& main = get_widget<Gtk::Box>(_builder, "main-box");
+ pack_start(main, Gtk::PACK_EXPAND_WIDGET);
+
+ _builder->get_widget("tools", tools);
+
+ icon_view = &get_widget<Gtk::IconView>(_builder, "icon-view");
+ _symbols._filtered->set_visible_func([=](const Gtk::TreeModel::const_iterator& it){
+ if (_search.get_text_length() == 0) return true;
+
+ auto text = _search.get_text().lowercase();
+ Glib::ustring title = (*it)[g_columns.symbol_search_title];
+ return title.lowercase().find(text) != Glib::ustring::npos;
+ });
+ icon_view->set_model(_symbols._filtered);
+ icon_view->set_tooltip_column(g_columns.symbol_title.index());
+
+ _search.signal_search_changed().connect([=](){
+ int delay = _search.get_text_length() == 0 ? 0 : 300;
+ _idle_search = Glib::signal_timeout().connect([=](){
+ auto scoped(_update.block());
+ _symbols.refilter();
+ set_info();
+ return false; // disconnect
+ }, delay);
+ });
+
+ auto show_names = &get_widget<Gtk::CheckButton>(_builder, "show-names");
+ auto names = prefs->getBool(path + "show-names", true);
+ show_names->set_active(names);
+ if (names) {
+ icon_view->set_markup_column(g_columns.symbol_short_title);
+ }
+ show_names->signal_toggled().connect([=](){
+ bool show = show_names->get_active();
+ icon_view->set_markup_column(show ? g_columns.symbol_short_title.index() : -1);
+ prefs->setBool(path + "show-names", show);
+ });
+
+ 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);
+ gtk_connections.emplace_back(
+ icon_view->signal_drag_data_get().connect(sigc::mem_fun(*this, &SymbolsDialog::iconDragDataGet)));
+ gtk_connections.emplace_back(
+ icon_view->signal_selection_changed().connect(sigc::mem_fun(*this, &SymbolsDialog::iconChanged)));
+ gtk_connections.emplace_back(icon_view->signal_button_press_event().connect([=](GdkEventButton *ev) -> bool {
+ _last_mousedown = {ev->x, ev->y - icon_view->get_vadjustment()->get_value()};
+ return false;
+ }, false));
+
+ _builder->get_widget("scroller", scroller);
+
+ // here we fix scoller to allow pass the scroll to parent scroll when reach upper or lower limit
+ // this must be added to al scrolleing window in dialogs. We dont do auto because dialogs can be recreated
+ // in the dialog code so think is safer call inside
+ fix_inner_scroll(scroller);
+
+ _builder->get_widget("overlay", overlay);
+
+ /*************************Overlays******************************/
+ // No results
+ overlay_icon = sp_get_icon_image("searching", Gtk::ICON_SIZE_DIALOG);
+ overlay_icon->set_pixel_size(40);
+ overlay_icon->set_halign(Gtk::ALIGN_CENTER);
+ overlay_icon->set_valign(Gtk::ALIGN_START);
+ overlay_icon->set_margin_top(90);
+ overlay_icon->set_no_show_all(true);
+
+ 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(135);
+ overlay_title->set_no_show_all(true);
+
+ overlay_desc = new Gtk::Label();
+ overlay_desc->set_halign(Gtk::ALIGN_CENTER);
+ overlay_desc->set_valign(Gtk::ALIGN_START);
+ overlay_desc->set_margin_top(160);
+ overlay_desc->set_justify(Gtk::JUSTIFY_CENTER);
+ overlay_desc->set_no_show_all(true);
+
+ overlay->add_overlay(*overlay_icon);
+ overlay->add_overlay(*overlay_title);
+ overlay->add_overlay(*overlay_desc);
+
+ previous_height = 0;
+ previous_width = 0;
+
+ /******************** Tools *******************************/
+
+ _builder->get_widget("add-symbol", add_symbol);
+ add_symbol->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::insertSymbol));
+
+ _builder->get_widget("remove-symbol", remove_symbol);
+ remove_symbol->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::revertSymbol));
+
+ // Pack size (controls display area)
+ pack_size = prefs->getIntLimited(path + "tile-size", 12, 0, SIZES);
+
+ auto scale = &get_widget<Gtk::Scale>(_builder, "symbol-size");
+ scale->set_value(pack_size);
+ scale->signal_value_changed().connect([=](){
+ pack_size = scale->get_value();
+ assert(pack_size >= 0 && pack_size < SIZES);
+ _image_cache.clear();
+ rebuild();
+ prefs->setInt(path + "tile-size", pack_size);
+ });
+
+ scale_factor = prefs->getIntLimited(path + "scale-factor", 0, -10, +10);
+
+ _zoom.set_value(scale_factor);
+ _zoom.signal_value_changed().connect([=](){
+ scale_factor = _zoom.get_value();
+ rebuild();
+ prefs->setInt(path + "scale-factor", scale_factor);
+ });
+
+ icon_view->set_columns(-1);
+ icon_view->pack_start(_renderer);
+ icon_view->add_attribute(_renderer, "surface", g_columns.symbol_image);
+ icon_view->set_cell_data_func(_renderer, [=](const Gtk::TreeModel::const_iterator& it){
+ Gdk::Rectangle rect;
+ Gtk::TreeModel::Path path(it);
+ if (icon_view->get_cell_rect(path, rect)) {
+ auto height = icon_view->get_allocated_height();
+ bool visible = !(rect.get_x() < 0 && rect.get_y() < 0);
+ // cell rect coordinates are not affected by scrolling
+ if (visible && (rect.get_y() + rect.get_height() < 0 || rect.get_y() > 0 + height)) {
+ visible = false;
+ }
+ get_cell_data_func(&_renderer, *it, visible);
+ }
+ });
+
+ // Toggle scale to fit on/off
+ _builder->get_widget("zoom-to-fit", fit_symbol);
+ auto fit = prefs->getBool(path + "zoom-to-fit", true);
+ fit_symbol->set_active(fit);
+ fit_symbol->signal_clicked().connect([=](){
+ rebuild();
+ prefs->setBool(path + "zoom-to-fit", fit_symbol->get_active());
+ });
+
+ scan_all_symbol_sets(symbol_sets);
+
+ for (auto&& it : symbol_sets) {
+ auto row = _symbol_sets->append();
+ auto& set = it.second;
+ (*row)[g_set_columns.set_id] = it.first;
+ (*row)[g_set_columns.translated_title] = g_dpgettext2(nullptr, "Symbol", set.title.c_str());
+ (*row)[g_set_columns.set_document] = set.document;
+ (*row)[g_set_columns.set_filename] = it.first;
+ }
+
+ // last selected set
+ auto current = prefs->getString(path + "current-set", CURRENT_DOC_ID);
+
+ // by default select current doc (first on the list) in case nothing else gets selected
+ select_set(Gtk::TreeModel::Path("0"));
+
+ sensitive = true;
+
+ // restore set selection; check if it is still available first
+ _sets._sorted->foreach_path([&](const Gtk::TreeModel::Path& path){
+ auto it = _sets.path_to_child_iter(path);
+ if (current == (*it)[g_set_columns.set_id]) {
+ select_set(path);
+ return true;
+ }
+ return false;
+ });
+}
+
+void SymbolsDialog::on_unrealize() {
+ for (auto &connection : gtk_connections) {
+ connection.disconnect();
+ }
+ gtk_connections.clear();
+ DialogBase::on_unrealize();
+}
+
+SymbolsDialog::~SymbolsDialog()
+{
+ Inkscape::GC::release(preview_document);
+ assert(preview_document->_anchored_refcount() == 0);
+ delete preview_document;
+}
+
+void collect_symbols(SPObject* object, std::vector<SPSymbol*>& symbols) {
+ if (!object) return;
+
+ if (auto symbol = cast<SPSymbol>(object)) {
+ symbols.push_back(symbol);
+ }
+
+ if (is<SPUse>(object)) return;
+
+ for (auto& child : object->children) {
+ collect_symbols(&child, symbols);
+ }
+}
+
+void SymbolsDialog::load_all_symbols() {
+ _sets._store->foreach_iter([=](const Gtk::TreeModel::iterator& it){
+ if (!(*it)[g_set_columns.set_document]) {
+ std::string path = (*it)[g_set_columns.set_filename];
+ if (!path.empty()) {
+ auto doc = load_symbol_set(path);
+ (*it)[g_set_columns.set_document] = doc;
+ }
+ }
+ return false;
+ });
+}
+
+std::map<std::string, SymbolSet> get_all_symbols(Glib::RefPtr<Gtk::ListStore>& store) {
+ std::map<std::string, SymbolSet> map;
+
+ store->foreach_iter([&](const Gtk::TreeModel::iterator& it){
+ if (SPDocument* doc = (*it)[g_set_columns.set_document]) {
+ SymbolSet vect;
+ collect_symbols(doc->getRoot(), vect.symbols);
+ vect.title = (*it)[g_set_columns.translated_title];
+ vect.document = doc;
+ Glib::ustring id = (*it)[g_set_columns.set_id];
+ map[id.raw()] = vect;
+ }
+ return false;
+ });
+
+ return map;
+}
+
+void SymbolsDialog::rebuild(Gtk::TreeIter current) {
+ if (!sensitive || !current) {
+ return;
+ }
+
+ auto pending = _update.block();
+
+ // remove model first, or else IconView will update N times as N rows get deleted...
+ icon_view->unset_model();
+
+ _symbols._store->clear();
+
+ auto it = current;
+
+ std::map<std::string, SymbolSet> symbols;
+
+ SPDocument* document = (*it)[g_set_columns.set_document];
+ Glib::ustring set_id = (*it)[g_set_columns.set_id];
+
+ if (!document) {
+ if (set_id == CURRENT_DOC_ID) {
+ document = getDocument();
+ }
+ else if (set_id == ALL_SETS_ID) {
+ // load symbol sets, if not yet open
+ load_all_symbols();
+ // get symbols from all symbol sets (apart from current document)
+ symbols = get_all_symbols(_sets._store);
+ }
+ else {
+ std::string path = (*it)[g_set_columns.set_filename];
+ // load symbol set
+ document = load_symbol_set(path);
+ (*it)[g_set_columns.set_document] = document;
+ }
+ }
+
+ if (document) {
+ auto& vect = symbols[set_id.raw()];
+ collect_symbols(document->getRoot(), vect.symbols);
+ vect.document = set_id == CURRENT_DOC_ID ? nullptr : document;
+ vect.title = (*it)[g_set_columns.translated_title];
+ }
+
+ size_t n = 0;
+ for (auto&& it : symbols) {
+ auto& set = it.second;
+ for (auto symbol : set.symbols) {
+ addSymbol(symbol, set.title, set.document);
+ }
+ n += set.symbols.size();
+ }
+
+ for (auto r : icon_view->get_cells()) {
+ if (auto t = dynamic_cast<Gtk::CellRendererText*>(r)) {
+ // sizable boost in layout speed at the cost of showing only part of the title...
+ if (n > 1000) {
+ t->set_fixed_height_from_font(1);
+ t->property_ellipsize() = Pango::EllipsizeMode::ELLIPSIZE_END;
+ }
+ else {
+ t->set_fixed_height_from_font(-1);
+ t->property_ellipsize() = Pango::EllipsizeMode::ELLIPSIZE_NONE;
+ // t->property_wrap_mode() = Pango::WrapMode::WRAP_CHAR;
+ }
+ }
+ }
+
+ // reattach the model, have IconView content rebuilt
+ icon_view->set_model(_symbols._filtered);
+
+// layout speed test:
+// Gtk::Allocation alloc;
+// alloc.set_width(200);
+// alloc.set_height(500);
+// alloc.set_x(0);
+// alloc.set_y(0);
+// auto old_time = std::chrono::high_resolution_clock::now();
+// icon_view->size_allocate(alloc);
+// auto current_time = std::chrono::high_resolution_clock::now();
+// auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(current_time - old_time);
+// g_warning("size time: %d ms", static_cast<int>(elapsed.count()));
+
+ set_info();
+}
+
+void SymbolsDialog::rebuild() {
+ if (auto set = get_current_set()) {
+ rebuild(*set);
+ }
+}
+
+void SymbolsDialog::showOverlay() {
+ auto search = _search.get_text_length() > 0;
+ auto visible = visible_symbols();
+ auto current = get_current_set_id() == CURRENT_DOC_ID;
+
+ auto small = [](const char* str){
+ return Glib::ustring::compose("<small>%1</small>", Glib::Markup::escape_text(str));
+ };
+ auto large = [](const char* str){
+ return Glib::ustring::compose("<span size='large'>%1</span>", Glib::Markup::escape_text(str));
+ };
+
+ if (!visible && search) {
+ overlay_title->set_markup(large(_("No symbols found.")));
+ overlay_desc->set_markup(small(_("Try a different search term,\nor switch to a different symbol set.")));
+ } else if (!visible && current) {
+ overlay_title->set_markup(large(_("No symbols found.")));
+ overlay_desc->set_markup(small(_("No symbols in current document.\nChoose a different symbol set\nor add a new symbol.")));
+ }
+
+ /*
+ if (current == ALLDOCS && !_l.size())
+ {
+ overlay_icon->hide();
+ if (!all_docs_processed) {
+ overlay_icon->show();
+ overlay_title->set_markup(
+ Glib::ustring("<span size=\"large\">") + _("Search in all symbol sets...") + "</span>");
+ overlay_desc->set_markup(
+ Glib::ustring("<span size=\"small\">") + _("The first search can be slow.") + "</span>");
+ } else if (!icons_found && !search_str.empty()) {
+ overlay_title->set_markup(
+ Glib::ustring("<span size=\"large\">") + _("No symbols found.") + "</span>");
+ overlay_desc->set_markup(
+ Glib::ustring("<span size=\"small\">") + _("Try a different search term.") + "</span>");
+ } else {
+ 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("</span>"));
+ }
+ } else if (!number_symbols && (current != CURRENTDOC || !search_str.empty())) {
+ 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(_("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 symbols 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_icon->show();
+ overlay_title->show();
+ overlay_desc->show();
+}
+
+void SymbolsDialog::hideOverlay() {
+ // overlay_opacity->hide();
+ overlay_icon->hide();
+ overlay_title->hide();
+ overlay_desc->hide();
+}
+
+void SymbolsDialog::insertSymbol() {
+ getDesktop()->getSelection()->toSymbol();
+}
+
+void SymbolsDialog::revertSymbol() {
+ if (auto document = getDocument()) {
+ if (auto symbol = cast<SPSymbol>(document->getObjectById(getSymbolId(get_selected_symbol())))) {
+ symbol->unSymbol();
+ }
+ Inkscape::DocumentUndo::done(document, _("Group from symbol"), "");
+ }
+}
+
+void SymbolsDialog::iconDragDataGet(const Glib::RefPtr<Gdk::DragContext>& /*context*/, Gtk::SelectionData& data, guint /*info*/, guint /*time*/)
+{
+ auto selected = get_selected_symbol();
+ if (!selected) {
+ return;
+ }
+ Glib::ustring symbol_id = (**selected)[g_columns.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::selectionChanged(Inkscape::Selection *selection) {
+ // what are we trying to do here? this code doesn't seem to accomplish anything in v1.2
+/*
+ auto selected = getSelected();
+ Glib::ustring symbol_id = getSymbolId(selected);
+ Glib::ustring doc_title = get_active_base_text(getSymbolDocTitle(selected));
+ if (!doc_title.empty()) {
+ SPDocument* symbol_document = symbol_sets[doc_title].second;
+ 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::refresh_on_idle(int delay) {
+ // if symbols from current document are presented...
+ if (get_current_set_id() == CURRENT_DOC_ID) {
+ // refresh them on idle; delay here helps to coalesce consecutive requests into one
+ _idle_refresh = Glib::signal_timeout().connect([=](){
+ rebuild(*get_current_set());
+ return false; // disconnect
+ }, delay, Glib::PRIORITY_DEFAULT_IDLE);
+ }
+}
+
+void SymbolsDialog::documentReplaced()
+{
+ _defs_modified.disconnect();
+ _doc_resource_changed.disconnect();
+
+ if (auto document = getDocument()) {
+ _defs_modified = document->getDefs()->connectModified([=](SPObject* ob, guint flags) {
+ refresh_on_idle();
+ });
+ _doc_resource_changed = document->connectResourcesChanged("symbol", [this](){
+ refresh_on_idle();
+ });
+ }
+
+ // if symbol set is from current document, need to rebuild
+ refresh_on_idle(0);
+ update_tool_buttons();
+}
+
+void SymbolsDialog::update_tool_buttons() {
+ if (get_current_set_id() == CURRENT_DOC_ID) {
+ add_symbol->set_sensitive();
+ remove_symbol->set_sensitive();
+ }
+ else {
+ add_symbol->set_sensitive(false);
+ remove_symbol->set_sensitive(false);
+ }
+}
+
+Glib::ustring SymbolsDialog::get_current_set_id() const {
+ auto cur = get_current_set();
+ if (cur.has_value()) {
+ return (*cur.value())[g_set_columns.set_id];
+ }
+ return {};
+}
+
+std::optional<Gtk::TreeIter> SymbolsDialog::get_current_set() const {
+ auto selected = _symbol_sets_view.get_selected_items();
+ if (selected.empty()) {
+ return std::nullopt;
+ }
+ return _sets.path_to_child_iter(selected.front());
+}
+
+SPDocument* SymbolsDialog::get_symbol_document(const std::optional<Gtk::TreeIter>& it) const {
+ if (!it) {
+ return nullptr;
+ }
+ SPDocument* doc = (**it)[g_columns.symbol_document];
+
+ return doc;
+}
+
+/** Return the path to the selected symbol, or an empty optional if nothing is selected. */
+std::optional<Gtk::TreeModel::Path> SymbolsDialog::get_selected_symbol_path() const {
+ auto selected = icon_view->get_selected_items();
+ if (selected.empty()) {
+ return std::nullopt;
+ }
+ return selected.front();
+}
+
+std::optional<Gtk::TreeIter> SymbolsDialog::get_selected_symbol() const {
+ auto selected = get_selected_symbol_path();
+ if (!selected) {
+ return std::nullopt;
+ }
+ return _symbols.path_to_child_iter(*selected);
+}
+
+/** Return the dimensions of the symbol at the given path, in document units. */
+Geom::Point SymbolsDialog::getSymbolDimensions(const std::optional<Gtk::TreeIter>& it) const
+{
+ if (!it) {
+ return Geom::Point();
+ }
+ return (**it)[g_columns.doc_dimensions];
+}
+
+/** Return the ID of the symbol at the given path, with empty string fallback. */
+Glib::ustring SymbolsDialog::getSymbolId(const std::optional<Gtk::TreeIter>& it) const
+{
+ if (!it) {
+ return "";
+ }
+ return (**it)[g_columns.symbol_id];
+}
+
+/** Store the symbol in the clipboard for further manipulation/insertion into document.
+ *
+ * @param symbol_path The path to the symbol in the tree model.
+ * @param bbox The bounding box to set on the clipboard document's clipnode.
+ */
+void SymbolsDialog::sendToClipboard(const Gtk::TreeIter& symbol_iter, Geom::Rect const &bbox)
+{
+ auto symbol_id = getSymbolId(symbol_iter);
+ if (symbol_id.empty()) return;
+
+ auto symbol_document = get_symbol_document(symbol_iter);
+ if (!symbol_document) {
+ //we are in global search so get the original symbol document by title
+ symbol_document = getDocument();
+ }
+ if (!symbol_document) {
+ return;
+ }
+ if (SPObject* symbol = symbol_document->getObjectById(symbol_id)) {
+ // 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 == getDocument()) {
+ style = styleFromUse(symbol_id.c_str(), symbol_document);
+ } else {
+ style = symbol_document->getReprRoot()->attribute("style");
+ }
+ }
+ ClipboardManager::get()->copySymbol(symbol->getRepr(), style, symbol_document, bbox);
+ }
+}
+
+void SymbolsDialog::iconChanged()
+{
+ if (_update.pending()) return;
+
+ if (auto selected = get_selected_symbol()) {
+ auto const dims = getSymbolDimensions(selected);
+ sendToClipboard(*selected, Geom::Rect(-0.5 * dims, 0.5 * dims));
+ }
+}
+
+#ifdef WITH_LIBVISIO
+
+// 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;
+};
+
+// Read Visio stencil files
+SPDocument* read_vss(std::string filename, std::string 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;
+ RVNGSVGDrawingGenerator_WithTitle generator(output, titles, "svg");
+
+ if (!libvisio::VisioDocument::parseStencils(&input, &generator)) {
+ 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 (titles.size() == output.size() && titles[i] != "") {
+ tmpSVGOutput += "<title>" + Glib::ustring(RVNGString::escapeXML(titles[i].cstr()).cstr()) + "</title>\n";
+ }
+
+ 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(), tmpSVGOutput.size(), false);
+}
+#endif
+
+/* Hunts preference directories for symbol files */
+void scan_all_symbol_sets(std::map<std::string, SymbolSet>& symbol_sets) {
+
+ using namespace Inkscape::IO::Resource;
+ std::regex matchtitle(".*?<title.*?>(.*?)<(/| /)");
+
+ for (auto& filename : get_filenames(SYMBOLS, {".svg", ".vss", "vssx", "vsdx"})) {
+ if (symbol_sets.count(filename)) continue;
+
+ if (Glib::str_has_suffix(filename, ".vss") || Glib::str_has_suffix(filename, ".vssx") || Glib::str_has_suffix(filename, ".vsdx")) {
+ std::size_t found = filename.find_last_of("/\\");
+ auto title = found != Glib::ustring::npos ? filename.substr(found + 1) : filename;
+ title = title.erase(title.rfind('.'));
+ if (title.empty()) {
+ title = _("Unnamed Symbols");
+ }
+ symbol_sets[filename].title = title;
+ } 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[filename].title = title_res;
+ break;
+ }
+ auto position_exit = line.find("<defs");
+ if (position_exit != std::string::npos) {
+ std::size_t found = filename.find_last_of("/\\");
+ auto title = found != std::string::npos ? filename.substr(found + 1) : filename;
+ title = title.erase(title.rfind('.'));
+ if (title.empty()) {
+ title = _("Unnamed Symbols");
+ }
+ symbol_sets[filename].title = title;
+ break;
+ }
+ }
+ }
+ }
+}
+
+// Load SVG or VSS document and create SPDocument
+SPDocument* load_symbol_set(std::string filename)
+{
+ SPDocument* symbol_doc = nullptr;
+ if (auto doc = symbol_sets[filename].document) {
+ return doc;
+ }
+
+ using namespace Inkscape::IO::Resource;
+ if (Glib::str_has_suffix(filename, ".vss") || Glib::str_has_suffix(filename, ".vssx") || Glib::str_has_suffix(filename, ".vsdx")) {
+#ifdef WITH_LIBVISIO
+ symbol_doc = read_vss(filename, symbol_sets[filename].title);
+#endif
+ } else if (Glib::str_has_suffix(filename, ".svg")) {
+ symbol_doc = SPDocument::createNewDoc(filename.c_str(), FALSE);
+ }
+
+ if (symbol_doc) {
+ symbol_sets[filename].document = symbol_doc;
+ }
+ return symbol_doc;
+}
+
+void SymbolsDialog::useInDoc (SPObject *r, std::vector<SPUse*> &l)
+{
+ if (is<SPUse>(r) ) {
+ l.push_back(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 = Inkscape::getHrefAttribute(*use->getRepr()).second;
+ if( href ) {
+ Glib::ustring href2(href);
+ Glib::ustring id2(id);
+ id2 = "#" + id2;
+ if( !href2.compare(id2) ) {
+ style = use->getRepr()->attribute("style");
+ break;
+ }
+ }
+ }
+ }
+ return style;
+}
+
+size_t SymbolsDialog::total_symbols() const {
+ return _symbols._store->children().size();
+}
+
+size_t SymbolsDialog::visible_symbols() const {
+ return _symbols._filtered->children().size();
+}
+
+void SymbolsDialog::set_info() {
+ auto total = total_symbols();
+ auto visible = visible_symbols();
+ if (!total) {
+ set_info("");
+ }
+ else if (total == visible) {
+ set_info(Glib::ustring::compose("%1: %2", _("Symbols"), total).c_str());
+ }
+ else if (visible == 0) {
+ set_info(Glib::ustring::compose("%1: %2 / %3", _("Symbols"), _("none"), total).c_str());
+ }
+ else {
+ set_info(Glib::ustring::compose("%1: %2 / %3", _("Symbols"), visible, total).c_str());
+ }
+
+ if (total == 0 || visible == 0) {
+ showOverlay();
+ }
+ else {
+ hideOverlay();
+ }
+}
+
+void SymbolsDialog::set_info(const Glib::ustring& text) {
+ auto info = "<small>" + Glib::Markup::escape_text(text) + "</small>";
+ get_widget<Gtk::Label>(_builder, "info").set_markup(info);
+}
+
+Cairo::RefPtr<Cairo::Surface> add_background(Cairo::RefPtr<Cairo::Surface> image, uint32_t rgb, double margin, double radius, int device_scale, std::optional<uint32_t> border = std::optional<uint32_t>()) {
+ auto w = image ? cairo_image_surface_get_width(image->cobj()) : 0;
+ auto h = image ? cairo_image_surface_get_height(image->cobj()) : 0;
+ auto width = w / device_scale + 2 * margin;
+ auto height = h / device_scale + 2 * margin;
+
+ auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, width * device_scale, height * device_scale);
+ cairo_surface_set_device_scale(surface->cobj(), device_scale, device_scale);
+ auto ctx = Cairo::Context::create(surface);
+
+ auto x = 0;
+ auto y = 0;
+ if (border.has_value()) {
+ x += 0.5 * device_scale;
+ y += 0.5 * device_scale;
+ width -= device_scale;
+ height -= device_scale;
+ }
+ ctx->arc(x + width - radius, y + radius, radius, -M_PI_2, 0);
+ ctx->arc(x + width - radius, y + height - radius, radius, 0, M_PI_2);
+ ctx->arc(x + radius, y + height - radius, radius, M_PI_2, M_PI);
+ ctx->arc(x + radius, y + radius, radius, M_PI, 3 * M_PI_2);
+ ctx->close_path();
+
+ ctx->set_source_rgb(SP_RGBA32_R_F(rgb), SP_RGBA32_G_F(rgb), SP_RGBA32_B_F(rgb));
+ if (border.has_value()) {
+ ctx->fill_preserve();
+
+ auto b = *border;
+ ctx->set_source_rgb(SP_RGBA32_R_F(b), SP_RGBA32_G_F(b), SP_RGBA32_B_F(b));
+ ctx->set_line_width(1.0);
+ ctx->stroke();
+ }
+ else {
+ ctx->fill();
+ }
+
+ if (image) {
+ ctx->set_source(image, margin, margin);
+ ctx->paint();
+ }
+
+ return surface;
+}
+
+void SymbolsDialog::addSymbol(SPSymbol* symbol, Glib::ustring doc_title, SPDocument* document)
+{
+ auto id = symbol->getRepr()->attribute("id");
+ auto title = symbol->title(); // From title element
+ Glib::ustring short_title = title ? g_dpgettext2(nullptr, "Symbol", title) : id;
+ g_free(title);
+ auto symbol_title = Glib::ustring::compose("%1 (%2)", short_title, doc_title);
+
+ Geom::Point dimensions{64, 64}; // Default to 64x64 px if size not available.
+ if (auto rect = symbol->documentVisualBounds()) {
+ dimensions = rect->dimensions();
+ }
+ auto set = symbol->document ? symbol->document->getDocumentFilename() : "null";
+ if (!set) set = "noname";
+ Gtk::ListStore::iterator row = _store->append();
+ std::ostringstream key;
+ key << set << '\n' << id;
+ (*row)[g_columns.cache_key] = key.str();
+ (*row)[g_columns.symbol_id] = Glib::ustring(id);
+ // symbol title and document name - used in a tooltip
+ (*row)[g_columns.symbol_title] = Glib::Markup::escape_text(symbol_title);
+ // symbol title shown below image
+ (*row)[g_columns.symbol_short_title] = "<small>" + Glib::Markup::escape_text(short_title) + "</small>";
+ // symbol title verbatim, used for searching/filtering
+ (*row)[g_columns.symbol_search_title] = short_title;
+ (*row)[g_columns.doc_dimensions] = dimensions;
+ (*row)[g_columns.symbol_document] = document;
+}
+
+Cairo::RefPtr<Cairo::Surface> SymbolsDialog::draw_symbol(SPSymbol* symbol) {
+ Cairo::RefPtr<Cairo::Surface> surface;
+ Cairo::RefPtr<Cairo::Surface> image;
+ int device_scale = get_scale_factor();
+
+ if (symbol) {
+ image = drawSymbol(symbol);
+ }
+ else {
+ unsigned psize = SYMBOL_ICON_SIZES[pack_size] * device_scale;
+ image = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, psize, psize);
+ cairo_surface_set_device_scale(image->cobj(), device_scale, device_scale);
+ }
+
+ // white background for typically black symbols, so they don't disappear in a dark theme
+ if (image) {
+ uint32_t background = 0xffffff00;
+ double margin = 3.0;
+ double radius = 3.0;
+ surface = add_background(image, background, margin, radius, device_scale);
+ }
+
+ return surface;
+}
+
+/*
+ * 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.
+ */
+Cairo::RefPtr<Cairo::Surface> SymbolsDialog::drawSymbol(SPSymbol *symbol)
+{
+ if (!symbol) return Cairo::RefPtr<Cairo::Surface>();
+ // Create a copy repr of the symbol with id="the_symbol"
+ Inkscape::XML::Node *repr = symbol->getRepr()->duplicate(preview_document->getReprDoc());
+ repr->setAttribute("id", "the_symbol");
+
+ // 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 == getDocument()) {
+ gchar const *id = symbol->getRepr()->attribute("id");
+ style = styleFromUse( id, symbol->document );
+ } else {
+ style = symbol->document->getReprRoot()->attribute("style");
+ }
+ }
+
+ // This is for display in Symbols dialog only
+ if( style ) repr->setAttribute( "style", style );
+
+ SPDocument::install_reference_document scoped(preview_document, symbol->document);
+ preview_document->getDefs()->getRepr()->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->ensureUpToDate();
+
+ // Make sure we have symbol in preview_document
+ SPObject *object_temp = preview_document->getObjectById( "the_use" );
+
+ auto item = cast<SPItem>(object_temp);
+ g_assert(item != nullptr);
+ unsigned psize = SYMBOL_ICON_SIZES[pack_size];
+
+ cairo_surface_t* surface = 0;
+ // 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 / 4.0) * psize / 32.0;
+ }
+
+ int device_scale = get_scale_factor();
+
+ surface = render_surface(renderDrawing, scale, *dbox, Geom::IntPoint(psize, psize), device_scale, nullptr, true);
+
+ if (surface) {
+ cairo_surface_set_device_scale(surface, device_scale, device_scale);
+ }
+ }
+
+ preview_document->getObjectByRepr(repr)->deleteObject(false);
+
+ return Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(surface, true));
+}
+
+/*
+ * 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>
+ const char 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\">"
+" <use id=\"the_use\" xlink:href=\"#the_symbol\"/>"
+"</svg>";
+ return SPDocument::createNewDocFromMem(buffer, strlen(buffer), false);
+}
+
+void SymbolsDialog::get_cell_data_func(Gtk::CellRenderer* cell_renderer, Gtk::TreeModel::Row row, bool visible)
+{
+ std::string cache_key = (row)[g_columns.cache_key];
+ Glib::ustring id = (row)[g_columns.symbol_id];
+ Cairo::RefPtr<Cairo::Surface> surface;
+
+ if (!visible) {
+ // cell is not visible, so this is layout pass; return empty image of the right size
+ int device_scale = get_scale_factor();
+ unsigned psize = SYMBOL_ICON_SIZES[pack_size] * device_scale;
+ if (!g_dummy || g_dummy->get_width() != psize) {
+ g_dummy = g_dummy.cast_static(draw_symbol(nullptr));
+ }
+ surface = g_dummy;
+ }
+ else {
+ // cell is visible, so we need to return correct symbol image and render it if it's missing
+ if (auto image = _image_cache.get(cache_key)) {
+ // cache hit
+ surface = *image;
+ }
+ else {
+ // render
+ SPDocument* doc = row[g_columns.symbol_document];
+ if (!doc) doc = getDocument();
+ SPSymbol* symbol = doc ? cast<SPSymbol>(doc->getObjectById(id)) : nullptr;
+ surface = draw_symbol(symbol);
+ _image_cache.insert(cache_key, surface);
+ }
+ }
+ cell_renderer->set_property("surface", surface);
+}
+
+} //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..92978de
--- /dev/null
+++ b/src/ui/dialog/symbols.h
@@ -0,0 +1,192 @@
+// 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
+ * 2023 Mike Kowalski
+ *
+ * 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 <cstddef>
+#include <glibmm/refptr.h>
+#include <glibmm/ustring.h>
+#include <gtkmm.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/cellrendererpixbuf.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/iconview.h>
+#include <gtkmm/label.h>
+#include <gtkmm/menubutton.h>
+#include <gtkmm/treeiter.h>
+#include <gtkmm/treemodel.h>
+#include <gtkmm/treemodelcolumn.h>
+#include <sigc++/connection.h>
+#include <string>
+#include <vector>
+#include <boost/compute/detail/lru_cache.hpp>
+
+#include "desktop.h"
+#include "display/drawing.h"
+#include "document.h"
+#include "helper/auto-connection.h"
+#include "selection.h"
+#include "ui/dialog/dialog-base.h"
+#include "ui/operation-blocker.h"
+
+class SPObject;
+class SPSymbol;
+class SPUse;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/**
+ * 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.
+ */
+
+
+class SymbolsDialog : public DialogBase
+{
+public:
+ SymbolsDialog(char const *prefsPath = "/dialogs/symbols");
+ ~SymbolsDialog() override;
+
+private:
+ void documentReplaced() override;
+ void selectionChanged(Inkscape::Selection *selection) override;
+ void on_unrealize() override;
+ void rebuild();
+ void rebuild(Gtk::TreeIter current);
+ void insertSymbol();
+ void revertSymbol();
+ void iconChanged();
+ void sendToClipboard(const Gtk::TreeIter& symbol_iter, Geom::Rect const &bbox);
+ Glib::ustring getSymbolId(const std::optional<Gtk::TreeIter>& it) const;
+ Geom::Point getSymbolDimensions(const std::optional<Gtk::TreeIter>& it) const;
+ SPDocument* get_symbol_document(const std::optional<Gtk::TreeIter>& it) const;
+ void iconDragDataGet(const Glib::RefPtr<Gdk::DragContext>& context, Gtk::SelectionData& selection_data, guint info, guint time);
+ void onDragStart();
+ void addSymbol(SPSymbol* symbol, Glib::ustring doc_title, SPDocument* document);
+ SPDocument* symbolsPreviewDoc();
+ void useInDoc(SPObject *r, std::vector<SPUse*> &l);
+ std::vector<SPUse*> useInDoc( SPDocument* document);
+ void addSymbols();
+ void showOverlay();
+ void hideOverlay();
+ gchar const* styleFromUse( gchar const* id, SPDocument* document);
+ Cairo::RefPtr<Cairo::Surface> drawSymbol(SPSymbol *symbol);
+ Cairo::RefPtr<Cairo::Surface> draw_symbol(SPSymbol* symbol);
+ Glib::RefPtr<Gdk::Pixbuf> getOverlay(gint width, gint height);
+ void set_info();
+ void set_info(const Glib::ustring& text);
+ std::optional<Gtk::TreeIter> get_current_set() const;
+ Glib::ustring get_current_set_id() const;
+ std::optional<Gtk::TreeModel::Path> get_selected_symbol_path() const;
+ std::optional<Gtk::TreeIter> get_selected_symbol() const;
+ void load_all_symbols();
+ void update_tool_buttons();
+ size_t total_symbols() const;
+ size_t visible_symbols() const;
+ void get_cell_data_func(Gtk::CellRenderer* cell_renderer, Gtk::TreeModel::Row row, bool visible);
+ void refresh_on_idle(int delay = 100);
+
+ auto_connection _idle_search;
+ Glib::RefPtr<Gtk::Builder> _builder;
+ Gtk::Scale& _zoom;
+ // Index into sizes which is selected
+ int pack_size;
+ // Scale factor
+ int scale_factor;
+ bool sensitive = false;
+ OperationBlocker _update;
+ double previous_height;
+ double previous_width;
+ Geom::Point _last_mousedown; ///< Last button press position in the icon view coordinates.
+ Glib::RefPtr<Gtk::ListStore> _store;
+ Gtk::MenuButton& _symbols_popup;
+ Gtk::SearchEntry& _set_search;
+ Gtk::IconView& _symbol_sets_view;
+ Gtk::Label& _cur_set_name;
+ Gtk::SearchEntry& _search;
+ Gtk::IconView* icon_view;
+ Gtk::Button* add_symbol;
+ Gtk::Button* remove_symbol;
+ Gtk::Box* tools;
+ Gtk::Overlay* overlay;
+ Gtk::Image* overlay_icon;
+ Gtk::Image* overlay_opacity;
+ Gtk::Label* overlay_title;
+ Gtk::Label* overlay_desc;
+ Gtk::ScrolledWindow *scroller;
+ Gtk::CheckButton* fit_symbol;
+ Gtk::CellRendererPixbuf _renderer;
+ Gtk::CellRendererPixbuf _renderer2;
+ SPDocument* preview_document = nullptr; /* Document to render single symbol */
+ Glib::RefPtr<Gtk::ListStore> _symbol_sets;
+ struct Store {
+ Glib::RefPtr<Gtk::ListStore> _store;
+ Glib::RefPtr<Gtk::TreeModelFilter> _filtered;
+ Glib::RefPtr<Gtk::TreeModelSort> _sorted;
+
+ Gtk::TreeIter path_to_child_iter(Gtk::TreeModel::Path path) const {
+ if (_sorted) path = _sorted->convert_path_to_child_path(path);
+ if (_filtered) path = _filtered->convert_path_to_child_path(path);
+ return _store->get_iter(path);
+ }
+ void refilter() {
+ if (_filtered) _filtered->refilter();
+ }
+ } _symbols, _sets;
+
+ /* For rendering the template drawing */
+ unsigned key;
+ Inkscape::Drawing renderDrawing;
+ std::vector<sigc::connection> gtk_connections;
+ auto_connection _defs_modified;
+ auto_connection _doc_resource_changed;
+ auto_connection _idle_refresh;
+ boost::compute::detail::lru_cache<std::string, Cairo::RefPtr<Cairo::Surface>> _image_cache;
+};
+
+} //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/text-edit.cpp b/src/ui/dialog/text-edit.cpp
new file mode 100644
index 0000000..6b4a433
--- /dev/null
+++ b/src/ui/dialog/text-edit.cpp
@@ -0,0 +1,647 @@
+// 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>
+
+#include <gtkmm/box.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/button.h>
+#include <gtkmm/buttonbox.h>
+#include <gtkmm/label.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/textbuffer.h>
+#include <gtkmm/textview.h>
+
+#ifdef WITH_GSPELL
+# include <gspell/gspell.h>
+#endif
+
+#include "desktop-style.h"
+#include "desktop.h"
+#include "dialog-notebook.h"
+#include "dialog-container.h"
+#include "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "style.h"
+#include "text-editing.h"
+
+#include <libnrtype/font-factory.h>
+#include <libnrtype/font-instance.h>
+#include <libnrtype/font-lister.h>
+
+#include "object/sp-flowtext.h"
+#include "object/sp-text.h"
+
+#include "io/resource.h"
+#include "svg/css-ostringstream.h"
+#include "ui/icon-names.h"
+#include "ui/widget/font-selector.h"
+
+#include "util/units.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+TextEdit::TextEdit()
+ : DialogBase("/dialogs/textandfont", "Text")
+ , 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?.;/()"))
+ , _undo{"doc.undo"}
+ , _redo{"doc.redo"}
+{
+ std::string gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "dialog-text-edit.glade");
+ Glib::RefPtr<Gtk::Builder> builder;
+ try {
+ builder = Gtk::Builder::create_from_file(gladefile);
+ } catch (const Glib::Error &ex) {
+ g_warning("GtkBuilder file loading failed for save template dialog");
+ return;
+ }
+
+ Inkscape::FontCollections *font_collections = Inkscape::FontCollections::get();
+
+ Gtk::Box *contents;
+ Gtk::Notebook *notebook;
+ Gtk::Box *font_box;
+ Gtk::Box *feat_box;
+
+ builder->get_widget("contents", contents);
+ builder->get_widget("notebook", notebook);
+ builder->get_widget("font_box", font_box);
+ builder->get_widget("feat_box", feat_box);
+ builder->get_widget("preview_label", preview_label);
+ builder->get_widget("preview_label2", preview_label2);
+ builder->get_widget("text_view", text_view);
+ builder->get_widget("setasdefault_button", setasdefault_button);
+ builder->get_widget("apply_button", apply_button);
+
+ builder->get_widget("settings_and_filters_box", settings_and_filters_box);
+ builder->get_widget("filter_menu_button", filter_menu_button);
+ builder->get_widget("reset_button", reset_button);
+ builder->get_widget("search_entry", search_entry);
+ builder->get_widget("font_count_label", font_count_label);
+ builder->get_widget("filter_popover", filter_popover);
+ builder->get_widget("popover_box", popover_box);
+ builder->get_widget("frame", frame);
+ builder->get_widget("frame_label", frame_label);
+ builder->get_widget("collection_editor_button", collection_editor_button);
+ builder->get_widget("collections_list", collections_list);
+
+ text_buffer = Glib::RefPtr<Gtk::TextBuffer>::cast_static(builder->get_object("text_buffer"));
+
+ font_box->pack_start(font_selector, true, true);
+ font_box->reorder_child(font_selector, 2);
+ feat_box->pack_start(font_features, true, true);
+ feat_box->reorder_child(font_features, 1);
+
+ // filter_popover->set_modal(false); // Stay open until button clicked again.
+ filter_popover->signal_show().connect([=](){
+ // update font collections checkboxes
+ display_font_collections();
+ }, false);
+
+ filter_menu_button->set_image_from_icon_name(INKSCAPE_ICON("font_collections"));
+ filter_menu_button->set_always_show_image(true);
+ filter_menu_button->set_label(_("Collections"));
+
+#ifdef WITH_GSPELL
+ /*
+ 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.
+ */
+ GspellTextView *gspell_view = gspell_text_view_get_from_gtk_text_view(text_view->gobj());
+ gspell_text_view_basic_setup(gspell_view);
+#endif
+
+ add(*contents);
+
+ /* Signal handlers */
+ text_view->signal_key_press_event().connect(sigc::mem_fun(*this, &TextEdit::captureUndo));
+ text_buffer->signal_changed().connect([=](){ onChange(); });
+ setasdefault_button->signal_clicked().connect([=](){ onSetDefault(); });
+ apply_button->signal_clicked().connect([=](){ onApply(); });
+ fontChangedConn = font_selector.connectChanged(sigc::mem_fun(*this, &TextEdit::onFontChange));
+ fontFeaturesChangedConn = font_features.connectChanged([=](){ onChange(); });
+ notebook->signal_switch_page().connect(sigc::mem_fun(*this, &TextEdit::onFontFeatures));
+ search_entry->signal_search_changed().connect([=](){ on_search_entry_changed(); });
+ reset_button->signal_clicked().connect([=](){ on_reset_button_pressed(); });
+ collection_editor_button->signal_clicked().connect([=](){ on_fcm_button_clicked(); });
+ Inkscape::FontLister::get_instance()->connectUpdate(sigc::mem_fun(*this, &TextEdit::change_font_count_label));
+ fontCollectionsUpdate = font_collections->connect_update([=]() { display_font_collections(); });
+ fontCollectionsChangedSelection = font_collections->connect_selection_update([=]() { display_font_collections(); });
+
+ font_selector.set_name("TextEdit");
+ change_font_count_label();
+
+ show_all_children();
+}
+
+TextEdit::~TextEdit()
+{
+ selectModifiedConn.disconnect();
+ subselChangedConn.disconnect();
+ selectChangedConn.disconnect();
+ fontChangedConn.disconnect();
+ fontFeaturesChangedConn.disconnect();
+}
+
+bool TextEdit::captureUndo(GdkEventKey *key)
+{
+ if (_undo.isTriggeredBy(key) || _redo.isTriggeredBy(key)) {
+ /*
+ * TODO: Handle these events separately after switching to GTKMM4
+ * Fixes: https://gitlab.com/inkscape/inkscape/-/issues/744
+ */
+ return true;
+ }
+
+ return false;
+}
+
+void TextEdit::onReadSelection ( gboolean dostyle, gboolean /*docontent*/ )
+{
+ if (blocked)
+ return;
+
+ blocked = true;
+
+ SPItem *text = getSelectedTextItem ();
+
+ Glib::ustring phrase = samplephrase;
+
+ if (text)
+ {
+ guint items = getSelectedTextCount ();
+ bool has_one_item = items == 1;
+ text_view->set_sensitive(has_one_item);
+ apply_button->set_sensitive(false);
+ setasdefault_button->set_sensitive(true);
+
+ Glib::ustring str = sp_te_get_string_multiline(text);
+ if (!str.empty()) {
+ if (has_one_item) {
+ text_buffer->set_text(str);
+ text_buffer->set_modified(false);
+ }
+ phrase = str;
+
+ } else {
+ text_buffer->set_text("");
+ }
+
+ text->getRepr(); // was being called but result ignored. Check this.
+ } else {
+ text_view->set_sensitive(false);
+ apply_button->set_sensitive(false);
+ setasdefault_button->set_sensitive(false);
+ }
+
+ if (dostyle && text) {
+ auto *desktop = getDesktop();
+
+ // create temporary style
+ SPStyle query(desktop->getDocument());
+
+ // 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 (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 (desktop, &query, QUERY_STYLE_PROPERTY_FONTVARIANTS);
+ int result_features =
+ sp_desktop_query_style (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 (!getDesktop())
+ return nullptr;
+
+ auto tmp= getDesktop()->getSelection()->items();
+ for(auto i=tmp.begin();i!=tmp.end();++i)
+ {
+ if (is<SPText>(*i) || is<SPFlowtext>(*i))
+ return *i;
+ }
+
+ return nullptr;
+}
+
+
+unsigned TextEdit::getSelectedTextCount ()
+{
+ if (!getDesktop())
+ return 0;
+
+ unsigned int items = 0;
+
+ auto tmp= getDesktop()->getSelection()->items();
+ for(auto i=tmp.begin();i!=tmp.end();++i)
+ {
+ if (is<SPText>(*i) || is<SPFlowtext>(*i))
+ ++items;
+ }
+
+ return items;
+}
+
+void TextEdit::documentReplaced()
+{
+ onReadSelection(true, true);
+}
+
+void TextEdit::selectionChanged(Selection *selection)
+{
+ onReadSelection(true, true);
+}
+
+void TextEdit::selectionModified(Selection *selection, 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 );
+ onReadSelection (style, content);
+}
+
+
+void TextEdit::updateObjectText ( SPItem *text )
+{
+ Gtk::TextIter start, end;
+
+ // write text
+ if (text_buffer->get_modified()) {
+ text_buffer->get_bounds(start, end);
+ Glib::ustring str = text_buffer->get_text(start, end);
+ sp_te_set_repr_text_multiline (text, str.c_str());
+ text_buffer->set_modified(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 = getDesktop();
+
+ 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 (is<SPText>(*i) || (is<SPFlowtext>(*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 = desktop->getSelection()->singleItem();
+ if (is<SPText>(item) || is<SPFlowtext>(item)) {
+ updateObjectText (item);
+ SPStyle *item_style = item->style;
+ if (is<SPText>(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(desktop->getDocument(), _("Set text style"), INKSCAPE_ICON("draw-text"));
+ apply_button->set_sensitive ( false );
+
+ sp_repr_css_attr_unref (css);
+ Inkscape::FontLister::get_instance()->update_font_list(desktop->getDocument());
+
+ blocked = false;
+}
+
+void TextEdit::display_font_collections()
+{
+ // std::cout << "TextEdit::display_font_collections()" << std::endl;
+
+ for (auto row : collections_list->get_children()) {
+ if (row) {
+ collections_list->remove(*row);
+ }
+ }
+
+ FontCollections *font_collections = Inkscape::FontCollections::get();
+
+ // Insert system collections.
+ for(auto const& col: font_collections->get_collections(true)) {
+ auto btn = Gtk::make_managed<Gtk::CheckButton>(col);
+ btn->set_margin_bottom(2);
+ btn->set_active(font_collections->is_collection_selected(col));
+ btn->signal_toggled().connect([=](){
+ // toggle font system collection
+ font_collections->update_selected_collections(col);
+ });
+// g_message("tag: %s", tag.display_name.c_str());
+ auto row = Gtk::make_managed<Gtk::ListBoxRow>();
+ row->set_can_focus(false);
+ row->add(*btn);
+ row->show_all();
+ collections_list->append(*row);
+ }
+
+ // Insert row separator.
+ auto sep = Gtk::make_managed<Gtk::Separator>();
+ sep->set_margin_bottom(2);
+ auto sep_row = Gtk::make_managed<Gtk::ListBoxRow>();
+ sep_row->set_can_focus(false);
+ sep_row->add(*sep);
+ sep_row->show_all();
+ collections_list->append(*sep_row);
+
+ // Insert user collections.
+ for (auto const& col: font_collections->get_collections()) {
+ auto btn = Gtk::make_managed<Gtk::CheckButton>(col);
+ btn->set_margin_bottom(2);
+ btn->set_active(font_collections->is_collection_selected(col));
+ btn->signal_toggled().connect([=](){
+ // toggle font collection
+ font_collections->update_selected_collections(col);
+ });
+// g_message("tag: %s", tag.display_name.c_str());
+ auto row = Gtk::make_managed<Gtk::ListBoxRow>();
+ row->set_can_focus(false);
+ row->add(*btn);
+ row->show_all();
+ collections_list->append(*row);
+ }
+}
+
+void TextEdit::onFontFeatures(Gtk::Widget * widgt, int pos)
+{
+ if (pos == 1) {
+ Glib::ustring fontspec = font_selector.get_fontspec();
+ if (!fontspec.empty()) {
+ auto res = FontFactory::get().FaceFromFontSpecification(fontspec.c_str());
+ if (res) {
+ font_features.update_opentype(fontspec);
+ }
+ }
+ }
+}
+
+void TextEdit::on_search_entry_changed()
+{
+ auto search_txt = search_entry->get_text();
+ font_selector.unset_model();
+ Inkscape::FontLister *font_lister = Inkscape::FontLister::get_instance();
+ font_lister->show_results(search_txt);
+ font_selector.set_model();
+}
+
+void TextEdit::on_reset_button_pressed()
+{
+ FontCollections *font_collections = Inkscape::FontCollections::get();
+ search_entry->set_text("");
+
+ // Un-select all the selected font collections.
+ font_collections->clear_selected_collections();
+
+ Inkscape::FontLister *font_lister = Inkscape::FontLister::get_instance();
+ font_lister->init_font_families();
+ font_lister->init_default_styles();
+ SPDocument *document = getDesktop()->getDocument();
+ font_lister->add_document_fonts_at_top(document);
+}
+
+void TextEdit::change_font_count_label()
+{
+ auto label = Inkscape::FontLister::get_instance()->get_font_count_label();
+ font_count_label->set_label(label);
+}
+
+void TextEdit::on_fcm_button_clicked()
+{
+ // Inkscape::UI::Dialog::FontCollectionsManager::getInstance();
+ if(auto desktop = SP_ACTIVE_DESKTOP) {
+ if (auto container = desktop->getContainer()) {
+ container->new_floating_dialog("FontCollections");
+ }
+ }
+}
+
+void TextEdit::onChange()
+{
+ if (blocked) {
+ return;
+ }
+
+ Gtk::TextIter start, end;
+ text_buffer->get_bounds(start, end);
+ Glib::ustring str = text_buffer->get_text(start, end);
+
+ Glib::ustring fontspec = font_selector.get_fontspec();
+ Glib::ustring features = font_features.get_markup();
+ const Glib::ustring& phrase = str.empty() ? samplephrase : str;
+ setPreviewText(fontspec, features, phrase);
+
+ SPItem *text = getSelectedTextItem();
+ if (text) {
+ apply_button->set_sensitive ( true );
+ }
+
+ setasdefault_button->set_sensitive ( true);
+}
+
+void TextEdit::onFontChange(Glib::ustring fontspec)
+{
+ // Is not necessary update open type features this done when user click on font features tab
+ onChange();
+}
+
+} //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..a20fcac
--- /dev/null
+++ b/src/ui/dialog/text-edit.h
@@ -0,0 +1,222 @@
+// 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 <glibmm/refptr.h>
+
+#include "helper/auto-connection.h"
+
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/frame.h"
+
+#include "ui/widget/font-selector.h"
+#include "ui/widget/font-variants.h"
+
+#include "util/action-accel.h"
+#include "util/font-collections.h"
+
+namespace Gtk {
+class Box;
+class Button;
+class ButtonBox;
+class Label;
+class Notebook;
+class TextBuffer;
+class TextView;
+}
+
+class SPItem;
+class FontInstance;
+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 DialogBase
+{
+public:
+ TextEdit();
+ ~TextEdit() override;
+
+ void documentReplaced() override;
+ void selectionChanged(Selection *selection) override;
+ void selectionModified(Selection *selection, guint flags) override;
+
+protected:
+ /**
+ * Callback for pressing the default button.
+ */
+ void onSetDefault ();
+
+ /**
+ * Callback for pressing the apply button.
+ */
+ void onApply ();
+
+ /**
+ * Function to list the font collections in the popover menu.
+ */
+ void display_font_collections();
+
+ /**
+ * 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);
+
+ /**
+ * This function would disable undo and redo if the text_view widget is in focus
+ * It is to fix the issue: https://gitlab.com/inkscape/inkscape/-/issues/744
+ */
+ bool captureUndo(GdkEventKey *event);
+
+ /**
+ * 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);
+
+ // Callback to handle changes in the search entry.
+ void on_search_entry_changed();
+ void on_reset_button_pressed();
+ void change_font_count_label();
+ void on_fcm_button_clicked();
+
+ /**
+ * 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 ();
+
+private:
+
+ /*
+ * All the dialogs widgets
+ */
+
+ // Tab 1: Font ---------------------- //
+ Gtk::Box *settings_and_filters_box;
+ Gtk::MenuButton *filter_menu_button;
+ Gtk::Button *reset_button;
+ Gtk::SearchEntry *search_entry;
+ Gtk::Label *font_count_label;
+ Gtk::Popover *filter_popover;
+ Gtk::Box *popover_box;
+ Gtk::Frame *frame;
+ Gtk::Label *frame_label;
+ Gtk::Button *collection_editor_button;
+ Gtk::ListBox *collections_list;
+
+ Inkscape::UI::Widget::FontSelector font_selector;
+ Inkscape::UI::Widget::FontVariations font_variations;
+ Gtk::Label *preview_label; // Share with variants tab?
+
+ // Tab 2: Text ---------------------- //
+ Gtk::TextView *text_view;
+ Glib::RefPtr<Gtk::TextBuffer> text_buffer;
+
+ // Tab 3: Features ----------------- //
+ Inkscape::UI::Widget::FontVariants font_features;
+ Gtk::Label *preview_label2; // Could reparent preview_label but having a second label is probably easier.
+
+ // Shared ------- ------------------ //
+ Gtk::Button *setasdefault_button;
+ Gtk::Button *apply_button;
+
+ // Signals
+ sigc::connection selectChangedConn;
+ sigc::connection subselChangedConn;
+ sigc::connection selectModifiedConn;
+ sigc::connection fontChangedConn;
+ sigc::connection fontFeaturesChangedConn;
+ auto_connection fontCollectionsChangedSelection;
+ auto_connection fontCollectionsUpdate;
+
+ // Other
+ double selected_fontsize;
+ bool blocked;
+ const Glib::ustring samplephrase;
+
+ // Track undo and redo keyboard shortcuts
+ Util::ActionAccel _undo, _redo;
+};
+
+} //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..661c4f6
--- /dev/null
+++ b/src/ui/dialog/tile.cpp
@@ -0,0 +1,136 @@
+// 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 "tile.h"
+
+#include <glibmm/i18n.h>
+
+#include "ui/dialog/grid-arrange-tab.h"
+#include "ui/dialog/polar-arrange-tab.h"
+#include "ui/dialog/align-and-distribute.h"
+#include "ui/icon-names.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+Gtk::Box& create_tab_label(const char* label_text, const char* icon_name) {
+ auto box = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL, 4);
+ auto image = Gtk::make_managed<Gtk::Image>();
+ image->set_from_icon_name(icon_name, Gtk::ICON_SIZE_MENU);
+ auto label = Gtk::make_managed<Gtk::Label>(label_text, true);
+ box->pack_start(*image, false, true);
+ box->pack_start(*label, false, true);
+ box->show_all();
+ return *box;
+}
+
+ArrangeDialog::ArrangeDialog()
+ : DialogBase("/dialogs/gridtiler", "AlignDistribute")
+{
+ _align_tab = Gtk::manage(new AlignAndDistribute(this));
+ _arrangeBox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
+ _arrangeBox->set_valign(Gtk::ALIGN_START);
+ _notebook = Gtk::manage(new Gtk::Notebook());
+ _gridArrangeTab = Gtk::manage(new GridArrangeTab(this));
+ _polarArrangeTab = Gtk::manage(new PolarArrangeTab(this));
+
+ set_valign(Gtk::ALIGN_START);
+
+ _notebook->set_valign(Gtk::ALIGN_START);
+ _notebook->append_page(*_align_tab, create_tab_label(C_("Arrange dialog", "Align"), INKSCAPE_ICON("dialog-align-and-distribute")));
+ // TRANSLATORS: "Grid" refers to grid (columns/rows) arrangement
+ _notebook->append_page(*_gridArrangeTab, create_tab_label(C_("Arrange dialog", "Grid"), INKSCAPE_ICON("arrange-grid")));
+ // TRANSLATORS: "Circular" refers to circular/radial arrangement
+ _notebook->append_page(*_polarArrangeTab, create_tab_label(C_("Arrange dialog", "Circular"), INKSCAPE_ICON("arrange-circular")));
+ _arrangeBox->pack_start(*_notebook);
+ _notebook->signal_switch_page().connect([=](Widget*, guint page){
+ update_arrange_btn();
+ });
+ pack_start(*_arrangeBox);
+
+ // Add button
+ _arrangeButton = Gtk::manage(new Gtk::Button(C_("Arrange dialog", "_Arrange")));
+ _arrangeButton->signal_clicked().connect(sigc::mem_fun(*this, &ArrangeDialog::_apply));
+ _arrangeButton->set_use_underline(true);
+ _arrangeButton->set_tooltip_text(_("Arrange selected objects"));
+ _arrangeButton->get_style_context()->add_class("wide-apply-button");
+ _arrangeButton->set_no_show_all();
+
+ Gtk::ButtonBox *button_box = Gtk::manage(new Gtk::ButtonBox());
+ button_box->set_layout(Gtk::BUTTONBOX_CENTER);
+ button_box->set_spacing(6);
+ button_box->set_border_width(4);
+ button_box->set_valign(Gtk::ALIGN_FILL);
+
+ button_box->pack_end(*_arrangeButton);
+ pack_start(*button_box);
+
+ show();
+ show_all_children();
+ update_arrange_btn();
+}
+
+void ArrangeDialog::update_arrange_btn() {
+ // "align" page doesn't use "Arrange" button
+ if (_notebook->get_current_page() == 0) {
+ _arrangeButton->hide();
+ }
+ else {
+ _arrangeButton->show();
+ }
+}
+
+ArrangeDialog::~ArrangeDialog()
+{ }
+
+void ArrangeDialog::_apply()
+{
+ switch(_notebook->get_current_page())
+ {
+ case 0:
+ // not applicable to align panel
+ break;
+ case 1:
+ _gridArrangeTab->arrange();
+ break;
+ case 2:
+ _polarArrangeTab->arrange();
+ break;
+ }
+}
+
+void ArrangeDialog::desktopReplaced()
+{
+ _gridArrangeTab->setDesktop(getDesktop());
+ _align_tab->desktop_changed(getDesktop());
+}
+
+} //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..8f1313f
--- /dev/null
+++ b/src/ui/dialog/tile.h
@@ -0,0 +1,81 @@
+// 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/checkbutton.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/radiobutton.h>
+
+#include "ui/dialog/dialog-base.h"
+
+namespace Gtk {
+class Button;
+class Grid;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class AlignAndDistribute;
+class ArrangeTab;
+class GridArrangeTab;
+class PolarArrangeTab;
+
+class ArrangeDialog : public DialogBase
+{
+public:
+ ArrangeDialog();
+ ~ArrangeDialog() override;
+
+ void desktopReplaced() override;
+
+ void update_arrange_btn();
+
+ /**
+ * Callback from Apply
+ */
+ void _apply();
+
+private:
+ Gtk::Box *_arrangeBox;
+ Gtk::Notebook *_notebook;
+ AlignAndDistribute* _align_tab;
+ GridArrangeTab *_gridArrangeTab;
+ PolarArrangeTab *_polarArrangeTab;
+ Gtk::Button *_arrangeButton;
+};
+
+} //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..6f173aa
--- /dev/null
+++ b/src/ui/dialog/tracedialog.cpp
@@ -0,0 +1,553 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Bitmap tracing settings dialog - second implementation.
+ */
+/* Authors:
+ * Marc Jeanmougin <marc.jeanmougin@telecom-paristech.fr>
+ * PBS <pbs3141@gmail.com>
+ *
+ * Copyright (C) 2019-2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "tracedialog.h"
+
+#include <glibmm/i18n.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/spinbutton.h>
+#include <gtkmm/progressbar.h>
+#include <gtkmm/stack.h>
+
+#include "desktop.h"
+#include "io/resource.h"
+#include "selection.h"
+#include "trace/autotrace/inkscape-autotrace.h"
+#include "trace/depixelize/inkscape-depixelize.h"
+#include "trace/potrace/inkscape-potrace.h"
+#include "ui/util.h"
+
+// This maps the column ids in the glade file to useful enums
+static const std::map<std::string, Inkscape::Trace::Potrace::TraceType> trace_types = {
+ {"SS_BC", Inkscape::Trace::Potrace::TraceType::BRIGHTNESS},
+ {"SS_ED", Inkscape::Trace::Potrace::TraceType::CANNY},
+ {"SS_CQ", Inkscape::Trace::Potrace::TraceType::QUANT},
+ {"SS_AT", Inkscape::Trace::Potrace::TraceType::AUTOTRACE_SINGLE},
+ {"SS_CT", Inkscape::Trace::Potrace::TraceType::AUTOTRACE_CENTERLINE},
+
+ {"MS_BS", Inkscape::Trace::Potrace::TraceType::BRIGHTNESS_MULTI},
+ {"MS_C", Inkscape::Trace::Potrace::TraceType::QUANT_COLOR},
+ {"MS_BW", Inkscape::Trace::Potrace::TraceType::QUANT_MONO},
+ {"MS_AT", Inkscape::Trace::Potrace::TraceType::AUTOTRACE_MULTI},
+};
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+enum class EngineType
+{
+ Potrace,
+ Autotrace,
+ Depixelize
+};
+
+struct TraceData
+{
+ std::unique_ptr<Trace::TracingEngine> engine;
+ bool sioxEnabled;
+};
+
+class TraceDialogImpl
+ : public TraceDialog
+{
+public:
+ TraceDialogImpl();
+ ~TraceDialogImpl() override;
+
+protected:
+ void selectionModified(Selection *selection, unsigned flags) override;
+ void selectionChanged(Selection *selection) override;
+
+private:
+ TraceData getTraceData() const;
+ bool paintPreview(Cairo::RefPtr<Cairo::Context> const &cr);
+ void setDefaults();
+ void adjustParamsVisible();
+ void onTraceClicked();
+ void onAbortClicked();
+ bool previewsEnabled() const;
+ void schedulePreviewUpdate(int msecs, bool force = false);
+ void updatePreview(bool force = false);
+ void launchPreviewGeneration();
+
+ // Handles to ongoing asynchronous computations.
+ Trace::TraceFuture trace_future;
+ Trace::TraceFuture preview_future;
+
+ // Delayed preview generation.
+ sigc::connection preview_timeout_conn;
+ bool preview_pending_recompute = false;
+ Glib::RefPtr<Gdk::Pixbuf> preview_image;
+
+ 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_PA_optimize, /* *CB_live,*/ *CB_SIOX;
+ Gtk::CheckButton* CB_SIOX1;
+ Gtk::CheckButton* CB_speckles1;
+ Gtk::CheckButton* CB_smooth1;
+ Gtk::CheckButton* CB_optimize1;
+ Gtk::RadioButton *RB_PA_voronoi;
+ Gtk::Button *B_RESET, *B_STOP, *B_OK, *B_Update;
+ Gtk::Box *mainBox;
+ Gtk::Notebook *choice_tab;
+ Gtk::DrawingArea *previewArea;
+ Gtk::Box* orient_box;
+ Gtk::Frame* _preview_frame;
+ Gtk::Grid* _param_grid;
+ Gtk::CheckButton* _live_preview;
+ Gtk::Stack *stack;
+ Gtk::ProgressBar *progressbar;
+ Gtk::Box *boxchild1, *boxchild2;
+};
+
+enum class Page
+{
+ SingleScan,
+ MultiScan,
+ PixelArt
+};
+
+TraceData TraceDialogImpl::getTraceData() const
+{
+ auto current_page = static_cast<Page>(choice_tab->get_current_page());
+
+ auto cb_siox = current_page == Page::SingleScan ? CB_SIOX : CB_SIOX1;
+ bool enable_siox = cb_siox->get_active();
+
+ auto trace_type_str = current_page == Page::SingleScan ? CBT_SS->get_active_id() : CBT_MS->get_active_id();
+ auto it = trace_types.find(trace_type_str);
+ assert(it != trace_types.end());
+ auto trace_type = it->second;
+
+ EngineType engine_type;
+ if (current_page == Page::PixelArt) {
+ engine_type = EngineType::Depixelize;
+ } else {
+ switch (trace_type) {
+ case Inkscape::Trace::Potrace::TraceType::AUTOTRACE_SINGLE:
+ case Inkscape::Trace::Potrace::TraceType::AUTOTRACE_CENTERLINE:
+ case Inkscape::Trace::Potrace::TraceType::AUTOTRACE_MULTI:
+ engine_type = EngineType::Autotrace;
+ break;
+ default:
+ engine_type = EngineType::Potrace;
+ break;
+ }
+ }
+
+ auto setup_potrace = [&, this] {
+ auto eng = std::make_unique<Trace::Potrace::PotraceTracingEngine>(
+ trace_type, 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());
+
+ auto cb_optimize = current_page == Page::SingleScan ? CB_optimize : CB_optimize1;
+ eng->setOptiCurve(cb_optimize->get_active());
+ eng->setOptTolerance(optimize->get_value());
+
+ auto cb_smooth = current_page == Page::SingleScan ? CB_smooth : CB_smooth1;
+ eng->setAlphaMax(cb_smooth->get_active() ? smooth->get_value() : 0);
+
+ auto cb_speckles = current_page == Page::SingleScan ? CB_speckles : CB_speckles1;
+ eng->setTurdSize(cb_speckles->get_active() ? (int)speckles->get_value() : 0);
+
+ return eng;
+ };
+
+ auto setup_autotrace = [&, this] {
+ auto eng = std::make_unique<Trace::Autotrace::AutotraceTracingEngine>();
+
+ switch (trace_type) {
+ case Inkscape::Trace::Potrace::TraceType::AUTOTRACE_SINGLE:
+ eng->setColorCount(2);
+ break;
+ case Inkscape::Trace::Potrace::TraceType::AUTOTRACE_CENTERLINE:
+ eng->setColorCount(2);
+ eng->setCenterLine(true);
+ eng->setPreserveWidth(true);
+ break;
+ case Inkscape::Trace::Potrace::TraceType::AUTOTRACE_MULTI:
+ eng->setColorCount((int)MS_scans->get_value() + 1);
+ break;
+ default:
+ assert(false);
+ break;
+ }
+
+ eng->setFilterIterations((int)SS_AT_FI_T->get_value());
+ eng->setErrorThreshold(SS_AT_ET_T->get_value());
+
+ return eng;
+ };
+
+ auto setup_depixelize = [this] {
+ return std::make_unique<Trace::Depixelize::DepixelizeTracingEngine>(
+ RB_PA_voronoi->get_active() ? Inkscape::Trace::Depixelize::TraceType::VORONOI : Inkscape::Trace::Depixelize::TraceType::BSPLINES,
+ PA_curves->get_value(), (int) PA_islands->get_value(),
+ (int) PA_sparse1->get_value(), PA_sparse2->get_value(),
+ CB_PA_optimize->get_active());
+ };
+
+ TraceData data;
+ switch (engine_type) {
+ case EngineType::Potrace: data.engine = setup_potrace(); break;
+ case EngineType::Autotrace: data.engine = setup_autotrace(); break;
+ case EngineType::Depixelize: data.engine = setup_depixelize(); break;
+ default: assert(false); break;
+ }
+ data.sioxEnabled = enable_siox;
+
+ return data;
+}
+
+bool TraceDialogImpl::paintPreview(Cairo::RefPtr<Cairo::Context> const &cr)
+{
+ if (preview_image) {
+ int width = preview_image->get_width();
+ int height = preview_image->get_height();
+ Gtk::Allocation const &allocation = previewArea->get_allocation();
+ double scaleFX = (double)allocation.get_width() / width;
+ double scaleFY = (double)allocation.get_height() / height;
+ double scaleFactor = std::min(scaleFX, scaleFY);
+ int newWidth = (double)width * scaleFactor;
+ int newHeight = (double)height * scaleFactor;
+ int offsetX = (allocation.get_width() - newWidth) / 2;
+ int offsetY = (allocation.get_height() - newHeight) / 2;
+ cr->scale(scaleFactor, scaleFactor);
+ Gdk::Cairo::set_source_pixbuf(cr, preview_image, offsetX / scaleFactor, offsetY / scaleFactor);
+ cr->paint();
+ } else {
+ cr->set_source_rgba(0, 0, 0, 0);
+ cr->paint();
+ }
+
+ return false;
+}
+
+void TraceDialogImpl::selectionChanged(Inkscape::Selection *selection)
+{
+ updatePreview();
+}
+
+void TraceDialogImpl::selectionModified(Selection *selection, unsigned flags)
+{
+ auto mask = SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_PARENT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG;
+ if ((flags & mask) == mask) {
+ // All flags set - preview instantly.
+ updatePreview();
+ } else if (flags & mask) {
+ // At least one flag set - preview after a long delay.
+ schedulePreviewUpdate(1000);
+ }
+}
+
+void TraceDialogImpl::setDefaults()
+{
+ 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_speckles1->set_active(true);
+ CB_smooth1->set_active(true);
+ CB_optimize1->set_active(true);
+ CB_PA_optimize->set_active(false);
+ CB_SIOX->set_active(false);
+ CB_SIOX1->set_active(false);
+}
+
+void TraceDialogImpl::onAbortClicked()
+{
+ if (!trace_future) {
+ // Not tracing; nothing to cancel.
+ return;
+ }
+
+ stack->set_visible_child(*boxchild1);
+ if (auto desktop = getDesktop()) desktop->clearWaitingCursor();
+ trace_future.cancel();
+}
+
+void TraceDialogImpl::onTraceClicked()
+{
+ if (trace_future) {
+ // Still tracing; wait for either finished or cancelled.
+ return;
+ }
+
+ // Attempt to fire off the tracer.
+ auto data = getTraceData();
+ trace_future = Trace::trace(std::move(data.engine), data.sioxEnabled,
+ // On progress:
+ [this] (double progress) {
+ progressbar->set_fraction(progress);
+ },
+ // On completion without cancelling:
+ [this] {
+ progressbar->set_fraction(1.0);
+ stack->set_visible_child(*boxchild1);
+ if (auto desktop = getDesktop()) desktop->clearWaitingCursor();
+ trace_future.cancel();
+ }
+ );
+
+ if (trace_future) {
+ // Put the UI into the tracing state.
+ if (auto desktop = getDesktop()) desktop->setWaitingCursor();
+ stack->set_visible_child(*boxchild2);
+ progressbar->set_fraction(0.0);
+ }
+}
+
+TraceDialogImpl::TraceDialogImpl()
+{
+ Glib::ustring const 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_speckles1", "CB_smooth1", "CB_optimize1", "CB_SIOX1",
+ "CB_PA_optimize", /*"CB_live",*/ "CB_SIOX", "CBT_SS", "CBT_MS",
+ "B_RESET", "B_STOP", "B_OK", "mainBox", "choice_tab",
+ /*"choice_scan",*/ "previewArea", "_live_preview",
+ "stack", "progressbar", "boxchild1", "boxchild2" };
+ auto gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "dialog-trace.glade");
+ try {
+ builder = Gtk::Builder::create_from_file(gladefile);
+ } catch (Glib::Error const &) {
+ g_warning("Trace dialog: Glade file loading failed");
+ return;
+ }
+
+ for (auto &w : req_widgets) {
+ auto test = builder->get_object(w);
+ if (!test) {
+ g_warning("Trace dialog: Required widget %s does not exist", w.c_str());
+ return;
+ }
+ }
+
+#define GET_O(name) name = Glib::RefPtr<Gtk::Adjustment>::cast_dynamic(builder->get_object(#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)
+#undef GET_O
+#define GET_W(name) builder->get_widget(#name, name);
+ 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_speckles1)
+ GET_W(CB_smooth1)
+ GET_W(CB_optimize1)
+ GET_W(CB_PA_optimize)
+ GET_W(CB_SIOX)
+ GET_W(CB_SIOX1)
+ 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(previewArea)
+ GET_W(orient_box)
+ GET_W(_preview_frame)
+ GET_W(_param_grid)
+ GET_W(_live_preview)
+ GET_W(stack)
+ GET_W(progressbar)
+ GET_W(boxchild1)
+ GET_W(boxchild2)
+#undef GET_W
+ add(*mainBox);
+
+ Inkscape::Preferences* prefs = Inkscape::Preferences::get();
+
+ _live_preview->set_active(prefs->getBool(getPrefsPath() + "liveUpdate", true));
+
+ B_Update->signal_clicked().connect([=] { updatePreview(true); });
+ B_OK->signal_clicked().connect(sigc::mem_fun(*this, &TraceDialogImpl::onTraceClicked));
+ B_STOP->signal_clicked().connect(sigc::mem_fun(*this, &TraceDialogImpl::onAbortClicked));
+ B_RESET->signal_clicked().connect(sigc::mem_fun(*this, &TraceDialogImpl::setDefaults));
+ previewArea->signal_draw().connect(sigc::mem_fun(*this, &TraceDialogImpl::paintPreview));
+
+ // attempt at making UI responsive: relocate preview to the right or bottom of dialog depending on dialog size
+ signal_size_allocate().connect([=] (Gtk::Allocation const &alloc) {
+ // skip bogus sizes
+ if (alloc.get_width() < 10 || alloc.get_height() < 10) return;
+ // ratio: is dialog wide or is it tall?
+ double const ratio = alloc.get_width() / static_cast<double>(alloc.get_height());
+ // g_warning("size alloc: %d x %d - %f", alloc.get_width(), alloc.get_height(), ratio);
+ double constexpr hysteresis = 0.01;
+ if (ratio < 1 - hysteresis) {
+ // narrow/tall
+ choice_tab->set_valign(Gtk::ALIGN_START);
+ orient_box->set_orientation(Gtk::ORIENTATION_VERTICAL);
+ }
+ else if (ratio > 1 + hysteresis) {
+ // wide/short
+ orient_box->set_orientation(Gtk::ORIENTATION_HORIZONTAL);
+ choice_tab->set_valign(Gtk::ALIGN_FILL);
+ }
+ });
+
+ CBT_SS->signal_changed().connect([=] { adjustParamsVisible(); });
+ adjustParamsVisible();
+
+ // watch for changes, but only in params that can impact preview bitmap
+ for (auto adj : {SS_BC_T, SS_ED_T, SS_CQ_T, SS_AT_FI_T, SS_AT_ET_T, /* optimize, smooth, speckles,*/ MS_scans, PA_curves, PA_islands, PA_sparse1, PA_sparse2 }) {
+ adj->signal_value_changed().connect([=] { updatePreview(); });
+ }
+ for (auto checkbtn : {CB_invert, CB_MS_rb, /* CB_MS_smooth, CB_MS_stack, CB_optimize1, CB_optimize, */ CB_PA_optimize, CB_SIOX1, CB_SIOX, /* CB_smooth1, CB_smooth, CB_speckles1, CB_speckles, */ _live_preview}) {
+ checkbtn->signal_toggled().connect([=] { updatePreview(); });
+ }
+ for (auto combo : {CBT_SS, CBT_MS}) {
+ combo->signal_changed().connect([=] { updatePreview(); });
+ }
+ choice_tab->signal_switch_page().connect([=] (Gtk::Widget*, unsigned) { updatePreview(); });
+
+ signal_set_focus_child().connect([=] (Gtk::Widget *w) {
+ if (w) updatePreview();
+ });
+}
+
+TraceDialogImpl::~TraceDialogImpl()
+{
+ Inkscape::Preferences* prefs = Inkscape::Preferences::get();
+ prefs->setBool(getPrefsPath() + "liveUpdate", _live_preview->get_active());
+ preview_timeout_conn.disconnect();
+}
+
+bool TraceDialogImpl::previewsEnabled() const
+{
+ return _live_preview->get_active() && is_widget_effectively_visible(this);
+}
+
+void TraceDialogImpl::schedulePreviewUpdate(int msecs, bool force)
+{
+ if (!previewsEnabled() && !force) {
+ return;
+ }
+
+ // Restart timeout.
+ preview_timeout_conn.disconnect();
+ preview_timeout_conn = Glib::signal_timeout().connect([this] {
+ updatePreview(true);
+ return false;
+ }, msecs);
+}
+
+void TraceDialogImpl::updatePreview(bool force)
+{
+ if (!previewsEnabled() && !force) {
+ return;
+ }
+
+ preview_timeout_conn.disconnect();
+
+ if (preview_future) {
+ // Preview generation already running - flag for recomputation when finished.
+ preview_pending_recompute = true;
+ return;
+ }
+
+ preview_pending_recompute = false;
+
+ auto data = getTraceData();
+ preview_future = Trace::preview(std::move(data.engine), data.sioxEnabled,
+ // On completion:
+ [this] (Glib::RefPtr<Gdk::Pixbuf> result) {
+ preview_image = std::move(result);
+ previewArea->queue_draw();
+ preview_future.cancel();
+
+ // Recompute if invalidated during computation.
+ if (preview_pending_recompute) {
+ updatePreview();
+ }
+ }
+ );
+
+ if (!preview_future) {
+ // On instant failure:
+ preview_image.reset();
+ previewArea->queue_draw();
+ }
+}
+
+void TraceDialogImpl::adjustParamsVisible()
+{
+ int constexpr start_row = 2;
+ int option = CBT_SS->get_active_row_number();
+ if (option >= 3) option = 3;
+ int show1 = start_row + option;
+ int show2 = show1;
+ if (option == 3) ++show2;
+
+ for (int row = start_row; row < start_row + 5; ++row) {
+ for (int col = 0; col < 4; ++col) {
+ if (auto widget = _param_grid->get_child_at(col, row)) {
+ if (row == show1 || row == show2) {
+ widget->show();
+ } else {
+ widget->hide();
+ }
+ }
+ }
+ }
+}
+
+std::unique_ptr<TraceDialog> TraceDialog::create()
+{
+ return std::make_unique<TraceDialogImpl>();
+}
+
+} // 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..6900a52
--- /dev/null
+++ b/src/ui/dialog/tracedialog.h
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Bitmap tracing settings dialog
+ */
+/* Authors:
+ * Bob Jamison
+ * Others - see git history.
+ *
+ * Copyright (C) 2004-2022 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_TRACE_H
+#define INKSCAPE_UI_DIALOG_TRACE_H
+
+#include <memory>
+#include "ui/dialog/dialog-base.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class TraceDialog : public DialogBase
+{
+public:
+ static std::unique_ptr<TraceDialog> create();
+
+protected:
+ TraceDialog() : DialogBase("/dialogs/trace", "Trace") {}
+};
+
+} //namespace Dialog
+} //namespace UI
+} //namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_TRACE_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..9426667
--- /dev/null
+++ b/src/ui/dialog/transformation.cpp
@@ -0,0 +1,1216 @@
+// 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 "transformation.h"
+
+#include <gtkmm/dialog.h>
+
+#include <2geom/transforms.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "message-stack.h"
+#include "selection-chemistry.h"
+
+#include "object/algorithms/bboxsort.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 {
+
+
+/*########################################################################
+# C O N S T R U C T O R
+########################################################################*/
+
+Transformation::Transformation()
+ : DialogBase("/dialogs/transformation", "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 ("", _("Transformation matrix element A")),
+ _scalar_transform_b ("", _("Transformation matrix element B")),
+ _scalar_transform_c ("", _("Transformation matrix element C")),
+ _scalar_transform_d ("", _("Transformation matrix element D")),
+ _scalar_transform_e ("", _("Transformation matrix element E"), UNIT_TYPE_LINEAR, "", "", &_units_transform),
+ _scalar_transform_f ("", _("Transformation matrix element F"), UNIT_TYPE_LINEAR, "", "", &_units_transform),
+
+ _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"));
+
+ set_spacing(0);
+
+ // Notebook for individual transformations
+ 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();
+
+ _tabSwitchConn = _notebook.signal_switch_page().connect(sigc::mem_fun(*this, &Transformation::onSwitchPage));
+
+ // Apply separately
+ 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()))->signal_activate().connect(sigc::mem_fun(*this, &Transformation::_apply));
+ ((Gtk::Entry *) (_scalar_move_vertical.getWidget()))->signal_activate().connect(sigc::mem_fun(*this, &Transformation::_apply));
+ ((Gtk::Entry *) (_scalar_scale_horizontal.getWidget()))->signal_activate().connect(sigc::mem_fun(*this, &Transformation::_apply));
+ ((Gtk::Entry *) (_scalar_scale_vertical.getWidget()))->signal_activate().connect(sigc::mem_fun(*this, &Transformation::_apply));
+ ((Gtk::Entry *) (_scalar_rotate.getWidget()))->signal_activate().connect(sigc::mem_fun(*this, &Transformation::_apply));
+ ((Gtk::Entry *) (_scalar_skew_horizontal.getWidget()))->signal_activate().connect(sigc::mem_fun(*this, &Transformation::_apply));
+ ((Gtk::Entry *) (_scalar_skew_vertical.getWidget()))->signal_activate().connect(sigc::mem_fun(*this, &Transformation::_apply));
+
+ resetButton = Gtk::manage(new Gtk::Button());
+ resetButton->set_image_from_icon_name("reset-settings-symbolic");
+ resetButton->set_size_request(30, -1);
+ resetButton->set_halign(Gtk::ALIGN_CENTER);
+ resetButton->set_use_underline();
+ 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 = Gtk::manage(new Gtk::Button(_("_Apply")));
+ applyButton->set_use_underline();
+ applyButton->set_halign(Gtk::ALIGN_CENTER);
+ applyButton->set_tooltip_text(_("Apply transformation to selection"));
+ applyButton->set_sensitive(false);
+ applyButton->signal_clicked().connect(sigc::mem_fun(*this, &Transformation::_apply));
+ applyButton->get_style_context()->add_class("wide-apply-button");
+
+ auto button_box = Gtk::manage(new Gtk::Box());
+ button_box->set_margin_top(4);
+ button_box->set_spacing(8);
+ button_box->set_halign(Gtk::ALIGN_CENTER);
+ button_box->pack_start(*applyButton);
+ button_box->pack_start(*resetButton);
+ pack_start(*button_box, Gtk::PACK_SHRINK, 0);
+
+ show_all_children();
+}
+
+Transformation::~Transformation()
+{
+ _tabSwitchConn.disconnect();
+}
+
+void Transformation::selectionChanged(Inkscape::Selection *selection)
+{
+ updateSelection((Inkscape::UI::Dialog::Transformation::PageType)getCurrentPage(), selection);
+}
+void Transformation::selectionModified(Inkscape::Selection *selection, guint flags)
+{
+ selectionChanged(selection);
+}
+
+/*########################################################################
+# U T I L I T Y
+########################################################################*/
+
+void Transformation::presentPage(Transformation::PageType page)
+{
+ _notebook.set_current_page(page);
+ show();
+}
+
+
+
+
+/*########################################################################
+# S E T U P L A Y O U T
+########################################################################*/
+
+
+void Transformation::layoutPageMove()
+{
+ _units_move.setUnitType(UNIT_TYPE_LINEAR);
+
+ _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_horizontal.setWidthChars(7);
+
+ _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.setWidthChars(7);
+
+ //_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_horizontal.setWidthChars(7);
+
+ _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();
+ _scalar_scale_vertical.setWidthChars(7);
+
+ _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);
+
+ auto box = Gtk::make_managed<Gtk::Box>();
+ _counterclockwise_rotate.set_halign(Gtk::ALIGN_START);
+ _clockwise_rotate.set_halign(Gtk::ALIGN_START);
+ box->pack_start(_counterclockwise_rotate);
+ box->pack_start(_clockwise_rotate);
+
+ _page_rotate.table().attach(_scalar_rotate, 0, 0, 1, 1);
+ _page_rotate.table().attach(_units_rotate, 1, 0, 1, 1);
+ _page_rotate.table().attach(*box, 1, 1, 1, 1);
+
+ _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_horizontal.setWidthChars(7);
+
+ _scalar_skew_vertical.initScalar(-1e6, 1e6);
+ _scalar_skew_vertical.setDigits(3);
+ _scalar_skew_vertical.setIncrements(0.1, 1.0);
+ _scalar_skew_vertical.set_hexpand();
+ _scalar_skew_vertical.setWidthChars(7);
+
+ _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()
+{
+ _units_transform.setUnitType(UNIT_TYPE_LINEAR);
+ _units_transform.set_tooltip_text(_("E and F units"));
+ _units_transform.set_halign(Gtk::ALIGN_END);
+ _units_transform.set_margin_top(3);
+ _units_transform.set_margin_bottom(3);
+
+ UI::Widget::Scalar* labels[] = {&_scalar_transform_a, &_scalar_transform_b, &_scalar_transform_c, &_scalar_transform_d, &_scalar_transform_e, &_scalar_transform_f};
+ for (auto label : labels) {
+ label->hide_label();
+ label->set_margin_start(2);
+ label->set_margin_end(2);
+ }
+ _page_transform.table().set_column_spacing(0);
+ _page_transform.table().set_row_spacing(1);
+ _page_transform.table().set_column_homogeneous(true);
+
+ _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(*Gtk::make_managed<Gtk::Label>("A:"), 0, 0, 1, 1);
+ _page_transform.table().attach(_scalar_transform_a, 0, 1, 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(*Gtk::make_managed<Gtk::Label>("B:"), 0, 2, 1, 1);
+ _page_transform.table().attach(_scalar_transform_b, 0, 3, 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(*Gtk::make_managed<Gtk::Label>("C:"), 1, 0, 1, 1);
+ _page_transform.table().attach(_scalar_transform_c, 1, 1, 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(*Gtk::make_managed<Gtk::Label>("D:"), 1, 2, 1, 1);
+ _page_transform.table().attach(_scalar_transform_d, 1, 3, 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(*Gtk::make_managed<Gtk::Label>("E:"), 2, 0, 1, 1);
+ _page_transform.table().attach(_scalar_transform_e, 2, 1, 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(*Gtk::make_managed<Gtk::Label>("F:"), 2, 2, 1, 1);
+ _page_transform.table().attach(_scalar_transform_f, 2, 3, 1, 1);
+
+ auto img = Gtk::make_managed<Gtk::Image>();
+ img->set_from_icon_name("matrix-2d", Gtk::ICON_SIZE_BUTTON);
+ img->set_pixel_size(52);
+ img->set_margin_top(4);
+ img->set_margin_bottom(4);
+ _page_transform.table().attach(*img, 0, 5, 1, 1);
+
+ auto descr = Gtk::make_managed<Gtk::Label>();
+ descr->set_line_wrap();
+ descr->set_line_wrap_mode(Pango::WRAP_WORD);
+ descr->set_text(
+ _("<small>"
+ "<a href=\"https://www.w3.org/TR/SVG11/coords.html#TransformMatrixDefined\">"
+ "2D transformation matrix</a> that combines translation (E,F), scaling (A,D),"
+ " rotation (A-D) and shearing (B,C)."
+ "</small>")
+ );
+ descr->set_use_markup();
+ _page_transform.table().attach(*descr, 1, 5, 2, 1);
+
+ _page_transform.table().attach(_units_transform, 2, 4, 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, 4, 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)
+{
+ applyButton->set_sensitive(selection && !selection->isEmpty());
+
+ 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;
+ }
+ }
+}
+
+void Transformation::onSwitchPage(Gtk::Widget * /*page*/, guint pagenum)
+{
+ if (!getDesktop()) {
+ return;
+ }
+
+ 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], "px");
+ _scalar_transform_f.setValue(new_displayed[5], "px");
+ } 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()
+{
+ auto 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
+ applyButton->set_sensitive(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(), _("Move"), INKSCAPE_ICON("dialog-transform"));
+}
+
+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(), _("Scale"), INKSCAPE_ICON("dialog-transform"));
+}
+
+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 {
+ std::optional<Geom::Point> center = selection->center();
+ if (center) {
+ selection->rotateRelative(*center, angle);
+ }
+ }
+
+ DocumentUndo::done(selection->desktop()->getDocument(), _("Rotate"), INKSCAPE_ICON("dialog-transform"));
+}
+
+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();
+ std::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(), _("Skew"), INKSCAPE_ICON("dialog-transform"));
+}
+
+
+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("px");
+ double f = _scalar_transform_f.getValue("px");
+
+ 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(), _("Edit transformation matrix"), INKSCAPE_ICON("dialog-transform"));
+}
+
+
+
+
+
+/*########################################################################
+# V A L U E - C H A N G E D C A L L B A C K S
+########################################################################*/
+
+void Transformation::onMoveValueChanged()
+{
+ applyButton->set_sensitive(true);
+}
+
+void Transformation::onMoveRelativeToggled()
+{
+ auto 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);
+ }
+ }
+
+ applyButton->set_sensitive(true);
+}
+
+void Transformation::onScaleXValueChanged()
+{
+ if (_scalar_scale_horizontal.setProgrammatically) {
+ _scalar_scale_horizontal.setProgrammatically = false;
+ return;
+ }
+
+ applyButton->set_sensitive(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;
+ }
+
+ applyButton->set_sensitive(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()
+{
+ applyButton->set_sensitive(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()
+{
+ applyButton->set_sensitive(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);
+ */
+
+ applyButton->set_sensitive(true);
+}
+
+void Transformation::onReplaceMatrixToggled()
+{
+ auto 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("px");
+ double f = _scalar_transform_f.getValue("px");
+
+ 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], "px");
+ _scalar_transform_f.setValue(new_displayed[5], "px");
+}
+
+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: {
+ auto 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, "px");
+ _scalar_transform_f.setValue(0, "px");
+ break;
+ }
+ }
+}
+
+void Transformation::onApplySeparatelyToggled()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/dialogs/transformation/applyseparately", _check_apply_separately.get_active());
+}
+
+void Transformation::desktopReplaced()
+{
+ // Setting default unit to document unit
+ if (auto desktop = getDesktop()) {
+ SPNamedView *nv = desktop->getNamedView();
+ if (nv->display_units) {
+ _units_move.setUnit(nv->display_units->abbr);
+ _units_transform.setUnit(nv->display_units->abbr);
+ }
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/dialogs/transformation/rotateCounterClockwise", true) != desktop->is_yaxisdown()) {
+ _counterclockwise_rotate.set_active();
+ onRotateCounterclockwiseClicked();
+ } else {
+ _clockwise_rotate.set_active();
+ onRotateClockwiseClicked();
+ }
+
+ updateSelection(PAGE_MOVE, getSelection());
+ }
+}
+
+} // 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..457cee0
--- /dev/null
+++ b/src/ui/dialog/transformation.h
@@ -0,0 +1,231 @@
+// 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 <glibmm/i18n.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/radiobutton.h>
+
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/notebook-page.h"
+#include "ui/widget/scalar-unit.h"
+
+namespace Gtk {
+class Button;
+}
+
+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 DialogBase
+{
+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;
+
+ /**
+ * 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 desktopReplaced() override;
+ void selectionChanged(Inkscape::Selection *selection) override;
+ void selectionModified(Inkscape::Selection *selection, guint flags) override;
+ 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::UnitMenu _units_transform;
+
+ 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::ScalarUnit _scalar_transform_e;
+ UI::Widget::ScalarUnit _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;
+
+ /**
+ * Layout the GUI components, and prepare for use
+ */
+ void layoutPageMove();
+ void layoutPageScale();
+ void layoutPageRotate();
+ void layoutPageSkew();
+ void layoutPageTransform();
+
+ void _apply();
+ 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 *);
+
+private:
+ Gtk::Button *applyButton;
+ Gtk::Button *resetButton;
+
+ sigc::connection _selChangeConn;
+ sigc::connection _selModifyConn;
+ sigc::connection _tabSwitchConn;
+};
+
+} // 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..335ec68
--- /dev/null
+++ b/src/ui/dialog/undo-history.cpp
@@ -0,0 +1,356 @@
+// 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 "actions/actions-tools.h"
+#include "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "ui/icon-loader.h"
+#include "util/signal-blocker.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 there is no icon name.
+ if ( _property_icon_name == "") return;
+
+ // if the icon isn't cached, render it to a pixbuf
+ if ( !_icon_cache[_property_icon_name] ) {
+
+ Gtk::Image* icon = Gtk::manage(new Gtk::Image());
+ icon = sp_get_icon_image(_property_icon_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(_property_icon_name, 16);
+ } else {
+ delete icon;
+ return;
+ }
+
+ delete icon;
+ property_pixbuf() = _icon_cache[_property_icon_name] = _property_icon.get_value();
+ }
+
+ } else {
+ property_pixbuf() = _icon_cache[_property_icon_name];
+ }
+
+ 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()
+ : DialogBase("/dialogs/undo-history", "UndoHistory"),
+ _event_log(nullptr),
+ _scrolled_window(),
+ _event_list_store(),
+ _event_list_selection(_event_list_view.get_selection()),
+ _callback_connections()
+{
+ auto *_columns = &EventLog::getColumns();
+
+ set_size_request(-1, -1);
+
+ pack_start(_scrolled_window);
+ _scrolled_window.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
+
+ _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_icon_name(), _columns->icon_name);
+
+ 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);
+ _scrolled_window.set_overlay_scrolling(false);
+ // 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));
+
+ show_all_children();
+}
+
+UndoHistory::~UndoHistory()
+{
+ disconnectEventLog();
+}
+
+void UndoHistory::documentReplaced()
+{
+ disconnectEventLog();
+ if (auto document = getDocument()) {
+ g_assert (document->get_event_log() != nullptr);
+ SignalBlocker blocker(&_callback_connections[EventLog::CALLB_SELECTION_CHANGE]);
+ _event_list_view.unset_model();
+ connectEventLog();
+ }
+}
+
+void UndoHistory::disconnectEventLog()
+{
+ if (_event_log) {
+ _event_log->removeDialogConnection(&_event_list_view, &_callback_connections);
+ _event_log->remove_destroy_notify_callback(this);
+ }
+}
+
+void UndoHistory::connectEventLog()
+{
+ if (auto document = getDocument()) {
+ _event_log = document->get_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::_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(getDocument());
+ }
+ _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(getDocument());
+
+ 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 (last_selected && selected != last_selected ) {
+
+ DocumentUndo::redo(getDocument());
+
+ 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(getDocument());
+
+ for ( --last ; curr_event != last ; ++curr_event ) {
+ DocumentUndo::redo(getDocument());
+ }
+ _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..43c99cd
--- /dev/null
+++ b/src/ui/dialog/undo-history.h
@@ -0,0 +1,161 @@
+// 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 <functional>
+#include <glibmm/property.h>
+#include <gtkmm/cellrendererpixbuf.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/treemodel.h>
+#include <gtkmm/treeselection.h>
+#include <sstream>
+
+#include "event-log.h"
+#include "ui/dialog/dialog-base.h"
+
+
+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_icon_name(*this, "our-icon-name", "inkscape-logo") // icon-name/icon_name used by Gtk
+ { }
+
+ Glib::PropertyProxy<Glib::ustring>
+ property_icon_name() { return _property_icon_name.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<Glib::ustring> _property_icon_name;
+ std::map<Glib::ustring, Glib::RefPtr<Gdk::Pixbuf> > _icon_cache;
+
+};
+
+
+class CellRendererInt : public Gtk::CellRendererText {
+public:
+
+ struct Filter
+ {
+ 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 DialogBase
+{
+public:
+ UndoHistory();
+ ~UndoHistory() override;
+
+ void documentReplaced() override;
+
+protected:
+ EventLog *_event_log;
+
+ Gtk::ScrolledWindow _scrolled_window;
+
+ Glib::RefPtr<Gtk::TreeModel> _event_list_store;
+ Gtk::TreeView _event_list_view;
+ Glib::RefPtr<Gtk::TreeSelection> _event_list_selection;
+
+ EventLog::CallbackMap _callback_connections;
+
+ static void *_handleEventLogDestroyCB(void *data);
+
+ void disconnectEventLog();
+ void connectEventLog();
+
+ 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:
+ 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..242a5d4
--- /dev/null
+++ b/src/ui/dialog/xml-tree.cpp
@@ -0,0 +1,928 @@
+// 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
+ * Mike Kowalski
+ *
+ * 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 <glibmm/ustring.h>
+#include <gtkmm/button.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/image.h>
+#include <gtkmm/menubutton.h>
+#include <gtkmm/object.h>
+#include <gtkmm/paned.h>
+#include <gtkmm/radiomenuitem.h>
+#include <gtkmm/scrolledwindow.h>
+#include <memory>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "layer-manager.h"
+#include "message-context.h"
+#include "message-stack.h"
+
+#include "object/sp-root.h"
+#include "object/sp-string.h"
+
+#include "preferences.h"
+#include "ui/builder-utils.h"
+#include "ui/dialog-events.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "ui/tools/tool-base.h"
+#include "ui/syntax.h"
+
+#include "util/trim.h"
+#include "widgets/sp-xmlview-tree.h"
+
+namespace {
+/**
+ * Set the orientation of `paned` to vertical or horizontal, and make the first child resizable
+ * if vertical, and the second child resizable if horizontal.
+ * @pre `paned` has two children
+ */
+void paned_set_vertical(Gtk::Paned &paned, bool vertical)
+{
+ auto& first = *paned.get_child1();
+ auto& second = *paned.get_child2();
+ const int space = 1;
+ paned.child_property_resize(first) = vertical;
+ first.set_margin_bottom(vertical ? space : 0);
+ first.set_margin_end(vertical ? 0 : space);
+ second.set_margin_top(vertical ? space : 0);
+ second.set_margin_start(vertical ? 0 : space);
+ assert(paned.child_property_resize(second));
+ paned.set_orientation(vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL);
+}
+} // namespace
+
+namespace Inkscape::UI::Dialog {
+
+XmlTree::XmlTree()
+ : DialogBase("/dialogs/xml/", "XMLEditor")
+ , _builder(create_builder("dialog-xml.glade"))
+ , _paned(get_widget<Gtk::Paned>(_builder, "pane"))
+ , xml_element_new_button(get_widget<Gtk::Button>(_builder, "new-elem"))
+ , xml_text_new_button(get_widget<Gtk::Button>(_builder, "new-text"))
+ , xml_node_delete_button(get_widget<Gtk::Button>(_builder, "del"))
+ , xml_node_duplicate_button(get_widget<Gtk::Button>(_builder, "dup"))
+ , unindent_node_button(get_widget<Gtk::Button>(_builder, "unindent"))
+ , indent_node_button(get_widget<Gtk::Button>(_builder, "indent"))
+ , lower_node_button(get_widget<Gtk::Button>(_builder, "lower"))
+ , raise_node_button(get_widget<Gtk::Button>(_builder, "raise"))
+ , _syntax_theme("/theme/syntax-color-theme")
+ , _mono_font("/dialogs/xml/mono-font", false)
+{
+ /* 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") );
+
+ Gtk::ScrolledWindow& tree_scroller = get_widget<Gtk::ScrolledWindow>(_builder, "tree-wnd");
+ _treemm = Gtk::manage(Glib::wrap(GTK_TREE_VIEW(tree)));
+ tree_scroller.add(*Gtk::manage(Glib::wrap(GTK_WIDGET(tree))));
+ fix_inner_scroll(&tree_scroller);
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ /* attributes */
+ attributes = Gtk::make_managed<AttrDialog>();
+ attributes->set_margin_top(0);
+ attributes->set_margin_bottom(0);
+ attributes->set_margin_start(0);
+ attributes->set_margin_end(0);
+ attributes->get_scrolled_window().set_shadow_type(Gtk::SHADOW_IN);
+ attributes->show();
+ attributes->get_status_box().hide();
+ attributes->get_status_box().set_no_show_all();
+ _paned.pack2(*attributes, true, false);
+
+ /* Signal handlers */
+ _treemm->get_selection()->signal_changed().connect([=]() {
+ if (blocked || !getDesktop())
+ return;
+ if (!_tree_select_idle) {
+ // Defer the update after all events have been processed.
+ _tree_select_idle = Glib::signal_idle().connect(sigc::mem_fun(*this, &XmlTree::deferred_on_tree_select_row));
+ }
+ });
+ tree->connectTreeMove([=]() {
+ if (auto doc = getDocument()) {
+ DocumentUndo::done(doc, Q_("Undo History / XML Editor|Drag XML subtree"), INKSCAPE_ICON("dialog-xml-editor"));
+ }
+ });
+
+ 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));
+
+ set_name("XMLAndAttributesDialog");
+ set_spacing(0);
+ 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));
+
+ pack_start(get_widget<Gtk::Box>(_builder, "main"), true, true);
+
+ int min_width = 0, dummy;
+ get_preferred_width(min_width, dummy);
+
+ auto auto_arrange_panels = [=](Gtk::Allocation const &alloc) {
+ // skip bogus sizes
+ if (alloc.get_width() < 10 || alloc.get_height() < 10) return;
+
+ // minimal width times fudge factor to arrive at "narrow" dialog with automatic vertical layout:
+ const bool narrow = alloc.get_width() < min_width * 1.5;
+ paned_set_vertical(_paned, narrow);
+ };
+
+ auto arrange_panels = [=](DialogLayout layout){
+ switch (layout) {
+ case Auto:
+ auto_arrange_panels(get_allocation());
+ break;
+ case Horizontal:
+ paned_set_vertical(_paned, false);
+ break;
+ case Vertical:
+ paned_set_vertical(_paned, true);
+ break;
+ }
+ // ensure_size();
+ };
+
+ signal_size_allocate().connect([=] (Gtk::Allocation const &alloc) {
+ arrange_panels(_layout);
+ });
+
+ auto& popup = get_widget<Gtk::MenuButton>(_builder, "layout-btn");
+ popup.set_has_tooltip();
+ popup.signal_query_tooltip().connect([=](int x, int y, bool kbd, const Glib::RefPtr<Gtk::Tooltip>& tooltip){
+ auto tip = "";
+ switch (_layout) {
+ case Auto: tip = _("Automatic panel layout:\nchanges with dialog size");
+ break;
+ case Horizontal: tip = _("Horizontal panel layout");
+ break;
+ case Vertical: tip = _("Vertical panel layout");
+ break;
+ }
+ tooltip->set_text(tip);
+ return true;
+ });
+
+ auto set_layout = [=](DialogLayout layout){
+ Glib::ustring icon = "layout-auto";
+ if (layout == Horizontal) {
+ icon = "layout-horizontal";
+ } else if (layout == Vertical) {
+ icon = "layout-vertical";
+ }
+ get_widget<Gtk::Image>(_builder, "layout-img").set_from_icon_name(icon + "-symbolic", Gtk::ICON_SIZE_SMALL_TOOLBAR);
+ prefs->setInt("/dialogs/xml/layout", layout);
+ arrange_panels(layout);
+ _layout = layout;
+ };
+
+ auto menu_items = get_widget<Gtk::Menu>(_builder, "menu-popup").get_children();
+
+ DialogLayout layouts[] = {Auto, Horizontal, Vertical};
+ int index = 0;
+ for (auto item : menu_items) {
+ g_assert(index < 3);
+ auto layout = layouts[index++];
+ static_cast<Gtk::RadioMenuItem*>(item)->signal_activate().connect([=](){ set_layout(layout); });
+ }
+
+ _layout = static_cast<DialogLayout>(prefs->getIntLimited("/dialogs/xml/layout", Auto, Auto, Vertical));
+ static_cast<Gtk::RadioMenuItem*>(menu_items.at(_layout))->set_active();
+ set_layout(_layout);
+ // establish initial layout to prevent unwanted panels resize in auto layout mode
+ paned_set_vertical(_paned, true);
+
+ _syntax_theme.action = [=]() {
+ setSyntaxStyle(Inkscape::UI::Syntax::build_xml_styles(_syntax_theme));
+ // rebuild tree to change markup
+ rebuildTree();
+ };
+
+ setSyntaxStyle(Inkscape::UI::Syntax::build_xml_styles(_syntax_theme));
+
+ _mono_font.action = [=]() {
+ Glib::ustring mono("mono-font");
+ if (_mono_font) {
+ _treemm->get_style_context()->add_class(mono);
+ } else {
+ _treemm->get_style_context()->remove_class(mono);
+ }
+ attributes->set_mono_font(_mono_font);
+ };
+ _mono_font.action();
+
+ tree->renderer->signal_editing_canceled().connect([=]() {
+ stopNodeEditing(false, "", "");
+ });
+ tree->renderer->signal_edited().connect([=](const Glib::ustring& path, const Glib::ustring& name) {
+ stopNodeEditing(true, path, name);
+ });
+ tree->renderer->signal_editing_started().connect([=](Gtk::CellEditable* cell, const Glib::ustring& path) {
+ startNodeEditing(cell, path);
+ });
+}
+
+XmlTree::~XmlTree()
+{
+ unsetDocument();
+}
+
+void XmlTree::rebuildTree()
+{
+ sp_xmlview_tree_set_repr(tree, nullptr);
+ if (auto document = getDocument()) {
+ set_tree_repr(document->getReprRoot());
+ }
+}
+
+void XmlTree::_resized()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setInt("/dialogs/xml/panedpos", _paned.property_position());
+}
+
+void XmlTree::unsetDocument()
+{
+ _tree_select_idle.disconnect();
+}
+
+void XmlTree::documentReplaced()
+{
+ unsetDocument();
+ if (auto document = getDocument()) {
+ // TODO: Why is this a document property?
+ document->setXMLDialogSelectedObject(nullptr);
+
+ set_tree_repr(document->getReprRoot());
+ } else {
+ set_tree_repr(nullptr);
+ }
+}
+
+void XmlTree::selectionChanged(Selection *selection)
+{
+ if (!blocked++) {
+ Inkscape::XML::Node *node = get_dt_select();
+ set_tree_select(node);
+ }
+ blocked--;
+}
+
+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, bool edit)
+{
+ if (selected_repr) {
+ Inkscape::GC::release(selected_repr);
+ }
+ selected_repr = repr;
+ if (selected_repr) {
+ Inkscape::GC::anchor(selected_repr);
+ }
+ if (auto document = getDocument()) {
+ document->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);
+ auto col = gtk_tree_view_get_column(&tree->tree, 0);
+ gtk_tree_view_set_cursor(GTK_TREE_VIEW(tree), path, edit ? col : nullptr, edit);
+ 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::NodeType::ELEMENT_NODE ||
+ repr->type() == Inkscape::XML::NodeType::TEXT_NODE ||
+ repr->type() == Inkscape::XML::NodeType::COMMENT_NODE))
+ {
+ attributes->setRepr(repr);
+ } else {
+ attributes->setRepr(nullptr);
+ }
+}
+
+
+Inkscape::XML::Node *XmlTree::get_dt_select()
+{
+ if (auto selection = getSelection()) {
+ return selection->singleRepr();
+ }
+ return nullptr;
+}
+
+
+/**
+ * Like SPDesktop::isLayer(), but ignores SPGroup::effectiveLayerMode().
+ */
+static bool isRealLayer(SPObject const *object)
+{
+ auto group = cast<SPGroup>(object);
+ return group && group->layerMode() == SPGroup::LAYER;
+}
+
+void XmlTree::set_dt_select(Inkscape::XML::Node *repr)
+{
+ auto document = getDocument();
+ if (!document)
+ return;
+
+ SPObject *object;
+ if (repr) {
+ while ( ( repr->type() != Inkscape::XML::NodeType::ELEMENT_NODE )
+ && repr->parent() )
+ {
+ repr = repr->parent();
+ } // end of while loop
+
+ object = document->getObjectByRepr(repr);
+ } else {
+ object = nullptr;
+ }
+
+ blocked++;
+
+ if (!object || !in_dt_coordsys(*object)) {
+ // object not on canvas
+ } else if (isRealLayer(object)) {
+ getDesktop()->layerManager().setCurrentLayer(object);
+ } else {
+ if (is<SPGroup>(object->parent)) {
+ getDesktop()->layerManager().setCurrentLayer(object->parent);
+ }
+
+ getSelection()->set(cast<SPItem>(object));
+ }
+
+ document->setXMLDialogSelectedObject(object);
+ blocked--;
+}
+
+bool XmlTree::deferred_on_tree_select_row()
+{
+ GtkTreeIter iter;
+ GtkTreeModel *model;
+
+ if (selected_repr) {
+ Inkscape::GC::release(selected_repr);
+ selected_repr = nullptr;
+ }
+
+ GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
+
+ if (!gtk_tree_selection_get_selected (selection, &model, &iter)) {
+ // Nothing selected, update widgets
+ propagate_tree_select(nullptr);
+ set_dt_select(nullptr);
+ on_tree_unselect_row_disable();
+ return false;
+ }
+
+ Inkscape::XML::Node *repr = sp_xmlview_tree_node_get_repr(model, &iter);
+ g_assert(repr != nullptr);
+
+
+ selected_repr = repr;
+ Inkscape::GC::anchor(selected_repr);
+
+ propagate_tree_select(selected_repr);
+ set_dt_select(selected_repr);
+ on_tree_select_row_enable(&iter);
+
+ return FALSE;
+}
+
+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::NodeType::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::NodeType::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::cmd_new_element_node()
+{
+ auto document = getDocument();
+ if (!document)
+ return;
+
+ // enable in-place node name editing
+ tree->renderer->property_editable() = true;
+
+ auto dummy = ""; // this element has no corresponding SP* object and its construction is silent
+ auto xml_doc = document->getReprDoc();
+ _dummy = xml_doc->createElement(dummy); // create dummy placeholder so we can have a new temporary row in xml tree
+ _node_parent = selected_repr; // remember where the node is inserted
+ selected_repr->appendChild(_dummy);
+ set_tree_select(_dummy, true); // enter in-place node name editing
+}
+
+void XmlTree::startNodeEditing(Gtk::CellEditable* cell, const Glib::ustring& path)
+{
+ if (!cell) {
+ return;
+ }
+ // remove dummy element name so user can start with an empty name
+ auto entry = dynamic_cast<Gtk::Entry *>(cell);
+ entry->get_buffer()->set_text("");
+}
+
+void XmlTree::stopNodeEditing(bool ok, const Glib::ustring& path, Glib::ustring element)
+{
+ tree->renderer->property_editable() = false;
+
+ auto document = getDocument();
+ if (!document) {
+ return;
+ }
+ // delete dummy node
+ if (_dummy) {
+ document->setXMLDialogSelectedObject(nullptr);
+
+ auto parent = _dummy->parent();
+ Inkscape::GC::release(_dummy);
+ sp_repr_unparent(_dummy);
+ if (parent) {
+ auto parentobject = document->getObjectByRepr(parent);
+ if (parentobject) {
+ parentobject->requestDisplayUpdate(SP_OBJECT_CHILD_MODIFIED_FLAG);
+ }
+ }
+
+ _dummy = nullptr;
+ }
+
+ Util::trim(element);
+ if (!ok || element.empty() || !_node_parent) {
+ return;
+ }
+
+ Inkscape::XML::Document* xml_doc = document->getReprDoc();
+ // Extract tag name
+ {
+ static auto const extract_tagname = Glib::Regex::create("^<?\\s*(\\w[\\w:\\-\\d]*)");
+ Glib::MatchInfo match_info;
+ extract_tagname->match(element, match_info);
+ if (!match_info.matches()) {
+ return;
+ }
+ element = match_info.fetch(1);
+ }
+
+ // prepend "svg:" namespace if none is given
+ if (element.find(':') == Glib::ustring::npos) {
+ element = "svg:" + element;
+ }
+ auto repr = xml_doc->createElement(element.c_str());
+ Inkscape::GC::release(repr);
+ _node_parent->appendChild(repr);
+ set_dt_select(repr);
+ set_tree_select(repr, true);
+ _node_parent = nullptr;
+
+ DocumentUndo::done(document, Q_("Undo History / XML Editor|Create new element node"), INKSCAPE_ICON("dialog-xml-editor"));
+}
+
+void XmlTree::cmd_new_text_node()
+{
+ auto document = getDocument();
+ if (!document)
+ return;
+
+ g_assert(selected_repr != nullptr);
+
+ Inkscape::XML::Document *xml_doc = document->getReprDoc();
+ Inkscape::XML::Node *text = xml_doc->createTextNode("");
+ selected_repr->appendChild(text);
+
+ DocumentUndo::done(document, Q_("Undo History / XML Editor|Create new text node"), INKSCAPE_ICON("dialog-xml-editor"));
+
+ set_tree_select(text);
+ set_dt_select(text);
+}
+
+void XmlTree::cmd_duplicate_node()
+{
+ auto document = getDocument();
+ if (!document)
+ return;
+
+ 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(document, Q_("Undo History / XML Editor|Duplicate node"), INKSCAPE_ICON("dialog-xml-editor"));
+
+ 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()
+{
+ auto document = getDocument();
+ if (!document)
+ return;
+
+ g_assert(selected_repr != nullptr);
+
+ document->setXMLDialogSelectedObject(nullptr);
+
+ Inkscape::XML::Node *parent = selected_repr->parent();
+
+ sp_repr_unparent(selected_repr);
+
+ if (parent) {
+ auto parentobject = document->getObjectByRepr(parent);
+ if (parentobject) {
+ parentobject->requestDisplayUpdate(SP_OBJECT_CHILD_MODIFIED_FLAG);
+ }
+ }
+
+ DocumentUndo::done(document, Q_("Undo History / XML Editor|Delete node"), INKSCAPE_ICON("dialog-xml-editor"));
+}
+
+void XmlTree::cmd_raise_node()
+{
+ auto document = getDocument();
+ if (!document)
+ return;
+
+ 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(document, Q_("Undo History / XML Editor|Raise node"), INKSCAPE_ICON("dialog-xml-editor"));
+
+ set_tree_select(selected_repr);
+ set_dt_select(selected_repr);
+}
+
+
+
+void XmlTree::cmd_lower_node()
+{
+ auto document = getDocument();
+ if (!document)
+ return;
+
+ 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(document, Q_("Undo History / XML Editor|Lower node"), INKSCAPE_ICON("dialog-xml-editor"));
+
+ set_tree_select(selected_repr);
+ set_dt_select(selected_repr);
+}
+
+void XmlTree::cmd_indent_node()
+{
+ auto document = getDocument();
+ if (!document)
+ return;
+
+ 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::NodeType::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(document, Q_("Undo History / XML Editor|Indent node"), INKSCAPE_ICON("dialog-xml-editor"));
+ set_tree_select(repr);
+ set_dt_select(repr);
+
+} // end of cmd_indent_node()
+
+
+
+void XmlTree::cmd_unindent_node()
+{
+ auto document = getDocument();
+ if (!document)
+ return;
+
+ 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(document, Q_("Undo History / XML Editor|Unindent node"), INKSCAPE_ICON("dialog-xml-editor"));
+
+ 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;
+ while (is<SPItem>(child)) {
+ SPObject const * const parent = child->parent;
+ if (parent == nullptr) {
+ g_assert(is<SPRoot>(child));
+ if (child == &item) {
+ // item is root
+ return false;
+ }
+ return true;
+ }
+ child = parent;
+ }
+ g_assert(!is<SPRoot>(child));
+ return false;
+}
+
+void XmlTree::desktopReplaced() {
+ // subdialog does not receive desktopReplace calls, we need to propagate desktop change
+ if (attributes) {
+ attributes->setDesktop(getDesktop());
+ }
+}
+
+void XmlTree::setSyntaxStyle(Inkscape::UI::Syntax::XMLStyles const &new_style)
+{
+ tree->formatter->setStyle(new_style);
+}
+
+} // namespace 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/xml-tree.h b/src/ui/dialog/xml-tree.h
new file mode 100644
index 0000000..0a6c4d9
--- /dev/null
+++ b/src/ui/dialog/xml-tree.h
@@ -0,0 +1,200 @@
+// 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 INKSCAPE_UI_DIALOG_XML_TREE_H
+#define INKSCAPE_UI_DIALOG_XML_TREE_H
+
+#include <glibmm/ustring.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/widget.h>
+#include <memory>
+#include <glibmm/refptr.h>
+#include <gtkmm/builder.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 "attrdialog.h"
+#include "dialog-base.h"
+#include "preferences.h"
+#include "ui/syntax.h"
+
+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 DialogBase
+{
+public:
+ XmlTree();
+ ~XmlTree() override;
+
+ void setSyntaxStyle(Inkscape::UI::Syntax::XMLStyles const &new_style);
+
+private:
+ void unsetDocument();
+ void documentReplaced() override;
+ void selectionChanged(Selection *selection) override;
+ void desktopReplaced() override;
+
+ /**
+ * Select a node in the xml tree
+ */
+ void set_tree_repr(Inkscape::XML::Node *repr);
+
+ /**
+ * Is the selected tree node editable
+ */
+ gboolean xml_tree_node_mutable(GtkTreeIter *node);
+
+ /**
+ * Select a node in the xml tree
+ */
+ void set_tree_select(Inkscape::XML::Node *repr, bool edit = false);
+
+ /**
+ * 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 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.
+ */
+ Inkscape::auto_connection _tree_select_idle;
+ bool deferred_on_tree_select_row();
+
+ /**
+ * 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
+ */
+ 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 _resized();
+ bool in_dt_coordsys(SPObject const &item);
+
+ void rebuildTree();
+ void stopNodeEditing(bool ok, Glib::ustring const &path, Glib::ustring name);
+ void startNodeEditing(Gtk::CellEditable *cell, Glib::ustring const &path);
+
+ /**
+ * Flag to ensure only one operation is performed at once
+ */
+ gint blocked = 0;
+
+ /**
+ * Signal handlers
+ */
+ Inkscape::XML::Node *selected_repr = nullptr;
+
+ /* XmlTree Widgets */
+ SPXMLViewTree *tree = nullptr;
+ Gtk::TreeView* _treemm = nullptr;
+ AttrDialog *attributes;
+ Gtk::Box *_attrbox;
+
+ /* XML Node Creation pop-up window */
+ Glib::RefPtr<Gtk::Builder> _builder;
+ Gtk::Entry *name_entry;
+ Gtk::Button *create_button;
+ Gtk::Paned& _paned;
+
+ // Gtk::Box node_box;
+ Gtk::Switch _attrswitch;
+ Gtk::Label status;
+ Gtk::Button& xml_element_new_button;
+ Gtk::Button& xml_text_new_button;
+ Gtk::Button& xml_node_delete_button;
+ Gtk::Button& xml_node_duplicate_button;
+ Gtk::Button& unindent_node_button;
+ Gtk::Button& indent_node_button;
+ Gtk::Button& raise_node_button;
+ Gtk::Button& lower_node_button;
+
+ enum DialogLayout: int { Auto = 0, Horizontal, Vertical };
+ DialogLayout _layout = Auto;
+ Pref<Glib::ustring> _syntax_theme;
+ Pref<bool> _mono_font;
+ Inkscape::XML::Node* _dummy = nullptr;
+ Inkscape::XML::Node* _node_parent = nullptr;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_XML_TREE_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/drag-and-drop.cpp b/src/ui/drag-and-drop.cpp
new file mode 100644
index 0000000..21ec2c3
--- /dev/null
+++ b/src/ui/drag-and-drop.cpp
@@ -0,0 +1,429 @@
+// 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 <array>
+#include <glibmm/i18n.h> // Internationalization
+
+#include "desktop-style.h"
+#include "document.h"
+#include "document-undo.h"
+#include "gradient-drag.h"
+#include "file.h"
+#include "selection.h"
+#include "style.h"
+#include "layer-manager.h"
+
+#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 "path/path-util.h"
+
+#include "svg/svg-color.h" // write color
+
+#include "ui/clipboard.h"
+#include "ui/interface.h"
+#include "ui/tools/tool-base.h"
+#include "ui/widget/canvas.h" // Target, canvas to world transform.
+
+#include "widgets/desktop-widget.h"
+#include "widgets/paintdef.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 const std::array<Gtk::TargetEntry, 8> ui_drop_target_entries = {
+ Gtk::TargetEntry("text/uri-list", Gtk::TargetFlags(0), URI_LIST ),
+ Gtk::TargetEntry("image/svg+xml", Gtk::TargetFlags(0), SVG_XML_DATA ),
+ Gtk::TargetEntry("image/svg", Gtk::TargetFlags(0), SVG_DATA ),
+ Gtk::TargetEntry("image/png", Gtk::TargetFlags(0), PNG_DATA ),
+ Gtk::TargetEntry("image/jpeg", Gtk::TargetFlags(0), JPEG_DATA ),
+ Gtk::TargetEntry("application/x-oswb-color", Gtk::TargetFlags(0), APP_OSWB_COLOR ),
+ Gtk::TargetEntry("application/x-color", Gtk::TargetFlags(0), APP_X_COLOR ),
+ Gtk::TargetEntry("application/x-inkscape-paste", Gtk::TargetFlags(0), APP_X_INK_PASTE)
+};
+
+static std::vector<Gtk::TargetEntry> completeDropTargets;
+
+/** Convert screen (x, y) coordinates to desktop coordinates. */
+inline Geom::Point world2desktop(SPDesktop *desktop, int x, int y)
+{
+ g_assert(desktop);
+ return (Geom::Point(x, y) + desktop->canvas->get_area_world().min()) * desktop->w2d();
+}
+
+// Drag and Drop
+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)
+{
+ auto dtw = static_cast<SPDesktopWidget *>(user_data);
+ SPDesktop *desktop = dtw->desktop;
+ SPDocument *doc = desktop->doc();
+
+ switch (info) {
+ case APP_X_COLOR:
+ {
+ int destX = 0;
+ int destY = 0;
+ auto canvas = dtw->get_canvas();
+ gtk_widget_translate_coordinates( widget, GTK_WIDGET(canvas->gobj()), x, y, &destX, &destY );
+ Geom::Point where( canvas->canvas_to_world(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 , _("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 , _("Drop color on gradient stop"), "");
+ // }
+ //}
+
+ if (!consumed && item) {
+ bool fillnotstroke = (gdk_drag_context_get_actions (drag_context) != GDK_ACTION_MOVE);
+ if (fillnotstroke &&
+ (is<SPShape>(item) || is<SPText>(item) || is<SPFlowtext>(item))) {
+ Path *livarot_path = Path_for_item(item, true, true);
+ livarot_path->ConvertWithBackData(0.04);
+
+ std::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 , _("Drop color"), "" );
+ }
+ }
+ }
+ break;
+
+ case APP_OSWB_COLOR:
+ {
+ bool worked = false;
+ Glib::ustring colorspec;
+ if ( gtk_selection_data_get_format (data) == 8 ) {
+ 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));
+ if ( worked ) {
+ if ( color.get_type() == PaintDef::NONE ) {
+ colorspec = "none";
+ } else {
+ auto [r, g, b] = color.get_rgb();
+
+ SPGradient* matches = nullptr;
+ std::vector<SPObject *> gradients = doc->getResourceList("gradient");
+ for (auto gradient : gradients) {
+ auto grad = cast<SPGradient>(gradient);
+ if (color.get_description() == 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;
+ auto canvas = dtw->get_canvas();
+ gtk_widget_translate_coordinates( widget, GTK_WIDGET(canvas->gobj()), x, y, &destX, &destY );
+ Geom::Point where( canvas->canvas_to_world(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, _("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 &&
+ (is<SPShape>(item) || is<SPText>(item) || is<SPFlowtext>(item))) {
+ Path *livarot_path = Path_for_item(item, true, true);
+ livarot_path->ConvertWithBackData(0.04);
+
+ std::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, _("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::Document * xml_doc = doc->getReprDoc();
+ Inkscape::XML::Node *newgroup = xml_doc->createElement("svg:g");
+ newgroup->setAttribute("style", style);
+ 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->layerManager().currentLayer()->appendChildRepr(newgroup);
+
+ Inkscape::Selection *selection = desktop->getSelection();
+ selection->set(cast<SPItem>(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, _("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: {
+ auto *cm = Inkscape::UI::ClipboardManager::get();
+ cm->insertSymbol(desktop, world2desktop(desktop, x, y));
+ DocumentUndo::done(doc, _("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", nullptr );
+ 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, _("Drop bitmap image"), "" );
+ break;
+ }
+ }
+}
+
+#if 0
+static
+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);
+}
+#endif
+
+void ink_drag_setup(SPDesktopWidget *dtw)
+{
+ if (completeDropTargets.empty()) {
+ for (auto const &entry : ui_drop_target_entries) {
+ completeDropTargets.emplace_back(entry);
+ }
+ for (auto const &fmt : Gdk::Pixbuf::get_formats()) {
+ for (auto &type : fmt.get_mime_types()) {
+ completeDropTargets.emplace_back(std::move(type), Gtk::TargetFlags(0), IMAGE_DATA);
+ }
+ }
+ }
+
+ auto canvas = dtw->get_canvas();
+
+ canvas->drag_dest_set(completeDropTargets,
+ Gtk::DestDefaults::DEST_DEFAULT_ALL,
+ Gdk::DragAction::ACTION_COPY | Gdk::DragAction::ACTION_MOVE);
+
+ g_signal_connect(G_OBJECT(canvas->gobj()),
+ "drag_data_received",
+ G_CALLBACK(ink_drag_data_received),
+ dtw);
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ 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..f6a6ac5
--- /dev/null
+++ b/src/ui/drag-and-drop.h
@@ -0,0 +1,32 @@
+// 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.
+ */
+
+class SPDesktopWidget;
+
+void ink_drag_setup(SPDesktopWidget *);
+
+#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..b0d22e5
--- /dev/null
+++ b/src/ui/draw-anchor.cpp
@@ -0,0 +1,81 @@
+// 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 "ui/tools/tool-base.h"
+#include "ui/tools/lpe-tool.h"
+
+#include "display/control/canvas-item-ctrl.h"
+#include "display/curve.h"
+
+const guint32 FILL_COLOR_NORMAL = 0xffffff7f;
+const guint32 FILL_COLOR_MOUSEOVER = 0xff0000ff;
+
+/**
+ * Creates an anchor object and initializes it.
+ */
+SPDrawAnchor::SPDrawAnchor(Inkscape::UI::Tools::FreehandBase *dc, std::shared_ptr<SPCurve> curve, bool start, Geom::Point delta)
+ : dc(dc), curve(std::move(curve)), start(start), active(FALSE), dp(delta),
+ ctrl(
+ make_canvasitem<Inkscape::CanvasItemCtrl>(
+ dc->getDesktop()->getCanvasControls(),
+ Inkscape::CANVAS_ITEM_CTRL_TYPE_ANCHOR
+ )
+ )
+{
+ ctrl->set_name("CanvasItemCtrl:DrawAnchor");
+ ctrl->set_fill(FILL_COLOR_NORMAL);
+ ctrl->set_position(delta);
+ ctrl->set_pickable(false); // We do our own checking. (TODO: Should be fixed!)
+}
+
+SPDrawAnchor::~SPDrawAnchor() = default;
+
+/**
+ * Test if point is near anchor, if so fill anchor on canvas and return
+ * pointer to it or NULL.
+ */
+SPDrawAnchor *SPDrawAnchor::anchorTest(Geom::Point w, bool activate)
+{
+ if ( activate && this->ctrl->contains(w)) {
+
+ if (!this->active) {
+ this->ctrl->set_size_extra(4);
+ this->ctrl->set_fill(FILL_COLOR_MOUSEOVER);
+ this->active = TRUE;
+ }
+ return this;
+ }
+
+ if (this->active) {
+ this->ctrl->set_size_extra(0);
+ this->ctrl->set_fill(FILL_COLOR_NORMAL);
+ this->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..6cff151
--- /dev/null
+++ b/src/ui/draw-anchor.h
@@ -0,0 +1,71 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * The nodes at the ends of the path in the pen/pencil tools.
+ *//*
+ * 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>
+
+#include "display/control/canvas-item-ptr.h"
+
+namespace Inkscape {
+
+class CanvasItemCtrl;
+
+namespace UI {
+namespace Tools {
+
+class FreehandBase;
+
+}
+}
+}
+
+class SPCurve;
+
+/// The drawing anchor.
+/// \todo Make this a regular knot, this will allow setting statusbar tips.
+
+// TODO Get rid of this class.
+
+class SPDrawAnchor
+{
+public:
+
+ Inkscape::UI::Tools::FreehandBase *dc;
+ std::shared_ptr<SPCurve> curve;
+ bool start : 1;
+ bool active : 1;
+ Geom::Point dp;
+ CanvasItemPtr<Inkscape::CanvasItemCtrl> ctrl;
+
+ SPDrawAnchor(Inkscape::UI::Tools::FreehandBase *dc,
+ std::shared_ptr<SPCurve> curve, bool start, Geom::Point delta);
+
+ ~SPDrawAnchor();
+
+ SPDrawAnchor *anchorTest(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..d46913d
--- /dev/null
+++ b/src/ui/event-debug.h
@@ -0,0 +1,125 @@
+// 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 motion notify events.
+ ++count;
+ if (merge && event->type == old_type && event->type == GDK_MOTION_NOTIFY) {
+ 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: " << std::hex
+ << " hardware: " << event->key.hardware_keycode
+ << " state: " << event->key.state
+ << " keyval: " << event->key.keyval << 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/filtered-store.h b/src/ui/filtered-store.h
new file mode 100644
index 0000000..47a99ba
--- /dev/null
+++ b/src/ui/filtered-store.h
@@ -0,0 +1,99 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifndef INKSCAPE_PATTERN_EDITOR_H
+#define INKSCAPE_PATTERN_EDITOR_H
+
+#include <giomm/liststore.h>
+#include <functional>
+#include <vector>
+
+// Simplistic filtered list store implementation
+// It's a GIO ListStore plus custom filter function to control visibility of items
+
+namespace Inkscape {
+
+template<class T> class FilteredStore {
+public:
+ FilteredStore() {
+ _store = Gio::ListStore<T>::create();
+ }
+
+ typedef Glib::RefPtr<T> TPtr;
+
+ bool assign(const std::vector<TPtr>& items) {
+ if (items == _items) return false; // not changed
+
+ _items = items;
+ apply_filter();
+ return true; // store updated
+ }
+
+ void refresh() {
+ apply_filter(true);
+ }
+
+ const std::vector<TPtr>& get_items() const {
+ return _items;
+ }
+
+ void set_filter(const std::function<bool (const TPtr&)>& filter_callback) {
+ _filter_callback = filter_callback;
+ }
+
+ void apply_filter(bool force_refresh = false) {
+ // if there's a filter, run it
+ std::vector<TPtr> visible;
+ if (_filter_callback) {
+ std::copy_if(_items.begin(), _items.end(), std::back_inserter(visible), [=](const TPtr& t){
+ return _filter_callback(t);
+ });
+ }
+ auto& items = _filter_callback ? visible : _items;
+
+ if (force_refresh) {
+ update_store(items);
+ return;
+ }
+
+ // compare store content with visible list items to avoid needless updates
+ const auto n = _store->get_n_items();
+ bool same = false;
+ if (n == items.size()) {
+ same = true;
+ // compare each item
+ for (int i = 0; i < n; ++i) {
+ if (items[i] != _store->get_item(i)) {
+ same = false;
+ break;
+ }
+ }
+ }
+
+ if (same) return; // nothing to do
+
+ update_store(items);
+ }
+
+ Glib::RefPtr<Gio::ListStore<T>> get_store() {
+ return _store;
+ }
+
+private:
+ void update_store(const std::vector<TPtr>& items) {
+ _store->freeze_notify();
+ _store->remove_all();
+ for (auto&& t : items) {
+ _store->append(t);
+ }
+ _store->thaw_notify();
+ }
+
+ Glib::RefPtr<Gio::ListStore<T>> _store;
+ std::function<bool (const TPtr&)> _filter_callback;
+ std::vector<TPtr> _items;
+ std::vector<TPtr> _visible;
+};
+
+} // namespace
+
+#endif
diff --git a/src/ui/icon-loader.cpp b/src/ui/icon-loader.cpp
new file mode 100644
index 0000000..a536162
--- /dev/null
+++ b/src/ui/icon-loader.cpp
@@ -0,0 +1,144 @@
+// 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 <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;
+}
+
+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)
+{
+ // SP_ACTIVE_DESKTOP is not always available when we want icons (see start screen)
+ auto window = SP_ACTIVE_DESKTOP ? SP_ACTIVE_DESKTOP->getToplevel() : nullptr;
+
+ 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);
+ auto prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/theme/symbolicIcons", false) && icon_name.find("-symbolic") == Glib::ustring::npos) {
+ icon_name += Glib::ustring("-symbolic");
+ }
+ Gtk::IconInfo iconinfo = icon_theme->lookup_icon(icon_name, size, Gtk::ICON_LOOKUP_FORCE_SIZE);
+ Glib::RefPtr<Gdk::Pixbuf> _icon_pixbuf;
+ if (prefs->getBool("/theme/symbolicIcons", false)) {
+ if (window) {
+ Glib::RefPtr<Gtk::StyleContext> stylecontext = window->get_style_context();
+ bool was_symbolic = false;
+ _icon_pixbuf = iconinfo.load_symbolic(stylecontext, was_symbolic);
+ } else {
+ // we never go here
+ _icon_pixbuf = iconinfo.load_icon();
+ }
+ } else {
+ _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 scale)
+{
+ int width, height;
+ Gtk::IconSize::lookup(Gtk::IconSize(icon_size), width, height);
+ return sp_get_icon_pixbuf(icon_name, width * scale);
+}
+
+Glib::RefPtr<Gdk::Pixbuf> sp_get_icon_pixbuf(Glib::ustring icon_name, GtkIconSize icon_size, int scale)
+{
+ gint width, height;
+ gtk_icon_size_lookup(icon_size, &width, &height);
+ return sp_get_icon_pixbuf(icon_name, width * scale);
+}
+
+/**
+ * Get the shape icon for this named shape type. For example 'rect'. These icons
+ * are always symbolic icons no matter the theme in order to be coloured by the highlight
+ * color.
+ *
+ * @param shape_type - A string id for the shape from SPItem->typeName()
+ * @param color - The fg color of the shape icon
+ * @param size - The icon size to generate
+ */
+Glib::RefPtr<Gdk::Pixbuf> sp_get_shape_icon(Glib::ustring shape_type, Gdk::RGBA color, gint size, int scale)
+{
+ 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);
+
+ Gtk::IconInfo iconinfo = icon_theme->lookup_icon("shape-" + shape_type + "-symbolic",
+ size * scale, Gtk::ICON_LOOKUP_FORCE_SIZE);
+ if (!iconinfo) {
+ // fallback to regular icons
+ iconinfo = icon_theme->lookup_icon(shape_type + "-symbolic", size * scale, Gtk::ICON_LOOKUP_FORCE_SIZE);
+ }
+
+ if (!iconinfo) {
+ iconinfo = icon_theme->lookup_icon("shape-unknown-symbolic", size * scale, Gtk::ICON_LOOKUP_FORCE_SIZE);
+ // We know this could fail, but it should exist, so persist.
+ }
+ // Gtkmm requires all colours, even though gtk does not
+ auto other = Gdk::RGBA("black");
+ bool was_symbolic = false;
+ return iconinfo.load_symbolic(color, other, other, other, was_symbolic);
+}
+
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 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..52fe070
--- /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, int scale=1);
+Glib::RefPtr<Gdk::Pixbuf> sp_get_icon_pixbuf(Glib::ustring icon_name, Gtk::BuiltinIconSize icon_size, int scale=1);
+Glib::RefPtr<Gdk::Pixbuf> sp_get_icon_pixbuf(Glib::ustring icon_name, GtkIconSize icon_size, int scale=1);
+Glib::RefPtr<Gdk::Pixbuf> sp_get_shape_icon(Glib::ustring shape_type, Gdk::RGBA color, gint size, int scale=1);
+
+#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..b3c3062
--- /dev/null
+++ b/src/ui/interface.cpp
@@ -0,0 +1,189 @@
+// 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 <glibmm/i18n.h>
+
+#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 "io/sys.h"
+
+#include "object/sp-namedview.h"
+#include "object/sp-root.h"
+
+#include "ui/dialog-events.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;
+
+ auto *app = InkscapeApplication::instance();
+
+ app->window_open(document);
+}
+
+void
+sp_ui_close_view(GtkWidget */*widget*/)
+{
+ auto *app = InkscapeApplication::instance();
+
+ auto window = app->get_active_window();
+ assert(window);
+ app->destroy_window(window, true); // Keep inkscape alive!
+}
+
+
+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_message_dialog_set_markup(GTK_MESSAGE_DIALOG(dlg),safeMsg);
+ 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,
+ nullptr );
+ 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..6707d8c
--- /dev/null
+++ b/src/ui/interface.h
@@ -0,0 +1,63 @@
+// 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);
+
+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/knot/README b/src/ui/knot/README
new file mode 100644
index 0000000..7a76593
--- /dev/null
+++ b/src/ui/knot/README
@@ -0,0 +1,62 @@
+
+
+This directory contains code related to on-screen editing knots.
+
+Note that there are classes with similar functionality based on the ControlPoint class in src/ui/tool.
+
+Classes here:
+
+ * SPKnot A class that describes a knot (size, type, position, state, etc.) with some signals.
+
+ * KnotHolderEntity A class that has an SPKnot with some signals connections.
+
+ Derived classes:
+
+ LPEKnotHolderEntity
+ PatternKnotHolderEntity
+ HatchKnotHolderEntity
+ FilterKnotHolderEntity
+ RectKnotHolderEntityRX
+ RectKnotHolderEntityRY
+ RectKnotHolderEntityWH
+ RectKnotHolderEntityXY
+ RectKnotHolderEntityCenter
+ Box3DKnotHolderEntity
+ Box3DKnotHolderEntityCenter
+ ArcKnotHolderEntityStart
+ ArcKnotHolderEntityEnd
+ ArcKnotHolderEntityRX
+ ArcKnotHolderEntityRY
+ ArcKnotHolderEntityCenter
+ StarKnotHolderEntity1
+ StarKnotHolderEntity2
+ StarKnotHolderEntityCenter
+ SpiralKnotHolderEntityInner
+ SpiralKnotHolderEntityOuter
+ SpiralKnotHolderEntityCenter
+ OffsetKnotHolderEntity
+ TextKnotHolderEntityInlineSize
+ TextKnotHolderEntityShapeInside
+ FilletChamferKnotHolderEntity
+ TransformedPointParamKnotHolderEntity_Vector
+ PowerStrokePointArrayParamKnotHolderEntity
+ PowerStrokePointArrayParamKnotHolderEntity
+ PointParamKnotHolderEntity
+ VectorParamKnotHolderEntity_Origin
+ VectorParamKnotHolderEntity_Vector
+
+ And many classes derived from above!
+
+ * KnotHolder A class that has one or more overlapping knots via KnotHolderEntity's.
+
+ Derived classes:
+
+ ArcKnotHolder
+ Box3DKnotHolder
+ FlowtextKnotHolder
+ MiscKnotHolder
+ OffsetKnotHolder
+ RectKnotHolder
+ SpiralKnotHolder
+ StarKnotHolder
+ TextKnotHolder
diff --git a/src/ui/knot/knot-enums.h b/src/ui/knot/knot-enums.h
new file mode 100644
index 0000000..4f12cd7
--- /dev/null
+++ b/src/ui/knot/knot-enums.h
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_KNOT_ENUMS_H
+#define SEEN_KNOT_ENUMS_H
+
+/**
+ * @file
+ * Some enums used by SPKnot and by related types \& functions.
+ */
+/*
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ *
+ * Copyright (C) 1999-2002 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+enum SPKnotStateType {
+ SP_KNOT_STATE_NORMAL,
+ SP_KNOT_STATE_MOUSEOVER,
+ SP_KNOT_STATE_DRAGGING,
+ SP_KNOT_STATE_SELECTED,
+ SP_KNOT_STATE_HIDDEN
+};
+
+#define SP_KNOT_VISIBLE_STATES 4
+
+enum {
+ SP_KNOT_VISIBLE = 1 << 0,
+ SP_KNOT_MOUSEOVER = 1 << 1,
+ SP_KNOT_DRAGGING = 1 << 2,
+ SP_KNOT_GRABBED = 1 << 3,
+ SP_KNOT_SELECTED = 1 << 4
+};
+
+
+#endif /* !SEEN_KNOT_ENUMS_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/knot/knot-holder-entity.cpp b/src/ui/knot/knot-holder-entity.cpp
new file mode 100644
index 0000000..e75c628
--- /dev/null
+++ b/src/ui/knot/knot-holder-entity.cpp
@@ -0,0 +1,633 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * KnotHolderEntity definition.
+ *
+ * Authors:
+ * Mitsuru Oka <oka326@parkcity.ne.jp>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 1999-2001 Lauris Kaplinski
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ * Copyright (C) 2001 Mitsuru Oka
+ * Copyright (C) 2004 Monash University
+ * Copyright (C) 2008 Maximilian Albert
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "knot-holder-entity.h"
+
+#include "desktop.h"
+#include "display/control/canvas-item-ctrl.h"
+#include "inkscape.h"
+#include "knot-holder.h"
+#include "live_effects/effect.h"
+#include "object/sp-hatch.h"
+#include "object/sp-item.h"
+#include "object/sp-marker.h"
+#include "object/sp-namedview.h"
+#include "object/sp-pattern.h"
+#include "object/filters/gaussian-blur.h"
+#include "preferences.h"
+#include "snap.h"
+#include "style.h"
+#include "object/sp-marker.h"
+
+#include "display/control/canvas-item-ctrl.h"
+#include <glibmm/i18n.h>
+
+void KnotHolderEntity::create(SPDesktop *desktop, SPItem *item, KnotHolder *parent,
+ Inkscape::CanvasItemCtrlType type,
+ Glib::ustring const & name,
+ const gchar *tip, guint32 color)
+{
+ if (!desktop) {
+ desktop = parent->getDesktop();
+ }
+
+ g_assert(item == parent->getItem());
+ g_assert(desktop && desktop == parent->getDesktop());
+ g_assert(knot == nullptr);
+
+ parent_holder = parent;
+ this->item = item; // TODO: remove the item either from here or from knotholder.cpp
+ this->desktop = desktop;
+
+ my_counter = KnotHolderEntity::counter++;
+
+ knot = new SPKnot(desktop, tip, type, name);
+ knot->fill [SP_KNOT_STATE_NORMAL] = color;
+ knot->ctrl->set_fill(color);
+ on_created();
+ update_knot();
+ knot->show();
+
+ _mousedown_connection = knot->mousedown_signal.connect(sigc::mem_fun(*parent_holder, &KnotHolder::knot_mousedown_handler));
+ _moved_connection = knot->moved_signal.connect(sigc::mem_fun(*parent_holder, &KnotHolder::knot_moved_handler));
+ _click_connection = knot->click_signal.connect(sigc::mem_fun(*parent_holder, &KnotHolder::knot_clicked_handler));
+ _ungrabbed_connection = knot->ungrabbed_signal.connect(sigc::mem_fun(*parent_holder, &KnotHolder::knot_ungrabbed_handler));
+}
+
+KnotHolderEntity::~KnotHolderEntity()
+{
+ _mousedown_connection.disconnect();
+ _moved_connection.disconnect();
+ _click_connection.disconnect();
+ _ungrabbed_connection.disconnect();
+
+ /* unref should call destroy */
+ if (knot) {
+ //g_object_unref(knot);
+ knot_unref(knot);
+ } else {
+ // FIXME: This shouldn't occur. Perhaps it is caused by LPE PointParams being knotholder entities, too
+ // If so, it will likely be fixed with upcoming refactoring efforts.
+ g_return_if_fail(knot);
+ }
+}
+
+void
+KnotHolderEntity::update_knot()
+{
+ Geom::Point knot_pos(knot_get());
+ if (knot_pos.isFinite()) {
+ Geom::Point dp(knot_pos * parent_holder->getEditTransform() * item->i2dt_affine());
+
+ _moved_connection.block();
+ knot->setPosition(dp, SP_KNOT_STATE_NORMAL);
+ _moved_connection.unblock();
+ } else {
+ // knot coords are non-finite, hide knot
+ knot->hide();
+ }
+}
+
+Geom::Point
+KnotHolderEntity::snap_knot_position(Geom::Point const &p, guint state)
+{
+ if (state & GDK_SHIFT_MASK) { // Don't snap when shift-key is held
+ return p;
+ }
+
+ Geom::Affine const i2dt (parent_holder->getEditTransform() * item->i2dt_affine());
+ Geom::Point s = p * i2dt;
+
+ if (!desktop) std::cerr << "No desktop" << std::endl;
+ if (!desktop->namedview) std::cerr << "No named view" << std::endl;
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop, true, item);
+ m.freeSnapReturnByRef(s, Inkscape::SNAPSOURCE_NODE_HANDLE);
+ m.unSetup();
+
+ return s * i2dt.inverse();
+}
+
+Geom::Point
+KnotHolderEntity::snap_knot_position_constrained(Geom::Point const &p, Inkscape::Snapper::SnapConstraint const &constraint, guint state)
+{
+ if (state & GDK_SHIFT_MASK) { // Don't snap when shift-key is held
+ return p;
+ }
+
+ Geom::Affine const i2d (parent_holder->getEditTransform() * item->i2dt_affine());
+ Geom::Point s = p * i2d;
+
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop, true, item);
+
+ // constrainedSnap() will first project the point p onto the constraint line and then try to snap along that line.
+ // This way the constraint is already enforced, no need to worry about that later on
+ Inkscape::Snapper::SnapConstraint transformed_constraint = Inkscape::Snapper::SnapConstraint(constraint.getPoint() * i2d, (constraint.getPoint() + constraint.getDirection()) * i2d - constraint.getPoint() * i2d);
+ m.constrainedSnapReturnByRef(s, Inkscape::SNAPSOURCE_NODE_HANDLE, transformed_constraint);
+ m.unSetup();
+
+ return s * i2d.inverse();
+}
+
+void
+LPEKnotHolderEntity::knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state)
+{
+ if (_effect) {
+ _effect->refresh_widgets = true;
+ _effect->makeUndoDone(_("Move handle"));
+ }
+}
+
+/* Pattern manipulation */
+
+void PatternKnotHolderEntity::on_created()
+{
+ // Setup an initial pattern transformation in the center
+ if (auto rect = item->documentGeometricBounds()) {
+ _cell = offset_to_cell(rect->midpoint());
+ }
+}
+
+/**
+ * Returns the position based on the pattern's origin, shifted by the percent x/y of it's size.
+ */
+Geom::Point PatternKnotHolderEntity::_get_pos(gdouble x, gdouble y, bool transform) const
+{
+ auto pat = _pattern();
+ auto pt = Geom::Point((_cell[Geom::X] + x) * pat->width(),
+ (_cell[Geom::Y] + y) * pat->height());
+ return transform ? pt * pat->getTransform() : pt;
+}
+
+bool PatternKnotHolderEntity::set_item_clickpos(Geom::Point loc)
+{
+ _cell = offset_to_cell(loc);
+ update_knot();
+ return true;
+}
+
+void PatternKnotHolderEntity::update_knot() {
+ KnotHolderEntity::update_knot();
+}
+
+Geom::IntPoint PatternKnotHolderEntity::offset_to_cell(Geom::Point loc) const {
+ auto pat = _pattern();
+
+ // 1. Turn the location into the pattern grid coordinate
+ auto scale = Geom::Scale(pat->width(), pat->height());
+ auto d2i = item->i2doc_affine().inverse();
+ auto i2p = pat->getTransform().inverse();
+
+ // Get grid index of nearest pattern repetition.
+ return (loc * d2i * i2p * scale.inverse()).floor();
+}
+
+
+SPPattern *PatternKnotHolderEntity::_pattern() const
+{
+ return _fill ? cast<SPPattern>(item->style->getFillPaintServer()) : cast<SPPattern>(item->style->getStrokePaintServer());
+}
+
+bool PatternKnotHolderEntity::knot_missing() const
+{
+ return !_pattern();
+}
+
+/* Pattern X/Y knot */
+
+void PatternKnotHolderEntityXY::on_created()
+{
+ PatternKnotHolderEntity::on_created();
+ // TODO: Move to constructor when desktop is generally available
+ _quad = make_canvasitem<Inkscape::CanvasItemQuad>(desktop->getCanvasControls());
+ _quad->lower_to_bottom();
+ _quad->set_fill(0x00000000);
+ _quad->set_stroke(0x808080ff);
+ _quad->set_inverted(true);
+ _quad->hide();
+}
+
+void PatternKnotHolderEntityXY::update_knot()
+{
+ PatternKnotHolderEntity::update_knot();
+ auto tr = item->i2dt_affine();
+ _quad->set_coords(_get_pos(0, 0) * tr, _get_pos(0, 1) * tr,
+ _get_pos(1, 1) * tr, _get_pos(1, 0) * tr);
+ _quad->show();
+}
+
+Geom::Point PatternKnotHolderEntityXY::knot_get() const
+{
+ return _get_pos(0, 0);
+}
+
+void
+PatternKnotHolderEntityXY::knot_set(Geom::Point const &p, Geom::Point const &origin, guint state)
+{
+ // FIXME: this snapping should be done together with knowing whether control was pressed. If GDK_CONTROL_MASK, then constrained snapping should be used.
+ Geom::Point p_snapped = snap_knot_position(p, state);
+
+ if ( state & GDK_CONTROL_MASK ) {
+ if (fabs((p - origin)[Geom::X]) > fabs((p - origin)[Geom::Y])) {
+ p_snapped[Geom::Y] = origin[Geom::Y];
+ } else {
+ p_snapped[Geom::X] = origin[Geom::X];
+ }
+ }
+
+ if (state) {
+ Geom::Point const q = p_snapped - knot_get();
+ item->adjust_pattern(Geom::Translate(q), false, _fill ? TRANSFORM_FILL : TRANSFORM_STROKE);
+ }
+
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+/* Pattern Angle knot */
+
+Geom::Point PatternKnotHolderEntityAngle::knot_get() const
+{
+ return _get_pos(1.0, 0);
+}
+
+void
+PatternKnotHolderEntityAngle::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint state)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int const snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12);
+
+ // get the angle from pattern 0,0 to the cursor pos
+ Geom::Point transform_origin = _get_pos(0, 0);
+ gdouble theta = atan2(p - transform_origin);
+ gdouble theta_old = atan2(knot_get() - transform_origin);
+
+ if ( state & GDK_CONTROL_MASK ) {
+ /* Snap theta */
+ double snaps_radian = M_PI/snaps;
+ theta = std::round(theta/snaps_radian) * snaps_radian;
+ }
+
+ Geom::Affine rot = Geom::Translate(-transform_origin)
+ * Geom::Rotate(theta - theta_old)
+ * Geom::Translate(transform_origin);
+ item->adjust_pattern(rot, false, _fill ? TRANSFORM_FILL : TRANSFORM_STROKE);
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+/* Pattern scale knot */
+
+Geom::Point PatternKnotHolderEntityScale::knot_get() const
+{
+ return _get_pos(1.0, 1.0);
+}
+
+/** Store pattern geometry info when the scale knot is first grabbed. */
+void PatternKnotHolderEntityScale::knot_grabbed(Geom::Point const &grab_pos, unsigned)
+{
+ _cached_transform = _pattern()->getTransform();
+ _cached_origin = _get_pos(0, 0);
+ _cached_inverse_linear = _cached_transform.withoutTranslation().inverse();
+ _cached_diagonal = (grab_pos - _cached_origin) * _cached_inverse_linear;
+
+ if (auto bounding_box = item->documentVisualBounds()) {
+ // Compare the areas of the pattern and the item to find the number of repetitions.
+ double const pattern_area = std::abs(_cached_diagonal[Geom::X] * _cached_diagonal[Geom::Y]);
+ double const item_area = bounding_box->area() * _cached_inverse_linear.descrim2() /
+ (item->i2doc_affine().descrim2() ?: 1e-3);
+ _cached_min_scale = std::sqrt(item_area / (pattern_area * MAX_REPETITIONS));
+ } else {
+ _cached_min_scale = 1e-6;
+ }
+}
+
+void
+PatternKnotHolderEntityScale::knot_set(Geom::Point const &p, Geom::Point const &, guint state)
+{
+ using namespace Geom;
+ // FIXME: this snapping should be done together with knowing whether control was pressed.
+ // If GDK_CONTROL_MASK, then constrained snapping should be used.
+ Point p_snapped = snap_knot_position(p, state);
+
+ Point const new_extent = (p_snapped - _cached_origin) * _cached_inverse_linear;
+
+ // 1. Calculate absolute scale factor first
+ double scale_x = std::clamp(new_extent[X] / _cached_diagonal[X], _cached_min_scale, 1e9);
+ double scale_y = std::clamp(new_extent[Y] / _cached_diagonal[Y], _cached_min_scale, 1e9);
+
+ Affine new_transform = (state & GDK_CONTROL_MASK) ? Scale(lerp(0.5, scale_x, scale_y))
+ : Scale(scale_x, scale_y);
+
+ // 2. Calculate offset to keep pattern origin aligned
+ new_transform *= _cached_transform;
+ auto const new_uncompensated_origin = _get_pos(0, 0, false) * new_transform;
+ new_transform *= Translate(_cached_origin - new_uncompensated_origin);
+
+ item->adjust_pattern(new_transform, true, _fill ? TRANSFORM_FILL : TRANSFORM_STROKE);
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+/* Hatch manipulation */
+bool HatchKnotHolderEntity::knot_missing() const
+{
+ SPHatch *hatch = _hatch();
+ return (hatch == nullptr);
+}
+
+SPHatch *HatchKnotHolderEntity::_hatch() const
+{
+ return _fill ? cast<SPHatch>(item->style->getFillPaintServer()) : cast<SPHatch>(item->style->getStrokePaintServer());
+}
+
+static Geom::Point sp_hatch_knot_get(SPHatch const *hatch, gdouble x, gdouble y)
+{
+ return Geom::Point(x, y) * hatch->hatchTransform();
+}
+
+Geom::Point HatchKnotHolderEntityXY::knot_get() const
+{
+ SPHatch *hatch = _hatch();
+ return sp_hatch_knot_get(hatch, 0, 0);
+}
+
+Geom::Point HatchKnotHolderEntityAngle::knot_get() const
+{
+ SPHatch *hatch = _hatch();
+ return sp_hatch_knot_get(hatch, hatch->pitch(), 0);
+}
+
+Geom::Point HatchKnotHolderEntityScale::knot_get() const
+{
+ SPHatch *hatch = _hatch();
+ return sp_hatch_knot_get(hatch, hatch->pitch(), hatch->pitch());
+}
+
+void HatchKnotHolderEntityXY::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ Geom::Point p_snapped = snap_knot_position(p, state);
+
+ if (state & GDK_CONTROL_MASK) {
+ if (fabs((p - origin)[Geom::X]) > fabs((p - origin)[Geom::Y])) {
+ p_snapped[Geom::Y] = origin[Geom::Y];
+ } else {
+ p_snapped[Geom::X] = origin[Geom::X];
+ }
+ }
+
+ if (state) {
+ Geom::Point const q = p_snapped - knot_get();
+ item->adjust_hatch(Geom::Translate(q), false, _fill ? TRANSFORM_FILL : TRANSFORM_STROKE);
+ }
+
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+void HatchKnotHolderEntityAngle::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int const snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12);
+
+ SPHatch *hatch = _hatch();
+
+ // get the angle from hatch 0,0 to the cursor pos
+ Geom::Point transform_origin = sp_hatch_knot_get(hatch, 0, 0);
+ gdouble theta = atan2(p - transform_origin);
+ gdouble theta_old = atan2(knot_get() - transform_origin);
+
+ if (state & GDK_CONTROL_MASK) {
+ /* Snap theta */
+ double snaps_radian = M_PI/snaps;
+ theta = std::round(theta/snaps_radian) * snaps_radian;
+ }
+
+ Geom::Affine rot =
+ Geom::Translate(-transform_origin) * Geom::Rotate(theta - theta_old) * Geom::Translate(transform_origin);
+ item->adjust_hatch(rot, false, _fill ? TRANSFORM_FILL : TRANSFORM_STROKE);
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+void HatchKnotHolderEntityScale::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ SPHatch *hatch = _hatch();
+
+ // FIXME: this snapping should be done together with knowing whether control was pressed.
+ // If GDK_CONTROL_MASK, then constrained snapping should be used.
+ Geom::Point p_snapped = snap_knot_position(p, state);
+
+ // Get the new scale from the position of the knotholder
+ Geom::Affine transform = hatch->hatchTransform();
+ Geom::Affine transform_inverse = transform.inverse();
+ Geom::Point d = p_snapped * transform_inverse;
+ Geom::Point d_origin = origin * transform_inverse;
+ Geom::Point origin_dt;
+ gdouble hatch_pitch = hatch->pitch();
+ if (state & GDK_CONTROL_MASK) {
+ // if ctrl is pressed: use 1:1 scaling
+ d = d_origin * (d.length() / d_origin.length());
+ }
+
+ Geom::Affine scale = Geom::Translate(-origin_dt) * Geom::Scale(d.x() / hatch_pitch, d.y() / hatch_pitch) *
+ Geom::Translate(origin_dt) * transform;
+
+ item->adjust_hatch(scale, true, _fill ? TRANSFORM_FILL : TRANSFORM_STROKE);
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+/* Filter visible size manipulation */
+void FilterKnotHolderEntity::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ // FIXME: this snapping should be done together with knowing whether control was pressed. If GDK_CONTROL_MASK, then constrained snapping should be used.
+ Geom::Point p_snapped = snap_knot_position(p, state);
+
+ if ( state & GDK_CONTROL_MASK ) {
+ if (fabs((p - origin)[Geom::X]) > fabs((p - origin)[Geom::Y])) {
+ p_snapped[Geom::Y] = origin[Geom::Y];
+ } else {
+ p_snapped[Geom::X] = origin[Geom::X];
+ }
+ }
+
+ if (state) {
+ SPFilter *filter = (item->style) ? item->style->getFilter() : nullptr;
+ if(!filter) return;
+ Geom::OptRect orig_bbox = item->visualBounds();
+ std::unique_ptr<Geom::Rect> new_bbox(_topleft ? new Geom::Rect(p,orig_bbox->max()) : new Geom::Rect(orig_bbox->min(), p));
+
+ if (!filter->width._set) {
+ filter->width.set(SVGLength::PERCENT, 1.2);
+ }
+ if (!filter->height._set) {
+ filter->height.set(SVGLength::PERCENT, 1.2);
+ }
+ if (!filter->x._set) {
+ filter->x.set(SVGLength::PERCENT, -0.1);
+ }
+ if (!filter->y._set) {
+ filter->y.set(SVGLength::PERCENT, -0.1);
+ }
+
+ if(_topleft) {
+ float x_a = filter->width.computed;
+ float y_a = filter->height.computed;
+ filter->height.scale(new_bbox->height()/orig_bbox->height());
+ filter->width.scale(new_bbox->width()/orig_bbox->width());
+ float x_b = filter->width.computed;
+ float y_b = filter->height.computed;
+ filter->x.set(filter->x.unit, filter->x.computed + x_a - x_b);
+ filter->y.set(filter->y.unit, filter->y.computed + y_a - y_b);
+ } else {
+ filter->height.scale(new_bbox->height()/orig_bbox->height());
+ filter->width.scale(new_bbox->width()/orig_bbox->width());
+ }
+ filter->auto_region = false;
+ filter->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+
+ }
+
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+Geom::Point FilterKnotHolderEntity::knot_get() const
+{
+ SPFilter *filter = (item->style) ? item->style->getFilter() : nullptr;
+ if(!filter) return Geom::Point(Geom::infinity(), Geom::infinity());
+ Geom::OptRect r = item->visualBounds();
+ if (_topleft) return Geom::Point(r->min());
+ else return Geom::Point(r->max());
+}
+
+/* Blur manipulation */
+
+void BlurKnotHolderEntity::on_created()
+{
+ KnotHolderEntity::on_created();
+ // TODO: Move to constructor when desktop is generally available
+
+ _line = make_canvasitem<Inkscape::CanvasItemCurve>(desktop->getCanvasControls());
+ _line->set_z_position(0);
+ _line->set_stroke(0x0033cccc);
+ _line->hide();
+
+ // This watcher makes sure that adding or removing a blur results in updated knots.
+ _watch_filter = item->style->signal_filter_changed.connect([=] (auto old_obj, auto obj) {
+ update_knot();
+ });
+}
+
+void BlurKnotHolderEntity::update_knot()
+{
+ auto blur = _blur();
+ if (blur) {
+ knot->show();
+ // This watcher makes sure anything outside that modifies the blur changes the knot.
+ _watch_blur = blur->connectModified([=](auto item, int flags) {
+ KnotHolderEntity::update_knot();
+ });
+
+ } else {
+ knot->hide();
+ _watch_blur.disconnect();
+ _line->hide();
+ }
+ KnotHolderEntity::update_knot();
+}
+
+
+
+/* Return the first blur primitive of any applied filter. */
+SPGaussianBlur *BlurKnotHolderEntity::_blur() const
+{
+ if (auto filter = item->style->getFilter()) {
+ for (auto &primitive : filter->children) {
+ if (auto blur = cast<SPGaussianBlur>(&primitive)) {
+ return blur;
+ }
+ }
+ }
+ return nullptr;
+}
+
+Geom::Point BlurKnotHolderEntity::_pos() const
+{
+ auto box = item->bbox(Geom::identity(), SPItem::VISUAL_BBOX);
+ if (_dir == Geom::Y) {
+ return Geom::Point(box->midpoint()[Geom::X], box->top());
+ }
+ return Geom::Point(box->right(), box->midpoint()[Geom::Y]);
+}
+
+Geom::Point BlurKnotHolderEntity::knot_get() const
+{
+ auto blur = _blur();
+ if (!blur)
+ return Geom::Point(0, 0);
+
+ // First let's find where the gradient is
+ auto tr = item->i2dt_affine();
+ auto dev = blur->get_std_deviation();
+
+ // Blur visibility is 2.4 times the deviation in that direction.
+ double x = dev.getNumber();
+ double y = dev.getOptNumber(true);
+
+ auto p0 = _pos();
+ auto p1 = p0 + Geom::Point(x * 2.4, 0);
+ if (_dir == Geom::Y) {
+ p1 = p0 - Geom::Point(0, y * 2.4);
+ }
+ _line->show();
+ _line->set_coords(p0 * tr, p1 * tr);
+
+ return p1;
+}
+void BlurKnotHolderEntity::knot_set(Geom::Point const &p, Geom::Point const &origin, guint state)
+{
+ auto blur = _blur();
+ if (!blur)
+ return;
+
+ NumberOptNumber dev = blur->get_std_deviation();
+ auto dp = Geom::Point(dev.getNumber(), dev.getOptNumber(true));
+ auto val = std::max(0.0, (((p - _pos()) * Geom::Scale(1, -1))[_dir]) / 2.4);
+
+ if (state & GDK_CONTROL_MASK) {
+ if (state & GDK_SHIFT_MASK) {
+ dp[!_dir] *= (val / dp[_dir]);
+ } else {
+ dp[!_dir] = val;
+ }
+ }
+ dp[_dir] = val;
+
+ // When X is set to zero the Opt blur disapears
+ dev.setNumber(std::max(0.001, dp[Geom::X]));
+ dev.setOptNumber(std::max(0.0, dp[Geom::Y]));
+
+ blur->set_deviation(dev);
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/knot/knot-holder-entity.h b/src/ui/knot/knot-holder-entity.h
new file mode 100644
index 0000000..62a0906
--- /dev/null
+++ b/src/ui/knot/knot-holder-entity.h
@@ -0,0 +1,254 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_KNOT_HOLDER_ENTITY_H
+#define SEEN_KNOT_HOLDER_ENTITY_H
+/*
+ * Authors:
+ * Mitsuru Oka <oka326@parkcity.ne.jp>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ *
+ * Copyright (C) 1999-2001 Lauris Kaplinski
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ * Copyright (C) 2001 Mitsuru Oka
+ * Copyright (C) 2004 Monash University
+ * Copyright (C) 2008 Maximilian Albert
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <2geom/forward.h>
+
+#include "knot.h"
+#include "snapper.h"
+
+#include "display/control/canvas-item-enums.h"
+#include "display/control/canvas-item-quad.h"
+#include "display/control/canvas-item-curve.h"
+#include "helper/auto-connection.h"
+#include "display/control/canvas-item-ptr.h"
+
+class SPHatch;
+class SPItem;
+class SPKnot;
+class SPDesktop;
+class SPPattern;
+class SPGaussianBlur;
+class KnotHolder;
+
+namespace Inkscape {
+namespace LivePathEffect {
+ class Effect;
+} // namespace LivePathEffect
+} // namespace Inkscape
+
+typedef void (* SPKnotHolderSetFunc) (SPItem *item, Geom::Point const &p, Geom::Point const &origin, unsigned int state);
+typedef Geom::Point (* SPKnotHolderGetFunc) (SPItem *item);
+
+/**
+ * KnotHolderEntity definition.
+ */
+class KnotHolderEntity {
+public:
+ KnotHolderEntity() {}
+ virtual ~KnotHolderEntity();
+
+ void create(SPDesktop *desktop, SPItem *item, KnotHolder *parent,
+ Inkscape::CanvasItemCtrlType type = Inkscape::CANVAS_ITEM_CTRL_TYPE_DEFAULT,
+ Glib::ustring const & name = Glib::ustring("unknown"),
+ char const *tip = "",
+ guint32 color = 0xffffff00);
+
+ /* the get/set/click handlers are virtual functions; each handler class for a knot
+ should be derived from KnotHolderEntity and override these functions */
+ virtual void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) = 0;
+ virtual void knot_grabbed(Geom::Point const &/*grab_position*/, unsigned /*state*/) {}
+ virtual void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, unsigned int state) = 0;
+ virtual bool knot_missing() const { return false; }
+ virtual Geom::Point knot_get() const = 0;
+ virtual void knot_click(unsigned int /*state*/) {}
+ virtual bool set_item_clickpos(Geom::Point loc) { return false; }
+
+ virtual void on_created() {}
+ virtual void update_knot();
+
+ // private:
+ Geom::Point snap_knot_position(Geom::Point const &p, unsigned int state);
+ Geom::Point snap_knot_position_constrained(Geom::Point const &p, Inkscape::Snapper::SnapConstraint const &constraint, unsigned int state);
+
+ SPKnot *knot = nullptr;
+ SPItem *item = nullptr;
+ SPDesktop *desktop = nullptr;
+ KnotHolder *parent_holder = nullptr;
+
+ int my_counter = 0;
+ inline static int counter = 0;
+
+ /** Connection to \a knot's "moved" signal. */
+ unsigned int handler_id = 0;
+ /** Connection to \a knot's "clicked" signal. */
+ unsigned int _click_handler_id = 0;
+ /** Connection to \a knot's "ungrabbed" signal. */
+ unsigned int _ungrab_handler_id = 0;
+
+private:
+ sigc::connection _mousedown_connection;
+ sigc::connection _moved_connection;
+ sigc::connection _click_connection;
+ sigc::connection _ungrabbed_connection;
+};
+
+// derived KnotHolderEntity class for LPEs
+class LPEKnotHolderEntity : public KnotHolderEntity {
+public:
+ LPEKnotHolderEntity(Inkscape::LivePathEffect::Effect *effect) : _effect(effect) {};
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override;
+protected:
+ Inkscape::LivePathEffect::Effect *_effect;
+};
+
+/* pattern manipulation */
+
+class PatternKnotHolderEntity : public KnotHolderEntity {
+ public:
+ PatternKnotHolderEntity(bool fill) : KnotHolderEntity(), _fill(fill) {}
+ void on_created() override;
+ bool knot_missing() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override{};
+ bool set_item_clickpos(Geom::Point loc) override;
+ void update_knot() override;
+
+protected:
+ // true if the entity tracks fill, false for stroke
+ bool _fill;
+ SPPattern *_pattern() const;
+ Geom::Point _get_pos(gdouble x, gdouble y, bool transform = true) const;
+ Geom::IntPoint offset_to_cell(Geom::Point loc) const;
+ Geom::IntPoint _cell;
+};
+
+class PatternKnotHolderEntityXY : public PatternKnotHolderEntity {
+public:
+ PatternKnotHolderEntityXY(bool fill) : PatternKnotHolderEntity(fill) {}
+ ~PatternKnotHolderEntityXY() override = default;
+
+ void on_created() override;
+ void update_knot() override;
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+
+private:
+ // Extra visual element to show the pattern editing area
+ CanvasItemPtr<Inkscape::CanvasItemQuad> _quad;
+};
+
+class PatternKnotHolderEntityAngle : public PatternKnotHolderEntity {
+public:
+ PatternKnotHolderEntityAngle(bool fill) : PatternKnotHolderEntity(fill) {}
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+class PatternKnotHolderEntityScale : public PatternKnotHolderEntity {
+public:
+ PatternKnotHolderEntityScale(bool fill) : PatternKnotHolderEntity(fill) {}
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+ void knot_grabbed(Geom::Point const &grab_pos, unsigned state) override;
+
+private:
+ /// Maximum number of pattern repetitons allowed in an item
+ inline static double const MAX_REPETITIONS = 1e6;
+ Geom::Affine _cached_transform, _cached_inverse_linear;
+ Geom::Point _cached_origin, _cached_diagonal;
+ double _cached_min_scale;
+};
+
+/* Hatch manipulation */
+class HatchKnotHolderEntity : public KnotHolderEntity {
+ public:
+ HatchKnotHolderEntity(bool fill)
+ : KnotHolderEntity()
+ , _fill(fill)
+ {
+ }
+ bool knot_missing() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override{};
+
+ protected:
+ // true if the entity tracks fill, false for stroke
+ bool _fill;
+ SPHatch *_hatch() const;
+};
+
+class HatchKnotHolderEntityXY : public HatchKnotHolderEntity {
+ public:
+ HatchKnotHolderEntityXY(bool fill)
+ : HatchKnotHolderEntity(fill)
+ {
+ }
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+class HatchKnotHolderEntityAngle : public HatchKnotHolderEntity {
+ public:
+ HatchKnotHolderEntityAngle(bool fill)
+ : HatchKnotHolderEntity(fill)
+ {
+ }
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+class HatchKnotHolderEntityScale : public HatchKnotHolderEntity {
+ public:
+ HatchKnotHolderEntityScale(bool fill)
+ : HatchKnotHolderEntity(fill)
+ {
+ }
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+
+/* Filter manipulation */
+class FilterKnotHolderEntity : public KnotHolderEntity {
+ public:
+ FilterKnotHolderEntity(bool topleft) : KnotHolderEntity(), _topleft(topleft) {}
+ 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;
+private:
+ bool _topleft; // true for topleft point, false for bottomright
+};
+
+class BlurKnotHolderEntity : public KnotHolderEntity {
+ public:
+ BlurKnotHolderEntity(int direction) : KnotHolderEntity(), _dir(direction) {}
+ void on_created() override;
+ void update_knot() override;
+ 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;
+
+ private:
+ SPGaussianBlur *_blur() const;
+ Geom::Point _pos() const;
+
+ int _dir;
+ CanvasItemPtr<Inkscape::CanvasItemCurve> _line;
+ Inkscape::auto_connection _watch_filter;
+ Inkscape::auto_connection _watch_blur;
+};
+
+#endif /* !SEEN_KNOT_HOLDER_ENTITY_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/knot/knot-holder.cpp b/src/ui/knot/knot-holder.cpp
new file mode 100644
index 0000000..138d809
--- /dev/null
+++ b/src/ui/knot/knot-holder.cpp
@@ -0,0 +1,512 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Container for SPKnot visual handles.
+ *
+ * Authors:
+ * Mitsuru Oka <oka326@parkcity.ne.jp>
+ * bulia byak <buliabyak@users.sf.net>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Abhishek Sharma
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2001-2008 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "knot-holder.h"
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "knot-holder-entity.h"
+#include "knot.h"
+
+#include "live_effects/effect.h"
+#include "live_effects/lpeobject.h"
+
+#include "object/box3d.h"
+#include "object/sp-ellipse.h"
+#include "object/sp-hatch.h"
+#include "object/sp-offset.h"
+#include "object/sp-pattern.h"
+#include "object/sp-rect.h"
+#include "object/sp-shape.h"
+#include "object/sp-spiral.h"
+#include "object/sp-star.h"
+#include "object/sp-marker.h"
+#include "object/filters/gaussian-blur.h"
+#include "style.h"
+
+#include "ui/icon-names.h"
+#include "ui/shape-editor.h"
+#include "ui/tools/arc-tool.h"
+#include "ui/tools/node-tool.h"
+#include "ui/tools/rect-tool.h"
+#include "ui/tools/spiral-tool.h"
+#include "ui/tools/tweak-tool.h"
+
+#include "display/control/snap-indicator.h"
+
+// TODO due to internal breakage in glibmm headers, this must be last:
+#include <glibmm/i18n.h>
+
+using Inkscape::DocumentUndo;
+
+class SPDesktop;
+
+KnotHolder::KnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) :
+ desktop(desktop),
+ item(item),
+ //XML Tree being used directly for item->getRepr() while it shouldn't be...
+ repr(item ? item->getRepr() : nullptr),
+ entity(),
+ released(relhandler),
+ local_change(FALSE),
+ dragging(false),
+ _edit_transform(Geom::identity())
+{
+ if (!desktop || !item) {
+ g_warning ("Error! Throw an exception, please!");
+ }
+
+ sp_object_ref(item);
+}
+
+KnotHolder::~KnotHolder() {
+ sp_object_unref(item);
+
+ for (auto & i : entity) {
+ delete i;
+ }
+ entity.clear(); // is this necessary?
+}
+
+void
+KnotHolder::setEditTransform(Geom::Affine edit_transform)
+{
+ _edit_transform = edit_transform;
+}
+
+void KnotHolder::update_knots()
+{
+ for (auto e = entity.begin(); e != entity.end(); ) {
+ // check if pattern was removed without deleting the knot
+ if ((*e)->knot_missing()) {
+ delete (*e);
+ e = entity.erase(e);
+ } else {
+ (*e)->update_knot();
+ ++e;
+ }
+ }
+}
+
+/**
+ * Returns true if at least one of the KnotHolderEntities has the mouse hovering above it.
+ */
+bool KnotHolder::knot_mouseover() const {
+ for (auto i : entity) {
+ const SPKnot *knot = i->knot;
+
+ if (knot && knot->is_mouseover()) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Returns true if at least one of the KnotHolderEntities is selected
+ */
+bool KnotHolder::knot_selected() const {
+ for (auto i : entity) {
+ const SPKnot *knot = i->knot;
+
+ if (knot && knot->is_selected()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void
+KnotHolder::knot_mousedown_handler(SPKnot *knot, guint state)
+{
+ if (!(state & GDK_SHIFT_MASK)) {
+ unselect_knots();
+ }
+ for(auto e : this->entity) {
+ if (!(state & GDK_SHIFT_MASK)) {
+ e->knot->selectKnot(false);
+ }
+ if (e->knot == knot) {
+ if (!(e->knot->is_selected()) || !(state & GDK_SHIFT_MASK)){
+ e->knot->selectKnot(true);
+ } else {
+ e->knot->selectKnot(false);
+ }
+ }
+ }
+}
+
+void
+KnotHolder::knot_clicked_handler(SPKnot *knot, guint state)
+{
+ SPItem *saved_item = this->item;
+
+ for(auto e : this->entity) {
+ if (e->knot == knot)
+ // no need to test whether knot_click exists since it's virtual now
+ e->knot_click(state);
+ }
+
+ {
+ auto savedShape = cast<SPShape>(saved_item);
+ if (savedShape) {
+ savedShape->set_shape();
+ }
+ }
+
+ this->update_knots();
+
+ Glib::ustring icon_name;
+
+ // TODO extract duplicated blocks;
+ if (is<SPRect>(saved_item)) {
+ icon_name = INKSCAPE_ICON("draw-rectangle");
+ } else if (is<SPBox3D>(saved_item)) {
+ icon_name = INKSCAPE_ICON("draw-cuboid");
+ } else if (is<SPGenericEllipse>(saved_item)) {
+ icon_name = INKSCAPE_ICON("draw-ellipse");
+ } else if (is<SPStar>(saved_item)) {
+ icon_name = INKSCAPE_ICON("draw-polygon-star");
+ } else if (is<SPSpiral>(saved_item)) {
+ icon_name = INKSCAPE_ICON("draw-spiral");
+ } else if (is<SPMarker>(saved_item)) {
+ icon_name = INKSCAPE_ICON("tool-pointer");
+ } else {
+ auto offset = cast<SPOffset>(saved_item);
+ if (offset) {
+ if (offset->sourceHref) {
+ icon_name = INKSCAPE_ICON("path-offset-linked");
+ } else {
+ icon_name = INKSCAPE_ICON("path-offset-dynamic");
+ }
+ }
+ }
+
+ // for drag, this is done by ungrabbed_handler, but for click we must do it here
+
+ if (saved_item && saved_item->document) { // increasingly aggressive sanity checks
+ DocumentUndo::done(saved_item->document, _("Change handle"), icon_name);
+ } else {
+ std::terminate();
+ }
+}
+
+void
+KnotHolder::transform_selected(Geom::Affine transform){
+ for (auto & i : entity) {
+ SPKnot *knot = i->knot;
+ if (knot->is_selected()) {
+ knot_moved_handler(knot, knot->pos * transform , 0);
+ knot->selectKnot(true);
+ }
+ }
+}
+
+void
+KnotHolder::unselect_knots(){
+ Inkscape::UI::Tools::NodeTool *nt = dynamic_cast<Inkscape::UI::Tools::NodeTool*>(desktop->event_context);
+ if (nt) {
+ for (auto &_shape_editor : nt->_shape_editors) {
+ Inkscape::UI::ShapeEditor *shape_editor = _shape_editor.second.get();
+ if (shape_editor && shape_editor->has_knotholder()) {
+ KnotHolder * knotholder = shape_editor->knotholder;
+ if (knotholder) {
+ for (auto e : knotholder->entity) {
+ if (e->knot->is_selected()) {
+ e->knot->selectKnot(false);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+/** Notifies an entity that its knot has just been grabbed. */
+void KnotHolder::knot_grabbed_handler(SPKnot *knot, unsigned state)
+{
+ auto grab_entity = std::find_if(entity.begin(), entity.end(),
+ [=](KnotHolderEntity *khe) -> bool { return khe->knot == knot; });
+ if (grab_entity == entity.end()) {
+ return;
+ }
+ auto const item_origin = (*grab_entity)->knot->drag_origin * item->dt2i_affine()
+ * _edit_transform.inverse();
+ (*grab_entity)->knot_grabbed(item_origin, state);
+}
+
+void
+KnotHolder::knot_moved_handler(SPKnot *knot, Geom::Point const &p, guint state)
+{
+ if (!dragging) {
+ // The knot has just been grabbed
+ knot_grabbed_handler(knot, state);
+ dragging = true;
+ }
+
+ // this was a local change and the knotholder does not need to be recreated:
+ this->local_change = TRUE;
+
+ for(auto e : this->entity) {
+ if (e->knot == knot) {
+ Geom::Point const q = p * item->i2dt_affine().inverse() * _edit_transform.inverse();
+ e->knot_set(q, e->knot->drag_origin * item->i2dt_affine().inverse() * _edit_transform.inverse(), state);
+ break;
+ }
+ }
+
+ auto shape = cast<SPShape>(item);
+ if (shape) {
+ shape->set_shape();
+ }
+
+ this->update_knots();
+}
+
+void
+KnotHolder::knot_ungrabbed_handler(SPKnot *knot, guint state)
+{
+ this->dragging = false;
+ desktop->snapindicator->remove_snaptarget();
+
+ if (this->released) {
+ this->released(this->item);
+ } else {
+ // if a point is dragged while not selected, it should select itself,
+ // even if it was just unselected in the mousedown event handler.
+ if (!(knot->is_selected())) {
+ knot->selectKnot(true);
+ } else {
+ for(auto e : this->entity) {
+ if (e->knot == knot) {
+ e->knot_ungrabbed(e->knot->position(), e->knot->drag_origin * item->i2dt_affine().inverse() * _edit_transform.inverse(), state);
+ if (e->knot->is_lpe) {
+ return;
+ }
+ break;
+ }
+ }
+ }
+
+ SPObject *object = (SPObject *) this->item;
+
+ // Caution: this call involves a screen update, which may process events, and as a
+ // result the knotholder may be destructed. So, after the updateRepr, we cannot use any
+ // fields of this knotholder (such as this->item), but only values we have saved beforehand
+ // (such as object).
+ object->updateRepr();
+
+
+ SPFilter *filter = (object->style) ? object->style->getFilter() : nullptr;
+ if (filter) {
+ filter->updateRepr();
+ }
+ Glib::ustring icon_name;
+
+ // TODO extract duplicated blocks;
+ if (is<SPRect>(object)) {
+ icon_name = INKSCAPE_ICON("draw-rectangle");
+ } else if (is<SPBox3D>(object)) {
+ icon_name = INKSCAPE_ICON("draw-cuboid");
+ } else if (is<SPGenericEllipse>(object)) {
+ icon_name = INKSCAPE_ICON("draw-ellipse");
+ } else if (is<SPStar>(object)) {
+ icon_name = INKSCAPE_ICON("draw-polygon-star");
+ } else if (is<SPSpiral>(object)) {
+ icon_name = INKSCAPE_ICON("draw-spiral");
+ } else if (is<SPMarker>(object)) {
+ icon_name = INKSCAPE_ICON("tool-pointer");
+ } else {
+ auto offset = cast<SPOffset>(object);
+ if (offset) {
+ if (offset->sourceHref) {
+ icon_name = INKSCAPE_ICON("path-offset-linked");
+ } else {
+ icon_name = INKSCAPE_ICON("path-offset-dynamic");
+ }
+ }
+ }
+ DocumentUndo::done(object->document, _("Move handle"), icon_name);
+ }
+}
+
+void KnotHolder::add(KnotHolderEntity *e)
+{
+ // g_message("Adding a knot at %p", e);
+ entity.push_back(e);
+}
+
+void KnotHolder::add_pattern_knotholder()
+{
+ if (is<SPPattern>(item->style->getFillPaintServer())) {
+ auto entity_xy = new PatternKnotHolderEntityXY(true);
+ auto entity_angle = new PatternKnotHolderEntityAngle(true);
+ auto entity_scale = new PatternKnotHolderEntityScale(true);
+ entity_xy->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Pattern:Fill:xy",
+ // TRANSLATORS: This refers to the pattern that's inside the object
+ _("<b>Move</b> the pattern fill inside the object"));
+
+ entity_scale->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Pattern:Fill:scale",
+ _("<b>Scale</b> the pattern fill; uniformly if with <b>Ctrl</b>"));
+
+ entity_angle->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_ROTATE, "Pattern:Fill:angle",
+ _("<b>Rotate</b> the pattern fill; with <b>Ctrl</b> to snap angle"));
+
+ entity.push_back(entity_xy);
+ entity.push_back(entity_angle);
+ entity.push_back(entity_scale);
+ }
+
+ if (is<SPPattern>(item->style->getStrokePaintServer())) {
+ auto entity_xy = new PatternKnotHolderEntityXY(false);
+ auto entity_angle = new PatternKnotHolderEntityAngle(false);
+ auto entity_scale = new PatternKnotHolderEntityScale(false);
+ entity_xy->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_POINT, "Pattern:Stroke:xy",
+ // TRANSLATORS: This refers to the pattern that's inside the object
+ _("<b>Move</b> the stroke's pattern inside the object"));
+
+ entity_scale->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Pattern:Stroke:scale",
+ _("<b>Scale</b> the stroke's pattern; uniformly if with <b>Ctrl</b>"));
+
+ entity_angle->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_ROTATE, "Pattern:Stroke:angle",
+ _("<b>Rotate</b> the stroke's pattern; with <b>Ctrl</b> to snap angle"));
+
+ entity.push_back(entity_xy);
+ entity.push_back(entity_angle);
+ entity.push_back(entity_scale);
+ }
+
+ // watch patterns and update knots when they change
+ install_modification_watch();
+}
+
+void KnotHolder::add_hatch_knotholder()
+{
+ if ((item->style->fill.isPaintserver()) && cast<SPHatch>(item->style->getFillPaintServer())) {
+ HatchKnotHolderEntityXY *entity_xy = new HatchKnotHolderEntityXY(true);
+ HatchKnotHolderEntityAngle *entity_angle = new HatchKnotHolderEntityAngle(true);
+ HatchKnotHolderEntityScale *entity_scale = new HatchKnotHolderEntityScale(true);
+ entity_xy->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_POINT, "Hatch:Fill:xy",
+ // TRANSLATORS: This refers to the hatch that's inside the object
+ _("<b>Move</b> the hatch fill inside the object"));
+
+ entity_scale->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Hatch:Fill:scale",
+ _("<b>Scale</b> the hatch fill; uniformly if with <b>Ctrl</b>"));
+
+ entity_angle->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_ROTATE, "Hatch:Fill:angle",
+ _("<b>Rotate</b> the hatch fill; with <b>Ctrl</b> to snap angle"));
+
+ entity.push_back(entity_xy);
+ entity.push_back(entity_angle);
+ entity.push_back(entity_scale);
+ }
+
+ if ((item->style->stroke.isPaintserver()) && cast<SPHatch>(item->style->getStrokePaintServer())) {
+ HatchKnotHolderEntityXY *entity_xy = new HatchKnotHolderEntityXY(false);
+ HatchKnotHolderEntityAngle *entity_angle = new HatchKnotHolderEntityAngle(false);
+ HatchKnotHolderEntityScale *entity_scale = new HatchKnotHolderEntityScale(false);
+ entity_xy->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_POINT, "Hatch:Stroke:xy",
+ // TRANSLATORS: This refers to the pattern that's inside the object
+ _("<b>Move</b> the hatch stroke inside the object"));
+
+ entity_scale->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Hatch:Stroke:scale",
+ _("<b>Scale</b> the hatch stroke; uniformly if with <b>Ctrl</b>"));
+
+ entity_angle->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_ROTATE, "Hatch:Stroke:angle",
+ _("<b>Rotate</b> the hatch stroke; with <b>Ctrl</b> to snap angle"));
+
+ entity.push_back(entity_xy);
+ entity.push_back(entity_angle);
+ entity.push_back(entity_scale);
+ }
+}
+
+void KnotHolder::add_filter_knotholder() {
+ if (auto filter = item->style->getFilter()) {
+ if (!filter->auto_region) {
+ auto entity_tl = new FilterKnotHolderEntity(true);
+ auto entity_br = new FilterKnotHolderEntity(false);
+ entity_tl->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_POINT, "Filter:TopLeft",
+ _("<b>Resize</b> the filter effect region"));
+ entity_br->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_POINT, "Filter:BottomRight",
+ _("<b>Resize</b> the filter effect region"));
+ entity.push_back(entity_tl);
+ entity.push_back(entity_br);
+ }
+ }
+
+ // always install blur nodes, they default to disabled.
+ auto entity_x = new BlurKnotHolderEntity(Geom::X);
+ auto entity_y = new BlurKnotHolderEntity(Geom::Y);
+ entity_x->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_ROTATE, "Filter:BlurX",
+ _("<b>Drag</b> to <b>adjust</b> blur in x direction; <b>Ctrl</b>+<b>Drag</b> makes x equal to y; <b>Shift</b>+<b>Ctrl</b>+<b>Drag</b> scales blur proportionately "));
+ entity_y->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_ROTATE, "Filter:BlurY",
+ _("<b>Drag</b> to <b>adjust</b> blur in y direction; <b>Ctrl</b>+<b>Drag</b> makes y equal to x; <b>Shift</b>+<b>Ctrl</b>+<b>Drag</b> scales blur proportionately "));
+ entity.push_back(entity_x);
+ entity.push_back(entity_y);
+}
+
+/**
+ * When editing an object, this extra information tells out knots
+ * where the user has clicked on the item.
+ */
+bool KnotHolder::set_item_clickpos(Geom::Point loc)
+{
+ bool ret = false;
+ for (auto i : entity) {
+ ret = i->set_item_clickpos(loc) || ret;
+ }
+ return ret;
+}
+
+/**
+ * When object being edited has some attributes changed (fill, stroke)
+ * update what objects we watch
+ */
+void KnotHolder::install_modification_watch() {
+ g_assert(item);
+
+ if (auto pattern = cast<SPPattern>(item->style->getFillPaintServer())) {
+ _watch_fill = pattern->connectModified([=](SPObject*, unsigned int){
+ update_knots();
+ });
+ }
+ else {
+ _watch_fill.disconnect();
+ }
+
+ if (auto pattern = cast<SPPattern>(item->style->getStrokePaintServer())) {
+ _watch_stroke = pattern->connectModified([=](SPObject*, unsigned int){
+ update_knots();
+ });
+ }
+ else {
+ _watch_stroke.disconnect();
+ }
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/knot/knot-holder.h b/src/ui/knot/knot-holder.h
new file mode 100644
index 0000000..0584182
--- /dev/null
+++ b/src/ui/knot/knot-holder.h
@@ -0,0 +1,123 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_SP_KNOTHOLDER_H
+#define SEEN_SP_KNOTHOLDER_H
+
+/*
+ * KnotHolder - Hold SPKnot list and manage signals
+ *
+ * Author:
+ * Mitsuru Oka <oka326@parkcity.ne.jp>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ *
+ * Copyright (C) 1999-2001 Lauris Kaplinski
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ * Copyright (C) 2001 Mitsuru Oka
+ * Copyright (C) 2008 Maximilian Albert
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ *
+ */
+
+#include <2geom/forward.h>
+#include <2geom/affine.h>
+#include <list>
+#include <sigc++/connection.h>
+#include "helper/auto-connection.h"
+
+namespace Inkscape {
+namespace UI {
+class ShapeEditor;
+}
+namespace XML {
+class Node;
+}
+namespace LivePathEffect {
+class PowerStrokePointArrayParamKnotHolderEntity;
+class NodeSatelliteArrayParam;
+class FilletChamferKnotHolderEntity;
+}
+}
+
+class KnotHolderEntity;
+class SPItem;
+class SPDesktop;
+class SPKnot;
+
+/* fixme: Think how to make callbacks most sensitive (Lauris) */
+typedef void (* SPKnotHolderReleasedFunc) (SPItem *item);
+
+class KnotHolder {
+public:
+ KnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler);
+ virtual ~KnotHolder();
+
+ KnotHolder() = delete; // declared but not defined
+
+ void update_knots();
+ void unselect_knots();
+ void knot_mousedown_handler(SPKnot *knot, unsigned int state);
+ void knot_moved_handler(SPKnot *knot, Geom::Point const &p, unsigned int state);
+ void knot_clicked_handler(SPKnot *knot, unsigned int state);
+ void knot_grabbed_handler(SPKnot *knot, unsigned state);
+ void knot_ungrabbed_handler(SPKnot *knot, unsigned int state);
+ void transform_selected(Geom::Affine transform);
+ void add(KnotHolderEntity *e);
+
+ void add_pattern_knotholder();
+ void add_hatch_knotholder();
+ void add_filter_knotholder();
+
+ void setEditTransform(Geom::Affine edit_transform);
+ Geom::Affine getEditTransform() const { return _edit_transform; }
+
+ bool knot_selected() const;
+ bool knot_mouseover() const;
+
+ SPDesktop *getDesktop() { return desktop; }
+ SPItem *getItem() { return item; }
+ bool is_dragging() const { return dragging; }
+
+ bool set_item_clickpos(Geom::Point loc);
+ void install_modification_watch();
+
+ std::list<KnotHolderEntity *> entity;
+ friend class Inkscape::UI::ShapeEditor; // FIXME why?
+ friend class Inkscape::LivePathEffect::NodeSatelliteArrayParam; // why?
+ friend class Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity; // why?
+ friend class Inkscape::LivePathEffect::FilletChamferKnotHolderEntity; // why?
+
+protected:
+
+ SPDesktop *desktop;
+ SPItem *item; // TODO: Remove this and keep the actual item (e.g., SPRect etc.) in the item-specific knotholders
+ Inkscape::XML::Node *repr; ///< repr of the item, for setting and releasing listeners.
+
+ SPKnotHolderReleasedFunc released;
+
+ bool local_change; ///< if true, no need to recreate knotholder if repr was changed.
+
+ bool dragging;
+
+ Geom::Affine _edit_transform;
+ Inkscape::auto_connection _watch_fill;
+ Inkscape::auto_connection _watch_stroke;
+};
+
+/**
+void knot_clicked_handler(SPKnot *knot, guint state, gpointer data);
+void knot_moved_handler(SPKnot *knot, Geom::Point const *p, guint state, gpointer data);
+void knot_ungrabbed_handler(SPKnot *knot, unsigned int state, KnotHolder *kh);
+**/
+
+#endif // SEEN_SP_KNOTHOLDER_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/knot/knot-ptr.cpp b/src/ui/knot/knot-ptr.cpp
new file mode 100644
index 0000000..8e275ac
--- /dev/null
+++ b/src/ui/knot/knot-ptr.cpp
@@ -0,0 +1,34 @@
+// 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 <algorithm>
+#include <glib.h>
+#include <list>
+#include "knot-ptr.h"
+
+static std::list<void*> deleted_knots;
+
+void knot_deleted_callback(void* knot) {
+ if (std::find(deleted_knots.begin(), deleted_knots.end(), knot) == deleted_knots.end()) {
+ deleted_knots.push_back(knot);
+ }
+}
+
+void knot_created_callback(void* knot) {
+ std::list<void*>::iterator it = std::find(deleted_knots.begin(), deleted_knots.end(), knot);
+ if (it != deleted_knots.end()) {
+ deleted_knots.erase(it);
+ }
+}
+
+void check_if_knot_deleted(void* knot) {
+ if (std::find(deleted_knots.begin(), deleted_knots.end(), knot) != deleted_knots.end()) {
+ g_warning("Accessed knot after it was freed at %p", knot);
+ }
+}
diff --git a/src/ui/knot/knot-ptr.h b/src/ui/knot/knot-ptr.h
new file mode 100644
index 0000000..5141822
--- /dev/null
+++ b/src/ui/knot/knot-ptr.h
@@ -0,0 +1,17 @@
+// 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 KNOT_PTR_DETECTOR
+#define KNOT_PTR_DETECTOR
+
+void knot_deleted_callback(void* knot);
+void knot_created_callback(void* knot);
+void check_if_knot_deleted(void* knot);
+
+#endif
diff --git a/src/ui/knot/knot.cpp b/src/ui/knot/knot.cpp
new file mode 100644
index 0000000..24fefb4
--- /dev/null
+++ b/src/ui/knot/knot.cpp
@@ -0,0 +1,515 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * SPKnot implementation
+ *
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 1999-2005 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifdef HAVE_CONFIG_H
+#endif
+#include <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.h>
+
+#include "desktop.h"
+
+#include "knot.h"
+#include "knot-ptr.h"
+#include "document.h"
+#include "document-undo.h"
+#include "message-stack.h"
+#include "message-context.h"
+
+#include "display/control/canvas-item-ctrl.h"
+#include "ui/tools/tool-base.h"
+#include "ui/tools/node-tool.h"
+#include "ui/widget/canvas.h" // autoscroll
+
+using Inkscape::DocumentUndo;
+
+Gdk::EventMask KNOT_EVENT_MASK (
+ Gdk::BUTTON_PRESS_MASK |
+ Gdk::BUTTON_RELEASE_MASK |
+ Gdk::POINTER_MOTION_MASK |
+ Gdk::KEY_PRESS_MASK |
+ Gdk::KEY_RELEASE_MASK);
+
+const gchar *nograbenv = getenv("INKSCAPE_NO_GRAB");
+static bool nograb = (nograbenv && *nograbenv && (*nograbenv != '0'));
+
+
+void knot_ref(SPKnot* knot) {
+ knot->ref_count++;
+}
+
+void knot_unref(SPKnot* knot) {
+ if (--knot->ref_count < 1) {
+ delete knot;
+ }
+}
+
+SPKnot::SPKnot(SPDesktop *desktop, gchar const *tip, Inkscape::CanvasItemCtrlType type, Glib::ustring const & name)
+ : desktop(desktop)
+ , ref_count(1)
+{
+ if (tip) {
+ this->tip = g_strdup (tip);
+ }
+
+ fill[SP_KNOT_STATE_NORMAL] = 0xffffff00;
+ fill[SP_KNOT_STATE_MOUSEOVER] = 0xff0000ff;
+ fill[SP_KNOT_STATE_DRAGGING] = 0xff0000ff;
+ fill[SP_KNOT_STATE_SELECTED] = 0x0000ffff;
+
+ stroke[SP_KNOT_STATE_NORMAL] = 0x01000000;
+ stroke[SP_KNOT_STATE_MOUSEOVER] = 0x01000000;
+ stroke[SP_KNOT_STATE_DRAGGING] = 0x01000000;
+ stroke[SP_KNOT_STATE_SELECTED] = 0x01000000;
+
+ image[SP_KNOT_STATE_NORMAL] = nullptr;
+ image[SP_KNOT_STATE_MOUSEOVER] = nullptr;
+ image[SP_KNOT_STATE_DRAGGING] = nullptr;
+ image[SP_KNOT_STATE_SELECTED] = nullptr;
+
+ ctrl = make_canvasitem<Inkscape::CanvasItemCtrl>(desktop->getCanvasControls(), type); // Shape, mode set
+ ctrl->set_name("CanvasItemCtrl:Knot:" + name);
+
+ // Are these needed?
+ ctrl->set_fill(0xffffff00);
+ ctrl->set_stroke(0x01000000);
+
+ _event_connection = ctrl->connect_event(sigc::mem_fun(*this, &SPKnot::eventHandler));
+
+ knot_created_callback(this);
+}
+
+SPKnot::~SPKnot() {
+ auto display = gdk_display_get_default();
+ auto seat = gdk_display_get_default_seat(display);
+ auto device = gdk_seat_get_pointer(seat);
+
+ if ((this->flags & SP_KNOT_GRABBED) && gdk_display_device_is_grabbed(display, device)) {
+ // This happens e.g. when deleting a node in node tool while dragging it
+ gdk_seat_ungrab(seat);
+ }
+
+ // Make sure the knot is not grabbed, as it's destructing can be deferred causing
+ // issues like https://gitlab.com/inkscape/inkscape/-/issues/4239
+ ctrl->ungrab();
+ ctrl.reset();
+
+ if (this->tip) {
+ g_free(this->tip);
+ this->tip = nullptr;
+ }
+
+ // FIXME: cannot snap to destroyed knot (lp:1309050)
+ // this->desktop->event_context->discard_delayed_snap_event();
+ knot_deleted_callback(this);
+}
+
+void SPKnot::startDragging(Geom::Point const &p, gint x, gint y, guint32 etime) {
+ // save drag origin
+ xp = x;
+ yp = y;
+ within_tolerance = true;
+
+ this->grabbed_rel_pos = p - this->pos;
+ this->drag_origin = this->pos;
+
+ if (!nograb && ctrl) {
+ ctrl->grab(KNOT_EVENT_MASK, _cursors[SP_KNOT_STATE_DRAGGING]);
+ }
+ this->setFlag(SP_KNOT_GRABBED, true);
+
+ grabbed = true;
+}
+
+void SPKnot::selectKnot(bool select)
+{
+ setFlag(SP_KNOT_SELECTED, select);
+}
+
+bool SPKnot::eventHandler(GdkEvent *event)
+{
+ /* Run client universal event handler, if present */
+ bool consumed = event_signal.emit(this, event);
+ if (consumed) {
+ return true;
+ }
+
+ bool key_press_event_unconsumed = false;
+
+ ref_count++;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+
+ switch (event->type) {
+ case GDK_2BUTTON_PRESS:
+ if (event->button.button == 1) {
+ doubleclicked_signal.emit(this, event->button.state);
+
+ grabbed = false;
+ moved = false;
+ consumed = true;
+ }
+ break;
+ case GDK_BUTTON_PRESS:
+ if ((event->button.button == 1) && desktop && desktop->event_context && !desktop->event_context->is_space_panning()) {
+ Geom::Point const p = desktop->w2d(Geom::Point(event->button.x, event->button.y));
+ startDragging(p, (gint) event->button.x, (gint) event->button.y, event->button.time);
+ mousedown_signal.emit(this, event->button.state);
+ consumed = true;
+ }
+ break;
+ case GDK_BUTTON_RELEASE:
+ if (event->button.button == 1 &&
+ desktop &&
+ desktop->event_context &&
+ !desktop->event_context->is_space_panning()) {
+
+ // If we have any pending snap event, then invoke it now
+ desktop->event_context->process_delayed_snap_event();
+ pressure = 0;
+
+ if (transform_escaped) {
+ transform_escaped = false;
+ consumed = true;
+ } else {
+ setFlag(SP_KNOT_GRABBED, false);
+
+ if (!nograb && ctrl) {
+ ctrl->ungrab();
+ }
+
+ if (moved) {
+ setFlag(SP_KNOT_DRAGGING, false);
+ ungrabbed_signal.emit(this, event->button.state);
+ } else {
+ click_signal.emit(this, event->button.state);
+ }
+
+ grabbed = false;
+ moved = false;
+ consumed = true;
+ }
+ }
+ Inkscape::UI::Tools::sp_update_helperpath(desktop);
+ break;
+
+ case GDK_MOTION_NOTIFY:
+
+ if (!(event->motion.state & GDK_BUTTON1_MASK) && flags & SP_KNOT_DRAGGING) {
+ pressure = 0;
+
+ if (transform_escaped) {
+ transform_escaped = false;
+ consumed = true;
+ } else {
+ setFlag(SP_KNOT_GRABBED, false);
+
+ if (!nograb && ctrl) {
+ ctrl->ungrab();
+ }
+
+ if (moved) {
+ setFlag(SP_KNOT_DRAGGING, false);
+ ungrabbed_signal.emit(this, event->motion.state);
+ } else {
+ click_signal.emit(this, event->motion.state);
+ }
+
+ grabbed = false;
+ moved = false;
+ consumed = true;
+ Inkscape::UI::Tools::sp_update_helperpath(desktop);
+ }
+ } else if (grabbed && desktop && desktop->event_context &&
+ !desktop->event_context->is_space_panning()) {
+ consumed = 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;
+
+ // Note: Synthesized events don't have a device.
+ if (event->motion.device && gdk_event_get_axis(event, GDK_AXIS_PRESSURE, &pressure)) {
+ pressure = CLAMP (pressure, 0, 1);
+ } else {
+ pressure = 0.5;
+ }
+
+ if (!moved) {
+ setFlag(SP_KNOT_DRAGGING, true);
+ grabbed_signal.emit(this, event->button.state);
+ }
+
+ desktop->event_context->snap_delay_handler(nullptr, this, reinterpret_cast<GdkEventMotion*>(event),
+ Inkscape::UI::Tools::DelayedSnapEvent::KNOT_HANDLER);
+ sp_knot_handler_request_position(event, this);
+ moved = true;
+ }
+ break;
+ case GDK_ENTER_NOTIFY:
+ setFlag(SP_KNOT_MOUSEOVER, true);
+ setFlag(SP_KNOT_GRABBED, false);
+
+ if (tip && desktop && desktop->event_context) {
+ desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, tip);
+ }
+ desktop->event_context->use_cursor(_cursors[SP_KNOT_STATE_MOUSEOVER]);
+
+ grabbed = false;
+ moved = false;
+ consumed = true;
+ break;
+ case GDK_LEAVE_NOTIFY:
+ setFlag(SP_KNOT_MOUSEOVER, false);
+ setFlag(SP_KNOT_GRABBED, false);
+
+ if (tip && desktop && desktop->event_context) {
+ desktop->event_context->defaultMessageContext()->clear();
+ }
+ desktop->event_context->use_cursor(_cursors[SP_KNOT_STATE_NORMAL]);
+
+ grabbed = false;
+ moved = false;
+ consumed = true;
+ break;
+ case GDK_KEY_PRESS: // keybindings for knot
+ switch (Inkscape::UI::Tools::get_latin_keyval(&event->key)) {
+ case GDK_KEY_Escape:
+ setFlag(SP_KNOT_GRABBED, false);
+
+ if (!nograb && ctrl) {
+ ctrl->ungrab();
+ }
+
+ if (moved) {
+ setFlag(SP_KNOT_DRAGGING, false);
+
+ ungrabbed_signal.emit(this, event->button.state);
+
+ DocumentUndo::undo(desktop->getDocument());
+ desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Node or handle drag canceled."));
+ transform_escaped = true;
+ consumed = true;
+ }
+
+ grabbed = false;
+ moved = false;
+
+ desktop->event_context->discard_delayed_snap_event();
+ break;
+ default:
+ consumed = false;
+ key_press_event_unconsumed = true;
+ break;
+ }
+ break;
+ default:
+ break;
+ }
+
+ knot_unref(this);
+
+ if (key_press_event_unconsumed) {
+ return false; // e.g. in case "%" was pressed to toggle snapping, or Q for quick zoom (while dragging a handle)
+ } else {
+ return consumed || grabbed;
+ }
+}
+
+void sp_knot_handler_request_position(GdkEvent *event, SPKnot *knot) {
+ Geom::Point const motion_w(event->motion.x, event->motion.y);
+ Geom::Point const motion_dt = knot->desktop->w2d(motion_w);
+ Geom::Point p = motion_dt - knot->grabbed_rel_pos;
+
+ knot->requestPosition(p, event->motion.state);
+ knot->desktop->getCanvas()->enable_autoscroll();
+ knot->desktop->set_coordinate_status(knot->pos); // display the coordinate of knot, not cursor - they may be different!
+
+ if (event->motion.state & GDK_BUTTON1_MASK) {
+ Inkscape::UI::Tools::gobble_motion_events(GDK_BUTTON1_MASK);
+ }
+}
+
+void SPKnot::show() {
+ this->setFlag(SP_KNOT_VISIBLE, true);
+}
+
+void SPKnot::hide() {
+ this->setFlag(SP_KNOT_VISIBLE, false);
+}
+
+void SPKnot::requestPosition(Geom::Point const &p, guint state) {
+ bool done = this->request_signal.emit(this, &const_cast<Geom::Point&>(p), state);
+
+ /* If user did not complete, we simply move knot to new position */
+ if (!done) {
+ this->setPosition(p, state);
+ }
+}
+
+void SPKnot::setPosition(Geom::Point const &p, guint state) {
+ this->pos = p;
+
+ if (ctrl) {
+ ctrl->set_position(p);
+ }
+
+ this->moved_signal.emit(this, p, state);
+}
+
+void SPKnot::moveto(Geom::Point const &p) {
+ this->pos = p;
+
+ if (ctrl) {
+ ctrl->set_position(p);
+ }
+}
+
+Geom::Point SPKnot::position() const {
+ return this->pos;
+}
+
+void SPKnot::setFlag(guint flag, bool set) {
+ if (set) {
+ this->flags |= flag;
+ } else {
+ this->flags &= ~flag;
+ }
+
+ switch (flag) {
+ case SP_KNOT_VISIBLE:
+ if (set) {
+ if (ctrl) {
+ ctrl->show();
+ }
+ } else {
+ if (ctrl) {
+ ctrl->hide();
+ }
+ }
+ break;
+ case SP_KNOT_MOUSEOVER:
+ case SP_KNOT_DRAGGING:
+ case SP_KNOT_SELECTED:
+ this->_setCtrlState();
+ break;
+ case SP_KNOT_GRABBED:
+ break;
+ default:
+ g_assert_not_reached();
+ break;
+ }
+}
+
+// TODO: Look at removing this and setting ctrl parameters directly.
+void SPKnot::updateCtrl() {
+
+ if (ctrl) {
+ if (shape_set) {
+ ctrl->set_shape(shape);
+ }
+ ctrl->set_mode(mode);
+ if (size_set) {
+ ctrl->set_size(size);
+ }
+ ctrl->set_angle(angle);
+ ctrl->set_anchor(anchor);
+ }
+
+ _setCtrlState();
+}
+
+void SPKnot::_setCtrlState() {
+ int state = SP_KNOT_STATE_NORMAL;
+
+ if (this->flags & SP_KNOT_DRAGGING) {
+ state = SP_KNOT_STATE_DRAGGING;
+ } else if (this->flags & SP_KNOT_MOUSEOVER) {
+ state = SP_KNOT_STATE_MOUSEOVER;
+ } else if (this->flags & SP_KNOT_SELECTED) {
+ state = SP_KNOT_STATE_SELECTED;
+ }
+ if (ctrl) {
+ ctrl->set_fill(fill[state]);
+ ctrl->set_stroke(stroke[state]);
+ }
+}
+
+
+void SPKnot::setSize(guint i) {
+ size = i;
+ size_set = true;
+}
+
+void SPKnot::setShape(Inkscape::CanvasItemCtrlShape s) {
+ shape = s;
+ shape_set = true;
+}
+
+void SPKnot::setAnchor(guint i) {
+ anchor = (SPAnchorType) i;
+}
+
+void SPKnot::setMode(Inkscape::CanvasItemCtrlMode m) {
+ mode = m;
+}
+
+void SPKnot::setAngle(double i) {
+ angle = i;
+}
+
+void SPKnot::setFill(guint32 normal, guint32 mouseover, guint32 dragging, guint32 selected) {
+ fill[SP_KNOT_STATE_NORMAL] = normal;
+ fill[SP_KNOT_STATE_MOUSEOVER] = mouseover;
+ fill[SP_KNOT_STATE_DRAGGING] = dragging;
+ fill[SP_KNOT_STATE_SELECTED] = selected;
+}
+
+void SPKnot::setStroke(guint32 normal, guint32 mouseover, guint32 dragging, guint32 selected) {
+ stroke[SP_KNOT_STATE_NORMAL] = normal;
+ stroke[SP_KNOT_STATE_MOUSEOVER] = mouseover;
+ stroke[SP_KNOT_STATE_DRAGGING] = dragging;
+ stroke[SP_KNOT_STATE_SELECTED] = selected;
+}
+
+void SPKnot::setImage(guchar* normal, guchar* mouseover, guchar* dragging, guchar* selected) {
+ image[SP_KNOT_STATE_NORMAL] = normal;
+ image[SP_KNOT_STATE_MOUSEOVER] = mouseover;
+ image[SP_KNOT_STATE_DRAGGING] = dragging;
+ image[SP_KNOT_STATE_SELECTED] = selected;
+}
+
+void SPKnot::setCursor(SPKnotStateType type, Glib::RefPtr<Gdk::Cursor> cursor)
+{
+ _cursors[type] = cursor;
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 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/knot/knot.h b/src/ui/knot/knot.h
new file mode 100644
index 0000000..e780d4e
--- /dev/null
+++ b/src/ui/knot/knot.h
@@ -0,0 +1,208 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_SP_KNOT_H
+#define SEEN_SP_KNOT_H
+
+/** \file
+ * Declarations for SPKnot: Desktop-bound visual control object.
+ */
+/*
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.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 <2geom/point.h>
+#include <sigc++/sigc++.h>
+#include <glibmm/ustring.h>
+#include <glibmm/refptr.h>
+#include <gdkmm/cursor.h>
+
+#include "knot-enums.h"
+#include "display/control/canvas-item-enums.h"
+#include "display/control/canvas-item-ptr.h"
+#include "enums.h"
+
+class SPDesktop;
+class SPItem;
+
+typedef union _GdkEvent GdkEvent;
+typedef unsigned int guint32;
+
+#define SP_KNOT(obj) (dynamic_cast<SPKnot*>(static_cast<SPKnot*>(obj)))
+#define SP_IS_KNOT(obj) (dynamic_cast<const SPKnot*>(static_cast<const SPKnot*>(obj)) != NULL)
+
+namespace Inkscape {
+class CanvasItemCtrl;
+}
+
+/**
+ * Desktop-bound visual control object.
+ *
+ * A knot is a draggable object, with callbacks to change something by
+ * dragging it, visually represented by a canvas item (mostly square).
+ *
+ * See also KnotHolderEntity.
+ * See also ControlPoint (which does the same kind of things).
+ */
+class SPKnot {
+public:
+ SPKnot(SPDesktop *desktop, char const *tip, Inkscape::CanvasItemCtrlType type, Glib::ustring const & name = Glib::ustring("unknown"));
+ virtual ~SPKnot();
+
+ SPKnot(SPKnot const&) = delete;
+ SPKnot& operator=(SPKnot const&) = delete;
+
+ int ref_count; // FIXME encapsulation
+
+ SPDesktop *desktop = nullptr; /**< Desktop we are on. */
+ CanvasItemPtr<Inkscape::CanvasItemCtrl> ctrl; /**< Our CanvasItemCtrl. */
+ SPItem *owner = nullptr; /**< Optional Owner Item */
+ SPItem *sub_owner = nullptr; /**< Optional SubOwner Item */
+ unsigned int flags = SP_KNOT_VISIBLE;
+
+ unsigned int size = 9; /**< Always square. Must be odd. */
+ bool size_set = false; /**< Use default size unless explicitly set. */
+ double angle = 0.0; /**< Angle of mesh handle. */
+ bool is_lpe = false; /**< is lpe knot. */
+ Geom::Point pos; /**< Our desktop coordinates. */
+ Geom::Point grabbed_rel_pos; /**< Grabbed relative position. */
+ Geom::Point drag_origin; /**< Origin of drag. */
+ SPAnchorType anchor = SP_ANCHOR_CENTER; /**< Anchor. */
+
+ bool grabbed = false;
+ bool moved = false;
+ int xp = 0.0; /**< Where drag started */
+ int yp = 0.0; /**< Where drag started */
+ int tolerance = 0;
+ bool within_tolerance = false;
+ bool transform_escaped = false; // true iff resize or rotate was cancelled by esc.
+
+ Inkscape::CanvasItemCtrlShape shape = Inkscape::CANVAS_ITEM_CTRL_SHAPE_SQUARE; /**< Shape type. */
+ bool shape_set = false; /**< Use default shape unless explicitly set. */
+ Inkscape::CanvasItemCtrlMode mode = Inkscape::CANVAS_ITEM_CTRL_MODE_XOR;
+
+ guint32 fill[SP_KNOT_VISIBLE_STATES];
+ guint32 stroke[SP_KNOT_VISIBLE_STATES];
+ unsigned char *image[SP_KNOT_VISIBLE_STATES];
+ Glib::RefPtr<Gdk::Cursor> _cursors[SP_KNOT_VISIBLE_STATES];
+
+ char *tip = nullptr;
+
+ sigc::connection _event_connection;
+
+ double pressure = 0.0; /**< The tablet pen pressure when the knot is being dragged. */
+
+ // FIXME: signals should NOT need to emit the object they came from, the callee should
+ // be able to figure that out
+ sigc::signal<void (SPKnot *, unsigned int)> click_signal;
+ sigc::signal<void (SPKnot*, unsigned int)> doubleclicked_signal;
+ sigc::signal<void (SPKnot*, unsigned int)> mousedown_signal;
+ sigc::signal<void (SPKnot*, unsigned int)> grabbed_signal;
+ sigc::signal<void (SPKnot *, unsigned int)> ungrabbed_signal;
+ sigc::signal<void (SPKnot *, Geom::Point const &, unsigned int)> moved_signal;
+ sigc::signal<bool (SPKnot*, GdkEvent*)> event_signal;
+
+ sigc::signal<bool (SPKnot*, Geom::Point*, unsigned int)> request_signal;
+
+
+ //TODO: all the members above should eventualle become private, accessible via setters/getters
+ void setSize(unsigned int i);
+ void setShape(Inkscape::CanvasItemCtrlShape s);
+ void setAnchor(unsigned int i);
+ void setMode(Inkscape::CanvasItemCtrlMode m);
+ void setAngle(double i);
+
+ void setFill(guint32 normal, guint32 mouseover, guint32 dragging, guint32 selected);
+ void setStroke(guint32 normal, guint32 mouseover, guint32 dragging, guint32 selected);
+ void setImage(unsigned char* normal, unsigned char* mouseover, unsigned char* dragging, unsigned char* selected);
+
+ void setCursor(SPKnotStateType type, Glib::RefPtr<Gdk::Cursor> cursor);
+
+ /**
+ * Show knot on its canvas.
+ */
+ void show();
+
+ /**
+ * Hide knot on its canvas.
+ */
+ void hide();
+
+ /**
+ * Set flag in knot, with side effects.
+ */
+ void setFlag(unsigned int flag, bool set);
+
+ /**
+ * Update knot's control state.
+ */
+ void updateCtrl();
+
+ /**
+ * Request or set new position for knot.
+ */
+ void requestPosition(Geom::Point const &pos, unsigned int state);
+
+ /**
+ * Update knot for dragging and tell canvas an item was grabbed.
+ */
+ void startDragging(Geom::Point const &p, int x, int y, guint32 etime);
+
+ /**
+ * Move knot to new position and emits "moved" signal.
+ */
+ void setPosition(Geom::Point const &p, unsigned int state);
+
+ /**
+ * Move knot to new position, without emitting a MOVED signal.
+ */
+ void moveto(Geom::Point const &p);
+ /**
+ * Select knot.
+ */
+ void selectKnot(bool select);
+
+ /**
+ * Returns position of knot.
+ */
+ Geom::Point position() const;
+
+ /**
+ * Event handler (from CanvasItem's).
+ */
+ bool eventHandler(GdkEvent *event);
+
+ bool is_visible() const { return (flags & SP_KNOT_VISIBLE) != 0; }
+ bool is_selected() const { return (flags & SP_KNOT_SELECTED) != 0; }
+ bool is_mouseover() const { return (flags & SP_KNOT_MOUSEOVER) != 0; }
+ bool is_dragging() const { return (flags & SP_KNOT_DRAGGING) != 0; }
+ bool is_grabbed() const { return (flags & SP_KNOT_GRABBED) != 0; }
+
+private:
+ /**
+ * Set knot control state (dragging/mouseover/normal).
+ */
+ void _setCtrlState();
+};
+
+void knot_ref(SPKnot* knot);
+void knot_unref(SPKnot* knot);
+
+void sp_knot_handler_request_position(GdkEvent *event, SPKnot *knot);
+
+#endif // SEEN_SP_KNOT_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/modifiers.cpp b/src/ui/modifiers.cpp
new file mode 100644
index 0000000..861660f
--- /dev/null
+++ b/src/ui/modifiers.cpp
@@ -0,0 +1,295 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Modifiers for inkscape
+ *
+ * The file provides a definition of all the ways shift/ctrl/alt modifiers
+ * are used in Inkscape, and allows users to customise them in keys.xml
+ *
+ *//*
+ * Authors:
+ * 2020 Martin Owens <doctormo@geek-2.com>
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cstring>
+#include <string>
+#include <bitset>
+#include <glibmm/i18n.h>
+
+#include "modifiers.h"
+#include "ui/tools/tool-base.h"
+
+namespace Inkscape {
+namespace Modifiers {
+
+Modifier::Lookup Modifier::_modifier_lookup;
+
+// these must be in the same order as the * enum in "modifiers.h"
+decltype(Modifier::_modifiers) Modifier::_modifiers {
+ // Canvas modifiers
+ {Type::CANVAS_PAN_Y, new Modifier("canvas-pan-y", _("Vertical pan"), _("Pan/Scroll up and down"), ALWAYS, CANVAS, SCROLL)},
+ {Type::CANVAS_PAN_X, new Modifier("canvas-pan-x", _("Horizontal pan"), _("Pan/Scroll left and right"), SHIFT, CANVAS, SCROLL)},
+ {Type::CANVAS_ZOOM, new Modifier("canvas-zoom", _("Canvas zoom"), _("Zoom in and out with scroll wheel"), CTRL, CANVAS, SCROLL)},
+ {Type::CANVAS_ROTATE, new Modifier("canvas-rotate", _("Canvas rotate"), _("Rotate the canvas with scroll wheel"), SHIFT | CTRL, CANVAS, SCROLL)},
+
+ // Select tool modifiers (minus transforms)
+ {Type::SELECT_ADD_TO, new Modifier("select-add-to", _("Add to selection"), _("Add items to existing selection"), SHIFT, SELECT, CLICK)},
+ {Type::SELECT_IN_GROUPS, new Modifier("select-in-groups", _("Select inside groups"), _("Ignore groups when selecting items"), CTRL, SELECT, CLICK)},
+ {Type::SELECT_TOUCH_PATH, new Modifier("select-touch-path", _("Select with touch-path"), _("Draw a band around items to select them"), ALT, SELECT, DRAG)},
+ {Type::SELECT_ALWAYS_BOX, new Modifier("select-always-box", _("Select with box"), _("Don't drag items, select more with a box"), SHIFT, SELECT, DRAG)},
+ {Type::SELECT_FIRST_HIT, new Modifier("select-first-hit", _("Select the first"), _("Drag the first item the mouse hits"), CTRL, SELECT, DRAG)},
+ {Type::SELECT_FORCE_DRAG, new Modifier("select-force-drag", _("Forced Drag"), _("Drag objects even if the mouse isn't over them"), ALT, SELECT, DRAG)},
+ {Type::SELECT_CYCLE, new Modifier("select-cycle", _("Cycle through objects"), _("Scroll through objects under the cursor"), ALT, SELECT, SCROLL)},
+
+ // Transform handle modifiers (applies to multiple tools)
+ {Type::MOVE_CONFINE, new Modifier("move-confine", _("Move one axis only"), _("When dragging items, confine to either x or y axis"), CTRL, MOVE, DRAG)},
+ {Type::MOVE_INCREMENT, new Modifier("move-increment", _("Move in increments"), _("Move the objects by set increments when dragging"), ALT, MOVE, DRAG)},
+ {Type::MOVE_SNAPPING, new Modifier("move-snapping", _("No Move Snapping"), _("Disable snapping when moving objects"), SHIFT, MOVE, DRAG)},
+ {Type::TRANS_CONFINE, new Modifier("trans-confine", _("Keep aspect ratio"), _("When resizing objects, confine the aspect ratio"), CTRL, TRANSFORM, DRAG)},
+ {Type::TRANS_INCREMENT, new Modifier("trans-increment", _("Transform in increments"), _("Scale, rotate or skew by set increments"), ALT, TRANSFORM, DRAG)},
+ {Type::TRANS_OFF_CENTER, new Modifier("trans-off-center", _("Transform around center"), _("When scaling, scale selection symmetrically around its rotation center. When rotating/skewing, transform relative to opposite corner/edge."), SHIFT, TRANSFORM, DRAG)},
+ {Type::TRANS_SNAPPING, new Modifier("trans-snapping", _("No Transform Snapping"), _("Disable snapping when transforming object."), SHIFT, TRANSFORM, DRAG)},
+ // Center handle click: seltrans.cpp:734 SHIFT
+ // Align handle click: seltrans.cpp:1365 SHIFT
+ {Type::BOOL_SHIFT, new Modifier("bool-shift", _("Switch mode"), _("Change shape builder mode temporarily by holding a modifier key."), SHIFT, BOOLEANS_TOOL, DRAG)},
+
+ {Type::NODE_GROW_LINEAR, new Modifier("node-grow-linear", _("Linear node selection"), _("Select the next nodes with scroll wheel or keyboard"), CTRL, NODE_TOOL, SCROLL)},
+ {Type::NODE_GROW_SPATIAL, new Modifier("node-grow-spatial", _("Spatial node selection"), _("Select more nodes with scroll wheel or keyboard"), ALWAYS, NODE_TOOL, SCROLL)},
+};
+
+decltype(Modifier::_category_names) Modifier::_category_names {
+ {NO_CATEGORY, _("No Category")},
+ {CANVAS, _("Canvas")},
+ {SELECT, _("Selection")},
+ {MOVE, _("Movement")},
+ {TRANSFORM, _("Transformations")},
+ {NODE_TOOL, _("Node Tool")},
+ {BOOLEANS_TOOL, _("Shape Builder")},
+};
+
+
+/**
+ * Given a Trigger, find which modifier is active (category lookup)
+ *
+ * @param trigger - The Modifier::Trigger category in the form "CANVAS | DRAG".
+ * @param button_state - The Gdk button state from an event.
+ * @return - Returns the best matching modifier id by the most number of keys.
+ */
+Type Modifier::which(Trigger trigger, int button_state)
+{
+ // Record each active modifier with it's weight
+ std::map<Type, unsigned long> scales;
+ for (auto const& [key, val] : _modifiers) {
+ if (val->get_trigger() == trigger) {
+ if(val->active(button_state)) {
+ scales[key] = val->get_weight();
+ }
+ }
+ }
+ // Sort the weightings
+ using pair_type = decltype(scales)::value_type;
+ auto sorted = std::max_element
+ (
+ std::begin(scales), std::end(scales),
+ [] (const pair_type & p1, const pair_type & p2) {
+ return p1.second < p2.second;
+ }
+ );
+ return sorted->first;
+}
+
+/**
+ * List all the modifiers available. Used in UI listing.
+ *
+ * @return a vector of Modifier objects.
+ */
+std::vector<Modifier *>
+Modifier::getList () {
+
+ std::vector<Modifier *> modifiers;
+ // Go through the dynamic modifier table
+ for( auto const& [key, val] : _modifiers ) {
+ modifiers.push_back(val);
+ }
+
+ return modifiers;
+};
+
+/**
+ * Test if this modifier is currently active.
+ *
+ * @param state - The GDK button state from an event
+ * @return a boolean, true if the modifiers for this action are active.
+ */
+bool Modifier::active(int state)
+{
+ // TODO:
+ // * ALT key is sometimes MOD1, MOD2 etc, if we find other ALT keys, set the ALT bit
+ // * SUPER key could be HYPER or META, these cases need to be considered.
+ auto and_mask = get_and_mask();
+ auto not_mask = get_not_mask();
+ auto active = Key::ALL_MODS & state;
+ // Check that all keys in AND mask are pressed, and NONE of the NOT mask are.
+ return and_mask != NEVER && ((active & and_mask) == and_mask) && (not_mask == NOT_SET || (active & not_mask) == 0);
+}
+
+/**
+ * Test if this modifier is currently active, adding or subtracting keyval
+ * during a key press or key release operation.
+ *
+ * @param state - The GDK button state from an event
+ * @param keyval - The GDK keyval from a key press/release event
+ * @param release - Boolean, if true the keyval is removed instead
+ *
+ * @return a boolean, true if the modifiers for this action are active.
+ */
+bool Modifier::active(int state, int keyval, bool release)
+{
+ return active(add_keyval(state, keyval, release));
+}
+
+/**
+ * Generate a label for any modifier keys based on the mask
+ *
+ * @param mask - The Modifier Mask such as {SHIFT & CTRL}
+ * @return a string of the keys needed for this mask to be true.
+ */
+std::string generate_label(KeyMask mask, std::string sep)
+{
+ auto ret = std::string();
+ if(mask == NOT_SET) {
+ return "-";
+ }
+ if(mask == NEVER) {
+ ret.append("[NEVER]");
+ return ret;
+ }
+ if(mask & CTRL) ret.append("Ctrl");
+ if(mask & SHIFT) {
+ if(!ret.empty()) ret.append(sep);
+ ret.append("Shift");
+ }
+ if(mask & ALT) {
+ if(!ret.empty()) ret.append(sep);
+ ret.append("Alt");
+ }
+ if(mask & SUPER) {
+ if(!ret.empty()) ret.append(sep);
+ ret.append("Super");
+ }
+ if(mask & HYPER) {
+ if(!ret.empty()) ret.append(sep);
+ ret.append("Hyper");
+ }
+ if(mask & META) {
+ if(!ret.empty()) ret.append(sep);
+ ret.append("Meta");
+ }
+ return ret;
+}
+
+/**
+ * Calculate the weight of this mask based on how many bits are set.
+ *
+ * @param mask - The Modifier Mask such as {SHIFT & CTRL}
+ * @return count of all modifiers being pressed (or excluded)
+ */
+unsigned long calculate_weight(KeyMask mask)
+{
+
+ if (mask < 0)
+ return 0;
+ std::bitset<sizeof(mask)> bit_mask(mask);
+ return bit_mask.count();
+}
+
+/**
+ * Set the responsive tooltip for this tool, given the selected types.
+ *
+ * @param message_context - The desktop's message context for showing tooltips
+ * @param event - The current event status (which keys are pressed)
+ * @param num_args - Number of Modifier::Type arguments to follow.
+ * @param ... - One or more Modifier::Type arguments.
+ */
+void responsive_tooltip(Inkscape::MessageContext *message_context, GdkEvent *event, int num_args, ...)
+{
+ std::string ctrl_msg = "<b>Ctrl</b>: ";
+ std::string shift_msg = "<b>Shift</b>: ";
+ std::string alt_msg = "<b>Alt</b>: ";
+
+ // NOTE: This will hide any keys changed to SUPER or multiple keys such as CTRL+SHIFT
+ va_list args;
+ va_start(args, num_args);
+ for(int i = 0; i < num_args; i++) {
+ auto modifier = Modifier::get(va_arg(args, Type));
+ auto name = std::string(_(modifier->get_name()));
+ switch (modifier->get_and_mask()) {
+ case CTRL:
+ ctrl_msg += name + ", ";
+ break;
+ case SHIFT:
+ shift_msg += name + ", ";
+ break;
+ case ALT:
+ alt_msg += name + ", ";
+ break;
+ default:
+ g_warning("Unhandled responsivle tooltip: %s", name.c_str());
+ }
+ }
+ va_end(args);
+ ctrl_msg.erase(ctrl_msg.size() - 2);
+ shift_msg.erase(shift_msg.size() - 2);
+ alt_msg.erase(alt_msg.size() - 2);
+
+ Inkscape::UI::Tools::sp_event_show_modifier_tip(message_context, event,
+ ctrl_msg.c_str(), shift_msg.c_str(), alt_msg.c_str());
+}
+
+static const std::map<int, int> key_map = {
+ {GDK_KEY_Alt_L, GDK_MOD1_MASK},
+ {GDK_KEY_Alt_R, GDK_MOD1_MASK},
+ {GDK_KEY_Control_L, GDK_CONTROL_MASK},
+ {GDK_KEY_Control_R, GDK_CONTROL_MASK},
+ {GDK_KEY_Shift_L, GDK_SHIFT_MASK},
+ {GDK_KEY_Shift_R, GDK_SHIFT_MASK},
+ {GDK_KEY_Meta_L, GDK_META_MASK},
+ {GDK_KEY_Meta_R, GDK_META_MASK},
+};
+
+/**
+ * Add or remove the GDK keyval to the button state if it's one of the
+ * keys that define the key mask. Useful for PRESS and RELEASE events.
+ *
+ * @param state - The GDK button state from an event
+ * @param keyval - The GDK keyval from a key press/release event
+ * @param release - Boolean, if true the keyval is removed instead
+ *
+ * @return a new state including the requested change.
+ */
+int add_keyval(int state, int keyval, bool release)
+{
+ if (auto it = key_map.find(keyval); it != key_map.end()) {
+ if (release)
+ state &= ~it->second;
+ else
+ state |= it->second;
+ }
+ return state;
+}
+
+} // namespace Modifiers
+} // 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/modifiers.h b/src/ui/modifiers.h
new file mode 100644
index 0000000..1f52fb3
--- /dev/null
+++ b/src/ui/modifiers.h
@@ -0,0 +1,256 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_SP_MODIFIERS_H
+#define SEEN_SP_MODIFIERS_H
+/*
+ * Copyright (C) 2020 Martin Owens <doctormo@gmail.com>
+
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cstring>
+#include <cstdarg>
+#include <string>
+#include <vector>
+#include <map>
+
+#include <gdk/gdk.h>
+
+#include "message-context.h"
+
+namespace Inkscape {
+namespace Modifiers {
+
+using KeyMask = int;
+using Trigger = int;
+
+enum Key : KeyMask {
+ NEVER = -2, // Never happen, switch off
+ NOT_SET = -1, // Not set (user or keys)
+ ALWAYS = 0, // Always happens, no modifier needed
+ SHIFT = GDK_SHIFT_MASK,
+ CTRL = GDK_CONTROL_MASK,
+ ALT = GDK_MOD1_MASK,
+ SUPER = GDK_SUPER_MASK,
+ HYPER = GDK_HYPER_MASK,
+ META = GDK_META_MASK,
+ ALL_MODS = SHIFT | CTRL | ALT | SUPER | HYPER | META,
+};
+
+// Triggers used for collision warnings, two tools are using the same trigger
+enum Triggers : Trigger {
+ NO_CATEGORY, CANVAS, SELECT, MOVE, TRANSFORM,
+ NODE_TOOL, BOOLEANS_TOOL,
+ // Action taken to trigger this modifier, starts at
+ // bit 6 so categories and triggers can be combined.
+ CLICK = 32,
+ DRAG = 64,
+ SCROLL = 128,
+};
+
+/**
+ * This anonymous enum is used to provide a list of the Shifts
+ */
+enum class Type {
+ // {TOOL_NAME}_{ACTION_NAME}
+
+ // Canvas tools (applies to any tool selection)
+ CANVAS_PAN_Y, // Pan up and down {NOTHING+SCROLL}
+ CANVAS_PAN_X, // Pan left and right {SHIFT+SCROLL}
+ CANVAS_ZOOM, // Zoom in and out {CTRL+SCROLL}
+ CANVAS_ROTATE, // Rotate CW and CCW {CTRL+SHIFT+SCROLL}
+
+ // Select tool (minus transform)
+ SELECT_ADD_TO, // Add selection {SHIFT+CLICK}
+ SELECT_IN_GROUPS, // Select within groups {CTRL+CLICK}
+ SELECT_TOUCH_PATH, // Draw band to select {ALT+DRAG+Nothing selected}
+ SELECT_ALWAYS_BOX, // Draw box to select {SHIFT+DRAG}
+ SELECT_FIRST_HIT, // Start dragging first item hit {CTRL+DRAG} (Is this an actual feature?)
+ SELECT_FORCE_DRAG, // Drag objects even if the mouse isn't over them {ALT+DRAG+Selected}
+ SELECT_CYCLE, // Cycle through objects under cursor {ALT+SCROLL}
+
+ // Transform handles (applies to multiple tools)
+ MOVE_CONFINE, // Limit dragging to X OR Y only {DRAG+CTRL}
+ MOVE_INCREMENT, // Move objects by fixed amounts {DRAG+ALT}
+ MOVE_SNAPPING, // Disable snapping while moving {DRAG+SHIFT}
+ TRANS_CONFINE, // Confine resize aspect ratio {HANDLE+CTRL}
+ TRANS_INCREMENT, // Scale/Rotate/skew by fixed ratio angles {HANDLE+ALT}
+ TRANS_OFF_CENTER, // Scale/Rotate/skew from opposite corner {HANDLE+SHIFT}
+ TRANS_SNAPPING, // Disable snapping while transforming {HANDLE+SHIFT}
+
+ BOOL_SHIFT, // Shift the shape builder into its alternative mode.
+ NODE_GROW_LINEAR, // Scroll wheel selection of nodes
+ NODE_GROW_SPATIAL, // Scroll wheel selection of nodes
+ // TODO: Alignment omitted because it's UX is not completed
+};
+
+// Generate a label such as Shift+Ctrl from any KeyMask
+std::string generate_label(KeyMask mask, std::string sep = "+");
+unsigned long calculate_weight(KeyMask mask);
+
+// Generate a responsivle tooltip set
+void responsive_tooltip(Inkscape::MessageContext *message_context, GdkEvent *event, int num_args, ...);
+
+int add_keyval(int state, int keyval, bool release = false);
+
+/**
+ * A class to represent ways functionality is driven by shift modifiers
+ */
+class Modifier {
+private:
+ /** An easy to use definition of the table of modifiers by Type and ID. */
+ typedef std::map<Type, Modifier *> Container;
+ typedef std::map<std::string, Modifier *> Lookup;
+ typedef std::map<Trigger, std::string> CategoryNames;
+
+ /** A table of all the created modifiers and their ID lookups. */
+ static Container _modifiers;
+ static Lookup _modifier_lookup;
+ static CategoryNames _category_names;
+
+ char const * _id; // A unique id used by keys.xml to identify it
+ char const * _name; // A descriptive name used in preferences UI
+ char const * _desc; // A more verbose description used in preferences UI
+
+ Trigger _category; // The category of tool, what it might conflict with
+ Trigger _trigger; // The type of trigger/action
+
+ // Default values if nothing is set in keys.xml
+ KeyMask _and_mask_default; // The pressed keys must have these bits set
+ unsigned long _weight_default = 0;
+
+ // User set data, set by keys.xml (or other included file)
+ KeyMask _and_mask_keys = NOT_SET;
+ KeyMask _not_mask_keys = NOT_SET;
+ unsigned long _weight_keys = 0;
+ KeyMask _and_mask_user = NOT_SET;
+ KeyMask _not_mask_user = NOT_SET;
+ unsigned long _weight_user = 0;
+
+protected:
+
+public:
+
+ char const * get_id() const { return _id; }
+ char const * get_name() const { return _name; }
+ char const * get_description() const { return _desc; }
+ Trigger get_trigger() const { return _category | _trigger; }
+
+ // Set user value
+ void set_keys(KeyMask and_mask, KeyMask not_mask) {
+ _and_mask_keys = and_mask;
+ _not_mask_keys = not_mask;
+ _weight_keys = calculate_weight(and_mask) + calculate_weight(not_mask);
+ }
+ void set_user(KeyMask and_mask, KeyMask not_mask) {
+ _and_mask_user = and_mask;
+ _not_mask_user = not_mask;
+ _weight_user = calculate_weight(and_mask) + calculate_weight(not_mask);
+ }
+ void unset_keys() { set_keys(NOT_SET, NOT_SET); }
+ void unset_user() { set_user(NOT_SET, NOT_SET); }
+ bool is_set_user() const { return _and_mask_user != NOT_SET; }
+
+ // Get value, either user defined value or default
+ KeyMask get_and_mask() {
+ if(_and_mask_user != NOT_SET) return _and_mask_user;
+ if(_and_mask_keys != NOT_SET) return _and_mask_keys;
+ return _and_mask_default;
+ }
+ KeyMask get_not_mask() {
+ // The not mask is enabled by the AND mask being set first.
+ if(_and_mask_user != NOT_SET) return _not_mask_user;
+ if(_and_mask_keys != NOT_SET) return _not_mask_keys;
+ return NOT_SET;
+ }
+ // Return number of bits set for the keys
+ unsigned long get_weight() {
+ if(_and_mask_user != NOT_SET) return _weight_user;
+ if(_and_mask_keys != NOT_SET) return _weight_keys;
+ return _weight_default;
+ }
+
+ // Generate labels such as "Shift+Ctrl" for the active modifier
+ std::string get_label() { return generate_label(get_and_mask()); }
+ std::string get_category() { return _category_names[_category]; }
+
+ // Configurations for saving the xml file
+ bool get_config_user_disabled() { return (_and_mask_user == NEVER); }
+ std::string get_config_user_and() { return generate_label(_and_mask_user, ","); }
+ std::string get_config_user_not() { return generate_label(_not_mask_keys, ","); }
+
+ /**
+ * Inititalizes the Modifier with the parameters.
+ *
+ * @param id Goes to \c _id.
+ * @param name Goes to \c _name.
+ * @param desc Goes to \c _desc.
+ * @param default_ Goes to \c _default.
+ */
+ Modifier(char const * id,
+ char const * name,
+ char const * desc,
+ const KeyMask and_mask,
+ const Trigger category,
+ const Trigger trigger) :
+ _id(id),
+ _name(name),
+ _desc(desc),
+ _and_mask_default(and_mask),
+ _category(category),
+ _trigger(trigger)
+ {
+ _modifier_lookup.emplace(_id, this);
+ _weight_default = calculate_weight(and_mask);
+ }
+ // Delete the destructor, because we are eternal
+ ~Modifier() = delete;
+
+ static Type which(Trigger trigger, int button_state);
+ static std::vector<Modifier *>getList ();
+ bool active(int button_state);
+ bool active(int button_state, int keyval, bool release = false);
+
+ /**
+ * A function to turn an enum index into a modifier object.
+ *
+ * @param index The enum index to be translated
+ * @return A pointer to a modifier object or a NULL if not found.
+ */
+ static Modifier * get(Type index) {
+ return _modifiers[index];
+ }
+ /**
+ * A function to turn a string id into a modifier object.
+ *
+ * @param id The id string to be translated
+ * @return A pointer to a modifier object or a NULL if not found.
+ */
+ static Modifier * get(char const * id) {
+ Modifier *modifier = nullptr;
+ Lookup::iterator mod_found = _modifier_lookup.find(id);
+
+ if (mod_found != _modifier_lookup.end()) {
+ modifier = mod_found->second;
+ }
+
+ return modifier;
+ }
+
+}; // Modifier class
+
+} // namespace Modifiers
+} // namespace Inkscape
+
+
+#endif // SEEN_SP_MODIFIERS_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/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/operation-blocker.h b/src/ui/operation-blocker.h
new file mode 100644
index 0000000..b9d714c
--- /dev/null
+++ b/src/ui/operation-blocker.h
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifndef SEEN_OPERATION_BLOCKER_H
+#define SEEN_OPERATION_BLOCKER_H
+
+// cooperative counter-based pending operation blocking
+
+class OperationBlocker {
+public:
+ OperationBlocker() = default;
+
+ bool pending() const {
+ return _counter > 0;
+ }
+
+ class scoped_block {
+ public:
+ scoped_block(unsigned int& counter): _c(counter) {
+ ++_c;
+ }
+
+ ~scoped_block() {
+ --_c;
+ }
+
+ private:
+ unsigned int& _c;
+ };
+
+ scoped_block block() {
+ return scoped_block(_counter);
+ }
+
+private:
+ unsigned int _counter = 0;
+};
+
+#endif \ No newline at end of file
diff --git a/src/ui/selected-color.cpp b/src/ui/selected-color.cpp
new file mode 100644
index 0000000..81cc4d2
--- /dev/null
+++ b/src/ui/selected-color.cpp
@@ -0,0 +1,155 @@
+// 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;
+}
+
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ 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..6010b76
--- /dev/null
+++ b/src/ui/selected-color.h
@@ -0,0 +1,100 @@
+// 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 <glibmm/ustring.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 emitIccChanged() { signal_icc_changed.emit(); }
+
+ void setHeld(bool held);
+
+ sigc::signal<void ()> signal_grabbed;
+ sigc::signal<void ()> signal_dragged;
+ sigc::signal<void ()> signal_released;
+ sigc::signal<void ()> signal_changed;
+ sigc::signal<void ()> signal_icc_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, bool no_alpha) 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..cd17888
--- /dev/null
+++ b/src/ui/shape-editor-knotholders.cpp
@@ -0,0 +1,2611 @@
+// 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.
+ */
+
+// Declared in shape-editor.cpp.
+
+#include <glibmm/i18n.h>
+
+#include "preferences.h"
+#include "desktop.h"
+#include "document.h"
+#include "style.h"
+
+#include "live_effects/effect.h"
+
+#include "object/box3d.h"
+#include "object/sp-marker.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"
+#include "svg/css-ostringstream.h"
+
+#include "ui/knot/knot-holder.h"
+#include "ui/knot/knot-holder-entity.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 MarkerKnotHolder : public KnotHolder {
+public:
+ MarkerKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler, double edit_rotation, int edit_marker_mode);
+ ~MarkerKnotHolder() 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);
+ for (auto i : knot_holder->entity) {
+ i->knot->is_lpe = true;
+ }
+ return knot_holder;
+}
+
+} // namespace
+
+namespace Inkscape {
+namespace UI {
+
+KnotHolder *createKnotHolder(SPItem *item, SPDesktop *desktop, double edit_rotation = 0.0, int edit_marker_mode = -1)
+{
+ KnotHolder *knotholder = nullptr;
+
+ if (is<SPRect>(item)) {
+ knotholder = new RectKnotHolder(desktop, item, nullptr);
+ } else if (is<SPBox3D>(item)) {
+ knotholder = new Box3DKnotHolder(desktop, item, nullptr);
+ } else if (is<SPMarker>(item)) {
+ knotholder = new MarkerKnotHolder(desktop, item, nullptr, edit_rotation, edit_marker_mode);
+ } else if (is<SPGenericEllipse>(item)) {
+ knotholder = new ArcKnotHolder(desktop, item, nullptr);
+ } else if (is<SPStar>(item)) {
+ knotholder = new StarKnotHolder(desktop, item, nullptr);
+ } else if (is<SPSpiral>(item)) {
+ knotholder = new SpiralKnotHolder(desktop, item, nullptr);
+ } else if (is<SPOffset>(item)) {
+ knotholder = new OffsetKnotHolder(desktop, item, nullptr);
+ } else if (is<SPText>(item)) {
+ auto text = 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 (is<SPTextPath>(child)) is_on_path = true;
+ }
+ if (!is_on_path) {
+ knotholder = new TextKnotHolder(desktop, item, nullptr);
+ }
+ } else {
+ auto flowtext = cast<SPFlowtext>(item);
+ if (flowtext && flowtext->has_internal_frame()) {
+ knotholder = new FlowtextKnotHolder(desktop, flowtext->get_frame(nullptr), nullptr);
+ } else if ((item->style->fill.isPaintserver() && cast<SPPattern>(item->style->getFillPaintServer())) ||
+ (item->style->stroke.isPaintserver() && 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;
+
+ auto lpe = 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
+{
+ auto rect = 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)
+{
+ auto rect = 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)
+{
+ auto rect = 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
+{
+ auto rect = 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)
+{
+ auto rect = 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)
+{
+ auto rect = 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
+{
+ auto rect = 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)
+{
+ auto rect = 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
+{
+ auto rect = 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)
+{
+ auto rect = 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
+{
+ auto rect = 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)
+{
+ auto rect = 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::CANVAS_ITEM_CTRL_TYPE_ROTATE, "Rect:rx",
+ _("Adjust the <b>horizontal rounding</b> radius; with <b>Ctrl</b> "
+ "to make the vertical radius the same"));
+
+ entity_ry->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_ROTATE, "Rect:ry",
+ _("Adjust the <b>vertical rounding</b> radius; with <b>Ctrl</b> "
+ "to make the horizontal radius the same"));
+
+ entity_wh->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Rect:wh",
+ _("Adjust the <b>width and height</b> of the rectangle; with <b>Ctrl</b> "
+ "to lock ratio or stretch in one dimension only"));
+
+ entity_xy->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Rect:xy",
+ _("Adjust the <b>width and height</b> of the rectangle; with <b>Ctrl</b> "
+ "to lock ratio or stretch in one dimension only"));
+
+ entity_center->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_POINT, "Rect:center",
+ _("Drag to move the rectangle"));
+
+ 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
+{
+ auto box = cast<SPBox3D>(item);
+ if (box) {
+ return box->get_corner_screen(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);
+ auto box = 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;
+ }
+
+ box->set_corner (knot_id, s * i2dt, movement, (state & GDK_CONTROL_MASK));
+ box->set_z_orders();
+ box->position_set();
+}
+
+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
+{
+ auto box = cast<SPBox3D>(item);
+ if (box) {
+ return box->get_center_screen();
+ } 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);
+
+ auto box = cast<SPBox3D>(item);
+ g_assert(box != nullptr);
+ Geom::Affine const i2dt (item->i2dt_affine ());
+
+ box->set_center(s * i2dt, origin * i2dt, !(state & GDK_SHIFT_MASK) ? Box3D::XY : Box3D::Z,
+ state & GDK_CONTROL_MASK);
+
+ box->set_z_orders();
+ box->position_set();
+}
+
+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::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Box3D:corner0",
+ _("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::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Box3D:corner1",
+ _("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::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Box3D:corner2",
+ _("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::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Box3D:corner3",
+ _("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::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Box3D:corner4",
+ _("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::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Box3D:corner5",
+ _("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::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Box3D:corner6",
+ _("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::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Box3D:corner7",
+ _("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::CANVAS_ITEM_CTRL_TYPE_POINT, "Box3D:center",
+ _("Move the box in perspective"));
+
+ 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();
+}
+
+/* SPMarker */
+
+// marker x scale = (marker width)/(view box width)
+double
+getMarkerXScale(SPItem* item){
+
+ auto sp_marker = cast<SPMarker>(item);
+ g_assert(sp_marker != nullptr);
+
+ return ((sp_marker->viewBox.width() != 0) ? sp_marker->markerWidth.computed/sp_marker->viewBox.width() : 1.0);
+}
+
+double
+getMarkerYScale(SPItem* item){
+
+ auto sp_marker = cast<SPMarker>(item);
+ g_assert(sp_marker != nullptr);
+
+ return ((sp_marker->viewBox.height() != 0) ? sp_marker->markerHeight.computed/sp_marker->viewBox.height() : 1.0);
+}
+
+/*
+- edit_rotation is the tangent angle that is used in orient auto mode.
+- edit_rotation is applied in the edit_transform, it needs to be undone and then the orient.computed can be applied.
+*/
+Geom::Affine
+getMarkerRotation(SPItem* item, double edit_rotation, int edit_marker_mode, bool reverse = false){
+
+ auto sp_marker = cast<SPMarker>(item);
+ g_assert(sp_marker != nullptr);
+
+ Geom::Affine rot = Geom::Rotate::from_degrees(0.0);
+
+ if ((sp_marker->orient_mode == MARKER_ORIENT_AUTO_START_REVERSE) && (edit_marker_mode == SP_MARKER_LOC_START)) {
+ rot = Geom::Rotate::from_degrees(180.0);
+ } else if (sp_marker->orient_mode == MARKER_ORIENT_ANGLE) {
+ rot = reverse? Geom::Rotate::from_degrees(edit_rotation - sp_marker->orient.computed) : Geom::Rotate::from_degrees(sp_marker->orient.computed - edit_rotation);
+ }
+
+ return rot;
+}
+
+// used to translate the knots when the marker's minimum bounds are less than zero.
+Geom::Rect
+getMarkerBounds(SPItem* item, SPDesktop *desktop){
+ auto sp_marker = cast<SPMarker>(item);
+ SPDocument *doc = desktop->getDocument();
+
+ g_assert(sp_marker != nullptr);
+ g_assert(doc != nullptr);
+
+ std::vector<SPObject*> items = sp_marker->childList(false, SPObject::ActionBBox);
+ Geom::OptRect r;
+
+ for (auto *i : items) {
+ auto item = cast<SPItem>(i);
+ r.unionWith(item->desktopVisualBounds());
+ }
+ Geom::Rect bounds(r->min() * doc->dt2doc(), r->max() * doc->dt2doc());
+ return bounds;
+}
+
+/*
+- this knot sets the refX/refY attributes of the marker
+- this knot is actually shown in the center of the shape vs the actual
+refX/refY position to make it more intuitive
+*/
+
+class MarkerKnotHolderEntityReference : public KnotHolderEntity {
+public:
+ double _edit_rotation = 0.0;
+ int _edit_marker_mode = -1;
+
+ MarkerKnotHolderEntityReference(double edit_rotation, int edit_marker_mode)
+ : _edit_rotation(edit_rotation),
+ _edit_marker_mode(edit_marker_mode)
+ {
+ }
+
+ 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
+MarkerKnotHolderEntityReference::knot_get() const
+{
+ auto sp_marker = cast<SPMarker>(item);
+ g_assert(sp_marker != nullptr);
+
+ // knot is actually shown at center of marker, not at its reference point
+ return Geom::Point((-sp_marker->refX.computed + getMarkerBounds(item, desktop).min()[Geom::X] + sp_marker->viewBox.width()/2) * getMarkerXScale(item),
+ (-sp_marker->refY.computed + getMarkerBounds(item, desktop).min()[Geom::Y] + sp_marker->viewBox.height()/2) * getMarkerYScale(item))
+ * getMarkerRotation(item, _edit_rotation, _edit_marker_mode);
+}
+
+void
+MarkerKnotHolderEntityReference::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ auto sp_marker = cast<SPMarker>(item);
+ g_assert(sp_marker != nullptr);
+
+ Geom::Point s = -p;
+ s = s * getMarkerRotation(item, _edit_rotation, _edit_marker_mode, true);
+ sp_marker->refX = (s[Geom::X]/ getMarkerXScale(item)) + getMarkerBounds(item, desktop).min()[Geom::X] + sp_marker->viewBox.width()/2;
+ sp_marker->refY = (s[Geom::Y]/ getMarkerYScale(item)) + getMarkerBounds(item, desktop).min()[Geom::Y] + sp_marker->viewBox.height()/2;
+
+ sp_marker->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+// marker orient section - handles rotation
+
+class MarkerKnotHolderEntityOrient : public KnotHolderEntity {
+public:
+ double _edit_rotation = 0.0;
+ int _edit_marker_mode = -1;
+
+ bool originals_set = false;
+
+ // angle that the center of the marker makes with the orient knot
+ double original_center_angle = 0;
+ double original_radius = 0;
+ Geom::Point original_center = Geom::Point(0, 0);
+
+ MarkerKnotHolderEntityOrient(double edit_rotation, int edit_marker_mode)
+ : _edit_rotation(edit_rotation),
+ _edit_marker_mode(edit_marker_mode)
+ {
+ }
+
+ 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);
+
+};
+
+void MarkerKnotHolderEntityOrient::knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) {
+ originals_set = false;
+}
+
+Geom::Point
+MarkerKnotHolderEntityOrient::knot_get() const
+{
+ auto sp_marker = cast<SPMarker>(item);
+ g_assert(sp_marker != nullptr);
+
+ return Geom::Point(
+ (-sp_marker->refX.computed + sp_marker->viewBox.width() + getMarkerBounds(item, desktop).min()[Geom::X]) * getMarkerXScale(item),
+ (-sp_marker->refY.computed + getMarkerBounds(item, desktop).min()[Geom::Y]) * getMarkerYScale(item))
+ * getMarkerRotation(item, _edit_rotation, _edit_marker_mode);
+}
+
+void
+MarkerKnotHolderEntityOrient::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ if(!originals_set) {
+ auto sp_marker = cast<SPMarker>(item);
+ g_assert(sp_marker != nullptr);
+
+ /*
+ - if the marker is set to auto or auto-start-reverse, set its type to orient
+ - calculate and set the default angle for the orient mode
+ */
+ if (sp_marker->orient_mode != MARKER_ORIENT_ANGLE) {
+ sp_marker->orient = (((sp_marker->orient_mode == MARKER_ORIENT_AUTO_START_REVERSE) && (_edit_marker_mode == SP_MARKER_LOC_START)) ? _edit_rotation + 180.0 : _edit_rotation);
+ sp_marker->orient_mode = MARKER_ORIENT_ANGLE;
+ sp_marker->orient_set = true;
+ }
+
+ /*
+ - the original marker center is used to calculate the angle with mouse
+ - the refX/refY will be changing to adjust for the new rotation to give appearance that it is stationary onCanvas while editing.
+ */
+ original_center = Geom::Point(
+ (-sp_marker->refX.computed + getMarkerBounds(item, desktop).min()[Geom::X] + sp_marker->viewBox.width()/2) * getMarkerXScale(item),
+ (-sp_marker->refY.computed + getMarkerBounds(item, desktop).min()[Geom::Y] + sp_marker->viewBox.height()/2) * getMarkerYScale(item))
+ * getMarkerRotation(item, _edit_rotation, _edit_marker_mode);
+
+ original_center_angle = atan2(
+ sp_marker->markerHeight.computed - sp_marker->markerHeight.computed/2,
+ sp_marker->markerWidth.computed - sp_marker->markerWidth.computed/2
+ ) * 180.0/M_PI;
+
+ original_radius = L2(original_center);
+ originals_set = true;
+ }
+
+ set_internal(p, origin, state);
+ update_knot();
+}
+
+void
+MarkerKnotHolderEntityOrient::set_internal(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ auto sp_marker = cast<SPMarker>(item);
+ g_assert(sp_marker != nullptr);
+
+ // edit_rotation is the tangest angle to the shapes and needs to be taken into account while setting the orient angle
+ double new_angle = atan2(p[Geom::Y] - original_center[Geom::Y], p[Geom::X] - original_center[Geom::X]) * 180.0/M_PI;
+ new_angle = new_angle + _edit_rotation + original_center_angle;
+
+ double axis_angle = -((atan2(original_center) * 180.0/M_PI) + _edit_rotation);
+
+ sp_marker->orient = new_angle;
+ sp_marker->orient_mode = MARKER_ORIENT_ANGLE;
+ sp_marker->orient_set = true;
+
+ Geom::Point ref = Geom::Point(
+ (-(original_radius * cos(-(axis_angle + sp_marker->orient.computed) * M_PI/180.0))/getMarkerXScale(item)) + getMarkerBounds(item, desktop).min()[Geom::X] + sp_marker->viewBox.width()/2,
+ (-(original_radius * sin(-(axis_angle + sp_marker->orient.computed) * M_PI/180.0))/getMarkerYScale(item)) + getMarkerBounds(item, desktop).min()[Geom::Y] + sp_marker->viewBox.height()/2);
+
+ sp_marker->refX = ref[Geom::X];
+ sp_marker->refY = ref[Geom::Y];
+
+ sp_marker->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+// marker has multiple scaling knots at its corners
+
+class MarkerKnotHolderEntityScale : public KnotHolderEntity {
+public:
+ double _edit_rotation = 0.0;
+ int _edit_marker_mode = -1;
+
+ /*
+ - related to the position(+/-) of the scaling knot in reference to the center
+ - makes sure scaling works correctly for derived classes
+ */
+ int _x_Sign = 1;
+ int _y_Sign = 1;
+
+ bool originals_set = false;
+
+ double original_scaleX = 1;
+ double original_scaleY = 1;
+
+ double original_refX = 0;
+ double original_refY = 0;
+
+ double original_width = 0;
+ double original_height = 0;
+
+ MarkerKnotHolderEntityScale(double edit_rotation, int edit_marker_mode, int x_Sign, int y_Sign)
+ : _edit_rotation(edit_rotation),
+ _edit_marker_mode(edit_marker_mode),
+ _x_Sign(x_Sign),
+ _y_Sign(y_Sign)
+ {
+ }
+
+ 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 knot_get() const override;
+
+protected:
+ void set_internal(Geom::Point const &p, Geom::Point const &origin, unsigned int state);
+
+};
+
+void
+MarkerKnotHolderEntityScale::knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) {
+ originals_set = false;
+}
+
+Geom::Point
+MarkerKnotHolderEntityScale::knot_get() const
+{
+ auto sp_marker = cast<SPMarker>(item);
+ g_assert(sp_marker != nullptr);
+
+ return Geom::Point(
+ (-sp_marker->refX.computed + sp_marker->viewBox.width() + getMarkerBounds(item, desktop).min()[Geom::X]) * getMarkerXScale(item),
+ (-sp_marker->refY.computed + sp_marker->viewBox.height() + getMarkerBounds(item, desktop).min()[Geom::Y]) * getMarkerYScale(item))
+ * getMarkerRotation(item, _edit_rotation, _edit_marker_mode);
+}
+
+void
+MarkerKnotHolderEntityScale::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ // keep track of the original values before the knot/mouse position is being moved
+ if(!originals_set) {
+
+ auto sp_marker = cast<SPMarker>(item);
+ g_assert(sp_marker != nullptr);
+
+ original_scaleX = getMarkerXScale(item);
+ original_scaleY = getMarkerYScale(item);
+
+ original_refX = sp_marker->refX.computed;
+ original_refY = sp_marker->refY.computed;
+
+ original_width = sp_marker->viewBox.width();
+ original_height = sp_marker->viewBox.height();
+
+ originals_set = true;
+ }
+
+ set_internal(p, origin, state);
+ update_knot();
+}
+
+// scaling takes place around center of marker, not its reference point
+void
+MarkerKnotHolderEntityScale::set_internal(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ auto sp_marker = cast<SPMarker>(item);
+ g_assert(sp_marker != nullptr);
+
+ Geom::Point adjusted_origin = origin;
+ Geom::Point adjusted_p = p;
+
+ if(sp_marker->orient_mode == MARKER_ORIENT_ANGLE) {
+
+ adjusted_origin = adjusted_origin
+ * Geom::Translate(getMarkerBounds(item, desktop).min())
+ * Geom::Rotate::from_degrees(_edit_rotation - sp_marker->orient.computed);
+
+ adjusted_p = adjusted_p
+ * Geom::Translate(getMarkerBounds(item, desktop).min())
+ * Geom::Rotate::from_degrees(_edit_rotation - sp_marker->orient.computed);
+
+ } else if ((sp_marker->orient_mode == MARKER_ORIENT_AUTO_START_REVERSE) && (_edit_marker_mode == SP_MARKER_LOC_START)) {
+
+ adjusted_origin = adjusted_origin
+ * Geom::Translate(getMarkerBounds(item, desktop).min())
+ * Geom::Rotate::from_degrees(180.0);
+
+ adjusted_p = adjusted_p
+ * Geom::Translate(getMarkerBounds(item, desktop).min())
+ * Geom::Rotate::from_degrees(180.0);
+ }
+
+ // x_Sign and y_Sign are (+/- 1) to set the appropriate sign for derived classes
+ double orig_width = _x_Sign*((original_width * original_scaleX)/2);
+ double orig_height = _y_Sign*((original_height * original_scaleY)/2);
+
+ // x & y displacement between origin and new mouse displacement
+ double dx = adjusted_p[Geom::X] - adjusted_origin[Geom::X];
+ double dy = adjusted_p[Geom::Y] - adjusted_origin[Geom::Y];
+ double adjusted_scaleX = 0.0;
+ double adjusted_scaleY = 0.0;
+
+ adjusted_scaleX = (dx/orig_width) + 1;
+ adjusted_scaleY = (dy/orig_height) + 1;
+
+ // uniform scaling when ctrl+key is pressed
+ if(state & GDK_CONTROL_MASK) {
+ adjusted_scaleX = fabs(adjusted_scaleX);
+ adjusted_scaleY = fabs(adjusted_scaleY);
+
+ // possible areas based on which x/y coord is used to calculate uniform scale
+ double dx_area = (sp_marker->viewBox.width()*adjusted_scaleX) * (sp_marker->viewBox.height()*adjusted_scaleX); // A = W*H
+ double dy_area = (sp_marker->viewBox.width()*adjusted_scaleY) * (sp_marker->viewBox.height()*adjusted_scaleY);
+
+ if (dy_area > dx_area) {
+ adjusted_scaleX = adjusted_scaleY;
+ } else if (dx_area > dy_area) {
+ adjusted_scaleY = adjusted_scaleX;
+ }
+
+ adjusted_scaleX = adjusted_scaleX * original_scaleX;
+ adjusted_scaleY = adjusted_scaleY * original_scaleY;
+
+ sp_marker->markerWidth = sp_marker->viewBox.width() * adjusted_scaleX;
+ sp_marker->markerHeight = sp_marker->viewBox.height() * adjusted_scaleY;
+
+ sp_marker->refX = ((original_refX * original_scaleX)/adjusted_scaleX) - ((getMarkerBounds(item, desktop).min()[Geom::X] + sp_marker->viewBox.width()/2) * (original_scaleX/adjusted_scaleX - 1));
+ sp_marker->refY = ((original_refY * original_scaleY)/adjusted_scaleY) - ((getMarkerBounds(item, desktop).min()[Geom::Y] + sp_marker->viewBox.height()/2) * (original_scaleY/adjusted_scaleY - 1));
+ } else {
+
+ adjusted_scaleX = adjusted_scaleX * original_scaleX;
+ adjusted_scaleY = adjusted_scaleY * original_scaleY;
+
+ // make sure the preserveAspectRatio is none when the user wants to use non-uniform scaling
+ if (sp_marker->aspect_align != SP_ASPECT_NONE) {
+ sp_marker->setAttribute("preserveAspectRatio", "none");
+ }
+
+ if(adjusted_scaleX > 0.0 && adjusted_scaleY > 0.0) {
+ sp_marker->markerWidth = sp_marker->viewBox.width() * adjusted_scaleX;
+ sp_marker->markerHeight = sp_marker->viewBox.height() * adjusted_scaleY;
+
+ sp_marker->refX = ((original_refX * original_scaleX)/adjusted_scaleX) - ((getMarkerBounds(item, desktop).min()[Geom::X] + sp_marker->viewBox.width()/2) * (original_scaleX/adjusted_scaleX - 1));
+ sp_marker->refY = ((original_refY * original_scaleY)/adjusted_scaleY) - ((getMarkerBounds(item, desktop).min()[Geom::Y] + sp_marker->viewBox.height()/2) * (original_scaleY/adjusted_scaleY - 1));
+ }
+ }
+
+ sp_marker->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG);
+}
+
+class MarkerKnotHolderEntityScale2 : public MarkerKnotHolderEntityScale {
+public:
+ MarkerKnotHolderEntityScale2(double edit_rotation, int edit_marker_mode, int x_Sign, int y_Sign)
+ : MarkerKnotHolderEntityScale(edit_rotation, edit_marker_mode, x_Sign, y_Sign)
+ {
+ }
+
+ Geom::Point knot_get() const override;
+};
+
+Geom::Point
+MarkerKnotHolderEntityScale2::knot_get() const
+{
+ auto sp_marker = cast<SPMarker>(item);
+ g_assert(sp_marker != nullptr);
+
+ // this corresponds to the reference point
+ return Geom::Point((-sp_marker->refX.computed + getMarkerBounds(item, desktop).min()[Geom::X]) * getMarkerXScale(item),
+ (-sp_marker->refY.computed + getMarkerBounds(item, desktop).min()[Geom::Y]) * getMarkerYScale(item))
+ * getMarkerRotation(item, _edit_rotation, _edit_marker_mode);
+}
+
+
+class MarkerKnotHolderEntityScale3 : public MarkerKnotHolderEntityScale {
+public:
+ MarkerKnotHolderEntityScale3(double edit_rotation, int edit_marker_mode, int x_Sign, int y_Sign)
+ : MarkerKnotHolderEntityScale(edit_rotation, edit_marker_mode, x_Sign, y_Sign)
+ {
+ }
+
+ Geom::Point knot_get() const override;
+};
+
+Geom::Point
+MarkerKnotHolderEntityScale3::knot_get() const
+{
+ auto sp_marker = cast<SPMarker>(item);
+ g_assert(sp_marker != nullptr);
+
+ return Geom::Point(
+ (-sp_marker->refX.computed + getMarkerBounds(item, desktop).min()[Geom::X]) * getMarkerXScale(item),
+ (-sp_marker->refY.computed + sp_marker->viewBox.height() + getMarkerBounds(item, desktop).min()[Geom::Y]) * getMarkerYScale(item))
+ * getMarkerRotation(item, _edit_rotation, _edit_marker_mode);
+}
+
+MarkerKnotHolder::MarkerKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler, double edit_rotation, int edit_marker_mode)
+ : KnotHolder(desktop, item, relhandler)
+{
+ MarkerKnotHolderEntityReference *entity_reference = new MarkerKnotHolderEntityReference(edit_rotation, edit_marker_mode);
+ MarkerKnotHolderEntityOrient *entity_orient = new MarkerKnotHolderEntityOrient(edit_rotation, edit_marker_mode);
+
+ MarkerKnotHolderEntityScale *entity_scale = new MarkerKnotHolderEntityScale(edit_rotation, edit_marker_mode, 1, 1);
+ // these two additional knots have the same scaling functionality but also serve as a fill in for the empty corners of the marker bounding box
+ MarkerKnotHolderEntityScale2 *entity_scale2 = new MarkerKnotHolderEntityScale2(edit_rotation, edit_marker_mode, -1, -1);
+ MarkerKnotHolderEntityScale3 *entity_scale3 = new MarkerKnotHolderEntityScale3(edit_rotation, edit_marker_mode, -1, 1);
+
+ entity_reference->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Marker:reference",
+ _("Drag to adjust the refX/refY position of the marker"));
+
+ entity_orient->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_ROTATE, "Marker:orient",
+ _("Adjust marker orientation through rotation"));
+
+ entity_scale->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Marker:scale",
+ _("Adjust the <b>size</b> of the marker"));
+
+ entity_scale2->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Marker:scale",
+ _("Adjust the <b>size</b> of the marker"));
+
+ entity_scale3->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Marker:scale",
+ _("Adjust the <b>size</b> of the marker"));
+
+ entity.push_back(entity_reference);
+ entity.push_back(entity_orient);
+ entity.push_back(entity_scale);
+ entity.push_back(entity_scale2);
+ entity.push_back(entity_scale3);
+
+ 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);
+
+ auto arc = 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 = cast<SPGenericEllipse>(item);
+ g_assert(ge != nullptr);
+
+ return ge->getPointAtAngle(ge->start);
+}
+
+void
+ArcKnotHolderEntityStart::knot_click(unsigned int state)
+{
+ auto ge = 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);
+
+ auto arc = 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 = cast<SPGenericEllipse>(item);
+ g_assert(ge != nullptr);
+
+ return ge->getPointAtAngle(ge->end);
+}
+
+
+void
+ArcKnotHolderEntityEnd::knot_click(unsigned int state)
+{
+ auto ge = 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)
+{
+ auto ge = 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 = cast<SPGenericEllipse>(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)
+{
+ auto ge = 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)
+{
+ auto ge = 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 = 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)
+{
+ auto ge = 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)
+{
+ auto ge = 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 = 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::CANVAS_ITEM_CTRL_TYPE_SIZER, "Arc:rx",
+ _("Adjust ellipse <b>width</b>, with <b>Ctrl</b> to make circle"));
+
+ entity_ry->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Arc:ry",
+ _("Adjust ellipse <b>height</b>, with <b>Ctrl</b> to make circle"));
+
+ entity_start->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_ROTATE, "Arc:start",
+ _("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"));
+
+ entity_end->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_ROTATE, "Arc:end",
+ _("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"));
+
+ entity_center->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_POINT, "Arc:center",
+ _("Drag to move the ellipse"));
+
+ 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)
+{
+ auto star = 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)
+{
+ auto star = 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)
+{
+ auto star = 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 = cast<SPStar>(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 = cast<SPStar>(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 = cast<SPStar>(item);
+ g_assert(star != nullptr);
+
+ return star->center;
+}
+
+static void
+sp_star_knot_click(SPItem *item, unsigned int state)
+{
+ auto star = 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)
+{
+ auto star = cast<SPStar>(item);
+ g_assert(item != nullptr);
+
+ StarKnotHolderEntity1 *entity1 = new StarKnotHolderEntity1();
+ entity1->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Star:entity1",
+ _("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::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Star:entity2",
+ _("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::CANVAS_ITEM_CTRL_TYPE_POINT, "Star:center",
+ _("Drag to move the star"));
+ 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);
+
+ auto spiral = 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);
+
+ auto spiral = 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)
+{
+ auto spiral = 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 = cast<SPSpiral>(item);
+ g_assert(spiral != nullptr);
+
+ return spiral->getXY(spiral->t0);
+}
+
+Geom::Point
+SpiralKnotHolderEntityOuter::knot_get() const
+{
+ SPSpiral const *spiral = cast<SPSpiral>(item);
+ g_assert(spiral != nullptr);
+
+ return spiral->getXY(1.0);
+}
+
+Geom::Point
+SpiralKnotHolderEntityCenter::knot_get() const
+{
+ SPSpiral const *spiral = cast<SPSpiral>(item);
+ g_assert(spiral != nullptr);
+
+ return Geom::Point(spiral->cx, spiral->cy);
+}
+
+void
+SpiralKnotHolderEntityInner::knot_click(unsigned int state)
+{
+ auto spiral = 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::CANVAS_ITEM_CTRL_TYPE_POINT, "Spiral:center",
+ _("Drag to move the spiral"));
+
+ entity_inner->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Spiral:inner",
+ _("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::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Spiral:outer",
+ _("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)
+{
+ auto offset = 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 = cast<SPOffset>(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::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Offset:entity",
+ _("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
+{
+ auto text = 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());
+ }
+ if (mode == SP_CSS_WRITING_MODE_TB_LR) {
+ 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)
+{
+ auto text = 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)
+{
+ auto text = 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();
+ }
+}
+
+/**
+ * Shape padding editor knot positioned top right corner of first object
+ */
+class TextKnotHolderEntityShapePadding : 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
+TextKnotHolderEntityShapePadding::knot_get() const
+{
+ auto text = cast<SPText>(item);
+ g_assert(text != nullptr);
+ Geom::Point corner {Geom::infinity(), Geom::infinity()};
+
+ if (!text->has_shape_inside()) {
+ return corner;
+ }
+
+ auto shape = text->get_first_shape_dependency();
+ if (!shape) {
+ return corner;
+ }
+
+ Geom::OptRect bounds = shape->geometricBounds();
+ if (bounds) {
+ corner = (*bounds).corner(1);
+ if (text->style->shape_padding.set) {
+ auto padding = text->style->shape_padding.computed;
+ corner *= Geom::Affine(Geom::Translate(-padding, padding));
+ }
+ corner *= shape->transform;
+ }
+ return corner;
+}
+
+void
+TextKnotHolderEntityShapePadding::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ // Text in a shape: rectangle
+ auto text = cast<SPText>(item);
+ g_assert(text != nullptr);
+ if (!text->has_shape_inside()) {
+ return;
+ }
+
+ if (auto shape = text->get_first_shape_dependency()) {
+ if (Geom::OptRect optbounds = shape->geometricBounds()) {
+ auto bounds = *optbounds;
+ Geom::Point const point_a = snap_knot_position(p, state);
+ Geom::Point point_b = point_a * shape->transform.inverse();
+
+ double padding = 0.0;
+ if (point_b[Geom::X] - 1 > bounds.midpoint()[Geom::X]) {
+ padding = bounds.corner(1)[Geom::X] - point_b[Geom::X];
+ }
+
+ // Padding can only be a positive value according to the CSS/text-padding spec
+ if (padding >= 0.0) {
+ Inkscape::CSSOStringStream os;
+ os << padding;
+ text->style->shape_padding.read(os.str().c_str());
+
+ text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ text->updateRepr();
+ }
+ }
+ }
+}
+
+
+/**
+ * Shape margin editor knot positioned top right corner of each object
+ */
+class TextKnotHolderEntityShapeMargin : 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 set_shape(SPShape *shape) { linked_shape = shape; }
+ SPShape *linked_shape;
+};
+
+Geom::Point
+TextKnotHolderEntityShapeMargin::knot_get() const
+{
+ Geom::Point corner;
+ if (linked_shape == nullptr) return corner;
+
+ Geom::OptRect bounds = linked_shape->geometricBounds();
+ if (bounds) {
+ corner = (*bounds).corner(1);
+ if (linked_shape->style->shape_margin.set) {
+ auto margin = linked_shape->style->shape_margin.computed;
+ corner *= Geom::Affine(Geom::Translate(margin, -margin));
+ }
+ corner *= linked_shape->transform;
+ }
+ return corner;
+}
+
+void
+TextKnotHolderEntityShapeMargin::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ g_assert(linked_shape != nullptr);
+
+ Geom::OptRect bounds = linked_shape->geometricBounds();
+ if (bounds) {
+ Geom::Point const point_a = snap_knot_position(p, state);
+ Geom::Point point_b = point_a * linked_shape->transform.inverse();
+ auto margin = -((*bounds).corner(1)[Geom::X] - point_b[Geom::X]);
+
+ // Margins can only be `non-negative` according to the CSS/shape-margin spec
+ if (margin >= 0.0) {
+ Inkscape::CSSOStringStream os;
+ os << margin;
+ linked_shape->style->shape_margin.read(os.str().c_str());
+
+ linked_shape->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ linked_shape->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.
+ auto text = cast<SPText>(item);
+ g_assert(text != nullptr);
+
+ Geom::Point p {Geom::infinity(), Geom::infinity()};
+ if (text->has_shape_inside()) {
+ 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
+ auto text = cast<SPText>(item);
+ g_assert(text != nullptr);
+
+ Geom::Point const s = snap_knot_position(p, state);
+
+ Inkscape::XML::Node* rectangle = text->get_first_rectangle();
+ if (!rectangle) {
+ return;
+ }
+ double x = rectangle->getAttributeDouble("x", 0.0);;
+ double y = rectangle->getAttributeDouble("y", 0.0);
+ double width = s[Geom::X] - x;
+ double height = s[Geom::Y] - y;
+ rectangle->setAttributeSvgDouble("width", width);
+ rectangle->setAttributeSvgDouble("height", height);
+ text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ text->updateRepr();
+}
+
+TextKnotHolder::TextKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) :
+ KnotHolder(desktop, item, relhandler)
+{
+ auto text = cast<SPText>(item);
+ g_assert(text != nullptr);
+
+ if (text->has_shape_inside()) {
+ // 'shape-inside'
+
+ if (text->get_first_rectangle()) {
+ auto entity_shapeinside = new TextKnotHolderEntityShapeInside();
+ entity_shapeinside->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Text:shapeinside",
+ _("Adjust the <b>rectangular</b> region of the text."));
+ entity.push_back(entity_shapeinside);
+ }
+
+ if (text->get_first_shape_dependency()) {
+ auto entity_shapepadding = new TextKnotHolderEntityShapePadding();
+ entity_shapepadding->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Text:shapepadding",
+ _("Adjust the text <b>shape padding</b>."));
+ entity.push_back(entity_shapepadding);
+ }
+
+
+ // Add knots for shape subtraction margins
+ if (text->style->shape_subtract.set) {
+ for (auto *href : text->style->shape_subtract.hrefs) {
+ if (auto shape = href->getObject()) {
+ auto entity_shapemargin = new TextKnotHolderEntityShapeMargin();
+ entity_shapemargin->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Text:shapemargin",
+ _("Adjust the shape's <b>text margin</b>."));
+ entity_shapemargin->set_shape(shape);
+ entity_shapemargin->update_knot();
+ entity.push_back(entity_shapemargin);
+ }
+ }
+ }
+
+ } else {
+ // 'inline-size' or normal text
+ TextKnotHolderEntityInlineSize *entity_inlinesize = new TextKnotHolderEntityInlineSize();
+
+ entity_inlinesize->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Text:inlinesize",
+ _("Adjust the <b>inline size</b> (line length) of the text."));
+ entity.push_back(entity_inlinesize);
+ }
+
+ add_pattern_knotholder();
+ add_hatch_knotholder();
+}
+
+
+// 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 = cast<SPRect>(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::CANVAS_ITEM_CTRL_TYPE_SHAPER, "FlowText:entity",
+ _("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..fe8e2a7
--- /dev/null
+++ b/src/ui/shape-editor.cpp
@@ -0,0 +1,210 @@
+// 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 "shape-editor.h"
+
+#include "desktop.h"
+#include "document.h"
+#include "live_effects/effect.h"
+#include "object/sp-lpe-item.h"
+#include "ui/knot/knot-holder.h"
+
+namespace Inkscape {
+namespace UI {
+
+KnotHolder *createKnotHolder(SPItem *item, SPDesktop *desktop, double edit_rotation, int edit_marker_mode);
+KnotHolder *createLPEKnotHolder(SPItem *item, SPDesktop *desktop);
+
+bool ShapeEditor::_blockSetItem = false;
+
+ShapeEditor::ShapeEditor(SPDesktop *dt, Geom::Affine edit_transform, double edit_rotation, int edit_marker_mode)
+ : desktop(dt)
+ , _edit_transform(edit_transform)
+ , _edit_rotation(edit_rotation)
+ , _edit_marker_mode(edit_marker_mode)
+{
+}
+
+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) {
+ old_repr->removeObserver(*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;
+ bool remove = false;
+ if (old_repr && old_repr == lpeknotholder_listener_attached_for) {
+ old_repr->removeObserver(*this);
+ Inkscape::GC::release(old_repr);
+ remove = true;
+ }
+
+ if (!keep_knotholder) {
+ delete this->lpeknotholder;
+ this->lpeknotholder = nullptr;
+ }
+ if (remove) {
+ lpeknotholder_listener_attached_for = 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::notifyAttributeChanged(Inkscape::XML::Node&, GQuark,
+ Inkscape::Util::ptr_shared,
+ Inkscape::Util::ptr_shared)
+{
+ bool changed_kh = false;
+
+ if (has_knotholder()) {
+ changed_kh = !has_local_change();
+ decrement_local_change();
+ if (changed_kh) {
+ reset_item();
+ }
+ }
+}
+
+
+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, _edit_rotation, _edit_marker_mode);
+ }
+ auto lpe = 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) {
+ knotholder->install_modification_watch(); // let knotholder know item's attribute may have changed
+ 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);
+ repr->addObserver(*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);
+ repr->addObserver(*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(cast<SPItem>(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(cast<SPItem>(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..fdaefe9
--- /dev/null
+++ b/src/ui/shape-editor.h
@@ -0,0 +1,78 @@
+// 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
+
+#include <2geom/affine.h>
+
+#include "xml/node-observer.h"
+
+class KnotHolder;
+class LivePathEffectObject;
+class SPDesktop;
+class SPItem;
+
+namespace Inkscape { namespace XML { class Node; }
+namespace UI {
+
+class ShapeEditor : private XML::NodeObserver
+{
+public:
+
+ ShapeEditor(SPDesktop *desktop, Geom::Affine edit_transform = Geom::identity(), double edit_rotation = 0.0, int edit_marker_mode = -1);
+ ~ShapeEditor() override;
+
+ 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{nullptr};
+ KnotHolder *lpeknotholder{nullptr};
+ bool has_knotholder();
+ static void blockSetItem(bool b) { _blockSetItem = b; } // kludge
+private:
+ void reset_item();
+ static bool _blockSetItem;
+
+ SPDesktop *desktop;
+ Inkscape::XML::Node *knotholder_listener_attached_for{nullptr};
+ Inkscape::XML::Node *lpeknotholder_listener_attached_for{nullptr};
+ Geom::Affine _edit_transform;
+ double _edit_rotation;
+ int _edit_marker_mode;
+
+ void notifyAttributeChanged(Inkscape::XML::Node &node, GQuark key, Inkscape::Util::ptr_shared oldvalue, Inkscape::Util::ptr_shared newval) final;
+};
+
+} // 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/shortcuts.cpp b/src/ui/shortcuts.cpp
new file mode 100644
index 0000000..f7d4c60
--- /dev/null
+++ b/src/ui/shortcuts.cpp
@@ -0,0 +1,1024 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Shortcuts
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ * Rewrite of code (C) MenTalguY and others.
+ *
+ * The contents of this file may be used under the GNU General Public License Version 2 or later.
+ *
+ */
+
+
+/* Much of the complexity of this code is in dealing with both Inkscape verbs and Gio::Actions at
+ * the same time. When we remove verbs we can avoid using 'unsigned long long int shortcut' to
+ * track keys and rely directly on Glib::ustring as used by
+ * Gtk::Application::get_accels_for_action(). This will then automatically handle the '<Primary>'
+ * modifier value (which takes care of the differences between Linux and OSX) as well as allowing
+ * us to set multiple accelerators for actions in InkscapePreferences. */
+
+#include "shortcuts.h"
+
+#include <iostream>
+#include <iomanip>
+
+#include <glibmm.h>
+#include <glibmm/i18n.h>
+#include <gtkmm.h>
+
+#include "preferences.h"
+#include "inkscape-application.h"
+#include "inkscape-window.h"
+
+#include "io/dir-util.h"
+#include "io/resource.h"
+#include "io/sys.h"
+
+#include "ui/modifiers.h"
+#include "ui/tools/tool-base.h" // For latin keyval
+#include "ui/dialog/filedialog.h" // Importing/exporting files.
+
+#include "xml/simple-document.h"
+#include "xml/node.h"
+#include "xml/node-iterators.h"
+
+using namespace Inkscape::IO::Resource;
+using namespace Inkscape::Modifiers;
+
+namespace Inkscape {
+
+Shortcuts::Shortcuts()
+{
+ Glib::RefPtr<Gio::Application> gapp = Gio::Application::get_default();
+ app = Glib::RefPtr<Gtk::Application>::cast_dynamic(gapp); // Save as we constantly use it.
+ if (!app) {
+ std::cerr << "Shortcuts::Shortcuts: No app! Shortcuts cannot be used without a Gtk::Application!" << std::endl;
+ }
+}
+
+
+void
+Shortcuts::init() {
+
+ initialized = true;
+
+ // Clear arrays (we may be re-reading).
+ clear();
+
+ bool success = false; // We've read a shortcut file!
+ std::string path;
+
+ // ------------ Open Inkscape shortcut file ------------
+
+ // Try filename from preferences first.
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ path = prefs->getString("/options/kbshortcuts/shortcutfile");
+ if (!path.empty()) {
+ bool absolute = true;
+ if (!Glib::path_is_absolute(path)) {
+ path = get_path_string(SYSTEM, KEYS, path.c_str());
+ absolute = false;
+ }
+
+ Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(path);
+ success = read(file);
+ if (!success) {
+ std::cerr << "Shortcut::Shortcut: Unable to read shortcut file listed in preferences: " + path << std::endl;;
+ }
+
+ // Save relative path to "share/keys" if possible to handle parallel installations of
+ // Inskcape gracefully.
+ if (success && absolute) {
+ std::string relative_path = sp_relative_path_from_path(path, std::string(get_path(SYSTEM, KEYS)));
+ prefs->setString("/options/kbshortcuts/shortcutfile", relative_path.c_str());
+ }
+ }
+
+ if (!success) {
+ // std::cerr << "Shortcut::Shortcut: " << reason << ", trying default.xml" << std::endl;
+
+ Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(get_path_string(SYSTEM, KEYS, "default.xml"));
+ success = read(file);
+ }
+
+ if (!success) {
+ std::cerr << "Shortcut::Shortcut: Failed to read file default.xml, trying inkscape.xml" << std::endl;
+
+ Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(get_path_string(SYSTEM, KEYS, "inkscape.xml"));
+ success = read(file);
+ }
+
+ if (!success) {
+ std::cerr << "Shortcut::Shortcut: Failed to read file inkscape.xml; giving up!" << std::endl;
+ }
+
+ // ------------ Open Shared shortcut file -------------
+ Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(get_path_string(SHARED, KEYS, "default.xml"));
+ // Test if file exists before attempting to read to avoid generating warning message.
+ if (file->query_exists()) {
+ read(file, true);
+ }
+ // ------------ Open User shortcut file -------------
+ file = Gio::File::create_for_path(get_path_string(USER, KEYS, "default.xml"));
+ // Test if file exists before attempting to read to avoid generating warning message.
+ if (file->query_exists()) {
+ read(file, true);
+ }
+
+ // dump();
+}
+
+// Clear all shortcuts
+void
+Shortcuts::clear()
+{
+ // Actions: We rely on Gtk for everything except user/system setting.
+ for (auto action_description : app->list_action_descriptions()) {
+ app->unset_accels_for_action(action_description);
+ }
+ action_user_set.clear();
+}
+
+/** Trigger action from a shortcut. Useful if we want to intercept the event from GTK */
+bool
+Shortcuts::invoke_action(GdkEventKey const *event)
+{
+ Gtk::AccelKey shortcut = get_from_event(event);
+
+ bool return_value = false;
+
+ // This can be simplified in GTK4.
+ Glib::ustring accel = Gtk::AccelGroup::name(shortcut.get_key(), shortcut.get_mod());
+ std::vector<Glib::ustring> actions = app->get_actions_for_accel(accel);
+ if (!actions.empty()) {
+ Glib::ustring action = actions[0];
+ Glib::ustring action_name;
+ Glib::VariantBase value;
+ Gio::SimpleAction::parse_detailed_name_variant(action.substr(4), action_name, value);
+ if (action.compare(0, 4, "app.") == 0) {
+ app->activate_action(action_name, value);
+ return_value = true;
+ } else if (action.compare(0, 4, "win.") == 0) {
+ auto window = dynamic_cast<InkscapeWindow *>(app->get_active_window());
+ if (window) {
+ window->activate_action(action_name, value);
+ return_value = true;
+ }
+ }
+ }
+ return return_value;
+}
+
+Gdk::ModifierType
+parse_modifier_string(gchar const *modifiers_string)
+{
+ Gdk::ModifierType modifiers(Gdk::ModifierType(0));
+ if (modifiers_string) {
+
+ Glib::ustring str(modifiers_string);
+ std::vector<Glib::ustring> mod_vector = Glib::Regex::split_simple("\\s*,\\s*", modifiers_string);
+
+ for (auto mod : mod_vector) {
+ if (mod == "Control" || mod == "Ctrl") {
+ modifiers |= Gdk::CONTROL_MASK;
+ } else if (mod == "Shift") {
+ modifiers |= Gdk::SHIFT_MASK;
+ } else if (mod == "Alt") {
+ modifiers |= Gdk::MOD1_MASK;
+ } else if (mod == "Super") {
+ modifiers |= Gdk::SUPER_MASK; // Not used
+ } else if (mod == "Hyper") {
+ modifiers |= Gdk::HYPER_MASK; // Not used
+ } else if (mod == "Meta") {
+ modifiers |= Gdk::META_MASK;
+ } else if (mod == "Primary") {
+
+ // System dependent key to invoke menus. (Needed for OSX in particular.)
+ // We only read "Primary" and never write it.
+ auto display = Gdk::Display::get_default();
+ if (display) {
+ GdkKeymap* keymap = display->get_keymap();
+ GdkModifierType type =
+ gdk_keymap_get_modifier_mask (keymap, GDK_MODIFIER_INTENT_PRIMARY_ACCELERATOR);
+ gdk_keymap_add_virtual_modifiers(keymap, &type);
+ if (type & Gdk::CONTROL_MASK)
+ modifiers |= Gdk::CONTROL_MASK;
+ else if (type & Gdk::META_MASK)
+ modifiers |= Gdk::META_MASK;
+ else {
+ std::cerr << "Shortcut::read: Unknown primary accelerator!" << std::endl;
+ modifiers |= Gdk::CONTROL_MASK;
+ }
+ } else {
+ modifiers |= Gdk::CONTROL_MASK;
+ }
+ } else {
+ std::cerr << "Shortcut::read: Unknown GDK modifier: " << mod.c_str() << std::endl;
+ }
+ }
+ }
+ return modifiers;
+}
+
+
+// Read a shortcut file.
+bool
+Shortcuts::read(Glib::RefPtr<Gio::File> file, bool user_set)
+{
+ if (!file->query_exists()) {
+ std::cerr << "Shortcut::read: file does not exist: " << file->get_path() << std::endl;
+ return false;
+ }
+
+ XML::Document *document = sp_repr_read_file(file->get_path().c_str(), nullptr, true);
+ if (!document) {
+ std::cerr << "Shortcut::read: could not parse file: " << file->get_path() << std::endl;
+ return false;
+ }
+
+ XML::NodeConstSiblingIterator iter = document->firstChild();
+ for ( ; iter ; ++iter ) { // We iterate in case of comments.
+ if (strcmp(iter->name(), "keys") == 0) {
+ break;
+ }
+ }
+
+ if (!iter) {
+ std::cerr << "Shortcuts::read: File in wrong format: " << file->get_path() << std::endl;
+ return false;
+ }
+
+ // Loop through the children in <keys> (may have nested keys)
+ _read(*iter, user_set);
+
+ return true;
+}
+
+/**
+ * Recursively reads shortcuts from shortcut file.
+ *
+ * @param keysnode The <keys> element. Its child nodes will be processed.
+ * @param user_set true if reading from user shortcut file
+ */
+void
+Shortcuts::_read(XML::Node const &keysnode, bool user_set)
+{
+ XML::NodeConstSiblingIterator iter {keysnode.firstChild()};
+ for ( ; iter ; ++iter ) {
+
+ if (strcmp(iter->name(), "modifier") == 0) {
+
+ gchar const *mod_name = iter->attribute("action");
+ if (!mod_name) {
+ std::cerr << "Shortcuts::read: Missing modifier for action!" << std::endl;;
+ continue;
+ }
+
+ Modifier *mod = Modifier::get(mod_name);
+ if (mod == nullptr) {
+ std::cerr << "Shortcuts::read: Can't find modifier: " << mod_name << std::endl;
+ continue;
+ }
+
+ // If mods isn't specified then it should use default, if it's an empty string
+ // then the modifier is None (i.e. happens all the time without a modifier)
+ KeyMask and_modifier = NOT_SET;
+ gchar const *mod_attr = iter->attribute("modifiers");
+ if (mod_attr) {
+ and_modifier = (KeyMask) parse_modifier_string(mod_attr);
+ }
+
+ // Parse not (cold key) modifier
+ KeyMask not_modifier = NOT_SET;
+ gchar const *not_attr = iter->attribute("not_modifiers");
+ if (not_attr) {
+ not_modifier = (KeyMask) parse_modifier_string(not_attr);
+ }
+
+ gchar const *disabled_attr = iter->attribute("disabled");
+ if (disabled_attr && strcmp(disabled_attr, "true") == 0) {
+ and_modifier = NEVER;
+ }
+
+ if (and_modifier != NOT_SET) {
+ if(user_set) {
+ mod->set_user(and_modifier, not_modifier);
+ } else {
+ mod->set_keys(and_modifier, not_modifier);
+ }
+ }
+ continue;
+ } else if (strcmp(iter->name(), "keys") == 0) {
+ _read(*iter, user_set);
+ continue;
+ } else if (strcmp(iter->name(), "bind") != 0) {
+ // Unknown element, do not complain.
+ continue;
+ }
+
+ // Gio::Action's
+ gchar const *gaction = iter->attribute("gaction");
+ gchar const *keys = iter->attribute("keys");
+ if (gaction && keys) {
+
+ // Trim leading spaces
+ Glib::ustring Keys = keys;
+ auto p = Keys.find_first_not_of(" ");
+ Keys = Keys.erase(0, p);
+
+ std::vector<Glib::ustring> key_vector = Glib::Regex::split_simple("\\s*,\\s*", Keys);
+ // Set one shortcut at a time so we can check if it has been previously used.
+ for (auto key : key_vector) {
+ add_shortcut(gaction, key, user_set);
+ }
+
+ // Uncomment to see what the cat dragged in.
+ // if (!key_vector.empty()) {
+ // std::cout << "Shortcut::read: gaction: "<< gaction
+ // << ", user set: " << std::boolalpha << user_set << ", ";
+ // for (auto key : key_vector) {
+ // std::cout << key << ", ";
+ // }
+ // std::cout << std::endl;
+ // }
+
+ continue;
+ }
+ }
+}
+
+bool
+Shortcuts::write_user() {
+ Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(get_path_string(USER, KEYS, "default.xml"));
+ return write(file, User);
+}
+
+// In principle, we only write User shortcuts. But for debugging, we might want to write something else.
+bool
+Shortcuts::write(Glib::RefPtr<Gio::File> file, What what) {
+
+ auto *document = new XML::SimpleDocument();
+ XML::Node * node = document->createElement("keys");
+ switch (what) {
+ case User:
+ node->setAttribute("name", "User Shortcuts");
+ break;
+ case System:
+ node->setAttribute("name", "System Shortcuts");
+ break;
+ default:
+ node->setAttribute("name", "Inkscape Shortcuts");
+ }
+
+ document->appendChild(node);
+
+ // Actions: write out all actions with accelerators.
+ for (auto action_name : list_all_detailed_action_names()) {
+ if ( what == All ||
+ (what == System && !action_user_set[action_name]) ||
+ (what == User && action_user_set[action_name]) )
+ {
+ std::vector<Glib::ustring> accels = app->get_accels_for_action(action_name);
+ if (!accels.empty()) {
+
+ XML::Node * node = document->createElement("bind");
+
+ node->setAttribute("gaction", action_name);
+
+ Glib::ustring keys;
+ for (auto accel : accels) {
+ keys += accel;
+ keys += ",";
+ }
+ keys.resize(keys.size() - 1);
+ node->setAttribute("keys", keys);
+
+ document->root()->appendChild(node);
+ }
+ }
+ }
+
+ for(auto modifier: Inkscape::Modifiers::Modifier::getList()) {
+ if (what == User && modifier->is_set_user()) {
+ XML::Node * node = document->createElement("modifier");
+ node->setAttribute("action", modifier->get_id());
+
+ if (modifier->get_config_user_disabled()) {
+ node->setAttribute("disabled", "true");
+ } else {
+ node->setAttribute("modifiers", modifier->get_config_user_and());
+ auto not_mask = modifier->get_config_user_not();
+ if (!not_mask.empty() and not_mask != "-") {
+ node->setAttribute("not_modifiers", not_mask);
+ }
+ }
+
+ document->root()->appendChild(node);
+ }
+ }
+
+ sp_repr_save_file(document, file->get_path().c_str(), nullptr);
+ GC::release(document);
+
+ return true;
+};
+
+// Return if user set shortcut for Gio::Action.
+bool
+Shortcuts::is_user_set(Glib::ustring& action)
+{
+ auto it = action_user_set.find(action);
+ if (it != action_user_set.end()) {
+ return action_user_set[action];
+ } else {
+ return false;
+ }
+}
+
+// Get a list of detailed action names (as defined in action extra data).
+// This is more useful for shortcuts than a list of all actions.
+std::vector<Glib::ustring>
+Shortcuts::list_all_detailed_action_names()
+{
+ auto *iapp = InkscapeApplication::instance();
+ InkActionExtraData& action_data = iapp->get_action_extra_data();
+ return action_data.get_actions();
+}
+
+// Get a list of all actions (application, window, and document), properly prefixed.
+// We need to do this ourselves as Gtk::Application does not have a function for this.
+std::vector<Glib::ustring>
+Shortcuts::list_all_actions()
+{
+ std::vector<Glib::ustring> all_actions;
+
+ std::vector<Glib::ustring> actions = app->list_actions();
+ std::sort(actions.begin(), actions.end());
+ for (auto const &action : actions) {
+ all_actions.emplace_back("app." + action);
+ }
+
+ auto gwindow = app->get_active_window();
+ auto window = dynamic_cast<InkscapeWindow *>(gwindow);
+ if (window) {
+ std::vector<Glib::ustring> actions = window->list_actions();
+ std::sort(actions.begin(), actions.end());
+ for (auto const &action : actions) {
+ all_actions.emplace_back("win." + action);
+ }
+
+ auto document = window->get_document();
+ if (document) {
+ auto map = document->getActionGroup();
+ if (map) {
+ std::vector<Glib::ustring> actions = map->list_actions();
+ for (auto const &action : actions) {
+ all_actions.emplace_back("doc." + action);
+ }
+ } else {
+ std::cerr << "Shortcuts::list_all_actions: No document map!" << std::endl;
+ }
+ }
+ }
+
+ return all_actions;
+}
+
+
+// Add a shortcut, removing any previous use of shortcut.
+bool
+Shortcuts::add_shortcut(Glib::ustring name, const Gtk::AccelKey& shortcut, bool user)
+{
+ // Remove previous use of shortcut (already removed if new user shortcut).
+ if (Glib::ustring old_name = remove_shortcut(shortcut); old_name != "") {
+ // std::cerr << "Shortcut::add_shortcut: duplicate shortcut found for: " << shortcut.get_abbrev().raw()
+ // << " Old: " << old_name.raw() << " New: " << name.raw() << " !" << std::endl;
+ }
+
+ // Add shortcut
+
+ // To see if action exists, We need to compare action names without values...
+ Glib::ustring action_name_new;
+ Glib::VariantBase value_new;
+ Gio::SimpleAction::parse_detailed_name_variant(name, action_name_new, value_new);
+
+ for (auto const &action : list_all_detailed_action_names()) {
+ Glib::ustring action_name_old;
+ Glib::VariantBase value_old;
+ Gio::SimpleAction::parse_detailed_name_variant(action, action_name_old, value_old);
+
+ if (action_name_new == action_name_old) {
+ std::vector<Glib::ustring> accels;
+ // Action exists, add shortcut to list of shortcuts, if it's not a user shortcut.
+ // If it is a user-defined shortcut, then it replaces any defaults that might have been present.
+ // That's what we show in the UI when we define shortcuts (only new one) and that's also
+ // the only way to let user "overwrite" default shortcut, as there's no removal possible.
+ if (!user) {
+ accels = app->get_accels_for_action(name);
+ }
+ accels.push_back(shortcut.get_abbrev());
+ app->set_accels_for_action(name, accels);
+ action_user_set[name] = user;
+ _changed.emit();
+ return true;
+ }
+ }
+
+ // Oops, not an action!
+ std::cerr << "Shortcuts::add_shortcut: No Action for " << name.raw() << std::endl;
+ return false;
+}
+
+
+// Add a user shortcut, updating user's shortcut file if successful.
+bool
+Shortcuts::add_user_shortcut(Glib::ustring name, const Gtk::AccelKey& shortcut)
+{
+ // Remove previous shortcut(s) for action.
+ remove_shortcut(name);
+
+ // Remove previous use of shortcut from other actions.
+ remove_shortcut(shortcut);
+
+ // Add shortcut, if successful, save to file.
+ if (add_shortcut(name, shortcut, true)) { // Always user.
+ // Save
+ return write_user();
+ }
+
+ std::cerr << "Shortcut::add_user_shortcut: Failed to add: " << name.raw() << " with shortcut " << shortcut.get_abbrev().raw() << std::endl;
+ return false;
+};
+
+
+// Remove a shortcut via key. Return name of removed action.
+Glib::ustring
+Shortcuts::remove_shortcut(const Gtk::AccelKey& shortcut)
+{
+ std::vector<Glib::ustring> actions = app->get_actions_for_accel(shortcut.get_abbrev());
+ if (actions.empty()) {
+ return Glib::ustring(); // No action, no pie.
+ }
+
+ Glib::ustring action_name;
+ for (auto const &action : actions) {
+ // Remove just the one shortcut, leaving the others intact.
+ std::vector<Glib::ustring> accels = app->get_accels_for_action(action);
+ auto it = std::find(accels.begin(), accels.end(), shortcut.get_abbrev());
+ if (it != accels.end()) {
+ action_name = action;
+ accels.erase(it);
+ _changed.emit();
+ }
+ app->set_accels_for_action(action, accels);
+ }
+
+ return action_name;
+}
+
+
+// Remove a shortcut via action name.
+bool
+Shortcuts::remove_shortcut(Glib::ustring name)
+{
+ for (auto const &action : list_all_detailed_action_names()) {
+ if (action == name) {
+ // Action exists
+ app->unset_accels_for_action(action);
+ action_user_set.erase(action);
+ _changed.emit();
+ return true;
+ }
+ }
+
+ return false;
+}
+
+// Remove a user shortcut, updating user's shortcut file.
+bool
+Shortcuts::remove_user_shortcut(Glib::ustring name)
+{
+ // Check if really user shortcut.
+ bool user_shortcut = is_user_set(name);
+
+ if (!user_shortcut) {
+ // We don't allow removing non-user shortcuts.
+ return false;
+ }
+
+ if (remove_shortcut(name)) {
+ // Save
+ write_user();
+
+ // Reread to get original shortcut (if any).
+ init();
+ return true;
+ }
+
+ std::cerr << "Shortcuts::remove_user_shortcut: Failed to remove shortcut for: " << name.raw() << std::endl;
+ return false;
+}
+
+
+// Remove all user's shortcuts (simply overwrites existing file).
+bool
+Shortcuts::clear_user_shortcuts()
+{
+ // Create new empty document and save
+ auto *document = new XML::SimpleDocument();
+ XML::Node * node = document->createElement("keys");
+ node->setAttribute("name", "User Shortcuts");
+ document->appendChild(node);
+ Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(get_path_string(USER, KEYS, "default.xml"));
+ sp_repr_save_file(document, file->get_path().c_str(), nullptr);
+ GC::release(document);
+
+ // Re-read everything!
+ init();
+ _changed.emit();
+ return true;
+}
+
+Glib::ustring
+Shortcuts::get_label(const Gtk::AccelKey& shortcut)
+{
+ Glib::ustring label;
+
+ if (!shortcut.is_null()) {
+ // ::get_label shows key pad and numeric keys identically.
+ // TODO: Results in labels like "Numpad Alt+5"
+ if (shortcut.get_abbrev().find("KP") != Glib::ustring::npos) {
+ label += _("Numpad");
+ label += " ";
+ }
+
+ label += Gtk::AccelGroup::get_label(shortcut.get_key(), shortcut.get_mod());
+ }
+
+ return label;
+}
+
+/*
+ * Return: keyval translated to group 0 in lower 32 bits, modifier encoded in upper 32 bits.
+ *
+ * Usage of group 0 (i.e. the main, typically English layout) instead of simply event->keyval
+ * ensures that shortcuts work regardless of the active keyboard layout (e.g. Cyrillic).
+ *
+ * The returned modifiers are the modifiers that were not "consumed" by the translation and
+ * can be used by the application to define a shortcut, e.g.
+ * - when pressing "Shift+9" the resulting character is "(";
+ * the shift key was "consumed" to make this character and should not be part of the shortcut
+ * - when pressing "Ctrl+9" the resulting character is "9";
+ * the ctrl key was *not* consumed to make this character and must be included in the shortcut
+ * - Exception: letter keys like [A-Z] always need the shift modifier,
+ * otherwise lower case and uper case keys are treated as equivalent.
+ */
+Gtk::AccelKey
+Shortcuts::get_from_event(GdkEventKey const *event, bool fix)
+{
+ // MOD2 corresponds to the NumLock key. Masking it out allows
+ // shortcuts to work regardless of its state.
+ Gdk::ModifierType initial_modifiers = Gdk::ModifierType(event->state & ~Gdk::MOD2_MASK);
+ unsigned int consumed_modifiers = 0;
+ //Gdk::ModifierType consumed_modifiers = Gdk::ModifierType(0);
+
+ unsigned int keyval = Inkscape::UI::Tools::get_latin_keyval(event, &consumed_modifiers);
+
+ // If a key value is "convertible", i.e. it has different lower case and upper case versions,
+ // convert to lower case and don't consume the "shift" modifier.
+ bool is_case_convertible = !(gdk_keyval_is_upper(keyval) && gdk_keyval_is_lower(keyval));
+ if (is_case_convertible) {
+ keyval = gdk_keyval_to_lower(keyval);
+ consumed_modifiers &= ~ Gdk::SHIFT_MASK;
+ }
+
+ // The InkscapePreferences dialog returns an event structure where the Shift modifier is not
+ // set for keys like '('. This causes '(' to be converted to '9' by get_latin_keyval. It also
+ // returns 'Shift-k' for 'K' (instead of 'Shift-K') but this is not a problem.
+ // We fix this by restoring keyval to its original value.
+ if (fix) {
+ keyval = event->keyval;
+ }
+
+ auto unused_modifiers = Gdk::ModifierType((initial_modifiers &~ consumed_modifiers)
+ & GDK_MODIFIER_MASK
+ &~ GDK_LOCK_MASK);
+
+ // std::cout << "Shortcuts::get_from_event: End: "
+ // << " Key: " << std::hex << keyval << " (" << (char)keyval << ")"
+ // << " Mod: " << std::hex << unused_modifiers << std::endl;
+ return (Gtk::AccelKey(keyval, unused_modifiers));
+}
+
+
+// Get a list of filenames to populate menu
+std::vector<std::pair<Glib::ustring, Glib::ustring>>
+Shortcuts::get_file_names()
+{
+ // TODO Filenames should be std::string but that means changing the whole stack.
+ using namespace Inkscape::IO::Resource;
+
+ // Make a list of all key files from System and User. Glib::ustring should be std::string!
+ std::vector<Glib::ustring> filenames = get_filenames(SYSTEM, KEYS, {".xml"});
+ // Exclude default.xml as it only contains user modifications.
+ std::vector<Glib::ustring> filenames_shared = get_filenames(SHARED, KEYS, {".xml"}, {"default.xml"});
+ // Exclude default.xml as it only contains user modifications.
+ std::vector<Glib::ustring> filenames_user = get_filenames(USER, KEYS, {".xml"}, {"default.xml"});
+ filenames.insert(filenames.end(), filenames_user.begin(), filenames_user.end());
+ filenames.insert(filenames.end(), filenames_shared.begin(), filenames_shared.end());
+
+ // Check file exists and extract out label if it does.
+ std::vector<std::pair<Glib::ustring, Glib::ustring>> names_and_paths;
+ for (auto &filename : filenames) {
+ std::string label = Glib::path_get_basename(filename);
+ Glib::ustring filename_relative = sp_relative_path_from_path(filename, std::string(get_path(SYSTEM, KEYS)));
+
+ XML::Document *document = sp_repr_read_file(filename.c_str(), nullptr, true);
+ if (!document) {
+ std::cerr << "Shortcut::get_file_names: could not parse file: " << filename.raw() << std::endl;
+ continue;
+ }
+
+ XML::NodeConstSiblingIterator iter = document->firstChild();
+ for ( ; iter ; ++iter ) { // We iterate in case of comments.
+ if (strcmp(iter->name(), "keys") == 0) {
+ gchar const *name = iter->attribute("name");
+ if (name) {
+ label = Glib::ustring(name) + " (" + label + ")";
+ }
+ std::pair<Glib::ustring, Glib::ustring> name_and_path = std::make_pair(label, filename_relative);
+ names_and_paths.emplace_back(name_and_path);
+ break;
+ }
+ }
+ if (!iter) {
+ std::cerr << "Shortcuts::get_File_names: not a shortcut keys file: " << filename.raw() << std::endl;
+ }
+
+ Inkscape::GC::release(document);
+ }
+
+ // Sort by name
+ std::sort(names_and_paths.begin(), names_and_paths.end(),
+ [](std::pair<Glib::ustring, Glib::ustring> pair1, std::pair<Glib::ustring, Glib::ustring> pair2) {
+ return Glib::path_get_basename(pair1.first).compare(Glib::path_get_basename(pair2.first)) < 0;
+ });
+
+ // But default.xml at top
+ auto it_default = std::find_if(names_and_paths.begin(), names_and_paths.end(),
+ [](std::pair<Glib::ustring, Glib::ustring>& pair) {
+ return !Glib::path_get_basename(pair.second).compare("default.xml");
+ });
+ if (it_default != names_and_paths.end()) {
+ std::rotate(names_and_paths.begin(), it_default, it_default+1);
+ }
+
+ return names_and_paths;
+}
+
+// void on_foreach(Gtk::Widget& widget) {
+// std::cout << "on_foreach: " << widget.get_name() << std::endl;;
+// }
+
+/*
+ * Update text with shortcuts.
+ * Inkscape includes shortcuts in tooltips and in dialog titles. They need to be updated
+ * anytime a tooltip is changed.
+ */
+void
+Shortcuts::update_gui_text_recursive(Gtk::Widget* widget)
+{
+
+ // NOT what we want
+ // auto activatable = dynamic_cast<Gtk::Activatable *>(widget);
+
+ // We must do this until GTK4
+ GtkWidget* gwidget = widget->gobj();
+ bool is_actionable = GTK_IS_ACTIONABLE(gwidget);
+
+ if (is_actionable) {
+ const gchar* gaction = gtk_actionable_get_action_name(GTK_ACTIONABLE(gwidget));
+ if (gaction) {
+ Glib::ustring action = gaction;
+
+ Glib::ustring variant;
+ GVariant* gvariant = gtk_actionable_get_action_target_value(GTK_ACTIONABLE(gwidget));
+ if (gvariant) {
+ Glib::ustring type = g_variant_get_type_string(gvariant);
+ if (type == "s") {
+ variant = g_variant_get_string(gvariant, nullptr);
+ action += "('" + variant + "')";
+ } else if (type == "i") {
+ variant = std::to_string(g_variant_get_int32(gvariant));
+ action += "(" + variant + ")";
+ } else {
+ std::cerr << "Shortcuts::update_gui_text_recursive: unhandled variant type: " << type.raw() << std::endl;
+ }
+ }
+
+ std::vector<Glib::ustring> accels = app->get_accels_for_action(action);
+
+ Glib::ustring tooltip;
+ auto *iapp = InkscapeApplication::instance();
+ if (iapp) {
+ tooltip = iapp->get_action_extra_data().get_tooltip_for_action(action, true, true);
+ }
+
+ // Add new primary accelerator.
+ if (accels.size() > 0) {
+
+ // Add space between tooltip and accel if there is a tooltip
+ if (!tooltip.empty()) {
+ tooltip += " ";
+ }
+
+ // Convert to more user friendly notation.
+ unsigned int key = 0;
+ Gdk::ModifierType mod = Gdk::ModifierType(0);
+ Gtk::AccelGroup::parse(accels[0], key, mod);
+ tooltip += "(" + Gtk::AccelGroup::get_label(key, mod) + ")";
+ }
+
+ // Update tooltip.
+ widget->set_tooltip_markup(tooltip);
+ }
+ }
+
+ auto container = dynamic_cast<Gtk::Container *>(widget);
+ if (container) {
+ auto children = container->get_children();
+ for (auto child : children) {
+ update_gui_text_recursive(child);
+ }
+ }
+
+}
+
+// Dialogs
+
+// Import user shortcuts from a file.
+bool
+Shortcuts::import_shortcuts() {
+
+ // Users key directory.
+ Glib::ustring directory = get_path_string(USER, KEYS, "");
+
+ // Create and show the dialog
+ Gtk::Window* window = app->get_active_window();
+ if (!window) {
+ return false;
+ }
+ Inkscape::UI::Dialog::FileOpenDialog *importFileDialog =
+ Inkscape::UI::Dialog::FileOpenDialog::create(*window, directory, Inkscape::UI::Dialog::CUSTOM_TYPE, _("Select a file to import"));
+ importFileDialog->addFilterMenu(_("Inkscape shortcuts (*.xml)"), "*.xml");
+ bool const success = importFileDialog->show();
+
+ if (!success) {
+ delete importFileDialog;
+ return false;
+ }
+
+ // Get file name and read.
+ Glib::ustring path = importFileDialog->getFilename(); // It's a full path, not just a filename!
+ delete importFileDialog;
+
+ Glib::RefPtr<Gio::File> file_read = Gio::File::create_for_path(path);
+ if (!read(file_read, true)) {
+ std::cerr << "Shortcuts::import_shortcuts: Failed to read file!" << std::endl;
+ return false;
+ }
+
+ // Save
+ return write_user();
+};
+
+bool
+Shortcuts::export_shortcuts() {
+
+ // Users key directory.
+ Glib::ustring directory = get_path_string(USER, KEYS, "");
+
+ // Create and show the dialog
+ Gtk::Window* window = app->get_active_window();
+ if (!window) {
+ return false;
+ }
+ Inkscape::UI::Dialog::FileSaveDialog *saveFileDialog =
+ Inkscape::UI::Dialog::FileSaveDialog::create(*window, directory, Inkscape::UI::Dialog::CUSTOM_TYPE, _("Select a filename for export"),
+ "", "", Inkscape::Extension::FILE_SAVE_METHOD_SAVE_AS);
+ saveFileDialog->addFilterMenu(_("Inkscape shortcuts (*.xml)"), "*.xml");
+ saveFileDialog->setFilename("shortcuts.xml");
+ bool success = saveFileDialog->show();
+
+ // Get file name and write.
+ if (success) {
+ Glib::ustring path = saveFileDialog->getFilename(); // It's a full path, not just a filename!
+ if (Inkscape::IO::get_file_extension(path) != ".xml") {
+ path += ".xml";
+ }
+ if (path.size() > 0) {
+ Glib::ustring newFileName = Glib::filename_to_utf8(path); // Is this really correct? (Paths should be std::string.)
+ Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(path);
+ success = write(file, User);
+ } else {
+ // Can this ever happen?
+ success = false;
+ }
+ }
+
+ delete saveFileDialog;
+
+ return success;
+};
+
+/** Connects to a signal emitted whenever the shortcuts change */
+sigc::connection Shortcuts::connect_changed(sigc::slot<void ()> const &slot)
+{
+ return _changed.connect(slot);
+}
+
+// For debugging.
+void
+Shortcuts::dump() {
+
+ // What shortcuts are being used?
+ std::vector<Gdk::ModifierType> modifiers {
+ Gdk::ModifierType(0),
+ Gdk::SHIFT_MASK,
+ Gdk::CONTROL_MASK,
+ Gdk::MOD1_MASK,
+ Gdk::SHIFT_MASK | Gdk::CONTROL_MASK,
+ Gdk::SHIFT_MASK | Gdk::MOD1_MASK,
+ Gdk::CONTROL_MASK | Gdk::MOD1_MASK,
+ Gdk::SHIFT_MASK | Gdk::CONTROL_MASK | Gdk::MOD1_MASK
+ };
+ for (auto mod : modifiers) {
+ for (gchar key = '!'; key <= '~'; ++key) {
+
+ Glib::ustring action;
+ Glib::ustring accel = Gtk::AccelGroup::name(key, mod);
+ std::vector<Glib::ustring> actions = app->get_actions_for_accel(accel);
+ if (!actions.empty()) {
+ action = actions[0];
+ }
+
+ std::cout << " shortcut:"
+ << " " << std::setw(8) << std::hex << mod
+ << " " << std::setw(8) << std::hex << key
+ << " " << std::setw(30) << std::left << accel
+ << " " << action
+ << std::endl;
+ }
+ }
+}
+
+void
+Shortcuts::dump_all_recursive(Gtk::Widget* widget)
+{
+ static unsigned int indent = 0;
+ ++indent;
+ for (int i = 0; i < indent; ++i) std::cout << " ";
+
+ // NOT what we want
+ // auto activatable = dynamic_cast<Gtk::Activatable *>(widget);
+
+ // We must do this until GTK4
+ GtkWidget* gwidget = widget->gobj();
+ bool is_actionable = GTK_IS_ACTIONABLE(gwidget);
+ Glib::ustring action;
+ if (is_actionable) {
+ const gchar* gaction = gtk_actionable_get_action_name( GTK_ACTIONABLE(gwidget) );
+ if (gaction) {
+ action = gaction;
+ }
+ }
+
+ std::cout << widget->get_name()
+ << ": actionable: " << std::boolalpha << is_actionable
+ << ": " << widget->get_tooltip_text()
+ << ": " << action
+ << std::endl;
+ auto container = dynamic_cast<Gtk::Container *>(widget);
+ if (container) {
+ auto children = container->get_children();
+ for (auto child : children) {
+ dump_all_recursive(child);
+ }
+ }
+ --indent;
+}
+
+
+} // 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 :
diff --git a/src/ui/shortcuts.h b/src/ui/shortcuts.h
new file mode 100644
index 0000000..7e0aa45
--- /dev/null
+++ b/src/ui/shortcuts.h
@@ -0,0 +1,140 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Shortcuts
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * The contents of this file may be used under the GNU General Public License Version 2 or later.
+ *
+ */
+
+#ifndef INK_SHORTCUTS_H
+#define INK_SHORTCUTS_H
+
+#include <map>
+#include <set>
+
+#include <giomm.h>
+#include <gtkmm.h>
+#include <sigc++/sigc++.h>
+
+namespace Inkscape {
+
+namespace UI {
+namespace View {
+class View;
+}
+}
+
+namespace XML {
+class Document;
+class Node;
+}
+
+struct accel_key_less
+{
+ bool operator()(const Gtk::AccelKey& key1, const Gtk::AccelKey& key2) const
+ {
+ if(key1.get_key() < key2.get_key()) return true;
+ if(key1.get_key() > key2.get_key()) return false;
+ return (key1.get_mod() < key2.get_mod());
+ }
+};
+
+class Shortcuts {
+
+public:
+
+ enum What {
+ All,
+ System,
+ User
+ };
+
+ static Shortcuts& getInstance()
+ {
+ static Shortcuts instance;
+
+ if (!instance.initialized) {
+ instance.init();
+ }
+
+ return instance;
+ }
+
+private:
+ Shortcuts();
+ ~Shortcuts() = default;
+
+public:
+ Shortcuts(Shortcuts const&) = delete;
+ void operator=(Shortcuts const&) = delete;
+
+ void init();
+ void clear();
+
+ bool read( Glib::RefPtr<Gio::File> file, bool user_set = false);
+ bool write(Glib::RefPtr<Gio::File> file, What what = User);
+ bool write_user();
+
+ bool is_user_set(Glib::ustring& action);
+
+ // Add/remove shortcuts
+ bool add_shortcut(Glib::ustring name, const Gtk::AccelKey& shortcut, bool user);
+ bool remove_shortcut(Glib::ustring name);
+ Glib::ustring remove_shortcut(const Gtk::AccelKey& shortcut);
+
+ // User shortcuts
+ bool add_user_shortcut(Glib::ustring name, const Gtk::AccelKey& shortcut);
+ bool remove_user_shortcut(Glib::ustring name);
+ bool clear_user_shortcuts();
+
+ // Invoke action corresponding to key
+ bool invoke_action(GdkEventKey const *event);
+
+ // Utility
+ sigc::connection connect_changed(sigc::slot<void ()> const &slot);
+ static Glib::ustring get_label(const Gtk::AccelKey& shortcut);
+ static Gtk::AccelKey get_from_event(GdkEventKey const *event, bool fix = false);
+ std::vector<Glib::ustring> list_all_detailed_action_names();
+ std::vector<Glib::ustring> list_all_actions();
+
+ static std::vector<std::pair<Glib::ustring, Glib::ustring>> get_file_names();
+
+ void update_gui_text_recursive(Gtk::Widget* widget);
+
+ // Dialogs
+ bool import_shortcuts();
+ bool export_shortcuts();
+
+ // Debug
+ void dump();
+
+ void dump_all_recursive(Gtk::Widget* widget);
+
+private:
+
+ // Gio::Actions
+ Glib::RefPtr<Gtk::Application> app;
+ std::map<Glib::ustring, bool> action_user_set;
+
+ void _read(XML::Node const &keysnode, bool user_set);
+
+ bool initialized = false;
+ sigc::signal<void ()> _changed;
+};
+
+} // Namespace Inkscape
+
+#endif // INK_SHORTCUTS_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/svg-renderer.cpp b/src/ui/svg-renderer.cpp
new file mode 100644
index 0000000..523580c
--- /dev/null
+++ b/src/ui/svg-renderer.cpp
@@ -0,0 +1,120 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * SVG to Pixbuf renderer
+ *
+ * Author:
+ * Michael Kowalski
+ *
+ * Copyright (C) 2020-2021 Michael Kowalski
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "svg-renderer.h"
+#include "io/file.h"
+#include "xml/repr.h"
+#include "object/sp-root.h"
+#include "display/cairo-utils.h"
+#include "helper/pixbuf-ops.h"
+#include "util/units.h"
+
+namespace Inkscape {
+
+Glib::ustring rgba_to_css_color(double r, double g, double b) {
+ char buffer[16];
+ sprintf(buffer, "#%02x%02x%02x",
+ static_cast<int>(r * 0xff + 0.5),
+ static_cast<int>(g * 0xff + 0.5),
+ static_cast<int>(b * 0xff + 0.5)
+ );
+ return Glib::ustring(buffer);
+}
+
+Glib::ustring rgba_to_css_color(const Gdk::RGBA& color) {
+ return rgba_to_css_color(color.get_red(), color.get_green(), color.get_blue());
+}
+
+Glib::ustring rgba_to_css_color(const SPColor& color) {
+ float rgb[3];
+ color.get_rgb_floatv(rgb);
+ return rgba_to_css_color(rgb[0], rgb[1], rgb[2]);
+}
+
+Glib::ustring double_to_css_value(double value) {
+ char buffer[32];
+ // arbitrarily chosen precision
+ sprintf(buffer, "%.4f", value);
+ return Glib::ustring(buffer);
+}
+
+std::shared_ptr<SPDocument> load_document(const char* svg_file_path) {
+ auto file = Gio::File::create_for_path(svg_file_path);
+ return std::shared_ptr<SPDocument>(ink_file_open(file, nullptr));
+}
+
+svg_renderer::svg_renderer(const char* svg_file_path): svg_renderer(load_document(svg_file_path)) {
+}
+
+svg_renderer::svg_renderer(std::shared_ptr<SPDocument> document) {
+ _document = document;
+ if (document) {
+ _root = _document->getRoot();
+ }
+
+ if (!_root) throw std::runtime_error("Cannot find root element in svg document");
+}
+
+size_t svg_renderer::set_style(const Glib::ustring& selector, const char* name, const Glib::ustring& value) {
+ auto objects = _document->getObjectsBySelector(selector);
+ for (auto el : objects) {
+ if (SPCSSAttr* css = sp_repr_css_attr(el->getRepr(), "style")) {
+ sp_repr_css_set_property(css, name, value.c_str());
+ el->changeCSS(css, "style");
+ sp_repr_css_attr_unref(css);
+ }
+ }
+ return objects.size();
+}
+
+double svg_renderer::get_width_px() const {
+ return _document->getWidth().value("px");
+}
+
+double svg_renderer::get_height_px() const {
+ return _document->getHeight().value("px");
+}
+
+Inkscape::Pixbuf *svg_renderer::do_render(double scale)
+{
+ auto dpi = 96 * scale;
+ auto area = *(_document->preferredBounds());
+
+ auto checkerboard_ptr = _checkerboard ? &*_checkerboard : nullptr;
+ return sp_generate_internal_bitmap(_document.get(), area, dpi, {}, false, checkerboard_ptr, scale);
+}
+
+Glib::RefPtr<Gdk::Pixbuf> svg_renderer::render(double scale) {
+ auto pixbuf = do_render(scale);
+ if (!pixbuf) return Glib::RefPtr<Gdk::Pixbuf>();
+
+ // ref it
+ auto raw = Glib::wrap(pixbuf->getPixbufRaw(), true);
+ delete pixbuf;
+ return raw;
+}
+
+Cairo::RefPtr<Cairo::Surface> svg_renderer::render_surface(double scale) {
+ auto pixbuf = do_render(scale);
+ if (!pixbuf) return Cairo::RefPtr<Cairo::Surface>();
+
+ // ref it by saying that we have no reference
+ auto surface = Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(pixbuf->getSurfaceRaw(), false));
+ delete pixbuf;
+ return surface;
+}
+
+void svg_renderer::set_checkerboard_color(unsigned int rgba) {
+ _checkerboard.emplace(rgba);
+}
+
+} // namespace
diff --git a/src/ui/svg-renderer.h b/src/ui/svg-renderer.h
new file mode 100644
index 0000000..aed6bf0
--- /dev/null
+++ b/src/ui/svg-renderer.h
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_SVG_RENDERER_H
+#define SEEN_SVG_RENDERER_H
+
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <gtkmm.h>
+#include <gdkmm/rgba.h>
+
+#include "document.h"
+#include "color.h"
+
+namespace Inkscape {
+
+// utilities for set_style:
+// Gdk color to CSS rgb (no alpha)
+Glib::ustring rgba_to_css_color(const Gdk::RGBA& color);
+Glib::ustring rgba_to_css_color(const SPColor& color);
+// double to low precision string
+Glib::ustring double_to_css_value(double value);
+class Pixbuf;
+
+class svg_renderer
+{
+public:
+ // load SVG document from file (abs path)
+ svg_renderer(const char* svg_file_path);
+ // pass in document to render
+ svg_renderer(std::shared_ptr<SPDocument> document);
+
+ // set inline style on selected elements; return number of elements modified
+ size_t set_style(const Glib::ustring& selector, const char* name, const Glib::ustring& value);
+
+ // render document at given scale
+ Glib::RefPtr<Gdk::Pixbuf> render(double scale);
+ Cairo::RefPtr<Cairo::Surface> render_surface(double scale);
+
+ // if set, draw checkerboard pattern before image
+ void set_checkerboard_color(uint32_t rgba);
+
+ double get_width_px() const;
+ double get_height_px() const;
+
+private:
+ Pixbuf* do_render(double scale);
+ std::shared_ptr<SPDocument> _document;
+ SPRoot* _root = nullptr;
+ std::optional<uint32_t> _checkerboard;
+};
+
+}
+
+#endif
diff --git a/src/ui/syntax.cpp b/src/ui/syntax.cpp
new file mode 100644
index 0000000..624bc46
--- /dev/null
+++ b/src/ui/syntax.cpp
@@ -0,0 +1,399 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file Syntax coloring via Gtksourceview and Pango markup.
+ */
+/* Authors:
+ * Rafael Siejakowski <rs@rs-math.net>
+ * Mike Kowalski
+ *
+ * Copyright (C) 2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/syntax.h"
+
+#include <glibmm/ustring.h>
+#include <pango/pango-attributes.h>
+#include <sstream>
+#include <stdexcept>
+
+#include "color.h"
+#include "config.h"
+#include "io/resource.h"
+#include "object/sp-factory.h"
+#include "util/trim.h"
+
+#if WITH_GSOURCEVIEW
+# include <gtksourceview/gtksource.h>
+#endif
+
+namespace Inkscape::UI::Syntax {
+
+Glib::ustring XMLFormatter::_format(Style const &style, Glib::ustring const &content) const
+{
+ return _format(style, content.c_str());
+}
+
+/** Get the opening tag of the Pango markup for this style. */
+Glib::ustring Style::openingTag() const
+{
+ if (isDefault()) {
+ return "";
+ }
+
+ std::ostringstream ost;
+ ost << "<span";
+ if (color) {
+ ost << " color=\"" << color->raw() << '"';
+ }
+ if (background) {
+ ost << " bgcolor=\"" << background->raw() << '"';
+ }
+ if (bold) {
+ ost << " weight=\"bold\"";
+ }
+ if (italic) {
+ ost << " font_style=\"italic\"";
+ }
+ if (underline) {
+ ost << " underline=\"single\"";
+ }
+
+ ost << ">";
+ return Glib::ustring(ost.str());
+}
+
+/** Get the closing tag of Pango markup for this style. */
+Glib::ustring Style::closingTag() const
+{
+ return isDefault() ? "" : "</span>";
+}
+
+Glib::ustring quote(const char* text)
+{
+ return Glib::ustring::compose("\"%1\"", text);
+}
+
+/** Open a new XML tag with the given tag name. */
+void XMLFormatter::openTag(char const *tag_name)
+{
+ _wip = _format(_style.angular_brackets, "<");
+
+ // Highlight as errors unsupported tags in SVG namespace (explicit or implicit).
+ bool error = false;
+ std::string fully_qualified_name(tag_name);
+ if (fully_qualified_name.empty()) {
+ return;
+ }
+ bool is_svg = false;
+ if (fully_qualified_name.find(':') == std::string::npos) {
+ fully_qualified_name = std::string("svg:") + fully_qualified_name;
+ is_svg = true;
+ } else if (fully_qualified_name.find("svg:") == 0) {
+ is_svg = true;
+ }
+ if (is_svg && !SPFactory::supportsType(fully_qualified_name)) {
+ error = true;
+ }
+ _wip += _format(error ? _style.error : _style.tag_name, tag_name);
+}
+
+void XMLFormatter::addAttribute(char const *name, char const *value)
+{
+ _wip += Glib::ustring::compose(" %1%2%3",
+ _format(_style.attribute_name, name),
+ _format(_style.angular_brackets, "="),
+ _format(_style.attribute_value, quote(value)));
+}
+
+Glib::ustring XMLFormatter::finishTag(bool self_close)
+{
+ return _wip + _format(_style.angular_brackets, self_close ? "/>" : ">");
+}
+
+Glib::ustring XMLFormatter::formatContent(char const* content, bool wrap_in_quotes) const
+{
+ Glib::ustring text = wrap_in_quotes ? quote(content) : content;
+ return _format(_style.content, text);
+}
+
+Glib::ustring XMLFormatter::formatComment(char const* comment, bool wrap_in_marks) const
+{
+ if (wrap_in_marks) {
+ auto wrapped = Glib::ustring::compose("<!--%1-->", comment);
+ return _format(_style.comment, wrapped.c_str());
+ }
+ return _format(_style.comment, comment);
+}
+
+XMLStyles build_xml_styles(const Glib::ustring& syntax_theme)
+{
+ XMLStyles styles;
+
+#if WITH_GSOURCEVIEW
+ auto manager = gtk_source_style_scheme_manager_get_default();
+ if (auto scheme = gtk_source_style_scheme_manager_get_scheme(manager, syntax_theme.c_str())) {
+
+ auto get_color = [](GtkSourceStyle* style, const char* prop) -> std::optional<Glib::ustring> {
+ std::optional<Glib::ustring> maybe_color;
+ Glib::ustring name(prop);
+ gboolean set;
+ gchar* color = 0;
+ g_object_get(style, (name + "-set").c_str(), &set, name.c_str(), &color, nullptr);
+ if (set && color && *color == '#') {
+ maybe_color = Glib::ustring(color);
+ }
+ g_free(color);
+ return maybe_color;
+ };
+
+ auto get_bool = [](GtkSourceStyle* style, const char* prop, bool def = false) -> bool {
+ Glib::ustring name(prop);
+ gboolean set;
+ gboolean flag;
+ g_object_get(style, (name + "-set").c_str(), &set, name.c_str(), &flag, nullptr);
+ return set ? !!flag : def;
+ };
+
+ auto get_underline = [](GtkSourceStyle* style, bool def = false) -> bool {
+ Glib::ustring name("underline");
+ gboolean set;
+ PangoUnderline underline;
+ g_object_get(style, (name + "-set").c_str(), &set, ("pango-" + name).c_str(), &underline, nullptr);
+ return set ? underline != PANGO_UNDERLINE_NONE : def;
+ };
+
+ auto to_style = [&](char const *id) -> Style {
+ auto s = gtk_source_style_scheme_get_style(scheme, id);
+ if (!s) {
+ return Style();
+ }
+
+ Style style;
+
+ style.color = get_color(s, "foreground");
+ style.background = get_color(s, "background");
+ style.bold = get_bool(s, "bold");
+ style.italic = get_bool(s, "italic");
+ style.underline = get_underline(s);
+
+ return style;
+ };
+
+ styles.tag_name = to_style("def:statement");
+ styles.attribute_name = to_style("def:number");
+ styles.attribute_value = to_style("def:string");
+ styles.content = to_style("def:string");
+ styles.comment = to_style("def:comment");
+ styles.prolog = to_style("def:warning");
+ styles.angular_brackets = to_style("draw-spaces");
+ styles.error = to_style("def:error");
+ }
+#endif
+
+ return styles;
+}
+
+/** @brief Reformat CSS for better readability.
+ */
+Glib::ustring prettify_css(Glib::ustring const &css)
+{
+ // Ensure that there's a space after every colon, unless there's a slash (as in a URL).
+ static auto const colon_without_space = Glib::Regex::create(":([^\\s\\/])");
+ auto reformatted = colon_without_space->replace(css, 0, ": \\1", Glib::RegexMatchFlags::REGEX_MATCH_NOTEMPTY);
+ // Ensure that there's a newline after every semicolon.
+ static auto const semicolon_without_newline = Glib::Regex::create(";([^\r\n])");
+ reformatted = semicolon_without_newline->replace(reformatted, 0, ";\n\\1", Glib::RegexMatchFlags::REGEX_MATCH_NEWLINE_ANYCRLF);
+ // If the last character is not a semicolon, append one.
+ if (auto len = css.size(); len && css[len - 1] != ';') {
+ reformatted += ";";
+ }
+ return reformatted;
+}
+
+/** Undo the CSS prettification by stripping some whitespace from CSS markup. */
+Glib::ustring minify_css(Glib::ustring const &css)
+{
+ static auto const space_after = Glib::Regex::create("(:|;)[\\s]+");
+ auto minified = space_after->replace(css, 0, "\\1", Glib::RegexMatchFlags::REGEX_MATCH_NEWLINE_ANY);
+ // Strip final semicolon
+ if (auto const len = minified.size(); len && minified[len - 1] == ';') {
+ minified = minified.erase(len - 1);
+ }
+ return minified;
+}
+
+/** @brief Reformat a path 'd' attibute for better readability. */
+Glib::ustring prettify_svgd(Glib::ustring const &d)
+{
+ auto result = d;
+ Util::trim(result);
+ // Ensure that a non-M command is preceded only by a newline.
+ static auto const space_b4_command = Glib::Regex::create("(?<=\\S)\\s*(?=[LHVCSQTAZlhvcsqtaz])");
+ result = space_b4_command->replace(result, 1, "\n", Glib::RegexMatchFlags::REGEX_MATCH_NEWLINE_ANY);
+
+ // Before a non-initial M command, we want to have two newlines to visually separate the subpaths.
+ static auto const space_b4_m = Glib::Regex::create("(?<=\\S)\\s*(?=[Mm])");
+ result = space_b4_m->replace(result, 1, "\n\n", Glib::RegexMatchFlags::REGEX_MATCH_NEWLINE_ANY);
+
+ // Ensure that there's a space after each command letter other than Z.
+ static auto const nospace = Glib::Regex::create("([MLHVCSQTAmlhvcsqta])(?=\\S)");
+ return nospace->replace(result, 0, "\\1 ", Glib::RegexMatchFlags::REGEX_MATCH_NEWLINE_ANY);
+}
+
+/** @brief Remove excessive space, including newlines, from a path 'd' attibute. */
+Glib::ustring minify_svgd(Glib::ustring const &d)
+{
+ static auto const excessive_space = Glib::Regex::create("[\\s]+");
+ auto result = excessive_space->replace(d, 0, " ", Glib::RegexMatchFlags::REGEX_MATCH_NEWLINE_ANY);
+ Util::trim(result);
+ return result;
+}
+
+/** Set default options on a TextView widget used for syntax-colored editing. */
+static void init_text_view(Gtk::TextView* textview)
+{
+ textview->set_wrap_mode(Gtk::WrapMode::WRAP_WORD);
+ textview->set_editable(true);
+ textview->show();
+}
+
+/// Plain text view widget without syntax coloring
+class PlainTextView : public TextEditView
+{
+public:
+ PlainTextView()
+ : _textview(std::make_unique<Gtk::TextView>(Gtk::TextBuffer::create()))
+ {
+ init_text_view(_textview.get());
+ }
+
+ void setStyle(const Glib::ustring& theme) override { /* no op */ }
+ void setText(const Glib::ustring& text) override { _textview->get_buffer()->set_text(text); }
+
+ Glib::ustring getText() const override { return _textview->get_buffer()->get_text(); }
+ Gtk::TextView& getTextView() const override { return *_textview; }
+
+private:
+ std::unique_ptr<Gtk::TextView> _textview;
+};
+
+#if WITH_GSOURCEVIEW
+
+/** @brief Return a pointer to a language manager which is aware of both
+ * default and custom syntaxes.
+ */
+static GtkSourceLanguageManager* get_language_manager()
+{
+ auto ui_path = IO::Resource::get_path_string(IO::Resource::SYSTEM, IO::Resource::UIS);
+ auto default_manager = gtk_source_language_manager_get_default();
+ auto default_paths = gtk_source_language_manager_get_search_path(default_manager);
+
+ std::vector<char const *> all_paths;
+ for (auto path = default_paths; *path; path++) {
+ all_paths.push_back(*path);
+ }
+ all_paths.push_back(ui_path.c_str());
+ all_paths.push_back(nullptr);
+
+ auto result = gtk_source_language_manager_new();
+ gtk_source_language_manager_set_search_path(result, (gchar **)all_paths.data());
+ return result;
+}
+
+class SyntaxHighlighting : public TextEditView
+{
+public:
+ SyntaxHighlighting() = delete;
+ /** @brief Construct a syntax highlighter for a given language. */
+ SyntaxHighlighting(char const* const language,
+ Glib::ustring (*prettify_func)(Glib::ustring const &),
+ Glib::ustring (*minify_func)(Glib::ustring const &))
+ : _prettify{prettify_func}
+ , _minify{minify_func}
+ {
+ auto manager = get_language_manager();
+ auto lang = gtk_source_language_manager_get_language(manager, language);
+ _buffer = gtk_source_buffer_new_with_language(lang);
+ auto view = gtk_source_view_new_with_buffer(_buffer);
+ // Increment Glib's internal refcount to prevent the destruction of the
+ // textview by a parent widget (if any); the textview is owned by us!
+ g_object_ref(view);
+
+ _textview = std::unique_ptr<Gtk::TextView>(Glib::wrap((GtkTextView*)view));
+ if (!_textview) {
+ // don't crash when sourceview cannot be created; substitute with a regular one;
+ // in this case GTK has already outputted warnings
+ _textview = std::make_unique<Gtk::TextView>(Gtk::TextBuffer::create());
+ }
+ init_text_view(_textview.get());
+ }
+
+ ~SyntaxHighlighting() override { g_object_unref(_buffer); }
+private:
+ GtkSourceBuffer *_buffer = nullptr; // Owned by us
+ std::unique_ptr<Gtk::TextView> _textview;
+ Glib::ustring (*_prettify)(Glib::ustring const &);
+ Glib::ustring (*_minify)(Glib::ustring const &);
+
+public:
+ void setStyle(Glib::ustring const &theme) override
+ {
+ auto manager = gtk_source_style_scheme_manager_get_default();
+ auto scheme = gtk_source_style_scheme_manager_get_scheme(manager, theme.c_str());
+ gtk_source_buffer_set_style_scheme(_buffer, scheme);
+ }
+
+ /** @brief Set the displayed text to a prettified version of the passed string. */
+ void setText(Glib::ustring const &text) override
+ {
+ _textview->get_buffer()->set_text(_prettify(text));
+ }
+
+ /** @brief Get a minified version of the buffer contents, suitable for inserting into XML. */
+ Glib::ustring getText() const override
+ {
+ return _minify(_textview->get_buffer()->get_text());
+ }
+
+ Gtk::TextView &getTextView() const override { return *_textview; };
+};
+
+#endif // WITH_GSOURCEVIEW
+
+/** Create a styled text view using the desired syntax highlighting mode. */
+std::unique_ptr<TextEditView> TextEditView::create(SyntaxMode mode)
+{
+#if WITH_GSOURCEVIEW
+ auto const no_reformat = [](auto &s) { return s; };
+ switch (mode) {
+ case SyntaxMode::PlainText:
+ return std::make_unique<PlainTextView>();
+ case SyntaxMode::InlineCss:
+ return std::make_unique<SyntaxHighlighting>("inline-css", &prettify_css, &minify_css);
+ case SyntaxMode::CssStyle:
+ return std::make_unique<SyntaxHighlighting>("css", no_reformat, no_reformat);
+ case SyntaxMode::SvgPathData:
+ return std::make_unique<SyntaxHighlighting>("svgd", &prettify_svgd, &minify_svgd);
+ case SyntaxMode::SvgPolyPoints:
+ return std::make_unique<SyntaxHighlighting>("svgpoints", no_reformat, no_reformat);
+ default:
+ throw std::runtime_error("Missing case statement in TetxEditView::create()");
+ }
+#else
+ return std::make_unique<PlainTextView>();
+#endif
+}
+
+} // namespace Inkscape::UI::Syntax
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/syntax.h b/src/ui/syntax.h
new file mode 100644
index 0000000..528ad93
--- /dev/null
+++ b/src/ui/syntax.h
@@ -0,0 +1,134 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file Syntax coloring via Gtksourceview and Pango markup.
+ */
+/* Authors:
+ * Rafael Siejakowski <rs@rs-math.net>
+ *
+ * Copyright (C) 2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_UI_SYNTAX_H
+#define SEEN_UI_UI_SYNTAX_H
+
+#include <memory>
+#include <optional>
+#include <vector>
+#include <gtkmm/textview.h>
+#include <glibmm.h>
+#include <glibmm/ustring.h>
+
+#include "color.h"
+
+namespace Inkscape::UI::Syntax {
+
+/** The style of a single element in a (Pango markup)-enabled widget. */
+struct Style
+{
+ std::optional<Glib::ustring> color;
+ std::optional<Glib::ustring> background;
+ uint8_t bold : 1;
+ uint8_t italic : 1;
+ uint8_t underline : 1;
+
+ Style()
+ : bold{false}
+ , italic{false}
+ , underline{false}
+ {}
+
+ bool isDefault() const { return !color && !background && !bold && !italic && !underline; }
+ Glib::ustring openingTag() const;
+ Glib::ustring closingTag() const;
+};
+
+/** The styles used for simple XML syntax highlighting. */
+struct XMLStyles
+{
+ Style prolog;
+ Style comment;
+ Style angular_brackets;
+ Style tag_name;
+ Style attribute_name;
+ Style attribute_value;
+ Style content;
+ Style error;
+};
+
+/** @brief A formatter for XML syntax, based on Pango markup.
+ *
+ * This mechanism is used in the TreeView in the XML Dialog,
+ * where the syntax highlighting of XML tags is accomplished
+ * via Pango markup.
+ */
+class XMLFormatter
+{
+public:
+ XMLFormatter() = default;
+ XMLFormatter(XMLStyles &&styles)
+ : _style{styles}
+ {}
+
+ void setStyle(XMLStyles const &new_style) { _style = new_style; }
+ void setStyle(XMLStyles &&new_style) { _style = new_style; }
+
+ void openTag(char const *tag_name);
+ void addAttribute(char const *attribute_name, char const *attribute_value);
+ Glib::ustring finishTag(bool self_close = false);
+
+ Glib::ustring formatContent(char const* content, bool wrap_in_quotes = true) const;
+ Glib::ustring formatComment(char const* comment, bool wrap_in_comment_marks = true) const;
+ Glib::ustring formatProlog(char const* prolog) const { return _format(_style.prolog, prolog); }
+
+private:
+ Glib::ustring _format(Style const &style, Glib::ustring const &content) const;
+ Glib::ustring _format(Style const &style, char const *content) const
+ {
+ return style.openingTag() + Glib::Markup::escape_text(content) + style.closingTag();
+ }
+
+ XMLStyles _style;
+ Glib::ustring _wip;
+};
+
+/// Build XML styles from a GTKSourceView syntax color theme.
+XMLStyles build_xml_styles(const Glib::ustring& syntax_theme);
+
+/// Syntax highlighting mode (language).
+enum class SyntaxMode
+{
+ PlainText, ///< Plain text (no highlighting).
+ InlineCss, ///< Inline CSS (contents of a style="..." attribute).
+ CssStyle, ///< File-scope CSS (contents of a CSS file or a <style> tag).
+ SvgPathData, ///< Contents of the 'd' attribute of the SVG <path> element.
+ SvgPolyPoints ///< Contents of the 'points' attribute of <polyline> or <polygon>.
+};
+
+/// Base class for styled text editing widget.
+class TextEditView
+{
+public:
+ virtual ~TextEditView() = default;
+ virtual void setStyle(const Glib::ustring& theme) = 0;
+ virtual void setText(const Glib::ustring& text) = 0;
+ virtual Glib::ustring getText() const = 0;
+ virtual Gtk::TextView& getTextView() const = 0;
+
+ static std::unique_ptr<TextEditView> create(SyntaxMode mode);
+};
+
+} // namespace Inkscape::UI::Syntax
+
+#endif // SEEN_UI_UI_SYNTAX_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/themes.cpp b/src/ui/themes.cpp
new file mode 100644
index 0000000..e882980
--- /dev/null
+++ b/src/ui/themes.cpp
@@ -0,0 +1,687 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** \file
+ * Gtk <themes> helper code.
+ */
+/*
+ * Authors:
+ * Jabiertxof
+ * Martin Owens
+ *
+ * Copyright (C) 2017-2021 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "themes.h"
+#include "inkscape.h"
+#include "preferences.h"
+#include "io/resource.h"
+#include "svg/svg-color.h"
+#include <cstddef>
+#include <cstring>
+#include <gio/gio.h>
+#include <glibmm.h>
+#include <glibmm/ustring.h>
+#include <gtkmm.h>
+#include <map>
+#include <pangomm/font.h>
+#include <pangomm/fontdescription.h>
+#include <utility>
+#include <vector>
+#include <regex>
+#include "svg/css-ostringstream.h"
+#include "ui/dialog/dialog-manager.h"
+#include "ui/dialog/dialog-window.h"
+#include "ui/util.h"
+#include "config.h"
+#if WITH_GSOURCEVIEW
+# include <gtksourceview/gtksource.h>
+#endif
+
+namespace Inkscape {
+namespace UI {
+
+ThemeContext::ThemeContext()
+{
+}
+
+/**
+ * Inkscape fill gtk, taken from glib/gtk code with our own checks.
+ */
+void
+ThemeContext::inkscape_fill_gtk(const gchar *path, gtkThemeList &themes)
+{
+ const gchar *dir_entry;
+ GDir *dir = g_dir_open(path, 0, nullptr);
+ if (!dir)
+ return;
+ while ((dir_entry = g_dir_read_name(dir))) {
+ gchar *filename = g_build_filename(path, dir_entry, "gtk-3.0", "gtk.css", nullptr);
+ bool has_prefer_dark = false;
+
+ Glib::ustring theme = dir_entry;
+ gchar *filenamedark = g_build_filename(path, dir_entry, "gtk-3.0", "gtk-dark.css", nullptr);
+ if (g_file_test(filenamedark, G_FILE_TEST_IS_REGULAR))
+ has_prefer_dark = true;
+ if (themes.find(theme) != themes.end() && !has_prefer_dark) {
+ continue;
+ }
+ if (g_file_test(filename, G_FILE_TEST_IS_REGULAR)) {
+ themes[theme] = has_prefer_dark;
+ }
+ g_free(filename);
+ g_free(filenamedark);
+ }
+
+ g_dir_close(dir);
+}
+
+/**
+ * Get available themes based on locations of gtk directories.
+ */
+std::map<Glib::ustring, bool>
+ThemeContext::get_available_themes()
+{
+ gtkThemeList themes;
+ Glib::ustring theme = "";
+ gchar *path;
+ gchar **builtin_themes;
+ guint i, j;
+ const gchar *const *dirs;
+
+ /* Builtin themes */
+ builtin_themes = g_resources_enumerate_children("/org/gtk/libgtk/theme", G_RESOURCE_LOOKUP_FLAGS_NONE, nullptr);
+ for (i = 0; builtin_themes[i] != NULL; i++) {
+ if (g_str_has_suffix(builtin_themes[i], "/")) {
+ theme = builtin_themes[i];
+ theme.resize(theme.size() - 1);
+ Glib::ustring theme_path = "/org/gtk/libgtk/theme";
+ theme_path += "/" + theme;
+ gchar **builtin_themes_files =
+ g_resources_enumerate_children(theme_path.c_str(), G_RESOURCE_LOOKUP_FLAGS_NONE, nullptr);
+ bool has_prefer_dark = false;
+ if (builtin_themes_files != NULL) {
+ for (j = 0; builtin_themes_files[j] != NULL; j++) {
+ Glib::ustring file = builtin_themes_files[j];
+ if (file == "gtk-dark.css") {
+ has_prefer_dark = true;
+ }
+ }
+ }
+ g_strfreev(builtin_themes_files);
+ themes[theme] = has_prefer_dark;
+ }
+ }
+
+ g_strfreev(builtin_themes);
+
+ path = g_build_filename(g_get_user_data_dir(), "themes", nullptr);
+ inkscape_fill_gtk(path, themes);
+ g_free(path);
+
+ path = g_build_filename(g_get_home_dir(), ".themes", nullptr);
+ inkscape_fill_gtk(path, themes);
+ g_free(path);
+
+ dirs = g_get_system_data_dirs();
+ for (i = 0; dirs[i]; i++) {
+ path = g_build_filename(dirs[i], "themes", nullptr);
+ inkscape_fill_gtk(path, themes);
+ g_free(path);
+ }
+ return themes;
+}
+
+Glib::ustring
+ThemeContext::get_symbolic_colors()
+{
+ Glib::ustring css_str;
+ gchar colornamed[64];
+ gchar colornamedsuccess[64];
+ gchar colornamedwarning[64];
+ gchar colornamederror[64];
+ gchar colornamed_inverse[64];
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring themeiconname = prefs->getString("/theme/iconTheme", prefs->getString("/theme/defaultIconTheme", ""));
+ guint32 colorsetbase = 0x2E3436ff;
+ guint32 colorsetbase_inverse;
+ guint32 colorsetsuccess = 0x4AD589ff;
+ guint32 colorsetwarning = 0xF57900ff;
+ guint32 colorseterror = 0xCC0000ff;
+ colorsetbase = prefs->getUInt("/theme/" + themeiconname + "/symbolicBaseColor", colorsetbase);
+ colorsetsuccess = prefs->getUInt("/theme/" + themeiconname + "/symbolicSuccessColor", colorsetsuccess);
+ colorsetwarning = prefs->getUInt("/theme/" + themeiconname + "/symbolicWarningColor", colorsetwarning);
+ colorseterror = prefs->getUInt("/theme/" + themeiconname + "/symbolicErrorColor", colorseterror);
+ 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);
+ colorsetbase_inverse = colorsetbase ^ 0xffffff00;
+ sp_svg_write_color(colornamed_inverse, sizeof(colornamed_inverse), colorsetbase_inverse);
+ css_str += "@define-color warning_color " + Glib::ustring(colornamedwarning) + ";\n";
+ css_str += "@define-color error_color " + Glib::ustring(colornamederror) + ";\n";
+ css_str += "@define-color success_color " + Glib::ustring(colornamedsuccess) + ";\n";
+ /* ":not(.rawstyle) > image" works only on images in first level of widget container
+ if in the future we use a complex widget with more levels and we dont want to tweak the color
+ here, retaining default we can add more lines like ":not(.rawstyle) > > image"
+ if we not override the color we use defautt theme colors*/
+ bool overridebasecolor = !prefs->getBool("/theme/symbolicDefaultBaseColors", true);
+ if (overridebasecolor) {
+ css_str += "#InkRuler,";
+ css_str += ":not(.rawstyle) > image";
+ css_str += "{color:";
+ css_str += colornamed;
+ css_str += ";}";
+ }
+ css_str += ".dark .forcebright :not(.rawstyle) > image,";
+ css_str += ".dark .forcebright image:not(.rawstyle),";
+ css_str += ".bright .forcedark :not(.rawstyle) > image,";
+ css_str += ".bright .forcedark image:not(.rawstyle),";
+ css_str += ".dark :not(.rawstyle) > image.forcebright,";
+ css_str += ".dark image.forcebright:not(.rawstyle),";
+ css_str += ".bright :not(.rawstyle) > image.forcedark,";
+ css_str += ".bright image.forcedark:not(.rawstyle),";
+ css_str += ".inverse :not(.rawstyle) > image,";
+ css_str += ".inverse image:not(.rawstyle)";
+ css_str += "{color:";
+ if (overridebasecolor) {
+ css_str += colornamed_inverse;
+ } else {
+ // we override base color in this special cases using inverse color
+ css_str += "@theme_bg_color";
+ }
+ css_str += ";}";
+ return css_str;
+}
+
+std::string sp_tweak_background_colors(std::string cssstring, double crossfade, double contrast, bool dark)
+{
+ static std::regex re_no_affect("(inherit|unset|initial|none|url)");
+ static std::regex re_color("background-color( ){0,3}:(.*?);");
+ static std::regex re_image("background-image( ){0,3}:(.*?\\)) *?;");
+ std::string sub = "";
+ std::smatch m;
+ std::regex_search(cssstring, m, re_no_affect);
+ if (m.size() == 0) {
+ if (cssstring.find("background-color") != std::string::npos) {
+ sub = "background-color:shade($2," + Glib::ustring::format(crossfade) + ");";
+ cssstring = std::regex_replace(cssstring, re_color, sub);
+ } else if (cssstring.find("background-image") != std::string::npos) {
+ if (dark) {
+ contrast = std::clamp((int)((contrast) * 27), 0, 100);
+ sub = "background-image:cross-fade(" + Glib::ustring::format(contrast) + "% image(rgb(255,255,255)), image($2));";
+ } else {
+ contrast = std::clamp((int)((contrast) * 90), 0 , 100);
+ sub = "background-image:cross-fade(" + Glib::ustring::format(contrast) + "% image(rgb(0,0,0)), image($2));";
+ }
+ cssstring = std::regex_replace(cssstring, re_image, sub);
+ }
+ } else {
+ cssstring = "";
+ }
+ return cssstring;
+}
+
+static void
+show_parsing_error(const Glib::RefPtr<const Gtk::CssSection>& section, const Glib::Error& error)
+{
+#ifndef NDEBUG
+ g_warning("There is a warning parsing theme CSS:: %s", error.what().c_str());
+#endif
+}
+
+// callback for a "narrow spinbutton" preference change
+struct NarrowSpinbuttonObserver : Preferences::Observer {
+ NarrowSpinbuttonObserver(const char* path, Glib::RefPtr<Gtk::CssProvider> provider):
+ Preferences::Observer(path), _provider(std::move(provider)) {}
+
+ void notify(Preferences::Entry const& new_val) override {
+ auto screen = Gdk::Screen::get_default();
+ if (new_val.getBool()) {
+ Gtk::StyleContext::add_provider_for_screen(screen, _provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ }
+ else {
+ Gtk::StyleContext::remove_provider_for_screen(screen, _provider);
+ }
+ }
+
+ Glib::RefPtr<Gtk::CssProvider> _provider;
+};
+
+/**
+ * \brief Add our CSS style sheets
+ * @param only_providers: Apply only the providers part, from inkscape preferences::theme change, no need to reaply
+ */
+void ThemeContext::add_gtk_css(bool only_providers, bool cached)
+{
+ using namespace Inkscape::IO::Resource;
+ // Add style sheet (GTK3)
+ auto const screen = Gdk::Screen::get_default();
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ gchar *gtkThemeName = nullptr;
+ gchar *gtkIconThemeName = nullptr;
+ Glib::ustring themeiconname;
+ gboolean gtkApplicationPreferDarkTheme;
+ GtkSettings *settings = gtk_settings_get_default();
+ if (settings && !only_providers) {
+ g_object_get(settings, "gtk-icon-theme-name", &gtkIconThemeName, nullptr);
+ g_object_get(settings, "gtk-theme-name", &gtkThemeName, nullptr);
+ g_object_get(settings, "gtk-application-prefer-dark-theme", &gtkApplicationPreferDarkTheme, nullptr);
+ prefs->setBool("/theme/defaultPreferDarkTheme", gtkApplicationPreferDarkTheme);
+ prefs->setString("/theme/defaultGtkTheme", Glib::ustring(gtkThemeName));
+ prefs->setString("/theme/defaultIconTheme", Glib::ustring(gtkIconThemeName));
+ Glib::ustring gtkthemename = prefs->getString("/theme/gtkTheme");
+ if (gtkthemename != "") {
+ g_object_set(settings, "gtk-theme-name", gtkthemename.c_str(), nullptr);
+ }
+ bool preferdarktheme = prefs->getBool("/theme/preferDarkTheme", false);
+ g_object_set(settings, "gtk-application-prefer-dark-theme", preferdarktheme, nullptr);
+ themeiconname = prefs->getString("/theme/iconTheme");
+ if (themeiconname != "") {
+ g_object_set(settings, "gtk-icon-theme-name", themeiconname.c_str(), nullptr);
+ }
+ }
+
+ g_free(gtkThemeName);
+ g_free(gtkIconThemeName);
+
+ int themecontrast = prefs->getInt("/theme/contrast", 10);
+ if (!_contrastthemeprovider) {
+ _contrastthemeprovider = Gtk::CssProvider::create();
+ // We can uncomment this line to remove warnings and errors on the theme
+ _contrastthemeprovider->signal_parsing_error().connect(sigc::ptr_fun(show_parsing_error));
+ }
+ static std::string cssstringcached = "";
+ // we use contrast only if is setup (!= 10)
+ if (themecontrast < 10) {
+ Glib::ustring css_contrast = "";
+ double contrast = (10 - themecontrast) / 30.0;
+ double shade = 1 - contrast;
+ const gchar *variant = nullptr;
+ if (prefs->getBool("/theme/preferDarkTheme", false)) {
+ variant = "dark";
+ }
+ bool dark = prefs->getBool("/theme/darkTheme", false);
+ if (dark) {
+ contrast *= 2.5;
+ shade = 1 + contrast;
+ }
+ Glib::ustring current_theme = prefs->getString("/theme/gtkTheme", prefs->getString("/theme/defaultGtkTheme", ""));
+
+ std::string cssstring = "";
+ if (cached && !cssstringcached.empty()) {
+ cssstring = cssstringcached;
+ } else {
+ GtkCssProvider *current_themeprovider = gtk_css_provider_get_named(current_theme.c_str(), variant);
+ cssstring = gtk_css_provider_to_string(current_themeprovider);
+ }
+ if (contrast) {
+ std::string cssdefined = "";
+ // we do this way to fix issue Inkscape#2345
+ // windows seem crash if text length > 2000;
+
+ std::istringstream f(cssstring);
+ std::string line;
+ while (std::getline(f, line)) {
+ // here we ignore most of class to parse because is in additive mode
+ // so stiles not applied are set on previous context style
+ if (line.find(";") != std::string::npos &&
+ line.find("background-image") == std::string::npos &&
+ line.find("background-color") == std::string::npos)
+ {
+ continue;
+ }
+ cssdefined += sp_tweak_background_colors(line, shade, contrast, dark);
+ cssdefined += "\n";
+ if (!cached) {
+ cssstringcached += line;
+ cssstringcached += "\n";
+ }
+ }
+ if (!cached) {
+ // Split on curly brackets. Even tokens are selectors, odd are values.
+ std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[}{]", cssstringcached);
+ cssstringcached = "";
+ for (unsigned i = 0; i < tokens.size() - 1; i += 2) {
+ Glib::ustring selector = tokens[i];
+ Glib::ustring properties = "";
+ if ((i + 1) < tokens.size()) {
+ properties = tokens[i + 1];
+ }
+ if (properties.find(";") != Glib::ustring::npos) {
+ cssstringcached += selector;
+ cssstringcached += "{\n";
+ cssstringcached += properties;
+ cssstringcached += "}\n";
+ }
+ }
+ }
+ cssstring = cssdefined;
+ }
+ if (!cssstring.empty()) {
+ // Use c format allow parse with errors or warnings
+ gtk_css_provider_load_from_data (_contrastthemeprovider->gobj(), cssstring.c_str(), -1, nullptr);
+ Gtk::StyleContext::add_provider_for_screen(screen, _contrastthemeprovider, GTK_STYLE_PROVIDER_PRIORITY_SETTINGS);
+ }
+ } else {
+ cssstringcached = "";
+ if (_contrastthemeprovider) {
+ Gtk::StyleContext::remove_provider_for_screen(screen, _contrastthemeprovider);
+ }
+ }
+ Glib::ustring style = get_filename(UIS, "style.css");
+ if (!style.empty()) {
+ if (_styleprovider) {
+ Gtk::StyleContext::remove_provider_for_screen(screen, _styleprovider);
+ }
+ if (!_styleprovider) {
+ _styleprovider = Gtk::CssProvider::create();
+ }
+ try {
+ _styleprovider->load_from_path(style);
+ } catch (const Gtk::CssProviderError &ex) {
+ g_critical("CSSProviderError::load_from_path(): failed to load '%s'\n(%s)", style.c_str(),
+ ex.what().c_str());
+ }
+ Gtk::StyleContext::add_provider_for_screen(screen, _styleprovider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ }
+ // load small CSS snippet to style spinbuttons by removing excessive padding
+ if (!_spinbuttonprovider) {
+ _spinbuttonprovider = Gtk::CssProvider::create();
+ Glib::ustring style = get_filename(UIS, "spinbutton.css");
+ if (!style.empty()) {
+ try {
+ _spinbuttonprovider->load_from_path(style);
+ } catch (const Gtk::CssProviderError &ex) {
+ g_critical("CSSProviderError::load_from_path(): failed to load '%s'\n(%s)", style.c_str(), ex.what().c_str());
+ }
+ }
+ }
+ _spinbutton_observer = std::make_unique<NarrowSpinbuttonObserver>("/theme/narrowSpinButton", _spinbuttonprovider);
+ // note: ideally we should remove the callback during destruction, but ThemeContext is never deleted
+ prefs->addObserver(*_spinbutton_observer);
+ // establish default value, so both this setting here and checkbox in preferences are in sync
+ if (!prefs->getEntry(_spinbutton_observer->observed_path).isValid()) {
+ prefs->setBool(_spinbutton_observer->observed_path, true);
+ }
+ _spinbutton_observer->notify(prefs->getEntry(_spinbutton_observer->observed_path));
+
+ Glib::ustring gtkthemename = prefs->getString("/theme/gtkTheme", prefs->getString("/theme/defaultGtkTheme", ""));
+ gtkthemename += ".css";
+ style = get_filename(UIS, gtkthemename.c_str(), false, true);
+ if (!style.empty()) {
+ if (_themeprovider) {
+ Gtk::StyleContext::remove_provider_for_screen(screen, _themeprovider);
+ }
+ if (!_themeprovider) {
+ _themeprovider = Gtk::CssProvider::create();
+ }
+ try {
+ _themeprovider->load_from_path(style);
+ } catch (const Gtk::CssProviderError &ex) {
+ g_critical("CSSProviderError::load_from_path(): failed to load '%s'\n(%s)", style.c_str(),
+ ex.what().c_str());
+ }
+ Gtk::StyleContext::add_provider_for_screen(screen, _themeprovider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ }
+
+ if (!_colorizeprovider) {
+ _colorizeprovider = Gtk::CssProvider::create();
+ }
+ Glib::ustring css_str = "";
+ if (prefs->getBool("/theme/symbolicIcons", false)) {
+ css_str = get_symbolic_colors();
+ }
+ try {
+ _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, _colorizeprovider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+#if __APPLE__
+ Glib::ustring macstyle = get_filename(UIS, "mac.css");
+ if (!macstyle.empty()) {
+ if (_macstyleprovider) {
+ Gtk::StyleContext::remove_provider_for_screen(screen, _macstyleprovider);
+ }
+ if (!_macstyleprovider) {
+ _macstyleprovider = Gtk::CssProvider::create();
+ }
+ try {
+ _macstyleprovider->load_from_path(macstyle);
+ } catch (const Gtk::CssProviderError &ex) {
+ g_critical("CSSProviderError::load_from_path(): failed to load '%s'\n(%s)", macstyle.c_str(),
+ ex.what().c_str());
+ }
+ Gtk::StyleContext::add_provider_for_screen(screen, _macstyleprovider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ }
+#endif
+ style = get_filename(UIS, "user.css");
+ if (!style.empty()) {
+ if (_userprovider) {
+ Gtk::StyleContext::remove_provider_for_screen(screen, _userprovider);
+ }
+ if (!_userprovider) {
+ _userprovider = Gtk::CssProvider::create();
+ }
+ try {
+ _userprovider->load_from_path(style);
+ } catch (const Gtk::CssProviderError &ex) {
+ g_critical("CSSProviderError::load_from_path(): failed to load '%s'\n(%s)", style.c_str(),
+ ex.what().c_str());
+ }
+ Gtk::StyleContext::add_provider_for_screen(screen, _userprovider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ }
+}
+
+/**
+ * Check if current applied theme is dark or not by looking at style context.
+ * This is important to check system default theme is dark or not
+ * It only return True for dark and False for Bright. It does not apply any
+ * property other than preferDarkTheme, so theme should be set before calling
+ * this function as it may otherwise return outdated result.
+ */
+bool ThemeContext::isCurrentThemeDark(Gtk::Container *window)
+{
+ bool dark = false;
+ if (window) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring current_theme =
+ prefs->getString("/theme/gtkTheme", prefs->getString("/theme/defaultGtkTheme", ""));
+ auto settings = Gtk::Settings::get_default();
+ if (settings) {
+ settings->property_gtk_application_prefer_dark_theme() = prefs->getBool("/theme/preferDarkTheme", false);
+ }
+ dark = current_theme.find(":dark") != std::string::npos;
+ // if theme is dark or we use contrast slider feature and have set preferDarkTheme we force the theme dark
+ // and avoid color check, this fix a issue with low contrast themes bad switch of dark theme toggle
+ dark = dark || (prefs->getInt("/theme/contrast", 10) != 10 && prefs->getBool("/theme/preferDarkTheme", false));
+ 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;
+ }
+ }
+ }
+ return dark;
+}
+
+void
+ThemeContext::themechangecallback() {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ // sync "dark" class between app window and floating dialog windows to ensure that
+ // CSS providers relying on it apply in dialog windows too
+ auto dark = prefs->getBool("/theme/darkTheme", false);
+ std::vector<Gtk::Window *> winds;
+ for (auto wnd : Inkscape::UI::Dialog::DialogManager::singleton().get_all_floating_dialog_windows()) {
+ winds.push_back(dynamic_cast<Gtk::Window *>(wnd));
+ }
+ if (auto desktops = INKSCAPE.get_desktops()) {
+ for (auto & desktop : *desktops) {
+ if (desktop == SP_ACTIVE_DESKTOP) {
+ winds.push_back(dynamic_cast<Gtk::Window *>(desktop->getToplevel()));
+ } else {
+ winds.insert(winds.begin(), dynamic_cast<Gtk::Window *>(desktop->getToplevel()));
+ }
+ }
+ }
+ for (auto wnd : winds) {
+ if (Glib::RefPtr<Gdk::Window> w = wnd->get_window()) {
+ set_dark_tittlebar(w, dark);
+ }
+ if (dark) {
+ wnd->get_style_context()->add_class("dark");
+ wnd->get_style_context()->remove_class("bright");
+ } else {
+ wnd->get_style_context()->add_class("bright");
+ wnd->get_style_context()->remove_class("dark");
+ }
+ if (prefs->getBool("/theme/symbolicIcons", false)) {
+ wnd->get_style_context()->add_class("symbolic");
+ wnd->get_style_context()->remove_class("regular");
+ } else {
+ wnd->get_style_context()->add_class("regular");
+ wnd->get_style_context()->remove_class("symbolic");
+ }
+#if (defined (_WIN32) || defined (_WIN64))
+ wnd->present();
+#endif
+ }
+
+ // set default highlight colors (dark/light theme-specific)
+ if (!winds.empty()) {
+ set_default_highlight_colors(getHighlightColors(winds.front()));
+ }
+
+ // select default syntax coloring theme, if needed
+ if (auto desktop = INKSCAPE.active_desktop()) {
+ select_default_syntax_style(isCurrentThemeDark(desktop->getToplevel()));
+ }
+}
+
+/**
+ * Load the highlight colours from the current theme. If the theme changes
+ * you can call this function again to refresh the list.
+ */
+std::vector<guint32> ThemeContext::getHighlightColors(Gtk::Window *window)
+{
+ std::vector<guint32> colors;
+ if (!window) return colors;
+
+ Glib::ustring name = "highlight-color-";
+
+ for (int i = 1; i <= 8; ++i) {
+ auto context = Gtk::StyleContext::create();
+
+ // The highlight colors will be attached to a GtkWidget
+ // but it isn't neccessary to use this in the .css file.
+ auto path = window->get_style_context()->get_path();
+ path.path_append_type(Gtk::Widget::get_type());
+ path.iter_add_class(-1, name + Glib::ustring::format(i));
+ context->set_path(path);
+
+ // Get the color from the new context
+ auto color = context->get_color();
+ guint32 rgba =
+ gint32(0xff * color.get_red()) << 24 |
+ gint32(0xff * color.get_green()) << 16 |
+ gint32(0xff * color.get_blue()) << 8 |
+ gint32(0xff * color.get_alpha());
+ colors.push_back(rgba);
+ }
+ return colors;
+}
+
+void ThemeContext::adjustGlobalFontScale(double factor) {
+ if (factor < 0.1 || factor > 10) {
+ g_warning("Invalid font scaling factor %f in ThemeContext::adjust_global_font_scale", factor);
+ return;
+ }
+
+ auto screen = Gdk::Screen::get_default();
+ Gtk::StyleContext::remove_provider_for_screen(screen, _fontsizeprovider);
+
+ Inkscape::CSSOStringStream os;
+ os.precision(3);
+ os << "widget, menuitem, popover { font-size: " << factor << "rem; }\n";
+
+ os << ".mono-font {";
+ auto desc = getMonospacedFont();
+ os << "font-family: " << desc.get_family() << ";";
+ switch (desc.get_style()) {
+ case Pango::STYLE_ITALIC:
+ os << "font-style: italic;";
+ break;
+ case Pango::STYLE_OBLIQUE:
+ os << "font-style: oblique;";
+ break;
+ }
+ os << "font-weight: " << static_cast<int>(desc.get_weight()) << ";";
+ double size = desc.get_size();
+ os << "font-size: " << factor * (desc.get_size_is_absolute() ? size : size / Pango::SCALE) << "px;";
+ os << "}";
+
+ _fontsizeprovider->load_from_data(os.str());
+
+ // note: priority set to APP - 1 to make sure styles.css take precedence over generic font-size
+ Gtk::StyleContext::add_provider_for_screen(screen, _fontsizeprovider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION - 1);
+}
+
+void ThemeContext::initialize_source_syntax_styles() {
+#if WITH_GSOURCEVIEW
+ auto manager = gtk_source_style_scheme_manager_get_default();
+ // to reset path: gtk_source_style_scheme_manager_set_search_path(manager, nullptr);
+ auto themes = IO::Resource::get_path_string(IO::Resource::SYSTEM, IO::Resource::UIS, "syntax-themes");
+ gtk_source_style_scheme_manager_prepend_search_path(manager, themes.c_str());
+#endif
+}
+
+void ThemeContext::select_default_syntax_style(bool dark_theme)
+{
+#if WITH_GSOURCEVIEW
+ auto prefs = Inkscape::Preferences::get();
+ auto default_theme = prefs->getString("/theme/syntax-color-theme");
+ auto light = "inkscape-light";
+ auto dark = "inkscape-dark";
+ if (default_theme.empty() || default_theme == light || default_theme == dark) {
+ prefs->setString("/theme/syntax-color-theme", dark_theme ? dark : light);
+ }
+#endif
+}
+
+void ThemeContext::saveMonospacedFont(Pango::FontDescription desc)
+{
+ Preferences::get()->setString(get_monospaced_font_pref_path(), desc.to_string());
+}
+
+Pango::FontDescription ThemeContext::getMonospacedFont() const
+{
+ auto font = Preferences::get()->getString(get_monospaced_font_pref_path(), "Monospace 13");
+ return Pango::FontDescription(font);
+}
+
+double ThemeContext::getFontScale() const
+{
+ return Preferences::get()->getDoubleLimited(get_font_scale_pref_path(), 100.0, 10.0, 500.0);
+}
+
+void ThemeContext::saveFontScale(double scale)
+{
+ Preferences::get()->setDouble(get_font_scale_pref_path(), scale);
+}
+
+} // UI
+} // 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/themes.h b/src/ui/themes.h
new file mode 100644
index 0000000..a463279
--- /dev/null
+++ b/src/ui/themes.h
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** \file
+ * Gtk <themes> helper code.
+ */
+/*
+ * Authors:
+ * Jabiertxof
+ * Martin Owens
+ *
+ * Copyright (C) 2017-2021 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef UI_THEMES_H_SEEN
+#define UI_THEMES_H_SEEN
+
+#include <cstring>
+#include <glibmm.h>
+#include <glibmm/ustring.h>
+#include <gtkmm.h>
+#include <map>
+#include <pangomm/fontdescription.h>
+#include <utility>
+#include <vector>
+#include <sigc++/signal.h>
+#include "preferences.h"
+
+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.
+ */
+typedef std::map<Glib::ustring, bool> gtkThemeList;
+class ThemeContext
+{
+public:
+ ThemeContext();
+ ~ThemeContext() = default;
+ // Name of theme -> has dark theme
+ typedef std::map<Glib::ustring, bool> gtkThemeList;
+ void inkscape_fill_gtk(const gchar *path, gtkThemeList &themes);
+ std::map<Glib::ustring, bool> get_available_themes();
+ void add_gtk_css(bool only_providers, bool cached = false);
+ void add_icon_theme();
+ Glib::ustring get_symbolic_colors();
+ Glib::RefPtr<Gtk::CssProvider> getColorizeProvider() { return _colorizeprovider;}
+ Glib::RefPtr<Gtk::CssProvider> getContrastThemeProvider() { return _contrastthemeprovider;}
+ Glib::RefPtr<Gtk::CssProvider> getThemeProvider() { return _themeprovider;}
+ Glib::RefPtr<Gtk::CssProvider> getStyleProvider() { return _styleprovider;}
+ Glib::RefPtr<Gtk::CssProvider> getUserProvider() { return _userprovider;}
+ sigc::signal<void ()> getChangeThemeSignal() { return _signal_change_theme;}
+ void themechangecallback();
+ /// Set application-wide font size adjustment by a factor, where 1 is 100% (no change)
+ void adjustGlobalFontScale(double factor);
+ /// Get current font scaling factor (50 - 150, percent of "normal" size)
+ double getFontScale() const;
+ /// Save font scaling factor in preferences
+ void saveFontScale(double scale);
+ static Glib::ustring get_font_scale_pref_path() { return "/theme/fontscale"; }
+
+ /// User-selected monospaced font used by XML dialog and attribute editor
+ Pango::FontDescription getMonospacedFont() const;
+ void saveMonospacedFont(Pango::FontDescription desc);
+ static Glib::ustring get_monospaced_font_pref_path() { return "/ui/mono-font/desc"; }
+
+ // True if current theme (applied one) is dark
+ bool isCurrentThemeDark(Gtk::Container *window);
+
+static std::vector<guint32> getHighlightColors(Gtk::Window *window);
+
+ static void initialize_source_syntax_styles();
+ static void select_default_syntax_style(bool dark_theme);
+
+private:
+ // user change theme
+ sigc::signal<void ()> _signal_change_theme;
+ Glib::RefPtr<Gtk::CssProvider> _styleprovider;
+ Glib::RefPtr<Gtk::CssProvider> _themeprovider;
+ Glib::RefPtr<Gtk::CssProvider> _contrastthemeprovider;
+ Glib::RefPtr<Gtk::CssProvider> _colorizeprovider;
+ Glib::RefPtr<Gtk::CssProvider> _spinbuttonprovider;
+ Glib::RefPtr<Gtk::CssProvider> _userprovider;
+#if __APPLE__
+ Glib::RefPtr<Gtk::CssProvider> _macstyleprovider;
+#endif
+ std::unique_ptr<Preferences::Observer> _spinbutton_observer;
+ Glib::RefPtr<Gtk::CssProvider> _fontsizeprovider = Gtk::CssProvider::create();
+};
+
+}
+}
+#endif /* !UI_THEMES_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/tool-factory.cpp b/src/ui/tool-factory.cpp
new file mode 100644
index 0000000..7c9232d
--- /dev/null
+++ b/src/ui/tool-factory.cpp
@@ -0,0 +1,113 @@
+// 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/pages-tool.h"
+#include "ui/tools/pencil-tool.h"
+#include "ui/tools/rect-tool.h"
+#include "ui/tools/marker-tool.h"
+#include "ui/tools/select-tool.h"
+#include "ui/tools/booleans-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(SPDesktop *desktop, std::string const &id)
+{
+ ToolBase *tool = nullptr;
+
+ if (id == "/tools/shapes/arc")
+ tool = new ArcTool(desktop);
+ else if (id == "/tools/shapes/3dbox")
+ tool = new Box3dTool(desktop);
+ else if (id == "/tools/calligraphic")
+ tool = new CalligraphicTool(desktop);
+ else if (id == "/tools/connector")
+ tool = new ConnectorTool(desktop);
+ else if (id == "/tools/dropper")
+ tool = new DropperTool(desktop);
+ else if (id == "/tools/eraser")
+ tool = new EraserTool(desktop);
+ else if (id == "/tools/paintbucket")
+ tool = new FloodTool(desktop);
+ else if (id == "/tools/gradient")
+ tool = new GradientTool(desktop);
+ else if (id == "/tools/lpetool")
+ tool = new LpeTool(desktop);
+ else if (id == "/tools/marker")
+ tool = new MarkerTool(desktop);
+ else if (id == "/tools/measure")
+ tool = new MeasureTool(desktop);
+ else if (id == "/tools/mesh")
+ tool = new MeshTool(desktop);
+ else if (id == "/tools/nodes")
+ tool = new NodeTool(desktop);
+ else if (id == "/tools/booleans")
+ tool = new InteractiveBooleansTool(desktop);
+ else if (id == "/tools/pages")
+ tool = new PagesTool(desktop);
+ else if (id == "/tools/freehand/pencil")
+ tool = new PencilTool(desktop);
+ else if (id == "/tools/freehand/pen")
+ tool = new PenTool(desktop);
+ else if (id == "/tools/shapes/rect")
+ tool = new RectTool(desktop);
+ else if (id == "/tools/select")
+ tool = new SelectTool(desktop);
+ else if (id == "/tools/shapes/spiral")
+ tool = new SpiralTool(desktop);
+ else if (id == "/tools/spray")
+ tool = new SprayTool(desktop);
+ else if (id == "/tools/shapes/star")
+ tool = new StarTool(desktop);
+ else if (id == "/tools/text")
+ tool = new TextTool(desktop);
+ else if (id == "/tools/tweak")
+ tool = new TweakTool(desktop);
+ else if (id == "/tools/zoom")
+ tool = new ZoomTool(desktop);
+ else {
+ fprintf(stderr, "WARNING: unknown tool: %s", id.c_str());
+ // Backup tool prevents crashes in signals that expect a tool to exist.
+ tool = new SelectTool(desktop);
+ }
+
+ 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..0addc0f
--- /dev/null
+++ b/src/ui/tool-factory.h
@@ -0,0 +1,42 @@
+// 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>
+
+class SPDesktop;
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+class ToolBase;
+}
+}
+}
+
+struct ToolFactory {
+ static Inkscape::UI::Tools::ToolBase *createObject(SPDesktop *desktop, 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/README b/src/ui/tool/README
new file mode 100644
index 0000000..8a1c41a
--- /dev/null
+++ b/src/ui/tool/README
@@ -0,0 +1,29 @@
+
+
+This directory contains code related to on-screen editing (nodes, handles, etc.).
+
+Note that there are classes with similar functionality based on the SPKnot class in src/ui/knot.
+
+Classes here:
+
+ * ControlPoint
+ ** CurveDragPoint
+ ** Handle
+ ** RotationHandle
+ ** SelectableContrlPoint
+ *** Node
+ ** SelectorPoint,
+ ** TransformHandle
+ *** RotateHandle
+ *** ScaleHandle
+ **** ScaleCornerHandle
+ **** ScaleSideHandle
+ *** SkewHandle
+
+ * Manipulator
+ ** PointManipulator
+ *** MultiManipulator
+ *** PathManipulator
+ *** MultiPathManipulator
+ ** Selector
+ ** TransformHandleSet
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..b94129c
--- /dev/null
+++ b/src/ui/tool/control-point-selection.cpp
@@ -0,0 +1,784 @@
+// 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 "display/control/snap-indicator.h"
+#include "ui/widget/canvas.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, Inkscape::CanvasItemGroup *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::Path const &path, bool invert)
+{
+ std::vector<SelectableControlPoint *> out;
+ for (auto _all_point : _all_points) {
+ if (path.winding(*_all_point) % 2 != 0) {
+ if (invert) {
+ erase(_all_point);
+ } else {
+ 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);
+ }
+ for (auto cur : _points) {
+ cur->fixNeighbors();
+ }
+
+ _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, AlignTargetNode target)
+{
+ if (empty()) return;
+ Geom::Dim2 d = static_cast<Geom::Dim2>((axis + 1) % 2);
+
+ Geom::OptInterval bound;
+ for (auto _point : _points) {
+ bound.unionWith(Geom::OptInterval(_point->position()[d]));
+ }
+
+ if (!bound) { return; }
+
+ double new_coord;
+ switch (target) {
+ case AlignTargetNode::FIRST_NODE:
+ new_coord=(_points_list.front())->position()[d];
+ break;
+ case AlignTargetNode::LAST_NODE:
+ new_coord=(_points_list.back())->position()[d];
+ break;
+ case AlignTargetNode::MID_NODE:
+ new_coord=bound->middle();
+ break;
+ case AlignTargetNode::MIN_NODE:
+ new_coord=bound->min();
+ break;
+ case AlignTargetNode::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);
+ }
+ for (auto cur : _points) {
+ cur->fixNeighbors();
+ }
+ signal_update.emit();
+}
+
+void ControlPointSelection::_pointUngrabbed()
+{
+ _desktop->snapindicator->remove_snaptarget();
+ _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 = std::nullopt;
+}
+
+void ControlPointSelection::_update()
+{
+ _updateBounds();
+ _updateTransformHandles(false);
+ if (_bounds) {
+ _handles->rotationCenter().move(_bounds->midpoint());
+ }
+}
+
+void ControlPointSelection::_updateBounds()
+{
+ _rot_radius = std::nullopt;
+ _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 + Tools::gobble_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..36260f8
--- /dev/null
+++ b/src/ui/tool/control-point-selection.h
@@ -0,0 +1,179 @@
+// 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 <optional>
+#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 "ui/tool/node-types.h"
+#include "snap-candidate.h"
+
+class SPDesktop;
+
+namespace Inkscape {
+class CanvasItemGroup;
+namespace UI {
+class TransformHandleSet;
+class SelectableControlPoint;
+}
+}
+
+namespace Inkscape {
+namespace UI {
+
+class ControlPointSelection : public Manipulator, public sigc::trackable {
+public:
+ ControlPointSelection(SPDesktop *d, Inkscape::CanvasItemGroup *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::Path const &, bool invert = false);
+ 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, AlignTargetNode target = AlignTargetNode::MID_NODE);
+ 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;
+ std::optional<double> _rot_radius;
+ std::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..1c40dfb
--- /dev/null
+++ b/src/ui/tool/control-point.cpp
@@ -0,0 +1,587 @@
+// 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/control/canvas-item-enums.h"
+#include "display/control/snap-indicator.h"
+
+#include "object/sp-namedview.h"
+
+#include "ui/tools/tool-base.h"
+#include "ui/tool/control-point.h"
+#include "ui/tool/event-utils.h"
+#include "ui/tool/transform-handle-set.h"
+#include "ui/widget/canvas.h" // autoscroll
+
+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());
+
+Gdk::EventMask const ControlPoint::_grab_event_mask = (Gdk::BUTTON_PRESS_MASK |
+ Gdk::BUTTON_RELEASE_MASK |
+ Gdk::POINTER_MOTION_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,
+ Inkscape::CanvasItemGroup *group)
+ : _desktop(d)
+ , _cset(cset)
+ , _position(initial_pos)
+{
+ _canvas_item_ctrl = make_canvasitem<Inkscape::CanvasItemCtrl>(group ? group : _desktop->getCanvasControls(),
+ Inkscape::CANVAS_ITEM_CTRL_SHAPE_BITMAP);
+ _canvas_item_ctrl->set_name("CanvasItemCtrl:ControlPoint");
+ _canvas_item_ctrl->set_pixbuf(std::move(pixbuf));
+ _canvas_item_ctrl->set_fill( _cset.normal.fill);
+ _canvas_item_ctrl->set_stroke(_cset.normal.stroke);
+ _canvas_item_ctrl->set_anchor(anchor);
+
+ _commonInit();
+}
+
+ControlPoint::ControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor,
+ Inkscape::CanvasItemCtrlType type,
+ ColorSet const &cset,
+ Inkscape::CanvasItemGroup *group)
+ : _desktop(d)
+ , _cset(cset)
+ , _position(initial_pos)
+{
+ _canvas_item_ctrl = make_canvasitem<Inkscape::CanvasItemCtrl>(group ? group : _desktop->getCanvasControls(), type);
+ _canvas_item_ctrl->set_name("CanvasItemCtrl:ControlPoint");
+ _canvas_item_ctrl->set_fill( _cset.normal.fill);
+ _canvas_item_ctrl->set_stroke(_cset.normal.stroke);
+ _canvas_item_ctrl->set_anchor(anchor);
+
+ _commonInit();
+}
+
+ControlPoint::~ControlPoint()
+{
+ // avoid storing invalid points in mouseovered_point
+ if (this == mouseovered_point) {
+ _clearMouseover();
+ }
+
+ //g_signal_handler_disconnect(G_OBJECT(_canvas_item_ctrl), _event_handler_connection);
+ _event_handler_connection.disconnect();
+ _canvas_item_ctrl->hide();
+}
+
+void ControlPoint::_commonInit()
+{
+ _canvas_item_ctrl->set_position(_position);
+ _event_handler_connection =
+ _canvas_item_ctrl->connect_event(sigc::bind(sigc::ptr_fun(_event_handler), this));
+ // _event_handler_connection = g_signal_connect(G_OBJECT(_canvas_item_ctrl), "event",
+ // G_CALLBACK(_event_handler), this);
+}
+
+void ControlPoint::setPosition(Geom::Point const &pos)
+{
+ _position = pos;
+ _canvas_item_ctrl->set_position(_position);
+}
+
+void ControlPoint::move(Geom::Point const &pos)
+{
+ setPosition(pos);
+}
+
+void ControlPoint::transform(Geom::Affine const &m) {
+ move(position() * m);
+}
+
+bool ControlPoint::visible() const
+{
+ return _canvas_item_ctrl->is_visible();
+}
+
+void ControlPoint::setVisible(bool v)
+{
+ if (v) {
+ _canvas_item_ctrl->show();
+ } else {
+ _canvas_item_ctrl->hide();
+ }
+}
+
+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;
+}
+
+
+// ===== Setters =====
+
+void ControlPoint::_setSize(unsigned int size)
+{
+ _canvas_item_ctrl->set_size(size);
+}
+
+void ControlPoint::_setControlType(Inkscape::CanvasItemCtrlType type)
+{
+ _canvas_item_ctrl->set_type(type);
+}
+
+void ControlPoint::_setAnchor(SPAnchorType anchor)
+{
+// g_object_set(_canvas_item_ctrl, "anchor", anchor, nullptr);
+}
+
+// re-routes events into the virtual function TODO: Refactor this nonsense.
+bool ControlPoint::_event_handler(GdkEvent *event, ControlPoint *point)
+{
+ if ((point == nullptr) || (point->_desktop == nullptr)) {
+ return false;
+ }
+ return point->_eventHandler(point->_desktop->event_context, event);
+}
+
+// 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->getDesktop() !=_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);
+
+ switch(event->type)
+ {
+ case GDK_BUTTON_PRESS:
+ next_release_doubleclick = 0;
+ if (event->button.button == 1 && !event_context->is_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
+ _canvas_item_ctrl->grab(_grab_event_mask); // cursor is null
+ _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:
+ if (_event_grab && ! event_context->is_space_panning()) {
+ _desktop->snapindicator->remove_snaptarget();
+ bool transferred = false;
+ if (!_drag_initiated) {
+ bool t = fabs(event->motion.x - _drag_event_origin[Geom::X]) <= drag_tolerance &&
+ fabs(event->motion.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(&event->motion);
+ // _drag_initiated might change during the above virtual call
+ _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, &event->motion);
+ move(new_pos);
+ _updateDragTip(&event->motion); // update dragging tip after moving to new position
+
+ _desktop->getCanvas()->enable_autoscroll();
+ _desktop->set_coordinate_status(_position);
+ event_context->snap_delay_handler(nullptr, 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 start_item_handler or start_root_handler
+ // if (_desktop && _desktop->event_context && _desktop->event_context->_delayed_snap_event) {
+ event_context->process_delayed_snap_event();
+
+ _canvas_item_ctrl->ungrab();
+ _setMouseover(this, event->button.state);
+ _event_grab = false;
+
+ if (_drag_initiated) {
+ // it is the end of a drag
+ _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);
+ }
+ _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
+ event_context->discard_delayed_snap_event();
+ 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);
+
+ _canvas_item_ctrl->ungrab();
+ _clearMouseover(); // this will also reset state to normal
+ _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);
+ prev_point->_canvas_item_ctrl->ungrab();
+ _canvas_item_ctrl->grab(_grab_event_mask); // cursor is null
+
+ _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;
+}
+
+// TODO: RENAME
+void ControlPoint::_handleControlStyling()
+{
+ _canvas_item_ctrl->set_size_default();
+}
+
+void ControlPoint::_setColors(ColorEntry colors)
+{
+ _canvas_item_ctrl->set_fill(colors.fill);
+ _canvas_item_ctrl->set_stroke(colors.stroke);
+}
+
+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..345f918
--- /dev/null
+++ b/src/ui/tool/control-point.h
@@ -0,0 +1,413 @@
+// 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/control/canvas-item-ctrl.h"
+#include "display/control/canvas-item-enums.h"
+#include "display/control/canvas-item-ptr.h"
+
+#include "enums.h" // TEMP TEMP
+
+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);
+
+ /**
+ * Apply any node repairs, by default no fixing is applied but Nodes will update
+ * smooth nodes to make sure nodes are kept consistent.
+ */
+ virtual void fixNeighbors() {};
+
+ /// @}
+
+ /// @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,
+ Inkscape::CanvasItemCtrlType type,
+ ColorSet const &cset = _default_color_set,
+ Inkscape::CanvasItemGroup *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,
+ Inkscape::CanvasItemGroup *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);
+
+ void _setSize(unsigned int size);
+
+ void _setControlType(Inkscape::CanvasItemCtrlType type);
+
+ void _setAnchor(SPAnchorType anchor);
+
+ /**
+ * 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; }
+
+
+ CanvasItemPtr<Inkscape::CanvasItemCtrl> _canvas_item_ctrl; ///< Visual representation of the control point.
+
+ ColorSet const &_cset; ///< Colors used to represent the point
+
+ State _state = STATE_NORMAL;
+
+ 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 Gdk::EventMask const _grab_event_mask;
+
+ static bool _drag_initiated;
+
+private:
+
+ ControlPoint(ControlPoint const &other);
+
+ void operator=(ControlPoint const &other);
+
+ static bool _event_handler(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
+
+ sigc::connection _event_handler_connection;
+
+ bool _lurking = false;
+
+ 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 = false;
+};
+
+
+} // 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..acf1299
--- /dev/null
+++ b/src/ui/tool/curve-drag-point.cpp
@@ -0,0 +1,247 @@
+// 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,
+ Inkscape::CANVAS_ITEM_CTRL_TYPE_INVISIPOINT,
+ invisible_cset, pm._multi_path_manipulator._path_data.dragpoint_group),
+ _pm(pm)
+{
+ _canvas_item_ctrl->set_name("CanvasItemCtrl:CurveDragPoint");
+ 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());
+ if (held_control(*event)) {
+ _pm.setSegmentType(Inkscape::UI::SEGMENT_STRAIGHT);
+ _pm.update(true);
+ _pm._commit(_("Straighten segments"));
+ }
+ }
+ return true;
+}
+
+bool CurveDragPoint::doubleclicked(GdkEventButton *event)
+{
+ if (event->button != 1 || !first || !first.next()) return false;
+ if (held_control(*event)) {
+ _pm.deleteSegments();
+ _pm.update(true);
+ _pm._commit(_("Remove segment"));
+ } else {
+ _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 (state_held_control(state)) {
+ return C_("Path segment tip",
+ "<b>Ctrl</b>: click to change line type");
+ }
+ 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..f131d4f
--- /dev/null
+++ b/src/ui/tool/event-utils.cpp
@@ -0,0 +1,93 @@
+// 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 "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;
+}
+
+/** 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..37961e3
--- /dev/null
+++ b/src/ui/tool/event-utils.h
@@ -0,0 +1,129 @@
+// 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>
+
+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 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.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..a4f34de
--- /dev/null
+++ b/src/ui/tool/multi-path-manipulator.cpp
@@ -0,0 +1,907 @@
+// 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 "live_effects/lpeobject.h"
+
+#include "object/sp-path.h"
+
+#include "ui/icon-names.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
+{
+ 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 beginning.
+ * @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) {
+ auto lpobj = cast<LivePathEffectObject>(r.object);
+ if (!is<SPPath>(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::copySelectedPath(Geom::PathBuilder *builder)
+{
+ if (_selection.empty())
+ return;
+ invokeForAll(&PathManipulator::copySelectedPath, builder);
+ _done(_("Copy 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) {
+ deleteNodes(keep_shape ? NodeDeleteMode::curve_fit : NodeDeleteMode::line_segment);
+}
+
+void MultiPathManipulator::deleteNodes(NodeDeleteMode mode)
+{
+ if (_selection.empty()) return;
+ invokeForAll(&PathManipulator::deleteNodes, mode);
+ _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, AlignTargetNode target)
+{
+ if (_selection.empty()) return;
+ _selection.align(d, target);
+ 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);
+}
+
+void MultiPathManipulator::updatePaths()
+{
+ invokeForAll(&PathManipulator::updatePath);
+}
+
+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);
+ //MK: how can multi-path-manipulator know it is dealing with a bspline if it's checking tool mode???
+ /*
+ // 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 {
+ */
+ auto mode =
+ held_control(event->key) ?
+ (del_preserves_shape ? NodeDeleteMode::inverse_auto : NodeDeleteMode::curve_fit) :
+ (del_preserves_shape ? NodeDeleteMode::automatic : NodeDeleteMode::line_segment);
+ deleteNodes(mode);
+
+ // 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:
+ 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, reason, INKSCAPE_ICON("tool-node-editor"));
+ } else {
+ DocumentUndo::done(_desktop->getDocument(), reason, INKSCAPE_ICON("tool-node-editor"));
+ }
+ 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(), reason, INKSCAPE_ICON("tool-node-editor"));
+ 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 cast<SPItem>(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..7ae6b9e
--- /dev/null
+++ b/src/ui/tool/multi-path-manipulator.h
@@ -0,0 +1,159 @@
+// 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 <2geom/path-sink.h>
+#include "node.h"
+#include "commit-events.h"
+#include "manipulator.h"
+#include "modifier-tracker.h"
+#include "node-types.h"
+#include "shape-record.h"
+#include "ui/tool/path-manipulator.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 copySelectedPath(Geom::PathBuilder *builder);
+ void joinNodes();
+ void breakNodes();
+ void deleteNodes(NodeDeleteMode mode);
+ void deleteNodes(bool keep_shape);
+ void joinSegments();
+ void deleteSegments();
+ void alignNodes(Geom::Dim2 d, AlignTargetNode target = AlignTargetNode::MID_NODE);
+ 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();
+ void updatePaths();
+
+ 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..bad6a5c
--- /dev/null
+++ b/src/ui/tool/node-types.h
@@ -0,0 +1,57 @@
+// 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
+};
+
+enum class AlignTargetNode {
+ LAST_NODE,
+ FIRST_NODE,
+ MID_NODE,
+ MIN_NODE,
+ MAX_NODE
+};
+
+} // 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..b1cd452
--- /dev/null
+++ b/src/ui/tool/node.cpp
@@ -0,0 +1,1915 @@
+// 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/control/canvas-item-group.h"
+#include "display/control/canvas-item-curve.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/widget/canvas.h"
+#include "ui/modifiers.h"
+
+namespace {
+
+Inkscape::CanvasItemCtrlType nodeTypeToCtrlType(Inkscape::UI::NodeType type)
+{
+ Inkscape::CanvasItemCtrlType result = Inkscape::CANVAS_ITEM_CTRL_TYPE_NODE_CUSP;
+ switch(type) {
+ case Inkscape::UI::NODE_SMOOTH:
+ result = Inkscape::CANVAS_ITEM_CTRL_TYPE_NODE_SMOOTH;
+ break;
+ case Inkscape::UI::NODE_AUTO:
+ result = Inkscape::CANVAS_ITEM_CTRL_TYPE_NODE_AUTO;
+ break;
+ case Inkscape::UI::NODE_SYMMETRIC:
+ result = Inkscape::CANVAS_ITEM_CTRL_TYPE_NODE_SYMETRICAL;
+ break;
+ case Inkscape::UI::NODE_CUSP:
+ default:
+ result = Inkscape::CANVAS_ITEM_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,
+ Inkscape::CANVAS_ITEM_CTRL_TYPE_ROTATE,
+ _handle_colors, data.handle_group)
+ , _handle_line(make_canvasitem<CanvasItemCurve>(data.handle_line_group))
+ , _parent(parent)
+ , _degenerate(true)
+{
+ setVisible(false);
+}
+
+Handle::~Handle() = default;
+
+void Handle::setVisible(bool v)
+{
+ ControlPoint::setVisible(v);
+ _handle_line->set_visible(v);
+}
+
+void Handle::_update_bspline_handles() {
+ // move the handle and its opposite the same proportion
+ if (_pm()._isBSpline()){
+ setPosition(_pm()._bsplineHandleReposition(this, false));
+ double bspline_weight = _pm()._bsplineHandlePosition(this, false);
+ other()->setPosition(_pm()._bsplineHandleReposition(other(), bspline_weight));
+ _pm().update();
+ }
+}
+
+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;
+ 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
+ _update_bspline_handles();
+ 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
+ _update_bspline_handles();
+
+ 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
+ _update_bspline_handles();
+ Inkscape::UI::Tools::sp_update_helperpath(_desktop);
+}
+
+void Handle::setPosition(Geom::Point const &p)
+{
+ ControlPoint::setPosition(p);
+ _handle_line->set_coords(_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();
+ std::optional<Inkscape::Snapper::SnapConstraint> ctrl_constraint;
+
+ // with Alt, preserve length of the handle
+ 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, use Geom::Ray instead of Geom::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 snapping is active and we're not working with a B-spline
+ if (snap && !_pm()._isBSpline()) {
+ // We will only snap this handle to stationary path segments; some path segments may move as we move the
+ // handle; those path segments are connected to the parent node of this handle.
+ ControlPointSelection::Set &nodes = _parent->_selection.allPoints();
+ for (auto node : nodes) {
+ Node *n = static_cast<Node*>(node);
+ if (_parent != n) { // We're adding all nodes in the path, except the parent node of this handle
+ 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();
+ }
+ _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;
+ Inkscape::UI::Tools::sp_update_helperpath(_desktop);
+ _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_("Status line hint",
+ "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_("Status line hint",
+ "<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_("Status line hint",
+ "<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_("Status line hint",
+ "Shift, Ctrl, Alt");
+ }
+ else if (isBSpline) {
+ more = C_("Status line hint",
+ "Shift, Ctrl");
+ }
+ else {
+ more = C_("Status line hint",
+ "Ctrl, Alt");
+ }
+ if (isBSpline) {
+ double power = _pm()._bsplineHandlePosition(h);
+ s = format_tip(C_("Status line hint",
+ "<b>BSpline node handle</b> (%.3g power): "
+ "Shift-drag to move, "
+ "double-click to reset. "
+ "(more: %s)"),
+ power, more);
+ } else if (_parent->type() == NODE_CUSP) {
+ s = format_tip(C_("Status line hint",
+ "<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_("Status line hint",
+ "<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_("Status line hint",
+ "<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_("Status line hint",
+ "<b>%s</b>: "
+ "drag to shape the path" ". "
+ "(more: %s)"),
+ handletype, more);
+ }
+ else {
+ s = C_("Status line hint",
+ "<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_("Status line hint",
+ "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,
+ Inkscape::CANVAS_ITEM_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)
+{
+ _canvas_item_ctrl->set_name("CanvasItemCtrl:Node");
+ // 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 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());
+ }
+
+ // Save original position for post-processing
+ _unfixed_pos = std::optional<Geom::Point>(position());
+
+ setPosition(new_pos);
+ _front.setPosition(_front.position() + delta);
+ _back.setPosition(_back.position() + delta);
+
+ // 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(_desktop);
+}
+
+void Node::transform(Geom::Affine const &m)
+{
+ // 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());
+ }
+
+ // Save original position for post-processing
+ _unfixed_pos = std::optional<Geom::Point>(position());
+
+ setPosition(position() * m);
+ _front.setPosition(_front.position() * m);
+ _back.setPosition(_back.position() * m);
+
+ // 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;
+}
+
+/**
+ * Affine transforms keep handle invariants for smooth and symmetric nodes,
+ * but smooth nodes at ends of linear segments and auto nodes need special treatment
+ *
+ * Call this function once you have finished called ::move or ::transform on ALL nodes
+ * that are being transformed in that one operation to avoid problematic bugs.
+ */
+void Node::fixNeighbors()
+{
+ if (!_unfixed_pos)
+ return;
+
+ Geom::Point const new_pos = position();
+
+ // 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 (*_unfixed_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());
+ }
+
+ _unfixed_pos.reset();
+}
+
+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()
+{
+ _canvas_item_ctrl->lower_to_bottom();
+}
+
+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;
+ int state = 0;
+
+ switch (event->type)
+ {
+ case GDK_SCROLL:
+ state = event->scroll.state;
+ 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;
+ }
+ break;
+ case GDK_KEY_PRESS:
+ state = event->key.state;
+ switch (shortcut_key(event->key))
+ {
+ case GDK_KEY_Page_Up:
+ dir = 1;
+ break;
+ case GDK_KEY_Page_Down:
+ dir = -1;
+ break;
+ default:
+ break;
+ }
+ default:
+ break;
+ }
+
+ using namespace Inkscape::Modifiers;
+ auto linear_grow = Modifier::get(Modifiers::Type::NODE_GROW_LINEAR)->active(state);
+ auto spatial_grow = Modifier::get(Modifiers::Type::NODE_GROW_SPATIAL)->active(state);
+
+ if (dir && (linear_grow || spatial_grow)) {
+ if (linear_grow)
+ _linearGrow(dir);
+ else if (spatial_grow)
+ _selection.spatialGrow(this, dir);
+ return true;
+ }
+
+ 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
+ _canvas_item_ctrl->set_size_extra(selected() ? 2 : 0);
+ switch (state) {
+ // These were used to set "active" and "prelight" flags but the flags weren't being used.
+ case STATE_NORMAL:
+ case STATE_MOUSEOVER:
+ break;
+ case STATE_CLICKED:
+ // 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());
+
+ std::optional<Geom::Point> front_direction, back_direction;
+ Geom::Point origin = _last_drag_origin();
+ Geom::Point dummy_cp;
+ if (_front.isDegenerate()) { // If there is no handle for the path segment towards the next node, then this segment may be straight
+ if (_is_line_segment(this, _next())) {
+ front_direction = _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 { // .. this path segment is curved
+ front_direction = _front.relativePos();
+ scp_free.addVector(*front_direction);
+ }
+
+ if (_back.isDegenerate()) { // If there is no handle for the path segment towards the previous node, then this segment may be straight
+ if (_is_line_segment(_prev(), this)) {
+ back_direction = _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 { // .. this path segment is curved
+ back_direction = _back.relativePos();
+ scp_free.addVector(*back_direction);
+ }
+
+ 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
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
+ double min_angle = M_PI / snaps;
+
+ if (front_direction) { // We only have a front_point if the front handle is extracted, or if it is not extracted but the path segment is straight (see above)
+ constraints.emplace_back(origin, *front_direction);
+ }
+
+ if (back_direction) {
+ constraints.emplace_back(origin, *back_direction);
+ }
+
+ // For smooth nodes, we will also snap to normals of handle lines. For cusp nodes this would be unintuitive and confusing
+ // Only snap to the normals when they are further than snap increment away from the second handle constraint
+ if (_type != NODE_CUSP) {
+ std::optional<Geom::Point> front_normal = Geom::rot90(*front_direction);
+ if (front_normal && (!back_direction ||
+ (fabs(Geom::angle_between(*front_normal, *back_direction)) > min_angle &&
+ fabs(Geom::angle_between(*front_normal, *back_direction)) < M_PI - min_angle)))
+ {
+ constraints.emplace_back(origin, *front_normal);
+ }
+
+ std::optional<Geom::Point> back_normal = Geom::rot90(*back_direction);
+ if (back_normal && (!front_direction ||
+ (fabs(Geom::angle_between(*back_normal, *front_direction)) > min_angle &&
+ fabs(Geom::angle_between(*back_normal, *front_direction)) < M_PI - min_angle)))
+ {
+ constraints.emplace_back(origin, *back_normal);
+ }
+ }
+
+ sp = sm.multipleConstrainedSnaps(Inkscape::SnapCandidatePoint(new_pos, _snapSourceType()), constraints, held_shift(*event));
+ } else {
+ // with Ctrl and no Alt: 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 or line segment, 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 (auto const &node : nodes) {
+ to_clear[node.second]->erase(node.first, false);
+ }
+ std::vector<std::vector<SelectableControlPoint *> > emission;
+ for (long i = 0, e = to_clear.size(); i != e; ++i) {
+ emission.emplace_back();
+ for (auto const &node : nodes) {
+ if (node.second != i)
+ break;
+ emission[i].push_back(node.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..7fa3d2c
--- /dev/null
+++ b/src/ui/tool/node.h
@@ -0,0 +1,513 @@
+// 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"
+
+namespace Inkscape {
+class CanvasItemGroup;
+class CanvasItemCurve;
+
+namespace UI {
+
+class PathManipulator;
+class MultiPathManipulator;
+
+class Node;
+class Handle;
+class NodeList;
+class SubpathList;
+template <typename> class NodeIterator;
+
+std::ostream &operator<<(std::ostream &, NodeType);
+
+struct ListNode {
+ ListNode *ln_next;
+ ListNode *ln_prev;
+ NodeList *ln_list;
+};
+
+struct NodeSharedData {
+ SPDesktop *desktop;
+ ControlPointSelection *selection;
+ Inkscape::CanvasItemGroup *node_group;
+ Inkscape::CanvasItemGroup *handle_group;
+ Inkscape::CanvasItemGroup *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;
+ void _update_bspline_handles();
+ 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
+ CanvasItemPtr<CanvasItemCurve> _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;
+ void fixNeighbors() 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 _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;
+
+ // This is used by fixNeighbors to repair smooth nodes after all move
+ // operations have been completed. If this is empty, no fixing is needed.
+ std::optional<Geom::Point> _unfixed_pos;
+
+ 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..d332c95
--- /dev/null
+++ b/src/ui/tool/path-manipulator.cpp
@@ -0,0 +1,1847 @@
+// 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 <2geom/point.h>
+
+#include <utility>
+#include <vector>
+
+#include "display/curve.h"
+#include "display/control/canvas-item-bpath.h"
+
+#include <2geom/forward.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-slice.h"
+#include "live_effects/lpe-bspline.h"
+#include "live_effects/parameter/path.h"
+
+#include "object/sp-path.h"
+#include "style.h"
+
+#include "ui/icon-names.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/node-types.h"
+#include "ui/tool/path-manipulator.h"
+#include "ui/tools/node-tool.h"
+#include "path/splinefit/bezier-fit.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 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)
+ , _dragpoint(new CurveDragPoint(*this))
+ , /* XML Tree being used here directly while it shouldn't be*/_observer(new PathManipulatorObserver(this, path->getRepr()))
+ , _edit_transform(et)
+ , _lpe_key(std::move(lpe_key))
+{
+ auto lpeobj = cast<LivePathEffectObject>(_path);
+ auto pathshadow = 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 = make_canvasitem<Inkscape::CanvasItemBpath>(_multi_path_manipulator._path_data.outline_group);
+ _outline->hide();
+ _outline->set_stroke(outline_color);
+ _outline->set_fill(0x0, 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)));
+
+ //Define if the path is BSpline on construction
+ _recalculateIsBSpline();
+ _createControlPointsFromGeometry();
+}
+
+PathManipulator::~PathManipulator()
+{
+ delete _dragpoint;
+ delete _observer;
+ _outline.reset();
+ 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) {
+ return;
+ }
+
+ XML::Node *node = _getXMLNode();
+ if (!node) {
+ return;
+ }
+
+ _observer->block();
+ if (!empty()) {
+ _path->updateRepr();
+ node->setAttribute(_nodetypesKey(), _createTypeString());
+ } else {
+ // this manipulator will have to be destroyed right after this call
+ node->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
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Copy the selected nodes using the PathBuilder
+ *
+ * @param builder[out] Selected nodes will be appended to this Path builder
+ * in pixel coordinates with all transforms applied.
+ */
+void PathManipulator::copySelectedPath(Geom::PathBuilder *builder)
+{
+ // Ignore LivePathEffect paths
+ if (!_path || cast<LivePathEffectObject>(_path))
+ return;
+ // Rebuild the selected parts of each subpath
+ for (auto &subpath : _subpaths) {
+ Node *prev = nullptr;
+ bool is_last_node = false;
+ for (auto &node : *subpath) {
+ if (node.selected()) {
+ // The node positions are already transformed
+ if (!builder->inPath() || !prev) {
+ builder->moveTo(node.position());
+ } else {
+ build_segment(*builder, prev, &node);
+ }
+ prev = &node;
+ is_last_node = true;
+ } else {
+ is_last_node = false;
+ }
+ }
+
+ // Complete the path, especially for closed sub paths where the last node is selected
+ if (subpath->closed() && is_last_node) {
+ if (!prev->front()->isDegenerate() || !subpath->begin()->back()->isDegenerate()) {
+ build_segment(*builder, prev, subpath->begin().ptr());
+ }
+ // if that segment is linear, we just call closePath().
+ builder->closePath();
+ }
+ }
+ builder->flush();
+}
+
+/** 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(NodeDeleteMode 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;
+ }
+}
+
+double get_angle(const Geom::Point& p0, const Geom::Point& p1, const Geom::Point& p2) {
+ auto d1 = p1 - p0;
+ auto d2 = p1 - p2;
+ if (d1.isZero() || d2.isZero()) return M_PI;
+
+ auto a1 = atan2(d1);
+ auto a2 = atan2(d2);
+ return a1 - a2;
+}
+
+/**
+ * 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, NodeDeleteMode mode)
+{
+ 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;
+
+ bool keep_shape = mode == NodeDeleteMode::automatic || mode == NodeDeleteMode::curve_fit;
+
+ if ((mode == NodeDeleteMode::automatic || mode == NodeDeleteMode::inverse_auto) && start.prev() && end) {
+ for (NodeList::iterator cur = start; cur != end; cur = cur.next()) {
+ auto back = cur->back() ->isDegenerate() ? cur.prev()->position() : cur->back() ->position();
+ auto front = cur->front()->isDegenerate() ? cur.next()->position() : cur->front()->position();
+ auto angle = get_angle(back, cur->position(), front);
+ auto a = fmod(fabs(angle), 2*M_PI);
+ auto diff = fabs(a - M_PI);
+ bool flat = diff < M_PI / 4; // flat if *somewhat* close to 180 degrees (+-45deg)
+ if (!flat && Geom::distance(back, front) > 1) {
+ // detected a cusp, so we'll try to remove nodes and insert line segment, rather than fitting a curve
+ // if in auto mode, or the opposite in inverse_auto
+ keep_shape = !keep_shape;
+ break;
+ }
+ }
+ }
+
+ // set surrounding node types to cusp if:
+ // 1. keep_shape is off, or
+ // 2. we are deleting at the end or beginning of an open path
+ if ((!keep_shape || !end) && start.prev()) {
+ auto p = start.prev();
+ p->setType(NODE_CUSP, false);
+ p->front()->retract();
+ }
+ if ((!keep_shape || !start.prev()) && end) {
+ end->setType(NODE_CUSP, false);
+ end->back()->retract();
+ }
+
+ if (keep_shape && start.prev() && end) {
+ std::vector<InputPoint> input;
+ Geom::Point result[4];
+ Geom::LineSegment s;
+ unsigned seg = 0;
+
+ for (NodeList::iterator cur = start.prev(); cur != end; cur = cur.next()) {
+ Geom::CubicBezier bc(*cur, *cur->front(), *cur.next()->back(), *cur.next());
+ for (unsigned s = 0; s < samples_per_segment; ++s) {
+ auto t = t_step * s;
+ input.emplace_back(InputPoint(bc.pointAt(t), t));
+ }
+ ++seg;
+ }
+ // Fill last point
+ // last point + its slope
+ input.emplace_back(InputPoint(end->position(), Geom::Point(), end->back()->position(), 1.0));
+
+ // get slope for the first point
+ input.front() = InputPoint(start.prev()->position(), start.prev()->front()->position(), Geom::Point(), 0.0);
+
+ // Compute replacement bezier curve
+ bezier_fit(result, input);
+
+ 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 (!keep_shape && _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;
+ SPCurve line_inside_nodes;
+ 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);
+ 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);
+ 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 when a process updates the path in-situe */
+void PathManipulator::updatePath()
+{
+ _externalChange(PATH_CHANGE_D);
+}
+
+/** 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: {
+ auto path = 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;
+ }
+}
+
+Geom::Affine PathManipulator::_getTransform() const
+{
+ return _i2d_transform * _edit_transform;
+}
+
+/** 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;
+ if (_is_bspline) {
+ pathv = pathv_to_cubicbezier(_spcurve.get_pathvector(), false);
+ } else {
+ 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 = SPCurve(pathv);
+
+ pathv *= _getTransform();
+
+ // 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;
+
+ auto path = cast<SPLPEItem>(_path);
+ if (path){
+ if(path->hasPathEffect()){
+ Inkscape::LivePathEffect::Effect const *this_effect =
+ path->getFirstPathEffectOfType(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(){
+ auto path = cast<SPPath>(_path);
+ if (path && path->hasPathEffect()) {
+ Inkscape::LivePathEffect::Effect const *this_effect =
+ path->getFirstPathEffectOfType(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){
+ SPCurve line_inside_nodes;
+ line_inside_nodes.moveto(n->position());
+ line_inside_nodes.lineto(next_node->position());
+ if(!are_near(h->position(), n->position())){
+ pos = Geom::nearest_time(h->position(), *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;
+ SPCurve line_inside_nodes;
+ 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);
+ } 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() * _getTransform().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 = SPCurve(pathv);
+ if (alert_LPE) {
+ /// \todo note that _path can be an Inkscape::LivePathEffect::Effect* too, kind of confusing, rework member naming?
+ auto path = cast<SPPath>(_path);
+ if (path && path->hasPathEffect()) {
+ Inkscape::LivePathEffect::Effect *this_effect =
+ path->getFirstPathEffectOfType(Inkscape::LivePathEffect::POWERSTROKE);
+ LivePathEffect::LPEPowerStroke *lpe_pwr = dynamic_cast<LivePathEffect::LPEPowerStroke*>(this_effect);
+ if (lpe_pwr) {
+ lpe_pwr->adjustForNewPath();
+ }
+ }
+ }
+ 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) {
+ _outline->hide();
+ return;
+ }
+
+ auto pv = _spcurve.get_pathvector() * _getTransform();
+ // This SPCurve thing has to be killed with extreme prejudice
+ 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());
+ }
+ auto tmp = SPCurve(std::move(pv));
+ _outline->set_bpath(&tmp);
+ _outline->show();
+}
+
+/** Retrieve the geometry of the edited object from the object tree */
+void PathManipulator::_getGeometry()
+{
+ using namespace Inkscape::LivePathEffect;
+ auto lpeobj = cast<LivePathEffectObject>(_path);
+ auto path = cast<SPPath>(_path);
+ if (lpeobj) {
+ Effect *lpe = lpeobj->get_lpe();
+ if (lpe) {
+ PathParam *pathparam = dynamic_cast<PathParam *>(lpe->getParameter(_lpe_key.data()));
+ _spcurve = SPCurve(pathparam->get_pathvector());
+ }
+ } else if (path) {
+ if (path->curveForEdit()) {
+ _spcurve = *path->curveForEdit();
+ } else {
+ _spcurve = 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;
+ auto lpeobj = cast<LivePathEffectObject>(_path);
+ auto path = 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->curveBeforeLPE()) {
+ path->setCurveBeforeLPE(&_spcurve);
+ if (path->hasPathEffectRecursive()) {
+ sp_lpe_item_update_patheffect(path, true, false);
+ }
+ } else {
+ path->setCurve(&_spcurve);
+ }
+ }
+}
+
+/** Figure out in what attribute to store the nodetype string. */
+Glib::ustring PathManipulator::_nodetypesKey()
+{
+ auto lpeobj = 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.
+ auto lpeobj = 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(), NodeDeleteMode::curve_fit);
+ }
+
+ 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(), annotation.data(), INKSCAPE_ICON("tool-node-editor"));
+ }
+}
+
+void PathManipulator::_commit(Glib::ustring const &annotation, gchar const *key)
+{
+ writeXML();
+ DocumentUndo::maybeDone(_desktop->getDocument(), key, annotation.data(), INKSCAPE_ICON("tool-node-editor"));
+}
+
+/** 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 = _getTransform();
+ Geom::PathVector pv = _spcurve.get_pathvector();
+ std::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)
+ {
+ // stroke_tolerance is at least two.
+ int tolerance = std::max(2, (int)stroke_tolerance);
+ _dragpoint->setVisible(true);
+ _dragpoint->setPosition(_desktop->w2d(nearest_pt));
+ _dragpoint->setSize(2 * tolerance - 1);
+ _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
+ * _getTransform().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..673424e
--- /dev/null
+++ b/src/ui/tool/path-manipulator.h
@@ -0,0 +1,195 @@
+// 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/path-sink.h>
+#include <2geom/affine.h>
+#include "ui/tool/node.h"
+#include "ui/tool/manipulator.h"
+#include "live_effects/lpe-bspline.h"
+#include "display/curve.h"
+
+struct SPCanvasItem;
+class SPCurve;
+class SPPath;
+
+namespace Inkscape {
+
+class CanvasItemBpath;
+
+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;
+ Inkscape::CanvasItemGroup *outline_group;
+ Inkscape::CanvasItemGroup *dragpoint_group;
+};
+
+enum class NodeDeleteMode {
+ automatic, // try to preserve shape if deleted nodes do not form sharp corners
+ inverse_auto, // opposite of what automatic mode would do
+ curve_fit, // preserve shape
+ line_segment // do not preserve shape; delete nodes and connect subpaths with a line segment
+};
+
+/**
+ * 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 copySelectedPath(Geom::PathBuilder *builder);
+ void weldNodes(NodeList::iterator preserve_pos = NodeList::iterator());
+ void weldSegments();
+ void breakNodes();
+ void deleteNodes(NodeDeleteMode mode);
+ 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 updatePath();
+ 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, NodeDeleteMode mode);
+ std::string _createTypeString();
+ void _updateOutline();
+ //void _setOutline(Geom::PathVector const &);
+ void _getGeometry();
+ void _setGeometry();
+ Glib::ustring _nodetypesKey();
+ Inkscape::XML::Node *_getXMLNode();
+ Geom::Affine _getTransform() const;
+
+ 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
+ CanvasItemPtr<Inkscape::CanvasItemBpath> _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 = true;
+ bool _show_outline = false;
+ bool _show_path_direction = false;
+ bool _live_outline = true;
+ bool _live_objects = true;
+ bool _is_bspline = false;
+ 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..5e5d0b2
--- /dev/null
+++ b/src/ui/tool/selectable-control-point.cpp
@@ -0,0 +1,150 @@
+// 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::CanvasItemCtrlType type,
+ ControlPointSelection &sel,
+ ColorSet const &cset,
+ Inkscape::CanvasItemGroup *group)
+ : ControlPoint(d, initial_pos, anchor, type, cset, group)
+ , _selection(sel)
+{
+ _canvas_item_ctrl->set_name("CanvasItemCtrl:SelectableControlPoint");
+ _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,
+ Inkscape::CanvasItemGroup *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..d57dd50
--- /dev/null
+++ b/src/ui/tool/selectable-control-point.h
@@ -0,0 +1,80 @@
+// 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::CanvasItemCtrlType type,
+ ControlPointSelection &sel,
+ ColorSet const &cset = _default_scp_color_set,
+ Inkscape::CanvasItemGroup *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,
+ Inkscape::CanvasItemGroup *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/shape-record.h b/src/ui/tool/shape-record.h
new file mode 100644
index 0000000..1f29453
--- /dev/null
+++ b/src/ui/tool/shape-record.h
@@ -0,0 +1,65 @@
+// 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;
+class SPObject;
+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
+ ShapeRole role;
+ Glib::ustring lpe_key; // name of LPE shape param being edited
+
+ Geom::Affine edit_transform; // how to transform controls - used for clipping paths, masks, and markers
+ double edit_rotation; // how to transform controls - used for markers
+
+ 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..875429a
--- /dev/null
+++ b/src/ui/tool/transform-handle-set.cpp
@@ -0,0 +1,827 @@
+// 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/control/canvas-item-rect.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"
+
+
+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}, // normal fill, stroke
+ {0x00ff66ff, 0x000000ff}, // mouseover fill, stroke
+ {0x00ff66ff, 0x000000ff}, // clicked fill, stroke
+ //
+ {0x000000ff, 0x000000ff}, // normal fill, stroke when selected
+ {0x00ff66ff, 0x000000ff}, // mouseover fill, stroke when selected
+ {0x00ff66ff, 0x000000ff} // clicked fill, stroke when selected
+};
+
+TransformHandle::TransformHandle(TransformHandleSet &th, SPAnchorType anchor, Inkscape::CanvasItemCtrlType type)
+ : ControlPoint(th._desktop, Geom::Point(), anchor, type, thandle_cset, th._transform_handle_group)
+ , _th(th)
+{
+ _canvas_item_ctrl->set_name("CanvasItemCtrl:TransformHandle");
+ 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, Inkscape::CanvasItemCtrlType type)
+ : TransformHandle(th, anchor, type)
+ {}
+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), Inkscape::CANVAS_ITEM_CTRL_TYPE_ADJ_HANDLE),
+ _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:
+
+ 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), Inkscape::CANVAS_ITEM_CTRL_TYPE_ADJ_HANDLE)
+ , _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:
+
+ 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), Inkscape::CANVAS_ITEM_CTRL_TYPE_ADJ_ROTATE)
+ , _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:
+ 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), Inkscape::CANVAS_ITEM_CTRL_TYPE_ADJ_SKEW)
+ , _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:
+
+ 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,
+ Inkscape::CANVAS_ITEM_CTRL_TYPE_ADJ_CENTER,
+ _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 ColorSet _center_cset;
+ TransformHandleSet &_th;
+};
+
+ControlPoint::ColorSet RotationCenter::_center_cset = {
+ {0x000000ff, 0x000000ff}, // normal fill, stroke
+ {0x00ff66ff, 0x000000ff}, // mouseover fill, stroke
+ {0x00ff66ff, 0x000000ff}, // clicked fill, stroke
+ //
+ {0x000000ff, 0x000000ff}, // normal fill, stroke when selected
+ {0x00ff66ff, 0x000000ff}, // mouseover fill, stroke when selected
+ {0x00ff66ff, 0x000000ff} // clicked fill, stroke when selected
+};
+
+
+TransformHandleSet::TransformHandleSet(SPDesktop *d, Inkscape::CanvasItemGroup *th_group)
+ : Manipulator(d)
+ , _active(nullptr)
+ , _transform_handle_group(th_group)
+ , _mode(MODE_SCALE)
+ , _in_transform(false)
+ , _visible(true)
+{
+ _trans_outline = new Inkscape::CanvasItemRect(_desktop->getCanvasControls());
+ _trans_outline->set_name("CanvasItemRect:Transform");
+ _trans_outline->hide();
+ _trans_outline->set_dashed(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->set_rect(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);
+ _trans_outline->show();
+}
+
+void TransformHandleSet::_clearActiveHandle()
+{
+ // This can only be called from handles, so they had to be visible before _setActiveHandle
+ _trans_outline->hide();
+ _active = nullptr;
+ _in_transform = false;
+ _updateVisibility(_visible);
+}
+
+void TransformHandleSet::_updateVisibility(bool v)
+{
+ if (v) {
+ Geom::Rect b = bounds();
+
+ // Roughly estimate handle size.
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int handle_index = prefs->getIntLimited("/options/grabsize/value", 3, 1, 15);
+ int handle_size = handle_index * 2 + 1; // Handle pixmaps are actually larger but that's to allow space when handle is rotated.
+
+ 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
+ : !Geom::are_near(bp[otherd], 0));
+ show_skew[i] = (show_rotate && bp[d] >= handle_size
+ && !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
+ _center->setVisible(show_rotate);
+ } 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..8e0eede
--- /dev/null
+++ b/src/ui/tool/transform-handle-set.h
@@ -0,0 +1,147 @@
+// 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 "ui/tool/control-point.h"
+#include "enums.h"
+#include "snap-candidate.h"
+
+class SPDesktop;
+
+namespace Inkscape {
+
+class CanvasItemGroup;
+class CanvasItemRect;
+
+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, Inkscape::CanvasItemGroup *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;
+ Inkscape::CanvasItemGroup *_transform_handle_group;
+ Inkscape::CanvasItemRect *_trans_outline;
+ Mode _mode;
+ bool _in_transform;
+ bool _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, Inkscape::CanvasItemCtrlType type);
+ 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..16e9ac4
--- /dev/null
+++ b/src/ui/toolbar/arc-toolbar.cpp
@@ -0,0 +1,542 @@
+// 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 "object/sp-ellipse.h"
+#include "object/sp-namedview.h"
+
+#include "ui/icon-names.h"
+#include "ui/tools/arc-tool.h"
+#include "ui/widget/canvas.h"
+#include "ui/widget/combo-tool-item.h"
+#include "ui/widget/label-tool-item.h"
+#include "ui/widget/spinbutton.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::DocumentUndo;
+using Inkscape::Util::Quantity;
+using Inkscape::Util::unit_table;
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+ArcToolbar::ArcToolbar(SPDesktop *desktop)
+ : Toolbar(desktop)
+ , _tracker(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR))
+{
+ auto init_units = desktop->getNamedView()->display_units;
+ _tracker->setActiveUnit(init_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_val = Quantity::convert(rx_val, "px", init_units);
+
+ _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->get_spin_button()->addUnitTracker(_tracker);
+ _rx_item->set_focus_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_val = Quantity::convert(ry_val, "px", init_units);
+
+ _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->get_spin_button()->addUnitTracker(_tracker);
+ _ry_item->set_focus_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(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(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->removeObserver(*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();
+
+ 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 (is<SPGenericEllipse>(item)) {
+
+ auto ge = cast<SPGenericEllipse>(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();
+ ge->updateRepr();
+ ge->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+
+ modmade = true;
+ }
+ }
+
+ if (modmade) {
+ DocumentUndo::done(_desktop->getDocument(), _("Ellipse: Change radius"), INKSCAPE_ICON("draw-ellipse"));
+ }
+
+ _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, nullptr);
+
+ bool modmade = false;
+ auto itemlist= _desktop->getSelection()->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ SPItem *item = *i;
+ if (is<SPGenericEllipse>(item)) {
+
+ auto ge = cast<SPGenericEllipse>(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();
+ ge->updateRepr();
+ 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, _("Arc: Change start/end"), INKSCAPE_ICON("draw-ellipse"));
+ }
+
+ _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 (is<SPGenericEllipse>(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(), _("Arc: Change arc type"), INKSCAPE_ICON("draw-ellipse"));
+ }
+
+ _freeze = false;
+}
+
+void
+ArcToolbar::defaults()
+{
+ _start_adj->set_value(0.0);
+ _end_adj->set_value(0.0);
+
+ if(_desktop->canvas) _desktop->canvas->grab_focus();
+}
+
+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->removeObserver(*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->removeObserver(*this);
+ GC::release(_repr);
+ _repr = nullptr;
+ }
+
+ SPItem *item = nullptr;
+
+ for(auto i : selection->items()){
+ if (is<SPGenericEllipse>(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->addObserver(*this);
+ _repr->synthesizeEvents(*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::notifyAttributeChanged(Inkscape::XML::Node &repr, GQuark,
+ Inkscape::Util::ptr_shared,
+ Inkscape::Util::ptr_shared)
+{
+ // quit if run by the _changed callbacks
+ if (_freeze) {
+ return;
+ }
+
+ // in turn, prevent callbacks from responding
+ _freeze = true;
+
+ if (auto ge = cast<SPGenericEllipse>(_item)) {
+ Unit const *unit = _tracker->getActiveUnit();
+ g_return_if_fail(unit != nullptr);
+
+ gdouble rx = ge->getVisibleRx();
+ gdouble ry = ge->getVisibleRy();
+ _rx_adj->set_value(Quantity::convert(rx, "px", unit));
+ _ry_adj->set_value(Quantity::convert(ry, "px", unit));
+ }
+
+ gdouble start = repr.getAttributeDouble("sodipodi:start", 0.0);;
+ gdouble end = repr.getAttributeDouble("sodipodi:end", 0.0);
+
+ _start_adj->set_value(mod360((start * 180)/M_PI));
+ _end_adj->set_value(mod360((end * 180)/M_PI));
+
+ sensitivize(_start_adj->get_value(), _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")) {
+ _type_buttons[0]->set_active();
+ } else if (!strcmp(arctypestr,"arc")) {
+ _type_buttons[1]->set_active();
+ } else {
+ _type_buttons[2]->set_active();
+ }
+
+ _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..284af61
--- /dev/null
+++ b/src/ui/toolbar/arc-toolbar.h
@@ -0,0 +1,120 @@
+// 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 <gtkmm/adjustment.h>
+
+#include "toolbar.h"
+
+#include "xml/node-observer.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 XML::NodeObserver
+{
+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{false};
+ bool _single;
+
+ XML::Node *_repr{nullptr};
+ 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;
+
+ void notifyAttributeChanged(Inkscape::XML::Node &node, GQuark name,
+ Inkscape::Util::ptr_shared old_value,
+ Inkscape::Util::ptr_shared new_value) final;
+
+
+protected:
+ ArcToolbar(SPDesktop *desktop);
+ ~ArcToolbar() override;
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+};
+
+}
+}
+}
+
+#endif /* !SEEN_ARC_TOOLBAR_H */
diff --git a/src/ui/toolbar/booleans-toolbar.cpp b/src/ui/toolbar/booleans-toolbar.cpp
new file mode 100644
index 0000000..e3172c8
--- /dev/null
+++ b/src/ui/toolbar/booleans-toolbar.cpp
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A toolbar for the Builder tool.
+ *
+ * Authors:
+ * Martin Owens
+ *
+ * Copyright (C) 2022 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "desktop.h"
+#include "ui/builder-utils.h"
+#include "ui/toolbar/booleans-toolbar.h"
+#include "ui/tools/booleans-tool.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+
+BooleansToolbar::BooleansToolbar(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &builder, SPDesktop *desktop)
+ : Gtk::Toolbar(cobject)
+ , _builder(builder)
+ , _btn_confirm(get_widget<Gtk::ToolButton>(builder, "confirm"))
+ , _btn_cancel(get_widget<Gtk::ToolButton>(builder, "cancel"))
+{
+ _btn_confirm.signal_clicked().connect([=]{
+ auto ec = dynamic_cast<Tools::InteractiveBooleansTool *>(desktop->event_context);
+ ec->shape_commit();
+ });
+ _btn_cancel.signal_clicked().connect([=]{
+ auto ec = dynamic_cast<Tools::InteractiveBooleansTool *>(desktop->event_context);
+ ec->shape_cancel();
+ });
+}
+
+void BooleansToolbar::on_parent_changed(Gtk::Widget *) {
+ _builder.reset();
+}
+
+GtkWidget *
+BooleansToolbar::create(SPDesktop *desktop)
+{
+ BooleansToolbar *toolbar;
+ auto builder = Inkscape::UI::create_builder("toolbar-booleans.ui");
+ builder->get_widget_derived("booleans-toolbar", toolbar, desktop);
+ return GTK_WIDGET(toolbar->gobj());
+}
+
+} // namespace Toolbar
+} // namespace UI
+} // namespace Inkscape
diff --git a/src/ui/toolbar/booleans-toolbar.h b/src/ui/toolbar/booleans-toolbar.h
new file mode 100644
index 0000000..167ec40
--- /dev/null
+++ b/src/ui/toolbar/booleans-toolbar.h
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A toolbar for the Builder tool.
+ *
+ * Authors:
+ * Martin Owens
+ *
+ * Copyright (C) 2022 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_TOOLBAR_BOOLEANS_TOOLBAR_H
+#define INKSCAPE_UI_TOOLBAR_BOOLEANS_TOOLBAR_H
+
+#include <gtkmm.h>
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+
+class BooleansToolbar : public Gtk::Toolbar
+{
+public:
+ static GtkWidget *create(SPDesktop *desktop);
+
+ BooleansToolbar(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &builder, SPDesktop *desktop);
+
+ void on_parent_changed(Gtk::Widget *) override;
+private:
+ Glib::RefPtr<Gtk::Builder> _builder;
+
+ Gtk::ToolButton &_btn_confirm;
+ Gtk::ToolButton &_btn_cancel;
+};
+
+} // namespace Toolbar
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_TOOLBAR_BOOLEANS_TOOLBAR_H
diff --git a/src/ui/toolbar/box3d-toolbar.cpp b/src/ui/toolbar/box3d-toolbar.cpp
new file mode 100644
index 0000000..8245f42
--- /dev/null
+++ b/src/ui/toolbar/box3d-toolbar.cpp
@@ -0,0 +1,408 @@
+// 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 <gtkmm/adjustment.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "selection.h"
+
+#include "object/box3d.h"
+#include "object/persp3d.h"
+
+#include "ui/icon-names.h"
+#include "ui/tools/box3d-tool.h"
+#include "ui/widget/canvas.h"
+#include "ui/widget/spin-button-tool-item.h"
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+
+Box3DToolbar::Box3DToolbar(SPDesktop *desktop)
+ : Toolbar(desktop)
+{
+ 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(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(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(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", _("3D Box: Change perspective (angle of infinite axis)"), INKSCAPE_ICON("draw-cuboid"));
+
+ _freeze = false;
+}
+
+void
+Box3DToolbar::vp_state_changed(Proj::Axis axis)
+{
+ // TODO: Take all selected perspectives into account
+ auto 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();
+
+ 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();
+ persp->set_VP_state (axis, set_infinite ? Proj::VP_INFINITE : Proj::VP_FINITE);
+}
+
+void
+Box3DToolbar::check_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec)
+{
+ if (dynamic_cast<Inkscape::UI::Tools::Box3dTool*>(ec)) {
+ _changed = desktop->getSelection()->connectChanged(sigc::mem_fun(*this, &Box3DToolbar::selection_changed));
+ selection_changed(desktop->getSelection());
+ } else {
+ if (_changed)
+ _changed.disconnect();
+
+ if (_repr) { // remove old listener
+ _repr->removeObserver(*this);
+ Inkscape::GC::release(_repr);
+ _repr = nullptr;
+ }
+ }
+}
+
+Box3DToolbar::~Box3DToolbar()
+{
+ if (_repr) { // remove old listener
+ _repr->removeObserver(*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->removeObserver(*this);
+ Inkscape::GC::release(_repr);
+ _repr = nullptr;
+ }
+
+ SPItem *item = selection->singleItem();
+ auto box = cast<SPBox3D>(item);
+ if (box) {
+ // FIXME: Also deal with multiple selected boxes
+ Persp3D *persp = box->get_perspective();
+ if (!persp) {
+ g_warning("Box has no perspective set!");
+ return;
+ }
+ persp_repr = persp->getRepr();
+ if (persp_repr) {
+ _repr = persp_repr;
+ Inkscape::GC::anchor(_repr);
+ _repr->addObserver(*this);
+ _repr->synthesizeEvents(*this);
+
+ selection->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_warning ("No perspective given to box3d_resync_toolbar().");
+ 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.get(), axis);
+
+ if (is_infinite) {
+ toggle_btn->set_active(true);
+ spin_btn->set_sensitive(true);
+
+ double angle = persp->get_infinite_angle(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::notifyAttributeChanged(Inkscape::XML::Node &repr, GQuark, Inkscape::Util::ptr_shared, Inkscape::Util::ptr_shared)
+{
+ // quit if run by the attr_changed or selection changed listener
+ if (_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)
+ _freeze = true;
+
+ // TODO: Only update the appropriate part of the toolbar
+// if (!strcmp(name, "inkscape:vp_z")) {
+ resync_toolbar(&repr);
+// }
+
+ Persp3D *persp = Persp3D::get_from_repr(&repr);
+ if (persp) {
+ persp->update_box_reprs();
+ }
+
+ _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..d81d823
--- /dev/null
+++ b/src/ui/toolbar/box3d-toolbar.h
@@ -0,0 +1,110 @@
+// 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 "axis-manip.h"
+#include "toolbar.h"
+
+#include "xml/node-observer.h"
+
+namespace Gtk {
+class Adjustment;
+}
+
+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 XML::NodeObserver
+{
+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{nullptr};
+ bool _freeze{false};
+
+ 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;
+
+ void notifyAttributeChanged(Inkscape::XML::Node &node, GQuark name,
+ Inkscape::Util::ptr_shared old_value,
+ Inkscape::Util::ptr_shared new_value) final;
+
+protected:
+ Box3DToolbar(SPDesktop *desktop);
+ ~Box3DToolbar() override;
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+};
+
+}
+}
+}
+#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..7015775
--- /dev/null
+++ b/src/ui/toolbar/calligraphy-toolbar.cpp
@@ -0,0 +1,625 @@
+// 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 "ui/dialog/calligraphic-profile-rename.h"
+#include "ui/icon-names.h"
+#include "ui/simple-pref-pusher.h"
+#include "ui/widget/canvas.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::Util::Quantity;
+using Inkscape::Util::Unit;
+using Inkscape::Util::unit_table;
+
+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)
+ , _tracker(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR))
+ , _presets_blocked(false)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ _tracker->prependUnit(unit_table.getUnit("px"));
+ _tracker->changeLabel("%", 0, true);
+ if (prefs->getBool("/tools/calligraphic/abs_width")) {
+ _tracker->setActiveUnitByLabel(prefs->getString("/tools/calligraphic/unit"));
+ }
+
+ /*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.118);
+ Unit const *unit = unit_table.getUnit(prefs->getString("/tools/calligraphic/unit"));
+ _width_adj = Gtk::Adjustment::create(Quantity::convert(width_val, "px", unit), 0.001, 100, 1.0, 10.0);
+ auto width_item =
+ Gtk::manage(new UI::Widget::SpinButtonToolItem("calligraphy-width", _("Width:"), _width_adj, 0.001, 3));
+ 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(desktop->canvas);
+ _width_adj->signal_value_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::width_value_changed));
+ _widget_map["width"] = G_OBJECT(_width_adj->gobj());
+ add(*width_item);
+ _tracker->addAdjustment(_width_adj->gobj());
+ width_item->set_sensitive(true);
+ }
+
+ /* Unit Menu */
+ {
+ auto unit_menu_ti = _tracker->create_tool_item(_("Units"), "");
+ add(*unit_menu_ti);
+ unit_menu_ti->signal_changed_after().connect(sigc::mem_fun(*this, &CalligraphyToolbar::unit_changed));
+ }
+
+ /* 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(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);
+ }
+
+ {
+ /* 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(desktop->canvas);
+ _mass_adj->signal_value_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::mass_value_changed));
+ _widget_map["mass"] = G_OBJECT(_mass_adj->gobj());
+ add(*mass_item);
+ mass_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(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, -100.0, 100.0, 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, -100 = fixed angle in opposite direction)"));
+ flatness_item->set_custom_numeric_menu_data(values, labels);
+ flatness_item->set_focus_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(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(desktop->canvas);
+ _tremor_adj->signal_value_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::tremor_value_changed));
+ _widget_map["tremor"] = G_OBJECT(_tremor_adj->gobj());
+ 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(desktop->canvas);
+ _wiggle_adj->signal_value_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::wiggle_value_changed));
+ _widget_map["wiggle"] = G_OBJECT(_wiggle_adj->gobj());
+ add(*wiggle_item);
+ wiggle_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()
+{
+ Unit const *unit = _tracker->getActiveUnit();
+ g_return_if_fail(unit != nullptr);
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool("/tools/calligraphic/abs_width", _tracker->getCurrentLabel() != "%");
+ prefs->setDouble("/tools/calligraphic/width", Quantity::convert(_width_adj->get_value(), unit, "px"));
+ 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) {
+ 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::unit_changed(int /* NotUsed */)
+{
+ Unit const *unit = _tracker->getActiveUnit();
+ g_return_if_fail(unit != nullptr);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/tools/calligraphic/abs_width", _tracker->getCurrentLabel() != "%");
+ prefs->setDouble("/tools/calligraphic/width",
+ CLAMP(prefs->getDouble("/tools/calligraphic/width"), Quantity::convert(0.001, unit, "px"),
+ Quantity::convert(100, unit, "px")));
+ prefs->setString("/tools/calligraphic/unit", unit->abbr);
+}
+
+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 const &[widget_name, widget] : _widget_map) {
+ 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..88f22ad
--- /dev/null
+++ b/src/ui/toolbar/calligraphy-toolbar.h
@@ -0,0 +1,105 @@
+// 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;
+class UnitTracker;
+}
+
+namespace Toolbar {
+class CalligraphyToolbar : public Toolbar {
+private:
+ UI::Widget::UnitTracker *_tracker;
+ 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 unit_changed(int not_used);
+ 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..5c21968
--- /dev/null
+++ b/src/ui/toolbar/connector-toolbar.cpp
@@ -0,0 +1,412 @@
+// 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 "layer-manager.h"
+#include "selection.h"
+
+#include "object/algorithms/graphlayout.h"
+#include "object/sp-namedview.h"
+#include "object/sp-path.h"
+
+#include "ui/icon-names.h"
+#include "ui/tools/connector-tool.h"
+#include "ui/widget/canvas.h"
+#include "ui/widget/spin-button-tool-item.h"
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+ConnectorToolbar::ConnectorToolbar(SPDesktop *desktop)
+ : Toolbar(desktop)
+{
+ 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(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(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(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->removeObserver(*this);
+ Inkscape::GC::release(_repr);
+ _repr = nullptr;
+ }
+
+ if (repr) {
+ _repr = repr;
+ Inkscape::GC::anchor(_repr);
+ _repr->addObserver(*this);
+ _repr->synthesizeEvents(*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(_desktop, true);
+}
+
+void
+ConnectorToolbar::path_set_ignore()
+{
+ Inkscape::UI::Tools::cc_selection_set_avoid(_desktop, 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);
+ item->getAvoidRef().handleSettingChange();
+ modmade = true;
+ }
+ }
+
+ if (!modmade) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/tools/connector/orthogonal", is_orthog);
+ } else {
+
+ DocumentUndo::done(doc, is_orthog ? _("Set connector type: orthogonal"): _("Set connector type: polyline"), INKSCAPE_ICON("draw-connector"));
+ }
+
+ _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);
+ 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, _("Change connector curvature"), INKSCAPE_ICON("draw-connector"));
+ }
+
+ _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;
+
+ repr->setAttributeCssDouble("inkscape:connector-spacing", _spacing_adj->get_value());
+ _desktop->namedview->updateRepr();
+ bool modmade = false;
+
+ auto items = get_avoided_items(_desktop->layerManager().currentRoot(), _desktop);
+ for (auto item : items) {
+ Geom::Affine m = Geom::identity();
+ avoid_item_move(&m, item);
+ modmade = true;
+ }
+
+ if(modmade) {
+ DocumentUndo::done(doc, _("Change connector spacing"), INKSCAPE_ICON("draw-connector"));
+ }
+ _freeze = false;
+}
+
+void
+ConnectorToolbar::graph_layout()
+{
+ assert(_desktop);
+ if (!_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 = _desktop->getSelection()->items();
+ std::vector<SPItem *> vec(tmp.begin(), tmp.end());
+ graphlayout(vec);
+
+ prefs->setInt("/options/clonecompensation/value", saved_compensation);
+
+ DocumentUndo::done(_desktop->getDocument(), _("Arrange connector network"), INKSCAPE_ICON("dialog-align-and-distribute"));
+}
+
+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 (is<SPPath>(item))
+ {
+ gdouble curvature = cast<SPPath>(item)->connEndPair.getCurvature();
+ bool is_orthog = cast<SPPath>(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::notifyAttributeChanged(Inkscape::XML::Node &repr, GQuark name_,
+ Inkscape::Util::ptr_shared,
+ Inkscape::Util::ptr_shared)
+{
+ auto const name = g_quark_to_string(name_);
+ if (!_freeze && (strcmp(name, "inkscape:connector-spacing") == 0) ) {
+ gdouble spacing = repr.getAttributeDouble("inkscape:connector-spacing", defaultConnSpacing);
+
+ _spacing_adj->set_value(spacing);
+
+ if (_desktop->canvas) {
+ _desktop->canvas->grab_focus();
+ }
+ }
+}
+
+}
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ 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..b2266bd
--- /dev/null
+++ b/src/ui/toolbar/connector-toolbar.h
@@ -0,0 +1,102 @@
+// 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 <gtkmm/adjustment.h>
+
+#include "toolbar.h"
+
+#include "xml/node-observer.h"
+
+class SPDesktop;
+
+namespace Gtk {
+class ToolButton;
+}
+
+namespace Inkscape {
+class Selection;
+
+namespace XML {
+class Node;
+}
+
+namespace UI {
+namespace Toolbar {
+class ConnectorToolbar
+ : public Toolbar
+ , private XML::NodeObserver
+{
+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{false};
+
+ Inkscape::XML::Node *_repr{nullptr};
+
+ 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);
+
+ void notifyAttributeChanged(Inkscape::XML::Node &node, GQuark name,
+ Inkscape::Util::ptr_shared old_value,
+ Inkscape::Util::ptr_shared new_value) final;
+
+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..83a18c3
--- /dev/null
+++ b/src/ui/toolbar/dropper-toolbar.cpp
@@ -0,0 +1,117 @@
+// 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 "desktop.h"
+
+#include "ui/widget/canvas.h" // Grab focus
+
+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);
+ _desktop->canvas->grab_focus();
+}
+
+void DropperToolbar::on_set_alpha_button_toggled()
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool( "/tools/dropper/setalpha", _set_alpha_button->get_active( ) );
+ _desktop->canvas->grab_focus();
+}
+
+/*
+ * 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..33487f4
--- /dev/null
+++ b/src/ui/toolbar/eraser-toolbar.cpp
@@ -0,0 +1,352 @@
+// 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/canvas.h" // Focus widget
+#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)
+{
+ auto prefs = Inkscape::Preferences::get();
+ gint const eraser_mode = prefs->getInt("/tools/eraser/mode", _modeAsInt(Tools::DEFAULT_ERASER_MODE));
+ // 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);
+
+ 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(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
+ 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(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(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(desktop->canvas);
+ _tremor_adj->signal_value_changed().connect(sigc::mem_fun(*this, &EraserToolbar::tremor_value_changed));
+
+ // TODO: Allow slider appearance
+ 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(desktop->canvas);
+ _mass_adj->signal_value_changed().connect(sigc::mem_fun(*this, &EraserToolbar::mass_value_changed));
+ // TODO: Allow slider appearance
+ 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());
+}
+
+/**
+ * @brief Computes the integer value representing eraser mode
+ * @param mode A mode of the eraser tool, from the enum EraserToolMode
+ * @return the integer to be stored in the prefs as the selected mode
+ */
+guint EraserToolbar::_modeAsInt(Inkscape::UI::Tools::EraserToolMode mode)
+{
+ using namespace Inkscape::UI::Tools;
+
+ if (mode == EraserToolMode::DELETE) {
+ return 0;
+ } else if (mode == EraserToolMode::CUT) {
+ return 1;
+ } else if (mode == EraserToolMode::CLIP) {
+ return 2;
+ } else {
+ return _modeAsInt(DEFAULT_ERASER_MODE);
+ }
+}
+
+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)
+{
+ using namespace Inkscape::UI::Tools;
+
+ _split->set_visible(eraser_mode == _modeAsInt(EraserToolMode::CUT));
+
+ const gboolean visibility = (eraser_mode != _modeAsInt(EraserToolMode::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..d03590f
--- /dev/null
+++ b/src/ui/toolbar/eraser-toolbar.h
@@ -0,0 +1,100 @@
+// 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 Tools {
+enum class EraserToolMode;
+} // namespace Tools
+
+namespace Widget {
+class SpinButtonToolItem;
+} // namespace Widget
+
+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;
+
+ static guint _modeAsInt(Inkscape::UI::Tools::EraserToolMode mode);
+ 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..1280047
--- /dev/null
+++ b/src/ui/toolbar/gradient-toolbar.cpp
@@ -0,0 +1,1189 @@
+// 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 "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/canvas.h"
+#include "ui/widget/color-preview.h"
+#include "ui/widget/combo-tool-item.h"
+#include "ui/widget/gradient-image.h"
+#include "ui/widget/spin-button-tool-item.h"
+#include "ui/widget/gradient-vector-selector.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())
+ //&& is<SPGradient>(isFill ? style->getFillPaintServer() : style->getStrokePaintServer()) ) {
+ && (isFill ? is<SPGradient>(style->getFillPaintServer()) : is<SPGradient>(style->getStrokePaintServer())) ) {
+ SPPaintServer *server = isFill ? style->getFillPaintServer() : style->getStrokePaintServer();
+ if ( is<SPLinearGradient>(server) ) {
+ sp_item_set_gradient(item, gr, SP_GRADIENT_TYPE_LINEAR, mode);
+ } else if ( is<SPRadialGradient>(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) {
+ auto grad = cast<SPGradient>(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) {
+ auto gradient = cast<SPGradient>(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 list of gradients of the selected desktop item
+ * These are the gradients containing the repeat settings, not the underlying "getVector" href linked gradient.
+ */
+void gr_get_dt_selected_gradient(Inkscape::Selection *selection, std::vector<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 ( is<SPGradient>(server) ) {
+ gradient = cast<SPGradient>(server);
+ }
+ if (gradient && gradient->isSolid()) {
+ gradient = nullptr;
+ }
+
+ if (gradient) {
+ gr_selected.push_back(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 ( is<SPGradient>(server) ) {
+ auto gradient = cast<SPGradient>(server)->getVector();
+ SPGradientSpread spread = cast<SPGradient>(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 ( is<SPGradient>(server) ) {
+ auto gradient = cast<SPGradient>(server)->getVector();
+ SPGradientSpread spread = cast<SPGradient>(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 */
+ _offset_adj_changed = false;
+ {
+ 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(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(), _("Assign gradient to object"), INKSCAPE_ICON("color-gradient"));
+ }
+
+ 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();
+ std::vector<SPGradient *> gradientList;
+ gr_get_dt_selected_gradient(selection, gradientList);
+
+ if (!gradientList.empty()) {
+ for (auto item: gradientList) {
+ SPGradientSpread spread = (SPGradientSpread) active;
+ item->setSpread(spread);
+ item->updateRepr();
+ }
+ DocumentUndo::done(_desktop->getDocument(), _("Set gradient repeat"), INKSCAPE_ICON("color-gradient"));
+ }
+
+ 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;
+ }
+
+ SPStop *prev = nullptr;
+ prev = stop->getPrevStop();
+ if (prev != nullptr ) {
+ _offset_adj->set_lower(prev->offset);
+ } else {
+ _offset_adj->set_lower(0);
+ }
+
+ SPStop *next = nullptr;
+ next = stop->getNextStop();
+ if (next != nullptr ) {
+ _offset_adj->set_upper(next->offset);
+ } else {
+ _offset_adj->set_upper(1.0);
+ }
+
+ _offset_adj->set_value(stop->offset);
+ _offset_item->set_sensitive(true);
+}
+
+/**
+ * \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();
+ _offset_adj_changed = true; // checked to stop changing the selected stop after the update of the offset
+ stop->getRepr()->setAttributeCssDouble("offset", stop->offset);
+
+ DocumentUndo::maybeDone(stop->document, "gradient:stop:offset", _("Change gradient stop offset"), INKSCAPE_ICON("color-gradient"));
+ }
+
+ 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();
+ if (auto rc = SP_GRADIENT_CONTEXT(ev)) {
+ rc->add_stops_between_selected_stops();
+ }
+}
+
+/**
+ * \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->connect_gradient_stop_selected([=](void* sender, SPStop* stop){
+ drag_selection_changed(nullptr);
+ });
+
+ // 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;
+
+ if (!_desktop) {
+ return;
+ }
+
+ if (_offset_adj_changed) { // stops change of selection when offset update event is triggered
+ _offset_adj_changed = false;
+ return;
+ }
+
+ blocked = true;
+
+ 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 );
+ _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);
+ _offset_item->set_sensitive(!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 (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;
+ selected = 0;
+ return selected;
+ }
+
+ if (!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 (is<SPStop>(&ochild)) {
+
+ auto stop = cast<SPStop>(&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 (is<SPStop>(&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..58f5cff
--- /dev/null
+++ b/src/ui/toolbar/gradient-toolbar.h
@@ -0,0 +1,106 @@
+// 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;
+class SPStop;
+class SPObject;
+
+namespace Gtk {
+class ComboBoxText;
+class RadioToolButton;
+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;
+ bool _offset_adj_changed;
+
+ 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..06327ff
--- /dev/null
+++ b/src/ui/toolbar/lpe-toolbar.cpp
@@ -0,0 +1,417 @@
+// 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 "ui/dialog/dialog-container.h"
+#include "ui/icon-names.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);
+
+ LpeTool *lc = dynamic_cast<LpeTool *>(_desktop->event_context);
+ if (lc) {
+ lpetool_context_reset_limiting_bbox(lc);
+ }
+}
+
+void
+LPEToolbar::toggle_set_bbox()
+{
+ auto selection = _desktop->getSelection();
+
+ 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()
+{
+ LpeTool *lc = dynamic_cast<LpeTool *>(_desktop->event_context);
+ if (!lc) {
+ return;
+ }
+
+ bool show = _measuring_item->get_active();
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/tools/lpetool/show_measuring_info", show);
+
+ 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 (dynamic_cast<LpeTool *>(_desktop->event_context)) {
+ _desktop->getContainer()->new_dialog("LivePathEffect");
+ } else {
+ std::cerr << "LPEToolbar::open_lpe_dialog: LPEToolbar active but current tool is not LPE tool!" << std::endl;
+ }
+ _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 && is<SPLPEItem>(item) && lpetool_item_has_construction(lc, item)) {
+
+ auto lpeitem = cast<SPLPEItem>(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/marker-toolbar.cpp b/src/ui/toolbar/marker-toolbar.cpp
new file mode 100644
index 0000000..d60f2d6
--- /dev/null
+++ b/src/ui/toolbar/marker-toolbar.cpp
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Marker edit mode toolbar - onCanvas marker editing of marker orientation, position, scale
+ *//*
+ * Authors:
+ * see git history
+ * Rachana Podaralla <rpodaralla3@gatech.edu>
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+#include "marker-toolbar.h"
+#include "document-undo.h"
+#include "preferences.h"
+#include "desktop.h"
+#include "ui/widget/canvas.h"
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+
+MarkerToolbar::MarkerToolbar(SPDesktop *desktop)
+ : Toolbar(desktop)
+{
+}
+
+GtkWidget* MarkerToolbar::create(SPDesktop *desktop)
+{
+ auto toolbar = Gtk::manage(new MarkerToolbar(desktop));
+ return GTK_WIDGET(toolbar->gobj());
+}
+
+}}} \ No newline at end of file
diff --git a/src/ui/toolbar/marker-toolbar.h b/src/ui/toolbar/marker-toolbar.h
new file mode 100644
index 0000000..f5f4d64
--- /dev/null
+++ b/src/ui/toolbar/marker-toolbar.h
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Marker edit mode toolbar - onCanvas marker editing of marker orientation, position, scale
+ *//*
+ * Authors:
+ * see git history
+ * Rachana Podaralla <rpodaralla3@gatech.edu>
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_MARKER_TOOLBAR_H
+#define SEEN_MARKER_TOOLBAR_H
+
+#include "toolbar.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+
+class MarkerToolbar : public Toolbar {
+protected:
+ MarkerToolbar(SPDesktop *desktop);
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+};
+
+}}}
+#endif \ No newline at end of file
diff --git a/src/ui/toolbar/measure-toolbar.cpp b/src/ui/toolbar/measure-toolbar.cpp
new file mode 100644
index 0000000..92ca4c5
--- /dev/null
+++ b/src/ui/toolbar/measure-toolbar.cpp
@@ -0,0 +1,448 @@
+// 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 "message-stack.h"
+#include "object/sp-namedview.h"
+
+#include "ui/icon-names.h"
+#include "ui/tools/measure-tool.h"
+#include "ui/widget/canvas.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;
+
+static MeasureTool *get_measure_tool(SPDesktop *desktop)
+{
+ if (desktop) {
+ return dynamic_cast<MeasureTool *>(desktop->event_context);
+ }
+ return nullptr;
+}
+
+
+
+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();
+ auto unit = desktop->getNamedView()->getDisplayUnit();
+ _tracker->setActiveUnitByAbbr(prefs->getString("/tools/measure/unit", unit->abbr).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(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(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(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(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(_desktop);
+ 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(_desktop);
+ 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(_desktop);
+ 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(_desktop);
+ 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(_desktop);
+ 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(_desktop);
+ 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(_desktop);
+ 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(_desktop);
+ 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(_desktop);
+ 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(_desktop);
+ if (mt) {
+ mt->showCanvasItems();
+ }
+}
+
+void
+MeasureToolbar::reverse_knots()
+{
+ MeasureTool *mt = get_measure_tool(_desktop);
+ if (mt) {
+ mt->reverseKnots();
+ }
+}
+
+void
+MeasureToolbar::to_phantom()
+{
+ MeasureTool *mt = get_measure_tool(_desktop);
+ if (mt) {
+ mt->toPhantom();
+ }
+}
+
+void
+MeasureToolbar::to_guides()
+{
+ MeasureTool *mt = get_measure_tool(_desktop);
+ if (mt) {
+ mt->toGuides();
+ }
+}
+
+void
+MeasureToolbar::to_item()
+{
+ MeasureTool *mt = get_measure_tool(_desktop);
+ if (mt) {
+ mt->toItem();
+ }
+}
+
+void
+MeasureToolbar::to_mark_dimension()
+{
+ MeasureTool *mt = get_measure_tool(_desktop);
+ 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..1bd1e54
--- /dev/null
+++ b/src/ui/toolbar/mesh-toolbar.cpp
@@ -0,0 +1,613 @@
+// 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 "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/canvas.h"
+#include "ui/widget/color-preview.h"
+#include "ui/widget/combo-tool-item.h"
+#include "ui/widget/gradient-image.h"
+#include "ui/widget/spin-button-tool-item.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();
+ auto mesh = cast<SPMeshGradient>(server);
+ if (mesh) {
+ ms_selected.push_back(mesh);
+ }
+ }
+
+ if (edit_stroke && style->stroke.isPaintserver()) {
+ SPPaintServer *server = item->style->getStrokePaintServer();
+ auto mesh = 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;
+}
+
+
+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(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(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->get_drag();
+ 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->get_drag();
+ 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(), _("Set mesh type"), INKSCAPE_ICON("mesh-gradient"));
+ }
+}
+
+void
+MeshToolbar::toggle_sides()
+{
+ if (MeshTool *mt = get_mesh_tool()) {
+ mt->corner_operation(MG_CORNER_SIDE_TOGGLE);
+ }
+}
+
+void
+MeshToolbar::make_elliptical()
+{
+ if (MeshTool *mt = get_mesh_tool()) {
+ mt->corner_operation(MG_CORNER_SIDE_ARC);
+ }
+}
+
+void
+MeshToolbar::pick_colors()
+{
+ if (MeshTool *mt = get_mesh_tool()) {
+ mt->corner_operation(MG_CORNER_COLOR_PICK);
+ }
+}
+
+void
+MeshToolbar::fit_mesh()
+{
+ if (MeshTool *mt = get_mesh_tool()) {
+ mt->fit_mesh_in_bbox();
+ }
+}
+
+
+}
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 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..b0fd3e9
--- /dev/null
+++ b/src/ui/toolbar/node-toolbar.cpp
@@ -0,0 +1,691 @@
+// 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 "object/sp-namedview.h"
+
+#include "page-manager.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/canvas.h"
+#include "ui/widget/combo-tool-item.h"
+#include "ui/widget/spinbutton.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(_("Straighten 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(_("Add curve handles"));
+ 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 lpe_corners_item = Gtk::manage(new Gtk::ToolButton(_("_Add corners")));
+ lpe_corners_item->set_tooltip_text(_("Add corners live path effect"));
+ lpe_corners_item->set_icon_name(INKSCAPE_ICON("corners"));
+ // Must use C API until GTK4.
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(lpe_corners_item->gobj()), "app.object-add-corners-lpe");
+ add(*lpe_corners_item);
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ {
+ auto object_to_path_item = Gtk::manage(new Gtk::ToolButton(_("_Object to Path")));
+ object_to_path_item->set_tooltip_text(_("Convert selected object to path"));
+ object_to_path_item->set_icon_name(INKSCAPE_ICON("object-to-path"));
+ // Must use C API until GTK4.
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(object_to_path_item->gobj()), "app.object-to-path");
+ add(*object_to_path_item);
+ }
+
+ {
+ auto stroke_to_path_item = Gtk::manage(new Gtk::ToolButton(_("_Stroke to Path")));
+ stroke_to_path_item->set_tooltip_text(_("Convert selected object's stroke to paths"));
+ stroke_to_path_item->set_icon_name(INKSCAPE_ICON("stroke-to-path"));
+ // Must use C API until GTK4.
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(stroke_to_path_item->gobj()), "app.object-stroke-to-path");
+ 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->get_spin_button()->addUnitTracker(_tracker.get());
+ _nodes_x_item->set_focus_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->get_spin_button()->addUnitTracker(_tracker.get());
+ _nodes_y_item->set_focus_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 = Gtk::manage(new Gtk::ToolButton(N_("Next path effect parameter")));
+ _nodes_lpeedit_item->set_tooltip_text(N_("Show next editable path effect parameter"));
+ _nodes_lpeedit_item->set_icon_name(INKSCAPE_ICON("path-effect-parameter-next"));
+ // Must use C API until GTK4.
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(_nodes_lpeedit_item->gobj()), "win.path-effect-parameter-next");
+ 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];
+
+ // Adjust the coordinate to the current page, if needed
+ auto &pm = _desktop->getDocument()->getPageManager();
+ if (prefs->getBool("/options/origincorrection/page", true)) {
+ auto page = pm.getSelectedPageRect();
+ oldval -= page.corner(0)[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 && is<SPLPEItem>(item)) {
+ if (cast_unsafe<SPLPEItem>(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->connect_control_point_selected([=](void* sender, Inkscape::UI::ControlPointSelection* selection) {
+ coord_changed(selection);
+ });
+
+ 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(Inkscape::UI::ControlPointSelection* selected_nodes) // 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);
+
+ if (!selected_nodes || 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 = selected_nodes->pointwiseBounds()->midpoint();
+
+ // Adjust shown coordinate according to the selected page
+ auto prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/options/origincorrection/page", true)) {
+ auto &pm = _desktop->getDocument()->getPageManager();
+ mid *= pm.getSelectedPageAffine().inverse();
+ }
+
+ 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..9723922
--- /dev/null
+++ b/src/ui/toolbar/node-toolbar.h
@@ -0,0 +1,116 @@
+// 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;
+class ControlPointSelection;
+
+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(Inkscape::UI::ControlPointSelection* selected_nodes);
+ 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/page-toolbar.cpp b/src/ui/toolbar/page-toolbar.cpp
new file mode 100644
index 0000000..a228232
--- /dev/null
+++ b/src/ui/toolbar/page-toolbar.cpp
@@ -0,0 +1,530 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Page aux toolbar: Temp until we convert all toolbars to ui files with Gio::Actions.
+ */
+/* Authors:
+ * Martin Owens <doctormo@geek-2.com>
+
+ * Copyright (C) 2021 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "page-toolbar.h"
+
+#include <glibmm/i18n.h>
+#include <gtkmm.h>
+#include <regex>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "extension/db.h"
+#include "extension/template.h"
+#include "io/resource.h"
+#include "object/sp-namedview.h"
+#include "object/sp-page.h"
+#include "ui/builder-utils.h"
+#include "ui/icon-names.h"
+#include "ui/themes.h"
+#include "ui/tools/pages-tool.h"
+#include "util/paper.h"
+#include "util/units.h"
+
+using Inkscape::IO::Resource::UIS;
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+
+class SearchCols : public Gtk::TreeModel::ColumnRecord
+{
+public:
+ // These types must match those for the model in the ui file
+ SearchCols()
+ {
+ add(name);
+ add(label);
+ add(key);
+ }
+ Gtk::TreeModelColumn<Glib::ustring> name; // translated name
+ Gtk::TreeModelColumn<Glib::ustring> label; // translated label
+ Gtk::TreeModelColumn<Glib::ustring> key;
+};
+
+PageToolbar::PageToolbar(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &builder, SPDesktop *desktop)
+ : Gtk::Toolbar(cobject)
+ , _desktop(desktop)
+ , combo_page_sizes(nullptr)
+ , text_page_label(nullptr)
+{
+ builder->get_widget("page_sizes", combo_page_sizes);
+ builder->get_widget("page_margins", text_page_margins);
+ builder->get_widget("page_bleeds", text_page_bleeds);
+ builder->get_widget("page_label", text_page_label);
+ builder->get_widget("page_pos", label_page_pos);
+ builder->get_widget("page_backward", btn_page_backward);
+ builder->get_widget("page_foreward", btn_page_foreward);
+ builder->get_widget("page_delete", btn_page_delete);
+ builder->get_widget("page_move_objects", btn_move_toggle);
+ builder->get_widget("sep1", sep1);
+
+ sizes_list = Glib::RefPtr<Gtk::ListStore>::cast_dynamic(
+ builder->get_object("page_sizes_list")
+ );
+ sizes_search = Glib::RefPtr<Gtk::ListStore>::cast_dynamic(
+ builder->get_object("page_sizes_search")
+ );
+ sizes_searcher = Glib::RefPtr<Gtk::EntryCompletion>::cast_dynamic(
+ builder->get_object("sizes_searcher")
+ );
+
+ builder->get_widget("margin_popover", margin_popover);
+ builder->get_widget_derived("margin_top", margin_top);
+ builder->get_widget_derived("margin_right", margin_right);
+ builder->get_widget_derived("margin_bottom", margin_bottom);
+ builder->get_widget_derived("margin_left", margin_left);
+
+ if (text_page_label) {
+ text_page_label->signal_changed().connect(sigc::mem_fun(*this, &PageToolbar::labelEdited));
+ }
+ if (sizes_searcher) {
+ sizes_searcher->signal_match_selected().connect([=](const Gtk::TreeModel::iterator &iter) {
+ SearchCols cols;
+ Gtk::TreeModel::Row row = *(iter);
+ Glib::ustring preset_key = row[cols.key];
+ sizeChoose(preset_key);
+ return false;
+ }, false);
+ }
+ text_page_bleeds->signal_activate().connect(sigc::mem_fun(*this, &PageToolbar::bleedsEdited));
+ text_page_margins->signal_activate().connect(sigc::mem_fun(*this, &PageToolbar::marginsEdited));
+ text_page_margins->signal_icon_press().connect([=](Gtk::EntryIconPosition, const GdkEventButton*){
+ if (auto page = _document->getPageManager().getSelected()) {
+ auto margin = page->getMargin();
+ auto unit = _document->getDisplayUnit()->abbr;
+ margin_top->set_value(margin.top().toValue(unit));
+ margin_right->set_value(margin.right().toValue(unit));
+ margin_bottom->set_value(margin.bottom().toValue(unit));
+ margin_left->set_value(margin.left().toValue(unit));
+ text_page_bleeds->set_text(page->getBleedLabel());
+ }
+ margin_popover->show();
+ });
+ margin_top->signal_value_changed().connect(sigc::mem_fun(*this, &PageToolbar::marginTopEdited));
+ margin_right->signal_value_changed().connect(sigc::mem_fun(*this, &PageToolbar::marginRightEdited));
+ margin_bottom->signal_value_changed().connect(sigc::mem_fun(*this, &PageToolbar::marginBottomEdited));
+ margin_left->signal_value_changed().connect(sigc::mem_fun(*this, &PageToolbar::marginLeftEdited));
+
+ if (combo_page_sizes) {
+ combo_page_sizes->set_id_column(2);
+ combo_page_sizes->signal_changed().connect([=] {
+ std::string preset_key = combo_page_sizes->get_active_id();
+ sizeChoose(preset_key);
+ });
+ entry_page_sizes = dynamic_cast<Gtk::Entry *>(combo_page_sizes->get_child());
+ if (entry_page_sizes) {
+ entry_page_sizes->set_placeholder_text(_("ex.: 100x100cm"));
+ entry_page_sizes->set_tooltip_text(_("Type in width & height of a page. (ex.: 15x10cm, 10in x 100mm)\n"
+ "or choose preset from dropdown."));
+ entry_page_sizes->get_style_context()->add_class("symbolic");
+ entry_page_sizes->signal_activate().connect(sigc::mem_fun(*this, &PageToolbar::sizeChanged));
+ entry_page_sizes->signal_icon_press().connect([=](Gtk::EntryIconPosition, const GdkEventButton*){
+ _document->getPageManager().changeOrientation();
+ DocumentUndo::maybeDone(_document, "page-resize", _("Resize Page"), INKSCAPE_ICON("tool-pages"));
+ setSizeText();
+ });
+ entry_page_sizes->signal_focus_in_event().connect([=](GdkEventFocus *) {
+ setSizeText(nullptr, false); // Show just raw dimensions when user starts editing
+ return false;
+ });
+ entry_page_sizes->signal_focus_out_event().connect([=](GdkEventFocus *) {
+ if (_document)
+ setSizeText(nullptr, true);
+ return false;
+ });
+ populate_sizes();
+ }
+ }
+
+ // Watch for when the tool changes
+ _ec_connection = _desktop->connectEventContextChanged(sigc::mem_fun(*this, &PageToolbar::toolChanged));
+ _doc_connection = _desktop->connectDocumentReplaced([=](SPDesktop *desktop, SPDocument *doc) {
+ if (doc) {
+ toolChanged(desktop, desktop->getEventContext());
+ }
+ });
+
+ // Constructed by a builder, so we're going to protect the widget from destruction.
+ this->reference();
+ was_referenced = true;
+}
+
+/**
+ * Take all selectable page sizes and add to search and dropdowns
+ */
+void PageToolbar::populate_sizes()
+{
+ SearchCols cols;
+
+ Inkscape::Extension::DB::TemplateList extensions;
+ Inkscape::Extension::db.get_template_list(extensions);
+
+ for (auto tmod : extensions) {
+ if (!tmod->can_resize())
+ continue;
+ for (auto preset : tmod->get_presets()) {
+ auto label = preset->get_label();
+ if (!label.empty()) label = _(label.c_str());
+
+ if (preset->is_visible(Inkscape::Extension::TEMPLATE_SIZE_LIST)) {
+ // Goes into drop down
+ Gtk::TreeModel::Row row = *(sizes_list->append());
+ row[cols.name] = _(preset->get_name().c_str());
+ row[cols.label] = " <small><span fgalpha=\"50%\">" + label + "</span></small>";
+ row[cols.key] = preset->get_key();
+ }
+ if (preset->is_visible(Inkscape::Extension::TEMPLATE_SIZE_SEARCH)) {
+ // Goes into text search
+ Gtk::TreeModel::Row row = *(sizes_search->append());
+ row[cols.name] = _(preset->get_name().c_str());
+ row[cols.label] = label;
+ row[cols.key] = preset->get_key();
+ }
+ }
+ }
+}
+
+void PageToolbar::on_parent_changed(Gtk::Widget *)
+{
+ if (was_referenced) {
+ // Undo the gtkbuilder protection now that we have a parent
+ this->unreference();
+ was_referenced = false;
+ }
+}
+
+PageToolbar::~PageToolbar()
+{
+ _ec_connection.disconnect();
+ _doc_connection.disconnect();
+ toolChanged(nullptr, nullptr);
+}
+
+void PageToolbar::toolChanged(SPDesktop *desktop, Inkscape::UI::Tools::ToolBase *ec)
+{
+ // Disconnect previous page changed signal
+ _page_selected.disconnect();
+ _pages_changed.disconnect();
+ _page_modified.disconnect();
+ _document = nullptr;
+
+ if (dynamic_cast<Inkscape::UI::Tools::PagesTool *>(ec)) {
+ // Save the document and page_manager for future use.
+ if ((_document = desktop->getDocument())) {
+ auto &page_manager = _document->getPageManager();
+ // Connect the page changed signal and indicate changed
+ _pages_changed = page_manager.connectPagesChanged(sigc::mem_fun(*this, &PageToolbar::pagesChanged));
+ _page_selected = page_manager.connectPageSelected(sigc::mem_fun(*this, &PageToolbar::selectionChanged));
+ // Update everything now.
+ pagesChanged();
+ }
+ }
+}
+
+void PageToolbar::labelEdited()
+{
+ auto text = text_page_label->get_text();
+ if (auto page = _document->getPageManager().getSelected()) {
+ page->setLabel(text.empty() ? nullptr : text.c_str());
+ DocumentUndo::maybeDone(_document, "page-relabel", _("Relabel Page"), INKSCAPE_ICON("tool-pages"));
+ }
+}
+
+void PageToolbar::bleedsEdited()
+{
+ auto text = text_page_bleeds->get_text();
+
+ // And modifiction to the bleed causes pages to be enabled
+ auto &pm = _document->getPageManager();
+ pm.enablePages();
+
+ if (auto page = pm.getSelected()) {
+ page->setBleed(text);
+ DocumentUndo::maybeDone(_document, "page-bleed", _("Edit page bleed"), INKSCAPE_ICON("tool-pages"));
+
+ auto bleed = page->getBleed();
+ text_page_bleeds->set_text(page->getBleedLabel());
+ }
+}
+
+void PageToolbar::marginsEdited()
+{
+ auto text = text_page_margins->get_text();
+
+ // And modifiction to the margin causes pages to be enabled
+ auto &pm = _document->getPageManager();
+ pm.enablePages();
+
+ if (auto page = pm.getSelected()) {
+ page->setMargin(text);
+ DocumentUndo::maybeDone(_document, "page-margin", _("Edit page margin"), INKSCAPE_ICON("tool-pages"));
+ setMarginText(page);
+ }
+}
+
+void PageToolbar::marginTopEdited()
+{
+ marginSideEdited(0, margin_top->get_text());
+}
+void PageToolbar::marginRightEdited()
+{
+ marginSideEdited(1, margin_right->get_text());
+}
+void PageToolbar::marginBottomEdited()
+{
+ marginSideEdited(2, margin_bottom->get_text());
+}
+void PageToolbar::marginLeftEdited()
+{
+ marginSideEdited(3, margin_left->get_text());
+}
+void PageToolbar::marginSideEdited(int side, const Glib::ustring &value)
+{
+ // And modifiction to the margin causes pages to be enabled
+ auto &pm = _document->getPageManager();
+ pm.enablePages();
+
+ if (auto page = pm.getSelected()) {
+ page->setMarginSide(side, value, false);
+ DocumentUndo::maybeDone(_document, "page-margin", _("Edit page margin"), INKSCAPE_ICON("tool-pages"));
+ setMarginText(page);
+ }
+}
+
+void PageToolbar::sizeChoose(const std::string &preset_key)
+{
+ if (auto preset = Extension::Template::get_any_preset(preset_key)) {
+ auto &pm = _document->getPageManager();
+ // The page orientation is a part of the toolbar widget, so we pass this
+ // as a specially named pref, the extension can then decide to use it or not.
+ auto p_rect = pm.getSelectedPageRect();
+ std::string orient = p_rect.width() > p_rect.height() ? "land" : "port";
+
+ auto page = pm.getSelected();
+ preset->resize_to_template(_document, page, {
+ {"orientation", orient},
+ });
+ if (page) {
+ page->setSizeLabel(preset->get_name());
+ }
+
+ setSizeText();
+ DocumentUndo::maybeDone(_document, "page-resize", _("Resize Page"), INKSCAPE_ICON("tool-pages"));
+ } else {
+ // Page not found, i.e., "Custom" was selected or user is typing in.
+ entry_page_sizes->grab_focus();
+ }
+}
+
+/**
+ * Convert the parsed sections of a text input into a desktop pixel value.
+ */
+double PageToolbar::_unit_to_size(std::string number, std::string unit_str, std::string backup)
+{
+ // We always support comma, even if not in that particular locale.
+ std::replace(number.begin(), number.end(), ',', '.');
+ double value = std::stod(number);
+
+ // Get the best unit, for example 50x40cm means cm for both
+ if (unit_str.empty() && !backup.empty())
+ unit_str = backup;
+ if (unit_str == "\"")
+ unit_str = "in";
+
+ // Output is always in px as it's the most useful.
+ auto px = Inkscape::Util::unit_table.getUnit("px");
+
+ // Convert from user entered unit to display unit
+ if (!unit_str.empty())
+ return Inkscape::Util::Quantity::convert(value, unit_str, px);
+
+ // Default unit is the document's display unit
+ auto unit = _document->getDisplayUnit();
+ return Inkscape::Util::Quantity::convert(value, unit, px);
+}
+
+/**
+ * A manually typed input size, parse out what we can understand from
+ * the text or ignore it if the text can't be parsed.
+ *
+ * Format: 50cm x 40mm
+ * 20',40"
+ * 30,4-40.2
+ */
+void PageToolbar::sizeChanged()
+{
+ // Parse the size out of the typed text if possible.
+ auto text = std::string(combo_page_sizes->get_active_text());
+ // This does not support negative values, because pages can not be negatively sized.
+ static std::string arg = "([0-9]+[\\.,]?[0-9]*|\\.[0-9]+) ?(px|mm|cm|in|\\\")?";
+ // We can't support × here since it's UTF8 and this doesn't match
+ static std::regex re_size("^ *" + arg + " *([ *Xx,\\-]) *" + arg + " *$");
+
+ std::smatch matches;
+ if (std::regex_match(text, matches, re_size)) {
+ // Convert the desktop px back into document units for 'resizePage'
+ double width = _unit_to_size(matches[1], matches[2], matches[5]);
+ double height = _unit_to_size(matches[4], matches[5], matches[2]);
+ if (width > 0 && height > 0) {
+ _document->getPageManager().resizePage(width, height);
+ }
+ }
+ setSizeText();
+}
+
+/**
+ * Sets the size of the current page into the entry page size.
+ */
+void PageToolbar::setSizeText(SPPage *page, bool display_only)
+{
+ SearchCols cols;
+
+ if (!page)
+ page = _document->getPageManager().getSelected();
+
+ auto label = _document->getPageManager().getSizeLabel(page);
+
+ // If this is a known size in our list, add the size paren to it.
+ for (auto iter : sizes_search->children()) {
+ auto row = *iter;
+ if (label == row[cols.name]) {
+ label = label + " (" + row[cols.label] + ")";
+ break;
+ }
+ }
+ entry_page_sizes->set_text(label);
+
+
+ // Orientation button
+ auto box = page ? page->getDesktopRect() : *_document->preferredBounds();
+ std::string icon = box.width() > box.height() ? "page-landscape" : "page-portrait";
+ if (box.width() == box.height()) {
+ entry_page_sizes->unset_icon(Gtk::ENTRY_ICON_SECONDARY);
+ } else {
+ entry_page_sizes->set_icon_from_icon_name(INKSCAPE_ICON(icon), Gtk::ENTRY_ICON_SECONDARY);
+ }
+
+ if (!display_only) {
+ // The user has started editing the combo box; we set up a convenient initial state.
+ // Select text if box is currently in focus.
+ if (entry_page_sizes->has_focus()) {
+ entry_page_sizes->select_region(0, -1);
+ }
+ }
+}
+
+void PageToolbar::setMarginText(SPPage *page)
+{
+ text_page_margins->set_text(page ? page->getMarginLabel() : "");
+ text_page_margins->set_sensitive(true);
+}
+
+void PageToolbar::pagesChanged()
+{
+ selectionChanged(_document->getPageManager().getSelected());
+}
+
+void PageToolbar::selectionChanged(SPPage *page)
+{
+ _page_modified.disconnect();
+ auto &page_manager = _document->getPageManager();
+ text_page_label->set_tooltip_text(_("Page label"));
+
+ setMarginText(page);
+
+ // Set label widget content with page label.
+ if (page) {
+ text_page_label->set_sensitive(true);
+ text_page_label->set_placeholder_text(page->getDefaultLabel());
+
+ if (auto label = page->label()) {
+ text_page_label->set_text(label);
+ } else {
+ text_page_label->set_text("");
+ }
+
+
+ // TRANSLATORS: "%1" is replaced with the page we are on, and "%2" is the total number of pages.
+ auto label = Glib::ustring::compose(_("%1/%2"), page->getPagePosition(), page_manager.getPageCount());
+ label_page_pos->set_label(label);
+
+ _page_modified = page->connectModified([=](SPObject *obj, unsigned int flags) {
+ if (auto page = cast<SPPage>(obj)) {
+ // Make sure we don't 'select' on removal of the page
+ if (flags & SP_OBJECT_MODIFIED_FLAG) {
+ selectionChanged(page);
+ }
+ }
+ });
+ } else {
+ text_page_label->set_text("");
+ text_page_label->set_sensitive(false);
+ text_page_label->set_placeholder_text(_("Single Page Document"));
+ label_page_pos->set_label(_("1/-"));
+
+ _page_modified = _document->connectModified([=](guint) {
+ selectionChanged(nullptr);
+ });
+ }
+ if (!page_manager.hasPrevPage() && !page_manager.hasNextPage() && !page) {
+ sep1->set_visible(false);
+ label_page_pos->get_parent()->set_visible(false);
+ btn_page_backward->set_visible(false);
+ btn_page_foreward->set_visible(false);
+ btn_page_delete->set_visible(false);
+ btn_move_toggle->set_sensitive(false);
+ } else {
+ // Set the forward and backward button sensitivities
+ sep1->set_visible(true);
+ label_page_pos->get_parent()->set_visible(true);
+ btn_page_backward->set_visible(true);
+ btn_page_foreward->set_visible(true);
+ btn_page_backward->set_sensitive(page_manager.hasPrevPage());
+ btn_page_foreward->set_sensitive(page_manager.hasNextPage());
+ btn_page_delete->set_visible(true);
+ btn_move_toggle->set_sensitive(true);
+ }
+ setSizeText(page);
+}
+
+GtkWidget *PageToolbar::create(SPDesktop *desktop)
+{
+ PageToolbar *toolbar = nullptr;
+ auto builder = Inkscape::UI::create_builder("toolbar-page.ui");
+ builder->get_widget_derived("page-toolbar", toolbar, desktop);
+
+ if (!toolbar) {
+ std::cerr << "InkscapeWindow: Failed to load page toolbar!" << std::endl;
+ return nullptr;
+ }
+ // This widget will be auto-freed by the builder unless you have called reference();
+ return GTK_WIDGET(toolbar->gobj());
+}
+
+
+} // namespace Toolbar
+} // 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/toolbar/page-toolbar.h b/src/ui/toolbar/page-toolbar.h
new file mode 100644
index 0000000..09ac6fe
--- /dev/null
+++ b/src/ui/toolbar/page-toolbar.h
@@ -0,0 +1,118 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_PAGE_TOOLBAR_H
+#define SEEN_PAGE_TOOLBAR_H
+
+/**
+ * @file
+ * Page toolbar
+ */
+/* Authors:
+ * Martin Owens <doctormo@geek-2.com>
+ *
+ * Copyright (C) 2021 Martin Owens
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm.h>
+#include <gtkmm/spinbutton.h>
+
+#include "toolbar.h"
+
+#include "ui/widget/spinbutton.h"
+
+class SPDesktop;
+class SPDocument;
+class SPPage;
+
+namespace Inkscape {
+class PaperSize;
+namespace UI {
+namespace Tools {
+class ToolBase;
+}
+namespace Toolbar {
+
+class PageToolbar : public Gtk::Toolbar
+{
+public:
+ PageToolbar(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &builder, SPDesktop *desktop);
+ ~PageToolbar() override;
+
+ static GtkWidget *create(SPDesktop *desktop);
+
+protected:
+ void labelEdited();
+ void bleedsEdited();
+ void marginsEdited();
+ void marginTopEdited();
+ void marginRightEdited();
+ void marginBottomEdited();
+ void marginLeftEdited();
+ void marginSideEdited(int side, const Glib::ustring &value);
+ void sizeChoose(const std::string &preset_key);
+ void sizeChanged();
+ void setSizeText(SPPage *page = nullptr, bool display_only = true);
+ void setMarginText(SPPage *page = nullptr);
+
+private:
+ SPDesktop *_desktop;
+ SPDocument *_document;
+
+ void toolChanged(SPDesktop *desktop, Inkscape::UI::Tools::ToolBase *ec);
+ void pagesChanged();
+ void selectionChanged(SPPage *page);
+ void on_parent_changed(Gtk::Widget *prev) override;
+ void populate_sizes();
+
+ sigc::connection _ec_connection;
+ sigc::connection _doc_connection;
+ sigc::connection _pages_changed;
+ sigc::connection _page_selected;
+ sigc::connection _page_modified;
+
+ bool was_referenced;
+ Gtk::ComboBoxText *combo_page_sizes;
+ Gtk::Entry *entry_page_sizes;
+ Gtk::Entry *text_page_margins;
+ Gtk::Entry *text_page_bleeds;
+ Gtk::Entry *text_page_label;
+ Gtk::Entry *text_page_width;
+ Gtk::Entry *text_page_height;
+ Gtk::Label *label_page_pos;
+ Gtk::ToolButton *btn_page_backward;
+ Gtk::ToolButton *btn_page_foreward;
+ Gtk::ToolButton *btn_page_delete;
+ Gtk::ToolButton *btn_move_toggle;
+ Gtk::SeparatorToolItem *sep1;
+
+ Glib::RefPtr<Gtk::ListStore> sizes_list;
+ Glib::RefPtr<Gtk::ListStore> sizes_search;
+ Glib::RefPtr<Gtk::EntryCompletion> sizes_searcher;
+
+ Gtk::Popover *margin_popover;
+
+ Inkscape::UI::Widget::MathSpinButton *margin_top;
+ Inkscape::UI::Widget::MathSpinButton *margin_right;
+ Inkscape::UI::Widget::MathSpinButton *margin_bottom;
+ Inkscape::UI::Widget::MathSpinButton *margin_left;
+
+ double _unit_to_size(std::string number, std::string unit_str, std::string backup);
+};
+
+} // namespace Toolbar
+} // namespace UI
+} // namespace Inkscape
+
+#endif /* !SEEN_PAGE_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/paintbucket-toolbar.cpp b/src/ui/toolbar/paintbucket-toolbar.cpp
new file mode 100644
index 0000000..41e4ed9
--- /dev/null
+++ b/src/ui/toolbar/paintbucket-toolbar.cpp
@@ -0,0 +1,220 @@
+// 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 "ui/icon-names.h"
+#include "ui/tools/flood-tool.h"
+#include "ui/widget/canvas.h"
+#include "ui/widget/combo-tool-item.h"
+#include "ui/widget/spinbutton.h"
+#include "ui/widget/spin-button-tool-item.h"
+#include "ui/widget/unit-tracker.h"
+
+using Inkscape::UI::Widget::UnitTracker;
+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.c_str());
+ 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(desktop->canvas);
+ _threshold_adj->signal_value_changed().connect(sigc::mem_fun(*this, &PaintbucketToolbar::threshold_changed));
+ 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->get_spin_button()->addUnitTracker(_tracker);
+ offset_item->set_focus_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..125453b
--- /dev/null
+++ b/src/ui/toolbar/pencil-toolbar.cpp
@@ -0,0 +1,691 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Pencil and pen toolbars
+ */
+/* 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 "pencil-toolbar.h"
+
+#include <glibmm/i18n.h>
+#include <gtkmm.h>
+
+#include "desktop.h"
+#include "display/curve.h"
+#include "live_effects/lpe-bendpath.h"
+#include "live_effects/lpe-bspline.h"
+#include "live_effects/lpe-patternalongpath.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 "object/sp-shape.h"
+#include "selection.h"
+#include "ui/icon-names.h"
+#include "ui/tools/freehand-base.h"
+#include "ui/tools/pen-tool.h"
+#include "ui/tools/pencil-tool.h"
+#include "ui/widget/canvas.h"
+#include "ui/widget/combo-tool-item.h"
+#include "ui/widget/label-tool-item.h"
+#include "ui/widget/spin-button-tool-item.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+PencilToolbar::PencilToolbar(SPDesktop *desktop,
+ bool pencil_mode)
+ : Toolbar(desktop),
+ _tool_is_pencil(pencil_mode)
+{
+ auto prefs = Inkscape::Preferences::get();
+
+ add_freehand_mode_toggle();
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ if (_tool_is_pencil) {
+ /* 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("/tools/freehand/pencil/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(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(desktop->canvas);
+ _maxpressure_adj->signal_value_changed().connect(
+ sigc::mem_fun(*this, &PencilToolbar::maxpressure_value_changed));
+ add(*_maxpressure);
+ }
+
+ /* powerstoke */
+ add_powerstroke_cap();
+
+ 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(desktop->canvas);
+ _tolerance_adj->signal_value_changed().connect(sigc::mem_fun(*this, &PencilToolbar::tolerance_value_changed));
+ 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();
+
+ show_all();
+
+ // Elements must be hidden after show_all() is called
+ guint freehandMode = prefs->getInt(( _tool_is_pencil ?
+ "/tools/freehand/pencil/freehand-mode" :
+ "/tools/freehand/pen/freehand-mode" ), 0);
+ if (freehandMode != 1 && freehandMode != 2) {
+ _flatten_spiro_bspline->set_visible(false);
+ }
+ if (_tool_is_pencil) {
+ use_pencil_pressure();
+ }
+}
+
+GtkWidget *
+PencilToolbar::create_pencil(SPDesktop *desktop)
+{
+ auto toolbar = new PencilToolbar(desktop, true);
+ return GTK_WIDGET(toolbar->gobj());
+}
+
+PencilToolbar::~PencilToolbar()
+{
+ if(_repr) {
+ 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());
+ }
+ }
+
+ // Recall, the PencilToolbar is also used as the PenToolbar with minor changes.
+ auto *pt = dynamic_cast<Inkscape::UI::Tools::PenTool *>(_desktop->event_context);
+ if (pt) {
+ pt->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 _tool_is_pencil ? "/tools/freehand/pencil" : "/tools/freehand/pen";
+}
+
+void
+PencilToolbar::add_freehand_mode_toggle()
+{
+ 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(_("Flatten Spiro or BSpline LPE")));
+ _flatten_spiro_bspline->set_tooltip_text(_("Flatten Spiro or BSpline LPE"));
+ _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()
+{
+ assert(_tool_is_pencil);
+ // 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()
+{
+ assert(_tool_is_pencil);
+ // 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::shapewidth_value_changed()
+{
+ // quit if run by the attr_changed listener
+ if (_freeze) {
+ return;
+ }
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Inkscape::Selection *selection = _desktop->getSelection();
+ SPItem *item = selection->singleItem();
+ SPLPEItem *lpeitem = nullptr;
+ if (item) {
+ lpeitem = cast<SPLPEItem>(item);
+ }
+ using namespace Inkscape::LivePathEffect;
+ double width = _shapescale_adj->get_value();
+ switch (_shape_item->get_active()) {
+ case Inkscape::UI::Tools::TRIANGLE_IN:
+ case Inkscape::UI::Tools::TRIANGLE_OUT:
+ prefs->setDouble("/live_effects/powerstroke/width", width);
+ if (lpeitem) {
+ LPEPowerStroke *effect = dynamic_cast<LPEPowerStroke *>(lpeitem->getFirstPathEffectOfType(POWERSTROKE));
+ if (effect) {
+ std::vector<Geom::Point> points = effect->offset_points.data();
+ if (points.size() == 1) {
+ points[0][Geom::Y] = width;
+ effect->offset_points.param_set_and_write_new_value(points);
+ }
+ }
+ }
+ break;
+ case Inkscape::UI::Tools::ELLIPSE:
+ case Inkscape::UI::Tools::CLIPBOARD:
+ // The scale of the clipboard isn't known, so getting it to the right size isn't possible.
+ prefs->setDouble("/live_effects/skeletal/width", width);
+ if (lpeitem) {
+ LPEPatternAlongPath *effect =
+ dynamic_cast<LPEPatternAlongPath *>(lpeitem->getFirstPathEffectOfType(PATTERN_ALONG_PATH));
+ if (effect) {
+ effect->prop_scale.param_set_value(width);
+ sp_lpe_item_update_patheffect(lpeitem, false, true);
+ }
+ }
+ break;
+ case Inkscape::UI::Tools::BEND_CLIPBOARD:
+ prefs->setDouble("/live_effects/bend_path/width", width);
+ if (lpeitem) {
+ LPEBendPath *effect = dynamic_cast<LPEBendPath *>(lpeitem->getFirstPathEffectOfType(BEND_PATH));
+ if (effect) {
+ effect->prop_scale.param_set_value(width);
+ sp_lpe_item_update_patheffect(lpeitem, false, true);
+ }
+ }
+ break;
+ case Inkscape::UI::Tools::NONE:
+ case Inkscape::UI::Tools::LAST_APPLIED:
+ default:
+ break;
+ }
+}
+
+void
+PencilToolbar::use_pencil_pressure() {
+ assert(_tool_is_pencil);
+ bool pressure = _pressure_item->get_active();
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool("/tools/freehand/pencil/pressure", pressure);
+ if (pressure) {
+ _minpressure->set_visible(true);
+ _maxpressure->set_visible(true);
+ _cap_item->set_visible(true);
+ _shape_item->set_visible(false);
+ _shapescale->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);
+ _shapescale->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()
+{
+ /*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);
+
+ /* power width setting */
+ {
+ _shapescale_adj = Gtk::Adjustment::create(2.0, 0.0, 1000.0, 0.5, 1.0);
+ _shapescale =
+ Gtk::manage(new UI::Widget::SpinButtonToolItem("pencil-maxpressure", _("Scale:"), _shapescale_adj, 1, 2));
+ _shapescale->set_tooltip_text(_("Scale of the width of the power stroke shape."));
+ _shapescale->set_focus_widget(_desktop->canvas);
+ _shapescale_adj->signal_value_changed().connect(sigc::mem_fun(*this, &PencilToolbar::shapewidth_value_changed));
+ update_width_value(shape);
+ add(*_shapescale);
+ }
+}
+
+void
+PencilToolbar::change_shape(int shape) {
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setInt(freehand_tool_name() + "/shape", shape);
+ update_width_value(shape);
+}
+
+void
+PencilToolbar::update_width_value(int shape) {
+ /* Update shape width with correct width */
+ auto prefs = Inkscape::Preferences::get();
+ double width = 1.0;
+ _shapescale->set_sensitive(true);
+ double powerstrokedefsize = 10 / (0.265 * _desktop->getDocument()->getDocumentScale()[0] * 2.0);
+ switch (shape) {
+ case Inkscape::UI::Tools::TRIANGLE_IN:
+ case Inkscape::UI::Tools::TRIANGLE_OUT:
+ width = prefs->getDouble("/live_effects/powerstroke/width", powerstrokedefsize);
+ break;
+ case Inkscape::UI::Tools::ELLIPSE:
+ case Inkscape::UI::Tools::CLIPBOARD:
+ width = prefs->getDouble("/live_effects/skeletal/width", 1.0);
+ break;
+ case Inkscape::UI::Tools::BEND_CLIPBOARD:
+ width = prefs->getDouble("/live_effects/bend_path/width", 1.0);
+ break;
+ case Inkscape::UI::Tools::NONE: // Apply width from style?
+ case Inkscape::UI::Tools::LAST_APPLIED:
+ default:
+ _shapescale->set_sensitive(false);
+ break;
+ }
+ _shapescale_adj->set_value(width);
+}
+
+void PencilToolbar::add_powerstroke_cap()
+{
+ /* 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"), _("Line endings when drawing with pressure-sensitive PowerPencil"), "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 = 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)) {
+ auto shape = cast<SPShape>(lpeitem);
+ if(shape){
+ auto c = *shape->curveForEdit();
+ lpe->doEffect(&c);
+ lpeitem->setCurrentPathEffect(*i);
+ if (lpelist.size() > 1){
+ lpeitem->removeCurrentPathEffect(true);
+ shape->setCurveBeforeLPE(std::move(c));
+ } else {
+ lpeitem->removeCurrentPathEffect(false);
+ shape->setCurve(std::move(c));
+ }
+ 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 = 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))
+ {
+ auto shape = cast<SPShape>(lpeitem);
+ if(shape){
+ auto c = *shape->curveForEdit();
+ lpe->doEffect(&c);
+ lpeitem->setCurrentPathEffect(*i);
+ if (lpelist.size() > 1){
+ lpeitem->removeCurrentPathEffect(true);
+ shape->setCurveBeforeLPE(std::move(c));
+ } else {
+ lpeitem->removeCurrentPathEffect(false);
+ shape->setCurve(std::move(c));
+ }
+ 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()
+{
+ assert(_tool_is_pencil);
+ // 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){
+ auto lpeitem = cast<SPLPEItem>(*it);
+ if (lpeitem && lpeitem->hasPathEffect()){
+ Inkscape::LivePathEffect::Effect *simplify =
+ lpeitem->getFirstPathEffectOfType(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->getFirstPathEffectOfType(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);
+ auto sp_shape = cast<SPShape>(lpeitem);
+ if (sp_shape) {
+ guint previous_curve_length = sp_shape->curve()->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->curve()->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..74f0f63
--- /dev/null
+++ b/src/ui/toolbar/pencil-toolbar.h
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_PENCIL_TOOLBAR_H
+#define SEEN_PENCIL_TOOLBAR_H
+
+/**
+ * @file
+ * Pencil and pen toolbars
+ */
+/* 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 <vector>
+
+class SPDesktop;
+
+namespace Gtk {
+class RadioToolButton;
+class ToggleToolButton;
+class ToolButton;
+}
+
+namespace Inkscape {
+namespace XML {
+class Node;
+}
+
+namespace UI {
+namespace Widget {
+class SpinButtonToolItem;
+class ComboToolItem;
+}
+
+namespace Toolbar {
+class PencilToolbar : public Toolbar {
+private:
+ bool const _tool_is_pencil;
+ std::vector<Gtk::RadioToolButton *> _mode_buttons;
+
+ Gtk::ToggleToolButton *_pressure_item = nullptr;
+ UI::Widget::SpinButtonToolItem *_minpressure = nullptr;
+ UI::Widget::SpinButtonToolItem *_maxpressure = nullptr;
+ UI::Widget::SpinButtonToolItem *_shapescale = nullptr;
+
+ XML::Node *_repr = nullptr;
+ Gtk::ToolButton *_flatten_spiro_bspline = nullptr;
+ Gtk::ToolButton *_flatten_simplify = nullptr;
+
+ UI::Widget::ComboToolItem *_shape_item = nullptr;
+ UI::Widget::ComboToolItem *_cap_item = nullptr;
+
+ Gtk::ToggleToolButton *_simplify = nullptr;
+
+ bool _freeze = false;
+
+ Glib::RefPtr<Gtk::Adjustment> _minpressure_adj;
+ Glib::RefPtr<Gtk::Adjustment> _maxpressure_adj;
+ Glib::RefPtr<Gtk::Adjustment> _tolerance_adj;
+ Glib::RefPtr<Gtk::Adjustment> _shapescale_adj;
+
+ void add_freehand_mode_toggle();
+ void mode_changed(int mode);
+ Glib::ustring const freehand_tool_name();
+ void minpressure_value_changed();
+ void maxpressure_value_changed();
+ void shapewidth_value_changed();
+ void use_pencil_pressure();
+ void tolerance_value_changed();
+ void add_advanced_shape_options();
+ void add_powerstroke_cap();
+ void change_shape(int shape);
+ void update_width_value(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..bfbeb41
--- /dev/null
+++ b/src/ui/toolbar/rect-toolbar.cpp
@@ -0,0 +1,383 @@
+// 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 "selection.h"
+
+#include "object/sp-namedview.h"
+#include "object/sp-rect.h"
+
+#include "ui/icon-names.h"
+#include "ui/tools/rect-tool.h"
+#include "ui/widget/canvas.h"
+#include "ui/widget/combo-tool-item.h"
+#include "ui/widget/label-tool-item.h"
+#include "ui/widget/spinbutton.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::DocumentUndo;
+using Inkscape::Util::Unit;
+using Inkscape::Util::Quantity;
+using Inkscape::Util::unit_table;
+
+namespace Inkscape::UI::Toolbar {
+
+RectToolbar::RectToolbar(SPDesktop *desktop)
+ : Toolbar(desktop)
+ , _tracker(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR))
+ , _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
+ auto init_units = desktop->getNamedView()->display_units;
+ _tracker->setActiveUnit(init_units);
+ _mode_item->set_use_markup(true);
+
+ /* W */
+ {
+ auto width_val = prefs->getDouble("/tools/shapes/rect/width", 0);
+ width_val = Quantity::convert(width_val, "px", init_units);
+
+ _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->get_spin_button()->addUnitTracker(_tracker);
+ _width_item->set_focus_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_val = Quantity::convert(height_val, "px", init_units);
+
+ _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->get_spin_button()->addUnitTracker(_tracker);
+ _height_item->set_custom_numeric_menu_data(values);
+ _height_item->set_all_tooltip_text(_("Height of rectangle"));
+ _height_item->set_focus_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_val = Quantity::convert(rx_val, "px", init_units);
+
+ _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->get_spin_button()->addUnitTracker(_tracker);
+ _rx_item->set_all_tooltip_text(_("Horizontal radius of rounded corners"));
+ _rx_item->set_focus_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_val = Quantity::convert(ry_val, "px", init_units);
+
+ _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->get_spin_button()->addUnitTracker(_tracker);
+ _ry_item->set_all_tooltip_text(_("Vertical radius of rounded corners"));
+ _ry_item->set_focus_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->removeObserver(*this);
+ Inkscape::GC::release(_repr);
+ _repr = nullptr;
+ }
+ _changed.disconnect();
+}
+
+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 (is<SPRect>(*i)) {
+ if (adj->get_value() != 0) {
+ (cast<SPRect>(*i)->*setter)(Quantity::convert(adj->get_value(), unit, "px"));
+ } else {
+ (*i)->removeAttribute(value_name);
+ }
+ modmade = true;
+ }
+ }
+
+ sensitivize();
+
+ if (modmade) {
+ DocumentUndo::done(_desktop->getDocument(), _("Change rectangle"), INKSCAPE_ICON("draw-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)
+{
+ // 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->removeObserver(*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->removeObserver(*this);
+ Inkscape::GC::release(_repr);
+ _repr = nullptr;
+ }
+
+ auto itemlist= selection->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ if (is<SPRect>(*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->addObserver(*this);
+ _repr->synthesizeEvents(*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::notifyAttributeChanged(Inkscape::XML::Node &repr, GQuark,
+ Inkscape::Util::ptr_shared,
+ Inkscape::Util::ptr_shared)
+{
+ // quit if run by the _changed callbacks
+ if (_freeze) {
+ return;
+ }
+
+ // in turn, prevent callbacks from responding
+ _freeze = true;
+
+ auto unit = _tracker->getActiveUnit();
+ if (!unit) {
+ return;
+ }
+
+ if (auto rect = cast<SPRect>(_item)) {
+ _rx_adj ->set_value(Quantity::convert(rect->getVisibleRx(), "px", unit));
+ _ry_adj ->set_value(Quantity::convert(rect->getVisibleRy(), "px", unit));
+ _width_adj ->set_value(Quantity::convert(rect->getVisibleWidth(), "px", unit));
+ _height_adj->set_value(Quantity::convert(rect->getVisibleHeight(), "px", unit));
+ }
+
+ sensitivize();
+ _freeze = false;
+}
+
+} // namespace Inkscape::UI::Toolbar
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 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..bfc46d2
--- /dev/null
+++ b/src/ui/toolbar/rect-toolbar.h
@@ -0,0 +1,116 @@
+// 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 <gtkmm/adjustment.h>
+
+#include "toolbar.h"
+
+#include "xml/node-observer.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 Inkscape::XML::NodeObserver
+{
+private:
+ UI::Widget::UnitTracker *_tracker;
+
+ XML::Node *_repr{nullptr};
+ 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{false};
+ bool _single{true};
+
+ 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);
+
+ sigc::connection _changed;
+
+ void notifyAttributeChanged(Inkscape::XML::Node &node, GQuark name,
+ Inkscape::Util::ptr_shared old_value,
+ Inkscape::Util::ptr_shared new_value) final;
+
+protected:
+ RectToolbar(SPDesktop *desktop);
+ ~RectToolbar() override;
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+};
+
+}
+}
+}
+
+#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..82a421c
--- /dev/null
+++ b/src/ui/toolbar/select-toolbar.cpp
@@ -0,0 +1,654 @@
+// 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 "selection.h"
+#include "message-stack.h"
+#include "selection-chemistry.h"
+
+#include "object/sp-item-transform.h"
+#include "object/sp-namedview.h"
+
+#include "page-manager.h"
+
+#include "ui/icon-names.h"
+#include "ui/widget/canvas.h" // Focus widget
+#include "ui/widget/combo-tool-item.h"
+#include "ui/widget/spin-button-tool-item.h"
+#include "ui/widget/spinbutton.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)),
+ _lock_btn(Gtk::manage(new Gtk::ToggleToolButton())),
+ _select_touch_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())),
+ _update(false),
+ _action_prefix("selector:toolbar:")
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ {
+ auto button = Gtk::manage(new Gtk::ToolButton(N_("Select Al_l")));
+ button->set_tooltip_text(N_("Select all objects"));
+ button->set_icon_name(INKSCAPE_ICON("edit-select-all"));
+ // Must use C API until GTK4
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(button->gobj()), "win.select-all");
+ add(*button);
+ }
+
+ {
+ auto button = Gtk::manage(new Gtk::ToolButton(N_("Select All in All La_yers")));
+ button->set_tooltip_text(N_("Select all objects in all visible and unlocked layers"));
+ button->set_icon_name(INKSCAPE_ICON("edit-select-all-layers"));
+ // Must use C API until GTK4
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(button->gobj()), "win.select-all-layers");
+ add(*button);
+ }
+
+ {
+ auto button = Gtk::manage(new Gtk::ToolButton(N_("D_eselect")));
+ button->set_tooltip_text(N_("Deselect any selected objects"));
+ button->set_icon_name(INKSCAPE_ICON("edit-select-none"));
+ // Must use C API until GTK4
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(button->gobj()), "win.select-none");
+ add(*button);
+ _context_items.push_back(button);
+ }
+
+ _select_touch_btn->set_label(_("Select by touch"));
+ _select_touch_btn->set_tooltip_text(_("Toggle selection box to select all touched objects."));
+ _select_touch_btn->set_icon_name(INKSCAPE_ICON("selection-touch"));
+ _select_touch_btn->set_active(prefs->getBool("/tools/select/touch_box", false));
+ _select_touch_btn->signal_toggled().connect(sigc::mem_fun(*this, &SelectToolbar::toggle_touch));
+
+ add(*_select_touch_btn);
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ {
+ auto button = Gtk::manage(new Gtk::ToolButton(N_("Rotate _90\xc2\xb0 CCW")));
+ button->set_tooltip_text(N_("Rotate selection 90\xc2\xb0 counter-clockwise"));
+ button->set_icon_name(INKSCAPE_ICON("object-rotate-left"));
+ // Must use C API until GTK4
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(button->gobj()), "app.object-rotate-90-ccw");
+ add(*button);
+ _context_items.push_back(button);
+ }
+
+ {
+ auto button = Gtk::manage(new Gtk::ToolButton(N_("Rotate _90\xc2\xb0 CW")));
+ button->set_tooltip_text(N_("Rotate selection 90\xc2\xb0 clockwise"));
+ button->set_icon_name(INKSCAPE_ICON("object-rotate-right"));
+ // Must use C API until GTK4
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(button->gobj()), "app.object-rotate-90-cw");
+ add(*button);
+ _context_items.push_back(button);
+ }
+
+ {
+ auto button = Gtk::manage(new Gtk::ToolButton(N_("Flip _Horizontal")));
+ button->set_tooltip_text(N_("Flip selected objects horizontally"));
+ button->set_icon_name(INKSCAPE_ICON("object-flip-horizontal"));
+ // Must use C API until GTK4
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(button->gobj()), "app.object-flip-horizontal");
+ add(*button);
+ _context_items.push_back(button);
+ }
+
+ {
+ auto button = Gtk::manage(new Gtk::ToolButton(N_("Flip _Vertical")));
+ button->set_tooltip_text(N_("Flip selected objects vertically"));
+ button->set_icon_name(INKSCAPE_ICON("object-flip-vertical"));
+ // Must use C API until GTK4
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(button->gobj()), "app.object-flip-vertical");
+ add(*button);
+ _context_items.push_back(button);
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ {
+ auto button = Gtk::manage(new Gtk::ToolButton(N_("Raise to _Top")));
+ button->set_tooltip_text(N_("Raise selection to top"));
+ button->set_icon_name(INKSCAPE_ICON("selection-top"));
+ // Must use C API until GTK4
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(button->gobj()), "app.selection-top");
+ add(*button);
+ _context_items.push_back(button);
+ }
+
+ {
+ auto button = Gtk::manage(new Gtk::ToolButton(N_("_Raise")));
+ button->set_tooltip_text(N_("Raise selection one step"));
+ button->set_icon_name(INKSCAPE_ICON("selection-raise"));
+ // Must use C API until GTK4
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(button->gobj()), "app.selection-raise");
+ add(*button);
+ _context_items.push_back(button);
+ }
+
+ {
+ auto button = Gtk::manage(new Gtk::ToolButton(N_("_Lower")));
+ button->set_tooltip_text(N_("Lower selection one step"));
+ button->set_icon_name(INKSCAPE_ICON("selection-lower"));
+ // Must use C API until GTK4
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(button->gobj()), "app.selection-lower");
+ add(*button);
+ _context_items.push_back(button);
+ }
+
+ {
+ auto button = Gtk::manage(new Gtk::ToolButton(N_("Lower to _Bottom")));
+ button->set_tooltip_text(N_("Lower selection to bottom"));
+ button->set_icon_name(INKSCAPE_ICON("selection-bottom"));
+ // Must use C API until GTK4
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(button->gobj()), "app.selection-bottom");
+ add(*button);
+ _context_items.push_back(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->get_spin_button()->addUnitTracker(_tracker.get());
+ x_btn->set_focus_widget(_desktop->getCanvas());
+ 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->get_spin_button()->addUnitTracker(_tracker.get());
+ y_btn->set_focus_widget(_desktop->getCanvas());
+ 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->get_spin_button()->addUnitTracker(_tracker.get());
+ w_btn->set_focus_widget(_desktop->getCanvas());
+ 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));
+ _lock_btn->set_name("lock");
+ 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->get_spin_button()->addUnitTracker(_tracker.get());
+ h_btn->set_focus_widget(_desktop->getCanvas());
+ 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);
+
+ assert(desktop);
+ auto *selection = desktop->getSelection();
+
+ // Force update when selection changes.
+ _connections.emplace_back( //
+ selection->connectModified(sigc::mem_fun(*this, &SelectToolbar::on_inkscape_selection_modified)));
+ _connections.emplace_back(
+ selection->connectChanged(sigc::mem_fun(*this, &SelectToolbar::on_inkscape_selection_changed)));
+
+ // Update now.
+ layout_widget_update(selection);
+
+ for (auto item : _context_items) {
+ if ( item->is_sensitive() ) {
+ item->set_sensitive(false);
+ }
+ }
+
+ show_all();
+}
+
+void SelectToolbar::on_unrealize()
+{
+ for (auto &conn : _connections) {
+ conn.disconnect();
+ }
+
+ parent_type::on_unrealize();
+}
+
+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;
+
+ auto prefs = Inkscape::Preferences::get();
+ auto selection = _desktop->getSelection();
+ auto document = _desktop->getDocument();
+ auto &pm = document->getPageManager();
+ auto page = pm.getSelectedPageRect();
+ auto page_correction = prefs->getBool("/options/origincorrection/page", true);
+
+ document->ensureUpToDate();
+
+ Geom::OptRect bbox_vis = selection->visualBounds();
+ Geom::OptRect bbox_geom = selection->geometricBounds();
+ Geom::OptRect bbox_user = selection->preferredBounds();
+
+ if ( !bbox_user ) {
+ _update = false;
+ return;
+ }
+
+ Unit const *unit = _tracker->getActiveUnit();
+ g_return_if_fail(unit != nullptr);
+
+ gdouble old_w = bbox_user->dimensions()[Geom::X];
+ gdouble old_h = bbox_user->dimensions()[Geom::Y];
+ gdouble new_w, new_h, new_x, new_y = 0;
+
+ if (unit->type == Inkscape::Util::UNIT_TYPE_LINEAR) {
+ new_w = Quantity::convert(_adj_w->get_value(), unit, "px");
+ new_h = Quantity::convert(_adj_h->get_value(), unit, "px");
+ new_x = Quantity::convert(_adj_x->get_value(), unit, "px");
+ new_y = Quantity::convert(_adj_y->get_value(), unit, "px");
+
+ } else {
+ gdouble old_x = bbox_user->min()[Geom::X] + (old_w * selection->anchor_x);
+ gdouble old_y = bbox_user->min()[Geom::Y] + (old_h * selection->anchor_y);
+
+ // Adjust against selected page, so later correction isn't broken.
+ if (page_correction) {
+ old_x -= page.left();
+ old_y -= page.top();
+ }
+
+ new_x = old_x * (_adj_x->get_value() / 100 / unit->factor);
+ new_y = old_y * (_adj_y->get_value() / 100 / unit->factor);
+ new_w = old_w * (_adj_w->get_value() / 100 / unit->factor);
+ new_h = old_h * (_adj_h->get_value() / 100 / unit->factor);
+ }
+
+ // Adjust depending on the selected anchor.
+ gdouble x0 = (new_x - (old_w * selection->anchor_x)) - ((new_w - old_w) * selection->anchor_x);
+ gdouble y0 = (new_y - (old_h * selection->anchor_y)) - ((new_h - old_h) * selection->anchor_y);
+
+ // Adjust according to the selected page, if needed
+ if (page_correction) {
+ x0 += page.left();
+ y0 += page.top();
+ }
+
+ gdouble x1 = x0 + new_w;
+ gdouble xrel = new_w / old_w;
+ gdouble y1 = y0 + new_h;
+ gdouble yrel = new_h / old_h;
+
+ // 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);
+ }
+
+ char const *const actionkey = get_action_key(mh, sh, mv, sv);
+
+ if (actionkey != nullptr) {
+
+ bool transform_stroke = prefs->getBool("/options/transform/stroke", true);
+ bool preserve = prefs->getBool("/options/preservetransform/value", false);
+
+ Geom::Affine scaler;
+ if (prefs->getInt("/tools/bounding_box") == 0) { // 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, _("Transform by toolbar"), INKSCAPE_ICON("tool-pointer"));
+ }
+
+ _update = false;
+}
+
+void
+SelectToolbar::layout_widget_update(Inkscape::Selection *sel)
+{
+ if (_update) {
+ return;
+ }
+
+ _update = true;
+ using Geom::X;
+ using Geom::Y;
+ if ( sel && !sel->isEmpty() ) {
+ Geom::OptRect const bbox(sel->preferredBounds());
+ if ( bbox ) {
+ Unit const *unit = _tracker->getActiveUnit();
+ g_return_if_fail(unit != nullptr);
+
+ auto width = bbox->dimensions()[X];
+ auto height = bbox->dimensions()[Y];
+ auto x = bbox->min()[X] + (width * sel->anchor_x);
+ auto y = bbox->min()[Y] + (height * sel->anchor_y);
+
+ auto prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/options/origincorrection/page", true)) {
+ auto &pm = _desktop->getDocument()->getPageManager();
+ auto page = pm.getSelectedPageRect();
+ x -= page.left();
+ y -= page.top();
+ }
+
+ 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(), x );
+ _tracker->setFullVal( _adj_y->gobj(), y );
+ _tracker->setFullVal( _adj_w->gobj(), width );
+ _tracker->setFullVal( _adj_h->gobj(), height );
+ } else {
+ _adj_x->set_value(Quantity::convert(x, "px", unit));
+ _adj_y->set_value(Quantity::convert(y, "px", unit));
+ _adj_w->set_value(Quantity::convert(width, "px", unit));
+ _adj_h->set_value(Quantity::convert(height, "px", unit));
+ }
+ }
+ }
+
+ _update = false;
+}
+
+void
+SelectToolbar::on_inkscape_selection_modified(Inkscape::Selection *selection, guint flags)
+{
+ assert(_desktop->getSelection() == selection);
+ if ((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)
+{
+ assert(_desktop->getSelection() == selection);
+ {
+ bool setActive = (selection && !selection->isEmpty());
+
+ for (auto item : _context_items) {
+ if ( setActive != item->get_sensitive() ) {
+ item->set_sensitive(setActive);
+ }
+ }
+
+ layout_widget_update(selection);
+ }
+}
+
+char const *SelectToolbar::get_action_key(double mh, double sh, double mv, double sv)
+{
+ // 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
+ double const threshold = 5e-4;
+ char const *const action = ( mh > threshold ? "move:horizontal:" :
+ sh > threshold ? "scale:horizontal:" :
+ mv > threshold ? "move:vertical:" :
+ sv > threshold ? "scale:vertical:" : nullptr );
+ if (!action) {
+ return nullptr;
+ }
+ _action_key = _action_prefix + action;
+ return _action_key.c_str();
+}
+
+void
+SelectToolbar::toggle_lock() {
+ // use this roundabout way of changing image to make sure its size is preserved
+ auto btn = static_cast<Gtk::ToggleButton*>(_lock_btn->get_child());
+ auto image = static_cast<Gtk::Image*>(btn->get_child());
+ if (!image) {
+ g_warning("No GTK image in toolbar button 'lock'");
+ return;
+ }
+ auto size = image->get_pixel_size();
+
+ if ( _lock_btn->get_active() ) {
+ image->set_from_icon_name("object-locked", Gtk::ICON_SIZE_BUTTON);
+ } else {
+ image->set_from_icon_name("object-unlocked", Gtk::ICON_SIZE_BUTTON);
+ }
+ image->set_pixel_size(size);
+}
+
+void
+SelectToolbar::toggle_touch()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/tools/select/touch_box", _select_touch_btn->get_active());
+}
+
+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..af6db27
--- /dev/null
+++ b/src/ui/toolbar/select-toolbar.h
@@ -0,0 +1,93 @@
+// 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 {
+ using parent_type = 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 *_select_touch_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;
+
+ std::vector<sigc::connection> _connections;
+
+ bool _update;
+ std::string _action_key;
+ std::string const _action_prefix;
+
+ char const *get_action_key(double mh, double sh, double mv, double sv);
+ 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_touch();
+ void toggle_stroke();
+ void toggle_corners();
+ void toggle_gradient();
+ void toggle_pattern();
+
+protected:
+ SelectToolbar(SPDesktop *desktop);
+
+ void on_unrealize() override;
+
+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/spiral-toolbar.cpp b/src/ui/toolbar/spiral-toolbar.cpp
new file mode 100644
index 0000000..86eda45
--- /dev/null
+++ b/src/ui/toolbar/spiral-toolbar.cpp
@@ -0,0 +1,277 @@
+// 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 "object/sp-spiral.h"
+
+#include "ui/icon-names.h"
+#include "ui/widget/canvas.h"
+#include "ui/widget/label-tool-item.h"
+#include "ui/widget/spin-button-tool-item.h"
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+
+SpiralToolbar::SpiralToolbar(SPDesktop *desktop)
+ : Toolbar(desktop)
+{
+ 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(desktop->getCanvas());
+ _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(desktop->getCanvas());
+ _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(desktop->getCanvas());
+ _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->removeObserver(*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(), nullptr);
+
+ bool modmade = false;
+ auto itemlist= _desktop->getSelection()->items();
+ for(auto i=itemlist.begin();i!=itemlist.end(); ++i){
+ SPItem *item = *i;
+ if (is<SPSpiral>(item)) {
+ Inkscape::XML::Node *repr = item->getRepr();
+ repr->setAttributeSvgDouble(namespaced_name, adj->get_value() );
+ item->updateRepr();
+ modmade = true;
+ }
+ }
+
+ g_free(namespaced_name);
+
+ if (modmade) {
+ DocumentUndo::done(_desktop->getDocument(), _("Change spiral"), INKSCAPE_ICON("draw-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->getCanvas()) _desktop->getCanvas()->grab_focus();
+}
+
+void
+SpiralToolbar::selection_changed(Inkscape::Selection *selection)
+{
+ int n_selected = 0;
+ Inkscape::XML::Node *repr = nullptr;
+
+ if ( _repr ) {
+ _repr->removeObserver(*this);
+ GC::release(_repr);
+ _repr = nullptr;
+ }
+
+ auto itemlist= selection->items();
+ for(auto i=itemlist.begin();i!=itemlist.end(); ++i){
+ SPItem *item = *i;
+ if (is<SPSpiral>(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->addObserver(*this);
+ _repr->synthesizeEvents(*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::notifyAttributeChanged(Inkscape::XML::Node &repr, GQuark, Inkscape::Util::ptr_shared, Inkscape::Util::ptr_shared)
+{
+
+ // quit if run by the _changed callbacks
+ if (_freeze) {
+ return;
+ }
+
+ // in turn, prevent callbacks from responding
+ _freeze = true;
+
+ double revolution = repr.getAttributeDouble("sodipodi:revolution", 3.0);
+ _revolution_adj->set_value(revolution);
+
+ double expansion = repr.getAttributeDouble("sodipodi:expansion", 1.0);
+ _expansion_adj->set_value(expansion);
+
+ double t0 = repr.getAttributeDouble("sodipodi:t0", 0.0);
+ _t0_adj->set_value(t0);
+
+ _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..bf696da
--- /dev/null
+++ b/src/ui/toolbar/spiral-toolbar.h
@@ -0,0 +1,101 @@
+// 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>
+
+#include "xml/node-observer.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 XML::NodeObserver
+{
+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{false};
+
+ XML::Node *_repr{nullptr};
+
+ 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;
+
+ void event_attr_changed(XML::Node &repr);
+
+ void notifyAttributeChanged(Inkscape::XML::Node &node, GQuark key, Inkscape::Util::ptr_shared oldval, Inkscape::Util::ptr_shared newval) final;
+
+protected:
+ SpiralToolbar(SPDesktop *desktop);
+ ~SpiralToolbar() override;
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+
+};
+}
+}
+}
+
+#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..de6939a
--- /dev/null
+++ b/src/ui/toolbar/spray-toolbar.cpp
@@ -0,0 +1,541 @@
+// 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 "ui/icon-names.h"
+#include "ui/simple-pref-pusher.h"
+
+#include "ui/dialog/clonetiler.h"
+#include "ui/dialog/dialog-container.h"
+#include "ui/dialog/dialog-base.h"
+
+#include "ui/widget/canvas.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)
+{
+ Inkscape::UI::Dialog::DialogBase *dialog = desktop->getContainer()->get_dialog("CloneTiler");
+ if (!dialog) {
+ desktop->getContainer()->new_dialog("CloneTiler");
+ return dynamic_cast<Inkscape::UI::Dialog::CloneTiler *>(
+ desktop->getContainer()->get_dialog("CloneTiler"));
+ }
+ return dynamic_cast<Inkscape::UI::Dialog::CloneTiler *>(dialog);
+}
+
+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(desktop->canvas);
+ _width_adj->signal_value_changed().connect(sigc::mem_fun(*this, &SprayToolbar::width_value_changed));
+ 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(desktop->canvas);
+ _population_adj->signal_value_changed().connect(sigc::mem_fun(*this, &SprayToolbar::population_value_changed));
+ 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(desktop->canvas);
+ _rotation_adj->signal_value_changed().connect(sigc::mem_fun(*this, &SprayToolbar::rotation_value_changed));
+ 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(desktop->canvas);
+ _scale_adj->signal_value_changed().connect(sigc::mem_fun(*this, &SprayToolbar::scale_value_changed));
+ 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(desktop->canvas);
+ _sd_adj->signal_value_changed().connect(sigc::mem_fun(*this, &SprayToolbar::standard_deviation_value_changed));
+ 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(desktop->canvas);
+ _mean_adj->signal_value_changed().connect(sigc::mem_fun(*this, &SprayToolbar::mean_value_changed));
+ 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(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 = _desktop;
+ if (Inkscape::UI::Dialog::CloneTiler *ct = get_clone_tiler_panel(dt)){
+ dt->getContainer()->new_dialog("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..a41dba8
--- /dev/null
+++ b/src/ui/toolbar/star-toolbar.cpp
@@ -0,0 +1,553 @@
+// 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 "object/sp-star.h"
+
+#include "ui/icon-names.h"
+#include "ui/tools/star-tool.h"
+#include "ui/widget/canvas.h"
+#include "ui/widget/label-tool-item.h"
+#include "ui/widget/spin-button-tool-item.h"
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+StarToolbar::StarToolbar(SPDesktop *desktop)
+ : Toolbar(desktop)
+ , _mode_item(Gtk::make_managed<UI::Widget::LabelToolItem>(_("<b>New:</b>")))
+{
+ _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 = {2, 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, isFlatSided ? 3 : 2, 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(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(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(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(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->removeObserver(*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 (is<SPStar>(item)) {
+ Inkscape::XML::Node *repr = item->getRepr();
+ if (flat) {
+ gint sides = (gint)_magnitude_adj->get_value();
+ if (sides < 3) {
+ repr->setAttributeInt("sodipodi:sides", 3);
+ }
+ }
+ repr->setAttribute("inkscape:flatsided", flat ? "true" : "false" );
+
+ item->updateRepr();
+ modmade = true;
+ }
+ }
+
+ _magnitude_adj->set_lower(flat ? 3 : 2);
+ if (flat && _magnitude_adj->get_value() < 3) {
+ _magnitude_adj->set_value(3);
+ }
+
+ if (modmade) {
+ DocumentUndo::done(_desktop->getDocument(), flat ? _("Make polygon") : _("Make star"), INKSCAPE_ICON("draw-polygon-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 (is<SPStar>(item)) {
+ Inkscape::XML::Node *repr = item->getRepr();
+ repr->setAttributeInt("sodipodi:sides", (gint)_magnitude_adj->get_value());
+ double arg1 = repr->getAttributeDouble("sodipodi:arg1", 0.5);
+ repr->setAttributeSvgDouble("sodipodi:arg2", (arg1 + M_PI / (gint)_magnitude_adj->get_value()));
+ item->updateRepr();
+ modmade = true;
+ }
+ }
+ if (modmade) {
+ DocumentUndo::done(_desktop->getDocument(), _("Star: Change number of corners"), INKSCAPE_ICON("draw-polygon-star"));
+ }
+
+ _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 (is<SPStar>(item)) {
+ Inkscape::XML::Node *repr = item->getRepr();
+
+ gdouble r1 = repr->getAttributeDouble("sodipodi:r1", 1.0);;
+ gdouble r2 = repr->getAttributeDouble("sodipodi:r2", 1.0);
+
+ if (r2 < r1) {
+ repr->setAttributeSvgDouble("sodipodi:r2", r1*_spoke_adj->get_value());
+ } else {
+ repr->setAttributeSvgDouble("sodipodi:r1", r2*_spoke_adj->get_value());
+ }
+
+ item->updateRepr();
+ modmade = true;
+ }
+ }
+
+ if (modmade) {
+ DocumentUndo::done(_desktop->getDocument(), _("Star: Change spoke ratio"), INKSCAPE_ICON("draw-polygon-star"));
+ }
+
+ _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 (is<SPStar>(item)) {
+ Inkscape::XML::Node *repr = item->getRepr();
+ repr->setAttributeSvgDouble("inkscape:rounded", (gdouble) _roundedness_adj->get_value());
+ item->updateRepr();
+ modmade = true;
+ }
+ }
+ if (modmade) {
+ DocumentUndo::done(_desktop->getDocument(), _("Star: Change rounding"), INKSCAPE_ICON("draw-polygon-star"));
+ }
+
+ _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 (is<SPStar>(item)) {
+ Inkscape::XML::Node *repr = item->getRepr();
+ repr->setAttributeSvgDouble("inkscape:randomized", (gdouble) _randomization_adj->get_value());
+ item->updateRepr();
+ modmade = true;
+ }
+ }
+ if (modmade) {
+ DocumentUndo::done(_desktop->getDocument(), _("Star: Change randomization"), INKSCAPE_ICON("draw-polygon-star"));
+ }
+
+ _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->removeObserver(*this);
+ Inkscape::GC::release(_repr);
+ _repr = nullptr;
+ }
+
+ auto itemlist= selection->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ SPItem *item = *i;
+ if (is<SPStar>(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->addObserver(*this);
+ _repr->synthesizeEvents(*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::notifyAttributeChanged(Inkscape::XML::Node &repr, GQuark name_,
+ Inkscape::Util::ptr_shared,
+ Inkscape::Util::ptr_shared)
+{
+ auto const name = g_quark_to_string(name_);
+ // quit if run by the _changed callbacks
+ if (_freeze) {
+ return;
+ }
+
+ // in turn, prevent callbacks from responding
+ _freeze = true;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool isFlatSided = prefs->getBool("/tools/shapes/star/isflatsided", false);
+
+ if (!strcmp(name, "inkscape:randomized")) {
+ double randomized = repr.getAttributeDouble("inkscape:randomized", 0.0);
+ _randomization_adj->set_value(randomized);
+ } else if (!strcmp(name, "inkscape:rounded")) {
+ double rounded = repr.getAttributeDouble("inkscape:rounded", 0.0);
+ _roundedness_adj->set_value(rounded);
+ } else if (!strcmp(name, "inkscape:flatsided")) {
+ char const *flatsides = repr.attribute("inkscape:flatsided");
+ if ( flatsides && !strcmp(flatsides,"false") ) {
+ _flat_item_buttons[1]->set_active();
+ _spoke_item->set_visible(true);
+ _magnitude_adj->set_lower(2);
+ } else {
+ _flat_item_buttons[0]->set_active();
+ _spoke_item->set_visible(false);
+ _magnitude_adj->set_lower(3);
+ }
+ } else if ((!strcmp(name, "sodipodi:r1") || !strcmp(name, "sodipodi:r2")) && (!isFlatSided) ) {
+ double r1 = repr.getAttributeDouble("sodipodi:r1", 1.0);
+ double r2 = repr.getAttributeDouble("sodipodi:r2", 1.0);
+
+ if (r2 < r1) {
+ _spoke_adj->set_value(r2 / r1);
+ } else {
+ _spoke_adj->set_value(r1 / r2);
+ }
+ } else if (!strcmp(name, "sodipodi:sides")) {
+ int sides = repr.getAttributeInt("sodipodi:sides", 0);
+ _magnitude_adj->set_value(sides);
+ }
+
+ _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..b163f3d
--- /dev/null
+++ b/src/ui/toolbar/star-toolbar.h
@@ -0,0 +1,111 @@
+// 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 <gtkmm/adjustment.h>
+
+#include "toolbar.h"
+
+#include "xml/node-observer.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 XML::NodeObserver
+{
+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{nullptr};
+
+ 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{false};
+ 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);
+
+ void notifyAttributeChanged(Inkscape::XML::Node &node, GQuark name,
+ Inkscape::Util::ptr_shared old_value,
+ Inkscape::Util::ptr_shared new_value) final;
+
+
+protected:
+ StarToolbar(SPDesktop *desktop);
+ ~StarToolbar() override;
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+};
+
+}
+}
+}
+
+#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..fe7f09b
--- /dev/null
+++ b/src/ui/toolbar/text-toolbar.cpp
@@ -0,0 +1,2647 @@
+// 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 "libnrtype/font-lister.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/dialog/dialog-container.h"
+#include "ui/icon-names.h"
+#include "ui/tools/select-tool.h"
+#include "ui/tools/text-tool.h"
+#include "ui/widget/canvas.h" // Focus
+#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 "util/font-collections.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();
+ auto allList = get_all_items(document->getRoot(), desktop, false, false, true);
+ 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);
+}
+
+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 Collections popover */
+ {
+ auto font_collection_item = Gtk::manage(new Gtk::ToolItem);
+ add(*font_collection_item);
+
+ auto font_collection_button = Gtk::manage(new Gtk::MenuButton);
+ font_collection_button->set_image_from_icon_name(INKSCAPE_ICON("font_collections"));
+ font_collection_button->set_always_show_image(true);
+ font_collection_button->set_tooltip_text(_("Select Font Collections"));
+ font_collection_item->add(*font_collection_button);
+
+ // Popover.
+ auto font_collection_popover = Gtk::manage(new Gtk::Popover(*font_collection_button));
+ // font_collection_popover->set_modal(false); // Stay open until button clicked again.
+ font_collection_button->set_popover(*font_collection_popover);
+
+ // Grid inside the popover.
+ auto popover_grid = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
+ popover_grid->set_margin_top(4);
+ popover_grid->set_margin_bottom(4);
+ popover_grid->set_margin_start(4);
+ popover_grid->set_margin_end(4);
+ popover_grid->show_all();
+
+ // This frame will contain the list of the font collections.
+ auto popover_frame = Gtk::manage(new Gtk::Frame);
+ popover_frame->show_all();
+ popover_frame->set_label(_("Font Collections"));
+ popover_frame->set_margin_top(4);
+ popover_grid->add(*popover_frame);
+
+ // The ListBox widget will display the names of the font collections.
+ font_collections_list = Gtk::manage(new Gtk::ListBox);
+ popover_frame->add(*font_collections_list);
+ font_collections_list->show_all();
+
+ // To open the Font Collections Manager dialogue.
+ auto fcm_btn = Gtk::manage(new Gtk::Button);
+ fcm_btn->set_tooltip_text(_("Open the Font Collections Manager dialog"));
+ fcm_btn->set_label(_("Open Collections Editor"));
+ fcm_btn->set_margin_top(4);
+ popover_grid->add(*fcm_btn);
+ fcm_btn->show_all();
+ fcm_btn->signal_clicked().connect([=](){ TextToolbar::on_fcm_button_pressed(); });
+
+ // To reset the selected font collections and the font list.
+ auto reset_item = Gtk::manage(new Gtk::ToolItem);
+ add(*reset_item);
+
+ auto reset_btn = Gtk::manage(new Gtk::Button);
+ reset_btn->set_tooltip_text(_("Show all available fonts"));
+ reset_btn->set_image_from_icon_name(INKSCAPE_ICON("view-refresh"));
+ reset_btn->set_always_show_image(true);
+ reset_item->add(*reset_btn);
+ reset_btn->show_all();
+ // reset_btn->set_hexpand(false);
+ reset_btn->signal_clicked().connect([=](){ TextToolbar::on_reset_button_pressed(); });
+ font_collection_popover->add(*popover_grid);
+
+ // Attach the signal to display the popover.
+ font_collection_popover->signal_show().connect([=](){
+ display_font_collections();
+ }, false);
+
+ FontCollections *font_collections = Inkscape::FontCollections::get();
+
+ // This signal will keep both the Text and Font dialogue and
+ // TextToolbar popovers in sync with each other.
+ fc_changed_selection = font_collections->connect_selection_update([=]() { display_font_collections(); });
+
+ // This one will keep the text toolbar Font Collections
+ // updated in case of any change in the Font Collections.
+ fc_update = font_collections->connect_update([=]() { display_font_collections(); });
+ }
+
+ /* Font family */
+ {
+ // Font list
+ Inkscape::FontLister* fontlister = Inkscape::FontLister::get_instance();
+ fontlister->update_font_list(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->getCanvas()->gobj()))); // 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->signal_changed().connect([=](){ 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->getCanvas()->gobj()))); // Focus widget
+
+ _font_style_item->signal_changed().connect([=](){ 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->getCanvas()->gobj()))); // Focus widget
+
+ _font_size_item->signal_changed().connect([=](){ 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(desktop->getCanvas());
+ _line_height_adj->signal_value_changed().connect([=](){ 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);
+ }
+
+ /* 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));
+ }
+
+ /* Character positioning popover */
+
+ auto positioning_item = Gtk::manage(new Gtk::ToolItem);
+ add(*positioning_item);
+
+ auto positioning_button = Gtk::manage(new Gtk::MenuButton);
+ positioning_button->set_image_from_icon_name(INKSCAPE_ICON("text_horz_kern"));
+ positioning_button->set_always_show_image(true);
+ positioning_button->set_tooltip_text(_("Kerning, word spacing, character positioning"));
+ positioning_button->set_label(_("Spacing"));
+ positioning_item->add(*positioning_button);
+
+ auto positioning_popover = Gtk::manage(new Gtk::Popover(*positioning_button));
+ positioning_popover->set_modal(false); // Stay open until button clicked again.
+ positioning_button->set_popover(*positioning_popover);
+
+ auto positioning_grid = Gtk::manage(new Gtk::Grid);
+ positioning_popover->add(*positioning_grid);
+
+ /* 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, -1000.0, 1000.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(desktop->getCanvas());
+ _letter_spacing_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TextToolbar::letterspacing_value_changed));
+ _letter_spacing_item->set_sensitive(true);
+ _letter_spacing_item->set_icon(INKSCAPE_ICON("text_letter_spacing"));
+
+ positioning_grid->attach(*_letter_spacing_item, 0, 0);
+ }
+
+ /* 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, -1000.0, 1000.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(desktop->getCanvas());
+ _word_spacing_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TextToolbar::wordspacing_value_changed));
+ _word_spacing_item->set_sensitive(true);
+ _word_spacing_item->set_icon(INKSCAPE_ICON("text_word_spacing"));
+
+ positioning_grid->attach(*_word_spacing_item, 1, 0);
+ }
+
+ /* 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, -1000.0, 1000.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(desktop->getCanvas());
+ _dx_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TextToolbar::dx_value_changed));
+ _dx_item->set_sensitive(true);
+ _dx_item->set_icon(INKSCAPE_ICON("text_horz_kern"));
+
+ positioning_grid->attach(*_dx_item, 0, 1);
+ }
+
+ /* 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, -1000.0, 1000.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(desktop->getCanvas());
+ _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"));
+
+ positioning_grid->attach(*_dy_item, 1, 1);
+ }
+
+ /* 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(desktop->getCanvas());
+ _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"));
+
+ positioning_grid->attach(*_rotation_item, 2, 1);
+ }
+
+ positioning_grid->show_all();
+
+ /* 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 = _desktop;
+ if(_outer) {
+ // Apply css to parent text objects directly.
+ for (auto item : desktop->getSelection()->items()) {
+ if (is<SPText>(item) || is<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;
+
+ Glib::ustring new_family = _font_family_item->get_active_text();
+ 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 );
+
+ if (mergeDefaultStyle(css)) {
+ // If there is a selection, update
+ DocumentUndo::done(_desktop->getDocument(), _("Text: Change font family"), INKSCAPE_ICON("draw-text"));
+ }
+ sp_repr_css_attr_unref (css);
+ }
+
+ // unfreeze
+ _freeze = false;
+
+ SPDocument *document = _desktop->getDocument();
+ fontlister->add_document_fonts_at_top(document);
+
+#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;
+
+ auto active_text = _font_size_item->get_active_text();
+ char const *text = active_text.c_str();
+ 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 );
+ _freeze = false;
+ return;
+ }
+
+ 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 (mergeDefaultStyle(css)) {
+ DocumentUndo::maybeDone(_desktop->getDocument(), "ttb:size", _("Text: Change font size"), INKSCAPE_ICON("draw-text"));
+ }
+
+ 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 = _desktop;
+ sp_desktop_set_style (desktop, css, true, true);
+
+ if (mergeDefaultStyle(css)) {
+ DocumentUndo::done(desktop->getDocument(), _("Text: Change font style"), INKSCAPE_ICON("draw-text"));
+ }
+
+ 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(_desktop->getDocument());
+ int result_baseline = sp_desktop_query_style (_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 = _desktop;
+ sp_desktop_set_style (desktop, css, true, false);
+
+ // Save for undo
+ if(result_baseline != QUERY_STYLE_NOTHING) {
+ DocumentUndo::maybeDone(_desktop->getDocument(), "ttb:script", _("Text: Change superscript or subscript"), INKSCAPE_ICON("draw-text"));
+ }
+ _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 = _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) {
+ auto text = cast<SPText>(i);
+ // auto flowtext = 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 = cast<SPText>(item)->attributes.firstXY();
+ if (axis == Geom::X) {
+ XY = XY + Geom::Point (move, 0);
+ } else {
+ XY = XY + Geom::Point (0, move);
+ }
+ cast<SPText>(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;
+ }
+ }
+
+ if (mergeDefaultStyle(css)) {
+ DocumentUndo::done(_desktop->getDocument(), _("Text: Change alignment"), INKSCAPE_ICON("draw-text"));
+ }
+ sp_repr_css_attr_unref (css);
+
+ desktop->getCanvas()->grab_focus();
+
+ _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;
+ }
+ }
+
+ if (mergeDefaultStyle(css)) {
+ DocumentUndo::done(_desktop->getDocument(), _("Text: Change writing mode"), INKSCAPE_ICON("draw-text"));
+ }
+ sp_repr_css_attr_unref (css);
+
+ _desktop->getCanvas()->grab_focus();
+
+ _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;
+ }
+ }
+
+ if (mergeDefaultStyle(css)) {
+ DocumentUndo::done(_desktop->getDocument(), _("Text: Change orientation"), INKSCAPE_ICON("draw-text"));
+ }
+ sp_repr_css_attr_unref (css);
+ _desktop->canvas->grab_focus();
+
+ _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;
+ }
+ }
+
+ if (mergeDefaultStyle(css)) {
+ DocumentUndo::done(_desktop->getDocument(), _("Text: Change direction"), INKSCAPE_ICON("draw-text"));
+ }
+ sp_repr_css_attr_unref (css);
+
+ _desktop->getCanvas()->grab_focus();
+
+ _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 = _desktop;
+ // Get user selected unit and save as preference
+ Unit const *unit = _tracker->getActiveUnit();
+ // @Tav same disabled unit
+ g_return_if_fail(unit != nullptr);
+
+ // 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 {
+ auto parent = itemlist.front();
+ 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)) {
+ auto child = 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) {
+ auto text = cast<SPText>(i);
+ auto flowtext = 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) {
+ auto text = cast<SPText>(i);
+ auto flowtext = cast<SPFlowtext>(i);
+ if (text || flowtext) {
+ (i)->updateRepr();
+ }
+ }
+ if (!_outer) {
+ prepare_inner();
+ }
+ DocumentUndo::maybeDone(desktop->getDocument(), "ttb:line-height", _("Text: Change line-height"), INKSCAPE_ICON("draw-text"));
+ }
+
+ mergeDefaultStyle(css);
+
+ sp_repr_css_attr_unref (css);
+
+ _freeze = false;
+}
+
+/**
+ * Merge the style into either the tool or the desktop style depending on
+ * which one the user has decided to use in the preferences.
+ *
+ * @returns true if style was set to an object.
+ */
+bool TextToolbar::mergeDefaultStyle(SPCSSAttr *css)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ // If no selected objects, set default.
+ SPStyle query(_desktop->getDocument());
+ int result_numbers = sp_desktop_query_style(_desktop, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS);
+ if (result_numbers == QUERY_STYLE_NOTHING) {
+ prefs->mergeStyle("/tools/text/style", css);
+ }
+ // This updates the global style
+ sp_desktop_set_style (_desktop, css, true, true);
+ return result_numbers != QUERY_STYLE_NOTHING;
+}
+
+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;
+ } else {
+ _lineheight_unit = temp_length.unit;
+ }
+
+ // Read current line height value
+ double line_height = _line_height_adj->get_value();
+ SPDesktop *desktop = _desktop;
+ Inkscape::Selection *selection = desktop->getSelection();
+ auto itemlist = selection->items();
+
+ // Convert between units
+ double font_size = 0;
+ double doc_scale = 1;
+ int count = 0;
+
+ for (auto i : itemlist) {
+ auto text = cast<SPText>(i);
+ auto flowtext = 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 (count > 0) {
+ font_size /= count;
+ } else {
+ // ideally use default font-size.
+ 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.
+ SPItem *parent = itemlist.empty() ? nullptr : itemlist.front();
+ SPStyle *parent_style = nullptr;
+ if (parent) {
+ parent_style = parent->style;
+ }
+ bool inside = false;
+ if (_outer) {
+ if (!selection->singleItem() || !parent_style || parent_style->line_height.computed != 0) {
+ for (auto i = itemlist.begin(); i != itemlist.end(); ++i) {
+ if (is<SPText>(*i) || is<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 {
+ inside = true;
+ }
+ }
+ if (!_outer || inside) {
+ 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)) {
+ auto child = 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) {
+ auto text = cast<SPText>(i);
+ auto flowtext = 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) {
+ auto text = cast<SPText>(i);
+ auto flowtext = cast<SPFlowtext>(i);
+ if (text || flowtext) {
+ (i)->updateRepr();
+ }
+ }
+ if (_outer) {
+ prepare_inner();
+ }
+ DocumentUndo::maybeDone(_desktop->getDocument(), "ttb:line-height", _("Text: Change line-height unit"), INKSCAPE_ICON("draw-text"));
+ }
+
+ mergeDefaultStyle(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(_desktop->getSelection());
+}
+
+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 (mergeDefaultStyle(css)) {
+ DocumentUndo::maybeDone(_desktop->getDocument(), "ttb:word-spacing", _("Text: Change word-spacing"), INKSCAPE_ICON("draw-text"));
+ }
+
+ 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 (mergeDefaultStyle(css)) {
+ DocumentUndo::maybeDone(_desktop->getDocument(), "ttb:letter-spacing", _("Text: Change letter-spacing"), INKSCAPE_ICON("draw-text"));
+ }
+
+ 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(_desktop->event_context) ) {
+ Inkscape::UI::Tools::TextTool *const tc = SP_TEXT_CONTEXT(_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, _desktop, delta_dx );
+ modmade = true;
+ }
+ }
+ }
+
+ if(modmade) {
+ // Save for undo
+ DocumentUndo::maybeDone(_desktop->getDocument(), "ttb:dx", _("Text: Change dx (kern)"), INKSCAPE_ICON("draw-text"));
+ }
+ _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(_desktop->event_context) ) {
+ Inkscape::UI::Tools::TextTool *const tc = SP_TEXT_CONTEXT(_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, _desktop, delta_dy );
+ modmade = true;
+ }
+ }
+ }
+
+ if(modmade) {
+ // Save for undo
+ DocumentUndo::maybeDone(_desktop->getDocument(), "ttb:dy", _("Text: Change dy"), INKSCAPE_ICON("draw-text"));
+ }
+
+ _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(_desktop->event_context) ) {
+ Inkscape::UI::Tools::TextTool *const tc = SP_TEXT_CONTEXT(_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, _desktop, delta_deg );
+ modmade = true;
+ }
+ }
+ }
+
+ // Save for undo
+ if(modmade) {
+ DocumentUndo::maybeDone(_desktop->getDocument(), "ttb:rotate", _("Text: Change rotate"), INKSCAPE_ICON("draw-text"));
+ }
+
+ _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 = _desktop;
+ SPDocument *document = _desktop->getDocument();
+ 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(_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;
+ std::vector<SPItem *> to_work;
+ for (auto i : itemlist) {
+ auto text = cast<SPText>(i);
+ auto flowtext = 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 };
+ auto parent = 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);
+ }
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ /*
+ * 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.
+ if (prefs->getBool("/tools/text/usecurrent")) {
+ query.mergeCSS(sp_desktop_get_style(desktop, true));
+ } else {
+ 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)
+
+ 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);
+ }
+ if (!size && _text_style_from_prefs) {
+ size = sp_style_css_size_px_to_units(query.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 (!height && _text_style_from_prefs) {
+ height = query.line_height.value;
+ line_height_unit = query.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(_desktop->event_context) ) {
+ Inkscape::UI::Tools::TextTool *const tc = SP_TEXT_CONTEXT(_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->connect_text_cursor_moved([=](void* sender, Inkscape::UI::Tools::TextTool* tool){
+ subselection_changed(tool);
+ });
+ this->_sub_active_item = nullptr;
+ this->_cusor_numbers = 0;
+ 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(_desktop->event_context)) {
+ Inkscape::UI::Tools::TextTool *const tc = SP_TEXT_CONTEXT(_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 parses the just created line height in one or more lines of a text subselection.
+* It can describe 2 kinds of input because when we store a text element we apply a fallback that change
+* structure. This visually is not reflected but user maybe want to change a part of this subselection
+* once the fallback is created, so we need more complex logic here to fill the gap.
+* Basically, we have a line height changed in the new wrapper element/s between wrap_start and wrap_end.
+* These variables store starting iterator of first char in line and last char in line in a subselection.
+* These elements are styled well but we can have orphaned text nodes before and after the subselection.
+* So, normally 3 elements are inside a container as direct child of a text element.
+* We need to apply the container style to the optional first and last text nodes,
+* wrapping into a new element that gets the container style (this is not part to the sub-selection).
+* After wrapping, we unindent all children of the container and remove the container.
+*
+*/
+void TextToolbar::prepare_inner()
+{
+ Inkscape::UI::Tools::TextTool *const tc = SP_TEXT_CONTEXT(_desktop->event_context);
+ if (!tc) {
+ return;
+ }
+ Inkscape::Text::Layout *layout = const_cast<Inkscape::Text::Layout *>(te_get_layout(tc->text));
+ if (!layout) {
+ return;
+ }
+ auto doc = _desktop->getDocument();
+ auto spobject = tc->text;
+ auto spitem = tc->text;
+ auto text = cast<SPText>(tc->text);
+ auto flowtext = 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 element
+ // and wrap it into a tspan elements as inkscape do.
+ if (text) {
+ bool changed = false;
+ std::vector<SPObject *> childs = spitem->childList(false);
+ for (auto child : childs) {
+ auto spstring = 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());
+ changed = true;
+ }
+ }
+ }
+ if (changed) {
+ // proper rebuild happens later,
+ // this just updates layout to use now, avoids use after free
+ text->rebuildLayout();
+ }
+ }
+
+ std::vector<SPObject *> containers;
+ {
+ // populate `containers` with objects that will be modified.
+
+ // Temporarily remove the shape so Layout calculates
+ // the position of wrap_end and wrap_start, even if
+ // one of these are hidden because the previous line height was changed
+ if (text) {
+ text->hide_shape_inside();
+ } else if (flowtext) {
+ flowtext->fix_overflow_flowregion(false);
+ }
+ SPObject *rawptr_start = nullptr;
+ SPObject *rawptr_end = nullptr;
+ layout->validateIterator(&wrap_start);
+ layout->validateIterator(&wrap_end);
+ layout->getSourceOfCharacter(wrap_start, &rawptr_start);
+ layout->getSourceOfCharacter(wrap_end, &rawptr_end);
+ if (text) {
+ text->show_shape_inside();
+ } else if (flowtext) {
+ flowtext->fix_overflow_flowregion(true);
+ }
+ if (!rawptr_start || !rawptr_end) {
+ return;
+ }
+
+ // Loop through parents of start and end till we reach
+ // first children of the text element.
+ // Get all objects between start and end (inclusive)
+ SPObject *start = rawptr_start;
+ SPObject *end = rawptr_end;
+ while (start->parent != spobject) {
+ start = start->parent;
+ }
+ while (end->parent != spobject) {
+ end = end->parent;
+ }
+
+ while (start && start != end) {
+ containers.push_back(start);
+ start = start->getNext();
+ }
+ if (start) {
+ containers.push_back(start);
+ }
+ }
+
+ for (auto container : containers) {
+ Inkscape::XML::Node *prevchild = container->getRepr();
+ std::vector<SPObject*> childs = container->childList(false);
+ for (auto child : childs) {
+ auto spstring = cast<SPString>(child);
+ auto flowtspan = cast<SPFlowtspan>(child);
+ auto tspan = cast<SPTSpan>(child);
+ // we need to upper all flowtspans to container level
+ // to do 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
+ // maybe we can move directly but it is safer for me to duplicate,
+ // inject into the new element and delete original
+ for (auto fts_child : fts_childs) {
+ // is this check necessary?
+ 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 different if in a text or flowtext.
+ // wrap a duplicate of the element and unindent after the prevchild
+ // and finally 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 = _desktop->getDocument();
+ 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::cerr << "TextToolbar::unindent_node error: node has no (grand)parent, nothing done.\n";
+ return repr;
+}
+
+void TextToolbar::display_font_collections()
+{
+ for (auto row : font_collections_list->get_children()) {
+ if (row) {
+ font_collections_list->remove(*row);
+ }
+ }
+
+ FontCollections *font_collections = Inkscape::FontCollections::get();
+
+ // Insert system collections.
+ for(auto const& col: font_collections->get_collections(true)) {
+ auto btn = Gtk::make_managed<Gtk::CheckButton>(col);
+ btn->set_margin_bottom(2);
+ btn->set_active(font_collections->is_collection_selected(col));
+ btn->signal_toggled().connect([=](){
+ // toggle font system collection
+ font_collections->update_selected_collections(col);
+ });
+// g_message("tag: %s", tag.display_name.c_str());
+ auto row = Gtk::make_managed<Gtk::ListBoxRow>();
+ row->set_can_focus(false);
+ row->add(*btn);
+ row->show_all();
+ font_collections_list->append(*row);
+ }
+
+ // Insert row separator.
+ auto sep = Gtk::manage(new Gtk::Separator());
+ sep->set_margin_bottom(2);
+ auto sep_row = Gtk::make_managed<Gtk::ListBoxRow>();
+ sep_row->set_can_focus(false);
+ sep_row->add(*sep);
+ sep_row->show_all();
+ font_collections_list->append(*sep_row);
+
+ // Insert user collections.
+ for (auto const& col: font_collections->get_collections()) {
+ auto btn = Gtk::make_managed<Gtk::CheckButton>(col);
+ btn->set_margin_bottom(2);
+ btn->set_active(font_collections->is_collection_selected(col));
+ btn->signal_toggled().connect([=](){
+ // toggle font collection
+ font_collections->update_selected_collections(col);
+ });
+// g_message("tag: %s", tag.display_name.c_str());
+ auto row = Gtk::make_managed<Gtk::ListBoxRow>();
+ row->set_can_focus(false);
+ row->add(*btn);
+ row->show_all();
+ font_collections_list->append(*row);
+ }
+}
+
+void TextToolbar::on_fcm_button_pressed()
+{
+ // Inkscape::UI::Dialog::FontCollectionsManager::getInstance();
+ if(auto desktop = SP_ACTIVE_DESKTOP) {
+ if (auto container = desktop->getContainer()) {
+ container->new_floating_dialog("FontCollections");
+ }
+ }
+}
+
+void TextToolbar::on_reset_button_pressed()
+{
+ FontCollections *font_collections = Inkscape::FontCollections::get();
+ font_collections->clear_selected_collections();
+
+ Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+ font_lister->init_font_families();
+ font_lister->init_default_styles();
+
+ SPDocument *document = _desktop->getDocument();
+
+ if(!document) {
+ return;
+ }
+
+ font_lister->add_document_fonts_at_top(document);
+}
+
+void TextToolbar::subselection_changed(Inkscape::UI::Tools::TextTool* tc)
+{
+#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;
+ }
+ 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);
+ if (start_selection == end_selection) {
+ this->_outer = true;
+ gint counter = 0;
+ for (auto child : tc->text->childList(false)) {
+ auto item = 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(_desktop->getDocument());
+ _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(_desktop, &_query_cursor, QUERY_STYLE_PROPERTY_FONTNUMBERS);
+ tc->text_sel_start = start_selection;
+ wrap_start = tc->text_sel_start;
+ wrap_end = tc->text_sel_end;
+ wrap_start.thisStartOfLine();
+ wrap_end.thisEndOfLine();
+ _updating = false;
+ break;
+ }
+ ++counter;
+ }
+ selection_changed(nullptr);
+ } else if ((start_selection == start && end_selection == end) ||
+ (start_selection == end && end_selection == start)) {
+ // 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..b0c6186
--- /dev/null
+++ b/src/ui/toolbar/text-toolbar.h
@@ -0,0 +1,158 @@
+// 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/listbox.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;
+class TextTool;
+}
+
+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;
+ Gtk::ListBox* font_collections_list;
+
+ 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;
+ auto_connection fc_changed_selection;
+ auto_connection fc_update;
+ 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(Inkscape::UI::Tools::TextTool* texttool);
+ void watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec);
+ void set_sizes(int unit);
+ void display_font_collections();
+ void on_fcm_button_pressed();
+ void on_reset_button_pressed();
+ Inkscape::XML::Node *unindent_node(Inkscape::XML::Node *repr, Inkscape::XML::Node *before);
+ bool mergeDefaultStyle(SPCSSAttr *css);
+
+ 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..c15a4ca
--- /dev/null
+++ b/src/ui/toolbar/toolbar.cpp
@@ -0,0 +1,84 @@
+// 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"
+
+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 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..bbbd7f0
--- /dev/null
+++ b/src/ui/toolbar/toolbar.h
@@ -0,0 +1,66 @@
+// 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);
+ 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..ed840cd
--- /dev/null
+++ b/src/ui/toolbar/tweak-toolbar.cpp
@@ -0,0 +1,346 @@
+// 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/canvas.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(desktop->canvas);
+ _width_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TweakToolbar::width_value_changed));
+ 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(desktop->canvas);
+ _force_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TweakToolbar::force_value_changed));
+ 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(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..d03d783
--- /dev/null
+++ b/src/ui/toolbar/zoom-toolbar.cpp
@@ -0,0 +1,67 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Zoom aux toolbar: Temp until we convert all toolbars to ui files with Gio::Actions.
+ */
+/* Authors:
+ * Tavmjong Bah <tavmjong@free.fr>
+
+ * Copyright (C) 2019 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm.h>
+
+#include "zoom-toolbar.h"
+
+#include "desktop.h"
+#include "io/resource.h"
+
+using Inkscape::IO::Resource::UIS;
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+
+GtkWidget *
+ZoomToolbar::create(SPDesktop *desktop)
+{
+ Glib::ustring zoom_toolbar_builder_file = get_filename(UIS, "toolbar-zoom.ui");
+ auto builder = Gtk::Builder::create();
+ try
+ {
+ builder->add_from_file(zoom_toolbar_builder_file);
+ }
+ catch (const Glib::Error& ex)
+ {
+ std::cerr << "ZoomToolbar: " << zoom_toolbar_builder_file.raw() << " file not read! " << ex.what().raw() << std::endl;
+ }
+
+ Gtk::Toolbar* toolbar = nullptr;
+ builder->get_widget("zoom-toolbar", toolbar);
+ if (!toolbar) {
+ std::cerr << "InkscapeWindow: Failed to load zoom toolbar!" << std::endl;
+ return nullptr;
+ }
+
+ toolbar->reference(); // Or it will be deleted when builder is destroyed since we haven't added
+ // it to a container yet. This probably causes a memory leak but we'll
+ // fix it when all toolbars are converted to use Gio::Actions.
+
+ 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..e3cfd29
--- /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 {
+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/arc-tool.cpp b/src/ui/tools/arc-tool.cpp
new file mode 100644
index 0000000..cd1c036
--- /dev/null
+++ b/src/ui/tools/arc-tool.cpp
@@ -0,0 +1,454 @@
+// 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 "include/macros.h"
+
+#include "object/sp-ellipse.h"
+#include "object/sp-namedview.h"
+
+#include "ui/icon-names.h"
+#include "ui/modifiers.h"
+#include "ui/tools/arc-tool.h"
+#include "ui/shape-editor.h"
+#include "ui/tools/tool-base.h"
+
+#include "xml/repr.h"
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+ArcTool::ArcTool(SPDesktop *desktop)
+ : ToolBase(desktop, "/tools/shapes/arc", "arc.svg")
+ , arc(nullptr)
+{
+ Inkscape::Selection *selection = desktop->getSelection();
+
+ this->shape_editor = new ShapeEditor(desktop);
+
+ SPItem *item = 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();
+ }
+}
+
+ArcTool::~ArcTool()
+{
+ ungrabCanvasEvents();
+ this->finishItem();
+ this->sel_changed_connection.disconnect();
+
+ 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());
+}
+
+
+bool ArcTool::item_handler(SPItem* item, GdkEvent* event) {
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 1) {
+ this->setup_for_drag_start(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) {
+ dragging = true;
+
+ this->center = this->setup_for_drag_start(event);
+
+ /* Snap center */
+ SnapManager &m = _desktop->namedview->snap_manager;
+ m.setup(_desktop);
+ m.freeSnapReturnByRef(this->center, Inkscape::SNAPSOURCE_NODE_HANDLE);
+
+ grabCanvasEvents();
+
+ handled = true;
+ m.unSetup();
+ }
+ break;
+ case GDK_MOTION_NOTIFY:
+ if (dragging && (event->motion.state & GDK_BUTTON1_MASK)) {
+ 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) {
+ dragging = false;
+ this->discard_delayed_snap_event();
+
+ if (arc) {
+ // 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 if (!selection->includes(this->item_to_select)) {
+ 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;
+ }
+ ungrabCanvasEvents();
+ 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;
+ this->discard_delayed_snap_event();
+ // if drawing, cancel, otherwise pass it up for deselecting
+ this->cancel();
+ handled = true;
+ }
+ break;
+
+ case GDK_KEY_space:
+ if (dragging) {
+ ungrabCanvasEvents();
+ dragging = false;
+ this->discard_delayed_snap_event();
+
+ 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);
+
+ auto layer = currentLayer();
+ this->arc = cast<SPGenericEllipse>(layer->appendChildRepr(repr));
+ Inkscape::GC::release(repr);
+ this->arc->transform = layer->i2doc_affine().inverse();
+ this->arc->updateRepr();
+ }
+
+ auto confine = Modifiers::Modifier::get(Modifiers::Type::TRANS_CONFINE)->active(state);
+ // Third is weirdly wrong, surely incrememnts should do something else.
+ auto circle_edge = Modifiers::Modifier::get(Modifiers::Type::TRANS_INCREMENT)->active(state);
+
+ Geom::Rect r = Inkscape::snap_rectangular_box(_desktop, this->arc, pt, this->center, state);
+
+ Geom::Point dir = r.dimensions() / 2;
+
+
+ if (circle_edge) {
+ /* With Alt let the ellipse pass through the mouse pointer */
+ Geom::Point c = r.midpoint();
+
+ if (!confine) {
+ 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->getSelection()->set(this->arc);
+
+ DocumentUndo::done(_desktop->getDocument(), _("Create ellipse"), INKSCAPE_ICON("draw-ellipse"));
+
+ this->arc = nullptr;
+ }
+}
+
+void ArcTool::cancel() {
+ _desktop->getSelection()->clear();
+ ungrabCanvasEvents();
+
+ 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;
+
+ 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..312f943
--- /dev/null
+++ b/src/ui/tools/arc-tool.h
@@ -0,0 +1,76 @@
+// 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(SPDesktop *desktop);
+ ~ArcTool() override;
+
+ bool root_handler(GdkEvent* event) override;
+ bool item_handler(SPItem* item, GdkEvent* event) 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/booleans-builder.cpp b/src/ui/tools/booleans-builder.cpp
new file mode 100644
index 0000000..8fd027a
--- /dev/null
+++ b/src/ui/tools/booleans-builder.cpp
@@ -0,0 +1,271 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Boolean tool shape builder.
+ *
+ *//*
+ * Authors:
+ * Martin Owens
+ *
+ * Copyright (C) 2022 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "booleans-builder.h"
+
+#include "actions/actions-undo-document.h"
+#include "display/control/canvas-item-group.h"
+#include "display/control/canvas-item-bpath.h"
+#include "object/object-set.h"
+#include "object/sp-item.h"
+#include "object/sp-namedview.h"
+#include "style.h"
+#include "ui/widget/canvas.h"
+#include "svg/svg.h"
+
+namespace Inkscape {
+
+static constexpr std::array<uint32_t, 6> fill_lite = {0x00000055, 0x0291ffff, 0x8eceffff, 0x0291ffff, 0xf299d6ff, 0xff0db3ff};
+static constexpr std::array<uint32_t, 6> fill_dark = {0xffffff55, 0x0291ffff, 0x8eceffff, 0x0291ffff, 0xf299d6ff, 0xff0db3ff};
+
+BooleanBuilder::BooleanBuilder(ObjectSet *set, bool flatten)
+ : _set(set)
+{
+ // Current state of all the items
+ _work_items = (flatten ? SubItem::build_flatten : SubItem::build_mosaic)(set->items_vector());
+
+ auto root = _set->desktop()->getCanvas()->get_canvas_item_root();
+ _group = make_canvasitem<CanvasItemGroup>(root);
+
+ auto nv = _set->desktop()->getNamedView();
+ desk_modified_connection = nv->connectModified([=](SPObject *obj, guint flags) {
+ redraw_items();
+ });
+ redraw_items();
+}
+
+BooleanBuilder::~BooleanBuilder() = default;
+
+/**
+ * Control the visual appearence of this particular bpath
+ */
+void BooleanBuilder::redraw_item(CanvasItemBpath &bpath, bool selected, TaskType task)
+{
+ int i = (int)task * 2 + (int)selected;
+ bpath.set_fill(_dark ? fill_dark[i] : fill_lite[i], SP_WIND_RULE_POSITIVE);
+ bpath.set_stroke(task == TaskType::NONE ? 0x000000dd : 0xffffffff);
+ bpath.set_stroke_width(task == TaskType::NONE ? 1.0 : 3.0);
+}
+
+/**
+ * Update to visuals with the latest subitem list.
+ */
+void BooleanBuilder::redraw_items()
+{
+ auto nv = _set->desktop()->getNamedView();
+ _dark = SP_RGBA32_LUMINANCE(nv->desk_color) < 100;
+
+ _screen_items.clear();
+
+ for (auto &subitem : _work_items) {
+ // Construct BPath from each subitem!
+ auto bpath = make_canvasitem<Inkscape::CanvasItemBpath>(_group.get(), subitem->get_pathv(), false);
+ redraw_item(*bpath, subitem->getSelected(), TaskType::NONE);
+ _screen_items.push_back({ subitem, std::move(bpath), true });
+ }
+
+ // Selectively handle the undo actions being enabled / disabled
+ enable_undo_actions(_set->document(), _undo.size(), _redo.size());
+}
+
+ItemPair *BooleanBuilder::get_item(const Geom::Point &point)
+{
+ for (auto &pair : _screen_items) {
+ if (pair.vis->contains(point, 2.0))
+ return &pair;
+ }
+ return nullptr;
+}
+
+/**
+ * Highlight any shape under the mouse at this point.
+ */
+bool BooleanBuilder::highlight(const Geom::Point &point, bool add)
+{
+ if (has_task())
+ return true;
+
+ bool done = false;
+ for (auto &si : _screen_items) {
+ bool hover = !done && si.vis->contains(point, 2.0);
+ redraw_item(*si.vis, si.work->getSelected(), hover ? (add ? TaskType::ADD : TaskType::DELETE) : TaskType::NONE);
+ if (hover)
+ si.vis->raise_to_top();
+ done = done || hover;
+ }
+ return done;
+}
+
+/**
+ * Select the shape under the cursor
+ */
+bool BooleanBuilder::task_select(const Geom::Point &point, bool add_task)
+{
+ if (has_task())
+ task_cancel();
+ if (auto si = get_item(point)) {
+ _add_task = add_task;
+ _work_task = std::make_shared<SubItem>(*si->work);
+ _work_task->setSelected(true);
+ _screen_task = make_canvasitem<Inkscape::CanvasItemBpath>(_group.get(), _work_task->get_pathv(), false);
+ redraw_item(*_screen_task, true, add_task ? TaskType::ADD : TaskType::DELETE);
+ si->vis->hide();
+ si->visible = false;
+ redraw_item(*si->vis, false, TaskType::NONE);
+ return true;
+ }
+ return false;
+}
+
+bool BooleanBuilder::task_add(const Geom::Point &point)
+{
+ if (!has_task())
+ return false;
+ if (auto si = get_item(point)) {
+ // Invisible items are already processed.
+ if (si->visible) {
+ si->vis->hide();
+ si->visible = false;
+ *_work_task += *si->work;
+ _screen_task->set_bpath(_work_task->get_pathv(), false);
+ return true;
+ }
+ }
+ return false;
+}
+
+void BooleanBuilder::task_cancel()
+{
+ _work_task.reset();
+ _screen_task.reset();
+ for (auto &si : _screen_items) {
+ si.vis->show();
+ si.visible = true;
+ }
+}
+
+void BooleanBuilder::task_commit()
+{
+ if (!has_task())
+ return;
+
+ // Manage undo/redo
+ _undo.emplace_back(std::move(_work_items));
+ _redo.clear();
+
+ // A. Delete all items from _work_items that aren't visible
+ _work_items.clear();
+ for (auto &si : _screen_items) {
+ if (si.visible) {
+ _work_items.emplace_back(si.work);
+ }
+ }
+ if (_add_task) {
+ // B. Add _work_task to _work_items for union tasks
+ _work_items.emplace_back(std::move(_work_task));
+ }
+
+ // C. Reset everything
+ redraw_items();
+ _work_task.reset();
+ _screen_task.reset();
+}
+
+/**
+ * Commit the changes to the document (finish)
+ */
+std::vector<SPObject *> BooleanBuilder::shape_commit(bool all)
+{
+ std::vector<SPObject *> ret;
+ auto doc = _set->document();
+ auto items = _set->items_vector();
+
+ // Only commit anything if we have changes, return selection.
+ if (!has_changes() && !all) {
+ ret.insert(ret.begin(), items.begin(), items.end());
+ return ret;
+ }
+
+ // Count number of selected items.
+ int selected = 0;
+ for (auto const &subitem : _work_items) {
+ selected += (int)subitem->getSelected();
+ }
+
+ for (auto const &subitem : _work_items) {
+ // Either this object is selected, or no objects are selected at all.
+ if (!subitem->getSelected() && selected)
+ continue;
+ auto item = subitem->get_item();
+ auto style = subitem->getStyle();
+ // For the rare occasion the user generates from a hole (no item)
+ if (!item) {
+ item = *items.begin();
+ style = item->style;
+ }
+ if (!item) {
+ g_warning("Can't generate itemless object in boolean-builder.");
+ continue;
+ }
+ auto parent = cast<SPItem>(item->parent);
+
+ Inkscape::XML::Node *repr = doc->getReprDoc()->createElement("svg:path");
+ repr->setAttribute("d", sp_svg_write_path(subitem->get_pathv() * parent->dt2i_affine()));
+ repr->setAttribute("style", style->writeIfDiff(parent->style));
+ parent->getRepr()->addChild(repr, item->getRepr());
+ ret.emplace_back(doc->getObjectByRepr(repr));
+ }
+ _work_items.clear();
+
+ for (auto item : items) {
+ sp_object_ref(item, nullptr);
+ item->deleteObject(true, true);
+ sp_object_unref(item, nullptr);
+ }
+ return ret;
+}
+
+void BooleanBuilder::undo()
+{
+ if (_undo.empty())
+ return;
+
+ // Cancel any task;
+ task_cancel();
+
+ // Shuffle the undo stack
+ _redo.emplace_back(std::move(_work_items));
+ _work_items = std::move(_undo.back());
+ _undo.pop_back();
+
+ // Redraw the screen items
+ redraw_items();
+}
+
+void BooleanBuilder::redo()
+{
+ if (_redo.empty())
+ return;
+
+ // Cancel any task;
+ task_cancel();
+
+ // Shuffle the undo stack
+ _undo.emplace_back(std::move(_work_items));
+ _work_items = std::move(_redo.back());
+ _redo.pop_back();
+
+ // Redraw the screen items
+ redraw_items();
+}
+
+} // namespace Inkscape
diff --git a/src/ui/tools/booleans-builder.h b/src/ui/tools/booleans-builder.h
new file mode 100644
index 0000000..33f4404
--- /dev/null
+++ b/src/ui/tools/booleans-builder.h
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ *//*
+ * Authors:
+ * Martin Owens
+ *
+ * Copyright (C) 2022 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_TOOLS_BOOLEANS_BUILDER_H
+#define INKSCAPE_UI_TOOLS_BOOLEANS_BUILDER_H
+
+#include <vector>
+#include <optional>
+#include "helper/auto-connection.h"
+
+#include "booleans-subitems.h"
+#include "helper/auto-connection.h"
+#include "display/control/canvas-item-ptr.h"
+
+class SPDesktop;
+class SPDocument;
+class SPObject;
+
+namespace Inkscape {
+
+class CanvasItemGroup;
+class CanvasItemBpath;
+class ObjectSet;
+
+using VisualItem = CanvasItemPtr<CanvasItemBpath>;
+struct ItemPair
+{
+ WorkItem work;
+ VisualItem vis;
+ bool visible;
+};
+
+enum class TaskType
+{
+ NONE,
+ ADD,
+ DELETE
+};
+
+class BooleanBuilder
+{
+public:
+ BooleanBuilder(ObjectSet *obj, bool flatten = false);
+ ~BooleanBuilder();
+
+ void undo();
+ void redo();
+
+ std::vector<SPObject *> shape_commit(bool all = false);
+ ItemPair *get_item(const Geom::Point &point);
+ bool task_select(const Geom::Point &point, bool add_task = true);
+ bool task_add(const Geom::Point &point);
+ void task_cancel();
+ void task_commit();
+ bool has_items() const { return !_work_items.empty(); }
+ bool has_task() const { return (bool)_work_task; }
+ bool has_changes() const { return !_undo.empty(); }
+ bool highlight(const Geom::Point &point, bool add_task = true);
+
+private:
+ ObjectSet *_set;
+ CanvasItemPtr<CanvasItemGroup> _group;
+
+ std::vector<WorkItem> _work_items;
+ std::vector<ItemPair> _screen_items;
+ WorkItem _work_task;
+ VisualItem _screen_task;
+ bool _add_task;
+ bool _dark = false;
+
+ // Lists of _work_items which can be brought back.
+ std::vector<std::vector<WorkItem>> _undo;
+ std::vector<std::vector<WorkItem>> _redo;
+
+ auto_connection desk_modified_connection;
+
+ void redraw_item(CanvasItemBpath &bpath, bool selected, TaskType task);
+ void redraw_items();
+};
+
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_TOOLS_BOOLEANS_BUILDER_H
diff --git a/src/ui/tools/booleans-subitems.cpp b/src/ui/tools/booleans-subitems.cpp
new file mode 100644
index 0000000..29309f8
--- /dev/null
+++ b/src/ui/tools/booleans-subitems.cpp
@@ -0,0 +1,356 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * SubItem controls each fractured piece and links it to its original items.
+ *
+ *//*
+ * Authors:
+ * Martin Owens
+ * PBS
+ *
+ * Copyright (C) 2022 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <numeric>
+#include <utility>
+#include <random>
+
+#include <boost/range/adaptor/reversed.hpp>
+
+#include "booleans-subitems.h"
+#include "helper/geom-pathstroke.h"
+#include "livarot/LivarotDefs.h"
+#include "livarot/Shape.h"
+#include "object/sp-shape.h"
+#include "object/sp-text.h"
+#include "object/sp-use.h"
+#include "object/sp-image.h"
+#include "path/path-boolop.h"
+#include "style.h"
+
+namespace Inkscape {
+
+// Todo: (Wishlist) Remove this function when no longer necessary to remove boolops artifacts.
+static Geom::PathVector clean_pathvector(Geom::PathVector &&pathv)
+{
+ Geom::PathVector result;
+
+ for (auto &path : pathv) {
+ if (path.closed() && !is_path_empty(path)) {
+ result.push_back(std::move(path));
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Union operator, merges two subitems when requested by the user
+ * The left hand side will retain priority for the resulting style
+ * so you should be mindful of how you merge these shapes.
+ */
+SubItem &SubItem::operator+=(SubItem const &other)
+{
+ _paths = sp_pathvector_boolop(_paths, other._paths, bool_op_union, fill_nonZero, fill_nonZero, true);
+ sp_flatten(_paths, fill_nonZero);
+ _paths = clean_pathvector(std::move(_paths));
+ return *this;
+}
+
+using ExtractPathvectorsResult = std::vector<std::pair<Geom::PathVector, SPStyle*>>;
+
+static void extract_pathvectors_recursive(SPItem *item, ExtractPathvectorsResult &result, Geom::Affine const &transform)
+{
+ if (is<SPGroup>(item)) {
+ for (auto &child : item->children | boost::adaptors::reversed) {
+ if (auto child_item = cast<SPItem>(&child)) {
+ extract_pathvectors_recursive(child_item, result, child_item->transform * transform);
+ }
+ }
+ } else if (auto img = cast<SPImage>(item)) {
+ result.emplace_back(img->get_curve()->get_pathvector() * transform, item->style);
+ } else if (auto shape = cast<SPShape>(item)) {
+ if (auto curve = shape->curve()) {
+ result.emplace_back(curve->get_pathvector() * transform, item->style);
+ }
+ } else if (auto text = cast<SPText>(item)) {
+ result.emplace_back(text->getNormalizedBpath().get_pathvector() * transform, item->style);
+ } else if (auto use = cast<SPUse>(item)) {
+ if (use->child) {
+ extract_pathvectors_recursive(use->child, result, use->child->transform * Geom::Translate(use->x.computed, use->y.computed) * transform);
+ }
+ }
+}
+
+// Return all pathvectors found within an item, along with their styles, sorted top-to-bottom.
+static ExtractPathvectorsResult extract_pathvectors(SPItem *item)
+{
+ ExtractPathvectorsResult result;
+ extract_pathvectors_recursive(item, result, item->i2dt_affine());
+ return result;
+}
+
+static FillRule sp_to_livarot(SPWindRule fillrule)
+{
+ return fillrule == SP_WIND_RULE_NONZERO ? fill_nonZero : fill_oddEven;
+}
+
+static double diameter(Geom::PathVector const &path)
+{
+ auto rect = path.boundsExact();
+ if (!rect) {
+ return 1;
+ }
+ return std::hypot(rect->width(), rect->height());
+}
+
+// Cut the given pathvector along the lines into several smaller pathvectors.
+static std::vector<Geom::PathVector> improved_cut(Geom::PathVector const &pathv, Geom::PathVector const &lines)
+{
+ Path patha;
+ patha.LoadPathVector(pathv);
+ patha.ConvertWithBackData(diameter(pathv) * 1e-3);
+
+ Path pathb;
+ pathb.LoadPathVector(lines);
+ pathb.ConvertWithBackData(diameter(lines) * 1e-3);
+
+ Shape shapea;
+ {
+ Shape tmp;
+ patha.Fill(&tmp, 0);
+ shapea.ConvertToShape(&tmp);
+ }
+
+ Shape shapeb;
+ {
+ Shape tmp;
+ bool isline = pathb.pts.size() == 2 && pathb.pts[0].isMoveTo && !pathb.pts[1].isMoveTo;
+ pathb.Fill(&tmp, 1, false, isline);
+ shapeb.ConvertToShape(&tmp, fill_justDont);
+ }
+
+ Shape shape;
+ shape.Booleen(&shapeb, &shapea, bool_op_cut, 1);
+
+ Path path;
+ int num_nesting = 0;
+ int *nesting = nullptr;
+ int *conts = nullptr;
+ {
+ path.SetBackData(false);
+ Path *paths[2] = { &patha, &pathb };
+ shape.ConvertToFormeNested(&path, 2, paths, 1, num_nesting, nesting, conts);
+ }
+
+ int num_paths;
+ auto paths = path.SubPathsWithNesting(num_paths, false, num_nesting, nesting, conts);
+
+ std::vector<Geom::PathVector> result;
+
+ for (int i = 0; i < num_paths; i++) {
+ result.emplace_back(paths[i]->MakePathVector());
+ }
+
+ g_free(paths);
+ g_free(conts);
+ g_free(nesting);
+
+ return result;
+}
+
+/**
+ * Take a list of items and fracture into a list of SubItems ready for
+ * use inside the booleans interactive tool.
+ */
+WorkItems SubItem::build_mosaic(std::vector<SPItem*> &&items)
+{
+ // Sort so that topmost items come first.
+ std::sort(items.begin(), items.end(), [] (auto a, auto b) {
+ return sp_object_compare_position_bool(b, a);
+ });
+
+ // Extract all individual pathvectors within the collection of items,
+ // keeping track of their associated item and style, again sorted topmost-first.
+ using AugmentedItem = std::tuple<Geom::PathVector, SPItem*, SPStyle*>;
+ std::vector<AugmentedItem> augmented;
+
+ for (auto item : items) {
+ // Get the correctly-transformed pathvectors, together with their corresponding styles.
+ auto extracted = extract_pathvectors(item);
+
+ // Append to the list of augmented items.
+ for (auto &[pathv, style] : extracted) {
+ augmented.emplace_back(std::move(pathv), item, style);
+ }
+ }
+
+ // Compute a slightly expanded bounding box, collect together all lines, and cut the former by the latter.
+ Geom::OptRect bounds;
+ Geom::PathVector lines;
+
+ for (auto &[pathv, item, style] : augmented) {
+ bounds |= pathv.boundsExact();
+ for (auto &path : pathv) {
+ lines.push_back(path);
+ }
+ }
+
+ if (!bounds) {
+ return {};
+ }
+
+ constexpr double expansion = 10.0;
+ bounds->expandBy(expansion);
+
+ auto bounds_pathv = Geom::PathVector(Geom::Path(*bounds));
+ auto pieces = improved_cut(bounds_pathv, lines);
+
+ // Construct the SubItems, attempting to guess the corresponding augmented item for each piece.
+ WorkItems result;
+
+ auto gen = std::default_random_engine(std::random_device()());
+ auto ranf = [&] { return std::uniform_real_distribution()(gen); };
+ auto randpt = [&] { return Geom::Point(ranf(), ranf()); };
+
+ for (auto &piece : pieces) {
+ // Skip the big enclosing piece that is touching the outer boundary.
+ if (auto rect = piece.boundsExact()) {
+ if ( Geom::are_near(rect->top(), bounds->top(), expansion / 2)
+ || Geom::are_near(rect->bottom(), bounds->bottom(), expansion / 2)
+ || Geom::are_near(rect->left(), bounds->left(), expansion / 2)
+ || Geom::are_near(rect->right(), bounds->right(), expansion / 2))
+ {
+ continue;
+ }
+ }
+
+ // Remove junk paths that are open and/or tiny.
+ for (auto it = piece.begin(); it != piece.end(); ) {
+ if (!it->closed() || is_path_empty(*it)) {
+ it = piece.erase(it);
+ } else {
+ ++it;
+ }
+ }
+
+ // Skip empty pathvectors.
+ if (piece.empty()) {
+ continue;
+ }
+
+ // Determine the corresponding augmented item.
+ // Fixme: (Wishlist) This is done unreliably and hackily, but livarot/2geom seemingly offer no alternative.
+ std::unordered_map<AugmentedItem*, int> hits;
+
+ auto rect = piece.boundsExact();
+
+ auto add_hit = [&] (Geom::Point const &pt) {
+ // Find an augmented item containing the point.
+ for (auto &aug : augmented) {
+ auto &[pathv, item, style] = aug;
+ auto fill_rule = style->fill_rule.computed;
+ auto winding = pathv.winding(pt);
+ if (fill_rule == SP_WIND_RULE_NONZERO ? winding : winding % 2) {
+ hits[&aug]++;
+ return;
+ }
+ }
+
+ // If none exists, register a background hit.
+ hits[nullptr]++;
+ };
+
+ for (int total_hits = 0, patience = 1000; total_hits < 20 && patience > 0; patience--) {
+ // Attempt to generate a point strictly inside the piece.
+ auto pt = rect->min() + randpt() * rect->dimensions();
+ if (piece.winding(pt)) {
+ add_hit(pt);
+ total_hits++;
+ }
+ }
+
+ // Pick the augmented item with the most hits.
+ AugmentedItem *found = nullptr;
+ int max_hits = 0;
+
+ for (auto &[a, h] : hits) {
+ if (h > max_hits) {
+ max_hits = h;
+ found = a;
+ }
+ }
+
+ // Add the SubItem.
+ auto item = found ? std::get<1>(*found) : nullptr;
+ auto style = found ? std::get<2>(*found) : nullptr;
+ result.emplace_back(std::make_shared<SubItem>(std::move(piece), item, style));
+ }
+
+ return result;
+}
+
+/**
+ * Take a list of items and flatten into a list of SubItems.
+ */
+WorkItems SubItem::build_flatten(std::vector<SPItem*> &&items)
+{
+ // Sort so that topmost items come first.
+ std::sort(items.begin(), items.end(), [] (auto a, auto b) {
+ return sp_object_compare_position_bool(b, a);
+ });
+
+ WorkItems result;
+ Geom::PathVector unioned;
+
+ for (auto item : items) {
+ // Get the correctly-transformed pathvectors, together with their corresponding styles.
+ auto extracted = extract_pathvectors(item);
+
+ for (auto &[pathv, style] : extracted) {
+ // Remove lines.
+ for (auto it = pathv.begin(); it != pathv.end(); ) {
+ if (!it->closed()) {
+ it = pathv.erase(it);
+ } else {
+ ++it;
+ }
+ }
+
+ // Skip pathvectors that are just lines.
+ if (pathv.empty()) {
+ continue;
+ }
+
+ // Flatten the remaining pathvector according to its fill rule.
+ auto fillrule = style->fill_rule.computed;
+ sp_flatten(pathv, sp_to_livarot(fillrule));
+
+ // Remove the union so far from the shape, then add the shape to the union so far.
+ Geom::PathVector uniq;
+
+ if (unioned.empty()) {
+ uniq = pathv;
+ unioned = std::move(pathv);
+ } else {
+ uniq = sp_pathvector_boolop(unioned, pathv, bool_op_diff, fill_nonZero, fill_nonZero, true);
+ unioned = sp_pathvector_boolop(unioned, pathv, bool_op_union, fill_nonZero, fill_nonZero, true);
+ }
+
+ // Add the new SubItem.
+ result.emplace_back(std::make_shared<SubItem>(std::move(uniq), item, style));
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Return true if this subitem contains the give point.
+ */
+bool SubItem::contains(Geom::Point const &pt) const
+{
+ return _paths.winding(pt) % 2 != 0;
+}
+
+} // namespace Inkscape
diff --git a/src/ui/tools/booleans-subitems.h b/src/ui/tools/booleans-subitems.h
new file mode 100644
index 0000000..bbb12f2
--- /dev/null
+++ b/src/ui/tools/booleans-subitems.h
@@ -0,0 +1,71 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ *
+ *//*
+ * Authors:
+ * Martin Owens
+ *
+ * Copyright (C) 2022 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_TOOLS_BOOLEANS_SUBITEMS_H
+#define INKSCAPE_UI_TOOLS_BOOLEANS_SUBITEMS_H
+
+#include <2geom/pathvector.h>
+#include <vector>
+#include <functional>
+
+class SPItem;
+class SPStyle;
+
+namespace Inkscape {
+
+class SubItem;
+using WorkItem = std::shared_ptr<SubItem>;
+using WorkItems = std::vector<WorkItem>;
+
+/**
+ * When an item is broken, each broken part is represented by
+ * the SubItem class. This class hold information such as the
+ * original items it originated from and the paths that it
+ * consists of.
+ **/
+class SubItem
+{
+public:
+
+ SubItem(Geom::PathVector paths, SPItem *item, SPStyle *style)
+ : _paths(std::move(paths))
+ , _item(item)
+ , _style(style)
+ {}
+
+ SubItem(const SubItem &copy)
+ : SubItem(copy._paths, copy._item, copy._style)
+ {}
+
+ SubItem &operator+=(const SubItem &other);
+
+ bool contains(const Geom::Point &pt) const;
+
+ const Geom::PathVector &get_pathv() const { return _paths; }
+ SPItem *get_item() const { return _item; }
+ SPStyle *getStyle() const { return _style; }
+
+ static WorkItems build_mosaic(std::vector<SPItem*> &&items);
+ static WorkItems build_flatten(std::vector<SPItem*> &&items);
+
+ bool getSelected() const { return _selected; }
+ void setSelected(bool selected) { _selected = selected; }
+
+private:
+ Geom::PathVector _paths;
+ SPItem *_item;
+ SPStyle *_style;
+ bool _selected = false;
+};
+
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_TOOLS_BOOLEANS_SUBITEMS_H
diff --git a/src/ui/tools/booleans-tool.cpp b/src/ui/tools/booleans-tool.cpp
new file mode 100644
index 0000000..2b3a82d
--- /dev/null
+++ b/src/ui/tools/booleans-tool.cpp
@@ -0,0 +1,255 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Martin Owens
+ *
+ * Copyright (C) 2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+
+#include "actions/actions-tools.h" // set_active_tool()
+#include "ui/tools/booleans-tool.h"
+#include "ui/tools/booleans-builder.h"
+#include "display/control/canvas-item-group.h"
+#include "display/control/canvas-item-drawing.h"
+
+#include "desktop.h"
+#include "document.h"
+#include "document-undo.h"
+#include "event-log.h"
+#include "include/macros.h"
+#include "selection.h"
+#include "ui/icon-names.h"
+#include "ui/modifiers.h"
+
+using Inkscape::DocumentUndo;
+using Inkscape::Modifiers::Modifier;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+InteractiveBooleansTool::InteractiveBooleansTool(SPDesktop *desktop)
+ : ToolBase(desktop, "/tools/booleans", "select.svg")
+{
+ to_commit = false;
+ change_mode(true);
+ update_status();
+ if (auto selection = desktop->getSelection()) {
+ desktop->setWaitingCursor();
+ boolean_builder = std::make_unique<BooleanBuilder>(selection);
+ desktop->clearWaitingCursor();
+
+ // Any changes to the selection cancel the shape building process
+ _sel_modified = selection->connectModified([=](Selection *sel, int) { shape_cancel(); });
+ _sel_changed = selection->connectChanged([=](Selection *sel) { shape_cancel(); });
+ }
+}
+
+InteractiveBooleansTool::~InteractiveBooleansTool()
+{
+ change_mode(false);
+ _sel_modified.disconnect();
+ _sel_changed.disconnect();
+}
+
+void InteractiveBooleansTool::change_mode(bool setup)
+{
+ _desktop->doc()->get_event_log()->updateUndoVerbs();
+ _desktop->getCanvasPagesBg()->set_visible(!setup);
+ _desktop->getCanvasPagesFg()->set_visible(!setup);
+ _desktop->getCanvasDrawing()->set_visible(!setup);
+}
+
+void InteractiveBooleansTool::switching_away(const std::string &new_tool)
+{
+ if (!new_tool.empty() && boolean_builder && new_tool == "/tools/select" || new_tool == "/tool/nodes") {
+ // Only forcefully commit if we have the user's explicit instruction to do so.
+ if (boolean_builder->has_changes() || to_commit) {
+ _desktop->getSelection()->setList(boolean_builder->shape_commit(true));
+ DocumentUndo::done(_desktop->doc(), "Built Shapes", INKSCAPE_ICON("draw-booleans"));
+ }
+ }
+}
+
+bool InteractiveBooleansTool::is_ready() const {
+ if (!boolean_builder || !boolean_builder->has_items()) {
+ if (_desktop->getSelection()->isEmpty()) {
+ _desktop->showNotice(_("You must select some objects to use the Shape Builder tool."), 5000);
+ } else {
+ _desktop->showNotice(_("The Shape Builder requires regular shapes to be selected."), 5000);
+ }
+ return false;
+ }
+ return true;
+}
+
+void InteractiveBooleansTool::set(const Inkscape::Preferences::Entry& val)
+{
+ Glib::ustring path = val.getEntryName();
+ if (path == "/tools/booleans/mode") {
+ update_status();
+ boolean_builder->task_cancel();
+ }
+}
+
+void InteractiveBooleansTool::shape_commit()
+{
+ to_commit = true;
+ // disconnect so we don't get canceled by accident.
+ _sel_modified.disconnect();
+ _sel_changed.disconnect();
+ set_active_tool(_desktop, "Select");
+}
+
+void InteractiveBooleansTool::shape_cancel()
+{
+ boolean_builder.reset();
+ set_active_tool(_desktop, "Select");
+}
+
+bool InteractiveBooleansTool::root_handler(GdkEvent* event)
+{
+ if (!boolean_builder)
+ return false;
+
+ bool ret = false;
+ bool add = should_add(event->button.state);
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ ret = event_button_press_handler(event);
+ break;
+ case GDK_BUTTON_RELEASE:
+ ret = event_button_release_handler(event);
+ break;
+ case GDK_KEY_PRESS:
+ ret = event_key_press_handler(event);
+ // no-break;
+ case GDK_KEY_RELEASE:
+ add = should_add(Modifiers::add_keyval(event->key.state, event->key.keyval, event->type == GDK_KEY_RELEASE));
+ break;
+ case GDK_MOTION_NOTIFY:
+ ret = event_motion_handler(event, add);
+ break;
+ }
+ if (!ret) {
+ set_cursor(add ? "cursor-union.svg" : "cursor-delete.svg");
+ update_status();
+ }
+ return ret || ToolBase::root_handler(event);
+}
+
+/**
+ * Returns true if the shape builder should add items,
+ * false if shape builder should delete items
+ */
+bool InteractiveBooleansTool::should_add(int state) const
+{
+ auto prefs = Inkscape::Preferences::get();
+ bool pref = prefs->getInt("/tools/booleans/mode", 0) != 0;
+ auto modifier = Modifier::get(Modifiers::Type::BOOL_SHIFT);
+ return pref == modifier->active(state);
+}
+
+void InteractiveBooleansTool::update_status()
+{
+ auto prefs = Inkscape::Preferences::get();
+ bool pref = prefs->getInt("/tools/booleans/mode", 0) == 0;
+ auto modifier = Modifier::get(Modifiers::Type::BOOL_SHIFT);
+ message_context->setF(Inkscape::IMMEDIATE_MESSAGE,
+ (pref ? "<b>Drag</b> over fragments to unite them. <b>Click</b> to create a segment. Hold <b>%s</b> to Subtract."
+ : "<b>Drag</b> over fragments to delete them. <b>Click</b> to delete a segment. Hold <b>%s</b> to Unite."),
+ modifier->get_label().c_str());
+}
+
+bool InteractiveBooleansTool::event_button_press_handler(GdkEvent *event)
+{
+ if (event->button.button == 1) {
+ Geom::Point const button_pt(event->button.x, event->button.y);
+ boolean_builder->task_select(button_pt, should_add(event->button.state));
+ return true;
+
+ } else if (event->button.button == 3) {
+ // right click; do not eat it so that right-click menu can appear, but cancel dragging
+ boolean_builder->task_cancel();
+ }
+
+ return false;
+}
+
+bool InteractiveBooleansTool::event_motion_handler(GdkEvent *event, bool add)
+{
+ Geom::Point const motion_pt(event->motion.x, event->motion.y);
+
+ if ((event->motion.state & GDK_BUTTON1_MASK)) {
+ if (boolean_builder->has_task()) {
+ return boolean_builder->task_add(motion_pt);
+ } else {
+ return boolean_builder->task_select(motion_pt, add);
+ }
+ } else {
+ return boolean_builder->highlight(motion_pt, add);
+ }
+
+ return false;
+}
+
+bool InteractiveBooleansTool::event_button_release_handler(GdkEvent *event)
+{
+ if (event->button.button == 1) {
+ boolean_builder->task_commit();
+ }
+ return true;
+}
+
+bool InteractiveBooleansTool::catch_undo(bool redo) {
+ if (redo) {
+ boolean_builder->redo();
+ } else {
+ boolean_builder->undo();
+ }
+ return true;
+}
+
+bool InteractiveBooleansTool::event_key_press_handler(GdkEvent *event)
+{
+ bool ret = false;
+ switch (get_latin_keyval (&event->key)) {
+ case GDK_KEY_Escape:
+ if (boolean_builder->has_task()) {
+ boolean_builder->task_cancel();
+ } else {
+ shape_cancel();
+ }
+ ret = true;
+ break;
+ case GDK_KEY_Return:
+ case GDK_KEY_KP_Enter:
+ if (boolean_builder->has_task()) {
+ boolean_builder->task_commit();
+ } else {
+ shape_commit();
+ }
+ ret = true;
+ break;
+ case GDK_KEY_z:
+ case GDK_KEY_Z:
+ if (event->key.state & INK_GDK_PRIMARY_MASK) {
+ ret = catch_undo(event->key.state & GDK_SHIFT_MASK);
+ }
+ break;
+
+
+ default:
+ break;
+ }
+
+ return ret;
+}
+
+} // namespace Tools
+} // namespace UI
+} // namespace Inkscape
diff --git a/src/ui/tools/booleans-tool.h b/src/ui/tools/booleans-tool.h
new file mode 100644
index 0000000..9eafb88
--- /dev/null
+++ b/src/ui/tools/booleans-tool.h
@@ -0,0 +1,70 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * A tool for building shapes.
+ */
+/* Authors:
+ * Martin Owens
+ *
+ * Copyright (C) 2022 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_TOOLS_BOOLEANS_TOOL
+#define INKSCAPE_UI_TOOLS_BOOLEANS_TOOL
+
+#include "ui/tools/tool-base.h"
+
+class SPDesktop;
+
+namespace Inkscape {
+class BooleanBuilder;
+
+namespace UI {
+namespace Tools {
+
+class InteractiveBooleansTool : public ToolBase {
+public:
+
+ InteractiveBooleansTool(SPDesktop *desktop);
+ ~InteractiveBooleansTool() override;
+
+ void switching_away(const std::string &new_tool) override;
+
+ // Preferences set
+ void set(const Inkscape::Preferences::Entry& val) override;
+
+ // Undo/redo catching
+ bool catch_undo(bool redo) override;
+
+ // Catch empty selections
+ bool is_ready() const override;
+
+ // Event functions
+ bool root_handler(GdkEvent* event) override;
+
+ void shape_commit();
+ void shape_cancel();
+private:
+ void update_status();
+ void change_mode(bool setup);
+ bool should_add(int state) const;
+
+ bool event_button_press_handler(GdkEvent* event);
+ bool event_button_release_handler(GdkEvent* event);
+ bool event_motion_handler(GdkEvent* event, bool add);
+ bool event_key_press_handler(GdkEvent* event);
+
+ std::unique_ptr<BooleanBuilder> boolean_builder;
+
+ sigc::connection _sel_modified;
+ sigc::connection _sel_changed;
+
+ bool to_commit = false;
+};
+
+} // namespace Tools
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_TOOLS_BOOLEANS_TOOL
diff --git a/src/ui/tools/box3d-tool.cpp b/src/ui/tools/box3d-tool.cpp
new file mode 100644
index 0000000..a9d972c
--- /dev/null
+++ b/src/ui/tools/box3d-tool.cpp
@@ -0,0 +1,570 @@
+// 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 "include/macros.h"
+
+#include "object/box3d-side.h"
+#include "object/box3d.h"
+#include "object/sp-defs.h"
+#include "object/sp-namedview.h"
+
+#include "ui/icon-names.h"
+#include "ui/shape-editor.h"
+#include "ui/tools/box3d-tool.h"
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+Box3dTool::Box3dTool(SPDesktop *desktop)
+ : ToolBase(desktop, "/tools/shapes/3dbox", "box.svg")
+ , _vpdrag(nullptr)
+ , box3d(nullptr)
+ , ctrl_dragged(false)
+ , extruded(false)
+{
+ this->shape_editor = new ShapeEditor(_desktop);
+
+ SPItem *item = desktop->getSelection()->singleItem();
+ if (item) {
+ this->shape_editor->set_item(item);
+ }
+
+ this->sel_changed_connection.disconnect();
+ this->sel_changed_connection = desktop->getSelection()->connectChanged(
+ sigc::mem_fun(*this, &Box3dTool::selection_changed)
+ );
+
+ this->_vpdrag = new Box3D::VPDrag(desktop->getDocument());
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (prefs->getBool("/tools/shapes/selcue")) {
+ this->enableSelectionCue();
+ }
+
+ if (prefs->getBool("/tools/shapes/gradientdrag")) {
+ this->enableGrDrag();
+ }
+}
+
+Box3dTool::~Box3dTool() {
+ ungrabCanvasEvents();
+ this->finishItem();
+ this->sel_changed_connection.disconnect();
+
+ this->enableGrDrag(false);
+
+ delete (this->_vpdrag);
+ this->_vpdrag = nullptr;
+
+ delete this->shape_editor;
+ this->shape_editor = nullptr;
+}
+
+/**
+ * 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
+ _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)
+{
+ auto defs = document->getDefs();
+
+ bool has_persp = false;
+ for (auto &child : defs->children) {
+ if (is<Persp3D>(&child)) {
+ has_persp = true;
+ break;
+ }
+ }
+
+ if (!has_persp) {
+ document->setCurrentPersp3D(Persp3D::create_xml_element (document));
+ }
+}
+
+bool Box3dTool::item_handler(SPItem* item, GdkEvent* event) {
+ gint ret = FALSE;
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ if ( event->button.button == 1) {
+ this->setup_for_drag_start(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) {
+ 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;
+
+ grabCanvasEvents();
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_MOTION_NOTIFY:
+ if (dragging && ( event->motion.state & GDK_BUTTON1_MASK )) {
+ if (!cur_persp) {
+ // Can happen if perspective is deleted while dragging, e.g. on document closure.
+ ret = true;
+ break;
+ }
+ 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) {
+ dragging = false;
+ this->discard_delayed_snap_event();
+
+ if (!this->within_tolerance) {
+ // we've been dragging (or switched tools if !box3d), finish the box
+ if (this->box3d) {
+ _desktop->getSelection()->set(this->box3d); // Updating the selection will send signals to the box3d-toolbar ...
+ }
+ this->finishItem(); // .. but finishItem() will be called from the deconstructor too and shall NOT fire such signals!
+ } 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;
+ ungrabCanvasEvents();
+ }
+ 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:
+ document->getCurrentPersp3D()->rotate_VP (Proj::X, 180 / snaps * y_dir, MOD__ALT(event));
+ DocumentUndo::done(document, _("Change perspective (angle of PLs)"), INKSCAPE_ICON("draw-cuboid"));
+ ret = true;
+ break;
+
+ case GDK_KEY_bracketleft:
+ document->getCurrentPersp3D()->rotate_VP (Proj::X, -180 / snaps * y_dir, MOD__ALT(event));
+ DocumentUndo::done(document, _("Change perspective (angle of PLs)"), INKSCAPE_ICON("draw-cuboid"));
+ ret = true;
+ break;
+
+ case GDK_KEY_parenright:
+ document->getCurrentPersp3D()->rotate_VP (Proj::Y, 180 / snaps * y_dir, MOD__ALT(event));
+ DocumentUndo::done(document, _("Change perspective (angle of PLs)"), INKSCAPE_ICON("draw-cuboid"));
+ ret = true;
+ break;
+
+ case GDK_KEY_parenleft:
+ document->getCurrentPersp3D()->rotate_VP (Proj::Y, -180 / snaps * y_dir, MOD__ALT(event));
+ DocumentUndo::done(document, _("Change perspective (angle of PLs)"), INKSCAPE_ICON("draw-cuboid"));
+ ret = true;
+ break;
+
+ case GDK_KEY_braceright:
+ document->getCurrentPersp3D()->rotate_VP (Proj::Z, 180 / snaps * y_dir, MOD__ALT(event));
+ DocumentUndo::done(document, _("Change perspective (angle of PLs)"), INKSCAPE_ICON("draw-cuboid"));
+ ret = true;
+ break;
+
+ case GDK_KEY_braceleft:
+ document->getCurrentPersp3D()->rotate_VP (Proj::Z, -180 / snaps * y_dir, MOD__ALT(event));
+ DocumentUndo::done(document, _("Change perspective (angle of PLs)"), INKSCAPE_ICON("draw-cuboid"));
+ ret = true;
+ break;
+
+ case GDK_KEY_g:
+ case GDK_KEY_G:
+ if (MOD__SHIFT_ONLY(event)) {
+ _desktop->getSelection()->toGuides();
+ ret = true;
+ }
+ break;
+
+ case GDK_KEY_p:
+ case GDK_KEY_P:
+ if (MOD__SHIFT_ONLY(event)) {
+ if (document->getCurrentPersp3D()) {
+ document->getCurrentPersp3D()->print_debugging_info();
+ }
+ 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) {
+ ungrabCanvasEvents();
+ dragging = false;
+ this->discard_delayed_snap_event();
+ if (!this->within_tolerance) {
+ // we've been dragging (or switched tools if !box3d), finish the box
+ if (this->box3d) {
+ _desktop->getSelection()->set(this->box3d); // Updating the selection will send signals to the box3d-toolbar ...
+ }
+ this->finishItem(); // .. but finishItem() will be called from the deconstructor too and shall NOT fire such signals!
+ }
+ // 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(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 += side->axes_string();
+ 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",
+ side->axes_string());
+ _desktop->applyCurrentOrToolStyle(side, tool_path, false);
+ }
+
+ side->updateRepr(); // calls Box3DSide::write() and updates, e.g., the axes string description
+ }
+
+ this->box3d->set_z_orders();
+ 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(); */
+ }
+
+ g_assert(this->box3d);
+
+ this->box3d->orig_corner0 = this->drag_origin_proj;
+ this->box3d->orig_corner7 = this->drag_ptC_proj;
+
+ this->box3d->check_for_swapped_coords();
+
+ /* we need to call this from here (instead of from SPBox3D::position_set(), for example)
+ because z-order setting must not interfere with display updates during undo/redo */
+ this->box3d->set_z_orders ();
+
+ this->box3d->position_set();
+
+ // 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 = _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();
+
+ this->box3d->relabel_corners();
+
+ DocumentUndo::done(_desktop->getDocument(), _("Create 3D box"), INKSCAPE_ICON("draw-cuboid"));
+
+ 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..a75c2db
--- /dev/null
+++ b/src/ui/tools/box3d-tool.h
@@ -0,0 +1,100 @@
+// 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;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+class Box3dTool : public ToolBase {
+public:
+ Box3dTool(SPDesktop *desktop);
+ ~Box3dTool() override;
+
+ Box3D::VPDrag *_vpdrag;
+
+ bool root_handler(GdkEvent *event) override;
+ bool item_handler(SPItem *item, GdkEvent *event) 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..ed23158
--- /dev/null
+++ b/src/ui/tools/calligraphic-tool.cpp
@@ -0,0 +1,1162 @@
+// 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 "ui/tools/calligraphic-tool.h"
+
+#include <cstring>
+#include <numeric>
+#include <string>
+
+#include <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.h>
+#include <gtk/gtk.h>
+
+#include <2geom/bezier-utils.h>
+#include <2geom/circle.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 "inkscape.h"
+#include "message-context.h"
+#include "selection.h"
+
+#include "display/control/canvas-item-bpath.h"
+#include "display/control/canvas-item-drawing.h" // ctx
+#include "display/curve.h"
+#include "display/drawing.h"
+
+#include "include/macros.h"
+
+#include "livarot/Path.h"
+#include "livarot/Shape.h"
+
+#include "object/sp-shape.h"
+#include "object/sp-text.h"
+
+#include "path/path-util.h"
+
+#include "svg/svg.h"
+
+#include "ui/icon-names.h"
+#include "ui/tools/freehand-base.h"
+#include "ui/widget/canvas.h"
+
+#include "util/units.h"
+
+using Inkscape::DocumentUndo;
+using Inkscape::Util::Quantity;
+using Inkscape::Util::Unit;
+using Inkscape::Util::unit_table;
+
+#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 {
+
+CalligraphicTool::CalligraphicTool(SPDesktop *desktop)
+ : DynamicBase(desktop, "/tools/calligraphic", "calligraphy.svg")
+ , 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)
+ , 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;
+
+ currentshape = make_canvasitem<CanvasItemBpath>(desktop->getCanvasSketch());
+ currentshape->set_stroke(0x0);
+ currentshape->set_fill(DDC_RED_RGBA, SP_WIND_RULE_EVENODD);
+
+ /* fixme: Cannot we cascade it to root more clearly? */
+ currentshape->connect_event(sigc::bind(sigc::ptr_fun(sp_desktop_root_handler), desktop));
+
+ hatch_area = make_canvasitem<CanvasItemBpath>(desktop->getCanvasControls());
+ hatch_area->set_fill(0x0, SP_WIND_RULE_EVENODD);
+ hatch_area->set_stroke(0x0000007f);
+ hatch_area->set_pickable(false);
+ hatch_area->hide();
+
+ 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();
+ }
+}
+
+CalligraphicTool::~CalligraphicTool() = default;
+
+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;
+}
+
+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 *= -_desktop->yaxisdir();
+ if (this->flatness < 0.0) {
+ // flips direction. Useful when this->usetilt
+ // allows simulating both pen and calligraphic brush
+ a1 *= -1;
+ }
+ 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 - fabs(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 = getViewPoint(this->cur);
+ Geom::Point brush_w = _desktop->d2w(brush);
+
+ double trace_thick = 1;
+ if (this->trace_bg) {
+ // Trace background, use single pixel under brush.
+ Geom::IntRect area = Geom::IntRect::from_xywh(brush_w.floor(), Geom::IntPoint(1, 1));
+
+ Inkscape::CanvasItemDrawing *canvas_item_drawing = _desktop->getCanvasDrawing();
+ Inkscape::Drawing *drawing = canvas_item_drawing->get_drawing();
+
+ // Get average color.
+ double R, G, B, A;
+ drawing->averageColor(area, R, G, B, A);
+
+ // Convert to thickness.
+ 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 /= _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;
+
+ ungrabCanvasEvents();
+
+ /* Remove all temporary line segments */
+ segments.clear();
+
+ /* reset accumulated curve */
+ accumulated.reset();
+ clear_current();
+
+ repr = nullptr;
+}
+
+bool CalligraphicTool::root_handler(GdkEvent* event) {
+ gint ret = FALSE;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Unit const *unit = unit_table.getUnit(prefs->getString("/tools/calligraphic/unit"));
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 1) {
+ if (Inkscape::have_viable_layer(_desktop, defaultMessageContext()) == false) {
+ return TRUE;
+ }
+
+ accumulated.reset();
+
+ repr = nullptr;
+
+ /* initialize first point */
+ npoints = 0;
+
+ grabCanvasEvents();
+
+ ret = TRUE;
+
+ 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 && (is<SPShape>(selected) || is<SPText>(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);
+ if (hatch_livarot_path) {
+ 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
+ std::optional<Path::cut_position> position = get_nearest_position_on_Path(this->hatch_livarot_path, pointer);
+ if (position) {
+ nearest = get_point_on_Path(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->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;
+ }
+
+ Geom::PathVector path = Geom::Path(Geom::Circle(0,0,1)); // Unit circle centered at origin.
+
+ // 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));
+ path *= sm;
+
+ hatch_area->set_bpath(std::move(path), true);
+ hatch_area->set_stroke(0x7f7f7fff);
+ hatch_area->show();
+
+ } else if (this->dragging && !this->hatch_escaped && hatch_dist != 0) {
+ // 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));
+ path *= sm;
+
+ hatch_area->set_bpath(std::move(path), true);
+ hatch_area->set_stroke(0x00FF00ff);
+ hatch_area->show();
+
+ } else if (this->dragging && this->hatch_escaped && hatch_dist != 0) {
+ // 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));
+ path *= sm;
+
+ hatch_area->set_bpath(std::move(path), true);
+ hatch_area->set_stroke(0xff0000ff);
+ hatch_area->show();
+
+ } 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]) && this->hatch_spacing!=0) {
+ Geom::Affine const sm (Geom::Scale(this->hatch_spacing, this->hatch_spacing) * Geom::Translate(c));
+ path *= sm;
+
+ hatch_area->set_bpath(std::move(path), true);
+ hatch_area->set_stroke(0x7f7f7fff);
+ hatch_area->show();
+ }
+ }
+ } else {
+ hatch_area->hide();
+ }
+ }
+ 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));
+
+ ungrabCanvasEvents();
+
+ set_high_motion_precision(false);
+ this->is_drawing = false;
+
+ if (this->dragging && event->button.button == 1) {
+ this->dragging = FALSE;
+
+ this->apply(motion_dt);
+
+ /* Remove all temporary line segments */
+ 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 */
+ accumulated.reset();
+
+ clear_current();
+ 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
+ && Inkscape::have_viable_layer(_desktop, defaultMessageContext()))
+ {
+ spdc_create_single_dot(this, _desktop->w2d(motion_w), "/tools/calligraphic", event->button.state);
+ ret = TRUE;
+ }
+ 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 = Quantity::convert(this->width, "px", unit) + 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 = Quantity::convert(this->width, "px", unit) - 0.01;
+ if (this->width < 0.00001)
+ this->width = 0.00001;
+ 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.00001;
+ 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 */
+ currentshape->set_bpath(nullptr);
+
+ /* reset curve */
+ currentcurve.reset();
+ cal1.reset();
+ cal2.reset();
+
+ /* reset points */
+ npoints = 0;
+}
+
+void CalligraphicTool::set_to_accumulated(bool unionize, bool subtract) {
+ if (!accumulated.is_empty()) {
+ if (!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;
+
+ auto layer = currentLayer();
+ auto item = cast<SPItem>(layer->appendChildRepr(this->repr));
+ Inkscape::GC::release(this->repr);
+ item->transform = layer->i2doc_affine().inverse();
+ item->updateRepr();
+ }
+
+ Geom::PathVector pathv = accumulated.get_pathvector() * _desktop->dt2doc();
+ repr->setAttribute("d", sp_svg_write_path(pathv));
+
+ 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.
+ auto result = cast<SPItem>(_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(), _("Draw calligraphic stroke"), INKSCAPE_ICON("draw-calligraphic"));
+}
+
+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 (
+ cal1.is_empty() ||
+ cal2.is_empty() ||
+ (cal1.get_segment_count() <= 0) ||
+ cal1.first_path()->closed()
+ ) {
+
+ cal1.reset();
+ cal2.reset();
+
+ return false; // failure
+ }
+
+ auto rev_cal2 = cal2.reversed();
+
+ if ((rev_cal2.get_segment_count() <= 0) || rev_cal2.first_path()->closed()) {
+ cal1.reset();
+ cal2.reset();
+
+ return false; // failure
+ }
+
+ Geom::Curve const * dc_cal1_firstseg = cal1.first_segment();
+ Geom::Curve const * rev_cal2_firstseg = rev_cal2.first_segment();
+ Geom::Curve const * dc_cal1_lastseg = cal1.last_segment();
+ Geom::Curve const * rev_cal2_lastseg = rev_cal2.last_segment();
+
+ accumulated.reset(); /* Is this required ?? */
+
+ accumulated.append(cal1);
+
+ add_cap(accumulated, dc_cal1_lastseg->finalPoint(), rev_cal2_firstseg->initialPoint(), cap_rounding);
+
+ accumulated.append(rev_cal2, true);
+
+ add_cap(accumulated, rev_cal2_lastseg->finalPoint(), dc_cal1_firstseg->initialPoint(), cap_rounding);
+
+ accumulated.closepath();
+
+ cal1.reset();
+ 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 ( cal1.is_empty() || cal2.is_empty() ) {
+ /* dc->npoints > 0 */
+ /* g_print("calligraphics(1|2) reset\n"); */
+ cal1.reset();
+ cal2.reset();
+
+ cal1.moveto(this->point1[0]);
+ 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) {
+ currentcurve.reset();
+ currentcurve.moveto(b1[0]);
+ for (Geom::Point *bp1 = b1; bp1 < b1 + BEZIER_SIZE * nb1; bp1 += BEZIER_SIZE) {
+ currentcurve.curveto(bp1[1], bp1[2], bp1[3]);
+ }
+ currentcurve.lineto(b2[BEZIER_SIZE*(nb2-1) + 3]);
+ for (Geom::Point *bp2 = b2 + BEZIER_SIZE * ( nb2 - 1 ); bp2 >= b2; bp2 -= BEZIER_SIZE) {
+ 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(currentcurve, b2[0], b1[0], cap_rounding);
+ }
+ currentcurve.closepath();
+ currentshape->set_bpath(&currentcurve, true);
+ }
+
+ /* Current calligraphic */
+ for (Geom::Point *bp1 = b1; bp1 < b1 + BEZIER_SIZE * nb1; bp1 += BEZIER_SIZE) {
+ cal1.curveto(bp1[1], bp1[2], bp1[3]);
+ }
+ for (Geom::Point *bp2 = b2; bp2 < b2 + BEZIER_SIZE * nb2; bp2 += BEZIER_SIZE) {
+ 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++) {
+ cal1.lineto(this->point1[i]);
+ }
+ for (gint i = 1; i < this->npoints; i++) {
+ 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(!currentcurve.is_empty());
+
+ guint32 fillColor = sp_desktop_get_color_tool(_desktop, "/tools/calligraphic", true);
+ double opacity = sp_desktop_get_master_opacity_tool(_desktop, "/tools/calligraphic");
+ double fillOpacity = sp_desktop_get_opacity_tool(_desktop, "/tools/calligraphic", true);
+ guint fill = (fillColor & 0xffffff00) | SP_COLOR_F_TO_U(opacity*fillOpacity);
+
+ auto cbp = new Inkscape::CanvasItemBpath(_desktop->getCanvasSketch(), currentcurve.get_pathvector(), true);
+ cbp->set_fill(fill, SP_WIND_RULE_EVENODD);
+ cbp->set_stroke(0x0);
+
+ /* fixme: Cannot we cascade it to root more clearly? */
+ cbp->connect_event(sigc::bind(sigc::ptr_fun(sp_desktop_root_handler), _desktop));
+
+ segments.emplace_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() {
+ currentcurve.reset();
+
+ currentcurve.moveto(this->point2[this->npoints-1]);
+
+ for (gint i = this->npoints-2; i >= 0; i--) {
+ currentcurve.lineto(this->point2[i]);
+ }
+
+ for (gint i = 0; i < this->npoints; i++) {
+ currentcurve.lineto(this->point1[i]);
+ }
+
+ if (this->npoints >= 2) {
+ add_cap(currentcurve, point1[npoints - 1], point2[npoints - 1], cap_rounding);
+ }
+
+ currentcurve.closepath();
+ currentshape->set_bpath(&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..75b2a4a
--- /dev/null
+++ b/src/ui/tools/calligraphic-tool.h
@@ -0,0 +1,101 @@
+// 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 "display/control/canvas-item-ptr.h"
+#include "ui/tools/dynamic-base.h"
+
+class SPItem;
+class Path;
+
+#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 {
+
+class CanvasItemBpath;
+
+namespace UI {
+namespace Tools {
+
+class CalligraphicTool : public DynamicBase {
+public:
+ CalligraphicTool(SPDesktop *desktop);
+ ~CalligraphicTool() override;
+
+ void set(const Inkscape::Preferences::Entry &val) override;
+ bool root_handler(GdkEvent *event) 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;
+ CanvasItemPtr<Inkscape::CanvasItemBpath> 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..8b6f64a
--- /dev/null
+++ b/src/ui/tools/connector-tool.cpp
@@ -0,0 +1,1324 @@
+// 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 "connector-tool.h"
+
+#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 "display/control/canvas-item-bpath.h"
+#include "display/control/canvas-item-ctrl.h"
+#include "display/curve.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 "object/sp-use.h"
+#include "object/sp-symbol.h"
+
+#include "ui/icon-names.h"
+#include "ui/knot/knot.h"
+#include "ui/widget/canvas.h" // Enter events
+
+#include "xml/node.h"
+
+#include "svg/svg.h"
+
+namespace Inkscape::UI::Tools {
+
+void CCToolShapeNodeObserver::notifyAttributeChanged(Inkscape::XML::Node &repr, GQuark name_, Util::ptr_shared, Util::ptr_shared)
+{
+ auto tool = static_cast<ConnectorTool*>(this);
+
+ auto const name = g_quark_to_string(name_);
+ // 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 == tool->active_shape_repr) {
+ // Active shape has moved. Clear active shape.
+ tool->cc_clear_active_shape();
+ } else if (&repr == tool->active_conn_repr) {
+ // The active conn has been moved.
+ // Set it again, which just sets new handle positions.
+ tool->cc_set_active_conn(tool->active_conn);
+ }
+ }
+}
+
+void CCToolLayerNodeObserver::notifyChildRemoved(Inkscape::XML::Node&, Inkscape::XML::Node &child, Inkscape::XML::Node*)
+{
+ auto tool = static_cast<ConnectorTool*>(this);
+
+ if (&child == tool->active_shape_repr) {
+ // The active shape has been deleted. Clear active shape.
+ tool->cc_clear_active_shape();
+ }
+}
+
+using Inkscape::DocumentUndo;
+
+static void cc_clear_active_knots(SPKnotList k);
+
+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;*/
+
+ConnectorTool::ConnectorTool(SPDesktop *desktop)
+ : ToolBase(desktop, "/tools/connector", "connector.svg")
+ , state {SP_CONNECTOR_CONTEXT_IDLE}
+{
+ this->selection = desktop->getSelection();
+
+ this->sel_changed_connection.disconnect();
+ this->sel_changed_connection = this->selection->connectChanged(
+ sigc::mem_fun(*this, &ConnectorTool::_selectionChanged)
+ );
+
+ /* Create red bpath */
+ red_bpath = new Inkscape::CanvasItemBpath(desktop->getCanvasSketch());
+ red_bpath->set_stroke(red_color);
+ red_bpath->set_fill(0x0, SP_WIND_RULE_NONZERO);
+
+ /* Create red curve */
+ red_curve.emplace();
+
+ /* Create green curve */
+ green_curve.emplace();
+
+ // 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.
+ desktop->getCanvas()->set_all_enter_events(true);
+}
+
+ConnectorTool::~ConnectorTool()
+{
+ this->_finish();
+ this->state = SP_CONNECTOR_CONTEXT_IDLE;
+
+ if (this->selection) {
+ this->selection = nullptr;
+ }
+
+ this->cc_clear_active_shape();
+ this->cc_clear_active_conn();
+
+ // Restore the default event generating behaviour.
+ _desktop->getCanvas()->set_all_enter_events(false);
+
+ this->sel_changed_connection.disconnect();
+
+ for (auto &i : this->endpt_handle) {
+ if (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::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::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) {
+ this->active_shape_repr->removeObserver(shapeNodeObserver());
+ Inkscape::GC::release(this->active_shape_repr);
+ this->active_shape_repr = nullptr;
+
+ this->active_shape_layer_repr->removeObserver(layerNodeObserver());
+ 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) {
+ this->active_conn_repr->removeObserver(shapeNodeObserver());
+ 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, gchar **subhref)
+{
+ 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());
+ if(this->active_handle->sub_owner) {
+ auto id = this->active_handle->sub_owner->getAttribute("id");
+ if(id) {
+ *subhref = g_strdup_printf("#%s", id);
+ }
+ } else {
+ *subhref = nullptr;
+ }
+ return true;
+ }
+ *href = nullptr;
+ *subhref = nullptr;
+ return false;
+}
+
+static void cc_select_handle(SPKnot* knot)
+{
+ knot->setShape(Inkscape::CANVAS_ITEM_CTRL_SHAPE_SQUARE);
+ knot->setSize(11); // Should be odd.
+ knot->setAnchor(SP_ANCHOR_CENTER);
+ knot->setFill(0x0000ffff, 0x0000ffff, 0x0000ffff, 0x0000ffff);
+ knot->updateCtrl();
+}
+
+static void cc_deselect_handle(SPKnot* knot)
+{
+ knot->setShape(Inkscape::CANVAS_ITEM_CTRL_SHAPE_SQUARE);
+ knot->setSize(9); // Should be odd.
+ 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) {
+ 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_MOTION_NOTIFY: {
+ auto last_pos = Geom::Point(event->motion.x, event->motion.y);
+ SPItem *item = _desktop->getItemAtPoint(last_pos, false);
+ if (cc_item_is_shape(item)) {
+ this->_setActiveShape(item);
+ }
+ ret = false;
+ 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 = _desktop->w2d(event_w);
+
+ bool ret = false;
+
+ if ( bevent.button == 1 ) {
+ 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 = _desktop->w2d(event_w);
+
+ SnapManager &m = _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();
+
+ _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, &this->sub_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(_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(_desktop);
+ m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE);
+ m.unSetup();
+
+ this->_setSubsequentPoint(p);
+ this->_finishSegment(p);
+
+ this->_ptHandleTest(p, &this->ehref, &this->sub_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 (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(is<SPPath>(clickeditem));
+
+ m.setup(_desktop);
+ m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE);
+ m.unSetup();
+
+ // Update the hidden path
+ auto i2d = clickeditem->i2dt_affine();
+ auto d2i = i2d.inverse();
+ auto path = cast<SPPath>(clickeditem);
+ auto curve = *path->curve();
+ if (clickedhandle == endpt_handle[0]) {
+ auto o = endpt_handle[1]->pos;
+ curve.stretch_endpoints(p * d2i, o * d2i);
+ } else {
+ auto o = endpt_handle[0]->pos;
+ curve.stretch_endpoints(o * d2i, p * d2i);
+ }
+ path->setCurve(std::move(curve));
+ sp_conn_reroute_path_immediate(path);
+
+ // Copy this to the temporary visible path
+ red_curve = path->curveForEdit()->transformed(i2d);
+ red_bpath->set_bpath(&*red_curve);
+
+ 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 ) {
+ 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 = _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, &this->sub_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();
+ red_bpath->set_bpath(nullptr);
+
+ if (p != nullptr) {
+ // Test whether we clicked on a connection point
+ gchar *shape_label;
+ gchar *sub_label;
+ bool found = this->_ptHandleTest(*p, &shape_label, &sub_label);
+
+ if (found) {
+ if (this->clickedhandle == this->endpt_handle[0]) {
+ this->clickeditem->setAttribute("inkscape:connection-start", shape_label);
+ this->clickeditem->setAttribute("inkscape:connection-start-point", sub_label);
+ } else {
+ this->clickeditem->setAttribute("inkscape:connection-end", shape_label);
+ this->clickeditem->setAttribute("inkscape:connection-end-point", sub_label);
+ }
+ g_free(shape_label);
+ if(sub_label) {
+ g_free(sub_label);
+ }
+ }
+ }
+ this->clickeditem->setHidden(false);
+ sp_conn_reroute_path_immediate(cast<SPPath>(this->clickeditem));
+ this->clickeditem->updateRepr();
+ DocumentUndo::done(doc, _("Reroute connector"), INKSCAPE_ICON("draw-connector"));
+ this->cc_set_active_conn(this->clickeditem);
+}
+
+
+void ConnectorTool::_resetColors()
+{
+ /* Red */
+ this->red_curve->reset();
+ red_bpath->set_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;
+ red_bpath->set_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.
+ red_curve = SPConnEndPair::createCurve(newConnRef, curvature);
+ red_curve->transform(_desktop->doc2dt());
+ red_bpath->set_bpath(&*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()
+{
+ auto c = std::make_optional<SPCurve>();
+ std::swap(c, green_curve);
+
+ red_curve->reset();
+ red_bpath->set_bpath(nullptr);
+
+ if (c->is_empty()) {
+ return;
+ }
+
+ _flushWhite(&*c);
+}
+
+
+/*
+ * 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 *c)
+{
+ /* Now we have to go back to item coordinates at last */
+ c->transform(_desktop->dt2doc());
+
+ SPDocument *doc = _desktop->getDocument();
+ Inkscape::XML::Document *xml_doc = doc->getReprDoc();
+
+ if ( !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);
+
+ repr->setAttribute("d", sp_svg_write_path(c->get_pathvector()));
+
+ /* Attach repr */
+ auto layer = currentLayer();
+ this->newconn = cast<SPItem>(layer->appendChildRepr(repr));
+ this->newconn->transform = layer->i2doc_affine().inverse();
+
+ bool connection = false;
+ this->newconn->setAttribute( "inkscape:connector-type",
+ this->isOrthogonal ? "orthogonal" : "polyline");
+ this->newconn->setAttribute( "inkscape:connector-curvature",
+ Glib::Ascii::dtostr(this->curvature).c_str());
+ if (this->shref) {
+ connection = true;
+ this->newconn->setAttribute( "inkscape:connection-start", this->shref);
+ if(this->sub_shref) {
+ this->newconn->setAttribute( "inkscape:connection-start-point", this->sub_shref);
+ }
+ }
+
+ if (this->ehref) {
+ connection = true;
+ this->newconn->setAttribute( "inkscape:connection-end", this->ehref);
+ if(this->sub_ehref) {
+ this->newconn->setAttribute( "inkscape:connection-end-point", this->sub_ehref);
+ }
+ }
+ // Process pending updates.
+ this->newconn->updateRepr();
+ doc->ensureUpToDate();
+
+ if (connection) {
+ // Adjust endpoints to shape edge.
+ sp_conn_reroute_path_immediate(cast<SPPath>(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);
+ }
+
+ DocumentUndo::done(doc, _("Create connector"), INKSCAPE_ICON("draw-connector"));
+}
+
+
+void ConnectorTool::_finishSegment(Geom::Point const /*p*/)
+{
+ if (!this->red_curve->is_empty()) {
+ green_curve->append_continuous(*red_curve);
+
+ 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 bool cc_generic_knot_handler(GdkEvent *event, SPKnot *knot)
+{
+ g_assert (knot != nullptr);
+
+ //g_object_ref(knot);
+ knot_ref(knot);
+
+ ConnectorTool *cc = SP_CONNECTOR_CONTEXT(
+ knot->desktop->event_context);
+
+ bool 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;
+ }
+
+ knot_unref(knot);
+
+ return consumed;
+}
+
+
+static bool endpt_handler(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.
+ auto path = static_cast<SPPath const *>(cc->clickeditem);
+ cc->red_curve = path->curveForEdit()->transformed(cc->clickeditem->i2dt_affine());
+ cc->red_bpath->set_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, SPItem* subitem)
+{
+ SPKnot *knot = new SPKnot(_desktop, "", Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "CanvasItemCtrl:ConnectorTool:Shape");
+ knot->owner = item;
+
+ if (subitem) {
+ auto use = cast<SPUse>(item);
+ g_assert(use != nullptr);
+ knot->sub_owner = subitem;
+ knot->setShape(Inkscape::CANVAS_ITEM_CTRL_SHAPE_SQUARE);
+ knot->setSize(11); // Must be odd
+ knot->setAnchor(SP_ANCHOR_CENTER);
+ knot->setFill(0xffffff00, 0xff0000ff, 0xff0000ff, 0xff0000ff);
+
+ // Set the point to the middle of the sub item
+ knot->setPosition(subitem->getAvoidRef().getConnectionPointPos() * _desktop->doc2dt(), 0);
+ } else {
+ knot->setShape(Inkscape::CANVAS_ITEM_CTRL_SHAPE_SQUARE);
+ knot->setSize(9); // Must be odd
+ knot->setAnchor(SP_ANCHOR_CENTER);
+ knot->setFill(0xffffff00, 0xff0000ff, 0xff0000ff, 0xff0000ff);
+
+ // Set the point to the middle of the object
+ knot->setPosition(item->getAvoidRef().getConnectionPointPos() * _desktop->doc2dt(), 0);
+ }
+
+ knot->updateCtrl();
+
+ // We don't want to use the standard knot handler.
+ knot->_event_connection.disconnect();
+ knot->_event_connection =
+ knot->ctrl->connect_event(sigc::bind(sigc::ptr_fun(cc_generic_knot_handler), knot));
+
+ 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) {
+ this->active_shape_repr->removeObserver(shapeNodeObserver());
+ Inkscape::GC::release(this->active_shape_repr);
+
+ this->active_shape_layer_repr->removeObserver(layerNodeObserver());
+ 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);
+ this->active_shape_repr->addObserver(shapeNodeObserver());
+
+ this->active_shape_layer_repr = this->active_shape_repr->parent();
+ Inkscape::GC::anchor(this->active_shape_layer_repr);
+ this->active_shape_layer_repr->addObserver(layerNodeObserver());
+ }
+
+ 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(child.getAttribute("inkscape:connector")) {
+ this->_activeShapeAddKnot((SPItem *) &child, nullptr);
+ }
+ }
+ // Special connector points in a symbol
+ if (auto use = cast<SPUse>(item)) {
+ SPItem *orig = use->root();
+ //SPItem *orig = use->get_original();
+ for (auto& child: orig->children) {
+ if(child.getAttribute("inkscape:connector")) {
+ this->_activeShapeAddKnot(item, (SPItem *) &child);
+ }
+ }
+ }
+ // Center point to any object
+ this->_activeShapeAddKnot(item, nullptr);
+
+ } else {
+ // Ensure the item's connection_points map
+ // has been updated
+ item->document->ensureUpToDate();
+ }
+}
+
+void ConnectorTool::cc_set_active_conn(SPItem *item)
+{
+ g_assert( is<SPPath>(item) );
+
+ const SPCurve *curve = cast<SPPath>(item)->curveForEdit();
+ 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) {
+ this->active_conn_repr->removeObserver(shapeNodeObserver());
+ 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);
+ this->active_conn_repr->addObserver(shapeNodeObserver());
+ }
+
+ 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(_desktop,
+ _("<b>Connector endpoint</b>: drag to reroute or connect to new shapes"),
+ Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "CanvasItemCtrl:ConnectorTool:Endpoint");
+
+ knot->setShape(Inkscape::CANVAS_ITEM_CTRL_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.
+ knot->_event_connection.disconnect();
+ knot->_event_connection =
+ knot->ctrl->connect_event(sigc::bind(sigc::ptr_fun(cc_generic_knot_handler), knot));
+
+ this->endpt_handle[i] = knot;
+ }
+
+ // Remove any existing handlers
+ this->endpt_handler_connection[i].disconnect();
+ this->endpt_handler_connection[i] =
+ this->endpt_handle[i]->ctrl->connect_event(sigc::bind(sigc::ptr_fun(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->getDesktop(), "", Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER,
+ "CanvasItemCtrl::ConnectorTool:ConnectionPoint");
+
+ // We do not process events on this knot.
+ knot->_event_connection.disconnect();
+
+ 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 (auto path = cast<SPPath>(item)) {
+ SPCurve const *curve = path->curve();
+ if ( curve && !(curve->is_closed()) ) {
+ // Open paths are connectors.
+ return false;
+ }
+ } else if (is<SPText>(item) || is<SPFlowtext>(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 (auto path = cast<SPPath>(item)) {
+ bool closed = path->curveForEdit()->is_closed();
+ if (path->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(SPDesktop *desktop, bool const set_avoid)
+{
+ 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, event_desc, INKSCAPE_ICON("draw-connector"));
+}
+
+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);
+ }
+}
+
+} // namespace Inkscape::UI::Tools
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ 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..f2271aa
--- /dev/null
+++ b/src/ui/tools/connector-tool.h
@@ -0,0 +1,191 @@
+// 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 <optional>
+#include <string>
+
+#include <2geom/point.h>
+#include <sigc++/connection.h>
+
+#include "display/curve.h"
+
+#include "ui/tools/tool-base.h"
+
+#include "xml/node-observer.h"
+
+class SPItem;
+class SPCurve;
+class SPKnot;
+
+namespace Avoid {
+ class ConnRef;
+}
+
+namespace Inkscape {
+ class CanvasItemBpath;
+ 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
+};
+
+using SPKnotList = std::map<SPKnot *, int>;
+
+namespace Inkscape::UI::Tools {
+
+class ConnectorTool;
+
+class CCToolShapeNodeObserver : public Inkscape::XML::NodeObserver
+{
+ friend class ConnectorTool;
+ ~CCToolShapeNodeObserver() override = default; // can only exist as a direct base of ConnectorTool
+
+ void notifyAttributeChanged(Inkscape::XML::Node &, GQuark, Util::ptr_shared, Util::ptr_shared) final;
+};
+
+class CCToolLayerNodeObserver : public Inkscape::XML::NodeObserver
+{
+ friend class ConnectorTool;
+ ~CCToolLayerNodeObserver() override = default; // can only exist as a direct base of ConnectorTool
+
+ void notifyChildRemoved(Inkscape::XML::Node &, Inkscape::XML::Node &, Inkscape::XML::Node *) final;
+};
+
+class ConnectorTool
+ : public ToolBase
+ , private CCToolShapeNodeObserver
+ , private CCToolLayerNodeObserver
+{
+public:
+ ConnectorTool(SPDesktop *desktop);
+ ~ConnectorTool() override;
+
+ Inkscape::Selection *selection{nullptr};
+ Geom::Point p[5];
+
+ /** \invar npoints in {0, 2}. */
+ gint npoints{0};
+ unsigned int state : 4;
+
+ // Red curve
+ Inkscape::CanvasItemBpath *red_bpath{nullptr};
+ std::optional<SPCurve> red_curve;
+ guint32 red_color{0xff00007f};
+
+ // Green curve
+ std::optional<SPCurve> green_curve;
+
+ // The new connector
+ SPItem *newconn{nullptr};
+ Avoid::ConnRef *newConnRef{nullptr};
+ gdouble curvature{0.0};
+ bool isOrthogonal{false};
+
+ // The active shape
+ SPItem *active_shape{nullptr};
+ Inkscape::XML::Node *active_shape_repr{nullptr};
+ Inkscape::XML::Node *active_shape_layer_repr{nullptr};
+
+ // Same as above, but for the active connector
+ SPItem *active_conn{nullptr};
+ Inkscape::XML::Node *active_conn_repr{nullptr};
+ sigc::connection sel_changed_connection;
+
+ // The activehandle
+ SPKnot *active_handle{nullptr};
+
+ // The selected handle, used in editing mode
+ SPKnot *selected_handle{nullptr};
+
+ SPItem *clickeditem{nullptr};
+ SPKnot *clickedhandle{nullptr};
+
+ SPKnotList knots;
+ SPKnot *endpt_handle[2]{};
+ sigc::connection endpt_handler_connection[2];
+ gchar *shref{nullptr};
+ gchar *sub_shref{nullptr};
+ gchar *ehref {nullptr};
+ gchar *sub_ehref{nullptr};
+
+ void set(const Inkscape::Preferences::Entry& val) override;
+ bool root_handler(GdkEvent* event) override;
+ bool item_handler(SPItem* item, GdkEvent* event) 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, SPItem* subitem);
+ void _setActiveShape(SPItem *item);
+ bool _ptHandleTest(Geom::Point& p, gchar **href, gchar **subhref);
+
+ void _reroutingFinish(Geom::Point *const p);
+
+ CCToolShapeNodeObserver &shapeNodeObserver() { return *this; }
+ CCToolLayerNodeObserver &layerNodeObserver() { return *this; }
+ friend CCToolShapeNodeObserver;
+ friend CCToolLayerNodeObserver;
+};
+
+void cc_selection_set_avoid(SPDesktop *, 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);
+
+} // namespace Inkscape::UI::Tools
+
+#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..a909df7
--- /dev/null
+++ b/src/ui/tools/dropper-tool.cpp
@@ -0,0 +1,394 @@
+// 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 "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "message-context.h"
+#include "preferences.h"
+#include "selection.h"
+#include "style.h"
+#include "page-manager.h"
+
+#include "display/curve.h"
+#include "display/drawing.h"
+#include "display/control/canvas-item-bpath.h"
+#include "display/control/canvas-item-drawing.h"
+
+#include "include/macros.h"
+
+#include "object/sp-namedview.h"
+
+#include "svg/svg-color.h"
+
+#include "ui/cursor-utils.h"
+#include "ui/icon-names.h"
+#include "ui/tools/dropper-tool.h"
+#include "ui/widget/canvas.h"
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+DropperTool::DropperTool(SPDesktop *desktop)
+ : ToolBase(desktop, "/tools/dropper", "dropper-pick-fill.svg")
+{
+ area = make_canvasitem<CanvasItemBpath>(desktop->getCanvasControls());
+ area->set_stroke(0x0000007f);
+ area->set_fill(0x0, SP_WIND_RULE_EVENODD);
+ area->hide();
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (prefs->getBool("/tools/dropper/selcue")) {
+ this->enableSelectionCue();
+ }
+
+ if (prefs->getBool("/tools/dropper/gradientdrag")) {
+ this->enableGrDrag();
+ }
+}
+
+DropperTool::~DropperTool()
+{
+ this->enableGrDrag(false);
+
+ ungrabCanvasEvents();
+}
+
+/**
+ * Returns the current dropper context color.
+ *
+ * - If in dropping mode, returns color from selected objects.
+ * Ignored if non_dropping set to true.
+ * - If in dragging mode, returns average color on canvas, depending on radius
+ * - If in pick mode, alpha is not premultiplied. Alpha is only set if in pick mode
+ * and setalpha is true. Both values are taken from preferences.
+ *
+ * @param invert If true, invert the rgb value
+ * @param non_dropping If true, use color from canvas, even in dropping mode.
+ */
+guint32 DropperTool::get_color(bool invert, bool non_dropping) {
+ 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);
+
+ // non_dropping ignores dropping mode and always uses color from canvas.
+ // Used by the clipboard
+ double r = non_dropping ? this->non_dropping_R : this->R;
+ double g = non_dropping ? this->non_dropping_G : this->G;
+ double b = non_dropping ? this->non_dropping_B : this->B;
+ double a = non_dropping ? this->non_dropping_A : this->alpha;
+
+ return SP_RGBA32_F_COMPOSE(
+ fabs(invert - r),
+ fabs(invert - g),
+ fabs(invert - b),
+ (pick == SP_DROPPER_PICK_ACTUAL && setalpha) ? a : 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
+ // Only if dropping mode enabled and object's color is set.
+ // Otherwise dropping mode disabled.
+ 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 = SP_SCALE24_TO_FLOAT(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 = SP_SCALE24_TO_FLOAT(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->centre = Geom::Point(event->button.x, event->button.y);
+ this->dragging = true;
+ ret = TRUE;
+ }
+
+ grabCanvasEvents(Gdk::KEY_PRESS_MASK |
+ Gdk::KEY_RELEASE_MASK |
+ Gdk::BUTTON_RELEASE_MASK |
+ Gdk::POINTER_MOTION_MASK |
+ Gdk::BUTTON_PRESS_MASK );
+ 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 {
+ // otherwise, constantly calculate color no matter if any button pressed or not
+
+ Geom::IntRect pick_area;
+ if (this->dragging) {
+ // calculate average
+
+ // radius
+ double 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) );
+
+ // Show circle on canvas
+ Geom::PathVector path = Geom::Path(Geom::Circle(0, 0, 1)); // Unit circle centered at origin.
+ path *= sm;
+ this->area->set_bpath(std::move(path));
+ this->area->show();
+
+ /* Get buffer */
+ Geom::Rect r(this->centre, this->centre);
+ r.expandBy(rw);
+ if (!r.hasZeroArea()) {
+ pick_area = r.roundOutwards();
+
+ }
+ } else {
+ // pick single pixel
+ pick_area = Geom::IntRect::from_xywh(floor(event->button.x), floor(event->button.y), 1, 1);
+ }
+
+ Inkscape::CanvasItemDrawing *canvas_item_drawing = _desktop->getCanvasDrawing();
+ Inkscape::Drawing *drawing = canvas_item_drawing->get_drawing();
+
+ // Get average color.
+ double R, G, B, A;
+ drawing->averageColor(pick_area, R, G, B, A);
+
+ if (pick == SP_DROPPER_PICK_VISIBLE) {
+ // compose with page color
+ auto bg = _desktop->getDocument()->getPageManager().getDefaultBackgroundColor();
+ R = R + bg[0] * (1 - A);
+ G = G + bg[1] * (1 - A);
+ B = B + bg[2] * (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) {
+ this->R = R;
+ this->G = G;
+ this->B = B;
+ this->alpha = A;
+ }
+ // remember color from canvas, even in dropping mode
+ // These values are used by the clipboard
+ this->non_dropping_R = R;
+ this->non_dropping_G = G;
+ this->non_dropping_B = B;
+ this->non_dropping_A = A;
+
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_BUTTON_RELEASE:
+ if (event->button.button == 1) {
+ this->area->hide();
+ this->dragging = false;
+
+ ungrabCanvasEvents();
+
+ 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);
+ }
+ }
+
+ auto picked_color = ColorRGBA(this->get_color(this->invert));
+
+ // One time pick has active signal, call them all and clear.
+ if (!onetimepick_signal.empty())
+ {
+ onetimepick_signal.emit(&picked_color);
+ onetimepick_signal.clear();
+ // Do this last as it destroys the picker tool.
+ sp_toggle_dropper(_desktop);
+ return true;
+ }
+
+ // do the actual color setting
+ sp_desktop_set_color(_desktop, picked_color, false, !this->stroke);
+
+ // REJON: set aux. toolbar input to hex color!
+ if (!(_desktop->getSelection()->isEmpty())) {
+ DocumentUndo::done(_desktop->getDocument(), _("Set picked color"), INKSCAPE_ICON("color-picker"));
+ }
+ if(this->dropping) {
+ selection->setList(old_selection);
+ }
+
+
+ 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
+ _cursor_filename = (this->dropping ? (this->stroke ? "dropper-drop-stroke.svg" : "dropper-drop-fill.svg") :
+ (this->stroke ? "dropper-pick-stroke.svg" : "dropper-pick-fill.svg") );
+
+ // We do this ourselves to get color correct.
+ auto display = _desktop->getCanvas()->get_display();
+ auto window = _desktop->getCanvas()->get_window();
+ auto cursor = load_svg_cursor(display, window, _cursor_filename, get_color(invert));
+ window->set_cursor(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..5222ca3
--- /dev/null
+++ b/src/ui/tools/dropper-tool.h
@@ -0,0 +1,94 @@
+// 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 "color-rgba.h"
+#include "display/control/canvas-item-ptr.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 {
+
+class CanvasItemBpath;
+
+namespace UI {
+namespace Tools {
+
+class DropperTool : public ToolBase {
+public:
+ DropperTool(SPDesktop *desktop);
+ ~DropperTool() override;
+
+ guint32 get_color(bool invert = false, bool non_dropping = false);
+ sigc::signal<void (ColorRGBA *)> onetimepick_signal;
+
+protected:
+ bool root_handler(GdkEvent *event) override;
+
+private:
+ // Stored color.
+ double R = 0.0;
+ double G = 0.0;
+ double B = 0.0;
+ double alpha = 0.0;
+ // Stored color taken from canvas. Used by clipboard.
+ // Identical to R, G, B, alpha if dropping disabled.
+ double non_dropping_R = 0.0;
+ double non_dropping_G = 0.0;
+ double non_dropping_B = 0.0;
+ double non_dropping_A = 0.0;
+
+ bool invert = false; ///< Set color to inverse rgb value
+ bool stroke = false; ///< Set to stroke color. In dropping mode, set from stroke color
+ bool dropping = false; ///< When true, get color from selected objects instead of canvas
+ bool dragging = false; ///< When true, get average color for region on canvas, instead of a single point
+
+ double radius = 0.0; ///< Size of region under dragging mode
+ CanvasItemPtr<CanvasItemBpath> area; ///< Circle depicting region's borders in dragging mode
+ Geom::Point centre {0, 0}; ///< Center of region in dragging mode
+
+};
+
+}
+}
+}
+
+#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..5f9de6a
--- /dev/null
+++ b/src/ui/tools/dynamic-base.cpp
@@ -0,0 +1,141 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Common drawing mode. Base class of Eraser and Calligraphic tools.
+ *//*
+ * 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 "desktop.h"
+
+#include "display/curve.h"
+#include "display/control/canvas-item-bpath.h"
+
+#include "util/units.h"
+
+using Inkscape::Util::Unit;
+using Inkscape::Util::Quantity;
+using Inkscape::Util::unit_table;
+
+#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(SPDesktop *desktop, std::string prefs_path, const std::string &cursor_filename)
+ : ToolBase(desktop, prefs_path, cursor_filename)
+ , 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() = default;
+
+void DynamicBase::set(const Inkscape::Preferences::Entry& value) {
+ Glib::ustring path = value.getEntryName();
+
+ // ignore preset modifications
+ static Glib::ustring const presets_path = getPrefsPath() + "/preset";
+ Glib::ustring const &full_path = value.getPath();
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Unit const *unit = unit_table.getUnit(prefs->getString("/tools/calligraphic/unit"));
+
+ 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.getDouble(), Quantity::convert(0.001, unit, "px"), Quantity::convert(100, unit, "px"));
+ } 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(), -100, 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 {
+ auto drect = _desktop->get_display_area();
+
+ double const max = drect.maxExtent();
+
+ return (v - drect.bounds().min()) / max;
+}
+
+/* Get view point */
+Geom::Point DynamicBase::getViewPoint(Geom::Point n) const {
+ auto drect = _desktop->get_display_area();
+
+ double const max = drect.maxExtent();
+
+ return n * max + drect.bounds().min();
+}
+
+}
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ 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..46fa5fd
--- /dev/null
+++ b/src/ui/tools/dynamic-base.h
@@ -0,0 +1,136 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef COMMON_CONTEXT_H_SEEN
+#define COMMON_CONTEXT_H_SEEN
+
+/*
+ * Common drawing mode. Base class of Eraser and Calligraphic tools.
+ *
+ * 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/curve.h"
+#include "display/control/canvas-item-ptr.h"
+
+#include <optional>
+
+class SPCurve;
+
+namespace Inkscape {
+ namespace XML {
+ class Node;
+ }
+}
+
+#define SAMPLING_SIZE 8 /* fixme: ?? */
+
+namespace Inkscape {
+
+class CanvasItemBpath;
+
+namespace UI {
+namespace Tools {
+
+class DynamicBase : public ToolBase {
+public:
+ DynamicBase(SPDesktop *desktop, std::string prefs_path, const std::string &cursor_filename);
+ ~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<CanvasItemPtr<CanvasItemBpath>> segments;
+
+ /** canvas item for red "leading" segment */
+ CanvasItemPtr<CanvasItemBpath> 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..e111533
--- /dev/null
+++ b/src/ui/tools/eraser-tool.cpp
@@ -0,0 +1,1413 @@
+// 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
+ * Rafael Siejakowski <rs@rs-math.net>
+ *
+ * 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 "eraser-tool.h"
+
+#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 "message-context.h"
+#include "message-stack.h"
+#include "path-chemistry.h"
+#include "preferences.h"
+#include "rubberband.h"
+#include "selection-chemistry.h"
+#include "selection.h"
+
+#include "display/curve.h"
+#include "display/control/canvas-item-bpath.h"
+
+#include "include/macros.h"
+
+#include "object/sp-clippath.h"
+#include "object/sp-image.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 "ui/icon-names.h"
+
+#include "svg/svg.h"
+
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+EraserTool::EraserTool(SPDesktop *desktop)
+ : DynamicBase(desktop, "/tools/eraser", "eraser.svg")
+ , _break_apart{"/tools/eraser/break_apart", false}
+ , _mode_int{"/tools/eraser/mode", 1} // Cut mode is default
+{
+ currentshape = make_canvasitem<CanvasItemBpath>(desktop->getCanvasSketch());
+ currentshape->set_stroke(0x0);
+ currentshape->set_fill(trace_color_rgba, trace_wind_rule);
+
+ /* fixme: Cannot we cascade it to root more clearly? */
+ currentshape->connect_event(sigc::bind(sigc::ptr_fun(sp_desktop_root_handler), desktop));
+
+ 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");
+
+ is_drawing = false;
+ //TODO not sure why get 0.01 if slider width == 0, maybe a double/int problem
+
+ _mode_int.min = 0;
+ _mode_int.max = 2;
+ _updateMode();
+ _mode_int.action = [this]() { _updateMode(); };
+
+ enableSelectionCue();
+}
+
+EraserTool::~EraserTool() = default;
+
+/** Reads the current Eraser mode from Preferences and sets `mode` accordingly. */
+void EraserTool::_updateMode()
+{
+ int const mode_idx = _mode_int;
+ // Note: the integer indices must agree with those in EraserToolbar::_modeAsInt()
+ if (mode_idx == 0) {
+ mode = EraserToolMode::DELETE;
+ } else if (mode_idx == 1) {
+ mode = EraserToolMode::CUT;
+ } else if (mode_idx == 2) {
+ mode = EraserToolMode::CLIP;
+ } else {
+ g_printerr("Error: invalid mode setting \"%d\" for Eraser tool!", mode_idx);
+ mode = DEFAULT_ERASER_MODE;
+ }
+}
+
+// TODO: After switch to C++20, replace this with std::lerp
+inline double flerp(double const f0, double const f1, double const p)
+{
+ return f0 + (f1 - f0) * p;
+}
+
+inline double square(double const x)
+{
+ return x * x;
+}
+
+void EraserTool::_reset(Geom::Point p)
+{
+ last = cur = getNormalizedPoint(p);
+ vel = Geom::Point(0, 0);
+ vel_max = 0;
+ acc = Geom::Point(0, 0);
+ ang = Geom::Point(0, 0);
+ del = Geom::Point(0, 0);
+}
+
+void EraserTool::_extinput(GdkEvent *event)
+{
+ if (gdk_event_get_axis(event, GDK_AXIS_PRESSURE, &pressure)) {
+ pressure = CLAMP(pressure, min_pressure, max_pressure);
+ } else {
+ pressure = default_pressure;
+ }
+
+ if (gdk_event_get_axis(event, GDK_AXIS_XTILT, &xtilt)) {
+ xtilt = CLAMP(xtilt, min_tilt, max_tilt);
+ } else {
+ xtilt = default_tilt;
+ }
+
+ if (gdk_event_get_axis(event, GDK_AXIS_YTILT, &ytilt)) {
+ ytilt = CLAMP(ytilt, min_tilt, max_tilt);
+ } else {
+ ytilt = default_tilt;
+ }
+}
+
+bool EraserTool::_apply(Geom::Point p)
+{
+ /* Calculate force and acceleration */
+ Geom::Point n = getNormalizedPoint(p);
+ Geom::Point force = n - cur;
+
+ // If force is below the absolute threshold `epsilon`,
+ // or we haven't yet reached `vel_start` (i.e. at the beginning of stroke)
+ // _and_ the force is below the (higher) `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) < epsilon || (vel_max < vel_start && Geom::L2(force) < epsilon_start)) {
+ return false;
+ }
+
+ // Calculate mass
+ double const m = flerp(1.0, 160.0, mass);
+ acc = force / m;
+ vel += acc; // Calculate new velocity
+ double const speed = Geom::L2(vel);
+
+ if (speed > vel_max) {
+ vel_max = speed;
+ } else if (speed < epsilon) {
+ return false; // return early if movement is insignificant
+ }
+
+ /* Calculate angle of eraser tool */
+ double angle_fixed{0.0};
+ if (usetilt) {
+ // 1a. calculate nib angle from input device tilt:
+ Geom::Point normal{ytilt, xtilt};
+ if (!Geom::is_zero(normal)) {
+ angle_fixed = Geom::atan2(normal);
+ }
+ } else {
+ // 1b. fixed angle (absolutely flat nib):
+ angle_fixed = angle * M_PI / 180.0; // convert to radians
+ }
+ if (flatness < 0.0) {
+ // flips direction. Useful when usetilt is true
+ // allows simulating both pen/charcoal and broad-nibbed pen
+ angle_fixed *= -1;
+ }
+
+ // 2. Angle perpendicular to vel (absolutely non-flat nib):
+ double angle_dynamic = Geom::atan2(Geom::rot90(vel));
+ // flip angle_dynamic to force it to be in the same half-circle as angle_fixed
+ bool flipped = false;
+ if (fabs(angle_dynamic - angle_fixed) > M_PI_2) {
+ angle_dynamic += M_PI;
+ flipped = true;
+ }
+ // normalize angle_dynamic
+ if (angle_dynamic > M_PI) {
+ angle_dynamic -= 2 * M_PI;
+ }
+ if (angle_dynamic < -M_PI) {
+ angle_dynamic += 2 * M_PI;
+ }
+
+ // 3. Average them using flatness parameter:
+ // find the flatness-weighted bisector angle, unflip if angle_dynamic was flipped
+ // FIXME: when `vel` is oscillating around the fixed angle, the new_ang flips back and forth.
+ // How to avoid this?
+ double new_ang = flerp(angle_dynamic, angle_fixed, fabs(flatness)) - (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)) - ang);
+ if (angle_delta / speed > 4000) {
+ return false;
+ }
+
+ // convert to point
+ ang = Geom::Point(cos(new_ang), sin(new_ang));
+
+ /* Apply drag */
+ double const d = flerp(0.0, 0.5, square(drag));
+ vel *= 1.0 - d;
+
+ /* Update position */
+ last = cur;
+ cur += vel;
+
+ return true;
+}
+
+void EraserTool::_brush()
+{
+ g_assert(npoints >= 0 && npoints < SAMPLING_SIZE);
+
+ // How much velocity thins strokestyle
+ double const vel_thinning = flerp(0, 160, vel_thin);
+
+ // Influence of pressure on thickness
+ double const pressure_thick = (usepressure ? pressure : 1.0);
+
+ // get the real brush point, not the same as pointer (affected by mass drag)
+ Geom::Point brush = getViewPoint(cur);
+
+ double const trace_thick = 1;
+ double const speed = Geom::L2(vel);
+ double effective_width = (pressure_thick * trace_thick - vel_thinning * speed) * width;
+
+ double tremble_left = 0, tremble_right = 0;
+ if (tremor > 0) {
+ // obtain two normally distributed random variables, using polar Box-Muller transform
+ double y1, y2;
+ _generateNormalDist2(y1, y2);
+
+ // deflect both left and right edges randomly and independently, so that:
+ // (1) tremor=1 corresponds to sigma=1, decreasing 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
+ double const width_coefficient = 0.15 + 0.8 * effective_width;
+ double const speed_coefficient = 0.35 + 14 * speed;
+ double const total_coefficient = tremor * width_coefficient * speed_coefficient;
+
+ tremble_left = y1 * total_coefficient;
+ tremble_right = y2 * total_coefficient;
+ }
+
+ double const min_width = 0.02 * width;
+ if (effective_width < min_width) {
+ effective_width = min_width;
+ }
+
+ double dezoomify_factor = 0.05 * 1000;
+ if (!abs_width) {
+ dezoomify_factor /= _desktop->current_zoom();
+ }
+
+ Geom::Point del_left = dezoomify_factor * (effective_width + tremble_left) * ang;
+ Geom::Point del_right = dezoomify_factor * (effective_width + tremble_right) * ang;
+
+ point1[npoints] = brush + del_left;
+ point2[npoints] = brush - del_right;
+
+ if (nowidth) {
+ point1[npoints] = Geom::middle_point(point1[npoints], point2[npoints]);
+ }
+ del = Geom::middle_point(del_left, del_right);
+
+ npoints++;
+}
+
+void EraserTool::_generateNormalDist2(double &r1, double &r2)
+{
+ // obtain two normally distributed random variables, using polar Box-Muller transform
+ double x1, x2, w;
+ do {
+ x1 = 2.0 * g_random_double_range(0, 1) - 1.0;
+ x2 = 2.0 * g_random_double_range(0, 1) - 1.0;
+ w = square(x1) + square(x2);
+ } while (w >= 1.0);
+ w = sqrt(-2.0 * log(w) / w);
+ r1 = x1 * w;
+ r2 = x2 * w;
+}
+
+void EraserTool::_cancel()
+{
+ dragging = false;
+ is_drawing = false;
+ ungrabCanvasEvents();
+
+ segments.clear();
+
+ /* reset accumulated curve */
+ accumulated.reset();
+ _clearCurrent();
+ repr = nullptr;
+}
+
+bool EraserTool::root_handler(GdkEvent* event)
+{
+ bool ret = false;
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 1) {
+ if (!Inkscape::have_viable_layer(_desktop, defaultMessageContext())) {
+ return true;
+ }
+
+ Geom::Point const button_w(event->button.x, event->button.y);
+ Geom::Point const button_dt(_desktop->w2d(button_w));
+
+ _reset(button_dt);
+ _extinput(event);
+ _apply(button_dt);
+ accumulated.reset();
+
+ repr = nullptr;
+
+ if (mode == EraserToolMode::DELETE) {
+ auto rubberband = Inkscape::Rubberband::get(_desktop);
+ rubberband->start(_desktop, button_dt);
+ rubberband->setMode(RUBBERBAND_MODE_TOUCHPATH);
+ }
+ /* initialize first point */
+ npoints = 0;
+
+ grabCanvasEvents();
+ is_drawing = 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));
+ _extinput(event);
+
+ message_context->clear();
+
+ if (is_drawing && (event->motion.state & GDK_BUTTON1_MASK)) {
+ dragging = true;
+
+ message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Drawing</b> an eraser stroke"));
+
+ if (!_apply(motion_dt)) {
+ ret = true;
+ break;
+ }
+
+ if (cur != last) {
+ _brush();
+ g_assert(npoints > 0);
+ _fitAndSplit(false);
+ }
+
+ ret = true;
+ }
+ if (mode == EraserToolMode::DELETE) {
+ accumulated.reset();
+ Inkscape::Rubberband::get(_desktop)->move(motion_dt);
+ }
+ break;
+ }
+ case GDK_BUTTON_RELEASE: {
+ if (event->button.button != 1) {
+ break;
+ }
+
+ Geom::Point const motion_w(event->button.x, event->button.y);
+ Geom::Point const motion_dt(_desktop->w2d(motion_w));
+
+ ungrabCanvasEvents();
+
+ is_drawing = false;
+
+ if (dragging) {
+ dragging = false;
+
+ _apply(motion_dt);
+ segments.clear();
+
+ // Create eraser stroke shape
+ _fitAndSplit(true);
+ _accumulate();
+
+ // Perform the actual erase operation
+ SPDocument *document = _desktop->getDocument();
+ if (_doWork()) {
+ DocumentUndo::done(document, _("Draw eraser stroke"), INKSCAPE_ICON("draw-eraser"));
+ } else {
+ DocumentUndo::cancel(document);
+ }
+
+ /* reset accumulated curve */
+ accumulated.reset();
+
+ _clearCurrent();
+ repr = nullptr;
+
+ message_context->clear();
+ ret = true;
+ }
+
+ if (mode == EraserToolMode::DELETE) {
+ auto r = Inkscape::Rubberband::get(_desktop);
+ if (r->is_started()) {
+ r->stop();
+ }
+ }
+
+ break;
+ }
+ case GDK_KEY_PRESS:
+ ret = _handleKeypress(&event->key);
+ break;
+
+ case GDK_KEY_RELEASE:
+ switch (get_latin_keyval(&event->key)) {
+ case GDK_KEY_Control_L:
+ case GDK_KEY_Control_R:
+ message_context->clear();
+ break;
+
+ default:
+ break;
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ if (!ret) {
+ ret = DynamicBase::root_handler(event);
+ }
+ return ret;
+}
+
+/** Analyses and handles a key press event, returns true if processed, false if not. */
+bool EraserTool::_handleKeypress(const GdkEventKey *key)
+{
+ bool ret = false;
+ bool just_ctrl = (key->state & GDK_CONTROL_MASK) // Ctrl key is down
+ && !(key->state & (GDK_MOD1_MASK | GDK_SHIFT_MASK)); // but not Alt or Shift
+
+ bool just_alt = (key->state & GDK_MOD1_MASK) // Alt is down
+ && !(key->state & (GDK_CONTROL_MASK | GDK_SHIFT_MASK)); // but not Ctrl or Shift
+
+ switch (get_latin_keyval(key)) {
+ case GDK_KEY_Right:
+ case GDK_KEY_KP_Right:
+ if (!just_ctrl) {
+ width += 0.01;
+ if (width > 1.0) {
+ width = 1.0;
+ }
+ // Alt+X sets focus to this spinbutton as well
+ _desktop->setToolboxAdjustmentValue("eraser-width", width * 100);
+ ret = true;
+ }
+ break;
+
+ case GDK_KEY_Left:
+ case GDK_KEY_KP_Left:
+ if (!just_ctrl) {
+ width -= 0.01;
+ if (width < 0.01) {
+ width = 0.01;
+ }
+ _desktop->setToolboxAdjustmentValue("eraser-width", width * 100);
+ ret = true;
+ }
+ break;
+
+ case GDK_KEY_Home:
+ case GDK_KEY_KP_Home:
+ width = 0.01;
+ _desktop->setToolboxAdjustmentValue("eraser-width", width * 100);
+ ret = true;
+ break;
+
+ case GDK_KEY_End:
+ case GDK_KEY_KP_End:
+ width = 1.0;
+ _desktop->setToolboxAdjustmentValue("eraser-width", width * 100);
+ ret = true;
+ break;
+
+ case GDK_KEY_x:
+ case GDK_KEY_X:
+ if (just_alt) {
+ _desktop->setToolboxFocusTo("eraser-width");
+ ret = true;
+ }
+ break;
+
+ case GDK_KEY_Escape:
+ if (mode == EraserToolMode::DELETE) {
+ Inkscape::Rubberband::get(_desktop)->stop();
+ }
+ if (is_drawing) {
+ // if drawing, cancel, otherwise pass it up for deselecting
+ _cancel();
+ ret = true;
+ }
+ break;
+
+ case GDK_KEY_z:
+ case GDK_KEY_Z:
+ if (just_ctrl && is_drawing) { // Ctrl+Z pressed while drawing
+ _cancel();
+ ret = true;
+ } // if not drawing, pass it up for undo
+ break;
+
+ default:
+ break;
+ }
+ return ret;
+}
+
+/** Inserts the temporary red shape of the eraser stroke (the "acid") into the document.
+ * @return a pointer to the inserted item
+ */
+SPItem *EraserTool::_insertAcidIntoDocument(SPDocument *document)
+{
+ auto *top_layer = _desktop->layerManager().currentRoot();
+ auto *eraser_item = cast<SPItem>(top_layer->appendChildRepr(repr));
+ Inkscape::GC::release(repr);
+ eraser_item->updateRepr();
+ Geom::PathVector pathv = accumulated.get_pathvector() * _desktop->dt2doc();
+ pathv *= eraser_item->i2doc_affine().inverse();
+ repr->setAttribute("d", sp_svg_write_path(pathv));
+ return cast<SPItem>(document->getObjectByRepr(repr));
+}
+
+void EraserTool::_clearCurrent()
+{
+ // reset bpath
+ currentshape->set_bpath(nullptr);
+
+ // reset curve
+ currentcurve.reset();
+ cal1.reset();
+ cal2.reset();
+
+ // reset points
+ npoints = 0;
+}
+
+/**
+ * @brief Performs the actual erase operation against the current document
+ * @return whether actual erasing took place (and undo history should be updated).
+ */
+bool EraserTool::_doWork()
+{
+ if (accumulated.is_empty()) {
+ if (repr) {
+ sp_repr_unparent(repr);
+ repr = nullptr;
+ }
+ return false;
+ }
+
+ SPDocument *document = _desktop->getDocument();
+ if (!repr) {
+ // Create eraser repr
+ Inkscape::XML::Document *xml_doc = document->getReprDoc();
+ Inkscape::XML::Node *eraser_repr = xml_doc->createElement("svg:path");
+
+ sp_desktop_apply_style_tool(_desktop, eraser_repr, "/tools/eraser", false);
+ repr = eraser_repr;
+ }
+ if (!repr) {
+ return false;
+ }
+
+ Selection *selection = _desktop->getSelection();
+ if (!selection) {
+ return false;
+ }
+ bool was_selection = !selection->isEmpty();
+
+ // Find items to work on as well as items that will be needed to restore the selection afterwards.
+ _survivers.clear();
+ _clearStatusBar();
+
+ std::vector<EraseTarget> to_erase = _findItemsToErase();
+
+ bool work_done = false;
+ if (!to_erase.empty()) {
+ selection->clear();
+ work_done = _performEraseOperation(to_erase, true);
+ if (was_selection && !_survivers.empty()) {
+ selection->add(_survivers.begin(), _survivers.end());
+ }
+ }
+ // Clean up the eraser stroke repr:
+ sp_repr_unparent(repr);
+ repr = nullptr;
+ _acid = nullptr;
+ return work_done;
+}
+
+/**
+ * @brief Erases from a shape by cutting (boolean difference or cut operation).
+ * @param target - the item to be erased
+ * @param store_survivers - whether the surviving selected items and their remains should be stored.
+ * @return whether the target was successfully processed.
+ */
+bool EraserTool::_cutErase(EraseTarget target, bool store_survivers)
+{
+ // If the item is a clone, we check if the original is cuttable before unlinking it
+ if (auto use = cast<SPUse>(target.item)) {
+ auto original = use->trueOriginal();
+ if (_uncuttableItemType(original)) {
+ if (store_survivers && target.was_selected) {
+ _survivers.push_back(target.item);
+ }
+ return false;
+ } else if (auto *group = cast<SPGroup>(original)) {
+ return _probeUnlinkCutClonedGroup(target, use, group, store_survivers);
+ }
+ // A simple clone of a cuttable item: unlink and erase it.
+ target.item = use->unlink();
+ if (target.was_selected && store_survivers) { // Reselect the freshly unlinked item
+ _survivers.push_back(target.item);
+ }
+ }
+ return _booleanErase(target, store_survivers);
+}
+
+/**
+ * @brief Analyses a cloned group and decides if the CUT mode should unlink the clone.
+ * The decision to unlink the clone is based on collision detection between the eraser stroke
+ * and any of the eraseable contents of the cloned group, in the clone's coordinates.
+ * Unlinking only happens if there's an overlap between the eraser stroke and something that
+ * can be erased in CUT mode (via boolean operations).
+ * If the decision is made to unlink the clone, a copy of the clone is inserted into the document,
+ * and the function then erases all elements of the newly inserted group.
+ * @param original_target - the original erase target which turned out to be a clone.
+ * @param clone - the pointer to the SPUse object representing the clone (assument non-null).
+ * @param cloned_group - the original group that is cloned (at the origin of the USE chain).
+ * @param store_survivers - whether the surviving selected items and their remains should be stored.
+ * @return whether the clone was unlinked and something was erased from the resulting new group.
+ */
+bool EraserTool::_probeUnlinkCutClonedGroup(EraseTarget &original_target, SPUse *clone, SPGroup *cloned_group,
+ bool store_survivers)
+{
+ std::vector<EraseTarget> children;
+ children.reserve(cloned_group->getItemCount());
+
+ for (auto *child : cloned_group->childList(false)) {
+ children.emplace_back(cast<SPItem>(child), false);
+ }
+ auto const filtered_children = _filterCutEraseables(children, true);
+
+ // We must now check if any of the eraseable items in the original group, after transforming
+ // to the coordinates of the clone, actually intersect the eraser stroke.
+ Geom::Affine parent_inverse_transform;
+ if (auto *parent_item = cast<SPItem>(cloned_group->parent)) {
+ parent_inverse_transform = parent_item->i2doc_affine().inverse();
+ }
+ auto const relative_transform = parent_inverse_transform * clone->i2doc_affine();
+ auto const eraser_bounds = _acid->documentExactBounds();
+ if (!eraser_bounds) {
+ return false;
+ }
+ auto const eraser_in_group_coordinates = *eraser_bounds * relative_transform.inverse();
+ bool found_collision = false;
+ for (auto const &orig_child : filtered_children) {
+ if (orig_child.item->collidesWith(eraser_in_group_coordinates)) {
+ found_collision = true;
+ break;
+ }
+ }
+ if (found_collision) {
+ auto *unlinked = cast<SPGroup>(clone->unlink());
+ if (!unlinked) {
+ return false;
+ }
+ std::vector<EraseTarget> unlinked_children;
+ unlinked_children.reserve(filtered_children.size());
+
+ for (auto *child : unlinked->childList(false)) {
+ unlinked_children.emplace_back(cast<SPItem>(child), false);
+ }
+ auto overlapping = _filterCutEraseables(_filterByCollision(unlinked_children, _acid));
+
+ // If the clone was selected, the newly unlinked group should stay selected
+ if (original_target.was_selected && store_survivers) {
+ _survivers.push_back(unlinked);
+ }
+
+ return _performEraseOperation(overlapping, false);
+ } else {
+ if (original_target.was_selected && store_survivers) {
+ _survivers.push_back(original_target.item); // If the clone was selected, it should stay so
+ }
+ if (filtered_children.size() < children.size()) {
+ auto non_eraseable_touched = [&](EraseTarget const &t) -> bool {
+ if (!t.item || !_uncuttableItemType(t.item)) {
+ return false;
+ }
+ return t.item->collidesWith(eraser_in_group_coordinates);
+ };
+ if (std::any_of(children.begin(), children.end(), non_eraseable_touched)) {
+ _setStatusBarMessage(_("Some objects could not be cut."));
+ }
+ }
+ return false;
+ }
+}
+
+/** Returns error flags for items that cannot be meaningfully erased in CUT mode */
+EraserTool::Error EraserTool::_uncuttableItemType(SPItem *item)
+{
+ if (!item) {
+ return NON_EXISTENT;
+ } else if (is<SPImage>(item)) {
+ return RASTER_IMAGE;
+ } else if (_isStraightSegment(item)) {
+ return NO_AREA_PATH;
+ } else {
+ return ALL_GOOD;
+ }
+}
+
+/**
+ * @brief Performs a boolean difference or cut operation which implements the CUT mode erasure.
+ * @param target - the item to be erased.
+ * @param store_survivers - whether the surviving selected items and their remains should be stored.
+ * @return true on success, false on failure
+ */
+bool EraserTool::_booleanErase(EraseTarget target, bool store_survivers)
+{
+ if (!target.item) {
+ return false;
+ }
+ XML::Document *xml_doc = _desktop->doc()->getReprDoc();
+ XML::Node *duplicate_stroke = repr->duplicate(xml_doc);
+ repr->parent()->appendChild(duplicate_stroke);
+ Glib::ustring duplicate_id = duplicate_stroke->attribute("id");
+ GC::release(duplicate_stroke); // parent takes over
+ ObjectSet operands(_desktop);
+ operands.set(duplicate_stroke);
+ if (!nowidth) {
+ operands.pathUnion(true, true);
+ }
+ operands.add(target.item);
+ operands.removeLPESRecursive(true);
+
+ _handleStrokeStyle(target.item);
+
+ if (nowidth) {
+ operands.pathCut(true, true);
+ } else {
+ operands.pathDiff(true, true);
+ }
+ if (auto *spill = _desktop->doc()->getObjectById(duplicate_id)) {
+ operands.remove(spill);
+ spill->deleteObject(false);
+ return false;
+ }
+ if (!_break_apart) {
+ operands.combine(true, true);
+ } else if (!nowidth) {
+ operands.breakApart(true, false, true);
+ }
+ if (store_survivers && target.was_selected) {
+ _survivers.insert(_survivers.end(), operands.items().begin(), operands.items().end());
+ }
+ return true;
+}
+
+/**
+ * @brief Performs the actual erasing on a collection of erase targets.
+ * In CUT mode, the optional survivers vector will be populated with leftover pieces of
+ * partially erased shapes that used to be selected.
+ * @param items_to_erase - a non-empty vector of erase targets.
+ * @param store_survivers - whether the surviving selected items and their remains should be stored.
+ * @return whether something was actually erased.
+ */
+bool EraserTool::_performEraseOperation(std::vector<EraseTarget> const &items_to_erase, bool store_survivers)
+{
+ if (mode == EraserToolMode::CUT) {
+ bool erased_something = false;
+ for (auto const &target : items_to_erase) {
+ erased_something = _cutErase(target, store_survivers) || erased_something;
+ }
+ return erased_something;
+ } else if (mode == EraserToolMode::CLIP) {
+ if (nowidth) {
+ return false;
+ }
+ for (auto const &target : items_to_erase) {
+ _clipErase(target.item);
+ }
+ return true;
+ } else { // mode == EraserToolMode::DELETE
+ for (auto const &target : items_to_erase) {
+ if (target.item) {
+ target.item->deleteObject(true);
+ }
+ }
+ return true;
+ }
+}
+
+/** Handles the "evenodd" stroke style */
+void EraserTool::_handleStrokeStyle(SPItem *item) const
+{
+ auto *style = item->style;
+ if (style && 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(_desktop, css);
+ sp_repr_css_attr_unref(css);
+ css = nullptr;
+ }
+}
+
+/** Sets an error message in the status bar */
+void EraserTool::_setStatusBarMessage(char *message)
+{
+ MessageId id = _desktop->messageStack()->flash(WARNING_MESSAGE, message);
+ _our_messages.push_back(id);
+}
+
+/** Clears all of messages sent by us to the status bar */
+void EraserTool::_clearStatusBar()
+{
+ if (!_our_messages.empty()) {
+ auto ms = _desktop->messageStack();
+ for (MessageId id : _our_messages) {
+ ms->cancel(id);
+ }
+ _our_messages.clear();
+ }
+}
+
+/** Clips through an item */
+void EraserTool::_clipErase(SPItem *item) const
+{
+ Inkscape::ObjectSet w_selection(_desktop);
+ Geom::OptRect bbox = item->documentVisualBounds();
+ Inkscape::XML::Document *xml_doc = _desktop->doc()->getReprDoc();
+ Inkscape::XML::Node *dup = repr->duplicate(xml_doc);
+ repr->parent()->appendChild(dup);
+ Inkscape::GC::release(dup); // parent takes over
+ w_selection.set(dup);
+ w_selection.pathUnion(true);
+ bool delete_old_clip_path = false;
+ SPClipPath *clip_path = item->getClipObject();
+ if (clip_path) {
+ std::vector<SPItem *> selected;
+ selected.push_back(cast<SPItem>(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 = cast<SPItem>(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) {
+ auto dup_clip_obj = cast<SPItem>(item->parent->appendChildRepr(dup_clip));
+ Inkscape::GC::release(dup_clip);
+ if (dup_clip_obj) {
+ dup_clip_obj->transform *= item->getRelativeTransform(cast<SPItem>(item->parent));
+ dup_clip_obj->updateRepr();
+ delete_old_clip_path = true;
+ w_selection.raiseToTop(true);
+ w_selection.add(dup_clip);
+ w_selection.pathDiff(true, true);
+ }
+ }
+ }
+ } else {
+ Inkscape::XML::Node *rect_repr = xml_doc->createElement("svg:rect");
+ sp_desktop_apply_style_tool(_desktop, rect_repr, "/tools/eraser", false);
+ auto rect = cast<SPRect>(item->parent->appendChildRepr(rect_repr));
+ Inkscape::GC::release(rect_repr);
+ rect->setPosition(bbox->left(), bbox->top(), bbox->width(), bbox->height());
+ rect->transform = cast<SPItem>(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, true);
+ }
+ w_selection.raiseToTop(true);
+ w_selection.add(item);
+ w_selection.setMask(true, false, true);
+ if (delete_old_clip_path) {
+ clip_path->deleteObject(true);
+ }
+}
+
+/** Detects whether the given path is a straight line segment which encloses no area
+ or consists of several such segments */
+bool EraserTool::_isStraightSegment(SPItem *path)
+{
+ auto as_path = cast<SPPath>(path);
+ if (!as_path) {
+ return false;
+ }
+
+ auto const &curve = as_path->curve();
+ if (!curve) {
+ return false;
+ }
+ auto const &pathvector = curve->get_pathvector();
+
+ // Check if all segments are straight and collinear
+ for (auto const &path : pathvector) {
+ Geom::Point initial_tangent = path.front().unitTangentAt(0.0);
+ for (auto const &segment : path) {
+ if (!segment.isLineSegment()) {
+ return false;
+ } else {
+ Geom::Point dir = segment.unitTangentAt(0.0);
+ if (!Geom::are_near(dir, initial_tangent) && !Geom::are_near(-dir, initial_tangent)) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+}
+
+void EraserTool::_addCap(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) / M_SQRT2;
+ double mag = Geom::L2(vel);
+
+ Geom::Point v_in = from - pre;
+ double mag_in = Geom::L2(v_in);
+
+ if (mag_in > 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 > epsilon) {
+ v_out = mag * v_out / mag_out;
+ } else {
+ v_out = Geom::Point(0, 0);
+ }
+
+ if (Geom::L2(v_in) > epsilon || Geom::L2(v_out) > 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 (!cal1.get_segment_count() || !cal2.get_segment_count()) {
+ return;
+ }
+
+ auto rev_cal2 = cal2.reversed();
+
+ g_assert(!cal1.first_path()->closed());
+ g_assert(!rev_cal2.first_path()->closed());
+
+ Geom::BezierCurve const *dc_cal1_firstseg = dynamic_cast<Geom::BezierCurve const *>(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 *>(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);
+
+ accumulated.append(cal1);
+ if (!nowidth) {
+ _addCap(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),
+ cap_rounding);
+
+ accumulated.append(rev_cal2, true);
+
+ _addCap(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),
+ cap_rounding);
+
+ accumulated.closepath();
+ }
+ cal1.reset();
+ cal2.reset();
+}
+
+/**
+ * @brief Filters out elements that can be erased in CUT mode (by boolean operations) from the given
+ * vector of potential erase targets. For items that cannot be erased in the CUT mode, a
+ * warning message can be flashed in the status bar.
+ * @param items - a vector containing EraseTarget structs
+ * @param silent - if set to true, the status bar messages will not be shown.
+ * @return a filtered vector whose elements can be erased in CUT mode
+*/
+std::vector<EraseTarget> EraserTool::_filterCutEraseables(std::vector<EraseTarget> const &items, bool silent)
+{
+ std::vector<EraseTarget> result;
+ result.reserve(items.size());
+
+ for (auto &target : items) {
+ if (Error e = _uncuttableItemType(target.item)) {
+ if (!silent) {
+ if (e & RASTER_IMAGE) {
+ _setStatusBarMessage(_("Cannot cut out from a bitmap, use <b>Clip</b> mode "
+ "instead."));
+ } else if (e & NO_AREA_PATH) {
+ _setStatusBarMessage(_("Cannot cut out from a path with zero area, use "
+ "<b>Clip</b> mode instead."));
+ }
+ }
+ } else {
+ result.push_back(target);
+ }
+ }
+ return result;
+}
+
+/**
+ * @brief Filters a list of potential erase targets by collision with a given item
+ * @param items - a vector of EraseTarget elements to be filtered
+ * @param with - a pointer to an SPItem to check collisions with
+ * @return a new vector containing those elements of `items` that have a collision with `with`.
+ */
+std::vector<EraseTarget> EraserTool::_filterByCollision(std::vector<EraseTarget> const &items, SPItem *with) const
+{
+ std::vector<EraseTarget> result;
+ if (!with) {
+ return result;
+ }
+ result.reserve(items.size());
+
+ if (auto const collision_shape = with->documentExactBounds()) {
+ for (auto const &target : items) {
+ if (target.item && target.item->collidesWith(*collision_shape)) {
+ result.push_back(target);
+ }
+ }
+ }
+ return result;
+}
+
+/**
+ * @brief Prepares a list of items in the current document containing the items which qualify
+ * for the erase operation (based on selection & collision detection).
+ * Additionally, the selected items which are going to survive the erase operation (and
+ * should be used to restore the selection afterwards) will be added to the _survivers member.
+ * If the user attempts to erase an illegal item, a warning message is shown in the status bar.
+ * @return items that should undergo the erase operation
+ */
+std::vector<EraseTarget> EraserTool::_findItemsToErase()
+{
+ std::vector<EraseTarget> result;
+
+ auto *document = _desktop->getDocument();
+ auto *selection = _desktop->getSelection();
+ if (!document || !selection) {
+ return result;
+ }
+
+ if (mode == EraserToolMode::DELETE) {
+ // In DELETE mode, the classification is based on having been touched by the mouse cursor:
+ // * result should contain touched items;
+ // * _survivers should contain selected but untouched items.
+ auto *r = Rubberband::get(_desktop);
+ std::vector<SPItem *> touched = document->getItemsAtPoints(_desktop->dkey, r->getPoints());
+ if (selection->isEmpty()) {
+ for (auto *item : touched) {
+ result.emplace_back(item, false);
+ }
+ } else {
+ for (auto *item : selection->items()) {
+ if (std::find(touched.begin(), touched.end(), item) == touched.end()) {
+ _survivers.push_back(item);
+ } else {
+ result.emplace_back(item, true);
+ }
+ }
+ }
+ } else {
+ // In the other modes, we start with a crude filtering step based on bounding boxes
+ _acid = _insertAcidIntoDocument(document);
+ if (!_acid) {
+ return result;
+ }
+ Geom::OptRect eraser_bbox = _acid->documentVisualBounds();
+ if (!eraser_bbox) {
+ return result;
+ }
+ std::vector<SPItem *> candidates = document->getItemsPartiallyInBox(_desktop->dkey, *eraser_bbox,
+ false, false, false, true);
+ std::vector<EraseTarget> allowed; ///< Items we're allowed to erase based on selection
+ allowed.reserve(candidates.size());
+
+ // If selection is empty, we're allowed to erase all items except the eraser stroke itself.
+ if (selection->isEmpty()) {
+ for (auto *candidate : candidates) {
+ if (candidate != _acid) {
+ allowed.emplace_back(candidate, false);
+ }
+ }
+ } // How we handle non-empty selection further depends on the mode.
+
+ if (mode == EraserToolMode::CUT) {
+ // In CUT mode, we must unpack groups, since the boolean difference/cut operation
+ // doesn't make sense for a group.
+ for (auto *selected : selection->items()) {
+ bool included_for_erase = false;
+ for (auto *candidate : candidates) {
+ if (selected == candidate || selected->isAncestorOf(candidate)) {
+ allowed.emplace_back(candidate, selection->includes(candidate));
+ included_for_erase = (candidate == selected) || included_for_erase;
+ }
+ }
+ if (!included_for_erase) {
+ _survivers.push_back(selected);
+ }
+ }
+ // The filtering is based on a precise collision detection procedure:
+ // * result will contain all eraseable items that overlap with the eraser stroke;
+ // * _survivers will contain all selected items that were rejected during this filtering.
+ auto overlapping = _filterByCollision(allowed, _acid);
+ auto valid = _filterCutEraseables(overlapping); // Sets status bar messages
+
+ for (auto const &element : allowed) {
+ if (element.item && element.was_selected &&
+ std::find(valid.begin(), valid.end(), element) == valid.end())
+ {
+ _survivers.push_back(element.item);
+ }
+ }
+ result.insert(result.end(), valid.begin(), valid.end());
+
+ } else if (mode == EraserToolMode::CLIP) {
+ // In CLIP mode, we don't check descendants, because clip can be set to an entire group.
+ auto const all_selected = selection->items();
+ for (auto *item : all_selected) {
+ allowed.emplace_back(item, true);
+ }
+
+ // The classification is also based on the precise collision detection:
+ // * result will contain all items that overlap with the eraser stroke;
+ // * _survivers will contain all selected items, since CLIP mode is always non-destructive.
+ auto overlapping = _filterByCollision(allowed, _acid);
+ result.insert(result.end(), overlapping.begin(), overlapping.end());
+ _survivers.insert(_survivers.end(), all_selected.begin(), all_selected.end());
+ }
+ }
+ return result;
+}
+
+void EraserTool::_fitAndSplit(bool releasing)
+{
+ double const tolerance_sq = square(_desktop->w2d().descrim() * tolerance);
+ nowidth = (width == 0); // setting width is managed by the base class
+
+#ifdef ERASER_VERBOSE
+ g_print("[F&S:R=%c]", releasing ? 'T' : 'F');
+#endif
+ if (npoints >= SAMPLING_SIZE || npoints <= 0) {
+ return; // just clicked
+ }
+
+ if (npoints == SAMPLING_SIZE - 1 || releasing) {
+ _completeBezier(tolerance_sq, releasing);
+
+#ifdef ERASER_VERBOSE
+ g_print("[%d]Yup\n", npoints);
+#endif
+ if (!releasing) {
+ _fitDrawLastPoint();
+ }
+
+ // Copy last point
+ point1[0] = point1[npoints - 1];
+ point2[0] = point2[npoints - 1];
+ npoints = 1;
+ } else {
+ _drawTemporaryBox();
+ }
+}
+
+void EraserTool::_completeBezier(double tolerance_sq, bool releasing)
+{
+ /* Current eraser */
+ if (cal1.is_empty() || cal2.is_empty()) {
+ /* dc->npoints > 0 */
+ cal1.reset();
+ cal2.reset();
+
+ cal1.moveto(point1[0]);
+ cal2.moveto(point2[0]);
+ }
+#ifdef ERASER_VERBOSE
+ g_print("[F&S:#] npoints:%d, releasing:%s\n", npoints, releasing ? "TRUE" : "FALSE");
+#endif
+
+ unsigned const bezier_size = 4;
+ unsigned const max_beziers = 8;
+ size_t const bezier_max_length = bezier_size * max_beziers;
+
+ Geom::Point b1[bezier_max_length];
+ gint const nb1 = Geom::bezier_fit_cubic_r(b1, point1, npoints, tolerance_sq, 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, point2, npoints, tolerance_sq, max_beziers);
+ g_assert(nb2 * bezier_size <= gint(G_N_ELEMENTS(b2)));
+
+ if (nb1 == -1 || nb2 == -1) {
+ _failedBezierFallback(); // TODO: do we ever need this?
+ return;
+ }
+
+ /* Fit and draw and reset state */
+#ifdef ERASER_VERBOSE
+ g_print("nb1:%d nb2:%d\n", nb1, nb2);
+#endif
+
+ /* CanvasShape */
+ if (!releasing) {
+ currentcurve.reset();
+ currentcurve.moveto(b1[0]);
+
+ for (Geom::Point *bp1 = b1; bp1 < b1 + bezier_size * nb1; bp1 += bezier_size) {
+ currentcurve.curveto(bp1[1], bp1[2], bp1[3]);
+ }
+
+ currentcurve.lineto(b2[bezier_size * (nb2 - 1) + 3]);
+
+ for (Geom::Point *bp2 = b2 + bezier_size * (nb2 - 1); bp2 >= b2; bp2 -= bezier_size) {
+ currentcurve.curveto(bp2[2], bp2[1], bp2[0]);
+ }
+
+ // FIXME: segments is always NULL at this point??
+ if (segments.empty()) { // first segment
+ _addCap(currentcurve, b2[1], b2[0], b1[0], b1[1], cap_rounding);
+ }
+
+ currentcurve.closepath();
+ currentshape->set_bpath(&currentcurve, true);
+ }
+
+ /* Current eraser */
+ for (Geom::Point *bp1 = b1; bp1 < b1 + bezier_size * nb1; bp1 += bezier_size) {
+ cal1.curveto(bp1[1], bp1[2], bp1[3]);
+ }
+
+ for (Geom::Point *bp2 = b2; bp2 < b2 + bezier_size * nb2; bp2 += bezier_size) {
+ cal2.curveto(bp2[1], bp2[2], bp2[3]);
+ }
+}
+
+void EraserTool::_failedBezierFallback()
+{
+ /* fixme: ??? */
+#ifdef ERASER_VERBOSE
+ g_print("[_failedBezierFallback] - failed to fit cubic.\n");
+#endif
+ _drawTemporaryBox();
+
+ for (gint i = 1; i < npoints; i++) {
+ cal1.lineto(point1[i]);
+ }
+
+ for (gint i = 1; i < npoints; i++) {
+ cal2.lineto(point2[i]);
+ }
+}
+
+void EraserTool::_fitDrawLastPoint()
+{
+ g_assert(!currentcurve.is_empty());
+
+ guint32 fillColor = sp_desktop_get_color_tool(_desktop, "/tools/eraser", true);
+ double opacity = sp_desktop_get_master_opacity_tool(_desktop, "/tools/eraser");
+ double fillOpacity = sp_desktop_get_opacity_tool(_desktop, "/tools/eraser", true);
+
+ guint fill = (fillColor & 0xffffff00) | SP_COLOR_F_TO_U(opacity * fillOpacity);
+
+ auto cbp = new Inkscape::CanvasItemBpath(_desktop->getCanvasSketch(), currentcurve.get_pathvector(), true);
+ cbp->set_fill(fill, trace_wind_rule);
+ cbp->set_stroke(0x0);
+
+ /* fixme: Cannot we cascade it to root more clearly? */
+ cbp->connect_event(sigc::bind(sigc::ptr_fun(sp_desktop_root_handler), _desktop));
+ segments.emplace_back(cbp);
+
+ if (mode == EraserToolMode::DELETE) {
+ cbp->hide();
+ currentshape->hide();
+ }
+}
+
+void EraserTool::_drawTemporaryBox()
+{
+ currentcurve.reset();
+
+ currentcurve.moveto(point1[npoints - 1]);
+
+ for (gint i = npoints - 2; i >= 0; i--) {
+ currentcurve.lineto(point1[i]);
+ }
+
+ for (gint i = 0; i < npoints; i++) {
+ currentcurve.lineto(point2[i]);
+ }
+
+ if (npoints >= 2) {
+ _addCap(currentcurve,
+ point2[npoints - 2], point2[npoints - 1],
+ point1[npoints - 1], point1[npoints - 2], cap_rounding);
+ }
+
+ currentcurve.closepath();
+ currentshape->set_bpath(&currentcurve, true);
+}
+
+} // namespace Tools
+} // 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/tools/eraser-tool.h b/src/ui/tools/eraser-tool.h
new file mode 100644
index 0000000..5198ebd
--- /dev/null
+++ b/src/ui/tools/eraser-tool.h
@@ -0,0 +1,155 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef ERASER_TOOL_H_SEEN
+#define ERASER_TOOL_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 "message-stack.h"
+#include "style.h"
+#include "ui/tools/dynamic-base.h"
+#include "object/sp-use.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+enum class EraserToolMode
+{
+ DELETE,
+ CUT,
+ CLIP
+};
+static inline constexpr auto DEFAULT_ERASER_MODE = EraserToolMode::CUT;
+
+/** Represents an item to erase */
+struct EraseTarget
+{
+ SPItem *item = nullptr; ///< Pointer to the item to be erased
+ bool was_selected = false; ///< Whether the item was part of selection
+
+ EraseTarget(SPItem *ptr, bool sel)
+ : item{ptr}
+ , was_selected{sel}
+ {}
+ inline bool operator==(EraseTarget const &other) const noexcept { return item == other.item; }
+};
+
+class EraserTool : public DynamicBase {
+
+private:
+ // non-static data:
+ EraserToolMode mode = DEFAULT_ERASER_MODE;
+ bool nowidth = false;
+ std::vector<MessageId> _our_messages;
+ SPItem *_acid = nullptr;
+ std::vector<SPItem *> _survivers;
+ Pref<bool> _break_apart;
+ Pref<int> _mode_int;
+
+ // static data:
+ static constexpr uint32_t trace_color_rgba = 0xff0000ff; // RGBA red
+ static constexpr SPWindRule trace_wind_rule = SP_WIND_RULE_EVENODD;
+
+ static constexpr double tolerance = 0.1;
+
+ static constexpr double epsilon = 0.5e-6;
+ static constexpr double epsilon_start = 0.5e-2;
+ static constexpr double vel_start = 1e-5;
+
+ static constexpr double drag_default = 1.0;
+ static constexpr double drag_min = 0.0;
+ static constexpr double drag_max = 1.0;
+
+ static constexpr double min_pressure = 0.0;
+ static constexpr double max_pressure = 1.0;
+ static constexpr double default_pressure = 1.0;
+
+ static constexpr double min_tilt = -1.0;
+ static constexpr double max_tilt = 1.0;
+ static constexpr double default_tilt = 0.0;
+
+public:
+ // public member functions
+ EraserTool(SPDesktop *desktop);
+ ~EraserTool() override;
+ bool root_handler(GdkEvent *event) final;
+
+ using Error = std::uint64_t;
+ static constexpr Error ALL_GOOD = 0x0;
+ static constexpr Error NON_EXISTENT = 0x1 << 1;
+ static constexpr Error NO_AREA_PATH = 0x1 << 2;
+ static constexpr Error RASTER_IMAGE = 0x1 << 3;
+ static constexpr Error ERROR_GROUP = 0x1 << 4;
+
+private:
+ // private member functions
+ void _accumulate();
+ bool _apply(Geom::Point p);
+ bool _booleanErase(EraseTarget target, bool store_survivers);
+ void _brush();
+ void _cancel();
+ void _clearCurrent();
+ void _clearStatusBar();
+ void _clipErase(SPItem *item) const;
+ void _completeBezier(double tolerance_sq, bool releasing);
+ bool _cutErase(EraseTarget target, bool store_survivers);
+ bool _doWork();
+ void _drawTemporaryBox();
+ void _extinput(GdkEvent *event);
+ void _failedBezierFallback();
+ std::vector<EraseTarget> _filterByCollision(std::vector<EraseTarget> const &items, SPItem *with) const;
+ std::vector<EraseTarget> _filterCutEraseables(std::vector<EraseTarget> const &items, bool silent = false);
+ std::vector<EraseTarget> _findItemsToErase();
+ void _fitAndSplit(bool releasing);
+ void _fitDrawLastPoint();
+ bool _handleKeypress(GdkEventKey const *key);
+ void _handleStrokeStyle(SPItem *item) const;
+ SPItem *_insertAcidIntoDocument(SPDocument *document);
+ bool _performEraseOperation(std::vector<EraseTarget> const &items_to_erase, bool store_survivers);
+ void _reset(Geom::Point p);
+ void _setStatusBarMessage(char *message);
+ void _updateMode();
+
+ static void _generateNormalDist2(double &r1, double &r2);
+ static void _addCap(SPCurve &curve, Geom::Point const &pre, Geom::Point const &from, Geom::Point const &to,
+ Geom::Point const &post, double rounding);
+ static bool _isStraightSegment(SPItem *path);
+ static Error _uncuttableItemType(SPItem *item);
+ bool _probeUnlinkCutClonedGroup(EraseTarget &original_target, SPUse* clone, SPGroup* cloned_group,
+ bool store_survivers = true);
+};
+
+} // namespace Tools
+} // namespace UI
+} // namespace Inkscape
+
+#endif // ERASER_TOOL_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..3e94f35
--- /dev/null
+++ b/src/ui/tools/flood-tool.cpp
@@ -0,0 +1,1230 @@
+// 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 "async/progress.h"
+#include "color.h"
+#include "context-fns.h"
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "layer-manager.h"
+#include "message-context.h"
+#include "message-stack.h"
+#include "rubberband.h"
+#include "selection.h"
+#include "page-manager.h"
+
+#include "display/cairo-utils.h"
+#include "display/drawing-context.h"
+#include "display/drawing-image.h"
+#include "display/drawing.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 "svg/svg.h"
+
+#include "trace/imagemap.h"
+#include "trace/potrace/inkscape-potrace.h"
+
+#include "ui/icon-names.h"
+#include "ui/shape-editor.h"
+#include "ui/widget/canvas.h" // Canvas area
+
+using Inkscape::DocumentUndo;
+
+using Inkscape::Display::ExtractARGB32;
+using Inkscape::Display::ExtractRGB32;
+using Inkscape::Display::AssembleARGB32;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+// 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(SPDesktop *desktop)
+ : ToolBase(desktop, "/tools/paintbucket", "flood.svg")
+ , item(nullptr)
+{
+ // TODO: Why does the flood tool use a hardcoded tolerance instead of a pref?
+ this->tolerance = 4;
+
+ this->shape_editor = new ShapeEditor(desktop);
+
+ SPItem *item = desktop->getSelection()->singleItem();
+ if (item) {
+ this->shape_editor->set_item(item);
+ }
+
+ this->sel_changed_connection.disconnect();
+ this->sel_changed_connection = desktop->getSelection()->connectChanged(
+ sigc::mem_fun(*this, &FloodTool::selection_changed)
+ );
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (prefs->getBool("/tools/paintbucket/selcue")) {
+ this->enableSelectionCue();
+ }
+}
+
+FloodTool::~FloodTool() {
+ this->sel_changed_connection.disconnect();
+
+ delete shape_editor;
+ 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());
+}
+
+// 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_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;
+
+ auto gray_map = Trace::GrayMap(max_x - min_x + 1, max_y - min_y + 1);
+ unsigned gray_map_y = 0;
+ for (unsigned y = min_y; y <= max_y; y++) {
+ auto gray_map_t = gray_map.row(gray_map_y);
+
+ trace_t = get_trace_pixel(trace_px, min_x, y, bci.width);
+ for (unsigned x = min_x; x <= max_x; x++) {
+ *gray_map_t = is_pixel_colored(trace_t) ? Trace::GrayMap::BLACK : Trace::GrayMap::WHITE;
+ gray_map_t++;
+ trace_t++;
+ }
+ gray_map_y++;
+ }
+
+ Trace::Potrace::PotraceTracingEngine pte;
+ auto progress = Async::ProgressAlways<double>();
+ auto results = pte.traceGrayMap(gray_map, progress);
+
+ // XML Tree being used here directly while it shouldn't be...."
+ Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc();
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double offset = prefs->getDouble("/tools/paintbucket/offset", 0.0);
+
+ for (auto result : results) {
+
+ Inkscape::XML::Node *pathRepr = xml_doc->createElement("svg:path");
+ /* Set style */
+ sp_desktop_apply_style_tool (desktop, pathRepr, "/tools/paintbucket", false);
+
+ Path *path = new Path;
+ path->LoadPathVector(result.path);
+
+ 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);
+ }
+
+ auto layer = desktop->layerManager().currentLayer();
+ layer->addChild(pathRepr, nullptr);
+
+ SPObject *reprobj = document->getObjectByRepr(pathRepr);
+ if (reprobj) {
+ cast<SPItem>(reprobj)->doWriteTransform(transform);
+
+ // premultiply the item transform by the accumulated parent transform in the paste layer
+ Geom::Affine local (layer->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)
+ pathRepr->setAttributeOrRemoveIfEmpty("transform", sp_svg_transform_write(item_t));
+ }
+
+ 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.",
+ cast<SPPath>(reprobj)->nodesInPath()), cast<SPPath>(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.",
+ cast<SPPath>(reprobj)->nodesInPath()), cast<SPPath>(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 desktop The desktop of this tool's event context.
+ * @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(SPDesktop *desktop, GdkEvent *event,
+ bool union_with_selection, bool is_point_fill, bool is_touch_fill) {
+
+ 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;
+
+ // image space is world space with an offset
+ Geom::Rect const screen_world = desktop->getCanvas()->get_area_world();
+ Geom::Rect const screen = screen_world * desktop->w2d();
+ 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
+
+ bgcolor = document->getPageManager().background_color;
+ bgcolor &= 0xffffff00; // make color transparent for 'alpha' flood mode to work
+ // bgcolor is 0xrrggbbaa, we need 0xaarrggbb
+ dtc = bgcolor >> 8; // keep color transparent; page color doesn't support transparency anymore
+
+ 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, _("Fill bounded area"), INKSCAPE_ICON("color-fill"));
+}
+
+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) {
+ 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(), _("Set style on object"), INKSCAPE_ICON("color-fill"));
+ // 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) {
+ 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 )) {
+ 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) {
+ Inkscape::Rubberband *r = Inkscape::Rubberband::get(_desktop);
+
+ if (r->is_started()) {
+ dragging = false;
+ bool is_point_fill = this->within_tolerance;
+ bool is_touch_fill = event->button.state & GDK_MOD1_MASK;
+
+ // It's possible for the user to sneakily change the tool while the
+ // Gtk main loop has control, so we save the current desktop address:
+ SPDesktop* current_desktop = _desktop;
+
+ current_desktop->setWaitingCursor();
+ sp_flood_do_flood_fill(current_desktop, event,
+ event->button.state & GDK_SHIFT_MASK,
+ is_point_fill, is_touch_fill);
+ current_desktop->clearWaitingCursor();
+ r->stop();
+
+ // We check whether our object was deleted by SPDesktop::setEventContext()
+ // TODO: fix SPDesktop so that it doesn't kill us before we're done
+ ToolBase *current_context = current_desktop->getEventContext();
+
+ if (current_context == (ToolBase*)this) { // We're still alive
+ this->defaultMessageContext()->clear();
+ } // else just return without dereferencing `this`.
+ 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;
+ 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->getSelection()->set(this->item);
+ DocumentUndo::done(_desktop->getDocument(), _("Fill bounded area"), INKSCAPE_ICON("color-fill"));
+
+ 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..290021e
--- /dev/null
+++ b/src/ui/tools/flood-tool.h
@@ -0,0 +1,67 @@
+// 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(SPDesktop *desktop);
+ ~FloodTool() override;
+
+ SPItem *item;
+
+ sigc::connection sel_changed_connection;
+
+ bool root_handler(GdkEvent* event) override;
+ bool item_handler(SPItem* item, GdkEvent* event) 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..35cf119
--- /dev/null
+++ b/src/ui/tools/freehand-base.cpp
@@ -0,0 +1,1007 @@
+// 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 "freehand-base.h"
+
+#include "desktop-style.h"
+#include "id-clash.h"
+#include "message-stack.h"
+#include "selection-chemistry.h"
+#include "style.h"
+
+#include "display/curve.h"
+#include "display/control/canvas-item-bpath.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 "object/sp-item-group.h"
+#include "object/sp-path.h"
+#include "object/sp-rect.h"
+#include "object/sp-use.h"
+
+#include "svg/svg-color.h"
+#include "svg/svg.h"
+
+#include "ui/clipboard.h"
+#include "ui/draw-anchor.h"
+#include "ui/icon-names.h"
+#include "ui/tools/lpe-tool.h" // TODO: Remove in the future
+#include "ui/tools/pencil-tool.h" // TODO: Remove in the future
+
+#define MIN_PRESSURE 0.0
+#define MAX_PRESSURE 1.0
+#define DEFAULT_PRESSURE 1.0
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+/**
+ * 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, std::shared_ptr<SPCurve> gc);
+
+static void spdc_free_colors(FreehandBase *dc);
+
+FreehandBase::FreehandBase(SPDesktop *desktop, std::string prefs_path, const std::string &cursor_filename)
+ : ToolBase(desktop, prefs_path, cursor_filename)
+ , selection(nullptr)
+ , red_color(0xff00007f)
+ , blue_color(0x0000ff7f)
+ , green_color(0x00ff007f)
+ , highlight_color(0x0000007f)
+ , green_closed(false)
+ , white_item(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)
+{
+ this->selection = desktop->getSelection();
+
+ // Connect signals to track selection changes
+ sel_changed_connection = selection->connectChanged([=](Selection *) { _attachSelection(); });
+ sel_modified_connection = selection->connectModified([=](Selection *, guint) { onSelectionModified(); });
+
+ // Create red bpath
+ this->red_bpath = make_canvasitem<CanvasItemBpath>(desktop->getCanvasSketch());
+ this->red_bpath->set_stroke(this->red_color);
+ this->red_bpath->set_fill(0x0, SP_WIND_RULE_NONZERO);
+
+ // Create blue bpath
+ this->blue_bpath = make_canvasitem<CanvasItemBpath>(desktop->getCanvasSketch());
+ this->blue_bpath->set_stroke(this->blue_color);
+ this->blue_bpath->set_fill(0x0, SP_WIND_RULE_NONZERO);
+
+ // Create green curve
+ green_curve = std::make_shared<SPCurve>();
+
+ // No green anchor by default
+ this->green_anchor = nullptr;
+ this->green_closed = false;
+
+ // Create start anchor alternative curve
+ this->sa_overwrited.reset(new SPCurve());
+
+ _attachSelection();
+}
+
+FreehandBase::~FreehandBase()
+{
+ this->sel_changed_connection.disconnect();
+ this->sel_modified_connection.disconnect();
+
+ ungrabCanvasEvents();
+
+ if (this->selection) {
+ this->selection = nullptr;
+ }
+
+ spdc_free_colors(this);
+}
+
+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;
+}
+
+std::optional<Geom::Point> FreehandBase::red_curve_get_last_point()
+{
+ std::optional<Geom::Point> p;
+ if (!red_curve.is_empty()) {
+ p = red_curve.last_point();
+ }
+ return p;
+}
+
+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->getDesktop()->doc();
+ Effect::createAndApply(PATTERN_ALONG_PATH, document, item);
+ Effect* lpe = cast<SPLPEItem>(item)->getCurrentLPE();
+ static_cast<LPEPatternAlongPath*>(lpe)->pattern.set_new_value(newpath,true);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double scale = prefs->getDouble("/live_effects/skeletal/width", 1);
+ if (!scale) {
+ scale = 1;
+ }
+ Inkscape::SVGOStringStream os;
+ os << scale;
+ lpe->getRepr()->setAttribute("prop_scale", os.str());
+}
+
+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)
+{
+ using namespace Inkscape::LivePathEffect;
+ SPDesktop *desktop = dc->getDesktop();
+ SPDocument *document = desktop->getDocument();
+ if (!document || !desktop) {
+ return;
+ }
+ if (SP_IS_PENCIL_CONTEXT(dc)) {
+ if (dc->tablet_enabled) {
+ SPObject *elemref = nullptr;
+ if ((elemref = document->getObjectById("power_stroke_preview"))) {
+ elemref->getRepr()->removeAttribute("style");
+ auto successor = cast<SPItem>(elemref);
+ sp_desktop_apply_style_tool(desktop, successor->getRepr(),
+ Glib::ustring("/tools/freehand/pencil").data(), false);
+ spdc_apply_style(successor);
+ sp_object_ref(item);
+ item->deleteObject(false);
+ item->setSuccessor(successor);
+ sp_object_unref(item);
+ item = successor;
+ dc->selection->set(item);
+ item->setLocked(false);
+ dc->white_item = item;
+ rename_id(item, "path-1");
+ }
+ return;
+ }
+ }
+ Effect::createAndApply(POWERSTROKE, document, item);
+ Effect* lpe = cast<SPLPEItem>(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("not_jump", "false");
+ 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");
+}
+
+static void spdc_apply_bend_shape(gchar const *svgd, FreehandBase *dc, SPItem *item)
+{
+ using namespace Inkscape::LivePathEffect;
+ auto use = cast<SPUse>(item);
+ if ( use ) {
+ return;
+ }
+ SPDesktop *desktop = dc->getDesktop();
+ SPDocument *document = desktop->getDocument();
+ if (!document || !desktop) {
+ return;
+ }
+ if (!is<SPLPEItem>(item)) {
+ return;
+ }
+ if(!cast_unsafe<SPLPEItem>(item)->hasPathEffectOfType(BEND_PATH)){
+ Effect::createAndApply(BEND_PATH, document, item);
+ }
+ Effect* lpe = cast<SPLPEItem>(item)->getCurrentLPE();
+
+ // write bend parameters:
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double scale = prefs->getDouble("/live_effects/bend_path/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);
+}
+
+static void spdc_apply_simplify(std::string threshold, FreehandBase *dc, SPItem *item)
+{
+ const SPDesktop *desktop = dc->getDesktop();
+ SPDocument *document = desktop->getDocument();
+ if (!document || !desktop) {
+ return;
+ }
+ using namespace Inkscape::LivePathEffect;
+
+ Effect::createAndApply(SIMPLIFY, document, item);
+ Effect* lpe = cast<SPLPEItem>(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");
+}
+
+static shapeType previous_shape_type = NONE;
+
+static void spdc_check_for_and_apply_waiting_LPE(FreehandBase *dc, SPItem *item, SPCurve const *curve, bool is_bend)
+{
+ using namespace Inkscape::LivePathEffect;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ auto *desktop = dc->getDesktop();
+
+ if (item && is<SPLPEItem>(item)) {
+ double defsize = 10 / (0.265 * dc->getDesktop()->getDocument()->getDocumentScale()[0]);
+#define SHAPE_LENGTH defsize
+#define SHAPE_HEIGHT defsize
+ //Store the clipboard path to apply in the future without the use of clipboard
+ static Geom::PathVector previous_shape_pathv;
+ static SPItem *bend_item = nullptr;
+ shapeType shape = (shapeType)prefs->getInt(dc->getPrefsPath() + "/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(desktop, true))
+ {
+ bend_item = dc->selection->singleItem();
+ if(!bend_item || (!is<SPShape>(bend_item) && !is<SPGroup>(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(dc->getPrefsPath() + "/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));
+ tol *= 10000;
+ std::ostringstream ss;
+ ss << tol;
+ spdc_apply_simplify(ss.str(), dc, item);
+ sp_lpe_item_update_patheffect(cast<SPLPEItem>(item), true, false);
+ }
+ if (prefs->getInt(dc->getPrefsPath() + "/freehand-mode", 0) == 1) {
+ Effect::createAndApply(SPIRO, dc->getDesktop()->getDocument(), item);
+ }
+
+ if (prefs->getInt(dc->getPrefsPath() + "/freehand-mode", 0) == 2) {
+ Effect::createAndApply(BSPLINE, dc->getDesktop()->getDocument(), item);
+ }
+ if (auto sp_shape = cast<SPShape>(item)) {
+ curve = sp_shape->curve();
+ }
+ 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", SHAPE_HEIGHT / 2);
+ if (!swidth) {
+ swidth = swidth/2;
+ }
+ swidth = std::abs(swidth);
+ guint curve_length = curve->get_segment_count();
+ 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;
+ }
+ }
+
+ 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 = false;
+ break;
+ }
+ case TRIANGLE_OUT:
+ {
+ // "triangle out"
+ 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 = false;
+ break;
+ }
+ case ELLIPSE:
+ {
+ // "ellipse"
+ SPCurve c;
+ constexpr 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);
+
+ shape_applied = true;
+ break;
+ }
+ case CLIPBOARD:
+ {
+ // take shape from clipboard;
+ Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get();
+ if(cm->paste(desktop,true)){
+ dc->selection->toCurves(true);
+ if (auto pasted_clipboard = dc->selection->singleItem()){
+ 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(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 && (is<SPShape>(bend_item) || is<SPGroup>(bend_item))){
+ // If item is a SPRect, convert it to path first:
+ if (is<SPRect>(bend_item) ) {
+ 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(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(bend_item);
+ dc->selection->duplicate();
+ dc->selection->remove(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(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->getDesktop()->getDocument(), 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
+ */
+
+/* fixme: We have to ensure this is not delayed (Lauris) */
+void FreehandBase::onSelectionModified()
+{
+ _attachSelection();
+}
+
+void FreehandBase::_attachSelection()
+{
+ // We reset white and forget white/start/end anchors
+ white_curves.clear();
+ white_anchors.clear();
+ white_item = nullptr;
+ sa = nullptr;
+ ea = nullptr;
+
+ SPItem *item = selection ? selection->singleItem() : nullptr;
+
+ if ( item && is<SPPath>(item) ) {
+ // Create new white data
+ // Item
+ white_item = item;
+
+ // Curve list
+ // We keep it in desktop coordinates to eliminate calculation errors
+ auto path = static_cast<SPPath *>(item);
+ if (!path->curveForEdit()) {
+ return;
+ }
+
+ auto tmp = path->curveForEdit()->transformed(white_item->i2dt_affine()).split();
+ white_curves.clear();
+ white_curves.reserve(tmp.size());
+ for (auto &t : tmp) {
+ white_curves.emplace_back(std::make_shared<SPCurve>(std::move(t)));
+ }
+
+ // Anchor list
+ for (auto const &c : white_curves) {
+ g_return_if_fail( c->get_segment_count() > 0 );
+ if ( !c->is_closed() ) {
+ white_anchors.emplace_back(std::make_unique<SPDrawAnchor>(this, c, true , *c->first_point()));
+ white_anchors.emplace_back(std::make_unique<SPDrawAnchor>(this, c, false, *c->last_point()));
+ }
+ }
+ // fixme: recalculate active anchor?
+ }
+}
+
+void spdc_endpoint_snap_rotation(ToolBase* 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->getDesktop()->namedview->snap_manager;
+ m.setup(ec->getDesktop());
+
+ 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), std::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 ec, Geom::Point& p, std::optional<Geom::Point> &start_of_line, guint const /*state*/)
+{
+ const SPDesktop *dt = ec->getDesktop();
+ 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();
+}
+
+void spdc_concat_colors_and_flush(FreehandBase *dc, gboolean forceclosed)
+{
+ // Concat RBG
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ // Green
+ auto c = std::make_shared<SPCurve>();
+ std::swap(c, dc->green_curve);
+ dc->green_bpaths.clear();
+
+ // Blue
+ c->append_continuous(std::move(dc->blue_curve));
+ dc->blue_curve.reset();
+ dc->blue_bpath->set_bpath(nullptr);
+
+ // Red
+ if (dc->red_curve_is_valid) {
+ c->append_continuous(dc->red_curve);
+ }
+ dc->red_curve.reset();
+ dc->red_bpath->set_bpath(nullptr);
+
+ if (c->is_empty()) {
+ 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->getDesktop()->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Path is closed."));
+ c->closepath_current();
+ // Closed path, just flush
+ spdc_flush_white(dc, std::move(c));
+ 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->getDesktop()->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Closing path."));
+ dc->sa_overwrited->append_continuous(*c);
+ 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(std::move(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));
+ }
+ dc->sa_overwrited->append_continuous(*c);
+ c = std::move(dc->sa_overwrited);
+ } else /* Step D - test end */ if (dc->ea) {
+ auto e = std::move(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 = std::make_shared<SPCurve>(e->reversed());
+ }
+ if(prefs->getInt(dc->getPrefsPath() + "/freehand-mode", 0) == 1 ||
+ prefs->getInt(dc->getPrefsPath() + "/freehand-mode", 0) == 2)
+ {
+ e = std::make_shared<SPCurve>(e->reversed());
+ Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*e->last_segment());
+ if(cubic){
+ auto lastSeg = std::make_shared<SPCurve>();
+ lastSeg->moveto((*cubic)[0]);
+ lastSeg->curveto((*cubic)[1],(*cubic)[3],(*cubic)[3]);
+ if ( e->get_segment_count() == 1) {
+ e = std::move(lastSeg);
+ } else {
+ //we eliminate the last segment
+ e->backspace();
+ //and we add it again with the recreation
+ e->append_continuous(*lastSeg);
+ }
+ }
+ e = std::make_shared<SPCurve>(e->reversed());
+ }
+ c->append_continuous(*e);
+ }
+ if (forceclosed)
+ {
+ dc->getDesktop()->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Path is closed."));
+ c->closepath_current();
+ }
+ spdc_flush_white(dc, std::move(c));
+}
+
+static void spdc_flush_white(FreehandBase *dc, std::shared_ptr<SPCurve> gc)
+{
+ std::shared_ptr<SPCurve> c;
+
+ if (! dc->white_curves.empty()) {
+ g_assert(dc->white_item);
+
+ c = std::make_shared<SPCurve>();
+ for (auto const &wc : dc->white_curves) {
+ c->append(*wc);
+ }
+
+ dc->white_curves.clear();
+ if (gc) {
+ c->append(*gc);
+ }
+ } else if (gc) {
+ c = std::move(gc);
+ } else {
+ return;
+ }
+
+ SPDesktop *desktop = dc->getDesktop();
+ SPDocument *doc = desktop->getDocument();
+ Inkscape::XML::Document *xml_doc = doc->getReprDoc();
+
+ // Now we have to go back to item coordinates at last
+ c->transform( dc->white_item
+ ? (dc->white_item)->dt2i_affine()
+ : desktop->dt2doc() );
+
+ if ( !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 = cast<SPLPEItem>(dc->white_item)->hasPathEffectRecursive();
+ } else {
+ repr = xml_doc->createElement("svg:path");
+ // Set style
+ sp_desktop_apply_style_tool(desktop, repr, dc->getPrefsPath(), false);
+ }
+
+ auto str = sp_svg_write_path(c->get_pathvector());
+ if (has_lpe)
+ repr->setAttribute("inkscape:original-d", str);
+ else
+ repr->setAttribute("d", str);
+
+ auto layer = dc->currentLayer();
+ if (SP_IS_PENCIL_CONTEXT(dc) && dc->tablet_enabled) {
+ if (!dc->white_item) {
+ dc->white_item = cast<SPItem>(layer->appendChildRepr(repr));
+ }
+ spdc_check_for_and_apply_waiting_LPE(dc, dc->white_item, c.get(), false);
+ }
+ if (!dc->white_item) {
+ // Attach repr
+ auto item = cast<SPItem>(layer->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.get(), true);
+ Inkscape::GC::release(repr);
+ item->transform = layer->i2doc_affine().inverse();
+ item->updateRepr();
+ item->doWriteTransform(item->transform, nullptr, true);
+ spdc_check_for_and_apply_waiting_LPE(dc, item, c.get(), false);
+ if(previous_shape_type == BEND_CLIPBOARD){
+ repr->parent()->removeChild(repr);
+ dc->white_item = nullptr;
+ } else {
+ dc->selection->set(repr);
+ }
+ }
+ auto lpeitem = cast<SPLPEItem>(dc->white_item);
+ if (lpeitem && lpeitem->hasPathEffectRecursive()) {
+ sp_lpe_item_update_patheffect(lpeitem, true, false);
+ }
+ DocumentUndo::done(doc, _("Draw path"), SP_IS_PEN_CONTEXT(dc)? INKSCAPE_ICON("draw-path") : INKSCAPE_ICON("draw-freehand"));
+
+ // 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.
+ dc->onSelectionModified();
+ }
+
+ // 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 = dc->green_anchor->anchorTest(p, TRUE);
+ }
+
+ for (auto& i:dc->white_anchors) {
+ SPDrawAnchor *na = i->anchorTest(p, !active);
+ if ( !active && na ) {
+ active = na;
+ }
+ }
+ return active;
+}
+
+static void spdc_free_colors(FreehandBase *dc)
+{
+ // Red
+ dc->red_bpath.reset();
+
+ // Blue
+ dc->blue_bpath.reset();
+ dc->blue_curve.reset();
+
+ // Overwrite start anchor curve
+ dc->sa_overwrited.reset();
+ // Green
+ dc->green_bpaths.clear();
+ dc->green_curve.reset();
+ dc->green_anchor.reset();
+
+ // White
+ if (dc->white_item) {
+ // We do not hold refcount
+ dc->white_item = nullptr;
+ }
+ dc->white_curves.clear();
+ 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->getDesktop();
+ Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc();
+ Inkscape::XML::Node *repr = xml_doc->createElement("svg:path");
+ repr->setAttribute("sodipodi:type", "arc");
+ auto layer = ec->currentLayer();
+ auto item = cast<SPItem>(layer->appendChildRepr(repr));
+ item->transform = layer->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(desktop->doc());
+ 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;
+ }
+
+ repr->setAttributeSvgDouble("sodipodi:cx", pp[Geom::X]);
+ repr->setAttributeSvgDouble("sodipodi:cy", pp[Geom::Y]);
+ repr->setAttributeSvgDouble("sodipodi:rx", rad * stroke_width);
+ repr->setAttributeSvgDouble("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(), _("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..a803a74
--- /dev/null
+++ b/src/ui/tools/freehand-base.h
@@ -0,0 +1,161 @@
+// 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 <memory>
+#include <optional>
+
+#include <sigc++/connection.h>
+
+#include "ui/tools/tool-base.h"
+#include "live_effects/effect-enum.h"
+#include "display/curve.h"
+#include "display/control/canvas-item-ptr.h"
+
+class SPCurve;
+class SPCanvasItem;
+
+struct SPDrawAnchor;
+
+namespace Inkscape {
+ class CanvasItemBpath;
+ class Selection;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+enum shapeType { NONE, TRIANGLE_IN, TRIANGLE_OUT, ELLIPSE, CLIPBOARD, BEND_CLIPBOARD, LAST_APPLIED };
+
+class FreehandBase : public ToolBase {
+public:
+ FreehandBase(SPDesktop *desktop, std::string prefs_path, const std::string &cursor_filename);
+ ~FreehandBase() override;
+
+ Inkscape::Selection *selection;
+
+protected:
+ guint32 red_color;
+ guint32 blue_color;
+ guint32 green_color;
+ guint32 highlight_color;
+
+public:
+ // Red - Last segment as it's drawn.
+ CanvasItemPtr<CanvasItemBpath> red_bpath;
+ SPCurve red_curve;
+ std::optional<Geom::Point> red_curve_get_last_point();
+
+ // Blue - New path after LPE as it's drawn.
+ CanvasItemPtr<CanvasItemBpath> blue_bpath;
+ SPCurve blue_curve;
+
+ // Green - New path as it's drawn.
+ std::vector<CanvasItemPtr<CanvasItemBpath>> green_bpaths;
+ std::shared_ptr<SPCurve> green_curve;
+ std::unique_ptr<SPDrawAnchor> green_anchor;
+ bool green_closed; // a flag meaning we hit the green anchor, so close the path on itself
+
+ // White
+ SPItem *white_item;
+ std::vector<std::shared_ptr<SPCurve>> white_curves;
+ std::vector<std::unique_ptr<SPDrawAnchor>> white_anchors;
+
+ // Temporary modified curve when start anchor
+ std::shared_ptr<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;
+
+ void onSelectionModified();
+
+protected:
+ bool root_handler(GdkEvent* event) override;
+ void _attachSelection();
+};
+
+/**
+ * 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 ec, Geom::Point &p, Geom::Point const &o, guint state);
+
+void spdc_endpoint_snap_free(ToolBase* const ec, Geom::Point &p, std::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..04acf4b
--- /dev/null
+++ b/src/ui/tools/gradient-tool.cpp
@@ -0,0 +1,822 @@
+// 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 "message-context.h"
+#include "message-stack.h"
+#include "rubberband.h"
+#include "selection-chemistry.h"
+#include "selection.h"
+#include "snap.h"
+
+#include "include/macros.h"
+
+#include "object/sp-namedview.h"
+#include "object/sp-stop.h"
+
+#include "display/control/canvas-item-curve.h"
+
+#include "svg/css-ostringstream.h"
+
+#include "ui/icon-names.h"
+#include "ui/tools/gradient-tool.h"
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+
+GradientTool::GradientTool(SPDesktop *desktop)
+ : ToolBase(desktop, "/tools/gradient", "gradient.svg")
+ , cursor_addnode(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;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (prefs->getBool("/tools/gradient/selcue", true)) {
+ this->enableSelectionCue();
+ }
+
+ this->enableGrDrag();
+ Inkscape::Selection *selection = desktop->getSelection();
+
+ this->selcon = new sigc::connection(selection->connectChanged(
+ sigc::mem_fun(*this, &GradientTool::selection_changed)
+ ));
+
+ subselcon = new sigc::connection(desktop->connect_gradient_stop_selected(
+ [=](void* sender, SPStop* stop) {
+ selection_changed(nullptr);
+ if (stop) {
+ // sync stop selection:
+ _grdrag->selectByStop(stop, false, true);
+ }
+ }
+ ));
+
+ this->selection_changed(selection);
+}
+
+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*) {
+
+ GrDrag *drag = _grdrag;
+ Inkscape::Selection *selection = _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),nullptr);
+ 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),nullptr);
+ 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),nullptr);
+ message_context->setF(Inkscape::NORMAL_MESSAGE,message, n_sel, n_tot, n_obj);
+ } else if (n_sel == 0) {
+ 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::select_next()
+{
+ g_assert(_grdrag);
+ GrDragger *d = _grdrag->select_next();
+ _desktop->scroll_to_point(d->point);
+}
+
+void GradientTool::select_prev()
+{
+ g_assert(_grdrag);
+ GrDragger *d = _grdrag->select_prev();
+ _desktop->scroll_to_point(d->point);
+}
+
+SPItem *GradientTool::is_over_curve(Geom::Point event_p)
+{
+ // Translate mouse point into proper coord system: needed later.
+ mousepoint_doc = _desktop->w2d(event_p);
+
+ for (auto &it : _grdrag->item_curves) {
+ if (it.curve->contains(event_p, tolerance)) {
+ return it.item;
+ }
+ }
+
+ return nullptr;
+}
+
+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 GradientTool::add_stops_between_selected_stops()
+{
+ SPDocument *doc = nullptr;
+ GrDrag *drag = _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 (is<SPGradient>(parent)) {
+ doc = parent->document;
+ SPStop *new_stop = sp_vector_add_stop (cast<SPGradient>(parent), this_stop, next_stop, offset);
+ new_stops.push_back(new_stop);
+ cast<SPGradient>(parent)->ensureVector();
+ }
+ }
+
+ if (!these_stops.empty() && doc) {
+ DocumentUndo::done(doc, _("Add gradient stop"), INKSCAPE_ICON("color-gradient"));
+ 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 tolerance maximum difference between stop and expected color at that position
+ */
+void GradientTool::simplify(double tolerance)
+{
+ SPDocument *doc = nullptr;
+ GrDrag *drag = _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, _("Simplify gradient"), INKSCAPE_ICON("color-gradient"));
+ drag->local_change = true;
+ drag->updateDraggers();
+ drag->selectByCoords(coords);
+ }
+}
+
+void GradientTool::add_stop_near_point(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
+ SPStop *newstop = get_drag()->addStopNearPoint(item, mouse_p, tolerance / _desktop->current_zoom());
+
+ DocumentUndo::done(_desktop->getDocument(), _("Add gradient stop"), INKSCAPE_ICON("color-gradient"));
+
+ get_drag()->updateDraggers();
+ get_drag()->local_change = true;
+ get_drag()->selectByStop(newstop);
+}
+
+bool GradientTool::root_handler(GdkEvent* event) {
+ static bool dragging;
+
+ Inkscape::Selection *selection = _desktop->getSelection();
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+
+ GrDrag *drag = this->_grdrag;
+ g_assert (drag);
+
+ gint ret = FALSE;
+
+ switch (event->type) {
+ case GDK_2BUTTON_PRESS:
+ if ( event->button.button == 1 ) {
+ SPItem *item = is_over_curve(Geom::Point(event->motion.x, event->motion.y));
+ if (item) {
+ // we take the first item in selection, because with doubleclick, the first click
+ // always resets selection to the single object under cursor
+ add_stop_near_point(selection->items().front(), 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);
+ }
+ DocumentUndo::done(_desktop->getDocument(), _("Create default gradient"), INKSCAPE_ICON("color-gradient"));
+ }
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_BUTTON_PRESS:
+ if ( event->button.button == 1 ) {
+ 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 && !(event->button.state & GDK_CONTROL_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 )) {
+ 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 = _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 {
+ this->drag(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 = _desktop->w2d(motion_w);
+
+ m.preSnap(Inkscape::SnapCandidatePoint(motion_dt, Inkscape::SNAPSOURCE_OTHER_HANDLE));
+ m.unSetup();
+ }
+
+ SPItem *item = is_over_curve(Geom::Point(event->motion.x, event->motion.y));
+
+ if (this->cursor_addnode && !item) {
+ this->set_cursor("gradient.svg");
+ this->cursor_addnode = false;
+ } else if (!this->cursor_addnode && item) {
+ this->set_cursor("gradient-add.svg");
+ this->cursor_addnode = true;
+ }
+ }
+ break;
+
+ case GDK_BUTTON_RELEASE:
+ this->xp = this->yp = 0;
+
+ if ( event->button.button == 1 ) {
+ SPItem *item = is_over_curve(Geom::Point(event->motion.x, event->motion.y));
+
+ if ( (event->button.state & GDK_CONTROL_MASK) && (event->button.state & GDK_MOD1_MASK ) ) {
+ if (item) {
+ this->add_stop_near_point(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 && !(event->button.state & GDK_SHIFT_MASK)) {
+ ret = TRUE;
+ Inkscape::Rubberband::get(_desktop)->stop();
+ 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 (item) {
+ // 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()) {
+ this->simplify(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_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:
+ this->add_stops_between_selected_stops();
+ 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)
+ this->add_stops_between_selected_stops();
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Delete:
+ case GDK_KEY_KP_Delete:
+ case GDK_KEY_BackSpace:
+ ret = this->deleteSelectedDrag(MOD__CTRL_ONLY(event));
+ break;
+
+ case GDK_KEY_Tab:
+ if (hasGradientDrag()) {
+ select_next();
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_ISO_Left_Tab:
+ if (hasGradientDrag()) {
+ select_prev();
+ ret = TRUE;
+ }
+ break;
+
+ default:
+ ret = drag->key_press_handler(event);
+ 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.
+void GradientTool::drag(Geom::Point const pt, guint /*state*/, guint32 etime)
+{
+ Inkscape::Selection *selection = _desktop->getSelection();
+ SPDocument *document = _desktop->getDocument();
+
+ 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 (item_to_select) {
+ // pick color from the object where drag started
+ vector = sp_gradient_vector_for_object(document, _desktop, 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, 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, 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, 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 (_grdrag) {
+ _grdrag->updateDraggers();
+ // prevent regenerating draggers by selection modified signal, which sometimes
+ // comes too late and thus destroys the knot which we will now grab:
+ _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
+ _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());
+ 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..6098a46
--- /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)
+
+class GrDrag;
+
+namespace Inkscape {
+
+class Selection;
+
+namespace UI {
+namespace Tools {
+
+class GradientTool : public ToolBase {
+public:
+ GradientTool(SPDesktop *desktop);
+ ~GradientTool() override;
+
+ bool root_handler(GdkEvent *event) override;
+ void add_stops_between_selected_stops();
+
+ void select_next();
+ void select_prev();
+
+private:
+ Geom::Point mousepoint_doc; // stores mousepoint when over_line in doc coords
+ Geom::Point origin;
+ bool cursor_addnode;
+
+ sigc::connection *selcon;
+ sigc::connection *subselcon;
+
+ void selection_changed(Inkscape::Selection *);
+ void simplify(double tolerance);
+ void add_stop_near_point(SPItem *item, Geom::Point mouse_p, guint32 etime);
+ void drag(Geom::Point const pt, guint state, guint32 etime);
+ SPItem *is_over_curve(Geom::Point event_p);
+};
+
+}
+}
+}
+
+#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..5149afc
--- /dev/null
+++ b/src/ui/tools/lpe-tool.cpp
@@ -0,0 +1,460 @@
+// 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 <iomanip>
+
+#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/control/canvas-item-rect.h"
+#include "display/control/canvas-item-text.h"
+
+#include "object/sp-path.h"
+
+#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::UI::Tools {
+
+void sp_lpetool_context_selection_changed(Inkscape::Selection *selection, gpointer data);
+
+LpeTool::LpeTool(SPDesktop *desktop)
+ : PenTool(desktop, "/tools/lpetool", "geometric.svg")
+ , mode(Inkscape::LivePathEffect::BEND_PATH)
+{
+ Inkscape::Selection *selection = 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));
+
+ shape_editor = std::make_unique<ShapeEditor>(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();
+ }
+}
+
+LpeTool::~LpeTool()
+{
+ shape_editor.reset();
+ canvas_bbox.reset();
+ measuring_items.clear();
+
+ sel_changed_connection.disconnect();
+}
+
+/**
+ * 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 = _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) {
+ 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 (!is<SPLPEItem>(item)) {
+ return -1;
+ }
+
+ Inkscape::LivePathEffect::Effect* lpe = cast<SPLPEItem>(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->getDesktop()->getSelection();
+ SPItem *item = selection->singleItem();
+
+ // TODO: should we check whether type represents a valid geometric construction?
+ if (item && is<SPLPEItem>(item) && Inkscape::LivePathEffect::Effect::acceptsNumClicks(type) == 0) {
+ Inkscape::LivePathEffect::Effect::createAndApply(type, lc->getDesktop()->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->getDesktop()->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)
+{
+ lc->canvas_bbox.reset();
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (!prefs->getBool("/tools/lpetool/show_bbox", true))
+ return;
+
+ SPDocument *document = lc->getDesktop()->getDocument();
+
+ Geom::Point A, B;
+ lpetool_get_limiting_bbox_corners(document, A, B);
+ Geom::Affine doc2dt(lc->getDesktop()->doc2dt());
+ A *= doc2dt;
+ B *= doc2dt;
+
+ Geom::Rect rect(A, B);
+ lc->canvas_bbox = make_canvasitem<CanvasItemRect>(lc->getDesktop()->getCanvasControls(), rect);
+ lc->canvas_bbox->set_stroke(0x0000ffff);
+ lc->canvas_bbox->set_dashed(true);
+}
+
+static void
+set_pos_and_anchor(Inkscape::CanvasItemText *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));
+
+ canvas_text->set_coord(pos + n * length);
+ canvas_text->set_anchor(Geom::Point(std::sin(angle), -std::cos(angle)));
+}
+
+void
+lpetool_create_measuring_items(LpeTool *lc, Inkscape::Selection *selection)
+{
+ if (!selection) {
+ selection = lc->getDesktop()->getSelection();
+ }
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool show = prefs->getBool("/tools/lpetool/show_measuring_info", true);
+
+ Inkscape::CanvasItemGroup *tmpgrp = lc->getDesktop()->getCanvasTemp();
+
+ 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");
+ }
+
+ auto items= selection->items();
+ for (auto i : items) {
+ auto path = cast<SPPath>(i);
+ if (path) {
+ SPCurve const *curve = path->curve();
+ Geom::Piecewise<Geom::D2<Geom::SBasis> > pwd2 = paths_to_pw(curve->get_pathvector());
+
+ double lengthval = Geom::length(pwd2);
+ lengthval = Inkscape::Util::Quantity::convert(lengthval, "px", unit);
+
+ Glib::ustring arc_length = Glib::ustring::format(std::setprecision(2), std::fixed, lengthval);
+ arc_length += " ";
+ arc_length += unit->abbr;
+
+ auto canvas_text = make_canvasitem<CanvasItemText>(tmpgrp, Geom::Point(0,0), arc_length);
+ set_pos_and_anchor(canvas_text.get(), pwd2, 0.5, 10);
+ if (!show) {
+ canvas_text->hide();
+ }
+
+ lc->measuring_items[path] = std::move(canvas_text);
+ }
+ }
+}
+
+void lpetool_delete_measuring_items(LpeTool *lc)
+{
+ lc->measuring_items.clear();
+}
+
+void
+lpetool_update_measuring_items(LpeTool *lc)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ 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");
+ }
+
+ for (auto& i : lc->measuring_items) {
+
+ SPPath *path = i.first;
+ SPCurve const *curve = path->curve();
+ Geom::Piecewise<Geom::D2<Geom::SBasis> > pwd2 = Geom::paths_to_pw(curve->get_pathvector());
+ double lengthval = Geom::length(pwd2);
+ lengthval = Inkscape::Util::Quantity::convert(lengthval, "px", unit);
+
+ Glib::ustring arc_length = Glib::ustring::format(std::setprecision(2), std::fixed, lengthval);
+ arc_length += " ";
+ arc_length += unit->abbr;
+
+ i.second->set_text(std::move(arc_length));
+ set_pos_and_anchor(i.second.get(), pwd2, 0.5, 10);
+ }
+}
+
+void
+lpetool_show_measuring_info(LpeTool *lc, bool show)
+{
+ for (auto& i : lc->measuring_items) {
+ if (show) {
+ i.second->show();
+ } else {
+ i.second->hide();
+ }
+ }
+}
+
+} // namespace Inkscape::UI::Tools
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ 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..498031e
--- /dev/null
+++ b/src/ui/tools/lpe-tool.h
@@ -0,0 +1,98 @@
+// 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 {
+
+class CanvasItemText;
+class CanvasItemRect;
+
+namespace UI {
+namespace Tools {
+
+class LpeTool : public PenTool {
+public:
+ LpeTool(SPDesktop *desktop);
+ ~LpeTool() override;
+
+ std::unique_ptr<ShapeEditor> shape_editor;
+ CanvasItemPtr<CanvasItemRect> canvas_bbox;
+ Inkscape::LivePathEffect::EffectType mode;
+
+ std::map<SPPath*, CanvasItemPtr<CanvasItemText>> measuring_items;
+
+ sigc::connection sel_changed_connection;
+ sigc::connection sel_modified_connection;
+protected:
+ 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/marker-tool.cpp b/src/ui/tools/marker-tool.cpp
new file mode 100644
index 0000000..5633871
--- /dev/null
+++ b/src/ui/tools/marker-tool.cpp
@@ -0,0 +1,302 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Marker edit mode - onCanvas marker editing of marker orientation, position, scale
+ *//*
+ * Authors:
+ * see git history
+ * Rachana Podaralla <rpodaralla3@gatech.edu>
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "display/curve.h"
+
+#include "desktop.h"
+#include "document.h"
+#include "style.h"
+#include "message-context.h"
+#include "selection.h"
+
+#include "object/sp-path.h"
+#include "object/sp-shape.h"
+#include "object/sp-marker.h"
+
+#include "ui/shape-editor.h"
+#include "ui/tool/event-utils.h"
+#include "ui/tool/multi-path-manipulator.h"
+#include "ui/tool/path-manipulator.h"
+#include "ui/tools/marker-tool.h"
+
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+MarkerTool::MarkerTool(SPDesktop *desktop)
+ : ToolBase(desktop, "/tools/marker", "select.svg")
+{
+ Inkscape::Selection *selection = desktop->getSelection();
+
+ this->sel_changed_connection.disconnect();
+ this->sel_changed_connection = selection->connectChanged(
+ sigc::mem_fun(*this, &MarkerTool::selection_changed)
+ );
+ this->selection_changed(selection);
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/tools/marker/selcue")) this->enableSelectionCue();
+ if (prefs->getBool("/tools/marker/gradientdrag")) this->enableGrDrag();
+}
+
+MarkerTool::~MarkerTool()
+{
+ ungrabCanvasEvents();
+
+ this->message_context->clear();
+ this->_shape_editors.clear();
+
+ this->enableGrDrag(false);
+ this->sel_changed_connection.disconnect();
+}
+
+/*
+- cycles through all the selected items to see if any have a marker in the right location (based on enterMarkerMode)
+- if a matching item is found, loads the corresponding marker on the shape into the shape-editor and exits the loop
+- forces user to only edit one marker at a time
+*/
+void MarkerTool::selection_changed(Inkscape::Selection *selection) {
+ using namespace Inkscape::UI;
+
+ g_assert(_desktop != nullptr);
+
+ SPDocument *doc = _desktop->getDocument();
+ g_assert(doc != nullptr);
+
+ auto selected_items = selection->items();
+ this->_shape_editors.clear();
+
+ for(auto i = selected_items.begin(); i != selected_items.end(); ++i){
+ SPItem *item = *i;
+
+ if(item) {
+ auto shape = cast<SPShape>(item);
+
+ if(shape && shape->hasMarkers() && (editMarkerMode != -1)) {
+ SPObject *obj = shape->_marker[editMarkerMode];
+
+ if(obj) {
+
+ auto sp_marker = cast<SPMarker>(obj);
+ g_assert(sp_marker != nullptr);
+
+ sp_validate_marker(sp_marker, doc);
+
+ ShapeRecord sr;
+ switch(editMarkerMode) {
+ case SP_MARKER_LOC_START:
+ sr = get_marker_transform(shape, item, sp_marker, SP_MARKER_LOC_START);
+ break;
+
+ case SP_MARKER_LOC_MID:
+ sr = get_marker_transform(shape, item, sp_marker, SP_MARKER_LOC_MID);
+ break;
+
+ case SP_MARKER_LOC_END:
+ sr = get_marker_transform(shape, item, sp_marker, SP_MARKER_LOC_END);
+ break;
+
+ default:
+ break;
+ }
+
+ auto si = std::make_unique<ShapeEditor>(_desktop, sr.edit_transform, sr.edit_rotation, editMarkerMode);
+ si->set_item(cast<SPItem>(sr.object));
+
+ this->_shape_editors.insert({item, std::move(si)});
+ break;
+ }
+ }
+ }
+ }
+}
+
+// handles selection of new items
+bool MarkerTool::root_handler(GdkEvent* event) {
+ g_assert(_desktop != nullptr);
+
+ Inkscape::Selection *selection = _desktop->getSelection();
+ gint ret = false;
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 1) {
+
+ Geom::Point const button_w(event->button.x, event->button.y);
+ this->item_to_select = sp_event_context_find_item (_desktop, button_w, event->button.state & GDK_MOD1_MASK, TRUE);
+
+ grabCanvasEvents();
+ ret = true;
+ }
+ break;
+ case GDK_BUTTON_RELEASE:
+ if (event->button.button == 1) {
+
+ if (this->item_to_select) {
+ // unselect all items, except for newly selected item
+ selection->set(this->item_to_select);
+ } else {
+ // clicked into empty space, deselect any selected items
+ selection->clear();
+ }
+
+ this->item_to_select = nullptr;
+ ungrabCanvasEvents();
+ ret = true;
+ }
+ break;
+ default:
+ break;
+ }
+
+ return (!ret? ToolBase::root_handler(event): ret);
+}
+
+/*
+- this function uses similar logic that exists in sp_shape_update_marker_view
+- however, the tangent angle needs to be saved here and parent_item->i2dt_affine() needs to also be accounted for in the right places
+- calculate where the shape-editor knotholders need to go based on the reference shape
+*/
+ShapeRecord MarkerTool::get_marker_transform(SPShape* shape, SPItem *parent_item, SPMarker *sp_marker, SPMarkerLoc marker_type)
+{
+
+ // scale marker transform with parent stroke width
+ SPStyle *style = shape->style;
+ SPDocument *doc = _desktop->getDocument();
+ Geom::Scale scale = doc->getDocumentScale();
+
+ if(sp_marker->markerUnits == SP_MARKER_UNITS_STROKEWIDTH) {
+ scale *= Geom::Scale(style->stroke_width.computed);
+ }
+
+ Geom::PathVector const &pathv = shape->curve()->get_pathvector();
+ Geom::Affine ret = Geom::identity(); //edit_transform
+ double angle = 0.0; // edit_rotation - tangent angle used for auto orientation
+ Geom::Point p;
+
+ if(marker_type == SP_MARKER_LOC_START) {
+
+ Geom::Curve const &c = pathv.begin()->front();
+ p = c.pointAt(0);
+ ret = Geom::Translate(p * parent_item->i2doc_affine());
+
+ if (!c.isDegenerate()) {
+ Geom::Point tang = c.unitTangentAt(0);
+ angle = Geom::atan2(tang);
+ ret = Geom::Rotate(angle) * ret;
+ }
+
+ } else if(marker_type == SP_MARKER_LOC_MID) {
+ /*
+ - a shape can have multiple mid markers - only one is needed
+ - once a valid mid marker is found, save edit_transfom and edit_rotation and break out of loop
+ */
+ for(Geom::PathVector::const_iterator path_it = pathv.begin(); path_it != pathv.end(); ++path_it) {
+
+ // mid marker start position
+ if (path_it != pathv.begin() && ! ((path_it == (pathv.end()-1)) && (path_it->size_default() == 0)))
+ {
+ Geom::Curve const &c = path_it->front();
+ p = c.pointAt(0);
+ ret = Geom::Translate(p * parent_item->i2doc_affine());
+
+ if (!c.isDegenerate()) {
+ Geom::Point tang = c.unitTangentAt(0);
+ angle = Geom::atan2(tang);
+ ret = Geom::Rotate(angle) * ret;
+ break;
+ }
+ }
+
+ // mid marker mid positions
+ if ( path_it->size_default() > 1) {
+ Geom::Path::const_iterator curve_it1 = path_it->begin();
+ Geom::Path::const_iterator curve_it2 = ++(path_it->begin());
+ while (curve_it2 != path_it->end_default())
+ {
+ Geom::Curve const & c1 = *curve_it1;
+ Geom::Curve const & c2 = *curve_it2;
+
+ p = c1.pointAt(1);
+ Geom::Curve * c1_reverse = c1.reverse();
+ Geom::Point tang1 = - c1_reverse->unitTangentAt(0);
+ delete c1_reverse;
+ Geom::Point tang2 = c2.unitTangentAt(0);
+
+ double const angle1 = Geom::atan2(tang1);
+ double const angle2 = Geom::atan2(tang2);
+
+ angle = .5 * (angle1 + angle2);
+
+ if ( fabs( angle2 - angle1 ) > M_PI ) {
+ angle += M_PI;
+ }
+
+ ret = Geom::Rotate(angle) * Geom::Translate(p * parent_item->i2doc_affine());
+
+ ++curve_it1;
+ ++curve_it2;
+ break;
+ }
+ }
+
+ // mid marker end position
+ if ( path_it != (pathv.end()-1) && !path_it->empty()) {
+ Geom::Curve const &c = path_it->back_default();
+ p = c.pointAt(1);
+ ret = Geom::Translate(p * parent_item->i2doc_affine());
+
+ if ( !c.isDegenerate() ) {
+ Geom::Curve * c_reverse = c.reverse();
+ Geom::Point tang = - c_reverse->unitTangentAt(0);
+ delete c_reverse;
+ angle = Geom::atan2(tang);
+ ret = Geom::Rotate(angle) * ret;
+ break;
+ }
+ }
+ }
+
+ } else if (marker_type == SP_MARKER_LOC_END) {
+
+ Geom::Path const &path_last = pathv.back();
+ unsigned int index = path_last.size_default();
+ if (index > 0) index--;
+
+ Geom::Curve const &c = path_last[index];
+ p = c.pointAt(1);
+ ret = Geom::Translate(p * parent_item->i2doc_affine());
+
+ if ( !c.isDegenerate() ) {
+ Geom::Curve * c_reverse = c.reverse();
+ Geom::Point tang = - c_reverse->unitTangentAt(0);
+ delete c_reverse;
+ angle = Geom::atan2(tang);
+ ret = Geom::Rotate(angle) * ret;
+ }
+ }
+
+ /* scale by stroke width */
+ ret = scale * ret;
+ /* account for parent transform */
+ ret = parent_item->transform.withoutTranslation() * ret;
+
+ ShapeRecord sr;
+ sr.object = sp_marker;
+ sr.edit_transform = ret;
+ sr.edit_rotation = angle * 180.0/M_PI;
+ sr.role = SHAPE_ROLE_NORMAL;
+ return sr;
+}
+
+}}}
diff --git a/src/ui/tools/marker-tool.h b/src/ui/tools/marker-tool.h
new file mode 100644
index 0000000..92d77a2
--- /dev/null
+++ b/src/ui/tools/marker-tool.h
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Marker edit mode - onCanvas marker editing of marker orientation, position, scale
+ *//*
+ * Authors:
+ * see git history
+ * Rachana Podaralla <rpodaralla3@gatech.edu>
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef __SP_MARKER_CONTEXT_H__
+#define __SP_MARKER_CONTEXT_H__
+
+#include <cstddef>
+#include <sigc++/sigc++.h>
+#include <2geom/point.h>
+
+#include "object/sp-marker.h"
+#include "object/sp-marker-loc.h"
+
+#include "ui/tools/tool-base.h"
+#include "ui/tool/shape-record.h"
+
+namespace Inkscape {
+class Selection;
+namespace UI {
+namespace Tools {
+
+class MarkerTool : public ToolBase {
+public:
+ MarkerTool(SPDesktop *desktop);
+ ~MarkerTool() override;
+
+ void selection_changed(Inkscape::Selection *selection);
+
+ bool root_handler(GdkEvent *event) override;
+ std::map<SPItem *, std::unique_ptr<ShapeEditor>> _shape_editors;
+
+ int editMarkerMode = -1;
+
+private:
+ sigc::connection sel_changed_connection;
+ ShapeRecord get_marker_transform(SPShape *shape, SPItem *parent_item, SPMarker *sp_marker, SPMarkerLoc marker_type);
+};
+
+}}}
+
+#endif
diff --git a/src/ui/tools/measure-tool.cpp b/src/ui/tools/measure-tool.cpp
new file mode 100644
index 0000000..beee75c
--- /dev/null
+++ b/src/ui/tools/measure-tool.cpp
@@ -0,0 +1,1445 @@
+// 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 "measure-tool.h"
+
+#include <iomanip>
+
+#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 "layer-manager.h"
+#include "page-manager.h"
+#include "path-chemistry.h"
+#include "rubberband.h"
+#include "text-editing.h"
+
+#include "display/curve.h"
+#include "display/control/canvas-item-curve.h"
+#include "display/control/canvas-item-ctrl.h"
+#include "display/control/canvas-item-group.h"
+#include "display/control/canvas-item-text.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 "svg/stringstream.h"
+#include "svg/svg-color.h"
+#include "svg/svg.h"
+
+#include "ui/dialog/knot-properties.h"
+#include "ui/icon-names.h"
+#include "ui/knot/knot.h"
+#include "ui/tools/freehand-base.h"
+#include "ui/widget/canvas.h" // Canvas area
+
+#include "util/units.h"
+
+using Inkscape::Util::unit_table;
+using Inkscape::DocumentUndo;
+
+const guint32 MT_KNOT_COLOR_NORMAL = 0xffffff00;
+const guint32 MT_KNOT_COLOR_MOUSEOVER = 0xff000000;
+
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+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 screen_world = desktop->getCanvas()->get_area_world();
+ if (screen_world.interiorContains(desktop->d2w(startPoint)) ||
+ screen_world.interiorContains(desktop->d2w(endPoint))) {
+ screen_world.expandBy(fontsize * -3, fontsize / -2);
+ where = desktop->w2d(screen_world.clamp(desktop->d2w(where)));
+ } // else likely initialized the measurement tool, keep display near the measurement.
+
+ return where;
+}
+
+} // namespace
+
+/**
+ * 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 MeasureTool::createAngleDisplayCurve(Geom::Point const &center, Geom::Point const &end, Geom::Point const &anchor,
+ double angle, bool to_phantom,
+ Inkscape::XML::Node *measure_repr)
+{
+ // 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;
+
+ /*
+ * The denominator of the expression for k2 can become 0, so this should be handled.
+ * The function for k2 tends to a limit for very small values of (ax * by) - (ay * bx), so theoretically
+ * it should be correct for values close to 0, however due to floating point inaccuracies this
+ * is not the case, and instabilities still exist. Therefore do a range check on the denominator.
+ * (This also solves some instances where again due to floating point inaccuracies, the square root term
+ * becomes slightly negative in case of very small values for ax * by - ay * bx).
+ * The values of this range have been generated by trying to make this term as small as possible,
+ * by zooming in as much as possible in the GUI, using the measurement tool and
+ * trying to get as close to 180 or 0 degrees as possible.
+ * Smallest value I was able to get was around 1e-5, and then I added some zeroes for good measure.
+ */
+ if (!((ax * by - ay * bx < 0.00000000001) && (ax * by - ay * bx > -0.00000000001))) {
+ k2 = (4.0 / 3.0) * (std::sqrt(2 * q1 * q2) - q2) / ((ax * by) - (ay * bx));
+ } else {
+ // If the denominator is 0, there are 2 cases:
+ // Either the angle is (almost) +-180 degrees, in which case the limit of k2 tends to -+4.0/3.0.
+ if (angle > 3.14 || angle < -3.14) { // The angle is in radians
+ // Now there are also 2 cases, where inkscape thinks it is 180 degrees, or -180 degrees.
+ // Adjust the value of k2 accordingly
+ if (angle > 0) {
+ k2 = -4.0 / 3.0;
+ } else {
+ k2 = 4.0 / 3.0;
+ }
+ } else {
+ // if the angle is (almost) 0, k2 is equal to 0
+ k2 = 0.0;
+ }
+ }
+
+ Geom::Point p2(xc + ax - (k2 * ay),
+ yc + ay + (k2 * ax));
+ Geom::Point p3(xc + bx + (k2 * by),
+ yc + by - (k2 * bx));
+
+ auto *curve = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p1, p2, p3, p4);
+ curve->set_name("CanvasItemCurve:MeasureToolCurve");
+ curve->set_stroke(Inkscape::CANVAS_ITEM_SECONDARY);
+ curve->lower_to_bottom();
+ curve->show();
+ if(to_phantom){
+ curve->set_stroke(0x8888887f);
+ measure_phantom_items.emplace_back(curve);
+ } else {
+ measure_tmp_items.emplace_back(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);
+ auto layer = _desktop->layerManager().currentLayer();
+ pathv *= layer->i2doc_affine().inverse();
+ if(!pathv.empty()) {
+ setMeasureItem(pathv, true, false, 0xff00007f, measure_repr);
+ }
+ }
+ }
+}
+
+std::optional<Geom::Point> explicit_base_tmp = std::nullopt;
+
+MeasureTool::MeasureTool(SPDesktop *desktop)
+ : ToolBase(desktop, "/tools/measure", "measure.svg")
+{
+ start_p = readMeasurePoint(true);
+ end_p = readMeasurePoint(false);
+
+ // create the knots
+ this->knot_start = new SPKnot(desktop, _("Measure start, <b>Shift+Click</b> for position dialog"),
+ Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "CanvasItemCtrl:MeasureTool");
+ this->knot_start->setMode(Inkscape::CANVAS_ITEM_CTRL_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(Inkscape::CANVAS_ITEM_CTRL_SHAPE_CIRCLE);
+ this->knot_start->updateCtrl();
+ this->knot_start->moveto(start_p);
+ this->knot_start->show();
+
+ this->knot_end = new SPKnot(desktop, _("Measure end, <b>Shift+Click</b> for position dialog"),
+ Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "CanvasItemCtrl:MeasureTool");
+ this->knot_end->setMode(Inkscape::CANVAS_ITEM_CTRL_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(Inkscape::CANVAS_ITEM_CTRL_SHAPE_CIRCLE);
+ this->knot_end->updateCtrl();
+ this->knot_end->moveto(end_p);
+ this->knot_end->show();
+
+ showCanvasItems();
+
+ 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->enableGrDrag(false);
+ ungrabCanvasEvents();
+
+ 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);
+
+ measure_tmp_items.clear();
+ measure_item.clear();
+ measure_phantom_items.clear();
+}
+
+static char const *endpoint_to_pref(bool is_start)
+{
+ return is_start ? "/tools/measure/measure-start" : "/tools/measure/measure-end";
+}
+
+Geom::Point MeasureTool::readMeasurePoint(bool is_start)
+{
+ return Preferences::get()->getPoint(endpoint_to_pref(is_start), Geom::Point(Geom::infinity(), Geom::infinity()));
+}
+
+void MeasureTool::writeMeasurePoint(Geom::Point point, bool is_start)
+{
+ Preferences::get()->setPoint(endpoint_to_pref(is_start), 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) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring const unit_name = prefs->getString("/tools/measure/unit", "px");
+ 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();
+}
+
+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);
+ 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: {
+ if (event->button.button != 1) {
+ break;
+ }
+ this->knot_start->hide();
+ this->knot_end->hide();
+ Geom::Point const button_w(event->button.x, event->button.y);
+ explicit_base = std::nullopt;
+ explicit_base_tmp = std::nullopt;
+ last_end = std::nullopt;
+
+ // save drag origin
+ start_p = _desktop->w2d(Geom::Point(event->button.x, event->button.y));
+ within_tolerance = true;
+
+ SnapManager &snap_manager = _desktop->namedview->snap_manager;
+ snap_manager.setup(_desktop);
+ snap_manager.freeSnapReturnByRef(start_p, Inkscape::SNAPSOURCE_OTHER_HANDLE);
+ snap_manager.unSetup();
+
+ grabCanvasEvents(Gdk::KEY_PRESS_MASK |
+ Gdk::KEY_RELEASE_MASK |
+ Gdk::BUTTON_PRESS_MASK |
+ Gdk::BUTTON_RELEASE_MASK |
+ Gdk::POINTER_MOTION_MASK );
+ ret = TRUE;
+ 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 {
+ // Inkscape::Util::Unit const * unit = _desktop->getNamedView()->getDisplayUnit();
+ 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: {
+ if (event->button.button != 1) {
+ break;
+ }
+ 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();
+
+ ungrabCanvasEvents();
+ break;
+ }
+ default:
+ break;
+ }
+ if (!ret) {
+ ret = ToolBase::root_handler(event);
+ }
+
+ return ret;
+}
+
+void MeasureTool::setMarkers()
+{
+ 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)
+{
+ 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;");
+ auto marker = cast<SPItem>(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)");
+ auto path = cast<SPItem>(marker->appendChildRepr(rpath));
+ Inkscape::GC::release(rpath);
+ path->updateRepr();
+}
+
+void MeasureTool::toGuides()
+{
+ 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) {
+ auto layer = _desktop->layerManager().currentLayer();
+ explicit_base = *explicit_base * layer->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(), _("Add guides from measure tool"), INKSCAPE_ICON("tool-measure"));
+}
+
+void MeasureTool::toPhantom()
+{
+ if (!_desktop || !start_p.isFinite() || !end_p.isFinite() || start_p == end_p) {
+ return;
+ }
+ SPDocument *doc = _desktop->getDocument();
+
+ measure_phantom_items.clear();
+ measure_tmp_items.clear();
+
+ showCanvasItems(false, false, true);
+ doc->ensureUpToDate();
+ DocumentUndo::done(_desktop->getDocument(), _("Keep last measure on the canvas, for reference"), INKSCAPE_ICON("tool-measure"));
+}
+
+void MeasureTool::toItem()
+{
+ 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);
+ auto measure_item = cast<SPItem>(_desktop->layerManager().currentLayer()->appendChildRepr(rgroup));
+ Inkscape::GC::release(rgroup);
+ measure_item->updateRepr();
+ doc->ensureUpToDate();
+ DocumentUndo::done(_desktop->getDocument(), _("Convert measure to items"), INKSCAPE_ICON("tool-measure"));
+ reset();
+}
+
+void MeasureTool::toMarkDimension()
+{
+ 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);
+
+ 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;
+
+
+ int precision = prefs->getInt("/tools/measure/precision", 2);
+ Glib::ustring total = Glib::ustring::format(std::fixed, std::setprecision(precision), totallengthval * scale);
+ total += unit_name;
+
+ double textangle = Geom::rad_from_deg(180) - ray.angle();
+ if (_desktop->is_yaxisdown()) {
+ textangle = ray.angle() - Geom::rad_from_deg(180);
+ }
+
+ setLabelText(total, middle, fontsize, textangle, color);
+
+ doc->ensureUpToDate();
+ DocumentUndo::done(_desktop->getDocument(), _("Add global measure line"), INKSCAPE_ICON("tool-measure"));
+}
+
+void MeasureTool::setGuide(Geom::Point origin, double angle, const char *label)
+{
+ 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)
+{
+ 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 *= _desktop->layerManager().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)
+{
+ 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 *= _desktop->layerManager().currentLayer()->i2doc_affine().inverse();
+ if (!pathv.empty()) {
+ guint32 line_color_secondary = 0xff0000ff;
+ setMeasureItem(pathv, false, false, line_color_secondary, measure_repr);
+ }
+}
+
+void MeasureTool::setLabelText(Glib::ustring const &value, Geom::Point pos, double fontsize, Geom::Coord angle,
+ guint32 background, Inkscape::XML::Node *measure_repr)
+{
+ 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) {
+ rtext->setAttributeSvgDouble("x", 2);
+ rtext->setAttributeSvgDouble("y", 2);
+ } else {
+ rtext->setAttributeSvgDouble("x", 0);
+ rtext->setAttributeSvgDouble("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.c_str());
+ rtspan->addChild(rstring, nullptr);
+ Inkscape::GC::release(rstring);
+ auto layer = _desktop->layerManager().currentLayer();
+ auto text_item = cast<SPText>(layer->appendChildRepr(rtext));
+ Inkscape::GC::release(rtext);
+ text_item->rebuildLayout();
+ 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);
+ rgroup->setAttributeSvgDouble("x", 0);
+ rgroup->setAttributeSvgDouble("y", 0);
+ rrect->setAttributeSvgDouble("x", -bbox->width()/2.0);
+ rrect->setAttributeSvgDouble("y", -bbox->height());
+ rrect->setAttributeSvgDouble("width", bbox->width() + 6);
+ rrect->setAttributeSvgDouble("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);
+ auto text_item_box = cast<SPItem>(layer->appendChildRepr(rgroup));
+ Geom::Scale scale = Geom::Scale(_desktop->current_zoom()).inverse();
+ if(bbox) {
+ 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 *= layer->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 *= layer->i2doc_affine().inverse();
+ text_item->doWriteTransform(text_item->transform, nullptr, true);
+ }
+}
+
+void MeasureTool::reset()
+{
+ this->knot_start->hide();
+ this->knot_end->hide();
+
+ 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,
+ bool to_left, bool to_item,
+ bool to_phantom, Inkscape::XML::Node *measure_repr)
+{
+ Glib::ustring measure = Glib::ustring::format(std::setprecision(precision), std::fixed, amount);
+ measure += " ";
+ measure += (is_angle ? "°" : unit_name);
+ auto canvas_tooltip = new Inkscape::CanvasItemText(_desktop->getCanvasTemp(), position, measure);
+ canvas_tooltip->set_fontsize(fontsize);
+ canvas_tooltip->set_fill(0xffffffff);
+ canvas_tooltip->set_background(background);
+ if (to_left) {
+ canvas_tooltip->set_anchor(Geom::Point(0, 0.5));
+ } else {
+ canvas_tooltip->set_anchor(Geom::Point(0.5, 0.5));
+ }
+
+ if (to_phantom){
+ canvas_tooltip->set_background(0x4444447f);
+ measure_phantom_items.emplace_back(canvas_tooltip);
+ } else {
+ measure_tmp_items.emplace_back(canvas_tooltip);
+ }
+
+ if (to_item) {
+ setLabelText(measure, position, fontsize, 0, background, measure_repr);
+ }
+
+ canvas_tooltip->show();
+
+}
+
+void MeasureTool::setMeasureCanvasItem(Geom::Point position, bool to_item, bool to_phantom, Inkscape::XML::Node *measure_repr){
+ guint32 color = 0xff0000ff;
+ if (to_phantom){
+ color = 0x888888ff;
+ }
+
+ auto canvas_item = new Inkscape::CanvasItemCtrl(_desktop->getCanvasTemp(), Inkscape::CANVAS_ITEM_CTRL_TYPE_POINT, position);
+ canvas_item->set_stroke(color);
+ canvas_item->lower_to_bottom();
+ canvas_item->set_pickable(false);
+ canvas_item->show();
+
+ if (to_phantom){
+ measure_phantom_items.emplace_back(canvas_item);
+ } else {
+ measure_tmp_items.emplace_back(canvas_item);
+ }
+
+ if(to_item) {
+ setPoint(position, measure_repr);
+ }
+}
+
+void MeasureTool::setMeasureCanvasControlLine(Geom::Point start, Geom::Point end, bool to_item, bool to_phantom,
+ Inkscape::CanvasItemColor ctrl_line_type,
+ Inkscape::XML::Node *measure_repr){
+ gint32 color = (ctrl_line_type == Inkscape::CANVAS_ITEM_PRIMARY) ? 0x0000ff7f : 0xff00007f;
+ if (to_phantom) {
+ color = (ctrl_line_type == Inkscape::CANVAS_ITEM_PRIMARY) ? 0x4444447f : 0x8888887f;
+ }
+
+ auto control_line = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), start, end);
+ control_line->set_stroke(color);
+ control_line->lower_to_bottom();
+ control_line->show();
+
+ if (to_phantom) {
+ measure_phantom_items.emplace_back(control_line);
+ } else {
+ measure_tmp_items.emplace_back(control_line);
+ }
+
+ if (to_item) {
+ setLine(start, end, false, color, measure_repr);
+ }
+}
+
+// This is the text that follows the cursor around.
+void MeasureTool::showItemInfoText(Geom::Point pos, Glib::ustring const &measure_str, double fontsize)
+{
+ auto canvas_tooltip = new CanvasItemText(_desktop->getCanvasTemp(), pos, measure_str);
+ canvas_tooltip->set_fontsize(fontsize);
+ canvas_tooltip->set_fill(0xffffffff);
+ canvas_tooltip->set_background(0x00000099);
+ canvas_tooltip->set_anchor(Geom::Point(0, 0));
+ canvas_tooltip->set_fixed_line(true);
+ canvas_tooltip->show();
+ measure_item.emplace_back(canvas_tooltip);
+}
+
+void MeasureTool::showInfoBox(Geom::Point cursor, bool into_groups)
+{
+ using Inkscape::Util::Quantity;
+
+ measure_item.clear();
+
+ SPItem *newover = _desktop->getItemAtPoint(cursor, into_groups);
+ if (!newover) {
+ // Clear over when the cursor isn't over anything.
+ over = nullptr;
+ return;
+ }
+ Inkscape::Util::Unit const *unit = _desktop->getNamedView()->getDisplayUnit();
+
+ // Load preferences for measuring the new object.
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int precision = prefs->getInt("/tools/measure/precision", 2);
+ bool selected = prefs->getBool("/tools/measure/only_selected", false);
+ auto box_type = prefs->getBool("/tools/bounding_box", false) ? SPItem::GEOMETRIC_BBOX : SPItem::VISUAL_BBOX;
+ double fontsize = prefs->getDouble("/tools/measure/fontsize", 10.0);
+ double scale = prefs->getDouble("/tools/measure/scale", 100.0) / 100.0;
+ Glib::ustring unit_name = prefs->getString("/tools/measure/unit", unit->abbr);
+
+ Geom::Scale zoom = Geom::Scale(Quantity::convert(_desktop->current_zoom(), "px", unit->abbr)).inverse();
+
+ if(newover != over) {
+ // Get information for the item, and cache it to save time.
+ over = newover;
+ auto affine = over->i2dt_affine() * Geom::Scale(scale);
+ // Correct for the current page's position.
+ if (prefs->getBool("/options/origincorrection/page", true)) {
+ affine *= _desktop->getDocument()->getPageManager().getSelectedPageAffine().inverse();
+ }
+ if (auto bbox = over->bounds(box_type, affine)) {
+ item_width = Quantity::convert(bbox->width(), "px", unit_name);
+ item_height = Quantity::convert(bbox->height(), "px", unit_name);
+ item_x = Quantity::convert(bbox->left(), "px", unit_name);
+ item_y = Quantity::convert(bbox->top(), "px", unit_name);
+
+ if (auto shape = cast<SPShape>(over)) {
+ auto pw = paths_to_pw(shape->curve()->get_pathvector());
+ item_length = Quantity::convert(Geom::length(pw * affine), "px", unit_name);
+ }
+ }
+ }
+
+ gchar *measure_str = nullptr;
+ std::stringstream precision_str;
+ precision_str.imbue(std::locale::classic());
+ double origin = Quantity::convert(14, "px", unit->abbr);
+ double yaxis_shift = Quantity::convert(fontsize, "px", unit->abbr);
+ Geom::Point rel_position = Geom::Point(origin, origin + yaxis_shift);
+ /* Keeps infobox just above the cursor */
+ Geom::Point pos = _desktop->w2d(cursor);
+ double gap = Quantity::convert(7 + fontsize, "px", unit->abbr);
+ double yaxisdir = _desktop->yaxisdir();
+
+ if (selected) {
+ showItemInfoText(pos - (yaxisdir * Geom::Point(0, rel_position[Geom::Y]) * zoom), _desktop->getSelection()->includes(over) ? _("Selected") : _("Not selected"), fontsize);
+ rel_position = Geom::Point(rel_position[Geom::X], rel_position[Geom::Y] + gap);
+ }
+
+ if (is<SPShape>(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 - (yaxisdir * Geom::Point(0, rel_position[Geom::Y]) * zoom), measure_str, fontsize);
+ rel_position = Geom::Point(rel_position[Geom::X], rel_position[Geom::Y] + gap);
+
+ } else if (is<SPGroup>(over)) {
+
+ measure_str = _("Press 'CTRL' to measure into group");
+ showItemInfoText(pos - (yaxisdir * Geom::Point(0, rel_position[Geom::Y]) * 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 - (yaxisdir * Geom::Point(0, rel_position[Geom::Y]) * 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 - (yaxisdir * Geom::Point(0, rel_position[Geom::Y]) * 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 - (yaxisdir * Geom::Point(0, rel_position[Geom::Y]) * 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 - (yaxisdir * Geom::Point(0, rel_position[Geom::Y]) * 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)
+{
+ 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
+ measure_tmp_items.clear();
+
+ //TODO:Calculate the measure area for current length and origin
+ // and use canvas->redraw_all(). 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 - start_p);
+ angle -= baseAngle;
+
+ // make sure that the angle is between -pi and pi.
+ if (angle > M_PI) {
+ angle -= 2 * M_PI;
+ }
+ if (angle < -M_PI) {
+ angle += 2 * M_PI;
+ }
+ }
+
+ 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);
+ SPGroup *current_layer = _desktop->layerManager().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 || _desktop->layerManager().layerForObject(item) == current_layer) {
+ if (auto shape = cast<SPShape>(item)) {
+ calculate_intersections(_desktop, item, lineseg, *shape->curve(), intersection_times);
+ } else {
+ if (is<SPText>(item) || is<SPFlowtext>(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:
+ auto curve = te_get_layout(item)->convertToCurves(iter, iter_next);
+ iter = iter_next; // shift to next glyph
+ if (curve.is_empty()) { // whitespace glyph?
+ continue;
+ }
+
+ calculate_intersections(_desktop, item, lineseg, std::move(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,
+ false, 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,
+ false, 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,
+ true, 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,
+ false, 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, Inkscape::CANVAS_ITEM_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, Inkscape::CANVAS_ITEM_SECONDARY, measure_repr);
+ createAngleDisplayCurve(start_p, end_p, angleDisplayPt, angle, to_phantom, 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, Inkscape::CANVAS_ITEM_PRIMARY , measure_repr);
+
+ setMeasureCanvasControlLine(_desktop->doc2dt(intersections[0]), _desktop->doc2dt(intersections[0]) + normal * dimension_offset, to_item, to_phantom, Inkscape::CANVAS_ITEM_PRIMARY , measure_repr);
+
+ setMeasureCanvasControlLine(_desktop->doc2dt(intersections[intersections.size() - 1]), _desktop->doc2dt(intersections[intersections.size() - 1]) + normal * dimension_offset, to_item, to_phantom, Inkscape::CANVAS_ITEM_PRIMARY , measure_repr);
+ }
+
+ // call-out lines
+ for (auto & place : placements) {
+ setMeasureCanvasControlLine(place.start, place.end, to_item, to_phantom, Inkscape::CANVAS_ITEM_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, Inkscape::CANVAS_ITEM_SECONDARY, measure_repr);
+ }
+ }
+}
+
+/**
+ * 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 MeasureTool::setMeasureItem(Geom::PathVector pathv, bool is_curve, bool markers, guint32 color, Inkscape::XML::Node *measure_repr)
+{
+ if(!_desktop) {
+ return;
+ }
+ SPDocument *doc = _desktop->getDocument();
+ Inkscape::XML::Document *xml_doc = doc->getReprDoc();
+ Inkscape::XML::Node *repr;
+ repr = xml_doc->createElement("svg:path");
+ auto str = sp_svg_write_path(pathv);
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ auto layer = _desktop->layerManager().currentLayer();
+ Geom::Coord strokewidth = layer->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);
+ repr->setAttribute("d", str);
+ if(measure_repr) {
+ measure_repr->addChild(repr, nullptr);
+ Inkscape::GC::release(repr);
+ } else {
+ auto item = cast<SPItem>(layer->appendChildRepr(repr));
+ Inkscape::GC::release(repr);
+ item->updateRepr();
+ _desktop->getSelection()->clear();
+ _desktop->getSelection()->add(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 :
diff --git a/src/ui/tools/measure-tool.h b/src/ui/tools/measure-tool.h
new file mode 100644
index 0000000..f8f1920
--- /dev/null
+++ b/src/ui/tools/measure-tool.h
@@ -0,0 +1,127 @@
+// 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 <boost/optional.hpp>
+#include <optional>
+
+#include <sigc++/sigc++.h>
+
+#include <2geom/point.h>
+
+#include "ui/tools/tool-base.h"
+
+#include "display/control/canvas-temporary-item.h"
+#include "display/control/canvas-item-enums.h"
+#include "display/control/canvas-item-ptr.h"
+
+class SPKnot;
+
+namespace Inkscape {
+
+class CanvasItemCurve;
+
+namespace UI {
+namespace Tools {
+
+class MeasureTool : public ToolBase {
+public:
+ MeasureTool(SPDesktop *desktop);
+ ~MeasureTool() 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);
+ Geom::Point readMeasurePoint(bool is_start);
+
+ void showInfoBox(Geom::Point cursor, bool into_groups);
+ void showItemInfoText(Geom::Point pos, Glib::ustring const &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,
+ bool to_left, 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::CanvasItemColor color, Inkscape::XML::Node *measure_repr);
+ void setLabelText(Glib::ustring const &value, Geom::Point pos, double fontsize, Geom::Coord angle,
+ guint32 background,
+ Inkscape::XML::Node *measure_repr = nullptr);
+
+ 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*/);
+ void setMeasureItem(Geom::PathVector pathv, bool is_curve, bool markers, guint32 color, Inkscape::XML::Node *measure_repr);
+ void createAngleDisplayCurve(Geom::Point const &center, Geom::Point const &end, Geom::Point const &anchor,
+ double angle, bool to_phantom,
+ Inkscape::XML::Node *measure_repr = nullptr);
+
+private:
+ std::optional<Geom::Point> explicit_base;
+ std::optional<Geom::Point> last_end;
+ SPKnot *knot_start = nullptr;
+ SPKnot *knot_end = nullptr;
+ gint dimension_offset = 20;
+ Geom::Point start_p;
+ Geom::Point end_p;
+ Geom::Point last_pos;
+
+ std::vector<CanvasItemPtr<CanvasItem>> measure_tmp_items;
+ std::vector<CanvasItemPtr<CanvasItem>> measure_phantom_items;
+ std::vector<CanvasItemPtr<CanvasItem>> 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..2521471
--- /dev/null
+++ b/src/ui/tools/mesh-tool.cpp
@@ -0,0 +1,970 @@
+// 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
+
+#include "mesh-tool.h"
+
+// 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 "display/control/canvas-item-curve.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/icon-names.h"
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+// TODO: The gradient tool class looks like a 1:1 copy.
+
+MeshTool::MeshTool(SPDesktop *desktop)
+ : ToolBase(desktop, "/tools/mesh", "mesh.svg")
+// TODO: Why are these connections stored as pointers?
+ , selcon(nullptr)
+ , subselcon(nullptr)
+ , cursor_addnode(false)
+ , show_handles(true)
+ , edit_fill(true)
+ , edit_stroke(true)
+{
+ // TODO: This value is overwritten in the root handler
+ this->tolerance = 6;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/tools/mesh/selcue", true)) {
+ this->enableSelectionCue();
+ }
+
+ this->enableGrDrag();
+ Inkscape::Selection *selection = desktop->getSelection();
+
+ this->selcon = new sigc::connection(selection->connectChanged(
+ sigc::mem_fun(*this, &MeshTool::selection_changed)
+ ));
+
+ this->subselcon = new sigc::connection(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);
+}
+
+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*/) {
+ Inkscape::Selection *selection = _desktop->getSelection();
+
+ if (selection == nullptr) {
+ return;
+ }
+
+ guint n_obj = (guint) boost::distance(selection->items());
+
+ if (!_grdrag->isNonEmpty() || selection->isEmpty()) {
+ return;
+ }
+
+ guint n_tot = _grdrag->numDraggers();
+ guint n_sel = _grdrag->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 (_grdrag->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),nullptr);
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE, message,
+ _(ms_handle_descr[_grdrag->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",
+ _grdrag->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),nullptr);
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE, message, _grdrag->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),nullptr);
+ 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::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 MeshTool::select_next()
+{
+ g_assert(_grdrag);
+ GrDragger *d = _grdrag->select_next();
+ _desktop->scroll_to_point(d->point);
+}
+
+void MeshTool::select_prev()
+{
+ g_assert(_grdrag);
+ GrDragger *d = _grdrag->select_prev();
+ _desktop->scroll_to_point(d->point);
+}
+
+/**
+ * Returns vector of control curves mouse is over. Returns only first if 'first' is true.
+ * event_p is in canvas (world) units.
+ */
+std::vector<GrDrag::ItemCurve*> MeshTool::over_curve(Geom::Point event_p, bool first)
+{
+ // Translate mouse point into proper coord system: needed later.
+ mousepoint_doc = _desktop->w2d(event_p);
+ std::vector<GrDrag::ItemCurve*> selected;
+
+ for (auto &it : _grdrag->item_curves) {
+ if (it.curve->contains(event_p, tolerance)) {
+ selected.emplace_back(&it);
+ if (first) {
+ break;
+ }
+ }
+ }
+ return selected;
+}
+
+/**
+Split row/column near the mouse point.
+*/
+void MeshTool::split_near_point(SPItem *item, Geom::Point mouse_p, guint32 /*etime*/)
+{
+#ifdef DEBUG_MESH
+ std::cout << "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
+ get_drag()->addStopNearPoint(item, mouse_p, tolerance / _desktop->current_zoom());
+ DocumentUndo::done(_desktop->getDocument(), _("Split mesh row/column"), INKSCAPE_ICON("mesh-gradient"));
+ get_drag()->updateDraggers();
+}
+
+/**
+Wrapper for various mesh operations that require a list of selected corner nodes.
+ */
+void MeshTool::corner_operation(MeshCornerOperation operation)
+{
+
+#ifdef DEBUG_MESH
+ std::cout << "sp_mesh_corner_operation: entrance: " << operation << std::endl;
+#endif
+
+ SPDocument *doc = nullptr;
+
+ 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 : _grdrag->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
+ auto gradient = cast<SPMeshGradient>( 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 = 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::cerr << "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, _("Toggled mesh path type."), INKSCAPE_ICON("mesh-gradient"));
+ _grdrag->local_change = true; // Don't create new draggers.
+ break;
+
+ case MG_CORNER_SIDE_ARC:
+ DocumentUndo::done(doc, _("Approximated arc for mesh side."), INKSCAPE_ICON("mesh-gradient"));
+ _grdrag->local_change = true; // Don't create new draggers.
+ break;
+
+ case MG_CORNER_TENSOR_TOGGLE:
+ DocumentUndo::done(doc, _("Toggled mesh tensors."), INKSCAPE_ICON("mesh-gradient"));
+ _grdrag->local_change = true; // Don't create new draggers.
+ break;
+
+ case MG_CORNER_COLOR_SMOOTH:
+ DocumentUndo::done(doc, _("Smoothed mesh corner color."), INKSCAPE_ICON("mesh-gradient"));
+ _grdrag->local_change = true; // Don't create new draggers.
+ break;
+
+ case MG_CORNER_COLOR_PICK:
+ DocumentUndo::done(doc, _("Picked mesh corner color."), INKSCAPE_ICON("mesh-gradient"));
+ _grdrag->local_change = true; // Don't create new draggers.
+ break;
+
+ case MG_CORNER_INSERT:
+ DocumentUndo::done(doc, _("Inserted new row or column."), INKSCAPE_ICON("mesh-gradient"));
+ break;
+
+ default:
+ std::cerr << "sp_mesh_corner_operation: unknown operation" << std::endl;
+ }
+ }
+ }
+ }
+}
+
+
+/**
+ * Scale mesh to just fit into bbox of selected items.
+ */
+void MeshTool::fit_mesh_in_bbox()
+{
+
+#ifdef DEBUG_MESH
+ std::cout << "fit_mesh_in_bbox: entrance: Entrance" << std::endl;
+#endif
+
+ 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 ( is<SPMeshGradient>(server) ) {
+
+ Geom::OptRect item_bbox = item->geometricBounds();
+ auto gradient = cast<SPMeshGradient>(server);
+ if (gradient->array.fill_box( item_bbox )) {
+ changed = true;
+ }
+ }
+ }
+
+ if (style->stroke.isPaintserver()) {
+ SPPaintServer *server = item->style->getStrokePaintServer();
+ if ( is<SPMeshGradient>(server) ) {
+
+ Geom::OptRect item_bbox = item->visualBounds();
+ auto gradient = cast<SPMeshGradient>(server);
+ if (gradient->array.fill_box( item_bbox )) {
+ changed = true;
+ }
+ }
+ }
+
+ }
+ }
+ if (changed) {
+ DocumentUndo::done(_desktop->getDocument(), _("Fit mesh inside bounding box"), INKSCAPE_ICON("mesh-gradient"));
+ }
+}
+
+
+/**
+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);
+
+ // Get value of fill or stroke preference
+ Inkscape::PaintTarget fill_or_stroke_pref =
+ static_cast<Inkscape::PaintTarget>(prefs->getInt("/tools/mesh/newfillorstroke"));
+
+ g_assert(_grdrag);
+ gint ret = FALSE;
+
+ switch (event->type) {
+ case GDK_2BUTTON_PRESS:
+
+#ifdef DEBUG_MESH
+ std::cout << "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? (Should replace by CanvasItem event.)
+ auto over_curve = this->over_curve(Geom::Point(event->motion.x, event->motion.y));
+
+ if (!over_curve.empty()) {
+ // We take the first item in selection, because with doubleclick, the first click
+ // always resets selection to the single object under cursor
+ split_near_point(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 && is<SPMeshGradient>(server))
+ has_mesh = true;
+ }
+ }
+
+ if (!has_mesh) {
+ new_default();
+ }
+ }
+
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_BUTTON_PRESS:
+
+#ifdef DEBUG_MESH
+ std::cout << "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 ) {
+
+ // Are we over a mesh curve?
+ auto over_curve = this->over_curve(Geom::Point(event->motion.x, event->motion.y), false);
+
+ if (!over_curve.empty()) {
+ for (auto it : over_curve) {
+ Inkscape::PaintTarget fill_or_stroke = it->is_fill ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE;
+ GrDragger *dragger0 = _grdrag->getDraggerFor(it->item, POINT_MG_CORNER, it->corner0, fill_or_stroke);
+ GrDragger *dragger1 = _grdrag->getDraggerFor(it->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 ) {
+ _grdrag->deselectAll();
+ }
+ _grdrag->setSelected( dragger0, true, !toggle );
+ _grdrag->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 && is<SPMeshGradient>(server))
+ has_mesh = true;
+ }
+ }
+
+ if (has_mesh && !(event->button.state & GDK_CONTROL_MASK)) {
+ 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 ) ) {
+
+#ifdef DEBUG_MESH
+ std::cout << "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 = _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 (!_grdrag->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 = _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 (_grdrag->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
+ auto over_curve = this->over_curve(Geom::Point(event->motion.x, event->motion.y));
+
+ if (this->cursor_addnode && over_curve.empty()) {
+ this->set_cursor("mesh.svg");
+ this->cursor_addnode = false;
+ } else if (!this->cursor_addnode && !over_curve.empty()) {
+ this->set_cursor("mesh-add.svg");
+ this->cursor_addnode = true;
+ }
+ }
+ break;
+
+ case GDK_BUTTON_RELEASE:
+
+#ifdef DEBUG_MESH
+ std::cout << "root_handler: GDK_BUTTON_RELEASE" << std::endl;
+#endif
+
+ this->xp = this->yp = 0;
+
+ if ( event->button.button == 1 ) {
+
+ // Check if over line
+ auto over_curve = this->over_curve(Geom::Point(event->motion.x, event->motion.y));
+
+ if ( (event->button.state & GDK_CONTROL_MASK) && (event->button.state & GDK_MOD1_MASK ) ) {
+ if (!over_curve.empty()) {
+ split_near_point(over_curve[0]->item, mousepoint_doc, 0);
+ ret = TRUE;
+ }
+ } else {
+ dragging = false;
+
+ // unless clicked with Ctrl (to enable Ctrl+doubleclick).
+ if (event->button.state & GDK_CONTROL_MASK && !(event->button.state & GDK_SHIFT_MASK)) {
+ ret = TRUE;
+ Inkscape::Rubberband::get(_desktop)->stop();
+ 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 && is<SPMeshGradient>(server))
+ has_mesh = true;
+ }
+ }
+
+ if (!has_mesh) {
+ new_default();
+ } 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)) {
+ _grdrag->deselectAll();
+ }
+ _grdrag->selectRect(*b);
+ }
+ }
+ }
+
+ } else if (this->item_to_select) {
+ if (!over_curve.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 {
+ _grdrag->deselectAll();
+ selection->set(this->item_to_select);
+ }
+ }
+ } else {
+ if (!over_curve.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 (!_grdrag->selected.empty()) {
+ _grdrag->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 << "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) && _grdrag->isNonEmpty()) {
+ _grdrag->selectAll();
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Escape:
+ if (!_grdrag->selected.empty()) {
+ _grdrag->deselectAll();
+ } else {
+ selection->clear();
+ }
+
+ ret = TRUE;
+ //TODO: make dragging escapable by Esc
+ break;
+
+ // Mesh Operations --------------------------------------------
+
+ case GDK_KEY_Insert:
+ case GDK_KEY_KP_Insert:
+ // with any modifiers:
+ this->corner_operation(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)
+ this->corner_operation(MG_CORNER_INSERT);
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Delete:
+ case GDK_KEY_KP_Delete:
+ case GDK_KEY_BackSpace:
+ if (!_grdrag->selected.empty()) {
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_b: // Toggle mesh side between lineto and curveto.
+ case GDK_KEY_B:
+ if (MOD__ALT(event) && _grdrag->isNonEmpty() && _grdrag->hasSelection()) {
+ this->corner_operation(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) && _grdrag->isNonEmpty() && _grdrag->hasSelection()) {
+ this->corner_operation(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) && _grdrag->isNonEmpty() && _grdrag->hasSelection()) {
+ this->corner_operation(MG_CORNER_TENSOR_TOGGLE);
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_j: // Smooth corner color
+ case GDK_KEY_J:
+ if (MOD__ALT(event) && _grdrag->isNonEmpty() && _grdrag->hasSelection()) {
+ this->corner_operation(MG_CORNER_COLOR_SMOOTH);
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_k: // Pick corner color
+ case GDK_KEY_K:
+ if (MOD__ALT(event) && _grdrag->isNonEmpty() && _grdrag->hasSelection()) {
+ this->corner_operation(MG_CORNER_COLOR_PICK);
+ ret = TRUE;
+ }
+ break;
+
+ default:
+ ret = _grdrag->key_press_handler(event);
+ break;
+ }
+
+ break;
+
+ case GDK_KEY_RELEASE:
+
+#ifdef DEBUG_MESH
+ std::cout << "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.
+void MeshTool::new_default()
+{
+ 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 = is<SPText>(*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(), _("Create mesh"), INKSCAPE_ICON("mesh-gradient"));
+
+ // 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());
+ 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..8fcf163
--- /dev/null
+++ b/src/ui/tools/mesh-tool.h
@@ -0,0 +1,86 @@
+// 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 "gradient-drag.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 {
+
+class Selection;
+class CanvasItemCurve;
+
+namespace UI {
+namespace Tools {
+
+class MeshTool : public ToolBase {
+public:
+ MeshTool(SPDesktop *desktop);
+ ~MeshTool() override;
+
+ Geom::Point origin;
+
+ Geom::Point mousepoint_doc; // stores mousepoint when over_line in doc coords
+
+ sigc::connection *selcon;
+ sigc::connection *subselcon;
+
+ void set(const Inkscape::Preferences::Entry& val) override;
+ bool root_handler(GdkEvent* event) override;
+ void fit_mesh_in_bbox();
+ void corner_operation(MeshCornerOperation operation);
+
+private:
+ bool cursor_addnode;
+ bool show_handles;
+ bool edit_fill;
+ bool edit_stroke;
+
+ void selection_changed(Inkscape::Selection *sel);
+ void select_next();
+ void select_prev();
+ void new_default();
+ void split_near_point(SPItem *item, Geom::Point mouse_p, guint32 /*etime*/);
+ std::vector<GrDrag::ItemCurve*> over_curve(Geom::Point event_p, bool first = true);
+};
+
+}
+}
+}
+
+#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..ad82477
--- /dev/null
+++ b/src/ui/tools/node-tool.cpp
@@ -0,0 +1,861 @@
+// 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 "rubberband.h"
+#include "selection-chemistry.h"
+#include "selection.h"
+#include "snap.h"
+
+#include "display/curve.h"
+#include "display/control/canvas-item-bpath.h"
+#include "display/control/canvas-item-group.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/knot/knot-holder.h"
+#include "ui/modifiers.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/tools/node-tool.h"
+
+using Inkscape::Modifiers::Modifier;
+
+/** @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.
+ */
+
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+Inkscape::CanvasItemGroup *create_control_group(SPDesktop *desktop)
+{
+ auto group = new Inkscape::CanvasItemGroup(desktop->getCanvasControls());
+ group->set_name("CanvasItemGroup:NodeTool");
+ return group;
+}
+
+NodeTool::NodeTool(SPDesktop *desktop)
+ : ToolBase(desktop, "/tools/nodes", "node.svg")
+{
+ this->_path_data = new Inkscape::UI::PathSharedData();
+
+ Inkscape::UI::PathSharedData &data = *this->_path_data;
+ data.node_data.desktop = 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(desktop);
+ data.node_data.handle_line_group = new Inkscape::CanvasItemGroup(desktop->getCanvasControls());
+ data.dragpoint_group = create_control_group(desktop);
+ _transform_handle_group = create_control_group(desktop);
+ data.node_data.node_group = create_control_group(desktop);
+ data.node_data.handle_group = create_control_group(desktop);
+
+ data.node_data.handle_line_group->set_name("CanvasItemGroup:NodeTool:handle_line_group");
+
+ Inkscape::Selection *selection = 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));
+
+ if (this->_transform_handle_group) {
+ this->_selected_nodes = new Inkscape::UI::ControlPointSelection(desktop, this->_transform_handle_group);
+ }
+ data.node_data.selection = this->_selected_nodes;
+
+ this->_multipath = new Inkscape::UI::MultiPathManipulator(data, this->_selection_changed_connection);
+
+ this->_multipath->signal_coords_changed.connect([=](){
+ desktop->emit_control_point_selected(this, _selected_nodes);
+ });
+
+ 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();
+ }
+
+ desktop->emit_control_point_selected(this, _selected_nodes); // sets the coord entry fields to inactive
+ sp_update_helperpath(desktop);
+}
+
+NodeTool::~NodeTool()
+{
+ this->_selected_nodes->clear();
+ this->get_rubberband()->stop();
+
+ this->enableGrDrag(false);
+
+ if (this->flash_tempitem) {
+ _desktop->remove_temporary_canvasitem(this->flash_tempitem);
+ }
+ for (auto hp : this->_helperpath_tmpitem) {
+ _desktop->remove_temporary_canvasitem(hp);
+ }
+ this->_selection_changed_connection.disconnect();
+ // this->_selection_modified_connection.disconnect();
+ this->_mouseover_changed_connection.disconnect();
+
+ delete this->_multipath;
+ delete this->_selected_nodes;
+
+ _path_data->node_data.node_group->unlink();
+ _path_data->node_data.handle_group->unlink();
+ _path_data->node_data.handle_line_group->unlink();
+ _path_data->outline_group->unlink();
+ _path_data->dragpoint_group->unlink();
+ _transform_handle_group->unlink();
+}
+
+Inkscape::Rubberband *NodeTool::get_rubberband() const
+{
+ return Inkscape::Rubberband::get(_desktop);
+}
+
+void NodeTool::deleteSelected()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ // This takes care of undo internally
+ _multipath->deleteNodes(prefs->getBool("/tools/nodes/delete_preserves_shape", true));
+}
+
+// show helper paths of the applied LPE, if any
+void sp_update_helperpath(SPDesktop *desktop)
+{
+ if (!desktop) {
+ return;
+ }
+
+ Inkscape::UI::Tools::NodeTool *nt = dynamic_cast<Inkscape::UI::Tools::NodeTool*>(desktop->event_context);
+ if (!nt) {
+ // We remove this warning and just stop execution
+ // because we are updating helper paths also from LPE dialog so we not unsure the tool used
+ // std::cerr << "sp_update_helperpath called when Node Tool not active!" << std::endl;
+ return;
+ }
+
+ 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) {
+ auto lpeitem = cast<SPLPEItem>(item);
+ if (lpeitem && lpeitem->hasPathEffectRecursive()) {
+ Inkscape::LivePathEffect::Effect *lpe = 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;
+ std::vector<Geom::PathVector> cs = lpe->getCanvasIndicators(lpeitem);
+ for (auto &p : cs) {
+ p *= desktop->dt2doc();
+ c.append(p);
+ }
+ if (!c.is_empty()) {
+ auto helperpath = new Inkscape::CanvasItemBpath(desktop->getCanvasTemp(), c.get_pathvector(), true);
+ helperpath->set_stroke(0x0000ff9a);
+ helperpath->set_fill(0x0, SP_WIND_RULE_NONZERO); // No fill
+ nt->_helperpath_tmpitem.emplace_back(desktop->add_temporary_canvasitem(helperpath, 0));
+ }
+ }
+ }
+ }
+}
+
+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(_desktop->getSelection());
+ } else if (entry_name == "edit_masks") {
+ this->edit_masks = value.getBool();
+ this->selection_changed(_desktop->getSelection());
+ } 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 && (is<SPGroup>(obj) || is<SPObjectGroup>(obj))) {
+ for (auto& c: obj->children) {
+ gather_items(nt, base, &c, role, s);
+ }
+ } else if (auto item = cast<SPItem>(obj)) {
+ ShapeRecord r;
+ r.object = obj;
+ r.role = role;
+
+ // TODO add support for objectBoundingBox
+ if (role != SHAPE_ROLE_NORMAL && base) {
+ r.edit_transform = base->i2doc_affine();
+ }
+
+ 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;
+ 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 (auto i = this->_shape_editors.begin(); i != this->_shape_editors.end();) {
+ ShapeRecord s;
+ s.object = i->first;
+
+ if (shapes.find(s) == shapes.end()) {
+ this->_shape_editors.erase(i++);
+ } else {
+ ++i;
+ }
+ }
+
+ for (const auto & r : shapes) {
+ if (this->_shape_editors.find(cast<SPItem>(r.object)) == this->_shape_editors.end()) {
+ auto si = std::make_unique<ShapeEditor>(_desktop, r.edit_transform);
+ auto item = cast<SPItem>(r.object);
+ si->set_item(item);
+ this->_shape_editors.insert({item, std::move(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(_desktop);
+ // This not need to be called canvas is updated on selection change on setItems
+ // _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
+
+ Inkscape::Selection *selection = _desktop->getSelection();
+ static Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ auto rband = get_rubberband();
+
+ if (!rband->is_started()) {
+ if (_multipath->event(this, event) || _selected_nodes->event(this, event))
+ return true;
+ }
+
+ switch (event->type)
+ {
+
+ case GDK_MOTION_NOTIFY: {
+ sp_update_helperpath(_desktop);
+ SPItem *over_item = nullptr;
+ 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(_desktop->w2d(motion_w));
+
+ if (event->motion.state & GDK_BUTTON1_MASK) {
+ if (rband->is_started()) {
+ rband->move(motion_dt);
+ }
+
+ auto touch_path = Modifier::get(Modifiers::Type::SELECT_TOUCH_PATH)->get_label();
+ if (rband->getMode() == RUBBERBAND_MODE_TOUCHPATH) {
+ this->defaultMessageContext()->setF(Inkscape::NORMAL_MESSAGE,
+ _("<b>Draw over</b> lines to select their nodes; release <b>%s</b> to switch to rubberband selection"), touch_path.c_str());
+ } else {
+ this->defaultMessageContext()->setF(Inkscape::NORMAL_MESSAGE,
+ _("<b>Drag around</b> nodes to select them; press <b>%s</b> to switch to box selection"), touch_path.c_str());
+ }
+ return true;
+ } else if (rband->is_moved()) {
+ // Mouse button is up, but rband is still kicking.
+ rband->stop();
+ }
+
+ SnapManager &m = _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 (!_desktop->getSelection()->isEmpty()) {
+ if (!(event->motion.state & GDK_SHIFT_MASK)) {
+ m.setup(_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")) {
+ if (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;
+ }
+
+ auto shape = cast<SPShape>(over_item);
+ if (!shape) {
+ break; // for now, handle only shapes
+ }
+
+ this->flashed_item = over_item;
+ if (!shape->curveForEdit()) {
+ break; // break out when curve doesn't exist
+ }
+
+ auto c = shape->curveForEdit()->transformed(over_item->i2dt_affine());
+
+ auto flash = new Inkscape::CanvasItemBpath(_desktop->getCanvasTemp(), c.get_pathvector(), true);
+ flash->set_stroke(over_item->highlight_color());
+ flash->set_fill(0x0, SP_WIND_RULE_NONZERO); // No fill.
+ flash_tempitem =
+ _desktop->add_temporary_canvasitem(flash, prefs->getInt("/tools/nodes/pathflash_timeout", 500));
+ }
+ 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;
+
+ case GDK_KEY_Tab:
+ _multipath->shiftSelection(1);
+ return TRUE;
+ break;
+ case GDK_KEY_ISO_Left_Tab:
+ _multipath->shiftSelection(-1);
+ 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_PRESS:
+ if (event->button.button == 1) {
+ if(Modifier::get(Modifiers::Type::SELECT_TOUCH_PATH)->active(event->button.state)) {
+ rband->setMode(RUBBERBAND_MODE_TOUCHPATH);
+ } else {
+ rband->defaultMode();
+ }
+
+ Geom::Point const event_pt(event->button.x, event->button.y);
+ Geom::Point const desktop_pt(_desktop->w2d(event_pt));
+ rband->start(_desktop, desktop_pt, true);
+ return true;
+ }
+ break;
+
+ case GDK_BUTTON_RELEASE:
+ if (event->button.button == 1) {
+ if (rband->is_started() && rband->is_moved()) {
+ select_area(rband->getPath(), &event->button);
+ } else {
+ select_point(&event->button);
+ }
+ rband->stop();
+ return true;
+ }
+ break;
+
+ case GDK_2BUTTON_PRESS:
+ if ( event->button.button == 1 ) {
+ // 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(_desktop->w2d(motion_w));
+
+ SnapManager &m = _desktop->namedview->snap_manager;
+ m.setup(_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(_desktop->d2w(sp.getPoint()));
+ return true;
+ }
+ }
+ }
+ break;
+
+ default:
+ break;
+ }
+ // we really dont want to stop any node operation we want to success all even the time consume it
+
+ return ToolBase::root_handler(event);
+}
+
+bool NodeTool::item_handler(SPItem *item, GdkEvent *event)
+{
+ bool ret = ToolBase::item_handler(item, event);
+
+ // Node shape editors are handled differently than shape tools
+ if (!ret && event->type == GDK_BUTTON_PRESS && event->button.button == 1) {
+ for (auto &se : _shape_editors) {
+ // This allows users to select an arbitary position in a pattern to edit on canvas.
+ if (auto knotholder = se.second->knotholder) {
+ auto point = Geom::Point(event->button.x, event->button.y);
+
+ // This allows us to dive into groups and find what the real item is
+ if (_desktop->getItemAtPoint(point, true) != knotholder->getItem())
+ continue;
+
+ ret = knotholder->set_item_clickpos(_desktop->w2d(point) * _desktop->dt2doc());
+ }
+ }
+ }
+ return ret;
+}
+
+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"));
+ }
+ }
+}
+
+void NodeTool::select_area(Geom::Path const &path, GdkEventButton *event) {
+ using namespace Inkscape::UI;
+
+ if (this->_multipath->empty()) {
+ // if multipath is empty, select rubberbanded items rather than nodes
+ Inkscape::Selection *selection = _desktop->getSelection();
+ auto sel_doc = _desktop->dt2doc() * *path.boundsFast();
+ std::vector<SPItem *> items = _desktop->getDocument()->getItemsInBox(_desktop->dkey, sel_doc);
+ selection->setList(items);
+ } else {
+ bool shift = held_shift(*event);
+ bool ctrl = held_control(*event);
+
+ if (!shift) {
+ // A/C. No modifier, selects all nodes, or selects all other nodes.
+ this->_selected_nodes->clear();
+ }
+ if (shift && ctrl) {
+ // D. Shift+Ctrl pressed, removes nodes under box from existing selection.
+ this->_selected_nodes->selectArea(path, true);
+ } else {
+ // A/B/C. Adds nodes under box to existing selection.
+ this->_selected_nodes->selectArea(path);
+ if (ctrl) {
+ // C. Selects the inverse of all nodes under the box.
+ this->_selected_nodes->invertSelection();
+ }
+ }
+ }
+}
+
+void NodeTool::select_point(GdkEventButton *event) {
+ using namespace Inkscape::UI; // pull in event helpers
+
+ if (!event) {
+ return;
+ }
+
+ if (event->button != 1) {
+ return;
+ }
+
+ Inkscape::Selection *selection = _desktop->getSelection();
+
+ SPItem *item_clicked = sp_event_context_find_item (_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 if (!selection->includes(item_clicked)) {
+ selection->set(item_clicked);
+ }
+ // This not need to be called canvas is updated on selection change
+ // _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->set_cursor("node-mouseover.svg");
+ this->cursor_drag = true;
+ } else if (!cdp && this->cursor_drag) {
+ this->set_cursor("node.svg");
+ 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..d02481b
--- /dev/null
+++ b/src/ui/tools/node-tool.h
@@ -0,0 +1,115 @@
+// 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 <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;
+ }
+
+ class Rubberband;
+}
+
+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(SPDesktop *desktop);
+ ~NodeTool() override;
+
+ Inkscape::UI::ControlPointSelection* _selected_nodes = nullptr;
+ Inkscape::UI::MultiPathManipulator* _multipath = nullptr;
+ std::vector<Inkscape::Display::TemporaryItem *> _helperpath_tmpitem;
+ std::map<SPItem *, std::unique_ptr<ShapeEditor>> _shape_editors;
+
+ bool edit_clipping_paths = false;
+ bool edit_masks = false;
+
+ void set(const Inkscape::Preferences::Entry& val) override;
+ bool root_handler(GdkEvent* event) override;
+ bool item_handler(SPItem *item, GdkEvent *event) override;
+ void deleteSelected();
+private:
+ Inkscape::Rubberband *get_rubberband() const;
+
+ sigc::connection _selection_changed_connection;
+ sigc::connection _mouseover_changed_connection;
+
+ SPItem *flashed_item = nullptr;
+
+ Inkscape::Display::TemporaryItem *flash_tempitem = nullptr;
+ Inkscape::UI::Selector* _selector = nullptr;
+ Inkscape::UI::PathSharedData* _path_data = nullptr;
+ Inkscape::CanvasItemGroup *_transform_handle_group = nullptr;
+ SPItem *_last_over = nullptr;
+
+ bool cursor_drag = false;
+ bool show_handles = false;
+ bool show_outline =false;
+ bool live_outline = false;
+ bool live_objects = false;
+ bool show_path_direction = false;
+ bool show_transform_handles = false;
+ bool single_node_transform_handles = false;
+
+ std::vector<SPItem*> _current_selection;
+ std::vector<SPItem*> _previous_selection;
+
+ void selection_changed(Inkscape::Selection *sel);
+
+ void select_area(Geom::Path const &path, GdkEventButton *event);
+ void select_point(GdkEventButton *event);
+ void mouseover_changed(Inkscape::UI::ControlPoint *p);
+ void update_tip(GdkEvent *event);
+ void handleControlUiStyleChange();
+};
+void sp_update_helperpath(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/tools/pages-tool.cpp b/src/ui/tools/pages-tool.cpp
new file mode 100644
index 0000000..45c5dfe
--- /dev/null
+++ b/src/ui/tools/pages-tool.cpp
@@ -0,0 +1,668 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Page editing tool
+ *
+ * Authors:
+ * Martin Owens <doctormo@geek-2.com>
+ *
+ * Copyright (C) 2021 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "pages-tool.h"
+
+#include <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.h>
+
+#include "desktop.h"
+#include "display/control/canvas-item-bpath.h"
+#include "display/control/canvas-item-curve.h"
+#include "display/control/canvas-item-group.h"
+#include "display/control/canvas-item-rect.h"
+#include "display/control/snap-indicator.h"
+#include "document-undo.h"
+#include "include/macros.h"
+#include "object/sp-page.h"
+#include "path/path-outline.h"
+#include "pure-transform.h"
+#include "rubberband.h"
+#include "selection-chemistry.h"
+#include "selection.h"
+#include "snap-preferences.h"
+#include "snap.h"
+#include "ui/icon-names.h"
+#include "ui/knot/knot.h"
+#include "ui/modifiers.h"
+#include "ui/widget/canvas.h"
+
+using Inkscape::Modifiers::Modifier;
+
+#define INDEX_OF(v, k) (std::distance(v.begin(), std::find(v.begin(), v.end(), k)));
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+PagesTool::PagesTool(SPDesktop *desktop)
+ : ToolBase(desktop, "/tools/pages", "select.svg")
+{
+ // Stash the regular object selection so we don't modify them in base-tools root handler.
+ desktop->getSelection()->setBackup();
+ desktop->getSelection()->clear();
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ drag_tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+
+ if (resize_knots.empty()) {
+ for (int i = 0; i < 4; i++) {
+ auto knot = new SPKnot(desktop, _("Resize page"), Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "PageTool:Resize");
+ knot->setShape(Inkscape::CANVAS_ITEM_CTRL_SHAPE_SQUARE);
+ knot->setFill(0xffffff00, 0x0000ff00, 0x000000ff, 0x000000ff);
+ knot->setSize(9);
+ knot->setAnchor(SP_ANCHOR_CENTER);
+ knot->updateCtrl();
+ knot->hide();
+ knot->moved_signal.connect(sigc::mem_fun(*this, &PagesTool::resizeKnotMoved));
+ knot->ungrabbed_signal.connect(sigc::mem_fun(*this, &PagesTool::resizeKnotFinished));
+ resize_knots.push_back(knot);
+
+ auto m_knot = new SPKnot(desktop, _("Set page margin"), Inkscape::CANVAS_ITEM_CTRL_TYPE_MARGIN, "PageTool:Margin");
+ m_knot->setFill(0xffffff00, 0x0000ff00, 0x000000ff, 0x000000ff);
+ m_knot->setStroke(0x1699d791, 0xff99d791, 0x000000ff, 0x000000ff);
+ m_knot->setSize(11);
+ m_knot->setAnchor(SP_ANCHOR_CENTER);
+ m_knot->updateCtrl();
+ m_knot->hide();
+ m_knot->request_signal.connect(sigc::mem_fun(*this, &PagesTool::marginKnotMoved));
+ m_knot->ungrabbed_signal.connect(sigc::mem_fun(*this, &PagesTool::marginKnotFinished));
+ margin_knots.push_back(m_knot);
+
+ if (auto window = desktop->getCanvas()->get_window()) {
+ knot->setCursor(SP_KNOT_STATE_DRAGGING, this->get_cursor(window, "page-resizing.svg"));
+ knot->setCursor(SP_KNOT_STATE_MOUSEOVER, this->get_cursor(window, "page-resize.svg"));
+ m_knot->setCursor(SP_KNOT_STATE_DRAGGING, this->get_cursor(window, "page-resizing.svg"));
+ m_knot->setCursor(SP_KNOT_STATE_MOUSEOVER, this->get_cursor(window, "page-resize.svg"));
+ }
+ }
+ }
+
+ if (!visual_box) {
+ visual_box = make_canvasitem<CanvasItemRect>(desktop->getCanvasControls());
+ visual_box->set_stroke(0x0000ff7f);
+ visual_box->hide();
+ }
+ if (!drag_group) {
+ drag_group = make_canvasitem<CanvasItemGroup>(desktop->getCanvasTemp());
+ drag_group->set_name("CanvasItemGroup:PagesDragShapes");
+ }
+
+ _doc_replaced_connection = desktop->connectDocumentReplaced([=](SPDesktop *desktop, SPDocument *doc) {
+ connectDocument(desktop->getDocument());
+ });
+ connectDocument(desktop->getDocument());
+
+ _zoom_connection = desktop->signal_zoom_changed.connect([=](double) {
+ // This readjusts the knot on zoom because the viewbox position
+ // becomes detached on zoom, likely a precision problem.
+ if (!desktop->getDocument()->getPageManager().hasPages()) {
+ selectionChanged(desktop->getDocument(), nullptr);
+ }
+ });
+}
+
+
+PagesTool::~PagesTool()
+{
+ connectDocument(nullptr);
+
+ ungrabCanvasEvents();
+
+ _desktop->getSelection()->restoreBackup();
+
+ visual_box.reset();
+
+ for (auto knot : resize_knots) {
+ delete knot;
+ }
+ resize_knots.clear();
+
+ if (drag_group) {
+ drag_group.reset();
+ drag_shapes.clear(); // Already deleted by group
+ }
+
+ _doc_replaced_connection.disconnect();
+ _zoom_connection.disconnect();
+}
+
+void PagesTool::resizeKnotSet(Geom::Rect rect)
+{
+ for (int i = 0; i < resize_knots.size(); i++) {
+ resize_knots[i]->moveto(rect.corner(i));
+ resize_knots[i]->show();
+ }
+}
+
+void PagesTool::marginKnotSet(Geom::Rect margin_rect)
+{
+ for (int i = 0; i < margin_knots.size(); i++) {
+ margin_knots[i]->moveto(middleOfSide(i, margin_rect) * _desktop->doc2dt());
+ margin_knots[i]->show();
+ }
+}
+
+/*
+ * Get the middle of the side of the rectangle.
+ */
+Geom::Point PagesTool::middleOfSide(int side, const Geom::Rect &rect)
+{
+ return Geom::middle_point(rect.corner(side), rect.corner((side + 1) % 4));
+}
+
+void PagesTool::resizeKnotMoved(SPKnot *knot, Geom::Point const &ppointer, guint state)
+{
+ Geom::Rect rect; ///< Page rectangle in desktop coordinates.
+
+ auto page = _desktop->getDocument()->getPageManager().getSelected();
+ if (page) {
+ // Resizing a specific selected page
+ rect = page->getDesktopRect();
+ } else if (auto document = _desktop->getDocument()) {
+ // Resizing the naked viewBox
+ rect = *(document->preferredBounds()) * document->doc2dt();
+ }
+
+ int index;
+ for (index = 0; index < 4; index++) {
+ if (knot == resize_knots[index]) {
+ break;
+ }
+ }
+ Geom::Point start = rect.corner(index);
+ Geom::Point point = getSnappedResizePoint(knot->position(), state, start, page);
+
+ if (point != start) {
+ if (index % 3 == 0)
+ rect[Geom::X].setMin(point[Geom::X]);
+ else
+ rect[Geom::X].setMax(point[Geom::X]);
+
+ if (index < 2)
+ rect[Geom::Y].setMin(point[Geom::Y]);
+ else
+ rect[Geom::Y].setMax(point[Geom::Y]);
+
+ visual_box->show();
+ visual_box->set_rect(rect);
+ on_screen_rect = rect;
+ mouse_is_pressed = true;
+ }
+}
+
+/**
+ * Resize snapping allows knot and tool point snapping consistency.
+ */
+Geom::Point PagesTool::getSnappedResizePoint(Geom::Point point, guint state, Geom::Point origin, SPObject *target)
+{
+ if (!(state & GDK_SHIFT_MASK)) {
+ SnapManager &snap_manager = _desktop->namedview->snap_manager;
+ snap_manager.setup(_desktop, true, target);
+ Inkscape::SnapCandidatePoint scp(point, Inkscape::SNAPSOURCE_PAGE_CORNER);
+ scp.addOrigin(origin);
+ Inkscape::SnappedPoint sp = snap_manager.freeSnap(scp);
+ point = sp.getPoint();
+ snap_manager.unSetup();
+ }
+ return point;
+}
+
+void PagesTool::resizeKnotFinished(SPKnot *knot, guint state)
+{
+ auto document = _desktop->getDocument();
+ auto page = document->getPageManager().getSelected();
+ if (on_screen_rect) {
+ document->getPageManager().fitToRect(*on_screen_rect * document->dt2doc(), page);
+ Inkscape::DocumentUndo::done(document, "Resize page", INKSCAPE_ICON("tool-pages"));
+ on_screen_rect = {};
+ }
+ visual_box->hide();
+ mouse_is_pressed = false;
+}
+
+
+bool PagesTool::marginKnotMoved(SPKnot *knot, Geom::Point *ppointer, guint state)
+{
+ auto document = _desktop->getDocument();
+ auto &pm = document->getPageManager();
+
+ // Editing margins creates a page for the margin to be stored in.
+ pm.enablePages();
+
+ if (auto page = pm.getSelected()) {
+ Geom::Point point = *ppointer * document->dt2doc();
+
+ // Confine knot to edge
+ auto confine = Modifiers::Modifier::get(Modifiers::Type::TRANS_CONFINE)->active(state);
+ if (!Modifiers::Modifier::get(Modifiers::Type::MOVE_SNAPPING)->active(state)) {
+ point = getSnappedResizePoint(point, state, knot->drag_origin, page);
+ }
+
+ // Calculate what we're acting on, clamp it depending on the side.
+ int side = INDEX_OF(margin_knots, knot);
+ auto axis = (side & 1) ? Geom::X : Geom::Y;
+ auto delta = (point - page->getDocumentRect().corner(side))[axis];
+ auto value = std::max(0.0, (side + 1) & 2 ? -delta : delta);
+
+ // Set to page and back to to knot to inform confinement.
+ page->setMarginSide(side, value, confine);
+ knot->setPosition(middleOfSide(side, page->getDocumentMargin()) * document->doc2dt(), state);
+
+ Inkscape::DocumentUndo::maybeDone(document, "page-margin", ("Adjust page margin"), INKSCAPE_ICON("tool-pages"));
+ } else {
+ g_warning("Can't add margin, pages not enabled correctly!");
+ }
+ return true;
+}
+
+void PagesTool::marginKnotFinished(SPKnot *knot, guint state)
+{
+ // Margins are updated in real time.
+}
+
+bool PagesTool::root_handler(GdkEvent *event)
+{
+ bool ret = false;
+ auto &page_manager = _desktop->getDocument()->getPageManager();
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS: {
+ if (event->button.button == 1) {
+ mouse_is_pressed = true;
+ drag_origin_w = Geom::Point(event->button.x, event->button.y);
+ drag_origin_dt = _desktop->w2d(drag_origin_w);
+ ret = true;
+ if (auto page = pageUnder(drag_origin_dt, false)) {
+ // Select the clicked on page. Manager ignores the same-page.
+ _desktop->getDocument()->getPageManager().selectPage(page);
+ this->set_cursor("page-dragging.svg");
+ } else if (viewboxUnder(drag_origin_dt)) {
+ dragging_viewbox = true;
+ this->set_cursor("page-dragging.svg");
+ } else {
+ drag_origin_dt = getSnappedResizePoint(drag_origin_dt, event->button.state, Geom::Point(0, 0));
+ }
+ }
+ break;
+ }
+ case GDK_MOTION_NOTIFY: {
+
+ auto point_w = Geom::Point(event->motion.x, event->motion.y);
+ auto point_dt = _desktop->w2d(point_w);
+ bool snap = !(event->motion.state & GDK_SHIFT_MASK);
+
+ if (event->motion.state & GDK_BUTTON1_MASK) {
+ if (!mouse_is_pressed) {
+ // this sometimes happens if the mouse was off the edge when the event started
+ drag_origin_w = point_w;
+ drag_origin_dt = point_dt;
+ mouse_is_pressed = true;
+ }
+
+ if (dragging_item || dragging_viewbox) {
+ // Continue to drag item.
+ Geom::Affine tr = moveTo(point_dt, snap);
+ // XXX Moving the existing shapes would be much better, but it has
+ // a weird bug which stops it from working well.
+ // drag_group->update(tr * drag_group->get_parent()->get_affine());
+ addDragShapes(dragging_item, tr);
+ _desktop->getCanvas()->enable_autoscroll();
+ } else if (on_screen_rect) {
+ // Continue to drag new box
+ point_dt = getSnappedResizePoint(point_dt, event->motion.state, drag_origin_dt);
+ on_screen_rect = Geom::Rect(drag_origin_dt, point_dt);
+ } else if (Geom::distance(drag_origin_w, point_w) < drag_tolerance) {
+ // do not start dragging anything new if we're within tolerance from origin.
+ // pass
+ } else if (auto page = pageUnder(drag_origin_dt)) {
+ // Starting to drag page around the screen, the pageUnder must
+ // be the drag_origin as small movements can kill the UX feel.
+ dragging_item = page;
+ page_manager.selectPage(page);
+ addDragShapes(page, Geom::Affine());
+ grabPage(page);
+ } else if (viewboxUnder(drag_origin_dt)) {
+ // Special handling of viewbox dragging
+ dragging_viewbox = true;
+ } else {
+ // Start making a new page.
+ dragging_item = nullptr;
+ on_screen_rect = Geom::Rect(drag_origin_dt, drag_origin_dt);
+ this->set_cursor("page-draw.svg");
+ }
+ } else {
+ mouse_is_pressed = false;
+ drag_origin_dt = point_dt;
+ }
+ break;
+ }
+ case GDK_BUTTON_RELEASE: {
+ if (event->button.button != 1) {
+ break;
+ }
+ auto point_w = Geom::Point(event->button.x, event->button.y);
+ auto point_dt = _desktop->w2d(point_w);
+ bool snap = !(event->button.state & GDK_SHIFT_MASK);
+ auto document = _desktop->getDocument();
+
+ if (dragging_viewbox || dragging_item) {
+ if (dragging_viewbox || dragging_item->isViewportPage()) {
+ // Move the document's viewport first
+ auto page_items = page_manager.getOverlappingItems(_desktop, dragging_item);
+ auto rect = document->preferredBounds();
+ auto affine = moveTo(point_dt, snap);
+ document->fitToRect(*rect * affine * document->dt2doc(), false);
+ // Now move the page back to where we expect it.
+ if (dragging_item) {
+ dragging_item->movePage(affine, false);
+ dragging_item->setDesktopRect(*rect);
+ }
+ // We have a custom move object because item detection is fubar after fitToRect
+ if (page_manager.move_objects()) {
+ SPPage::moveItems(affine, page_items);
+ }
+ } else {
+ // Move the page object on the canvas.
+ dragging_item->movePage(moveTo(point_dt, snap), page_manager.move_objects());
+ }
+ Inkscape::DocumentUndo::done(_desktop->getDocument(), "Move page position", INKSCAPE_ICON("tool-pages"));
+ } else if (on_screen_rect) {
+ // conclude box here (make new page)
+ page_manager.selectPage(page_manager.newDesktopPage(*on_screen_rect));
+ Inkscape::DocumentUndo::done(_desktop->getDocument(), "Create new drawn page", INKSCAPE_ICON("tool-pages"));
+ }
+ mouse_is_pressed = false;
+ drag_origin_dt = point_dt;
+ ret = true;
+
+ // Clear snap indication on mouse up.
+ _desktop->snapindicator->remove_snaptarget();
+ break;
+ }
+ case GDK_KEY_PRESS: {
+ if (event->key.keyval == GDK_KEY_Escape) {
+ mouse_is_pressed = false;
+ ret = true;
+ }
+ if (event->key.keyval == GDK_KEY_Delete) {
+ page_manager.deletePage(page_manager.move_objects());
+
+ Inkscape::DocumentUndo::done(_desktop->getDocument(), "Delete Page", INKSCAPE_ICON("tool-pages"));
+ ret = true;
+ }
+ }
+ default:
+ break;
+ }
+
+ // Clean up any finished dragging, doesn't matter how it ends
+ if (!mouse_is_pressed && (dragging_item || on_screen_rect || dragging_viewbox)) {
+ dragging_viewbox = false;
+ dragging_item = nullptr;
+ on_screen_rect = {};
+ clearDragShapes();
+ visual_box->hide();
+ ret = true;
+ } else if (on_screen_rect) {
+ visual_box->show();
+ visual_box->set_rect(*on_screen_rect);
+ ret = true;
+ }
+ if (!mouse_is_pressed) {
+ if (pageUnder(drag_origin_dt) || viewboxUnder(drag_origin_dt)) {
+ // This page under uses the current mouse position (unlike the above)
+ this->set_cursor("page-mouseover.svg");
+ } else {
+ this->set_cursor("page-draw.svg");
+ }
+ }
+
+
+ return ret ? true : ToolBase::root_handler(event);
+}
+
+void PagesTool::menu_popup(GdkEvent *event, SPObject *obj)
+{
+ auto &page_manager = _desktop->getDocument()->getPageManager();
+ SPPage *page = page_manager.getSelected();
+ if (event->type != GDK_KEY_PRESS) {
+ drag_origin_w = Geom::Point(event->button.x, event->button.y);
+ drag_origin_dt = _desktop->w2d(drag_origin_w);
+ page = pageUnder(drag_origin_dt);
+ }
+ if (page) {
+ ToolBase::menu_popup(event, page);
+ }
+}
+
+/**
+ * Creates the right snapping setup for dragging items around.
+ */
+void PagesTool::grabPage(SPPage *target)
+{
+ _bbox_points.clear();
+ getBBoxPoints(target->getDesktopRect(), &_bbox_points, false, SNAPSOURCE_PAGE_CORNER, SNAPTARGET_UNDEFINED,
+ SNAPSOURCE_UNDEFINED, SNAPTARGET_UNDEFINED, SNAPSOURCE_PAGE_CENTER, SNAPTARGET_UNDEFINED);
+}
+
+/*
+ * Generate the movement affine as the page is dragged around (including snapping)
+ */
+Geom::Affine PagesTool::moveTo(Geom::Point xy, bool snap)
+{
+ Geom::Point dxy = xy - drag_origin_dt;
+
+ if (snap) {
+ SnapManager &snap_manager = _desktop->namedview->snap_manager;
+ snap_manager.setup(_desktop, true, dragging_item);
+ snap_manager.snapprefs.clearTargetMask(0); // Disable all snapping targets
+ snap_manager.snapprefs.setTargetMask(SNAPTARGET_ALIGNMENT_CATEGORY, -1);
+ snap_manager.snapprefs.setTargetMask(SNAPTARGET_ALIGNMENT_PAGE_EDGE_CORNER, -1);
+ snap_manager.snapprefs.setTargetMask(SNAPTARGET_ALIGNMENT_PAGE_EDGE_CENTER, -1);
+ snap_manager.snapprefs.setTargetMask(SNAPTARGET_PAGE_EDGE_CORNER, -1);
+ snap_manager.snapprefs.setTargetMask(SNAPTARGET_PAGE_EDGE_CENTER, -1);
+ snap_manager.snapprefs.setTargetMask(SNAPTARGET_GRID_INTERSECTION, -1);
+ snap_manager.snapprefs.setTargetMask(SNAPTARGET_GUIDE, -1);
+ snap_manager.snapprefs.setTargetMask(SNAPTARGET_GUIDE_INTERSECTION, -1);
+
+ Inkscape::PureTranslate *bb = new Inkscape::PureTranslate(dxy);
+ snap_manager.snapTransformed(_bbox_points, drag_origin_dt, (*bb));
+
+ if (bb->best_snapped_point.getSnapped()) {
+ dxy = bb->getTranslationSnapped();
+ _desktop->snapindicator->set_new_snaptarget(bb->best_snapped_point);
+ }
+
+ snap_manager.snapprefs.clearTargetMask(-1); // Reset preferences
+ snap_manager.unSetup();
+ }
+
+ return Geom::Translate(dxy);
+}
+
+/**
+ * Add all the shapes needed to see it being dragged.
+ */
+void PagesTool::addDragShapes(SPPage *page, Geom::Affine tr)
+{
+ clearDragShapes();
+ auto doc = _desktop->getDocument();
+
+ if (page) {
+ addDragShape(Geom::PathVector(Geom::Path(page->getDesktopRect())), tr);
+ } else {
+ auto doc_rect = doc->preferredBounds();
+ addDragShape(Geom::PathVector(Geom::Path(*doc_rect)), tr);
+ }
+ if (Inkscape::Preferences::get()->getBool("/tools/pages/move_objects", true)) {
+ for (auto &item : doc->getPageManager().getOverlappingItems(_desktop, page)) {
+ if (item && !item->isLocked()) {
+ addDragShape(item, tr);
+ }
+ }
+ }
+}
+
+/**
+ * Add an SPItem to the things being dragged.
+ */
+void PagesTool::addDragShape(SPItem *item, Geom::Affine tr)
+{
+ if (auto shape = item_to_outline(item)) {
+ addDragShape(*shape * item->i2dt_affine(), tr);
+ }
+}
+
+/**
+ * Add a shape to the set of dragging shapes, these are deleted when dragging stops.
+ */
+void PagesTool::addDragShape(Geom::PathVector &&pth, Geom::Affine tr)
+{
+ auto shape = new CanvasItemBpath(drag_group.get(), pth * tr, false);
+ shape->set_stroke(0x00ff007f);
+ shape->set_fill(0x00000000, SP_WIND_RULE_EVENODD);
+ drag_shapes.push_back(shape);
+}
+
+/**
+ * Remove all drag shapes from the canvas.
+ */
+void PagesTool::clearDragShapes()
+{
+ for (auto &shape : drag_shapes) {
+ shape->unlink();
+ }
+ drag_shapes.clear();
+}
+
+/**
+ * Find a page under the cursor point.
+ */
+SPPage *PagesTool::pageUnder(Geom::Point pt, bool retain_selected)
+{
+ auto &pm = _desktop->getDocument()->getPageManager();
+
+ // If the point is still on the selected, favour that one.
+ if (auto selected = pm.getSelected()) {
+ if (retain_selected && selected->getSensitiveRect().contains(pt)) {
+ return selected;
+ }
+ }
+ // This provides a simple way of selecting a page based on their layering
+ // Pages which are entirely contained within another are selected before
+ // their larger parents.
+ SPPage* ret = nullptr;
+ for (auto &page : pm.getPages()) {
+ auto rect = page->getSensitiveRect();
+ // If the point is inside the page boundry
+ if (rect.contains(pt)) {
+ // If we don't have a page yet, or the new page is inside the old one.
+ if (!ret || ret->getSensitiveRect().contains(rect)) {
+ ret = page;
+ }
+ }
+ }
+ return ret;
+}
+
+/**
+ * Returns true if the document contains no pages AND the point
+ * is within the document viewbox.
+ */
+bool PagesTool::viewboxUnder(Geom::Point pt)
+{
+ if (auto document = _desktop->getDocument()) {
+ auto rect = document->preferredBounds();
+ rect->expandBy(-0.1); // see sp-page getSensitiveRect
+ return !document->getPageManager().hasPages() && rect.contains(pt);
+ }
+ return true;
+}
+
+void PagesTool::connectDocument(SPDocument *doc)
+{
+ _selector_changed_connection.disconnect();
+ if (doc) {
+ auto &page_manager = doc->getPageManager();
+ _selector_changed_connection =
+ page_manager.connectPageSelected([=](SPPage *page) {
+ selectionChanged(doc, page);
+ });
+ selectionChanged(doc, page_manager.getSelected());
+ } else {
+ selectionChanged(doc, nullptr);
+ }
+}
+
+
+
+void PagesTool::selectionChanged(SPDocument *doc, SPPage *page)
+{
+ if (_page_modified_connection) {
+ _page_modified_connection.disconnect();
+ for (auto knot : resize_knots) {
+ knot->hide();
+ }
+ for (auto knot : margin_knots) {
+ knot->hide();
+ }
+ }
+
+ // Loop existing pages because highlight_item is unsafe.
+ // Use desktop's document instead of doc, which may be nullptr.
+ for (auto &possible : _desktop->getDocument()->getPageManager().getPages()) {
+ if (highlight_item == possible) {
+ highlight_item->setSelected(false);
+ }
+ }
+ highlight_item = page;
+ if (doc) {
+ if (page) {
+ _page_modified_connection = page->connectModified(sigc::mem_fun(*this, &PagesTool::pageModified));
+ page->setSelected(true);
+ pageModified(page, 0);
+ } else {
+ // This is for viewBox editng directly. A special extra feature
+ _page_modified_connection = doc->connectModified([=](guint){
+ resizeKnotSet(*(doc->preferredBounds()));
+ marginKnotSet(*(doc->preferredBounds()));
+ });
+ resizeKnotSet(*(doc->preferredBounds()));
+ marginKnotSet(*(doc->preferredBounds()));
+ }
+ }
+}
+
+
+void PagesTool::pageModified(SPObject *object, guint /*flags*/)
+{
+ if (auto page = cast<SPPage>(object)) {
+ resizeKnotSet(page->getDesktopRect());
+ marginKnotSet(page->getDocumentMargin());
+ }
+}
+
+} // namespace Tools
+} // 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/tools/pages-tool.h b/src/ui/tools/pages-tool.h
new file mode 100644
index 0000000..30887b1
--- /dev/null
+++ b/src/ui/tools/pages-tool.h
@@ -0,0 +1,99 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __UI_TOOLS_PAGES_CONTEXT_H__
+#define __UI_TOOLS_PAGES_CONTEXT_H__
+
+/*
+ * Page editing tool
+ *
+ * Authors:
+ * Martin Owens <doctormo@geek-2.com>
+ *
+ * Copyright (C) 2021 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/tools/tool-base.h"
+#include "2geom/rect.h"
+#include "display/control/canvas-item-ptr.h"
+
+#define SP_PAGES_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::PagesTool *>((Inkscape::UI::Tools::ToolBase *)obj))
+#define SP_IS_PAGES_CONTEXT(obj) \
+ (dynamic_cast<const Inkscape::UI::Tools::PagesTool *>((const Inkscape::UI::Tools::ToolBase *)obj) != NULL)
+
+class SPDocument;
+class SPObject;
+class SPPage;
+class SPKnot;
+class SnapManager;
+
+namespace Inkscape {
+class SnapCandidatePoint;
+class CanvasItemGroup;
+class CanvasItemRect;
+class CanvasItemBpath;
+
+namespace UI {
+namespace Tools {
+
+class PagesTool : public ToolBase
+{
+public:
+ PagesTool(SPDesktop *desktop);
+ ~PagesTool() override;
+
+ bool root_handler(GdkEvent *event) override;
+ void menu_popup(GdkEvent *event, SPObject *obj = nullptr) override;
+private:
+ void selectionChanged(SPDocument *doc, SPPage *page);
+ void connectDocument(SPDocument *doc);
+ SPPage *pageUnder(Geom::Point pt, bool retain_selected = true);
+ bool viewboxUnder(Geom::Point pt);
+ void addDragShapes(SPPage *page, Geom::Affine tr);
+ void addDragShape(SPItem *item, Geom::Affine tr);
+ void addDragShape(Geom::PathVector &&pth, Geom::Affine tr);
+ void clearDragShapes();
+
+ Geom::Point getSnappedResizePoint(Geom::Point point, guint state, Geom::Point origin, SPObject *target = nullptr);
+ void resizeKnotSet(Geom::Rect rect);
+ void resizeKnotMoved(SPKnot *knot, Geom::Point const &ppointer, guint state);
+ void resizeKnotFinished(SPKnot *knot, guint state);
+ void pageModified(SPObject *object, guint flags);
+
+ void marginKnotSet(Geom::Rect margin_rect);
+ bool marginKnotMoved(SPKnot *knot, Geom::Point *point, guint state);
+ void marginKnotFinished(SPKnot *knot, guint state);
+
+ void grabPage(SPPage *target);
+ Geom::Affine moveTo(Geom::Point xy, bool snap);
+
+ sigc::connection _selector_changed_connection;
+ sigc::connection _page_modified_connection;
+ sigc::connection _doc_replaced_connection;
+ sigc::connection _zoom_connection;
+
+ bool dragging_viewbox = false;
+ bool mouse_is_pressed = false;
+ Geom::Point drag_origin_w;
+ Geom::Point drag_origin_dt;
+ int drag_tolerance = 5;
+
+ std::vector<SPKnot *> resize_knots;
+ std::vector<SPKnot *> margin_knots;
+ SPKnot *grabbed_knot = nullptr;
+ SPPage *highlight_item = nullptr;
+ SPPage *dragging_item = nullptr;
+ std::optional<Geom::Rect> on_screen_rect; ///< On-screen rectangle, in desktop coordinates.
+ CanvasItemPtr<CanvasItemRect> visual_box;
+ CanvasItemPtr<CanvasItemGroup> drag_group;
+ std::vector<Inkscape::CanvasItemBpath *> drag_shapes;
+ std::vector<Inkscape::SnapCandidatePoint> _bbox_points;
+
+ static Geom::Point middleOfSide(int side, const Geom::Rect &rect);
+};
+
+} // namespace Tools
+} // namespace UI
+} // namespace Inkscape
+
+#endif
diff --git a/src/ui/tools/pen-tool.cpp b/src/ui/tools/pen-tool.cpp
new file mode 100644
index 0000000..2c280c6
--- /dev/null
+++ b/src/ui/tools/pen-tool.cpp
@@ -0,0 +1,2043 @@
+// 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 "inkscape-application.h" // Undo check
+#include "message-context.h"
+#include "message-stack.h"
+#include "selection-chemistry.h"
+#include "selection.h"
+
+#include "display/curve.h"
+#include "display/control/canvas-item-bpath.h"
+#include "display/control/canvas-item-ctrl.h"
+#include "display/control/canvas-item-curve.h"
+
+#include "object/sp-path.h"
+
+#include "ui/draw-anchor.h"
+#include "ui/shortcuts.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"
+
+// Given an optionally-present SPCurve, e.g. a smart/raw pointer or an optional,
+// return a copy of its pathvector if present, or a blank pathvector otherwise.
+template <typename T>
+static Geom::PathVector copy_pathvector_optional(T &p)
+{
+ if (p) {
+ return p->get_pathvector();
+ } else {
+ return {};
+ }
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+static Geom::Point pen_drag_origin_w(0, 0);
+static bool pen_within_tolerance = false;
+
+PenTool::PenTool(SPDesktop *desktop, std::string prefs_path, const std::string &cursor_filename)
+ : FreehandBase(desktop, prefs_path, cursor_filename)
+ , _undo{"doc.undo"}
+ , _redo{"doc.redo"}
+{
+ tablet_enabled = false;
+
+ // Pen indicators (temporary handles shown when adding a new node).
+ auto canvas = desktop->getCanvasControls();
+ for (int i = 0; i < 4; i++) {
+ ctrl[i] = make_canvasitem<CanvasItemCtrl>(canvas, ctrl_types[i]);
+ ctrl[i]->set_fill(0x0);
+ ctrl[i]->hide();
+ }
+
+ cl0 = make_canvasitem<CanvasItemCurve>(canvas);
+ cl1 = make_canvasitem<CanvasItemCurve>(canvas);
+ cl0->hide();
+ cl1->hide();
+
+ 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();
+ }
+
+ _desktop_destroy = _desktop->connectDestroy([=](SPDesktop *) { state = State::DEAD; });
+}
+
+PenTool::~PenTool() {
+ _desktop_destroy.disconnect();
+ this->discard_delayed_snap_event();
+
+ if (this->npoints != 0) {
+ // switching context - finish path
+ this->ea = nullptr; // unset end anchor if set (otherwise crashes)
+ if (state != State::DEAD) {
+ _finish(false);
+ }
+ }
+
+ for (auto &c : ctrl) {
+ c.reset();
+ }
+ cl0.reset();
+ cl1.reset();
+
+ 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();
+ }
+}
+
+
+void PenTool::_cancel() {
+ this->state = PenTool::STOP;
+ this->_resetColors();
+ for (auto &c : ctrl) {
+ c->hide();
+ }
+ cl0->hide();
+ cl1->hide();
+ this->message_context->clear();
+ this->message_context->flash(Inkscape::NORMAL_MESSAGE, _("Drawing cancelled"));
+ _redo_stack.clear();
+}
+
+/**
+ * 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) {
+ // 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 {
+ std::optional<Geom::Point> origin = std::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
+ std::optional<Geom::Point> origin = this->npoints > 0 ? this->p[0] : std::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) {
+ 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
+ std::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
+ // 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;
+ }
+
+ grabCanvasEvents();
+
+ 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
+
+ sa = anchor;
+ if (anchor) {
+ //Put the start overwrite curve always on the same direction
+ if (anchor->start) {
+ sa_overwrited = std::make_shared<SPCurve>(sa->curve->reversed());
+ } else {
+ sa_overwrited = std::make_shared<SPCurve>(*sa->curve);
+ }
+ _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() && is<SPPath>(selection->singleItem())) {
+ _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Appending to selected path"));
+ }
+
+ // Create green anchor
+ p = event_dt;
+ _endpointSnap(p, bevent.state);
+ green_anchor = std::make_unique<SPDrawAnchor>(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 && !_button1on) {
+ // right click - finish path, but only if the left click isn't pressed.
+ 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 (mevent.state & GDK_BUTTON2_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) {
+ Geom::Point const event_w(revent.x, revent.y);
+
+ // Find desktop coordinates
+ Geom::Point p = _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){
+ ctrl[1]->hide();
+ }
+ 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){
+ ctrl[1]->hide();
+ }
+ 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;
+ }
+
+ ungrabCanvasEvents();
+
+ 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 = _desktop->getSelection();
+
+ if (this->waiting_LPE) {
+ // we have an already created LPE waiting for a path
+ this->waiting_LPE->acceptParamPath(cast<SPPath>(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
+ this->green_bpaths.clear();
+
+ // one canvas bpath for all of green_curve
+ auto canvas_shape = new Inkscape::CanvasItemBpath(_desktop->getCanvasSketch(), copy_pathvector_optional(green_curve), true);
+ canvas_shape->set_stroke(green_color);
+ canvas_shape->set_fill(0x0, SP_WIND_RULE_NONZERO);
+ this->green_bpaths.emplace_back(canvas_shape);
+ }
+ if (this->green_anchor) {
+ this->green_anchor->ctrl->set_position(this->green_anchor->dp);
+ }
+
+ red_curve.reset();
+ red_curve.moveto(p[0]);
+ red_curve.curveto(p[1], p[2], p[3]);
+ red_bpath->set_bpath(&red_curve, true);
+
+ for (auto &c : ctrl) {
+ c->hide();
+ }
+ // handles
+ // hide the handlers in bspline and spiro modes
+ if (this->npoints == 5) {
+ ctrl[0]->set_position(p[0]);
+ ctrl[0]->show();
+ ctrl[3]->set_position(p[3]);
+ ctrl[3]->show();
+ }
+
+ if (this->p[0] != this->p[1] && !this->spiro && !this->bspline) {
+ ctrl[1]->set_position(p[1]);
+ ctrl[1]->show();
+ cl1->set_coords(p[0], p[1]);
+ cl1->show();
+ } else {
+ cl1->hide();
+ }
+
+ 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];
+ ctrl[2]->set_position(p2);
+ ctrl[2]->show();
+ cl0->set_coords(p2, p[0]);
+ cl0->show();
+ } else {
+ cl0->hide();
+ }
+ }
+
+ // 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 *= -_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 / _desktop->current_zoom(), y / _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();
+ }
+ auto previous = std::make_shared<SPCurve>();
+ previous->moveto(A);
+ previous->curveto(B, C, D);
+ if (green_curve->get_segment_count() == 1) {
+ green_curve = std::move(previous);
+ } else {
+ //we eliminate the last segment
+ green_curve->backspace();
+ //and we add it again with the recreation
+ green_curve->append_continuous(*previous);
+ }
+ }
+ //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);
+ auto previous = std::make_shared<SPCurve>();
+ if (auto const cubic = dynamic_cast<Geom::CubicBezier const *>(green_curve->last_segment())) {
+ A = green_curve->last_segment()->initialPoint();
+ B = (*cubic)[1];
+ C = *green_curve->last_point();
+ D = C;
+ } else {
+ //We obtain the last segment 4 points in the previous curve
+ A = green_curve->last_segment()->initialPoint();
+ B = A;
+ C = *green_curve->last_point();
+ D = C;
+ }
+ previous->moveto(A);
+ previous->curveto(B, C, D);
+ if (green_curve->get_segment_count() == 1){
+ green_curve = std::move(previous);
+ }else{
+ //we eliminate the last segment
+ green_curve->backspace();
+ //and we add it again with the recreation
+ green_curve->append_continuous(*previous);
+ }
+ }
+ // if the last node is an union with another curve
+ if (green_curve->is_unset() && sa && !sa->curve->is_unset()) {
+ _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/redo.
+ if (npoints > 0 && _undo.isTriggeredBy(&event->key)) {
+ return _undoLastPoint(true);
+ } else if (_redo.isTriggeredBy(&event->key)) {
+ return _redoLastPoint();
+ }
+
+ 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)) {
+ _desktop->getSelection()->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();
+ this->red_bpath->set_bpath(nullptr);
+
+ // Blue
+ blue_curve.reset();
+ blue_bpath->set_bpath(nullptr);
+
+ // Green
+ this->green_bpaths.clear();
+ this->green_curve->reset();
+ this->green_anchor.reset();
+
+ this->sa = nullptr;
+ this->ea = nullptr;
+
+ if (this->sa_overwrited) {
+ 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;
+ this->red_bpath->set_bpath(nullptr);
+}
+
+/**
+ * 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 = 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 = 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;
+ }
+ blue_bpath->hide();
+ }
+
+ //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
+ this->green_bpaths.clear();
+
+ // one canvas bpath for all of green_curve
+ auto canvas_shape = new Inkscape::CanvasItemBpath(_desktop->getCanvasSketch(), copy_pathvector_optional(green_curve), true);
+ canvas_shape->set_stroke(green_color);
+ canvas_shape->set_fill(0x0, SP_WIND_RULE_NONZERO);
+ green_bpaths.emplace_back(canvas_shape);
+ }
+
+ this->red_bpath->set_stroke(red_color);
+}
+
+
+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()){
+ 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]);
+ }
+}
+
+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 (is<SPLPEItem>(this->white_item) && cast<SPLPEItem>(this->white_item)->hasPathEffect()){
+ Inkscape::LivePathEffect::Effect *thisEffect =
+ cast<SPLPEItem>(this->white_item)->getFirstPathEffectOfType(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 (is<SPLPEItem>(this->white_item) && cast<SPLPEItem>(this->white_item)->hasPathEffect()){
+ Inkscape::LivePathEffect::Effect *thisEffect =
+ cast<SPLPEItem>(this->white_item)->getFirstPathEffectOfType(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());
+ auto last_segment = std::make_shared<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);
+ 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 = std::move(last_segment);
+ } else {
+ //we eliminate the last segment
+ this->sa_overwrited->backspace();
+ //and we add it again with the recreation
+ sa_overwrited->append_continuous(*last_segment);
+ }
+}
+
+void PenTool::_bsplineSpiroStartAnchorOff()
+{
+ Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*this->sa_overwrited->last_segment());
+ if(cubic){
+ auto last_segment = std::make_shared<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 = std::move(last_segment);
+ }else{
+ //we eliminate the last segment
+ this->sa_overwrited->backspace();
+ //and we add it again with the recreation
+ sa_overwrited->append_continuous(*last_segment);
+ }
+ }
+}
+
+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;
+ SPCurve tmp_curve;
+ this->p[2] = this->p[3] + (1./3)*(this->p[0] - this->p[3]);
+ if (this->green_curve->is_unset() && !this->sa) {
+ this->p[1] = this->p[0] + (1./3)*(this->p[3] - this->p[0]);
+ if (shift) {
+ this->p[2] = this->p[3];
+ }
+ } else if (!this->green_curve->is_unset()){
+ tmp_curve = *green_curve;
+ } else {
+ tmp_curve = *sa_overwrited;
+ }
+ 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;
+ previous_weight_power.moveto(tmp_curve.last_segment()->initialPoint());
+ previous_weight_power.lineto(this->p[0]);
+ auto SBasisweight_power = previous_weight_power.first_segment()->toSBasis();
+ 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 (sa && green_curve->is_unset()) {
+ sa_overwrited = std::make_shared<SPCurve>(tmp_curve);
+ }
+ green_curve = std::make_shared<SPCurve>(std::move(tmp_curve));
+ }
+ if (cubic) {
+ if (this->bspline) {
+ SPCurve weight_power;
+ weight_power.moveto(red_curve.last_segment()->initialPoint());
+ weight_power.lineto(*red_curve.last_point());
+ auto SBasisweight_power = weight_power.first_segment()->toSBasis();
+ this->p[1] = SBasisweight_power.valueAt(0.33334);
+ if (Geom::are_near(this->p[1],this->p[0])) {
+ 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 = *red_curve.last_point();
+ SPCurve red;
+ red.moveto(this->p[0]);
+ red.curveto(this->p[1],this->p[2],this->p[3]);
+ red_bpath->set_bpath(&red, true);
+ }
+
+ if(this->anchor_statusbar && !this->red_curve.is_unset()){
+ if(shift){
+ this->_bsplineSpiroEndAnchorOff();
+ }else{
+ this->_bsplineSpiroEndAnchorOn();
+ }
+ }
+
+ // remove old piecewise green canvasitems
+ green_bpaths.clear();
+
+ // one canvas bpath for all of green_curve
+ auto canvas_shape = new Inkscape::CanvasItemBpath(_desktop->getCanvasSketch(), copy_pathvector_optional(green_curve), true);
+ canvas_shape->set_stroke(green_color);
+ canvas_shape->set_fill(0x0, SP_WIND_RULE_NONZERO);
+ green_bpaths.emplace_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]);
+ SPCurve tmp_curve;
+ SPCurve last_segment;
+ Geom::Point point_c(0,0);
+ if( green_anchor && green_anchor->active ){
+ tmp_curve = green_curve->reversed();
+ if (green_curve->get_segment_count() == 0) {
+ return;
+ }
+ } else if(this->sa){
+ tmp_curve = sa_overwrited->reversed();
+ }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());
+ } 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(std::move(last_segment));
+ }
+ tmp_curve.reverse();
+ if (green_anchor && green_anchor->active) {
+ green_curve->reset();
+ green_curve = std::make_shared<SPCurve>(std::move(tmp_curve));
+ } else {
+ sa_overwrited->reset();
+ sa_overwrited = std::make_shared<SPCurve>(std::move(tmp_curve));
+ }
+}
+
+void PenTool::_bsplineSpiroEndAnchorOff()
+{
+ SPCurve tmp_curve;
+ SPCurve last_segment;
+ this->p[2] = this->p[3];
+ if (green_anchor && green_anchor->active) {
+ tmp_curve = green_curve->reversed();
+ if (green_curve->get_segment_count() == 0) {
+ return;
+ }
+ } else if (sa) {
+ tmp_curve = sa_overwrited->reversed();
+ } 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(std::move(last_segment));
+ }
+ tmp_curve.reverse();
+
+ if (green_anchor && green_anchor->active) {
+ green_curve->reset();
+ green_curve = std::make_shared<SPCurve>(std::move(tmp_curve));
+ } else {
+ sa_overwrited->reset();
+ sa_overwrited = std::make_shared<SPCurve>(std::move(tmp_curve));
+ }
+}
+
+//prepares the curves for its transformation into BSpline curve.
+void PenTool::_bsplineSpiroBuild()
+{
+ if (!spiro && !bspline){
+ return;
+ }
+
+ //We create the base curve
+ SPCurve curve;
+ //If we continuate the existing curve we add it at the start
+ if (sa && !sa->curve->is_unset()){
+ curve = *sa_overwrited;
+ }
+
+ if (!green_curve->is_unset()) {
+ curve.append_continuous(*green_curve);
+ }
+
+ //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]);
+ }
+ red_bpath->set_bpath(&red_curve, true);
+ curve.append_continuous(red_curve);
+ }
+ 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 (bspline) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Geom::PathVector hp;
+ bool uniform = false;
+ Glib::ustring pref_path = "/live_effects/bspline/uniform";
+ if (prefs->getEntry(pref_path).isValid()) {
+ uniform = prefs->getString(pref_path) == "true";
+ }
+ LivePathEffect::sp_bspline_do_effect(curve, 0, hp, uniform);
+ } else {
+ LivePathEffect::sp_spiro_do_effect(curve);
+ }
+
+ blue_bpath->set_bpath(&curve, true);
+ blue_bpath->set_stroke(blue_color);
+ blue_bpath->show();
+
+ blue_curve.reset();
+ //We hide the holders that doesn't contribute anything
+ for (auto &c : ctrl) {
+ c->hide();
+ }
+ if (spiro){
+ ctrl[1]->set_position(p[0]);
+ ctrl[1]->show();
+ }
+ cl0->hide();
+ cl1->hide();
+ } else {
+ //if the curve is empty
+ blue_bpath->hide();
+ }
+}
+
+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;
+ }
+ }
+
+ red_bpath->set_bpath(&red_curve, true);
+
+ if (statusbar) {
+ gchar *message;
+ if(this->spiro || this->bspline){
+ message = is_curve ?
+ _("<b>Curve segment</b>: angle %3.2f&#176;; <b>Shift+Click</b> creates cusp node, <b>ALT</b> moves previous, <b>Enter</b> or <b>Shift+Enter</b> to finish" ):
+ _("<b>Line segment</b>: angle %3.2f&#176;; <b>Shift+Click</b> creates cusp node, <b>ALT</b> moves 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 q, guint const state)
+{
+ // use 'q' as 'p' shadows member variable.
+ for (auto &c : ctrl) {
+ c->hide();
+ }
+
+ ctrl[1]->show();
+ cl1->show();
+
+ if ( this->npoints == 2 ) {
+ this->p[1] = q;
+ cl0->hide();
+ ctrl[1]->set_position(p[1]);
+ ctrl[1]->show();
+ cl1->set_coords(p[0], p[1]);
+ this->_setAngleDistanceStatusMessage(q, 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] = q;
+ cl0->show();
+ 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 = q - 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]);
+ red_bpath->set_bpath(&red_curve, true);
+ }
+ // Avoid conflicting with initial point ctrl
+ if (green_curve->get_segment_count() > 0) {
+ ctrl[0]->set_position(this->p[0]);
+ ctrl[0]->show();
+ }
+ ctrl[3]->set_position(this->p[3]);
+ ctrl[3]->show();
+ ctrl[2]->set_position(this->p[2]);
+ ctrl[2]->show();
+ ctrl[1]->set_position(this->p[4]);
+ ctrl[1]->show();
+
+ cl0->set_coords(this->p[3], this->p[2]);
+ cl1->set_coords(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(q, 3, message);
+ } else {
+ g_warning("Something bad happened - npoints is %d", this->npoints);
+ }
+}
+
+void PenTool::_finishSegment(Geom::Point const q, guint const state) { // use 'q' as 'p' shadows member variable.
+ if (this->polylines_paraxial) {
+ this->nextParaxialDirection(q, this->p[0], state);
+ }
+
+ 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]))
+ {
+ SPCurve lsegment;
+ 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());
+ green_curve->backspace();
+ green_curve->append_continuous(std::move(lsegment));
+ }
+ }
+ green_curve->append_continuous(red_curve);
+ auto curve = red_curve;
+
+ /// \todo fixme:
+ auto canvas_shape = new Inkscape::CanvasItemBpath(_desktop->getCanvasSketch(), curve.get_pathvector(), true);
+ canvas_shape->set_stroke(green_color);
+ canvas_shape->set_fill(0x0, SP_WIND_RULE_NONZERO);
+ green_bpaths.emplace_back(canvas_shape);
+
+ this->p[0] = this->p[3];
+ this->p[1] = this->p[4];
+ this->npoints = 2;
+
+ red_curve.reset();
+ _redo_stack.clear();
+ }
+}
+
+bool PenTool::_undoLastPoint(bool user_undo) {
+ bool ret = false;
+
+ if ( this->green_curve->is_unset() || (this->green_curve->last_segment() == nullptr) ) {
+ if (red_curve.is_unset()) {
+ return ret; // do nothing; this event should be handled upstream
+ }
+ _cancel();
+ ret = true;
+ } else {
+ red_curve.reset();
+ if (user_undo) {
+ if (_did_redo) {
+ _redo_stack.clear();
+ _did_redo = false;
+ }
+ _redo_stack.push_back(green_curve->get_pathvector());
+ }
+ // 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()) {
+ this->green_bpaths.pop_back();
+ }
+ this->green_curve->reset();
+ } else {
+ this->green_curve->backspace();
+ if (this->green_bpaths.size() > 1) {
+ this->green_bpaths.pop_back();
+ } else if (this->green_bpaths.size() == 1) {
+ green_bpaths.back()->set_bpath(green_curve.get(), 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];
+ ctrl[1]->set_position(this->p[0]);
+ } else {
+ this->p[1] = this->p[0];
+ }
+ }
+
+ for (auto &c : ctrl) {
+ c->hide();
+ }
+ cl0->hide();
+ cl1->hide();
+ 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;
+}
+
+/** Re-add the last undone point to the path being drawn */
+bool PenTool::_redoLastPoint()
+{
+ if (_redo_stack.empty()) {
+ return false;
+ }
+
+ auto old_green = std::move(_redo_stack.back());
+ _redo_stack.pop_back();
+ green_curve->set_pathvector(old_green);
+
+ if (auto const *last_seg = green_curve->last_segment()) {
+ Geom::Path freshly_added;
+ freshly_added.append(*last_seg);
+ green_bpaths.emplace_back(make_canvasitem<CanvasItemBpath>(_desktop->getCanvasSketch(), freshly_added, true));
+ }
+ green_bpaths.back()->set_stroke(green_color);
+ green_bpaths.back()->set_fill(0x0, SP_WIND_RULE_NONZERO);
+
+ auto const last_point = green_curve->last_point();
+ if (last_point) {
+ p[0] = p[1] = *last_point;
+ }
+ _setSubsequentPoint(p[3], true);
+ _bsplineSpiroBuild();
+
+ _did_redo = true;
+ return true;
+}
+
+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->_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;
+
+ for (auto &c : ctrl) {
+ c->hide();
+ }
+
+ cl0->hide();
+ cl1->hide();
+
+ this->green_anchor.reset();
+ _redo_stack.clear();
+ 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 = _desktop->namedview->snap_manager;
+
+ Inkscape::Selection *selection = _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(_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..a7053e8
--- /dev/null
+++ b/src/ui/tools/pen-tool.h
@@ -0,0 +1,175 @@
+// 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 <array>
+#include <sigc++/sigc++.h>
+
+#include "display/control/canvas-item-enums.h"
+#include "live_effects/effect.h"
+#include "ui/tools/freehand-base.h"
+#include "util/action-accel.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)
+
+namespace Inkscape {
+
+class CanvasItemCtrl;
+class CanvasItemCurve;
+
+namespace UI {
+namespace Tools {
+
+/**
+ * PenTool: a context for pen tool events.
+ */
+class PenTool : public FreehandBase {
+public:
+ PenTool(SPDesktop *desktop,
+ std::string prefs_path = "/tools/freehand/pen",
+ const std::string& cursor_filename = "pen.svg");
+ ~PenTool() override;
+
+ enum Mode {
+ MODE_CLICK,
+ MODE_DRAG
+ };
+
+ enum State {
+ POINT,
+ CONTROL,
+ CLOSE,
+ STOP,
+ DEAD
+ };
+
+ 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 = 0;
+
+ Mode mode = MODE_CLICK;
+ State state = POINT;
+ bool polylines_only = false;
+ bool polylines_paraxial = false;
+ Geom::Point paraxial_angle;
+
+ bool spiro = false; // Spiro mode active?
+ bool bspline = false; // BSpline mode active?
+
+ unsigned int expecting_clicks_for_LPE = 0; // if positive, finish the path after this many clicks
+ Inkscape::LivePathEffect::Effect *waiting_LPE = nullptr; // if NULL, waiting_LPE_type in SPDrawContext is taken into account
+ SPLPEItem *waiting_item = nullptr;
+
+ CanvasItemPtr<CanvasItemCtrl> ctrl[4]; // Origin, Start, Center, End point of path.
+ static constexpr std::array<CanvasItemCtrlType, 4> ctrl_types = {
+ CANVAS_ITEM_CTRL_TYPE_NODE_SMOOTH, CANVAS_ITEM_CTRL_TYPE_ROTATE,
+ CANVAS_ITEM_CTRL_TYPE_ROTATE, CANVAS_ITEM_CTRL_TYPE_NODE_SMOOTH};
+
+ CanvasItemPtr<CanvasItemCurve> cl0;
+ CanvasItemPtr<CanvasItemCurve> cl1;
+
+ bool events_disabled = false;
+
+ 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 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(bool user_undo = false);
+ bool _redoLastPoint();
+
+ 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);
+ void _endpointSnap(Geom::Point &p, guint const state);
+
+ void _cancel();
+
+ sigc::connection _desktop_destroy;
+ Util::ActionAccel _undo, _redo; ///< Keep track of Undo and Redo keybindings
+ // NOTE: undoing work in progress always deletes the last added point,
+ // so there's no need for an undo stack.
+ std::vector<Geom::PathVector> _redo_stack; ///< History of undone events
+ bool _did_redo = false;
+};
+
+}
+}
+}
+
+#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..568606d
--- /dev/null
+++ b/src/ui/tools/pencil-tool.cpp
@@ -0,0 +1,1177 @@
+// 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 <numeric> // For std::accumulate
+#include <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.h>
+
+#include <2geom/bezier-utils.h>
+#include <2geom/circle.h>
+#include <2geom/sbasis-to-bezier.h>
+#include <2geom/svg-path-parser.h>
+
+#include "pencil-tool.h"
+
+#include "context-fns.h"
+#include "desktop.h"
+#include "desktop-style.h"
+#include "layer-manager.h"
+#include "message-context.h"
+#include "message-stack.h"
+#include "selection-chemistry.h"
+#include "selection.h"
+#include "snap.h"
+
+#include "display/curve.h"
+#include "display/control/canvas-item-bpath.h"
+#include "display/control/snap-indicator.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 "path/path-boolop.h"
+#include "style.h"
+
+#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"
+
+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; }
+
+PencilTool::PencilTool(SPDesktop *desktop)
+ : FreehandBase(desktop, "/tools/freehand/pencil", "pencil.svg")
+ , p()
+ , _npoints(0)
+ , _state(SP_PENCIL_CONTEXT_IDLE)
+ , _req_tangent(0, 0)
+ , _is_drawing(false)
+ , sketch_n(0)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/tools/freehand/pencil/selcue")) {
+ this->enableSelectionCue();
+ }
+ this->_is_drawing = false;
+ this->anchor_statusbar = false;
+}
+
+PencilTool::~PencilTool() {
+}
+
+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
+ std::optional<Geom::Point> origin = this->_npoints > 0 ? this->p[0] : std::optional<Geom::Point>();
+ spdc_endpoint_snap_free(this, p, origin, state);
+ } else {
+ _desktop->snapindicator->remove_snaptarget();
+ }
+ }
+}
+
+/**
+ * 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) {
+ Inkscape::Selection *selection = _desktop->getSelection();
+
+ if (Inkscape::have_viable_layer(_desktop, defaultMessageContext()) == false) {
+ return true;
+ }
+
+ /* Grab mouse, so release will not pass unnoticed */
+ grabCanvasEvents();
+
+ Geom::Point const button_w(bevent.x, bevent.y);
+
+ /* Find desktop coordinates */
+ Geom::Point p = _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) {
+ sa_overwrited = std::make_shared<SPCurve>(anchor->curve->reversed());
+ } else {
+ sa_overwrited = std::make_shared<SPCurve>(*anchor->curve);
+ }
+ _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() && is<SPPath>(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 ((mevent.state & GDK_BUTTON2_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->_is_drawing) {
+ /* Grab mouse, so release will not pass unnoticed */
+ grabCanvasEvents();
+ }
+
+ /* 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) {
+ this->discard_delayed_snap_event();
+ }
+ this->_state = SP_PENCIL_CONTEXT_FREEHAND;
+
+ if ( !sa && !green_anchor ) {
+ /* Create green anchor */
+ green_anchor = std::make_unique<SPDrawAnchor>(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->_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)) {
+ if (is<SPLPEItem>(item)) {
+ Effect* lpe = cast<SPLPEItem>(item)->getCurrentLPE();
+ if (lpe) {
+ LPEPowerStroke* ps = static_cast<LPEPowerStroke*>(lpe);
+ if (ps) {
+ _desktop->getSelection()->clear();
+ _desktop->getSelection()->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;
+ this->discard_delayed_snap_event();
+ 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();
+
+ this->green_anchor.reset();
+
+ 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) {
+ _addFreehandPoint(p_end, revent.state, true);
+ _pressure_curve.reset();
+ } else {
+ _endpointSnap(p_end, revent.state);
+ if (p_end != p) {
+ // then we must have snapped!
+ _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();
+ this->green_anchor.reset();
+ this->_state = SP_PENCIL_CONTEXT_IDLE;
+ // reset sketch mode too
+ this->sketch_n = 0;
+ }
+ break;
+ case SP_PENCIL_CONTEXT_SKETCH:
+ default:
+ break;
+ }
+
+ ungrabCanvasEvents();
+
+ ret = true;
+ }
+ return ret;
+}
+
+void PencilTool::_cancel() {
+ ungrabCanvasEvents();
+
+ this->_is_drawing = false;
+ this->_state = SP_PENCIL_CONTEXT_IDLE;
+ this->discard_delayed_snap_event();
+
+ this->red_curve.reset();
+ this->red_bpath->set_bpath(&red_curve);
+
+ this->green_bpaths.clear();
+ this->green_curve->reset();
+ this->green_anchor.reset();
+
+ this->message_context->clear();
+ this->message_context->flash(Inkscape::NORMAL_MESSAGE, _("Drawing cancelled"));
+}
+
+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)) {
+ _desktop->getSelection()->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) {
+ _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;
+ this->green_anchor.reset();
+ this->_state = SP_PENCIL_CONTEXT_IDLE;
+ this->discard_delayed_snap_event();
+ _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) {
+ red_bpath->set_bpath(&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) {
+ red_bpath->set_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()
+{
+ {
+ SPDocument *document = _desktop->doc();
+ 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(_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;
+ 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]);
+ }
+ }
+ curvepressure.transform(currentLayer()->i2dt_affine().inverse());
+ 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");
+ pp->setAttribute("d", sp_svg_write_path(path));
+ pp->setAttribute("id", "power_stroke_preview");
+ Inkscape::GC::release(pp);
+
+ auto powerpreview = cast<SPShape>(currentLayer()->appendChildRepr(pp));
+ auto lpeitem = powerpreview;
+ if (!lpeitem) {
+ return;
+ }
+ DocumentUndo::ScopedInsensitive tmp(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, document, 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);
+ SPCurve const *curvepressure = powerpreview->curve();
+ 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, document, 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");
+ pspreview->getRepr()->setAttribute("not_jump", "true");
+ 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;");
+ }
+ 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 / _desktop->current_zoom();
+ double const pressure_shrunk = pressure * (max - min) + min; // C++20 -> use std::lerp()
+ double pressure_computed = std::abs(pressure_shrunk * dezoomify_factor);
+ double pressure_computed_scaled = std::abs(pressure_computed * _desktop->getDocument()->getDocumentScale().inverse()[Geom::X]);
+ if (p != this->p[this->_npoints - 1]) {
+ this->_wps.emplace_back(distance, pressure_computed_scaled);
+ }
+ 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 = _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);
+ }
+ _pressure_curve = SPCurve(std::move(pressure_path));
+ red_bpath->set_bpath(&_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 / _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];
+ }
+ }
+ if (points.empty() && !_wps.empty()) {
+ // Synthesize a pressure data point based on the average pressure
+ double average_pressure = std::accumulate(_wps.begin(), _wps.end(), 0.0,
+ [](double const &sum_so_far, Geom::Point const &point) -> double {
+ return sum_so_far + point[Geom::Y];
+ }) / (double)_wps.size();
+ points.emplace_back(0.5 * path.size(), /* place halfway along the path */
+ 2.0 * average_pressure /* 2.0 - for correct average thickness of a kite */);
+ }
+}
+
+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, 0.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 = 0.02 * square(_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]);
+ Geom::Point point_at2 = b[4 * c + 3] + (1./3) * (b[4 * c + 0] - b[4 * c + 3]);
+ 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 {
+ std::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) {
+ red_bpath->set_bpath(green_curve.get());
+ }
+
+ /* 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(_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();
+ } 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) {
+ red_bpath->set_bpath(green_curve.get());
+ }
+ /* 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]);
+ Geom::Point point_at2 = b[3] + (1./3)*(b[0] - b[3]);
+ this->red_curve.curveto(point_at1,point_at2,b[3]);
+ }else{
+ this->red_curve.curveto(b[1], b[2], b[3]);
+ }
+ if (!tablet_enabled) {
+ red_bpath->set_bpath(&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) );
+ }
+
+ green_curve->append_continuous(red_curve);
+
+ /// \todo fixme:
+
+ auto layer = _desktop->layerManager().currentLayer();
+ this->highlight_color = layer->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;
+ }
+
+ auto cshape = new Inkscape::CanvasItemBpath(_desktop->getCanvasSketch(), red_curve.get_pathvector(), true);
+ cshape->set_stroke(green_color);
+ cshape->set_fill(0x0, SP_WIND_RULE_NONZERO);
+
+ this->green_bpaths.emplace_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..b1e0b2c
--- /dev/null
+++ b/src/ui/tools/pencil-tool.h
@@ -0,0 +1,101 @@
+// 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>
+
+#include <memory>
+
+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(SPDesktop *desktop);
+ ~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
+
+protected:
+ 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 _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..a7b0e5a
--- /dev/null
+++ b/src/ui/tools/rect-tool.cpp
@@ -0,0 +1,464 @@
+// 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 "object/sp-rect.h"
+#include "object/sp-namedview.h"
+
+#include "ui/icon-names.h"
+#include "ui/shape-editor.h"
+#include "ui/tools/rect-tool.h"
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+RectTool::RectTool(SPDesktop *desktop)
+ : ToolBase(desktop, "/tools/shapes/rect", "rect.svg")
+ , rect(nullptr)
+ , rx(0)
+ , ry(0)
+{
+ this->shape_editor = new ShapeEditor(desktop);
+
+ SPItem *item = desktop->getSelection()->singleItem();
+ if (item) {
+ this->shape_editor->set_item(item);
+ }
+
+ this->sel_changed_connection.disconnect();
+ this->sel_changed_connection = 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();
+ }
+}
+
+RectTool::~RectTool() {
+ ungrabCanvasEvents();
+
+ this->finishItem();
+ 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::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->setup_for_drag_start(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;
+
+ 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) {
+ 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;
+
+ grabCanvasEvents();
+ ret = TRUE;
+ }
+ break;
+ case GDK_MOTION_NOTIFY:
+ if ( dragging
+ && (event->motion.state & GDK_BUTTON1_MASK))
+ {
+ 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) {
+ dragging = false;
+ this->discard_delayed_snap_event();
+
+ if (rect) {
+ // 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 if (!selection->includes(this->item_to_select)) {
+ selection->set(this->item_to_select);
+ }
+ } else {
+ // click in an empty space
+ selection->clear();
+ }
+
+ this->item_to_select = nullptr;
+ ret = TRUE;
+ ungrabCanvasEvents();
+ }
+ 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->getSelection()->toGuides();
+ ret = true;
+ }
+ break;
+
+ case GDK_KEY_Escape:
+ if (dragging) {
+ dragging = false;
+ this->discard_delayed_snap_event();
+ // if drawing, cancel, otherwise pass it up for deselecting
+ this->cancel();
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_space:
+ if (dragging) {
+ ungrabCanvasEvents();
+ dragging = false;
+ this->discard_delayed_snap_event();
+
+ 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) {
+ if (!this->rect) {
+ 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:rect");
+
+ // Set style
+ sp_desktop_apply_style_tool(_desktop, repr, "/tools/shapes/rect", false);
+
+ this->rect = cast<SPRect>(currentLayer()->appendChildRepr(repr));
+ Inkscape::GC::release(repr);
+
+ this->rect->transform = currentLayer()->i2doc_affine().inverse();
+ this->rect->updateRepr();
+ }
+
+ 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);
+
+ _desktop->getSelection()->set(this->rect);
+
+ DocumentUndo::done(_desktop->getDocument(), _("Create rectangle"), INKSCAPE_ICON("draw-rectangle"));
+
+ this->rect = nullptr;
+ }
+}
+
+void RectTool::cancel(){
+ _desktop->getSelection()->clear();
+ ungrabCanvasEvents();
+
+ 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;
+
+ 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/rect-tool.h b/src/ui/tools/rect-tool.h
new file mode 100644
index 0000000..79d1a8a
--- /dev/null
+++ b/src/ui/tools/rect-tool.h
@@ -0,0 +1,60 @@
+// 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 {
+
+class Selection;
+
+namespace UI {
+namespace Tools {
+
+class RectTool : public ToolBase {
+public:
+ RectTool(SPDesktop *desktop);
+ ~RectTool() override;
+
+ void set(const Inkscape::Preferences::Entry& val) override;
+ bool root_handler(GdkEvent* event) override;
+ bool item_handler(SPItem* item, GdkEvent* event) 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..6137c94
--- /dev/null
+++ b/src/ui/tools/select-tool.cpp
@@ -0,0 +1,1148 @@
+// 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 "layer-manager.h"
+#include "message-stack.h"
+#include "rubberband.h"
+#include "selection-chemistry.h"
+#include "selection-describer.h"
+#include "selection.h"
+#include "seltrans.h"
+
+#include "actions/actions-tools.h" // set_active_tool()
+
+#include "display/drawing-item.h"
+#include "display/control/canvas-item-catchall.h"
+#include "display/control/canvas-item-drawing.h"
+#include "display/control/snap-indicator.h"
+
+#include "object/box3d.h"
+#include "style.h"
+
+#include "ui/modifiers.h"
+#include "ui/tools/select-tool.h"
+#include "ui/widget/canvas.h"
+
+using Inkscape::DocumentUndo;
+using Inkscape::Modifiers::Modifier;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+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
+static bool is_cycling = false;
+
+SelectTool::SelectTool(SPDesktop *desktop)
+ : ToolBase(desktop, "/tools/select", "select.svg")
+ , dragging(false)
+ , _force_dragging(false)
+ , _alt_on(false)
+ , moved(false)
+ , button_press_state(0)
+ , cycling_wrap(true)
+ , item(nullptr)
+ , _seltrans(nullptr)
+ , _describer(nullptr)
+{
+ auto select_click = Modifier::get(Modifiers::Type::SELECT_ADD_TO)->get_label();
+ auto select_scroll = Modifier::get(Modifiers::Type::SELECT_CYCLE)->get_label();
+
+ // cursors in select context
+ _default_cursor = "select.svg";
+
+ no_selection_msg = g_strdup_printf(
+ _("No objects selected. Click, %s+click, %s+scroll mouse on top of objects, or drag around objects to select."),
+ select_click.c_str(), select_scroll.c_str());
+
+ this->_describer = new Inkscape::SelectionDescriber(
+ desktop->getSelection(),
+ desktop->messageStack(),
+ _("Click selection again to toggle scale/rotation handles"),
+ no_selection_msg);
+
+ 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();
+ }
+}
+
+SelectTool::~SelectTool()
+{
+ this->enableGrDrag(false);
+
+ if (grabbed) {
+ grabbed->ungrab();
+ grabbed = nullptr;
+ }
+
+ delete this->_seltrans;
+ this->_seltrans = nullptr;
+
+ delete this->_describer;
+ this->_describer = nullptr;
+ g_free(no_selection_msg);
+
+ if (item) {
+ sp_object_unref(item);
+ item = nullptr;
+ }
+}
+
+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;
+ this->discard_delayed_snap_event();
+ 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);
+ }
+ this->item = nullptr;
+
+ _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;
+ defaultMessageContext()->clear();
+ _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.
+ */
+ if (SPObject *const current_layer = desktop->layerManager().currentLayer()) {
+ SPObject *const parent = current_layer->parent;
+ auto current_group = cast<SPGroup>(current_layer);
+ if ( parent
+ && ( parent->parent
+ || !( current_group
+ && ( SPGroup::LAYER == current_group->layerMode() ) ) ) )
+ {
+ desktop->layerManager().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) {
+ /* 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_state = event->button.state;
+ bool first_hit = Modifier::get(Modifiers::Type::SELECT_FIRST_HIT)->active(this->button_press_state);
+ bool force_drag = Modifier::get(Modifiers::Type::SELECT_FORCE_DRAG)->active(this->button_press_state);
+ bool always_box = Modifier::get(Modifiers::Type::SELECT_ALWAYS_BOX)->active(this->button_press_state);
+ bool touch_path = Modifier::get(Modifiers::Type::SELECT_TOUCH_PATH)->active(this->button_press_state);
+
+ // 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
+ if (!(always_box || first_hit || touch_path)) {
+
+ this->dragging = TRUE;
+ this->moved = FALSE;
+
+ this->set_cursor("select-dragging.svg");
+
+ // 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), force_drag, FALSE);
+ sp_object_ref(this->item, nullptr);
+
+ rb_escaped = drag_escaped = 0;
+
+ if (grabbed) {
+ grabbed->ungrab();
+ grabbed = nullptr;
+ }
+
+ grabbed = _desktop->getCanvasDrawing();
+ grabbed->grab(Gdk::KEY_PRESS_MASK |
+ Gdk::KEY_RELEASE_MASK |
+ Gdk::BUTTON_PRESS_MASK |
+ Gdk::BUTTON_RELEASE_MASK |
+ Gdk::POINTER_MOTION_MASK );
+
+ 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 (!dragging && !_alt_on && !_desktop->isWaitingCursor()) {
+ this->set_cursor("select-mouseover.svg");
+ }
+ break;
+ }
+ case GDK_LEAVE_NOTIFY:
+ if (!dragging && !_force_dragging && !_desktop->isWaitingCursor()) {
+ this->set_cursor("select.svg");
+ }
+ 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);
+ } else {
+ sp_selection_item_next(_desktop);
+ }
+ ret = TRUE;
+ } else if (get_latin_keyval (&event->key) == GDK_KEY_ISO_Left_Tab) {
+ if (this->dragging && this->grabbed) {
+ _seltrans->getNextClosestPoint(true);
+ } else {
+ sp_selection_item_prev(_desktop);
+ }
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_BUTTON_RELEASE:
+ case GDK_KEY_RELEASE:
+ if (_alt_on) {
+ _default_cursor = "select-mouseover.svg";
+ }
+ 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) {
+ 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 (Modifier::get(Modifiers::Type::SELECT_ADD_TO)->active(scroll_event->state)) {
+ 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();
+ }
+
+ switch (event->type) {
+ case GDK_2BUTTON_PRESS:
+ if (event->button.button == 1) {
+ if (!selection->isEmpty()) {
+ SPItem *clicked_item = selection->items().front();
+
+ if (is<SPGroup>(clicked_item) && !is<SPBox3D>(clicked_item)) { // enter group if it's not a 3D box
+ _desktop->layerManager().setCurrentLayer(clicked_item);
+ _desktop->getSelection()->clear();
+ this->dragging = false;
+ this->discard_delayed_snap_event();
+
+ } else { // switch tool
+ Geom::Point const button_pt(event->button.x, event->button.y);
+ Geom::Point const p(_desktop->w2d(button_pt));
+ set_active_tool(_desktop, clicked_item, p);
+ }
+ } else {
+ sp_select_context_up_one_layer(_desktop);
+ }
+
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 1) {
+ // 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(Modifier::get(Modifiers::Type::SELECT_TOUCH_PATH)->active(event->button.state)) {
+ Inkscape::Rubberband::get(_desktop)->setMode(RUBBERBAND_MODE_TOUCHPATH);
+ } else {
+ Inkscape::Rubberband::get(_desktop)->defaultMode();
+ }
+
+ Inkscape::Rubberband::get(_desktop)->start(_desktop, p);
+
+ if (this->grabbed) {
+ grabbed->ungrab();
+ this->grabbed = nullptr;
+ }
+
+ grabbed = _desktop->getCanvasCatchall();
+ grabbed->grab(Gdk::KEY_PRESS_MASK |
+ Gdk::KEY_RELEASE_MASK |
+ Gdk::BUTTON_PRESS_MASK |
+ Gdk::BUTTON_RELEASE_MASK |
+ Gdk::POINTER_MOTION_MASK );
+
+ // remember what modifiers were on before button press
+ this->button_press_state = event->button.state;
+
+ 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:
+ {
+ if (this->grabbed && event->button.state & (GDK_SHIFT_MASK | GDK_MOD1_MASK)) {
+ _desktop->snapindicator->remove_snaptarget();
+ }
+
+ tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+
+ bool first_hit = Modifier::get(Modifiers::Type::SELECT_FIRST_HIT)->active(this->button_press_state);
+ bool force_drag = Modifier::get(Modifiers::Type::SELECT_FORCE_DRAG)->active(this->button_press_state);
+ bool always_box = Modifier::get(Modifiers::Type::SELECT_ALWAYS_BOX)->active(this->button_press_state);
+
+ if ((event->motion.state & GDK_BUTTON1_MASK)) {
+ 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 (first_hit || (force_drag && !always_box && !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;
+ this->set_cursor("select-dragging.svg");
+ }
+
+ 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();
+
+ // Look for an item where the mouse was reported to be by mouse press (not mouse move).
+ item_at_point = _desktop->getItemAtPoint(Geom::Point(xp, yp), FALSE);
+
+ if (item_at_point || this->moved || force_drag) {
+ // 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));
+
+ {
+ auto selGroup = 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)) && !force_drag) {
+ // 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()) {
+ // this->discard_delayed_snap_event();
+ _seltrans->moveTo(p, event->button.state);
+ }
+
+ _desktop->getCanvas()->enable_autoscroll();
+ gobble_motion_events(GDK_BUTTON1_MASK);
+ ret = TRUE;
+ } else {
+ this->dragging = FALSE;
+ this->discard_delayed_snap_event();
+ }
+
+ } else {
+ if (Inkscape::Rubberband::get(_desktop)->is_started()) {
+ Inkscape::Rubberband::get(_desktop)->move(p);
+
+ auto touch_path = Modifier::get(Modifiers::Type::SELECT_TOUCH_PATH)->get_label();
+ auto mode = Inkscape::Rubberband::get(_desktop)->getMode();
+ if (mode == RUBBERBAND_MODE_TOUCHPATH) {
+ this->defaultMessageContext()->setF(Inkscape::NORMAL_MESSAGE,
+ _("<b>Draw over</b> objects to select them; release <b>%s</b> to switch to rubberband selection"), touch_path.c_str());
+ } else if (mode == RUBBERBAND_MODE_TOUCHRECT) {
+ this->defaultMessageContext()->setF(Inkscape::NORMAL_MESSAGE,
+ _("<b>Drag near</b> objects to select them; press <b>%s</b> to switch to touch selection"), touch_path.c_str());
+ } else {
+ this->defaultMessageContext()->setF(Inkscape::NORMAL_MESSAGE,
+ _("<b>Drag around</b> objects to select them; press <b>%s</b> to switch to touch selection"), touch_path.c_str());
+ }
+
+ gobble_motion_events(GDK_BUTTON1_MASK);
+ }
+ }
+ }
+ break;
+ }
+ case GDK_BUTTON_RELEASE:
+ xp = yp = 0;
+
+ if ((event->button.button == 1) && (this->grabbed)) {
+ if (this->dragging) {
+ if (this->moved) {
+ // item has been moved
+ _seltrans->ungrab();
+ this->moved = FALSE;
+ } else if (this->item && !drag_escaped) {
+ // item has not been moved -> simply a click, do selecting
+ if (!selection->isEmpty()) {
+ if(Modifier::get(Modifiers::Type::SELECT_ADD_TO)->active(event->button.state)) {
+ // with shift, toggle selection
+ _seltrans->resetState();
+ selection->toggle(this->item);
+ } else {
+ SPObject* single = selection->single();
+ auto singleGroup = 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;
+
+ if (!_alt_on) {
+ if (_force_dragging) {
+ this->set_cursor(_default_cursor);
+ _force_dragging = false;
+ } else {
+ this->set_cursor("select-mouseover.svg");
+ }
+ }
+
+ this->discard_delayed_snap_event();
+
+ 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_TOUCHRECT) {
+ Geom::OptRect const b = r->getRectangle();
+ items = _desktop->getDocument()->getItemsPartiallyInBox(_desktop->dkey, (*b) * _desktop->dt2doc());
+ } else if (r->getMode() == RUBBERBAND_MODE_TOUCHPATH) {
+ bool topmost_items_only = prefs->getBool("/options/selection/touchsel_topmost_only");
+ items = _desktop->getDocument()->getItemsAtPoints(_desktop->dkey, r->getPoints(), true, topmost_items_only);
+ }
+
+ _seltrans->resetState();
+ r->stop();
+ this->defaultMessageContext()->clear();
+
+ if(Modifier::get(Modifiers::Type::SELECT_ADD_TO)->active(event->button.state)) {
+ // 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();
+
+ bool add_to = Modifier::get(Modifiers::Type::SELECT_ADD_TO)->active(event->button.state);
+ bool in_groups = Modifier::get(Modifiers::Type::SELECT_IN_GROUPS)->active(event->button.state);
+ bool force_drag = Modifier::get(Modifiers::Type::SELECT_FORCE_DRAG)->active(event->button.state);
+
+ if (add_to && !rb_escaped && !drag_escaped) {
+ // this was a shift+click or alt+shift+click, select what was clicked upon
+
+ if (in_groups) {
+ // go into groups, honoring force_drag (Alt)
+ item = sp_event_context_find_item (_desktop,
+ Geom::Point(event->button.x, event->button.y), force_drag, TRUE);
+ } else {
+ // don't go into groups, honoring Alt
+ item = sp_event_context_find_item (_desktop,
+ Geom::Point(event->button.x, event->button.y), force_drag, FALSE);
+ }
+
+ if (item) {
+ selection->toggle(item);
+ item = nullptr;
+ }
+
+ } else if ((in_groups || force_drag) && !rb_escaped && !drag_escaped) { // ctrl+click, alt+click
+ item = sp_event_context_find_item (_desktop,
+ Geom::Point(event->button.x, event->button.y), force_drag, in_groups);
+
+ 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) && !force_drag) {
+ selection->clear();
+ }
+
+ rb_escaped = 0;
+ }
+ }
+ }
+
+ ret = TRUE;
+ }
+ if (grabbed) {
+ grabbed->ungrab();
+ grabbed = nullptr;
+ }
+ // Think is not necessary now
+ // _desktop->updateNow();
+ }
+
+ if (event->button.button == 1) {
+ Inkscape::Rubberband::get(_desktop)->stop(); // might have been started in another tool!
+ }
+
+ this->button_press_state = 0;
+ break;
+
+ case GDK_SCROLL: {
+
+ GdkEventScroll *scroll_event = (GdkEventScroll*) event;
+
+ // do nothing specific if alt was not pressed
+ if ( ! Modifier::get(Modifiers::Type::SELECT_CYCLE)->active(scroll_event->state))
+ break;
+
+ 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 (item && 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);
+
+ ret = TRUE;
+
+ GtkWindow *w = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(_desktop->getCanvas()->gobj())));
+ if (w) {
+ gtk_window_present(w);
+ _desktop->getCanvas()->grab_focus();
+ }
+ 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 (alt) {
+ _alt_on = true;
+ }
+
+ 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 (Modifier::get(Modifiers::Type::SELECT_TOUCH_PATH)->active(event->key.state | keyval)) {
+ 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 {
+ Modifiers::responsive_tooltip(this->defaultMessageContext(), event, 6,
+ Modifiers::Type::SELECT_IN_GROUPS, Modifiers::Type::MOVE_CONFINE,
+ Modifiers::Type::SELECT_ADD_TO, Modifiers::Type::SELECT_TOUCH_PATH,
+ Modifiers::Type::SELECT_CYCLE, Modifiers::Type::SELECT_FORCE_DRAG);
+
+ // if Alt and nonempty selection, show moving cursor ("move selected"):
+ if (alt && !selection->isEmpty() && !_desktop->isWaitingCursor()) {
+ this->set_cursor("select-dragging.svg");
+ _force_dragging = true;
+ _default_cursor = "select.svg";
+ }
+ //*/
+ break;
+ }
+ }
+
+ gdouble const nudge = prefs->getDoubleLimited("/options/nudgedistance/value", 2, 0, 1000, "px"); // in px
+ int const snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12);
+ auto const y_dir = _desktop->yaxisdir();
+
+ switch (keyval) {
+ case GDK_KEY_Left: // move selection left
+ case GDK_KEY_KP_Left:
+ if (!MOD__CTRL(event)) { // not ctrl
+ gint mul = 1 + gobble_key_events(keyval, 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(keyval, 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(keyval, 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(keyval, 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:
+ case GDK_KEY_c:
+ case GDK_KEY_C:
+ /* stamping mode: show outline mode moving */
+ if (this->dragging && this->grabbed) {
+ _seltrans->stamp(keyval != GDK_KEY_space);
+ 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(keyval, 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(keyval, 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();
+ auto clickedGroup = cast<SPGroup>(clicked_item);
+ if ( (clickedGroup && (clickedGroup->layerMode() != SPGroup::LAYER)) || is<SPBox3D>(clicked_item)) { // enter group or a 3D box
+ _desktop->layerManager().setCurrentLayer(clicked_item);
+ _desktop->getSelection()->clear();
+ } else {
+ _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->getSelection()->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 (alt) {
+ _alt_on = false;
+ }
+
+ if (Inkscape::Rubberband::get(_desktop)->is_started()) {
+ // if Alt then change cursor to moving cursor:
+ if (alt) {
+ Inkscape::Rubberband::get(_desktop)->defaultMode();
+ }
+ } 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 (alt && !(this->grabbed || _seltrans->isGrabbed()) && !selection->isEmpty() && !_desktop->isWaitingCursor()) {
+ this->set_cursor(_default_cursor);
+ _force_dragging = false;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ if (!ret) {
+ ret = ToolBase::root_handler(event);
+ }
+
+ return ret;
+}
+
+/**
+ * Update the toolbar description to this selection.
+ */
+void SelectTool::updateDescriber(Inkscape::Selection *selection)
+{
+ _describer->updateMessage(selection);
+}
+
+}
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 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..e71a61f
--- /dev/null
+++ b/src/ui/tools/select-tool.h
@@ -0,0 +1,81 @@
+// 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)
+
+namespace Inkscape {
+ class SelTrans;
+ class SelectionDescriber;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+class SelectTool : public ToolBase {
+public:
+ SelectTool(SPDesktop *desktop);
+ ~SelectTool() override;
+
+ bool dragging;
+ bool moved;
+ guint button_press_state;
+
+ std::vector<SPItem *> cycling_items;
+ std::vector<SPItem *> cycling_items_cmp;
+ SPItem *cycling_cur_item;
+ bool cycling_wrap;
+
+ SPItem *item;
+ Inkscape::CanvasItem *grabbed = nullptr;
+ Inkscape::SelTrans *_seltrans;
+ Inkscape::SelectionDescriber *_describer;
+ gchar *no_selection_msg = nullptr;
+
+ void set(const Inkscape::Preferences::Entry& val) override;
+ bool root_handler(GdkEvent* event) override;
+ bool item_handler(SPItem* item, GdkEvent* event) override;
+
+ void updateDescriber(Inkscape::Selection *sel);
+private:
+ bool sp_select_context_abort();
+ void sp_select_context_cycle_through_items(Inkscape::Selection *selection, GdkEventScroll *scroll_event);
+ void sp_select_context_reset_opacities();
+
+ bool _alt_on;
+ bool _force_dragging;
+
+ std::string _default_cursor;
+};
+
+} // namespace Tools
+} // 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 :
diff --git a/src/ui/tools/spiral-tool.cpp b/src/ui/tools/spiral-tool.cpp
new file mode 100644
index 0000000..8ab8efb
--- /dev/null
+++ b/src/ui/tools/spiral-tool.cpp
@@ -0,0 +1,409 @@
+// 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 "spiral-tool.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 "include/macros.h"
+
+#include "object/sp-namedview.h"
+#include "object/sp-spiral.h"
+
+#include "ui/icon-names.h"
+#include "ui/shape-editor.h"
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+SpiralTool::SpiralTool(SPDesktop *desktop)
+ : ToolBase(desktop, "/tools/shapes/spiral", "spiral.svg")
+ , spiral(nullptr)
+ , revo(3)
+ , exp(1)
+ , t0(0)
+{
+ sp_event_context_read(this, "expansion");
+ sp_event_context_read(this, "revolution");
+ sp_event_context_read(this, "t0");
+
+ this->shape_editor = new ShapeEditor(desktop);
+
+ SPItem *item = desktop->getSelection()->singleItem();
+ if (item) {
+ this->shape_editor->set_item(item);
+ }
+
+ Inkscape::Selection *selection = 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();
+ }
+}
+
+SpiralTool::~SpiralTool() {
+ ungrabCanvasEvents();
+
+ this->finishItem();
+ this->sel_changed_connection.disconnect();
+
+ this->enableGrDrag(false);
+
+ 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::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;
+
+ 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) {
+ dragging = TRUE;
+
+ this->center = this->setup_for_drag_start(event);
+
+ SnapManager &m = _desktop->namedview->snap_manager;
+ m.setup(_desktop);
+ m.freeSnapReturnByRef(this->center, Inkscape::SNAPSOURCE_NODE_HANDLE);
+ m.unSetup();
+
+ grabCanvasEvents();
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_MOTION_NOTIFY:
+ if (dragging && (event->motion.state & GDK_BUTTON1_MASK)) {
+ 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->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) {
+ dragging = FALSE;
+ this->discard_delayed_snap_event();
+
+ if (spiral) {
+ // 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;
+ ungrabCanvasEvents();
+ }
+ 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;
+ this->discard_delayed_snap_event();
+ // if drawing, cancel, otherwise pass it up for deselecting
+ this->cancel();
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_space:
+ if (dragging) {
+ ungrabCanvasEvents();
+ dragging = false;
+ this->discard_delayed_snap_event();
+
+ if (!this->within_tolerance) {
+ // we've been dragging, finish the spiral
+ 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 SpiralTool::drag(Geom::Point const &p, guint state) {
+
+ 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 = _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 = cast<SPSpiral>(currentLayer()->appendChildRepr(repr));
+ Inkscape::GC::release(repr);
+ this->spiral->transform = currentLayer()->i2doc_affine().inverse();
+ this->spiral->updateRepr();
+ }
+
+ 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);
+ // compensate stroke scaling couldn't be done in doWriteTransform
+ double const expansion = spiral->transform.descrim();
+ spiral->doWriteTransform(spiral->transform, nullptr, true);
+ spiral->adjust_stroke_width_recursive(expansion);
+
+ _desktop->getSelection()->set(this->spiral);
+ DocumentUndo::done(_desktop->getDocument(), _("Create spiral"), INKSCAPE_ICON("draw-spiral"));
+
+ this->spiral = nullptr;
+ }
+}
+
+void SpiralTool::cancel() {
+ _desktop->getSelection()->clear();
+ ungrabCanvasEvents();
+
+ 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;
+
+ 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/spiral-tool.h b/src/ui/tools/spiral-tool.h
new file mode 100644
index 0000000..203617c
--- /dev/null
+++ b/src/ui/tools/spiral-tool.h
@@ -0,0 +1,61 @@
+// 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 {
+
+class Selection;
+
+namespace UI {
+namespace Tools {
+
+class SpiralTool : public ToolBase {
+public:
+ SpiralTool(SPDesktop *desktop);
+ ~SpiralTool() override;
+
+ void set(const Inkscape::Preferences::Entry& val) override;
+ bool root_handler(GdkEvent* event) 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..c6089b3
--- /dev/null
+++ b/src/ui/tools/spray-tool.cpp
@@ -0,0 +1,1528 @@
+// 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 <tuple>
+
+#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 "display/cairo-utils.h"
+#include "display/curve.h"
+#include "display/drawing-context.h"
+#include "display/drawing.h"
+#include "display/control/canvas-item-bpath.h"
+#include "display/control/canvas-item-drawing.h"
+
+#include "object/box3d.h"
+#include "object/sp-use.h"
+#include "object/sp-item-transform.h"
+
+#include "svg/svg.h"
+#include "svg/svg-color.h"
+
+#include "ui/icon-names.h"
+#include "ui/toolbar/spray-toolbar.h"
+#include "ui/tools/spray-tool.h"
+#include "ui/widget/canvas.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
+};
+
+/**
+ * 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(SPDesktop *desktop)
+ : ToolBase(desktop, "/tools/spray", "spray.svg", 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)
+ , 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)
+{
+ dilate_area = make_canvasitem<CanvasItemBpath>(desktop->getCanvasControls());
+ dilate_area->set_stroke(0xff9900ff);
+ dilate_area->set_fill(0x0, SP_WIND_RULE_EVENODD);
+ dilate_area->hide();
+
+ 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();
+ }
+ desktop->getSelection()->setBackup();
+ 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");
+}
+
+SprayTool::~SprayTool() {
+ if (!object_set.isEmpty()) {
+ object_set.clear();
+ }
+ _desktop->getSelection()->restoreBackup();
+ this->enableGrDrag(false);
+ this->style_set_connection.disconnect();
+}
+
+void SprayTool::update_cursor(bool /*with_shift*/) {
+ guint num = 0;
+ gchar *sel_message = nullptr;
+
+ if (!_desktop->getSelection()->isEmpty()) {
+ num = (guint)boost::distance(_desktop->getSelection()->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;
+ }
+ g_free(sel_message);
+}
+
+
+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)/tc->getDesktop()->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...
+}
+
+static guint32 getPickerData(Geom::IntRect area, SPDesktop *desktop)
+{
+ Inkscape::CanvasItemDrawing *canvas_item_drawing = desktop->getCanvasDrawing();
+ Inkscape::Drawing *drawing = canvas_item_drawing->get_drawing();
+
+ // Get average color.
+ double R, G, B, A;
+ drawing->averageColor(area, R, G, B, A);
+
+ //this can fix the bug #1511998 if confirmed
+ if ( A < 1e-6) {
+ R = 1.0;
+ G = 1.0;
+ B = 1.0;
+ }
+
+ 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);
+ path *= desktop->doc2dt();
+ 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());
+ Geom::IntRect area = Geom::IntRect::from_xywh(floor(mid_point[Geom::X]), floor(mid_point[Geom::Y]), 1, 1);
+ guint32 rgba = getPickerData(area, desktop);
+ 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(), desktop);
+ }
+ 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, desktop);
+ if (!rect_sprayed.hasZeroArea()) {
+ rgba2 = getPickerData(rect_sprayed.roundOutwards(), desktop);
+ }
+ }
+ 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,
+ SPItem *&single_path_output,
+ 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;
+
+ {
+ // convert 3D boxes to ordinary groups before spraying their shapes
+ // TODO: ideally the original object is preserved.
+ if (auto box = cast<SPBox3D>(item)) {
+ desktop->getSelection()->remove(item);
+ set->remove(item);
+ item = box->convert_to_group();
+ set->add(item);
+ desktop->getSelection()->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 ||
+ pick_no_overlap || 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 = 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) {
+ if (item) {
+ SPDocument *doc = item->document;
+ Inkscape::XML::Document* xml_doc = doc->getReprDoc();
+ Inkscape::XML::Node *old_repr = item->getRepr();
+ Inkscape::XML::Node *parent = old_repr->parent();
+
+ Geom::OptRect a = 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"));
+ } else {
+ spray_origin = copy->attribute("inkscape:spray-origin");
+ }
+ parent->appendChild(copy);
+ SPObject *new_obj = doc->getObjectByRepr(copy);
+ auto item_copied = 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 = 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
+ // only works if no groups in selection
+ ObjectSet object_set_tmp = *desktop->getSelection();
+ object_set_tmp.clear();
+ object_set_tmp.add(item_copied);
+ object_set_tmp.removeLPESRecursive(true);
+ if (is<SPUse>(object_set_tmp.objects().front())) {
+ object_set_tmp.unlinkRecursive(true);
+ }
+ if (single_path_output) { // Previous result
+ object_set_tmp.add(single_path_output);
+ }
+ object_set_tmp.pathUnion(true);
+ single_path_output = object_set_tmp.items().front();
+ for (auto item : object_set_tmp.items()) {
+ auto repr = item->getRepr();
+ repr->setAttribute("inkscape:spray-origin", spray_origin);
+ }
+ object_set_tmp.clear();
+ 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 ||
+ pick_no_overlap || 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 = 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->getDesktop();
+ 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
+ , tc->single_path_output
+ , 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)) *
+ Geom::Rotate(tc->tilt) *
+ Geom::Translate(tc->getDesktop()->point()));
+
+ Geom::PathVector path = Geom::Path(Geom::Circle(0,0,1)); // Unit circle centered at origin.
+ path *= sm;
+ tc->dilate_area->set_bpath(path);
+ tc->dilate_area->show();
+}
+
+static void sp_spray_switch_mode(SprayTool *tc, gint mode, bool with_shift)
+{
+ // Select the button mode
+ auto tb = dynamic_cast<UI::Toolbar::SprayToolbar*>(tc->getDesktop()->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:
+ dilate_area->show();
+ break;
+ case GDK_LEAVE_NOTIFY:
+ dilate_area->hide();
+ break;
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 1) {
+ _desktop->getSelection()->restoreBackup();
+ 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);
+
+ 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) {
+ this->single_path_output = nullptr;
+ }
+
+ 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)) *
+ Geom::Rotate(this->tilt) *
+ Geom::Translate(_desktop->w2d(motion_w)));
+
+ Geom::PathVector path = Geom::Path(Geom::Circle(0, 0, 1)); // Unit circle centered at origin.
+ path *= sm;
+ this->dilate_area->set_bpath(path);
+ this->dilate_area->show();
+
+ guint num = 0;
+ if (!_desktop->getSelection()->isEmpty()) {
+ num = (guint)boost::distance(_desktop->getSelection()->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);
+ this->is_drawing = true;
+ this->is_dilating = true;
+ this->has_dilated = false;
+ if(this->is_dilating) {
+ 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));
+
+ set_high_motion_precision(false);
+ this->is_drawing = false;
+
+ if (this->is_dilating && event->button.button == 1) {
+ 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(_desktop->getDocument(), _("Spray with copies"), INKSCAPE_ICON("tool-spray"));
+ break;
+ case SPRAY_MODE_CLONE:
+ DocumentUndo::done(_desktop->getDocument(), _("Spray with clones"), INKSCAPE_ICON("tool-spray"));
+ break;
+ case SPRAY_MODE_SINGLE_PATH:
+ DocumentUndo::done(_desktop->getDocument(), _("Spray in single path"), INKSCAPE_ICON("tool-spray"));
+ break;
+ }
+ }
+ _desktop->getSelection()->clear();
+ 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..f8bda36
--- /dev/null
+++ b/src/ui/tools/spray-tool.h
@@ -0,0 +1,148 @@
+// 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"
+#include "object/object-set.h"
+#include "display/control/canvas-item-ptr.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 {
+ class CanvasItemBpath;
+ 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(SPDesktop *desktop);
+ ~SprayTool() override;
+
+ //ToolBase event_context;
+ /* 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;
+ CanvasItemPtr<CanvasItemBpath> 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;
+
+ void set(const Inkscape::Preferences::Entry& val) override;
+ virtual void setCloneTilerPrefs();
+ bool root_handler(GdkEvent* event) override;
+ void update_cursor(bool /*with_shift*/);
+
+ ObjectSet* objectSet() {
+ return &object_set;
+ }
+ SPItem* single_path_output = nullptr;
+
+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..b211916
--- /dev/null
+++ b/src/ui/tools/star-tool.cpp
@@ -0,0 +1,428 @@
+// 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 "star-tool.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 "include/macros.h"
+
+#include "object/sp-namedview.h"
+#include "object/sp-star.h"
+
+#include "ui/icon-names.h"
+#include "ui/shape-editor.h"
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+StarTool::StarTool(SPDesktop *desktop)
+ : ToolBase(desktop, "/tools/shapes/star", "star.svg")
+ , star(nullptr)
+ , magnitude(5)
+ , proportion(0.5)
+ , isflatsided(false)
+ , rounded(0)
+ , randomized(0)
+{
+ sp_event_context_read(this, "isflatsided");
+ sp_event_context_read(this, "magnitude");
+ sp_event_context_read(this, "proportion");
+ sp_event_context_read(this, "rounded");
+ sp_event_context_read(this, "randomized");
+
+ this->shape_editor = new ShapeEditor(desktop);
+
+ SPItem *item = desktop->getSelection()->singleItem();
+ if (item) {
+ this->shape_editor->set_item(item);
+ }
+
+ Inkscape::Selection *selection = 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();
+ }
+}
+
+StarTool::~StarTool() {
+ ungrabCanvasEvents();
+
+ this->finishItem();
+ this->sel_changed_connection.disconnect();
+
+ this->enableGrDrag(false);
+
+ 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::set(const Inkscape::Preferences::Entry& val) {
+ Glib::ustring path = val.getEntryName();
+
+ if (path == "magnitude") {
+ this->magnitude = CLAMP(val.getInt(5), this->isflatsided ? 3 : 2, 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;
+
+ 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) {
+ dragging = true;
+
+ this->center = this->setup_for_drag_start(event);
+
+ /* Snap center */
+ SnapManager &m = _desktop->namedview->snap_manager;
+ m.setup(_desktop, true);
+ m.freeSnapReturnByRef(this->center, Inkscape::SNAPSOURCE_NODE_HANDLE);
+ m.unSetup();
+
+ grabCanvasEvents();
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_MOTION_NOTIFY:
+ if (dragging && (event->motion.state & GDK_BUTTON1_MASK)) {
+ 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) {
+ dragging = false;
+
+ this->discard_delayed_snap_event();
+
+ if (star) {
+ // 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 if (!selection->includes(this->item_to_select)) {
+ selection->set(this->item_to_select);
+ }
+ } else {
+ // click in an empty space
+ selection->clear();
+ }
+
+ this->item_to_select = nullptr;
+ ret = TRUE;
+ ungrabCanvasEvents();
+ }
+ 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;
+ this->discard_delayed_snap_event();
+ // if drawing, cancel, otherwise pass it up for deselecting
+ this->cancel();
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_space:
+ if (dragging) {
+ ungrabCanvasEvents();
+
+ dragging = false;
+
+ this->discard_delayed_snap_event();
+
+ 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)
+{
+ 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 = _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 = cast<SPStar>(currentLayer()->appendChildRepr(repr));
+
+ Inkscape::GC::release(repr);
+ this->star->transform = currentLayer()->i2doc_affine().inverse();
+ this->star->updateRepr();
+ }
+
+ /* 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);
+ // compensate stroke scaling couldn't be done in doWriteTransform
+ double const expansion = this->star->transform.descrim();
+ this->star->doWriteTransform(this->star->transform, nullptr, true);
+ this->star->adjust_stroke_width_recursive(expansion);
+
+ _desktop->getSelection()->set(this->star);
+ DocumentUndo::done(_desktop->getDocument(), _("Create star"), INKSCAPE_ICON("draw-polygon-star"));
+
+ this->star = nullptr;
+ }
+}
+
+void StarTool::cancel() {
+ _desktop->getSelection()->clear();
+ ungrabCanvasEvents();
+
+ 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;
+
+ 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..4a06a42
--- /dev/null
+++ b/src/ui/tools/star-tool.h
@@ -0,0 +1,72 @@
+// 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 {
+
+class Selection;
+
+namespace UI {
+namespace Tools {
+
+class StarTool : public ToolBase {
+public:
+ StarTool(SPDesktop *desktop);
+ ~StarTool() override;
+
+ void set(const Inkscape::Preferences::Entry &val) override;
+ bool root_handler(GdkEvent *event) 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..9aaffa7
--- /dev/null
+++ b/src/ui/tools/text-tool.cpp
@@ -0,0 +1,1905 @@
+// 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 <cmath>
+#include <gdk/gdkkeysyms.h>
+#include <gtkmm/clipboard.h>
+#include <glibmm/i18n.h>
+#include <glibmm/regex.h>
+
+#include "text-tool.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 "style.h"
+#include "text-editing.h"
+
+#include "display/control/canvas-item-curve.h"
+#include "display/control/canvas-item-quad.h"
+#include "display/control/canvas-item-rect.h"
+#include "display/control/canvas-item-bpath.h"
+#include "display/curve.h"
+
+#include "livarot/Path.h"
+#include "livarot/Shape.h"
+
+#include "object/sp-flowtext.h"
+#include "object/sp-namedview.h"
+#include "object/sp-text.h"
+#include "object/sp-textpath.h"
+#include "object/sp-rect.h"
+#include "object/sp-shape.h"
+#include "object/sp-ellipse.h"
+
+#include "ui/knot/knot-holder.h"
+#include "ui/icon-names.h"
+#include "ui/shape-editor.h"
+#include "ui/widget/canvas.h"
+#include "ui/event-debug.h"
+
+#include "xml/attribute-record.h"
+#include "xml/sp-css-attr.h"
+
+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);
+
+TextTool::TextTool(SPDesktop *desktop)
+ : ToolBase(desktop, "/tools/text", "text.svg")
+{
+ GtkSettings* settings = gtk_settings_get_default();
+ gint timeout = 0;
+ g_object_get( settings, "gtk-cursor-blink-time", &timeout, nullptr );
+
+ if (timeout < 0) {
+ timeout = 200;
+ } else {
+ timeout /= 2;
+ }
+
+ cursor = make_canvasitem<CanvasItemCurve>(desktop->getCanvasControls());
+ cursor->set_stroke(0x000000ff);
+ cursor->hide();
+
+ // The rectangle box tightly wrapping text object when selected or under cursor.
+ indicator = make_canvasitem<CanvasItemRect>(desktop->getCanvasControls());
+ indicator->set_stroke(0x0000ff7f);
+ indicator->set_shadow(0xffffff7f, 1);
+ indicator->hide();
+
+ // The shape that the text is flowing into
+ frame = make_canvasitem<CanvasItemBpath>(desktop->getCanvasControls());
+ frame->set_fill(0x00 /* zero alpha */, SP_WIND_RULE_NONZERO);
+ frame->set_stroke(0x0000ff7f);
+ frame->hide();
+
+ // A second frame for showing the padding of the above frame
+ padding_frame = make_canvasitem<CanvasItemBpath>(desktop->getCanvasControls());
+ padding_frame->set_fill(0x00 /* zero alpha */, SP_WIND_RULE_NONZERO);
+ padding_frame->set_stroke(0xccccccdf);
+ padding_frame->hide();
+
+ 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()->gobj());
+
+ /* 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);
+ }
+ }
+
+ this->shape_editor = new ShapeEditor(desktop);
+
+ SPItem *item = desktop->getSelection()->singleItem();
+ if (item && (is<SPFlowtext>(item) || is<SPText>(item))) {
+ 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();
+ }
+}
+
+TextTool::~TextTool()
+{
+ if (_desktop) {
+ sp_signal_disconnect_by_data(_desktop->getCanvas()->gobj(), 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;
+ }
+
+ cursor.reset();
+ indicator.reset();
+ frame.reset();
+ padding_frame.reset();
+ text_selection_quads.clear();
+
+ delete this->shape_editor;
+ this->shape_editor = nullptr;
+
+ ungrabCanvasEvents();
+
+ Inkscape::Rubberband::get(_desktop)->stop();
+}
+
+void TextTool::deleteSelected()
+{
+ Inkscape::UI::Tools::sp_text_delete_selection(_desktop->event_context);
+ DocumentUndo::done(_desktop->getDocument(), _("Delete text"), INKSCAPE_ICON("draw-text"));
+}
+
+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 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 (is<SPText>(item_ungrouped) || is<SPFlowtext>(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->dragging = 0;
+ this->discard_delayed_snap_event();
+ ret = TRUE;
+ _desktop->emit_text_cursor_moved(this, 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)
+{
+ SPDesktop *desktop = tc->getDesktop();
+
+ /* Create <text> */
+ Inkscape::XML::Document *xml_doc = 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(desktop, rtext, "/tools/text", true);
+
+ rtext->setAttributeSvgDouble("x", tc->pdoc[Geom::X]);
+ rtext->setAttributeSvgDouble("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);
+ auto text_item = cast<SPItem>(tc->currentLayer()->appendChildRepr(rtext));
+ /* fixme: Is selection::changed really immediate? */
+ /* yes, it's immediate .. why does it matter? */
+ desktop->getSelection()->set(text_item);
+ Inkscape::GC::release(rtext);
+ text_item->transform = tc->currentLayer()->i2doc_affine().inverse();
+
+ text_item->updateRepr();
+ text_item->doWriteTransform(text_item->transform, nullptr, true);
+ DocumentUndo::done(desktop->getDocument(), _("Create text"), INKSCAPE_ICON("draw-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->getDesktop()->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->getDesktop()->getDocument(), _("Insert Unicode character"), INKSCAPE_ICON("draw-text"));
+ }
+}
+
+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) {
+
+#if EVENT_DEBUG
+ ui_dump_event(reinterpret_cast<GdkEvent *>(event), "TextTool::root_handler");
+#endif
+
+ indicator->hide();
+
+ sp_text_context_validate_cursor_iterators(this);
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 1) {
+ 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);
+
+ grabCanvasEvents();
+
+ this->creating = true;
+
+ /* Processed */
+ return TRUE;
+ }
+ break;
+ case GDK_MOTION_NOTIFY: {
+ if (this->creating && (event->motion.state & GDK_BUTTON1_MASK)) {
+ 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) {
+ 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 (is<SPText>(item_ungrouped) || is<SPFlowtext>(item_ungrouped)) {
+ Inkscape::Text::Layout const *layout = te_get_layout(item_ungrouped);
+ if (layout->inputTruncated()) {
+ indicator->set_stroke(0xff0000ff);
+ } else {
+ indicator->set_stroke(0x0000ff7f);
+ }
+ Geom::OptRect ibbox = item_ungrouped->desktopVisualBounds();
+ if (ibbox) {
+ indicator->set_rect(*ibbox);
+ }
+ indicator->show();
+
+ this->set_cursor("text-insert.svg");
+ sp_text_context_update_text_selection(this);
+ if (is<SPText>(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 {
+ // update cursor and statusbar: we are not over a text object now
+ this->set_cursor("text.svg");
+ _desktop->event_context->defaultMessageContext()->clear();
+ this->over_text = false;
+ }
+ } break;
+
+ case GDK_BUTTON_RELEASE:
+ if (event->button.button == 1) {
+ this->discard_delayed_snap_event();
+
+ 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();
+
+ ungrabCanvasEvents();
+
+ 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 */
+ cursor->show();
+ // 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();
+ Geom::Point const cursor_size(0, y_dir * cursor_height);
+ cursor->set_coords(p1, p1 - cursor_size);
+ if (this->imc) {
+ GdkRectangle im_cursor;
+ Geom::Point const top_left = _desktop->get_display_area().corner(0);
+ Geom::Point const im_d0 = _desktop->d2w(p1 - top_left);
+ Geom::Point const im_d1 = _desktop->d2w(p1 - cursor_size - top_left);
+ Geom::Rect const im_rect(im_d0, im_d1);
+ im_cursor.x = (int) floor(im_rect.left());
+ im_cursor.y = (int) floor(im_rect.top());
+ im_cursor.width = (int) floor(im_rect.width());
+ im_cursor.height = (int) floor(im_rect.height());
+ 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);
+
+ } else {
+ // SVG 1.2 text
+
+ SPItem *ft = create_flowtext_with_internal_frame (_desktop, this->p0, p1);
+
+ _desktop->getSelection()->set(ft);
+ }
+
+ _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Flowed text is created."));
+ DocumentUndo::done(_desktop->getDocument(), _("Create flowed text"), INKSCAPE_ICON("draw-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->emit_text_cursor_moved(this, 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
+
+ // Input methods often use Ctrl+Shift+U for preediting (unimode).
+ // Override it so we can use our unimode.
+ bool preedit_activation = (MOD__CTRL(event) && MOD__SHIFT(event) && !MOD__ALT(event))
+ && (group0_keyval == GDK_KEY_U || group0_keyval == GDK_KEY_u);
+
+ if (this->unimode || !this->imc || preedit_activation
+ || !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: {
+ guint32 xdigit = gdk_keyval_to_unicode(group0_keyval);
+ if (xdigit <= 255 && g_ascii_isxdigit(xdigit)) {
+ g_return_val_if_fail(this->unipos < sizeof(this->uni) - 1, TRUE);
+ this->uni[this->unipos++] = xdigit;
+ 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().bounds();
+ 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("TextFontFamilyAction_entry");
+ 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(), _("Insert no-break space"), INKSCAPE_ICON("draw-text"));
+ 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(), _("Make bold"), INKSCAPE_ICON("draw-text"));
+ 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(), _("Make italic"), INKSCAPE_ICON("draw-text"));
+ 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>
+ }
+
+ auto text_element = 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(), _("New line"), INKSCAPE_ICON("draw-text"));
+ 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(), _("Backspace"), INKSCAPE_ICON("draw-text"));
+ }
+ 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(), _("Delete"), INKSCAPE_ICON("draw-text"));
+ }
+ 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", _("Kern to the left"), INKSCAPE_ICON("draw-text"));
+ } 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", _("Kern to the right"), INKSCAPE_ICON("draw-text"));
+ } 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", _("Kern up"), INKSCAPE_ICON("draw-text"));
+ } 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", _("Kern down"), INKSCAPE_ICON("draw-text"));
+ } 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;
+ ungrabCanvasEvents();
+ 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", _("Rotate counterclockwise"), INKSCAPE_ICON("draw-text"));
+ 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", _("Rotate clockwise"), INKSCAPE_ICON("draw-text"));
+ 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", _("Contract line spacing"), INKSCAPE_ICON("draw-text"));
+ } 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", _("Contract letter spacing"), INKSCAPE_ICON("draw-text"));
+ }
+ 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", _("Expand line spacing"), INKSCAPE_ICON("draw-text"));
+ } 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", _("Expand letter spacing"), INKSCAPE_ICON("draw-text"));
+ }
+ 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;
+ ungrabCanvasEvents();
+ Inkscape::Rubberband::get(_desktop)->stop();
+ }
+ } else if ((group0_keyval == GDK_KEY_x || group0_keyval == GDK_KEY_X) && MOD__ALT_ONLY(event)) {
+ _desktop->setToolboxFocusTo("TextFontFamilyAction_entry");
+ 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;
+ auto textitem = 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();
+ }
+
+ auto flowtext = 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->getDesktop()->getDocument(), _("Paste text"), INKSCAPE_ICON("draw-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 couldn't get it working properly we miss font size font style or never work
+// and user usually 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);
+ SPItem *item = selection->singleItem();
+
+ if (this->text && (item != this->text)) {
+ sp_text_context_forget_text(this);
+ }
+ this->text = nullptr;
+
+ shape_editor->unset_item();
+ if (is<SPText>(item) || is<SPFlowtext>(item)) {
+ shape_editor->set_item(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*/)
+{
+ bool scroll = !this->shape_editor->has_knotholder() ||
+ !this->shape_editor->knotholder->is_dragging();
+ sp_text_context_update_cursor(this, scroll);
+ 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.
+ auto sptext = cast<SPText>(text);
+ if (sptext) {
+ sptext->rebuildLayout();
+ sptext->updateRepr();
+ }
+
+ DocumentUndo::done(_desktop->getDocument(), _("Set text style"), INKSCAPE_ICON("draw-text"));
+ 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 ( is<SPString>(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->getDesktop()) return;
+ auto desktop = tc->getDesktop();
+
+ 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 (is<SPText>(tc->text)) {
+ Geom::OptRect opt_frame = cast<SPText>(tc->text)->get_frame();
+ if (opt_frame && (!opt_frame->contains(p0))) {
+ scroll = false;
+ }
+ } else if (is<SPFlowtext>(tc->text)) {
+ SPItem *frame = cast<SPFlowtext>(tc->text)->get_frame(nullptr); // first frame only
+ Geom::OptRect opt_frame = frame->geometricBounds();
+ if (opt_frame && (!opt_frame->contains(p0))) {
+ scroll = false;
+ }
+ }
+
+ if (scroll) {
+ Geom::Point const center = desktop->current_center();
+ 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
+ desktop->scroll_to_point(d0);
+ else
+ desktop->scroll_to_point(d1);
+ }
+ }
+
+ tc->cursor->set_coords(d0, d1);
+ tc->cursor->show();
+
+ /* 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 = desktop->get_display_area().corner(0);
+ Geom::Point const im_d0 = desktop->d2w(d0 - top_left);
+ Geom::Point const im_d1 = desktop->d2w(d1 - top_left);
+ Geom::Rect const im_rect(im_d0, im_d1);
+ im_cursor.x = (int) floor(im_rect.left());
+ im_cursor.y = (int) floor(im_rect.top());
+ im_cursor.width = (int) floor(im_rect.width());
+ im_cursor.height = (int) floor(im_rect.height());
+ 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 *edit_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);
+ char const *edit_message_flowed = 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);
+ bool truncated = layout->inputTruncated();
+ char const *trunc = truncated ? _(" [truncated]") : "";
+
+ if (truncated) {
+ tc->frame->set_stroke(0xff0000ff);
+ } else {
+ tc->frame->set_stroke(0x0000ff7f);
+ }
+
+ std::vector<SPItem const *> shapes;
+ std::unique_ptr<Shape> exclusion_shape;
+ double padding = 0.0;
+
+ // Frame around text
+ if (is<SPFlowtext>(tc->text)) {
+ SPItem *frame = cast<SPFlowtext>(tc->text)->get_frame (nullptr); // first frame only
+ shapes.push_back(frame);
+
+ tc->message_context->setF(Inkscape::NORMAL_MESSAGE, edit_message_flowed, nChars, trunc);
+
+ } else if (auto text = cast<SPText>(tc->text)) {
+ if (text->style->shape_inside.set) {
+ for (auto const *href : text->style->shape_inside.hrefs) {
+ shapes.push_back(href->getObject());
+ }
+ if (text->style->shape_padding.set) {
+ // Calculate it here so we never show padding on FlowText or non-flowed Text (even if set)
+ padding = text->style->shape_padding.computed;
+ }
+ if(text->style->shape_subtract.set) {
+ // Find union of all exclusion shapes for later use
+ exclusion_shape = text->getExclusionShape();
+ }
+ tc->message_context->setF(Inkscape::NORMAL_MESSAGE, edit_message_flowed, nChars, trunc);
+ } else {
+ for (SPObject &child : tc->text->children) {
+ if (auto textpath = cast<SPTextPath>(&child)) {
+ shapes.push_back(sp_textpath_get_path_item(textpath));
+ }
+ }
+ tc->message_context->setF(Inkscape::NORMAL_MESSAGE, edit_message, nChars, trunc);
+ }
+ }
+
+ SPCurve curve;
+ for (auto const *shape_item : shapes) {
+ if (auto shape = cast<SPShape>(shape_item)) {
+ if (shape->curve()) {
+ curve.append(shape->curve()->transformed(shape->transform));
+ }
+ }
+ }
+
+ if (!curve.is_empty()) {
+ bool has_padding = std::fabs(padding) > 1e-12;
+
+ if (has_padding || exclusion_shape) {
+ // Should only occur for SVG2 autoflowed text
+ // See sp-text.cpp function _buildLayoutInit()
+ Path *temp = new Path;
+ temp->LoadPathVector(curve.get_pathvector());
+
+ // Get initial shape-inside curve
+ Shape *uncross = new Shape;
+ {
+ Shape *sh = new Shape;
+ temp->ConvertWithBackData(0.25); // Convert to polyline
+ temp->Fill(sh, 0);
+ uncross->ConvertToShape(sh);
+ delete sh;
+ }
+
+ // Get padded shape exclusion
+ if (has_padding) {
+ Shape *pad_shape = new Shape;
+ Path *padded = new Path;
+ Path *padt = new Path;
+ Shape *sh = new Shape;
+ padt->LoadPathVector(curve.get_pathvector());
+ padt->Outline(padded, padding, join_round, butt_straight, 20.0);
+ padded->ConvertWithBackData(1.0); // Convert to polyline
+ padded->Fill(sh, 0);
+ pad_shape->ConvertToShape(sh);
+ delete sh;
+ delete padt;
+ delete padded;
+
+ Shape *copy = new Shape;
+ copy->Booleen(uncross, pad_shape, (padding > 0.0) ? bool_op_diff : bool_op_union);
+ delete uncross;
+ delete pad_shape;
+ uncross = copy;
+ }
+
+ // Remove exclusions plus margins from padding frame
+ if (exclusion_shape && exclusion_shape->hasEdges()) {
+ Shape *copy = new Shape;
+ copy->Booleen(uncross, exclusion_shape.get(), bool_op_diff);
+ delete uncross;
+ uncross = copy;
+ }
+
+ uncross->ConvertToForme(temp);
+ tc->padding_frame->set_bpath(temp->MakePathVector() * tc->text->i2dt_affine());
+ tc->padding_frame->show();
+
+ delete temp;
+ delete uncross;
+ } else {
+ tc->padding_frame->hide();
+ }
+
+ // Transform curve after doing padding.
+ curve.transform(tc->text->i2dt_affine());
+ tc->frame->set_bpath(&curve);
+ tc->frame->show();
+ } else {
+ tc->frame->hide();
+ tc->padding_frame->hide();
+ }
+
+ } else {
+ tc->cursor->hide();
+ tc->frame->hide();
+ tc->show = FALSE;
+ if (!tc->nascent_object) {
+ 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
+ }
+ }
+
+ desktop->emit_text_cursor_moved(tc, 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->getDesktop()) return;
+
+ 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) {
+ auto quad = new CanvasItemQuad(tc->getDesktop()->getCanvasControls(), quads[i], quads[i+1], quads[i+2], quads[i+3]);
+ quad->set_fill(0x00777777); // Semi-transparent blue as Cairo cannot do inversion.
+ quad->show();
+ tc->text_selection_quads.emplace_back(quad);
+ }
+
+ if (tc->shape_editor) {
+ if (tc->shape_editor->knotholder) {
+ tc->shape_editor->knotholder->update_knots();
+ }
+ }
+}
+
+static gint sp_text_context_timeout(TextTool *tc)
+{
+ if (tc->show) {
+ if (tc->phase) {
+ tc->phase = false;
+ tc->cursor->set_stroke(0x000000ff);
+ } else {
+ tc->phase = true;
+ tc->cursor->set_stroke(0xffffffff);
+ }
+ tc->cursor->show();
+ }
+
+ 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 editor 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 ((is<SPText>(ti) || is<SPFlowtext>(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(), _("Remove empty text"), INKSCAPE_ICON("draw-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, _("Type text"), INKSCAPE_ICON("draw-text"));
+}
+
+void sp_text_context_place_cursor (TextTool *tc, SPObject *text, Inkscape::Text::Layout::iterator where)
+{
+ tc->getDesktop()->getSelection()->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->getDesktop()->getSelection()->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..c87431e
--- /dev/null
+++ b/src/ui/tools/text-tool.h
@@ -0,0 +1,122 @@
+// 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"
+#include "display/control/canvas-item-ptr.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;
+
+namespace Inkscape {
+
+class CanvasItemCurve; // Cursor
+class CanvasItemQuad; // Highlighted text
+class CanvasItemRect; // Indicator, Frame
+class CanvasItemBpath;
+class Selection;
+
+namespace UI {
+namespace Tools {
+
+class TextTool : public ToolBase {
+public:
+ TextTool(SPDesktop *desktop);
+ ~TextTool() override;
+
+ sigc::connection sel_changed_connection;
+ sigc::connection sel_modified_connection;
+ sigc::connection style_set_connection;
+ sigc::connection style_query_connection;
+
+ GtkIMContext *imc = nullptr;
+
+ SPItem *text = nullptr; // 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 = false;
+ guint unipos = 0;
+
+ // ---- On canvas editing ---
+ CanvasItemPtr<CanvasItemCurve> cursor;
+ CanvasItemPtr<CanvasItemRect> indicator;
+ CanvasItemPtr<CanvasItemBpath> frame; // Highlighting flowtext shapes or textpath path
+ CanvasItemPtr<CanvasItemBpath> padding_frame; // Highlighting flowtext padding
+ std::vector<CanvasItemPtr<CanvasItemQuad>> text_selection_quads;
+
+ gint timeout = 0;
+ bool show = false;
+ bool phase = false;
+ bool nascent_object = false; // true if we're clicked on canvas to put cursor,
+ // but no text typed yet so ->text is still NULL
+
+ bool over_text = false; // true if cursor is over a text object
+
+ guint dragging = 0; // dragging selection over text
+ bool creating = false; // dragging rubberband to create flowtext
+ Geom::Point p0; // initial point if the flowtext rect
+
+ /* Preedit String */
+ gchar* preedit_string = nullptr;
+
+ bool root_handler(GdkEvent* event) override;
+ bool item_handler(SPItem* item, GdkEvent* event) override;
+ void deleteSelected();
+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
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ 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.cpp b/src/ui/tools/tool-base.cpp
new file mode 100644
index 0000000..59a6470
--- /dev/null
+++ b/src/ui/tools/tool-base.cpp
@@ -0,0 +1,1712 @@
+// 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 <set>
+
+#include "desktop-events.h"
+#include "desktop-style.h"
+#include "desktop.h"
+#include "file.h"
+#include "gradient-drag.h"
+#include "layer-manager.h"
+#include "message-context.h"
+#include "rubberband.h"
+#include "selcue.h"
+#include "selection-chemistry.h"
+#include "selection.h"
+
+#include "actions/actions-tools.h"
+
+#include "display/control/canvas-item-catchall.h" // Grab/Ungrab
+#include "display/control/snap-indicator.h"
+
+#include "include/gtkmm_version.h"
+#include "include/macros.h"
+
+#include "object/sp-guide.h"
+
+#include "ui/contextmenu.h"
+#include "ui/cursor-utils.h"
+#include "ui/event-debug.h"
+#include "ui/interface.h"
+#include "ui/knot/knot.h"
+#include "ui/knot/knot-holder.h"
+#include "ui/knot/knot-ptr.h"
+#include "ui/modifiers.h"
+#include "ui/shape-editor.h"
+#include "ui/shortcuts.h"
+
+#include "ui/tool/commit-events.h"
+#include "ui/tool/control-point.h"
+#include "ui/tool/event-utils.h"
+#include "ui/tool/shape-record.h"
+#include "ui/tools/calligraphic-tool.h"
+#include "ui/tools/dropper-tool.h"
+#include "ui/tools/lpe-tool.h"
+#include "ui/tools/node-tool.h"
+#include "ui/tools/select-tool.h"
+#include "ui/tools/tool-base.h"
+#include "ui/widget/canvas.h"
+
+#include "widgets/desktop-widget.h"
+
+// globals for temporary switching to selector by space
+static bool selector_toggled = FALSE;
+static Glib::ustring switch_selector_to;
+
+// globals for temporary switching to dropper by 'D'
+static bool dropper_toggled = FALSE;
+static Glib::ustring switch_dropper_to;
+
+// globals for keeping track of keyboard scroll events in order to accelerate
+static guint32 scroll_event_time = 0;
+static double scroll_multiply = 1;
+static unsigned scroll_keyval = 0;
+
+// globals for key processing
+static bool latin_keys_group_valid = FALSE;
+static int latin_keys_group;
+static std::set<int> latin_keys_groups;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+static void set_event_location(SPDesktop *desktop, GdkEvent *event);
+
+ToolBase::ToolBase(SPDesktop *desktop, std::string prefs_path, std::string cursor_filename, bool uses_snap)
+ : _prefs_path(std::move(prefs_path))
+ , _cursor_filename("none")
+ , _cursor_default(std::move(cursor_filename))
+ , _uses_snap(uses_snap)
+ , _desktop(desktop)
+{
+ pref_observer = Inkscape::Preferences::PreferencesObserver::create(_prefs_path, [this] (auto &val) { set(val); });
+ set_cursor(_cursor_default);
+ _desktop->getCanvas()->grab_focus();
+
+ message_context = std::make_unique<Inkscape::MessageContext>(desktop->messageStack());
+
+ // Make sure no delayed snapping events are carried over after switching tools
+ // (this is only an additional safety measure against sloppy coding, because each
+ // tool should take care of this by itself)
+ discard_delayed_snap_event();
+}
+
+ToolBase::~ToolBase()
+{
+ enableSelectionCue(false);
+ _dse_timeout_conn.disconnect();
+}
+
+/**
+ * Called by our pref_observer if a preference has been changed.
+ */
+void ToolBase::set(Inkscape::Preferences::Entry const &/*val*/)
+{
+}
+
+SPGroup *ToolBase::currentLayer() const
+{
+ return _desktop->layerManager().currentLayer();
+}
+
+/**
+ * Sets the current cursor to the given filename. Does not readload if not changed.
+ */
+void ToolBase::set_cursor(std::string filename)
+{
+ if (filename != _cursor_filename) {
+ _cursor_filename = filename;
+ use_tool_cursor();
+ }
+}
+
+/**
+ * Returns the Gdk Cursor for the given filename
+ *
+ * WARNING: currently this changes the window cursor, see load_svg_cursor
+ */
+Glib::RefPtr<Gdk::Cursor> ToolBase::get_cursor(Glib::RefPtr<Gdk::Window> window, std::string const &filename) const
+{
+ bool fillHasColor = false;
+ bool strokeHasColor = false;
+ guint32 fillColor = sp_desktop_get_color_tool(_desktop, getPrefsPath(), true, &fillHasColor);
+ guint32 strokeColor = sp_desktop_get_color_tool(_desktop, getPrefsPath(), false, &strokeHasColor);
+ double fillOpacity = fillHasColor ? sp_desktop_get_opacity_tool(_desktop, getPrefsPath(), true) : 1.0;
+ double strokeOpacity = strokeHasColor ? sp_desktop_get_opacity_tool(_desktop, getPrefsPath(), false) : 1.0;
+
+ return load_svg_cursor(window->get_display(), window, filename,
+ fillColor, strokeColor, fillOpacity, strokeOpacity);
+}
+
+/**
+ * Uses the saved cursor, based on the saved filename.
+ */
+void ToolBase::use_tool_cursor()
+{
+ if (auto window = _desktop->getCanvas()->get_window()) {
+ _cursor = get_cursor(window, _cursor_filename);
+ window->set_cursor(_cursor);
+ }
+ _desktop->waiting_cursor = false;
+}
+
+/**
+ * Set the cursor to this specific one, don't remember it.
+ *
+ * If RefPtr is empty, sets the remembered cursor (reverting it)
+ */
+void ToolBase::use_cursor(Glib::RefPtr<Gdk::Cursor> cursor)
+{
+ if (auto window = _desktop->getCanvas()->get_window()) {
+ window->set_cursor(cursor ? cursor : _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, guint 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.
+ */
+void gobble_motion_events(guint mask) {
+ GdkEvent *event_next;
+
+ 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();
+ }
+ // otherwise, put it back onto the queue
+ if (event_next)
+ gdk_event_put(event_next);
+}
+
+/**
+ * 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 (dynamic_cast<Inkscape::UI::Tools::SelectTool *>(dt->event_context)) {
+ if (selector_toggled) {
+ set_active_tool(dt, switch_selector_to);
+ selector_toggled = false;
+ }
+ } else {
+ selector_toggled = TRUE;
+ switch_selector_to = get_active_tool(dt);
+ set_active_tool(dt, "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 (dynamic_cast<Inkscape::UI::Tools::DropperTool *>(dt->event_context)) {
+ if (dropper_toggled) {
+ set_active_tool(dt, switch_dropper_to);
+ dropper_toggled = FALSE;
+ }
+ } else {
+ dropper_toggled = TRUE;
+ switch_dropper_to = get_active_tool(dt);
+ set_active_tool(dt, "Dropper");
+ }
+}
+
+/**
+ * Calculates and keeps track of scroll acceleration.
+ * Subroutine of sp_event_context_private_root_handler().
+ */
+static double accelerate_scroll(GdkEvent *event, double acceleration)
+{
+ auto time_diff = event->key.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 = event->key.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 + gobble_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;
+ }
+
+ bool moved = false;
+ if (shape_editor && shape_editor->has_knotholder()) {
+ KnotHolder * knotholder = shape_editor->knotholder;
+ if (knotholder && knotholder->knot_selected()) {
+ knotholder->transform_selected(Geom::Translate(delta));
+ moved = true;
+ }
+ } else {
+ auto nt = dynamic_cast<Inkscape::UI::Tools::NodeTool *>(_desktop->event_context);
+ if (nt) {
+ for (auto &_shape_editor : nt->_shape_editors) {
+ ShapeEditor *shape_editor = _shape_editor.second.get();
+ if (shape_editor && shape_editor->has_knotholder()) {
+ KnotHolder * knotholder = shape_editor->knotholder;
+ if (knotholder && knotholder->knot_selected()) {
+ knotholder->transform_selected(Geom::Translate(delta));
+ moved = true;
+ }
+ }
+ }
+ }
+ }
+
+ return moved;
+}
+
+bool ToolBase::root_handler(GdkEvent *event)
+{
+
+#ifdef EVENT_DUMP
+ ui_dump_event (event, "ToolBase::root_handler");
+#endif
+
+ static Geom::Point button_w;
+ 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");
+ bool ret = false;
+
+ auto compute_angle = [&] {
+ // Hack: Undo coordinate transformation applied by canvas to get events back to window coordinates.
+ // Real solution: Move all this functionality out of this file to somewhere higher up in the chain.
+ auto cursor = Geom::Point(event->motion.x, event->motion.y) * _desktop->canvas->get_geom_affine().inverse() * _desktop->canvas->get_affine() - _desktop->canvas->get_pos();
+ return Geom::deg_from_rad(Geom::atan2(cursor - Geom::Point(_desktop->canvas->get_dimensions()) / 2.0));
+ };
+
+ switch (event->type) {
+ case GDK_2BUTTON_PRESS:
+ if (panning) {
+ panning = PANNING_NONE;
+ ungrabCanvasEvents();
+ ret = true;
+ } else {
+ /* sp_desktop_dialog(); */
+ }
+ break;
+
+ case GDK_BUTTON_PRESS:
+ // save drag origin
+ xp = event->button.x;
+ yp = event->button.y;
+ within_tolerance = true;
+
+ button_w = Geom::Point(event->button.x, event->button.y);
+
+ switch (event->button.button) {
+ case 1:
+ // TODO Does this make sense? Panning starts on passive mouse motion while space
+ // bar is pressed, it's not necessary to press the mouse button.
+ if (is_space_panning()) {
+ // When starting panning, make sure there are no snap events pending because these might disable the panning again
+ if (_uses_snap) {
+ discard_delayed_snap_event();
+ }
+ panning = PANNING_SPACE_BUTTON1;
+
+ grabCanvasEvents(Gdk::KEY_RELEASE_MASK |
+ Gdk::BUTTON_RELEASE_MASK |
+ Gdk::POINTER_MOTION_MASK );
+
+ ret = true;
+ }
+ break;
+
+ case 2:
+ if ((event->button.state & GDK_CONTROL_MASK) && !_desktop->get_rotation_lock()) {
+ // Canvas ctrl + middle-click to rotate
+ rotating = true;
+
+ start_angle = current_angle = compute_angle();
+
+ grabCanvasEvents(Gdk::KEY_PRESS_MASK |
+ Gdk::KEY_RELEASE_MASK |
+ Gdk::BUTTON_RELEASE_MASK |
+ Gdk::POINTER_MOTION_MASK);
+
+ } 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) {
+ discard_delayed_snap_event();
+ }
+ panning = PANNING_BUTTON2;
+
+ grabCanvasEvents(Gdk::BUTTON_RELEASE_MASK |
+ Gdk::POINTER_MOTION_MASK );
+ }
+
+ ret = true;
+ break;
+
+ case 3:
+ if (event->button.state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK)) {
+ // When starting panning, make sure there are no snap events pending because these might disable the panning again
+ if (_uses_snap) {
+ discard_delayed_snap_event();
+ }
+ panning = PANNING_BUTTON3;
+
+ grabCanvasEvents(Gdk::BUTTON_RELEASE_MASK |
+ Gdk::POINTER_MOTION_MASK );
+ ret = true;
+ } else if (!are_buttons_1_and_3_on(event)) {
+ menu_popup(event);
+ ret = true;
+ }
+ 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);
+
+ grabCanvasEvents(Gdk::KEY_RELEASE_MASK |
+ Gdk::BUTTON_RELEASE_MASK |
+ Gdk::POINTER_MOTION_MASK );
+ }
+
+ 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 = PANNING_NONE;
+ ungrabCanvasEvents();
+ 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 &&
+ std::abs((int)event->motion.x - xp) < tolerance * 3 &&
+ std::abs((int)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;
+ auto display = _desktop->getCanvas()->get_display();
+ auto window = _desktop->getCanvas()->get_window();
+ auto cursor = Gdk::Cursor::create(display, "move");
+ window->set_cursor(cursor);
+ }
+
+ auto const motion_w = Geom::Point(event->motion.x, event->motion.y);
+ auto const moved_w = motion_w - button_w;
+ _desktop->scroll_relative(moved_w);
+ ret = true;
+ }
+ } else if (zoom_rb) {
+ if (within_tolerance &&
+ std::abs((int)event->motion.x - xp) < tolerance &&
+ std::abs((int)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()) {
+ auto const motion_w = Geom::Point(event->motion.x, event->motion.y);
+ auto const motion_dt = _desktop->w2d(motion_w);
+
+ Inkscape::Rubberband::get(_desktop)->move(motion_dt);
+ } else {
+ // Start the box where the mouse was clicked, not where it is now
+ // because otherwise our box would be offset by the amount of tolerance.
+ auto const motion_w = Geom::Point(xp, yp);
+ auto const motion_dt = _desktop->w2d(motion_w);
+
+ Inkscape::Rubberband::get(_desktop)->start(_desktop, motion_dt);
+ }
+
+ if (zoom_rb == 2) {
+ gobble_motion_events(GDK_BUTTON2_MASK);
+ }
+ } else if (rotating) {
+ auto angle = compute_angle();
+
+ double constexpr rotation_snap = 15.0;
+ double delta_angle = angle - start_angle;
+ if (event->motion.state & GDK_SHIFT_MASK &&
+ event->motion.state & GDK_CONTROL_MASK) {
+ delta_angle = 0.0;
+ } else if (event->motion.state & GDK_SHIFT_MASK) {
+ delta_angle = std::round(delta_angle / rotation_snap) * rotation_snap;
+ } else if (event->motion.state & GDK_CONTROL_MASK) {
+ // ?
+ } else if (event->motion.state & GDK_MOD1_MASK) {
+ // Decimal raw angle
+ } else {
+ delta_angle = std::floor(delta_angle);
+ }
+ angle = start_angle + delta_angle;
+
+ _desktop->rotate_relative_keep_point(_desktop->w2d(Geom::Rect(_desktop->canvas->get_area_world()).midpoint()),
+ Geom::rad_from_deg(angle - current_angle));
+ current_angle = angle;
+ ret = true;
+ }
+ break;
+
+ case GDK_BUTTON_RELEASE: {
+ bool middle_mouse_zoom = prefs->getBool("/options/middlemousezoom/value");
+
+ xp = yp = 0;
+
+ if (panning_cursor == 1) {
+ panning_cursor = 0;
+ _desktop->getCanvas()->get_window()->set_cursor(_cursor);
+ }
+
+ if (event->button.button == 2 && rotating) {
+ rotating = false;
+ ungrabCanvasEvents();
+ }
+
+ if (middle_mouse_zoom && within_tolerance && (panning || zoom_rb)) {
+ zoom_rb = 0;
+
+ if (panning) {
+ panning = PANNING_NONE;
+ ungrabCanvasEvents();
+ }
+
+ auto const event_w = Geom::Point(event->button.x, event->button.y);
+ auto const event_dt = _desktop->w2d(event_w);
+
+ double const zoom_inc = prefs->getDoubleLimited("/options/zoomincrement/value", M_SQRT2, 1.01, 10);
+
+ _desktop->zoom_relative(event_dt, (event->button.state & GDK_SHIFT_MASK) ? 1 / zoom_inc : zoom_inc);
+ ret = true;
+ } else if (panning == event->button.button) {
+ panning = PANNING_NONE;
+ ungrabCanvasEvents();
+
+ // 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)
+ auto const motion_w = Geom::Point(event->button.x, event->button.y);
+ auto const moved_w = motion_w - button_w;
+
+ _desktop->scroll_relative(moved_w);
+ 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. Tab is hardcoded. When actions are triggered by tab,
+ // we end up stealing events from GTK widgets.
+ case GDK_KEY_F1:
+ ret = Inkscape::Shortcuts::getInstance().invoke_action(&event->key);
+ break;
+ case GDK_KEY_Tab:
+ sp_selection_item_next(_desktop);
+ ret = true;
+ break;
+ case GDK_KEY_ISO_Left_Tab:
+ sp_selection_item_prev(_desktop);
+ ret = true;
+ break;
+
+ // TODO: make these keys customizable
+ case GDK_KEY_F:
+ case GDK_KEY_f:
+ if (!MOD__SHIFT(event) && !MOD__CTRL(event) && !MOD__ALT(event)) {
+ _desktop->quick_preview(true);
+ ret = true;
+ }
+ 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 = std::floor(key_scroll * accelerate_scroll(event, acceleration));
+
+ gobble_key_events(get_latin_keyval(&event->key), GDK_CONTROL_MASK);
+ _desktop->scroll_relative(Geom::Point(i, 0));
+ } else if (!_keyboardMove(event->key, Geom::Point(-1, 0))) {
+ Inkscape::Shortcuts::getInstance().invoke_action(&event->key);
+ }
+ ret = true;
+ break;
+
+ case GDK_KEY_Up: // Ctrl Up
+ case GDK_KEY_KP_Up:
+ case GDK_KEY_KP_8:
+ if (MOD__CTRL_ONLY(event)) {
+ int i = std::floor(key_scroll * accelerate_scroll(event, acceleration));
+
+ gobble_key_events(get_latin_keyval(&event->key), GDK_CONTROL_MASK);
+ _desktop->scroll_relative(Geom::Point(0, i));
+ } else if (!_keyboardMove(event->key, Geom::Point(0, -_desktop->yaxisdir()))) {
+ Inkscape::Shortcuts::getInstance().invoke_action(&event->key);
+ }
+ ret = true;
+ break;
+
+ case GDK_KEY_Right: // Ctrl Right
+ case GDK_KEY_KP_Right:
+ case GDK_KEY_KP_6:
+ if (MOD__CTRL_ONLY(event)) {
+ int i = std::floor(key_scroll * accelerate_scroll(event, acceleration));
+
+ gobble_key_events(get_latin_keyval(&event->key), GDK_CONTROL_MASK);
+ _desktop->scroll_relative(Geom::Point(-i, 0));
+ } else if (!_keyboardMove(event->key, Geom::Point(1, 0))) {
+ Inkscape::Shortcuts::getInstance().invoke_action(&event->key);
+ }
+ ret = true;
+ break;
+
+ case GDK_KEY_Down: // Ctrl Down
+ case GDK_KEY_KP_Down:
+ case GDK_KEY_KP_2:
+ if (MOD__CTRL_ONLY(event)) {
+ int i = std::floor(key_scroll * accelerate_scroll(event, acceleration));
+
+ gobble_key_events(get_latin_keyval(&event->key), GDK_CONTROL_MASK);
+ _desktop->scroll_relative(Geom::Point(0, -i));
+ } else if (!_keyboardMove(event->key, Geom::Point(0, _desktop->yaxisdir()))) {
+ Inkscape::Shortcuts::getInstance().invoke_action(&event->key);
+ }
+ ret = true;
+ break;
+
+ case GDK_KEY_Menu:
+ menu_popup(event);
+ ret = true;
+ break;
+
+ case GDK_KEY_F10:
+ if (MOD__SHIFT_ONLY(event)) {
+ menu_popup(event);
+ ret = true;
+ }
+ break;
+
+ case GDK_KEY_space:
+ within_tolerance = true;
+ xp = yp = 0;
+ if (!allow_panning) break;
+ panning = PANNING_SPACE;
+ 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 (is_space_panning()) {
+ message_context->clear();
+ }
+
+ if (panning) {
+ panning = PANNING_NONE;
+ xp = yp = 0;
+
+ ungrabCanvasEvents();
+ }
+
+ if (panning_cursor == 1) {
+ panning_cursor = 0;
+ _desktop->getCanvas()->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;
+
+ // TODO: make these keys customizable
+ case GDK_KEY_F:
+ case GDK_KEY_f:
+ _desktop->quick_preview(false);
+ ret = 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: {
+ int constexpr WHEEL_SCROLL_DEFAULT = 40;
+
+ // previously we did two wheel_scrolls for each mouse scroll
+ int const wheel_scroll = prefs->getIntLimited( "/options/wheelscroll/value", WHEEL_SCROLL_DEFAULT, 0, 1000) * 2;
+
+ // Size of smooth-scrolls (only used in GTK+ 3)
+ double delta_x = 0;
+ double delta_y = 0;
+
+ using Modifiers::Type;
+ using Modifiers::Triggers;
+ Type action = Modifiers::Modifier::which(Triggers::CANVAS | Triggers::SCROLL, event->scroll.state);
+
+ if (action == Type::CANVAS_ROTATE && !_desktop->get_rotation_lock()) {
+ 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 = std::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) {
+ auto const scroll_dt = _desktop->point();
+ _desktop->rotate_relative_keep_point(scroll_dt, rotate_inc);
+ ret = true;
+ }
+
+ } else if (action == Type::CANVAS_PAN_X) {
+ /* 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));
+ ret = true;
+ break;
+
+ case GDK_SCROLL_DOWN:
+ case GDK_SCROLL_RIGHT:
+ _desktop->scroll_relative(Geom::Point(-wheel_scroll, 0));
+ ret = true;
+ 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));
+ ret = true;
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ } else if (action == Type::CANVAS_ZOOM) {
+ /* 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 = std::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) {
+ auto scroll_dt = _desktop->point();
+ _desktop->zoom_relative(scroll_dt, rel_zoom);
+ ret = true;
+ }
+
+ /* no modifier, pan up--down (left--right on multiwheel mice?) */
+ } else if (action == Type::CANVAS_PAN_Y) {
+ 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;
+ }
+ ret = true;
+ } else {
+ g_warning("unhandled scroll event with scroll.state=0x%x", event->scroll.state);
+ }
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ return ret;
+}
+
+/**
+ * This function allows to handle global tool events if _pre function is not fully overridden.
+ */
+void ToolBase::set_on_buttons(GdkEvent *event)
+{
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ switch (event->button.button) {
+ case 1:
+ _button1on = true;
+ break;
+ case 2:
+ _button2on = true;
+ break;
+ case 3:
+ _button3on = true;
+ break;
+ }
+ break;
+ case GDK_BUTTON_RELEASE:
+ switch (event->button.button) {
+ case 1:
+ _button1on = false;
+ break;
+ case 2:
+ _button2on = false;
+ break;
+ case 3:
+ _button3on = false;
+ break;
+ }
+ break;
+ case GDK_MOTION_NOTIFY:
+ _button1on = event->motion.state & Gdk::ModifierType::BUTTON1_MASK;
+ _button2on = event->motion.state & Gdk::ModifierType::BUTTON2_MASK;
+ _button3on = event->motion.state & Gdk::ModifierType::BUTTON3_MASK;
+ break;
+ }
+}
+
+bool ToolBase::are_buttons_1_and_3_on() const
+{
+ return _button1on && _button3on;
+}
+
+bool ToolBase::are_buttons_1_and_3_on(GdkEvent *event)
+{
+ set_on_buttons(event);
+ return are_buttons_1_and_3_on();
+}
+
+/**
+ * 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)
+{
+ bool ret = false;
+
+ if (event->type == GDK_BUTTON_PRESS) {
+ if (!are_buttons_1_and_3_on(event) && event->button.button == 3 &&
+ !((event->button.state & GDK_SHIFT_MASK) || (event->button.state & GDK_CONTROL_MASK))) {
+ menu_popup(event);
+ ret = true;
+ } else if (event->button.button == 1 && shape_editor && shape_editor->has_knotholder()) {
+ // This allows users to select an arbitary position in a pattern to edit on canvas.
+ auto knotholder = shape_editor->knotholder;
+ auto point = Geom::Point(event->button.x, event->button.y);
+ if (_desktop->getItemAtPoint(point, true) == knotholder->getItem()) {
+ ret = knotholder->set_item_clickpos(_desktop->w2d(point) * _desktop->dt2doc());
+ }
+ }
+ }
+
+ 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 (shape_editor) {
+ return 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;
+}
+
+/**
+ * Return true if there is a gradient drag.
+ */
+bool ToolBase::hasGradientDrag() const
+{
+ return _grdrag && _grdrag->isNonEmpty();
+}
+
+/**
+ * Grab events from the Canvas Catchall. (Common configuration.)
+ */
+void ToolBase::grabCanvasEvents(Gdk::EventMask mask)
+{
+ _desktop->getCanvasCatchall()->grab(mask); // Cursor is null.
+}
+
+/**
+ * Ungrab events from the Canvas Catchall. (Common configuration.)
+ */
+void ToolBase::ungrabCanvasEvents()
+{
+ _desktop->snapindicator->remove_snaptarget();
+ _desktop->getCanvasCatchall()->ungrab();
+}
+
+/** 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)
+{
+ if (auto window = _desktop->getToplevel()->get_window()) {
+ window->set_event_compression(!high_precision);
+ }
+}
+
+Geom::Point ToolBase::setup_for_drag_start(GdkEvent *ev)
+{
+ xp = ev->button.x;
+ yp = ev->button.y;
+ within_tolerance = true;
+
+ auto const p = Geom::Point(ev->button.x, ev->button.y);
+ item_to_select = Inkscape::UI::Tools::sp_event_context_find_item(_desktop, p, ev->button.state & GDK_MOD1_MASK, true);
+ return _desktop->w2d(p);
+}
+
+/**
+ * Calls virtual set() function of ToolBase.
+ */
+void sp_event_context_read(ToolBase *ec, char const *key)
+{
+ if (!ec || !key) return;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Inkscape::Preferences::Entry val = prefs->getEntry(ec->getPrefsPath() + '/' + key);
+ ec->set(val);
+}
+
+/**
+ * Handles snapping events for all tools and then passes to tool_root_handler.
+ */
+gint ToolBase::start_root_handler(GdkEvent *event)
+{
+#ifdef EVENT_DEBUG
+ ui_dump_event(reinterpret_cast<GdkEvent *>(event), "ToolBase::start_root_handler");
+#endif
+
+ if (!_uses_snap) {
+ return tool_root_handler(event);
+ }
+
+ switch (event->type) {
+ case GDK_MOTION_NOTIFY:
+ snap_delay_handler(nullptr, nullptr, reinterpret_cast<GdkEventMotion*>(event),
+ DelayedSnapEvent::EVENTCONTEXT_ROOT_HANDLER);
+ break;
+ case GDK_BUTTON_RELEASE:
+ // If we have any pending snapping action, then invoke it now
+ process_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.
+ _desktop->namedview->snap_manager.snapprefs.setSnapPostponedGlobally(false);
+ break;
+ default:
+ break;
+ }
+
+ return tool_root_handler(event);
+}
+
+/**
+ * Calls the right tool's event handler, depending on the selected tool and state.
+ */
+gint ToolBase::tool_root_handler(GdkEvent *event)
+{
+#ifdef EVENT_DEBUG
+ ui_dump_event(reinterpret_cast<GdkEvent *>(event), "tool_root_handler");
+#endif
+ gint ret = 0;
+
+ // Just set the on buttons for now. later, behave as intended.
+ set_on_buttons(event);
+
+ // refresh coordinates UI here while 'event' is still valid
+ set_event_location(_desktop, event);
+
+ // Panning has priority over tool-specific event handling
+ if (is_panning()) {
+ ret = ToolBase::root_handler(event);
+ } else {
+ ret = root_handler(event);
+ }
+
+ // at this point 'event' could be deleted already (after ctrl+w document close)
+
+ return ret;
+}
+
+/**
+ * Starts handling item snapping and pass to virtual_item_handler afterwards.
+ */
+gint ToolBase::start_item_handler(SPItem *item, GdkEvent *event)
+{
+ if (!_uses_snap) {
+ return virtual_item_handler(item, event);
+ }
+
+ switch (event->type) {
+ case GDK_MOTION_NOTIFY:
+ snap_delay_handler(item, nullptr, reinterpret_cast<GdkEventMotion*>(event),
+ DelayedSnapEvent::EVENTCONTEXT_ITEM_HANDLER);
+ break;
+ case GDK_BUTTON_RELEASE:
+ // If we have any pending snapping action, then invoke it now
+ process_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.
+ _desktop->namedview->snap_manager.snapprefs.setSnapPostponedGlobally(false);
+ break;
+ default:
+ break;
+ }
+
+ return this->virtual_item_handler(item, event);
+}
+
+gint ToolBase::virtual_item_handler(SPItem *item, GdkEvent *event)
+{
+ gint ret = false;
+
+ // Just set the on buttons for now. later, behave as intended.
+ set_on_buttons(event);
+
+ // Panning has priority over tool-specific event handling
+ if (is_panning()) {
+ ret = ToolBase::item_handler(item, event);
+ } else {
+ ret = item_handler(item, event);
+ }
+
+ if (!ret) {
+ ret = tool_root_handler(event);
+ } else {
+ set_event_location(_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;
+ }
+
+ auto const button_w = Geom::Point(event->button.x, event->button.y);
+ auto const button_dt = desktop->w2d(button_w);
+ desktop->set_coordinate_status(button_dt);
+}
+
+//-------------------------------------------------------------------
+/**
+ * Create popup menu and tell Gtk to show it.
+ */
+void ToolBase::menu_popup(GdkEvent *event, SPObject *obj)
+{
+
+ if (!obj) {
+ if (event->type == GDK_KEY_PRESS && !_desktop->getSelection()->isEmpty()) {
+ obj = _desktop->getSelection()->items().front();
+ } else {
+ // 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
+ auto p = Geom::Point(event->button.x, event->button.y);
+ obj = sp_event_context_find_item (_desktop, p, false, false);
+ }
+ }
+
+ auto menu = new ContextMenu(_desktop, obj);
+ menu->attach_to_widget(*_desktop->getCanvas()); // So actions work!
+ menu->show();
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ case GDK_KEY_PRESS:
+ menu->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, char const *ctrl_tip, char const *shift_tip,
+ char 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);
+
+ char *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 (std::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;
+ latin_keys_groups.clear();
+
+ if (gdk_keymap_get_entries_for_keyval(Gdk::Display::get_default()->get_keymap(), GDK_KEY_a, &keys, &n_keys)) {
+ for (int i = 0; i < n_keys; i++) {
+ latin_keys_groups.insert(keys[i].group);
+
+ 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), nullptr);
+ 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 /*= nullptr*/)
+{
+ guint keyval = 0;
+ GdkModifierType modifiers;
+ gint group = latin_keys_group_valid ? latin_keys_group : event->group;
+
+ if (latin_keys_groups.count(event->group)) {
+ // Keyboard group is a latin layout, so just use it.
+ 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) {
+ *consumed_modifiers = modifiers;
+ }
+#ifndef __APPLE__
+ // on macOS <option> key inserts special characters and below condition fires all the time
+ if (keyval != event->keyval) {
+ std::cerr << "get_latin_keyval: OH OH OH keyval did change! "
+ << " keyval: " << keyval << " (" << (char)keyval << ")"
+ << " event->keyval: " << event->keyval << "(" << (char)event->keyval << ")" << std::endl;
+ }
+#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->getSelection()->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) { // 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 timeout).
+ *
+ * @param item Pointer that store a reference to a canvas or to an item.
+ * @param 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 ToolBase::snap_delay_handler(gpointer item, gpointer item2, GdkEventMotion const *event, DelayedSnapEvent::DelayedSnapEventOrigin origin)
+{
+ static guint32 prev_time;
+ static std::optional<Geom::Point> prev_pos;
+
+ if (!_uses_snap || _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 = dynamic_cast<Inkscape::UI::Tools::CalligraphicTool*>(this);
+ // 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 = is_panning(); // Don't snap while panning
+
+ 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
+ discard_delayed_snap_event();
+ } else if (getDesktop() && getDesktop()->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.
+ getDesktop()->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;
+ double 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)
+ _dse.emplace(this, item, item2, event, origin);
+ _schedule_delayed_snap_event(); // 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 (!_dse) { // 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
+ _dse.emplace(this, item, item2, event, origin);
+ _schedule_delayed_snap_event();
+ } // 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(!_dse);
+ _dse.emplace(this, item, item2, event, origin);
+ _schedule_delayed_snap_event();
+ }
+
+ prev_pos = event_pos;
+ prev_time = event_t;
+ }
+}
+
+/**
+ * When the delayed snap event timer expires, this method will be called and will re-inject the last motion
+ * event in an appropriate place, with snapping being turned on again.
+ */
+void ToolBase::process_delayed_snap_event()
+{
+ // Snap NOW! For this the "postponed" flag will be reset and the last motion event will be repeated
+
+ _dse_timeout_conn.disconnect();
+
+ if (!_dse) {
+ // This might occur when this method is called directly, i.e. not through the timer
+ // E.g. on GDK_BUTTON_RELEASE in start_root_handler()
+ return;
+ }
+
+ auto dt = getDesktop();
+ if (!dt) {
+ _dse.reset();
+ return;
+ }
+
+ _dse_callback_in_process = true;
+ dt->namedview->snap_manager.snapprefs.setSnapPostponedGlobally(false);
+
+ // Depending on where the delayed snap event originated from, we will inject it back at its origin.
+ // The switch below takes care of that and prepares the relevant parameters.
+ switch (_dse->getOrigin()) {
+ case DelayedSnapEvent::EVENTCONTEXT_ROOT_HANDLER:
+ tool_root_handler(_dse->getEvent());
+ break;
+ case DelayedSnapEvent::EVENTCONTEXT_ITEM_HANDLER: {
+ auto item = reinterpret_cast<SPItem*>(_dse->getItem());
+ if (item) {
+ virtual_item_handler(item, _dse->getEvent());
+ }
+ break;
+ }
+ case DelayedSnapEvent::KNOT_HANDLER: {
+ auto knot = reinterpret_cast<SPKnot*>(_dse->getItem2());
+ check_if_knot_deleted(knot);
+ if (knot) {
+ bool was_grabbed = knot->is_grabbed();
+ knot->setFlag(SP_KNOT_GRABBED, true); // Must be grabbed for Inkscape::SelTrans::handleRequest() to pass
+ sp_knot_handler_request_position(_dse->getEvent(), knot);
+ knot->setFlag(SP_KNOT_GRABBED, was_grabbed);
+ }
+ break;
+ }
+ case DelayedSnapEvent::CONTROL_POINT_HANDLER: {
+ using Inkscape::UI::ControlPoint;
+ auto point = reinterpret_cast<ControlPoint*>(_dse->getItem2());
+ if (point) {
+ if (point->position().isFinite() && dt == point->_desktop) {
+ point->_eventHandler(this, _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: {
+ auto guideline = reinterpret_cast<CanvasItemGuideLine*>(_dse->getItem());
+ auto guide = reinterpret_cast<SPGuide*> (_dse->getItem2());
+ if (guideline && guide) {
+ sp_dt_guide_event(_dse->getEvent(), guideline, guide);
+ }
+ break;
+ }
+ case DelayedSnapEvent::GUIDE_HRULER:
+ case DelayedSnapEvent::GUIDE_VRULER: {
+ gpointer item = _dse->getItem();
+ auto widget = reinterpret_cast<Gtk::Widget*>(_dse->getItem2());
+ if (item && widget) {
+ g_assert(GTK_IS_WIDGET(item));
+ bool horiz = _dse->getOrigin() == DelayedSnapEvent::GUIDE_HRULER;
+ SPDesktopWidget::ruler_event(GTK_WIDGET(item), _dse->getEvent(), SP_DESKTOP_WIDGET(widget), horiz);
+ }
+ break;
+ }
+ default:
+ g_warning("Origin of snap-delay event has not been defined!");
+ break;
+ }
+
+ _dse_callback_in_process = false;
+ _dse.reset();
+}
+
+/**
+ * If a delayed snap event has been scheduled, this function will cancel it.
+ */
+void ToolBase::discard_delayed_snap_event()
+{
+ _dse_timeout_conn.disconnect();
+ _desktop->namedview->snap_manager.snapprefs.setSnapPostponedGlobally(false);
+ _dse.reset();
+}
+
+/**
+ * Internal function used to set process_delayed_snap_event() to occur a given delay in the future
+ * from now. Subsequent calls will reset the timer. Calling process_delayed_snap_event() manually
+ * will cancel the timer.
+ */
+void ToolBase::_schedule_delayed_snap_event()
+{
+ // Get timeout value in seconds.
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double value = prefs->getDoubleLimited("/options/snapdelay/value", 0, 0, 1000);
+
+ // If the timeout value is too large, we assume it comes from an old preferences file
+ // where it used to be measured in milliseconds, and convert it appropriately.
+ if (value > 1.0) {
+ value /= 1000.0; // convert milliseconds to seconds
+ }
+
+ _dse_timeout_conn.disconnect();
+ _dse_timeout_conn = Glib::signal_timeout().connect([this] {
+ process_delayed_snap_event();
+ return false; // one-shot
+ }, value * 1000.0);
+}
+
+} // namespace Tools
+} // 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/tools/tool-base.h b/src/ui/tools/tool-base.h
new file mode 100644
index 0000000..aaf0b9a
--- /dev/null
+++ b/src/ui/tools/tool-base.h
@@ -0,0 +1,262 @@
+// 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 <optional>
+
+#include <boost/noncopyable.hpp>
+#include <gdkmm/device.h> // EventMask
+#include <gdkmm/cursor.h>
+#include <glib-object.h>
+#include <sigc++/trackable.h>
+
+#include <2geom/point.h>
+
+#include "preferences.h"
+
+class GrDrag;
+class SPDesktop;
+class SPObject;
+class SPItem;
+class SPGroup;
+class KnotHolder;
+
+namespace Inkscape {
+class MessageContext;
+class SelCue;
+
+namespace UI {
+class ShapeEditor;
+
+namespace Tools {
+class ToolBase;
+
+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 *tool, gpointer item, gpointer item2, GdkEventMotion const *event,
+ DelayedSnapEvent::DelayedSnapEventOrigin origin)
+ : _tool(tool)
+ , _item(item)
+ , _item2(item2)
+ , _origin(origin)
+ {
+ _event = gdk_event_copy(reinterpret_cast<GdkEvent const*>(event));
+ _event->motion.time = GDK_CURRENT_TIME;
+ }
+
+ ~DelayedSnapEvent()
+ {
+ gdk_event_free(_event);
+ }
+
+ ToolBase *getEventContext() const { return _tool; }
+ gpointer getItem() const { return _item; }
+ gpointer getItem2() const { return _item2; }
+ GdkEvent *getEvent() const { return _event; }
+ DelayedSnapEventOrigin getOrigin() const { return _origin; }
+
+private:
+ ToolBase *_tool;
+ gpointer _item;
+ gpointer _item2;
+ GdkEvent *_event;
+ 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
+ , boost::noncopyable
+{
+public:
+ ToolBase(SPDesktop *desktop, std::string prefs_path, std::string cursor_filename, bool uses_snap = true);
+ virtual ~ToolBase();
+
+ virtual void set(const Inkscape::Preferences::Entry &val);
+ virtual bool root_handler(GdkEvent *event);
+ virtual bool item_handler(SPItem *item, GdkEvent *event);
+ virtual void menu_popup(GdkEvent *event, SPObject *obj = nullptr);
+ virtual bool catch_undo(bool redo = false) { return false; }
+ virtual bool can_undo(bool redo = false) { return false; }
+ virtual bool is_ready() const { return true; }
+
+ void set_on_buttons(GdkEvent *event);
+ bool are_buttons_1_and_3_on() const;
+ bool are_buttons_1_and_3_on(GdkEvent *event);
+
+ std::string const &getPrefsPath() const { return _prefs_path; };
+ void enableSelectionCue(bool enable = true);
+
+ Inkscape::MessageContext *defaultMessageContext() const { return message_context.get(); }
+
+ SPDesktop *getDesktop() const { return _desktop; }
+ SPGroup *currentLayer() const;
+
+ // Commonly used CanvasItemCatchall grab/ungrab.
+ void grabCanvasEvents(Gdk::EventMask mask =
+ Gdk::KEY_PRESS_MASK |
+ Gdk::BUTTON_RELEASE_MASK |
+ Gdk::POINTER_MOTION_MASK |
+ Gdk::BUTTON_PRESS_MASK);
+ void ungrabCanvasEvents();
+
+ virtual void switching_away(const std::string &new_tool) {}
+private:
+ std::unique_ptr<Inkscape::Preferences::PreferencesObserver> pref_observer;
+ std::string _prefs_path;
+
+protected:
+ Glib::RefPtr<Gdk::Cursor> _cursor;
+ std::string _cursor_filename = "select.svg";
+ std::string _cursor_default = "select.svg";
+
+ int xp = 0; ///< where drag started
+ int yp = 0; ///< where drag started
+ int tolerance = 0;
+ bool within_tolerance = false; ///< are we still within tolerance of origin
+ bool _button1on = false;
+ bool _button2on = false;
+ bool _button3on = false;
+ SPItem *item_to_select = nullptr; ///< the item where mouse_press occurred, to
+ ///< be selected if this is a click not drag
+
+ Geom::Point setup_for_drag_start(GdkEvent *ev);
+
+private:
+ enum
+ {
+ PANNING_NONE = 0, //
+ PANNING_SPACE_BUTTON1 = 1, // TODO is this mode relevant?
+ PANNING_BUTTON2 = 2, //
+ PANNING_BUTTON3 = 3, //
+ PANNING_SPACE = 4
+ } panning = PANNING_NONE;
+
+ bool rotating = false;
+ double start_angle, current_angle;
+
+public:
+ gint start_root_handler(GdkEvent *event);
+ gint tool_root_handler(GdkEvent *event);
+ gint start_item_handler(SPItem *item, GdkEvent *event);
+ gint virtual_item_handler(SPItem *item, GdkEvent *event);
+
+ /// True if we're panning with any method (space bar, middle-mouse, right-mouse+Ctrl)
+ bool is_panning() const { return panning != 0; }
+
+ /// True if we're panning with the space bar
+ bool is_space_panning() const { return panning == PANNING_SPACE || panning == PANNING_SPACE_BUTTON1; }
+
+ std::unique_ptr<Inkscape::MessageContext> message_context;
+ Inkscape::SelCue *_selcue = nullptr;
+
+ GrDrag *_grdrag = nullptr;
+
+ ShapeEditor *shape_editor = nullptr;
+
+ void snap_delay_handler(gpointer item, gpointer item2, GdkEventMotion const *event, DelayedSnapEvent::DelayedSnapEventOrigin origin);
+ void process_delayed_snap_event();
+ void discard_delayed_snap_event();
+ bool _uses_snap = false;
+
+ void set_cursor(std::string filename);
+ void use_cursor(Glib::RefPtr<Gdk::Cursor> cursor);
+ Glib::RefPtr<Gdk::Cursor> get_cursor(Glib::RefPtr<Gdk::Window> window, std::string const &filename) const;
+ void use_tool_cursor();
+
+ void enableGrDrag(bool enable = true);
+ bool deleteSelectedDrag(bool just_one);
+ bool hasGradientDrag() const;
+ GrDrag *get_drag() { return _grdrag; }
+
+protected:
+ bool sp_event_context_knot_mouseover() const;
+
+ void set_high_motion_precision(bool high_precision = true);
+
+ SPDesktop *_desktop = nullptr;
+
+private:
+ bool _keyboardMove(GdkEventKey const &event, Geom::Point const &dir);
+
+ std::optional<DelayedSnapEvent> _dse;
+ void _schedule_delayed_snap_event();
+ sigc::connection _dse_timeout_conn;
+ bool _dse_callback_in_process = false;
+};
+
+void sp_event_context_read(ToolBase *ec, char const *key);
+
+void sp_event_root_menu_popup(SPDesktop *desktop, SPItem *item, GdkEvent *event);
+
+gint gobble_key_events(guint keyval, guint mask);
+void gobble_motion_events(guint mask);
+
+void sp_event_show_modifier_tip(Inkscape::MessageContext *message_context, GdkEvent *event,
+ char const *ctrl_tip, char const *shift_tip, char const *alt_tip);
+
+void init_latin_keys_group();
+unsigned get_latin_keyval(GdkEventKey const *event, unsigned *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..808a4b4
--- /dev/null
+++ b/src/ui/tools/tweak-tool.cpp
@@ -0,0 +1,1482 @@
+// 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 "tweak-tool.h"
+
+#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 "style.h"
+
+#include "display/curve.h"
+#include "display/control/canvas-item-bpath.h"
+
+#include "livarot/Path.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 "path/path-util.h"
+
+#include "svg/svg.h"
+
+#include "ui/icon-names.h"
+#include "ui/toolbar/tweak-toolbar.h"
+
+
+using Inkscape::DocumentUndo;
+
+#define DDC_RED_RGBA 0xff0000ff
+
+#define DYNA_MIN_WIDTH 1.0e-6
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+TweakTool::TweakTool(SPDesktop *desktop)
+ : ToolBase(desktop, "/tools/tweak", "tweak-push.svg")
+ , 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)
+ , do_h(true)
+ , do_s(true)
+ , do_l(true)
+ , do_o(false)
+{
+ dilate_area = make_canvasitem<CanvasItemBpath>(desktop->getCanvasSketch());
+ dilate_area->set_stroke(0xff9900ff);
+ dilate_area->set_fill(0x0, SP_WIND_RULE_EVENODD);
+ dilate_area->hide();
+
+ 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");
+
+ style_set_connection = 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();
+ }
+}
+
+TweakTool::~TweakTool()
+{
+ enableGrDrag(false);
+}
+
+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->getSelection()->isEmpty()) {
+ num = (guint)boost::distance(_desktop->getSelection()->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->set_cursor("tweak-move.svg");
+ 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->set_cursor("tweak-move-out.svg");
+ } else {
+ this->set_cursor("tweak-move-in.svg");
+ }
+ 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->set_cursor("tweak-move-jitter.svg");
+ 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->set_cursor("tweak-scale-up.svg");
+ } else {
+ this->set_cursor("tweak-scale-down.svg");
+ }
+ 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->set_cursor("tweak-rotate-counterclockwise.svg");
+ } else {
+ this->set_cursor("tweak-rotate-clockwise.svg");
+ }
+ 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->set_cursor("tweak-less.svg");
+ } else {
+ this->set_cursor("tweak-more.svg");
+ }
+ break;
+ case TWEAK_MODE_PUSH:
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag to <b>push paths</b>."), sel_message);
+ this->set_cursor("tweak-push.svg");
+ 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->set_cursor("tweak-outset.svg");
+ } else {
+ this->set_cursor("tweak-inset.svg");
+ }
+ 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->set_cursor("tweak-repel.svg");
+ } else {
+ this->set_cursor("tweak-attract.svg");
+ }
+ break;
+ case TWEAK_MODE_ROUGHEN:
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>roughen paths</b>."), sel_message);
+ this->set_cursor("tweak-roughen.svg");
+ 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->set_cursor("tweak-color.svg");
+ break;
+ case TWEAK_MODE_COLORJITTER:
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>randomize colors</b>."), sel_message);
+ this->set_cursor("tweak-color.svg");
+ 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->set_cursor("tweak-color.svg");
+ break;
+ }
+ 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::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/tc->getDesktop()->current_zoom();
+}
+
+static double
+get_path_force (TweakTool *tc)
+{
+ double force = 8 * (tc->usepressure? tc->pressure : TC_DEFAULT_PRESSURE)
+ /sqrt(tc->getDesktop()->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;
+
+ {
+ auto box = cast<SPBox3D>(item);
+ if (box && !is_transform_mode(mode) && !is_color_mode(mode)) {
+ // convert 3D boxes to ordinary groups before tweaking their shapes
+ item = box->convert_to_group();
+ selection->add(item);
+ }
+ }
+
+ if (is<SPText>(item) || is<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 = cast<SPItem>(newObj);
+ g_assert(item != nullptr);
+ selection->add(item);
+ }
+
+ if (is<SPGroup>(item) && !is<SPBox3D>(item)) {
+ std::vector<SPItem *> children;
+ for (auto& child: item->children) {
+ if (is<SPItem>(&child)) {
+ children.push_back(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 (is<SPPath>(item) || is<SPShape>(item)) {
+
+ Inkscape::XML::Node *newrepr = nullptr;
+ gint pos = 0;
+ Inkscape::XML::Node *parent = nullptr;
+ char const *id = nullptr;
+ if (!is<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 {
+ auto lpeitem = 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) {
+ 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
+
+ auto lg = cast<SPLinearGradient>(gradient);
+ auto rg = 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) {
+ auto stop = cast<SPStop>(&child);
+ if (!stop) {
+ continue;
+ }
+
+ offset_h = stop->offset;
+
+ if (child_prev) {
+ auto prevStop = 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
+ auto mg = cast<SPMeshGradient>(gradient);
+ if (mg) {
+ auto mg_array = 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 (is<SPGroup>(item)) {
+ for (auto& child: item->children) {
+ auto childItem = 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) {
+ auto primitive = cast<SPFilterPrimitive>(&primitive_obj);
+ if (primitive) {
+ //if primitive is gaussianblur
+ auto spblur = cast<SPGaussianBlur>(primitive);
+ if (spblur) {
+ float num = spblur->get_std_deviation().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)
+{
+ SPDesktop *desktop = tc->getDesktop();
+ Inkscape::Selection *selection = desktop->getSelection();
+
+ if (selection->isEmpty()) {
+ return false;
+ }
+
+ bool did = false;
+ double radius = get_dilate_radius(tc);
+
+ SPItem *item_at_point = tc->getDesktop()->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(tc->getDesktop()->point()));
+
+ Geom::PathVector path = Geom::Path(Geom::Circle(0,0,1)); // Unit circle centered at origin.
+ path *= sm;
+ tc->dilate_area->set_bpath(path);
+ tc->dilate_area->show();
+}
+
+ static void
+sp_tweak_switch_mode (TweakTool *tc, gint mode, bool with_shift)
+{
+ auto tb = dynamic_cast<UI::Toolbar::TweakToolbar*>(tc->getDesktop()->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*>(tc->getDesktop()->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:
+ dilate_area->show();
+ break;
+ case GDK_LEAVE_NOTIFY:
+ dilate_area->hide();
+ break;
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 1) {
+ 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);
+
+ 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)));
+ Geom::PathVector path = Geom::Path(Geom::Circle(0,0,1)); // Unit circle centered at origin.
+ path *= sm;
+ dilate_area->set_bpath(path);
+ dilate_area->show();
+
+ guint num = 0;
+ if (!_desktop->getSelection()->isEmpty()) {
+ num = (guint)boost::distance(_desktop->getSelection()->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));
+
+ this->is_drawing = false;
+
+ if (this->is_dilating && event->button.button == 1) {
+ 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;
+ Glib::ustring text;
+ switch (this->mode) {
+ case TWEAK_MODE_MOVE:
+ text = _("Move tweak");
+ break;
+ case TWEAK_MODE_MOVE_IN_OUT:
+ text = _("Move in/out tweak");
+ break;
+ case TWEAK_MODE_MOVE_JITTER:
+ text = _("Move jitter tweak");
+ break;
+ case TWEAK_MODE_SCALE:
+ text = _("Scale tweak");
+ break;
+ case TWEAK_MODE_ROTATE:
+ text = _("Rotate tweak");
+ break;
+ case TWEAK_MODE_MORELESS:
+ text = _("Duplicate/delete tweak");
+ break;
+ case TWEAK_MODE_PUSH:
+ text = _("Push path tweak");
+ break;
+ case TWEAK_MODE_SHRINK_GROW:
+ text = _("Shrink/grow path tweak");
+ break;
+ case TWEAK_MODE_ATTRACT_REPEL:
+ text = _("Attract/repel path tweak");
+ break;
+ case TWEAK_MODE_ROUGHEN:
+ text = _("Roughen path tweak");
+ break;
+ case TWEAK_MODE_COLORPAINT:
+ text = _("Color paint tweak");
+ break;
+ case TWEAK_MODE_COLORJITTER:
+ text = _("Color jitter tweak");
+ break;
+ case TWEAK_MODE_BLUR:
+ text = _("Blur tweak");
+ break;
+ }
+ DocumentUndo::done(_desktop->getDocument(), text.c_str(), INKSCAPE_ICON("tool-tweak"));
+ }
+ 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..77bfb1f
--- /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 <2geom/point.h>
+#include "ui/tools/tool-base.h"
+#include "display/control/canvas-item-ptr.h"
+#include "helper/auto-connection.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 {
+
+class CanvasItemBpath;
+
+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(SPDesktop *desktop);
+ ~TweakTool() override;
+
+ /* extended input data */
+ double pressure;
+
+ /* attributes */
+ bool dragging; /* mouse state: mouse is dragging */
+ bool usepressure;
+ bool usetilt;
+
+ double width;
+ double force;
+ double fidelity;
+
+ int mode;
+
+ bool is_drawing;
+
+ bool is_dilating;
+ bool has_dilated;
+ Geom::Point last_push;
+ CanvasItemPtr<CanvasItemBpath> dilate_area;
+
+ bool do_h;
+ bool do_s;
+ bool do_l;
+ bool do_o;
+
+ auto_connection style_set_connection;
+
+ void set(const Inkscape::Preferences::Entry &val) override;
+ bool root_handler(GdkEvent *event) 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..dec3a52
--- /dev/null
+++ b/src/ui/tools/zoom-tool.cpp
@@ -0,0 +1,214 @@
+// 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 "zoom-tool.h"
+
+#include "desktop.h"
+#include "rubberband.h"
+#include "selection-chemistry.h"
+
+#include "include/macros.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+ZoomTool::ZoomTool(SPDesktop *desktop)
+ : ToolBase(desktop, "/tools/zoom", "zoom-in.svg")
+ , escaped(false)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (prefs->getBool("/tools/zoom/selcue")) {
+ this->enableSelectionCue();
+ }
+
+ if (prefs->getBool("/tools/zoom/gradientdrag")) {
+ this->enableGrDrag();
+ }
+}
+
+ZoomTool::~ZoomTool()
+{
+ this->enableGrDrag(false);
+ ungrabCanvasEvents();
+}
+
+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) {
+ // 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(button_dt, zoom_rel);
+ ret = true;
+ }
+
+ grabCanvasEvents(Gdk::KEY_PRESS_MASK |
+ Gdk::KEY_RELEASE_MASK |
+ Gdk::BUTTON_PRESS_MASK |
+ Gdk::BUTTON_RELEASE_MASK |
+ Gdk::POINTER_MOTION_MASK );
+ break;
+ }
+
+ case GDK_MOTION_NOTIFY:
+ if ((event->motion.state & GDK_BUTTON1_MASK)) {
+ 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) {
+ 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(button_dt, zoom_rel);
+ }
+
+ ret = true;
+ }
+
+ Inkscape::Rubberband::get(_desktop)->stop();
+
+ ungrabCanvasEvents();
+
+ 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->set_cursor("zoom-out.svg");
+ 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->set_cursor("zoom-in.svg");
+ 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..d7b97ad
--- /dev/null
+++ b/src/ui/tools/zoom-tool.h
@@ -0,0 +1,41 @@
+// 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(SPDesktop *desktop);
+ ~ZoomTool() override;
+
+ bool root_handler(GdkEvent *event) override;
+
+private:
+ bool escaped;
+};
+
+}
+}
+}
+
+#endif
diff --git a/src/ui/util.cpp b/src/ui/util.cpp
new file mode 100644
index 0000000..04a414a
--- /dev/null
+++ b/src/ui/util.cpp
@@ -0,0 +1,291 @@
+// 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"
+#include "inkscape.h"
+
+#include <cairomm/pattern.h>
+#include <cstdint>
+#include <gdkmm/rgba.h>
+#include <gtkmm.h>
+#include <gtkmm/cellrenderer.h>
+#include <gtkmm/enums.h>
+#include <stdexcept>
+#include <tuple>
+#if (defined (_WIN32) || defined (_WIN64))
+#include <gdk/gdkwin32.h>
+#include <dwmapi.h>
+/* For Windows 10 version 1809, 1903, 1909. */
+#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE_OLD
+#define DWMWA_USE_IMMERSIVE_DARK_MODE_OLD 19
+#endif
+/* For Windows 10 version 2004 and higher, and Windows 11. */
+#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
+#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
+#endif
+#endif
+
+// TODO due to internal breakage in glibmm headers, this must be last:
+#include <glibmm/i18n.h>
+#include "widgets/spw-utilities.h" // sp_traverse_widget_tree()
+
+/**
+ * Recursively look through pre-constructed widget parents for a specific named widget.
+ */
+Gtk::Widget *get_widget_by_name(Gtk::Container *parent, const std::string &name)
+{
+ for (auto child : parent->get_children()) {
+ if (name == child->get_name())
+ return child;
+ if (auto recurse = dynamic_cast<Gtk::Container *>(child)) {
+ if (auto decendant = get_widget_by_name(recurse, name))
+ return decendant;
+ }
+ }
+ return nullptr;
+}
+
+/*
+ * 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;
+}
+
+/**
+ * Show widget, if the widget has a Gtk::Reveal parent, reveal instead.
+ *
+ * @param widget - The child widget to show.
+ */
+void reveal_widget(Gtk::Widget *widget, bool show)
+{
+ auto revealer = dynamic_cast<Gtk::Revealer *>(widget->get_parent());
+ if (revealer) {
+ revealer->set_reveal_child(show);
+ }
+ if (show) {
+ widget->show();
+ } else if (!revealer) {
+ widget->hide();
+ }
+}
+
+
+bool is_widget_effectively_visible(Gtk::Widget const *widget) {
+ if (!widget) return false;
+
+ // TODO: what's the right way to determine if widget is visible on the screen?
+ return widget->get_child_visible();
+}
+
+namespace Inkscape::UI {
+
+/**
+ * Recursively set all the icon sizes inside this parent widget. Any GtkImage will be changed
+ * so only call this on widget stacks where all children have the same expected sizes.
+ *
+ * @param parent - The parent widget to traverse
+ * @param pixel_size - The new pixel size of the images it contains
+ */
+void set_icon_sizes(Gtk::Widget* parent, int pixel_size) {
+ sp_traverse_widget_tree(parent, [=](Gtk::Widget* widget) {
+ if (auto ico = dynamic_cast<Gtk::Image*>(widget)) {
+ ico->set_from_icon_name(ico->get_icon_name(), static_cast<Gtk::IconSize>(Gtk::ICON_SIZE_BUTTON));
+ ico->set_pixel_size(pixel_size);
+ }
+ return false;
+ });
+}
+void set_icon_sizes(GtkWidget* parent, int pixel_size) {
+ set_icon_sizes(Glib::wrap(parent), pixel_size);
+}
+
+void gui_warning(const std::string &msg, Gtk::Window *parent_window) {
+ g_warning("%s", msg.c_str());
+ if (INKSCAPE.active_desktop()) {
+ Gtk::MessageDialog warning(_(msg.c_str()), false, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_OK, true);
+ warning.set_transient_for( parent_window ? *parent_window : *(INKSCAPE.active_desktop()->getToplevel()) );
+ warning.run();
+ }
+}
+
+void resize_widget_children(Gtk::Widget *widget) {
+ if(widget) {
+ Gtk::Allocation allocation;
+ int baseline;
+ widget->get_allocated_size(allocation, baseline);
+ widget->size_allocate(allocation, baseline);
+ }
+}
+
+Gtk::StateFlags cell_flags_to_state_flags(Gtk::CellRendererState state)
+{
+ auto flags = Gtk::STATE_FLAG_NORMAL;
+
+ for (auto [s, f]: (std::tuple<Gtk::CellRendererState, Gtk::StateFlags>[]) {
+ {Gtk::CELL_RENDERER_SELECTED, Gtk::STATE_FLAG_SELECTED},
+ {Gtk::CELL_RENDERER_PRELIT, Gtk::STATE_FLAG_PRELIGHT},
+ {Gtk::CELL_RENDERER_INSENSITIVE, Gtk::STATE_FLAG_INSENSITIVE},
+ {Gtk::CELL_RENDERER_FOCUSED, Gtk::STATE_FLAG_FOCUSED},
+ })
+ {
+ if (state & s) {
+ flags |= f;
+ }
+ }
+
+ return flags;
+}
+
+} // namespace Inkscape::UI
+
+Gdk::RGBA mix_colors(const Gdk::RGBA& a, const Gdk::RGBA& b, float ratio) {
+ auto lerp = [](double v0, double v1, double t){ return (1.0 - t) * v0 + t * v1; };
+ Gdk::RGBA result;
+ result.set_rgba(
+ lerp(a.get_red(), b.get_red(), ratio),
+ lerp(a.get_green(), b.get_green(), ratio),
+ lerp(a.get_blue(), b.get_blue(), ratio),
+ lerp(a.get_alpha(), b.get_alpha(), ratio)
+ );
+ return result;
+}
+
+Gdk::RGBA get_background_color(const Glib::RefPtr<Gtk::StyleContext> &context,
+ Gtk::StateFlags state) {
+ return get_context_color(context, GTK_STYLE_PROPERTY_BACKGROUND_COLOR, state);
+}
+
+Gdk::RGBA get_context_color(const Glib::RefPtr<Gtk::StyleContext> &context,
+ const gchar *property,
+ Gtk::StateFlags state) {
+ GdkRGBA *c;
+ gtk_style_context_get(context->gobj(),
+ static_cast<GtkStateFlags>(state),
+ property, &c, nullptr);
+ return Glib::wrap(c);
+}
+
+// 2Geom <-> Cairo
+
+Cairo::RectangleInt geom_to_cairo(const Geom::IntRect &rect)
+{
+ return Cairo::RectangleInt{rect.left(), rect.top(), rect.width(), rect.height()};
+}
+
+Geom::IntRect cairo_to_geom(const Cairo::RectangleInt &rect)
+{
+ return Geom::IntRect::from_xywh(rect.x, rect.y, rect.width, rect.height);
+}
+
+Cairo::Matrix geom_to_cairo(const Geom::Affine &affine)
+{
+ return Cairo::Matrix(affine[0], affine[1], affine[2], affine[3], affine[4], affine[5]);
+}
+
+Geom::IntPoint dimensions(const Cairo::RefPtr<Cairo::ImageSurface> &surface)
+{
+ return Geom::IntPoint(surface->get_width(), surface->get_height());
+}
+
+Geom::IntPoint dimensions(const Gdk::Rectangle &allocation)
+{
+ return Geom::IntPoint(allocation.get_width(), allocation.get_height());
+}
+
+Cairo::RefPtr<Cairo::LinearGradient> create_cubic_gradient(
+ Geom::Rect rect,
+ const Gdk::RGBA& from,
+ const Gdk::RGBA& to,
+ Geom::Point ctrl1,
+ Geom::Point ctrl2,
+ Geom::Point p0,
+ Geom::Point p1,
+ int steps
+) {
+ // validate input points
+ for (auto&& pt : {p0, ctrl1, ctrl2, p1}) {
+ if (pt.x() < 0 || pt.x() > 1 ||
+ pt.y() < 0 || pt.y() > 1) {
+ throw std::invalid_argument("Invalid points for cubic gradient; 0..1 coordinates expected.");
+ }
+ }
+ if (steps < 2 || steps > 999) {
+ throw std::invalid_argument("Invalid number of steps for cubic gradient; 2 to 999 steps expected.");
+ }
+
+ auto g = Cairo::LinearGradient::create(rect.min().x(), rect.min().y(), rect.max().x(), rect.max().y());
+
+ --steps;
+ for (int step = 0; step <= steps; ++step) {
+ auto t = 1.0 * step / steps;
+ auto s = 1.0 - t;
+ auto p = (t * t * t) * p0 + (3 * t * t * s) * ctrl1 + (3 * t * s * s) * ctrl2 + (s * s * s) * p1;
+
+ auto offset = p.x();
+ auto ratio = p.y();
+
+ auto color = mix_colors(from, to, ratio);
+ g->add_color_stop_rgba(offset, color.get_red(), color.get_green(), color.get_blue(), color.get_alpha());
+ }
+
+ return g;
+}
+
+Gdk::RGBA change_alpha(const Gdk::RGBA& color, double new_alpha) {
+ auto copy(color);
+ copy.set_alpha(new_alpha);
+ return copy;
+}
+
+uint32_t conv_gdk_color_to_rgba(const Gdk::RGBA& color, double replace_alpha) {
+ auto alpha = replace_alpha >= 0 ? replace_alpha : color.get_alpha();
+ auto rgba =
+ uint32_t(0xff * color.get_red()) << 24 |
+ uint32_t(0xff * color.get_green()) << 16 |
+ uint32_t(0xff * color.get_blue()) << 8 |
+ uint32_t(0xff * alpha);
+ return rgba;
+}
+
+void set_dark_tittlebar(Glib::RefPtr<Gdk::Window> win, bool is_dark){
+#if (defined (_WIN32) || defined (_WIN64))
+ if (win->gobj()) {
+ BOOL w32_darkmode = is_dark;
+ HWND hwnd = (HWND)gdk_win32_window_get_handle((GdkWindow*)win->gobj());
+ if (DwmSetWindowAttribute) {
+ DWORD attr = DWMWA_USE_IMMERSIVE_DARK_MODE;
+ if (FAILED(DwmSetWindowAttribute(hwnd, attr, &w32_darkmode, sizeof(w32_darkmode)))) {
+ attr = DWMWA_USE_IMMERSIVE_DARK_MODE_OLD;
+ DwmSetWindowAttribute(hwnd, attr, &w32_darkmode, sizeof(w32_darkmode));
+ }
+ }
+ }
+#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/util.h b/src/ui/util.h
new file mode 100644
index 0000000..779b35f
--- /dev/null
+++ b/src/ui/util.h
@@ -0,0 +1,126 @@
+// 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 <cstddef> // size_t
+#include <exception>
+
+#include <gdkmm/rgba.h>
+#include <gtkmm/cellrenderer.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/stylecontext.h>
+#include <2geom/point.h>
+#include <2geom/rect.h>
+#include <2geom/affine.h>
+#include <gtkmm/widget.h>
+
+/*
+ * Use these errors when building from glade files for graceful
+ * fallbacks and prevent crashes from corrupt ui files.
+ */
+class UIBuilderError : public std::exception {};
+class UIFileUnavailable : public UIBuilderError {};
+class WidgetUnavailable : public UIBuilderError {};
+
+namespace Cairo {
+class Matrix;
+class ImageSurface;
+}
+
+namespace Glib {
+class ustring;
+}
+
+namespace Gtk {
+class Revealer;
+class Container;
+class Widget;
+}
+
+Gtk::Widget *get_widget_by_name(Gtk::Container *parent, const std::string &name);
+
+Glib::ustring ink_ellipsize_text (Glib::ustring const &src, size_t maxlen);
+void reveal_widget(Gtk::Widget *widget, bool show);
+
+// check if widget in a container is actually visible
+bool is_widget_effectively_visible(Gtk::Widget const *widget);
+
+namespace Inkscape::UI {
+
+void set_icon_sizes(Gtk::Widget* parent, int pixel_size);
+void set_icon_sizes(GtkWidget* parent, int pixel_size);
+
+/// Utility function to ensure correct sizing after adding child widgets.
+void resize_widget_children(Gtk::Widget *widget);
+
+void gui_warning(const std::string &msg, Gtk::Window * parent_window = nullptr);
+
+inline void widget_show(Gtk::Widget &widget, bool show)
+{
+ show ? widget.show() : widget.hide();
+}
+
+/// Translate cell renderer state to style flags.
+Gtk::StateFlags cell_flags_to_state_flags(Gtk::CellRendererState state);
+
+} // namespace Inkscape::UI
+
+// Mix two RGBA colors using simple linear interpolation:
+// 0 -> only a, 1 -> only b, x in 0..1 -> (1 - x)*a + x*b
+Gdk::RGBA mix_colors(const Gdk::RGBA& a, const Gdk::RGBA& b, float ratio);
+
+// Create the same color, but with a different opacity (alpha)
+Gdk::RGBA change_alpha(const Gdk::RGBA& color, double new_alpha);
+
+// Get the background-color style property for a given StyleContext
+Gdk::RGBA get_context_color(const Glib::RefPtr<Gtk::StyleContext> &context,
+ const gchar *property,
+ Gtk::StateFlags state = static_cast<Gtk::StateFlags>(0));
+Gdk::RGBA get_background_color(const Glib::RefPtr<Gtk::StyleContext> &context,
+ Gtk::StateFlags state = static_cast<Gtk::StateFlags>(0));
+
+Geom::IntRect cairo_to_geom(const Cairo::RectangleInt &rect);
+Cairo::RectangleInt geom_to_cairo(const Geom::IntRect &rect);
+Cairo::Matrix geom_to_cairo(const Geom::Affine &affine);
+Geom::IntPoint dimensions(const Cairo::RefPtr<Cairo::ImageSurface> &surface);
+Geom::IntPoint dimensions(const Gdk::Rectangle &allocation);
+
+// create a gradient with multiple steps to approximate profile described by given cubic spline
+Cairo::RefPtr<Cairo::LinearGradient> create_cubic_gradient(
+ Geom::Rect rect,
+ const Gdk::RGBA& from,
+ const Gdk::RGBA& to,
+ Geom::Point ctrl1,
+ Geom::Point ctrl2,
+ Geom::Point p0 = Geom::Point(0, 0),
+ Geom::Point p1 = Geom::Point(1, 1),
+ int steps = 8
+);
+
+void set_dark_tittlebar(Glib::RefPtr<Gdk::Window> win, bool is_dark);
+// convert Gdk::RGBA into 32-bit rrggbbaa color, optionally replacing alpha, if specified
+uint32_t conv_gdk_color_to_rgba(const Gdk::RGBA& color, double replace_alpha = -1);
+
+#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/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/svg-view-widget.cpp b/src/ui/view/svg-view-widget.cpp
new file mode 100644
index 0000000..f4b930c
--- /dev/null
+++ b/src/ui/view/svg-view-widget.cpp
@@ -0,0 +1,259 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * A light-weight widget containing an Inkscape canvas 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/drawing.h"
+#include "display/control/canvas-item.h"
+#include "display/control/canvas-item-drawing.h"
+#include "display/control/canvas-item-group.h"
+
+#include "object/sp-item.h"
+#include "object/sp-root.h"
+
+#include "ui/widget/canvas.h"
+
+#include "util/units.h"
+
+namespace Inkscape {
+namespace UI {
+namespace View {
+
+/**
+ * Callback connected with drawing_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 bool _drawing_handler(GdkEvent *event, Inkscape::DrawingItem *drawing_item, SVGViewWidget *svgview)
+{
+ static gdouble x, y;
+ static gboolean active = FALSE;
+ SPEvent spev;
+
+ SPItem *spitem = (drawing_item) ? drawing_item->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.
+ */
+SVGViewWidget::SVGViewWidget(SPDocument* document)
+{
+ _canvas = Gtk::make_managed<Inkscape::UI::Widget::Canvas>();
+ add(*_canvas);
+
+ _parent = new Inkscape::CanvasItemGroup(_canvas->get_canvas_item_root());
+ _drawing = new Inkscape::CanvasItemDrawing(_parent);
+ _canvas->set_drawing(_drawing->get_drawing());
+ _drawing->connect_drawing_event(sigc::bind(sigc::ptr_fun(_drawing_handler), this));
+
+ setDocument(document);
+
+ show_all();
+}
+
+SVGViewWidget::~SVGViewWidget()
+{
+ setDocument(nullptr);
+}
+
+void
+SVGViewWidget::setDocument(SPDocument* document)
+{
+ // Clear old document
+ if (_document) {
+ _document->getRoot()->invoke_hide(_dkey); // Removed from display tree
+ }
+
+ _document = document;
+
+ // Add new document
+ if (_document) {
+ Inkscape::DrawingItem *drawing_item = _document->getRoot()->invoke_show(
+ *_drawing->get_drawing(),
+ _dkey,
+ SP_ITEM_SHOW_DISPLAY);
+
+ if (drawing_item) {
+ _drawing->get_drawing()->root()->prependChild(drawing_item);
+ }
+
+ 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::on_size_allocate(Gtk::Allocation& allocation)
+{
+ if (!(_allocation == allocation)) {
+ _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;
+ Gtk::Bin::on_size_allocate(allocation);
+ return;
+ }
+
+ _rescale = true;
+ _keepaspect = true;
+ _width = width;
+ _height = height;
+
+ doRescale ();
+ }
+
+ Gtk::Bin::on_size_allocate(allocation);
+}
+
+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 = (_document->getWidth().value("px") * _hscale - _width) / 2.0;
+ } else {
+ _vscale = _hscale;
+ y_offset = (_document->getHeight().value("px") * _vscale - _height) / 2.0;
+ }
+ }
+ }
+
+ if (_drawing) {
+ _canvas->set_affine(Geom::Scale(_hscale, _vscale));
+ _canvas->set_pos(Geom::Point(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(_canvas->gobj()));
+ gdk_window_set_cursor(window, cursor);
+ g_object_unref(cursor);
+}
+
+void
+SVGViewWidget::mouseout()
+{
+ GdkWindow *window = gtk_widget_get_window (GTK_WIDGET(_canvas->gobj()));
+ 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..e12e7aa
--- /dev/null
+++ b/src/ui/view/svg-view-widget.h
@@ -0,0 +1,97 @@
+// 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;
+
+namespace Inkscape {
+
+class CanvasItemDrawing;
+class CanvasItemGroup;
+
+namespace UI {
+
+namespace Widget {
+class Canvas;
+}
+
+namespace View {
+
+/**
+ * A light-weight widget containing an Inkscape canvas for rendering an SVG.
+ */
+class SVGViewWidget : public Gtk::Bin {
+
+public:
+ SVGViewWidget(SPDocument* document);
+ ~SVGViewWidget() override;
+ void setDocument( SPDocument* document);
+ void setResize( int width, int height);
+ void on_size_allocate(Gtk::Allocation& allocation) override;
+
+private:
+
+ Inkscape::UI::Widget::Canvas *_canvas;
+
+// From SVGView ---------------------------------
+
+public:
+ SPDocument* _document = nullptr;
+ unsigned int _dkey = 0;
+ Inkscape::CanvasItemGroup *_parent = nullptr;
+ Inkscape::CanvasItemDrawing *_drawing = nullptr;
+ Gtk::Allocation _allocation;
+ double _hscale = 1.0; ///< horizontal scale
+ double _vscale = 1.0; ///< vertical scale
+ bool _rescale = false; ///< whether to rescale automatically
+ bool _keepaspect = false;
+ double _width = 0.0;
+ double _height = 0.0;
+
+ /**
+ * 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..2b749cc
--- /dev/null
+++ b/src/ui/view/view-widget.cpp
@@ -0,0 +1,58 @@
+// 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"
+
+/**
+ * Callback to disconnect from view and destroy SPViewWidget.
+ *
+ * Apparently, this gets only called when a desktop is closed, but then twice!
+ */
+void SPViewWidget::on_unrealize()
+{
+ SPViewWidget *vw = this;
+
+ if (vw->view) {
+ vw->view->close();
+ Inkscape::GC::release(vw->view);
+ vw->view = nullptr;
+ }
+
+ parent_type::on_unrealize();
+
+ Inkscape::GC::request_early_collection();
+}
+
+void SPViewWidget::setView(view_type *view)
+{
+ auto vw = this;
+ g_return_if_fail(view != nullptr);
+
+ g_return_if_fail(vw->view == nullptr);
+
+ vw->view = view;
+ Inkscape::GC::anchor(view);
+}
+
+
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 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..e67280d
--- /dev/null
+++ b/src/ui/view/view-widget.h
@@ -0,0 +1,56 @@
+// 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 <gtkmm/eventbox.h>
+
+
+namespace Inkscape {
+namespace UI {
+namespace View {
+class View;
+} // namespace View
+} // namespace UI
+} // namespace Inkscape
+
+/**
+ * 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 Gtk::EventBox {
+ using parent_type = Gtk::EventBox;
+ using view_type = Inkscape::UI::View::View;
+
+ view_type *view = nullptr;
+
+ public:
+ void on_unrealize() override;
+
+ view_type *getView() { return view; }
+
+ void setView(view_type *view);
+};
+
+#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..1fca79d
--- /dev/null
+++ b/src/ui/view/view.cpp
@@ -0,0 +1,95 @@
+// 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 "inkscape.h"
+
+namespace Inkscape {
+namespace UI {
+namespace View {
+
+View::View()
+: _doc(nullptr)
+{
+ _message_stack = std::make_shared<Inkscape::MessageStack>();
+ _tips_message_context = std::make_unique<Inkscape::MessageContext>(_message_stack);
+
+ _resized_connection = _resized_signal.connect([this] (double x, double y) {
+ onResized(x, y);
+ });
+
+ _message_changed_connection = _message_stack->connectChanged([this] (Inkscape::MessageType type, const gchar *message) {
+ onStatusMessage(type, message);
+ });
+}
+
+View::~View()
+{
+ _close();
+}
+
+void View::_close() {
+ _message_changed_connection.disconnect();
+
+ _tips_message_context = nullptr;
+
+ _message_stack = nullptr;
+
+ if (_doc) {
+ _document_uri_set_connection.disconnect();
+ INKSCAPE.remove_document(_doc);
+ _doc = nullptr;
+ }
+}
+
+void View::emitResized (double width, double height)
+{
+ _resized_signal.emit (width, height);
+}
+
+void View::setDocument(SPDocument *doc) {
+ if (!doc) return;
+
+ if (_doc) {
+ _document_uri_set_connection.disconnect();
+ INKSCAPE.remove_document(_doc);
+ }
+
+ INKSCAPE.add_document(doc);
+
+ _doc = doc;
+ _document_uri_set_connection = _doc->connectFilenameSet([this] (const gchar *filename) {
+ onDocumentFilenameSet(filename);
+ });
+ _document_filename_set_signal.emit( _doc->getDocumentFilename() );
+}
+
+} // 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 :
diff --git a/src/ui/view/view.h b/src/ui/view/view.h
new file mode 100644
index 0000000..7c5420e
--- /dev/null
+++ b/src/ui/view/view.h
@@ -0,0 +1,141 @@
+// 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);
+
+ virtual void onResized (double, double) {};
+ virtual void onStatusMessage (Inkscape::MessageType type, gchar const *message) {};
+ virtual void onDocumentFilenameSet (gchar const* filename) {};
+
+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_filename_set_signal;
+
+private:
+ sigc::connection _resized_connection;
+ sigc::connection _message_changed_connection; // foreign
+ sigc::connection _document_uri_set_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..11d1166
--- /dev/null
+++ b/src/ui/widget/alignment-selector.cpp
@@ -0,0 +1,80 @@
+// 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);
+ // clang-format off
+ 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]);
+ // clang-format on
+
+ _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..58f13c8
--- /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..0be3513
--- /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..4815d74
--- /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 SPAttr a, unsigned int value)
+ : _attr(a),
+ _default(value)
+ {}
+
+ AttrWidget(const SPAttr a, double value)
+ : _attr(a),
+ _default(value)
+ {}
+
+ AttrWidget(const SPAttr a, bool value)
+ : _attr(a),
+ _default(value)
+ {}
+
+ AttrWidget(const SPAttr a, char* value)
+ : _attr(a),
+ _default(value)
+ {}
+
+ AttrWidget(const SPAttr a)
+ : _attr(a),
+ _default()
+ {}
+
+ virtual ~AttrWidget()
+ = default;
+
+ virtual Glib::ustring get_as_attribute() const = 0;
+ virtual void set_from_attribute(SPObject*) = 0;
+
+ SPAttr 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 SPAttr _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/canvas-grid.cpp b/src/ui/widget/canvas-grid.cpp
new file mode 100644
index 0000000..460b606
--- /dev/null
+++ b/src/ui/widget/canvas-grid.cpp
@@ -0,0 +1,419 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Rewrite of code originally in desktop-widget.cpp.
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+// The scrollbars, and canvas are tightly coupled so it makes sense to have a dedicated
+// widget to handle their interactions. The buttons are along for the ride. I don't see
+// how to add the buttons easily via a .ui file (which would allow the user to put any
+// buttons they want in their place).
+
+#include <glibmm/i18n.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/label.h>
+
+#include "canvas-grid.h"
+
+#include "desktop.h" // Hopefully temp.
+#include "desktop-events.h" // Hopefully temp.
+
+#include "display/control/canvas-item-drawing.h" // sticky
+
+#include "page-manager.h"
+
+#include "ui/dialog/command-palette.h"
+#include "ui/icon-loader.h"
+#include "ui/widget/canvas.h"
+#include "ui/widget/canvas-notice.h"
+#include "ui/widget/ink-ruler.h"
+#include "io/resource.h"
+
+#include "widgets/desktop-widget.h" // Hopefully temp.
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+CanvasGrid::CanvasGrid(SPDesktopWidget *dtw)
+{
+ _dtw = dtw;
+ set_name("CanvasGrid");
+
+ // Canvas
+ _canvas = std::make_unique<Inkscape::UI::Widget::Canvas>();
+ _canvas->set_hexpand(true);
+ _canvas->set_vexpand(true);
+ _canvas->set_can_focus(true);
+ _canvas->signal_event().connect(sigc::mem_fun(*this, &CanvasGrid::SignalEvent)); // TEMP
+
+ // Command palette
+ _command_palette = std::make_unique<Inkscape::UI::Dialog::CommandPalette>();
+
+ // Notice overlay, note using unique_ptr will cause destruction race conditions
+ _notice = CanvasNotice::create();
+
+ // Canvas overlay
+ _canvas_overlay.add(*_canvas);
+ _canvas_overlay.add_overlay(*_command_palette->get_base_widget());
+ _canvas_overlay.add_overlay(*_notice);
+
+ // Horizontal Ruler
+ _hruler = std::make_unique<Inkscape::UI::Widget::Ruler>(Gtk::ORIENTATION_HORIZONTAL);
+ _hruler->add_track_widget(*_canvas);
+ _hruler->set_hexpand(true);
+ _hruler->show();
+ // Tooltip/Unit set elsewhere
+
+ // Vertical Ruler
+ _vruler = std::make_unique<Inkscape::UI::Widget::Ruler>(Gtk::ORIENTATION_VERTICAL);
+ _vruler->add_track_widget(*_canvas);
+ _vruler->set_vexpand(true);
+ _vruler->show();
+ // Tooltip/Unit set elsewhere.
+
+ // Guide Lock
+ _guide_lock.set_name("LockGuides");
+ _guide_lock.add(*Gtk::make_managed<Gtk::Image>("object-locked", Gtk::ICON_SIZE_MENU));
+ // To be replaced by Gio::Action:
+ _guide_lock.signal_toggled().connect(sigc::mem_fun(*_dtw, &SPDesktopWidget::update_guides_lock));
+ _guide_lock.set_tooltip_text(_("Toggle lock of all guides in the document"));
+ // Subgrid
+ _subgrid.attach(_guide_lock, 0, 0, 1, 1);
+ _subgrid.attach(*_vruler, 0, 1, 1, 1);
+ _subgrid.attach(*_hruler, 1, 0, 1, 1);
+ _subgrid.attach(_canvas_overlay, 1, 1, 1, 1);
+
+ // Horizontal Scrollbar
+ _hadj = Gtk::Adjustment::create(0.0, -4000.0, 4000.0, 10.0, 100.0, 4.0);
+ _hadj->signal_value_changed().connect(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_adjustment_value_changed));
+ _hscrollbar = Gtk::Scrollbar(_hadj, Gtk::ORIENTATION_HORIZONTAL);
+ _hscrollbar.set_name("CanvasScrollbar");
+ _hscrollbar.set_hexpand(true);
+
+ // Vertical Scrollbar
+ _vadj = Gtk::Adjustment::create(0.0, -4000.0, 4000.0, 10.0, 100.0, 4.0);
+ _vadj->signal_value_changed().connect(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_adjustment_value_changed));
+ _vscrollbar = Gtk::Scrollbar(_vadj, Gtk::ORIENTATION_VERTICAL);
+ _vscrollbar.set_name("CanvasScrollbar");
+ _vscrollbar.set_vexpand(true);
+
+ // CMS Adjust (To be replaced by Gio::Action)
+ _cms_adjust.set_name("CMS_Adjust");
+ _cms_adjust.add(*Gtk::make_managed<Gtk::Image>("color-management", Gtk::ICON_SIZE_MENU));
+ // Can't access via C++ API, fixed in Gtk4.
+ gtk_actionable_set_action_name( GTK_ACTIONABLE(_cms_adjust.gobj()), "win.canvas-color-manage");
+ _cms_adjust.set_tooltip_text(_("Toggle color-managed display for this document window"));
+
+ // popover with some common display mode related options
+ auto builder = Gtk::Builder::create_from_file(Inkscape::IO::Resource::get_filename(Inkscape::IO::Resource::UIS, "display-popup.glade"));
+ _display_popup = builder;
+ Gtk::Popover* popover;
+ _display_popup->get_widget("popover", popover);
+ Gtk::CheckButton* sticky_zoom;
+ _display_popup->get_widget("zoom-resize", sticky_zoom);
+ // To be replaced by Gio::Action:
+ sticky_zoom->signal_toggled().connect([=](){ _dtw->sticky_zoom_toggled(); });
+ _quick_actions.set_name("QuickActions");
+ _quick_actions.set_popover(*popover);
+ _quick_actions.set_image_from_icon_name("display-symbolic");
+ _quick_actions.set_direction(Gtk::ARROW_LEFT);
+ _quick_actions.set_tooltip_text(_("Display options"));
+
+ // Main grid
+ attach(_subgrid, 0, 0, 1, 2);
+ attach(_hscrollbar, 0, 2, 1, 1);
+ attach(_cms_adjust, 1, 2, 1, 1);
+ attach(_quick_actions, 1, 0, 1, 1);
+ attach(_vscrollbar, 1, 1, 1, 1);
+
+ // For creating guides, etc.
+ _hruler->signal_button_press_event().connect(
+ sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_button_press_event), _hruler.get(), true));
+ _hruler->signal_button_release_event().connect(
+ sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_button_release_event), _hruler.get(), true));
+ _hruler->signal_motion_notify_event().connect(
+ sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_motion_notify_event), _hruler.get(), true));
+
+ // For creating guides, etc.
+ _vruler->signal_button_press_event().connect(
+ sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_button_press_event), _vruler.get(), false));
+ _vruler->signal_button_release_event().connect(
+ sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_button_release_event), _vruler.get(), false));
+ _vruler->signal_motion_notify_event().connect(
+ sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_motion_notify_event), _vruler.get(), false));
+
+ show_all();
+}
+
+CanvasGrid::~CanvasGrid()
+{
+ _page_modified_connection.disconnect();
+ _page_selected_connection.disconnect();
+ _sel_modified_connection.disconnect();
+ _sel_changed_connection.disconnect();
+ _document = nullptr;
+ _notice = nullptr;
+}
+
+void CanvasGrid::on_realize() {
+ // actions should be available now
+
+ if (auto map = _dtw->get_action_map()) {
+ auto set_display_icon = [=]() {
+ Glib::ustring id;
+ auto mode = _canvas->get_render_mode();
+ switch (mode) {
+ case RenderMode::NORMAL: id = "display";
+ break;
+ case RenderMode::OUTLINE: id = "display-outline";
+ break;
+ case RenderMode::OUTLINE_OVERLAY: id = "display-outline-overlay";
+ break;
+ case RenderMode::VISIBLE_HAIRLINES: id = "display-enhance-stroke";
+ break;
+ case RenderMode::NO_FILTERS: id = "display-no-filter";
+ break;
+ default:
+ g_warning("Unknown display mode in canvas-grid");
+ break;
+ }
+
+ if (!id.empty()) {
+ // if CMS is ON show alternative icons
+ if (_canvas->get_cms_active()) {
+ id += "-alt";
+ }
+ _quick_actions.set_image_from_icon_name(id + "-symbolic");
+ }
+ };
+
+ set_display_icon();
+
+ // when display mode state changes, update icon
+ auto cms_action = Glib::RefPtr<Gio::SimpleAction>::cast_dynamic(map->lookup_action("canvas-color-manage"));
+ auto disp_action = Glib::RefPtr<Gio::SimpleAction>::cast_dynamic(map->lookup_action("canvas-display-mode"));
+
+ if (cms_action && disp_action) {
+ disp_action->signal_activate().connect([=](const Glib::VariantBase& state){ set_display_icon(); });
+ cms_action-> signal_activate().connect([=](const Glib::VariantBase& state){ set_display_icon(); });
+ }
+ else {
+ g_warning("No canvas-display-mode and/or canvas-color-manage action available to canvas-grid");
+ }
+ }
+ else {
+ g_warning("No action map available to canvas-grid");
+ }
+
+ parent_type::on_realize();
+}
+
+// TODO: remove when sticky zoom gets replaced by Gio::Action:
+Gtk::ToggleButton* CanvasGrid::GetStickyZoom() {
+ Gtk::CheckButton* sticky_zoom;
+ _display_popup->get_widget("zoom-resize", sticky_zoom);
+ return sticky_zoom;
+}
+
+// _dt2r should be a member of _canvas.
+// get_display_area should be a member of _canvas.
+void
+CanvasGrid::UpdateRulers()
+{
+ auto prefs = Inkscape::Preferences::get();
+ auto desktop = _dtw->desktop;
+ auto document = desktop->getDocument();
+ auto &pm = document->getPageManager();
+ auto sel = desktop->getSelection();
+
+ // Our connections to the document are handled with a lazy pattern to avoid
+ // having to refactor the SPDesktopWidget class. We know UpdateRulers is
+ // called in all situations when documents are loaded and replaced.
+ if (document != _document) {
+ _document = document;
+ _page_selected_connection = pm.connectPageSelected([=](SPPage *) { UpdateRulers(); });
+ _page_modified_connection = pm.connectPageModified([=](SPPage *) { UpdateRulers(); });
+ _sel_modified_connection = sel->connectModified([=](Inkscape::Selection *, int) { UpdateRulers(); });
+ _sel_changed_connection = sel->connectChanged([=](Inkscape::Selection *) { UpdateRulers(); });
+ }
+
+ Geom::Rect viewbox = desktop->get_display_area().bounds();
+ Geom::Rect startbox = viewbox;
+ if (prefs->getBool("/options/origincorrection/page", true)) {
+ // Move viewbox according to the selected page's position (if any)
+ startbox *= pm.getSelectedPageAffine().inverse();
+ }
+
+ // Scale and offset the ruler coordinates
+ // Use an integer box to align the ruler to the grid and page.
+ auto rulerbox = (startbox * Geom::Scale(_dtw->_dt2r));
+ _hruler->set_range(rulerbox.left(), rulerbox.right());
+ if (_dtw->desktop->is_yaxisdown()) {
+ _vruler->set_range(rulerbox.top(), rulerbox.bottom());
+ } else {
+ _vruler->set_range(rulerbox.bottom(), rulerbox.top());
+ }
+
+ Geom::Point pos(_canvas->get_pos());
+ auto scale = _canvas->get_affine();
+ auto d2c = Geom::Translate(pos * scale.inverse()).inverse() * scale;
+ auto pagebox = (pm.getSelectedPageRect() * d2c).roundOutwards();
+ _hruler->set_page(pagebox.left(), pagebox.right());
+ _vruler->set_page(pagebox.top(), pagebox.bottom());
+
+ Geom::Rect selbox = Geom::IntRect(0, 0, 0, 0);
+ if (auto bbox = sel->preferredBounds())
+ selbox = (*bbox * d2c).roundOutwards();
+ _hruler->set_selection(selbox.left(), selbox.right());
+ _vruler->set_selection(selbox.top(), selbox.bottom());
+}
+
+void
+CanvasGrid::ShowScrollbars(bool state)
+{
+ if (_show_scrollbars == state) return;
+ _show_scrollbars = state;
+
+ if (_show_scrollbars) {
+ // Show scrollbars
+ _hscrollbar.show();
+ _vscrollbar.show();
+ _cms_adjust.show();
+ _cms_adjust.show_all_children();
+ _quick_actions.show();
+ } else {
+ // Hide scrollbars
+ _hscrollbar.hide();
+ _vscrollbar.hide();
+ _cms_adjust.hide();
+ _quick_actions.hide();
+ }
+}
+
+void
+CanvasGrid::ToggleScrollbars()
+{
+ _show_scrollbars = !_show_scrollbars;
+ ShowScrollbars(_show_scrollbars);
+
+ // Will be replaced by actions
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool("/fullscreen/scrollbars/state", _show_scrollbars);
+ prefs->setBool("/window/scrollbars/state", _show_scrollbars);
+}
+
+void
+CanvasGrid::ShowRulers(bool state)
+{
+ if (_show_rulers == state) return;
+ _show_rulers = state;
+
+ if (_show_rulers) {
+ // Show rulers
+ _hruler->show();
+ _vruler->show();
+ _guide_lock.show();
+ _guide_lock.show_all_children();
+ } else {
+ // Hide rulers
+ _hruler->hide();
+ _vruler->hide();
+ _guide_lock.hide();
+ }
+}
+
+void
+CanvasGrid::ToggleRulers()
+{
+ _show_rulers = !_show_rulers;
+ ShowRulers(_show_rulers);
+
+ // Will be replaced by actions
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool("/fullscreen/rulers/state", _show_rulers);
+ prefs->setBool("/window/rulers/state", _show_rulers);
+}
+
+void
+CanvasGrid::ToggleCommandPalette()
+{
+ _command_palette->toggle();
+}
+
+void
+CanvasGrid::showNotice(Glib::ustring const &msg, unsigned timeout)
+{
+ _notice->show(msg, timeout);
+}
+
+void
+CanvasGrid::ShowCommandPalette(bool state)
+{
+ if (state) {
+ _command_palette->open();
+ }
+ _command_palette->close();
+}
+
+// Update rulers on change of widget size, but only if allocation really changed.
+void
+CanvasGrid::on_size_allocate(Gtk::Allocation& allocation)
+{
+ Gtk::Grid::on_size_allocate(allocation);
+ if (!(_allocation == allocation)) { // No != function defined!
+ _allocation = allocation;
+ UpdateRulers();
+ }
+}
+
+// This belong in Canvas class
+bool
+CanvasGrid::SignalEvent(GdkEvent *event)
+{
+ if (event->type == GDK_BUTTON_PRESS) {
+ _canvas->grab_focus();
+ _command_palette->close();
+ }
+
+ if (event->type == GDK_BUTTON_PRESS && event->button.button == 3) {
+ _dtw->desktop->getCanvasDrawing()->set_sticky(event->button.state & GDK_SHIFT_MASK);
+ }
+
+ // Pass keyboard events back to the desktop root handler so TextTool can work
+ if ((event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE)
+ && !_canvas->get_current_canvas_item())
+ {
+ return sp_desktop_root_handler(event, _dtw->desktop);
+ }
+
+ return false;
+}
+
+// TODO Add actions so we can set shortcuts.
+// * Sticky Zoom
+// * CMS Adjust
+// * Guide Lock
+
+
+} // 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/canvas-grid.h b/src/ui/widget/canvas-grid.h
new file mode 100644
index 0000000..5cddbbe
--- /dev/null
+++ b/src/ui/widget/canvas-grid.h
@@ -0,0 +1,131 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifndef INKSCAPE_UI_WIDGET_CANVASGRID_H
+#define INKSCAPE_UI_WIDGET_CANVASGRID_H
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm.h>
+#include <gtkmm/label.h>
+#include <gtkmm/overlay.h>
+#include <gtkmm/menubutton.h>
+#include <gtkmm/builder.h>
+
+class SPPage;
+class SPDocument;
+class SPCanvas;
+class SPDesktopWidget;
+
+namespace Inkscape {
+namespace UI {
+
+namespace Dialog {
+class CommandPalette;
+}
+
+namespace Widget {
+
+class Canvas;
+class CanvasNotice;
+class Ruler;
+
+/**
+ * A Gtk::Grid widget that contains rulers, scrollbars, buttons, and, of course, the canvas.
+ * Canvas has an overlay to let us put stuff on the canvas.
+ */
+class CanvasGrid : public Gtk::Grid
+{
+ using parent_type = Gtk::Grid;
+public:
+ CanvasGrid(SPDesktopWidget *dtw);
+ ~CanvasGrid() override;
+
+ void ShowScrollbars(bool state = true);
+ void ToggleScrollbars();
+
+ void ShowRulers(bool state = true);
+ void ToggleRulers();
+ void UpdateRulers();
+
+ void ShowCommandPalette(bool state = true);
+ void ToggleCommandPalette();
+
+ void showNotice(Glib::ustring const &msg, unsigned timeout = 0);
+
+ Inkscape::UI::Widget::Canvas *GetCanvas() { return _canvas.get(); };
+
+ // Hopefully temp.
+ Inkscape::UI::Widget::Ruler *GetHRuler() { return _vruler.get(); };
+ Inkscape::UI::Widget::Ruler *GetVRuler() { return _hruler.get(); };
+ Gtk::Adjustment *GetHAdj() { return _hadj.get(); };
+ Gtk::Adjustment *GetVAdj() { return _vadj.get(); };
+ Gtk::ToggleButton *GetGuideLock() { return &_guide_lock; }
+ Gtk::ToggleButton *GetCmsAdjust() { return &_cms_adjust; }
+ Gtk::ToggleButton *GetStickyZoom();
+
+private:
+ // Signal callbacks
+ void on_size_allocate(Gtk::Allocation& allocation) override;
+ bool SignalEvent(GdkEvent *event);
+ void on_realize() override;
+
+ // The widgets
+ std::unique_ptr<Inkscape::UI::Widget::Canvas> _canvas;
+ std::unique_ptr<Dialog::CommandPalette> _command_palette;
+ CanvasNotice *_notice;
+ Gtk::Overlay _canvas_overlay;
+ Gtk::Grid _subgrid;
+
+ Glib::RefPtr<Gtk::Adjustment> _hadj;
+ Glib::RefPtr<Gtk::Adjustment> _vadj;
+ Gtk::Scrollbar _hscrollbar;
+ Gtk::Scrollbar _vscrollbar;
+
+ std::unique_ptr<Inkscape::UI::Widget::Ruler> _hruler;
+ std::unique_ptr<Inkscape::UI::Widget::Ruler> _vruler;
+
+ Gtk::ToggleButton _guide_lock;
+ Gtk::ToggleButton _cms_adjust;
+ Gtk::MenuButton _quick_actions;
+ Glib::RefPtr<Gtk::Builder> _display_popup;
+
+ // To be replaced by stateful Gio::Actions
+ bool _show_scrollbars = true;
+ bool _show_rulers = true;
+
+ // Hopefully temp
+ SPDesktopWidget *_dtw;
+ SPDocument *_document = nullptr;
+
+ // Store allocation so we don't redraw too often.
+ Gtk::Allocation _allocation;
+
+ // Connections for page and selection tracking
+ sigc::connection _page_selected_connection;
+ sigc::connection _page_modified_connection;
+ sigc::connection _sel_changed_connection;
+ sigc::connection _sel_modified_connection;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVASGRID_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/canvas-notice.cpp b/src/ui/widget/canvas-notice.cpp
new file mode 100644
index 0000000..0337bf9
--- /dev/null
+++ b/src/ui/widget/canvas-notice.cpp
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "canvas-notice.h"
+
+#include <utility>
+#include <glibmm/main.h>
+
+#include "ui/builder-utils.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+
+CanvasNotice::CanvasNotice(BaseObjectType *cobject, Glib::RefPtr<Gtk::Builder> builder)
+ : Gtk::Revealer(cobject)
+ , _builder(std::move(builder))
+ , _icon(get_widget<Gtk::Image>(_builder, "notice-icon"))
+ , _label(get_widget<Gtk::Label>(_builder, "notice-label"))
+{
+ auto &close = get_widget<Gtk::Button>(_builder, "notice-close");
+ close.signal_clicked().connect([=]() {
+ hide();
+ });
+}
+
+void CanvasNotice::show(Glib::ustring const &msg, unsigned timeout)
+{
+ _label.set_text(msg);
+ set_reveal_child(true);
+ if (timeout != 0) {
+ _timeout = Glib::signal_timeout().connect([=]() {
+ hide();
+ return false;
+ }, timeout);
+ }
+}
+
+void CanvasNotice::hide()
+{
+ set_reveal_child(false);
+}
+
+CanvasNotice *CanvasNotice::create()
+{
+ CanvasNotice *widget = nullptr;
+ auto builder = create_builder("canvas-notice.glade");
+ builder->get_widget_derived("canvas-notice", widget);
+ return widget;
+}
+
+}}} // namespaces
diff --git a/src/ui/widget/canvas-notice.h b/src/ui/widget/canvas-notice.h
new file mode 100644
index 0000000..88c7bed
--- /dev/null
+++ b/src/ui/widget/canvas-notice.h
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_NOTICE_H
+#define INKSCAPE_UI_WIDGET_CANVAS_NOTICE_H
+
+#include <glibmm/refptr.h>
+#include <gtkmm/builder.h>
+
+#include <gtkmm/revealer.h>
+#include <gtkmm/image.h>
+#include <gtkmm/label.h>
+#include <gtkmm/button.h>
+
+#include "helper/auto-connection.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class CanvasNotice : public Gtk::Revealer {
+public:
+ static CanvasNotice *create();
+
+ CanvasNotice(BaseObjectType *cobject, Glib::RefPtr<Gtk::Builder> refGlade);
+ void show(Glib::ustring const &msg, unsigned timeout = 0);
+ void hide();
+private:
+ Glib::RefPtr<Gtk::Builder> _builder;
+
+ Gtk::Image& _icon;
+ Gtk::Label& _label;
+
+ Inkscape::auto_connection _timeout;
+};
+
+}}} // namespaces
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_NOTICE_H
diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp
new file mode 100644
index 0000000..cfdb966
--- /dev/null
+++ b/src/ui/widget/canvas.cpp
@@ -0,0 +1,2426 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Tavmjong Bah
+ * PBS <pbs3141@gmail.com>
+ *
+ * Copyright (C) 2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <iostream> // Logging
+#include <algorithm> // Sort
+#include <set> // Coarsener
+#include <thread>
+#include <mutex>
+#include <array>
+#include <cassert>
+#include <boost/asio/thread_pool.hpp>
+#include <boost/asio/post.hpp>
+#include <2geom/convex-hull.h>
+
+#include "canvas.h"
+#include "canvas-grid.h"
+
+#include "color.h" // Background color
+#include "cms-system.h" // Color correction
+#include "desktop.h"
+#include "document.h"
+#include "preferences.h"
+#include "ui/util.h"
+#include "helper/geom.h"
+
+#include "canvas/prefs.h"
+#include "canvas/fragment.h"
+#include "canvas/util.h"
+#include "canvas/stores.h"
+#include "canvas/graphics.h"
+#include "canvas/synchronizer.h"
+#include "display/drawing.h"
+#include "display/control/canvas-item-drawing.h"
+#include "display/control/canvas-item-group.h"
+#include "display/control/snap-indicator.h"
+
+#include "ui/tools/tool-base.h" // Default cursor
+
+#include "canvas/updaters.h" // Update strategies
+#include "canvas/framecheck.h" // For frame profiling
+#define framecheck_whole_function(D) \
+ auto framecheckobj = D->prefs.debug_framecheck ? FrameCheck::Event(__func__) : FrameCheck::Event();
+
+/*
+ * The canvas is responsible for rendering the SVG drawing with various "control"
+ * items below and on top of the drawing. Rendering is triggered by a call to one of:
+ *
+ *
+ * * redraw_all() Redraws the entire canvas by calling redraw_area() with the canvas area.
+ *
+ * * redraw_area() Redraws the indicated area. Use when there is a change that doesn't affect
+ * a CanvasItem's geometry or size.
+ *
+ * * request_update() Redraws after recalculating bounds for changed CanvasItems. Use if a
+ * CanvasItem's geometry or size has changed.
+ *
+ * The first three functions add a request to the Gtk's "idle" list via
+ *
+ * * add_idle() Which causes Gtk to call when resources are available:
+ *
+ * * on_idle() Which sets up the backing stores, divides the area of the canvas that has been marked
+ * unclean into rectangles that are small enough to render quickly, and renders them outwards
+ * from the mouse with a call to:
+ *
+ * * paint_rect_internal() Which paints the rectangle using paint_single_buffer(). It renders onto a Cairo
+ * surface "backing_store". After a piece is rendered there is a call to:
+ *
+ * * queue_draw_area() A Gtk function for marking areas of the window as needing a repaint, which when
+ * the time is right calls:
+ *
+ * * on_draw() Which blits the Cairo surface to the screen.
+ *
+ * The other responsibility of the canvas is to determine where to send GUI events. It does this
+ * by determining which CanvasItem is "picked" and then forwards the events to that item. Not all
+ * items can be picked. As a last resort, the "CatchAll" CanvasItem will be picked as it is the
+ * lowest CanvasItem in the stack (except for the "root" CanvasItem). With a small be of work, it
+ * should be possible to make the "root" CanvasItem a "CatchAll" eliminating the need for a
+ * dedicated "CatchAll" CanvasItem. There probably could be efficiency improvements as some
+ * items that are not pickable probably should be which would save having to effectively pick
+ * them "externally" (e.g. gradient CanvasItemCurves).
+ */
+
+namespace Inkscape::UI::Widget {
+namespace {
+
+/*
+ * Utilities
+ */
+
+// GdkEvents can only be safely copied using gdk_event_copy. Since this function allocates, we need the following smart pointer to wrap the result.
+struct GdkEventFreer {void operator()(GdkEvent *ev) const {gdk_event_free(ev);}};
+using GdkEventUniqPtr = std::unique_ptr<GdkEvent, GdkEventFreer>;
+
+// Copies a GdkEvent, returning the result as a smart pointer.
+auto make_unique_copy(GdkEvent const *ev) { return GdkEventUniqPtr(gdk_event_copy(ev)); }
+
+// Convert an integer received from preferences into an Updater enum.
+auto pref_to_updater(int index)
+{
+ constexpr auto arr = std::array{Updater::Strategy::Responsive,
+ Updater::Strategy::FullRedraw,
+ Updater::Strategy::Multiscale};
+ assert(1 <= index && index <= arr.size());
+ return arr[index - 1];
+}
+
+// Represents the raster data and location of an in-flight tile (one that is drawn, but not yet pasted into the stores).
+struct Tile
+{
+ Fragment fragment;
+ Cairo::RefPtr<Cairo::ImageSurface> surface;
+ Cairo::RefPtr<Cairo::ImageSurface> outline_surface;
+};
+
+// The urgency with which the async redraw process should exit.
+enum class AbortFlags : int
+{
+ None = 0,
+ Soft = 1, // exit if reached prerender phase
+ Hard = 2 // exit in any phase
+};
+
+// A copy of all the data the async redraw process needs access to, along with its internal state.
+struct RedrawData
+{
+ // Data on what/how to draw.
+ Geom::IntPoint mouse_loc;
+ Geom::IntRect visible;
+ Fragment store;
+ bool decoupled_mode;
+ Cairo::RefPtr<Cairo::Region> snapshot_drawn;
+ Geom::OptIntRect grabbed;
+
+ // Saved prefs
+ int coarsener_min_size;
+ int coarsener_glue_size;
+ double coarsener_min_fullness;
+ int tile_size;
+ int preempt;
+ int margin;
+ std::optional<int> redraw_delay;
+ int render_time_limit;
+ int numthreads;
+ bool background_in_stores_required;
+ uint64_t page, desk;
+ bool debug_framecheck;
+ bool debug_show_redraw;
+
+ // State
+ std::mutex mutex;
+ gint64 start_time;
+ int numactive;
+ int phase;
+ Geom::OptIntRect vis_store;
+
+ Geom::IntRect bounds;
+ Cairo::RefPtr<Cairo::Region> clean;
+ bool interruptible;
+ bool preemptible;
+ std::vector<Geom::IntRect> rects;
+ int effective_tile_size;
+
+ // Results
+ std::mutex tiles_mutex;
+ std::vector<Tile> tiles;
+ bool timeoutflag;
+
+ // Return comparison object for sorting rectangles by distance from mouse point.
+ auto getcmp() const
+ {
+ return [mouse_loc = mouse_loc] (Geom::IntRect const &a, Geom::IntRect const &b) {
+ return a.distanceSq(mouse_loc) > b.distanceSq(mouse_loc);
+ };
+ }
+};
+
+} // namespace
+
+/*
+ * Implementation class
+ */
+
+class CanvasPrivate
+{
+public:
+ friend class Canvas;
+ Canvas *q;
+ CanvasPrivate(Canvas *q)
+ : q(q)
+ , stores(prefs) {}
+
+ // Lifecycle
+ bool active = false;
+ void activate();
+ void deactivate();
+
+ // CanvasItem tree
+ std::optional<CanvasItemContext> canvasitem_ctx;
+
+ // Preferences
+ Prefs prefs;
+
+ // Stores
+ Stores stores;
+ void handle_stores_action(Stores::Action action);
+
+ // Invalidation
+ std::unique_ptr<Updater> updater; // Tracks the unclean region and decides how to redraw it.
+ Cairo::RefPtr<Cairo::Region> invalidated; // Buffers invalidations while the updater is in use by the background process.
+
+ // Graphics state; holds all the graphics resources, including the drawn content.
+ std::unique_ptr<Graphics> graphics;
+ void activate_graphics();
+ void deactivate_graphics();
+
+ // Redraw process management.
+ bool redraw_active = false;
+ bool redraw_requested = false;
+ sigc::connection schedule_redraw_conn;
+ void schedule_redraw();
+ void launch_redraw();
+ void after_redraw();
+ void commit_tiles();
+
+ // Event handling.
+ bool process_event(const GdkEvent*);
+ bool pick_current_item(const GdkEvent*);
+ bool emit_event(const GdkEvent*);
+ Inkscape::CanvasItem *pre_scroll_grabbed_item;
+
+ // Various state affecting what is drawn.
+ uint32_t desk = 0xffffffff; // The background colour, with the alpha channel used to control checkerboard.
+ uint32_t border = 0x000000ff; // The border colour, used only to control shadow colour.
+ uint32_t page = 0xffffffff; // The page colour, also with alpha channel used to control checkerboard.
+
+ bool clip_to_page = false; // Whether to enable clip-to-page mode.
+ PageInfo pi; // The list of page rectangles.
+ std::optional<Geom::PathVector> calc_page_clip() const; // Union of the page rectangles if in clip-to-page mode, otherwise no clip.
+
+ int scale_factor = 1; // The device scale the stores are drawn at.
+
+ bool outlines_enabled = false; // Whether to enable the outline layer.
+ bool outlines_required() const { return q->_split_mode != Inkscape::SplitMode::NORMAL || q->_render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY; }
+
+ bool background_in_stores_enabled = false; // Whether the page and desk should be drawn into the stores/tiles; if not then transparency is used instead.
+ bool background_in_stores_required() const { return !q->get_opengl_enabled() && SP_RGBA32_A_U(page) == 255 && SP_RGBA32_A_U(desk) == 255; } // Enable solid colour optimisation if both page and desk are solid (as opposed to checkerboard).
+
+ // Async redraw process.
+ std::optional<boost::asio::thread_pool> pool;
+ int numthreads;
+ int get_numthreads() const;
+
+ Synchronizer sync;
+ RedrawData rd;
+ std::atomic<int> abort_flags;
+
+ void init_tiler();
+ bool init_redraw();
+ bool end_redraw(); // returns true to indicate further redraw cycles required
+ void process_redraw(Geom::IntRect const &bounds, Cairo::RefPtr<Cairo::Region> clean, bool interruptible = true, bool preemptible = true);
+ void render_tile(int debug_id);
+ void paint_rect(Geom::IntRect const &rect);
+ void paint_single_buffer(const Cairo::RefPtr<Cairo::ImageSurface> &surface, const Geom::IntRect &rect, bool need_background, bool outline_pass);
+ void paint_error_buffer(const Cairo::RefPtr<Cairo::ImageSurface> &surface);
+
+ // Trivial overload of GtkWidget function.
+ void queue_draw_area(Geom::IntRect const &rect);
+
+ // For tracking the last known mouse position. (The function Gdk::Window::get_device_position cannot be used because of slow X11 round-trips. Remove this workaround when X11 dies.)
+ std::optional<Geom::IntPoint> last_mouse;
+
+ // Auto-scrolling.
+ std::optional<guint> tick_callback;
+ std::optional<gint64> last_time;
+ Geom::IntPoint strain;
+ Geom::Point displacement, velocity;
+ void autoscroll_begin(Geom::IntPoint const &to);
+ void autoscroll_end();
+};
+
+/*
+ * Lifecycle
+ */
+
+Canvas::Canvas()
+ : d(std::make_unique<CanvasPrivate>(this))
+{
+ set_name("InkscapeCanvas");
+
+ // Events
+ add_events(Gdk::BUTTON_PRESS_MASK |
+ Gdk::BUTTON_RELEASE_MASK |
+ Gdk::ENTER_NOTIFY_MASK |
+ Gdk::LEAVE_NOTIFY_MASK |
+ Gdk::FOCUS_CHANGE_MASK |
+ Gdk::KEY_PRESS_MASK |
+ Gdk::KEY_RELEASE_MASK |
+ Gdk::POINTER_MOTION_MASK |
+ Gdk::SCROLL_MASK |
+ Gdk::SMOOTH_SCROLL_MASK );
+
+ // Updater
+ d->updater = Updater::create(pref_to_updater(d->prefs.update_strategy));
+ d->updater->reset();
+ d->invalidated = Cairo::Region::create();
+
+ // Preferences
+ d->prefs.grabsize.action = [=] { d->canvasitem_ctx->root()->update_canvas_item_ctrl_sizes(d->prefs.grabsize); };
+ d->prefs.debug_show_unclean.action = [=] { queue_draw(); };
+ d->prefs.debug_show_clean.action = [=] { queue_draw(); };
+ d->prefs.debug_disable_redraw.action = [=] { d->schedule_redraw(); };
+ d->prefs.debug_sticky_decoupled.action = [=] { d->schedule_redraw(); };
+ d->prefs.debug_animate.action = [=] { queue_draw(); };
+ d->prefs.outline_overlay_opacity.action = [=] { queue_draw(); };
+ d->prefs.softproof.action = [=] { redraw_all(); };
+ d->prefs.displayprofile.action = [=] { redraw_all(); };
+ d->prefs.request_opengl.action = [=] {
+ if (get_realized()) {
+ d->deactivate();
+ d->deactivate_graphics();
+ set_opengl_enabled(d->prefs.request_opengl);
+ d->updater->reset();
+ d->activate_graphics();
+ d->activate();
+ }
+ };
+ d->prefs.pixelstreamer_method.action = [=] {
+ if (get_realized() && get_opengl_enabled()) {
+ d->deactivate();
+ d->deactivate_graphics();
+ d->activate_graphics();
+ d->activate();
+ }
+ };
+ d->prefs.numthreads.action = [=] {
+ if (!d->active) return;
+ int const new_numthreads = d->get_numthreads();
+ if (d->numthreads == new_numthreads) return;
+ d->numthreads = new_numthreads;
+ d->deactivate();
+ d->deactivate_graphics();
+ d->pool.emplace(d->numthreads);
+ d->activate_graphics();
+ d->activate();
+ };
+
+ // Canvas item tree
+ d->canvasitem_ctx.emplace(this);
+
+ // Split view.
+ _split_direction = Inkscape::SplitDirection::EAST;
+ _split_frac = {0.5, 0.5};
+
+ // Recreate stores on HiDPI change.
+ property_scale_factor().signal_changed().connect([this] { d->schedule_redraw(); });
+
+ // OpenGL switch.
+ set_opengl_enabled(d->prefs.request_opengl);
+
+ // Async redraw process.
+ d->numthreads = d->get_numthreads();
+ d->pool.emplace(d->numthreads);
+
+ d->sync.connectExit([this] { d->after_redraw(); });
+}
+
+int CanvasPrivate::get_numthreads() const
+{
+ if (int n = prefs.numthreads; n > 0) {
+ // First choice is the value set in preferences.
+ return n;
+ } else if (int n = std::thread::hardware_concurrency(); n > 0) {
+ // If not set, use the number of processors minus one. (Using all of them causes stuttering.)
+ return n == 1 ? 1 : n - 1;
+ } else {
+ // If not reported, use a sensible fallback.
+ return 4;
+ }
+}
+
+// Graphics becomes active when the widget is realized.
+void CanvasPrivate::activate_graphics()
+{
+ if (q->get_opengl_enabled()) {
+ q->make_current();
+ graphics = Graphics::create_gl(prefs, stores, pi);
+ } else {
+ graphics = Graphics::create_cairo(prefs, stores, pi);
+ }
+ stores.set_graphics(graphics.get());
+ stores.reset();
+}
+
+// After graphics becomes active, the canvas becomes active when additionally a drawing is set.
+void CanvasPrivate::activate()
+{
+ // Event handling/item picking
+ q->_pick_event.type = GDK_LEAVE_NOTIFY;
+ q->_pick_event.crossing.x = 0;
+ q->_pick_event.crossing.y = 0;
+
+ q->_in_repick = false;
+ q->_left_grabbed_item = false;
+ q->_all_enter_events = false;
+ q->_is_dragging = false;
+ q->_state = 0;
+
+ q->_current_canvas_item = nullptr;
+ q->_current_canvas_item_new = nullptr;
+ q->_grabbed_canvas_item = nullptr;
+ q->_grabbed_event_mask = (Gdk::EventMask)0;
+ pre_scroll_grabbed_item = nullptr;
+
+ // Drawing
+ q->_need_update = true;
+
+ // Split view
+ q->_split_dragging = false;
+
+ // Todo: Disable GTK event compression again when doing so is no longer buggy.
+ // Note: ToolBase::set_high_motion_precision() will keep turning it back on.
+ // q->get_window()->set_event_compression(false);
+
+ active = true;
+
+ schedule_redraw();
+}
+
+void CanvasPrivate::deactivate()
+{
+ active = false;
+
+ if (redraw_active) {
+ if (schedule_redraw_conn.connected()) {
+ // In first link in chain, from schedule_redraw() to launch_redraw(). Break the link and exit.
+ schedule_redraw_conn.disconnect();
+ } else {
+ // Otherwise, the background process is running. Interrupt the signal chain at exit.
+ abort_flags.store((int)AbortFlags::Hard, std::memory_order_relaxed);
+ if (prefs.debug_logging) std::cout << "Hard exit request" << std::endl;
+ sync.waitForExit();
+
+ // Unsnapshot the CanvasItems and DrawingItems.
+ canvasitem_ctx->unsnapshot();
+ q->_drawing->unsnapshot();
+ }
+
+ redraw_active = false;
+ redraw_requested = false;
+ assert(!schedule_redraw_conn.connected());
+ }
+}
+
+void CanvasPrivate::deactivate_graphics()
+{
+ if (q->get_opengl_enabled()) q->make_current();
+ commit_tiles();
+ stores.set_graphics(nullptr);
+ graphics.reset();
+}
+
+Canvas::~Canvas()
+{
+ // Remove entire CanvasItem tree.
+ d->canvasitem_ctx.reset();
+}
+
+void Canvas::set_drawing(Drawing *drawing)
+{
+ if (d->active && !drawing) d->deactivate();
+ _drawing = drawing;
+ if (_drawing) {
+ _drawing->setRenderMode(_render_mode == RenderMode::OUTLINE_OVERLAY ? RenderMode::NORMAL : _render_mode);
+ _drawing->setColorMode(_color_mode);
+ _drawing->setOutlineOverlay(d->outlines_required());
+ }
+ if (!d->active && get_realized() && drawing) d->activate();
+}
+
+CanvasItemGroup *Canvas::get_canvas_item_root() const
+{
+ return d->canvasitem_ctx->root();
+}
+
+void Canvas::on_realize()
+{
+ parent_type::on_realize();
+ d->activate_graphics();
+ if (_drawing) d->activate();
+}
+
+void Canvas::on_unrealize()
+{
+ if (_drawing) d->deactivate();
+ d->deactivate_graphics();
+ parent_type::on_unrealize();
+}
+
+/*
+ * Redraw process managment
+ */
+
+// Schedule another redraw iteration to take place, waiting for the current one to finish if necessary.
+void CanvasPrivate::schedule_redraw()
+{
+ if (!active) {
+ // We can safely discard calls until active, because we will run an iteration on activation later in initialisation.
+ return;
+ }
+
+ // Ensure another iteration is performed if one is in progress.
+ redraw_requested = true;
+
+ if (redraw_active) {
+ return;
+ }
+
+ redraw_active = true;
+
+ // Call run_redraw() as soon as possible on the main loop. (Cannot run now since CanvasItem tree could be in an invalid intermediate state.)
+ assert(!schedule_redraw_conn.connected());
+ schedule_redraw_conn = Glib::signal_idle().connect([this] {
+ if (q->get_opengl_enabled()) {
+ q->make_current();
+ }
+ if (prefs.debug_logging) std::cout << "Redraw start" << std::endl;
+ launch_redraw();
+ return false;
+ }, Glib::PRIORITY_HIGH);
+}
+
+// Update state and launch redraw process in background. Requires a current OpenGL context.
+void CanvasPrivate::launch_redraw()
+{
+ assert(redraw_active);
+
+ // Determine whether the rendering parameters have changed, and trigger full store recreation if so.
+ if ((outlines_required() && !outlines_enabled) || scale_factor != q->get_scale_factor()) {
+ stores.reset();
+ }
+
+ outlines_enabled = outlines_required();
+ scale_factor = q->get_scale_factor();
+
+ graphics->set_outlines_enabled(outlines_enabled);
+ graphics->set_scale_factor(scale_factor);
+
+ /*
+ * Update state.
+ */
+
+ // Page information.
+ pi.pages.clear();
+ canvasitem_ctx->root()->visit_page_rects([this] (auto &rect) {
+ pi.pages.emplace_back(rect);
+ });
+
+ graphics->set_colours(page, desk, border);
+ graphics->set_background_in_stores(background_in_stores_required());
+
+ q->_drawing->setClip(calc_page_clip());
+
+ // Stores.
+ handle_stores_action(stores.update(Fragment{ q->_affine, q->get_area_world() }));
+
+ // Geometry.
+ bool const affine_changed = canvasitem_ctx->affine() != stores.store().affine;
+ if (q->_need_update || affine_changed) {
+ FrameCheck::Event fc;
+ if (prefs.debug_framecheck) fc = FrameCheck::Event("update");
+ q->_need_update = false;
+ canvasitem_ctx->setAffine(stores.store().affine);
+ canvasitem_ctx->root()->update(affine_changed);
+ }
+
+ // Update strategy.
+ auto const strategy = pref_to_updater(prefs.update_strategy);
+ if (updater->get_strategy() != strategy) {
+ auto new_updater = Updater::create(strategy);
+ new_updater->clean_region = std::move(updater->clean_region);
+ updater = std::move(new_updater);
+ }
+
+ updater->mark_dirty(invalidated);
+ invalidated = Cairo::Region::create();
+
+ updater->next_frame();
+
+ /*
+ * Launch redraw process in background.
+ */
+
+ // If asked to, don't paint anything and instead halt the redraw process.
+ if (prefs.debug_disable_redraw) {
+ redraw_active = false;
+ return;
+ }
+
+ // Snapshot the CanvasItems and DrawingItems.
+ canvasitem_ctx->snapshot();
+ q->_drawing->snapshot();
+
+ // Get the mouse position in screen space.
+ rd.mouse_loc = last_mouse.value_or((Geom::Point(q->get_dimensions()) / 2).round());
+
+ // Map the mouse to canvas space.
+ rd.mouse_loc += q->_pos;
+ if (stores.mode() == Stores::Mode::Decoupled) {
+ rd.mouse_loc = (Geom::Point(rd.mouse_loc) * q->_affine.inverse() * stores.store().affine).round();
+ }
+
+ // Get the visible rect.
+ rd.visible = q->get_area_world();
+ if (stores.mode() == Stores::Mode::Decoupled) {
+ rd.visible = (Geom::Parallelogram(rd.visible) * q->_affine.inverse() * stores.store().affine).bounds().roundOutwards();
+ }
+
+ // Get other misc data.
+ rd.store = Fragment{ stores.store().affine, stores.store().rect };
+ rd.decoupled_mode = stores.mode() == Stores::Mode::Decoupled;
+ rd.coarsener_min_size = prefs.coarsener_min_size;
+ rd.coarsener_glue_size = prefs.coarsener_glue_size;
+ rd.coarsener_min_fullness = prefs.coarsener_min_fullness;
+ rd.tile_size = prefs.tile_size;
+ rd.preempt = prefs.preempt;
+ rd.margin = prefs.prerender;
+ rd.redraw_delay = prefs.debug_delay_redraw ? std::make_optional<int>(prefs.debug_delay_redraw_time) : std::nullopt;
+ rd.render_time_limit = prefs.render_time_limit;
+ rd.numthreads = get_numthreads();
+ rd.background_in_stores_required = background_in_stores_required();
+ rd.page = page;
+ rd.desk = desk;
+ rd.debug_framecheck = prefs.debug_framecheck;
+ rd.debug_show_redraw = prefs.debug_show_redraw;
+
+ rd.snapshot_drawn = stores.snapshot().drawn ? stores.snapshot().drawn->copy() : Cairo::RefPtr<Cairo::Region>();
+ rd.grabbed = q->_grabbed_canvas_item && prefs.block_updates ? (roundedOutwards(q->_grabbed_canvas_item->get_bounds()) & rd.visible & rd.store.rect).regularized() : Geom::OptIntRect();
+
+ abort_flags.store((int)AbortFlags::None, std::memory_order_relaxed);
+
+ boost::asio::post(*pool, [this] { init_tiler(); });
+}
+
+void CanvasPrivate::after_redraw()
+{
+ assert(redraw_active);
+
+ // Unsnapshot the CanvasItems and DrawingItems.
+ canvasitem_ctx->unsnapshot();
+ q->_drawing->unsnapshot();
+
+ // OpenGL context needed for commit_tiles(), stores.finished_draw(), and launch_redraw().
+ if (q->get_opengl_enabled()) {
+ q->make_current();
+ }
+
+ // Commit tiles before stores.finished_draw() to avoid changing stores while tiles are still pending.
+ commit_tiles();
+
+ // Handle any pending stores action.
+ bool stores_changed = false;
+ if (!rd.timeoutflag) {
+ auto const ret = stores.finished_draw(Fragment{ q->_affine, q->get_area_world() });
+ handle_stores_action(ret);
+ if (ret != Stores::Action::None) {
+ stores_changed = true;
+ }
+ }
+
+ // Relaunch or stop as necessary.
+ if (rd.timeoutflag || redraw_requested || stores_changed) {
+ if (prefs.debug_logging) std::cout << "Continuing redrawing" << std::endl;
+ redraw_requested = false;
+ launch_redraw();
+ } else {
+ if (prefs.debug_logging) std::cout << "Redraw exit" << std::endl;
+ redraw_active = false;
+ }
+}
+
+void CanvasPrivate::handle_stores_action(Stores::Action action)
+{
+ switch (action) {
+ case Stores::Action::Recreated:
+ // Set everything as needing redraw.
+ invalidated->do_union(geom_to_cairo(stores.store().rect));
+ updater->reset();
+
+ if (prefs.debug_show_unclean) q->queue_draw();
+ break;
+
+ case Stores::Action::Shifted:
+ invalidated->intersect(geom_to_cairo(stores.store().rect));
+ updater->intersect(stores.store().rect);
+
+ if (prefs.debug_show_unclean) q->queue_draw();
+ break;
+
+ default:
+ break;
+ }
+
+ if (action != Stores::Action::None) {
+ q->_drawing->setCacheLimit(stores.store().rect);
+ }
+}
+
+// Commit all in-flight tiles to the stores. Requires a current OpenGL context (for graphics->draw_tile).
+void CanvasPrivate::commit_tiles()
+{
+ framecheck_whole_function(this)
+
+ decltype(rd.tiles) tiles;
+
+ {
+ auto lock = std::lock_guard(rd.tiles_mutex);
+ tiles = std::move(rd.tiles);
+ }
+
+ for (auto &tile : tiles) {
+ // Todo: Make CMS system thread-safe, then move this to render thread too.
+ if (q->_cms_active) {
+ auto transf = prefs.from_display
+ ? Inkscape::CMSSystem::getDisplayPer(q->_cms_key)
+ : Inkscape::CMSSystem::getDisplayTransform();
+ if (transf) {
+ tile.surface->flush();
+ auto px = tile.surface->get_data();
+ int stride = tile.surface->get_stride();
+ for (int i = 0; i < tile.surface->get_height(); i++) {
+ auto row = px + i * stride;
+ Inkscape::CMSSystem::doTransform(transf, row, row, tile.surface->get_width());
+ }
+ tile.surface->mark_dirty();
+ }
+ }
+
+ // Paste tile content onto stores.
+ graphics->draw_tile(tile.fragment, std::move(tile.surface), std::move(tile.outline_surface));
+
+ // Add to drawn region.
+ assert(stores.store().rect.contains(tile.fragment.rect));
+ stores.mark_drawn(tile.fragment.rect);
+
+ // Get the rectangle of screen-space needing repaint.
+ Geom::IntRect repaint_rect;
+ if (stores.mode() == Stores::Mode::Normal) {
+ // Simply translate to get back to screen space.
+ repaint_rect = tile.fragment.rect - q->_pos;
+ } else {
+ // Transform into screen space, take bounding box, and round outwards.
+ auto pl = Geom::Parallelogram(tile.fragment.rect);
+ pl *= stores.store().affine.inverse() * q->_affine;
+ pl *= Geom::Translate(-q->_pos);
+ repaint_rect = pl.bounds().roundOutwards();
+ }
+
+ // Check if repaint is necessary - some rectangles could be entirely off-screen.
+ auto screen_rect = Geom::IntRect({0, 0}, q->get_dimensions());
+ if ((repaint_rect & screen_rect).regularized()) {
+ // Schedule repaint.
+ queue_draw_area(repaint_rect);
+ }
+ }
+}
+
+/*
+ * Auto-scrolling
+ */
+
+static Geom::Point cap_length(Geom::Point const &pt, double max)
+{
+ auto const r = pt.length();
+ return r <= max ? pt : pt * max / r;
+}
+
+static double profile(double r)
+{
+ constexpr double max_speed = 30.0;
+ constexpr double max_distance = 25.0;
+ return std::clamp(Geom::sqr(r / max_distance) * max_speed, 1.0, max_speed);
+}
+
+static Geom::Point apply_profile(Geom::Point const &pt)
+{
+ auto const r = pt.length();
+ if (r <= Geom::EPSILON) return {};
+ return pt * profile(r) / r;
+}
+
+void CanvasPrivate::autoscroll_begin(Geom::IntPoint const &to)
+{
+ if (!q->_desktop) {
+ return;
+ }
+
+ auto const rect = expandedBy(Geom::IntRect({}, q->get_dimensions()), -(int)prefs.autoscrolldistance);
+ strain = to - rect.clamp(to);
+
+ if (strain == Geom::IntPoint(0, 0) || tick_callback) {
+ return;
+ }
+
+ tick_callback = q->add_tick_callback([this] (Glib::RefPtr<Gdk::FrameClock> const &clock) {
+ auto timings = clock->get_current_timings();
+ auto const t = timings->get_frame_time();
+ double dt;
+ if (last_time) {
+ dt = t - *last_time;
+ } else {
+ dt = timings->get_refresh_interval();
+ }
+ last_time = t;
+ dt *= 60.0 / 1e6 * prefs.autoscrollspeed;
+
+ bool const strain_zero = strain == Geom::IntPoint(0, 0);
+
+ if (strain.x() * velocity.x() < 0) velocity.x() = 0;
+ if (strain.y() * velocity.y() < 0) velocity.y() = 0;
+ auto const tgtvel = apply_profile(strain);
+ auto const max_accel = strain_zero ? 3 : 2;
+ velocity += cap_length(tgtvel - velocity, max_accel * dt);
+ displacement += velocity * dt;
+ auto const dpos = displacement.round();
+ q->_desktop->scroll_relative(-dpos);
+ displacement -= dpos;
+
+ if (last_mouse) {
+ GdkEventMotion event;
+ memset(&event, 0, sizeof(GdkEventMotion));
+ event.type = GDK_MOTION_NOTIFY;
+ event.x = last_mouse->x();
+ event.y = last_mouse->y();
+ event.state = q->_state;
+ emit_event(reinterpret_cast<GdkEvent*>(&event));
+ }
+
+ if (strain_zero && velocity.length() <= 0.1) {
+ tick_callback = {};
+ last_time = {};
+ displacement = velocity = {};
+ return false;
+ }
+
+ q->queue_draw();
+
+ return true;
+ });
+}
+
+void CanvasPrivate::autoscroll_end()
+{
+ strain = {};
+}
+
+// Allow auto-scrolling to take place if the mouse reaches the edge.
+// The effect wears off when the mouse is next released.
+void Canvas::enable_autoscroll()
+{
+ if (d->last_mouse) {
+ d->autoscroll_begin(*d->last_mouse);
+ } else {
+ d->autoscroll_end();
+ }
+}
+
+/*
+ * Event handling
+ */
+
+bool Canvas::on_scroll_event(GdkEventScroll *scroll_event)
+{
+ return d->process_event(reinterpret_cast<GdkEvent*>(scroll_event));
+}
+
+bool Canvas::on_button_press_event(GdkEventButton *button_event)
+{
+ return on_button_event(button_event);
+}
+
+bool Canvas::on_button_release_event(GdkEventButton *button_event)
+{
+ if (button_event->button == 1) {
+ d->autoscroll_end();
+ }
+
+ return on_button_event(button_event);
+}
+
+// Unified handler for press and release events.
+bool Canvas::on_button_event(GdkEventButton *button_event)
+{
+ // Sanity-check event type.
+ switch (button_event->type) {
+ case GDK_BUTTON_PRESS:
+ case GDK_2BUTTON_PRESS:
+ case GDK_3BUTTON_PRESS:
+ case GDK_BUTTON_RELEASE:
+ break; // Good
+ default:
+ std::cerr << "Canvas::on_button_event: illegal event type!" << std::endl;
+ return false;
+ }
+
+ // Drag the split view controller.
+ if (_split_mode == Inkscape::SplitMode::SPLIT) {
+ auto cursor_position = Geom::IntPoint(button_event->x, button_event->y);
+ switch (button_event->type) {
+ case GDK_BUTTON_PRESS:
+ if (_hover_direction != Inkscape::SplitDirection::NONE) {
+ _split_dragging = true;
+ _split_drag_start = cursor_position;
+ return true;
+ }
+ break;
+ case GDK_2BUTTON_PRESS:
+ if (_hover_direction != Inkscape::SplitDirection::NONE) {
+ _split_direction = _hover_direction;
+ _split_dragging = false;
+ queue_draw();
+ return true;
+ }
+ break;
+ case GDK_BUTTON_RELEASE:
+ if (!_split_dragging) break;
+ _split_dragging = false;
+
+ // Check if we are near the edge. If so, revert to normal mode.
+ if (cursor_position.x() < 5 ||
+ cursor_position.y() < 5 ||
+ cursor_position.x() > get_allocation().get_width() - 5 ||
+ cursor_position.y() > get_allocation().get_height() - 5)
+ {
+ // Reset everything.
+ _split_frac = {0.5, 0.5};
+ set_cursor();
+ set_split_mode(Inkscape::SplitMode::NORMAL);
+
+ // Update action (turn into utility function?).
+ auto window = dynamic_cast<Gtk::ApplicationWindow*>(get_toplevel());
+ if (!window) {
+ std::cerr << "Canvas::on_motion_notify_event: window missing!" << std::endl;
+ return true;
+ }
+
+ auto action = window->lookup_action("canvas-split-mode");
+ if (!action) {
+ std::cerr << "Canvas::on_motion_notify_event: action 'canvas-split-mode' missing!" << std::endl;
+ return true;
+ }
+
+ auto saction = Glib::RefPtr<Gio::SimpleAction>::cast_dynamic(action);
+ if (!saction) {
+ std::cerr << "Canvas::on_motion_notify_event: action 'canvas-split-mode' not SimpleAction!" << std::endl;
+ return true;
+ }
+
+ saction->change_state(static_cast<int>(Inkscape::SplitMode::NORMAL));
+ }
+
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ return d->process_event(reinterpret_cast<GdkEvent*>(button_event));
+}
+
+bool Canvas::on_enter_notify_event(GdkEventCrossing *crossing_event)
+{
+ if (crossing_event->window != get_window()->gobj()) {
+ return false;
+ }
+ return d->process_event(reinterpret_cast<GdkEvent*>(crossing_event));
+}
+
+bool Canvas::on_leave_notify_event(GdkEventCrossing *crossing_event)
+{
+ if (crossing_event->window != get_window()->gobj()) {
+ return false;
+ }
+ d->last_mouse = {};
+ return d->process_event(reinterpret_cast<GdkEvent*>(crossing_event));
+}
+
+bool Canvas::on_focus_in_event(GdkEventFocus *focus_event)
+{
+ grab_focus();
+ return false;
+}
+
+bool Canvas::on_key_press_event(GdkEventKey *key_event)
+{
+ return d->process_event(reinterpret_cast<GdkEvent*>(key_event));
+}
+
+bool Canvas::on_key_release_event(GdkEventKey *key_event)
+{
+ return d->process_event(reinterpret_cast<GdkEvent*>(key_event));
+}
+
+bool Canvas::on_motion_notify_event(GdkEventMotion *motion_event)
+{
+ // Record the last mouse position.
+ d->last_mouse = Geom::IntPoint(motion_event->x, motion_event->y);
+
+ // Handle interactions with the split view controller.
+ if (_split_mode == Inkscape::SplitMode::XRAY) {
+ queue_draw();
+ } else if (_split_mode == Inkscape::SplitMode::SPLIT) {
+ auto cursor_position = Geom::IntPoint(motion_event->x, motion_event->y);
+
+ // Move controller.
+ if (_split_dragging) {
+ auto delta = cursor_position - _split_drag_start;
+ if (_hover_direction == Inkscape::SplitDirection::HORIZONTAL) {
+ delta.x() = 0;
+ } else if (_hover_direction == Inkscape::SplitDirection::VERTICAL) {
+ delta.y() = 0;
+ }
+ _split_frac += Geom::Point(delta) / get_dimensions();
+ _split_drag_start = cursor_position;
+ queue_draw();
+ return true;
+ }
+
+ auto split_position = (_split_frac * get_dimensions()).round();
+ auto diff = cursor_position - split_position;
+ auto hover_direction = Inkscape::SplitDirection::NONE;
+ if (Geom::Point(diff).length() < 20.0) {
+ // We're hovering over circle, figure out which direction we are in.
+ if (diff.y() - diff.x() > 0) {
+ if (diff.y() + diff.x() > 0) {
+ hover_direction = Inkscape::SplitDirection::SOUTH;
+ } else {
+ hover_direction = Inkscape::SplitDirection::WEST;
+ }
+ } else {
+ if (diff.y() + diff.x() > 0) {
+ hover_direction = Inkscape::SplitDirection::EAST;
+ } else {
+ hover_direction = Inkscape::SplitDirection::NORTH;
+ }
+ }
+ } else if (_split_direction == Inkscape::SplitDirection::NORTH ||
+ _split_direction == Inkscape::SplitDirection::SOUTH)
+ {
+ if (std::abs(diff.y()) < 3) {
+ // We're hovering over the horizontal line.
+ hover_direction = Inkscape::SplitDirection::HORIZONTAL;
+ }
+ } else {
+ if (std::abs(diff.x()) < 3) {
+ // We're hovering over the vertical line.
+ hover_direction = Inkscape::SplitDirection::VERTICAL;
+ }
+ }
+
+ if (_hover_direction != hover_direction) {
+ _hover_direction = hover_direction;
+ set_cursor();
+ queue_draw();
+ }
+
+ if (_hover_direction != Inkscape::SplitDirection::NONE) {
+ // We're hovering, don't pick or emit event.
+ return true;
+ }
+ }
+
+ // Avoid embarrassing neverending autoscroll in case the button-released handler somehow doesn't fire.
+ if (!(motion_event->state & (GDK_BUTTON1_MASK | GDK_BUTTON2_MASK | GDK_BUTTON3_MASK))) {
+ d->autoscroll_end();
+ }
+
+ return d->process_event(reinterpret_cast<GdkEvent*>(motion_event));
+}
+
+// Unified handler for all events.
+bool CanvasPrivate::process_event(const GdkEvent *event)
+{
+ framecheck_whole_function(this)
+
+ if (!active) {
+ std::cerr << "Canvas::process_event: Called while not active!" << std::endl;
+ return false;
+ }
+
+ auto calc_button_mask = [&] () -> int {
+ switch (event->button.button) {
+ case 1: return GDK_BUTTON1_MASK; break;
+ case 2: return GDK_BUTTON2_MASK; break;
+ case 3: return GDK_BUTTON3_MASK; break;
+ case 4: return GDK_BUTTON4_MASK; break;
+ case 5: return GDK_BUTTON5_MASK; break;
+ default: return 0; // Buttons can range at least to 9 but mask defined only to 5.
+ }
+ };
+
+ // Do event-specific processing.
+ switch (event->type) {
+ case GDK_SCROLL:
+ {
+ // Save the current event-receiving item just before scrolling starts. It will continue to receive scroll events until the mouse is moved.
+ if (!pre_scroll_grabbed_item) {
+ pre_scroll_grabbed_item = q->_current_canvas_item;
+ if (q->_grabbed_canvas_item && !q->_current_canvas_item->is_descendant_of(q->_grabbed_canvas_item)) {
+ pre_scroll_grabbed_item = q->_grabbed_canvas_item;
+ }
+ }
+
+ // Process the scroll event...
+ bool retval = emit_event(event);
+
+ // ...then repick.
+ q->_state = event->scroll.state;
+ pick_current_item(event);
+
+ return retval;
+ }
+
+ case GDK_BUTTON_PRESS:
+ case GDK_2BUTTON_PRESS:
+ case GDK_3BUTTON_PRESS:
+ {
+ pre_scroll_grabbed_item = nullptr;
+
+ // Pick the current item as if the button were not pressed...
+ q->_state = event->button.state;
+ pick_current_item(event);
+
+ // ...then process the event.
+ q->_state ^= calc_button_mask();
+ return emit_event(event);
+ }
+
+ case GDK_BUTTON_RELEASE:
+ {
+ pre_scroll_grabbed_item = nullptr;
+
+ // Process the event as if the button were pressed...
+ q->_state = event->button.state;
+ bool retval = emit_event(event);
+
+ // ...then repick after the button has been released.
+ auto event_copy = make_unique_copy(event);
+ event_copy->button.state ^= calc_button_mask();
+ q->_state = event_copy->button.state;
+ pick_current_item(event_copy.get());
+
+ return retval;
+ }
+
+ case GDK_ENTER_NOTIFY:
+ pre_scroll_grabbed_item = nullptr;
+ q->_state = event->crossing.state;
+ return pick_current_item(event);
+
+ case GDK_LEAVE_NOTIFY:
+ pre_scroll_grabbed_item = nullptr;
+ q->_state = event->crossing.state;
+ // This is needed to remove alignment or distribution snap indicators.
+ if (q->_desktop) {
+ q->_desktop->snapindicator->remove_snaptarget();
+ }
+ return pick_current_item(event);
+
+ case GDK_KEY_PRESS:
+ case GDK_KEY_RELEASE:
+ return emit_event(event);
+
+ case GDK_MOTION_NOTIFY:
+ pre_scroll_grabbed_item = nullptr;
+ q->_state = event->motion.state;
+ pick_current_item(event);
+ return emit_event(event);
+
+ default:
+ return false;
+ }
+}
+
+// This function is called by 'process_event' to manipulate the state variables relating
+// to the current object under the mouse, for example, to generate enter and leave events.
+//
+// This routine reacts to events from the canvas. Its main purpose is to find the canvas item
+// closest to the cursor where the event occurred and then send the event (sometimes modified) to
+// that item. The event then bubbles up the canvas item tree until an object handles it. If the
+// widget is redrawn, this routine may be called again for the same event.
+//
+// Canvas items register their interest by connecting to the "event" signal.
+// Example in desktop.cpp:
+// canvas_catchall->connect_event(sigc::bind(sigc::ptr_fun(sp_desktop_root_handler), this));
+bool CanvasPrivate::pick_current_item(const GdkEvent *event)
+{
+ // Ensure requested geometry updates are performed first.
+ if (q->_need_update && !q->_drawing->snapshotted() && !canvasitem_ctx->snapshotted()) {
+ FrameCheck::Event fc;
+ if (prefs.debug_framecheck) fc = FrameCheck::Event("update", 1);
+ q->_need_update = false;
+ canvasitem_ctx->root()->update(false);
+ }
+
+ int button_down = 0;
+ if (!q->_all_enter_events) {
+ // Only set true in connector-tool.cpp.
+
+ // If a button is down, we'll perform enter and leave events on the
+ // current item, but not enter on any other item. This is more or
+ // less like X pointer grabbing for canvas items.
+ button_down = q->_state & (GDK_BUTTON1_MASK |
+ GDK_BUTTON2_MASK |
+ GDK_BUTTON3_MASK |
+ GDK_BUTTON4_MASK |
+ GDK_BUTTON5_MASK);
+ if (!button_down) q->_left_grabbed_item = false;
+ }
+
+ // Save the event in the canvas. This is used to synthesize enter and
+ // leave events in case the current item changes. It is also used to
+ // re-pick the current item if the current one gets deleted. Also,
+ // synthesize an enter event.
+ if (event != &q->_pick_event) {
+ if (event->type == GDK_MOTION_NOTIFY || event->type == GDK_SCROLL || event->type == GDK_BUTTON_RELEASE) {
+ // Convert to GDK_ENTER_NOTIFY
+
+ // These fields have the same offsets in all types of events.
+ q->_pick_event.crossing.type = GDK_ENTER_NOTIFY;
+ q->_pick_event.crossing.window = event->motion.window;
+ q->_pick_event.crossing.send_event = event->motion.send_event;
+ q->_pick_event.crossing.subwindow = nullptr;
+ q->_pick_event.crossing.x = event->motion.x;
+ q->_pick_event.crossing.y = event->motion.y;
+ q->_pick_event.crossing.mode = GDK_CROSSING_NORMAL;
+ q->_pick_event.crossing.detail = GDK_NOTIFY_NONLINEAR;
+ q->_pick_event.crossing.focus = false;
+
+ // These fields don't have the same offsets in all types of events.
+ switch (event->type)
+ {
+ case GDK_MOTION_NOTIFY:
+ q->_pick_event.crossing.state = event->motion.state;
+ q->_pick_event.crossing.x_root = event->motion.x_root;
+ q->_pick_event.crossing.y_root = event->motion.y_root;
+ break;
+ case GDK_SCROLL:
+ q->_pick_event.crossing.state = event->scroll.state;
+ q->_pick_event.crossing.x_root = event->scroll.x_root;
+ q->_pick_event.crossing.y_root = event->scroll.y_root;
+ break;
+ case GDK_BUTTON_RELEASE:
+ q->_pick_event.crossing.state = event->button.state;
+ q->_pick_event.crossing.x_root = event->button.x_root;
+ q->_pick_event.crossing.y_root = event->button.y_root;
+ break;
+ default:
+ assert(false);
+ }
+
+ } else {
+ q->_pick_event = *event;
+ }
+ }
+
+ if (q->_in_repick) {
+ // Don't do anything else if this is a recursive call.
+ return false;
+ }
+
+ // Find new item
+ q->_current_canvas_item_new = nullptr;
+
+ if (q->_pick_event.type != GDK_LEAVE_NOTIFY && canvasitem_ctx->root()->is_visible()) {
+ // Leave notify means there is no current item.
+ // Find closest item.
+ double x = 0.0;
+ double y = 0.0;
+
+ if (q->_pick_event.type == GDK_ENTER_NOTIFY) {
+ x = q->_pick_event.crossing.x;
+ y = q->_pick_event.crossing.y;
+ } else {
+ x = q->_pick_event.motion.x;
+ y = q->_pick_event.motion.y;
+ }
+
+ // Look at where the cursor is to see if one should pick with outline mode.
+ bool outline = q->canvas_point_in_outline_zone({ x, y });
+
+ // Convert to world coordinates.
+ auto p = Geom::Point(x, y) + q->_pos;
+ if (stores.mode() == Stores::Mode::Decoupled) {
+ p *= q->_affine.inverse() * canvasitem_ctx->affine();
+ }
+
+ q->_drawing->getCanvasItemDrawing()->set_pick_outline(outline);
+ q->_current_canvas_item_new = canvasitem_ctx->root()->pick_item(p);
+ // if (q->_current_canvas_item_new) {
+ // std::cout << " PICKING: FOUND ITEM: " << q->_current_canvas_item_new->get_name() << std::endl;
+ // } else {
+ // std::cout << " PICKING: DID NOT FIND ITEM" << std::endl;
+ // }
+ }
+
+ if (q->_current_canvas_item_new == q->_current_canvas_item && !q->_left_grabbed_item) {
+ // Current item did not change!
+ return false;
+ }
+
+ // Synthesize events for old and new current items.
+ bool retval = false;
+ if (q->_current_canvas_item_new != q->_current_canvas_item &&
+ q->_current_canvas_item != nullptr &&
+ !q->_left_grabbed_item ) {
+
+ GdkEvent new_event;
+ new_event = q->_pick_event;
+ new_event.type = GDK_LEAVE_NOTIFY;
+ new_event.crossing.detail = GDK_NOTIFY_ANCESTOR;
+ new_event.crossing.subwindow = nullptr;
+ q->_in_repick = true;
+ retval = emit_event(&new_event);
+ q->_in_repick = false;
+ }
+
+ if (q->_all_enter_events == false) {
+ // new_current_item may have been set to nullptr during the call to emitEvent() above.
+ if (q->_current_canvas_item_new != q->_current_canvas_item && button_down) {
+ q->_left_grabbed_item = true;
+ return retval;
+ }
+ }
+
+ // Handle the rest of cases
+ q->_left_grabbed_item = false;
+ q->_current_canvas_item = q->_current_canvas_item_new;
+
+ if (q->_current_canvas_item != nullptr) {
+ GdkEvent new_event;
+ new_event = q->_pick_event;
+ new_event.type = GDK_ENTER_NOTIFY;
+ new_event.crossing.detail = GDK_NOTIFY_ANCESTOR;
+ new_event.crossing.subwindow = nullptr;
+ retval = emit_event(&new_event);
+ }
+
+ return retval;
+}
+
+// Fires an event at the canvas, after a little pre-processing. Returns true if handled.
+bool CanvasPrivate::emit_event(const GdkEvent *event)
+{
+ // Handle grabbed items.
+ if (q->_grabbed_canvas_item) {
+ auto mask = (Gdk::EventMask)0;
+
+ switch (event->type) {
+ case GDK_ENTER_NOTIFY:
+ mask = Gdk::ENTER_NOTIFY_MASK;
+ break;
+ case GDK_LEAVE_NOTIFY:
+ mask = Gdk::LEAVE_NOTIFY_MASK;
+ break;
+ case GDK_MOTION_NOTIFY:
+ mask = Gdk::POINTER_MOTION_MASK;
+ break;
+ case GDK_BUTTON_PRESS:
+ case GDK_2BUTTON_PRESS:
+ case GDK_3BUTTON_PRESS:
+ mask = Gdk::BUTTON_PRESS_MASK;
+ break;
+ case GDK_BUTTON_RELEASE:
+ mask = Gdk::BUTTON_RELEASE_MASK;
+ break;
+ case GDK_KEY_PRESS:
+ mask = Gdk::KEY_PRESS_MASK;
+ break;
+ case GDK_KEY_RELEASE:
+ mask = Gdk::KEY_RELEASE_MASK;
+ break;
+ case GDK_SCROLL:
+ mask = Gdk::SCROLL_MASK;
+ mask |= Gdk::SMOOTH_SCROLL_MASK;
+ break;
+ default:
+ break;
+ }
+
+ if (!(mask & q->_grabbed_event_mask)) {
+ return false;
+ }
+ }
+
+ // Convert to world coordinates. We have two different cases due to different event structures.
+ auto conv = [&, this] (double &x, double &y) {
+ auto p = Geom::Point(x, y) + q->_pos;
+ if (stores.mode() == Stores::Mode::Decoupled) {
+ p *= q->_affine.inverse() * canvasitem_ctx->affine();
+ }
+ x = p.x();
+ y = p.y();
+ };
+
+ auto event_copy = make_unique_copy(event);
+
+ switch (event->type) {
+ case GDK_ENTER_NOTIFY:
+ case GDK_LEAVE_NOTIFY:
+ conv(event_copy->crossing.x, event_copy->crossing.y);
+ break;
+ case GDK_MOTION_NOTIFY:
+ case GDK_BUTTON_PRESS:
+ case GDK_2BUTTON_PRESS:
+ case GDK_3BUTTON_PRESS:
+ case GDK_BUTTON_RELEASE:
+ conv(event_copy->motion.x, event_copy->motion.y);
+ break;
+ default:
+ break;
+ }
+
+ // Block undo/redo while anything is dragged.
+ if (event->type == GDK_BUTTON_PRESS && event->button.button == 1) {
+ q->_is_dragging = true;
+ } else if (event->type == GDK_BUTTON_RELEASE) {
+ q->_is_dragging = false;
+ }
+
+ if (q->_current_canvas_item) {
+ // Choose where to send event.
+ auto item = q->_current_canvas_item;
+
+ if (q->_grabbed_canvas_item && !q->_current_canvas_item->is_descendant_of(q->_grabbed_canvas_item)) {
+ item = q->_grabbed_canvas_item;
+ }
+
+ if (pre_scroll_grabbed_item && event->type == GDK_SCROLL) {
+ item = pre_scroll_grabbed_item;
+ }
+
+ // Propagate the event up the canvas item hierarchy until handled.
+ while (item) {
+ if (item->handle_event(event_copy.get())) return true;
+ item = item->get_parent();
+ }
+ }
+
+ return false;
+}
+
+/*
+ * Protected functions
+ */
+
+Geom::IntPoint Canvas::get_dimensions() const
+{
+ return dimensions(get_allocation());
+}
+
+/**
+ * Is world point inside canvas area?
+ */
+bool Canvas::world_point_inside_canvas(Geom::Point const &world) const
+{
+ return get_area_world().contains(world.floor());
+}
+
+/**
+ * Translate point in canvas to world coordinates.
+ */
+Geom::Point Canvas::canvas_to_world(Geom::Point const &point) const
+{
+ return point + _pos;
+}
+
+/**
+ * Return the area shown in the canvas in world coordinates.
+ */
+Geom::IntRect Canvas::get_area_world() const
+{
+ return Geom::IntRect(_pos, _pos + get_dimensions());
+}
+
+/**
+ * Return whether a point in screen space / canvas coordinates is inside the region
+ * of the canvas where things respond to mouse clicks as if they are in outline mode.
+ */
+bool Canvas::canvas_point_in_outline_zone(Geom::Point const &p) const
+{
+ if (_render_mode == RenderMode::OUTLINE || _render_mode == RenderMode::OUTLINE_OVERLAY) {
+ return true;
+ } else if (_split_mode == SplitMode::SPLIT) {
+ auto split_position = _split_frac * get_dimensions();
+ switch (_split_direction) {
+ case SplitDirection::NORTH: return p.y() > split_position.y();
+ case SplitDirection::SOUTH: return p.y() < split_position.y();
+ case SplitDirection::WEST: return p.x() > split_position.x();
+ case SplitDirection::EAST: return p.x() < split_position.x();
+ default: return false;
+ }
+ } else {
+ return false;
+ }
+}
+
+/**
+ * Return the last known mouse position of center if off-canvas.
+ */
+std::optional<Geom::Point> Canvas::get_last_mouse() const
+{
+ return d->last_mouse;
+}
+
+const Geom::Affine &Canvas::get_geom_affine() const
+{
+ return d->canvasitem_ctx->affine();
+}
+
+void CanvasPrivate::queue_draw_area(const Geom::IntRect &rect)
+{
+ if (q->get_opengl_enabled()) {
+ // Note: GTK glitches out when you use queue_draw_area in OpenGL mode.
+ // It's also pointless, because it seems to just call queue_draw anyway.
+ q->queue_draw();
+ } else {
+ q->queue_draw_area(rect.left(), rect.top(), rect.width(), rect.height());
+ }
+}
+
+/**
+ * Invalidate drawing and redraw during idle.
+ */
+void Canvas::redraw_all()
+{
+ if (!d->active) {
+ // CanvasItems redraw their area when being deleted... which happens when the Canvas is destroyed.
+ // We need to ignore their requests!
+ return;
+ }
+ d->invalidated->do_union(geom_to_cairo(d->stores.store().rect));
+ d->schedule_redraw();
+ if (d->prefs.debug_show_unclean) queue_draw();
+}
+
+/**
+ * Redraw the given area during idle.
+ */
+void Canvas::redraw_area(int x0, int y0, int x1, int y1)
+{
+ if (!d->active) {
+ // CanvasItems redraw their area when being deleted... which happens when the Canvas is destroyed.
+ // We need to ignore their requests!
+ return;
+ }
+
+ // Clamp area to Cairo's technically supported max size (-2^30..+2^30-1).
+ // This ensures that the rectangle dimensions don't overflow and wrap around.
+ constexpr int min_coord = -(1 << 30);
+ constexpr int max_coord = (1 << 30) - 1;
+
+ x0 = std::clamp(x0, min_coord, max_coord);
+ y0 = std::clamp(y0, min_coord, max_coord);
+ x1 = std::clamp(x1, min_coord, max_coord);
+ y1 = std::clamp(y1, min_coord, max_coord);
+
+ if (x0 >= x1 || y0 >= y1) {
+ return;
+ }
+
+ if (d->redraw_active && d->invalidated->empty()) {
+ d->abort_flags.store((int)AbortFlags::Soft, std::memory_order_relaxed); // responding to partial invalidations takes priority over prerendering
+ if (d->prefs.debug_logging) std::cout << "Soft exit request" << std::endl;
+ }
+
+ auto const rect = Geom::IntRect(x0, y0, x1, y1);
+ d->invalidated->do_union(geom_to_cairo(rect));
+ d->schedule_redraw();
+ if (d->prefs.debug_show_unclean) queue_draw();
+}
+
+void Canvas::redraw_area(Geom::Coord x0, Geom::Coord y0, Geom::Coord x1, Geom::Coord y1)
+{
+ // Handle overflow during conversion gracefully.
+ // Round outward to make sure integral coordinates cover the entire area.
+ constexpr Geom::Coord min_int = std::numeric_limits<int>::min();
+ constexpr Geom::Coord max_int = std::numeric_limits<int>::max();
+
+ redraw_area(
+ (int)std::floor(std::clamp(x0, min_int, max_int)),
+ (int)std::floor(std::clamp(y0, min_int, max_int)),
+ (int)std::ceil (std::clamp(x1, min_int, max_int)),
+ (int)std::ceil (std::clamp(y1, min_int, max_int))
+ );
+}
+
+void Canvas::redraw_area(Geom::Rect const &area)
+{
+ redraw_area(area.left(), area.top(), area.right(), area.bottom());
+}
+
+/**
+ * Redraw after changing canvas item geometry.
+ */
+void Canvas::request_update()
+{
+ // Flag geometry as needing update.
+ _need_update = true;
+
+ // Trigger the redraw process to perform the update.
+ d->schedule_redraw();
+}
+
+/**
+ * Scroll window so drawing point 'pos' is at upper left corner of canvas.
+ */
+void Canvas::set_pos(Geom::IntPoint const &pos)
+{
+ if (pos == _pos) {
+ return;
+ }
+
+ _pos = pos;
+
+ d->schedule_redraw();
+ queue_draw();
+}
+
+/**
+ * Set the affine for the canvas.
+ */
+void Canvas::set_affine(Geom::Affine const &affine)
+{
+ if (_affine == affine) {
+ return;
+ }
+
+ _affine = affine;
+
+ d->schedule_redraw();
+ queue_draw();
+}
+
+/**
+ * Set the desk colour. Transparency is interpreted as amount of checkerboard.
+ */
+void Canvas::set_desk(uint32_t rgba)
+{
+ if (d->desk == rgba) return;
+ bool invalidated = d->background_in_stores_enabled;
+ d->desk = rgba;
+ invalidated |= d->background_in_stores_enabled = d->background_in_stores_required();
+ if (get_realized() && invalidated) redraw_all();
+ queue_draw();
+}
+
+/**
+ * Set the page border colour. Although we don't draw the borders, this colour affects the shadows which we do draw (in OpenGL mode).
+ */
+void Canvas::set_border(uint32_t rgba)
+{
+ if (d->border == rgba) return;
+ d->border = rgba;
+ if (get_realized() && get_opengl_enabled()) queue_draw();
+}
+
+/**
+ * Set the page colour. Like the desk colour, transparency is interpreted as checkerboard.
+ */
+void Canvas::set_page(uint32_t rgba)
+{
+ if (d->page == rgba) return;
+ bool invalidated = d->background_in_stores_enabled;
+ d->page = rgba;
+ invalidated |= d->background_in_stores_enabled = d->background_in_stores_required();
+ if (get_realized() && invalidated) redraw_all();
+ queue_draw();
+}
+
+uint32_t Canvas::get_effective_background() const
+{
+ auto arr = checkerboard_darken(rgb_to_array(d->desk), 1.0f - 0.5f * SP_RGBA32_A_U(d->desk) / 255.0f);
+ return SP_RGBA32_F_COMPOSE(arr[0], arr[1], arr[2], 1.0);
+}
+
+void Canvas::set_render_mode(Inkscape::RenderMode mode)
+{
+ if ((_render_mode == RenderMode::OUTLINE_OVERLAY) != (mode == RenderMode::OUTLINE_OVERLAY) && !get_opengl_enabled()) {
+ queue_draw();
+ }
+ _render_mode = mode;
+ if (_drawing) {
+ _drawing->setRenderMode(_render_mode == RenderMode::OUTLINE_OVERLAY ? RenderMode::NORMAL : _render_mode);
+ _drawing->setOutlineOverlay(d->outlines_required());
+ }
+ if (_desktop) {
+ _desktop->setWindowTitle(); // Mode is listed in title.
+ }
+}
+
+void Canvas::set_color_mode(Inkscape::ColorMode mode)
+{
+ _color_mode = mode;
+ if (_drawing) {
+ _drawing->setColorMode(_color_mode);
+ }
+ if (_desktop) {
+ _desktop->setWindowTitle(); // Mode is listed in title.
+ }
+}
+
+void Canvas::set_split_mode(Inkscape::SplitMode mode)
+{
+ if (_split_mode != mode) {
+ _split_mode = mode;
+ if (_split_mode == Inkscape::SplitMode::SPLIT) {
+ _hover_direction = Inkscape::SplitDirection::NONE;
+ }
+ if (_drawing) {
+ _drawing->setOutlineOverlay(d->outlines_required());
+ }
+ redraw_all();
+ }
+}
+
+void Canvas::set_clip_to_page_mode(bool clip)
+{
+ if (clip != d->clip_to_page) {
+ d->clip_to_page = clip;
+ d->schedule_redraw();
+ }
+}
+
+void Canvas::set_cms_key(std::string key)
+{
+ _cms_key = std::move(key);
+ _cms_active = !_cms_key.empty();
+ redraw_all();
+}
+
+/**
+ * Clear current and grabbed items.
+ */
+void Canvas::canvas_item_destructed(Inkscape::CanvasItem *item)
+{
+ if (!d->active) {
+ return;
+ }
+
+ if (item == _current_canvas_item) {
+ _current_canvas_item = nullptr;
+ }
+
+ if (item == _current_canvas_item_new) {
+ _current_canvas_item_new = nullptr;
+ }
+
+ if (item == _grabbed_canvas_item) {
+ _grabbed_canvas_item = nullptr;
+ auto const display = Gdk::Display::get_default();
+ auto const seat = display->get_default_seat();
+ seat->ungrab();
+ }
+
+ if (item == d->pre_scroll_grabbed_item) {
+ d->pre_scroll_grabbed_item = nullptr;
+ }
+}
+
+std::optional<Geom::PathVector> CanvasPrivate::calc_page_clip() const
+{
+ if (!clip_to_page) {
+ return {};
+ }
+
+ Geom::PathVector pv;
+ for (auto &rect : pi.pages) {
+ pv.push_back(Geom::Path(rect));
+ }
+ return pv;
+}
+
+// Change cursor
+void Canvas::set_cursor()
+{
+ if (!_desktop) {
+ return;
+ }
+
+ auto display = Gdk::Display::get_default();
+
+ switch (_hover_direction) {
+ case Inkscape::SplitDirection::NONE:
+ _desktop->event_context->use_tool_cursor();
+ break;
+
+ case Inkscape::SplitDirection::NORTH:
+ case Inkscape::SplitDirection::EAST:
+ case Inkscape::SplitDirection::SOUTH:
+ case Inkscape::SplitDirection::WEST:
+ {
+ auto cursor = Gdk::Cursor::create(display, "pointer");
+ get_window()->set_cursor(cursor);
+ break;
+ }
+
+ case Inkscape::SplitDirection::HORIZONTAL:
+ {
+ auto cursor = Gdk::Cursor::create(display, "ns-resize");
+ get_window()->set_cursor(cursor);
+ break;
+ }
+
+ case Inkscape::SplitDirection::VERTICAL:
+ {
+ auto cursor = Gdk::Cursor::create(display, "ew-resize");
+ get_window()->set_cursor(cursor);
+ break;
+ }
+
+ default:
+ // Shouldn't reach.
+ std::cerr << "Canvas::set_cursor: Unknown hover direction!" << std::endl;
+ }
+}
+
+void Canvas::get_preferred_width_vfunc(int &minimum_width, int &natural_width) const
+{
+ minimum_width = natural_width = 256;
+}
+
+void Canvas::get_preferred_height_vfunc(int &minimum_height, int &natural_height) const
+{
+ minimum_height = natural_height = 256;
+}
+
+void Canvas::on_size_allocate(Gtk::Allocation &allocation)
+{
+ auto const old_dimensions = get_dimensions();
+ parent_type::on_size_allocate(allocation);
+ auto const new_dimensions = get_dimensions();
+
+ // Necessary as GTK seems to somehow invalidate the current pipeline state upon resize.
+ if (d->active) {
+ d->graphics->invalidated_glstate();
+ }
+
+ // Trigger the size update to be applied to the stores before the next redraw of the window.
+ d->schedule_redraw();
+
+ // Keep canvas centered and optionally zoomed in.
+ if (_desktop && new_dimensions != old_dimensions) {
+ auto const midpoint = _desktop->w2d(_pos + Geom::Point(old_dimensions) * 0.5);
+ double zoom = _desktop->current_zoom();
+
+ auto prefs = Preferences::get();
+ if (prefs->getBool("/options/stickyzoom/value", false)) {
+ // Calculate adjusted zoom.
+ auto const old_minextent = min(old_dimensions);
+ auto const new_minextent = min(new_dimensions);
+ if (old_minextent != 0) {
+ zoom *= (double)new_minextent / old_minextent;
+ }
+ }
+
+ _desktop->zoom_absolute(midpoint, zoom, false);
+ }
+}
+
+Glib::RefPtr<Gdk::GLContext> Canvas::create_context()
+{
+ Glib::RefPtr<Gdk::GLContext> result;
+
+ try {
+ result = get_window()->create_gl_context();
+ } catch (const Gdk::GLError &e) {
+ std::cerr << "Failed to create OpenGL context: " << e.what().raw() << std::endl;
+ return {};
+ }
+
+ try {
+ result->realize();
+ } catch (const Glib::Error &e) {
+ std::cerr << "Failed to realize OpenGL context: " << e.what().raw() << std::endl;
+ return {};
+ }
+
+ return result;
+}
+
+void Canvas::paint_widget(Cairo::RefPtr<Cairo::Context> const &cr)
+{
+ framecheck_whole_function(d)
+
+ if (!d->active) {
+ std::cerr << "Canvas::paint_widget: Called while not active!" << std::endl;
+ return;
+ }
+
+ if constexpr (false) d->canvasitem_ctx->root()->canvas_item_print_tree();
+
+ // Although launch_redraw() is scheduled at a priority higher than draw, and should therefore always be called first if
+ // asked, there are times when GTK simply decides to call on_draw anyway. Since launch_redraw() is required to have been
+ // called at least once to perform vital initalisation, if it has not been called, we have to exit.
+ if (d->stores.mode() == Stores::Mode::None) {
+ return;
+ }
+
+ // Commit pending tiles in case GTK called on_draw even though after_redraw() is scheduled at higher priority.
+ if (!d->redraw_active) {
+ d->commit_tiles();
+ }
+
+ if (get_opengl_enabled()) {
+ bind_framebuffer();
+ }
+
+ Graphics::PaintArgs args;
+ args.mouse = d->last_mouse;
+ args.render_mode = _render_mode;
+ args.splitmode = _split_mode;
+ args.splitfrac = _split_frac;
+ args.splitdir = _split_direction;
+ args.hoverdir = _hover_direction;
+ args.yaxisdir = _desktop ? _desktop->yaxisdir() : 1.0;
+
+ d->graphics->paint_widget(Fragment{ _affine, get_area_world() }, args, cr);
+
+ // If asked, run an animation loop.
+ if (d->prefs.debug_animate) {
+ auto t = g_get_monotonic_time() / 1700000.0;
+ auto affine = Geom::Rotate(t * 5) * Geom::Scale(1.0 + 0.6 * cos(t * 2));
+ set_affine(affine);
+ auto dim = _desktop && _desktop->doc() ? _desktop->doc()->getDimensions() : Geom::Point();
+ set_pos(Geom::Point((0.5 + 0.3 * cos(t * 2)) * dim.x(), (0.5 + 0.3 * sin(t * 3)) * dim.y()) * affine - Geom::Point(get_dimensions()) * 0.5);
+ }
+}
+
+/*
+ * Async redrawing process
+ */
+
+// Replace a region with a larger region consisting of fewer, larger rectangles. (Allowed to slightly overlap.)
+auto coarsen(const Cairo::RefPtr<Cairo::Region> &region, int min_size, int glue_size, double min_fullness)
+{
+ // Sort the rects by minExtent.
+ struct Compare
+ {
+ bool operator()(const Geom::IntRect &a, const Geom::IntRect &b) const {
+ return a.minExtent() < b.minExtent();
+ }
+ };
+ std::multiset<Geom::IntRect, Compare> rects;
+ int nrects = region->get_num_rectangles();
+ for (int i = 0; i < nrects; i++) {
+ rects.emplace(cairo_to_geom(region->get_rectangle(i)));
+ }
+
+ // List of processed rectangles.
+ std::vector<Geom::IntRect> processed;
+ processed.reserve(nrects);
+
+ // Removal lists.
+ std::vector<decltype(rects)::iterator> remove_rects;
+ std::vector<int> remove_processed;
+
+ // Repeatedly expand small rectangles by absorbing their nearby small rectangles.
+ while (!rects.empty() && rects.begin()->minExtent() < min_size) {
+ // Extract the smallest unprocessed rectangle.
+ auto rect = *rects.begin();
+ rects.erase(rects.begin());
+
+ // Initialise the effective glue size.
+ int effective_glue_size = glue_size;
+
+ while (true) {
+ // Find the glue zone.
+ auto glue_zone = rect;
+ glue_zone.expandBy(effective_glue_size);
+
+ // Absorb rectangles in the glue zone. We could do better algorithmically speaking, but in real life it's already plenty fast.
+ auto newrect = rect;
+ int absorbed_area = 0;
+
+ remove_rects.clear();
+ for (auto it = rects.begin(); it != rects.end(); ++it) {
+ if (glue_zone.contains(*it)) {
+ newrect.unionWith(*it);
+ absorbed_area += it->area();
+ remove_rects.emplace_back(it);
+ }
+ }
+
+ remove_processed.clear();
+ for (int i = 0; i < processed.size(); i++) {
+ auto &r = processed[i];
+ if (glue_zone.contains(r)) {
+ newrect.unionWith(r);
+ absorbed_area += r.area();
+ remove_processed.emplace_back(i);
+ }
+ }
+
+ // If the result was too empty, try again with a smaller glue size.
+ double fullness = (double)(rect.area() + absorbed_area) / newrect.area();
+ if (fullness < min_fullness) {
+ effective_glue_size /= 2;
+ continue;
+ }
+
+ // Commit the change.
+ rect = newrect;
+
+ for (auto &it : remove_rects) {
+ rects.erase(it);
+ }
+
+ for (int j = (int)remove_processed.size() - 1; j >= 0; j--) {
+ int i = remove_processed[j];
+ processed[i] = processed.back();
+ processed.pop_back();
+ }
+
+ // Stop growing if not changed or now big enough.
+ bool finished = absorbed_area == 0 || rect.minExtent() >= min_size;
+ if (finished) {
+ break;
+ }
+
+ // Otherwise, continue normally.
+ effective_glue_size = glue_size;
+ }
+
+ // Put the finished rectangle in processed.
+ processed.emplace_back(rect);
+ }
+
+ // Put any remaining rectangles in processed.
+ for (auto &rect : rects) {
+ processed.emplace_back(rect);
+ }
+
+ return processed;
+}
+
+static std::optional<Geom::Dim2> bisect(Geom::IntRect const &rect, int tile_size)
+{
+ int bw = rect.width();
+ int bh = rect.height();
+
+ // Chop in half along the bigger dimension if the bigger dimension is too big.
+ if (bw > bh) {
+ if (bw > tile_size) {
+ return Geom::X;
+ }
+ } else {
+ if (bh > tile_size) {
+ return Geom::Y;
+ }
+ }
+
+ return {};
+}
+
+void CanvasPrivate::init_tiler()
+{
+ // Begin processing redraws.
+ rd.start_time = g_get_monotonic_time();
+ rd.phase = 0;
+ rd.vis_store = (rd.visible & rd.store.rect).regularized();
+
+ if (!init_redraw()) {
+ sync.signalExit();
+ return;
+ }
+
+ // Launch render threads to process tiles.
+ rd.timeoutflag = false;
+
+ rd.numactive = rd.numthreads;
+
+ for (int i = 0; i < rd.numthreads - 1; i++) {
+ boost::asio::post(*pool, [=] { render_tile(i); });
+ }
+
+ render_tile(rd.numthreads - 1);
+}
+
+bool CanvasPrivate::init_redraw()
+{
+ assert(rd.rects.empty());
+
+ switch (rd.phase) {
+ case 0:
+ if (rd.vis_store && rd.decoupled_mode) {
+ // The highest priority to redraw is the region that is visible but not covered by either clean or snapshot content, if in decoupled mode.
+ // If this is not rendered immediately, it will be perceived as edge flicker, most noticeably on zooming out, but also on rotation too.
+ process_redraw(*rd.vis_store, unioned(updater->clean_region->copy(), rd.snapshot_drawn));
+ return true;
+ } else {
+ rd.phase++;
+ // fallthrough
+ }
+
+ case 1:
+ // Another high priority to redraw is the grabbed canvas item, if the user has requested block updates.
+ if (rd.grabbed) {
+ process_redraw(*rd.grabbed, updater->clean_region, false, false); // non-interruptible, non-preemptible
+ return true;
+ } else {
+ rd.phase++;
+ // fallthrough
+ }
+
+ case 2:
+ if (rd.vis_store) {
+ // The main priority to redraw, and the bread and butter of Inkscape's painting, is the visible content that is not clean.
+ // This may be done over several cycles, at the direction of the Updater, each outwards from the mouse.
+ process_redraw(*rd.vis_store, updater->get_next_clean_region());
+ return true;
+ } else {
+ rd.phase++;
+ // fallthrough
+ }
+
+ case 3: {
+ // The lowest priority to redraw is the prerender margin around the visible rectangle.
+ // (This is in addition to any opportunistic prerendering that may have already occurred in the above steps.)
+ auto prerender = expandedBy(rd.visible, rd.margin);
+ auto prerender_store = (prerender & rd.store.rect).regularized();
+ if (prerender_store) {
+ process_redraw(*prerender_store, updater->clean_region);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ default:
+ assert(false);
+ return false;
+ }
+}
+
+// Paint a given subrectangle of the store given by 'bounds', but avoid painting the part of it within 'clean' if possible.
+// Some parts both outside the bounds and inside the clean region may also be painted if it helps reduce fragmentation.
+void CanvasPrivate::process_redraw(Geom::IntRect const &bounds, Cairo::RefPtr<Cairo::Region> clean, bool interruptible, bool preemptible)
+{
+ rd.bounds = bounds;
+ rd.clean = std::move(clean);
+ rd.interruptible = interruptible;
+ rd.preemptible = preemptible;
+
+ // Assert that we do not render outside of store.
+ assert(rd.store.rect.contains(rd.bounds));
+
+ // Get the region we are asked to paint.
+ auto region = Cairo::Region::create(geom_to_cairo(rd.bounds));
+ region->subtract(rd.clean);
+
+ // Get the list of rectangles to paint, coarsened to avoid fragmentation.
+ rd.rects = coarsen(region,
+ std::min<int>(rd.coarsener_min_size, rd.tile_size / 2),
+ std::min<int>(rd.coarsener_glue_size, rd.tile_size / 2),
+ rd.coarsener_min_fullness);
+
+ // Put the rectangles into a heap sorted by distance from mouse.
+ std::make_heap(rd.rects.begin(), rd.rects.end(), rd.getcmp());
+
+ // Adjust the effective tile size proportional to the painting area.
+ double adjust = (double)cairo_to_geom(region->get_extents()).maxExtent() / rd.visible.maxExtent();
+ adjust = std::clamp(adjust, 0.3, 1.0);
+ rd.effective_tile_size = rd.tile_size * adjust;
+}
+
+// Process rectangles until none left or timed out.
+void CanvasPrivate::render_tile(int debug_id)
+{
+ rd.mutex.lock();
+
+ std::string fc_str;
+ FrameCheck::Event fc;
+ if (rd.debug_framecheck) {
+ fc_str = "render_thread_" + std::to_string(debug_id + 1);
+ fc = FrameCheck::Event(fc_str.c_str());
+ }
+
+ while (true) {
+ // If we've run out of rects, try to start a new redraw cycle.
+ if (rd.rects.empty()) {
+ if (end_redraw()) {
+ // More redraw cycles to do.
+ continue;
+ } else {
+ // All finished.
+ break;
+ }
+ }
+
+ // Check for cancellation.
+ auto const flags = abort_flags.load(std::memory_order_relaxed);
+ bool const soft = flags & (int)AbortFlags::Soft;
+ bool const hard = flags & (int)AbortFlags::Hard;
+ if (hard || (rd.phase == 3 && soft)) {
+ break;
+ }
+
+ // Extract the closest rectangle to the mouse.
+ std::pop_heap(rd.rects.begin(), rd.rects.end(), rd.getcmp());
+ auto rect = rd.rects.back();
+ rd.rects.pop_back();
+
+ // Cull empty rectangles.
+ if (rect.hasZeroArea()) {
+ continue;
+ }
+
+ // Cull rectangles that lie entirely inside the clean region.
+ // (These can be generated by coarsening; they must be discarded to avoid getting stuck re-rendering the same rectangles.)
+ if (rd.clean->contains_rectangle(geom_to_cairo(rect)) == Cairo::REGION_OVERLAP_IN) {
+ continue;
+ }
+
+ // Lambda to add a rectangle to the heap.
+ auto add_rect = [&] (Geom::IntRect const &rect) {
+ rd.rects.emplace_back(rect);
+ std::push_heap(rd.rects.begin(), rd.rects.end(), rd.getcmp());
+ };
+
+ // If the rectangle needs bisecting, bisect it and put it back on the heap.
+ if (auto axis = bisect(rect, rd.effective_tile_size)) {
+ int mid = rect[*axis].middle();
+ auto lo = rect; lo[*axis].setMax(mid); add_rect(lo);
+ auto hi = rect; hi[*axis].setMin(mid); add_rect(hi);
+ continue;
+ }
+
+ // Extend thin rectangles at the edge of the bounds rect to at least some minimum size, being sure to keep them within the store.
+ // (This ensures we don't end up rendering one thin rectangle at the edge every frame while the view is moved continuously.)
+ if (rd.preemptible) {
+ if (rect.width() < rd.preempt) {
+ if (rect.left() == rd.bounds.left() ) rect.setLeft (std::max(rect.right() - rd.preempt, rd.store.rect.left() ));
+ if (rect.right() == rd.bounds.right()) rect.setRight(std::min(rect.left() + rd.preempt, rd.store.rect.right()));
+ }
+ if (rect.height() < rd.preempt) {
+ if (rect.top() == rd.bounds.top() ) rect.setTop (std::max(rect.bottom() - rd.preempt, rd.store.rect.top() ));
+ if (rect.bottom() == rd.bounds.bottom()) rect.setBottom(std::min(rect.top() + rd.preempt, rd.store.rect.bottom()));
+ }
+ }
+
+ // Mark the rectangle as clean.
+ updater->mark_clean(rect);
+
+ rd.mutex.unlock();
+
+ // Paint the rectangle.
+ paint_rect(rect);
+
+ rd.mutex.lock();
+
+ // Check for timeout.
+ if (rd.interruptible) {
+ auto now = g_get_monotonic_time();
+ auto elapsed = now - rd.start_time;
+ if (elapsed > rd.render_time_limit * 1000) {
+ // Timed out. Temporarily return to GTK main loop, and come back here when next idle.
+ rd.timeoutflag = true;
+ break;
+ }
+ }
+ }
+
+ if (rd.debug_framecheck && rd.timeoutflag) {
+ fc.subtype = 1;
+ }
+
+ rd.numactive--;
+ bool const done = rd.numactive == 0;
+
+ rd.mutex.unlock();
+
+ if (done) {
+ rd.rects.clear();
+ sync.signalExit();
+ }
+}
+
+bool CanvasPrivate::end_redraw()
+{
+ switch (rd.phase) {
+ case 0:
+ rd.phase++;
+ return init_redraw();
+
+ case 1:
+ rd.phase++;
+ // Reset timeout to leave the normal amount of time for clearing up artifacts.
+ rd.start_time = g_get_monotonic_time();
+ return init_redraw();
+
+ case 2:
+ if (!updater->report_finished()) {
+ rd.phase++;
+ }
+ return init_redraw();
+
+ case 3:
+ return false;
+
+ default:
+ assert(false);
+ return false;
+ }
+}
+
+void CanvasPrivate::paint_rect(Geom::IntRect const &rect)
+{
+ // Make sure the paint rectangle lies within the store.
+ assert(rd.store.rect.contains(rect));
+
+ auto paint = [&, this] (bool need_background, bool outline_pass) {
+
+ auto surface = graphics->request_tile_surface(rect, true);
+ if (!surface) {
+ sync.runInMain([&] {
+ if (prefs.debug_logging) std::cout << "Blocked - buffer mapping" << std::endl;
+ if (q->get_opengl_enabled()) q->make_current();
+ surface = graphics->request_tile_surface(rect, false);
+ });
+ }
+
+ try {
+
+ paint_single_buffer(surface, rect, need_background, outline_pass);
+
+ } catch (std::bad_alloc const &) {
+ // Note: std::bad_alloc actually indicates a Cairo error that occurs regularly at high zoom, and we must handle it.
+ // See https://gitlab.com/inkscape/inkscape/-/issues/3975
+ sync.runInMain([&] {
+ std::cerr << "Rendering failure. You probably need to zoom out!" << std::endl;
+ if (q->get_opengl_enabled()) q->make_current();
+ graphics->junk_tile_surface(std::move(surface));
+ surface = graphics->request_tile_surface(rect, false);
+ paint_error_buffer(surface);
+ });
+ }
+
+ return surface;
+ };
+
+ // Create and render the tile.
+ Tile tile;
+ tile.fragment.affine = rd.store.affine;
+ tile.fragment.rect = rect;
+ tile.surface = paint(background_in_stores_required(), false);
+ if (outlines_enabled) {
+ tile.outline_surface = paint(false, true);
+ }
+
+ // Introduce an artificial delay for each rectangle.
+ if (rd.redraw_delay) g_usleep(*rd.redraw_delay);
+
+ // Stick the tile on the list of tiles to reap.
+ {
+ auto g = std::lock_guard(rd.tiles_mutex);
+ rd.tiles.emplace_back(std::move(tile));
+ }
+}
+
+void CanvasPrivate::paint_single_buffer(Cairo::RefPtr<Cairo::ImageSurface> const &surface, Geom::IntRect const &rect, bool need_background, bool outline_pass)
+{
+ // Create Cairo context.
+ auto cr = Cairo::Context::create(surface);
+
+ // Clear background.
+ cr->save();
+ if (need_background) {
+ Graphics::paint_background(Fragment{ rd.store.affine, rect }, pi, rd.page, rd.desk, cr);
+ } else {
+ cr->set_operator(Cairo::OPERATOR_CLEAR);
+ cr->paint();
+ }
+ cr->restore();
+
+ // Render drawing on top of background.
+ auto buf = Inkscape::CanvasItemBuffer{ rect, scale_factor, cr, outline_pass };
+ canvasitem_ctx->root()->render(buf);
+
+ // Paint over newly drawn content with a translucent random colour.
+ if (rd.debug_show_redraw) {
+ cr->set_source_rgba((rand() % 256) / 255.0, (rand() % 256) / 255.0, (rand() % 256) / 255.0, 0.2);
+ cr->set_operator(Cairo::OPERATOR_OVER);
+ cr->paint();
+ }
+}
+
+void CanvasPrivate::paint_error_buffer(Cairo::RefPtr<Cairo::ImageSurface> const &surface)
+{
+ // Paint something into surface to represent an "error" state for that tile.
+ // Currently just paints solid black.
+ auto cr = Cairo::Context::create(surface);
+ cr->set_source_rgb(0, 0, 0);
+ cr->paint();
+}
+
+} // namespace Inkscape::UI::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:textwidth=99 :
diff --git a/src/ui/widget/canvas.h b/src/ui/widget/canvas.h
new file mode 100644
index 0000000..970488e
--- /dev/null
+++ b/src/ui/widget/canvas.h
@@ -0,0 +1,224 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_H
+#define INKSCAPE_UI_WIDGET_CANVAS_H
+/*
+ * Authors:
+ * Tavmjong Bah
+ * PBS <pbs3141@gmail.com>
+ *
+ * Copyright (C) 2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <memory>
+#include <gtkmm.h>
+#include <2geom/rect.h>
+#include <2geom/int-rect.h>
+#include "display/rendermode.h"
+#include "optglarea.h"
+
+class SPDesktop;
+
+namespace Inkscape {
+
+class CanvasItem;
+class CanvasItemGroup;
+class Drawing;
+
+namespace UI {
+namespace Widget {
+
+class CanvasPrivate;
+
+/**
+ * A widget for Inkscape's canvas.
+ */
+class Canvas : public OptGLArea
+{
+ using parent_type = OptGLArea;
+
+public:
+ Canvas();
+ ~Canvas() override;
+
+ /* Configuration */
+
+ // Desktop (Todo: Remove.)
+ void set_desktop(SPDesktop *desktop) { _desktop = desktop; }
+ SPDesktop *get_desktop() const { return _desktop; }
+
+ // Drawing
+ void set_drawing(Inkscape::Drawing *drawing);
+
+ // Canvas item root
+ CanvasItemGroup *get_canvas_item_root() const;
+
+ // Geometry
+ void set_pos (const Geom::IntPoint &pos);
+ void set_pos (const Geom::Point &fpos) { set_pos(fpos.round()); }
+ void set_affine(const Geom::Affine &affine);
+ const Geom::IntPoint &get_pos () const { return _pos; }
+ const Geom::Affine &get_affine() const { return _affine; }
+ const Geom::Affine &get_geom_affine() const; // tool-base.cpp (todo: remove this dependency)
+
+ // Background
+ void set_desk (uint32_t rgba);
+ void set_border(uint32_t rgba);
+ void set_page (uint32_t rgba);
+ uint32_t get_effective_background() const; // This function is now wrong.
+
+ // Rendering modes
+ void set_render_mode(Inkscape::RenderMode mode);
+ void set_color_mode (Inkscape::ColorMode mode);
+ void set_split_mode (Inkscape::SplitMode mode);
+ Inkscape::RenderMode get_render_mode() const { return _render_mode; }
+ Inkscape::ColorMode get_color_mode() const { return _color_mode; }
+ Inkscape::SplitMode get_split_mode() const { return _split_mode; }
+ void set_clip_to_page_mode(bool clip);
+
+ // CMS
+ void set_cms_key(std::string key);
+ const std::string &get_cms_key() const { return _cms_key; }
+ void set_cms_active(bool active) { _cms_active = active; }
+ bool get_cms_active() const { return _cms_active; }
+
+ /* Observers */
+
+ // Geometry
+ Geom::IntPoint get_dimensions() const;
+ bool world_point_inside_canvas(Geom::Point const &world) const; // desktop-events.cpp
+ Geom::Point canvas_to_world(Geom::Point const &window) const;
+ Geom::IntRect get_area_world() const;
+ bool canvas_point_in_outline_zone(Geom::Point const &world) const;
+
+ // State
+ bool is_dragging() const { return _is_dragging; } // selection-chemistry.cpp
+
+ // Mouse
+ std::optional<Geom::Point> get_last_mouse() const; // desktop-widget.cpp
+
+ /* Methods */
+
+ // Invalidation
+ void redraw_all(); // Mark everything as having changed.
+ void redraw_area(Geom::Rect const &area); // Mark a rectangle of world space as having changed.
+ void redraw_area(int x0, int y0, int x1, int y1);
+ void redraw_area(Geom::Coord x0, Geom::Coord y0, Geom::Coord x1, Geom::Coord y1);
+ void request_update(); // Mark geometry as needing recalculation.
+
+ // Callback run on destructor of any canvas item
+ void canvas_item_destructed(Inkscape::CanvasItem *item);
+
+ // State
+ Inkscape::CanvasItem *get_current_canvas_item() const { return _current_canvas_item; }
+ void set_current_canvas_item(Inkscape::CanvasItem *item) {
+ _current_canvas_item = item;
+ }
+ Inkscape::CanvasItem *get_grabbed_canvas_item() const { return _grabbed_canvas_item; }
+ void set_grabbed_canvas_item(Inkscape::CanvasItem *item, Gdk::EventMask mask) {
+ _grabbed_canvas_item = item;
+ _grabbed_event_mask = mask;
+ }
+ void set_all_enter_events(bool on) { _all_enter_events = on; }
+
+ void enable_autoscroll();
+
+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;
+
+ // Event handlers
+ bool on_scroll_event (GdkEventScroll* ) override;
+ bool on_button_event (GdkEventButton* );
+ bool on_button_press_event (GdkEventButton* ) override;
+ bool on_button_release_event(GdkEventButton* ) override;
+ bool on_enter_notify_event (GdkEventCrossing*) override;
+ bool on_leave_notify_event (GdkEventCrossing*) override;
+ bool on_focus_in_event (GdkEventFocus* ) override;
+ bool on_key_press_event (GdkEventKey* ) override;
+ bool on_key_release_event (GdkEventKey* ) override;
+ bool on_motion_notify_event (GdkEventMotion* ) override;
+
+ void on_realize() override;
+ void on_unrealize() override;
+ void on_size_allocate(Gtk::Allocation&) override;
+
+ Glib::RefPtr<Gdk::GLContext> create_context() override;
+ void paint_widget(const Cairo::RefPtr<Cairo::Context>&) override;
+
+private:
+ /* Configuration */
+
+ // Desktop
+ SPDesktop *_desktop = nullptr;
+
+ // Drawing
+ Inkscape::Drawing *_drawing = nullptr;
+
+ // Geometry
+ Geom::IntPoint _pos = {0, 0}; ///< Coordinates of top-left pixel of canvas view within canvas.
+ Geom::Affine _affine; ///< The affine that we have been requested to draw at.
+
+ // Rendering modes
+ Inkscape::RenderMode _render_mode = Inkscape::RenderMode::NORMAL;
+ Inkscape::SplitMode _split_mode = Inkscape::SplitMode::NORMAL;
+ Inkscape::ColorMode _color_mode = Inkscape::ColorMode::NORMAL;
+
+ // CMS
+ std::string _cms_key;
+ bool _cms_active = false;
+
+ /* Internal state */
+
+ // Event handling/item picking
+ GdkEvent _pick_event; ///< Event used to find currently selected item.
+ bool _in_repick; ///< For tracking recursion of pick_current_item().
+ bool _left_grabbed_item; ///< ?
+ bool _all_enter_events; ///< Keep all enter events. Only set true in connector-tool.cpp.
+ bool _is_dragging; ///< Used in selection-chemistry to block undo/redo.
+ int _state; ///< Last known modifier state (SHIFT, CTRL, etc.).
+
+ Inkscape::CanvasItem *_current_canvas_item; ///< Item containing cursor, nullptr if none.
+ Inkscape::CanvasItem *_current_canvas_item_new; ///< Item to become _current_item, nullptr if none.
+ Inkscape::CanvasItem *_grabbed_canvas_item; ///< Item that holds a pointer grab; nullptr if none.
+ Gdk::EventMask _grabbed_event_mask;
+
+ // Drawing
+ bool _need_update = true; // Set true so setting CanvasItem bounds are calculated at least once.
+
+ // Split view
+ Inkscape::SplitDirection _split_direction;
+ Geom::Point _split_frac;
+ Inkscape::SplitDirection _hover_direction;
+ bool _split_dragging;
+ Geom::IntPoint _split_drag_start;
+
+ void set_cursor();
+
+ // Opaque pointer to implementation
+ friend class CanvasPrivate;
+ std::unique_ptr<CanvasPrivate> d;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_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/canvas/cairographics.cpp b/src/ui/widget/canvas/cairographics.cpp
new file mode 100644
index 0000000..42b3353
--- /dev/null
+++ b/src/ui/widget/canvas/cairographics.cpp
@@ -0,0 +1,423 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <2geom/parallelogram.h>
+#include "ui/util.h"
+#include "helper/geom.h"
+#include "cairographics.h"
+#include "stores.h"
+#include "prefs.h"
+#include "util.h"
+#include "framecheck.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+CairoGraphics::CairoGraphics(Prefs const &prefs, Stores const &stores, PageInfo const &pi)
+ : prefs(prefs)
+ , stores(stores)
+ , pi(pi) {}
+
+std::unique_ptr<Graphics> Graphics::create_cairo(Prefs const &prefs, Stores const &stores, PageInfo const &pi)
+{
+ return std::make_unique<CairoGraphics>(prefs, stores, pi);
+}
+
+void CairoGraphics::set_outlines_enabled(bool enabled)
+{
+ outlines_enabled = enabled;
+ if (!enabled) {
+ store.outline_surface.clear();
+ snapshot.outline_surface.clear();
+ }
+}
+
+void CairoGraphics::recreate_store(Geom::IntPoint const &dims)
+{
+ auto surface_size = dims * scale_factor;
+
+ auto make_surface = [&, this] {
+ auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, surface_size.x(), surface_size.y());
+ cairo_surface_set_device_scale(surface->cobj(), scale_factor, scale_factor); // No C++ API!
+ return surface;
+ };
+
+ // Recreate the store surface.
+ bool reuse_surface = store.surface && dimensions(store.surface) == surface_size;
+ if (!reuse_surface) {
+ store.surface = make_surface();
+ }
+
+ // Ensure the store surface is filled with the correct default background.
+ if (background_in_stores) {
+ auto cr = Cairo::Context::create(store.surface);
+ paint_background(stores.store(), pi, page, desk, cr);
+ } else if (reuse_surface) {
+ auto cr = Cairo::Context::create(store.surface);
+ cr->set_operator(Cairo::OPERATOR_CLEAR);
+ cr->paint();
+ }
+
+ // Do the same for the outline surface (except always clearing it to transparent).
+ if (outlines_enabled) {
+ bool reuse_outline_surface = store.outline_surface && dimensions(store.outline_surface) == surface_size;
+ if (!reuse_outline_surface) {
+ store.outline_surface = make_surface();
+ } else {
+ auto cr = Cairo::Context::create(store.outline_surface);
+ cr->set_operator(Cairo::OPERATOR_CLEAR);
+ cr->paint();
+ }
+ }
+}
+
+void CairoGraphics::shift_store(Fragment const &dest)
+{
+ auto surface_size = dest.rect.dimensions() * scale_factor;
+
+ // Determine the geometry of the shift.
+ auto shift = dest.rect.min() - stores.store().rect.min();
+ auto reuse_rect = (dest.rect & cairo_to_geom(stores.store().drawn->get_extents())).regularized();
+ assert(reuse_rect); // Should not be called if there is no overlap.
+
+ auto make_surface = [&, this] {
+ auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, surface_size.x(), surface_size.y());
+ cairo_surface_set_device_scale(surface->cobj(), scale_factor, scale_factor); // No C++ API!
+ return surface;
+ };
+
+ // Create the new store surface.
+ bool reuse_surface = snapshot.surface && dimensions(snapshot.surface) == surface_size;
+ auto new_surface = reuse_surface ? std::move(snapshot.surface) : make_surface();
+
+ // Paint background into region of store not covered by next operation.
+ auto cr = Cairo::Context::create(new_surface);
+ if (background_in_stores || reuse_surface) {
+ auto reg = Cairo::Region::create(geom_to_cairo(dest.rect));
+ reg->subtract(geom_to_cairo(*reuse_rect));
+ reg->translate(-dest.rect.left(), -dest.rect.top());
+ cr->save();
+ region_to_path(cr, reg);
+ cr->clip();
+ if (background_in_stores) {
+ paint_background(dest, pi, page, desk, cr);
+ } else { // otherwise, reuse_surface is true
+ cr->set_operator(Cairo::OPERATOR_CLEAR);
+ cr->paint();
+ }
+ cr->restore();
+ }
+
+ // Copy re-usuable contents of old store into new store, shifted.
+ cr->rectangle(reuse_rect->left() - dest.rect.left(), reuse_rect->top() - dest.rect.top(), reuse_rect->width(), reuse_rect->height());
+ cr->clip();
+ cr->set_source(store.surface, -shift.x(), -shift.y());
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->paint();
+
+ // Set the result as the new store surface.
+ snapshot.surface = std::move(store.surface);
+ store.surface = std::move(new_surface);
+
+ // Do the same for the outline store
+ if (outlines_enabled) {
+ // Create.
+ bool reuse_outline_surface = snapshot.outline_surface && dimensions(snapshot.outline_surface) == surface_size;
+ auto new_outline_surface = reuse_outline_surface ? std::move(snapshot.outline_surface) : make_surface();
+ // Background.
+ auto cr = Cairo::Context::create(new_outline_surface);
+ if (reuse_outline_surface) {
+ cr->set_operator(Cairo::OPERATOR_CLEAR);
+ cr->paint();
+ }
+ // Copy.
+ cr->rectangle(reuse_rect->left() - dest.rect.left(), reuse_rect->top() - dest.rect.top(), reuse_rect->width(), reuse_rect->height());
+ cr->clip();
+ cr->set_source(store.outline_surface, -shift.x(), -shift.y());
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->paint();
+ // Set.
+ snapshot.outline_surface = std::move(store.outline_surface);
+ store.outline_surface = std::move(new_outline_surface);
+ }
+}
+
+void CairoGraphics::swap_stores()
+{
+ std::swap(store, snapshot);
+}
+
+void CairoGraphics::fast_snapshot_combine()
+{
+ auto copy = [&, this] (Cairo::RefPtr<Cairo::ImageSurface> const &from,
+ Cairo::RefPtr<Cairo::ImageSurface> const &to) {
+ auto cr = Cairo::Context::create(to);
+ cr->set_antialias(Cairo::ANTIALIAS_NONE);
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->translate(-stores.snapshot().rect.left(), -stores.snapshot().rect.top());
+ cr->transform(geom_to_cairo(stores.store().affine.inverse() * stores.snapshot().affine));
+ cr->translate(-1.0, -1.0);
+ region_to_path(cr, shrink_region(stores.store().drawn, 2));
+ cr->translate(1.0, 1.0);
+ cr->clip();
+ cr->set_source(from, stores.store().rect.left(), stores.store().rect.top());
+ Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST);
+ cr->paint();
+ };
+
+ copy(store.surface, snapshot.surface);
+ if (outlines_enabled) copy(store.outline_surface, snapshot.outline_surface);
+}
+
+void CairoGraphics::snapshot_combine(Fragment const &dest)
+{
+ // Create the new fragment.
+ auto content_size = dest.rect.dimensions() * scale_factor;
+
+ auto make_surface = [&] {
+ auto result = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, content_size.x(), content_size.y());
+ cairo_surface_set_device_scale(result->cobj(), scale_factor, scale_factor); // No C++ API!
+ return result;
+ };
+
+ CairoFragment fragment;
+ fragment.surface = make_surface();
+ if (outlines_enabled) fragment.outline_surface = make_surface();
+
+ auto copy = [&, this] (Cairo::RefPtr<Cairo::ImageSurface> const &store_from,
+ Cairo::RefPtr<Cairo::ImageSurface> const &snapshot_from,
+ Cairo::RefPtr<Cairo::ImageSurface> const &to, bool background) {
+ auto cr = Cairo::Context::create(to);
+ cr->set_antialias(Cairo::ANTIALIAS_NONE);
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ if (background) paint_background(dest, pi, page, desk, cr);
+ cr->translate(-dest.rect.left(), -dest.rect.top());
+ cr->transform(geom_to_cairo(stores.snapshot().affine.inverse() * dest.affine));
+ cr->rectangle(stores.snapshot().rect.left(), stores.snapshot().rect.top(), stores.snapshot().rect.width(), stores.snapshot().rect.height());
+ cr->set_source(snapshot_from, stores.snapshot().rect.left(), stores.snapshot().rect.top());
+ Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST);
+ cr->fill();
+ cr->transform(geom_to_cairo(stores.store().affine.inverse() * stores.snapshot().affine));
+ cr->translate(-1.0, -1.0);
+ region_to_path(cr, shrink_region(stores.store().drawn, 2));
+ cr->translate(1.0, 1.0);
+ cr->clip();
+ cr->set_source(store_from, stores.store().rect.left(), stores.store().rect.top());
+ Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST);
+ cr->paint();
+ };
+
+ copy(store.surface, snapshot.surface, fragment.surface, background_in_stores);
+ if (outlines_enabled) copy(store.outline_surface, snapshot.outline_surface, fragment.outline_surface, false);
+
+ snapshot = std::move(fragment);
+}
+
+Cairo::RefPtr<Cairo::ImageSurface> CairoGraphics::request_tile_surface(Geom::IntRect const &rect, bool /*nogl*/)
+{
+ // Create temporary surface, isolated from store.
+ auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, rect.width() * scale_factor, rect.height() * scale_factor);
+ cairo_surface_set_device_scale(surface->cobj(), scale_factor, scale_factor);
+ return surface;
+}
+
+void CairoGraphics::draw_tile(Fragment const &fragment, Cairo::RefPtr<Cairo::ImageSurface> surface, Cairo::RefPtr<Cairo::ImageSurface> outline_surface)
+{
+ // Blit from the temporary surface to the store.
+ auto diff = fragment.rect.min() - stores.store().rect.min();
+
+ auto cr = Cairo::Context::create(store.surface);
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->set_source(surface, diff.x(), diff.y());
+ cr->rectangle(diff.x(), diff.y(), fragment.rect.width(), fragment.rect.height());
+ cr->fill();
+
+ if (outlines_enabled) {
+ auto cr = Cairo::Context::create(store.outline_surface);
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->set_source(outline_surface, diff.x(), diff.y());
+ cr->rectangle(diff.x(), diff.y(), fragment.rect.width(), fragment.rect.height());
+ cr->fill();
+ }
+}
+
+void CairoGraphics::paint_widget(Fragment const &view, PaintArgs const &a, Cairo::RefPtr<Cairo::Context> const &cr)
+{
+ auto f = FrameCheck::Event();
+
+ // Turn off anti-aliasing while compositing the widget for large performance gains. (We can usually
+ // get away with it without any negative visual impact; when we can't, we turn it back on.)
+ cr->set_antialias(Cairo::ANTIALIAS_NONE);
+
+ // Due to a Cairo bug, Cairo sometimes draws outside of its clip region. This results in flickering as Canvas content is drawn
+ // over the bottom scrollbar. This cannot be fixed by setting the correct clip region, as Cairo detects that and turns it into
+ // a no-op. Hence the following workaround, which recreates the clip region from scratch, is required.
+ auto rlist = cairo_copy_clip_rectangle_list(cr->cobj());
+ cr->reset_clip();
+ for (int i = 0; i < rlist->num_rectangles; i++) {
+ cr->rectangle(rlist->rectangles[i].x, rlist->rectangles[i].y, rlist->rectangles[i].width, rlist->rectangles[i].height);
+ }
+ cr->clip();
+ cairo_rectangle_list_destroy(rlist);
+
+ // Draw background if solid colour optimisation is not enabled. (If enabled, it is baked into the stores.)
+ if (!background_in_stores) {
+ if (prefs.debug_framecheck) f = FrameCheck::Event("background");
+ paint_background(view, pi, page, desk, cr);
+ }
+
+ // Even if in solid colour mode, draw the part of background that is not going to be rendered.
+ if (background_in_stores) {
+ auto const &s = stores.mode() == Stores::Mode::Decoupled ? stores.snapshot() : stores.store();
+ if (!(Geom::Parallelogram(s.rect) * s.affine.inverse() * view.affine).contains(view.rect)) {
+ if (prefs.debug_framecheck) f = FrameCheck::Event("background", 2);
+ cr->save();
+ cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD);
+ cr->rectangle(0, 0, view.rect.width(), view.rect.height());
+ cr->translate(-view.rect.left(), -view.rect.top());
+ cr->transform(geom_to_cairo(s.affine.inverse() * view.affine));
+ cr->rectangle(s.rect.left(), s.rect.top(), s.rect.width(), s.rect.height());
+ cr->clip();
+ cr->transform(geom_to_cairo(view.affine.inverse() * s.affine));
+ cr->translate(view.rect.left(), view.rect.top());
+ paint_background(view, pi, page, desk, cr);
+ cr->restore();
+ }
+ }
+
+ auto draw_store = [&, this] (Cairo::RefPtr<Cairo::ImageSurface> const &store, Cairo::RefPtr<Cairo::ImageSurface> const &snapshot_store) {
+ if (stores.mode() == Stores::Mode::Normal) {
+ // Blit store to view.
+ if (prefs.debug_framecheck) f = FrameCheck::Event("draw");
+ cr->save();
+ auto const &r = stores.store().rect;
+ cr->translate(-view.rect.left(), -view.rect.top());
+ cr->transform(geom_to_cairo(stores.store().affine.inverse() * view.affine)); // Almost always the identity.
+ cr->rectangle(r.left(), r.top(), r.width(), r.height());
+ cr->set_source(store, r.left(), r.top());
+ Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST);
+ cr->fill();
+ cr->restore();
+ } else {
+ // Draw transformed snapshot, clipped to the complement of the store's clean region.
+ if (prefs.debug_framecheck) f = FrameCheck::Event("composite", 1);
+
+ cr->save();
+ cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD);
+ cr->rectangle(0, 0, view.rect.width(), view.rect.height());
+ cr->translate(-view.rect.left(), -view.rect.top());
+ cr->transform(geom_to_cairo(stores.store().affine.inverse() * view.affine));
+ region_to_path(cr, stores.store().drawn);
+ cr->transform(geom_to_cairo(stores.snapshot().affine.inverse() * stores.store().affine));
+ cr->clip();
+ auto const &r = stores.snapshot().rect;
+ cr->rectangle(r.left(), r.top(), r.width(), r.height());
+ cr->clip();
+ cr->set_source(snapshot_store, r.left(), r.top());
+ Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST);
+ cr->paint();
+ if (prefs.debug_show_snapshot) {
+ cr->set_source_rgba(0, 0, 1, 0.2);
+ cr->set_operator(Cairo::OPERATOR_OVER);
+ cr->paint();
+ }
+ cr->restore();
+
+ // Draw transformed store, clipped to drawn region.
+ if (prefs.debug_framecheck) f = FrameCheck::Event("composite", 0);
+ cr->save();
+ cr->translate(-view.rect.left(), -view.rect.top());
+ cr->transform(geom_to_cairo(stores.store().affine.inverse() * view.affine));
+ cr->set_source(store, stores.store().rect.left(), stores.store().rect.top());
+ Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST);
+ region_to_path(cr, stores.store().drawn);
+ cr->fill();
+ cr->restore();
+ }
+ };
+
+ auto draw_overlay = [&, this] {
+ // Get whitewash opacity.
+ double outline_overlay_opacity = prefs.outline_overlay_opacity / 100.0;
+
+ // Partially obscure drawing by painting semi-transparent white, then paint outline content.
+ // Note: Unfortunately this also paints over the background, but this is unavoidable.
+ cr->save();
+ cr->set_operator(Cairo::OPERATOR_OVER);
+ cr->set_source_rgb(1.0, 1.0, 1.0);
+ cr->paint_with_alpha(outline_overlay_opacity);
+ draw_store(store.outline_surface, snapshot.outline_surface);
+ cr->restore();
+ };
+
+ if (a.splitmode == Inkscape::SplitMode::SPLIT) {
+
+ // Calculate the clipping rectangles for split view.
+ auto [store_clip, outline_clip] = calc_splitview_cliprects(view.rect.dimensions(), a.splitfrac, a.splitdir);
+
+ // Draw normal content.
+ cr->save();
+ cr->rectangle(store_clip.left(), store_clip.top(), store_clip.width(), store_clip.height());
+ cr->clip();
+ cr->set_operator(background_in_stores ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER);
+ draw_store(store.surface, snapshot.surface);
+ if (a.render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY) draw_overlay();
+ cr->restore();
+
+ // Draw outline.
+ if (background_in_stores) {
+ cr->save();
+ cr->translate(outline_clip.left(), outline_clip.top());
+ paint_background(Fragment{view.affine, view.rect.min() + outline_clip}, pi, page, desk, cr);
+ cr->restore();
+ }
+ cr->save();
+ cr->rectangle(outline_clip.left(), outline_clip.top(), outline_clip.width(), outline_clip.height());
+ cr->clip();
+ cr->set_operator(Cairo::OPERATOR_OVER);
+ draw_store(store.outline_surface, snapshot.outline_surface);
+ cr->restore();
+
+ } else {
+
+ // Draw the normal content over the whole view.
+ cr->set_operator(background_in_stores ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER);
+ draw_store(store.surface, snapshot.surface);
+ if (a.render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY) draw_overlay();
+
+ // Draw outline if in X-ray mode.
+ if (a.splitmode == Inkscape::SplitMode::XRAY && a.mouse) {
+ // Clip to circle
+ cr->set_antialias(Cairo::ANTIALIAS_DEFAULT);
+ cr->arc(a.mouse->x(), a.mouse->y(), prefs.xray_radius, 0, 2 * M_PI);
+ cr->clip();
+ cr->set_antialias(Cairo::ANTIALIAS_NONE);
+ // Draw background.
+ paint_background(view, pi, page, desk, cr);
+ // Draw outline.
+ cr->set_operator(Cairo::OPERATOR_OVER);
+ draw_store(store.outline_surface, snapshot.outline_surface);
+ }
+ }
+
+ // The rest can be done with antialiasing.
+ cr->set_antialias(Cairo::ANTIALIAS_DEFAULT);
+
+ if (a.splitmode == Inkscape::SplitMode::SPLIT) {
+ paint_splitview_controller(view.rect.dimensions(), a.splitfrac, a.splitdir, a.hoverdir, cr);
+ }
+}
+
+} // 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 :
diff --git a/src/ui/widget/canvas/cairographics.h b/src/ui/widget/canvas/cairographics.h
new file mode 100644
index 0000000..c29eff1
--- /dev/null
+++ b/src/ui/widget/canvas/cairographics.h
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Cairo display backend.
+ * Copyright (C) 2022 PBS <pbs3141@gmail.com>
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_CAIROGRAPHICS_H
+#define INKSCAPE_UI_WIDGET_CANVAS_CAIROGRAPHICS_H
+
+#include "graphics.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+struct CairoFragment
+{
+ Cairo::RefPtr<Cairo::ImageSurface> surface;
+ Cairo::RefPtr<Cairo::ImageSurface> outline_surface;
+};
+
+class CairoGraphics : public Graphics
+{
+public:
+ CairoGraphics(Prefs const &prefs, Stores const &stores, PageInfo const &pi);
+
+ void set_scale_factor(int scale) override { scale_factor = scale; }
+ void set_outlines_enabled(bool) override;
+ void set_background_in_stores(bool enabled) override { background_in_stores = enabled; }
+ void set_colours(uint32_t p, uint32_t d, uint32_t b) override { page = p; desk = d; border = b; }
+
+ void recreate_store(Geom::IntPoint const &dimensions) override;
+ void shift_store(Fragment const &dest) override;
+ void swap_stores() override;
+ void fast_snapshot_combine() override;
+ void snapshot_combine(Fragment const &dest) override;
+ void invalidate_snapshot() override {}
+
+ bool is_opengl() const override { return false; }
+ void invalidated_glstate() override {}
+
+ Cairo::RefPtr<Cairo::ImageSurface> request_tile_surface(Geom::IntRect const &rect, bool nogl) override;
+ void draw_tile(Fragment const &fragment, Cairo::RefPtr<Cairo::ImageSurface> surface, Cairo::RefPtr<Cairo::ImageSurface> outline_surface) override;
+ void junk_tile_surface(Cairo::RefPtr<Cairo::ImageSurface> surface) override {}
+
+ void paint_widget(Fragment const &view, PaintArgs const &args, Cairo::RefPtr<Cairo::Context> const &cr) override;
+
+private:
+ // Drawn content.
+ CairoFragment store, snapshot;
+
+ // Dependency objects in canvas.
+ Prefs const &prefs;
+ Stores const &stores;
+ PageInfo const &pi;
+
+ // Backend-agnostic state.
+ int scale_factor = 1;
+ bool outlines_enabled = false;
+ bool background_in_stores = false;
+ uint32_t page, desk, border;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_CAIROGRAPHICS_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/canvas/fragment.h b/src/ui/widget/canvas/fragment.h
new file mode 100644
index 0000000..d3edc74
--- /dev/null
+++ b/src/ui/widget/canvas/fragment.h
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_FRAGMENT_H
+#define INKSCAPE_UI_WIDGET_CANVAS_FRAGMENT_H
+
+#include <2geom/int-rect.h>
+#include <2geom/affine.h>
+
+namespace Inkscape::UI::Widget {
+
+/// A "fragment" is a rectangle of drawn content at a specfic place.
+struct Fragment
+{
+ // The matrix the geometry was transformed with when the content was drawn.
+ Geom::Affine affine;
+
+ // The rectangle of world space where the fragment was drawn.
+ Geom::IntRect rect;
+};
+
+} // namespace Inkscape::UI::Widget
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_FRAGMENT_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/canvas/framecheck.cpp b/src/ui/widget/canvas/framecheck.cpp
new file mode 100644
index 0000000..c127c8e
--- /dev/null
+++ b/src/ui/widget/canvas/framecheck.cpp
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <fstream>
+#include <iostream>
+#include <mutex>
+#include <boost/filesystem.hpp> // Using boost::filesystem instead of std::filesystem due to broken C++17 on MacOS.
+#include "framecheck.h"
+namespace fs = boost::filesystem;
+
+namespace Inkscape::FrameCheck {
+
+void Event::write()
+{
+ static std::mutex mutex;
+ static auto logfile = [] {
+ auto path = fs::temp_directory_path() / "framecheck.txt";
+ auto mode = std::ios_base::out | std::ios_base::app | std::ios_base::binary;
+ return std::ofstream(path.string(), mode);
+ }();
+
+ auto lock = std::lock_guard(mutex);
+ logfile << name << ' ' << start << ' ' << g_get_monotonic_time() << ' ' << subtype << std::endl;
+}
+
+} // namespace Inkscape::FrameCheck
diff --git a/src/ui/widget/canvas/framecheck.h b/src/ui/widget/canvas/framecheck.h
new file mode 100644
index 0000000..8964561
--- /dev/null
+++ b/src/ui/widget/canvas/framecheck.h
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_FRAMECHECK_H
+#define INKSCAPE_FRAMECHECK_H
+
+#include <glib.h>
+
+namespace Inkscape::FrameCheck {
+
+/// RAII object that logs a timing event for the duration of its lifetime.
+struct Event
+{
+ gint64 start;
+ char const *name;
+ int subtype;
+
+ Event() : start(-1) {}
+
+ Event(char const *name, int subtype = 0) : start(g_get_monotonic_time()), name(name), subtype(subtype) {}
+
+ Event(Event &&p) { movefrom(p); }
+
+ ~Event() { finish(); }
+
+ Event &operator=(Event &&p)
+ {
+ finish();
+ movefrom(p);
+ return *this;
+ }
+
+private:
+ void movefrom(Event &p)
+ {
+ start = p.start;
+ name = p.name;
+ subtype = p.subtype;
+ p.start = -1;
+ }
+
+ void finish() { if (start != -1) write(); }
+
+ void write();
+};
+
+} // namespace Inkscape::FrameCheck
+
+#endif // INKSCAPE_FRAMECHECK_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/canvas/glgraphics.cpp b/src/ui/widget/canvas/glgraphics.cpp
new file mode 100644
index 0000000..b00503c
--- /dev/null
+++ b/src/ui/widget/canvas/glgraphics.cpp
@@ -0,0 +1,873 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <2geom/transforms.h>
+#include <2geom/rect.h>
+#include "ui/util.h"
+#include "helper/geom.h"
+#include "glgraphics.h"
+#include "stores.h"
+#include "prefs.h"
+#include "pixelstreamer.h"
+#include "util.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+namespace {
+
+// 2Geom <-> OpenGL
+
+void geom_to_uniform_mat(Geom::Affine const &affine, GLuint location)
+{
+ glUniformMatrix2fv(location, 1, GL_FALSE, std::begin({(GLfloat)affine[0], (GLfloat)affine[1], (GLfloat)affine[2], (GLfloat)affine[3]}));
+}
+
+void geom_to_uniform_trans(Geom::Affine const &affine, GLuint location)
+{
+ glUniform2fv(location, 1, std::begin({(GLfloat)affine[4], (GLfloat)affine[5]}));
+}
+
+void geom_to_uniform(Geom::Affine const &affine, GLuint mat_location, GLuint trans_location)
+{
+ geom_to_uniform_mat(affine, mat_location);
+ geom_to_uniform_trans(affine, trans_location);
+}
+
+void geom_to_uniform(Geom::Point const &vec, GLuint location)
+{
+ glUniform2fv(location, 1, std::begin({(GLfloat)vec.x(), (GLfloat)vec.y()}));
+}
+
+// Get the affine transformation required to paste fragment A onto fragment B, assuming
+// coordinates such that A is a texture (0 to 1) and B is a framebuffer (-1 to 1).
+static auto calc_paste_transform(Fragment const &a, Fragment const &b)
+{
+ Geom::Affine result = Geom::Scale(a.rect.dimensions());
+
+ if (a.affine == b.affine) {
+ result *= Geom::Translate(a.rect.min() - b.rect.min());
+ } else {
+ result *= Geom::Translate(a.rect.min()) * a.affine.inverse() * b.affine * Geom::Translate(-b.rect.min());
+ }
+
+ return result * Geom::Scale(2.0 / b.rect.dimensions()) * Geom::Translate(-1.0, -1.0);
+}
+
+// Given a region, shrink it by 0.5px, and convert the result to a VAO of triangles.
+static auto region_shrink_vao(Cairo::RefPtr<Cairo::Region> const &reg, Geom::IntRect const &rel)
+{
+ // Shrink the region by 0.5 (translating it by (0.5, 0.5) in the process).
+ auto reg2 = shrink_region(reg, 1);
+
+ // Preallocate the vertex buffer.
+ int nrects = reg2->get_num_rectangles();
+ std::vector<GLfloat> verts;
+ verts.reserve(nrects * 12);
+
+ // Add a vertex to the buffer, transformed to a coordinate system in which the enclosing rectangle 'rel' goes from 0 to 1.
+ // Also shift them up/left by 0.5px; combined with the width/height increase from earlier, this shrinks the region by 0.5px.
+ auto emit_vertex = [&] (Geom::IntPoint const &pt) {
+ verts.emplace_back((pt.x() - 0.5f - rel.left()) / rel.width());
+ verts.emplace_back((pt.y() - 0.5f - rel.top() ) / rel.height());
+ };
+
+ // Todo: Use a better triangulation algorithm here that results in 1) less triangles, and 2) no seaming.
+ for (int i = 0; i < nrects; i++) {
+ auto rect = cairo_to_geom(reg2->get_rectangle(i));
+ for (int j = 0; j < 6; j++) {
+ int constexpr indices[] = {0, 1, 2, 0, 2, 3};
+ emit_vertex(rect.corner(indices[j]));
+ }
+ }
+
+ // Package the data in a VAO.
+ VAO result;
+ glGenBuffers(1, &result.vbuf);
+ glBindBuffer(GL_ARRAY_BUFFER, result.vbuf);
+ glBufferData(GL_ARRAY_BUFFER, verts.size() * sizeof(GLfloat), verts.data(), GL_STREAM_DRAW);
+ glGenVertexArrays(1, &result.vao);
+ glBindVertexArray(result.vao);
+ glEnableVertexAttribArray(0);
+ glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 2, 0);
+
+ // Return the VAO and the number of rectangles.
+ return std::make_pair(std::move(result), nrects);
+}
+
+auto pref_to_pixelstreamer(int index)
+{
+ auto constexpr arr = std::array{PixelStreamer::Method::Auto,
+ PixelStreamer::Method::Persistent,
+ PixelStreamer::Method::Asynchronous,
+ PixelStreamer::Method::Synchronous};
+ assert(1 <= index && index <= arr.size());
+ return arr[index - 1];
+}
+
+} // namespace
+
+GLGraphics::GLGraphics(Prefs const &prefs, Stores const &stores, PageInfo const &pi)
+ : prefs(prefs)
+ , stores(stores)
+ , pi(pi)
+{
+ // Create rectangle geometry.
+ GLfloat constexpr verts[] = {0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f};
+ glGenBuffers(1, &rect.vbuf);
+ glBindBuffer(GL_ARRAY_BUFFER, rect.vbuf);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);
+ glGenVertexArrays(1, &rect.vao);
+ glBindVertexArray(rect.vao);
+ glEnableVertexAttribArray(0);
+ glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 2, 0);
+
+ // Create shader programs.
+ auto vs = VShader(R"(
+ #version 330 core
+
+ uniform mat2 mat;
+ uniform vec2 trans;
+ uniform vec2 subrect;
+ layout(location = 0) in vec2 pos;
+ smooth out vec2 uv;
+
+ void main()
+ {
+ uv = pos * subrect;
+ vec2 pos2 = mat * pos + trans;
+ gl_Position = vec4(pos2.x, pos2.y, 0.0, 1.0);
+ }
+ )");
+
+ auto texcopy_fs = FShader(R"(
+ #version 330 core
+
+ uniform sampler2D tex;
+ smooth in vec2 uv;
+ out vec4 outColour;
+
+ void main()
+ {
+ outColour = texture(tex, uv);
+ }
+ )");
+
+ auto texcopydouble_fs = FShader(R"(
+ #version 330 core
+
+ uniform sampler2D tex;
+ uniform sampler2D tex_outline;
+ smooth in vec2 uv;
+ layout(location = 0) out vec4 outColour;
+ layout(location = 1) out vec4 outColour_outline;
+
+ void main()
+ {
+ outColour = texture(tex, uv);
+ outColour_outline = texture(tex_outline, uv);
+ }
+ )");
+
+ auto outlineoverlay_fs = FShader(R"(
+ #version 330 core
+
+ uniform sampler2D tex;
+ uniform sampler2D tex_outline;
+ uniform float opacity;
+ smooth in vec2 uv;
+ out vec4 outColour;
+
+ void main()
+ {
+ vec4 c1 = texture(tex, uv);
+ vec4 c2 = texture(tex_outline, uv);
+ vec4 c1w = vec4(mix(c1.rgb, vec3(1.0, 1.0, 1.0) * c1.a, opacity), c1.a);
+ outColour = c1w * (1.0 - c2.a) + c2;
+ }
+ )");
+
+ auto xray_fs = FShader(R"(
+ #version 330 core
+
+ uniform sampler2D tex;
+ uniform sampler2D tex_outline;
+ uniform vec2 pos;
+ uniform float radius;
+ smooth in vec2 uv;
+ out vec4 outColour;
+
+ void main()
+ {
+ vec4 c1 = texture(tex, uv);
+ vec4 c2 = texture(tex_outline, uv);
+
+ float r = length(gl_FragCoord.xy - pos);
+ r = clamp((radius - r) / 2.0, 0.0, 1.0);
+
+ outColour = mix(c1, c2, r);
+ }
+ )");
+
+ auto outlineoverlayxray_fs = FShader(R"(
+ #version 330 core
+
+ uniform sampler2D tex;
+ uniform sampler2D tex_outline;
+ uniform float opacity;
+ uniform vec2 pos;
+ uniform float radius;
+ smooth in vec2 uv;
+ out vec4 outColour;
+
+ void main()
+ {
+ vec4 c1 = texture(tex, uv);
+ vec4 c2 = texture(tex_outline, uv);
+ vec4 c1w = vec4(mix(c1.rgb, vec3(1.0, 1.0, 1.0) * c1.a, opacity), c1.a);
+ outColour = c1w * (1.0 - c2.a) + c2;
+
+ float r = length(gl_FragCoord.xy - pos);
+ r = clamp((radius - r) / 2.0, 0.0, 1.0);
+
+ outColour = mix(outColour, c2, r);
+ }
+ )");
+
+ auto checker_fs = FShader(R"(
+ #version 330 core
+
+ uniform float size;
+ uniform vec3 col1, col2;
+ out vec4 outColour;
+
+ void main()
+ {
+ vec2 a = floor(fract(gl_FragCoord.xy / size) * 2.0);
+ float b = abs(a.x - a.y);
+ outColour = vec4((1.0 - b) * col1 + b * col2, 1.0);
+ }
+ )");
+
+ auto shadow_gs = GShader(R"(
+ #version 330 core
+
+ layout(triangles) in;
+ layout(triangle_strip, max_vertices = 10) out;
+
+ uniform vec2 wh;
+ uniform float size;
+ uniform vec2 dir;
+
+ smooth out vec2 uv;
+ flat out vec2 maxuv;
+
+ void f(vec4 p, vec4 v0, mat2 m)
+ {
+ gl_Position = p;
+ uv = m * (p.xy - v0.xy);
+ EmitVertex();
+ }
+
+ float push(float x)
+ {
+ return 0.15 * (1.0 + clamp(x / 0.707, -1.0, 1.0));
+ }
+
+ void main()
+ {
+ vec4 v0 = gl_in[0].gl_Position;
+ vec4 v1 = gl_in[1].gl_Position;
+ vec4 v2 = gl_in[2].gl_Position;
+ vec4 v3 = gl_in[2].gl_Position - gl_in[1].gl_Position + gl_in[0].gl_Position;
+
+ vec2 a = normalize((v1 - v0).xy * wh);
+ vec2 b = normalize((v3 - v0).xy * wh);
+ float det = a.x * b.y - a.y * b.x;
+ float s = -sign(det);
+ vec2 c = size / abs(det) / wh;
+ vec4 d = vec4(a * c, 0.0, 0.0);
+ vec4 e = vec4(b * c, 0.0, 0.0);
+ mat2 m = s * mat2(a.y, -b.y, -a.x, b.x) * mat2(wh.x, 0.0, 0.0, wh.y) / size;
+
+ float ap = s * dot(vec2(a.y, -a.x), dir);
+ float bp = s * dot(vec2(-b.y, b.x), dir);
+ v0.xy += (b * push( ap) + a * push( bp)) * size / wh;
+ v1.xy += (b * push( ap) + a * -push(-bp)) * size / wh;
+ v2.xy += (b * -push(-ap) + a * -push(-bp)) * size / wh;
+ v3.xy += (b * -push(-ap) + a * push( bp)) * size / wh;
+
+ maxuv = m * (v2.xy - v0.xy);
+ f(v0, v0, m);
+ f(v0 - d - e, v0, m);
+ f(v1, v0, m);
+ f(v1 + d - e, v0, m);
+ f(v2, v0, m);
+ f(v2 + d + e, v0, m);
+ f(v3, v0, m);
+ f(v3 - d + e, v0, m);
+ f(v0, v0, m);
+ f(v0 - d - e, v0, m);
+ EndPrimitive();
+ }
+ )");
+
+ auto shadow_fs = FShader(R"(
+ #version 330 core
+
+ uniform vec4 shadow_col;
+
+ smooth in vec2 uv;
+ flat in vec2 maxuv;
+
+ out vec4 outColour;
+
+ void main()
+ {
+ float x = max(uv.x - maxuv.x, 0.0) - max(-uv.x, 0.0);
+ float y = max(uv.y - maxuv.y, 0.0) - max(-uv.y, 0.0);
+ float s = min(length(vec2(x, y)), 1.0);
+
+ float A = 4.0; // This coefficient changes how steep the curve is and controls shadow drop-off.
+ s = (exp(A * (1.0 - s)) - 1.0) / (exp(A) - 1.0); // Exponential decay for drop shadow - long tail.
+
+ outColour = shadow_col * s;
+ }
+ )");
+
+ texcopy.create(vs, texcopy_fs);
+ texcopydouble.create(vs, texcopydouble_fs);
+ outlineoverlay.create(vs, outlineoverlay_fs);
+ xray.create(vs, xray_fs);
+ outlineoverlayxray.create(vs, outlineoverlayxray_fs);
+ checker.create(vs, checker_fs);
+ shadow.create(vs, shadow_gs, shadow_fs);
+
+ // Create the framebuffer object for rendering to off-view fragments.
+ glGenFramebuffers(1, &fbo);
+
+ // Create the texture cache.
+ texturecache = TextureCache::create();
+
+ // Create the PixelStreamer.
+ pixelstreamer = PixelStreamer::create_supported(pref_to_pixelstreamer(prefs.pixelstreamer_method));
+
+ // Set the last known state as unspecified, forcing a pipeline recreation whatever the next operation is.
+ state = State::None;
+}
+
+GLGraphics::~GLGraphics()
+{
+ glDeleteFramebuffers(1, &fbo);
+}
+
+std::unique_ptr<Graphics> Graphics::create_gl(Prefs const &prefs, Stores const &stores, PageInfo const &pi)
+{
+ return std::make_unique<GLGraphics>(prefs, stores, pi);
+}
+
+void GLGraphics::set_outlines_enabled(bool enabled)
+{
+ outlines_enabled = enabled;
+ if (!enabled) {
+ store.outline_texture.clear();
+ snapshot.outline_texture.clear();
+ }
+}
+
+void GLGraphics::setup_stores_pipeline()
+{
+ if (state == State::Stores) return;
+ state = State::Stores;
+
+ glDisable(GL_BLEND);
+
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
+ GLuint constexpr attachments[2] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1};
+ glDrawBuffers(outlines_enabled ? 2 : 1, attachments);
+
+ auto const &shader = outlines_enabled ? texcopydouble : texcopy;
+ glUseProgram(shader.id);
+ mat_loc = shader.loc("mat");
+ trans_loc = shader.loc("trans");
+ geom_to_uniform({1.0, 1.0}, shader.loc("subrect"));
+ tex_loc = shader.loc("tex");
+ if (outlines_enabled) texoutline_loc = shader.loc("tex_outline");
+}
+
+void GLGraphics::recreate_store(Geom::IntPoint const &dims)
+{
+ auto tex_size = dims * scale_factor;
+
+ // Setup the base pipeline.
+ setup_stores_pipeline();
+
+ // Recreate the store textures.
+ auto recreate = [&] (Texture &tex) {
+ if (tex && tex.size() == tex_size) {
+ tex.invalidate();
+ } else {
+ tex = Texture(tex_size);
+ }
+ };
+
+ recreate(store.texture);
+ if (outlines_enabled) {
+ recreate(store.outline_texture);
+ }
+
+ // Bind the store to the framebuffer for writing to.
+ glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, store.texture.id(), 0);
+ if (outlines_enabled) glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, store.outline_texture.id(), 0);
+ glViewport(0, 0, store.texture.size().x(), store.texture.size().y());
+
+ // Clear the store to transparent.
+ glClearColor(0.0, 0.0, 0.0, 0.0);
+ glClear(GL_COLOR_BUFFER_BIT);
+}
+
+void GLGraphics::shift_store(Fragment const &dest)
+{
+ auto tex_size = dest.rect.dimensions() * scale_factor;
+
+ // Setup the base pipeline.
+ setup_stores_pipeline();
+
+ // Create the new fragment.
+ auto create_or_reuse = [&] (Texture &tex, Texture &from) {
+ if (from && from.size() == tex_size) {
+ from.invalidate();
+ tex = std::move(from);
+ } else {
+ tex = Texture(tex_size);
+ }
+ };
+
+ GLFragment fragment;
+ create_or_reuse(fragment.texture, snapshot.texture);
+ if (outlines_enabled) {
+ create_or_reuse(fragment.outline_texture, snapshot.outline_texture);
+ }
+
+ // Bind new store to the framebuffer to writing to.
+ glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fragment.texture .id(), 0);
+ if (outlines_enabled) glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, fragment.outline_texture.id(), 0);
+ glViewport(0, 0, fragment.texture.size().x(), fragment.texture.size().y());
+
+ // Clear new store to transparent.
+ glClearColor(0.0, 0.0, 0.0, 0.0);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ // Bind the old store to texture units 0 and 1 for reading from.
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, store.texture.id());
+ glUniform1i(tex_loc, 0);
+ if (outlines_enabled) {
+ glActiveTexture(GL_TEXTURE1);
+ glBindTexture(GL_TEXTURE_2D, store.outline_texture.id());
+ glUniform1i(texoutline_loc, 1);
+ }
+ glBindVertexArray(rect.vao);
+
+ // Copy re-usuable contents of the old store into the new store.
+ geom_to_uniform(calc_paste_transform(stores.store(), dest), mat_loc, trans_loc);
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+
+ // Set the result as the new store.
+ snapshot = std::move(store);
+ store = std::move(fragment);
+}
+
+void GLGraphics::swap_stores()
+{
+ std::swap(store, snapshot);
+}
+
+void GLGraphics::fast_snapshot_combine()
+{
+ // Ensure the base pipeline is correctly set up.
+ setup_stores_pipeline();
+
+ // Compute the vertex data for the drawn region.
+ auto [clean_vao, clean_numrects] = region_shrink_vao(stores.store().drawn, stores.store().rect);
+
+ // Bind the snapshot to the framebuffer for writing to.
+ glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, snapshot.texture.id(), 0);
+ if (outlines_enabled) glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, snapshot.outline_texture.id(), 0);
+ glViewport(0, 0, snapshot.texture.size().x(), snapshot.texture.size().y());
+
+ // Bind the store to texture unit 0 (and its outline to 1, if necessary).
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, store.texture.id());
+ glUniform1i(tex_loc, 0);
+ if (outlines_enabled) {
+ glActiveTexture(GL_TEXTURE1);
+ glBindTexture(GL_TEXTURE_2D, store.outline_texture.id());
+ glUniform1i(texoutline_loc, 1);
+ }
+
+ // Copy the clean region of the store to the snapshot.
+ geom_to_uniform(calc_paste_transform(stores.store(), stores.snapshot()), mat_loc, trans_loc);
+ glBindVertexArray(clean_vao.vao);
+ glDrawArrays(GL_TRIANGLES, 0, 6 * clean_numrects);
+}
+
+void GLGraphics::snapshot_combine(Fragment const &dest)
+{
+ // Create the new fragment.
+ auto content_size = dest.rect.dimensions() * scale_factor;
+
+ // Ensure the base pipeline is correctly set up.
+ setup_stores_pipeline();
+
+ // Compute the vertex data for the clean region.
+ auto [clean_vao, clean_numrects] = region_shrink_vao(stores.store().drawn, stores.store().rect);
+
+ GLFragment fragment;
+ fragment.texture = Texture(content_size);
+ if (outlines_enabled) fragment.outline_texture = Texture(content_size);
+
+ // Bind the new fragment to the framebuffer for writing to.
+ glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fragment.texture.id(), 0);
+ if (outlines_enabled) glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, fragment.outline_texture.id(), 0);
+
+ // Clear the new fragment to transparent.
+ glViewport(0, 0, fragment.texture.size().x(), fragment.texture.size().y());
+ glClearColor(0.0, 0.0, 0.0, 0.0);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ // Bind the store and snapshot to texture units 0 and 1 (and their outlines to 2 and 3, if necessary).
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, snapshot.texture.id());
+ glActiveTexture(GL_TEXTURE1);
+ glBindTexture(GL_TEXTURE_2D, store.texture.id());
+ if (outlines_enabled) {
+ glActiveTexture(GL_TEXTURE2);
+ glBindTexture(GL_TEXTURE_2D, snapshot.outline_texture.id());
+ glActiveTexture(GL_TEXTURE3);
+ glBindTexture(GL_TEXTURE_2D, store.outline_texture.id());
+ }
+
+ // Paste the snapshot store onto the new fragment.
+ glUniform1i(tex_loc, 0);
+ if (outlines_enabled) glUniform1i(texoutline_loc, 2);
+ geom_to_uniform(calc_paste_transform(stores.snapshot(), dest), mat_loc, trans_loc);
+ glBindVertexArray(rect.vao);
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+
+ // Paste the backing store onto the new fragment.
+ glUniform1i(tex_loc, 1);
+ if (outlines_enabled) glUniform1i(texoutline_loc, 3);
+ geom_to_uniform(calc_paste_transform(stores.store(), dest), mat_loc, trans_loc);
+ glBindVertexArray(clean_vao.vao);
+ glDrawArrays(GL_TRIANGLES, 0, 6 * clean_numrects);
+
+ // Set the result as the new snapshot.
+ snapshot = std::move(fragment);
+}
+
+void GLGraphics::invalidate_snapshot()
+{
+ if (snapshot.texture) snapshot.texture.invalidate();
+ if (snapshot.outline_texture) snapshot.outline_texture.invalidate();
+}
+
+void GLGraphics::setup_tiles_pipeline()
+{
+ if (state == State::Tiles) return;
+ state = State::Tiles;
+
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
+ GLuint constexpr attachments[2] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1};
+ glDrawBuffers(outlines_enabled ? 2 : 1, attachments);
+ glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, store.texture.id(), 0);
+ if (outlines_enabled) glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, store.outline_texture.id(), 0);
+ glViewport(0, 0, store.texture.size().x(), store.texture.size().y());
+
+ auto const &shader = outlines_enabled ? texcopydouble : texcopy;
+ glUseProgram(shader.id);
+ mat_loc = shader.loc("mat");
+ trans_loc = shader.loc("trans");
+ subrect_loc = shader.loc("subrect");
+ glUniform1i(shader.loc("tex"), 0);
+ if (outlines_enabled) glUniform1i(shader.loc("tex_outline"), 1);
+
+ glBindVertexArray(rect.vao);
+ glDisable(GL_BLEND);
+};
+
+Cairo::RefPtr<Cairo::ImageSurface> GLGraphics::request_tile_surface(Geom::IntRect const &rect, bool nogl)
+{
+ Cairo::RefPtr<Cairo::ImageSurface> surface;
+
+ {
+ auto g = std::lock_guard(ps_mutex);
+ surface = pixelstreamer->request(rect.dimensions() * scale_factor, nogl);
+ }
+
+ if (surface) {
+ cairo_surface_set_device_scale(surface->cobj(), scale_factor, scale_factor);
+ }
+
+ return surface;
+}
+
+void GLGraphics::draw_tile(Fragment const &fragment, Cairo::RefPtr<Cairo::ImageSurface> surface, Cairo::RefPtr<Cairo::ImageSurface> outline_surface)
+{
+ auto g = std::lock_guard(ps_mutex);
+ auto surface_size = dimensions(surface);
+
+ Texture texture, outline_texture;
+
+ glActiveTexture(GL_TEXTURE0);
+ texture = texturecache->request(surface_size); // binds
+ pixelstreamer->finish(std::move(surface)); // uploads content
+
+ if (outlines_enabled) {
+ glActiveTexture(GL_TEXTURE1);
+ outline_texture = texturecache->request(surface_size);
+ pixelstreamer->finish(std::move(outline_surface));
+ }
+
+ setup_tiles_pipeline();
+
+ geom_to_uniform(calc_paste_transform(fragment, stores.store()), mat_loc, trans_loc);
+ geom_to_uniform(Geom::Point(surface_size) / texture.size(), subrect_loc);
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+
+ texturecache->finish(std::move(texture));
+ if (outlines_enabled) {
+ texturecache->finish(std::move(outline_texture));
+ }
+}
+
+void GLGraphics::junk_tile_surface(Cairo::RefPtr<Cairo::ImageSurface> surface)
+{
+ auto g = std::lock_guard(ps_mutex);
+ pixelstreamer->finish(std::move(surface), true);
+}
+
+void GLGraphics::setup_widget_pipeline(Fragment const &view)
+{
+ state = State::Widget;
+
+ glDrawBuffer(GL_COLOR_ATTACHMENT0);
+ glViewport(0, 0, view.rect.width() * scale_factor, view.rect.height() * scale_factor);
+ glEnable(GL_STENCIL_TEST);
+ glStencilFunc(GL_NOTEQUAL, 1, 1);
+ glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, store.texture.id());
+ glActiveTexture(GL_TEXTURE1);
+ glBindTexture(GL_TEXTURE_2D, snapshot.texture.id());
+ if (outlines_enabled) {
+ glActiveTexture(GL_TEXTURE2);
+ glBindTexture(GL_TEXTURE_2D, store.outline_texture.id());
+ glActiveTexture(GL_TEXTURE3);
+ glBindTexture(GL_TEXTURE_2D, snapshot.outline_texture.id());
+ }
+ glBindVertexArray(rect.vao);
+};
+
+void GLGraphics::paint_widget(Fragment const &view, PaintArgs const &a, Cairo::RefPtr<Cairo::Context> const&)
+{
+ // If in decoupled mode, create the vertex data describing the drawn region of the store.
+ VAO clean_vao;
+ int clean_numrects;
+ if (stores.mode() == Stores::Mode::Decoupled) {
+ std::tie(clean_vao, clean_numrects) = region_shrink_vao(stores.store().drawn, stores.store().rect);
+ }
+
+ setup_widget_pipeline(view);
+
+ // Clear the buffers. Since we have to pick a clear colour, we choose the page colour, enabling the single-page optimisation later.
+ glClearColor(SP_RGBA32_R_U(page) / 255.0f, SP_RGBA32_G_U(page) / 255.0f, SP_RGBA32_B_U(page) / 255.0f, 1.0);
+ glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
+
+ if (check_single_page(view, pi)) {
+ // A single page occupies the whole view.
+ if (SP_RGBA32_A_U(page) == 255) {
+ // Page is solid - nothing to do, since already cleared to this colour.
+ } else {
+ // Page is checkerboard - fill view with page pattern.
+ glDisable(GL_BLEND);
+ glUseProgram(checker.id);
+ glUniform1f(checker.loc("size"), 12.0 * scale_factor);
+ glUniform3fv(checker.loc("col1"), 1, std::begin(rgb_to_array(page)));
+ glUniform3fv(checker.loc("col2"), 1, std::begin(checkerboard_darken(page)));
+ geom_to_uniform(Geom::Scale(2.0, -2.0) * Geom::Translate(-1.0, 1.0), checker.loc("mat"), checker.loc("trans"));
+ geom_to_uniform({1.0, 1.0}, checker.loc("subrect"));
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+ }
+
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
+ } else {
+ glDisable(GL_BLEND);
+
+ auto set_page_transform = [&] (Geom::Rect const &rect, Program const &prog) {
+ geom_to_uniform(Geom::Scale(rect.dimensions()) * Geom::Translate(rect.min()) * calc_paste_transform({{}, Geom::IntRect::from_xywh(0, 0, 1, 1)}, view) * Geom::Scale(1.0, -1.0), prog.loc("mat"), prog.loc("trans"));
+ };
+
+ // Pages
+ glUseProgram(checker.id);
+ glUniform1f(checker.loc("size"), 12.0 * scale_factor);
+ glUniform3fv(checker.loc("col1"), 1, std::begin(rgb_to_array(page)));
+ glUniform3fv(checker.loc("col2"), 1, std::begin(checkerboard_darken(page)));
+ geom_to_uniform({1.0, 1.0}, checker.loc("subrect"));
+ for (auto &rect : pi.pages) {
+ set_page_transform(rect, checker);
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+ }
+
+ glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
+
+ // Desk
+ glUniform3fv(checker.loc("col1"), 1, std::begin(rgb_to_array(desk)));
+ glUniform3fv(checker.loc("col2"), 1, std::begin(checkerboard_darken(desk)));
+ geom_to_uniform(Geom::Scale(2.0, -2.0) * Geom::Translate(-1.0, 1.0), checker.loc("mat"), checker.loc("trans"));
+ geom_to_uniform({1.0, 1.0}, checker.loc("subrect"));
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
+
+ // Shadows
+ if (SP_RGBA32_A_U(border) != 0) {
+ auto dir = (Geom::Point(1.0, a.yaxisdir) * view.affine * Geom::Scale(1.0, -1.0)).normalized(); // Shadow direction rotates with view.
+ glUseProgram(shadow.id);
+ geom_to_uniform({1.0, 1.0}, shadow.loc("subrect"));
+ glUniform2fv(shadow.loc("wh"), 1, std::begin({(GLfloat)view.rect.width(), (GLfloat)view.rect.height()}));
+ glUniform1f(shadow.loc("size"), 40.0 * std::pow(std::abs(view.affine.det()), 0.25));
+ glUniform2fv(shadow.loc("dir"), 1, std::begin({(GLfloat)dir.x(), (GLfloat)dir.y()}));
+ glUniform4fv(shadow.loc("shadow_col"), 1, std::begin(premultiplied(rgba_to_array(border))));
+ for (auto &rect : pi.pages) {
+ set_page_transform(rect, shadow);
+ glDrawArrays(GL_TRIANGLES, 0, 3);
+ }
+ }
+
+ glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
+ }
+
+ glStencilFunc(GL_NOTEQUAL, 2, 2);
+
+ enum class DrawMode
+ {
+ Store,
+ Outline,
+ Combine
+ };
+
+ auto draw_store = [&, this] (Program const &prog, DrawMode drawmode) {
+ glUseProgram(prog.id);
+ geom_to_uniform({1.0, 1.0}, prog.loc("subrect"));
+ glUniform1i(prog.loc("tex"), drawmode == DrawMode::Outline ? 2 : 0);
+ if (drawmode == DrawMode::Combine) {
+ glUniform1i(prog.loc("tex_outline"), 2);
+ glUniform1f(prog.loc("opacity"), prefs.outline_overlay_opacity / 100.0);
+ }
+
+ if (stores.mode() == Stores::Mode::Normal) {
+ // Backing store fragment.
+ geom_to_uniform(calc_paste_transform(stores.store(), view) * Geom::Scale(1.0, -1.0), prog.loc("mat"), prog.loc("trans"));
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+ } else {
+ // Backing store fragment, clipped to its clean region.
+ geom_to_uniform(calc_paste_transform(stores.store(), view) * Geom::Scale(1.0, -1.0), prog.loc("mat"), prog.loc("trans"));
+ glBindVertexArray(clean_vao.vao);
+ glDrawArrays(GL_TRIANGLES, 0, 6 * clean_numrects);
+
+ // Snapshot fragment.
+ glUniform1i(prog.loc("tex"), drawmode == DrawMode::Outline ? 3 : 1);
+ if (drawmode == DrawMode::Combine) glUniform1i(prog.loc("tex_outline"), 3);
+ geom_to_uniform(calc_paste_transform(stores.snapshot(), view) * Geom::Scale(1.0, -1.0), prog.loc("mat"), prog.loc("trans"));
+ glBindVertexArray(rect.vao);
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+ }
+ };
+
+ if (a.splitmode == Inkscape::SplitMode::NORMAL || (a.splitmode == Inkscape::SplitMode::XRAY && !a.mouse)) {
+
+ // Drawing the backing store over the whole view.
+ a.render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY
+ ? draw_store(outlineoverlay, DrawMode::Combine)
+ : draw_store(texcopy, DrawMode::Store);
+
+ } else if (a.splitmode == Inkscape::SplitMode::SPLIT) {
+
+ // Calculate the clipping rectangles for split view.
+ auto [store_clip, outline_clip] = calc_splitview_cliprects(view.rect.dimensions(), a.splitfrac, a.splitdir);
+
+ glEnable(GL_SCISSOR_TEST);
+
+ // Draw the backing store.
+ glScissor(store_clip.left() * scale_factor, (view.rect.height() - store_clip.bottom()) * scale_factor, store_clip.width() * scale_factor, store_clip.height() * scale_factor);
+ a.render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY
+ ? draw_store(outlineoverlay, DrawMode::Combine)
+ : draw_store(texcopy, DrawMode::Store);
+
+ // Draw the outline store.
+ glScissor(outline_clip.left() * scale_factor, (view.rect.height() - outline_clip.bottom()) * scale_factor, outline_clip.width() * scale_factor, outline_clip.height() * scale_factor);
+ draw_store(texcopy, DrawMode::Outline);
+
+ glDisable(GL_SCISSOR_TEST);
+ glDisable(GL_STENCIL_TEST);
+
+ // Calculate the bounding rectangle of the split view controller.
+ auto rect = Geom::IntRect({0, 0}, view.rect.dimensions());
+ auto dim = a.splitdir == Inkscape::SplitDirection::EAST || a.splitdir == Inkscape::SplitDirection::WEST ? Geom::X : Geom::Y;
+ rect[dim] = Geom::IntInterval(-21, 21) + std::round(a.splitfrac[dim] * view.rect.dimensions()[dim]);
+
+ // Lease out a PixelStreamer mapping to draw on.
+ auto surface_size = rect.dimensions() * scale_factor;
+ auto surface = pixelstreamer->request(surface_size);
+ cairo_surface_set_device_scale(surface->cobj(), scale_factor, scale_factor);
+
+ // Actually draw the content with Cairo.
+ auto cr = Cairo::Context::create(surface);
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->set_source_rgba(0.0, 0.0, 0.0, 0.0);
+ cr->paint();
+ cr->translate(-rect.left(), -rect.top());
+ paint_splitview_controller(view.rect.dimensions(), a.splitfrac, a.splitdir, a.hoverdir, cr);
+
+ // Convert the surface to a texture.
+ glActiveTexture(GL_TEXTURE0);
+ auto texture = texturecache->request(surface_size);
+ pixelstreamer->finish(std::move(surface));
+
+ // Paint the texture onto the view.
+ glUseProgram(texcopy.id);
+ glUniform1i(texcopy.loc("tex"), 0);
+ geom_to_uniform(Geom::Scale(rect.dimensions()) * Geom::Translate(rect.min()) * Geom::Scale(2.0 / view.rect.width(), -2.0 / view.rect.height()) * Geom::Translate(-1.0, 1.0), texcopy.loc("mat"), texcopy.loc("trans"));
+ geom_to_uniform(Geom::Point(surface_size) / texture.size(), texcopy.loc("subrect"));
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+
+ // Return the texture back to the texture cache.
+ texturecache->finish(std::move(texture));
+
+ } else { // if (_split_mode == Inkscape::SplitMode::XRAY && a.mouse)
+
+ // Draw the backing store over the whole view.
+ auto const &shader = a.render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY ? outlineoverlayxray : xray;
+ glUseProgram(shader.id);
+ glUniform1f(shader.loc("radius"), prefs.xray_radius * scale_factor);
+ glUniform2fv(shader.loc("pos"), 1, std::begin({(GLfloat)(a.mouse->x() * scale_factor), (GLfloat)((view.rect.height() - a.mouse->y()) * scale_factor)}));
+ draw_store(shader, DrawMode::Combine);
+ }
+}
+
+} // 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 :
diff --git a/src/ui/widget/canvas/glgraphics.h b/src/ui/widget/canvas/glgraphics.h
new file mode 100644
index 0000000..7cb6ecf
--- /dev/null
+++ b/src/ui/widget/canvas/glgraphics.h
@@ -0,0 +1,144 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * OpenGL display backend.
+ * Copyright (C) 2022 PBS <pbs3141@gmail.com>
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_GLGRAPHICS_H
+#define INKSCAPE_UI_WIDGET_CANVAS_GLGRAPHICS_H
+
+#include <mutex>
+#include <epoxy/gl.h>
+#include "graphics.h"
+#include "texturecache.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+class Stores;
+class Prefs;
+class PixelStreamer;
+
+template <GLuint type>
+struct Shader : boost::noncopyable
+{
+ GLuint id;
+ Shader(char const *src) { id = glCreateShader(type); glShaderSource(id, 1, &src, nullptr); glCompileShader(id); }
+ ~Shader() { glDeleteShader(id); }
+};
+using GShader = Shader<GL_GEOMETRY_SHADER>;
+using VShader = Shader<GL_VERTEX_SHADER>;
+using FShader = Shader<GL_FRAGMENT_SHADER>;
+
+struct Program : boost::noncopyable
+{
+ GLuint id = 0;
+ void create(VShader const &v, FShader const &f) { id = glCreateProgram(); glAttachShader(id, v.id); glAttachShader(id, f.id); glLinkProgram(id); }
+ void create(VShader const &v, const GShader &g, FShader const &f) { id = glCreateProgram(); glAttachShader(id, v.id); glAttachShader(id, g.id); glAttachShader(id, f.id); glLinkProgram(id); }
+ auto loc(char const *str) const { return glGetUniformLocation(id, str); }
+ ~Program() { glDeleteProgram(id); }
+};
+
+class VAO
+{
+public:
+ GLuint vao = 0;
+ GLuint vbuf;
+
+ VAO() = default;
+ VAO(GLuint vao, GLuint vbuf) : vao(vao), vbuf(vbuf) {}
+ VAO(VAO &&other) noexcept { movefrom(other); }
+ VAO &operator=(VAO &&other) noexcept { reset(); movefrom(other); return *this; }
+ ~VAO() { reset(); }
+
+private:
+ void reset() noexcept { if (vao) { glDeleteVertexArrays(1, &vao); glDeleteBuffers(1, &vbuf); } }
+ void movefrom(VAO &other) noexcept { vao = other.vao; vbuf = other.vbuf; other.vao = 0; }
+};
+
+struct GLFragment
+{
+ Texture texture;
+ Texture outline_texture;
+};
+
+class GLGraphics : public Graphics
+{
+public:
+ GLGraphics(Prefs const &prefs, Stores const &stores, PageInfo const &pi);
+ ~GLGraphics() override;
+
+ void set_scale_factor(int scale) override { scale_factor = scale; }
+ void set_outlines_enabled(bool) override;
+ void set_background_in_stores(bool enabled) override { background_in_stores = enabled; }
+ void set_colours(uint32_t p, uint32_t d, uint32_t b) override { page = p; desk = d; border = b; }
+
+ void recreate_store(Geom::IntPoint const &dimensions) override;
+ void shift_store(Fragment const &dest) override;
+ void swap_stores() override;
+ void fast_snapshot_combine() override;
+ void snapshot_combine(Fragment const &dest) override;
+ void invalidate_snapshot() override;
+
+ bool is_opengl() const override { return true; }
+ void invalidated_glstate() override { state = State::None; }
+
+ Cairo::RefPtr<Cairo::ImageSurface> request_tile_surface(Geom::IntRect const &rect, bool nogl) override;
+ void draw_tile(Fragment const &fragment, Cairo::RefPtr<Cairo::ImageSurface> surface, Cairo::RefPtr<Cairo::ImageSurface> outline_surface) override;
+ void junk_tile_surface(Cairo::RefPtr<Cairo::ImageSurface> surface) override;
+
+ void paint_widget(Fragment const &view, PaintArgs const &args, Cairo::RefPtr<Cairo::Context> const &cr) override;
+
+private:
+ // Drawn content.
+ GLFragment store, snapshot;
+
+ // OpenGL objects.
+ VAO rect; // Rectangle vertex data.
+ Program checker, shadow, texcopy, texcopydouble, outlineoverlay, xray, outlineoverlayxray; // Shaders
+ GLuint fbo; // Framebuffer object for rendering to stores.
+
+ // Pixel streamer and texture cache for uploading pixel data to GPU.
+ std::unique_ptr<PixelStreamer> pixelstreamer;
+ std::unique_ptr<TextureCache> texturecache;
+ std::mutex ps_mutex;
+
+ // For preventing unnecessary pipeline recreation.
+ enum class State { None, Widget, Stores, Tiles };
+ State state;
+ void setup_stores_pipeline();
+ void setup_tiles_pipeline();
+ void setup_widget_pipeline(Fragment const &view);
+
+ // For caching frequently-used uniforms.
+ GLuint mat_loc, trans_loc, subrect_loc, tex_loc, texoutline_loc;
+
+ // Dependency objects in canvas.
+ Prefs const &prefs;
+ Stores const &stores;
+ PageInfo const &pi;
+
+ // Backend-agnostic state.
+ int scale_factor = 1;
+ bool outlines_enabled = false;
+ bool background_in_stores = false;
+ uint32_t page, desk, border;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_GLGRAPHICS_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/canvas/graphics.cpp b/src/ui/widget/canvas/graphics.cpp
new file mode 100644
index 0000000..28972e2
--- /dev/null
+++ b/src/ui/widget/canvas/graphics.cpp
@@ -0,0 +1,166 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <2geom/parallelogram.h>
+#include "ui/util.h"
+#include "helper/geom.h"
+#include "graphics.h"
+#include "util.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+namespace {
+
+// Convert an rgba into a pattern, turning transparency into checkerboard-ness.
+Cairo::RefPtr<Cairo::Pattern> rgba_to_pattern(uint32_t rgba)
+{
+ if (SP_RGBA32_A_U(rgba) == 255) {
+ return Cairo::SolidPattern::create_rgb(SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), SP_RGBA32_B_F(rgba));
+ } else {
+ int constexpr w = 6;
+ int constexpr h = 6;
+
+ auto dark = checkerboard_darken(rgba);
+
+ auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, 2 * w, 2 * h);
+
+ auto cr = Cairo::Context::create(surface);
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->set_source_rgb(SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), SP_RGBA32_B_F(rgba));
+ cr->paint();
+ cr->set_source_rgb(dark[0], dark[1], dark[2]);
+ cr->rectangle(0, 0, w, h);
+ cr->rectangle(w, h, w, h);
+ cr->fill();
+
+ auto pattern = Cairo::SurfacePattern::create(surface);
+ pattern->set_extend(Cairo::EXTEND_REPEAT);
+ pattern->set_filter(Cairo::FILTER_NEAREST);
+
+ return pattern;
+ }
+}
+
+} // namespace
+
+// Paint the background and pages using Cairo into the given fragment.
+void Graphics::paint_background(Fragment const &fragment, PageInfo const &pi, uint32_t page, uint32_t desk, Cairo::RefPtr<Cairo::Context> const &cr)
+{
+ cr->save();
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->rectangle(0, 0, fragment.rect.width(), fragment.rect.height());
+ cr->clip();
+
+ if (desk == page || check_single_page(fragment, pi)) {
+ // Desk and page are the same, or a single page fills the whole screen; just clear the fragment to page.
+ cr->set_source(rgba_to_pattern(page));
+ cr->paint();
+ } else {
+ // Paint the background to the complement of the pages. (Slightly overpaints when pages overlap.)
+ cr->save();
+ cr->set_source(rgba_to_pattern(desk));
+ cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD);
+ cr->rectangle(0, 0, fragment.rect.width(), fragment.rect.height());
+ cr->translate(-fragment.rect.left(), -fragment.rect.top());
+ cr->transform(geom_to_cairo(fragment.affine));
+ for (auto &rect : pi.pages) {
+ cr->rectangle(rect.left(), rect.top(), rect.width(), rect.height());
+ }
+ cr->fill();
+ cr->restore();
+
+ // Paint the pages.
+ cr->save();
+ cr->set_source(rgba_to_pattern(page));
+ cr->translate(-fragment.rect.left(), -fragment.rect.top());
+ cr->transform(geom_to_cairo(fragment.affine));
+ for (auto &rect : pi.pages) {
+ cr->rectangle(rect.left(), rect.top(), rect.width(), rect.height());
+ }
+ cr->fill();
+ cr->restore();
+ }
+
+ cr->restore();
+}
+
+std::pair<Geom::IntRect, Geom::IntRect> Graphics::calc_splitview_cliprects(Geom::IntPoint const &size, Geom::Point const &split_frac, SplitDirection split_direction)
+{
+ auto window = Geom::IntRect({0, 0}, size);
+
+ auto content = window;
+ auto outline = window;
+ auto split = [&] (Geom::Dim2 dim, Geom::IntRect &lo, Geom::IntRect &hi) {
+ int s = std::round(split_frac[dim] * size[dim]);
+ lo[dim].setMax(s);
+ hi[dim].setMin(s);
+ };
+
+ switch (split_direction) {
+ case Inkscape::SplitDirection::NORTH: split(Geom::Y, content, outline); break;
+ case Inkscape::SplitDirection::EAST: split(Geom::X, outline, content); break;
+ case Inkscape::SplitDirection::SOUTH: split(Geom::Y, outline, content); break;
+ case Inkscape::SplitDirection::WEST: split(Geom::X, content, outline); break;
+ default: assert(false); break;
+ }
+
+ return std::make_pair(content, outline);
+}
+
+void Graphics::paint_splitview_controller(Geom::IntPoint const &size, Geom::Point const &split_frac, SplitDirection split_direction, SplitDirection hover_direction, Cairo::RefPtr<Cairo::Context> const &cr)
+{
+ auto split_position = (split_frac * size).round();
+
+ // Add dividing line.
+ cr->set_source_rgb(0.0, 0.0, 0.0);
+ cr->set_line_width(1.0);
+ if (split_direction == Inkscape::SplitDirection::EAST ||
+ split_direction == Inkscape::SplitDirection::WEST) {
+ cr->move_to(split_position.x() + 0.5, 0.0 );
+ cr->line_to(split_position.x() + 0.5, size.y());
+ cr->stroke();
+ } else {
+ cr->move_to(0.0 , split_position.y() + 0.5);
+ cr->line_to(size.x(), split_position.y() + 0.5);
+ cr->stroke();
+ }
+
+ // Add controller image.
+ double a = hover_direction == Inkscape::SplitDirection::NONE ? 0.5 : 1.0;
+ cr->set_source_rgba(0.2, 0.2, 0.2, a);
+ cr->arc(split_position.x(), split_position.y(), 20, 0, 2 * M_PI);
+ cr->fill();
+
+ for (int i = 0; i < 4; i++) {
+ // The four direction triangles.
+ cr->save();
+
+ // Position triangle.
+ cr->translate(split_position.x(), split_position.y());
+ cr->rotate((i + 2) * M_PI / 2);
+
+ // Draw triangle.
+ cr->move_to(-5, 8);
+ cr->line_to( 0, 18);
+ cr->line_to( 5, 8);
+ cr->close_path();
+
+ double b = (int)hover_direction == (i + 1) ? 0.9 : 0.7;
+ cr->set_source_rgba(b, b, b, a);
+ cr->fill();
+
+ cr->restore();
+ }
+}
+
+bool Graphics::check_single_page(Fragment const &view, PageInfo const &pi)
+{
+ auto pl = Geom::Parallelogram(view.rect) * view.affine.inverse();
+ return std::any_of(pi.pages.begin(), pi.pages.end(), [&] (auto &rect) {
+ return Geom::Parallelogram(rect).contains(pl);
+ });
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
diff --git a/src/ui/widget/canvas/graphics.h b/src/ui/widget/canvas/graphics.h
new file mode 100644
index 0000000..0e7767d
--- /dev/null
+++ b/src/ui/widget/canvas/graphics.h
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Display backend interface.
+ * Copyright (C) 2022 PBS <pbs3141@gmail.com>
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_GRAPHICS_H
+#define INKSCAPE_UI_WIDGET_CANVAS_GRAPHICS_H
+
+#include <memory>
+#include <cstdint>
+#include <boost/noncopyable.hpp>
+#include <2geom/rect.h>
+#include <cairomm/cairomm.h>
+#include "display/rendermode.h"
+#include "fragment.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+class Stores;
+class Prefs;
+
+struct PageInfo
+{
+ std::vector<Geom::Rect> pages;
+};
+
+class Graphics
+{
+public:
+ // Creation/destruction.
+ static std::unique_ptr<Graphics> create_gl (Prefs const &prefs, Stores const &stores, PageInfo const &pi);
+ static std::unique_ptr<Graphics> create_cairo(Prefs const &prefs, Stores const &stores, PageInfo const &pi);
+ virtual ~Graphics() = default;
+
+ // State updating.
+ virtual void set_scale_factor(int) = 0; ///< Set the HiDPI scale factor.
+ virtual void set_outlines_enabled(bool) = 0; ///< Whether to maintain a second layer of outline content.
+ virtual void set_background_in_stores(bool) = 0; ///< Whether to assume the first layer is drawn on top of background or transparency.
+ virtual void set_colours(uint32_t page, uint32_t desk, uint32_t border) = 0; ///< Set colours for background/page shadow drawing.
+
+ // Store manipulation.
+ virtual void recreate_store(Geom::IntPoint const &dims) = 0; ///< Set the store to a surface of the given size, of unspecified contents.
+ virtual void shift_store(Fragment const &dest) = 0; ///< Called when the store fragment shifts position to \a dest.
+ virtual void swap_stores() = 0; ///< Exchange the store and snapshot surfaces.
+ virtual void fast_snapshot_combine() = 0; ///< Paste the store onto the snapshot.
+ virtual void snapshot_combine(Fragment const &dest) = 0; ///< Paste the snapshot followed by the store onto a new snapshot at \a dest.
+ virtual void invalidate_snapshot() = 0; ///< Indicate that the content in the snapshot store is not going to be used again.
+
+ // Misc.
+ virtual bool is_opengl() const = 0; ///< Whether this is an OpenGL backend.
+ virtual void invalidated_glstate() = 0; ///< Tells the Graphics to no longer rely on any OpenGL state it had set up.
+
+ // Tile drawing.
+ /// Return a surface for drawing on. If nogl is true, no GL commands are issued, as is a requirement off-main-thread. All such surfaces must be
+ /// returned by passing them either to draw_tile() or junk_tile_surface().
+ virtual Cairo::RefPtr<Cairo::ImageSurface> request_tile_surface(Geom::IntRect const &rect, bool nogl) = 0;
+ /// Commit the contents of a surface previously issued by request_tile_surface() to the canvas. In outline mode, a second surface must be passed
+ /// containing the outline content, otherwise it should be null.
+ virtual void draw_tile(Fragment const &fragment, Cairo::RefPtr<Cairo::ImageSurface> surface, Cairo::RefPtr<Cairo::ImageSurface> outline_surface) = 0;
+ /// Get rid of a surface previously issued by request_tile_surface() without committing it to the canvas. Usually useful only to dispose of
+ /// surfaces which have gone into an error state while rendering, which is irreversible, and therefore we can't do anything useful with them.
+ virtual void junk_tile_surface(Cairo::RefPtr<Cairo::ImageSurface> surface) = 0;
+
+ // Widget painting.
+ struct PaintArgs
+ {
+ std::optional<Geom::IntPoint> mouse;
+ RenderMode render_mode;
+ SplitMode splitmode;
+ Geom::Point splitfrac;
+ SplitDirection splitdir;
+ SplitDirection hoverdir;
+ double yaxisdir;
+ };
+ virtual void paint_widget(Fragment const &view, PaintArgs const &args, Cairo::RefPtr<Cairo::Context> const &cr) = 0;
+
+ // Static functions providing common functionality.
+ static bool check_single_page(Fragment const &view, PageInfo const &pi);
+ static std::pair<Geom::IntRect, Geom::IntRect> calc_splitview_cliprects(Geom::IntPoint const &size, Geom::Point const &splitfrac, SplitDirection splitdir);
+ static void paint_splitview_controller(Geom::IntPoint const &size, Geom::Point const &splitfrac, SplitDirection splitdir, SplitDirection hoverdir, Cairo::RefPtr<Cairo::Context> const &cr);
+ static void paint_background(Fragment const &fragment, PageInfo const &pi, uint32_t page, uint32_t desk, Cairo::RefPtr<Cairo::Context> const &cr);
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_GRAPHICS_H
diff --git a/src/ui/widget/canvas/pixelstreamer.cpp b/src/ui/widget/canvas/pixelstreamer.cpp
new file mode 100644
index 0000000..74d557b
--- /dev/null
+++ b/src/ui/widget/canvas/pixelstreamer.cpp
@@ -0,0 +1,501 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <cassert>
+#include <cmath>
+#include <vector>
+#include <epoxy/gl.h>
+#include "pixelstreamer.h"
+#include "helper/mathfns.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+namespace {
+
+cairo_user_data_key_t constexpr key{};
+
+class PersistentPixelStreamer : public PixelStreamer
+{
+ static int constexpr bufsize = 0x1000000; // 16 MiB
+
+ struct Buffer
+ {
+ GLuint pbo; // Pixel buffer object.
+ unsigned char *data; // The pointer to the mapped region.
+ int off; // Offset of the unused region, in bytes. Always a multiple of 64.
+ int refs; // How many mappings are currently using this buffer.
+ GLsync sync; // Sync object for telling us when the GPU has finished reading from this buffer.
+ bool ready; // Whether this buffer is ready for re-use.
+
+ void create()
+ {
+ glGenBuffers(1, &pbo);
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
+ glBufferStorage(GL_PIXEL_UNPACK_BUFFER, bufsize, nullptr, GL_MAP_READ_BIT | GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT);
+ data = (unsigned char*)glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, bufsize, GL_MAP_READ_BIT | GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_FLUSH_EXPLICIT_BIT);
+ off = 0;
+ refs = 0;
+ }
+
+ void destroy()
+ {
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
+ glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
+ glDeleteBuffers(1, &pbo);
+ }
+
+ // Advance a buffer in state 3 or 4 as far as possible towards state 5.
+ void advance()
+ {
+ if (!sync) {
+ sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+ } else {
+ auto ret = glClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, 0);
+ if (ret == GL_CONDITION_SATISFIED || ret == GL_ALREADY_SIGNALED) {
+ glDeleteSync(sync);
+ ready = true;
+ }
+ }
+ }
+ };
+ std::vector<Buffer> buffers;
+
+ int current_buffer;
+
+ struct Mapping
+ {
+ bool used; // Whether the mapping is in use, or on the freelist.
+ int buf; // The buffer the mapping is using.
+ int off; // Offset of the mapped region.
+ int size; // Size of the mapped region.
+ int width, height, stride; // Image properties.
+ };
+ std::vector<Mapping> mappings;
+
+ /*
+ * A Buffer cycles through the following five states:
+ *
+ * 1. Current --> We are currently filling this buffer up with allocations.
+ * 2. Not current, refs > 0 --> Finished the above, but may still be writing into it and issuing GL commands from it.
+ * 3. Not current, refs == 0, !ready, !sync --> Finished the above, but GL may be reading from it. We have yet to create its sync object.
+ * 4. Not current, refs == 0, !ready, sync --> We have now created its sync object, but it has not been signalled yet.
+ * 5. Not current, refs == 0, ready --> The sync object has been signalled and deleted.
+ *
+ * Only one Buffer is Current at any given time, and is marked by the current_buffer variable.
+ */
+
+public:
+ PersistentPixelStreamer()
+ {
+ // Create a single initial buffer and make it the current buffer.
+ buffers.emplace_back();
+ buffers.back().create();
+ current_buffer = 0;
+ }
+
+ Method get_method() const override { return Method::Persistent; }
+
+ Cairo::RefPtr<Cairo::ImageSurface> request(Geom::IntPoint const &dimensions, bool nogl) override
+ {
+ // Calculate image properties required by cairo.
+ int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, dimensions.x());
+ int size = stride * dimensions.y();
+ int sizeup = Util::roundup(size, 64);
+ assert(sizeup < bufsize);
+
+ // Attempt to advance buffers in states 3 or 4 towards 5, if allowed.
+ if (!nogl) {
+ for (int i = 0; i < buffers.size(); i++) {
+ if (i != current_buffer && buffers[i].refs == 0 && !buffers[i].ready) {
+ buffers[i].advance();
+ }
+ }
+ }
+ // Continue using the current buffer if possible.
+ if (buffers[current_buffer].off + sizeup <= bufsize) {
+ goto chosen_buffer;
+ }
+ // Otherwise, the current buffer has filled up. After this point, the current buffer will change.
+ // Therefore, handle the state change of the current buffer out of the Current state. Usually that
+ // means doing nothing because the transition to state 2 is automatic. But if refs == 0 already,
+ // then we need to transition into state 3 by setting ready = false. If we're allowed to use GL,
+ // then we can additionally transition into state 4 by creating the sync object.
+ if (buffers[current_buffer].refs == 0) {
+ buffers[current_buffer].ready = false;
+ buffers[current_buffer].sync = nogl ? nullptr : glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+ }
+ // Attempt to re-use a old buffer that has reached state 5.
+ for (int i = 0; i < buffers.size(); i++) {
+ if (i != current_buffer && buffers[i].refs == 0 && buffers[i].ready) {
+ // Found an unused buffer. Re-use it. (Move to state 1.)
+ buffers[i].off = 0;
+ current_buffer = i;
+ goto chosen_buffer;
+ }
+ }
+ // Otherwise, there are no available buffers. Create and use a new one. That requires GL, so fail if not allowed.
+ if (nogl) {
+ return {};
+ }
+ buffers.emplace_back();
+ buffers.back().create();
+ current_buffer = buffers.size() - 1;
+ chosen_buffer:
+ // Finished changing the current buffer.
+ auto &b = buffers[current_buffer];
+
+ // Choose/create the mapping to use.
+ auto choose_mapping = [&, this] {
+ for (int i = 0; i < mappings.size(); i++) {
+ if (!mappings[i].used) {
+ // Found unused mapping.
+ return i;
+ }
+ }
+ // No free mapping; create one.
+ mappings.emplace_back();
+ return (int)mappings.size() - 1;
+ };
+
+ auto mapping = choose_mapping();
+ auto &m = mappings[mapping];
+
+ // Set up the mapping bookkeeping.
+ m = {true, current_buffer, b.off, size, dimensions.x(), dimensions.y(), stride};
+ b.off += sizeup;
+ b.refs++;
+
+ // Create the image surface.
+ auto surface = Cairo::ImageSurface::create(b.data + m.off, Cairo::FORMAT_ARGB32, dimensions.x(), dimensions.y(), stride);
+
+ // Attach the mapping handle as user data.
+ cairo_surface_set_user_data(surface->cobj(), &key, (void*)(uintptr_t)mapping, nullptr);
+
+ return surface;
+ }
+
+ void finish(Cairo::RefPtr<Cairo::ImageSurface> surface, bool junk) override
+ {
+ // Extract the mapping handle from the surface's user data.
+ auto mapping = (int)(uintptr_t)cairo_surface_get_user_data(surface->cobj(), &key);
+
+ // Flush all changes from the image surface to the buffer, and delete it.
+ surface.clear();
+
+ auto &m = mappings[mapping];
+ auto &b = buffers[m.buf];
+
+ // Flush the mapped subregion.
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, b.pbo);
+ glFlushMappedBufferRange(GL_PIXEL_UNPACK_BUFFER, m.off, m.size);
+
+ // Tear down the mapping bookkeeping. (if this causes transition 2 --> 3, it is handled below.)
+ m.used = false;
+ b.refs--;
+
+ // Upload to the texture from the mapped subregion.
+ if (!junk) {
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, m.stride / 4);
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m.width, m.height, GL_BGRA, GL_UNSIGNED_BYTE, (void*)(uintptr_t)m.off);
+ }
+
+ // If the buffer is due for recycling, issue a sync command so that we can recycle it when it's ready. (Handle transition 2 --> 4.)
+ if (m.buf != current_buffer && b.refs == 0) {
+ b.ready = false;
+ b.sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+ }
+
+ // Check other buffers to see if they're ready for recycling. (Advance from 3/4 towards 5.)
+ for (int i = 0; i < buffers.size(); i++) {
+ if (i != current_buffer && i != m.buf && buffers[i].refs == 0 && !buffers[i].ready) {
+ buffers[i].advance();
+ }
+ }
+ }
+
+ ~PersistentPixelStreamer() override
+ {
+ // Delete any sync objects. (For buffers in state 4.)
+ for (int i = 0; i < buffers.size(); i++) {
+ if (i != current_buffer && buffers[i].refs == 0 && !buffers[i].ready && buffers[i].sync) {
+ glDeleteSync(buffers[i].sync);
+ }
+ }
+
+ // Wait for GL to finish reading out of all the buffers.
+ glFinish();
+
+ // Deallocate the buffers on the GL side.
+ for (auto &b : buffers) {
+ b.destroy();
+ }
+ }
+};
+
+class AsynchronousPixelStreamer : public PixelStreamer
+{
+ static int constexpr minbufsize = 0x4000; // 16 KiB
+ static int constexpr expire_timeout = 10000;
+
+ static int constexpr size_to_bucket(int size) { return Util::floorlog2((size - 1) / minbufsize) + 1; }
+ static int constexpr bucket_maxsize(int b) { return minbufsize * (1 << b); }
+
+ struct Buffer
+ {
+ GLuint pbo;
+ unsigned char *data;
+
+ void create(int size)
+ {
+ glGenBuffers(1, &pbo);
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
+ glBufferData(GL_PIXEL_UNPACK_BUFFER, size, nullptr, GL_STREAM_DRAW);
+ data = (unsigned char*)glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, size, GL_MAP_READ_BIT | GL_MAP_WRITE_BIT);
+ }
+
+ void destroy()
+ {
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
+ glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
+ glDeleteBuffers(1, &pbo);
+ }
+ };
+
+ struct Bucket
+ {
+ std::vector<Buffer> spares;
+ int used = 0;
+ int high_use_count = 0;
+ };
+ std::vector<Bucket> buckets;
+
+ struct Mapping
+ {
+ bool used;
+ Buffer buf;
+ int bucket;
+ int width, height, stride;
+ };
+ std::vector<Mapping> mappings;
+
+ int expire_timer = 0;
+
+public:
+ Method get_method() const override { return Method::Asynchronous; }
+
+ Cairo::RefPtr<Cairo::ImageSurface> request(Geom::IntPoint const &dimensions, bool nogl) override
+ {
+ // Calculate image properties required by cairo.
+ int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, dimensions.x());
+ int size = stride * dimensions.y();
+
+ // Find the bucket that size falls into.
+ int bucket = size_to_bucket(size);
+ if (bucket >= buckets.size()) {
+ buckets.resize(bucket + 1);
+ }
+ auto &b = buckets[bucket];
+
+ // Find/create a buffer of the appropriate size.
+ Buffer buf;
+ if (!b.spares.empty()) {
+ // If the bucket has any spare mapped buffers, then use one of them.
+ buf = std::move(b.spares.back());
+ b.spares.pop_back();
+ } else if (!nogl) {
+ // Otherwise, we have to use OpenGL to create and map a new buffer.
+ buf.create(bucket_maxsize(bucket));
+ } else {
+ // If we're not allowed to issue GL commands, then that is a failure.
+ return {};
+ }
+
+ // Record the new use count of the bucket.
+ b.used++;
+ if (b.used > b.high_use_count) {
+ // If the use count has gone above the high-water mark, record it and reset the timer for when to clean up excess spares.
+ b.high_use_count = b.used;
+ expire_timer = 0;
+ }
+
+ auto choose_mapping = [&, this] {
+ for (int i = 0; i < mappings.size(); i++) {
+ if (!mappings[i].used) {
+ return i;
+ }
+ }
+ mappings.emplace_back();
+ return (int)mappings.size() - 1;
+ };
+
+ auto mapping = choose_mapping();
+ auto &m = mappings[mapping];
+
+ m.used = true;
+ m.buf = std::move(buf);
+ m.bucket = bucket;
+ m.width = dimensions.x();
+ m.height = dimensions.y();
+ m.stride = stride;
+
+ auto surface = Cairo::ImageSurface::create(m.buf.data, Cairo::FORMAT_ARGB32, m.width, m.height, m.stride);
+ cairo_surface_set_user_data(surface->cobj(), &key, (void*)(uintptr_t)mapping, nullptr);
+ return surface;
+ }
+
+ void finish(Cairo::RefPtr<Cairo::ImageSurface> surface, bool junk) override
+ {
+ auto mapping = (int)(uintptr_t)cairo_surface_get_user_data(surface->cobj(), &key);
+ surface.clear();
+
+ auto &m = mappings[mapping];
+ auto &b = buckets[m.bucket];
+
+ // Unmap the buffer.
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, m.buf.pbo);
+ glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
+
+ // Upload the buffer to the texture.
+ if (!junk) {
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, m.stride / 4);
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m.width, m.height, GL_BGRA, GL_UNSIGNED_BYTE, nullptr);
+ }
+
+ // Mark the mapping slot as unused.
+ m.used = false;
+
+ // Orphan and re-map the buffer.
+ auto size = bucket_maxsize(m.bucket);
+ glBufferData(GL_PIXEL_UNPACK_BUFFER, size, nullptr, GL_STREAM_DRAW);
+ m.buf.data = (unsigned char*)glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, size, GL_MAP_READ_BIT | GL_MAP_WRITE_BIT);
+
+ // Put the buffer back in its corresponding bucket's pile of spares.
+ b.spares.emplace_back(std::move(m.buf));
+ b.used--;
+
+ // If the expiration timeout has been reached, get rid of excess spares from all buckets, and reset the high use counts.
+ expire_timer++;
+ if (expire_timer >= expire_timeout) {
+ expire_timer = 0;
+
+ for (auto &b : buckets) {
+ int max_spares = b.high_use_count - b.used;
+ assert(max_spares >= 0);
+ if (b.spares.size() > max_spares) {
+ for (int i = max_spares; i < b.spares.size(); i++) {
+ b.spares[i].destroy();
+ }
+ b.spares.resize(max_spares);
+ }
+ b.high_use_count = b.used;
+ }
+ }
+ }
+
+ ~AsynchronousPixelStreamer() override
+ {
+ // Unmap and delete all spare buffers. (They are not being used.)
+ for (auto &b : buckets) {
+ for (auto &buf : b.spares) {
+ buf.destroy();
+ }
+ }
+ }
+};
+
+class SynchronousPixelStreamer : public PixelStreamer
+{
+ struct Mapping
+ {
+ bool used;
+ std::vector<unsigned char> data;
+ int size, width, height, stride;
+ };
+ std::vector<Mapping> mappings;
+
+public:
+ Method get_method() const override { return Method::Synchronous; }
+
+ Cairo::RefPtr<Cairo::ImageSurface> request(Geom::IntPoint const &dimensions, bool) override
+ {
+ auto choose_mapping = [&, this] {
+ for (int i = 0; i < mappings.size(); i++) {
+ if (!mappings[i].used) {
+ return i;
+ }
+ }
+ mappings.emplace_back();
+ return (int)mappings.size() - 1;
+ };
+
+ auto mapping = choose_mapping();
+ auto &m = mappings[mapping];
+
+ m.used = true;
+ m.width = dimensions.x();
+ m.height = dimensions.y();
+ m.stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, m.width);
+ m.size = m.stride * m.height;
+ m.data.resize(m.size);
+
+ auto surface = Cairo::ImageSurface::create(&m.data[0], Cairo::FORMAT_ARGB32, m.width, m.height, m.stride);
+ cairo_surface_set_user_data(surface->cobj(), &key, (void*)(uintptr_t)mapping, nullptr);
+ return surface;
+ }
+
+ void finish(Cairo::RefPtr<Cairo::ImageSurface> surface, bool junk) override
+ {
+ auto mapping = (int)(uintptr_t)cairo_surface_get_user_data(surface->cobj(), &key);
+ surface.clear();
+
+ auto &m = mappings[mapping];
+
+ if (!junk) {
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, m.stride / 4);
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m.width, m.height, GL_BGRA, GL_UNSIGNED_BYTE, &m.data[0]);
+ }
+
+ m.used = false;
+ m.data.clear();
+ }
+};
+
+} // namespace
+
+std::unique_ptr<PixelStreamer> PixelStreamer::create_supported(Method method)
+{
+ int ver = epoxy_gl_version();
+
+ if (method <= Method::Asynchronous) {
+ if (ver >= 30 || epoxy_has_gl_extension("GL_ARB_map_buffer_range")) {
+ if (method <= Method::Persistent) {
+ if (ver >= 44 || (epoxy_has_gl_extension("GL_ARB_buffer_storage") &&
+ epoxy_has_gl_extension("GL_ARB_texture_storage") &&
+ epoxy_has_gl_extension("GL_ARB_SYNC")))
+ {
+ return std::make_unique<PersistentPixelStreamer>();
+ } else if (method != Method::Auto) {
+ std::cerr << "Persistent PixelStreamer not available" << std::endl;
+ }
+ }
+ return std::make_unique<AsynchronousPixelStreamer>();
+ } else if (method != Method::Auto) {
+ std::cerr << "Asynchronous PixelStreamer not available" << std::endl;
+ }
+ }
+ return std::make_unique<SynchronousPixelStreamer>();
+}
+
+} // 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 :
diff --git a/src/ui/widget/canvas/pixelstreamer.h b/src/ui/widget/canvas/pixelstreamer.h
new file mode 100644
index 0000000..bcd3684
--- /dev/null
+++ b/src/ui/widget/canvas/pixelstreamer.h
@@ -0,0 +1,75 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A class hierarchy implementing various ways of streaming pixel buffers to the GPU.
+ * Copyright (C) 2022 PBS <pbs3141@gmail.com>
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_PIXELSTREAMER_H
+#define INKSCAPE_UI_WIDGET_CANVAS_PIXELSTREAMER_H
+
+#include <memory>
+#include <2geom/int-point.h>
+#include <cairomm/refptr.h>
+#include <cairomm/surface.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+// A class for turning Cairo image surfaces into OpenGL textures.
+class PixelStreamer
+{
+public:
+ virtual ~PixelStreamer() = default;
+
+ // Method for streaming pixels to the GPU.
+ enum class Method
+ {
+ Auto, // Use the best option available at runtime.
+ Persistent, // Persistent buffer mapping. (Best, requires OpenGL 4.4.)
+ Asynchronous, // Ordinary buffer mapping. (Almost as good, requires OpenGL 3.0.)
+ Synchronous // Synchronous texture uploads. (Worst but still tolerable, requires OpenGL 1.1.)
+ };
+
+ // Create a PixelStreamer using a choice of method specified at runtime, falling back if unsupported.
+ static std::unique_ptr<PixelStreamer> create_supported(Method method);
+
+ // Return the method in use.
+ virtual Method get_method() const = 0;
+
+ /**
+ * Request a drawing surface of the given dimensions. If nogl is true, no GL commands will be issued,
+ * but the request may fail. An effort is made to keep such failures to a minimum.
+ *
+ * The surface must be returned to the PixelStreamer by calling finish(), in order to deallocate
+ * GL resourecs.
+ */
+ virtual Cairo::RefPtr<Cairo::ImageSurface> request(Geom::IntPoint const &dimensions, bool nogl = false) = 0;
+
+ /**
+ * Give back a drawing surface produced by request(), uploading the contents to the currently bound texture.
+ * The texture must be at least as big as the surface.
+ *
+ * If junk is true, then the surface will be junked instead, meaning nothing will be done with the contents,
+ * and its GL resources will simply be deallocated.
+ */
+ virtual void finish(Cairo::RefPtr<Cairo::ImageSurface> surface, bool junk = false) = 0;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_PIXELSTREAMER_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/canvas/prefs.h b/src/ui/widget/canvas/prefs.h
new file mode 100644
index 0000000..363fb6d
--- /dev/null
+++ b/src/ui/widget/canvas/prefs.h
@@ -0,0 +1,102 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_PREFS_H
+#define INKSCAPE_UI_WIDGET_CANVAS_PREFS_H
+
+#include "preferences.h"
+
+namespace Inkscape::UI::Widget {
+
+class Prefs
+{
+public:
+ Prefs()
+ {
+ devmode.action = [this] { set_devmode(devmode); };
+ devmode.action();
+ }
+
+ // Main preferences
+ Pref<int> xray_radius = { "/options/rendering/xray-radius", 100, 1, 1500 };
+ Pref<int> outline_overlay_opacity = { "/options/rendering/outline-overlay-opacity", 50, 0, 100 };
+ Pref<int> update_strategy = { "/options/rendering/update_strategy", 3, 1, 3 };
+ Pref<bool> request_opengl = { "/options/rendering/request_opengl" };
+ Pref<int> grabsize = { "/options/grabsize/value", 3, 1, 15 };
+ Pref<int> numthreads = { "/options/threading/numthreads", 0, 1, 256 };
+
+ // Colour management
+ Pref<bool> from_display = { "/options/displayprofile/from_display" };
+ Pref<void> displayprofile = { "/options/displayprofile" };
+ Pref<void> softproof = { "/options/softproof" };
+
+ // Auto-scrolling
+ Pref<int> autoscrolldistance = { "/options/autoscrolldistance/value", 0, -1000, 10000 };
+ Pref<double> autoscrollspeed = { "/options/autoscrollspeed/value", 1.0, 0.0, 10.0 };
+
+ // Devmode preferences
+ Pref<int> tile_size = { "/options/rendering/tile_size", 300, 1, 10000 };
+ Pref<int> render_time_limit = { "/options/rendering/render_time_limit", 80, 1, 5000 };
+ Pref<bool> block_updates = { "/options/rendering/block_updates", true };
+ Pref<int> pixelstreamer_method = { "/options/rendering/pixelstreamer_method", 1, 1, 4 };
+ Pref<int> padding = { "/options/rendering/padding", 350, 0, 1000 };
+ Pref<int> prerender = { "/options/rendering/prerender", 100, 0, 1000 };
+ Pref<int> preempt = { "/options/rendering/preempt", 250, 0, 1000 };
+ Pref<int> coarsener_min_size = { "/options/rendering/coarsener_min_size", 200, 0, 1000 };
+ Pref<int> coarsener_glue_size = { "/options/rendering/coarsener_glue_size", 80, 0, 1000 };
+ Pref<double> coarsener_min_fullness = { "/options/rendering/coarsener_min_fullness", 0.3, 0.0, 1.0 };
+
+ // Debug switches
+ Pref<bool> debug_framecheck = { "/options/rendering/debug_framecheck" };
+ Pref<bool> debug_logging = { "/options/rendering/debug_logging" };
+ Pref<bool> debug_delay_redraw = { "/options/rendering/debug_delay_redraw" };
+ Pref<int> debug_delay_redraw_time = { "/options/rendering/debug_delay_redraw_time", 50, 0, 1000000 };
+ Pref<bool> debug_show_redraw = { "/options/rendering/debug_show_redraw" };
+ Pref<bool> debug_show_unclean = { "/options/rendering/debug_show_unclean" }; // no longer implemented
+ Pref<bool> debug_show_snapshot = { "/options/rendering/debug_show_snapshot" };
+ Pref<bool> debug_show_clean = { "/options/rendering/debug_show_clean" }; // no longer implemented
+ Pref<bool> debug_disable_redraw = { "/options/rendering/debug_disable_redraw" };
+ Pref<bool> debug_sticky_decoupled = { "/options/rendering/debug_sticky_decoupled" };
+ Pref<bool> debug_animate = { "/options/rendering/debug_animate" };
+
+private:
+ // Developer mode
+ Pref<bool> devmode = { "/options/rendering/devmode" };
+
+ void set_devmode(bool on)
+ {
+ tile_size.set_enabled(on);
+ render_time_limit.set_enabled(on);
+ pixelstreamer_method.set_enabled(on);
+ padding.set_enabled(on);
+ prerender.set_enabled(on);
+ preempt.set_enabled(on);
+ coarsener_min_size.set_enabled(on);
+ coarsener_glue_size.set_enabled(on);
+ coarsener_min_fullness.set_enabled(on);
+ debug_framecheck.set_enabled(on);
+ debug_logging.set_enabled(on);
+ debug_delay_redraw.set_enabled(on);
+ debug_delay_redraw_time.set_enabled(on);
+ debug_show_redraw.set_enabled(on);
+ debug_show_unclean.set_enabled(on);
+ debug_show_snapshot.set_enabled(on);
+ debug_show_clean.set_enabled(on);
+ debug_disable_redraw.set_enabled(on);
+ debug_sticky_decoupled.set_enabled(on);
+ debug_animate.set_enabled(on);
+ }
+};
+
+} // namespace Inkscape::UI::Widget
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_PREFS_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/canvas/stores.cpp b/src/ui/widget/canvas/stores.cpp
new file mode 100644
index 0000000..70327f5
--- /dev/null
+++ b/src/ui/widget/canvas/stores.cpp
@@ -0,0 +1,371 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <array>
+#include <cmath>
+#include <2geom/transforms.h>
+#include <2geom/parallelogram.h>
+#include <2geom/point.h>
+#include "helper/geom.h"
+#include "ui/util.h"
+#include "stores.h"
+#include "prefs.h"
+#include "fragment.h"
+#include "graphics.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+namespace {
+
+// Determine whether an affine transformation approximately maps the unit square [0, 1]^2 to itself.
+bool preserves_unitsquare(Geom::Affine const &affine)
+{
+ return approx_dihedral(Geom::Translate(0.5, 0.5) * affine * Geom::Translate(-0.5, -0.5));
+}
+
+// Apply an affine transformation to a region, then return a strictly smaller region approximating it, made from chunks of size roughly d.
+// To reduce computation, only the intersection of the result with bounds will be valid.
+auto region_affine_approxinwards(Cairo::RefPtr<Cairo::Region> const &reg, Geom::Affine const &affine, Geom::IntRect const &bounds, int d = 200)
+{
+ // Trivial empty case.
+ if (reg->empty()) return Cairo::Region::create();
+
+ // Trivial identity case.
+ if (affine.isIdentity(0.001)) return reg->copy();
+
+ // Fast-path for rectilinear transformations.
+ if (affine.withoutTranslation().isScale(0.001)) {
+ auto regdst = Cairo::Region::create();
+
+ auto transform = [&] (const Geom::IntPoint &p) {
+ return (Geom::Point(p) * affine).round();
+ };
+
+ for (int i = 0; i < reg->get_num_rectangles(); i++) {
+ auto rect = cairo_to_geom(reg->get_rectangle(i));
+ regdst->do_union(geom_to_cairo(Geom::IntRect(transform(rect.min()), transform(rect.max()))));
+ }
+
+ return regdst;
+ }
+
+ // General case.
+ auto ext = cairo_to_geom(reg->get_extents());
+ auto rectdst = ((Geom::Parallelogram(ext) * affine).bounds().roundOutwards() & bounds).regularized();
+ if (!rectdst) return Cairo::Region::create();
+ auto rectsrc = (Geom::Parallelogram(*rectdst) * affine.inverse()).bounds().roundOutwards();
+
+ auto regdst = Cairo::Region::create(geom_to_cairo(*rectdst));
+ auto regsrc = Cairo::Region::create(geom_to_cairo(rectsrc));
+ regsrc->subtract(reg);
+
+ double fx = min(absolute(Geom::Point(1.0, 0.0) * affine.withoutTranslation()));
+ double fy = min(absolute(Geom::Point(0.0, 1.0) * affine.withoutTranslation()));
+
+ for (int i = 0; i < regsrc->get_num_rectangles(); i++)
+ {
+ auto rect = cairo_to_geom(regsrc->get_rectangle(i));
+ int nx = std::ceil(rect.width() * fx / d);
+ int ny = std::ceil(rect.height() * fy / d);
+ auto pt = [&] (int x, int y) {
+ return rect.min() + (rect.dimensions() * Geom::IntPoint(x, y)) / Geom::IntPoint(nx, ny);
+ };
+ for (int x = 0; x < nx; x++) {
+ for (int y = 0; y < ny; y++) {
+ auto r = Geom::IntRect(pt(x, y), pt(x + 1, y + 1));
+ auto r2 = (Geom::Parallelogram(r) * affine).bounds().roundOutwards();
+ regdst->subtract(geom_to_cairo(r2));
+ }
+ }
+ }
+
+ return regdst;
+}
+
+} // namespace
+
+Geom::IntRect Stores::centered(Fragment const &view) const
+{
+ // Return the visible region of the view, plus the prerender and padding margins.
+ return expandedBy(view.rect, _prefs.prerender + _prefs.padding);
+}
+
+void Stores::recreate_store(Fragment const &view)
+{
+ // Recreate the store at the view's affine.
+ _store.affine = view.affine;
+ _store.rect = centered(view);
+ _store.drawn = Cairo::Region::create();
+ // Tell the graphics to create a blank new store.
+ _graphics->recreate_store(_store.rect.dimensions());
+}
+
+void Stores::shift_store(Fragment const &view)
+{
+ // Create a new fragment centred on the viewport.
+ auto rect = centered(view);
+ // Tell the graphics to copy the drawn part of the old store to the new store.
+ _graphics->shift_store(Fragment{ _store.affine, rect });
+ // Set the shifted store as the new store.
+ _store.rect = rect;
+ // Clip the drawn region to the new store.
+ _store.drawn->intersect(geom_to_cairo(_store.rect));
+};
+
+void Stores::take_snapshot(Fragment const &view)
+{
+ // Copy the store to the snapshot, leaving us temporarily in an invalid state.
+ _snapshot = std::move(_store);
+ // Tell the graphics to do the same, except swapping them so we can re-use the old snapshot store.
+ _graphics->swap_stores();
+ // Reset the store.
+ recreate_store(view);
+ // Transform the snapshot's drawn region to the new store's affine.
+ _snapshot.drawn = shrink_region(region_affine_approxinwards(_snapshot.drawn, _snapshot.affine.inverse() * _store.affine, _store.rect), 4, -2);
+}
+
+void Stores::snapshot_combine(Fragment const &view)
+{
+ // Add the drawn region to the snapshot drawn region (they both exist in store space, so this is valid), and save its affine.
+ _snapshot.drawn->do_union(_store.drawn);
+ auto old_store_affine = _store.affine;
+
+ // Get the list of corner points in the store's drawn region and the snapshot bounds rect, all at the view's affine.
+ std::vector<Geom::Point> pts;
+ auto add_rect = [&, this] (Geom::Parallelogram const &pl) {
+ for (int i = 0; i < 4; i++) {
+ pts.emplace_back(Geom::Point(pl.corner(i)));
+ }
+ };
+ auto add_store = [&, this] (Store const &s) {
+ int nrects = s.drawn->get_num_rectangles();
+ auto affine = s.affine.inverse() * view.affine;
+ for (int i = 0; i < nrects; i++) {
+ add_rect(Geom::Parallelogram(cairo_to_geom(s.drawn->get_rectangle(i))) * affine);
+ }
+ };
+ add_store(_store);
+ add_rect(Geom::Parallelogram(_snapshot.rect) * _snapshot.affine.inverse() * view.affine);
+
+ // Compute their minimum-area bounding box as a fragment - an (affine, rect) pair.
+ auto [affine, rect] = min_bounding_box(pts);
+ affine = view.affine * affine;
+
+ // Check if the paste transform takes the snapshot store exactly onto the new fragment, possibly with a dihedral transformation.
+ auto paste = Geom::Scale(_snapshot.rect.dimensions())
+ * Geom::Translate(_snapshot.rect.min())
+ * _snapshot.affine.inverse()
+ * affine
+ * Geom::Translate(-rect.min())
+ * Geom::Scale(rect.dimensions()).inverse();
+ if (preserves_unitsquare(paste)) {
+ // If so, simply take the new fragment to be exactly the same as the snapshot store.
+ rect = _snapshot.rect;
+ affine = _snapshot.affine;
+ }
+
+ // Compute the scale difference between the backing store and the new fragment, giving the amount of detail that would be lost by pasting.
+ if ( double scale_ratio = std::sqrt(std::abs(_store.affine.det() / affine.det()));
+ scale_ratio > 4.0 )
+ {
+ // Zoom the new fragment in to increase its quality.
+ double grow = scale_ratio / 2.0;
+ rect *= Geom::Scale(grow);
+ affine *= Geom::Scale(grow);
+ }
+
+ // Do not allow the fragment to become more detailed than the window.
+ if ( double scale_ratio = std::sqrt(std::abs(affine.det() / view.affine.det()));
+ scale_ratio > 1.0 )
+ {
+ // Zoom the new fragment out to reduce its quality.
+ double shrink = 1.0 / scale_ratio;
+ rect *= Geom::Scale(shrink);
+ affine *= Geom::Scale(shrink);
+ }
+
+ // Find the bounding rect of the visible region + prerender margin within the new fragment. We do not want to discard this content in the next clipping step.
+ auto renderable = (Geom::Parallelogram(expandedBy(view.rect, _prefs.prerender)) * view.affine.inverse() * affine).bounds() & rect;
+
+ // Cap the dimensions of the new fragment to slightly larger than the maximum dimension of the window by clipping it towards the screen centre. (Lower in Cairo mode since otherwise too slow to cope.)
+ double max_dimension = max(view.rect.dimensions()) * (_graphics->is_opengl() ? 1.7 : 0.8);
+ auto dimens = rect.dimensions();
+ dimens.x() = std::min(dimens.x(), max_dimension);
+ dimens.y() = std::min(dimens.y(), max_dimension);
+ auto center = Geom::Rect(view.rect).midpoint() * view.affine.inverse() * affine;
+ center.x() = Util::safeclamp(center.x(), rect.left() + dimens.x() * 0.5, rect.right() - dimens.x() * 0.5);
+ center.y() = Util::safeclamp(center.y(), rect.top() + dimens.y() * 0.5, rect.bottom() - dimens.y() * 0.5);
+ rect = Geom::Rect(center - dimens * 0.5, center + dimens * 0.5);
+
+ // Ensure the new fragment contains the renderable rect from earlier, enlarging it and reducing resolution if necessary.
+ if (!rect.contains(renderable)) {
+ auto oldrect = rect;
+ rect.unionWith(renderable);
+ double shrink = 1.0 / std::max(rect.width() / oldrect.width(), rect.height() / oldrect.height());
+ rect *= Geom::Scale(shrink);
+ affine *= Geom::Scale(shrink);
+ }
+
+ // Calculate the paste transform from the snapshot store to the new fragment (again).
+ paste = Geom::Scale(_snapshot.rect.dimensions())
+ * Geom::Translate(_snapshot.rect.min())
+ * _snapshot.affine.inverse()
+ * affine
+ * Geom::Translate(-rect.min())
+ * Geom::Scale(rect.dimensions()).inverse();
+
+ if (_prefs.debug_logging) std::cout << "New fragment dimensions " << rect.width() << ' ' << rect.height() << std::endl;
+
+ if (paste.isIdentity(0.001) && rect.dimensions().round() == _snapshot.rect.dimensions()) {
+ // Fast path: simply paste the backing store onto the snapshot store.
+ if (_prefs.debug_logging) std::cout << "Fast snapshot combine" << std::endl;
+ _graphics->fast_snapshot_combine();
+ } else {
+ // General path: paste the snapshot store and then the backing store onto a new fragment, then set that as the snapshot store.
+ auto frag_rect = rect.roundOutwards();
+ _graphics->snapshot_combine(Fragment{ affine, frag_rect });
+ _snapshot.rect = frag_rect;
+ _snapshot.affine = affine;
+ }
+
+ // Start drawing again on a new blank store aligned to the screen.
+ recreate_store(view);
+ // Transform the snapshot clean region to the new store.
+ // Todo: Should really clip this to the new snapshot rect, only we can't because it's generally not aligned with the store's affine.
+ _snapshot.drawn = shrink_region(region_affine_approxinwards(_snapshot.drawn, old_store_affine.inverse() * _store.affine, _store.rect), 4, -2);
+};
+
+void Stores::reset()
+{
+ _mode = Mode::None;
+ _store.drawn.clear();
+ _snapshot.drawn.clear();
+}
+
+// Handle transitions and actions in response to viewport changes.
+auto Stores::update(Fragment const &view) -> Action
+{
+ switch (_mode) {
+
+ case Mode::None: {
+ // Not yet initialised or just reset - create store for first time.
+ recreate_store(view);
+ _mode = Mode::Normal;
+ if (_prefs.debug_logging) std::cout << "Full reset" << std::endl;
+ return Action::Recreated;
+ }
+
+ case Mode::Normal: {
+ auto result = Action::None;
+ // Enter decoupled mode if the affine has changed from what the store was drawn at.
+ if (view.affine != _store.affine) {
+ // Snapshot and reset the store.
+ take_snapshot(view);
+ // Enter decoupled mode.
+ _mode = Mode::Decoupled;
+ if (_prefs.debug_logging) std::cout << "Enter decoupled mode" << std::endl;
+ result = Action::Recreated;
+ } else {
+ // Determine whether the view has moved sufficiently far that we need to shift the store.
+ if (!_store.rect.contains(expandedBy(view.rect, _prefs.prerender))) {
+ // The visible region + prerender margin has reached the edge of the store.
+ if (!(cairo_to_geom(_store.drawn->get_extents()) & expandedBy(view.rect, _prefs.prerender + _prefs.padding)).regularized()) {
+ // If the store contains no reusable content at all, recreate it.
+ recreate_store(view);
+ if (_prefs.debug_logging) std::cout << "Recreate store" << std::endl;
+ result = Action::Recreated;
+ } else {
+ // Otherwise shift it.
+ shift_store(view);
+ if (_prefs.debug_logging) std::cout << "Shift store" << std::endl;
+ result = Action::Shifted;
+ }
+ }
+ }
+ // After these operations, the store should now contain the visible region + prerender margin.
+ assert(_store.rect.contains(expandedBy(view.rect, _prefs.prerender)));
+ return result;
+ }
+
+ case Mode::Decoupled: {
+ // Completely cancel the previous redraw and start again if the viewing parameters have changed too much.
+ auto check_restart_redraw = [&, this] {
+ // With this debug feature on, redraws should never be restarted.
+ if (_prefs.debug_sticky_decoupled) return false;
+
+ // Restart if the store is no longer covering the middle 50% of the screen. (Usually triggered by rotating or zooming out.)
+ auto pl = Geom::Parallelogram(view.rect);
+ pl *= Geom::Translate(-pl.midpoint()) * Geom::Scale(0.5) * Geom::Translate(pl.midpoint());
+ pl *= view.affine.inverse() * _store.affine;
+ if (!Geom::Parallelogram(_store.rect).contains(pl)) {
+ if (_prefs.debug_logging) std::cout << "Restart redraw (store not fully covering screen)" << std::endl;
+ return true;
+ }
+
+ // Also restart if zoomed in or out too much.
+ auto scale_ratio = std::abs(view.affine.det() / _store.affine.det());
+ if (scale_ratio > 3.0 || scale_ratio < 0.7) {
+ // Todo: Un-hard-code these thresholds.
+ // * The threshold 3.0 is for zooming in. It says that if the quality of what is being redrawn is more than 3x worse than that of the screen, restart. This is necessary to ensure acceptably high resolution is kept as you zoom in.
+ // * The threshold 0.7 is for zooming out. It says that if the quality of what is being redrawn is too high compared to the screen, restart. This prevents wasting time redrawing the screen slowly, at too high a quality that will probably not ever be seen.
+ if (_prefs.debug_logging) std::cout << "Restart redraw (zoomed changed too much)" << std::endl;
+ return true;
+ }
+
+ // Don't restart.
+ return false;
+ };
+
+ if (check_restart_redraw()) {
+ // Re-use as much content as possible from the store and the snapshot, and set as the new snapshot.
+ snapshot_combine(view);
+ return Action::Recreated;
+ }
+
+ return Action::None;
+ }
+
+ default: {
+ assert(false);
+ return Action::None;
+ }
+ }
+}
+
+auto Stores::finished_draw(Fragment const &view) -> Action
+{
+ // Finished drawing. Handle transitions out of decoupled mode, by checking if we need to reset the store to the correct affine.
+ if (_mode == Mode::Decoupled) {
+ if (_prefs.debug_sticky_decoupled) {
+ // Debug feature: stop redrawing, but stay in decoupled mode.
+ } else if (_store.affine == view.affine) {
+ // Store is at the correct affine - exit decoupled mode.
+ if (_prefs.debug_logging) std::cout << "Exit decoupled mode" << std::endl;
+ // Exit decoupled mode.
+ _mode = Mode::Normal;
+ _graphics->invalidate_snapshot();
+ } else {
+ // Content is rendered at the wrong affine - take a new snapshot and continue idle process to continue rendering at the new affine.
+ // Snapshot and reset the backing store.
+ take_snapshot(view);
+ if (_prefs.debug_logging) std::cout << "Remain in decoupled mode" << std::endl;
+ return Action::Recreated;
+ }
+ }
+
+ return Action::None;
+}
+
+} // 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 :
diff --git a/src/ui/widget/canvas/stores.h b/src/ui/widget/canvas/stores.h
new file mode 100644
index 0000000..70b10cc
--- /dev/null
+++ b/src/ui/widget/canvas/stores.h
@@ -0,0 +1,106 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Abstraction of the store/snapshot mechanism.
+ * Copyright (C) 2022 PBS <pbs3141@gmail.com>
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_STORES_H
+#define INKSCAPE_UI_WIDGET_CANVAS_STORES_H
+
+#include "fragment.h"
+#include "util.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+struct Fragment;
+class Prefs;
+class Graphics;
+
+class Stores
+{
+public:
+ enum class Mode
+ {
+ None, /// Not initialised or just reset; no stores exist yet.
+ Normal, /// Normal mode consisting of just a backing store.
+ Decoupled /// Decoupled mode consisting of both a backing store and a snapshot store.
+ };
+
+ enum class Action
+ {
+ None, /// The backing store was not changed.
+ Recreated, /// The backing store was completely recreated.
+ Shifted /// The backing store was shifted into a new rectangle.
+ };
+
+ struct Store : Fragment
+ {
+ /**
+ * The region of space containing drawn content.
+ * For the snapshot, this region is transformed to store space and approximated inwards.
+ */
+ Cairo::RefPtr<Cairo::Region> drawn;
+ };
+
+ /// Construct a blank object with no stores.
+ Stores(Prefs const &prefs)
+ : _mode(Mode::None)
+ , _graphics(nullptr)
+ , _prefs(prefs) {}
+
+ /// Set the pointer to the graphics object.
+ void set_graphics(Graphics *g) { _graphics = g; }
+
+ /// Discards all stores. (The actual operation on the graphics is performed on the next update().)
+ void reset();
+
+ /// Respond to a viewport change. (Requires a valid graphics.)
+ Action update(Fragment const &view);
+
+ /// Respond to drawing of the backing store having finished. (Requires a valid graphics.)
+ Action finished_draw(Fragment const &view);
+
+ /// Record a rectangle as being drawn to the store.
+ void mark_drawn(Geom::IntRect const &rect) { _store.drawn->do_union(geom_to_cairo(rect)); }
+
+ // Getters.
+ Store const &store() const { return _store; }
+ Store const &snapshot() const { return _snapshot; }
+ Mode mode() const { return _mode; }
+
+private:
+ // Internal state.
+ Mode _mode;
+ Store _store, _snapshot;
+
+ // The graphics object that executes the operations on the stores.
+ Graphics *_graphics;
+
+ // The preferences object we read preferences from.
+ Prefs const &_prefs;
+
+ // Internal actions.
+ Geom::IntRect centered(Fragment const &view) const;
+ void recreate_store(Fragment const &view);
+ void shift_store(Fragment const &view);
+ void take_snapshot(Fragment const &view);
+ void snapshot_combine(Fragment const &view);
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_STORES_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/canvas/synchronizer.cpp b/src/ui/widget/canvas/synchronizer.cpp
new file mode 100644
index 0000000..331057b
--- /dev/null
+++ b/src/ui/widget/canvas/synchronizer.cpp
@@ -0,0 +1,103 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include "synchronizer.h"
+#include <cassert>
+
+namespace Inkscape::UI::Widget {
+
+Synchronizer::Synchronizer()
+{
+ dispatcher.connect([this] { on_dispatcher(); });
+}
+
+void Synchronizer::signalExit() const
+{
+ auto lock = std::unique_lock(mutables);
+ awaken();
+ assert(slots.empty());
+ exitposted = true;
+}
+
+void Synchronizer::runInMain(std::function<void()> const &f) const
+{
+ auto lock = std::unique_lock(mutables);
+ awaken();
+ auto s = Slot{ &f };
+ slots.emplace_back(&s);
+ assert(!exitposted);
+ slots_cond.wait(lock, [&] { return !s.func; });
+}
+
+void Synchronizer::waitForExit() const
+{
+ auto lock = std::unique_lock(mutables);
+ main_blocked = true;
+ while (true) {
+ if (!slots.empty()) {
+ process_slots(lock);
+ } else if (exitposted) {
+ exitposted = false;
+ break;
+ }
+ main_cond.wait(lock);
+ }
+ main_blocked = false;
+}
+
+sigc::connection Synchronizer::connectExit(sigc::slot<void()> const &slot)
+{
+ return signal_exit.connect(slot);
+}
+
+void Synchronizer::awaken() const
+{
+ if (exitposted || !slots.empty()) {
+ return;
+ }
+
+ if (main_blocked) {
+ main_cond.notify_all();
+ } else {
+ const_cast<Glib::Dispatcher&>(dispatcher).emit(); // Glib::Dispatcher is const-incorrect.
+ }
+}
+
+void Synchronizer::on_dispatcher() const
+{
+ auto lock = std::unique_lock(mutables);
+ if (!slots.empty()) {
+ process_slots(lock);
+ } else if (exitposted) {
+ exitposted = false;
+ lock.unlock();
+ signal_exit.emit();
+ }
+}
+
+void Synchronizer::process_slots(std::unique_lock<std::mutex> &lock) const
+{
+ while (!slots.empty()) {
+ auto slots_grabbed = std::move(slots);
+ lock.unlock();
+ for (auto &s : slots_grabbed) {
+ (*s->func)();
+ }
+ lock.lock();
+ for (auto &s : slots_grabbed) {
+ s->func = nullptr;
+ }
+ slots_cond.notify_all();
+ }
+}
+
+} // namespace Inkscape::UI::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 :
diff --git a/src/ui/widget/canvas/synchronizer.h b/src/ui/widget/canvas/synchronizer.h
new file mode 100644
index 0000000..45c88d2
--- /dev/null
+++ b/src/ui/widget/canvas/synchronizer.h
@@ -0,0 +1,72 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_SYNCHRONIZER_H
+#define INKSCAPE_UI_WIDGET_CANVAS_SYNCHRONIZER_H
+
+#include <functional>
+#include <vector>
+#include <mutex>
+#include <condition_variable>
+
+#include <sigc++/sigc++.h>
+#include <glibmm/dispatcher.h>
+
+namespace Inkscape::UI::Widget {
+
+// Synchronisation primitive suiting the canvas's needs. All synchronisation between the main/render threads goes through here.
+class Synchronizer
+{
+public:
+ Synchronizer();
+
+ // Background side:
+
+ // Indicate that the background process has exited, causing EITHER signal_exit to be emitted OR waitforexit() to unblock.
+ void signalExit() const;
+
+ // Block until the given function has executed in the main thread, possibly waking it up if it is itself blocked.
+ // (Note: This is necessary for servicing occasional buffer mapping requests where one can't be pulled from a pool.)
+ void runInMain(std::function<void()> const &f) const;
+
+ // Main-thread side:
+
+ // Block until the background process has exited, gobbling the emission of signal_exit in the process.
+ void waitForExit() const;
+
+ // Connect to signal_exit.
+ sigc::connection connectExit(sigc::slot<void()> const &slot);
+
+private:
+ struct Slot
+ {
+ std::function<void()> const *func;
+ };
+
+ Glib::Dispatcher dispatcher; // Used to wake up main thread if idle in GTK main loop.
+ sigc::signal<void()> signal_exit;
+
+ mutable std::mutex mutables;
+ mutable bool exitposted = false;
+ mutable bool main_blocked = false; // Whether main thread is blocked in waitForExit().
+ mutable std::condition_variable main_cond; // Used to wake up main thread if blocked.
+ mutable std::vector<Slot*> slots; // List of functions from runInMain() waiting to be run.
+ mutable std::condition_variable slots_cond; // Used to wake up render threads blocked in runInMain().
+
+ void awaken() const;
+ void on_dispatcher() const;
+ void process_slots(std::unique_lock<std::mutex> &lock) const;
+};
+
+} // namespace Inkscape::UI::Widget
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_SYNCHRONIZER_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/canvas/texture.cpp b/src/ui/widget/canvas/texture.cpp
new file mode 100644
index 0000000..420937a
--- /dev/null
+++ b/src/ui/widget/canvas/texture.cpp
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include "texture.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+static bool have_gltexstorage()
+{
+ static bool result = [] {
+ return epoxy_gl_version() >= 42 || epoxy_has_gl_extension("GL_ARB_texture_storage");
+ }();
+ return result;
+}
+
+static bool have_glinvalidateteximage()
+{
+ static bool result = [] {
+ return epoxy_gl_version() >= 43 || epoxy_has_gl_extension("ARB_invalidate_subdata");
+ }();
+ return result;
+}
+
+Texture::Texture(Geom::IntPoint const &size)
+ : _size(size)
+{
+ glGenTextures(1, &_id);
+ glBindTexture(GL_TEXTURE_2D, _id);
+
+ // Common flags for all textures used at the moment.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+
+ if (have_gltexstorage()) {
+ glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, size.x(), size.y());
+ } else {
+ // Note: This fallback path is always chosen on the Mac due to Apple's crippling of OpenGL.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, size.x(), size.y(), 0, GL_BGRA, GL_UNSIGNED_BYTE, nullptr);
+ }
+}
+
+void Texture::invalidate()
+{
+ if (have_glinvalidateteximage()) {
+ glInvalidateTexImage(_id, 0);
+ }
+}
+
+} // 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 :
diff --git a/src/ui/widget/canvas/texture.h b/src/ui/widget/canvas/texture.h
new file mode 100644
index 0000000..98aeba2
--- /dev/null
+++ b/src/ui/widget/canvas/texture.h
@@ -0,0 +1,62 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_TEXTURE_H
+#define INKSCAPE_UI_WIDGET_CANVAS_TEXTURE_H
+
+#include <boost/noncopyable.hpp>
+#include <2geom/point.h>
+#include <epoxy/gl.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class Texture
+{
+public:
+ // Create null texture owning no resources.
+ Texture() = default;
+
+ // Allocate a blank texture of a given size. The texture is bound to GL_TEXTURE_2D.
+ Texture(Geom::IntPoint const &size);
+
+ // Wrap an existing texture.
+ Texture(GLuint id, Geom::IntPoint const &size) : _id(id), _size(size) {}
+
+ // Boilerplate constructors/operators
+ Texture(Texture &&other) noexcept { _movefrom(other); }
+ Texture &operator=(Texture &&other) noexcept { _reset(); _movefrom(other); return *this; }
+ ~Texture() { _reset(); }
+
+ // Observers
+ GLuint id() const { return _id; }
+ Geom::IntPoint const &size() const { return _size; }
+ explicit operator bool() const { return _id; }
+
+ // Methods
+ void clear() { _reset(); _id = 0; }
+ void invalidate();
+
+private:
+ GLuint _id = 0;
+ Geom::IntPoint _size;
+
+ void _reset() noexcept { if (_id) glDeleteTextures(1, &_id); }
+ void _movefrom(Texture &other) noexcept { _id = other._id; _size = other._size; other._id = 0; }
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_TEXTURE_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/canvas/texturecache.cpp b/src/ui/widget/canvas/texturecache.cpp
new file mode 100644
index 0000000..6215849
--- /dev/null
+++ b/src/ui/widget/canvas/texturecache.cpp
@@ -0,0 +1,115 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <unordered_map>
+#include <vector>
+#include <cassert>
+#include <boost/unordered_map.hpp> // For hash of pair
+#include "helper/mathfns.h"
+#include "texturecache.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+namespace {
+
+class BasicTextureCache : public TextureCache
+{
+ static int constexpr min_dimension = 16;
+ static int constexpr expiration_timeout = 10000;
+
+ static int constexpr dim_to_ind(int dim) { return Util::floorlog2((dim - 1) / min_dimension) + 1; }
+ static int constexpr ind_to_maxdim(int index) { return min_dimension * (1 << index); }
+
+ static std::pair<int, int> dims_to_inds(Geom::IntPoint const &dims) { return { dim_to_ind(dims.x()), dim_to_ind(dims.y()) }; }
+ static Geom::IntPoint inds_to_maxdims(std::pair<int, int> const &inds) { return { ind_to_maxdim(inds.first), ind_to_maxdim(inds.second) }; }
+
+ // A cache of POT textures.
+ struct Bucket
+ {
+ std::vector<Texture> unused;
+ int used = 0;
+ int high_use_count = 0;
+ };
+ boost::unordered_map<std::pair<int, int>, Bucket> buckets;
+
+ // Used to periodicially discard excess cached textures.
+ int expiration_timer = 0;
+
+public:
+ Texture request(Geom::IntPoint const &dimensions) override
+ {
+ // Find the bucket that the dimensions fall into.
+ auto indexes = dims_to_inds(dimensions);
+ auto &b = buckets[indexes];
+
+ // Reuse or create a texture of the appropriate dimensions.
+ Texture tex;
+ if (!b.unused.empty()) {
+ tex = std::move(b.unused.back());
+ b.unused.pop_back();
+ glBindTexture(GL_TEXTURE_2D, tex.id());
+ } else {
+ tex = Texture(inds_to_maxdims(indexes)); // binds
+ }
+
+ // Record the new use count of the bucket.
+ b.used++;
+ if (b.used > b.high_use_count) {
+ // If the use count has gone above the high-water mark, record this, and reset the timer for when to clean up excess unused textures.
+ b.high_use_count = b.used;
+ expiration_timer = 0;
+ }
+
+ return tex;
+ }
+
+ void finish(Texture tex) override
+ {
+ auto indexes = dims_to_inds(tex.size());
+ auto &b = buckets[indexes];
+
+ // Orphan the texture, if possible.
+ tex.invalidate();
+
+ // Put the texture back in its corresponding bucket's cache of unused textures.
+ b.unused.emplace_back(std::move(tex));
+ b.used--;
+
+ // If the expiration timeout has been reached, prune the cache of textures down to what was actually used in the last cycle.
+ expiration_timer++;
+ if (expiration_timer >= expiration_timeout) {
+ expiration_timer = 0;
+
+ for (auto &[k, b] : buckets) {
+ int max_unused = b.high_use_count - b.used;
+ assert(max_unused >= 0);
+ if (b.unused.size() > max_unused) {
+ b.unused.resize(max_unused);
+ }
+ b.high_use_count = b.used;
+ }
+ }
+ }
+};
+
+} // namespace
+
+std::unique_ptr<TextureCache> TextureCache::create()
+{
+ return std::make_unique<BasicTextureCache>();
+}
+
+} // 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 :
diff --git a/src/ui/widget/canvas/texturecache.h b/src/ui/widget/canvas/texturecache.h
new file mode 100644
index 0000000..ea78a67
--- /dev/null
+++ b/src/ui/widget/canvas/texturecache.h
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Extremely basic gadget for re-using textures, since texture creation turns out to be quite expensive.
+ * Copyright (C) 2022 PBS <pbs3141@gmail.com>
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_TEXTURECACHE_H
+#define INKSCAPE_UI_WIDGET_CANVAS_TEXTURECACHE_H
+
+#include <memory>
+#include "texture.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class TextureCache
+{
+public:
+ virtual ~TextureCache() = default;
+
+ static std::unique_ptr<TextureCache> create();
+
+ /**
+ * Request a texture of at least the given dimensions.
+ * The texture is bound to GL_TEXTURE_2D.
+ */
+ virtual Texture request(Geom::IntPoint const &dimensions) = 0;
+
+ /**
+ * Return a no-longer used texture to the pool.
+ */
+ virtual void finish(Texture tex) = 0;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_TEXTURECACHE_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/canvas/updaters.cpp b/src/ui/widget/canvas/updaters.cpp
new file mode 100644
index 0000000..8441be0
--- /dev/null
+++ b/src/ui/widget/canvas/updaters.cpp
@@ -0,0 +1,235 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include "updaters.h"
+#include "ui/util.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class ResponsiveUpdater : public Updater
+{
+public:
+ Strategy get_strategy() const override { return Strategy::Responsive; }
+
+ void reset() override { clean_region = Cairo::Region::create(); }
+ void intersect (Geom::IntRect const &rect) override { clean_region->intersect(geom_to_cairo(rect)); }
+ void mark_dirty(Geom::IntRect const &rect) override { clean_region->subtract(geom_to_cairo(rect)); }
+ void mark_dirty(Cairo::RefPtr<Cairo::Region> const &reg) override { clean_region->subtract(reg); }
+ void mark_clean(Geom::IntRect const &rect) override { clean_region->do_union(geom_to_cairo(rect)); }
+
+ Cairo::RefPtr<Cairo::Region> get_next_clean_region() override { return clean_region; }
+ bool report_finished () override { return false; }
+ void next_frame () override {}
+};
+
+class FullRedrawUpdater : public ResponsiveUpdater
+{
+ // Whether we are currently in the middle of a redraw.
+ bool inprogress = false;
+
+ // Contains a copy of the old clean region if damage events occurred during the current redraw, otherwise null.
+ Cairo::RefPtr<Cairo::Region> old_clean_region;
+
+public:
+ Strategy get_strategy() const override { return Strategy::FullRedraw; }
+
+ void reset() override
+ {
+ ResponsiveUpdater::reset();
+ inprogress = false;
+ old_clean_region.clear();
+ }
+
+ void intersect(const Geom::IntRect &rect) override
+ {
+ ResponsiveUpdater::intersect(rect);
+ if (old_clean_region) old_clean_region->intersect(geom_to_cairo(rect));
+ }
+
+ void mark_dirty(Geom::IntRect const &rect) override
+ {
+ if (inprogress && !old_clean_region) old_clean_region = clean_region->copy();
+ ResponsiveUpdater::mark_dirty(rect);
+ }
+
+ void mark_dirty(const Cairo::RefPtr<Cairo::Region> &reg) override
+ {
+ if (inprogress && !old_clean_region) old_clean_region = clean_region->copy();
+ ResponsiveUpdater::mark_dirty(reg);
+ }
+
+ void mark_clean(const Geom::IntRect &rect) override
+ {
+ ResponsiveUpdater::mark_clean(rect);
+ if (old_clean_region) old_clean_region->do_union(geom_to_cairo(rect));
+ }
+
+ Cairo::RefPtr<Cairo::Region> get_next_clean_region() override
+ {
+ inprogress = true;
+ if (!old_clean_region) {
+ return clean_region;
+ } else {
+ return old_clean_region;
+ }
+ }
+
+ bool report_finished() override
+ {
+ assert(inprogress);
+ if (!old_clean_region) {
+ // Completed redraw without being damaged => finished.
+ inprogress = false;
+ return false;
+ } else {
+ // Completed redraw but damage events arrived => ask for another redraw, using the up-to-date clean region.
+ old_clean_region.clear();
+ return true;
+ }
+ }
+};
+
+class MultiscaleUpdater : public ResponsiveUpdater
+{
+ // Whether we are currently in the middle of a redraw.
+ bool inprogress = false;
+
+ // Whether damage events occurred during the current redraw.
+ bool activated = false;
+
+ int counter; // A steadily incrementing counter from which the current scale is derived.
+ int scale; // The current scale to process updates at.
+ int elapsed; // How much time has been spent at the current scale.
+ std::vector<Cairo::RefPtr<Cairo::Region>> blocked; // The region blocked from being updated at each scale.
+
+public:
+ Strategy get_strategy() const override { return Strategy::Multiscale; }
+
+ void reset() override
+ {
+ ResponsiveUpdater::reset();
+ inprogress = activated = false;
+ }
+
+ void intersect(const Geom::IntRect &rect) override
+ {
+ ResponsiveUpdater::intersect(rect);
+ if (activated) {
+ for (auto &reg : blocked) {
+ reg->intersect(geom_to_cairo(rect));
+ }
+ }
+ }
+
+ void mark_dirty(Geom::IntRect const &rect) override
+ {
+ ResponsiveUpdater::mark_dirty(rect);
+ post_mark_dirty();
+ }
+
+ void mark_dirty(const Cairo::RefPtr<Cairo::Region> &reg) override
+ {
+ ResponsiveUpdater::mark_dirty(reg);
+ post_mark_dirty();
+ }
+
+ void post_mark_dirty()
+ {
+ if (inprogress && !activated) {
+ counter = scale = elapsed = 0;
+ blocked = { Cairo::Region::create() };
+ activated = true;
+ }
+ }
+
+ void mark_clean(const Geom::IntRect &rect) override
+ {
+ ResponsiveUpdater::mark_clean(rect);
+ if (activated) blocked[scale]->do_union(geom_to_cairo(rect));
+ }
+
+ Cairo::RefPtr<Cairo::Region> get_next_clean_region() override
+ {
+ inprogress = true;
+ if (!activated) {
+ return clean_region;
+ } else {
+ auto result = clean_region->copy();
+ result->do_union(blocked[scale]);
+ return result;
+ }
+ }
+
+ bool report_finished() override
+ {
+ assert(inprogress);
+ if (!activated) {
+ // Completed redraw without damage => finished.
+ inprogress = false;
+ return false;
+ } else {
+ // Completed redraw but damage events arrived => begin updating any remaining damaged regions.
+ activated = false;
+ blocked.clear();
+ return true;
+ }
+ }
+
+ void next_frame() override
+ {
+ if (!activated) return;
+
+ // Stay at the current scale for 2^scale frames.
+ elapsed++;
+ if (elapsed < (1 << scale)) return;
+ elapsed = 0;
+
+ // Adjust the counter, which causes scale to hop around the values 0, 1, 2... spending half as much time at each subsequent scale.
+ counter++;
+ scale = 0;
+ for (int tmp = counter; tmp % 2 == 1; tmp /= 2) {
+ scale++;
+ }
+
+ // Ensure sufficiently many blocked zones exist.
+ if (scale == blocked.size()) {
+ blocked.emplace_back();
+ }
+
+ // Recreate the current blocked zone as the union of the clean region and lower-scale blocked zones.
+ blocked[scale] = clean_region->copy();
+ for (int i = 0; i < scale; i++) {
+ blocked[scale]->do_union(blocked[i]);
+ }
+ }
+};
+
+template<> std::unique_ptr<Updater> Updater::create<Updater::Strategy::Responsive>() {return std::make_unique<ResponsiveUpdater>();}
+template<> std::unique_ptr<Updater> Updater::create<Updater::Strategy::FullRedraw>() {return std::make_unique<FullRedrawUpdater>();}
+template<> std::unique_ptr<Updater> Updater::create<Updater::Strategy::Multiscale>() {return std::make_unique<MultiscaleUpdater>();}
+
+std::unique_ptr<Updater> Updater::create(Strategy strategy)
+{
+ switch (strategy)
+ {
+ case Strategy::Responsive: return create<Strategy::Responsive>();
+ case Strategy::FullRedraw: return create<Strategy::FullRedraw>();
+ case Strategy::Multiscale: return create<Strategy::Multiscale>();
+ default: return nullptr; // Never triggered, but GCC errors out on build without.
+ }
+}
+
+} // 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 :
diff --git a/src/ui/widget/canvas/updaters.h b/src/ui/widget/canvas/updaters.h
new file mode 100644
index 0000000..d36685a
--- /dev/null
+++ b/src/ui/widget/canvas/updaters.h
@@ -0,0 +1,78 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Controls the order to update invalidated regions.
+ * Copyright (C) 2022 PBS <pbs3141@gmail.com>
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_UPDATERS_H
+#define INKSCAPE_UI_WIDGET_CANVAS_UPDATERS_H
+
+#include <vector>
+#include <memory>
+#include <2geom/int-rect.h>
+#include <cairomm/refptr.h>
+#include <cairomm/region.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+// A class for tracking invalidation events and producing redraw regions.
+class Updater
+{
+public:
+ virtual ~Updater() = default;
+
+ // The subregion of the store with up-to-date content.
+ Cairo::RefPtr<Cairo::Region> clean_region;
+
+ enum class Strategy
+ {
+ Responsive, // As soon as a region is invalidated, redraw it.
+ FullRedraw, // When a region is invalidated, delay redraw until after the current redraw is completed.
+ Multiscale, // Updates tiles near the mouse faster. Gives the best of both.
+ };
+
+ // Create an Updater using the given strategy.
+ template <Strategy strategy>
+ static std::unique_ptr<Updater> create();
+
+ // Create an Updater using a choice of strategy specified at runtime.
+ static std::unique_ptr<Updater> create(Strategy strategy);
+
+ // Return the strategy in use.
+ virtual Strategy get_strategy() const = 0;
+
+ virtual void reset() = 0; // Reset the clean region to empty.
+ virtual void intersect (Geom::IntRect const &) = 0; // Called when the store changes position; clip everything to the new store rectangle.
+ virtual void mark_dirty(Geom::IntRect const &) = 0; // Called on every invalidate event.
+ virtual void mark_dirty(Cairo::RefPtr<Cairo::Region> const &) = 0; // Called on every invalidate event.
+ virtual void mark_clean(Geom::IntRect const &) = 0; // Called on every rectangle redrawn.
+
+ // Called at the start of a redraw to determine what region to consider clean (i.e. will not be drawn).
+ virtual Cairo::RefPtr<Cairo::Region> get_next_clean_region() = 0;
+
+ // Called after a redraw has finished. Returns true to indicate that further redraws are required with different clean regions.
+ virtual bool report_finished() = 0;
+
+ // Called at the start of each frame. Some updaters (Multiscale) require this information.
+ virtual void next_frame() = 0;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_UPDATERS_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/canvas/util.cpp b/src/ui/widget/canvas/util.cpp
new file mode 100644
index 0000000..3d9d59b
--- /dev/null
+++ b/src/ui/widget/canvas/util.cpp
@@ -0,0 +1,70 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include "ui/util.h"
+#include "helper/geom.h"
+#include "util.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+void region_to_path(Cairo::RefPtr<Cairo::Context> const &cr, Cairo::RefPtr<Cairo::Region> const &reg)
+{
+ for (int i = 0; i < reg->get_num_rectangles(); i++) {
+ auto rect = reg->get_rectangle(i);
+ cr->rectangle(rect.x, rect.y, rect.width, rect.height);
+ }
+}
+
+Cairo::RefPtr<Cairo::Region> shrink_region(Cairo::RefPtr<Cairo::Region> const &reg, int d, int t)
+{
+ // Find the bounding rect, expanded by 1 in all directions.
+ auto rect = geom_to_cairo(expandedBy(cairo_to_geom(reg->get_extents()), 1));
+
+ // Take the complement of the region within the rect.
+ auto reg2 = Cairo::Region::create(rect);
+ reg2->subtract(reg);
+
+ // Increase the width and height of every rectangle by d.
+ auto reg3 = Cairo::Region::create();
+ for (int i = 0; i < reg2->get_num_rectangles(); i++) {
+ auto rect = reg2->get_rectangle(i);
+ rect.x += t;
+ rect.y += t;
+ rect.width += d;
+ rect.height += d;
+ reg3->do_union(rect);
+ }
+
+ // Take the complement of the region within the rect.
+ reg2 = Cairo::Region::create(rect);
+ reg2->subtract(reg3);
+
+ return reg2;
+}
+
+std::array<float, 3> checkerboard_darken(std::array<float, 3> const &rgb, float amount)
+{
+ std::array<float, 3> hsl;
+ SPColor::rgb_to_hsl_floatv(&hsl[0], rgb[0], rgb[1], rgb[2]);
+ hsl[2] += (hsl[2] < 0.08 ? 0.08 : -0.08) * amount;
+
+ std::array<float, 3> rgb2;
+ SPColor::hsl_to_rgb_floatv(&rgb2[0], hsl[0], hsl[1], hsl[2]);
+
+ return rgb2;
+}
+
+} // 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 :
diff --git a/src/ui/widget/canvas/util.h b/src/ui/widget/canvas/util.h
new file mode 100644
index 0000000..c2c1ad3
--- /dev/null
+++ b/src/ui/widget/canvas/util.h
@@ -0,0 +1,75 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_UTIL_H
+#define INKSCAPE_UI_WIDGET_CANVAS_UTIL_H
+
+#include <array>
+#include <2geom/int-rect.h>
+#include <2geom/affine.h>
+#include <cairomm/cairomm.h>
+#include "color.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+// Cairo additions
+
+/**
+ * Turn a Cairo region into a path on a given Cairo context.
+ */
+void region_to_path(Cairo::RefPtr<Cairo::Context> const &cr, Cairo::RefPtr<Cairo::Region> const &reg);
+
+/**
+ * Shrink a region by d/2 in all directions, while also translating it by (d/2 + t, d/2 + t).
+ */
+Cairo::RefPtr<Cairo::Region> shrink_region(Cairo::RefPtr<Cairo::Region> const &reg, int d, int t = 0);
+
+inline auto unioned(Cairo::RefPtr<Cairo::Region> a, Cairo::RefPtr<Cairo::Region> const &b)
+{
+ a->do_union(b);
+ return a;
+}
+
+// Colour operations
+
+inline auto rgb_to_array(uint32_t rgb)
+{
+ return std::array{SP_RGBA32_R_U(rgb) / 255.0f, SP_RGBA32_G_U(rgb) / 255.0f, SP_RGBA32_B_U(rgb) / 255.0f};
+}
+
+inline auto rgba_to_array(uint32_t rgba)
+{
+ return std::array{SP_RGBA32_R_U(rgba) / 255.0f, SP_RGBA32_G_U(rgba) / 255.0f, SP_RGBA32_B_U(rgba) / 255.0f, SP_RGBA32_A_U(rgba) / 255.0f};
+}
+
+inline auto premultiplied(std::array<float, 4> arr)
+{
+ arr[0] *= arr[3];
+ arr[1] *= arr[3];
+ arr[2] *= arr[3];
+ return arr;
+}
+
+std::array<float, 3> checkerboard_darken(std::array<float, 3> const &rgb, float amount = 1.0f);
+
+inline auto checkerboard_darken(uint32_t rgba)
+{
+ return checkerboard_darken(rgb_to_array(rgba), 1.0f - SP_RGBA32_A_U(rgba) / 255.0f);
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_UTIL_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-entry.cpp b/src/ui/widget/color-entry.cpp
new file mode 100644
index 0000000..c3d8ec3
--- /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..184427f
--- /dev/null
+++ b/src/ui/widget/color-icc-selector.cpp
@@ -0,0 +1,987 @@
+// 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 <gtkmm/combobox.h>
+#include <gtkmm/spinbutton.h>
+#include <glibmm/i18n.h>
+
+#include "colorspace.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"
+#include "ui/widget/scrollprotected.h"
+
+#define noDEBUG_LCMS
+
+#include "object/color-profile.h"
+#include "cms-system.h"
+#include "color-profile-cms-fns.h"
+
+#ifdef DEBUG_LCMS
+#include "preferences.h"
+#endif // DEBUG_LCMS
+
+#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 {
+
+GtkWidget *_scrollprotected_combo_box_new_with_model(GtkTreeModel *model)
+{
+ auto combobox = Gtk::manage(new Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBox>());
+ gtk_combo_box_set_model(combobox->gobj(), model);
+ return GTK_WIDGET(combobox->gobj());
+}
+
+size_t maxColorspaceComponentCount = 0;
+
+
+/**
+ * Internal variable to track all known colorspaces.
+ */
+std::set<cmsUInt32Number> knownColorspaces;
+
+/**
+ * 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)
+{
+}
+
+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()));
+}
+
+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;
+ Glib::RefPtr<Gtk::Adjustment> _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();
+
+ void _adjustmentChanged(Glib::RefPtr<Gtk::Adjustment> &adjustment);
+
+ void _sliderGrabbed();
+ void _sliderReleased();
+ void _sliderChanged();
+
+ static void _profileSelected(GtkWidget *src, gpointer data);
+ static void _fixupHit(GtkWidget *src, gpointer data);
+
+ void _setProfile(const std::string &profile);
+ void _switchToProfile(gchar const *name);
+
+ 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;
+
+ Glib::RefPtr<Gtk::Adjustment> _adj; // Channel adjustment
+ Inkscape::UI::Widget::ColorSlider *_slider;
+ GtkWidget *_sbtn; // Spinbutton
+ GtkWidget *_label; // Label
+
+ std::string _profileName;
+ Inkscape::ColorProfile *_prof;
+ guint _profChannelCount;
+ gulong _profChangedID;
+};
+
+
+
+const gchar *ColorICCSelector::MODE_NAME = N_("CMS");
+
+ColorICCSelector::ColorICCSelector(SelectedColor &color, bool no_alpha)
+ : _impl(nullptr)
+{
+ _impl = new ColorICCSelectorImpl(this, color);
+ init(no_alpha);
+ color.signal_changed.connect(sigc::mem_fun(*this, &ColorICCSelector::_colorChanged));
+ color.signal_icc_changed.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)
+ , _profileName()
+ , _prof(nullptr)
+ , _profChannelCount(0)
+ , _profChangedID(0)
+{
+}
+
+ColorICCSelectorImpl::~ColorICCSelectorImpl()
+{
+ _sbtn = nullptr;
+ _label = nullptr;
+}
+
+void ColorICCSelector::init(bool no_alpha)
+{
+ 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 = _scrollprotected_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, nullptr);
+
+ GtkTreeIter iter;
+ gtk_list_store_append(store, &iter);
+ gtk_list_store_set(store, &iter, 0, _("<none>"), 1, "null", -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);
+
+ _impl->_profChangedID = g_signal_connect(G_OBJECT(_impl->_profileSel), "changed",
+ G_CALLBACK(ColorICCSelectorImpl::_profileSelected), (gpointer)_impl);
+
+ row++;
+
+// populate the data for colorspaces and channels:
+ std::vector<colorspace::Component> things = colorspace::getColorSpaceInfo(cmsSigRgbData);
+
+ for (size_t i = 0; i < maxColorspaceComponentCount; i++) {
+ 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() : "";
+
+ _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::create(0.0, 0.0, scaleValue, step, page, page);
+
+ // Slider
+ _impl->_compUI[i]._slider =
+ Gtk::manage(new Inkscape::UI::Widget::ColorSlider(_impl->_compUI[i]._adj));
+ _impl->_compUI[i]._slider->set_tooltip_text((i < things.size()) ? things[i].tip.c_str() : "");
+ _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);
+
+ auto spinbutton = Gtk::manage(new ScrollProtected<Gtk::SpinButton>(_impl->_compUI[i]._adj, step, digits));
+ _impl->_compUI[i]._btn = GTK_WIDGET(spinbutton->gobj());
+ gtk_widget_set_tooltip_text(_impl->_compUI[i]._btn, (i < things.size()) ? things[i].tip.c_str() : "");
+ 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
+ _impl->_compUI[i]._adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*_impl, &ColorICCSelectorImpl::_adjustmentChanged), _impl->_compUI[i]._adj));
+
+ _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::create(0.0, 0.0, 100.0, 1.0, 10.0, 10.0);
+
+ // Slider
+ _impl->_slider = Gtk::manage(new Inkscape::UI::Widget::ColorSlider(_impl->_adj));
+ _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
+ auto spinbuttonalpha = Gtk::manage(new ScrollProtected<Gtk::SpinButton>(_impl->_adj, 1.0));
+ _impl->_sbtn = GTK_WIDGET(spinbuttonalpha->gobj());
+ 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);
+
+ if (no_alpha) {
+ _impl->_slider->hide();
+ gtk_widget_hide(_impl->_label);
+ gtk_widget_hide(_impl->_sbtn);
+ }
+
+ attachToGridOrTable(t, _impl->_sbtn, 2, row, 1, 1, false, true);
+
+ // Signals
+ _impl->_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*_impl, &ColorICCSelectorImpl::_adjustmentChanged), _impl->_adj));
+
+ _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);
+}
+
+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);
+ }
+}
+
+void ColorICCSelectorImpl::_switchToProfile(gchar const *name)
+{
+ bool dirty = false;
+ SPColor tmp(_color.color());
+
+ if (name && std::string(name) != "null") {
+ if (tmp.getColorProfile() == 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 (auto newProf = SP_ACTIVE_DOCUMENT->getProfileManager().find(name)) {
+ 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
+ guint count = cmsChannelsOf(asICColorSpaceSig(newProf->getColorSpace()));
+
+ std::vector<colorspace::Component> things =
+ colorspace::getColorSpaceInfo(asICColorSpaceSig(newProf->getColorSpace()));
+
+ std::vector<double> colors;
+ 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
+ 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));
+ tmp.setColorProfile(newProf);
+ tmp.setColors(std::move(colors));
+ } else {
+ g_warning("Couldn't get sRGB from color profile.");
+ }
+
+ dirty = true;
+ }
+ }
+ }
+ }
+ else {
+#ifdef DEBUG_LCMS
+ g_message("NUKE THE ICC");
+#endif // DEBUG_LCMS
+ if (tmp.hasColorProfile()) {
+ tmp.unsetColorProfile();
+ 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.getColorProfile());
+ _color.setColor(tmp);
+#ifdef DEBUG_LCMS
+ g_message("+_________________");
+#endif // DEBUG_LCMS
+ }
+}
+
+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, "null", -1);
+
+ gtk_combo_box_set_active(combo, 0);
+
+ int index = 1;
+ std::vector<SPObject *> current = SP_ACTIVE_DOCUMENT->getResourceList("iccprofile");
+
+ std::set<Inkscape::ColorProfile *> _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);
+}
+
+void ColorICCSelector::on_show()
+{
+ Gtk::Grid::on_show();
+ _colorChanged();
+}
+
+// Helpers for setting color value
+
+void ColorICCSelector::_colorChanged()
+{
+ _impl->_updating = TRUE;
+ auto color = _impl->_color.color();
+ auto name = color.getColorProfile();
+
+#ifdef DEBUG_LCMS
+ g_message("/^^^^^^^^^ %p::_colorChanged(%08x:%s)", this, color.toRGBA32(_impl->_color.alpha()), name.c_str());
+#endif // DEBUG_LCMS
+
+ _impl->_profilesChanged(name);
+ ColorScales<>::setScaled(_impl->_adj, _impl->_color.alpha());
+
+ _impl->_setProfile(name);
+ _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++) {
+ auto colors = color.getColors();
+ gdouble val = 0.0;
+ if (colors.size() > i) {
+ auto scale = static_cast<double>(_impl->_compUI[i]._component.scale);
+ if (_impl->_compUI[i]._component.scale == 256) {
+ val = (colors[i] + 128.0) / scale;
+ }
+ else {
+ val = colors[i] / 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 != 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
+ }
+ }
+ }
+ }
+ _impl->_updateSliders(-1);
+
+
+ _impl->_updating = FALSE;
+#ifdef DEBUG_LCMS
+ g_message("\\_________ %p::_colorChanged()", this);
+#endif // DEBUG_LCMS
+}
+
+void ColorICCSelectorImpl::_setProfile(const std::string &profile)
+{
+#ifdef DEBUG_LCMS
+ g_message("/^^^^^^^^^ %p::_setProfile(%s)", this, profile.c_str());
+#endif // DEBUG_LCMS
+ bool profChanged = false;
+ if (_prof && _profileName != profile) {
+ // Need to clear out the prior one
+ profChanged = true;
+ _profileName.clear();
+ _prof = nullptr;
+ _profChannelCount = 0;
+ } else if (!_prof && !profile.empty()) {
+ profChanged = true;
+ }
+
+ for (auto & i : _compUI) {
+ gtk_widget_hide(i._label);
+ i._slider->hide();
+ gtk_widget_hide(i._btn);
+ }
+
+ if (!profile.empty()) {
+ _prof = SP_ACTIVE_DOCUMENT->getProfileManager().find(profile.c_str());
+ if (_prof && (asICColorProfileClassSig(_prof->getProfileClass()) != cmsSigNamedColorClass)) {
+ _profChannelCount = _prof->getChannelCount();
+
+ 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));
+ gtk_widget_show(_compUI[i]._label);
+ _compUI[i]._slider->show();
+ gtk_widget_show(_compUI[i]._btn);
+ }
+ 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
+}
+
+void ColorICCSelectorImpl::_updateSliders(gint ignore)
+{
+ _slider->set_sensitive(false);
+
+ if (_color.color().hasColorProfile()) {
+ auto colors = _color.color().getColors();
+ if (colors.size() != _profChannelCount) {
+ g_warning("Can't set profile with %d colors to %d channels", (int)colors.size(), _profChannelCount);
+ }
+ for (guint i = 0; i < _profChannelCount; i++) {
+ double val = 0.0;
+ auto scale = static_cast<double>(_compUI[i]._component.scale);
+ if (_compUI[i]._component.scale == 256) {
+ val = (colors[i] + 128.0) / scale;
+ } else {
+ val = colors[i] / scale;
+ }
+ _compUI[i]._adj->set_value(val);
+ }
+
+ if (_prof) {
+ _slider->set_sensitive(true);
+
+ 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);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ 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(Glib::RefPtr<Gtk::Adjustment> &adjustment)
+{
+#ifdef DEBUG_LCMS
+ g_message("/^^^^^^^^^ %p::_adjustmentChanged()", this);
+#endif // DEBUG_LCMS
+
+ ColorICCSelector *iccSelector = _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 {
+ 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);
+ }
+
+ // Set the sRGB version of the color first.
+ guint32 prior = iccSelector->_impl->_color.color().toRGBA32(255);
+ guint32 newer = SP_RGBA32_U_COMPOSE(post[0], post[1], post[2], 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
+
+ // Be careful to always set() and then setColors() to retain ICC data.
+ newColor.set(newer);
+ if (iccSelector->_impl->_color.color().hasColorProfile()) {
+ std::vector<double> colors;
+ for (guint i = 0; i < iccSelector->_impl->_profChannelCount; i++) {
+ double 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;
+ }
+ colors.push_back(val);
+ }
+ newColor.setColors(std::move(colors));
+ }
+ }
+ }
+ iccSelector->_impl->_color.setColorAlpha(newColor, scaled);
+ iccSelector->_impl->_updateSliders(match);
+
+ iccSelector->_impl->_updating = FALSE;
+#ifdef DEBUG_LCMS
+ g_message("\\_________ %p::_adjustmentChanged()", this);
+#endif // DEBUG_LCMS
+}
+
+void ColorICCSelectorImpl::_sliderGrabbed()
+{
+}
+
+void ColorICCSelectorImpl::_sliderReleased()
+{
+}
+
+void ColorICCSelectorImpl::_sliderChanged()
+{
+}
+
+Gtk::Widget *ColorICCSelectorFactory::createWidget(Inkscape::UI::SelectedColor &color, bool no_alpha) const
+{
+ Gtk::Widget *w = Gtk::manage(new ColorICCSelector(color, no_alpha));
+ 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..444fbe2
--- /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, bool no_alpha);
+ ~ColorICCSelector() override;
+
+ void init(bool no_alpha);
+
+ 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, bool no_alpha) 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..f28ba25
--- /dev/null
+++ b/src/ui/widget/color-notebook.cpp
@@ -0,0 +1,382 @@
+// 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/dropper-tool.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 "widgets/spw-utilities.h"
+
+using Inkscape::CMSSystem;
+
+#define XPAD 2
+#define YPAD 1
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+
+ColorNotebook::ColorNotebook(SelectedColor &color, bool no_alpha)
+ : Gtk::Grid()
+ , _selected_color(color)
+{
+ set_name("ColorNotebook");
+
+ _initUI(no_alpha);
+
+ _selected_color.signal_changed.connect(sigc::mem_fun(*this, &ColorNotebook::_onSelectedColorChanged));
+ _selected_color.signal_dragged.connect(sigc::mem_fun(*this, &ColorNotebook::_onSelectedColorChanged));
+
+ auto desktop = SP_ACTIVE_DESKTOP;
+ _doc_replaced_connection = desktop->connectDocumentReplaced(sigc::hide<0>(sigc::mem_fun(*this, &ColorNotebook::setDocument)));
+ setDocument(desktop->getDocument());
+}
+
+ColorNotebook::~ColorNotebook()
+{
+ if (_onetimepick)
+ _onetimepick.disconnect();
+ _doc_replaced_connection.disconnect();
+ setDocument(nullptr);
+}
+
+ColorNotebook::Page::Page(std::unique_ptr<Inkscape::UI::ColorSelectorFactory> selector_factory, const char* icon)
+ : selector_factory(std::move(selector_factory)), icon_name(icon)
+{
+}
+
+void ColorNotebook::setDocument(SPDocument *document)
+{
+ _document = document;
+ _icc_changed_connection.disconnect();
+ if (document) {
+ _icc_changed_connection = document->connectResourcesChanged("iccprofile", [this]() {
+ _selected_color.emitIccChanged();
+ });
+ }
+}
+
+void ColorNotebook::set_label(const Glib::ustring& label) {
+ _label->set_markup(label);
+}
+
+void ColorNotebook::_initUI(bool no_alpha)
+{
+ guint row = 0;
+
+ _book = Gtk::make_managed<Gtk::Stack>();
+ _book->show();
+ _book->set_transition_type(Gtk::STACK_TRANSITION_TYPE_CROSSFADE);
+ _book->set_transition_duration(130);
+
+ // mode selection switcher widget shows all buttons for color mode selection, side by side
+ _switcher = Gtk::make_managed<Gtk::StackSwitcher>();
+ _switcher->set_stack(*_book);
+ // cannot leave it homogeneous - in some themes switcher gets very wide
+ _switcher->set_homogeneous(false);
+ _switcher->set_halign(Gtk::ALIGN_CENTER);
+ _switcher->show();
+ attach(*_switcher, 0, row++, 2);
+
+ _buttonbox = Gtk::make_managed<Gtk::Box>();
+ _buttonbox->show();
+
+ // combo mode selection is compact and only shows one entry (active)
+ _combo = Gtk::manage(new IconComboBox());
+ _combo->set_can_focus(false);
+ _combo->set_visible();
+ _combo->set_tooltip_text(_("Choose style of color selection"));
+
+ for (auto&& picker : get_color_pickers()) {
+ auto page = Page(std::move(picker.factory), picker.icon);
+ _addPage(page, no_alpha, picker.visibility_path);
+ }
+
+ _label = Gtk::make_managed<Gtk::Label>();
+ _label->set_visible();
+ _buttonbox->pack_start(*_label, false, true);
+ _buttonbox->pack_end(*_combo, false, false);
+ _combo->signal_changed().connect([=](){ _setCurrentPage(_combo->get_active_row_id(), false); });
+
+ _buttonbox->set_margin_start(XPAD);
+ _buttonbox->set_margin_end(XPAD);
+ _buttonbox->set_margin_top(YPAD);
+ _buttonbox->set_margin_bottom(YPAD);
+ _buttonbox->set_hexpand();
+ _buttonbox->set_valign(Gtk::ALIGN_START);
+ attach(*_buttonbox, 0, row, 2);
+
+ row++;
+
+ _book->set_margin_start(XPAD);
+ _book->set_margin_end(XPAD);
+ _book->set_margin_top(YPAD);
+ _book->set_margin_bottom(YPAD);
+ _book->set_hexpand();
+ _book->set_vexpand();
+ attach(*_book, 0, row, 2, 1);
+
+ // restore the last active page
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring page_name = prefs->getString("/colorselector/page", "");
+ _setCurrentPage(getPageIndex(page_name), true);
+ row++;
+
+ _observer = prefs->createObserver("/colorselector/switcher", [=](const Preferences::Entry& new_value) {
+ _switcher->set_visible(!new_value.getBool());
+ _buttonbox->set_visible(new_value.getBool());
+ });
+ _observer->call();
+
+ GtkWidget *rgbabox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+
+ /* 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);
+
+
+ /* 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);
+
+ // the "too much ink" icon is initially hidden
+ gtk_widget_hide(GTK_WIDGET(_box_toomuchink));
+
+ 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);
+
+ // remember the page we switched to
+ _book->property_visible_child_name().signal_changed().connect([=]() {
+ // We don't want to remember auto cms selection
+ Glib::ustring name = _book->get_visible_child_name();
+ if (get_visible() && !name.empty() && name != "CMS") {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setString("/colorselector/page", name);
+ }
+ });
+
+#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
+}
+
+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
+ if (colorbook->_onetimepick) {
+ colorbook->_onetimepick.disconnect();
+ }
+ else {
+ Inkscape::UI::Tools::sp_toggle_dropper(SP_ACTIVE_DESKTOP);
+ auto tool = dynamic_cast<Inkscape::UI::Tools::DropperTool *>(SP_ACTIVE_DESKTOP->event_context);
+ if (tool) {
+ colorbook->_onetimepick = tool->onetimepick_signal.connect(sigc::mem_fun(*colorbook, &ColorNotebook::_pickColor));
+ }
+ }
+}
+
+void ColorNotebook::_pickColor(ColorRGBA *color) {
+ // Set color to color notebook here.
+ _selected_color.setValue(color->getIntValue());
+ _onSelectedColorChanged();
+}
+
+void ColorNotebook::_onSelectedColorChanged() { _updateICCButtons(); }
+
+void ColorNotebook::_updateICCButtons()
+{
+ if (!_document) {
+ return;
+ }
+
+ SPColor color = _selected_color.color();
+ gfloat alpha = _selected_color.alpha();
+
+ g_return_if_fail((0.0 <= alpha) && (alpha <= 1.0));
+
+ /* update color management icon*/
+ gtk_widget_set_sensitive(_box_colormanaged, color.hasColorProfile());
+ gtk_widget_set_sensitive(_box_toomuchink, false);
+ gtk_widget_set_sensitive(_box_outofgamut, false);
+
+ if (color.hasColors()) {
+ auto name = color.getColorProfile();
+
+ // Set notebook page to cms if icc profile being used.
+ _setCurrentPage(getPageIndex("CMS"), true);
+
+ /* update out-of-gamut icon */
+ Inkscape::ColorProfile *target_profile =
+ _document->getProfileManager().find(name.c_str());
+ if (target_profile)
+ gtk_widget_set_sensitive(_box_outofgamut, target_profile->GamutCheck(color));
+
+ /* update too-much-ink icon */
+ Inkscape::ColorProfile *prof = _document->getProfileManager().find(name.c_str());
+ if (prof && CMSSystem::isPrintColorSpace(prof)) {
+ gtk_widget_show(GTK_WIDGET(_box_toomuchink));
+ double ink_sum = 0;
+ for (double i : color.getColors()) {
+ 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));
+ }
+ } else {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ auto page = prefs->getString("/colorselector/page");
+ _setCurrentPage(getPageIndex(page), true);
+ }
+}
+
+int ColorNotebook::getPageIndex(const Glib::ustring &name)
+{
+ return getPageIndex(_book->get_child_by_name(name));
+}
+
+int ColorNotebook::getPageIndex(Gtk::Widget *widget)
+{
+ const auto pages = _book->get_children();
+ for (int i = 0; i < pages.size(); i++) {
+ if (pages[i] == widget) {
+ return i;
+ }
+ }
+ return 0;
+}
+
+void ColorNotebook::_setCurrentPage(int i, bool sync_combo)
+{
+ const auto pages = _book->get_children();
+
+ if (i >= pages.size()) {
+ // page index could be outside the valid range if we manipulate visible color pickers;
+ // default to the first page, so we show something
+ i = 0;
+ }
+
+ if (i >= 0 && i < pages.size()) {
+ _book->set_visible_child(*pages[i]);
+ if (sync_combo) {
+ _combo->set_active_by_id(i);
+ }
+ }
+}
+
+void ColorNotebook::_addPage(Page &page, bool no_alpha, const Glib::ustring vpath)
+{
+ if (auto selector_widget = page.selector_factory->createWidget(_selected_color, no_alpha)) {
+ Glib::ustring mode_name = page.selector_factory->modeName();
+ _book->add(*selector_widget, mode_name, mode_name);
+ int page_num = _book->get_children().size() - 1;
+
+ _combo->add_row(page.icon_name, mode_name, page_num);
+
+ auto prefs = Inkscape::Preferences::get();
+ auto obs = prefs->createObserver(vpath, [=](const Preferences::Entry& value) {
+ _combo->set_row_visible(page_num, value.getBool());
+ selector_widget->set_visible(value.getBool());
+ });
+ obs->call();
+ _visibility_observers.emplace_back(std::move(obs));
+ }
+}
+
+}
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ 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..6e805eb
--- /dev/null
+++ b/src/ui/widget/color-notebook.h
@@ -0,0 +1,108 @@
+// 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
+
+#include <memory>
+#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 <gtkmm/stack.h>
+#include <gtkmm/stackswitcher.h>
+#include <glib.h>
+
+#include "color.h"
+#include "color-rgba.h"
+#include "preferences.h"
+#include "ui/selected-color.h"
+#include "ui/widget/icon-combobox.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class ColorNotebook
+ : public Gtk::Grid
+{
+public:
+ ColorNotebook(SelectedColor &color, bool no_alpha = false);
+ ~ColorNotebook() override;
+
+ void set_label(const Glib::ustring& label);
+
+protected:
+ struct Page {
+ Page(std::unique_ptr<Inkscape::UI::ColorSelectorFactory> selector_factory, const char* icon);
+
+ std::unique_ptr<Inkscape::UI::ColorSelectorFactory> selector_factory;
+ Glib::ustring icon_name;
+ };
+
+ void _initUI(bool no_alpha);
+ void _addPage(Page &page, bool no_alpha, const Glib::ustring vpath);
+ void setDocument(SPDocument *document);
+
+ void _pickColor(ColorRGBA *color);
+ static void _onPickerClicked(GtkWidget *widget, ColorNotebook *colorbook);
+ virtual void _onSelectedColorChanged();
+ int getPageIndex(const Glib::ustring &name);
+ int getPageIndex(Gtk::Widget *widget);
+
+ void _updateICCButtons();
+ void _setCurrentPage(int i, bool sync_combo);
+
+ Inkscape::UI::SelectedColor &_selected_color;
+ gulong _entryId;
+ Gtk::Stack* _book;
+ Gtk::StackSwitcher* _switcher;
+ Gtk::Box* _buttonbox;
+ Gtk::Label* _label;
+ GtkWidget *_rgbal; /* RGBA entry */
+ GtkWidget *_box_outofgamut, *_box_colormanaged, *_box_toomuchink;
+ GtkWidget *_btn_picker;
+ GtkWidget *_p; /* Color preview */
+ sigc::connection _onetimepick;
+ IconComboBox* _combo = nullptr;
+
+public:
+ // By default, disallow copy constructor and assignment operator
+ ColorNotebook(const ColorNotebook &obj) = delete;
+ ColorNotebook &operator=(const ColorNotebook &obj) = delete;
+
+ PrefObserver _observer;
+ std::vector<PrefObserver> _visibility_observers;
+
+ SPDocument *_document = nullptr;
+ sigc::connection _doc_replaced_connection;
+ sigc::connection _selection_connection;
+ sigc::connection _icc_changed_connection;
+};
+
+}
+}
+}
+#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-palette.cpp b/src/ui/widget/color-palette.cpp
new file mode 100644
index 0000000..e85ac01
--- /dev/null
+++ b/src/ui/widget/color-palette.cpp
@@ -0,0 +1,724 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <gtkmm/adjustment.h>
+#include <gtkmm/box.h>
+#include <gtkmm/button.h>
+#include <gtkmm/cssprovider.h>
+#include <gtkmm/menu.h>
+#include <gtkmm/menubutton.h>
+#include <gtkmm/popover.h>
+#include <gtkmm/radiomenuitem.h>
+#include <gtkmm/scale.h>
+#include <gtkmm/scrollbar.h>
+
+#include "color-palette.h"
+#include "ui/builder-utils.h"
+#include "ui/dialog/color-item.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+ColorPalette::ColorPalette():
+ _builder(create_builder("color-palette.glade")),
+ _normal_box(get_widget<Gtk::FlowBox>(_builder, "flow-box")),
+ _pinned_box(get_widget<Gtk::FlowBox>(_builder, "pinned")),
+ _menu(get_widget<Gtk::Menu>(_builder, "menu")),
+ _scroll_btn(get_widget<Gtk::FlowBox>(_builder, "scroll-buttons")),
+ _scroll_left(get_widget<Gtk::Button>(_builder, "btn-left")),
+ _scroll_right(get_widget<Gtk::Button>(_builder, "btn-right")),
+ _scroll_up(get_widget<Gtk::Button>(_builder, "btn-up")),
+ _scroll_down(get_widget<Gtk::Button>(_builder, "btn-down")),
+ _scroll(get_widget<Gtk::ScrolledWindow>(_builder, "scroll-wnd"))
+ {
+
+ auto& box = get_widget<Gtk::Box>(_builder, "palette-box");
+ this->add(box);
+
+ auto& config = get_widget<Gtk::MenuItem>(_builder, "config");
+ auto& dlg = get_widget<Gtk::Popover>(_builder, "config-popup");
+ config.signal_activate().connect([=,&dlg](){
+ dlg.popup();
+ });
+
+ auto& size = get_widget<Gtk::Scale>(_builder, "size-slider");
+ size.signal_change_value().connect([=,&size](Gtk::ScrollType, double val) {
+ _set_tile_size(static_cast<int>(size.get_value()));
+ _signal_settings_changed.emit();
+ return true;
+ });
+
+ auto& aspect = get_widget<Gtk::Scale>(_builder, "aspect-slider");
+ aspect.signal_change_value().connect([=,&aspect](Gtk::ScrollType, double val) {
+ _set_aspect(aspect.get_value());
+ _signal_settings_changed.emit();
+ return true;
+ });
+
+ auto& border = get_widget<Gtk::Scale>(_builder, "border-slider");
+ border.signal_change_value().connect([=,&border](Gtk::ScrollType, double val) {
+ _set_tile_border(static_cast<int>(border.get_value()));
+ _signal_settings_changed.emit();
+ return true;
+ });
+
+ auto& rows = get_widget<Gtk::Scale>(_builder, "row-slider");
+ rows.signal_change_value().connect([=,&rows](Gtk::ScrollType, double val) {
+ _set_rows(static_cast<int>(rows.get_value()));
+ _signal_settings_changed.emit();
+ return true;
+ });
+
+ auto& sb = get_widget<Gtk::CheckButton>(_builder, "use-sb");
+ sb.set_active(_force_scrollbar);
+ sb.signal_toggled().connect([=,&sb](){
+ _enable_scrollbar(sb.get_active());
+ _signal_settings_changed.emit();
+ });
+
+ auto& stretch = get_widget<Gtk::CheckButton>(_builder, "stretch");
+ stretch.set_active(_force_scrollbar);
+ stretch.signal_toggled().connect([=,&stretch](){
+ _enable_stretch(stretch.get_active());
+ _signal_settings_changed.emit();
+ });
+ update_stretch();
+
+ auto& large = get_widget<Gtk::CheckButton>(_builder, "enlarge");
+ large.set_active(_large_pinned_panel);
+ large.signal_toggled().connect([=,&large](){
+ _set_large_pinned_panel(large.get_active());
+ _signal_settings_changed.emit();
+ });
+ update_checkbox();
+
+ auto& sl = get_widget<Gtk::CheckButton>(_builder, "show-labels");
+ sl.set_visible(false);
+ sl.set_active(_show_labels);
+ sl.signal_toggled().connect([=,&sl](){
+ _show_labels = sl.get_active();
+ _signal_settings_changed.emit();
+ rebuild_widgets();
+ });
+
+ _scroll.set_min_content_height(1);
+
+ // set style for small buttons; we need them reasonably small, since they impact min height of color palette strip
+ {
+ auto css_provider = Gtk::CssProvider::create();
+ css_provider->load_from_data(
+ ".small {"
+ " padding: 1px;"
+ " margin: 0;"
+ "}"
+ );
+
+ auto& btn_menu = get_widget<Gtk::MenuButton>(_builder, "btn-menu");
+ Gtk::Widget* small_buttons[5] = {&_scroll_up, &_scroll_down, &_scroll_left, &_scroll_right, &btn_menu};
+ for (auto button : small_buttons) {
+ button->get_style_context()->add_provider(css_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ }
+ }
+
+ _scroll_down.signal_clicked().connect([=](){ scroll(0, get_palette_height(), get_tile_height() + _border, true); });
+ _scroll_up.signal_clicked().connect([=](){ scroll(0, -get_palette_height(), get_tile_height() + _border, true); });
+ _scroll_left.signal_clicked().connect([=](){ scroll(-10 * (get_tile_width() + _border), 0, 0.0, false); });
+ _scroll_right.signal_clicked().connect([=](){ scroll(10 * (get_tile_width() + _border), 0, 0.0, false); });
+
+ {
+ auto css_provider = Gtk::CssProvider::create();
+ css_provider->load_from_data(
+ "flowbox, scrolledwindow {"
+ " padding: 0;"
+ " border: 0;"
+ " margin: 0;"
+ " min-width: 1px;"
+ " min-height: 1px;"
+ "}");
+ _scroll.get_style_context()->add_provider(css_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ _normal_box.get_style_context()->add_provider(css_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ _pinned_box.get_style_context()->add_provider(css_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ }
+
+ // remove padding/margins from FlowBoxChild widgets, so previews can be adjacent to each other
+ {
+ auto css_provider = Gtk::CssProvider::create();
+ css_provider->load_from_data(
+ ".color-palette-main-box flowboxchild {"
+ " padding: 0;"
+ " border: 0;"
+ " margin: 0;"
+ " min-width: 1px;"
+ " min-height: 1px;"
+ "}");
+ get_style_context()->add_provider_for_screen(this->get_screen(), css_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ }
+
+ set_vexpand_set(true);
+ set_up_scrolling();
+
+ signal_size_allocate().connect([=](Gtk::Allocation& a){ set_up_scrolling(); });
+}
+
+ColorPalette::~ColorPalette() {
+ if (_active_timeout) {
+ g_source_remove(_active_timeout);
+ }
+}
+
+void ColorPalette::do_scroll(int dx, int dy) {
+ if (auto vert = _scroll.get_vscrollbar()) {
+ vert->set_value(vert->get_value() + dy);
+ }
+ if (auto horz = _scroll.get_hscrollbar()) {
+ horz->set_value(horz->get_value() + dx);
+ }
+}
+
+std::pair<double, double> get_range(Gtk::Scrollbar& sb) {
+ auto adj = sb.get_adjustment();
+ return std::make_pair(adj->get_lower(), adj->get_upper() - adj->get_page_size());
+}
+
+gboolean ColorPalette::scroll_cb(gpointer self) {
+ auto ptr = static_cast<ColorPalette*>(self);
+ bool fire_again = false;
+
+ if (auto vert = ptr->_scroll.get_vscrollbar()) {
+ auto value = vert->get_value();
+ // is this the final adjustment step?
+ if (fabs(ptr->_scroll_final - value) < fabs(ptr->_scroll_step)) {
+ vert->set_value(ptr->_scroll_final);
+ fire_again = false; // cancel timer
+ }
+ else {
+ auto pos = value + ptr->_scroll_step;
+ vert->set_value(pos);
+ auto range = get_range(*vert);
+ if (pos > range.first && pos < range.second) {
+ // not yet done
+ fire_again = true; // fire this callback again
+ }
+ }
+ }
+
+ if (!fire_again) {
+ ptr->_active_timeout = 0;
+ }
+
+ return fire_again;
+}
+
+void ColorPalette::scroll(int dx, int dy, double snap, bool smooth) {
+ if (auto vert = _scroll.get_vscrollbar()) {
+ if (smooth && dy != 0.0) {
+ _scroll_final = vert->get_value() + dy;
+ if (snap > 0) {
+ // round it to whole 'dy' increments
+ _scroll_final -= fmod(_scroll_final, snap);
+ }
+ auto range = get_range(*vert);
+ if (_scroll_final < range.first) {
+ _scroll_final = range.first;
+ }
+ else if (_scroll_final > range.second) {
+ _scroll_final = range.second;
+ }
+ _scroll_step = dy / 4.0;
+ if (!_active_timeout && vert->get_value() != _scroll_final) {
+ // limit refresh to 60 fps, in practice it will be slower
+ _active_timeout = g_timeout_add(1000 / 60, &ColorPalette::scroll_cb, this);
+ }
+ }
+ else {
+ vert->set_value(vert->get_value() + dy);
+ }
+ }
+ if (auto horz = _scroll.get_hscrollbar()) {
+ horz->set_value(horz->get_value() + dx);
+ }
+}
+
+int ColorPalette::get_tile_size() const {
+ return _size;
+}
+
+int ColorPalette::get_tile_border() const {
+ return _border;
+}
+
+int ColorPalette::get_rows() const {
+ return _rows;
+}
+
+double ColorPalette::get_aspect() const {
+ return _aspect;
+}
+
+void ColorPalette::set_tile_border(int border) {
+ _set_tile_border(border);
+ auto& slider = get_widget<Gtk::Scale>(_builder, "border-slider");
+ slider.set_value(border);
+}
+
+void ColorPalette::_set_tile_border(int border) {
+ if (border == _border) return;
+
+ if (border < 0 || border > 100) {
+ g_warning("Unexpected tile border size of color palette: %d", border);
+ return;
+ }
+
+ _border = border;
+ set_up_scrolling();
+}
+
+void ColorPalette::set_tile_size(int size) {
+ _set_tile_size(size);
+ auto& slider = get_widget<Gtk::Scale>(_builder, "size-slider");
+ slider.set_value(size);
+}
+
+void ColorPalette::_set_tile_size(int size) {
+ if (size == _size) return;
+
+ if (size < 1 || size > 1000) {
+ g_warning("Unexpected tile size for color palette: %d", size);
+ return;
+ }
+
+ _size = size;
+ set_up_scrolling();
+}
+
+void ColorPalette::set_aspect(double aspect) {
+ _set_aspect(aspect);
+ auto& slider = get_widget<Gtk::Scale>(_builder, "aspect-slider");
+ slider.set_value(aspect);
+}
+
+void ColorPalette::_set_aspect(double aspect) {
+ if (aspect == _aspect) return;
+
+ if (aspect < -2.0 || aspect > 2.0) {
+ g_warning("Unexpected aspect ratio for color palette: %f", aspect);
+ return;
+ }
+
+ _aspect = aspect;
+ set_up_scrolling();
+}
+
+void ColorPalette::set_rows(int rows) {
+ _set_rows(rows);
+ auto& slider = get_widget<Gtk::Scale>(_builder, "row-slider");
+ slider.set_value(rows);
+}
+
+void ColorPalette::_set_rows(int rows) {
+ if (rows == _rows) return;
+
+ if (rows < 1 || rows > 1000) {
+ g_warning("Unexpected number of rows for color palette: %d", rows);
+ return;
+ }
+
+ _rows = rows;
+ update_checkbox();
+ set_up_scrolling();
+}
+
+void ColorPalette::update_checkbox() {
+ auto& sb = get_widget<Gtk::CheckButton>(_builder, "use-sb");
+ // scrollbar can only be applied to single-row layouts
+ sb.set_sensitive(_rows == 1);
+}
+
+void ColorPalette::set_compact(bool compact) {
+ if (_compact != compact) {
+ _compact = compact;
+ set_up_scrolling();
+
+ get_widget<Gtk::Scale>(_builder, "row-slider").set_visible(compact);
+ get_widget<Gtk::Label>(_builder, "row-label").set_visible(compact);
+ get_widget<Gtk::CheckButton>(_builder, "enlarge").set_visible(compact);
+ get_widget<Gtk::CheckButton>(_builder, "show-labels").set_visible(!compact);
+ }
+}
+
+bool ColorPalette::is_scrollbar_enabled() const {
+ return _force_scrollbar;
+}
+
+bool ColorPalette::is_stretch_enabled() const {
+ return _stretch_tiles;
+}
+
+void ColorPalette::enable_stretch(bool enable) {
+ auto& stretch = get_widget<Gtk::CheckButton>(_builder, "stretch");
+ stretch.set_active(enable);
+ _enable_stretch(enable);
+}
+
+void ColorPalette::_enable_stretch(bool enable) {
+ if (_stretch_tiles == enable) return;
+
+ _stretch_tiles = enable;
+ _normal_box.set_halign(enable ? Gtk::ALIGN_FILL : Gtk::ALIGN_START);
+ update_stretch();
+ set_up_scrolling();
+}
+
+void ColorPalette::enable_labels(bool labels) {
+ auto& sl = get_widget<Gtk::CheckButton>(_builder, "show-labels");
+ sl.set_active(labels);
+ _show_labels = labels;
+}
+
+void ColorPalette::update_stretch() {
+ auto& aspect = get_widget<Gtk::Scale>(_builder, "aspect-slider");
+ aspect.set_sensitive(!_stretch_tiles);
+ auto& label = get_widget<Gtk::Label>(_builder, "aspect-label");
+ label.set_sensitive(!_stretch_tiles);
+}
+
+void ColorPalette::enable_scrollbar(bool show) {
+ auto& sb = get_widget<Gtk::CheckButton>(_builder, "use-sb");
+ sb.set_active(show);
+ _enable_scrollbar(show);
+}
+
+void ColorPalette::_enable_scrollbar(bool show) {
+ if (_force_scrollbar == show) return;
+
+ _force_scrollbar = show;
+ set_up_scrolling();
+}
+
+void ColorPalette::set_up_scrolling() {
+ auto& box = get_widget<Gtk::Box>(_builder, "palette-box");
+ auto& btn_menu = get_widget<Gtk::MenuButton>(_builder, "btn-menu");
+ auto normal_count = std::max(1, static_cast<int>(_normal_box.get_children().size()));
+ auto pinned_count = std::max(1, static_cast<int>(_pinned_box.get_children().size()));
+
+ _normal_box.set_max_children_per_line(_show_labels ? 1 : normal_count);
+ _normal_box.set_min_children_per_line(1);
+ _pinned_box.set_max_children_per_line(_show_labels ? 1 : pinned_count);
+ _pinned_box.set_min_children_per_line(1);
+
+ if (_compact) {
+ box.set_orientation(Gtk::ORIENTATION_HORIZONTAL);
+ btn_menu.set_margin_bottom(0);
+ btn_menu.set_margin_end(0);
+ // in compact mode scrollbars are hidden; they take up too much space
+ set_valign(Gtk::ALIGN_START);
+ set_vexpand(false);
+
+ _scroll.set_valign(Gtk::ALIGN_END);
+ _normal_box.set_valign(Gtk::ALIGN_END);
+
+ if (_rows == 1 && _force_scrollbar) {
+ // horizontal scrolling with single row
+ _normal_box.set_min_children_per_line(normal_count);
+
+ _scroll_btn.hide();
+
+ if (_force_scrollbar) {
+ _scroll_left.hide();
+ _scroll_right.hide();
+ }
+ else {
+ _scroll_left.show();
+ _scroll_right.show();
+ }
+
+ // ideally we should be able to use POLICY_AUTOMATIC, but on some themes this leads to a scrollbar
+ // that obscures color tiles (it overlaps them); thus resorting to manual scrollbar selection
+ _scroll.set_policy(_force_scrollbar ? Gtk::POLICY_ALWAYS : Gtk::POLICY_EXTERNAL, Gtk::POLICY_NEVER);
+ }
+ else {
+ // vertical scrolling with multiple rows
+ // 'external' allows scrollbar to shrink vertically
+ _scroll.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_EXTERNAL);
+ _scroll_left.hide();
+ _scroll_right.hide();
+ _scroll_btn.show();
+ }
+
+ int div = _large_pinned_panel ? (_rows > 2 ? 2 : 1) : _rows;
+ _pinned_box.set_max_children_per_line(std::max((pinned_count + div - 1) / div, 1));
+ _pinned_box.set_margin_end(_border);
+ }
+ else {
+ box.set_orientation(Gtk::ORIENTATION_VERTICAL);
+ btn_menu.set_margin_bottom(2);
+ btn_menu.set_margin_end(2);
+ // in normal mode use regular full-size scrollbars
+ set_valign(Gtk::ALIGN_FILL);
+ set_vexpand(true);
+
+ _scroll_left.hide();
+ _scroll_right.hide();
+ _scroll_btn.hide();
+
+ _normal_box.set_valign(Gtk::ALIGN_START);
+ _scroll.set_valign(Gtk::ALIGN_FILL);
+ // 'always' allocates space for scrollbar
+ _scroll.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS);
+ }
+
+ resize();
+}
+
+int ColorPalette::get_tile_size(bool horz) const {
+ if (_stretch_tiles) return _size;
+
+ double aspect = horz ? _aspect : -_aspect;
+
+ if (aspect > 0) {
+ return static_cast<int>(round((1.0 + aspect) * _size));
+ }
+ else if (aspect < 0) {
+ return static_cast<int>(round((1.0 / (1.0 - aspect)) * _size));
+ }
+ else {
+ return _size;
+ }
+}
+
+int ColorPalette::get_tile_width() const {
+ return get_tile_size(true);
+}
+
+int ColorPalette::get_tile_height() const {
+ return get_tile_size(false);
+}
+
+int ColorPalette::get_palette_height() const {
+ return (get_tile_height() + _border) * _rows;
+}
+
+void ColorPalette::set_large_pinned_panel(bool large) {
+ auto& checkbox = get_widget<Gtk::CheckButton>(_builder, "enlarge");
+ checkbox.set_active(large);
+ _set_large_pinned_panel(large);
+}
+
+void ColorPalette::_set_large_pinned_panel(bool large) {
+ if (_large_pinned_panel == large) return;
+
+ _large_pinned_panel = large;
+ set_up_scrolling();
+}
+
+bool ColorPalette::is_pinned_panel_large() const {
+ return _large_pinned_panel;
+}
+
+bool ColorPalette::are_labels_enabled() const {
+ return _show_labels;
+}
+
+void ColorPalette::resize() {
+ if ((_rows == 1 && _force_scrollbar) || !_compact) {
+ // auto size for single row to allocate space for scrollbar
+ _scroll.set_size_request(-1, -1);
+ }
+ else {
+ // exact size for multiple rows
+ int height = get_palette_height() - _border;
+ _scroll.set_size_request(1, height);
+ }
+
+ _normal_box.set_column_spacing(_border);
+ _normal_box.set_row_spacing(_border);
+ _pinned_box.set_column_spacing(_border);
+ _pinned_box.set_row_spacing(_border);
+
+ double scale = _show_labels ? 2.0 : 1.0;
+
+ int width = get_tile_width() * scale;
+ int height = get_tile_height() * scale;
+ for (auto item : _normal_items) {
+ item->set_size_request(width, height);
+ }
+
+ int pinned_width = width;
+ int pinned_height = height;
+ if (_large_pinned_panel) {
+ double mult = _rows > 2 ? _rows / 2.0 : 2.0;
+ pinned_width = pinned_height = static_cast<int>((height + _border) * mult - _border);
+ }
+ for (auto item : _pinned_items) {
+ item->set_size_request(pinned_width, pinned_height);
+ }
+}
+
+void free_colors(Gtk::FlowBox& flowbox) {
+ for (auto widget : flowbox.get_children()) {
+ if (widget) {
+ flowbox.remove(*widget);
+ }
+ }
+}
+
+void ColorPalette::set_colors(std::vector<Dialog::ColorItem*> const &swatches)
+{
+ _normal_items.clear();
+ _pinned_items.clear();
+
+ for (auto item : swatches) {
+ if (item->is_pinned()) {
+ _pinned_items.emplace_back(item);
+ } else {
+ _normal_items.emplace_back(item);
+ }
+ item->signal_modified().connect([=] {
+ item->get_parent()->foreach([=](Gtk::Widget& w) {
+ if (auto label = dynamic_cast<Gtk::Label *>(&w)) {
+ label->set_text(item->get_description());
+ }
+ });
+ });
+ }
+ rebuild_widgets();
+}
+
+Gtk::Widget *ColorPalette::_get_widget(Dialog::ColorItem *item) {
+ if (auto parent = item->get_parent()) {
+ parent->remove(*item);
+ }
+ if (_show_labels) {
+ item->set_valign(Gtk::ALIGN_CENTER);
+ auto box = Gtk::make_managed<Gtk::Box>();
+ auto label = Gtk::make_managed<Gtk::Label>(item->get_description());
+ box->add(*item);
+ box->add(*label);
+ return box;
+ }
+ return Gtk::manage(item);
+}
+
+void ColorPalette::rebuild_widgets()
+{
+ _normal_box.freeze_notify();
+ _normal_box.freeze_child_notify();
+ _pinned_box.freeze_notify();
+ _pinned_box.freeze_child_notify();
+
+ free_colors(_normal_box);
+ free_colors(_pinned_box);
+
+ for (auto item : _normal_items) {
+ _normal_box.add(*_get_widget(item));
+ }
+ for (auto item : _pinned_items) {
+ _pinned_box.add(*_get_widget(item));
+ }
+
+ _normal_box.show_all();
+ _pinned_box.show_all();
+
+ set_up_scrolling();
+
+ _normal_box.thaw_child_notify();
+ _normal_box.thaw_notify();
+ _pinned_box.thaw_child_notify();
+ _pinned_box.thaw_notify();
+}
+
+class CustomMenuItem : public Gtk::RadioMenuItem {
+public:
+ CustomMenuItem(Gtk::RadioMenuItem::Group& group, const Glib::ustring& label, std::vector<ColorPalette::rgb_t> colors):
+ Gtk::RadioMenuItem(group, label), _colors(std::move(colors)) {
+
+ set_margin_bottom(2);
+ }
+private:
+ bool on_draw(const Cairo::RefPtr<Cairo::Context>& cr) override;
+ std::vector<ColorPalette::rgb_t> _colors;
+};
+
+bool CustomMenuItem::on_draw(const Cairo::RefPtr<Cairo::Context>& cr) {
+ RadioMenuItem::on_draw(cr);
+ if (_colors.empty()) return false;
+
+ auto allocation = get_allocation();
+ auto x = 0;
+ auto y = 0;
+ auto width = allocation.get_width();
+ auto height = allocation.get_height();
+ auto left = x + height;
+ auto right = x + width - height;
+ auto dx = 1;
+ auto dy = 2;
+ auto px = left;
+ auto py = y + height - dy;
+ auto w = right - left;
+ if (w <= 0) return false;
+
+ for (int i = 0; i < w; ++i) {
+ if (px >= right) break;
+
+ int index = i * _colors.size() / w;
+ auto& color = _colors.at(index);
+
+ cr->set_source_rgb(color.r, color.g, color.b);
+ cr->rectangle(px, py, dx, dy);
+ cr->fill();
+
+ px += dx;
+ }
+
+ return false;
+}
+
+void ColorPalette::set_palettes(const std::vector<ColorPalette::palette_t>& palettes) {
+ auto items = _menu.get_children();
+ auto count = items.size();
+
+ int index = 0;
+ while (count > 2) {
+ if (auto item = items[index++]) {
+ _menu.remove(*item);
+ delete item;
+ }
+ count--;
+ }
+
+ Gtk::RadioMenuItem::Group group;
+ for (auto it = palettes.rbegin(); it != palettes.rend(); ++it) {
+ auto& name = it->name;
+ auto item = Gtk::manage(new CustomMenuItem(group, name, it->colors));
+ item->signal_activate().connect([=](){
+ if (!_in_update) {
+ _in_update = true;
+ _signal_palette_selected.emit(name);
+ _in_update = false;
+ }
+ });
+ item->show();
+ _menu.prepend(*item);
+ }
+}
+
+sigc::signal<void (Glib::ustring)>& ColorPalette::get_palette_selected_signal() {
+ return _signal_palette_selected;
+}
+
+sigc::signal<void ()>& ColorPalette::get_settings_changed_signal() {
+ return _signal_settings_changed;
+}
+
+void ColorPalette::set_selected(const Glib::ustring& name) {
+ auto items = _menu.get_children();
+ _in_update = true;
+ for (auto item : items) {
+ if (auto radio = dynamic_cast<Gtk::RadioMenuItem*>(item)) {
+ radio->set_active(radio->get_label() == name);
+ }
+ }
+ _in_update = false;
+}
+
+}}} // namespace
diff --git a/src/ui/widget/color-palette.h b/src/ui/widget/color-palette.h
new file mode 100644
index 0000000..1f20d24
--- /dev/null
+++ b/src/ui/widget/color-palette.h
@@ -0,0 +1,130 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Color palette widget
+ */
+/* Authors:
+ * Michael Kowalski
+ *
+ * Copyright (C) 2021 Michael Kowalski
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_COLOR_PALETTE_H
+#define SEEN_COLOR_PALETTE_H
+
+#include <gtkmm/bin.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/button.h>
+#include <gtkmm/flowbox.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/menu.h>
+#include <vector>
+
+namespace Inkscape {
+namespace UI {
+ namespace Dialog {
+ class ColorItem;
+ };
+
+namespace Widget {
+
+class ColorPalette : public Gtk::Bin {
+public:
+ ColorPalette();
+ ~ColorPalette() override;
+
+ struct rgb_t { double r; double g; double b; };
+ struct palette_t { Glib::ustring name; std::vector<rgb_t> colors; };
+
+ // set colors presented in a palette
+ void set_colors(std::vector<Dialog::ColorItem*> const &swatches);
+ // list of palettes to present in the menu
+ void set_palettes(const std::vector<palette_t>& palettes);
+ // enable compact mode (true) with mini-scroll buttons, or normal mode (false) with regular scrollbars
+ void set_compact(bool compact);
+ // enlarge color tiles in a pinned panel
+ void set_large_pinned_panel(bool large);
+
+ void set_tile_size(int size_px);
+ void set_tile_border(int border_px);
+ void set_rows(int rows);
+ void set_aspect(double aspect);
+ // show horizontal scrollbar when only 1 row is set
+ void enable_scrollbar(bool show);
+ // allow tile stretching (horizontally)
+ void enable_stretch(bool enable);
+ // Show labels in swatches dialog
+ void enable_labels(bool labels);
+
+ int get_tile_size() const;
+ int get_tile_border() const;
+ int get_rows() const;
+ double get_aspect() const;
+ bool is_scrollbar_enabled() const;
+ bool is_stretch_enabled() const;
+ bool is_pinned_panel_large() const;
+ bool are_labels_enabled() const;
+
+ void set_selected(const Glib::ustring& name);
+
+ sigc::signal<void (Glib::ustring)>& get_palette_selected_signal();
+ sigc::signal<void ()>& get_settings_changed_signal();
+
+private:
+ void resize();
+ void set_up_scrolling();
+ void free(Gtk::FlowBox& box);
+ void scroll(int dx, int dy, double snap, bool smooth);
+ void do_scroll(int dx, int dy);
+ static gboolean scroll_cb(gpointer self);
+ void _set_tile_size(int size_px);
+ void _set_tile_border(int border_px);
+ void _set_rows(int rows);
+ void _set_aspect(double aspect);
+ void _enable_scrollbar(bool show);
+ void _enable_stretch(bool enable);
+ void _set_large_pinned_panel(bool large);
+ static gboolean check_scrollbar(gpointer self);
+ void update_checkbox();
+ void update_stretch();
+ int get_tile_size(bool horz) const;
+ int get_tile_width() const;
+ int get_tile_height() const;
+ int get_palette_height() const;
+
+ Gtk::Widget *_get_widget(Dialog::ColorItem *item);
+ void rebuild_widgets();
+
+ std::vector<Dialog::ColorItem *> _normal_items;
+ std::vector<Dialog::ColorItem *> _pinned_items;
+
+ Glib::RefPtr<Gtk::Builder> _builder;
+ Gtk::FlowBox& _normal_box;
+ Gtk::FlowBox& _pinned_box;
+ Gtk::ScrolledWindow& _scroll;
+ Gtk::FlowBox& _scroll_btn;
+ Gtk::Button& _scroll_up;
+ Gtk::Button& _scroll_down;
+ Gtk::Button& _scroll_left;
+ Gtk::Button& _scroll_right;
+ Gtk::Menu& _menu;
+ int _size = 10;
+ int _border = 0;
+ int _rows = 1;
+ double _aspect = 0.0;
+ bool _compact = true;
+ sigc::signal<void (Glib::ustring)> _signal_palette_selected;
+ sigc::signal<void ()> _signal_settings_changed;
+ bool _in_update = false;
+ guint _active_timeout = 0;
+ bool _force_scrollbar = false;
+ bool _stretch_tiles = false;
+ double _scroll_step = 0.0; // smooth scrolling step
+ double _scroll_final = 0.0; // smooth scroll final value
+ bool _large_pinned_panel = false;
+ bool _show_labels = false;
+};
+
+}}} // namespace
+
+#endif // SEEN_COLOR_PALETTE_H
diff --git a/src/ui/widget/color-picker.cpp b/src/ui/widget/color-picker.cpp
new file mode 100644
index 0000000..5794065
--- /dev/null
+++ b/src/ui/widget/color-picker.cpp
@@ -0,0 +1,173 @@
+// 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"
+
+
+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, Gtk::Button* external_button)
+ : _preview(new ColorPreview(rgba))
+ , _title(title)
+ , _rgba(rgba)
+ , _undo(undo)
+ , _colorSelectorDialog("dialogs.colorpickerwindow")
+{
+ Gtk::Button* button = external_button ? external_button : this;
+ _color_selector = nullptr;
+ setupDialog(title);
+ _preview->show();
+ button->add(*Gtk::manage(_preview));
+ // set tooltip if given, otherwise leave original tooltip in place (from external button)
+ if (!tip.empty()) {
+ button->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));
+
+ if (external_button) {
+ external_button->signal_clicked().connect([=](){ on_clicked(); });
+ }
+}
+
+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);
+}
+
+void ColorPicker::setSensitive(bool sensitive) { set_sensitive(sensitive); }
+
+void ColorPicker::setRgba32 (guint32 rgba)
+{
+ if (_in_use) return;
+
+ set_preview(rgba);
+ _rgba = rgba;
+ if (_color_selector)
+ {
+ _updating = true;
+ _selected_color.setValue(rgba);
+ _updating = false;
+ }
+}
+
+void ColorPicker::closeWindow()
+{
+ _colorSelectorDialog.hide();
+}
+
+void ColorPicker::open() {
+ on_clicked();
+}
+
+void ColorPicker::on_clicked()
+{
+ if (!_color_selector) {
+ auto selector = Gtk::manage(new ColorNotebook(_selected_color, _ignore_transparency));
+ selector->set_label(_title);
+ _color_selector = selector;
+ _colorSelectorDialog.get_content_area()->pack_start(*_color_selector, true, true, 0);
+ _color_selector->show();
+ }
+
+ _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();
+ set_preview(rgba);
+
+ if (_undo && SP_ACTIVE_DESKTOP) {
+ DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), /* TODO: annotate */ "color-picker.cpp:129", "");
+ }
+
+ on_changed(rgba);
+ _in_use = false;
+ _rgba = rgba;
+ _changed_signal.emit(rgba);
+}
+
+void ColorPicker::set_preview(guint32 rgba) {
+ _preview->setRgba32(_ignore_transparency ? rgba | 0xff : rgba);
+}
+
+void ColorPicker::use_transparency(bool enable) {
+ _ignore_transparency = !enable;
+ set_preview(_rgba);
+}
+
+guint32 ColorPicker::get_current_color() const {
+ return _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..61b3834
--- /dev/null
+++ b/src/ui/widget/color-picker.h
@@ -0,0 +1,119 @@
+// 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,
+ Gtk::Button* external_button = nullptr);
+
+ ~ColorPicker() override;
+
+ void setRgba32 (guint32 rgba);
+ void setSensitive(bool sensitive);
+ void open();
+ void closeWindow();
+ sigc::connection connectChanged (const sigc::slot<void (guint)>& slot)
+ { return _changed_signal.connect (slot); }
+ void use_transparency(bool enable);
+ guint32 get_current_color() const;
+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;
+
+private:
+ void set_preview(guint32 rgba);
+
+ Gtk::Widget *_color_selector;
+ bool _ignore_transparency = false;
+};
+
+
+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)) {}
+
+ 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..ab81c60
--- /dev/null
+++ b/src/ui/widget/color-preview.cpp
@@ -0,0 +1,172 @@
+// 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() - 1;
+
+ 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_arc(ct, carea.x + carea.width / 2, carea.y + carea.height / 2, carea.width / 2, 0, 2 * M_PI);
+ 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..5a2fe42
--- /dev/null
+++ b/src/ui/widget/color-scales.cpp
@@ -0,0 +1,1247 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Color selector using sliders for each components, for multiple color modes
+ *//*
+ * Authors:
+ * see git history
+ * bulia byak <buliabyak@users.sf.net>
+ * Massinissa Derriche <massinissa.derriche@gmail.com>
+ *
+ * Copyright (C) 2018, 2021 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/ustring.h>
+#include <gtkmm/adjustment.h>
+#include <gtkmm/spinbutton.h>
+#include <gtkmm/grid.h>
+#include <glibmm/i18n.h>
+#include <functional>
+#include <memory>
+#include <stdexcept>
+#include <vector>
+
+#include "ui/dialog-events.h"
+#include "ui/selected-color.h"
+#include "ui/widget/color-scales.h"
+#include "ui/widget/color-slider.h"
+#include "ui/widget/color-icc-selector.h"
+#include "ui/widget/scrollprotected.h"
+#include "ui/icon-loader.h"
+#include "oklab.h"
+#include "preferences.h"
+
+#include "ui/widget/ink-color-wheel.h"
+#include "ui/widget/oklab-color-wheel.h"
+
+static int const CSC_CHANNEL_R = (1 << 0);
+static int const CSC_CHANNEL_G = (1 << 1);
+static int const CSC_CHANNEL_B = (1 << 2);
+static int const CSC_CHANNEL_A = (1 << 3);
+static int const CSC_CHANNEL_H = (1 << 0);
+static int const CSC_CHANNEL_S = (1 << 1);
+static int const CSC_CHANNEL_V = (1 << 2);
+static int const CSC_CHANNEL_C = (1 << 0);
+static int const CSC_CHANNEL_M = (1 << 1);
+static int const CSC_CHANNEL_Y = (1 << 2);
+static int const CSC_CHANNEL_K = (1 << 3);
+static int const CSC_CHANNEL_CMYKA = (1 << 4);
+
+static int const CSC_CHANNELS_ALL = 0;
+
+static int const XPAD = 2;
+static int const YPAD = 2;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+
+static guchar const *sp_color_scales_hue_map();
+static guchar const *sp_color_scales_hsluv_map(guchar *map,
+ std::function<void(float*, float)> callback);
+
+static const char* color_mode_icons[] = {
+ nullptr,
+ "color-selector-rgb",
+ "color-selector-hsx",
+ "color-selector-cmyk",
+ "color-selector-hsx",
+ "color-selector-hsluv",
+ "color-selector-okhsl",
+ "color-selector-cms",
+ nullptr
+};
+
+const char* color_mode_name[] = {
+ N_("None"), N_("RGB"), N_("HSL"), N_("CMYK"), N_("HSV"), N_("HSLuv"), N_("OKHSL"), N_("CMS"), nullptr
+};
+
+const char* get_color_mode_icon(SPColorScalesMode mode) {
+ auto index = static_cast<size_t>(mode);
+ assert(index > 0 && index < (sizeof(color_mode_icons) / sizeof(color_mode_icons[0])));
+ return color_mode_icons[index];
+}
+
+const char* get_color_mode_label(SPColorScalesMode mode) {
+ auto index = static_cast<size_t>(mode);
+ assert(index > 0 && index < (sizeof(color_mode_name) / sizeof(color_mode_name[0])));
+ return color_mode_name[index];
+}
+
+std::unique_ptr<Inkscape::UI::ColorSelectorFactory> get_factory(SPColorScalesMode mode) {
+ switch (mode) {
+ case SPColorScalesMode::RGB: return std::make_unique<ColorScalesFactory<SPColorScalesMode::RGB>>();
+ case SPColorScalesMode::HSL: return std::make_unique<ColorScalesFactory<SPColorScalesMode::HSL>>();
+ case SPColorScalesMode::HSV: return std::make_unique<ColorScalesFactory<SPColorScalesMode::HSV>>();
+ case SPColorScalesMode::CMYK: return std::make_unique<ColorScalesFactory<SPColorScalesMode::CMYK>>();
+ case SPColorScalesMode::HSLUV: return std::make_unique<ColorScalesFactory<SPColorScalesMode::HSLUV>>();
+ case SPColorScalesMode::OKLAB: return std::make_unique<ColorScalesFactory<SPColorScalesMode::OKLAB>>();
+ case SPColorScalesMode::CMS: return std::make_unique<ColorICCSelectorFactory>();
+ default:
+ throw std::invalid_argument("There's no factory for the requested color mode");
+ }
+}
+
+std::vector<ColorPickerDescription> get_color_pickers() {
+ std::vector<ColorPickerDescription> pickers;
+
+ for (auto mode : {
+ SPColorScalesMode::HSL,
+ SPColorScalesMode::HSV,
+ SPColorScalesMode::RGB,
+ SPColorScalesMode::CMYK,
+ SPColorScalesMode::OKLAB,
+ SPColorScalesMode::HSLUV,
+ SPColorScalesMode::CMS
+ }) {
+ auto label = get_color_mode_label(mode);
+
+ pickers.emplace_back(ColorPickerDescription {
+ mode,
+ get_color_mode_icon(mode),
+ label,
+ Glib::ustring::format("/colorselector/", label, "/visible"),
+ get_factory(mode)
+ });
+ }
+
+ return pickers;
+}
+
+
+template <SPColorScalesMode MODE>
+gchar const *ColorScales<MODE>::SUBMODE_NAMES[] = { N_("None"), N_("RGB"), N_("HSL"),
+ N_("CMYK"), N_("HSV"), N_("HSLuv"), N_("OKHSL") };
+
+// Preference name for the saved state of toggle-able color wheel
+template <>
+gchar const * const ColorScales<SPColorScalesMode::HSL>::_pref_wheel_visibility =
+ "/wheel_vis_hsl";
+
+template <>
+gchar const * const ColorScales<SPColorScalesMode::HSV>::_pref_wheel_visibility =
+ "/wheel_vis_hsv";
+
+template <>
+gchar const * const ColorScales<SPColorScalesMode::HSLUV>::_pref_wheel_visibility =
+ "/wheel_vis_hsluv";
+
+template <>
+gchar const * const ColorScales<SPColorScalesMode::OKLAB>::_pref_wheel_visibility =
+ "/wheel_vis_okhsl";
+
+template <SPColorScalesMode MODE>
+ColorScales<MODE>::ColorScales(SelectedColor &color, bool no_alpha)
+ : Gtk::Box()
+ , _color(color)
+ , _range_limit(255.0)
+ , _updating(false)
+ , _dragging(false)
+ , _wheel(nullptr)
+{
+ for (gint i = 0; i < 5; i++) {
+ _l[i] = nullptr;
+ _s[i] = nullptr;
+ _b[i] = nullptr;
+ }
+
+ _initUI(no_alpha);
+
+ _color_changed = _color.signal_changed.connect([this](){ _onColorChanged(); });
+ _color_dragged = _color.signal_dragged.connect([this](){ _onColorChanged(); });
+}
+
+template <SPColorScalesMode MODE>
+ColorScales<MODE>::~ColorScales()
+{
+ _color_changed.disconnect();
+ _color_dragged.disconnect();
+
+ for (gint i = 0; i < 5; i++) {
+ _l[i] = nullptr;
+ _s[i] = nullptr;
+ _b[i] = nullptr;
+ }
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::_initUI(bool no_alpha)
+{
+ set_orientation(Gtk::ORIENTATION_VERTICAL);
+
+ Gtk::Expander *wheel_frame = nullptr;
+
+ if constexpr (
+ MODE == SPColorScalesMode::HSL ||
+ MODE == SPColorScalesMode::HSV ||
+ MODE == SPColorScalesMode::HSLUV ||
+ MODE == SPColorScalesMode::OKLAB)
+ {
+ /* Create wheel */
+ if constexpr (MODE == SPColorScalesMode::HSLUV) {
+ _wheel = Gtk::manage(new Inkscape::UI::Widget::ColorWheelHSLuv());
+ } else if constexpr (MODE == SPColorScalesMode::OKLAB) {
+ _wheel = Gtk::make_managed<OKWheel>();
+ } else {
+ _wheel = Gtk::manage(new Inkscape::UI::Widget::ColorWheelHSL());
+ }
+
+ _wheel->show();
+ _wheel->set_halign(Gtk::ALIGN_FILL);
+ _wheel->set_valign(Gtk::ALIGN_FILL);
+ _wheel->set_hexpand(true);
+ _wheel->set_vexpand(true);
+ _wheel->set_name("ColorWheel");
+ _wheel->set_size_request(-1, 130); // minimal size
+
+ /* Signal */
+ _wheel->signal_color_changed().connect([this](){ _wheelChanged(); });
+
+ /* Expander */
+ // Label icon
+ Gtk::Image *expander_icon = Gtk::manage(
+ sp_get_icon_image("color-wheel", Gtk::ICON_SIZE_BUTTON)
+ );
+ expander_icon->show();
+ expander_icon->set_margin_start(2 * XPAD);
+ expander_icon->set_margin_end(3 * XPAD);
+ // Label
+ Gtk::Label *expander_label = Gtk::manage(new Gtk::Label(_("Color Wheel")));
+ expander_label->show();
+ // Content
+ Gtk::Box *expander_box = Gtk::manage(new Gtk::Box());
+ expander_box->show();
+ expander_box->pack_start(*expander_icon);
+ expander_box->pack_start(*expander_label);
+ expander_box->set_orientation(Gtk::ORIENTATION_HORIZONTAL);
+ // Expander
+ wheel_frame = Gtk::manage(new Gtk::Expander());
+ wheel_frame->show();
+ wheel_frame->set_margin_start(2 * XPAD);
+ wheel_frame->set_margin_end(XPAD);
+ wheel_frame->set_margin_top(2 * YPAD);
+ wheel_frame->set_margin_bottom(2 * YPAD);
+ wheel_frame->set_halign(Gtk::ALIGN_FILL);
+ wheel_frame->set_valign(Gtk::ALIGN_FILL);
+ wheel_frame->set_hexpand(true);
+ wheel_frame->set_vexpand(false);
+ wheel_frame->set_label_widget(*expander_box);
+
+ // Signal
+ wheel_frame->property_expanded().signal_changed().connect([=](){
+ bool visible = wheel_frame->get_expanded();
+ wheel_frame->set_vexpand(visible);
+
+ // Save wheel visibility
+ Inkscape::Preferences::get()->setBool(_prefs + _pref_wheel_visibility, visible);
+ });
+
+ wheel_frame->add(*_wheel);
+ add(*wheel_frame);
+ }
+
+ /* Create sliders */
+ Gtk::Grid *grid = Gtk::manage(new Gtk::Grid());
+ grid->show();
+ add(*grid);
+
+ for (gint i = 0; i < 5; i++) {
+ /* Label */
+ _l[i] = Gtk::manage(new Gtk::Label("", true));
+
+ _l[i]->set_halign(Gtk::ALIGN_START);
+ _l[i]->show();
+
+ _l[i]->set_margin_start(2 * XPAD);
+ _l[i]->set_margin_end(XPAD);
+ _l[i]->set_margin_top(YPAD);
+ _l[i]->set_margin_bottom(YPAD);
+ grid->attach(*_l[i], 0, i, 1, 1);
+
+ /* Adjustment */
+ _a.push_back(Gtk::Adjustment::create(0.0, 0.0, _range_limit, 1.0, 10.0, 10.0));
+ /* Slider */
+ _s[i] = Gtk::manage(new Inkscape::UI::Widget::ColorSlider(_a[i]));
+ _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);
+ grid->attach(*_s[i], 1, i, 1, 1);
+
+ /* Spinbutton */
+ _b[i] = Gtk::manage(new ScrollProtected<Gtk::SpinButton>(_a[i], 1.0));
+ sp_dialog_defocus_on_enter(_b[i]->gobj());
+ _l[i]->set_mnemonic_widget(*_b[i]);
+ _b[i]->show();
+
+ _b[i]->set_margin_start(XPAD);
+ _b[i]->set_margin_end(XPAD);
+ _b[i]->set_margin_top(YPAD);
+ _b[i]->set_margin_bottom(YPAD);
+ _b[i]->set_halign(Gtk::ALIGN_END);
+ _b[i]->set_valign(Gtk::ALIGN_CENTER);
+ grid->attach(*_b[i], 2, i, 1, 1);
+
+ /* Signals */
+ _a[i]->signal_value_changed().connect([this, i](){ _adjustmentChanged(i); });
+ _s[i]->signal_grabbed.connect([this](){ _sliderAnyGrabbed(); });
+ _s[i]->signal_released.connect([this](){ _sliderAnyReleased(); });
+ _s[i]->signal_value_changed.connect([this](){ _sliderAnyChanged(); });
+ }
+
+ // Prevent 5th bar from being shown by PanelDialog::show_all_children
+ _l[4]->set_no_show_all(true);
+ _s[4]->set_no_show_all(true);
+ _b[4]->set_no_show_all(true);
+
+ setupMode(no_alpha);
+
+ if constexpr (
+ MODE == SPColorScalesMode::HSL ||
+ MODE == SPColorScalesMode::HSV ||
+ MODE == SPColorScalesMode::HSLUV ||
+ MODE == SPColorScalesMode::OKLAB)
+ {
+ // Restore the visibility of the wheel
+ bool visible = Inkscape::Preferences::get()->getBool(_prefs + _pref_wheel_visibility,
+ false);
+ wheel_frame->set_expanded(visible);
+ wheel_frame->set_vexpand(visible);
+ }
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::_recalcColor()
+{
+ SPColor color;
+ gfloat alpha = 1.0;
+ gfloat c[5];
+
+ if constexpr (
+ MODE == SPColorScalesMode::RGB ||
+ MODE == SPColorScalesMode::HSL ||
+ MODE == SPColorScalesMode::HSV ||
+ MODE == SPColorScalesMode::HSLUV ||
+ MODE == SPColorScalesMode::OKLAB)
+ {
+ _getRgbaFloatv(c);
+ color.set(c[0], c[1], c[2]);
+ alpha = c[3];
+ } else if constexpr (MODE == SPColorScalesMode::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];
+ } else {
+ g_warning("file %s: line %d: Illegal color selector mode NONE", __FILE__, __LINE__);
+ }
+
+ _color.setColorAlpha(color, alpha);
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::_updateDisplay(bool update_wheel)
+{
+#ifdef DUMP_CHANGE_INFO
+ g_message("ColorScales::_onColorChanged( this=%p, %f, %f, %f, %f) %d", this,
+ _color.color().v.c[0],
+ _color.color().v.c[1], _color.color().v.c[2], _color.alpha(), int(update_wheel);
+#endif
+
+ gfloat tmp[3];
+ gfloat c[5] = { 0.0, 0.0, 0.0, 0.0 };
+
+ SPColor color = _color.color();
+
+ if constexpr (MODE == SPColorScalesMode::RGB) {
+ color.get_rgb_floatv(c);
+ c[3] = _color.alpha();
+ c[4] = 0.0;
+ } else if constexpr (MODE == SPColorScalesMode::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;
+ if (update_wheel) { _wheel->setRgb(tmp[0], tmp[1], tmp[2]); }
+ } else if constexpr (MODE == SPColorScalesMode::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;
+ if (update_wheel) { _wheel->setRgb(tmp[0], tmp[1], tmp[2]); }
+ } else if constexpr (MODE == SPColorScalesMode::CMYK) {
+ color.get_cmyk_floatv(c);
+ c[4] = _color.alpha();
+ } else if constexpr (MODE == SPColorScalesMode::HSLUV) {
+ color.get_rgb_floatv(tmp);
+ SPColor::rgb_to_hsluv_floatv(c, tmp[0], tmp[1], tmp[2]);
+ c[3] = _color.alpha();
+ c[4] = 0.0;
+ if (update_wheel) { _wheel->setRgb(tmp[0], tmp[1], tmp[2]); }
+ } else if constexpr (MODE == SPColorScalesMode::OKLAB) {
+ color.get_rgb_floatv(tmp);
+ // OKLab color space is more sensitive to numerical errors; use doubles.
+ auto const hsl = Oklab::oklab_to_okhsl(Oklab::rgb_to_oklab({tmp[0], tmp[1], tmp[2]}));
+ _updating = true;
+ for (size_t i : {0, 1, 2}) {
+ setScaled(_a[i], hsl[i]);
+ }
+ setScaled(_a[3], _color.alpha());
+ setScaled(_a[4], 0.0);
+ _updateSliders(CSC_CHANNELS_ALL);
+ _updating = false;
+ if (update_wheel) {
+ _wheel->setRgb(tmp[0], tmp[1], tmp[2]);
+ }
+ return;
+ } else {
+ g_warning("file %s: line %d: Illegal color selector mode NONE", __FILE__, __LINE__);
+ }
+
+ _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 */
+template <SPColorScalesMode MODE>
+double ColorScales<MODE>::getScaled(Glib::RefPtr<Gtk::Adjustment> const &a)
+{
+ return a->get_value() / a->get_upper();
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::setScaled(Glib::RefPtr<Gtk::Adjustment> &a, double v, bool constrained)
+{
+ auto upper = a->get_upper();
+ double 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;
+ }
+ }
+ a->set_value(val);
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::_setRangeLimit(gdouble upper)
+{
+ _range_limit = upper;
+ for (auto & i : _a) {
+ i->set_upper(upper);
+ }
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::_onColorChanged()
+{
+ if (!get_visible()) { return; }
+
+ _updateDisplay();
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::on_show()
+{
+ Gtk::Box::on_show();
+
+ _updateDisplay();
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::_getRgbaFloatv(gfloat *rgba)
+{
+ g_return_if_fail(rgba != nullptr);
+
+ if constexpr (MODE == SPColorScalesMode::RGB) {
+ rgba[0] = getScaled(_a[0]);
+ rgba[1] = getScaled(_a[1]);
+ rgba[2] = getScaled(_a[2]);
+ rgba[3] = getScaled(_a[3]);
+ } else if constexpr (MODE == SPColorScalesMode::HSL) {
+ SPColor::hsl_to_rgb_floatv(rgba, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]));
+ rgba[3] = getScaled(_a[3]);
+ } else if constexpr (MODE == SPColorScalesMode::HSV) {
+ SPColor::hsv_to_rgb_floatv(rgba, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]));
+ rgba[3] = getScaled(_a[3]);
+ } else if constexpr (MODE == SPColorScalesMode::CMYK) {
+ SPColor::cmyk_to_rgb_floatv(rgba, getScaled(_a[0]), getScaled(_a[1]),
+ getScaled(_a[2]), getScaled(_a[3]));
+ rgba[3] = getScaled(_a[4]);
+ } else if constexpr (MODE == SPColorScalesMode::HSLUV) {
+ SPColor::hsluv_to_rgb_floatv(rgba, getScaled(_a[0]), getScaled(_a[1]),
+ getScaled(_a[2]));
+ rgba[3] = getScaled(_a[3]);
+ } else if constexpr (MODE == SPColorScalesMode::OKLAB) {
+ auto const tmp = Oklab::oklab_to_rgb(
+ Oklab::okhsl_to_oklab({ getScaled(_a[0]),
+ getScaled(_a[1]),
+ getScaled(_a[2]) }));
+ for (size_t i : {0, 1, 2}) {
+ rgba[i] = static_cast<float>(tmp[i]);
+ }
+ rgba[3] = getScaled(_a[3]);
+ } else {
+ g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__);
+ }
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::_getCmykaFloatv(gfloat *cmyka)
+{
+ gfloat rgb[3];
+
+ g_return_if_fail(cmyka != nullptr);
+
+ if constexpr (MODE == SPColorScalesMode::RGB) {
+ SPColor::rgb_to_cmyk_floatv(cmyka, getScaled(_a[0]), getScaled(_a[1]),
+ getScaled(_a[2]));
+ cmyka[4] = getScaled(_a[3]);
+ } else if constexpr (MODE == SPColorScalesMode::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]);
+ } else if constexpr (MODE == SPColorScalesMode::HSLUV) {
+ SPColor::hsluv_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]);
+ } else if constexpr (MODE == SPColorScalesMode::OKLAB) {
+ auto const tmp = Oklab::oklab_to_rgb(
+ Oklab::okhsl_to_oklab({ getScaled(_a[0]),
+ getScaled(_a[1]),
+ getScaled(_a[2]) }));
+ SPColor::rgb_to_cmyk_floatv(cmyka, (float)tmp[0], (float)tmp[1], (float)tmp[2]);
+ cmyka[4] = getScaled(_a[3]);
+ } else if constexpr (MODE == SPColorScalesMode::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]);
+ } else {
+ g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__);
+ }
+}
+
+template <SPColorScalesMode MODE>
+guint32 ColorScales<MODE>::_getRgba32()
+{
+ gfloat c[4];
+ guint32 rgba;
+
+ _getRgbaFloatv(c);
+
+ rgba = SP_RGBA32_F_COMPOSE(c[0], c[1], c[2], c[3]);
+
+ return rgba;
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::setupMode(bool no_alpha)
+{
+ gfloat rgba[4];
+ gfloat c[4];
+ int alpha_index = 0;
+
+ if constexpr (MODE == SPColorScalesMode::NONE) {
+ rgba[0] = rgba[1] = rgba[2] = rgba[3] = 1.0;
+ } else {
+ _getRgbaFloatv(rgba);
+ }
+
+ if constexpr (MODE == SPColorScalesMode::RGB) {
+ _setRangeLimit(255.0);
+ _a[3]->set_upper(100.0);
+ _l[0]->set_markup_with_mnemonic(_("_R:"));
+ _s[0]->set_tooltip_text(_("Red"));
+ _b[0]->set_tooltip_text(_("Red"));
+ _l[1]->set_markup_with_mnemonic(_("_G:"));
+ _s[1]->set_tooltip_text(_("Green"));
+ _b[1]->set_tooltip_text(_("Green"));
+ _l[2]->set_markup_with_mnemonic(_("_B:"));
+ _s[2]->set_tooltip_text(_("Blue"));
+ _b[2]->set_tooltip_text(_("Blue"));
+ alpha_index = 3;
+ _l[3]->set_markup_with_mnemonic(_("_A:"));
+ _s[3]->set_tooltip_text(_("Alpha (opacity)"));
+ _b[3]->set_tooltip_text(_("Alpha (opacity)"));
+ _s[0]->setMap(nullptr);
+ _l[4]->hide();
+ _s[4]->hide();
+ _b[4]->hide();
+ _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;
+ } else if constexpr (MODE == SPColorScalesMode::HSL) {
+ _setRangeLimit(100.0);
+
+ _l[0]->set_markup_with_mnemonic(_("_H:"));
+ _s[0]->set_tooltip_text(_("Hue"));
+ _b[0]->set_tooltip_text(_("Hue"));
+ _a[0]->set_upper(360.0);
+
+ _l[1]->set_markup_with_mnemonic(_("_S:"));
+ _s[1]->set_tooltip_text(_("Saturation"));
+ _b[1]->set_tooltip_text(_("Saturation"));
+
+ _l[2]->set_markup_with_mnemonic(_("_L:"));
+ _s[2]->set_tooltip_text(_("Lightness"));
+ _b[2]->set_tooltip_text(_("Lightness"));
+
+ alpha_index = 3;
+ _l[3]->set_markup_with_mnemonic(_("_A:"));
+ _s[3]->set_tooltip_text(_("Alpha (opacity)"));
+ _b[3]->set_tooltip_text(_("Alpha (opacity)"));
+ _s[0]->setMap(sp_color_scales_hue_map());
+ _l[4]->hide();
+ _s[4]->hide();
+ _b[4]->hide();
+ _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;
+ } else if constexpr (MODE == SPColorScalesMode::HSV) {
+ _setRangeLimit(100.0);
+
+ _l[0]->set_markup_with_mnemonic(_("_H:"));
+ _s[0]->set_tooltip_text(_("Hue"));
+ _b[0]->set_tooltip_text(_("Hue"));
+ _a[0]->set_upper(360.0);
+
+ _l[1]->set_markup_with_mnemonic(_("_S:"));
+ _s[1]->set_tooltip_text(_("Saturation"));
+ _b[1]->set_tooltip_text(_("Saturation"));
+
+ _l[2]->set_markup_with_mnemonic(_("_V:"));
+ _s[2]->set_tooltip_text(_("Value"));
+ _b[2]->set_tooltip_text(_("Value"));
+
+ alpha_index = 3;
+ _l[3]->set_markup_with_mnemonic(_("_A:"));
+ _s[3]->set_tooltip_text(_("Alpha (opacity)"));
+ _b[3]->set_tooltip_text(_("Alpha (opacity)"));
+ _s[0]->setMap(sp_color_scales_hue_map());
+ _l[4]->hide();
+ _s[4]->hide();
+ _b[4]->hide();
+ _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;
+ } else if constexpr (MODE == SPColorScalesMode::CMYK) {
+ _setRangeLimit(100.0);
+ _l[0]->set_markup_with_mnemonic(_("_C:"));
+ _s[0]->set_tooltip_text(_("Cyan"));
+ _b[0]->set_tooltip_text(_("Cyan"));
+
+ _l[1]->set_markup_with_mnemonic(_("_M:"));
+ _s[1]->set_tooltip_text(_("Magenta"));
+ _b[1]->set_tooltip_text(_("Magenta"));
+
+ _l[2]->set_markup_with_mnemonic(_("_Y:"));
+ _s[2]->set_tooltip_text(_("Yellow"));
+ _b[2]->set_tooltip_text(_("Yellow"));
+
+ _l[3]->set_markup_with_mnemonic(_("_K:"));
+ _s[3]->set_tooltip_text(_("Black"));
+ _b[3]->set_tooltip_text(_("Black"));
+
+ alpha_index = 4;
+ _l[4]->set_markup_with_mnemonic(_("_A:"));
+ _s[4]->set_tooltip_text(_("Alpha (opacity)"));
+ _b[4]->set_tooltip_text(_("Alpha (opacity)"));
+
+ _s[0]->setMap(nullptr);
+ _l[4]->show();
+ _s[4]->show();
+ _b[4]->show();
+ _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;
+ } else if constexpr (MODE == SPColorScalesMode::HSLUV) {
+ _setRangeLimit(100.0);
+
+ _l[0]->set_markup_with_mnemonic(_("_H*:"));
+ _s[0]->set_tooltip_text(_("Hue"));
+ _b[0]->set_tooltip_text(_("Hue"));
+ _a[0]->set_upper(360.0);
+
+ _l[1]->set_markup_with_mnemonic(_("_S*:"));
+ _s[1]->set_tooltip_text(_("Saturation"));
+ _b[1]->set_tooltip_text(_("Saturation"));
+
+ _l[2]->set_markup_with_mnemonic(_("_L*:"));
+ _s[2]->set_tooltip_text(_("Lightness"));
+ _b[2]->set_tooltip_text(_("Lightness"));
+
+ alpha_index = 3;
+ _l[3]->set_markup_with_mnemonic(_("_A:"));
+ _s[3]->set_tooltip_text(_("Alpha (opacity)"));
+ _b[3]->set_tooltip_text(_("Alpha (opacity)"));
+
+ _s[0]->setMap(hsluvHueMap(0.0f, 0.0f, &_sliders_maps[0]));
+ _s[1]->setMap(hsluvSaturationMap(0.0f, 0.0f, &_sliders_maps[1]));
+ _s[2]->setMap(hsluvLightnessMap(0.0f, 0.0f, &_sliders_maps[2]));
+
+ _l[4]->hide();
+ _s[4]->hide();
+ _b[4]->hide();
+ _updating = true;
+ c[0] = 0.0;
+
+ SPColor::rgb_to_hsluv_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;
+ } else if constexpr (MODE == SPColorScalesMode::OKLAB) {
+ _setRangeLimit(100.0);
+
+ _l[0]->set_markup_with_mnemonic(_("_H<sub>OK</sub>:"));
+ _s[0]->set_tooltip_text(_("Hue"));
+ _b[0]->set_tooltip_text(_("Hue"));
+ _a[0]->set_upper(360.0);
+
+ _l[1]->set_markup_with_mnemonic(_("_S<sub>OK</sub>:"));
+ _s[1]->set_tooltip_text(_("Saturation"));
+ _b[1]->set_tooltip_text(_("Saturation"));
+
+ _l[2]->set_markup_with_mnemonic(_("_L<sub>OK</sub>:"));
+ _s[2]->set_tooltip_text(_("Lightness"));
+ _b[2]->set_tooltip_text(_("Lightness"));
+
+ alpha_index = 3;
+ _l[3]->set_markup_with_mnemonic(_("_A:"));
+ _s[3]->set_tooltip_text(_("Alpha (opacity)"));
+ _b[3]->set_tooltip_text(_("Alpha (opacity)"));
+
+ _l[4]->hide();
+ _s[4]->hide();
+ _b[4]->hide();
+ _updating = true;
+
+ auto const tmp = Oklab::oklab_to_okhsl(Oklab::rgb_to_oklab({rgba[0], rgba[1], rgba[2]}));
+ for (size_t i : {0, 1, 2}) {
+ setScaled(_a[i], tmp[i]);
+ }
+ setScaled(_a[3], rgba[3]);
+
+ _updateSliders(CSC_CHANNELS_ALL);
+ _updating = false;
+ } else {
+ g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__);
+ }
+
+ if (no_alpha && alpha_index > 0) {
+ _l[alpha_index]->hide();
+ _s[alpha_index]->hide();
+ _b[alpha_index]->hide();
+ _l[alpha_index]->set_no_show_all(true);
+ _s[alpha_index]->set_no_show_all(true);
+ _b[alpha_index]->set_no_show_all(true);
+ }
+}
+
+template <SPColorScalesMode MODE>
+SPColorScalesMode ColorScales<MODE>::getMode() const { return MODE; }
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::_sliderAnyGrabbed()
+{
+ if (_updating) { return; }
+
+ if (!_dragging) {
+ _dragging = true;
+ _color.setHeld(true);
+ }
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::_sliderAnyReleased()
+{
+ if (_updating) { return; }
+
+ if (_dragging) {
+ _dragging = false;
+ _color.setHeld(false);
+ }
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::_sliderAnyChanged()
+{
+ if (_updating) { return; }
+
+ _recalcColor();
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::_adjustmentChanged(int channel)
+{
+ if (_updating) { return; }
+
+ _updateSliders((1 << channel));
+ _recalcColor();
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::_wheelChanged()
+{
+ if constexpr (
+ MODE == SPColorScalesMode::NONE ||
+ MODE == SPColorScalesMode::RGB ||
+ MODE == SPColorScalesMode::CMYK)
+ {
+ return;
+ }
+
+ if (_updating) { return; }
+
+ _updating = true;
+
+ double rgb[3];
+ _wheel->getRgbV(rgb);
+ SPColor color(rgb[0], rgb[1], rgb[2]);
+
+ _color_changed.block();
+ _color_dragged.block();
+
+ // Color
+ _color.setHeld(_wheel->isAdjusting());
+ _color.setColor(color);
+
+ // Sliders
+ _updateDisplay(false);
+
+ _color_changed.unblock();
+ _color_dragged.unblock();
+
+ _updating = false;
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::_updateSliders(guint channels)
+{
+ gfloat rgb0[3], rgbm[3], rgb1[3];
+
+#ifdef SPCS_PREVIEW
+ guint32 rgba;
+#endif
+
+ std::array<double, 4> const adj = [this]() -> std::array<double, 4> {
+ if constexpr (MODE == SPColorScalesMode::CMYK) {
+ return { getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]), getScaled(_a[3]) };
+ } else {
+ return { getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]), 0.0 };
+ }
+ }();
+
+ if constexpr (MODE == SPColorScalesMode::RGB) {
+ if ((channels != CSC_CHANNEL_R) && (channels != CSC_CHANNEL_A)) {
+ /* Update red */
+ _s[0]->setColors(SP_RGBA32_F_COMPOSE(0.0, adj[1], adj[2], 1.0),
+ SP_RGBA32_F_COMPOSE(0.5, adj[1], adj[2], 1.0),
+ SP_RGBA32_F_COMPOSE(1.0, adj[1], adj[2], 1.0));
+ }
+ if ((channels != CSC_CHANNEL_G) && (channels != CSC_CHANNEL_A)) {
+ /* Update green */
+ _s[1]->setColors(SP_RGBA32_F_COMPOSE(adj[0], 0.0, adj[2], 1.0),
+ SP_RGBA32_F_COMPOSE(adj[0], 0.5, adj[2], 1.0),
+ SP_RGBA32_F_COMPOSE(adj[0], 1.0, adj[2], 1.0));
+ }
+ if ((channels != CSC_CHANNEL_B) && (channels != CSC_CHANNEL_A)) {
+ /* Update blue */
+ _s[2]->setColors(SP_RGBA32_F_COMPOSE(adj[0], adj[1], 0.0, 1.0),
+ SP_RGBA32_F_COMPOSE(adj[0], adj[1], 0.5, 1.0),
+ SP_RGBA32_F_COMPOSE(adj[0], adj[1], 1.0, 1.0));
+ }
+ if (channels != CSC_CHANNEL_A) {
+ /* Update alpha */
+ _s[3]->setColors(SP_RGBA32_F_COMPOSE(adj[0], adj[1], adj[2], 0.0),
+ SP_RGBA32_F_COMPOSE(adj[0], adj[1], adj[2], 0.5),
+ SP_RGBA32_F_COMPOSE(adj[0], adj[1], adj[2], 1.0));
+ }
+ } else if constexpr (MODE == SPColorScalesMode::HSL) {
+ /* Hue is never updated */
+ if ((channels != CSC_CHANNEL_S) && (channels != CSC_CHANNEL_A)) {
+ /* Update saturation */
+ SPColor::hsl_to_rgb_floatv(rgb0, adj[0], 0.0, adj[2]);
+ SPColor::hsl_to_rgb_floatv(rgbm, adj[0], 0.5, adj[2]);
+ SPColor::hsl_to_rgb_floatv(rgb1, adj[0], 1.0, adj[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, adj[0], adj[1], 0.0);
+ SPColor::hsl_to_rgb_floatv(rgbm, adj[0], adj[1], 0.5);
+ SPColor::hsl_to_rgb_floatv(rgb1, adj[0], adj[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, adj[0], adj[1], adj[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));
+ }
+ } else if constexpr (MODE == SPColorScalesMode::HSV) {
+ /* Hue is never updated */
+ if ((channels != CSC_CHANNEL_S) && (channels != CSC_CHANNEL_A)) {
+ /* Update saturation */
+ SPColor::hsv_to_rgb_floatv(rgb0, adj[0], 0.0, adj[2]);
+ SPColor::hsv_to_rgb_floatv(rgbm, adj[0], 0.5, adj[2]);
+ SPColor::hsv_to_rgb_floatv(rgb1, adj[0], 1.0, adj[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, adj[0], adj[1], 0.0);
+ SPColor::hsv_to_rgb_floatv(rgbm, adj[0], adj[1], 0.5);
+ SPColor::hsv_to_rgb_floatv(rgb1, adj[0], adj[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, adj[0], adj[1], adj[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));
+ }
+ } else if constexpr (MODE == SPColorScalesMode::CMYK) {
+ if ((channels != CSC_CHANNEL_C) && (channels != CSC_CHANNEL_CMYKA)) {
+ /* Update C */
+ SPColor::cmyk_to_rgb_floatv(rgb0, 0.0, adj[1], adj[2], adj[3]);
+ SPColor::cmyk_to_rgb_floatv(rgbm, 0.5, adj[1], adj[2], adj[3]);
+ SPColor::cmyk_to_rgb_floatv(rgb1, 1.0, adj[1], adj[2], adj[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, adj[0], 0.0, adj[2], adj[3]);
+ SPColor::cmyk_to_rgb_floatv(rgbm, adj[0], 0.5, adj[2], adj[3]);
+ SPColor::cmyk_to_rgb_floatv(rgb1, adj[0], 1.0, adj[2], adj[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, adj[0], adj[1], 0.0, adj[3]);
+ SPColor::cmyk_to_rgb_floatv(rgbm, adj[0], adj[1], 0.5, adj[3]);
+ SPColor::cmyk_to_rgb_floatv(rgb1, adj[0], adj[1], 1.0, adj[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, adj[0], adj[1], adj[2], 0.0);
+ SPColor::cmyk_to_rgb_floatv(rgbm, adj[0], adj[1], adj[2], 0.5);
+ SPColor::cmyk_to_rgb_floatv(rgb1, adj[0], adj[1], adj[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, adj[0], adj[1], adj[2], adj[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));
+ }
+ } else if constexpr (MODE == SPColorScalesMode::HSLUV) {
+ if ((channels != CSC_CHANNEL_H) && (channels != CSC_CHANNEL_A)) {
+ /* Update hue */
+ _s[0]->setMap(hsluvHueMap(adj[1], adj[2], &_sliders_maps[0]));
+ }
+ if ((channels != CSC_CHANNEL_S) && (channels != CSC_CHANNEL_A)) {
+ /* Update saturation (scaled chroma) */
+ _s[1]->setMap(hsluvSaturationMap(adj[0], adj[2], &_sliders_maps[1]));
+ }
+ if ((channels != CSC_CHANNEL_V) && (channels != CSC_CHANNEL_A)) {
+ /* Update lightness */
+ _s[2]->setMap(hsluvLightnessMap(adj[0], adj[1], &_sliders_maps[2]));
+ }
+ if (channels != CSC_CHANNEL_A) {
+ /* Update alpha */
+ SPColor::hsluv_to_rgb_floatv(rgb0, adj[0], adj[1], adj[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));
+ }
+ } else if constexpr (MODE == SPColorScalesMode::OKLAB) {
+ if (channels != CSC_CHANNEL_H && channels != CSC_CHANNEL_A) {
+ _s[0]->setMap(Oklab::render_hue_scale(adj[1], adj[2], &_sliders_maps[0]));
+ }
+ if ((channels != CSC_CHANNEL_S) && (channels != CSC_CHANNEL_A)) {
+ _s[1]->setMap(Oklab::render_saturation_scale(360.0 * adj[0], adj[2], &_sliders_maps[1]));
+ }
+ if ((channels != CSC_CHANNEL_V) && (channels != CSC_CHANNEL_A)) {
+ _s[2]->setMap(Oklab::render_lightness_scale(360.0 * adj[0], adj[1], &_sliders_maps[2]));
+ }
+ if (channels != CSC_CHANNEL_A) { // Update the alpha gradient.
+ auto const rgb = Oklab::oklab_to_rgb(
+ Oklab::okhsl_to_oklab({ getScaled(_a[0]),
+ getScaled(_a[1]),
+ getScaled(_a[2]) }));
+ _s[3]->setColors(SP_RGBA32_F_COMPOSE(rgb[0], rgb[1], rgb[2], 0.0),
+ SP_RGBA32_F_COMPOSE(rgb[0], rgb[1], rgb[2], 0.5),
+ SP_RGBA32_F_COMPOSE(rgb[0], rgb[1], rgb[2], 1.0));
+ }
+ } else {
+ g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__);
+ }
+
+#ifdef SPCS_PREVIEW
+ rgba = sp_color_scales_get_rgba32(cs);
+ sp_color_preview_set_rgba32(SP_COLOR_PREVIEW(_p), rgba);
+#endif
+}
+
+static guchar const *sp_color_scales_hue_map()
+{
+ static std::array<guchar, 4 * 1024> const map = []() {
+ std::array<guchar, 4 * 1024> m;
+
+ guchar *p;
+ p = m.data();
+ for (gint 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 m;
+ }();
+
+ return map.data();
+}
+
+static void sp_color_interp(guchar *out, gint steps, gfloat *start, gfloat *end)
+{
+ gfloat s[3] = {
+ (end[0] - start[0]) / steps,
+ (end[1] - start[1]) / steps,
+ (end[2] - start[2]) / steps
+ };
+
+ guchar *p = out;
+ for (int i = 0; i < steps; i++) {
+ *p++ = SP_COLOR_F_TO_U(start[0] + s[0] * i);
+ *p++ = SP_COLOR_F_TO_U(start[1] + s[1] * i);
+ *p++ = SP_COLOR_F_TO_U(start[2] + s[2] * i);
+ *p++ = 0xFF;
+ }
+}
+
+// TODO: consider turning this into a generator (without memory allocation).
+template <typename T>
+static std::vector<T> range (int const steps, T start, T end)
+{
+ T step = (end - start) / (steps - 1);
+
+ std::vector<T> out;
+ out.reserve(steps);
+
+ for (int i = 0; i < steps-1; i++) {
+ out.emplace_back(start + step * i);
+ }
+ out.emplace_back(end);
+
+ return out;
+}
+
+static guchar const *sp_color_scales_hsluv_map(guchar *map,
+ std::function<void(float*, float)> callback)
+{
+ // Only generate 21 colors and interpolate between them to get 1024
+ static int const STEPS = 21;
+ static int const COLORS = (STEPS+1) * 3;
+
+ std::vector<float> steps = range<float>(STEPS+1, 0.f, 1.f);
+
+ // Generate color steps
+ gfloat colors[COLORS];
+ for (int i = 0; i < STEPS+1; i++) {
+ callback(colors+(i*3), steps[i]);
+ }
+
+ for (int i = 0; i < STEPS; i++) {
+ int a = steps[i] * 1023,
+ b = steps[i+1] * 1023;
+ sp_color_interp(map+(a * 4), b-a, colors+(i*3), colors+((i+1)*3));
+ }
+
+ return map;
+}
+
+template <SPColorScalesMode MODE>
+guchar const *ColorScales<MODE>::hsluvHueMap(gfloat s, gfloat l,
+ std::array<guchar, 4 * 1024> *map)
+{
+ return sp_color_scales_hsluv_map(map->data(), [s, l] (float *colors, float h) {
+ SPColor::hsluv_to_rgb_floatv(colors, h, s, l);
+ });
+}
+
+template <SPColorScalesMode MODE>
+guchar const *ColorScales<MODE>::hsluvSaturationMap(gfloat h, gfloat l,
+ std::array<guchar, 4 * 1024> *map)
+{
+ return sp_color_scales_hsluv_map(map->data(), [h, l] (float *colors, float s) {
+ SPColor::hsluv_to_rgb_floatv(colors, h, s, l);
+ });
+}
+
+template <SPColorScalesMode MODE>
+guchar const *ColorScales<MODE>::hsluvLightnessMap(gfloat h, gfloat s,
+ std::array<guchar, 4 * 1024> *map)
+{
+ return sp_color_scales_hsluv_map(map->data(), [h, s] (float *colors, float l) {
+ SPColor::hsluv_to_rgb_floatv(colors, h, s, l);
+ });
+}
+
+template <SPColorScalesMode MODE>
+ColorScalesFactory<MODE>::ColorScalesFactory()
+{}
+
+template <SPColorScalesMode MODE>
+Gtk::Widget *ColorScalesFactory<MODE>::createWidget(Inkscape::UI::SelectedColor &color, bool no_alpha) const
+{
+ Gtk::Widget *w = Gtk::manage(new ColorScales<MODE>(color, no_alpha));
+ return w;
+}
+
+template <SPColorScalesMode MODE>
+Glib::ustring ColorScalesFactory<MODE>::modeName() const
+{
+ if constexpr (MODE == SPColorScalesMode::RGB) {
+ return gettext(ColorScales<>::SUBMODE_NAMES[1]);
+ } else if constexpr (MODE == SPColorScalesMode::HSL) {
+ return gettext(ColorScales<>::SUBMODE_NAMES[2]);
+ } else if constexpr (MODE == SPColorScalesMode::CMYK) {
+ return gettext(ColorScales<>::SUBMODE_NAMES[3]);
+ } else if constexpr (MODE == SPColorScalesMode::HSV) {
+ return gettext(ColorScales<>::SUBMODE_NAMES[4]);
+ } else if constexpr (MODE == SPColorScalesMode::HSLUV) {
+ return gettext(ColorScales<>::SUBMODE_NAMES[5]);
+ } else if constexpr (MODE == SPColorScalesMode::OKLAB) {
+ return gettext(ColorScales<>::SUBMODE_NAMES[6]);
+ } else {
+ return gettext(ColorScales<>::SUBMODE_NAMES[0]);
+ }
+}
+
+// Explicit instantiations
+template class ColorScales<SPColorScalesMode::NONE>;
+template class ColorScales<SPColorScalesMode::RGB>;
+template class ColorScales<SPColorScalesMode::HSL>;
+template class ColorScales<SPColorScalesMode::CMYK>;
+template class ColorScales<SPColorScalesMode::HSV>;
+template class ColorScales<SPColorScalesMode::HSLUV>;
+template class ColorScales<SPColorScalesMode::OKLAB>;
+
+template class ColorScalesFactory<SPColorScalesMode::NONE>;
+template class ColorScalesFactory<SPColorScalesMode::RGB>;
+template class ColorScalesFactory<SPColorScalesMode::HSL>;
+template class ColorScalesFactory<SPColorScalesMode::CMYK>;
+template class ColorScalesFactory<SPColorScalesMode::HSV>;
+template class ColorScalesFactory<SPColorScalesMode::HSLUV>;
+template class ColorScalesFactory<SPColorScalesMode::OKLAB>;
+
+} // 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-scales.h b/src/ui/widget/color-scales.h
new file mode 100644
index 0000000..0ded543
--- /dev/null
+++ b/src/ui/widget/color-scales.h
@@ -0,0 +1,141 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Color selector using sliders for each components, for multiple color modes
+ *//*
+ * Authors:
+ * see git history
+ *
+ * Copyright (C) 2018, 2021 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/box.h>
+#include <array>
+#include <vector>
+
+#include "ui/selected-color.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class ColorSlider;
+class ColorWheel;
+
+enum class SPColorScalesMode {
+ NONE,
+ RGB,
+ HSL,
+ CMYK,
+ HSV,
+ HSLUV,
+ OKLAB,
+ CMS
+};
+
+template <SPColorScalesMode MODE = SPColorScalesMode::NONE>
+class ColorScales
+ : public Gtk::Box
+{
+public:
+ static gchar const *SUBMODE_NAMES[];
+
+ static double getScaled(Glib::RefPtr<Gtk::Adjustment> const &a);
+ static void setScaled(Glib::RefPtr<Gtk::Adjustment> &a, double v, bool constrained = false);
+
+ ColorScales(SelectedColor &color, bool no_alpha);
+ ~ColorScales() override;
+
+ void setupMode(bool no_alpha);
+ SPColorScalesMode getMode() const;
+
+ static guchar const *hsluvHueMap(gfloat s, gfloat l,
+ std::array<guchar, 4 * 1024> *map);
+ static guchar const *hsluvSaturationMap(gfloat h, gfloat l,
+ std::array<guchar, 4 * 1024> *map);
+ static guchar const *hsluvLightnessMap(gfloat h, gfloat s,
+ std::array<guchar, 4 * 1024> *map);
+
+protected:
+ void _onColorChanged();
+ void on_show() override;
+
+ void _initUI(bool no_alpha);
+
+ void _sliderAnyGrabbed();
+ void _sliderAnyReleased();
+ void _sliderAnyChanged();
+ void _adjustmentChanged(int channel);
+ void _wheelChanged();
+
+ void _getRgbaFloatv(gfloat *rgba);
+ void _getCmykaFloatv(gfloat *cmyka);
+ guint32 _getRgba32();
+ void _updateSliders(guint channels);
+ void _recalcColor();
+ void _updateDisplay(bool update_wheel = true);
+
+ void _setRangeLimit(gdouble upper);
+
+ SelectedColor &_color;
+ gdouble _range_limit;
+ gboolean _updating : 1;
+ gboolean _dragging : 1;
+ std::vector<Glib::RefPtr<Gtk::Adjustment>> _a; /* Channel adjustments */
+ Inkscape::UI::Widget::ColorSlider *_s[5]; /* Channel sliders */
+ Gtk::Widget *_b[5]; /* Spinbuttons */
+ Gtk::Label *_l[5]; /* Labels */
+ std::array<guchar, 4 * 1024> _sliders_maps[4];
+ Inkscape::UI::Widget::ColorWheel *_wheel;
+
+ const Glib::ustring _prefs = "/color_scales";
+ static gchar const * const _pref_wheel_visibility;
+
+ sigc::connection _color_changed;
+ sigc::connection _color_dragged;
+
+public:
+ // By default, disallow copy constructor and assignment operator
+ ColorScales(ColorScales const &obj) = delete;
+ ColorScales &operator=(ColorScales const &obj) = delete;
+};
+
+template <SPColorScalesMode MODE>
+class ColorScalesFactory : public Inkscape::UI::ColorSelectorFactory
+{
+public:
+ ColorScalesFactory();
+
+ Gtk::Widget *createWidget(Inkscape::UI::SelectedColor &color, bool no_alpha) const override;
+ Glib::ustring modeName() const override;
+};
+
+struct ColorPickerDescription
+{
+ SPColorScalesMode mode;
+ const char* icon;
+ const char* label;
+ Glib::ustring visibility_path;
+ std::unique_ptr<Inkscape::UI::ColorSelectorFactory> factory;
+};
+
+std::vector<ColorPickerDescription> get_color_pickers();
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#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:fileencoding=utf-8:textwidth=99:
diff --git a/src/ui/widget/color-slider.cpp b/src/ui/widget/color-slider.cpp
new file mode 100644
index 0000000..2b71e6c
--- /dev/null
+++ b/src/ui/widget/color-slider.cpp
@@ -0,0 +1,546 @@
+// 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 = 8;
+
+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)
+ , _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, 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, 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);
+
+ _onAdjustmentChanged();
+ }
+}
+
+void ColorSlider::_onAdjustmentChanged() { queue_draw(); }
+
+void ColorSlider::_onAdjustmentValueChanged()
+{
+ if (_value != ColorScales<>::getScaled(_adjustment)) {
+ 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) * cw) != (gint)(_value * cw)) {
+ gint ax, ay;
+ gfloat value;
+ value = _value;
+ _value = ColorScales<>::getScaled(_adjustment);
+ 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);
+ }
+ }
+}
+
+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());
+
+ int scale = style_context->get_scale();
+ carea.set_x(padding.get_left() * scale);
+ carea.set_y(padding.get_top() * scale);
+
+ carea.set_width(allocation.get_width() * scale - 2 * carea.get_x());
+ carea.set_height(allocation.get_height() * scale - 2 * carea.get_y());
+
+ cr->save();
+ // changing scale to draw pixmap at display resolution
+ cr->scale(1.0 / scale, 1.0 / scale);
+
+ 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 * scale);
+
+ 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 * scale);
+
+ /* 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 * scale);
+
+ /* 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();
+ }
+ }
+ }
+
+ cr->restore();
+
+ /* 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() / scale) - ARROW_SIZE / 2 + carea.get_x() / scale);
+ gint y1 = carea.get_y() / scale;
+ gint y2 = carea.get_y() / scale + carea.get_height() / scale - 1;
+ cr->set_line_width(2.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->close_path();
+
+ // 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->close_path();
+
+ // Render both arrows
+ cr->set_source_rgb(0.0, 0.0, 0.0);
+ cr->stroke_preserve();
+ cr->set_source_rgb(1.0, 1.0, 1.0);
+ cr->fill();
+
+ return false;
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+/* Colors are << 16 */
+
+inline bool checkerboard(gint x, gint y, guint size) {
+ return ((x / size) & 1) != ((y / size) & 1);
+}
+
+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 = checkerboard(x, 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 = checkerboard(x, 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..8257409
--- /dev/null
+++ b/src/ui/widget/color-slider.h
@@ -0,0 +1,92 @@
+// 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;
+
+ 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/combo-box-entry-tool-item.cpp b/src/ui/widget/combo-box-entry-tool-item.cpp
new file mode 100644
index 0000000..94efec8
--- /dev/null
+++ b/src/ui/widget/combo-box-entry-tool-item.cpp
@@ -0,0 +1,725 @@
+// 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 "libnrtype/font-lister.h"
+
+#include <cassert>
+#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),
+ _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)
+{
+ set_name(name);
+
+ gchar *action_name = g_strdup( get_name().c_str() );
+ gchar *combobox_name = g_strjoin( nullptr, action_name, "_combobox", nullptr );
+ gchar *entry_name = g_strjoin( nullptr, action_name, "_entry", nullptr );
+ 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 );
+ }
+
+ // Optionally add formatting...
+ if( _cell_data_func != nullptr ) {
+ gtk_combo_box_set_popup_fixed_width (GTK_COMBO_BOX(comboBoxEntry), false);
+ this->_cell = gtk_cell_renderer_text_new();
+ int total = gtk_tree_model_iter_n_children (model, nullptr);
+ int height = 30;
+ if (total > 1000) {
+ height = 30000/total;
+ g_warning("You have a huge number of font families (%d), "
+ "and Cairo is limiting the size of widgets you can draw.\n"
+ "Your preview cell height is capped to %d.",
+ total, height);
+ // hope we dont need a forced height because now pango line height
+ // not add data outside parent rendered expanding it so no naturall cells become over 30 height
+ gtk_cell_renderer_set_fixed_size(_cell, -1, height);
+ } else {
+#if !PANGO_VERSION_CHECK(1,50,0)
+ gtk_cell_renderer_set_fixed_size(_cell, -1, height);
+#endif
+ }
+ 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(_combobox ), _cell,
+ GtkCellLayoutDataFunc (_cell_data_func), nullptr, nullptr );
+ g_signal_connect(G_OBJECT(comboBoxEntry), "popup", G_CALLBACK(combo_box_popup_cb), this);
+ }
+
+ // 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 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 ---------------------------------------------------
+
+Glib::ustring
+ComboBoxEntryToolItem::get_active_text()
+{
+ assert(_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;
+}
+
+// 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();
+ }
+}
+
+static gboolean add_more_font_families_idle(gpointer user_data)
+{
+ FontLister* fl = FontLister::get_instance();
+ static int q = 1;
+ static unsigned recurse_times = fl->get_font_families_size() / FONT_FAMILIES_GROUP_SIZE;
+
+ fl->init_font_families(q, FONT_FAMILIES_GROUP_SIZE);
+ if (q < recurse_times)
+ gdk_threads_add_idle (add_more_font_families_idle, NULL);
+ q++;
+ return false;
+}
+
+gboolean ComboBoxEntryToolItem::combo_box_popup_cb(ComboBoxEntryToolItem *widget, gpointer data)
+{
+ auto action = reinterpret_cast<ComboBoxEntryToolItem *>( data );
+ g_idle_add(ComboBoxEntryToolItem::set_cell_markup, action);
+ return true;
+}
+
+gboolean ComboBoxEntryToolItem::set_cell_markup(gpointer data)
+{
+ ComboBoxEntryToolItem *self = static_cast<ComboBoxEntryToolItem *>(data);
+ gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT( self->_combobox ), self->_cell,
+ GtkCellLayoutDataFunc (self->_cell_data_func), self, nullptr );
+ return false;
+}
+
+
+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 *entry, 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 ) {
+
+ 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_Tab:
+ {
+ // Fire activation similar to how Return does, but also return focus to text object
+ // itself
+ entry_activate_cb( GTK_ENTRY (entry), data );
+ 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..61ecb9f
--- /dev/null
+++ b/src/ui/widget/combo-box-entry-tool-item.h
@@ -0,0 +1,153 @@
+// 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
+ GtkCellRenderer *_cell;
+
+ 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;
+
+ // Signals
+ sigc::signal<void ()> _signal_changed;
+
+ 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 gboolean combo_box_popup_cb( ComboBoxEntryToolItem* widget, gpointer data );
+ static gboolean set_cell_markup(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);
+
+ Glib::ustring 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 );
+
+ // 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..1b0112a
--- /dev/null
+++ b/src/ui/widget/combo-enums.h
@@ -0,0 +1,232 @@
+// 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
+{
+public:
+ ComboBoxEnum(E default_value, const Util::EnumDataConverter<E>& c, const SPAttr a = SPAttr::INVALID, bool sort = true, const char* translation_context = nullptr) :
+ ComboBoxEnum(c, a, sort, translation_context, static_cast<unsigned int>(default_value))
+ {
+ set_active_by_id(default_value);
+ sort_items();
+ }
+
+ ComboBoxEnum(const Util::EnumDataConverter<E>& c, const SPAttr a = SPAttr::INVALID, bool sort = true, const char* translation_context = nullptr) :
+ ComboBoxEnum(c, a, sort, translation_context, 0)
+ {
+ set_active(0);
+ sort_items();
+ }
+
+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;
+
+ ComboBoxEnum(const Util::EnumDataConverter<E>& c, const SPAttr a, bool sort, const char* translation_context, unsigned int default_value)
+ : AttrWidget(a, 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;
+ auto label = _converter.get_label(data->id);
+ auto trans = translation_context ?
+ g_dpgettext2(nullptr, translation_context, label.c_str()) :
+ gettext(label.c_str());
+ row[_columns.label] = trans;
+ row[_columns.is_separator] = _converter.get_key(data->id) == "-";
+ }
+ set_row_separator_func(sigc::mem_fun(*this, &ComboBoxEnum<E>::combo_separator_func));
+ }
+
+ void sort_items() {
+ // 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);
+ }
+ }
+
+public:
+ 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;
+ auto index = get_active_by_id(id);
+ if (index >= 0) {
+ set_active(index);
+ }
+ };
+
+ 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 combo_separator_func(const Glib::RefPtr<Gtk::TreeModel>& model,
+ const Gtk::TreeModel::iterator& iter) {
+ return (*iter)[_columns.is_separator];
+ };
+
+ bool setProgrammatically;
+
+private:
+ int get_active_by_id(E id) const {
+ int index = 0;
+ for (auto&& child : _model->children()) {
+ const Util::EnumData<E>* data = child[_columns.data];
+ if (data->id == id) {
+ return index;
+ }
+ ++index;
+ }
+ return -1;
+ };
+
+ class Columns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ Columns()
+ {
+ add(data);
+ add(label);
+ add(is_separator);
+ }
+
+ Gtk::TreeModelColumn<const Util::EnumData<E>*> data;
+ Gtk::TreeModelColumn<Glib::ustring> label;
+ Gtk::TreeModelColumn<bool> is_separator;
+ };
+
+ 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, SPAttr::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..74a38ee
--- /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/completion-popup.cpp b/src/ui/widget/completion-popup.cpp
new file mode 100644
index 0000000..1522d4f
--- /dev/null
+++ b/src/ui/widget/completion-popup.cpp
@@ -0,0 +1,108 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "completion-popup.h"
+#include <cassert>
+#include <glibmm/ustring.h>
+#include <gtkmm/entrycompletion.h>
+#include <gtkmm/searchentry.h>
+#include "ui/builder-utils.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+enum Columns {
+ ColID = 0,
+ ColName,
+ ColIcon,
+ ColSearch
+};
+
+CompletionPopup::CompletionPopup() :
+ _builder(create_builder("completion-box.glade")),
+ _search(get_widget<Gtk::SearchEntry>(_builder, "search")),
+ _button(get_widget<Gtk::MenuButton>(_builder, "menu-btn")),
+ _completion(get_object<Gtk::EntryCompletion>(_builder, "completion")),
+ _popup(get_widget<Gtk::Menu>(_builder, "popup"))
+{
+ _list = Glib::RefPtr<Gtk::ListStore>::cast_dynamic(_builder->get_object("list"));
+ assert(_list);
+
+ add(get_widget<Gtk::Box>(_builder, "main-box"));
+
+ _completion->set_match_func([=](const Glib::ustring& text, const Gtk::TreeModel::const_iterator& it){
+ Glib::ustring str;
+ it->get_value(ColSearch, str);
+ if (str.empty()) {
+ return false;
+ }
+ return str.lowercase().find(text.lowercase()) != Glib::ustring::npos;
+ });
+
+ // clear search box without triggering completion popup menu
+ auto clear = [=]() { _search.get_buffer()->set_text(Glib::ustring()); };
+
+ _completion->signal_match_selected().connect([=](const Gtk::TreeModel::iterator& it){
+ int id;
+ it->get_value(ColID, id);
+ _match_selected.emit(id);
+ clear();
+ return true;
+ }, false);
+
+ _search.signal_focus_in_event().connect([=](GdkEventFocus*){
+ _on_focus.emit();
+ clear();
+ return false;
+ });
+ _button.signal_button_press_event().connect([=](GdkEventButton*){
+ _button_press.emit();
+ clear();
+ return false;
+ }, false);
+ _search.signal_focus_out_event().connect([=](GdkEventFocus*){
+ clear();
+ return false;
+ });
+
+ _search.signal_stop_search().connect([=](){
+ clear();
+ });
+
+ show();
+}
+
+void CompletionPopup::clear_completion_list() {
+ _list->clear();
+}
+
+void CompletionPopup::add_to_completion_list(int id, Glib::ustring name, Glib::ustring icon_name, Glib::ustring search_text) {
+ auto row = *_list->append();
+ row.set_value(ColID, id);
+ row.set_value(ColName, name);
+ row.set_value(ColIcon, icon_name);
+ row.set_value(ColSearch, search_text.empty() ? name : search_text);
+}
+
+Gtk::Menu& CompletionPopup::get_menu() {
+ return _popup;
+}
+
+Gtk::SearchEntry& CompletionPopup::get_entry() {
+ return _search;
+}
+
+sigc::signal<void (int)>& CompletionPopup::on_match_selected() {
+ return _match_selected;
+}
+
+sigc::signal<void ()>& CompletionPopup::on_button_press() {
+ return _button_press;
+}
+
+sigc::signal<bool ()>& CompletionPopup::on_focus() {
+ return _on_focus;
+}
+
+
+}}} // namespaces
diff --git a/src/ui/widget/completion-popup.h b/src/ui/widget/completion-popup.h
new file mode 100644
index 0000000..b7b77a1
--- /dev/null
+++ b/src/ui/widget/completion-popup.h
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+
+#ifndef INKSCAPE_UI_WIDGET_COMPLETION_POPUP_H
+#define INKSCAPE_UI_WIDGET_COMPLETION_POPUP_H
+
+#include <glibmm/refptr.h>
+#include <glibmm/ustring.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/entrycompletion.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/menu.h>
+#include <gtkmm/menubutton.h>
+#include <gtkmm/searchentry.h>
+#include <sigc++/connection.h>
+#include "labelled.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class CompletionPopup : public Gtk::Box {
+public:
+ CompletionPopup();
+
+ Gtk::Menu& get_menu();
+ Gtk::SearchEntry& get_entry();
+ Glib::RefPtr<Gtk::ListStore> get_list();
+
+ void clear_completion_list();
+ void add_to_completion_list(int id, Glib::ustring name, Glib::ustring icon_name, Glib::ustring search_text = Glib::ustring());
+
+ sigc::signal<void (int)>& on_match_selected();
+ sigc::signal<void ()>& on_button_press();
+ sigc::signal<bool ()>& on_focus();
+
+private:
+ Glib::RefPtr<Gtk::Builder> _builder;
+ Glib::RefPtr<Gtk::ListStore> _list;
+ Gtk::SearchEntry& _search;
+ Gtk::MenuButton& _button;
+ Gtk::Menu& _popup;
+ Glib::RefPtr<Gtk::EntryCompletion> _completion;
+ sigc::signal<void (int)> _match_selected;
+ sigc::signal<void ()> _button_press;
+ sigc::signal<bool ()> _on_focus;
+};
+
+}}} // namespaces
+
+#endif // INKSCAPE_UI_WIDGET_COMPLETION_POPUP_H
diff --git a/src/ui/widget/custom-tooltip.cpp b/src/ui/widget/custom-tooltip.cpp
new file mode 100644
index 0000000..c77082e
--- /dev/null
+++ b/src/ui/widget/custom-tooltip.cpp
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include "custom-tooltip.h"
+#include "gtkmm/box.h"
+#include "gtkmm/label.h"
+#include "gtkmm/image.h"
+#include <ctime>
+#include <chrono>
+#include <gdk/gdk.h>
+
+static gint timeoutid = -1;
+
+static
+gboolean
+delaytooltip (gpointer data)
+{
+ GdkDisplay *display = reinterpret_cast<GdkDisplay *>(data);
+ gtk_tooltip_trigger_tooltip_query(display);
+ return true;
+}
+
+void sp_clear_custom_tooltip()
+{
+ if (timeoutid != -1) {
+ g_source_remove(timeoutid);
+ timeoutid = -1;
+ }
+}
+
+bool
+sp_query_custom_tooltip(int x, int y, bool keyboard_tooltip, const Glib::RefPtr<Gtk::Tooltip>& tooltipw, gint id, Glib::ustring tooltip, Glib::ustring icon, Gtk::IconSize iconsize, int delaytime)
+{
+ sp_clear_custom_tooltip();
+
+ static gint last = -1;
+ static auto start = std::chrono::steady_clock::now();
+ auto end = std::chrono::steady_clock::now();
+ if (last != id) {
+ start = std::chrono::steady_clock::now();
+ last = id;
+ }
+ Gtk::Box *box = Gtk::make_managed<Gtk::Box>();
+ Gtk::Label *label = Gtk::make_managed<Gtk::Label>();
+ label->set_line_wrap(true);
+ label->set_markup(tooltip);
+ label->set_max_width_chars(40);
+ if (icon != "") {
+ box->pack_start(*Gtk::make_managed<Gtk::Image>(icon, iconsize), true, true, 2);
+ }
+ box->pack_start(*label, true, true, 2);
+ tooltipw->set_custom(*box);
+ box->get_style_context()->add_class("symbolic");
+ box->show_all_children();
+ auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+ if (elapsed.count() / delaytime < 0.5) {
+ GdkDisplay *display = gdk_display_get_default();
+ if (display) {
+ timeoutid = g_timeout_add(501-elapsed.count(), delaytooltip, display);
+ }
+ }
+ return elapsed.count() / delaytime > 0.5;
+}
diff --git a/src/ui/widget/custom-tooltip.h b/src/ui/widget/custom-tooltip.h
new file mode 100644
index 0000000..32647bc
--- /dev/null
+++ b/src/ui/widget/custom-tooltip.h
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifndef INKSCAPE_UI_WIDGET_CUSTOM_TOOLTIP_H
+#define INKSCAPE_UI_WIDGET_CUSTOM_TOOLTIP_H
+
+#include <gtkmm/tooltip.h>
+
+void sp_clear_custom_tooltip();
+
+bool
+sp_query_custom_tooltip(
+ int x,
+ int y,
+ bool keyboard_tooltip,
+ const Glib::RefPtr<Gtk::Tooltip>& tooltipw,
+ gint id,
+ Glib::ustring tooltip,
+ Glib::ustring icon = "",
+ Gtk::IconSize iconsize = Gtk::ICON_SIZE_DIALOG,
+ int delaytime = 1000.0);
+
+#endif // INKSCAPE_UI_WIDGET_CUSTOM_TOOLTIP_H
diff --git a/src/ui/widget/dash-selector.cpp b/src/ui/widget/dash-selector.cpp
new file mode 100644
index 0000000..016025f
--- /dev/null
+++ b/src/ui/widget/dash-selector.cpp
@@ -0,0 +1,260 @@
+// 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 <numeric>
+
+#include "preferences.h"
+#include "display/cairo-utils.h"
+#include "style.h"
+
+#include "ui/dialog-events.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+gchar const *const DashSelector::_prefs_path = "/palette/dashes";
+
+static std::vector<std::vector<double>> s_dashes;
+
+DashSelector::DashSelector()
+ : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4),
+ _preview_width(100),
+ _preview_height(16),
+ _preview_lineheight(2)
+{
+ // 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.show();
+ _dash_combo.signal_changed().connect( sigc::mem_fun(*this, &DashSelector::on_selection) );
+ // show dashes in two columns to eliminate or minimize scrolling
+ _dash_combo.set_wrap_width(2);
+
+ this->pack_start(_dash_combo, true, true, 0);
+
+ _offset = Gtk::Adjustment::create(0.0, 0.0, 1000.0, 0.1, 1.0, 0.0);
+ _offset->signal_value_changed().connect(sigc::mem_fun(*this, &DashSelector::offset_value_changed));
+ _sb = new Inkscape::UI::Widget::SpinButton(_offset, 0.1, 2);
+ _sb->set_tooltip_text(_("Pattern offset"));
+ sp_dialog_defocus_on_enter_cpp(_sb);
+ _sb->set_width_chars(4);
+ _sb->show();
+
+ this->pack_start(*_sb, false, false, 0);
+
+ for (std::size_t i = 0; i < s_dashes.size(); ++i) {
+ Gtk::TreeModel::Row row = *(_dash_store->append());
+ row[dash_columns.dash] = i;
+ }
+
+ _pattern = &s_dashes.front();
+}
+
+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 ) {
+ // dashes are rendered on the fly to adapt to current theme colors
+ std::size_t index = (*row)[dash_columns.dash];
+ Cairo::RefPtr<Cairo::Surface> surface;
+ if (index == 1) {
+ // add the custom one as a second option; it'll show up at the top of second column
+ // TRANSLATORS: 'Custom' here means, that user-defined dash pattern is specified in an entry box
+ surface = sp_text_to_pixbuf(_("Custom"));
+ }
+ else if (index < s_dashes.size()) {
+ // add the dash to the combobox
+ surface = sp_dash_to_pixbuf(s_dashes[index]);
+ }
+ else {
+ surface = Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 1, 1)));
+ g_warning("No surface in prepareImageRenderer.");
+ }
+ _image_renderer.property_surface() = surface;
+}
+
+static std::vector<double> map_values(const std::vector<SPILength>& values) {
+ std::vector<double> out;
+ out.reserve(values.size());
+ for (auto&& v : values) {
+ out.push_back(v.value);
+ }
+ return out;
+}
+
+void DashSelector::init_dashes() {
+ if (s_dashes.empty()) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ std::vector<Glib::ustring> dash_prefs = prefs->getAllDirs(_prefs_path);
+
+ if (!dash_prefs.empty()) {
+ SPStyle style;
+ s_dashes.reserve(dash_prefs.size() + 1);
+
+ for (auto & dash_pref : dash_prefs) {
+ style.readFromPrefs( dash_pref );
+
+ if (!style.stroke_dasharray.values.empty()) {
+ s_dashes.emplace_back(map_values(style.stroke_dasharray.values));
+ } else {
+ s_dashes.emplace_back(std::vector<double>());
+ }
+ }
+ } else {
+ g_warning("Missing stock dash definitions. DashSelector::init_dashes.");
+ // This code may never execute - a new preferences.xml is created for a new user. Maybe if the user deletes dashes from preferences.xml?
+ s_dashes.emplace_back(std::vector<double>());
+ }
+
+ std::vector<double> custom {1, 2, 1, 4}; // 'custom' dashes second on the list, so they are at the top of the second column in a combo box
+ s_dashes.insert(s_dashes.begin() + 1, custom);
+ }
+}
+
+void DashSelector::set_dash(const std::vector<double>& dash, double offset) {
+ int pos = -1; // Allows custom patterns to remain unscathed by this.
+
+ double delta = std::accumulate(dash.begin(), dash.end(), 0.0) / (10000.0 * (dash.empty() ? 1 : dash.size()));
+
+ int index = 0;
+ for (auto&& pattern : s_dashes) {
+ if (dash.size() == pattern.size() &&
+ std::equal(dash.begin(), dash.end(), pattern.begin(),
+ [=](double a, double b) { return Geom::are_near(a, b, delta); })) {
+ pos = index;
+ break;
+ }
+ ++index;
+ }
+
+ if (pos >= 0) {
+ _pattern = &s_dashes.at(pos);
+ _dash_combo.set_active(pos);
+ _offset->set_value(offset);
+ }
+ else { // Hit a custom pattern in the SVG, write it into the combobox.
+ pos = 1; // the one slot for custom patterns
+ _pattern = &s_dashes[pos];
+ _pattern->assign(dash.begin(), dash.end());
+ _dash_combo.set_active(pos);
+ _offset->set_value(offset);
+ }
+}
+
+const std::vector<double>& DashSelector::get_dash(double* offset) const {
+ if (offset) *offset = _offset->get_value();
+ return *_pattern;
+}
+
+double DashSelector::get_offset() {
+ return _offset ? _offset->get_value() : 0.0;
+}
+
+/**
+ * Fill a pixbuf with the dash pattern using standard cairo drawing
+ */
+Cairo::RefPtr<Cairo::Surface> DashSelector::sp_dash_to_pixbuf(const std::vector<double>& pattern) {
+ auto device_scale = get_scale_factor();
+
+ auto height = _preview_height * device_scale;
+ auto width = _preview_width * device_scale;
+ cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
+ cairo_t *ct = cairo_create(s);
+
+ auto context = get_style_context();
+ Gdk::RGBA fg = context->get_color(get_state_flags());
+
+ cairo_set_line_width (ct, _preview_lineheight * device_scale);
+ cairo_scale (ct, _preview_lineheight * device_scale, 1);
+ cairo_move_to (ct, 0, height/2);
+ cairo_line_to (ct, width, height/2);
+ cairo_set_dash(ct, pattern.data(), pattern.size(), 0);
+ cairo_set_source_rgb(ct, fg.get_red(), fg.get_green(), fg.get_blue());
+ cairo_stroke (ct);
+
+ cairo_destroy(ct);
+ cairo_surface_flush(s);
+
+ cairo_surface_set_device_scale(s, device_scale, device_scale);
+ return Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(s));
+}
+
+/**
+ * Fill a pixbuf with a text label using standard cairo drawing
+ */
+Cairo::RefPtr<Cairo::Surface> DashSelector::sp_text_to_pixbuf(const char* text) {
+ auto device_scale = get_scale_factor();
+ cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, _preview_width * device_scale, _preview_height * device_scale);
+ cairo_t *ct = cairo_create(s);
+
+ cairo_select_font_face (ct, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
+ // todo: how to find default font face and size?
+ cairo_set_font_size (ct, 12 * device_scale);
+ auto context = get_style_context();
+ Gdk::RGBA fg = context->get_color(get_state_flags());
+ cairo_set_source_rgb(ct, fg.get_red(), fg.get_green(), fg.get_blue());
+ cairo_move_to (ct, 16.0 * device_scale, 13.0 * device_scale);
+ cairo_show_text (ct, text);
+
+ cairo_destroy(ct);
+ cairo_surface_flush(s);
+
+ cairo_surface_set_device_scale(s, device_scale, device_scale);
+ return Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(s));
+}
+
+void DashSelector::on_selection()
+{
+ _pattern = &s_dashes.at(_dash_combo.get_active()->get_value(dash_columns.dash));
+ changed_signal.emit();
+}
+
+void DashSelector::offset_value_changed()
+{
+ Glib::ustring offset = _("Pattern offset");
+ offset += " (";
+ offset += Glib::ustring::format(_sb->get_value());
+ offset += ")";
+ _sb->set_tooltip_text(offset.c_str());
+ 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..21b370a
--- /dev/null
+++ b/src/ui/widget/dash-selector.h
@@ -0,0 +1,116 @@
+// 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 "ui/widget/spinbutton.h"
+#include <gtkmm/box.h>
+#include <gtkmm/combobox.h>
+#include <gtkmm/liststore.h>
+
+#include <sigc++/signal.h>
+
+#include "scrollprotected.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Class that wraps a combobox and spinbutton for selecting dash patterns.
+ */
+class DashSelector : public Gtk::Box {
+public:
+ DashSelector();
+ ~DashSelector() override;
+
+ /**
+ * Get and set methods for dashes
+ */
+ void set_dash(const std::vector<double>& dash, double offset);
+
+ const std::vector<double>& get_dash(double* offset) const;
+
+ sigc::signal<void ()> changed_signal;
+
+ double get_offset();
+
+private:
+
+ /**
+ * Initialize dashes list from preferences
+ */
+ static void init_dashes();
+
+ /**
+ * Fill a pixbuf with the dash pattern using standard cairo drawing
+ */
+ Cairo::RefPtr<Cairo::Surface> sp_dash_to_pixbuf(const std::vector<double>& pattern);
+
+ /**
+ * Fill a pixbuf with text standard cairo drawing
+ */
+ Cairo::RefPtr<Cairo::Surface> sp_text_to_pixbuf(const 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<std::size_t> dash;
+ DashColumns() {
+ add(dash);
+ }
+ };
+ DashColumns dash_columns;
+ Glib::RefPtr<Gtk::ListStore> _dash_store;
+ ScrollProtected<Gtk::ComboBox> _dash_combo;
+ Gtk::CellRendererPixbuf _image_renderer;
+ Glib::RefPtr<Gtk::Adjustment> _offset;
+ Inkscape::UI::Widget::SpinButton *_sb;
+ static gchar const *const _prefs_path;
+ int _preview_width;
+ int _preview_height;
+ int _preview_lineheight;
+ std::vector<double>* _pattern = nullptr;
+};
+
+} // 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/entity-entry.cpp b/src/ui/widget/entity-entry.cpp
new file mode 100644
index 0000000..cfff80f
--- /dev/null
+++ b/src/ui/widget/entity-entry.cpp
@@ -0,0 +1,220 @@
+// 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 "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, bool read_only)
+{
+ 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();
+ if (!read_only) {
+ 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() || !_wr->desktop())
+ return;
+
+ _wr->setUpdating (true);
+ SPDocument *doc = _wr->desktop()->getDocument();
+ 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, "Document metadata updated", "");
+ }
+ }
+ _wr->setUpdating (false);
+}
+
+Glib::ustring EntityLineEntry::content() const {
+ return static_cast<Gtk::Entry*>(_packable)->get_text();
+}
+
+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));
+}
+
+Glib::ustring EntityMultiLineEntry::content() const {
+ return _v.get_buffer()->get_text();
+}
+
+EntityMultiLineEntry::~EntityMultiLineEntry()
+{
+ delete static_cast<Gtk::ScrolledWindow*>(_packable);
+}
+
+void EntityMultiLineEntry::update(SPDocument* doc, bool read_only)
+{
+ 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();
+ if (!read_only) {
+ 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() || !_wr->desktop())
+ return;
+
+ _wr->setUpdating (true);
+ SPDocument *doc = _wr->desktop()->getDocument();
+ 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, "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..7fd243b
--- /dev/null
+++ b/src/ui/widget/entity-entry.h
@@ -0,0 +1,89 @@
+// 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 <glibmm/ustring.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, bool read_only) = 0;
+ virtual void on_changed() = 0;
+ virtual void load_from_preferences() = 0;
+ virtual Glib::ustring content() const = 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, bool read_only) override;
+ void load_from_preferences() override;
+ Glib::ustring content() const 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, bool read_only) override;
+ void load_from_preferences() override;
+ Glib::ustring content() const 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/export-lists.cpp b/src/ui/widget/export-lists.cpp
new file mode 100644
index 0000000..857bc00
--- /dev/null
+++ b/src/ui/widget/export-lists.cpp
@@ -0,0 +1,321 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Anshudhar Kumar Singh <anshudhar2001@gmail.com>
+ *
+ * Copyright (C) 2021 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "export-lists.h"
+
+#include <glibmm/convert.h>
+#include <glibmm/i18n.h>
+#include <glibmm/miscutils.h>
+#include <gtkmm.h>
+#include <png.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "extension/db.h"
+#include "extension/output.h"
+#include "file.h"
+#include "helper/png-write.h"
+#include "inkscape-window.h"
+#include "inkscape.h"
+#include "io/resource.h"
+#include "io/sys.h"
+#include "message-stack.h"
+#include "object/object-set.h"
+#include "object/sp-namedview.h"
+#include "object/sp-page.h"
+#include "object/sp-root.h"
+#include "page-manager.h"
+#include "preferences.h"
+#include "selection-chemistry.h"
+#include "ui/dialog-events.h"
+#include "ui/dialog/dialog-notebook.h"
+#include "ui/dialog/filedialog.h"
+#include "ui/icon-loader.h"
+#include "ui/interface.h"
+#include "ui/widget/scrollprotected.h"
+#include "ui/widget/unit-menu.h"
+#include "ui/builder-utils.h"
+
+using Inkscape::Util::unit_table;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+ExtensionList::ExtensionList()
+{
+ init();
+}
+
+ExtensionList::ExtensionList(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade)
+ : Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBoxText>(cobject, refGlade)
+{
+ init();
+}
+
+ExtensionList::~ExtensionList()
+{
+ _popover_signal.disconnect();
+}
+
+void ExtensionList::init()
+{
+ _builder = create_builder("dialog-export-prefs.glade");
+ _builder->get_widget("pref_button", _pref_button);
+ _builder->get_widget("pref_popover", _pref_popover);
+ _builder->get_widget("pref_holder", _pref_holder);
+
+ _popover_signal = _pref_popover->signal_show().connect([=]() {
+ _pref_holder->remove();
+ if (auto ext = getExtension()) {
+ if (auto gui = ext->autogui(nullptr, nullptr)) {
+ _pref_holder->add(*gui);
+ _pref_popover->grab_focus();
+ }
+ }
+ });
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ _watch_pref = prefs->createObserver("/dialogs/export/show_all_extensions", [=]() { setup(); });
+}
+
+void ExtensionList::on_changed()
+{
+ bool has_prefs = false;
+ if (auto ext = getExtension()) {
+ has_prefs = (ext->widget_visible_count() > 0);
+ }
+ _pref_button->set_sensitive(has_prefs);
+}
+
+void ExtensionList::setup()
+{
+ this->remove_all();
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool export_all = prefs->getBool("/dialogs/export/show_all_extensions", false);
+
+ Inkscape::Extension::DB::OutputList extensions;
+ Inkscape::Extension::db.get_output_list(extensions);
+ for (auto omod : extensions) {
+ auto oid = Glib::ustring(omod->get_id());
+ if (!export_all && !omod->is_raster() && !omod->is_exported())
+ continue;
+ // Comboboxes don't have a disabled row property
+ if (omod->deactivated())
+ continue;
+ this->append(oid, omod->get_filetypename());
+ // Record extensions map for filename-to-combo selections
+ auto ext = omod->get_extension();
+ if (!ext_to_mod[ext]) {
+ // Some extensions have multiple of the same extension (for example PNG)
+ // we're going to pick the first in the found list to back-link to.
+ ext_to_mod[ext] = omod;
+ }
+ }
+ this->set_active_id(SP_MODULE_KEY_RASTER_PNG);
+}
+
+/**
+ * Returns the Output extension currently selected in this dropdown.
+ */
+Inkscape::Extension::Output *ExtensionList::getExtension()
+{
+ return dynamic_cast<Inkscape::Extension::Output *>(Inkscape::Extension::db.get(this->get_active_id().c_str()));
+}
+
+/**
+ * Returns the file extension (file ending) of the currently selected extension.
+ */
+Glib::ustring ExtensionList::getFileExtension()
+{
+ if (auto ext = getExtension()) {
+ return ext->get_extension();
+ }
+ return "";
+}
+
+/**
+ * Removes the file extension, *if* it's one of the extensions in the list.
+ */
+void ExtensionList::removeExtension(Glib::ustring &filename)
+{
+ auto ext = Inkscape::IO::get_file_extension(filename);
+ if (ext_to_mod[ext]) {
+ filename.erase(filename.size()-ext.size());
+ }
+}
+
+void ExtensionList::setExtensionFromFilename(Glib::ustring const &filename)
+{
+ auto ext = Inkscape::IO::get_file_extension(filename);
+ if (auto omod = ext_to_mod[ext]) {
+ this->set_active_id(omod->get_id());
+ }
+}
+
+void ExportList::setup()
+{
+ if (_initialised) {
+ return;
+ }
+ _initialised = true;
+ prefs = Inkscape::Preferences::get();
+ default_dpi = prefs->getDouble("/dialogs/export/defaultxdpi/value", DPI_BASE);
+
+ Gtk::Button *add_button = Gtk::manage(new Gtk::Button());
+ Glib::ustring label = _("Add Export");
+ add_button->set_label(label);
+ this->attach(*add_button, 0, 0, 4, 1);
+
+ this->insert_row(0);
+
+ Gtk::Label *suffix_label = Gtk::manage(new Gtk::Label(_("Suffix")));
+ this->attach(*suffix_label, _suffix_col, 0, 1, 1);
+ suffix_label->show();
+
+ Gtk::Label *extension_label = Gtk::manage(new Gtk::Label(_("Format")));
+ this->attach(*extension_label, _extension_col, 0, 2, 1);
+ extension_label->show();
+
+ Gtk::Label *dpi_label = Gtk::manage(new Gtk::Label(_("DPI")));
+ this->attach(*dpi_label, _dpi_col, 0, 1, 1);
+ dpi_label->show();
+
+ append_row();
+
+ add_button->signal_clicked().connect(sigc::mem_fun(*this, &ExportList::append_row));
+ add_button->set_hexpand(true);
+ add_button->show();
+
+ this->set_row_spacing(5);
+ this->set_column_spacing(2);
+}
+
+void ExportList::removeExtension(Glib::ustring &filename)
+{
+ ExtensionList *extension_cb = dynamic_cast<ExtensionList *>(this->get_child_at(_extension_col, 1));
+ if (extension_cb) {
+ extension_cb->removeExtension(filename);
+ return;
+ }
+}
+
+void ExportList::append_row()
+{
+ int current_row = _num_rows + 1; // because we have label row at top
+ this->insert_row(current_row);
+
+ Gtk::Entry *suffix = Gtk::manage(new Gtk::Entry());
+ this->attach(*suffix, _suffix_col, current_row, 1, 1);
+ suffix->set_width_chars(2);
+ suffix->set_hexpand(true);
+ suffix->set_placeholder_text(_("Suffix"));
+ suffix->show();
+
+ ExtensionList *extension = Gtk::manage(new ExtensionList());
+ SpinButton *dpi_sb = Gtk::manage(new SpinButton());
+
+ extension->setup();
+ extension->show();
+ this->attach(*extension, _extension_col, current_row, 1, 1);
+ this->attach(*extension->getPrefButton(), _prefs_col, current_row, 1, 1);
+
+ // Disable DPI when not using a raster image output
+ extension->signal_changed().connect([=]() {
+ if (auto ext = extension->getExtension()) {
+ dpi_sb->set_sensitive(ext->is_raster());
+ }
+ });
+
+ dpi_sb->set_digits(2);
+ dpi_sb->set_increments(0.1, 1.0);
+ dpi_sb->set_range(1.0, 100000.0);
+ dpi_sb->set_value(default_dpi);
+ dpi_sb->set_sensitive(true);
+ dpi_sb->set_width_chars(6);
+ dpi_sb->set_max_width_chars(6);
+ dpi_sb->show();
+ this->attach(*dpi_sb, _dpi_col, current_row, 1, 1);
+
+ Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("window-close", Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ Gtk::Button *delete_btn = Gtk::manage(new Gtk::Button());
+ delete_btn->set_relief(Gtk::RELIEF_NONE);
+ delete_btn->add(*pIcon);
+ delete_btn->show_all();
+ delete_btn->set_no_show_all(true);
+ this->attach(*delete_btn, _delete_col, current_row, 1, 1);
+ delete_btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &ExportList::delete_row), delete_btn));
+
+ _num_rows++;
+}
+
+void ExportList::delete_row(Gtk::Widget *widget)
+{
+ if (widget == nullptr) {
+ return;
+ }
+ if (_num_rows <= 1) {
+ return;
+ }
+ int row = this->child_property_top_attach(*widget);
+ this->remove_row(row);
+ _num_rows--;
+ if (_num_rows <= 1) {
+ Gtk::Widget *d_button_0 = dynamic_cast<Gtk::Widget *>(this->get_child_at(_delete_col, 1));
+ if (d_button_0) {
+ d_button_0->hide();
+ }
+ }
+}
+
+Glib::ustring ExportList::get_suffix(int row)
+{
+ Glib::ustring suffix = "";
+ Gtk::Entry *entry = dynamic_cast<Gtk::Entry *>(this->get_child_at(_suffix_col, row + 1));
+ if (entry == nullptr) {
+ return suffix;
+ }
+ suffix = entry->get_text();
+ return suffix;
+}
+Inkscape::Extension::Output *ExportList::getExtension(int row)
+{
+ ExtensionList *extension_cb = dynamic_cast<ExtensionList *>(this->get_child_at(_extension_col, row + 1));
+ return extension_cb->getExtension();
+}
+
+double ExportList::get_dpi(int row)
+{
+ double dpi = default_dpi;
+ SpinButton *spin_sb = dynamic_cast<SpinButton *>(this->get_child_at(_dpi_col, row + 1));
+ if (spin_sb == nullptr) {
+ return dpi;
+ }
+ dpi = spin_sb->get_value();
+ return dpi;
+}
+
+
+
+} // 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/export-lists.h b/src/ui/widget/export-lists.h
new file mode 100644
index 0000000..67108f3
--- /dev/null
+++ b/src/ui/widget/export-lists.h
@@ -0,0 +1,114 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Anshudhar Kumar Singh <anshudhar2001@gmail.com>
+ *
+ * Copyright (C) 2021 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SP_EXPORT_HELPER_H
+#define SP_EXPORT_HELPER_H
+
+#include "2geom/rect.h"
+#include "preferences.h"
+#include "ui/widget/scrollprotected.h"
+
+class SPDocument;
+class SPItem;
+class SPPage;
+
+namespace Inkscape {
+ namespace Util {
+ class Unit;
+ }
+ namespace Extension {
+ class Output;
+ }
+namespace UI {
+namespace Dialog {
+
+#define EXPORT_COORD_PRECISION 3
+#define SP_EXPORT_MIN_SIZE 1.0
+#define DPI_BASE Inkscape::Util::Quantity::convert(1, "in", "px")
+
+// Class for storing and manipulating extensions
+class ExtensionList : public Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBoxText>
+{
+public:
+ ExtensionList();
+ ExtensionList(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade);
+ ~ExtensionList() override;
+
+ void setup();
+ Glib::ustring getFileExtension();
+ void setExtensionFromFilename(Glib::ustring const &filename);
+ void removeExtension(Glib::ustring &filename);
+ void createList();
+ Gtk::MenuButton *getPrefButton() const { return _pref_button; }
+ Inkscape::Extension::Output *getExtension();
+
+private:
+ void init();
+ void on_changed() override;
+
+ PrefObserver _watch_pref;
+ std::map<std::string, Inkscape::Extension::Output *> ext_to_mod;
+
+ sigc::connection _popover_signal;
+ Glib::RefPtr<Gtk::Builder> _builder;
+ Gtk::MenuButton *_pref_button = nullptr;
+ Gtk::Popover *_pref_popover = nullptr;
+ Gtk::Viewport *_pref_holder = nullptr;
+};
+
+class ExportList : public Gtk::Grid
+{
+public:
+ ExportList() = default;
+ ExportList(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &)
+ : Gtk::Grid(cobject)
+ {
+ }
+ ~ExportList() override = default;
+
+public:
+ void setup();
+ void append_row();
+ void delete_row(Gtk::Widget *widget);
+ Glib::ustring get_suffix(int row);
+ Inkscape::Extension::Output *getExtension(int row);
+ void removeExtension(Glib::ustring &filename);
+ double get_dpi(int row);
+ int get_rows() { return _num_rows; }
+
+private:
+ typedef Inkscape::UI::Widget::ScrollProtected<Gtk::SpinButton> SpinButton;
+ Inkscape::Preferences *prefs = nullptr;
+ double default_dpi = 96.00;
+
+private:
+ bool _initialised = false;
+ int _num_rows = 0;
+ int _suffix_col = 0;
+ int _extension_col = 1;
+ int _prefs_col = 2;
+ int _dpi_col = 3;
+ int _delete_col = 4;
+};
+
+} // 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/widget/export-preview.cpp b/src/ui/widget/export-preview.cpp
new file mode 100644
index 0000000..cf99258
--- /dev/null
+++ b/src/ui/widget/export-preview.cpp
@@ -0,0 +1,208 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Anshudhar Kumar Singh <anshudhar2001@gmail.com>
+ *
+ * Copyright (C) 2021 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "export-preview.h"
+
+#include "document.h"
+#include "display/cairo-utils.h"
+#include "object/sp-item.h"
+#include "object/sp-root.h"
+#include "util/preview.h"
+#include "io/resource.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/**
+ * A preview drawing object is responsible for constructing a drawing and showing it's contents
+ *
+ * On destruction it will gracefully invoke hide itself. You should destroy this object when
+ * you need to change the document object being used for the preview.
+ */
+PreviewDrawing::PreviewDrawing(SPDocument *doc)
+{
+ _document = doc;
+}
+
+PreviewDrawing::~PreviewDrawing()
+{
+ destruct();
+ _document = nullptr;
+}
+
+void PreviewDrawing::destruct()
+{
+ if (!_visionkey)
+ return;
+
+ // On exiting the document root might have gone already.
+ if (auto root = _document->getRoot()) {
+ root->invoke_hide(_visionkey);
+ }
+ _drawing.reset();
+ _visionkey = 0;
+}
+
+/**
+ * Construct the drawing, when needed
+ */
+void PreviewDrawing::construct()
+{
+ auto drawing = std::make_shared<Inkscape::Drawing>();
+ _visionkey = SPItem::display_key_new(1);
+ if (auto di = _document->getRoot()->invoke_show(*drawing, _visionkey, SP_ITEM_SHOW_DISPLAY)) {
+ drawing->setRoot(di);
+ } else {
+ drawing.reset();
+ }
+
+ if (!_shown_items.empty()) {
+ _document->getRoot()->invoke_hide_except(_visionkey, _shown_items);
+ }
+ _drawing = drawing;
+}
+
+/**
+ * Render the drawing into a cairo image surface.
+ */
+bool PreviewDrawing::render(ExportPreview *widget, uint32_t bg, SPItem *item, unsigned size, Geom::OptRect const &dbox)
+{
+ if (!_drawing || _to_destruct) {
+ if (!_construct_idle.connected()) {
+ _construct_idle = Glib::signal_timeout().connect([=]() {
+ _to_destruct = false;
+ destruct();
+ construct();
+ return false;
+ }, 100);
+ }
+ return false;
+ }
+
+ Geom::OptRect bbox = dbox;
+ DrawingItem *di = nullptr;
+
+ if (item) {
+ bbox = item->documentVisualBounds();
+ di = item->get_arenaitem(_visionkey);
+ } else if (!dbox)
+ bbox = _document->getRoot()->documentVisualBounds();
+
+ if (!bbox)
+ return true; // Force quit
+
+ // Use a callback to set the preview rendering;
+ widget->setPreview(UI::Preview::render_preview(_document, _drawing, bg, di, size, size, *bbox));
+ return true;
+}
+
+/**
+ * Limit the preview to just these items.
+ *
+ * You must call refresh after this for the change to take effect.
+ */
+void PreviewDrawing::set_shown_items(std::vector<SPItem*> &&list)
+{
+ _shown_items = std::move(list);
+ _to_destruct = true;
+}
+
+void ExportPreview::resetPixels(bool new_size)
+{
+ clear();
+ // An icon to use when the preview hasn't loaded yet
+ static Glib::RefPtr<Gdk::Pixbuf> preview_loading;
+ if (!preview_loading || new_size) {
+ using namespace Inkscape::IO::Resource;
+ preview_loading = Gdk::Pixbuf::create_from_file(get_filename(PIXMAPS, "preview_loading.svg"), size, size);
+ }
+ if (preview_loading) {
+ set(preview_loading);
+ }
+ show();
+}
+
+void ExportPreview::setSize(int newSize)
+{
+ size = newSize;
+ resetPixels(true);
+}
+
+ExportPreview::~ExportPreview()
+{
+ refresh_conn.disconnect();
+}
+
+void ExportPreview::setItem(SPItem *item)
+{
+ _item = item;
+ _dbox = {};
+}
+
+void ExportPreview::setBox(Geom::Rect const &bbox)
+{
+ if (bbox.hasZeroArea())
+ return;
+
+ _item = nullptr;
+ _dbox = bbox;
+}
+
+void ExportPreview::setDrawing(std::shared_ptr<PreviewDrawing> drawing)
+{
+ _drawing = drawing;
+}
+
+/*
+ * This is the main function which finally renders the preview.
+ * If dbox is given it will use it.
+ * if item is given and not dbox then item is used.
+ * If both are not given then we simply do nothing.
+ */
+void ExportPreview::queueRefresh()
+{
+ if (!_drawing || _render_idle.connected())
+ return;
+
+ _render_idle = Glib::signal_timeout().connect([=]() {
+ return !_drawing->render(this, _bg_color, _item, size, _dbox);
+ }, 100);
+}
+
+/**
+ * Callback when the rendering is complete.
+ */
+void ExportPreview::setPreview(Cairo::RefPtr<Cairo::ImageSurface> surface)
+{
+ if (surface) {
+ set(Gdk::Pixbuf::create(surface, 0, 0, surface->get_width(), surface->get_height()));
+ show();
+ }
+}
+
+void ExportPreview::setBackgroundColor(uint32_t bg_color)
+{
+ _bg_color = bg_color;
+}
+
+} // 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/export-preview.h b/src/ui/widget/export-preview.h
new file mode 100644
index 0000000..e0be123
--- /dev/null
+++ b/src/ui/widget/export-preview.h
@@ -0,0 +1,99 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Anshudhar Kumar Singh <anshudhar2001@gmail.com>
+ *
+ * Copyright (C) 2021 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_EXPORT_PREVIEW_H
+#define INKSCAPE_UI_WIDGET_EXPORT_PREVIEW_H
+
+#include <cstdint>
+#include <2geom/rect.h>
+#include <gtkmm.h>
+#include "display/drawing.h"
+#include "helper/auto-connection.h"
+#include "async/channel.h"
+
+class SPDocument;
+class SPObject;
+class SPItem;
+
+namespace Inkscape {
+class Drawing;
+
+namespace UI {
+namespace Dialog {
+class ExportPreview;
+
+class PreviewDrawing
+{
+public:
+ PreviewDrawing(SPDocument *document);
+ ~PreviewDrawing();
+
+ bool render(ExportPreview *widget, uint32_t bg, SPItem *item, unsigned size, Geom::OptRect const &dboxIn);
+ void set_shown_items(std::vector<SPItem*> &&list = {});
+
+private:
+ void destruct();
+ void construct();
+
+ SPDocument *_document = nullptr;
+ std::shared_ptr<Inkscape::Drawing> _drawing;
+ unsigned _visionkey = 0;
+ bool _to_destruct = false;
+
+ std::vector<SPItem*> _shown_items;
+ Inkscape::auto_connection _construct_idle;
+};
+
+class ExportPreview final : public Gtk::Image
+{
+public:
+ ExportPreview() = default;
+ ExportPreview(BaseObjectType *cobj, Glib::RefPtr<Gtk::Builder> const &) : Gtk::Image(cobj) {}
+ ~ExportPreview() override;
+
+ void setDrawing(std::shared_ptr<PreviewDrawing> drawing);
+ void setItem(SPItem *item);
+ void setBox(Geom::Rect const &bbox);
+ void queueRefresh();
+ void resetPixels(bool new_size = false);
+ void setSize(int newSize);
+ void setPreview(Cairo::RefPtr<Cairo::ImageSurface>);
+ void setBackgroundColor(uint32_t bg_color);
+
+ static std::shared_ptr<Inkscape::Drawing> makeDrawing(SPDocument *doc);
+
+private:
+ int size = 128; // size of preview image
+ sigc::connection refresh_conn;
+
+ SPItem *_item = nullptr;
+ Geom::OptRect _dbox;
+
+ std::shared_ptr<PreviewDrawing> _drawing;
+ uint32_t _bg_color = 0;
+
+ Inkscape::auto_connection _render_idle;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_EXPORT_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/fill-style.cpp b/src/ui/widget/fill-style.cpp
new file mode 100644
index 0000000..124d6d8
--- /dev/null
+++ b/src/ui/widget/fill-style.cpp
@@ -0,0 +1,738 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Fill style widget.
+ */
+/* 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
+ *
+ * Copyright (C) 1999-2005 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ * Copyright (C) 2010 Jon A. Cruz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#define noSP_FS_VERBOSE
+
+#include <glibmm/i18n.h>
+
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "fill-style.h"
+#include "gradient-chemistry.h"
+#include "inkscape.h"
+#include "selection.h"
+
+#include "actions/actions-tools.h"
+#include "object/sp-defs.h"
+#include "object/sp-linear-gradient.h"
+#include "object/sp-mesh-gradient.h"
+#include "object/sp-pattern.h"
+#include "object/sp-radial-gradient.h"
+#include "object/sp-text.h"
+#include "object/sp-stop.h"
+#include "object/sp-object.h"
+#include "ui/dialog/dialog-base.h"
+#include "style.h"
+#include "object/sp-use.h"
+#include "pattern-manipulation.h"
+#include "ui/icon-names.h"
+
+// These can be deleted once we sort out the libart dependence.
+
+#define ART_WIND_RULE_NONZERO 0
+
+/* Fill */
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+FillNStroke::FillNStroke(FillOrStroke k)
+ : Gtk::Box(Gtk::ORIENTATION_VERTICAL)
+ , kind(k)
+ , subselChangedConn()
+ , eventContextConn()
+{
+ // Add and connect up the paint selector widget:
+ _psel = Gtk::manage(new UI::Widget::PaintSelector(kind));
+ _psel->show();
+ add(*_psel);
+ _psel->signal_mode_changed().connect(sigc::mem_fun(*this, &FillNStroke::paintModeChangeCB));
+ _psel->signal_dragged().connect(sigc::mem_fun(*this, &FillNStroke::dragFromPaint));
+ _psel->signal_changed().connect(sigc::mem_fun(*this, &FillNStroke::paintChangedCB));
+ _psel->signal_stop_selected().connect([=](SPStop* stop) {
+ if (_desktop) { _desktop->emit_gradient_stop_selected(this, stop); }
+ });
+ _psel->signal_edit_pattern().connect([=](){
+ if (_desktop) set_active_tool(_desktop, "Node");
+ });
+
+ if (kind == FILL) {
+ _psel->signal_fillrule_changed().connect(sigc::mem_fun(*this, &FillNStroke::setFillrule));
+ }
+
+ performUpdate();
+}
+
+FillNStroke::~FillNStroke()
+{
+ if (_drag_id) {
+ g_source_remove(_drag_id);
+ _drag_id = 0;
+ }
+
+ _psel = nullptr;
+ subselChangedConn.disconnect();
+ eventContextConn.disconnect();
+}
+
+/**
+ * On signal modified, invokes an update of the fill or stroke style paint object.
+ */
+void FillNStroke::selectionModifiedCB(guint flags)
+{
+ if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) {
+#ifdef SP_FS_VERBOSE
+ g_message("selectionModifiedCB(%d) on %p", flags, this);
+#endif
+ performUpdate();
+ }
+}
+
+void FillNStroke::setDesktop(SPDesktop *desktop)
+{
+ if (_desktop != desktop) {
+ if (_drag_id) {
+ g_source_remove(_drag_id);
+ _drag_id = 0;
+ }
+ if (_desktop) {
+ subselChangedConn.disconnect();
+ eventContextConn.disconnect();
+ stop_selected_connection.disconnect();
+ }
+ _desktop = desktop;
+ if (desktop && desktop->getSelection()) {
+ subselChangedConn = desktop->connect_text_cursor_moved([=](void* sender, Inkscape::UI::Tools::TextTool* tool) {
+ performUpdate();
+ });
+
+ eventContextConn = desktop->connectEventContextChanged(sigc::hide(sigc::bind(
+ sigc::mem_fun(*this, &FillNStroke::eventContextCB), (Inkscape::UI::Tools::ToolBase *)nullptr)));
+
+ stop_selected_connection = desktop->connect_gradient_stop_selected([=](void* sender, SPStop* stop){
+ if (sender != this) {
+ performUpdate();
+ }
+ });
+ }
+ performUpdate();
+ }
+}
+
+/**
+ * Listen to this "change in tool" event, in case a subselection tool (such as Gradient or Node) selection
+ * is changed back to a selection tool - especially needed for selected gradient stops.
+ */
+void FillNStroke::eventContextCB(SPDesktop * /*desktop*/, Inkscape::UI::Tools::ToolBase * /*eventcontext*/)
+{
+ performUpdate();
+}
+
+/**
+ * Gets the active fill or stroke style property, then sets the appropriate
+ * color, alpha, gradient, pattern, etc. for the paint-selector.
+ *
+ * @param sel Selection to use, or NULL.
+ */
+void FillNStroke::performUpdate()
+{
+ if (_update || !_desktop) {
+ return;
+ }
+ auto *widg = get_parent()->get_parent()->get_parent()->get_parent();
+ auto dialogbase = dynamic_cast<Inkscape::UI::Dialog::DialogBase*>(widg);
+ if (dialogbase && !dialogbase->getShowing()) {
+ return;
+ }
+ if (_drag_id) {
+ // local change; do nothing, but reset the flag
+ g_source_remove(_drag_id);
+ _drag_id = 0;
+ return;
+ }
+
+ _update = true;
+
+ // create temporary style
+ SPStyle query(_desktop->doc());
+
+ // query style from desktop into it. This returns a result flag and fills query with the style of subselection, if
+ // any, or selection
+ const int property = kind == FILL ? QUERY_STYLE_PROPERTY_FILL : QUERY_STYLE_PROPERTY_STROKE;
+ int result = sp_desktop_query_style(_desktop, &query, property);
+ SPIPaint& paint = *query.getFillOrStroke(kind == FILL);
+ auto stop = cast<SPStop>(paint.getTag());
+ if (stop) {
+ // there's a stop selected, which is part of subselection, now query selection only to find selected gradient
+ if (auto selection = _desktop->getSelection()) {
+ std::vector<SPItem*> vec(selection->items().begin(), selection->items().end());
+ result = sp_desktop_query_style_from_list(vec, &query, property);
+ }
+ }
+ SPIPaint &targPaint = *query.getFillOrStroke(kind == FILL);
+ SPIScale24 &targOpacity = *(kind == FILL ? query.fill_opacity.upcast() : query.stroke_opacity.upcast());
+
+ switch (result) {
+ case QUERY_STYLE_NOTHING: {
+ /* No paint at all */
+ _psel->setMode(UI::Widget::PaintSelector::MODE_EMPTY);
+ break;
+ }
+
+ case QUERY_STYLE_SINGLE:
+ case QUERY_STYLE_MULTIPLE_AVERAGED: // TODO: treat this slightly differently, e.g. display "averaged" somewhere
+ // in paint selector
+ case QUERY_STYLE_MULTIPLE_SAME: {
+ auto pselmode = UI::Widget::PaintSelector::getModeForStyle(query, kind);
+ _psel->setMode(pselmode);
+
+ if (kind == FILL) {
+ _psel->setFillrule(query.fill_rule.computed == ART_WIND_RULE_NONZERO
+ ? UI::Widget::PaintSelector::FILLRULE_NONZERO
+ : UI::Widget::PaintSelector::FILLRULE_EVENODD);
+ }
+
+ if (targPaint.set && targPaint.isColor()) {
+ _psel->setColorAlpha(targPaint.value.color, SP_SCALE24_TO_FLOAT(targOpacity.value));
+ } else if (targPaint.set && targPaint.isPaintserver()) {
+ SPPaintServer* server = (kind == FILL) ? query.getFillPaintServer() : query.getStrokePaintServer();
+
+ if (server) {
+ if (is<SPGradient>(server) && cast<SPGradient>(server)->getVector()->isSwatch()) {
+ auto vector = cast<SPGradient>(server)->getVector();
+ _psel->setSwatch(vector);
+ } else if (is<SPLinearGradient>(server)) {
+ auto vector = cast<SPGradient>(server)->getVector();
+ auto lg = cast<SPLinearGradient>(server);
+ _psel->setGradientLinear(vector, lg, stop);
+
+ _psel->setGradientProperties(lg->getUnits(), lg->getSpread());
+ } else if (is<SPRadialGradient>(server)) {
+ auto vector = cast<SPGradient>(server)->getVector();
+ auto rg = cast<SPRadialGradient>(server);
+ _psel->setGradientRadial(vector, rg, stop);
+
+ _psel->setGradientProperties(rg->getUnits(), rg->getSpread());
+#ifdef WITH_MESH
+ } else if (is<SPMeshGradient>(server)) {
+ auto array = cast<SPGradient>(server)->getArray();
+ _psel->setGradientMesh(cast<SPMeshGradient>(array));
+ _psel->updateMeshList(cast<SPMeshGradient>(array));
+#endif
+ } else if (is<SPPattern>(server)) {
+ _psel->updatePatternList(cast<SPPattern>(server));
+ }
+ }
+ }
+ break;
+ }
+
+ case QUERY_STYLE_MULTIPLE_DIFFERENT: {
+ _psel->setMode(UI::Widget::PaintSelector::MODE_MULTIPLE);
+ break;
+ }
+ }
+
+ _update = false;
+}
+
+/**
+ * When the mode is changed, invoke a regular changed handler.
+ */
+void FillNStroke::paintModeChangeCB(UI::Widget::PaintSelector::Mode /*mode*/, bool switch_style)
+{
+#ifdef SP_FS_VERBOSE
+ g_message("paintModeChangeCB()");
+#endif
+ if (!_update) {
+ updateFromPaint(switch_style);
+ }
+}
+
+void FillNStroke::setFillrule(UI::Widget::PaintSelector::FillRule mode)
+{
+ if (!_update && _desktop) {
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_set_property(css, "fill-rule",
+ (mode == UI::Widget::PaintSelector::FILLRULE_EVENODD) ? "evenodd" : "nonzero");
+
+ sp_desktop_set_style(_desktop, css);
+
+ sp_repr_css_attr_unref(css);
+ css = nullptr;
+
+ DocumentUndo::done(_desktop->doc(), _("Change fill rule"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ }
+}
+
+static gchar const *undo_F_label_1 = "fill:flatcolor:1";
+static gchar const *undo_F_label_2 = "fill:flatcolor:2";
+
+static gchar const *undo_S_label_1 = "stroke:flatcolor:1";
+static gchar const *undo_S_label_2 = "stroke:flatcolor:2";
+
+static gchar const *undo_F_label = undo_F_label_1;
+static gchar const *undo_S_label = undo_S_label_1;
+
+gboolean FillNStroke::dragDelayCB(gpointer data)
+{
+ gboolean keepGoing = TRUE;
+ if (data) {
+ FillNStroke *self = reinterpret_cast<FillNStroke *>(data);
+ if (!self->_update) {
+ if (self->_drag_id) {
+ g_source_remove(self->_drag_id);
+ self->_drag_id = 0;
+
+ self->dragFromPaint();
+ self->performUpdate();
+ }
+ keepGoing = FALSE;
+ }
+ } else {
+ keepGoing = FALSE;
+ }
+ return keepGoing;
+}
+
+/**
+ * This is called repeatedly while you are dragging a color slider, only for flat color
+ * modes. Previously it set the color in style but did not update the repr for efficiency, however
+ * this was flakey and didn't buy us almost anything. So now it does the same as _changed, except
+ * lumps all its changes for undo.
+ */
+void FillNStroke::dragFromPaint()
+{
+ if (!_desktop || _update) {
+ return;
+ }
+
+ guint32 when = gtk_get_current_event_time();
+
+ // Don't attempt too many updates per second.
+ // Assume a base 15.625ms resolution on the timer.
+ if (!_drag_id && _last_drag && when && ((when - _last_drag) < 32)) {
+ // local change, do not update from selection
+ _drag_id = g_timeout_add_full(G_PRIORITY_DEFAULT, 33, dragDelayCB, this, nullptr);
+ }
+
+ if (_drag_id) {
+ // previous local flag not cleared yet;
+ // this means dragged events come too fast, so we better skip this one to speed up display
+ // (it's safe to do this in any case)
+ return;
+ }
+ _last_drag = when;
+
+ _update = true;
+
+ switch (_psel->get_mode()) {
+ case UI::Widget::PaintSelector::MODE_SOLID_COLOR: {
+ // local change, do not update from selection
+ _drag_id = g_timeout_add_full(G_PRIORITY_DEFAULT, 100, dragDelayCB, this, nullptr);
+ _psel->setFlatColor(_desktop,
+ (kind == FILL) ? "fill" : "stroke",
+ (kind == FILL) ? "fill-opacity" : "stroke-opacity");
+ DocumentUndo::maybeDone(_desktop->doc(), (kind == FILL) ? undo_F_label : undo_S_label,
+ (kind == FILL) ? _("Set fill color") : _("Set stroke color"),
+ INKSCAPE_ICON("dialog-fill-and-stroke"));
+ break;
+ }
+
+ default:
+ g_warning("file %s: line %d: Paint %d should not emit 'dragged'", __FILE__, __LINE__, _psel->get_mode());
+ break;
+ }
+ _update = false;
+}
+
+void unset_recursive(const char *attribute, SPObject* object)
+{
+ object->removeAttribute(attribute);
+
+ for (auto& child: object->children)
+ {
+ if (is<SPUse> (object)) return;
+ unset_recursive(attribute, &child);
+ }
+}
+/**
+This is called (at least) when:
+1 paint selector mode is switched (e.g. flat color -> gradient)
+2 you finished dragging a gradient node and released mouse
+3 you changed a gradient selector parameter (e.g. spread)
+Must update repr.
+ */
+void FillNStroke::paintChangedCB()
+{
+#ifdef SP_FS_VERBOSE
+ g_message("paintChangedCB()");
+#endif
+ if (!_update) {
+ updateFromPaint();
+ }
+}
+
+void FillNStroke::updateFromPaint(bool switch_style)
+{
+ if (!_desktop) {
+ return;
+ }
+ _update = true;
+
+ auto document = _desktop->getDocument();
+ auto selection = _desktop->getSelection();
+
+ std::vector<SPItem *> const items(selection->items().begin(), selection->items().end());
+
+ switch (_psel->get_mode()) {
+ case UI::Widget::PaintSelector::MODE_EMPTY:
+ // This should not happen.
+ g_warning("file %s: line %d: Paint %d should not emit 'changed'", __FILE__, __LINE__, _psel->get_mode());
+ break;
+ case UI::Widget::PaintSelector::MODE_MULTIPLE:
+ // This happens when you switch multiple objects with different gradients to flat color;
+ // nothing to do here.
+ break;
+
+ case UI::Widget::PaintSelector::MODE_NONE: {
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_set_property(css, (kind == FILL) ? "fill" : "stroke", "none");
+
+ sp_desktop_set_style(_desktop, css, true, true, switch_style);
+
+ sp_repr_css_attr_unref(css);
+ css = nullptr;
+
+ DocumentUndo::done(document, (kind == FILL) ? _("Remove fill") : _("Remove stroke"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ break;
+ }
+
+ case UI::Widget::PaintSelector::MODE_SOLID_COLOR: {
+ _psel->setFlatColor(_desktop, (kind == FILL) ? "fill" : "stroke",
+ (kind == FILL) ? "fill-opacity" : "stroke-opacity");
+ DocumentUndo::maybeDone(_desktop->getDocument(), (kind == FILL) ? undo_F_label : undo_S_label,
+ (kind == FILL) ? _("Set fill color") : _("Set stroke color"),
+ INKSCAPE_ICON("dialog-fill-and-stroke"));
+
+ // on release, toggle undo_label so that the next drag will not be lumped with this one
+ if (undo_F_label == undo_F_label_1) {
+ undo_F_label = undo_F_label_2;
+ undo_S_label = undo_S_label_2;
+ } else {
+ undo_F_label = undo_F_label_1;
+ undo_S_label = undo_S_label_1;
+ }
+
+ break;
+ }
+
+ case UI::Widget::PaintSelector::MODE_GRADIENT_LINEAR:
+ case UI::Widget::PaintSelector::MODE_GRADIENT_RADIAL:
+ case UI::Widget::PaintSelector::MODE_SWATCH:
+ if (!items.empty()) {
+ SPGradientType const gradient_type =
+ (_psel->get_mode() != UI::Widget::PaintSelector::MODE_GRADIENT_RADIAL ? SP_GRADIENT_TYPE_LINEAR
+ : SP_GRADIENT_TYPE_RADIAL);
+ bool createSwatch = (_psel->get_mode() == UI::Widget::PaintSelector::MODE_SWATCH);
+
+ auto vector = _psel->getGradientVector();
+ if (!vector) {
+ /* No vector in paint selector should mean that we just changed mode */
+ SPStyle query(_desktop->doc());
+ int result = objects_query_fillstroke(items, &query, kind == FILL);
+ if (result == QUERY_STYLE_MULTIPLE_SAME) {
+ SPIPaint &targPaint = *query.getFillOrStroke(kind == FILL);
+ SPColor common;
+ if (!targPaint.isColor()) {
+ common = sp_desktop_get_color(_desktop, kind == FILL);
+ } else {
+ common = targPaint.value.color;
+ }
+ vector = sp_document_default_gradient_vector(document, common, 1.0, createSwatch);
+ }
+ if (vector)
+ vector->setSwatch(createSwatch);
+
+ for (auto item : items) {
+ if (!vector) {
+ auto gr = sp_gradient_vector_for_object(
+ document, _desktop, item,
+ (kind == FILL) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE, createSwatch);
+ if (gr) {
+ gr->setSwatch(createSwatch);
+ }
+ sp_item_set_gradient(item, gr, gradient_type,
+ (kind == FILL) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE);
+ } else {
+ sp_item_set_gradient(item, vector, gradient_type,
+ (kind == FILL) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE);
+ }
+ }
+ } else {
+ // We have changed from another gradient type, or modified spread/units within
+ // this gradient type.
+ vector = sp_gradient_ensure_vector_normalized(vector);
+ for (auto item : items) {
+ SPGradient *gr = sp_item_set_gradient(
+ item, vector, gradient_type, (kind == FILL) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE);
+ _psel->pushAttrsToGradient(gr);
+ }
+ }
+
+ for (auto item : items) {
+ // fill and stroke opacity should never be set on gradients since in our user interface
+ // these are controlled by the gradient stops themselves.
+ item->style->clear(kind == FILL ? SPAttr::FILL_OPACITY : SPAttr::STROKE_OPACITY);
+ }
+ DocumentUndo::done(document, (kind == FILL) ? _("Set gradient on fill") : _("Set gradient on stroke"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ }
+ break;
+
+#ifdef WITH_MESH
+ case UI::Widget::PaintSelector::MODE_GRADIENT_MESH:
+
+ if (!items.empty()) {
+ SPCSSAttr *css = nullptr;
+ if (kind == FILL) {
+ // HACK: reset fill-opacity - that 0.75 is annoying; BUT remove this when we have an opacity slider
+ // for all tabs
+ 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 mesh = _psel->getMeshGradient();
+
+ for (auto item : items) {
+
+ // FIXME: see above
+ if (kind == FILL) {
+ sp_repr_css_change_recursive(item->getRepr(), css, "style");
+ }
+
+ // Check if object already has mesh.
+ bool has_mesh = false;
+ SPStyle *style = item->style;
+ if (style) {
+ SPPaintServer *server =
+ (kind == FILL) ? style->getFillPaintServer() : style->getStrokePaintServer();
+ if (server && is<SPMeshGradient>(server))
+ has_mesh = true;
+ }
+
+ if (!mesh || !has_mesh) {
+ // No mesh in document or object does not already have mesh ->
+ // Create new mesh.
+
+ // 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, item, (kind == FILL) ? item->geometricBounds() : item->visualBounds());
+
+ bool isText = is<SPText>(item);
+ sp_style_set_property_url(item, ((kind == FILL) ? "fill" : "stroke"), mg, isText);
+
+ // (*i)->requestModified(SP_OBJECT_MODIFIED_FLAG|SP_OBJECT_STYLE_MODIFIED_FLAG);
+
+ } else {
+ // Using found mesh
+
+ // Duplicate
+ Inkscape::XML::Node *mesh_repr = mesh->getRepr();
+ Inkscape::XML::Node *copy_repr = mesh_repr->duplicate(xml_doc);
+
+ // privates are garbage-collectable
+ copy_repr->setAttribute("inkscape:collect", "always");
+
+ // Attach to document
+ defs->getRepr()->appendChild(copy_repr);
+ Inkscape::GC::release(copy_repr);
+
+ // Get corresponding object
+ SPMeshGradient *mg = static_cast<SPMeshGradient *>(document->getObjectByRepr(copy_repr));
+ // std::cout << " " << (mg->getId()?mg->getId():"null") << std::endl;
+ mg->array.read(mg);
+
+ Geom::OptRect item_bbox = (kind == FILL) ? item->geometricBounds() : item->visualBounds();
+ mg->array.fill_box(item_bbox);
+
+ bool isText = is<SPText>(item);
+ sp_style_set_property_url(item, ((kind == FILL) ? "fill" : "stroke"), mg, isText);
+ }
+ }
+
+ if (css) {
+ sp_repr_css_attr_unref(css);
+ css = nullptr;
+ }
+
+ DocumentUndo::done(document, (kind == FILL) ? _("Set mesh on fill") : _("Set mesh on stroke"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ }
+ break;
+#endif
+
+ case UI::Widget::PaintSelector::MODE_PATTERN:
+
+ if (!items.empty()) {
+
+ auto pattern = _psel->getPattern();
+ if (!pattern) {
+ /* No Pattern in paint selector should mean that we just
+ * changed mode - don't do jack.
+ */
+ } else {
+ auto link_pattern = pattern;
+ auto root_pattern = pattern->rootPattern();
+ if (auto color = _psel->get_pattern_color()) {
+ sp_pattern_set_color(root_pattern, color.value());
+ }
+ // pattern name is applied to the root
+ root_pattern->setAttribute("inkscape:label", _psel->get_pattern_label().c_str());
+ // remaining settings apply to link pattern
+ if (link_pattern != root_pattern) {
+ auto transform = _psel->get_pattern_transform();
+ sp_pattern_set_transform(link_pattern, transform);
+ auto offset = _psel->get_pattern_offset();
+ sp_pattern_set_offset(link_pattern, offset);
+ auto uniform = _psel->is_pattern_scale_uniform();
+ sp_pattern_set_uniform_scale(link_pattern, uniform);
+ // gap requires both patterns, but they are only created later by calling "adjust_pattern" below
+ // it is OK to ignore it for now, during initial creation gap is 0,0
+ auto gap = _psel->get_pattern_gap();
+ sp_pattern_set_gap(link_pattern, gap);
+ }
+
+ Inkscape::XML::Node *patrepr = root_pattern->getRepr();
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ gchar *urltext = g_strdup_printf("url(#%s)", patrepr->attribute("id"));
+ sp_repr_css_set_property(css, (kind == FILL) ? "fill" : "stroke", urltext);
+
+ // HACK: reset fill-opacity - that 0.75 is annoying; BUT remove this when we have an opacity slider
+ // for all tabs
+ if (kind == FILL) {
+ sp_repr_css_set_property(css, "fill-opacity", "1.0");
+ }
+
+ // cannot just call sp_desktop_set_style, because we don't want to touch those
+ // objects who already have the same root pattern but through a different href
+ // chain. FIXME: move this to a sp_item_set_pattern
+ for (auto item : items) {
+ Inkscape::XML::Node *selrepr = item->getRepr();
+ if ((kind == STROKE) && !selrepr) {
+ continue;
+ }
+ SPObject *selobj = item;
+
+ SPStyle *style = selobj->style;
+ if (style && ((kind == FILL) ? style->fill.isPaintserver() : style->stroke.isPaintserver())) {
+ SPPaintServer *server = (kind == FILL) ? selobj->style->getFillPaintServer()
+ : selobj->style->getStrokePaintServer();
+ if (is<SPPattern>(server) && cast<SPPattern>(server)->rootPattern() == root_pattern)
+ // only if this object's pattern is not rooted in our selected pattern, apply
+ continue;
+ }
+
+ if (kind == FILL) {
+ sp_desktop_apply_css_recursive(selobj, css, true);
+ } else {
+ sp_repr_css_change_recursive(selrepr, css, "style");
+ }
+
+ // create link to pattern right away, without waiting for object to be moved;
+ // otherwise pattern editor may end up modifying pattern shared by different objects
+ item->adjust_pattern(Geom::Affine());
+ }
+
+ sp_repr_css_attr_unref(css);
+ css = nullptr;
+ g_free(urltext);
+ } // end if
+
+ DocumentUndo::done(document, (kind == FILL) ? _("Set pattern on fill") : _("Set pattern on stroke"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ } // end if
+
+ break;
+
+ case UI::Widget::PaintSelector::MODE_UNSET:
+ if (!items.empty()) {
+ for (auto item: items) {
+ if (item) {
+ unset_recursive((kind == FILL) ? "fill" : "stroke", item);
+ }
+ }
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ if (kind == FILL) {
+ sp_repr_css_unset_property(css, "fill");
+ } else {
+ 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);
+ sp_repr_css_attr_unref(css);
+ css = nullptr;
+
+ DocumentUndo::done(document, (kind == FILL) ? _("Unset fill") : _("Unset stroke"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ }
+ break;
+
+ default:
+ g_warning("file %s: line %d: Paint selector should not be in "
+ "mode %d",
+ __FILE__, __LINE__, _psel->get_mode());
+ break;
+ }
+
+ _update = 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/fill-style.h b/src/ui/widget/fill-style.h
new file mode 100644
index 0000000..165aec7
--- /dev/null
+++ b/src/ui/widget/fill-style.h
@@ -0,0 +1,85 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Fill style configuration
+ */
+/* Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2010 Jon A. Cruz
+ * Copyright (C) 2002 Lauris Kaplinski
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_DIALOGS_SP_FILL_STYLE_H
+#define SEEN_DIALOGS_SP_FILL_STYLE_H
+
+#include "ui/widget/paint-selector.h"
+
+#include <gtkmm/box.h>
+
+namespace Gtk {
+class Widget;
+}
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+class ToolBase;
+}
+
+namespace Widget {
+
+class FillNStroke : public Gtk::Box {
+ private:
+ FillOrStroke kind;
+ SPDesktop *_desktop = nullptr;
+ PaintSelector *_psel = nullptr;
+ guint32 _last_drag = 0;
+ guint _drag_id = 0;
+ bool _update = false;
+
+ sigc::connection subselChangedConn;
+ sigc::connection eventContextConn;
+ sigc::connection stop_selected_connection;
+
+ void paintModeChangeCB(UI::Widget::PaintSelector::Mode mode, bool switch_style);
+ void paintChangedCB();
+ static gboolean dragDelayCB(gpointer data);
+
+
+ void eventContextCB(SPDesktop *desktop, Inkscape::UI::Tools::ToolBase *eventcontext);
+
+ void dragFromPaint();
+ void updateFromPaint(bool switch_style = false);
+
+ public:
+ FillNStroke(FillOrStroke k);
+ ~FillNStroke() override;
+
+ void selectionModifiedCB(guint flags);
+ void performUpdate();
+
+ void setFillrule(PaintSelector::FillRule mode);
+ void setDesktop(SPDesktop *desktop);
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // SEEN_DIALOGS_SP_FILL_STYLE_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/filter-effect-chooser.cpp b/src/ui/widget/filter-effect-chooser.cpp
new file mode 100644
index 0000000..2d26d9a
--- /dev/null
+++ b/src/ui/widget/filter-effect-chooser.cpp
@@ -0,0 +1,211 @@
+// 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 {
+
+// Blend modes are in six groups according to the types of changes they make to luminosity
+// See: https://typefully.com/DanHollick/blending-modes-KrBa0JP
+// Add 5 to ENDMODE for the five additional separators in the list
+const int SP_CSS_BLEND_COUNT = SP_CSS_BLEND_ENDMODE + 5;
+const EnumData<SPBlendMode> SPBlendModeData[SP_CSS_BLEND_COUNT] = {
+ { SP_CSS_BLEND_NORMAL, NC_("BlendMode", "Normal"), "normal" },
+ { SP_CSS_BLEND_ENDMODE, "-", "-" },
+ { SP_CSS_BLEND_DARKEN, NC_("BlendMode", "Darken"), "darken" },
+ { SP_CSS_BLEND_MULTIPLY, NC_("BlendMode", "Multiply"), "multiply" },
+ { SP_CSS_BLEND_COLORBURN, NC_("BlendMode", "Color Burn"), "color-burn" },
+ { SP_CSS_BLEND_ENDMODE, "-", "-" },
+ { SP_CSS_BLEND_LIGHTEN, NC_("BlendMode", "Lighten"), "lighten" },
+ { SP_CSS_BLEND_SCREEN, NC_("BlendMode", "Screen"), "screen" },
+ { SP_CSS_BLEND_COLORDODGE, NC_("BlendMode", "Color Dodge"), "color-dodge" },
+ { SP_CSS_BLEND_ENDMODE, "-", "-" },
+ { SP_CSS_BLEND_OVERLAY, NC_("BlendMode", "Overlay"), "overlay" },
+ { SP_CSS_BLEND_SOFTLIGHT, NC_("BlendMode", "Soft Light"), "soft-light" },
+ { SP_CSS_BLEND_HARDLIGHT, NC_("BlendMode", "Hard Light"), "hard-light" },
+ { SP_CSS_BLEND_ENDMODE, "-", "-" },
+ { SP_CSS_BLEND_DIFFERENCE, NC_("BlendMode", "Difference"), "difference" },
+ { SP_CSS_BLEND_EXCLUSION, NC_("BlendMode", "Exclusion"), "exclusion" },
+ { SP_CSS_BLEND_ENDMODE, "-", "-" },
+ { SP_CSS_BLEND_HUE, NC_("BlendMode", "Hue"), "hue" },
+ { SP_CSS_BLEND_SATURATION, NC_("BlendMode", "Saturation"), "saturation" },
+ { SP_CSS_BLEND_COLOR, NC_("BlendMode", "Color"), "color" },
+ { SP_CSS_BLEND_LUMINOSITY, NC_("BlendMode", "Luminosity"), "luminosity" }
+};
+const EnumDataConverter<SPBlendMode> SPBlendModeConverter(SPBlendModeData, SP_CSS_BLEND_COUNT);
+
+
+namespace UI {
+namespace Widget {
+
+SimpleFilterModifier::SimpleFilterModifier(int flags)
+ : Gtk::Box(Gtk::ORIENTATION_VERTICAL)
+ , _flags(flags)
+ , _lb_blend(_("Blend mode:"))
+ , _lb_isolation("Isolate") // Translate for 1.1
+ , _blend(SPBlendModeConverter, SPAttr::INVALID, false, "BlendMode")
+ , _blur(_("Blur (%)"), 0, 0, 100, 1, 0.1, 1)
+ , _opacity(_("Opacity (%)"), 0, 0, 100, 1, 0.1, 1)
+ , _notify(true)
+ , _hb_blend(Gtk::ORIENTATION_HORIZONTAL)
+{
+ set_name("SimpleFilterModifier");
+
+ /* "More options" expander --------
+ _extras.set_visible();
+ _extras.set_label(_("More options"));
+ auto box = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_VERTICAL);
+ _extras.add(*box);
+ if (flags & (BLEND | BLUR)) {
+ add(_extras);
+ }
+ */
+
+ _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(0);
+ _hb_blend.set_margin_bottom(1);
+ _hb_blend.set_margin_end(2);
+ _lb_blend.set_mnemonic_widget(_blend);
+ _hb_blend.pack_start(_lb_blend, false, false, 0);
+ _hb_blend.pack_start(_blend, false, false, 0);
+ /*
+ * 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 Inkscape
+ * user "strange" behaviour from the designer point of view.
+ * It's strange because it only happens when object doesn't have: clip, mask,
+ * filter, blending, or opacity.
+ * Anyway the feature is a no-gui feature and renders as expected.
+ */
+ /* 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");
+ } */
+ }
+
+ 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_by_id(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..5001e5b
--- /dev/null
+++ b/src/ui/widget/filter-effect-chooser.h
@@ -0,0 +1,97 @@
+// 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 <gtkmm/expander.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::Box
+{
+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::Expander _extras;
+ Gtk::Box _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-collection-selector.cpp b/src/ui/widget/font-collection-selector.cpp
new file mode 100644
index 0000000..70d1e11
--- /dev/null
+++ b/src/ui/widget/font-collection-selector.cpp
@@ -0,0 +1,674 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Vaibhav Malik <vaibhavmalik2018@gmail.com>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+#include <glibmm/markup.h>
+
+#include "font-collection-selector.h"
+
+#include "libnrtype/font-lister.h"
+
+// For updating from selection
+#include "util/document-fonts.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+FontCollectionSelector::FontCollectionSelector()
+{
+ // Step 1: Initialize the treeview.
+ treeview = Gtk::manage(new Gtk::TreeView());
+
+ // Step 2: Setup the treeview.
+ setup_tree_view(treeview);
+
+ // Step 3: Intialize the model.
+ store = Gtk::TreeStore::create(FontCollection);
+ // Step 4: Populate the ListStore.
+ treeview->set_model(store);
+
+ // Signals.
+ setup_signals();
+
+ show_all_children();
+}
+
+// Setup the treeview of the widget.
+void FontCollectionSelector::setup_tree_view(Gtk::TreeView *tv)
+{
+ cell_text = new Gtk::CellRendererText();
+ del_icon_renderer = manage(new Inkscape::UI::Widget::IconRenderer());
+ del_icon_renderer->add_icon("edit-delete");
+
+ text_column.pack_start (*cell_text, true);
+ text_column.add_attribute (*cell_text, "text", TEXT_COLUMN);
+ text_column.set_expand(true);
+
+ del_icon_column.pack_start (*del_icon_renderer, false);
+
+ // Attach the cell data functions.
+ text_column.set_cell_data_func(*cell_text, sigc::mem_fun(*this, &FontCollectionSelector::text_cell_data_func));
+
+ treeview->enable_model_drag_dest (Gdk::ACTION_MOVE);
+ treeview->set_headers_visible (false);
+
+ // Target entries for Drag and Drop.
+ target_entries.emplace_back(Gtk::TargetEntry("STRING", (Gtk::TargetFlags)0, 0));
+ target_entries.emplace_back(Gtk::TargetEntry("text/plain", (Gtk::TargetFlags)0, 0));
+
+ treeview->drag_dest_set(target_entries, Gtk::DEST_DEFAULT_ALL, Gdk::ACTION_COPY);
+
+ // Append the columns to the treeview.
+ treeview->append_column(text_column);
+ treeview->append_column(del_icon_column);
+
+ scroll.set_policy (Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ scroll.set_overlay_scrolling(false);
+ scroll.add (*treeview);
+
+ frame.set_hexpand (true);
+ frame.set_vexpand (true);
+ frame.add (scroll);
+
+ // Grid
+ set_name("FontCollection");
+ set_row_spacing(4);
+ set_column_spacing(1);
+
+ // Add extra columns to the "frame" to change space distribution
+ attach (frame, 0, 0, 1, 2);
+}
+
+void FontCollectionSelector::change_frame_name(const Glib::ustring& name)
+{
+ frame.set_label(name);
+}
+
+void FontCollectionSelector::setup_signals()
+{
+ cell_text->signal_edited().connect(sigc::mem_fun(*this, &FontCollectionSelector::on_rename_collection));
+ del_icon_renderer->signal_activated().connect(sigc::mem_fun(*this, &FontCollectionSelector::on_delete_icon_clicked));
+ treeview->signal_key_press_event().connect([=](GdkEventKey *ev){ return on_key_pressed(ev); });
+ treeview->set_row_separator_func(sigc::mem_fun(*this, &FontCollectionSelector::row_separator_func));
+ treeview->get_column(ICON_COLUMN)->set_cell_data_func(*del_icon_renderer, sigc::mem_fun(*this, &FontCollectionSelector::icon_cell_data_func));
+
+ // Signals for drag and drop.
+ treeview->signal_drag_motion().connect(sigc::mem_fun(*this, &FontCollectionSelector::on_drag_motion), false);
+ treeview->signal_drag_data_received().connect(sigc::mem_fun(*this, &FontCollectionSelector::on_drag_data_received), false);
+ treeview->signal_drag_drop().connect(sigc::mem_fun(*this, &FontCollectionSelector::on_drag_drop), false);
+ // treeview->signal_drag_failed().connect(sigc::mem_fun(*this, &FontCollectionSelector::on_drag_failed), false);
+ treeview->signal_drag_leave().connect(sigc::mem_fun(*this, &FontCollectionSelector::on_drag_leave), false);
+ treeview->signal_drag_end().connect(sigc::mem_fun(*this, &FontCollectionSelector::on_drag_end), false);
+ treeview->get_selection()->signal_changed().connect([=](){ on_selection_changed(); });
+ Inkscape::RecentlyUsedFonts::get()->connectUpdate(sigc::mem_fun(*this, &FontCollectionSelector::populate_system_collections));
+}
+
+// To distinguish the collection name and the font name.
+Glib::ustring FontCollectionSelector::get_text_cell_markup(Gtk::TreeIter const &iter)
+{
+ Glib::ustring markup;
+ auto parent = (*iter)->parent();
+
+ if(parent) {
+ // It is a font.
+ markup = "<span alpha='50%'>";
+ markup += (*iter)[FontCollection.name];
+ markup += "</span>";
+ }
+ else {
+ // It is a collection.
+ markup = "<span>";
+ markup += (*iter)[FontCollection.name];
+ markup += "</span>";
+ }
+
+ return markup;
+}
+
+// This function will TURN OFF the visibility of the delete icon for system collections.
+void FontCollectionSelector::text_cell_data_func(Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter)
+{
+ // Add the delete icon only if the collection is editable(user-collection).
+ Glib::ustring markup = get_text_cell_markup(iter);
+ renderer->set_property("markup", markup);
+}
+
+// This function will TURN OFF the visibility of the delete icon for system collections.
+void FontCollectionSelector::icon_cell_data_func(Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter)
+{
+ // Add the delete icon only if the collection is editable(user-collection).
+ Gtk::TreeModel::Row row = *iter;
+ auto parent = (*iter)->parent();
+
+ if(parent) {
+ // Case: It is a font.
+ bool is_user = (*parent)[FontCollection.is_editable];
+ del_icon_renderer->set_visible(is_user);
+ cell_text->property_editable() = false;
+ } else if((*iter)[FontCollection.is_editable]) {
+ // Case: User font collection.
+ del_icon_renderer->set_visible(true);
+ cell_text->property_editable() = true;
+ } else {
+ // Case: System font collection.
+ del_icon_renderer->set_visible(false);
+ cell_text->property_editable() = false;
+ }
+}
+
+// This function will TURN OFF the visibility of checkbuttons for children in the TreeStore.
+void FontCollectionSelector::check_button_cell_data_func(Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter)
+{
+ renderer->set_visible(false);
+ /*
+ // Append the checkbutton column only if the iterator have some children.
+ Gtk::TreeModel::Row row = *iter;
+ auto parent = row->parent();
+
+ if(parent) {
+ renderer->set_visible(false);
+ }
+ else {
+ renderer->set_visible(true);
+ }
+ */
+}
+
+bool FontCollectionSelector::row_separator_func(const Glib::RefPtr<Gtk::TreeModel>& model, const Gtk::TreeModel::iterator& iter)
+{
+ return (*iter)[FontCollection.name] == "#";
+}
+
+void FontCollectionSelector::populate_collections()
+{
+ store->clear();
+ populate_system_collections();
+ populate_user_collections();
+}
+
+// This function will keep the populate the system collections and their fonts.
+void FontCollectionSelector::populate_system_collections()
+{
+ FontCollections *font_collections = Inkscape::FontCollections::get();
+ std::vector <Glib::ustring> system_collections = font_collections->get_collections(true);
+
+ // Erase the previous collections.
+ store->freeze_notify();
+ Gtk::TreePath path;
+ path.push_back(0);
+ Gtk::TreeModel::iterator iter;
+ bool row_0 = false, row_1 = false;
+
+ for(int i = 0; i < 3; i++) {
+ iter = store->get_iter(path);
+ if(iter) {
+ if(treeview->row_expanded(path)) {
+ if(i == 0) {
+ row_0 = true;
+ } else if(i == 1) {
+ row_1 = true;
+ }
+ }
+ store->erase(iter);
+ }
+ }
+
+ // Insert a separator.
+ iter = store->prepend();
+ (*iter)[FontCollection.name] = "#";
+ (*iter)[FontCollection.is_editable] = false;
+ iter = store->children();
+
+ for(auto const &col: system_collections) {
+ iter = store->prepend();
+ (*iter)[FontCollection.name] = col;
+ (*iter)[FontCollection.is_editable] = false;
+ }
+
+ populate_document_fonts();
+ populate_recently_used_fonts();
+ store->thaw_notify();
+
+ if(row_0) {
+ treeview->expand_row(Gtk::TreePath("0"), true);
+ }
+ if(row_1) {
+ treeview->expand_row(Gtk::TreePath("1"), true);
+ }
+}
+
+void FontCollectionSelector::populate_document_fonts()
+{
+ // The position of the recently used collection is hardcoded for now.
+ Gtk::TreePath path;
+ path.push_back(1);
+ Gtk::TreeModel::iterator iter = store->get_iter(path);
+
+ for(auto const& font: Inkscape::DocumentFonts::get()->get_fonts()) {
+ Gtk::TreeModel::iterator child = store->append((*iter).children());
+ (*child)[FontCollection.name] = font;
+ (*child)[FontCollection.is_editable] = false;
+ }
+}
+
+void FontCollectionSelector::populate_recently_used_fonts()
+{
+ // The position of the recently used collection is hardcoded for now.
+ Gtk::TreePath path;
+ path.push_back(0);
+ Gtk::TreeModel::iterator iter = store->get_iter(path);
+
+ for(auto const& font: Inkscape::RecentlyUsedFonts::get()->get_fonts()) {
+ Gtk::TreeModel::iterator child = store->append((*iter).children());
+ (*child)[FontCollection.name] = font;
+ (*child)[FontCollection.is_editable] = false;
+ }
+}
+
+// This function will keep the collections_list updated after any event.
+void FontCollectionSelector::populate_user_collections()
+{
+ // Get the list of all the user collections.
+ auto collections = Inkscape::FontCollections::get()->get_collections();
+
+ // Now insert these collections one by one into the treeview.
+ store->freeze_notify();
+ Gtk::TreeModel::iterator iter;
+
+ for(const auto &col: collections) {
+ iter = store->append();
+ (*iter)[FontCollection.name] = col;
+
+ // User collections are editable.
+ (*iter)[FontCollection.is_editable] = true;
+
+ // Alright, now populate the fonts of this collection.
+ populate_fonts(col);
+ }
+ store->thaw_notify();
+}
+
+void FontCollectionSelector::populate_fonts(const Glib::ustring& collection_name)
+{
+ // Get the FontLister instance to get the list of all the collections.
+ FontCollections *font_collections = Inkscape::FontCollections::get();
+ std::set <Glib::ustring> fonts = font_collections->get_fonts(collection_name);
+
+ // First find the location of this collection_name in the map.
+ // +1 for the separator.
+ int index = font_collections->get_user_collection_location(collection_name) + 1;
+
+ store->freeze_notify();
+
+ // Generate the iterator path.
+ Gtk::TreePath path;
+ path.push_back(index);
+ Gtk::TreeModel::iterator iter = store->get_iter(path);
+
+ // auto child_iter = iter->children();
+ auto size = iter->children().size();
+
+ // Clear the previously stored fonts at this path.
+ while(size--) {
+ Gtk::TreeModel::iterator child = iter->children().begin();
+ store->erase(child);
+ }
+
+ for(auto const &font: fonts) {
+ Gtk::TreeModel::iterator child = store->append((*iter).children());
+ (*child)[FontCollection.name] = font;
+ (*child)[FontCollection.is_editable] = false;
+ }
+
+ store->thaw_notify();
+}
+
+void FontCollectionSelector::on_delete_icon_clicked(Glib::ustring const &path)
+{
+ FontCollections *collections = Inkscape::FontCollections::get();
+ Gtk::TreeModel::iterator iter = store->get_iter(path);
+ auto parent = (*iter)->parent();
+ if(!parent) {
+ // It is a collection.
+ // No need to confirm in case of empty collections.
+ if (!collections->get_fonts((*iter)[FontCollection.name]).empty()) {
+ // Warn the user and then proceed.
+ int response = deleltion_warning_message_dialog((*iter)[FontCollection.name]);
+ if (response != Gtk::RESPONSE_YES) {
+ return;
+ }
+ }
+ collections->remove_collection((*iter)[FontCollection.name]);
+ }
+ else {
+ // It is a font.
+ collections->remove_font((*parent)[FontCollection.name], (*iter)[FontCollection.name]);
+ }
+ store->erase(iter);
+}
+
+void FontCollectionSelector::on_create_collection()
+{
+ Gtk::TreeModel::iterator iter = store->append();
+ (*iter)[FontCollection.is_editable] = true;
+
+ Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iter;
+ treeview->set_cursor(path, text_column, true);
+ grab_focus();
+}
+
+void FontCollectionSelector::on_rename_collection(const Glib::ustring& path, const Glib::ustring& new_text)
+{
+ // Fetch the collections.
+ FontCollections *collections = Inkscape::FontCollections::get();
+
+ // Check if the same collection is already present.
+ bool is_system = collections->find_collection(new_text, true);
+ bool is_user = collections->find_collection(new_text, false);
+
+ // Return if the new name is empty.
+ // Do not allow user collections to be named as system collections.
+ if (new_text == "" || is_system || is_user) {
+ return;
+ }
+
+ Gtk::TreeModel::iterator iter = store->get_iter(path);
+
+ // Return if it is not a valid iter.
+ if(!iter) {
+ return;
+ }
+
+ // To check if it's a font-collection or a font.
+ auto parent = (*iter)->parent();
+
+ if(!parent) {
+ // Call the rename_collection function
+ collections->rename_collection((*iter)[FontCollection.name], new_text);
+ }
+ else {
+ collections->rename_font((*parent)[FontCollection.name], (*iter)[FontCollection.name], new_text);
+ }
+
+ (*iter)[FontCollection.name] = new_text;
+ populate_collections();
+}
+
+void FontCollectionSelector::on_delete_button_pressed()
+{
+ // Get the current collection.
+ Glib::RefPtr<Gtk::TreeSelection> selection = treeview->get_selection();
+ Gtk::TreeModel::iterator iter = selection->get_selected();
+ Gtk::TreeModel::Row row = *iter;
+ auto parent = row->parent();
+
+ FontCollections *collections = Inkscape::FontCollections::get();
+
+ if(!parent) {
+ // It is a collection.
+ // Check if it is a system collection.
+ bool is_system = collections->find_collection((*iter)[FontCollection.name], true);
+ if(is_system) {
+ return;
+ }
+
+ // Warn the user and then proceed.
+ int response = deleltion_warning_message_dialog((*iter)[FontCollection.name]);
+
+ if (response != Gtk::RESPONSE_YES) {
+ return;
+ }
+
+ collections->remove_collection((*iter)[FontCollection.name]);
+ }
+ else {
+ // It is a font.
+ // Check if it belongs to a system collection.
+ bool is_system = collections->find_collection((*parent)[FontCollection.name], true);
+
+ if(is_system) {
+ return;
+ }
+
+ collections->remove_font((*parent)[FontCollection.name], row[FontCollection.name]);
+ }
+ store->erase(iter);
+}
+
+// Function to edit the name of the collection or font.
+void FontCollectionSelector::on_edit_button_pressed()
+{
+ Glib::RefPtr<Gtk::TreeSelection> selection = treeview->get_selection();
+
+ if(selection) {
+ Gtk::TreeModel::iterator iter = selection->get_selected();
+ if(!iter) {
+ return;
+ }
+
+ Gtk::TreeModel::Row row = *iter;
+ auto parent = row->parent();
+ bool is_system = Inkscape::FontCollections::get()->find_collection((*iter)[FontCollection.name], true);
+
+ if(!parent && !is_system) {
+ // It is a collection.
+ treeview->set_cursor(Gtk::TreePath(iter), text_column, true);
+ }
+ }
+}
+
+int FontCollectionSelector::deleltion_warning_message_dialog(const Glib::ustring &collection_name)
+{
+ Glib::ustring message =
+ Glib::ustring::compose(_("Are you sure want to delete the \"%1\" font collection?\n"), collection_name);
+ Gtk::MessageDialog dialog(message, false, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_YES_NO, true);
+ dialog.set_transient_for(*dynamic_cast<Gtk::Window *>(get_toplevel()));
+ return dialog.run();
+}
+
+bool FontCollectionSelector::on_key_pressed(GdkEventKey *event)
+{
+ if (event->type == GDK_KEY_PRESS && frame.get_label() == "Collections")
+ {
+ // std::cout << "Key pressed" << std::endl;
+ switch (Inkscape::UI::Tools::get_latin_keyval (event)) {
+ case GDK_KEY_Delete:
+ on_delete_button_pressed();
+ break;
+ }
+ // We handled this event.
+ return true;
+ }
+ // We did not handle this event.
+ return false;
+}
+
+bool FontCollectionSelector::on_drag_motion(const Glib::RefPtr<Gdk::DragContext> &context,
+ int x,
+ int y,
+ guint time)
+{
+ Gtk::TreeModel::Path path;
+ Gtk::TreeViewDropPosition pos;
+
+ treeview->get_dest_row_at_pos(x, y, path, pos);
+ treeview->drag_unhighlight();
+
+ if (path) {
+ context->drag_status(Gdk::ACTION_COPY, time);
+ return false;
+ }
+
+ // remove drop highlight
+ context->drag_refuse(time);
+ return true;
+}
+
+void FontCollectionSelector::on_drag_data_received(const Glib::RefPtr<Gdk::DragContext> context,
+ int x,
+ int y,
+ const Gtk::SelectionData &selection_data,
+ guint info, guint time)
+{
+ // std::cout << "FontCollectionSelector::on_drag_data_received()" << std::endl;
+ // 1. Get the row at which the data is dropped.
+ Gtk::TreePath path;
+ treeview->get_path_at_pos(x, y, path);
+ Gtk::TreeModel::iterator iter = store->get_iter(path);
+ bool is_expanded = false;
+
+ // Case when the font is dragged in the empty space.
+ if(!iter) {
+ return;
+ }
+
+ Glib::ustring collection_name = (*iter)[FontCollection.name];
+ auto font_name = Inkscape::FontLister::get_instance()->get_dragging_family();
+
+ FontCollections *collections = Inkscape::FontCollections::get();
+ std::vector <Glib::ustring> system_collections = collections->get_collections(true);
+ auto parent = (*iter)->parent();
+
+ if(parent) {
+ is_expanded = true;
+ collection_name = (*parent)[FontCollection.name];
+ bool is_system = collections->find_collection(collection_name, true);
+
+ if(is_system) {
+ // The font is dropped in a system collection.
+ return;
+ }
+ } else {
+ if (treeview->row_expanded(path)) {
+ is_expanded = true;
+ }
+
+ bool is_system = collections->find_collection(collection_name, true);
+
+ if(is_system) {
+ // The font is dropped in a system collection.
+ return;
+ }
+ }
+
+ // 2. Get the data that is sent by the source.
+ // std::cout << "Received: " << selection_data.get_data() << std::endl;
+ // std::cout << (*iter)[FontCollection.name] << std::endl;
+ // Add the font into the collection.
+ collections->add_font(collection_name, font_name);
+
+ // Re-populate the collection.
+ populate_fonts(collection_name);
+
+ // Re-expand this row after re-population.
+ if(is_expanded) {
+ treeview->expand_to_path(path);
+ }
+
+ // Call gtk_drag_finish(context, success, del = false, time)
+ gtk_drag_finish(context->gobj(), TRUE, FALSE, time);
+}
+
+bool FontCollectionSelector::on_drag_drop(const Glib::RefPtr<Gdk::DragContext> &context,
+ int x,
+ int y,
+ guint time)
+{
+ // std::cout << "FontCollectionSelector::on_drag_drop()" << std::endl;
+ Gtk::TreeModel::Path path;
+ Gtk::TreeViewDropPosition pos;
+ treeview->get_dest_row_at_pos(x, y, path, pos);
+
+ if (!path) {
+ // std::cout << "Not on target\n";
+ return false;
+ }
+
+ on_drag_end(context);
+ return true;
+}
+
+/*
+bool FontCollectionSelector::on_drag_failed(const Glib::RefPtr<Gdk::DragContext> &context,
+ const Gtk::DragResult result)
+{
+ std::cout << "Drag Failed\n";
+ return true;
+}
+*/
+
+void FontCollectionSelector::on_drag_leave(const Glib::RefPtr<Gdk::DragContext> &context,
+ guint time)
+{
+ // std::cout << "Drag Leave\n";
+}
+
+/*
+void FontCollectionSelector::on_drag_start(const Glib::RefPtr<Gdk::DragContext> &context)
+{
+ // std::cout << "FontCollectionSelector::on_drag_start()" << std::endl;
+}
+*/
+
+void FontCollectionSelector::on_drag_end(const Glib::RefPtr<Gdk::DragContext> &context)
+{
+ // std::cout << "FontCollection::on_drag_end()" << std::endl;
+ treeview->drag_unhighlight();
+}
+
+void FontCollectionSelector::on_selection_changed()
+{
+ Glib::RefPtr <Gtk::TreeSelection> selection = treeview->get_selection();
+ if(selection) {
+ FontCollections *font_collections = Inkscape::FontCollections::get();
+ Gtk::TreeModel::iterator iter = selection->get_selected();
+ auto parent = iter->parent();
+
+ // We use 3 states to adjust the sensitivity of the edit and
+ // delete buttons in the font collections manager dialog.
+ int state = 0;
+
+ // State -1: Selection is a system collection or a system
+ // collection font.(Neither edit nor delete)
+
+ // State 0: It's not a system collection or it's font. But it is
+ // a user collection.(Both edit and delete).
+
+ // State 1: It is a font that belongs to a user collection.
+ // (Only delete)
+
+ if(parent) {
+ // It is a font, and thus it is not editable.
+ // Now check if it's parent is a system collection.
+ bool is_system = font_collections->find_collection((*parent)[FontCollection.name], true);
+ state = (is_system) ? SYSTEM_COLLECTION: USER_COLLECTION_FONT;
+ } else {
+ // Check if it is a system collection.
+ bool is_system = font_collections->find_collection((*iter)[FontCollection.name], true);
+ state = (is_system) ? SYSTEM_COLLECTION: USER_COLLECTION;
+ }
+
+ signal_changed.emit(state);
+ }
+}
+
+} // 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-collection-selector.h b/src/ui/widget/font-collection-selector.h
new file mode 100644
index 0000000..cf21ec4
--- /dev/null
+++ b/src/ui/widget/font-collection-selector.h
@@ -0,0 +1,139 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * This file contains the definition of the FontCollectionSelector class. This widget
+ * defines a treeview to provide the interface to create, read, update and delete font
+ * collections and their respective fonts. This class contains all the code related to
+ * population of collections and their fonts in the TreeStore.
+ *
+ * Author:
+ * Vaibhav Malik <vaibhavmalik2018@gmail.com>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ *
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_FONT_COLLECTION_SELECTOR_H
+#define INKSCAPE_UI_WIDGET_FONT_COLLECTION_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/tools/tool-base.h"
+#include "ui/widget/iconrenderer.h"
+#include "ui/widget/scrollprotected.h"
+#include "util/font-collections.h"
+#include "util/document-fonts.h"
+#include "util/recently-used-fonts.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A container of widgets for selecting font faces.
+ */
+class FontCollectionSelector : public Gtk::Grid
+{
+public:
+
+ enum {TEXT_COLUMN, ICON_COLUMN, N_COLUMNS};
+ enum SelectionStates {SYSTEM_COLLECTION = -1, USER_COLLECTION, USER_COLLECTION_FONT};
+
+ FontCollectionSelector();
+
+ // Basic setup.
+ void setup_tree_view(Gtk::TreeView*);
+ void change_frame_name(const Glib::ustring&);
+ void setup_signals();
+
+ Glib::ustring get_text_cell_markup(Gtk::TreeIter const &iter);
+
+ // Custom renderers.
+ void text_cell_data_func(Gtk::CellRenderer*, Gtk::TreeIter const&);
+ void icon_cell_data_func(Gtk::CellRenderer*, Gtk::TreeIter const&);
+ void check_button_cell_data_func(Gtk::CellRenderer*, Gtk::TreeIter const&);
+ bool row_separator_func(const Glib::RefPtr<Gtk::TreeModel>&, const Gtk::TreeModel::iterator&);
+
+ void populate_collections();
+
+ void populate_system_collections();
+ void populate_document_fonts();
+ void populate_recently_used_fonts();
+
+ void populate_user_collections();
+ void populate_fonts(const Glib::ustring&);
+
+ // Signal handlers
+ void on_delete_icon_clicked(Glib::ustring const&);
+ void on_create_collection();
+ void on_rename_collection(const Glib::ustring&, const Glib::ustring&);
+ void on_delete_button_pressed();
+ void on_edit_button_pressed();
+
+ int deleltion_warning_message_dialog(const Glib::ustring&);
+ bool on_key_pressed(GdkEventKey*);
+
+ bool on_drag_motion(const Glib::RefPtr<Gdk::DragContext> &, int, int, guint) override;
+ void on_drag_data_received(const Glib::RefPtr<Gdk::DragContext>, int, int, const Gtk::SelectionData&, guint, guint);
+ bool on_drag_drop(const Glib::RefPtr<Gdk::DragContext> &, int, int, guint) override;
+ // bool on_drag_failed(const Glib::RefPtr<Gdk::DragContext> &, const Gtk::DragResult);
+ void on_drag_leave(const Glib::RefPtr<Gdk::DragContext> &, guint) override;
+ // void on_drag_start(const Glib::RefPtr<Gdk::DragContext> &);
+ void on_drag_end(const Glib::RefPtr<Gdk::DragContext> &) override;
+ void on_selection_changed();
+
+ sigc::connection connect_signal_changed(sigc::slot <void (int)> slot) {
+ return signal_changed.connect(slot);
+ }
+
+protected:
+
+ class FontCollectionClass : public Gtk::TreeModelColumnRecord
+ {
+ public:
+ Gtk::TreeModelColumn<Glib::ustring> name;
+ Gtk::TreeModelColumn<bool> is_editable;
+
+ FontCollectionClass()
+ {
+ add(name);
+ add(is_editable);
+ }
+ };
+ FontCollectionClass FontCollection;
+
+ Gtk::TreeView *treeview;
+ Gtk::Frame frame;
+ Gtk::ScrolledWindow scroll;
+ Gtk::TreeViewColumn text_column;
+ Gtk::TreeViewColumn del_icon_column;
+ Gtk::CellRendererText *cell_text;
+ Inkscape::UI::Widget::IconRenderer *del_icon_renderer;
+
+ Glib::RefPtr<Gtk::TreeStore> store;
+
+ // What type of object can be dropped.
+ std::vector<Gtk::TargetEntry> target_entries;
+ sigc::signal <void (int)> signal_changed;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_FONT_COLLECTION_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 :
diff --git a/src/ui/widget/font-selector-toolbar.cpp b/src/ui/widget/font-selector-toolbar.cpp
new file mode 100644
index 0000000..830a54b
--- /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. The setToolboxFocusTo method now just searches for a named widget.
+ * We just need to do the following:
+ * * Set the name of the family_combo child widget
+ * * Change the setToolboxFocusTo() argument in tools/text-tool to point to that widget name
+ */
+
+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
+
+ 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 ([=](){ on_family_changed(); });
+ style_combo.signal_changed().connect ([=](){ 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([=](){ 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().raw() << 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().raw() << 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::cerr << "FontSelectorToolbar::on_entry_icon_pressed" << std::endl;
+ std::cerr << " .... 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..510f306
--- /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..ea7d53b
--- /dev/null
+++ b/src/ui/widget/font-selector.cpp
@@ -0,0 +1,562 @@
+// 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"
+#include "libnrtype/font-factory.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();
+ Glib::RefPtr<Gtk::TreeModel> model = font_lister->get_font_list();
+ // Font family
+ family_treecolumn.pack_start (family_cell, false);
+ int total = model->children().size();
+ int height = 30;
+ if (total > 1000) {
+ height = 30000/total;
+ g_warning("You have a huge number of font families (%d), "
+ "and Cairo is limiting the size of widgets you can draw.\n"
+ "Your preview cell height is capped to %d.",
+ total, height);
+ // hope we dont need a forced height because now pango line height
+ // not add data outside parent rendered expanding it so no naturall cells become over 30 height
+ family_cell.set_fixed_size(-1, height);
+ } else {
+#if !PANGO_VERSION_CHECK(1,50,0)
+ family_cell.set_fixed_size(-1, height);
+#endif
+ }
+ family_treecolumn.set_fixed_width (120); // limit minimal width to keep entire dialog narrow; column can still grow
+ family_treecolumn.add_attribute (family_cell, "text", 0);
+ // family_treecolumn.set_cell_data_func (family_cell, &font_lister_cell_data_func);
+ family_treecolumn.set_cell_data_func (family_cell, &font_lister_cell_data_func_markup);
+ family_treeview.set_row_separator_func (&font_lister_separator_func);
+ family_treeview.set_model(model);
+ 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");
+ if (auto entry = size_combobox.get_entry()) {
+ // limit min size of the entry box to 6 chars, so it doesn't inflate entire dialog!
+ entry->set_width_chars(6);
+ }
+ 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);
+ }
+
+ // For drag and drop.
+ // Target entries for Drag and Drop.
+ // target_entries.emplace_back(Gtk::TargetEntry("text/uri-list", (Gtk::TargetFlags)0, 0));
+ target_entries.emplace_back(Gtk::TargetEntry("STRING", (Gtk::TargetFlags)0, 0));
+ target_entries.emplace_back(Gtk::TargetEntry("text/plain", (Gtk::TargetFlags)0, 0));
+
+ family_treeview.drag_source_set(target_entries, Gdk::BUTTON1_MASK, Gdk::ACTION_COPY | Gdk::ACTION_DEFAULT);
+ family_treeview.signal_drag_data_get().connect(sigc::mem_fun(*this, &FontSelector::on_drag_data_get));
+ family_treeview.signal_drag_begin().connect(sigc::mem_fun(*this, &FontSelector::on_drag_start), false);
+
+ // 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));
+ family_treeview.signal_realize().connect(sigc::mem_fun(*this, &FontSelector::on_realize_list));
+ show_all_children();
+ font_variations_scroll.set_vexpand(false);
+
+ // 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::on_realize_list() {
+ family_treecolumn.set_cell_data_func (family_cell, &font_lister_cell_data_func);
+ g_idle_add(FontSelector::set_cell_markup, this);
+}
+
+gboolean FontSelector::set_cell_markup(gpointer data)
+{
+ FontSelector *self = static_cast<FontSelector *>(data);
+ self->family_treeview.hide();
+ self->family_treecolumn.set_cell_data_func (self->family_cell, &font_lister_cell_data_func_markup);
+ self->family_treeview.show();
+ return false;
+}
+
+void FontSelector::hide_others()
+{
+ style_frame.set_no_show_all();
+ style_frame.hide();
+ size_label.set_no_show_all();
+ size_label.hide();
+ size_combobox.set_no_show_all();
+ size_combobox.hide();
+ font_variations.set_no_show_all();
+ font_variations_scroll.hide();
+ font_variations_scroll.set_vexpand(false);
+}
+
+// TODO:
+void FontSelector::on_drag_start(const Glib::RefPtr<Gdk::DragContext> &context)
+{
+ // Get the current collection.
+ Glib::RefPtr<Gtk::TreeSelection> selection = family_treeview.get_selection();
+ Gtk::TreeModel::iterator iter = selection->get_selected();
+ Gtk::TreePath path(iter);
+ auto surface = family_treeview.create_row_drag_icon(path);
+
+ context->set_icon(surface);
+ /*
+ Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+ Glib::ustring family_name = font_lister->get_treeview_drag_selection();
+ // std::cout << "FontSelector::on_drag_start()" << std::endl;
+ auto drag_label = Gtk::manage(new Gtk::Label(family_name));
+
+ gtk_drag_set_icon_widget(context, drag_label, 0, 0);
+ */
+}
+
+void FontSelector::on_drag_data_get(Glib::RefPtr<Gdk::DragContext> const &context, Gtk::SelectionData &selection_data, guint info, guint time)
+{
+ // std::cout << "FontSelector::on_drag_data_get()" << std::endl;
+ Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+
+ // std::cout << font_lister->get_font_family_row() << ", " << font_lister->get_treeview_selection() << std::endl;
+
+ Glib::ustring family_name = font_lister->get_dragging_family();
+ // std::cout << "Family: " << family_name << std::endl;
+
+ selection_data.set_text(family_name);
+}
+
+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.raw() << 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;
+}
+
+void FontSelector::unset_model()
+{
+ family_treeview.unset_model();
+}
+
+void FontSelector::set_model()
+{
+ Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+ Glib::RefPtr<Gtk::TreeModel> model = font_lister->get_font_list();
+ family_treeview.set_model(model);
+}
+
+// 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);
+
+ fontlister->set_dragging_family(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.raw() << 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());
+ if (initial) {
+ initial = false;
+ family_treecolumn.unset_cell_data_func (family_cell);
+ family_treecolumn.set_cell_data_func (family_cell, &font_lister_cell_data_func);
+ g_idle_add(FontSelector::set_cell_markup, this);
+ }
+ 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..ca3cfea
--- /dev/null
+++ b/src/ui/widget/font-selector.h
@@ -0,0 +1,177 @@
+// 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"
+#include "ui/widget/scrollprotected.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);
+ void hide_others();
+
+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;
+ ScrollProtected<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;
+
+ bool initial = true;
+
+ // control font variations update and UI element size
+ void update_variations(const Glib::ustring& fontspec);
+
+ // What type of object can be dropped.
+ std::vector<Gtk::TargetEntry> target_entries;
+ static gboolean set_cell_markup(gpointer);
+ void on_realize_list();
+ // For drag and drop.
+ void on_drag_start(const Glib::RefPtr<Gdk::DragContext> &context);
+ void on_drag_data_get(Glib::RefPtr<Gdk::DragContext> const &context, Gtk::SelectionData &selection_data, guint info, guint time) override;
+
+public:
+
+ /**
+ * Update GUI based on fontspec
+ */
+ void update_font ();
+ void update_size (double size);
+ void unset_model();
+ void set_model();
+
+ /**
+ * 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..504eda5
--- /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 "libnrtype/font-factory.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(Glib::ustring const &name, OTSubstitution const &glyphs, int options, Glib::ustring family, Gtk::Grid& grid, int &row, FontVariants* parent)
+ : _name (name)
+ {
+ 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;
+ std::vector <Gtk::RadioButton*> buttons;
+ };
+
+ FontVariants::FontVariants () :
+ Gtk::Box (Gtk::ORIENTATION_VERTICAL),
+ _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 ),
+ _feature_vbox(Gtk::ORIENTATION_VERTICAL)
+
+ {
+
+ 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.
+ Gtk::Label* labels[] = {
+ &_ligatures_label_common,
+ &_ligatures_label_discretionary,
+ &_ligatures_label_historical,
+ &_ligatures_label_contextual
+ };
+ for (auto label : labels) {
+ // char limit - not really needed, since number of lines is restricted
+ label->set_max_width_chars(999);
+ // show ellipsis when text overflows
+ label->set_ellipsize(Pango::ELLIPSIZE_END);
+ // up to 5 lines
+ label->set_lines(5);
+ // multiline
+ label->set_line_wrap();
+ // break it as needed
+ label->set_line_wrap_mode(Pango::WRAP_WORD_CHAR);
+ }
+
+ // 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.
+ auto res = FontFactory::get().FaceFromFontSpecification(font_spec.c_str());
+ if (res) {
+
+ auto const &tab = res->get_opentype_tables();
+ std::remove_reference<decltype(tab)>::type::const_iterator it;
+
+ if((it = tab.find("liga"))!= tab.end() ||
+ (it = tab.find("clig"))!= tab.end()) {
+ _ligatures_common.set_sensitive();
+ } else {
+ _ligatures_common.set_sensitive( false );
+ }
+
+ if((it = tab.find("dlig"))!= tab.end()) {
+ _ligatures_discretionary.set_sensitive();
+ } else {
+ _ligatures_discretionary.set_sensitive( false );
+ }
+
+ if((it = tab.find("hlig"))!= tab.end()) {
+ _ligatures_historical.set_sensitive();
+ } else {
+ _ligatures_historical.set_sensitive( false );
+ }
+
+ if((it = tab.find("calt"))!= tab.end()) {
+ _ligatures_contextual.set_sensitive();
+ } else {
+ _ligatures_contextual.set_sensitive( false );
+ }
+
+ if((it = tab.find("subs"))!= tab.end()) {
+ _position_sub.set_sensitive();
+ } else {
+ _position_sub.set_sensitive( false );
+ }
+
+ if((it = tab.find("sups"))!= tab.end()) {
+ _position_super.set_sensitive();
+ } else {
+ _position_super.set_sensitive( false );
+ }
+
+ if((it = tab.find("smcp"))!= tab.end()) {
+ _caps_small.set_sensitive();
+ } else {
+ _caps_small.set_sensitive( false );
+ }
+
+ if((it = tab.find("c2sc"))!= tab.end() &&
+ (it = tab.find("smcp"))!= tab.end()) {
+ _caps_all_small.set_sensitive();
+ } else {
+ _caps_all_small.set_sensitive( false );
+ }
+
+ if((it = tab.find("pcap"))!= tab.end()) {
+ _caps_petite.set_sensitive();
+ } else {
+ _caps_petite.set_sensitive( false );
+ }
+
+ if((it = tab.find("c2sc"))!= tab.end() &&
+ (it = tab.find("pcap"))!= tab.end()) {
+ _caps_all_petite.set_sensitive();
+ } else {
+ _caps_all_petite.set_sensitive( false );
+ }
+
+ if((it = tab.find("unic"))!= tab.end()) {
+ _caps_unicase.set_sensitive();
+ } else {
+ _caps_unicase.set_sensitive( false );
+ }
+
+ if((it = tab.find("titl"))!= tab.end()) {
+ _caps_titling.set_sensitive();
+ } else {
+ _caps_titling.set_sensitive( false );
+ }
+
+ if((it = tab.find("lnum"))!= tab.end()) {
+ _numeric_lining.set_sensitive();
+ } else {
+ _numeric_lining.set_sensitive( false );
+ }
+
+ if((it = tab.find("onum"))!= tab.end()) {
+ _numeric_old_style.set_sensitive();
+ } else {
+ _numeric_old_style.set_sensitive( false );
+ }
+
+ if((it = tab.find("pnum"))!= tab.end()) {
+ _numeric_proportional.set_sensitive();
+ } else {
+ _numeric_proportional.set_sensitive( false );
+ }
+
+ if((it = tab.find("tnum"))!= tab.end()) {
+ _numeric_tabular.set_sensitive();
+ } else {
+ _numeric_tabular.set_sensitive( false );
+ }
+
+ if((it = tab.find("frac"))!= tab.end()) {
+ _numeric_diagonal.set_sensitive();
+ } else {
+ _numeric_diagonal.set_sensitive( false );
+ }
+
+ if((it = tab.find("afrac"))!= tab.end()) {
+ _numeric_stacked.set_sensitive();
+ } else {
+ _numeric_stacked.set_sensitive( false );
+ }
+
+ if((it = tab.find("ordn"))!= tab.end()) {
+ _numeric_ordinal.set_sensitive();
+ } else {
+ _numeric_ordinal.set_sensitive( false );
+ }
+
+ if((it = tab.find("zero"))!= tab.end()) {
+ _numeric_slashed_zero.set_sensitive();
+ } else {
+ _numeric_slashed_zero.set_sensitive( false );
+ }
+
+ // East-Asian
+ if((it = tab.find("jp78"))!= tab.end()) {
+ _asian_jis78.set_sensitive();
+ } else {
+ _asian_jis78.set_sensitive( false );
+ }
+
+ if((it = tab.find("jp83"))!= tab.end()) {
+ _asian_jis83.set_sensitive();
+ } else {
+ _asian_jis83.set_sensitive( false );
+ }
+
+ if((it = tab.find("jp90"))!= tab.end()) {
+ _asian_jis90.set_sensitive();
+ } else {
+ _asian_jis90.set_sensitive( false );
+ }
+
+ if((it = tab.find("jp04"))!= tab.end()) {
+ _asian_jis04.set_sensitive();
+ } else {
+ _asian_jis04.set_sensitive( false );
+ }
+
+ if((it = tab.find("smpl"))!= tab.end()) {
+ _asian_simplified.set_sensitive();
+ } else {
+ _asian_simplified.set_sensitive( false );
+ }
+
+ if((it = tab.find("trad"))!= tab.end()) {
+ _asian_traditional.set_sensitive();
+ } else {
+ _asian_traditional.set_sensitive( false );
+ }
+
+ if((it = tab.find("fwid"))!= tab.end()) {
+ _asian_full_width.set_sensitive();
+ } else {
+ _asian_full_width.set_sensitive( false );
+ }
+
+ if((it = tab.find("pwid"))!= tab.end()) {
+ _asian_proportional_width.set_sensitive();
+ } else {
+ _asian_proportional_width.set_sensitive( false );
+ }
+
+ if((it = tab.find("ruby"))!= tab.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 : tab) {
+
+ 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->get_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->get_opentype_tables()) {
+
+ Glib::ustring markup;
+ markup += "<span font_family='";
+ markup += sp_font_description_get_family(res->get_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.
+ auto table_copy = res->get_opentype_tables();
+ 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->get_opentype_tables()) {
+ 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->get_descr()),
+ _feature_grid, grid_row, this);
+ grid_row++;
+ }
+ }
+
+ // GSUB lookup type 3 (1 to many mapping). Optionally type 1.
+ for (auto &table : res->get_opentype_tables()) {
+ 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->get_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 FontInstance for: "
+ << font_spec.raw() << 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 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 lining = _numeric_lining.get_active();
+ bool old_style = _numeric_old_style.get_active();
+
+ bool proportional = _numeric_proportional.get_active();
+ bool tabular = _numeric_tabular.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 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..8af2e23
--- /dev/null
+++ b/src/ui/widget/font-variants.h
@@ -0,0 +1,223 @@
+// 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>
+#include <gtkmm/hvbox.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::Box
+{
+
+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::Box _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..d0e7e9b
--- /dev/null
+++ b/src/ui/widget/font-variations.cpp
@@ -0,0 +1,182 @@
+// 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 "libnrtype/font-factory.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 const &axis)
+ : name(std::move(name_))
+{
+
+ // std::cout << "FontVariationAxis::FontVariationAxis:: "
+ // << " name: " << name
+ // << " min: " << axis.minimum
+ // << " def: " << axis.def
+ // << " max: " << axis.maximum
+ // << " val: " << axis.set_val << std::endl;
+
+ label = Gtk::make_managed<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 );
+
+ def = axis.def; // Default value
+}
+
+
+// ------------------------------------------------------------- //
+
+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(Glib::ustring const &font_spec)
+{
+ auto res = FontFactory::get().FaceFromFontSpecification(font_spec.c_str());
+
+ auto children = get_children();
+ for (auto child : children) {
+ remove(*child);
+ }
+ axes.clear();
+
+ for (auto &a : res->get_opentype_varaxes()) {
+ // std::cout << "Creating axis: " << a.first << std::endl;
+ auto axis = Gtk::make_managed<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 == "OpticalSize") 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() == axis->get_def()) continue;
+ 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 == "OpticalSize") 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 ',' or '@'
+ }
+
+ 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..dbc5eb8
--- /dev/null
+++ b/src/ui/widget/font-variations.h
@@ -0,0 +1,128 @@
+// 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 const &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; }
+ double get_def() { return def; }
+
+private:
+
+ // Widgets
+ Glib::ustring name;
+ Gtk::Label* label;
+ Gtk::Scale* scale;
+
+ int precision;
+ double def = 0.0; // Default value
+
+ // 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/gradient-editor.cpp b/src/ui/widget/gradient-editor.cpp
new file mode 100644
index 0000000..2edb6c5
--- /dev/null
+++ b/src/ui/widget/gradient-editor.cpp
@@ -0,0 +1,653 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * Gradient editor widget for "Fill and Stroke" dialog
+ *
+ * Author:
+ * Michael Kowalski
+ *
+ * Copyright (C) 2020-2021 Michael Kowalski
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "gradient-editor.h"
+
+#include <gtkmm/builder.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/togglebutton.h>
+#include <gtkmm/button.h>
+#include <gtkmm/menubutton.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/treemodelcolumn.h>
+#include <glibmm/i18n.h>
+#include <cairo.h>
+
+#include "document-undo.h"
+#include "gradient-chemistry.h"
+#include "gradient-selector.h"
+#include "preferences.h"
+
+#include "display/cairo-utils.h"
+
+#include "io/resource.h"
+
+#include "object/sp-gradient-vector.h"
+#include "object/sp-linear-gradient.h"
+
+#include "svg/css-ostringstream.h"
+
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "ui/widget/color-notebook.h"
+#include "ui/widget/color-preview.h"
+
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+using namespace Inkscape::IO;
+using Inkscape::UI::Widget::ColorNotebook;
+
+class scope {
+public:
+ scope(bool& flag): _flag(flag) {
+ flag = true;
+ }
+
+ ~scope() {
+ _flag = false;
+ }
+
+private:
+ bool& _flag;
+};
+
+void set_icon(Gtk::Button& btn, gchar const* pixmap) {
+ if (Gtk::Image* img = sp_get_icon_image(pixmap, Gtk::ICON_SIZE_BUTTON)) {
+ btn.set_image(*img);
+ }
+}
+
+// draw solid color circle with black outline; right side is to show checkerboard if color's alpha is > 0
+Glib::RefPtr<Gdk::Pixbuf> draw_circle(int size, guint32 rgba) {
+ int width = size;
+ int height = size;
+ gint w2 = width / 2;
+
+ cairo_surface_t* s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
+ cairo_t* cr = cairo_create(s);
+
+ int x = 0, y = 0;
+ double radius = size / 2;
+ double degrees = M_PI / 180.0;
+ cairo_new_sub_path(cr);
+ cairo_arc(cr, x + radius, y + radius, radius, 0, 2 * M_PI);
+ cairo_close_path(cr);
+ // semi-transparent black outline
+ cairo_set_source_rgba(cr, 0, 0, 0, 0.2);
+ cairo_fill(cr);
+
+ radius--;
+
+ cairo_new_sub_path(cr);
+ cairo_line_to(cr, x + w2, 0);
+ cairo_line_to(cr, x + w2, height);
+ cairo_arc(cr, x + w2, y + w2, radius, 90 * degrees, 270 * degrees);
+ cairo_close_path(cr);
+
+ // solid part
+ ink_cairo_set_source_rgba32(cr, rgba | 0xff);
+ cairo_fill(cr);
+
+ x = w2;
+
+ cairo_new_sub_path(cr);
+ cairo_arc(cr, x, y + w2, radius, -90 * degrees, 90 * degrees);
+ cairo_line_to(cr, x, y);
+ cairo_close_path(cr);
+
+ // (semi)transparent part
+ if ((rgba & 0xff) != 0xff) {
+ cairo_pattern_t* checkers = ink_cairo_pattern_create_checkerboard();
+ cairo_set_source(cr, checkers);
+ cairo_fill_preserve(cr);
+ cairo_pattern_destroy(checkers);
+ }
+ ink_cairo_set_source_rgba32(cr, rgba);
+ cairo_fill(cr);
+
+ cairo_destroy(cr);
+ cairo_surface_flush(s);
+
+ GdkPixbuf* pixbuf = ink_pixbuf_create_from_cairo_surface(s);
+ return Glib::wrap(pixbuf);
+}
+
+
+Glib::RefPtr<Gdk::Pixbuf> get_stop_pixmap(SPStop* stop) {
+ const int size = 30;
+ return draw_circle(size, stop->getColor().toRGBA32(stop->getOpacity()));
+}
+
+// get widget from builder or throw
+template<class W> W& get_widget(Glib::RefPtr<Gtk::Builder>& builder, const char* id) {
+ W* widget;
+ builder->get_widget(id, widget);
+ if (!widget) {
+ throw std::runtime_error("Missing widget in a glade resource file");
+ }
+ return *widget;
+}
+
+Glib::RefPtr<Gtk::Builder> create_builder() {
+ auto glade = Resource::get_filename(Resource::UIS, "gradient-edit.glade");
+ Glib::RefPtr<Gtk::Builder> builder;
+ try {
+ return Gtk::Builder::create_from_file(glade);
+ }
+ catch (Glib::Error& ex) {
+ g_error("Cannot load glade file for gradient editor: %s", + ex.what().c_str());
+ throw;
+ }
+}
+
+Glib::ustring get_repeat_icon(SPGradientSpread mode) {
+ const char* ico = "";
+ switch (mode) {
+ case SP_GRADIENT_SPREAD_PAD:
+ ico = "gradient-spread-pad";
+ break;
+ case SP_GRADIENT_SPREAD_REPEAT:
+ ico = "gradient-spread-repeat";
+ break;
+ case SP_GRADIENT_SPREAD_REFLECT:
+ ico = "gradient-spread-reflect";
+ break;
+ default:
+ g_warning("Missing case in %s\n", __func__);
+ break;
+ }
+ return ico;
+}
+
+GradientEditor::GradientEditor(const char* prefs) :
+ _builder(create_builder()),
+ _selector(Gtk::manage(new GradientSelector())),
+ _repeat_icon(get_widget<Gtk::Image>(_builder, "repeatIco")),
+ _popover(get_widget<Gtk::Popover>(_builder, "libraryPopover")),
+ _stop_tree(get_widget<Gtk::TreeView>(_builder, "stopList")),
+ _offset_btn(get_widget<Gtk::SpinButton>(_builder, "offsetSpin")),
+ _show_stops_list(get_widget<Gtk::Expander>(_builder, "stopsBtn")),
+ _add_stop(get_widget<Gtk::Button>(_builder, "stopAdd")),
+ _delete_stop(get_widget<Gtk::Button>(_builder, "stopDelete")),
+ _stops_gallery(get_widget<Gtk::Box>(_builder, "stopsGallery")),
+ _colors_box(get_widget<Gtk::Box>(_builder, "colorsBox")),
+ _linear_btn(get_widget<Gtk::ToggleButton>(_builder, "linearBtn")),
+ _radial_btn(get_widget<Gtk::ToggleButton>(_builder, "radialBtn")),
+ _main_grid(get_widget<Gtk::Grid>(_builder, "mainGrid")),
+ _prefs(prefs)
+{
+ // gradient type buttons; not currently used, hidden, WIP
+ set_icon(_linear_btn, INKSCAPE_ICON("paint-gradient-linear"));
+ set_icon(_radial_btn, INKSCAPE_ICON("paint-gradient-radial"));
+
+ auto& reverse = get_widget<Gtk::Button>(_builder, "reverseBtn");
+ set_icon(reverse, INKSCAPE_ICON("object-flip-horizontal"));
+ reverse.signal_clicked().connect([=](){ reverse_gradient(); });
+
+ auto& gradBox = get_widget<Gtk::Box>(_builder, "gradientBox");
+ const int dot_size = 8;
+ _gradient_image.show();
+ _gradient_image.set_margin_start(dot_size / 2);
+ _gradient_image.set_margin_end(dot_size / 2);
+ // gradient stop selected in a gradient widget; sync list selection
+ _gradient_image.signal_stop_selected().connect([=](size_t index) {
+ select_stop(index);
+ fire_stop_selected(get_current_stop());
+ });
+ _gradient_image.signal_stop_offset_changed().connect([=](size_t index, double offset) {
+ set_stop_offset(index, offset);
+ });
+ _gradient_image.signal_add_stop_at().connect([=](double offset) {
+ insert_stop_at(offset);
+ });
+ _gradient_image.signal_delete_stop().connect([=](size_t index) {
+ delete_stop(index);
+ });
+
+ gradBox.pack_start(_gradient_image, true, true, 0);
+
+ // add color selector
+ auto color_selector = Gtk::manage(new ColorNotebook(_selected_color));
+ color_selector->set_label(_("Stop color"));
+ color_selector->show();
+ _colors_box.pack_start(*color_selector, true, true, 0);
+
+ // gradient library in a popup
+ _popover.add(*_selector);
+ const int h = 5;
+ const int v = 3;
+ _selector->set_margin_start(h);
+ _selector->set_margin_end(h);
+ _selector->set_margin_top(v);
+ _selector->set_margin_bottom(v);
+ _selector->show();
+ _selector->show_edit_button(false);
+ _selector->set_gradient_size(160, 20);
+ _selector->set_name_col_size(120);
+ // gradient changed is currently the only signal that GradientSelector can emit:
+ _selector->signal_changed().connect([=](SPGradient* gradient) {
+ // new gradient selected from the library
+ _signal_changed.emit(gradient);
+ });
+
+ // construct store for a list of stops
+ _stop_columns.add(_stopObj);
+ _stop_columns.add(_stopIdx);
+ _stop_columns.add(_stopID);
+ _stop_columns.add(_stop_color);
+ _stop_list_store = Gtk::ListStore::create(_stop_columns);
+ _stop_tree.set_model(_stop_list_store);
+ // indices in the stop list view; currently hidden
+ // _stop_tree.append_column("n", _stopID); // 1-based stop index
+ _stop_tree.append_column("c", _stop_color); // and its color
+
+ auto selection = _stop_tree.get_selection();
+ selection->signal_changed().connect([=]() {
+ if (!_update.pending()) {
+ stop_selected();
+ fire_stop_selected(get_current_stop());
+ }
+ });
+
+ _show_stops_list.property_expanded().signal_changed().connect(
+ [&](){ show_stops(_show_stops_list.get_expanded()); }
+ );
+
+ set_icon(_add_stop, "list-add");
+ _add_stop.signal_clicked().connect([=](){
+ if (auto row = current_stop()) {
+ auto index = row->get_value(_stopIdx);
+ add_stop(static_cast<int>(index));
+ }
+ });
+
+ set_icon(_delete_stop, "list-remove");
+ _delete_stop.signal_clicked().connect([=]() {
+ if (auto row = current_stop()) {
+ auto index = row->get_value(_stopIdx);
+ delete_stop(static_cast<int>(index));
+ }
+ });
+
+ // connect gradient repeat modes menu
+ std::tuple<const char*, SPGradientSpread> repeats[3] = {
+ {"repeatNone", SP_GRADIENT_SPREAD_PAD},
+ {"repeatDirect", SP_GRADIENT_SPREAD_REPEAT},
+ {"repeatReflected", SP_GRADIENT_SPREAD_REFLECT}
+ };
+ for (auto& el : repeats) {
+ auto& item = get_widget<Gtk::MenuItem>(_builder, std::get<0>(el));
+ auto mode = std::get<1>(el);
+ item.signal_activate().connect([=](){ set_repeat_mode(mode); });
+ // pack icon and text into MenuItem, since MenuImageItem is deprecated
+ auto text = item.get_label();
+ auto hbox = Gtk::manage(new Gtk::Box);
+ Gtk::Image* img = sp_get_icon_image(get_repeat_icon(mode), Gtk::ICON_SIZE_BUTTON);
+ hbox->pack_start(*img, false, true, 8);
+ auto label = Gtk::manage(new Gtk::Label);
+ label->set_label(text);
+ hbox->pack_start(*label, false, true, 8);
+ hbox->show_all();
+ item.remove();
+ item.add(*hbox);
+ }
+
+ set_repeat_icon(SP_GRADIENT_SPREAD_PAD);
+
+ _selected_color.signal_changed.connect([=]() {
+ set_stop_color(_selected_color.color(), _selected_color.alpha());
+ });
+ _selected_color.signal_dragged.connect([=]() {
+ set_stop_color(_selected_color.color(), _selected_color.alpha());
+ });
+
+ _offset_btn.signal_changed().connect([=]() {
+ if (auto row = current_stop()) {
+ auto index = row->get_value(_stopIdx);
+ double offset = _offset_btn.get_value();
+ set_stop_offset(index, offset);
+ }
+ });
+
+ pack_start(_main_grid);
+
+ // restore visibility of the stop list view
+ _stops_list_visible = Inkscape::Preferences::get()->getBool(_prefs + "/stoplist", true);
+ _show_stops_list.set_expanded(_stops_list_visible);
+ update_stops_layout();
+}
+
+GradientEditor::~GradientEditor() noexcept {
+}
+
+void GradientEditor::set_stop_color(SPColor color, float opacity) {
+ if (_update.pending()) return;
+
+ SPGradient* vector = get_gradient_vector();
+ if (!vector) return;
+
+ if (auto row = current_stop()) {
+ auto index = row->get_value(_stopIdx);
+ SPStop* stop = sp_get_nth_stop(vector, index);
+ if (stop && _document) {
+ auto scoped(_update.block());
+
+ // update list view too
+ row->set_value(_stop_color, get_stop_pixmap(stop));
+
+ sp_set_gradient_stop_color(_document, stop, color, opacity);
+ }
+ }
+}
+
+std::optional<Gtk::TreeRow> GradientEditor::current_stop() {
+ auto sel = _stop_tree.get_selection();
+ auto it = sel->get_selected();
+ if (!it) {
+ return std::nullopt;
+ }
+ else {
+ return *it;
+ }
+}
+
+SPStop* GradientEditor::get_nth_stop(size_t index) {
+ if (SPGradient* vector = get_gradient_vector()) {
+ return sp_get_nth_stop(vector, index);
+ }
+ return nullptr;
+}
+
+// stop has been selected in a list view
+void GradientEditor::stop_selected() {
+ if (auto row = current_stop()) {
+ SPStop* stop = row->get_value(_stopObj);
+ if (stop) {
+ auto scoped(_update.block());
+
+ _selected_color.setColor(stop->getColor());
+ _selected_color.setAlpha(stop->getOpacity());
+
+ auto stops = sp_get_before_after_stops(stop);
+ if (stops.first && stops.second) {
+ _offset_btn.set_range(stops.first->offset, stops.second->offset);
+ }
+ else {
+ _offset_btn.set_range(stops.first ? stops.first->offset : 0, stops.second ? stops.second->offset : 1);
+ }
+ _offset_btn.set_sensitive();
+ _offset_btn.set_value(stop->offset);
+
+ int index = row->get_value(_stopIdx);
+ _gradient_image.set_focused_stop(index);
+ }
+ }
+ else {
+ // no selection
+ auto scoped(_update.block());
+
+ _selected_color.setColor(SPColor());
+
+ _offset_btn.set_range(0, 0);
+ _offset_btn.set_value(0);
+ _offset_btn.set_sensitive(false);
+ }
+}
+
+void GradientEditor::insert_stop_at(double offset) {
+ if (SPGradient* vector = get_gradient_vector()) {
+ // only insert new stop if there are some stops present
+ if (vector->hasStops()) {
+ SPStop* stop = sp_gradient_add_stop_at(vector, offset);
+ // just select next stop; newly added stop will be in a list view after selection refresh (on idle)
+ auto pos = sp_number_of_stops_before_stop(vector, stop);
+ auto selected = select_stop(pos);
+ fire_stop_selected(stop);
+ if (!selected) {
+ select_stop(pos);
+ }
+ }
+ }
+}
+
+void GradientEditor::add_stop(int index) {
+ if (SPGradient* vector = get_gradient_vector()) {
+ if (SPStop* current = sp_get_nth_stop(vector, index)) {
+ SPStop* stop = sp_gradient_add_stop(vector, current);
+ // just select next stop; newly added stop will be in a list view after selection refresh (on idle)
+ select_stop(sp_number_of_stops_before_stop(vector, stop));
+ fire_stop_selected(stop);
+ }
+ }
+}
+
+void GradientEditor::delete_stop(int index) {
+ if (SPGradient* vector = get_gradient_vector()) {
+ if (SPStop* stop = sp_get_nth_stop(vector, index)) {
+ // try deleting a stop, if it can be
+ sp_gradient_delete_stop(vector, stop);
+ }
+ }
+}
+
+// collapse/expand list of stops in the UI
+void GradientEditor::show_stops(bool visible) {
+ _stops_list_visible = visible;
+ update_stops_layout();
+ Inkscape::Preferences::get()->setBool(_prefs + "/stoplist", _stops_list_visible);
+}
+
+void GradientEditor::update_stops_layout() {
+ if (_stops_list_visible) {
+ _stops_gallery.show();
+ }
+ else {
+ _stops_gallery.hide();
+ }
+}
+
+void GradientEditor::reverse_gradient() {
+ if (_document && _gradient) {
+ // reverse works on a gradient definition, the one with stops:
+ SPGradient* vector = get_gradient_vector();
+
+ if (vector) {
+ sp_gradient_reverse_vector(vector);
+ DocumentUndo::done(_document, _("Reverse gradient"), INKSCAPE_ICON("color-gradient"));
+ }
+ }
+}
+
+void GradientEditor::set_repeat_mode(SPGradientSpread mode) {
+ if (_update.pending()) return;
+
+ if (_document && _gradient) {
+ auto scoped(_update.block());
+
+ // spread is set on a gradient reference, which is _gradient object
+ _gradient->setSpread(mode);
+ _gradient->updateRepr();
+
+ DocumentUndo::done(_document, _("Set gradient repeat"), INKSCAPE_ICON("color-gradient"));
+
+ set_repeat_icon(mode);
+ }
+}
+
+void GradientEditor::set_repeat_icon(SPGradientSpread mode) {
+ auto ico = get_repeat_icon(mode);
+ if (!ico.empty()) {
+ _repeat_icon.set_from_icon_name(ico, Gtk::ICON_SIZE_BUTTON);
+ }
+}
+
+void GradientEditor::setGradient(SPGradient* gradient) {
+ auto scoped(_update.block());
+ auto scoped2(_notification.block());
+ _gradient = gradient;
+ _document = gradient ? gradient->document : nullptr;
+ set_gradient(gradient);
+}
+
+SPGradient* GradientEditor::getVector() {
+ return _selector->getVector();
+}
+
+void GradientEditor::setVector(SPDocument* doc, SPGradient* vector) {
+ auto scoped(_update.block());
+ _selector->setVector(doc, vector);
+}
+
+void GradientEditor::setMode(SelectorMode mode) {
+ _selector->setMode(mode);
+}
+
+void GradientEditor::setUnits(SPGradientUnits units) {
+ _selector->setUnits(units);
+}
+
+SPGradientUnits GradientEditor::getUnits() {
+ return _selector->getUnits();
+}
+
+void GradientEditor::setSpread(SPGradientSpread spread) {
+ _selector->setSpread(spread);
+}
+
+SPGradientSpread GradientEditor::getSpread() {
+ return _selector->getSpread();
+}
+
+void GradientEditor::selectStop(SPStop* selected) {
+ if (_notification.pending()) return;
+
+ auto scoped(_notification.block());
+ // request from the outside to sync stop selection
+ const auto& items = _stop_tree.get_model()->children();
+ auto it = std::find_if(items.begin(), items.end(), [=](const auto& row) {
+ SPStop* stop = row->get_value(_stopObj);
+ return stop == selected;
+ });
+ if (it != items.end()) {
+ select_stop(std::distance(items.begin(), it));
+ }
+}
+
+SPGradient* GradientEditor::get_gradient_vector() {
+ if (!_gradient) return nullptr;
+ return sp_gradient_get_forked_vector_if_necessary(_gradient, false);
+}
+
+void GradientEditor::set_gradient(SPGradient* gradient) {
+ auto scoped(_update.block());
+
+ // remember which stop is selected, so we can restore it
+ size_t selected_stop_index = 0;
+ if (auto it = _stop_tree.get_selection()->get_selected()) {
+ selected_stop_index = it->get_value(_stopIdx);
+ }
+
+ _stop_list_store->clear();
+
+ SPGradient* vector = gradient ? gradient->getVector() : nullptr;
+
+ if (vector) {
+ vector->ensureVector();
+ }
+
+ _gradient_image.set_gradient(vector);
+
+ if (!vector || !vector->hasStops()) return;
+
+ size_t index = 0;
+ for (auto& child : vector->children) {
+ if (is<SPStop>(&child)) {
+ auto stop = cast<SPStop>(&child);
+ auto it = _stop_list_store->append();
+ it->set_value(_stopObj, stop);
+ it->set_value(_stopIdx, index);
+ it->set_value(_stopID, Glib::ustring::compose("%1.", index + 1));
+ it->set_value(_stop_color, get_stop_pixmap(stop));
+
+ ++index;
+ }
+ }
+
+ auto mode = gradient->isSpreadSet() ? gradient->getSpread() : SP_GRADIENT_SPREAD_PAD;
+ set_repeat_icon(mode);
+
+ // list not empty?
+ if (index > 0) {
+ select_stop(std::min(selected_stop_index, index - 1));
+ // update related widgets
+ stop_selected();
+ //
+ // emit_stop_selected(get_current_stop());
+ }
+}
+
+void GradientEditor::set_stop_offset(size_t index, double offset) {
+ if (_update.pending()) return;
+
+ // adjust stop's offset after user edits it in offset spin button or drags stop handle
+ SPStop* stop = get_nth_stop(index);
+ if (stop) {
+ auto scoped(_update.block());
+
+ stop->offset = offset;
+ if (auto repr = stop->getRepr()) {
+ repr->setAttributeCssDouble("offset", stop->offset);
+ }
+
+ DocumentUndo::maybeDone(stop->document, "gradient:stop:offset", _("Change gradient stop offset"), INKSCAPE_ICON("color-gradient"));
+ }
+}
+
+// select requested stop in a list view
+bool GradientEditor::select_stop(size_t index) {
+ if (!_gradient) return false;
+
+ bool selected = false;
+ const auto& items = _stop_tree.get_model()->children();
+ if (index < items.size()) {
+ auto it = items.begin();
+ std::advance(it, index);
+ auto path = _stop_tree.get_model()->get_path(it);
+ _stop_tree.get_selection()->select(it);
+ _stop_tree.scroll_to_cell(path, *_stop_tree.get_column(0));
+ selected = true;
+ }
+
+ return selected;
+}
+
+SPStop* GradientEditor::get_current_stop() {
+ if (auto row = current_stop()) {
+ SPStop* stop = row->get_value(_stopObj);
+ return stop;
+ }
+ return nullptr;
+}
+
+void GradientEditor::fire_stop_selected(SPStop* stop) {
+ if (!_notification.pending()) {
+ auto scoped(_notification.block());
+ emit_stop_selected(stop);
+ }
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
diff --git a/src/ui/widget/gradient-editor.h b/src/ui/widget/gradient-editor.h
new file mode 100644
index 0000000..792c042
--- /dev/null
+++ b/src/ui/widget/gradient-editor.h
@@ -0,0 +1,114 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_GRADIENT_EDITOR_H
+#define SEEN_GRADIENT_EDITOR_H
+
+#include <gtkmm/box.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/button.h>
+#include <gtkmm/spinbutton.h>
+#include <gtkmm/popover.h>
+#include <gtkmm/image.h>
+#include <gtkmm/expander.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/treemodelcolumn.h>
+#include <gtkmm/builder.h>
+#include <optional>
+
+#include "object/sp-gradient.h"
+#include "object/sp-stop.h"
+#include "ui/selected-color.h"
+#include "spin-scale.h"
+#include "gradient-with-stops.h"
+#include "gradient-selector-interface.h"
+#include "ui/operation-blocker.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class GradientSelector;
+
+class GradientEditor : public Gtk::Box, public GradientSelectorInterface {
+public:
+ GradientEditor(const char* prefs);
+ ~GradientEditor() noexcept override;
+
+private:
+ sigc::signal<void ()> _signal_grabbed;
+ sigc::signal<void ()> _signal_dragged;
+ sigc::signal<void ()> _signal_released;
+ sigc::signal<void (SPGradient*)> _signal_changed;
+
+public:
+ decltype(_signal_changed) signal_changed() const { return _signal_changed; }
+ decltype(_signal_grabbed) signal_grabbed() const { return _signal_grabbed; }
+ decltype(_signal_dragged) signal_dragged() const { return _signal_dragged; }
+ decltype(_signal_released) signal_released() const { return _signal_released; }
+
+ void setGradient(SPGradient* gradient) override;
+ SPGradient* getVector() override;
+ void setVector(SPDocument* doc, SPGradient* vector) override;
+ void setMode(SelectorMode mode) override;
+ void setUnits(SPGradientUnits units) override;
+ SPGradientUnits getUnits() override;
+ void setSpread(SPGradientSpread spread) override;
+ SPGradientSpread getSpread() override;
+ void selectStop(SPStop* selected) override;
+
+private:
+ void set_gradient(SPGradient* gradient);
+ void stop_selected();
+ void insert_stop_at(double offset);
+ void add_stop(int index);
+ void duplicate_stop();
+ void delete_stop(int index);
+ void show_stops(bool visible);
+ void update_stops_layout();
+ void set_repeat_mode(SPGradientSpread mode);
+ void set_repeat_icon(SPGradientSpread mode);
+ void reverse_gradient();
+ void set_stop_color(SPColor color, float opacity);
+ std::optional<Gtk::TreeRow> current_stop();
+ SPStop* get_nth_stop(size_t index);
+ SPStop* get_current_stop();
+ bool select_stop(size_t index);
+ void set_stop_offset(size_t index, double offset);
+ SPGradient* get_gradient_vector();
+ void fire_stop_selected(SPStop* stop);
+
+ Glib::RefPtr<Gtk::Builder> _builder;
+ GradientSelector* _selector;
+ Inkscape::UI::SelectedColor _selected_color;
+ Gtk::Popover& _popover;
+ Gtk::Image& _repeat_icon;
+ GradientWithStops _gradient_image;
+ Glib::RefPtr<Gtk::ListStore> _stop_list_store;
+ Gtk::TreeModelColumnRecord _stop_columns;
+ Gtk::TreeModelColumn<SPStop*> _stopObj;
+ Gtk::TreeModelColumn<size_t> _stopIdx;
+ Gtk::TreeModelColumn<Glib::ustring> _stopID;
+ Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf>> _stop_color;
+ Gtk::TreeView& _stop_tree;
+ Gtk::SpinButton& _offset_btn;
+ Gtk::Button& _add_stop;
+ Gtk::Button& _delete_stop;
+ Gtk::Expander& _show_stops_list;
+ bool _stops_list_visible = true;
+ Gtk::Box& _stops_gallery;
+ Gtk::Box& _colors_box;
+ Gtk::ToggleButton& _linear_btn;
+ Gtk::ToggleButton& _radial_btn;
+ Gtk::Grid& _main_grid;
+ SPGradient* _gradient = nullptr;
+ SPDocument* _document = nullptr;
+ OperationBlocker _update;
+ OperationBlocker _notification;
+ Glib::ustring _prefs;
+};
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif
diff --git a/src/ui/widget/gradient-image.cpp b/src/ui/widget/gradient-image.cpp
new file mode 100644
index 0000000..0322157
--- /dev/null
+++ b/src/ui/widget/gradient-image.cpp
@@ -0,0 +1,247 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * A simple gradient preview
+ *
+ * Author:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ *
+ * Copyright (C) 2001-2002 Lauris Kaplinski
+ * Copyright (C) 2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <sigc++/sigc++.h>
+
+#include <glibmm/refptr.h>
+#include <gdkmm/pixbuf.h>
+
+#include <cairomm/surface.h>
+
+#include "gradient-image.h"
+
+#include "display/cairo-utils.h"
+
+#include "object/sp-gradient.h"
+#include "object/sp-stop.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+GradientImage::GradientImage(SPGradient *gradient)
+ : _gradient(nullptr)
+{
+ set_has_window(false);
+ set_gradient(gradient);
+}
+
+GradientImage::~GradientImage()
+{
+ if (_gradient) {
+ _release_connection.disconnect();
+ _modified_connection.disconnect();
+ _gradient = nullptr;
+ }
+}
+
+void
+GradientImage::size_request(GtkRequisition *requisition) const
+{
+ requisition->width = 54;
+ requisition->height = 12;
+}
+
+void
+GradientImage::get_preferred_width_vfunc(int &minimal_width, int &natural_width) const
+{
+ GtkRequisition requisition;
+ size_request(&requisition);
+ minimal_width = natural_width = requisition.width;
+}
+
+void
+GradientImage::get_preferred_height_vfunc(int &minimal_height, int &natural_height) const
+{
+ GtkRequisition requisition;
+ size_request(&requisition);
+ minimal_height = natural_height = requisition.height;
+}
+
+bool
+GradientImage::on_draw(const Cairo::RefPtr<Cairo::Context> &cr)
+{
+ auto allocation = get_allocation();
+
+ cairo_pattern_t *check = ink_cairo_pattern_create_checkerboard();
+ auto ct = cr->cobj();
+
+ cairo_set_source(ct, check);
+ cairo_paint(ct);
+ cairo_pattern_destroy(check);
+
+ if (_gradient) {
+ auto p = _gradient->create_preview_pattern(allocation.get_width());
+ cairo_set_source(ct, p);
+ cairo_paint(ct);
+ cairo_pattern_destroy(p);
+ }
+
+ return true;
+}
+
+void
+GradientImage::set_gradient(SPGradient *gradient)
+{
+ if (_gradient) {
+ _release_connection.disconnect();
+ _modified_connection.disconnect();
+ }
+
+ _gradient = gradient;
+
+ if (gradient) {
+ _release_connection = gradient->connectRelease(sigc::mem_fun(*this, &GradientImage::gradient_release));
+ _modified_connection = gradient->connectModified(sigc::mem_fun(*this, &GradientImage::gradient_modified));
+ }
+
+ update();
+}
+
+void
+GradientImage::gradient_release(SPObject *)
+{
+ if (_gradient) {
+ _release_connection.disconnect();
+ _modified_connection.disconnect();
+ }
+
+ _gradient = nullptr;
+
+ update();
+}
+
+void
+GradientImage::gradient_modified(SPObject *, guint /*flags*/)
+{
+ update();
+}
+
+void
+GradientImage::update()
+{
+ if (get_is_drawable()) {
+ queue_draw();
+ }
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+GdkPixbuf*
+sp_gradient_to_pixbuf (SPGradient *gr, int width, int height)
+{
+ cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
+ cairo_t *ct = cairo_create(s);
+
+ cairo_pattern_t *check = ink_cairo_pattern_create_checkerboard();
+ cairo_set_source(ct, check);
+ cairo_paint(ct);
+ cairo_pattern_destroy(check);
+
+ if (gr) {
+ cairo_pattern_t *p = gr->create_preview_pattern(width);
+ cairo_set_source(ct, p);
+ cairo_paint(ct);
+ cairo_pattern_destroy(p);
+ }
+
+ cairo_destroy(ct);
+ cairo_surface_flush(s);
+
+ // no need to free s - the call below takes ownership
+ GdkPixbuf *pixbuf = ink_pixbuf_create_from_cairo_surface(s);
+ return pixbuf;
+}
+
+
+Glib::RefPtr<Gdk::Pixbuf>
+sp_gradient_to_pixbuf_ref (SPGradient *gr, int width, int height)
+{
+ cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
+ cairo_t *ct = cairo_create(s);
+
+ cairo_pattern_t *check = ink_cairo_pattern_create_checkerboard();
+ cairo_set_source(ct, check);
+ cairo_paint(ct);
+ cairo_pattern_destroy(check);
+
+ if (gr) {
+ cairo_pattern_t *p = gr->create_preview_pattern(width);
+ cairo_set_source(ct, p);
+ cairo_paint(ct);
+ cairo_pattern_destroy(p);
+ }
+
+ 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, width, height);
+
+ cairo_surface_destroy(s);
+
+ return pixbuf;
+}
+
+
+Glib::RefPtr<Gdk::Pixbuf>
+sp_gradstop_to_pixbuf_ref (SPStop *stop, int width, int height)
+{
+ cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
+ cairo_t *ct = cairo_create(s);
+
+ /* Checkerboard background */
+ cairo_pattern_t *check = ink_cairo_pattern_create_checkerboard();
+ cairo_rectangle(ct, 0, 0, width, height);
+ cairo_set_source(ct, check);
+ cairo_fill_preserve(ct);
+ cairo_pattern_destroy(check);
+
+ if (stop) {
+ /* Alpha area */
+ cairo_rectangle(ct, 0, 0, width/2, height);
+ ink_cairo_set_source_rgba32(ct, stop->get_rgba32());
+ cairo_fill(ct);
+
+ /* Solid area */
+ cairo_rectangle(ct, width/2, 0, width, height);
+ ink_cairo_set_source_rgba32(ct, stop->get_rgba32() | 0xff);
+ cairo_fill(ct);
+ }
+
+ 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, width, height);
+
+ cairo_surface_destroy(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:fileencoding=utf-8 :
diff --git a/src/ui/widget/gradient-image.h b/src/ui/widget/gradient-image.h
new file mode 100644
index 0000000..d583dbe
--- /dev/null
+++ b/src/ui/widget/gradient-image.h
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_SP_GRADIENT_IMAGE_H
+#define SEEN_SP_GRADIENT_IMAGE_H
+
+/**
+ * A simple gradient preview
+ *
+ * Author:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ *
+ * Copyright (C) 2001-2002 Lauris Kaplinski
+ * Copyright (C) 2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/refptr.h>
+#include <gtkmm/widget.h>
+
+class SPGradient;
+class SPObject;
+class SPStop;
+
+namespace Gdk {
+ class Pixbuf;
+}
+
+#include <sigc++/connection.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+class GradientImage : public Gtk::Widget {
+ private:
+ SPGradient *_gradient;
+
+ sigc::connection _release_connection;
+ sigc::connection _modified_connection;
+
+ void gradient_release(SPObject *obj);
+ void gradient_modified(SPObject *obj, guint flags);
+ void update();
+ void size_request(GtkRequisition *requisition) const;
+
+ 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;
+ bool on_draw(const Cairo::RefPtr<Cairo::Context> &cr) override;
+
+ public:
+ GradientImage(SPGradient *gradient);
+ ~GradientImage() override;
+
+ void set_gradient(SPGradient *gr);
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+GdkPixbuf *sp_gradient_to_pixbuf (SPGradient *gr, int width, int height);
+Glib::RefPtr<Gdk::Pixbuf> sp_gradient_to_pixbuf_ref (SPGradient *gr, int width, int height);
+Glib::RefPtr<Gdk::Pixbuf> sp_gradstop_to_pixbuf_ref (SPStop *gr, int width, int height);
+
+#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/widget/gradient-selector-interface.h b/src/ui/widget/gradient-selector-interface.h
new file mode 100644
index 0000000..b6833cf
--- /dev/null
+++ b/src/ui/widget/gradient-selector-interface.h
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_GRADIENT_SELECTOR_INTERFACE_H
+#define SEEN_GRADIENT_SELECTOR_INTERFACE_H
+
+#include "object/sp-gradient.h"
+#include "object/sp-gradient-spread.h"
+#include "object/sp-gradient-units.h"
+
+class GradientSelectorInterface {
+public:
+ enum SelectorMode { MODE_LINEAR, MODE_RADIAL, MODE_SWATCH };
+
+ // pass gradient object (SPLinearGradient or SPRadialGradient)
+ virtual void setGradient(SPGradient* gradient) = 0;
+
+ virtual SPGradient* getVector() = 0;
+ virtual void setVector(SPDocument* doc, SPGradient* vector) = 0;
+ virtual void setMode(SelectorMode mode) = 0;
+ virtual void setUnits(SPGradientUnits units) = 0;
+ virtual SPGradientUnits getUnits() = 0;
+ virtual void setSpread(SPGradientSpread spread) = 0;
+ virtual SPGradientSpread getSpread() = 0;
+ virtual void selectStop(SPStop* selected) {};
+
+ sigc::signal<void (SPStop*)>& signal_stop_selected() { return _signal_stop_selected; }
+ void emit_stop_selected(SPStop* stop) { _signal_stop_selected.emit(stop); }
+
+private:
+ sigc::signal<void (SPStop*)> _signal_stop_selected;
+};
+
+#endif
diff --git a/src/ui/widget/gradient-selector.cpp b/src/ui/widget/gradient-selector.cpp
new file mode 100644
index 0000000..3a5380d
--- /dev/null
+++ b/src/ui/widget/gradient-selector.cpp
@@ -0,0 +1,611 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Gradient vector widget
+ *
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2001-2002 Lauris Kaplinski
+ * Copyright (C) 2001 Ximian, Inc.
+ * Copyright (C) 2010 Jon A. Cruz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+#include <gtkmm/treeview.h>
+#include <vector>
+
+#include "document-undo.h"
+#include "document.h"
+#include "gradient-chemistry.h"
+#include "id-clash.h"
+#include "inkscape.h"
+#include "preferences.h"
+
+#include "object/sp-defs.h"
+#include "style.h"
+
+#include "actions/actions-tools.h" // Invoke gradient tool
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "ui/widget/gradient-vector-selector.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+void GradientSelector::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(*manage(Glib::wrap(child)));
+ btn->set_relief(Gtk::RELIEF_NONE);
+}
+
+GradientSelector::GradientSelector()
+ : _blocked(false)
+ , _mode(MODE_LINEAR)
+ , _gradientUnits(SP_GRADIENT_UNITS_USERSPACEONUSE)
+ , _gradientSpread(SP_GRADIENT_SPREAD_PAD)
+{
+ set_orientation(Gtk::ORIENTATION_VERTICAL);
+
+ /* Vectors */
+ _vectors = Gtk::manage(new GradientVectorSelector(nullptr, nullptr));
+ _store = _vectors->get_store();
+ _columns = _vectors->get_columns();
+
+ _treeview = Gtk::manage(new Gtk::TreeView());
+ _treeview->set_model(_store);
+ _treeview->set_headers_clickable(true);
+ _treeview->set_search_column(1);
+ _treeview->set_vexpand();
+ _icon_renderer = Gtk::manage(new Gtk::CellRendererPixbuf());
+ _text_renderer = Gtk::manage(new Gtk::CellRendererText());
+
+ _treeview->append_column(_("Gradient"), *_icon_renderer);
+ auto icon_column = _treeview->get_column(0);
+ icon_column->add_attribute(_icon_renderer->property_pixbuf(), _columns->pixbuf);
+ icon_column->set_sort_column(_columns->color);
+ icon_column->set_clickable(true);
+
+ _treeview->append_column(_("Name"), *_text_renderer);
+ auto name_column = _treeview->get_column(1);
+ _text_renderer->property_editable() = true;
+ name_column->add_attribute(_text_renderer->property_text(), _columns->name);
+ name_column->set_min_width(180);
+ name_column->set_clickable(true);
+ name_column->set_resizable(true);
+
+ _treeview->append_column("#", _columns->refcount);
+ auto count_column = _treeview->get_column(2);
+ count_column->set_clickable(true);
+ count_column->set_resizable(true);
+
+ _treeview->signal_key_press_event().connect(sigc::mem_fun(*this, &GradientSelector::onKeyPressEvent), false);
+
+ _treeview->show();
+
+ icon_column->signal_clicked().connect(sigc::mem_fun(*this, &GradientSelector::onTreeColorColClick));
+ name_column->signal_clicked().connect(sigc::mem_fun(*this, &GradientSelector::onTreeNameColClick));
+ count_column->signal_clicked().connect(sigc::mem_fun(*this, &GradientSelector::onTreeCountColClick));
+
+ auto tree_select_connection = _treeview->get_selection()->signal_changed().connect(sigc::mem_fun(*this, &GradientSelector::onTreeSelection));
+ _vectors->set_tree_select_connection(tree_select_connection);
+ _text_renderer->signal_edited().connect(sigc::mem_fun(*this, &GradientSelector::onGradientRename));
+
+ _scrolled_window = Gtk::manage(new Gtk::ScrolledWindow());
+ _scrolled_window->add(*_treeview);
+ _scrolled_window->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ _scrolled_window->set_shadow_type(Gtk::SHADOW_IN);
+ _scrolled_window->set_size_request(0, 180);
+ _scrolled_window->set_hexpand();
+ _scrolled_window->show();
+
+ pack_start(*_scrolled_window, true, true, 4);
+
+
+ /* Create box for buttons */
+ auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0));
+ hb->set_homogeneous(false);
+ pack_start(*hb, false, false, 0);
+
+ _add = Gtk::manage(new Gtk::Button());
+ style_button(_add, INKSCAPE_ICON("list-add"));
+
+ _nonsolid.push_back(_add);
+ hb->pack_start(*_add, false, false, 0);
+
+ _add->signal_clicked().connect(sigc::mem_fun(*this, &GradientSelector::add_vector_clicked));
+ _add->set_sensitive(false);
+ _add->set_relief(Gtk::RELIEF_NONE);
+ _add->set_tooltip_text(_("Create a duplicate gradient"));
+
+ _del2 = Gtk::manage(new Gtk::Button());
+ style_button(_del2, INKSCAPE_ICON("list-remove"));
+
+ _nonsolid.push_back(_del2);
+ hb->pack_start(*_del2, false, false, 0);
+ _del2->signal_clicked().connect(sigc::mem_fun(*this, &GradientSelector::delete_vector_clicked_2));
+ _del2->set_sensitive(false);
+ _del2->set_relief(Gtk::RELIEF_NONE);
+ _del2->set_tooltip_text(_("Delete unused gradient"));
+
+ // The only use of this button is hidden!
+ _edit = Gtk::manage(new Gtk::Button());
+ style_button(_edit, INKSCAPE_ICON("edit"));
+
+ _nonsolid.push_back(_edit);
+ hb->pack_start(*_edit, false, false, 0);
+ _edit->signal_clicked().connect(sigc::mem_fun(*this, &GradientSelector::edit_vector_clicked));
+ _edit->set_sensitive(false);
+ _edit->set_relief(Gtk::RELIEF_NONE);
+ _edit->set_tooltip_text(_("Edit gradient"));
+ _edit->set_no_show_all();
+
+ _del = Gtk::manage(new Gtk::Button());
+ style_button(_del, INKSCAPE_ICON("list-remove"));
+
+ _swatch_widgets.push_back(_del);
+ hb->pack_start(*_del, false, false, 0);
+ _del->signal_clicked().connect(sigc::mem_fun(*this, &GradientSelector::delete_vector_clicked));
+ _del->set_sensitive(false);
+ _del->set_relief(Gtk::RELIEF_NONE);
+ _del->set_tooltip_text(_("Delete swatch"));
+
+ hb->show_all();
+}
+
+void GradientSelector::setSpread(SPGradientSpread spread)
+{
+ _gradientSpread = spread;
+ // gtk_combo_box_set_active (GTK_COMBO_BOX(this->spread), gradientSpread);
+}
+
+void GradientSelector::setMode(SelectorMode mode)
+{
+ if (mode != _mode) {
+ _mode = mode;
+ if (mode == MODE_SWATCH) {
+ for (auto &it : _nonsolid) {
+ it->hide();
+ }
+ for (auto &swatch_widget : _swatch_widgets) {
+ swatch_widget->show_all();
+ }
+
+ auto icon_column = _treeview->get_column(0);
+ icon_column->set_title(_("Swatch"));
+
+ _vectors->setSwatched();
+ } else {
+ for (auto &it : _nonsolid) {
+ it->show_all();
+ }
+ for (auto &swatch_widget : _swatch_widgets) {
+ swatch_widget->hide();
+ }
+ auto icon_column = _treeview->get_column(0);
+ icon_column->set_title(_("Gradient"));
+ }
+ }
+}
+
+void GradientSelector::setUnits(SPGradientUnits units) { _gradientUnits = units; }
+
+SPGradientUnits GradientSelector::getUnits() { return _gradientUnits; }
+
+SPGradientSpread GradientSelector::getSpread() { return _gradientSpread; }
+
+void GradientSelector::onGradientRename(const Glib::ustring &path_string, const Glib::ustring &new_text)
+{
+ Gtk::TreePath path(path_string);
+ auto iter = _store->get_iter(path);
+
+ if (iter) {
+ Gtk::TreeModel::Row row = *iter;
+ if (row) {
+ SPObject *obj = row[_columns->data];
+ if (obj) {
+ if (!new_text.empty() && new_text != gr_prepare_label(obj)) {
+ obj->setLabel(new_text.c_str());
+ Inkscape::DocumentUndo::done(obj->document, _("Rename gradient"), INKSCAPE_ICON("color-gradient"));
+ }
+ row[_columns->name] = gr_prepare_label(obj);
+ }
+ }
+ }
+}
+
+void GradientSelector::onTreeColorColClick()
+{
+ auto column = _treeview->get_column(0);
+ column->set_sort_column(_columns->color);
+}
+
+void GradientSelector::onTreeNameColClick()
+{
+ auto column = _treeview->get_column(1);
+ column->set_sort_column(_columns->name);
+}
+
+
+void GradientSelector::onTreeCountColClick()
+{
+ auto column = _treeview->get_column(2);
+ column->set_sort_column(_columns->refcount);
+}
+
+void GradientSelector::moveSelection(int amount, bool down, bool toEnd)
+{
+ auto select = _treeview->get_selection();
+ auto iter = select->get_selected();
+
+ if (amount < 0) {
+ down = !down;
+ amount = -amount;
+ }
+
+ auto canary = iter;
+ if (down) {
+ ++canary;
+ } else {
+ --canary;
+ }
+ while (canary && (toEnd || amount > 0)) {
+ --amount;
+ if (down) {
+ ++canary;
+ ++iter;
+ } else {
+ --canary;
+ --iter;
+ }
+ }
+
+ select->select(iter);
+ _treeview->scroll_to_row(_store->get_path(iter), 0.5);
+}
+
+bool GradientSelector::onKeyPressEvent(GdkEventKey *event)
+{
+ bool consume = false;
+ auto display = Gdk::Display::get_default();
+ auto keymap = display->get_keymap();
+ guint key = 0;
+ gdk_keymap_translate_keyboard_state(keymap, event->hardware_keycode, static_cast<GdkModifierType>(event->state), 0,
+ &key, 0, 0, 0);
+
+ switch (key) {
+ case GDK_KEY_Up:
+ case GDK_KEY_KP_Up: {
+ moveSelection(-1);
+ consume = true;
+ break;
+ }
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Down: {
+ moveSelection(1);
+ consume = true;
+ break;
+ }
+ case GDK_KEY_Page_Up:
+ case GDK_KEY_KP_Page_Up: {
+ moveSelection(-5);
+ consume = true;
+ break;
+ }
+
+ case GDK_KEY_Page_Down:
+ case GDK_KEY_KP_Page_Down: {
+ moveSelection(5);
+ consume = true;
+ break;
+ }
+
+ case GDK_KEY_End:
+ case GDK_KEY_KP_End: {
+ moveSelection(0, true, true);
+ consume = true;
+ break;
+ }
+
+ case GDK_KEY_Home:
+ case GDK_KEY_KP_Home: {
+ moveSelection(0, false, true);
+ consume = true;
+ break;
+ }
+ }
+ return consume;
+}
+
+void GradientSelector::onTreeSelection()
+{
+ if (!_treeview) {
+ return;
+ }
+
+ if (_blocked) {
+ return;
+ }
+
+ if (!_treeview->has_focus()) {
+ /* Workaround for GTK bug on Windows/OS X
+ * When the treeview initially doesn't have focus and is clicked
+ * sometimes get_selection()->signal_changed() has the wrong selection
+ */
+ _treeview->grab_focus();
+ }
+
+ const auto sel = _treeview->get_selection();
+ if (!sel) {
+ return;
+ }
+
+ SPGradient *obj = nullptr;
+ /* Single selection */
+ auto iter = sel->get_selected();
+ if (iter) {
+ Gtk::TreeModel::Row row = *iter;
+ obj = row[_columns->data];
+ }
+
+ if (obj) {
+ vector_set(obj);
+ }
+
+ check_del_button();
+}
+
+void GradientSelector::check_del_button() {
+ const auto sel = _treeview->get_selection();
+ if (!sel) {
+ return;
+ }
+
+ SPGradient *obj = nullptr;
+ /* Single selection */
+ auto iter = sel->get_selected();
+ if (iter) {
+ Gtk::TreeModel::Row row = *iter;
+ obj = row[_columns->data];
+ }
+ if (_del2) {
+ _del2->set_sensitive(obj && sp_get_gradient_refcount(obj->document, obj) < 2 && _store->children().size() > 1);
+ }
+}
+
+bool GradientSelector::_checkForSelected(const Gtk::TreePath &path, const Gtk::TreeIter &iter, SPGradient *vector)
+{
+ bool found = false;
+
+ Gtk::TreeModel::Row row = *iter;
+ if (vector == row[_columns->data]) {
+ _treeview->scroll_to_row(path, 0.5);
+ auto select = _treeview->get_selection();
+ bool wasBlocked = _blocked;
+ _blocked = true;
+ select->select(iter);
+ _blocked = wasBlocked;
+ found = true;
+ }
+
+ return found;
+}
+
+void GradientSelector::selectGradientInTree(SPGradient *vector)
+{
+ _store->foreach (sigc::bind<SPGradient *>(sigc::mem_fun(*this, &GradientSelector::_checkForSelected), vector));
+}
+
+void GradientSelector::setVector(SPDocument *doc, SPGradient *vector)
+{
+ g_return_if_fail(!vector || (vector->document == doc));
+
+ if (vector && !vector->hasStops()) {
+ return;
+ }
+
+ _vectors->set_gradient(doc, vector);
+
+ selectGradientInTree(vector);
+
+ if (vector) {
+ if ((_mode == MODE_SWATCH) && vector->isSwatch()) {
+ if (vector->isSolid()) {
+ for (auto &it : _nonsolid) {
+ it->hide();
+ }
+ } else {
+ for (auto &it : _nonsolid) {
+ it->show_all();
+ }
+ }
+ } else if (_mode != MODE_SWATCH) {
+
+ for (auto &swatch_widget : _swatch_widgets) {
+ swatch_widget->hide();
+ }
+ for (auto &it : _nonsolid) {
+ it->show_all();
+ }
+ }
+
+ if (_edit) {
+ _edit->set_sensitive(true);
+ }
+ if (_add) {
+ _add->set_sensitive(true);
+ }
+ if (_del) {
+ _del->set_sensitive(true);
+ }
+ check_del_button();
+ } else {
+ if (_edit) {
+ _edit->set_sensitive(false);
+ }
+ if (_add) {
+ _add->set_sensitive(doc != nullptr);
+ }
+ if (_del) {
+ _del->set_sensitive(false);
+ }
+ if (_del2) {
+ _del2->set_sensitive(false);
+ }
+ }
+}
+
+SPGradient *GradientSelector::getVector()
+{
+ return _vectors->get_gradient();
+}
+
+
+void GradientSelector::vector_set(SPGradient *gr)
+{
+ if (!_blocked) {
+ _blocked = true;
+ gr = sp_gradient_ensure_vector_normalized(gr);
+ setVector((gr) ? gr->document : nullptr, gr);
+ _signal_changed.emit(gr);
+ _blocked = false;
+ }
+}
+
+void GradientSelector::delete_vector_clicked_2() {
+ const auto selection = _treeview->get_selection();
+ if (!selection) {
+ return;
+ }
+
+ SPGradient *obj = nullptr;
+ /* Single selection */
+ Gtk::TreeModel::iterator iter = selection->get_selected();
+ if (iter) {
+ Gtk::TreeModel::Row row = *iter;
+ obj = row[_columns->data];
+ }
+
+ if (obj) {
+ if (auto repr = obj->getRepr()) {
+ repr->setAttribute("inkscape:collect", "always");
+
+ auto move = iter;
+ --move;
+ if (!move) {
+ move = iter;
+ ++move;
+ }
+ if (move) {
+ selection->select(move);
+ _treeview->scroll_to_row(_store->get_path(move), 0.5);
+ }
+ }
+ }
+}
+
+void GradientSelector::delete_vector_clicked()
+{
+ const auto selection = _treeview->get_selection();
+ if (!selection) {
+ return;
+ }
+
+ SPGradient *obj = nullptr;
+ /* Single selection */
+ Gtk::TreeModel::iterator iter = selection->get_selected();
+ if (iter) {
+ Gtk::TreeModel::Row row = *iter;
+ obj = row[_columns->data];
+ }
+
+ if (obj) {
+ std::string id = obj->getId();
+ sp_gradient_unset_swatch(SP_ACTIVE_DESKTOP, id);
+ }
+}
+
+void GradientSelector::edit_vector_clicked()
+{
+ // Invoke the gradient tool.... never actually called as button is hidden in only use!
+ set_active_tool(SP_ACTIVE_DESKTOP, "Gradient");
+}
+
+void GradientSelector::add_vector_clicked()
+{
+ auto doc = _vectors->get_document();
+
+ if (!doc)
+ return;
+
+ auto gr = _vectors->get_gradient();
+ Inkscape::XML::Document *xml_doc = doc->getReprDoc();
+
+ Inkscape::XML::Node *repr = nullptr;
+
+ if (gr) {
+ gr->getRepr()->removeAttribute("inkscape:collect");
+ repr = gr->getRepr()->duplicate(xml_doc);
+ // Rename the new gradients id to be similar to the cloned gradients
+ auto new_id = generate_similar_unique_id(doc, gr->getId());
+ gr->setAttribute("id", new_id.c_str());
+ doc->getDefs()->getRepr()->addChild(repr, nullptr);
+ } else {
+ repr = xml_doc->createElement("svg:linearGradient");
+ Inkscape::XML::Node *stop = xml_doc->createElement("svg:stop");
+ stop->setAttribute("offset", "0");
+ stop->setAttribute("style", "stop-color:#000;stop-opacity:1;");
+ repr->appendChild(stop);
+ Inkscape::GC::release(stop);
+ stop = xml_doc->createElement("svg:stop");
+ stop->setAttribute("offset", "1");
+ stop->setAttribute("style", "stop-color:#fff;stop-opacity:1;");
+ repr->appendChild(stop);
+ Inkscape::GC::release(stop);
+ doc->getDefs()->getRepr()->addChild(repr, nullptr);
+ gr = cast<SPGradient>(doc->getObjectByRepr(repr));
+ }
+
+ _vectors->set_gradient(doc, gr);
+
+ selectGradientInTree(gr);
+
+ // assign gradient to selection
+ vector_set(gr);
+
+ Inkscape::GC::release(repr);
+}
+
+void GradientSelector::show_edit_button(bool show) {
+ if (show) _edit->show(); else _edit->hide();
+}
+
+void GradientSelector::set_name_col_size(int min_width) {
+ auto name_column = _treeview->get_column(1);
+ name_column->set_min_width(min_width);
+}
+
+void GradientSelector::set_gradient_size(int width, int height) {
+ _vectors->set_pixmap_size(width, height);
+}
+
+} // 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 :
diff --git a/src/ui/widget/gradient-selector.h b/src/ui/widget/gradient-selector.h
new file mode 100644
index 0000000..aae4369
--- /dev/null
+++ b/src/ui/widget/gradient-selector.h
@@ -0,0 +1,160 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_GRADIENT_SELECTOR_H
+#define SEEN_GRADIENT_SELECTOR_H
+
+/*
+ * Gradient vector and position widget
+ *
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2001-2002 Lauris Kaplinski
+ * Copyright (C) 2001 Ximian, Inc.
+ * Copyright (C) 2010 Jon A. Cruz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/box.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/scrolledwindow.h>
+
+#include "object/sp-gradient-spread.h"
+#include "object/sp-gradient-units.h"
+#include <vector>
+#include "gradient-selector-interface.h"
+
+class SPDocument;
+class SPGradient;
+
+namespace Gtk {
+class Button;
+class CellRendererPixbuf;
+class CellRendererText;
+class ScrolledWindow;
+class TreeView;
+} // namespace Gtk
+
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+class GradientVectorSelector;
+
+class GradientSelector : public Gtk::Box, public GradientSelectorInterface {
+ public:
+ // enum SelectorMode { MODE_LINEAR, MODE_RADIAL, MODE_SWATCH };
+
+ class ModelColumns : public Gtk::TreeModel::ColumnRecord {
+ public:
+ ModelColumns()
+ {
+ add(name);
+ add(refcount);
+ add(color);
+ add(data);
+ add(pixbuf);
+ }
+ ~ModelColumns() override = default;
+
+ Gtk::TreeModelColumn<Glib::ustring> name;
+ Gtk::TreeModelColumn<unsigned long> color;
+ Gtk::TreeModelColumn<gint> refcount;
+ Gtk::TreeModelColumn<SPGradient *> data;
+ Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf>> pixbuf;
+ };
+
+
+ private:
+ sigc::signal<void ()> _signal_grabbed;
+ sigc::signal<void ()> _signal_dragged;
+ sigc::signal<void ()> _signal_released;
+ sigc::signal<void (SPGradient *)> _signal_changed;
+ SelectorMode _mode;
+
+ SPGradientUnits _gradientUnits;
+ SPGradientSpread _gradientSpread;
+
+ /* Vector selector */
+ GradientVectorSelector *_vectors;
+
+ /* Tree */
+ bool _checkForSelected(const Gtk::TreePath &path, const Gtk::TreeIter &iter, SPGradient *vector);
+ bool onKeyPressEvent(GdkEventKey *event);
+ void onTreeSelection();
+ void onGradientRename(const Glib::ustring &path_string, const Glib::ustring &new_text);
+ void onTreeNameColClick();
+ void onTreeColorColClick();
+ void onTreeCountColClick();
+
+ Gtk::TreeView *_treeview;
+ Gtk::ScrolledWindow *_scrolled_window;
+ ModelColumns *_columns;
+ Glib::RefPtr<Gtk::ListStore> _store;
+ Gtk::CellRendererPixbuf *_icon_renderer;
+ Gtk::CellRendererText *_text_renderer;
+
+ /* Editing buttons */
+ Gtk::Button *_edit;
+ Gtk::Button *_add;
+ Gtk::Button *_del;
+ Gtk::Button *_del2;
+
+ bool _blocked;
+
+ std::vector<Gtk::Widget *> _nonsolid;
+ std::vector<Gtk::Widget *> _swatch_widgets;
+
+ void selectGradientInTree(SPGradient *vector);
+ void moveSelection(int amount, bool down = true, bool toEnd = false);
+
+ void style_button(Gtk::Button *btn, char const *iconName);
+ void check_del_button();
+
+ // Signal handlers
+ void add_vector_clicked();
+ void edit_vector_clicked();
+ void delete_vector_clicked();
+ void delete_vector_clicked_2();
+ void vector_set(SPGradient *gr);
+
+ public:
+ GradientSelector();
+
+ void show_edit_button(bool show);
+ void set_name_col_size(int min_width);
+ void set_gradient_size(int width, int height);
+
+ inline decltype(_signal_changed) signal_changed() const { return _signal_changed; }
+ inline decltype(_signal_grabbed) signal_grabbed() const { return _signal_grabbed; }
+ inline decltype(_signal_dragged) signal_dragged() const { return _signal_dragged; }
+ inline decltype(_signal_released) signal_released() const { return _signal_released; }
+
+ void setGradient(SPGradient* gradient) override { /* no op */ }
+ SPGradient *getVector() override;
+ void setVector(SPDocument *doc, SPGradient *vector) override;
+ void setMode(SelectorMode mode) override;
+ void setUnits(SPGradientUnits units) override;
+ SPGradientUnits getUnits() override;
+ void setSpread(SPGradientSpread spread) override;
+ SPGradientSpread getSpread() override;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // SEEN_GRADIENT_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 :
diff --git a/src/ui/widget/gradient-vector-selector.cpp b/src/ui/widget/gradient-vector-selector.cpp
new file mode 100644
index 0000000..7626b50
--- /dev/null
+++ b/src/ui/widget/gradient-vector-selector.cpp
@@ -0,0 +1,326 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Gradient vector selection widget
+ *
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * MenTaLguY <mental@rydia.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2001-2002 Lauris Kaplinski
+ * Copyright (C) 2001 Ximian, Inc.
+ * Copyright (C) 2004 Monash University
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2006 MenTaLguY
+ * Copyright (C) 2010 Jon A. Cruz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ *
+ */
+
+#include "ui/widget/gradient-vector-selector.h"
+
+#include <set>
+
+#include <glibmm.h>
+#include <glibmm/i18n.h>
+
+#include "gradient-chemistry.h"
+#include "inkscape.h"
+#include "preferences.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "layer-manager.h"
+#include "include/macros.h"
+#include "selection-chemistry.h"
+
+#include "io/resource.h"
+
+#include "object/sp-defs.h"
+#include "object/sp-linear-gradient.h"
+#include "object/sp-radial-gradient.h"
+#include "object/sp-root.h"
+#include "object/sp-stop.h"
+#include "style.h"
+
+#include "svg/css-ostringstream.h"
+
+#include "ui/dialog-events.h"
+#include "ui/selected-color.h"
+#include "ui/widget/color-notebook.h"
+#include "ui/widget/color-preview.h"
+#include "ui/widget/gradient-image.h"
+
+#include "xml/repr.h"
+
+using Inkscape::UI::SelectedColor;
+
+void gr_get_usage_counts(SPDocument *doc, std::map<SPGradient *, gint> *mapUsageCount );
+unsigned long sp_gradient_to_hhssll(SPGradient *gr);
+
+// TODO FIXME kill these globals!!!
+static Glib::ustring const prefs_path = "/dialogs/gradienteditor/";
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+GradientVectorSelector::GradientVectorSelector(SPDocument *doc, SPGradient *gr)
+{
+ _columns = new GradientSelector::ModelColumns();
+ _store = Gtk::ListStore::create(*_columns);
+ set_orientation(Gtk::ORIENTATION_VERTICAL);
+
+ if (doc) {
+ set_gradient(doc, gr);
+ } else {
+ rebuild_gui_full();
+ }
+}
+
+GradientVectorSelector::~GradientVectorSelector()
+{
+ if (_gr) {
+ _gradient_release_connection.disconnect();
+ _tree_select_connection.disconnect();
+ _gr = nullptr;
+ }
+
+ if (_doc) {
+ _defs_release_connection.disconnect();
+ _defs_modified_connection.disconnect();
+ _doc = nullptr;
+ }
+}
+
+void GradientVectorSelector::set_gradient(SPDocument *doc, SPGradient *gr)
+{
+// g_message("sp_gradient_vector_selector_set_gradient(%p, %p, %p) [%s] %d %d", gvs, doc, gr,
+// (gr ? gr->getId():"N/A"),
+// (gr ? gr->isSwatch() : -1),
+// (gr ? gr->isSolid() : -1));
+ static gboolean suppress = FALSE;
+
+ g_return_if_fail(!gr || (doc != nullptr));
+ g_return_if_fail(!gr || (gr->document == doc));
+ g_return_if_fail(!gr || gr->hasStops());
+
+ if (doc != _doc) {
+ /* Disconnect signals */
+ if (_gr) {
+ _gradient_release_connection.disconnect();
+ _gr = nullptr;
+ }
+ if (_doc) {
+ _defs_release_connection.disconnect();
+ _defs_modified_connection.disconnect();
+ _doc = nullptr;
+ }
+
+ // Connect signals
+ if (doc) {
+ _defs_release_connection = doc->getDefs()->connectRelease(sigc::mem_fun(*this, &GradientVectorSelector::defs_release));
+ _defs_modified_connection = doc->getDefs()->connectModified(sigc::mem_fun(*this, &GradientVectorSelector::defs_modified));
+ }
+ if (gr) {
+ _gradient_release_connection = gr->connectRelease(sigc::mem_fun(*this, &GradientVectorSelector::gradient_release));
+ }
+ _doc = doc;
+ _gr = gr;
+ rebuild_gui_full();
+ if (!suppress) _signal_vector_set.emit(gr);
+ } else if (gr != _gr) {
+ // Harder case - keep document, rebuild list and stuff
+ // fixme: (Lauris)
+ suppress = TRUE;
+ set_gradient(nullptr, nullptr);
+ set_gradient(doc, gr);
+ suppress = FALSE;
+ _signal_vector_set.emit(gr);
+ }
+ /* The case of setting NULL -> NULL is not very interesting */
+}
+
+void
+GradientVectorSelector::gradient_release(SPObject * /*obj*/)
+{
+ /* Disconnect gradient */
+ if (_gr) {
+ _gradient_release_connection.disconnect();
+ _gr = nullptr;
+ }
+
+ /* Rebuild GUI */
+ rebuild_gui_full();
+}
+
+void
+GradientVectorSelector::defs_release(SPObject * /*defs*/)
+{
+ _doc = nullptr;
+
+ _defs_release_connection.disconnect();
+ _defs_modified_connection.disconnect();
+
+ /* Disconnect gradient as well */
+ if (_gr) {
+ _gradient_release_connection.disconnect();
+ _gr = nullptr;
+ }
+
+ /* Rebuild GUI */
+ rebuild_gui_full();
+}
+
+void
+GradientVectorSelector::defs_modified(SPObject *defs, guint flags)
+{
+ /* fixme: We probably have to check some flags here (Lauris) */
+ rebuild_gui_full();
+}
+
+void
+GradientVectorSelector::rebuild_gui_full()
+{
+ _tree_select_connection.block();
+
+ /* Clear old list, if there is any */
+ _store->clear();
+
+ /* Pick up all gradients with vectors */
+ std::vector<SPGradient *> gl;
+ if (_gr) {
+ auto gradients = _gr->document->getResourceList("gradient");
+ for (auto gradient : gradients) {
+ auto grad = cast<SPGradient>(gradient);
+ if ( grad->hasStops() && (grad->isSwatch() == _swatched) ) {
+ gl.push_back(cast<SPGradient>(gradient));
+ }
+ }
+ }
+
+ /* Get usage count of all the gradients */
+ std::map<SPGradient *, gint> usageCount;
+ gr_get_usage_counts(_doc, &usageCount);
+
+ if (!_doc) {
+ Gtk::TreeModel::Row row = *(_store->append());
+ row[_columns->name] = _("No document selected");
+
+ } else if (gl.empty()) {
+ Gtk::TreeModel::Row row = *(_store->append());
+ row[_columns->name] = _("No gradients in document");
+
+ } else if (!_gr) {
+ Gtk::TreeModel::Row row = *(_store->append());
+ row[_columns->name] = _("No gradient selected");
+
+ } else {
+ for (auto gr:gl) {
+ unsigned long hhssll = sp_gradient_to_hhssll(gr);
+ GdkPixbuf *pixb = sp_gradient_to_pixbuf (gr, _pix_width, _pix_height);
+ Glib::ustring label = gr_prepare_label(gr);
+
+ Gtk::TreeModel::Row row = *(_store->append());
+ row[_columns->name] = label.c_str();
+ row[_columns->color] = hhssll;
+ row[_columns->refcount] = usageCount[gr];
+ row[_columns->data] = gr;
+ row[_columns->pixbuf] = Glib::wrap(pixb);
+ }
+ }
+
+ _tree_select_connection.unblock();
+}
+
+void
+GradientVectorSelector::setSwatched()
+{
+ _swatched = true;
+ rebuild_gui_full();
+}
+
+void GradientVectorSelector::set_pixmap_size(int width, int height) {
+ _pix_width = width;
+ _pix_height = height;
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+Glib::ustring gr_prepare_label(SPObject *obj)
+{
+ const gchar *id = obj->label() ? obj->label() : obj->getId();
+ if (!id) {
+ id = obj->getRepr()->name();
+ }
+
+ if (strlen(id) > 14 && (!strncmp (id, "linearGradient", 14) || !strncmp (id, "radialGradient", 14)))
+ return gr_ellipsize_text(id+14, 35);
+ return gr_ellipsize_text (id, 35);
+}
+
+/*
+ * 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 gr_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;
+}
+
+
+/*
+ * Return a "HHSSLL" version of the first stop color so we can sort by it
+ */
+unsigned long sp_gradient_to_hhssll(SPGradient *gr)
+{
+ SPStop *stop = gr->getFirstStop();
+ unsigned long rgba = stop->get_rgba32();
+ float hsl[3];
+ SPColor::rgb_to_hsl_floatv (hsl, SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), SP_RGBA32_B_F(rgba));
+
+ return ((int)(hsl[0]*100 * 10000)) + ((int)(hsl[1]*100 * 100)) + ((int)(hsl[2]*100 * 1));
+}
+
+/*
+ * Map each gradient to its usage count for both fill and stroke styles
+ */
+void gr_get_usage_counts(SPDocument *doc, std::map<SPGradient *, gint> *mapUsageCount )
+{
+ if (!doc)
+ return;
+
+ for (auto item : sp_get_all_document_items(doc)) {
+ if (!item->getId())
+ continue;
+ SPGradient *gr = nullptr;
+ gr = sp_item_get_gradient(item, true); // fill
+ if (gr) {
+ mapUsageCount->count(gr) > 0 ? (*mapUsageCount)[gr] += 1 : (*mapUsageCount)[gr] = 1;
+ }
+ gr = sp_item_get_gradient(item, false); // stroke
+ if (gr) {
+ mapUsageCount->count(gr) > 0 ? (*mapUsageCount)[gr] += 1 : (*mapUsageCount)[gr] = 1;
+ }
+ }
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ 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/gradient-vector-selector.h b/src/ui/widget/gradient-vector-selector.h
new file mode 100644
index 0000000..ac5460e
--- /dev/null
+++ b/src/ui/widget/gradient-vector-selector.h
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_GRADIENT_VECTOR_H
+#define SEEN_GRADIENT_VECTOR_H
+
+/*
+ * Gradient vector selection widget
+ *
+ * Author:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2010 Jon A. Cruz
+ * Copyright (C) 2001-2002 Lauris Kaplinski
+ * Copyright (C) 2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/widget/gradient-selector.h"
+
+#include <gtkmm/liststore.h>
+#include <sigc++/connection.h>
+
+class SPDocument;
+class SPObject;
+class SPGradient;
+class SPStop;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class GradientVectorSelector : public Gtk::Box {
+ private:
+ bool _swatched = false;
+
+ SPDocument *_doc = nullptr;
+ SPGradient *_gr = nullptr;
+
+ /* Gradient vectors store */
+ Glib::RefPtr<Gtk::ListStore> _store;
+ Inkscape::UI::Widget::GradientSelector::ModelColumns *_columns;
+
+ sigc::connection _gradient_release_connection;
+ sigc::connection _defs_release_connection;
+ sigc::connection _defs_modified_connection;
+ sigc::connection _tree_select_connection;
+
+ sigc::signal<void (SPGradient *)> _signal_vector_set;
+
+ void gradient_release(SPObject *obj);
+ void defs_release(SPObject *defs);
+ void defs_modified(SPObject *defs, guint flags);
+ void rebuild_gui_full();
+
+ public:
+ GradientVectorSelector(SPDocument *doc, SPGradient *gradient);
+ ~GradientVectorSelector() override;
+
+ void setSwatched();
+ void set_gradient(SPDocument *doc, SPGradient *gr);
+ // width and height of gradient preview pixmap
+ void set_pixmap_size(int width, int height);
+
+ inline decltype(_columns) get_columns() const { return _columns; }
+ inline decltype(_doc) get_document() const { return _doc; }
+ inline decltype(_gr) get_gradient() const { return _gr; }
+ inline decltype(_store) get_store() const { return _store; }
+
+ inline decltype(_signal_vector_set) signal_vector_set() const { return _signal_vector_set; }
+
+ inline void set_tree_select_connection(sigc::connection &connection) { _tree_select_connection = connection; }
+
+private:
+ int _pix_width = 64;
+ int _pix_height = 18;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+Glib::ustring gr_prepare_label (SPObject *obj);
+Glib::ustring gr_ellipsize_text(Glib::ustring const &src, size_t maxlen);
+
+#endif // SEEN_GRADIENT_VECTOR_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/gradient-with-stops.cpp b/src/ui/widget/gradient-with-stops.cpp
new file mode 100644
index 0000000..1d413dd
--- /dev/null
+++ b/src/ui/widget/gradient-with-stops.cpp
@@ -0,0 +1,552 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * Gradient image widget with stop handles
+ *
+ * Author:
+ * Michael Kowalski
+ *
+ * Copyright (C) 2020-2021 Michael Kowalski
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <string>
+
+#include "gradient-with-stops.h"
+#include "object/sp-gradient.h"
+#include "object/sp-stop.h"
+#include "display/cairo-utils.h"
+#include "io/resource.h"
+#include "ui/cursor-utils.h"
+#include "ui/util.h"
+
+// widget's height; it should take stop template's height into account
+// current value is fine-tuned to make stop handles overlap gradient image just the right amount
+const int GRADIENT_WIDGET_HEIGHT = 33;
+// gradient's image height (multiple of checkerboard tiles, they are 6x6)
+const int GRADIENT_IMAGE_HEIGHT = 3 * 6;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+using namespace Inkscape::IO;
+
+std::string get_stop_template_path(const char* filename) {
+ // "stop handle" template files path
+ return Resource::get_filename(Resource::UIS, filename);
+}
+
+GradientWithStops::GradientWithStops() :
+ _template(get_stop_template_path("gradient-stop.svg").c_str()),
+ _tip_template(get_stop_template_path("gradient-tip.svg").c_str())
+ {
+ // default color, it will be updated
+ _background_color.set_grey(0.5);
+ // for theming, but not used
+ set_name("GradientEdit");
+ // we need some events
+ add_events(Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK | Gdk::BUTTON_MOTION_MASK |
+ Gdk::POINTER_MOTION_MASK | Gdk::KEY_PRESS_MASK);
+ set_can_focus();
+}
+
+void GradientWithStops::set_gradient(SPGradient* gradient) {
+ _gradient = gradient;
+
+ // listen to release & changes
+ _release = gradient ? gradient->connectRelease([=](SPObject*){ set_gradient(nullptr); }) : sigc::connection();
+ _modified = gradient ? gradient->connectModified([=](SPObject*, guint){ modified(); }) : sigc::connection();
+
+ // TODO: check selected/focused stop index
+
+ modified();
+
+ set_sensitive(gradient != nullptr);
+}
+
+void GradientWithStops::modified() {
+ // gradient has been modified
+
+ // read all stops
+ _stops.clear();
+
+ if (_gradient) {
+ SPStop* stop = _gradient->getFirstStop();
+ while (stop) {
+ _stops.push_back(stop_t {
+ .offset = stop->offset, .color = stop->getColor(), .opacity = stop->getOpacity()
+ });
+ stop = stop->getNextStop();
+ }
+ }
+
+ update();
+}
+
+void GradientWithStops::size_request(GtkRequisition* requisition) const {
+ requisition->width = 60;
+ requisition->height = GRADIENT_WIDGET_HEIGHT;
+}
+
+void GradientWithStops::get_preferred_width_vfunc(int& minimal_width, int& natural_width) const {
+ GtkRequisition requisition;
+ size_request(&requisition);
+ minimal_width = natural_width = requisition.width;
+}
+
+void GradientWithStops::get_preferred_height_vfunc(int& minimal_height, int& natural_height) const {
+ GtkRequisition requisition;
+ size_request(&requisition);
+ minimal_height = natural_height = requisition.height;
+}
+
+void GradientWithStops::update() {
+ if (get_is_drawable()) {
+ queue_draw();
+ }
+}
+
+// capture background color when styles change
+void GradientWithStops::on_style_updated() {
+ if (auto wnd = dynamic_cast<Gtk::Window*>(this->get_toplevel())) {
+ auto sc = wnd->get_style_context();
+ _background_color = get_background_color(sc);
+ }
+
+ // load and cache cursors
+ auto wnd = get_window();
+ if (wnd && !_cursor_mouseover) {
+ // use standard cursors:
+ _cursor_mouseover = Gdk::Cursor::create(get_display(), "grab");
+ _cursor_dragging = Gdk::Cursor::create(get_display(), "grabbing");
+ _cursor_insert = Gdk::Cursor::create(get_display(), "crosshair");
+ // or custom cursors:
+ // _cursor_mouseover = load_svg_cursor(get_display(), wnd, "gradient-over-stop.svg");
+ // _cursor_dragging = load_svg_cursor(get_display(), wnd, "gradient-drag-stop.svg");
+ // _cursor_insert = load_svg_cursor(get_display(), wnd, "gradient-add-stop.svg");
+ wnd->set_cursor();
+ }
+}
+
+void draw_gradient(const Cairo::RefPtr<Cairo::Context>& cr, SPGradient* gradient, int x, int width) {
+ cairo_pattern_t* check = ink_cairo_pattern_create_checkerboard();
+
+ cairo_set_source(cr->cobj(), check);
+ cr->fill_preserve();
+ cairo_pattern_destroy(check);
+
+ if (gradient) {
+ auto p = gradient->create_preview_pattern(width);
+ cairo_matrix_t m;
+ cairo_matrix_init_translate(&m, -x, 0);
+ cairo_pattern_set_matrix(p, &m);
+ cairo_set_source(cr->cobj(), p);
+ cr->fill();
+ cairo_pattern_destroy(p);
+ }
+}
+
+// return on-screen position of the UI stop corresponding to the gradient's color stop at 'index'
+GradientWithStops::stop_pos_t GradientWithStops::get_stop_position(size_t index, const layout_t& layout) const {
+ if (!_gradient || index >= _stops.size()) {
+ return stop_pos_t {};
+ }
+
+ // half of the stop template width; round it to avoid half-pixel coordinates
+ const auto dx = round((_template.get_width_px() + 1) / 2);
+
+ auto pos = [&](double offset) { return round(layout.x + layout.width * CLAMP(offset, 0, 1)); };
+ const auto& v = _stops;
+
+ auto offset = pos(v[index].offset);
+ auto left = offset - dx;
+ if (index > 0) {
+ // check previous stop; it may overlap
+ auto prev = pos(v[index - 1].offset) + dx;
+ if (prev > left) {
+ // overlap
+ left = round((left + prev) / 2);
+ }
+ }
+
+ auto right = offset + dx;
+ if (index + 1 < v.size()) {
+ // check next stop for overlap
+ auto next = pos(v[index + 1].offset) - dx;
+ if (right > next) {
+ // overlap
+ right = round((right + next) / 2);
+ }
+ }
+
+ return stop_pos_t {
+ .left = left,
+ .tip = offset,
+ .right = right,
+ .top = layout.height - _template.get_height_px(),
+ .bottom = layout.height
+ };
+}
+
+// widget's layout; mainly location of the gradient's image and stop handles
+GradientWithStops::layout_t GradientWithStops::get_layout() const {
+ auto allocation = get_allocation();
+
+ const auto stop_width = _template.get_width_px();
+ const auto half_stop = round((stop_width + 1) / 2);
+ const auto x = half_stop;
+ const double width = allocation.get_width() - stop_width;
+ const double height = allocation.get_height();
+
+ return layout_t {
+ .x = x,
+ .y = 0,
+ .width = width,
+ .height = height
+ };
+}
+
+// check if stop handle is under (x, y) location, return its index or -1 if not hit
+int GradientWithStops::find_stop_at(double x, double y) const {
+ if (!_gradient) return -1;
+
+ const auto& v = _stops;
+ const auto& layout = get_layout();
+
+ // find stop handle at (x, y) position; note: stops may not be ordered by offsets
+ for (size_t i = 0; i < v.size(); ++i) {
+ auto pos = get_stop_position(i, layout);
+ if (x >= pos.left && x <= pos.right && y >= pos.top && y <= pos.bottom) {
+ return static_cast<int>(i);
+ }
+ }
+
+ return -1;
+}
+
+// this is range of offset adjustment for a given stop
+GradientWithStops::limits_t GradientWithStops::get_stop_limits(int maybe_index) const {
+ if (!_gradient) return limits_t {};
+
+ // let negative index turn into a large out-of-range number
+ auto index = static_cast<size_t>(maybe_index);
+
+ const auto& v = _stops;
+
+ if (index < v.size()) {
+ double min = 0;
+ double max = 1;
+
+ if (v.size() > 1) {
+ std::vector<double> offsets;
+ offsets.reserve(v.size());
+ for (auto& s : _stops) {
+ offsets.push_back(s.offset);
+ }
+ std::sort(offsets.begin(), offsets.end());
+
+ // special cases:
+ if (index == 0) { // first stop
+ max = offsets[index + 1];
+ }
+ else if (index + 1 == v.size()) { // last stop
+ min = offsets[index - 1];
+ }
+ else {
+ // stops "inside" gradient
+ min = offsets[index - 1];
+ max = offsets[index + 1];
+ }
+ }
+ return limits_t { .min_offset = min, .max_offset = max, .offset = v[index].offset };
+ }
+ else {
+ return limits_t {};
+ }
+}
+
+bool GradientWithStops::on_focus_out_event(GdkEventFocus* event) {
+ update();
+ return false;
+}
+
+bool GradientWithStops::on_focus_in_event(GdkEventFocus* event) {
+ update();
+ return false;
+}
+
+bool GradientWithStops::on_focus(Gtk::DirectionType direction) {
+ if (has_focus()) {
+ return false; // let focus go
+ }
+
+ grab_focus();
+ // TODO - add focus indicator frame or some focus indicator
+ return true;
+}
+
+bool GradientWithStops::on_key_press_event(GdkEventKey* key_event) {
+ bool consumed = false;
+ // currently all keyboard activity involves acting on focused stop handle; bail if nothing's selected
+ if (_focused_stop < 0) return consumed;
+
+ unsigned int key = 0;
+ auto modifier = static_cast<GdkModifierType>(key_event->state);
+ gdk_keymap_translate_keyboard_state(Gdk::Display::get_default()->get_keymap(),
+ key_event->hardware_keycode, modifier, 0, &key, nullptr, nullptr, nullptr);
+
+ auto delta = _stop_move_increment;
+ if (modifier & GDK_SHIFT_MASK) {
+ delta *= 10;
+ }
+
+ consumed = true;
+
+ switch (key) {
+ case GDK_KEY_Left:
+ case GDK_KEY_KP_Left:
+ move_stop(_focused_stop, -delta);
+ break;
+ case GDK_KEY_Right:
+ case GDK_KEY_KP_Right:
+ move_stop(_focused_stop, delta);
+ break;
+ case GDK_KEY_BackSpace:
+ case GDK_KEY_Delete:
+ _signal_delete_stop.emit(_focused_stop);
+ break;
+ default:
+ consumed = false;
+ break;
+ }
+
+ return consumed;
+}
+
+bool GradientWithStops::on_button_press_event(GdkEventButton* event) {
+ // single button press selects stop and can start dragging it
+ constexpr auto LEFT_BTN = 1;
+ if (event->button == LEFT_BTN && _gradient && event->type == GDK_BUTTON_PRESS) {
+ _focused_stop = -1;
+
+ if (!has_focus()) {
+ // grab focus, so we can show selection indicator and move selected stop with left/right keys
+ grab_focus();
+ }
+ update();
+
+ // find stop handle
+ auto index = find_stop_at(event->x, event->y);
+
+ if (index >= 0) {
+ _focused_stop = index;
+ // fire stop selection, whether stop can be moved or not
+ _signal_stop_selected.emit(index);
+
+ auto limits = get_stop_limits(index);
+
+ // check if clicked stop can be moved
+ if (limits.min_offset < limits.max_offset) {
+ // TODO: to facilitate selecting stops without accidentally moving them,
+ // delay dragging mode until mouse cursor moves certain distance...
+ _dragging = true;
+ _pointer_x = event->x;
+ _stop_offset = _stops.at(index).offset;
+
+ if (_cursor_dragging) {
+ gdk_window_set_cursor(event->window, _cursor_dragging->gobj());
+ }
+ }
+ }
+ }
+ else if (event->button == LEFT_BTN && _gradient && event->type == GDK_2BUTTON_PRESS) {
+ // double-click may insert a new stop
+ auto index = find_stop_at(event->x, event->y);
+ if (index < 0) {
+ auto layout = get_layout();
+ if (layout.width > 0 && event->x > layout.x && event->x < layout.x + layout.width) {
+ double position = (event->x - layout.x) / layout.width;
+ // request new stop
+ _signal_add_stop_at.emit(position);
+ }
+ }
+ }
+
+ return false;
+}
+
+bool GradientWithStops::on_button_release_event(GdkEventButton* event) {
+ GdkCursor* cursor = get_cursor(event->x, event->y);
+ gdk_window_set_cursor(event->window, cursor);
+
+ _dragging = false;
+ return false;
+}
+
+// move stop by a given amount (delta)
+void GradientWithStops::move_stop(int stop_index, double offset_shift) {
+ auto layout = get_layout();
+ if (layout.width > 0) {
+ auto limits = get_stop_limits(stop_index);
+ if (limits.min_offset < limits.max_offset) {
+ auto new_offset = CLAMP(limits.offset + offset_shift, limits.min_offset, limits.max_offset);
+ if (new_offset != limits.offset) {
+ _signal_stop_offset_changed.emit(stop_index, new_offset);
+ }
+ }
+ }
+}
+
+bool GradientWithStops::on_motion_notify_event(GdkEventMotion* event) {
+ if (_dragging && _gradient) {
+ // move stop to a new position (adjust offset)
+ auto dx = event->x - _pointer_x;
+ auto layout = get_layout();
+ if (layout.width > 0) {
+ auto delta = dx / layout.width;
+ auto limits = get_stop_limits(_focused_stop);
+ if (limits.min_offset < limits.max_offset) {
+ auto new_offset = CLAMP(_stop_offset + delta, limits.min_offset, limits.max_offset);
+ _signal_stop_offset_changed.emit(_focused_stop, new_offset);
+ }
+ }
+ }
+ else if (!_dragging && _gradient) {
+ GdkCursor* cursor = get_cursor(event->x, event->y);
+ gdk_window_set_cursor(event->window, cursor);
+ }
+
+ return false;
+}
+
+GdkCursor* GradientWithStops::get_cursor(double x, double y) const {
+ GdkCursor* cursor = nullptr;
+ if (_gradient) {
+ // check if mouse if over stop handle that we can adjust
+ auto index = find_stop_at(x, y);
+ if (index >= 0) {
+ auto limits = get_stop_limits(index);
+ if (limits.min_offset < limits.max_offset && _cursor_mouseover) {
+ cursor = _cursor_mouseover->gobj();
+ }
+ }
+ else {
+ if (_cursor_insert) {
+ cursor = _cursor_insert->gobj();
+ }
+ }
+ }
+ return cursor;
+}
+
+bool GradientWithStops::on_draw(const Cairo::RefPtr<Cairo::Context>& cr) {
+ auto allocation = get_allocation();
+ auto context = get_style_context();
+ const double scale = get_scale_factor();
+ const auto layout = get_layout();
+
+ if (layout.width <= 0) return true;
+
+ context->render_background(cr, 0, 0, allocation.get_width(), allocation.get_height());
+
+ // empty gradient checkboard or gradient itself
+ cr->rectangle(layout.x, layout.y, layout.width, GRADIENT_IMAGE_HEIGHT);
+ draw_gradient(cr, _gradient, layout.x, layout.width);
+
+ if (!_gradient) return true;
+
+ // draw stop handles
+
+ cr->begin_new_path();
+
+ Gdk::RGBA fg = context->get_color(get_state_flags());
+ Gdk::RGBA bg = _background_color;
+
+ // stop handle outlines and selection indicator use theme colors:
+ _template.set_style(".outer", "fill", rgba_to_css_color(fg));
+ _template.set_style(".inner", "stroke", rgba_to_css_color(bg));
+ _template.set_style(".hole", "fill", rgba_to_css_color(bg));
+
+ auto tip = _tip_template.render(scale);
+
+ for (size_t i = 0; i < _stops.size(); ++i) {
+ const auto& stop = _stops[i];
+
+ // stop handle shows stop color and opacity:
+ _template.set_style(".color", "fill", rgba_to_css_color(stop.color));
+ _template.set_style(".opacity", "opacity", double_to_css_value(stop.opacity));
+
+ // show/hide selection indicator
+ const auto is_selected = _focused_stop == static_cast<int>(i);
+ _template.set_style(".selected", "opacity", double_to_css_value(is_selected ? 1 : 0));
+
+ // render stop handle
+ auto pix = _template.render(scale);
+
+ if (!pix) {
+ g_warning("Rendering gradient stop failed.");
+ break;
+ }
+
+ auto pos = get_stop_position(i, layout);
+
+ // selected handle sports a 'tip' to make it easily noticeable
+ if (is_selected && tip) {
+ if (auto surface = Gdk::Cairo::create_surface_from_pixbuf(tip, 1)) {
+ cr->save();
+ // scale back to physical pixels
+ cr->scale(1 / scale, 1 / scale);
+ // paint tip bitmap
+ cr->set_source(surface, round(pos.tip * scale - tip->get_width() / 2), layout.y * scale);
+ cr->paint();
+ cr->restore();
+ }
+ }
+
+ // surface from pixbuf *without* scaling (scale = 1)
+ auto surface = Gdk::Cairo::create_surface_from_pixbuf(pix, 1);
+ if (!surface) continue;
+
+ // calc space available for stop marker
+ cr->save();
+ cr->rectangle(pos.left, layout.y, pos.right - pos.left, layout.height);
+ cr->clip();
+ // scale back to physical pixels
+ cr->scale(1 / scale, 1 / scale);
+ // paint bitmap
+ cr->set_source(surface, round(pos.tip * scale - pix->get_width() / 2), pos.top * scale);
+ cr->paint();
+ cr->restore();
+ cr->reset_clip();
+ }
+
+ return true;
+}
+
+// focused/selected stop indicator
+void GradientWithStops::set_focused_stop(int index) {
+ if (_focused_stop != index) {
+ _focused_stop = index;
+
+ if (has_focus()) {
+ update();
+ }
+ }
+}
+
+
+} // 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/gradient-with-stops.h b/src/ui/widget/gradient-with-stops.h
new file mode 100644
index 0000000..0276fa2
--- /dev/null
+++ b/src/ui/widget/gradient-with-stops.h
@@ -0,0 +1,117 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_GRADIENT_WITH_STOPS_H
+#define SEEN_GRADIENT_WITH_STOPS_H
+
+#include <gtkmm/widget.h>
+#include <gdkmm/color.h>
+#include "ui/svg-renderer.h"
+#include "helper/auto-connection.h"
+
+class SPGradient;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class GradientWithStops : public Gtk::DrawingArea {
+public:
+ GradientWithStops();
+
+ // gradient to draw or nullptr
+ void set_gradient(SPGradient* gradient);
+
+ // set selected stop handle (or pass -1 to deselect)
+ void set_focused_stop(int index);
+
+ // stop has been selected
+ sigc::signal<void (size_t)>& signal_stop_selected() {
+ return _signal_stop_selected;
+ }
+
+ // request to change stop's offset
+ sigc::signal<void (size_t, double)>& signal_stop_offset_changed() {
+ return _signal_stop_offset_changed;
+ }
+
+ sigc::signal<void (double)>& signal_add_stop_at() {
+ return _signal_add_stop_at;
+ }
+
+ sigc::signal<void (size_t)>& signal_delete_stop() {
+ return _signal_delete_stop;
+ }
+
+private:
+ 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;
+ bool on_draw(const Cairo::RefPtr<Cairo::Context>& cr) override;
+ void on_style_updated() 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;
+ bool on_focus_in_event(GdkEventFocus* event) override;
+ bool on_focus_out_event(GdkEventFocus* event) override;
+ bool on_focus(Gtk::DirectionType direction) override;
+ void size_request(GtkRequisition* requisition) const;
+ void modified();
+ // repaint widget
+ void update();
+ // index of gradient stop handle under (x, y) or -1
+ int find_stop_at(double x, double y) const;
+ // request stop move
+ void move_stop(int stop_index, double offset_shift);
+
+ // layout of gradient image/editor
+ struct layout_t {
+ double x, y, width, height;
+ };
+ layout_t get_layout() const;
+
+ // position of single gradient stop handle
+ struct stop_pos_t {
+ double left, tip, right, top, bottom;
+ };
+ stop_pos_t get_stop_position(size_t index, const layout_t& layout) const;
+
+ struct limits_t {
+ double min_offset, max_offset, offset;
+ };
+ limits_t get_stop_limits(int index) const;
+ GdkCursor* get_cursor(double x, double y) const;
+
+ SPGradient* _gradient = nullptr;
+ struct stop_t {
+ double offset;
+ SPColor color;
+ double opacity;
+ };
+ std::vector<stop_t> _stops;
+ // handle stop SVG template
+ svg_renderer _template;
+ // selected handle indicator
+ svg_renderer _tip_template;
+ auto_connection _release;
+ auto_connection _modified;
+ Gdk::RGBA _background_color;
+ sigc::signal<void (size_t)> _signal_stop_selected;
+ sigc::signal<void (size_t, double)> _signal_stop_offset_changed;
+ sigc::signal<void (double)> _signal_add_stop_at;
+ sigc::signal<void (size_t)> _signal_delete_stop;
+ bool _dragging = false;
+ // index of handle stop that user clicked; may be out of range
+ int _focused_stop = -1;
+ double _pointer_x = 0;
+ double _stop_offset = 0;
+ Glib::RefPtr<Gdk::Cursor> _cursor_mouseover;
+ Glib::RefPtr<Gdk::Cursor> _cursor_dragging;
+ Glib::RefPtr<Gdk::Cursor> _cursor_insert;
+ // TODO: customize this amount or read prefs
+ double _stop_move_increment = 0.01;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif
diff --git a/src/ui/widget/icon-combobox.h b/src/ui/widget/icon-combobox.h
new file mode 100644
index 0000000..e5fd3e6
--- /dev/null
+++ b/src/ui/widget/icon-combobox.h
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifndef ICON_COMBO_BOX_SEEN_
+#define ICON_COMBO_BOX_SEEN_
+
+#include <gtkmm/combobox.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/treemodelfilter.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class IconComboBox : public Gtk::ComboBox {
+public:
+ IconComboBox() {
+ _model = Gtk::ListStore::create(_columns);
+
+ pack_start(_renderer, false);
+ _renderer.set_property("stock_size", Gtk::ICON_SIZE_BUTTON);
+ _renderer.set_padding(2, 0);
+ add_attribute(_renderer, "icon_name", _columns.icon_name);
+
+ pack_start(_columns.label);
+
+ _filter = Gtk::TreeModelFilter::create(_model);
+ _filter->set_visible_column(_columns.is_visible);
+ set_model(_filter);
+ }
+
+ void add_row(const Glib::ustring& icon_name, const Glib::ustring& label, int id) {
+ Gtk::TreeModel::Row row = *_model->append();
+ row[_columns.id] = id;
+ row[_columns.icon_name] = icon_name;
+ row[_columns.label] = ' ' + label;
+ row[_columns.is_visible] = true;
+ }
+
+ void set_active_by_id(int id) {
+ for (auto i = _filter->children().begin(); i != _filter->children().end(); ++i) {
+ const int data = (*i)[_columns.id];
+ if (data == id) {
+ set_active(i);
+ break;
+ }
+ }
+ };
+
+ void set_row_visible(int id, bool visible = true) {
+ auto active_id = get_active_row_id();
+ for (const auto & i : _model->children()) {
+ const int data = i[_columns.id];
+ if (data == id) {
+ i[_columns.is_visible] = visible;
+ }
+ }
+ _filter->refilter();
+
+ // Reset the selected row if needed
+ if (active_id == id) {
+ for (const auto & i : _filter->children()) {
+ const int data = i[_columns.id];
+ set_active_by_id(data);
+ break;
+ }
+ }
+ }
+
+ int get_active_row_id() const {
+ if (auto it = get_active()) {
+ return (*it)[_columns.id];
+ }
+ return -1;
+ }
+
+private:
+ class Columns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ Columns() {
+ add(icon_name);
+ add(label);
+ add(id);
+ add(is_visible);
+ }
+
+ Gtk::TreeModelColumn<Glib::ustring> icon_name;
+ Gtk::TreeModelColumn<Glib::ustring> label;
+ Gtk::TreeModelColumn<int> id;
+ Gtk::TreeModelColumn<bool> is_visible;
+ };
+
+ Columns _columns;
+ Glib::RefPtr<Gtk::ListStore> _model;
+ Glib::RefPtr<Gtk::TreeModelFilter> _filter;
+ Gtk::CellRendererPixbuf _renderer;
+};
+
+}}}
+
+#endif
diff --git a/src/ui/widget/iconrenderer.cpp b/src/ui/widget/iconrenderer.cpp
new file mode 100644
index 0000000..f761316
--- /dev/null
+++ b/src/ui/widget/iconrenderer.cpp
@@ -0,0 +1,119 @@
+// 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 "ui/icon-loader.h"
+#include "ui/icon-names.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..01d6277
--- /dev/null
+++ b/src/ui/widget/iconrenderer.h
@@ -0,0 +1,84 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __UI_WIDGET_ICONRENDERER_H__
+#define __UI_WIDGET_ICONRENDERER_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_WIDGET_ICONRENDERER_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/image-properties.cpp b/src/ui/widget/image-properties.cpp
new file mode 100644
index 0000000..e661dde
--- /dev/null
+++ b/src/ui/widget/image-properties.cpp
@@ -0,0 +1,295 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * Image properties widget for "Fill and Stroke" dialog
+ *
+ * Copyright (C) 2023 Michael Kowalski
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "image-properties.h"
+#include <array>
+#include <glib/gi18n.h>
+#include <glibmm/convert.h>
+#include <glibmm/markup.h>
+#include <glibmm/ustring.h>
+#include <gtkmm/button.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/drawingarea.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/label.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/window.h>
+#include <sstream>
+#include <string>
+#include "display/cairo-utils.h"
+#include "document-undo.h"
+#include "enums.h"
+#include "helper/choose-file.h"
+#include "helper/save-image.h"
+#include "object/sp-image.h"
+#include "ui/builder-utils.h"
+#include "ui/icon-names.h"
+#include "ui/util.h"
+#include "util/format_size.h"
+#include "util/object-renderer.h"
+#include "xml/href-attribute-helper.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Cairo::RefPtr<Cairo::Surface> draw_preview(SPImage* image, double width, double height, int device_scale, uint32_t frame_color, uint32_t background) {
+ if (!image || !image->pixbuf) return Cairo::RefPtr<Cairo::Surface>();
+
+ object_renderer r;
+ object_renderer::options opt;
+ opt.frame(frame_color);
+ auto s = image->style;
+ // here for preview purposes using image's own opacity only
+ double alpha = s && s->opacity.set && !s->opacity.inherit ? SP_SCALE24_TO_FLOAT(s->opacity.value) : 1.0;
+ opt.image_opacity(alpha);
+ opt.checkerboard(background);
+ return r.render(*image, width, height, device_scale, opt);
+}
+
+void link_image(Gtk::Window* window, SPImage* image) {
+ if (!window || !image) return;
+
+ static std::string current_folder;
+ std::vector<Glib::ustring> mime_types = {
+ "image/png", "image/jpeg", "image/gif", "image/bmp", "image/tiff"
+ };
+ auto file = choose_file_open(_("Change Image"), window, mime_types, current_folder);
+ if (file.empty()) return;
+
+ // link image now
+ // todo: set/calc dpi?
+ // todo: set color profile?
+ try {
+ // convert filename to uri
+ auto uri = Glib::filename_to_uri(file);
+ setHrefAttribute(*image->getRepr(), uri);
+ }
+ catch (Glib::ConvertError const &e) {
+ g_warning("Error converting path to URI: %s", e.what().c_str());
+ setHrefAttribute(*image->getRepr(), file);
+ }
+ // SPImage modifies size when href changes; trigger it now before undo concludes
+ // TODO: this needs to be fixed in SPImage
+ image->document->_updateDocument(0);
+ DocumentUndo::done(image->document, _("Change image"), INKSCAPE_ICON("shape-image"));
+}
+
+void set_rendering_mode(SPImage* image, int index) {
+ static const std::array<const char*, 5> render = {
+ "auto", "optimizeSpeed", "optimizeQuality", "crisp-edges", "pixelated"
+ }; // SPImageRendering values
+
+ if (!image || index < 0 || index >= render.size()) return;
+
+ auto css = sp_repr_css_attr_new();
+ sp_repr_css_set_property(css, "image-rendering", render[index]);
+ if (auto image_node = image->getRepr()) {
+ sp_repr_css_change(image_node, css, "style");
+ DocumentUndo::done(image->document, _("Set image rendering option"), INKSCAPE_ICON("shape-image"));
+ }
+ sp_repr_css_attr_unref(css);
+}
+
+void set_aspect_ratio(SPImage* image, bool preserve_aspect_ratio) {
+ if (!image) return;
+ image->setAttribute("preserveAspectRatio", preserve_aspect_ratio ? "xMidYMid" : "none");
+ DocumentUndo::done(image->document, _("Preserve image aspect ratio"), INKSCAPE_ICON("shape-image"));
+}
+
+ImageProperties::ImageProperties() :
+ Gtk::Box(Gtk::ORIENTATION_HORIZONTAL),
+ _builder(create_builder("image-properties.glade")),
+ _preview(get_widget<Gtk::DrawingArea>(_builder, "preview")),
+ _aspect(get_widget<Gtk::RadioButton>(_builder, "preserve")),
+ _stretch(get_widget<Gtk::RadioButton>(_builder, "stretch")),
+ _rendering(get_widget<Gtk::ComboBoxText>(_builder, "rendering")),
+ _embed(get_widget<Gtk::Button>(_builder, "embed"))
+{
+
+ auto& main = get_widget<Gtk::Grid>(_builder, "main");
+ pack_start(main, true, true);
+
+ // arbitrarily selected max preview size for image content:
+ _preview_max_width = 120;
+ _preview_max_height = 90;
+
+ _preview.signal_draw().connect([=](const Cairo::RefPtr<Cairo::Context>& ctx){
+ if (_preview_image) {
+ ctx->set_source(_preview_image, 0, 0);
+ ctx->paint();
+ }
+ return true;
+ });
+
+ auto& change = get_widget<Gtk::Button>(_builder, "change-img");
+ change.signal_clicked().connect([=](){
+ if (_update.pending()) return;
+ auto window = dynamic_cast<Gtk::Window*>(get_toplevel());
+ link_image(window, _image);
+ });
+
+ auto& extract = get_widget<Gtk::Button>(_builder, "export");
+ extract.signal_clicked().connect([=](){
+ if (_update.pending()) return;
+ auto window = dynamic_cast<Gtk::Window*>(get_toplevel());
+ extract_image(window, _image);
+ });
+
+ _embed.signal_clicked().connect([=](){
+ if (_update.pending() || !_image) return;
+ // embed image in the current document
+ Inkscape::Pixbuf copy(*_image->pixbuf);
+ sp_embed_image(_image->getRepr(), &copy);
+ DocumentUndo::done(_image->document, _("Embed image"), INKSCAPE_ICON("selection-make-bitmap-copy"));
+ });
+
+ _rendering.signal_changed().connect([=](){
+ if (_update.pending()) return;
+ auto index = _rendering.get_active_row_number();
+ set_rendering_mode(_image, index);
+ });
+
+ _aspect.signal_toggled().connect([=](){
+ if (_update.pending()) return;
+ set_aspect_ratio(_image, _aspect.get_active());
+ });
+ _stretch.signal_toggled().connect([=](){
+ if (_update.pending()) return;
+ set_aspect_ratio(_image, !_stretch.get_active());
+ });
+}
+
+void ImageProperties::update(SPImage* image) {
+ if (!image && !_image) return; // nothing to do
+
+ _image = image;
+
+ auto scoped(_update.block());
+
+ auto small = [](const char* str) { return "<small>" + Glib::Markup::escape_text(str ? str : "") + "</small>"; };
+ auto& name = get_widget<Gtk::Label>(_builder, "name");
+ auto& info = get_widget<Gtk::Label>(_builder, "info");
+ auto& url = get_widget<Gtk::Entry>(_builder, "href");
+
+ if (!image) {
+ name.set_markup(small("-"));
+ info.set_markup(small("-"));
+ }
+ else {
+ Glib::ustring id(image->getId() ? image->getId() : "");
+ name.set_markup(small(id.empty() ? "-" : ("#" + id).c_str()));
+
+ bool embedded = false;
+ bool linked = false;
+ auto href = Inkscape::getHrefAttribute(*image->getRepr()).second;
+ if (href && std::strncmp(href, "data:", 5) == 0) {
+ embedded = true;
+ }
+ else if (href && *href) {
+ linked = true;
+ }
+
+ if (image->pixbuf) {
+ std::ostringstream ost;
+ if (!image->missing) {
+ auto times = "\u00d7"; // multiplication sign
+ // dimensions
+ ost << image->pixbuf->width() << times << image->pixbuf->height() << " px\n";
+
+ if (embedded) {
+ ost << _("Embedded");
+ ost << " (" << Util::format_file_size(std::strlen(href)) << ")\n";
+ }
+ if (linked) {
+ ost << _("Linked");
+ ost << '\n';
+ }
+ // color space
+ if (image->color_profile && *image->color_profile) {
+ ost << _("Color profile:") << ' ' << image->color_profile << '\n';
+ }
+ }
+ else {
+ ost << _("Missing image") << '\n';
+ }
+ info.set_markup(small(ost.str().c_str()));
+ }
+ else {
+ info.set_markup(small("-"));
+ }
+
+ url.set_text(linked ? href : "");
+ url.set_sensitive(linked);
+ _embed.set_sensitive(linked && image->pixbuf);
+
+ // aspect ratio
+ bool aspect_none = false;
+ if (image->aspect_set) {
+ aspect_none = image->aspect_align == SP_ASPECT_NONE;
+ }
+ if (aspect_none) {
+ _stretch.set_active();
+ }
+ else {
+ _aspect.set_active();
+ }
+
+ // rendering
+ _rendering.set_active(image->style ? image->style->image_rendering.value : -1);
+ }
+
+ int width = _preview_max_width;
+ int height = _preview_max_height;
+ if (image && image->pixbuf) {
+ double sw = image->pixbuf->width();
+ double sh = image->pixbuf->height();
+ double sx = sw / width;
+ double sy = sh / height;
+ auto scale = 1.0 / std::max(sx, sy);
+ width = std::max(1, int(sw * scale + 0.5));
+ height = std::max(1, int(sh * scale + 0.5));
+ }
+ // expand size to account for a frame around the image
+ int frame = 2;
+ width += frame;
+ height += frame;
+ _preview.set_size_request(width, height);
+ _preview.queue_draw();
+
+ // prepare preview
+ auto device_scale = get_scale_factor();
+ auto context = get_style_context();
+ Gdk::RGBA fg = context->get_color(Gtk::STATE_FLAG_NORMAL);
+ auto foreground = conv_gdk_color_to_rgba(fg, 0.30);
+ if (!_background_color) {
+ update_bg_color();
+ }
+ _preview_image = draw_preview(_image, width, height, device_scale, foreground, _background_color);
+}
+
+void ImageProperties::update_bg_color() {
+ if (auto wnd = dynamic_cast<Gtk::Window*>(get_toplevel())) {
+ auto sc = wnd->get_style_context();
+ auto color = get_background_color(sc);
+ _background_color = conv_gdk_color_to_rgba(color);
+ }
+ else {
+ _background_color = 0x808080ff;
+ }
+}
+
+void ImageProperties::on_style_updated() {
+ update_bg_color();
+ update(_image);
+}
+
+}}} // namespaces
diff --git a/src/ui/widget/image-properties.h b/src/ui/widget/image-properties.h
new file mode 100644
index 0000000..24678d2
--- /dev/null
+++ b/src/ui/widget/image-properties.h
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_IMAGE_PROPERTIES_H
+#define SEEN_IMAGE_PROPERTIES_H
+
+#include <gtkmm/box.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/button.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/drawingarea.h>
+#include <gtkmm/radiobutton.h>
+#include "helper/auto-connection.h"
+#include "object/sp-image.h"
+#include "ui/operation-blocker.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class ImageProperties : public Gtk::Box {
+public:
+ ImageProperties();
+ ~ImageProperties() override = default;
+
+ void update(SPImage* image);
+
+private:
+ void on_style_updated() override;
+ void update_bg_color();
+
+ Glib::RefPtr<Gtk::Builder> _builder;
+
+ Gtk::DrawingArea& _preview;
+ Gtk::RadioButton& _aspect;
+ Gtk::RadioButton& _stretch;
+ Gtk::ComboBoxText& _rendering;
+ Gtk::Button& _embed;
+ int _preview_max_height;
+ int _preview_max_width;
+ SPImage* _image = nullptr;
+ OperationBlocker _update;
+ Cairo::RefPtr<Cairo::Surface> _preview_image;
+ uint32_t _background_color = 0;
+};
+
+}}} // namespaces
+
+#endif // SEEN_IMAGE_PROPERTIES_H
diff --git a/src/ui/widget/imagetoggler.cpp b/src/ui/widget/imagetoggler.cpp
new file mode 100644
index 0000000..829c470
--- /dev/null
+++ b/src/ui/widget/imagetoggler.cpp
@@ -0,0 +1,148 @@
+// 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 <gtkmm/iconinfo.h>
+
+#include "ui/widget/imagetoggler.h"
+
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+ImageToggler::ImageToggler( char const* on, char const* off) :
+ Glib::ObjectBase(typeid(ImageToggler)),
+ Gtk::CellRenderer(),
+ _pixOnName(on),
+ _pixOffName(off),
+ _property_active(*this, "active", false),
+ _property_activatable(*this, "activatable", true),
+ _property_gossamer(*this, "gossamer", false),
+ _property_active_icon(*this, "active_icon", ""),
+ _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;
+ Gtk::IconSize::lookup(Gtk::ICON_SIZE_MENU, _size, _size);
+}
+
+void ImageToggler::get_preferred_height_vfunc(Gtk::Widget& widget, int& min_h, int& nat_h) const
+{
+ min_h = _size + 6;
+ nat_h = _size + 8;
+}
+
+void ImageToggler::get_preferred_width_vfunc(Gtk::Widget& widget, int& min_w, int& nat_w) const
+{
+ min_w = _size + 12;
+ nat_w = _size + 16;
+}
+
+void ImageToggler::set_active(bool active) {
+ _active = active;
+}
+
+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 )
+{
+ // Lazy/late pixbuf rendering to get access to scale factor from widget.
+ if(!_property_pixbuf_on.get_value()) {
+ int scale = widget.get_scale_factor();
+ _property_pixbuf_on = sp_get_icon_pixbuf(_pixOnName, _size * scale);
+ _property_pixbuf_off = sp_get_icon_pixbuf(_pixOffName, _size * scale);
+ }
+
+ std::string icon_name = _property_active_icon.get_value();
+ // if the icon isn't cached, render it to a pixbuf
+ if (!icon_name.empty() && !_icon_cache[icon_name]) {
+ int scale = widget.get_scale_factor();
+ _icon_cache[icon_name] = sp_get_icon_pixbuf(icon_name, _size * scale);
+ }
+
+ // Hide when not being used.
+ double alpha = 1.0;
+ bool visible = _property_activatable.get_value()
+ || _property_active.get_value()
+ || _active;
+ if (!visible) {
+ // XXX There is conflict about this value, some users want 0.2, others want 0.0
+ alpha = 0.0;
+ }
+ if (_property_gossamer.get_value()) {
+ alpha += 0.2;
+ }
+ if (alpha <= 0.0) {
+ return;
+ }
+
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf;
+ if (_property_active.get_value()) {
+ pixbuf = icon_name.empty() ? _property_pixbuf_on.get_value() : _icon_cache[icon_name];
+ } else {
+ pixbuf = _property_pixbuf_off.get_value();
+ }
+
+ cairo_surface_t *surface = gdk_cairo_surface_create_from_pixbuf(
+ pixbuf->gobj(), 0, widget.get_window()->gobj());
+ g_return_if_fail(surface);
+
+ // Center the icon in the cell area
+ int x = cell_area.get_x() + int((cell_area.get_width() - _size) * 0.5);
+ int y = cell_area.get_y() + int((cell_area.get_height() - _size) * 0.5);
+
+ cairo_set_source_surface(cr->cobj(), surface, x, y);
+ cr->set_operator(Cairo::OPERATOR_ATOP);
+ cr->rectangle(x, y, _size, _size);
+ if (alpha < 1.0) {
+ cr->clip();
+ cr->paint_with_alpha(alpha);
+ } else {
+ cr->fill();
+ }
+ cairo_surface_destroy(surface); // free!
+}
+
+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..579f6b9
--- /dev/null
+++ b/src/ui/widget/imagetoggler.h
@@ -0,0 +1,97 @@
+// 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::CellRenderer {
+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<bool> property_gossamer() { return _property_gossamer.get_proxy(); }
+ Glib::PropertyProxy<std::string> property_active_icon() { return _property_active_icon.get_proxy(); }
+ Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_on();
+ Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_off();
+
+ void set_active(bool active = true);
+
+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 _size;
+ Glib::ustring _pixOnName;
+ Glib::ustring _pixOffName;
+ bool _active = false;
+ Glib::Property<bool> _property_active;
+ Glib::Property<bool> _property_activatable;
+ Glib::Property<bool> _property_gossamer;
+ Glib::Property< Glib::RefPtr<Gdk::Pixbuf> > _property_pixbuf_on;
+ Glib::Property< Glib::RefPtr<Gdk::Pixbuf> > _property_pixbuf_off;
+ Glib::Property<std::string> _property_active_icon;
+ std::map<const std::string, Glib::RefPtr<Gdk::Pixbuf>> _icon_cache;
+
+ 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..3c35779
--- /dev/null
+++ b/src/ui/widget/ink-color-wheel.cpp
@@ -0,0 +1,1356 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * HSLuv color wheel widget, based on the web implementation at
+ * https://www.hsluv.org
+ *//*
+ * Authors:
+ * Tavmjong Bah
+ * Massinissa Derriche <massinissa.derriche@gmail.com>
+ *
+ * Copyright (C) 2018, 2021 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cstring>
+#include <algorithm>
+#include <2geom/angle.h>
+#include <2geom/coord.h>
+#include <2geom/point.h>
+#include <2geom/line.h>
+
+#include "ui/dialog/color-item.h"
+#include "hsluv.h"
+#include "ui/widget/ink-color-wheel.h"
+
+// Sizes in pixels
+static int const SIZE = 400;
+static int const OUTER_CIRCLE_RADIUS = 190;
+
+static double const MAX_HUE = 360.0;
+static double const MAX_SATURATION = 100.0;
+static double const MAX_LIGHTNESS = 100.0;
+static double const MIN_HUE = 0.0;
+static double const MIN_SATURATION = 0.0;
+static double const MIN_LIGHTNESS = 0.0;
+static double const OUTER_CIRCLE_DASH_SIZE = 10.0;
+static double const VERTEX_EPSILON = 0.01;
+
+struct ColorPoint
+{
+ ColorPoint();
+ ColorPoint(double x, double y, double r, double g, double b);
+ ColorPoint(double x, double y, guint color);
+
+ guint32 get_color();
+ void set_color(Hsluv::Triplet const &rgb)
+ {
+ r = rgb[0];
+ g = rgb[1];
+ b = rgb[2];
+ }
+
+ double x;
+ double y;
+ double r;
+ double g;
+ double b;
+};
+
+/** Represents a vertex of the Luv color polygon (intersection of bounding lines). */
+struct Intersection
+{
+ Intersection();
+ Intersection(int line_1, int line_2, Geom::Point &&intersection_point, Geom::Angle start_angle)
+ : line1{line_1}
+ , line2{line_2}
+ , point{intersection_point}
+ , polar_angle{point}
+ , relative_angle{polar_angle - start_angle}
+ {
+ }
+
+ int line1 = 0; ///< Index of the first of the intersecting lines.
+ int line2 = 0; ///< Index of the second of the intersecting lines.
+ Geom::Point point; ///< The geometric position of the intersection.
+ Geom::Angle polar_angle = 0.0; ///< Polar angle of the point (in radians).
+ /** Angle relative to the polar angle of the point at which the boundary of the polygon
+ * passes the origin at the minimum distance (i.e., where an expanding origin-centered
+ * circle inside the polygon starts touching an edge of the polygon.)
+ */
+ Geom::Angle relative_angle = 0.0;
+};
+
+static double lerp(double v0, double v1, double t0, double t1, double t);
+static ColorPoint lerp(ColorPoint const &v0, ColorPoint const &v1, double t0, double t1, double t);
+static guint32 hsv_to_rgb(double h, double s, double v);
+static double luminance(guint32 color);
+static Geom::Point to_pixel_coordinate(Geom::Point const &point, double scale, double resize);
+static Geom::Point from_pixel_coordinate(Geom::Point const &point, double scale, double resize);
+static std::vector<Geom::Point> to_pixel_coordinate(std::vector<Geom::Point> const &points, double scale,
+ double resize);
+static void draw_vertical_padding(ColorPoint p0, ColorPoint p1, int padding, bool pad_upwards, guint32 *buffer,
+ int height, int stride);
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+
+/* Base Color Wheel */
+ColorWheel::ColorWheel()
+ : _adjusting(false)
+{
+ 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::setRgb(double /*r*/, double /*g*/, double /*b*/, bool /*overrideHue*/)
+{}
+
+void ColorWheel::getRgb(double */*r*/, double */*g*/, double */*b*/) const
+{}
+
+void ColorWheel::getRgbV(double *rgb) const {}
+
+guint32 ColorWheel::getRgb() const { return 0; }
+
+void ColorWheel::setHue(double h)
+{
+ _values[0] = std::clamp(h, MIN_HUE, MAX_HUE);
+}
+
+void ColorWheel::setSaturation(double s)
+{
+ _values[1] = std::clamp(s, MIN_SATURATION, MAX_SATURATION);
+}
+
+void ColorWheel::setLightness(double l)
+{
+ _values[2] = std::clamp(l, MIN_LIGHTNESS, MAX_LIGHTNESS);
+}
+
+void ColorWheel::getValues(double *a, double *b, double *c) const
+{
+ if (a) *a = _values[0];
+ if (b) *b = _values[1];
+ if (c) *c = _values[2];
+}
+
+void ColorWheel::_set_from_xy(double const x, double const y)
+{}
+
+bool ColorWheel::on_key_release_event(GdkEventKey* key_event)
+{
+ 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_Up:
+ case GDK_KEY_KP_Up:
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Down:
+ case GDK_KEY_Left:
+ case GDK_KEY_KP_Left:
+ case GDK_KEY_Right:
+ case GDK_KEY_KP_Right:
+ _adjusting = false;
+ return true;
+ }
+
+ return false;
+}
+
+sigc::signal<void ()> ColorWheel::signal_color_changed()
+{
+ return _signal_color_changed;
+}
+
+/* HSL Color Wheel */
+void ColorWheelHSL::setRgb(double r, double g, double b, bool overrideHue)
+{
+ double min = std::min({r, g, b});
+ double max = std::max({r, g, b});
+
+ _values[2] = max;
+
+ if (min == max) {
+ if (overrideHue) {
+ _values[0] = 0.0;
+ }
+ } else {
+ if (max == r) {
+ _values[0] = ((g - b) / (max - min) ) / 6.0;
+ } else if (max == g) {
+ _values[0] = ((b - r) / (max - min) + 2) / 6.0;
+ } else {
+ _values[0] = ((r - g) / (max - min) + 4) / 6.0;
+ }
+
+ if (_values[0] < 0.0) {
+ _values[0] += 1.0;
+ }
+ }
+
+ if (max == 0) {
+ _values[1] = 0;
+ } else {
+ _values[1] = (max - min) / max;
+ }
+}
+
+void ColorWheelHSL::getRgb(double *r, double *g, double *b) const
+{
+ guint32 color = getRgb();
+ *r = ((color & 0x00ff0000) >> 16) / 255.0;
+ *g = ((color & 0x0000ff00) >> 8) / 255.0;
+ *b = ((color & 0x000000ff) ) / 255.0;
+}
+
+void ColorWheelHSL::getRgbV(double *rgb) const
+{
+ guint32 color = getRgb();
+ rgb[0] = ((color & 0x00ff0000) >> 16) / 255.0;
+ rgb[1] = ((color & 0x0000ff00) >> 8) / 255.0;
+ rgb[2] = ((color & 0x000000ff) ) / 255.0;
+}
+
+guint32 ColorWheelHSL::getRgb() const
+{
+ return hsv_to_rgb(_values[0], _values[1], _values[2]);
+}
+
+void ColorWheelHSL::getHsl(double *h, double *s, double *l) const
+{
+ getValues(h, s, l);
+}
+
+bool ColorWheelHSL::on_draw(::Cairo::RefPtr<::Cairo::Context> const &cr)
+{
+ Gtk::Allocation allocation = get_allocation();
+ int const width = allocation.get_width();
+ int const height = allocation.get_height();
+
+ int const cx = width/2;
+ int const cy = height/2;
+
+ int const stride = Cairo::ImageSurface::format_stride_for_width(Cairo::FORMAT_RGB24, width);
+
+ 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+2) * (r_max+2); // Must expand a bit to avoid edge effects.
+ double r2_min = (r_min-2) * (r_min-2); // 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(_values[0], 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(_values[0] * M_PI * 2.0) * r_max+1,
+ cy - sin(_values[0] * 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(_values[0], 1.0, 1.0);
+ guint32 color1 = hsv_to_rgb(_values[0], 1.0, 0.0);
+ guint32 color2 = hsv_to_rgb(_values[0], 0.0, 1.0);
+
+ ColorPoint p0 (x0, y0, color0);
+ ColorPoint p1 (x1, y1, color1);
+ ColorPoint 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.
+ ColorPoint side0;
+ double y_inter = std::clamp(static_cast<double>(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);
+ }
+ ColorPoint 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
+ ColorPoint 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) * _values[2] + (x0-x2) * _values[1] * _values[2];
+ double my = y1 + (y2-y1) * _values[2] + (y0-y2) * _values[1] * _values[2];
+
+ double a = 0.0;
+ guint32 color_at_marker = getRgb();
+ 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;
+}
+
+bool ColorWheelHSL::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;
+}
+
+void ColorWheelHSL::_set_from_xy(double const x, double const y)
+{
+ Gtk::Allocation allocation = get_allocation();
+ int const width = allocation.get_width();
+ int const height = allocation.get_height();
+
+ double const cx = width/2.0;
+ double const cy = height/2.0;
+ double const 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 = _values[0] * 2 * M_PI;
+ double sin = std::sin(angle);
+ double cos = std::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 = std::clamp(xt, 0.0, 1.0);
+
+ double dy = (1-xt) * std::cos(M_PI / 6.0);
+ double yt = lerp(0.0, 1.0, -dy, dy, yp);
+ yt = std::clamp(yt, 0.0, 1.0);
+
+ ColorPoint c0(0, 0, yt, yt, yt); // Grey point along base.
+ ColorPoint c1(0, 0, hsv_to_rgb(_values[0], 1, 1)); // Hue point at apex
+ ColorPoint c = lerp(c0, c1, 0, 1, xt);
+
+ setRgb(c.r, c.g, c.b, false); // Don't override previous hue.
+}
+
+bool ColorWheelHSL::_is_in_ring(double x, double y)
+{
+ Gtk::Allocation allocation = get_allocation();
+ int const width = allocation.get_width();
+ int const height = allocation.get_height();
+
+ int const cx = width/2;
+ int const 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 ColorWheelHSL::_is_in_triangle(double x, 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);
+}
+
+void ColorWheelHSL::_update_triangle_color(double x, double y)
+{
+ _set_from_xy(x, y);
+ _signal_color_changed.emit();
+ queue_draw();
+}
+
+void ColorWheelHSL::_triangle_corners(double &x0, double &y0, double &x1, double &y1,
+ double &x2, double &y2)
+{
+ Gtk::Allocation allocation = get_allocation();
+ int const width = allocation.get_width();
+ int const height = allocation.get_height();
+
+ int const cx = width / 2;
+ int const 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 = _values[0] * 2.0 * M_PI;
+
+ x0 = cx + std::cos(angle) * r_min;
+ y0 = cy - std::sin(angle) * r_min;
+ x1 = cx + std::cos(angle + 2.0 * M_PI / 3.0) * r_min;
+ y1 = cy - std::sin(angle + 2.0 * M_PI / 3.0) * r_min;
+ x2 = cx + std::cos(angle + 4.0 * M_PI / 3.0) * r_min;
+ y2 = cy - std::sin(angle + 4.0 * M_PI / 3.0) * r_min;
+}
+
+void ColorWheelHSL::_update_ring_color(double x, double y)
+{
+ Gtk::Allocation allocation = get_allocation();
+ int const width = allocation.get_width();
+ int const height = allocation.get_height();
+
+ double cx = width / 2.0;
+ double cy = height / 2.0;
+ double angle = -atan2(y - cy, x - cx);
+
+ if (angle < 0) {
+ angle += 2.0 * M_PI;
+ }
+ _values[0] = angle / (2.0 * M_PI);
+
+ queue_draw();
+ _signal_color_changed.emit();
+}
+
+bool ColorWheelHSL::on_button_press_event(GdkEventButton* event)
+{
+ // Seat is automatically grabbed.
+ double x = event->x;
+ double y = event->y;
+
+ if (_is_in_ring(x, y) ) {
+ _adjusting = true;
+ _mode = DragMode::HUE;
+ grab_focus();
+ _focus_on_ring = true;
+ _update_ring_color(x, y);
+ return true;
+ } else if (_is_in_triangle(x, y)) {
+ _adjusting = true;
+ _mode = DragMode::SATURATION_VALUE;
+ grab_focus();
+ _focus_on_ring = false;
+ _update_triangle_color(x, y);
+ return true;
+ }
+
+ return false;
+}
+
+bool ColorWheelHSL::on_button_release_event(GdkEventButton */*event*/)
+{
+ _mode = DragMode::NONE;
+
+ _adjusting = false;
+ return true;
+}
+
+bool ColorWheelHSL::on_motion_notify_event(GdkEventMotion* event)
+{
+ if (!_adjusting) { return false; }
+
+ double x = event->x;
+ double y = event->y;
+
+ if (_mode == DragMode::HUE) {
+ _update_ring_color(x, y);
+ return true;
+ } else if (_mode == DragMode::SATURATION_VALUE) {
+ _update_triangle_color(x, y);
+ return true;
+ } else {
+ return false;
+ }
+}
+
+bool ColorWheelHSL::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) * _values[2] + (x0 - x2) * _values[1] * _values[2];
+ double my = y1 + (y2 - y1) * _values[2] + (y0 - y2) * _values[1] * _values[2];
+
+ double const delta_hue = 2.0 / MAX_HUE;
+
+ switch (key) {
+ case GDK_KEY_Up:
+ case GDK_KEY_KP_Up:
+ if (_focus_on_ring) {
+ _values[0] += 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) {
+ _values[0] -= 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) {
+ _values[0] += 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) {
+ _values[0] -= delta_hue;
+ } else {
+ mx += 1.0;
+ _set_from_xy(mx, my);
+ }
+ consumed = true;
+ break;
+ }
+
+ if (consumed) {
+ if (_values[0] >= 1.0) {
+ _values[0] -= 1.0;
+ } else if (_values[0] < 0.0) {
+ _values[0] += 1.0;
+ }
+
+ _signal_color_changed.emit();
+ queue_draw();
+ }
+
+ return consumed;
+}
+
+/* HSLuv Color Wheel */
+ColorWheelHSLuv::ColorWheelHSLuv()
+{
+ _picker_geometry = std::make_unique<Hsluv::PickerGeometry>();
+ setHsluv(MIN_HUE, MAX_SATURATION, 0.5 * MAX_LIGHTNESS);
+}
+
+void ColorWheelHSLuv::setRgb(double r, double g, double b, bool /*overrideHue*/)
+{
+ auto hsl = Hsluv::rgb_to_hsluv(r, g, b);
+ setHue(hsl[0]);
+ setSaturation(hsl[1]);
+ setLightness(hsl[2]);
+}
+
+void ColorWheelHSLuv::getRgb(double *r, double *g, double *b) const
+{
+ auto rgb = Hsluv::hsluv_to_rgb(_values[0], _values[1], _values[2]);
+ *r = rgb[0];
+ *g = rgb[1];
+ *b = rgb[2];
+}
+
+void ColorWheelHSLuv::getRgbV(double *rgb) const
+{
+ auto converted = Hsluv::hsluv_to_rgb(_values[0], _values[1], _values[2]);
+ for (size_t i : {0, 1, 2}) {
+ rgb[i] = converted[i];
+ }
+}
+
+guint32 ColorWheelHSLuv::getRgb() const
+{
+ auto rgb = Hsluv::hsluv_to_rgb(_values[0], _values[1], _values[2]);
+ return (
+ (static_cast<guint32>(rgb[0] * 255.0) << 16) |
+ (static_cast<guint32>(rgb[1] * 255.0) << 8) |
+ (static_cast<guint32>(rgb[2] * 255.0) )
+ );
+}
+
+void ColorWheelHSLuv::setHsluv(double h, double s, double l)
+{
+ setHue(h);
+ setSaturation(s);
+ setLightness(l);
+}
+
+/**
+ * Update the PickerGeometry structure owned by the instance.
+ */
+void ColorWheelHSLuv::updateGeometry()
+{
+ // Separate from the extremes to avoid overlapping intersections
+ double lightness = std::clamp(_values[2] + 0.01, 0.1, 99.9);
+
+ // Find the lines bounding the gamut polygon
+ auto const lines = Hsluv::get_bounds(lightness);
+
+ // Find the line closest to origin
+ Geom::Line const *closest_line = nullptr;
+ double closest_distance = -1;
+
+ for (auto const &line : lines) {
+ double d = Geom::distance(Geom::Point(0, 0), line);
+ if (closest_distance < 0 || d < closest_distance) {
+ closest_distance = d;
+ closest_line = &line;
+ }
+ }
+
+ g_assert(closest_line);
+ auto const nearest_time = closest_line->nearestTime(Geom::Point(0, 0));
+ Geom::Angle start_angle{closest_line->pointAt(nearest_time)};
+
+ std::vector<Intersection> intersections;
+ unsigned const num_lines = 6;
+ unsigned const max_intersections = num_lines * (num_lines - 1) / 2;
+ intersections.reserve(max_intersections);
+
+ for (int i = 0; i < num_lines - 1; i++) {
+ for (int j = i + 1; j < num_lines; j++) {
+ auto xings = lines[i].intersect(lines[j]);
+ if (xings.empty()) {
+ continue;
+ }
+ intersections.emplace_back(i, j, xings.front().point(), start_angle);
+ }
+ }
+
+ std::sort(intersections.begin(), intersections.end(), [](Intersection const &lhs, Intersection const &rhs) {
+ return lhs.relative_angle.radians0() >= rhs.relative_angle.radians0();
+ });
+
+ // Find the relevant vertices of the polygon, in the counter-clockwise order.
+ std::vector<Geom::Point> ordered_vertices;
+ double circumradius = 0.0;
+ unsigned current_index = closest_line - &lines[0];
+
+ for (auto const &intersection : intersections) {
+ if (intersection.line1 == current_index) {
+ current_index = intersection.line2;
+ } else if (intersection.line2 == current_index) {
+ current_index = intersection.line1;
+ } else {
+ continue;
+ }
+ ordered_vertices.emplace_back(intersection.point);
+ circumradius = std::max(circumradius, intersection.point.length());
+ }
+
+ _picker_geometry->vertices = std::move(ordered_vertices);
+ _picker_geometry->outer_circle_radius = circumradius;
+ _picker_geometry->inner_circle_radius = closest_distance;
+}
+
+void ColorWheelHSLuv::setLightness(double l)
+{
+ _values[2] = std::clamp(l, MIN_LIGHTNESS, MAX_LIGHTNESS);
+
+ // Update polygon
+ updateGeometry();
+ _scale = OUTER_CIRCLE_RADIUS / _picker_geometry->outer_circle_radius;
+ _updatePolygon();
+
+ queue_draw();
+}
+
+void ColorWheelHSLuv::getHsluv(double *h, double *s, double *l) const
+{
+ getValues(h, s, l);
+}
+
+Geom::IntPoint ColorWheelHSLuv::_getMargin(Gtk::Allocation const &allocation)
+{
+ int const width = allocation.get_width();
+ int const height = allocation.get_height();
+
+ return {std::max(0, (width - height) / 2),
+ std::max(0, (height - width) / 2)};
+}
+
+/// Detect whether we're at the top or bottom vertex of the color space.
+bool ColorWheelHSLuv::_vertex() const
+{
+ return _values[2] < VERTEX_EPSILON || _values[2] > MAX_LIGHTNESS - VERTEX_EPSILON;
+}
+
+bool ColorWheelHSLuv::on_draw(::Cairo::RefPtr<::Cairo::Context> const &cr)
+{
+ Gtk::Allocation allocation = get_allocation();
+ auto dimensions = _getAllocationDimensions(allocation);
+ auto center = (0.5 * (Geom::Point)dimensions).floor();
+
+ auto size = _getAllocationSize(allocation);
+ double const resize = size / static_cast<double>(SIZE);
+
+ auto const margin = _getMargin(allocation);
+ auto polygon_vertices_px = to_pixel_coordinate(_picker_geometry->vertices, _scale, resize);
+ for (auto &point : polygon_vertices_px) {
+ point += margin;
+ }
+
+ bool const is_vertex = _vertex();
+ cr->set_antialias(Cairo::ANTIALIAS_SUBPIXEL);
+
+ if (size > _square_size) {
+ if (_cache_width != dimensions[Geom::X] || _cache_height != dimensions[Geom::Y]) {
+ _updatePolygon();
+ }
+ if (!is_vertex) {
+ // Paint with surface, clipping to polygon
+ cr->save();
+ cr->set_source(_surface_polygon, 0, 0);
+ auto it = polygon_vertices_px.begin();
+ cr->move_to((*it)[Geom::X], (*it)[Geom::Y]);
+ for (++it; it != polygon_vertices_px.end(); ++it) {
+ cr->line_to((*it)[Geom::X], (*it)[Geom::Y]);
+ }
+ cr->close_path();
+ cr->fill();
+ cr->restore();
+ }
+ }
+
+ // Draw foreground
+
+ // Outer circle
+ std::vector<double> dashes{OUTER_CIRCLE_DASH_SIZE};
+ cr->set_line_width(1);
+ // White dashes
+ cr->set_source_rgb(1.0, 1.0, 1.0);
+ cr->set_dash(dashes, 0.0);
+ cr->begin_new_path();
+ cr->arc(center[Geom::X], center[Geom::Y], _scale * resize * _picker_geometry->outer_circle_radius, 0, 2 * M_PI);
+ cr->stroke();
+ // Black dashes
+ cr->set_source_rgb(0.0, 0.0, 0.0);
+ cr->set_dash(dashes, OUTER_CIRCLE_DASH_SIZE);
+ cr->begin_new_path();
+ cr->arc(center[Geom::X], center[Geom::Y], _scale * resize * _picker_geometry->outer_circle_radius, 0, 2 * M_PI);
+ cr->stroke();
+ cr->unset_dash();
+
+ // Contrast
+ auto [gray, alpha] = Hsluv::get_contrasting_color(Hsluv::perceptual_lightness(_values[2]));
+ cr->set_source_rgba(gray, gray, gray, alpha);
+
+ // Draw inscribed circle
+ double const inner_stroke_width = 2.0;
+ double inner_radius = is_vertex ? 0.01 : _picker_geometry->inner_circle_radius;
+ cr->set_line_width(inner_stroke_width);
+ cr->begin_new_path();
+ cr->arc(center[Geom::X], center[Geom::Y], _scale * resize * inner_radius, 0, 2 * M_PI);
+ cr->stroke();
+
+ // Center
+ cr->begin_new_path();
+ cr->arc(center[Geom::X], center[Geom::Y], 2, 0, 2 * M_PI);
+ cr->fill();
+
+ // Draw marker
+ auto luv = Hsluv::hsluv_to_luv(_values);
+ auto mp = to_pixel_coordinate({luv[1], luv[2]}, _scale, resize) + margin;
+
+ cr->set_line_width(inner_stroke_width);
+ cr->begin_new_path();
+ cr->arc(mp[Geom::X], mp[Geom::Y], 2 * inner_stroke_width, 0, 2 * M_PI);
+ cr->stroke();
+
+ // Focus
+ if (has_focus()) {
+ Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context();
+ style_context->render_focus(cr, mp[Geom::X] - 4, mp[Geom::Y] - 4, 8, 8);
+
+ cr->set_line_width(0.25 * inner_stroke_width);
+ cr->set_source_rgb(1 - gray, 1 - gray, 1 - gray);
+ cr->begin_new_path();
+ cr->arc(mp[Geom::X], mp[Geom::Y], 7, 0, 2 * M_PI);
+ cr->stroke();
+ }
+
+ return true;
+}
+
+void ColorWheelHSLuv::_set_from_xy(double const x, double const y)
+{
+ Gtk::Allocation allocation = get_allocation();
+ int const width = allocation.get_width();
+ int const height = allocation.get_height();
+
+ double const resize = std::min(width, height) / static_cast<double>(SIZE);
+ auto const p = from_pixel_coordinate(Geom::Point(x, y) - _getMargin(allocation), _scale, resize);
+
+ auto hsluv = Hsluv::luv_to_hsluv(_values[2], p[Geom::X], p[Geom::Y]);
+ setHue(hsluv[0]);
+ setSaturation(hsluv[1]);
+
+ _signal_color_changed.emit();
+ queue_draw();
+}
+
+void ColorWheelHSLuv::_updatePolygon()
+{
+ Gtk::Allocation allocation = get_allocation();
+ auto allocation_size = _getAllocationDimensions(allocation);
+ int const size = std::min(allocation_size[Geom::X], allocation_size[Geom::Y]);
+
+ // Update square size
+ _square_size = std::max(1, static_cast<int>(size / 50));
+ if (size < _square_size) {
+ return;
+ }
+
+ _cache_width = allocation_size[Geom::X];
+ _cache_height = allocation_size[Geom::Y];
+
+ double const resize = size / static_cast<double>(SIZE);
+
+ auto const margin = _getMargin(allocation);
+ auto polygon_vertices_px = to_pixel_coordinate(_picker_geometry->vertices, _scale, resize);
+
+ // Find the bounding rectangle containing all points (adjusted by the margin).
+ Geom::Rect bounding_rect;
+ for (auto const &point : polygon_vertices_px) {
+ bounding_rect.expandTo(point + margin);
+ }
+ bounding_rect *= Geom::Scale(1.0 / _square_size);
+
+ // Round to integer pixel coords
+ auto const bounding_max = bounding_rect.max().ceil();
+ auto const bounding_min = bounding_rect.min().floor();
+
+ int const stride = Cairo::ImageSurface::format_stride_for_width(Cairo::FORMAT_RGB24, _cache_width);
+
+ _buffer_polygon.resize(_cache_height * stride / 4);
+ std::vector<guint32> buffer_line;
+ buffer_line.resize(stride / 4);
+
+ ColorPoint clr;
+ auto const square_center = Geom::IntPoint(_square_size / 2, _square_size / 2);
+
+ // Set the color of each pixel/square
+ for (int y = bounding_min[Geom::Y]; y < bounding_max[Geom::Y]; y++) {
+ for (int x = bounding_min[Geom::X]; x < bounding_max[Geom::X]; x++) {
+ auto pos = Geom::IntPoint(x * _square_size, y * _square_size);
+ auto point = from_pixel_coordinate(pos + square_center - margin, _scale, resize);
+
+ auto rgb = Hsluv::luv_to_rgb(_values[2], point[Geom::X], point[Geom::Y]); // safe with _values[2] == 0
+ clr.set_color(rgb);
+
+ guint32 *p = buffer_line.data() + (x * _square_size);
+ for (int i = 0; i < _square_size; i++) {
+ p[i] = clr.get_color();
+ }
+ }
+
+ // Copy the line buffer to the surface buffer
+ int const scaled_y = y * _square_size;
+ for (int i = 0; i < _square_size; i++) {
+ guint32 *t = _buffer_polygon.data() + (scaled_y + i) * (stride / 4);
+ std::memcpy(t, buffer_line.data(), stride);
+ }
+ }
+
+ _surface_polygon = ::Cairo::ImageSurface::create(reinterpret_cast<unsigned char *>(_buffer_polygon.data()),
+ Cairo::FORMAT_RGB24, _cache_width, _cache_height, stride);
+}
+
+bool ColorWheelHSLuv::on_button_press_event(GdkEventButton* event)
+{
+ auto event_pt = Geom::Point(event->x, event->y);
+ Gtk::Allocation allocation = get_allocation();
+ int const size = _getAllocationSize(allocation);
+ auto const region = Geom::IntRect::from_xywh(_getMargin(allocation), {size, size});
+
+ if (region.contains(event_pt.round())) {
+ _adjusting = true;
+ grab_focus();
+ _setFromPoint(event_pt);
+ return true;
+ }
+
+ return false;
+}
+
+bool ColorWheelHSLuv::on_button_release_event(GdkEventButton */*event*/)
+{
+ _adjusting = false;
+ return true;
+}
+
+bool ColorWheelHSLuv::on_motion_notify_event(GdkEventMotion* event)
+{
+ if (!_adjusting) {
+ return false;
+ }
+ _set_from_xy(event->x, event->y);
+ return true;
+}
+
+bool ColorWheelHSLuv::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);
+
+ // Get current point
+ auto luv = Hsluv::hsluv_to_luv(_values);
+
+ double const marker_move = 1.0 / _scale;
+
+ switch (key) {
+ case GDK_KEY_Up:
+ case GDK_KEY_KP_Up:
+ luv[2] += marker_move;
+ consumed = true;
+ break;
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Down:
+ luv[2] -= marker_move;
+ consumed = true;
+ break;
+ case GDK_KEY_Left:
+ case GDK_KEY_KP_Left:
+ luv[1] -= marker_move;
+ consumed = true;
+ break;
+ case GDK_KEY_Right:
+ case GDK_KEY_KP_Right:
+ luv[1] += marker_move;
+ consumed = true;
+ break;
+ }
+
+ if (consumed) {
+ auto hsluv = Hsluv::luv_to_hsluv(luv[0], luv[1], luv[1]);
+ setHue(hsluv[0]);
+ setSaturation(hsluv[1]);
+
+ _adjusting = true;
+ _signal_color_changed.emit();
+ queue_draw();
+ }
+
+ return consumed;
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+/* ColorPoint */
+ColorPoint::ColorPoint()
+ : x(0), y(0), r(0), g(0), b(0)
+{}
+
+ColorPoint::ColorPoint(double x, double y, double r, double g, double b)
+ : x(x), y(y), r(r), g(g), b(b)
+{}
+
+ColorPoint::ColorPoint(double x, double y, guint color)
+ : x(x)
+ , y(y)
+ , r(((color & 0xff0000) >> 16) / 255.0)
+ , g(((color & 0x00ff00) >> 8) / 255.0)
+ , b(((color & 0x0000ff) ) / 255.0)
+{}
+
+guint32 ColorPoint::get_color()
+{
+ return (static_cast<int>(r * 255) << 16 |
+ static_cast<int>(g * 255) << 8 |
+ static_cast<int>(b * 255)
+ );
+};
+
+static double lerp(double v0, double v1, double t0, double t1, double t)
+{
+ double const s = (t0 != t1) ? (t - t0) / (t1 - t0) : 0.0;
+ return Geom::lerp(s, v0, v1);
+}
+
+static ColorPoint lerp(ColorPoint const &v0, ColorPoint const &v1, double t0, double t1,
+ 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 ColorPoint(x, y, r, g, b);
+}
+
+/**
+ * @param h Hue. Between 0 and 1.
+ * @param s Saturation. Between 0 and 1.
+ * @param v Value. Between 0 and 1.
+ */
+static guint32 hsv_to_rgb(double h, double s, double v)
+{
+ h = std::clamp(h, 0.0, 1.0);
+ s = std::clamp(s, 0.0, 1.0);
+ v = std::clamp(v, 0.0, 1.0);
+
+ double r = v;
+ double g = v;
+ double b = v;
+
+ if (s != 0.0) {
+ 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 (static_cast<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 = (static_cast<int>(floor(r * 255 + 0.5)) << 16) |
+ (static_cast<int>(floor(g * 255 + 0.5)) << 8) |
+ (static_cast<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);
+}
+
+/**
+ * Convert a point of the gamut color polygon (Luv) to pixel coordinates.
+ *
+ * @param point The point in Luv coordinates.
+ * @param scale Zoom amount to fit polygon to outer circle.
+ * @param resize Zoom amount to fit wheel in widget.
+ */
+static Geom::Point to_pixel_coordinate(Geom::Point const &point, double scale, double resize)
+{
+ return Geom::Point(
+ point[Geom::X] * scale * resize + (SIZE * resize / 2.0),
+ (SIZE * resize / 2.0) - point[Geom::Y] * scale * resize
+ );
+}
+
+/**
+ * Convert a point in pixels on the widget to Luv coordinates.
+ *
+ * @param point The point in pixel coordinates.
+ * @param scale Zoom amount to fit polygon to outer circle.
+ * @param resize Zoom amount to fit wheel in widget.
+ */
+static Geom::Point from_pixel_coordinate(Geom::Point const &point, double scale, double resize)
+{
+ return Geom::Point(
+ (point[Geom::X] - (SIZE * resize / 2.0)) / (scale * resize),
+ ((SIZE * resize / 2.0) - point[Geom::Y]) / (scale * resize)
+ );
+}
+
+/**
+ * @overload
+ * @param point A vector of points in Luv coordinates.
+ * @param scale Zoom amount to fit polygon to outer circle.
+ * @param resize Zoom amount to fit wheel in widget.
+ */
+static std::vector<Geom::Point> to_pixel_coordinate(std::vector<Geom::Point> const &points,
+ double scale, double resize)
+{
+ std::vector<Geom::Point> result;
+
+ for (auto const &p : points) {
+ result.emplace_back(to_pixel_coordinate(p, scale, resize));
+ }
+
+ return result;
+}
+
+/**
+ * Paints padding for an edge of the triangle,
+ * using the (vertically) closest point.
+ *
+ * @param p0 A corner of the triangle. Not the same corner as p1
+ * @param p1 A corner of the triangle. Not the same corner as p0
+ * @param padding The height of the padding
+ * @param pad_upwards True if padding is above the line
+ * @param buffer Array that the triangle is painted to
+ * @param height Height of buffer
+ * @param stride Stride of buffer
+*/
+
+void draw_vertical_padding(ColorPoint p0, ColorPoint p1, int padding, bool pad_upwards,
+ guint32 *buffer, int height, int stride)
+{
+ // skip if horizontal padding is more accurate, e.g. if the edge is vertical
+ 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);
+
+ // go through every point on the line
+ for (int y = min_y; y <= max_y; ++y) {
+ double start_x = lerp(p0, p1, p0.y, p1.y, std::clamp(static_cast<double>(y), min_y,
+ max_y)).x;
+ double end_x = lerp(p0, p1, p0.y, p1.y, std::clamp(static_cast<double>(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) {
+ // get the color at this point on the line
+ ColorPoint point = lerp(p0, p1, p0.x, p1.x, std::clamp(static_cast<double>(x),
+ min_x, max_x));
+ // paint the padding vertically above or below this point
+ 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;
+ }
+ }
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace .0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ 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..5fbd05c
--- /dev/null
+++ b/src/ui/widget/ink-color-wheel.h
@@ -0,0 +1,167 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * @file
+ * HSLuv color wheel widget, based on the web implementation at
+ * https://www.hsluv.org
+ *
+ * Authors:
+ * Tavmjong Bah
+ * Massinissa Derriche <massinissa.derriche@gmail.com>
+ *
+ * Copyright (C) 2018, 2021 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INK_COLORWHEEL_H
+#define INK_COLORWHEEL_H
+
+#include <gtkmm.h>
+#include <2geom/point.h>
+#include <2geom/line.h>
+
+#include "hsluv.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * @class ColorWheel
+ */
+class ColorWheel : public Gtk::DrawingArea
+{
+public:
+ ColorWheel();
+
+ virtual void setRgb(double r, double g, double b, bool overrideHue = true);
+ virtual void getRgb(double *r, double *g, double *b) const;
+ virtual void getRgbV(double *rgb) const;
+ virtual guint32 getRgb() const;
+
+ void setHue(double h);
+ void setSaturation(double s);
+ virtual void setLightness(double l);
+ void getValues(double *a, double *b, double *c) const;
+
+ bool isAdjusting() const { return _adjusting; }
+
+protected:
+ virtual void _set_from_xy(double const x, double const y);
+
+ double _values[3];
+ bool _adjusting;
+
+private:
+ // Callbacks
+ bool on_key_release_event(GdkEventKey* key_event) override;
+
+ // Signals
+public:
+ sigc::signal<void ()> signal_color_changed();
+
+protected:
+ sigc::signal<void ()> _signal_color_changed;
+};
+
+/**
+ * @class ColorWheelHSL
+ */
+class ColorWheelHSL : public ColorWheel
+{
+public:
+ void setRgb(double r, double g, double b, bool overrideHue = true) override;
+ void getRgb(double *r, double *g, double *b) const override;
+ void getRgbV(double *rgb) const override;
+ guint32 getRgb() const override;
+
+ void getHsl(double *h, double *s, double *l) const;
+
+protected:
+ bool on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) override;
+ bool on_focus(Gtk::DirectionType direction) override;
+
+private:
+ void _set_from_xy(double const x, double const y) override;
+ bool _is_in_ring(double x, double y);
+ bool _is_in_triangle(double x, double y);
+ void _update_triangle_color(double x, double y);
+ void _update_ring_color(double x, double y);
+ void _triangle_corners(double& x0, double& y0, double& x1, double& y1, double& x2,
+ double& y2);
+
+ enum class DragMode {
+ NONE,
+ HUE,
+ SATURATION_VALUE
+ };
+
+ double _ring_width = 0.2;
+ DragMode _mode = DragMode::NONE;
+ bool _focus_on_ring = true;
+
+ // Callbacks
+ 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;
+};
+
+/**
+ * @class ColorWheelHSLuv
+ */
+class ColorWheelHSLuv : public ColorWheel
+{
+public:
+ ColorWheelHSLuv();
+ ~ColorWheelHSLuv() override = default;
+
+ void setRgb(double r, double g, double b, bool overrideHue = true) override;
+ void getRgb(double *r, double *g, double *b) const override;
+ void getRgbV(double *rgb) const override;
+ guint32 getRgb() const override;
+
+ void setHsluv(double h, double s, double l);
+ void setLightness(double l) override;
+
+ void getHsluv(double *h, double *s, double *l) const;
+ void updateGeometry();
+
+protected:
+ bool on_draw(::Cairo::RefPtr<::Cairo::Context> const &cr) override;
+
+private:
+ void _set_from_xy(double const x, double const y) override;
+ void _setFromPoint(Geom::Point const &pt) { _set_from_xy(pt[Geom::X], pt[Geom::Y]); }
+ void _updatePolygon();
+
+ static Geom::IntPoint _getMargin(Gtk::Allocation const &allocation);
+ inline static Geom::IntPoint _getAllocationDimensions(Gtk::Allocation const &allocation)
+ {
+ return {allocation.get_width(), allocation.get_height()};
+ }
+ inline static int _getAllocationSize(Gtk::Allocation const &allocation)
+ {
+ return std::min(allocation.get_width(), allocation.get_height());
+ }
+ bool _vertex() const;
+
+ // Callbacks
+ 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;
+
+ double _scale = 1.0;
+ std::unique_ptr<Hsluv::PickerGeometry> _picker_geometry;
+ std::vector<guint32> _buffer_polygon;
+ Cairo::RefPtr<::Cairo::ImageSurface> _surface_polygon;
+ int _cache_width = 0, _cache_height = 0;
+ int _square_size = 1;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INK_COLORWHEEL_HSLUV_H
diff --git a/src/ui/widget/ink-ruler.cpp b/src/ui/widget/ink-ruler.cpp
new file mode 100644
index 0000000..1705978
--- /dev/null
+++ b/src/ui/widget/ink-ruler.cpp
@@ -0,0 +1,645 @@
+// 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
+ * 2022 Martin Owens
+ *
+ * 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 <gdkmm/rgba.h>
+#include <glibmm/ustring.h>
+#include <iostream>
+#include <cmath>
+
+#include "inkscape.h"
+#include "ui/themes.h"
+#include "ui/util.h"
+#include "util/units.h"
+
+using Inkscape::Util::unit_table;
+
+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 |
+ Gdk::BUTTON_PRESS_MASK | // For guide creation
+ Gdk::BUTTON_RELEASE_MASK );
+
+ set_no_show_all();
+
+ auto prefs = Inkscape::Preferences::get();
+ _watch_prefs = prefs->createObserver("/options/ruler/show_bbox", sigc::mem_fun(*this, &Ruler::on_prefs_changed));
+ on_prefs_changed();
+
+ INKSCAPE.themecontext->getChangeThemeSignal().connect(sigc::mem_fun(*this, &Ruler::on_style_updated));
+}
+
+void Ruler::on_prefs_changed()
+{
+ auto prefs = Inkscape::Preferences::get();
+ _sel_visible = prefs->getBool("/options/ruler/show_bbox", true);
+
+ _backing_store_valid = false;
+ queue_draw();
+}
+
+// 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(double lower, 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();
+ }
+}
+
+/**
+ * Set the location of the currently selected page.
+ */
+void Ruler::set_page(double lower, double upper)
+{
+ if (_page_lower != lower || _page_upper != upper) {
+ _page_lower = lower;
+ _page_upper = upper;
+
+ _backing_store_valid = false;
+ queue_draw();
+ }
+}
+
+/**
+ * Set the location of the currently selected page.
+ */
+void Ruler::set_selection(double lower, double upper)
+{
+ if (_sel_lower != lower || _sel_upper != upper) {
+ _sel_lower = lower;
+ _sel_upper = upper;
+
+ _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::on_motion_notify_event), 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::on_motion_notify_event(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;
+}
+
+bool Ruler::on_button_press_event(GdkEventButton *event)
+{
+ if (event->button == 3) {
+ auto menu = getContextMenu();
+ menu->popup_at_pointer(reinterpret_cast<GdkEvent *>(event));
+ // Question to Reviewer: Does this leak?
+ return true;
+ }
+ 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 ruler's size from CSS style
+ GValue minimum_height = G_VALUE_INIT;
+ gtk_style_context_get_property(style_context->gobj(), "min-height", GTK_STATE_FLAG_NORMAL, &minimum_height);
+ auto size = g_value_get_int(&minimum_height);
+ g_value_unset(&minimum_height);
+
+ 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)
+{
+ Gtk::Allocation allocation = get_allocation();
+ int awidth = allocation.get_width();
+ int aheight = allocation.get_height();
+
+ // 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);
+
+ // background
+ auto context = get_style_context();
+ context->render_background(cr, 0, 0, awidth, aheight);
+
+ // Color in page indication box
+ if (double psize = std::abs(_page_upper - _page_lower)) {
+ Gdk::Cairo::set_source_rgba(cr, _page_fill);
+ cr->begin_new_path();
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ cr->rectangle(_page_lower, 0, psize, aheight);
+ } else {
+ cr->rectangle(0, _page_lower, awidth, psize);
+ }
+ cr->fill();
+ } else {
+ g_warning("No size?");
+ }
+ cr->set_line_width(1.0);
+
+ // 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());
+
+ auto paint_line = [=](Gdk::RGBA color, int offset) {
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ cr->move_to(0, offset - 0.5);
+ cr->line_to(allocation.get_width(), offset - 0.5);
+ } else {
+ cr->move_to(offset - 0.5, 0);
+ cr->line_to(offset - 0.5, allocation.get_height());
+ }
+ Gdk::Cairo::set_source_rgba(cr, color);
+ cr->stroke();
+ };
+
+ if (_orientation != Gtk::ORIENTATION_HORIZONTAL) {
+ // From here on, awidth is the longest dimension of the ruler, rheight is the shortest.
+ std::swap(awidth, aheight);
+ std::swap(rwidth, rheight);
+ }
+ // Draw bottom/right line of ruler
+ paint_line(_foreground, aheight);
+
+ // Draw a shadow which overlaps any previously painted object.
+ auto paint_shadow = [=](double size_x, double size_y, double width, double height) {
+ auto trans = change_alpha(_shadow, 0.0);
+ auto gr = create_cubic_gradient(Geom::Rect(0, 0, size_x, size_y), _shadow, trans, Geom::Point(0, 0.5), Geom::Point(0.5, 1));
+ cr->rectangle(0, 0, width, height);
+ cr->set_source(gr);
+ cr->fill();
+ };
+ int gradient_size = 4;
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ paint_shadow(0, gradient_size, allocation.get_width(), gradient_size);
+ } else {
+ paint_shadow(gradient_size, 0, gradient_size, allocation.get_height());
+ }
+
+ // 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);
+ }
+
+ // Loop over all ticks
+ Gdk::Cairo::set_source_rgba(cr, _foreground);
+ 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 - 7;
+ 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) {
+ cr->save();
+
+ int label_value = std::round(i * units_per_tick);
+
+ auto &label = _label_cache[label_value];
+ if (!label) {
+ label = draw_label(surface_in, label_value);
+ }
+
+ // Align text to pixel
+ int x = _border.get_left() + 3;
+ int y = position + 2.5;
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ x = position + 2.5;
+ y = _border.get_top() + 3;
+ }
+
+ // We don't know the surface height/width, damn you cairo.
+ cr->rectangle(x, y, 100, 100);
+ cr->clip();
+ cr->set_source(label, x, y);
+ cr->paint();
+ cr->restore();
+ }
+
+ // Draw ticks
+ Gdk::Cairo::set_source_rgba(cr, _foreground);
+ 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();
+ }
+
+ // Draw a selection bar
+ if (_sel_lower != _sel_upper && _sel_visible) {
+
+ const auto radius = 3.0;
+ const auto delta = _sel_upper - _sel_lower;
+ const auto dxy = delta > 0 ? radius : -radius;
+ double sy0 = _sel_lower;
+ double sy1 = _sel_upper;
+ double sx0 = floor(aheight * 0.7);
+ double sx1 = sx0;
+
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ std::swap(sy0, sx0);
+ std::swap(sy1, sx1);
+ }
+
+ cr->set_line_width(2.0);
+
+ if (fabs(delta) > 2 * radius) {
+ Gdk::Cairo::set_source_rgba(cr, _select_stroke);
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ cr->move_to(sx0 + dxy, sy0);
+ cr->line_to(sx1 - dxy, sy1);
+ }
+ else {
+ cr->move_to(sx0, sy0 + dxy);
+ cr->line_to(sx1, sy1 - dxy);
+ }
+ cr->stroke();
+ }
+
+ // Markers
+ Gdk::Cairo::set_source_rgba(cr, _select_fill);
+ cr->begin_new_path();
+ cr->arc(sx0, sy0, radius, 0, 2 * M_PI);
+ cr->arc(sx1, sy1, radius, 0, 2 * M_PI);
+ cr->fill();
+
+ Gdk::Cairo::set_source_rgba(cr, _select_stroke);
+ cr->begin_new_path();
+ cr->arc(sx0, sy0, radius, 0, 2 * M_PI);
+ cr->stroke();
+ cr->begin_new_path();
+ cr->arc(sx1, sy1, radius, 0, 2 * M_PI);
+ cr->stroke();
+ }
+
+ _backing_store_valid = true;
+ return true;
+}
+
+/**
+ * Generate the label as it's only small surface for caching.
+ */
+Cairo::RefPtr<Cairo::Surface> Ruler::draw_label(Cairo::RefPtr<Cairo::Surface> const &surface_in, int label_value)
+{
+ bool rotate = _orientation != Gtk::ORIENTATION_HORIZONTAL;
+
+ Glib::RefPtr<Pango::Layout> layout = create_pango_layout(std::to_string(label_value));
+ layout->set_font_description(_font);
+
+ int text_width;
+ int text_height;
+ layout->get_pixel_size(text_width, text_height);
+ if (rotate) {
+ std::swap(text_width, text_height);
+ }
+
+ auto surface = Cairo::Surface::create(surface_in, Cairo::CONTENT_COLOR_ALPHA, text_width, text_height);
+ Cairo::RefPtr<::Cairo::Context> cr = ::Cairo::Context::create(surface);
+
+ cr->save();
+ Gdk::Cairo::set_source_rgba(cr, _foreground);
+ if (rotate) {
+ cr->translate(text_width / 2, text_height / 2);
+ cr->rotate(-M_PI_2);
+ cr->translate(-text_height / 2, -text_width / 2);
+ }
+ layout->show_in_cairo_context(cr);
+ cr->restore();
+
+ return surface;
+}
+
+// Draw position marker, we use doubles here.
+void
+Ruler::draw_marker(const Cairo::RefPtr<::Cairo::Context>& cr)
+{
+ Gtk::Allocation allocation = get_allocation();
+ const int awidth = allocation.get_width();
+ const int aheight = allocation.get_height();
+
+ 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()
+{
+ 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();
+
+ Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context();
+ style_context->add_class(_orientation == Gtk::ORIENTATION_HORIZONTAL ? "horz" : "vert");
+
+ // Cache all our colors to speed up rendering.
+ _border = style_context->get_border();
+ _foreground = get_context_color(style_context, "color");
+ _font = style_context->get_font();
+ _font_size = _font.get_size();
+ if (!_font.get_size_is_absolute())
+ _font_size /= Pango::SCALE;
+
+ style_context->add_class("shadow");
+ _shadow = get_context_color(style_context, "border-color");
+ style_context->remove_class("shadow");
+
+ style_context->add_class("page");
+ _page_fill = get_background_color(style_context);
+ style_context->remove_class("page");
+
+ style_context->add_class("selection");
+ _select_fill = get_background_color(style_context);
+ _select_stroke = get_context_color(style_context, "border-color");
+ style_context->remove_class("selection");
+ _label_cache.clear();
+ _backing_store_valid = false;
+ queue_resize();
+ queue_draw();
+}
+
+/**
+ * Return a contextmenu for the ruler
+ */
+Gtk::Menu *Ruler::getContextMenu()
+{
+ auto gtk_menu = new Gtk::Menu();
+ auto gio_menu = Gio::Menu::create();
+ auto unit_menu = Gio::Menu::create();
+
+ for (auto &pair : unit_table.units(Inkscape::Util::UNIT_TYPE_LINEAR)) {
+ auto unit = pair.second.abbr;
+ Glib::ustring action_name = "doc.set-display-unit('" + unit + "')";
+ auto item = Gio::MenuItem::create(unit, action_name);
+ unit_menu->append_item(item);
+ }
+
+ gio_menu->append_section(unit_menu);
+ gtk_menu->bind_model(gio_menu, true);
+ gtk_menu->attach_to_widget(*this); // Might need canvas here
+ gtk_menu->show();
+ return gtk_menu;
+}
+
+} // 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..987519e
--- /dev/null
+++ b/src/ui/widget/ink-ruler.h
@@ -0,0 +1,112 @@
+// 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 "preferences.h"
+#include <gtkmm.h>
+#include <unordered_map>
+
+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(double lower, double upper);
+ void set_page(double lower, double upper);
+ void set_selection(double lower, double upper);
+
+ void add_track_widget(Gtk::Widget& widget);
+
+ 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;
+ void on_prefs_changed();
+
+ bool on_motion_notify_event(GdkEventMotion *motion_event) override;
+ bool on_button_press_event(GdkEventButton *button_event) override;
+
+private:
+ Inkscape::PrefObserver _watch_prefs;
+
+ Gtk::Menu *getContextMenu();
+ Cairo::RefPtr<Cairo::Surface> draw_label(Cairo::RefPtr<Cairo::Surface> const &surface_in, int label_value);
+
+ Gtk::Orientation _orientation;
+
+ Inkscape::Util::Unit const* _unit;
+ double _lower;
+ double _upper;
+ double _position;
+ double _max_size;
+
+ // Page block
+ double _page_lower = 0.0;
+ double _page_upper = 0.0;
+
+ // Selection block
+ double _sel_lower = 0.0;
+ double _sel_upper = 0.0;
+ double _sel_visible = true;
+
+ bool _backing_store_valid;
+
+ Cairo::RefPtr<::Cairo::Surface> _backing_store;
+ Cairo::RectangleInt _rect;
+
+ std::unordered_map<int, Cairo::RefPtr<::Cairo::Surface>> _label_cache;
+
+ // Cached style properties
+ Gtk::Border _border;
+ Gdk::RGBA _shadow;
+ Gdk::RGBA _foreground;
+ Pango::FontDescription _font;
+ int _font_size;
+ Gdk::RGBA _page_fill;
+ Gdk::RGBA _select_fill;
+ Gdk::RGBA _select_stroke;
+};
+
+} // 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..b977d21
--- /dev/null
+++ b/src/ui/widget/ink-spinscale.cpp
@@ -0,0 +1,288 @@
+// 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")
+ , parent_type(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);
+ auto btn_alloc = _spinbutton->get_allocation();
+ auto alloc = get_allocation();
+ y += btn_alloc.get_y() - alloc.get_y();
+
+ // Fill widget proportional to value.
+ double fraction = get_fraction();
+
+ // Get through 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;
+
+ 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 Inkscape::UI::Widget::ScrollProtected<Gtk::SpinButton>(_adjustment));
+ _spinbutton->set_valign(Gtk::ALIGN_CENTER);
+ _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 Inkscape::UI::Widget::ScrollProtected<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..4f07b27
--- /dev/null
+++ b/src/ui/widget/ink-spinscale.h
@@ -0,0 +1,100 @@
+// 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>
+
+#include "scrollprotected.h"
+
+namespace Gtk {
+ class SpinButton;
+}
+
+class InkScale : public Inkscape::UI::Widget::ScrollProtected<Gtk::Scale>
+{
+ using parent_type = ScrollProtected<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/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..348092f
--- /dev/null
+++ b/src/ui/widget/labelled.cpp
@@ -0,0 +1,106 @@
+// 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)
+ : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL),
+ _widget(widget),
+ _label(new Gtk::Label(label, Gtk::ALIGN_START, Gtk::ALIGN_CENTER, mnemonic)),
+ _suffix(nullptr)
+{
+ _widget->drag_dest_unset();
+ 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_markup(tooltip);
+}
+
+
+void Labelled::setWidgetSizeRequest(int width, int height)
+{
+ if (_widget)
+ _widget->set_size_request(width, height);
+
+
+}
+
+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::Box::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..bd82090
--- /dev/null
+++ b/src/ui/widget/labelled.h
@@ -0,0 +1,88 @@
+// 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::Box
+{
+protected:
+ Gtk::Widget *_widget;
+ Gtk::Label *_label;
+ Gtk::Label *_suffix;
+ Gtk::Image *_icon;
+
+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);
+
+ inline decltype(_widget) getWidget() const { return _widget; }
+ 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;
+};
+
+} // 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..a96c120
--- /dev/null
+++ b/src/ui/widget/layer-selector.cpp
@@ -0,0 +1,219 @@
+// 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 <boost/range/adaptor/filtered.hpp>
+#include <boost/range/adaptor/reversed.hpp>
+
+#include <glibmm/i18n.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "layer-manager.h"
+
+#include "ui/widget/layer-selector.h"
+#include "ui/dialog/dialog-container.h"
+#include "ui/dialog/objects.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "ui/util.h"
+
+#include "object/sp-root.h"
+#include "object/sp-item-group.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class AlternateIcons : public Gtk::Box {
+public:
+ AlternateIcons(Gtk::BuiltinIconSize size, Glib::ustring const &a, Glib::ustring const &b)
+ : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)
+ , _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::LayerSelector(SPDesktop *desktop)
+ : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)
+ , _desktop(nullptr)
+ , _observer(new Inkscape::XML::SignalObserver)
+{
+ set_name("LayerSelector");
+
+ _layer_name.signal_clicked().connect(sigc::mem_fun(*this, &LayerSelector::_layerChoose));
+ _layer_name.set_relief(Gtk::RELIEF_NONE);
+ _layer_name.set_tooltip_text(_("Current layer"));
+ pack_start(_layer_name, Gtk::PACK_EXPAND_WIDGET);
+
+ _eye_label = Gtk::manage(new AlternateIcons(Gtk::ICON_SIZE_MENU,
+ INKSCAPE_ICON("object-visible"), INKSCAPE_ICON("object-hidden")));
+ _eye_toggle.add(*_eye_label);
+ _hide_layer_connection = _eye_toggle.signal_toggled().connect(sigc::mem_fun(*this, &LayerSelector::_hideLayer));
+
+ _eye_toggle.set_relief(Gtk::RELIEF_NONE);
+ _eye_toggle.set_tooltip_text(_("Toggle current layer visibility"));
+ pack_start(_eye_toggle, Gtk::PACK_EXPAND_PADDING);
+
+ _lock_label = Gtk::manage(new AlternateIcons(Gtk::ICON_SIZE_MENU,
+ INKSCAPE_ICON("object-unlocked"), INKSCAPE_ICON("object-locked")));
+ _lock_toggle.add(*_lock_label);
+ _lock_layer_connection = _lock_toggle.signal_toggled().connect(sigc::mem_fun(*this, &LayerSelector::_lockLayer));
+
+ _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);
+
+ _layer_name.add(_layer_label);
+ _layer_label.set_max_width_chars(16);
+ _layer_label.set_ellipsize(Pango::ELLIPSIZE_END);
+ _layer_label.set_markup("<i>Unset</i>");
+ _layer_label.set_valign(Gtk::ALIGN_CENTER);
+
+ _observer->signal_changed().connect(sigc::mem_fun(*this, &LayerSelector::_layerModified));
+ setDesktop(desktop);
+}
+
+LayerSelector::~LayerSelector() {
+ setDesktop(nullptr);
+}
+
+void LayerSelector::setDesktop(SPDesktop *desktop) {
+ if ( desktop == _desktop )
+ return;
+
+ _layer_changed.disconnect();
+ _desktop = desktop;
+
+ if (_desktop) {
+ _layer_changed = _desktop->layerManager().connectCurrentLayerChanged(sigc::mem_fun(*this, &LayerSelector::_layerChanged));
+ _layerChanged(_desktop->layerManager().currentLayer());
+ }
+}
+
+/**
+ * Selects the given layer in the widget.
+ */
+void LayerSelector::_layerChanged(SPGroup *layer)
+{
+ _layer = layer;
+ _observer->set(layer);
+ _layerModified();
+}
+
+/**
+ * If anything happens to the layer, refresh it.
+ */
+void LayerSelector::_layerModified()
+{
+ auto root = _desktop->layerManager().currentRoot();
+ bool active = _layer && _layer != root;
+
+ if (_label_style) {
+ _layer_label.get_style_context()->remove_provider(_label_style);
+ }
+ auto color_str = std::string("white");
+
+ if (active) {
+ _layer_label.set_text(_layer->defaultLabel());
+ color_str = SPColor(_layer->highlight_color()).toString();
+ } else {
+ _layer_label.set_markup(_layer ? "<i>[root]</i>" : "<i>nothing</i>");
+ }
+
+ Glib::RefPtr<Gtk::StyleContext> style_context = _layer_label.get_style_context();
+ _label_style = Gtk::CssProvider::create();
+ _label_style->load_from_data("#LayerSelector label {border-color:" + color_str + ";}");
+ _layer_label.get_style_context()->add_provider(_label_style, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+
+ _hide_layer_connection.block();
+ _lock_layer_connection.block();
+ _eye_toggle.set_sensitive(active);
+ _lock_toggle.set_sensitive(active);
+ _eye_label->setState(active && _layer->isHidden());
+ _eye_toggle.set_active(active && _layer->isHidden());
+ _lock_label->setState(active && _layer->isLocked());
+ _lock_toggle.set_active(active && _layer->isLocked());
+ _hide_layer_connection.unblock();
+ _lock_layer_connection.unblock();
+}
+
+void LayerSelector::_lockLayer()
+{
+ bool lock = _lock_toggle.get_active();
+ if (auto layer = _desktop->layerManager().currentLayer()) {
+ layer->setLocked(lock);
+ DocumentUndo::done(_desktop->getDocument(), lock ? _("Lock layer") : _("Unlock layer"), "");
+ }
+}
+
+void LayerSelector::_hideLayer()
+{
+ bool hide = _eye_toggle.get_active();
+ if (auto layer = _desktop->layerManager().currentLayer()) {
+ layer->setHidden(hide);
+ DocumentUndo::done(_desktop->getDocument(), hide ? _("Hide layer") : _("Unhide layer"), "");
+ }
+}
+
+void LayerSelector::_layerChoose()
+{
+ _desktop->getContainer()->new_dialog("Objects");
+}
+
+} // 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..6300cba
--- /dev/null
+++ b/src/ui/widget/layer-selector.h
@@ -0,0 +1,82 @@
+// 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 <gtkmm/cssprovider.h>
+#include <sigc++/slot.h>
+
+#include "xml/helper-observer.h"
+
+class SPDesktop;
+class SPDocument;
+class SPGroup;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class AlternateIcons;
+
+class LayerSelector : public Gtk::Box {
+public:
+ LayerSelector(SPDesktop *desktop = nullptr);
+ ~LayerSelector() override;
+
+ void setDesktop(SPDesktop *desktop);
+private:
+ SPDesktop *_desktop;
+ SPGroup *_layer;
+
+ Gtk::ToggleButton _eye_toggle;
+ Gtk::ToggleButton _lock_toggle;
+ Gtk::Button _layer_name;
+ Gtk::Label _layer_label;
+ Glib::RefPtr<Gtk::CssProvider> _label_style;
+ AlternateIcons * _eye_label;
+ AlternateIcons * _lock_label;
+
+ sigc::connection _layer_changed;
+ sigc::connection _hide_layer_connection;
+ sigc::connection _lock_layer_connection;
+ std::unique_ptr<Inkscape::XML::SignalObserver> _observer;
+
+ void _layerChanged(SPGroup *layer);
+ void _layerModified();
+ void _selectLayer();
+ void _hideLayer();
+ void _lockLayer();
+ void _layerChoose();
+};
+
+} // 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/licensor.cpp b/src/ui/widget/licensor.cpp
new file mode 100644
index 0000000..31ca2a0
--- /dev/null
+++ b/src/ui/widget/licensor.cpp
@@ -0,0 +1,157 @@
+// 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 "rdf.h"
+#include "inkscape.h"
+#include "document-undo.h"
+
+#include "ui/widget/entity-entry.h"
+#include "ui/widget/registry.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() || !_wr.desktop())
+ return;
+
+ _wr.setUpdating (true);
+ SPDocument *doc = _wr.desktop()->getDocument();
+ rdf_set_license (doc, _lic->details ? _lic : nullptr);
+ if (doc->isSensitive()) {
+ DocumentUndo::done(doc, _("Document license updated"), "");
+ }
+ _wr.setUpdating (false);
+ static_cast<Gtk::Entry*>(_eep->_packable)->set_text (_lic->uri);
+ _eep->on_changed();
+}
+
+//---------------------------------------------------
+
+Licensor::Licensor()
+: Gtk::Box(Gtk::ORIENTATION_VERTICAL, 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::Box *box = Gtk::manage (new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
+ 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 */
+ constexpr bool read_only = false;
+ struct rdf_license_t * license = rdf_get_license(doc, read_only);
+
+ 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, read_only);
+}
+
+} // 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..214ffca
--- /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::Box {
+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/marker-combo-box.cpp b/src/ui/widget/marker-combo-box.cpp
new file mode 100644
index 0000000..0b712b2
--- /dev/null
+++ b/src/ui/widget/marker-combo-box.cpp
@@ -0,0 +1,814 @@
+// 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 "marker-combo-box.h"
+
+#include <glibmm/fileutils.h>
+#include <glibmm/i18n.h>
+#include <gtkmm/icontheme.h>
+#include <gtkmm/menubutton.h>
+
+#include "desktop-style.h"
+#include "helper/stock-items.h"
+#include "io/resource.h"
+#include "io/sys.h"
+#include "manipulation/copy-resource.h"
+#include "object/sp-defs.h"
+#include "object/sp-marker.h"
+#include "object/sp-root.h"
+#include "path-prefix.h"
+#include "style.h"
+#include "ui/builder-utils.h"
+#include "ui/cache/svg_preview_cache.h"
+#include "ui/dialog-events.h"
+#include "ui/icon-loader.h"
+#include "ui/svg-renderer.h"
+#include "ui/util.h"
+#include "ui/widget/spinbutton.h"
+#include "ui/widget/stroke-style.h"
+#include "util/object-renderer.h"
+
+#define noTIMING_INFO 1;
+
+using Inkscape::UI::get_widget;
+using Inkscape::UI::create_builder;
+
+// size of marker image in a list
+static const int ITEM_WIDTH = 40;
+static const int ITEM_HEIGHT = 32;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+// separator for FlowBox widget
+static cairo_surface_t* create_separator(double alpha, int width, int height, int device_scale) {
+ width *= device_scale;
+ height *= device_scale;
+ cairo_surface_t* surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
+ cairo_t* ctx = cairo_create(surface);
+ cairo_set_source_rgba(ctx, 0.5, 0.5, 0.5, alpha);
+ cairo_move_to(ctx, 0.5, height / 2 + 0.5);
+ cairo_line_to(ctx, width + 0.5, height / 2 + 0.5);
+ cairo_set_line_width(ctx, 1.0 * device_scale);
+ cairo_stroke(ctx);
+ cairo_surface_flush(surface);
+ cairo_surface_set_device_scale(surface, device_scale, device_scale);
+ return surface;
+}
+
+// empty image; "no marker"
+static Cairo::RefPtr<Cairo::Surface> g_image_none;
+// error extracting/rendering marker; "bad marker"
+static Cairo::RefPtr<Cairo::Surface> g_bad_marker;
+
+Glib::ustring get_attrib(SPMarker* marker, const char* attrib) {
+ auto value = marker->getAttribute(attrib);
+ return value ? value : "";
+}
+
+double get_attrib_num(SPMarker* marker, const char* attrib) {
+ auto val = get_attrib(marker, attrib);
+ return strtod(val.c_str(), nullptr);
+}
+
+MarkerComboBox::MarkerComboBox(Glib::ustring id, int l) :
+ _combo_id(std::move(id)),
+ _loc(l),
+ _builder(create_builder("marker-popup.glade")),
+ _marker_list(get_widget<Gtk::FlowBox>(_builder, "flowbox")),
+ _preview(get_widget<Gtk::Image>(_builder, "preview")),
+ _marker_name(get_widget<Gtk::Label>(_builder, "marker-id")),
+ _link_scale(get_widget<Gtk::Button>(_builder, "link-scale")),
+ _scale_x(get_widget<Gtk::SpinButton>(_builder, "scale-x")),
+ _scale_y(get_widget<Gtk::SpinButton>(_builder, "scale-y")),
+ _scale_with_stroke(get_widget<Gtk::CheckButton>(_builder, "scale-with-stroke")),
+ _menu_btn(get_widget<Gtk::MenuButton>(_builder, "menu-btn")),
+ _angle_btn(get_widget<Gtk::SpinButton>(_builder, "angle")),
+ _offset_x(get_widget<Gtk::SpinButton>(_builder, "offset-x")),
+ _offset_y(get_widget<Gtk::SpinButton>(_builder, "offset-y")),
+ _input_grid(get_widget<Gtk::Grid>(_builder, "input-grid")),
+ _orient_auto_rev(get_widget<Gtk::RadioButton>(_builder, "orient-auto-rev")),
+ _orient_auto(get_widget<Gtk::RadioButton>(_builder, "orient-auto")),
+ _orient_angle(get_widget<Gtk::RadioButton>(_builder, "orient-angle")),
+ _orient_flip_horz(get_widget<Gtk::Button>(_builder, "btn-horz-flip")),
+ _current_img(get_widget<Gtk::Image>(_builder, "current-img")),
+ _edit_marker(get_widget<Gtk::Button>(_builder, "edit-marker"))
+{
+ _background_color = 0x808080ff;
+ _foreground_color = 0x808080ff;
+
+ if (!g_image_none) {
+ auto device_scale = get_scale_factor();
+ g_image_none = Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(create_separator(1, ITEM_WIDTH, ITEM_HEIGHT, device_scale)));
+ }
+
+ if (!g_bad_marker) {
+ auto path = Inkscape::IO::Resource::get_filename(Inkscape::IO::Resource::UIS, "bad-marker.svg");
+ Inkscape::svg_renderer renderer(path.c_str());
+ g_bad_marker = renderer.render_surface(1.0);
+ }
+
+ add(_menu_btn);
+
+ _preview.signal_size_allocate().connect([=](Gtk::Allocation& a){
+ // refresh after preview widget has been finally resized/expanded
+ if (_preview_no_alloc) update_preview(find_marker_item(get_current()));
+ });
+
+ _marker_store = Gio::ListStore<MarkerItem>::create();
+ _marker_list.bind_list_store(_marker_store, [=](const Glib::RefPtr<MarkerItem>& item){
+ auto image = Gtk::make_managed<Gtk::Image>(item->pix);
+ image->show();
+ auto box = Gtk::make_managed<Gtk::FlowBoxChild>();
+ box->add(*image);
+ if (item->separator) {
+ image->set_sensitive(false);
+ image->set_can_focus(false);
+ image->set_size_request(-1, 10);
+ box->set_sensitive(false);
+ box->set_can_focus(false);
+ box->get_style_context()->add_class("marker-separator");
+ }
+ else {
+ box->get_style_context()->add_class("marker-item-box");
+ }
+ _widgets_to_markers[image] = item;
+ box->set_size_request(item->width, item->height);
+ return box;
+ });
+
+ _sandbox = Inkscape::ink_markers_preview_doc(_combo_id);
+
+ set_sensitive(true);
+
+ _marker_list.signal_selected_children_changed().connect([=](){
+ auto item = get_active();
+ if (!item && !_marker_list.get_selected_children().empty()) {
+ _marker_list.unselect_all();
+ }
+ });
+
+ _marker_list.signal_child_activated().connect([=](Gtk::FlowBoxChild* box){
+ if (box->get_sensitive()) _signal_changed.emit();
+ });
+
+ auto set_orient = [=](bool enable_angle, const char* value) {
+ if (_update.pending()) return;
+ _angle_btn.set_sensitive(enable_angle);
+ sp_marker_set_orient(get_current(), value);
+ };
+ _orient_auto_rev.signal_toggled().connect([=](){ set_orient(false, "auto-start-reverse"); });
+ _orient_auto.signal_toggled().connect([=]() { set_orient(false, "auto"); });
+ _orient_angle.signal_toggled().connect([=]() { set_orient(true, _angle_btn.get_text().c_str()); });
+ _orient_flip_horz.signal_clicked().connect([=]() { sp_marker_flip_horizontally(get_current()); });
+
+ _angle_btn.signal_value_changed().connect([=]() {
+ if (_update.pending() || !_angle_btn.is_sensitive()) return;
+ sp_marker_set_orient(get_current(), _angle_btn.get_text().c_str());
+ });
+
+ auto set_scale = [=](bool changeWidth) {
+ if (_update.pending()) return;
+
+ if (auto marker = get_current()) {
+ auto sx = _scale_x.get_value();
+ auto sy = _scale_y.get_value();
+ auto width = get_attrib_num(marker, "markerWidth");
+ auto height = get_attrib_num(marker, "markerHeight");
+ if (_scale_linked && width > 0.0 && height > 0.0) {
+ auto scoped(_update.block());
+ if (changeWidth) {
+ // scale height proportionally
+ sy = height * (sx / width);
+ _scale_y.set_value(sy);
+ }
+ else {
+ // scale width proportionally
+ sx = width * (sy / height);
+ _scale_x.set_value(sx);
+ }
+ }
+ sp_marker_set_size(marker, sx, sy);
+ }
+ };
+
+ // delay setting scale to idle time; if invoked by focus change due to new marker selection
+ // it leads to marker list rebuild and apparent flowbox content corruption
+ auto idle_set_scale = [=](bool changeWidth) {
+ if (_update.pending()) return;
+
+ if (auto orig_marker = get_current()) {
+ _idle = Glib::signal_idle().connect([=](){
+ if (auto marker = get_current()) {
+ if (marker == orig_marker) {
+ set_scale(changeWidth);
+ }
+ }
+ return false; // don't call again
+ });
+ }
+ };
+
+ _link_scale.signal_clicked().connect([=](){
+ if (_update.pending()) return;
+ _scale_linked = !_scale_linked;
+ sp_marker_set_uniform_scale(get_current(), _scale_linked);
+ update_scale_link();
+ });
+
+ _scale_x.signal_value_changed().connect([=]() { idle_set_scale(true); });
+ _scale_y.signal_value_changed().connect([=]() { idle_set_scale(false); });
+
+ _scale_with_stroke.signal_toggled().connect([=](){
+ if (_update.pending()) return;
+ sp_marker_scale_with_stroke(get_current(), _scale_with_stroke.get_active());
+ });
+
+ auto set_offset = [=](){
+ if (_update.pending()) return;
+ sp_marker_set_offset(get_current(), _offset_x.get_value(), _offset_y.get_value());
+ };
+ _offset_x.signal_value_changed().connect([=]() { set_offset(); });
+ _offset_y.signal_value_changed().connect([=]() { set_offset(); });
+
+ // request to edit marker on canvas; close popup to get it out of the way and call marker edit tool
+ _edit_marker.signal_clicked().connect([=]() { _menu_btn.get_popover()->popdown(); edit_signal(); });
+
+ // before showing popover refresh marker attributes
+ _menu_btn.get_popover()->signal_show().connect([=](){ update_ui(get_current(), false); }, false);
+
+ update_scale_link();
+ _current_img.set(g_image_none);
+ show();
+}
+
+MarkerComboBox::~MarkerComboBox() {
+ if (_idle) {
+ _idle.disconnect();
+ }
+ if (_document) {
+ modified_connection.disconnect();
+ }
+}
+
+void MarkerComboBox::update_widgets_from_marker(SPMarker* marker) {
+ _input_grid.set_sensitive(marker != nullptr);
+
+ if (marker) {
+ _scale_x.set_value(get_attrib_num(marker, "markerWidth"));
+ _scale_y.set_value(get_attrib_num(marker, "markerHeight"));
+ auto units = get_attrib(marker, "markerUnits");
+ _scale_with_stroke.set_active(units == "strokeWidth" || units == "");
+ auto aspect = get_attrib(marker, "preserveAspectRatio");
+ _scale_linked = aspect != "none";
+ update_scale_link();
+ // marker->setAttribute("markerUnits", scale_with_stroke ? "strokeWidth" : "userSpaceOnUse");
+ _offset_x.set_value(get_attrib_num(marker, "refX"));
+ _offset_y.set_value(get_attrib_num(marker, "refY"));
+ auto orient = get_attrib(marker, "orient");
+
+ // try parsing as number
+ _angle_btn.set_value(strtod(orient.c_str(), nullptr));
+ if (orient == "auto-start-reverse") {
+ _orient_auto_rev.set_active();
+ _angle_btn.set_sensitive(false);
+ }
+ else if (orient == "auto") {
+ _orient_auto.set_active();
+ _angle_btn.set_sensitive(false);
+ }
+ else {
+ _orient_angle.set_active();
+ _angle_btn.set_sensitive(true);
+ }
+ }
+}
+
+void MarkerComboBox::update_scale_link() {
+ _link_scale.remove();
+ _link_scale.add(get_widget<Gtk::Image>(_builder, _scale_linked ? "image-linked" : "image-unlinked"));
+}
+
+// update marker image inside the menu button
+void MarkerComboBox::update_menu_btn(Glib::RefPtr<MarkerItem> marker) {
+ _current_img.set(marker ? marker->pix : g_image_none);
+}
+
+// update marker preview image in the popover panel
+void MarkerComboBox::update_preview(Glib::RefPtr<MarkerItem> item) {
+ Cairo::RefPtr<Cairo::Surface> surface;
+ Glib::ustring label;
+
+ if (!item) {
+ // TRANSLATORS: None - no marker selected for a path
+ label = _("None");
+ }
+
+ if (item && item->source && !item->id.empty()) {
+ Inkscape::Drawing drawing;
+ unsigned const visionkey = SPItem::display_key_new(1);
+ drawing.setRoot(_sandbox->getRoot()->invoke_show(drawing, visionkey, SP_ITEM_SHOW_DISPLAY));
+ // generate preview
+ auto alloc = _preview.get_allocation();
+ auto size = Geom::IntPoint(alloc.get_width() - 10, alloc.get_height() - 10);
+ if (size.x() > 0 && size.y() > 0) {
+ surface = create_marker_image(size, item->id.c_str(), item->source, drawing, visionkey, true, true, 2.60);
+ }
+ else {
+ // too early, preview hasn't been expanded/resized yet
+ _preview_no_alloc = true;
+ }
+ _sandbox->getRoot()->invoke_hide(visionkey);
+ label = _(item->label.c_str());
+ }
+
+ _preview.set(surface);
+ std::ostringstream ost;
+ ost << "<small>" << label.raw() << "</small>";
+ _marker_name.set_markup(ost.str().c_str());
+}
+
+bool MarkerComboBox::MarkerItem::operator == (const MarkerItem& item) const {
+ return
+ id == item.id &&
+ label == item.label &&
+ separator == item.separator &&
+ stock == item.stock &&
+ history == item.history &&
+ source == item.source &&
+ width == item.width &&
+ height == item.height;
+}
+
+// find marker object by ID in a document
+SPMarker* find_marker(SPDocument* document, const Glib::ustring& marker_id) {
+ if (!document) return nullptr;
+
+ SPDefs* defs = document->getDefs();
+ if (!defs) return nullptr;
+
+ for (auto& child : defs->children) {
+ if (is<SPMarker>(&child)) {
+ auto marker = cast<SPMarker>(&child);
+ auto id = marker->getId();
+ if (id && marker_id == id) {
+ // found it
+ return marker;
+ }
+ }
+ }
+
+ // not found
+ return nullptr;
+}
+
+SPMarker* MarkerComboBox::get_current() const {
+ // find current marker
+ return find_marker(_document, _current_marker_id);
+}
+
+void MarkerComboBox::set_active(Glib::RefPtr<MarkerItem> item) {
+ bool selected = false;
+ if (item) {
+ _marker_list.foreach([=,&selected](Gtk::Widget& widget){
+ if (auto box = dynamic_cast<Gtk::FlowBoxChild*>(&widget)) {
+ if (auto marker = _widgets_to_markers[box->get_child()]) {
+ if (*marker.get() == *item.get()) {
+ _marker_list.select_child(*box);
+ selected = true;
+ }
+ }
+ }
+ });
+ }
+
+ if (!selected) {
+ _marker_list.unselect_all();
+ }
+}
+
+Glib::RefPtr<MarkerComboBox::MarkerItem> MarkerComboBox::find_marker_item(SPMarker* marker) {
+ std::string id;
+ if (marker != nullptr) {
+ if (auto markname = marker->getRepr()->attribute("id")) {
+ id = markname;
+ }
+ }
+
+ Glib::RefPtr<MarkerItem> marker_item;
+ if (!id.empty()) {
+ for (auto&& item : _history_items) {
+ if (item->id == id) {
+ marker_item = item;
+ break;
+ }
+ }
+ }
+
+ return marker_item;
+}
+
+Glib::RefPtr<MarkerComboBox::MarkerItem> MarkerComboBox::get_active() {
+ auto empty = Glib::RefPtr<MarkerItem>();
+ auto sel = _marker_list.get_selected_children();
+ if (sel.size() == 1) {
+ auto item = _widgets_to_markers[sel.front()->get_child()];
+ if (item && item->separator) {
+ return empty;
+ }
+ return item;
+ }
+ else {
+ return empty;
+ }
+}
+
+void MarkerComboBox::setDocument(SPDocument *document)
+{
+ if (_document != document) {
+
+ if (_document) {
+ modified_connection.disconnect();
+ }
+
+ _document = document;
+
+ if (_document) {
+ modified_connection = _document->getDefs()->connectModified([=](SPObject*, unsigned int){
+ refresh_after_markers_modified();
+ });
+ }
+
+ _current_marker_id = "";
+
+ refresh_after_markers_modified();
+ }
+}
+
+/**
+ * This function is invoked after document "defs" section changes.
+ * It will change when current marker's attributes are modified in this popup
+ * and this function will refresh the recent list and a preview to reflect the changes.
+ * It would be more efficient if there was a way to determine what has changed
+ * and perform only more targeted update.
+ */
+void MarkerComboBox::refresh_after_markers_modified() {
+ if (_update.pending()) return;
+
+ auto scoped(_update.block());
+
+ /*
+ * Seems to be no way to get notified of changes just to markers,
+ * so listen to changes in all defs and check if the number of markers has changed here
+ * to avoid unnecessary refreshes when things like gradients change
+ */
+ // TODO: detect changes to markers; ignore changes to everything else;
+ // simple count check doesn't cut it, so just do it unconditionally for now
+ marker_list_from_doc(_document, true);
+
+ auto marker = find_marker_item(get_current());
+ update_menu_btn(marker);
+ update_preview(marker);
+}
+
+Glib::RefPtr<MarkerComboBox::MarkerItem> MarkerComboBox::add_separator(bool filler) {
+ auto item = Glib::RefPtr<MarkerItem>(new MarkerItem);
+ item->history = false;
+ item->separator = true;
+ item->id = "None";
+ item->label = filler ? "filler" : "Separator";
+ item->stock = false;
+ if (!filler) {
+ auto device_scale = get_scale_factor();
+ static Cairo::RefPtr<Cairo::Surface> separator(new Cairo::Surface(create_separator(0.7, ITEM_WIDTH, 10, device_scale)));
+ item->pix = separator;
+ }
+ item->height = 10;
+ item->width = -1;
+ return item;
+}
+
+/**
+ * Init the combobox widget to display markers from markers.svg
+ */
+void
+MarkerComboBox::init_combo()
+{
+ if (_update.pending()) return;
+
+ static SPDocument *markers_doc = nullptr;
+
+ // find and load markers.svg
+ if (markers_doc == nullptr) {
+ using namespace Inkscape::IO::Resource;
+ auto markers_source = get_path_string(SYSTEM, MARKERS, "markers.svg");
+ if (Glib::file_test(markers_source, Glib::FILE_TEST_IS_REGULAR)) {
+ markers_doc = SPDocument::createNewDoc(markers_source.c_str(), false);
+ }
+ }
+
+ // load markers from markers.svg
+ if (markers_doc) {
+ marker_list_from_doc(markers_doc, false);
+ }
+
+ refresh_after_markers_modified();
+}
+
+/**
+ * Sets the current marker in the marker combobox.
+ */
+void MarkerComboBox::set_current(SPObject *marker)
+{
+ auto sp_marker = cast<SPMarker>(marker);
+
+ bool reselect = sp_marker != get_current();
+
+ update_ui(sp_marker, reselect);
+}
+
+void MarkerComboBox::update_ui(SPMarker* marker, bool select) {
+ auto scoped(_update.block());
+
+ auto id = marker ? marker->getId() : nullptr;
+ _current_marker_id = id ? id : "";
+
+ auto marker_item = find_marker_item(marker);
+
+ if (select) {
+ set_active(marker_item);
+ }
+
+ update_widgets_from_marker(marker);
+ update_menu_btn(marker_item);
+ update_preview(marker_item);
+}
+
+/**
+ * Return a uri string representing the current selected marker used for setting the marker style in the document
+ */
+std::string MarkerComboBox::get_active_marker_uri()
+{
+ /* Get Marker */
+ auto item = get_active();
+ if (!item) {
+ return std::string();
+ }
+
+ std::string marker;
+
+ if (item->id != "none") {
+ bool stockid = item->stock;
+
+ std::string markurn = stockid ? "urn:inkscape:marker:" + item->id : item->id;
+ auto mark = cast<SPMarker>(get_stock_item(markurn.c_str(), stockid));
+
+ if (mark) {
+ Inkscape::XML::Node* repr = mark->getRepr();
+ auto id = repr->attribute("id");
+ if (id) {
+ std::ostringstream ost;
+ ost << "url(#" << id << ")";
+ marker = ost.str();
+ }
+ if (stockid) {
+ mark->getRepr()->setAttribute("inkscape:collect", "always");
+ }
+ // adjust marker's attributes (or add missing ones) to stay in sync with marker tool
+ sp_validate_marker(mark, _document);
+ }
+ } else {
+ marker = item->id;
+ }
+
+ return marker;
+}
+
+/**
+ * Pick up all markers from source and add items to the list/store.
+ * If 'history' is true, then update recently used in-document portion of the list;
+ * otherwise update list of stock markers, which is displayed after recent ones
+ */
+void MarkerComboBox::marker_list_from_doc(SPDocument* source, bool history) {
+ std::vector<SPMarker*> markers = get_marker_list(source);
+ remove_markers(history);
+ add_markers(markers, source, history);
+ update_store();
+}
+
+void MarkerComboBox::update_store() {
+ _marker_store->freeze_notify();
+
+ auto selected = get_active();
+
+ _marker_store->remove_all();
+ _widgets_to_markers.clear();
+
+ // recent and user-defined markers come first
+ for (auto&& item : _history_items) {
+ _marker_store->append(item);
+ }
+
+ // separator
+ if (!_history_items.empty()) {
+ // add empty boxes to fill up the row to 'max' elements and then
+ // extra ones to create entire new empty row (a separator of sorts)
+ auto max = _marker_list.get_max_children_per_line();
+ auto fillup = max - _history_items.size() % max;
+
+ for (int i = 0; i < fillup; ++i) {
+ _marker_store->append(add_separator(true));
+ }
+ for (int i = 0; i < max; ++i) {
+ _marker_store->append(add_separator(false));
+ }
+ }
+
+ // stock markers
+ for (auto&& item : _stock_items) {
+ _marker_store->append(item);
+ }
+
+ _marker_store->thaw_notify();
+
+ // reselect current
+ set_active(selected);
+}
+/**
+ * Returns a vector of markers in the defs of the given source document as a vector.
+ * Returns empty vector if there are no markers in the document.
+ * If validate is true then it runs each marker through the validation routine that alters some attributes.
+ */
+std::vector<SPMarker*> MarkerComboBox::get_marker_list(SPDocument* source)
+{
+ std::vector<SPMarker *> ml;
+ if (source == nullptr) return ml;
+
+ SPDefs *defs = source->getDefs();
+ if (!defs) {
+ return ml;
+ }
+
+ for (auto& child: defs->children) {
+ if (is<SPMarker>(&child)) {
+ auto marker = cast<SPMarker>(&child);
+ ml.push_back(marker);
+ }
+ }
+ return ml;
+}
+
+/**
+ * Remove history or non-history markers from the combo
+ */
+void MarkerComboBox::remove_markers (gboolean history)
+{
+ if (history) {
+ _history_items.clear();
+ }
+ else {
+ _stock_items.clear();
+ }
+}
+
+/**
+ * Adds markers in marker_list to the combo
+ */
+void MarkerComboBox::add_markers (std::vector<SPMarker *> const& marker_list, SPDocument *source, gboolean history)
+{
+ // Do this here, outside of loop, to speed up preview generation:
+ Inkscape::Drawing drawing;
+ unsigned const visionkey = SPItem::display_key_new(1);
+ drawing.setRoot(_sandbox->getRoot()->invoke_show(drawing, visionkey, SP_ITEM_SHOW_DISPLAY));
+
+ if (history) {
+ // add "None"
+ auto item = Glib::RefPtr<MarkerItem>(new MarkerItem);
+ item->pix = g_image_none;
+ item->history = true;
+ item->separator = false;
+ item->id = "None";
+ item->label = "None";
+ item->stock = false;
+ item->width = ITEM_WIDTH;
+ item->height = ITEM_HEIGHT;
+ _history_items.push_back(item);
+ }
+
+#if TIMING_INFO
+auto old_time = std::chrono::high_resolution_clock::now();
+#endif
+
+ for (auto i:marker_list) {
+
+ Inkscape::XML::Node *repr = i->getRepr();
+ gchar const *markid = repr->attribute("inkscape:stockid") ? repr->attribute("inkscape:stockid") : repr->attribute("id");
+
+ // generate preview
+ auto pixbuf = create_marker_image(Geom::IntPoint(ITEM_WIDTH, ITEM_HEIGHT), repr->attribute("id"), source, drawing, visionkey, false, true, 1.50);
+
+ auto item = Glib::RefPtr<MarkerItem>(new MarkerItem);
+ item->source = source;
+ item->pix = pixbuf;
+ if (auto id = repr->attribute("id")) {
+ item->id = id;
+ }
+ item->label = markid ? markid : "";
+ item->stock = !history;
+ item->history = history;
+ item->width = ITEM_WIDTH;
+ item->height = ITEM_HEIGHT;
+
+ if (history) {
+ _history_items.emplace_back(std::move(item));
+ }
+ else {
+ _stock_items.emplace_back(std::move(item));
+ }
+ }
+
+ _sandbox->getRoot()->invoke_hide(visionkey);
+
+#if TIMING_INFO
+auto current_time = std::chrono::high_resolution_clock::now();
+auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(current_time - old_time);
+g_warning("%s render time for %d markers: %d ms", combo_id, (int)marker_list.size(), static_cast<int>(elapsed.count()));
+#endif
+}
+
+/**
+ * Creates a copy of the marker named mname, determines its visible and renderable
+ * area in the bounding box, and then renders it. This allows us to fill in
+ * preview images of each marker in the marker combobox.
+ */
+Cairo::RefPtr<Cairo::Surface>
+MarkerComboBox::create_marker_image(Geom::IntPoint pixel_size, gchar const *mname,
+ SPDocument *source, Inkscape::Drawing &drawing, unsigned /*visionkey*/, bool checkerboard, bool no_clip, double scale)
+{
+ std::optional<guint32> checkerboard_color;
+ if (checkerboard) {
+ checkerboard_color = _background_color;
+ }
+ int device_scale = get_scale_factor();
+ auto context = get_style_context();
+ Gdk::RGBA fg = context->get_color(get_state_flags());
+
+ return Inkscape::create_marker_image(_combo_id, _sandbox.get(), fg, pixel_size, mname, source,
+ drawing, checkerboard_color, no_clip, scale, device_scale);
+}
+
+// capture background color when styles change
+void MarkerComboBox::on_style_updated() {
+ auto background = _background_color;
+ if (auto wnd = dynamic_cast<Gtk::Window*>(this->get_toplevel())) {
+ auto sc = wnd->get_style_context();
+ auto color = get_background_color(sc);
+ background =
+ gint32(0xff * color.get_red()) << 24 |
+ gint32(0xff * color.get_green()) << 16 |
+ gint32(0xff * color.get_blue()) << 8 |
+ 0xff;
+ }
+
+ auto context = get_style_context();
+ Gdk::RGBA color = context->get_color(get_state_flags());
+ auto foreground =
+ gint32(0xff * color.get_red()) << 24 |
+ gint32(0xff * color.get_green()) << 16 |
+ gint32(0xff * color.get_blue()) << 8 |
+ 0xff;
+ if (foreground != _foreground_color || background != _background_color) {
+ _foreground_color = foreground;
+ _background_color = background;
+ // theme changed?
+ init_combo();
+ }
+}
+
+} // 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/marker-combo-box.h b/src/ui/widget/marker-combo-box.h
new file mode 100644
index 0000000..8e3436d
--- /dev/null
+++ b/src/ui/widget/marker-combo-box.h
@@ -0,0 +1,175 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_SP_MARKER_SELECTOR_NEW_H
+#define SEEN_SP_MARKER_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 <vector>
+
+#include <gtkmm/bin.h>
+#include <gtkmm/box.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/flowbox.h>
+#include <gtkmm/image.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/menubutton.h>
+#include <gtkmm/spinbutton.h>
+#include <gio/gliststore.h>
+
+#include <sigc++/signal.h>
+
+#include "document.h"
+#include "inkscape.h"
+#include "scrollprotected.h"
+#include "display/drawing.h"
+#include "ui/operation-blocker.h"
+
+class SPMarker;
+
+namespace Gtk {
+
+class Container;
+class Adjustment;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * ComboBox-like class for selecting stroke markers.
+ */
+class MarkerComboBox : public Gtk::Bin {
+ using parent_type = Gtk::Bin;
+
+public:
+ MarkerComboBox(Glib::ustring id, int loc);
+ ~MarkerComboBox() override;
+
+ void setDocument(SPDocument *);
+
+ sigc::signal<void ()> changed_signal;
+ sigc::signal<void ()> edit_signal;
+
+ void set_current(SPObject *marker);
+ std::string get_active_marker_uri();
+ bool in_update() { return _update.pending(); };
+ const char* get_id() { return _combo_id.c_str(); };
+ int get_loc() { return _loc; };
+
+ sigc::signal<void()> signal_changed() { return _signal_changed; }
+
+private:
+ struct MarkerItem : Glib::Object {
+ Cairo::RefPtr<Cairo::Surface> pix;
+ SPDocument* source = nullptr;
+ std::string id;
+ std::string label;
+ bool stock = false;
+ bool history = false;
+ bool separator = false;
+ int width = 0;
+ int height = 0;
+
+ bool operator == (const MarkerItem& item) const;
+ };
+
+ SPMarker* get_current() const;
+ Glib::ustring _current_marker_id;
+ // SPMarker* _current_marker = nullptr;
+ sigc::signal<void()> _signal_changed;
+ Glib::RefPtr<Gtk::Builder> _builder;
+ Gtk::FlowBox& _marker_list;
+ Gtk::Label& _marker_name;
+ Glib::RefPtr<Gio::ListStore<MarkerItem>> _marker_store;
+ std::vector<Glib::RefPtr<MarkerItem>> _stock_items;
+ std::vector<Glib::RefPtr<MarkerItem>> _history_items;
+ std::map<Gtk::Widget*, Glib::RefPtr<MarkerItem>> _widgets_to_markers;
+ Gtk::Image& _preview;
+ bool _preview_no_alloc = true;
+ Gtk::Button& _link_scale;
+ Gtk::SpinButton& _angle_btn;
+ Gtk::MenuButton& _menu_btn;
+ Gtk::SpinButton& _scale_x;
+ Gtk::SpinButton& _scale_y;
+ Gtk::CheckButton& _scale_with_stroke;
+ Gtk::SpinButton& _offset_x;
+ Gtk::SpinButton& _offset_y;
+ Gtk::Widget& _input_grid;
+ Gtk::RadioButton& _orient_auto_rev;
+ Gtk::RadioButton& _orient_auto;
+ Gtk::RadioButton& _orient_angle;
+ Gtk::Button& _orient_flip_horz;
+ Gtk::Image& _current_img;
+ Gtk::Button& _edit_marker;
+ bool _scale_linked = true;
+ guint32 _background_color;
+ guint32 _foreground_color;
+ Glib::ustring _combo_id;
+ int _loc;
+ OperationBlocker _update;
+ SPDocument *_document = nullptr;
+ std::unique_ptr<SPDocument> _sandbox;
+ Gtk::CellRendererPixbuf _image_renderer;
+
+ class MarkerColumns : public Gtk::TreeModel::ColumnRecord {
+ public:
+ Gtk::TreeModelColumn<Glib::ustring> label;
+ Gtk::TreeModelColumn<const gchar *> marker; // ustring doesn't work here on windows due to unicode
+ Gtk::TreeModelColumn<gboolean> stock;
+ Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf>> pixbuf;
+ Gtk::TreeModelColumn<gboolean> history;
+ Gtk::TreeModelColumn<gboolean> separator;
+
+ MarkerColumns() {
+ add(label); add(stock); add(marker); add(history); add(separator); add(pixbuf);
+ }
+ };
+ MarkerColumns marker_columns;
+
+ void update_ui(SPMarker* marker, bool select);
+ void update_widgets_from_marker(SPMarker* marker);
+ void update_store();
+ Glib::RefPtr<MarkerItem> add_separator(bool filler);
+ void update_scale_link();
+ Glib::RefPtr<MarkerItem> get_active();
+ Glib::RefPtr<MarkerItem> find_marker_item(SPMarker* marker);
+ void on_style_updated() override;
+ void update_preview(Glib::RefPtr<MarkerItem> marker_item);
+ void update_menu_btn(Glib::RefPtr<MarkerItem> marker_item);
+ void set_active(Glib::RefPtr<MarkerItem> item);
+ void init_combo();
+ void set_history(Gtk::TreeModel::Row match_row);
+ void marker_list_from_doc(SPDocument* source, bool history);
+ std::vector<SPMarker*> get_marker_list(SPDocument* source);
+ void add_markers (std::vector<SPMarker *> const& marker_list, SPDocument *source, gboolean history);
+ void remove_markers (gboolean history);
+ Cairo::RefPtr<Cairo::Surface> create_marker_image(Geom::IntPoint pixel_size, gchar const *mname,
+ SPDocument *source, Inkscape::Drawing &drawing, unsigned /*visionkey*/, bool checkerboard, bool no_clip, double scale);
+ void refresh_after_markers_modified();
+ sigc::connection modified_connection;
+ sigc::connection _idle;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+#endif // SEEN_SP_MARKER_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/notebook-page.cpp b/src/ui/widget/notebook-page.cpp
new file mode 100644
index 0000000..876edb6
--- /dev/null
+++ b/src/ui/widget/notebook-page.cpp
@@ -0,0 +1,48 @@
+// 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)
+ : Gtk::Box(Gtk::ORIENTATION_VERTICAL)
+ , _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..9c9bd06
--- /dev/null
+++ b/src/ui/widget/notebook-page.h
@@ -0,0 +1,57 @@
+// 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::Box
+{
+public:
+
+ /**
+ * 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..23daa04
--- /dev/null
+++ b/src/ui/widget/object-composite-settings.cpp
@@ -0,0 +1,302 @@
+// 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 "object-composite-settings.h"
+
+#include <utility>
+
+#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 "object/filters/blend.h"
+#include "svg/css-ostringstream.h"
+#include "ui/widget/style-subject.h"
+
+constexpr double BLUR_MULTIPLIER = 4.0;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+ObjectCompositeSettings::ObjectCompositeSettings(Glib::ustring icon_name, char const *history_prefix, int flags)
+: Gtk::Box(Gtk::ORIENTATION_VERTICAL),
+ _icon_name(std::move(icon_name)),
+ _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));
+ }
+}
+
+// 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;
+
+ 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 (!is<SPItem>(i)) {
+ continue;
+ }
+ auto item = cast<SPItem>(i);
+ SPStyle *style = item->style;
+ g_assert(style != nullptr);
+ bool change_blend = set_blend_mode(item, _filter_modifier.get_blend_mode());
+
+ if (radius == 0 && item->style->filter.set && item->style->getFilter()
+ && filter_is_single_gaussian_blur(item->style->getFilter())) {
+ remove_filter(item, false);
+ } else if (radius != 0) {
+ SPFilter *filter = modify_filter_gaussian_blur_from_item(document, item, radius);
+ filter->update_filter_region(item);
+ sp_style_set_property_url(item, "filter", filter, false);
+ }
+ if (change_blend) {
+ ; // update done already
+ } else {
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
+ }
+ }
+
+ DocumentUndo::maybeDone(document, _blur_tag.c_str(), _("Change blur/blend filter"), _icon_name);
+
+ _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(), _("Change opacity"), _icon_name);
+
+ _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(), _("Change isolation"), _icon_name);
+
+ _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..bb2ef85
--- /dev/null
+++ b/src/ui/widget/object-composite-settings.h
@@ -0,0 +1,78 @@
+// 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 "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::Box {
+public:
+ ObjectCompositeSettings(Glib::ustring icon_name, char const *history_prefix, int flags);
+ ~ObjectCompositeSettings() override;
+
+ void setSubject(StyleSubject *subject);
+
+private:
+ Glib::ustring _icon_name; // Used by History dialog.
+
+ Glib::ustring _blend_tag;
+ Glib::ustring _blur_tag;
+ Glib::ustring _opacity_tag;
+ Glib::ustring _isolation_tag;
+
+ StyleSubject *_subject = nullptr;
+
+ 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/objects-dialog-cells.cpp b/src/ui/widget/objects-dialog-cells.cpp
new file mode 100644
index 0000000..00ee641
--- /dev/null
+++ b/src/ui/widget/objects-dialog-cells.cpp
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Mike Kowalski
+ * Martin Owens
+ *
+ * Copyright (C) 2021-2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/widget/objects-dialog-cells.h"
+#include "color-rgba.h"
+#include "preferences.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A colored tag cell which indicates which layer an object is in.
+ */
+ColorTagRenderer::ColorTagRenderer() :
+ Glib::ObjectBase(typeid(CellRenderer)),
+ Gtk::CellRenderer(),
+ _property_color(*this, "tagcolor", 0),
+ _property_hover(*this, "taghover", false)
+{
+ property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE;
+
+ int dummy_width;
+ // height size is not critical
+ Gtk::IconSize::lookup(Gtk::ICON_SIZE_MENU, dummy_width, _height);
+}
+
+void ColorTagRenderer::render_vfunc(const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags) {
+ cr->rectangle(cell_area.get_x(), cell_area.get_y(), cell_area.get_width(), cell_area.get_height());
+ ColorRGBA color(_property_color.get_value());
+ cr->set_source_rgb(color[0], color[1], color[2]);
+ cr->fill();
+ if (_property_hover.get_value()) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring themeiconname = prefs->getString("/theme/iconTheme", prefs->getString("/theme/defaultIconTheme", ""));
+ guint32 colorsetbase = prefs->getUInt("/theme/" + themeiconname + "/symbolicBaseColor", 0x2E3436ff);
+ double r = ((colorsetbase >> 24) & 0xFF) / 255.0;
+ double g = ((colorsetbase >> 16) & 0xFF) / 255.0;
+ double b = ((colorsetbase >> 8) & 0xFF) / 255.0;
+ cr->set_source_rgba(r, g, b, 0.6);
+ cr->rectangle(background_area.get_x() + 0.5, background_area.get_y() + 0.5, background_area.get_width() - 1.0, background_area.get_height() - 1.0);
+ cr->set_line_width(1.0);
+ cr->stroke();
+ }
+}
+
+void ColorTagRenderer::get_preferred_width_vfunc(Gtk::Widget& widget, int& min_w, int& nat_w) const {
+ min_w = nat_w = _width;
+}
+
+void ColorTagRenderer::get_preferred_height_vfunc(Gtk::Widget& widget, int& min_h, int& nat_h) const {
+ min_h = 1;
+ nat_h = _height;
+}
+
+bool ColorTagRenderer::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_clicked.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/objects-dialog-cells.h b/src/ui/widget/objects-dialog-cells.h
new file mode 100644
index 0000000..448ea1b
--- /dev/null
+++ b/src/ui/widget/objects-dialog-cells.h
@@ -0,0 +1,75 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_UI_WIDGET_OBJECTS_CELLS_H
+#define SEEN_UI_WIDGET_OBJECTS_CELLS_H
+/*
+ * Authors:
+ * Mike Kowalski
+ * Martin Owens
+ *
+ * Copyright (C) 2021-2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/cellrenderer.h>
+#include <gtkmm/widget.h>
+#include <glibmm/property.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class ColorTagRenderer : public Gtk::CellRenderer {
+public:
+ ColorTagRenderer();
+ ~ColorTagRenderer() override = default;
+
+ Glib::PropertyProxy<unsigned int> property_color() {
+ return _property_color.get_proxy();
+ }
+ Glib::PropertyProxy<bool> property_hover() {
+ return _property_hover.get_proxy();
+ }
+ sigc::signal<void (const Glib::ustring&)> signal_clicked() {
+ return _signal_clicked;
+ }
+
+ int get_width() const { return _width; }
+
+private:
+ 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;
+
+ int _width = 8;
+ int _height;
+ Glib::Property<unsigned int> _property_color;
+ Glib::Property<bool> _property_hover;
+ sigc::signal<void (const Glib::ustring&)> _signal_clicked;
+};
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // SEEN_UI_WIDGET_OBJECTS_CELLS_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/oklab-color-wheel.cpp b/src/ui/widget/oklab-color-wheel.cpp
new file mode 100644
index 0000000..4324b4e
--- /dev/null
+++ b/src/ui/widget/oklab-color-wheel.cpp
@@ -0,0 +1,309 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file OKHSL color wheel widget implementation.
+ */
+/*
+ * Authors:
+ * Rafael Siejakowski <rs@rs-math.net>
+ *
+ * Copyright (C) 2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/widget/oklab-color-wheel.h"
+
+#include <algorithm>
+
+#include "display/cairo-utils.h"
+#include "oklab.h"
+
+namespace Inkscape::UI::Widget {
+
+OKWheel::OKWheel()
+{
+ // Set to black
+ _values[H] = 0;
+ _values[S] = 0;
+ _values[L] = 0;
+}
+
+void OKWheel::setRgb(double r, double g, double b, bool)
+{
+ using namespace Oklab;
+ auto [h, s, l] = oklab_to_okhsl(rgb_to_oklab({ r, g, b }));
+ _values[H] = h * 2.0 * M_PI;
+ _values[S] = s;
+ bool const changed_lightness = _values[L] != l;
+ _values[L] = l;
+ if (changed_lightness) {
+ _updateChromaBounds();
+ _redrawDisc();
+ }
+}
+
+void OKWheel::getRgb(double *red, double *green, double *blue) const
+{
+ using namespace Oklab;
+ auto [r, g, b] = oklab_to_rgb(okhsl_to_oklab({ _values[H] / (2.0 * M_PI), _values[S], _values[L] }));
+ *red = r;
+ *green = g;
+ *blue = b;
+}
+
+guint32 OKWheel::getRgb() const
+{
+ guint32 result = 0x0;
+ double rgb[3];
+ getRgbV(rgb);
+ for (auto component : rgb) {
+ result <<= 8;
+ result |= SP_COLOR_F_TO_U(component);
+ }
+ return result;
+}
+
+/** @brief Compute the chroma bounds around the picker disc.
+ *
+ * Calculates the maximum absolute Lch chroma along rays emanating
+ * from the center of the picker disc. CHROMA_BOUND_SAMPLES evenly
+ * spaced rays will be used. The result is stored in _bounds.
+ */
+void OKWheel::_updateChromaBounds()
+{
+ double const angle_step = 360.0 / CHROMA_BOUND_SAMPLES;
+ double hue_angle_deg = 0.0;
+ for (unsigned i = 0; i < CHROMA_BOUND_SAMPLES; i++) {
+ _bounds[i] = Oklab::max_chroma(_values[L], hue_angle_deg);
+ hue_angle_deg += angle_step;
+ }
+}
+
+/** @brief Update the size of the color disc and margins
+ * depending on the widget's allocation.
+ *
+ * @return Whether the colorful disc background needs to be regenerated.
+ */
+bool OKWheel::_updateDimensions()
+{
+ auto allocation = get_allocation();
+ auto width = allocation.get_width();
+ auto height = allocation.get_height();
+ double new_radius = 0.5 * std::min(width, height);
+ // Allow the halo to fit at coordinate extrema.
+ new_radius -= HALO_RADIUS + 0.5 * HALO_STROKE;
+ bool disc_needs_redraw = (_disc_radius != new_radius);
+ _disc_radius = new_radius;
+ _margin = {std::max(0.0, 0.5 * (width - 2.0 * _disc_radius)),
+ std::max(0.0, 0.5 * (height - 2.0 * _disc_radius))};
+ return disc_needs_redraw;
+}
+
+/** @brief Compute the ARGB32 color for a point inside the picker disc.
+ *
+ * The picker disc is viewed as the unit disc in the xy-plane, with
+ * the y-axis pointing up. If the passed point lies outside of the unit
+ * disc, the returned color is the same as for a point rescaled to the
+ * unit circle (outermost possible color in that direction).
+ *
+ * @param point A point in the normalized disc coordinates.
+ * @return a Cairo-compatible ARGB32 color.
+ */
+uint32_t OKWheel::_discColor(Geom::Point const &point) const
+{
+ using namespace Oklab;
+ using Display::AssembleARGB32;
+
+ double saturation = point.length();
+ if (saturation == 0.0) {
+ auto [r, g, b] = oklab_to_rgb({ _values[L], 0.0, 0.0 });
+ return AssembleARGB32(0xFF, (guint)(r * 255.5), (guint)(g * 255.5), (guint)(b * 255.5));
+ } else if (saturation > 1.0) {
+ saturation = 1.0;
+ }
+
+ double const hue_radians = Geom::Angle(Geom::atan2(point)).radians0();
+
+ // Find the precomputed chroma bounds on both sides of this angle.
+ unsigned previous_sample = std::floor(hue_radians * 0.5 * CHROMA_BOUND_SAMPLES / M_PI);
+ if (previous_sample >= CHROMA_BOUND_SAMPLES) {
+ previous_sample = 0;
+ }
+ unsigned const next_sample = (previous_sample == CHROMA_BOUND_SAMPLES - 1) ? 0 : previous_sample + 1;
+ double const previous_sample_angle = 2.0 * M_PI * previous_sample / CHROMA_BOUND_SAMPLES;
+ double const angle_delta = hue_radians - previous_sample_angle;
+ double const t = angle_delta * 0.5 * CHROMA_BOUND_SAMPLES / M_PI;
+ double const chroma_bound_estimate = Geom::lerp(t, _bounds[previous_sample], _bounds[next_sample]);
+ double const absolute_chroma = chroma_bound_estimate * saturation;
+
+ auto [r, g, b] = oklab_to_rgb(oklch_radians_to_oklab({ _values[L], absolute_chroma, hue_radians }));
+ return AssembleARGB32(0xFF, (guint)(r * 255.5), (guint)(g * 255.5), (guint)(b * 255.5));
+}
+
+/** @brief Returns the position of the current color in the coordinates
+ * of the picker wheel.
+ *
+ * The picker wheel is inscribed in a square with side length 2 * _disc_radius.
+ * The point (0, 0) corresponds to the center of the disc; y-axis points down.
+ */
+Geom::Point OKWheel::_curColorWheelCoords() const
+{
+ Geom::Point result;
+ Geom::sincos(_values[H], result.y(), result.x());
+ result *= _values[S];
+ return result * Geom::Scale(_disc_radius, -_disc_radius);
+}
+
+/** @brief Draw the widget into the Cairo context. */
+bool OKWheel::on_draw(Cairo::RefPtr<Cairo::Context> const &cr)
+{
+ if(_updateDimensions()) {
+ _redrawDisc();
+ }
+
+ cr->save();
+ cr->set_antialias(Cairo::ANTIALIAS_SUBPIXEL);
+
+ // Draw the colorful disc background from the cached pixbuf,
+ // clipping to a geometric circle (avoids aliasing).
+ cr->translate(_margin[Geom::X], _margin[Geom::Y]);
+ cr->move_to(2 * _disc_radius, _disc_radius);
+ cr->arc(_disc_radius, _disc_radius, _disc_radius, 0.0, 2.0 * M_PI);
+ cr->close_path();
+ cr->set_source(_disc, 0, 0);
+ cr->fill();
+
+ // Draw the halo around the current color.
+ {
+ auto const where = _curColorWheelCoords();
+ cr->translate(_disc_radius, _disc_radius);
+ cr->move_to(where.x() + HALO_RADIUS, where.y());
+ cr->arc(where.x(), where.y(), HALO_RADIUS, 0.0, 2.0 * M_PI);
+ cr->close_path();
+ // Fill the halo with the current color.
+ {
+ double r, g, b;
+ getRgb(&r, &g, &b);
+ cr->set_source_rgba(r, g, b, 1.0);
+ }
+ cr->fill_preserve();
+
+ // Stroke the border of the halo.
+ {
+ auto [gray, alpha] = Hsluv::get_contrasting_color(_values[L]);
+ cr->set_source_rgba(gray, gray, gray, alpha);
+ }
+ cr->set_line_width(HALO_STROKE);
+ cr->stroke();
+ }
+ cr->restore();
+ return true;
+}
+
+/** @brief Recreate the pixel buffer containing the colourful disc. */
+void OKWheel::_redrawDisc()
+{
+ int const size = std::ceil(2.0 * _disc_radius);
+ _pixbuf.resize(4 * size * size);
+
+ double const radius = 0.5 * size;
+ double const inverse_radius = 1.0 / radius;
+
+ // Fill buffer with (<don't care>, R, G, B) values.
+ uint32_t *pos = (uint32_t *)(_pixbuf.data());
+ for (int y = 0; y < size; y++) {
+ // Convert (x, y) to a coordinate system where the
+ // disc is the unit disc and the y-axis points up.
+ double const normalized_y = inverse_radius * (radius - y);
+ for (int x = 0; x < size; x++) {
+ auto const pt = Geom::Point(inverse_radius * (x - radius), normalized_y);
+ *pos++ = _discColor(pt);
+ }
+ }
+
+ int const stride = Cairo::ImageSurface::format_stride_for_width(Cairo::FORMAT_RGB24, size);
+ _disc = Cairo::ImageSurface::create(_pixbuf.data(), Cairo::FORMAT_RGB24, size, size, stride);
+}
+
+/** @brief Convert widget (event) coordinates to an abstract coordinate system
+ * in which the picker disc is the unit disc and the y-axis points up.
+ */
+Geom::Point OKWheel::_event2abstract(Geom::Point const &event_pt) const
+{
+ auto result = event_pt - _margin - Geom::Point(_disc_radius, _disc_radius);
+ double const scale = 1.0 / _disc_radius;
+ return result * Geom::Scale(scale, -scale);
+}
+
+/** @brief Set the current color based on a point on the wheel.
+ *
+ * @param pt A point in the abstract coordinate system in which the picker
+ * disc is the unit disc and the y-axis points up.
+ */
+void OKWheel::_setColor(Geom::Point const &pt)
+{
+ _values[S] = std::clamp(pt.length(), 0.0, 1.0);
+ Geom::Angle clicked_hue = _values[S] ? Geom::atan2(pt) : 0.0;
+ _values[H] = clicked_hue.radians0();
+ _signal_color_changed.emit();
+ queue_draw();
+}
+
+/** @brief Handle a left mouse click on the widget.
+ *
+ * @param pt The clicked point expressed in the coordinate system in which
+ * the picker disc is the unit disc and the y-axis points up.
+ * @return Whether the click has been handled.
+ */
+bool OKWheel::_onClick(Geom::Point const &pt)
+{
+ auto r = pt.length();
+ if (r > 1.0) { // Clicked outside the disc, no cookie.
+ return false;
+ }
+ _adjusting = true;
+ _setColor(pt);
+ return true;
+}
+
+/** @brief Handle a button press event. */
+bool OKWheel::on_button_press_event(GdkEventButton *event)
+{
+ if (event->button == 1) {
+ // Convert the click coordinates to the abstract coords in which
+ // the picker disc is the unit disc in the xy-plane.
+ return _onClick(_event2abstract({event->x, event->y}));
+ }
+ // TODO: add a context menu to copy out the CSS4 color values.
+ return false;
+}
+
+/** @brief Handle a button release event. */
+bool OKWheel::on_button_release_event(GdkEventButton *event)
+{
+ _adjusting = false;
+ return true;
+}
+
+/** @brief Handle a drag (motion notify event). */
+bool OKWheel::on_motion_notify_event(GdkEventMotion *event)
+{
+ if (!_adjusting) {
+ return false;
+ }
+ _setColor(_event2abstract({event->x, event->y}));
+ return true;
+}
+
+} // namespace Inkscape::UI::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:textwidth=99 : \ No newline at end of file
diff --git a/src/ui/widget/oklab-color-wheel.h b/src/ui/widget/oklab-color-wheel.h
new file mode 100644
index 0000000..f1a55fd
--- /dev/null
+++ b/src/ui/widget/oklab-color-wheel.h
@@ -0,0 +1,83 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file OKHSL color wheel widget, based on the OKLab/OKLch color space.
+ */
+/*
+ * Authors:
+ * Rafael Siejakowski <rs@rs-math.net>
+ *
+ * Copyright (C) 2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_OKLAB_COLOR_WHEEL_H
+#define SEEN_OKLAB_COLOR_WHEEL_H
+
+#include "ui/widget/ink-color-wheel.h"
+
+namespace Inkscape::UI::Widget {
+
+/** @brief The color wheel used in the OKHSL picker. */
+class OKWheel : public ColorWheel
+{
+public:
+ OKWheel();
+ ~OKWheel() override = default;
+
+ /** @brief Set the displayed color to the specified gamma-compressed sRGB color. */
+ void setRgb(double r, double g, double b, bool overrideHue = true) override;
+
+ /** @brief Get the gamma-compressed sRGB color from the picker wheel. */
+ void getRgb(double *r, double *g, double *b) const override;
+ void getRgbV(double *rgb) const override { getRgb(rgb, rgb + 1, rgb + 2); }
+ guint32 getRgb() const override;
+
+protected:
+ bool on_draw(Cairo::RefPtr<Cairo::Context> const &cr) override;
+
+private:
+ static unsigned constexpr H = 0, S = 1, L = 2; ///< Indices into _values
+
+ /** How many samples for the chroma bounds to use for the color disc.
+ * A larger value produces a nicer gradient at the cost of slower performance.
+ */
+ static unsigned constexpr CHROMA_BOUND_SAMPLES = 120;
+ static double constexpr HALO_RADIUS = 4.5; ///< Radius of the halo around the current color.
+ static double constexpr HALO_STROKE = 1.5; ///< Width of the halo's stroke.
+
+ Geom::Point _curColorWheelCoords() const;
+ uint32_t _discColor(Geom::Point const &point) const;
+ Geom::Point _event2abstract(Geom::Point const &point) const;
+ void _redrawDisc();
+ void _setColor(Geom::Point const &pt);
+ void _updateChromaBounds();
+ bool _updateDimensions();
+
+ // Event handlers
+ bool on_button_press_event(GdkEventButton *event) override;
+ bool _onClick(Geom::Point const &unit_pos);
+ bool on_button_release_event(GdkEventButton *event) override;
+ bool on_motion_notify_event(GdkEventMotion *event) override;
+
+ double _disc_radius = 1.0;
+ Geom::Point _margin;
+ Cairo::RefPtr<Cairo::ImageSurface> _disc;
+ std::vector<uint8_t> _pixbuf;
+ std::array<double, CHROMA_BOUND_SAMPLES> _bounds;
+};
+
+} // namespace Inkscape::UI::Widget
+
+#endif // SEEN_OKLAB_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 : \ No newline at end of file
diff --git a/src/ui/widget/optglarea.cpp b/src/ui/widget/optglarea.cpp
new file mode 100644
index 0000000..72ec362
--- /dev/null
+++ b/src/ui/widget/optglarea.cpp
@@ -0,0 +1,127 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <cassert>
+#include "optglarea.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+OptGLArea::OptGLArea()
+{
+ set_app_paintable(true); // No problem for GTK4 port since this whole widget will be deleted.
+ opengl_enabled = false;
+}
+
+void OptGLArea::on_realize()
+{
+ Gtk::DrawingArea::on_realize();
+ if (opengl_enabled) init_opengl();
+}
+
+void OptGLArea::on_unrealize()
+{
+ if (context) {
+ if (framebuffer) {
+ context->make_current();
+ delete_framebuffer();
+ }
+ if (context == Gdk::GLContext::get_current()) {
+ Gdk::GLContext::clear_current(); // ?
+ }
+ context.reset();
+ }
+ Gtk::DrawingArea::on_unrealize();
+}
+
+void OptGLArea::on_size_allocate(Gtk::Allocation &allocation)
+{
+ Gtk::DrawingArea::on_size_allocate(allocation);
+ if (get_realized()) need_resize = true;
+}
+
+void OptGLArea::set_opengl_enabled(bool enabled)
+{
+ if (opengl_enabled == enabled) return;
+ opengl_enabled = enabled;
+ if (opengl_enabled && get_realized()) init_opengl();
+}
+
+void OptGLArea::init_opengl()
+{
+ context = create_context();
+ if (!context) opengl_enabled = false;
+ framebuffer = 0;
+ need_resize = true;
+}
+
+void OptGLArea::make_current()
+{
+ assert(context);
+ context->make_current();
+}
+
+bool OptGLArea::on_draw(const Cairo::RefPtr<Cairo::Context> &cr)
+{
+ if (opengl_enabled) {
+ context->make_current();
+
+ if (!framebuffer) {
+ create_framebuffer();
+ }
+
+ if (need_resize) {
+ resize_framebuffer();
+ need_resize = false;
+ }
+
+ paint_widget(cr);
+
+ int s = get_scale_factor();
+ int w = get_allocated_width() * s;
+ int h = get_allocated_height() * s;
+ gdk_cairo_draw_from_gl(cr->cobj(), get_window()->gobj(), renderbuffer, GL_RENDERBUFFER, s, 0, 0, w, h);
+
+ context->make_current(); // ?
+ } else {
+ paint_widget(cr);
+ }
+
+ return true;
+}
+
+void OptGLArea::bind_framebuffer() const
+{
+ assert(context);
+ glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, stencilbuffer);
+}
+
+void OptGLArea::create_framebuffer()
+{
+ glGenFramebuffers (1, &framebuffer);
+ glGenRenderbuffers(1, &renderbuffer);
+ glGenRenderbuffers(1, &stencilbuffer);
+}
+
+void OptGLArea::delete_framebuffer()
+{
+ glDeleteRenderbuffers(1, &renderbuffer);
+ glDeleteRenderbuffers(1, &stencilbuffer);
+ glDeleteFramebuffers (1, &framebuffer);
+}
+
+void OptGLArea::resize_framebuffer() const
+{
+ int s = get_scale_factor();
+ int w = get_allocated_width() * s;
+ int h = get_allocated_height() * s;
+ glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
+ glRenderbufferStorage(GL_RENDERBUFFER, GL_RGB8, w, h);
+ glBindRenderbuffer(GL_RENDERBUFFER, stencilbuffer);
+ glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, w, h);
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
diff --git a/src/ui/widget/optglarea.h b/src/ui/widget/optglarea.h
new file mode 100644
index 0000000..fa71afc
--- /dev/null
+++ b/src/ui/widget/optglarea.h
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_UI_WIDGET_OPTGLAREA_H
+#define INKSCAPE_UI_WIDGET_OPTGLAREA_H
+
+#include <gtkmm.h>
+#include <epoxy/gl.h>
+
+namespace Cairo {
+class Context;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A widget that can dynamically switch between a Gtk::DrawingArea and a Gtk::GLArea.
+ * Based on the GTK source code for both widgets.
+ */
+class OptGLArea : public Gtk::DrawingArea
+{
+public:
+ OptGLArea();
+
+ /**
+ * Set whether OpenGL is enabled. Initially it is disabled. Upon enabling it,
+ * create_context will be called as soon as the widget is realized. If
+ * context creation fails, OpenGL will be disabled again.
+ */
+ void set_opengl_enabled(bool);
+ bool get_opengl_enabled() const { return opengl_enabled; }
+
+ /**
+ * Call before doing any OpenGL operations to make the context current.
+ * Automatically done before calling opengl_render.
+ */
+ void make_current();
+
+ /**
+ * Call before rendering to the widget to bind the widget's framebuffer.
+ */
+ void bind_framebuffer() const;
+
+protected:
+ void on_realize() override;
+ void on_unrealize() override;
+ void on_size_allocate(Gtk::Allocation&) override;
+ bool on_draw(const Cairo::RefPtr<Cairo::Context>&) final;
+
+ /**
+ * Reimplement to create the desired OpenGL context. Return nullptr on error.
+ */
+ virtual Glib::RefPtr<Gdk::GLContext> create_context() = 0;
+
+ /**
+ * Reimplement to render the widget. The Cairo context is only for when OpenGL is disabled.
+ */
+ virtual void paint_widget(const Cairo::RefPtr<Cairo::Context>&) {}
+
+private:
+ void init_opengl();
+ void create_framebuffer();
+ void delete_framebuffer();
+ void resize_framebuffer() const;
+
+ Glib::RefPtr<Gdk::GLContext> context;
+
+ bool opengl_enabled;
+ bool need_resize;
+
+ GLuint framebuffer;
+ GLuint renderbuffer;
+ GLuint stencilbuffer;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_OPTGLAREA_H
diff --git a/src/ui/widget/page-properties.cpp b/src/ui/widget/page-properties.cpp
new file mode 100644
index 0000000..927544b
--- /dev/null
+++ b/src/ui/widget/page-properties.cpp
@@ -0,0 +1,525 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ *
+ * Document properties widget: viewbox, document size, colors
+ */
+/*
+ * Authors:
+ * Mike Kowalski
+ *
+ * Copyright (C) 2021 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+#include <gtkmm/box.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/button.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/expander.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/label.h>
+#include <gtkmm/menu.h>
+#include <gtkmm/menubutton.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/spinbutton.h>
+#include <gtkmm/togglebutton.h>
+
+#include <type_traits>
+
+#include "page-properties.h"
+#include "page-size-preview.h"
+#include "ui/widget/spinbutton.h"
+#include "util/paper.h"
+#include "ui/widget/registry.h"
+#include "ui/widget/color-picker.h"
+#include "ui/widget/unit-menu.h"
+#include "ui/builder-utils.h"
+#include "ui/operation-blocker.h"
+
+using Inkscape::UI::create_builder;
+using Inkscape::UI::get_widget;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+void show_widget(Gtk::Widget& widget, bool show) {
+ if (show) {
+ widget.show();
+ }
+ else {
+ widget.hide();
+ }
+};
+
+const char* g_linked = "entries-linked-symbolic";
+const char* g_unlinked = "entries-unlinked-symbolic";
+
+#define GET(prop, id) prop(get_widget<std::remove_reference_t<decltype(prop)>>(_builder, id))
+#define GETD(prop, id) prop(get_derived_widget<std::remove_reference_t<decltype(prop)>>(_builder, id))
+
+class PagePropertiesBox : public PageProperties {
+public:
+ PagePropertiesBox() :
+ _builder(create_builder("page-properties.glade")),
+ GET(_main_grid, "main-grid"),
+ GET(_left_grid, "left-grid"),
+ GETD(_page_width, "page-width"),
+ GETD(_page_height, "page-height"),
+ GET(_portrait, "page-portrait"),
+ GET(_landscape, "page-landscape"),
+ GETD(_scale_x, "scale-x"),
+ GET(_doc_units, "user-units"),
+ GET(_unsupported_size, "unsupported"),
+ GET(_nonuniform_scale, "nonuniform-scale"),
+ GETD(_viewbox_x, "viewbox-x"),
+ GETD(_viewbox_y, "viewbox-y"),
+ GETD(_viewbox_width, "viewbox-width"),
+ GETD(_viewbox_height, "viewbox-height"),
+ GET(_page_templates_menu, "page-templates-menu"),
+ GET(_template_name, "page-template-name"),
+ GET(_preview_box, "preview-box"),
+ GET(_checkerboard, "checkerboard"),
+ GET(_antialias, "use-antialias"),
+ GET(_clip_to_page, "clip-to-page"),
+ GET(_page_label_style, "page-label-style"),
+ GET(_border, "border"),
+ GET(_border_on_top, "border-top"),
+ GET(_shadow, "shadow"),
+ GET(_link_width_height, "link-width-height"),
+ GET(_viewbox_expander, "viewbox-expander"),
+ GET(_linked_viewbox_scale, "linked-scale-img")
+ {
+#undef GET
+#undef GETD
+
+ _backgnd_color_picker = std::make_unique<ColorPicker>(
+ _("Background color"), "", 0xffffff00, true,
+ &get_widget<Gtk::Button>(_builder, "background-color"));
+ _backgnd_color_picker->use_transparency(false);
+
+ _border_color_picker = std::make_unique<ColorPicker>(
+ _("Border and shadow color"), "", 0x0000001f, true,
+ &get_widget<Gtk::Button>(_builder, "border-color"));
+
+ _desk_color_picker = std::make_unique<ColorPicker>(
+ _("Desk color"), "", 0xd0d0d0ff, true,
+ &get_widget<Gtk::Button>(_builder, "desk-color"));
+ _desk_color_picker->use_transparency(false);
+
+ for (auto element : {Color::Background, Color::Border, Color::Desk}) {
+ get_color_picker(element).connectChanged([=](guint rgba) {
+ update_preview_color(element, rgba);
+ if (_update.pending()) return;
+ _signal_color_changed.emit(rgba, element);
+ });
+ }
+
+ _builder->get_widget_derived("display-units", _display_units);
+ _display_units->setUnitType(UNIT_TYPE_LINEAR);
+ _display_units->signal_changed().connect([=](){ set_display_unit(); });
+
+ _builder->get_widget_derived("page-units", _page_units);
+ _page_units->setUnitType(UNIT_TYPE_LINEAR);
+ _current_page_unit = _page_units->getUnit();
+ _page_units->signal_changed().connect([=](){ set_page_unit(); });
+
+ for (auto&& page : PaperSize::getPageSizes()) {
+ auto item = Gtk::manage(new Gtk::MenuItem(page.getDescription(false)));
+ item->show();
+ _page_templates_menu.append(*item);
+ item->signal_activate().connect([=](){ set_page_template(page); });
+ }
+
+ _preview->set_hexpand();
+ _preview->set_vexpand();
+ _preview_box.add(*_preview);
+
+ for (auto check : {Check::Border, Check::Shadow, Check::Checkerboard, Check::BorderOnTop, Check::AntiAlias, Check::ClipToPage, Check::PageLabelStyle}) {
+ auto checkbutton = &get_checkbutton(check);
+ checkbutton->signal_toggled().connect([=](){ fire_checkbox_toggled(*checkbutton, check); });
+ }
+ _border.signal_toggled().connect([=](){
+ _preview->draw_border(_border.get_active());
+ });
+ _shadow.signal_toggled().connect([=](){
+ //
+ _preview->enable_drop_shadow(_shadow.get_active());
+ });
+ _checkerboard.signal_toggled().connect([=](){
+ _preview->enable_checkerboard(_checkerboard.get_active());
+ });
+
+ _viewbox_expander.property_expanded().signal_changed().connect([=](){
+ // hide/show viewbox controls
+ show_viewbox(_viewbox_expander.get_expanded());
+ });
+ show_viewbox(_viewbox_expander.get_expanded());
+
+ _link_width_height.signal_clicked().connect([=](){
+ // toggle size link
+ _locked_size_ratio = !_locked_size_ratio;
+ // set image
+ _link_width_height.set_image_from_icon_name(_locked_size_ratio && _size_ratio > 0 ? g_linked : g_unlinked, Gtk::ICON_SIZE_LARGE_TOOLBAR);
+ });
+ _link_width_height.set_image_from_icon_name(g_unlinked, Gtk::ICON_SIZE_LARGE_TOOLBAR);
+ // set image for linked scale
+ _linked_viewbox_scale.set_from_icon_name(g_linked, Gtk::ICON_SIZE_LARGE_TOOLBAR);
+
+ // report page size changes
+ _page_width .signal_value_changed().connect([=](){ set_page_size_linked(true); });
+ _page_height.signal_value_changed().connect([=](){ set_page_size_linked(false); });
+ // enforce uniform scale thru viewbox
+ _viewbox_width. signal_value_changed().connect([=](){ set_viewbox_size_linked(true); });
+ _viewbox_height.signal_value_changed().connect([=](){ set_viewbox_size_linked(false); });
+
+ _landscape.signal_toggled().connect([=](){ if (_landscape.get_active()) swap_width_height(); });
+ _portrait .signal_toggled().connect([=](){ if (_portrait .get_active()) swap_width_height(); });
+
+ for (auto dim : {Dimension::Scale, Dimension::ViewboxPosition}) {
+ auto pair = get_dimension(dim);
+ auto b1 = &pair.first;
+ auto b2 = &pair.second;
+ if (dim == Dimension::Scale) {
+ // uniform scale: report the same x and y
+ b1->signal_value_changed().connect([=](){ fire_value_changed(*b1, *b1, nullptr, dim); });
+ }
+ else {
+ b1->signal_value_changed().connect([=](){ fire_value_changed(*b1, *b2, nullptr, dim); });
+ b2->signal_value_changed().connect([=](){ fire_value_changed(*b1, *b2, nullptr, dim); });
+ }
+ }
+
+ auto& page_resize = get_widget<Gtk::Button>(_builder, "page-resize");
+ page_resize.signal_clicked().connect([=](){ _signal_resize_to_fit.emit(); });
+
+ add(_main_grid);
+ show();
+ }
+
+private:
+
+ void show_viewbox(bool show_widgets) {
+ auto show = [=](Gtk::Widget* w) { show_widget(*w, show_widgets); };
+
+ for (auto&& widget : _left_grid.get_children()) {
+ if (widget->get_style_context()->has_class("viewbox")) {
+ show(widget);
+ }
+ }
+ }
+
+ void update_preview_color(Color element, guint rgba) {
+ switch (element) {
+ case Color::Desk: _preview->set_desk_color(rgba); break;
+ case Color::Border: _preview->set_border_color(rgba); break;
+ case Color::Background: _preview->set_page_color(rgba); break;
+ }
+ }
+
+ void set_page_template(const PaperSize& page) {
+ if (_update.pending()) return;
+
+ {
+ auto scoped(_update.block());
+ auto width = page.width;
+ auto height = page.height;
+ if (_landscape.get_active() != (width > height)) {
+ std::swap(width, height);
+ }
+ _page_width.set_value(width);
+ _page_height.set_value(height);
+ _page_units->setUnit(page.unit->abbr);
+ _doc_units.set_text(page.unit->abbr);
+ _current_page_unit = _page_units->getUnit();
+ if (width > 0 && height > 0) {
+ _size_ratio = width / height;
+ }
+ }
+ set_page_size(true);
+ }
+
+ void changed_linked_value(bool width_changing, Gtk::SpinButton& wedit, Gtk::SpinButton& hedit) {
+ if (_size_ratio > 0) {
+ auto scoped(_update.block());
+ if (width_changing) {
+ auto width = wedit.get_value();
+ hedit.set_value(width / _size_ratio);
+ }
+ else {
+ auto height = hedit.get_value();
+ wedit.set_value(height * _size_ratio);
+ }
+ }
+ }
+
+ void set_viewbox_size_linked(bool width_changing) {
+ if (_update.pending()) return;
+
+ if (_scale_is_uniform) {
+ // viewbox size - width and height always linked to make scaling uniform
+ changed_linked_value(width_changing, _viewbox_width, _viewbox_height);
+ }
+
+ auto width = _viewbox_width.get_value();
+ auto height = _viewbox_height.get_value();
+ _signal_dimmension_changed.emit(width, height, nullptr, Dimension::ViewboxSize);
+ }
+
+ void set_page_size_linked(bool width_changing) {
+ if (_update.pending()) return;
+
+ // if size ratio is locked change the other dimension too
+ if (_locked_size_ratio) {
+ changed_linked_value(width_changing, _page_width, _page_height);
+ }
+ set_page_size();
+ }
+
+ void set_page_size(bool template_selected = false) {
+ auto pending = _update.pending();
+
+ auto scoped(_update.block());
+
+ auto unit = _page_units->getUnit();
+ auto width = _page_width.get_value();
+ auto height = _page_height.get_value();
+ _preview->set_page_size(width, height);
+ if (width != height) {
+ (width > height ? _landscape : _portrait).set_active();
+ _portrait.set_sensitive();
+ _landscape.set_sensitive();
+ }
+ else {
+ _portrait.set_sensitive(false);
+ _landscape.set_sensitive(false);
+ }
+ if (width > 0 && height > 0) {
+ _size_ratio = width / height;
+ }
+
+ auto templ = find_page_template(width, height, *unit);
+ _template_name.set_label(templ && !templ->name.empty() ? _(templ->name.c_str()) : _("Custom"));
+
+ if (!pending) {
+ _signal_dimmension_changed.emit(width, height, unit, template_selected ? Dimension::PageTemplate : Dimension::PageSize);
+ }
+ }
+
+ void swap_width_height() {
+ if (_update.pending()) return;
+
+ {
+ auto scoped(_update.block());
+ auto width = _page_width.get_value();
+ auto height = _page_height.get_value();
+ _page_width.set_value(height);
+ _page_height.set_value(width);
+ }
+ set_page_size();
+ };
+
+ void set_display_unit() {
+ if (_update.pending()) return;
+
+ const auto unit = _display_units->getUnit();
+ _signal_unit_changed.emit(unit, Units::Display);
+ }
+
+ void set_page_unit() {
+ if (_update.pending()) return;
+
+ const auto old_unit = _current_page_unit;
+ _current_page_unit = _page_units->getUnit();
+ const auto new_unit = _current_page_unit;
+
+ {
+ auto width = _page_width.get_value();
+ auto height = _page_height.get_value();
+ Quantity w(width, old_unit->abbr);
+ Quantity h(height, old_unit->abbr);
+ auto scoped(_update.block());
+ _page_width.set_value(w.value(new_unit));
+ _page_height.set_value(h.value(new_unit));
+ }
+ _doc_units.set_text(new_unit->abbr);
+ set_page_size();
+ _signal_unit_changed.emit(new_unit, Units::Document);
+ }
+
+ void set_color(Color element, unsigned int color) override {
+ auto scoped(_update.block());
+
+ get_color_picker(element).setRgba32(color);
+ update_preview_color(element, color);
+ }
+
+ void set_check(Check element, bool checked) override {
+ auto scoped(_update.block());
+
+ if (element == Check::NonuniformScale) {
+ show_widget(_nonuniform_scale, checked);
+ _scale_is_uniform = !checked;
+ _scale_x.set_sensitive(_scale_is_uniform);
+ _linked_viewbox_scale.set_from_icon_name(_scale_is_uniform ? g_linked : g_unlinked, Gtk::ICON_SIZE_LARGE_TOOLBAR);
+ }
+ else if (element == Check::DisabledScale) {
+ _scale_x.set_sensitive(!checked);
+ }
+ else if (element == Check::UnsupportedSize) {
+ show_widget(_unsupported_size, checked);
+ }
+ else {
+ get_checkbutton(element).set_active(checked);
+
+ // special cases
+ if (element == Check::Checkerboard) _preview->enable_checkerboard(checked);
+ if (element == Check::Shadow) _preview->enable_drop_shadow(checked);
+ if (element == Check::Border) _preview->draw_border(checked);
+ }
+ }
+
+ void set_dimension(Dimension dimension, double x, double y) override {
+ auto scoped(_update.block());
+
+ auto dim = get_dimension(dimension);
+ dim.first.set_value(x);
+ dim.second.set_value(y);
+
+ set_page_size();
+ }
+
+ void set_unit(Units unit, const Glib::ustring& abbr) override {
+ auto scoped(_update.block());
+
+ if (unit == Units::Display) {
+ _display_units->setUnit(abbr);
+ }
+ else if (unit == Units::Document) {
+ _doc_units.set_text(abbr);
+ _page_units->setUnit(abbr);
+ _current_page_unit = _page_units->getUnit();
+ set_page_size();
+ }
+ }
+
+ ColorPicker& get_color_picker(Color element) {
+ switch (element) {
+ case Color::Background: return *_backgnd_color_picker;
+ case Color::Desk: return *_desk_color_picker;
+ case Color::Border: return *_border_color_picker;
+
+ default:
+ throw std::runtime_error("missing case in get_color_picker");
+ }
+ }
+
+ void fire_value_changed(Gtk::SpinButton& b1, Gtk::SpinButton& b2, const Util::Unit* unit, Dimension dim) {
+ if (!_update.pending()) {
+ _signal_dimmension_changed.emit(b1.get_value(), b2.get_value(), unit, dim);
+ }
+ }
+
+ void fire_checkbox_toggled(Gtk::CheckButton& checkbox, Check check) {
+ if (!_update.pending()) {
+ _signal_check_toggled.emit(checkbox.get_active(), check);
+ }
+ }
+
+ const PaperSize* find_page_template(double width, double height, const Unit& unit) {
+ Quantity w(std::min(width, height), &unit);
+ Quantity h(std::max(width, height), &unit);
+
+ const double eps = 1e-6;
+ for (auto&& page : PaperSize::getPageSizes()) {
+ Quantity pw(std::min(page.width, page.height), page.unit);
+ Quantity ph(std::max(page.width, page.height), page.unit);
+
+ if (are_near(w, pw, eps) && are_near(h, ph, eps)) {
+ return &page;
+ }
+ }
+
+ return nullptr;
+ }
+
+ Gtk::CheckButton& get_checkbutton(Check check) {
+ switch (check) {
+ case Check::AntiAlias: return _antialias;
+ case Check::Border: return _border;
+ case Check::Shadow: return _shadow;
+ case Check::BorderOnTop: return _border_on_top;
+ case Check::Checkerboard: return _checkerboard;
+ case Check::ClipToPage: return _clip_to_page;
+ case Check::PageLabelStyle: return _page_label_style;
+
+ default:
+ throw std::runtime_error("missing case in get_checkbutton");
+ }
+ }
+
+ typedef std::pair<Gtk::SpinButton&, Gtk::SpinButton&> spin_pair;
+ spin_pair get_dimension(Dimension dimension) {
+ switch (dimension) {
+ case Dimension::PageSize: return spin_pair(_page_width, _page_height);
+ case Dimension::PageTemplate: return spin_pair(_page_width, _page_height);
+ case Dimension::Scale: return spin_pair(_scale_x, _scale_x);
+ case Dimension::ViewboxPosition: return spin_pair(_viewbox_x, _viewbox_y);
+ case Dimension::ViewboxSize: return spin_pair(_viewbox_width, _viewbox_height);
+
+ default:
+ throw std::runtime_error("missing case in get_dimension");
+ }
+ }
+
+ Glib::RefPtr<Gtk::Builder> _builder;
+ Gtk::Grid& _main_grid;
+ Gtk::Grid& _left_grid;
+ MathSpinButton& _page_width;
+ MathSpinButton& _page_height;
+ Gtk::RadioButton& _portrait;
+ Gtk::RadioButton& _landscape;
+ MathSpinButton& _scale_x;
+ Gtk::Label& _unsupported_size;
+ Gtk::Label& _nonuniform_scale;
+ Gtk::Label& _doc_units;
+ MathSpinButton& _viewbox_x;
+ MathSpinButton& _viewbox_y;
+ MathSpinButton& _viewbox_width;
+ MathSpinButton& _viewbox_height;
+ std::unique_ptr<ColorPicker> _backgnd_color_picker;
+ std::unique_ptr<ColorPicker> _border_color_picker;
+ std::unique_ptr<ColorPicker> _desk_color_picker;
+ Gtk::Menu& _page_templates_menu;
+ Gtk::Label& _template_name;
+ Gtk::Box& _preview_box;
+ std::unique_ptr<PageSizePreview> _preview = std::make_unique<PageSizePreview>();
+ Gtk::CheckButton& _border;
+ Gtk::CheckButton& _border_on_top;
+ Gtk::CheckButton& _shadow;
+ Gtk::CheckButton& _checkerboard;
+ Gtk::CheckButton& _antialias;
+ Gtk::CheckButton& _clip_to_page;
+ Gtk::CheckButton& _page_label_style;
+ Gtk::Button& _link_width_height;
+ UnitMenu *_display_units;
+ UnitMenu *_page_units;
+ const Unit* _current_page_unit = nullptr;
+ OperationBlocker _update;
+ double _size_ratio = 1; // width to height ratio
+ bool _locked_size_ratio = false;
+ bool _scale_is_uniform = true;
+ Gtk::Expander& _viewbox_expander;
+ Gtk::Image& _linked_viewbox_scale;
+};
+
+PageProperties* PageProperties::create() {
+ return new PagePropertiesBox();
+}
+
+
+} } } // namespace Inkscape/Widget/UI
diff --git a/src/ui/widget/page-properties.h b/src/ui/widget/page-properties.h
new file mode 100644
index 0000000..a2d0e06
--- /dev/null
+++ b/src/ui/widget/page-properties.h
@@ -0,0 +1,59 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Michael Kowalski
+ *
+ * Copyright (C) 2021 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_PAGE_PROPERTIES_H
+#define INKSCAPE_UI_WIDGET_PAGE_PROPERTIES_H
+
+#include <gtkmm/box.h>
+
+namespace Inkscape {
+ namespace Util { class Unit; }
+namespace UI {
+namespace Widget {
+
+class PageProperties : public Gtk::Box {
+public:
+ static PageProperties* create();
+
+ ~PageProperties() override = default;
+
+ enum class Color { Background, Desk, Border };
+ virtual void set_color(Color element, unsigned int rgba) = 0;
+
+ sigc::signal<void (unsigned int, Color)>& signal_color_changed() { return _signal_color_changed; }
+
+ enum class Check { Checkerboard, Border, Shadow, BorderOnTop, AntiAlias, NonuniformScale, DisabledScale, UnsupportedSize, ClipToPage, PageLabelStyle };
+ virtual void set_check(Check element, bool checked) = 0;
+
+ sigc::signal<void (bool, Check)>& signal_check_toggled() { return _signal_check_toggled; }
+
+ enum class Dimension { PageSize, ViewboxSize, ViewboxPosition, Scale, PageTemplate };
+ virtual void set_dimension(Dimension dim, double x, double y) = 0;
+
+ sigc::signal<void (double, double, const Util::Unit*, Dimension)>& signal_dimmension_changed() { return _signal_dimmension_changed; }
+
+ enum class Units { Display, Document };
+ virtual void set_unit(Units unit, const Glib::ustring& abbr) = 0;
+
+ sigc::signal<void (const Util::Unit*, Units)> signal_unit_changed() { return _signal_unit_changed; }
+
+ sigc::signal<void ()> signal_resize_to_fit() { return _signal_resize_to_fit; }
+
+protected:
+ sigc::signal<void (unsigned int, Color)> _signal_color_changed;
+ sigc::signal<void (bool, Check)> _signal_check_toggled;
+ sigc::signal<void (double, double, const Util::Unit*, Dimension)> _signal_dimmension_changed;
+ sigc::signal<void (const Util::Unit*, Units)> _signal_unit_changed;
+ sigc::signal<void ()> _signal_resize_to_fit;
+};
+
+} } } // namespace Inkscape/Widget/UI
+
+#endif // INKSCAPE_UI_WIDGET_PAGE_PROPERTIES_H
diff --git a/src/ui/widget/page-selector.cpp b/src/ui/widget/page-selector.cpp
new file mode 100644
index 0000000..38e1410
--- /dev/null
+++ b/src/ui/widget/page-selector.cpp
@@ -0,0 +1,198 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Inkscape::Widgets::PageSelector - select and move to pages
+ *
+ * Authors:
+ * Martin Owens
+ *
+ * Copyright (C) 2021 Martin Owens
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "page-selector.h"
+
+#include <cstring>
+#include <glibmm/i18n.h>
+#include <string>
+
+#include "desktop.h"
+#include "document.h"
+#include "object/sp-namedview.h"
+#include "object/sp-page.h"
+#include "page-manager.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+PageSelector::PageSelector(SPDesktop *desktop)
+ : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)
+ , _desktop(desktop)
+{
+ set_name("PageSelector");
+
+ _prev_button.add(*Gtk::manage(sp_get_icon_image(INKSCAPE_ICON("pan-start"), Gtk::ICON_SIZE_MENU)));
+ _prev_button.set_relief(Gtk::RELIEF_NONE);
+ _prev_button.set_tooltip_text(_("Move to previous page"));
+ _prev_button.signal_clicked().connect(sigc::mem_fun(*this, &PageSelector::prevPage));
+
+ _next_button.add(*Gtk::manage(sp_get_icon_image(INKSCAPE_ICON("pan-end"), Gtk::ICON_SIZE_MENU)));
+ _next_button.set_relief(Gtk::RELIEF_NONE);
+ _next_button.set_tooltip_text(_("Move to next page"));
+ _next_button.signal_clicked().connect(sigc::mem_fun(*this, &PageSelector::nextPage));
+
+ _selector.set_tooltip_text(_("Current page"));
+
+ _page_model = Gtk::ListStore::create(_model_columns);
+ _selector.set_model(_page_model);
+ _selector.pack_start(_label_renderer);
+ _selector.set_cell_data_func(_label_renderer, sigc::mem_fun(*this, &PageSelector::renderPageLabel));
+
+ _selector_changed_connection =
+ _selector.signal_changed().connect(sigc::mem_fun(*this, &PageSelector::setSelectedPage));
+
+ pack_start(_prev_button, Gtk::PACK_EXPAND_PADDING);
+ pack_start(_selector, Gtk::PACK_EXPAND_WIDGET);
+ pack_start(_next_button, Gtk::PACK_EXPAND_PADDING);
+
+ _doc_replaced_connection =
+ _desktop->connectDocumentReplaced(sigc::hide<0>(sigc::mem_fun(*this, &PageSelector::setDocument)));
+
+ this->show_all();
+ this->set_no_show_all();
+ setDocument(desktop->getDocument());
+}
+
+PageSelector::~PageSelector()
+{
+ _doc_replaced_connection.disconnect();
+ _selector_changed_connection.disconnect();
+ setDocument(nullptr);
+}
+
+void PageSelector::setDocument(SPDocument *document)
+{
+ _document = document;
+ _pages_changed_connection.disconnect();
+ _page_selected_connection.disconnect();
+ if (document) {
+ auto &page_manager = document->getPageManager();
+ _pages_changed_connection =
+ page_manager.connectPagesChanged(sigc::mem_fun(*this, &PageSelector::pagesChanged));
+ _page_selected_connection =
+ page_manager.connectPageSelected(sigc::mem_fun(*this, &PageSelector::selectonChanged));
+ pagesChanged();
+ }
+}
+
+void PageSelector::pagesChanged()
+{
+ _selector_changed_connection.block();
+ auto &page_manager = _document->getPageManager();
+
+ // Destroy all existing pages in the model.
+ while (!_page_model->children().empty()) {
+ Gtk::ListStore::iterator row(_page_model->children().begin());
+ // Put cleanup here if any
+ _page_model->erase(row);
+ }
+
+ // Hide myself when there's no pages (single page document)
+ this->set_visible(page_manager.hasPages());
+
+ // Add in pages, do not use getResourcelist("page") because the items
+ // are not guaranteed to be in node order, they are in first-seen order.
+ for (auto &page : page_manager.getPages()) {
+ Gtk::ListStore::iterator row(_page_model->append());
+ row->set_value(_model_columns.object, page);
+ }
+
+ selectonChanged(page_manager.getSelected());
+
+ _selector_changed_connection.unblock();
+}
+
+void PageSelector::selectonChanged(SPPage *page)
+{
+ _selector_changed_connection.block();
+ _next_button.set_sensitive(_document->getPageManager().hasNextPage());
+ _prev_button.set_sensitive(_document->getPageManager().hasPrevPage());
+
+ auto active = _selector.get_active();
+
+ if (!active || active->get_value(_model_columns.object) != page) {
+ for (auto row : _page_model->children()) {
+ if (page == row->get_value(_model_columns.object)) {
+ _selector.set_active(row);
+ break;
+ }
+ }
+ }
+ _selector_changed_connection.unblock();
+}
+
+/**
+ * Render the page icon into a suitable label.
+ */
+void PageSelector::renderPageLabel(Gtk::TreeModel::const_iterator const &row)
+{
+ SPPage *page = (*row)[_model_columns.object];
+
+ if (page && page->getRepr()) {
+ int page_num = page->getPagePosition();
+
+ gchar *format;
+ if (auto label = page->label()) {
+ format = g_strdup_printf("<span size=\"smaller\"><tt>%d.</tt>%s</span>", page_num, label);
+ } else {
+ format = g_strdup_printf("<span size=\"smaller\"><i>%s</i></span>", page->getDefaultLabel().c_str());
+ }
+
+ _label_renderer.property_markup() = format;
+ g_free(format);
+ } else {
+ _label_renderer.property_markup() = "⚠️";
+ }
+
+ _label_renderer.property_ypad() = 1;
+}
+
+void PageSelector::setSelectedPage()
+{
+ SPPage *page = _selector.get_active()->get_value(_model_columns.object);
+ if (page && _document->getPageManager().selectPage(page)) {
+ _document->getPageManager().zoomToSelectedPage(_desktop);
+ }
+}
+
+void PageSelector::nextPage()
+{
+ if (_document->getPageManager().selectNextPage()) {
+ _document->getPageManager().zoomToSelectedPage(_desktop);
+ }
+}
+
+void PageSelector::prevPage()
+{
+ if (_document->getPageManager().selectPrevPage()) {
+ _document->getPageManager().zoomToSelectedPage(_desktop);
+ }
+}
+
+} // 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-selector.h b/src/ui/widget/page-selector.h
new file mode 100644
index 0000000..a439386
--- /dev/null
+++ b/src/ui/widget/page-selector.h
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Inkscape::UI::Widget::PageSelector - page 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_PAGE_SELECTOR
+#define SEEN_INKSCAPE_WIDGETS_PAGE_SELECTOR
+
+#include <gtkmm/box.h>
+#include <gtkmm/cellrenderertext.h>
+#include <gtkmm/combobox.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/togglebutton.h>
+#include <gtkmm/treemodel.h>
+#include <sigc++/slot.h>
+
+#include "object/sp-page.h"
+
+class SPDesktop;
+class SPDocument;
+class SPPage;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+// class DocumentTreeModel;
+
+class PageSelector : public Gtk::Box
+{
+public:
+ PageSelector(SPDesktop *desktop = nullptr);
+ ~PageSelector() override;
+
+private:
+ class PageModelColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ Gtk::TreeModelColumn<SPPage *> object;
+
+ PageModelColumns() { add(object); }
+ };
+
+ SPDesktop *_desktop;
+ SPDocument *_document;
+
+ Gtk::ComboBox _selector;
+ Gtk::Button _prev_button;
+ Gtk::Button _next_button;
+
+ PageModelColumns _model_columns;
+ Gtk::CellRendererText _label_renderer;
+ Glib::RefPtr<Gtk::ListStore> _page_model;
+
+ sigc::connection _selector_changed_connection;
+ sigc::connection _pages_changed_connection;
+ sigc::connection _page_selected_connection;
+ sigc::connection _doc_replaced_connection;
+
+ void setDocument(SPDocument *document);
+ void pagesChanged();
+ void selectonChanged(SPPage *page);
+
+ void renderPageLabel(Gtk::TreeModel::const_iterator const &row);
+ void setSelectedPage();
+ void nextPage();
+ void prevPage();
+};
+
+} // 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/page-size-preview.cpp b/src/ui/widget/page-size-preview.cpp
new file mode 100644
index 0000000..6cf2ed3
--- /dev/null
+++ b/src/ui/widget/page-size-preview.cpp
@@ -0,0 +1,186 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ *
+ * Page size preview widget
+ */
+/*
+ * Authors:
+ * Mike Kowalski
+ *
+ * Copyright (C) 2021 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "page-size-preview.h"
+#include "display/cairo-utils.h"
+#include "2geom/rect.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+PageSizePreview::PageSizePreview() {
+ show();
+}
+
+void rounded_rectangle(const Cairo::RefPtr<Cairo::Context>& cr, double x, double y, double w, double h, double r) {
+ cr->begin_new_sub_path();
+ cr->arc(x + r, y + r, r, M_PI, 3 * M_PI / 2);
+ cr->arc(x + w - r, y + r, r, 3 * M_PI / 2, 2 * M_PI);
+ cr->arc(x + w - r, y + h - r, r, 0, M_PI / 2);
+ cr->arc(x + r, y + h - r, r, M_PI / 2, M_PI);
+ cr->close_path();
+}
+
+void set_source_rgba(const Cairo::RefPtr<Cairo::Context>& ctx, unsigned int rgba) {
+ ctx->set_source_rgba(SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), SP_RGBA32_B_F(rgba), SP_RGBA32_A_F(rgba));
+}
+
+bool PageSizePreview::on_draw(const Cairo::RefPtr<Cairo::Context>& ctx) {
+ auto alloc = get_allocation();
+ double width = alloc.get_width();
+ double height = alloc.get_height();
+ // too small to fit anything?
+ if (width <= 2 || height <= 2) return false;
+
+ double x = 0;//alloc.get_x();
+ double y = 0;//alloc.get_y();
+
+ if (_draw_checkerboard) {
+ // auto device_scale = get_scale_factor();
+ Cairo::RefPtr<Cairo::Pattern> pattern(new Cairo::Pattern(ink_cairo_pattern_create_checkerboard(_desk_color)));
+ ctx->save();
+ ctx->set_operator(Cairo::OPERATOR_SOURCE);
+ ctx->set_source(pattern);
+ rounded_rectangle(ctx, x, y, width, height, 2.0);
+ ctx->fill();
+ ctx->restore();
+ }
+ else {
+ rounded_rectangle(ctx, x, y, width, height, 2.0);
+ set_source_rgba(ctx, _desk_color);
+ ctx->fill();
+ }
+
+ // use lesser dimension to prevent page from changing size when
+ // switching from portrait to landscape or vice versa
+ auto size = std::round(std::min(width, height) * 0.90); // 90% to leave margins
+ double w, h;
+ if (_width > _height) {
+ w = size;
+ h = std::round(size * _height / _width);
+ }
+ else {
+ h = size;
+ w = std::round(size * _width / _height);
+ }
+ if (w < 2) w = 2;
+ if (h < 2) h = 2;
+
+ // center page
+ double ox = std::round(x + (width - w) / 2);
+ double oy = std::round(y + (height - h) / 2);
+ Geom::Rect rect(ox, oy, ox + w, oy + h);
+
+ ctx->rectangle(rect.left(), rect.top(), rect.width(), rect.height());
+
+ if (_draw_checkerboard) {
+ Cairo::RefPtr<Cairo::Pattern> pattern(new Cairo::Pattern(ink_cairo_pattern_create_checkerboard(_page_color)));
+ ctx->save();
+ ctx->set_operator(Cairo::OPERATOR_SOURCE);
+ ctx->set_source(pattern);
+ ctx->rectangle(rect.left(), rect.top(), rect.width(), rect.height());
+ ctx->fill();
+ ctx->restore();
+ }
+ else {
+ ctx->rectangle(rect.left(), rect.top(), rect.width(), rect.height());
+ set_source_rgba(ctx, _page_color | 0xff);
+ ctx->fill();
+ }
+
+ // draw cross
+ /*
+ {
+ double gradient_size = 4;
+ double cx = std::round(x + (width - gradient_size) / 2);
+ double cy = std::round(y + (height - gradient_size) / 2);
+ auto horz = Cairo::LinearGradient::create(x, cy, x, cy + gradient_size);
+ auto vert = Cairo::LinearGradient::create(cx, y, cx + gradient_size, y);
+
+ horz->add_color_stop_rgba(0.0, 0, 0, 0, 0.0);
+ horz->add_color_stop_rgba(0.5, 0, 0, 0, 0.2);
+ horz->add_color_stop_rgba(0.5, 1, 1, 1, 0.8);
+ horz->add_color_stop_rgba(1.0, 1, 1, 1, 0.0);
+
+ vert->add_color_stop_rgba(0.0, 0, 0, 0, 0.0);
+ vert->add_color_stop_rgba(0.5, 0, 0, 0, 0.2);
+ vert->add_color_stop_rgba(0.5, 1, 1, 1, 0.8);
+ vert->add_color_stop_rgba(1.0, 1, 1, 1, 0.0);
+
+ ctx->rectangle(x, cy, width, gradient_size);
+ ctx->set_source(horz);
+ ctx->fill();
+
+ ctx->rectangle(cx, y, gradient_size, height);
+ ctx->set_source(vert);
+ ctx->fill();
+ }
+
+ ctx->rectangle(rect.left(), rect.top(), rect.width(), rect.height());
+ set_source_rgba(ctx, _page_color);
+ ctx->fill();
+*/
+ if (_draw_border) {
+ // stoke; not pixel aligned, just like page on canvas
+ ctx->rectangle(rect.left(), rect.top(), rect.width(), rect.height());
+ set_source_rgba(ctx, _border_color);
+ ctx->set_line_width(1);
+ ctx->stroke();
+
+ if (_draw_shadow) {
+ const auto a = (exp(-3 * SP_RGBA32_A_F(_border_color)) - 1) / (exp(-3) - 1);
+ ink_cairo_draw_drop_shadow(ctx, rect, 12, _border_color, a);
+ }
+ }
+
+ return true;
+}
+
+void PageSizePreview::draw_border(bool border) {
+ _draw_border = border;
+ queue_draw();
+}
+
+void PageSizePreview::set_desk_color(unsigned int rgba) {
+ _desk_color = rgba | 0xff; // desk always opaque
+ queue_draw();
+}
+void PageSizePreview::set_page_color(unsigned int rgba) {
+ _page_color = rgba;
+ queue_draw();
+}
+void PageSizePreview::set_border_color(unsigned int rgba) {
+ _border_color = rgba;
+ queue_draw();
+}
+
+void PageSizePreview::enable_drop_shadow(bool shadow) {
+ _draw_shadow = shadow;
+ queue_draw();
+}
+
+void PageSizePreview::enable_checkerboard(bool checkerboard) {
+ _draw_checkerboard = checkerboard;
+ queue_draw();
+}
+
+void PageSizePreview::set_page_size(double width, double height) {
+ _width = width;
+ _height = height;
+ queue_draw();
+}
+
+} } } // namespace Inkscape/Widget/UI
diff --git a/src/ui/widget/page-size-preview.h b/src/ui/widget/page-size-preview.h
new file mode 100644
index 0000000..093e79b
--- /dev/null
+++ b/src/ui/widget/page-size-preview.h
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Michael Kowalski
+ *
+ * Copyright (C) 2021 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_PAGE_SIZE_PREVIEW_H
+#define INKSCAPE_UI_WIDGET_PAGE_SIZE_PREVIEW_H
+
+#include <gtkmm/drawingarea.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class PageSizePreview : public Gtk::DrawingArea {
+public:
+ PageSizePreview();
+ // static PageSizePreview* create();
+
+ void set_desk_color(unsigned int rgba);
+ void set_page_color(unsigned int rgba);
+ void set_border_color(unsigned int rgba);
+ void draw_border(bool border);
+ void enable_drop_shadow(bool shadow);
+ void set_page_size(double width, double height);
+ void enable_checkerboard(bool checkerboard);
+
+ ~PageSizePreview() override = default;
+
+private:
+ bool on_draw(const Cairo::RefPtr<Cairo::Context>& ctx) override;
+ unsigned int _border_color = 0x0000001f;
+ unsigned int _page_color = 0xffffff00;
+ unsigned int _desk_color = 0xc8c8c8ff;
+ bool _draw_border = true;
+ bool _draw_shadow = true;
+ bool _draw_checkerboard = false;
+ double _width = 10;
+ double _height = 7;
+};
+
+} } } // namespace Inkscape/Widget/UI
+
+#endif // INKSCAPE_UI_WIDGET_PAGE_SIZE_PREVIEW_H
+
diff --git a/src/ui/widget/paint-selector.cpp b/src/ui/widget/paint-selector.cpp
new file mode 100644
index 0000000..480bea0
--- /dev/null
+++ b/src/ui/widget/paint-selector.cpp
@@ -0,0 +1,1267 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * PaintSelector: Generic paint selector widget.
+ *//*
+ * Authors:
+ * see git history
+ * Lauris Kaplinski
+ * bulia byak <buliabyak@users.sf.net>
+ * John Cliff <simarilius@yahoo.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#define noSP_PS_VERBOSE
+
+#include <cstring>
+#include <string>
+#include <vector>
+
+#include <glibmm/i18n.h>
+#include <glibmm/fileutils.h>
+
+#include "desktop-style.h"
+#include "inkscape.h"
+#include "paint-selector.h"
+#include "path-prefix.h"
+#include "pattern-manipulation.h"
+
+#include "helper/stock-items.h"
+#include "ui/icon-loader.h"
+
+#include "style.h"
+
+#include "io/sys.h"
+#include "io/resource.h"
+#include "object/sp-hatch.h"
+#include "object/sp-linear-gradient.h"
+#include "object/sp-mesh-gradient.h"
+#include "object/sp-pattern.h"
+#include "object/sp-radial-gradient.h"
+#include "object/sp-stop.h"
+
+#include "svg/css-ostringstream.h"
+
+#include "ui/icon-names.h"
+#include "ui/widget/color-notebook.h"
+#include "ui/widget/gradient-selector.h"
+#include "ui/widget/gradient-editor.h"
+#include "ui/widget/pattern-editor.h"
+#include "ui/widget/swatch-selector.h"
+#include "ui/widget/scrollprotected.h"
+
+#include "widgets/widget-sizes.h"
+
+#include "xml/repr.h"
+
+#ifdef SP_PS_VERBOSE
+#include "svg/svg-icc-color.h"
+#endif // SP_PS_VERBOSE
+
+#include <gtkmm/label.h>
+#include <gtkmm/combobox.h>
+
+using Inkscape::UI::SelectedColor;
+
+#ifdef SP_PS_VERBOSE
+static gchar const *modeStrings[] = {
+ "MODE_EMPTY",
+ "MODE_MULTIPLE",
+ "MODE_NONE",
+ "MODE_SOLID_COLOR",
+ "MODE_GRADIENT_LINEAR",
+ "MODE_GRADIENT_RADIAL",
+#ifdef WITH_MESH
+ "MODE_GRADIENT_MESH",
+#endif
+ "MODE_PATTERN",
+ "MODE_SWATCH",
+ "MODE_UNSET",
+ ".",
+ ".",
+};
+#endif
+
+namespace {
+GtkWidget *_scrollprotected_combo_box_new_with_model(GtkTreeModel *model)
+{
+ auto combobox = Gtk::manage(new Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBox>());
+ gtk_combo_box_set_model(combobox->gobj(), model);
+ return GTK_WIDGET(combobox->gobj());
+}
+} // namespace
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class FillRuleRadioButton : public Gtk::RadioButton {
+ private:
+ PaintSelector::FillRule _fillrule;
+
+ public:
+ FillRuleRadioButton()
+ : Gtk::RadioButton()
+ {}
+
+ FillRuleRadioButton(Gtk::RadioButton::Group &group)
+ : Gtk::RadioButton(group)
+ {}
+
+ inline void set_fillrule(PaintSelector::FillRule fillrule) { _fillrule = fillrule; }
+ inline PaintSelector::FillRule get_fillrule() const { return _fillrule; }
+};
+
+class StyleToggleButton : public Gtk::ToggleButton {
+ private:
+ PaintSelector::Mode _style;
+
+ public:
+ inline void set_style(PaintSelector::Mode style) { _style = style; }
+ inline PaintSelector::Mode get_style() const { return _style; }
+};
+
+static bool isPaintModeGradient(PaintSelector::Mode mode)
+{
+ bool isGrad = (mode == PaintSelector::MODE_GRADIENT_LINEAR) || (mode == PaintSelector::MODE_GRADIENT_RADIAL) ||
+ (mode == PaintSelector::MODE_SWATCH);
+
+ return isGrad;
+}
+
+GradientSelectorInterface *PaintSelector::getGradientFromData() const
+{
+ if (_mode == PaintSelector::MODE_SWATCH && _selector_swatch) {
+ return _selector_swatch->getGradientSelector();
+ }
+ return _selector_gradient;
+}
+
+#define XPAD 4
+#define YPAD 1
+
+PaintSelector::PaintSelector(FillOrStroke kind)
+{
+ set_orientation(Gtk::ORIENTATION_VERTICAL);
+
+ _mode = static_cast<PaintSelector::Mode>(-1); // huh? do you mean 0xff? -- I think this means "not in the enum"
+
+ /* Paint style button box */
+ _style = Gtk::manage(new Gtk::Box());
+ _style->set_homogeneous(false);
+ _style->set_name("PaintSelector");
+ _style->show();
+ _style->set_border_width(0);
+ pack_start(*_style, false, false);
+
+ /* Buttons */
+ _none = style_button_add(INKSCAPE_ICON("paint-none"), PaintSelector::MODE_NONE, _("No paint"));
+ _solid = style_button_add(INKSCAPE_ICON("paint-solid"), PaintSelector::MODE_SOLID_COLOR, _("Flat color"));
+ _gradient = style_button_add(INKSCAPE_ICON("paint-gradient-linear"), PaintSelector::MODE_GRADIENT_LINEAR,
+ _("Linear gradient"));
+ _radial = style_button_add(INKSCAPE_ICON("paint-gradient-radial"), PaintSelector::MODE_GRADIENT_RADIAL,
+ _("Radial gradient"));
+#ifdef WITH_MESH
+ _mesh =
+ style_button_add(INKSCAPE_ICON("paint-gradient-mesh"), PaintSelector::MODE_GRADIENT_MESH, _("Mesh gradient"));
+#endif
+ _pattern = style_button_add(INKSCAPE_ICON("paint-pattern"), PaintSelector::MODE_PATTERN, _("Pattern"));
+ _swatch = style_button_add(INKSCAPE_ICON("paint-swatch"), PaintSelector::MODE_SWATCH, _("Swatch"));
+ _unset = style_button_add(INKSCAPE_ICON("paint-unknown"), PaintSelector::MODE_UNSET,
+ _("Unset paint (make it undefined so it can be inherited)"));
+
+ /* Fillrule */
+ {
+ _fillrulebox = Gtk::manage(new Gtk::Box());
+ _fillrulebox->set_homogeneous(false);
+ _style->pack_end(*_fillrulebox, false, false, 0);
+
+ _evenodd = Gtk::manage(new FillRuleRadioButton());
+ _evenodd->set_relief(Gtk::RELIEF_NONE);
+ _evenodd->set_mode(false);
+ // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/painting.html#FillRuleProperty
+ _evenodd->set_tooltip_text(
+ _("Any path self-intersections or subpaths create holes in the fill (fill-rule: evenodd)"));
+ _evenodd->set_fillrule(PaintSelector::FILLRULE_EVENODD);
+ auto w = sp_get_icon_image("fill-rule-even-odd", GTK_ICON_SIZE_MENU);
+ gtk_container_add(GTK_CONTAINER(_evenodd->gobj()), w);
+ _fillrulebox->pack_start(*_evenodd, false, false, 0);
+ _evenodd->signal_toggled().connect(
+ sigc::bind(sigc::mem_fun(*this, &PaintSelector::fillrule_toggled), _evenodd));
+
+ auto grp = _evenodd->get_group();
+ _nonzero = Gtk::manage(new FillRuleRadioButton(grp));
+ _nonzero->set_relief(Gtk::RELIEF_NONE);
+ _nonzero->set_mode(false);
+ // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/painting.html#FillRuleProperty
+ _nonzero->set_tooltip_text(_("Fill is solid unless a subpath is counterdirectional (fill-rule: nonzero)"));
+ _nonzero->set_fillrule(PaintSelector::FILLRULE_NONZERO);
+ w = sp_get_icon_image("fill-rule-nonzero", GTK_ICON_SIZE_MENU);
+ gtk_container_add(GTK_CONTAINER(_nonzero->gobj()), w);
+ _fillrulebox->pack_start(*_nonzero, false, false, 0);
+ _nonzero->signal_toggled().connect(
+ sigc::bind(sigc::mem_fun(*this, &PaintSelector::fillrule_toggled), _nonzero));
+ }
+
+ /* Frame */
+ _label = Gtk::manage(new Gtk::Label(""));
+ auto lbbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0));
+ lbbox->set_homogeneous(false);
+ _label->show();
+ lbbox->pack_start(*_label, false, false, 4);
+ pack_start(*lbbox, false, false, 4);
+
+ _frame = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
+ _frame->set_homogeneous(false);
+ _frame->show();
+ // gtk_container_set_border_width(GTK_CONTAINER(psel->frame), 0);
+ pack_start(*_frame, true, true, 0);
+
+
+ /* Last used color */
+ _selected_color = new SelectedColor;
+ _updating_color = false;
+
+ _selected_color->signal_grabbed.connect(sigc::mem_fun(*this, &PaintSelector::onSelectedColorGrabbed));
+ _selected_color->signal_dragged.connect(sigc::mem_fun(*this, &PaintSelector::onSelectedColorDragged));
+ _selected_color->signal_released.connect(sigc::mem_fun(*this, &PaintSelector::onSelectedColorReleased));
+ _selected_color->signal_changed.connect(sigc::mem_fun(*this, &PaintSelector::onSelectedColorChanged));
+
+ // from _new function
+ setMode(PaintSelector::MODE_MULTIPLE);
+
+ if (kind == FILL)
+ _fillrulebox->show_all();
+ else
+ _fillrulebox->hide();
+
+ show_all();
+
+ // don't let docking manager uncover hidden widgets
+ set_no_show_all();
+}
+
+PaintSelector::~PaintSelector()
+{
+ if (_selected_color) {
+ delete _selected_color;
+ _selected_color = nullptr;
+ }
+}
+
+StyleToggleButton *PaintSelector::style_button_add(gchar const *pixmap, PaintSelector::Mode mode, gchar const *tip)
+{
+ GtkWidget *w;
+
+ auto b = Gtk::manage(new StyleToggleButton());
+ b->set_tooltip_text(tip);
+ b->show();
+ b->set_border_width(0);
+ b->set_relief(Gtk::RELIEF_NONE);
+ b->set_mode(false);
+ b->set_style(mode);
+
+ w = sp_get_icon_image(pixmap, GTK_ICON_SIZE_BUTTON);
+ gtk_container_add(GTK_CONTAINER(b->gobj()), w);
+
+ _style->pack_start(*b, false, false);
+ b->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &PaintSelector::style_button_toggled), b));
+
+ return b;
+}
+
+void PaintSelector::style_button_toggled(StyleToggleButton *tb)
+{
+ if (!_update && tb->get_active()) {
+ // button toggled: explicit user action where fill/stroke style change is initiated/requested
+ set_mode_ex(tb->get_style(), true);
+ }
+}
+
+void PaintSelector::fillrule_toggled(FillRuleRadioButton *tb)
+{
+ if (!_update && tb->get_active()) {
+ auto fr = tb->get_fillrule();
+ _signal_fillrule_changed.emit(fr);
+ }
+}
+
+void PaintSelector::setMode(Mode mode) {
+ set_mode_ex(mode, false);
+}
+
+void PaintSelector::set_mode_ex(Mode mode, bool switch_style) {
+ if (_mode != mode) {
+ _update = true;
+ _label->show();
+#ifdef SP_PS_VERBOSE
+ g_print("Mode change %d -> %d %s -> %s\n", _mode, mode, modeStrings[_mode], modeStrings[mode]);
+#endif
+ switch (mode) {
+ case MODE_EMPTY:
+ set_mode_empty();
+ break;
+ case MODE_MULTIPLE:
+ set_mode_multiple();
+ break;
+ case MODE_NONE:
+ set_mode_none();
+ break;
+ case MODE_SOLID_COLOR:
+ set_mode_color(mode);
+ break;
+ case MODE_GRADIENT_LINEAR:
+ case MODE_GRADIENT_RADIAL:
+ set_mode_gradient(mode);
+ break;
+#ifdef WITH_MESH
+ case MODE_GRADIENT_MESH:
+ set_mode_mesh(mode);
+ break;
+#endif
+ case MODE_PATTERN:
+ set_mode_pattern(mode);
+ break;
+ case MODE_HATCH:
+ set_mode_hatch(mode);
+ break;
+ case MODE_SWATCH:
+ set_mode_swatch(mode);
+ break;
+ case MODE_UNSET:
+ set_mode_unset();
+ break;
+ default:
+ g_warning("file %s: line %d: Unknown paint mode %d", __FILE__, __LINE__, mode);
+ break;
+ }
+ _mode = mode;
+ _signal_mode_changed.emit(_mode, switch_style);
+ _update = false;
+ }
+}
+
+void PaintSelector::setFillrule(FillRule fillrule)
+{
+ if (_fillrulebox) {
+ // TODO this flips widgets but does not use a member to store state. Revisit
+ _evenodd->set_active(fillrule == FILLRULE_EVENODD);
+ _nonzero->set_active(fillrule == FILLRULE_NONZERO);
+ }
+}
+
+void PaintSelector::setColorAlpha(SPColor const &color, float alpha)
+{
+ g_return_if_fail((0.0 <= alpha) && (alpha <= 1.0));
+ {
+#ifdef SP_PS_VERBOSE
+ g_print("PaintSelector set RGBA\n");
+#endif
+ setMode(MODE_SOLID_COLOR);
+ }
+ _updating_color = true;
+ _selected_color->setColorAlpha(color, alpha);
+ _updating_color = false;
+}
+
+void PaintSelector::setSwatch(SPGradient *vector)
+{
+#ifdef SP_PS_VERBOSE
+ g_print("PaintSelector set SWATCH\n");
+#endif
+ setMode(MODE_SWATCH);
+
+ if (_selector_swatch) {
+ _selector_swatch->setVector((vector) ? vector->document : nullptr, vector);
+ }
+}
+
+void PaintSelector::setGradientLinear(SPGradient *vector, SPLinearGradient* gradient, SPStop* selected)
+{
+#ifdef SP_PS_VERBOSE
+ g_print("PaintSelector set GRADIENT LINEAR\n");
+#endif
+ setMode(MODE_GRADIENT_LINEAR);
+
+ auto gsel = getGradientFromData();
+
+ gsel->setMode(GradientSelector::MODE_LINEAR);
+ gsel->setGradient(gradient);
+ gsel->setVector((vector) ? vector->document : nullptr, vector);
+ gsel->selectStop(selected);
+}
+
+void PaintSelector::setGradientRadial(SPGradient *vector, SPRadialGradient* gradient, SPStop* selected)
+{
+#ifdef SP_PS_VERBOSE
+ g_print("PaintSelector set GRADIENT RADIAL\n");
+#endif
+ setMode(MODE_GRADIENT_RADIAL);
+
+ auto gsel = getGradientFromData();
+
+ gsel->setMode(GradientSelector::MODE_RADIAL);
+ gsel->setGradient(gradient);
+ gsel->setVector((vector) ? vector->document : nullptr, vector);
+ gsel->selectStop(selected);
+}
+
+#ifdef WITH_MESH
+void PaintSelector::setGradientMesh(SPMeshGradient *array)
+{
+#ifdef SP_PS_VERBOSE
+ g_print("PaintSelector set GRADIENT MESH\n");
+#endif
+ setMode(MODE_GRADIENT_MESH);
+
+ // GradientSelector *gsel = getGradientFromData(this);
+
+ // gsel->setMode(GradientSelector::MODE_GRADIENT_MESH);
+ // gsel->setVector((mesh) ? mesh->document : 0, mesh);
+}
+#endif
+
+void PaintSelector::setGradientProperties(SPGradientUnits units, SPGradientSpread spread)
+{
+ g_return_if_fail(isPaintModeGradient(_mode));
+
+ auto gsel = getGradientFromData();
+ gsel->setUnits(units);
+ gsel->setSpread(spread);
+}
+
+void PaintSelector::getGradientProperties(SPGradientUnits &units, SPGradientSpread &spread) const
+{
+ g_return_if_fail(isPaintModeGradient(_mode));
+
+ auto gsel = getGradientFromData();
+ units = gsel->getUnits();
+ spread = gsel->getSpread();
+}
+
+
+/**
+ * \post (alpha == NULL) || (*alpha in [0.0, 1.0]).
+ */
+void PaintSelector::getColorAlpha(SPColor &color, gfloat &alpha) const
+{
+ _selected_color->colorAlpha(color, alpha);
+
+ g_assert((0.0 <= alpha) && (alpha <= 1.0));
+}
+
+SPGradient *PaintSelector::getGradientVector()
+{
+ SPGradient *vect = nullptr;
+
+ if (isPaintModeGradient(_mode)) {
+ auto gsel = getGradientFromData();
+ vect = gsel->getVector();
+ }
+
+ return vect;
+}
+
+
+void PaintSelector::pushAttrsToGradient(SPGradient *gr) const
+{
+ SPGradientUnits units = SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX;
+ SPGradientSpread spread = SP_GRADIENT_SPREAD_PAD;
+ getGradientProperties(units, spread);
+ gr->setUnits(units);
+ gr->setSpread(spread);
+ gr->updateRepr();
+}
+
+void PaintSelector::clear_frame()
+{
+ if (_selector_solid_color) {
+ _selector_solid_color->hide();
+ }
+ if (_selector_gradient) {
+ _selector_gradient->hide();
+ }
+ if (_selector_mesh) {
+ _selector_mesh->hide();
+ }
+ if (_selector_pattern) {
+ _selector_pattern->hide();
+ }
+ if (_selector_swatch) {
+ _selector_swatch->hide();
+ }
+}
+
+void PaintSelector::set_mode_empty()
+{
+ set_style_buttons(nullptr);
+ _style->set_sensitive(false);
+ clear_frame();
+ _label->set_markup(_("<b>No objects</b>"));
+}
+
+void PaintSelector::set_mode_multiple()
+{
+ set_style_buttons(nullptr);
+ _style->set_sensitive(true);
+ clear_frame();
+ _label->set_markup(_("<b>Multiple styles</b>"));
+}
+
+void PaintSelector::set_mode_unset()
+{
+ set_style_buttons(_unset);
+ _style->set_sensitive(true);
+ clear_frame();
+ _label->set_markup(_("<b>Paint is undefined</b>"));
+}
+
+void PaintSelector::set_mode_none()
+{
+ set_style_buttons(_none);
+ _style->set_sensitive(true);
+ clear_frame();
+ _label->set_markup(_("<b>No paint</b>"));
+}
+
+/* Color paint */
+
+void PaintSelector::onSelectedColorGrabbed() { _signal_grabbed.emit(); }
+
+void PaintSelector::onSelectedColorDragged()
+{
+ if (_updating_color) {
+ return;
+ }
+
+ _signal_dragged.emit();
+}
+
+void PaintSelector::onSelectedColorReleased() { _signal_released.emit(); }
+
+void PaintSelector::onSelectedColorChanged()
+{
+ if (_updating_color) {
+ return;
+ }
+
+ if (_mode == MODE_SOLID_COLOR) {
+ _signal_changed.emit();
+ } else {
+ g_warning("PaintSelector::onSelectedColorChanged(): selected color changed while not in color selection mode");
+ }
+}
+
+void PaintSelector::set_mode_color(PaintSelector::Mode /*mode*/)
+{
+ using Inkscape::UI::Widget::ColorNotebook;
+
+ if (_mode == PaintSelector::MODE_SWATCH) {
+ auto gsel = getGradientFromData();
+ if (gsel) {
+ SPGradient *gradient = gsel->getVector();
+
+ // Gradient can be null if object paint is changed externally (ie. with a color picker tool)
+ if (gradient) {
+ SPColor color = gradient->getFirstStop()->getColor();
+ float alpha = gradient->getFirstStop()->getOpacity();
+ _selected_color->setColorAlpha(color, alpha, false);
+ }
+ }
+ }
+
+ set_style_buttons(_solid);
+ _style->set_sensitive(true);
+
+ if (_mode == PaintSelector::MODE_SOLID_COLOR) {
+ /* Already have color selector */
+ // Do nothing
+ } else {
+ clear_frame();
+
+ /* Create new color selector */
+ /* Create vbox */
+ if (!_selector_solid_color) {
+ _selector_solid_color = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4));
+ _selector_solid_color->set_homogeneous(false);
+
+ /* Color selector */
+ auto color_selector = Gtk::manage(new ColorNotebook(*(_selected_color)));
+ color_selector->show();
+ _selector_solid_color->pack_start(*color_selector, true, true, 0);
+ /* Pack everything to frame */
+ _frame->add(*_selector_solid_color);
+ color_selector->set_label(_("<b>Flat color</b>"));
+ }
+
+ _selector_solid_color->show();
+ }
+
+ _label->set_markup(""); //_("<b>Flat color</b>"));
+ _label->hide();
+
+#ifdef SP_PS_VERBOSE
+ g_print("Color req\n");
+#endif
+}
+
+/* Gradient */
+
+void PaintSelector::gradient_grabbed() { _signal_grabbed.emit(); }
+
+void PaintSelector::gradient_dragged() { _signal_dragged.emit(); }
+
+void PaintSelector::gradient_released() { _signal_released.emit(); }
+
+void PaintSelector::gradient_changed(SPGradient * /* gr */) { _signal_changed.emit(); }
+
+void PaintSelector::set_mode_gradient(PaintSelector::Mode mode)
+{
+ if (mode == PaintSelector::MODE_GRADIENT_LINEAR) {
+ set_style_buttons(_gradient);
+ } else if (mode == PaintSelector::MODE_GRADIENT_RADIAL) {
+ set_style_buttons(_radial);
+ }
+ _style->set_sensitive(true);
+
+ if ((_mode == PaintSelector::MODE_GRADIENT_LINEAR) || (_mode == PaintSelector::MODE_GRADIENT_RADIAL)) {
+ // do nothing - the selector should already be a GradientSelector
+ } else {
+ clear_frame();
+ if (!_selector_gradient) {
+ /* Create new gradient selector */
+ try {
+ _selector_gradient = Gtk::manage(new GradientEditor("/gradient-edit"));
+ _selector_gradient->show();
+ _selector_gradient->signal_grabbed().connect(sigc::mem_fun(*this, &PaintSelector::gradient_grabbed));
+ _selector_gradient->signal_dragged().connect(sigc::mem_fun(*this, &PaintSelector::gradient_dragged));
+ _selector_gradient->signal_released().connect(sigc::mem_fun(*this, &PaintSelector::gradient_released));
+ _selector_gradient->signal_changed().connect(sigc::mem_fun(*this, &PaintSelector::gradient_changed));
+ _selector_gradient->signal_stop_selected().connect([=](SPStop* stop) { _signal_stop_selected.emit(stop); });
+ /* Pack everything to frame */
+ _frame->add(*_selector_gradient);
+ }
+ catch (std::exception& ex) {
+ g_error("Creation of GradientEditor widget failed: %s.", ex.what());
+ throw;
+ }
+ } else {
+ // Necessary when creating new gradients via the Fill and Stroke dialog
+ _selector_gradient->setVector(nullptr, nullptr);
+ }
+ _selector_gradient->show();
+ }
+
+ /* Actually we have to set option menu history here */
+ if (mode == PaintSelector::MODE_GRADIENT_LINEAR) {
+ _selector_gradient->setMode(GradientSelector::MODE_LINEAR);
+ // sp_gradient_selector_set_mode(SP_GRADIENT_SELECTOR(gsel), SP_GRADIENT_SELECTOR_MODE_LINEAR);
+ // _label->set_markup(_("<b>Linear gradient</b>"));
+ _label->hide();
+ } else if (mode == PaintSelector::MODE_GRADIENT_RADIAL) {
+ _selector_gradient->setMode(GradientSelector::MODE_RADIAL);
+ // _label->set_markup(_("<b>Radial gradient</b>"));
+ _label->hide();
+ }
+
+#ifdef SP_PS_VERBOSE
+ g_print("Gradient req\n");
+#endif
+}
+
+// ************************* MESH ************************
+#ifdef WITH_MESH
+void PaintSelector::mesh_destroy(GtkWidget *widget, PaintSelector * /*psel*/)
+{
+ // drop our reference to the mesh menu widget
+ g_object_unref(G_OBJECT(widget));
+}
+
+void PaintSelector::mesh_change(GtkWidget * /*widget*/, PaintSelector *psel) { psel->_signal_changed.emit(); }
+
+
+/**
+ * Returns a list of meshes in the defs of the given source document as a vector
+ */
+static std::vector<SPMeshGradient *> ink_mesh_list_get(SPDocument *source)
+{
+ std::vector<SPMeshGradient *> pl;
+ if (source == nullptr)
+ return pl;
+
+
+ std::vector<SPObject *> meshes = source->getResourceList("gradient");
+ for (auto meshe : meshes) {
+ if (is<SPMeshGradient>(meshe) && cast<SPGradient>(meshe) == cast<SPGradient>(meshe)->getArray()) { // only if this is a
+ // root mesh
+ pl.push_back(cast<SPMeshGradient>(meshe));
+ }
+ }
+ return pl;
+}
+
+/**
+ * Adds menu items for mesh list.
+ */
+static void sp_mesh_menu_build(GtkWidget *combo, std::vector<SPMeshGradient *> &mesh_list, SPDocument * /*source*/)
+{
+ GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo)));
+ GtkTreeIter iter;
+
+ for (auto i : mesh_list) {
+
+ Inkscape::XML::Node *repr = i->getRepr();
+
+ gchar const *meshid = repr->attribute("id");
+ gchar const *label = meshid;
+
+ // Only relevant if we supply a set of canned meshes.
+ gboolean stockid = false;
+ if (repr->attribute("inkscape:stockid")) {
+ label = _(repr->attribute("inkscape:stockid"));
+ stockid = true;
+ }
+
+ gtk_list_store_append(store, &iter);
+ gtk_list_store_set(store, &iter, COMBO_COL_LABEL, label, COMBO_COL_STOCK, stockid, COMBO_COL_MESH, meshid,
+ COMBO_COL_SEP, FALSE, -1);
+ }
+}
+
+/**
+ * Pick up all meshes from source, except those that are in
+ * current_doc (if non-NULL), and add items to the mesh menu.
+ */
+static void sp_mesh_list_from_doc(GtkWidget *combo, SPDocument * /*current_doc*/, SPDocument *source,
+ SPDocument * /*mesh_doc*/)
+{
+ std::vector<SPMeshGradient *> pl = ink_mesh_list_get(source);
+ sp_mesh_menu_build(combo, pl, source);
+}
+
+
+static void ink_mesh_menu_populate_menu(GtkWidget *combo, SPDocument *doc)
+{
+ static SPDocument *meshes_doc = nullptr;
+
+ // If we ever add a list of canned mesh gradients, uncomment following:
+
+ // find and load meshes.svg
+ // if (meshes_doc == NULL) {
+ // char *meshes_source = g_build_filename(INKSCAPE_MESHESDIR, "meshes.svg", NULL);
+ // if (Inkscape::IO::file_test(meshes_source, G_FILE_TEST_IS_REGULAR)) {
+ // meshes_doc = SPDocument::createNewDoc(meshes_source, FALSE);
+ // }
+ // g_free(meshes_source);
+ // }
+
+ // suck in from current doc
+ sp_mesh_list_from_doc(combo, nullptr, doc, meshes_doc);
+
+ // add separator
+ // {
+ // GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo)));
+ // GtkTreeIter iter;
+ // gtk_list_store_append (store, &iter);
+ // gtk_list_store_set(store, &iter,
+ // COMBO_COL_LABEL, "", COMBO_COL_STOCK, false, COMBO_COL_MESH, "", COMBO_COL_SEP, true, -1);
+ // }
+
+ // suck in from meshes.svg
+ // if (meshes_doc) {
+ // doc->ensureUpToDate();
+ // sp_mesh_list_from_doc ( combo, doc, meshes_doc, NULL );
+ // }
+}
+
+
+static GtkWidget *ink_mesh_menu(GtkWidget *combo)
+{
+ SPDocument *doc = SP_ACTIVE_DOCUMENT;
+
+ GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo)));
+ GtkTreeIter iter;
+
+ if (!doc) {
+
+ gtk_list_store_append(store, &iter);
+ gtk_list_store_set(store, &iter, COMBO_COL_LABEL, _("No document selected"), COMBO_COL_STOCK, false,
+ COMBO_COL_MESH, "", COMBO_COL_SEP, false, -1);
+ gtk_widget_set_sensitive(combo, FALSE);
+
+ } else {
+
+ ink_mesh_menu_populate_menu(combo, doc);
+ gtk_widget_set_sensitive(combo, TRUE);
+ }
+
+ // Select the first item that is not a separator
+ if (gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter)) {
+ gboolean sep = false;
+ gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, COMBO_COL_SEP, &sep, -1);
+ if (sep) {
+ gtk_tree_model_iter_next(GTK_TREE_MODEL(store), &iter);
+ }
+ gtk_combo_box_set_active_iter(GTK_COMBO_BOX(combo), &iter);
+ }
+
+ return combo;
+}
+
+
+/*update mesh list*/
+void PaintSelector::updateMeshList(SPMeshGradient *mesh)
+{
+ if (_update) {
+ return;
+ }
+
+ g_assert(_meshmenu != nullptr);
+
+ /* Clear existing menu if any */
+ GtkTreeModel *store = gtk_combo_box_get_model(GTK_COMBO_BOX(_meshmenu));
+ gtk_list_store_clear(GTK_LIST_STORE(store));
+
+ ink_mesh_menu(_meshmenu);
+
+ /* Set history */
+
+ if (mesh && !_meshmenu_update) {
+ _meshmenu_update = true;
+ gchar const *meshname = mesh->getRepr()->attribute("id");
+
+ // Find this mesh and set it active in the combo_box
+ GtkTreeIter iter;
+ gchar *meshid = nullptr;
+ bool valid = gtk_tree_model_get_iter_first(store, &iter);
+ if (!valid) {
+ return;
+ }
+ gtk_tree_model_get(store, &iter, COMBO_COL_MESH, &meshid, -1);
+ while (valid && strcmp(meshid, meshname) != 0) {
+ valid = gtk_tree_model_iter_next(store, &iter);
+ g_free(meshid);
+ meshid = nullptr;
+ gtk_tree_model_get(store, &iter, COMBO_COL_MESH, &meshid, -1);
+ }
+
+ if (valid) {
+ gtk_combo_box_set_active_iter(GTK_COMBO_BOX(_meshmenu), &iter);
+ }
+
+ _meshmenu_update = false;
+ g_free(meshid);
+ }
+}
+
+#ifdef WITH_MESH
+void PaintSelector::set_mode_mesh(PaintSelector::Mode mode)
+{
+ if (mode == PaintSelector::MODE_GRADIENT_MESH) {
+ set_style_buttons(_mesh);
+ }
+ _style->set_sensitive(true);
+
+ if (_mode == PaintSelector::MODE_GRADIENT_MESH) {
+ /* Already have mesh menu */
+ // Do nothing - the Selector is already a Gtk::Box with the required contents
+ } else {
+ clear_frame();
+
+ if (!_selector_mesh) {
+ /* Create vbox */
+ _selector_mesh = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4));
+ _selector_mesh->set_homogeneous(false);
+
+ auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 1));
+ hb->set_homogeneous(false);
+
+ /**
+ * Create a combo_box and store with 4 columns,
+ * The label, a pointer to the mesh, is stockid or not, is a separator or not.
+ */
+ GtkListStore *store =
+ gtk_list_store_new(COMBO_N_COLS, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_STRING, G_TYPE_BOOLEAN);
+ GtkWidget *combo = _scrollprotected_combo_box_new_with_model(GTK_TREE_MODEL(store));
+ gtk_combo_box_set_row_separator_func(GTK_COMBO_BOX(combo), PaintSelector::isSeparator, nullptr, nullptr);
+
+ GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
+ gtk_cell_renderer_set_padding(renderer, 2, 0);
+ gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(combo), renderer, TRUE);
+ gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(combo), renderer, "text", COMBO_COL_LABEL, nullptr);
+
+ ink_mesh_menu(combo);
+ g_signal_connect(G_OBJECT(combo), "changed", G_CALLBACK(PaintSelector::mesh_change), this);
+ g_signal_connect(G_OBJECT(combo), "destroy", G_CALLBACK(PaintSelector::mesh_destroy), this);
+ _meshmenu = combo;
+ g_object_ref(G_OBJECT(combo));
+
+ gtk_container_add(GTK_CONTAINER(hb->gobj()), combo);
+ _selector_mesh->pack_start(*hb, false, false, AUX_BETWEEN_BUTTON_GROUPS);
+
+ g_object_unref(G_OBJECT(store));
+
+ auto hb2 = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0));
+ hb2->set_homogeneous(false);
+
+ auto l = Gtk::manage(new Gtk::Label());
+ l->set_markup(_("Use the <b>Mesh tool</b> to modify the mesh."));
+ l->set_line_wrap(true);
+ l->set_size_request(180, -1);
+ hb2->pack_start(*l, true, true, AUX_BETWEEN_BUTTON_GROUPS);
+ _selector_mesh->pack_start(*hb2, false, false, AUX_BETWEEN_BUTTON_GROUPS);
+ _selector_mesh->show_all();
+
+ _frame->add(*_selector_mesh);
+ }
+
+ _selector_mesh->show();
+ _label->set_markup(_("<b>Mesh fill</b>"));
+ }
+#ifdef SP_PS_VERBOSE
+ g_print("Mesh req\n");
+#endif
+}
+#endif // WITH_MESH
+
+SPMeshGradient *PaintSelector::getMeshGradient()
+{
+ g_return_val_if_fail((_mode == MODE_GRADIENT_MESH), NULL);
+
+ /* no mesh menu if we were just selected */
+ if (_meshmenu == nullptr) {
+ return nullptr;
+ }
+ GtkTreeModel *store = gtk_combo_box_get_model(GTK_COMBO_BOX(_meshmenu));
+
+ /* Get the selected mesh */
+ GtkTreeIter iter;
+ if (!gtk_combo_box_get_active_iter(GTK_COMBO_BOX(_meshmenu), &iter) ||
+ !gtk_list_store_iter_is_valid(GTK_LIST_STORE(store), &iter)) {
+ return nullptr;
+ }
+
+ gchar *meshid = nullptr;
+ gboolean stockid = FALSE;
+ // gchar *label = nullptr;
+ gtk_tree_model_get(store, &iter, COMBO_COL_STOCK, &stockid, COMBO_COL_MESH, &meshid, -1);
+ // gtk_tree_model_get (store, &iter, COMBO_COL_LABEL, &label, COMBO_COL_STOCK, &stockid, COMBO_COL_MESH, &meshid,
+ // -1); std::cout << " .. meshid: " << (meshid?meshid:"null") << " label: " << (label?label:"null") << std::endl;
+ // g_free(label);
+ if (meshid == nullptr) {
+ return nullptr;
+ }
+
+ SPMeshGradient *mesh = nullptr;
+ if (strcmp(meshid, "none")) {
+
+ gchar *mesh_name;
+ if (stockid) {
+ mesh_name = g_strconcat("urn:inkscape:mesh:", meshid, nullptr);
+ } else {
+ mesh_name = g_strdup(meshid);
+ }
+
+ SPObject *mesh_obj = get_stock_item(mesh_name);
+ if (mesh_obj && is<SPMeshGradient>(mesh_obj)) {
+ mesh = cast<SPMeshGradient>(mesh_obj);
+ }
+ g_free(mesh_name);
+ } else {
+ std::cerr << "PaintSelector::getMeshGradient: Unexpected meshid value." << std::endl;
+ }
+
+ g_free(meshid);
+
+ return mesh;
+}
+
+#endif
+// ************************ End Mesh ************************
+
+void PaintSelector::set_style_buttons(Gtk::ToggleButton *active)
+{
+ _none->set_active(active == _none);
+ _solid->set_active(active == _solid);
+ _gradient->set_active(active == _gradient);
+ _radial->set_active(active == _radial);
+#ifdef WITH_MESH
+ _mesh->set_active(active == _mesh);
+#endif
+ _pattern->set_active(active == _pattern);
+ _swatch->set_active(active == _swatch);
+ _unset->set_active(active == _unset);
+}
+
+void PaintSelector::pattern_destroy(GtkWidget *widget, PaintSelector * /*psel*/)
+{
+ // drop our reference to the pattern menu widget
+ g_object_unref(G_OBJECT(widget));
+}
+
+void PaintSelector::pattern_change(GtkWidget * /*widget*/, PaintSelector *psel) { psel->_signal_changed.emit(); }
+
+
+/*update pattern list*/
+void PaintSelector::updatePatternList(SPPattern *pattern)
+{
+ if (_update) return;
+ if (!_selector_pattern) return;
+
+ _selector_pattern->set_selected(pattern);
+}
+
+void PaintSelector::set_mode_pattern(PaintSelector::Mode mode)
+{
+ if (mode == PaintSelector::MODE_PATTERN) {
+ set_style_buttons(_pattern);
+ }
+
+ _style->set_sensitive(true);
+
+ if (_mode == PaintSelector::MODE_PATTERN) {
+ /* Already have pattern menu */
+ } else {
+ clear_frame();
+
+ if (!_selector_pattern) {
+ _selector_pattern = Gtk::manage(new PatternEditor("/pattern-edit", PatternManager::get()));
+ _selector_pattern->signal_changed().connect([=](){ _signal_changed.emit(); });
+ _selector_pattern->signal_color_changed().connect([=](unsigned){ _signal_changed.emit(); });
+ _selector_pattern->signal_edit().connect([=](){ _signal_edit_pattern.emit(); });
+ _selector_pattern->show_all();
+ _frame->add(*_selector_pattern);
+ }
+
+ SPDocument* document = SP_ACTIVE_DOCUMENT;
+ _selector_pattern->set_document(document);
+ _selector_pattern->show();
+ _label->hide();
+ }
+#ifdef SP_PS_VERBOSE
+ g_print("Pattern req\n");
+#endif
+}
+
+void PaintSelector::set_mode_hatch(PaintSelector::Mode mode)
+{
+ if (mode == PaintSelector::MODE_HATCH) {
+ set_style_buttons(_unset);
+ }
+
+ _style->set_sensitive(true);
+
+ if (_mode == PaintSelector::MODE_HATCH) {
+ /* Already have hatch menu, for the moment unset */
+ } else {
+ clear_frame();
+
+ _label->set_markup(_("<b>Hatch fill</b>"));
+ }
+#ifdef SP_PS_VERBOSE
+ g_print("Hatch req\n");
+#endif
+}
+
+gboolean PaintSelector::isSeparator(GtkTreeModel *model, GtkTreeIter *iter, gpointer /*data*/)
+{
+
+ gboolean sep = FALSE;
+ gtk_tree_model_get(model, iter, COMBO_COL_SEP, &sep, -1);
+ return sep;
+}
+
+std::optional<unsigned int> PaintSelector::get_pattern_color() {
+ if (!_selector_pattern) return 0;
+
+ return _selector_pattern->get_selected_color();
+}
+
+Geom::Affine PaintSelector::get_pattern_transform() {
+ Geom::Affine matrix;
+ if (!_selector_pattern) return matrix;
+
+ return _selector_pattern->get_selected_transform();
+}
+
+Geom::Point PaintSelector::get_pattern_offset() {
+ Geom::Point offset;
+ if (!_selector_pattern) return offset;
+
+ return _selector_pattern->get_selected_offset();
+}
+
+Geom::Scale PaintSelector::get_pattern_gap() {
+ Geom::Scale gap(0, 0);
+ if (!_selector_pattern) return gap;
+
+ return _selector_pattern->get_selected_gap();
+}
+
+Glib::ustring PaintSelector::get_pattern_label() {
+ if (!_selector_pattern) return Glib::ustring();
+
+ return _selector_pattern->get_label();
+}
+
+bool PaintSelector::is_pattern_scale_uniform() {
+ if (!_selector_pattern) return false;
+
+ return _selector_pattern->is_selected_scale_uniform();
+}
+
+SPPattern* PaintSelector::getPattern() {
+ g_return_val_if_fail(_mode == MODE_PATTERN, nullptr);
+
+ if (!_selector_pattern) return nullptr;
+
+ auto sel = _selector_pattern->get_selected();
+ auto stock_doc = sel.second;
+
+ if (sel.first.empty()) return nullptr;
+
+ auto patid = sel.first;
+ SPObject* pat_obj = nullptr;
+ if (patid != "none") {
+ if (stock_doc) {
+ patid = "urn:inkscape:pattern:" + patid;
+ }
+ pat_obj = get_stock_item(patid.c_str(), stock_doc != nullptr, stock_doc);
+ } else {
+ SPDocument *doc = SP_ACTIVE_DOCUMENT;
+ pat_obj = doc->getObjectById(patid);
+ }
+
+ return cast<SPPattern>(pat_obj);
+}
+
+void PaintSelector::set_mode_swatch(PaintSelector::Mode mode)
+{
+ if (mode == PaintSelector::MODE_SWATCH) {
+ set_style_buttons(_swatch);
+ }
+
+ _style->set_sensitive(true);
+
+ if (_mode == PaintSelector::MODE_SWATCH) {
+ // Do nothing. The selector is already a SwatchSelector
+ } else {
+ clear_frame();
+
+ if (!_selector_swatch) {
+ // Create new gradient selector
+ _selector_swatch = Gtk::manage(new SwatchSelector());
+
+ auto gsel = _selector_swatch->getGradientSelector();
+ gsel->signal_grabbed().connect(sigc::mem_fun(*this, &PaintSelector::gradient_grabbed));
+ gsel->signal_dragged().connect(sigc::mem_fun(*this, &PaintSelector::gradient_dragged));
+ gsel->signal_released().connect(sigc::mem_fun(*this, &PaintSelector::gradient_released));
+ gsel->signal_changed().connect(sigc::mem_fun(*this, &PaintSelector::gradient_changed));
+
+ // Pack everything to frame
+ _frame->add(*_selector_swatch);
+ } else {
+ // Necessary when creating new swatches via the Fill and Stroke dialog
+ _selector_swatch->setVector(nullptr, nullptr);
+ }
+ _selector_swatch->show();
+ _label->set_markup(_("<b>Swatch fill</b>"));
+ }
+
+#ifdef SP_PS_VERBOSE
+ g_print("Swatch req\n");
+#endif
+}
+
+// TODO this seems very bad to be taking in a desktop pointer to muck with. Logic probably belongs elsewhere
+void PaintSelector::setFlatColor(SPDesktop *desktop, gchar const *color_property, gchar const *opacity_property)
+{
+ SPCSSAttr *css = sp_repr_css_attr_new();
+
+ SPColor color;
+ gfloat alpha = 0;
+ getColorAlpha(color, alpha);
+
+ std::string colorStr = color.toString();
+
+#ifdef SP_PS_VERBOSE
+ guint32 rgba = color.toRGBA32(alpha);
+ g_message("sp_paint_selector_set_flat_color() to '%s' from 0x%08x::%s", colorStr.c_str(), rgba,
+ (color.icc ? color.icc->colorProfile.c_str() : "<null>"));
+#endif // SP_PS_VERBOSE
+
+ sp_repr_css_set_property(css, color_property, colorStr.c_str());
+ Inkscape::CSSOStringStream osalpha;
+ osalpha << alpha;
+ sp_repr_css_set_property(css, opacity_property, osalpha.str().c_str());
+
+ sp_desktop_set_style(desktop, css);
+
+ sp_repr_css_attr_unref(css);
+}
+
+PaintSelector::Mode PaintSelector::getModeForStyle(SPStyle const &style, FillOrStroke kind)
+{
+ Mode mode = MODE_UNSET;
+ SPIPaint const &target = *style.getFillOrStroke(kind == FILL);
+
+ if (!target.set) {
+ mode = MODE_UNSET;
+ } else if (target.isPaintserver()) {
+ SPPaintServer const *server = kind == FILL ? style.getFillPaintServer() : style.getStrokePaintServer();
+
+#ifdef SP_PS_VERBOSE
+ g_message("PaintSelector::getModeForStyle(%p, %d)", &style, kind);
+ g_message("==== server:%p %s grad:%s swatch:%s", server, server->getId(),
+ (is<SPGradient>(server) ? "Y" : "n"),
+ (is<SPGradient>(server) && cast<SPGradient>(server)->getVector()->isSwatch() ? "Y" : "n"));
+#endif // SP_PS_VERBOSE
+
+
+ if (server && is<SPGradient>(server) && cast<SPGradient>(server)->getVector()->isSwatch()) {
+ mode = MODE_SWATCH;
+ } else if (is<SPLinearGradient>(server)) {
+ mode = MODE_GRADIENT_LINEAR;
+ } else if (is<SPRadialGradient>(server)) {
+ mode = MODE_GRADIENT_RADIAL;
+#ifdef WITH_MESH
+ } else if (is<SPMeshGradient>(server)) {
+ mode = MODE_GRADIENT_MESH;
+#endif
+ } else if (is<SPPattern>(server)) {
+ mode = MODE_PATTERN;
+ } else if (is<SPHatch>(server)) {
+ mode = MODE_HATCH;
+ } else {
+ g_warning("file %s: line %d: Unknown paintserver", __FILE__, __LINE__);
+ mode = MODE_NONE;
+ }
+ } else if (target.isColor()) {
+ // TODO this is no longer a valid assertion:
+ mode = MODE_SOLID_COLOR; // so far only rgb can be read from svg
+ } else if (target.isNone()) {
+ mode = MODE_NONE;
+ } else {
+ g_warning("file %s: line %d: Unknown paint type", __FILE__, __LINE__);
+ mode = MODE_NONE;
+ }
+
+ return mode;
+}
+
+} // 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/paint-selector.h b/src/ui/widget/paint-selector.h
new file mode 100644
index 0000000..34ebc17
--- /dev/null
+++ b/src/ui/widget/paint-selector.h
@@ -0,0 +1,236 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Generic paint selector widget
+ *//*
+ * Authors:
+ * Lauris
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef SEEN_SP_PAINT_SELECTOR_H
+#define SEEN_SP_PAINT_SELECTOR_H
+
+#include "color.h"
+#include "fill-or-stroke.h"
+#include <glib.h>
+#include <gtkmm/box.h>
+#include <optional>
+
+#include "object/sp-gradient-spread.h"
+#include "object/sp-gradient-units.h"
+#include "gradient-selector-interface.h"
+#include "ui/selected-color.h"
+#include "ui/widget/gradient-selector.h"
+#include "ui/widget/swatch-selector.h"
+
+class SPGradient;
+class SPLinearGradient;
+class SPRadialGradient;
+#ifdef WITH_MESH
+class SPMeshGradient;
+#endif
+class SPDesktop;
+class SPPattern;
+class SPStyle;
+
+namespace Gtk {
+class Label;
+class RadioButton;
+class ToggleButton;
+} // namespace Gtk
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class FillRuleRadioButton;
+class StyleToggleButton;
+class GradientEditor;
+class PatternEditor;
+
+/**
+ * Generic paint selector widget.
+ */
+class PaintSelector : public Gtk::Box {
+ public:
+ enum Mode {
+ MODE_EMPTY,
+ MODE_MULTIPLE,
+ MODE_NONE,
+ MODE_SOLID_COLOR,
+ MODE_GRADIENT_LINEAR,
+ MODE_GRADIENT_RADIAL,
+#ifdef WITH_MESH
+ MODE_GRADIENT_MESH,
+#endif
+ MODE_PATTERN,
+ MODE_HATCH,
+ MODE_SWATCH,
+ MODE_UNSET
+ };
+
+ enum FillRule { FILLRULE_NONZERO, FILLRULE_EVENODD };
+
+ private:
+ bool _update = false;
+
+ Mode _mode;
+
+ Gtk::Box *_style;
+ StyleToggleButton *_none;
+ StyleToggleButton *_solid;
+ StyleToggleButton *_gradient;
+ StyleToggleButton *_radial;
+#ifdef WITH_MESH
+ StyleToggleButton *_mesh;
+#endif
+ StyleToggleButton *_pattern;
+ StyleToggleButton *_swatch;
+ StyleToggleButton *_unset;
+
+ Gtk::Box *_fillrulebox;
+ FillRuleRadioButton *_evenodd;
+ FillRuleRadioButton *_nonzero;
+
+ Gtk::Box *_frame;
+
+ Gtk::Box *_selector_solid_color = nullptr;
+ GradientEditor *_selector_gradient = nullptr;
+ Gtk::Box *_selector_mesh = nullptr;
+ SwatchSelector *_selector_swatch = nullptr;
+ PatternEditor* _selector_pattern = nullptr;
+
+ Gtk::Label *_label;
+ GtkWidget *_patternmenu = nullptr;
+ bool _patternmenu_update = false;
+#ifdef WITH_MESH
+ GtkWidget *_meshmenu = nullptr;
+ bool _meshmenu_update = false;
+#endif
+
+ Inkscape::UI::SelectedColor *_selected_color;
+ bool _updating_color;
+
+ void getColorAlpha(SPColor &color, gfloat &alpha) const;
+
+ static gboolean isSeparator(GtkTreeModel *model, GtkTreeIter *iter, gpointer data);
+
+ private:
+ sigc::signal<void (FillRule)> _signal_fillrule_changed;
+ sigc::signal<void ()> _signal_dragged;
+ sigc::signal<void (Mode, bool)> _signal_mode_changed;
+ sigc::signal<void ()> _signal_grabbed;
+ sigc::signal<void ()> _signal_released;
+ sigc::signal<void ()> _signal_changed;
+ sigc::signal<void (SPStop*)> _signal_stop_selected;
+ sigc::signal<void> _signal_edit_pattern;
+
+ StyleToggleButton *style_button_add(gchar const *px, PaintSelector::Mode mode, gchar const *tip);
+ void style_button_toggled(StyleToggleButton *tb);
+ void fillrule_toggled(FillRuleRadioButton *tb);
+ void onSelectedColorGrabbed();
+ void onSelectedColorDragged();
+ void onSelectedColorReleased();
+ void onSelectedColorChanged();
+ void set_mode_empty();
+ void set_style_buttons(Gtk::ToggleButton *active);
+ void set_mode_multiple();
+ void set_mode_none();
+ GradientSelectorInterface *getGradientFromData() const;
+ void clear_frame();
+ void set_mode_unset();
+ void set_mode_color(PaintSelector::Mode mode);
+ void set_mode_gradient(PaintSelector::Mode mode);
+#ifdef WITH_MESH
+ void set_mode_mesh(PaintSelector::Mode mode);
+#endif
+ void set_mode_pattern(PaintSelector::Mode mode);
+ void set_mode_hatch(PaintSelector::Mode mode);
+ void set_mode_swatch(PaintSelector::Mode mode);
+ void set_mode_ex(Mode mode, bool switch_style);
+
+ void gradient_grabbed();
+ void gradient_dragged();
+ void gradient_released();
+ void gradient_changed(SPGradient *gr);
+
+ static void mesh_change(GtkWidget *widget, PaintSelector *psel);
+ static void mesh_destroy(GtkWidget *widget, PaintSelector *psel);
+
+ static void pattern_change(GtkWidget *widget, PaintSelector *psel);
+ static void pattern_destroy(GtkWidget *widget, PaintSelector *psel);
+
+ public:
+ PaintSelector(FillOrStroke kind);
+ ~PaintSelector() override;
+
+ inline decltype(_signal_fillrule_changed) signal_fillrule_changed() const { return _signal_fillrule_changed; }
+ inline decltype(_signal_dragged) signal_dragged() const { return _signal_dragged; }
+ inline decltype(_signal_mode_changed) signal_mode_changed() const { return _signal_mode_changed; }
+ inline decltype(_signal_grabbed) signal_grabbed() const { return _signal_grabbed; }
+ inline decltype(_signal_released) signal_released() const { return _signal_released; }
+ inline decltype(_signal_changed) signal_changed() const { return _signal_changed; }
+ inline decltype(_signal_stop_selected) signal_stop_selected() const { return _signal_stop_selected; }
+ inline decltype(_signal_edit_pattern) signal_edit_pattern() const { return _signal_edit_pattern; }
+
+ void setMode(Mode mode);
+ static Mode getModeForStyle(SPStyle const &style, FillOrStroke kind);
+ void setFillrule(FillRule fillrule);
+ void setColorAlpha(SPColor const &color, float alpha);
+ void setSwatch(SPGradient *vector);
+ void setGradientLinear(SPGradient *vector, SPLinearGradient* gradient, SPStop* selected);
+ void setGradientRadial(SPGradient *vector, SPRadialGradient* gradient, SPStop* selected);
+#ifdef WITH_MESH
+ void setGradientMesh(SPMeshGradient *array);
+#endif
+ void setGradientProperties(SPGradientUnits units, SPGradientSpread spread);
+ void getGradientProperties(SPGradientUnits &units, SPGradientSpread &spread) const;
+
+#ifdef WITH_MESH
+ SPMeshGradient *getMeshGradient();
+ void updateMeshList(SPMeshGradient *pat);
+#endif
+
+ void updatePatternList(SPPattern *pat);
+ inline decltype(_mode) get_mode() const { return _mode; }
+
+ // TODO move this elsewhere:
+ void setFlatColor(SPDesktop *desktop, const gchar *color_property, const gchar *opacity_property);
+
+ SPGradient *getGradientVector();
+ void pushAttrsToGradient(SPGradient *gr) const;
+ SPPattern *getPattern();
+ std::optional<unsigned int> get_pattern_color();
+ Geom::Affine get_pattern_transform();
+ Geom::Point get_pattern_offset();
+ Geom::Scale get_pattern_gap();
+ Glib::ustring get_pattern_label();
+ bool is_pattern_scale_uniform();
+};
+
+enum {
+ COMBO_COL_LABEL = 0,
+ COMBO_COL_STOCK = 1,
+ COMBO_COL_PATTERN = 2,
+ COMBO_COL_MESH = COMBO_COL_PATTERN,
+ COMBO_COL_SEP = 3,
+ COMBO_N_COLS = 4
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+#endif // SEEN_SP_PAINT_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/pattern-editor.cpp b/src/ui/widget/pattern-editor.cpp
new file mode 100644
index 0000000..e31cad0
--- /dev/null
+++ b/src/ui/widget/pattern-editor.cpp
@@ -0,0 +1,685 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * Pattern editor widget for "Fill and Stroke" dialog
+ *
+ * Copyright (C) 2022 Michael Kowalski
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "pattern-editor.h"
+
+#include <gtkmm/widget.h>
+#include <optional>
+#include <gtkmm/builder.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/togglebutton.h>
+#include <gtkmm/button.h>
+#include <gtkmm/menubutton.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/treemodelcolumn.h>
+#include <glibmm/i18n.h>
+#include <cairo.h>
+#include <iomanip>
+
+#include "object/sp-defs.h"
+#include "object/sp-root.h"
+#include "style.h"
+#include "ui/builder-utils.h"
+#include "ui/svg-renderer.h"
+#include "io/resource.h"
+#include "manipulation/copy-resource.h"
+#include "pattern-manager.h"
+#include "pattern-manipulation.h"
+#include "preferences.h"
+#include "util/units.h"
+#include "widgets/spw-utilities.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+using namespace Inkscape::IO;
+
+// default size of pattern image in a list
+static const int ITEM_WIDTH = 45;
+
+// get slider position 'index' (linear) and transform that into gap percentage (non-linear)
+static double slider_to_gap(double index, double upper) {
+ auto v = std::tan(index / (upper + 1) * M_PI / 2.0) * 500;
+ return std::round(v / 20) * 20;
+}
+// transform gap percentage value into slider position
+static double gap_to_slider(double gap, double upper) {
+ return std::atan(gap / 500) * (upper + 1) / M_PI * 2;
+}
+
+// tile size slider functions
+static int slider_to_tile(double index) {
+ return 30 + static_cast<int>(index) * 5;
+}
+static double tile_to_slider(int tile) {
+ return (tile - 30) / 5.0;
+}
+
+Glib::ustring get_attrib(SPPattern* pattern, const char* attrib) {
+ auto value = pattern->getAttribute(attrib);
+ return value ? value : "";
+}
+
+double get_attrib_num(SPPattern* pattern, const char* attrib) {
+ auto val = get_attrib(pattern, attrib);
+ return strtod(val.c_str(), nullptr);
+}
+
+const double ANGLE_STEP = 15.0;
+
+PatternEditor::PatternEditor(const char* prefs, Inkscape::PatternManager& manager) :
+ _manager(manager),
+ _builder(create_builder("pattern-edit.glade")),
+ _offset_x(get_widget<Gtk::SpinButton>(_builder, "offset-x")),
+ _offset_y(get_widget<Gtk::SpinButton>(_builder, "offset-y")),
+ _scale_x(get_widget<Gtk::SpinButton>(_builder, "scale-x")),
+ _scale_y(get_widget<Gtk::SpinButton>(_builder, "scale-y")),
+ _angle_btn(get_widget<Gtk::SpinButton>(_builder, "angle")),
+ _orient_slider(get_widget<Gtk::Scale>(_builder, "orient")),
+ _gap_x_slider(get_widget<Gtk::Scale>(_builder, "gap-x")),
+ _gap_y_slider(get_widget<Gtk::Scale>(_builder, "gap-y")),
+ _edit_btn(get_widget<Gtk::Button>(_builder, "edit-pattern")),
+ _preview_img(get_widget<Gtk::Image>(_builder, "preview")),
+ _preview(get_widget<Gtk::Viewport>(_builder, "preview-box")),
+ _color_btn(get_widget<Gtk::Button>(_builder, "color-btn")),
+ _color_label(get_widget<Gtk::Label>(_builder, "color-label")),
+ _paned(get_widget<Gtk::Paned>(_builder, "paned")),
+ _main_grid(get_widget<Gtk::Box>(_builder, "main-box")),
+ _input_grid(get_widget<Gtk::Grid>(_builder, "input-grid")),
+ _stock_gallery(get_widget<Gtk::FlowBox>(_builder, "flowbox")),
+ _doc_gallery(get_widget<Gtk::FlowBox>(_builder, "doc-flowbox")),
+ _link_scale(get_widget<Gtk::Button>(_builder, "link-scale")),
+ _name_box(get_widget<Gtk::Entry>(_builder, "pattern-name")),
+ _combo_set(get_widget<Gtk::ComboBoxText>(_builder, "pattern-combo")),
+ _search_box(get_widget<Gtk::SearchEntry>(_builder, "search")),
+ _tile_slider(get_widget<Gtk::Scale>(_builder, "tile-slider")),
+ _show_names(get_widget<Gtk::CheckButton>(_builder, "show-names")),
+ _prefs(prefs)
+{
+ _color_picker = std::make_unique<ColorPicker>(
+ _("Pattern color"), "", 0x7f7f7f00, true,
+ &get_widget<Gtk::Button>(_builder, "color-btn"));
+ _color_picker->use_transparency(false);
+ _color_picker->connectChanged([=](guint color){
+ if (_update.pending()) return;
+ _signal_color_changed.emit(color);
+ });
+
+ _tile_size = Inkscape::Preferences::get()->getIntLimited(_prefs + "/tileSize", ITEM_WIDTH, 30, 1000);
+ _tile_slider.set_value(tile_to_slider(_tile_size));
+ _tile_slider.signal_change_value().connect([=](Gtk::ScrollType st, double value){
+ if (_update.pending()) return true;
+ auto scoped(_update.block());
+ auto size = slider_to_tile(value);
+ if (size != _tile_size) {
+ _tile_slider.set_value(tile_to_slider(size));
+ // change pattern tile size
+ _tile_size = size;
+ update_pattern_tiles();
+ Inkscape::Preferences::get()->setInt(_prefs + "/tileSize", size);
+ }
+ return true;
+ });
+
+ auto show_labels = Inkscape::Preferences::get()->getBool(_prefs + "/showLabels", false);
+ _show_names.set_active(show_labels);
+ _show_names.signal_toggled().connect([=](){
+ // toggle pattern labels
+ _stock_pattern_store.store.refresh();
+ _doc_pattern_store.store.refresh();
+ Inkscape::Preferences::get()->setBool(_prefs + "/showLabels", _show_names.get_active());
+ });
+
+ const auto max = 180.0 / ANGLE_STEP;
+ _orient_slider.set_range(-max, max);
+ _orient_slider.set_increments(1, 1);
+ _orient_slider.set_digits(0);
+ _orient_slider.set_value(0);
+ _orient_slider.signal_change_value().connect([=](Gtk::ScrollType st, double value){
+ if (_update.pending()) return false;
+ auto scoped(_update.block());
+ // slider works with 15deg discrete steps
+ _angle_btn.set_value(round(CLAMP(value, -max, max)) * ANGLE_STEP);
+ _signal_changed.emit();
+ return true;
+ });
+
+ for (auto slider : {&_gap_x_slider, &_gap_y_slider}) {
+ slider->set_increments(1, 1);
+ slider->set_digits(0);
+ slider->set_value(0);
+ slider->signal_format_value().connect([=](double val){
+ auto upper = slider->get_adjustment()->get_upper();
+ return Glib::ustring::format(std::fixed, std::setprecision(0), slider_to_gap(val, upper)) + "%";
+ });
+ slider->signal_change_value().connect([=](Gtk::ScrollType st, double value){
+ if (_update.pending()) return false;
+ _signal_changed.emit();
+ return true;
+ });
+ }
+
+ _angle_btn.signal_value_changed().connect([=]() {
+ if (_update.pending() || !_angle_btn.is_sensitive()) return;
+ auto scoped(_update.block());
+ auto angle = _angle_btn.get_value();
+ _orient_slider.set_value(round(angle / ANGLE_STEP));
+ _signal_changed.emit();
+ });
+
+ _link_scale.signal_clicked().connect([=](){
+ if (_update.pending()) return;
+ auto scoped(_update.block());
+ _scale_linked = !_scale_linked;
+ if (_scale_linked) {
+ // this is simplistic
+ _scale_x.set_value(_scale_y.get_value());
+ }
+ update_scale_link();
+ _signal_changed.emit();
+ });
+
+ for (auto el : {&_scale_x, &_scale_y, &_offset_x, &_offset_y}) {
+ el->signal_value_changed().connect([=]() {
+ if (_update.pending()) return;
+ if (_scale_linked && (el == &_scale_x || el == &_scale_y)) {
+ auto scoped(_update.block());
+ // enforce uniform scaling
+ (el == &_scale_x) ? _scale_y.set_value(el->get_value()) : _scale_x.set_value(el->get_value());
+ }
+ _signal_changed.emit();
+ });
+ }
+
+ _name_box.signal_changed().connect([=](){
+ if (_update.pending()) return;
+
+ _signal_changed.emit();
+ });
+
+ _search_box.signal_search_changed().connect([=](){
+ if (_update.pending()) return;
+
+ // filter patterns
+ _filter_text = _search_box.get_text();
+ apply_filter(false);
+ apply_filter(true);
+ });
+
+ // populate combo box with all patern categories
+ auto pattern_categories = _manager.get_categories()->children();
+ int cat_count = pattern_categories.size();
+ for (auto row : pattern_categories) {
+ auto name = row.get_value(_manager.columns.name);
+ _combo_set.append(name);
+ }
+
+ get_widget<Gtk::Button>(_builder, "previous").signal_clicked().connect([=](){
+ int previous = _combo_set.get_active_row_number() - 1;
+ if (previous >= 0) _combo_set.set_active(previous);
+ });
+ get_widget<Gtk::Button>(_builder, "next").signal_clicked().connect([=](){
+ auto next = _combo_set.get_active_row_number() + 1;
+ if (next < cat_count) _combo_set.set_active(next);
+ });
+ _combo_set.signal_changed().connect([=](){
+ // select pattern category to show
+ auto index = _combo_set.get_active_row_number();
+ select_pattern_set(index);
+ Inkscape::Preferences::get()->setInt(_prefs + "/currentSet", index);
+ });
+
+ bind_store(_doc_gallery, _doc_pattern_store);
+ bind_store(_stock_gallery, _stock_pattern_store);
+
+ _stock_gallery.signal_child_activated().connect([=](Gtk::FlowBoxChild* box){
+ if (_update.pending()) return;
+ auto scoped(_update.block());
+ auto pat = _stock_pattern_store.widgets_to_pattern[box];
+ update_ui(pat);
+ _doc_gallery.unselect_all();
+ _signal_changed.emit();
+ });
+
+ _doc_gallery.signal_child_activated().connect([=](Gtk::FlowBoxChild* box){
+ if (_update.pending()) return;
+ auto scoped(_update.block());
+ auto pat = _doc_pattern_store.widgets_to_pattern[box];
+ update_ui(pat);
+ _stock_gallery.unselect_all();
+ _signal_changed.emit();
+ });
+
+ _edit_btn.signal_clicked().connect([=](){
+ _signal_edit.emit();
+ });
+
+ _paned.set_position(Inkscape::Preferences::get()->getIntLimited(_prefs + "/handlePos", 50, 10, 9999));
+ _paned.property_position().signal_changed().connect([=](){
+ Inkscape::Preferences::get()->setInt(_prefs + "/handlePos", _paned.get_position());
+ });
+
+ // current pattern category
+ _combo_set.set_active(Inkscape::Preferences::get()->getIntLimited(_prefs + "/currentSet", 0, 0, std::max(cat_count - 1, 0)));
+
+ update_scale_link();
+ pack_start(_main_grid);
+}
+
+PatternEditor::~PatternEditor() noexcept {}
+
+void PatternEditor::bind_store(Gtk::FlowBox& list, PatternStore& pat) {
+ pat.store.set_filter([=](const Glib::RefPtr<PatternItem>& p){
+ if (!p) return false;
+ if (_filter_text.empty()) return true;
+
+ auto name = Glib::ustring(p->label).lowercase();
+ auto expr = _filter_text.lowercase();
+ auto pos = name.find(expr);
+ return pos != Glib::ustring::npos;
+ });
+
+ list.bind_list_store(pat.store.get_store(), [=, &pat](const Glib::RefPtr<PatternItem>& item){
+ auto box = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_VERTICAL);
+ auto image = Gtk::make_managed<Gtk::Image>(item->pix);
+ box->pack_start(*image);
+ auto name = Glib::ustring(item->label.c_str());
+ if (_show_names.get_active()) {
+ auto label = Gtk::make_managed<Gtk::Label>(name);
+ label->get_style_context()->add_class("small-font");
+ // limit label size to tile size
+ label->set_ellipsize(Pango::EllipsizeMode::ELLIPSIZE_END);
+ label->set_max_width_chars(0);
+ label->set_size_request(_tile_size);
+ box->pack_end(*label);
+ }
+ image->set_tooltip_text(name);
+ box->show_all();
+ auto cbox = Gtk::make_managed<Gtk::FlowBoxChild>();
+ cbox->add(*box);
+ cbox->get_style_context()->add_class("pattern-item-box");
+ pat.widgets_to_pattern[cbox] = item;
+ cbox->set_size_request(_tile_size, _tile_size);
+ return cbox;
+ });
+}
+
+void PatternEditor::select_pattern_set(int index) {
+ auto sets = _manager.get_categories()->children();
+ if (index >= 0 && index < sets.size()) {
+ auto row = sets[index];
+ if (auto category = row.get_value(_manager.columns.category)) {
+ set_stock_patterns(category->patterns);
+ }
+ }
+}
+
+void PatternEditor::update_scale_link() {
+ _link_scale.remove();
+ _link_scale.add(get_widget<Gtk::Image>(_builder, _scale_linked ? "image-linked" : "image-unlinked"));
+}
+
+void PatternEditor::update_widgets_from_pattern(Glib::RefPtr<PatternItem>& pattern) {
+ _input_grid.set_sensitive(!!pattern);
+
+ PatternItem empty;
+ const auto& item = pattern ? *pattern.get() : empty;
+
+ _name_box.set_text(item.label.c_str());
+
+ _scale_x.set_value(item.transform.xAxis().length());
+ _scale_y.set_value(item.transform.yAxis().length());
+
+ // TODO if needed
+ // auto units = get_attrib(pattern, "patternUnits");
+
+ _scale_linked = item.uniform_scale;
+ update_scale_link();
+
+ _offset_x.set_value(item.offset.x());
+ _offset_y.set_value(item.offset.y());
+
+ auto degrees = 180.0 / M_PI * Geom::atan2(item.transform.xAxis());
+ _orient_slider.set_value(round(degrees / ANGLE_STEP));
+ _angle_btn.set_value(degrees);
+
+ double x_index = gap_to_slider(item.gap[Geom::X], _gap_x_slider.get_adjustment()->get_upper());
+ _gap_x_slider.set_value(x_index);
+ double y_index = gap_to_slider(item.gap[Geom::Y], _gap_y_slider.get_adjustment()->get_upper());
+ _gap_y_slider.set_value(y_index);
+
+ if (item.color.has_value()) {
+ _color_picker->setRgba32(item.color->toRGBA32(1.0));
+ _color_btn.set_sensitive();
+ _color_label.set_opacity(1.0); // hack: sensitivity doesn't change appearance, so using opacity directly
+ }
+ else {
+ _color_picker->setRgba32(0);
+ _color_btn.set_sensitive(false);
+ _color_label.set_opacity(0.6);
+ _color_picker->closeWindow();
+ }
+}
+
+void PatternEditor::update_ui(Glib::RefPtr<PatternItem> pattern) {
+ update_widgets_from_pattern(pattern);
+}
+
+// sort patterns in-place by name/id
+void sort_patterns(std::vector<Glib::RefPtr<PatternItem>>& list) {
+ std::sort(list.begin(), list.end(), [](Glib::RefPtr<PatternItem>& a, Glib::RefPtr<PatternItem>& b) {
+ if (!a || !b) return false;
+ if (a->label == b->label) {
+ return a->id < b->id;
+ }
+ return a->label < b->label;
+ });
+}
+
+// given a pattern, create a PatternItem instance that describes it;
+// input pattern can be a link or a root pattern
+Glib::RefPtr<PatternItem> create_pattern_item(PatternManager& manager, SPPattern* pattern, int tile_size, double scale) {
+ auto item = manager.get_item(pattern);
+ if (item && scale > 0) {
+ item->pix = manager.get_image(pattern, tile_size, tile_size, scale);
+ }
+ return item;
+}
+
+// update editor UI
+void PatternEditor::set_selected(SPPattern* pattern) {
+ auto scoped(_update.block());
+
+ _stock_gallery.unselect_all();
+
+ // current pattern (should be a link)
+ auto link_pattern = pattern;
+ if (pattern) pattern = pattern->rootPattern();
+
+ if (pattern && pattern != link_pattern) {
+ _current_pattern.id = pattern->getId();
+ _current_pattern.link_id = link_pattern->getId();
+ }
+ else {
+ _current_pattern.id.clear();
+ _current_pattern.link_id.clear();
+ }
+
+ auto item = create_pattern_item(_manager, link_pattern, 0, 0);
+
+ update_widgets_from_pattern(item);
+
+ auto list = update_doc_pattern_list(pattern ? pattern->document : nullptr);
+ if (pattern) {
+ // patch up tile image on a list of document root patterns, it might have changed;
+ // color attribute for instance is being set directly on the root pattern;
+ // other attributes are per-object, so should not be taken into account when rendering tile
+ for (auto& pattern_item : list) {
+ if (pattern_item->id == item->id && pattern_item->collection == nullptr) {
+ // update preview
+ const double device_scale = get_scale_factor();
+ pattern_item->pix = _manager.get_image(pattern, _tile_size, _tile_size, device_scale);
+ item->pix = pattern_item->pix;
+ break;
+ }
+ }
+ }
+
+ set_active(_doc_gallery, _doc_pattern_store, item);
+
+ // generate large preview of selected pattern
+ Cairo::RefPtr<Cairo::Surface> surface;
+ if (link_pattern) {
+ const double device_scale = get_scale_factor();
+ auto size = _preview.get_allocation();
+ const int m = 1;
+ if (size.get_width() <= m || size.get_height() <= m) {
+ // widgets not resized yet, choose arbitrary size, so preview is not missing when widget is shown
+ size.set_width(200);
+ size.set_height(200);
+ }
+ // use white for checkerboard since most stock patterns are black
+ unsigned int background = 0xffffffff;
+ surface = _manager.get_preview(link_pattern, size.get_width(), size.get_height(), background, device_scale);
+ }
+ _preview_img.set(surface);
+}
+
+// generate preview images for patterns
+std::vector<Glib::RefPtr<PatternItem>> create_pattern_items(PatternManager& manager, const std::vector<SPPattern*>& list, int tile_size, double device_scale) {
+ std::vector<Glib::RefPtr<PatternItem>> output;
+ output.reserve(list.size());
+
+ for (auto pat : list) {
+ if (auto item = create_pattern_item(manager, pat, tile_size, device_scale)) {
+ output.push_back(item);
+ }
+ }
+
+ return output;
+}
+
+// populate store with document patterns if list has changed, minimize amount of work by using cached previews
+std::vector<Glib::RefPtr<PatternItem>> PatternEditor::update_doc_pattern_list(SPDocument* document) {
+ auto list = sp_get_pattern_list(document);
+ std::shared_ptr<SPDocument> nil;
+ const double device_scale = get_scale_factor();
+ // create pattern items (cheap), but skip preview generation (expansive)
+ auto patterns = create_pattern_items(_manager, list, 0, 0);
+ bool modified = false;
+ for (auto&& item : patterns) {
+ auto it = _cached_items.find(item->id);
+ if (it != end(_cached_items)) {
+ // reuse cached preview image
+ if (!item->pix) item->pix = it->second->pix;
+ }
+ else {
+ if (!item->pix) {
+ // generate preview for newly added pattern
+ item->pix = _manager.get_image(cast<SPPattern>(document->getObjectById(item->id)), _tile_size, _tile_size, device_scale);
+ }
+ modified = true;
+ _cached_items[item->id] = item;
+ }
+ }
+
+ update_store(patterns, _doc_gallery, _doc_pattern_store);
+
+ return patterns;
+}
+
+void PatternEditor::set_document(SPDocument* document) {
+ _current_document = document;
+ _cached_items.clear();
+ update_doc_pattern_list(document);
+}
+
+// populate store with stock patterns
+void PatternEditor::set_stock_patterns(const std::vector<SPPattern*>& list) {
+ const double device_scale = get_scale_factor();
+ auto patterns = create_pattern_items(_manager, list, _tile_size, device_scale);
+ sort_patterns(patterns);
+ update_store(patterns, _stock_gallery, _stock_pattern_store);
+}
+
+void PatternEditor::apply_filter(bool stock) {
+ auto scoped(_update.block());
+ if (!stock) {
+ _doc_pattern_store.store.apply_filter();
+ }
+ else {
+ _stock_pattern_store.store.apply_filter();
+ }
+}
+
+void PatternEditor::update_store(const std::vector<Glib::RefPtr<PatternItem>>& list, Gtk::FlowBox& gallery, PatternStore& pat) {
+ auto selected = get_active(gallery, pat);
+ if (pat.store.assign(list)) {
+ // reselect current
+ set_active(gallery, pat, selected);
+ }
+}
+
+Glib::RefPtr<PatternItem> PatternEditor::get_active(Gtk::FlowBox& gallery, PatternStore& pat) {
+ auto empty = Glib::RefPtr<PatternItem>();
+
+ auto sel = gallery.get_selected_children();
+ if (sel.size() == 1) {
+ return pat.widgets_to_pattern[sel.front()];
+ }
+ else {
+ return empty;
+ }
+}
+
+std::pair<Glib::RefPtr<PatternItem>, SPDocument*> PatternEditor::get_active() {
+ SPDocument* stock = nullptr;
+ auto sel = get_active(_doc_gallery, _doc_pattern_store);
+ if (!sel) {
+ sel = get_active(_stock_gallery, _stock_pattern_store);
+ stock = sel ? sel->collection : nullptr;
+ }
+ return std::make_pair(sel, stock);
+}
+
+void PatternEditor::set_active(Gtk::FlowBox& gallery, PatternStore& pat, Glib::RefPtr<PatternItem> item) {
+ bool selected = false;
+ if (item) {
+ gallery.foreach([=,&selected,&pat,&gallery](Gtk::Widget& widget){
+ if (auto box = dynamic_cast<Gtk::FlowBoxChild*>(&widget)) {
+ if (auto pattern = pat.widgets_to_pattern[box]) {
+ if (pattern->id == item->id && pattern->collection == item->collection) {
+ gallery.select_child(*box);
+ if (item->pix) {
+ // update preview, it might be stale
+ sp_traverse_widget_tree(box->get_child(), [&](Gtk::Widget* widget){
+ if (auto image = dynamic_cast<Gtk::Image*>(widget)) {
+ image->set(item->pix);
+ return true; // stop
+ }
+ return false; // continue
+ });
+ }
+ selected = true;
+ }
+ }
+ }
+ });
+ }
+
+ if (!selected) {
+ gallery.unselect_all();
+ }
+}
+
+std::pair<std::string, SPDocument*> PatternEditor::get_selected() {
+ // document patterns first
+ auto active = get_active();
+ auto sel = active.first;
+ auto stock_doc = active.second;
+ std::string id;
+ if (sel) {
+ if (stock_doc) {
+ // for stock pattern, report its root pattern ID
+ return std::make_pair(sel->id, stock_doc);
+ }
+ else {
+ // for current document, if selection hasn't changed return linked pattern ID
+ // so that we can modify its properties (transform, offset, gap)
+ if (sel->id == _current_pattern.id) {
+ return std::make_pair(_current_pattern.link_id, nullptr);
+ }
+ // different pattern from current document selected; use its root pattern
+ // as a starting point; link pattern will be injected by adjust_pattern()
+ return std::make_pair(sel->id, nullptr);
+ }
+ }
+ else {
+ // if nothing is selected, pick first stock pattern, so we have something to assign
+ // to selected object(s); without it, pattern editing will not be activated
+ if (auto first = _stock_pattern_store.store.get_store()->get_item(0)) {
+ return std::make_pair(first->id, first->collection);
+ }
+
+ // no stock patterns available
+ return std::make_pair("", nullptr);
+ }
+}
+
+std::optional<unsigned int> PatternEditor::get_selected_color() {
+ auto pat = get_active();
+ if (pat.first && pat.first->color.has_value()) {
+ return _color_picker->get_current_color();
+ }
+ return std::optional<unsigned int>(); // color not supported
+}
+
+Geom::Point PatternEditor::get_selected_offset() {
+ return Geom::Point(_offset_x.get_value(), _offset_y.get_value());
+}
+
+Geom::Affine PatternEditor::get_selected_transform() {
+ Geom::Affine matrix;
+
+ matrix *= Geom::Scale(_scale_x.get_value(), _scale_y.get_value());
+ matrix *= Geom::Rotate(_angle_btn.get_value() / 180.0 * M_PI);
+ auto pat = get_active();
+ if (pat.first) {
+ //TODO: this is imperfect; calculate better offset, if possible
+ // this translation is kept so there's no sudden jump when editing pattern attributes
+ matrix.setTranslation(pat.first->transform.translation());
+ }
+ return matrix;
+}
+
+bool PatternEditor::is_selected_scale_uniform() {
+ return _scale_linked;
+}
+
+Geom::Scale PatternEditor::get_selected_gap() {
+ auto vx = _gap_x_slider.get_value();
+ auto gap_x = slider_to_gap(vx, _gap_x_slider.get_adjustment()->get_upper());
+
+ auto vy = _gap_y_slider.get_value();
+ auto gap_y = slider_to_gap(vy, _gap_y_slider.get_adjustment()->get_upper());
+
+ return Geom::Scale(gap_x, gap_y);
+}
+
+Glib::ustring PatternEditor::get_label() {
+ return _name_box.get_text();
+}
+
+SPPattern* get_pattern(const PatternItem& item, SPDocument* document) {
+ auto doc = item.collection ? item.collection : document;
+ if (!doc) return nullptr;
+
+ return cast<SPPattern>(doc->getObjectById(item.id));
+}
+
+void regenerate_tile_images(PatternManager& manager, PatternStore& pat_store, int tile_size, double device_scale, SPDocument* current) {
+ auto& patterns = pat_store.store.get_items();
+ for (auto& item : patterns) {
+ if (auto pattern = get_pattern(*item.get(), current)) {
+ item->pix = manager.get_image(pattern, tile_size, tile_size, device_scale);
+ }
+ }
+ pat_store.store.refresh();
+}
+
+void PatternEditor::update_pattern_tiles() {
+ const double device_scale = get_scale_factor();
+ regenerate_tile_images(_manager, _doc_pattern_store, _tile_size, device_scale, _current_document);
+ regenerate_tile_images(_manager, _stock_pattern_store, _tile_size, device_scale, nullptr);
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
diff --git a/src/ui/widget/pattern-editor.h b/src/ui/widget/pattern-editor.h
new file mode 100644
index 0000000..9c52e74
--- /dev/null
+++ b/src/ui/widget/pattern-editor.h
@@ -0,0 +1,134 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_PATTERN_EDITOR_H
+#define SEEN_PATTERN_EDITOR_H
+
+#include <unordered_map>
+#include <vector>
+#include <gtkmm/box.h>
+#include <gtkmm/combobox.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/flowbox.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/button.h>
+#include <gtkmm/togglebutton.h>
+#include <gtkmm/scale.h>
+#include <gtkmm/searchentry.h>
+#include <gtkmm/spinbutton.h>
+#include <gtkmm/image.h>
+#include <gtkmm/label.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/paned.h>
+#include <gtkmm/builder.h>
+#include <optional>
+#include <2geom/transforms.h>
+#include "color.h"
+#include "object/sp-pattern.h"
+#include "pattern-manager.h"
+#include "spin-scale.h"
+#include "ui/operation-blocker.h"
+#include "ui/widget/color-picker.h"
+#include "ui/widget/pattern-store.h"
+
+class SPDocument;
+class ColorPicker;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class PatternEditor : public Gtk::Box {
+public:
+ PatternEditor(const char* prefs, PatternManager& manager);
+ ~PatternEditor() noexcept override;
+
+ // pass current document to extract patterns
+ void set_document(SPDocument* document);
+ // set selected pattern
+ void set_selected(SPPattern* pattern);
+ // selected pattern ID if any plus stock pattern collection document (or null)
+ std::pair<std::string, SPDocument*> get_selected();
+ // and its color
+ std::optional<unsigned int> get_selected_color();
+ // return combined scale and rotation
+ Geom::Affine get_selected_transform();
+ // return pattern offset
+ Geom::Point get_selected_offset();
+ // is scale uniform?
+ bool is_selected_scale_uniform();
+ // return gap size for pattern tiles
+ Geom::Scale get_selected_gap();
+ // get pattern label
+ Glib::ustring get_label();
+
+private:
+ sigc::signal<void> _signal_changed;
+ sigc::signal<void, unsigned int> _signal_color_changed;
+ sigc::signal<void> _signal_edit;
+
+public:
+ decltype(_signal_changed) signal_changed() const { return _signal_changed; }
+ decltype(_signal_color_changed) signal_color_changed() const { return _signal_color_changed; }
+ decltype(_signal_edit) signal_edit() const { return _signal_edit; }
+
+private:
+ void bind_store(Gtk::FlowBox& list, PatternStore& store);
+ void update_store(const std::vector<Glib::RefPtr<PatternItem>>& list, Gtk::FlowBox& gallery, PatternStore& store);
+ Glib::RefPtr<PatternItem> get_active(Gtk::FlowBox& gallery, PatternStore& pat);
+ std::pair<Glib::RefPtr<PatternItem>, SPDocument*> get_active();
+ void set_active(Gtk::FlowBox& gallery, PatternStore& pat, Glib::RefPtr<PatternItem> item);
+ void update_widgets_from_pattern(Glib::RefPtr<PatternItem>& pattern);
+ void update_scale_link();
+ void update_ui(Glib::RefPtr<PatternItem> pattern);
+ std::vector<Glib::RefPtr<PatternItem>> update_doc_pattern_list(SPDocument* document);
+ void set_stock_patterns(const std::vector<SPPattern*>& patterns);
+ void select_pattern_set(int index);
+ void apply_filter(bool stock);
+ void update_pattern_tiles();
+
+ Glib::RefPtr<Gtk::Builder> _builder;
+ Gtk::Paned& _paned;
+ Gtk::Box& _main_grid;
+ Gtk::Grid& _input_grid;
+ Gtk::SpinButton& _offset_x;
+ Gtk::SpinButton& _offset_y;
+ Gtk::SpinButton& _scale_x;
+ Gtk::SpinButton& _scale_y;
+ Gtk::SpinButton& _angle_btn;
+ Gtk::Scale& _orient_slider;
+ Gtk::Scale& _gap_x_slider;
+ Gtk::Scale& _gap_y_slider;
+ Gtk::Button& _edit_btn;
+ Gtk::Label& _color_label;
+ Gtk::Button& _color_btn;
+ Gtk::Button& _link_scale;
+ Gtk::Image& _preview_img;
+ Gtk::Viewport& _preview;
+ Gtk::FlowBox& _doc_gallery;
+ Gtk::FlowBox& _stock_gallery;
+ Gtk::Entry& _name_box;
+ Gtk::ComboBoxText& _combo_set;
+ Gtk::SearchEntry& _search_box;
+ Gtk::Scale& _tile_slider;
+ Gtk::CheckButton& _show_names;
+ Glib::RefPtr<Gtk::TreeModel> _categories;
+ bool _scale_linked = true;
+ Glib::ustring _prefs;
+ PatternStore _doc_pattern_store;
+ PatternStore _stock_pattern_store;
+ std::unique_ptr<ColorPicker> _color_picker;
+ OperationBlocker _update;
+ std::unordered_map<std::string, Glib::RefPtr<PatternItem>> _cached_items; // cached current document patterns
+ Inkscape::PatternManager& _manager;
+ Glib::ustring _filter_text;
+ int _tile_size = 0;
+ SPDocument* _current_document = nullptr;
+ // pattern being currently edited: id for a root pattern, and link id of a pattern with href set
+ struct { Glib::ustring id; Glib::ustring link_id; } _current_pattern;
+};
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif
diff --git a/src/ui/widget/pattern-store.h b/src/ui/widget/pattern-store.h
new file mode 100644
index 0000000..9c183d1
--- /dev/null
+++ b/src/ui/widget/pattern-store.h
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifndef INKSCAPE_UI_WIDGET_PATTERN_STORE_H
+#define INKSCAPE_UI_WIDGET_PATTERN_STORE_H
+/*
+ * Copyright (C) 2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <optional>
+#include <2geom/transforms.h>
+#include <giomm/liststore.h>
+#include <gtkmm/widget.h>
+#include "color.h"
+#include "ui/filtered-store.h"
+
+class SPDocument;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+// pattern parameters
+struct PatternItem : Glib::Object {
+ Cairo::RefPtr<Cairo::Surface> pix;
+ std::string id;
+ std::string label;
+ bool stock = false;
+ bool uniform_scale = false;
+ Geom::Affine transform;
+ Geom::Point offset;
+ std::optional<SPColor> color;
+ Geom::Scale gap;
+ SPDocument* collection = nullptr;
+
+ bool operator == (const PatternItem& item) const {
+ // compare all attributes apart from pixmap preview
+ return
+ id == item.id &&
+ label == item.label &&
+ stock == item.stock &&
+ uniform_scale == item.uniform_scale &&
+ transform == item.transform &&
+ offset == item.offset &&
+ color == item.color &&
+ gap == item.gap &&
+ collection == item.collection;
+ }
+};
+
+struct PatternStore {
+ Inkscape::FilteredStore<PatternItem> store;
+ std::map<Gtk::Widget*, Glib::RefPtr<PatternItem>> widgets_to_pattern;
+};
+
+}
+}
+}
+
+#endif
diff --git a/src/ui/widget/point.cpp b/src/ui/widget/point.cpp
new file mode 100644
index 0000000..9099988
--- /dev/null
+++ b/src/ui/widget/point.cpp
@@ -0,0 +1,186 @@
+// 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::Box(Gtk::ORIENTATION_VERTICAL), suffix, icon, mnemonic),
+ xwidget("X:",""),
+ ywidget("Y:","")
+{
+ xwidget.drag_dest_unset();
+ ywidget.drag_dest_unset();
+ static_cast<Gtk::Box*>(_widget)->pack_start(xwidget, true, true);
+ static_cast<Gtk::Box*>(_widget)->pack_start(ywidget, true, true);
+ static_cast<Gtk::Box*>(_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::Box(Gtk::ORIENTATION_VERTICAL), suffix, icon, mnemonic),
+ xwidget("X:","", digits),
+ ywidget("Y:","", digits)
+{
+ xwidget.drag_dest_unset();
+ ywidget.drag_dest_unset();
+ static_cast<Gtk::Box*>(_widget)->pack_start(xwidget, true, true);
+ static_cast<Gtk::Box*>(_widget)->pack_start(ywidget, true, true);
+ static_cast<Gtk::Box*>(_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::Box(Gtk::ORIENTATION_VERTICAL), suffix, icon, mnemonic),
+ xwidget("X:","", adjust, digits),
+ ywidget("Y:","", adjust, digits)
+{
+ xwidget.drag_dest_unset();
+ ywidget.drag_dest_unset();
+ static_cast<Gtk::Box*>(_widget)->pack_start(xwidget, true, true);
+ static_cast<Gtk::Box*>(_widget)->pack_start(ywidget, true, true);
+ static_cast<Gtk::Box*>(_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..b76f077
--- /dev/null
+++ b/src/ui/widget/preferences-widget.cpp
@@ -0,0 +1,1117 @@
+// 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 "include/gtkmm_version.h"
+
+#include "io/sys.h"
+
+#include "ui/dialog/filedialog.h"
+#include "ui/icon-loader.h"
+#include "ui/util.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);
+ 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);
+ }
+
+ // Pack an additional widget into a box with the widget if desired
+ if (other_widget)
+ hb->pack_start(*other_widget, expand_widget, expand_widget);
+}
+
+void DialogPage::add_group_header(Glib::ustring name, int columns)
+{
+ 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);
+ if (columns > 1) {
+ GValue width = G_VALUE_INIT;
+ g_value_init(&width, G_TYPE_INT);
+ g_value_set_int(&width, columns);
+ gtk_container_child_set_property(GTK_CONTAINER(gobj()), GTK_WIDGET(label_widget->gobj()), "width", &width);
+ }
+ }
+}
+
+void DialogPage::add_group_note(Glib::ustring name)
+{
+ if (name != "")
+ {
+ Gtk::Label* label_widget = Gtk::manage(new Gtk::Label(Glib::ustring("<i>") + name +
+ Glib::ustring("</i>") , Gtk::ALIGN_START , Gtk::ALIGN_CENTER, true));
+ label_widget->set_use_markup(true);
+ label_widget->set_valign(Gtk::ALIGN_CENTER);
+ label_widget->set_line_wrap(true);
+ label_widget->set_line_wrap_mode(Pango::WRAP_WORD);
+
+ add(*label_widget);
+ 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(label_widget->gobj()), "width", &width);
+ }
+}
+
+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();
+ if (!label.empty())
+ this->set_label(label);
+ this->set_active( prefs->getBool(_prefs_path, default_value) );
+}
+
+void PrefCheckButton::on_toggled()
+{
+ 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());
+ }
+ this->changed_signal.emit(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()
+{
+ 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);
+ }
+ this->changed_signal.emit(this->get_active());
+}
+
+
+PrefRadioButtons::PrefRadioButtons(const std::vector<PrefItem>& buttons, const Glib::ustring& prefs_path) {
+ set_spacing(2);
+
+ PrefRadioButton* group = nullptr;
+ for (auto&& item : buttons) {
+ auto* btn = Gtk::make_managed<PrefRadioButton>();
+ btn->init(item.label, prefs_path, item.int_value, item.is_default, group);
+ btn->set_tooltip_text(item.tooltip);
+ add(*btn);
+ if (!group) group = btn;
+ }
+}
+
+
+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());
+ }
+ }
+ this->changed_signal.emit(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;
+ double step = dist * zoomcorr / _unitconv;
+ bool draw_minor = true;
+ if (step <= 0) {
+ return;
+ }
+ else if (step < 2) {
+ // marks too dense
+ draw_minor = false;
+ }
+ int last_pos = -1;
+ while (mark <= _drawing_width) {
+ cr->move_to(mark, _height);
+ if ((i % major_interval) == 0) {
+ // don't overcrowd the marks
+ if (static_cast<int>(mark) > last_pos) {
+ // major mark
+ cr->line_to(mark, 0);
+ Geom::Point textpos(mark + 3, ZoomCorrRuler::textsize + ZoomCorrRuler::textpadding);
+ draw_number(cr->cobj(), textpos, dist * i);
+
+ last_pos = static_cast<int>(mark) + 1;
+ }
+ } else if (draw_minor) {
+ // minor mark
+ cr->line_to(mark, ZoomCorrRuler::textsize + 2 * ZoomCorrRuler::textpadding);
+ }
+ mark += step;
+ ++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;
+
+ auto context = get_style_context();
+ Gdk::RGBA fg = context->get_color(get_state_flags());
+
+ context->render_background(cr, 0, 0, w, _height + _border * 2);
+
+ cr->set_line_width(1);
+ cr->set_source_rgb(fg.get_red(), fg.get_green(), fg.get_blue());
+
+ 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 (!_unit.get_sensitive()) {
+ // 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 = Gtk::manage(new Inkscape::UI::Widget::SpinButton());
+ _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_sensitive(false);
+ _unit.setUnitType(UNIT_TYPE_LINEAR);
+ _unit.set_sensitive(true);
+ _unit.setUnit(prefs->getString("/options/zoomcorrection/unit"));
+ _unit.set_halign(Gtk::ALIGN_CENTER);
+ _unit.set_valign(Gtk::ALIGN_END);
+
+ _slider->set_hexpand(true);
+ _ruler.set_hexpand(true);
+ 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());
+ if (_sb) _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();
+ if (_sb) {
+ 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 ? _sb->mnemonic_activate ( group_cycling ) : false;
+}
+
+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));
+ if (_spin) {
+ _sb = Gtk::manage(new Inkscape::UI::Widget::SpinButton());
+ _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);
+ if (_sb) 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 const labels[], int const 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::cerr << "PrefCombo::"
+ << "Different number of values/labels in " << prefs_path.raw() << 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::cerr << "PrefCombo::"
+ << "Different number of values/labels in " << prefs_path.raw() << 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::init(Glib::ustring const &prefs_path,
+ std::vector<std::pair<Glib::ustring, Glib::ustring>> labels_and_values,
+ Glib::ustring default_value)
+{
+ _prefs_path = prefs_path;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring value = prefs->getString(_prefs_path);
+ if (value.empty()) {
+ value = default_value;
+ }
+
+ int row = 0;
+ int i = 0;
+ for (auto entry : labels_and_values) {
+ this->append(entry.first);
+ _ustr_values.push_back(entry.second);
+ if (value == entry.second) {
+ row = i;
+ }
+ ++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::Box* pixlabel = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 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::Box *pixlabel = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 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);
+ std::vector<std::string> argv = { "xdg-open", path };
+ Glib::spawn_async("", argv, Glib::SpawnFlags::SPAWN_SEARCH_PATH);
+ 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 PrefEntryFile::on_changed()
+{
+ if (this->get_visible()) //only take action if user changed value
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setString(_prefs_path, Glib::filename_to_utf8(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..f4a990f
--- /dev/null
+++ b/src/ui/widget/preferences-widget.h
@@ -0,0 +1,357 @@
+// 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);
+ // Allow use with the GtkBuilder get_derived_widget
+ PrefCheckButton(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade, Glib::ustring pref, bool def)
+ : Gtk::CheckButton(cobject)
+ {
+ init("", pref, def);
+ }
+ PrefCheckButton() : Gtk::CheckButton() {};
+ 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;
+};
+
+struct PrefItem { Glib::ustring label; int int_value; Glib::ustring tooltip; bool is_default = false; };
+
+class PrefRadioButtons : public Gtk::Box {
+public:
+ PrefRadioButtons(const std::vector<PrefItem>& buttons, const Glib::ustring& prefs_path);
+
+private:
+};
+
+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);
+ sigc::signal<void (double)> changed_signal;
+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::Box
+{
+public:
+ ZoomCorrRulerSlider() : Gtk::Box(Gtk::ORIENTATION_VERTICAL) {}
+
+ 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::Box
+{
+public:
+ PrefSlider(bool spin = true) : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) { _spin = spin; }
+
+ void init(Glib::ustring const &prefs_path,
+ double lower, double upper, double step_increment, double page_increment, double default_value, int digits);
+
+ Gtk::Scale* getSlider() {return _slider;};
+ Inkscape::UI::Widget::SpinButton * getSpinButton() {return _sb;};
+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 = nullptr;
+ bool _spin;
+ Gtk::Scale* _slider = nullptr;
+
+ 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 const labels[], int const 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);
+
+ /**
+ * Initialize a combo box with a vector of Glib::ustring pairs.
+ */
+ void init(Glib::ustring const &prefs_path,
+ std::vector<std::pair<Glib::ustring, Glib::ustring>> labels_and_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 PrefEntryFile : public PrefEntry
+{
+ 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::Box
+{
+public:
+ PrefEntryButtonHBox() : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) {}
+
+ 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::Box
+{
+public:
+ PrefEntryFileButtonHBox() : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) {}
+
+ 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::Box {
+ public:
+ PrefOpenFolder() : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) {}
+
+ 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, int columns = 1);
+ void add_group_note(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/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..5464227
--- /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..6df1981
--- /dev/null
+++ b/src/ui/widget/registered-widget.cpp
@@ -0,0 +1,830 @@
+// 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 "object/sp-root.h"
+
+#include "svg/svg-color.h"
+#include "svg/stringstream.h"
+
+#include <glibmm/i18n.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/*#########################################
+ * Registered CHECKBUTTON
+ */
+
+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);
+}
+
+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 (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);
+}
+
+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) {
+ SPDesktop *dt = _wr->desktop();
+ if (!dt) {
+ _wr->setUpdating(false);
+ 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);
+ }
+ {
+ DocumentUndo::ScopedInsensitive _no_undo(local_doc);
+ local_repr->setAttribute(_ckey, c);
+ local_repr->setAttributeCssDouble(_akey.c_str(), (rgba & 0xff) / 255.0);
+ }
+ local_doc->setModifiedSinceSave();
+ DocumentUndo::done(local_doc, "registered-widget.cpp: RegisteredColorPicker::on_changed", ""); // TODO Fix description.
+
+ _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::Box>(),
+ _rb1(nullptr),
+ _rb2(nullptr)
+{
+ init_parent(key, wr, repr_in, doc_in);
+
+ setProgrammatically = false;
+
+ set_orientation(Gtk::ORIENTATION_HORIZONTAL);
+ 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..9e3d815
--- /dev/null
+++ b/src/ui/widget/registered-widget.h
@@ -0,0 +1,452 @@
+// 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.
+ *
+ *
+ * Used by Live Path Effects (see src/live_effects/parameter/) and Document Properties dialog.
+ *
+ */
+
+#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(Glib::ustring _event_description, Glib::ustring _icon_name)
+ {
+ icon_name = _icon_name;
+ 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() {}
+ template< typename A >
+ explicit RegisteredWidget( A& a ): W( a ) {}
+ template< typename A, typename B >
+ RegisteredWidget( A& a, B& b ): W( a, b ) {}
+ template< typename A, typename B, typename C >
+ RegisteredWidget( A& a, B& b, C* c ): W( a, b, c ) {}
+ template< typename A, typename B, typename C >
+ RegisteredWidget( A& a, B& b, C& c ): W( a, b, c ) {}
+ template< typename A, typename B, typename C, typename D >
+ RegisteredWidget( A& a, B& b, C c, D d ): W( a, b, c, d ) {}
+ 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 ) {}
+ 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) {}
+ 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) {}
+
+ ~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) {
+ SPDesktop* dt = _wr->desktop();
+ if (!dt) {
+ return;
+ }
+ local_repr = reinterpret_cast<SPObject *>(dt->getNamedView())->getRepr();
+ local_doc = dt->getDocument();
+ }
+ const char * svgstr_old = local_repr->attribute(_key.c_str());
+ {
+ DocumentUndo::ScopedInsensitive _no_undo(local_doc);
+ if (!write_undo) {
+ local_repr->setAttribute(_key, svgstr);
+ }
+ }
+ if (svgstr_old && svgstr && strcmp(svgstr_old,svgstr)) {
+ local_doc->setModifiedSinceSave();
+ }
+
+ if (write_undo) {
+ local_repr->setAttribute(_key, svgstr);
+ DocumentUndo::done(local_doc, event_description, icon_name);
+ }
+ }
+
+ Registry * _wr = nullptr;
+ Glib::ustring _key;
+ Inkscape::XML::Node * repr = nullptr;
+ SPDocument * doc = nullptr;
+ Glib::ustring event_description;
+ Glib::ustring icon_name; // Used by History dialog.
+ bool write_undo = false;
+};
+
+//#######################################################
+
+class RegisteredCheckButton : public RegisteredWidget<Gtk::CheckButton> {
+public:
+ ~RegisteredCheckButton() override = default;
+ 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*> const &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;
+ void on_toggled() override;
+};
+
+class RegisteredToggleButton : public RegisteredWidget<Gtk::ToggleButton> {
+public:
+ ~RegisteredToggleButton() override = default;
+ 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*> const &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:
+ 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::Box> {
+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 cartesian 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..4bc8e00
--- /dev/null
+++ b/src/ui/widget/registry.cpp
@@ -0,0 +1,58 @@
+// 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;
+}
+
+void Registry::setDesktop(SPDesktop *desktop)
+{ //
+ _desktop = 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/widget/registry.h b/src/ui/widget/registry.h
new file mode 100644
index 0000000..e6a190d
--- /dev/null
+++ b/src/ui/widget/registry.h
@@ -0,0 +1,51 @@
+// 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
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class Registry {
+public:
+ Registry();
+ ~Registry();
+
+ bool isUpdating();
+ void setUpdating (bool);
+
+ SPDesktop *desktop() const { return _desktop; }
+ void setDesktop(SPDesktop *desktop);
+
+protected:
+ bool _updating;
+
+ SPDesktop *_desktop = nullptr;
+};
+
+} // 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..4640b1e
--- /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::Box (Gtk::ORIENTATION_VERTICAL),
+ _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::Box *box_vector = Gtk::manage( new Gtk::Box (Gtk::ORIENTATION_VERTICAL) );
+ box_vector->set_border_width (2);
+ box_vector->add (_radio_vector);
+ box_vector->add (_radio_bitmap);
+ _frame_backends.add (*box_vector);
+
+ Gtk::Box *box_bitmap = Gtk::manage( new Gtk::Box (Gtk::ORIENTATION_HORIZONTAL) );
+ 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..65d96f4
--- /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::Box
+{
+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..9db6b79
--- /dev/null
+++ b/src/ui/widget/scalar-unit.cpp
@@ -0,0 +1,271 @@
+// 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::setAlignment(double xalign)
+{
+ xalign = std::clamp(xalign,0.0,1.0);
+ static_cast<Gtk::Entry*>(_widget)->set_alignment(xalign);
+}
+
+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();
+
+ if (_suffix) {
+ _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..3d7b77d
--- /dev/null
+++ b/src/ui/widget/scalar-unit.h
@@ -0,0 +1,196 @@
+// 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);
+
+ /**
+ * allow align text in entry.
+ */
+ void setAlignment(double xalign);
+
+ /**
+ * 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..a7a2bf6
--- /dev/null
+++ b/src/ui/widget/scalar.cpp
@@ -0,0 +1,212 @@
+// 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::setNoLeadingZeros()
+{
+ g_assert(_widget != nullptr);
+ if (getDigits()) {
+ static_cast<SpinButton*>(_widget)->set_numeric(false);
+ static_cast<SpinButton*>(_widget)->set_update_policy(Gtk::UPDATE_ALWAYS);
+ static_cast<SpinButton*>(_widget)->signal_output().connect(sigc::mem_fun(*this, &Scalar::setNoLeadingZerosOutput));
+ }
+}
+
+bool
+Scalar::setNoLeadingZerosOutput()
+{
+ g_assert(_widget != nullptr);
+ double digits = (double)pow(10.0,static_cast<SpinButton*>(_widget)->get_digits());
+ double val = std::round(static_cast<SpinButton*>(_widget)->get_value() * digits) / digits;
+ static_cast<SpinButton*>(_widget)->set_text(Glib::ustring::format(val).c_str());
+ return true;
+}
+
+void
+Scalar::setWidthChars(gint width_chars) {
+ g_assert(_widget != nullptr);
+ static_cast<SpinButton*>(_widget)->property_width_chars() = width_chars;
+}
+
+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);
+ pack_start(*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();
+}
+
+void Scalar::hide_label() {
+ if (auto label = const_cast<Gtk::Label*>(getLabel())) {
+ label->hide();
+ label->set_no_show_all();
+ label->set_hexpand(true);
+ }
+ if (_widget) {
+ remove(*_widget);
+ _widget->set_hexpand();
+ this->pack_end(*_widget);
+ }
+}
+
+
+} // 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..85ed72e
--- /dev/null
+++ b/src/ui/widget/scalar.h
@@ -0,0 +1,202 @@
+// 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();
+
+ /**
+ * remove leading zeros fron widget.
+ */
+ void setNoLeadingZeros();
+ bool setNoLeadingZerosOutput();
+
+ /**
+ * Set the number of set width chars of entry.
+ */
+ void setWidthChars(gint width_chars);
+
+ /**
+ * 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;
+
+ // permanently hide label part
+ void hide_label();
+};
+
+} // 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/scroll-utils.cpp b/src/ui/widget/scroll-utils.cpp
new file mode 100644
index 0000000..5822a15
--- /dev/null
+++ b/src/ui/widget/scroll-utils.cpp
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Thomas Holder
+ *
+ * Copyright (C) 2020 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "scroll-utils.h"
+
+#include <gtkmm/scrolledwindow.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Get the first ancestor which is scrollable.
+ */
+Gtk::Widget *get_scrollable_ancestor(Gtk::Widget *widget)
+{
+ auto parent = widget->get_parent();
+ if (!parent) {
+ return nullptr;
+ }
+ if (auto scrollable = dynamic_cast<Gtk::ScrolledWindow *>(parent)) {
+ return scrollable;
+ }
+ return get_scrollable_ancestor(parent);
+}
+
+/**
+ * Return true if scrolling is allowed.
+ *
+ * Scrolling is allowed for any of:
+ * - Shift modifier is pressed
+ * - Widget has focus
+ * - Widget has no scrollable ancestor
+ */
+bool scrolling_allowed(Gtk::Widget *widget, GdkEventScroll *event)
+{
+ bool const shift = event && (event->state & GDK_SHIFT_MASK);
+ return shift || //
+ widget->has_focus() || //
+ get_scrollable_ancestor(widget) == nullptr;
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+// vim: filetype=cpp:expandtab:shiftwidth=4:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/widget/scroll-utils.h b/src/ui/widget/scroll-utils.h
new file mode 100644
index 0000000..14b45de
--- /dev/null
+++ b/src/ui/widget/scroll-utils.h
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_INKSCAPE_UI_WIDGET_SCROLL_UTILS_H
+#define SEEN_INKSCAPE_UI_WIDGET_SCROLL_UTILS_H
+
+/* Authors:
+ * Thomas Holder
+ *
+ * Copyright (C) 2020 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gdk/gdk.h>
+
+namespace Gtk {
+class Widget;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Gtk::Widget *get_scrollable_ancestor(Gtk::Widget *widget);
+
+bool scrolling_allowed(Gtk::Widget *widget, GdkEventScroll *event = nullptr);
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif
+// vim: filetype=cpp:expandtab:shiftwidth=4:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/widget/scrollprotected.h b/src/ui/widget/scrollprotected.h
new file mode 100644
index 0000000..c060398
--- /dev/null
+++ b/src/ui/widget/scrollprotected.h
@@ -0,0 +1,117 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_INKSCAPE_UI_WIDGET_SCROLLPROTECTED_H
+#define SEEN_INKSCAPE_UI_WIDGET_SCROLLPROTECTED_H
+
+/* Authors:
+ * Thomas Holder
+ * Anshudhar Kumar Singh <anshudhar2001@gmail.com>
+ *
+ * Copyright (C) 2020-2021 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm.h>
+
+#include "scroll-utils.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A class decorator which blocks the scroll event if the widget does not have
+ * focus and any ancestor is a scrollable window, and SHIFT is not pressed.
+ *
+ * For custom scroll event handlers, derived classes must implement
+ * on_safe_scroll_event instead of on_scroll_event. Directly connecting to
+ * signal_scroll_event() will bypass the scroll protection.
+ *
+ * @tparam Base A subclass of Gtk::Widget
+ */
+template <typename Base>
+class ScrollProtected : public Base
+{
+public:
+ using Base::Base;
+ using typename Base::BaseObjectType;
+ ScrollProtected()
+ : Base()
+ {}
+ ScrollProtected(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade)
+ : Base(cobject){};
+ ~ScrollProtected() override{};
+
+protected:
+ /**
+ * Event handler for "safe" scroll events which are only triggered if:
+ * - the widget has focus
+ * - or the widget has no scrolled window ancestor
+ * - or the Shift key is pressed
+ */
+ virtual bool on_safe_scroll_event(GdkEventScroll *event)
+ { //
+ return Base::on_scroll_event(event);
+ }
+
+ bool on_scroll_event(GdkEventScroll *event) final
+ {
+ if (!scrolling_allowed(this, event)) {
+ return false;
+ }
+ return on_safe_scroll_event(event);
+ }
+};
+
+/**
+ * A class decorator for scroll widgets like scrolled window to transfer scroll to
+ * any ancestor which is is a scrollable window when scroll reached end.
+ *
+ * For custom scroll event handlers, derived classes must implement
+ * on_safe_scroll_event instead of on_scroll_event. Directly connecting to
+ * signal_scroll_event() will bypass the scroll protection.
+ *
+ * @tparam Base A subclass of Gtk::Widget
+ */
+template <typename Base>
+class ScrollTransfer : public Base
+{
+public:
+ using Base::Base;
+ using typename Base::BaseObjectType;
+ ScrollTransfer()
+ : Base()
+ {}
+ ScrollTransfer(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade)
+ : Base(cobject){};
+ ~ScrollTransfer() override{};
+protected:
+ /**
+ * Event handler for "safe" scroll events
+ */
+ virtual bool on_safe_scroll_event(GdkEventScroll *event)
+ { //
+ return Base::on_scroll_event(event);
+ }
+
+ bool on_scroll_event(GdkEventScroll *event) final
+ {
+ auto scrollable = dynamic_cast<Gtk::Widget *>(Inkscape::UI::Widget::get_scrollable_ancestor(this));
+ auto adj = this->get_vadjustment();
+ auto before = adj->get_value();
+ bool result = on_safe_scroll_event(event);
+ auto after = adj->get_value();
+ if (scrollable && before == after) {
+ return false;
+ }
+
+ return result;
+ }
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif
+// vim: filetype=cpp:expandtab:shiftwidth=4: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..43a9039
--- /dev/null
+++ b/src/ui/widget/selected-style.cpp
@@ -0,0 +1,1416 @@
+// 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 <vector>
+
+#include <gtkmm/separatormenuitem.h>
+
+
+#include "desktop-style.h"
+#include "document-undo.h"
+#include "gradient-chemistry.h"
+#include "message-context.h"
+#include "selection.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 "svg/css-ostringstream.h"
+#include "svg/svg-color.h"
+
+#include "ui/cursor-utils.h"
+#include "ui/dialog/dialog-container.h"
+#include "ui/dialog/dialog-base.h"
+#include "ui/dialog/fill-and-stroke.h"
+#include "ui/icon-names.h"
+#include "ui/tools/tool-base.h"
+#include "ui/widget/color-preview.h"
+#include "ui/widget/gradient-image.h"
+
+#include "widgets/paintdef.h"
+#include "widgets/spw-utilities.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
+};
+
+static const std::vector<Gtk::TargetEntry> ui_drop_target_entries = {
+ Gtk::TargetEntry("application/x-oswb-color", Gtk::TargetFlags(0), APP_OSWB_COLOR)
+};
+
+/* convenience function */
+static Dialog::FillAndStroke *get_fill_and_stroke_panel(SPDesktop *desktop);
+
+SelectedStyle::SelectedStyle(bool /*layout*/)
+ : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)
+ , 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(Gtk::ORIENTATION_HORIZONTAL, 1)
+ , _stroke(Gtk::ORIENTATION_HORIZONTAL)
+ , _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_END);
+ _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_END);
+ _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::manage(new GradientImage(nullptr));
+ _gradient_box_l[i].set_orientation(Gtk::ORIENTATION_HORIZONTAL);
+ _gradient_box_l[i].pack_start(_lgradient[i]);
+ _gradient_box_l[i].pack_start(*_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::manage(new GradientImage(nullptr));
+ _gradient_box_r[i].set_orientation(Gtk::ORIENTATION_HORIZONTAL);
+ _gradient_box_r[i].pack_start(_rgradient[i]);
+ _gradient_box_r[i].pack_start(*_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::manage(new GradientImage(nullptr));
+ _gradient_box_m[i].set_orientation(Gtk::ORIENTATION_HORIZONTAL);
+ _gradient_box_m[i].pack_start(_mgradient[i]);
+ _gradient_box_m[i].pack_start(*_gradient_preview_m[i]);
+ _gradient_box_m[i].show_all();
+#endif
+
+ _many[i].set_markup (_("≠"));
+ _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 row
+ _fill_flag_place.set_size_request(SELECTED_STYLE_FLAG_WIDTH , -1);
+
+ _fill_place.add(_na[SS_FILL]);
+ _fill_place.set_tooltip_text(__na[SS_FILL]);
+ _fill.set_size_request(SELECTED_STYLE_PLACE_WIDTH, -1);
+ _fill.pack_start(_fill_place, Gtk::PACK_EXPAND_WIDGET);
+
+ _fill_empty_space.set_size_request(SELECTED_STYLE_STROKE_WIDTH);
+
+ // stroke row
+ _stroke_flag_place.set_size_request(SELECTED_STYLE_FLAG_WIDTH, -1);
+
+ _stroke_place.add(_na[SS_STROKE]);
+ _stroke_place.set_tooltip_text(__na[SS_STROKE]);
+ _stroke.set_size_request(SELECTED_STYLE_PLACE_WIDTH, -1);
+ _stroke.pack_start(_stroke_place, Gtk::PACK_EXPAND_WIDGET);
+
+ _stroke_width_place.add(_stroke_width);
+ _stroke_width_place.set_size_request(SELECTED_STYLE_STROKE_WIDTH);
+
+ // opacity selector
+ _opacity_place.add(_opacity_label);
+
+ _opacity_sb.set_adjustment(_opacity_adjustment);
+ _opacity_sb.set_size_request (SELECTED_STYLE_SB_WIDTH, -1);
+ _opacity_sb.set_sensitive (false);
+
+ // arrange in table
+ _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);
+
+ _table.attach(_fill_empty_space, 3, 0, 1, 1);
+ _table.attach(_stroke_width_place, 3, 1, 1, 1);
+
+ _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));
+}
+
+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();
+
+ _fill_place.remove();
+ _stroke_place.remove();
+
+ for (int i = SS_FILL; i <= SS_STROKE; i++) {
+ delete _color_preview[i];
+ }
+
+ delete (DropTracker*)_drop[SS_FILL];
+ delete (DropTracker*)_drop[SS_STROKE];
+}
+
+void
+SelectedStyle::setDesktop(SPDesktop *desktop)
+{
+ _desktop = desktop;
+
+ 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) {
+ 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));
+ if (worked) {
+ if (color.get_type() == PaintDef::NONE) {
+ colorspec = "none";
+ } else {
+ auto [r, g, b] = color.get_rgb();
+ 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(), _("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(), _("Remove fill"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+}
+
+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(), _("Remove stroke"), INKSCAPE_ICON("dialog-fill-and-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(), _("Unset fill"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+
+}
+
+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(), _("Unset stroke"), INKSCAPE_ICON("dialog-fill-and-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(), _("Make fill opaque"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+}
+
+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(), _("Make fill opaque"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+}
+
+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(), _("Apply last set color to fill"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+}
+
+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(), _("Apply last set color to stroke"), INKSCAPE_ICON("dialog-fill-and-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(), _("Apply last selected color to fill"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+}
+
+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(), _("Apply last selected color to stroke"), INKSCAPE_ICON("dialog-fill-and-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(), _("Invert fill"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+}
+
+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(), _("Invert stroke"), INKSCAPE_ICON("dialog-fill-and-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(), _("White fill"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+}
+
+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(), _("White stroke"), INKSCAPE_ICON("dialog-fill-and-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(), _("Black fill"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+}
+
+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(), _("Black stroke"), INKSCAPE_ICON("dialog-fill-and-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(), _("Paste fill"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ }
+}
+
+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(), _("Paste stroke"), INKSCAPE_ICON("dialog-fill-and-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(), _("Change opacity"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ 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(), _("Change stroke width"), INKSCAPE_ICON("swatches"));
+}
+
+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]) {
+ auto widget = i == SS_FILL ? &_fill_place : &_stroke_place;
+ widget->drag_dest_unset();
+ _dropEnabled[i] = false;
+ }
+ break;
+ case QUERY_STYLE_SINGLE:
+ case QUERY_STYLE_MULTIPLE_AVERAGED:
+ case QUERY_STYLE_MULTIPLE_SAME: {
+ if (!_dropEnabled[i]) {
+ auto widget = i == SS_FILL ? &_fill_place : &_stroke_place;
+ widget->drag_dest_set(ui_drop_target_entries,
+ Gtk::DestDefaults::DEST_DEFAULT_ALL,
+ Gdk::DragAction::ACTION_COPY | Gdk::DragAction::ACTION_MOVE);
+ _dropEnabled[i] = true;
+ }
+ auto paint = i == SS_FILL ? query.fill.upcast() : query.stroke.upcast();
+ 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 (is<SPLinearGradient>(server)) {
+ auto vector = cast<SPGradient>(server)->getVector();
+ _gradient_preview_l[i]->set_gradient(vector);
+ place->add(_gradient_box_l[i]);
+ place->set_tooltip_text(__lgradient[i]);
+ _mode[i] = SS_LGRADIENT;
+ } else if (is<SPRadialGradient>(server)) {
+ auto vector = cast<SPGradient>(server)->getVector();
+ _gradient_preview_r[i]->set_gradient(vector);
+ place->add(_gradient_box_r[i]);
+ place->set_tooltip_text(__rgradient[i]);
+ _mode[i] = SS_RGRADIENT;
+#ifdef WITH_MESH
+ } else if (is<SPMeshGradient>(server)) {
+ auto array = cast<SPGradient>(server)->getArray();
+ _gradient_preview_m[i]->set_gradient(array);
+ place->add(_gradient_box_m[i]);
+ place->set_tooltip_text(__mgradient[i]);
+ _mode[i] = SS_MGRADIENT;
+#endif
+ } else if (is<SPPattern>(server)) {
+ place->add(_pattern[i]);
+ place->set_tooltip_text(__pattern[i]);
+ _mode[i] = SS_PATTERN;
+ } else if (is<SPHatch>(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:
+ {
+ if (query.stroke_extensions.hairline) {
+ _stroke_width.set_markup(_("Hairline"));
+ auto str = Glib::ustring::compose(_("Stroke width: %1"), _("Hairline"));
+ _stroke_width_place.set_tooltip_text(str);
+ } else {
+ 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) {
+
+ std::vector<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());
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::maybeDone(_desktop->getDocument(), "fillstroke:opacity", _("Change opacity"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ _opacity_blocked = false;
+}
+
+/* ============================================= RotateableSwatch */
+
+RotateableSwatch::RotateableSwatch(SelectedStyle *parent, guint mode)
+ : fillstroke(mode)
+ , parent(parent)
+{
+}
+
+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) {
+
+ std::string cursor_filename = "adjust_hue.svg";
+ if (modifier == 2) {
+ cursor_filename = "adjust_saturation.svg";
+ } else if (modifier == 1) {
+ cursor_filename = "adjust_lightness.svg";
+ } else if (modifier == 3) {
+ cursor_filename = "adjust_alpha.svg";
+ }
+
+ auto window = get_window();
+ auto cursor = load_svg_cursor(get_display(), window, cursor_filename);
+ get_window()->set_cursor(cursor);
+ }
+
+ 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, (_("Adjust alpha")), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ 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, (_("Adjust saturation")), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ 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, (_("Adjust lightness")), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ 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, (_("Adjust hue")), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ 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) {
+ get_window()->set_cursor(); // Use parent window cursor.
+ cr_set = false;
+ }
+
+ if (modifier == 3) { // alpha
+ DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, ("Adjust alpha"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+
+ } else if (modifier == 2) { // saturation
+ DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, ("Adjust saturation"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+
+ } else if (modifier == 1) { // lightness
+ DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, ("Adjust lightness"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+
+ } else { // hue
+ DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, ("Adjust hue"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ }
+
+ 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, (_("Adjust stroke width")), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ 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, (_("Adjust stroke width")), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ }
+
+ 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)
+{
+ desktop->getContainer()->new_dialog("FillStroke");
+ return dynamic_cast<Dialog::FillAndStroke *>(desktop->getContainer()->get_dialog("FillStroke"));
+}
+
+} // 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..0ad002b
--- /dev/null
+++ b/src/ui/widget/selected-style.h
@@ -0,0 +1,302 @@
+// 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"
+
+constexpr int SELECTED_STYLE_SB_WIDTH = 48;
+constexpr int SELECTED_STYLE_PLACE_WIDTH = 50;
+constexpr int SELECTED_STYLE_STROKE_WIDTH = 40;
+constexpr int SELECTED_STYLE_FLAG_WIDTH = 12;
+constexpr int SELECTED_STYLE_WIDTH = 250;
+
+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 GradientImage;
+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 = 0;
+ bool startcolor_set = false;
+
+ gchar const *undokey = "ssrot1";
+
+ bool cr_set = false;
+};
+
+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::Box
+{
+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];
+
+ GradientImage *_gradient_preview_l[2];
+ Gtk::Box _gradient_box_l[2];
+
+ Gtk::Label _rgradient[2];
+ Glib::ustring __rgradient[2];
+
+ GradientImage *_gradient_preview_r[2];
+ Gtk::Box _gradient_box_r[2];
+
+#ifdef WITH_MESH
+ Gtk::Label _mgradient[2];
+ Glib::ustring __mgradient[2];
+
+ GradientImage *_gradient_preview_m[2];
+ Gtk::Box _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::Box _fill;
+ Gtk::Box _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/shapeicon.cpp b/src/ui/widget/shapeicon.cpp
new file mode 100644
index 0000000..7a451ab
--- /dev/null
+++ b/src/ui/widget/shapeicon.cpp
@@ -0,0 +1,148 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Martin Owens
+ *
+ * Copyright (C) 2020 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+
+#include <gtkmm/cellrenderer.h>
+#include <gtkmm/enums.h>
+#include "color.h"
+#include "ui/util.h"
+#include "ui/widget/shapeicon.h"
+#include "ui/icon-loader.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/*
+ * This is a type of CellRenderer which you might expect to inherit from the
+ * pixbuf CellRenderer, but we actually need to write a Cairo surface directly
+ * in order to maintain HiDPI sharpness in icons. Upstream Gtk have made it clear
+ * that CellRenderers are going away in Gtk4 so they aren't interested in fixing
+ * rendering problems like the one in CellRendererPixbuf.
+ *
+ * See: https://gitlab.gnome.org/GNOME/gtk/-/issues/613
+ */
+
+void CellRendererItemIcon::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_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE;
+
+ std::string shape_type = _property_shape_type.get_value();
+ if (shape_type == "-") return; // "-" is an explicit request not to draw any icon
+
+ std::string highlight;
+ auto color = _property_color.get_value();
+ if (color == 0) {
+ auto style_context = widget.get_style_context();
+ Gdk::RGBA fg = style_context->get_color(cell_flags_to_state_flags(flags));
+ highlight = fg.to_string();
+ }
+ else {
+ highlight = SPColor(color).toString();
+ }
+ std::string cache_id = shape_type + "-" + highlight;
+
+ // if the icon isn't cached, render it to a pixbuf
+ int scale = widget.get_scale_factor();
+ if ( !_icon_cache[cache_id] ) {
+ _icon_cache[cache_id] = sp_get_shape_icon(shape_type, Gdk::RGBA(highlight), _size, scale);
+ }
+ g_return_if_fail(_icon_cache[cache_id]);
+
+ // Center the icon in the cell area
+ int x = cell_area.get_x() + int((cell_area.get_width() - _size) * 0.5);
+ int y = cell_area.get_y() + int((cell_area.get_height() - _size) * 0.5);
+
+ // Paint the pixbuf to a cairo surface to get HiDPI support
+ paint_icon(cr, widget, _icon_cache[cache_id], x, y);
+
+ // Create an overlay icon
+ int clipmask = _property_clipmask.get_value();
+ if (clipmask > 0) {
+ if (!_clip_overlay) {
+ _clip_overlay = sp_get_icon_pixbuf("overlay-clip", Gtk::ICON_SIZE_MENU, scale);
+ }
+ if (!_mask_overlay) {
+ _mask_overlay = sp_get_icon_pixbuf("overlay-mask", Gtk::ICON_SIZE_MENU, scale);
+ }
+ if (!_both_overlay) {
+ _both_overlay = sp_get_icon_pixbuf("overlay-clipmask", Gtk::ICON_SIZE_MENU, scale);
+ }
+
+ if (clipmask == OVERLAY_CLIP && _clip_overlay) {
+ paint_icon(cr, widget, _clip_overlay, x, y);
+ }
+ if (clipmask == OVERLAY_MASK && _mask_overlay) {
+ paint_icon(cr, widget, _mask_overlay, x, y);
+ }
+ if (clipmask == OVERLAY_BOTH && _both_overlay) {
+ paint_icon(cr, widget, _both_overlay, x, y);
+ }
+ }
+
+}
+
+void CellRendererItemIcon::paint_icon(const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf,
+ int x, int y)
+{
+ cairo_surface_t *surface = gdk_cairo_surface_create_from_pixbuf(
+ pixbuf->gobj(), 0, widget.get_window()->gobj());
+ if (!surface) return;
+ cairo_set_source_surface(cr->cobj(), surface, x, y);
+ cr->set_operator(Cairo::OPERATOR_ATOP);
+ cr->rectangle(x, y, _size, _size);
+ cr->fill();
+ cairo_surface_destroy(surface); // free!
+}
+
+void CellRendererItemIcon::get_preferred_height_vfunc(Gtk::Widget& widget, int& min_h, int& nat_h) const
+{
+ min_h = _size;
+ nat_h = _size + 4;
+}
+
+void CellRendererItemIcon::get_preferred_width_vfunc(Gtk::Widget& widget, int& min_w, int& nat_w) const
+{
+ min_w = _size;
+ nat_w = _size + 4;
+}
+
+bool CellRendererItemIcon::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_activated.emit(path);
+ return true;
+}
+
+} // 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/shapeicon.h b/src/ui/widget/shapeicon.h
new file mode 100644
index 0000000..cc6daab
--- /dev/null
+++ b/src/ui/widget/shapeicon.h
@@ -0,0 +1,115 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __UI_DIALOG_SHAPEICON_H__
+#define __UI_DIALOG_SHAPEICON_H__
+/*
+ * Authors:
+ * Martin Owens
+ *
+ * Copyright (C) 2020 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/iconinfo.h>
+#include <gtkmm/cellrenderer.h>
+#include <gtkmm/widget.h>
+#include <glibmm/property.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+// Object overlay states usually modify the icon and indicate
+// That there may be non-item children under this item (e.g. clip)
+using OverlayState = int;
+enum OverlayStates : OverlayState {
+ OVERLAY_NONE = 0, // Nothing special about the object.
+ OVERLAY_CLIP = 1, // Object has a clip
+ OVERLAY_MASK = 2, // Object has a mask
+ OVERLAY_BOTH = 3, // Object has both clip and mask
+};
+
+/* Custom cell renderer for type icon */
+class CellRendererItemIcon : public Gtk::CellRenderer {
+public:
+
+ CellRendererItemIcon() :
+ Glib::ObjectBase(typeid(CellRenderer)),
+ Gtk::CellRenderer(),
+ _property_shape_type(*this, "shape_type", "unknown"),
+ _property_color(*this, "color", 0),
+ _property_clipmask(*this, "clipmask", 0),
+ _clip_overlay(nullptr),
+ _mask_overlay(nullptr),
+ _both_overlay(nullptr)
+ {
+ Gtk::IconSize::lookup(Gtk::ICON_SIZE_MENU, _size, _size);
+ }
+ ~CellRendererItemIcon() override = default;
+
+ Glib::PropertyProxy<std::string> property_shape_type() {
+ return _property_shape_type.get_proxy();
+ }
+ Glib::PropertyProxy<unsigned int> property_color() {
+ return _property_color.get_proxy();
+ }
+ Glib::PropertyProxy<unsigned int> property_clipmask() {
+ return _property_clipmask.get_proxy();
+ }
+
+ typedef sigc::signal<void (Glib::ustring)> type_signal_activated;
+ type_signal_activated signal_activated() {
+ return _signal_activated;
+ }
+
+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 paint_icon(const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf,
+ int x, int y);
+
+ 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:
+ type_signal_activated _signal_activated;
+ int _size;
+ Glib::Property<std::string> _property_shape_type;
+ Glib::Property<unsigned int> _property_color;
+ Glib::Property<unsigned int> _property_clipmask;
+ std::map<const std::string, Glib::RefPtr<Gdk::Pixbuf> > _icon_cache;
+
+ // Overlay indicators
+ Glib::RefPtr<Gdk::Pixbuf> _mask_overlay;
+ Glib::RefPtr<Gdk::Pixbuf> _clip_overlay;
+ Glib::RefPtr<Gdk::Pixbuf> _both_overlay;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+
+#endif /* __UI_DIALOG_SHAPEICON_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..08ba38b
--- /dev/null
+++ b/src/ui/widget/spin-button-tool-item.cpp
@@ -0,0 +1,607 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "spin-button-tool-item.h"
+
+#include <algorithm>
+#include <gtkmm/box.h>
+#include <gtkmm/image.h>
+#include <gtkmm/radiomenuitem.h>
+#include <gtkmm/toolbar.h>
+
+#include <cmath>
+#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 limits 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 (auto sb_tool_item = dynamic_cast<SpinButtonToolItem *>(tool_item)) {
+ // (1) The tool item is a SpinButtonToolItem, in which case, we just pass
+ // focus to its spin-button
+ sb_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, Gtk::RadioMenuItem* button)
+{
+ // Called both when Radio button is deactivated and activated. Only set when activated.
+ if (button->get_active()) {
+ 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,
+ bool enable)
+{
+ // Represent the value as a string
+ std::ostringstream ss;
+ ss << value;
+
+ Glib::ustring item_label = ss.str();
+
+ // Append the label if specified
+ if (!label.empty()) {
+ item_label += ": " + label;
+ }
+
+ auto numeric_option = Gtk::manage(new Gtk::RadioMenuItem(*group, item_label));
+ if (enable) {
+ numeric_option->set_active(); // Do before connecting toggled_handler.
+ }
+
+ // 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);
+ 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 = round_to_precision(adj->get_value());
+ auto lower = round_to_precision(adj->get_lower());
+ auto upper = round_to_precision(adj->get_upper());
+ auto page = adj->get_page_increment();
+
+ // Start by setting some fixed values based on the adjustment's
+ // parameters.
+ NumericMenuData values;
+
+ // first add all custom items (necessary)
+ for (auto custom_data : _custom_menu_data) {
+ if (custom_data.first >= lower && custom_data.first <= upper) {
+ values.emplace(custom_data);
+ }
+ }
+
+ values.emplace(adj_value, "");
+
+ // for quick page changes using mouse, step can changes can be done with +/- buttons on
+ // SpinButton
+ values.emplace(::fmin(adj_value + page, upper), "");
+ values.emplace(::fmax(adj_value - page, lower), "");
+
+ // add upper/lower limits to options
+ if (_show_upper_limit) {
+ values.emplace(upper, "");
+ }
+ if (_show_lower_limit) {
+ values.emplace(lower, "");
+ }
+
+ auto add_item = [&numeric_menu, this, &group, adj_value](ValueLabel value){
+ bool enable = (adj_value == value.first);
+ auto numeric_menu_item = create_numeric_menu_item(&group, value.first, value.second, enable);
+ numeric_menu->append(*numeric_menu_item);
+ };
+
+ if (_sort_decreasing) {
+ std::for_each(values.crbegin(), values.crend(), add_item);
+ } else {
+ std::for_each(values.cbegin(), values.cend(), add_item);
+ }
+
+ 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),
+ _digits(digits)
+{
+ 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();
+}
+
+/**
+ * \brief A wrapper of Geom::decimal_round to remember precision
+ */
+double
+SpinButtonToolItem::round_to_precision(double value) {
+ return Geom::decimal_round(value, _digits);
+}
+
+/**
+ * \brief [discouraged] Set numeric data option in Radio menu.
+ *
+ * \param[in] values values to provide as options
+ * \param[in] labels label to show for the value at same index in values.
+ *
+ * \detail Use is advised only when there are no labels.
+ * This is discouraged in favor of other overloads of the function, due to error prone
+ * usage. Using two vectors for related data, undermining encapsulation.
+ */
+void
+SpinButtonToolItem::set_custom_numeric_menu_data(const 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();
+
+ if (labels.empty()) {
+ for (const auto &value : values) {
+ _custom_menu_data.emplace(round_to_precision(value), "");
+ }
+ return;
+ }
+
+ int i = 0;
+ for (const auto &value : values) {
+ _custom_menu_data.emplace(round_to_precision(value), labels[i++]);
+ }
+}
+
+/**
+ * \brief Set numeric data options for Radio menu (densely labeled data).
+ *
+ * \param[in] value_labels value and labels to provide as options
+ *
+ * \detail Should be used when most of the values have an associated label (densely labeled data)
+ *
+ */
+void
+SpinButtonToolItem::set_custom_numeric_menu_data(const std::vector<ValueLabel>& value_labels) {
+ _custom_menu_data.clear();
+ for(const auto& value_label : value_labels) {
+ _custom_menu_data.emplace(round_to_precision(value_label.first), value_label.second);
+ }
+}
+
+
+/**
+ * \brief Set numeric data options for Radio menu (sparsely labeled data).
+ *
+ * \param[in] values values without labels
+ * \param[in] sparse_labels value and labels to provide as options
+ *
+ * \detail Should be used when very few values have an associated label (sparsely labeled data).
+ * Duplicate values in vector and map are acceptable but, values labels in map are
+ * preferred. Avoid using duplicate values intentionally though.
+ *
+ */
+void
+SpinButtonToolItem::set_custom_numeric_menu_data(const std::vector<double> &values,
+ const std::unordered_map<double, Glib::ustring> &sparse_labels)
+{
+ _custom_menu_data.clear();
+
+ for(const auto& value_label : sparse_labels) {
+ _custom_menu_data.emplace(round_to_precision(value_label.first), value_label.second);
+ }
+
+ for(const auto& value : values) {
+ _custom_menu_data.emplace(round_to_precision(value), "");
+ }
+
+}
+
+
+void SpinButtonToolItem::show_upper_limit(bool show) { _show_upper_limit = show; }
+
+void SpinButtonToolItem::show_lower_limit(bool show) { _show_lower_limit = show; }
+
+void SpinButtonToolItem::show_limits(bool show) { _show_upper_limit = _show_lower_limit = show; }
+
+void SpinButtonToolItem::sort_decreasing(bool decreasing) { _sort_decreasing = decreasing; }
+
+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..73caf14
--- /dev/null
+++ b/src/ui/widget/spin-button-tool-item.h
@@ -0,0 +1,130 @@
+// 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>
+#include <unordered_map>
+#include <utility>
+
+#include "2geom/math-utils.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:
+ using ValueLabel = std::pair<double, Glib::ustring>;
+ using NumericMenuData = std::map<double, Glib::ustring>;
+
+ 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 = 0.0; ///< The last value of the adjustment
+ bool _transfer_focus = false; ///< 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 = nullptr;
+
+ // Custom values and labels to add to the numeric popup-menu
+ NumericMenuData _custom_menu_data;
+
+ // To show or not to show upper/lower limit of the adjustment
+ bool _show_upper_limit = false;
+ bool _show_lower_limit = false;
+
+ // sort in decreasing order
+ bool _sort_decreasing = false;
+
+ // digits of adjustment
+ int _digits;
+
+ // just a wrapper for Geom::decimal_round to simplify calls
+ double round_to_precision(double value);
+
+ // 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::RadioMenuItem* button);
+
+ Gtk::Menu * create_numeric_menu();
+
+ Gtk::RadioMenuItem * create_numeric_menu_item(Gtk::RadioButtonGroup *group,
+ double value,
+ const Glib::ustring& label = "",
+ bool enable = false);
+
+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(const std::vector<double>& values,
+ const std::vector<Glib::ustring>& labels = std::vector<Glib::ustring>());
+
+ void set_custom_numeric_menu_data(const std::vector<ValueLabel> &value_labels);
+
+ void set_custom_numeric_menu_data(const std::vector<double>& values,
+ const std::unordered_map<double, Glib::ustring>& sparse_labels);
+
+ Glib::RefPtr<Gtk::Adjustment> get_adjustment();
+ void set_icon(const Glib::ustring& icon_name);
+
+ // display limits
+ void show_upper_limit(bool show = true);
+ void show_lower_limit(bool show = true);
+ void show_limits (bool show = true);
+
+ // sorting order
+ void sort_decreasing(bool decreasing = true);
+
+ SpinButton *get_spin_button() { return _btn; };
+};
+} // 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..ee59b22
--- /dev/null
+++ b/src/ui/widget/spin-scale.cpp
@@ -0,0 +1,247 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ *
+ * Copyright (C) 2007 Nicholas Bishop <nicholasbishop@gmail.com>
+ * 2008 Felipe C. da S. Sanches <juca@members.fsf.org>
+ * 2017 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+/*
+ * Derived from and replaces SpinSlider
+ */
+
+#include "spin-scale.h"
+
+#include <glibmm/i18n.h>
+#include <glibmm/stringutils.h>
+#include <gtkmm/enums.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 SPAttr a, const Glib::ustring tip_text)
+ : AttrWidget(a, value)
+ , _inkspinscale(value, lower, upper, step_increment, page_increment, 0)
+{
+ set_name("SpinScale");
+ _inkspinscale.drag_dest_unset();
+ _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 SPAttr 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 SPAttr a,
+ const Glib::ustring tip_text1, const Glib::ustring tip_text2)
+ : AttrWidget(a),
+ _s1(label1, value, lower, upper, step_increment, page_increment, digits, SPAttr::INVALID, tip_text1),
+ _s2(label2, value, lower, upper, step_increment, page_increment, digits, SPAttr::INVALID, tip_text2)
+{
+ 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.set_relief(Gtk::RELIEF_NONE);
+ _link.set_focus_on_click(false);
+ _link.set_can_focus(false);
+ _link.get_style_context()->add_class("link-edit-button");
+ _link.set_valign(Gtk::ALIGN_CENTER);
+ _link.signal_clicked().connect(sigc::mem_fun(*this, &DualSpinScale::link_toggled));
+
+ Gtk::Box* vb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
+ vb->add(_s1);
+ _s1.set_margin_bottom(3);
+ vb->add(_s2);
+ pack_start(*vb);
+ pack_start(_link, false, false);
+ set_link_active(true);
+ _s2.set_sensitive(false);
+
+ show_all();
+}
+
+void DualSpinScale::set_link_active(bool link) {
+ _linked = link;
+ _link.set_image_from_icon_name(_linked ? "entries-linked" : "entries-unlinked", Gtk::ICON_SIZE_LARGE_TOOLBAR);
+}
+
+Glib::ustring DualSpinScale::get_as_attribute() const
+{
+ if (_linked) {
+ 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]);
+
+ set_link_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()
+{
+ _linked = !_linked;
+ set_link_active(_linked);
+ _s2.set_sensitive(!_linked);
+ update_linked();
+}
+
+void DualSpinScale::update_linked()
+{
+ if (_linked) {
+ _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..c924a43
--- /dev/null
+++ b/src/ui/widget/spin-scale.h
@@ -0,0 +1,116 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ *
+ * Copyright (C) 2007 Nicholas Bishop <nicholasbishop@gmail.com>
+ * 2017 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+/*
+ * Derived from and replaces SpinSlider
+ */
+
+#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 SPAttr a = SPAttr::INVALID, const Glib::ustring tip_text = "");
+
+ // Used by extensions
+ SpinScale(const Glib::ustring label,
+ Glib::RefPtr<Gtk::Adjustment> adjustment, int digits,
+ const SPAttr a = SPAttr::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 SPAttr 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();
+ void set_link_active(bool link);
+ sigc::signal<void ()> _signal_value_changed;
+ SpinScale _s1, _s2;
+ bool _linked = true;
+ Gtk::Button _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/spinbutton.cpp b/src/ui/widget/spinbutton.cpp
new file mode 100644
index 0000000..f63ffd4
--- /dev/null
+++ b/src/ui/widget/spinbutton.cpp
@@ -0,0 +1,144 @@
+// 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 "scroll-utils.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 {
+
+MathSpinButton::MathSpinButton(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade)
+ : Gtk::SpinButton(cobject)
+{
+ drag_dest_unset();
+}
+
+int MathSpinButton::on_input(double *newvalue)
+{
+ try {
+ auto eval = Inkscape::Util::ExpressionEvaluator(get_text().c_str(), nullptr);
+ auto result = eval.evaluate();
+ *newvalue = result.value;
+ } catch (Inkscape::Util::EvaluatorException &e) {
+ g_message ("%s", e.what());
+ return false;
+ }
+ return true;
+}
+
+int SpinButton::on_input(double* newvalue)
+{
+ if (_dont_evaluate) return false;
+
+ 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_focus_in_event(GdkEventFocus *event)
+{
+ _on_focus_in_value = get_value();
+ return parent_type::on_focus_in_event(event);
+}
+
+bool SpinButton::on_key_press_event(GdkEventKey* event)
+{
+ switch (Inkscape::UI::Tools::get_latin_keyval (event)) {
+ case GDK_KEY_Escape: // defocus
+ undo();
+ defocus();
+ break;
+ case GDK_KEY_Return: // defocus
+ case GDK_KEY_KP_Enter:
+ defocus();
+ break;
+ case GDK_KEY_Tab:
+ case GDK_KEY_ISO_Left_Tab:
+ // set the flag meaning "do not leave toolbar when changing value"
+ _stay = true;
+ break;
+ case GDK_KEY_z:
+ case GDK_KEY_Z:
+ _stay = true;
+ if (event->state & GDK_CONTROL_MASK) {
+ undo();
+ return true; // I consumed the event
+ }
+ break;
+ default:
+ break;
+ }
+
+ return parent_type::on_key_press_event(event);
+}
+
+void SpinButton::undo()
+{
+ set_value(_on_focus_in_value);
+}
+
+void SpinButton::defocus()
+{
+ // defocus spinbutton by moving focus to the canvas, unless "stay" is on
+ if (_stay) {
+ _stay = false;
+ } else {
+ Gtk::Widget *widget = _defocus_widget ? _defocus_widget : get_scrollable_ancestor(this);
+ if (widget) {
+ widget->grab_focus();
+ }
+ }
+}
+
+} // 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..2c211a7
--- /dev/null
+++ b/src/ui/widget/spinbutton.h
@@ -0,0 +1,124 @@
+// 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>
+
+#include "scrollprotected.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class UnitMenu;
+class UnitTracker;
+
+/**
+ * A spin button for use with builders.
+ */
+class MathSpinButton : public Gtk::SpinButton
+{
+public:
+ MathSpinButton(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade);
+ ~MathSpinButton() override{};
+protected:
+ int on_input(double* newvalue) override;
+};
+
+/**
+ * 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 ScrollProtected<Gtk::SpinButton>
+{
+ using parent_type = ScrollProtected<Gtk::SpinButton>;
+
+public:
+ using parent_type::parent_type;
+
+ void setUnitMenu(UnitMenu* unit_menu) { _unit_menu = unit_menu; };
+
+ void addUnitTracker(UnitTracker* ut) { _unit_tracker = ut; };
+
+ // TODO: Might be better to just have a default value and a reset() method?
+ inline void set_zeroable(const bool zeroable = true) { _zeroable = zeroable; }
+ inline void set_oneable(const bool oneable = true) { _oneable = oneable; }
+
+ inline bool get_zeroable() const { return _zeroable; }
+ inline bool get_oneable() const { return _oneable; }
+
+ void defocus();
+
+protected:
+ UnitMenu *_unit_menu = nullptr; ///< Linked unit menu for unit conversion in entered expressions.
+ UnitTracker *_unit_tracker = nullptr; ///< Linked unit tracker for unit conversion in entered expressions.
+ double _on_focus_in_value = 0.;
+ Gtk::Widget *_defocus_widget = nullptr; ///< Widget that should grab focus when the spinbutton defocuses
+
+ bool _zeroable = false; ///< Reset-value should be zero
+ bool _oneable = false; ///< Reset-value should be one
+
+ bool _stay = false; ///< Whether to ignore defocusing
+ bool _dont_evaluate = false; ///< Don't attempt to evaluate expressions
+
+ /**
+ * 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_focus_in_event(GdkEventFocus *) override;
+
+ /**
+ * Handle specific keypress events, like Ctrl+Z.
+ *
+ * @retval false continue with default handler.
+ * @retval true don't call default handler.
+ */
+ bool on_key_press_event(GdkEventKey *) override;
+
+ /**
+ * Undo the editing, by resetting the value upon when the spinbutton got focus.
+ */
+ void undo();
+
+ public:
+ inline void set_defocus_widget(const decltype(_defocus_widget) widget) { _defocus_widget = widget; }
+ inline void set_dont_evaluate(bool flag) { _dont_evaluate = flag; }
+};
+
+} // 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/stroke-style.cpp b/src/ui/widget/stroke-style.cpp
new file mode 100644
index 0000000..9233586
--- /dev/null
+++ b/src/ui/widget/stroke-style.cpp
@@ -0,0 +1,1223 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Bryce Harrington <brycehar@bryceharrington.org>
+ * bulia byak <buliabyak@users.sf.net>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Josh Andler <scislac@users.sf.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2001-2005 authors
+ * Copyright (C) 2001 Ximian, Inc.
+ * Copyright (C) 2004 John Cliff
+ * Copyright (C) 2008 Maximilian Albert (gtkmm-ification)
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "stroke-style.h"
+
+#include "object/sp-marker.h"
+#include "object/sp-namedview.h"
+#include "object/sp-rect.h"
+#include "object/sp-stop.h"
+#include "object/sp-text.h"
+
+#include "svg/svg-color.h"
+
+#include "ui/icon-loader.h"
+#include "ui/widget/dash-selector.h"
+#include "ui/widget/marker-combo-box.h"
+#include "ui/widget/unit-menu.h"
+#include "ui/tools/marker-tool.h"
+#include "ui/dialog/dialog-base.h"
+
+#include "actions/actions-tools.h"
+
+#include "widgets/style-utils.h"
+
+using Inkscape::DocumentUndo;
+using Inkscape::Util::unit_table;
+
+/**
+ * Extract the actual name of the link
+ * e.g. get mTriangle from url(#mTriangle).
+ * \return Buffer containing the actual name, allocated from GLib;
+ * the caller should free the buffer when they no longer need it.
+ */
+SPObject* getMarkerObj(gchar const *n, SPDocument *doc)
+{
+ gchar const *p = n;
+ while (*p != '\0' && *p != '#') {
+ p++;
+ }
+
+ if (*p == '\0' || p[1] == '\0') {
+ return nullptr;
+ }
+
+ p++;
+ int c = 0;
+ while (p[c] != '\0' && p[c] != ')') {
+ c++;
+ }
+
+ if (p[c] == '\0') {
+ return nullptr;
+ }
+
+ gchar* b = g_strdup(p);
+ b[c] = '\0';
+
+ // FIXME: get the document from the object and let the caller pass it in
+ SPObject *marker = doc->getObjectById(b);
+
+ g_free(b);
+ return marker;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Construct a stroke-style radio button with a given icon
+ *
+ * \param[in] grp The Gtk::RadioButtonGroup to which to add the new button
+ * \param[in] icon The icon to use for the button
+ * \param[in] button_type The type of stroke-style radio button (join/cap)
+ * \param[in] stroke_style The style attribute to associate with the button
+ */
+StrokeStyle::StrokeStyleButton::StrokeStyleButton(Gtk::RadioButtonGroup &grp,
+ char const *icon,
+ StrokeStyleButtonType button_type,
+ gchar const *stroke_style)
+ :
+ Gtk::RadioButton(grp),
+ button_type(button_type),
+ stroke_style(stroke_style)
+{
+ show();
+ set_mode(false);
+
+ auto px = Gtk::manage(sp_get_icon_image(icon, Gtk::ICON_SIZE_LARGE_TOOLBAR));
+ g_assert(px != nullptr);
+ px->show();
+ add(*px);
+}
+
+std::vector<double> parse_pattern(const Glib::ustring& input) {
+ std::vector<double> output;
+ if (input.empty()) return output;
+
+ std::istringstream stream(input.c_str());
+ while (stream) {
+ double val;
+ stream >> val;
+ if (stream) {
+ output.push_back(val);
+ }
+ }
+
+ return output;
+}
+
+StrokeStyle::StrokeStyle() :
+ Gtk::Box(),
+ miterLimitSpin(),
+ widthSpin(),
+ unitSelector(),
+ joinMiter(),
+ joinRound(),
+ joinBevel(),
+ capButt(),
+ capRound(),
+ capSquare(),
+ dashSelector(),
+ update(false),
+ desktop(nullptr),
+ startMarkerConn(),
+ midMarkerConn(),
+ endMarkerConn(),
+ _old_unit(nullptr)
+{
+ set_name("StrokeSelector");
+ table = Gtk::manage(new Gtk::Grid());
+ table->set_border_width(4);
+ table->set_row_spacing(4);
+ table->set_hexpand(false);
+ table->set_halign(Gtk::ALIGN_CENTER);
+ table->show();
+ add(*table);
+
+ Gtk::Box *hb;
+ gint i = 0;
+
+ //spw_label(t, C_("Stroke width", "_Width:"), 0, i);
+
+ hb = spw_hbox(table, 3, 1, i);
+
+// TODO: when this is gtkmmified, use an Inkscape::UI::Widget::ScalarUnit instead of the separate
+// spinbutton and unit selector for stroke width. In sp_stroke_style_line_update, use
+// setHundredPercent to remember the averaged width corresponding to 100%. Then the
+// stroke_width_set_unit will be removed (because ScalarUnit takes care of conversions itself)
+ widthAdj = new Glib::RefPtr<Gtk::Adjustment>(Gtk::Adjustment::create(1.0, 0.0, 1000.0, 0.1, 10.0, 0.0));
+ widthSpin = new Inkscape::UI::Widget::SpinButton(*widthAdj, 0.1, 3);
+ widthSpin->set_tooltip_text(_("Stroke width"));
+ widthSpin->show();
+ spw_label(table, C_("Stroke width", "_Width:"), 0, i, widthSpin);
+
+ sp_dialog_defocus_on_enter_cpp(widthSpin);
+
+ hb->pack_start(*widthSpin, false, false, 0);
+ unitSelector = Gtk::manage(new Inkscape::UI::Widget::UnitMenu());
+ unitSelector->setUnitType(Inkscape::Util::UNIT_TYPE_LINEAR);
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+
+ unitSelector->addUnit(*unit_table.getUnit("%"));
+ unitSelector->append("hairline", _("Hairline"));
+ _old_unit = unitSelector->getUnit();
+ if (desktop) {
+ unitSelector->setUnit(desktop->getNamedView()->display_units->abbr);
+ _old_unit = desktop->getNamedView()->display_units;
+ }
+ widthSpin->setUnitMenu(unitSelector);
+ unitSelector->signal_changed().connect(sigc::mem_fun(*this, &StrokeStyle::unitChangedCB));
+ unitSelector->show();
+
+ hb->pack_start(*unitSelector, FALSE, FALSE, 0);
+ (*widthAdj)->signal_value_changed().connect(sigc::mem_fun(*this, &StrokeStyle::setStrokeWidth));
+
+ i++;
+
+ /* Dash */
+ spw_label(table, _("Dashes:"), 0, i, nullptr); //no mnemonic for now
+ //decide what to do:
+ // implement a set_mnemonic_source function in the
+ // Inkscape::UI::Widget::DashSelector class, so that we do not have to
+ // expose any of the underlying widgets?
+ dashSelector = Gtk::manage(new Inkscape::UI::Widget::DashSelector);
+ _pattern = Gtk::make_managed<Gtk::Entry>();
+
+ dashSelector->show();
+ dashSelector->set_hexpand();
+ dashSelector->set_halign(Gtk::ALIGN_FILL);
+ dashSelector->set_valign(Gtk::ALIGN_CENTER);
+ table->attach(*dashSelector, 1, i, 3, 1);
+ dashSelector->changed_signal.connect(sigc::mem_fun(*this, &StrokeStyle::setStrokeDash));
+
+ i++;
+
+ table->attach(*_pattern, 1, i, 4, 1);
+ _pattern_label = spw_label(table, _("_Pattern:"), 0, i, _pattern);
+ _pattern_label->set_tooltip_text(_("Repeating \"dash gap ...\" pattern"));
+ _pattern->set_no_show_all();
+ _pattern_label->set_no_show_all();
+ _pattern->signal_changed().connect([=](){
+ if (update || _editing_pattern) return;
+
+ auto pat = parse_pattern(_pattern->get_text());
+ _editing_pattern = true;
+ update = true;
+ dashSelector->set_dash(pat, dashSelector->get_offset());
+ update = false;
+ setStrokeDash();
+ _editing_pattern = false;
+ });
+ update_pattern(0, nullptr);
+
+ i++;
+
+ /* Drop down marker selectors*/
+ // TRANSLATORS: Path markers are an SVG feature that allows you to attach arbitrary shapes
+ // (arrowheads, bullets, faces, whatever) to the start, end, or middle nodes of a path.
+
+ spw_label(table, _("Markers:"), 0, i, nullptr);
+
+ hb = spw_hbox(table, 1, 1, i);
+ i++;
+
+ startMarkerCombo = Gtk::manage(new MarkerComboBox("marker-start", SP_MARKER_LOC_START));
+ startMarkerCombo->set_tooltip_text(_("Start Markers are drawn on the first node of a path or shape"));
+ startMarkerConn = startMarkerCombo->signal_changed().connect([=]() { markerSelectCB(startMarkerCombo, SP_MARKER_LOC_START); });
+ startMarkerCombo->edit_signal.connect([=] { enterEditMarkerMode(SP_MARKER_LOC_START); });
+ startMarkerCombo->show();
+
+ hb->pack_start(*startMarkerCombo, true, true, 0);
+
+ midMarkerCombo = Gtk::manage(new MarkerComboBox("marker-mid", SP_MARKER_LOC_MID));
+ midMarkerCombo->set_tooltip_text(_("Mid Markers are drawn on every node of a path or shape except the first and last nodes"));
+ midMarkerConn = midMarkerCombo->signal_changed().connect([=]() { markerSelectCB(midMarkerCombo, SP_MARKER_LOC_MID); });
+ midMarkerCombo->edit_signal.connect([=] { enterEditMarkerMode(SP_MARKER_LOC_MID); });
+ midMarkerCombo->show();
+
+ hb->pack_start(*midMarkerCombo, true, true, 0);
+
+ endMarkerCombo = Gtk::manage(new MarkerComboBox("marker-end", SP_MARKER_LOC_END));
+ endMarkerCombo->set_tooltip_text(_("End Markers are drawn on the last node of a path or shape"));
+ endMarkerConn = endMarkerCombo->signal_changed().connect([=]() { markerSelectCB(endMarkerCombo, SP_MARKER_LOC_END); });
+ endMarkerCombo->edit_signal.connect([=] { enterEditMarkerMode(SP_MARKER_LOC_END); });
+ endMarkerCombo->show();
+
+ hb->pack_start(*endMarkerCombo, true, true, 0);
+ i++;
+
+ /* Join type */
+ // TRANSLATORS: The line join style specifies the shape to be used at the
+ // corners of paths. It can be "miter", "round" or "bevel".
+ spw_label(table, _("Join:"), 0, i, nullptr);
+
+ hb = spw_hbox(table, 3, 1, i);
+
+ Gtk::RadioButtonGroup joinGrp;
+
+ joinBevel = makeRadioButton(joinGrp, INKSCAPE_ICON("stroke-join-bevel"),
+ hb, STROKE_STYLE_BUTTON_JOIN, "bevel");
+
+ // TRANSLATORS: Bevel join: joining lines with a blunted (flattened) corner.
+ // For an example, draw a triangle with a large stroke width and modify the
+ // "Join" option (in the Fill and Stroke dialog).
+ joinBevel->set_tooltip_text(_("Bevel join"));
+
+ joinRound = makeRadioButton(joinGrp, INKSCAPE_ICON("stroke-join-round"),
+ hb, STROKE_STYLE_BUTTON_JOIN, "round");
+
+ // TRANSLATORS: Round join: joining lines with a rounded corner.
+ // For an example, draw a triangle with a large stroke width and modify the
+ // "Join" option (in the Fill and Stroke dialog).
+ joinRound->set_tooltip_text(_("Round join"));
+
+ joinMiter = makeRadioButton(joinGrp, INKSCAPE_ICON("stroke-join-miter"),
+ hb, STROKE_STYLE_BUTTON_JOIN, "miter");
+
+ // TRANSLATORS: Miter join: joining lines with a sharp (pointed) corner.
+ // For an example, draw a triangle with a large stroke width and modify the
+ // "Join" option (in the Fill and Stroke dialog).
+ joinMiter->set_tooltip_text(_("Miter join"));
+
+ /* Miterlimit */
+ // TRANSLATORS: Miter limit: only for "miter join", this limits the length
+ // of the sharp "spike" when the lines connect at too sharp an angle.
+ // When two line segments meet at a sharp angle, a miter join results in a
+ // spike that extends well beyond the connection point. The purpose of the
+ // miter limit is to cut off such spikes (i.e. convert them into bevels)
+ // when they become too long.
+ //spw_label(t, _("Miter _limit:"), 0, i);
+ miterLimitAdj = new Glib::RefPtr<Gtk::Adjustment>(Gtk::Adjustment::create(4.0, 0.0, 100000.0, 0.1, 10.0, 0.0));
+ miterLimitSpin = new Inkscape::UI::Widget::SpinButton(*miterLimitAdj, 0.1, 2);
+ miterLimitSpin->set_tooltip_text(_("Maximum length of the miter (in units of stroke width)"));
+ miterLimitSpin->set_width_chars(6);
+ miterLimitSpin->show();
+ sp_dialog_defocus_on_enter_cpp(miterLimitSpin);
+
+ hb->pack_start(*miterLimitSpin, false, false, 0);
+ (*miterLimitAdj)->signal_value_changed().connect(sigc::mem_fun(*this, &StrokeStyle::setStrokeMiter));
+ i++;
+
+ /* Cap type */
+ // TRANSLATORS: cap type specifies the shape for the ends of lines
+ //spw_label(t, _("_Cap:"), 0, i);
+ spw_label(table, _("Cap:"), 0, i, nullptr);
+
+ hb = spw_hbox(table, 3, 1, i);
+
+ Gtk::RadioButtonGroup capGrp;
+
+ capButt = makeRadioButton(capGrp, INKSCAPE_ICON("stroke-cap-butt"),
+ hb, STROKE_STYLE_BUTTON_CAP, "butt");
+
+ // TRANSLATORS: Butt cap: the line shape does not extend beyond the end point
+ // of the line; the ends of the line are square
+ capButt->set_tooltip_text(_("Butt cap"));
+
+ capRound = makeRadioButton(capGrp, INKSCAPE_ICON("stroke-cap-round"),
+ hb, STROKE_STYLE_BUTTON_CAP, "round");
+
+ // TRANSLATORS: Round cap: the line shape extends beyond the end point of the
+ // line; the ends of the line are rounded
+ capRound->set_tooltip_text(_("Round cap"));
+
+ capSquare = makeRadioButton(capGrp, INKSCAPE_ICON("stroke-cap-square"),
+ hb, STROKE_STYLE_BUTTON_CAP, "square");
+
+ // TRANSLATORS: Square cap: the line shape extends beyond the end point of the
+ // line; the ends of the line are square
+ capSquare->set_tooltip_text(_("Square cap"));
+
+ i++;
+
+ /* Paint order */
+ // TRANSLATORS: Paint order determines the order the 'fill', 'stroke', and 'markers are painted.
+ spw_label(table, _("Order:"), 0, i, nullptr);
+
+ hb = spw_hbox(table, 4, 1, i);
+
+ Gtk::RadioButtonGroup paintOrderGrp;
+
+ paintOrderFSM = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-fsm"),
+ hb, STROKE_STYLE_BUTTON_ORDER, "normal");
+ paintOrderFSM->set_tooltip_text(_("Fill, Stroke, Markers"));
+
+ paintOrderSFM = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-sfm"),
+ hb, STROKE_STYLE_BUTTON_ORDER, "stroke fill markers");
+ paintOrderSFM->set_tooltip_text(_("Stroke, Fill, Markers"));
+
+ paintOrderFMS = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-fms"),
+ hb, STROKE_STYLE_BUTTON_ORDER, "fill markers stroke");
+ paintOrderFMS->set_tooltip_text(_("Fill, Markers, Stroke"));
+
+ i++;
+
+ hb = spw_hbox(table, 4, 1, i);
+
+ paintOrderMFS = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-mfs"),
+ hb, STROKE_STYLE_BUTTON_ORDER, "markers fill stroke");
+ paintOrderMFS->set_tooltip_text(_("Markers, Fill, Stroke"));
+
+ paintOrderSMF = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-smf"),
+ hb, STROKE_STYLE_BUTTON_ORDER, "stroke markers fill");
+ paintOrderSMF->set_tooltip_text(_("Stroke, Markers, Fill"));
+
+ paintOrderMSF = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-msf"),
+ hb, STROKE_STYLE_BUTTON_ORDER, "markers stroke fill");
+ paintOrderMSF->set_tooltip_text(_("Markers, Stroke, Fill"));
+
+ i++;
+}
+
+StrokeStyle::~StrokeStyle()
+{
+}
+
+void StrokeStyle::setDesktop(SPDesktop *desktop)
+{
+ if (this->desktop != desktop) {
+
+ if (this->desktop) {
+ _document_replaced_connection.disconnect();
+ }
+ this->desktop = desktop;
+
+ if (!desktop) {
+ return;
+ }
+
+ _document_replaced_connection =
+ desktop->connectDocumentReplaced(sigc::mem_fun(*this, &StrokeStyle::_handleDocumentReplaced));
+
+ _handleDocumentReplaced(nullptr, desktop->getDocument());
+
+ updateLine();
+ }
+}
+
+void StrokeStyle::_handleDocumentReplaced(SPDesktop *, SPDocument *document)
+{
+ for (MarkerComboBox *combo : { startMarkerCombo, midMarkerCombo, endMarkerCombo }) {
+ combo->setDocument(document);
+ }
+}
+
+
+/**
+ * Helper function for creating stroke-style radio buttons.
+ *
+ * \param[in] grp The Gtk::RadioButtonGroup in which to add the button
+ * \param[in] icon The icon for the button
+ * \param[in] hb The Gtk::Box container in which to add the button
+ * \param[in] button_type The type (join/cap) for the button
+ * \param[in] stroke_style The style attribute to associate with the button
+ *
+ * \details After instantiating the button, it is added to a container box and
+ * a handler for the toggle event is connected.
+ */
+StrokeStyle::StrokeStyleButton *
+StrokeStyle::makeRadioButton(Gtk::RadioButtonGroup &grp,
+ char const *icon,
+ Gtk::Box *hb,
+ StrokeStyleButtonType button_type,
+ gchar const *stroke_style)
+{
+ g_assert(icon != nullptr);
+ g_assert(hb != nullptr);
+
+ StrokeStyleButton *tb = new StrokeStyleButton(grp, icon, button_type, stroke_style);
+
+ hb->pack_start(*tb, false, false, 0);
+
+ tb->signal_toggled().connect(sigc::bind<StrokeStyleButton *, StrokeStyle *>(
+ sigc::ptr_fun(&StrokeStyle::buttonToggledCB), tb, this));
+
+ return tb;
+}
+
+void StrokeStyle::enterEditMarkerMode(SPMarkerLoc _editMarkerMode)
+{
+ SPDesktop *desktop = this->desktop;
+
+ if (desktop) {
+ set_active_tool(desktop, "Marker");
+ Inkscape::UI::Tools::MarkerTool *mt = dynamic_cast<Inkscape::UI::Tools::MarkerTool*>(desktop->event_context);
+
+ if(mt) {
+ mt->editMarkerMode = _editMarkerMode;
+ mt->selection_changed(desktop->getSelection());
+ }
+ }
+}
+
+
+bool StrokeStyle::areMarkersBeingUpdated()
+{
+ return startMarkerCombo->in_update() || midMarkerCombo->in_update() || endMarkerCombo->in_update();
+}
+
+/**
+ * Handles when user selects one of the markers from the marker combobox.
+ * Gets the marker uri string and applies it to all selected
+ * items in the current desktop.
+ */
+void StrokeStyle::markerSelectCB(MarkerComboBox *marker_combo, SPMarkerLoc const which)
+{
+ if (update || areMarkersBeingUpdated()) {
+ return;
+ }
+
+ SPDocument *document = desktop->getDocument();
+ if (!document) {
+ return;
+ }
+
+ // Get marker ID; could be empty (to remove marker)
+ std::string marker = marker_combo->get_active_marker_uri();
+
+ update = true;
+
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ gchar const *combo_id = marker_combo->get_id();
+ sp_repr_css_set_property(css, combo_id, marker.c_str());
+
+ for (auto item : desktop->getSelection()->items()) {
+ if (!is<SPShape>(item)) {
+ continue;
+ }
+ if (Inkscape::XML::Node* selrepr = item->getRepr()) {
+ sp_repr_css_change_recursive(selrepr, css, "style");
+ }
+
+ item->requestModified(SP_OBJECT_MODIFIED_FLAG);
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
+ // perform update to make sure any previously referenced markers are released,
+ // so they can be collected by DocumentUndo::done collect orphans
+ document->ensureUpToDate();
+
+ DocumentUndo::done(document, _("Set markers"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ }
+
+ // edit marker mode - update
+ if (auto mt = dynamic_cast<Inkscape::UI::Tools::MarkerTool*>(desktop->event_context)) {
+ mt->editMarkerMode = which;
+ mt->selection_changed(desktop->getSelection());
+ }
+
+ sp_repr_css_attr_unref(css);
+ css = nullptr;
+
+ update = false;
+};
+
+/**
+ * Callback for when UnitMenu widget is modified.
+ * Triggers update action.
+ */
+void StrokeStyle::unitChangedCB()
+{
+ Inkscape::Util::Unit const *new_unit = unitSelector->getUnit();
+
+ if (_old_unit == new_unit)
+ return;
+
+ // If the unit selector is set to hairline, don't do the normal conversion.
+ if (isHairlineSelected()) {
+ // Force update in setStrokeWidth
+ _old_unit = new_unit;
+ _last_width = -1;
+ setStrokeWidth();
+ return;
+ }
+
+ if (new_unit->type == Inkscape::Util::UNIT_TYPE_DIMENSIONLESS) {
+ // Prevent update in setStrokeWidth
+ _last_width = 100.0;
+ widthSpin->set_value(100);
+ } else {
+ // Remove the non-scaling-stroke effect and the hairline extensions
+ if (!update) {
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_unset_property(css, "vector-effect");
+ sp_repr_css_unset_property(css, "-inkscape-stroke");
+ sp_desktop_set_style(desktop, css);
+ sp_repr_css_attr_unref(css);
+ css = nullptr;
+ DocumentUndo::done(desktop->getDocument(), _("Remove hairline stroke"),
+ INKSCAPE_ICON("dialog-fill-and-stroke"));
+ }
+ if (_old_unit->type == Inkscape::Util::UNIT_TYPE_DIMENSIONLESS) {
+ // Prevent update of unit (inf-loop) in updateLine
+ _old_unit = new_unit;
+ // Going from % to any other unit means our widthSpin is completely invalid.
+ updateLine();
+ } else {
+ // Scale the value and record the old_unit
+ widthSpin->set_value(Inkscape::Util::Quantity::convert(widthSpin->get_value(), _old_unit, new_unit));
+ }
+ }
+ _old_unit = new_unit;
+}
+
+/**
+ * Callback for when stroke style widget is modified.
+ * Triggers update action.
+ */
+void
+StrokeStyle::selectionModifiedCB(guint flags)
+{
+ // We care deeply about only updating when the style is updated
+ // if we update on other flags, we slow inkscape down when dragging
+ if (flags & (SP_OBJECT_STYLE_MODIFIED_FLAG)) {
+ updateLine();
+ }
+}
+
+/**
+ * Callback for when stroke style widget is changed.
+ * Triggers update action.
+ */
+void
+StrokeStyle::selectionChangedCB()
+{
+ updateLine();
+}
+
+/**
+ * Get a dash array and offset from the style.
+ *
+ * Both values are de-scaled by the style's width if needed.
+ */
+std::vector<double>
+StrokeStyle::getDashFromStyle(SPStyle *style, double &offset)
+{
+ auto prefs = Inkscape::Preferences::get();
+
+ std::vector<double> ret;
+ size_t len = style->stroke_dasharray.values.size();
+
+ double scaledash = 1.0;
+ if (prefs->getBool("/options/dash/scale", true) && style->stroke_width.computed) {
+ scaledash = style->stroke_width.computed;
+ }
+
+ offset = style->stroke_dashoffset.value / scaledash;
+ for (unsigned i = 0; i < len; i++) {
+ ret.push_back(style->stroke_dasharray.values[i].value / scaledash);
+ }
+ return ret;
+}
+
+/**
+ * Sets selector widgets' dash style from an SPStyle object.
+ */
+void
+StrokeStyle::setDashSelectorFromStyle(Inkscape::UI::Widget::DashSelector *dsel, SPStyle *style)
+{
+ double offset = 0;
+ auto d = getDashFromStyle(style, offset);
+ if (!d.empty()) {
+ dsel->set_dash(d, offset);
+ update_pattern(d.size(), d.data());
+ } else {
+ dsel->set_dash(std::vector<double>(), 0.0);
+ update_pattern(0, nullptr);
+ }
+}
+
+void StrokeStyle::update_pattern(int ndash, const double* pattern) {
+ if (_editing_pattern || _pattern->has_focus()) return;
+
+ std::ostringstream ost;
+ for (int i = 0; i < ndash; ++i) {
+ ost << pattern[i] << ' ';
+ }
+ _pattern->set_text(ost.str().c_str());
+ if (ndash > 0) {
+ _pattern_label->show();
+ _pattern->show();
+ }
+ else {
+ _pattern_label->hide();
+ _pattern->hide();
+ }
+}
+
+/**
+ * Sets the join type for a line, and updates the stroke style widget's buttons
+ */
+void
+StrokeStyle::setJoinType (unsigned const jointype)
+{
+ Gtk::RadioButton *tb = nullptr;
+ switch (jointype) {
+ case SP_STROKE_LINEJOIN_MITER:
+ tb = joinMiter;
+ break;
+ case SP_STROKE_LINEJOIN_ROUND:
+ tb = joinRound;
+ break;
+ case SP_STROKE_LINEJOIN_BEVEL:
+ tb = joinBevel;
+ break;
+ default:
+ // Should not happen
+ std::cerr << "StrokeStyle::setJoinType(): Invalid value: " << jointype << std::endl;
+ tb = joinMiter;
+ break;
+ }
+ setJoinButtons(tb);
+}
+
+/**
+ * Sets the cap type for a line, and updates the stroke style widget's buttons
+ */
+void
+StrokeStyle::setCapType (unsigned const captype)
+{
+ Gtk::RadioButton *tb = nullptr;
+ switch (captype) {
+ case SP_STROKE_LINECAP_BUTT:
+ tb = capButt;
+ break;
+ case SP_STROKE_LINECAP_ROUND:
+ tb = capRound;
+ break;
+ case SP_STROKE_LINECAP_SQUARE:
+ tb = capSquare;
+ break;
+ default:
+ // Should not happen
+ std::cerr << "StrokeStyle::setCapType(): Invalid value: " << captype << std::endl;
+ tb = capButt;
+ break;
+ }
+ setCapButtons(tb);
+}
+
+/**
+ * Sets the cap type for a line, and updates the stroke style widget's buttons
+ */
+void
+StrokeStyle::setPaintOrder (gchar const *paint_order)
+{
+ Gtk::RadioButton *tb = paintOrderFSM;
+
+ SPIPaintOrder temp;
+ temp.read( paint_order );
+
+ if (temp.layer[0] != SP_CSS_PAINT_ORDER_NORMAL) {
+
+ if (temp.layer[0] == SP_CSS_PAINT_ORDER_FILL) {
+ if (temp.layer[1] == SP_CSS_PAINT_ORDER_STROKE) {
+ tb = paintOrderFSM;
+ } else {
+ tb = paintOrderFMS;
+ }
+ } else if (temp.layer[0] == SP_CSS_PAINT_ORDER_STROKE) {
+ if (temp.layer[1] == SP_CSS_PAINT_ORDER_FILL) {
+ tb = paintOrderSFM;
+ } else {
+ tb = paintOrderSMF;
+ }
+ } else {
+ if (temp.layer[1] == SP_CSS_PAINT_ORDER_STROKE) {
+ tb = paintOrderMSF;
+ } else {
+ tb = paintOrderMFS;
+ }
+ }
+
+ }
+ setPaintOrderButtons(tb);
+}
+
+/**
+ * Callback for when stroke style widget is updated, including markers, cap type,
+ * join type, etc.
+ */
+void
+StrokeStyle::updateLine()
+{
+ if (update) {
+ return;
+ }
+
+ auto *widg = get_parent()->get_parent()->get_parent()->get_parent();
+ auto dialogbase = dynamic_cast<Inkscape::UI::Dialog::DialogBase*>(widg);
+ if (dialogbase && !dialogbase->getShowing()) {
+ return;
+ }
+
+ update = true;
+
+ Inkscape::Selection *sel = desktop ? desktop->getSelection() : nullptr;
+
+ if (!sel || sel->isEmpty()) {
+ // Nothing selected, grey-out all controls in the stroke-style dialog
+ table->set_sensitive(false);
+
+ update = false;
+
+ return;
+ }
+
+ FillOrStroke kind = STROKE;
+
+ // create temporary style
+ SPStyle query(SP_ACTIVE_DOCUMENT);
+ // query into it
+ int result_sw = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_STROKEWIDTH);
+ int result_ml = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_STROKEMITERLIMIT);
+ int result_cap = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_STROKECAP);
+ int result_join = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_STROKEJOIN);
+ int result_order = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_PAINTORDER);
+
+ SPIPaint &targPaint = *query.getFillOrStroke(kind == FILL);
+
+ {
+ table->set_sensitive(true);
+ widthSpin->set_sensitive(true);
+
+ if (result_sw == QUERY_STYLE_MULTIPLE_AVERAGED) {
+ unitSelector->setUnit("%");
+ } else if (query.stroke_extensions.hairline) {
+ unitSelector->set_active_id("hairline");
+ } else {
+ // same width, or only one object; no sense to keep percent, switch to absolute
+ Inkscape::Util::Unit const *tempunit = unitSelector->getUnit();
+ if (tempunit->type != Inkscape::Util::UNIT_TYPE_LINEAR) {
+ unitSelector->setUnit(desktop->getNamedView()->display_units->abbr);
+ }
+ }
+
+ Inkscape::Util::Unit const *unit = unitSelector->getUnit();
+
+ if (query.stroke_extensions.hairline) {
+ widthSpin->set_sensitive(false);
+ (*widthAdj)->set_value(1);
+ } else if (unit->type == Inkscape::Util::UNIT_TYPE_LINEAR) {
+ double avgwidth = Inkscape::Util::Quantity::convert(query.stroke_width.computed, "px", unit);
+ (*widthAdj)->set_value(avgwidth);
+ } else {
+ (*widthAdj)->set_value(100);
+ }
+
+ // if none of the selected objects has a stroke, than quite some controls should be disabled
+ // These options should also be disabled for hairlines, since they don't make sense for
+ // 0-width lines.
+ // The markers might still be shown though, so marker and stroke-width widgets stay enabled
+ bool is_enabled = (result_sw != QUERY_STYLE_NOTHING) && !targPaint.isNoneSet()
+ && !query.stroke_extensions.hairline;
+ joinMiter->set_sensitive(is_enabled);
+ joinRound->set_sensitive(is_enabled);
+ joinBevel->set_sensitive(is_enabled);
+
+ miterLimitSpin->set_sensitive(is_enabled);
+
+ capButt->set_sensitive(is_enabled);
+ capRound->set_sensitive(is_enabled);
+ capSquare->set_sensitive(is_enabled);
+
+ dashSelector->set_sensitive(is_enabled);
+ _pattern->set_sensitive(is_enabled);
+ }
+
+ if (result_ml != QUERY_STYLE_NOTHING)
+ (*miterLimitAdj)->set_value(query.stroke_miterlimit.value); // TODO: reflect averagedness?
+
+ using Inkscape::is_query_style_updateable;
+ if (! is_query_style_updateable(result_join)) {
+ setJoinType(query.stroke_linejoin.value);
+ } else {
+ setJoinButtons(nullptr);
+ }
+
+ if (! is_query_style_updateable(result_cap)) {
+ setCapType (query.stroke_linecap.value);
+ } else {
+ setCapButtons(nullptr);
+ }
+
+ if (! is_query_style_updateable(result_order)) {
+ setPaintOrder (query.paint_order.value);
+ } else {
+ setPaintOrder (nullptr);
+ }
+
+ std::vector<SPItem*> const objects(sel->items().begin(), sel->items().end());
+ if (objects.size()) {
+ SPObject *const object = objects[0];
+ SPStyle *const style = object->style;
+ /* Markers */
+ updateAllMarkers(objects, true); // FIXME: make this desktop query too
+
+ /* Dash */
+ setDashSelectorFromStyle(dashSelector, style); // FIXME: make this desktop query too
+ }
+ table->set_sensitive(true);
+
+ update = false;
+}
+
+/**
+ * Sets a line's dash properties in a CSS style object.
+ */
+void
+StrokeStyle::setScaledDash(SPCSSAttr *css,
+ int ndash, const double *dash, double offset,
+ double scale)
+{
+ if (ndash > 0) {
+ Inkscape::CSSOStringStream osarray;
+ for (int i = 0; i < ndash; i++) {
+ osarray << dash[i] * scale;
+ if (i < (ndash - 1)) {
+ osarray << ",";
+ }
+ }
+ sp_repr_css_set_property(css, "stroke-dasharray", osarray.str().c_str());
+
+ Inkscape::CSSOStringStream osoffset;
+ osoffset << offset * scale;
+ sp_repr_css_set_property(css, "stroke-dashoffset", osoffset.str().c_str());
+ } else {
+ sp_repr_css_set_property(css, "stroke-dasharray", "none");
+ sp_repr_css_set_property(css, "stroke-dashoffset", nullptr);
+ }
+}
+
+static inline double calcScaleLineWidth(const double width_typed, SPItem *const item, Inkscape::Util::Unit const *const unit)
+{
+ if (unit->abbr == "%") {
+ auto scale = item->i2doc_affine().descrim();;
+ const gdouble old_w = item->style->stroke_width.computed;
+ return (old_w * width_typed / 100) * scale;
+ } else if (unit->type == Inkscape::Util::UNIT_TYPE_LINEAR) {
+ return Inkscape::Util::Quantity::convert(width_typed, unit, "px");
+ }
+ return width_typed;
+}
+
+/**
+ * Set the stroke width and adjust the dash pattern if needed.
+ */
+void StrokeStyle::setStrokeWidth()
+{
+ double width_typed = (*widthAdj)->get_value();
+
+ // Don't change the selection if an update is happening,
+ // but also store the value for later comparison.
+ if (update || fabs(_last_width - width_typed) < 1E-6) {
+ _last_width = width_typed;
+ return;
+ }
+ update = true;
+
+ auto prefs = Inkscape::Preferences::get();
+ auto unit = unitSelector->getUnit();
+
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ if (isHairlineSelected()) {
+ /* For renderers that don't understand -inkscape-stroke:hairline, fall back to 1px non-scaling */
+ width_typed = 1;
+ sp_repr_css_set_property(css, "vector-effect", "non-scaling-stroke");
+ sp_repr_css_set_property(css, "-inkscape-stroke", "hairline");
+ } else {
+ sp_repr_css_unset_property(css, "vector-effect");
+ sp_repr_css_unset_property(css, "-inkscape-stroke");
+ }
+
+ for (auto item : desktop->getSelection()->items()) {
+ const double width = calcScaleLineWidth(width_typed, item, unit);
+ sp_repr_css_set_property_double(css, "stroke-width", width);
+
+ if (prefs->getBool("/options/dash/scale", true)) {
+ // This will read the old stroke-width to un-scale the pattern.
+ double offset = 0;
+ auto dash = getDashFromStyle(item->style, offset);
+ setScaledDash(css, dash.size(), dash.data(), offset, width);
+ }
+ sp_desktop_apply_css_recursive (item, css, true);
+ }
+ sp_desktop_set_style (desktop, css, false);
+
+ sp_repr_css_attr_unref(css);
+ DocumentUndo::done(desktop->getDocument(), _("Set stroke width"),
+ INKSCAPE_ICON("dialog-fill-and-stroke"));
+
+ if (unit->abbr == "%") {
+ // reset to 100 percent
+ _last_width = 100.0;
+ (*widthAdj)->set_value(100.0);
+ } else {
+ _last_width = width_typed;
+ }
+ update = false;
+}
+
+/**
+ * Set the stroke dash pattern, scale to the existing width if needed
+ */
+void StrokeStyle::setStrokeDash()
+{
+ if (update) return;
+ update = true;
+
+ auto document = desktop->getDocument();
+ auto prefs = Inkscape::Preferences::get();
+
+ double offset = 0;
+ const auto& dash = dashSelector->get_dash(&offset);
+ update_pattern(dash.size(), dash.data());
+
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ for (auto item : desktop->getSelection()->items()) {
+ double scale = item->i2doc_affine().descrim();
+ if(prefs->getBool("/options/dash/scale", true)) {
+ scale = item->style->stroke_width.computed * scale;
+ }
+
+ setScaledDash(css, dash.size(), dash.data(), offset, scale);
+ sp_desktop_apply_css_recursive (item, css, true);
+ }
+ sp_desktop_set_style (desktop, css, false);
+
+ sp_repr_css_attr_unref(css);
+ DocumentUndo::done(document, _("Set stroke dash"),
+ INKSCAPE_ICON("dialog-fill-and-stroke"));
+ update = false;
+}
+
+/**
+ * Set the Miter Limit value only.
+ */
+void StrokeStyle::setStrokeMiter()
+{
+ if (update) return;
+ update = true;
+
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ auto value = (*miterLimitAdj)->get_value();
+ sp_repr_css_set_property_double(css, "stroke-miterlimit", value);
+
+ for (auto item : desktop->getSelection()->items()) {
+ sp_desktop_apply_css_recursive(item, css, true);
+ }
+ sp_desktop_set_style (desktop, css, false);
+ sp_repr_css_attr_unref(css);
+ DocumentUndo::done(desktop->getDocument(), _("Set stroke miter"),
+ INKSCAPE_ICON("dialog-fill-and-stroke"));
+ update = false;
+}
+
+/**
+ * Returns whether the currently selected stroke width is "hairline"
+ *
+ */
+bool
+StrokeStyle::isHairlineSelected() const
+{
+ return unitSelector->get_active_id() == "hairline";
+}
+
+
+/**
+ * This routine handles toggle events for buttons in the stroke style dialog.
+ *
+ * When activated, this routine gets the data for the various widgets, and then
+ * calls the respective routines to update css properties, etc.
+ *
+ */
+void StrokeStyle::buttonToggledCB(StrokeStyleButton *tb, StrokeStyle *spw)
+{
+ if (spw->update) {
+ return;
+ }
+
+ if (tb->get_active()) {
+ if (tb->get_button_type() == STROKE_STYLE_BUTTON_JOIN) {
+ spw->miterLimitSpin->set_sensitive(!strcmp(tb->get_stroke_style(), "miter"));
+ }
+
+ /* TODO: Create some standardized method */
+ SPCSSAttr *css = sp_repr_css_attr_new();
+
+ switch (tb->get_button_type()) {
+ case STROKE_STYLE_BUTTON_JOIN:
+ sp_repr_css_set_property(css, "stroke-linejoin", tb->get_stroke_style());
+ sp_desktop_set_style (spw->desktop, css);
+ spw->setJoinButtons(tb);
+ break;
+ case STROKE_STYLE_BUTTON_CAP:
+ sp_repr_css_set_property(css, "stroke-linecap", tb->get_stroke_style());
+ sp_desktop_set_style (spw->desktop, css);
+ spw->setCapButtons(tb);
+ break;
+ case STROKE_STYLE_BUTTON_ORDER:
+ sp_repr_css_set_property(css, "paint-order", tb->get_stroke_style());
+ sp_desktop_set_style (spw->desktop, css);
+ //spw->setPaintButtons(tb);
+ }
+
+ sp_repr_css_attr_unref(css);
+ css = nullptr;
+
+ DocumentUndo::done(spw->desktop->getDocument(), _("Set stroke style"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ }
+}
+
+/**
+ * Updates the join style toggle buttons
+ */
+void
+StrokeStyle::setJoinButtons(Gtk::ToggleButton *active)
+{
+ joinMiter->set_active(active == joinMiter);
+ miterLimitSpin->set_sensitive(active == joinMiter && !isHairlineSelected());
+ joinRound->set_active(active == joinRound);
+ joinBevel->set_active(active == joinBevel);
+}
+
+/**
+ * Updates the cap style toggle buttons
+ */
+void
+StrokeStyle::setCapButtons(Gtk::ToggleButton *active)
+{
+ capButt->set_active(active == capButt);
+ capRound->set_active(active == capRound);
+ capSquare->set_active(active == capSquare);
+}
+
+
+/**
+ * Updates the paint order style toggle buttons
+ */
+void
+StrokeStyle::setPaintOrderButtons(Gtk::ToggleButton *active)
+{
+ paintOrderFSM->set_active(active == paintOrderFSM);
+ paintOrderSFM->set_active(active == paintOrderSFM);
+ paintOrderFMS->set_active(active == paintOrderFMS);
+ paintOrderMFS->set_active(active == paintOrderMFS);
+ paintOrderSMF->set_active(active == paintOrderSMF);
+ paintOrderMSF->set_active(active == paintOrderMSF);
+}
+
+
+/**
+ * Recursively builds a simple list from an arbitrarily complex selection
+ * of items and grouped items
+ */
+static void buildGroupedItemList(SPObject *element, std::vector<SPObject*> &simple_list)
+{
+ if (is<SPGroup>(element)) {
+ for (SPObject *i = element->firstChild(); i; i = i->getNext()) {
+ buildGroupedItemList(i, simple_list);
+ }
+ } else {
+ simple_list.push_back(element);
+ }
+}
+
+
+/**
+ * Updates the marker combobox to highlight the appropriate marker and scroll to
+ * that marker.
+ */
+void
+StrokeStyle::updateAllMarkers(std::vector<SPItem*> const &objects, bool skip_undo)
+{
+ struct { MarkerComboBox *key; int loc; } const keyloc[] = {
+ { startMarkerCombo, SP_MARKER_LOC_START },
+ { midMarkerCombo, SP_MARKER_LOC_MID },
+ { endMarkerCombo, SP_MARKER_LOC_END }
+ };
+
+ bool all_texts = true;
+
+ auto simplified_list = std::vector<SPObject *>();
+ for (SPItem *item : objects) {
+ buildGroupedItemList(item, simplified_list);
+ }
+
+ for (SPObject *object : simplified_list) {
+ if (!is<SPText>(object)) {
+ all_texts = false;
+ break;
+ }
+ }
+
+ // We show markers of the last object in the list only
+ // FIXME: use the first in the list that has the marker of each type, if any
+
+ for (auto const &markertype : keyloc) {
+ // For all three marker types,
+
+ // find the corresponding combobox item
+ MarkerComboBox *combo = markertype.key;
+
+ // Quit if we're in update state
+ if (combo->in_update()) {
+ return;
+ }
+
+ // Per SVG spec, text objects cannot have markers; disable combobox if only texts are selected
+ // They should also be disabled for hairlines, since scaling against a 0-width line doesn't
+ // make sense.
+ combo->set_sensitive(!all_texts && !isHairlineSelected());
+
+ SPObject *marker = nullptr;
+
+ if (!all_texts && !isHairlineSelected()) {
+ for (SPObject *object : simplified_list) {
+ char const *value = object->style->marker_ptrs[markertype.loc]->value();
+
+ // If the object has this type of markers,
+ if (value == nullptr)
+ continue;
+
+ // Extract the name of the marker that the object uses
+ marker = getMarkerObj(value, object->document);
+ }
+ }
+
+ // Scroll the combobox to that marker
+ combo->set_current(marker);
+ }
+
+}
+
+} // 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/stroke-style.h b/src/ui/widget/stroke-style.h
new file mode 100644
index 0000000..0cc29d5
--- /dev/null
+++ b/src/ui/widget/stroke-style.h
@@ -0,0 +1,213 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Widgets used in the stroke style dialog.
+ */
+/* Author:
+ * Lauris Kaplinski <lauris@ximian.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2010 Jon A. Cruz
+ * Copyright (C) 2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+// WHOA! talk about header bloat!
+
+#ifndef SEEN_DIALOGS_STROKE_STYLE_H
+#define SEEN_DIALOGS_STROKE_STYLE_H
+
+#include <glibmm/i18n.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/radiobutton.h>
+
+
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "fill-style.h" // to get sp_fill_style_widget_set_desktop
+#include "gradient-chemistry.h"
+#include "inkscape.h"
+#include "path-prefix.h"
+#include "preferences.h"
+#include "selection.h"
+#include "style.h"
+
+#include "display/drawing.h"
+
+#include "helper/stock-items.h"
+
+#include "io/sys.h"
+
+#include "svg/css-ostringstream.h"
+
+#include "ui/cache/svg_preview_cache.h"
+#include "ui/dialog-events.h"
+#include "ui/icon-names.h"
+#include "ui/widget/spinbutton.h"
+
+#include "widgets/spw-utilities.h"
+
+
+namespace Gtk {
+class Widget;
+class Container;
+}
+
+namespace Inkscape {
+ namespace Util {
+ class Unit;
+ }
+ namespace UI {
+ namespace Widget {
+ class DashSelector;
+ class MarkerComboBox;
+ class UnitMenu;
+ }
+ }
+}
+
+struct { gchar const *key; gint value; } const SPMarkerNames[] = {
+ {"marker-all", SP_MARKER_LOC},
+ {"marker-start", SP_MARKER_LOC_START},
+ {"marker-mid", SP_MARKER_LOC_MID},
+ {"marker-end", SP_MARKER_LOC_END},
+ {"", SP_MARKER_LOC_QTY},
+ {nullptr, -1}
+};
+
+
+SPObject *getMarkerObj(gchar const *n, SPDocument *doc);
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+class StrokeStyleButton;
+
+class StrokeStyle : public Gtk::Box
+{
+public:
+ StrokeStyle();
+ ~StrokeStyle() override;
+ void setDesktop(SPDesktop *desktop);
+ void updateLine();
+ void selectionModifiedCB(guint flags);
+ void selectionChangedCB();
+private:
+ /** List of valid types for the stroke-style radio-button widget */
+ enum StrokeStyleButtonType {
+ STROKE_STYLE_BUTTON_JOIN, ///< A button to set the line-join style
+ STROKE_STYLE_BUTTON_CAP, ///< A button to set the line-cap style
+ STROKE_STYLE_BUTTON_ORDER ///< A button to set the paint-order style
+ };
+
+ /**
+ * A custom radio-button for setting the stroke style. It can be configured
+ * to set either the join or cap style by setting the button_type field.
+ */
+ class StrokeStyleButton : public Gtk::RadioButton {
+ public:
+ StrokeStyleButton(Gtk::RadioButtonGroup &grp,
+ char const *icon,
+ StrokeStyleButtonType button_type,
+ gchar const *stroke_style);
+
+ /** Get the type (line/cap) of the stroke-style button */
+ inline StrokeStyleButtonType get_button_type() {return button_type;}
+
+ /** Get the stroke style attribute associated with the button */
+ inline gchar const * get_stroke_style() {return stroke_style;}
+
+ private:
+ StrokeStyleButtonType button_type; ///< The type (line/cap) of the button
+ gchar const *stroke_style; ///< The stroke style associated with the button
+ };
+
+ std::vector<double> getDashFromStyle(SPStyle *style, double &offset);
+
+ void updateAllMarkers(std::vector<SPItem*> const &objects, bool skip_undo = false);
+ void setDashSelectorFromStyle(Inkscape::UI::Widget::DashSelector *dsel, SPStyle *style);
+ void setJoinType (unsigned const jointype);
+ void setCapType (unsigned const captype);
+ void setPaintOrder (gchar const *paint_order);
+ void setJoinButtons(Gtk::ToggleButton *active);
+ void setCapButtons(Gtk::ToggleButton *active);
+ void setPaintOrderButtons(Gtk::ToggleButton *active);
+ void setStrokeWidth();
+ void setStrokeDash();
+ void setStrokeMiter();
+ void setScaledDash(SPCSSAttr *css, int ndash, const double *dash, double offset, double scale);
+ bool isHairlineSelected() const;
+
+ StrokeStyleButton * makeRadioButton(Gtk::RadioButtonGroup &grp,
+ char const *icon,
+ Gtk::Box *hb,
+ StrokeStyleButtonType button_type,
+ gchar const *stroke_style);
+
+ // Callback functions
+ void unitChangedCB();
+ bool areMarkersBeingUpdated();
+ void markerSelectCB(MarkerComboBox *marker_combo, SPMarkerLoc const which);
+ static void buttonToggledCB(StrokeStyleButton *tb, StrokeStyle *spw);
+
+
+ MarkerComboBox *startMarkerCombo;
+ MarkerComboBox *midMarkerCombo;
+ MarkerComboBox *endMarkerCombo;
+ Gtk::Grid *table;
+ Glib::RefPtr<Gtk::Adjustment> *widthAdj;
+ Glib::RefPtr<Gtk::Adjustment> *miterLimitAdj;
+ Inkscape::UI::Widget::SpinButton *miterLimitSpin;
+ Inkscape::UI::Widget::SpinButton *widthSpin;
+ Inkscape::UI::Widget::UnitMenu *unitSelector;
+ //Gtk::ToggleButton *hairline;
+ StrokeStyleButton *joinMiter;
+ StrokeStyleButton *joinRound;
+ StrokeStyleButton *joinBevel;
+ StrokeStyleButton *capButt;
+ StrokeStyleButton *capRound;
+ StrokeStyleButton *capSquare;
+ StrokeStyleButton *paintOrderFSM;
+ StrokeStyleButton *paintOrderSFM;
+ StrokeStyleButton *paintOrderFMS;
+ StrokeStyleButton *paintOrderMFS;
+ StrokeStyleButton *paintOrderSMF;
+ StrokeStyleButton *paintOrderMSF;
+ Inkscape::UI::Widget::DashSelector *dashSelector;
+ Gtk::Entry* _pattern = nullptr;
+ Gtk::Label* _pattern_label = nullptr;
+ void update_pattern(int ndash, const double* pattern);
+ bool _editing_pattern = false;
+
+ gboolean update;
+ double _last_width = 0.0;
+ SPDesktop *desktop;
+ sigc::connection startMarkerConn;
+ sigc::connection midMarkerConn;
+ sigc::connection endMarkerConn;
+
+ Inkscape::Util::Unit const *_old_unit;
+
+ void _handleDocumentReplaced(SPDesktop *, SPDocument *);
+ void enterEditMarkerMode(SPMarkerLoc editMarkerMode);
+ sigc::connection _document_replaced_connection;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // SEEN_DIALOGS_STROKE_STYLE_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..110f6ff
--- /dev/null
+++ b/src/ui/widget/style-subject.cpp
@@ -0,0 +1,113 @@
+// 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 "layer-manager.h"
+#include "selection.h"
+
+#include "xml/sp-css-attr.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+StyleSubject::StyleSubject() {
+}
+
+StyleSubject::~StyleSubject() {
+ setDesktop(nullptr);
+}
+
+void StyleSubject::setDesktop(SPDesktop *desktop) {
+ if (desktop != _desktop) {
+ _desktop = desktop;
+ _afterDesktopSwitch(desktop);
+ if (_desktop) {
+ _emitChanged(); // This updates the widgets.
+ }
+ }
+}
+
+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);
+ }
+}
+
+}
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ 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..ad5b3ff
--- /dev/null
+++ b/src/ui/widget/style-subject.h
@@ -0,0 +1,114 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Abstraction for different style widget operands. Used by ObjectCompositeSettings in Layers and
+ * Fill and Stroke dialogs. Dialog is responsible for keeping desktop pointer valid.
+ *
+ * This class is due to the need to differentiate between layers and objects but a layer is just a
+ * a group object with an extra tag. There should be no need to differentiate between the two.
+ * To do: remove this class and intergrate the functionality into ObjectCompositeSettings.
+ */
+/*
+ * 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 <optional>
+#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 = nullptr;
+};
+
+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;
+};
+
+}
+}
+}
+
+#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..05d149e
--- /dev/null
+++ b/src/ui/widget/style-swatch.cpp
@@ -0,0 +1,405 @@
+// 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/enums.h>
+#include <gtkmm/grid.h>
+
+#include "inkscape.h"
+#include "style.h"
+
+#include "actions/actions-tools.h" // Open tool preferences.
+
+#include "object/sp-linear-gradient.h"
+#include "object/sp-pattern.h"
+#include "object/sp-radial-gradient.h"
+
+#include "ui/widget/color-preview.h"
+#include "util/units.h"
+
+#include "widgets/spw-utilities.h"
+
+#include "xml/sp-css-attr.h"
+#include "xml/attribute-record.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");
+ const auto & al = css->attributeList();
+ if (al.empty()) {
+ 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, Gtk::Orientation orient)
+ : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL),
+ _desktop(nullptr),
+ _css(nullptr),
+ _tool_obs(nullptr),
+ _style_obs(nullptr),
+ _table(Gtk::manage(new Gtk::Grid())),
+ _sw_unit(nullptr),
+ _stroke(Gtk::ORIENTATION_HORIZONTAL)
+{
+ 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);
+
+ if (orient == Gtk::ORIENTATION_VERTICAL) {
+ _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(_empty_space, 2, 0, 1, 2);
+ _table->attach(_opacity_place, 2, 0, 1, 2);
+ _swatch.add(*_table);
+ pack_start(_swatch, true, true, 0);
+
+ set_size_request (STYLE_SWATCH_WIDTH, -1);
+ }
+ else {
+ _table->set_column_spacing(4);
+ _table->attach(_label[SS_FILL], 0, 0, 1, 1);
+ _table->attach(_place[SS_FILL], 1, 0, 1, 1);
+ _label[SS_STROKE].set_margin_start(6);
+ _table->attach(_label[SS_STROKE], 2, 0, 1, 1);
+ _table->attach(_stroke, 3, 0, 1, 1);
+ _opacity_place.set_margin_start(6);
+ _table->attach(_opacity_place, 4, 0, 1, 1);
+ _swatch.add(*_table);
+ pack_start(_swatch, true, true, 0);
+
+ int patch_w = 6 * 6;
+ _place[SS_FILL].set_size_request(patch_w, -1);
+ _place[SS_STROKE].set_size_request(patch_w, -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::setToolName(const Glib::ustring& tool_name) {
+ _tool_name = tool_name;
+}
+
+void StyleSwatch::setDesktop(SPDesktop *desktop) {
+ _desktop = desktop;
+}
+
+bool
+StyleSwatch::on_click(GdkEventButton */*event*/)
+{
+ if (_desktop && !_tool_name.empty()) {
+ auto win = _desktop->getInkscapeWindow();
+ open_tool_preferences(win, _tool_name);
+ 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(_desktop ? _desktop->getDocument() : nullptr);
+ 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 (is<SPLinearGradient>(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 (is<SPRadialGradient>(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 (is<SPPattern>(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) {
+ if (query->stroke_extensions.hairline) {
+ Glib::ustring swidth = "<small>";
+ swidth += _("Hairline");
+ swidth += "</small>";
+ _stroke_width.set_markup(swidth.c_str());
+ auto str = Glib::ustring::compose(_("Stroke width: %1"), _("Hairline"));
+ _stroke_width_place.set_tooltip_text(str);
+ } else {
+ 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);
+ Glib::ustring swidth = "<small>";
+ swidth += str;
+ swidth += "</small>";
+ _stroke_width.set_markup(swidth.c_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));
+ Glib::ustring opacity = "<small>";
+ opacity += str;
+ opacity += "</small>";
+ _opacity_value.set_markup (opacity.c_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..514cd6d
--- /dev/null
+++ b/src/ui/widget/style-swatch.h
@@ -0,0 +1,107 @@
+// 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"
+
+constexpr int STYLE_SWATCH_WIDTH = 135;
+
+class SPStyle;
+class SPCSSAttr;
+
+namespace Gtk {
+class Grid;
+}
+
+namespace Inkscape {
+
+namespace Util {
+ class Unit;
+}
+
+namespace UI {
+namespace Widget {
+
+class StyleSwatch : public Gtk::Box
+{
+public:
+ StyleSwatch (SPCSSAttr *attr, gchar const *main_tip, Gtk::Orientation orient = Gtk::ORIENTATION_VERTICAL);
+
+ ~StyleSwatch() override;
+
+ void setStyle(SPStyle *style);
+ void setStyle(SPCSSAttr *attr);
+ SPCSSAttr *getStyle();
+
+ void setWatchedTool (const char *path, bool synthesize);
+ void setToolName(const Glib::ustring& tool_name);
+ void setDesktop(SPDesktop *desktop);
+ bool on_click(GdkEventButton *event);
+
+private:
+ class ToolObserver;
+ class StyleObserver;
+
+ SPDesktop *_desktop;
+ Glib::ustring _tool_name;
+ SPCSSAttr *_css;
+ ToolObserver *_tool_obs;
+ StyleObserver *_style_obs;
+ Glib::ustring _tool_path;
+
+ Gtk::EventBox _swatch;
+
+ Gtk::Grid *_table;
+
+ Gtk::Label _label[2];
+ Gtk::Box _empty_space;
+ 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::Box _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/swatch-selector.cpp b/src/ui/widget/swatch-selector.cpp
new file mode 100644
index 0000000..54376fc
--- /dev/null
+++ b/src/ui/widget/swatch-selector.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 "swatch-selector.h"
+
+#include <glibmm/i18n.h>
+
+#include "document-undo.h"
+#include "document.h"
+#include "gradient-chemistry.h"
+
+#include "object/sp-stop.h"
+
+#include "ui/icon-names.h"
+#include "ui/widget/color-notebook.h"
+#include "ui/widget/gradient-selector.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+SwatchSelector::SwatchSelector()
+ : Gtk::Box(Gtk::ORIENTATION_VERTICAL)
+{
+ using Inkscape::UI::Widget::ColorNotebook;
+
+ _gsel = Gtk::make_managed<GradientSelector>();
+ _gsel->setMode(GradientSelector::MODE_SWATCH);
+
+ _gsel->show();
+
+ pack_start(*_gsel);
+
+ auto color_selector = Gtk::make_managed<ColorNotebook>(_selected_color);
+ color_selector->set_label(_("Swatch color"));
+ color_selector->show();
+ pack_start(*color_selector);
+
+ _selected_color.signal_dragged.connect(sigc::mem_fun(*this, &SwatchSelector::_changedCb));
+ _selected_color.signal_released.connect(sigc::mem_fun(*this, &SwatchSelector::_changedCb));
+ // signal_changed doesn't get called if updating shape with colour.
+ _selected_color.signal_changed.connect(sigc::mem_fun(*this, &SwatchSelector::_changedCb));
+}
+
+void SwatchSelector::_changedCb()
+{
+ if (_updating_color) {
+ return;
+ }
+ // TODO might have to block cycles
+
+ if (_gsel && _gsel->getVector()) {
+ SPGradient *gradient = _gsel->getVector();
+ SPGradient *ngr = sp_gradient_ensure_vector_normalized(gradient);
+ if (ngr != gradient) {
+ /* Our master gradient has changed */
+ // TODO replace with proper - sp_gradient_vector_widget_load_gradient(GTK_WIDGET(swsel->_gsel), ngr);
+ }
+
+ ngr->ensureVector();
+
+ if (auto stop = ngr->getFirstStop()) {
+ stop->setColor(_selected_color.color(), _selected_color.alpha());
+ DocumentUndo::done(ngr->document, _("Change swatch color"), INKSCAPE_ICON("color-gradient"));
+ }
+ }
+}
+
+void SwatchSelector::setVector(SPDocument */*doc*/, SPGradient *vector)
+{
+ _gsel->setVector(vector ? vector->document : nullptr, vector);
+
+ if (vector && vector->isSolid()) {
+ _updating_color = true;
+ auto stop = vector->getFirstStop();
+ _selected_color.setColorAlpha(stop->getColor(), stop->getOpacity(), true);
+ _updating_color = 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/swatch-selector.h b/src/ui/widget/swatch-selector.h
new file mode 100644
index 0000000..c67b013
--- /dev/null
+++ b/src/ui/widget/swatch-selector.h
@@ -0,0 +1,58 @@
+// 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_SWATCH_SELECTOR_H
+#define SEEN_SP_SWATCH_SELECTOR_H
+
+#include <gtkmm/box.h>
+#include "ui/selected-color.h"
+
+class SPDocument;
+class SPGradient;
+struct SPColorSelector;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+class GradientSelector;
+
+class SwatchSelector : public Gtk::Box
+{
+public:
+ SwatchSelector();
+
+ void setVector(SPDocument *doc, SPGradient *vector);
+
+ GradientSelector *getGradientSelector() { return _gsel; }
+
+private:
+ void _changedCb();
+
+ GradientSelector *_gsel = nullptr;
+ Inkscape::UI::SelectedColor _selected_color;
+ bool _updating_color = false;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // SEEN_SP_SWATCH_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/template-list.cpp b/src/ui/widget/template-list.cpp
new file mode 100644
index 0000000..0538439
--- /dev/null
+++ b/src/ui/widget/template-list.cpp
@@ -0,0 +1,224 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Martin Owens <doctormo@geek-2.com>
+ *
+ * Copyright (C) 2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "template-list.h"
+
+#include <glibmm/i18n.h>
+
+#include "extension/db.h"
+#include "extension/template.h"
+#include "inkscape-application.h"
+#include "io/resource.h"
+#include "ui/util.h"
+#include "ui/icon-loader.h"
+#include "ui/svg-renderer.h"
+
+using namespace Inkscape::IO::Resource;
+using Inkscape::Extension::TemplatePreset;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class TemplateCols : public Gtk::TreeModel::ColumnRecord
+{
+public:
+ // These types must match those for the model in the .glade file
+ TemplateCols()
+ {
+ this->add(this->name);
+ this->add(this->label);
+ this->add(this->icon);
+ this->add(this->key);
+ }
+ Gtk::TreeModelColumn<Glib::ustring> name;
+ Gtk::TreeModelColumn<Glib::ustring> label;
+ Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf>> icon;
+ Gtk::TreeModelColumn<Glib::ustring> key;
+};
+
+TemplateList::TemplateList() {}
+
+TemplateList::TemplateList(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade)
+ : Gtk::Notebook(cobject)
+{
+ TemplateList();
+}
+
+/**
+ * Initialise this template list with categories and icons
+ */
+void TemplateList::init(Inkscape::Extension::TemplateShow mode)
+{
+ TemplateCols cols;
+ std::map<std::string, Glib::RefPtr<Gtk::ListStore>> _stores;
+
+ Inkscape::Extension::DB::TemplateList extensions;
+ Inkscape::Extension::db.get_template_list(extensions);
+
+ for (auto tmod : extensions) {
+ std::string cat = tmod->get_category();
+ if (!_stores.count(cat)) {
+ try {
+ _stores[cat] = this->generate_category(cat);
+ _stores[cat]->clear();
+ } catch (UIBuilderError &e) {
+ return;
+ }
+ }
+ for (auto preset : tmod->get_presets(mode)) {
+ Gtk::TreeModel::Row row = *(_stores[cat]->append());
+ auto name = preset->get_name();
+ row[cols.name] = name.empty() ? "" : _(name.c_str());
+ row[cols.icon] = icon_to_pixbuf(preset->get_icon_path());
+ auto label = preset->get_label();
+ row[cols.label] = label.empty() ? "" : _(label.c_str());
+ row[cols.key] = preset->get_key();
+ }
+ }
+
+ reset_selection();
+}
+
+/**
+ * Turn the requested template icon name into a pixbuf
+ */
+Glib::RefPtr<Gdk::Pixbuf> TemplateList::icon_to_pixbuf(std::string path)
+{
+ // TODO: Add some caching here.
+ if (!path.empty()) {
+ Inkscape::svg_renderer renderer(path.c_str());
+ return renderer.render(1.0);
+ }
+ Glib::RefPtr<Gdk::Pixbuf> no_image;
+ return no_image;
+}
+
+/**
+ * Generate a new category with the given label and return it's list store.
+ */
+Glib::RefPtr<Gtk::ListStore> TemplateList::generate_category(std::string label)
+{
+ static Glib::ustring uifile = get_filename(UIS, "widget-new-from-template.ui");
+
+ Glib::RefPtr<Gtk::Builder> builder;
+ try {
+ builder = Gtk::Builder::create_from_file(uifile);
+ } catch (const Glib::Error &ex) {
+ g_error("UI file loading failed for template list widget: %s", ex.what().c_str());
+ throw UIFileUnavailable();
+ }
+
+ Gtk::Widget *container = nullptr;
+ Gtk::IconView *icons = nullptr;
+ builder->get_widget("container", container);
+ builder->get_widget("iconview", icons);
+
+ if (!icons || !container) {
+ throw WidgetUnavailable();
+ }
+
+ // This packing keeps the Gtk widget alive, beyond the builder's lifetime
+ this->append_page(*container, g_dpgettext2(nullptr, "TemplateCategory", label.c_str()));
+
+ icons->signal_selection_changed().connect([=]() { _item_selected_signal.emit(); });
+ icons->signal_item_activated().connect([=](const Gtk::TreeModel::Path) { _item_activated_signal.emit(); });
+
+ return Glib::RefPtr<Gtk::ListStore>::cast_dynamic(icons->get_model());
+}
+
+/**
+ * Returns true if the template list has a visible, selected preset.
+ */
+bool TemplateList::has_selected_preset()
+{
+ return (bool)get_selected_preset();
+}
+
+/**
+ * Returns the selected template preset, if one is not selected returns nullptr.
+ */
+std::shared_ptr<TemplatePreset> TemplateList::get_selected_preset()
+{
+ TemplateCols cols;
+ if (auto iconview = get_iconview(get_nth_page(get_current_page()))) {
+ auto items = iconview->get_selected_items();
+ if (!items.empty()) {
+ auto iter = iconview->get_model()->get_iter(items[0]);
+ if (Gtk::TreeModel::Row row = *iter) {
+ Glib::ustring key = row[cols.key];
+ return Extension::Template::get_any_preset(key);
+ }
+ }
+ }
+ return nullptr;
+}
+
+/**
+ * Create a new document based on the selected item and return.
+ */
+SPDocument *TemplateList::new_document()
+{
+ auto app = InkscapeApplication::instance();
+ if (auto preset = get_selected_preset()) {
+ if (auto doc = preset->new_from_template()) {
+ // TODO: Add memory to remember this preset for next time.
+ app->document_add(doc);
+ return doc;
+ } else {
+ // Cancel pressed in options box.
+ return nullptr;
+ }
+ }
+ // Fallback to the default template (already added)!
+ return app->document_new();
+}
+
+/**
+ * Reset the selection, forcing the use of the default template.
+ */
+void TemplateList::reset_selection()
+{
+ // TODO: Add memory here for the new document default (see new_document).
+ for (auto widget : get_children()) {
+ if (auto iconview = get_iconview(widget)) {
+ iconview->unselect_all();
+ }
+ }
+}
+
+/**
+ * Returns the internal iconview for the given widget.
+ */
+Gtk::IconView *TemplateList::get_iconview(Gtk::Widget *widget)
+{
+ if (auto container = dynamic_cast<Gtk::Container *>(widget)) {
+ for (auto child : container->get_children()) {
+ if (auto iconview = get_iconview(child)) {
+ return iconview;
+ }
+ }
+ }
+ return dynamic_cast<Gtk::IconView *>(widget);
+}
+
+} // 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/template-list.h b/src/ui/widget/template-list.h
new file mode 100644
index 0000000..7b7d425
--- /dev/null
+++ b/src/ui/widget/template-list.h
@@ -0,0 +1,62 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Martin Owens <doctormo@geek-2.com>
+ *
+ * Copyright (C) 2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef WIDGET_TEMPLATE_LIST_H
+#define WIDGET_TEMPLATE_LIST_H
+
+#include <gtkmm.h>
+#include "extension/template.h"
+
+class SPDocument;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class TemplateList : public Gtk::Notebook
+{
+public:
+ TemplateList();
+ TemplateList(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade);
+ ~TemplateList() override{};
+
+ void init(Extension::TemplateShow mode);
+ void reset_selection();
+ bool has_selected_preset();
+ std::shared_ptr<Extension::TemplatePreset> get_selected_preset();
+ SPDocument *new_document();
+
+ sigc::connection connectItemSelected(const sigc::slot<void ()> &slot) { return _item_selected_signal.connect(slot); }
+ sigc::connection connectItemActivated(const sigc::slot<void ()> &slot) { return _item_activated_signal.connect(slot); }
+
+private:
+ Glib::RefPtr<Gtk::ListStore> generate_category(std::string label);
+ Glib::RefPtr<Gdk::Pixbuf> icon_to_pixbuf(std::string name);
+ Gtk::IconView *get_iconview(Gtk::Widget *widget);
+ std::shared_ptr<Extension::TemplatePreset> get_preset(std::string key);
+
+ sigc::signal<void ()> _item_selected_signal;
+ sigc::signal<void ()> _item_activated_signal;
+};
+
+} // 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/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..2d1463d
--- /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::Box(Gtk::ORIENTATION_VERTICAL);
+ _hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
+
+ 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 = _wr->desktop();
+ if (!dt)
+ return;
+
+ Inkscape::SVGOStringStream os;
+ os << val;
+
+ _wr->setUpdating (true);
+
+ SPDocument *doc = dt->getDocument();
+ {
+ DocumentUndo::ScopedInsensitive _no_undo(doc);
+ Inkscape::XML::Node *repr = dt->getNamedView()->getRepr();
+ repr->setAttribute(_key, os.str());
+ }
+
+ 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..cb12116
--- /dev/null
+++ b/src/ui/widget/tolerance-slider.h
@@ -0,0 +1,88 @@
+// 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 Box;
+}
+
+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::Box* _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::Box *_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..a0ae163
--- /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);
+ add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK);
+ signal_scroll_event().connect([](GdkEventScroll*){ return false; });
+}
+
+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..1f10cc2
--- /dev/null
+++ b/src/ui/widget/unit-menu.h
@@ -0,0 +1,154 @@
+// 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 <gtkmm.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();
+
+ /* GtkBuilder constructor */
+ UnitMenu(BaseObjectType* cobject, const Glib::RefPtr<Gtk::Builder>& refGlade):Gtk::ComboBoxText(cobject){
+ 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;
+
+ Gtk::ComboBoxText* _combo;
+};
+
+} // 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..7c52b3b
--- /dev/null
+++ b/src/ui/widget/unit-tracker.cpp
@@ -0,0 +1,315 @@
+// 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;
+}
+
+Glib::ustring UnitTracker::getCurrentLabel()
+{
+ ComboToolItemColumns columns;
+ return _store->children()[_active][columns.col_label];
+}
+
+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::setActiveUnitByLabel(Glib::ustring label)
+{
+ ComboToolItemColumns columns;
+ int index = 0;
+ for (auto &row : _store->children()) {
+ Glib::ustring storedUnit = row[columns.col_label];
+ if (!label.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_name("unit-tracker");
+ combo->set_data(Glib::Quark("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..243b19f
--- /dev/null
+++ b/src/ui/widget/unit-tracker.h
@@ -0,0 +1,89 @@
+// 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);
+ void setActiveUnitByLabel(Glib::ustring label);
+ 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);
+ Glib::ustring getCurrentLabel();
+ 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