summaryrefslogtreecommitdiffstats
path: root/src/ui
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/CMakeLists.txt515
-rw-r--r--src/ui/README21
-rw-r--r--src/ui/builder-utils.cpp20
-rw-r--r--src/ui/builder-utils.h35
-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.cpp1886
-rw-r--r--src/ui/clipboard.h77
-rw-r--r--src/ui/contextmenu.cpp340
-rw-r--r--src/ui/contextmenu.h54
-rw-r--r--src/ui/control-types.h58
-rw-r--r--src/ui/cursor-utils.cpp249
-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.cpp153
-rw-r--r--src/ui/desktop/menu-icon-shift.h46
-rw-r--r--src/ui/desktop/menubar.cpp322
-rw-r--r--src/ui/desktop/menubar.h49
-rw-r--r--src/ui/dialog-events.cpp240
-rw-r--r--src/ui/dialog-events.h76
-rw-r--r--src/ui/dialog/README.md46
-rw-r--r--src/ui/dialog/about.cpp200
-rw-r--r--src/ui/dialog/about.h43
-rw-r--r--src/ui/dialog/align-and-distribute.cpp304
-rw-r--r--src/ui/dialog/align-and-distribute.h95
-rw-r--r--src/ui/dialog/arrange-tab.h55
-rw-r--r--src/ui/dialog/attrdialog.cpp711
-rw-r--r--src/ui/dialog/attrdialog.h130
-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.cpp2819
-rw-r--r--src/ui/dialog/clonetiler.h211
-rw-r--r--src/ui/dialog/color-item.cpp667
-rw-r--r--src/ui/dialog/color-item.h135
-rw-r--r--src/ui/dialog/command-palette.cpp1636
-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.cpp320
-rw-r--r--src/ui/dialog/dialog-base.h135
-rw-r--r--src/ui/dialog/dialog-container.cpp1111
-rw-r--r--src/ui/dialog/dialog-container.h130
-rw-r--r--src/ui/dialog/dialog-data.cpp79
-rw-r--r--src/ui/dialog/dialog-data.h54
-rw-r--r--src/ui/dialog/dialog-manager.cpp310
-rw-r--r--src/ui/dialog/dialog-manager.h97
-rw-r--r--src/ui/dialog/dialog-multipaned.cpp1280
-rw-r--r--src/ui/dialog/dialog-multipaned.h201
-rw-r--r--src/ui/dialog/dialog-notebook.cpp994
-rw-r--r--src/ui/dialog/dialog-notebook.h124
-rw-r--r--src/ui/dialog/dialog-window.cpp308
-rw-r--r--src/ui/dialog/dialog-window.h78
-rw-r--r--src/ui/dialog/document-properties.cpp1784
-rw-r--r--src/ui/dialog/document-properties.h253
-rw-r--r--src/ui/dialog/export-batch.cpp768
-rw-r--r--src/ui/dialog/export-batch.h163
-rw-r--r--src/ui/dialog/export-single.cpp990
-rw-r--r--src/ui/dialog/export-single.h217
-rw-r--r--src/ui/dialog/export.cpp511
-rw-r--r--src/ui/dialog/export.h138
-rw-r--r--src/ui/dialog/filedialog.cpp201
-rw-r--r--src/ui/dialog/filedialog.h256
-rw-r--r--src/ui/dialog/filedialogimpl-gtkmm.cpp867
-rw-r--r--src/ui/dialog/filedialogimpl-gtkmm.h307
-rw-r--r--src/ui/dialog/filedialogimpl-win32.cpp1929
-rw-r--r--src/ui/dialog/filedialogimpl-win32.h392
-rw-r--r--src/ui/dialog/fill-and-stroke.cpp215
-rw-r--r--src/ui/dialog/fill-and-stroke.h96
-rw-r--r--src/ui/dialog/filter-effects-dialog.cpp3124
-rw-r--r--src/ui/dialog/filter-effects-dialog.h333
-rw-r--r--src/ui/dialog/find.cpp1132
-rw-r--r--src/ui/dialog/find.h320
-rw-r--r--src/ui/dialog/font-substitution.cpp271
-rw-r--r--src/ui/dialog/font-substitution.h59
-rw-r--r--src/ui/dialog/glyphs.cpp783
-rw-r--r--src/ui/dialog/glyphs.h91
-rw-r--r--src/ui/dialog/grid-arrange-tab.cpp659
-rw-r--r--src/ui/dialog/grid-arrange-tab.h152
-rw-r--r--src/ui/dialog/guides.cpp369
-rw-r--r--src/ui/dialog/guides.h106
-rw-r--r--src/ui/dialog/icon-preview.cpp646
-rw-r--r--src/ui/dialog/icon-preview.h114
-rw-r--r--src/ui/dialog/inkscape-preferences.cpp3707
-rw-r--r--src/ui/dialog/inkscape-preferences.h747
-rw-r--r--src/ui/dialog/input.cpp1798
-rw-r--r--src/ui/dialog/input.h45
-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.cpp981
-rw-r--r--src/ui/dialog/livepatheffect-add.h147
-rw-r--r--src/ui/dialog/livepatheffect-editor.cpp639
-rw-r--r--src/ui/dialog/livepatheffect-editor.h140
-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.cpp261
-rw-r--r--src/ui/dialog/memory.h55
-rw-r--r--src/ui/dialog/messages.cpp214
-rw-r--r--src/ui/dialog/messages.h102
-rw-r--r--src/ui/dialog/new-from-template.cpp72
-rw-r--r--src/ui/dialog/new-from-template.h45
-rw-r--r--src/ui/dialog/object-attributes.cpp182
-rw-r--r--src/ui/dialog/object-attributes.h85
-rw-r--r--src/ui/dialog/object-properties.cpp581
-rw-r--r--src/ui/dialog/object-properties.h145
-rw-r--r--src/ui/dialog/objects.cpp1468
-rw-r--r--src/ui/dialog/objects.h174
-rw-r--r--src/ui/dialog/paint-servers.cpp567
-rw-r--r--src/ui/dialog/paint-servers.h142
-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.cpp262
-rw-r--r--src/ui/dialog/print.h80
-rw-r--r--src/ui/dialog/prototype.cpp101
-rw-r--r--src/ui/dialog/prototype.h71
-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.cpp1350
-rw-r--r--src/ui/dialog/selectorsdialog.h198
-rw-r--r--src/ui/dialog/spellcheck.cpp761
-rw-r--r--src/ui/dialog/spellcheck.h282
-rw-r--r--src/ui/dialog/startup.cpp849
-rw-r--r--src/ui/dialog/startup.h85
-rw-r--r--src/ui/dialog/styledialog.cpp1609
-rw-r--r--src/ui/dialog/styledialog.h199
-rw-r--r--src/ui/dialog/svg-fonts-dialog.cpp1795
-rw-r--r--src/ui/dialog/svg-fonts-dialog.h386
-rw-r--r--src/ui/dialog/svg-preview.cpp477
-rw-r--r--src/ui/dialog/svg-preview.h123
-rw-r--r--src/ui/dialog/swatches.cpp1113
-rw-r--r--src/ui/dialog/swatches.h108
-rw-r--r--src/ui/dialog/symbols.cpp1354
-rw-r--r--src/ui/dialog/symbols.h179
-rw-r--r--src/ui/dialog/template-load-tab.cpp340
-rw-r--r--src/ui/dialog/template-load-tab.h119
-rw-r--r--src/ui/dialog/template-widget.cpp155
-rw-r--r--src/ui/dialog/template-widget.h51
-rw-r--r--src/ui/dialog/text-edit.cpp512
-rw-r--r--src/ui/dialog/text-edit.h194
-rw-r--r--src/ui/dialog/tile.cpp136
-rw-r--r--src/ui/dialog/tile.h83
-rw-r--r--src/ui/dialog/tracedialog.cpp485
-rw-r--r--src/ui/dialog/tracedialog.h66
-rw-r--r--src/ui/dialog/transformation.cpp1216
-rw-r--r--src/ui/dialog/transformation.h259
-rw-r--r--src/ui/dialog/undo-history.cpp386
-rw-r--r--src/ui/dialog/undo-history.h165
-rw-r--r--src/ui/dialog/xml-tree.cpp952
-rw-r--r--src/ui/dialog/xml-tree.h238
-rw-r--r--src/ui/drag-and-drop.cpp464
-rw-r--r--src/ui/drag-and-drop.h32
-rw-r--r--src/ui/draw-anchor.cpp86
-rw-r--r--src/ui/draw-anchor.h71
-rw-r--r--src/ui/event-debug.h125
-rw-r--r--src/ui/icon-loader.cpp137
-rw-r--r--src/ui/icon-loader.h29
-rw-r--r--src/ui/icon-names.h32
-rw-r--r--src/ui/interface.cpp188
-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.cpp454
-rw-r--r--src/ui/knot/knot-holder-entity.h205
-rw-r--r--src/ui/knot/knot-holder.cpp455
-rw-r--r--src/ui/knot/knot-holder.h116
-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.cpp520
-rw-r--r--src/ui/knot/knot.h208
-rw-r--r--src/ui/modifiers.cpp242
-rw-r--r--src/ui/modifiers.h249
-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/previewable.h64
-rw-r--r--src/ui/previewholder.cpp445
-rw-r--r--src/ui/previewholder.h91
-rw-r--r--src/ui/selected-color.cpp159
-rw-r--r--src/ui/selected-color.h100
-rw-r--r--src/ui/shape-editor-knotholders.cpp2610
-rw-r--r--src/ui/shape-editor.cpp223
-rw-r--r--src/ui/shape-editor.h75
-rw-r--r--src/ui/shortcuts.cpp996
-rw-r--r--src/ui/shortcuts.h137
-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.cpp112
-rw-r--r--src/ui/svg-renderer.h45
-rw-r--r--src/ui/themes.cpp546
-rw-r--r--src/ui/themes.h89
-rw-r--r--src/ui/tool-factory.cpp107
-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.cpp597
-rw-r--r--src/ui/tool/control-point.h414
-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.cpp899
-rw-r--r--src/ui/tool/multi-path-manipulator.h157
-rw-r--r--src/ui/tool/node-types.h57
-rw-r--r--src/ui/tool/node.cpp1923
-rw-r--r--src/ui/tool/node.h532
-rw-r--r--src/ui/tool/path-manipulator.cpp1804
-rw-r--r--src/ui/tool/path-manipulator.h186
-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/selector.cpp153
-rw-r--r--src/ui/tool/selector.h59
-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.cpp559
-rw-r--r--src/ui/toolbar/arc-toolbar.h116
-rw-r--r--src/ui/toolbar/box3d-toolbar.cpp428
-rw-r--r--src/ui/toolbar/box3d-toolbar.h108
-rw-r--r--src/ui/toolbar/calligraphy-toolbar.cpp628
-rw-r--r--src/ui/toolbar/calligraphy-toolbar.h105
-rw-r--r--src/ui/toolbar/connector-toolbar.cpp431
-rw-r--r--src/ui/toolbar/connector-toolbar.h93
-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.h101
-rw-r--r--src/ui/toolbar/gradient-toolbar.cpp1173
-rw-r--r--src/ui/toolbar/gradient-toolbar.h105
-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.cpp663
-rw-r--r--src/ui/toolbar/node-toolbar.h116
-rw-r--r--src/ui/toolbar/page-toolbar.cpp351
-rw-r--r--src/ui/toolbar/page-toolbar.h91
-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.cpp692
-rw-r--r--src/ui/toolbar/pencil-toolbar.h111
-rw-r--r--src/ui/toolbar/rect-toolbar.cpp416
-rw-r--r--src/ui/toolbar/rect-toolbar.h115
-rw-r--r--src/ui/toolbar/select-toolbar.cpp631
-rw-r--r--src/ui/toolbar/select-toolbar.h94
-rw-r--r--src/ui/toolbar/spiral-toolbar.cpp295
-rw-r--r--src/ui/toolbar/spiral-toolbar.h98
-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.cpp569
-rw-r--r--src/ui/toolbar/star-toolbar.h108
-rw-r--r--src/ui/toolbar/text-toolbar.cpp2576
-rw-r--r--src/ui/toolbar/text-toolbar.h150
-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.cpp455
-rw-r--r--src/ui/tools/arc-tool.h76
-rw-r--r--src/ui/tools/box3d-tool.cpp566
-rw-r--r--src/ui/tools/box3d-tool.h103
-rw-r--r--src/ui/tools/calligraphic-tool.cpp1192
-rw-r--r--src/ui/tools/calligraphic-tool.h100
-rw-r--r--src/ui/tools/connector-tool.cpp1383
-rw-r--r--src/ui/tools/connector-tool.h164
-rw-r--r--src/ui/tools/dropper-tool.cpp402
-rw-r--r--src/ui/tools/dropper-tool.h93
-rw-r--r--src/ui/tools/dynamic-base.cpp155
-rw-r--r--src/ui/tools/dynamic-base.h134
-rw-r--r--src/ui/tools/eraser-tool.cpp1229
-rw-r--r--src/ui/tools/eraser-tool.h132
-rw-r--r--src/ui/tools/flood-tool.cpp1239
-rw-r--r--src/ui/tools/flood-tool.h67
-rw-r--r--src/ui/tools/freehand-base.cpp1077
-rw-r--r--src/ui/tools/freehand-base.h157
-rw-r--r--src/ui/tools/gradient-tool.cpp822
-rw-r--r--src/ui/tools/gradient-tool.h78
-rw-r--r--src/ui/tools/lpe-tool.cpp473
-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.cpp1470
-rw-r--r--src/ui/tools/measure-tool.h128
-rw-r--r--src/ui/tools/mesh-tool.cpp973
-rw-r--r--src/ui/tools/mesh-tool.h87
-rw-r--r--src/ui/tools/node-tool.cpp808
-rw-r--r--src/ui/tools/node-tool.h110
-rw-r--r--src/ui/tools/pages-tool.cpp593
-rw-r--r--src/ui/tools/pages-tool.h91
-rw-r--r--src/ui/tools/pen-tool.cpp2027
-rw-r--r--src/ui/tools/pen-tool.h165
-rw-r--r--src/ui/tools/pencil-tool.cpp1189
-rw-r--r--src/ui/tools/pencil-tool.h101
-rw-r--r--src/ui/tools/rect-tool.cpp465
-rw-r--r--src/ui/tools/rect-tool.h60
-rw-r--r--src/ui/tools/select-tool.cpp1146
-rw-r--r--src/ui/tools/select-tool.h79
-rw-r--r--src/ui/tools/spiral-tool.cpp411
-rw-r--r--src/ui/tools/spiral-tool.h61
-rw-r--r--src/ui/tools/spray-tool.cpp1538
-rw-r--r--src/ui/tools/spray-tool.h147
-rw-r--r--src/ui/tools/star-tool.cpp430
-rw-r--r--src/ui/tools/star-tool.h72
-rw-r--r--src/ui/tools/text-tool.cpp1933
-rw-r--r--src/ui/tools/text-tool.h121
-rw-r--r--src/ui/tools/tool-base.cpp1649
-rw-r--r--src/ui/tools/tool-base.h309
-rw-r--r--src/ui/tools/tweak-tool.cpp1489
-rw-r--r--src/ui/tools/tweak-tool.h104
-rw-r--r--src/ui/tools/zoom-tool.cpp214
-rw-r--r--src/ui/tools/zoom-tool.h41
-rw-r--r--src/ui/util.cpp93
-rw-r--r--src/ui/util.h59
-rw-r--r--src/ui/view/README51
-rw-r--r--src/ui/view/svg-view-widget.cpp261
-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.cpp124
-rw-r--r--src/ui/view/view.h145
-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.cpp318
-rw-r--r--src/ui/widget/canvas-grid.h113
-rw-r--r--src/ui/widget/canvas.cpp2797
-rw-r--r--src/ui/widget/canvas.h234
-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.cpp1025
-rw-r--r--src/ui/widget/color-icc-selector.h75
-rw-r--r--src/ui/widget/color-notebook.cpp347
-rw-r--r--src/ui/widget/color-notebook.h100
-rw-r--r--src/ui/widget/color-palette.cpp618
-rw-r--r--src/ui/widget/color-palette.h111
-rw-r--r--src/ui/widget/color-picker.cpp169
-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.cpp1062
-rw-r--r--src/ui/widget/color-scales.h127
-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.cpp707
-rw-r--r--src/ui/widget/combo-box-entry-tool-item.h154
-rw-r--r--src/ui/widget/combo-enums.h224
-rw-r--r--src/ui/widget/combo-tool-item.cpp290
-rw-r--r--src/ui/widget/combo-tool-item.h136
-rw-r--r--src/ui/widget/dash-selector.cpp259
-rw-r--r--src/ui/widget/dash-selector.h116
-rw-r--r--src/ui/widget/entity-entry.cpp208
-rw-r--r--src/ui/widget/entity-entry.h85
-rw-r--r--src/ui/widget/entry.cpp30
-rw-r--r--src/ui/widget/entry.h45
-rw-r--r--src/ui/widget/export-lists.cpp287
-rw-r--r--src/ui/widget/export-lists.h102
-rw-r--r--src/ui/widget/export-preview.cpp235
-rw-r--r--src/ui/widget/export-preview.h87
-rw-r--r--src/ui/widget/fill-style.cpp714
-rw-r--r--src/ui/widget/fill-style.h85
-rw-r--r--src/ui/widget/filter-effect-chooser.cpp203
-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-selector-toolbar.cpp302
-rw-r--r--src/ui/widget/font-selector-toolbar.h120
-rw-r--r--src/ui/widget/font-selector.cpp471
-rw-r--r--src/ui/widget/font-selector.h165
-rw-r--r--src/ui/widget/font-variants.cpp1459
-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/framecheck.cpp29
-rw-r--r--src/ui/widget/framecheck.h66
-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.cpp612
-rw-r--r--src/ui/widget/gradient-selector.h160
-rw-r--r--src/ui/widget/gradient-vector-selector.cpp327
-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.h73
-rw-r--r--src/ui/widget/iconrenderer.cpp119
-rw-r--r--src/ui/widget/iconrenderer.h84
-rw-r--r--src/ui/widget/imagetoggler.cpp135
-rw-r--r--src/ui/widget/imagetoggler.h92
-rw-r--r--src/ui/widget/ink-color-wheel.cpp1507
-rw-r--r--src/ui/widget/ink-color-wheel.h152
-rw-r--r--src/ui/widget/ink-flow-box.cpp144
-rw-r--r--src/ui/widget/ink-flow-box.h68
-rw-r--r--src/ui/widget/ink-ruler.cpp476
-rw-r--r--src/ui/widget/ink-ruler.h79
-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.cpp105
-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.cpp156
-rw-r--r--src/ui/widget/licensor.h57
-rw-r--r--src/ui/widget/marker-combo-box.cpp1033
-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.cpp312
-rw-r--r--src/ui/widget/object-composite-settings.h78
-rw-r--r--src/ui/widget/page-properties.cpp515
-rw-r--r--src/ui/widget/page-properties.h59
-rw-r--r--src/ui/widget/page-selector.cpp196
-rw-r--r--src/ui/widget/page-selector.h91
-rw-r--r--src/ui/widget/page-size-preview.cpp185
-rw-r--r--src/ui/widget/page-size-preview.h50
-rw-r--r--src/ui/widget/paint-selector.cpp1476
-rw-r--r--src/ui/widget/paint-selector.h226
-rw-r--r--src/ui/widget/point.cpp180
-rw-r--r--src/ui/widget/point.h196
-rw-r--r--src/ui/widget/preferences-widget.cpp1118
-rw-r--r--src/ui/widget/preferences-widget.h345
-rw-r--r--src/ui/widget/preview.cpp511
-rw-r--r--src/ui/widget/preview.h165
-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.cpp843
-rw-r--r--src/ui/widget/registered-widget.h455
-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.cpp179
-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.cpp186
-rw-r--r--src/ui/widget/scalar.h191
-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.cpp1435
-rw-r--r--src/ui/widget/selected-style.h302
-rw-r--r--src/ui/widget/shapeicon.cpp117
-rw-r--r--src/ui/widget/shapeicon.h101
-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.cpp231
-rw-r--r--src/ui/widget/spin-scale.h114
-rw-r--r--src/ui/widget/spinbutton.cpp126
-rw-r--r--src/ui/widget/spinbutton.h112
-rw-r--r--src/ui/widget/stroke-style.cpp1232
-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.cpp386
-rw-r--r--src/ui/widget/style-swatch.h107
-rw-r--r--src/ui/widget/swatch-selector.cpp148
-rw-r--r--src/ui/widget/swatch-selector.h65
-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
490 files changed, 170986 insertions, 0 deletions
diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt
new file mode 100644
index 0000000..0fde2c0
--- /dev/null
+++ b/src/ui/CMakeLists.txt
@@ -0,0 +1,515 @@
+# 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
+ previewholder.cpp
+ selected-color.cpp
+ shape-editor.cpp
+ shape-editor-knotholders.cpp
+ simple-pref-pusher.cpp
+ shortcuts.cpp
+ svg-renderer.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/selector.cpp
+ tool/transform-handle-set.cpp
+
+ toolbar/arc-toolbar.cpp
+ toolbar/box3d-toolbar.cpp
+ toolbar/calligraphy-toolbar.cpp
+ toolbar/connector-toolbar.cpp
+ toolbar/dropper-toolbar.cpp
+ toolbar/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/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/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/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-substitution.cpp
+ dialog/glyphs.cpp
+ dialog/grid-arrange-tab.cpp
+ dialog/guides.cpp
+ dialog/icon-preview.cpp
+ dialog/inkscape-preferences.cpp
+ dialog/input.cpp
+ dialog/knot-properties.cpp
+ dialog/layer-properties.cpp
+ dialog/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/template-load-tab.cpp
+ dialog/template-widget.cpp
+ dialog/text-edit.cpp
+ dialog/tile.cpp
+ dialog/tracedialog.cpp
+ dialog/transformation.cpp
+ dialog/undo-history.cpp
+ dialog/xml-tree.cpp
+ dialog/save-template-dialog.cpp
+
+ widget/iconrenderer.cpp
+ widget/alignment-selector.cpp
+ widget/anchor-selector.cpp
+ widget/canvas.cpp
+ widget/canvas-grid.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/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/framecheck.cpp
+ widget/gradient-image.cpp
+ widget/gradient-editor.cpp
+ widget/gradient-selector.cpp
+ widget/gradient-vector-selector.cpp
+ widget/gradient-with-stops.cpp
+ widget/imagetoggler.cpp
+ widget/ink-color-wheel.cpp
+ widget/ink-flow-box.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/page-properties.cpp
+ widget/page-size-preview.cpp
+ widget/page-selector.cpp
+ widget/paint-selector.cpp
+ widget/point.cpp
+ widget/preferences-widget.cpp
+ widget/preview.cpp
+ widget/random.cpp
+ widget/registered-widget.cpp
+ widget/registry.cpp
+ widget/rendering-options.cpp
+ widget/rotateable.cpp
+ widget/scalar-unit.cpp
+ widget/scalar.cpp
+ widget/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/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
+ icon-names.h
+ icon-loader.h
+ interface.h
+ monitor.h
+ previewable.h
+ previewholder.h
+ selected-color.h
+ shape-editor.h
+ simple-pref-pusher.h
+ shortcuts.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-substitution.h
+ dialog/glyphs.h
+ dialog/grid-arrange-tab.h
+ dialog/guides.h
+ dialog/icon-preview.h
+ dialog/inkscape-preferences.h
+ dialog/input.h
+ dialog/knot-properties.h
+ dialog/layer-properties.h
+ dialog/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/template-load-tab.h
+ dialog/template-widget.h
+ dialog/text-edit.h
+ dialog/tile.h
+ dialog/tracedialog.h
+ dialog/transformation.h
+ dialog/undo-history.h
+ dialog/xml-tree.h
+ dialog/save-template-dialog.h
+
+ 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/selector.h
+ tool/shape-record.h
+ tool/transform-handle-set.h
+
+ toolbar/arc-toolbar.h
+ toolbar/box3d-toolbar.h
+ toolbar/calligraphy-toolbar.h
+ toolbar/connector-toolbar.h
+ toolbar/dropper-toolbar.h
+ toolbar/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/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/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-grid.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/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-selector.h
+ widget/font-selector-toolbar.h
+ widget/font-variants.h
+ widget/font-variations.h
+ widget/frame.h
+ widget/framecheck.h
+ widget/gradient-image.h
+ widget/gradient-editor.h
+ widget/gradient-selector.h
+ widget/gradient-vector-selector.h
+ widget/gradient-with-stops.h
+ widget/imagetoggler.h
+ widget/ink-color-wheel.h
+ widget/ink-flow-box.h
+ widget/ink-ruler.h
+ widget/ink-spinscale.h
+ widget/label-tool-item.h
+ widget/labelled.h
+ widget/layer-selector.h
+ widget/licensor.h
+ widget/marker-combo-box.h
+ widget/notebook-page.h
+ widget/object-composite-settings.h
+ widget/page-selector.h
+ widget/paint-selector.h
+ widget/point.h
+ widget/preferences-widget.h
+ widget/preview.h
+ widget/random.h
+ widget/registered-enums.h
+ widget/registered-widget.h
+ widget/registry.h
+ widget/rendering-options.h
+ widget/rotateable.h
+ widget/scalar-unit.h
+ widget/scalar.h
+ widget/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/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..ef6d181
--- /dev/null
+++ b/src/ui/builder-utils.h
@@ -0,0 +1,35 @@
+// 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 <gtkmm/builder.h>
+
+namespace Inkscape {
+namespace UI {
+
+// 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;
+}
+
+// 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 \ No newline at end of file
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..a144af2
--- /dev/null
+++ b/src/ui/clipboard.cpp
@@ -0,0 +1,1886 @@
+// 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>
+
+// 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-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-textpath.h"
+#include "object/sp-use.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) override;
+ bool paste(SPDesktop *desktop, bool in_place) override;
+ bool pasteStyle(ObjectSet *set) override;
+ bool pasteSize(ObjectSet *set, bool separately, bool apply_x, bool apply_y) override;
+ bool pastePathEffect(ObjectSet *set) override;
+ Glib::ustring getPathParameter(SPDesktop* desktop) override;
+ Glib::ustring getShapeOrTextObjectId(SPDesktop *desktop) override;
+ std::vector<Glib::ustring> getElementsOfType(SPDesktop *desktop, gchar const* type = "*", gint maxdepth = -1) override;
+ 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);
+ 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();
+ 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.
+ */
+void ClipboardManagerImpl::copySymbol(Inkscape::XML::Node* symbol, gchar const* style, SPDocument *source)
+{
+ 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.
+ 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 scale = _clipboardSPDoc->getDocumentScale();
+ if (auto group = dynamic_cast<SPGroup *>(_clipboardSPDoc->getObjectByRepr(repr))) {
+ // Convert scale from source to clipboard user units
+ group->scaleChildItemsRec(scale, Geom::Point(0, 0), false);
+ }
+
+ auto href = Glib::ustring("#") + symbol->attribute("id");
+ 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);
+
+ if (auto use = dynamic_cast<SPUse *>(_clipboardSPDoc->getObjectByRepr(use_repr))) {
+ Geom::Affine affine = source->getDocumentScale();
+ use->doWriteTransform(affine, &affine, false);
+ }
+
+ // This min and max sets offsets, we don't have any so set to zero.
+ _clipnode->setAttributePoint("min", Geom::Point(0, 0));
+ _clipnode->setAttributePoint("max", Geom::Point(0, 0));
+
+ fit_canvas_to_drawing(_clipboardSPDoc.get());
+ _setClipboardTargets();
+}
+
+/**
+ * Paste from the system clipboard into the active desktop.
+ * @param in_place Whether to put the contents where they were when copied.
+ */
+bool ClipboardManagerImpl::paste(SPDesktop *desktop, bool in_place)
+{
+ // do any checking whether we really are able to paste before requesting the contents
+ if ( desktop == nullptr ) {
+ return false;
+ }
+ if ( Inkscape::have_viable_layer(desktop, desktop->getMessageStack()) == false ) {
+ return false;
+ }
+
+ Glib::ustring target = _getBestTarget();
+
+ // Special cases of clipboard content handling go here
+ // Note that target priority is determined in _getBestTarget.
+ // TODO: Handle x-special/gnome-copied-files and text/uri-list to support pasting files
+
+ // if there is an image on the clipboard, paste it
+ if ( target == CLIPBOARD_GDK_PIXBUF_TARGET ) {
+ return _pasteImage(desktop->doc());
+ }
+ if (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)) {
+ return true;
+ }
+
+ // copy definitions
+ prevent_id_clashes(tempdoc.get(), desktop->getDocument(), true);
+ sp_import_document(desktop, tempdoc.get(), in_place);
+ // _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") {
+ desktop->selection->ungroup(true);
+ std::vector<SPItem *> vec2(desktop->selection->items().begin(), desktop->selection->items().end());
+ for (auto item : vec2) {
+ // just a bit beauty on paste hidden items unselect
+ if (vec2.size() > 1 && item->isHidden()) {
+ desktop->selection->remove(item);
+ }
+ SPLPEItem *pasted_lpe_item = dynamic_cast<SPLPEItem *>(item);
+ if (pasted_lpe_item) {
+ remove_hidder_filter(pasted_lpe_item);
+ }
+ }
+ }
+
+ 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(SP_IS_PATH(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 = dynamic_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 = dynamic_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)
+{
+ auto node_tool = dynamic_cast<Inkscape::UI::Tools::NodeTool *>(desktop->event_context);
+ if (!node_tool || desktop->selection->objects().size() != 1)
+ return false;
+
+ SPObject *obj = desktop->selection->objects().back();
+ auto target_path = dynamic_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 = dynamic_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 = dynamic_cast<SPPath *>(source_obj)) {
+ auto source_curve = SPCurve::copy(source_path->curveForEdit());
+ auto target_curve = SPCurve::copy(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(*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);
+ }
+ }
+ // 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)
+{
+ if (set->desktop() == nullptr) {
+ return false;
+ }
+
+ // check whether something is selected
+ if (set->isEmpty()) {
+ _userWarn(set->desktop(), _("Select <b>object(s)</b> to paste style to."));
+ return false;
+ }
+
+ 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;
+ }
+ }
+
+ Inkscape::XML::Node *root = tempdoc->getReprRoot();
+ Inkscape::XML::Node *clipnode = sp_repr_lookup_name(root, "inkscape:clipboard", 1);
+
+ bool pasted = false;
+
+ if (clipnode) {
+ set->document()->importDefs(tempdoc.get());
+ SPCSSAttr *style = sp_repr_css_attr(clipnode, "style");
+ sp_desktop_set_style(set, set->desktop(), style);
+ pasted = true;
+ }
+ else {
+ _userWarn(set->desktop(), _("No style on the clipboard."));
+ }
+
+ 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);
+ SPItem *item = dynamic_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)
+{
+ // 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) {
+ SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item);
+ if (lpeitem) {
+ for (auto satellite : lpeitem->get_satellites(false, true)) {
+ if (satellite) {
+ SPItem *item2 = dynamic_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){
+ SPItem *item = dynamic_cast<SPItem*>(sorted_item);
+ if (item) {
+ // Create a group with the parent transform. This group will be ungrouped when pasting
+ // und takes care of transform relationships of clones, text-on-path, etc.
+ auto &group = groups[item->parent];
+ if (!group) {
+ group = _doc->createElement("svg:g");
+ _root->appendChild(group);
+ Inkscape::GC::release(group);
+
+ if (auto parent = dynamic_cast<SPItem *>(item->parent)) {
+ auto transform_str = sp_svg_transform_write(parent->i2doc_affine());
+ group->setAttributeOrRemoveIfEmpty("transform", transform_str);
+ }
+ }
+
+ Inkscape::XML::Node *obj = item->getRepr();
+ Inkscape::XML::Node *obj_copy;
+ if(cloned_elements.find(item)==cloned_elements.end())
+ obj_copy = _copyNode(obj, _doc, group);
+ else
+ obj_copy = _copyNode(obj, _doc, _clipnode);
+
+ // copy complete inherited style
+ _copyCompleteStyle(item, obj_copy);
+ }
+ }
+ // copy style for Paste Style action
+ if (auto item = selection->singleItem()) {
+ SPCSSAttr *style = take_style_from_item(item);
+ _cleanStyle(style);
+ sp_repr_css_set(_clipnode, style, "style");
+ sp_repr_css_attr_unref(style);
+
+ // 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());
+ }
+}
+
+/**
+ * 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 (dynamic_cast<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 = dynamic_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)
+{
+ SPUse *use=dynamic_cast<SPUse *>(item);
+ if (use && use->get_original()) {
+ if(cloned_elements.insert(use->get_original()).second)
+ _copyUsedDefs(use->get_original());
+ }
+
+ // copy fill and stroke styles (patterns and gradients)
+ SPStyle *style = item->style;
+
+ if (style && (style->fill.isPaintserver())) {
+ SPPaintServer *server = item->style->getFillPaintServer();
+ if ( dynamic_cast<SPLinearGradient *>(server) || dynamic_cast<SPRadialGradient *>(server) || dynamic_cast<SPMeshGradient *>(server) ) {
+ _copyGradient(dynamic_cast<SPGradient *>(server));
+ }
+ SPPattern *pattern = dynamic_cast<SPPattern *>(server);
+ if (pattern) {
+ _copyPattern(pattern);
+ }
+ SPHatch *hatch = dynamic_cast<SPHatch *>(server);
+ if (hatch) {
+ _copyHatch(hatch);
+ }
+ }
+ if (style && (style->stroke.isPaintserver())) {
+ SPPaintServer *server = item->style->getStrokePaintServer();
+ if ( dynamic_cast<SPLinearGradient *>(server) || dynamic_cast<SPRadialGradient *>(server) || dynamic_cast<SPMeshGradient *>(server) ) {
+ _copyGradient(dynamic_cast<SPGradient *>(server));
+ }
+ SPPattern *pattern = dynamic_cast<SPPattern *>(server);
+ if (pattern) {
+ _copyPattern(pattern);
+ }
+ SPHatch *hatch = dynamic_cast<SPHatch *>(server);
+ if (hatch) {
+ _copyHatch(hatch);
+ }
+ }
+
+ // For shapes, copy all of the shape's markers
+ SPShape *shape = dynamic_cast<SPShape *>(item);
+ if (shape) {
+ for (auto & i : shape->_marker) {
+ if (i) {
+ _copyNode(i->getRepr(), _doc, _defs);
+ }
+ }
+ }
+
+ // For 3D boxes, copy perspectives
+ if (SPBox3D *box = dynamic_cast<SPBox3D *>(item)) {
+ if (auto perspective = box->get_perspective()) {
+ _copyNode(perspective->getRepr(), _doc, _defs);
+ }
+ }
+
+ // Copy text paths
+ {
+ SPText *text = dynamic_cast<SPText *>(item);
+ SPTextPath *textpath = (text) ? dynamic_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);
+ }
+ // Copy mask objects
+ if (SPObject *mask = item->getMaskObject()) {
+ _copyNode(mask->getRepr(), _doc, _defs);
+ // recurse into the mask for its gradients etc.
+ for(auto& o: mask->children) {
+ SPItem *childItem = dynamic_cast<SPItem *>(&o);
+ if (childItem) {
+ _copyUsedDefs(childItem);
+ }
+ }
+ }
+
+ // Copy filters
+ if (style->getFilter()) {
+ SPObject *filter = style->getFilter();
+ if (dynamic_cast<SPFilter *>(filter)) {
+ _copyNode(filter->getRepr(), _doc, _defs);
+ }
+ }
+
+ // For lpe items, copy lpe stack if applicable
+ SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item);
+ if (lpeitem) {
+ if (lpeitem->hasPathEffect()) {
+ PathEffectList path_effect_list( *lpeitem->path_effect_list);
+ for (auto &lperef : path_effect_list) {
+ LivePathEffectObject *lpeobj = lperef->lpeobject;
+ if (lpeobj) {
+ _copyNode(lpeobj->getRepr(), _doc, _defs);
+ }
+ }
+ }
+ }
+
+ // recurse
+ for(auto& o: item->children) {
+ SPItem *childItem = dynamic_cast<SPItem *>(&o);
+ if (childItem) {
+ _copyUsedDefs(childItem);
+ }
+ }
+}
+
+/**
+ * Copy a single gradient to the clipboard's defs element.
+ */
+void ClipboardManagerImpl::_copyGradient(SPGradient *gradient)
+{
+ while (gradient) {
+ // climb up the refs, copying each one in the chain
+ _copyNode(gradient->getRepr(), _doc, _defs);
+ if (gradient->ref){
+ gradient = gradient->ref->getObject();
+ }
+ else {
+ gradient = nullptr;
+ }
+ }
+}
+
+
+/**
+ * Copy a single pattern to the clipboard document's defs element.
+ */
+void ClipboardManagerImpl::_copyPattern(SPPattern *pattern)
+{
+ // climb up the references, copying each one in the chain
+ while (pattern) {
+ _copyNode(pattern->getRepr(), _doc, _defs);
+
+ // items in the pattern may also use gradients and other patterns, so recurse
+ for (auto& child: pattern->children) {
+ SPItem *childItem = dynamic_cast<SPItem *>(&child);
+ if (childItem) {
+ _copyUsedDefs(childItem);
+ }
+ }
+ if (pattern->ref){
+ pattern = pattern->ref->getObject();
+ }
+ else{
+ pattern = nullptr;
+ }
+ }
+}
+
+/**
+ * Copy a single hatch to the clipboard document's defs element.
+ */
+void ClipboardManagerImpl::_copyHatch(SPHatch *hatch)
+{
+ // climb up the references, copying each one in the chain
+ while (hatch) {
+ _copyNode(hatch->getRepr(), _doc, _defs);
+
+ for (auto &child : hatch->children) {
+ SPItem *childItem = dynamic_cast<SPItem *>(&child);
+ if (childItem) {
+ _copyUsedDefs(childItem);
+ }
+ }
+ if (hatch->ref) {
+ hatch = hatch->ref->getObject();
+ } else {
+ hatch = nullptr;
+ }
+ }
+}
+
+
+/**
+ * Copy a text path to the clipboard's defs element.
+ */
+void ClipboardManagerImpl::_copyTextPath(SPTextPath *tp)
+{
+ SPItem *path = sp_textpath_get_path_item(tp);
+ if (!path) {
+ return;
+ }
+ // textpaths that aren't in defs (on the canvas) shouldn't be copied because if
+ // both objects are being copied already, this ends up stealing the refs id.
+ if(path->parent && SP_IS_DEFS(path->parent)) {
+ _copyIgnoreDup(path->getRepr(), _doc, _defs);
+ }
+}
+
+
+/**
+ * Copy a single XML node from one document to another.
+ * @param node The node to be copied
+ * @param target_doc The document to which the node is to be copied
+ * @param parent The node in the target document which will become the parent of the copied node
+ * @return Pointer to the copied node
+ */
+Inkscape::XML::Node *ClipboardManagerImpl::_copyNode(Inkscape::XML::Node *node, Inkscape::XML::Document *target_doc, Inkscape::XML::Node *parent)
+{
+ Inkscape::XML::Node *dup = node->duplicate(target_doc);
+ parent->appendChild(dup);
+ Inkscape::GC::release(dup);
+ return dup;
+}
+
+Inkscape::XML::Node *ClipboardManagerImpl::_copyIgnoreDup(Inkscape::XML::Node *node, Inkscape::XML::Document *target_doc, Inkscape::XML::Node *parent)
+{
+ if (sp_repr_lookup_child(_root, "id", node->attribute("id"))) {
+ // node already copied
+ return nullptr;
+ }
+ Inkscape::XML::Node *dup = node->duplicate(target_doc);
+ parent->appendChild(dup);
+ Inkscape::GC::release(dup);
+ return dup;
+}
+
+
+/**
+ * Retrieve a bitmap image from the clipboard and paste it into the active document.
+ */
+bool ClipboardManagerImpl::_pasteImage(SPDocument *doc)
+{
+ if ( doc == nullptr ) {
+ return false;
+ }
+
+ // retrieve image data
+ Glib::RefPtr<Gdk::Pixbuf> img = _clipboard->wait_for_image();
+ 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;
+ }
+
+ SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item);
+ if (lpeitem && effectstack) {
+ std::istringstream iss(effectstack);
+ std::string href;
+ while (std::getline(iss, href, ';'))
+ {
+ SPObject *obj = sp_uri_reference_resolve(_clipboardSPDoc.get(), href.c_str());
+ if (!obj) {
+ return;
+ }
+ LivePathEffectObject *lpeobj = dynamic_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);
+}
+
+
+/**
+ * Callback called when some other application requests data from Inkscape.
+ *
+ * Finds a suitable output extension to save the internal clipboard document,
+ * then saves it to memory and sets the clipboard contents.
+ */
+void ClipboardManagerImpl::_onGet(Gtk::SelectionData &sel, guint /*info*/)
+{
+ if (_clipboardSPDoc == nullptr)
+ return;
+
+ Glib::ustring target = sel.get_target();
+ if (target == "") {
+ return; // this shouldn't happen
+ }
+
+ if (target == CLIPBOARD_TEXT_TARGET) {
+ target = "image/x-inkscape-svg";
+ }
+
+ // 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 {
+ // TODO: In the future we may want to detect raster image types such as jpeg
+ // and use export_raster to get the right output for some programs.
+ if (target == "image/png")
+ {
+ gdouble dpi = Inkscape::Util::Quantity::convert(1, "in", "px");
+ guint32 bgcolor = 0x00000000;
+
+ Geom::Point origin (_clipboardSPDoc->getRoot()->x.computed, _clipboardSPDoc->getRoot()->y.computed);
+ Geom::Rect area = Geom::Rect(origin, origin + _clipboardSPDoc->getDimensions());
+
+ unsigned long int width = (unsigned long int) (area.width() + 0.5);
+ unsigned long int height = (unsigned long int) (area.height() + 0.5);
+
+ // read from namedview
+ Inkscape::XML::Node *nv = _clipboardSPDoc->getReprNamedView();
+ if (nv && nv->attribute("pagecolor")) {
+ bgcolor = sp_svg_read_color(nv->attribute("pagecolor"), 0xffffff00);
+ }
+ if (nv && nv->attribute("inkscape:pageopacity")) {
+ double opacity = nv->getAttributeDouble("inkscape:pageopacity", 1.0);
+ bgcolor |= SP_COLOR_F_TO_U(opacity);
+ }
+ std::vector<SPItem*> x;
+ sp_export_png_file(_clipboardSPDoc.get(), filename, area, width, height, dpi, dpi, bgcolor, nullptr,
+ nullptr, true, x);
+ }
+ else
+ {
+ 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);
+ }
+
+ (*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 != nullptr ) {
+ // Explicit delete required to free SPDocument
+ // see https://gitlab.com/inkscape/inkscape/-/issues/2723
+ delete _clipboardSPDoc.release();
+ _defs = nullptr;
+ _doc = nullptr;
+ _root = nullptr;
+ _clipnode = nullptr;
+ }
+}
+
+
+/**
+ * Get the scale to resize an item, based on the command and desktop state.
+ */
+Geom::Scale ClipboardManagerImpl::_getScale(SPDesktop *desktop, Geom::Point const &min, Geom::Point const &max, Geom::Rect const &obj_rect, bool apply_x, bool apply_y)
+{
+ double scale_x = 1.0;
+ double scale_y = 1.0;
+
+ if (apply_x) {
+ scale_x = (max[Geom::X] - min[Geom::X]) / obj_rect[Geom::X].extent();
+ }
+ if (apply_y) {
+ scale_y = (max[Geom::Y] - min[Geom::Y]) / obj_rect[Geom::Y].extent();
+ }
+ // If the "lock aspect ratio" button is pressed and we paste only a single coordinate,
+ // resize the second one by the same ratio too
+ if (desktop && desktop->isToolboxButtonActive("lock")) {
+ if (apply_x && !apply_y) {
+ scale_y = scale_x;
+ }
+ if (apply_y && !apply_x) {
+ scale_x = scale_y;
+ }
+ }
+
+ return Geom::Scale(scale_x, scale_y);
+}
+
+
+/**
+ * Find the most suitable clipboard target.
+ */
+Glib::ustring ClipboardManagerImpl::_getBestTarget()
+{
+ auto targets = _clipboard->wait_for_targets();
+
+ // clipboard target debugging snippet
+ /*
+ g_message("Begin clipboard targets");
+ for ( std::list<Glib::ustring>::iterator x = targets.begin() ; x != targets.end(); ++x )
+ g_message("Clipboard target: %s", (*x).data());
+ g_message("End clipboard targets\n");
+ //*/
+
+ for (auto & _preferred_target : _preferred_targets)
+ {
+ if ( std::find(targets.begin(), targets.end(), _preferred_target) != targets.end() ) {
+ return _preferred_target;
+ }
+ }
+#ifdef _WIN32
+ if (OpenClipboard(NULL))
+ { // If both bitmap and metafile are present, pick the one that was exported first.
+ UINT format = EnumClipboardFormats(0);
+ while (format) {
+ if (format == CF_ENHMETAFILE || format == CF_DIB || format == CF_BITMAP) {
+ break;
+ }
+ format = EnumClipboardFormats(format);
+ }
+ CloseClipboard();
+
+ if (format == CF_ENHMETAFILE) {
+ return "CF_ENHMETAFILE";
+ }
+ if (format == CF_DIB || format == CF_BITMAP) {
+ return CLIPBOARD_GDK_PIXBUF_TARGET;
+ }
+ }
+
+ if (IsClipboardFormatAvailable(CF_ENHMETAFILE)) {
+ return "CF_ENHMETAFILE";
+ }
+#endif
+ if (_clipboard->wait_is_image_available()) {
+ return CLIPBOARD_GDK_PIXBUF_TARGET;
+ }
+ if (_clipboard->wait_is_text_available()) {
+ return CLIPBOARD_TEXT_TARGET;
+ }
+
+ return "";
+}
+
+
+/**
+ * Set the clipboard targets to reflect the mimetypes Inkscape can output.
+ */
+void ClipboardManagerImpl::_setClipboardTargets()
+{
+ Inkscape::Extension::DB::OutputList outlist;
+ Inkscape::Extension::db.get_output_list(outlist);
+ std::vector<Gtk::TargetEntry> target_list;
+
+ bool plaintextSet = false;
+ for (Inkscape::Extension::DB::OutputList::const_iterator out = outlist.begin() ; out != outlist.end() ; ++out) {
+ if ( !(*out)->deactivated() ) {
+ Glib::ustring mime = (*out)->get_mimetype();
+ if (mime != CLIPBOARD_TEXT_TARGET) {
+ if ( !plaintextSet && (mime.find("svg") == Glib::ustring::npos) ) {
+ target_list.emplace_back(CLIPBOARD_TEXT_TARGET);
+ plaintextSet = true;
+ }
+ target_list.emplace_back(mime);
+ }
+ }
+ }
+
+ // Add PNG export explicitly since there is no extension for this...
+ // On Windows, GTK will also present this as a CF_DIB/CF_BITMAP
+ target_list.emplace_back( "image/png" );
+
+ _clipboard->set(target_list,
+ sigc::mem_fun(*this, &ClipboardManagerImpl::_onGet),
+ sigc::mem_fun(*this, &ClipboardManagerImpl::_onClear));
+
+#ifdef _WIN32
+ // If the "image/x-emf" target handled by the emf extension would be
+ // presented as a CF_ENHMETAFILE automatically (just like an "image/bmp"
+ // is presented as a CF_BITMAP) this code would not be needed.. ???
+ // Or maybe there is some other way to achieve the same?
+
+ // Note: Metafile is the only format that is rendered and stored in clipboard
+ // on Copy, all other formats are rendered only when needed by a Paste command.
+
+ // FIXME: This should at least be rewritten to use "delayed rendering".
+ // If possible make it delayed rendering by using GTK API only.
+
+ if (OpenClipboard(NULL)) {
+ if ( _clipboardSPDoc != NULL ) {
+ const Glib::ustring target = "image/x-emf";
+
+ Inkscape::Extension::DB::OutputList outlist;
+ Inkscape::Extension::db.get_output_list(outlist);
+ Inkscape::Extension::DB::OutputList::const_iterator out = outlist.begin();
+ for ( ; out != outlist.end() && target != (*out)->get_mimetype() ; ++out) {
+ }
+ if ( out != outlist.end() ) {
+ // FIXME: Temporary hack until we add support for memory output.
+ // Save to a temporary file, read it back and then set the clipboard contents
+ gchar *filename = g_build_filename( g_get_user_cache_dir(), "inkscape-clipboard-export.emf", 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..732dd56
--- /dev/null
+++ b/src/ui/clipboard.h
@@ -0,0 +1,77 @@
+// 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>
+
+// 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) = 0;
+ virtual bool paste(SPDesktop *desktop, bool in_place = false) = 0;
+ virtual bool pasteStyle(ObjectSet *set) = 0;
+ virtual bool pasteSize(ObjectSet *set, bool separately, bool apply_x, bool apply_y) = 0;
+ virtual bool pastePathEffect(ObjectSet *set) = 0;
+ virtual Glib::ustring getPathParameter(SPDesktop* desktop) = 0;
+ virtual Glib::ustring getShapeOrTextObjectId(SPDesktop *desktop) = 0;
+ virtual std::vector<Glib::ustring> getElementsOfType(SPDesktop *desktop, gchar const* type = "*", gint maxdepth = -1) = 0;
+ virtual 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/contextmenu.cpp b/src/ui/contextmenu.cpp
new file mode 100644
index 0000000..221a3e6
--- /dev/null
+++ b/src/ui/contextmenu.cpp
@@ -0,0 +1,340 @@
+// 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");
+
+ SPItem *item = dynamic_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 = dynamic_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->selection;
+ if (object && !selection->includes(object)) {
+ selection->set(object);
+ }
+
+ gmenu_section = Gio::Menu::create();
+ AppendItemFromAction(gmenu_section, "app.cut", _("Cu_t"), "edit-cut");
+ AppendItemFromAction(gmenu_section, "app.copy", _("_Copy"), "edit-copy");
+ AppendItemFromAction(gmenu_section, "win.paste", _("_Paste"), "edit-paste");
+ gmenu->append_section(gmenu_section);
+
+ gmenu_section = Gio::Menu::create();
+ AppendItemFromAction(gmenu_section, "app.duplicate", _("Duplic_ate"), "edit-duplicate");
+ AppendItemFromAction(gmenu_section, "app.delete-selection", _("_Delete"), "edit-delete");
+ gmenu->append_section(gmenu_section);
+
+ if (item) {
+
+ // 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 (dynamic_cast<SPShape*>(item) || dynamic_cast<SPText*>(item) || dynamic_cast<SPGroup*>(item)) {
+ AppendItemFromAction(gmenu_dialogs, "win.dialog-open('FillStroke')", _("_Fill and Stroke..."), "dialog-fill-and-stroke" );
+ }
+
+ // Image dialogs (mostly).
+ if (dynamic_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" );
+ auto image = dynamic_cast<SPImage*>(item);
+ 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 (dynamic_cast<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 (!dynamic_cast<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 (dynamic_cast<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 = dynamic_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.
+#if 0
+ // Install CSS to shift icons into the space reserved for toggles (i.e. check and radio items).
+ signal_map().connect(sigc::bind<Gtk::MenuShell *>(sigc::ptr_fun(shift_icons), this));
+#endif
+
+ // Set the style and icon theme of the new menu based on the desktop
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ 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");
+ }
+ }
+}
+
+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_canvas", 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..2b15111
--- /dev/null
+++ b/src/ui/contextmenu.h
@@ -0,0 +1,54 @@
+// 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:
+ void AppendItemFromAction(Glib::RefPtr<Gio::Menu> gmenu, Glib::ustring action, Glib::ustring label, Glib::ustring icon = "");
+
+ // 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..5887974
--- /dev/null
+++ b/src/ui/cursor-utils.cpp
@@ -0,0 +1,249 @@
+// 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;
+ bool cursor_scaling = false;
+#ifndef GDK_WINDOWING_QUARTZ
+ // Default cursor size (get_default_cursor_size()) fixed to 32 on Quartz. Cursor scaling handled elsewhere.
+
+ cursor_scaling = prefs->getBool("/options/cursorscaling"); // Fractional scaling is broken but we can't detect it.
+ if (cursor_scaling) {
+ scale = window->get_scale_factor(); // Adjust for HiDPI screens.
+ }
+#endif
+ 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 theme_name : theme_names) {
+ for (auto 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;
+ }
+
+ // Explicit delete required for SPDocument to be freed
+ // see https://gitlab.com/inkscape/inkscape/-/issues/2723
+ delete document.release();
+
+ 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..0da36d5
--- /dev/null
+++ b/src/ui/desktop/menu-icon-shift.cpp
@@ -0,0 +1,153 @@
+// 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.
+ */
+void
+shift_icons(Gtk::MenuShell* menu)
+{
+ static Glib::RefPtr<Gtk::CssProvider> provider;
+ if (!provider) {
+ provider = Gtk::CssProvider::create();
+ auto const screen = Gdk::Screen::get_default();
+ Gtk::StyleContext::add_provider_for_screen(screen, provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ }
+
+ // Calculate required shift. We need an example!
+ // Search for Gtk::MenuItem -> Gtk::Box -> Gtk::Image
+ for (auto child : menu->get_children()) {
+
+ auto menuitem = dynamic_cast<Gtk::MenuItem *>(child);
+ if (menuitem) {
+
+ auto box = dynamic_cast<Gtk::Box *>(menuitem->get_child());
+ if (box) {
+
+ box->set_spacing(0); // Match ImageMenuItem
+
+ auto children = box->get_children();
+ if (children.size() == 2) { // Image + Label
+
+ auto image = dynamic_cast<Gtk::Image *>(box->get_children()[0]);
+ if (image) {
+
+ // OK, we have an example, do calculation.
+ auto allocation_menuitem = menuitem->get_allocation();
+ auto allocation_image = image->get_allocation();
+
+ int shift = -allocation_image.get_x();
+ if (menuitem->get_direction() == Gtk::TEXT_DIR_RTL) {
+ shift += (allocation_menuitem.get_width() - allocation_image.get_width());
+ }
+
+ static int current_shift = 0;
+ if (std::abs(current_shift - shift) > 2) {
+ // Only do this once per menu, and only if there is a large change.
+ current_shift = shift;
+ std::string css_str;
+ if (menuitem->get_direction() == Gtk::TEXT_DIR_RTL) {
+ css_str = "menuitem box image {margin-right:" + std::to_string(shift) + "px;}";
+ } else {
+ css_str = "menuitem box image {margin-left:" + std::to_string(shift) + "px;}";
+ }
+ provider->load_from_data(css_str);
+ }
+ }
+ }
+ }
+ }
+ }
+ // If we get here, it means there were no examples... but we don't care as there are no icons to shift anyway.
+}
+
+/*
+ * Find all submenus to add "shift_icons" callback. We need to do this for
+ * all submenus as some submenus are children of other submenus without icons.
+ */
+void
+shift_icons_recursive(Gtk::MenuShell *menu)
+{
+ if (menu) {
+
+ // Connect signal
+ menu->signal_map().connect(sigc::bind<Gtk::MenuShell *>(sigc::ptr_fun(&shift_icons), menu));
+
+ static auto app = InkscapeApplication::instance();
+ auto label_to_tooltip_map = app->get_menu_label_to_tooltip_map();
+
+ // Look for descendent menus.
+ auto children = menu->get_children(); // Should be Gtk::MenuItem's
+ for (auto child : children) {
+ auto menuitem = dynamic_cast<Gtk::MenuItem *>(child);
+ if (menuitem) {
+
+ // Use label as alternative way to figure out tooltip.
+ auto label = menuitem->get_label();
+ if (label.empty()) {
+ auto container = menuitem->get_child();
+ auto box = dynamic_cast<Gtk::Box *>(container);
+ if (box) {
+ 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 = label_widget->get_label();
+ }
+ }
+ }
+ }
+
+ auto it = label_to_tooltip_map.find(label);
+ if (it != label_to_tooltip_map.end()) {
+ menuitem->set_tooltip_text(it->second);
+ }
+
+ auto submenu = menuitem->get_submenu();
+ if (submenu) {
+ shift_icons_recursive(submenu);
+ }
+ }
+ }
+ }
+}
+
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ 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..78462a8
--- /dev/null
+++ b/src/ui/desktop/menu-icon-shift.h
@@ -0,0 +1,46 @@
+// 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).
+ */
+void shift_icons(Gtk::MenuShell *menu);
+
+/**
+ * Add callbacks recursively to menus.
+ */
+void shift_icons_recursive(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..a686070
--- /dev/null
+++ b/src/ui/desktop/menubar.cpp
@@ -0,0 +1,322 @@
+// 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 "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();
+ 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;
+
+ 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() << 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..59009a6
--- /dev/null
+++ b/src/ui/dialog-events.cpp
@@ -0,0 +1,240 @@
+// 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 "include/macros.h"
+#include "ui/dialog-events.h"
+#include "ui/tools/tool-base.h"
+
+
+/**
+ * Remove focus from window to whoever it is transient for.
+ */
+void sp_dialog_defocus_cpp(Gtk::Window *win)
+{
+ //find out the document window we're transient for
+ Gtk::Window *w = win->get_transient_for();
+
+ //switch to it
+ if (w) {
+ w->present();
+ }
+}
+
+void
+sp_dialog_defocus (GtkWindow *win)
+{
+ GtkWindow *w;
+ //find out the document window we're transient for
+ w = gtk_window_get_transient_for(GTK_WINDOW(win));
+ //switch to it
+
+ if (w) {
+ gtk_window_present (w);
+ }
+}
+
+
+/**
+ * Callback to defocus a widget's parent dialog.
+ */
+void sp_dialog_defocus_callback_cpp(Gtk::Entry *e)
+{
+ sp_dialog_defocus_cpp(dynamic_cast<Gtk::Window *>(e->get_toplevel()));
+}
+
+void
+sp_dialog_defocus_callback (GtkWindow * /*win*/, gpointer data)
+{
+ sp_dialog_defocus( GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(data))) );
+}
+
+
+
+void
+sp_dialog_defocus_on_enter_cpp (Gtk::Entry *e)
+{
+ e->signal_activate().connect(sigc::bind(sigc::ptr_fun(&sp_dialog_defocus_callback_cpp), e));
+}
+
+void
+sp_dialog_defocus_on_enter (GtkWidget *w)
+{
+ g_signal_connect ( G_OBJECT (w), "activate",
+ G_CALLBACK (sp_dialog_defocus_callback), w );
+}
+
+
+
+gboolean
+sp_dialog_event_handler (GtkWindow *win, GdkEvent *event, gpointer data)
+{
+ gboolean ret = FALSE;
+
+ switch (event->type) {
+
+ case GDK_KEY_PRESS:
+
+ switch (Inkscape::UI::Tools::get_latin_keyval (&event->key)) {
+ case GDK_KEY_Escape:
+ sp_dialog_defocus (win);
+ ret = TRUE;
+ break;
+ case GDK_KEY_F4:
+ case GDK_KEY_w:
+ case GDK_KEY_W:
+ // close dialog
+ if (MOD__CTRL_ONLY(event)) {
+
+ /* this code sends a delete_event to the dialog,
+ * instead of just destroying it, so that the
+ * dialog can do some housekeeping, such as remember
+ * its position.
+ */
+ GdkEventAny event;
+ GtkWidget *widget = GTK_WIDGET(win);
+ event.type = GDK_DELETE;
+ event.window = gtk_widget_get_window (widget);
+ event.send_event = TRUE;
+ g_object_ref (G_OBJECT (event.window));
+ gtk_main_do_event(reinterpret_cast<GdkEvent*>(&event));
+ g_object_unref (G_OBJECT (event.window));
+
+ ret = TRUE;
+ }
+ break;
+ default: // pass keypress to the canvas
+ break;
+ }
+ default:
+ ;
+ }
+
+ return ret;
+
+}
+
+
+
+/**
+ * Make the argument dialog transient to the currently active document
+ * window.
+ */
+void sp_transientize(GtkWidget *dialog)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+#ifndef _WIN32 // FIXME: Temporary Win32 special code to enable transient dialogs
+ // _set_skip_taskbar_hint makes transient dialogs NON-transient! When dialogs
+ // are made transient (_set_transient_for), they are already removed from
+ // the taskbar in Win32.
+ if (prefs->getBool( "/options/dialogsskiptaskbar/value")) {
+ gtk_window_set_skip_taskbar_hint (GTK_WINDOW (dialog), TRUE);
+ }
+#endif
+
+ gint transient_policy = prefs->getIntLimited("/options/transientpolicy/value", 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);
+ }
+ }
+} // end of sp_transientize()
+
+void on_transientize (SPDesktop *desktop, win_data *wd )
+{
+ sp_transientize_callback (desktop, wd);
+}
+
+void
+sp_transientize_callback ( SPDesktop *desktop, win_data *wd )
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ gint transient_policy = prefs->getIntLimited("/options/transientpolicy/value", 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_NORMAL;
+#endif
+
+ if (!transient_policy)
+ return;
+
+ if (wd->win)
+ {
+ desktop->setWindowTransient (wd->win, transient_policy);
+ }
+}
+
+void on_dialog_hide (GtkWidget *w)
+{
+ if (w)
+ gtk_widget_hide (w);
+}
+
+void on_dialog_unhide (GtkWidget *w)
+{
+ if (w)
+ gtk_widget_show (w);
+}
+
+gboolean
+sp_dialog_hide(GObject * /*object*/, gpointer data)
+{
+ GtkWidget *dlg = GTK_WIDGET(data);
+
+ if (dlg)
+ gtk_widget_hide (dlg);
+
+ return TRUE;
+}
+
+
+
+gboolean
+sp_dialog_unhide(GObject * /*object*/, gpointer data)
+{
+ GtkWidget *dlg = GTK_WIDGET(data);
+
+ if (dlg)
+ gtk_widget_show (dlg);
+
+ return TRUE;
+}
+
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog-events.h b/src/ui/dialog-events.h
new file mode 100644
index 0000000..7bc7483
--- /dev/null
+++ b/src/ui/dialog-events.h
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Event handler for dialog windows
+ */
+/* Authors:
+ * bulia byak <bulia@dr.com>
+ *
+ * Copyright (C) 2003-2014 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_DIALOG_EVENTS_H
+#define SEEN_DIALOG_EVENTS_H
+
+#include <gtk/gtk.h>
+
+/*
+ * event callback can only accept one argument, but we need two,
+ * hence this struct.
+ * each dialog has a local static copy:
+ * win is the dialog window
+ * stop is the transientize semaphore: when 0, retransientizing this dialog
+ * is allowed
+ */
+
+namespace Gtk {
+class Window;
+class Entry;
+}
+
+class SPDesktop;
+
+struct win_data {
+ GtkWidget *win;
+ guint stop;
+};
+
+
+gboolean sp_dialog_event_handler ( GtkWindow *win,
+ GdkEvent *event,
+ gpointer data );
+
+void sp_dialog_defocus_cpp (Gtk::Window *win);
+void sp_dialog_defocus_callback_cpp(Gtk::Entry *e);
+void sp_dialog_defocus_on_enter_cpp(Gtk::Entry *e);
+
+void sp_dialog_defocus ( GtkWindow *win );
+void sp_dialog_defocus_callback ( GtkWindow *win, gpointer data );
+void sp_dialog_defocus_on_enter ( GtkWidget *w );
+void sp_transientize ( GtkWidget *win );
+
+void on_transientize ( SPDesktop *desktop,
+ win_data *wd );
+
+void sp_transientize_callback ( SPDesktop *desktop,
+ win_data *wd );
+
+void on_dialog_hide (GtkWidget *w);
+void on_dialog_unhide (GtkWidget *w);
+
+//gboolean sp_dialog_hide (GObject *object, gpointer data);
+//gboolean sp_dialog_unhide (GObject *object, gpointer data);
+
+#endif
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/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..89c3551
--- /dev/null
+++ b/src/ui/dialog/about.cpp
@@ -0,0 +1,200 @@
+// 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));
+ }
+
+ // 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..cc6c58b
--- /dev/null
+++ b/src/ui/dialog/align-and-distribute.cpp
@@ -0,0 +1,304 @@
+// 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.
+
+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 << " 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);
+ }
+}
+
+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..15cfdd9
--- /dev/null
+++ b/src/ui/dialog/align-and-distribute.h
@@ -0,0 +1,95 @@
+// 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>
+
+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;
+
+};
+
+} // 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..4d792b5
--- /dev/null
+++ b/src/ui/dialog/attrdialog.cpp
@@ -0,0 +1,711 @@
+// 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 "selection.h"
+#include "document-undo.h"
+#include "message-context.h"
+#include "message-stack.h"
+#include "style.h"
+
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "ui/widget/iconrenderer.h"
+
+#include "xml/node-event-vector.h"
+#include "xml/attribute-record.h"
+
+#include <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.h>
+
+/**
+ * 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 void on_attr_changed (Inkscape::XML::Node * repr,
+ const gchar * name,
+ const gchar * /*old_value*/,
+ const gchar * new_value,
+ bool /*is_interactive*/,
+ gpointer data)
+{
+ ATTR_DIALOG(data)->onAttrChanged(repr, name, new_value);
+}
+
+static void on_content_changed (Inkscape::XML::Node * repr,
+ gchar const * oldcontent,
+ gchar const * newcontent,
+ gpointer data)
+{
+ auto self = ATTR_DIALOG(data);
+ auto buffer = self->_content_tv->get_buffer();
+ if (!buffer->get_modified()) {
+ const char *c = repr->content();
+ buffer->set_text(c ? c : "");
+ }
+ buffer->set_modified(false);
+}
+
+Inkscape::XML::NodeEventVector _repr_events = {
+ nullptr, /* child_added */
+ nullptr, /* child_removed */
+ on_attr_changed,
+ on_content_changed, /* content_changed */
+ nullptr /* order_changed */
+};
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+static gboolean key_callback(GtkWidget *widget, GdkEventKey *event, AttrDialog *attrdialog);
+/**
+ * Constructor
+ * A treeview whose each row corresponds to an XML attribute of a selected node
+ * New attribute can be added by clicking '+' at bottom of the attr pane. '-'
+ */
+AttrDialog::AttrDialog()
+ : DialogBase("/dialogs/attr", "AttrDialog")
+ , _repr(nullptr)
+ , _mainBox(Gtk::ORIENTATION_VERTICAL)
+ , status_box(Gtk::ORIENTATION_HORIZONTAL)
+{
+ set_size_request(20, 15);
+ _mainBox.pack_start(_scrolledWindow, Gtk::PACK_EXPAND_WIDGET);
+
+ // For text and comment nodes
+ _content_tv = Gtk::manage(new Gtk::TextView());
+ _content_tv->show();
+ _content_tv->set_wrap_mode(Gtk::WrapMode::WRAP_CHAR);
+ _content_tv->set_monospace(true);
+ _content_tv->set_border_width(4);
+ _content_tv->set_buffer(Gtk::TextBuffer::create());
+ _content_tv->get_buffer()->signal_end_user_action().connect([this]() {
+ if (_repr) {
+ _repr->setContent(_content_tv->get_buffer()->get_text().c_str());
+ setUndo(_("Type text"));
+ }
+ });
+ _content_sw = Gtk::manage(new Gtk::ScrolledWindow());
+ _content_sw->hide();
+ _content_sw->set_no_show_all();
+ _content_sw->add(*_content_tv);
+ _mainBox.pack_start(*_content_sw);
+
+ // For element nodes
+ _treeView.set_headers_visible(true);
+ _treeView.set_hover_selection(true);
+ _treeView.set_activate_on_single_click(true);
+ _treeView.set_can_focus(false);
+ _scrolledWindow.add(_treeView);
+ _scrolledWindow.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+
+ _store = Gtk::ListStore::create(_attrColumns);
+ _treeView.set_model(_store);
+
+ Inkscape::UI::Widget::IconRenderer * addRenderer = manage(new Inkscape::UI::Widget::IconRenderer());
+ addRenderer->add_icon("edit-delete");
+
+ _treeView.append_column("", *addRenderer);
+ Gtk::TreeViewColumn *col = _treeView.get_column(0);
+ if (col) {
+ auto add_icon = Gtk::manage(sp_get_icon_image("list-add", Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ col->set_clickable(true);
+ col->set_widget(*add_icon);
+ add_icon->set_tooltip_text(_("Add a new attribute"));
+ add_icon->show();
+ auto button = add_icon->get_parent()->get_parent()->get_parent();
+ // Assign the button event so that create happens BEFORE delete. If this code
+ // isn't in this exact way, the onAttrDelete is called when the header lines are pressed.
+ button->signal_button_release_event().connect(sigc::mem_fun(*this, &AttrDialog::onAttrCreate), false);
+ }
+ addRenderer->signal_activated().connect(sigc::mem_fun(*this, &AttrDialog::onAttrDelete));
+ _treeView.signal_key_press_event().connect(sigc::mem_fun(*this, &AttrDialog::onKeyPressed));
+ _treeView.set_search_column(-1);
+
+ _nameRenderer = Gtk::manage(new Gtk::CellRendererText());
+ _nameRenderer->property_editable() = true;
+ _nameRenderer->property_placeholder_text().set_value(_("Attribute Name"));
+ _nameRenderer->signal_edited().connect(sigc::mem_fun(*this, &AttrDialog::nameEdited));
+ _nameRenderer->signal_editing_started().connect(sigc::mem_fun(*this, &AttrDialog::startNameEdit));
+ _treeView.append_column(_("Name"), *_nameRenderer);
+ _nameCol = _treeView.get_column(1);
+ if (_nameCol) {
+ _nameCol->set_resizable(true);
+ _nameCol->add_attribute(_nameRenderer->property_text(), _attrColumns._attributeName);
+ }
+ status.set_halign(Gtk::ALIGN_START);
+ status.set_valign(Gtk::ALIGN_CENTER);
+ status.set_size_request(1, -1);
+ status.set_markup("");
+ status.set_line_wrap(true);
+ status.get_style_context()->add_class("inksmall");
+ status_box.pack_start(status, TRUE, TRUE, 0);
+ pack_end(status_box, false, false, 2);
+
+ _message_stack = std::make_shared<Inkscape::MessageStack>();
+ _message_context = std::unique_ptr<Inkscape::MessageContext>(new Inkscape::MessageContext(_message_stack));
+ _message_changed_connection =
+ _message_stack->connectChanged(sigc::bind(sigc::ptr_fun(_set_status_message), GTK_WIDGET(status.gobj())));
+
+ _valueRenderer = Gtk::manage(new Gtk::CellRendererText());
+ _valueRenderer->property_editable() = true;
+ _valueRenderer->property_placeholder_text().set_value(_("Attribute Value"));
+ _valueRenderer->property_ellipsize().set_value(Pango::ELLIPSIZE_END);
+ _valueRenderer->signal_edited().connect(sigc::mem_fun(*this, &AttrDialog::valueEdited));
+ _valueRenderer->signal_editing_started().connect(sigc::mem_fun(*this, &AttrDialog::startValueEdit));
+ _treeView.append_column(_("Value"), *_valueRenderer);
+ _valueCol = _treeView.get_column(2);
+ if (_valueCol) {
+ _valueCol->add_attribute(_valueRenderer->property_text(), _attrColumns._attributeValueRender);
+ }
+ _popover = Gtk::manage(new Gtk::Popover());
+ Gtk::Box *vbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
+ Gtk::Box *hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
+ _textview = Gtk::manage(new Gtk::TextView());
+ _textview->set_wrap_mode(Gtk::WrapMode::WRAP_CHAR);
+ _textview->set_editable(true);
+ _textview->set_monospace(true);
+ _textview->set_border_width(6);
+ _textview->signal_map().connect(sigc::mem_fun(*this, &AttrDialog::textViewMap));
+ Glib::RefPtr<Gtk::TextBuffer> textbuffer = Gtk::TextBuffer::create();
+ textbuffer->set_text("");
+ _textview->set_buffer(textbuffer);
+ _scrolled_text_view.add(*_textview);
+ _scrolled_text_view.set_max_content_height(450);
+ _scrolled_text_view.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ _scrolled_text_view.set_propagate_natural_width(true);
+ Gtk::Label *helpreturn = Gtk::manage(new Gtk::Label(_("Shift+Return for a new line")));
+ helpreturn->get_style_context()->add_class("inksmall");
+ Gtk::Button *apply = Gtk::manage(new Gtk::Button());
+ Gtk::Image *icon = Gtk::manage(sp_get_icon_image("on-outline", 26));
+ apply->set_relief(Gtk::RELIEF_NONE);
+ icon->show();
+ apply->add(*icon);
+ apply->signal_clicked().connect(sigc::mem_fun(*this, &AttrDialog::valueEditedPop));
+ Gtk::Button *cancel = Gtk::manage(new Gtk::Button());
+ icon = Gtk::manage(sp_get_icon_image("off-outline", 26));
+ cancel->set_relief(Gtk::RELIEF_NONE);
+ icon->show();
+ cancel->add(*icon);
+ cancel->signal_clicked().connect(sigc::mem_fun(*this, &AttrDialog::valueCanceledPop));
+ hbox->pack_end(*apply, Gtk::PACK_SHRINK, 3);
+ hbox->pack_end(*cancel, Gtk::PACK_SHRINK, 3);
+ hbox->pack_end(*helpreturn, Gtk::PACK_SHRINK, 3);
+ vbox->pack_start(_scrolled_text_view, Gtk::PACK_EXPAND_WIDGET, 3);
+ vbox->pack_start(*hbox, Gtk::PACK_EXPAND_WIDGET, 3);
+ _popover->add(*vbox);
+ _popover->show();
+ _popover->set_relative_to(_treeView);
+ _popover->set_position(Gtk::PositionType::POS_BOTTOM);
+ _popover->signal_closed().connect(sigc::mem_fun(*this, &AttrDialog::popClosed));
+ _popover->get_style_context()->add_class("attrpop");
+ attr_reset_context(0);
+ pack_start(_mainBox, Gtk::PACK_EXPAND_WIDGET);
+ // I couldn't get the signal go well not using C way signals
+ g_signal_connect(GTK_WIDGET(_popover->gobj()), "key-press-event", G_CALLBACK(key_callback), this);
+ _popover->hide();
+ _updating = false;
+}
+
+void AttrDialog::textViewMap()
+{
+ auto vscroll = _scrolled_text_view.get_vadjustment();
+ int height = vscroll->get_upper() + 12; // padding 6+6
+ if (height < 450) {
+ _scrolled_text_view.set_min_content_height(height);
+ vscroll->set_value(vscroll->get_lower());
+ } else {
+ _scrolled_text_view.set_min_content_height(450);
+ }
+}
+
+gboolean sp_show_pop_map(gpointer data)
+{
+ AttrDialog *attrdialog = reinterpret_cast<AttrDialog *>(data);
+ attrdialog->textViewMap();
+ return FALSE;
+}
+
+static gboolean key_callback(GtkWidget *widget, GdkEventKey *event, AttrDialog *attrdialog)
+{
+ switch (event->keyval) {
+ case GDK_KEY_Return:
+ case GDK_KEY_KP_Enter: {
+ if (attrdialog->_popover->is_visible()) {
+ if (!(event->state & GDK_SHIFT_MASK)) {
+ attrdialog->valueEditedPop();
+ attrdialog->_popover->hide();
+ return true;
+ } else {
+ g_timeout_add(50, &sp_show_pop_map, attrdialog);
+ }
+ }
+ } break;
+ }
+ return false;
+}
+
+/**
+ * Prepare value string suitable for display in a Gtk::CellRendererText
+ *
+ * Value is truncated at the first new line character (if any) and a visual indicator and ellipsis is added.
+ * Overall length is limited as well to prevent performance degradation for very long values.
+ *
+ * @param value Raw attribute value as UTF-8 encoded string
+ * @return Single-line string with fixed maximum length
+ */
+static Glib::ustring prepare_rendervalue(const char *value)
+{
+ constexpr int MAX_LENGTH = 500; // maximum length of string before it's truncated for performance reasons
+ // ~400 characters fit horizontally on a WQHD display, so 500 should be plenty
+
+ Glib::ustring renderval;
+
+ // truncate to MAX_LENGTH
+ if (g_utf8_strlen(value, -1) > MAX_LENGTH) {
+ renderval = Glib::ustring(value, MAX_LENGTH) + "…";
+ } else {
+ renderval = value;
+ }
+
+ // truncate at first newline (if present) and add a visual indicator
+ auto ind = renderval.find('\n');
+ if (ind != Glib::ustring::npos) {
+ renderval.replace(ind, Glib::ustring::npos, " ⏎ …");
+ }
+
+ return renderval;
+}
+
+
+/**
+ * @brief AttrDialog::~AttrDialog
+ * Class destructor
+ */
+AttrDialog::~AttrDialog()
+{
+ _message_changed_connection.disconnect();
+ _message_context = nullptr;
+ _message_stack = nullptr;
+}
+
+void AttrDialog::startNameEdit(Gtk::CellEditable *cell, const Glib::ustring &path)
+{
+ Gtk::Entry *entry = dynamic_cast<Gtk::Entry *>(cell);
+ entry->signal_key_press_event().connect(sigc::bind(sigc::mem_fun(*this, &AttrDialog::onNameKeyPressed), entry));
+}
+
+
+gboolean sp_show_attr_pop(gpointer data)
+{
+ AttrDialog *attrdialog = reinterpret_cast<AttrDialog *>(data);
+ attrdialog->_popover->show_all();
+
+ return FALSE;
+}
+
+gboolean sp_close_entry(gpointer data)
+{
+ Gtk::CellEditable *cell = reinterpret_cast<Gtk::CellEditable *>(data);
+ if (cell) {
+ cell->property_editing_canceled() = true;
+ cell->remove_widget();
+ }
+ return FALSE;
+}
+
+void AttrDialog::startValueEdit(Gtk::CellEditable *cell, const Glib::ustring &path)
+{
+ Gtk::Entry *entry = dynamic_cast<Gtk::Entry *>(cell);
+ int width = 0;
+ int height = 0;
+ int colwidth = _valueCol->get_width();
+ _textview->set_size_request(510, -1);
+ _popover->set_size_request(520, -1);
+ valuepath = path;
+ entry->get_layout()->get_pixel_size(width, height);
+ Gtk::TreeIter iter = *_store->get_iter(path);
+ Gtk::TreeModel::Row row = *iter;
+ if (row && this->_repr) {
+ Glib::ustring name = row[_attrColumns._attributeName];
+ if (row[_attrColumns._attributeValue] != row[_attrColumns._attributeValueRender] || colwidth - 10 < width) {
+ valueediting = entry->get_text();
+ Gdk::Rectangle rect;
+ _treeView.get_cell_area((Gtk::TreeModel::Path)iter, *_valueCol, rect);
+ if (_popover->get_position() == Gtk::PositionType::POS_BOTTOM) {
+ rect.set_y(rect.get_y() + 20);
+ }
+ _popover->set_pointing_to(rect);
+ Glib::RefPtr<Gtk::TextBuffer> textbuffer = Gtk::TextBuffer::create();
+ textbuffer->set_text(row[_attrColumns._attributeValue]);
+ _textview->set_buffer(textbuffer);
+ g_timeout_add(50, &sp_close_entry, cell);
+ g_timeout_add(50, &sp_show_attr_pop, this);
+ } else {
+ entry->signal_key_press_event().connect(
+ sigc::bind(sigc::mem_fun(*this, &AttrDialog::onValueKeyPressed), entry));
+ }
+ }
+}
+
+void AttrDialog::popClosed()
+{
+ Glib::RefPtr<Gtk::TextBuffer> textbuffer = Gtk::TextBuffer::create();
+ textbuffer->set_text("");
+ _textview->set_buffer(textbuffer);
+ _scrolled_text_view.set_min_content_height(20);
+}
+
+/**
+ * @brief AttrDialog::setRepr
+ * Set the internal xml object that I'm working on right now.
+ */
+void AttrDialog::setRepr(Inkscape::XML::Node * repr)
+{
+ if ( repr == _repr ) return;
+ if (_repr) {
+ _store->clear();
+ _repr->removeListenerByData(this);
+ Inkscape::GC::release(_repr);
+ _repr = nullptr;
+ }
+ _repr = repr;
+ if (repr) {
+ Inkscape::GC::anchor(_repr);
+ _repr->addListener(&_repr_events, this);
+ _repr->synthesizeEvents(&_repr_events, this);
+
+ // show either attributes or content
+ bool show_content = is_text_or_comment_node(*_repr);
+ _scrolledWindow.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"));
+}
+
+void AttrDialog::_set_status_message(Inkscape::MessageType /*type*/, const gchar *message, GtkWidget *widget)
+{
+ if (widget) {
+ gtk_label_set_markup(GTK_LABEL(widget), message ? message : "");
+ }
+}
+
+/**
+ * Sets the AttrDialog status bar, depending on which attr is selected.
+ */
+void AttrDialog::attr_reset_context(gint attr)
+{
+ if (attr == 0) {
+ _message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> attribute to edit."));
+ } else {
+ const gchar *name = g_quark_to_string(attr);
+ _message_context->setF(
+ Inkscape::NORMAL_MESSAGE,
+ _("Attribute <b>%s</b> selected. Press <b>Ctrl+Enter</b> when done editing to commit changes."), name);
+ }
+}
+
+/**
+ * @brief AttrDialog::onAttrChanged
+ * This is called when the XML has an updated attribute
+ */
+void AttrDialog::onAttrChanged(Inkscape::XML::Node *repr, const gchar * name, const gchar * new_value)
+{
+ if (_updating) {
+ return;
+ }
+ Glib::ustring renderval;
+ if (new_value) {
+ renderval = prepare_rendervalue(new_value);
+ }
+ for(auto iter: this->_store->children())
+ {
+ Gtk::TreeModel::Row row = *iter;
+ Glib::ustring col_name = row[_attrColumns._attributeName];
+ if(name == col_name) {
+ if(new_value) {
+ row[_attrColumns._attributeValue] = new_value;
+ row[_attrColumns._attributeValueRender] = renderval;
+ new_value = nullptr; // Don't make a new one
+ } else {
+ _store->erase(iter);
+ }
+ break;
+ }
+ }
+ if (new_value) {
+ Gtk::TreeModel::Row row = *(_store->prepend());
+ row[_attrColumns._attributeName] = name;
+ row[_attrColumns._attributeValue] = new_value;
+ row[_attrColumns._attributeValueRender] = renderval;
+ }
+}
+
+/**
+ * @brief AttrDialog::onAttrCreate
+ * This function is a slot to signal_clicked for '+' button panel.
+ */
+bool AttrDialog::onAttrCreate(GdkEventButton *event)
+{
+ if(event->type == GDK_BUTTON_RELEASE && event->button == 1 && this->_repr) {
+ Gtk::TreeIter iter = _store->prepend();
+ Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iter;
+ _treeView.set_cursor(path, *_nameCol, true);
+ grab_focus();
+ return true;
+ }
+ return false;
+}
+
+/**
+ * @brief AttrDialog::onAttrDelete
+ * @param event
+ * @return true
+ * Delete the attribute from the xml
+ */
+void AttrDialog::onAttrDelete(Glib::ustring path)
+{
+ Gtk::TreeModel::Row row = *_store->get_iter(path);
+ if (row) {
+ Glib::ustring name = row[_attrColumns._attributeName];
+ {
+ this->_store->erase(row);
+ this->_repr->removeAttribute(name);
+ this->setUndo(_("Delete attribute"));
+ }
+ }
+}
+
+/**
+ * @brief AttrDialog::onKeyPressed
+ * @param event
+ * @return true
+ * Delete or create elements based on key presses
+ */
+bool AttrDialog::onKeyPressed(GdkEventKey *event)
+{
+ bool ret = false;
+ if(this->_repr) {
+ auto selection = this->_treeView.get_selection();
+ Gtk::TreeModel::Row row = *(selection->get_selected());
+ Gtk::TreeIter iter = *(selection->get_selected());
+ switch (event->keyval)
+ {
+ case GDK_KEY_Delete:
+ case GDK_KEY_KP_Delete: {
+ // Create new attribute (repeat code, fold into above event!)
+ Glib::ustring name = row[_attrColumns._attributeName];
+ {
+ this->_store->erase(row);
+ this->_repr->removeAttribute(name);
+ this->setUndo(_("Delete attribute"));
+ }
+ ret = true;
+ } break;
+ case GDK_KEY_plus:
+ case GDK_KEY_Insert:
+ {
+ // Create new attribute (repeat code, fold into above event!)
+ Gtk::TreeIter iter = this->_store->prepend();
+ Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iter;
+ this->_treeView.set_cursor(path, *this->_nameCol, true);
+ grab_focus();
+ ret = true;
+ } break;
+ case GDK_KEY_Return:
+ case GDK_KEY_KP_Enter: {
+ if (_popover->is_visible()) {
+ if (!(event->state & GDK_SHIFT_MASK)) {
+ valueEditedPop();
+ _popover->hide();
+ ret = true;
+ }
+ }
+ } break;
+ }
+ }
+ return ret;
+}
+
+bool AttrDialog::onNameKeyPressed(GdkEventKey *event, Gtk::Entry *entry)
+{
+ g_debug("StyleDialog::_onNameKeyPressed");
+ bool ret = false;
+ switch (event->keyval) {
+ case GDK_KEY_Tab:
+ case GDK_KEY_KP_Tab:
+ entry->editing_done();
+ ret = true;
+ break;
+ }
+ return ret;
+}
+
+
+bool AttrDialog::onValueKeyPressed(GdkEventKey *event, Gtk::Entry *entry)
+{
+ g_debug("StyleDialog::_onValueKeyPressed");
+ bool ret = false;
+ switch (event->keyval) {
+ case GDK_KEY_Tab:
+ case GDK_KEY_KP_Tab:
+ entry->editing_done();
+ ret = true;
+ break;
+ }
+ return ret;
+}
+
+gboolean sp_attrdialog_store_move_to_next(gpointer data)
+{
+ AttrDialog *attrdialog = reinterpret_cast<AttrDialog *>(data);
+ auto selection = attrdialog->_treeView.get_selection();
+ Gtk::TreeIter iter = *(selection->get_selected());
+ Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iter;
+ Gtk::TreeViewColumn *focus_column;
+ attrdialog->_treeView.get_cursor(path, focus_column);
+ if (path == attrdialog->_modelpath && focus_column == attrdialog->_treeView.get_column(1)) {
+ attrdialog->_treeView.set_cursor(attrdialog->_modelpath, *attrdialog->_valueCol, true);
+ }
+ return FALSE;
+}
+
+/**
+ *
+ *
+ * @brief AttrDialog::nameEdited
+ * @param event
+ * @return
+ * Called when the name is edited in the TreeView editable column
+ */
+void AttrDialog::nameEdited (const Glib::ustring& path, const Glib::ustring& name)
+{
+ Gtk::TreeIter iter = *_store->get_iter(path);
+ _modelpath = (Gtk::TreeModel::Path)iter;
+ Gtk::TreeModel::Row row = *iter;
+ if(row && this->_repr) {
+ Glib::ustring old_name = row[_attrColumns._attributeName];
+ if (old_name == name) {
+ g_timeout_add(50, &sp_attrdialog_store_move_to_next, this);
+ grab_focus();
+ return;
+ }
+ // Do not allow empty name (this would delete the attribute)
+ if (name.empty()) {
+ return;
+ }
+ // Do not allow duplicate names
+ const auto children = _store->children();
+ for (const auto &child : children) {
+ if (name == child[_attrColumns._attributeName]) {
+ return;
+ }
+ }
+ if(std::any_of(name.begin(), name.end(), isspace)) {
+ return;
+ }
+ // Copy old value and remove old name
+ Glib::ustring value;
+ if (!old_name.empty()) {
+ value = row[_attrColumns._attributeValue];
+ _updating = true;
+ _repr->removeAttribute(old_name);
+ _updating = false;
+ }
+
+ // Do the actual renaming and set new value
+ row[_attrColumns._attributeName] = name;
+ grab_focus();
+ _updating = true;
+ _repr->setAttributeOrRemoveIfEmpty(name, value); // use char * overload (allows empty attribute values)
+ _updating = false;
+ g_timeout_add(50, &sp_attrdialog_store_move_to_next, this);
+ this->setUndo(_("Rename attribute"));
+ }
+}
+
+void AttrDialog::valueEditedPop()
+{
+ Glib::ustring value = _textview->get_buffer()->get_text();
+ valueEdited(valuepath, value);
+ valueediting = "";
+ _popover->hide();
+}
+
+void AttrDialog::valueCanceledPop()
+{
+ if (!valueediting.empty()) {
+ Glib::RefPtr<Gtk::TextBuffer> textbuffer = Gtk::TextBuffer::create();
+ textbuffer->set_text(valueediting);
+ _textview->set_buffer(textbuffer);
+ }
+ _popover->hide();
+}
+
+/**
+ * @brief AttrDialog::valueEdited
+ * @param event
+ * @return
+ * Called when the value is edited in the TreeView editable column
+ */
+void AttrDialog::valueEdited (const Glib::ustring& path, const Glib::ustring& value)
+{
+ auto selection = getSelection();
+ if (!selection)
+ return;
+
+ Gtk::TreeModel::Row row = *_store->get_iter(path);
+ if(row && this->_repr) {
+ Glib::ustring name = row[_attrColumns._attributeName];
+ Glib::ustring old_value = row[_attrColumns._attributeValue];
+ if (old_value == value) {
+ return;
+ }
+ if(name.empty()) return;
+ {
+ _repr->setAttributeOrRemoveIfEmpty(name, value);
+ }
+ if(!value.empty()) {
+ row[_attrColumns._attributeValue] = value;
+ Glib::ustring renderval = prepare_rendervalue(value.c_str());
+ row[_attrColumns._attributeValueRender] = renderval;
+ }
+ SPObject *obj = nullptr;
+ if (selection->objects().size() == 1) {
+ obj = selection->objects().back();
+
+ obj->style->readFromObject(obj);
+ obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
+ }
+ this->setUndo(_("Change attribute value"));
+ }
+}
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
diff --git a/src/ui/dialog/attrdialog.h b/src/ui/dialog/attrdialog.h
new file mode 100644
index 0000000..2d4a6a5
--- /dev/null
+++ b/src/ui/dialog/attrdialog.h
@@ -0,0 +1,130 @@
+// 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/dialog.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/popover.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/textview.h>
+#include <gtkmm/treeview.h>
+
+#include "desktop.h"
+#include "message.h"
+#include "ui/dialog/dialog-base.h"
+
+#define ATTR_DIALOG(obj) (dynamic_cast<Inkscape::UI::Dialog::AttrDialog*>((Inkscape::UI::Dialog::AttrDialog*)obj))
+
+namespace Inkscape {
+class MessageStack;
+class MessageContext;
+namespace UI {
+namespace Dialog {
+
+/**
+ * @brief The AttrDialog class
+ * This dialog allows to add, delete and modify XML attributes created in the
+ * xml editor.
+ */
+class AttrDialog : public DialogBase
+{
+public:
+ AttrDialog();
+ ~AttrDialog() override;
+
+ static AttrDialog &getInstance() { return *new AttrDialog(); }
+
+ // Data structure
+ class AttrColumns : public Gtk::TreeModel::ColumnRecord {
+ public:
+ AttrColumns() {
+ add(_attributeName);
+ add(_attributeValue);
+ add(_attributeValueRender);
+ }
+ Gtk::TreeModelColumn<Glib::ustring> _attributeName;
+ Gtk::TreeModelColumn<Glib::ustring> _attributeValue;
+ Gtk::TreeModelColumn<Glib::ustring> _attributeValueRender;
+ };
+ AttrColumns _attrColumns;
+
+ // TreeView
+ Gtk::TreeView _treeView;
+ Glib::RefPtr<Gtk::ListStore> _store;
+ Gtk::CellRendererText *_nameRenderer;
+ Gtk::CellRendererText *_valueRenderer;
+ Gtk::TreeViewColumn *_nameCol;
+ Gtk::TreeViewColumn *_valueCol;
+ Gtk::TreeModel::Path _modelpath;
+ Gtk::Popover *_popover;
+ Gtk::TextView *_textview;
+ Glib::ustring valuepath;
+ Glib::ustring valueediting;
+
+ // Text/comment nodes
+ Gtk::TextView *_content_tv;
+ Gtk::ScrolledWindow *_content_sw;
+
+ /**
+ * Status bar
+ */
+ std::shared_ptr<Inkscape::MessageStack> _message_stack;
+ std::unique_ptr<Inkscape::MessageContext> _message_context;
+
+ // Widgets
+ Gtk::Box _mainBox;
+ Gtk::ScrolledWindow _scrolledWindow;
+ Gtk::ScrolledWindow _scrolled_text_view;
+ Gtk::Button _buttonAddAttribute;
+ // Variables - Inkscape
+ Inkscape::XML::Node* _repr;
+ Gtk::Box status_box;
+ Gtk::Label status;
+ bool _updating;
+
+ // Helper functions
+ void setRepr(Inkscape::XML::Node * repr);
+ void setUndo(Glib::ustring const &event_description);
+ /**
+ * Sets the XML status bar, depending on which attr is selected.
+ */
+ void attr_reset_context(gint attr);
+ static void _set_status_message(Inkscape::MessageType type, const gchar *message, GtkWidget *dialog);
+
+ /**
+ * Signal handlers
+ */
+ sigc::connection _message_changed_connection;
+ void onAttrChanged(Inkscape::XML::Node *repr, const gchar * name, const gchar * new_value);
+ bool onNameKeyPressed(GdkEventKey *event, Gtk::Entry *entry);
+ bool onValueKeyPressed(GdkEventKey *event, Gtk::Entry *entry);
+ void onAttrDelete(Glib::ustring path);
+ bool onAttrCreate(GdkEventButton *event);
+ bool onKeyPressed(GdkEventKey *event);
+ void popClosed();
+ void startNameEdit(Gtk::CellEditable *cell, const Glib::ustring &path);
+ void startValueEdit(Gtk::CellEditable *cell, const Glib::ustring &path);
+ void nameEdited(const Glib::ustring &path, const Glib::ustring &name);
+ void valueEdited(const Glib::ustring &path, const Glib::ustring &value);
+ void textViewMap();
+ void valueCanceledPop();
+ void valueEditedPop();
+};
+
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // ATTRDIALOG_H
diff --git a/src/ui/dialog/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..9769018
--- /dev/null
+++ b/src/ui/dialog/clonetiler.cpp
@@ -0,0 +1,2819 @@
+// 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"
+
+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"));
+ }
+
+ if (dynamic_cast<SPUse *>(tile) &&
+ tile->getRepr()->attribute("xlink:href") &&
+ (!id_href || !strcmp(id_href, tile->getRepr()->attribute("xlink:href"))) &&
+ tile->getRepr()->attribute("inkscape:tiled-clone-of") &&
+ (!id_href || !strcmp(id_href, tile->getRepr()->attribute("inkscape:tiled-clone-of"))))
+ {
+ result = true;
+ } else {
+ result = false;
+ }
+ if (id_href) {
+ g_free(id_href);
+ id_href = nullptr;
+ }
+ return result;
+}
+
+void CloneTiler::trace_hide_tiled_clones_recursively(SPObject *from)
+{
+ if (!trace_drawing)
+ return;
+
+ for (auto& o: from->children) {
+ SPItem *item = dynamic_cast<SPItem *>(&o);
+ if (item && is_a_clone_of(&o, nullptr)) {
+ item->invoke_hide(trace_visionkey); // FIXME: hide each tiled clone's original too!
+ }
+ trace_hide_tiled_clones_recursively (&o);
+ }
+}
+
+void CloneTiler::trace_setup(SPDocument *doc, gdouble zoom, SPItem *original)
+{
+ trace_drawing = new Inkscape::Drawing();
+ /* Create ArenaItem and set transform */
+ trace_visionkey = SPItem::display_key_new(1);
+ trace_doc = doc;
+ trace_drawing->setRoot(trace_doc->getRoot()->invoke_show(*trace_drawing, trace_visionkey, SP_ITEM_SHOW_DISPLAY));
+
+ // hide the (current) original and any tiled clones, we only want to pick the background
+ original->invoke_hide(trace_visionkey);
+ trace_hide_tiled_clones_recursively(trace_doc->getRoot());
+
+ trace_doc->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ trace_doc->ensureUpToDate();
+
+ trace_zoom = zoom;
+}
+
+guint32 CloneTiler::trace_pick(Geom::Rect box)
+{
+ if (!trace_drawing) {
+ return 0;
+ }
+
+ trace_drawing->root()->setTransform(Geom::Scale(trace_zoom));
+ trace_drawing->update();
+
+ /* Item integer bbox in points */
+ Geom::IntRect ibox = (box * Geom::Scale(trace_zoom)).roundOutwards();
+
+ /* Find visible area */
+ cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, ibox.width(), ibox.height());
+ Inkscape::DrawingContext dc(s, ibox.min());
+ /* Render */
+ trace_drawing->render(dc, ibox);
+ double R = 0, G = 0, B = 0, A = 0;
+ ink_cairo_surface_average_color(s, R, G, B, A);
+ cairo_surface_destroy(s);
+
+ return SP_RGBA32_F_COMPOSE (R, G, B, A);
+}
+
+void CloneTiler::trace_finish()
+{
+ if (trace_doc) {
+ trace_doc->getRoot()->invoke_hide(trace_visionkey);
+ delete trace_drawing;
+ trace_doc = nullptr;
+ trace_drawing = nullptr;
+ }
+}
+
+void CloneTiler::unclump()
+{
+ auto 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);
+
+ SPItem *item = dynamic_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);
+ SPItem *item = dynamic_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);
+ SPItem *item = dynamic_cast<SPItem *>(clone_object);
+ if (clone_object && item) {
+ clone_object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ item->setCenter(desktop->doc2dt(new_center));
+ clone_object->updateRepr();
+ }
+ }
+
+ Inkscape::GC::release(clone);
+ }
+ cur[Geom::Y] = 0;
+ }
+
+ if (dotrace) {
+ trace_finish ();
+ }
+
+ change_selection(selection);
+
+ desktop->clearWaitingCursor();
+ DocumentUndo::done(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..8713d10
--- /dev/null
+++ b/src/ui/dialog/clonetiler.h
@@ -0,0 +1,211 @@
+// 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;
+
+ static CloneTiler &getInstance() { return *new CloneTiler(); }
+ void show_page_trace();
+protected:
+ enum PickType {
+ PICK_COLOR,
+ PICK_OPACITY,
+ PICK_R,
+ PICK_G,
+ PICK_B,
+ PICK_H,
+ PICK_S,
+ PICK_L
+ };
+
+ 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:
+ CloneTiler(CloneTiler const &d) = delete;
+ CloneTiler& operator=(CloneTiler const &d) = delete;
+
+ 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;
+
+ // Variables that used to be set using GObject
+ 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..1daadd9
--- /dev/null
+++ b/src/ui/dialog/color-item.cpp
@@ -0,0 +1,667 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Inkscape color swatch UI item.
+ */
+/* Authors:
+ * Jon A. Cruz
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2010 Jon A. Cruz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cerrno>
+
+#include <gtkmm/label.h>
+#include <glibmm/i18n.h>
+
+#include "color-item.h"
+
+#include "desktop.h"
+
+#include "desktop-style.h"
+#include "display/cairo-utils.h"
+#include "document.h"
+#include "document-undo.h"
+#include "inkscape.h" // for SP_ACTIVE_DESKTOP
+#include "message-context.h"
+
+#include "io/resource.h"
+#include "io/sys.h"
+#include "svg/svg-color.h"
+#include "ui/icon-names.h"
+#include "ui/widget/gradient-vector-selector.h"
+
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+static std::vector<std::string> mimeStrings;
+static std::map<std::string, guint> mimeToInt;
+
+
+void
+ColorItem::handleClick() {
+ buttonClicked(false);
+}
+
+void
+ColorItem::handleSecondaryClick(gint /*arg1*/) {
+ buttonClicked(true);
+}
+
+bool
+ColorItem::handleEnterNotify(GdkEventCrossing* /*event*/) {
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if ( desktop ) {
+ gchar* msg = g_strdup_printf(_("Color: <b>%s</b>; <b>Click</b> to set fill, <b>Shift+click</b> to set stroke"),
+ def.descr.c_str());
+ desktop->tipsMessageContext()->set(Inkscape::INFORMATION_MESSAGE, msg);
+ g_free(msg);
+ }
+
+ return false;
+}
+
+bool
+ColorItem::handleLeaveNotify(GdkEventCrossing* /*event*/) {
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+
+ if ( desktop ) {
+ desktop->tipsMessageContext()->clear();
+ }
+
+ return false;
+}
+
+static bool getBlock( std::string& dst, guchar ch, std::string const & str )
+{
+ bool good = false;
+ std::string::size_type pos = str.find(ch);
+ if ( pos != std::string::npos )
+ {
+ std::string::size_type pos2 = str.find( '(', pos );
+ if ( pos2 != std::string::npos ) {
+ std::string::size_type endPos = str.find( ')', pos2 );
+ if ( endPos != std::string::npos ) {
+ dst = str.substr( pos2 + 1, (endPos - pos2 - 1) );
+ good = true;
+ }
+ }
+ }
+ return good;
+}
+
+static bool popVal( guint64& numVal, std::string& str )
+{
+ bool good = false;
+ std::string::size_type endPos = str.find(',');
+ if ( endPos == std::string::npos ) {
+ endPos = str.length();
+ }
+
+ if ( endPos != std::string::npos && endPos > 0 ) {
+ std::string xxx = str.substr( 0, endPos );
+ const gchar* ptr = xxx.c_str();
+ gchar* endPtr = nullptr;
+ numVal = g_ascii_strtoull( ptr, &endPtr, 10 );
+ if ( (numVal == G_MAXUINT64) && (ERANGE == errno) ) {
+ // overflow
+ } else if ( (numVal == 0) && (endPtr == ptr) ) {
+ // failed conversion
+ } else {
+ good = true;
+ str.erase( 0, endPos + 1 );
+ }
+ }
+
+ return good;
+}
+
+// TODO resolve this more cleanly:
+extern bool colorItemHandleButtonPress(GdkEventButton* event, UI::Widget::Preview *preview, gpointer user_data);
+
+void
+ColorItem::drag_begin(const Glib::RefPtr<Gdk::DragContext> &dc)
+{
+ using Inkscape::IO::Resource::get_path;
+ using Inkscape::IO::Resource::PIXMAPS;
+ using Inkscape::IO::Resource::SYSTEM;
+ int width = 32;
+ int height = 24;
+
+ if (def.getType() != ege::PaintDef::RGB){
+ GError *error;
+ gsize bytesRead = 0;
+ gsize bytesWritten = 0;
+ gchar *localFilename = g_filename_from_utf8(get_path(SYSTEM, PIXMAPS, "remove-color.png"), -1, &bytesRead,
+ &bytesWritten, &error);
+ auto pixbuf = Gdk::Pixbuf::create_from_file(localFilename, width, height, false);
+ g_free(localFilename);
+ dc->set_icon(pixbuf, 0, 0);
+ } else {
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf;
+ if (getGradient() ){
+ cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
+ cairo_pattern_t *gradient = getGradient()->create_preview_pattern(width);
+ cairo_t *ct = cairo_create(s);
+ cairo_set_source(ct, gradient);
+ cairo_paint(ct);
+ cairo_destroy(ct);
+ cairo_pattern_destroy(gradient);
+ cairo_surface_flush(s);
+
+ pixbuf = Glib::wrap(ink_pixbuf_create_from_cairo_surface(s));
+ } else {
+ pixbuf = Gdk::Pixbuf::create( Gdk::COLORSPACE_RGB, false, 8, width, height );
+ guint32 fillWith = (0xff000000 & (def.getR() << 24))
+ | (0x00ff0000 & (def.getG() << 16))
+ | (0x0000ff00 & (def.getB() << 8));
+ pixbuf->fill( fillWith );
+ }
+ dc->set_icon(pixbuf, 0, 0);
+ }
+}
+
+//"drag-drop"
+// gboolean dragDropColorData( GtkWidget *widget,
+// GdkDragContext *drag_context,
+// gint x,
+// gint y,
+// guint time,
+// gpointer user_data)
+// {
+// // TODO finish
+
+// return TRUE;
+// }
+
+
+SwatchPage::SwatchPage()
+ : _prefWidth(0)
+{
+}
+
+SwatchPage::~SwatchPage()
+= default;
+
+
+ColorItem::ColorItem(ege::PaintDef::ColorType type) :
+ def(type),
+ _isFill(false),
+ _isStroke(false),
+ _isLive(false),
+ _linkIsTone(false),
+ _linkPercent(0),
+ _linkGray(0),
+ _linkSrc(nullptr),
+ _grad(nullptr),
+ _pattern(nullptr)
+{
+}
+
+ColorItem::ColorItem( unsigned int r, unsigned int g, unsigned int b, Glib::ustring& name ) :
+ def( r, g, b, name.raw() ),
+ _isFill(false),
+ _isStroke(false),
+ _isLive(false),
+ _linkIsTone(false),
+ _linkPercent(0),
+ _linkGray(0),
+ _linkSrc(nullptr),
+ _grad(nullptr),
+ _pattern(nullptr)
+{
+}
+
+ColorItem::~ColorItem()
+{
+ if (_pattern != nullptr) {
+ cairo_pattern_destroy(_pattern);
+ }
+}
+
+ColorItem::ColorItem(ColorItem const &other) :
+ Inkscape::UI::Previewable()
+{
+ if ( this != &other ) {
+ *this = other;
+ }
+}
+
+ColorItem &ColorItem::operator=(ColorItem const &other)
+{
+ if ( this != &other ) {
+ def = other.def;
+
+ // TODO - correct linkage
+ _linkSrc = other._linkSrc;
+ g_message("Erk!");
+ }
+ return *this;
+}
+
+void ColorItem::setState( bool fill, bool stroke )
+{
+ if ( (_isFill != fill) || (_isStroke != stroke) ) {
+ _isFill = fill;
+ _isStroke = stroke;
+
+ for ( auto widget : _previews ) {
+ auto preview = dynamic_cast<UI::Widget::Preview *>(widget);
+
+ if (preview) {
+ int val = preview->get_linked();
+ val &= ~(UI::Widget::PREVIEW_FILL | UI::Widget::PREVIEW_STROKE);
+ if ( _isFill ) {
+ val |= UI::Widget::PREVIEW_FILL;
+ }
+ if ( _isStroke ) {
+ val |= UI::Widget::PREVIEW_STROKE;
+ }
+ preview->set_linked(static_cast<UI::Widget::LinkType>(val));
+ }
+ }
+ }
+}
+
+void ColorItem::setGradient(SPGradient *grad)
+{
+ if (_grad != grad) {
+ _grad = grad;
+ // TODO regen and push to listeners
+ }
+
+ setName( gr_prepare_label(_grad) );
+}
+
+void ColorItem::setName(const Glib::ustring name)
+{
+ //def.descr = name;
+
+ for (auto widget : _previews) {
+ auto preview = dynamic_cast<UI::Widget::Preview *>(widget);
+ auto label = dynamic_cast<Gtk::Label *>(widget);
+ if (preview) {
+ preview->set_tooltip_text(name);
+ }
+ else if (label) {
+ label->set_text(name);
+ }
+ }
+}
+
+void ColorItem::setPattern(cairo_pattern_t *pattern)
+{
+ if (pattern) {
+ cairo_pattern_reference(pattern);
+ }
+ if (_pattern) {
+ cairo_pattern_destroy(_pattern);
+ }
+ _pattern = pattern;
+
+ _updatePreviews();
+}
+
+void
+ColorItem::_dragGetColorData(const Glib::RefPtr<Gdk::DragContext>& /*drag_context*/,
+ Gtk::SelectionData &data,
+ guint info,
+ guint /*time*/)
+{
+ std::string key;
+ if ( info < mimeStrings.size() ) {
+ key = mimeStrings[info];
+ } else {
+ g_warning("ERROR: unknown value (%d)", info);
+ }
+
+ if ( !key.empty() ) {
+ char* tmp = nullptr;
+ int len = 0;
+ int format = 0;
+ def.getMIMEData(key, tmp, len, format);
+ if ( tmp ) {
+ data.set(key, format, (guchar*)tmp, len );
+ delete[] tmp;
+ }
+ }
+}
+
+void ColorItem::_dropDataIn( GtkWidget */*widget*/,
+ GdkDragContext */*drag_context*/,
+ gint /*x*/, gint /*y*/,
+ GtkSelectionData */*data*/,
+ guint /*info*/,
+ guint /*event_time*/,
+ gpointer /*user_data*/)
+{
+}
+
+void ColorItem::_colorDefChanged(void* data)
+{
+ ColorItem* item = reinterpret_cast<ColorItem*>(data);
+ if ( item ) {
+ item->_updatePreviews();
+ }
+}
+
+void ColorItem::_updatePreviews()
+{
+ for (auto widget : _previews) {
+ auto preview = dynamic_cast<UI::Widget::Preview *>(widget);
+ if (preview) {
+ _regenPreview(preview);
+ preview->queue_draw();
+ }
+ }
+
+ for (auto & _listener : _listeners) {
+ guint r = def.getR();
+ guint g = def.getG();
+ guint b = def.getB();
+
+ if ( _listener->_linkIsTone ) {
+ r = ( (_listener->_linkPercent * _listener->_linkGray) + ((100 - _listener->_linkPercent) * r) ) / 100;
+ g = ( (_listener->_linkPercent * _listener->_linkGray) + ((100 - _listener->_linkPercent) * g) ) / 100;
+ b = ( (_listener->_linkPercent * _listener->_linkGray) + ((100 - _listener->_linkPercent) * b) ) / 100;
+ } else {
+ r = ( (_listener->_linkPercent * 255) + ((100 - _listener->_linkPercent) * r) ) / 100;
+ g = ( (_listener->_linkPercent * 255) + ((100 - _listener->_linkPercent) * g) ) / 100;
+ b = ( (_listener->_linkPercent * 255) + ((100 - _listener->_linkPercent) * b) ) / 100;
+ }
+
+ _listener->def.setRGB( r, g, b );
+ }
+}
+
+void ColorItem::_regenPreview(UI::Widget::Preview * preview)
+{
+ if ( def.getType() != ege::PaintDef::RGB ) {
+ using Inkscape::IO::Resource::get_path;
+ using Inkscape::IO::Resource::PIXMAPS;
+ using Inkscape::IO::Resource::SYSTEM;
+ GError *error = nullptr;
+ gsize bytesRead = 0;
+ gsize bytesWritten = 0;
+ gchar *localFilename =
+ g_filename_from_utf8(get_path(SYSTEM, PIXMAPS, "remove-color.png"), -1, &bytesRead, &bytesWritten, &error);
+ auto pixbuf = Gdk::Pixbuf::create_from_file(localFilename);
+ if (!pixbuf) {
+ g_warning("Null pixbuf for %p [%s]", localFilename, localFilename );
+ }
+ g_free(localFilename);
+
+ preview->set_pixbuf(pixbuf);
+ }
+ else if ( !_pattern ){
+ preview->set_color((def.getR() << 8) | def.getR(),
+ (def.getG() << 8) | def.getG(),
+ (def.getB() << 8) | def.getB() );
+ } else {
+ // These correspond to PREVIEW_PIXBUF_WIDTH and VBLOCK from swatches.cpp
+ // TODO: the pattern to draw should be in the widget that draws the preview,
+ // so the preview can be scalable
+ int w = 128;
+ int h = 16;
+
+ cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h);
+ cairo_t *ct = cairo_create(s);
+ cairo_set_source(ct, _pattern);
+ cairo_paint(ct);
+ cairo_destroy(ct);
+ cairo_surface_flush(s);
+
+ auto pixbuf = Glib::wrap(ink_pixbuf_create_from_cairo_surface(s));
+ preview->set_pixbuf(pixbuf);
+ }
+
+ preview->set_linked(static_cast<UI::Widget::LinkType>( (_linkSrc ? UI::Widget::PREVIEW_LINK_IN : 0)
+ | (_listeners.empty() ? 0 : UI::Widget::PREVIEW_LINK_OUT)
+ | (_isLive ? UI::Widget::PREVIEW_LINK_OTHER:0)) );
+}
+
+Gtk::Widget* ColorItem::createWidget() {
+ auto widget = dynamic_cast<UI::Widget::Preview*>(_getPreview(Inkscape::UI::Widget::PREVIEW_STYLE_ICON,
+ Inkscape::UI::Widget::VIEW_TYPE_GRID, Inkscape::UI::Widget::PREVIEW_SIZE_TINY, 100, 0));
+
+ if (widget) widget->set_freesize(true);
+
+ return widget;
+}
+
+Gtk::Widget*
+ColorItem::getPreview(UI::Widget::PreviewStyle style,
+ UI::Widget::ViewType view,
+ UI::Widget::PreviewSize size,
+ guint ratio,
+ guint border)
+{
+ auto widget = _getPreview(style, view, size, ratio, border);
+ _previews.push_back( widget );
+ return widget;
+}
+
+
+Gtk::Widget* ColorItem::_getPreview(UI::Widget::PreviewStyle style,
+ UI::Widget::ViewType view, UI::Widget::PreviewSize size,
+ guint ratio, guint border) {
+
+ Gtk::Widget* widget = nullptr;
+ if ( style == UI::Widget::PREVIEW_STYLE_BLURB) {
+ Gtk::Label *lbl = new Gtk::Label(def.descr);
+ lbl->set_halign(Gtk::ALIGN_START);
+ lbl->set_valign(Gtk::ALIGN_CENTER);
+ widget = lbl;
+ } else {
+ auto preview = Gtk::manage(new UI::Widget::Preview());
+ preview->set_name("ColorItemPreview");
+
+ _regenPreview(preview);
+
+ preview->set_details((UI::Widget::ViewType)view,
+ (UI::Widget::PreviewSize)size,
+ ratio,
+ border );
+
+ def.addCallback( _colorDefChanged, this );
+ preview->set_focus_on_click(false);
+ preview->set_tooltip_text(def.descr);
+
+ preview->signal_clicked().connect(sigc::mem_fun(*this, &ColorItem::handleClick));
+ preview->signal_alt_clicked().connect(sigc::mem_fun(*this, &ColorItem::handleSecondaryClick));
+ preview->signal_button_press_event().connect(sigc::bind(sigc::ptr_fun(&colorItemHandleButtonPress), preview, this));
+
+ {
+ auto listing = def.getMIMETypes();
+ std::vector<Gtk::TargetEntry> entries;
+
+ for ( auto str : listing ) {
+ auto target = str.c_str();
+ guint flags = 0;
+ if ( mimeToInt.find(str) == mimeToInt.end() ){
+ // these next lines are order-dependent:
+ mimeToInt[str] = mimeStrings.size();
+ mimeStrings.push_back(str);
+ }
+ auto info = mimeToInt[target];
+ Gtk::TargetEntry entry(target, (Gtk::TargetFlags)flags, info);
+ entries.push_back(entry);
+ }
+
+ preview->drag_source_set(entries, Gdk::BUTTON1_MASK,
+ Gdk::DragAction(Gdk::ACTION_MOVE | Gdk::ACTION_COPY) );
+ }
+
+ preview->signal_drag_data_get().connect(sigc::mem_fun(*this, &ColorItem::_dragGetColorData));
+ preview->signal_drag_begin().connect(sigc::mem_fun(*this, &ColorItem::drag_begin));
+ preview->signal_enter_notify_event().connect(sigc::mem_fun(*this, &ColorItem::handleEnterNotify));
+ preview->signal_leave_notify_event().connect(sigc::mem_fun(*this, &ColorItem::handleLeaveNotify));
+
+ widget = preview;
+ }
+
+ return widget;
+}
+
+void ColorItem::buttonClicked(bool secondary)
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if (desktop) {
+ char const * attrName = secondary ? "stroke" : "fill";
+
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ Glib::ustring descr;
+ switch (def.getType()) {
+ case ege::PaintDef::CLEAR: {
+ // TODO actually make this clear
+ sp_repr_css_set_property( css, attrName, "none" );
+ descr = secondary? _("Remove stroke color") : _("Remove fill color");
+ break;
+ }
+ case ege::PaintDef::NONE: {
+ sp_repr_css_set_property( css, attrName, "none" );
+ descr = secondary? _("Set stroke color to none") : _("Set fill color to none");
+ break;
+ }
+//mark
+ case ege::PaintDef::RGB: {
+ Glib::ustring colorspec;
+ if ( _grad ){
+ colorspec = "url(#";
+ colorspec += _grad->getId();
+ colorspec += ")";
+ } else {
+ gchar c[64];
+ guint32 rgba = (def.getR() << 24) | (def.getG() << 16) | (def.getB() << 8) | 0xff;
+ sp_svg_write_color(c, sizeof(c), rgba);
+ colorspec = c;
+ }
+//end mark
+ sp_repr_css_set_property( css, attrName, colorspec.c_str() );
+ descr = secondary? _("Set stroke color from swatch") : _("Set fill color from swatch");
+ break;
+ }
+ }
+ sp_desktop_set_style(desktop, css);
+ sp_repr_css_attr_unref(css);
+
+ DocumentUndo::done( desktop->getDocument(), descr.c_str(), INKSCAPE_ICON("swatches"));
+ }
+}
+
+void ColorItem::_wireMagicColors( SwatchPage *colorSet )
+{
+ if ( colorSet )
+ {
+ for ( boost::ptr_vector<ColorItem>::iterator it = colorSet->_colors.begin(); it != colorSet->_colors.end(); ++it )
+ {
+ std::string::size_type pos = it->def.descr.find("*{");
+ if ( pos != std::string::npos )
+ {
+ std::string subby = it->def.descr.substr( pos + 2 );
+ std::string::size_type endPos = subby.find("}*");
+ if ( endPos != std::string::npos )
+ {
+ subby.erase( endPos );
+ //g_message("FOUND MAGIC at '%s'", (*it)->def.descr.c_str());
+ //g_message(" '%s'", subby.c_str());
+
+ if ( subby.find('E') != std::string::npos )
+ {
+ it->def.setEditable( true );
+ }
+
+ if ( subby.find('L') != std::string::npos )
+ {
+ it->_isLive = true;
+ }
+
+ std::string part;
+ // Tint. index + 1 more val.
+ if ( getBlock( part, 'T', subby ) ) {
+ guint64 colorIndex = 0;
+ if ( popVal( colorIndex, part ) ) {
+ guint64 percent = 0;
+ if ( popVal( percent, part ) ) {
+ it->_linkTint( colorSet->_colors[colorIndex], percent );
+ }
+ }
+ }
+
+ // Shade/tone. index + 1 or 2 more val.
+ if ( getBlock( part, 'S', subby ) ) {
+ guint64 colorIndex = 0;
+ if ( popVal( colorIndex, part ) ) {
+ guint64 percent = 0;
+ if ( popVal( percent, part ) ) {
+ guint64 grayLevel = 0;
+ if ( !popVal( grayLevel, part ) ) {
+ grayLevel = 0;
+ }
+ it->_linkTone( colorSet->_colors[colorIndex], percent, grayLevel );
+ }
+ }
+ }
+
+ }
+ }
+ }
+ }
+}
+
+
+void ColorItem::_linkTint( ColorItem& other, int percent )
+{
+ if ( !_linkSrc )
+ {
+ other._listeners.push_back(this);
+ _linkIsTone = false;
+ _linkPercent = percent;
+ if ( _linkPercent > 100 )
+ _linkPercent = 100;
+ if ( _linkPercent < 0 )
+ _linkPercent = 0;
+ _linkGray = 0;
+ _linkSrc = &other;
+
+ ColorItem::_colorDefChanged(&other);
+ }
+}
+
+void ColorItem::_linkTone( ColorItem& other, int percent, int grayLevel )
+{
+ if ( !_linkSrc )
+ {
+ other._listeners.push_back(this);
+ _linkIsTone = true;
+ _linkPercent = percent;
+ if ( _linkPercent > 100 )
+ _linkPercent = 100;
+ if ( _linkPercent < 0 )
+ _linkPercent = 0;
+ _linkGray = grayLevel;
+ _linkSrc = &other;
+
+ ColorItem::_colorDefChanged(&other);
+ }
+}
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/color-item.h b/src/ui/dialog/color-item.h
new file mode 100644
index 0000000..8ca7d9e
--- /dev/null
+++ b/src/ui/dialog/color-item.h
@@ -0,0 +1,135 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Inkscape color swatch UI item.
+ */
+/* Authors:
+ * Jon A. Cruz
+ *
+ * Copyright (C) 2010 Jon A. Cruz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_DIALOGS_COLOR_ITEM_H
+#define SEEN_DIALOGS_COLOR_ITEM_H
+
+#include <boost/ptr_container/ptr_vector.hpp>
+
+#include "widgets/ege-paint-def.h"
+#include "ui/previewable.h"
+
+class SPGradient;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class ColorItem;
+
+class SwatchPage
+{
+public:
+ SwatchPage();
+ ~SwatchPage();
+
+ Glib::ustring _name;
+ int _prefWidth;
+ boost::ptr_vector<ColorItem> _colors;
+};
+
+
+/**
+ * The color swatch you see on screen as a clickable box.
+ */
+class ColorItem : public Inkscape::UI::Previewable
+{
+ friend void _loadPaletteFile( gchar const *filename );
+public:
+ ColorItem( ege::PaintDef::ColorType type );
+ ColorItem( unsigned int r, unsigned int g, unsigned int b,
+ Glib::ustring& name );
+ ~ColorItem() override;
+ ColorItem(ColorItem const &other);
+ virtual ColorItem &operator=(ColorItem const &other);
+ Gtk::Widget* getPreview(UI::Widget::PreviewStyle style,
+ UI::Widget::ViewType view,
+ UI::Widget::PreviewSize size,
+ guint ratio,
+ guint border) override;
+ void buttonClicked(bool secondary = false);
+
+ void setGradient(SPGradient *grad);
+ SPGradient * getGradient() const { return _grad; }
+ void setPattern(cairo_pattern_t *pattern);
+ void setName(const Glib::ustring name);
+
+ void setState( bool fill, bool stroke );
+ bool isFill() { return _isFill; }
+ bool isStroke() { return _isStroke; }
+
+ ege::PaintDef def;
+
+ Gtk::Widget* createWidget();
+
+private:
+ Gtk::Widget* _getPreview(UI::Widget::PreviewStyle style,
+ UI::Widget::ViewType view, UI::Widget::PreviewSize size,
+ guint ratio, guint border);
+
+ static void _dropDataIn( GtkWidget *widget,
+ GdkDragContext *drag_context,
+ gint x, gint y,
+ GtkSelectionData *data,
+ guint info,
+ guint event_time,
+ gpointer user_data);
+
+ void _dragGetColorData(const Glib::RefPtr<Gdk::DragContext> &drag_context,
+ Gtk::SelectionData &data,
+ guint info,
+ guint time);
+
+ static void _wireMagicColors( SwatchPage *colorSet );
+ static void _colorDefChanged(void* data);
+
+ void _updatePreviews();
+ void _regenPreview(UI::Widget::Preview * preview);
+
+ void _linkTint( ColorItem& other, int percent );
+ void _linkTone( ColorItem& other, int percent, int grayLevel );
+ void drag_begin(const Glib::RefPtr<Gdk::DragContext> &dc);
+ void handleClick();
+ void handleSecondaryClick(gint arg1);
+ bool handleEnterNotify(GdkEventCrossing* event);
+ bool handleLeaveNotify(GdkEventCrossing* event);
+
+ std::vector<Gtk::Widget*> _previews;
+
+ bool _isFill;
+ bool _isStroke;
+ bool _isLive;
+ bool _linkIsTone;
+ int _linkPercent;
+ int _linkGray;
+ ColorItem* _linkSrc;
+ SPGradient* _grad;
+ cairo_pattern_t *_pattern;
+ std::vector<ColorItem*> _listeners;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // SEEN_DIALOGS_COLOR_ITEM_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/command-palette.cpp b/src/ui/dialog/command-palette.cpp
new file mode 100644
index 0000000..32d2ca9
--- /dev/null
+++ b/src/ui/dialog/command-palette.cpp
@@ -0,0 +1,1636 @@
+// 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);
+
+ _CPFilter->signal_key_press_event().connect(sigc::mem_fun(*this, &CommandPalette::on_key_press_cpfilter_escape),
+ false);
+ _CPSuggestions->signal_key_press_event().connect(sigc::mem_fun(*this, &CommandPalette::on_key_press_cpfilter_escape), false);
+ _CPHistory->signal_key_press_event().connect(sigc::mem_fun(*this, &CommandPalette::on_key_press_cpfilter_escape), 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 *CPActionFullName;
+ 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("CPActionFullName", CPActionFullName);
+ 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");
+ CPActionFullName->set_label("import"); // For filtering only
+
+ } else {
+ CPGroup->set_text("open");
+ CPActionFullName->set_label("open"); // For filtering only
+ }
+
+ // Hide for recent_file, not required
+ CPActionFullName->set_no_show_all();
+ CPActionFullName->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 *CPActionFullName;
+
+ // 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("CPActionFullName", CPActionFullName);
+ 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);
+ }
+
+ {
+ CPActionFullName->set_label(action_ptr_name.second);
+
+ if (not show_full_action_name) {
+ CPActionFullName->set_no_show_all();
+ CPActionFullName->hide();
+ } else {
+ CPActionFullName->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) {
+ ss << accel << ',';
+ }
+ 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
+ }
+}
+
+bool CommandPalette::on_filter_full_action_name(Gtk::ListBoxRow *child)
+{
+ if (auto CPActionFullName = get_full_action_name(child);
+ CPActionFullName and _search_text == CPActionFullName->get_label()) {
+ return true;
+ }
+ return false;
+}
+
+bool CommandPalette::on_filter_recent_file(Gtk::ListBoxRow *child, bool const is_import)
+{
+ auto CPActionFullName = get_full_action_name(child);
+ if (is_import) {
+ if (CPActionFullName and CPActionFullName->get_label() == "import") {
+ auto [CPName, CPDescription] = get_name_desc(child);
+ if (CPDescription && CPDescription->get_text() == _search_text) {
+ return true;
+ }
+ }
+ return false;
+ }
+ if (CPActionFullName and CPActionFullName->get_label() == "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 (not _CPHistory->get_children().empty()) {
+ set_mode(CPMode::HISTORY);
+ return true;
+ }
+ }
+ 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 << 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)
+{
+ 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 += subject[j];
+ }
+ 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 += subject[i];
+ }
+ }
+ }
+
+ 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 << ":" << 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
+ << 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 CPName = dynamic_cast<Gtk::Label *>(synapse_children[0]);
+ auto CPDescription = dynamic_cast<Gtk::Label *>(synapse_children[1]);
+
+ return std::pair(CPName, CPDescription);
+ }
+ }
+
+ return std::pair(nullptr, nullptr);
+}
+
+Gtk::Button *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 CPActionFullName = dynamic_cast<Gtk::Button *>(synapse_children[2]);
+
+ return CPActionFullName;
+ }
+ }
+
+ 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..e0316ac
--- /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::Button *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..8ddc5c4
--- /dev/null
+++ b/src/ui/dialog/dialog-base.cpp
@@ -0,0 +1,320 @@
+// 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
+ ensure_size();
+}
+
+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());
+ }
+}
+
+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();
+}
+
+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 (desktop->selection) {
+ selection = desktop->selection;
+ _select_changed = selection->connectChanged(sigc::mem_fun(*this, &DialogBase::selectionChanged_impl));
+ _select_modified = selection->connectModified(sigc::mem_fun(*this, &DialogBase::selectionModified_impl));
+ }
+
+ _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->selection) {
+ this->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;
+ });
+ }
+}
+
+/**
+ * implementation method that call to main function only when tab is showing
+ */
+void
+DialogBase::selectionChanged_impl(Inkscape::Selection *selection) {
+ if (_showing) {
+ selectionChanged(selection);
+ }
+}
+
+/**
+ * implementation method that call to main function only when tab is showing
+ */
+void
+DialogBase::selectionModified_impl(Inkscape::Selection *selection, guint flags) {
+ if (_showing) {
+ selectionModified(selection, flags);
+ }
+}
+
+/**
+ * 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;
+ selectionChanged(getSelection());
+}
+
+/**
+ * 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();
+ 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..4e3f7e4
--- /dev/null
+++ b/src/ui/dialog/dialog-base.h
@@ -0,0 +1,135 @@
+// 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 it's inner focus
+ * (be it of the active desktop, document, selection, etc.) in the update() method.
+ *
+ * DialogsBase 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(gchar const *prefs_path = nullptr, Glib::ustring dialog_type = "");
+ ~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() {}
+ void selectionChanged_impl(Inkscape::Selection *selection);
+ void selectionModified_impl(Inkscape::Selection *selection, guint flags);
+ 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;
+
+ 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..a060e63
--- /dev/null
+++ b/src/ui/dialog/dialog-container.cpp
@@ -0,0 +1,1111 @@
+// 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/export.h"
+#include "ui/dialog/fill-and-stroke.h"
+#include "ui/dialog/filter-effects-dialog.h"
+#include "ui/dialog/find.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/prototype.h"
+#include "ui/dialog/selectorsdialog.h"
+#include "ui/shortcuts.h"
+#if WITH_GSPELL
+#include "ui/dialog/spellcheck.h"
+#endif
+#include "ui/dialog/styledialog.h"
+#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/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.
+ */
+DialogBase *DialogContainer::dialog_factory(const Glib::ustring& dialog_type)
+{
+
+ // clang-format off
+ if( dialog_type == "AlignDistribute") return &Inkscape::UI::Dialog::ArrangeDialog::getInstance();
+ else if(dialog_type == "CloneTiler") return &Inkscape::UI::Dialog::CloneTiler::getInstance();
+ else if(dialog_type == "DocumentProperties") return &Inkscape::UI::Dialog::DocumentProperties::getInstance();
+ else if(dialog_type == "Export") return &Inkscape::UI::Dialog::Export::getInstance();
+ else if(dialog_type == "FillStroke") return &Inkscape::UI::Dialog::FillAndStroke::getInstance();
+ else if(dialog_type == "FilterEffects") return &Inkscape::UI::Dialog::FilterEffectsDialog::getInstance();
+ else if(dialog_type == "Find") return &Inkscape::UI::Dialog::Find::getInstance();
+ else if(dialog_type == "Glyphs") return &Inkscape::UI::Dialog::GlyphsPanel::getInstance();
+ else if(dialog_type == "IconPreview") return &Inkscape::UI::Dialog::IconPreviewPanel::getInstance();
+ else if(dialog_type == "Input") return &Inkscape::UI::Dialog::InputDialog::getInstance();
+ else if(dialog_type == "LivePathEffect") return &Inkscape::UI::Dialog::LivePathEffectEditor::getInstance();
+ else if(dialog_type == "Memory") return &Inkscape::UI::Dialog::Memory::getInstance();
+ else if(dialog_type == "Messages") return &Inkscape::UI::Dialog::Messages::getInstance();
+ else if(dialog_type == "ObjectAttributes") return &Inkscape::UI::Dialog::ObjectAttributes::getInstance();
+ else if(dialog_type == "ObjectProperties") return &Inkscape::UI::Dialog::ObjectProperties::getInstance();
+ else if(dialog_type == "Objects") return &Inkscape::UI::Dialog::ObjectsPanel::getInstance();
+ else if(dialog_type == "PaintServers") return &Inkscape::UI::Dialog::PaintServersDialog::getInstance();
+ else if(dialog_type == "Preferences") return &Inkscape::UI::Dialog::InkscapePreferences::getInstance();
+ else if(dialog_type == "Selectors") return &Inkscape::UI::Dialog::SelectorsDialog::getInstance();
+ else if(dialog_type == "SVGFonts") return &Inkscape::UI::Dialog::SvgFontsDialog::getInstance();
+ else if(dialog_type == "Swatches") return &Inkscape::UI::Dialog::SwatchesPanel::getInstance();
+ else if(dialog_type == "Symbols") return &Inkscape::UI::Dialog::SymbolsDialog::getInstance();
+ else if(dialog_type == "Text") return &Inkscape::UI::Dialog::TextEdit::getInstance();
+ else if(dialog_type == "Trace") return &Inkscape::UI::Dialog::TraceDialog::getInstance();
+ else if(dialog_type == "Transform") return &Inkscape::UI::Dialog::Transformation::getInstance();
+ else if(dialog_type == "UndoHistory") return &Inkscape::UI::Dialog::UndoHistory::getInstance();
+ else if(dialog_type == "XMLEditor") return &Inkscape::UI::Dialog::XmlTree::getInstance();
+#if WITH_GSPELL
+ else if(dialog_type == "Spellcheck") return &Inkscape::UI::Dialog::SpellCheck::getInstance();
+#endif
+#ifdef DEBUG
+ else if(dialog_type == "Prototype") return &Inkscape::UI::Dialog::Prototype::getInstance();
+#endif
+ else {
+ std::cerr << "DialogContainer::dialog_factory: Unhandled dialog: " << dialog_type << 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);
+
+ if (!dialog) {
+ std::cerr << "DialogContainer::new_dialog(): couldn't find dialog for: " << dialog_type << 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();
+ }
+}
+
+// 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 {
+ 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 << 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();
+ 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);
+
+ if (!dialog) {
+ std::cerr << "DialogContainer::new_dialog(): couldn't find dialog for: " << dialog_type << 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;
+ }
+
+ 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 << std::endl;
+ }
+ }
+ }
+ }
+
+ 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();
+ }
+ }
+}
+
+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
+
+ // 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);
+
+ // 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);
+ 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.
+ 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..c6fa30a
--- /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);
+ DialogBase *dialog_factory(const Glib::ustring& 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..4dd9aed
--- /dev/null
+++ b/src/ui/dialog/dialog-data.cpp
@@ -0,0 +1,79 @@
+// 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<Glib::ustring, DialogData> const &get_dialog_data()
+{
+ static std::map<Glib::ustring, 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 }},
+ {"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 }},
+ {"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::NOPROVIDE }},
+ {"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("symbols"), 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..5f17723
--- /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<Glib::ustring, 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..e082467
--- /dev/null
+++ b/src/ui/dialog/dialog-manager.cpp
@@ -0,0 +1,310 @@
+// 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;
+ }
+ }
+}
+
+// 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);
+
+#ifdef G_OS_WIN32
+ bool exists = filesystem::exists(filesystem::u8path(filename));
+#else
+ bool exists = filesystem::exists(filesystem::path(filename));
+#endif
+
+ 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();
+ }
+ } 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 can define which dialogs should by default be open floating rather then docked
+void DialogManager::dialog_defaults() {
+ std::shared_ptr<Glib::KeyFile> floating;
+ // strings are dialog types
+ _floating_dialogs["CloneTiler"] = floating;
+ _floating_dialogs["DocumentProperties"] = floating;
+ _floating_dialogs["FilterEffects"] = floating;
+ _floating_dialogs["Input"] = floating;
+ _floating_dialogs["Preferences"] = floating;
+ _floating_dialogs["XMLEditor"] = floating;
+}
+
+} // 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..fc4cd9b
--- /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();
+
+ // 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..531c636
--- /dev/null
+++ b/src/ui/dialog/dialog-multipaned.cpp
@@ -0,0 +1,1280 @@
+// 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;
+ }
+ }
+ }
+}
+
+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;
+ }
+ }
+ }
+}
+
+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;
+ }
+
+ {
+ 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(children.size(), 0); // The new allocation sizes
+ std::vector<int> sizes_current; // The current sizes along main axis
+ int left = horizontal ? allocation.get_width() : allocation.get_height();
+
+ int index = 0;
+
+ int canvas_index = -1;
+ for (auto &child : children) {
+ bool visible;
+
+ Inkscape::UI::Widget::CanvasGrid *canvas = dynamic_cast<Inkscape::UI::Widget::CanvasGrid *>(child);
+ if (canvas) {
+ canvas_index = index;
+ }
+
+ {
+ visible = child->get_visible();
+ 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++;
+ }
+
+ // Precalculate the minimum, natural and current totals
+ int sum_minimums = std::accumulate(sizes_minimums.begin(), sizes_minimums.end(), 0);
+ int sum_naturals = std::accumulate(sizes_naturals.begin(), sizes_naturals.end(), 0);
+ int sum_current = std::accumulate(sizes_current.begin(), sizes_current.end(), 0);
+
+ if (sum_naturals <= left) {
+ sizes = sizes_naturals;
+ left -= sum_naturals;
+ } else if (sum_minimums <= left && left < sum_naturals) {
+ sizes = sizes_minimums;
+ left -= sum_minimums;
+ }
+
+ if (canvas_index >= 0) { // give remaining space to canvas element
+ sizes[canvas_index] += left;
+ } else { // or, if in a sub-dialogmultipaned, give it evenly to widgets
+
+ int d = 0;
+ for (int i = 0; i < (int)children.size(); ++i) {
+ if (expandables[i]) {
+ d++;
+ }
+ }
+
+ if (d > 0) {
+ int idx = 0;
+ for (int i = 0; i < (int)children.size(); ++i) {
+ if (expandables[i]) {
+ sizes[i] += (left / d);
+ if (idx < (left % d))
+ sizes[i]++;
+ idx++;
+ }
+ }
+ }
+ }
+ left = 0;
+
+ // 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 (int i = 0; i < (int)children.size(); ++i) {
+ valid = 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 (int i = 0; i < (int)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;
+}
+
+} // 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..1d1bb50
--- /dev/null
+++ b/src/ui/dialog/dialog-multipaned.h
@@ -0,0 +1,201 @@
+// 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();
+
+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;
+};
+
+} // 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..babcd2d
--- /dev/null
+++ b/src/ui/dialog/dialog-notebook.cpp
@@ -0,0 +1,994 @@
+// 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 box = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL, 8);
+ box->pack_start(*Gtk::make_managed<Gtk::Image>(data.icon_name, Gtk::ICON_SIZE_MENU), false, true);
+ box->pack_start(*Gtk::make_managed<Gtk::Label>(data.label, Gtk::ALIGN_START, Gtk::ALIGN_CENTER, true), false, true);
+ dlg->add(*box);
+ 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 ) {
+ 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);
+ }
+}
+
+} // 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..6b0c7e6
--- /dev/null
+++ b/src/ui/dialog/dialog-notebook.h
@@ -0,0 +1,124 @@
+// 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);
+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);
+};
+
+} // 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..f91f399
--- /dev/null
+++ b/src/ui/dialog/dialog-window.cpp
@@ -0,0 +1,308 @@
+// 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
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool window_above = true;
+ if (prefs) {
+ window_above =
+ prefs->getInt("/options/transientpolicy/value", PREFS_DIALOGS_WINDOWS_NORMAL) != PREFS_DIALOGS_WINDOWS_NONE;
+ }
+
+ 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 ==============
+
+
+ // Set the style and icon theme of the new menu based on the desktop
+ if (auto desktop = SP_ACTIVE_DESKTOP) {
+ if (Gtk::Window *window = desktop->getToplevel()) {
+ if (window->get_style_context()->has_class("dark")) {
+ get_style_context()->add_class("dark");
+ get_style_context()->remove_class("bright");
+ } else {
+ get_style_context()->add_class("bright");
+ get_style_context()->remove_class("dark");
+ }
+ 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");
+ }
+ }
+ }
+
+ // ================ 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..b9cbe86
--- /dev/null
+++ b/src/ui/dialog/document-properties.cpp
@@ -0,0 +1,1784 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Document properties dialog, Gtkmm-style.
+ */
+/* Authors:
+ * bulia byak <buliabyak@users.sf.net>
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Jon Phillips <jon@rejon.org>
+ * Ralf Stephan <ralf@ark.in-berlin.de> (Gtkmm)
+ * Diederik van Lierop <mail@diedenrezi.nl>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2006-2008 Johan Engelen <johan@shouraizou.nl>
+ * Copyright (C) 2000 - 2008 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifdef HAVE_CONFIG_H
+# include "config.h" // only include where actually required!
+#endif
+
+#include <vector>
+#include "style.h"
+#include "rdf.h"
+
+#include "actions/actions-tools.h"
+#include "display/control/canvas-grid.h"
+#include "document-properties.h"
+#include "include/gtkmm_version.h"
+#include "io/sys.h"
+#include "object/sp-root.h"
+#include "object/sp-script.h"
+#include "object/color-profile.h"
+#include "ui/dialog/filedialog.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "ui/shape-editor.h"
+#include "ui/widget/entity-entry.h"
+#include "ui/widget/notebook-page.h"
+#include "xml/node-event-vector.h"
+
+#include "page-manager.h"
+#include "svg/svg-color.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 on_child_added(Inkscape::XML::Node *repr, Inkscape::XML::Node *child, Inkscape::XML::Node *ref, void * data);
+static void on_child_removed(Inkscape::XML::Node *repr, Inkscape::XML::Node *child, Inkscape::XML::Node *ref, void * data);
+static void on_repr_attr_changed (Inkscape::XML::Node *, gchar const *, gchar const *, gchar const *, bool, gpointer);
+
+static Inkscape::XML::NodeEventVector const _repr_events = {
+ on_child_added, // child_added
+ on_child_removed, // child_removed
+ on_repr_attr_changed,
+ nullptr, // content_changed
+ nullptr // order_changed
+};
+
+static void docprops_style_button(Gtk::Button& btn, char const* iconName)
+{
+ GtkWidget *child = sp_get_icon_image(iconName, GTK_ICON_SIZE_SMALL_TOOLBAR);
+ gtk_widget_show( child );
+ btn.add(*Gtk::manage(Glib::wrap(child)));
+ btn.set_relief(Gtk::RELIEF_NONE);
+}
+
+DocumentProperties& DocumentProperties::getInstance()
+{
+ DocumentProperties &instance = *new DocumentProperties();
+ instance.init();
+
+ return instance;
+}
+
+DocumentProperties::DocumentProperties()
+ : 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 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)
+{
+ 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));
+}
+
+void DocumentProperties::init()
+{
+ 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, SPAttr::INKSCAPE_PAGEOPACITY);
+ 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;
+ }
+ _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;
+ }
+}
+
+/**
+ * Cleans up name to remove disallowed characters.
+ * Some discussion at http://markmail.org/message/bhfvdfptt25kgtmj
+ * Allowed ASCII first characters: ':', 'A'-'Z', '_', 'a'-'z'
+ * Allowed ASCII remaining chars add: '-', '.', '0'-'9',
+ *
+ * @param str the string to clean up.
+ */
+static void sanitizeName( Glib::ustring& str )
+{
+ if (str.size() > 0) {
+ char val = str.at(0);
+ if (((val < 'A') || (val > 'Z'))
+ && ((val < 'a') || (val > 'z'))
+ && (val != '_')
+ && (val != ':')) {
+ str.insert(0, "_");
+ }
+ for (Glib::ustring::size_type i = 1; i < str.size(); i++) {
+ char val = str.at(i);
+ if (((val < 'A') || (val > 'Z'))
+ && ((val < 'a') || (val > 'z'))
+ && ((val < '0') || (val > '9'))
+ && (val != '_')
+ && (val != ':')
+ && (val != '-')
+ && (val != '.')) {
+ str.replace(i, 1, "-");
+ }
+ }
+ }
+}
+
+/// Links the selected color profile in the combo box to the document
+void DocumentProperties::linkSelectedProfile()
+{
+ //store this profile in the SVG document (create <color-profile> element in the XML)
+ 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());
+ Glib::ustring nameStr = tmp ? tmp : "profile"; // TODO add some auto-numbering to avoid collisions
+ sanitizeName(nameStr);
+ cprofRepr->setAttribute("name", nameStr);
+ cprofRepr->setAttribute("xlink:href", Glib::filename_to_uri(Glib::filename_from_utf8(file)));
+ cprofRepr->setAttribute("id", file);
+
+
+ // Checks whether there is a defs element. Creates it when needed
+ Inkscape::XML::Node *defsRepr = sp_repr_lookup_name(xml_doc, "svg:defs");
+ if (!defsRepr) {
+ defsRepr = xml_doc->createElement("svg:defs");
+ xml_doc->root()->addChild(defsRepr, nullptr);
+ }
+
+ g_assert(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) {
+ SPScript* script = dynamic_cast<SPScript *>(obj);
+ if (script && (name == script->xlinkhref)) {
+
+ //XML Tree being used directly here while it shouldn't be.
+ Inkscape::XML::Node *repr = obj->getRepr();
+ if (repr){
+ sp_repr_unparent(repr);
+
+ // inform the document, so we can undo
+ DocumentUndo::done(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) {
+ SPScript* script = dynamic_cast<SPScript *>(obj);
+ g_assert(script != nullptr);
+ if (script->xlinkhref)
+ {
+ Gtk::TreeModel::Row row = *(_ExternalScriptsListStore->append());
+ row[_ExternalScriptsListColumns.filenameColumn] = script->xlinkhref;
+ }
+ else // Embedded scripts
+ {
+ Gtk::TreeModel::Row row = *(_EmbeddedScriptsListStore->append());
+ row[_EmbeddedScriptsListColumns.idColumn] = obj->getId();
+ }
+ }
+}
+
+/**
+* Called for _updating_ the dialog. DO NOT call this a lot. It's expensive!
+*/
+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->repr->attribute("id")) continue; // update_gridspage is called again when "id" is added
+ Glib::ustring name(grid->repr->attribute("id"));
+ const char *icon = nullptr;
+ switch (grid->getGridType()) {
+ case GRID_RECTANGULAR:
+ icon = "grid-rectangular";
+ break;
+ case GRID_AXONOMETRIC:
+ icon = "grid-axonometric";
+ break;
+ default:
+ break;
+ }
+ _grids_notebook.append_page(*grid->newWidget(), _createPageTabLabel(name, icon));
+ }
+ _grids_notebook.show_all();
+
+ int cur_page_count = _grids_notebook.get_n_pages();
+ if (cur_page_count > 0) {
+ _grids_button_remove.set_sensitive(true);
+
+ // The following is not correct if grid added/removed via XML
+ if (cur_page_count == prev_page_count + 1) {
+ _grids_notebook.set_current_page(cur_page_count - 1);
+ } else if (cur_page_count == prev_page_count) {
+ _grids_notebook.set_current_page(prev_page_pos);
+ } else if (cur_page_count == prev_page_count - 1) {
+ _grids_notebook.set_current_page(prev_page_pos < 1 ? 0 : prev_page_pos - 1);
+ }
+ } else {
+ _grids_button_remove.set_sensitive(false);
+ }
+}
+
+/**
+ * Build grid page of dialog.
+ */
+void DocumentProperties::build_gridspage()
+{
+ /// \todo FIXME: gray out snapping when grid is off.
+ /// Dissenting view: you want snapping without grid.
+
+ _grids_label_crea.set_markup(_("<b>Creation</b>"));
+ _grids_label_def.set_markup(_("<b>Defined grids</b>"));
+ _grids_hbox_crea.pack_start(_grids_combo_gridtype, true, true);
+ _grids_hbox_crea.pack_start(_grids_button_new, true, true);
+
+ for (gint t = 0; t <= GRID_MAXTYPENR; t++) {
+ _grids_combo_gridtype.append( CanvasGrid::getName( (GridType) t ) );
+ }
+ _grids_combo_gridtype.set_active_text( CanvasGrid::getName(GRID_RECTANGULAR) );
+
+ _grids_space.set_size_request (SPACE_SIZE_X, SPACE_SIZE_Y);
+
+ _grids_vbox.set_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::AntiAlias, root->style->shape_rendering.computed != SP_CSS_SHAPE_RENDERING_CRISPEDGES);
+
+ //-----------------------------------------------------------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 */
+ if (auto document = getDocument()) {
+ for (auto & it : _rdflist)
+ it->update(document);
+
+ _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::watch_connection::connect(Inkscape::XML::Node* node, const Inkscape::XML::NodeEventVector& vector, void* data) {
+ disconnect();
+ if (!node) return;
+
+ _node = node;
+ _data = data;
+ node->addListener(&vector, data);
+}
+
+void DocumentProperties::watch_connection::disconnect() {
+ if (_node) {
+ _node->removeListenerByData(_data);
+ _node = nullptr;
+ _data = nullptr;
+ }
+}
+
+void DocumentProperties::documentReplaced()
+{
+ _root_connection.disconnect();
+ _namedview_connection.disconnect();
+
+ if (auto desktop = getDesktop()) {
+ _wr.setDesktop(desktop);
+ _namedview_connection.connect(desktop->getNamedView()->getRepr(), _repr_events, this);
+ if (auto document = desktop->getDocument()) {
+ _root_connection.connect(document->getRoot()->getRepr(), _repr_events, this);
+ }
+ populate_linked_profiles_box();
+ update_widgets();
+ }
+}
+
+void DocumentProperties::update()
+{
+ update_widgets();
+}
+
+static void on_child_added(Inkscape::XML::Node */*repr*/, Inkscape::XML::Node */*child*/, Inkscape::XML::Node */*ref*/, void *data)
+{
+ if (DocumentProperties *dialog = static_cast<DocumentProperties *>(data))
+ dialog->update_gridspage();
+}
+
+static void on_child_removed(Inkscape::XML::Node */*repr*/, Inkscape::XML::Node */*child*/, Inkscape::XML::Node */*ref*/, void *data)
+{
+ if (DocumentProperties *dialog = static_cast<DocumentProperties *>(data))
+ dialog->update_gridspage();
+}
+
+
+
+/**
+ * Called when XML node attribute changed; updates dialog widgets.
+ */
+static void on_repr_attr_changed(Inkscape::XML::Node *, gchar const *, gchar const *, gchar const *, bool, gpointer data)
+{
+ if (DocumentProperties *dialog = static_cast<DocumentProperties *>(data))
+ dialog->update_widgets();
+}
+
+
+/*########################################################################
+# BUTTON CLICK HANDLERS (callbacks)
+########################################################################*/
+
+void DocumentProperties::onNewGrid()
+{
+ if (auto desktop = getDesktop()) {
+ Inkscape::XML::Node *repr = desktop->getNamedView()->getRepr();
+ Glib::ustring typestring = _grids_combo_gridtype.get_active_text();
+ CanvasGrid::writeNewGridToRepr(repr, getDocument(), CanvasGrid::getGridTypeFromName(typestring.c_str()));
+
+ // toggle grid showing to ON:
+ desktop->showGrids(true);
+ }
+}
+
+
+void DocumentProperties::onRemoveGrid()
+{
+ gint pagenum = _grids_notebook.get_current_page();
+ if (pagenum == -1) // no pages
+ return;
+
+ SPNamedView *nv = getDesktop()->getNamedView();
+ Inkscape::CanvasGrid * 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->repr->parent()->removeChild(found_grid->repr);
+ 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;
+ }
+
+ Inkscape::XML::Node *repr = getDesktop()->getNamedView()->getRepr();
+ /*Inkscape::Util::Unit const *old_doc_unit = unit_table.getUnit("px");
+ if(repr->attribute("inkscape:document-units")) {
+ old_doc_unit = unit_table.getUnit(repr->attribute("inkscape:document-units"));
+ }*/
+ // Inkscape::Util::Unit const *doc_unit = _rum_deflt.getUnit();
+
+ // Set document unit
+ Inkscape::SVGOStringStream os;
+ os << doc_unit->abbr;
+ repr->setAttribute("inkscape:document-units", os.str());
+
+ // Disable changing of SVG Units. The intent here is to change the units in the UI, not the units in SVG.
+ // This code should be moved (and fixed) once we have an "SVG Units" setting that sets what units are used in SVG data.
+#if 0
+ // Set viewBox
+ if (doc->getRoot()->viewBox_set) {
+ gdouble scale = Inkscape::Util::Quantity::convert(1, old_doc_unit, doc_unit);
+ doc->setViewBox(doc->getRoot()->viewBox*Geom::Scale(scale));
+ } else {
+ Inkscape::Util::Quantity width = doc->getWidth();
+ Inkscape::Util::Quantity height = doc->getHeight();
+ doc->setViewBox(Geom::Rect::from_xywh(0, 0, width.value(doc_unit), height.value(doc_unit)));
+ }
+
+ // TODO: Fix bug in nodes tool instead of switching away from it
+ if (get_active_tool(get_desktop()) == "Node") {
+ set_active_tool(get_desktop(), "Select");
+ }
+
+ // Scale and translate objects
+ // set transform options to scale all things with the transform, so all things scale properly after the viewbox change.
+ /// \todo this "low-level" code of changing viewbox/unit should be moved somewhere else
+
+ // save prefs
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool transform_stroke = prefs->getBool("/options/transform/stroke", true);
+ bool transform_rectcorners = prefs->getBool("/options/transform/rectcorners", true);
+ bool transform_pattern = prefs->getBool("/options/transform/pattern", true);
+ bool transform_gradient = prefs->getBool("/options/transform/gradient", true);
+
+ prefs->setBool("/options/transform/stroke", true);
+ prefs->setBool("/options/transform/rectcorners", true);
+ prefs->setBool("/options/transform/pattern", true);
+ prefs->setBool("/options/transform/gradient", true);
+ {
+ ShapeEditor::blockSetItem(true);
+ gdouble viewscale = 1.0;
+ Geom::Rect vb = doc->getRoot()->viewBox;
+ if ( !vb.hasZeroArea() ) {
+ gdouble viewscale_w = doc->getWidth().value("px") / vb.width();
+ gdouble viewscale_h = doc->getHeight().value("px")/ vb.height();
+ viewscale = std::min(viewscale_h, viewscale_w);
+ }
+ gdouble scale = Inkscape::Util::Quantity::convert(1, old_doc_unit, doc_unit);
+ doc->getRoot()->scaleChildItemsRec(Geom::Scale(scale), Geom::Point(-viewscale*doc->getRoot()->viewBox.min()[Geom::X] +
+ (doc->getWidth().value("px") - viewscale*doc->getRoot()->viewBox.width())/2,
+ viewscale*doc->getRoot()->viewBox.min()[Geom::Y] +
+ (doc->getHeight().value("px") + viewscale*doc->getRoot()->viewBox.height())/2),
+ false);
+ ShapeEditor::blockSetItem(false);
+ }
+ prefs->setBool("/options/transform/stroke", transform_stroke);
+ prefs->setBool("/options/transform/rectcorners", transform_rectcorners);
+ prefs->setBool("/options/transform/pattern", transform_pattern);
+ prefs->setBool("/options/transform/gradient", transform_gradient);
+#endif
+
+ document->setModifiedSinceSave();
+
+ DocumentUndo::done(document, _("Changed default display unit"), "");
+}
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/document-properties.h b/src/ui/dialog/document-properties.h
new file mode 100644
index 0000000..26bae47
--- /dev/null
+++ b/src/ui/dialog/document-properties.h
@@ -0,0 +1,253 @@
+// 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"
+
+namespace Inkscape {
+ namespace XML {
+ class Node;
+ }
+ namespace UI {
+ namespace Widget {
+ class EntityEntry;
+ class NotebookPage;
+ class PageProperties;
+ }
+ namespace Dialog {
+
+typedef std::list<UI::Widget::EntityEntry*> RDElist;
+
+class DocumentProperties : public DialogBase
+{
+public:
+ 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();
+ void init();
+
+ 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:
+ DocumentProperties();
+ ~DocumentProperties() override;
+
+ // 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);
+
+ struct watch_connection {
+ ~watch_connection() { disconnect(); }
+ void connect(Inkscape::XML::Node* node, const Inkscape::XML::NodeEventVector& vector, void* data);
+ void disconnect();
+ private:
+ Inkscape::XML::Node* _node = nullptr;
+ void* _data = nullptr;
+ };
+ // nodes connected to listeners
+ watch_connection _namedview_connection;
+ watch_connection _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/export-batch.cpp b/src/ui/dialog/export-batch.cpp
new file mode 100644
index 0000000..ae92f9e
--- /dev/null
+++ b/src/ui/dialog/export-batch.cpp
@@ -0,0 +1,768 @@
+// 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-batch.h"
+
+#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/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/dialog-notebook.h"
+#include "ui/dialog/filedialog.h"
+#include "ui/interface.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 {
+
+class BatchItem : public Gtk::FlowBoxChild
+{
+public:
+ BatchItem(SPItem *item);
+ BatchItem(SPPage *page);
+ ~BatchItem() override = default;
+
+ Glib::ustring getLabel() { return _label_str; }
+ SPItem *getItem() { return _item; }
+ SPPage *getPage() { return _page; }
+ bool isActive() { return _selector.get_active(); }
+ void refresh(bool hide = false);
+ void refreshHide(const std::vector<SPItem *> &list) { _preview.refreshHide(list); }
+ void setDocument(SPDocument *doc) { _preview.setDocument(doc); }
+
+private:
+ void init(SPDocument *doc, Glib::ustring label);
+
+ Glib::ustring _label_str;
+ Gtk::Grid _grid;
+ Gtk::Label _label;
+ Gtk::CheckButton _selector;
+ ExportPreview _preview;
+ SPItem *_item = nullptr;
+ SPPage *_page = nullptr;
+ bool is_hide = false;
+};
+
+BatchItem::BatchItem(SPItem *item)
+{
+ _item = item;
+
+ Glib::ustring id = _item->defaultLabel();
+ if (id.empty()) {
+ if (auto _id = _item->getId()) {
+ id = _id;
+ } else {
+ id = "no-id";
+ }
+ }
+ init(_item->document, id);
+}
+
+BatchItem::BatchItem(SPPage *page)
+{
+ _page = page;
+
+ Glib::ustring label = _page->getDefaultLabel();
+ if (auto id = _page->label()) {
+ label = id;
+ }
+ init(_page->document, label);
+}
+
+void BatchItem::init(SPDocument *doc, Glib::ustring label) {
+ _label_str = label;
+
+ _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);
+
+ _preview.set_name("export_preview_batch");
+ _preview.setItem(_item);
+ _preview.setDocument(doc);
+ _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);
+ _label.set_text(label);
+
+ set_valign(Gtk::Align::ALIGN_START);
+ set_halign(Gtk::Align::ALIGN_START);
+ add(_grid);
+ show();
+ this->set_can_focus(false);
+ this->set_tooltip_text(label);
+
+ // This initially packs the widgets with a hidden preview.
+ refresh(!is_hide);
+}
+
+void BatchItem::refresh(bool hide)
+{
+ if (_page) {
+ auto b = _page->getDesktopRect();
+ _preview.setDbox(b.left(), b.right(), b.top(), b.bottom());
+ }
+
+ // 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(_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(_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(_label, 0, 2, 2, 1);
+ _grid.attach(_preview, 0, 0, 2, 2);
+ }
+ show_all_children();
+ }
+
+ if (!hide) {
+ _preview.queueRefresh();
+ }
+}
+
+
+void BatchExport::initialise(const Glib::RefPtr<Gtk::Builder> &builder)
+{
+ 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_progress_bar", _prog);
+ builder->get_widget_derived("b_export_list", export_list);
+
+ Inkscape::UI::Widget::ScrollTransfer<Gtk::ScrolledWindow> *temp = nullptr;
+ builder->get_widget_derived("b_pbox_scroll", temp);
+ builder->get_widget_derived("b_scroll", temp);
+}
+
+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;
+ }
+ refreshItems();
+}
+
+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;
+ }
+ }
+ refreshItems();
+ loadExportHints();
+}
+
+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();
+ }
+
+ refreshItems();
+ loadExportHints();
+}
+
+// Setup Single Export.Called by export on realize
+void BatchExport::setup()
+{
+ if (setupDone) {
+ return;
+ }
+ setupDone = true;
+ prefs = Inkscape::Preferences::get();
+
+ export_list->setup();
+
+ // set them before connecting to signals
+ setDefaultSelectionMode();
+ loadExportHints();
+
+ refreshItems();
+
+ // 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));
+ filenameConn = filename_entry->signal_changed().connect(sigc::mem_fun(*this, &BatchExport::onFilenameModified));
+ exportConn = export_btn->signal_clicked().connect(sigc::mem_fun(*this, &BatchExport::onExport));
+ browseConn = filename_entry->signal_icon_release().connect(sigc::mem_fun(*this, &BatchExport::onBrowse));
+ hide_all->signal_toggled().connect(sigc::mem_fun(*this, &BatchExport::refreshPreview));
+}
+
+void BatchExport::refreshItems()
+{
+ if (!_desktop || !_document) return;
+
+ // Create New List of Items
+ std::set<SPItem *> itemsList;
+ std::set<SPPage *> pageList;
+
+ 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);
+ }
+ 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 = pageList.find(page);
+ if (pageItr == pageList.end() || !(*pageItr)->getId() || (*pageItr)->getId() != key) {
+ toRemove.push_back(key);
+ }
+ }
+ }
+
+ // now remove all the items
+ for (auto key : toRemove) {
+ if (current_items[key]) {
+ // Preview Boxes are GTK managed so simply removing from container will handle delete
+ 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] = Gtk::manage(new BatchItem(item));
+ preview_container->insert(*current_items[id], -1);
+ }
+ }
+ for (auto &page : pageList) {
+ if (auto id = page->getId()) {
+ if (current_items[id] && current_items[id]->getPage() == page) {
+ continue;
+ }
+ current_items[id] = Gtk::manage(new BatchItem(page));
+ preview_container->insert(*current_items[id], -1);
+ }
+ }
+
+ 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);
+
+ for (auto &[key, val] : current_items) {
+ if (preview) {
+ std::vector<SPItem *> selected;
+ if (hide) {
+ if (auto item = val->getItem()) {
+ selected = std::vector<SPItem *>({item});
+ } else if (val->getPage()) {
+ auto sels = _desktop->getSelection()->items();
+ selected = std::vector<SPItem *>(sels.begin(), sels.end());
+ }
+ }
+ val->refreshHide(selected);
+ }
+ val->refresh(!preview);
+ }
+}
+
+void BatchExport::loadExportHints()
+{
+ 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]);
+
+ refreshItems();
+ loadExportHints();
+}
+
+void BatchExport::onFilenameModified()
+{
+ ;
+}
+
+void BatchExport::onExport()
+{
+ interrupted = false;
+ if (!_desktop)
+ return;
+ export_btn->set_sensitive(false);
+
+ // 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."));
+ export_btn->set_sensitive(true);
+ return;
+ }
+
+ // 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);
+
+ // create vector of exports
+ int num_rows = export_list->get_rows();
+ std::vector<Glib::ustring> suffixs;
+ std::vector<Inkscape::Extension::Output *> extensions;
+ std::vector<double> dpis;
+ for (int i = 0; i < num_rows; i++) {
+ suffixs.push_back(export_list->get_suffix(i));
+ extensions.push_back(export_list->getExtension(i));
+ dpis.push_back(export_list->get_dpi(i));
+ }
+
+ // We are exporting standalone items only for now
+ // std::vector<SPItem *> selected(_desktop->getSelection()->items().begin(),
+ // _desktop->getSelection()->items().end());
+ bool hide = hide_all->get_active();
+
+ auto sels = _desktop->getSelection()->items();
+ std::vector<SPItem *> selected_items(sels.begin(), sels.end());
+
+ // Start Exporting Each Item
+ for (int i = 0; i < num_rows; i++) {
+ auto suffix = suffixs[i];
+ auto omod = extensions[i];
+ float dpi = dpis[i];
+
+ if (!omod || omod->deactivated() || !omod->prefs()) {
+ continue;
+ }
+
+ int count = 0;
+ for (auto i = current_items.begin(); i != current_items.end() && !interrupted; ++i) {
+ count++;
+
+ BatchItem *batchItem = i->second;
+ if (!batchItem->isActive()) {
+ 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 + "_" + id;
+ if (!suffix.empty()) {
+ if (omod->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, omod->get_extension());
+ if (!found) {
+ continue;
+ }
+
+ delete prog_dlg;
+ prog_dlg = create_progress_dialog(Glib::ustring::compose(_("Exporting %1 files"), num));
+ prog_dlg->set_export_panel(this);
+ setExporting(true, Glib::ustring::compose(_("Exporting %1 files"), num));
+ prog_dlg->set_current(count);
+ prog_dlg->set_total(num);
+
+ onProgressCallback(0.0, prog_dlg);
+
+ if (omod->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, item_filename, true, onProgressCallback,
+ prog_dlg, omod, hide ? &show_only : nullptr);
+ } else {
+ setExporting(true, Glib::ustring::compose(_("Exporting %1"), filename));
+ auto copy_doc = _document->copy();
+ Export::exportVector(omod, copy_doc.get(), item_filename, true, &show_only, page);
+ }
+
+ if (prog_dlg) {
+ delete prog_dlg;
+ prog_dlg = nullptr;
+ }
+ }
+ }
+ // 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();
+ browseConn.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::RASTER_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;
+ onExport();
+ } else {
+ delete dialog;
+ }
+ browseConn.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)
+{
+ if (exporting) {
+ _prog->set_text(text);
+ _prog->set_fraction(0.0);
+ _prog->set_sensitive(true);
+ export_btn->set_sensitive(false);
+ } else {
+ _prog->set_text("");
+ _prog->set_fraction(0.0);
+ _prog->set_sensitive(false);
+ export_btn->set_sensitive(true);
+ }
+}
+
+ExportProgressDialog *BatchExport::create_progress_dialog(Glib::ustring progress_text)
+{
+ // dont forget to delete it later
+ auto dlg = new ExportProgressDialog(_("Export in progress"), true);
+ dlg->set_transient_for(*(INKSCAPE.active_desktop()->getToplevel()));
+
+ Gtk::ProgressBar *prg = Gtk::manage(new Gtk::ProgressBar());
+ prg->set_text(progress_text);
+ dlg->set_progress(prg);
+ auto CA = dlg->get_content_area();
+ CA->pack_start(*prg, FALSE, FALSE, 4);
+
+ Gtk::Button *btn = dlg->add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL);
+
+ btn->signal_clicked().connect(sigc::mem_fun(*this, &BatchExport::onProgressCancel));
+ dlg->signal_delete_event().connect(sigc::mem_fun(*this, &BatchExport::onProgressDelete));
+
+ dlg->show_all();
+ return dlg;
+}
+
+/// Called when dialog is deleted
+bool BatchExport::onProgressDelete(GdkEventAny * /*event*/)
+{
+ interrupted = true;
+ prog_dlg->set_stopped();
+ return TRUE;
+}
+
+/// Called when progress is cancelled
+void BatchExport::onProgressCancel()
+{
+ interrupted = true;
+ prog_dlg->set_stopped();
+}
+
+/// Called for every progress iteration
+unsigned int BatchExport::onProgressCallback(float value, void *dlg)
+{
+ auto dlg2 = reinterpret_cast<ExportProgressDialog *>(dlg);
+
+ auto self = dynamic_cast<BatchExport *>(dlg2->get_export_panel());
+
+ if (!self || self->interrupted)
+ return FALSE;
+
+ auto current = dlg2->get_current();
+ auto total = dlg2->get_total();
+ if (total > 0) {
+ double completed = current;
+ completed /= static_cast<double>(total);
+
+ value = completed + (value / static_cast<double>(total));
+ }
+
+ auto prg = dlg2->get_progress();
+ prg->set_fraction(value);
+
+ if (self) {
+ self->_prog->set_fraction(value);
+ }
+
+ int evtcount = 0;
+ while ((evtcount < 16) && gdk_events_pending()) {
+ Gtk::Main::iteration(false);
+ evtcount += 1;
+ }
+
+ Gtk::Main::iteration(false);
+ return TRUE;
+}
+
+void BatchExport::setDesktop(SPDesktop *desktop)
+{
+ if (desktop != _desktop) {
+ _pages_changed_connection.disconnect();
+ _desktop = desktop;
+ }
+}
+
+void BatchExport::setDocument(SPDocument *document)
+{
+ if (!_desktop) {
+ document = nullptr;
+ }
+
+ _document = document;
+ _pages_changed_connection.disconnect();
+ if (document) {
+ // when the page selected is changes, update the export area
+ _pages_changed_connection = document->getPageManager().connectPagesChanged([=]() { pagesChanged(); });
+ }
+ for (auto &[key, val] : current_items) {
+ val->setDocument(document);
+ }
+}
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/export-batch.h b/src/ui/dialog/export-batch.h
new file mode 100644
index 0000000..9c49f29
--- /dev/null
+++ b/src/ui/dialog/export-batch.h
@@ -0,0 +1,163 @@
+// 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 "ui/widget/scrollprotected.h"
+
+class InkscapeApplication;
+class SPDocument;
+class SPDesktop;
+
+namespace Inkscape {
+ class Preferences;
+ class Selection;
+
+namespace UI {
+namespace Dialog {
+
+class ExportList;
+class BatchItem;
+class ExportProgressDialog;
+
+class BatchExport : public Gtk::Box
+{
+public:
+ BatchExport() {};
+ BatchExport(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade)
+ : Gtk::Box(cobject){};
+ ~BatchExport() override = default;
+
+private:
+ InkscapeApplication *_app;
+ SPDesktop *_desktop = nullptr;
+ SPDocument *_document = nullptr;
+
+private:
+ bool setupDone = false; // To prevent setup() call add connections again.
+
+public:
+ 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();
+
+private:
+ enum selection_mode
+ {
+ SELECTION_LAYER = 0, // Default is alaways placed first
+ SELECTION_SELECTION,
+ SELECTION_PAGE,
+ };
+
+private:
+ typedef Inkscape::UI::Widget::ScrollProtected<Gtk::SpinButton> SpinButton;
+
+ 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::ProgressBar *_prog = nullptr;
+ ExportList *export_list = nullptr;
+
+ // Store all items to be displayed in flowbox
+ std::map<std::string, BatchItem *> current_items;
+
+ bool filename_modified;
+ 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;
+
+public:
+ // initialise variables from builder
+ void initialise(const Glib::RefPtr<Gtk::Builder> &builder);
+ void setup();
+
+private:
+ void setDefaultSelectionMode();
+ void onFilenameModified();
+ void onAreaTypeToggle(selection_mode key);
+ void onExport();
+ void onBrowse(Gtk::EntryIconPosition pos, const GdkEventButton *ev);
+
+ void refreshPreview();
+ void refreshItems();
+ void loadExportHints();
+
+public:
+ void refresh()
+ {
+ refreshItems();
+ loadExportHints();
+ };
+
+private:
+ void setExporting(bool exporting, Glib::ustring const &text = "");
+ ExportProgressDialog *create_progress_dialog(Glib::ustring progress_text);
+ /**
+ * Callback to be used in for loop to update the progress bar.
+ *
+ * @param value number between 0 and 1 indicating the fraction of progress (0.17 = 17 % progress)
+ * @param dlg void pointer to the Gtk::Dialog progress dialog
+ */
+ static unsigned int onProgressCallback(float value, void *dlg);
+
+ /**
+ * Callback for pressing the cancel button.
+ */
+ void onProgressCancel();
+
+ /**
+ * Callback invoked on closing the progress dialog.
+ */
+ bool onProgressDelete(GdkEventAny *event);
+
+private:
+ ExportProgressDialog *prog_dlg = nullptr;
+ bool interrupted;
+
+ // Gtk Signals
+ sigc::connection filenameConn;
+ sigc::connection exportConn;
+ sigc::connection browseConn;
+ sigc::connection selectionModifiedConn;
+ sigc::connection selectionChangedConn;
+ // SVG Signals
+ sigc::connection _pages_changed_connection;
+};
+} // 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..dc17227
--- /dev/null
+++ b/src/ui/dialog/export-single.cpp
@@ -0,0 +1,990 @@
+// 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/export-lists.h"
+#include "ui/widget/export-preview.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 {
+
+
+/**
+ * Initialise Builder Objects. Called in Export constructor.
+ */
+void SingleExport::initialise(const Glib::RefPtr<Gtk::Builder> &builder)
+{
+ 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("page_prev", page_prev);
+ page_prev->signal_clicked().connect([=]() {
+ if (_document && _document->getPageManager().selectPrevPage()) {
+ _document->getPageManager().zoomToSelectedPage(_desktop);
+ }
+ });
+
+ builder->get_widget("page_next", page_next);
+ page_next->signal_clicked().connect([=]() {
+ if (_document && _document->getPageManager().selectNextPage()) {
+ _document->getPageManager().zoomToSelectedPage(_desktop);
+ }
+ });
+
+ // builder->get_widget("si_show_export_area", show_export_area);
+ builder->get_widget_derived("si_units", units);
+ builder->get_widget("si_units_row", si_units_row);
+ builder->get_widget("si_area_name", si_name_label);
+
+ builder->get_widget("si_hide_all", si_hide_all);
+ builder->get_widget("si_show_preview", si_show_preview);
+ builder->get_widget("si_default_opts", si_default_opts);
+ builder->get_widget_derived("si_preview", preview);
+
+ builder->get_widget_derived("si_extention", si_extension_cb);
+ builder->get_widget("si_filename", si_filename_entry);
+ builder->get_widget("si_export", si_export);
+
+ builder->get_widget("si_progress", _prog);
+}
+
+// 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;
+ prefs = Inkscape::Preferences::get();
+ si_extension_cb->setup();
+
+ setupUnits();
+ setupSpinButtons();
+
+ // set them before connecting to signals
+ setDefaultSelectionMode();
+
+ // Refresh values to sync them with defaults.
+ refreshArea();
+ refreshPage();
+ loadExportHints();
+
+ // 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));
+ 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));
+
+ si_default_opts->set_active(prefs->getBool("/dialogs/export/defaultopts", true));
+}
+
+// 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;
+
+ 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:
+ 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()
+{
+ bool pages = current_key == SELECTION_PAGE;
+ si_name_label->set_visible(pages);
+ page_prev->set_visible(pages);
+ page_next->set_visible(pages);
+
+ auto &page_manager = _document->getPageManager();
+ page_prev->set_sensitive(page_manager.hasPrevPage());
+ page_next->set_sensitive(page_manager.hasNextPage());
+
+ if (auto page = page_manager.getSelected()) {
+ if (auto label = page->label()) {
+ si_name_label->set_text(label);
+ } else {
+ si_name_label->set_text(page->getDefaultLabel());
+ }
+ } else {
+ si_name_label->set_text(_("First Page"));
+ }
+}
+
+void SingleExport::loadExportHints()
+{
+ if (filename_modified) return;
+
+ Glib::ustring old_filename = si_filename_entry->get_text();
+ Glib::ustring filename;
+ Geom::Point dpi;
+ switch (current_key) {
+ case SELECTION_PAGE:
+ if (auto page = _document->getPageManager().getSelected()) {
+ dpi = page->getExportDpi();
+ filename = page->getExportFilename();
+ if (filename.empty()) {
+ filename = Export::filePathFromId(_document, page->getLabel(), old_filename);
+ }
+ break;
+ }
+ // No page means page 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]);
+
+ refreshPage();
+ refreshArea();
+ loadExportHints();
+ toggleSpinButtonVisibility();
+}
+
+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()
+{
+ filenameConn.block();
+ Glib::ustring filename = si_filename_entry->get_text();
+ if (auto ext = si_extension_cb->getExtension()) {
+ si_extension_cb->removeExtension(filename);
+ ext->add_extension(filename);
+ }
+ si_filename_entry->set_text(filename);
+ si_filename_entry->set_position(filename.length());
+ filenameConn.unblock();
+}
+
+void SingleExport::onExport()
+{
+ interrupted = false;
+ if (!_desktop || !_document)
+ return;
+
+ auto &page_manager = _document->getPageManager();
+ auto selection = _desktop->getSelection();
+ si_export->set_sensitive(false);
+ bool exportSuccessful = false;
+ auto omod = si_extension_cb->getExtension();
+ if (!omod) {
+ si_export->set_sensitive(true);
+ return;
+ }
+
+ 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));
+
+ bool default_opts = si_default_opts->get_active();
+ prefs->setBool("/dialogs/export/defaultopts", default_opts);
+ if (!default_opts && !omod->prefs()) {
+ si_export->set_sensitive(true);
+ return; // cancel button
+ }
+
+ 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();
+
+ /* TRANSLATORS: %1 will be the filename, %2 the width, and %3 the height of the image */
+ prog_dlg = create_progress_dialog(Glib::ustring::compose(_("Exporting %1 (%2 x %3)"), filename, width, height));
+ prog_dlg->set_export_panel(this);
+ setExporting(true, Glib::ustring::compose(_("Exporting %1 (%2 x %3)"), filename, width, height));
+ prog_dlg->set_current(0);
+ prog_dlg->set_total(0);
+
+ std::vector<SPItem *> selected(selection->items().begin(), selection->items().end());
+
+ exportSuccessful = Export::exportRaster(
+ area, width, height, dpi, filename, false, onProgressCallback, prog_dlg,
+ 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);
+ }
+ }
+
+ SPPage *page;
+ if (current_key == SELECTION_PAGE && page_manager.hasPages()) {
+ page = page_manager.getSelected();
+ } 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
+ page = copy_doc->getPageManager().newDocumentPage(area);
+ }
+
+ exportSuccessful = Export::exportVector(omod, copy_doc.get(), filename, false, &items, page);
+ }
+ if (prog_dlg) {
+ delete prog_dlg;
+ prog_dlg = nullptr;
+ }
+ // Save the export hints back to the svg document
+ if (exportSuccessful) {
+ 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::RASTER_TYPES, _("Select a filename for exporting"), "", "",
+ Inkscape::Extension::FILE_SAVE_METHOD_EXPORT);
+
+ if (dialog->show()) {
+ filename = dialog->getFilename();
+ if (auto ext = si_extension_cb->getExtension()) {
+ si_extension_cb->removeExtension(filename);
+ ext->add_extension(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();
+}
+
+void SingleExport::setExporting(bool exporting, Glib::ustring const &text)
+{
+ if (exporting) {
+ _prog->set_text(text);
+ _prog->set_fraction(0.0);
+ _prog->set_sensitive(true);
+ si_export->set_sensitive(false);
+ } else {
+ _prog->set_text("");
+ _prog->set_fraction(0.0);
+ _prog->set_sensitive(false);
+ si_export->set_sensitive(true);
+ }
+}
+
+ExportProgressDialog *SingleExport::create_progress_dialog(Glib::ustring progress_text)
+{
+ // dont forget to delete it later
+ auto dlg = new ExportProgressDialog(_("Export in progress"), true);
+ dlg->set_transient_for(*(INKSCAPE.active_desktop()->getToplevel()));
+
+ Gtk::ProgressBar *prg = Gtk::manage(new Gtk::ProgressBar());
+ prg->set_text(progress_text);
+ dlg->set_progress(prg);
+ auto CA = dlg->get_content_area();
+ CA->pack_start(*prg, FALSE, FALSE, 4);
+
+ Gtk::Button *btn = dlg->add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL);
+
+ btn->signal_clicked().connect(sigc::mem_fun(*this, &SingleExport::onProgressCancel));
+ dlg->signal_delete_event().connect(sigc::mem_fun(*this, &SingleExport::onProgressDelete));
+
+ dlg->show_all();
+ return dlg;
+}
+
+/// Called when dialog is deleted
+bool SingleExport::onProgressDelete(GdkEventAny * /*event*/)
+{
+ interrupted = true;
+ prog_dlg->set_stopped();
+ return TRUE;
+}
+/// Called when progress is cancelled
+void SingleExport::onProgressCancel()
+{
+ interrupted = true;
+ prog_dlg->set_stopped();
+}
+
+// Called for every progress iteration
+unsigned int SingleExport::onProgressCallback(float value, void *dlg)
+{
+ auto dlg2 = reinterpret_cast<ExportProgressDialog *>(dlg);
+
+ auto self = dynamic_cast<SingleExport *>(dlg2->get_export_panel());
+
+ if (!self || self->interrupted)
+ return FALSE;
+
+ auto current = dlg2->get_current();
+ auto total = dlg2->get_total();
+ if (total > 0) {
+ double completed = current;
+ completed /= static_cast<double>(total);
+
+ value = completed + (value / static_cast<double>(total));
+ }
+
+ auto prg = dlg2->get_progress();
+ prg->set_fraction(value);
+
+ if (self) {
+ self->_prog->set_fraction(value);
+ }
+
+ int evtcount = 0;
+ while ((evtcount < 16) && gdk_events_pending()) {
+ Gtk::Main::iteration(false);
+ evtcount += 1;
+ }
+
+ Gtk::Main::iteration(false);
+ return TRUE;
+}
+
+void SingleExport::refreshPreview()
+{
+ if (!_desktop) {
+ return;
+ }
+ if (!si_show_preview->get_active()) {
+ 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());
+ }
+
+ 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->setDbox(x0, x1, y0, y1);
+ preview->refreshHide(selected);
+ preview->queueRefresh();
+}
+
+void SingleExport::setDesktop(SPDesktop *desktop)
+{
+ if (desktop != _desktop) {
+ _page_selected_connection.disconnect();
+ _desktop = desktop;
+ }
+}
+
+void SingleExport::setDocument(SPDocument *document)
+{
+ _document = document;
+ _page_selected_connection.disconnect();
+ if (document) {
+ // when the page selected is changes, update the export area
+ _page_selected_connection = document->getPageManager().connectPageSelected([=](SPPage *page) {
+ refreshPage();
+ refresh();
+ });
+ }
+ preview->setDocument(document);
+}
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/export-single.h b/src/ui/dialog/export-single.h
new file mode 100644
index 0000000..c7c5470
--- /dev/null
+++ b/src/ui/dialog/export-single.h
@@ -0,0 +1,217 @@
+// 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;
+
+namespace Inkscape {
+ class Selection;
+ class Preferences;
+
+namespace Util {
+ class Unit;
+}
+namespace UI {
+ namespace Widget {
+ class UnitMenu;
+ }
+namespace Dialog {
+ class ExportPreview;
+ class ExtensionList;
+ class ExportProgressDialog;
+
+class SingleExport : public Gtk::Box
+{
+public:
+ SingleExport(){};
+ SingleExport(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade)
+ : Gtk::Box(cobject){};
+ ~SingleExport() override{};
+
+private:
+ InkscapeApplication *_app = nullptr;
+ SPDesktop *_desktop = nullptr;
+ SPDocument *_document = nullptr;
+
+private:
+ bool setupDone = false; // To prevent setup() call add connections again.
+
+public:
+ 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);
+
+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,
+ };
+
+private:
+ 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::Label *si_name_label = nullptr;
+
+ Gtk::CheckButton *si_hide_all = nullptr;
+ Gtk::CheckButton *si_show_preview = nullptr;
+ Gtk::CheckButton *si_default_opts = 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::ProgressBar *_prog = nullptr;
+ Gtk::Button *page_prev = nullptr;
+ Gtk::Button *page_next = 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;
+
+public:
+ // initialise variables from builder
+ void initialise(const Glib::RefPtr<Gtk::Builder> &builder);
+ void setup();
+
+private:
+ 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 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);
+
+public:
+ void refresh()
+ {
+ refreshArea();
+ refreshPage();
+ loadExportHints();
+ };
+
+private:
+ 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);
+
+private:
+ void setExporting(bool exporting, Glib::ustring const &text = "");
+ ExportProgressDialog *create_progress_dialog(Glib::ustring progress_text);
+ /**
+ * Callback to be used in for loop to update the progress bar.
+ *
+ * @param value number between 0 and 1 indicating the fraction of progress (0.17 = 17 % progress)
+ * @param dlg void pointer to the Gtk::Dialog progress dialog
+ */
+ static unsigned int onProgressCallback(float value, void *dlg);
+
+ /**
+ * Callback for pressing the cancel button.
+ */
+ void onProgressCancel();
+
+ /**
+ * Callback invoked on closing the progress dialog.
+ */
+ bool onProgressDelete(GdkEventAny *event);
+
+private:
+ ExportProgressDialog *prog_dlg = nullptr;
+ bool interrupted;
+
+private:
+ // Gtk Signals
+ std::vector<sigc::connection> spinButtonConns;
+ sigc::connection filenameConn;
+ sigc::connection extensionConn;
+ sigc::connection exportConn;
+ sigc::connection browseConn;
+ // Document Signals
+ sigc::connection _page_selected_connection;
+};
+} // 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..4895e8e
--- /dev/null
+++ b/src/ui/dialog/export.cpp
@@ -0,0 +1,511 @@
+// 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 "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 "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/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 Dialog 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);
+ single_image->initialise(builder);
+
+ // Initialise Batch Export and its objects
+ builder->get_widget_derived("Batch Export", batch_export);
+ batch_export->initialise(builder);
+
+ container->signal_realize().connect([=]() {
+ single_image->setup();
+ batch_export->setup();
+ setDefaultNotebookPage();
+ notebook_signal = export_notebook->signal_switch_page().connect(sigc::mem_fun(*this, &Export::onNotebookPageSwitch));
+ });
+ container->signal_unrealize().connect([=]() {
+ notebook_signal.disconnect();
+ });
+}
+
+Export::~Export()
+{
+ single_image->setDocument(nullptr);
+ single_image->setDesktop(nullptr);
+ batch_export->setDocument(nullptr);
+ batch_export->setDesktop(nullptr);
+}
+
+// Set current page based on preference/last visited page
+void Export::setDefaultNotebookPage()
+{
+ pages[BATCH_EXPORT] = export_notebook->page_num(*batch_export);
+ pages[SINGLE_IMAGE] = export_notebook->page_num(*single_image);
+ 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, Glib::ustring const &filename, bool overwrite,
+ unsigned (*callback)(float, void *), ExportProgressDialog *&prog_dialog,
+ 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;
+ }
+
+ auto bg_color = doc->getPageManager().background_color;
+ ExportResult result = sp_export_png_file(desktop->getDocument(), png_filename.c_str(), area, width, height, pHYs,
+ pHYs, // previously xdpi, ydpi.
+ bg_color, callback, (void *)prog_dialog, true, selected,
+ use_interlacing, color_type, bit_depth, zlib, antialiasing);
+
+ bool failed = result == EXPORT_ERROR || prog_dialog->get_stopped();
+ delete prog_dialog;
+ prog_dialog = nullptr;
+ 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;
+ }
+
+ 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);
+ }
+
+ 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, std::vector<SPItem *> *items, SPPage *page)
+{
+ 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(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;
+ }
+ doc->ensureUpToDate();
+ auto copy_doc = doc->copy();
+ copy_doc->ensureUpToDate();
+
+ std::vector<SPItem *> objects = *items;
+ std::set<std::string> page_ids;
+ if (page) {
+ // If page then our item set is limited to the overlapping items
+ auto page_items = page->getOverlappingItems();
+
+ if (items->size() == 0) {
+ // Items is page_items, remove all items not in this page.
+ objects = page_items;
+ } else {
+ for (auto &item : page_items) {
+ item->getIds(page_ids);
+ }
+ }
+ }
+
+ // Save the page rect, must be done before disabledPages in case page is from copy doc.
+ Geom::OptRect page_rect = page ? page->getDesktopRect() : Geom::OptRect();
+
+ // We never export multiple pages here, must be done before fitToRect and fitCanvas
+ copy_doc->getPageManager().disablePages();
+
+ // Page export ALWAYS restricts, even if nothing would be on the page.
+ if (objects.size() > 0 || page) {
+ std::vector<SPObject *> objects_to_export;
+ Inkscape::ObjectSet object_set(copy_doc.get());
+ for (auto &object : objects) {
+ auto _id = object->getId();
+ if (!_id || (!page_ids.empty() && page_ids.find(_id) == page_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 (page) {
+ // Resize to page here.
+ copy_doc->fitToRect(*page_rect, true);
+ } else {
+ object_set.fitCanvas(true, true);
+ }
+ }
+
+ // Remove all unused definitions
+ copy_doc->vacuumDocument();
+
+ try {
+ extension->save(copy_doc.get(), 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;
+ }
+
+ 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);
+ }
+
+ 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(nullptr);
+ }
+
+ 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;
+}
+
+
+
+} // 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..fe5f024
--- /dev/null
+++ b/src/ui/dialog/export.h
@@ -0,0 +1,138 @@
+// 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
+};
+
+class ExportProgressDialog : public Gtk::Dialog
+{
+private:
+ Gtk::ProgressBar *_progress = nullptr;
+ Gtk::Widget *_export_panel = nullptr;
+ int _current = 0;
+ int _total = 0;
+ bool _stopped = false;
+
+public:
+ ExportProgressDialog(const Glib::ustring &title, bool modal = false)
+ : Gtk::Dialog(title, modal)
+ {}
+
+ inline void set_export_panel(const decltype(_export_panel) export_panel) { _export_panel = export_panel; }
+ inline decltype(_export_panel) get_export_panel() const { return _export_panel; }
+
+ inline void set_progress(const decltype(_progress) progress) { _progress = progress; }
+ inline decltype(_progress) get_progress() const { return _progress; }
+
+ inline void set_current(const int current) { _current = current; }
+ inline int get_current() const { return _current; }
+
+ inline void set_total(const int total) { _total = total; }
+ inline int get_total() const { return _total; }
+
+ inline bool get_stopped() const { return _stopped; }
+ inline void set_stopped() { _stopped = true; }
+};
+
+class Export : public DialogBase
+{
+public:
+ Export();
+ ~Export() override;
+
+ static Export &getInstance() { return *new Export(); }
+
+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, Glib::ustring const &filename, bool overwrite,
+ unsigned (*callback)(float, void *), ExportProgressDialog *&prog_dialog,
+ Inkscape::Extension::Output *extension, std::vector<SPItem *> *items = nullptr);
+
+ static bool exportVector(
+ Inkscape::Extension::Output *extension, SPDocument *doc, Glib::ustring const &filename,
+ bool overwrite, std::vector<SPItem *> *items = nullptr, SPPage *page = nullptr);
+
+};
+
+} // 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..fdce498
--- /dev/null
+++ b/src/ui/dialog/filedialog.cpp
@@ -0,0 +1,201 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Implementation of the file dialog interfaces defined in filedialog.h.
+ */
+/* Authors:
+ * Bob Jamison
+ * Joel Holdsworth
+ * Other dudes from The Inkscape Organization
+ *
+ * Copyright (C) 2004-2007 Bob Jamison
+ * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl>
+ * Copyright (C) 2007-2008 Joel Holdsworth
+ * Copyright (C) 2004-2008 The Inkscape Organization
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifdef _WIN32
+# include "filedialogimpl-win32.h"
+# include "preferences.h"
+#endif
+
+#include "filedialogimpl-gtkmm.h"
+
+#include "ui/dialog-events.h"
+#include "extension/output.h"
+
+#include <glibmm/convert.h>
+
+namespace Inkscape
+{
+namespace UI
+{
+namespace Dialog
+{
+
+/*#########################################################################
+### U T I L I T Y
+#########################################################################*/
+
+bool hasSuffix(const Glib::ustring &str, const Glib::ustring &ext)
+{
+ int strLen = str.length();
+ int extLen = ext.length();
+ if (extLen > strLen)
+ return false;
+ int strpos = strLen-1;
+ for (int extpos = extLen-1 ; extpos>=0 ; extpos--, strpos--)
+ {
+ Glib::ustring::value_type ch = str[strpos];
+ if (ch != ext[extpos])
+ {
+ if ( ((ch & 0xff80) != 0) ||
+ static_cast<Glib::ustring::value_type>( g_ascii_tolower( static_cast<gchar>(0x07f & ch) ) ) != ext[extpos] )
+ {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+bool isValidImageFile(const Glib::ustring &fileName)
+{
+ std::vector<Gdk::PixbufFormat>formats = Gdk::Pixbuf::get_formats();
+ for (auto format : formats)
+ {
+ std::vector<Glib::ustring>extensions = format.get_extensions();
+ for (auto ext : extensions)
+ {
+ if (hasSuffix(fileName, ext))
+ return true;
+ }
+ }
+ return false;
+}
+
+/*#########################################################################
+### F I L E O P E N
+#########################################################################*/
+
+/**
+ * Public factory. Called by file.cpp, among others.
+ */
+FileOpenDialog *FileOpenDialog::create(Gtk::Window &parentWindow,
+ const Glib::ustring &path,
+ FileDialogType fileTypes,
+ const char *title)
+{
+#ifdef _WIN32
+ FileOpenDialog *dialog = NULL;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool( "/options/desktopintegration/value")) {
+ dialog = new FileOpenDialogImplWin32(parentWindow, path, fileTypes, title);
+ } else {
+ dialog = new FileOpenDialogImplGtk(parentWindow, path, fileTypes, title);
+ }
+#else
+ FileOpenDialog *dialog = new FileOpenDialogImplGtk(parentWindow, path, fileTypes, title);
+#endif
+
+ return dialog;
+}
+
+Glib::ustring FileOpenDialog::getFilename()
+{
+ return myFilename;
+}
+
+//########################################################################
+//# F I L E S A V E
+//########################################################################
+
+/**
+ * Public factory method. Used in file.cpp
+ */
+FileSaveDialog *FileSaveDialog::create(Gtk::Window& parentWindow,
+ const Glib::ustring &path,
+ FileDialogType fileTypes,
+ const char *title,
+ const Glib::ustring &default_key,
+ const gchar *docTitle,
+ const Inkscape::Extension::FileSaveMethod save_method)
+{
+#ifdef _WIN32
+ FileSaveDialog *dialog = NULL;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool( "/options/desktopintegration/value")) {
+ dialog = new FileSaveDialogImplWin32(parentWindow, path, fileTypes, title, default_key, docTitle, save_method);
+ } else {
+ dialog = new FileSaveDialogImplGtk(parentWindow, path, fileTypes, title, default_key, docTitle, save_method);
+ }
+#else
+ FileSaveDialog *dialog = new FileSaveDialogImplGtk(parentWindow, path, fileTypes, title, default_key, docTitle, save_method);
+#endif
+ return dialog;
+}
+
+Glib::ustring FileSaveDialog::getFilename()
+{
+ return myFilename;
+}
+
+Glib::ustring FileSaveDialog::getDocTitle()
+{
+ return myDocTitle;
+}
+
+//void FileSaveDialog::change_path(const Glib::ustring& path)
+//{
+// myFilename = path;
+//}
+
+void FileSaveDialog::appendExtension(Glib::ustring& path, Inkscape::Extension::Output* outputExtension)
+{
+ if (!outputExtension)
+ return;
+
+ try {
+ bool appendExtension = true;
+ Glib::ustring utf8Name = Glib::filename_to_utf8( path );
+ Glib::ustring::size_type pos = utf8Name.rfind('.');
+ if ( pos != Glib::ustring::npos ) {
+ Glib::ustring trail = utf8Name.substr( pos );
+ Glib::ustring foldedTrail = trail.casefold();
+ if ( (trail == ".")
+ | (foldedTrail != Glib::ustring( outputExtension->get_extension() ).casefold()
+ && ( knownExtensions.find(foldedTrail) != knownExtensions.end() ) ) ) {
+ utf8Name = utf8Name.erase( pos );
+ } else {
+ appendExtension = false;
+ }
+ }
+
+ if (appendExtension) {
+ utf8Name = utf8Name + outputExtension->get_extension();
+ myFilename = Glib::filename_from_utf8( utf8Name );
+ }
+ } catch ( Glib::ConvertError& e ) {
+ // ignore
+ }
+}
+
+
+} //namespace Dialog
+} //namespace UI
+} //namespace Inkscape
+
+
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/filedialog.h b/src/ui/dialog/filedialog.h
new file mode 100644
index 0000000..8521f01
--- /dev/null
+++ b/src/ui/dialog/filedialog.h
@@ -0,0 +1,256 @@
+// 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,
+ RASTER_TYPES,
+ EXE_TYPES,
+ SWATCH_TYPES,
+ CUSTOM_TYPE
+};
+
+/**
+ * Used for returning the type selected in a SaveAs
+ */
+enum FileDialogSelectionType {
+ SVG_NAMESPACE,
+ SVG_NAMESPACE_WITH_EXTENSIONS
+ };
+
+
+/**
+ * Return true if the string ends with the given suffix
+ */
+bool hasSuffix(const Glib::ustring &str, const Glib::ustring &ext);
+
+/**
+ * Return true if the image is loadable by Gdk, else false
+ */
+bool isValidImageFile(const Glib::ustring &fileName);
+
+/**
+ * This class provides an implementation-independent API for
+ * file "Open" dialogs. Using a standard interface obviates the need
+ * for ugly #ifdefs in file open code
+ */
+class FileOpenDialog
+{
+public:
+
+
+ /**
+ * Constructor .. do not call directly
+ * @param path the directory where to start searching
+ * @param fileTypes one of FileDialogTypes
+ * @param title the title of the dialog
+ */
+ FileOpenDialog()
+ = default;;
+
+ /**
+ * Factory.
+ * @param path the directory where to start searching
+ * @param fileTypes one of FileDialogTypes
+ * @param title the title of the dialog
+ */
+ static FileOpenDialog *create(Gtk::Window& parentWindow,
+ const Glib::ustring &path,
+ FileDialogType fileTypes,
+ const char *title);
+
+
+ /**
+ * Destructor.
+ * Perform any necessary cleanups.
+ */
+ virtual ~FileOpenDialog() = default;;
+
+ /**
+ * Show an OpenFile file selector.
+ * @return the selected path if user selected one, else NULL
+ */
+ virtual bool show() = 0;
+
+ /**
+ * Return the 'key' (filetype) of the selection, if any
+ * @return a pointer to a string if successful (which must
+ * be later freed with g_free(), else NULL.
+ */
+ virtual Inkscape::Extension::Extension * getSelectionType() = 0;
+
+ Glib::ustring getFilename();
+
+ virtual std::vector<Glib::ustring> getFilenames() = 0;
+
+ virtual Glib::ustring getCurrentDirectory() = 0;
+
+ virtual void addFilterMenu(Glib::ustring name, Glib::ustring pattern) = 0;
+
+protected:
+ /**
+ * Filename that was given
+ */
+ Glib::ustring myFilename;
+
+}; //FileOpenDialog
+
+
+
+
+
+
+/**
+ * This class provides an implementation-independent API for
+ * file "Save" dialogs.
+ */
+class FileSaveDialog
+{
+public:
+
+ /**
+ * Constructor. Do not call directly . Use the factory.
+ * @param path the directory where to start searching
+ * @param fileTypes one of FileDialogTypes
+ * @param title the title of the dialog
+ * @param key a list of file types from which the user can select
+ */
+ FileSaveDialog ()
+ = default;;
+
+ /**
+ * Factory.
+ * @param path the directory where to start searching
+ * @param fileTypes one of FileDialogTypes
+ * @param title the title of the dialog
+ * @param key a list of file types from which the user can select
+ */
+ static FileSaveDialog *create(Gtk::Window& parentWindow,
+ const Glib::ustring &path,
+ FileDialogType fileTypes,
+ const char *title,
+ const Glib::ustring &default_key,
+ const gchar *docTitle,
+ const Inkscape::Extension::FileSaveMethod save_method);
+
+
+ /**
+ * Destructor.
+ * Perform any necessary cleanups.
+ */
+ virtual ~FileSaveDialog() = default;;
+
+
+ /**
+ * Show an SaveAs file selector.
+ * @return the selected path if user selected one, else NULL
+ */
+ virtual bool show() =0;
+
+ /**
+ * Return the 'key' (filetype) of the selection, if any
+ * @return a pointer to a string if successful (which must
+ * be later freed with g_free(), else NULL.
+ */
+ virtual Inkscape::Extension::Extension * getSelectionType() = 0;
+
+ virtual void setSelectionType( Inkscape::Extension::Extension * key ) = 0;
+
+ /**
+ * Get the file name chosen by the user. Valid after an [OK]
+ */
+ Glib::ustring getFilename ();
+
+ /**
+ * Get the document title chosen by the user. Valid after an [OK]
+ */
+ Glib::ustring getDocTitle ();
+
+ virtual Glib::ustring getCurrentDirectory() = 0;
+
+ virtual void addFileType(Glib::ustring name, Glib::ustring pattern) = 0;
+
+protected:
+
+ /**
+ * Filename that was given
+ */
+ Glib::ustring myFilename;
+
+ /**
+ * Doc Title that was given
+ */
+ Glib::ustring myDocTitle;
+
+ /**
+ * List of known file extensions.
+ */
+ std::map<Glib::ustring, Inkscape::Extension::Output*> knownExtensions;
+
+
+ void appendExtension(Glib::ustring& path, Inkscape::Extension::Output* outputExtension);
+
+}; //FileSaveDialog
+
+
+} //namespace Dialog
+} //namespace UI
+} //namespace Inkscape
+
+#endif /* __FILE_DIALOG_H__ */
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/filedialogimpl-gtkmm.cpp b/src/ui/dialog/filedialogimpl-gtkmm.cpp
new file mode 100644
index 0000000..c3aba06
--- /dev/null
+++ b/src/ui/dialog/filedialogimpl-gtkmm.cpp
@@ -0,0 +1,867 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Implementation of the file dialog interfaces defined in filedialogimpl.h.
+ */
+/* Authors:
+ * Bob Jamison
+ * Joel Holdsworth
+ * Bruno Dilly
+ * Other dudes from The Inkscape Organization
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2004-2007 Bob Jamison
+ * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl>
+ * Copyright (C) 2007-2008 Joel Holdsworth
+ * Copyright (C) 2004-2007 The Inkscape Organization
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <iostream>
+
+#include <glibmm/convert.h>
+#include <glibmm/fileutils.h>
+#include <glibmm/i18n.h>
+#include <glibmm/miscutils.h>
+#include <glibmm/regex.h>
+#include <gtkmm/expander.h>
+
+#include "filedialogimpl-gtkmm.h"
+
+#include "document.h"
+#include "inkscape.h"
+#include "path-prefix.h"
+#include "preferences.h"
+
+#include "extension/db.h"
+#include "extension/input.h"
+#include "extension/output.h"
+
+#include "io/resource.h"
+#include "io/sys.h"
+
+#include "ui/dialog-events.h"
+#include "ui/view/svg-view-widget.h"
+
+// Routines from file.cpp
+#undef INK_DUMP_FILENAME_CONV
+
+#ifdef INK_DUMP_FILENAME_CONV
+void dump_str(const gchar *str, const gchar *prefix);
+void dump_ustr(const Glib::ustring &ustr);
+#endif
+
+
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+
+
+//########################################################################
+//### U T I L I T Y
+//########################################################################
+
+void fileDialogExtensionToPattern(Glib::ustring &pattern, Glib::ustring &extension)
+{
+ for (unsigned int ch : extension) {
+ if (Glib::Unicode::isalpha(ch)) {
+ pattern += '[';
+ pattern += Glib::Unicode::toupper(ch);
+ pattern += Glib::Unicode::tolower(ch);
+ pattern += ']';
+ } else {
+ pattern += ch;
+ }
+ }
+}
+
+
+void findEntryWidgets(Gtk::Container *parent, std::vector<Gtk::Entry *> &result)
+{
+ if (!parent) {
+ return;
+ }
+ std::vector<Gtk::Widget *> children = parent->get_children();
+ for (auto child : children) {
+ GtkWidget *wid = child->gobj();
+ if (GTK_IS_ENTRY(wid))
+ result.push_back(dynamic_cast<Gtk::Entry *>(child));
+ else if (GTK_IS_CONTAINER(wid))
+ findEntryWidgets(dynamic_cast<Gtk::Container *>(child), result);
+ }
+}
+
+void findExpanderWidgets(Gtk::Container *parent, std::vector<Gtk::Expander *> &result)
+{
+ if (!parent)
+ return;
+ std::vector<Gtk::Widget *> children = parent->get_children();
+ for (auto child : children) {
+ GtkWidget *wid = child->gobj();
+ if (GTK_IS_EXPANDER(wid))
+ result.push_back(dynamic_cast<Gtk::Expander *>(child));
+ else if (GTK_IS_CONTAINER(wid))
+ findExpanderWidgets(dynamic_cast<Gtk::Container *>(child), result);
+ }
+}
+
+
+/*#########################################################################
+### F I L E D I A L O G B A S E C L A S S
+#########################################################################*/
+
+void FileDialogBaseGtk::internalSetup()
+{
+ // Open executable file dialogs don't need the preview panel
+ if (_dialogType != EXE_TYPES) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool enablePreview = prefs->getBool(preferenceBase + "/enable_preview", true);
+ bool enableSVGExport = prefs->getBool(preferenceBase + "/enable_svgexport", false);
+
+ previewCheckbox.set_label(Glib::ustring(_("Enable preview")));
+ previewCheckbox.set_active(enablePreview);
+
+ previewCheckbox.signal_toggled().connect(sigc::mem_fun(*this, &FileDialogBaseGtk::_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();
+ }
+}
+
+
+/*#########################################################################
+### F I L E O P E N
+#########################################################################*/
+
+/**
+ * Constructor. Not called directly. Use the factory.
+ */
+FileOpenDialogImplGtk::FileOpenDialogImplGtk(Gtk::Window &parentWindow, const Glib::ustring &dir,
+ FileDialogType fileTypes, const Glib::ustring &title)
+ : FileDialogBaseGtk(parentWindow, title, Gtk::FILE_CHOOSER_ACTION_OPEN, fileTypes, "/dialogs/open")
+{
+
+
+ if (_dialogType == EXE_TYPES) {
+ /* One file at a time */
+ set_select_multiple(false);
+ } else {
+ /* And also Multiple Files */
+ set_select_multiple(true);
+ }
+
+ set_local_only(false);
+
+ /* Initialize to Autodetect */
+ extension = nullptr;
+ /* No filename to start out with */
+ myFilename = "";
+
+ /* Set our dialog type (open, import, etc...)*/
+ _dialogType = fileTypes;
+
+
+ /* Set the pwd and/or the filename */
+ if (dir.size() > 0) {
+ Glib::ustring udir(dir);
+ Glib::ustring::size_type len = udir.length();
+ // leaving a trailing backslash on the directory name leads to the infamous
+ // double-directory bug on win32
+ if (len != 0 && udir[len - 1] == '\\')
+ udir.erase(len - 1);
+ if (_dialogType == EXE_TYPES) {
+ set_filename(udir.c_str());
+ } else {
+ set_current_folder(udir.c_str());
+ }
+ }
+
+ if (_dialogType != EXE_TYPES) {
+ set_extra_widget(previewCheckbox);
+ }
+
+ //###### Add the file types menu
+ createFilterMenu();
+
+ add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL);
+ set_default(*add_button(_("_Open"), Gtk::RESPONSE_OK));
+
+ //###### Allow easy access to our examples folder
+
+ 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);
+ }
+}
+
+/**
+ * Destructor
+ */
+FileOpenDialogImplGtk::~FileOpenDialogImplGtk()
+= default;
+
+void FileOpenDialogImplGtk::addFilterMenu(Glib::ustring name, Glib::ustring pattern)
+{
+ auto allFilter = Gtk::FileFilter::create();
+ allFilter->set_name(_(name.c_str()));
+ allFilter->add_pattern(pattern);
+ extensionMap[Glib::ustring(_("All Files"))] = nullptr;
+ add_filter(allFilter);
+}
+
+void FileOpenDialogImplGtk::createFilterMenu()
+{
+ if (_dialogType == CUSTOM_TYPE) {
+ return;
+ }
+
+ if (_dialogType == EXE_TYPES) {
+ auto allFilter = Gtk::FileFilter::create();
+ allFilter->set_name(_("All Files"));
+ allFilter->add_pattern("*");
+ extensionMap[Glib::ustring(_("All Files"))] = nullptr;
+ add_filter(allFilter);
+ } else {
+ auto allInkscapeFilter = Gtk::FileFilter::create();
+ allInkscapeFilter->set_name(_("All Inkscape Files"));
+
+ auto allFilter = Gtk::FileFilter::create();
+ allFilter->set_name(_("All Files"));
+ allFilter->add_pattern("*");
+
+ auto allImageFilter = Gtk::FileFilter::create();
+ allImageFilter->set_name(_("All Images"));
+
+ auto allVectorFilter = Gtk::FileFilter::create();
+ allVectorFilter->set_name(_("All Vectors"));
+
+ auto allBitmapFilter = Gtk::FileFilter::create();
+ allBitmapFilter->set_name(_("All Bitmaps"));
+ extensionMap[Glib::ustring(_("All Inkscape Files"))] = nullptr;
+ add_filter(allInkscapeFilter);
+
+ extensionMap[Glib::ustring(_("All Files"))] = nullptr;
+ add_filter(allFilter);
+
+ extensionMap[Glib::ustring(_("All Images"))] = nullptr;
+ add_filter(allImageFilter);
+
+ extensionMap[Glib::ustring(_("All Vectors"))] = nullptr;
+ add_filter(allVectorFilter);
+
+ extensionMap[Glib::ustring(_("All Bitmaps"))] = nullptr;
+ add_filter(allBitmapFilter);
+
+ // patterns added dynamically below
+ Inkscape::Extension::DB::InputList extension_list;
+ Inkscape::Extension::db.get_input_list(extension_list);
+
+ for (auto imod : extension_list)
+ {
+ // FIXME: would be nice to grey them out instead of not listing them
+ if (imod->deactivated())
+ continue;
+
+ Glib::ustring upattern("*");
+ Glib::ustring extension = imod->get_extension();
+ fileDialogExtensionToPattern(upattern, extension);
+
+ Glib::ustring uname(imod->get_filetypename(true));
+
+ auto filter = Gtk::FileFilter::create();
+ filter->set_name(uname);
+ filter->add_pattern(upattern);
+ add_filter(filter);
+ extensionMap[uname] = imod;
+
+// g_message("ext %s:%s '%s'\n", ioext->name, ioext->mimetype, upattern.c_str());
+ allInkscapeFilter->add_pattern(upattern);
+ if (strncmp("image", imod->get_mimetype(), 5) == 0)
+ allImageFilter->add_pattern(upattern);
+
+ // uncomment this to find out all mime types supported by Inkscape import/open
+ // g_print ("%s\n", imod->get_mimetype());
+
+ // I don't know of any other way to define "bitmap" formats other than by listing them
+ if (strncmp("image/png", imod->get_mimetype(), 9) == 0 ||
+ strncmp("image/jpeg", imod->get_mimetype(), 10) == 0 ||
+ strncmp("image/gif", imod->get_mimetype(), 9) == 0 ||
+ strncmp("image/x-icon", imod->get_mimetype(), 12) == 0 ||
+ strncmp("image/x-navi-animation", imod->get_mimetype(), 22) == 0 ||
+ strncmp("image/x-cmu-raster", imod->get_mimetype(), 18) == 0 ||
+ strncmp("image/x-xpixmap", imod->get_mimetype(), 15) == 0 ||
+ strncmp("image/bmp", imod->get_mimetype(), 9) == 0 ||
+ strncmp("image/vnd.wap.wbmp", imod->get_mimetype(), 18) == 0 ||
+ strncmp("image/tiff", imod->get_mimetype(), 10) == 0 ||
+ strncmp("image/x-xbitmap", imod->get_mimetype(), 15) == 0 ||
+ strncmp("image/x-tga", imod->get_mimetype(), 11) == 0 ||
+ strncmp("image/x-pcx", imod->get_mimetype(), 11) == 0)
+ {
+ allBitmapFilter->add_pattern(upattern);
+ } else {
+ allVectorFilter->add_pattern(upattern);
+ }
+ }
+ }
+ return;
+}
+
+/**
+ * Show this dialog modally. Return true if user hits [OK]
+ */
+bool FileOpenDialogImplGtk::show()
+{
+ set_modal(TRUE); // Window
+ sp_transientize(GTK_WIDGET(gobj())); // Make transient
+ gint b = run(); // Dialog
+ svgPreview.showNoPreview();
+ hide();
+
+ if (b == Gtk::RESPONSE_OK) {
+ // This is a hack, to avoid the warning messages that
+ // Gtk::FileChooser::get_filter() returns
+ // should be: Gtk::FileFilter *filter = get_filter();
+ GtkFileChooser *gtkFileChooser = Gtk::FileChooser::gobj();
+ GtkFileFilter *filter = gtk_file_chooser_get_filter(gtkFileChooser);
+ if (filter) {
+ // Get which extension was chosen, if any
+ extension = extensionMap[gtk_file_filter_get_name(filter)];
+ }
+ myFilename = get_filename();
+
+ if (myFilename.empty()) {
+ myFilename = get_uri();
+ }
+
+ cleanup(true);
+ return true;
+ } else {
+ cleanup(false);
+ return false;
+ }
+}
+
+
+
+/**
+ * Get the file extension type that was selected by the user. Valid after an [OK]
+ */
+Inkscape::Extension::Extension *FileOpenDialogImplGtk::getSelectionType()
+{
+ return extension;
+}
+
+
+/**
+ * Get the file name chosen by the user. Valid after an [OK]
+ */
+Glib::ustring FileOpenDialogImplGtk::getFilename()
+{
+ return myFilename;
+}
+
+
+/**
+ * To Get Multiple filenames selected at-once.
+ */
+std::vector<Glib::ustring> FileOpenDialogImplGtk::getFilenames()
+{
+ auto result_tmp = get_filenames();
+
+ // Copy filenames to a vector of type Glib::ustring
+ std::vector<Glib::ustring> result;
+
+ for (auto it : result_tmp)
+ result.emplace_back(it);
+
+ if (result.empty()) {
+ result = get_uris();
+ }
+
+ return result;
+}
+
+Glib::ustring FileOpenDialogImplGtk::getCurrentDirectory()
+{
+ return get_current_folder();
+}
+
+
+
+//########################################################################
+//# F I L E S A V E
+//########################################################################
+
+/**
+ * Constructor
+ */
+FileSaveDialogImplGtk::FileSaveDialogImplGtk(Gtk::Window &parentWindow, const Glib::ustring &dir,
+ FileDialogType fileTypes, const Glib::ustring &title,
+ const Glib::ustring & /*default_key*/, const gchar *docTitle,
+ const Inkscape::Extension::FileSaveMethod save_method)
+ : FileDialogBaseGtk(parentWindow, title, Gtk::FILE_CHOOSER_ACTION_SAVE, fileTypes,
+ (save_method == Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY) ? "/dialogs/save_copy"
+ : "/dialogs/save_as")
+ , save_method(save_method)
+ , fromCB(false)
+ , checksBox(Gtk::ORIENTATION_VERTICAL)
+ , childBox(Gtk::ORIENTATION_HORIZONTAL)
+{
+ FileSaveDialog::myDocTitle = docTitle;
+
+ /* One file at a time */
+ set_select_multiple(false);
+
+ set_local_only(false);
+
+ /* Initialize to Autodetect */
+ extension = nullptr;
+ /* No filename to start out with */
+ myFilename = "";
+
+ /* Set our dialog type (save, export, etc...)*/
+ _dialogType = fileTypes;
+
+ /* Set the pwd and/or the filename */
+ if (dir.size() > 0) {
+ Glib::ustring udir(dir);
+ Glib::ustring::size_type len = udir.length();
+ // leaving a trailing backslash on the directory name leads to the infamous
+ // double-directory bug on win32
+ if ((len != 0) && (udir[len - 1] == '\\')) {
+ udir.erase(len - 1);
+ }
+ myFilename = udir;
+ }
+
+ //###### 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));
+ }
+
+ fileTypeComboBox.set_size_request(200, 40);
+ fileTypeComboBox.signal_changed().connect(sigc::mem_fun(*this, &FileSaveDialogImplGtk::fileTypeChangedCallback));
+
+ if (_dialogType != CUSTOM_TYPE)
+ createFilterMenu();
+
+ childBox.pack_start(checksBox);
+ childBox.pack_end(fileTypeComboBox);
+ checksBox.pack_start(fileTypeCheckbox);
+ checksBox.pack_start(previewCheckbox);
+ checksBox.pack_start(svgexportCheckbox);
+
+ set_extra_widget(childBox);
+
+ // Let's do some customization
+ fileNameEntry = nullptr;
+ Gtk::Container *cont = get_toplevel();
+ std::vector<Gtk::Entry *> entries;
+ findEntryWidgets(cont, entries);
+ // g_message("Found %d entry widgets\n", entries.size());
+ if (!entries.empty()) {
+ // Catch when user hits [return] on the text field
+ fileNameEntry = entries[0];
+ fileNameEntry->signal_activate().connect(
+ sigc::mem_fun(*this, &FileSaveDialogImplGtk::fileNameEntryChangedCallback));
+ }
+ signal_selection_changed().connect(
+ sigc::mem_fun(*this, &FileSaveDialogImplGtk::fileNameChanged));
+
+ // Let's do more customization
+ std::vector<Gtk::Expander *> expanders;
+ findExpanderWidgets(cont, expanders);
+ // g_message("Found %d expander widgets\n", expanders.size());
+ if (!expanders.empty()) {
+ // Always show the file list
+ Gtk::Expander *expander = expanders[0];
+ expander->set_expanded(true);
+ }
+
+ // allow easy access to the user's own templates folder
+ using namespace Inkscape::IO::Resource;
+ char const *templates = Inkscape::IO::Resource::get_path(USER, TEMPLATES);
+ if (Inkscape::IO::file_test(templates, G_FILE_TEST_EXISTS) &&
+ Inkscape::IO::file_test(templates, G_FILE_TEST_IS_DIR) && g_path_is_absolute(templates)) {
+ add_shortcut_folder(templates);
+ }
+
+ // if (extension == NULL)
+ // checkbox.set_sensitive(FALSE);
+
+ add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL);
+ set_default(*add_button(_("_Save"), Gtk::RESPONSE_OK));
+
+ show_all_children();
+}
+
+/**
+ * Destructor
+ */
+FileSaveDialogImplGtk::~FileSaveDialogImplGtk()
+= default;
+
+/**
+ * Callback for fileNameEntry widget
+ */
+void FileSaveDialogImplGtk::fileNameEntryChangedCallback()
+{
+ if (!fileNameEntry)
+ return;
+
+ Glib::ustring fileName = fileNameEntry->get_text();
+ if (!Glib::get_charset()) // If we are not utf8
+ fileName = Glib::filename_to_utf8(fileName);
+
+ // g_message("User hit return. Text is '%s'\n", fileName.c_str());
+
+ if (!Glib::path_is_absolute(fileName)) {
+ // try appending to the current path
+ // not this way: fileName = get_current_folder() + "/" + fileName;
+ std::vector<Glib::ustring> pathSegments;
+ pathSegments.emplace_back(get_current_folder());
+ pathSegments.push_back(fileName);
+ fileName = Glib::build_filename(pathSegments);
+ }
+
+ // g_message("path:'%s'\n", fileName.c_str());
+
+ if (Glib::file_test(fileName, Glib::FILE_TEST_IS_DIR)) {
+ set_current_folder(fileName);
+ } else if (/*Glib::file_test(fileName, Glib::FILE_TEST_IS_REGULAR)*/ true) {
+ // dialog with either (1) select a regular file or (2) cd to dir
+ // simulate an 'OK'
+ set_filename(fileName);
+ response(Gtk::RESPONSE_OK);
+ }
+}
+
+
+
+/**
+ * Callback for fileNameEntry widget
+ */
+void FileSaveDialogImplGtk::fileTypeChangedCallback()
+{
+ int sel = fileTypeComboBox.get_active_row_number();
+ if ((sel < 0) || (sel >= (int)fileTypes.size()))
+ return;
+
+ FileType type = fileTypes[sel];
+ // g_message("selected: %s\n", type.name.c_str());
+
+ extension = type.extension;
+ auto filter = Gtk::FileFilter::create();
+ filter->add_pattern(type.pattern);
+ set_filter(filter);
+
+ if (fromCB) {
+ //do not update if called from a name change
+ fromCB = false;
+ return;
+ }
+
+ updateNameAndExtension();
+}
+
+void FileSaveDialogImplGtk::fileNameChanged() {
+ Glib::ustring name = get_filename();
+ Glib::ustring::size_type pos = name.rfind('.');
+ if ( pos == Glib::ustring::npos ) return;
+ Glib::ustring ext = name.substr( pos ).casefold();
+ if (extension && Glib::ustring(static_cast<Inkscape::Extension::Output *>(extension)->get_extension()).casefold() == ext ) return;
+ if (knownExtensions.find(ext) == knownExtensions.end()) return;
+ fromCB = true;
+ fileTypeComboBox.set_active_text(knownExtensions[ext]->get_filetypename(true));
+}
+
+void FileSaveDialogImplGtk::addFileType(Glib::ustring name, Glib::ustring pattern)
+{
+ //#Let user choose
+ FileType guessType;
+ guessType.name = name;
+ guessType.pattern = pattern;
+ guessType.extension = nullptr;
+ fileTypeComboBox.append(guessType.name);
+ fileTypes.push_back(guessType);
+
+
+ fileTypeComboBox.set_active(0);
+ fileTypeChangedCallback(); // call at least once to set the filter
+}
+
+void FileSaveDialogImplGtk::createFilterMenu()
+{
+ Inkscape::Extension::DB::OutputList extension_list;
+ Inkscape::Extension::db.get_output_list(extension_list);
+ knownExtensions.clear();
+
+ bool is_raster = _dialogType == RASTER_TYPES;
+
+ for (auto omod : extension_list) {
+ // FIXME: would be nice to grey them out instead of not listing them
+ if (omod->deactivated() || (omod->is_raster() != is_raster))
+ continue;
+
+ // This extension is limited to save copy only.
+ if (omod->savecopy_only() && save_method != Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY)
+ continue;
+
+ FileType type;
+ type.name = omod->get_filetypename(true);
+ type.pattern = "*";
+ Glib::ustring extension = omod->get_extension();
+ knownExtensions.insert(std::pair<Glib::ustring, Inkscape::Extension::Output*>(extension.casefold(), omod));
+ fileDialogExtensionToPattern(type.pattern, extension);
+ type.extension = omod;
+ fileTypeComboBox.append(type.name);
+ fileTypes.push_back(type);
+ }
+
+ //#Let user choose
+ FileType guessType;
+ guessType.name = _("Guess from extension");
+ guessType.pattern = "*";
+ guessType.extension = nullptr;
+ fileTypeComboBox.append(guessType.name);
+ fileTypes.push_back(guessType);
+
+
+ fileTypeComboBox.set_active(0);
+ fileTypeChangedCallback(); // call at least once to set the filter
+}
+
+
+
+/**
+ * Show this dialog modally. Return true if user hits [OK]
+ */
+bool FileSaveDialogImplGtk::show()
+{
+ change_path(myFilename);
+ set_modal(TRUE); // Window
+ sp_transientize(GTK_WIDGET(gobj())); // Make transient
+ gint b = run(); // Dialog
+ svgPreview.showNoPreview();
+ set_preview_widget_active(false);
+ hide();
+
+ if (b == Gtk::RESPONSE_OK) {
+ updateNameAndExtension();
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ // Store changes of the "Append filename automatically" checkbox back to preferences.
+ if (save_method == Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY) {
+ prefs->setBool("/dialogs/save_copy/append_extension", fileTypeCheckbox.get_active());
+ } else {
+ prefs->setBool("/dialogs/save_as/append_extension", fileTypeCheckbox.get_active());
+ }
+
+ Inkscape::Extension::store_file_extension_in_prefs((extension != nullptr ? extension->get_id() : ""), save_method);
+
+ cleanup(true);
+
+ return true;
+ } else {
+ cleanup(false);
+ return false;
+ }
+}
+
+
+/**
+ * Get the file extension type that was selected by the user. Valid after an [OK]
+ */
+Inkscape::Extension::Extension *FileSaveDialogImplGtk::getSelectionType()
+{
+ return extension;
+}
+
+void FileSaveDialogImplGtk::setSelectionType(Inkscape::Extension::Extension *key)
+{
+ // If no pointer to extension is passed in, look up based on filename extension.
+ if (!key) {
+ // Not quite UTF-8 here.
+ gchar *filenameLower = g_ascii_strdown(myFilename.c_str(), -1);
+ for (int i = 0; !key && (i < (int)fileTypes.size()); i++) {
+ Inkscape::Extension::Output *ext = dynamic_cast<Inkscape::Extension::Output *>(fileTypes[i].extension);
+ if (ext && ext->get_extension()) {
+ gchar *extensionLower = g_ascii_strdown(ext->get_extension(), -1);
+ if (g_str_has_suffix(filenameLower, extensionLower)) {
+ key = fileTypes[i].extension;
+ }
+ g_free(extensionLower);
+ }
+ }
+ g_free(filenameLower);
+ }
+
+ // Ensure the proper entry in the combo box is selected.
+ if (key) {
+ extension = key;
+ gchar const *extensionID = extension->get_id();
+ if (extensionID) {
+ for (int i = 0; i < (int)fileTypes.size(); i++) {
+ Inkscape::Extension::Extension *ext = fileTypes[i].extension;
+ if (ext) {
+ gchar const *id = ext->get_id();
+ if (id && (strcmp(extensionID, id) == 0)) {
+ int oldSel = fileTypeComboBox.get_active_row_number();
+ if (i != oldSel) {
+ fileTypeComboBox.set_active(i);
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+}
+
+Glib::ustring FileSaveDialogImplGtk::getCurrentDirectory()
+{
+ return get_current_folder();
+}
+
+
+/*void
+FileSaveDialogImplGtk::change_title(const Glib::ustring& title)
+{
+ set_title(title);
+}*/
+
+/**
+ * Change the default save path location.
+ */
+void FileSaveDialogImplGtk::change_path(const Glib::ustring &path)
+{
+ myFilename = path;
+
+ if (Glib::file_test(myFilename, Glib::FILE_TEST_IS_DIR)) {
+ // fprintf(stderr,"set_current_folder(%s)\n",myFilename.c_str());
+ set_current_folder(myFilename);
+ } else {
+ // fprintf(stderr,"set_filename(%s)\n",myFilename.c_str());
+ if (Glib::file_test(myFilename, Glib::FILE_TEST_EXISTS)) {
+ set_filename(myFilename);
+ } else {
+ std::string dirName = Glib::path_get_dirname(myFilename);
+ if (dirName != get_current_folder()) {
+ set_current_folder(dirName);
+ }
+ }
+ Glib::ustring basename = Glib::path_get_basename(myFilename);
+ // fprintf(stderr,"set_current_name(%s)\n",basename.c_str());
+ try
+ {
+ set_current_name(Glib::filename_to_utf8(basename));
+ }
+ catch (Glib::ConvertError &e)
+ {
+ g_warning("Error converting save filename to UTF-8.");
+ // try a fallback.
+ set_current_name(basename);
+ }
+ }
+}
+
+void FileSaveDialogImplGtk::updateNameAndExtension()
+{
+ // Pick up any changes the user has typed in.
+ Glib::ustring tmp = get_filename();
+
+ if (tmp.empty()) {
+ tmp = get_uri();
+ }
+
+ if (!tmp.empty()) {
+ myFilename = tmp;
+ }
+
+ Inkscape::Extension::Output *newOut = extension ? dynamic_cast<Inkscape::Extension::Output *>(extension) : nullptr;
+ if (fileTypeCheckbox.get_active() && newOut) {
+ // Append the file extension if it's not already present and display it in the file name entry field
+ appendExtension(myFilename, newOut);
+ change_path(myFilename);
+ }
+}
+
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/filedialogimpl-gtkmm.h b/src/ui/dialog/filedialogimpl-gtkmm.h
new file mode 100644
index 0000000..b16d362
--- /dev/null
+++ b/src/ui/dialog/filedialogimpl-gtkmm.h
@@ -0,0 +1,307 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Implementation of the file dialog interfaces defined in filedialogimpl.h
+ */
+/* Authors:
+ * Bob Jamison
+ * Johan Engelen <johan@shouraizou.nl>
+ * Joel Holdsworth
+ * Bruno Dilly
+ * Other dudes from The Inkscape Organization
+ *
+ * Copyright (C) 2004-2008 Authors
+ * Copyright (C) 2004-2007 The Inkscape Organization
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef __FILE_DIALOGIMPL_H__
+#define __FILE_DIALOGIMPL_H__
+
+//Gtk includes
+#include <gtkmm/filechooserdialog.h>
+#include <glib/gstdio.h>
+
+#include "filedialog.h"
+#include "svg-preview.h"
+
+namespace Gtk {
+class CheckButton;
+class ComboBoxText;
+class Expander;
+}
+
+namespace Inkscape {
+ class URI;
+
+namespace UI {
+
+namespace View {
+ class SVGViewWidget;
+}
+
+namespace Dialog {
+
+/*#########################################################################
+### Utility
+#########################################################################*/
+void
+fileDialogExtensionToPattern(Glib::ustring &pattern,
+ Glib::ustring &extension);
+
+void
+findEntryWidgets(Gtk::Container *parent,
+ std::vector<Gtk::Entry *> &result);
+
+void
+findExpanderWidgets(Gtk::Container *parent,
+ std::vector<Gtk::Expander *> &result);
+
+class FileType
+{
+ public:
+ FileType(): name(), pattern(),extension(nullptr) {}
+ ~FileType() = default;
+ Glib::ustring name;
+ Glib::ustring pattern;
+ Inkscape::Extension::Extension *extension;
+};
+
+/*#########################################################################
+### F I L E D I A L O G B A S E C L A S S
+#########################################################################*/
+
+/**
+ * This class is the base implementation for the others. This
+ * reduces redundancies and bugs.
+ */
+class FileDialogBaseGtk : public Gtk::FileChooserDialog
+{
+public:
+
+ /**
+ *
+ */
+ FileDialogBaseGtk(Gtk::Window& parentWindow, const Glib::ustring &title,
+ Gtk::FileChooserAction dialogType, FileDialogType type, gchar const* preferenceBase) :
+ Gtk::FileChooserDialog(parentWindow, title, dialogType),
+ preferenceBase(preferenceBase ? preferenceBase : "unknown"),
+ _dialogType(type)
+ {
+ internalSetup();
+ }
+
+ /**
+ *
+ */
+ FileDialogBaseGtk(Gtk::Window& parentWindow, const char *title,
+ Gtk::FileChooserAction dialogType, FileDialogType type, gchar const* preferenceBase) :
+ Gtk::FileChooserDialog(parentWindow, title, dialogType),
+ preferenceBase(preferenceBase ? preferenceBase : "unknown"),
+ _dialogType(type)
+ {
+ internalSetup();
+ }
+
+ /**
+ *
+ */
+ ~FileDialogBaseGtk() override
+ = default;
+
+protected:
+ void cleanup( bool showConfirmed );
+
+ Glib::ustring const preferenceBase;
+ /**
+ * What type of 'open' are we? (open, import, place, etc)
+ */
+ FileDialogType _dialogType;
+
+ /**
+ * Our svg preview widget
+ */
+ SVGPreview svgPreview;
+
+ /**
+ * Child widgets
+ */
+ Gtk::CheckButton previewCheckbox;
+ Gtk::CheckButton svgexportCheckbox;
+
+private:
+ void internalSetup();
+
+ /**
+ * Callback for seeing if the preview needs to be drawn
+ */
+ void _updatePreviewCallback();
+
+ /**
+ * Callback to for SVG 2 to SVG 1.1 export.
+ */
+ void _svgexportEnabledCB();
+};
+
+
+
+
+/*#########################################################################
+### F I L E O P E N
+#########################################################################*/
+
+/**
+ * Our implementation class for the FileOpenDialog interface..
+ */
+class FileOpenDialogImplGtk : public FileOpenDialog, public FileDialogBaseGtk
+{
+public:
+
+ FileOpenDialogImplGtk(Gtk::Window& parentWindow,
+ const Glib::ustring &dir,
+ FileDialogType fileTypes,
+ const Glib::ustring &title);
+
+ ~FileOpenDialogImplGtk() override;
+
+ bool show() override;
+
+ Inkscape::Extension::Extension *getSelectionType() override;
+
+ Glib::ustring getFilename();
+
+ std::vector<Glib::ustring> getFilenames() override;
+
+ Glib::ustring getCurrentDirectory() override;
+
+ /// Add a custom file filter menu item
+ /// @param name - Name of the filter (such as "Javscript")
+ /// @param pattern - File filtering pattern (such as "*.js")
+ /// Use the FileDialogType::CUSTOM_TYPE in constructor to not include other file types
+ void addFilterMenu(Glib::ustring name, Glib::ustring pattern) override;
+
+private:
+
+ /**
+ * Create a filter menu for this type of dialog
+ */
+ void createFilterMenu();
+
+
+ /**
+ * Filter name->extension lookup
+ */
+ std::map<Glib::ustring, Inkscape::Extension::Extension *> extensionMap;
+
+ /**
+ * The extension to use to write this file
+ */
+ Inkscape::Extension::Extension *extension;
+
+};
+
+
+
+//########################################################################
+//# F I L E S A V E
+//########################################################################
+
+/**
+ * Our implementation of the FileSaveDialog interface.
+ */
+class FileSaveDialogImplGtk : public FileSaveDialog, public FileDialogBaseGtk
+{
+
+public:
+ FileSaveDialogImplGtk(Gtk::Window &parentWindow,
+ const Glib::ustring &dir,
+ FileDialogType fileTypes,
+ const Glib::ustring &title,
+ const Glib::ustring &default_key,
+ const gchar* docTitle,
+ const Inkscape::Extension::FileSaveMethod save_method);
+
+ ~FileSaveDialogImplGtk() override;
+
+ bool show() override;
+
+ Inkscape::Extension::Extension *getSelectionType() override;
+ void setSelectionType( Inkscape::Extension::Extension * key ) override;
+
+ Glib::ustring getCurrentDirectory() override;
+ void addFileType(Glib::ustring name, Glib::ustring pattern) override;
+
+private:
+ //void change_title(const Glib::ustring& title);
+ void change_path(const Glib::ustring& path);
+ void updateNameAndExtension();
+
+ /**
+ * The file save method (essentially whether the dialog was invoked by "Save as ..." or "Save a
+ * copy ..."), which is used to determine file extensions and save paths.
+ */
+ Inkscape::Extension::FileSaveMethod save_method;
+
+ /**
+ * Fix to allow the user to type the file name
+ */
+ Gtk::Entry *fileNameEntry;
+
+
+ /**
+ * Allow the specification of the output file type
+ */
+ Gtk::ComboBoxText fileTypeComboBox;
+
+
+ /**
+ * Data mirror of the combo box
+ */
+ std::vector<FileType> fileTypes;
+
+ //# Child widgets
+ Gtk::Box childBox;
+ Gtk::Box checksBox;
+
+ Gtk::CheckButton fileTypeCheckbox;
+
+ /**
+ * Callback for user input into fileNameEntry
+ */
+ void fileTypeChangedCallback();
+
+ /**
+ * Create a filter menu for this type of dialog
+ */
+ void createFilterMenu();
+
+ /**
+ * The extension to use to write this file
+ */
+ Inkscape::Extension::Extension *extension;
+
+ /**
+ * Callback for user input into fileNameEntry
+ */
+ void fileNameEntryChangedCallback();
+ void fileNameChanged();
+ bool fromCB;
+};
+
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif /*__FILE_DIALOGIMPL_H__*/
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/filedialogimpl-win32.cpp b/src/ui/dialog/filedialogimpl-win32.cpp
new file mode 100644
index 0000000..b44d90c
--- /dev/null
+++ b/src/ui/dialog/filedialogimpl-win32.cpp
@@ -0,0 +1,1929 @@
+// 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);
+}
+
+Inkscape::Extension::Extension *FileDialogBaseWin32::getSelectionType()
+{
+ return _extension;
+}
+
+Glib::ustring FileDialogBaseWin32::getCurrentDirectory()
+{
+ return _current_directory;
+}
+
+/*#########################################################################
+### F I L E O P E N
+#########################################################################*/
+
+bool FileOpenDialogImplWin32::_show_preview = true;
+
+/**
+ * Constructor. Not called directly. Use the factory.
+ */
+FileOpenDialogImplWin32::FileOpenDialogImplWin32(Gtk::Window &parent,
+ const Glib::ustring &dir,
+ FileDialogType fileTypes,
+ const gchar *title) :
+ FileDialogBaseWin32(parent, dir, title, fileTypes, "dialogs.open")
+{
+ // Initialize to Autodetect
+ _extension = NULL;
+
+ // Set our dialog type (open, import, etc...)
+ dialogType = fileTypes;
+
+ _show_preview_button_bitmap = NULL;
+ _preview_wnd = NULL;
+ _file_dialog_wnd = NULL;
+ _base_window_proc = NULL;
+
+ _preview_file_size = 0;
+ _preview_bitmap = NULL;
+ _preview_file_icon = NULL;
+ _preview_document_width = 0;
+ _preview_document_height = 0;
+ _preview_image_width = 0;
+ _preview_image_height = 0;
+ _preview_emf_image = false;
+
+ _mutex = NULL;
+
+ if (dialogType != CUSTOM_TYPE)
+ createFilterMenu();
+}
+
+
+/**
+ * Destructor
+ */
+FileOpenDialogImplWin32::~FileOpenDialogImplWin32()
+{
+ if(_filter != NULL)
+ delete[] _filter;
+ if(_extension_map != NULL)
+ delete[] _extension_map;
+}
+
+void FileOpenDialogImplWin32::addFilterMenu(Glib::ustring name, Glib::ustring pattern)
+{
+ std::list<Filter> filter_list;
+
+ Filter all_exe_files;
+
+ const gchar *all_exe_files_filter_name = name.data();
+ const gchar *all_exe_files_filter = pattern.data();
+
+ // Calculate the amount of memory required
+ int filter_count = 1;
+ int filter_length = 1;
+
+ int extension_index = 0;
+ _extension_map = new Inkscape::Extension::Extension*[filter_count];
+
+ // Filter Executable Files
+ all_exe_files.name = g_utf8_to_utf16(all_exe_files_filter_name,
+ -1, NULL, &all_exe_files.name_length, NULL);
+ all_exe_files.filter = g_utf8_to_utf16(all_exe_files_filter,
+ -1, NULL, &all_exe_files.filter_length, NULL);
+ all_exe_files.mod = NULL;
+ filter_list.push_front(all_exe_files);
+
+ filter_length = all_exe_files.name_length + all_exe_files.filter_length + 3; // Add 3 for two \0s and a *
+
+ _filter = new wchar_t[filter_length];
+ wchar_t *filterptr = _filter;
+
+ for(std::list<Filter>::iterator filter_iterator = filter_list.begin();
+ filter_iterator != filter_list.end(); ++filter_iterator)
+ {
+ const Filter &filter = *filter_iterator;
+
+ wcsncpy(filterptr, (wchar_t*)filter.name, filter.name_length);
+ filterptr += filter.name_length;
+ g_free(filter.name);
+
+ *(filterptr++) = L'\0';
+ *(filterptr++) = L'*';
+
+ if(filter.filter != NULL)
+ {
+ wcsncpy(filterptr, (wchar_t*)filter.filter, filter.filter_length);
+ filterptr += filter.filter_length;
+ g_free(filter.filter);
+ }
+
+ *(filterptr++) = L'\0';
+
+ // Associate this input extension with the file type name
+ _extension_map[extension_index++] = filter.mod;
+ }
+ *(filterptr++) = L'\0';
+
+ _filter_count = extension_index;
+ _filter_index = 1; // Select the 1st filter in the list
+}
+
+void FileOpenDialogImplWin32::createFilterMenu()
+{
+ std::list<Filter> filter_list;
+
+ int extension_index = 0;
+ int filter_length = 1;
+
+ if (dialogType == CUSTOM_TYPE) {
+ return;
+ }
+
+ if (dialogType != EXE_TYPES) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ _show_preview = prefs->getBool("/dialogs/open/enable_preview", true);
+
+ // Compose the filter string
+ Inkscape::Extension::DB::InputList extension_list;
+ Inkscape::Extension::db.get_input_list(extension_list);
+
+ ustring all_inkscape_files_filter, all_image_files_filter, all_vectors_filter, all_bitmaps_filter;
+ Filter all_files, all_inkscape_files, all_image_files, all_vectors, all_bitmaps;
+
+ const gchar *all_files_filter_name = _("All Files");
+ const gchar *all_inkscape_files_filter_name = _("All Inkscape Files");
+ const gchar *all_image_files_filter_name = _("All Images");
+ const gchar *all_vectors_filter_name = _("All Vectors");
+ const gchar *all_bitmaps_filter_name = _("All Bitmaps");
+
+ // Calculate the amount of memory required
+ int filter_count = 5; // 5 - one for each filter type
+
+ for (Inkscape::Extension::DB::InputList::iterator current_item = extension_list.begin();
+ current_item != extension_list.end(); ++current_item)
+ {
+ Filter filter;
+
+ Inkscape::Extension::Input *imod = *current_item;
+ if (imod->deactivated()) continue;
+
+ // Type
+ filter.name = g_utf8_to_utf16(imod->get_filetypename(true), -1, NULL, &filter.name_length, NULL);
+
+ // Extension
+ const gchar *file_extension_name = imod->get_extension();
+ filter.filter = g_utf8_to_utf16(file_extension_name, -1, NULL, &filter.filter_length, NULL);
+
+ filter.mod = imod;
+ filter_list.push_back(filter);
+
+ filter_length += filter.name_length + filter.filter_length + 3; // Add 3 for two \0s and a *
+
+ // Add to the "All Inkscape Files" Entry
+ if(all_inkscape_files_filter.length() > 0)
+ all_inkscape_files_filter += ";*";
+ all_inkscape_files_filter += file_extension_name;
+ if( strncmp("image", imod->get_mimetype(), 5) == 0)
+ {
+ // Add to the "All Image Files" Entry
+ if(all_image_files_filter.length() > 0)
+ all_image_files_filter += ";*";
+ all_image_files_filter += file_extension_name;
+ }
+
+ // I don't know of any other way to define "bitmap" formats other than by listing them
+ // if you change it here, do the same change in filedialogimpl-gtkmm
+ if (
+ strncmp("image/png", imod->get_mimetype(), 9)==0 ||
+ strncmp("image/jpeg", imod->get_mimetype(), 10)==0 ||
+ strncmp("image/gif", imod->get_mimetype(), 9)==0 ||
+ strncmp("image/x-icon", imod->get_mimetype(), 12)==0 ||
+ strncmp("image/x-navi-animation", imod->get_mimetype(), 22)==0 ||
+ strncmp("image/x-cmu-raster", imod->get_mimetype(), 18)==0 ||
+ strncmp("image/x-xpixmap", imod->get_mimetype(), 15)==0 ||
+ strncmp("image/bmp", imod->get_mimetype(), 9)==0 ||
+ strncmp("image/vnd.wap.wbmp", imod->get_mimetype(), 18)==0 ||
+ strncmp("image/tiff", imod->get_mimetype(), 10)==0 ||
+ strncmp("image/x-xbitmap", imod->get_mimetype(), 15)==0 ||
+ strncmp("image/x-tga", imod->get_mimetype(), 11)==0 ||
+ strncmp("image/x-pcx", imod->get_mimetype(), 11)==0
+ ) {
+ if(all_bitmaps_filter.length() > 0)
+ all_bitmaps_filter += ";*";
+ all_bitmaps_filter += file_extension_name;
+ } else {
+ if(all_vectors_filter.length() > 0)
+ all_vectors_filter += ";*";
+ all_vectors_filter += file_extension_name;
+ }
+
+ filter_count++;
+ }
+
+ _extension_map = new Inkscape::Extension::Extension*[filter_count];
+
+ // Filter bitmap files
+ all_bitmaps.name = g_utf8_to_utf16(all_bitmaps_filter_name,
+ -1, NULL, &all_bitmaps.name_length, NULL);
+ all_bitmaps.filter = g_utf8_to_utf16(all_bitmaps_filter.data(),
+ -1, NULL, &all_bitmaps.filter_length, NULL);
+ all_bitmaps.mod = NULL;
+ filter_list.push_front(all_bitmaps);
+
+ // Filter vector files
+ all_vectors.name = g_utf8_to_utf16(all_vectors_filter_name,
+ -1, NULL, &all_vectors.name_length, NULL);
+ all_vectors.filter = g_utf8_to_utf16(all_vectors_filter.data(),
+ -1, NULL, &all_vectors.filter_length, NULL);
+ all_vectors.mod = NULL;
+ filter_list.push_front(all_vectors);
+
+ // Filter Image Files
+ all_image_files.name = g_utf8_to_utf16(all_image_files_filter_name,
+ -1, NULL, &all_image_files.name_length, NULL);
+ all_image_files.filter = g_utf8_to_utf16(all_image_files_filter.data(),
+ -1, NULL, &all_image_files.filter_length, NULL);
+ all_image_files.mod = NULL;
+ filter_list.push_front(all_image_files);
+
+ // Filter Inkscape Files
+ all_inkscape_files.name = g_utf8_to_utf16(all_inkscape_files_filter_name,
+ -1, NULL, &all_inkscape_files.name_length, NULL);
+ all_inkscape_files.filter = g_utf8_to_utf16(all_inkscape_files_filter.data(),
+ -1, NULL, &all_inkscape_files.filter_length, NULL);
+ all_inkscape_files.mod = NULL;
+ filter_list.push_front(all_inkscape_files);
+
+ // Filter All Files
+ all_files.name = g_utf8_to_utf16(all_files_filter_name,
+ -1, NULL, &all_files.name_length, NULL);
+ all_files.filter = NULL;
+ all_files.filter_length = 0;
+ all_files.mod = NULL;
+ filter_list.push_front(all_files);
+
+ filter_length += all_files.name_length + 3 +
+ all_inkscape_files.filter_length +
+ all_inkscape_files.name_length + 3 +
+ all_image_files.filter_length +
+ all_image_files.name_length + 3 +
+ all_vectors.filter_length +
+ all_vectors.name_length + 3 +
+ all_bitmaps.filter_length +
+ all_bitmaps.name_length + 3 +
+ 1;
+ // Add 3 for 2*2 \0s and a *, and 1 for a trailing \0
+ } else {
+ // Executables only
+ ustring all_exe_files_filter = "*.exe;*.bat;*.com";
+ Filter all_exe_files, all_files;
+
+ const gchar *all_files_filter_name = _("All Files");
+ const gchar *all_exe_files_filter_name = _("All Executable Files");
+
+ // Calculate the amount of memory required
+ int filter_count = 2; // 2 - All Files and All Executable Files
+
+ _extension_map = new Inkscape::Extension::Extension*[filter_count];
+
+ // Filter Executable Files
+ all_exe_files.name = g_utf8_to_utf16(all_exe_files_filter_name,
+ -1, NULL, &all_exe_files.name_length, NULL);
+ all_exe_files.filter = g_utf8_to_utf16(all_exe_files_filter.data(),
+ -1, NULL, &all_exe_files.filter_length, NULL);
+ all_exe_files.mod = NULL;
+ filter_list.push_front(all_exe_files);
+
+ // Filter All Files
+ all_files.name = g_utf8_to_utf16(all_files_filter_name,
+ -1, NULL, &all_files.name_length, NULL);
+ all_files.filter = NULL;
+ all_files.filter_length = 0;
+ all_files.mod = NULL;
+ filter_list.push_front(all_files);
+
+ filter_length += all_files.name_length + 3 +
+ all_exe_files.filter_length +
+ all_exe_files.name_length + 3 +
+ 1;
+ // Add 3 for 2*2 \0s and a *, and 1 for a trailing \0
+ }
+
+ _filter = new wchar_t[filter_length];
+ wchar_t *filterptr = _filter;
+
+ for(std::list<Filter>::iterator filter_iterator = filter_list.begin();
+ filter_iterator != filter_list.end(); ++filter_iterator)
+ {
+ const Filter &filter = *filter_iterator;
+
+ wcsncpy(filterptr, (wchar_t*)filter.name, filter.name_length);
+ filterptr += filter.name_length;
+ g_free(filter.name);
+
+ *(filterptr++) = L'\0';
+ *(filterptr++) = L'*';
+
+ if(filter.filter != NULL)
+ {
+ wcsncpy(filterptr, (wchar_t*)filter.filter, filter.filter_length);
+ filterptr += filter.filter_length;
+ g_free(filter.filter);
+ }
+
+ *(filterptr++) = L'\0';
+
+ // Associate this input extension with the file type name
+ _extension_map[extension_index++] = filter.mod;
+ }
+ *(filterptr++) = L'\0';
+
+ _filter_count = extension_index;
+ _filter_index = 2; // Select the 2nd filter in the list - 2 is NOT the 3rd
+}
+
+void FileOpenDialogImplWin32::GetOpenFileName_thread()
+{
+ OPENFILENAMEW ofn;
+
+ g_assert(_mutex != NULL);
+
+ WCHAR* current_directory_string = (WCHAR*)g_utf8_to_utf16(
+ _current_directory.data(), _current_directory.length(),
+ NULL, NULL, NULL);
+
+ memset(&ofn, 0, sizeof(ofn));
+
+ // Copy the selected file name, converting from UTF-8 to UTF-16
+ memset(_path_string, 0, sizeof(_path_string));
+ gunichar2* utf16_path_string = g_utf8_to_utf16(
+ myFilename.data(), -1, NULL, NULL, NULL);
+ wcsncpy(_path_string, (wchar_t*)utf16_path_string, _MAX_PATH);
+ g_free(utf16_path_string);
+
+ ofn.lStructSize = sizeof(ofn);
+ ofn.hwndOwner = _ownerHwnd;
+ ofn.lpstrFile = _path_string;
+ ofn.nMaxFile = _MAX_PATH;
+ ofn.lpstrFileTitle = NULL;
+ ofn.nMaxFileTitle = 0;
+ ofn.lpstrInitialDir = current_directory_string;
+ ofn.lpstrTitle = _title;
+ ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_EXPLORER | OFN_ENABLEHOOK | OFN_HIDEREADONLY | OFN_ENABLESIZING;
+ ofn.lpstrFilter = _filter;
+ ofn.nFilterIndex = _filter_index;
+ ofn.lpfnHook = GetOpenFileName_hookproc;
+ ofn.lCustData = (LPARAM)this;
+
+ _result = GetOpenFileNameW(&ofn) != 0;
+
+ g_assert(ofn.nFilterIndex >= 1 && ofn.nFilterIndex <= _filter_count);
+ _filter_index = ofn.nFilterIndex;
+ _extension = _extension_map[ofn.nFilterIndex - 1];
+
+ // Copy the selected file name, converting from UTF-16 to UTF-8
+ myFilename = utf16_to_ustring(_path_string, _MAX_PATH);
+
+ // Tidy up
+ g_free(current_directory_string);
+
+ _mutex->lock();
+ _finished = true;
+ _mutex->unlock();
+}
+
+void FileOpenDialogImplWin32::register_preview_wnd_class()
+{
+ HINSTANCE hInstance = GetModuleHandle(NULL);
+ const WNDCLASSA PreviewWndClass =
+ {
+ CS_HREDRAW | CS_VREDRAW,
+ preview_wnd_proc,
+ 0,
+ 0,
+ hInstance,
+ NULL,
+ LoadCursor(hInstance, IDC_ARROW),
+ (HBRUSH)(COLOR_BTNFACE + 1),
+ NULL,
+ PreviewWindowClassName
+ };
+
+ RegisterClassA(&PreviewWndClass);
+}
+
+UINT_PTR CALLBACK FileOpenDialogImplWin32::GetOpenFileName_hookproc(
+ HWND hdlg, UINT uiMsg, WPARAM, LPARAM lParam)
+{
+ FileOpenDialogImplWin32 *pImpl = reinterpret_cast<FileOpenDialogImplWin32*>
+ (GetWindowLongPtr(hdlg, GWLP_USERDATA));
+
+ switch(uiMsg)
+ {
+ case WM_INITDIALOG:
+ {
+ HWND hParentWnd = GetParent(hdlg);
+ HINSTANCE hInstance = GetModuleHandle(NULL);
+
+ // Set the pointer to the object
+ OPENFILENAMEW *ofn = reinterpret_cast<OPENFILENAMEW*>(lParam);
+ SetWindowLongPtr(hdlg, GWLP_USERDATA, ofn->lCustData);
+ SetWindowLongPtr(hParentWnd, GWLP_USERDATA, ofn->lCustData);
+ pImpl = reinterpret_cast<FileOpenDialogImplWin32*>(ofn->lCustData);
+
+ // Make the window a bit wider
+ RECT rcRect;
+ GetWindowRect(hParentWnd, &rcRect);
+
+ // Don't show the preview when opening executable files
+ if ( pImpl->dialogType == EXE_TYPES) {
+ MoveWindow(hParentWnd, rcRect.left, rcRect.top,
+ rcRect.right - rcRect.left,
+ rcRect.bottom - rcRect.top,
+ FALSE);
+ } else {
+ MoveWindow(hParentWnd, rcRect.left, rcRect.top,
+ rcRect.right - rcRect.left + PREVIEW_WIDENING,
+ rcRect.bottom - rcRect.top,
+ FALSE);
+ }
+
+ // Subclass the parent
+ pImpl->_base_window_proc = (WNDPROC)GetWindowLongPtr(hParentWnd, GWLP_WNDPROC);
+ SetWindowLongPtr(hParentWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(file_dialog_subclass_proc));
+
+ if ( pImpl->dialogType != EXE_TYPES) {
+ // Add a button to the toolbar
+ pImpl->_toolbar_wnd = FindWindowEx(hParentWnd, NULL, "ToolbarWindow32", NULL);
+
+ pImpl->_show_preview_button_bitmap = LoadBitmap(
+ hInstance, MAKEINTRESOURCE(IDC_SHOW_PREVIEW));
+ TBADDBITMAP tbAddBitmap = {NULL, reinterpret_cast<UINT_PTR>(pImpl->_show_preview_button_bitmap)};
+ const int iBitmapIndex = SendMessage(pImpl->_toolbar_wnd,
+ TB_ADDBITMAP, 1, (LPARAM)&tbAddBitmap);
+
+
+ TBBUTTON tbButton;
+ memset(&tbButton, 0, sizeof(TBBUTTON));
+ tbButton.iBitmap = iBitmapIndex;
+ tbButton.idCommand = IDC_SHOW_PREVIEW;
+ tbButton.fsState = (pImpl->_show_preview ? TBSTATE_CHECKED : 0)
+ | TBSTATE_ENABLED;
+ tbButton.fsStyle = TBSTYLE_CHECK;
+ tbButton.iString = (INT_PTR)_("Show Preview");
+ SendMessage(pImpl->_toolbar_wnd, TB_ADDBUTTONS, 1, (LPARAM)&tbButton);
+
+ // Create preview pane
+ register_preview_wnd_class();
+ }
+
+ pImpl->_mutex->lock();
+
+ pImpl->_file_dialog_wnd = hParentWnd;
+
+ if ( pImpl->dialogType != EXE_TYPES) {
+ pImpl->_preview_wnd =
+ CreateWindowA(PreviewWindowClassName, "",
+ WS_CHILD | WS_VISIBLE,
+ 0, 0, 100, 100, hParentWnd, NULL, hInstance, NULL);
+ SetWindowLongPtr(pImpl->_preview_wnd, GWLP_USERDATA, ofn->lCustData);
+ }
+
+ pImpl->_mutex->unlock();
+
+ pImpl->layout_dialog();
+ }
+ break;
+
+ case WM_NOTIFY:
+ {
+
+ OFNOTIFY *pOFNotify = reinterpret_cast<OFNOTIFY*>(lParam);
+ switch(pOFNotify->hdr.code)
+ {
+ case CDN_SELCHANGE:
+ {
+ if(pImpl != NULL)
+ {
+ // Get the file name
+ pImpl->_mutex->lock();
+
+ SendMessage(pOFNotify->hdr.hwndFrom, CDM_GETFILEPATH,
+ sizeof(pImpl->_path_string) / sizeof(wchar_t),
+ (LPARAM)pImpl->_path_string);
+
+ pImpl->_file_selected = true;
+
+ pImpl->_mutex->unlock();
+ }
+ }
+ break;
+ }
+ }
+ break;
+
+ case WM_CLOSE:
+ pImpl->_mutex->lock();
+ pImpl->_preview_file_size = 0;
+
+ pImpl->_file_dialog_wnd = NULL;
+ DestroyWindow(pImpl->_preview_wnd);
+ pImpl->_preview_wnd = NULL;
+ DeleteObject(pImpl->_show_preview_button_bitmap);
+ pImpl->_show_preview_button_bitmap = NULL;
+ pImpl->_mutex->unlock();
+
+ break;
+ }
+
+ // Use default dialog behaviour
+ return 0;
+}
+
+LRESULT CALLBACK FileOpenDialogImplWin32::file_dialog_subclass_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
+{
+ FileOpenDialogImplWin32 *pImpl = reinterpret_cast<FileOpenDialogImplWin32*>
+ (GetWindowLongPtr(hwnd, GWLP_USERDATA));
+
+ LRESULT lResult = CallWindowProc(pImpl->_base_window_proc, hwnd, uMsg, wParam, lParam);
+
+ switch(uMsg)
+ {
+ case WM_SHOWWINDOW:
+ if(wParam != 0)
+ pImpl->layout_dialog();
+ break;
+
+ case WM_SIZE:
+ pImpl->layout_dialog();
+ break;
+
+ case WM_COMMAND:
+ if(wParam == IDC_SHOW_PREVIEW)
+ {
+ const bool enable = SendMessage(pImpl->_toolbar_wnd,
+ TB_ISBUTTONCHECKED, IDC_SHOW_PREVIEW, 0) != 0;
+ pImpl->enable_preview(enable);
+ }
+ break;
+ }
+
+ return lResult;
+}
+
+LRESULT CALLBACK FileOpenDialogImplWin32::preview_wnd_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
+{
+ const int CaptionPadding = 4;
+ const int IconSize = 32;
+
+ FileOpenDialogImplWin32 *pImpl = reinterpret_cast<FileOpenDialogImplWin32*>
+ (GetWindowLongPtr(hwnd, GWLP_USERDATA));
+
+ LRESULT lResult = 0;
+
+ switch(uMsg)
+ {
+ case WM_ERASEBKGND:
+ // Do nothing to erase the background
+ // - otherwise there'll be flicker
+ lResult = 1;
+ break;
+
+ case WM_PAINT:
+ {
+ // Get the client rect
+ RECT rcClient;
+ GetClientRect(hwnd, &rcClient);
+
+ // Prepare to paint
+ PAINTSTRUCT paint_struct;
+ HDC dc = BeginPaint(hwnd, &paint_struct);
+
+ HFONT hCaptionFont = reinterpret_cast<HFONT>(SendMessage(GetParent(hwnd),
+ WM_GETFONT, 0, 0));
+ HFONT hOldFont = static_cast<HFONT>(SelectObject(dc, hCaptionFont));
+ SetBkMode(dc, TRANSPARENT);
+
+ pImpl->_mutex->lock();
+
+ if(pImpl->_path_string[0] == 0)
+ {
+ WCHAR* noFileText=(WCHAR*)g_utf8_to_utf16(_("No file selected"),
+ -1, NULL, NULL, NULL);
+ FillRect(dc, &rcClient, reinterpret_cast<HBRUSH>(COLOR_3DFACE + 1));
+ DrawTextW(dc, noFileText, -1, &rcClient,
+ DT_CENTER | DT_VCENTER | DT_NOPREFIX);
+ g_free(noFileText);
+ }
+ else if(pImpl->_preview_bitmap != NULL)
+ {
+ BITMAP bitmap;
+ GetObject(pImpl->_preview_bitmap, sizeof(bitmap), &bitmap);
+ const int destX = (rcClient.right - bitmap.bmWidth) / 2;
+
+ // Render the image
+ HDC hSrcDC = CreateCompatibleDC(dc);
+ HBITMAP hOldBitmap = (HBITMAP)SelectObject(hSrcDC, pImpl->_preview_bitmap);
+
+ BitBlt(dc, destX, 0, bitmap.bmWidth, bitmap.bmHeight,
+ hSrcDC, 0, 0, SRCCOPY);
+
+ SelectObject(hSrcDC, hOldBitmap);
+ DeleteDC(hSrcDC);
+
+ // Fill in the background area
+ HRGN hEraseRgn = CreateRectRgn(rcClient.left, rcClient.top,
+ rcClient.right, rcClient.bottom);
+ HRGN hImageRgn = CreateRectRgn(destX, 0,
+ destX + bitmap.bmWidth, bitmap.bmHeight);
+ CombineRgn(hEraseRgn, hEraseRgn, hImageRgn, RGN_DIFF);
+
+ FillRgn(dc, hEraseRgn, GetSysColorBrush(COLOR_3DFACE));
+
+ DeleteObject(hImageRgn);
+ DeleteObject(hEraseRgn);
+
+ // Draw the caption on
+ RECT rcCaptionRect = {rcClient.left,
+ rcClient.top + bitmap.bmHeight + CaptionPadding,
+ rcClient.right, rcClient.bottom};
+
+ WCHAR szCaption[_MAX_FNAME + 32];
+ const int iLength = pImpl->format_caption(
+ szCaption, sizeof(szCaption) / sizeof(WCHAR));
+
+ DrawTextW(dc, szCaption, iLength, &rcCaptionRect,
+ DT_CENTER | DT_TOP | DT_NOPREFIX | DT_PATH_ELLIPSIS);
+ }
+ else if(pImpl->_preview_file_icon != NULL)
+ {
+ FillRect(dc, &rcClient, reinterpret_cast<HBRUSH>(COLOR_3DFACE + 1));
+
+ // Draw the files icon
+ const int destX = (rcClient.right - IconSize) / 2;
+ DrawIconEx(dc, destX, 0, pImpl->_preview_file_icon,
+ IconSize, IconSize, 0, NULL,
+ DI_NORMAL | DI_COMPAT);
+
+ // Draw the caption on
+ RECT rcCaptionRect = {rcClient.left,
+ rcClient.top + IconSize + CaptionPadding,
+ rcClient.right, rcClient.bottom};
+
+ WCHAR szFileName[_MAX_FNAME], szCaption[_MAX_FNAME + 32];
+ _wsplitpath(pImpl->_path_string, NULL, NULL, szFileName, NULL);
+
+ const int iLength = snwprintf(szCaption,
+ sizeof(szCaption), L"%ls\n%d kB",
+ szFileName, pImpl->_preview_file_size);
+
+ DrawTextW(dc, szCaption, iLength, &rcCaptionRect,
+ DT_CENTER | DT_TOP | DT_NOPREFIX | DT_PATH_ELLIPSIS);
+ }
+ else
+ {
+ // Can't show anything!
+ FillRect(dc, &rcClient, reinterpret_cast<HBRUSH>(COLOR_3DFACE + 1));
+ }
+
+ pImpl->_mutex->unlock();
+
+ // Finish painting
+ SelectObject(dc, hOldFont);
+ EndPaint(hwnd, &paint_struct);
+ }
+
+ break;
+
+ case WM_DESTROY:
+ pImpl->free_preview();
+ break;
+
+ default:
+ lResult = DefWindowProc(hwnd, uMsg, wParam, lParam);
+ break;
+ }
+
+ return lResult;
+}
+
+void FileOpenDialogImplWin32::enable_preview(bool enable)
+{
+ if (_show_preview != enable) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/dialogs/open/enable_preview", enable);
+ }
+ _show_preview = enable;
+
+ // Relayout the dialog
+ ShowWindow(_preview_wnd, enable ? SW_SHOW : SW_HIDE);
+ layout_dialog();
+
+ // Load or unload the preview
+ if(enable)
+ {
+ _mutex->lock();
+ _file_selected = true;
+ _mutex->unlock();
+ }
+ else free_preview();
+}
+
+void FileOpenDialogImplWin32::layout_dialog()
+{
+ union RECTPOINTS
+ {
+ RECT r;
+ POINT p[2];
+ };
+
+ const float MaxExtentScale = 2.0f / 3.0f;
+
+ RECT rcClient;
+ GetClientRect(_file_dialog_wnd, &rcClient);
+
+ // Re-layout the dialog
+ HWND hFileListWnd = GetDlgItem(_file_dialog_wnd, lst2);
+ HWND hFolderComboWnd = GetDlgItem(_file_dialog_wnd, cmb2);
+
+
+ RECT rcFolderComboRect;
+ RECTPOINTS rcFileList;
+ GetWindowRect(hFileListWnd, &rcFileList.r);
+ GetWindowRect(hFolderComboWnd, &rcFolderComboRect);
+ const int iPadding = rcFileList.r.top - rcFolderComboRect.bottom;
+ MapWindowPoints(NULL, _file_dialog_wnd, rcFileList.p, 2);
+
+ RECT rcPreview;
+ RECT rcBody = {rcFileList.r.left, rcFileList.r.top,
+ rcClient.right - iPadding, rcFileList.r.bottom};
+ rcFileList.r.right = rcBody.right;
+
+ if(_show_preview && dialogType != EXE_TYPES)
+ {
+ rcPreview.top = rcBody.top;
+ rcPreview.left = rcClient.right - (rcBody.bottom - rcBody.top);
+ const int iMaxExtent = (int)(MaxExtentScale * (float)(rcBody.left + rcBody.right)) + iPadding / 2;
+ if(rcPreview.left < iMaxExtent) rcPreview.left = iMaxExtent;
+ rcPreview.bottom = rcBody.bottom;
+ rcPreview.right = rcBody.right;
+
+ // Re-layout the preview box
+ _mutex->lock();
+
+ _preview_width = rcPreview.right - rcPreview.left;
+ _preview_height = rcPreview.bottom - rcPreview.top;
+
+ _mutex->unlock();
+
+ render_preview();
+
+ MoveWindow(_preview_wnd, rcPreview.left, rcPreview.top,
+ _preview_width, _preview_height, TRUE);
+
+ rcFileList.r.right = rcPreview.left - iPadding;
+ }
+
+ // Re-layout the file list box
+ MoveWindow(hFileListWnd, rcFileList.r.left, rcFileList.r.top,
+ rcFileList.r.right - rcFileList.r.left,
+ rcFileList.r.bottom - rcFileList.r.top, TRUE);
+
+ // Re-layout the toolbar
+ RECTPOINTS rcToolBar;
+ GetWindowRect(_toolbar_wnd, &rcToolBar.r);
+ MapWindowPoints(NULL, _file_dialog_wnd, rcToolBar.p, 2);
+ MoveWindow(_toolbar_wnd, rcToolBar.r.left, rcToolBar.r.top,
+ rcToolBar.r.right - rcToolBar.r.left, rcToolBar.r.bottom - rcToolBar.r.top, TRUE);
+}
+
+void FileOpenDialogImplWin32::file_selected()
+{
+ // Destroy any previous previews
+ free_preview();
+
+
+ // Determine if the file exists
+ DWORD attributes = GetFileAttributesW(_path_string);
+ if(attributes == 0xFFFFFFFF ||
+ attributes == FILE_ATTRIBUTE_DIRECTORY)
+ {
+ InvalidateRect(_preview_wnd, NULL, FALSE);
+ return;
+ }
+
+ // Check the file exists and get the file size
+ HANDLE file_handle = CreateFileW(_path_string, GENERIC_READ,
+ FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
+ if(file_handle == INVALID_HANDLE_VALUE) return;
+ const DWORD file_size = GetFileSize(file_handle, NULL);
+ if (file_size == INVALID_FILE_SIZE) return;
+ _preview_file_size = file_size / 1024;
+ CloseHandle(file_handle);
+
+ if(_show_preview) load_preview();
+}
+
+void FileOpenDialogImplWin32::load_preview()
+{
+ // Destroy any previous previews
+ free_preview();
+
+ // Try to get the file icon
+ SHFILEINFOW fileInfo;
+ if(SUCCEEDED(SHGetFileInfoW(_path_string, 0, &fileInfo,
+ sizeof(fileInfo), SHGFI_ICON | SHGFI_LARGEICON)))
+ _preview_file_icon = fileInfo.hIcon;
+
+ // Will this file be too big?
+ if(_preview_file_size > MaxPreviewFileSize)
+ {
+ InvalidateRect(_preview_wnd, NULL, FALSE);
+ return;
+ }
+
+ // Prepare to render a preview
+ const Glib::ustring svg = ".svg";
+ const Glib::ustring svgz = ".svgz";
+ const Glib::ustring emf = ".emf";
+ const Glib::ustring wmf = ".wmf";
+ const Glib::ustring path = utf16_to_ustring(_path_string);
+
+ bool success = false;
+
+ _preview_document_width = _preview_document_height = 0;
+
+ if ((dialogType == SVG_TYPES || dialogType == IMPORT_TYPES) &&
+ (hasSuffix(path, svg) || hasSuffix(path, svgz)))
+ success = set_svg_preview();
+ else if (hasSuffix(path, emf) || hasSuffix(path, wmf))
+ success = set_emf_preview();
+ else if (isValidImageFile(path))
+ success = set_image_preview();
+ else {
+ // Show no preview
+ }
+
+ if(success) render_preview();
+
+ InvalidateRect(_preview_wnd, NULL, FALSE);
+}
+
+void FileOpenDialogImplWin32::free_preview()
+{
+ _mutex->lock();
+ if(_preview_bitmap != NULL)
+ DeleteObject(_preview_bitmap);
+ _preview_bitmap = NULL;
+
+ if(_preview_file_icon != NULL)
+ DestroyIcon(_preview_file_icon);
+ _preview_file_icon = NULL;
+
+ _preview_bitmap_image.reset();
+ _preview_emf_image = false;
+ _mutex->unlock();
+}
+
+bool FileOpenDialogImplWin32::set_svg_preview()
+{
+ const int PreviewSize = 512;
+
+ gchar *utf8string = g_utf16_to_utf8((const gunichar2*)_path_string,
+ _MAX_PATH, NULL, NULL, NULL);
+ 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 */
+ myFilename = "";
+ if (dir.size() > 0) {
+ Glib::ustring udir(dir);
+ Glib::ustring::size_type len = udir.length();
+ // leaving a trailing backslash on the directory name leads to the infamous
+ // double-directory bug on win32
+ if (len != 0 && udir[len - 1] == '\\') udir.erase(len - 1);
+
+ // Remove the extension: remove everything past the last period found past the last slash
+ // (not for CUSTOM_TYPE as we can not automatically add a file extension in that case yet)
+ if (dialogType == CUSTOM_TYPE) {
+ myFilename = udir;
+ } else {
+ size_t last_slash_index = udir.find_last_of( '\\' );
+ size_t last_period_index = udir.find_last_of( '.' );
+ if (last_period_index > last_slash_index) {
+ myFilename = udir.substr(0, last_period_index );
+ }
+ }
+
+ // remove one slash if double
+ if (1 + myFilename.find("\\\\",2)) {
+ myFilename.replace(myFilename.find("\\\\",2), 1, "");
+ }
+ }
+}
+
+FileSaveDialogImplWin32::~FileSaveDialogImplWin32()
+{
+}
+
+void FileSaveDialogImplWin32::createFilterMenu()
+{
+ std::list<Filter> filter_list;
+
+ knownExtensions.clear();
+
+ // Compose the filter string
+ Glib::ustring all_inkscape_files_filter, all_image_files_filter;
+ Inkscape::Extension::DB::OutputList extension_list;
+ Inkscape::Extension::db.get_output_list(extension_list);
+
+ int filter_count = 0;
+ int filter_length = 1;
+ bool is_raster = dialogType == RASTER_TYPES;
+
+ for (auto omod : extension_list) {
+ // FIXME: would be nice to grey them out instead of not listing them
+ if (omod->deactivated() || (omod->is_raster() != is_raster))
+ 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(
+ myFilename.data(), -1, NULL, NULL, NULL);
+ wcsncpy(_path_string, (wchar_t*)utf16_path_string, _MAX_PATH);
+ g_free(utf16_path_string);
+
+ ZeroMemory(&ofn, sizeof(ofn));
+ ofn.lStructSize = sizeof(ofn);
+ ofn.hwndOwner = _ownerHwnd;
+ ofn.lpstrFile = _path_string;
+ ofn.nMaxFile = _MAX_PATH;
+ ofn.nFilterIndex = _filter_index;
+ ofn.lpstrFileTitle = NULL;
+ ofn.nMaxFileTitle = 0;
+ ofn.lpstrInitialDir = current_directory_string;
+ ofn.lpstrTitle = _title;
+ ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_EXPLORER | OFN_ENABLEHOOK | OFN_ENABLESIZING;
+ ofn.lpstrFilter = _filter;
+ ofn.nFilterIndex = _filter_index;
+ ofn.lpfnHook = GetSaveFileName_hookproc;
+ ofn.lpstrDefExt = L"svg\0";
+ ofn.lCustData = (LPARAM)this;
+ _result = GetSaveFileNameW(&ofn) != 0;
+
+ g_assert(ofn.nFilterIndex >= 1 && ofn.nFilterIndex <= _filter_count);
+ _filter_index = ofn.nFilterIndex;
+ _extension = _extension_map[ofn.nFilterIndex - 1];
+
+ // Copy the selected file name, converting from UTF-16 to UTF-8
+ myFilename = utf16_to_ustring(_path_string, _MAX_PATH);
+
+ // Tidy up
+ g_free(current_directory_string);
+
+ g_main_loop_quit(_main_loop);
+}
+
+/**
+ * Show this dialog modally. Return true if user hits [OK]
+ */
+bool
+FileSaveDialogImplWin32::show()
+{
+ _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(myFilename, (Inkscape::Extension::Output*)_extension);
+
+ thethread.join();
+ }
+
+ return _result;
+}
+
+void FileSaveDialogImplWin32::setSelectionType( Inkscape::Extension::Extension * /*key*/ )
+{
+ // If no pointer to extension is passed in, look up based on filename extension.
+
+}
+
+
+UINT_PTR CALLBACK FileSaveDialogImplWin32::GetSaveFileName_hookproc(
+ HWND hdlg, UINT uiMsg, WPARAM, LPARAM lParam)
+{
+ FileSaveDialogImplWin32 *pImpl = reinterpret_cast<FileSaveDialogImplWin32*>
+ (GetWindowLongPtr(hdlg, GWLP_USERDATA));
+
+ switch(uiMsg)
+ {
+ case WM_INITDIALOG:
+ {
+ HWND hParentWnd = GetParent(hdlg);
+ HINSTANCE hInstance = GetModuleHandle(NULL);
+
+ // get size/pos of typical combo box
+ RECT rEDT1, rCB1, rROOT, rST;
+ GetWindowRect(GetDlgItem(hParentWnd, cmb1), &rCB1);
+ GetWindowRect(GetDlgItem(hParentWnd, cmb13), &rEDT1);
+ GetWindowRect(GetDlgItem(hParentWnd, stc2), &rST);
+ GetWindowRect(hdlg, &rROOT);
+ int ydelta = rCB1.top - rEDT1.top;
+ if ( ydelta < 0 ) {
+ g_warning("Negative dialog ydelta");
+ ydelta = 0;
+ }
+
+ // Make the window a bit longer
+ // Note: we have a width delta of 1 because there is a suspicion that MoveWindow() to the same size causes zero-width results.
+ RECT rcRect;
+ GetWindowRect(hParentWnd, &rcRect);
+ MoveWindow(hParentWnd, rcRect.left, rcRect.top,
+ sanitizeWindowSizeParam( rcRect.right - rcRect.left, 1, WINDOW_WIDTH_MINIMUM, WINDOW_WIDTH_FALLBACK ),
+ sanitizeWindowSizeParam( rcRect.bottom - rcRect.top, ydelta, WINDOW_HEIGHT_MINIMUM, WINDOW_HEIGHT_FALLBACK ),
+ FALSE);
+
+ // It is not necessary to delete stock objects by calling DeleteObject
+ HGDIOBJ dlgFont = GetStockObject(DEFAULT_GUI_FONT);
+
+ // Set the pointer to the object
+ OPENFILENAMEW *ofn = reinterpret_cast<OPENFILENAMEW*>(lParam);
+ SetWindowLongPtr(hdlg, GWLP_USERDATA, ofn->lCustData);
+ SetWindowLongPtr(hParentWnd, GWLP_USERDATA, ofn->lCustData);
+ pImpl = reinterpret_cast<FileSaveDialogImplWin32*>(ofn->lCustData);
+
+ // Create the Title label and edit control
+ wchar_t *title_label_str = (wchar_t *)g_utf8_to_utf16(_("Title:"), -1, NULL, NULL, NULL);
+ pImpl->_title_label = CreateWindowExW(0, L"STATIC", title_label_str,
+ WS_VISIBLE|WS_CHILD,
+ CW_USEDEFAULT, CW_USEDEFAULT, rCB1.left-rST.left, rST.bottom-rST.top,
+ hParentWnd, NULL, hInstance, NULL);
+ g_free(title_label_str);
+
+ if(pImpl->_title_label) {
+ if(dlgFont) SendMessage(pImpl->_title_label, WM_SETFONT, (WPARAM)dlgFont, MAKELPARAM(FALSE, 0));
+ SetWindowPos(pImpl->_title_label, NULL, rST.left-rROOT.left, rST.top+ydelta-rROOT.top,
+ rCB1.left-rST.left, rST.bottom-rST.top, SWP_SHOWWINDOW|SWP_NOZORDER);
+ }
+
+ pImpl->_title_edit = CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", "",
+ WS_VISIBLE|WS_CHILD|WS_TABSTOP|ES_AUTOHSCROLL,
+ CW_USEDEFAULT, CW_USEDEFAULT, rCB1.right-rCB1.left, rCB1.bottom-rCB1.top,
+ hParentWnd, NULL, hInstance, NULL);
+ if(pImpl->_title_edit) {
+ if(dlgFont) SendMessage(pImpl->_title_edit, WM_SETFONT, (WPARAM)dlgFont, MAKELPARAM(FALSE, 0));
+ SetWindowPos(pImpl->_title_edit, NULL, rCB1.left-rROOT.left, rCB1.top+ydelta-rROOT.top,
+ rCB1.right-rCB1.left, rCB1.bottom-rCB1.top, SWP_SHOWWINDOW|SWP_NOZORDER);
+ SetWindowTextW(pImpl->_title_edit,
+ (const wchar_t*)g_utf8_to_utf16(pImpl->myDocTitle.c_str(), -1, NULL, NULL, NULL));
+ }
+ }
+ break;
+ case WM_DESTROY:
+ {
+ if(pImpl->_title_edit) {
+ int length = GetWindowTextLengthW(pImpl->_title_edit)+1;
+ wchar_t* temp_title = new wchar_t[length];
+ GetWindowTextW(pImpl->_title_edit, temp_title, length);
+ pImpl->myDocTitle = g_utf16_to_utf8((gunichar2*)temp_title, -1, NULL, NULL, NULL);
+ delete[] temp_title;
+ DestroyWindow(pImpl->_title_label);
+ pImpl->_title_label = NULL;
+ DestroyWindow(pImpl->_title_edit);
+ pImpl->_title_edit = NULL;
+ }
+ }
+ break;
+ }
+
+ // Use default dialog behaviour
+ return 0;
+}
+
+} } } // namespace Dialog, UI, Inkscape
+
+#endif // ifdef _WIN32
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/filedialogimpl-win32.h b/src/ui/dialog/filedialogimpl-win32.h
new file mode 100644
index 0000000..b115c61
--- /dev/null
+++ b/src/ui/dialog/filedialogimpl-win32.h
@@ -0,0 +1,392 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Implementation of native file dialogs for Win32
+ */
+/* Authors:
+ * Joel Holdsworth
+ * The Inkscape Organization
+ *
+ * Copyright (C) 2004-2008 The Inkscape Organization
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm.h>
+
+#ifdef _WIN32
+
+#include "filedialogimpl-gtkmm.h"
+
+#include "inkgc/gc-core.h"
+
+#include <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:
+
+ /// Gets the currently selected extension. Valid after an [OK]
+ /// @return Returns a pointer to the selected extension, or NULL
+ /// if the selected filter requires an automatic type detection
+ Inkscape::Extension::Extension* getSelectionType();
+
+ /// Get the path of the current directory
+ Glib::ustring getCurrentDirectory();
+
+
+protected:
+ /// The dialog type
+ FileDialogType dialogType;
+
+ /// A pointer to the GTK main-loop context object. This
+ /// is used to keep the rest of the inkscape UI running
+ /// while the file dialog is displayed
+ GMainLoop *_main_loop;
+
+ /// The result of the call to GetOpenFileName. If true
+ /// the user clicked OK, if false the user clicked cancel
+ bool _result;
+
+ /// The parent window
+ Gtk::Window &parent;
+
+ /// The windows handle of the parent window
+ HWND _ownerHwnd;
+
+ /// The path of the directory that is currently being
+ /// browsed
+ Glib::ustring _current_directory;
+
+ /// The title of the dialog in UTF-16
+ wchar_t *_title;
+
+ /// The path of the currently selected file in UTF-16
+ wchar_t _path_string[_MAX_PATH];
+
+ /// The filter string for GetOpenFileName in UTF-16
+ wchar_t *_filter;
+
+ /// The index of the currently selected filter.
+ /// This value must be greater than or equal to 1,
+ /// and less than or equal to _filter_count.
+ unsigned int _filter_index;
+
+ /// The number of filters registered
+ unsigned int _filter_count;
+
+ /// An array of the extensions associated with the
+ /// file types of each filter. So the Nth entry of
+ /// this array corresponds to the extension of the Nth
+ /// filter in the list. NULL if no specific extension is
+ /// specified/
+ Inkscape::Extension::Extension **_extension_map;
+
+ /// The currently selected extension. Valid after an [OK]
+ Inkscape::Extension::Extension *_extension;
+};
+
+
+/*#########################################################################
+### F I L E O P E N
+#########################################################################*/
+
+/// An Inkscape compatible wrapper around MS Windows GetOpenFileName API
+class FileOpenDialogImplWin32 : public FileOpenDialog, public FileDialogBaseWin32
+{
+public:
+ /// Constructor
+ /// @param parent The parent window for the dialog
+ /// @param dir The directory to begin 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(); }
+
+ /// Gets the currently selected extension. Valid after an [OK]
+ /// @return Returns a pointer to the selected extension, or NULL
+ /// if the selected filter requires an automatic type detection
+ virtual Inkscape::Extension::Extension* getSelectionType()
+ { return FileDialogBaseWin32::getSelectionType(); }
+
+
+ /// Add a custom file filter menu item
+ /// @param name - Name of the filter (such as "Javscript")
+ /// @param pattern - File filtering pattern (such as "*.js")
+ /// Use the FileDialogType::CUSTOM_TYPE in constructor to not include other file types
+ virtual void addFilterMenu(Glib::ustring name, Glib::ustring pattern);
+
+private:
+
+ /// Create filter menu for this type of dialog
+ void createFilterMenu();
+
+ /// The handle of the preview pane window
+ HWND _preview_wnd;
+
+ /// The handle of the file dialog window
+ HWND _file_dialog_wnd;
+
+ /// A pointer to the standard window proc of the
+ /// unhooked file dialog
+ WNDPROC _base_window_proc;
+
+ /// The handle of the bitmap of the "show preview"
+ /// toggle button
+ HBITMAP _show_preview_button_bitmap;
+
+ /// The handle of the toolbar's window
+ HWND _toolbar_wnd;
+
+ /// This flag is set true when the preview should be
+ /// shown, or false when it should be hidden
+ static bool _show_preview;
+
+
+ /// The current width of the preview pane in pixels
+ int _preview_width;
+
+ /// The current height of the preview pane in pixels
+ int _preview_height;
+
+ /// The handle of the windows to display within the
+ /// preview pane, or NULL if no image should be displayed
+ HBITMAP _preview_bitmap;
+
+ /// The windows shell icon for the selected file
+ HICON _preview_file_icon;
+
+ /// The size of the preview file in kilobytes
+ unsigned long _preview_file_size;
+
+
+ /// The width of the document to be shown in the preview panel
+ double _preview_document_width;
+
+ /// The width of the document to be shown in the preview panel
+ double _preview_document_height;
+
+ /// The width of the rendered preview image in pixels
+ int _preview_image_width;
+
+ /// The height of the rendered preview image in pixels
+ int _preview_image_height;
+
+ /// A GDK Pixbuf of the rendered preview to be displayed
+ Glib::RefPtr<Gdk::Pixbuf> _preview_bitmap_image;
+
+ /// This flag is set true if a file has been selected
+ bool _file_selected;
+
+ /// This flag is set true when the GetOpenFileName call
+ /// has returned
+ bool _finished;
+
+ /// This mutex is used to ensure that the worker thread
+ /// that calls GetOpenFileName cannot collide with the
+ /// main Inkscape thread
+ 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(); }
+
+ /// Gets the currently selected extension. Valid after an [OK]
+ /// @return Returns a pointer to the selected extension, or NULL
+ /// if the selected filter requires an automatic type detection
+ virtual Inkscape::Extension::Extension* getSelectionType()
+ { return FileDialogBaseWin32::getSelectionType(); }
+
+ virtual void setSelectionType( Inkscape::Extension::Extension *key );
+
+ virtual void addFileType(Glib::ustring name, Glib::ustring pattern);
+
+private:
+ /// A handle to the title label and edit box
+ HWND _title_label;
+ HWND _title_edit;
+
+ /// Create a filter menu for this type of dialog
+ void createFilterMenu();
+
+ // 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..66b3dcc
--- /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..f5c9bad
--- /dev/null
+++ b/src/ui/dialog/fill-and-stroke.h
@@ -0,0 +1,96 @@
+// 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;
+
+ static FillAndStroke &getInstance() { return *new FillAndStroke(); }
+
+ 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;
+ FillAndStroke(FillAndStroke const &d) = delete;
+ FillAndStroke& operator=(FillAndStroke const &d) = delete;
+
+ 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..7bb4fe6
--- /dev/null
+++ b/src/ui/dialog/filter-effects-dialog.cpp
@@ -0,0 +1,3124 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Filter Effects dialog.
+ */
+/* Authors:
+ * Nicholas Bishop <nicholasbishop@gmail.org>
+ * Rodrigo Kumpera <kumpera@gmail.com>
+ * Felipe C. da S. Sanches <juca@members.fsf.org>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ * insaner
+ *
+ * Copyright (C) 2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/imagemenuitem.h>
+
+#include <gdkmm/display.h>
+#include <gdkmm/general.h>
+#include <gdkmm/seat.h>
+
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/colorbutton.h>
+#include <gtkmm/eventbox.h>
+#include <gtkmm/sizegroup.h>
+
+#include <glibmm/i18n.h>
+#include <glibmm/stringutils.h>
+#include <glibmm/main.h>
+#include <glibmm/convert.h>
+
+#include <utility>
+
+#include "desktop.h"
+#include "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/dialog/filedialog.h"
+#include "ui/icon-names.h"
+#include "ui/util.h"
+#include "ui/widget/filter-effect-chooser.h"
+#include "ui/widget/spinbutton.h"
+
+#include "io/sys.h"
+
+
+using namespace Inkscape::Filters;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+using Inkscape::UI::Widget::AttrWidget;
+using Inkscape::UI::Widget::ComboBoxEnum;
+using Inkscape::UI::Widget::DualSpinScale;
+using Inkscape::UI::Widget::SpinScale;
+
+
+// Returns the number of inputs available for the filter primitive type
+static int input_count(const SPFilterPrimitive* prim)
+{
+ if(!prim)
+ return 0;
+ else if(SP_IS_FEBLEND(prim) || SP_IS_FECOMPOSITE(prim) || SP_IS_FEDISPLACEMENTMAP(prim))
+ return 2;
+ else if(SP_IS_FEMERGE(prim)) {
+ // Return the number of feMergeNode connections plus an extra
+ return (int) (prim->children.size() + 1);
+ }
+ else
+ return 1;
+}
+
+class CheckButtonAttr : public Gtk::CheckButton, public AttrWidget
+{
+public:
+ CheckButtonAttr(bool def, const Glib::ustring& label,
+ Glib::ustring tv, Glib::ustring fv,
+ const 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(), false, false);
+ }
+ }
+
+ ~MultiSpinButton() override
+ {
+ for(auto & _spin : _spins)
+ delete _spin;
+ }
+
+ std::vector<SpinButtonAttr*>& get_spinbuttons()
+ {
+ return _spins;
+ }
+private:
+ std::vector<SpinButtonAttr*> _spins;
+};
+
+// Contains two spinbuttons that describe a NumberOptNumber
+class DualSpinButton : public Gtk::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, false, false);
+ pack_end(_s1, false, false);
+ }
+
+ Inkscape::UI::Widget::SpinButton& get_spinbutton1()
+ {
+ return _s1;
+ }
+
+ Inkscape::UI::Widget::SpinButton& get_spinbutton2()
+ {
+ return _s2;
+ }
+
+ Glib::ustring get_as_attribute() const override
+ {
+ double v1 = _s1.get_value();
+ double v2 = _s2.get_value();
+
+ if(_s1.get_digits() == 0) {
+ v1 = (int)v1;
+ v2 = (int)v2;
+ }
+
+ return Glib::Ascii::dtostr(v1) + " " + Glib::Ascii::dtostr(v2);
+ }
+
+ void set_from_attribute(SPObject* o) override
+ {
+ const gchar* val = attribute_value(o);
+ NumberOptNumber n;
+ if(val) {
+ n.set(val);
+ } else {
+ n.set(get_default()->as_charptr());
+ }
+ _s1.set_value(n.getNumber());
+ _s2.set_value(n.getOptNumber());
+
+ }
+private:
+ Inkscape::UI::Widget::SpinButton _s1, _s2;
+};
+
+class ColorButton : public Gtk::ColorButton, public AttrWidget
+{
+public:
+ ColorButton(unsigned int def, const SPAttr a, char* tip_text)
+ : AttrWidget(a, def)
+ {
+ signal_color_set().connect(signal_attr_changed().make_slot());
+ if (tip_text) {
+ set_tooltip_text(tip_text);
+ }
+
+ Gdk::RGBA col;
+ col.set_rgba_u(65535, 65535, 65535);
+ set_rgba(col);
+ }
+
+ // Returns the color in 'rgb(r,g,b)' form.
+ Glib::ustring get_as_attribute() const override
+ {
+ // no doubles here, so we can use the standard string stream.
+ std::ostringstream os;
+
+ const auto c = get_rgba();
+ const int r = c.get_red_u() / 257, g = c.get_green_u() / 257, b = c.get_blue_u() / 257;//TO-DO: verify this. This sounds a lot strange! shouldn't it be 256?
+ os << "rgb(" << r << "," << g << "," << b << ")";
+ return os.str();
+ }
+
+
+ void set_from_attribute(SPObject* o) override
+ {
+ const gchar* val = attribute_value(o);
+ guint32 i = 0;
+ if(val) {
+ i = sp_svg_read_color(val, 0xFFFFFFFF);
+ } else {
+ i = (guint32) get_default()->as_uint();
+ }
+ const int r = SP_RGBA32_R_U(i), g = SP_RGBA32_G_U(i), b = SP_RGBA32_B_U(i);
+
+ Gdk::RGBA col;
+ col.set_rgba_u(r * 256, g * 256, b * 256);
+ set_rgba(col);
+ }
+};
+
+// Used for tableValue in feComponentTransfer
+class EntryAttr : public Gtk::Entry, public AttrWidget
+{
+public:
+ EntryAttr(const SPAttr a, char* tip_text)
+ : AttrWidget(a)
+ {
+ signal_changed().connect(signal_attr_changed().make_slot());
+ if (tip_text) {
+ set_tooltip_text(tip_text);
+ }
+ }
+
+ // No validity checking is done
+ Glib::ustring get_as_attribute() const override
+ {
+ return get_text();
+ }
+
+ void set_from_attribute(SPObject* o) override
+ {
+ const gchar* val = attribute_value(o);
+ if(val) {
+ set_text( val );
+ } else {
+ set_text( "" );
+ }
+ }
+};
+
+/* Displays/Edits the matrix for feConvolveMatrix or feColorMatrix */
+class FilterEffectsDialog::MatrixAttr : public Gtk::Frame, public AttrWidget
+{
+public:
+ MatrixAttr(const 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(SP_IS_FECONVOLVEMATRIX(o)) {
+ SPFeConvolveMatrix* conv = SP_FECONVOLVEMATRIX(o);
+ int cols, rows;
+ cols = (int)conv->order.getNumber();
+ if(cols > 5)
+ cols = 5;
+ rows = conv->order.optNumber_set ? (int)conv->order.getOptNumber() : cols;
+ update(o, rows, cols);
+ }
+ else if(SP_IS_FECOLORMATRIX(o))
+ update(o, 4, 5);
+ }
+ }
+private:
+ class MatrixColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ MatrixColumns()
+ {
+ cols.resize(5);
+ for(auto & col : cols)
+ add(col);
+ }
+ std::vector<Gtk::TreeModelColumn<double> > cols;
+ };
+
+ void update(SPObject* o, const int rows, const int cols)
+ {
+ if(_locked)
+ return;
+
+ _model->clear();
+
+ _tree.remove_all_columns();
+
+ std::vector<gdouble>* values = nullptr;
+ if(SP_IS_FECOLORMATRIX(o))
+ values = &SP_FECOLORMATRIX(o)->values;
+ else if(SP_IS_FECONVOLVEMATRIX(o))
+ values = &SP_FECONVOLVEMATRIX(o)->kernelMatrix;
+ else
+ return;
+
+ if(o) {
+ int ndx = 0;
+
+ for(int i = 0; i < cols; ++i) {
+ _tree.append_column_numeric_editable("", _columns.cols[i], "%.2f");
+ 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(SP_IS_FECOLORMATRIX(o)) {
+ SPFeColorMatrix* col = SP_FECOLORMATRIX(o);
+ remove();
+ switch(col->type) {
+ case COLORMATRIX_SATURATE:
+ add(_saturation);
+ if(_use_stored)
+ _saturation.set_value(_saturation_store);
+ else
+ _saturation.set_from_attribute(o);
+ 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)
+ {
+ pack_start(_entry, false, false);
+ pack_start(_fromFile, false, false);
+ pack_start(_fromSVGElement, false, false);
+
+ _fromFile.set_label(_("Image File"));
+ _fromFile.signal_clicked().connect(sigc::mem_fun(*this, &FileOrElementChooser::select_file));
+
+ _fromSVGElement.set_label(_("Selected SVG Element"));
+ _fromSVGElement.signal_clicked().connect(sigc::mem_fun(*this, &FileOrElementChooser::select_svg_element));
+
+ _entry.signal_changed().connect(signal_attr_changed().make_slot());
+
+ show_all();
+
+ }
+
+ // Returns the element in xlink:href form.
+ Glib::ustring get_as_attribute() const override
+ {
+ return _entry.get_text();
+ }
+
+
+ void set_from_attribute(SPObject* o) override
+ {
+ const gchar* val = attribute_value(o);
+ if(val) {
+ _entry.set_text(val);
+ } else {
+ _entry.set_text("");
+ }
+ }
+
+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;
+ }
+ }
+
+ // Show the active settings group and update all the AttrWidgets with new values
+ void show_and_update(const int t, SPObject* ob)
+ {
+ if(t != _current_type) {
+ type(t);
+ for(auto & _group : _groups)
+ _group->hide();
+ }
+ if(t >= 0) {
+ _groups[t]->show(); // Do not use show_all(), it shows children than should be hidden
+ }
+ _dialog.set_attrs_locked(true);
+ for(auto & i : _attrwidgets[_current_type])
+ i->set_from_attribute(ob);
+ _dialog.set_attrs_locked(false);
+ }
+
+ int get_current_type() const
+ {
+ return _current_type;
+ }
+
+ void type(const int t)
+ {
+ _current_type = t;
+ }
+
+ void add_no_params()
+ {
+ Gtk::Label* lbl = Gtk::manage(new Gtk::Label(_("This SVG filter effect does not require any parameters.")));
+ add_widget(lbl, "");
+ }
+
+ // 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 climb, const int digits, char* tip_text = nullptr)
+ {
+ Glib::ustring tip_text2;
+ if (tip_text)
+ tip_text2 = tip_text;
+ SpinScale* spinslider = new SpinScale("", def, lo, hi, step_inc, climb, digits, attr, tip_text2);
+ add_widget(spinslider, label);
+ add_attr_widget(spinslider);
+ return spinslider;
+ }
+
+ // DualSpinScale
+ DualSpinScale* add_dualspinscale(const 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(12);
+
+ if (label != "") {
+ Gtk::Label *lbl = Gtk::manage(new Gtk::Label(label));
+ lbl->set_xalign(0.0);
+ hb->pack_start(*lbl, Gtk::PACK_SHRINK);
+ _size_group->add_widget(*lbl);
+ }
+
+ hb->pack_start(*w, Gtk::PACK_EXPAND_WIDGET);
+ _groups[_current_type]->pack_start(*hb, Gtk::PACK_EXPAND_WIDGET);
+ hb->show_all();
+ }
+
+ std::vector<Gtk::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_IN);
+ add(_box);
+ _box.add(_type);
+ _box.reorder_child(_type, 0);
+ _type.signal_changed().connect(sigc::mem_fun(*this, &ComponentTransferValues::on_type_changed));
+
+ _settings.type(COMPONENTTRANSFER_TYPE_LINEAR);
+ _settings.add_spinscale(1, 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, _("Table"));
+
+ _settings.type(COMPONENTTRANSFER_TYPE_DISCRETE);
+ _settings.add_entry(SPAttr::TABLEVALUES, _("Discrete"));
+
+ //_settings.type(COMPONENTTRANSFER_TYPE_IDENTITY);
+ _settings.type(-1); // Force update_and_show() to show/hide windows correctly
+ }
+
+ // FuncNode can be in any order so we must search to find correct one.
+ SPFeFuncNode* find_node(SPFeComponentTransfer* ct)
+ {
+ SPFeFuncNode* funcNode = nullptr;
+ bool found = false;
+ for(auto& node: ct->children) {
+ funcNode = SP_FEFUNCNODE(&node);
+ if( funcNode->channel == _channel ) {
+ found = true;
+ break;
+ }
+ }
+ if( !found )
+ funcNode = nullptr;
+
+ return funcNode;
+ }
+
+ void set_func_attr(const AttrWidget* input)
+ {
+ _dialog.set_attr( _funcNode, input->get_attribute(), input->get_as_attribute().c_str());
+ }
+
+ // Set new type and update widget visibility
+ void set_from_attribute(SPObject* o) override
+ {
+ // See componenttransfer.cpp
+ if(SP_IS_FECOMPONENTTRANSFER(o)) {
+ SPFeComponentTransfer* ct = SP_FECOMPONENTTRANSFER(o);
+
+ _funcNode = find_node(ct);
+ if( _funcNode ) {
+ _type.set_from_attribute( _funcNode );
+ } else {
+ // Create <funcNode>
+ SPFilterPrimitive* prim = _dialog._primitive_list.get_selected();
+ if(prim) {
+ Inkscape::XML::Document *xml_doc = prim->document->getReprDoc();
+ Inkscape::XML::Node *repr = nullptr;
+ switch(_channel) {
+ case SPFeFuncNode::R:
+ repr = xml_doc->createElement("svg:feFuncR");
+ break;
+ case SPFeFuncNode::G:
+ repr = xml_doc->createElement("svg:feFuncG");
+ break;
+ case SPFeFuncNode::B:
+ repr = xml_doc->createElement("svg:feFuncB");
+ break;
+ case SPFeFuncNode::A:
+ repr = xml_doc->createElement("svg:feFuncA");
+ break;
+ }
+
+ //XML Tree being used directly here while it shouldn't be.
+ prim->getRepr()->appendChild(repr);
+ Inkscape::GC::release(repr);
+
+ // Now we should find it!
+ _funcNode = find_node(ct);
+ if( _funcNode ) {
+ _funcNode->setAttribute( "type", "identity" );
+ } else {
+ //std::cout << "ERROR ERROR: feFuncX not found!" << std::endl;
+ }
+ }
+ }
+
+ update();
+ }
+ }
+
+private:
+ void on_type_changed()
+ {
+ SPFilterPrimitive* prim = _dialog._primitive_list.get_selected();
+ if(prim) {
+
+ _funcNode->setAttributeOrRemoveIfEmpty("type", _type.get_as_attribute());
+
+ SPFilter* filter = _dialog._filter_modifier.get_selected_filter();
+ filter->requestModified(SP_OBJECT_MODIFIED_FLAG);
+
+ DocumentUndo::done(prim->document, _("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(12);
+
+ _box.add(_light_box);
+ _box.reorder_child(_light_box, 0);
+ _light_source.signal_changed().connect(sigc::mem_fun(*this, &LightSourceControl::on_source_changed));
+
+ // FIXME: these range values are complete crap
+
+ _settings.type(LIGHT_DISTANT);
+ _settings.add_spinscale(0, 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:"), 1, 100, 1, 1, 0, _("Exponent value controlling the focus for the light source"));
+ //TODO: here I have used 100 degrees as default value. But spec says that if not specified, no limiting cone is applied. So, there should be a way for the user to set a "no limiting cone" option.
+ _settings.add_spinscale(100, SPAttr::LIMITINGCONEANGLE, _("Cone Angle:"), 1, 100, 1, 1, 0, _("This is the angle between the spot light axis (i.e. the axis between the light source and the point to which it is pointing at) and the spot light cone. No light is projected outside this cone."));
+
+ _settings.type(-1); // Force update_and_show() to show/hide windows correctly
+
+ }
+
+ Gtk::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(SP_IS_FEDISTANTLIGHT(child))
+ _light_source.set_active(0);
+ else if(SP_IS_FEPOINTLIGHT(child))
+ _light_source.set_active(1);
+ else if(SP_IS_FESPOTLIGHT(child))
+ _light_source.set_active(2);
+ else
+ _light_source.set_active(-1);
+
+ update();
+
+ _locked = false;
+ }
+private:
+ void on_source_changed()
+ {
+ if(_locked)
+ return;
+
+ SPFilterPrimitive* prim = _dialog._primitive_list.get_selected();
+ if(prim) {
+ _locked = true;
+
+ SPObject* child = prim->firstChild();
+ const int ls = _light_source.get_active_row_number();
+ // Check if the light source type has changed
+ if(!(ls == -1 && !child) &&
+ !(ls == 0 && SP_IS_FEDISTANTLIGHT(child)) &&
+ !(ls == 1 && SP_IS_FEPOINTLIGHT(child)) &&
+ !(ls == 2 && SP_IS_FESPOTLIGHT(child))) {
+ if(child)
+ //XML Tree being used directly here while it shouldn't be.
+ sp_repr_unparent(child->getRepr());
+
+ if(ls != -1) {
+ Inkscape::XML::Document *xml_doc = prim->document->getReprDoc();
+ Inkscape::XML::Node *repr = xml_doc->createElement(_light_source.get_active_data()->key.c_str());
+ //XML Tree being used directly here while it shouldn't be.
+ prim->getRepr()->appendChild(repr);
+ Inkscape::GC::release(repr);
+ }
+
+ DocumentUndo::done(prim->document, _("New light source"), INKSCAPE_ICON("dialog-filters"));
+ update();
+ }
+
+ _locked = false;
+ }
+ }
+
+ void update()
+ {
+ _box.hide();
+ _box.show();
+ _light_box.show_all();
+
+ SPFilterPrimitive* prim = _dialog._primitive_list.get_selected();
+ if(prim && prim->firstChild())
+ _settings.show_and_update(_light_source.get_active_data()->id, prim->firstChild());
+ }
+
+ FilterEffectsDialog& _dialog;
+ Gtk::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);
+ 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)
+ : Gtk::Box(Gtk::ORIENTATION_VERTICAL),
+ _dialog(d),
+ _add(_("_New"), true),
+ _observer(new Inkscape::XML::SignalObserver)
+{
+ Gtk::ScrolledWindow* sw = Gtk::manage(new Gtk::ScrolledWindow);
+ pack_start(*sw);
+ pack_start(_add, false, false);
+ sw->add(_list);
+
+ _model = Gtk::ListStore::create(_columns);
+ _list.set_model(_model);
+ _cell_toggle.set_active(true);
+ const int selcol = _list.append_column("", _cell_toggle);
+ Gtk::TreeViewColumn* col = _list.get_column(selcol - 1);
+ if(col)
+ col->add_attribute(_cell_toggle.property_active(), _columns.sel);
+ _list.append_column_editable(_("_Filter"), _columns.label);
+ ((Gtk::CellRendererText*)_list.get_column(1)->get_first_cell())->
+ signal_edited().connect(sigc::mem_fun(*this, &FilterEffectsDialog::FilterModifier::on_name_edited));
+
+ _list.append_column("#", _columns.count);
+ _list.get_column(2)->set_sizing(Gtk::TREE_VIEW_COLUMN_AUTOSIZE);
+ _list.get_column(2)->set_expand(false);
+ _list.get_column(2)->set_reorderable(true);
+
+ sw->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ _list.get_column(1)->set_resizable(true);
+ _list.get_column(1)->set_sizing(Gtk::TREE_VIEW_COLUMN_AUTOSIZE);
+ _list.get_column(1)->set_expand(true);
+
+ _list.set_reorderable(true);
+ _list.enable_model_drag_dest (Gdk::ACTION_MOVE);
+
+ _list.signal_drag_drop().connect( sigc::mem_fun(*this, &FilterModifier::on_filter_move), false );
+
+ sw->set_shadow_type(Gtk::SHADOW_IN);
+ show_all_children();
+ _add.signal_clicked().connect(sigc::mem_fun(*this, &FilterModifier::add_filter));
+ _cell_toggle.signal_toggled().connect(sigc::mem_fun(*this, &FilterModifier::on_selection_toggled));
+ _list.signal_button_release_event().connect_notify(
+ sigc::mem_fun(*this, &FilterModifier::filter_list_button_release));
+
+ _menu = create_popup_menu(*this,
+ sigc::mem_fun(*this, &FilterModifier::duplicate_filter),
+ sigc::mem_fun(*this, &FilterModifier::remove_filter));
+
+ Gtk::MenuItem *rename_item = Gtk::manage(new Gtk::MenuItem(_("R_ename"), true));
+ rename_item->signal_activate().connect(sigc::mem_fun(*this, &FilterModifier::rename_filter));
+ rename_item->show();
+ _menu->append(*rename_item);
+ _menu->accelerate(*this);
+
+ Gtk::MenuItem *select_item = Gtk::manage(new Gtk::MenuItem(_("Select"), true));
+ select_item->signal_activate().connect(sigc::mem_fun(*this, &FilterModifier::select_filter_elements));
+ select_item->show();
+ _menu->append(*select_item);
+ _menu->accelerate(*this);
+
+ _list.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &FilterModifier::on_filter_selection_changed));
+ _observer->signal_changed().connect(signal_filter_changed().make_slot());
+}
+
+// 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;
+ auto itemlist= sel->items();
+ for(auto i=itemlist.begin(); itemlist.end() != i; ++i) {
+ SPObject *obj = *i;
+ SPStyle *style = obj->style;
+ if (!style || !SP_IS_ITEM(obj)) {
+ continue;
+ }
+
+ if (style->filter.set && style->getFilter()) {
+ SP_ITEM(obj)->bbox_valid = FALSE;
+ used.insert(style->getFilter());
+ } else {
+ used.insert(nullptr);
+ }
+ }
+
+ const int size = used.size();
+
+ for (Gtk::TreeIter iter = _model->children().begin(); iter != _model->children().end(); ++iter) {
+ if (used.find((*iter)[_columns.filter]) != used.end()) {
+ // If only one filter is in use by the selection, select it
+ if (size == 1) {
+ _list.get_selection()->select(iter);
+ }
+ (*iter)[_columns.sel] = size;
+ } else {
+ (*iter)[_columns.sel] = 0;
+ }
+ }
+ update_counts();
+}
+
+void FilterEffectsDialog::FilterModifier::on_filter_selection_changed()
+{
+ _observer->set(get_selected_filter());
+ signal_filter_changed()();
+}
+
+void FilterEffectsDialog::FilterModifier::on_name_edited(const Glib::ustring& path, const Glib::ustring& text)
+{
+ Gtk::TreeModel::iterator iter = _model->get_iter(path);
+
+ if(iter) {
+ SPFilter* filter = (*iter)[_columns.filter];
+ filter->setLabel(text.c_str());
+ DocumentUndo::done(filter->document, _("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 = _model->get_iter(path);
+
+ if(iter) {
+ 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)
+ 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(const auto & i : _model->children()) {
+ SPFilter* f = i[_columns.filter];
+ i[_columns.count] = f->getRefCount();
+ }
+}
+
+/* Add all filters in the document to the combobox.
+ Keeps the same selection if possible, otherwise selects the first element */
+void FilterEffectsDialog::FilterModifier::update_filters()
+{
+ std::vector<SPObject *> filters = _dialog.getDocument()->getResourceList("filter");
+
+ _model->clear();
+
+ for (auto filter : filters) {
+ Gtk::TreeModel::Row row = *_model->append();
+ SPFilter* f = SP_FILTER(filter);
+ row[_columns.filter] = f;
+ const gchar* lbl = f->label();
+ const gchar* id = f->getId();
+ row[_columns.label] = lbl ? lbl : (id ? id : "filter");
+ }
+
+ update_selection(_dialog.getSelection());
+ _dialog.update_filter_general_settings_view();
+}
+
+SPFilter* FilterEffectsDialog::FilterModifier::get_selected_filter()
+{
+ if(_list.get_selection()) {
+ Gtk::TreeModel::iterator i = _list.get_selection()->get_selected();
+
+ if(i)
+ return (*i)[_columns.filter];
+ }
+
+ return nullptr;
+}
+
+void FilterEffectsDialog::FilterModifier::select_filter(const SPFilter* filter)
+{
+ if(filter) {
+ for(Gtk::TreeModel::iterator i = _model->children().begin();
+ i != _model->children().end(); ++i) {
+ if((*i)[_columns.filter] == filter) {
+ _list.get_selection()->select(i);
+ break;
+ }
+ }
+ }
+}
+
+void FilterEffectsDialog::FilterModifier::filter_list_button_release(GdkEventButton* event)
+{
+ if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) {
+ const bool sensitive = get_selected_filter() != nullptr;
+ auto items = _menu->get_children();
+ items[0]->set_sensitive(sensitive);
+ items[1]->set_sensitive(sensitive);
+ 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 = _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
+ std::vector<SPItem*> x,y;
+ std::vector<SPItem*> all = get_all_items(x, desktop->layerManager().currentRoot(), desktop, false, false, true, y);
+ for(std::vector<SPItem*>::const_iterator i=all.begin(); all.end() != i; ++i) {
+ if (!SP_IS_ITEM(*i)) {
+ continue;
+ }
+ SPItem *item = *i;
+ if (!item->style) {
+ continue;
+ }
+
+ const SPIFilter *ifilter = &(item->style->filter);
+ if (ifilter && ifilter->href) {
+ const SPObject *obj = ifilter->href->getObject();
+ if (obj && obj == (SPObject *)filter) {
+ ::remove_filter(item, false);
+ }
+ }
+ }
+
+ //XML Tree being used directly here while it shouldn't be.
+ sp_repr_unparent(filter->getRepr());
+
+ DocumentUndo::done(doc, _("Remove filter"), INKSCAPE_ICON("dialog-filters"));
+
+ update_filters();
+ }
+}
+
+void FilterEffectsDialog::FilterModifier::duplicate_filter()
+{
+ SPFilter* filter = get_selected_filter();
+
+ if (filter) {
+ Inkscape::XML::Node *repr = filter->getRepr();
+ Inkscape::XML::Node *parent = repr->parent();
+ repr = repr->duplicate(repr->document());
+ parent->appendChild(repr);
+
+ DocumentUndo::done(filter->document, _("Duplicate filter"), INKSCAPE_ICON("dialog-filters"));
+
+ update_filters();
+ }
+}
+
+void FilterEffectsDialog::FilterModifier::rename_filter()
+{
+ _list.set_cursor(_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*> x,y;
+ std::vector<SPItem*> items;
+ std::vector<SPItem*> all = get_all_items(x, desktop->layerManager().currentRoot(), desktop, false, false, true, y);
+ 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
+{
+ PrimitiveList& primlist = dynamic_cast<PrimitiveList&>(widget);
+ minimum_width = natural_width = size * primlist.primitive_count() + primlist.get_input_type_width() * 6;
+}
+
+void FilterEffectsDialog::CellRendererConnection::get_preferred_width_for_height_vfunc(Gtk::Widget& widget,
+ int /* height */,
+ int& minimum_width,
+ int& natural_width) const
+{
+ get_preferred_width(widget, minimum_width, natural_width);
+}
+
+void FilterEffectsDialog::CellRendererConnection::get_preferred_height_vfunc(Gtk::Widget& widget,
+ int& minimum_height,
+ int& natural_height) const
+{
+ // Scale the height depending on the number of inputs, unless it's
+ // the first primitive, in which case there are no connections
+ SPFilterPrimitive* prim = SP_FILTER_PRIMITIVE(_primitive.get_value());
+ minimum_height = natural_height = size * input_count(prim);
+}
+
+void FilterEffectsDialog::CellRendererConnection::get_preferred_height_for_width_vfunc(Gtk::Widget& widget,
+ int /* width */,
+ int& minimum_height,
+ int& natural_height) const
+{
+ get_preferred_height(widget, minimum_height, natural_height);
+}
+
+/*** PrimitiveList ***/
+FilterEffectsDialog::PrimitiveList::PrimitiveList(FilterEffectsDialog& d)
+ : _dialog(d),
+ _in_drag(0),
+ _observer(new Inkscape::XML::SignalObserver)
+{
+ signal_draw().connect(sigc::mem_fun(*this, &PrimitiveList::on_draw_signal));
+
+ add_events(Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK);
+
+ _model = Gtk::ListStore::create(_columns);
+
+ set_reorderable(true);
+
+ set_model(_model);
+ append_column(_("_Effect"), _columns.type);
+ get_column(0)->set_resizable(true);
+ set_headers_visible();
+
+ _observer->signal_changed().connect(signal_primitive_changed().make_slot());
+ get_selection()->signal_changed().connect(sigc::mem_fun(*this, &PrimitiveList::on_primitive_selection_changed));
+ signal_primitive_changed().connect(sigc::mem_fun(*this, &PrimitiveList::queue_draw));
+
+ init_text();
+
+ int cols_count = append_column(_("Connections"), _connection_cell);
+ Gtk::TreeViewColumn* col = get_column(cols_count - 1);
+ if(col)
+ col->add_attribute(_connection_cell.property_primitive(), _columns.primitive);
+}
+
+// Sets up a vertical Pango context/layout, and returns the largest
+// width needed to render the FilterPrimitiveInput labels.
+void FilterEffectsDialog::PrimitiveList::init_text()
+{
+ // Set up a vertical context+layout
+ Glib::RefPtr<Pango::Context> context = create_pango_context();
+ const Pango::Matrix matrix = {0, -1, 1, 0, 0, 0};
+ context->set_matrix(matrix);
+ _vertical_layout = Pango::Layout::create(context);
+
+ // Store the maximum height and width of the an input type label
+ // for later use in drawing and measuring.
+ _input_type_height = _input_type_width = 0;
+ for(unsigned int i = 0; i < FPInputConverter._length; ++i) {
+ _vertical_layout->set_text(_(FPInputConverter.get_label((FilterPrimitiveInput)i).c_str()));
+ int fontw, fonth;
+ _vertical_layout->get_pixel_size(fontw, fonth);
+ if(fonth > _input_type_width)
+ _input_type_width = fonth;
+ if (fontw > _input_type_height)
+ _input_type_height = fontw;
+ }
+}
+
+sigc::signal<void>& FilterEffectsDialog::PrimitiveList::signal_primitive_changed()
+{
+ return _signal_primitive_changed;
+}
+
+void FilterEffectsDialog::PrimitiveList::on_primitive_selection_changed()
+{
+ _observer->set(get_selected());
+ signal_primitive_changed()();
+ _dialog._color_matrix_values->clear_store();
+}
+
+/* Add all filter primitives in the current to the list.
+ Keeps the same selection if possible, otherwise selects the first element */
+void FilterEffectsDialog::PrimitiveList::update()
+{
+ SPFilter* f = _dialog._filter_modifier.get_selected_filter();
+ const SPFilterPrimitive* active_prim = get_selected();
+ _model->clear();
+
+ if(f) {
+ bool active_found = false;
+ _dialog._primitive_box->set_sensitive(true);
+ _dialog.update_filter_general_settings_view();
+ for(auto& prim_obj: f->children) {
+ SPFilterPrimitive *prim = SP_FILTER_PRIMITIVE(&prim_obj);
+ if(!prim) {
+ break;
+ }
+ Gtk::TreeModel::Row row = *_model->append();
+ row[_columns.primitive] = prim;
+
+ //XML Tree being used directly here while it shouldn't be.
+ row[_columns.type_id] = FPConverter.get_id_from_key(prim->getRepr()->name());
+ row[_columns.type] = _(FPConverter.get_label(row[_columns.type_id]).c_str());
+
+ if (prim->getId()) {
+ row[_columns.id] = Glib::ustring(prim->getId());
+ }
+
+ if(prim == active_prim) {
+ get_selection()->select(row);
+ active_found = true;
+ }
+ }
+
+ if(!active_found && _model->children().begin())
+ get_selection()->select(_model->children().begin());
+
+ columns_autosize();
+
+ int width, height;
+ get_size_request(width, height);
+ if (height == -1) {
+ // Need to account for the height of the input type text (rotated text) as well as the
+ // column headers. Input type text height determined in init_text() by measuring longest
+ // string. Column header height determined by mapping y coordinate of visible
+ // rectangle to widget coordinates.
+ Gdk::Rectangle vis;
+ int vis_x, vis_y;
+ get_visible_rect(vis);
+ convert_tree_to_widget_coords(vis.get_x(), vis.get_y(), vis_x, vis_y);
+ set_size_request(width, _input_type_height + 2 + vis_y);
+ }
+ }
+ else {
+ _dialog._primitive_box->set_sensitive(false);
+ set_size_request(-1, -1);
+ }
+}
+
+void FilterEffectsDialog::PrimitiveList::set_menu(Gtk::Widget& parent,
+ sigc::slot<void> dup,
+ sigc::slot<void> rem)
+{
+ _primitive_menu = create_popup_menu(parent, dup, rem);
+}
+
+SPFilterPrimitive* FilterEffectsDialog::PrimitiveList::get_selected()
+{
+ if(_dialog._filter_modifier.get_selected_filter()) {
+ Gtk::TreeModel::iterator i = get_selection()->get_selected();
+ if(i)
+ return (*i)[_columns.primitive];
+ }
+
+ return nullptr;
+}
+
+void FilterEffectsDialog::PrimitiveList::select(SPFilterPrimitive* prim)
+{
+ for(Gtk::TreeIter i = _model->children().begin();
+ i != _model->children().end(); ++i) {
+ if((*i)[_columns.primitive] == prim)
+ get_selection()->select(i);
+ }
+}
+
+void FilterEffectsDialog::PrimitiveList::remove_selected()
+{
+ SPFilterPrimitive* prim = get_selected();
+
+ if(prim) {
+ _observer->set(nullptr);
+ _model->erase(get_selection()->get_selected());
+
+ //XML Tree being used directly here while it shouldn't be.
+ sp_repr_unparent(prim->getRepr());
+
+ DocumentUndo::done(_dialog.getDocument(), _("Remove filter primitive"), INKSCAPE_ICON("dialog-filters"));
+
+ update();
+ }
+}
+
+bool FilterEffectsDialog::PrimitiveList::on_draw_signal(const Cairo::RefPtr<Cairo::Context> & cr)
+{
+ cr->set_line_width(1.0);
+ // In GTK+ 3, the draw function receives the widget window, not the
+ // bin_window (i.e., just the area under the column headers). We
+ // therefore translate the origin of our coordinate system to account for this
+ int x_origin, y_origin;
+ convert_bin_window_to_widget_coords(0,0,x_origin,y_origin);
+ cr->translate(x_origin, y_origin);
+
+ auto sc = 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;
+ Gdk::RGBA mid_color;
+ {
+ auto lerp = [](double v0, double v1, double t){ return (1.0 - t) * v0 + t * v1; };
+ fg_color.set_rgba(
+ lerp(bg_color.get_red(), orig_color.get_red(), 0.95),
+ lerp(bg_color.get_green(), orig_color.get_green(), 0.95),
+ lerp(bg_color.get_blue(), orig_color.get_blue(), 0.95),
+ orig_color.get_alpha());
+ bg_color.set_rgba(
+ lerp(bg_color.get_red(), orig_color.get_red(), 0.05),
+ lerp(bg_color.get_green(), orig_color.get_green(), 0.05),
+ lerp(bg_color.get_blue(), orig_color.get_blue(), 0.05),
+ bg_color.get_alpha());
+ mid_color.set_rgba(
+ lerp(bg_color.get_red(), fg_color.get_red(), 0.65),
+ lerp(bg_color.get_green(), fg_color.get_green(), 0.65),
+ lerp(bg_color.get_blue(), fg_color.get_blue(), 0.65),
+ fg_color.get_alpha());
+ }
+
+ SPFilterPrimitive* prim = get_selected();
+ int row_count = get_model()->children().size();
+
+ int fheight = CellRendererConnection::size;
+ Gdk::Rectangle rct, vis;
+ Gtk::TreeIter row = get_model()->children().begin();
+ int text_start_x = 0;
+ if(row) {
+ get_cell_area(get_model()->get_path(row), *get_column(1), rct);
+ get_visible_rect(vis);
+ text_start_x = rct.get_x() + rct.get_width() - get_input_type_width() * FPInputConverter._length + 1;
+
+ for(unsigned int i = 0; i < FPInputConverter._length; ++i) {
+ _vertical_layout->set_text(_(FPInputConverter.get_label((FilterPrimitiveInput)i).c_str()));
+ const int x = text_start_x + get_input_type_width() * i;
+ cr->save();
+ Gdk::Cairo::set_source_rgba(cr, bg_color);
+ cr->rectangle(x, 0, get_input_type_width(), vis.get_height());
+ cr->fill_preserve();
+
+ Gdk::Cairo::set_source_rgba(cr, fg_color);
+ cr->move_to(x + get_input_type_width(), 5);
+ cr->rotate_degrees(90);
+ _vertical_layout->show_in_cairo_context(cr);
+
+ Gdk::Cairo::set_source_rgba(cr, mid_color);
+ cr->move_to(x, 0);
+ cr->line_to(x, vis.get_height());
+ cr->stroke();
+ cr->restore();
+ }
+ cr->rectangle(vis.get_x(), 0, vis.get_width(), vis.get_height());
+ 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(0.5);
+ get_bin_window()->get_device_position(device, mx, my, mask);
+
+ // Outline the bottom of the connection area
+ const int outline_x = x + fheight * (row_count - row_index);
+ cr->save();
+
+ Gdk::Cairo::set_source_rgba(cr, mid_color);
+
+ cr->move_to(vis.get_x(), y + h);
+ cr->line_to(outline_x, y + h);
+ // Side outline
+ cr->line_to(outline_x, y - 1);
+
+ cr->stroke();
+ cr->restore();
+
+ std::vector<Gdk::Point> con_poly;
+ int con_drag_y = 0;
+ int con_drag_x = 0;
+ bool inside;
+ const SPFilterPrimitive* row_prim = (*row)[_columns.primitive];
+ const int inputs = input_count(row_prim);
+
+ if(SP_IS_FEMERGE(row_prim)) {
+ for(int i = 0; i < inputs; ++i) {
+ inside = do_connection_node(row, i, con_poly, mx, my);
+
+ cr->save();
+
+ Gdk::Cairo::set_source_rgba(cr, inside ? mid_color : fg_color);
+ draw_connection_node(cr, con_poly, inside);
+
+ cr->restore();
+
+ if(_in_drag == (i + 1)) {
+ con_drag_y = con_poly[2].get_y();
+ con_drag_x = con_poly[2].get_x();
+ }
+
+ if(_in_drag != (i + 1) || row_prim != prim) {
+ draw_connection(cr, row, 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();
+
+ cr->save();
+
+ Gdk::Cairo::set_source_rgba(cr, inside ? mid_color : fg_color);
+ draw_connection_node(cr, con_poly, inside);
+
+ cr->restore();
+
+ // Draw "in" connection
+ if(_in_drag != 1 || row_prim != prim) {
+ draw_connection(cr, row, 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();
+ }
+
+ cr->save();
+
+ Gdk::Cairo::set_source_rgba(cr, inside ? mid_color : fg_color);
+ draw_connection_node(cr, con_poly, inside);
+
+ cr->restore();
+
+ // Draw "in2" connection
+ if(_in_drag != 2 || row_prim != prim) {
+ draw_connection(cr, row, 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 = SP_IS_FEMERGE((SPFilterPrimitive*)(*input)[_columns.primitive]);
+ const bool use_default = !res && !is_merge;
+
+ if(res == input || (use_default && is_first)) {
+ // Draw straight connection to a standard input
+ // Draw a lighter line for an implicit connection to a standard input
+ const int tw = get_input_type_width();
+ gint end_x = text_start_x + tw * src_id + (int)(tw * 0.5f) + 1;
+
+ if(use_default && is_first) {
+ Gdk::Cairo::set_source_rgba(cr, mid_color);
+ } else {
+ Gdk::Cairo::set_source_rgba(cr, fg_color);
+ }
+
+ cr->rectangle(end_x-2, y1-2, 5, 5);
+ cr->fill_preserve();
+ cr->move_to(x1, y1);
+ cr->line_to(end_x, y1);
+ cr->stroke();
+ }
+ else {
+ // Draw an 'L'-shaped connection to another filter primitive
+ // If no connection is specified, draw a light connection to the previous primitive
+ if(use_default) {
+ res = input;
+ --res;
+ }
+
+ if(res) {
+ Gdk::Rectangle rct;
+
+ get_cell_area(get_model()->get_path(_model->children().begin()), *get_column(1), rct);
+ const int fheight = CellRendererConnection::size;
+
+ get_cell_area(get_model()->get_path(res), *get_column(1), rct);
+ const int row_index = find_index(res);
+ const int x2 = rct.get_x() + fheight * (row_count - row_index) - fheight / 2;
+ const int y2 = rct.get_y() + rct.get_height();
+
+ // Draw a bevelled 'L'-shaped connection
+ Gdk::Cairo::set_source_rgba(cr, fg_color);
+ cr->move_to(x1, y1);
+ cr->line_to(x2-fheight/4, y1);
+ cr->line_to(x2, y1-fheight/4);
+ cr->line_to(x2, y2);
+ cr->stroke();
+ }
+ }
+ cr->restore();
+}
+
+// Draw the triangular outline of the connection node, and fill it
+// if desired
+void FilterEffectsDialog::PrimitiveList::draw_connection_node(const Cairo::RefPtr<Cairo::Context>& cr,
+ const std::vector<Gdk::Point>& points,
+ const bool fill)
+{
+ cr->save();
+ cr->move_to(points[0].get_x()+0.5, points[0].get_y()+0.5);
+ cr->line_to(points[1].get_x()+0.5, points[1].get_y()+0.5);
+ cr->line_to(points[2].get_x()+0.5, points[2].get_y()+0.5);
+ cr->line_to(points[0].get_x()+0.5, points[0].get_y()+0.5);
+
+ if(fill) cr->fill();
+ else cr->stroke();
+
+ cr->restore();
+}
+
+// Creates a triangle outline of the connection node and returns true if (x,y) is inside the node
+bool FilterEffectsDialog::PrimitiveList::do_connection_node(const Gtk::TreeIter& row, const int input,
+ std::vector<Gdk::Point>& points,
+ const int ix, const int iy)
+{
+ Gdk::Rectangle rct;
+ const int icnt = input_count((*row)[_columns.primitive]);
+
+ get_cell_area(get_model()->get_path(_model->children().begin()), *get_column(1), rct);
+ const int fheight = CellRendererConnection::size;
+
+ get_cell_area(_model->get_path(row), *get_column(1), rct);
+ const float h = rct.get_height() / icnt;
+
+ const int x = rct.get_x() + fheight * (_model->children().size() - find_index(row));
+ const int con_w = (int)(fheight * 0.35f);
+ const int con_y = (int)(rct.get_y() + (h / 2) - con_w + (input * h));
+ points.clear();
+ points.emplace_back(x, con_y);
+ points.emplace_back(x, con_y + con_w * 2);
+ points.emplace_back(x - con_w, con_y + con_w);
+
+ return ix >= x - h && iy >= con_y && ix <= x && iy <= points[1].get_y();
+}
+
+const Gtk::TreeIter FilterEffectsDialog::PrimitiveList::find_result(const Gtk::TreeIter& start,
+ const SPAttr attr, int& src_id,
+ const int pos)
+{
+ SPFilterPrimitive* prim = (*start)[_columns.primitive];
+ Gtk::TreeIter target = _model->children().end();
+ int image = 0;
+
+ if(SP_IS_FEMERGE(prim)) {
+ int c = 0;
+ bool found = false;
+ for (auto& o: prim->children) {
+ if(c == pos && SP_IS_FEMERGENODE(&o)) {
+ image = SP_FEMERGENODE(&o)->input;
+ found = true;
+ }
+ ++c;
+ }
+ if(!found)
+ return target;
+ }
+ else {
+ if(attr == SPAttr::IN_)
+ image = prim->image_in;
+ else if(attr == SPAttr::IN2) {
+ if(SP_IS_FEBLEND(prim))
+ image = SP_FEBLEND(prim)->in2;
+ else if(SP_IS_FECOMPOSITE(prim))
+ image = SP_FECOMPOSITE(prim)->in2;
+ else if(SP_IS_FEDISPLACEMENTMAP(prim))
+ image = SP_FEDISPLACEMENTMAP(prim)->in2;
+ else
+ return target;
+ }
+ else
+ return target;
+ }
+
+ if(image >= 0) {
+ for(Gtk::TreeIter i = _model->children().begin();
+ i != start; ++i) {
+ if(((SPFilterPrimitive*)(*i)[_columns.primitive])->image_out == image)
+ target = i;
+ }
+ return target;
+ }
+ else if(image < -1) {
+ src_id = -(image + 2);
+ return start;
+ }
+
+ return target;
+}
+
+int FilterEffectsDialog::PrimitiveList::find_index(const Gtk::TreeIter& target)
+{
+ int i = 0;
+ for(Gtk::TreeIter iter = _model->children().begin();
+ iter != target; ++iter, ++i){};
+ return i;
+}
+
+bool FilterEffectsDialog::PrimitiveList::on_button_press_event(GdkEventButton* e)
+{
+ Gtk::TreePath path;
+ Gtk::TreeViewColumn* col;
+ const int x = (int)e->x, y = (int)e->y;
+ int cx, cy;
+
+ _drag_prim = nullptr;
+
+ if(get_path_at_pos(x, y, path, col, cx, cy)) {
+ Gtk::TreeIter iter = _model->get_iter(path);
+ std::vector<Gdk::Point> points;
+
+ _drag_prim = (*iter)[_columns.primitive];
+ const int icnt = input_count(_drag_prim);
+
+ for(int i = 0; i < icnt; ++i) {
+ if(do_connection_node(_model->get_iter(path), i, points, x, y)) {
+ _in_drag = i + 1;
+ break;
+ }
+ }
+
+ queue_draw();
+ }
+
+ if(_in_drag) {
+ _scroll_connection = Glib::signal_timeout().connect(sigc::mem_fun(*this, &PrimitiveList::on_scroll_timeout), 150);
+ _autoscroll_x = 0;
+ _autoscroll_y = 0;
+ get_selection()->select(path);
+ return true;
+ }
+ else
+ return Gtk::TreeView::on_button_press_event(e);
+}
+
+bool FilterEffectsDialog::PrimitiveList::on_motion_notify_event(GdkEventMotion* e)
+{
+ const int speed = 10;
+ const int limit = 15;
+
+ Gdk::Rectangle vis;
+ get_visible_rect(vis);
+ int vis_x, vis_y;
+
+ int vis_x2, vis_y2;
+ convert_widget_to_tree_coords(vis.get_x(), vis.get_y(), vis_x2, vis_y2);
+
+ convert_tree_to_widget_coords(vis.get_x(), vis.get_y(), vis_x, vis_y);
+ const int top = vis_y + vis.get_height();
+ const int right_edge = vis_x + vis.get_width();
+
+ // When autoscrolling during a connection drag, set the speed based on
+ // where the mouse is in relation to the edges.
+ if(e->y < vis_y)
+ _autoscroll_y = -(int)(speed + (vis_y - e->y) / 5);
+ else if(e->y < vis_y + limit)
+ _autoscroll_y = -speed;
+ else if(e->y > top)
+ _autoscroll_y = (int)(speed + (e->y - top) / 5);
+ else if(e->y > top - limit)
+ _autoscroll_y = speed;
+ else
+ _autoscroll_y = 0;
+
+ double e2 = ( e->x - vis_x2/2);
+ // horizontal scrolling
+ if(e2 < vis_x)
+ _autoscroll_x = -(int)(speed + (vis_x - e2) / 5);
+ else if(e2 < vis_x + limit)
+ _autoscroll_x = -speed;
+ else if(e2 > right_edge)
+ _autoscroll_x = (int)(speed + (e2 - right_edge) / 5);
+ else if(e2 > right_edge - limit)
+ _autoscroll_x = speed;
+ else
+ _autoscroll_x = 0;
+
+
+
+ queue_draw();
+
+ return Gtk::TreeView::on_motion_notify_event(e);
+}
+
+bool FilterEffectsDialog::PrimitiveList::on_button_release_event(GdkEventButton* e)
+{
+ SPFilterPrimitive *prim = get_selected(), *target;
+
+ _scroll_connection.disconnect();
+
+ if(_in_drag && prim) {
+ Gtk::TreePath path;
+ Gtk::TreeViewColumn* col;
+ int cx, cy;
+
+ if(get_path_at_pos((int)e->x, (int)e->y, path, col, cx, cy)) {
+ const gchar *in_val = nullptr;
+ Glib::ustring result;
+ Gtk::TreeIter target_iter = _model->get_iter(path);
+ target = (*target_iter)[_columns.primitive];
+ col = get_column(1);
+
+ Gdk::Rectangle rct;
+ get_cell_area(path, *col, rct);
+ const int twidth = get_input_type_width();
+ const int sources_x = rct.get_width() - twidth * FPInputConverter._length;
+ if(cx > sources_x) {
+ int src = (cx - sources_x) / twidth;
+ if (src < 0) {
+ src = 0;
+ } else if(src >= static_cast<int>(FPInputConverter._length)) {
+ src = FPInputConverter._length - 1;
+ }
+ result = FPInputConverter.get_key((FilterPrimitiveInput)src);
+ in_val = result.c_str();
+ }
+ else {
+ // Ensure that the target comes before the selected primitive
+ for(Gtk::TreeIter iter = _model->children().begin();
+ iter != get_selection()->get_selected(); ++iter) {
+ if(iter == target_iter) {
+ Inkscape::XML::Node *repr = target->getRepr();
+ // Make sure the target has a result
+ const gchar *gres = repr->attribute("result");
+ if(!gres) {
+ result = SP_FILTER(prim->parent)->get_new_result_name();
+ repr->setAttributeOrRemoveIfEmpty("result", result);
+ in_val = result.c_str();
+ }
+ else
+ in_val = gres;
+ break;
+ }
+ }
+ }
+
+ if(SP_IS_FEMERGE(prim)) {
+ int c = 1;
+ bool handled = false;
+ for (auto& o: prim->children) {
+ if(c == _in_drag && SP_IS_FEMERGENODE(&o)) {
+ // If input is null, delete it
+ if(!in_val) {
+
+ //XML Tree being used directly here while it shouldn't be.
+ sp_repr_unparent(o.getRepr());
+ DocumentUndo::done(prim->document, _("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);
+ SPFeMergeNode *node = SP_FEMERGENODE(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->image_in == result) {
+ prim->removeAttribute("in");
+ }
+
+ if (SP_IS_FEBLEND(prim)) {
+ if (SP_FEBLEND(prim)->in2 == result) {
+ prim->removeAttribute("in2");
+ }
+ } else if (SP_IS_FECOMPOSITE(prim)) {
+ if (SP_FECOMPOSITE(prim)->in2 == result) {
+ prim->removeAttribute("in2");
+ }
+ } else if (SP_IS_FEDISPLACEMENTMAP(prim)) {
+ if (SP_FEDISPLACEMENTMAP(prim)->in2 == result) {
+ prim->removeAttribute("in2");
+ }
+ }
+ }
+}
+
+// Remove any connections going to/from prim_iter that forward-reference other primitives
+void FilterEffectsDialog::PrimitiveList::sanitize_connections(const Gtk::TreeIter& prim_iter)
+{
+ SPFilterPrimitive *prim = (*prim_iter)[_columns.primitive];
+ bool before = true;
+
+ for(Gtk::TreeIter iter = _model->children().begin();
+ iter != _model->children().end(); ++iter) {
+ if(iter == prim_iter)
+ before = false;
+ else {
+ SPFilterPrimitive* cur_prim = (*iter)[_columns.primitive];
+ if(before)
+ check_single_connection(cur_prim, prim->image_out);
+ else
+ check_single_connection(prim, cur_prim->image_out);
+ }
+ }
+}
+
+// Reorder the filter primitives to match the list order
+void FilterEffectsDialog::PrimitiveList::on_drag_end(const Glib::RefPtr<Gdk::DragContext>& /*dc*/)
+{
+ SPFilter* filter = _dialog._filter_modifier.get_selected_filter();
+ int ndx = 0;
+
+ for (Gtk::TreeModel::iterator iter = _model->children().begin();
+ iter != _model->children().end(); ++iter, ++ndx) {
+ SPFilterPrimitive* prim = (*iter)[_columns.primitive];
+ if (prim && prim == _drag_prim) {
+ prim->getRepr()->setPosition(ndx);
+ break;
+ }
+ }
+
+ for (Gtk::TreeModel::iterator iter = _model->children().begin();
+ iter != _model->children().end(); ++iter, ++ndx) {
+ SPFilterPrimitive* prim = (*iter)[_columns.primitive];
+ if (prim && prim == _drag_prim) {
+ sanitize_connections(iter);
+ get_selection()->select(iter);
+ break;
+ }
+ }
+
+ filter->requestModified(SP_OBJECT_MODIFIED_FLAG);
+
+ DocumentUndo::done(filter->document, _("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;
+}
+
+/*** FilterEffectsDialog ***/
+
+FilterEffectsDialog::FilterEffectsDialog()
+ : DialogBase("/dialogs/filtereffects", "FilterEffects")
+ , _add_primitive_type(FPConverter)
+ , _add_primitive(_("Add Effect:"))
+ , _empty_settings(_("No effect selected"), Gtk::ALIGN_START)
+ , _no_filter_selected(_("No filter selected"), Gtk::ALIGN_START)
+ , _settings_initialized(false)
+ , _locked(false)
+ , _attr_lock(false)
+ , _filter_modifier(*this)
+ , _primitive_list(*this)
+ , _settings_tab1(Gtk::ORIENTATION_VERTICAL)
+ , _settings_tab2(Gtk::ORIENTATION_VERTICAL)
+{
+ _settings = new Settings(*this, _settings_tab1, sigc::mem_fun(*this, &FilterEffectsDialog::set_attr_direct),
+ NR_FILTER_ENDPRIMITIVETYPE);
+ _filter_general_settings = new Settings(*this, _settings_tab2, sigc::mem_fun(*this, &FilterEffectsDialog::set_filternode_attr),
+ 1);
+
+ // Initialize widget hierarchy
+ auto hpaned = Gtk::manage(new Gtk::Paned());
+ _primitive_box = Gtk::manage(new Gtk::Paned(Gtk::ORIENTATION_VERTICAL));
+
+ _sw_infobox = Gtk::manage(new Gtk::ScrolledWindow);
+ Gtk::ScrolledWindow* sw_prims = Gtk::manage(new Gtk::ScrolledWindow);
+ Gtk::Box* infobox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, /*spacing:*/4));
+ Gtk::Box* hb_prims = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
+ Gtk::Box* vb_prims = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
+ Gtk::Box* vb_desc = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
+
+ Gtk::Box* prim_vbox_p = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
+ Gtk::Box* prim_vbox_i = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
+
+ sw_prims->add(_primitive_list);
+
+ prim_vbox_p->pack_start(*sw_prims, true, true);
+ prim_vbox_i->pack_start(*vb_prims, true, true);
+
+ _primitive_box->pack1(*prim_vbox_p);
+ _primitive_box->pack2(*prim_vbox_i, false, false);
+
+ hpaned->pack1(_filter_modifier);
+ hpaned->pack2(*_primitive_box);
+ pack_start(*hpaned, true, true);
+
+ _infobox_icon.set_halign(Gtk::ALIGN_START);
+ _infobox_icon.set_valign(Gtk::ALIGN_START);
+ _infobox_desc.set_halign(Gtk::ALIGN_START);
+ _infobox_desc.set_valign(Gtk::ALIGN_START);
+ _infobox_desc.set_justify(Gtk::JUSTIFY_LEFT);
+ _infobox_desc.set_line_wrap(true);
+
+ vb_desc->pack_start(_infobox_desc, true, true);
+
+ infobox->pack_start(_infobox_icon, false, false);
+ infobox->pack_start(*vb_desc, true, true);
+
+ _sw_infobox->add(*infobox);
+
+ vb_prims->pack_start(*hb_prims, false, false);
+ vb_prims->pack_start(*_sw_infobox, true, true);
+
+ hb_prims->pack_start(_add_primitive, false, false);
+ hb_prims->pack_start(_add_primitive_type, true, true);
+ pack_start(_settings_tabs, false, false);
+ _settings_tabs.append_page(_settings_tab1, _("Effect parameters"));
+ _settings_tabs.append_page(_settings_tab2, _("Filter General Settings"));
+
+ _primitive_list.signal_primitive_changed().connect(
+ sigc::mem_fun(*this, &FilterEffectsDialog::update_settings_view));
+ _filter_modifier.signal_filter_changed().connect(
+ sigc::mem_fun(_primitive_list, &PrimitiveList::update));
+
+ _add_primitive_type.signal_changed().connect(
+ sigc::mem_fun(*this, &FilterEffectsDialog::update_primitive_infobox));
+
+ sw_prims->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ sw_prims->set_shadow_type(Gtk::SHADOW_IN);
+ _sw_infobox->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+
+// al_settings->set_padding(0, 0, 12, 0);
+// fr_settings->set_shadow_type(Gtk::SHADOW_NONE);
+// ((Gtk::Label*)fr_settings->get_label_widget())->set_use_markup();
+ _add_primitive.signal_clicked().connect(sigc::mem_fun(*this, &FilterEffectsDialog::add_primitive));
+ _primitive_list.set_menu(*this, sigc::mem_fun(*this, &FilterEffectsDialog::duplicate_primitive),
+ sigc::mem_fun(_primitive_list, &PrimitiveList::remove_selected));
+
+ init_settings_widgets();
+ _primitive_list.update();
+ update_primitive_infobox();
+
+ show();
+ show_all_children();
+ update();
+}
+
+FilterEffectsDialog::~FilterEffectsDialog()
+{
+ delete _settings;
+ delete _filter_general_settings;
+}
+
+void FilterEffectsDialog::documentReplaced()
+{
+ _resource_changed.disconnect();
+ if (auto document = getDocument()) {
+ _resource_changed = document->connectResourcesChanged("filter", sigc::mem_fun(_filter_modifier, &FilterModifier::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!
+
+ _settings_tab1.set_border_width(4);
+ _settings_tab2.set_border_width(4);
+
+ _empty_settings.set_sensitive(false);
+ _settings_tab1.pack_start(_empty_settings);
+
+ _no_filter_selected.set_sensitive(false);
+ _settings_tab2.pack_start(_no_filter_selected);
+ _settings_initialized = true;
+
+ _filter_general_settings->type(0);
+ 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, 5, 1, 1, 0, _("width of the convolve matrix"), _("height of the convolve matrix"));
+ _convolve_target = _settings->add_multispinbutton(/*default x:*/ (double) 0, /*default y:*/ (double) 0, SPAttr::TARGETX, SPAttr::TARGETY, _("Target:"), 0, 4, 1, 1, 0, _("X coordinate of the target point in the convolve matrix. The convolution is applied to pixels around this point."), _("Y coordinate of the target point in the convolve matrix. The convolution is applied to pixels around this point."));
+ //TRANSLATORS: for info on "Kernel", see http://en.wikipedia.org/wiki/Kernel_(matrix)
+ _convolve_matrix = _settings->add_matrix(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, 1, 0.01, 1, _("This value is added to each component. This is useful to define a constant value as the zero response of the filter."));
+ _settings->add_combo(CONVOLVEMATRIX_EDGEMODE_DUPLICATE, 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."));
+ _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, _("Flood 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, _("Standard Deviation:"), 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,_("X"),_("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,_("Y"),_("Y"));
+ _image_y->signal_attr_changed().connect(sigc::mem_fun(*this, &FilterEffectsDialog::image_y_changed));
+ _settings->type(NR_FILTER_OFFSET);
+ _settings->add_checkbutton(false, 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\"."));
+ _settings->add_dualspinscale(SPAttr::KERNELUNITLENGTH, _("Kernel Unit Length:"), 0.01, 10, 1, 0.01, 1);
+ _settings->add_lightsource();
+
+ _settings->type(NR_FILTER_TILE);
+ _settings->add_no_params();
+
+ _settings->type(NR_FILTER_TURBULENCE);
+// _settings->add_checkbutton(false, 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, _("Base Frequency:"), 0, 100, 0.1, 2, 2);
+ _settings->add_spinscale(1, SPAttr::NUMOCTAVES, _("Octaves:"), 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_primitive()
+{
+ SPFilter* filter = _filter_modifier.get_selected_filter();
+
+ if(filter) {
+ SPFilterPrimitive* prim = filter_add_primitive(filter, _add_primitive_type.get_active_data()->id);
+
+ _primitive_list.select(prim);
+
+ DocumentUndo::done(filter->document, _("Add filter primitive"), INKSCAPE_ICON("dialog-filters"));
+ }
+}
+
+void FilterEffectsDialog::update_primitive_infobox()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/options/showfiltersinfobox/value", true)){
+ _sw_infobox->show();
+ } else {
+ _sw_infobox->hide();
+ }
+ switch(_add_primitive_type.get_active_data()->id){
+ case(NR_FILTER_BLEND):
+ _infobox_icon.set_from_icon_name("feBlend-icon", Gtk::ICON_SIZE_DIALOG);
+ _infobox_desc.set_markup(_("Provides image blending modes, such as screen, multiply, darken and lighten."));
+ break;
+ case(NR_FILTER_COLORMATRIX):
+ _infobox_icon.set_from_icon_name("feColorMatrix-icon", Gtk::ICON_SIZE_DIALOG);
+ _infobox_desc.set_markup(_("Modifies pixel colors based on a transformation matrix. Useful for adjusting color hue and saturation."));
+ break;
+ case(NR_FILTER_COMPONENTTRANSFER):
+ _infobox_icon.set_from_icon_name("feComponentTransfer-icon", Gtk::ICON_SIZE_DIALOG);
+ _infobox_desc.set_markup(_("Manipulates color components according to particular transfer functions. Useful for brightness and contrast adjustment, color balance, and thresholding."));
+ break;
+ case(NR_FILTER_COMPOSITE):
+ _infobox_icon.set_from_icon_name("feComposite-icon", Gtk::ICON_SIZE_DIALOG);
+ _infobox_desc.set_markup(_("Composites two images using one of the Porter-Duff blending modes or the arithmetic mode described in SVG standard."));
+ break;
+ case(NR_FILTER_CONVOLVEMATRIX):
+ _infobox_icon.set_from_icon_name("feConvolveMatrix-icon", Gtk::ICON_SIZE_DIALOG);
+ _infobox_desc.set_markup(_("Performs a convolution on the input image enabling effects like blur, sharpening, embossing and edge detection."));
+ break;
+ case(NR_FILTER_DIFFUSELIGHTING):
+ _infobox_icon.set_from_icon_name("feDiffuseLighting-icon", Gtk::ICON_SIZE_DIALOG);
+ _infobox_desc.set_markup(_("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."));
+ break;
+ case(NR_FILTER_DISPLACEMENTMAP):
+ _infobox_icon.set_from_icon_name("feDisplacementMap-icon", Gtk::ICON_SIZE_DIALOG);
+ _infobox_desc.set_markup(_("Displaces pixels from the first input using the second as a map of displacement intensity. Classical examples are whirl and pinch effects."));
+ break;
+ case(NR_FILTER_FLOOD):
+ _infobox_icon.set_from_icon_name("feFlood-icon", Gtk::ICON_SIZE_DIALOG);
+ _infobox_desc.set_markup(_("Fills the region with a given color and opacity. Often used as input to other filters to apply color to a graphic."));
+ break;
+ case(NR_FILTER_GAUSSIANBLUR):
+ _infobox_icon.set_from_icon_name("feGaussianBlur-icon", Gtk::ICON_SIZE_DIALOG);
+ _infobox_desc.set_markup(_("Uniformly blurs its input. Commonly used together with Offset to create a drop shadow effect."));
+ break;
+ case(NR_FILTER_IMAGE):
+ _infobox_icon.set_from_icon_name("feImage-icon", Gtk::ICON_SIZE_DIALOG);
+ _infobox_desc.set_markup(_("Fills the region with graphics from an external file or from another portion of the document."));
+ break;
+ case(NR_FILTER_MERGE):
+ _infobox_icon.set_from_icon_name("feMerge-icon", Gtk::ICON_SIZE_DIALOG);
+ _infobox_desc.set_markup(_("Merges multiple inputs using normal alpha compositing. Equivalent to using several Blend primitives in 'normal' mode or several Composite primitives in 'over' mode."));
+ break;
+ case(NR_FILTER_MORPHOLOGY):
+ _infobox_icon.set_from_icon_name("feMorphology-icon", Gtk::ICON_SIZE_DIALOG);
+ _infobox_desc.set_markup(_("Provides erode and dilate effects. For single-color objects erode makes the object thinner and dilate makes it thicker."));
+ break;
+ case(NR_FILTER_OFFSET):
+ _infobox_icon.set_from_icon_name("feOffset-icon", Gtk::ICON_SIZE_DIALOG);
+ _infobox_desc.set_markup(_("Offsets the input by an user-defined amount. Commonly used for drop shadow effects."));
+ break;
+ case(NR_FILTER_SPECULARLIGHTING):
+ _infobox_icon.set_from_icon_name("feSpecularLighting-icon", Gtk::ICON_SIZE_DIALOG);
+ _infobox_desc.set_markup(_("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."));
+ break;
+ case(NR_FILTER_TILE):
+ _infobox_icon.set_from_icon_name("feTile-icon", Gtk::ICON_SIZE_DIALOG);
+ _infobox_desc.set_markup(_("Tiles a region with an input graphic. The source tile is defined by the filter primitive subregion of the input."));
+ break;
+ case(NR_FILTER_TURBULENCE):
+ _infobox_icon.set_from_icon_name("feTurbulence-icon", Gtk::ICON_SIZE_DIALOG);
+ _infobox_desc.set_markup(_("Renders Perlin noise, which is useful to generate textures such as clouds, fire, smoke, marble or granite."));
+ break;
+ default:
+ g_assert(false);
+ break;
+ }
+ //_infobox_icon.set_pixel_size(96);
+ _infobox_icon.set_pixel_size(64);
+}
+
+void FilterEffectsDialog::duplicate_primitive()
+{
+ SPFilter* filter = _filter_modifier.get_selected_filter();
+ SPFilterPrimitive* origprim = _primitive_list.get_selected();
+
+ if (filter && origprim) {
+ Inkscape::XML::Node *repr;
+ repr = origprim->getRepr()->duplicate(origprim->getRepr()->document());
+ filter->getRepr()->appendChild(repr);
+
+ DocumentUndo::done(filter->document, _("Duplicate filter primitive"), INKSCAPE_ICON("dialog-filters"));
+
+ _primitive_list.update();
+ }
+}
+
+void FilterEffectsDialog::convolve_order_changed()
+{
+ _convolve_matrix->set_from_attribute(_primitive_list.get_selected());
+ _convolve_target->get_spinbuttons()[0]->get_adjustment()->set_upper(_convolve_order->get_spinbutton1().get_value() - 1);
+ _convolve_target->get_spinbuttons()[1]->get_adjustment()->set_upper(_convolve_order->get_spinbutton2().get_value() - 1);
+}
+
+bool number_or_empy(const Glib::ustring& text) {
+ if (text.empty()) {
+ return true;
+ }
+ double n = 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_tab2.get_children();
+ vect[0]->hide();
+ _no_filter_selected.show();
+ }
+
+ _attr_lock = false;
+ }
+}
+
+void FilterEffectsDialog::update_settings_view()
+{
+ update_settings_sensitivity();
+
+ if(_attr_lock)
+ return;
+
+//First Tab
+
+ std::vector<Gtk::Widget*> vect1 = _settings_tab1.get_children();
+ for(auto & i : vect1)
+ i->hide();
+ _empty_settings.show();
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/options/showfiltersinfobox/value", true)){
+ _sw_infobox->show();
+ } else {
+ _sw_infobox->hide();
+ }
+
+ SPFilterPrimitive* prim = _primitive_list.get_selected();
+
+ if(prim && prim->getRepr()) {
+
+ //XML Tree being used directly here while it shouldn't be.
+ _settings->show_and_update(FPConverter.get_id_from_key(prim->getRepr()->name()), prim);
+ _empty_settings.hide();
+ }
+
+//Second Tab
+
+ std::vector<Gtk::Widget*> vect2 = _settings_tab2.get_children();
+ vect2[0]->hide();
+ _no_filter_selected.show();
+
+ SPFilter* filter = _filter_modifier.get_selected_filter();
+
+ if(filter) {
+ _filter_general_settings->show_and_update(0, filter);
+ _no_filter_selected.hide();
+ }
+
+}
+
+void FilterEffectsDialog::update_settings_sensitivity()
+{
+ SPFilterPrimitive* prim = _primitive_list.get_selected();
+ const bool use_k = SP_IS_FECOMPOSITE(prim) && SP_FECOMPOSITE(prim)->composite_operator == COMPOSITE_ARITHMETIC;
+ _k1->set_sensitive(use_k);
+ _k2->set_sensitive(use_k);
+ _k3->set_sensitive(use_k);
+ _k4->set_sensitive(use_k);
+
+}
+
+void FilterEffectsDialog::update_color_matrix()
+{
+ _color_matrix_values->set_from_attribute(_primitive_list.get_selected());
+}
+
+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..3970c43
--- /dev/null
+++ b/src/ui/dialog/filter-effects-dialog.h
@@ -0,0 +1,333 @@
+// 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 <gtkmm/notebook.h>
+#include <gtkmm/paned.h>
+#include <gtkmm/scrolledwindow.h>
+#include <memory>
+
+#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/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;
+
+ static FilterEffectsDialog &getInstance() { return *new FilterEffectsDialog(); }
+
+ 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&);
+
+ void update_filters();
+ void update_selection(Selection *);
+
+ SPFilter* get_selected_filter();
+ void select_filter(const SPFilter*);
+
+ sigc::signal<void>& signal_filter_changed()
+ {
+ return _signal_filter_changed;
+ }
+
+ private:
+ class Columns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ Columns()
+ {
+ add(filter);
+ add(label);
+ add(sel);
+ add(count);
+ }
+
+ Gtk::TreeModelColumn<SPFilter*> filter;
+ Gtk::TreeModelColumn<Glib::ustring> label;
+ Gtk::TreeModelColumn<int> sel;
+ Gtk::TreeModelColumn<int> count;
+ };
+
+ void on_filter_selection_changed();
+ void on_name_edited(const Glib::ustring&, const Glib::ustring&);
+ bool on_filter_move(const Glib::RefPtr<Gdk::DragContext>& /*context*/, int x, int y, guint /*time*/);
+ void on_selection_toggled(const Glib::ustring&);
+
+ void update_counts();
+ void filter_list_button_release(GdkEventButton*);
+ void add_filter();
+ void remove_filter();
+ void duplicate_filter();
+ void rename_filter();
+ void select_filter_elements();
+
+ FilterEffectsDialog& _dialog;
+ Gtk::TreeView _list;
+ Glib::RefPtr<Gtk::ListStore> _model;
+ Columns _columns;
+ Gtk::CellRendererToggle _cell_toggle;
+ Gtk::Button _add;
+ Gtk::Menu *_menu;
+ sigc::signal<void> _signal_filter_changed;
+ std::unique_ptr<Inkscape::XML::SignalObserver> _observer;
+ };
+
+ class PrimitiveColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ PrimitiveColumns()
+ {
+ add(primitive);
+ add(type_id);
+ add(type);
+ add(id);
+ }
+
+ Gtk::TreeModelColumn<SPFilterPrimitive*> primitive;
+ Gtk::TreeModelColumn<Inkscape::Filters::FilterPrimitiveType> type_id;
+ Gtk::TreeModelColumn<Glib::ustring> type;
+ Gtk::TreeModelColumn<Glib::ustring> id;
+ };
+
+ class CellRendererConnection : public Gtk::CellRenderer
+ {
+ public:
+ CellRendererConnection();
+ Glib::PropertyProxy<void*> property_primitive();
+
+ static const int size = 24;
+
+ protected:
+ void get_preferred_width_vfunc(Gtk::Widget& widget,
+ int& minimum_width,
+ int& natural_width) const override;
+
+ void get_preferred_width_for_height_vfunc(Gtk::Widget& widget,
+ int height,
+ int& minimum_width,
+ int& natural_width) const override;
+
+ void get_preferred_height_vfunc(Gtk::Widget& widget,
+ int& minimum_height,
+ int& natural_height) const override;
+
+ void get_preferred_height_for_width_vfunc(Gtk::Widget& widget,
+ int width,
+ int& minimum_height,
+ int& natural_height) const override;
+ private:
+ // void* should be SPFilterPrimitive*, some weirdness with properties prevents this
+ Glib::Property<void*> _primitive;
+ };
+
+ class PrimitiveList : public Gtk::TreeView
+ {
+ public:
+ PrimitiveList(FilterEffectsDialog&);
+
+ sigc::signal<void>& signal_primitive_changed();
+
+ void update();
+ void set_menu(Gtk::Widget &parent,
+ sigc::slot<void> dup,
+ sigc::slot<void> rem);
+
+ SPFilterPrimitive* get_selected();
+ void select(SPFilterPrimitive *prim);
+ void remove_selected();
+
+ int primitive_count() const;
+ int get_input_type_width() const;
+
+ protected:
+ bool on_draw_signal(const Cairo::RefPtr<Cairo::Context> &cr);
+
+
+ bool on_button_press_event(GdkEventButton*) override;
+ bool on_motion_notify_event(GdkEventMotion*) override;
+ bool on_button_release_event(GdkEventButton*) override;
+ void on_drag_end(const Glib::RefPtr<Gdk::DragContext>&) override;
+ private:
+ void init_text();
+
+ void draw_connection_node(const Cairo::RefPtr<Cairo::Context>& cr,
+ const std::vector<Gdk::Point>& points,
+ const bool fill);
+
+ bool do_connection_node(const Gtk::TreeIter& row, const int input, std::vector<Gdk::Point>& points,
+ const int ix, const int iy);
+
+ const Gtk::TreeIter find_result(const Gtk::TreeIter& start, const 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;
+ };
+
+ void init_settings_widgets();
+
+ // Handlers
+ void add_primitive();
+ void remove_primitive();
+ void duplicate_primitive();
+ void convolve_order_changed();
+ void image_x_changed();
+ void image_y_changed();
+
+ void set_attr_direct(const UI::Widget::AttrWidget*);
+ void set_child_attr_direct(const UI::Widget::AttrWidget*);
+ void set_filternode_attr(const UI::Widget::AttrWidget*);
+ void set_attr(SPObject*, const 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 update_primitive_infobox();
+
+ // Primitives Info Box
+ Gtk::Label _infobox_desc;
+ Gtk::Image _infobox_icon;
+ Gtk::ScrolledWindow* _sw_infobox;
+
+ // View/add primitives
+ Gtk::Paned* _primitive_box;
+
+ UI::Widget::ComboBoxEnum<Inkscape::Filters::FilterPrimitiveType> _add_primitive_type;
+ Gtk::Button _add_primitive;
+
+ // Bottom pane (filter effect primitive settings)
+ Gtk::Notebook _settings_tabs;
+ Gtk::Box _settings_tab2;
+ Gtk::Box _settings_tab1;
+ Gtk::Label _empty_settings;
+ Gtk::Label _no_filter_selected;
+ bool _settings_initialized;
+
+ class Settings;
+ class MatrixAttr;
+ class ColorMatrixValues;
+ class ComponentTransferValues;
+ class LightSourceControl;
+ Settings* _settings;
+ Settings* _filter_general_settings;
+
+ // 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;
+
+ FilterEffectsDialog(FilterEffectsDialog const &d);
+ FilterEffectsDialog& operator=(FilterEffectsDialog const &d);
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_FILTER_EFFECTS_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/find.cpp b/src/ui/dialog/find.cpp
new file mode 100644
index 0000000..a2bc355
--- /dev/null
+++ b/src/ui/dialog/find.cpp
@@ -0,0 +1,1132 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ * Johan Engelen <goejendaagh@zonnet.nl>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2004-2006 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "find.h"
+
+#include <gtkmm/entry.h>
+#include <glibmm/i18n.h>
+#include <glibmm/regex.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "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)
+
+{
+ 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() == nullptr) {
+ return false;
+ }
+
+ if (dynamic_cast<SPString *>(item)) { // SPStrings have "on demand" ids which are useless for searching
+ return false;
+ }
+
+ const gchar *item_id = item->getRepr()->attribute("id");
+ if (item_id == nullptr) {
+ return false;
+ }
+
+ bool found = find_strcmp(item_id, id, exact, casematch);
+
+ if (found && replace) {
+ gchar * replace_text = g_strdup(entry_replace.getEntry()->get_text().c_str());
+ Glib::ustring new_item_style = find_replace(item_id, id, replace_text , exact, casematch, true);
+ if (new_item_style != item_id) {
+ item->setAttribute("id", new_item_style);
+ }
+ g_free(replace_text);
+ }
+
+ return found;
+}
+
+bool Find::item_style_match (SPItem *item, const gchar *text, bool exact, bool casematch, bool replace/*=false*/)
+{
+ if (item->getRepr() == nullptr) {
+ return false;
+ }
+
+ gchar *item_style = g_strdup(item->getRepr()->attribute("style"));
+ if (item_style == nullptr) {
+ return false;
+ }
+
+ bool found = find_strcmp(item_style, text, exact, casematch);
+
+ if (found && replace) {
+ gchar * replace_text = g_strdup(entry_replace.getEntry()->get_text().c_str());
+ Glib::ustring new_item_style = find_replace(item_style, text, replace_text , exact, casematch, true);
+ if (new_item_style != item_style) {
+ item->setAttribute("style", new_item_style);
+ }
+ g_free(replace_text);
+ }
+
+ g_free(item_style);
+ return found;
+}
+
+bool Find::item_attr_match(SPItem *item, const gchar *text, bool exact, bool /*casematch*/, bool replace/*=false*/)
+{
+ bool found = false;
+
+ if (item->getRepr() == nullptr) {
+ return false;
+ }
+
+ gchar *attr_value = g_strdup(item->getRepr()->attribute(text));
+ if (exact) {
+ found = (attr_value != nullptr);
+ } else {
+ found = item->getRepr()->matchAttributeName(text);
+ }
+ g_free(attr_value);
+
+ // TODO - Rename attribute name ?
+ if (found && replace) {
+ found = false;
+ }
+
+ return found;
+}
+
+bool Find::item_attrvalue_match(SPItem *item, const gchar *text, bool exact, bool casematch, bool replace/*=false*/)
+{
+ bool ret = false;
+
+ if (item->getRepr() == nullptr) {
+ return false;
+ }
+
+ 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;
+ SPItem *item = dynamic_cast<SPItem *>(obj);
+ g_assert(item != nullptr);
+ if (item_text_match(item, text, exact, casematch)) {
+ if (out.end()==find(out.begin(),out.end(), *i)) {
+ out.push_back(*i);
+ if (_action_replace) {
+ item_text_match(item, text, exact, casematch, _action_replace);
+ }
+ }
+ }
+ }
+ }
+ else if (check_searchin_property.get_active()) {
+
+ bool ids = check_ids.get_active();
+ bool style = check_style.get_active();
+ bool font = check_font.get_active();
+ bool 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;
+ SPItem *item = dynamic_cast<SPItem *>(obj);
+ if (item_id_match(item, text, exact, casematch)) {
+ if (out.end()==find(out.begin(),out.end(), *i)) {
+ out.push_back(*i);
+ if (_action_replace) {
+ item_id_match(item, text, exact, casematch, _action_replace);
+ }
+ }
+ }
+ }
+ }
+
+
+ if (style) {
+ for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) {
+ SPObject *obj = *i;
+ SPItem *item = dynamic_cast<SPItem *>(obj);
+ g_assert(item != nullptr);
+ if (item_style_match(item, text, exact, casematch)) {
+ if (out.end()==find(out.begin(),out.end(), *i)){
+ out.push_back(*i);
+ if (_action_replace) {
+ item_style_match(item, text, exact, casematch, _action_replace);
+ }
+ }
+ }
+ }
+ }
+
+
+ if (attrname) {
+ for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) {
+ SPObject *obj = *i;
+ SPItem *item = dynamic_cast<SPItem *>(obj);
+ g_assert(item != nullptr);
+ if (item_attr_match(item, text, exact, casematch)) {
+ if (out.end()==find(out.begin(),out.end(), *i)) {
+ out.push_back(*i);
+ if (_action_replace) {
+ item_attr_match(item, text, exact, casematch, _action_replace);
+ }
+ }
+ }
+ }
+ }
+
+
+ if (attrvalue) {
+ for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) {
+ SPObject *obj = *i;
+ SPItem *item = dynamic_cast<SPItem *>(obj);
+ g_assert(item != nullptr);
+ if (item_attrvalue_match(item, text, exact, casematch)) {
+ if (out.end()==find(out.begin(),out.end(), *i)) {
+ out.push_back(*i);
+ if (_action_replace) {
+ item_attrvalue_match(item, text, exact, casematch, _action_replace);
+ }
+ }
+ }
+ }
+ }
+
+
+ if (font) {
+ for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) {
+ SPObject *obj = *i;
+ SPItem *item = dynamic_cast<SPItem *>(obj);
+ g_assert(item != nullptr);
+ if (item_font_match(item, text, exact, casematch)) {
+ if (out.end()==find(out.begin(),out.end(),*i)) {
+ out.push_back(*i);
+ if (_action_replace) {
+ item_font_match(item, text, exact, casematch, _action_replace);
+ }
+ }
+ }
+ }
+ }
+ if (desc) {
+ for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) {
+ SPObject *obj = *i;
+ SPItem *item = dynamic_cast<SPItem *>(obj);
+ g_assert(item != nullptr);
+ if (item_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;
+ SPItem *item = dynamic_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 ( dynamic_cast<SPRect *>(item)) {
+ return ( all ||check_rects.get_active());
+
+ } else if (dynamic_cast<SPGenericEllipse *>(item)) {
+ return ( all || check_ellipses.get_active());
+
+ } else if (dynamic_cast<SPStar *>(item) || dynamic_cast<SPPolygon *>(item)) {
+ return ( all || check_stars.get_active());
+
+ } else if (dynamic_cast<SPSpiral *>(item)) {
+ return ( all || check_spirals.get_active());
+
+ } else if (dynamic_cast<SPPath *>(item) || dynamic_cast<SPLine *>(item) || dynamic_cast<SPPolyLine *>(item)) {
+ return (all || check_paths.get_active());
+
+ } else if (dynamic_cast<SPText *>(item) || dynamic_cast<SPTSpan *>(item) ||
+ dynamic_cast<SPTRef *>(item) || dynamic_cast<SPString *>(item) ||
+ dynamic_cast<SPFlowtext *>(item) || dynamic_cast<SPFlowdiv *>(item) ||
+ dynamic_cast<SPFlowtspan *>(item) || dynamic_cast<SPFlowpara *>(item)) {
+ return (all || check_texts.get_active());
+
+ } else if (dynamic_cast<SPGroup *>(item) &&
+ !getDesktop()->layerManager().isLayer(item)) { // never select layers!
+ return (all || check_groups.get_active());
+
+ } else if (dynamic_cast<SPUse *>(item)) {
+ return (all || check_clones.get_active());
+
+ } else if (dynamic_cast<SPImage *>(item)) {
+ return (all || check_images.get_active());
+
+ } else if (dynamic_cast<SPOffset *>(item)) {
+ return (all || check_offsets.get_active());
+ }
+
+ return false;
+}
+
+std::vector<SPItem*> Find::filter_types (std::vector<SPItem*> &l)
+{
+ std::vector<SPItem*> n;
+ for (std::vector<SPItem*>::const_reverse_iterator i=l.rbegin(); l.rend() != i; ++i) {
+ SPObject *obj = *i;
+ SPItem *item = dynamic_cast<SPItem *>(obj);
+ g_assert(item != nullptr);
+ if (item_type_match(item)) {
+ n.push_back(*i);
+ }
+ }
+ return n;
+}
+
+
+std::vector<SPItem*> &Find::filter_list (std::vector<SPItem*> &l, bool exact, bool casematch)
+{
+ l = filter_types (l);
+ l = filter_fields (l, exact, casematch);
+ return l;
+}
+
+std::vector<SPItem*> &Find::all_items (SPObject *r, std::vector<SPItem*> &l, bool hidden, bool locked)
+{
+ if (dynamic_cast<SPDefs *>(r)) {
+ return l; // we're not interested in items in defs
+ }
+
+ if (!strcmp(r->getRepr()->name(), "svg:metadata")) {
+ return l; // we're not interested in metadata
+ }
+
+ auto desktop = getDesktop();
+ for (auto& child: r->children) {
+ SPItem *item = dynamic_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;
+ SPItem *item = dynamic_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->selection, l, desktop->layerManager().currentLayer(), hidden, locked);
+ } else {
+ l = all_selection_items (desktop->selection, l, nullptr, hidden, locked);
+ }
+ } else {
+ if (check_scope_layer.get_active()) {
+ l = all_items (desktop->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];
+ SPItem *item = dynamic_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..14ee6b8
--- /dev/null
+++ b/src/ui/dialog/find.h
@@ -0,0 +1,320 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Find dialog
+ */
+/* Authors:
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ *
+ * Copyright (C) 2004, 2005 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_FIND_H
+#define INKSCAPE_UI_DIALOG_FIND_H
+
+#include <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;
+ /**
+ * Helper function which returns a new instance of the dialog.
+ * getInstance is needed by the dialog manager (Inkscape::UI::Dialog::DialogManager).
+ */
+ static Find &getInstance() { return *new Find(); }
+
+protected:
+
+
+ /**
+ * Callbacks for pressing the dialog buttons.
+ */
+ void onFind();
+ void onReplace();
+ void onExpander();
+ void onAction();
+ void onToggleAlltypes();
+ void onToggleCheck();
+ void onSearchinText();
+ void onSearchinProperty();
+
+ /**
+ * Toggle all the properties checkboxes
+ */
+ void searchinToggle(bool on);
+
+ /**
+ * Returns true if the SPItem 'item' has the same id
+ *
+ * @param item the SPItem to check
+ * @param id the value to compare with
+ * @param exact do an exact match
+ * @param casematch match the text case exactly
+ * @param replace replace the value if found
+ *
+ */
+ bool item_id_match (SPItem *item, const gchar *id, bool exact, bool casematch, bool replace=false);
+ /**
+ * Returns true if the SPItem 'item' has the same text content
+ *
+ * @param item the SPItem to check
+ * @param name the value to compare with
+ * @param exact do an exact match
+ * @param casematch match the text case exactly
+ * @param replace replace the value if found
+ *
+ *
+ */
+ bool item_text_match (SPItem *item, const gchar *text, bool exact, bool casematch, bool replace=false);
+ /**
+ * Returns true if the SPItem 'item' has the same text in the style attribute
+ *
+ * @param item the SPItem to check
+ * @param name the value to compare with
+ * @param exact do an exact match
+ * @param casematch match the text case exactly
+ * @param replace replace the value if found
+ *
+ */
+ bool item_style_match (SPItem *item, const gchar *text, bool exact, bool casematch, bool replace=false);
+ /**
+ * Returns true if 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(Find const &d) = delete;
+ Find& operator=(Find const &d) = delete;
+
+ /*
+ * Find and replace combo box widgets
+ */
+ UI::Widget::Entry entry_find;
+ UI::Widget::Entry entry_replace;
+
+ /**
+ * Scope and search in widgets
+ */
+ Gtk::RadioButton check_scope_all;
+ Gtk::RadioButton check_scope_layer;
+ Gtk::RadioButton check_scope_selection;
+ Gtk::RadioButton check_searchin_text;
+ Gtk::RadioButton check_searchin_property;
+ Gtk::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-substitution.cpp b/src/ui/dialog/font-substitution.cpp
new file mode 100644
index 0000000..bf91259
--- /dev/null
+++ b/src/ui/dialog/font-substitution.cpp
@@ -0,0 +1,271 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ *
+ * Copyright (C) 2012 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <set>
+
+#include <glibmm/i18n.h>
+#include <glibmm/regex.h>
+
+#include <gtkmm/messagedialog.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/textview.h>
+
+#include "font-substitution.h"
+
+#include "desktop.h"
+#include "document.h"
+#include "inkscape.h"
+#include "selection-chemistry.h"
+#include "text-editing.h"
+
+#include "object/sp-root.h"
+#include "object/sp-text.h"
+#include "object/sp-textpath.h"
+#include "object/sp-flowtext.h"
+#include "object/sp-flowdiv.h"
+#include "object/sp-tspan.h"
+
+#include "libnrtype/FontFactory.h"
+#include "libnrtype/font-instance.h"
+
+#include "ui/dialog-events.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+FontSubstitution::FontSubstitution()
+= default;
+
+FontSubstitution::~FontSubstitution()
+= default;
+
+void
+FontSubstitution::checkFontSubstitutions(SPDocument* doc)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int show_dlg = prefs->getInt("/options/font/substitutedlg", 0);
+ if (show_dlg) {
+ Glib::ustring out;
+ std::vector<SPItem*> l = getFontReplacedItems(doc, &out);
+ if (out.length() > 0) {
+ show(out, l);
+ }
+ }
+}
+
+void
+FontSubstitution::show(Glib::ustring out, std::vector<SPItem*> &l)
+{
+ Gtk::MessageDialog warning(_("\nSome fonts are not available and have been substituted."),
+ false, Gtk::MESSAGE_INFO, Gtk::BUTTONS_OK, true);
+ warning.set_resizable(true);
+ warning.set_title(_("Font substitution"));
+
+ GtkWidget *dlg = GTK_WIDGET(warning.gobj());
+ sp_transientize(dlg);
+
+ Gtk::TextView * textview = new Gtk::TextView();
+ textview->set_editable(false);
+ textview->set_wrap_mode(Gtk::WRAP_WORD);
+ textview->show();
+ textview->get_buffer()->set_text(_(out.c_str()));
+
+ Gtk::ScrolledWindow * scrollwindow = new Gtk::ScrolledWindow();
+ scrollwindow->add(*textview);
+ scrollwindow->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ scrollwindow->set_shadow_type(Gtk::SHADOW_IN);
+ scrollwindow->set_size_request(0, 100);
+ scrollwindow->show();
+
+ Gtk::CheckButton *cbSelect = new Gtk::CheckButton();
+ cbSelect->set_label(_("Select all the affected items"));
+ cbSelect->set_active(true);
+ cbSelect->show();
+
+ Gtk::CheckButton *cbWarning = new Gtk::CheckButton();
+ cbWarning->set_label(_("Don't show this warning again"));
+ cbWarning->show();
+
+ auto box = warning.get_content_area();
+ box->set_spacing(2);
+ box->pack_start(*scrollwindow, true, true, 4);
+ box->pack_start(*cbSelect, false, false, 0);
+ box->pack_start(*cbWarning, false, false, 0);
+
+ warning.run();
+
+ if (cbWarning->get_active()) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setInt("/options/font/substitutedlg", 0);
+ }
+
+ if (cbSelect->get_active()) {
+
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ Inkscape::Selection *selection = desktop->getSelection();
+ selection->clear();
+ selection->setList(l);
+ }
+
+}
+
+/*
+ * Find all the fonts that are in the document but not available on the users system
+ * and have been substituted for other fonts
+ *
+ * Return a list of SPItems where fonts have been substituted.
+ *
+ * Walk through all the objects ...
+ * a. Build up a list of the objects with fonts defined in the style attribute
+ * b. Build up a list of the objects rendered fonts - taken for the objects layout/spans
+ * If there are fonts in a. that are not in b. then those fonts have been substituted.
+ */
+std::vector<SPItem*> FontSubstitution::getFontReplacedItems(SPDocument* doc, Glib::ustring *out)
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ std::vector<SPItem*> allList;
+ std::vector<SPItem*> outList,x,y;
+ std::set<Glib::ustring> setErrors;
+ std::set<Glib::ustring> setFontSpans;
+ std::map<SPItem *, Glib::ustring> mapFontStyles;
+
+ allList = get_all_items(x, doc->getRoot(), desktop, false, false, true, y);
+ for(auto item : allList){
+ SPStyle *style = item->style;
+ Glib::ustring family = "";
+
+ if (is_top_level_text_object (item)) {
+ // Should only need to check the first span, since the others should be covered by TSPAN's etc
+ family = te_get_layout(item)->getFontFamily(0);
+ setFontSpans.insert(family);
+ }
+ else if (SP_IS_TEXTPATH(item)) {
+ SPTextPath const *textpath = SP_TEXTPATH(item);
+ if (textpath->originalPath != nullptr) {
+ family = SP_TEXT(item->parent)->layout.getFontFamily(0);
+ setFontSpans.insert(family);
+ }
+ }
+ else if (SP_IS_TSPAN(item) || SP_IS_FLOWTSPAN(item)) {
+ // is_part_of_text_subtree (item)
+ // TSPAN layout comes from the parent->layout->_spans
+ SPObject *parent_text = item;
+ while (parent_text && !SP_IS_TEXT(parent_text)) {
+ parent_text = parent_text->parent;
+ }
+ if (parent_text != nullptr) {
+ family = SP_TEXT(parent_text)->layout.getFontFamily(0);
+ // Add all the spans fonts to the set
+ for (unsigned int f=0; f < parent_text->children.size(); f++) {
+ family = SP_TEXT(parent_text)->layout.getFontFamily(f);
+ setFontSpans.insert(family);
+ }
+ }
+ }
+
+ if (style) {
+ gchar const *style_font = nullptr;
+ if (style->font_family.set)
+ style_font = style->font_family.value();
+ else if (style->font_specification.set)
+ style_font = style->font_specification.value();
+ else
+ style_font = style->font_family.value();
+
+ if (style_font) {
+ if (has_visible_text(item)) {
+ mapFontStyles.insert(std::make_pair (item, style_font));
+ }
+ }
+ }
+ }
+
+ // Check if any document styles are not in the actual layout
+ std::map<SPItem *, Glib::ustring>::const_reverse_iterator mapIter;
+ for (mapIter = mapFontStyles.rbegin(); mapIter != mapFontStyles.rend(); ++mapIter) {
+ SPItem *item = mapIter->first;
+ Glib::ustring fonts = mapIter->second;
+
+ // CSS font fallbacks can have more that one font listed, split the font list
+ std::vector<Glib::ustring> vFonts = Glib::Regex::split_simple("," , fonts);
+ bool fontFound = false;
+ for(auto font : vFonts) {
+ // trim whitespace
+ size_t startpos = font.find_first_not_of(" \n\r\t");
+ size_t endpos = font.find_last_not_of(" \n\r\t");
+ if(( std::string::npos == startpos ) || ( std::string::npos == endpos)) {
+ continue; // empty font name
+ }
+ font = font.substr( startpos, endpos-startpos+1 );
+ std::set<Glib::ustring>::const_iterator iter = setFontSpans.find(font);
+ if (iter != setFontSpans.end() ||
+ font == Glib::ustring("sans-serif") ||
+ font == Glib::ustring("Sans") ||
+ font == Glib::ustring("serif") ||
+ font == Glib::ustring("Serif") ||
+ font == Glib::ustring("monospace") ||
+ font == Glib::ustring("Monospace")) {
+ fontFound = true;
+ break;
+ }
+ }
+ if (fontFound == false) {
+ Glib::ustring subName = getSubstituteFontName(fonts);
+ Glib::ustring err = Glib::ustring::compose(
+ _("Font '%1' substituted with '%2'"), fonts.c_str(), subName.c_str());
+ setErrors.insert(err);
+ outList.push_back(item);
+ }
+ }
+
+ std::set<Glib::ustring>::const_iterator setIter;
+ for (setIter = setErrors.begin(); setIter != setErrors.end(); ++setIter) {
+ Glib::ustring err = (*setIter);
+ out->append(err + "\n");
+ g_warning("%s", err.c_str());
+ }
+
+ return outList;
+}
+
+
+Glib::ustring FontSubstitution::getSubstituteFontName (Glib::ustring font)
+{
+ Glib::ustring out = font;
+
+ PangoFontDescription *descr = pango_font_description_new();
+ pango_font_description_set_family(descr,font.c_str());
+ font_instance *res = (font_factory::Default())->Face(descr);
+ if (res->pFont) {
+ PangoFontDescription *nFaceDesc = pango_font_describe(res->pFont);
+ out = sp_font_description_get_family(nFaceDesc);
+ }
+ pango_font_description_free(descr);
+
+ return out;
+}
+
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/font-substitution.h b/src/ui/dialog/font-substitution.h
new file mode 100644
index 0000000..6ab01d3
--- /dev/null
+++ b/src/ui/dialog/font-substitution.h
@@ -0,0 +1,59 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief FontSubstitution dialog
+ */
+/* Authors:
+ *
+ *
+ * Copyright (C) 2012 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_FONT_SUBSTITUTION_H
+#define INKSCAPE_UI_FONT_SUBSTITUTION_H
+
+#include <glibmm/ustring.h>
+#include <vector>
+
+class SPItem;
+class SPDocument;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class FontSubstitution {
+public:
+ FontSubstitution();
+ virtual ~FontSubstitution();
+ void checkFontSubstitutions(SPDocument* doc);
+ void show(Glib::ustring out, std::vector<SPItem*> &l);
+
+ static FontSubstitution &getInstance() { return *new FontSubstitution(); }
+ Glib::ustring getSubstituteFontName (Glib::ustring font);
+
+protected:
+ std::vector<SPItem*> getFontReplacedItems(SPDocument* doc, Glib::ustring *out);
+
+private:
+ FontSubstitution(FontSubstitution const &d) = delete;
+ FontSubstitution& operator=(FontSubstitution const &d) = delete;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_FONT_SUBSTITUTION_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/glyphs.cpp b/src/ui/dialog/glyphs.cpp
new file mode 100644
index 0000000..f67059a
--- /dev/null
+++ b/src/ui/dialog/glyphs.cpp
@@ -0,0 +1,783 @@
+// 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 "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 {
+
+
+GlyphsPanel &GlyphsPanel::getInstance()
+{
+ return *new GlyphsPanel();
+}
+
+
+static std::map<GUnicodeScript, Glib::ustring> & getScriptToName()
+{
+ static bool init = false;
+ static std::map<GUnicodeScript, Glib::ustring> mappings;
+ if (!init) {
+ init = true;
+ mappings[G_UNICODE_SCRIPT_INVALID_CODE] = _("all");
+ mappings[G_UNICODE_SCRIPT_COMMON] = _("common");
+ mappings[G_UNICODE_SCRIPT_INHERITED] = _("inherited");
+ mappings[G_UNICODE_SCRIPT_ARABIC] = _("Arabic");
+ mappings[G_UNICODE_SCRIPT_ARMENIAN] = _("Armenian");
+ mappings[G_UNICODE_SCRIPT_BENGALI] = _("Bengali");
+ mappings[G_UNICODE_SCRIPT_BOPOMOFO] = _("Bopomofo");
+ mappings[G_UNICODE_SCRIPT_CHEROKEE] = _("Cherokee");
+ mappings[G_UNICODE_SCRIPT_COPTIC] = _("Coptic");
+ mappings[G_UNICODE_SCRIPT_CYRILLIC] = _("Cyrillic");
+ mappings[G_UNICODE_SCRIPT_DESERET] = _("Deseret");
+ mappings[G_UNICODE_SCRIPT_DEVANAGARI] = _("Devanagari");
+ mappings[G_UNICODE_SCRIPT_ETHIOPIC] = _("Ethiopic");
+ mappings[G_UNICODE_SCRIPT_GEORGIAN] = _("Georgian");
+ mappings[G_UNICODE_SCRIPT_GOTHIC] = _("Gothic");
+ mappings[G_UNICODE_SCRIPT_GREEK] = _("Greek");
+ mappings[G_UNICODE_SCRIPT_GUJARATI] = _("Gujarati");
+ mappings[G_UNICODE_SCRIPT_GURMUKHI] = _("Gurmukhi");
+ mappings[G_UNICODE_SCRIPT_HAN] = _("Han");
+ mappings[G_UNICODE_SCRIPT_HANGUL] = _("Hangul");
+ mappings[G_UNICODE_SCRIPT_HEBREW] = _("Hebrew");
+ mappings[G_UNICODE_SCRIPT_HIRAGANA] = _("Hiragana");
+ mappings[G_UNICODE_SCRIPT_KANNADA] = _("Kannada");
+ mappings[G_UNICODE_SCRIPT_KATAKANA] = _("Katakana");
+ mappings[G_UNICODE_SCRIPT_KHMER] = _("Khmer");
+ mappings[G_UNICODE_SCRIPT_LAO] = _("Lao");
+ mappings[G_UNICODE_SCRIPT_LATIN] = _("Latin");
+ mappings[G_UNICODE_SCRIPT_MALAYALAM] = _("Malayalam");
+ mappings[G_UNICODE_SCRIPT_MONGOLIAN] = _("Mongolian");
+ mappings[G_UNICODE_SCRIPT_MYANMAR] = _("Myanmar");
+ mappings[G_UNICODE_SCRIPT_OGHAM] = _("Ogham");
+ mappings[G_UNICODE_SCRIPT_OLD_ITALIC] = _("Old Italic");
+ mappings[G_UNICODE_SCRIPT_ORIYA] = _("Oriya");
+ mappings[G_UNICODE_SCRIPT_RUNIC] = _("Runic");
+ mappings[G_UNICODE_SCRIPT_SINHALA] = _("Sinhala");
+ mappings[G_UNICODE_SCRIPT_SYRIAC] = _("Syriac");
+ mappings[G_UNICODE_SCRIPT_TAMIL] = _("Tamil");
+ mappings[G_UNICODE_SCRIPT_TELUGU] = _("Telugu");
+ mappings[G_UNICODE_SCRIPT_THAANA] = _("Thaana");
+ mappings[G_UNICODE_SCRIPT_THAI] = _("Thai");
+ mappings[G_UNICODE_SCRIPT_TIBETAN] = _("Tibetan");
+ mappings[G_UNICODE_SCRIPT_CANADIAN_ABORIGINAL] = _("Canadian Aboriginal");
+ mappings[G_UNICODE_SCRIPT_YI] = _("Yi");
+ mappings[G_UNICODE_SCRIPT_TAGALOG] = _("Tagalog");
+ mappings[G_UNICODE_SCRIPT_HANUNOO] = _("Hanunoo");
+ mappings[G_UNICODE_SCRIPT_BUHID] = _("Buhid");
+ mappings[G_UNICODE_SCRIPT_TAGBANWA] = _("Tagbanwa");
+ mappings[G_UNICODE_SCRIPT_BRAILLE] = _("Braille");
+ mappings[G_UNICODE_SCRIPT_CYPRIOT] = _("Cypriot");
+ mappings[G_UNICODE_SCRIPT_LIMBU] = _("Limbu");
+ mappings[G_UNICODE_SCRIPT_OSMANYA] = _("Osmanya");
+ mappings[G_UNICODE_SCRIPT_SHAVIAN] = _("Shavian");
+ mappings[G_UNICODE_SCRIPT_LINEAR_B] = _("Linear B");
+ mappings[G_UNICODE_SCRIPT_TAI_LE] = _("Tai Le");
+ mappings[G_UNICODE_SCRIPT_UGARITIC] = _("Ugaritic");
+ mappings[G_UNICODE_SCRIPT_NEW_TAI_LUE] = _("New Tai Lue");
+ mappings[G_UNICODE_SCRIPT_BUGINESE] = _("Buginese");
+ mappings[G_UNICODE_SCRIPT_GLAGOLITIC] = _("Glagolitic");
+ mappings[G_UNICODE_SCRIPT_TIFINAGH] = _("Tifinagh");
+ mappings[G_UNICODE_SCRIPT_SYLOTI_NAGRI] = _("Syloti Nagri");
+ mappings[G_UNICODE_SCRIPT_OLD_PERSIAN] = _("Old Persian");
+ mappings[G_UNICODE_SCRIPT_KHAROSHTHI] = _("Kharoshthi");
+ mappings[G_UNICODE_SCRIPT_UNKNOWN] = _("unassigned");
+ mappings[G_UNICODE_SCRIPT_BALINESE] = _("Balinese");
+ mappings[G_UNICODE_SCRIPT_CUNEIFORM] = _("Cuneiform");
+ mappings[G_UNICODE_SCRIPT_PHOENICIAN] = _("Phoenician");
+ mappings[G_UNICODE_SCRIPT_PHAGS_PA] = _("Phags-pa");
+ mappings[G_UNICODE_SCRIPT_NKO] = _("N'Ko");
+ mappings[G_UNICODE_SCRIPT_KAYAH_LI] = _("Kayah Li");
+ mappings[G_UNICODE_SCRIPT_LEPCHA] = _("Lepcha");
+ mappings[G_UNICODE_SCRIPT_REJANG] = _("Rejang");
+ mappings[G_UNICODE_SCRIPT_SUNDANESE] = _("Sundanese");
+ mappings[G_UNICODE_SCRIPT_SAURASHTRA] = _("Saurashtra");
+ mappings[G_UNICODE_SCRIPT_CHAM] = _("Cham");
+ mappings[G_UNICODE_SCRIPT_OL_CHIKI] = _("Ol Chiki");
+ mappings[G_UNICODE_SCRIPT_VAI] = _("Vai");
+ mappings[G_UNICODE_SCRIPT_CARIAN] = _("Carian");
+ mappings[G_UNICODE_SCRIPT_LYCIAN] = _("Lycian");
+ mappings[G_UNICODE_SCRIPT_LYDIAN] = _("Lydian");
+ mappings[G_UNICODE_SCRIPT_AVESTAN] = _("Avestan"); // Since: 2.26
+ mappings[G_UNICODE_SCRIPT_BAMUM] = _("Bamum"); // Since: 2.26
+ mappings[G_UNICODE_SCRIPT_EGYPTIAN_HIEROGLYPHS] = _("Egyptian Hieroglpyhs"); // Since: 2.26
+ mappings[G_UNICODE_SCRIPT_IMPERIAL_ARAMAIC] = _("Imperial Aramaic"); // Since: 2.26
+ mappings[G_UNICODE_SCRIPT_INSCRIPTIONAL_PAHLAVI]= _("Inscriptional Pahlavi"); // Since: 2.26
+ mappings[G_UNICODE_SCRIPT_INSCRIPTIONAL_PARTHIAN]= _("Inscriptional Parthian"); // Since: 2.26
+ mappings[G_UNICODE_SCRIPT_JAVANESE] = _("Javanese"); // Since: 2.26
+ mappings[G_UNICODE_SCRIPT_KAITHI] = _("Kaithi"); // Since: 2.26
+ mappings[G_UNICODE_SCRIPT_LISU] = _("Lisu"); // Since: 2.26
+ mappings[G_UNICODE_SCRIPT_MEETEI_MAYEK] = _("Meetei Mayek"); // Since: 2.26
+ mappings[G_UNICODE_SCRIPT_OLD_SOUTH_ARABIAN] = _("Old South Arabian"); // Since: 2.26
+ mappings[G_UNICODE_SCRIPT_OLD_TURKIC] = _("Old Turkic"); // Since: 2.28
+ mappings[G_UNICODE_SCRIPT_SAMARITAN] = _("Samaritan"); // Since: 2.26
+ mappings[G_UNICODE_SCRIPT_TAI_THAM] = _("Tai Tham"); // Since: 2.26
+ mappings[G_UNICODE_SCRIPT_TAI_VIET] = _("Tai Viet"); // Since: 2.26
+ mappings[G_UNICODE_SCRIPT_BATAK] = _("Batak"); // Since: 2.28
+ mappings[G_UNICODE_SCRIPT_BRAHMI] = _("Brahmi"); // Since: 2.28
+ mappings[G_UNICODE_SCRIPT_MANDAIC] = _("Mandaic"); // Since: 2.28
+ mappings[G_UNICODE_SCRIPT_CHAKMA] = _("Chakma"); // Since: 2.32
+ mappings[G_UNICODE_SCRIPT_MEROITIC_CURSIVE] = _("Meroitic Cursive"); // Since: 2.32
+ mappings[G_UNICODE_SCRIPT_MEROITIC_HIEROGLYPHS] = _("Meroitic Hieroglyphs"); // Since: 2.32
+ mappings[G_UNICODE_SCRIPT_MIAO] = _("Miao"); // Since: 2.32
+ mappings[G_UNICODE_SCRIPT_SHARADA] = _("Sharada"); // Since: 2.32
+ mappings[G_UNICODE_SCRIPT_SORA_SOMPENG] = _("Sora Sompeng"); // Since: 2.32
+ mappings[G_UNICODE_SCRIPT_TAKRI] = _("Takri"); // Since: 2.32
+ mappings[G_UNICODE_SCRIPT_BASSA_VAH] = _("Bassa"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_CAUCASIAN_ALBANIAN] = _("Caucasian Albanian"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_DUPLOYAN] = _("Duployan"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_ELBASAN] = _("Elbasan"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_GRANTHA] = _("Grantha"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_KHOJKI] = _("Khojki"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_KHUDAWADI] = _("Khudawadi, Sindhi"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_LINEAR_A] = _("Linear A"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_MAHAJANI] = _("Mahajani"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_MANICHAEAN] = _("Manichaean"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_MENDE_KIKAKUI] = _("Mende Kikakui"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_MODI] = _("Modi"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_MRO] = _("Mro"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_NABATAEAN] = _("Nabataean"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_OLD_NORTH_ARABIAN] = _("Old North Arabian"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_OLD_PERMIC] = _("Old Permic"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_PAHAWH_HMONG] = _("Pahawh Hmong"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_PALMYRENE] = _("Palmyrene"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_PAU_CIN_HAU] = _("Pau Cin Hau"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_PSALTER_PAHLAVI] = _("Psalter Pahlavi"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_SIDDHAM] = _("Siddham"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_TIRHUTA] = _("Tirhuta"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_WARANG_CITI] = _("Warang Citi"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_AHOM] = _("Ahom"); // Since: 2.48
+ mappings[G_UNICODE_SCRIPT_ANATOLIAN_HIEROGLYPHS]= _("Anatolian Hieroglyphs"); // Since: 2.48
+ mappings[G_UNICODE_SCRIPT_HATRAN] = _("Hatran"); // Since: 2.48
+ mappings[G_UNICODE_SCRIPT_MULTANI] = _("Multani"); // Since: 2.48
+ mappings[G_UNICODE_SCRIPT_OLD_HUNGARIAN] = _("Old Hungarian"); // Since: 2.48
+ mappings[G_UNICODE_SCRIPT_SIGNWRITING] = _("Signwriting"); // Since: 2.48
+/*
+ mappings[G_UNICODE_SCRIPT_ADLAM] = _("Adlam"); // Since: 2.50
+ mappings[G_UNICODE_SCRIPT_BHAIKSUKI] = _("Bhaiksuki"); // Since: 2.50
+ mappings[G_UNICODE_SCRIPT_MARCHEN] = _("Marchen"); // Since: 2.50
+ mappings[G_UNICODE_SCRIPT_NEWA] = _("Newa"); // Since: 2.50
+ mappings[G_UNICODE_SCRIPT_OSAGE] = _("Osage"); // Since: 2.50
+ mappings[G_UNICODE_SCRIPT_TANGUT] = _("Tangut"); // Since: 2.50
+ mappings[G_UNICODE_SCRIPT_MASARAM_GONDI] = _("Masaram Gondi"); // Since: 2.54
+ mappings[G_UNICODE_SCRIPT_NUSHU] = _("Nushu"); // Since: 2.54
+ mappings[G_UNICODE_SCRIPT_SOYOMBO] = _("Soyombo"); // Since: 2.54
+ mappings[G_UNICODE_SCRIPT_ZANABAZAR_SQUARE] = _("Zanabazar Square"); // Since: 2.54
+ mappings[G_UNICODE_SCRIPT_DOGRA] = _("Dogra"); // Since: 2.58
+ mappings[G_UNICODE_SCRIPT_GUNJALA_GONDI] = _("Gunjala Gondi"); // Since: 2.58
+ mappings[G_UNICODE_SCRIPT_HANIFI_ROHINGYA] = _("Hanifi Rohingya"); // Since: 2.58
+ mappings[G_UNICODE_SCRIPT_MAKASAR] = _("Makasar"); // Since: 2.58
+ mappings[G_UNICODE_SCRIPT_MEDEFAIDRIN] = _("Medefaidrin"); // Since: 2.58
+ mappings[G_UNICODE_SCRIPT_OLD_SOGDIAN] = _("Old Sogdian"); // Since: 2.58
+ mappings[G_UNICODE_SCRIPT_SOGDIAN] = _("Sogdian"); // Since: 2.58
+ mappings[G_UNICODE_SCRIPT_ELYMAIC] = _("Elym"); // Since: 2.62
+ mappings[G_UNICODE_SCRIPT_NANDINAGARI] = _("Nand"); // Since: 2.62
+ mappings[G_UNICODE_SCRIPT_NYIAKENG_PUACHUE_HMONG]= _("Rohg"); // Since: 2.62
+ mappings[G_UNICODE_SCRIPT_WANCHO] = _("Wcho"); // Since: 2.62
+*/
+ }
+ return mappings;
+}
+
+typedef std::pair<gunichar, gunichar> Range;
+typedef std::pair<Range, Glib::ustring> NamedRange;
+
+static std::vector<NamedRange> & getRanges()
+{
+ static bool init = false;
+ static std::vector<NamedRange> ranges;
+ if (!init) {
+ init = true;
+ ranges.emplace_back(std::make_pair(0x00000, 0x2FFFF), _("all"));
+ ranges.emplace_back(std::make_pair(0x00000, 0x0FFFF), _("Basic Plane"));
+ ranges.emplace_back(std::make_pair(0x10000, 0x1FFFF), _("Extended Multilingual Plane"));
+ ranges.emplace_back(std::make_pair(0x20000, 0x2FFFF), _("Supplementary Ideographic Plane"));
+
+ ranges.emplace_back(std::make_pair(0x0000, 0x007F), _("Basic Latin"));
+ ranges.emplace_back(std::make_pair(0x0080, 0x00FF), _("Latin-1 Supplement"));
+ ranges.emplace_back(std::make_pair(0x0100, 0x017F), _("Latin Extended-A"));
+ ranges.emplace_back(std::make_pair(0x0180, 0x024F), _("Latin Extended-B"));
+ ranges.emplace_back(std::make_pair(0x0250, 0x02AF), _("IPA Extensions"));
+ ranges.emplace_back(std::make_pair(0x02B0, 0x02FF), _("Spacing Modifier Letters"));
+ ranges.emplace_back(std::make_pair(0x0300, 0x036F), _("Combining Diacritical Marks"));
+ ranges.emplace_back(std::make_pair(0x0370, 0x03FF), _("Greek and Coptic"));
+ ranges.emplace_back(std::make_pair(0x0400, 0x04FF), _("Cyrillic"));
+ ranges.emplace_back(std::make_pair(0x0500, 0x052F), _("Cyrillic Supplement"));
+ ranges.emplace_back(std::make_pair(0x0530, 0x058F), _("Armenian"));
+ ranges.emplace_back(std::make_pair(0x0590, 0x05FF), _("Hebrew"));
+ ranges.emplace_back(std::make_pair(0x0600, 0x06FF), _("Arabic"));
+ ranges.emplace_back(std::make_pair(0x0700, 0x074F), _("Syriac"));
+ ranges.emplace_back(std::make_pair(0x0750, 0x077F), _("Arabic Supplement"));
+ ranges.emplace_back(std::make_pair(0x0780, 0x07BF), _("Thaana"));
+ ranges.emplace_back(std::make_pair(0x07C0, 0x07FF), _("NKo"));
+ ranges.emplace_back(std::make_pair(0x0800, 0x083F), _("Samaritan"));
+ ranges.emplace_back(std::make_pair(0x0900, 0x097F), _("Devanagari"));
+ ranges.emplace_back(std::make_pair(0x0980, 0x09FF), _("Bengali"));
+ ranges.emplace_back(std::make_pair(0x0A00, 0x0A7F), _("Gurmukhi"));
+ ranges.emplace_back(std::make_pair(0x0A80, 0x0AFF), _("Gujarati"));
+ ranges.emplace_back(std::make_pair(0x0B00, 0x0B7F), _("Oriya"));
+ ranges.emplace_back(std::make_pair(0x0B80, 0x0BFF), _("Tamil"));
+ ranges.emplace_back(std::make_pair(0x0C00, 0x0C7F), _("Telugu"));
+ ranges.emplace_back(std::make_pair(0x0C80, 0x0CFF), _("Kannada"));
+ ranges.emplace_back(std::make_pair(0x0D00, 0x0D7F), _("Malayalam"));
+ ranges.emplace_back(std::make_pair(0x0D80, 0x0DFF), _("Sinhala"));
+ ranges.emplace_back(std::make_pair(0x0E00, 0x0E7F), _("Thai"));
+ ranges.emplace_back(std::make_pair(0x0E80, 0x0EFF), _("Lao"));
+ ranges.emplace_back(std::make_pair(0x0F00, 0x0FFF), _("Tibetan"));
+ ranges.emplace_back(std::make_pair(0x1000, 0x109F), _("Myanmar"));
+ ranges.emplace_back(std::make_pair(0x10A0, 0x10FF), _("Georgian"));
+ ranges.emplace_back(std::make_pair(0x1100, 0x11FF), _("Hangul Jamo"));
+ ranges.emplace_back(std::make_pair(0x1200, 0x137F), _("Ethiopic"));
+ ranges.emplace_back(std::make_pair(0x1380, 0x139F), _("Ethiopic Supplement"));
+ ranges.emplace_back(std::make_pair(0x13A0, 0x13FF), _("Cherokee"));
+ ranges.emplace_back(std::make_pair(0x1400, 0x167F), _("Unified Canadian Aboriginal Syllabics"));
+ ranges.emplace_back(std::make_pair(0x1680, 0x169F), _("Ogham"));
+ ranges.emplace_back(std::make_pair(0x16A0, 0x16FF), _("Runic"));
+ ranges.emplace_back(std::make_pair(0x1700, 0x171F), _("Tagalog"));
+ ranges.emplace_back(std::make_pair(0x1720, 0x173F), _("Hanunoo"));
+ ranges.emplace_back(std::make_pair(0x1740, 0x175F), _("Buhid"));
+ ranges.emplace_back(std::make_pair(0x1760, 0x177F), _("Tagbanwa"));
+ ranges.emplace_back(std::make_pair(0x1780, 0x17FF), _("Khmer"));
+ ranges.emplace_back(std::make_pair(0x1800, 0x18AF), _("Mongolian"));
+ ranges.emplace_back(std::make_pair(0x18B0, 0x18FF), _("Unified Canadian Aboriginal Syllabics Extended"));
+ ranges.emplace_back(std::make_pair(0x1900, 0x194F), _("Limbu"));
+ ranges.emplace_back(std::make_pair(0x1950, 0x197F), _("Tai Le"));
+ ranges.emplace_back(std::make_pair(0x1980, 0x19DF), _("New Tai Lue"));
+ ranges.emplace_back(std::make_pair(0x19E0, 0x19FF), _("Khmer Symbols"));
+ ranges.emplace_back(std::make_pair(0x1A00, 0x1A1F), _("Buginese"));
+ ranges.emplace_back(std::make_pair(0x1A20, 0x1AAF), _("Tai Tham"));
+ ranges.emplace_back(std::make_pair(0x1B00, 0x1B7F), _("Balinese"));
+ ranges.emplace_back(std::make_pair(0x1B80, 0x1BBF), _("Sundanese"));
+ ranges.emplace_back(std::make_pair(0x1C00, 0x1C4F), _("Lepcha"));
+ ranges.emplace_back(std::make_pair(0x1C50, 0x1C7F), _("Ol Chiki"));
+ ranges.emplace_back(std::make_pair(0x1CD0, 0x1CFF), _("Vedic Extensions"));
+ ranges.emplace_back(std::make_pair(0x1D00, 0x1D7F), _("Phonetic Extensions"));
+ ranges.emplace_back(std::make_pair(0x1D80, 0x1DBF), _("Phonetic Extensions Supplement"));
+ ranges.emplace_back(std::make_pair(0x1DC0, 0x1DFF), _("Combining Diacritical Marks Supplement"));
+ ranges.emplace_back(std::make_pair(0x1E00, 0x1EFF), _("Latin Extended Additional"));
+ ranges.emplace_back(std::make_pair(0x1F00, 0x1FFF), _("Greek Extended"));
+ ranges.emplace_back(std::make_pair(0x2000, 0x206F), _("General Punctuation"));
+ ranges.emplace_back(std::make_pair(0x2070, 0x209F), _("Superscripts and Subscripts"));
+ ranges.emplace_back(std::make_pair(0x20A0, 0x20CF), _("Currency Symbols"));
+ ranges.emplace_back(std::make_pair(0x20D0, 0x20FF), _("Combining Diacritical Marks for Symbols"));
+ ranges.emplace_back(std::make_pair(0x2100, 0x214F), _("Letterlike Symbols"));
+ ranges.emplace_back(std::make_pair(0x2150, 0x218F), _("Number Forms"));
+ ranges.emplace_back(std::make_pair(0x2190, 0x21FF), _("Arrows"));
+ ranges.emplace_back(std::make_pair(0x2200, 0x22FF), _("Mathematical Operators"));
+ ranges.emplace_back(std::make_pair(0x2300, 0x23FF), _("Miscellaneous Technical"));
+ ranges.emplace_back(std::make_pair(0x2400, 0x243F), _("Control Pictures"));
+ ranges.emplace_back(std::make_pair(0x2440, 0x245F), _("Optical Character Recognition"));
+ ranges.emplace_back(std::make_pair(0x2460, 0x24FF), _("Enclosed Alphanumerics"));
+ ranges.emplace_back(std::make_pair(0x2500, 0x257F), _("Box Drawing"));
+ ranges.emplace_back(std::make_pair(0x2580, 0x259F), _("Block Elements"));
+ ranges.emplace_back(std::make_pair(0x25A0, 0x25FF), _("Geometric Shapes"));
+ ranges.emplace_back(std::make_pair(0x2600, 0x26FF), _("Miscellaneous Symbols"));
+ ranges.emplace_back(std::make_pair(0x2700, 0x27BF), _("Dingbats"));
+ ranges.emplace_back(std::make_pair(0x27C0, 0x27EF), _("Miscellaneous Mathematical Symbols-A"));
+ ranges.emplace_back(std::make_pair(0x27F0, 0x27FF), _("Supplemental Arrows-A"));
+ ranges.emplace_back(std::make_pair(0x2800, 0x28FF), _("Braille Patterns"));
+ ranges.emplace_back(std::make_pair(0x2900, 0x297F), _("Supplemental Arrows-B"));
+ ranges.emplace_back(std::make_pair(0x2980, 0x29FF), _("Miscellaneous Mathematical Symbols-B"));
+ ranges.emplace_back(std::make_pair(0x2A00, 0x2AFF), _("Supplemental Mathematical Operators"));
+ ranges.emplace_back(std::make_pair(0x2B00, 0x2BFF), _("Miscellaneous Symbols and Arrows"));
+ ranges.emplace_back(std::make_pair(0x2C00, 0x2C5F), _("Glagolitic"));
+ ranges.emplace_back(std::make_pair(0x2C60, 0x2C7F), _("Latin Extended-C"));
+ ranges.emplace_back(std::make_pair(0x2C80, 0x2CFF), _("Coptic"));
+ ranges.emplace_back(std::make_pair(0x2D00, 0x2D2F), _("Georgian Supplement"));
+ ranges.emplace_back(std::make_pair(0x2D30, 0x2D7F), _("Tifinagh"));
+ ranges.emplace_back(std::make_pair(0x2D80, 0x2DDF), _("Ethiopic Extended"));
+ ranges.emplace_back(std::make_pair(0x2DE0, 0x2DFF), _("Cyrillic Extended-A"));
+ ranges.emplace_back(std::make_pair(0x2E00, 0x2E7F), _("Supplemental Punctuation"));
+ ranges.emplace_back(std::make_pair(0x2E80, 0x2EFF), _("CJK Radicals Supplement"));
+ ranges.emplace_back(std::make_pair(0x2F00, 0x2FDF), _("Kangxi Radicals"));
+ ranges.emplace_back(std::make_pair(0x2FF0, 0x2FFF), _("Ideographic Description Characters"));
+ ranges.emplace_back(std::make_pair(0x3000, 0x303F), _("CJK Symbols and Punctuation"));
+ ranges.emplace_back(std::make_pair(0x3040, 0x309F), _("Hiragana"));
+ ranges.emplace_back(std::make_pair(0x30A0, 0x30FF), _("Katakana"));
+ ranges.emplace_back(std::make_pair(0x3100, 0x312F), _("Bopomofo"));
+ ranges.emplace_back(std::make_pair(0x3130, 0x318F), _("Hangul Compatibility Jamo"));
+ ranges.emplace_back(std::make_pair(0x3190, 0x319F), _("Kanbun"));
+ ranges.emplace_back(std::make_pair(0x31A0, 0x31BF), _("Bopomofo Extended"));
+ ranges.emplace_back(std::make_pair(0x31C0, 0x31EF), _("CJK Strokes"));
+ ranges.emplace_back(std::make_pair(0x31F0, 0x31FF), _("Katakana Phonetic Extensions"));
+ ranges.emplace_back(std::make_pair(0x3200, 0x32FF), _("Enclosed CJK Letters and Months"));
+ ranges.emplace_back(std::make_pair(0x3300, 0x33FF), _("CJK Compatibility"));
+ ranges.emplace_back(std::make_pair(0x3400, 0x4DBF), _("CJK Unified Ideographs Extension A"));
+ ranges.emplace_back(std::make_pair(0x4DC0, 0x4DFF), _("Yijing Hexagram Symbols"));
+ ranges.emplace_back(std::make_pair(0x4E00, 0x9FFF), _("CJK Unified Ideographs"));
+ ranges.emplace_back(std::make_pair(0xA000, 0xA48F), _("Yi Syllables"));
+ ranges.emplace_back(std::make_pair(0xA490, 0xA4CF), _("Yi Radicals"));
+ ranges.emplace_back(std::make_pair(0xA4D0, 0xA4FF), _("Lisu"));
+ ranges.emplace_back(std::make_pair(0xA500, 0xA63F), _("Vai"));
+ ranges.emplace_back(std::make_pair(0xA640, 0xA69F), _("Cyrillic Extended-B"));
+ ranges.emplace_back(std::make_pair(0xA6A0, 0xA6FF), _("Bamum"));
+ ranges.emplace_back(std::make_pair(0xA700, 0xA71F), _("Modifier Tone Letters"));
+ ranges.emplace_back(std::make_pair(0xA720, 0xA7FF), _("Latin Extended-D"));
+ ranges.emplace_back(std::make_pair(0xA800, 0xA82F), _("Syloti Nagri"));
+ ranges.emplace_back(std::make_pair(0xA830, 0xA83F), _("Common Indic Number Forms"));
+ ranges.emplace_back(std::make_pair(0xA840, 0xA87F), _("Phags-pa"));
+ ranges.emplace_back(std::make_pair(0xA880, 0xA8DF), _("Saurashtra"));
+ ranges.emplace_back(std::make_pair(0xA8E0, 0xA8FF), _("Devanagari Extended"));
+ ranges.emplace_back(std::make_pair(0xA900, 0xA92F), _("Kayah Li"));
+ ranges.emplace_back(std::make_pair(0xA930, 0xA95F), _("Rejang"));
+ ranges.emplace_back(std::make_pair(0xA960, 0xA97F), _("Hangul Jamo Extended-A"));
+ ranges.emplace_back(std::make_pair(0xA980, 0xA9DF), _("Javanese"));
+ ranges.emplace_back(std::make_pair(0xAA00, 0xAA5F), _("Cham"));
+ ranges.emplace_back(std::make_pair(0xAA60, 0xAA7F), _("Myanmar Extended-A"));
+ ranges.emplace_back(std::make_pair(0xAA80, 0xAADF), _("Tai Viet"));
+ ranges.emplace_back(std::make_pair(0xABC0, 0xABFF), _("Meetei Mayek"));
+ ranges.emplace_back(std::make_pair(0xAC00, 0xD7AF), _("Hangul Syllables"));
+ ranges.emplace_back(std::make_pair(0xD7B0, 0xD7FF), _("Hangul Jamo Extended-B"));
+ ranges.emplace_back(std::make_pair(0xD800, 0xDB7F), _("High Surrogates"));
+ ranges.emplace_back(std::make_pair(0xDB80, 0xDBFF), _("High Private Use Surrogates"));
+ ranges.emplace_back(std::make_pair(0xDC00, 0xDFFF), _("Low Surrogates"));
+ ranges.emplace_back(std::make_pair(0xE000, 0xF8FF), _("Private Use Area"));
+ ranges.emplace_back(std::make_pair(0xF900, 0xFAFF), _("CJK Compatibility Ideographs"));
+ ranges.emplace_back(std::make_pair(0xFB00, 0xFB4F), _("Alphabetic Presentation Forms"));
+ ranges.emplace_back(std::make_pair(0xFB50, 0xFDFF), _("Arabic Presentation Forms-A"));
+ ranges.emplace_back(std::make_pair(0xFE00, 0xFE0F), _("Variation Selectors"));
+ ranges.emplace_back(std::make_pair(0xFE10, 0xFE1F), _("Vertical Forms"));
+ ranges.emplace_back(std::make_pair(0xFE20, 0xFE2F), _("Combining Half Marks"));
+ ranges.emplace_back(std::make_pair(0xFE30, 0xFE4F), _("CJK Compatibility Forms"));
+ ranges.emplace_back(std::make_pair(0xFE50, 0xFE6F), _("Small Form Variants"));
+ ranges.emplace_back(std::make_pair(0xFE70, 0xFEFF), _("Arabic Presentation Forms-B"));
+ ranges.emplace_back(std::make_pair(0xFF00, 0xFFEF), _("Halfwidth and Fullwidth Forms"));
+ ranges.emplace_back(std::make_pair(0xFFF0, 0xFFFF), _("Specials"));
+
+ // Selected ranges in Extended Multilingual Plane
+ ranges.emplace_back(std::make_pair(0x1F300, 0x1F5FF), _("Miscellaneous Symbols and Pictographs"));
+ ranges.emplace_back(std::make_pair(0x1F600, 0x1F64F), _("Emoticons"));
+ ranges.emplace_back(std::make_pair(0x1F650, 0x1F67F), _("Ornamental Dingbats"));
+ ranges.emplace_back(std::make_pair(0x1F680, 0x1F6FF), _("Transport and Map Symbols"));
+ ranges.emplace_back(std::make_pair(0x1F700, 0x1F77F), _("Alchemical Symbols"));
+ ranges.emplace_back(std::make_pair(0x1F780, 0x1F7FF), _("Geometric Shapes Extended"));
+ ranges.emplace_back(std::make_pair(0x1F800, 0x1F8FF), _("Supplemental Arrows-C"));
+ ranges.emplace_back(std::make_pair(0x1F900, 0x1F9FF), _("Supplemental Symbols and Pictographs"));
+ ranges.emplace_back(std::make_pair(0x1FA00, 0x1FA7F), _("Chess Symbols"));
+ ranges.emplace_back(std::make_pair(0x1FA80, 0x1FAFF), _("Symbols and Pictographs Extended-A"));
+
+ }
+
+ return ranges;
+}
+
+class GlyphColumns : public Gtk::TreeModel::ColumnRecord
+{
+public:
+ Gtk::TreeModelColumn<gunichar> code;
+ Gtk::TreeModelColumn<Glib::ustring> name;
+ Gtk::TreeModelColumn<Glib::ustring> tooltip;
+
+ GlyphColumns()
+ {
+ add(code);
+ add(name);
+ add(tooltip);
+ }
+};
+
+GlyphColumns *GlyphsPanel::getColumns()
+{
+ static GlyphColumns *columns = new GlyphColumns();
+
+ return columns;
+}
+
+/**
+ * Constructor
+ */
+GlyphsPanel::GlyphsPanel()
+ : 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 (SP_IS_TEXT(*i) || SP_IS_FLOWTEXT(*i)) {
+ textItem = *i;
+ break;
+ }
+ }
+
+ if (textItem) {
+ Glib::ustring glyphs;
+ if (entry->get_text_length() > 0) {
+ glyphs = entry->get_text();
+ } else {
+ auto itemArray = iconView->get_selected_items();
+
+ if (!itemArray.empty()) {
+ Gtk::TreeModel::Path const & path = *itemArray.begin();
+ Gtk::ListStore::iterator row = store->get_iter(path);
+ gunichar ch = (*row)[getColumns()->code];
+ glyphs = ch;
+ }
+ }
+
+ if (!glyphs.empty()) {
+ Glib::ustring combined = 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 (SP_IS_TEXT(*i) || SP_IS_FLOWTEXT(*i)) {
+ ++items;
+ }
+ }
+
+ bool enable = (items == 1);
+ if (enable) {
+ enable &= (!iconView->get_selected_items().empty()
+ || (entry->get_text_length() > 0));
+ }
+
+ if (enable != insertBtn->is_sensitive()) {
+ insertBtn->set_sensitive(enable);
+ }
+}
+
+void GlyphsPanel::readSelection( bool updateStyle, bool updateContent )
+{
+ calcCanInsert();
+
+ if (updateStyle) {
+ Inkscape::FontLister* fontlister = Inkscape::FontLister::get_instance();
+
+ // Update family/style based on selection.
+ fontlister->selection_update();
+
+ // Update GUI (based on fontlister values).
+ fontSelector->update_font ();
+ }
+}
+
+
+void GlyphsPanel::rebuild()
+{
+ Glib::ustring fontspec = fontSelector->get_fontspec();
+
+ font_instance* font = nullptr;
+ if( !fontspec.empty() ) {
+ font = font_factory::Default()->FaceFromFontSpecification( fontspec.c_str() );
+ }
+
+ if (font) {
+
+ GUnicodeScript script = G_UNICODE_SCRIPT_INVALID_CODE;
+ Glib::ustring scriptName = scriptCombo->get_active_text();
+ std::map<GUnicodeScript, Glib::ustring> items = getScriptToName();
+ for (auto & item : items) {
+ if (scriptName == item.second) {
+ script = item.first;
+ break;
+ }
+ }
+
+ // Disconnect the model while we update it. Simple work-around for 5x+ performance boost.
+ Glib::RefPtr<Gtk::ListStore> tmp = Gtk::ListStore::create(*getColumns());
+ iconView->set_model(tmp);
+
+ gunichar lower = 0x00001;
+ gunichar upper = 0x2FFFF;
+ int active = rangeCombo->get_active_row_number();
+ if (active >= 0) {
+ lower = getRanges()[active].first.first;
+ upper = getRanges()[active].first.second;
+ }
+ std::vector<gunichar> present;
+ for (gunichar ch = lower; ch <= upper; ch++) {
+ int glyphId = font->MapUnicodeChar(ch);
+ if (glyphId > 0) {
+ if ((script == G_UNICODE_SCRIPT_INVALID_CODE) || (script == g_unichar_get_script(ch))) {
+ present.push_back(ch);
+ }
+ }
+ }
+
+ GlyphColumns *columns = getColumns();
+ store->clear();
+ for (unsigned int & it : present)
+ {
+ Gtk::ListStore::iterator row = store->append();
+ Glib::ustring tmp;
+ tmp += it;
+ tmp = Glib::Markup::escape_text(tmp); // Escape '&', '<', etc.
+ (*row)[columns->code] = it;
+ (*row)[columns->name] = "<span font_desc=\"" + fontspec + "\">" + tmp + "</span>";
+ (*row)[columns->tooltip] = "<span font_desc=\"" + fontspec + "\" size=\"42000\">" + tmp + "</span>";
+ }
+
+ // Reconnect the model once it has been updated:
+ iconView->set_model(store);
+ }
+}
+
+
+} // namespace Dialogs
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/glyphs.h b/src/ui/dialog/glyphs.h
new file mode 100644
index 0000000..9e22d61
--- /dev/null
+++ b/src/ui/dialog/glyphs.h
@@ -0,0 +1,91 @@
+// 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;
+
+ static GlyphsPanel& getInstance();
+
+ void selectionChanged(Selection *selection) override;
+ void selectionModified(Selection *selection, guint flags) override;
+
+protected:
+
+private:
+ GlyphsPanel(GlyphsPanel const &) = delete; // no copy
+ GlyphsPanel &operator=(GlyphsPanel const &) = delete; // no assign
+
+ static GlyphColumns *getColumns();
+
+ void rebuild();
+
+ void glyphActivated(Gtk::TreeModel::Path const & path);
+ void glyphSelectionChanged();
+ void 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..a0508bc
--- /dev/null
+++ b/src/ui/dialog/grid-arrange-tab.cpp
@@ -0,0 +1,659 @@
+// 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) return;
+
+ auto sel_box = selection->documentBounds(SPItem::VISUAL_BBOX);
+ 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, 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->selection : 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->selection : 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->selection : nullptr;
+ std::vector<SPItem*> items;
+ if (selection) {
+ items.insert(items.end(), selection->items().begin(), selection->items().end());
+ }
+
+ if (!items.empty()) {
+ int selcount = items.size();
+
+ if (NoOfColsSpinner.get_value() > 1 && NoOfRowsSpinner.get_value() > 1){
+ // Update the number of rows assuming number of columns wanted remains same.
+ double NoOfRows = ceil(selcount / NoOfColsSpinner.get_value());
+ NoOfRowsSpinner.set_value(NoOfRows);
+
+ // if the selection has less than the number set for one row, reduce it appropriately
+ if (selcount < NoOfColsSpinner.get_value()) {
+ double NoOfCols = ceil(selcount / NoOfRowsSpinner.get_value());
+ NoOfColsSpinner.set_value(NoOfCols);
+ }
+ } 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..9be41a5
--- /dev/null
+++ b/src/ui/dialog/guides.cpp
@@ -0,0 +1,369 @@
+// 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 "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);
+
+ _spin_button_x.setValueKeepUnit(_oldpos[Geom::X], "px");
+ _spin_button_y.setValueKeepUnit(_oldpos[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);
+ if (!_mode)
+ newpos += _oldpos;
+
+ _guide->moveto(newpos, true);
+
+ const gchar* name = g_strdup( _label_entry.getEntry()->get_text().c_str() );
+
+ _guide->set_label(name, true);
+
+ const bool locked = _locked_toggle.get_active();
+
+ _guide->set_locked(locked, true);
+
+ g_free((gpointer) name);
+
+ const auto c = _color.get_rgba();
+ unsigned r = c.get_red_u()/257, g = c.get_green_u()/257, b = c.get_blue_u()/257;
+ //TODO: why 257? verify this!
+ // don't know why, but introduced: 761f7da58cd6d625b88c24eee6fae1b7fa3bfcdd
+
+ _guide->set_color(r, g, b, true);
+}
+
+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..f09f6ae
--- /dev/null
+++ b/src/ui/dialog/icon-preview.cpp
@@ -0,0 +1,646 @@
+// 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 {
+
+
+IconPreviewPanel &IconPreviewPanel::getInstance()
+{
+ IconPreviewPanel *instance = new IconPreviewPanel();
+
+ instance->refreshPreview();
+
+ return *instance;
+}
+
+//#########################################################################
+//## E V E N T S
+//#########################################################################
+
+void IconPreviewPanel::on_button_clicked(int which)
+{
+ if ( hot != which ) {
+ buttons[hot]->set_active( false );
+
+ hot = which;
+ updateMagnify();
+ 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();
+}
+
+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 && SP_IS_ITEM(object)) {
+ SPItem *item = SP_ITEM(object);
+ // Find bbox in document
+ Geom::OptRect dbox = item->documentVisualBounds();
+
+ if ( object->parent == nullptr )
+ {
+ dbox = Geom::Rect(Geom::Point(0, 0),
+ Geom::Point(doc->getWidth().value("px"), doc->getHeight().value("px")));
+ }
+
+ /* This is in document coordinates, i.e. pixels */
+ if ( dbox ) {
+ /* Update to renderable state */
+ double sf = 1.0;
+ drawing.root()->setTransform(Geom::Scale(sf));
+ drawing.update();
+ /* Item integer bbox in points */
+ // NOTE: previously, each rect coordinate was rounded using floor(c + 0.5)
+ Geom::IntRect ibox = dbox->roundOutwards();
+
+ if ( dump ) {
+ g_message( " box --'%s' (%f,%f)-(%f,%f)", name, (double)ibox.left(), (double)ibox.top(), (double)ibox.right(), (double)ibox.bottom() );
+ }
+
+ /* Find button visible area */
+ int width = ibox.width();
+ int height = ibox.height();
+
+ if ( dump ) {
+ g_message( " vis --'%s' (%d,%d)", name, width, height );
+ }
+
+ {
+ int block = std::max(width, height);
+ if (block != static_cast<int>(psize) ) {
+ if ( dump ) {
+ g_message(" resizing" );
+ }
+ sf = (double)psize / (double)block;
+
+ drawing.root()->setTransform(Geom::Scale(sf));
+ drawing.update();
+
+ auto scaled_box = *dbox * Geom::Scale(sf);
+ ibox = scaled_box.roundOutwards();
+ if ( dump ) {
+ g_message( " box2 --'%s' (%f,%f)-(%f,%f)", name, (double)ibox.left(), (double)ibox.top(), (double)ibox.right(), (double)ibox.bottom() );
+ }
+
+ /* Find button visible area */
+ width = ibox.width();
+ height = ibox.height();
+ if ( dump ) {
+ g_message( " vis2 --'%s' (%d,%d)", name, width, height );
+ }
+ }
+ }
+
+ Geom::IntPoint pdim(psize, psize);
+ int dx, dy;
+ //dx = (psize - width) / 2;
+ //dy = (psize - height) / 2;
+ dx=dy=psize;
+ dx=(dx-width)/2; // watch out for psize, since 'unsigned'-'signed' can cause problems if the result is negative
+ dy=(dy-height)/2;
+ Geom::IntRect area = Geom::IntRect::from_xywh(ibox.min() - Geom::IntPoint(dx,dy), pdim);
+ /* Actual renderable area */
+ Geom::IntRect ua = *Geom::intersect(ibox, area);
+
+ if ( dump ) {
+ g_message( " area --'%s' (%f,%f)-(%f,%f)", name, (double)area.left(), (double)area.top(), (double)area.right(), (double)area.bottom() );
+ g_message( " ua --'%s' (%f,%f)-(%f,%f)", name, (double)ua.left(), (double)ua.top(), (double)ua.right(), (double)ua.bottom() );
+ }
+
+ stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, psize);
+
+ /* Set up pixblock */
+ px = g_new(guchar, stride * psize);
+ memset(px, 0x00, stride * psize);
+
+ /* Render */
+ cairo_surface_t *s = cairo_image_surface_create_for_data(px,
+ CAIRO_FORMAT_ARGB32, psize, psize, stride);
+ Inkscape::DrawingContext dc(s, ua.min());
+
+ 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..3ab7624
--- /dev/null
+++ b/src/ui/dialog/icon-preview.h
@@ -0,0 +1,114 @@
+// 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(Glib::ustring const &label);
+ ~IconPreviewPanel() override;
+
+ static IconPreviewPanel& getInstance();
+ void selectionModified(Selection *selection, guint flags) override;
+ void documentReplaced() override;
+
+ void refreshPreview();
+ void modeToggled();
+
+private:
+ IconPreviewPanel(IconPreviewPanel const &) = delete; // no copy
+ IconPreviewPanel &operator=(IconPreviewPanel const &) = delete; // no assign
+
+ 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..fed10b7
--- /dev/null
+++ b/src/ui/dialog/inkscape-preferences.cpp
@@ -0,0 +1,3707 @@
+// 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>
+ *F
+ * Copyright (C) 2004-2013 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifdef HAVE_CONFIG_H
+# include "config.h" // only include where actually required!
+#endif
+
+#include "inkscape-preferences.h"
+
+#include <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-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 "util/trim.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
+
+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;
+}
+
+/**
+ * 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;
+}
+
+
+bool InkscapePreferences::on_outline_overlay_changed(GdkEventFocus * /* focus_event */)
+{
+ if (auto *desktop = SP_ACTIVE_DESKTOP) {
+ desktop->getCanvas()->redraw_all();
+ }
+ return false;
+
+}
+
+/**
+ * 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"));
+}
+
+void InkscapePreferences::initPageTools()
+{
+ Gtk::TreeModel::iterator iter_tools = this->AddPage(_page_tools, _("Tools"), PREFS_PAGE_TOOLS);
+ this->AddPage(_page_selector, _("Selector"), iter_tools, PREFS_PAGE_TOOLS_SELECTOR);
+ this->AddPage(_page_node, _("Node"), iter_tools, PREFS_PAGE_TOOLS_NODE);
+
+ // shapes
+ Gtk::TreeModel::iterator iter_shapes = this->AddPage(_page_shapes, _("Shapes"), iter_tools, PREFS_PAGE_TOOLS_SHAPES);
+ this->AddPage(_page_rectangle, _("Rectangle"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_RECT);
+ this->AddPage(_page_ellipse, _("Ellipse"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_ELLIPSE);
+ this->AddPage(_page_star, _("Star"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_STAR);
+ this->AddPage(_page_3dbox, _("3D Box"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_3DBOX);
+ this->AddPage(_page_spiral, _("Spiral"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_SPIRAL);
+
+ this->AddPage(_page_pen, _("Pen"), iter_tools, PREFS_PAGE_TOOLS_PEN);
+ this->AddPage(_page_pencil, _("Pencil"), iter_tools, PREFS_PAGE_TOOLS_PENCIL);
+ this->AddPage(_page_calligraphy, _("Calligraphy"), iter_tools, PREFS_PAGE_TOOLS_CALLIGRAPHY);
+ this->AddPage(_page_text, C_("ContextVerb", "Text"), iter_tools, PREFS_PAGE_TOOLS_TEXT);
+
+ this->AddPage(_page_gradient, _("Gradient"), iter_tools, PREFS_PAGE_TOOLS_GRADIENT);
+ this->AddPage(_page_dropper, _("Dropper"), iter_tools, PREFS_PAGE_TOOLS_DROPPER);
+ this->AddPage(_page_paintbucket, _("Paint Bucket"), iter_tools, PREFS_PAGE_TOOLS_PAINTBUCKET);
+
+ this->AddPage(_page_tweak, _("Tweak"), iter_tools, PREFS_PAGE_TOOLS_TWEAK);
+ this->AddPage(_page_spray, _("Spray"), iter_tools, PREFS_PAGE_TOOLS_SPRAY);
+ this->AddPage(_page_eraser, _("Eraser"), iter_tools, PREFS_PAGE_TOOLS_ERASER);
+ this->AddPage(_page_connector, _("Connector"), iter_tools, PREFS_PAGE_TOOLS_CONNECTOR);
+#ifdef WITH_LPETOOL
+ this->AddPage(_page_lpetool, _("LPE Tool"), iter_tools, PREFS_PAGE_TOOLS_LPETOOL);
+#endif // WITH_LPETOOL
+ this->AddPage(_page_zoom, _("Zoom"), iter_tools, PREFS_PAGE_TOOLS_ZOOM);
+ this->AddPage(_page_measure, C_("ContextVerb", "Measure"), iter_tools, PREFS_PAGE_TOOLS_MEASURE);
+
+ _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)"));
+ }
+
+ //_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();
+}
+
+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;
+ if (dark) {
+ prefs->setBool("/theme/darkTheme", true);
+ window->get_style_context()->add_class("dark");
+ window->get_style_context()->remove_class("bright");
+ } else {
+ prefs->setBool("/theme/darkTheme", false);
+ window->get_style_context()->add_class("bright");
+ window->get_style_context()->remove_class("dark");
+ }
+ INKSCAPE.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;
+ if (dark) {
+ prefs->setBool("/theme/darkTheme", true);
+ window->get_style_context()->add_class("dark");
+ window->get_style_context()->remove_class("bright");
+ } else {
+ prefs->setBool("/theme/darkTheme", false);
+ window->get_style_context()->add_class("bright");
+ window->get_style_context()->remove_class("dark");
+ }
+ INKSCAPE.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."));
+
+ /* show infobox */
+ _show_filters_info_box.init( _("Show filter primitives infobox"), "/options/showfiltersinfobox/value", true);
+ _page_ui.add_line(false, "", _show_filters_info_box, "",
+ _("Show icons and descriptions for the filter primitives available at the filter effects dialog"), false, reset_icon());
+
+ _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);
+
+ _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);
+ _compact_colorselector.init(_("Use compact color selector mode switch"), "/colorselector/switcher", true);
+ _page_ui.add_line(false, "", _compact_colorselector, "", _("Use compact combo box for selecting color modes"), 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("/theme/fontscale", 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->adjust_global_font_scale(1.0);
+ });
+ apply->signal_clicked().connect([=](){
+ INKSCAPE.themecontext->adjust_global_font_scale(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());
+
+
+ this->AddPage(_page_theme, _("Theming"), iter_ui, PREFS_PAGE_UI_THEME);
+ symbolicThemeCheck();
+
+ // Toolbars
+ _page_toolbars.add_group_header(_("Toolbars"));
+ try {
+ auto builder = Gtk::Builder::create_from_file(get_filename(UIS, "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 path = ToolboxFactory::get_tool_visible_buttons_path(sp_get_action_target(button));
+ auto visible = Inkscape::Preferences::get()->getBool(path, true);
+ button->set_active(visible);
+ button->signal_toggled().connect([=](){
+ Inkscape::Preferences::get()->setBool(path, button->get_active());
+ });
+ }
+ 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") }
+ };
+ _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);
+
+ // Grids
+ _page_grids.add_group_header( _("Line color when zooming out"));
+
+ _grids_no_emphasize_on_zoom.init( _("Minor grid line color"), "/options/grids/no_emphasize_when_zoomedout", 1, true, nullptr);
+ _page_grids.add_line( true, "", _grids_no_emphasize_on_zoom, "", _("The gridlines will be shown in minor grid line color"), false);
+ _grids_emphasize_on_zoom.init( _("Major grid line color"), "/options/grids/no_emphasize_when_zoomedout", 0, false, &_grids_no_emphasize_on_zoom);
+ _page_grids.add_line( true, "", _grids_emphasize_on_zoom, "", _("The gridlines will be shown in major grid line color"), false);
+
+ _page_grids.add_group_header( _("Default grid settings"));
+
+ _page_grids.add_line( true, "", _grids_notebook, "", "", false);
+ _grids_notebook.append_page(_grids_xy, CanvasGrid::getName( GRID_RECTANGULAR ));
+ _grids_notebook.append_page(_grids_axonom, CanvasGrid::getName( GRID_AXONOMETRIC ));
+ _grids_xy_units.init("/options/grids/xy/units");
+ _grids_xy.add_line( false, _("Grid units:"), _grids_xy_units, "", "", false);
+ _grids_xy_origin_x.init("/options/grids/xy/origin_x", -10000.0, 10000.0, 0.1, 1.0, 0.0, false, false);
+ _grids_xy_origin_y.init("/options/grids/xy/origin_y", -10000.0, 10000.0, 0.1, 1.0, 0.0, false, false);
+ _grids_xy_origin_x.set_digits(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_COLOR);
+ _grids_xy.add_line( false, _("Minor grid line color:"), _grids_xy_color, "", _("Color used for normal grid lines"), false);
+ _grids_xy_empcolor.init(_("Major grid line color:"), "/options/grids/xy/empcolor", GRID_DEFAULT_EMPCOLOR);
+ _grids_xy.add_line( false, _("Major grid line color:"), _grids_xy_empcolor, "", _("Color used for major (highlighted) grid lines"), false);
+ _grids_xy_empspacing.init("/options/grids/xy/empspacing", 1.0, 1000.0, 1.0, 5.0, 5.0, true, false);
+ _grids_xy.add_line( false, _("Major grid line every:"), _grids_xy_empspacing, "", "", false);
+ _grids_xy_dotted.init( _("Show dots instead of lines"), "/options/grids/xy/dotted", false);
+ _grids_xy.add_line( false, "", _grids_xy_dotted, "", _("If set, display dots at gridpoints instead of gridlines"), false);
+
+ // CanvasAxonomGrid properties:
+ _grids_axonom_units.init("/options/grids/axonom/units");
+ _grids_axonom.add_line( false, _("Grid units:"), _grids_axonom_units, "", "", false);
+ _grids_axonom_origin_x.init("/options/grids/axonom/origin_x", -10000.0, 10000.0, 0.1, 1.0, 0.0, false, false);
+ _grids_axonom_origin_y.init("/options/grids/axonom/origin_y", -10000.0, 10000.0, 0.1, 1.0, 0.0, false, false);
+ _grids_axonom_origin_x.set_digits(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_COLOR);
+ _grids_axonom.add_line( false, _("Minor grid line color:"), _grids_axonom_color, "", _("Color used for normal grid lines"), false);
+ _grids_axonom_empcolor.init(_("Major grid line color:"), "/options/grids/axonom/empcolor", GRID_DEFAULT_EMPCOLOR);
+ _grids_axonom.add_line( false, _("Major grid line color:"), _grids_axonom_empcolor, "", _("Color used for major (highlighted) grid lines"), false);
+ _grids_axonom_empspacing.init("/options/grids/axonom/empspacing", 1.0, 1000.0, 1.0, 5.0, 5.0, true, false);
+ _grids_axonom.add_line( false, _("Major grid line every:"), _grids_axonom_empspacing, "", "", false);
+
+ this->AddPage(_page_grids, _("Grids"), iter_ui, PREFS_PAGE_UI_GRIDS);
+
+ // 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);
+
+ _cms_proof_preserveblack.init( _("Preserve black"), "/options/softproof/preserveblack", false);
+
+#if !defined(cmsFLAGS_PRESERVEBLACK)
+ _cms_proof_preserveblack.set_sensitive( false );
+#endif // !defined(cmsFLAGS_PRESERVEBLACK)
+
+
+ {
+ 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);
+
+ _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_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"));
+
+ //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 ungroup, clip/mask is preserved in childrens"), "/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);
+
+
+ _page_markers.add_group_header( _("Stroke Style Markers"));
+ _page_markers.add_line( true, "", _markers_color_stock, "",
+ _("Stroke color same as object, fill color either object fill color or marker fill color"));
+ _page_markers.add_line( true, "", _markers_color_custom, "",
+ _("Stroke color same as object, fill color either object fill color or marker fill color"));
+ _page_markers.add_line( true, "", _markers_color_update, "",
+ _("Update marker color when object color changes"));
+
+ this->AddPage(_page_markers, _("Markers"), iter_behavior, PREFS_PAGE_BEHAVIOR_MARKERS);
+
+
+ _page_cleanup.add_group_header( _("Document cleanup"));
+ _cleanup_swatches.init ( _("Remove unused swatches when doing a document cleanup"), "/options/cleanupswatches/value", false); // text label
+ _page_cleanup.add_line( true, "", _cleanup_swatches, "",
+ _("Remove unused swatches when doing a document cleanup")); // tooltip
+ this->AddPage(_page_cleanup, _("Cleanup"), iter_behavior, PREFS_PAGE_BEHAVIOR_CLEANUP);
+
+ _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()
+{
+ /* threaded blur */ //related comments/widgets/functions should be renamed and option should be moved elsewhere when inkscape is fully multi-threaded
+ _filter_multi_threaded.init("/options/threading/numthreads", 1.0, 8.0, 1.0, 2.0, 4.0, true, false);
+ _page_rendering.add_line( false, _("Number of _Threads:"), _filter_multi_threaded, "", _("Configure number of processors/threads to use when rendering filters"), false, reset_icon());
+
+ // rendering cache
+ _rendering_cache_size.init("/options/renderingcache/size", 0.0, 4096.0, 1.0, 32.0, 64.0, true, false);
+ _page_rendering.add_line( false, _("Rendering _cache size:"), _rendering_cache_size, C_("mebibyte (2^20 bytes) abbreviation","MiB"), _("Set the amount of memory per document which can be used to store rendered parts of the drawing for later reuse; set to zero to disable caching"), false);
+
+ // rendering tile multiplier
+ _rendering_tile_multiplier.init("/options/rendering/tile-multiplier", 1.0, 512.0, 1.0, 16.0, 16.0, true, false);
+ _page_rendering.add_line( false, _("Rendering tile multiplier:"), _rendering_tile_multiplier, "", _("On modern hardware, increasing this value (default is 16) can help to get a better performance when there are large areas with filtered objects (this includes blur and blend modes) in your drawing. Decrease the value to make zooming and panning in relevant areas faster on low-end hardware in drawings with few or no filters."), false);
+
+ // rendering 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", 1.0, 100.0, 1.0, 5.0, 50.0, true, false);
+ _rendering_outline_overlay_opacity.signal_focus_out_event().connect(sigc::mem_fun(*this, &InkscapePreferences::on_outline_overlay_changed));
+ _page_rendering.add_line( false, _("Outline overlay opacity:"), _rendering_outline_overlay_opacity, _("%"), _("Opacity of the color in outline overlay view mode"), false);
+
+ // update strategy
+ int values[] = {1, 2, 3};
+ Glib::ustring 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);
+
+ /* 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/render", false);
+ _page_rendering.add_line(false, "", _cairo_dithering, "", _("Makes gradients smoother. This can significantly impact the size of generated PNG files. To update the display after changing this option, just zoom out/in."));
+#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, const Glib::ustring &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, 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 != "") {
+ 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 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_render_time_limit.init("/options/rendering/render_time_limit", 100.0, 1000000.0, 1.0, 0.0, 1000.0, true, false);
+ add_devmode_line(_("Render time limit"), _canvas_render_time_limit, C_("microsecond abbreviation", "μs"), _("The maximum time allowed for a rendering time slice"));
+ _canvas_use_new_bisector.init("", "/options/rendering/use_new_bisector", true);
+ add_devmode_line(_("Use new bisector algorithm"), _canvas_use_new_bisector, "", _("Use an alternative, more obvious bisection strategy: just chop tile in half along the larger dimension until small enough"));
+ _canvas_new_bisector_size.init("/options/rendering/new_bisector_size", 1.0, 10000.0, 1.0, 0.0, 500.0, true, false);
+ add_devmode_line(_("Smallest tile size for new bisector"), _canvas_new_bisector_size, C_("pixel abbreviation", "px"), _("Halve rendering tile rectangles until their largest dimension is this small"));
+ _rendering_tile_size.init("/options/rendering/tile-size", 1.0, 10000.0, 1.0, 0.0, 16.0, true, false);
+ add_devmode_line(_("Tile size:"), _rendering_tile_size, "", _("The \"tile size\" parameter previously hard-coded into Inkscape's original tile bisector."));
+ _canvas_max_affine_diff.init("/options/rendering/max_affine_diff", 0.0, 100.0, 0.1, 0.0, 1.8, false, false);
+ add_devmode_line(_("Transformation threshold"), _canvas_max_affine_diff, "", _("How much the viewing transformation can change before throwing away the current redraw and starting again"));
+ _canvas_pad.init("/options/rendering/pad", 0.0, 1000.0, 1.0, 0.0, 200.0, true, false);
+ add_devmode_line(_("Buffer padding"), _canvas_pad, C_("pixel abbreviation", "px"), _("Do not throw away rendered content in this area around the window yet"));
+ _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_slow_redraw.init("", "/options/rendering/debug_slow_redraw", false);
+ add_devmode_line(_("Slow redraw"), _canvas_debug_slow_redraw, "", _("Introduce a fixed delay for each tile"));
+ _canvas_debug_slow_redraw_time.init("/options/rendering/debug_slow_redraw_time", 0.0, 1000000.0, 1.0, 0.0, 50.0, true, false);
+ add_devmode_line(_("Slow redraw time"), _canvas_debug_slow_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"));
+ _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"));
+ _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"));
+ _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"));
+
+ this->AddPage(_page_rendering, _("Rendering"), PREFS_PAGE_RENDERING);
+}
+
+void InkscapePreferences::initPageBitmaps()
+{
+ /* Note: /options/bitmapoversample removed with Cairo renderer */
+ _page_bitmaps.add_group_header( _("Edit"));
+ _misc_bitmap_autoreload.init(_("Automatically reload images"), "/options/bitmapautoreload/value", true);
+ _page_bitmaps.add_line( false, "", _misc_bitmap_autoreload, "",
+ _("Automatically reload linked images when file is changed on disk"));
+ _misc_bitmap_editor.init("/options/bitmapeditor/value", true);
+ _page_bitmaps.add_line( false, _("_Bitmap editor:"), _misc_bitmap_editor, "", "", true);
+ _misc_svg_editor.init("/options/svgeditor/value", true);
+ _page_bitmaps.add_line( false, _("_SVG editor:"), _misc_svg_editor, "", "", true);
+
+ _page_bitmaps.add_group_header( _("Export"));
+ _importexport_export_res.init("/dialogs/export/defaultxdpi/value", 0.0, 6000.0, 1.0, 1.0, Inkscape::Util::Quantity::convert(1, "in", "px"), true, false);
+ _page_bitmaps.add_line( false, _("Default export _resolution:"), _importexport_export_res, _("dpi"),
+ _("Default image resolution (in dots per inch) in the Export dialog"), false);
+ _page_bitmaps.add_group_header( _("Create"));
+ _bitmap_copy_res.init("/options/createbitmap/resolution", 1.0, 6000.0, 1.0, 1.0, Inkscape::Util::Quantity::convert(1, "in", "px"), true, false);
+ _page_bitmaps.add_line( false, _("Resolution for Create Bitmap _Copy:"), _bitmap_copy_res, _("dpi"),
+ _("Resolution used by the Create Bitmap Copy command"), false);
+
+ _page_bitmaps.add_group_header( _("Import"));
+ _bitmap_ask.init(_("Ask about linking and scaling when importing bitmap images"), "/dialogs/import/ask", true);
+ _page_bitmaps.add_line( true, "", _bitmap_ask, "",
+ _("Pop-up linking and scaling dialog when importing bitmap image."));
+ _svg_ask.init(_("Ask about linking and scaling when importing SVG images"), "/dialogs/import/ask_svg", true);
+ _page_bitmaps.add_line( true, "", _svg_ask, "",
+ _("Pop-up linking and scaling dialog when importing SVG image."));
+
+ _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);
+ _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();
+ 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();
+ }
+}
+
+bool InkscapePreferences::onKBSearchFilter(const Gtk::TreeModel::const_iterator& iter)
+{
+ Glib::ustring search = _kb_search.get_text().lowercase();
+ if (search.empty()) {
+ return TRUE;
+ }
+
+ Glib::ustring name = (*iter)[_kb_columns.name];
+ Glib::ustring desc = (*iter)[_kb_columns.description];
+ Glib::ustring shortcut = (*iter)[_kb_columns.shortcut];
+ Glib::ustring id = (*iter)[_kb_columns.id];
+
+ if (id.empty()) {
+ return TRUE; // Keep all group nodes visible
+ }
+
+ return (name.lowercase().find(search) != name.npos
+ || shortcut.lowercase().find(search) != name.npos
+ || desc.lowercase().find(search) != name.npos
+ || id.lowercase().find(search) != name.npos);
+}
+
+void InkscapePreferences::onKBRealize()
+{
+ if (!_kb_shortcuts_loaded /*&& _current_page == &_page_keyshortcuts*/) {
+ _kb_shortcuts_loaded = true;
+ onKBListKeyboardShortcuts();
+ }
+}
+
+InkscapePreferences::ModelColumns &InkscapePreferences::onKBGetCols()
+{
+ static InkscapePreferences::ModelColumns cols;
+ return cols;
+}
+
+void InkscapePreferences::onKBShortcutRenderer(Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter) {
+
+ Glib::ustring shortcut = (*iter)[onKBGetCols().shortcut];
+ unsigned int user_set = (*iter)[onKBGetCols().user_set];
+ Gtk::CellRendererAccel *accel = dynamic_cast<Gtk::CellRendererAccel *>(renderer);
+ if (user_set) {
+ accel->property_markup() = Glib::ustring("<span 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()
+{
+ _misc_latency_skew.init("/debug/latency/skew", 0.5, 2.0, 0.01, 0.10, 1.0, false, false);
+ _page_system.add_line( false, _("Latency _skew:"), _misc_latency_skew, "",
+ _("Factor by which the event clock is skewed from the actual time (0.9766 on some systems)"), false, reset_icon());
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ _misc_namedicon_delay.init( _("Pre-render named icons"), "/options/iconrender/named_nodelay", false);
+ _page_system.add_line( false, "", _misc_namedicon_delay, "",
+ _("When on, named icons will be rendered before displaying the ui. This is for working around bugs in GTK+ named icon notification"), true);
+
+ _page_system.add_group_header( _("System info"));
+
+ _sys_user_prefs.set_text(prefs->getPrefsFilename());
+ _sys_user_prefs.set_editable(false);
+ Gtk::Button* reset_prefs = Gtk::manage(new Gtk::Button(_("Reset Preferences")));
+ reset_prefs->signal_clicked().connect(sigc::mem_fun(*this, &InkscapePreferences::on_reset_prefs_clicked));
+
+ _page_system.add_line(true, _("User preferences:"), _sys_user_prefs, "",
+ _("Location of the user’s preferences file"), true, reset_prefs);
+
+ _sys_user_config.init((char const *)Inkscape::IO::Resource::profile_path(""), _("Open preferences folder"));
+ _page_system.add_line(true, _("User config:"), _sys_user_config, "", _("Location of users configuration"), true);
+
+ 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..87345a4
--- /dev/null
+++ b/src/ui/dialog/inkscape-preferences.h
@@ -0,0 +1,747 @@
+// 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
+#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_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_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() override;
+
+ static InkscapePreferences &getInstance() { return *new InkscapePreferences(); }
+ 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_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_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::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 _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_tile_multiplier;
+ UI::Widget::PrefSpinButton _rendering_xray_radius;
+ UI::Widget::PrefSpinButton _rendering_outline_overlay_opacity;
+ UI::Widget::PrefCombo _canvas_update_strategy;
+ 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_render_time_limit;
+ UI::Widget::PrefCheckButton _canvas_use_new_bisector;
+ UI::Widget::PrefSpinButton _canvas_new_bisector_size;
+ UI::Widget::PrefSpinButton _rendering_tile_size;
+ UI::Widget::PrefSpinButton _canvas_max_affine_diff;
+ UI::Widget::PrefSpinButton _canvas_pad;
+ 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_slow_redraw;
+ UI::Widget::PrefSpinButton _canvas_debug_slow_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 _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 _markers_color_stock;
+ UI::Widget::PrefCheckButton _markers_color_custom;
+ UI::Widget::PrefCheckButton _markers_color_update;
+
+ UI::Widget::PrefCheckButton _cleanup_swatches;
+
+ UI::Widget::PrefCheckButton _lpe_copy_mirroricons;
+
+ 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::PrefCheckButton _misc_gradient_collect;
+ UI::Widget::PrefCheckButton _misc_scripts;
+ UI::Widget::PrefCheckButton _misc_namedicon_delay;
+
+ // System page
+ UI::Widget::PrefSpinButton _misc_latency_skew;
+ UI::Widget::PrefSpinButton _misc_simpl;
+ Gtk::Entry _sys_user_prefs;
+ Gtk::Entry _sys_tmp_files;
+ Gtk::Entry _sys_extension_dir;
+ UI::Widget::PrefOpenFolder _sys_user_config;
+ UI::Widget::PrefOpenFolder _sys_user_extension_dir;
+ UI::Widget::PrefOpenFolder _sys_user_themes_dir;
+ UI::Widget::PrefOpenFolder _sys_user_ui_dir;
+ UI::Widget::PrefOpenFolder _sys_user_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;
+ 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_realworldzoom;
+ UI::Widget::PrefCheckButton _ui_partialdynamic;
+ UI::Widget::ZoomCorrRulerSlider _ui_zoom_correction;
+ UI::Widget::PrefCheckButton _show_filters_info_box;
+ 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;
+ UI::Widget::PrefCheckButton _cms_proof_preserveblack;
+
+ Gtk::Notebook _grids_notebook;
+ UI::Widget::PrefRadioButton _grids_no_emphasize_on_zoom;
+ UI::Widget::PrefRadioButton _grids_emphasize_on_zoom;
+ UI::Widget::DialogPage _grids_xy;
+ UI::Widget::DialogPage _grids_axonom;
+ // CanvasXYGrid properties:
+ UI::Widget::PrefUnit _grids_xy_units;
+ UI::Widget::PrefSpinButton _grids_xy_origin_x;
+ UI::Widget::PrefSpinButton _grids_xy_origin_y;
+ UI::Widget::PrefSpinButton _grids_xy_spacing_x;
+ UI::Widget::PrefSpinButton _grids_xy_spacing_y;
+ UI::Widget::PrefColorPicker _grids_xy_color;
+ UI::Widget::PrefColorPicker _grids_xy_empcolor;
+ UI::Widget::PrefSpinButton _grids_xy_empspacing;
+ UI::Widget::PrefCheckButton _grids_xy_dotted;
+ // CanvasAxonomGrid properties:
+ UI::Widget::PrefUnit _grids_axonom_units;
+ UI::Widget::PrefSpinButton _grids_axonom_origin_x;
+ UI::Widget::PrefSpinButton _grids_axonom_origin_y;
+ UI::Widget::PrefSpinButton _grids_axonom_spacing_y;
+ UI::Widget::PrefSpinButton _grids_axonom_angle_x;
+ UI::Widget::PrefSpinButton _grids_axonom_angle_z;
+ UI::Widget::PrefColorPicker _grids_axonom_color;
+ UI::Widget::PrefColorPicker _grids_axonom_empcolor;
+ UI::Widget::PrefSpinButton _grids_axonom_empspacing;
+
+ // SVG Output page:
+ UI::Widget::PrefCheckButton _svgoutput_usenamedcolors;
+ UI::Widget::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
+ */
+ 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();
+ 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);
+
+ bool on_outline_overlay_changed(GdkEventFocus * /* focus_event */);
+ std::map<Glib::ustring, bool> dark_themes;
+ InkscapePreferences();
+ InkscapePreferences(InkscapePreferences const &d);
+ InkscapePreferences operator=(InkscapePreferences const &d);
+ 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..df071c7
--- /dev/null
+++ b/src/ui/dialog/input.cpp
@@ -0,0 +1,1798 @@
+// 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;
+}
+
+
+// Now that we've defined the *Impl class, we can do the method to acquire one.
+InputDialog &InputDialog::getInstance()
+{
+ InputDialog *dialog = new InputDialogImpl();
+ return *dialog;
+}
+
+
+InputDialogImpl::InputDialogImpl() :
+ InputDialog(),
+ lastSourceSeen(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..90fb8a5
--- /dev/null
+++ b/src/ui/dialog/input.h
@@ -0,0 +1,45 @@
+// 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 "ui/dialog/dialog-base.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class InputDialog : public DialogBase
+{
+public:
+ static InputDialog &getInstance();
+
+ InputDialog() : DialogBase("/dialogs/inputdevices", "Input") {}
+ ~InputDialog() override = default;
+};
+
+} // namespace Dialog
+} // namesapce UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_INPUT_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/knot-properties.cpp b/src/ui/dialog/knot-properties.cpp
new file mode 100644
index 0000000..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..76a2778
--- /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->selection->toLayer(moveto);
+ }
+}
+
+/** 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] = SP_IS_ITEM(child) ? !SP_ITEM(child)->isHidden() : false;
+ row[_model->_colLocked] = SP_IS_ITEM(child) ? SP_ITEM(child)->isLocked() : false;
+
+ if (target && child == target) {
+ _tree.expand_to_path(_store->get_path(iter));
+ Glib::RefPtr<Gtk::TreeSelection> select = _tree.get_selection();
+ select->select(iter);
+ }
+
+ _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();
+ _close();
+ 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..b7a48e7
--- /dev/null
+++ b/src/ui/dialog/livepatheffect-add.cpp
@@ -0,0 +1,981 @@
+// 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 "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(Glib::ustring effect)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring favlist = prefs->getString("/dialogs/livepatheffect/favs");
+ size_t pos = favlist.find(effect);
+ if (pos != std::string::npos) {
+ return true;
+ }
+ return false;
+}
+
+void sp_add_fav(Glib::ustring effect)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring favlist = prefs->getString("/dialogs/livepatheffect/favs");
+ if (!sp_has_fav(effect)) {
+ prefs->setString("/dialogs/livepatheffect/favs", favlist + effect + ";");
+ }
+}
+
+void sp_remove_fav(Glib::ustring effect)
+{
+ if (sp_has_fav(effect)) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring favlist = prefs->getString("/dialogs/livepatheffect/favs");
+ effect += ";";
+ size_t pos = favlist.find(effect);
+ if (pos != std::string::npos) {
+ favlist.erase(pos, effect.length());
+ prefs->setString("/dialogs/livepatheffect/favs", favlist);
+ }
+ }
+}
+
+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);
+ const Glib::ustring label = g_dpgettext2(nullptr, "path effect", converter.get_label(data->id).c_str());
+ const Glib::ustring untranslated_label = converter.get_label(data->id);
+ if (untranslated_label == label) {
+ LPEName->set_text(label);
+ } else {
+ LPEName->set_markup((label + "\n<span size='x-small'>" + untranslated_label + "</span>").c_str());
+ }
+ Gtk::Label *LPEDescription;
+ builder_effect->get_widget("LPEDescription", LPEDescription);
+ const Glib::ustring description = _(converter.get_description(data->id).c_str());
+ LPEDescription->set_text(description);
+ Gtk::ToggleButton *LPEExperimentalToggle;
+ builder_effect->get_widget("LPEExperimentalToggle", LPEExperimentalToggle);
+ bool active = converter.get_experimental(data->id) ? true : false;
+ LPEExperimentalToggle->set_active(active);
+ Gtk::Image *LPEIcon;
+ builder_effect->get_widget("LPEIcon", LPEIcon);
+ LPEIcon->set_from_icon_name(converter.get_icon(data->id), Gtk::IconSize(Gtk::ICON_SIZE_DIALOG));
+ Gtk::EventBox *LPESelectorEffectEventInfo;
+ builder_effect->get_widget("LPESelectorEffectEventInfo", LPESelectorEffectEventInfo);
+ LPESelectorEffectEventInfo->signal_enter_notify_event().connect(sigc::bind<Glib::RefPtr<Gtk::Builder>>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::pop_description), builder_effect));
+ Gtk::EventBox *LPESelectorEffectEventFav;
+ builder_effect->get_widget("LPESelectorEffectEventFav", LPESelectorEffectEventFav);
+ Gtk::Image *fav = dynamic_cast<Gtk::Image *>(LPESelectorEffectEventFav->get_child());
+ if (sp_has_fav(LPEName->get_text())) {
+ fav->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ } else {
+ fav->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ }
+ Gtk::EventBox *LPESelectorEffectEventFavTop;
+ builder_effect->get_widget("LPESelectorEffectEventFavTop", LPESelectorEffectEventFavTop);
+ LPESelectorEffectEventFav->signal_button_press_event().connect(sigc::bind<Glib::RefPtr<Gtk::Builder>>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::fav_toggler), builder_effect));
+ LPESelectorEffectEventFavTop->signal_button_press_event().connect(sigc::bind<Glib::RefPtr<Gtk::Builder>>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::fav_toggler), builder_effect));
+ Gtk::Image *favtop = dynamic_cast<Gtk::Image *>(LPESelectorEffectEventFavTop->get_child());
+ if (sp_has_fav(LPEName->get_text())) {
+ favtop->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ } else {
+ favtop->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ }
+ Gtk::EventBox *LPESelectorEffectEventApply;
+ builder_effect->get_widget("LPESelectorEffectEventApply", LPESelectorEffectEventApply);
+ LPESelectorEffectEventApply->signal_button_press_event().connect(
+ sigc::bind<Glib::RefPtr<Gtk::Builder>, const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::apply), builder_effect, &converter.data(i)));
+ LPESelectorEffectEventApply->signal_enter_notify_event().connect(sigc::bind<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(LPESelectorEffectEventApply->gobj())));
+ LPESelectorEffectEventApply->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(LPESelectorEffectEventApply->gobj())));
+ Gtk::ButtonBox *LPESelectorButtonBox;
+ builder_effect->get_widget("LPESelectorButtonBox", LPESelectorButtonBox);
+ LPESelectorButtonBox->signal_enter_notify_event().connect(sigc::bind<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(LPESelectorEffect->gobj())));
+ LPESelectorButtonBox->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(LPESelectorEffect->gobj())));
+ LPESelectorEffect->signal_enter_notify_event().connect(sigc::bind<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(LPESelectorEffect->gobj())));
+ LPESelectorEffect->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(LPESelectorEffect->gobj())));
+ _LPESelectorFlowBox->insert(*LPESelectorEffect, i);
+ LPESelectorEffect->get_parent()->signal_key_press_event().connect(
+ sigc::bind<Glib::RefPtr<Gtk::Builder>, const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::on_press_enter), builder_effect, &converter.data(i)));
+ LPESelectorEffect->get_parent()->get_style_context()->add_class(
+ ("LPEIndex" + Glib::ustring::format(i)).c_str());
+ }
+ _LPESelectorFlowBox->set_activate_on_single_click(false);
+ _visiblelpe = _LPESelectorFlowBox->get_children().size();
+ _LPEInfo->set_visible(false);
+ _LPESelectorEffectRadioPackLess->signal_clicked().connect(
+ sigc::bind(sigc::mem_fun(*this, &LivePathEffectAdd::viewChanged), 0));
+ _LPESelectorEffectRadioPackMore->signal_clicked().connect(
+ sigc::bind(sigc::mem_fun(*this, &LivePathEffectAdd::viewChanged), 1));
+ _LPESelectorEffectRadioList->signal_clicked().connect(
+ sigc::bind(sigc::mem_fun(*this, &LivePathEffectAdd::viewChanged), 2));
+ _LPESelectorEffectEventFavShow->signal_enter_notify_event().connect(sigc::bind<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(_LPESelectorEffectEventFavShow->gobj())));
+ _LPESelectorEffectEventFavShow->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(_LPESelectorEffectEventFavShow->gobj())));
+ _LPESelectorEffectEventFavShow->signal_button_press_event().connect(
+ sigc::mem_fun(*this, &LivePathEffectAdd::show_fav_toggler));
+ _LPESelectorEffectInfoEventBox->signal_leave_notify_event().connect(
+ sigc::mem_fun(*this, &LivePathEffectAdd::hide_pop_description));
+ _LPESelectorEffectInfoEventBox->signal_enter_notify_event().connect(sigc::bind<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(_LPESelectorEffectInfoEventBox->gobj())));
+ _LPESelectorEffectInfoEventBox->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(_LPESelectorEffectInfoEventBox->gobj())));
+ _LPEExperimental->property_active().signal_changed().connect(
+ sigc::mem_fun(*this, &LivePathEffectAdd::reload_effect_list));
+ Gtk::Window *window = SP_ACTIVE_DESKTOP->getToplevel();
+ int width;
+ int height;
+ window->get_size(width, height);
+ _LPEDialogSelector->resize(std::min(width - 300, 1440), std::min(height - 300, 900));
+ _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::Image *LPESelectorEffectFav;
+ builder_effect->get_widget("LPESelectorEffectFav", LPESelectorEffectFav);
+ Gtk::Image *LPESelectorEffectFavTop;
+ builder_effect->get_widget("LPESelectorEffectFavTop", LPESelectorEffectFavTop);
+ Gtk::EventBox *LPESelectorEffectEventFavTop;
+ builder_effect->get_widget("LPESelectorEffectEventFavTop", LPESelectorEffectEventFavTop);
+ if (LPESelectorEffectFav && LPESelectorEffectEventFavTop) {
+ if (sp_has_fav(LPEName->get_text())) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ gint mode = prefs->getInt("/dialogs/livepatheffect/dialogmode", 0);
+ if (mode == 2) {
+ LPESelectorEffectEventFavTop->set_visible(true);
+ LPESelectorEffectEventFavTop->show();
+ } else {
+ LPESelectorEffectEventFavTop->set_visible(false);
+ LPESelectorEffectEventFavTop->hide();
+ }
+ LPESelectorEffectFavTop->set_from_icon_name("draw-star-outline",
+ Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ LPESelectorEffectFav->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ sp_remove_fav(LPEName->get_text());
+ LPESelectorEffect->get_parent()->get_style_context()->remove_class("lpefav");
+ LPESelectorEffect->get_parent()->get_style_context()->add_class("lpenormal");
+ LPESelectorEffect->get_parent()->get_style_context()->add_class("lpe");
+ if (_showfavs) {
+ reload_effect_list();
+ }
+ } else {
+ LPESelectorEffectEventFavTop->set_visible(true);
+ LPESelectorEffectEventFavTop->show();
+ LPESelectorEffectFavTop->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ LPESelectorEffectFav->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ sp_add_fav(LPEName->get_text());
+ LPESelectorEffect->get_parent()->get_style_context()->add_class("lpefav");
+ LPESelectorEffect->get_parent()->get_style_context()->remove_class("lpenormal");
+ LPESelectorEffect->get_parent()->get_style_context()->add_class("lpe");
+ }
+ }
+ return true;
+}
+
+bool LivePathEffectAdd::show_fav_toggler(GdkEventButton *evt)
+{
+ _showfavs = !_showfavs;
+ Gtk::Image *favimage = dynamic_cast<Gtk::Image *>(_LPESelectorEffectEventFavShow->get_child());
+ if (favimage) {
+ if (_showfavs) {
+ favimage->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ } else {
+ favimage->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ }
+ }
+ reload_effect_list();
+ return true;
+}
+
+bool LivePathEffectAdd::apply(GdkEventButton *evt, Glib::RefPtr<Gtk::Builder> builder_effect,
+ const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *to_add)
+{
+ _to_add = to_add;
+ Gtk::EventBox *LPESelectorEffect;
+ builder_effect->get_widget("LPESelectorEffect", LPESelectorEffect);
+ Gtk::FlowBoxChild *flowboxchild = dynamic_cast<Gtk::FlowBoxChild *>(LPESelectorEffect->get_parent());
+ _LPESelectorFlowBox->select_child(*flowboxchild);
+ if (flowboxchild && flowboxchild->get_style_context()->has_class("lpedisabled")) {
+ return true;
+ }
+ _applied = true;
+ _lasteffect = flowboxchild;
+ _LPEDialogSelector->response(Gtk::RESPONSE_APPLY);
+ _LPEDialogSelector->hide();
+ return true;
+}
+
+bool LivePathEffectAdd::on_press_enter(GdkEventKey *key, Glib::RefPtr<Gtk::Builder> builder_effect,
+ const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *to_add)
+{
+ if (key->keyval == 65293 || key->keyval == 65421) {
+ _to_add = to_add;
+ Gtk::EventBox *LPESelectorEffect;
+ builder_effect->get_widget("LPESelectorEffect", LPESelectorEffect);
+ Gtk::FlowBoxChild *flowboxchild = dynamic_cast<Gtk::FlowBoxChild *>(LPESelectorEffect->get_parent());
+ if (flowboxchild && flowboxchild->get_style_context()->has_class("lpedisabled")) {
+ return true;
+ }
+ _applied = true;
+ _lasteffect = flowboxchild;
+ _LPEDialogSelector->response(Gtk::RESPONSE_APPLY);
+ _LPEDialogSelector->hide();
+ return true;
+ }
+ return false;
+}
+
+bool LivePathEffectAdd::expand(GdkEventButton *evt, Glib::RefPtr<Gtk::Builder> builder_effect)
+{
+ Gtk::EventBox *LPESelectorEffect;
+ builder_effect->get_widget("LPESelectorEffect", LPESelectorEffect);
+ Gtk::FlowBoxChild *child = dynamic_cast<Gtk::FlowBoxChild *>(LPESelectorEffect->get_parent());
+ if (child) {
+ child->grab_focus();
+ }
+ return true;
+}
+
+
+
+bool LivePathEffectAdd::on_filter(Gtk::FlowBoxChild *child)
+{
+ std::vector<Glib::ustring> classes = child->get_style_context()->list_classes();
+ int pos = 0;
+ for (auto childclass : classes) {
+ size_t s = childclass.find("LPEIndex", 0);
+ if (s != -1) {
+ childclass = childclass.erase(0, 8);
+ pos = std::stoi(childclass.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]);
+ if (!sp_has_fav(lpename->get_text()) && _showfavs) {
+ return false;
+ }
+ Gtk::ToggleButton *experimental = dynamic_cast<Gtk::ToggleButton *>(contents[3]);
+ if (experimental) {
+ if (experimental->get_active() && !_LPEExperimental->get_active()) {
+ return false;
+ }
+ }
+ Gtk::Label *lpedesc = dynamic_cast<Gtk::Label *>(contents[2]);
+ if (lpedesc) {
+ size_t s = lpedesc->get_text().uppercase().find(_LPEFilter->get_text().uppercase(), 0);
+ if (s != -1) {
+ _visiblelpe++;
+ return true;
+ }
+ }
+ if (_LPEFilter->get_text().length() < 1) {
+ _visiblelpe++;
+ return true;
+ }
+ if (lpename) {
+ size_t s = lpename->get_text().uppercase().find(_LPEFilter->get_text().uppercase(), 0);
+ if (s != -1) {
+ _visiblelpe++;
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+}
+
+void LivePathEffectAdd::reload_effect_list()
+{
+ /* if(_LPEExperimental->get_active()) {
+ _LPEExperimental->get_style_context()->add_class("active");
+ } else {
+ _LPEExperimental->get_style_context()->remove_class("active");
+ } */
+ _visiblelpe = 0;
+ _LPESelectorFlowBox->invalidate_filter();
+ if (_showfavs) {
+ if (_visiblelpe == 0) {
+ _LPEInfo->set_text(_("You don't have any favorites yet. 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 name2 = "";
+ Gtk::EventBox *eventbox = dynamic_cast<Gtk::EventBox *>(child1->get_child());
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ gint mode = prefs->getInt("/dialogs/livepatheffect/dialogmode", 0);
+ if (mode == 2) {
+ eventbox->set_halign(Gtk::ALIGN_START);
+ } else {
+ eventbox->set_halign(Gtk::ALIGN_CENTER);
+ }
+ if (eventbox) {
+ Gtk::Box *box = dynamic_cast<Gtk::Box *>(eventbox->get_child());
+ if (mode == 2) {
+ box->set_orientation(Gtk::ORIENTATION_HORIZONTAL);
+ } else {
+ box->set_orientation(Gtk::ORIENTATION_VERTICAL);
+ }
+ if (box) {
+ std::vector<Gtk::Widget *> contents = box->get_children();
+ Gtk::Label *lpename = dynamic_cast<Gtk::Label *>(contents[1]);
+ name1 = lpename->get_text();
+ if (lpename) {
+ if (mode == 2) {
+ lpename->set_justify(Gtk::JUSTIFY_LEFT);
+ lpename->set_halign(Gtk::ALIGN_START);
+ lpename->set_valign(Gtk::ALIGN_CENTER);
+ lpename->set_width_chars(-1);
+ lpename->set_max_width_chars(-1);
+ } else {
+ lpename->set_justify(Gtk::JUSTIFY_CENTER);
+ lpename->set_halign(Gtk::ALIGN_CENTER);
+ lpename->set_valign(Gtk::ALIGN_CENTER);
+ lpename->set_width_chars(14);
+ lpename->set_max_width_chars(23);
+ }
+ }
+ Gtk::EventBox *lpemore = dynamic_cast<Gtk::EventBox *>(contents[4]);
+ if (lpemore) {
+ if (mode == 2) {
+ lpemore->hide();
+ } else {
+ if (child1->is_selected()) {
+ lpemore->hide();
+ } else {
+ lpemore->show();
+ }
+ }
+ }
+ Gtk::ButtonBox *lpebuttonbox = dynamic_cast<Gtk::ButtonBox *>(contents[5]);
+ if (lpebuttonbox) {
+ if (mode == 2) {
+ lpebuttonbox->hide();
+ } else {
+ if (child1->is_selected()) {
+ lpebuttonbox->show();
+ } else {
+ lpebuttonbox->hide();
+ }
+ }
+ }
+ Gtk::Label *lpedesc = dynamic_cast<Gtk::Label *>(contents[2]);
+ if (lpedesc) {
+ if (mode == 2) {
+ lpedesc->show();
+ lpedesc->set_justify(Gtk::JUSTIFY_LEFT);
+ lpedesc->set_halign(Gtk::ALIGN_START);
+ lpedesc->set_valign(Gtk::ALIGN_CENTER);
+ lpedesc->set_ellipsize(Pango::ELLIPSIZE_END);
+ } else {
+ lpedesc->hide();
+ lpedesc->set_justify(Gtk::JUSTIFY_CENTER);
+ lpedesc->set_halign(Gtk::ALIGN_CENTER);
+ lpedesc->set_valign(Gtk::ALIGN_CENTER);
+ lpedesc->set_ellipsize(Pango::ELLIPSIZE_NONE);
+ }
+ }
+ Gtk::Overlay *overlay = dynamic_cast<Gtk::Overlay *>(contents[0]);
+ if (overlay) {
+ std::vector<Gtk::Widget *> contents_overlay = overlay->get_children();
+ Gtk::Image *icon = dynamic_cast<Gtk::Image *>(contents_overlay[0]);
+ if (icon) {
+ if (mode == 2) {
+ icon->set_pixel_size(40);
+ icon->set_margin_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(name1)) {
+ fav->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ LPESelectorEffectEventFavTop->set_visible(true);
+ LPESelectorEffectEventFavTop->show();
+ child1->get_style_context()->add_class("lpefav");
+ child1->get_style_context()->remove_class("lpenormal");
+ } else if (!sp_has_fav(name1)) {
+ fav->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ LPESelectorEffectEventFavTop->set_visible(false);
+ LPESelectorEffectEventFavTop->hide();
+ child1->get_style_context()->remove_class("lpefav");
+ child1->get_style_context()->add_class("lpenormal");
+ }
+ if (mode == 2) {
+ LPESelectorEffectEventFavTop->set_visible(true);
+ LPESelectorEffectEventFavTop->show();
+ LPESelectorEffectEventFavTop->set_halign(Gtk::ALIGN_END);
+ LPESelectorEffectEventFavTop->set_valign(Gtk::ALIGN_CENTER);
+ } else {
+ LPESelectorEffectEventFavTop->set_halign(Gtk::ALIGN_END);
+ LPESelectorEffectEventFavTop->set_valign(Gtk::ALIGN_START);
+ }
+ child1->get_style_context()->add_class("lpe");
+ }
+ }
+ }
+ }
+ eventbox = dynamic_cast<Gtk::EventBox *>(child2->get_child());
+ if (mode == 2) {
+ eventbox->set_halign(Gtk::ALIGN_START);
+ } else {
+ eventbox->set_halign(Gtk::ALIGN_CENTER);
+ }
+ if (eventbox) {
+ Gtk::Box *box = dynamic_cast<Gtk::Box *>(eventbox->get_child());
+ if (mode == 2) {
+ box->set_orientation(Gtk::ORIENTATION_HORIZONTAL);
+ } else {
+ box->set_orientation(Gtk::ORIENTATION_VERTICAL);
+ }
+ if (box) {
+ std::vector<Gtk::Widget *> contents = box->get_children();
+ Gtk::Label *lpename = dynamic_cast<Gtk::Label *>(contents[1]);
+ name2 = lpename->get_text();
+ if (lpename) {
+ if (mode == 2) {
+ lpename->set_justify(Gtk::JUSTIFY_LEFT);
+ lpename->set_halign(Gtk::ALIGN_START);
+ lpename->set_valign(Gtk::ALIGN_CENTER);
+ lpename->set_width_chars(-1);
+ lpename->set_max_width_chars(-1);
+ } else {
+ lpename->set_justify(Gtk::JUSTIFY_CENTER);
+ lpename->set_halign(Gtk::ALIGN_CENTER);
+ lpename->set_valign(Gtk::ALIGN_CENTER);
+ lpename->set_width_chars(14);
+ lpename->set_max_width_chars(23);
+ }
+ }
+ Gtk::EventBox *lpemore = dynamic_cast<Gtk::EventBox *>(contents[4]);
+ if (lpemore) {
+ if (mode == 2) {
+ lpemore->hide();
+ } else {
+ if (child2->is_selected()) {
+ lpemore->hide();
+ } else {
+ lpemore->show();
+ }
+ }
+ }
+ Gtk::ButtonBox *lpebuttonbox = dynamic_cast<Gtk::ButtonBox *>(contents[5]);
+ if (lpebuttonbox) {
+ if (mode == 2) {
+ lpebuttonbox->hide();
+ } else {
+ if (child2->is_selected()) {
+ lpebuttonbox->show();
+ } else {
+ lpebuttonbox->hide();
+ }
+ }
+ }
+ Gtk::Label *lpedesc = dynamic_cast<Gtk::Label *>(contents[2]);
+ if (lpedesc) {
+ if (mode == 2) {
+ lpedesc->show();
+ lpedesc->set_justify(Gtk::JUSTIFY_LEFT);
+ lpedesc->set_halign(Gtk::ALIGN_START);
+ lpedesc->set_valign(Gtk::ALIGN_CENTER);
+ lpedesc->set_ellipsize(Pango::ELLIPSIZE_END);
+ } else {
+ lpedesc->hide();
+ lpedesc->set_justify(Gtk::JUSTIFY_CENTER);
+ lpedesc->set_halign(Gtk::ALIGN_CENTER);
+ lpedesc->set_valign(Gtk::ALIGN_CENTER);
+ lpedesc->set_ellipsize(Pango::ELLIPSIZE_NONE);
+ }
+ }
+ Gtk::Overlay *overlay = dynamic_cast<Gtk::Overlay *>(contents[0]);
+ if (overlay) {
+ std::vector<Gtk::Widget *> contents_overlay = overlay->get_children();
+ Gtk::Image *icon = dynamic_cast<Gtk::Image *>(contents_overlay[0]);
+ if (icon) {
+ if (mode == 2) {
+ icon->set_pixel_size(33);
+ icon->set_margin_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(name2)) {
+ fav->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ LPESelectorEffectEventFavTop->set_visible(true);
+ LPESelectorEffectEventFavTop->show();
+ child2->get_style_context()->add_class("lpefav");
+ child2->get_style_context()->remove_class("lpenormal");
+ } else if (!sp_has_fav(name2)) {
+ fav->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ LPESelectorEffectEventFavTop->set_visible(false);
+ LPESelectorEffectEventFavTop->hide();
+ child2->get_style_context()->remove_class("lpefav");
+ child2->get_style_context()->add_class("lpenormal");
+ }
+ if (mode == 2) {
+ LPESelectorEffectEventFavTop->set_visible(true);
+ LPESelectorEffectEventFavTop->show();
+ LPESelectorEffectEventFavTop->set_halign(Gtk::ALIGN_END);
+ LPESelectorEffectEventFavTop->set_valign(Gtk::ALIGN_CENTER);
+ } else {
+ LPESelectorEffectEventFavTop->set_halign(Gtk::ALIGN_END);
+ LPESelectorEffectEventFavTop->set_valign(Gtk::ALIGN_START);
+ }
+ child2->get_style_context()->add_class("lpe");
+ }
+ }
+ }
+ }
+ std::vector<Glib::ustring> effect;
+ effect.push_back(name1);
+ effect.push_back(name2);
+ sort(effect.begin(), effect.end());
+ /* if (sp_has_fav(name1) && sp_has_fav(name2)) {
+ return effect[0] == name1?-1:1;
+ }
+ if (sp_has_fav(name1)) {
+ return -1;
+ } */
+ if (effect[0] == name1) { //&& !sp_has_fav(name2)) {
+ return -1;
+ }
+ return 1;
+}
+
+
+void LivePathEffectAdd::onClose() { _LPEDialogSelector->hide(); }
+
+void LivePathEffectAdd::onKeyEvent(GdkEventKey *evt)
+{
+ if (evt->keyval == GDK_KEY_Escape) {
+ onClose();
+ }
+}
+
+void LivePathEffectAdd::show(SPDesktop *desktop)
+{
+ LivePathEffectAdd &dial = instance();
+ Inkscape::Selection *sel = desktop->getSelection();
+ if (sel && !sel->isEmpty()) {
+ SPItem *item = sel->singleItem();
+ if (item) {
+ SPShape *shape = dynamic_cast<SPShape *>(item);
+ SPPath *path = dynamic_cast<SPPath *>(item);
+ SPGroup *group = dynamic_cast<SPGroup *>(item);
+ dial._has_clip = (item->getClipObject() != nullptr);
+ dial._has_mask = (item->getMaskObject() != nullptr);
+ dial._item_type = "";
+ if (group) {
+ dial._item_type = "group";
+ } else if (path) {
+ dial._item_type = "path";
+ } else if (shape) {
+ dial._item_type = "shape";
+ } else {
+ dial._LPEDialogSelector->hide();
+ return;
+ }
+ }
+ }
+ dial._applied = false;
+ dial._LPESelectorFlowBox->unset_sort_func();
+ dial._LPESelectorFlowBox->unset_filter_func();
+ dial._LPESelectorFlowBox->set_filter_func(sigc::mem_fun(dial, &LivePathEffectAdd::on_filter));
+ dial._LPESelectorFlowBox->set_sort_func(sigc::mem_fun(dial, &LivePathEffectAdd::on_sort));
+ Glib::RefPtr<Gtk::Adjustment> vadjust = dial._LPEScrolled->get_vadjustment();
+ vadjust->set_value(vadjust->get_lower());
+ 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..9705dc8
--- /dev/null
+++ b/src/ui/dialog/livepatheffect-editor.cpp
@@ -0,0 +1,639 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Live Path Effect editing dialog - implementation.
+ */
+/* Authors:
+ * Johan Engelen <j.b.c.engelen@utwente.nl>
+ * Steren Giannini <steren.giannini@gmail.com>
+ * Bastien Bouclet <bgkweb@gmail.com>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2007 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "livepatheffect-editor.h"
+
+#include <gtkmm/expander.h>
+
+#include "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "livepatheffect-add.h"
+#include "path-chemistry.h"
+#include "selection-chemistry.h"
+#include "svg/svg.h"
+#include "ui/icon-loader.h"
+
+#include "live_effects/effect.h"
+#include "live_effects/lpeobject-reference.h"
+#include "live_effects/lpeobject.h"
+
+#include "object/sp-item-group.h"
+#include "object/sp-path.h"
+#include "object/sp-use.h"
+#include "object/sp-symbol.h"
+#include "object/sp-text.h"
+
+#include "ui/icon-names.h"
+#include "ui/tools/node-tool.h"
+#include "ui/widget/imagetoggler.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+
+/*####################
+ * Callback functions
+ */
+
+
+void LivePathEffectEditor::selectionChanged(Inkscape::Selection * selection)
+{
+ selection_changed_lock = true;
+ lpe_list_locked = false;
+ onSelectionChanged(selection);
+ _on_button_release(nullptr); //to force update widgets
+ selection_changed_lock = false;
+}
+
+void LivePathEffectEditor::selectionModified(Inkscape::Selection * selection, guint flags)
+{
+ lpe_list_locked = false;
+ onSelectionChanged(selection);
+}
+
+static void lpe_style_button(Gtk::Button& btn, char const* iconName)
+{
+ GtkWidget *child = sp_get_icon_image(iconName, GTK_ICON_SIZE_SMALL_TOOLBAR);
+ gtk_widget_show( child );
+ btn.add(*Gtk::manage(Glib::wrap(child)));
+ btn.set_relief(Gtk::RELIEF_NONE);
+}
+
+
+/*
+ * LivePathEffectEditor
+ *
+ * TRANSLATORS: this dialog is accessible via menu Path - Path Effect Editor...
+ *
+ */
+
+LivePathEffectEditor::LivePathEffectEditor()
+ : DialogBase("/dialogs/livepatheffect", "LivePathEffect")
+ , lpe_list_locked(false)
+ , effectwidget(nullptr)
+ , status_label("", Gtk::ALIGN_CENTER)
+ , effectcontrol_frame("")
+ , button_add()
+ , button_remove()
+ , button_original()
+ , button_up()
+ , button_down()
+ , current_lpeitem(nullptr)
+ , current_lperef(nullptr)
+ , effectcontrol_vbox(Gtk::ORIENTATION_VERTICAL)
+ , effectlist_vbox(Gtk::ORIENTATION_VERTICAL)
+ , effectapplication_hbox(Gtk::ORIENTATION_HORIZONTAL, 4)
+{
+ set_spacing(4);
+
+ //Add the TreeView, inside a ScrolledWindow, with the button underneath:
+ scrolled_window.add(effectlist_view);
+ scrolled_window.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ scrolled_window.set_shadow_type(Gtk::SHADOW_IN);
+ scrolled_window.set_size_request(210, 70);
+ fix_inner_scroll(&scrolled_window);
+
+ effectcontrol_vbox.set_spacing(4);
+
+ effectlist_vbox.pack_start(scrolled_window, Gtk::PACK_EXPAND_WIDGET);
+ effectlist_vbox.pack_end(toolbar_hbox, Gtk::PACK_SHRINK);
+ effectcontrol_eventbox.add_events(Gdk::BUTTON_RELEASE_MASK);
+ effectcontrol_eventbox.signal_button_release_event().connect(sigc::mem_fun(*this, &LivePathEffectEditor::_on_button_release) );
+ effectcontrol_eventbox.add(effectcontrol_vbox);
+ effectcontrol_frame.add(effectcontrol_eventbox);
+
+ button_add.set_tooltip_text(_("Add path effect"));
+ lpe_style_button(button_add, INKSCAPE_ICON("list-add"));
+ button_add.set_relief(Gtk::RELIEF_NONE);
+
+ button_remove.set_tooltip_text(_("Delete current path effect"));
+ lpe_style_button(button_remove, INKSCAPE_ICON("list-remove"));
+ button_remove.set_relief(Gtk::RELIEF_NONE);
+
+ button_original.set_tooltip_text(_("Select origin item"));
+ lpe_style_button(button_original, INKSCAPE_ICON("clone-original"));
+ button_original.set_relief(Gtk::RELIEF_NONE);
+
+ button_up.set_tooltip_text(_("Raise the current path effect"));
+ lpe_style_button(button_up, INKSCAPE_ICON("go-up"));
+ button_up.set_relief(Gtk::RELIEF_NONE);
+
+ button_down.set_tooltip_text(_("Lower the current path effect"));
+ lpe_style_button(button_down, INKSCAPE_ICON("go-down"));
+ button_down.set_relief(Gtk::RELIEF_NONE);
+
+ // Add toolbar items to toolbar
+ toolbar_hbox.set_layout (Gtk::BUTTONBOX_END);
+ toolbar_hbox.add( button_add );
+ toolbar_hbox.set_child_secondary( button_add , true);
+ toolbar_hbox.add( button_remove );
+ toolbar_hbox.set_child_secondary( button_remove , true);
+ toolbar_hbox.add(button_original);
+ toolbar_hbox.add( button_up );
+ toolbar_hbox.add( button_down );
+ toolbar_hbox.set_child_non_homogeneous (button_add,true);
+ toolbar_hbox.set_child_non_homogeneous (button_remove,true);
+ toolbar_hbox.set_child_non_homogeneous (button_up,true);
+ toolbar_hbox.set_child_non_homogeneous (button_down,true);
+ toolbar_hbox.set_child_non_homogeneous(button_original, true);
+
+ //Create the Tree model:
+ effectlist_store = Gtk::ListStore::create(columns);
+ effectlist_view.set_model(effectlist_store);
+ effectlist_view.set_headers_visible(false);
+
+ // Handle tree selections
+ effectlist_selection = effectlist_view.get_selection();
+ effectlist_selection->signal_changed().connect( sigc::mem_fun(*this, &LivePathEffectEditor::on_effect_selection_changed) );
+
+ //Add the visibility icon column:
+ Inkscape::UI::Widget::ImageToggler *eyeRenderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler(
+ INKSCAPE_ICON("object-visible"), INKSCAPE_ICON("object-hidden")) );
+ int visibleColNum = effectlist_view.append_column("is_visible", *eyeRenderer) - 1;
+ eyeRenderer->signal_toggled().connect( sigc::mem_fun(*this, &LivePathEffectEditor::on_visibility_toggled) );
+ eyeRenderer->property_activatable() = true;
+ Gtk::TreeViewColumn* col = effectlist_view.get_column(visibleColNum);
+ if ( col ) {
+ col->add_attribute( eyeRenderer->property_active(), columns.col_visible );
+ }
+
+ //Add the effect name column:
+ effectlist_view.append_column("Effect", columns.col_name);
+
+ pack_start(effectlist_vbox, true, true);
+ pack_start(status_label, false, false);
+ pack_start(effectcontrol_frame, false, false);
+
+ effectcontrol_frame.hide();
+ selection_changed_lock = false;
+ // connect callback functions to buttons
+ button_add.signal_clicked().connect(sigc::mem_fun(*this, &LivePathEffectEditor::onAdd));
+ button_remove.signal_clicked().connect(sigc::mem_fun(*this, &LivePathEffectEditor::onRemove));
+ button_original.signal_clicked().connect(sigc::mem_fun(*this, &LivePathEffectEditor::onOriginal));
+ button_up.signal_clicked().connect(sigc::mem_fun(*this, &LivePathEffectEditor::onUp));
+ button_down.signal_clicked().connect(sigc::mem_fun(*this, &LivePathEffectEditor::onDown));
+
+ show_all_children();
+}
+
+LivePathEffectEditor::~LivePathEffectEditor()
+{
+ if (effectwidget) {
+ effectcontrol_vbox.remove(*effectwidget);
+ delete effectwidget;
+ effectwidget = nullptr;
+ }
+}
+
+bool LivePathEffectEditor::_on_button_release(GdkEventButton* button_event) {
+ Glib::RefPtr<Gtk::TreeSelection> sel = effectlist_view.get_selection();
+ if (sel->count_selected_rows () == 0) {
+ return true;
+ }
+ Gtk::TreeModel::iterator it = sel->get_selected();
+ std::shared_ptr<LivePathEffect::LPEObjectReference> lperef = (*it)[columns.lperef];
+ if (lperef && current_lpeitem && current_lperef != lperef) {
+ if (lperef->getObject()) {
+ LivePathEffect::Effect * effect = lperef->lpeobject->get_lpe();
+ if (effect) {
+ effect->refresh_widgets = true;
+ showParams(*effect);
+ }
+ }
+ }
+ return true;
+}
+
+void
+LivePathEffectEditor::showParams(LivePathEffect::Effect& effect)
+{
+ if (effectwidget && !effect.refresh_widgets) {
+ return;
+ }
+ if (effectwidget) {
+ effectcontrol_vbox.remove(*effectwidget);
+ delete effectwidget;
+ effectwidget = nullptr;
+ }
+ effectwidget = effect.newWidget();
+ effectcontrol_frame.set_label(effect.getName());
+ effectcontrol_vbox.pack_start(*effectwidget, true, true);
+
+ button_remove.show();
+ status_label.hide();
+ effectcontrol_frame.show();
+ effectcontrol_vbox.show_all_children();
+ // fixme: add resizing of dialog
+ effect.refresh_widgets = false;
+}
+
+void
+LivePathEffectEditor::selectInList(LivePathEffect::Effect* effect)
+{
+ Gtk::TreeNodeChildren chi = effectlist_view.get_model()->children();
+ for (Gtk::TreeIter ci = chi.begin() ; ci != chi.end(); ci++) {
+ if (ci->get_value(columns.lperef)->lpeobject->get_lpe() == effect && effectlist_view.get_selection()) {
+ effectlist_view.get_selection()->select(ci);
+ break;
+ }
+ }
+}
+
+
+void
+LivePathEffectEditor::showText(Glib::ustring const &str)
+{
+ if (effectwidget) {
+ effectcontrol_vbox.remove(*effectwidget);
+ delete effectwidget;
+ effectwidget = nullptr;
+ }
+
+ status_label.show();
+ status_label.set_label(str);
+
+ effectcontrol_frame.hide();
+
+ // fixme: do resizing of dialog ?
+}
+
+void
+LivePathEffectEditor::set_sensitize_all(bool sensitive)
+{
+ //combo_effecttype.set_sensitive(sensitive);
+ button_add.set_sensitive(sensitive);
+ button_remove.set_sensitive(sensitive);
+ effectlist_view.set_sensitive(sensitive);
+ button_up.set_sensitive(sensitive);
+ button_down.set_sensitive(sensitive);
+}
+
+void
+LivePathEffectEditor::onSelectionChanged(Inkscape::Selection *sel)
+{
+ if (lpe_list_locked) {
+ // this was triggered by selecting a row in the list, so skip reloading
+ lpe_list_locked = false;
+ return;
+ }
+ current_lpeitem = nullptr;
+ effectlist_store->clear();
+ button_original.set_sensitive(false);
+ if ( sel && !sel->isEmpty() ) {
+ SPItem *item = sel->singleItem();
+ if ( item ) {
+ button_original.set_sensitive(true);
+ SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item);
+ if ( lpeitem ) {
+ effect_list_reload(lpeitem);
+ current_lpeitem = lpeitem;
+ set_sensitize_all(true);
+ if ( lpeitem->hasPathEffect() ) {
+ Inkscape::LivePathEffect::Effect *lpe = lpeitem->getCurrentLPE();
+ if (lpe) {
+ showParams(*lpe);
+ lpe_list_locked = true;
+ selectInList(lpe);
+ } else {
+ showText(_("Unknown effect is applied"));
+ }
+ } else {
+ showText(_("Click button to add an effect"));
+ button_remove.set_sensitive(false);
+ button_up.set_sensitive(false);
+ button_down.set_sensitive(false);
+ }
+ } else {
+ SPUse *use = dynamic_cast<SPUse *>(item);
+ if (use) {
+ // test whether linked object is supported by the CLONE_ORIGINAL LPE
+ SPItem *root = use->root();
+ SPItem *orig = use->get_original();
+ if (dynamic_cast<SPSymbol *>(root)) {
+ showText(_("Path effect cannot be applied to symbols"));
+ set_sensitize_all(false);
+ } else if (dynamic_cast<SPShape *>(orig) || dynamic_cast<SPGroup *>(orig) ||
+ dynamic_cast<SPText *>(orig)) {
+ // Note that an SP_USE cannot have an LPE applied, so we only need to worry about the "add
+ // effect" case.
+ set_sensitize_all(true);
+ showText(_("Click add button to convert clone"));
+ button_remove.set_sensitive(false);
+ button_up.set_sensitive(false);
+ button_down.set_sensitive(false);
+ } else {
+ showText(_("Select a path or shape"));
+ set_sensitize_all(false);
+ }
+ } else {
+ showText(_("Select a path or shape"));
+ set_sensitize_all(false);
+ }
+ }
+ } else {
+ showText(_("Only one item can be selected"));
+ set_sensitize_all(false);
+ }
+ } else {
+ showText(_("Select a path or shape"));
+ set_sensitize_all(false);
+ }
+}
+
+/*
+ * First clears the effectlist_store, then appends all effects from the effectlist.
+ */
+void
+LivePathEffectEditor::effect_list_reload(SPLPEItem *lpeitem)
+{
+ effectlist_store->clear();
+
+ PathEffectList effectlist = lpeitem->getEffectList();
+ PathEffectList::iterator it;
+ for( it = effectlist.begin() ; it!=effectlist.end(); ++it)
+ {
+ if ( !(*it)->lpeobject ) {
+ continue;
+ }
+
+ if ((*it)->lpeobject->get_lpe()) {
+ Gtk::TreeModel::Row row = *(effectlist_store->append());
+ row[columns.col_name] = (*it)->lpeobject->get_lpe()->getName();
+ row[columns.lperef] = *it;
+ row[columns.col_visible] = (*it)->lpeobject->get_lpe()->isVisible();
+ } else {
+ Gtk::TreeModel::Row row = *(effectlist_store->append());
+ row[columns.col_name] = _("Unknown effect");
+ row[columns.lperef] = *it;
+ row[columns.col_visible] = false;
+ }
+ }
+}
+
+/*########################################################################
+# BUTTON CLICK HANDLERS (callbacks)
+########################################################################*/
+
+// TODO: factor out the effect applying code which can be called from anywhere. (selection-chemistry.cpp also needs it)
+void LivePathEffectEditor::onAdd()
+{
+ auto selection = getSelection();
+ if (selection && !selection->isEmpty() ) {
+ SPItem *item = selection->singleItem();
+ if (item) {
+ if ( dynamic_cast<SPLPEItem *>(item) ) {
+ // show effectlist dialog
+ using Inkscape::UI::Dialog::LivePathEffectAdd;
+ LivePathEffectAdd::show(getDesktop());
+ if ( !LivePathEffectAdd::isApplied()) {
+ return;
+ }
+
+ const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *data =
+ LivePathEffectAdd::getActiveData();
+ if (!data) {
+ return;
+ }
+ item = selection->singleItem(); // get new item
+
+ LivePathEffect::Effect::createAndApply(data->key.c_str(), getDocument(), item);
+ DocumentUndo::done(getDocument(), _("Create and apply path effect"), INKSCAPE_ICON("dialog-path-effects"));
+
+ lpe_list_locked = false;
+ onSelectionChanged(selection);
+ } else {
+ SPUse *use = dynamic_cast<SPUse *>(item);
+ if ( use ) {
+ // item is a clone. do not show effectlist dialog.
+ // convert to path, apply CLONE_ORIGINAL LPE, link it to the cloned path
+
+ // test whether linked object is supported by the CLONE_ORIGINAL LPE
+ SPItem *orig = use->get_original();
+ if ( dynamic_cast<SPShape *>(orig) ||
+ dynamic_cast<SPGroup *>(orig) ||
+ dynamic_cast<SPText *>(orig) )
+ {
+ // select original
+ selection->set(orig);
+
+ // delete clone but remember its id and transform
+ gchar *id = g_strdup(item->getAttribute("id"));
+ gchar *transform = g_strdup(item->getAttribute("transform"));
+ item->deleteObject(false);
+ item = nullptr;
+
+ // run sp_selection_clone_original_path_lpe
+ selection->cloneOriginalPathLPE(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", id);
+ if (transform) {
+ Geom::Affine item_t(Geom::identity());
+ sp_svg_transform_read(transform, &item_t);
+ new_item->transform *= item_t;
+ new_item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ }
+ new_item->setAttribute("class", "fromclone");
+ }
+ g_free(id);
+ g_free(transform);
+
+ /// \todo Add the LPE stack of the original path?
+
+ DocumentUndo::done(getDocument(), _("Create and apply Clone original path effect"), INKSCAPE_ICON("dialog-path-effects"));
+
+ lpe_list_locked = false;
+ onSelectionChanged(selection);
+ }
+ }
+ }
+ }
+ }
+}
+
+void
+LivePathEffectEditor::onRemove()
+{
+ auto selection = getSelection();
+ if (selection && !selection->isEmpty() ) {
+ SPItem *item = selection->singleItem();
+ SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item);
+ if (lpeitem) {
+ sp_lpe_item_update_patheffect(lpeitem, false, false);
+ lpeitem->removeCurrentPathEffect(false);
+ current_lperef = nullptr;
+ DocumentUndo::done(getDocument(), _("Remove path effect"), INKSCAPE_ICON("dialog-path-effects"));
+ lpe_list_locked = false;
+ onSelectionChanged(selection);
+ }
+ }
+
+}
+
+gboolean removeselectclass(gpointer data)
+{
+ SPItem *item = reinterpret_cast<SPItem *>(data);
+ const gchar *classitem = item->getAttribute("class");
+ if (classitem) {
+ Glib::ustring classtoparent = classitem;
+ classtoparent.erase(classtoparent.find("lpeselectparent "), 16);
+ if (classtoparent.empty()) {
+ item->setAttribute("class", nullptr);
+ } else {
+ item->setAttribute("class", classtoparent.c_str());
+ }
+ }
+ return FALSE;
+}
+
+void LivePathEffectEditor::onOriginal()
+{
+ auto selection = getSelection();
+ if (selection && !selection->isEmpty()) {
+ if (SPItem *item = selection->singleItem()) {
+ const gchar *classtoparentchar = item->getAttribute("class");
+ Glib::ustring classtoparent = "lpeselectparent ";
+ if (classtoparentchar) {
+ classtoparent += classtoparentchar;
+ }
+ // here we fire a update and the lpe original check for this class and select
+ item->setAttribute("class", classtoparent.c_str());
+ selection->set(item);
+ g_timeout_add(100, &removeselectclass, item);
+ }
+ }
+}
+
+void LivePathEffectEditor::onUp()
+{
+ auto selection = getSelection();
+ if (selection && !selection->isEmpty() ) {
+ SPItem *item = selection->singleItem();
+ SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item);
+ if (lpeitem) {
+ Inkscape::LivePathEffect::Effect *lpe = lpeitem->getCurrentLPE();
+ lpeitem->upCurrentPathEffect();
+ DocumentUndo::done(getDocument(), _("Move path effect up"), INKSCAPE_ICON("dialog-path-effects"));
+ effect_list_reload(lpeitem);
+ if (lpe) {
+ showParams(*lpe);
+ lpe_list_locked = true;
+ selectInList(lpe);
+ }
+ }
+ }
+}
+
+void LivePathEffectEditor::onDown()
+{
+ auto selection = getSelection();
+ if (selection && !selection->isEmpty() ) {
+ SPItem *item = selection->singleItem();
+ SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item);
+ if ( lpeitem ) {
+ Inkscape::LivePathEffect::Effect *lpe = lpeitem->getCurrentLPE();
+ lpeitem->downCurrentPathEffect();
+ DocumentUndo::done(getDocument(), _("Move path effect down"), INKSCAPE_ICON("dialog-path-effects"));
+ effect_list_reload(lpeitem);
+ if (lpe) {
+ showParams(*lpe);
+ lpe_list_locked = true;
+ selectInList(lpe);
+ }
+ }
+ }
+}
+
+void LivePathEffectEditor::on_effect_selection_changed()
+{
+ Glib::RefPtr<Gtk::TreeSelection> sel = effectlist_view.get_selection();
+ if (sel->count_selected_rows () == 0) {
+ button_remove.set_sensitive(false);
+ return;
+ }
+ button_remove.set_sensitive(true);
+ Gtk::TreeModel::iterator it = sel->get_selected();
+ std::shared_ptr<LivePathEffect::LPEObjectReference> lperef = (*it)[columns.lperef];
+
+ if (lperef && current_lpeitem && current_lperef != lperef) {
+ // The last condition ignore Gtk::TreeModel may occasionally be changed emitted when nothing has happened
+ if (current_lpeitem->pathEffectsEnabled() && lperef->getObject()) {
+ lpe_list_locked = true; // prevent reload of the list which would lose selection
+ current_lpeitem->setCurrentPathEffect(lperef);
+ current_lperef = lperef;
+ LivePathEffect::Effect * effect = lperef->lpeobject->get_lpe();
+ if (effect) {
+ effect->refresh_widgets = true;
+ showParams(*effect);
+ // To reload knots and helper paths
+ auto selection = getSelection();
+ if (selection && !selection->isEmpty() && !selection_changed_lock) {
+ SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(selection->singleItem());
+ if (lpeitem) {
+ // this is need because set dont update selected LPE knots
+ selection->clear();
+ selection->add(lpeitem);
+ Inkscape::UI::Tools::sp_update_helperpath(getDesktop());
+ }
+ }
+ }
+ }
+ }
+}
+
+void LivePathEffectEditor::on_visibility_toggled( Glib::ustring const& str )
+{
+
+ Gtk::TreeModel::Children::iterator iter = effectlist_view.get_model()->get_iter(str);
+ Gtk::TreeModel::Row row = *iter;
+
+ std::shared_ptr<LivePathEffect::LPEObjectReference> lpeobjref = row[columns.lperef];
+
+ if ( lpeobjref && lpeobjref->lpeobject->get_lpe() ) {
+ bool newValue = !row[columns.col_visible];
+ row[columns.col_visible] = newValue;
+ /* FIXME: this explicit writing to SVG is wrong. The lpe_item should have a method to disable/enable an effect within its stack.
+ * So one can call: lpe_item->setActive(lpeobjref->lpeobject); */
+ lpeobjref->lpeobject->get_lpe()->getRepr()->setAttribute("is_visible", newValue ? "true" : "false");
+ auto selection = getSelection();
+ if (selection && !selection->isEmpty() ) {
+ SPItem *item = selection->singleItem();
+ SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item);
+ if ( lpeitem ) {
+ lpeobjref->lpeobject->get_lpe()->doOnVisibilityToggled(lpeitem);
+ }
+ }
+ DocumentUndo::done(getDocument(), newValue ? _("Activate path effect") : _("Deactivate path effect"), INKSCAPE_ICON("dialog-path-effects"));
+ }
+}
+
+} // 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..b9cf35f
--- /dev/null
+++ b/src/ui/dialog/livepatheffect-editor.h
@@ -0,0 +1,140 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Live Path Effect editing dialog
+ */
+/* Author:
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ *
+ * Copyright (C) 2007 Author
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_LIVE_PATH_EFFECT_H
+#define INKSCAPE_UI_DIALOG_LIVE_PATH_EFFECT_H
+
+#include <gtkmm/buttonbox.h>
+#include <gtkmm/eventbox.h>
+#include <gtkmm/frame.h>
+#include <gtkmm/label.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/toolbar.h>
+#include <gtkmm/treeview.h>
+
+#include "live_effects/effect-enum.h"
+#include "object/sp-item.h"
+#include "selection.h"
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/combo-enums.h"
+#include "ui/widget/frame.h"
+
+class SPLPEItem;
+
+namespace Inkscape {
+
+namespace LivePathEffect {
+ class Effect;
+ class LPEObjectReference;
+}
+
+namespace UI {
+namespace Dialog {
+
+class LivePathEffectEditor : public DialogBase
+{
+public:
+ LivePathEffectEditor();
+ ~LivePathEffectEditor() override;
+
+ static LivePathEffectEditor &getInstance() { return *new LivePathEffectEditor(); }
+
+ void selectionChanged(Inkscape::Selection *selection) override;
+ void selectionModified(Inkscape::Selection *selection, guint flags) override;
+
+ void onSelectionChanged(Inkscape::Selection *selection);
+
+ virtual void on_effect_selection_changed();
+
+private:
+ void effect_list_reload(SPLPEItem *lpeitem);
+ void set_sensitize_all(bool sensitive);
+ void showParams(LivePathEffect::Effect& effect);
+ void showText(Glib::ustring const &str);
+ void selectInList(LivePathEffect::Effect* effect);
+
+ // callback methods for buttons on grids page.
+ void onAdd();
+ void onRemove();
+ void onUp();
+ void onDown();
+ void onOriginal();
+
+ class ModelColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ ModelColumns()
+ {
+ add(col_name);
+ add(lperef);
+ add(col_visible);
+ }
+ ~ModelColumns() override = default;
+
+ Gtk::TreeModelColumn<Glib::ustring> col_name;
+ Gtk::TreeModelColumn<std::shared_ptr<LivePathEffect::LPEObjectReference>> lperef;
+ Gtk::TreeModelColumn<bool> col_visible;
+ };
+
+ bool lpe_list_locked;
+ bool selection_changed_lock;
+ //Inkscape::UI::Widget::ComboBoxEnum<LivePathEffect::EffectType> combo_effecttype;
+
+ Gtk::Widget * effectwidget;
+ Gtk::Label status_label;
+ UI::Widget::Frame effectcontrol_frame;
+ Gtk::Box effectapplication_hbox;
+ Gtk::Box effectcontrol_vbox;
+ Gtk::EventBox effectcontrol_eventbox;
+ Gtk::Box effectlist_vbox;
+ ModelColumns columns;
+ Gtk::ScrolledWindow scrolled_window;
+ Gtk::TreeView effectlist_view;
+ Glib::RefPtr<Gtk::ListStore> effectlist_store;
+ Glib::RefPtr<Gtk::TreeSelection> effectlist_selection;
+
+ void on_visibility_toggled( Glib::ustring const& str);
+ bool _on_button_release(GdkEventButton* button_event);
+ Gtk::ButtonBox toolbar_hbox;
+ Gtk::Button button_add;
+ Gtk::Button button_remove;
+ Gtk::Button button_original;
+ Gtk::Button button_up;
+ Gtk::Button button_down;
+
+ SPLPEItem * current_lpeitem;
+
+ std::shared_ptr<LivePathEffect::LPEObjectReference> current_lperef;
+
+ friend void lpeeditor_selection_changed (Inkscape::Selection * selection, gpointer data);
+ friend void lpeeditor_selection_modified (Inkscape::Selection * selection, guint /*flags*/, gpointer data);
+
+ LivePathEffectEditor(LivePathEffectEditor const &d) = delete;
+ LivePathEffectEditor& operator=(LivePathEffectEditor const &d) = delete;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_LIVE_PATH_EFFECT_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/lpe-fillet-chamfer-properties.cpp b/src/ui/dialog/lpe-fillet-chamfer-properties.cpp
new file mode 100644
index 0000000..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..10829e6
--- /dev/null
+++ b/src/ui/dialog/memory.cpp
@@ -0,0 +1,261 @@
+// 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"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+namespace {
+
+Glib::ustring format_size(std::size_t value) {
+ if (!value) {
+ return Glib::ustring("0");
+ }
+
+ typedef std::vector<char> Digits;
+ typedef std::vector<Digits *> Groups;
+
+ Groups groups;
+
+ Digits *digits;
+
+ while (value) {
+ unsigned places=3;
+ digits = new Digits();
+ digits->reserve(places);
+
+ while ( value && places ) {
+ digits->push_back('0' + (char)( value % 10 ));
+ value /= 10;
+ --places;
+ }
+
+ groups.push_back(digits);
+ }
+
+ Glib::ustring temp;
+
+ while (true) {
+ digits = groups.back();
+ while (!digits->empty()) {
+ temp.append(1, digits->back());
+ digits->pop_back();
+ }
+ delete digits;
+
+ groups.pop_back();
+ if (groups.empty()) {
+ break;
+ }
+
+ temp.append(",");
+ }
+
+ return temp;
+}
+
+}
+
+struct Memory::Private {
+ class ModelColumns : public Gtk::TreeModel::ColumnRecord {
+ public:
+ Gtk::TreeModelColumn<Glib::ustring> name;
+ Gtk::TreeModelColumn<Glib::ustring> used;
+ Gtk::TreeModelColumn<Glib::ustring> slack;
+ Gtk::TreeModelColumn<Glib::ustring> total;
+
+ ModelColumns() { add(name); add(used); add(slack); add(total); }
+ };
+
+ Private() {
+ model = Gtk::ListStore::create(columns);
+ view.set_model(model);
+ view.append_column(_("Heap"), columns.name);
+ view.append_column(_("In Use"), columns.used);
+ // TRANSLATORS: "Slack" refers to memory which is in the heap but currently unused.
+ // More typical usage is to call this memory "free" rather than "slack".
+ view.append_column(_("Slack"), columns.slack);
+ view.append_column(_("Total"), columns.total);
+ }
+
+ void update();
+
+ void start_update_task();
+ void stop_update_task();
+
+ ModelColumns columns;
+ Glib::RefPtr<Gtk::ListStore> model;
+ Gtk::TreeView view;
+
+ sigc::connection update_task;
+};
+
+void Memory::Private::update() {
+ Debug::Heap::Stats total = { 0, 0 };
+
+ int aggregate_features = Debug::Heap::SIZE_AVAILABLE | Debug::Heap::USED_AVAILABLE;
+ Gtk::ListStore::iterator row;
+
+ row = model->children().begin();
+
+ for ( unsigned i = 0 ; i < Debug::heap_count() ; i++ ) {
+ Debug::Heap *heap=Debug::get_heap(i);
+ if (heap) {
+ Debug::Heap::Stats stats=heap->stats();
+ int features=heap->features();
+
+ aggregate_features &= features;
+
+ if ( row == model->children().end() ) {
+ row = model->append();
+ }
+
+ row->set_value(columns.name, Glib::ustring(heap->name()));
+ if ( features & Debug::Heap::SIZE_AVAILABLE ) {
+ row->set_value(columns.total, format_size(stats.size));
+ total.size += stats.size;
+ } else {
+ row->set_value(columns.total, Glib::ustring(_("Unknown")));
+ }
+ if ( features & Debug::Heap::USED_AVAILABLE ) {
+ row->set_value(columns.used, format_size(stats.bytes_used));
+ total.bytes_used += stats.bytes_used;
+ } else {
+ row->set_value(columns.used, Glib::ustring(_("Unknown")));
+ }
+ if ( features & Debug::Heap::SIZE_AVAILABLE &&
+ features & Debug::Heap::USED_AVAILABLE )
+ {
+ row->set_value(columns.slack, format_size(stats.size - stats.bytes_used));
+ } else {
+ row->set_value(columns.slack, Glib::ustring(_("Unknown")));
+ }
+
+ ++row;
+ }
+ }
+
+ if ( row == model->children().end() ) {
+ row = model->append();
+ }
+
+ Glib::ustring value;
+
+ row->set_value(columns.name, Glib::ustring(_("Combined")));
+
+ if ( aggregate_features & Debug::Heap::SIZE_AVAILABLE ) {
+ row->set_value(columns.total, format_size(total.size));
+ } else {
+ row->set_value(columns.total, Glib::ustring("> ") + format_size(total.size));
+ }
+
+ if ( aggregate_features & Debug::Heap::USED_AVAILABLE ) {
+ row->set_value(columns.used, format_size(total.bytes_used));
+ } else {
+ row->set_value(columns.used, Glib::ustring("> ") + format_size(total.bytes_used));
+ }
+
+ if ( aggregate_features & Debug::Heap::SIZE_AVAILABLE &&
+ aggregate_features & Debug::Heap::USED_AVAILABLE )
+ {
+ row->set_value(columns.slack, format_size(total.size - total.bytes_used));
+ } else {
+ row->set_value(columns.slack, Glib::ustring(_("Unknown")));
+ }
+
+ ++row;
+
+ while ( row != model->children().end() ) {
+ row = model->erase(row);
+ }
+}
+
+void Memory::Private::start_update_task() {
+ update_task.disconnect();
+ update_task = Glib::signal_timeout().connect(
+ sigc::bind_return(sigc::mem_fun(*this, &Private::update), true),
+ 500
+ );
+}
+
+void Memory::Private::stop_update_task() {
+ update_task.disconnect();
+}
+
+Memory::Memory()
+ : DialogBase("/dialogs/memory", "Memory")
+ , _private(*(new Memory::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
+ Gtk::Button *button = Gtk::manage(new Gtk::Button(_("Recalculate")));
+ button->signal_button_press_event().connect(sigc::mem_fun(*this, &Memory::_apply));
+
+ Gtk::ButtonBox *button_box = Gtk::manage(new 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();
+ delete &_private;
+}
+
+bool Memory::_apply(GdkEventButton * /* button */)
+{
+ 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..b528cf0
--- /dev/null
+++ b/src/ui/dialog/memory.h
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Memory statistics dialog
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ *
+ * Copyright 2005 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_INKSCAPE_UI_DIALOG_MEMORY_H
+#define SEEN_INKSCAPE_UI_DIALOG_MEMORY_H
+
+#include "ui/dialog/dialog-base.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class Memory : public DialogBase
+{
+public:
+ Memory();
+ ~Memory() override;
+
+ static Memory &getInstance() { return *new Memory(); }
+
+protected:
+ bool _apply(GdkEventButton *);
+
+private:
+ Memory(Memory const &d) = delete; // no copy
+ void operator=(Memory const &d) = delete; // no assign
+
+ struct Private;
+ Private &_private;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/messages.cpp b/src/ui/dialog/messages.cpp
new file mode 100644
index 0000000..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..62ed7c3
--- /dev/null
+++ b/src/ui/dialog/messages.h
@@ -0,0 +1,102 @@
+// 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;
+
+ static Messages &getInstance() { return *new Messages(); }
+
+ /**
+ * Clear all information from the dialog
+ */
+ void clear();
+
+ /**
+ * Display a message
+ */
+ void message(char *msg);
+
+ /**
+ * Redirect g_log() messages to this widget
+ */
+ void captureLogMessages();
+
+ /**
+ * Return g_log() messages to normal handling
+ */
+ void releaseLogMessages();
+
+ void toggleCapture();
+
+protected:
+ //Gtk::MenuBar menuBar;
+ //Gtk::Menu fileMenu;
+ Gtk::ScrolledWindow textScroll;
+ Gtk::TextView messageText;
+ Gtk::Box buttonBox;
+ Gtk::Button buttonClear;
+ Gtk::CheckButton checkCapture;
+
+ //Handler ID's
+ guint handlerDefault;
+ guint handlerGlibmm;
+ guint handlerAtkmm;
+ guint handlerPangomm;
+ guint handlerGdkmm;
+ guint handlerGtkmm;
+
+private:
+ Messages(Messages const &d) = delete;
+ Messages operator=(Messages const &d) = delete;
+};
+
+} //namespace Dialog
+} //namespace UI
+} //namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_MESSAGES_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/new-from-template.cpp b/src/ui/dialog/new-from-template.cpp
new file mode 100644
index 0000000..bfccdb6
--- /dev/null
+++ b/src/ui/dialog/new-from-template.cpp
@@ -0,0 +1,72 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief New From Template main dialog - implementation
+ */
+/* Authors:
+ * Jan Darowski <jan.darowski@gmail.com>, supervised by Krzysztof Kosiński
+ *
+ * Copyright (C) 2013 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "new-from-template.h"
+#include "file.h"
+
+#include "include/gtkmm_version.h"
+
+namespace Inkscape {
+namespace UI {
+
+
+NewFromTemplate::NewFromTemplate()
+ : _create_template_button(_("Create from template"))
+{
+ set_title(_("New From Template"));
+ resize(400, 400);
+
+ _main_widget = new TemplateLoadTab(this);
+
+ get_content_area()->pack_start(*_main_widget);
+
+ _create_template_button.set_halign(Gtk::ALIGN_END);
+ _create_template_button.set_valign(Gtk::ALIGN_END);
+ _create_template_button.set_margin_end(15);
+
+ get_content_area()->pack_end(_create_template_button, Gtk::PACK_SHRINK);
+
+ _create_template_button.signal_clicked().connect(
+ sigc::mem_fun(*this, &NewFromTemplate::_createFromTemplate));
+ _create_template_button.set_sensitive(false);
+
+ show_all();
+}
+
+NewFromTemplate::~NewFromTemplate()
+{
+ delete _main_widget;
+}
+
+void NewFromTemplate::setCreateButtonSensitive(bool value)
+{
+ _create_template_button.set_sensitive(value);
+}
+
+void NewFromTemplate::_createFromTemplate()
+{
+ _main_widget->createTemplate();
+ _onClose();
+}
+
+void NewFromTemplate::_onClose()
+{
+ response(0);
+}
+
+void NewFromTemplate::load_new_from_template()
+{
+ NewFromTemplate dl;
+ dl.run();
+}
+
+}
+}
diff --git a/src/ui/dialog/new-from-template.h b/src/ui/dialog/new-from-template.h
new file mode 100644
index 0000000..a308fe4
--- /dev/null
+++ b/src/ui/dialog/new-from-template.h
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief New From Template main dialog
+ */
+/* Authors:
+ * Jan Darowski <jan.darowski@gmail.com>, supervised by Krzysztof Kosiński
+ *
+ * Copyright (C) 2013 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_SEEN_UI_DIALOG_NEW_FROM_TEMPLATE_H
+#define INKSCAPE_SEEN_UI_DIALOG_NEW_FROM_TEMPLATE_H
+
+#include <gtkmm/dialog.h>
+#include <gtkmm/button.h>
+
+#include "template-load-tab.h"
+
+
+namespace Inkscape {
+namespace UI {
+
+
+class NewFromTemplate : public Gtk::Dialog
+{
+
+friend class TemplateLoadTab;
+public:
+ static void load_new_from_template();
+ void setCreateButtonSensitive(bool value);
+ ~NewFromTemplate() override;
+
+private:
+ NewFromTemplate();
+ Gtk::Button _create_template_button;
+ TemplateLoadTab* _main_widget;
+
+ void _createFromTemplate();
+ void _onClose();
+};
+
+}
+}
+#endif
diff --git a/src/ui/dialog/object-attributes.cpp b/src/ui/dialog/object-attributes.cpp
new file mode 100644
index 0000000..9588211
--- /dev/null
+++ b/src/ui/dialog/object-attributes.cpp
@@ -0,0 +1,182 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Generic object attribute editor
+ *//*
+ * Authors:
+ * see git history
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+
+#include <glibmm/i18n.h>
+
+#include "desktop.h"
+#include "inkscape.h"
+
+#include "object/sp-anchor.h"
+#include "object/sp-image.h"
+
+#include "ui/dialog/object-attributes.h"
+
+#include "widgets/sp-attribute-widget.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+struct SPAttrDesc {
+ gchar const *label;
+ gchar const *attribute;
+};
+
+static const SPAttrDesc anchor_desc[] = {
+ { N_("Href:"), "xlink:href"},
+ { N_("Target:"), "target"},
+ { N_("Type:"), "xlink:type"},
+ // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/linking.html#AElementXLinkRoleAttribute
+ // Identifies the type of the related resource with an absolute URI
+ { N_("Role:"), "xlink:role"},
+ // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/linking.html#AElementXLinkArcRoleAttribute
+ // For situations where the nature/role alone isn't enough, this offers an additional URI defining the purpose of the link.
+ { N_("Arcrole:"), "xlink:arcrole"},
+ // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/linking.html#AElementXLinkTitleAttribute
+ { N_("Title:"), "xlink:title"},
+ { N_("Show:"), "xlink:show"},
+ // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/linking.html#AElementXLinkActuateAttribute
+ { N_("Actuate:"), "xlink:actuate"},
+ { nullptr, nullptr}
+};
+
+static const SPAttrDesc image_desc[] = {
+ { N_("URL:"), "xlink:href"},
+ { N_("X:"), "x"},
+ { N_("Y:"), "y"},
+ { N_("Width:"), "width"},
+ { N_("Height:"), "height"},
+ { nullptr, nullptr}
+};
+
+static const SPAttrDesc image_nohref_desc[] = {
+ { N_("X:"), "x"},
+ { N_("Y:"), "y"},
+ { N_("Width:"), "width"},
+ { N_("Height:"), "height"},
+ { nullptr, nullptr}
+};
+
+ObjectAttributes::ObjectAttributes()
+ : DialogBase("/dialogs/objectattr/", "ObjectAttributes")
+ , blocked(false)
+ , CurrentItem(nullptr)
+ , attrTable(Gtk::manage(new SPAttributeTable()))
+{
+ attrTable->show();
+}
+
+void ObjectAttributes::widget_setup ()
+{
+ if (blocked || !getDesktop()) {
+ return;
+ }
+
+ Inkscape::Selection *selection = getDesktop()->getSelection();
+ SPItem *item = selection->singleItem();
+ if (!item)
+ {
+ set_sensitive (false);
+ CurrentItem = nullptr;
+ //no selection anymore or multiple objects selected, means that we need
+ //to close the connections to the previously selected object
+ return;
+ }
+
+ blocked = true;
+
+ // CPPIFY
+ SPObject *obj = item; //to get the selected item
+// GObjectClass *klass = G_OBJECT_GET_CLASS(obj); //to deduce the object's type
+// GType type = G_TYPE_FROM_CLASS(klass);
+ const SPAttrDesc *desc;
+
+// if (type == SP_TYPE_ANCHOR)
+ if (SP_IS_ANCHOR(item))
+ {
+ desc = anchor_desc;
+ }
+// else if (type == SP_TYPE_IMAGE)
+ else if (SP_IS_IMAGE(item))
+ {
+ Inkscape::XML::Node *ir = obj->getRepr();
+ const gchar *href = ir->attribute("xlink:href");
+ if ( (!href) || ((strncmp(href, "data:", 5) == 0)) )
+ {
+ desc = image_nohref_desc;
+ }
+ else
+ {
+ desc = image_desc;
+ }
+ }
+ else
+ {
+ blocked = false;
+ set_sensitive (false);
+ return;
+ }
+
+ std::vector<Glib::ustring> labels;
+ std::vector<Glib::ustring> attrs;
+ if (CurrentItem != item)
+ {
+ int len = 0;
+ while (desc[len].label)
+ {
+ labels.emplace_back(desc[len].label);
+ attrs.emplace_back(desc[len].attribute);
+ len += 1;
+ }
+ attrTable->set_object(obj, labels, attrs, (GtkWidget*)gobj());
+ CurrentItem = item;
+ }
+ else
+ {
+ attrTable->change_object(obj);
+ }
+
+ set_sensitive (true);
+ show_all();
+ blocked = false;
+}
+
+void ObjectAttributes::selectionChanged(Selection *selection)
+{
+ widget_setup();
+}
+
+void ObjectAttributes::selectionModified(Selection *selection, guint flags)
+{
+ if (flags & ( SP_OBJECT_MODIFIED_FLAG |
+ SP_OBJECT_PARENT_MODIFIED_FLAG |
+ SP_OBJECT_STYLE_MODIFIED_FLAG) ) {
+ attrTable->reread_properties();
+ }
+}
+
+
+}
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/object-attributes.h b/src/ui/dialog/object-attributes.h
new file mode 100644
index 0000000..21c106a
--- /dev/null
+++ b/src/ui/dialog/object-attributes.h
@@ -0,0 +1,85 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Generic object attribute editor
+ *//*
+ * Authors:
+ * see git history
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_DIALOGS_OBJECT_ATTRIBUTES_H
+#define SEEN_DIALOGS_OBJECT_ATTRIBUTES_H
+
+#include "ui/dialog/dialog-base.h"
+
+class SPAttributeTable;
+class SPItem;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/**
+ * 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;
+
+ /**
+ * Returns a new instance of the object attributes dialog.
+ *
+ * Auxiliary function needed by the DialogManager.
+ */
+ static ObjectAttributes &getInstance() { return *new ObjectAttributes(); }
+
+ /**
+ * Updates entries and other child widgets on selection change, object modification, etc.
+ */
+ void widget_setup();
+
+private:
+ /**
+ * Is UI update bloched?
+ */
+ bool blocked;
+
+ /**
+ * Contains a pointer to the currently selected item (NULL in case nothing is or multiple objects are selected).
+ */
+ SPItem *CurrentItem;
+
+ /**
+ * Child widget to show the object attributes.
+ *
+ * attrTable makes the labels and edit boxes for the attributes defined
+ * in the SPAttrDesc arrays at the top of the cpp-file. This widgets also
+ * ensures object attribute modifications by the user are set.
+ */
+ SPAttributeTable *attrTable;
+};
+
+}
+}
+}
+
+#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..6acce53
--- /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 (SP_IS_IMAGE(item)) {
+ _combo_image_rendering.show();
+ _label_image_rendering.show();
+ _combo_image_rendering.set_active(obj->style->image_rendering.value);
+ if (obj->getAttribute("inkscape:svg-dpi")) {
+ _spin_dpi.set_value(std::stod(obj->getAttribute("inkscape:svg-dpi")));
+ _spin_dpi.show();
+ _label_dpi.show();
+ } else {
+ _spin_dpi.hide();
+ _label_dpi.hide();
+ }
+ } else {
+ _combo_image_rendering.hide();
+ _combo_image_rendering.unset_active();
+ _label_image_rendering.hide();
+ _spin_dpi.hide();
+ _label_dpi.hide();
+ }
+
+ /* Description */
+ gchar *desc = obj->desc();
+ if (desc) {
+ _tv_description.get_buffer()->set_text(desc);
+ g_free(desc);
+ } else {
+ _tv_description.get_buffer()->set_text("");
+ }
+ _ft_description.set_sensitive(TRUE);
+
+ if (_current_item == nullptr) {
+ _attr_table->set_object(obj, _int_labels, _int_attrs, (GtkWidget*) _exp_interactivity.gobj());
+ } else {
+ _attr_table->change_object(obj);
+ }
+ _attr_table->show_all();
+ }
+ _current_item = item;
+ _blocked = false;
+}
+
+void ObjectProperties::_labelChanged()
+{
+ if (_blocked) {
+ return;
+ }
+
+ SPItem *item = 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 (SP_IS_IMAGE(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 (SP_IS_IMAGE(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..86f2fd1
--- /dev/null
+++ b/src/ui/dialog/object-properties.h
@@ -0,0 +1,145 @@
+// 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 {};
+
+ static ObjectProperties &getInstance() { return *new ObjectProperties(); }
+
+ /// 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..fb8e98f
--- /dev/null
+++ b/src/ui/dialog/objects.cpp
@@ -0,0 +1,1468 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A simple panel for objects (originally developed for Ponyscape, an Inkscape derivative)
+ *
+ * Authors:
+ * Martin Owens, completely rewritten
+ * Theodore Janeczko
+ * Tweaked by Liam P White for use in Inkscape
+ * Tavmjong Bah
+ *
+ * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com>
+ * Tavmjong Bah 2017
+ * Martin Owens 2020-2021
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "objects.h"
+
+#include <gtkmm/cellrenderer.h>
+#include <gtkmm/icontheme.h>
+#include <gtkmm/imagemenuitem.h>
+#include <gtkmm/separatormenuitem.h>
+#include <glibmm/main.h>
+#include <glibmm/i18n.h>
+
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "filter-chemistry.h"
+#include "inkscape.h"
+#include "layer-manager.h"
+#include "message-stack.h"
+
+#include "actions/actions-tools.h"
+
+#include "include/gtkmm_version.h"
+
+#include "object/filters/blend.h"
+#include "object/filters/gaussian-blur.h"
+#include "object/sp-clippath.h"
+#include "object/sp-mask.h"
+#include "object/sp-root.h"
+#include "object/sp-shape.h"
+#include "style.h"
+
+#include "ui/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/imagetoggler.h"
+#include "ui/widget/shapeicon.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
+ 1.00, //1 selected
+ 0.50, //2 layer focused
+ 1.00, //3 layer focused & selected
+ 0.00, //4 child of focused layer
+ 1.00, //5 selected child of focused layer
+ 0.50, //6 2 and 4
+ 1.00 //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 layers_only);
+ ~ObjectWatcher() override;
+
+ 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 moveChild(Node &child, Node *sibling);
+
+ 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 layers_only;
+};
+
+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);
+ }
+ ~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;
+};
+
+/**
+ * Gets an instance of the Objects panel
+ */
+ObjectsPanel& ObjectsPanel::getInstance()
+{
+ return *new ObjectsPanel();
+}
+
+/**
+ * Creates a new ObjectWatcher, a gtk TreeView iterated watching device.
+ *
+ * @param panel The panel to which the object watcher belongs
+ * @param obj The object to watch
+ * @param iter The optional list store iter for the item, if not provided,
+ * assumes this is the root 'document' object.
+ * @param layers If true, only show and watch layers, not groups or other objects.
+ */
+ObjectWatcher::ObjectWatcher(ObjectsPanel* panel, SPItem* obj, Gtk::TreeRow *row, bool layers)
+ : panel(panel)
+ , layers_only(layers)
+ , row_ref()
+ , selection_state(0)
+ , node(obj->getRepr())
+{
+ if(row != nullptr) {
+ assert(row->children().empty());
+ setRow(*row);
+ updateRowInfo();
+ }
+ node->addObserver(*this);
+
+ // Only show children for groups (and their subclasses like SPAnchor or SPRoot)
+ if (!dynamic_cast<SPGroup const*>(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 && !layers);
+}
+ObjectWatcher::~ObjectWatcher()
+{
+ node->removeObserver(*this);
+ Gtk::TreeModel::Path path;
+ if (bool(row_ref) && (path = row_ref.get_path())) {
+ auto iter = panel->_store->get_iter(path);
+ if(iter) {
+ panel->_store->erase(iter);
+ }
+ }
+ child_watchers.clear();
+}
+
+/**
+ * Update the information in the row from the stored node
+ */
+void ObjectWatcher::updateRowInfo() {
+ if (auto item = dynamic_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();
+
+ updateRowHighlight();
+ updateRowAncestorState(row[_model->_colAncestorInvisible], row[_model->_colAncestorLocked]);
+ }
+}
+
+/**
+ * Propegate changes to the highlight color to all children.
+ */
+void ObjectWatcher::updateRowHighlight() {
+ if (auto item = dynamic_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();
+ }
+ }
+ }
+}
+
+/**
+ * Propegate 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 it's 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);
+ }
+}
+
+/**
+ * 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)
+{
+ auto group = dynamic_cast<SPGroup *>(child);
+ if (layers_only && (!group || group->layerMode() != SPGroup::LAYER)) {
+ return false;
+ }
+
+ auto const children = getChildren();
+ if (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, layers_only));
+
+ // 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 = dynamic_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 && !dynamic_cast<SPItem const *>(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 = dynamic_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;
+}
+
+class ColorTagRenderer : public Gtk::CellRenderer {
+public:
+ ColorTagRenderer() :
+ Glib::ObjectBase(typeid(CellRenderer)),
+ Gtk::CellRenderer(),
+ _property_color(*this, "tagcolor", 0) {
+ 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);
+ }
+
+ ~ColorTagRenderer() override = default;
+
+ Glib::PropertyProxy<unsigned int> property_color() {
+ return _property_color.get_proxy();
+ }
+
+ int get_width() const {
+ return _width;
+ }
+
+ sigc::signal<void, const Glib::ustring&> signal_clicked() {
+ return _signal_clicked;
+ }
+
+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 {
+ 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();
+ }
+
+ 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 = 1;
+ nat_h = _height;
+ }
+
+ 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 {
+ _signal_clicked.emit(path);
+ return false;
+ }
+
+ int _width = 8;
+ int _height;
+ Glib::Property<unsigned int> _property_color;
+ sigc::signal<void, const Glib::ustring&> _signal_clicked;
+};
+
+/**
+ * Constructor
+ */
+ObjectsPanel::ObjectsPanel() :
+ DialogBase("/dialogs/objects", "Objects"),
+ root_watcher(nullptr),
+ _model(nullptr),
+ _layer(nullptr),
+ _is_editing(false),
+ _page(Gtk::ORIENTATION_VERTICAL),
+ _color_picker(_("Highlight color"), "", 0, true)
+{
+ //Create the tree model and store
+ ModelColumns *zoop = new ModelColumns();
+ _model = zoop;
+
+ _store = Gtk::TreeStore::create( *zoop );
+
+ _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.set_reorderable(true);
+ _tree.enable_model_drag_dest (Gdk::ACTION_MOVE);
+
+ //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);
+
+ // 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 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->set_fixed_width(tag_renderer->get_width());
+ }
+ 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(), "highligh-color", _("Set item highlight color"), INKSCAPE_ICON("dialog-object-properties"));
+ }
+ });
+
+ //Set the expander and search columns
+ _tree.set_expander_column(*_name_column);
+ // Disable search (it doesn't make much sense)
+ _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::_handleKeyEvent), 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*){
+ getDesktop()->messageStack()->flash(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*){ 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 (auto selection = getSelection()) {
+ selectionChanged(selection);
+ }
+ }
+ return 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(_buttonsRow, Gtk::PACK_SHRINK);
+ _page.pack_end(_scroller, Gtk::PACK_EXPAND_WIDGET);
+ pack_start(_page, Gtk::PACK_EXPAND_WIDGET);
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ {
+ auto child = Glib::wrap(sp_get_icon_image("layer-duplicate", GTK_ICON_SIZE_SMALL_TOOLBAR));
+ child->show();
+ _object_mode.add(*child);
+ _object_mode.set_relief(Gtk::RELIEF_NONE);
+ }
+ _object_mode.set_tooltip_text(_("Switch to layers only view."));
+ _object_mode.property_active() = prefs->getBool("/dialogs/objects/layers_only", false);
+ _object_mode.property_active().signal_changed().connect(sigc::mem_fun(*this, &ObjectsPanel::_objects_toggle));
+
+ _buttonsPrimary.pack_start(_object_mode, Gtk::PACK_SHRINK);
+ _buttonsPrimary.pack_start(*_addBarButton(INKSCAPE_ICON("layer-new"), _("Add layer..."), "win.layer-new"), Gtk::PACK_SHRINK);
+
+ _buttonsSecondary.pack_end(*_addBarButton(INKSCAPE_ICON("edit-delete"), _("Remove object"), "app.delete-selection"), Gtk::PACK_SHRINK);
+ _buttonsSecondary.pack_end(*_addBarButton(INKSCAPE_ICON("go-down"), _("Move Down"), "app.selection-stack-down"), Gtk::PACK_SHRINK);
+ _buttonsSecondary.pack_end(*_addBarButton(INKSCAPE_ICON("go-up"), _("Move Up"), "app.selection-stack-up"), Gtk::PACK_SHRINK);
+
+ _buttonsRow.pack_start(_buttonsPrimary, Gtk::PACK_SHRINK);
+ _buttonsRow.pack_end(_buttonsSecondary, Gtk::PACK_SHRINK);
+
+ 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)
+ _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::_objects_toggle()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/dialogs/objects/layers_only", _object_mode.get_active());
+}
+
+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()
+{
+ if (root_watcher) {
+ delete root_watcher;
+ }
+ root_watcher = nullptr;
+
+ if (auto document = getDocument()) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool layers_only = prefs->getBool("/dialogs/objects/layers_only", false);
+ root_watcher = new ObjectWatcher(this, document->getRoot(), nullptr, layers_only);
+ layerChanged(getDesktop()->layerManager().currentLayer());
+ selectionChanged(getSelection());
+ }
+}
+
+void ObjectsPanel::selectionChanged(Selection *selected)
+{
+ root_watcher->setSelectedBitRecursive(SELECTED_OBJECT, false);
+
+ for (auto item : selected->items()) {
+ ObjectWatcher *watcher = nullptr;
+ // This both unpacks the tree, and populates lazy loading
+ 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);
+ }
+ }
+ }
+ }
+ if (watcher) {
+ if (auto final_watcher = watcher->findChild(item->getRepr())) {
+ final_watcher->setSelectedBit(SELECTED_OBJECT, true);
+ _tree.expand_to_path(final_watcher->getTreePath());
+ } else {
+ g_warning("Can't find final step in tree selection!");
+ }
+ } else {
+ g_warning("Can't find a mid step in tree selection!");
+ }
+ }
+}
+
+/**
+ * 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) 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(GdkEventButton* event, Gtk::TreeModel::Row row)
+{
+ if (SPItem* item = getItem(row)) {
+ if (event->state & GDK_SHIFT_MASK) {
+ // Toggle Visible for layers (hide all other layers)
+ if (auto desktop = getDesktop()) {
+ if (desktop->layerManager().isLayer(item)) {
+ desktop->layerManager().toggleLayerSolo(item);
+ DocumentUndo::done(getDocument(), _("Hide other layers"), "");
+ }
+ }
+ } else {
+ item->setHidden(!row[_model->_colInvisible]);
+ // Use maybeDone so user can flip back and forth without making loads of undo items
+ DocumentUndo::maybeDone(getDocument(), "toggle-vis", _("Toggle item visibility"), "");
+ }
+ }
+ 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(GdkEventButton* event, Gtk::TreeModel::Row row)
+{
+ if (SPItem* item = getItem(row)) {
+ if (event->state & GDK_SHIFT_MASK) {
+ // Toggle lock for layers (lock all other layers)
+ if (auto desktop = getDesktop()) {
+ if (desktop->layerManager().isLayer(item)) {
+ desktop->layerManager().toggleLockOtherLayers(item);
+ DocumentUndo::done(getDocument(), _("Lock other layers"), "");
+ }
+ }
+ } else {
+ item->setLocked(!row[_model->_colLocked]);
+ // Use maybeDone so user can flip back and forth without making loads of undo items
+ DocumentUndo::maybeDone(getDocument(), "toggle-lock", _("Toggle item locking"), "");
+ }
+ }
+ return true;
+}
+
+/**
+ * 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()) {
+ case GDK_KEY_Escape:
+ if (desktop->canvas) {
+ desktop->canvas->grab_focus();
+ return true;
+ }
+ break;
+
+ // space and return enter label editing mode; leave them for the tree to handle
+ case GDK_KEY_Return:
+ case GDK_KEY_space:
+ 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;
+ }
+ // 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)) {
+ if (auto row = *_store->get_iter(path)) {
+ row[_model->_colHover] = true;
+ _hovered_row_ref = Gtk::TreeModel::RowReference(_store, path);
+ }
+ }
+
+ _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 = dynamic_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;
+
+ 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_RELEASE) {
+ if (col == _eye_column) {
+ return toggleVisible(event, row);
+ } else if (col == _lock_column) {
+ return toggleLocked(event, row);
+ }
+ }
+ }
+ // Only the label reacts to clicks, nothing else, only need to test horz
+ Gdk::Rectangle r;
+ _tree.get_cell_area(path, *_name_column, r);
+ if (x < r.get_x() || x > (r.get_x() + r.get_width()))
+ 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;
+ SPGroup *group = SP_GROUP(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 (event->state & GDK_SHIFT_MASK && !selection->isEmpty()) {
+ // Select everything between this row and the last selected item
+ selection->setBetween(item);
+ } else if (event->state & GDK_CONTROL_MASK) {
+ selection->toggle(item);
+ } else if (group && group->layerMode() == SPGroup::LAYER) {
+ // if right-clicking on a layer, make it current for context menu actions to work correctly
+ if (context_menu) {
+ if (getDesktop()->layerManager().currentLayer() != item) {
+ getDesktop()->layerManager().setCurrentLayer(item, true);
+ }
+ } else {
+ selection->set(item);
+ }
+ } else if (!context_menu) {
+ selection->set(item);
+ }
+
+ if (context_menu) {
+ 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);
+ }
+ 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 dynamic_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);
+ getWatcher(getRepr(row))->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 iter = _store->get_iter(path);
+ auto repr = getRepr(*iter);
+ auto obj = document->getObjectByRepr(repr);
+
+ bool const drop_into = pos != Gtk::TREE_VIEW_DROP_BEFORE && //
+ pos != Gtk::TREE_VIEW_DROP_AFTER;
+
+ // don't drop on self
+ if (selection->includes(obj)) {
+ goto finally;
+ }
+
+ auto item = getItem(*iter);
+
+ // only groups can have children
+ if (drop_into && !dynamic_cast<SPGroup const *>(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) {
+ if (drop_into) {
+ selection->toLayer(document->getObjectByRepr(drop_repr));
+ } else {
+ Node *after = (pos == Gtk::TREE_VIEW_DROP_BEFORE) ? drop_repr : drop_repr->prev();
+ selection->toLayer(document->getObjectByRepr(drop_repr->parent()), false, after);
+ }
+ }
+
+ on_drag_end(context);
+ return true;
+}
+
+void ObjectsPanel::on_drag_start(const Glib::RefPtr<Gdk::DragContext> &context)
+{
+ 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;
+}
+
+} //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/objects.h b/src/ui/dialog/objects.h
new file mode 100644
index 0000000..a0e1764
--- /dev/null
+++ b/src/ui/dialog/objects.h
@@ -0,0 +1,174 @@
+// 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/dialog.h>
+
+#include "helper/auto-connection.h"
+#include "xml/node-observer.h"
+
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/color-picker.h"
+
+#include "selection.h"
+#include "color-rgba.h"
+#include "helper/auto-connection.h"
+
+using Inkscape::XML::Node;
+
+class SPObject;
+class SPGroup;
+// struct SPColorSelector;
+
+namespace Inkscape {
+namespace UI {
+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;
+ static ObjectsPanel& getInstance();
+
+protected:
+
+ void desktopReplaced() override;
+ void documentReplaced() override;
+ void layerChanged(SPObject *obj);
+ void selectionChanged(Selection *selected) override;
+
+ // Accessed by ObjectWatcher directly (friend class)
+ SPObject* getObject(Node *node);
+ ObjectWatcher* getWatcher(Node *node);
+ ObjectWatcher *getRootWatcher() const { return root_watcher; };
+
+ 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:
+
+ 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;
+
+ 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 *_eye_column = nullptr;
+ Gtk::TreeView::Column *_lock_column = nullptr;
+ Gtk::Box _buttonsRow;
+ Gtk::Box _buttonsPrimary;
+ Gtk::Box _buttonsSecondary;
+ Gtk::ScrolledWindow _scroller;
+ Gtk::Menu _popupMenu;
+ Gtk::Box _page;
+ Gtk::ToggleButton _object_mode;
+ Inkscape::auto_connection _tree_style;
+ Inkscape::UI::Widget::ColorPicker _color_picker;
+ Gtk::TreeRow _clicked_item_row;
+
+ ObjectsPanel(ObjectsPanel const &) = delete; // no copy
+ ObjectsPanel &operator=(ObjectsPanel const &) = delete; // no assign
+
+ Gtk::Button *_addBarButton(char const* iconName, char const* tooltip, char const *action_name);
+ void _objects_toggle();
+
+ bool toggleVisible(GdkEventButton* event, Gtk::TreeModel::Row row);
+ bool toggleLocked(GdkEventButton* event, Gtk::TreeModel::Row row);
+
+ bool _handleButtonEvent(GdkEventButton *event);
+ bool _handleKeyEvent(GdkEventKey *event);
+ bool _handleMotionEvent(GdkEventMotion* motion_event);
+
+ 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;
+
+ friend class ObjectWatcher;
+
+ SPItem *_solid_item;
+ std::list<SPItem *> _translucent_items;
+};
+
+
+
+} //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..5fc4895
--- /dev/null
+++ b/src/ui/dialog/paint-servers.cpp
@@ -0,0 +1,567 @@
+// 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 <iostream>
+#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"
+
+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>
+)=====";
+
+// Constructor
+PaintServersDialog::PaintServersDialog()
+ : DialogBase("/dialogs/paint", "PaintServers")
+ , target_selected(true)
+ , ALLDOCS(_("All paint servers"))
+ , CURRENTDOC(_("Current document"))
+ , columns()
+{
+ current_store = ALLDOCS;
+
+ store[ALLDOCS] = Gtk::ListStore::create(columns);
+ store[CURRENTDOC] = Gtk::ListStore::create(columns);
+
+ // Grid holding the contents
+ Gtk::Grid *grid = Gtk::manage(new Gtk::Grid());
+ grid->set_margin_start(3);
+ grid->set_margin_end(3);
+ grid->set_margin_top(3);
+ grid->set_row_spacing(3);
+ pack_start(*grid, Gtk::PACK_EXPAND_WIDGET);
+
+ // Grid row 0
+ Gtk::Label *file_label = Gtk::manage(new Gtk::Label(Glib::ustring(_("Server")) + ": "));
+ grid->attach(*file_label, 0, 0, 1, 1);
+
+ dropdown = Gtk::manage(new Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBoxText>());
+ dropdown->append(ALLDOCS);
+ dropdown->append(CURRENTDOC);
+ dropdown->set_active_text(ALLDOCS);
+ dropdown->set_hexpand();
+ grid->attach(*dropdown, 1, 0, 1, 1);
+
+ // Grid row 1
+ Gtk::Label *fill_label = Gtk::manage(new Gtk::Label(Glib::ustring(_("Change")) + ": "));
+ grid->attach(*fill_label, 0, 1, 1, 1);
+
+ target_dropdown = Gtk::manage(new Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBoxText>());
+ target_dropdown->append(_("Fill"));
+ target_dropdown->append(_("Stroke"));
+ target_dropdown->set_active_text(_("Fill"));
+ target_dropdown->set_hexpand();
+ grid->attach(*target_dropdown, 1, 1, 1, 1);
+
+ // Grid row 2
+ icon_view = Gtk::manage(new Gtk::IconView(
+ static_cast<Glib::RefPtr<Gtk::TreeModel>>(store[current_store])
+ ));
+ icon_view->set_tooltip_column(0);
+ icon_view->set_pixbuf_column(2);
+ icon_view->set_size_request(200, -1);
+ icon_view->show_all_children();
+ icon_view->set_selection_mode(Gtk::SELECTION_SINGLE);
+ icon_view->set_activate_on_single_click(true);
+
+ Gtk::ScrolledWindow *scroller = Gtk::manage(new Gtk::ScrolledWindow());
+ scroller->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_ALWAYS);
+ scroller->set_hexpand();
+ scroller->set_vexpand();
+ scroller->add(*icon_view);
+ scroller->set_overlay_scrolling(false);
+ grid->attach(*scroller, 0, 2, 2, 1);
+ fix_inner_scroll(scroller);
+
+ // Events
+ target_dropdown->signal_changed().connect(
+ sigc::mem_fun(*this, &PaintServersDialog::on_target_changed)
+ );
+
+ dropdown->signal_changed().connect([=]() {onPaintSourceDocumentChanged();});
+ icon_view->signal_item_activated().connect([=](Gtk::TreeModel::Path const &p) {onPaintClicked(p);});
+
+ // Get wrapper document (rectangle to fill with paint server).
+ preview_document = SPDocument::createNewDocFromMem(wrapper.c_str(), wrapper.length(), true);
+
+ SPObject *rect = preview_document->getObjectById("Rect");
+ SPObject *defs = preview_document->getObjectById("Defs");
+ if (!rect || !defs) {
+ std::cerr << "PaintServersDialog::PaintServersDialog: Failed to get wrapper defs or rectangle!!" << std::endl;
+ }
+
+ // Set up preview document.
+ unsigned key = SPItem::display_key_new(1);
+ preview_document->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ preview_document->ensureUpToDate();
+ renderDrawing.setRoot(preview_document->getRoot()->invoke_show(renderDrawing, key, SP_ITEM_SHOW_DISPLAY));
+
+ _loadStockPaints();
+}
+
+void PaintServersDialog::documentReplaced()
+{
+ auto document = getDocument();
+ if (!document) {
+ return;
+ }
+ document_map[CURRENTDOC] = document;
+ _loadFromCurrentDocument();
+ _regenerateAll();
+}
+
+PaintServersDialog::~PaintServersDialog() = default;
+
+// Get url or color value.
+Glib::ustring get_url(Glib::ustring paint)
+{
+
+ Glib::MatchInfo matchInfo;
+
+ // Paint server
+ static Glib::RefPtr<Glib::Regex> regex1 = Glib::Regex::create(":(url\\(#([A-z0-9\\-_\\.#])*\\))");
+ regex1->match(paint, matchInfo);
+
+ if (matchInfo.matches()) {
+ return matchInfo.fetch(1);
+ }
+
+ // Color
+ static Glib::RefPtr<Glib::Regex> regex2 = Glib::Regex::create(":(([A-z0-9#])*)");
+ regex2->match(paint, matchInfo);
+
+ if (matchInfo.matches()) {
+ return matchInfo.fetch(1);
+ }
+
+ return Glib::ustring();
+}
+
+// This is too complicated to use selectors!
+void recurse_find_paint(SPObject* in, std::vector<Glib::ustring>& list)
+{
+
+ g_return_if_fail(in != nullptr);
+
+ // Add paint servers in <defs> section.
+ if (dynamic_cast<SPPaintServer *>(in)) {
+ if (in->getId()) {
+ // Need to check as one can't construct Glib::ustring with nullptr.
+ list.push_back (Glib::ustring("url(#") + in->getId() + ")");
+ }
+ // Don't recurse into paint servers.
+ return;
+ }
+
+ // Add paint servers referenced by shapes.
+ if (dynamic_cast<SPShape *>(in)) {
+ list.push_back (get_url(in->style->fill.write()));
+ list.push_back (get_url(in->style->stroke.write()));
+ }
+
+ for (auto child: in->childList(false)) {
+ recurse_find_paint(child, list);
+ }
+}
+
+/** Load 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"})) {
+ SPDocument *doc = SPDocument::createNewDoc(path.c_str(), false);
+ _loadPaintsFromDocument(doc, paints);
+ }
+
+ _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 (store.find(paint.doc_title) == store.end()) {
+ store[paint.doc_title] = Gtk::ListStore::create(columns);
+ }
+
+ Glib::ustring id;
+ paint.bitmap = get_pixbuf(paint.source_document, paint.url, id);
+ if (!paint.bitmap) {
+ return;
+ }
+
+ Gtk::ListStore::iterator iter = store[paint.doc_title]->append();
+ (*iter)[columns.id] = id;
+ (*iter)[columns.paint] = paint.url;
+ (*iter)[columns.pixbuf] = paint.bitmap;
+ (*iter)[columns.document] = paint.doc_title;
+
+ if (document_map.find(paint.doc_title) == document_map.end()) {
+ document_map[paint.doc_title] = paint.source_document;
+ dropdown->append(paint.doc_title);
+ }
+}
+
+/** 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()
+{
+ // Save active item
+ bool showing_all = (current_store == ALLDOCS);
+ Gtk::TreePath active;
+ if (showing_all) {
+ std::vector<Gtk::TreePath> selected = icon_view->get_selected_items();
+ if (selected.empty()) {
+ showing_all = false;
+ } else {
+ active = selected[0];
+ }
+ }
+
+ 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
+ {
+ return (a.url < b.url) || ((a.url == b.url) && a.doc_title != CURRENTDOC);
+ });
+ 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) {
+ Gtk::ListStore::iterator iter = store[ALLDOCS]->append();
+ (*iter)[columns.id] = std::move(paint.id);
+ (*iter)[columns.paint] = std::move(paint.url);
+ (*iter)[columns.pixbuf] = std::move(paint.bitmap);
+ (*iter)[columns.document] = std::move(paint.doc_title);
+ }
+
+ // Restore active item
+ if (showing_all) {
+ icon_view->select_path(active);
+ }
+}
+
+Glib::RefPtr<Gdk::Pixbuf> PaintServersDialog::get_pixbuf(SPDocument *document, Glib::ustring const &paint,
+ Glib::ustring &id)
+{
+
+ SPObject *rect = preview_document->getObjectById("Rect");
+ SPObject *defs = preview_document->getObjectById("Defs");
+
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf(nullptr);
+ if (paint.empty()) {
+ return pixbuf;
+ }
+
+ // Set style on wrapper
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_set_property(css, "fill", paint.c_str());
+ rect->changeCSS(css, "style");
+ sp_repr_css_attr_unref(css);
+
+ // Insert paint into defs if required
+ Glib::MatchInfo matchInfo;
+ static Glib::RefPtr<Glib::Regex> regex = Glib::Regex::create("url\\(#([A-Za-z0-9#._-]*)\\)");
+ regex->match(paint, matchInfo);
+ if (matchInfo.matches()) {
+ id = matchInfo.fetch(1);
+
+ // Delete old paint if necessary
+ std::vector<SPObject *> old_paints = preview_document->getObjectsBySelector("defs > *");
+ for (auto paint : old_paints) {
+ paint->deleteObject(false);
+ }
+
+ // Add new paint
+ SPObject *new_paint = document->getObjectById(id);
+ if (!new_paint) {
+ std::cerr << "PaintServersDialog::get_pixbuf: cannot find paint server: " << id << std::endl;
+ return pixbuf;
+ }
+
+ // Create a copy repr of the paint
+ Inkscape::XML::Document *xml_doc = preview_document->getReprDoc();
+ Inkscape::XML::Node *repr = new_paint->getRepr()->duplicate(xml_doc);
+ defs->appendChild(repr);
+ } else {
+ // Temporary block solid color fills.
+ return pixbuf;
+ }
+
+ preview_document->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ preview_document->ensureUpToDate();
+
+ Geom::OptRect dbox = static_cast<SPItem *>(rect)->visualBounds();
+
+ if (!dbox) {
+ return pixbuf;
+ }
+
+ double size = std::max(dbox->width(), dbox->height());
+
+ pixbuf = Glib::wrap(render_pixbuf(renderDrawing, 1, *dbox, size));
+
+ return pixbuf;
+}
+
+/** @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;
+ recurse_find_paint(document->getRoot(), urls);
+
+ for (auto const &url : urls) {
+ output.emplace_back(document, document_title, std::move(url));
+ }
+}
+
+void PaintServersDialog::on_target_changed()
+{
+ target_selected = !target_selected;
+}
+
+/** Handles the change of the dropdown for selecting paint sources */
+void PaintServersDialog::onPaintSourceDocumentChanged()
+{
+ current_store = dropdown->get_active_text();
+ icon_view->set_model(store[current_store]);
+}
+
+/** 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*> const selected_items(selection->items().begin(), selection->items().end());
+
+ if (selected_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;
+ }
+
+ // Recursively find elements in groups, if any
+ std::vector<SPObject*> items;
+ for (auto item : selected_items) {
+ std::vector<SPObject*> current_items = extract_elements(item);
+ items.insert(std::end(items), std::begin(current_items), std::end(current_items));
+ }
+
+ for (auto item : items) {
+ item->style->getFillOrStroke(target_selected)->read(paint.c_str());
+ item->updateRepr();
+ }
+
+ _cleanupUnused();
+}
+
+/** Cleans up paints that aren't used in the document anymore and updates our store accordingly */
+void PaintServersDialog::_cleanupUnused()
+{
+ auto doc = getDocument();
+ if (!doc) {
+ return;
+ }
+ doc->collectOrphans();
+
+ // We check if the removal of orphans deleted some paints for which we're still
+ // holding representations in the dialog. If that happened, we must remove these
+ // entries from our list store.
+ std::vector<Gtk::ListStore::Path> removed;
+
+ store[CURRENTDOC]->foreach(
+ [=, &removed](const Gtk::ListStore::Path &path, const Gtk::ListStore::iterator &it) -> bool
+ {
+ if (!doc->getObjectById((*it)[columns.id])) {
+ removed.push_back(path);
+ }
+ return false;
+ }
+ );
+
+ for (auto const &path : removed) {
+ store[CURRENTDOC]->erase(store[CURRENTDOC]->get_iter(path));
+ }
+
+ if (!removed.empty()) {
+ _regenerateAll();
+ }
+}
+
+/** Recursively extracts elements from groups, if any */
+std::vector<SPObject*> PaintServersDialog::extract_elements(SPObject* item)
+{
+ std::vector<SPObject*> elements;
+ std::vector<SPObject*> children = item->childList(false);
+ if (!children.size()) {
+ elements.push_back(item);
+ } else {
+ for (auto e : children) {
+ std::vector<SPObject*> current_items = extract_elements(e);
+ elements.insert(std::end(elements), std::begin(current_items), std::end(current_items));
+ }
+ }
+
+ return elements;
+}
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-basic-offset:2
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/paint-servers.h b/src/ui/dialog/paint-servers.h
new file mode 100644
index 0000000..80e2c0f
--- /dev/null
+++ b/src/ui/dialog/paint-servers.h
@@ -0,0 +1,142 @@
+// 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 "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)
+ : source_document{source_doc}
+ , doc_title{std::move(title)}
+ , id{} // id will be filled in when generating the bitmap
+ , url{paint_url}
+ , bitmap{nullptr}
+ {}
+
+ /** Two paints are considered the same if they have the same urls */
+ 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
+{
+public:
+ ~PaintServersDialog() override;
+ static PaintServersDialog &getInstance() { return *new PaintServersDialog(); }
+
+ void documentReplaced() override;
+
+private:
+ // No default constructor, noncopyable, nonassignable
+ PaintServersDialog();
+ PaintServersDialog(PaintServersDialog const &d) = delete;
+ PaintServersDialog operator=(PaintServersDialog const &d) = delete;
+
+ void _cleanupUnused();
+ void _createPaints(std::vector<PaintDescription> &collection);
+ PaintDescription _descriptionFromIterator(Gtk::ListStore::iterator const &iter) const;
+ std::vector<SPObject *> extract_elements(SPObject *item);
+ Glib::RefPtr<Gdk::Pixbuf> get_pixbuf(SPDocument *, Glib::ustring const &, Glib::ustring &);
+ void _instantiatePaint(PaintDescription &paint);
+ void _loadFromCurrentDocument();
+ void _loadPaintsFromDocument(SPDocument *document, std::vector<PaintDescription> &output);
+ void _loadStockPaints();
+ void _regenerateAll();
+ void onPaintClicked(const Gtk::TreeModel::Path &path);
+ void onPaintSourceDocumentChanged();
+ void on_target_changed();
+
+ bool target_selected; ///< whether setting fill (true) or stroke (false)
+ const Glib::ustring ALLDOCS;
+ const Glib::ustring CURRENTDOC;
+ std::map<Glib::ustring, Glib::RefPtr<Gtk::ListStore>> store;
+ Glib::ustring current_store;
+ std::map<Glib::ustring, SPDocument *> document_map;
+ SPDocument *preview_document;
+ Inkscape::Drawing renderDrawing;
+ Gtk::ComboBoxText *dropdown;
+ Gtk::IconView *icon_view;
+ Gtk::ComboBoxText *target_dropdown;
+ PaintServersColumns const columns;
+};
+
+} // 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..f3f645f
--- /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(SP_IS_GENERICELLIPSE(item))
+ referenceEllipse = SP_GENERICELLIPSE(item);
+ } else {
+ if(SP_IS_GENERICELLIPSE(item) && referenceEllipse == nullptr)
+ referenceEllipse = SP_GENERICELLIPSE(item);
+ }
+ }
+ ++count;
+ }
+
+ float cx, cy; // Center of the ellipse
+ float rx, ry; // Radiuses of the ellipse in x and y direction
+ float arcBeg, arcEnd; // begin and end angles for arcs
+ Geom::Affine transformation; // Any additional transformation to apply to the objects
+
+ if(arrangeOnEllipse)
+ {
+ if(referenceEllipse == nullptr)
+ {
+ Gtk::MessageDialog dialog(_("Couldn't find an ellipse in selection"), false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_CLOSE, true);
+ dialog.run();
+ return;
+ } else {
+ cx = referenceEllipse->cx.value;
+ cy = referenceEllipse->cy.value;
+ rx = referenceEllipse->rx.value;
+ ry = referenceEllipse->ry.value;
+ arcBeg = referenceEllipse->start;
+ arcEnd = referenceEllipse->end;
+
+ transformation = referenceEllipse->i2dt_affine();
+
+ // We decrement the count by 1 as we are not going to lay
+ // out the reference ellipse
+ --count;
+ }
+
+ } else {
+ // Read options from UI
+ cx = centerX.getValue("px");
+ cy = centerY.getValue("px");
+ rx = radiusX.getValue("px");
+ ry = radiusY.getValue("px");
+ arcBeg = angleX.getValue("rad");
+ arcEnd = angleY.getValue("rad") * yaxisdir;
+ transformation.setIdentity();
+ referenceEllipse = nullptr;
+ }
+
+ int anchor = 9;
+ if(anchorBoundingBoxRadio.get_active())
+ {
+ anchor = anchorSelector.getHorizontalAlignment() +
+ anchorSelector.getVerticalAlignment() * 3;
+ }
+
+ Geom::Point realCenter = Geom::Point(cx, cy) * transformation;
+
+ int i = 0;
+ for(auto item : tmp)
+ {
+ // Ignore the reference ellipse if any
+ if(item != referenceEllipse)
+ {
+ float angle = calcAngle(arcBeg, arcEnd, count, i);
+ Geom::Point newLocation = calcPoint(cx, cy, rx, ry, angle) * transformation;
+
+ moveToPoint(anchor, item, newLocation);
+
+ if(rotateObjectsCheckBox.get_active()) {
+ // Calculate the angle by which to rotate each object
+ angle = -atan2f(-yaxisdir * (newLocation.x() - realCenter.x()), -yaxisdir * (newLocation.y() - realCenter.y()));
+ rotateAround(item, newLocation, Geom::Rotate(angle));
+ }
+
+ ++i;
+ }
+ }
+
+ DocumentUndo::done(parent->getDesktop()->getDocument(), _("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..0cbc49c
--- /dev/null
+++ b/src/ui/dialog/print.cpp
@@ -0,0 +1,262 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Print dialog.
+ */
+/* Authors:
+ * Kees Cook <kees@outflux.net>
+ * Abhishek Sharma
+ * Patrick McDermott
+ *
+ * Copyright (C) 2007 Kees Cook
+ * Copyright (C) 2017 Patrick McDermott
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cmath>
+
+#include <gtkmm.h>
+
+#include "inkscape.h"
+#include "preferences.h"
+#include "print.h"
+
+#include "extension/internal/cairo-render-context.h"
+#include "extension/internal/cairo-renderer.h"
+#include "document.h"
+
+#include "util/units.h"
+#include "helper/png-write.h"
+#include "svg/svg-color.h"
+
+#include <glibmm/i18n.h>
+
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+Print::Print(SPDocument *doc, SPItem *base) :
+ _doc (doc),
+ _base (base)
+{
+ g_assert (_doc);
+ g_assert (_base);
+
+ _printop = Gtk::PrintOperation::create();
+
+ // set up dialog title, based on document name
+ const Glib::ustring jobname = _doc->getDocumentName() ? _doc->getDocumentName() : _("SVG Document");
+ Glib::ustring title = _("Print");
+ title += " ";
+ title += jobname;
+ _printop->set_job_name(title);
+
+ _printop->set_unit(Gtk::UNIT_POINTS);
+ Glib::RefPtr<Gtk::PageSetup> page_setup = Gtk::PageSetup::create();
+
+ // Default to a custom paper size, in case we can't find a more specific size
+ gdouble doc_width = _doc->getWidth().value("pt");
+ gdouble doc_height = _doc->getHeight().value("pt");
+ page_setup->set_paper_size(
+ Gtk::PaperSize("custom", "custom", doc_width, doc_height, Gtk::UNIT_POINTS));
+
+ // Some print drivers, like the EPSON's ESC/P-R CUPS driver, don't accept custom
+ // page sizes, so we'll try to find a known page size.
+ // GTK+'s known paper sizes always have a longer height than width, so we'll rotate
+ // the page and set its orientation to landscape as necessary in order to match a paper size.
+ // Unfortunately, some printers, like Epilog laser cutters, don't understand landscape
+ // mode.
+ // As a compromise, we'll only rotate the page if we actually find a matching paper size,
+ // since laser cutter beds tend to be custom sizes.
+ Gtk::PageOrientation orientation = Gtk::PAGE_ORIENTATION_PORTRAIT;
+ if (_doc->getWidth().value("pt") > _doc->getHeight().value("pt")) {
+ orientation = Gtk::PAGE_ORIENTATION_REVERSE_LANDSCAPE;
+ std::swap(doc_width, doc_height);
+ }
+
+ // attempt to match document size against known paper sizes
+ std::vector<Gtk::PaperSize> known_sizes = Gtk::PaperSize::get_paper_sizes(false);
+ for (auto& size : known_sizes) {
+ if (fabs(size.get_width(Gtk::UNIT_POINTS) - doc_width) >= 1.0) {
+ // width (short edge) doesn't match
+ continue;
+ }
+ if (fabs(size.get_height(Gtk::UNIT_POINTS) - doc_height) >= 1.0) {
+ // height (short edge) doesn't match
+ continue;
+ }
+ // size matches
+ page_setup->set_paper_size(size);
+ page_setup->set_orientation(orientation);
+ break;
+ }
+
+ _printop->set_default_page_setup(page_setup);
+ _printop->set_use_full_page(true);
+
+ // set up signals
+ _workaround._doc = _doc;
+ _workaround._base = _base;
+ _workaround._tab = &_tab;
+ _printop->signal_create_custom_widget().connect(sigc::mem_fun(*this, &Print::create_custom_widget));
+ _printop->signal_begin_print().connect(sigc::mem_fun(*this, &Print::begin_print));
+ _printop->signal_draw_page().connect(sigc::mem_fun(*this, &Print::draw_page));
+
+ // build custom preferences tab
+ _printop->set_custom_tab_label(_("Rendering"));
+}
+
+void Print::draw_page(const Glib::RefPtr<Gtk::PrintContext>& context, int /*page_nr*/)
+{
+ // TODO: If the user prints multiple copies we render the whole page for each copy
+ // It would be more efficient to render the page once (e.g. in "begin_print")
+ // and simply print this result as often as necessary
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ //printf("%s %d\n",__FUNCTION__, page_nr);
+
+ if (_workaround._tab->as_bitmap()) {
+ // Render as exported PNG
+ prefs->setBool("/dialogs/printing/asbitmap", true);
+ gdouble width = (_workaround._doc)->getWidth().value("px");
+ gdouble height = (_workaround._doc)->getHeight().value("px");
+ gdouble dpi = _workaround._tab->bitmap_dpi();
+ prefs->setDouble("/dialogs/printing/dpi", dpi);
+
+ std::string tmp_png;
+ std::string tmp_base = "inkscape-print-png-XXXXXX";
+
+ int tmp_fd;
+ if ( (tmp_fd = Glib::file_open_tmp(tmp_png, tmp_base)) >= 0) {
+ close(tmp_fd);
+
+ guint32 bgcolor = 0x00000000;
+ Inkscape::XML::Node *nv = _workaround._doc->getReprNamedView();
+ if (nv && nv->attribute("pagecolor")){
+ bgcolor = sp_svg_read_color(nv->attribute("pagecolor"), 0xffffff00);
+ }
+ if (nv && nv->attribute("inkscape:pageopacity")){
+ double opacity = nv->getAttributeDouble("inkscape:pageopacity", 1.0);
+ bgcolor |= SP_COLOR_F_TO_U(opacity);
+ }
+
+ sp_export_png_file(_workaround._doc, tmp_png.c_str(), 0.0, 0.0,
+ width, height,
+ (unsigned long)(Inkscape::Util::Quantity::convert(width, "px", "in") * dpi),
+ (unsigned long)(Inkscape::Util::Quantity::convert(height, "px", "in") * dpi),
+ dpi, dpi, bgcolor, nullptr, nullptr, true, std::vector<SPItem*>());
+
+ // This doesn't seem to work:
+ //context->set_cairo_context ( Cairo::Context::create (Cairo::ImageSurface::create_from_png (tmp_png) ), dpi, dpi );
+ //
+ // so we'll use a surface pattern blat instead...
+ //
+ // but the C++ interface isn't implemented in cairomm:
+ //context->get_cairo_context ()->set_source_surface(Cairo::ImageSurface::create_from_png (tmp_png) );
+ //
+ // so do it in C:
+ {
+ 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, TRUE, 0., nullptr);
+ if (ret) {
+ renderer.renderItem(ctx, _workaround._base);
+ ctx->finish(false); // do not finish the cairo_surface_t - it's owned by our GtkPrintContext!
+ }
+ else {
+ g_warning("%s", _("Could not set up Document"));
+ }
+ }
+ else {
+ g_warning("%s", _("Failed to set CairoRenderContext"));
+ }
+
+ // Clean up
+ renderer.destroyContext(ctx);
+ }
+
+}
+
+Gtk::Widget *Print::create_custom_widget()
+{
+ //printf("%s\n",__FUNCTION__);
+ return &_tab;
+}
+
+void Print::begin_print(const Glib::RefPtr<Gtk::PrintContext>&)
+{
+ //printf("%s\n",__FUNCTION__);
+ _printop->set_n_pages(1);
+}
+
+Gtk::PrintOperationResult Print::run(Gtk::PrintOperationAction, Gtk::Window &parent_window)
+{
+ // Remember to restore the previous print settings
+ _printop->set_print_settings(SP_ACTIVE_DESKTOP->printer_settings._gtk_print_settings);
+
+ try {
+ Gtk::PrintOperationResult res = _printop->run(Gtk::PRINT_OPERATION_ACTION_PRINT_DIALOG, parent_window);
+
+ // Save printer settings (but only on success)
+ if (res == Gtk::PRINT_OPERATION_RESULT_APPLY) {
+ SP_ACTIVE_DESKTOP->printer_settings._gtk_print_settings = _printop->get_print_settings();
+ }
+
+ return res;
+ } catch (const Glib::Error &e) {
+ g_warning("Failed to print '%s': %s", _doc->getDocumentName(), e.what().c_str());
+ }
+
+ return Gtk::PRINT_OPERATION_RESULT_ERROR;
+}
+
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/print.h b/src/ui/dialog/print.h
new file mode 100644
index 0000000..d015210
--- /dev/null
+++ b/src/ui/dialog/print.h
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Print dialog
+ */
+/* Authors:
+ * Kees Cook <kees@outflux.net>
+ *
+ * Copyright (C) 2007 Kees Cook
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_PRINT_H
+#define INKSCAPE_UI_DIALOG_PRINT_H
+
+#include "ui/widget/rendering-options.h"
+#include <gtkmm/printoperation.h> // GtkMM
+
+class SPItem;
+class SPDocument;
+
+
+/*
+ * gtk 2.12.0 has a bug (http://bugzilla.gnome.org/show_bug.cgi?id=482089)
+ * where it fails to correctly deal with gtkmm signal management. As a result
+ * we have call gtk directly instead of doing a much cleaner version of
+ * this printing dialog, using full gtkmmification. (The bug was fixed
+ * in 2.12.1, so when the Inkscape gtk minimum version is bumped there,
+ * we can revert Inkscape commit 16865.
+ */
+struct workaround_gtkmm
+{
+ SPDocument *_doc;
+ SPItem *_base;
+ Inkscape::UI::Widget::RenderingOptions *_tab;
+};
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+struct PrinterSettings {
+ Glib::RefPtr<Gtk::PrintSettings> _gtk_print_settings;
+};
+
+class Print {
+public:
+ Print(SPDocument *doc, SPItem *base);
+ Gtk::PrintOperationResult run(Gtk::PrintOperationAction, Gtk::Window &parent_window);
+
+protected:
+
+private:
+ Glib::RefPtr<Gtk::PrintOperation> _printop;
+ SPDocument *_doc;
+ SPItem *_base;
+ Inkscape::UI::Widget::RenderingOptions _tab;
+
+ struct workaround_gtkmm _workaround;
+
+ void draw_page(const Glib::RefPtr<Gtk::PrintContext>& context, int /*page_nr*/);
+ Gtk::Widget *create_custom_widget();
+ void begin_print(const Glib::RefPtr<Gtk::PrintContext>&);
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_PRINT_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/prototype.cpp b/src/ui/dialog/prototype.cpp
new file mode 100644
index 0000000..97649c8
--- /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::manage(new 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()
+{
+ Gtk::Window *window = dynamic_cast<Gtk::Window *>(get_toplevel());
+ if (window) {
+ std::cout << "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..9bf5b59
--- /dev/null
+++ b/src/ui/dialog/prototype.h
@@ -0,0 +1,71 @@
+// 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() override { std::cout << "Prototype::~Prototype()" << std::endl; }
+ static Prototype &getInstance() { return *new Prototype(); }
+
+ void documentReplaced(SPDocument *document) override;
+ void selectionChanged(Inkscape::Selection *selection) override;
+
+private:
+ // No default constructor, noncopyable, nonassignable
+ Prototype();
+ Prototype(Prototype const &d) = delete;
+ Prototype operator=(Prototype const &d) = delete;
+
+ // 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..30765fd
--- /dev/null
+++ b/src/ui/dialog/selectorsdialog.cpp
@@ -0,0 +1,1350 @@
+// 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 & /*node*/,
+ Inkscape::Util::ptr_shared /*old_content*/,
+ Inkscape::Util::ptr_shared /*new_content*/)
+{
+
+ 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 &/*node*/,
+ Inkscape::XML::Node &child,
+ Inkscape::XML::Node */*prev*/ ) override
+ {
+ _selectorsdialog->_nodeAdded(child);
+ }
+
+ void notifyChildRemoved( Inkscape::XML::Node &/*node*/,
+ Inkscape::XML::Node &child,
+ Inkscape::XML::Node */*prev*/ ) override
+ {
+ _selectorsdialog->_nodeRemoved(child);
+ }
+
+ void notifyAttributeChanged( Inkscape::XML::Node &node,
+ GQuark qname,
+ Util::ptr_shared /*old_value*/,
+ Util::ptr_shared /*new_value*/ ) override {
+
+ static GQuark const CODE_id = g_quark_from_static_string("id");
+ static GQuark const CODE_class = g_quark_from_static_string("class");
+
+ if (qname == CODE_id || qname == CODE_class) {
+ _selectorsdialog->_nodeChanged(node);
+ }
+ }
+
+ SelectorsDialog *_selectorsdialog;
+};
+
+void SelectorsDialog::_nodeAdded(Inkscape::XML::Node &node)
+{
+ _readStyleElement();
+ _selectRow();
+}
+
+void SelectorsDialog::_nodeRemoved(Inkscape::XML::Node &repr)
+{
+ if (_textNode == &repr) {
+ _textNode = nullptr;
+ }
+
+ _readStyleElement();
+ _selectRow();
+}
+
+void SelectorsDialog::_nodeChanged(Inkscape::XML::Node &object)
+{
+
+ g_debug("SelectorsDialog::NodeChanged");
+
+ _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")
+ , _updating(false)
+ , _textNode(nullptr)
+ , _scrollpos(0)
+ , _scrollock(false)
+{
+ g_debug("SelectorsDialog::SelectorsDialog");
+
+ m_nodewatcher.reset(new SelectorsDialog::NodeWatcher(this));
+ m_styletextwatcher.reset(new SelectorsDialog::NodeObserver(this));
+
+ // Tree
+ Inkscape::UI::Widget::IconRenderer * addRenderer = manage(
+ new Inkscape::UI::Widget::IconRenderer() );
+ addRenderer->add_icon("edit-delete");
+ addRenderer->add_icon("list-add");
+ addRenderer->add_icon("empty-icon");
+ _store = TreeStore::create(this);
+ _treeView.set_model(_store);
+
+ // 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::manage(new StyleDialog);
+ _style_dialog->set_name("StyleDialog");
+ _paned.pack1(*_style_dialog, Gtk::SHRINK);
+ _paned.pack2(_selectors_box, true, true);
+ _paned.set_wide_handle(true);
+ Gtk::Box *contents = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
+ contents->pack_start(_paned, Gtk::PACK_EXPAND_WIDGET);
+ contents->pack_start(_button_box, false, false, 0);
+ contents->set_valign(Gtk::ALIGN_FILL);
+ contents->child_property_fill(_paned);
+ 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()->selection->clear();
+ Gtk::TreeModel::iterator iter = _store->get_iter(path);
+ if (iter) {
+ Gtk::TreeModel::Row row = *iter;
+ if (row[_mColumns._colObj]) {
+ getDesktop()->selection->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()->selection->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();
+ SPObject *obj = nullptr;
+ if (!selection->isEmpty()) {
+ obj = selection->objects().back();
+ } else {
+ _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..1e14cfe
--- /dev/null
+++ b/src/ui/dialog/selectorsdialog.h
@@ -0,0 +1,198 @@
+// 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:
+ // No default constructor, noncopyable, nonassignable
+ SelectorsDialog();
+ ~SelectorsDialog() override;
+ SelectorsDialog(SelectorsDialog const &d) = delete;
+ SelectorsDialog operator=(SelectorsDialog const &d) = delete;
+ static SelectorsDialog &getInstance() { return *new SelectorsDialog(); }
+
+ 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;
+ bool _scrollock;
+ bool _updating; // Prevent cyclic actions: read <-> write, select via dialog <-> via desktop
+ Inkscape::XML::Node *m_root = nullptr;
+ Inkscape::XML::Node *_textNode; // Track so we know when to add a NodeObserver.
+
+ 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 Dialogc
+} // namespace UI
+} // namespace Inkscape
+
+#endif // SELECTORSDIALOG_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/spellcheck.cpp b/src/ui/dialog/spellcheck.cpp
new file mode 100644
index 0000000..41df154
--- /dev/null
+++ b/src/ui/dialog/spellcheck.cpp
@@ -0,0 +1,761 @@
+// 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()
+{
+ clearRects();
+ disconnect();
+}
+
+void SpellCheck::documentReplaced()
+{
+ if (_working) {
+ // Stop and start on the new desktop
+ finished();
+ onStart();
+ }
+}
+
+void SpellCheck::clearRects()
+{
+ for(auto rect : _rects) {
+ rect->hide();
+ delete rect;
+ }
+ _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 (SP_IS_DEFS(r))
+ return; // we're not interested in items in defs
+
+ if (!strcmp(r->getRepr()->name(), "svg:metadata")) {
+ return; // we're not interested in metadata
+ }
+
+ if (auto desktop = getDesktop()) {
+ for (auto& child: r->children) {
+ if (auto item = dynamic_cast<SPItem *>(&child)) {
+ if (!child.cloned && !desktop->layerManager().isLayer(item)) {
+ if ((hidden || !desktop->itemIsHidden(item)) && (locked || !item->isLocked())) {
+ if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(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 (SP_IS_STRING(char_item)) {
+ int this_char = *text_iter;
+ if (this_char == '\'' || this_char == 0x2019) {
+ Inkscape::Text::Layout::iterator end_t = _end_w;
+ end_t.nextCharacter();
+ _layout->getSourceOfCharacter(end_t, &char_item, &text_iter);
+ if (SP_IS_STRING(char_item)) {
+ int this_char = *text_iter;
+ if (g_ascii_isalpha(this_char)) { // 's
+ _end_w.nextEndOfWord();
+ _word = sp_te_get_string_multiline (_text, _begin_w, _end_w);
+ }
+ }
+ }
+ }
+
+ // skip words containing digits
+ if (_prefs->getInt(_prefs_path + "ignorenumbers") != 0) {
+ bool digits = false;
+ for (unsigned int i : _word) {
+ if (g_unichar_isdigit(i)) {
+ digits = true;
+ break;
+ }
+ }
+ if (digits) {
+ return false;
+ }
+ }
+
+ // skip ALL-CAPS words
+ if (_prefs->getInt(_prefs_path + "ignoreallcaps") != 0) {
+ bool allcaps = true;
+ for (unsigned int i : _word) {
+ if (!g_unichar_isupper(i)) {
+ allcaps = false;
+ break;
+ }
+ }
+ if (allcaps) {
+ return false;
+ }
+ }
+
+ int have = 0;
+
+#if 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.push_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, 1.0);
+ }
+
+ // select text; if in Text tool, position cursor to the beginning of word
+ // unless it is already in the word
+ if (desktop->selection->singleItem() != _text) {
+ desktop->selection->set (_text);
+ }
+
+ if (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->selection->set (_text);
+ else if (*cursor <= _begin_w || *cursor >= _end_w)
+ sp_text_context_place_cursor (SP_TEXT_CONTEXT(desktop->event_context), _text, _begin_w);
+ }
+
+#if 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.back()->hide();
+ delete _rects.back();
+ _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..ddef42f
--- /dev/null
+++ b/src/ui/dialog/spellcheck.h
@@ -0,0 +1,282 @@
+// 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"
+
+#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 SpellCheck &getInstance() { return *new SpellCheck(); }
+
+ 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<Inkscape::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..21510cf
--- /dev/null
+++ b/src/ui/dialog/startup.cpp
@@ -0,0 +1,849 @@
+// 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 "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 "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 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);
+ }
+ 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;
+};
+
+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->icon);
+ this->add(this->filename);
+ this->add(this->width);
+ this->add(this->height);
+ }
+ Gtk::TreeModelColumn<Glib::ustring> name;
+ Gtk::TreeModelColumn<Glib::ustring> icon;
+ Gtk::TreeModelColumn<Glib::ustring> filename;
+ Gtk::TreeModelColumn<Glib::ustring> width;
+ Gtk::TreeModelColumn<Glib::ustring> height;
+};
+
+
+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("kinds", kinds);
+ builder->get_widget("banner", banners);
+ builder->get_widget("themes", themes);
+ builder->get_widget("recent_treeview", recent_treeview);
+
+ // 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));
+ kinds->signal_switch_page().connect(sigc::mem_fun(*this, &StartScreen::on_kind_changed));
+ load_btn->set_sensitive(true);
+
+ for (auto widget : kinds->get_children()) {
+ auto container = dynamic_cast<Gtk::Container *>(widget);
+ if (container) {
+ widget = container->get_children()[0];
+ }
+ auto template_list = dynamic_cast<Gtk::IconView *>(widget);
+ if (template_list) {
+ template_list->signal_selection_changed().connect([=]() { response(GTK_RESPONSE_APPLY); });
+ }
+ }
+
+ show_toggle->signal_clicked().connect(sigc::mem_fun(*this, &StartScreen::show_toggle));
+ load_btn->signal_clicked().connect(sigc::mem_fun(*this, &StartScreen::load_document));
+ new_btn->signal_clicked().connect([=] { response(GTK_RESPONSE_APPLY); });
+ 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;
+ _first_open = prefs->getBool(opt_shown, false);
+ if(!_first_open) {
+ 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()
+{
+ NameIdCols 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();
+
+ // Open [other]
+ Gtk::TreeModel::Row first_row = *(store->append());
+ first_row[cols.col_name] = _("Browse for other files...");
+ first_row[cols.col_id] = "";
+ 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();
+ }
+ }
+ }
+}
+
+/**
+ * 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()
+{
+ Glib::ustring filename = sp_file_default_template_uri();
+ Glib::ustring width = "";
+ Glib::ustring height = "";
+
+ // Find requested file name.
+ Glib::RefPtr<Gio::File> file;
+ if (kinds) {
+ Gtk::Widget *selected_widget = kinds->get_children()[kinds->get_current_page()];
+ auto container = dynamic_cast<Gtk::Container *>(selected_widget);
+ if (container) {
+ selected_widget = container->get_children()[0];
+ }
+
+ auto template_list = dynamic_cast<Gtk::IconView *>(selected_widget);
+ if (template_list) {
+ auto items = template_list->get_selected_items();
+ if (!items.empty()) {
+ auto iter = template_list->get_model()->get_iter(items[0]);
+ Gtk::TreeModel::Row row = *iter;
+ if (row) {
+ TemplateCols cols;
+ Glib::ustring template_filename = row[cols.filename];
+ if (!(template_filename == "-")) {
+ filename = Resource::get_filename_string(
+ Resource::TEMPLATES, template_filename.c_str(), true);
+ }
+ // This isn't used on opening, just for checking it's existence.
+ file = Gio::File::create_for_path(filename);
+ width = row[cols.width];
+ height = row[cols.height];
+ }
+ }
+ }
+ }
+
+ if (!file) {
+ // Failure to open, so open up a new document instead.
+ file = Gio::File::create_for_path(filename);
+ }
+
+ if (!file) {
+ // We're really messed up... so give up!
+ std::cerr << "StartScreen::load_document(): Failed to find: " << filename << std::endl;
+ return;
+ }
+
+ // Now we have filename, open document.
+ auto app = InkscapeApplication::instance();
+
+ // If it was a template file, modify the document according to user's input.
+ _document = app->document_new (filename);
+ auto nv = _document->getNamedView();
+
+ if (!width.empty()) {
+ // Set the width, height and default display units for the selected template
+ auto q_width = unit_table.parseQuantity(width);
+ _document->setWidthAndHeight(q_width, unit_table.parseQuantity(height), true);
+ nv->setAttribute("inkscape:document-units", q_width.unit->abbr);
+ _document->setDocumentScale(1.0);
+ }
+
+ DocumentUndo::clearUndo(_document);
+ _document->setModifiedSinceSave(false);
+}
+
+/**
+ * Called when load button clicked.
+ */
+void
+StartScreen::load_document()
+{
+ NameIdCols 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) {
+ kinds = nullptr;
+ if (_first_open) {
+ // Disable the screen if the user cancels on their first run.
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool("/options/boot/enabled", false);
+ }
+ }
+ if (response_id != GTK_RESPONSE_OK) {
+ // Most actions cause a new document to appear.
+ 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);
+
+ } 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..b555aae
--- /dev/null
+++ b/src/ui/dialog/startup.h
@@ -0,0 +1,85 @@
+// 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 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::Notebook *kinds = nullptr;
+ Gtk::Fixed *banners = nullptr;
+ Gtk::ComboBox *themes = nullptr;
+ Gtk::TreeView *recent_treeview = nullptr;
+ Gtk::Button *load_btn = nullptr;
+
+ SPDocument* _document = nullptr;
+
+ bool _first_open = false;
+};
+
+
+} // 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..1b05e78
--- /dev/null
+++ b/src/ui/dialog/styledialog.cpp
@@ -0,0 +1,1609 @@
+// 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")
+ , _updating(false)
+ , _textNode(nullptr)
+ , _scrollpos(0)
+ , _deleted_pos(0)
+ , _deletion(false)
+{
+ g_debug("StyleDialog::StyleDialog");
+
+ m_nodewatcher.reset(new StyleDialog::NodeWatcher(this));
+ m_styletextwatcher.reset(new StyleDialog::NodeObserver(this));
+
+ // Pack widgets
+ _mainBox.pack_start(_scrolledWindow, Gtk::PACK_EXPAND_WIDGET);
+ _scrolledWindow.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ _styleBox.set_orientation(Gtk::ORIENTATION_VERTICAL);
+ _styleBox.set_valign(Gtk::ALIGN_START);
+ _scrolledWindow.add(_styleBox);
+ _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)
+ 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);
+ }
+}
+
+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");
+ _deletion = false;
+ _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 == "overflow") {
+ _setAutocompletion(entry, enum_overflow);
+ } else if (name == "clip-rule") {
+ _setAutocompletion(entry, enum_clip_rule);
+ } else if (name == "color-interpolation") {
+ _setAutocompletion(entry, enum_color_interpolation);
+ }
+ entry->signal_key_release_event().connect(
+ sigc::bind(sigc::mem_fun(*this, &StyleDialog::_onValueKeyReleased), entry));
+ entry->signal_key_press_event().connect(
+ sigc::bind(sigc::mem_fun(*this, &StyleDialog::_onValueKeyPressed), entry));
+ }
+}
+
+void StyleDialog::_startNameEdit(Gtk::CellEditable *cell, const Glib::ustring &path)
+{
+ _deletion = false;
+ g_debug("StyleDialog::_startNameEdit");
+ _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);
+ if (!styledialog->_deletion) {
+ auto selection = styledialog->_current_css_tree->get_selection();
+ Gtk::TreeIter iter = *(selection->get_selected());
+ Gtk::TreeModel::Path model = (Gtk::TreeModel::Path)iter;
+ if (model == styledialog->_current_path) {
+ styledialog->_current_css_tree->set_cursor(styledialog->_current_path, *styledialog->_current_value_col,
+ true);
+ }
+ }
+ return FALSE;
+}
+
+/**
+ * @brief StyleDialog::nameEdited
+ * @param event
+ * @return
+ * Called when the name is edited in the TreeView editable column
+ */
+void StyleDialog::_nameEdited(const Glib::ustring &path, const Glib::ustring &name, Glib::RefPtr<Gtk::TreeStore> store,
+ Gtk::TreeView *css_tree)
+{
+ g_debug("StyleDialog::_nameEdited");
+
+ _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..dd2171e
--- /dev/null
+++ b/src/ui/dialog/styledialog.h
@@ -0,0 +1,199 @@
+// 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:
+ // No default constructor, noncopyable, nonassignable
+ StyleDialog();
+ ~StyleDialog() override;
+ StyleDialog(StyleDialog const &d) = delete;
+ StyleDialog operator=(StyleDialog const &d) = delete;
+
+ void documentReplaced() override;
+ void selectionChanged(Selection *selection) override;
+
+ static StyleDialog &getInstance() { return *new StyleDialog(); }
+ void setCurrentSelector(Glib::ustring current_selector);
+ Gtk::TreeView *_current_css_tree;
+ Gtk::TreeViewColumn *_current_value_col;
+ Gtk::TreeModel::Path _current_path;
+ bool _deletion;
+ Glib::ustring fixCSSSelectors(Glib::ustring selector);
+ void readStyleElement();
+
+ private:
+ // Monitor <style> element for changes.
+ class NodeObserver;
+ // Monitor all objects for addition/removal/attribute change
+ class NodeWatcher;
+ Glib::RefPtr<Glib::Regex> r_props = Glib::Regex::create("\\s*;\\s*");
+ Glib::RefPtr<Glib::Regex> r_pair = Glib::Regex::create("\\s*:\\s*");
+ void _nodeAdded(Inkscape::XML::Node &repr);
+ void _nodeRemoved(Inkscape::XML::Node &repr);
+ void _nodeChanged(Inkscape::XML::Node &repr);
+ void 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;
+ // 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;
+ 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; // Track so we know when to add a NodeObserver.
+ bool _updating; // 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..e7a34ff
--- /dev/null
+++ b/src/ui/dialog/svg-fonts-dialog.cpp
@@ -0,0 +1,1795 @@
+// 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 (SP_IS_FONTFACE(&node)){
+ o = &node;
+ continue;
+ }
+ break;
+ default:
+ o = nullptr;
+ }
+ }
+
+ const gchar* name = (const gchar*)sp_attribute_name(this->attr);
+ if(name && o) {
+ o->setAttribute((const gchar*) name, this->entry.get_text());
+ o->parent->requestModified(SP_OBJECT_MODIFIED_FLAG);
+
+ Glib::ustring undokey = "svgfonts:";
+ undokey += name;
+ DocumentUndo::maybeDone(o->document, undokey.c_str(), _("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 (SP_IS_FONTFACE(&node)){
+ o = &node;
+ continue;
+ }
+ }
+ break;
+
+ default:
+ o = nullptr;
+ }
+
+ const gchar* name = (const gchar*)sp_attribute_name(this->attr);
+ if(name && o) {
+ std::ostringstream temp;
+ temp << this->spin.get_value();
+ o->setAttribute(name, temp.str());
+ o->parent->requestModified(SP_OBJECT_MODIFIED_FLAG);
+
+ Glib::ustring undokey = "svgfonts:";
+ undokey += name;
+ DocumentUndo::maybeDone(o->document, undokey.c_str(), _("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 (SP_IS_GLYPH(&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 dynamic_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();
+ SPFont* f = SP_FONT(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 = dynamic_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 (SP_IS_FONTFACE(&obj)){
+ _familyname_entry->set_text((SP_FONTFACE(&obj))->font_family);
+ _units_per_em_spin->set_value((SP_FONTFACE(&obj))->units_per_em);
+ _ascent_spin->set_value((SP_FONTFACE(&obj))->ascent);
+ _descent_spin->set_value((SP_FONTFACE(&obj))->descent);
+ _x_height_spin->set_value((SP_FONTFACE(&obj))->x_height);
+ _cap_height_spin->set_value((SP_FONTFACE(&obj))->cap_height);
+ }
+ }
+}
+
+void SvgFontsDialog::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 = dynamic_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 (SP_IS_GLYPH(&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 (SP_IS_HKERN(&node)){
+ Gtk::TreeModel::Row row = *(_KerningPairsListStore->append());
+ row[_KerningPairsListColumns.first_glyph] = (static_cast<SPGlyphKerning*>(&node))->u1->attribute_string().c_str();
+ row[_KerningPairsListColumns.second_glyph] = (static_cast<SPGlyphKerning*>(&node))->u2->attribute_string().c_str();
+ row[_KerningPairsListColumns.kerning_value] = (static_cast<SPGlyphKerning*>(&node))->k;
+ row[_KerningPairsListColumns.spnode] = static_cast<SPGlyphKerning*>(&node);
+ }
+ }
+ }
+}
+
+// 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 (SP_IS_FONTFACE(&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 (SP_IS_MISSING_GLYPH(&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 (SP_IS_MISSING_GLYPH(&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 << 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 = dynamic_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(dynamic_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 (SP_IS_HKERN(&node) && (static_cast<SPGlyphKerning*>(&node))->u1->contains((gchar) first_glyph.get_active_text().c_str()[0])
+ && (static_cast<SPGlyphKerning*>(&node))->u2->contains((gchar) second_glyph.get_active_text().c_str()[0]) ){
+ this->kerning_pair = static_cast<SPGlyphKerning*>(&node);
+ continue;
+ }
+ }
+
+ if (this->kerning_pair) return; //We already have this kerning pair
+
+ 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
+ this->kerning_pair = SP_HKERN( 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
+ SPFont *f = SP_FONT( document->getObjectByRepr(repr) );
+
+ g_assert(f != nullptr);
+ g_assert(SP_IS_FONT(f));
+ Inkscape::GC::release(mg);
+ Inkscape::GC::release(repr);
+ return f;
+}
+
+void set_font_family(SPFont* font, char* str){
+ if (!font) return;
+ for (auto& obj: font->children) {
+ if (SP_IS_FONTFACE(&obj)){
+ //XML Tree being directly used here while it shouldn't be.
+ obj.setAttribute("font-family", str);
+ }
+ }
+
+ DocumentUndo::done(font->document, _("Set font family"), "");
+}
+
+void SvgFontsDialog::add_font(){
+ SPDocument* doc = this->getDesktop()->getDocument();
+ SPFont* font = new_font(doc);
+
+ const int count = _model->children().size();
+ std::ostringstream os, os2;
+ os << _("font") << " " << count;
+ font->setLabel(os.str().c_str());
+
+ os2 << "SVGFont " << count;
+ for (auto& obj: font->children) {
+ if (SP_IS_FONTFACE(&obj)){
+ //XML Tree being directly used here while it shouldn't be.
+ obj.setAttribute("font-family", os2.str());
+ }
+ }
+
+ update_fonts(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..1138bc5
--- /dev/null
+++ b/src/ui/dialog/svg-fonts-dialog.h
@@ -0,0 +1,386 @@
+// 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 {};
+
+ static SvgFontsDialog &getInstance() { return *new SvgFontsDialog(); }
+
+ 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..2d6cc4e
--- /dev/null
+++ b/src/ui/dialog/svg-preview.cpp
@@ -0,0 +1,477 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Implementation of the file dialog interfaces defined in filedialogimpl.h.
+ */
+/* Authors:
+ * Bob Jamison
+ * Joel Holdsworth
+ * Bruno Dilly
+ * Other dudes from The Inkscape Organization
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2004-2007 Bob Jamison
+ * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl>
+ * Copyright (C) 2007-2008 Joel Holdsworth
+ * Copyright (C) 2004-2007 The Inkscape Organization
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <iostream>
+#include <fstream>
+
+#include <glibmm/i18n.h>
+#include <glib/gstdio.h> // GStatBuf
+
+#include "svg-preview.h"
+
+#include "document.h"
+#include "ui/view/svg-view-widget.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/*#########################################################################
+### SVG Preview Widget
+#########################################################################*/
+
+bool SVGPreview::setDocument(SPDocument *doc)
+{
+ if (viewer) {
+ viewer->setDocument(doc);
+ } else {
+ viewer = Gtk::manage(new Inkscape::UI::View::SVGViewWidget(doc));
+ pack_start(*viewer, true, true);
+ }
+
+ if (document) {
+ delete document;
+ }
+ document = doc;
+
+ show_all();
+
+ return true;
+}
+
+
+bool SVGPreview::setFileName(Glib::ustring &theFileName)
+{
+ Glib::ustring fileName = theFileName;
+
+ fileName = Glib::filename_to_utf8(fileName);
+
+ /**
+ * I don't know why passing false to keepalive is bad. But it
+ * prevents the display of an svg with a non-ascii filename
+ */
+ SPDocument *doc = SPDocument::createNewDoc(fileName.c_str(), true);
+ if (!doc) {
+ g_warning("SVGView: error loading document '%s'\n", fileName.c_str());
+ return false;
+ }
+
+ setDocument(doc);
+
+ return true;
+}
+
+
+
+bool SVGPreview::setFromMem(char const *xmlBuffer)
+{
+ if (!xmlBuffer)
+ return false;
+
+ gint len = (gint)strlen(xmlBuffer);
+ SPDocument *doc = SPDocument::createNewDocFromMem(xmlBuffer, len, false);
+ if (!doc) {
+ g_warning("SVGView: error loading buffer '%s'\n", xmlBuffer);
+ return false;
+ }
+
+ setDocument(doc);
+
+ return true;
+}
+
+
+
+void SVGPreview::showImage(Glib::ustring &theFileName)
+{
+ Glib::ustring fileName = theFileName;
+
+ // Let's get real width and height from SVG file. These are template
+ // files so we assume they are well formed.
+
+ // std::cout << "SVGPreview::showImage: " << theFileName << std::endl;
+ std::string width;
+ std::string height;
+
+ /*#####################################
+ # LET'S HAVE SOME FUN WITH SVG!
+ # Instead of just loading an image, why
+ # don't we make a lovely little svg and
+ # display it nicely?
+ #####################################*/
+
+ // Arbitrary size of svg doc -- rather 'portrait' shaped
+ gint previewWidth = 400;
+ gint previewHeight = 600;
+
+ // Get some image info. Smart pointer does not need to be deleted
+ Glib::RefPtr<Gdk::Pixbuf> img(nullptr);
+ try
+ {
+ img = Gdk::Pixbuf::create_from_file(fileName);
+ }
+ catch (const Glib::FileError &e)
+ {
+ g_message("caught Glib::FileError in SVGPreview::showImage");
+ return;
+ }
+ catch (const Gdk::PixbufError &e)
+ {
+ g_message("Gdk::PixbufError in SVGPreview::showImage");
+ return;
+ }
+ catch (...)
+ {
+ g_message("Caught ... in SVGPreview::showImage");
+ return;
+ }
+
+ gint imgWidth = img->get_width();
+ gint imgHeight = img->get_height();
+
+ Glib::ustring svg = ".svg";
+ if (hasSuffix(fileName, svg)) {
+ std::ifstream input(theFileName.c_str());
+ if( !input ) {
+ std::cerr << "SVGPreview::showImage: Failed to open file: " << theFileName << std::endl;
+ } else {
+
+ 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();
+ }
+
+ }
+ }
+ }
+
+ // TODO: replace int to string conversion with std::to_string when fully C++11 compliant
+ if (height.empty() || width.empty()) {
+ std::ostringstream s_width;
+ std::ostringstream s_height;
+ s_width << imgWidth;
+ s_height << imgHeight;
+ width = s_width.str();
+ height = s_height.str();
+ }
+
+ // Find the minimum scale to fit the image inside the preview area
+ double scaleFactorX = (0.9 * (double)previewWidth) / ((double)imgWidth);
+ double scaleFactorY = (0.9 * (double)previewHeight) / ((double)imgHeight);
+ double scaleFactor = scaleFactorX;
+ if (scaleFactorX > scaleFactorY)
+ scaleFactor = scaleFactorY;
+
+ // Now get the resized values
+ gint scaledImgWidth = (int)(scaleFactor * (double)imgWidth);
+ gint scaledImgHeight = (int)(scaleFactor * (double)imgHeight);
+
+ // center the image on the area
+ gint imgX = (previewWidth - scaledImgWidth) / 2;
+ gint imgY = (previewHeight - scaledImgHeight) / 2;
+
+ // wrap a rectangle around the image
+ gint rectX = imgX - 1;
+ gint rectY = imgY - 1;
+ gint rectWidth = scaledImgWidth + 2;
+ gint rectHeight = scaledImgHeight + 2;
+
+ // Our template. Modify to taste
+ gchar const *xformat = R"A(
+<svg width="%d" height="%d"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <rect width="100%" height="100%" style="fill:#eeeeee"/>
+ <image x="%d" y="%d" width="%d" height="%d" xlink:href="%s"/>
+ <rect x="%d" y="%d" width="%d" height="%d" style="fill:none;stroke:black"/>
+ <text x="50%" y="55%" style="font-family:sans-serif;font-size:24px;text-anchor:middle">%s x %s</text>
+</svg>
+)A";
+
+ // if (!Glib::get_charset()) //If we are not utf8
+ fileName = Glib::filename_to_utf8(fileName);
+ // Filenames in xlinks are decoded, so any % will break without this.
+ auto encodedName = Glib::uri_escape_string(fileName);
+
+ // Fill in the template
+ /* FIXME: Do proper XML quoting for fileName. */
+ gchar *xmlBuffer =
+ g_strdup_printf(xformat, previewWidth, previewHeight, imgX, imgY, scaledImgWidth, scaledImgHeight,
+ encodedName.c_str(), rectX, rectY, rectWidth, rectHeight, width.c_str(), height.c_str() );
+
+ // g_message("%s\n", xmlBuffer);
+
+ // now show it!
+ setFromMem(xmlBuffer);
+ g_free(xmlBuffer);
+}
+
+
+
+void SVGPreview::showNoPreview()
+{
+ // Are we already showing it?
+ if (showingNoPreview)
+ return;
+
+ // Our template. Modify to taste
+ gchar const *xformat = R"B(
+<svg width="400" height="600"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g transform="translate(-160,90)" style="opacity:0.10">
+ <path style="fill:white"
+ d="M 397.64309 320.25301 L 280.39197 282.517 L 250.74227 124.83447 L 345.08225
+ 29.146783 L 393.59996 46.667064 L 483.89679 135.61619 L 397.64309 320.25301 z"/>
+ <path d="M 476.95792 339.17168 C 495.78197 342.93607 499.54842 356.11361 495.78197 359.87802
+ C 492.01856 363.6434 482.6065 367.40781 475.07663 361.76014 C 467.54478
+ 356.11361 467.54478 342.93607 476.95792 339.17168 z"
+ id="droplet01" />
+ <path d="M 286.46194 340.42914 C 284.6277 340.91835 269.30405 327.71337 257.16909 333.8338
+ C 245.03722 339.95336 236.89276 353.65666 248.22676 359.27982 C 259.56184 364.90298
+ 267.66433 358.41867 277.60113 351.44119 C 287.53903 344.46477
+ 287.18046 343.1206 286.46194 340.42914 z"
+ id= "droplet02"/>
+ <path d="M 510.35756 306.92856 C 520.59494 304.36879 544.24333 306.92856 540.47688 321.98634
+ C 536.71354 337.04806 504.71297 331.39827 484.00371 323.87156 C 482.12141
+ 308.81083 505.53237 308.13423 510.35756 306.92856 z"
+ id="droplet03"/>
+ <path d="M 359.2403 21.362537 C 347.92693 21.362537 336.6347 25.683095 327.96556 34.35223
+ L 173.87387 188.41466 C 165.37697 196.9114 161.1116 207.95813 160.94269 219.04577
+ L 160.88418 219.04577 C 160.88418 219.08524 160.94076 219.12322 160.94269 219.16279
+ C 160.94033 219.34888 160.88418 219.53256 160.88418 219.71865 L 161.14748 219.71865
+ C 164.0966 230.93917 240.29699 245.24198 248.79866 253.74346 C 261.63771 266.58263
+ 199.5652 276.01151 212.4041 288.85074 C 225.24316 301.68979 289.99433 313.6933 302.8346
+ 326.53254 C 315.67368 339.37161 276.5961 353.04289 289.43532 365.88196 C 302.27439
+ 378.72118 345.40201 362.67257 337.5908 396.16198 C 354.92909 413.50026 391.10302
+ 405.2208 415.32417 387.88252 C 428.16323 375.04345 390.6948 376.17577 403.53397
+ 363.33668 C 416.37304 350.49745 448.78128 350.4282 476.08902 319.71589 C 465.09739
+ 302.62116 429.10801 295.34136 441.94719 282.50217 C 454.78625 269.66311 479.74708
+ 276.18423 533.60644 251.72479 C 559.89837 239.78398 557.72636 230.71459 557.62567
+ 219.71865 C 557.62356 219.48727 557.62567 219.27892 557.62567 219.04577 L 557.56716
+ 219.04577 C 557.3983 207.95812 553.10345 196.9114 544.60673 188.41466 L 390.54428
+ 34.35223 C 381.87515 25.683095 370.55366 21.362537 359.2403 21.362537 z M 357.92378
+ 41.402939 C 362.95327 41.533963 367.01541 45.368018 374.98006 50.530832 L 447.76915
+ 104.50827 C 448.56596 105.02498 449.32484 105.564 450.02187 106.11735 C 450.7189 106.67062
+ 451.3556 107.25745 451.95277 107.84347 C 452.54997 108.42842 453.09281 109.01553 453.59111
+ 109.62808 C 454.08837 110.24052 454.53956 110.86661 454.93688 111.50048 C 455.33532 112.13538
+ 455.69164 112.78029 455.9901 113.43137 C 456.28877 114.08363 456.52291 114.75639 456.7215
+ 115.42078 C 456.92126 116.08419 457.08982 116.73973 457.18961 117.41019 C 457.28949
+ 118.08184 457.33588 118.75535 457.33588 119.42886 L 414.21245 98.598549 L 409.9118
+ 131.16055 L 386.18512 120.04324 L 349.55654 144.50131 L 335.54288 96.1703 L 317.4919
+ 138.4453 L 267.08369 143.47735 L 267.63956 121.03795 C 267.63956 115.64823 296.69685
+ 77.915899 314.39075 68.932902 L 346.77721 45.674327 C 351.55594 42.576634 354.90608
+ 41.324327 357.92378 41.402939 z M 290.92738 261.61333 C 313.87149 267.56365 339.40299
+ 275.37038 359.88393 275.50997 L 360.76161 284.72563 C 343.2235 282.91785 306.11346
+ 274.45012 297.36372 269.98057 L 290.92738 261.61333 z"
+ id="mountainDroplet"/>
+ </g>
+ <text xml:space="preserve" x="200" y="320"
+ style="font-size:32px;font-weight:bold;text-anchor:middle">%s</text>
+</svg>
+)B";
+
+ // Fill in the template
+ gchar *xmlBuffer = g_strdup_printf(xformat, _("No preview"));
+
+ // g_message("%s\n", xmlBuffer);
+
+ // Now show it!
+ setFromMem(xmlBuffer);
+ g_free(xmlBuffer);
+ showingNoPreview = true;
+}
+
+
+/**
+ * Inform the user that the svg file is too large to be displayed.
+ * This does not check for sizes of embedded images (yet)
+ */
+void SVGPreview::showTooLarge(long fileLength)
+{
+ // Our template. Modify to taste
+ gchar const *xformat = R"C(
+<svg width="400" height="600"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g transform="translate(-160,90)" style="opacity:0.10">
+ <path style="fill:white"
+ d="M 397.64309 320.25301 L 280.39197 282.517 L 250.74227 124.83447 L 345.08225
+ 29.146783 L 393.59996 46.667064 L 483.89679 135.61619 L 397.64309 320.25301 z"/>
+ <path d="M 476.95792 339.17168 C 495.78197 342.93607 499.54842 356.11361 495.78197 359.87802
+ C 492.01856 363.6434 482.6065 367.40781 475.07663 361.76014 C 467.54478
+ 356.11361 467.54478 342.93607 476.95792 339.17168 z"
+ id="droplet01" />
+ <path d="M 286.46194 340.42914 C 284.6277 340.91835 269.30405 327.71337 257.16909 333.8338
+ C 245.03722 339.95336 236.89276 353.65666 248.22676 359.27982 C 259.56184 364.90298
+ 267.66433 358.41867 277.60113 351.44119 C 287.53903 344.46477
+ 287.18046 343.1206 286.46194 340.42914 z"
+ id= "droplet02"/>
+ <path d="M 510.35756 306.92856 C 520.59494 304.36879 544.24333 306.92856 540.47688 321.98634
+ C 536.71354 337.04806 504.71297 331.39827 484.00371 323.87156 C 482.12141
+ 308.81083 505.53237 308.13423 510.35756 306.92856 z"
+ id="droplet03"/>
+ <path d="M 359.2403 21.362537 C 347.92693 21.362537 336.6347 25.683095 327.96556 34.35223
+ L 173.87387 188.41466 C 165.37697 196.9114 161.1116 207.95813 160.94269 219.04577
+ L 160.88418 219.04577 C 160.88418 219.08524 160.94076 219.12322 160.94269 219.16279
+ C 160.94033 219.34888 160.88418 219.53256 160.88418 219.71865 L 161.14748 219.71865
+ C 164.0966 230.93917 240.29699 245.24198 248.79866 253.74346 C 261.63771 266.58263
+ 199.5652 276.01151 212.4041 288.85074 C 225.24316 301.68979 289.99433 313.6933 302.8346
+ 326.53254 C 315.67368 339.37161 276.5961 353.04289 289.43532 365.88196 C 302.27439
+ 378.72118 345.40201 362.67257 337.5908 396.16198 C 354.92909 413.50026 391.10302
+ 405.2208 415.32417 387.88252 C 428.16323 375.04345 390.6948 376.17577 403.53397
+ 363.33668 C 416.37304 350.49745 448.78128 350.4282 476.08902 319.71589 C 465.09739
+ 302.62116 429.10801 295.34136 441.94719 282.50217 C 454.78625 269.66311 479.74708
+ 276.18423 533.60644 251.72479 C 559.89837 239.78398 557.72636 230.71459 557.62567
+ 219.71865 C 557.62356 219.48727 557.62567 219.27892 557.62567 219.04577 L 557.56716
+ 219.04577 C 557.3983 207.95812 553.10345 196.9114 544.60673 188.41466 L 390.54428
+ 34.35223 C 381.87515 25.683095 370.55366 21.362537 359.2403 21.362537 z M 357.92378
+ 41.402939 C 362.95327 41.533963 367.01541 45.368018 374.98006 50.530832 L 447.76915
+ 104.50827 C 448.56596 105.02498 449.32484 105.564 450.02187 106.11735 C 450.7189 106.67062
+ 451.3556 107.25745 451.95277 107.84347 C 452.54997 108.42842 453.09281 109.01553 453.59111
+ 109.62808 C 454.08837 110.24052 454.53956 110.86661 454.93688 111.50048 C 455.33532 112.13538
+ 455.69164 112.78029 455.9901 113.43137 C 456.28877 114.08363 456.52291 114.75639 456.7215
+ 115.42078 C 456.92126 116.08419 457.08982 116.73973 457.18961 117.41019 C 457.28949
+ 118.08184 457.33588 118.75535 457.33588 119.42886 L 414.21245 98.598549 L 409.9118
+ 131.16055 L 386.18512 120.04324 L 349.55654 144.50131 L 335.54288 96.1703 L 317.4919
+ 138.4453 L 267.08369 143.47735 L 267.63956 121.03795 C 267.63956 115.64823 296.69685
+ 77.915899 314.39075 68.932902 L 346.77721 45.674327 C 351.55594 42.576634 354.90608
+ 41.324327 357.92378 41.402939 z M 290.92738 261.61333 C 313.87149 267.56365 339.40299
+ 275.37038 359.88393 275.50997 L 360.76161 284.72563 C 343.2235 282.91785 306.11346
+ 274.45012 297.36372 269.98057 L 290.92738 261.61333 z"
+ id="mountainDroplet"/>
+ </g>
+ <text xml:space="preserve" x="200" y="280"
+ style="font-size:20px;font-weight:bold;text-anchor:middle">%.1f MB</text>
+ <text xml:space="preserve" x="200" y="360"
+ style="font-size:20px;font-weight:bold;text-anchor:middle">%s</text>
+</svg>
+)C";
+
+
+ // Fill in the template
+ double floatFileLength = ((double)fileLength) / 1048576.0;
+ // printf("%ld %f\n", fileLength, floatFileLength);
+
+ gchar *xmlBuffer =
+ g_strdup_printf(xformat, floatFileLength, _("Too large for preview"));
+
+ // g_message("%s\n", xmlBuffer);
+
+ // now show it!
+ setFromMem(xmlBuffer);
+ g_free(xmlBuffer);
+}
+
+bool SVGPreview::set(Glib::ustring &fileName, int dialogType)
+{
+
+ if (!Glib::file_test(fileName, Glib::FILE_TEST_EXISTS)) {
+ showNoPreview();
+ return false;
+ }
+
+ if (Glib::file_test(fileName, Glib::FILE_TEST_IS_DIR)) {
+ showNoPreview();
+ return false;
+ }
+
+ if (Glib::file_test(fileName, Glib::FILE_TEST_IS_REGULAR)) {
+ Glib::ustring fileNameUtf8 = Glib::filename_to_utf8(fileName);
+ gchar *fName = const_cast<gchar *>(
+ fileNameUtf8.c_str()); // const-cast probably not necessary? (not necessary on Windows version of stat())
+ GStatBuf info;
+ if (g_stat(fName, &info)) // stat returns 0 upon success
+ {
+ g_warning("SVGPreview::set() : %s : %s", fName, strerror(errno));
+ return false;
+ }
+ if (info.st_size > 0xA00000L) {
+ showingNoPreview = false;
+ showTooLarge(info.st_size);
+ return false;
+ }
+ }
+
+ Glib::ustring svg = ".svg";
+ Glib::ustring svgz = ".svgz";
+
+ if ((dialogType == SVG_TYPES || dialogType == IMPORT_TYPES) &&
+ (hasSuffix(fileName, svg) || hasSuffix(fileName, svgz))) {
+ bool retval = setFileName(fileName);
+ showingNoPreview = false;
+ return retval;
+ } else if (isValidImageFile(fileName)) {
+ showImage(fileName);
+ showingNoPreview = false;
+ return true;
+ } else {
+ showNoPreview();
+ return false;
+ }
+}
+
+
+SVGPreview::SVGPreview()
+ : Gtk::Box(Gtk::ORIENTATION_VERTICAL)
+ , document(nullptr)
+ , viewer(nullptr)
+ , showingNoPreview(false)
+{
+ set_size_request(200, 300);
+}
+
+SVGPreview::~SVGPreview()
+{
+ if (viewer) {
+ viewer->setDocument(nullptr);
+ }
+ delete document;
+}
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
+
diff --git a/src/ui/dialog/svg-preview.h b/src/ui/dialog/svg-preview.h
new file mode 100644
index 0000000..55b9291
--- /dev/null
+++ b/src/ui/dialog/svg-preview.h
@@ -0,0 +1,123 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Implementation of the file dialog interfaces defined in filedialogimpl.h
+ */
+/* Authors:
+ * Bob Jamison
+ * Johan Engelen <johan@shouraizou.nl>
+ * Joel Holdsworth
+ * Bruno Dilly
+ * Other dudes from The Inkscape Organization
+ *
+ * Copyright (C) 2004-2008 Authors
+ * Copyright (C) 2004-2007 The Inkscape Organization
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef __SVG_PREVIEW_H__
+#define __SVG_PREVIEW_H__
+
+//Gtk includes
+#include <gtkmm.h>
+
+//General includes
+#include <unistd.h>
+#include <sys/stat.h>
+#include <cerrno>
+
+#include "filedialog.h"
+
+
+namespace Gtk {
+class Expander;
+}
+
+namespace Inkscape {
+ class URI;
+
+namespace UI {
+
+namespace View {
+ class SVGViewWidget;
+}
+
+namespace Dialog {
+
+/*#########################################################################
+### SVG Preview Widget
+#########################################################################*/
+
+/**
+ * Simple class for displaying an SVG file in the "preview widget."
+ * Currently, this is just a wrapper of the sp_svg_view Gtk widget.
+ * Hopefully we will eventually replace with a pure Gtkmm widget.
+ */
+class SVGPreview : public Gtk::Box
+{
+public:
+
+ SVGPreview();
+
+ ~SVGPreview() override;
+
+ bool setDocument(SPDocument *doc);
+
+ bool setFileName(Glib::ustring &fileName);
+
+ bool setFromMem(char const *xmlBuffer);
+
+ bool set(Glib::ustring &fileName, int dialogType);
+
+ bool setURI(URI &uri);
+
+ /**
+ * Show image embedded in SVG
+ */
+ void showImage(Glib::ustring &fileName);
+
+ /**
+ * Show the "No preview" image
+ */
+ void showNoPreview();
+
+ /**
+ * Show the "Too large" image
+ */
+ void showTooLarge(long fileLength);
+
+private:
+ /**
+ * The svg document we are currently showing
+ */
+ SPDocument *document;
+
+ /**
+ * The sp_svg_view widget
+ */
+ Inkscape::UI::View::SVGViewWidget *viewer;
+
+ /**
+ * are we currently showing the "no preview" image?
+ */
+ bool showingNoPreview;
+
+};
+
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif /*__SVG_PREVIEW_H__*/
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/swatches.cpp b/src/ui/dialog/swatches.cpp
new file mode 100644
index 0000000..65c4f4c
--- /dev/null
+++ b/src/ui/dialog/swatches.cpp
@@ -0,0 +1,1113 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Jon A. Cruz
+ * John Bintz
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2005 Jon A. Cruz
+ * Copyright (C) 2008 John Bintz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "swatches.h"
+
+#include <map>
+#include <algorithm>
+#include <iomanip>
+#include <set>
+
+#include <gtkmm/checkmenuitem.h>
+#include <gtkmm/menu.h>
+#include <gtkmm/menubutton.h>
+#include <gtkmm/radiomenuitem.h>
+#include <gtkmm/radiomenuitem.h>
+#include <gtkmm/separatormenuitem.h>
+
+#include <glibmm/i18n.h>
+#include <glibmm/main.h>
+#include <glibmm/timer.h>
+#include <glibmm/fileutils.h>
+#include <glibmm/miscutils.h>
+
+#include "color-item.h"
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "gradient-chemistry.h"
+#include "inkscape.h"
+#include "message-context.h"
+#include "path-prefix.h"
+
+#include "actions/actions-tools.h" // Invoke gradient tool
+#include "display/cairo-utils.h"
+#include "extension/db.h"
+#include "io/resource.h"
+#include "io/sys.h"
+#include "object/sp-defs.h"
+#include "object/sp-gradient-reference.h"
+#include "ui/dialog/dialog-container.h"
+#include "ui/icon-names.h"
+#include "ui/previewholder.h"
+#include "ui/widget/color-palette.h"
+#include "ui/widget/gradient-vector-selector.h"
+#include "widgets/desktop-widget.h"
+#include "widgets/ege-paint-def.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+
+enum {
+ SWATCHES_SETTINGS_SIZE = 0,
+ SWATCHES_SETTINGS_MODE = 1,
+ SWATCHES_SETTINGS_SHAPE = 2,
+ SWATCHES_SETTINGS_WRAP = 3,
+ SWATCHES_SETTINGS_BORDER = 4,
+ SWATCHES_SETTINGS_PALETTE = 5
+};
+
+#define VBLOCK 16
+#define PREVIEW_PIXBUF_WIDTH 128
+
+std::list<SwatchPage*> userSwatchPages;
+std::list<SwatchPage*> systemSwatchPages;
+static std::map<SPDocument*, SwatchPage*> docPalettes;
+static std::vector<DocTrack*> docTrackings;
+static std::map<SwatchesPanel*, SPDocument*> docPerPanel;
+
+
+class SwatchesPanelHook : public SwatchesPanel
+{
+public:
+ static void convertGradient( GtkMenuItem *menuitem, gpointer userData );
+ static void deleteGradient( GtkMenuItem *menuitem, gpointer userData );
+};
+
+static void handleClick( GtkWidget* /*widget*/, gpointer callback_data ) {
+ ColorItem* item = reinterpret_cast<ColorItem*>(callback_data);
+ if ( item ) {
+ item->buttonClicked(false);
+ }
+}
+
+static void handleSecondaryClick( GtkWidget* /*widget*/, gint /*arg1*/, gpointer callback_data ) {
+ ColorItem* item = reinterpret_cast<ColorItem*>(callback_data);
+ if ( item ) {
+ item->buttonClicked(true);
+ }
+}
+
+static GtkWidget* popupMenu = nullptr;
+static GtkWidget *popupSubHolder = nullptr;
+static GtkWidget *popupSub = nullptr;
+static std::vector<Glib::ustring> popupItems;
+static std::vector<GtkWidget*> popupExtras;
+static ColorItem* bounceTarget = nullptr;
+static SwatchesPanel* bouncePanel = nullptr;
+
+static void redirClick( GtkMenuItem *menuitem, gpointer /*user_data*/ )
+{
+ if ( bounceTarget ) {
+ handleClick( GTK_WIDGET(menuitem), bounceTarget );
+ }
+}
+
+static void redirSecondaryClick( GtkMenuItem *menuitem, gpointer /*user_data*/ )
+{
+ if ( bounceTarget ) {
+ handleSecondaryClick( GTK_WIDGET(menuitem), 0, bounceTarget );
+ }
+}
+
+static void editGradientImpl( SPDesktop* desktop, SPGradient* gr )
+{
+ g_assert(desktop != nullptr);
+ g_assert(desktop->doc() != nullptr);
+
+ if ( gr ) {
+ bool shown = false;
+ {
+ Inkscape::Selection *selection = desktop->getSelection();
+ std::vector<SPItem*> const items(selection->items().begin(), selection->items().end());
+ if (!items.empty()) {
+ SPStyle query( desktop->doc() );
+ int result = objects_query_fillstroke((items), &query, true);
+ if ( (result == QUERY_STYLE_MULTIPLE_SAME) || (result == QUERY_STYLE_SINGLE) ) {
+ // could be pertinent
+ if (query.fill.isPaintserver()) {
+ SPPaintServer* server = query.getFillPaintServer();
+ if ( SP_IS_GRADIENT(server) ) {
+ SPGradient* grad = SP_GRADIENT(server);
+ if ( grad->isSwatch() && grad->getId() == gr->getId()) {
+ desktop->getContainer()->new_dialog("FillStroke");
+ shown = true;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (!shown) { // WHEN DOES THIS HAPPEN?
+ // Invoke the gradient tool
+ set_active_tool(desktop, "Gradient");
+ }
+ }
+}
+
+static void editGradient( GtkMenuItem */*menuitem*/, gpointer /*user_data*/ )
+{
+ if ( bounceTarget ) {
+ SwatchesPanel* swp = bouncePanel;
+ SPDesktop* desktop = swp ? swp->getDesktop() : nullptr;
+ SPDocument *doc = desktop ? desktop->doc() : nullptr;
+ if (doc) {
+ std::string targetName(bounceTarget->def.descr);
+ std::vector<SPObject *> gradients = doc->getResourceList("gradient");
+ for (auto gradient : gradients) {
+ SPGradient* grad = SP_GRADIENT(gradient);
+ if ( targetName == grad->getId() ) {
+ editGradientImpl( desktop, grad );
+ break;
+ }
+ }
+ }
+ }
+}
+
+void SwatchesPanelHook::convertGradient( GtkMenuItem * /*menuitem*/, gpointer userData )
+{
+ if ( bounceTarget ) {
+ SwatchesPanel* swp = bouncePanel;
+ SPDesktop* desktop = swp ? swp->getDesktop() : nullptr;
+ SPDocument *doc = desktop ? desktop->doc() : nullptr;
+ gint index = GPOINTER_TO_INT(userData);
+ if ( doc && (index >= 0) && (static_cast<guint>(index) < popupItems.size()) ) {
+ Glib::ustring targetName = popupItems[index];
+ std::vector<SPObject *> gradients = doc->getResourceList("gradient");
+ for (auto gradient : gradients) {
+ SPGradient* grad = SP_GRADIENT(gradient);
+
+ if ( targetName == grad->getId() ) {
+ grad->setSwatch();
+ DocumentUndo::done(doc, _("Add gradient stop"), INKSCAPE_ICON("color-gradient"));
+ break;
+ }
+ }
+ }
+ }
+}
+
+void SwatchesPanelHook::deleteGradient( GtkMenuItem */*menuitem*/, gpointer /*userData*/ )
+{
+ if ( bounceTarget ) {
+ SwatchesPanel* swp = bouncePanel;
+ SPDesktop* desktop = swp ? swp->getDesktop() : nullptr;
+ sp_gradient_unset_swatch(desktop, bounceTarget->def.descr);
+ }
+}
+
+static SwatchesPanel* findContainingPanel( GtkWidget *widget )
+{
+ SwatchesPanel *swp = nullptr;
+
+ std::map<GtkWidget*, SwatchesPanel*> rawObjects;
+ for (std::map<SwatchesPanel*, SPDocument*>::iterator it = docPerPanel.begin(); it != docPerPanel.end(); ++it) {
+ rawObjects[GTK_WIDGET(it->first->gobj())] = it->first;
+ }
+
+ for (GtkWidget* curr = widget; curr && !swp; curr = gtk_widget_get_parent(curr)) {
+ if (rawObjects.find(curr) != rawObjects.end()) {
+ swp = rawObjects[curr];
+ }
+ }
+
+ return swp;
+}
+
+static void removeit( GtkWidget *widget, gpointer data )
+{
+ gtk_container_remove( GTK_CONTAINER(data), widget );
+}
+
+/* extern'ed from color-item.cpp */
+bool colorItemHandleButtonPress(GdkEventButton* event, UI::Widget::Preview *preview, gpointer user_data)
+{
+ gboolean handled = FALSE;
+
+ if ( event && (event->button == 3) && (event->type == GDK_BUTTON_PRESS) ) {
+ SwatchesPanel* swp = findContainingPanel( GTK_WIDGET(preview->gobj()) );
+
+ if ( !popupMenu ) {
+ popupMenu = gtk_menu_new();
+ GtkWidget* child = nullptr;
+
+ //TRANSLATORS: An item in context menu on a colour in the swatches
+ child = gtk_menu_item_new_with_label(_("Set fill"));
+ g_signal_connect( G_OBJECT(child),
+ "activate",
+ G_CALLBACK(redirClick),
+ user_data);
+ gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child);
+
+ //TRANSLATORS: An item in context menu on a colour in the swatches
+ child = gtk_menu_item_new_with_label(_("Set stroke"));
+
+ g_signal_connect( G_OBJECT(child),
+ "activate",
+ G_CALLBACK(redirSecondaryClick),
+ user_data);
+ gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child);
+
+ child = gtk_separator_menu_item_new();
+ gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child);
+ popupExtras.push_back(child);
+
+ child = gtk_menu_item_new_with_label(_("Delete"));
+ g_signal_connect( G_OBJECT(child),
+ "activate",
+ G_CALLBACK(SwatchesPanelHook::deleteGradient),
+ user_data );
+ gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child);
+ popupExtras.push_back(child);
+ gtk_widget_set_sensitive( child, FALSE );
+
+ child = gtk_menu_item_new_with_label(_("Edit..."));
+ g_signal_connect( G_OBJECT(child),
+ "activate",
+ G_CALLBACK(editGradient),
+ user_data );
+ gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child);
+ popupExtras.push_back(child);
+
+ child = gtk_separator_menu_item_new();
+ gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child);
+ popupExtras.push_back(child);
+
+ child = gtk_menu_item_new_with_label(_("Convert"));
+ gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child);
+ //popupExtras.push_back(child);
+ //gtk_widget_set_sensitive( child, FALSE );
+ {
+ popupSubHolder = child;
+ popupSub = gtk_menu_new();
+ gtk_menu_item_set_submenu( GTK_MENU_ITEM(child), popupSub );
+ }
+
+ gtk_widget_show_all(popupMenu);
+ }
+
+ if ( user_data ) {
+ ColorItem* item = reinterpret_cast<ColorItem*>(user_data);
+ bool show = swp && (swp->getSelectedIndex() == 0);
+ for (auto & popupExtra : popupExtras) {
+ gtk_widget_set_sensitive(popupExtra, show);
+ }
+
+ bounceTarget = item;
+ bouncePanel = swp;
+ popupItems.clear();
+ if ( popupMenu ) {
+ gtk_container_foreach(GTK_CONTAINER(popupSub), removeit, popupSub);
+ bool processed = false;
+ auto *wdgt = preview->get_ancestor(SPDesktopWidget::get_type());
+ if ( wdgt ) {
+ SPDesktopWidget *dtw = SP_DESKTOP_WIDGET(wdgt);
+ if ( dtw && dtw->desktop ) {
+ // Pick up all gradients with vectors
+ std::vector<SPObject *> gradients = (dtw->desktop->doc())->getResourceList("gradient");
+ gint index = 0;
+ for (auto gradient : gradients) {
+ SPGradient* grad = SP_GRADIENT(gradient);
+ if ( grad->hasStops() && !grad->isSwatch() ) {
+ //gl = g_slist_prepend(gl, curr->data);
+ processed = true;
+ GtkWidget *child = gtk_menu_item_new_with_label(grad->getId());
+ gtk_menu_shell_append(GTK_MENU_SHELL(popupSub), child);
+
+ popupItems.emplace_back(grad->getId());
+ g_signal_connect( G_OBJECT(child),
+ "activate",
+ G_CALLBACK(SwatchesPanelHook::convertGradient),
+ GINT_TO_POINTER(index) );
+ index++;
+ }
+ }
+
+ gtk_widget_show_all(popupSub);
+ }
+ }
+ gtk_widget_set_sensitive( popupSubHolder, processed );
+ gtk_menu_popup_at_pointer(GTK_MENU(popupMenu), reinterpret_cast<GdkEvent *>(event));
+ handled = TRUE;
+ }
+ }
+ }
+
+ return handled;
+}
+
+
+static char* trim( char* str ) {
+ char* ret = str;
+ while ( *str && (*str == ' ' || *str == '\t') ) {
+ str++;
+ }
+ ret = str;
+ while ( *str ) {
+ str++;
+ }
+ str--;
+ while ( str >= ret && (( *str == ' ' || *str == '\t' ) || *str == '\r' || *str == '\n') ) {
+ *str-- = 0;
+ }
+ return ret;
+}
+
+static void skipWhitespace( char*& str ) {
+ while ( *str == ' ' || *str == '\t' ) {
+ str++;
+ }
+}
+
+static bool parseNum( char*& str, int& val ) {
+ val = 0;
+ while ( '0' <= *str && *str <= '9' ) {
+ val = val * 10 + (*str - '0');
+ str++;
+ }
+ bool retval = !(*str == 0 || *str == ' ' || *str == '\t' || *str == '\r' || *str == '\n');
+ return retval;
+}
+
+
+static
+void _loadPaletteFile(Glib::ustring path, gboolean user/*=FALSE*/)
+{
+ auto filename = Glib::path_get_basename(path.raw());
+ char block[1024];
+ FILE *f = Inkscape::IO::fopen_utf8name(path.c_str(), "r");
+ if ( f ) {
+ char* result = fgets( block, sizeof(block), f );
+ if ( result ) {
+ if ( strncmp( "GIMP Palette", block, 12 ) == 0 ) {
+ bool inHeader = true;
+ bool hasErr = false;
+
+ SwatchPage *onceMore = new SwatchPage();
+ onceMore->_name = filename.c_str();
+
+ do {
+ result = fgets( block, sizeof(block), f );
+ block[sizeof(block) - 1] = 0;
+ if ( result ) {
+ if ( block[0] == '#' ) {
+ // ignore comment
+ } else {
+ char *ptr = block;
+ // very simple check for header versus entry
+ while ( *ptr == ' ' || *ptr == '\t' ) {
+ ptr++;
+ }
+ if ( (*ptr == 0) || (*ptr == '\r') || (*ptr == '\n') ) {
+ // blank line. skip it.
+ } else if ( '0' <= *ptr && *ptr <= '9' ) {
+ // should be an entry link
+ inHeader = false;
+ ptr = block;
+ Glib::ustring name("");
+ skipWhitespace(ptr);
+ if ( *ptr ) {
+ int r = 0;
+ int g = 0;
+ int b = 0;
+ hasErr = parseNum(ptr, r);
+ if ( !hasErr ) {
+ skipWhitespace(ptr);
+ hasErr = parseNum(ptr, g);
+ }
+ if ( !hasErr ) {
+ skipWhitespace(ptr);
+ hasErr = parseNum(ptr, b);
+ }
+ if ( !hasErr && *ptr ) {
+ char* n = trim(ptr);
+ if (n != nullptr && *n) {
+ name = g_dpgettext2(nullptr, "Palette", n);
+ }
+ if (name == "") {
+ name = Glib::ustring::compose("#%1%2%3",
+ Glib::ustring::format(std::hex, std::setw(2), std::setfill(L'0'), r),
+ Glib::ustring::format(std::hex, std::setw(2), std::setfill(L'0'), g),
+ Glib::ustring::format(std::hex, std::setw(2), std::setfill(L'0'), b)
+ ).uppercase();
+ }
+ }
+ if ( !hasErr ) {
+ // Add the entry now
+ Glib::ustring nameStr(name);
+ ColorItem* item = new ColorItem( r, g, b, nameStr );
+ onceMore->_colors.push_back(item);
+ }
+ } else {
+ hasErr = true;
+ }
+ } else {
+ if ( !inHeader ) {
+ // Hmmm... probably bad. Not quite the format we want?
+ hasErr = true;
+ } else {
+ char* sep = strchr(result, ':');
+ if ( sep ) {
+ *sep = 0;
+ char* val = trim(sep + 1);
+ char* name = trim(result);
+ if ( *name ) {
+ if ( strcmp( "Name", name ) == 0 )
+ {
+ onceMore->_name = val;
+ }
+ else if ( strcmp( "Columns", name ) == 0 )
+ {
+ gchar* endPtr = nullptr;
+ guint64 numVal = g_ascii_strtoull( val, &endPtr, 10 );
+ if ( (numVal == G_MAXUINT64) && (ERANGE == errno) ) {
+ // overflow
+ } else if ( (numVal == 0) && (endPtr == val) ) {
+ // failed conversion
+ } else {
+ onceMore->_prefWidth = numVal;
+ }
+ }
+ } else {
+ // error
+ hasErr = true;
+ }
+ } else {
+ // error
+ hasErr = true;
+ }
+ }
+ }
+ }
+ }
+ } while ( result && !hasErr );
+ if ( !hasErr ) {
+ if (user)
+ userSwatchPages.push_back(onceMore);
+ else
+ systemSwatchPages.push_back(onceMore);
+ } else {
+ delete onceMore;
+ }
+ }
+ }
+
+ fclose(f);
+ }
+}
+
+static bool
+compare_swatch_names(SwatchPage const *a, SwatchPage const *b) {
+
+ return g_utf8_collate(a->_name.c_str(), b->_name.c_str()) < 0;
+}
+
+static void load_palettes()
+{
+ static bool init_done = false;
+
+ if (init_done) {
+ return;
+ }
+ init_done = true;
+
+ for (auto &filename: Inkscape::IO::Resource::get_filenames(Inkscape::IO::Resource::PALETTES, {".gpl"})) {
+ bool userPalette = Inkscape::IO::file_is_writable(filename.c_str());
+ _loadPaletteFile(filename, userPalette);
+ }
+
+ // Sort the list of swatches by name, grouped by user/system
+ userSwatchPages.sort(compare_swatch_names);
+ systemSwatchPages.sort(compare_swatch_names);
+}
+
+SwatchesPanel& SwatchesPanel::getInstance()
+{
+ return *new SwatchesPanel();
+}
+
+
+class DocTrack
+{
+public:
+ DocTrack(SPDocument *doc, sigc::connection &gradientRsrcChanged, sigc::connection &defsChanged, sigc::connection &defsModified) :
+ doc(doc->doRef()),
+ updatePending(false),
+ lastGradientUpdate(0.0),
+ gradientRsrcChanged(gradientRsrcChanged),
+ defsChanged(defsChanged),
+ defsModified(defsModified)
+ {
+ if ( !timer ) {
+ timer = new Glib::Timer();
+ refreshTimer = Glib::signal_timeout().connect( sigc::ptr_fun(handleTimerCB), 33 );
+ }
+ timerRefCount++;
+ }
+
+ ~DocTrack()
+ {
+ timerRefCount--;
+ if ( timerRefCount <= 0 ) {
+ refreshTimer.disconnect();
+ timerRefCount = 0;
+ if ( timer ) {
+ timer->stop();
+ delete timer;
+ timer = nullptr;
+ }
+ }
+ if (doc) {
+ gradientRsrcChanged.disconnect();
+ defsChanged.disconnect();
+ defsModified.disconnect();
+ }
+ }
+
+ static bool handleTimerCB();
+
+ /**
+ * Checks if update should be queued or executed immediately.
+ *
+ * @return true if the update was queued and should not be immediately executed.
+ */
+ static bool queueUpdateIfNeeded(SPDocument *doc);
+
+ static Glib::Timer *timer;
+ static int timerRefCount;
+ static sigc::connection refreshTimer;
+
+ std::unique_ptr<SPDocument> doc;
+ bool updatePending;
+ double lastGradientUpdate;
+ sigc::connection gradientRsrcChanged;
+ sigc::connection defsChanged;
+ sigc::connection defsModified;
+
+private:
+ DocTrack(DocTrack const &) = delete; // no copy
+ DocTrack &operator=(DocTrack const &) = delete; // no assign
+};
+
+Glib::Timer *DocTrack::timer = nullptr;
+int DocTrack::timerRefCount = 0;
+sigc::connection DocTrack::refreshTimer;
+
+static const double DOC_UPDATE_THREASHOLD = 0.090;
+
+bool DocTrack::handleTimerCB()
+{
+ double now = timer->elapsed();
+
+ std::vector<DocTrack *> needCallback;
+ for (auto track : docTrackings) {
+ if ( track->updatePending && ( (now - track->lastGradientUpdate) >= DOC_UPDATE_THREASHOLD) ) {
+ needCallback.push_back(track);
+ }
+ }
+
+ for (auto track : needCallback) {
+ if ( std::find(docTrackings.begin(), docTrackings.end(), track) != docTrackings.end() ) { // Just in case one gets deleted while we are looping
+ // Note: calling handleDefsModified will call queueUpdateIfNeeded and thus update the time and flag.
+ SwatchesPanel::handleDefsModified(track->doc.get());
+ }
+ }
+
+ return true;
+}
+
+bool DocTrack::queueUpdateIfNeeded( SPDocument *doc )
+{
+ bool deferProcessing = false;
+ for (auto track : docTrackings) {
+ if ( track->doc.get() == doc ) {
+ double now = timer->elapsed();
+ double elapsed = now - track->lastGradientUpdate;
+
+ if ( elapsed < DOC_UPDATE_THREASHOLD ) {
+ deferProcessing = true;
+ track->updatePending = true;
+ } else {
+ track->lastGradientUpdate = now;
+ track->updatePending = false;
+ }
+
+ break;
+ }
+ }
+ return deferProcessing;
+}
+
+void SwatchesPanel::_trackDocument( SwatchesPanel *panel, SPDocument *document )
+{
+ SPDocument *oldDoc = nullptr;
+ if (docPerPanel.find(panel) != docPerPanel.end()) {
+ oldDoc = docPerPanel[panel];
+ if (!oldDoc) {
+ docPerPanel.erase(panel); // Should not be needed, but clean up just in case.
+ }
+ }
+ if (oldDoc != document) {
+ if (oldDoc) {
+ docPerPanel[panel] = nullptr;
+ bool found = false;
+ for (std::map<SwatchesPanel*, SPDocument*>::iterator it = docPerPanel.begin(); (it != docPerPanel.end()) && !found; ++it) {
+ found = (it->second == document);
+ }
+ if (!found) {
+ for (std::vector<DocTrack*>::iterator it = docTrackings.begin(); it != docTrackings.end(); ++it){
+ if ((*it)->doc.get() == oldDoc) {
+ delete *it;
+ docTrackings.erase(it);
+ break;
+ }
+ }
+ }
+ }
+
+ if (document) {
+ bool found = false;
+ for (std::map<SwatchesPanel*, SPDocument*>::iterator it = docPerPanel.begin(); (it != docPerPanel.end()) && !found; ++it) {
+ found = (it->second == document);
+ }
+ docPerPanel[panel] = document;
+ if (!found) {
+ sigc::connection conn1 = document->connectResourcesChanged( "gradient", sigc::bind(sigc::ptr_fun(&SwatchesPanel::handleGradientsChange), document) );
+ sigc::connection conn2 = document->getDefs()->connectRelease( sigc::hide(sigc::bind(sigc::ptr_fun(&SwatchesPanel::handleDefsModified), document)) );
+ sigc::connection conn3 = document->getDefs()->connectModified( sigc::hide(sigc::hide(sigc::bind(sigc::ptr_fun(&SwatchesPanel::handleDefsModified), document))) );
+
+ DocTrack *dt = new DocTrack(document, conn1, conn2, conn3);
+ docTrackings.push_back(dt);
+
+ if (docPalettes.find(document) == docPalettes.end()) {
+ SwatchPage *docPalette = new SwatchPage();
+ docPalette->_name = "Auto";
+ docPalettes[document] = docPalette;
+ }
+ }
+ // Always update the palettes if there's a document.
+ panel->updatePalettes();
+ }
+ }
+}
+
+
+/**
+ * Constructor
+ */
+SwatchesPanel::SwatchesPanel(gchar const *prefsPath)
+ : DialogBase(prefsPath, "Swatches")
+ , _menu(nullptr)
+ , _holder(nullptr)
+ , _clear(nullptr)
+ , _remove(nullptr)
+ , _currentIndex(0)
+{
+ _palette = Gtk::manage(new Inkscape::UI::Widget::ColorPalette());
+ pack_start(*_palette);
+
+ if (_prefs_path == "/dialogs/swatches") {
+ _palette->set_compact(false);
+ } else {
+ _palette->set_compact(true);
+ }
+
+ load_palettes();
+
+ _clear = new ColorItem( ege::PaintDef::CLEAR );
+ _remove = new ColorItem( ege::PaintDef::NONE );
+
+ if (docPalettes.empty()) {
+ SwatchPage *docPalette = new SwatchPage();
+
+ docPalette->_name = "Empty";
+ docPalettes[nullptr] = docPalette;
+ }
+
+ if ( !systemSwatchPages.empty() || !userSwatchPages.empty()) {
+ SwatchPage* first = nullptr;
+ int index = 0;
+ Glib::ustring targetName;
+ if ( !_prefs_path.empty() ) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ targetName = prefs->getString(_prefs_path + "/palette");
+ if (!targetName.empty()) {
+ if (targetName == "Empty") {
+ first = docPalettes[nullptr];
+ } else {
+ std::vector<SwatchPage*> pages = _getSwatchSets();
+ for (auto & page : pages) {
+ index++;
+ if ( page->_name == targetName ) {
+ first = page;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if ( !first ) {
+ first = docPalettes[nullptr];
+ _currentIndex = 0;
+ } else {
+ _currentIndex = index;
+ }
+
+ // restore palette settings
+ Inkscape::Preferences* prefs = Inkscape::Preferences::get();
+ _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));
+ // 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());
+ });
+
+ // switch swatch palettes
+ _palette->get_palette_selected_signal().connect([=](Glib::ustring name) {
+ std::vector<SwatchPage*> pages = _getSwatchSets();
+ auto it = std::find_if(pages.begin(), pages.end(), [&](auto el){ return el->_name == name; });
+ if (it != pages.end()) {
+ auto index = static_cast<int>(it - pages.begin());
+ if (_currentIndex != index) {
+ _currentIndex = index;
+ Inkscape::Preferences* prefs = Inkscape::Preferences::get();
+ prefs->setString(_prefs_path + "/palette", pages[_currentIndex]->_name);
+ _rebuild();
+ }
+ }
+ });
+ }
+}
+
+SwatchesPanel::~SwatchesPanel()
+{
+ _trackDocument( this, nullptr );
+ for (auto & docTracking : docTrackings){
+ delete docTracking;
+ }
+ docTrackings.clear();
+
+ docPerPanel.erase(this);
+
+ delete _clear;
+ delete _remove;
+}
+
+/**
+ * Process the list of available palettes and update the list
+ * in the _palette widget. The widget will take care of cleaning.
+ */
+void SwatchesPanel::updatePalettes()
+{
+ std::vector<SwatchPage*> swatchSets = _getSwatchSets();
+
+ std::vector<Inkscape::UI::Widget::ColorPalette::palette_t> palettes;
+ palettes.reserve(swatchSets.size());
+ for (auto curr : swatchSets) {
+ Inkscape::UI::Widget::ColorPalette::palette_t palette;
+ palette.name = curr->_name;
+ for (const auto& color : curr->_colors) {
+ if (color.def.getType() == ege::PaintDef::RGB) {
+ auto& c = color.def;
+ palette.colors.push_back(
+ Inkscape::UI::Widget::ColorPalette::rgb_t { c.getR() / 255.0, c.getG() / 255.0, c.getB() / 255.0 });
+ }
+ }
+ palettes.push_back(palette);
+ }
+
+ // pass list of available palettes
+ _palette->set_palettes(palettes);
+ _rebuild();
+}
+
+void SwatchesPanel::_updateSettings(int settings, int value)
+{
+}
+
+void SwatchesPanel::documentReplaced()
+{
+ _trackDocument(this, getDocument());
+ if (auto document = getDocument()) {
+ handleGradientsChange(document);
+ }
+}
+
+static void recalcSwatchContents(SPDocument* doc,
+ boost::ptr_vector<ColorItem> &tmpColors,
+ std::map<ColorItem*, cairo_pattern_t*> &previewMappings,
+ std::map<ColorItem*, SPGradient*> &gradMappings)
+{
+ std::vector<SPGradient*> newList;
+ std::vector<SPObject *> gradients = doc->getResourceList("gradient");
+ for (auto gradient : gradients) {
+ SPGradient* grad = SP_GRADIENT(gradient);
+ if ( grad->isSwatch() ) {
+ newList.push_back(SP_GRADIENT(gradient));
+ }
+ }
+
+ if ( !newList.empty() ) {
+ std::reverse(newList.begin(), newList.end());
+ for (auto grad : newList)
+ {
+ cairo_surface_t *preview = cairo_image_surface_create(CAIRO_FORMAT_ARGB32,
+ PREVIEW_PIXBUF_WIDTH, VBLOCK);
+ cairo_t *ct = cairo_create(preview);
+
+ Glib::ustring name( grad->getId() );
+ ColorItem* item = new ColorItem( 0, 0, 0, name );
+
+ cairo_pattern_t *check = ink_cairo_pattern_create_checkerboard();
+ cairo_pattern_t *gradient = grad->create_preview_pattern(PREVIEW_PIXBUF_WIDTH);
+ cairo_set_source(ct, check);
+ cairo_paint(ct);
+ cairo_set_source(ct, gradient);
+ cairo_paint(ct);
+
+ cairo_destroy(ct);
+ cairo_pattern_destroy(gradient);
+ cairo_pattern_destroy(check);
+
+ cairo_pattern_t *prevpat = cairo_pattern_create_for_surface(preview);
+ cairo_surface_destroy(preview);
+
+ previewMappings[item] = prevpat;
+
+ tmpColors.push_back(item);
+ gradMappings[item] = grad;
+ }
+ }
+}
+
+void SwatchesPanel::handleGradientsChange(SPDocument *document)
+{
+ SwatchPage *docPalette = (docPalettes.find(document) != docPalettes.end()) ? docPalettes[document] : nullptr;
+ if (docPalette) {
+ boost::ptr_vector<ColorItem> tmpColors;
+ std::map<ColorItem*, cairo_pattern_t*> tmpPrevs;
+ std::map<ColorItem*, SPGradient*> tmpGrads;
+ recalcSwatchContents(document, tmpColors, tmpPrevs, tmpGrads);
+
+ for (auto & tmpPrev : tmpPrevs) {
+ tmpPrev.first->setPattern(tmpPrev.second);
+ cairo_pattern_destroy(tmpPrev.second);
+ }
+
+ for (auto & tmpGrad : tmpGrads) {
+ tmpGrad.first->setGradient(tmpGrad.second);
+ }
+
+ docPalette->_colors.swap(tmpColors);
+
+ _rebuildDocumentSwatch(docPalette, document);
+ }
+}
+
+/**
+ * Figure out which SwatchesPanel instances are affected and update them.
+ */
+void SwatchesPanel::_rebuildDocumentSwatch(SwatchPage *docPalette, SPDocument *document)
+{
+ for (auto & it : docPerPanel) {
+ if (it.second == document) {
+ SwatchesPanel* swp = it.first;
+ std::vector<SwatchPage*> pages = swp->_getSwatchSets();
+ SwatchPage* curr = pages[swp->_currentIndex];
+ if (curr == docPalette) {
+ swp->_rebuild();
+ }
+ }
+ }
+}
+
+void SwatchesPanel::handleDefsModified(SPDocument *document)
+{
+ SwatchPage *docPalette = (docPalettes.find(document) != docPalettes.end()) ? docPalettes[document] : nullptr;
+ if (docPalette && !DocTrack::queueUpdateIfNeeded(document) ) {
+ boost::ptr_vector<ColorItem> tmpColors;
+ std::map<ColorItem*, cairo_pattern_t*> tmpPrevs;
+ std::map<ColorItem*, SPGradient*> tmpGrads;
+ recalcSwatchContents(document, tmpColors, tmpPrevs, tmpGrads);
+
+ if ( tmpColors.size() != docPalette->_colors.size() ) {
+ handleGradientsChange(document);
+ } else {
+ int cap = std::min(docPalette->_colors.size(), tmpColors.size());
+ for (int i = 0; i < cap; i++) {
+ ColorItem *newColor = &tmpColors[i];
+ ColorItem *oldColor = &docPalette->_colors[i];
+ if ( (newColor->def.getType() != oldColor->def.getType()) ||
+ (newColor->def.getR() != oldColor->def.getR()) ||
+ (newColor->def.getG() != oldColor->def.getG()) ||
+ (newColor->def.getB() != oldColor->def.getB()) ) {
+ oldColor->def.setRGB(newColor->def.getR(), newColor->def.getG(), newColor->def.getB());
+ }
+ if (tmpGrads.find(newColor) != tmpGrads.end()) {
+ oldColor->setGradient(tmpGrads[newColor]);
+ }
+ if ( tmpPrevs.find(newColor) != tmpPrevs.end() ) {
+ oldColor->setPattern(tmpPrevs[newColor]);
+ }
+ }
+ }
+
+ for (auto & tmpPrev : tmpPrevs) {
+ cairo_pattern_destroy(tmpPrev.second);
+ }
+ _rebuildDocumentSwatch(docPalette, document);
+ }
+}
+
+
+std::vector<SwatchPage*> SwatchesPanel::_getSwatchSets() const
+{
+ std::vector<SwatchPage*> tmp;
+ if (auto document = getDocument()) {
+ if (docPalettes.find(document) != docPalettes.end()) {
+ tmp.push_back(docPalettes[document]);
+ }
+ }
+ tmp.insert(tmp.end(), userSwatchPages.begin(), userSwatchPages.end());
+ tmp.insert(tmp.end(), systemSwatchPages.begin(), systemSwatchPages.end());
+ return tmp;
+}
+
+void SwatchesPanel::selectionChanged(Selection *selection)
+{
+ auto document = getDocument();
+ if (!document)
+ return;
+
+ SwatchPage *docPalette = (docPalettes.find(document) != docPalettes.end()) ? docPalettes[document] : nullptr;
+ if ( docPalette ) {
+ std::string fillId;
+ std::string strokeId;
+
+ SPStyle tmpStyle(document);
+ int result = sp_desktop_query_style(getDesktop(), &tmpStyle, QUERY_STYLE_PROPERTY_FILL );
+ switch (result) {
+ case QUERY_STYLE_SINGLE:
+ case QUERY_STYLE_MULTIPLE_AVERAGED:
+ case QUERY_STYLE_MULTIPLE_SAME:
+ {
+ if (tmpStyle.fill.set && tmpStyle.fill.isPaintserver()) {
+ SPPaintServer* server = tmpStyle.getFillPaintServer();
+ if ( SP_IS_GRADIENT(server) ) {
+ SPGradient* target = nullptr;
+ SPGradient* grad = SP_GRADIENT(server);
+
+ if ( grad->isSwatch() ) {
+ target = grad;
+ } else if ( grad->ref ) {
+ SPGradient *tmp = grad->ref->getObject();
+ if ( tmp && tmp->isSwatch() ) {
+ target = tmp;
+ }
+ }
+ if ( target ) {
+ //XML Tree being used directly here while it shouldn't be
+ gchar const* id = target->getRepr()->attribute("id");
+ if ( id ) {
+ fillId = id;
+ }
+ }
+ }
+ }
+ break;
+ }
+ }
+
+ result = sp_desktop_query_style(getDesktop(), &tmpStyle, QUERY_STYLE_PROPERTY_STROKE);
+ switch (result) {
+ case QUERY_STYLE_SINGLE:
+ case QUERY_STYLE_MULTIPLE_AVERAGED:
+ case QUERY_STYLE_MULTIPLE_SAME:
+ {
+ if (tmpStyle.stroke.set && tmpStyle.stroke.isPaintserver()) {
+ SPPaintServer* server = tmpStyle.getStrokePaintServer();
+ if ( SP_IS_GRADIENT(server) ) {
+ SPGradient* target = nullptr;
+ SPGradient* grad = SP_GRADIENT(server);
+ if ( grad->isSwatch() ) {
+ target = grad;
+ } else if ( grad->ref ) {
+ SPGradient *tmp = grad->ref->getObject();
+ if ( tmp && tmp->isSwatch() ) {
+ target = tmp;
+ }
+ }
+ if ( target ) {
+ //XML Tree being used directly here while it shouldn't be
+ gchar const* id = target->getRepr()->attribute("id");
+ if ( id ) {
+ strokeId = id;
+ }
+ }
+ }
+ }
+ break;
+ }
+ }
+
+ for (auto & _color : docPalette->_colors) {
+ ColorItem* item = &_color;
+ bool isFill = (fillId == item->def.descr);
+ bool isStroke = (strokeId == item->def.descr);
+ item->setState( isFill, isStroke );
+ }
+ }
+}
+
+void SwatchesPanel::_rebuild()
+{
+ std::vector<SwatchPage*> pages = _getSwatchSets();
+ SwatchPage* curr = pages[_currentIndex];
+
+ std::vector<Widget*> palette;
+ palette.reserve(curr->_colors.size() + 1);
+ palette.push_back(_remove->createWidget());
+ for (auto & _color : curr->_colors) {
+ palette.push_back(_color.createWidget());
+ }
+ _palette->set_colors(palette);
+ _palette->set_selected(curr->_name);
+}
+
+} //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..477a7a9
--- /dev/null
+++ b/src/ui/dialog/swatches.h
@@ -0,0 +1,108 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Color swatches dialog
+ */
+/* Authors:
+ * Jon A. Cruz
+ *
+ * Copyright (C) 2005 Jon A. Cruz
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef SEEN_DIALOGS_SWATCHES_H
+#define SEEN_DIALOGS_SWATCHES_H
+
+#include "ui/dialog/dialog-base.h"
+
+namespace Gtk {
+ class Menu;
+ class MenuItem;
+ class CheckMenuItem;
+}
+
+namespace Inkscape {
+namespace UI {
+
+class PreviewHolder;
+
+namespace Widget {
+ class ColorPalette;
+}
+
+namespace Dialog {
+
+class ColorItem;
+class SwatchPage;
+class DocTrack;
+
+/**
+ * A panel that displays paint swatches.
+ *
+ * It comes in two flavors, depending on the prefsPath argument passed to
+ * the constructor: the default "/dialog/swatches" is just a regular panel;
+ * the "/embedded/swatches/" is the horizontal color swatches at the bottom
+ * of window.
+ */
+class SwatchesPanel : public DialogBase
+{
+public:
+ SwatchesPanel(gchar const* prefsPath = "/dialogs/swatches");
+ ~SwatchesPanel() override;
+
+ void updatePalettes();
+ void documentReplaced() override;
+ static SwatchesPanel& getInstance();
+ virtual int getSelectedIndex() {return _currentIndex;} // temporary
+
+protected:
+ static void handleGradientsChange(SPDocument *document);
+
+ virtual void _rebuild();
+
+ virtual std::vector<SwatchPage*> _getSwatchSets() const;
+
+private:
+ SwatchesPanel(SwatchesPanel const &) = delete; // no copy
+ SwatchesPanel &operator=(SwatchesPanel const &) = delete; // no assign
+
+ void _build_menu();
+
+ void selectionChanged(Selection *selection) override;
+ static void _rebuildDocumentSwatch(SwatchPage *docPalette, SPDocument *document);
+ static void _trackDocument( SwatchesPanel *panel, SPDocument *document );
+ static void handleDefsModified(SPDocument *document);
+
+ PreviewHolder* _holder;
+ ColorItem* _clear;
+ ColorItem* _remove;
+ int _currentIndex;
+ Inkscape::UI::Widget::ColorPalette* _palette;
+
+ void _regItem(Gtk::MenuItem* item, int id);
+
+ void _updateSettings(int settings, int value);
+
+ void _wrapToggled(Gtk::CheckMenuItem *toggler);
+
+ Gtk::Menu *_menu;
+
+ friend class DocTrack;
+};
+
+} //namespace Dialog
+} //namespace UI
+} //namespace Inkscape
+
+
+
+#endif // SEEN_SWATCHES_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/symbols.cpp b/src/ui/dialog/symbols.cpp
new file mode 100644
index 0000000..147958b
--- /dev/null
+++ b/src/ui/dialog/symbols.cpp
@@ -0,0 +1,1354 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Symbols dialog.
+ */
+/* Authors:
+ * Copyright (C) 2012 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifdef HAVE_CONFIG_H
+# include "config.h" // only include where actually required!
+#endif
+
+#include "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"
+
+#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 {
+
+// See: http://developer.gnome.org/gtkmm/stable/classGtk_1_1TreeModelColumnRecord.html
+class SymbolColumns : public Gtk::TreeModel::ColumnRecord
+{
+public:
+
+ Gtk::TreeModelColumn<Glib::ustring> symbol_id;
+ Gtk::TreeModelColumn<Glib::ustring> symbol_title;
+ Gtk::TreeModelColumn<Glib::ustring> symbol_doc_title;
+ Gtk::TreeModelColumn< Glib::RefPtr<Gdk::Pixbuf> > symbol_image;
+
+
+ SymbolColumns() {
+ add(symbol_id);
+ add(symbol_title);
+ add(symbol_doc_title);
+ add(symbol_image);
+ }
+};
+
+SymbolColumns* SymbolsDialog::getColumns()
+{
+ SymbolColumns* columns = new SymbolColumns();
+ return columns;
+}
+
+/**
+ * Constructor
+ */
+SymbolsDialog::SymbolsDialog(gchar const *prefsPath)
+ : DialogBase(prefsPath, "Symbols")
+ , store(Gtk::ListStore::create(*getColumns()))
+ , all_docs_processed(false)
+ , icon_view(nullptr)
+ , preview_document(nullptr)
+ , gtk_connections()
+ , CURRENTDOC(_("Current document"))
+ , ALLDOCS(_("All symbol sets"))
+{
+ /******************** Table *************************/
+ auto table = new Gtk::Grid();
+
+ table->set_margin_start(3);
+ table->set_margin_end(3);
+ table->set_margin_top(4);
+ // panel is a locked Gtk::VBox
+ pack_start(*Gtk::manage(table), Gtk::PACK_EXPAND_WIDGET);
+ guint row = 0;
+
+ /******************** Symbol Sets *************************/
+ Gtk::Label* label_set = new Gtk::Label(Glib::ustring(_("Symbol set")) + ": ");
+ table->attach(*Gtk::manage(label_set),0,row,1,1);
+ symbol_set = new Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBoxText>(); // Fill in later
+ symbol_set->append(CURRENTDOC);
+ symbol_set->append(ALLDOCS);
+ symbol_set->set_active_text(CURRENTDOC);
+ symbol_set->set_hexpand();
+ gtk_connections.emplace_back(
+ symbol_set->signal_changed().connect(sigc::mem_fun(*this, &SymbolsDialog::rebuild)));
+
+ table->attach(*Gtk::manage(symbol_set),1,row,1,1);
+ ++row;
+
+ /******************** Separator *************************/
+
+
+ Gtk::Separator* separator = Gtk::manage(new Gtk::Separator()); // Search
+ separator->set_margin_top(10);
+ separator->set_margin_bottom(10);
+ table->attach(*Gtk::manage(separator),0,row,2,1);
+
+ ++row;
+
+ /******************** Search *************************/
+
+ search = Gtk::manage(new Gtk::SearchEntry()); // Search
+ search->set_tooltip_text(_("Press 'Return' to start search."));
+ search->signal_key_press_event().connect_notify( sigc::mem_fun(*this, &SymbolsDialog::beforeSearch));
+ search->signal_key_release_event().connect_notify(sigc::mem_fun(*this, &SymbolsDialog::unsensitive));
+
+ search->set_margin_bottom(6);
+ search->signal_search_changed().connect(sigc::mem_fun(*this, &SymbolsDialog::clearSearch));
+ table->attach(*Gtk::manage(search),0,row,2,1);
+ search_str = "";
+
+ ++row;
+
+
+ /********************* Icon View **************************/
+ SymbolColumns* columns = getColumns();
+
+ icon_view = new Gtk::IconView(static_cast<Glib::RefPtr<Gtk::TreeModel> >(store));
+ //icon_view->set_text_column( columns->symbol_id );
+ icon_view->set_tooltip_column( 1 );
+ icon_view->set_pixbuf_column( columns->symbol_image );
+ // Giving the iconview a small minimum size will help users understand
+ // What the dialog does.
+ icon_view->set_size_request( 100, 250 );
+ icon_view->set_vexpand(true);
+ 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)));
+
+ scroller = new Gtk::ScrolledWindow();
+ scroller->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ scroller->add(*Gtk::manage(icon_view));
+ scroller->set_hexpand();
+ scroller->set_vexpand();
+ scroller->set_overlay_scrolling(false);
+ // 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);
+ overlay = new Gtk::Overlay();
+ overlay->set_hexpand();
+ overlay->set_vexpand();
+ overlay->add(* scroller);
+ overlay->get_style_context()->add_class("forcebright");
+ scroller->set_size_request(100, -1);
+ table->attach(*Gtk::manage(overlay), 0, row, 2, 1);
+
+ /*************************Overlays******************************/
+ overlay_opacity = new Gtk::Image();
+ overlay_opacity->set_halign(Gtk::ALIGN_START);
+ overlay_opacity->set_valign(Gtk::ALIGN_START);
+ overlay_opacity->get_style_context()->add_class("rawstyle");
+ overlay_opacity->set_no_show_all(true);
+ // No results
+ overlay_icon = sp_get_icon_image("searching", Gtk::ICON_SIZE_DIALOG);
+ overlay_icon->set_pixel_size(110);
+ overlay_icon->set_halign(Gtk::ALIGN_CENTER);
+ overlay_icon->set_valign(Gtk::ALIGN_START);
+
+ overlay_icon->set_margin_top(25);
+ 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_opacity);
+ overlay->add_overlay(*overlay_icon);
+ overlay->add_overlay(*overlay_title);
+ overlay->add_overlay(*overlay_desc);
+
+ previous_height = 0;
+ previous_width = 0;
+ ++row;
+
+ /******************** Progress *******************************/
+ progress = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL);
+ progress_bar = Gtk::manage(new Gtk::ProgressBar());
+ table->attach(*Gtk::manage(progress),0,row, 2, 1);
+ progress->pack_start(* progress_bar, Gtk::PACK_EXPAND_WIDGET);
+ progress->set_margin_top(15);
+ progress->set_margin_bottom(15);
+ progress->set_margin_start(20);
+ progress->set_margin_end(20);
+
+ ++row;
+
+ /******************** Tools *******************************/
+ tools = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL);
+
+ //tools->set_layout( Gtk::BUTTONBOX_END );
+ scroller->set_hexpand();
+ table->attach(*Gtk::manage(tools),0,row,2,1);
+
+ auto add_symbol_image = Gtk::manage(sp_get_icon_image("symbol-add", Gtk::ICON_SIZE_SMALL_TOOLBAR));
+
+ add_symbol = Gtk::manage(new Gtk::Button());
+ add_symbol->add(*add_symbol_image);
+ add_symbol->set_tooltip_text(_("Add Symbol from the current document."));
+ add_symbol->set_relief( Gtk::RELIEF_NONE );
+ add_symbol->set_focus_on_click( false );
+ add_symbol->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::insertSymbol));
+ tools->pack_start(* add_symbol, Gtk::PACK_SHRINK);
+
+ auto remove_symbolImage = Gtk::manage(sp_get_icon_image("symbol-remove", Gtk::ICON_SIZE_SMALL_TOOLBAR));
+
+ remove_symbol = Gtk::manage(new Gtk::Button());
+ remove_symbol->add(*remove_symbolImage);
+ remove_symbol->set_tooltip_text(_("Remove Symbol from the current document."));
+ remove_symbol->set_relief( Gtk::RELIEF_NONE );
+ remove_symbol->set_focus_on_click( false );
+ remove_symbol->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::revertSymbol));
+ tools->pack_start(* remove_symbol, Gtk::PACK_SHRINK);
+
+ Gtk::Label* spacer = Gtk::manage(new Gtk::Label(""));
+ tools->pack_start(* Gtk::manage(spacer));
+
+ // Pack size (controls display area)
+ pack_size = 2; // Default 32px
+
+ auto packMoreImage = Gtk::manage(sp_get_icon_image("pack-more", Gtk::ICON_SIZE_SMALL_TOOLBAR));
+
+ more = Gtk::manage(new Gtk::Button());
+ more->add(*packMoreImage);
+ more->set_tooltip_text(_("Display more icons in row."));
+ more->set_relief( Gtk::RELIEF_NONE );
+ more->set_focus_on_click( false );
+ more->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::packmore));
+ tools->pack_start(* more, Gtk::PACK_SHRINK);
+
+ auto packLessImage = Gtk::manage(sp_get_icon_image("pack-less", Gtk::ICON_SIZE_SMALL_TOOLBAR));
+
+ fewer = Gtk::manage(new Gtk::Button());
+ fewer->add(*packLessImage);
+ fewer->set_tooltip_text(_("Display fewer icons in row."));
+ fewer->set_relief( Gtk::RELIEF_NONE );
+ fewer->set_focus_on_click( false );
+ fewer->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::packless));
+ tools->pack_start(* fewer, Gtk::PACK_SHRINK);
+
+ // Toggle scale to fit on/off
+ auto fit_symbolImage = Gtk::manage(sp_get_icon_image("symbol-fit", Gtk::ICON_SIZE_SMALL_TOOLBAR));
+
+ fit_symbol = Gtk::manage(new Gtk::ToggleButton());
+ fit_symbol->add(*fit_symbolImage);
+ fit_symbol->set_tooltip_text(_("Toggle 'fit' symbols in icon space."));
+ fit_symbol->set_relief( Gtk::RELIEF_NONE );
+ fit_symbol->set_focus_on_click( false );
+ fit_symbol->set_active( true );
+ fit_symbol->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::rebuild));
+ tools->pack_start(* fit_symbol, Gtk::PACK_SHRINK);
+
+ // Render size (scales symbols within display area)
+ scale_factor = 0; // Default 1:1 * pack_size/pack_size default
+ auto zoom_outImage = Gtk::manage(sp_get_icon_image("symbol-smaller", Gtk::ICON_SIZE_SMALL_TOOLBAR));
+
+ zoom_out = Gtk::manage(new Gtk::Button());
+ zoom_out->add(*zoom_outImage);
+ zoom_out->set_tooltip_text(_("Make symbols smaller by zooming out."));
+ zoom_out->set_relief( Gtk::RELIEF_NONE );
+ zoom_out->set_focus_on_click( false );
+ zoom_out->set_sensitive( false );
+ zoom_out->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::zoomout));
+ tools->pack_start(* zoom_out, Gtk::PACK_SHRINK);
+
+ auto zoom_inImage = Gtk::manage(sp_get_icon_image("symbol-bigger", Gtk::ICON_SIZE_SMALL_TOOLBAR));
+
+ zoom_in = Gtk::manage(new Gtk::Button());
+ zoom_in->add(*zoom_inImage);
+ zoom_in->set_tooltip_text(_("Make symbols bigger by zooming in."));
+ zoom_in->set_relief( Gtk::RELIEF_NONE );
+ zoom_in->set_focus_on_click( false );
+ zoom_in->set_sensitive( false );
+ zoom_in->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::zoomin));
+ tools->pack_start(* zoom_in, Gtk::PACK_SHRINK);
+
+ ++row;
+
+ sensitive = true;
+
+ preview_document = symbolsPreviewDoc(); /* Template to render symbols in */
+ preview_document->ensureUpToDate(); /* Necessary? */
+ key = SPItem::display_key_new(1);
+ renderDrawing.setRoot(preview_document->getRoot()->invoke_show(renderDrawing, key, SP_ITEM_SHOW_DISPLAY ));
+
+ getSymbolsTitle();
+ icons_found = false;
+}
+
+SymbolsDialog::~SymbolsDialog()
+{
+ for (auto &connection : gtk_connections) {
+ connection.disconnect();
+ }
+ gtk_connections.clear();
+ idleconn.disconnect();
+
+ Inkscape::GC::release(preview_document);
+ assert(preview_document->_anchored_refcount() == 0);
+ delete preview_document;
+}
+
+SymbolsDialog& SymbolsDialog::getInstance()
+{
+ return *new SymbolsDialog();
+}
+
+void SymbolsDialog::packless() {
+ if(pack_size < 4) {
+ pack_size++;
+ rebuild();
+ }
+}
+
+void SymbolsDialog::packmore() {
+ if(pack_size > 0) {
+ pack_size--;
+ rebuild();
+ }
+}
+
+void SymbolsDialog::zoomin() {
+ if(scale_factor < 4) {
+ scale_factor++;
+ rebuild();
+ }
+}
+
+void SymbolsDialog::zoomout() {
+ if(scale_factor > -8) {
+ scale_factor--;
+ rebuild();
+ }
+}
+
+void SymbolsDialog::rebuild() {
+
+ if (!sensitive) {
+ return;
+ }
+
+ if( fit_symbol->get_active() ) {
+ zoom_in->set_sensitive( false );
+ zoom_out->set_sensitive( false );
+ } else {
+ zoom_in->set_sensitive( true);
+ zoom_out->set_sensitive( true );
+ }
+ store->clear();
+ SPDocument* symbol_document = selectedSymbols();
+ icons_found = false;
+ //We are not in search all docs
+ if (search->get_text() != _("Searching...") && search->get_text() != _("Loading all symbols...")) {
+ Glib::ustring current = Glib::Markup::escape_text(symbol_set->get_active_text());
+ if (current == ALLDOCS && search->get_text() != "") {
+ searchsymbols();
+ return;
+ }
+ }
+ if (symbol_document) {
+ addSymbolsInDoc(symbol_document);
+ } else {
+ showOverlay();
+ }
+}
+void SymbolsDialog::showOverlay() {
+ Glib::ustring current = Glib::Markup::escape_text(symbol_set->get_active_text());
+ if (current == ALLDOCS && !l.size())
+ {
+ overlay_icon->hide();
+ if (!all_docs_processed ) {
+ overlay_icon->show();
+ overlay_title->set_markup(Glib::ustring("<span size=\"large\">") +
+ Glib::ustring(_("Search in all symbol sets...")) + Glib::ustring("</span>"));
+ overlay_desc->set_markup(Glib::ustring("<span size=\"small\">") +
+ Glib::ustring(_("The first search can be slow.")) + Glib::ustring("</span>"));
+ } else if (!icons_found && !search_str.empty()) {
+ overlay_title->set_markup(Glib::ustring("<span size=\"large\">") + Glib::ustring(_("No symbols found.")) +
+ Glib::ustring("</span>"));
+ overlay_desc->set_markup(Glib::ustring("<span size=\"small\">") +
+ Glib::ustring(_("Try a different search term.")) + Glib::ustring("</span>"));
+ } else {
+ overlay_icon->show();
+ overlay_title->set_markup(Glib::ustring("<spansize=\"large\">") +
+ Glib::ustring(_("Search in all symbol sets...")) + Glib::ustring("</span>"));
+ overlay_desc->set_markup(Glib::ustring("<span size=\"small\">") + Glib::ustring("</span>"));
+ }
+ } else if (!number_symbols && (current != CURRENTDOC || !search_str.empty())) {
+ overlay_title->set_markup(Glib::ustring("<span size=\"large\">") + Glib::ustring(_("No 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_opacity->set_size_request(width, height);
+ overlay_opacity->set(getOverlay(width, height));
+ }
+ overlay_opacity->hide();
+ overlay_icon->show();
+ overlay_title->show();
+ overlay_desc->show();
+ if (l.size()) {
+ overlay_opacity->show();
+ overlay_icon->hide();
+ overlay_title->hide();
+ overlay_desc->hide();
+ }
+}
+
+void SymbolsDialog::hideOverlay() {
+ overlay_opacity->hide();
+ overlay_icon->hide();
+ overlay_title->hide();
+ overlay_desc->hide();
+}
+
+void SymbolsDialog::insertSymbol() {
+ getDesktop()->selection->toSymbol();
+}
+
+void SymbolsDialog::revertSymbol() {
+ if (auto document = getDocument()) {
+ if (auto symbol = dynamic_cast<SPSymbol*> (document->getObjectById(selectedSymbolId()))) {
+ 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 iconArray = icon_view->get_selected_items();
+
+ if( iconArray.empty() ) {
+ //std::cout << " iconArray empty: huh? " << std::endl;
+ } else {
+ Gtk::TreeModel::Path const & path = *iconArray.begin();
+ Gtk::ListStore::iterator row = store->get_iter(path);
+ Glib::ustring symbol_id = (*row)[getColumns()->symbol_id];
+ GdkAtom dataAtom = gdk_atom_intern( "application/x-inkscape-paste", FALSE );
+ gtk_selection_data_set( data.gobj(), dataAtom, 9, (guchar*)symbol_id.c_str(), symbol_id.length() );
+ }
+
+}
+
+void SymbolsDialog::defsModified(SPObject * /*object*/, guint /*flags*/)
+{
+ Glib::ustring doc_title = symbol_set->get_active_text();
+ if (doc_title != ALLDOCS && !symbol_sets[doc_title] ) {
+ rebuild();
+ }
+}
+
+void SymbolsDialog::selectionChanged(Inkscape::Selection *selection) {
+ Glib::ustring symbol_id = selectedSymbolId();
+ Glib::ustring doc_title = selectedSymbolDocTitle();
+ if (!doc_title.empty()) {
+ SPDocument* symbol_document = symbol_sets[doc_title];
+ if (!symbol_document) {
+ //we are in global search so get the original symbol document by title
+ symbol_document = selectedSymbols();
+ }
+ if (symbol_document) {
+ SPObject* symbol = symbol_document->getObjectById(symbol_id);
+ if(symbol && !selection->includes(symbol)) {
+ icon_view->unselect_all();
+ }
+ }
+ }
+}
+
+void SymbolsDialog::documentReplaced()
+{
+ defs_modified.disconnect();
+ if (auto document = getDocument()) {
+ defs_modified = document->getDefs()->connectModified(sigc::mem_fun(*this, &SymbolsDialog::defsModified));
+ if (!symbol_sets[symbol_set->get_active_text()]) {
+ // Symbol set is from Current document, need to rebuild
+ rebuild();
+ }
+ }
+}
+
+SPDocument* SymbolsDialog::selectedSymbols() {
+ /* OK, we know symbol name... now we need to copy it to clipboard, bon chance! */
+ Glib::ustring doc_title = symbol_set->get_active_text();
+ if (doc_title == ALLDOCS) {
+ return nullptr;
+ }
+ SPDocument* symbol_document = symbol_sets[doc_title];
+ if( !symbol_document ) {
+ symbol_document = getSymbolsSet(doc_title).second;
+ // Symbol must be from Current Document (this method of checking should be language independent).
+ if( !symbol_document ) {
+ // Symbol must be from Current Document (this method of
+ // checking should be language independent).
+ symbol_document = getDocument();
+ add_symbol->set_sensitive( true );
+ remove_symbol->set_sensitive( true );
+ } else {
+ add_symbol->set_sensitive( false );
+ remove_symbol->set_sensitive( false );
+ }
+ }
+ return symbol_document;
+}
+
+Glib::ustring SymbolsDialog::selectedSymbolId() {
+
+ auto iconArray = icon_view->get_selected_items();
+
+ if( !iconArray.empty() ) {
+ Gtk::TreeModel::Path const & path = *iconArray.begin();
+ Gtk::ListStore::iterator row = store->get_iter(path);
+ return (*row)[getColumns()->symbol_id];
+ }
+ return Glib::ustring("");
+}
+
+Glib::ustring SymbolsDialog::selectedSymbolDocTitle() {
+
+ auto iconArray = icon_view->get_selected_items();
+
+ if( !iconArray.empty() ) {
+ Gtk::TreeModel::Path const & path = *iconArray.begin();
+ Gtk::ListStore::iterator row = store->get_iter(path);
+ return (*row)[getColumns()->symbol_doc_title];
+ }
+ return Glib::ustring("");
+}
+
+Glib::ustring SymbolsDialog::documentTitle(SPDocument* symbol_doc) {
+ if (symbol_doc) {
+ SPRoot * root = symbol_doc->getRoot();
+ gchar * title = root->title();
+ if (title) {
+ return ellipsize(Glib::ustring(title), 33);
+ }
+ g_free(title);
+ }
+ Glib::ustring current = symbol_set->get_active_text();
+ if (current == CURRENTDOC) {
+ return current;
+ }
+ return _("Untitled document");
+}
+
+void SymbolsDialog::iconChanged() {
+
+ Glib::ustring symbol_id = selectedSymbolId();
+ SPDocument* symbol_document = selectedSymbols();
+ if (!symbol_document) {
+ //we are in global search so get the original symbol document by title
+ Glib::ustring doc_title = selectedSymbolDocTitle();
+ if (!doc_title.empty()) {
+ symbol_document = symbol_sets[doc_title];
+ }
+ }
+ if (symbol_document) {
+ SPObject* symbol = symbol_document->getObjectById(symbol_id);
+
+ if( symbol ) {
+ // 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 *cm = ClipboardManager::get();
+ cm->copySymbol(symbol->getRepr(), style, symbol_document);
+ }
+ }
+}
+
+#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(Glib::ustring filename, Glib::ustring name ) {
+ gchar *fullname;
+ #ifdef _WIN32
+ // RVNGFileStream uses fopen() internally which unfortunately only uses ANSI encoding on Windows
+ // therefore attempt to convert uri to the system codepage
+ // even if this is not possible the alternate short (8.3) file name will be used if available
+ fullname = g_win32_locale_filename_from_utf8(filename.c_str());
+ #else
+ fullname = strdup(filename.c_str());
+ #endif
+
+ RVNGFileStream input(fullname);
+ g_free(fullname);
+
+ if (!libvisio::VisioDocument::isSupported(&input)) {
+ return nullptr;
+ }
+ RVNGStringVector output;
+ RVNGStringVector titles;
+ 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(), strlen( tmpSVGOutput.c_str()), false );
+
+}
+#endif
+
+/* Hunts preference directories for symbol files */
+void SymbolsDialog::getSymbolsTitle() {
+
+ using namespace Inkscape::IO::Resource;
+ Glib::ustring title;
+ number_docs = 0;
+ std::regex matchtitle (".*?<title.*?>(.*?)<(/| /)");
+ for(auto &filename: get_filenames(SYMBOLS, {".svg", ".vss"})) {
+ if(Glib::str_has_suffix(filename, ".vss")) {
+ std::size_t found = filename.find_last_of("/\\");
+ filename = filename.substr(found+1);
+ title = filename.erase(filename.rfind('.'));
+ if(title.empty()) {
+ title = _("Unnamed Symbols");
+ }
+ symbol_sets[title]= nullptr;
+ ++number_docs;
+ } else {
+ std::ifstream infile(filename);
+ std::string line;
+ while (std::getline(infile, line)) {
+ std::string title_res = std::regex_replace (line, matchtitle,"$1",std::regex_constants::format_no_copy);
+ if (!title_res.empty()) {
+ title_res = g_dpgettext2(nullptr, "Symbol", title_res.c_str());
+ symbol_sets[ellipsize(Glib::ustring(title_res), 33)]= nullptr;
+ ++number_docs;
+ break;
+ }
+ std::string::size_type position_exit = line.find ("<defs");
+ if (position_exit != std::string::npos) {
+ std::size_t found = filename.find_last_of("/\\");
+ filename = filename.substr(found+1);
+ title = filename.erase(filename.rfind('.'));
+ if(title.empty()) {
+ title = _("Unnamed Symbols");
+ }
+ symbol_sets[title]= nullptr;
+ ++number_docs;
+ break;
+ }
+ }
+ }
+ }
+ for(auto const &symbol_document_map : symbol_sets) {
+ symbol_set->append(symbol_document_map.first);
+ }
+}
+
+/* Hunts preference directories for symbol files */
+std::pair<Glib::ustring, SPDocument*>
+SymbolsDialog::getSymbolsSet(Glib::ustring title)
+{
+ SPDocument* symbol_doc = nullptr;
+ Glib::ustring current = symbol_set->get_active_text();
+ if (current == CURRENTDOC) {
+ return std::make_pair(CURRENTDOC, symbol_doc);
+ }
+ if (symbol_sets[title]) {
+ sensitive = false;
+ symbol_set->remove_all();
+ symbol_set->append(CURRENTDOC);
+ symbol_set->append(ALLDOCS);
+ for(auto const &symbol_document_map : symbol_sets) {
+ if (CURRENTDOC != symbol_document_map.first) {
+ symbol_set->append(symbol_document_map.first);
+ }
+ }
+ symbol_set->set_active_text(title);
+ sensitive = true;
+ return std::make_pair(title, symbol_sets[title]);
+ }
+ using namespace Inkscape::IO::Resource;
+ Glib::ustring new_title;
+
+ std::regex matchtitle (".*?<title.*?>(.*?)<(/| /)");
+ for(auto &filename: get_filenames(SYMBOLS, {".svg", ".vss"})) {
+ if(Glib::str_has_suffix(filename, ".vss")) {
+#ifdef WITH_LIBVISIO
+ std::size_t pos = filename.find_last_of("/\\");
+ Glib::ustring filename_short = "";
+ if (pos != std::string::npos) {
+ filename_short = filename.substr(pos+1);
+ }
+ if (filename_short == title + ".vss") {
+ new_title = title;
+ symbol_doc = read_vss(Glib::ustring(filename), title);
+ }
+#endif
+ } else {
+ std::ifstream infile(filename);
+ std::string line;
+ while (std::getline(infile, line)) {
+ std::string title_res = std::regex_replace (line, matchtitle,"$1",std::regex_constants::format_no_copy);
+ if (!title_res.empty()) {
+ title_res = g_dpgettext2(nullptr, "Symbol", title_res.c_str());
+ new_title = ellipsize(Glib::ustring(title_res), 33);
+ }
+ std::size_t pos = filename.find_last_of("/\\");
+ Glib::ustring filename_short = "";
+ if (pos != std::string::npos) {
+ filename_short = filename.substr(pos+1);
+ }
+ if (title == new_title || filename_short == title + ".svg") {
+ new_title = title;
+ if(Glib::str_has_suffix(filename, ".svg")) {
+ symbol_doc = SPDocument::createNewDoc(filename.c_str(), FALSE);
+ }
+ }
+ if (symbol_doc) {
+ break;
+ }
+ std::string::size_type position_exit = line.find ("<defs");
+ if (position_exit != std::string::npos) {
+ break;
+ }
+ }
+ }
+ if (symbol_doc) {
+ break;
+ }
+ }
+ if(symbol_doc) {
+ symbol_sets.erase(title);
+ symbol_sets[new_title] = symbol_doc;
+ sensitive = false;
+ symbol_set->remove_all();
+ symbol_set->append(CURRENTDOC);
+ symbol_set->append(ALLDOCS);
+ for(auto const &symbol_document_map : symbol_sets) {
+ if (CURRENTDOC != symbol_document_map.first) {
+ symbol_set->append(symbol_document_map.first);
+ }
+ }
+ symbol_set->set_active_text(new_title);
+ sensitive = true;
+ }
+ return std::make_pair(new_title, symbol_doc);
+}
+
+void SymbolsDialog::symbolsInDocRecursive (SPObject *r, std::map<Glib::ustring, std::pair<Glib::ustring, SPSymbol*> > &l, Glib::ustring doc_title)
+{
+ if(!r) return;
+
+ // Stop multiple counting of same symbol
+ if ( dynamic_cast<SPUse *>(r) ) {
+ return;
+ }
+
+ if ( dynamic_cast<SPSymbol *>(r)) {
+ Glib::ustring id = r->getAttribute("id");
+ gchar * title = r->title();
+ if(title) {
+ l[doc_title + title + id] = std::make_pair(doc_title,dynamic_cast<SPSymbol *>(r));
+ } else {
+ l[Glib::ustring(_("notitle_")) + id] = std::make_pair(doc_title,dynamic_cast<SPSymbol *>(r));
+ }
+ g_free(title);
+ }
+ for (auto& child: r->children) {
+ symbolsInDocRecursive(&child, l, doc_title);
+ }
+}
+
+std::map<Glib::ustring, std::pair<Glib::ustring, SPSymbol*> >
+SymbolsDialog::symbolsInDoc( SPDocument* symbol_document, Glib::ustring doc_title)
+{
+
+ std::map<Glib::ustring, std::pair<Glib::ustring, SPSymbol*> > l;
+ if (symbol_document) {
+ symbolsInDocRecursive (symbol_document->getRoot(), l , doc_title);
+ }
+ return l;
+}
+
+void SymbolsDialog::useInDoc (SPObject *r, std::vector<SPUse*> &l)
+{
+
+ if ( dynamic_cast<SPUse *>(r) ) {
+ l.push_back(dynamic_cast<SPUse *>(r));
+ }
+
+ for (auto& child: r->children) {
+ useInDoc( &child, l );
+ }
+}
+
+std::vector<SPUse*> SymbolsDialog::useInDoc( SPDocument* useDocument) {
+ std::vector<SPUse*> l;
+ useInDoc (useDocument->getRoot(), l);
+ return l;
+}
+
+// Returns style from first <use> element found that references id.
+// This is a last ditch effort to find a style.
+gchar const* SymbolsDialog::styleFromUse( gchar const* id, SPDocument* document) {
+
+ gchar const* style = nullptr;
+ std::vector<SPUse*> l = useInDoc( document );
+ for( auto use:l ) {
+ if ( use ) {
+ gchar const *href = use->getRepr()->attribute("xlink:href");
+ if( href ) {
+ Glib::ustring href2(href);
+ Glib::ustring id2(id);
+ id2 = "#" + id2;
+ if( !href2.compare(id2) ) {
+ style = use->getRepr()->attribute("style");
+ break;
+ }
+ }
+ }
+ }
+ return style;
+}
+
+void SymbolsDialog::clearSearch()
+{
+ if(search->get_text().empty() && sensitive) {
+ enableWidgets(false);
+ search_str = "";
+ store->clear();
+ SPDocument* symbol_document = selectedSymbols();
+ if (symbol_document) {
+ //We are not in search all docs
+ icons_found = false;
+ addSymbolsInDoc(symbol_document);
+ } else {
+ showOverlay();
+ enableWidgets(true);
+ }
+ }
+}
+
+void SymbolsDialog::enableWidgets(bool enable)
+{
+ symbol_set->set_sensitive(enable);
+ search->set_sensitive(enable);
+ tools ->set_sensitive(enable);
+}
+
+void SymbolsDialog::beforeSearch(GdkEventKey* evt)
+{
+ sensitive = false;
+ search_str = search->get_text().lowercase();
+ if (evt->keyval != GDK_KEY_Return) {
+ return;
+ }
+ searchsymbols();
+}
+
+void SymbolsDialog::searchsymbols()
+{
+ progress_bar->set_fraction(0.0);
+ enableWidgets(false);
+ SPDocument *symbol_document = selectedSymbols();
+ if (symbol_document) {
+ // We are not in search all docs
+ search->set_text(_("Searching..."));
+ store->clear();
+ icons_found = false;
+ addSymbolsInDoc(symbol_document);
+ } else {
+ idleconn.disconnect();
+ idleconn = Glib::signal_idle().connect(sigc::mem_fun(*this, &SymbolsDialog::callbackAllSymbols));
+ search->set_text(_("Loading all symbols..."));
+ }
+}
+
+void SymbolsDialog::unsensitive(GdkEventKey* evt)
+{
+ sensitive = true;
+}
+
+bool SymbolsDialog::callbackSymbols(){
+ if (l.size()) {
+ showOverlay();
+ for (auto symbol_data = l.begin(); symbol_data != l.end();) {
+ Glib::ustring doc_title = symbol_data->second.first;
+ SPSymbol * symbol = symbol_data->second.second;
+ counter_symbols ++;
+ gchar *symbol_title_char = symbol->title();
+ gchar *symbol_desc_char = symbol->description();
+ bool found = false;
+ if (symbol_title_char) {
+ Glib::ustring symbol_title = Glib::ustring(symbol_title_char).lowercase();
+ auto pos = symbol_title.rfind(search_str);
+ auto pos_translated = Glib::ustring(g_dpgettext2(nullptr, "Symbol", symbol_title_char)).lowercase().rfind(search_str);
+ if ((pos != std::string::npos) || (pos_translated != std::string::npos)) {
+ found = true;
+ }
+ if (!found && symbol_desc_char) {
+ Glib::ustring symbol_desc = Glib::ustring(symbol_desc_char).lowercase();
+ auto pos = symbol_desc.rfind(search_str);
+ auto pos_translated = Glib::ustring(g_dpgettext2(nullptr, "Symbol", symbol_desc_char)).lowercase().rfind(search_str);
+ if ((pos != std::string::npos) || (pos_translated != std::string::npos)) {
+ found = true;
+ }
+ }
+ }
+ if (symbol && (search_str.empty() || found)) {
+ addSymbol( symbol, doc_title);
+ icons_found = true;
+ }
+
+ progress_bar->set_fraction(((100.0/number_symbols) * counter_symbols)/100.0);
+ symbol_data = l.erase(l.begin());
+ //to get more items and best performance
+ int modulus = number_symbols > 200 ? 50 : (number_symbols/4);
+ g_free(symbol_title_char);
+ g_free(symbol_desc_char);
+ if (modulus && counter_symbols % modulus == 0 && !l.empty()) {
+ return true;
+ }
+ }
+ if (!icons_found && !search_str.empty()) {
+ showOverlay();
+ } else {
+ hideOverlay();
+ }
+ progress_bar->set_fraction(0);
+ sensitive = false;
+ search->set_text(search_str);
+ sensitive = true;
+ enableWidgets(true);
+ return false;
+ }
+ return true;
+}
+
+bool SymbolsDialog::callbackAllSymbols(){
+ Glib::ustring current = symbol_set->get_active_text();
+ if (current == ALLDOCS && search->get_text() == _("Loading all symbols...")) {
+ size_t counter = 0;
+ std::map<Glib::ustring, SPDocument*> symbol_sets_tmp = symbol_sets;
+ for(auto const &symbol_document_map : symbol_sets_tmp) {
+ ++counter;
+ SPDocument* symbol_document = symbol_document_map.second;
+ if (symbol_document) {
+ continue;
+ }
+ symbol_document = getSymbolsSet(symbol_document_map.first).second;
+ symbol_set->set_active_text(ALLDOCS);
+ if (!symbol_document) {
+ continue;
+ }
+ progress_bar->set_fraction(((100.0/number_docs) * counter)/100.0);
+ return true;
+ }
+ symbol_sets_tmp.clear();
+ hideOverlay();
+ all_docs_processed = true;
+ addSymbols();
+ progress_bar->set_fraction(0);
+ search->set_text("Searching...");
+ return false;
+ }
+ return true;
+}
+
+Glib::ustring SymbolsDialog::ellipsize(Glib::ustring data, size_t limit) {
+ if (data.length() > limit) {
+ data = data.substr(0, limit-3);
+ return data + "...";
+ }
+ return data;
+}
+
+void SymbolsDialog::addSymbolsInDoc(SPDocument* symbol_document) {
+
+ if (!symbol_document) {
+ return; //Search all
+ }
+ Glib::ustring doc_title = documentTitle(symbol_document);
+ progress_bar->set_fraction(0.0);
+ counter_symbols = 0;
+ l = symbolsInDoc(symbol_document, doc_title);
+ number_symbols = l.size();
+ if (!number_symbols) {
+ sensitive = false;
+ search->set_text(search_str);
+ sensitive = true;
+ enableWidgets(true);
+ idleconn.disconnect();
+ showOverlay();
+ } else {
+ idleconn.disconnect();
+ idleconn = Glib::signal_idle().connect( sigc::mem_fun(*this, &SymbolsDialog::callbackSymbols));
+ }
+}
+
+void SymbolsDialog::addSymbols() {
+ store->clear();
+ icons_found = false;
+ for(auto const &symbol_document_map : symbol_sets) {
+ SPDocument* symbol_document = symbol_document_map.second;
+ if (!symbol_document) {
+ continue;
+ }
+ Glib::ustring doc_title = documentTitle(symbol_document);
+ std::map<Glib::ustring, std::pair<Glib::ustring, SPSymbol*> > l_tmp = symbolsInDoc(symbol_document, doc_title);
+ for(auto &p : l_tmp ) {
+ l[p.first] = p.second;
+ }
+ l_tmp.clear();
+ }
+ counter_symbols = 0;
+ progress_bar->set_fraction(0.0);
+ number_symbols = l.size();
+ if (!number_symbols) {
+ showOverlay();
+ idleconn.disconnect();
+ sensitive = false;
+ search->set_text(search_str);
+ sensitive = true;
+ enableWidgets(true);
+ } else {
+ idleconn.disconnect();
+ idleconn = Glib::signal_idle().connect( sigc::mem_fun(*this, &SymbolsDialog::callbackSymbols));
+ }
+}
+
+void SymbolsDialog::addSymbol( SPObject* symbol, Glib::ustring doc_title)
+{
+ gchar const *id = symbol->getRepr()->attribute("id");
+
+ if (doc_title.empty()) {
+ doc_title = CURRENTDOC;
+ } else {
+ doc_title = g_dpgettext2(nullptr, "Symbol", doc_title.c_str());
+ }
+
+ Glib::ustring symbol_title;
+ gchar *title = symbol->title(); // From title element
+ if (title) {
+ symbol_title = Glib::ustring::compose("%1 (%2)", g_dpgettext2(nullptr, "Symbol", title), doc_title.c_str());
+ } else {
+ symbol_title = Glib::ustring::compose("%1 %2 (%3)", _("Symbol without title"), Glib::ustring(id), doc_title);
+ }
+ g_free(title);
+
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf = drawSymbol( symbol );
+ if( pixbuf ) {
+ Gtk::ListStore::iterator row = store->append();
+ SymbolColumns* columns = getColumns();
+ (*row)[columns->symbol_id] = Glib::ustring( id );
+ (*row)[columns->symbol_title] = Glib::Markup::escape_text(symbol_title);
+ (*row)[columns->symbol_doc_title] = Glib::Markup::escape_text(doc_title);
+ (*row)[columns->symbol_image] = pixbuf;
+ delete columns;
+ }
+}
+
+/*
+ * Returns image of symbol.
+ *
+ * Symbols normally are not visible. They must be referenced by a
+ * <use> element. A temporary document is created with a dummy
+ * <symbol> element and a <use> element that references the symbol
+ * element. Each real symbol is swapped in for the dummy symbol and
+ * the temporary document is rendered.
+ */
+Glib::RefPtr<Gdk::Pixbuf>
+SymbolsDialog::drawSymbol(SPObject *symbol)
+{
+ // Create a copy repr of the symbol with id="the_symbol"
+ Inkscape::XML::Document *xml_doc = preview_document->getReprDoc();
+ Inkscape::XML::Node *repr = symbol->getRepr()->duplicate(xml_doc);
+ repr->setAttribute("id", "the_symbol");
+
+ // Replace old "the_symbol" in preview_document by new.
+ Inkscape::XML::Node *root = preview_document->getReprRoot();
+ SPObject *symbol_old = preview_document->getObjectById("the_symbol");
+ if (symbol_old) {
+ symbol_old->deleteObject(false);
+ }
+
+ SPDocument::install_reference_document scoped(preview_document, getDocument());
+
+ // 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 );
+
+ root->appendChild(repr);
+ Inkscape::GC::release(repr);
+
+ // Uncomment this to get the preview_document documents saved (useful for debugging)
+ // FILE *fp = fopen (g_strconcat(id, ".svg", NULL), "w");
+ // sp_repr_save_stream(preview_document->getReprDoc(), fp);
+ // fclose (fp);
+
+ // Make sure preview_document is up-to-date.
+ preview_document->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ preview_document->ensureUpToDate();
+
+ // Make sure we have symbol in preview_document
+ SPObject *object_temp = preview_document->getObjectById( "the_use" );
+
+ SPItem *item = dynamic_cast<SPItem *>(object_temp);
+ g_assert(item != nullptr);
+ unsigned psize = SYMBOL_ICON_SIZES[pack_size];
+
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf(nullptr);
+ // We could use cache here, but it doesn't really work with the structure
+ // of this user interface and we've already cached the pixbuf in the gtklist
+
+ // Find object's bbox in document.
+ // Note symbols can have own viewport... ignore for now.
+ //Geom::OptRect dbox = item->geometricBounds();
+ Geom::OptRect dbox = item->documentVisualBounds();
+
+ if (dbox) {
+ /* Scale symbols to fit */
+ double scale = 1.0;
+ double width = dbox->width();
+ double height = dbox->height();
+
+ if( width == 0.0 ) width = 1.0;
+ if( height == 0.0 ) height = 1.0;
+
+ if( fit_symbol->get_active() )
+ scale = psize / ceil(std::max(width, height));
+ else
+ scale = pow( 2.0, scale_factor/2.0 ) * psize / 32.0;
+
+ pixbuf = Glib::wrap(render_pixbuf(renderDrawing, scale, *dbox, psize));
+ }
+
+ return pixbuf;
+}
+
+/*
+ * Return empty doc to render symbols in.
+ * Symbols are by default not rendered so a <use> element is
+ * provided.
+ */
+SPDocument* SymbolsDialog::symbolsPreviewDoc()
+{
+ // BUG: <symbol> must be inside <defs>
+ gchar const *buffer =
+"<svg xmlns=\"http://www.w3.org/2000/svg\""
+" xmlns:sodipodi=\"http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd\""
+" xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\""
+" xmlns:xlink=\"http://www.w3.org/1999/xlink\">"
+" <defs id=\"defs\">"
+" <symbol id=\"the_symbol\"/>"
+" </defs>"
+" <use id=\"the_use\" xlink:href=\"#the_symbol\"/>"
+"</svg>";
+ return SPDocument::createNewDocFromMem( buffer, strlen(buffer), FALSE );
+}
+
+/*
+ * Update image widgets
+ */
+Glib::RefPtr<Gdk::Pixbuf>
+SymbolsDialog::getOverlay(gint width, gint height)
+{
+ cairo_surface_t *surface;
+ cairo_t *cr;
+ surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height);
+ cr = cairo_create (surface);
+ cairo_set_source_rgba(cr, 1, 1, 1, 0.75);
+ cairo_rectangle (cr, 0, 0, width, height);
+ cairo_fill (cr);
+ GdkPixbuf* pixbuf = ink_pixbuf_create_from_cairo_surface(surface);
+ cairo_destroy (cr);
+ return Glib::wrap(pixbuf);
+}
+
+} //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..c507ffb
--- /dev/null
+++ b/src/ui/dialog/symbols.h
@@ -0,0 +1,179 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Symbols dialog
+ */
+/* Authors:
+ * Tavmjong Bah, Martin Owens
+ *
+ * Copyright (C) 2012 Tavmjong Bah
+ * 2013 Martin Owens
+ * 2017 Jabiertxo Arraiza
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_SYMBOLS_H
+#define INKSCAPE_UI_DIALOG_SYMBOLS_H
+
+#include <gtkmm.h>
+#include <vector>
+
+#include "display/drawing.h"
+#include "helper/auto-connection.h"
+#include "include/gtkmm_version.h"
+#include "ui/dialog/dialog-base.h"
+
+class SPObject;
+class SPSymbol;
+class SPUse;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class SymbolColumns; // For Gtk::ListStore
+
+/**
+ * A dialog that displays selectable symbols and allows users to drag or paste
+ * those symbols from the dialog into the document.
+ *
+ * Symbol documents are loaded from the preferences paths and displayed in a
+ * drop-down list to the user. The user then selects which of the symbols
+ * documents they want to get symbols from. The first document in the list is
+ * always the current document.
+ *
+ * This then updates an icon-view with all the symbols available. Selecting one
+ * puts it onto the clipboard. Dragging it or pasting it onto the canvas copies
+ * the symbol from the symbol document, into the current document and places a
+ * new <use element at the correct location on the canvas.
+ *
+ * Selected groups on the canvas can be added to the current document's symbols
+ * table, and symbols can be removed from the current document. This allows
+ * new symbols documents to be constructed and if saved in the prefs folder will
+ * make those symbols available for all future documents.
+ */
+
+const int SYMBOL_ICON_SIZES[] = {16, 24, 32, 48, 64};
+
+class SymbolsDialog : public DialogBase
+{
+public:
+ SymbolsDialog( gchar const* prefsPath = "/dialogs/symbols" );
+ ~SymbolsDialog() override;
+
+ static SymbolsDialog& getInstance();
+private:
+ SymbolsDialog(SymbolsDialog const &) = delete; // no copy
+ SymbolsDialog &operator=(SymbolsDialog const &) = delete; // no assign
+
+ static SymbolColumns *getColumns();
+ void documentReplaced() override;
+ void selectionChanged(Inkscape::Selection *selection) override;
+
+ Glib::ustring CURRENTDOC;
+ Glib::ustring ALLDOCS;
+
+ void packless();
+ void packmore();
+ void zoomin();
+ void zoomout();
+ void rebuild();
+ void insertSymbol();
+ void revertSymbol();
+ void defsModified(SPObject *object, guint flags);
+ SPDocument* selectedSymbols();
+ Glib::ustring selectedSymbolId();
+ Glib::ustring selectedSymbolDocTitle();
+ void iconChanged();
+ void iconDragDataGet(const Glib::RefPtr<Gdk::DragContext>& context, Gtk::SelectionData& selection_data, guint info, guint time);
+ void getSymbolsTitle();
+ Glib::ustring documentTitle(SPDocument* doc);
+ std::pair<Glib::ustring, SPDocument*> getSymbolsSet(Glib::ustring title);
+ void addSymbol( SPObject* symbol, Glib::ustring doc_title);
+ SPDocument* symbolsPreviewDoc();
+ void symbolsInDocRecursive (SPObject *r, std::map<Glib::ustring, std::pair<Glib::ustring, SPSymbol*> > &l, Glib::ustring doc_title);
+ std::map<Glib::ustring, std::pair<Glib::ustring, SPSymbol*> > symbolsInDoc( SPDocument* document, Glib::ustring doc_title);
+ void useInDoc(SPObject *r, std::vector<SPUse*> &l);
+ std::vector<SPUse*> useInDoc( SPDocument* document);
+ void beforeSearch(GdkEventKey* evt);
+ void unsensitive(GdkEventKey* evt);
+ void searchsymbols();
+ void addSymbols();
+ void addSymbolsInDoc(SPDocument* document);
+ void showOverlay();
+ void hideOverlay();
+ void clearSearch();
+ bool callbackSymbols();
+ bool callbackAllSymbols();
+ void enableWidgets(bool enable);
+ Glib::ustring ellipsize(Glib::ustring data, size_t limit);
+ gchar const* styleFromUse( gchar const* id, SPDocument* document);
+ Glib::RefPtr<Gdk::Pixbuf> drawSymbol(SPObject *symbol);
+ Glib::RefPtr<Gdk::Pixbuf> getOverlay(gint width, gint height);
+ /* Keep track of all symbol template documents */
+ std::map<Glib::ustring, SPDocument*> symbol_sets;
+ std::map<Glib::ustring, std::pair<Glib::ustring, SPSymbol*> > l;
+ // Index into sizes which is selected
+ int pack_size;
+ // Scale factor
+ int scale_factor;
+ bool sensitive;
+ double previous_height;
+ double previous_width;
+ bool all_docs_processed;
+ size_t number_docs;
+ size_t number_symbols;
+ size_t counter_symbols;
+ bool icons_found;
+ Glib::RefPtr<Gtk::ListStore> store;
+ Glib::ustring search_str;
+ Gtk::ComboBoxText* symbol_set;
+ Gtk::ProgressBar* progress_bar;
+ Gtk::Box* progress;
+ Gtk::SearchEntry* search;
+ Gtk::IconView* icon_view;
+ Gtk::Button* add_symbol;
+ Gtk::Button* remove_symbol;
+ Gtk::Button* zoom_in;
+ Gtk::Button* zoom_out;
+ Gtk::Button* more;
+ Gtk::Button* fewer;
+ Gtk::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::ToggleButton* fit_symbol;
+ Gtk::IconSize iconsize;
+
+ SPDocument* preview_document; /* Document to render single symbol */
+
+ sigc::connection idleconn;
+
+ /* For rendering the template drawing */
+ unsigned key;
+ Inkscape::Drawing renderDrawing;
+
+ std::vector<sigc::connection> gtk_connections;
+ Inkscape::auto_connection defs_modified;
+};
+
+} //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/template-load-tab.cpp b/src/ui/dialog/template-load-tab.cpp
new file mode 100644
index 0000000..120ea25
--- /dev/null
+++ b/src/ui/dialog/template-load-tab.cpp
@@ -0,0 +1,340 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief New From Template abstract tab implementation
+ */
+/* Authors:
+ * Jan Darowski <jan.darowski@gmail.com>, supervised by Krzysztof Kosiński
+ *
+ * Copyright (C) 2013 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "template-widget.h"
+#include "new-from-template.h"
+
+#include <glibmm/miscutils.h>
+#include <glibmm/stringutils.h>
+#include <glibmm/fileutils.h>
+#include <gtkmm/messagedialog.h>
+#include <gtkmm/scrolledwindow.h>
+#include <iostream>
+
+#include "extension/extension.h"
+#include "extension/db.h"
+#include "inkscape.h"
+#include "file.h"
+#include "path-prefix.h"
+
+using namespace Inkscape::IO::Resource;
+
+namespace Inkscape {
+namespace UI {
+
+TemplateLoadTab::TemplateLoadTab(NewFromTemplate* parent)
+ : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)
+ , _current_keyword("")
+ , _keywords_combo(true)
+ , _current_search_type(ALL)
+ , _parent_widget(parent)
+ , _tlist_box(Gtk::ORIENTATION_VERTICAL)
+ , _search_box(Gtk::ORIENTATION_HORIZONTAL)
+{
+ set_border_width(10);
+
+ _info_widget = Gtk::manage(new TemplateWidget());
+
+ Gtk::Label *title;
+ title = Gtk::manage(new Gtk::Label(_("Search:")));
+ _search_box.pack_start(*title, Gtk::PACK_SHRINK);
+ _search_box.pack_start(_keywords_combo, Gtk::PACK_SHRINK, 5);
+
+ _tlist_box.pack_start(_search_box, Gtk::PACK_SHRINK, 10);
+
+ pack_start(_tlist_box, Gtk::PACK_SHRINK);
+ pack_start(*_info_widget, Gtk::PACK_EXPAND_WIDGET, 5);
+
+ Gtk::ScrolledWindow *scrolled;
+ scrolled = Gtk::manage(new Gtk::ScrolledWindow());
+ scrolled->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
+ scrolled->add(_tlist_view);
+ _tlist_box.pack_start(*scrolled, Gtk::PACK_EXPAND_WIDGET, 5);
+
+ _keywords_combo.signal_changed().connect(
+ sigc::mem_fun(*this, &TemplateLoadTab::_keywordSelected));
+ this->show_all();
+
+ _loadTemplates();
+ _initLists();
+}
+
+
+TemplateLoadTab::~TemplateLoadTab()
+= default;
+
+
+void TemplateLoadTab::createTemplate()
+{
+ _info_widget->create();
+}
+
+
+void TemplateLoadTab::_onRowActivated(const Gtk::TreeModel::Path &, Gtk::TreeViewColumn*)
+{
+ createTemplate();
+ NewFromTemplate* parent = static_cast<NewFromTemplate*> (this->get_toplevel());
+ parent->_onClose();
+}
+
+void TemplateLoadTab::_displayTemplateInfo()
+{
+ Glib::RefPtr<Gtk::TreeSelection> templateSelectionRef = _tlist_view.get_selection();
+ if (templateSelectionRef->get_selected()) {
+ _current_template = (*templateSelectionRef->get_selected())[_columns.textValue];
+
+ _info_widget->display(_tdata[_current_template]);
+ _parent_widget->setCreateButtonSensitive(true);
+ }
+
+}
+
+
+void TemplateLoadTab::_initKeywordsList()
+{
+ _keywords_combo.append(_("All"));
+
+ for (const auto & _keyword : _keywords){
+ _keywords_combo.append(_keyword);
+ }
+}
+
+
+void TemplateLoadTab::_initLists()
+{
+ _tlist_store = Gtk::ListStore::create(_columns);
+ _tlist_view.set_model(_tlist_store);
+ _tlist_view.append_column("", _columns.textValue);
+ _tlist_view.set_headers_visible(false);
+
+ _initKeywordsList();
+ _refreshTemplatesList();
+
+ Glib::RefPtr<Gtk::TreeSelection> templateSelectionRef =
+ _tlist_view.get_selection();
+ templateSelectionRef->signal_changed().connect(
+ sigc::mem_fun(*this, &TemplateLoadTab::_displayTemplateInfo));
+
+ _tlist_view.signal_row_activated().connect(
+ sigc::mem_fun(*this, &TemplateLoadTab::_onRowActivated));
+}
+
+void TemplateLoadTab::_keywordSelected()
+{
+ _current_keyword = _keywords_combo.get_active_text();
+ if (_current_keyword == ""){
+ _current_keyword = _keywords_combo.get_entry_text();
+ _current_search_type = USER_SPECIFIED;
+ }
+ else
+ _current_search_type = LIST_KEYWORD;
+
+ if (_current_keyword == "" || _current_keyword == _("All"))
+ _current_search_type = ALL;
+
+ _refreshTemplatesList();
+}
+
+
+void TemplateLoadTab::_refreshTemplatesList()
+{
+ _tlist_store->clear();
+
+ switch (_current_search_type){
+ case ALL :{
+ for (auto & it : _tdata) {
+ Gtk::TreeModel::iterator iter = _tlist_store->append();
+ Gtk::TreeModel::Row row = *iter;
+ row[_columns.textValue] = it.first;
+ }
+ break;
+ }
+
+ case LIST_KEYWORD: {
+ for (auto & it : _tdata) {
+ if (it.second.keywords.count(_current_keyword.lowercase()) != 0){
+ Gtk::TreeModel::iterator iter = _tlist_store->append();
+ Gtk::TreeModel::Row row = *iter;
+ row[_columns.textValue] = it.first;
+ }
+ }
+ break;
+ }
+
+ case USER_SPECIFIED : {
+ for (auto & it : _tdata) {
+ if (it.second.keywords.count(_current_keyword.lowercase()) != 0 ||
+ it.second.display_name.lowercase().find(_current_keyword.lowercase()) != Glib::ustring::npos ||
+ it.second.author.lowercase().find(_current_keyword.lowercase()) != Glib::ustring::npos ||
+ it.second.short_description.lowercase().find(_current_keyword.lowercase()) != Glib::ustring::npos)
+ {
+ Gtk::TreeModel::iterator iter = _tlist_store->append();
+ Gtk::TreeModel::Row row = *iter;
+ row[_columns.textValue] = it.first;
+ }
+ }
+ break;
+ }
+ }
+
+ // reselect item
+ Gtk::TreeIter* item_to_select = nullptr;
+ for (Gtk::TreeModel::Children::iterator it = _tlist_store->children().begin(); it != _tlist_store->children().end(); ++it) {
+ Gtk::TreeModel::Row row = *it;
+ if (_current_template == row[_columns.textValue]) {
+ item_to_select = new Gtk::TreeIter(it);
+ break;
+ }
+ }
+ if (_tlist_store->children().size() == 1) {
+ delete item_to_select;
+ item_to_select = new Gtk::TreeIter(_tlist_store->children().begin());
+ }
+ if (item_to_select) {
+ _tlist_view.get_selection()->select(*item_to_select);
+ delete item_to_select;
+ } else {
+ _current_template = "";
+ _info_widget->clear();
+ _parent_widget->setCreateButtonSensitive(false);
+ }
+}
+
+
+void TemplateLoadTab::_loadTemplates()
+{
+ for(auto &filename: get_filenames(TEMPLATES, {".svg"}, {"default."})) {
+ TemplateData tmp = _processTemplateFile(filename.raw());
+ if (tmp.display_name != "")
+ _tdata[tmp.display_name] = tmp;
+
+ }
+ // procedural templates
+ _getProceduralTemplates();
+}
+
+
+TemplateLoadTab::TemplateData TemplateLoadTab::_processTemplateFile(const std::string &path)
+{
+ TemplateData result;
+ result.path = path;
+ result.is_procedural = false;
+ result.preview_name = "";
+
+ // convert path into valid template name
+ result.display_name = Glib::path_get_basename(path);
+ gsize n = 0;
+ while ((n = result.display_name.find_first_of("_", 0)) < Glib::ustring::npos){
+ result.display_name.replace(n, 1, 1, ' ');
+ }
+ n = result.display_name.rfind(".svg");
+ result.display_name.replace(n, 4, 1, ' ');
+
+ Inkscape::XML::Document *rdoc = sp_repr_read_file(path.data(), SP_SVG_NS_URI);
+ if (rdoc){
+ Inkscape::XML::Node *root = rdoc->root();
+ if (strcmp(root->name(), "svg:svg") != 0){ // Wrong file format
+ return result;
+ }
+
+ Inkscape::XML::Node *templateinfo = sp_repr_lookup_name(root, "inkscape:templateinfo");
+ if (!templateinfo) {
+ templateinfo = sp_repr_lookup_name(root, "inkscape:_templateinfo"); // backwards-compatibility
+ }
+
+ if (templateinfo == nullptr) // No template info
+ return result;
+ _getDataFromNode(templateinfo, result);
+ }
+
+ return result;
+}
+
+void TemplateLoadTab::_getProceduralTemplates()
+{
+ std::list<Inkscape::Extension::Effect *> effects;
+ Inkscape::Extension::db.get_effect_list(effects);
+
+ std::list<Inkscape::Extension::Effect *>::iterator it = effects.begin();
+ while (it != effects.end()){
+ Inkscape::XML::Node *repr = (*it)->get_repr();
+ Inkscape::XML::Node *templateinfo = sp_repr_lookup_name(repr, "inkscape:templateinfo");
+ if (!templateinfo) {
+ templateinfo = sp_repr_lookup_name(repr, "inkscape:_templateinfo"); // backwards-compatibility
+ }
+
+ if (templateinfo){
+ TemplateData result;
+ result.display_name = (*it)->get_name();
+ result.is_procedural = true;
+ result.path = "";
+ result.tpl_effect = *it;
+
+ _getDataFromNode(templateinfo, result, *it);
+ _tdata[result.display_name] = result;
+ }
+ ++it;
+ }
+}
+
+// if the template data comes from a procedural template (aka Effect extension),
+// attempt to translate within the extension's context (which might use a different gettext textdomain)
+const char *_translate(const char* msgid, Extension::Extension *extension)
+{
+ if (extension) {
+ return extension->get_translation(msgid);
+ } else {
+ return _(msgid);
+ }
+}
+
+void TemplateLoadTab::_getDataFromNode(Inkscape::XML::Node *dataNode, TemplateData &data, Extension::Extension *extension)
+{
+ Inkscape::XML::Node *currentData;
+ if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:name")) != nullptr)
+ data.display_name = _translate(currentData->firstChild()->content(), extension);
+ else if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:_name")) != nullptr) // backwards-compatibility
+ data.display_name = _translate(currentData->firstChild()->content(), extension);
+
+ if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:author")) != nullptr)
+ data.author = currentData->firstChild()->content();
+
+ if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:shortdesc")) != nullptr)
+ data.short_description = _translate(currentData->firstChild()->content(), extension);
+ else if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:_shortdesc")) != nullptr) // backwards-compatibility
+ data.short_description = _translate(currentData->firstChild()->content(), extension);
+
+ if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:preview")) != nullptr)
+ data.preview_name = currentData->firstChild()->content();
+
+ if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:date")) != nullptr)
+ data.creation_date = currentData->firstChild()->content();
+
+ if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:_keywords")) != nullptr){
+ Glib::ustring tplKeywords = _translate(currentData->firstChild()->content(), extension);
+ while (!tplKeywords.empty()){
+ std::size_t pos = tplKeywords.find_first_of(" ");
+ if (pos == Glib::ustring::npos)
+ pos = tplKeywords.size();
+
+ Glib::ustring keyword = tplKeywords.substr(0, pos).data();
+ data.keywords.insert(keyword.lowercase());
+ _keywords.insert(keyword.lowercase());
+
+ if (pos == tplKeywords.size())
+ break;
+ tplKeywords.erase(0, pos+1);
+ }
+ }
+}
+
+}
+}
diff --git a/src/ui/dialog/template-load-tab.h b/src/ui/dialog/template-load-tab.h
new file mode 100644
index 0000000..ad87aac
--- /dev/null
+++ b/src/ui/dialog/template-load-tab.h
@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief New From Template abstract tab class
+ */
+/* Authors:
+ * Jan Darowski <jan.darowski@gmail.com>, supervised by Krzysztof Kosiński
+ *
+ * Copyright (C) 2013 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_SEEN_UI_DIALOG_TEMPLATE_LOAD_TAB_H
+#define INKSCAPE_SEEN_UI_DIALOG_TEMPLATE_LOAD_TAB_H
+
+#include <gtkmm/box.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/frame.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/treeview.h>
+#include <map>
+#include <set>
+#include <string>
+
+#include "xml/node.h"
+#include "io/resource.h"
+#include "extension/effect.h"
+
+
+namespace Inkscape {
+
+namespace Extension {
+class Extension;
+}
+
+namespace UI {
+
+class TemplateWidget;
+class NewFromTemplate;
+
+class TemplateLoadTab : public Gtk::Box
+{
+
+public:
+ struct TemplateData
+ {
+ bool is_procedural;
+ std::string path;
+ Glib::ustring display_name;
+ Glib::ustring author;
+ Glib::ustring short_description;
+ Glib::ustring long_description; // unused
+ Glib::ustring preview_name;
+ Glib::ustring creation_date;
+ std::set<Glib::ustring> keywords;
+ Inkscape::Extension::Effect *tpl_effect;
+ };
+
+ TemplateLoadTab(NewFromTemplate* parent);
+ ~TemplateLoadTab() override;
+ virtual void createTemplate();
+
+protected:
+ class StringModelColumns : public Gtk::TreeModelColumnRecord
+ {
+ public:
+ StringModelColumns()
+ {
+ add(textValue);
+ }
+
+ Gtk::TreeModelColumn<Glib::ustring> textValue;
+ };
+
+ Glib::ustring _current_keyword;
+ Glib::ustring _current_template;
+ std::map<Glib::ustring, TemplateData> _tdata;
+ std::set<Glib::ustring> _keywords;
+
+
+ virtual void _displayTemplateInfo();
+ virtual void _initKeywordsList();
+ virtual void _refreshTemplatesList();
+ void _loadTemplates();
+ void _initLists();
+
+ Gtk::Box _tlist_box;
+ Gtk::Box _search_box;
+ TemplateWidget *_info_widget;
+
+ Gtk::ComboBoxText _keywords_combo;
+
+ Gtk::TreeView _tlist_view;
+ Glib::RefPtr<Gtk::ListStore> _tlist_store;
+ StringModelColumns _columns;
+
+private:
+ enum SearchType
+ {
+ LIST_KEYWORD,
+ USER_SPECIFIED,
+ ALL
+ };
+
+ SearchType _current_search_type;
+ NewFromTemplate* _parent_widget;
+
+ void _getDataFromNode(Inkscape::XML::Node *, TemplateData &, Extension::Extension *extension=nullptr);
+ void _getProceduralTemplates();
+ void _getTemplatesFromDomain(Inkscape::IO::Resource::Domain domain);
+ void _keywordSelected();
+ TemplateData _processTemplateFile(const std::string &);
+
+ void _onRowActivated(const Gtk::TreeModel::Path &, Gtk::TreeViewColumn*);
+};
+
+}
+}
+
+#endif
diff --git a/src/ui/dialog/template-widget.cpp b/src/ui/dialog/template-widget.cpp
new file mode 100644
index 0000000..595e74a
--- /dev/null
+++ b/src/ui/dialog/template-widget.cpp
@@ -0,0 +1,155 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief New From Template - templates widget - implementation
+ */
+/* Authors:
+ * Jan Darowski <jan.darowski@gmail.com>, supervised by Krzysztof Kosiński
+ *
+ * Copyright (C) 2013 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "template-widget.h"
+
+#include <glibmm/miscutils.h>
+#include <gtkmm/messagedialog.h>
+
+#include "desktop.h"
+#include "document.h"
+#include "document-undo.h"
+#include "file.h"
+#include "inkscape.h"
+
+#include "extension/implementation/implementation.h"
+
+#include "object/sp-namedview.h"
+
+namespace Inkscape {
+namespace UI {
+
+
+TemplateWidget::TemplateWidget()
+ : Gtk::Box(Gtk::ORIENTATION_VERTICAL)
+ , _more_info_button(_("More info"))
+ , _short_description_label(" ")
+ , _template_name_label(_("no template selected"))
+ , _effect_prefs(nullptr)
+ , _preview_box(Gtk::ORIENTATION_HORIZONTAL)
+{
+ pack_start(_template_name_label, Gtk::PACK_SHRINK, 10);
+ pack_start(_preview_box, Gtk::PACK_SHRINK, 0);
+
+ _preview_box.pack_start(_preview_image, Gtk::PACK_EXPAND_PADDING, 15);
+ _preview_box.pack_start(_preview_render, Gtk::PACK_EXPAND_PADDING, 10);
+
+ _short_description_label.set_line_wrap(true);
+
+ _more_info_button.set_halign(Gtk::ALIGN_END);
+ _more_info_button.set_valign(Gtk::ALIGN_CENTER);
+ pack_end(_more_info_button, Gtk::PACK_SHRINK);
+
+ pack_end(_short_description_label, Gtk::PACK_SHRINK, 5);
+
+ _more_info_button.signal_clicked().connect(
+ sigc::mem_fun(*this, &TemplateWidget::_displayTemplateDetails));
+ _more_info_button.set_sensitive(false);
+}
+
+
+void TemplateWidget::create()
+{
+ if (_current_template.display_name == "")
+ return;
+
+ if (_current_template.is_procedural){
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ SPDesktop *desc = sp_file_new_default();
+ _current_template.tpl_effect->effect(desc);
+ DocumentUndo::clearUndo(desc->getDocument());
+ desc->getDocument()->setModifiedSinceSave(false);
+
+ // Apply cx,cy etc. from document
+ sp_namedview_window_from_document( desc );
+
+ if (desktop)
+ desktop->clearWaitingCursor();
+ }
+ else {
+ sp_file_new(_current_template.path);
+ }
+}
+
+
+void TemplateWidget::display(TemplateLoadTab::TemplateData data)
+{
+ clear();
+ _current_template = data;
+
+ _template_name_label.set_text(_current_template.display_name);
+ _short_description_label.set_text(_current_template.short_description);
+
+ if (data.preview_name != ""){
+ std::string imagePath = Glib::build_filename(Glib::path_get_dirname(_current_template.path),
+ Glib::filename_from_utf8(_current_template.preview_name));
+ _preview_image.set(imagePath);
+ _preview_image.show();
+ }
+ else if (!data.is_procedural){
+ Glib::ustring gPath = data.path.c_str();
+ _preview_render.showImage(gPath);
+ _preview_render.show();
+ }
+
+ if (data.is_procedural){
+ _effect_prefs = data.tpl_effect->get_imp()->prefs_effect(data.tpl_effect, SP_ACTIVE_DESKTOP, nullptr, nullptr);
+ pack_start(*_effect_prefs);
+ }
+ _more_info_button.set_sensitive(true);
+}
+
+void TemplateWidget::clear()
+{
+ _template_name_label.set_text("");
+ _short_description_label.set_text("");
+ _preview_render.hide();
+ _preview_image.hide();
+ if (_effect_prefs != nullptr){
+ remove (*_effect_prefs);
+ _effect_prefs = nullptr;
+ }
+ _more_info_button.set_sensitive(false);
+}
+
+void TemplateWidget::_displayTemplateDetails()
+{
+ Glib::ustring message = _current_template.display_name + "\n\n";
+
+ if (!_current_template.author.empty()) {
+ message += _("Author");
+ message += ": ";
+ message += _current_template.author + " " + _current_template.creation_date + "\n\n";
+ }
+
+ if (!_current_template.keywords.empty()){
+ message += _("Keywords");
+ message += ":";
+ for (const auto & keyword : _current_template.keywords) {
+ message += " ";
+ message += keyword;
+ }
+ message += "\n\n";
+ }
+
+ if (!_current_template.path.empty()) {
+ message += _("Path");
+ message += ": ";
+ message += _current_template.path;
+ message += "\n\n";
+ }
+
+ Gtk::MessageDialog dl(message, false, Gtk::MESSAGE_OTHER);
+ dl.run();
+}
+
+}
+}
diff --git a/src/ui/dialog/template-widget.h b/src/ui/dialog/template-widget.h
new file mode 100644
index 0000000..b13a3b9
--- /dev/null
+++ b/src/ui/dialog/template-widget.h
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief New From Template - template widget
+ */
+/* Authors:
+ * Jan Darowski <jan.darowski@gmail.com>, supervised by Krzysztof Kosiński
+ *
+ * Copyright (C) 2013 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_SEEN_UI_DIALOG_TEMPLATE_WIDGET_H
+#define INKSCAPE_SEEN_UI_DIALOG_TEMPLATE_WIDGET_H
+
+#include "svg-preview.h"
+
+#include <gtkmm/box.h>
+
+#include "template-load-tab.h"
+
+
+namespace Inkscape {
+namespace UI {
+
+
+class TemplateWidget : public Gtk::Box
+{
+public:
+ TemplateWidget ();
+ void create();
+ void display(TemplateLoadTab::TemplateData);
+ void clear();
+
+private:
+ TemplateLoadTab::TemplateData _current_template;
+
+ Gtk::Button _more_info_button;
+ Gtk::Box _preview_box;
+ Gtk::Image _preview_image;
+ Dialog::SVGPreview _preview_render;
+ Gtk::Label _short_description_label;
+ Gtk::Label _template_name_label;
+ Gtk::Widget *_effect_prefs;
+
+ void _displayTemplateDetails();
+};
+
+}
+}
+
+#endif
diff --git a/src/ui/dialog/text-edit.cpp b/src/ui/dialog/text-edit.cpp
new file mode 100644
index 0000000..b8d258b
--- /dev/null
+++ b/src/ui/dialog/text-edit.cpp
@@ -0,0 +1,512 @@
+// 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 "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "style.h"
+#include "text-editing.h"
+
+#include <libnrtype/FontFactory.h>
+#include <libnrtype/font-instance.h>
+#include <libnrtype/font-lister.h>
+
+#include "object/sp-flowtext.h"
+#include "object/sp-text.h"
+#include "object/sp-textpath.h"
+
+#include "io/resource.h"
+#include "svg/css-ostringstream.h"
+#include "ui/icon-names.h"
+#include "ui/toolbar/text-toolbar.h"
+#include "ui/widget/font-selector.h"
+
+#include "util/units.h"
+
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+TextEdit::TextEdit()
+ : DialogBase("/dialogs/textandfont", "Text"),
+ selectChangedConn(),
+ subselChangedConn(),
+ selectModifiedConn(),
+ blocked(false),
+ /*
+ TRANSLATORS: Test string used in text and font dialog (when no
+ * text has been entered) to get a preview of the font. Choose
+ * some representative characters that users of your locale will be
+ * interested in.*/
+ samplephrase(_("AaBbCcIiPpQq12369$\342\202\254\302\242?.;/()"))
+{
+
+ 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;
+ }
+
+ 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);
+
+ 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, 0);
+ feat_box->pack_start(font_features, true, true);
+ feat_box->reorder_child(font_features, 0);
+
+#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_buffer->signal_changed().connect(sigc::mem_fun(*this, &TextEdit::onChange));
+ setasdefault_button->signal_clicked().connect(sigc::mem_fun(*this, &TextEdit::onSetDefault));
+ apply_button->signal_clicked().connect(sigc::mem_fun(*this, &TextEdit::onApply));
+ fontChangedConn = font_selector.connectChanged(sigc::mem_fun(*this, &TextEdit::onFontChange));
+ fontFeaturesChangedConn = font_features.connectChanged(sigc::mem_fun(*this, &TextEdit::onChange));
+ notebook->signal_switch_page().connect(sigc::mem_fun(*this, &TextEdit::onFontFeatures));
+
+ font_selector.set_name("TextEdit");
+
+ show_all_children();
+}
+
+TextEdit::~TextEdit()
+{
+ selectModifiedConn.disconnect();
+ subselChangedConn.disconnect();
+ selectChangedConn.disconnect();
+ fontChangedConn.disconnect();
+ fontFeaturesChangedConn.disconnect();
+}
+
+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 (SP_IS_TEXT(*i) || SP_IS_FLOWTEXT(*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 (SP_IS_TEXT(*i) || SP_IS_FLOWTEXT(*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 (SP_IS_TEXT (*i) || (SP_IS_FLOWTEXT (*i)) ) {
+ ++items;
+ }
+ }
+ if (items == 1) {
+ double factor = font_selector.get_fontsize() / selected_fontsize;
+ prefs->setDouble("/options/font/scaleLineHeightFromFontSIze", factor);
+ }
+ sp_desktop_set_style(desktop, css, true);
+
+ if (items == 0) {
+ // no text objects; apply style to prefs for new objects
+ prefs->mergeStyle("/tools/text/style", css);
+ setasdefault_button->set_sensitive ( false );
+
+ } else if (items == 1) {
+ // exactly one text object; now set its text, too
+ SPItem *item = desktop->getSelection()->singleItem();
+ if (SP_IS_TEXT (item) || SP_IS_FLOWTEXT(item)) {
+ updateObjectText (item);
+ SPStyle *item_style = item->style;
+ if (SP_IS_TEXT(item) && item_style->inline_size.value == 0) {
+ css = sp_css_attr_from_style(item_style, SP_STYLE_FLAG_IFSET);
+ sp_repr_css_unset_property(css, "inline-size");
+ item->changeCSS(css, "style");
+ }
+ }
+ }
+
+ // Update FontLister
+ Glib::ustring fontspec = font_selector.get_fontspec();
+ if( !fontspec.empty() ) {
+ Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance();
+ fontlister->set_fontspec( fontspec, false );
+ }
+
+ // complete the transaction
+ DocumentUndo::done(desktop->getDocument(), _("Set text style"), INKSCAPE_ICON("draw-text"));
+ apply_button->set_sensitive ( false );
+
+ sp_repr_css_attr_unref (css);
+
+ Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+ font_lister->update_font_list(desktop->getDocument());
+
+ blocked = false;
+}
+
+void TextEdit::onFontFeatures(Gtk::Widget * widgt, int pos)
+{
+ if (pos == 1) {
+ Glib::ustring fontspec = font_selector.get_fontspec();
+ if (!fontspec.empty()) {
+ font_instance *res = font_factory::Default()->FaceFromFontSpecification(fontspec.c_str());
+ if (res && !res->fulloaded) {
+ res->InitTheFace(true);
+ font_features.update_opentype(fontspec);
+ }
+ }
+ }
+}
+
+void TextEdit::onChange()
+{
+ if (blocked) {
+ return;
+ }
+
+ 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..72b8589
--- /dev/null
+++ b/src/ui/dialog/text-edit.h
@@ -0,0 +1,194 @@
+// 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 "ui/dialog/dialog-base.h"
+#include "ui/widget/frame.h"
+
+#include "ui/widget/font-selector.h"
+#include "ui/widget/font-variants.h"
+
+namespace Gtk {
+class Box;
+class Button;
+class ButtonBox;
+class Label;
+class Notebook;
+class TextBuffer;
+class TextView;
+}
+
+class SPItem;
+class font_instance;
+class SPCSSAttr;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+#define VB_MARGIN 4
+/**
+ * The TextEdit class defines the Text and font dialog.
+ *
+ * The Text and font dialog allows you to set the font family, style and size
+ * and shows a preview of the result. The dialogs layout settings include
+ * horizontal and vertical alignment and inter line distance.
+ */
+class TextEdit : public DialogBase
+{
+public:
+ TextEdit();
+ ~TextEdit() override;
+
+ void documentReplaced() override;
+ void selectionChanged(Selection *selection) override;
+ void selectionModified(Selection *selection, guint flags) override;
+
+ /**
+ * Helper function which returns a new instance of the dialog.
+ * getInstance is needed by the dialog manager (Inkscape::UI::Dialog::DialogManager).
+ */
+ static TextEdit &getInstance() { return *new TextEdit(); }
+
+protected:
+
+ /**
+ * Callback for pressing the default button.
+ */
+ void onSetDefault ();
+
+ /**
+ * Callback for pressing the apply button.
+ */
+ void onApply ();
+
+ /**
+ * Called whenever something 'changes' on canvas.
+ *
+ * onReadSelection gets the currently selected item from the canvas and sets all the controls in this dialog to the correct state.
+ *
+ * @param dostyle Indicates whether the modification of the user includes a style change.
+ * @param content Indicates whether the modification of the user includes a style change. Actually refers to the question if we do want to show the content? (Parameter currently not used)
+ */
+ void onReadSelection (gboolean style, gboolean content);
+
+ /**
+ * Callback invoked when the user modifies the text of the selected text object.
+ *
+ * onTextChange is responsible for initiating the commands after the user
+ * modified the text in the selected object. The UI of the dialog is
+ * updated. The subfunction setPreviewText updates the preview label.
+ *
+ * @param self pointer to the current instance of the dialog.
+ */
+ void onChange ();
+ void onFontFeatures (Gtk::Widget * widgt, int pos);
+
+ /**
+ * 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 ---------------------- //
+ 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;
+
+ // Other
+ double selected_fontsize;
+ bool blocked;
+ const Glib::ustring samplephrase;
+
+
+ TextEdit(TextEdit const &d) = delete;
+ TextEdit operator=(TextEdit const &d) = delete;
+};
+
+} //namespace Dialog
+} //namespace UI
+} //namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_TEXT_EDIT_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/tile.cpp b/src/ui/dialog/tile.cpp
new file mode 100644
index 0000000..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..484c3a0
--- /dev/null
+++ b/src/ui/dialog/tile.h
@@ -0,0 +1,83 @@
+// 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
+{
+private:
+ Gtk::Box *_arrangeBox;
+ Gtk::Notebook *_notebook;
+ AlignAndDistribute* _align_tab;
+ GridArrangeTab *_gridArrangeTab;
+ PolarArrangeTab *_polarArrangeTab;
+ Gtk::Button *_arrangeButton;
+
+public:
+ ArrangeDialog();
+ ~ArrangeDialog() override;
+
+ void desktopReplaced() override;
+
+ void update_arrange_btn();
+
+ /**
+ * Callback from Apply
+ */
+ void _apply();
+
+ static ArrangeDialog& getInstance() { return *new ArrangeDialog(); }
+};
+
+} //namespace Dialog
+} //namespace UI
+} //namespace Inkscape
+
+
+#endif /* __TILEDIALOG_H__ */
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/tracedialog.cpp b/src/ui/dialog/tracedialog.cpp
new file mode 100644
index 0000000..114e341
--- /dev/null
+++ b/src/ui/dialog/tracedialog.cpp
@@ -0,0 +1,485 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Bitmap tracing settings dialog - second implementation.
+ */
+/* Authors:
+ * Marc Jeanmougin <marc.jeanmougin@telecom-paristech.fr>
+ *
+ * Copyright (C) 2019 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "tracedialog.h"
+
+#include <glibmm/i18n.h>
+#include <gtkmm.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/spinbutton.h>
+#include <gtkmm/stack.h>
+
+#include "desktop.h"
+#include "display/cairo-utils.h"
+#include "inkscape.h"
+#include "io/resource.h"
+#include "io/sys.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::TRACE_BRIGHTNESS},
+ {"SS_ED", Inkscape::Trace::Potrace::TRACE_CANNY},
+ {"SS_CQ", Inkscape::Trace::Potrace::TRACE_QUANT},
+ {"SS_AT", Inkscape::Trace::Potrace::AUTOTRACE_SINGLE},
+ {"SS_CT", Inkscape::Trace::Potrace::AUTOTRACE_CENTERLINE},
+
+ {"MS_BS", Inkscape::Trace::Potrace::TRACE_BRIGHTNESS_MULTI},
+ {"MS_C", Inkscape::Trace::Potrace::TRACE_QUANT_COLOR},
+ {"MS_BW", Inkscape::Trace::Potrace::TRACE_QUANT_MONO},
+ {"MS_AT", Inkscape::Trace::Potrace::AUTOTRACE_MULTI},
+};
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class TraceDialogImpl2 : public TraceDialog {
+ public:
+ TraceDialogImpl2();
+ ~TraceDialogImpl2() override;
+
+ void selectionModified(Selection *selection, guint flags) override;
+ void selectionChanged(Inkscape::Selection *selection) override;
+private:
+ Inkscape::Trace::Tracer tracer;
+ void traceProcess(bool do_i_trace);
+ void abort();
+
+ void previewCallback(bool force);
+ bool previewResize(const Cairo::RefPtr<Cairo::Context>&);
+ void traceCallback();
+ void onSetDefaults();
+ void show_hide_params();
+ void schedule_preview_update();
+ static gboolean update_cb(gpointer user_data);
+
+ 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;
+ Glib::RefPtr<Gdk::Pixbuf> scaledPreview;
+ Gtk::DrawingArea *previewArea;
+ Gtk::Box* orient_box;
+ Gtk::Frame* _preview_frame;
+ Gtk::Grid* _param_grid;
+ Gtk::CheckButton* _live_preview;
+ guint _source = 0;
+};
+
+enum Page {
+ SingleScan, MultiScan, PixelArt
+};
+
+void TraceDialogImpl2::traceProcess(bool do_i_trace)
+{
+ SPDesktop* desktop = getDesktop();
+ if (desktop)
+ desktop->setWaitingCursor();
+
+ auto current_page = choice_tab->get_current_page();
+
+ auto cb_siox = current_page == SingleScan ? CB_SIOX : CB_SIOX1;
+ if (cb_siox->get_active())
+ tracer.enableSiox(true);
+ else
+ tracer.enableSiox(false);
+
+ Glib::ustring type = current_page == SingleScan ? CBT_SS->get_active_id() : CBT_MS->get_active_id();
+
+ bool use_autotrace = false;
+ Inkscape::Trace::Autotrace::AutotraceTracingEngine ate; // TODO
+
+ auto potraceType = trace_types.find(type);
+ assert(potraceType != trace_types.end());
+ switch (potraceType->second) {
+ case Inkscape::Trace::Potrace::AUTOTRACE_SINGLE:
+ use_autotrace = true;
+ ate.opts->color_count = 2;
+ break;
+ case Inkscape::Trace::Potrace::AUTOTRACE_CENTERLINE:
+ use_autotrace = true;
+ ate.opts->color_count = 2;
+ ate.opts->centerline = true;
+ ate.opts->preserve_width = true;
+ break;
+ case Inkscape::Trace::Potrace::AUTOTRACE_MULTI:
+ use_autotrace = true;
+ ate.opts->color_count = (int)MS_scans->get_value() + 1;
+ break;
+ default:
+ break;
+ }
+
+ ate.opts->filter_iterations = (int) SS_AT_FI_T->get_value();
+ ate.opts->error_threshold = SS_AT_ET_T->get_value();
+
+ Inkscape::Trace::Potrace::PotraceTracingEngine pte(
+ potraceType->second, 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 == SingleScan ? CB_optimize : CB_optimize1;
+ pte.potraceParams->opticurve = cb_optimize->get_active();
+ pte.potraceParams->opttolerance = optimize->get_value();
+
+ auto cb_smooth = current_page == SingleScan ? CB_smooth : CB_smooth1;
+ pte.potraceParams->alphamax = cb_smooth->get_active() ? smooth->get_value() : 0;
+
+ auto cb_speckles = current_page == SingleScan ? CB_speckles : CB_speckles1;
+ pte.potraceParams->turdsize = cb_speckles->get_active() ? (int)speckles->get_value() : 0;
+
+ //Inkscape::Trace::Autotrace::AutotraceTracingEngine ate; // TODO
+ Inkscape::Trace::Depixelize::DepixelizeTracingEngine dte(
+ RB_PA_voronoi->get_active() ? Inkscape::Trace::Depixelize::TraceType::TRACE_VORONOI : Inkscape::Trace::Depixelize::TraceType::TRACE_BSPLINES,
+ PA_curves->get_value(), (int) PA_islands->get_value(),
+ (int) PA_sparse1->get_value(), PA_sparse2->get_value(),
+ CB_PA_optimize->get_active());
+
+ //TODO: preview for multiscan: grayscale bitmap with matching number of gray levels?
+ // Currently there's one created using brightness threshold, which is wrong and misleading
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf = tracer.getSelectedImage();
+ if (pixbuf) {
+ scaledPreview = use_autotrace ? ate.preview(pixbuf) : pte.preview(pixbuf);
+ }
+ else {
+ scaledPreview.reset();
+ }
+
+ previewArea->queue_draw();
+
+ if (do_i_trace){
+ if (current_page == PixelArt){
+ tracer.trace(&dte);
+ printf("dt\n");
+ } else if (use_autotrace) {
+ tracer.trace(&ate);
+ printf("at\n");
+ } else if (current_page == SingleScan || current_page == MultiScan) {
+ tracer.trace(&pte);
+ printf("pt\n");
+ }
+ }
+
+ if (desktop)
+ desktop->clearWaitingCursor();
+}
+
+bool TraceDialogImpl2::previewResize(const Cairo::RefPtr<Cairo::Context>& cr)
+{
+ /* Checkerboard - is not applicable here; left for reference
+ if (auto wnd = dynamic_cast<Gtk::Window*>(this->get_toplevel())) {
+ auto color = wnd->get_style_context()->get_background_color();
+ auto background =
+ gint32(0xff * color.get_red()) << 24 |
+ gint32(0xff * color.get_green()) << 16 |
+ gint32(0xff * color.get_blue()) << 8 |
+ 0xff;
+
+ auto device_scale = get_scale_factor();
+ Cairo::RefPtr<Cairo::Pattern> pattern(new Cairo::Pattern(ink_cairo_pattern_create_checkerboard(background)));
+ cr->save();
+ cr->scale(device_scale, device_scale);
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->set_source(pattern);
+ cr->paint();
+ cr->restore();
+ } */
+
+ if (scaledPreview) {
+ int width = scaledPreview->get_width();
+ int height = scaledPreview->get_height();
+ const Gtk::Allocation &vboxAlloc = previewArea->get_allocation();
+ double scaleFX = vboxAlloc.get_width() / (double)width;
+ double scaleFY = vboxAlloc.get_height() / (double)height;
+ double scaleFactor = scaleFX > scaleFY ? scaleFY : scaleFX;
+ int newWidth = (int)(((double)width) * scaleFactor);
+ int newHeight = (int)(((double)height) * scaleFactor);
+ int offsetX = (vboxAlloc.get_width() - newWidth)/2;
+ int offsetY = (vboxAlloc.get_height() - newHeight)/2;
+ cr->scale(scaleFactor, scaleFactor);
+ Gdk::Cairo::set_source_pixbuf(cr, scaledPreview, offsetX / scaleFactor, offsetY / scaleFactor);
+ cr->paint();
+ }
+ else {
+ cr->set_source_rgba(0, 0, 0, 0);
+ cr->paint();
+ }
+
+ return false;
+}
+
+void TraceDialogImpl2::abort()
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if (desktop)
+ desktop->clearWaitingCursor();
+ tracer.abort();
+}
+
+void TraceDialogImpl2::selectionChanged(Inkscape::Selection *selection) {
+ // refresh preview when selecting or deselecting images (in general: when selection changes)
+ previewCallback(false);
+}
+
+void TraceDialogImpl2::selectionModified(Selection *selection, guint flags) {
+ // Note: original refresh condition commended out, as selection modified fires when moving images around slowing on-canvas operations.
+ // Note: is there a use-case where preview needs to be refreshed when images are manipulated? I haven't found one.
+
+// if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_PARENT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG)) {
+
+ // this condition is satisfied when imported image gets places on canvas; this is actually wrong
+ // and there should just be "selection changed", but there isn't
+ auto mask = SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_PARENT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG;
+ if ((flags & mask) == mask) {
+ previewCallback(false);
+ }
+}
+
+void TraceDialogImpl2::onSetDefaults()
+{
+ MS_scans->set_value(8);
+ PA_curves->set_value(1);
+ PA_islands->set_value(5);
+ PA_sparse1->set_value(4);
+ PA_sparse2->set_value(1);
+ SS_AT_FI_T->set_value(4);
+ SS_AT_ET_T->set_value(2);
+ SS_BC_T->set_value(0.45);
+ SS_CQ_T->set_value(64);
+ SS_ED_T->set_value(.65);
+ optimize->set_value(0.2);
+ smooth->set_value(1);
+ speckles->set_value(2);
+ CB_invert->set_active(false);
+ CB_MS_smooth->set_active(true);
+ CB_MS_stack->set_active(true);
+ CB_MS_rb->set_active(false);
+ CB_speckles->set_active(true);
+ CB_smooth->set_active(true);
+ CB_optimize->set_active(true);
+ CB_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 TraceDialogImpl2::previewCallback(bool force) {
+ if (force || (_live_preview->get_active() && is_widget_effectively_visible(this))) {
+ traceProcess(false);
+ }
+}
+
+void TraceDialogImpl2::traceCallback() { traceProcess(true); }
+
+
+TraceDialogImpl2::TraceDialogImpl2()
+ : TraceDialog()
+{
+ const std::string req_widgets[] = { "MS_scans", "PA_curves", "PA_islands", "PA_sparse1", "PA_sparse2",
+ "SS_AT_FI_T", "SS_AT_ET_T", "SS_BC_T", "SS_CQ_T", "SS_ED_T",
+ "optimize", "smooth", "speckles", "CB_invert", "CB_MS_smooth",
+ "CB_MS_stack", "CB_MS_rb", "CB_speckles", "CB_smooth", "CB_optimize",
+ "CB_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" };
+ auto gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "dialog-trace.glade");
+ try {
+ builder = Gtk::Builder::create_from_file(gladefile);
+ } catch (const Glib::Error &ex) {
+ g_warning("Glade file loading failed for filter effect dialog");
+ return;
+ }
+
+ Glib::RefPtr<Glib::Object> test;
+ for (std::string w : req_widgets) {
+ test = builder->get_object(w);
+ if (!test) {
+ g_warning("Required widget %s does not exist", w.c_str());
+ return;
+ }
+ }
+
+#define GET_O(name) \
+ tmp = builder->get_object(#name); \
+ name = Glib::RefPtr<Gtk::Adjustment>::cast_dynamic(tmp);
+
+ Glib::RefPtr<Glib::Object> tmp;
+
+#define GET_W(name) builder->get_widget(#name, name);
+ GET_O(MS_scans)
+ GET_O(PA_curves)
+ GET_O(PA_islands)
+ GET_O(PA_sparse1)
+ GET_O(PA_sparse2)
+ GET_O(SS_AT_FI_T)
+ GET_O(SS_AT_ET_T)
+ GET_O(SS_BC_T)
+ GET_O(SS_CQ_T)
+ GET_O(SS_ED_T)
+ GET_O(optimize)
+ GET_O(smooth)
+ GET_O(speckles)
+
+ GET_W(CB_invert)
+ GET_W(CB_MS_smooth)
+ GET_W(CB_MS_stack)
+ GET_W(CB_MS_rb)
+ GET_W(CB_speckles)
+ GET_W(CB_smooth)
+ GET_W(CB_optimize)
+ GET_W(CB_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)
+#undef GET_W
+#undef GET_O
+ add(*mainBox);
+
+ Inkscape::Preferences* prefs = Inkscape::Preferences::get();
+
+ _live_preview->set_active(prefs->getBool(getPrefsPath() + "liveUpdate", true));
+
+ B_Update->signal_clicked().connect([=](){ previewCallback(true); });
+ B_OK->signal_clicked().connect(sigc::mem_fun(*this, &TraceDialogImpl2::traceCallback));
+ B_STOP->signal_clicked().connect(sigc::mem_fun(*this, &TraceDialogImpl2::abort));
+ B_RESET->signal_clicked().connect(sigc::mem_fun(*this, &TraceDialogImpl2::onSetDefaults));
+ previewArea->signal_draw().connect(sigc::mem_fun(*this, &TraceDialogImpl2::previewResize));
+
+ // attempt at making UI responsive: relocate preview to the right or bottom of dialog depending on dialog size
+ this->signal_size_allocate().connect([=](const Gtk::Allocation& alloc){
+ // skip bogus sizes
+ if (alloc.get_width() < 10 || alloc.get_height() < 10) return;
+ // ratio: is dialog wide or is it tall?
+ double 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);
+ const auto 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([=](){ show_hide_params(); });
+ show_hide_params();
+
+ // 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([=](){ schedule_preview_update(); });
+ }
+ 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([=](){ schedule_preview_update(); });
+ }
+ for (auto combo : {CBT_SS, CBT_MS}) {
+ combo->signal_changed().connect([=](){ schedule_preview_update(); });
+ }
+ choice_tab->signal_switch_page().connect([=](Gtk::Widget*, guint){ schedule_preview_update(); });
+
+ signal_set_focus_child().connect([=](Gtk::Widget* w){
+ if (w) schedule_preview_update();
+ });
+}
+
+TraceDialogImpl2::~TraceDialogImpl2() {
+ Inkscape::Preferences* prefs = Inkscape::Preferences::get();
+ prefs->setBool(getPrefsPath() + "liveUpdate", _live_preview->get_active());
+
+ if (_source) {
+ g_source_destroy(g_main_context_find_source_by_id(nullptr, _source));
+ }
+}
+
+gboolean TraceDialogImpl2::update_cb(gpointer user_data) {
+ auto self = static_cast<TraceDialogImpl2*>(user_data);
+ self->previewCallback(false);
+ self->_source = 0;
+ return FALSE;
+}
+
+void TraceDialogImpl2::schedule_preview_update() {
+ if (!_live_preview->get_active() || _source) return;
+
+ _source = g_idle_add(&TraceDialogImpl2::update_cb, this);
+}
+
+void TraceDialogImpl2::show_hide_params() {
+ int start_row = 2;
+ int option = CBT_SS->get_active_row_number();
+ if (option >= 3) option = 3;
+ int show1 = start_row + option;
+ int show2 = start_row + option;
+ 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();
+ }
+ }
+ }
+}
+
+TraceDialog &TraceDialog::getInstance()
+{
+ TraceDialog *dialog = new TraceDialogImpl2();
+ return *dialog;
+}
+
+
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
diff --git a/src/ui/dialog/tracedialog.h b/src/ui/dialog/tracedialog.h
new file mode 100644
index 0000000..92c9e59
--- /dev/null
+++ b/src/ui/dialog/tracedialog.h
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Bitmap tracing settings dialog
+ */
+/* Authors:
+ * Bob Jamison
+ * Other dudes from The Inkscape Organization
+ *
+ * Copyright (C) 2004, 2005 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef __TRACEDIALOG_H__
+#define __TRACEDIALOG_H__
+
+#include "ui/dialog/dialog-base.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+
+/**
+ * A dialog that displays log messages
+ */
+class TraceDialog : public DialogBase
+{
+
+public:
+
+ /**
+ * Constructor
+ */
+ TraceDialog() : DialogBase("/dialogs/trace", "Trace") {}
+
+
+ /**
+ * Factory method
+ */
+ static TraceDialog &getInstance();
+
+ /**
+ * Destructor
+ */
+ ~TraceDialog() override = default;;
+
+
+};
+
+
+} //namespace Dialog
+} //namespace UI
+} //namespace Inkscape
+
+#endif /* __TRACEDIALOG_H__ */
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/transformation.cpp b/src/ui/dialog/transformation.cpp
new file mode 100644
index 0000000..51ca959
--- /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..77210d9
--- /dev/null
+++ b/src/ui/dialog/transformation.h
@@ -0,0 +1,259 @@
+// 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;
+
+ /**
+ * Factory method. Create an instance of this class/interface
+ */
+ static Transformation &getInstance()
+ { return *new Transformation(); }
+
+
+ /**
+ * Show the Move panel
+ */
+ void setPageMove()
+ { presentPage(PAGE_MOVE); }
+
+
+ /**
+ * Show the Scale panel
+ */
+ void setPageScale()
+ { presentPage(PAGE_SCALE); }
+
+
+ /**
+ * Show the Rotate panel
+ */
+ void setPageRotate()
+ { presentPage(PAGE_ROTATE); }
+
+ /**
+ * Show the Skew panel
+ */
+ void setPageSkew()
+ { presentPage(PAGE_SKEW); }
+
+ /**
+ * Show the Transform panel
+ */
+ void setPageTransform()
+ { presentPage(PAGE_TRANSFORM); }
+
+
+ int getCurrentPage()
+ { return _notebook.get_current_page(); }
+
+ enum PageType {
+ PAGE_MOVE, PAGE_SCALE, PAGE_ROTATE, PAGE_SKEW, PAGE_TRANSFORM, PAGE_QTY
+ };
+
+ void 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:
+
+ /**
+ * Copy constructor
+ */
+ Transformation(Transformation const &d) = delete;
+
+ /**
+ * Assignment operator
+ */
+ Transformation operator=(Transformation const &d) = delete;
+
+ 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..0e617a3
--- /dev/null
+++ b/src/ui/dialog/undo-history.cpp
@@ -0,0 +1,386 @@
+// 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::getInstance()
+{
+ return *new UndoHistory();
+}
+
+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.
+ */
+ // this fix crashes on redo with knots forcing regenerate knots on undo
+ SPDesktop *dt = getDesktop();
+ Glib::ustring switch_selector_to = "";
+ if (dt) {
+ switch_selector_to = get_active_tool(dt);
+ if (switch_selector_to != "Select") {
+ set_active_tool(dt, "Select");
+ }
+ }
+ 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 ( 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();
+ }
+ if (dt && switch_selector_to != "Select") {
+ set_active_tool(dt, switch_selector_to);
+ }
+}
+
+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() ) {
+ // this fix crashes on redo with knots forcing regenerate knots on undo
+ SPDesktop *dt = getDesktop();
+ Glib::ustring switch_selector_to = "";
+ if (dt) {
+ switch_selector_to = get_active_tool(dt);
+ if (switch_selector_to != "Select") {
+ set_active_tool(dt, "Select");
+ }
+ }
+ 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);
+ if (dt && switch_selector_to != "Select") {
+ set_active_tool(dt, switch_selector_to);
+ }
+ }
+}
+
+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..0b69693
--- /dev/null
+++ b/src/ui/dialog/undo-history.h
@@ -0,0 +1,165 @@
+// 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 : std::unary_function<int, bool> {
+ virtual ~Filter() = default;
+ virtual bool operator() (const int&) const =0;
+ };
+
+ CellRendererInt(const Filter& filter=no_filter) :
+ Glib::ObjectBase(typeid(CellRendererText)),
+ Gtk::CellRendererText(),
+ _property_number(*this, "number", 0),
+ _filter (filter)
+ { }
+
+
+ Glib::PropertyProxy<int>
+ property_number() { return _property_number.get_proxy(); }
+
+ static const Filter& no_filter;
+
+protected:
+ void render_vfunc(const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags) override;
+
+private:
+
+ Glib::Property<int> _property_number;
+ const Filter& _filter;
+
+ struct NoFilter : Filter { bool operator() (const int& /*x*/) const override { return true; } };
+};
+
+/**
+ * \brief Dialog for presenting document change history
+ *
+ * This dialog allows the user to undo and redo multiple events in a more convenient way
+ * than repateaded ctrl-z, ctrl-shift-z.
+ */
+class UndoHistory : public DialogBase
+{
+public:
+ ~UndoHistory() override;
+
+ static UndoHistory &getInstance();
+ 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:
+ UndoHistory();
+
+ // no default constructor, noncopyable, nonassignable
+ UndoHistory(UndoHistory const &d) = delete;
+ UndoHistory operator=(UndoHistory const &d) = delete;
+
+ struct GreaterThan : CellRendererInt::Filter {
+ GreaterThan(int _i) : i (_i) {}
+ bool operator() (const int& x) const override { return x > i; }
+ int i;
+ };
+
+ static const CellRendererInt::Filter& greater_than_1;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif //INKSCAPE_UI_DIALOG_UNDO_HISTORY_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/xml-tree.cpp b/src/ui/dialog/xml-tree.cpp
new file mode 100644
index 0000000..1c82a11
--- /dev/null
+++ b/src/ui/dialog/xml-tree.cpp
@@ -0,0 +1,952 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * XML editor.
+ */
+/* Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * MenTaLguY <mental@rydia.net>
+ * bulia byak <buliabyak@users.sf.net>
+ * Johan Engelen <goejendaagh@zonnet.nl>
+ * David Turner
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 1999-2006 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ *
+ */
+
+#include "xml-tree.h"
+
+#include <glibmm/i18n.h>
+#include <memory>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "layer-manager.h"
+#include "message-context.h"
+#include "message-stack.h"
+
+#include "object/sp-root.h"
+#include "object/sp-string.h"
+
+#include "ui/dialog-events.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "ui/tools/tool-base.h"
+
+#include "widgets/sp-xmlview-tree.h"
+
+namespace {
+/**
+ * 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)
+{
+ paned.child_property_resize(*paned.get_child1()) = vertical;
+ assert(paned.child_property_resize(*paned.get_child2()));
+ paned.set_orientation(vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL);
+}
+} // namespace
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+XmlTree::XmlTree()
+ : DialogBase("/dialogs/xml/", "XMLEditor")
+ , blocked(0)
+ , _message_stack(nullptr)
+ , _message_context(nullptr)
+ , selected_attr(0)
+ , selected_repr(nullptr)
+ , tree(nullptr)
+ , status("")
+ , new_window(nullptr)
+ , _updating(false)
+ , node_box(Gtk::ORIENTATION_VERTICAL)
+ , status_box(Gtk::ORIENTATION_HORIZONTAL)
+{
+ Gtk::Box *contents = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
+ status.set_halign(Gtk::ALIGN_START);
+ status.set_valign(Gtk::ALIGN_CENTER);
+ status.set_size_request(1, -1);
+ status.set_markup("");
+ status.set_line_wrap(true);
+ status.get_style_context()->add_class("inksmall");
+ status_box.pack_start( status, TRUE, TRUE, 0);
+ contents->pack_start(_paned, true, true, 0);
+ contents->set_valign(Gtk::ALIGN_FILL);
+ contents->child_property_fill(_paned);
+
+ _paned.set_vexpand(true);
+ _message_stack = std::make_shared<Inkscape::MessageStack>();
+ _message_context = std::unique_ptr<Inkscape::MessageContext>(new Inkscape::MessageContext(_message_stack));
+ _message_changed_connection = _message_stack->connectChanged(
+ sigc::bind(sigc::ptr_fun(_set_status_message), GTK_WIDGET(status.gobj())));
+
+ /* tree view */
+ tree = SP_XMLVIEW_TREE(sp_xmlview_tree_new(nullptr, nullptr, nullptr));
+ gtk_widget_set_tooltip_text( GTK_WIDGET(tree), _("Drag to reorder nodes") );
+
+ tree_toolbar.set_toolbar_style(Gtk::TOOLBAR_ICONS);
+
+ auto xml_element_new_icon = Gtk::manage(sp_get_icon_image("xml-element-new", Gtk::ICON_SIZE_LARGE_TOOLBAR));
+
+ xml_element_new_button.set_icon_widget(*xml_element_new_icon);
+ xml_element_new_button.set_label(_("New element node"));
+ xml_element_new_button.set_tooltip_text(_("New element node"));
+ xml_element_new_button.set_sensitive(false);
+ tree_toolbar.add(xml_element_new_button);
+
+ auto xml_text_new_icon = Gtk::manage(sp_get_icon_image("xml-text-new", Gtk::ICON_SIZE_LARGE_TOOLBAR));
+
+ xml_text_new_button.set_icon_widget(*xml_text_new_icon);
+ xml_text_new_button.set_label(_("New text node"));
+ xml_text_new_button.set_tooltip_text(_("New text node"));
+ xml_text_new_button.set_sensitive(false);
+ tree_toolbar.add(xml_text_new_button);
+
+ auto xml_node_duplicate_icon = Gtk::manage(sp_get_icon_image("xml-node-duplicate", Gtk::ICON_SIZE_LARGE_TOOLBAR));
+
+ xml_node_duplicate_button.set_icon_widget(*xml_node_duplicate_icon);
+ xml_node_duplicate_button.set_label(_("Duplicate node"));
+ xml_node_duplicate_button.set_tooltip_text(_("Duplicate node"));
+ xml_node_duplicate_button.set_sensitive(false);
+ tree_toolbar.add(xml_node_duplicate_button);
+
+ tree_toolbar.add(separator);
+
+ auto xml_node_delete_icon = Gtk::manage(sp_get_icon_image("xml-node-delete", Gtk::ICON_SIZE_LARGE_TOOLBAR));
+
+ xml_node_delete_button.set_icon_widget(*xml_node_delete_icon);
+ xml_node_delete_button.set_label(_("Delete node"));
+ xml_node_delete_button.set_tooltip_text(_("Delete node"));
+ xml_node_delete_button.set_sensitive(false);
+ tree_toolbar.add(xml_node_delete_button);
+
+ tree_toolbar.add(separator2);
+
+ auto format_indent_less_icon = Gtk::manage(sp_get_icon_image("format-indent-less", Gtk::ICON_SIZE_LARGE_TOOLBAR));
+
+ unindent_node_button.set_icon_widget(*format_indent_less_icon);
+ unindent_node_button.set_label(_("Unindent node"));
+ unindent_node_button.set_tooltip_text(_("Unindent node"));
+ unindent_node_button.set_sensitive(false);
+ tree_toolbar.add(unindent_node_button);
+
+ auto format_indent_more_icon = Gtk::manage(sp_get_icon_image("format-indent-more", Gtk::ICON_SIZE_LARGE_TOOLBAR));
+
+ indent_node_button.set_icon_widget(*format_indent_more_icon);
+ indent_node_button.set_label(_("Indent node"));
+ indent_node_button.set_tooltip_text(_("Indent node"));
+ indent_node_button.set_sensitive(false);
+ tree_toolbar.add(indent_node_button);
+
+ auto go_up_icon = Gtk::manage(sp_get_icon_image("go-up", Gtk::ICON_SIZE_LARGE_TOOLBAR));
+
+ raise_node_button.set_icon_widget(*go_up_icon);
+ raise_node_button.set_label(_("Raise node"));
+ raise_node_button.set_tooltip_text(_("Raise node"));
+ raise_node_button.set_sensitive(false);
+ tree_toolbar.add(raise_node_button);
+
+ auto go_down_icon = Gtk::manage(sp_get_icon_image("go-down", Gtk::ICON_SIZE_LARGE_TOOLBAR));
+
+ lower_node_button.set_icon_widget(*go_down_icon);
+ lower_node_button.set_label(_("Lower node"));
+ lower_node_button.set_tooltip_text(_("Lower node"));
+ lower_node_button.set_sensitive(false);
+ tree_toolbar.add(lower_node_button);
+
+ node_box.pack_start(tree_toolbar, FALSE, TRUE, 0);
+
+ Gtk::ScrolledWindow *tree_scroller = new Gtk::ScrolledWindow();
+ tree_scroller->set_overlay_scrolling(false);
+ tree_scroller->set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC );
+ tree_scroller->set_shadow_type(Gtk::SHADOW_IN);
+ tree_scroller->add(*Gtk::manage(Glib::wrap(GTK_WIDGET(tree))));
+ fix_inner_scroll(tree_scroller);
+
+ node_box.pack_start(*Gtk::manage(tree_scroller));
+
+ node_box.pack_end(status_box, false, false, 2);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool attrtoggler = prefs->getBool("/dialogs/xml/attrtoggler", true);
+ bool dir = prefs->getBool("/dialogs/xml/vertical", true);
+ attributes = new AttrDialog();
+ _paned.set_wide_handle(true);
+ _paned.pack1(node_box, false, false);
+ /* attributes */
+ Gtk::Box *actionsbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
+ actionsbox->set_valign(Gtk::ALIGN_START);
+ Gtk::Label *attrtogglerlabel = Gtk::manage(new Gtk::Label(_("Show attributes")));
+ attrtogglerlabel->set_margin_end(5);
+ _attrswitch.get_style_context()->add_class("inkswitch");
+ _attrswitch.get_style_context()->add_class("rawstyle");
+ _attrswitch.property_active() = attrtoggler;
+ _attrswitch.property_active().signal_changed().connect(sigc::mem_fun(*this, &XmlTree::_attrtoggler));
+ attrtogglerlabel->get_style_context()->add_class("inksmall");
+ actionsbox->pack_start(*attrtogglerlabel, Gtk::PACK_SHRINK);
+ actionsbox->pack_start(_attrswitch, Gtk::PACK_SHRINK);
+ Gtk::RadioButton::Group group;
+ Gtk::RadioButton *_horizontal = Gtk::manage(new Gtk::RadioButton());
+ Gtk::RadioButton *_vertical = Gtk::manage(new Gtk::RadioButton());
+ _horizontal->set_image_from_icon_name(INKSCAPE_ICON("horizontal"));
+ _vertical->set_image_from_icon_name(INKSCAPE_ICON("vertical"));
+ _horizontal->set_group(group);
+ _vertical->set_group(group);
+ _vertical->set_active(dir);
+ _vertical->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &XmlTree::_toggleDirection), _vertical));
+ _horizontal->property_draw_indicator() = false;
+ _vertical->property_draw_indicator() = false;
+ actionsbox->pack_end(*_horizontal, false, false, 0);
+ actionsbox->pack_end(*_vertical, false, false, 0);
+ _paned.pack2(*attributes, true, false);
+ paned_set_vertical(_paned, dir);
+ contents->pack_start(*actionsbox, false, false, 0);
+ /* Signal handlers */
+ GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW(tree));
+ _selection_changed = g_signal_connect (G_OBJECT(selection), "changed", G_CALLBACK (on_tree_select_row), this);
+ _tree_move = g_signal_connect_after( G_OBJECT(tree), "tree_move", G_CALLBACK(after_tree_move), this);
+
+ xml_element_new_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_new_element_node));
+ xml_text_new_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_new_text_node));
+ xml_node_duplicate_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_duplicate_node));
+ xml_node_delete_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_delete_node));
+ unindent_node_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_unindent_node));
+ indent_node_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_indent_node));
+ raise_node_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_raise_node));
+ lower_node_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_lower_node));
+
+ set_name("XMLAndAttributesDialog");
+ set_spacing(0);
+ set_size_request(320, -1);
+ show_all();
+
+ int panedpos = prefs->getInt("/dialogs/xml/panedpos", 200);
+ _paned.property_position() = panedpos;
+ _paned.property_position().signal_changed().connect(sigc::mem_fun(*this, &XmlTree::_resized));
+
+ tree_reset_context();
+ pack_start(*Gtk::manage(contents), true, true);
+}
+
+void XmlTree::_resized()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ prefs->setInt("/dialogs/xml/panedpos", _paned.property_position());
+}
+
+void XmlTree::_toggleDirection(Gtk::RadioButton *vertical)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool dir = vertical->get_active();
+ prefs->setBool("/dialogs/xml/vertical", dir);
+ paned_set_vertical(_paned, dir);
+ prefs->setInt("/dialogs/xml/panedpos", _paned.property_position());
+}
+
+void XmlTree::_attrtoggler()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool attrtoggler = !prefs->getBool("/dialogs/xml/attrtoggler", true);
+ prefs->setBool("/dialogs/xml/attrtoggler", attrtoggler);
+ if (attrtoggler) {
+ attributes->show();
+ } else {
+ attributes->hide();
+ }
+}
+
+XmlTree::~XmlTree ()
+{
+ // disconnect signals, they can fire after we leave destructor when 'tree' gets deleted
+ GtkTreeSelection* selection = gtk_tree_view_get_selection (GTK_TREE_VIEW(tree));
+ g_signal_handler_disconnect(G_OBJECT(selection), _selection_changed);
+ g_signal_handler_disconnect(G_OBJECT(tree), _tree_move);
+
+ unsetDocument();
+ _message_changed_connection.disconnect();
+}
+
+/**
+ * Sets the XML status bar when the tree is selected.
+ */
+void XmlTree::tree_reset_context()
+{
+ _message_context->set(Inkscape::NORMAL_MESSAGE,
+ _("<b>Click</b> to select nodes, <b>drag</b> to rearrange."));
+}
+
+void XmlTree::unsetDocument()
+{
+ document_uri_set_connection.disconnect();
+ if (deferred_on_tree_select_row_id != 0) {
+ g_source_destroy(g_main_context_find_source_by_id(nullptr, deferred_on_tree_select_row_id));
+ deferred_on_tree_select_row_id = 0;
+ }
+}
+
+void XmlTree::documentReplaced()
+{
+ unsetDocument();
+ if (auto document = getDocument()) {
+ // TODO: Why is this a document property?
+ document->setXMLDialogSelectedObject(nullptr);
+
+ document_uri_set_connection =
+ document->connectFilenameSet(sigc::bind(sigc::ptr_fun(&on_document_uri_set), document));
+ on_document_uri_set(document->getDocumentFilename(), document);
+ 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)
+{
+ 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);
+ gtk_tree_view_set_cursor(GTK_TREE_VIEW(tree), path, NULL, false);
+ gtk_tree_path_free(path);
+
+ } else {
+ g_message("XmlTree::set_tree_select : Couldn't find repr node");
+ }
+ } else {
+ GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
+ gtk_tree_selection_unselect_all (selection);
+
+ on_tree_unselect_row_disable();
+ }
+ propagate_tree_select(repr);
+}
+
+
+
+void XmlTree::propagate_tree_select(Inkscape::XML::Node *repr)
+{
+ if (repr &&
+ (repr->type() == Inkscape::XML::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 = dynamic_cast<SPGroup const *>(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 (SP_IS_GROUP(object->parent)) {
+ getDesktop()->layerManager().setCurrentLayer(object->parent);
+ }
+
+ getSelection()->set(SP_ITEM(object));
+ }
+
+ document->setXMLDialogSelectedObject(object);
+ blocked--;
+}
+
+
+void XmlTree::on_tree_select_row(GtkTreeSelection *selection, gpointer data)
+{
+ XmlTree *self = static_cast<XmlTree *>(data);
+
+ if (self->blocked || !self->getDesktop()) {
+ return;
+ }
+
+ // Defer the update after all events have been processed. Allows skipping
+ // of invalid intermediate selection states, like the automatic next row
+ // selection after `gtk_tree_store_remove`.
+ if (self->deferred_on_tree_select_row_id == 0) {
+ self->deferred_on_tree_select_row_id = //
+ g_idle_add(XmlTree::deferred_on_tree_select_row, data);
+ }
+}
+
+gboolean XmlTree::deferred_on_tree_select_row(gpointer data)
+{
+ XmlTree *self = static_cast<XmlTree *>(data);
+
+ self->deferred_on_tree_select_row_id = 0;
+
+ GtkTreeIter iter;
+ GtkTreeModel *model;
+
+ if (self->selected_repr) {
+ Inkscape::GC::release(self->selected_repr);
+ self->selected_repr = nullptr;
+ }
+
+ GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(self->tree));
+
+ if (!gtk_tree_selection_get_selected (selection, &model, &iter)) {
+ // Nothing selected, update widgets
+ self->propagate_tree_select(nullptr);
+ self->set_dt_select(nullptr);
+ self->on_tree_unselect_row_disable();
+ return FALSE;
+ }
+
+ Inkscape::XML::Node *repr = sp_xmlview_tree_node_get_repr(model, &iter);
+ g_assert(repr != nullptr);
+
+
+ self->selected_repr = repr;
+ Inkscape::GC::anchor(self->selected_repr);
+
+ self->propagate_tree_select(self->selected_repr);
+
+ self->set_dt_select(self->selected_repr);
+
+ self->tree_reset_context();
+
+ self->on_tree_select_row_enable(&iter);
+
+ return FALSE;
+}
+
+
+void XmlTree::after_tree_move(SPXMLViewTree * /*tree*/, gpointer value, gpointer data)
+{
+ XmlTree *self = static_cast<XmlTree *>(data);
+ guint val = GPOINTER_TO_UINT(value);
+
+ if (val) {
+ DocumentUndo::done(self->getDocument(), Q_("Undo History / XML dialog|Drag XML subtree"), INKSCAPE_ICON("dialog-xml-editor"));
+ } else {
+ DocumentUndo::cancel(self->getDocument());
+ }
+}
+
+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::on_document_uri_set(gchar const * /*uri*/, SPDocument * /*document*/)
+{
+/*
+ * Seems to be no way to set the title on a docked dialog
+*/
+}
+
+gboolean XmlTree::quit_on_esc (GtkWidget *w, GdkEventKey *event, GObject */*tbl*/)
+{
+ switch (Inkscape::UI::Tools::get_latin_keyval (event)) {
+ case GDK_KEY_Escape: // defocus
+ gtk_widget_destroy(w);
+ return TRUE;
+ case GDK_KEY_Return: // create
+ case GDK_KEY_KP_Enter:
+ gtk_widget_destroy(w);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+void XmlTree::cmd_new_element_node()
+{
+ auto document = getDocument();
+ if (!document)
+ return;
+
+ Gtk::Dialog dialog;
+ Gtk::Entry entry;
+
+ dialog.get_content_area()->pack_start(entry);
+ dialog.add_button("Cancel", Gtk::RESPONSE_CANCEL);
+ dialog.add_button("Create", Gtk::RESPONSE_OK);
+ dialog.show_all();
+
+ int result = dialog.run();
+ if (result == Gtk::RESPONSE_OK) {
+ Glib::ustring new_name = entry.get_text();
+ if (!new_name.empty()) {
+ Inkscape::XML::Document *xml_doc = document->getReprDoc();
+ Inkscape::XML::Node *new_repr;
+ new_repr = xml_doc->createElement(new_name.c_str());
+ Inkscape::GC::release(new_repr);
+ selected_repr->appendChild(new_repr);
+ set_tree_select(new_repr);
+ set_dt_select(new_repr);
+
+ DocumentUndo::done(document, Q_("Undo History / XML dialog|Create new element node"), INKSCAPE_ICON("dialog-xml-editor"));
+ }
+ }
+} // end of cmd_new_element_node()
+
+
+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 dialog|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 dialog|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 dialog|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 dialog|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 dialog|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 dialog|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 dialog|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 (SP_IS_ITEM(child)) {
+ SPObject const * const parent = child->parent;
+ if (parent == nullptr) {
+ g_assert(SP_IS_ROOT(child));
+ if (child == &item) {
+ // item is root
+ return false;
+ }
+ return true;
+ }
+ child = parent;
+ }
+ g_assert(!SP_IS_ROOT(child));
+ return false;
+}
+
+void XmlTree::desktopReplaced() {
+ // subdialog does not receive desktopReplace calls, we need to propagate desktop change
+ if (attributes) {
+ attributes->setDesktop(getDesktop());
+ }
+}
+
+}
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-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..3586666
--- /dev/null
+++ b/src/ui/dialog/xml-tree.h
@@ -0,0 +1,238 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * This is XML tree editor, which allows direct modifying of all elements
+ * of Inkscape document, including foreign ones.
+ *//*
+ * Authors: see git history
+ * Lauris Kaplinski, 2000
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_DIALOGS_XML_TREE_H
+#define SEEN_UI_DIALOGS_XML_TREE_H
+
+#include <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 <memory>
+
+#include "message.h"
+#include "ui/dialog/attrdialog.h"
+#include "ui/dialog/dialog-base.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;
+
+ static XmlTree &getInstance() { return *new XmlTree(); }
+
+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);
+
+ /**
+ * Sets the XML status bar when the tree is selected.
+ */
+ void tree_reset_context();
+
+ /**
+ * Is the selected tree node editable
+ */
+ gboolean xml_tree_node_mutable(GtkTreeIter *node);
+
+ /**
+ * Callback to close the add dialog on Escape key
+ */
+ static gboolean quit_on_esc (GtkWidget *w, GdkEventKey *event, GObject */*tbl*/);
+
+ /**
+ * Select a node in the xml tree
+ */
+ void set_tree_select(Inkscape::XML::Node *repr);
+
+ /**
+ * Set the attribute list to match the selected node in the tree
+ */
+ void propagate_tree_select(Inkscape::XML::Node *repr);
+
+ /**
+ * Find the current desktop selection
+ */
+ Inkscape::XML::Node *get_dt_select();
+
+ /**
+ * Select the current desktop selection
+ */
+ void set_dt_select(Inkscape::XML::Node *repr);
+
+ /**
+ * Callback for a node in the tree being selected
+ */
+ static void on_tree_select_row(GtkTreeSelection *selection, gpointer data);
+ /**
+ * Callback for deferring the `on_tree_select_row` response in order to
+ * skip invalid intermediate selection states. In particular,
+ * `gtk_tree_store_remove` makes an undesired selection that we will
+ * immediately revert and don't want to an early response for.
+ */
+ static gboolean deferred_on_tree_select_row(gpointer);
+ /// Event source ID for the last scheduled `deferred_on_tree_select_row` event.
+ guint deferred_on_tree_select_row_id = 0;
+
+ /**
+ * Callback when a node is moved in the tree
+ */
+ static void after_tree_move(SPXMLViewTree *tree, gpointer value, gpointer data);
+
+ /**
+ * Callback for when an attribute is edited.
+ */
+ //static void on_attr_edited(SPXMLViewAttrList *attributes, const gchar * name, const gchar * value, gpointer /*data*/);
+
+ /**
+ * Callback for when attribute list values change
+ */
+ //static void on_attr_row_changed(SPXMLViewAttrList *attributes, const gchar * name, gpointer data);
+
+ /**
+ * Enable widgets based on current selections
+ */
+ void on_tree_select_row_enable(GtkTreeIter *node);
+ void on_tree_unselect_row_disable();
+ void on_tree_unselect_row_hide();
+ void on_attr_unselect_row_disable();
+
+ void onNameChanged();
+ void onCreateNameChanged();
+
+ /**
+ * Callbacks for changes in desktop selection and current document
+ */
+ static void on_document_uri_set(gchar const *uri, SPDocument *document);
+ static void _set_status_message(Inkscape::MessageType type, const gchar *message, GtkWidget *dialog);
+
+ /**
+ * Callbacks for toolbar buttons being pressed
+ */
+ void cmd_new_element_node();
+ void cmd_new_text_node();
+ void cmd_duplicate_node();
+ void cmd_delete_node();
+ void cmd_raise_node();
+ void cmd_lower_node();
+ void cmd_indent_node();
+ void cmd_unindent_node();
+
+ void _attrtoggler();
+ void _toggleDirection(Gtk::RadioButton *vertical);
+ void _resized();
+ bool in_dt_coordsys(SPObject const &item);
+
+ /**
+ * Flag to ensure only one operation is performed at once
+ */
+ gint blocked;
+
+ bool _updating;
+ /**
+ * Status bar
+ */
+ std::shared_ptr<Inkscape::MessageStack> _message_stack;
+ std::unique_ptr<Inkscape::MessageContext> _message_context;
+
+ /**
+ * Signal handlers
+ */
+ sigc::connection _message_changed_connection;
+ sigc::connection document_uri_set_connection;
+
+ gint selected_attr;
+ Inkscape::XML::Node *selected_repr;
+
+ /* XmlTree Widgets */
+ SPXMLViewTree *tree;
+ //SPXMLViewAttrList *attributes;
+ AttrDialog *attributes;
+ Gtk::Box *_attrbox;
+
+ /* XML Node Creation pop-up window */
+ Gtk::Entry *name_entry;
+ Gtk::Button *create_button;
+ Gtk::Paned _paned;
+
+ Gtk::Box node_box;
+ Gtk::Box status_box;
+ Gtk::Switch _attrswitch;
+ Gtk::Label status;
+ Gtk::Toolbar tree_toolbar;
+ Gtk::ToolButton xml_element_new_button;
+ Gtk::ToolButton xml_text_new_button;
+ Gtk::ToolButton xml_node_delete_button;
+ Gtk::SeparatorToolItem separator;
+ Gtk::ToolButton xml_node_duplicate_button;
+ Gtk::SeparatorToolItem separator2;
+ Gtk::ToolButton unindent_node_button;
+ Gtk::ToolButton indent_node_button;
+ Gtk::ToolButton raise_node_button;
+ Gtk::ToolButton lower_node_button;
+
+ GtkWidget *new_window;
+
+ gulong _selection_changed = 0;
+ gulong _tree_move = 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/drag-and-drop.cpp b/src/ui/drag-and-drop.cpp
new file mode 100644
index 0000000..fc279b9
--- /dev/null
+++ b/src/ui/drag-and-drop.cpp
@@ -0,0 +1,464 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/**
+ * @file
+ * Drag and drop of drawings onto canvas.
+ */
+
+/* Authors:
+ *
+ * Copyright (C) Tavmjong Bah 2019
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "drag-and-drop.h"
+
+#include <glibmm/i18n.h> // Internationalization
+
+#include "desktop-style.h"
+#include "document.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/ege-paint-def.h"
+
+using Inkscape::DocumentUndo;
+
+/* Drag and Drop */
+enum ui_drop_target_info {
+ URI_LIST,
+ SVG_XML_DATA,
+ SVG_DATA,
+ PNG_DATA,
+ JPEG_DATA,
+ IMAGE_DATA,
+ APP_X_INKY_COLOR,
+ APP_X_COLOR,
+ APP_OSWB_COLOR,
+ APP_X_INK_PASTE
+};
+
+static GtkTargetEntry ui_drop_target_entries [] = {
+ // clang-format off
+ {(gchar *)"text/uri-list", 0, URI_LIST },
+ {(gchar *)"image/svg+xml", 0, SVG_XML_DATA },
+ {(gchar *)"image/svg", 0, SVG_DATA },
+ {(gchar *)"image/png", 0, PNG_DATA },
+ {(gchar *)"image/jpeg", 0, JPEG_DATA },
+ {(gchar *)"application/x-oswb-color", 0, APP_OSWB_COLOR },
+ {(gchar *)"application/x-color", 0, APP_X_COLOR },
+ {(gchar *)"application/x-inkscape-paste", 0, APP_X_INK_PASTE }
+ // clang-format on
+};
+
+static GtkTargetEntry *completeDropTargets = nullptr;
+static int completeDropTargetsCount = 0;
+
+static guint nui_drop_target_entries = G_N_ELEMENTS(ui_drop_target_entries);
+
+/* 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 &&
+ (SP_IS_SHAPE(item) || SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item))) {
+ Path *livarot_path = Path_for_item(item, true, true);
+ livarot_path->ConvertWithBackData(0.04);
+
+ 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 ) {
+ ege::PaintDef color;
+ worked = color.fromMIMEData("application/x-oswb-color",
+ reinterpret_cast<char const *>(gtk_selection_data_get_data (data)),
+ gtk_selection_data_get_length (data),
+ gtk_selection_data_get_format (data));
+ if ( worked ) {
+ if ( color.getType() == ege::PaintDef::CLEAR ) {
+ colorspec = ""; // TODO check if this is sufficient
+ } else if ( color.getType() == ege::PaintDef::NONE ) {
+ colorspec = "none";
+ } else {
+ unsigned int r = color.getR();
+ unsigned int g = color.getG();
+ unsigned int b = color.getB();
+
+ SPGradient* matches = nullptr;
+ std::vector<SPObject *> gradients = doc->getResourceList("gradient");
+ for (auto gradient : gradients) {
+ SPGradient* grad = SP_GRADIENT(gradient);
+ if ( color.descr == grad->getId() ) {
+ if ( grad->hasStops() ) {
+ matches = grad;
+ break;
+ }
+ }
+ }
+ if (matches) {
+ colorspec = "url(#";
+ colorspec += matches->getId();
+ colorspec += ")";
+ } else {
+ gchar* tmp = g_strdup_printf("#%02x%02x%02x", r, g, b);
+ colorspec = tmp;
+ g_free(tmp);
+ }
+ }
+ }
+ }
+ if ( worked ) {
+ int destX = 0;
+ int destY = 0;
+ 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 &&
+ (SP_IS_SHAPE(item) || SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item))) {
+ Path *livarot_path = Path_for_item(item, true, true);
+ livarot_path->ConvertWithBackData(0.04);
+
+ 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(SP_ITEM(new_obj));
+
+ // move to mouse pointer
+ {
+ desktop->getDocument()->ensureUpToDate();
+ Geom::OptRect sel_bbox = selection->visualBounds();
+ if (sel_bbox) {
+ Geom::Point m( desktop->point() - sel_bbox->midpoint() );
+ selection->moveRelative(m, false);
+ }
+ }
+
+ Inkscape::GC::release(newgroup);
+ DocumentUndo::done( doc, _("Drop SVG"), "" );
+ prefs->setBool("/options/onimport", false);
+ break;
+ }
+
+ case URI_LIST: {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/options/onimport", true);
+ gchar *uri = (gchar *)gtk_selection_data_get_data (data);
+ sp_ui_import_files(uri);
+ prefs->setBool("/options/onimport", false);
+ break;
+ }
+
+ case APP_X_INK_PASTE: {
+ Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get();
+ cm->paste(desktop);
+ DocumentUndo::done( doc, _("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 == nullptr || completeDropTargetsCount == 0 )
+ {
+ std::vector<Glib::ustring> types;
+
+ std::vector<Gdk::PixbufFormat> list = Gdk::Pixbuf::get_formats();
+ for (auto one:list) {
+ std::vector<Glib::ustring> typesXX = one.get_mime_types();
+ for (auto i:typesXX) {
+ types.push_back(i);
+ }
+ }
+ completeDropTargetsCount = nui_drop_target_entries + types.size();
+ completeDropTargets = new GtkTargetEntry[completeDropTargetsCount];
+ for ( int i = 0; i < (int)nui_drop_target_entries; i++ ) {
+ completeDropTargets[i] = ui_drop_target_entries[i];
+ }
+ int pos = nui_drop_target_entries;
+
+ for (auto & type : types) {
+ completeDropTargets[pos].target = g_strdup(type.c_str());
+ completeDropTargets[pos].flags = 0;
+ completeDropTargets[pos].info = IMAGE_DATA;
+ pos++;
+ }
+ }
+
+ auto canvas = dtw->get_canvas();
+
+ gtk_drag_dest_set(GTK_WIDGET(canvas->gobj()),
+ GTK_DEST_DEFAULT_ALL,
+ completeDropTargets,
+ completeDropTargetsCount,
+ GdkDragAction(GDK_ACTION_COPY | GDK_ACTION_MOVE));
+
+ g_signal_connect(G_OBJECT(canvas->gobj()),
+ "drag_data_received",
+ G_CALLBACK(ink_drag_data_received),
+ dtw);
+
+#if 0
+ g_signal_connect(G_OBJECT(win->gobj()),
+ "drag_motion",
+ G_CALLBACK(ink_drag_motion),
+ NULL);
+
+ g_signal_connect(G_OBJECT(win->gobj()),
+ "drag_leave",
+ G_CALLBACK(ink_drag_leave),
+ NULL);
+#endif
+}
+
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/drag-and-drop.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..3daf4a3
--- /dev/null
+++ b/src/ui/draw-anchor.cpp
@@ -0,0 +1,86 @@
+// 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, SPCurve *curve, bool start, Geom::Point delta)
+ : dc(dc), curve(curve->ref()), start(start), active(FALSE), dp(delta),
+ ctrl(
+ new 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()
+{
+ if (ctrl) {
+ delete (ctrl);
+ }
+}
+
+/**
+ * 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..dd0b13a
--- /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 <memory>
+
+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::unique_ptr<SPCurve> curve;
+ bool start : 1;
+ bool active : 1;
+ Geom::Point dp;
+ Inkscape::CanvasItemCtrl *ctrl = nullptr;
+
+ SPDrawAnchor(Inkscape::UI::Tools::FreehandBase *dc,
+ 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/icon-loader.cpp b/src/ui/icon-loader.cpp
new file mode 100644
index 0000000..8216bf8
--- /dev/null
+++ b/src/ui/icon-loader.cpp
@@ -0,0 +1,137 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Icon Loader
+ *
+ * Icon Loader management code
+ *
+ * Authors:
+ * Jabiertxo Arraiza <jabier.arraiza@marker.es>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+
+#include "icon-loader.h"
+#include "inkscape.h"
+#include "io/resource.h"
+#include "svg/svg-color.h"
+#include "widgets/toolbox.h"
+
+#include <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)
+{
+ 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)) {
+ Gtk::Window *window = SP_ACTIVE_DESKTOP->getToplevel();
+ 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) {
+ 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..01e2937
--- /dev/null
+++ b/src/ui/interface.cpp
@@ -0,0 +1,188 @@
+// 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_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..a339539
--- /dev/null
+++ b/src/ui/knot/knot-holder-entity.cpp
@@ -0,0 +1,454 @@
+// 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 "knot-holder.h"
+
+#include "desktop.h"
+#include "inkscape.h"
+#include "preferences.h"
+#include "snap.h"
+#include "style.h"
+
+#include "live_effects/effect.h"
+#include "object/sp-hatch.h"
+#include "object/sp-item.h"
+#include "object/sp-namedview.h"
+#include "object/sp-pattern.h"
+#include "object/sp-marker.h"
+
+#include "display/control/canvas-item-ctrl.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);
+ 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::cout << "No desktop" << std::endl;
+ if (!desktop->namedview) std::cout << "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)
+{
+ Inkscape::LivePathEffect::Effect *effect = _effect;
+ if (effect) {
+ effect->refresh_widgets = true;
+ effect->writeParamsToSVG();
+ }
+}
+
+/* Pattern manipulation */
+
+/* TODO: this pattern manipulation is not able to handle general transformation matrices. Only matrices that are the result of a pure scale times a pure rotation. */
+
+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);
+}
+
+static Geom::Point sp_pattern_knot_get(SPPattern const *pat, gdouble x, gdouble y)
+{
+ return Geom::Point(x, y) * pat->getTransform();
+}
+
+bool
+PatternKnotHolderEntity::knot_missing() const
+{
+ SPPattern *pat = _pattern();
+ return (pat == nullptr);
+}
+
+SPPattern*
+PatternKnotHolderEntity::_pattern() const
+{
+ return _fill ? SP_PATTERN(item->style->getFillPaintServer()) : SP_PATTERN(item->style->getStrokePaintServer());
+}
+
+Geom::Point
+PatternKnotHolderEntityXY::knot_get() const
+{
+ SPPattern *pat = _pattern();
+ return sp_pattern_knot_get(pat, 0, 0);
+}
+
+Geom::Point
+PatternKnotHolderEntityAngle::knot_get() const
+{
+ SPPattern *pat = _pattern();
+ return sp_pattern_knot_get(pat, pat->width(), 0);
+}
+
+Geom::Point
+PatternKnotHolderEntityScale::knot_get() const
+{
+ SPPattern *pat = _pattern();
+ return sp_pattern_knot_get(pat, pat->width(), pat->height());
+}
+
+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);
+
+ SPPattern *pat = _fill ? SP_PATTERN(item->style->getFillPaintServer()) : SP_PATTERN(item->style->getStrokePaintServer());
+
+ // get the angle from pattern 0,0 to the cursor pos
+ Geom::Point transform_origin = sp_pattern_knot_get(pat, 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);
+}
+
+void
+PatternKnotHolderEntityScale::knot_set(Geom::Point const &p, Geom::Point const &origin, guint state)
+{
+ SPPattern *pat = _pattern();
+
+ // 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 = pat->getTransform();
+ 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 pat_x = pat->width();
+ gdouble pat_y = pat->height();
+ if ( state & GDK_CONTROL_MASK ) {
+ // if ctrl is pressed: use 1:1 scaling
+ d = d_origin * (d.length() / d_origin.length());
+ }
+
+ Geom::Affine rot = Geom::Translate(-origin_dt)
+ * Geom::Scale(d.x() / pat_x, d.y() / pat_y)
+ * Geom::Translate(origin_dt)
+ * transform;
+
+ item->adjust_pattern(rot, 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 ? SP_HATCH(item->style->getFillPaintServer()) : SP_HATCH(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 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());
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ 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..3195c2a
--- /dev/null
+++ b/src/ui/knot/knot-holder-entity.h
@@ -0,0 +1,205 @@
+// 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"
+
+class SPHatch;
+class SPItem;
+class SPKnot;
+class SPDesktop;
+class SPPattern;
+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_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*/) {}
+
+ 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) {}
+ 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;
+ SPPattern *_pattern() const;
+};
+
+class PatternKnotHolderEntityXY : public PatternKnotHolderEntity {
+public:
+ PatternKnotHolderEntityXY(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 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;
+};
+
+/* 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
+};
+
+#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..a743168
--- /dev/null
+++ b/src/ui/knot/knot-holder.cpp
@@ -0,0 +1,455 @@
+// 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 "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_print ("Error! Throw an exception, please!\n");
+ }
+
+ 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);
+ }
+
+ {
+ SPShape *savedShape = dynamic_cast<SPShape *>(saved_item);
+ if (savedShape) {
+ savedShape->set_shape();
+ }
+ }
+
+ this->update_knots();
+
+ Glib::ustring icon_name;
+
+ // TODO extract duplicated blocks;
+ if (dynamic_cast<SPRect *>(saved_item)) {
+ icon_name = INKSCAPE_ICON("draw-rectangle");
+ } else if (dynamic_cast<SPBox3D *>(saved_item)) {
+ icon_name = INKSCAPE_ICON("draw-cuboid");
+ } else if (dynamic_cast<SPGenericEllipse *>(saved_item)) {
+ icon_name = INKSCAPE_ICON("draw-ellipse");
+ } else if (dynamic_cast<SPStar *>(saved_item)) {
+ icon_name = INKSCAPE_ICON("draw-polygon-star");
+ } else if (dynamic_cast<SPSpiral *>(saved_item)) {
+ icon_name = INKSCAPE_ICON("draw-spiral");
+ } else if (dynamic_cast<SPMarker *>(saved_item)) {
+ icon_name = INKSCAPE_ICON("tool-pointer");
+ } else {
+ SPOffset *offset = dynamic_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) { //increasingly aggressive sanity checks
+ if (saved_item->document) {
+ DocumentUndo::done(saved_item->document, _("Change handle"), icon_name);
+ }
+ } // else { abort(); }
+}
+
+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);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+void
+KnotHolder::knot_moved_handler(SPKnot *knot, Geom::Point const &p, guint state)
+{
+ if (this->dragging == false) {
+ this->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;
+ }
+ }
+
+ SPShape *shape = dynamic_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);
+ 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();
+
+ /* do cleanup tasks (e.g., for LPE items write the parameter values
+ * that were changed by dragging the handle to SVG)
+ */
+ SPLPEItem *lpeItem = dynamic_cast<SPLPEItem *>(object);
+ if (lpeItem) {
+ // This writes all parameters to SVG. Is this sufficiently efficient or should we only
+ // write the ones that were changed?
+ Inkscape::LivePathEffect::Effect *lpe = lpeItem->getCurrentLPE();
+ if (lpe) {
+ LivePathEffectObject *lpeobj = lpe->getLPEObj();
+ lpeobj->updateRepr();
+ }
+ }
+
+ SPFilter *filter = (object->style) ? object->style->getFilter() : nullptr;
+ if (filter) {
+ filter->updateRepr();
+ }
+
+ Glib::ustring icon_name;
+
+ // TODO extract duplicated blocks;
+ if (dynamic_cast<SPRect *>(object)) {
+ icon_name = INKSCAPE_ICON("draw-rectangle");
+ } else if (dynamic_cast<SPBox3D *>(object)) {
+ icon_name = INKSCAPE_ICON("draw-cuboid");
+ } else if (dynamic_cast<SPGenericEllipse *>(object)) {
+ icon_name = INKSCAPE_ICON("draw-ellipse");
+ } else if (dynamic_cast<SPStar *>(object)) {
+ icon_name = INKSCAPE_ICON("draw-polygon-star");
+ } else if (dynamic_cast<SPSpiral *>(object)) {
+ icon_name = INKSCAPE_ICON("draw-spiral");
+ } else if (dynamic_cast<SPMarker *>(object)) {
+ icon_name = INKSCAPE_ICON("tool-pointer");
+ } else {
+ SPOffset *offset = dynamic_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 ((item->style->fill.isPaintserver()) && dynamic_cast<SPPattern *>(item->style->getFillPaintServer())) {
+ PatternKnotHolderEntityXY *entity_xy = new PatternKnotHolderEntityXY(true);
+ PatternKnotHolderEntityAngle *entity_angle = new PatternKnotHolderEntityAngle(true);
+ PatternKnotHolderEntityScale *entity_scale = new PatternKnotHolderEntityScale(true);
+ entity_xy->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_POINT, "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 ((item->style->stroke.isPaintserver()) && dynamic_cast<SPPattern *>(item->style->getStrokePaintServer())) {
+ PatternKnotHolderEntityXY *entity_xy = new PatternKnotHolderEntityXY(false);
+ PatternKnotHolderEntityAngle *entity_angle = new PatternKnotHolderEntityAngle(false);
+ PatternKnotHolderEntityScale *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);
+ }
+}
+
+void KnotHolder::add_hatch_knotholder()
+{
+ if ((item->style->fill.isPaintserver()) && dynamic_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()) && dynamic_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 (!item->style->filter.set || !item->style->getFilter() || item->style->getFilter()->auto_region) {
+ return;
+ }
+
+ FilterKnotHolderEntity *entity_tl = new FilterKnotHolderEntity(true);
+ FilterKnotHolderEntity *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);
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ 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..95c9915
--- /dev/null
+++ b/src/ui/knot/knot-holder.h
@@ -0,0 +1,116 @@
+// 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>
+
+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_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; }
+
+ 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.
+ std::list<KnotHolderEntity *> entity;
+
+ SPKnotHolderReleasedFunc released;
+
+ bool local_change; ///< if true, no need to recreate knotholder if repr was changed.
+
+ bool dragging;
+
+ Geom::Affine _edit_transform;
+};
+
+/**
+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..d0b2e0c
--- /dev/null
+++ b/src/ui/knot/knot.cpp
@@ -0,0 +1,520 @@
+// 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"
+
+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 = new Inkscape::CanvasItemCtrl(desktop->getCanvasControls(), type); // Shape, mode set
+ Glib::ustring ctrl_name = "CanvasItemCtrl:Knot: " + name;
+ ctrl->set_name(ctrl_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);
+ }
+
+ if (ctrl) {
+ delete ctrl;
+ }
+
+ 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
+ if (desktop->event_context->_delayed_snap_event) {
+ sp_event_context_snap_watchdog_callback(desktop->event_context->_delayed_snap_event);
+ }
+ desktop->event_context->discard_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;
+
+ if (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);
+ }
+
+ sp_event_context_snap_delay_handler(desktop->event_context, nullptr, this, (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->scroll_to_point (motion_dt);
+ 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) {
+ knot->desktop->canvas->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);
+ ctrl->set_pixbuf(static_cast<GdkPixbuf *>(pixbuf));
+ }
+
+ _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::setPixbuf(gpointer p) {
+ pixbuf = p;
+}
+
+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..59104e7
--- /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 "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. */
+ Inkscape::CanvasItemCtrl *ctrl = nullptr; /**< 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. */
+ 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];
+
+ void* pixbuf = nullptr;
+ 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 setPixbuf(void* p);
+ 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 pixbuf and set its 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..4ac3f1f
--- /dev/null
+++ b/src/ui/modifiers.cpp
@@ -0,0 +1,242 @@
+// 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
+};
+
+decltype(Modifier::_category_names) Modifier::_category_names {
+ {NO_CATEGORY, _("No Category")},
+ {CANVAS, _("Canvas")},
+ {SELECT, _("Selection")},
+ {MOVE, _("Movement")},
+ {TRANSFORM, _("Transformations")},
+};
+
+
+/**
+ * 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 button_state - The GDK button state from an event
+ * @return a boolean, true if the modifiers for this action are active.
+ */
+bool Modifier::active(int button_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 & button_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);
+}
+
+/**
+ * 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());
+}
+
+} // 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..54765c4
--- /dev/null
+++ b/src/ui/modifiers.h
@@ -0,0 +1,249 @@
+// 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,
+ // 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}
+ // 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, ...);
+
+/**
+ * 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);
+
+ /**
+ * 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/previewable.h b/src/ui/previewable.h
new file mode 100644
index 0000000..c25f2db
--- /dev/null
+++ b/src/ui/previewable.h
@@ -0,0 +1,64 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifndef SEEN_PREVIEWABLE_H
+#define SEEN_PREVIEWABLE_H
+/*
+ * A simple interface for previewing representations.
+ *
+ * Authors:
+ * Jon A. Cruz
+ *
+ * Copyright (C) 2005 Jon A. Cruz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+
+#include <gtkmm/widget.h>
+
+#include "widget/preview.h"
+
+namespace Inkscape {
+namespace UI {
+
+enum PreviewStyle {
+ PREVIEW_STYLE_ICON = 0,
+ PREVIEW_STYLE_PREVIEW,
+ PREVIEW_STYLE_NAME,
+ PREVIEW_STYLE_BLURB,
+ PREVIEW_STYLE_ICON_NAME,
+ PREVIEW_STYLE_ICON_BLURB,
+ PREVIEW_STYLE_PREVIEW_NAME,
+ PREVIEW_STYLE_PREVIEW_BLURB
+};
+
+
+class Previewable
+{
+public:
+// TODO need to add some nice parameters
+ virtual ~Previewable() = default;
+ virtual Gtk::Widget* getPreview(UI::Widget::PreviewStyle style,
+ UI::Widget::ViewType view,
+ UI::Widget::PreviewSize size,
+ guint ratio,
+ guint border) = 0;
+};
+
+
+} //namespace UI
+} //namespace Inkscape
+
+
+#endif // SEEN_PREVIEWABLE_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/previewholder.cpp b/src/ui/previewholder.cpp
new file mode 100644
index 0000000..12a71c8
--- /dev/null
+++ b/src/ui/previewholder.cpp
@@ -0,0 +1,445 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A simple interface for previewing representations.
+ *
+ * Authors:
+ * Jon A. Cruz
+ *
+ * Copyright (C) 2005 Jon A. Cruz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+
+#include "previewable.h"
+#include "previewholder.h"
+
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/sizegroup.h>
+#include <gtkmm/scrollbar.h>
+#include <gtkmm/adjustment.h>
+#include <gtkmm/grid.h>
+
+#define COLUMNS_FOR_SMALL 16
+#define COLUMNS_FOR_LARGE 8
+//#define COLUMNS_FOR_SMALL 48
+//#define COLUMNS_FOR_LARGE 32
+
+
+namespace Inkscape {
+namespace UI {
+
+
+PreviewHolder::PreviewHolder() :
+ Bin(),
+ _scroller(nullptr),
+ _insides(nullptr),
+ _prefCols(0),
+ _updatesFrozen(false),
+ _anchor(SP_ANCHOR_CENTER),
+ _baseSize(UI::Widget::PREVIEW_SIZE_SMALL),
+ _ratio(100),
+ _view(UI::Widget::VIEW_TYPE_LIST),
+ _wrap(false),
+ _border(UI::Widget::BORDER_NONE)
+{
+ set_name( "PreviewHolder" );
+ _scroller = Gtk::manage(new Gtk::ScrolledWindow());
+ _scroller->set_name( "PreviewHolderScroller" );
+ _scroller->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+
+ _insides = Gtk::manage(new Gtk::Grid());
+ _insides->set_name( "PreviewHolderGrid" );
+ _insides->set_column_spacing(8);
+
+ _scroller->set_hexpand();
+ _scroller->set_vexpand();
+ _scroller->add( *_insides );
+
+ // Disable overlay scrolling as the scrollbar covers up swatches.
+ // For some reason this also makes the height 55px.
+ _scroller->set_overlay_scrolling(false);
+
+ add(*_scroller);
+}
+
+PreviewHolder::~PreviewHolder()
+= default;
+
+/**
+ * Translates vertical scrolling into horizontal
+ */
+bool PreviewHolder::on_scroll_event(GdkEventScroll *event)
+{
+ if (_wrap) {
+ return FALSE;
+ }
+
+ // Scroll horizontally by page on mouse wheel
+ auto adj = _scroller->get_hadjustment();
+
+ if (!adj) {
+ return FALSE;
+ }
+
+ double move;
+ switch (event->direction) {
+ case GDK_SCROLL_UP:
+ case GDK_SCROLL_LEFT:
+ move = -adj->get_page_size();
+ break;
+ case GDK_SCROLL_DOWN:
+ case GDK_SCROLL_RIGHT:
+ move = adj->get_page_size();
+ break;
+ case GDK_SCROLL_SMOOTH:
+ if (fabs(event->delta_y) <= fabs(event->delta_x)) {
+ return FALSE;
+ }
+#ifdef GDK_WINDOWING_QUARTZ
+ move = event->delta_y;
+#else
+ move = event->delta_y * adj->get_page_size();
+#endif
+ break;
+ default:
+ return FALSE;
+ }
+
+ double value = adj->get_value() + move;
+
+ adj->set_value(value);
+
+ return TRUE;
+}
+
+void PreviewHolder::clear()
+{
+ items.clear();
+ _prefCols = 0;
+ // Kludge to restore scrollbars
+ if ( !_wrap && (_view != UI::Widget::VIEW_TYPE_LIST) && (_anchor == SP_ANCHOR_NORTH || _anchor == SP_ANCHOR_SOUTH) ) {
+ _scroller->set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_NEVER );
+ }
+ rebuildUI();
+}
+
+/**
+ * Add a Previewable item to the PreviewHolder
+ *
+ * \param[in] preview The Previewable item to add
+ */
+void PreviewHolder::addPreview( Previewable* preview )
+{
+ items.push_back(preview);
+ if ( !_updatesFrozen )
+ {
+ int i = items.size() - 1;
+
+ switch(_view) {
+ case UI::Widget::VIEW_TYPE_LIST:
+ {
+ Gtk::Widget* label = Gtk::manage(preview->getPreview(UI::Widget::PREVIEW_STYLE_BLURB,
+ UI::Widget::VIEW_TYPE_LIST,
+ _baseSize, _ratio, _border));
+ Gtk::Widget* item = Gtk::manage(preview->getPreview(UI::Widget::PREVIEW_STYLE_PREVIEW,
+ UI::Widget::VIEW_TYPE_LIST,
+ _baseSize, _ratio, _border));
+
+ item->set_hexpand();
+ item->set_vexpand();
+ _insides->attach(*item, 0, i, 1, 1);
+
+ label->set_hexpand();
+ label->set_valign(Gtk::ALIGN_CENTER);
+ _insides->attach(*label, 1, i, 1, 1);
+ }
+
+ break;
+ case UI::Widget::VIEW_TYPE_GRID:
+ {
+ Gtk::Widget* item = Gtk::manage(items[i]->getPreview(UI::Widget::PREVIEW_STYLE_PREVIEW,
+ UI::Widget::VIEW_TYPE_GRID,
+ _baseSize, _ratio, _border));
+
+ int ncols = 1;
+ int nrows = 1;
+ int col = 0;
+ int row = 0;
+
+ // To get size
+ auto kids = _insides->get_children();
+ int childCount = (int)kids.size();
+ if (childCount > 0 ) {
+
+ // Need already shown widget
+ calcGridSize( kids[0], items.size()+1, ncols, nrows );
+
+ // Column and row for the new widget
+ col = i % ncols;
+ row = i / ncols;
+
+ }
+
+ // Loop through the existing widgets and move them to new location
+ for ( int j = 1; j < childCount; j++ ) {
+ auto target = kids[childCount - (j + 1)];
+ int col2 = j % ncols;
+ int row2 = j / ncols;
+ _insides->remove( *target );
+
+ target->set_hexpand();
+ target->set_vexpand();
+ _insides->attach( *target, col2, row2, 1, 1);
+ }
+ item->set_hexpand();
+ item->set_vexpand();
+ _insides->attach(*item, col, row, 1, 1);
+ }
+ }
+
+ _scroller->show_all_children();
+ }
+}
+
+void PreviewHolder::freezeUpdates()
+{
+ _updatesFrozen = true;
+}
+
+void PreviewHolder::thawUpdates()
+{
+ _updatesFrozen = false;
+ rebuildUI();
+}
+
+void
+PreviewHolder::setStyle(UI::Widget::PreviewSize size,
+ UI::Widget::ViewType view,
+ guint ratio,
+ UI::Widget::BorderStyle border )
+{
+ if ( size != _baseSize || view != _view || ratio != _ratio || border != _border ) {
+ _baseSize = size;
+ _view = view;
+ _ratio = ratio;
+ _border = border;
+ // Kludge to restore scrollbars
+ if ( !_wrap && (_view != UI::Widget::VIEW_TYPE_LIST) && (_anchor == SP_ANCHOR_NORTH || _anchor == SP_ANCHOR_SOUTH) ) {
+ _scroller->set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_NEVER );
+ }
+ rebuildUI();
+ }
+}
+
+void PreviewHolder::setOrientation(SPAnchorType anchor)
+{
+ if ( _anchor != anchor )
+ {
+ _anchor = anchor;
+ switch ( _anchor )
+ {
+ case SP_ANCHOR_NORTH:
+ case SP_ANCHOR_SOUTH:
+ {
+ _scroller->set_policy( Gtk::POLICY_AUTOMATIC, _wrap ? Gtk::POLICY_AUTOMATIC : Gtk::POLICY_NEVER );
+ }
+ break;
+
+ case SP_ANCHOR_EAST:
+ case SP_ANCHOR_WEST:
+ {
+ _scroller->set_policy( Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC );
+ }
+ break;
+
+ default:
+ {
+ _scroller->set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC );
+ }
+ }
+ rebuildUI();
+ }
+}
+
+void PreviewHolder::setWrap( bool wrap )
+{
+ if (_wrap != wrap) {
+ _wrap = wrap;
+ switch ( _anchor )
+ {
+ case SP_ANCHOR_NORTH:
+ case SP_ANCHOR_SOUTH:
+ {
+ _scroller->set_policy( Gtk::POLICY_AUTOMATIC, _wrap ? Gtk::POLICY_AUTOMATIC : Gtk::POLICY_NEVER );
+ }
+ break;
+ default:
+ {
+ (void)0;
+ // do nothing;
+ }
+ }
+ rebuildUI();
+ }
+}
+
+void PreviewHolder::setColumnPref( int cols )
+{
+ _prefCols = cols;
+}
+
+
+/**
+ * Calculate the grid side of a preview holder
+ *
+ * \param[in] item A sample preview widget.
+ * \param[in] itemCount The number of items to pack into the grid.
+ * \param[out] ncols The number of columns in grid.
+ * \param[out] nrows The number of rows in grid.
+ */
+void PreviewHolder::calcGridSize( const Gtk::Widget* item, int itemCount, int& ncols, int& nrows )
+{
+ // Initially set all items in a horizontal row
+ ncols = itemCount;
+ nrows = 1;
+
+ if ( _anchor == SP_ANCHOR_SOUTH || _anchor == SP_ANCHOR_NORTH ) {
+ Gtk::Requisition req;
+ Gtk::Requisition req_natural;
+ _scroller->get_preferred_size(req, req_natural);
+ int currW = _scroller->get_width();
+ if ( currW > req.width ) {
+ req.width = currW;
+ }
+
+ if (_wrap && item != nullptr) {
+
+ // Get width of bar.
+ int width_scroller = _scroller->get_width();
+
+ // Get width of one item (must be visible).
+ int minimum_width_item = 0;
+ int natural_width_item = 0;
+ item->get_preferred_width(minimum_width_item, natural_width_item);
+
+ // Calculate columns and rows.
+ if (natural_width_item < 1) {
+ natural_width_item = 1;
+ }
+ ncols = width_scroller / natural_width_item - 1;
+
+ // On first run, scroller width is not set correct... so we need to fudge it:
+ if (ncols < 2) {
+ ncols = itemCount/2;
+ nrows = 2;
+ } else {
+ nrows = itemCount / ncols;
+ }
+ }
+ } else {
+ ncols = (_baseSize == UI::Widget::PREVIEW_SIZE_SMALL || _baseSize == UI::Widget::PREVIEW_SIZE_TINY) ?
+ COLUMNS_FOR_SMALL : COLUMNS_FOR_LARGE;
+ if ( _prefCols > 0 ) {
+ ncols = _prefCols;
+ }
+ nrows = (itemCount + (ncols - 1)) / ncols;
+ if ( nrows < 1 ) {
+ nrows = 1;
+ }
+ }
+}
+
+void PreviewHolder::rebuildUI()
+{
+ auto children = _insides->get_children();
+ for (auto child : children) {
+ _insides->remove(*child);
+ delete child;
+ }
+
+ _insides->set_column_spacing(0);
+ _insides->set_row_spacing(0);
+ if (_border == UI::Widget::BORDER_WIDE) {
+ _insides->set_column_spacing(1);
+ _insides->set_row_spacing(1);
+ }
+
+ switch (_view) {
+ case UI::Widget::VIEW_TYPE_LIST:
+ {
+ _insides->set_column_spacing(8);
+
+ for ( unsigned int i = 0; i < items.size(); i++ ) {
+ Gtk::Widget* label = Gtk::manage(items[i]->getPreview(UI::Widget::PREVIEW_STYLE_BLURB, _view, _baseSize, _ratio, _border));
+ //label->set_alignment(Gtk::ALIGN_LEFT, Gtk::ALIGN_CENTER);
+
+ Gtk::Widget* item = Gtk::manage(items[i]->getPreview(UI::Widget::PREVIEW_STYLE_PREVIEW, _view, _baseSize, _ratio, _border));
+
+ item->set_hexpand();
+ item->set_vexpand();
+ _insides->attach(*item, 0, i, 1, 1);
+
+ label->set_hexpand();
+ label->set_valign(Gtk::ALIGN_CENTER);
+ _insides->attach(*label, 1, i, 1, 1);
+ }
+ }
+ break;
+
+ case UI::Widget::VIEW_TYPE_GRID:
+ {
+ int col = 0;
+ int row = 0;
+ int ncols = 2;
+ int nrows = 1;
+
+ for ( unsigned int i = 0; i < items.size(); i++ ) {
+
+ // If this is the last row, flag so the previews can draw a bottom
+ UI::Widget::BorderStyle border = ((row == nrows -1) && (_border == UI::Widget::BORDER_SOLID)) ?
+ UI::Widget::BORDER_SOLID_LAST_ROW : _border;
+
+ Gtk::Widget* item = Gtk::manage(items[i]->getPreview(UI::Widget::PREVIEW_STYLE_PREVIEW, _view, _baseSize, _ratio, border));
+ item->set_hexpand();
+ item->set_vexpand();
+
+ if (i == 0) {
+ // We need one item shown before we can call calcGridSize()...
+ _insides->attach( *item, 0, 0, 1, 1);
+ _scroller->show_all_children();
+ calcGridSize( item, items.size(), ncols, nrows );
+ } else {
+ // We've already calculated grid size.
+ _insides->attach( *item, col, row, 1, 1);
+ }
+
+ if ( ++col >= ncols ) {
+ col = 0;
+ row++;
+ }
+ }
+ }
+ }
+
+ _scroller->show_all_children();
+}
+
+
+
+
+
+} //namespace UI
+} //namespace Inkscape
+
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/previewholder.h b/src/ui/previewholder.h
new file mode 100644
index 0000000..1dd35bf
--- /dev/null
+++ b/src/ui/previewholder.h
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifndef SEEN_PREVIEW_HOLDER_H
+#define SEEN_PREVIEW_HOLDER_H
+/*
+ * A simple interface for previewing representations.
+ * Used by Swatches
+ *
+ * Authors:
+ * Jon A. Cruz
+ *
+ * Copyright (C) 2005 Jon A. Cruz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/bin.h>
+#include <ui/widget/preview.h>
+
+namespace Gtk {
+class Grid;
+class ScrolledWindow;
+}
+
+#include "enums.h"
+
+namespace Inkscape {
+namespace UI {
+
+class Previewable;
+
+class PreviewHolder : public Gtk::Bin
+{
+public:
+ PreviewHolder();
+ ~PreviewHolder() override;
+
+ virtual void clear();
+ virtual void addPreview( Previewable* preview );
+ virtual void freezeUpdates();
+ virtual void thawUpdates();
+ virtual void setStyle(UI::Widget::PreviewSize size,
+ UI::Widget::ViewType view,
+ guint ratio,
+ UI::Widget::BorderStyle border);
+ virtual void setOrientation(SPAnchorType how);
+ virtual int getColumnPref() const { return _prefCols; }
+ virtual void setColumnPref( int cols );
+ virtual UI::Widget::PreviewSize getPreviewSize() const { return _baseSize; }
+ virtual UI::Widget::ViewType getPreviewType() const { return _view; }
+ virtual guint getPreviewRatio() const { return _ratio; }
+ virtual UI::Widget::BorderStyle getPreviewBorder() const { return _border; }
+ virtual void setWrap( bool wrap );
+ virtual bool getWrap() const { return _wrap; }
+
+protected:
+ bool on_scroll_event(GdkEventScroll*) override;
+
+private:
+ void rebuildUI();
+ void calcGridSize( const Gtk::Widget* item, int itemCount, int& ncols, int& nrows );
+
+ std::vector<Previewable*> items;
+ Gtk::ScrolledWindow *_scroller;
+ Gtk::Grid *_insides;
+
+ int _prefCols;
+ bool _updatesFrozen;
+ SPAnchorType _anchor;
+ UI::Widget::PreviewSize _baseSize;
+ guint _ratio;
+ UI::Widget::ViewType _view;
+ bool _wrap;
+ UI::Widget::BorderStyle _border;
+};
+
+} //namespace UI
+} //namespace Inkscape
+
+#endif // SEEN_PREVIEW_HOLDER_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/selected-color.cpp b/src/ui/selected-color.cpp
new file mode 100644
index 0000000..d8bbab1
--- /dev/null
+++ b/src/ui/selected-color.cpp
@@ -0,0 +1,159 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Color selected in color selector widget.
+ * This file was created during the refactoring of SPColorSelector
+ *//*
+ * Authors:
+ * bulia byak <buliabyak@users.sf.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Tomasz Boczkowski <penginsbacon@gmail.com>
+ *
+ * Copyright (C) 2014 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/ustring.h>
+#include <cmath>
+
+#include "svg/svg-icc-color.h"
+#include "ui/selected-color.h"
+
+namespace Inkscape {
+namespace UI {
+
+double const SelectedColor::_EPSILON = 1e-4;
+
+SelectedColor::SelectedColor()
+ : _color(0)
+ , _alpha(1.0)
+ , _held(false)
+ , _virgin(true)
+ , _updating(false)
+{
+
+}
+
+SelectedColor::~SelectedColor() = default;
+
+void SelectedColor::setColor(SPColor const &color)
+{
+ setColorAlpha( color, _alpha);
+}
+
+SPColor SelectedColor::color() const
+{
+ return _color;
+}
+
+void SelectedColor::setAlpha(gfloat alpha)
+{
+ g_return_if_fail( ( 0.0 <= alpha ) && ( alpha <= 1.0 ) );
+ setColorAlpha( _color, alpha);
+}
+
+gfloat SelectedColor::alpha() const
+{
+ return _alpha;
+}
+
+void SelectedColor::setValue(guint32 value)
+{
+ SPColor color(value);
+ gfloat alpha = SP_RGBA32_A_F(value);
+ setColorAlpha(color, alpha);
+}
+
+guint32 SelectedColor::value() const
+{
+ return color().toRGBA32(_alpha);
+}
+
+void SelectedColor::setColorAlpha(SPColor const &color, gfloat alpha, bool emit_signal)
+{
+#ifdef DUMP_CHANGE_INFO
+ g_message("SelectedColor::setColorAlpha( this=%p, %f, %f, %f, %s, %f, %s)", this, color.v.c[0], color.v.c[1], color.v.c[2], (color.icc?color.icc->colorProfile.c_str():"<null>"), alpha, (emit_signal?"YES":"no"));
+#endif
+ g_return_if_fail( ( 0.0 <= alpha ) && ( alpha <= 1.0 ) );
+
+ if (_updating) {
+ return;
+ }
+
+#ifdef DUMP_CHANGE_INFO
+ g_message("---- SelectedColor::setColorAlpha virgin:%s !close:%s alpha is:%s",
+ (_virgin?"YES":"no"),
+ (!color.isClose( _color, _EPSILON )?"YES":"no"),
+ ((fabs((_alpha) - (alpha)) >= _EPSILON )?"YES":"no")
+ );
+#endif
+
+ if ( _virgin || !color.isClose( _color, _EPSILON ) ||
+ (fabs((_alpha) - (alpha)) >= _EPSILON )) {
+
+ _virgin = false;
+
+ _color = color;
+ _alpha = alpha;
+
+ if (emit_signal)
+ {
+ _updating = true;
+ if (_held) {
+ signal_dragged.emit();
+ } else {
+ signal_changed.emit();
+ }
+ _updating = false;
+ }
+
+#ifdef DUMP_CHANGE_INFO
+ } else {
+ g_message("++++ SelectedColor::setColorAlpha color:%08x ==> _color:%08X isClose:%s", color.toRGBA32(alpha), _color.toRGBA32(_alpha),
+ (color.isClose( _color, _EPSILON )?"YES":"no"));
+#endif
+ }
+}
+
+void SelectedColor::colorAlpha(SPColor &color, gfloat &alpha) const {
+ color = _color;
+ alpha = _alpha;
+}
+
+void SelectedColor::setHeld(bool held) {
+ if (_updating) {
+ return;
+ }
+ bool grabbed = held && !_held;
+ bool released = !held && _held;
+
+ _held = held;
+
+ _updating = true;
+ if (grabbed) {
+ signal_grabbed.emit();
+ }
+
+ if (released) {
+ signal_released.emit();
+ // signal_changed.emit(); // TODO: signal_changed isn't emitted after dragging!
+ }
+ _updating = false;
+}
+
+void SelectedColor::preserveICC() {
+ _color.icc = _color.icc ? new SVGICCColor(*_color.icc) : nullptr;
+}
+
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/selected-color.h b/src/ui/selected-color.h
new file mode 100644
index 0000000..9543111
--- /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 setHeld(bool held);
+
+ void preserveICC();
+
+ sigc::signal<void> signal_grabbed;
+ sigc::signal<void> signal_dragged;
+ sigc::signal<void> signal_released;
+ sigc::signal<void> signal_changed;
+
+private:
+ SPColor _color;
+ /**
+ * Color alpha value guaranteed to be in [0, 1].
+ */
+ gfloat _alpha;
+
+ bool _held;
+ /**
+ * This flag is true if no color is set yet
+ */
+ bool _virgin;
+
+ bool _updating;
+
+ static double const _EPSILON;
+};
+
+class ColorSelectorFactory {
+public:
+ virtual ~ColorSelectorFactory() = default;
+
+ virtual Gtk::Widget* createWidget(SelectedColor &color) const = 0;
+ virtual Glib::ustring modeName() const = 0;
+};
+
+}
+}
+
+#endif
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/shape-editor-knotholders.cpp b/src/ui/shape-editor-knotholders.cpp
new file mode 100644
index 0000000..a56612f
--- /dev/null
+++ b/src/ui/shape-editor-knotholders.cpp
@@ -0,0 +1,2610 @@
+// 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);
+
+ 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 (dynamic_cast<SPRect *>(item)) {
+ knotholder = new RectKnotHolder(desktop, item, nullptr);
+ } else if (dynamic_cast<SPBox3D *>(item)) {
+ knotholder = new Box3DKnotHolder(desktop, item, nullptr);
+ } else if (dynamic_cast<SPMarker *>(item)) {
+ knotholder = new MarkerKnotHolder(desktop, item, nullptr, edit_rotation, edit_marker_mode);
+ } else if (dynamic_cast<SPGenericEllipse *>(item)) {
+ knotholder = new ArcKnotHolder(desktop, item, nullptr);
+ } else if (dynamic_cast<SPStar *>(item)) {
+ knotholder = new StarKnotHolder(desktop, item, nullptr);
+ } else if (dynamic_cast<SPSpiral *>(item)) {
+ knotholder = new SpiralKnotHolder(desktop, item, nullptr);
+ } else if (dynamic_cast<SPOffset *>(item)) {
+ knotholder = new OffsetKnotHolder(desktop, item, nullptr);
+ } else if (dynamic_cast<SPText *>(item)) {
+ SPText *text = dynamic_cast<SPText *>(item);
+
+ // Do not allow conversion to 'inline-size' wrapped text if on path!
+ // <textPath> might not be first child if <title> or <desc> is present.
+ bool is_on_path = false;
+ for (auto child : text->childList(false)) {
+ if (dynamic_cast<SPTextPath *>(child)) is_on_path = true;
+ }
+ if (!is_on_path) {
+ knotholder = new TextKnotHolder(desktop, item, nullptr);
+ }
+ } else {
+ SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(item);
+ if (flowtext && flowtext->has_internal_frame()) {
+ knotholder = new FlowtextKnotHolder(desktop, flowtext->get_frame(nullptr), nullptr);
+ } else if ((item->style->fill.isPaintserver() && dynamic_cast<SPPattern *>(item->style->getFillPaintServer())) ||
+ (item->style->stroke.isPaintserver() && dynamic_cast<SPPattern *>(item->style->getStrokePaintServer()))) {
+ knotholder = new KnotHolder(desktop, item, nullptr);
+ knotholder->add_pattern_knotholder();
+ }
+ }
+ if (!knotholder) knotholder = new KnotHolder(desktop, item, nullptr);
+ knotholder->add_filter_knotholder();
+
+ return knotholder;
+}
+
+KnotHolder *createLPEKnotHolder(SPItem *item, SPDesktop *desktop)
+{
+ KnotHolder *knotholder = nullptr;
+
+ SPLPEItem *lpe = dynamic_cast<SPLPEItem *>(item);
+ if (lpe &&
+ lpe->getCurrentLPE() &&
+ lpe->getCurrentLPE()->isVisible() &&
+ lpe->getCurrentLPE()->providesKnotholder()) {
+ knotholder = sp_lpe_knot_holder(lpe, desktop);
+ }
+ return knotholder;
+}
+
+}
+} // namespace Inkscape
+
+/* SPRect */
+
+/* handle for horizontal rounding radius */
+class RectKnotHolderEntityRX : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_click(unsigned int state) override;
+};
+
+/* handle for vertical rounding radius */
+class RectKnotHolderEntityRY : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_click(unsigned int state) override;
+};
+
+/* handle for width/height adjustment */
+class RectKnotHolderEntityWH : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+
+protected:
+ void set_internal(Geom::Point const &p, Geom::Point const &origin, unsigned int state);
+};
+
+/* handle for x/y adjustment */
+class RectKnotHolderEntityXY : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+/* handle for position */
+class RectKnotHolderEntityCenter : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+Geom::Point
+RectKnotHolderEntityRX::knot_get() const
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ return Geom::Point(rect->x.computed + rect->width.computed - rect->rx.computed, rect->y.computed);
+}
+
+void
+RectKnotHolderEntityRX::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ //In general we cannot just snap this radius to an arbitrary point, as we have only a single
+ //degree of freedom. For snapping to an arbitrary point we need two DOF. If we're going to snap
+ //the radius then we should have a constrained snap. snap_knot_position() is unconstrained
+ Geom::Point const s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(Geom::Point(rect->x.computed + rect->width.computed, rect->y.computed), Geom::Point(-1, 0)), state);
+
+ if (state & GDK_CONTROL_MASK) {
+ gdouble temp = MIN(rect->height.computed, rect->width.computed) / 2.0;
+ rect->rx = rect->ry = CLAMP(rect->x.computed + rect->width.computed - s[Geom::X], 0.0, temp);
+ } else {
+ rect->rx = CLAMP(rect->x.computed + rect->width.computed - s[Geom::X], 0.0, rect->width.computed / 2.0);
+ }
+
+ update_knot();
+
+ rect->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+void
+RectKnotHolderEntityRX::knot_click(unsigned int state)
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ if (state & GDK_SHIFT_MASK) {
+ /* remove rounding from rectangle */
+ rect->getRepr()->removeAttribute("rx");
+ rect->getRepr()->removeAttribute("ry");
+ } else if (state & GDK_CONTROL_MASK) {
+ /* Ctrl-click sets the vertical rounding to be the same as the horizontal */
+ rect->getRepr()->setAttribute("ry", rect->getRepr()->attribute("rx"));
+ }
+
+}
+
+Geom::Point
+RectKnotHolderEntityRY::knot_get() const
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ return Geom::Point(rect->x.computed + rect->width.computed, rect->y.computed + rect->ry.computed);
+}
+
+void
+RectKnotHolderEntityRY::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ //In general we cannot just snap this radius to an arbitrary point, as we have only a single
+ //degree of freedom. For snapping to an arbitrary point we need two DOF. If we're going to snap
+ //the radius then we should have a constrained snap. snap_knot_position() is unconstrained
+ Geom::Point const s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(Geom::Point(rect->x.computed + rect->width.computed, rect->y.computed), Geom::Point(0, 1)), state);
+
+ if (state & GDK_CONTROL_MASK) { // When holding control then rx will be kept equal to ry,
+ // resulting in a perfect circle (and not an ellipse)
+ gdouble temp = MIN(rect->height.computed, rect->width.computed) / 2.0;
+ rect->rx = rect->ry = CLAMP(s[Geom::Y] - rect->y.computed, 0.0, temp);
+ } else {
+ if (!rect->rx._set || rect->rx.computed == 0) {
+ rect->ry = CLAMP(s[Geom::Y] - rect->y.computed,
+ 0.0,
+ MIN(rect->height.computed / 2.0, rect->width.computed / 2.0));
+ } else {
+ rect->ry = CLAMP(s[Geom::Y] - rect->y.computed,
+ 0.0,
+ rect->height.computed / 2.0);
+ }
+ }
+
+ update_knot();
+
+ rect->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+void
+RectKnotHolderEntityRY::knot_click(unsigned int state)
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ if (state & GDK_SHIFT_MASK) {
+ /* remove rounding */
+ rect->getRepr()->removeAttribute("rx");
+ rect->getRepr()->removeAttribute("ry");
+ } else if (state & GDK_CONTROL_MASK) {
+ /* Ctrl-click sets the vertical rounding to be the same as the horizontal */
+ rect->getRepr()->setAttribute("rx", rect->getRepr()->attribute("ry"));
+ }
+}
+
+#define SGN(x) ((x)>0?1:((x)<0?-1:0))
+
+static void sp_rect_clamp_radii(SPRect *rect)
+{
+ // clamp rounding radii so that they do not exceed width/height
+ if (2 * rect->rx.computed > rect->width.computed) {
+ rect->rx = 0.5 * rect->width.computed;
+ }
+ if (2 * rect->ry.computed > rect->height.computed) {
+ rect->ry = 0.5 * rect->height.computed;
+ }
+}
+
+Geom::Point
+RectKnotHolderEntityWH::knot_get() const
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ return Geom::Point(rect->x.computed + rect->width.computed, rect->y.computed + rect->height.computed);
+}
+
+void
+RectKnotHolderEntityWH::set_internal(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ Geom::Point s = p;
+
+ if (state & GDK_CONTROL_MASK) {
+ // original width/height when drag started
+ gdouble const w_orig = (origin[Geom::X] - rect->x.computed);
+ gdouble const h_orig = (origin[Geom::Y] - rect->y.computed);
+
+ //original ratio
+ gdouble ratio = (w_orig / h_orig);
+
+ // mouse displacement since drag started
+ gdouble minx = p[Geom::X] - origin[Geom::X];
+ gdouble miny = p[Geom::Y] - origin[Geom::Y];
+
+ Geom::Point p_handle(rect->x.computed + rect->width.computed, rect->y.computed + rect->height.computed);
+
+ if (fabs(minx) > fabs(miny)) {
+ // snap to horizontal or diagonal
+ if (minx != 0 && fabs(miny/minx) > 0.5 * 1/ratio && (SGN(minx) == SGN(miny))) {
+ // closer to the diagonal and in same-sign quarters, change both using ratio
+ s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(-ratio, -1)), state);
+ minx = s[Geom::X] - origin[Geom::X];
+ // Dead assignment: Value stored to 'miny' is never read
+ //miny = s[Geom::Y] - origin[Geom::Y];
+ rect->height = MAX(h_orig + minx / ratio, 0);
+ } else {
+ // closer to the horizontal, change only width, height is h_orig
+ s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(-1, 0)), state);
+ minx = s[Geom::X] - origin[Geom::X];
+ // Dead assignment: Value stored to 'miny' is never read
+ //miny = s[Geom::Y] - origin[Geom::Y];
+ rect->height = MAX(h_orig, 0);
+ }
+ rect->width = MAX(w_orig + minx, 0);
+
+ } else {
+ // snap to vertical or diagonal
+ if (miny != 0 && fabs(minx/miny) > 0.5 * ratio && (SGN(minx) == SGN(miny))) {
+ // closer to the diagonal and in same-sign quarters, change both using ratio
+ s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(-ratio, -1)), state);
+ // Dead assignment: Value stored to 'minx' is never read
+ //minx = s[Geom::X] - origin[Geom::X];
+ miny = s[Geom::Y] - origin[Geom::Y];
+ rect->width = MAX(w_orig + miny * ratio, 0);
+ } else {
+ // closer to the vertical, change only height, width is w_orig
+ s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(0, -1)), state);
+ // Dead assignment: Value stored to 'minx' is never read
+ //minx = s[Geom::X] - origin[Geom::X];
+ miny = s[Geom::Y] - origin[Geom::Y];
+ rect->width = MAX(w_orig, 0);
+ }
+ rect->height = MAX(h_orig + miny, 0);
+
+ }
+
+ } else {
+ // move freely
+ s = snap_knot_position(p, state);
+ rect->width = MAX(s[Geom::X] - rect->x.computed, 0);
+ rect->height = MAX(s[Geom::Y] - rect->y.computed, 0);
+ }
+
+ sp_rect_clamp_radii(rect);
+
+ rect->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+void
+RectKnotHolderEntityWH::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ set_internal(p, origin, state);
+ update_knot();
+}
+
+Geom::Point
+RectKnotHolderEntityXY::knot_get() const
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ return Geom::Point(rect->x.computed, rect->y.computed);
+}
+
+void
+RectKnotHolderEntityXY::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ // opposite corner (unmoved)
+ gdouble opposite_x = (rect->x.computed + rect->width.computed);
+ gdouble opposite_y = (rect->y.computed + rect->height.computed);
+
+ // original width/height when drag started
+ gdouble w_orig = opposite_x - origin[Geom::X];
+ gdouble h_orig = opposite_y - origin[Geom::Y];
+
+ Geom::Point s = p;
+ Geom::Point p_handle(rect->x.computed, rect->y.computed);
+
+ // mouse displacement since drag started
+ gdouble minx = p[Geom::X] - origin[Geom::X];
+ gdouble miny = p[Geom::Y] - origin[Geom::Y];
+
+ if (state & GDK_CONTROL_MASK) {
+ //original ratio
+ gdouble ratio = (w_orig / h_orig);
+
+ if (fabs(minx) > fabs(miny)) {
+ // snap to horizontal or diagonal
+ if (minx != 0 && fabs(miny/minx) > 0.5 * 1/ratio && (SGN(minx) == SGN(miny))) {
+ // closer to the diagonal and in same-sign quarters, change both using ratio
+ s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(-ratio, -1)), state);
+ minx = s[Geom::X] - origin[Geom::X];
+ // Dead assignment: Value stored to 'miny' is never read
+ //miny = s[Geom::Y] - origin[Geom::Y];
+ rect->y = MIN(origin[Geom::Y] + minx / ratio, opposite_y);
+ rect->height = MAX(h_orig - minx / ratio, 0);
+ } else {
+ // closer to the horizontal, change only width, height is h_orig
+ s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(-1, 0)), state);
+ minx = s[Geom::X] - origin[Geom::X];
+ // Dead assignment: Value stored to 'miny' is never read
+ //miny = s[Geom::Y] - origin[Geom::Y];
+ rect->y = MIN(origin[Geom::Y], opposite_y);
+ rect->height = MAX(h_orig, 0);
+ }
+ rect->x = MIN(s[Geom::X], opposite_x);
+ rect->width = MAX(w_orig - minx, 0);
+ } else {
+ // snap to vertical or diagonal
+ if (miny != 0 && fabs(minx/miny) > 0.5 *ratio && (SGN(minx) == SGN(miny))) {
+ // closer to the diagonal and in same-sign quarters, change both using ratio
+ s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(-ratio, -1)), state);
+ // Dead assignment: Value stored to 'minx' is never read
+ //minx = s[Geom::X] - origin[Geom::X];
+ miny = s[Geom::Y] - origin[Geom::Y];
+ rect->x = MIN(origin[Geom::X] + miny * ratio, opposite_x);
+ rect->width = MAX(w_orig - miny * ratio, 0);
+ } else {
+ // closer to the vertical, change only height, width is w_orig
+ s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(0, -1)), state);
+ // Dead assignment: Value stored to 'minx' is never read
+ //minx = s[Geom::X] - origin[Geom::X];
+ miny = s[Geom::Y] - origin[Geom::Y];
+ rect->x = MIN(origin[Geom::X], opposite_x);
+ rect->width = MAX(w_orig, 0);
+ }
+ rect->y = MIN(s[Geom::Y], opposite_y);
+ rect->height = MAX(h_orig - miny, 0);
+ }
+
+ } else {
+ // move freely
+ s = snap_knot_position(p, state);
+ minx = s[Geom::X] - origin[Geom::X];
+ miny = s[Geom::Y] - origin[Geom::Y];
+
+ rect->x = MIN(s[Geom::X], opposite_x);
+ rect->y = MIN(s[Geom::Y], opposite_y);
+ rect->width = MAX(w_orig - minx, 0);
+ rect->height = MAX(h_orig - miny, 0);
+ }
+
+ sp_rect_clamp_radii(rect);
+
+ update_knot();
+
+ rect->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+Geom::Point
+RectKnotHolderEntityCenter::knot_get() const
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ return Geom::Point(rect->x.computed + (rect->width.computed / 2.), rect->y.computed + (rect->height.computed / 2.));
+}
+
+void
+RectKnotHolderEntityCenter::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ Geom::Point const s = snap_knot_position(p, state);
+
+ rect->x = s[Geom::X] - (rect->width.computed / 2.);
+ rect->y = s[Geom::Y] - (rect->height.computed / 2.);
+
+ // No need to call sp_rect_clamp_radii(): width and height haven't changed.
+ // No need to call update_knot(): the knot is set directly by the user.
+
+ rect->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+RectKnotHolder::RectKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) :
+ KnotHolder(desktop, item, relhandler)
+{
+ RectKnotHolderEntityRX *entity_rx = new RectKnotHolderEntityRX();
+ RectKnotHolderEntityRY *entity_ry = new RectKnotHolderEntityRY();
+ RectKnotHolderEntityWH *entity_wh = new RectKnotHolderEntityWH();
+ RectKnotHolderEntityXY *entity_xy = new RectKnotHolderEntityXY();
+ RectKnotHolderEntityCenter *entity_center = new RectKnotHolderEntityCenter();
+
+ entity_rx->create(desktop, item, this, Inkscape::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
+{
+ SPBox3D *box = dynamic_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);
+ SPBox3D *box = dynamic_cast<SPBox3D *>(item);
+ g_assert(box != nullptr);
+ Geom::Affine const i2dt (item->i2dt_affine ());
+
+ Box3D::Axis movement;
+ if ((knot_id < 4) != (state & GDK_SHIFT_MASK)) {
+ movement = Box3D::XY;
+ } else {
+ movement = Box3D::Z;
+ }
+
+ 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
+{
+ SPBox3D *box = dynamic_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);
+
+ SPBox3D *box = dynamic_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){
+
+ SPMarker *sp_marker = dynamic_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){
+
+ SPMarker *sp_marker = dynamic_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){
+
+ SPMarker *sp_marker = dynamic_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){
+ SPMarker *sp_marker = dynamic_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) {
+ SPItem *item = dynamic_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
+{
+ SPMarker *sp_marker = dynamic_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)
+{
+ SPMarker *sp_marker = dynamic_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
+{
+ SPMarker *sp_marker = dynamic_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) {
+ SPMarker *sp_marker = dynamic_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)
+{
+ SPMarker *sp_marker = dynamic_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
+{
+ SPMarker *sp_marker = dynamic_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) {
+
+ SPMarker *sp_marker = dynamic_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)
+{
+ SPMarker *sp_marker = dynamic_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
+{
+ SPMarker *sp_marker = dynamic_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
+{
+ SPMarker *sp_marker = dynamic_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);
+
+ SPGenericEllipse *arc = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(arc != nullptr);
+
+ gint side = sp_genericellipse_side(arc, p);
+ if(side != 0) { arc->setArcType( (side == -1) ?
+ SP_GENERIC_ELLIPSE_ARC_TYPE_SLICE :
+ SP_GENERIC_ELLIPSE_ARC_TYPE_ARC); }
+
+ Geom::Point delta = p - Geom::Point(arc->cx.computed, arc->cy.computed);
+ Geom::Scale sc(arc->rx.computed, arc->ry.computed);
+
+ double offset = arc->start - atan2(delta * sc.inverse());
+ arc->start -= offset;
+
+ if ((state & GDK_CONTROL_MASK) && snaps) {
+ double snaps_radian = M_PI/snaps;
+ arc->start = std::round(arc->start/snaps_radian) * snaps_radian;
+ }
+ if (state & GDK_SHIFT_MASK) {
+ arc->end -= offset;
+ }
+
+ arc->normalize();
+ arc->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+Geom::Point
+ArcKnotHolderEntityStart::knot_get() const
+{
+ SPGenericEllipse const *ge = dynamic_cast<SPGenericEllipse const *>(item);
+ g_assert(ge != nullptr);
+
+ return ge->getPointAtAngle(ge->start);
+}
+
+void
+ArcKnotHolderEntityStart::knot_click(unsigned int state)
+{
+ SPGenericEllipse *ge = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(ge != nullptr);
+
+ if (state & GDK_SHIFT_MASK) {
+ ge->end = ge->start = 0;
+ ge->updateRepr();
+ }
+}
+
+void
+ArcKnotHolderEntityEnd::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ int snaps = Inkscape::Preferences::get()->getInt("/options/rotationsnapsperpi/value", 12);
+
+ SPGenericEllipse *arc = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(arc != nullptr);
+
+ gint side = sp_genericellipse_side(arc, p);
+ if(side != 0) { arc->setArcType( (side == -1) ?
+ SP_GENERIC_ELLIPSE_ARC_TYPE_SLICE :
+ SP_GENERIC_ELLIPSE_ARC_TYPE_ARC); }
+
+ Geom::Point delta = p - Geom::Point(arc->cx.computed, arc->cy.computed);
+ Geom::Scale sc(arc->rx.computed, arc->ry.computed);
+
+ double offset = arc->end - atan2(delta * sc.inverse());
+ arc->end -= offset;
+
+ if ((state & GDK_CONTROL_MASK) && snaps) {
+ double snaps_radian = M_PI/snaps;
+ arc->end = std::round(arc->end/snaps_radian) * snaps_radian;
+ }
+ if (state & GDK_SHIFT_MASK) {
+ arc->start -= offset;
+ }
+
+ arc->normalize();
+ arc->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+Geom::Point
+ArcKnotHolderEntityEnd::knot_get() const
+{
+ SPGenericEllipse const *ge = dynamic_cast<SPGenericEllipse const *>(item);
+ g_assert(ge != nullptr);
+
+ return ge->getPointAtAngle(ge->end);
+}
+
+
+void
+ArcKnotHolderEntityEnd::knot_click(unsigned int state)
+{
+ SPGenericEllipse *ge = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(ge != nullptr);
+
+ if (state & GDK_SHIFT_MASK) {
+ ge->end = ge->start = 0;
+ ge->updateRepr();
+ }
+}
+
+
+void
+ArcKnotHolderEntityRX::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPGenericEllipse *ge = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(ge != nullptr);
+
+ Geom::Point const s = snap_knot_position(p, state);
+
+ ge->rx = fabs( ge->cx.computed - s[Geom::X] );
+
+ if ( state & GDK_CONTROL_MASK ) {
+ ge->ry = ge->rx.computed;
+ }
+
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+Geom::Point
+ArcKnotHolderEntityRX::knot_get() const
+{
+ SPGenericEllipse const *ge = dynamic_cast<SPGenericEllipse const *>(item);
+ g_assert(ge != nullptr);
+
+ return (Geom::Point(ge->cx.computed, ge->cy.computed) - Geom::Point(ge->rx.computed, 0));
+}
+
+void
+ArcKnotHolderEntityRX::knot_click(unsigned int state)
+{
+ SPGenericEllipse *ge = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(ge != nullptr);
+
+ if (state & GDK_CONTROL_MASK) {
+ ge->ry = ge->rx.computed;
+ ge->updateRepr();
+ }
+}
+
+void
+ArcKnotHolderEntityRY::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPGenericEllipse *ge = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(ge != nullptr);
+
+ Geom::Point const s = snap_knot_position(p, state);
+
+ ge->ry = fabs( ge->cy.computed - s[Geom::Y] );
+
+ if ( state & GDK_CONTROL_MASK ) {
+ ge->rx = ge->ry.computed;
+ }
+
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+Geom::Point
+ArcKnotHolderEntityRY::knot_get() const
+{
+ SPGenericEllipse const *ge = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(ge != nullptr);
+
+ return (Geom::Point(ge->cx.computed, ge->cy.computed) - Geom::Point(0, ge->ry.computed));
+}
+
+void
+ArcKnotHolderEntityRY::knot_click(unsigned int state)
+{
+ SPGenericEllipse *ge = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(ge != nullptr);
+
+ if (state & GDK_CONTROL_MASK) {
+ ge->rx = ge->ry.computed;
+ ge->updateRepr();
+ }
+}
+
+void
+ArcKnotHolderEntityCenter::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPGenericEllipse *ge = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(ge != nullptr);
+
+ Geom::Point const s = snap_knot_position(p, state);
+
+ ge->cx = s[Geom::X];
+ ge->cy = s[Geom::Y];
+
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+Geom::Point
+ArcKnotHolderEntityCenter::knot_get() const
+{
+ SPGenericEllipse const *ge = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(ge != nullptr);
+
+ return Geom::Point(ge->cx.computed, ge->cy.computed);
+}
+
+
+ArcKnotHolder::ArcKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) :
+ KnotHolder(desktop, item, relhandler)
+{
+ ArcKnotHolderEntityRX *entity_rx = new ArcKnotHolderEntityRX();
+ ArcKnotHolderEntityRY *entity_ry = new ArcKnotHolderEntityRY();
+ ArcKnotHolderEntityStart *entity_start = new ArcKnotHolderEntityStart();
+ ArcKnotHolderEntityEnd *entity_end = new ArcKnotHolderEntityEnd();
+ ArcKnotHolderEntityCenter *entity_center = new ArcKnotHolderEntityCenter();
+
+ entity_rx->create(desktop, item, this, Inkscape::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)
+{
+ SPStar *star = dynamic_cast<SPStar *>(item);
+ g_assert(star != nullptr);
+
+ Geom::Point const s = snap_knot_position(p, state);
+
+ Geom::Point d = s - star->center;
+
+ double arg1 = atan2(d);
+ double darg1 = arg1 - star->arg[0];
+
+ if (state & GDK_MOD1_MASK) {
+ star->randomized = darg1/(star->arg[0] - star->arg[1]);
+ } else if (state & GDK_SHIFT_MASK) {
+ star->rounded = darg1/(star->arg[0] - star->arg[1]);
+ } else if (state & GDK_CONTROL_MASK) {
+ star->r[0] = L2(d);
+ } else {
+ star->r[0] = L2(d);
+ star->arg[0] = arg1;
+ star->arg[1] += darg1;
+ }
+ star->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+void
+StarKnotHolderEntity2::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPStar *star = dynamic_cast<SPStar *>(item);
+ g_assert(star != nullptr);
+
+ Geom::Point const s = snap_knot_position(p, state);
+
+ if (star->flatsided == false) {
+ Geom::Point d = s - star->center;
+
+ double arg1 = atan2(d);
+ double darg1 = arg1 - star->arg[1];
+
+ if (state & GDK_MOD1_MASK) {
+ star->randomized = darg1/(star->arg[0] - star->arg[1]);
+ } else if (state & GDK_SHIFT_MASK) {
+ star->rounded = fabs(darg1/(star->arg[0] - star->arg[1]));
+ } else if (state & GDK_CONTROL_MASK) {
+ star->r[1] = L2(d);
+ star->arg[1] = star->arg[0] + M_PI / star->sides;
+ }
+ else {
+ star->r[1] = L2(d);
+ star->arg[1] = atan2(d);
+ }
+ star->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ }
+}
+
+void
+StarKnotHolderEntityCenter::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPStar *star = dynamic_cast<SPStar *>(item);
+ g_assert(star != nullptr);
+
+ star->center = snap_knot_position(p, state);
+
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+Geom::Point
+StarKnotHolderEntity1::knot_get() const
+{
+ g_assert(item != nullptr);
+
+ SPStar const *star = dynamic_cast<SPStar const *>(item);
+ g_assert(star != nullptr);
+
+ return sp_star_get_xy(star, SP_STAR_POINT_KNOT1, 0);
+
+}
+
+Geom::Point
+StarKnotHolderEntity2::knot_get() const
+{
+ g_assert(item != nullptr);
+
+ SPStar const *star = dynamic_cast<SPStar const *>(item);
+ g_assert(star != nullptr);
+
+ return sp_star_get_xy(star, SP_STAR_POINT_KNOT2, 0);
+}
+
+Geom::Point
+StarKnotHolderEntityCenter::knot_get() const
+{
+ g_assert(item != nullptr);
+
+ SPStar const *star = dynamic_cast<SPStar const *>(item);
+ g_assert(star != nullptr);
+
+ return star->center;
+}
+
+static void
+sp_star_knot_click(SPItem *item, unsigned int state)
+{
+ SPStar *star = dynamic_cast<SPStar *>(item);
+ g_assert(star != nullptr);
+
+ if (state & GDK_MOD1_MASK) {
+ star->randomized = 0;
+ star->updateRepr();
+ } else if (state & GDK_SHIFT_MASK) {
+ star->rounded = 0;
+ star->updateRepr();
+ } else if (state & GDK_CONTROL_MASK) {
+ star->arg[1] = star->arg[0] + M_PI / star->sides;
+ star->updateRepr();
+ }
+}
+
+void
+StarKnotHolderEntity1::knot_click(unsigned int state)
+{
+ sp_star_knot_click(item, state);
+}
+
+void
+StarKnotHolderEntity2::knot_click(unsigned int state)
+{
+ sp_star_knot_click(item, state);
+}
+
+StarKnotHolder::StarKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) :
+ KnotHolder(desktop, item, relhandler)
+{
+ SPStar *star = dynamic_cast<SPStar *>(item);
+ g_assert(item != nullptr);
+
+ StarKnotHolderEntity1 *entity1 = new StarKnotHolderEntity1();
+ entity1->create(desktop, item, this, Inkscape::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);
+
+ SPSpiral *spiral = dynamic_cast<SPSpiral *>(item);
+ g_assert(spiral != nullptr);
+
+ gdouble dx = p[Geom::X] - spiral->cx;
+ gdouble dy = p[Geom::Y] - spiral->cy;
+
+ gdouble moved_y = p[Geom::Y] - origin[Geom::Y];
+
+ if (state & GDK_MOD1_MASK) {
+ // adjust divergence by vertical drag, relative to rad
+ if (spiral->rad > 0) {
+ double exp_delta = 0.1*moved_y/(spiral->rad); // arbitrary multiplier to slow it down
+ spiral->exp += exp_delta;
+ if (spiral->exp < 1e-3)
+ spiral->exp = 1e-3;
+ }
+ } else {
+ // roll/unroll from inside
+ gdouble arg_t0;
+ spiral->getPolar(spiral->t0, nullptr, &arg_t0);
+
+ gdouble arg_tmp = atan2(dy, dx) - arg_t0;
+ gdouble arg_t0_new = arg_tmp - floor((arg_tmp+M_PI)/(2.0*M_PI))*2.0*M_PI + arg_t0;
+ spiral->t0 = (arg_t0_new - spiral->arg) / (2.0*M_PI*spiral->revo);
+
+ /* round inner arg per PI/snaps, if CTRL is pressed */
+ if ( ( state & GDK_CONTROL_MASK )
+ && ( fabs(spiral->revo) > SP_EPSILON_2 )
+ && ( snaps != 0 ) ) {
+ gdouble arg = 2.0*M_PI*spiral->revo*spiral->t0 + spiral->arg;
+ double snaps_radian = M_PI/snaps;
+ spiral->t0 = (std::round(arg/snaps_radian)*snaps_radian - spiral->arg)/(2.0*M_PI*spiral->revo);
+ }
+
+ spiral->t0 = CLAMP(spiral->t0, 0.0, 0.999);
+ }
+
+ spiral->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+/*
+ * set attributes via outer (t=1) knot point:
+ * [default] increase/decrease revolution factor
+ * [control] constrain inner arg to round per PI/4
+ */
+void
+SpiralKnotHolderEntityOuter::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12);
+
+ SPSpiral *spiral = dynamic_cast<SPSpiral *>(item);
+ g_assert(spiral != nullptr);
+
+ gdouble dx = p[Geom::X] - spiral->cx;
+ gdouble dy = p[Geom::Y] - spiral->cy;
+
+ if (state & GDK_SHIFT_MASK) { // rotate without roll/unroll
+ spiral->arg = atan2(dy, dx) - 2.0*M_PI*spiral->revo;
+ if (!(state & GDK_MOD1_MASK)) {
+ // if alt not pressed, change also rad; otherwise it is locked
+ spiral->rad = MAX(hypot(dx, dy), 0.001);
+ }
+ if ( ( state & GDK_CONTROL_MASK ) && snaps ) {
+ double snaps_radian = M_PI/snaps;
+ spiral->arg = std::round(spiral->arg/snaps_radian) * snaps_radian;
+ }
+ } else { // roll/unroll
+ // arg of the spiral outer end
+ double arg_1;
+ spiral->getPolar(1, nullptr, &arg_1);
+
+ // its fractional part after the whole turns are subtracted
+ static double _2PI = 2.0 * M_PI;
+ double arg_r = arg_1 - std::round(arg_1/_2PI) * _2PI;
+
+ // arg of the mouse point relative to spiral center
+ double mouse_angle = atan2(dy, dx);
+ if (mouse_angle < 0)
+ mouse_angle += _2PI;
+
+ // snap if ctrl
+ if ( ( state & GDK_CONTROL_MASK ) && snaps ) {
+ double snaps_radian = M_PI/snaps;
+ mouse_angle = std::round(mouse_angle/snaps_radian) * snaps_radian;
+ }
+
+ // by how much we want to rotate the outer point
+ double diff = mouse_angle - arg_r;
+ if (diff > M_PI)
+ diff -= _2PI;
+ else if (diff < -M_PI)
+ diff += _2PI;
+
+ // calculate the new rad;
+ // the value of t corresponding to the angle arg_1 + diff:
+ double t_temp = ((arg_1 + diff) - spiral->arg)/(_2PI*spiral->revo);
+ // the rad at that t:
+ double rad_new = 0;
+ if (t_temp > spiral->t0)
+ spiral->getPolar(t_temp, &rad_new, nullptr);
+
+ // change the revo (converting diff from radians to the number of turns)
+ spiral->revo += diff/(2*M_PI);
+ if (spiral->revo < 1e-3)
+ spiral->revo = 1e-3;
+
+ // if alt not pressed and the values are sane, change the rad
+ if (!(state & GDK_MOD1_MASK) && rad_new > 1e-3 && rad_new/spiral->rad < 2) {
+ // adjust t0 too so that the inner point stays unmoved
+ double r0;
+ spiral->getPolar(spiral->t0, &r0, nullptr);
+ spiral->rad = rad_new;
+ spiral->t0 = pow(r0 / spiral->rad, 1.0/spiral->exp);
+ }
+ if (!std::isfinite(spiral->t0)) spiral->t0 = 0.0;
+ spiral->t0 = CLAMP(spiral->t0, 0.0, 0.999);
+ }
+
+ spiral->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+void
+SpiralKnotHolderEntityCenter::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPSpiral *spiral = dynamic_cast<SPSpiral *>(item);
+ g_assert(spiral != nullptr);
+
+ Geom::Point const s = snap_knot_position(p, state);
+
+ spiral->cx = s[Geom::X];
+ spiral->cy = s[Geom::Y];
+
+ spiral->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+Geom::Point
+SpiralKnotHolderEntityInner::knot_get() const
+{
+ SPSpiral const *spiral = dynamic_cast<SPSpiral const *>(item);
+ g_assert(spiral != nullptr);
+
+ return spiral->getXY(spiral->t0);
+}
+
+Geom::Point
+SpiralKnotHolderEntityOuter::knot_get() const
+{
+ SPSpiral const *spiral = dynamic_cast<SPSpiral const *>(item);
+ g_assert(spiral != nullptr);
+
+ return spiral->getXY(1.0);
+}
+
+Geom::Point
+SpiralKnotHolderEntityCenter::knot_get() const
+{
+ SPSpiral const *spiral = dynamic_cast<SPSpiral const *>(item);
+ g_assert(spiral != nullptr);
+
+ return Geom::Point(spiral->cx, spiral->cy);
+}
+
+void
+SpiralKnotHolderEntityInner::knot_click(unsigned int state)
+{
+ SPSpiral *spiral = dynamic_cast<SPSpiral *>(item);
+ g_assert(spiral != nullptr);
+
+ if (state & GDK_MOD1_MASK) {
+ spiral->exp = 1;
+ spiral->updateRepr();
+ } else if (state & GDK_SHIFT_MASK) {
+ spiral->t0 = 0;
+ spiral->updateRepr();
+ }
+}
+
+SpiralKnotHolder::SpiralKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) :
+ KnotHolder(desktop, item, relhandler)
+{
+ SpiralKnotHolderEntityCenter *entity_center = new SpiralKnotHolderEntityCenter();
+ SpiralKnotHolderEntityInner *entity_inner = new SpiralKnotHolderEntityInner();
+ SpiralKnotHolderEntityOuter *entity_outer = new SpiralKnotHolderEntityOuter();
+
+ // NOTE: entity_central and entity_inner can overlap.
+ //
+ // In that case it would be a problem if the center control point was ON
+ // TOP because it would steal the mouse focus and the user would loose the
+ // ability to access the inner control point using only the mouse.
+ //
+ // However if the inner control point is ON TOP, taking focus, the
+ // situation is a lot better: the user can still move the inner control
+ // point with the mouse to regain access to the center control point.
+ //
+ // So, create entity_inner AFTER entity_center; this ensures that
+ // entity_inner gets rendered ON TOP.
+ entity_center->create(desktop, item, this, Inkscape::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)
+{
+ SPOffset *offset = dynamic_cast<SPOffset *>(item);
+ g_assert(offset != nullptr);
+
+ Geom::Point const p_snapped = snap_knot_position(p, state);
+
+ offset->rad = sp_offset_distance_to_original(offset, p_snapped);
+ offset->knot = p_snapped;
+ offset->knotSet = true;
+
+ offset->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+
+Geom::Point
+OffsetKnotHolderEntity::knot_get() const
+{
+ SPOffset const *offset = dynamic_cast<SPOffset const *>(item);
+ g_assert(offset != nullptr);
+
+ Geom::Point np;
+ sp_offset_top_point(offset,&np);
+ return np;
+}
+
+OffsetKnotHolder::OffsetKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) :
+ KnotHolder(desktop, item, relhandler)
+{
+ OffsetKnotHolderEntity *entity_offset = new OffsetKnotHolderEntity();
+ entity_offset->create(desktop, item, this, Inkscape::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
+{
+ SPText *text = dynamic_cast<SPText *>(item);
+ g_assert(text != nullptr);
+
+ SPStyle* style = text->style;
+ double inline_size = style->inline_size.computed;
+ unsigned mode = style->writing_mode.computed;
+ unsigned anchor = style->text_anchor.computed;
+ unsigned direction = style->direction.computed;
+
+ Geom::Point p(text->attributes.firstXY());
+
+ if (text->has_inline_size()) {
+ // SVG 2 'inline-size'
+
+ // Keep handle at end of text line.
+ if (mode == SP_CSS_WRITING_MODE_LR_TB ||
+ mode == SP_CSS_WRITING_MODE_RL_TB) {
+ // horizontal
+ if ( (direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_START ) ||
+ (direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_END) ) {
+ p *= Geom::Translate (inline_size, 0);
+ } else if ( direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) {
+ p *= Geom::Translate (inline_size/2.0, 0 );
+ } else if ( direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) {
+ p *= Geom::Translate (-inline_size/2.0, 0 );
+ } else if ( (direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_END ) ||
+ (direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_START) ) {
+ p *= Geom::Translate (-inline_size, 0);
+ }
+ } else {
+ // vertical
+ if (anchor == SP_CSS_TEXT_ANCHOR_START) {
+ p *= Geom::Translate (0, inline_size);
+ } else if (anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) {
+ p *= Geom::Translate (0, inline_size/2.0);
+ } else if (anchor == SP_CSS_TEXT_ANCHOR_END) {
+ p *= Geom::Translate (0, -inline_size);
+ }
+ }
+ } else {
+ // Normal single line text.
+ Geom::OptRect bbox = text->geometricBounds(); // Check if this is best.
+ if (bbox) {
+ if (mode == SP_CSS_WRITING_MODE_LR_TB ||
+ mode == SP_CSS_WRITING_MODE_RL_TB) {
+ // horizontal
+ if ( (direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_START ) ||
+ (direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_END) ) {
+ p *= Geom::Translate ((*bbox).width(), 0);
+ } else if ( direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) {
+ p *= Geom::Translate ((*bbox).width()/2, 0);
+ } else if ( direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) {
+ p *= Geom::Translate (-(*bbox).width()/2, 0);
+ } else if ( (direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_END ) ||
+ (direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_START) ) {
+ p *= Geom::Translate (-(*bbox).width(), 0);
+ }
+ } else {
+ // vertical
+ if (anchor == SP_CSS_TEXT_ANCHOR_START) {
+ p *= Geom::Translate (0, (*bbox).height());
+ } else if (anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) {
+ p *= Geom::Translate (0, (*bbox).height()/2);
+ } else if (anchor == SP_CSS_TEXT_ANCHOR_END) {
+ p *= Geom::Translate (0, -(*bbox).height());
+ }
+ 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)
+{
+ SPText *text = dynamic_cast<SPText *>(item);
+ g_assert(text != nullptr);
+
+ SPStyle* style = text->style;
+ unsigned mode = style->writing_mode.computed;
+ unsigned anchor = style->text_anchor.computed;
+ unsigned direction = style->direction.computed;
+
+ Geom::Point const s = snap_knot_position(p, state);
+ Geom::Point delta = s - text->attributes.firstXY();
+ double size = 0.0;
+ if (mode == SP_CSS_WRITING_MODE_LR_TB ||
+ mode == SP_CSS_WRITING_MODE_RL_TB) {
+ // horizontal
+
+ size = delta[Geom::X];
+ if ( (direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_START ) ||
+ (direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_END) ) {
+ // Do nothing
+ } else if ( (direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_END ) ||
+ (direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_START) ) {
+ size = -size;
+ } else if ( anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) {
+ size = 2.0 * abs(size);
+ } else {
+ std::cerr << "TextKnotHolderEntityInlinSize: Should not be reached!" << std::endl;
+ }
+
+ } else {
+ // vertical
+
+ size = delta[Geom::Y];
+ if (anchor == SP_CSS_TEXT_ANCHOR_START) {
+ // Do nothing
+ } else if (anchor == SP_CSS_TEXT_ANCHOR_END) {
+ size = -size;
+ } else if (anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) {
+ size = 2.0 * abs(size);
+ }
+ }
+
+ // Size should never be negative
+ if (size < 0.0) {
+ size = 0.0;
+ }
+
+ // Set 'inline-size'.
+ text->style->inline_size.setDouble(size);
+ text->style->inline_size.set = true;
+
+ // Ensure we respect new lines.
+ text->style->white_space.read("pre");
+ text->style->white_space.set = true;
+
+ // Convert sodipodi:role="line" to '\n'.
+ text->sodipodi_to_newline();
+
+ text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ text->updateRepr();
+}
+
+// Conversion from SVG 2 'inline-size' to Inkscape's SVG 1.1.
+void
+TextKnotHolderEntityInlineSize::knot_click(unsigned int state)
+{
+ SPText *text = dynamic_cast<SPText *>(item);
+ g_assert(text != nullptr);
+
+ if (state & GDK_CONTROL_MASK) {
+
+ text->style->inline_size.clear();
+ text->remove_svg11_fallback(); // Else 'x' and 'y' will be interpreted as absolute positions.
+ text->newline_to_sodipodi(); // Convert '\n' to tspans with sodipodi:role="line".
+
+ text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ text->updateRepr();
+ }
+}
+
+/**
+ * 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
+{
+ SPText *text = dynamic_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
+ SPText *text = dynamic_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.
+ SPText *text = dynamic_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
+ SPText *text = dynamic_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)
+{
+ SPText *text = dynamic_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) {
+ auto shape = href->getObject();
+ if (dynamic_cast<SPShape *>(shape)) {
+ 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 = dynamic_cast<SPRect const *>(item);
+ g_assert(rect != nullptr);
+
+ return Geom::Point(rect->x.computed + rect->width.computed, rect->y.computed + rect->height.computed);
+}
+
+void
+FlowtextKnotHolderEntity::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ set_internal(p, origin, state);
+}
+
+FlowtextKnotHolder::FlowtextKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) :
+ KnotHolder(desktop, item, relhandler)
+{
+ g_assert(item != nullptr);
+
+ FlowtextKnotHolderEntity *entity_flowtext = new FlowtextKnotHolderEntity();
+ entity_flowtext->create(desktop, item, this, Inkscape::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..6290014
--- /dev/null
+++ b/src/ui/shape-editor.cpp
@@ -0,0 +1,223 @@
+// 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"
+#include "xml/node-event-vector.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),
+ knotholder(nullptr),
+ lpeknotholder(nullptr),
+ knotholder_listener_attached_for(nullptr),
+ lpeknotholder_listener_attached_for(nullptr),
+ _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) {
+ sp_repr_remove_listener_by_data(old_repr, this);
+ Inkscape::GC::release(old_repr);
+ knotholder_listener_attached_for = nullptr;
+ }
+
+ if (!keep_knotholder) {
+ delete this->knotholder;
+ this->knotholder = nullptr;
+ }
+ }
+ if (this->lpeknotholder) {
+ Inkscape::XML::Node *old_repr = this->lpeknotholder->repr;
+ bool remove = false;
+ if (old_repr && old_repr == lpeknotholder_listener_attached_for) {
+ sp_repr_remove_listener_by_data(old_repr, 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::event_attr_changed(Inkscape::XML::Node * node, gchar const *name, gchar const *, gchar const *, bool, void *data)
+{
+ g_assert(data);
+ ShapeEditor *sh = static_cast<ShapeEditor *>(data);
+ bool changed_kh = false;
+
+ if (sh->has_knotholder())
+ {
+ changed_kh = !sh->has_local_change();
+ sh->decrement_local_change();
+ if (changed_kh) {
+ sh->reset_item();
+ }
+ }
+}
+
+static Inkscape::XML::NodeEventVector shapeeditor_repr_events = {
+ nullptr, /* child_added */
+ nullptr, /* child_removed */
+ ShapeEditor::event_attr_changed,
+ nullptr, /* content_changed */
+ nullptr /* order_changed */
+};
+
+
+void ShapeEditor::set_item(SPItem *item) {
+ if (_blockSetItem) {
+ return;
+ }
+ // this happens (and should only happen) when for an LPEItem having both knotholder and
+ // nodepath the knotholder is adapted; in this case we don't want to delete the knotholder
+ // since this freezes the handles
+ unset_item(true);
+
+ if (item) {
+ Inkscape::XML::Node *repr;
+ if (!this->knotholder) {
+ // only recreate knotholder if none is present
+ this->knotholder = createKnotHolder(item, desktop, _edit_rotation, _edit_marker_mode);
+ }
+ SPLPEItem *lpe = dynamic_cast<SPLPEItem *>(item);
+ if (!(lpe &&
+ lpe->getCurrentLPE() &&
+ lpe->getCurrentLPE()->isVisible() &&
+ lpe->getCurrentLPE()->providesKnotholder()))
+ {
+ delete this->lpeknotholder;
+ this->lpeknotholder = nullptr;
+ }
+ if (!this->lpeknotholder) {
+ // only recreate knotholder if none is present
+ this->lpeknotholder = createLPEKnotHolder(item, desktop);
+ }
+ if (this->knotholder) {
+ this->knotholder->setEditTransform(_edit_transform);
+ this->knotholder->update_knots();
+ // setting new listener
+ repr = this->knotholder->repr;
+ if (repr != knotholder_listener_attached_for) {
+ Inkscape::GC::anchor(repr);
+ sp_repr_add_listener(repr, &shapeeditor_repr_events, this);
+ knotholder_listener_attached_for = repr;
+ }
+ }
+ if (this->lpeknotholder) {
+ this->lpeknotholder->setEditTransform(_edit_transform);
+ this->lpeknotholder->update_knots();
+ // setting new listener
+ repr = this->lpeknotholder->repr;
+ if (repr != lpeknotholder_listener_attached_for) {
+ Inkscape::GC::anchor(repr);
+ sp_repr_add_listener(repr, &shapeeditor_repr_events, this);
+ lpeknotholder_listener_attached_for = repr;
+ }
+ }
+ }
+}
+
+
+/** FIXME: This thing is only called when the item needs to be updated in response to repr change.
+ Why not make a reload function in KnotHolder? */
+void ShapeEditor::reset_item()
+{
+ if (knotholder) {
+ SPObject *obj = desktop->getDocument()->getObjectByRepr(knotholder_listener_attached_for); /// note that it is not certain that this is an SPItem; it could be a LivePathEffectObject.
+ set_item(SP_ITEM(obj));
+ } else if (lpeknotholder) {
+ SPObject *obj = desktop->getDocument()->getObjectByRepr(lpeknotholder_listener_attached_for); /// note that it is not certain that this is an SPItem; it could be a LivePathEffectObject.
+ set_item(SP_ITEM(obj));
+ }
+}
+
+/**
+ * Returns true if this ShapeEditor has a knot above which the mouse currently hovers.
+ */
+bool ShapeEditor::knot_mouseover() const {
+ if (this->knotholder) {
+ return knotholder->knot_mouseover();
+ }
+ if (this->lpeknotholder) {
+ return lpeknotholder->knot_mouseover();
+ }
+
+ return false;
+}
+
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/ui/shape-editor.h b/src/ui/shape-editor.h
new file mode 100644
index 0000000..ff666fe
--- /dev/null
+++ b/src/ui/shape-editor.h
@@ -0,0 +1,75 @@
+// 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>
+
+class KnotHolder;
+class LivePathEffectObject;
+class SPDesktop;
+class SPItem;
+
+namespace Inkscape { namespace XML { class Node; }
+namespace UI {
+
+class ShapeEditor {
+public:
+
+ ShapeEditor(SPDesktop *desktop, Geom::Affine edit_transform = Geom::identity(), double edit_rotation = 0.0, int edit_marker_mode = -1);
+ ~ShapeEditor();
+
+ void set_item(SPItem *item);
+ void unset_item(bool keep_knotholder = false);
+
+ void update_knotholder(); //((deprecated))
+
+ bool has_local_change();
+ void decrement_local_change();
+
+ bool knot_mouseover() const;
+ KnotHolder *knotholder;
+ KnotHolder *lpeknotholder;
+ bool has_knotholder();
+ static void blockSetItem(bool b) { _blockSetItem = b; } // kludge
+ static void event_attr_changed(Inkscape::XML::Node * /*repr*/, char const *name, char const * /*old_value*/,
+ char const * /*new_value*/, bool /*is_interactive*/, void *data);
+private:
+ void reset_item();
+ static bool _blockSetItem;
+
+ SPDesktop *desktop;
+ Inkscape::XML::Node *knotholder_listener_attached_for;
+ Inkscape::XML::Node *lpeknotholder_listener_attached_for;
+ Geom::Affine _edit_transform;
+ double _edit_rotation;
+ int _edit_marker_mode;
+};
+
+} // 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..7198640
--- /dev/null
+++ b/src/ui/shortcuts.cpp
@@ -0,0 +1,996 @@
+// 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/resource.h"
+#include "io/dir-util.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 User shortcut file -------------
+ Glib::RefPtr<Gio::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);
+ 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 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 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 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()
+ << " Old: " << old_name << " New: " << name << " !" << 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 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) {
+ // Action exists, add shortcut to list of shortcuts.
+ std::vector<Glib::ustring> 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;
+ return true;
+ }
+ }
+
+ // Oops, not an action!
+ std::cerr << "Shortcuts::add_shortcut: No Action for " << name << 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 << " with shortcut " << shortcut.get_abbrev() << 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 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);
+ }
+ 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 action : list_all_detailed_action_names()) {
+ if (action == name) {
+ // Action exists
+ app->unset_accels_for_action(action);
+ action_user_set.erase(action);
+ 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 << 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();
+ 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_user = get_filenames(USER, KEYS, {".xml"}, {"default.xml"});
+ filenames.insert(filenames.end(), filenames_user.begin(), filenames_user.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);
+ if (!document) {
+ std::cerr << "Shortcut::get_file_names: could not parse file: " << filename << 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 << 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 << 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);
+ }
+
+ // 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_text(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->addFileType(_("Inkscape shortcuts (*.xml)"), "*.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 (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;
+};
+
+
+// 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..1146bf6
--- /dev/null
+++ b/src/ui/shortcuts.h
@@ -0,0 +1,137 @@
+// 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>
+
+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
+ 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;
+};
+
+} // 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..e8901d5
--- /dev/null
+++ b/src/ui/svg-renderer.cpp
@@ -0,0 +1,112 @@
+// 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);
+}
+
+svg_renderer::svg_renderer(const char* svg_file_path) {
+
+ auto file = Gio::File::create_for_path(svg_file_path);
+
+ _document.reset(ink_file_open(file, nullptr));
+
+ 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 w = _document->getWidth().value("px");
+ auto h = _document->getHeight().value("px");
+ auto dpi = 96 * scale;
+ auto area = Geom::Rect(0, 0, w, h);
+
+ return sp_generate_internal_bitmap(_document.get(), area, dpi);
+}
+
+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;
+}
+
+} // namespace
diff --git a/src/ui/svg-renderer.h b/src/ui/svg-renderer.h
new file mode 100644
index 0000000..b586ea9
--- /dev/null
+++ b/src/ui/svg-renderer.h
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_SVG_RENDERER_H
+#define SEEN_SVG_RENDERER_H
+
+#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);
+
+ // 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);
+
+ double get_width_px() const;
+ double get_height_px() const;
+
+private:
+ Pixbuf* do_render(double scale);
+ std::unique_ptr<SPDocument> _document;
+ SPRoot* _root = nullptr;
+};
+
+}
+
+#endif
diff --git a/src/ui/themes.cpp b/src/ui/themes.cpp
new file mode 100644
index 0000000..bb58245
--- /dev/null
+++ b/src/ui/themes.cpp
@@ -0,0 +1,546 @@
+// 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 "preferences.h"
+#include "io/resource.h"
+#include "svg/svg-color.h"
+#include <cstring>
+#include <gio/gio.h>
+#include <glibmm.h>
+#include <gtkmm.h>
+#include <map>
+#include <utility>
+#include <vector>
+#include <regex>
+#include "svg/css-ostringstream.h"
+
+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
+}
+
+/**
+ * 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;
+}
+
+/**
+ * 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::adjust_global_font_scale(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; }";
+ _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);
+}
+
+} // 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..4e896e2
--- /dev/null
+++ b/src/ui/themes.h
@@ -0,0 +1,89 @@
+// 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 <gtkmm.h>
+#include <map>
+#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;}
+sigc::signal<void> getChangeThemeSignal() { return _signal_change_theme;}
+// set application-wide font size adjustment by a factor, where 1 is 100% (no change)
+void adjust_global_font_scale(double factor);
+
+// True if current theme (applied one) is dark
+bool isCurrentThemeDark(Gtk::Container *window);
+
+static std::vector<guint32> getHighlightColors(Gtk::Window *window);
+
+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;
+#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..2b3b49b
--- /dev/null
+++ b/src/ui/tool-factory.cpp
@@ -0,0 +1,107 @@
+// 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/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/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());
+
+ 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..3f910de
--- /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::Rect const &r, bool invert)
+{
+ std::vector<SelectableControlPoint *> out;
+ for (auto _all_point : _all_points) {
+ if (r.contains(*_all_point)) {
+ 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 + _desktop->canvas->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..dc58211
--- /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::Rect 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..da6bdaf
--- /dev/null
+++ b/src/ui/tool/control-point.cpp
@@ -0,0 +1,597 @@
+// 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" // Forced redraws
+
+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 = new Inkscape::CanvasItemCtrl(group ? group : _desktop->getCanvasControls(),
+ Inkscape::CANVAS_ITEM_CTRL_SHAPE_BITMAP);
+ _canvas_item_ctrl->set_name("CanvasItemCtrl:ControlPoint");
+ _canvas_item_ctrl->set_pixbuf(pixbuf->gobj());
+ _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 = new 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();
+ delete _canvas_item_ctrl;
+}
+
+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);
+}
+
+void ControlPoint::_setPixbuf(Glib::RefPtr<Gdk::Pixbuf> p)
+{
+ _canvas_item_ctrl->set_pixbuf(Glib::unwrap(p));
+}
+
+// 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, nullptr); // 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->scroll_to_point(new_pos);
+ _desktop->set_coordinate_status(_position);
+ sp_event_context_snap_delay_handler(event_context, nullptr,
+ (gpointer) this, &event->motion,
+ Inkscape::UI::Tools::DelayedSnapEvent::CONTROL_POINT_HANDLER);
+ }
+ return true;
+ }
+ break;
+
+ case GDK_BUTTON_RELEASE:
+ if (_event_grab && event->button.button == 1) {
+ // If we have any pending snap event, then invoke it now!
+ // (This is needed because we might not have snapped on the latest GDK_MOTION_NOTIFY event
+ // if the mouse speed was too high. This is inherent to the snap-delay mechanism.
+ // We must snap at some point in time though, and this is our last chance)
+ // PS: For other contexts this is handled already in start_item_handler or start_root_handler
+ // if (_desktop && _desktop->event_context && _desktop->event_context->_delayed_snap_event) {
+ if (event_context->_delayed_snap_event) {
+ sp_event_context_snap_watchdog_callback(event_context->_delayed_snap_event);
+ }
+
+ _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, nullptr); // 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..7cc3977
--- /dev/null
+++ b/src/ui/tool/control-point.h
@@ -0,0 +1,414 @@
+// 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 "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);
+
+ void _setPixbuf(Glib::RefPtr<Gdk::Pixbuf>);
+
+ /**
+ * Determines if the control point is not visible yet still reacting to events.
+ *
+ * @return true if non-visible, false otherwise.
+ */
+ bool _isLurking();
+
+ /**
+ * Sets the control point to be non-visible yet still reacting to events.
+ *
+ * @param lurking true to make non-visible, false otherwise.
+ */
+ void _setLurking(bool lurking);
+
+ /// @}
+
+ virtual Glib::ustring _getTip(unsigned /*state*/) const { return ""; }
+
+ virtual Glib::ustring _getDragTip(GdkEventMotion */*event*/) const { return ""; }
+
+ virtual bool _hasDragTips() const { return false; }
+
+
+ Inkscape::CanvasItemCtrl * _canvas_item_ctrl = nullptr; ///< 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..c3bf969
--- /dev/null
+++ b/src/ui/tool/multi-path-manipulator.cpp
@@ -0,0 +1,899 @@
+// 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
+ : public std::unary_function<NodeList::iterator, std::size_t>
+{
+ std::size_t operator()(NodeList::iterator i) const {
+ return std::hash<NodeList::iterator::pointer>()(&*i);
+ }
+};
+
+typedef std::pair<NodeList::iterator, NodeList::iterator> IterPair;
+typedef std::vector<IterPair> IterPairList;
+typedef std::unordered_set<NodeList::iterator, hash_nodelist_iterator> IterSet;
+typedef std::multimap<double, IterPair> DistanceMap;
+typedef std::pair<double, IterPair> DistanceMapItem;
+
+/** Find pairs of selected endnodes suitable for joining. */
+void find_join_iterators(ControlPointSelection &sel, IterPairList &pairs)
+{
+ IterSet join_iters;
+
+ // find all endnodes in selection
+ for (auto i : sel) {
+ Node *node = dynamic_cast<Node*>(i);
+ if (!node) continue;
+ NodeList::iterator iter = NodeList::get_iterator(node);
+ if (!iter.next() || !iter.prev()) join_iters.insert(iter);
+ }
+
+ if (join_iters.size() < 2) return;
+
+ // Below we find the closest pairs. The algorithm is O(N^3).
+ // We can go down to O(N^2 log N) by using O(N^2) memory, by putting all pairs
+ // with their distances in a multimap (not worth it IMO).
+ while (join_iters.size() >= 2) {
+ double closest = DBL_MAX;
+ IterPair closest_pair;
+ for (IterSet::iterator i = join_iters.begin(); i != join_iters.end(); ++i) {
+ for (IterSet::iterator j = join_iters.begin(); j != i; ++j) {
+ double dist = Geom::distance(**i, **j);
+ if (dist < closest) {
+ closest = dist;
+ closest_pair = std::make_pair(*i, *j);
+ }
+ }
+ }
+ pairs.push_back(closest_pair);
+ join_iters.erase(closest_pair.first);
+ join_iters.erase(closest_pair.second);
+ }
+}
+
+/** After this function, first should be at the end of path and second at the 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) {
+ LivePathEffectObject *lpobj = dynamic_cast<LivePathEffectObject *>(r.object);
+ if (!SP_IS_PATH(r.object) && !lpobj) continue;
+ std::shared_ptr<PathManipulator> newpm(new PathManipulator(*this, (SPPath*) r.object,
+ r.edit_transform, _getOutlineColor(r.role, r.object), r.lpe_key));
+ newpm->showHandles(_show_handles);
+ // always show outlines for clips and masks
+ newpm->showOutline(_show_outline || r.role != SHAPE_ROLE_NORMAL);
+ newpm->showPathDirection(_show_path_direction);
+ newpm->setLiveOutline(_live_outline);
+ newpm->setLiveObjects(_live_objects);
+ _mmap.insert(std::make_pair(r, newpm));
+ }
+}
+
+void MultiPathManipulator::selectSubpaths()
+{
+ if (_selection.empty()) {
+ _selection.selectAll();
+ } else {
+ invokeForAll(&PathManipulator::selectSubpaths);
+ }
+}
+
+void MultiPathManipulator::shiftSelection(int dir)
+{
+ if (empty()) return;
+
+ // 1. find last selected node
+ // 2. select the next node; if the last node or nothing is selected,
+ // select first node
+ MapType::iterator last_i;
+ SubpathList::iterator last_j;
+ NodeList::iterator last_k;
+ bool anything_found = false;
+ bool anynode_found = false;
+
+ for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) {
+ SubpathList &sp = i->second->subpathList();
+ for (SubpathList::iterator j = sp.begin(); j != sp.end(); ++j) {
+ anynode_found = true;
+ for (NodeList::iterator k = (*j)->begin(); k != (*j)->end(); ++k) {
+ if (k->selected()) {
+ last_i = i;
+ last_j = j;
+ last_k = k;
+ anything_found = true;
+ // when tabbing backwards, we want the first node
+ if (dir == -1) goto exit_loop;
+ }
+ }
+ }
+ }
+ exit_loop:
+
+ // NOTE: we should not assume the _selection contains only nodes
+ // in future it might also contain handles and other types of control points
+ // this is why we use a flag instead in the loop above, instead of calling
+ // selection.empty()
+ if (!anything_found) {
+ // select first / last node
+ // this should never fail because there must be at least 1 non-empty manipulator
+ if (anynode_found) {
+ if (dir == 1) {
+ _selection.insert((*_mmap.begin()->second->subpathList().begin())->begin().ptr());
+ } else {
+ _selection.insert((--(*--(--_mmap.end())->second->subpathList().end())->end()).ptr());
+ }
+ }
+ return;
+ }
+
+ // three levels deep - w00t!
+ if (dir == 1) {
+ if (++last_k == (*last_j)->end()) {
+ // here, last_k points to the node to be selected
+ ++last_j;
+ if (last_j == last_i->second->subpathList().end()) {
+ ++last_i;
+ if (last_i == _mmap.end()) {
+ last_i = _mmap.begin();
+ }
+ last_j = last_i->second->subpathList().begin();
+ }
+ last_k = (*last_j)->begin();
+ }
+ } else {
+ if (!last_k || last_k == (*last_j)->begin()) {
+ if (last_j == last_i->second->subpathList().begin()) {
+ if (last_i == _mmap.begin()) {
+ last_i = _mmap.end();
+ }
+ --last_i;
+ last_j = last_i->second->subpathList().end();
+ }
+ --last_j;
+ last_k = (*last_j)->end();
+ }
+ --last_k;
+ }
+ _selection.clear();
+ _selection.insert(last_k.ptr());
+}
+
+void MultiPathManipulator::invertSelectionInSubpaths()
+{
+ invokeForAll(&PathManipulator::invertSelectionInSubpaths);
+}
+
+void MultiPathManipulator::setNodeType(NodeType type)
+{
+ if (_selection.empty()) return;
+
+ // When all selected nodes are already cusp, retract their handles
+ bool retract_handles = (type == NODE_CUSP);
+
+ for (auto i : _selection) {
+ Node *node = dynamic_cast<Node*>(i);
+ if (node) {
+ retract_handles &= (node->type() == NODE_CUSP);
+ node->setType(type);
+ }
+ }
+
+ if (retract_handles) {
+ for (auto i : _selection) {
+ Node *node = dynamic_cast<Node*>(i);
+ if (node) {
+ node->front()->retract();
+ node->back()->retract();
+ }
+ }
+ }
+
+ _done(retract_handles ? _("Retract handles") : _("Change node type"));
+}
+
+void MultiPathManipulator::setSegmentType(SegmentType type)
+{
+ if (_selection.empty()) return;
+ invokeForAll(&PathManipulator::setSegmentType, type);
+ if (type == SEGMENT_STRAIGHT) {
+ _done(_("Straighten segments"));
+ } else {
+ _done(_("Make segments curves"));
+ }
+}
+
+void MultiPathManipulator::insertNodes()
+{
+ if (_selection.empty()) return;
+ invokeForAll(&PathManipulator::insertNodes);
+ _done(_("Add nodes"));
+}
+void MultiPathManipulator::insertNodesAtExtrema(ExtremumType extremum)
+{
+ if (_selection.empty()) return;
+ invokeForAll(&PathManipulator::insertNodeAtExtremum, extremum);
+ _done(_("Add extremum nodes"));
+}
+
+void MultiPathManipulator::insertNode(Geom::Point pt)
+{
+ // When double clicking to insert nodes, we might not have a selection of nodes (and we don't need one)
+ // so don't check for "_selection.empty()" here, contrary to the other methods above and below this one
+ invokeForAll(&PathManipulator::insertNode, pt);
+ _done(_("Add nodes"));
+}
+
+void MultiPathManipulator::duplicateNodes()
+{
+ if (_selection.empty()) return;
+ invokeForAll(&PathManipulator::duplicateNodes);
+ _done(_("Duplicate nodes"));
+}
+
+void MultiPathManipulator::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)
+{
+ if (_selection.empty()) return;
+ invokeForAll(&PathManipulator::deleteNodes, keep_shape);
+ _doneWithCleanup(_("Delete nodes"), true);
+}
+
+/** Join selected endpoints to create segments. */
+void MultiPathManipulator::joinSegments()
+{
+ if (_selection.empty()) return;
+ IterPairList joins;
+ find_join_iterators(_selection, joins);
+
+ for (auto & join : joins) {
+ bool same_path = prepare_join(join);
+ NodeList &sp_first = NodeList::get(join.first);
+ NodeList &sp_second = NodeList::get(join.second);
+ join.first->setType(NODE_CUSP, false);
+ join.second->setType(NODE_CUSP, false);
+ if (same_path) {
+ sp_first.setClosed(true);
+ } else {
+ sp_first.splice(sp_first.end(), sp_second);
+ sp_second.kill();
+ }
+ }
+
+ if (joins.empty()) {
+ invokeForAll(&PathManipulator::weldSegments);
+ }
+ _doneWithCleanup("Join segments", true);
+}
+
+void MultiPathManipulator::deleteSegments()
+{
+ if (_selection.empty()) return;
+ invokeForAll(&PathManipulator::deleteSegments);
+ _doneWithCleanup("Delete segments", true);
+}
+
+void MultiPathManipulator::alignNodes(Geom::Dim2 d, 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);
+ // pass keep_shape = true when:
+ // a) del preserves shape, and control is not pressed
+ // b) ctrl+del preserves shape (del_preserves_shape is false), and control is pressed
+ // Hence xor
+ guint mode = prefs->getInt("/tools/freehand/pen/freehand-mode", 0);
+
+ //if the trace is bspline ( mode 2)
+ if(mode==2){
+ // is this correct ?
+ if(del_preserves_shape ^ held_control(event->key)){
+ deleteNodes(false);
+ } else {
+ deleteNodes(true);
+ }
+ } else {
+ deleteNodes(del_preserves_shape ^ held_control(event->key));
+ }
+
+ // Delete any selected gradient nodes as well
+ event_context->deleteSelectedDrag(held_control(event->key));
+ }
+ return true;
+ case GDK_KEY_c:
+ case GDK_KEY_C:
+ if (held_only_shift(event->key)) {
+ // Shift+C - make nodes cusp
+ setNodeType(NODE_CUSP);
+ return true;
+ }
+ break;
+ case GDK_KEY_s:
+ case GDK_KEY_S:
+ if (held_only_shift(event->key)) {
+ // Shift+S - make nodes smooth
+ setNodeType(NODE_SMOOTH);
+ return true;
+ }
+ break;
+ case GDK_KEY_a:
+ case GDK_KEY_A:
+ if (held_only_shift(event->key)) {
+ // Shift+A - make nodes auto-smooth
+ setNodeType(NODE_AUTO);
+ return true;
+ }
+ break;
+ case GDK_KEY_y:
+ case GDK_KEY_Y:
+ if (held_only_shift(event->key)) {
+ // Shift+Y - make nodes symmetric
+ setNodeType(NODE_SYMMETRIC);
+ return true;
+ }
+ break;
+ case GDK_KEY_r:
+ case GDK_KEY_R:
+ if (held_only_shift(event->key)) {
+ // Shift+R - reverse subpaths
+ reverseSubpaths();
+ return true;
+ }
+ break;
+ case GDK_KEY_l:
+ case GDK_KEY_L:
+ if (held_only_shift(event->key)) {
+ // Shift+L - make segments linear
+ setSegmentType(SEGMENT_STRAIGHT);
+ return true;
+ }
+ case GDK_KEY_u:
+ case GDK_KEY_U:
+ if (held_only_shift(event->key)) {
+ // Shift+U - make segments curves
+ setSegmentType(SEGMENT_CUBIC_BEZIER);
+ return true;
+ }
+ default:
+ break;
+ }
+ break;
+ case GDK_MOTION_NOTIFY:
+ 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 SP_ITEM(object)->highlight_color();
+ }
+}
+
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/tool/multi-path-manipulator.h b/src/ui/tool/multi-path-manipulator.h
new file mode 100644
index 0000000..c00436c
--- /dev/null
+++ b/src/ui/tool/multi-path-manipulator.h
@@ -0,0 +1,157 @@
+// 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"
+
+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(bool keep_shape = true);
+ 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..57be0af
--- /dev/null
+++ b/src/ui/tool/node.cpp
@@ -0,0 +1,1923 @@
+// 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"
+
+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(new Inkscape::CanvasItemCurve(data.handle_line_group))
+ , _parent(parent)
+ , _degenerate(true)
+{
+ setVisible(false);
+}
+
+Handle::~Handle()
+{
+ delete _handle_line;
+}
+
+void Handle::setVisible(bool v)
+{
+ ControlPoint::setVisible(v);
+ if (v) {
+ _handle_line->show();
+ } else {
+ _handle_line->hide();
+ }
+}
+
+void Handle::move(Geom::Point const &new_pos)
+{
+ Handle *other = this->other();
+ Node *node_towards = _parent->nodeToward(this); // node in direction of this handle
+ Node *node_away = _parent->nodeAwayFrom(this); // node in the opposite direction
+ Handle *towards = node_towards ? node_towards->handleAwayFrom(_parent) : nullptr;
+ Handle *towards_second = node_towards ? node_towards->handleToward(_parent) : nullptr;
+ double bspline_weight = 0.0;
+
+ if (Geom::are_near(new_pos, _parent->position())) {
+ // The handle becomes degenerate.
+ // Adjust node type as necessary.
+ if (other->isDegenerate()) {
+ // If both handles become degenerate, convert to parent cusp node
+ _parent->setType(NODE_CUSP, false);
+ } else {
+ // Only 1 handle becomes degenerate
+ switch (_parent->type()) {
+ case NODE_AUTO:
+ case NODE_SYMMETRIC:
+ _parent->setType(NODE_SMOOTH, false);
+ break;
+ default:
+ // do nothing for other node types
+ break;
+ }
+ }
+ // If the segment between the handle and the node in its direction becomes linear,
+ // and there are smooth nodes at its ends, make their handles collinear with the segment.
+ if (towards && towards_second->isDegenerate()) {
+ if (node_towards->type() == NODE_SMOOTH) {
+ towards->setDirection(*_parent, *node_towards);
+ }
+ if (_parent->type() == NODE_SMOOTH) {
+ other->setDirection(*node_towards, *_parent);
+ }
+ }
+ setPosition(new_pos);
+
+ // move the handle and its opposite the same proportion
+ if(_pm()._isBSpline()){
+ setPosition(_pm()._bsplineHandleReposition(this, false));
+ bspline_weight = _pm()._bsplineHandlePosition(this, false);
+ this->other()->setPosition(_pm()._bsplineHandleReposition(this->other(), bspline_weight));
+ }
+ return;
+ }
+
+ if (_parent->type() == NODE_SMOOTH && Node::_is_line_segment(_parent, node_away)) {
+ // restrict movement to the line joining the nodes
+ Geom::Point direction = _parent->position() - node_away->position();
+ Geom::Point delta = new_pos - _parent->position();
+ // project the relative position on the direction line
+ Geom::Coord direction_length = Geom::L2sq(direction);
+ Geom::Point new_delta;
+ if (direction_length == 0) {
+ // joining line has zero length - any direction is okay, prevent division by zero
+ new_delta = delta;
+ } else {
+ new_delta = (Geom::dot(delta, direction) / direction_length) * direction;
+ }
+ setRelativePos(new_delta);
+
+ // move the handle and its opposite the same proportion
+ if(_pm()._isBSpline()){
+ setPosition(_pm()._bsplineHandleReposition(this, false));
+ bspline_weight = _pm()._bsplineHandlePosition(this, false);
+ this->other()->setPosition(_pm()._bsplineHandleReposition(this->other(), bspline_weight));
+ }
+
+ return;
+ }
+
+ switch (_parent->type()) {
+ case NODE_AUTO:
+ _parent->setType(NODE_SMOOTH, false);
+ // fall through - auto nodes degrade into smooth nodes
+ case NODE_SMOOTH: {
+ // for smooth nodes, we need to rotate the opposite handle
+ // so that it's collinear with the dragged one, while conserving length.
+ other->setDirection(new_pos, *_parent);
+ } break;
+ case NODE_SYMMETRIC:
+ // for symmetric nodes, place the other handle on the opposite side
+ other->setRelativePos(-(new_pos - _parent->position()));
+ break;
+ default: break;
+ }
+ setPosition(new_pos);
+
+ // move the handle and its opposite the same proportion
+ if(_pm()._isBSpline()){
+ setPosition(_pm()._bsplineHandleReposition(this, false));
+ bspline_weight = _pm()._bsplineHandlePosition(this, false);
+ this->other()->setPosition(_pm()._bsplineHandleReposition(this->other(), bspline_weight));
+ }
+ Inkscape::UI::Tools::sp_update_helperpath(_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
+ if (held_alt(*event)) {
+ new_pos = parent_pos + Geom::unit_vector(new_pos - parent_pos) * _saved_length;
+ snap = false;
+ }
+ // with Ctrl, constrain to M_PI/rotationsnapsperpi increments from vertical
+ // and the original position.
+ if (held_control(*event)) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int snaps = 2 * prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
+
+ // note: if snapping to the original position is only desired in the original
+ // direction of the handle, change to Ray instead of Line
+ Geom::Line original_line(parent_pos, origin);
+ Geom::Line perp_line(parent_pos, parent_pos + Geom::rot90(origin - parent_pos));
+ Geom::Point snap_pos = parent_pos + Geom::constrain_angle(
+ Geom::Point(0,0), new_pos - parent_pos, snaps, Geom::Point(1,0));
+ Geom::Point orig_pos = original_line.pointAt(original_line.nearestTime(new_pos));
+ Geom::Point perp_pos = perp_line.pointAt(perp_line.nearestTime(new_pos));
+
+ Geom::Point result = snap_pos;
+ ctrl_constraint = Inkscape::Snapper::SnapConstraint(parent_pos, parent_pos - snap_pos);
+ if (Geom::distance(orig_pos, new_pos) < Geom::distance(result, new_pos)) {
+ result = orig_pos;
+ ctrl_constraint = Inkscape::Snapper::SnapConstraint(parent_pos, parent_pos - orig_pos);
+ }
+ if (Geom::distance(perp_pos, new_pos) < Geom::distance(result, new_pos)) {
+ result = perp_pos;
+ ctrl_constraint = Inkscape::Snapper::SnapConstraint(parent_pos, parent_pos - perp_pos);
+ }
+ new_pos = result;
+ // move the handle and its opposite in X fixed positions depending on parameter "steps with control"
+ // by default in live BSpline
+ if(_pm()._isBSpline()){
+ setPosition(new_pos);
+ int steps = _pm()._bsplineGetSteps();
+ new_pos=_pm()._bsplineHandleReposition(this,ceilf(_pm()._bsplineHandlePosition(this, false)*steps)/steps);
+ }
+ }
+
+ std::vector<Inkscape::SnapCandidatePoint> unselected;
+ // if the snap adjustment is activated and it is not BSpline
+ if (snap && !_pm()._isBSpline()) {
+ ControlPointSelection::Set &nodes = _parent->_selection.allPoints();
+ for (auto node : nodes) {
+ Node *n = static_cast<Node*>(node);
+ unselected.push_back(n->snapCandidatePoint());
+ }
+ sm.setupIgnoreSelection(_desktop, true, &unselected);
+
+ Node *node_away = _parent->nodeAwayFrom(this);
+ if (_parent->type() == NODE_SMOOTH && Node::_is_line_segment(_parent, node_away)) {
+ Inkscape::Snapper::SnapConstraint cl(_parent->position(),
+ _parent->position() - node_away->position());
+ Inkscape::SnappedPoint p;
+ p = sm.constrainedSnap(Inkscape::SnapCandidatePoint(new_pos, SNAPSOURCE_NODE_HANDLE), cl);
+ new_pos = p.getPoint();
+ } else if (ctrl_constraint) {
+ // NOTE: this is subtly wrong.
+ // We should get all possible constraints and snap along them using
+ // multipleConstrainedSnaps, instead of first snapping to angle and then to objects
+ Inkscape::SnappedPoint p;
+ p = sm.constrainedSnap(Inkscape::SnapCandidatePoint(new_pos, SNAPSOURCE_NODE_HANDLE), *ctrl_constraint);
+ new_pos = p.getPoint();
+ } else {
+ sm.freeSnapReturnByRef(new_pos, SNAPSOURCE_NODE_HANDLE);
+ }
+ sm.unSetup();
+ }
+
+
+ // with Shift, if the node is cusp, rotate the other handle as well
+ if (_parent->type() == NODE_CUSP && !_drag_out) {
+ if (held_shift(*event)) {
+ Geom::Point other_relpos = _saved_other_pos - parent_pos;
+ other_relpos *= Geom::Rotate(Geom::angle_between(origin - parent_pos, new_pos - parent_pos));
+ other()->setRelativePos(other_relpos);
+ } else {
+ // restore the position
+ other()->setPosition(_saved_other_pos);
+ }
+ }
+ // if it is BSpline, but SHIFT or CONTROL are not pressed, fix it in the original position
+ if(_pm()._isBSpline() && !held_shift(*event) && !held_control(*event)){
+ new_pos=_last_drag_origin();
+ }
+ _pm().update();
+}
+
+void Handle::ungrabbed(GdkEventButton *event)
+{
+ // hide the handle if it's less than dragtolerance away from the node
+ // however, never do this for cancelled drag / broken grab
+ // TODO is this actually a good idea?
+ if (event) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int drag_tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+
+ Geom::Point dist = _desktop->d2w(_parent->position()) - _desktop->d2w(position());
+ if (dist.length() <= drag_tolerance) {
+ move(_parent->position());
+ }
+ }
+
+ // HACK: If the handle was dragged out, call parent's ungrabbed handler,
+ // so that transform handles reappear
+ if (_drag_out) {
+ _parent->ungrabbed(event);
+ }
+ _drag_out = false;
+
+ _pm()._handleUngrabbed();
+}
+
+bool Handle::clicked(GdkEventButton *event)
+{
+ _pm()._handleClicked(this, event);
+ return true;
+}
+
+Handle const *Handle::other() const
+{
+ return const_cast<Handle *>(this)->other();
+}
+
+Handle *Handle::other()
+{
+ if (this == &_parent->_front) {
+ return &_parent->_back;
+ } else {
+ return &_parent->_front;
+ }
+}
+
+static double snap_increment_degrees() {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
+ return 180.0 / snaps;
+}
+
+Glib::ustring Handle::_getTip(unsigned state) const
+{
+ /* a trick to mark as BSpline if the node has no strength;
+ we are going to use it later to show the appropriate messages.
+ we cannot do it in any different way because the function is constant. */
+ Handle *h = const_cast<Handle *>(this);
+ bool isBSpline = _pm()._isBSpline();
+ bool can_shift_rotate = _parent->type() == NODE_CUSP && !other()->isDegenerate();
+ Glib::ustring s = C_("Path handle tip",
+ "node control handle"); // not expected
+
+ if (state_held_alt(state) && !isBSpline) {
+ if (state_held_control(state)) {
+ if (state_held_shift(state) && can_shift_rotate) {
+ s = format_tip(C_("Path handle tip",
+ "<b>Shift+Ctrl+Alt</b>: "
+ "preserve length and snap rotation angle to %g° increments, "
+ "and rotate both handles"),
+ snap_increment_degrees());
+ }
+ else {
+ s = format_tip(C_("Path handle tip",
+ "<b>Ctrl+Alt</b>: "
+ "preserve length and snap rotation angle to %g° increments"),
+ snap_increment_degrees());
+ }
+ }
+ else {
+ if (state_held_shift(state) && can_shift_rotate) {
+ s = C_("Path handle tip",
+ "<b>Shift+Alt</b>: preserve handle length and rotate both handles");
+ }
+ else {
+ s = C_("Path handle tip",
+ "<b>Alt</b>: preserve handle length while dragging");
+ }
+ }
+ }
+ else {
+ if (state_held_control(state)) {
+ if (state_held_shift(state) && can_shift_rotate && !isBSpline) {
+ s = format_tip(C_("Path handle tip",
+ "<b>Shift+Ctrl</b>: "
+ "snap rotation angle to %g° increments, and rotate both handles"),
+ snap_increment_degrees());
+ }
+ else if (isBSpline) {
+ s = C_("Path handle tip",
+ "<b>Ctrl</b>: "
+ "Snap handle to steps defined in BSpline Live Path Effect");
+ }
+ else {
+ s = format_tip(C_("Path handle tip",
+ "<b>Ctrl</b>: "
+ "snap rotation angle to %g° increments, click to retract"),
+ snap_increment_degrees());
+ }
+ }
+ else if (state_held_shift(state) && can_shift_rotate && !isBSpline) {
+ s = C_("Path handle tip",
+ "<b>Shift</b>: rotate both handles by the same angle");
+ }
+ else if (state_held_shift(state) && isBSpline) {
+ s = C_("Path handle tip",
+ "<b>Shift</b>: move handle");
+ }
+ else {
+ char const *handletype = handle_type_to_localized_string(_parent->_type);
+ char const *more;
+
+ if (can_shift_rotate && !isBSpline) {
+ more = C_("Path handle tip",
+ "Shift, Ctrl, Alt");
+ }
+ else if (isBSpline) {
+ more = C_("Path handle tip",
+ "Ctrl");
+ }
+ else {
+ more = C_("Path handle tip",
+ "Ctrl, Alt");
+ }
+
+ if (_parent->type() == NODE_CUSP) {
+ s = format_tip(C_("Path handle tip",
+ "<b>%s</b>: "
+ "drag to shape the path" ", "
+ "hover to lock" ", "
+ "Shift+S to make smooth" ", "
+ "Shift+Y to make symmetric" ". "
+ "(more: %s)"),
+ handletype, more);
+ }
+ else if (_parent->type() == NODE_SMOOTH) {
+ s = format_tip(C_("Path handle tip",
+ "<b>%s</b>: "
+ "drag to shape the path" ", "
+ "hover to lock" ", "
+ "Shift+Y to make symmetric" ". "
+ "(more: %s)"),
+ handletype, more);
+ }
+ else if (_parent->type() == NODE_AUTO) {
+ s = format_tip(C_("Path handle tip",
+ "<b>%s</b>: "
+ "drag to make smooth, "
+ "hover to lock" ", "
+ "Shift+Y to make symmetric" ". "
+ "(more: %s)"),
+ handletype, more);
+ }
+ else if (_parent->type() == NODE_SYMMETRIC) {
+ s = format_tip(C_("Path handle tip",
+ "<b>%s</b>: "
+ "drag to shape the path" ". "
+ "(more: %s)"),
+ handletype, more);
+ }
+ else if (isBSpline) {
+ double power = _pm()._bsplineHandlePosition(h);
+ s = format_tip(C_("Path handle tip",
+ "<b>BSpline node handle</b> (%.3g power): "
+ "Shift-drag to move, "
+ "double-click to reset. "
+ "(more: %s)"),
+ power, more);
+ }
+ else {
+ s = C_("Path handle tip",
+ "<b>unknown node handle</b>"); // not expected
+ }
+ }
+ }
+
+ return (s);
+}
+
+Glib::ustring Handle::_getDragTip(GdkEventMotion */*event*/) const
+{
+ Geom::Point dist = position() - _last_drag_origin();
+ // report angle in mathematical convention
+ double angle = Geom::angle_between(Geom::Point(-1,0), position() - _parent->position());
+ angle += M_PI; // angle is (-M_PI...M_PI] - offset by +pi and scale to 0...360
+ angle *= 360.0 / (2 * M_PI);
+
+ Inkscape::Util::Quantity x_q = Inkscape::Util::Quantity(dist[Geom::X], "px");
+ Inkscape::Util::Quantity y_q = Inkscape::Util::Quantity(dist[Geom::Y], "px");
+ Inkscape::Util::Quantity len_q = Inkscape::Util::Quantity(length(), "px");
+ Glib::ustring x = x_q.string(_desktop->namedview->display_units);
+ Glib::ustring y = y_q.string(_desktop->namedview->display_units);
+ Glib::ustring len = len_q.string(_desktop->namedview->display_units);
+ Glib::ustring ret = format_tip(C_("Path handle tip",
+ "Move handle by %s, %s; angle %.2f°, length %s"), x.c_str(), y.c_str(), angle, len.c_str());
+ return ret;
+}
+
+Node::Node(NodeSharedData const &data, Geom::Point const &initial_pos) :
+ SelectableControlPoint(data.desktop, initial_pos, SP_ANCHOR_CENTER,
+ 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 old_pos = position();
+ Geom::Point delta = new_pos - position();
+
+ // save the previous nodes strength to apply it again once the node is moved
+ double nodeWeight = NO_POWER;
+ double nextNodeWeight = NO_POWER;
+ double prevNodeWeight = NO_POWER;
+ Node *n = this;
+ Node * nextNode = n->nodeToward(n->front());
+ Node * prevNode = n->nodeToward(n->back());
+ nodeWeight = fmax(_pm()._bsplineHandlePosition(n->front(), false),_pm()._bsplineHandlePosition(n->back(), false));
+ if(prevNode){
+ prevNodeWeight = _pm()._bsplineHandlePosition(prevNode->front());
+ }
+ if(nextNode){
+ nextNodeWeight = _pm()._bsplineHandlePosition(nextNode->back());
+ }
+
+ // 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));
+ }
+ }
+}
+
+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->set_z_position(0);
+}
+
+NodeType Node::parse_nodetype(char x)
+{
+ switch (x) {
+ case 'a': return NODE_AUTO;
+ case 'c': return NODE_CUSP;
+ case 's': return NODE_SMOOTH;
+ case 'z': return NODE_SYMMETRIC;
+ default: return NODE_PICK_BEST;
+ }
+}
+
+bool Node::_eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event)
+{
+ int dir = 0;
+
+ switch (event->type)
+ {
+ case GDK_SCROLL:
+ if (event->scroll.direction == GDK_SCROLL_UP) {
+ dir = 1;
+ } else if (event->scroll.direction == GDK_SCROLL_DOWN) {
+ dir = -1;
+ } else if (event->scroll.direction == GDK_SCROLL_SMOOTH) {
+ dir = event->scroll.delta_y > 0 ? -1 : 1;
+ } else {
+ break;
+ }
+ if (held_control(event->scroll)) {
+ _linearGrow(dir);
+ } else {
+ _selection.spatialGrow(this, dir);
+ }
+ return true;
+ case GDK_KEY_PRESS:
+ switch (shortcut_key(event->key))
+ {
+ case GDK_KEY_Page_Up:
+ dir = 1;
+ break;
+ case GDK_KEY_Page_Down:
+ dir = -1;
+ break;
+ default: goto bail_out;
+ }
+
+ if (held_control(event->key)) {
+ _linearGrow(dir);
+ } else {
+ _selection.spatialGrow(this, dir);
+ }
+ return true;
+
+ default:
+ break;
+ }
+
+ bail_out:
+ return ControlPoint::_eventHandler(event_context, event);
+}
+
+void Node::_linearGrow(int dir)
+{
+ // Interestingly, we do not need any help from PathManipulator when doing linear grow.
+ // First handle the trivial case of growing over an unselected node.
+ if (!selected() && dir > 0) {
+ _selection.insert(this);
+ return;
+ }
+
+ NodeList::iterator this_iter = NodeList::get_iterator(this);
+ NodeList::iterator fwd = this_iter, rev = this_iter;
+ double distance_back = 0, distance_front = 0;
+
+ // Linear grow is simple. We find the first unselected nodes in each direction
+ // and compare the linear distances to them.
+ if (dir > 0) {
+ if (!selected()) {
+ _selection.insert(this);
+ return;
+ }
+
+ // find first unselected nodes on both sides
+ while (fwd && fwd->selected()) {
+ NodeList::iterator n = fwd.next();
+ distance_front += Geom::bezier_length(*fwd, fwd->_front, n->_back, *n);
+ fwd = n;
+ if (fwd == this_iter)
+ // there is no unselected node in this cyclic subpath
+ return;
+ }
+ // do the same for the second direction. Do not check for equality with
+ // this node, because there is at least one unselected node in the subpath,
+ // so we are guaranteed to stop.
+ while (rev && rev->selected()) {
+ NodeList::iterator p = rev.prev();
+ distance_back += Geom::bezier_length(*rev, rev->_back, p->_front, *p);
+ rev = p;
+ }
+
+ NodeList::iterator t; // node to select
+ if (fwd && rev) {
+ if (distance_front <= distance_back) t = fwd;
+ else t = rev;
+ } else {
+ if (fwd) t = fwd;
+ if (rev) t = rev;
+ }
+ if (t) _selection.insert(t.ptr());
+
+ // Linear shrink is more complicated. We need to find the farthest selected node.
+ // This means we have to check the entire subpath. We go in the direction in which
+ // the distance we traveled is lower. We do this until we run out of nodes (ends of path)
+ // or the two iterators meet. On the way, we store the last selected node and its distance
+ // in each direction (if any). At the end, we choose the one that is farther and deselect it.
+ } else {
+ // both iterators that store last selected nodes are initially empty
+ NodeList::iterator last_fwd, last_rev;
+ double last_distance_back = 0, last_distance_front = 0;
+
+ while (rev || fwd) {
+ if (fwd && (!rev || distance_front <= distance_back)) {
+ if (fwd->selected()) {
+ last_fwd = fwd;
+ last_distance_front = distance_front;
+ }
+ NodeList::iterator n = fwd.next();
+ if (n) distance_front += Geom::bezier_length(*fwd, fwd->_front, n->_back, *n);
+ fwd = n;
+ } else if (rev && (!fwd || distance_front > distance_back)) {
+ if (rev->selected()) {
+ last_rev = rev;
+ last_distance_back = distance_back;
+ }
+ NodeList::iterator p = rev.prev();
+ if (p) distance_back += Geom::bezier_length(*rev, rev->_back, p->_front, *p);
+ rev = p;
+ }
+ // Check whether we walked the entire cyclic subpath.
+ // This is initially true because both iterators start from this node,
+ // so this check cannot go in the while condition.
+ // When this happens, we need to check the last node, pointed to by the iterators.
+ if (fwd && fwd == rev) {
+ if (!fwd->selected()) break;
+ NodeList::iterator fwdp = fwd.prev(), revn = rev.next();
+ double df = distance_front + Geom::bezier_length(*fwdp, fwdp->_front, fwd->_back, *fwd);
+ double db = distance_back + Geom::bezier_length(*revn, revn->_back, rev->_front, *rev);
+ if (df > db) {
+ last_fwd = fwd;
+ last_distance_front = df;
+ } else {
+ last_rev = rev;
+ last_distance_back = db;
+ }
+ break;
+ }
+ }
+
+ NodeList::iterator t;
+ if (last_fwd && last_rev) {
+ if (last_distance_front >= last_distance_back) t = last_fwd;
+ else t = last_rev;
+ } else {
+ if (last_fwd) t = last_fwd;
+ if (last_rev) t = last_rev;
+ }
+ if (t) _selection.erase(t.ptr());
+ }
+}
+
+void Node::_setState(State state)
+{
+ // change node size to match type and selection state
+ _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_point, back_point;
+ 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 is straight
+ if (_is_line_segment(this, _next())) {
+ front_point = _next()->position() - origin;
+ if (_next()->selected()) {
+ dummy_cp = _next()->position() - position();
+ scp_free.addVector(dummy_cp);
+ } else {
+ dummy_cp = _next()->position();
+ scp_free.addOrigin(dummy_cp);
+ }
+ }
+ } else { // .. this path segment is curved
+ front_point = _front.relativePos();
+ scp_free.addVector(*front_point);
+ }
+ if (_back.isDegenerate()) { // If there is no handle for the path segment towards the previous node, then this segment is straight
+ if (_is_line_segment(_prev(), this)) {
+ back_point = _prev()->position() - origin;
+ if (_prev()->selected()) {
+ dummy_cp = _prev()->position() - position();
+ scp_free.addVector(dummy_cp);
+ } else {
+ dummy_cp = _prev()->position();
+ scp_free.addOrigin(dummy_cp);
+ }
+ }
+ } else { // .. this path segment is curved
+ back_point = _back.relativePos();
+ scp_free.addVector(*back_point);
+ }
+
+ if (held_control(*event)) {
+ // We're about to consider a constrained snap, which is already limited to 1D
+ // Therefore tangential or perpendicular snapping will not be considered, and therefore
+ // all calls above to scp_free.addVector() and scp_free.addOrigin() can be neglected
+ std::vector<Inkscape::Snapper::SnapConstraint> constraints;
+ if (held_alt(*event)) { // with Ctrl+Alt, constrain to handle lines
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
+ double min_angle = M_PI / snaps;
+
+ // If the node is cusp and both handles are retracted (degenerate; straight line segment at both sides of the node), then snap to those line segments
+ if (isDegenerate()) { // Cusp node with both handles retracted
+ if (front_point) {
+ constraints.emplace_back(origin, *front_point);
+ }
+ if (back_point) {
+ constraints.emplace_back(origin, *back_point);
+ }
+ }
+
+ // 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> fperp_point = Geom::rot90(*front_point);
+ if (fperp_point && (!back_point ||
+ (fabs(Geom::angle_between(*fperp_point, *back_point)) > min_angle &&
+ fabs(Geom::angle_between(*fperp_point, *back_point)) < M_PI - min_angle)))
+ {
+ constraints.emplace_back(origin, *fperp_point);
+ }
+
+ std::optional<Geom::Point> bperp_point = Geom::rot90(*back_point);
+ if (bperp_point && (!front_point ||
+ (fabs(Geom::angle_between(*bperp_point, *front_point)) > min_angle &&
+ fabs(Geom::angle_between(*bperp_point, *front_point)) < M_PI - min_angle)))
+ {
+ constraints.emplace_back(origin, *bperp_point);
+ }
+ }
+
+ sp = sm.multipleConstrainedSnaps(Inkscape::SnapCandidatePoint(new_pos, _snapSourceType()), constraints, held_shift(*event));
+ } else {
+ // with Ctrl, constrain to axes
+ constraints.emplace_back(origin, Geom::Point(1, 0));
+ constraints.emplace_back(origin, Geom::Point(0, 1));
+ sp = sm.multipleConstrainedSnaps(Inkscape::SnapCandidatePoint(new_pos, _snapSourceType()), constraints, held_shift(*event));
+ }
+ new_pos = sp.getPoint();
+ } else if (snap) {
+ Inkscape::SnappedPoint sp = sm.freeSnap(scp_free);
+ new_pos = sp.getPoint();
+ }
+
+ sm.unSetup();
+
+ SelectableControlPoint::dragged(new_pos, event);
+}
+
+bool Node::clicked(GdkEventButton *event)
+{
+ if(_pm()._nodeClicked(this, event))
+ return true;
+ return SelectableControlPoint::clicked(event);
+}
+
+Inkscape::SnapSourceType Node::_snapSourceType() const
+{
+ if (_type == NODE_SMOOTH || _type == NODE_AUTO)
+ return SNAPSOURCE_NODE_SMOOTH;
+ return SNAPSOURCE_NODE_CUSP;
+}
+Inkscape::SnapTargetType Node::_snapTargetType() const
+{
+ if (_type == NODE_SMOOTH || _type == NODE_AUTO)
+ return SNAPTARGET_NODE_SMOOTH;
+ return SNAPTARGET_NODE_CUSP;
+}
+
+Inkscape::SnapCandidatePoint Node::snapCandidatePoint()
+{
+ return SnapCandidatePoint(position(), _snapSourceType(), _snapTargetType());
+}
+
+Handle *Node::handleToward(Node *to)
+{
+ if (_next() == to) {
+ return front();
+ }
+ if (_prev() == to) {
+ return back();
+ }
+ g_error("Node::handleToward(): second node is not adjacent!");
+ return nullptr;
+}
+
+Node *Node::nodeToward(Handle *dir)
+{
+ if (front() == dir) {
+ return _next();
+ }
+ if (back() == dir) {
+ return _prev();
+ }
+ g_error("Node::nodeToward(): handle is not a child of this node!");
+ return nullptr;
+}
+
+Handle *Node::handleAwayFrom(Node *to)
+{
+ if (_next() == to) {
+ return back();
+ }
+ if (_prev() == to) {
+ return front();
+ }
+ g_error("Node::handleAwayFrom(): second node is not adjacent!");
+ return nullptr;
+}
+
+Node *Node::nodeAwayFrom(Handle *h)
+{
+ if (front() == h) {
+ return _prev();
+ }
+ if (back() == h) {
+ return _next();
+ }
+ g_error("Node::nodeAwayFrom(): handle is not a child of this node!");
+ return nullptr;
+}
+
+Glib::ustring Node::_getTip(unsigned state) const
+{
+ bool isBSpline = _pm()._isBSpline();
+ Handle *h = const_cast<Handle *>(&_front);
+ Glib::ustring s = C_("Path node tip",
+ "node handle"); // not expected
+
+ if (state_held_shift(state)) {
+ bool can_drag_out = (_next() && _front.isDegenerate()) ||
+ (_prev() && _back.isDegenerate());
+
+ if (can_drag_out) {
+ /*if (state_held_control(state)) {
+ s = format_tip(C_("Path node tip",
+ "<b>Shift+Ctrl:</b> drag out a handle and snap its angle "
+ "to %f° increments"), snap_increment_degrees());
+ }*/
+ s = C_("Path node tip",
+ "<b>Shift</b>: drag out a handle, click to toggle selection");
+ }
+ else {
+ s = C_("Path node tip",
+ "<b>Shift</b>: click to toggle selection");
+ }
+ }
+
+ else if (state_held_control(state)) {
+ if (state_held_alt(state)) {
+ s = C_("Path node tip",
+ "<b>Ctrl+Alt</b>: move along handle lines, click to delete node");
+ }
+ else {
+ s = C_("Path node tip",
+ "<b>Ctrl</b>: move along axes, click to change node type");
+ }
+ }
+
+ else if (state_held_alt(state)) {
+ s = C_("Path node tip",
+ "<b>Alt</b>: sculpt nodes");
+ }
+
+ else { // No modifiers: assemble tip from node type
+ char const *nodetype = node_type_to_localized_string(_type);
+ double power = _pm()._bsplineHandlePosition(h);
+
+ if (_selection.transformHandlesEnabled() && selected()) {
+ if (_selection.size() == 1) {
+ if (!isBSpline) {
+ s = format_tip(C_("Path node tip",
+ "<b>%s</b>: "
+ "drag to shape the path" ". "
+ "(more: Shift, Ctrl, Alt)"),
+ nodetype);
+ }
+ else {
+ s = format_tip(C_("Path node tip",
+ "<b>BSpline node</b> (%.3g power): "
+ "drag to shape the path" ". "
+ "(more: Shift, Ctrl, Alt)"),
+ power);
+ }
+ }
+ else {
+ s = format_tip(C_("Path node tip",
+ "<b>%s</b>: "
+ "drag to shape the path" ", "
+ "click to toggle scale/rotation handles" ". "
+ "(more: Shift, Ctrl, Alt)"),
+ nodetype);
+ }
+ }
+ else if (!isBSpline) {
+ s = format_tip(C_("Path node tip",
+ "<b>%s</b>: "
+ "drag to shape the path" ", "
+ "click to select only this node" ". "
+ "(more: Shift, Ctrl, Alt)"),
+ nodetype);
+ }
+ else {
+ s = format_tip(C_("Path node tip",
+ "<b>BSpline node</b> (%.3g power): "
+ "drag to shape the path" ", "
+ "click to select only this node" ". "
+ "(more: Shift, Ctrl, Alt)"),
+ power);
+ }
+ }
+
+ return (s);
+}
+
+Glib::ustring Node::_getDragTip(GdkEventMotion */*event*/) const
+{
+ Geom::Point dist = position() - _last_drag_origin();
+
+ Inkscape::Util::Quantity x_q = Inkscape::Util::Quantity(dist[Geom::X], "px");
+ Inkscape::Util::Quantity y_q = Inkscape::Util::Quantity(dist[Geom::Y], "px");
+ Glib::ustring x = x_q.string(_desktop->namedview->display_units);
+ Glib::ustring y = y_q.string(_desktop->namedview->display_units);
+ Glib::ustring ret = format_tip(C_("Path node tip", "Move node by %s, %s"), x.c_str(), y.c_str());
+ return ret;
+}
+
+/**
+ * See also: Handle::handle_type_to_localized_string(NodeType type)
+ */
+char const *Node::node_type_to_localized_string(NodeType type)
+{
+ switch (type) {
+ case NODE_CUSP:
+ return _("Corner node");
+ case NODE_SMOOTH:
+ return _("Smooth node");
+ case NODE_SYMMETRIC:
+ return _("Symmetric node");
+ case NODE_AUTO:
+ return _("Auto-smooth node");
+ default:
+ return "";
+ }
+}
+
+bool Node::_is_line_segment(Node *first, Node *second)
+{
+ if (!first || !second) return false;
+ if (first->_next() == second)
+ return first->_front.isDegenerate() && second->_back.isDegenerate();
+ if (second->_next() == first)
+ return second->_front.isDegenerate() && first->_back.isDegenerate();
+ return false;
+}
+
+NodeList::NodeList(SubpathList &splist)
+ : _list(splist)
+ , _closed(false)
+{
+ this->ln_list = this;
+ this->ln_next = this;
+ this->ln_prev = this;
+}
+
+NodeList::~NodeList()
+{
+ clear();
+}
+
+bool NodeList::empty()
+{
+ return ln_next == this;
+}
+
+NodeList::size_type NodeList::size()
+{
+ size_type sz = 0;
+ for (ListNode *ln = ln_next; ln != this; ln = ln->ln_next) ++sz;
+ return sz;
+}
+
+bool NodeList::closed()
+{
+ return _closed;
+}
+
+bool NodeList::degenerate()
+{
+ return closed() ? empty() : ++begin() == end();
+}
+
+NodeList::iterator NodeList::before(double t, double *fracpart)
+{
+ double intpart;
+ *fracpart = std::modf(t, &intpart);
+ int index = intpart;
+
+ iterator ret = begin();
+ std::advance(ret, index);
+ return ret;
+}
+
+NodeList::iterator NodeList::before(Geom::PathTime const &pvp)
+{
+ iterator ret = begin();
+ std::advance(ret, pvp.curve_index);
+ return ret;
+}
+
+NodeList::iterator NodeList::insert(iterator pos, Node *x)
+{
+ ListNode *ins = pos._node;
+ x->ln_next = ins;
+ x->ln_prev = ins->ln_prev;
+ ins->ln_prev->ln_next = x;
+ ins->ln_prev = x;
+ x->ln_list = this;
+ return iterator(x);
+}
+
+void NodeList::splice(iterator pos, NodeList &list)
+{
+ splice(pos, list, list.begin(), list.end());
+}
+
+void NodeList::splice(iterator pos, NodeList &list, iterator i)
+{
+ NodeList::iterator j = i;
+ ++j;
+ splice(pos, list, i, j);
+}
+
+void NodeList::splice(iterator pos, NodeList &/*list*/, iterator first, iterator last)
+{
+ ListNode *ins_beg = first._node, *ins_end = last._node, *at = pos._node;
+ for (ListNode *ln = ins_beg; ln != ins_end; ln = ln->ln_next) {
+ ln->ln_list = this;
+ }
+ ins_beg->ln_prev->ln_next = ins_end;
+ ins_end->ln_prev->ln_next = at;
+ at->ln_prev->ln_next = ins_beg;
+
+ ListNode *atprev = at->ln_prev;
+ at->ln_prev = ins_end->ln_prev;
+ ins_end->ln_prev = ins_beg->ln_prev;
+ ins_beg->ln_prev = atprev;
+}
+
+void NodeList::shift(int n)
+{
+ // 1. make the list perfectly cyclic
+ ln_next->ln_prev = ln_prev;
+ ln_prev->ln_next = ln_next;
+ // 2. find new begin
+ ListNode *new_begin = ln_next;
+ if (n > 0) {
+ for (; n > 0; --n) new_begin = new_begin->ln_next;
+ } else {
+ for (; n < 0; ++n) new_begin = new_begin->ln_prev;
+ }
+ // 3. relink begin to list
+ ln_next = new_begin;
+ ln_prev = new_begin->ln_prev;
+ new_begin->ln_prev->ln_next = this;
+ new_begin->ln_prev = this;
+}
+
+void NodeList::reverse()
+{
+ for (ListNode *ln = ln_next; ln != this; ln = ln->ln_prev) {
+ std::swap(ln->ln_next, ln->ln_prev);
+ Node *node = static_cast<Node*>(ln);
+ Geom::Point save_pos = node->front()->position();
+ node->front()->setPosition(node->back()->position());
+ node->back()->setPosition(save_pos);
+ }
+ std::swap(ln_next, ln_prev);
+}
+
+void NodeList::clear()
+{
+ // ugly but more efficient clearing mechanism
+ std::vector<ControlPointSelection *> to_clear;
+ std::vector<std::pair<SelectableControlPoint *, long> > nodes;
+ long in = -1;
+ for (iterator i = begin(); i != end(); ++i) {
+ SelectableControlPoint *rm = static_cast<Node*>(i._node);
+ if (std::find(to_clear.begin(), to_clear.end(), &rm->_selection) == to_clear.end()) {
+ to_clear.push_back(&rm->_selection);
+ ++in;
+ }
+ nodes.emplace_back(rm, in);
+ }
+ for (size_t i = 0, e = nodes.size(); i != e; ++i) {
+ to_clear[nodes[i].second]->erase(nodes[i].first, false);
+ }
+ std::vector<std::vector<SelectableControlPoint *> > emission;
+ for (long i = 0, e = to_clear.size(); i != e; ++i) {
+ emission.emplace_back();
+ for (size_t j = 0, f = nodes.size(); j != f; ++j) {
+ if (nodes[j].second != i)
+ break;
+ emission[i].push_back(nodes[j].first);
+ }
+ }
+
+ for (size_t i = 0, e = emission.size(); i != e; ++i) {
+ to_clear[i]->signal_selection_changed.emit(emission[i], false);
+ }
+
+ for (iterator i = begin(); i != end();)
+ erase (i++);
+}
+
+NodeList::iterator NodeList::erase(iterator i)
+{
+ // some gymnastics are required to ensure that the node is valid when deleted;
+ // otherwise the code that updates handle visibility will break
+ Node *rm = static_cast<Node*>(i._node);
+ ListNode *rmnext = rm->ln_next, *rmprev = rm->ln_prev;
+ ++i;
+ delete rm;
+ rmprev->ln_next = rmnext;
+ rmnext->ln_prev = rmprev;
+ return i;
+}
+
+// TODO this method is very ugly!
+// converting SubpathList to an intrusive list might allow us to get rid of it
+void NodeList::kill()
+{
+ for (SubpathList::iterator i = _list.begin(); i != _list.end(); ++i) {
+ if (i->get() == this) {
+ _list.erase(i);
+ return;
+ }
+ }
+}
+
+NodeList &NodeList::get(Node *n) {
+ return n->nodeList();
+}
+NodeList &NodeList::get(iterator const &i) {
+ return *(i._node->ln_list);
+}
+
+
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/tool/node.h b/src/ui/tool/node.h
new file mode 100644
index 0000000..0630e4f
--- /dev/null
+++ b/src/ui/tool/node.h
@@ -0,0 +1,532 @@
+// 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 {
+template <typename> class NodeIterator;
+}
+}
+
+namespace Inkscape {
+namespace UI {
+
+class PathManipulator;
+class MultiPathManipulator;
+
+class Node;
+class Handle;
+class NodeList;
+class SubpathList;
+template <typename> class NodeIterator;
+
+std::ostream &operator<<(std::ostream &, NodeType);
+
+/*
+template <typename T>
+struct ListMember {
+ T *next;
+ T *prev;
+};
+struct SubpathMember : public ListMember<NodeListMember> {
+ Subpath *list;
+};
+struct SubpathListMember : public ListMember<SubpathListMember> {
+ SubpathList *list;
+};
+*/
+
+struct ListNode {
+ ListNode *ln_next;
+ ListNode *ln_prev;
+ NodeList *ln_list;
+};
+
+struct NodeSharedData {
+ SPDesktop *desktop;
+ ControlPointSelection *selection;
+ 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;
+ 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
+ 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..7f30633
--- /dev/null
+++ b/src/ui/tool/path-manipulator.cpp
@@ -0,0 +1,1804 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Path manipulator - implementation.
+ */
+/* Authors:
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <2geom/bezier-utils.h>
+#include <2geom/path-sink.h>
+
+#include <utility>
+
+#include "display/curve.h"
+#include "display/control/canvas-item-bpath.h"
+
+#include "helper/geom.h"
+
+#include "live_effects/lpeobject.h"
+#include "live_effects/lpeobject-reference.h"
+#include "live_effects/lpe-powerstroke.h"
+#include "live_effects/lpe-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/path-manipulator.h"
+#include "ui/tools/node-tool.h"
+
+#include "xml/node-observer.h"
+
+namespace Inkscape {
+namespace UI {
+
+namespace {
+/// Types of path changes that we must react to.
+enum PathChange {
+ PATH_CHANGE_D,
+ PATH_CHANGE_TRANSFORM
+};
+
+} // anonymous namespace
+const double HANDLE_CUBIC_GAP = 0.001;
+const double NO_POWER = 0.0;
+const double DEFAULT_START_POWER = 1.0/3.0;
+
+
+/**
+ * Notifies the path manipulator when something changes the path being edited
+ * (e.g. undo / redo)
+ */
+class PathManipulatorObserver : public Inkscape::XML::NodeObserver {
+public:
+ PathManipulatorObserver(PathManipulator *p, Inkscape::XML::Node *node)
+ : _pm(p)
+ , _node(node)
+ , _blocked(false)
+ {
+ Inkscape::GC::anchor(_node);
+ _node->addObserver(*this);
+ }
+
+ ~PathManipulatorObserver() override {
+ _node->removeObserver(*this);
+ Inkscape::GC::release(_node);
+ }
+
+ void notifyAttributeChanged(Inkscape::XML::Node &/*node*/, GQuark attr,
+ Util::ptr_shared, Util::ptr_shared) override
+ {
+ // do nothing if blocked
+ if (_blocked) return;
+
+ GQuark path_d = g_quark_from_static_string("d");
+ GQuark path_transform = g_quark_from_static_string("transform");
+ GQuark lpe_quark = _pm->_lpe_key.empty() ? 0 : g_quark_from_string(_pm->_lpe_key.data());
+
+ // only react to "d" (path data) and "transform" attribute changes
+ if (attr == lpe_quark || attr == path_d) {
+ _pm->_externalChange(PATH_CHANGE_D);
+ } else if (attr == path_transform) {
+ _pm->_externalChange(PATH_CHANGE_TRANSFORM);
+ }
+ }
+
+ void block() { _blocked = true; }
+ void unblock() { _blocked = false; }
+private:
+ PathManipulator *_pm;
+ Inkscape::XML::Node *_node;
+ bool _blocked;
+};
+
+void build_segment(Geom::PathBuilder &, Node *, Node *);
+PathManipulator::PathManipulator(MultiPathManipulator &mpm, SPObject *path,
+ Geom::Affine const &et, guint32 outline_color, Glib::ustring lpe_key)
+ : PointManipulator(mpm._path_data.node_data.desktop, *mpm._path_data.node_data.selection)
+ , _subpaths(*this)
+ , _multi_path_manipulator(mpm)
+ , _path(path)
+ , _spcurve(new SPCurve())
+ , _dragpoint(new CurveDragPoint(*this))
+ , /* XML Tree being used here directly while it shouldn't be*/_observer(new PathManipulatorObserver(this, path->getRepr()))
+ , _edit_transform(et)
+ , _lpe_key(std::move(lpe_key))
+{
+ LivePathEffectObject *lpeobj = dynamic_cast<LivePathEffectObject *>(_path);
+ SPPath *pathshadow = dynamic_cast<SPPath *>(_path);
+ if (!lpeobj) {
+ _i2d_transform = pathshadow->i2dt_affine();
+ } else {
+ _i2d_transform = Geom::identity();
+ }
+ _d2i_transform = _i2d_transform.inverse();
+ _dragpoint->setVisible(false);
+
+ _getGeometry();
+
+ _outline = new 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)));
+
+ _createControlPointsFromGeometry();
+ //Define if the path is BSpline on construction
+ _recalculateIsBSpline();
+}
+
+PathManipulator::~PathManipulator()
+{
+ delete _dragpoint;
+ delete _observer;
+ delete _outline;
+ 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 || dynamic_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(bool keep_shape)
+{
+ if (_selection.empty()) return;
+ hideDragPoint();
+
+ for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end();) {
+ SubpathPtr sp = *i;
+
+ // If there are less than 2 unselected nodes in an open subpath or no unselected nodes
+ // in a closed one, delete entire subpath.
+ unsigned num_unselected = 0, num_selected = 0;
+ for (auto & j : *sp) {
+ if (j.selected()) ++num_selected;
+ else ++num_unselected;
+ }
+ if (num_selected == 0) {
+ ++i;
+ continue;
+ }
+ if (sp->closed() ? (num_unselected < 1) : (num_unselected < 2)) {
+ _subpaths.erase(i++);
+ continue;
+ }
+
+ // In closed paths, start from an unselected node - otherwise we might start in the middle
+ // of a selected stretch and the resulting bezier fit would be suboptimal
+ NodeList::iterator sel_beg = sp->begin(), sel_end;
+ if (sp->closed()) {
+ while (sel_beg->selected()) ++sel_beg;
+ }
+ sel_end = sel_beg;
+
+ while (num_selected > 0) {
+ while (sel_beg && !sel_beg->selected()) {
+ sel_beg = sel_beg.next();
+ }
+ sel_end = sel_beg;
+
+ while (sel_end && sel_end->selected()) {
+ sel_end = sel_end.next();
+ }
+
+ num_selected -= _deleteStretch(sel_beg, sel_end, keep_shape);
+ sel_beg = sel_end;
+ }
+ ++i;
+ }
+}
+
+/**
+ * Delete nodes between the two iterators.
+ * The given range can cross the beginning of the subpath in closed subpaths.
+ * @param start Beginning of the range to delete
+ * @param end End of the range
+ * @param keep_shape Whether to fit the handles at surrounding nodes to approximate
+ * the shape before deletion
+ * @return Number of deleted nodes
+ */
+unsigned PathManipulator::_deleteStretch(NodeList::iterator start, NodeList::iterator end, bool keep_shape)
+{
+ unsigned const samples_per_segment = 10;
+ double const t_step = 1.0 / samples_per_segment;
+
+ unsigned del_len = 0;
+ for (NodeList::iterator i = start; i != end; i = i.next()) {
+ ++del_len;
+ }
+ if (del_len == 0) return 0;
+
+ // set surrounding node types to cusp if:
+ // 1. keep_shape is on, or
+ // 2. we are deleting at the end or beginning of an open path
+ if ((keep_shape || !end) && start.prev()) start.prev()->setType(NODE_CUSP, false);
+ if ((keep_shape || !start.prev()) && end) end->setType(NODE_CUSP, false);
+
+ if (keep_shape && start.prev() && end) {
+ unsigned num_samples = (del_len + 1) * samples_per_segment + 1;
+ Geom::Point *bezier_data = new Geom::Point[num_samples];
+ Geom::Point result[4];
+ unsigned seg = 0;
+
+ for (NodeList::iterator cur = start.prev(); cur != end; cur = cur.next()) {
+ Geom::CubicBezier bc(*cur, *cur->front(), *cur.next(), *cur.next()->back());
+ for (unsigned s = 0; s < samples_per_segment; ++s) {
+ bezier_data[seg * samples_per_segment + s] = bc.pointAt(t_step * s);
+ }
+ ++seg;
+ }
+ // Fill last point
+ bezier_data[num_samples - 1] = end->position();
+ // Compute replacement bezier curve
+ // TODO the fitting algorithm sucks - rewrite it to be awesome
+ bezier_fit_cubic(result, bezier_data, num_samples, 0.5);
+ delete[] bezier_data;
+
+ start.prev()->front()->setPosition(result[1]);
+ end->back()->setPosition(result[2]);
+ }
+
+ // We can't use nl->erase(start, end), because it would break when the stretch
+ // crosses the beginning of a closed subpath
+ NodeList &nl = start->nodeList();
+ while (start != end) {
+ NodeList::iterator next = start.next();
+ nl.erase(start);
+ start = next;
+ }
+ // if we are removing, we readjust the handlers
+ if(_isBSpline()){
+ if(start.prev()){
+ double bspline_weight = _bsplineHandlePosition(start.prev()->back(), false);
+ start.prev()->front()->setPosition(_bsplineHandleReposition(start.prev()->front(), bspline_weight));
+ }
+ if(end){
+ double bspline_weight = _bsplineHandlePosition(end->front(), false);
+ end->back()->setPosition(_bsplineHandleReposition(end->back(),bspline_weight));
+ }
+ }
+
+ return del_len;
+}
+
+/** Removes selected segments */
+void PathManipulator::deleteSegments()
+{
+ if (_selection.empty()) return;
+ hideDragPoint();
+
+ for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end();) {
+ SubpathPtr sp = *i;
+ bool has_unselected = false;
+ unsigned num_selected = 0;
+ for (auto & j : *sp) {
+ if (j.selected()) {
+ ++num_selected;
+ } else {
+ has_unselected = true;
+ }
+ }
+ if (!has_unselected) {
+ _subpaths.erase(i++);
+ continue;
+ }
+
+ NodeList::iterator sel_beg = sp->begin();
+ if (sp->closed()) {
+ while (sel_beg && sel_beg->selected()) ++sel_beg;
+ }
+ while (num_selected > 0) {
+ if (!sel_beg->selected()) {
+ sel_beg = sel_beg.next();
+ continue;
+ }
+ NodeList::iterator sel_end = sel_beg;
+ unsigned num_points = 0;
+ while (sel_end && sel_end->selected()) {
+ sel_end = sel_end.next();
+ ++num_points;
+ }
+ if (num_points >= 2) {
+ // Retract end handles
+ sel_end.prev()->setType(NODE_CUSP, false);
+ sel_end.prev()->back()->retract();
+ sel_beg->setType(NODE_CUSP, false);
+ sel_beg->front()->retract();
+ if (sp->closed()) {
+ // In closed paths, relocate the beginning of the path to the last selected
+ // node and then unclose it. Remove the nodes from the first selected node
+ // to the new end of path.
+ if (sel_end.prev() != sp->begin())
+ sp->splice(sp->begin(), *sp, sel_end.prev(), sp->end());
+ sp->setClosed(false);
+ sp->erase(sel_beg.next(), sp->end());
+ } else {
+ // for open paths:
+ // 1. At end or beginning, delete including the node on the end or beginning
+ // 2. In the middle, delete only inner nodes
+ if (sel_beg == sp->begin()) {
+ sp->erase(sp->begin(), sel_end.prev());
+ } else if (sel_end == sp->end()) {
+ sp->erase(sel_beg.next(), sp->end());
+ } else {
+ SubpathPtr new_sp(new NodeList(_subpaths));
+ new_sp->splice(new_sp->end(), *sp, sp->begin(), sel_beg.next());
+ _subpaths.insert(i, new_sp);
+ if (sel_end.prev())
+ sp->erase(sp->begin(), sel_end.prev());
+ }
+ }
+ }
+ sel_beg = sel_end;
+ num_selected -= num_points;
+ }
+ ++i;
+ }
+}
+
+/** Reverse subpaths of the path.
+ * @param selected_only If true, only paths that have at least one selected node
+ * will be reversed. Otherwise all subpaths will be reversed. */
+void PathManipulator::reverseSubpaths(bool selected_only)
+{
+ for (auto & _subpath : _subpaths) {
+ if (selected_only) {
+ for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) {
+ if (j->selected()) {
+ _subpath->reverse();
+ break; // continue with the next subpath
+ }
+ }
+ } else {
+ _subpath->reverse();
+ }
+ }
+}
+
+/** Make selected segments curves / lines. */
+void PathManipulator::setSegmentType(SegmentType type)
+{
+ if (_selection.empty()) return;
+ for (auto & _subpath : _subpaths) {
+ for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) {
+ NodeList::iterator k = j.next();
+ if (!(k && j->selected() && k->selected())) continue;
+ switch (type) {
+ case SEGMENT_STRAIGHT:
+ if (j->front()->isDegenerate() && k->back()->isDegenerate())
+ break;
+ j->front()->move(*j);
+ k->back()->move(*k);
+ break;
+ case SEGMENT_CUBIC_BEZIER:
+ if (!j->front()->isDegenerate() || !k->back()->isDegenerate())
+ break;
+ // move both handles to 1/3 of the line
+ j->front()->move(j->position() + (k->position() - j->position()) / 3);
+ k->back()->move(k->position() + (j->position() - k->position()) / 3);
+ break;
+ }
+ }
+ }
+}
+
+void PathManipulator::scaleHandle(Node *n, int which, int dir, bool pixel)
+{
+ if (n->type() == NODE_SYMMETRIC || n->type() == NODE_AUTO) {
+ n->setType(NODE_SMOOTH);
+ }
+ Handle *h = _chooseHandle(n, which);
+ double length_change;
+
+ if (pixel) {
+ length_change = 1.0 / _desktop->current_zoom() * dir;
+ } else {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ length_change = prefs->getDoubleLimited("/options/defaultscale/value", 2, 1, 1000, "px");
+ length_change *= dir;
+ }
+
+ Geom::Point relpos;
+ if (h->isDegenerate()) {
+ if (dir < 0) return;
+ Node *nh = n->nodeToward(h);
+ if (!nh) return;
+ relpos = Geom::unit_vector(nh->position() - n->position()) * length_change;
+ } else {
+ relpos = h->relativePos();
+ double rellen = relpos.length();
+ relpos *= ((rellen + length_change) / rellen);
+ }
+ h->setRelativePos(relpos);
+ update();
+ gchar const *key = which < 0 ? "handle:scale:left" : "handle:scale:right";
+ _commit(_("Scale handle"), key);
+}
+
+void PathManipulator::rotateHandle(Node *n, int which, int dir, bool pixel)
+{
+ if (n->type() != NODE_CUSP) {
+ n->setType(NODE_CUSP);
+ }
+ Handle *h = _chooseHandle(n, which);
+ if (h->isDegenerate()) return;
+
+ double angle;
+ if (pixel) {
+ // Rotate by "one pixel"
+ angle = atan2(1.0 / _desktop->current_zoom(), h->length()) * dir;
+ } else {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
+ angle = M_PI * dir / snaps;
+ }
+
+ h->setRelativePos(h->relativePos() * Geom::Rotate(angle));
+ update();
+ gchar const *key = which < 0 ? "handle:rotate:left" : "handle:rotate:right";
+ _commit(_("Rotate handle"), key);
+}
+
+Handle *PathManipulator::_chooseHandle(Node *n, int which)
+{
+ NodeList::iterator i = NodeList::get_iterator(n);
+ Node *prev = i.prev().ptr();
+ Node *next = i.next().ptr();
+
+ // on an endnode, the remaining handle automatically wins
+ if (!next) return n->back();
+ if (!prev) return n->front();
+
+ // compare X coord offline segments
+ Geom::Point npos = next->position();
+ Geom::Point ppos = prev->position();
+ if (which < 0) {
+ // pick left handle.
+ // we just swap the handles and pick the right handle below.
+ std::swap(npos, ppos);
+ }
+
+ if (npos[Geom::X] >= ppos[Geom::X]) {
+ return n->front();
+ } else {
+ return n->back();
+ }
+}
+
+/** Set the visibility of handles. */
+void PathManipulator::showHandles(bool show)
+{
+ if (show == _show_handles) return;
+ if (show) {
+ for (auto & _subpath : _subpaths) {
+ for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) {
+ if (!j->selected()) continue;
+ j->showHandles(true);
+ if (j.prev()) j.prev()->showHandles(true);
+ if (j.next()) j.next()->showHandles(true);
+ }
+ }
+ } else {
+ for (auto & _subpath : _subpaths) {
+ for (auto & j : *_subpath) {
+ j.showHandles(false);
+ }
+ }
+ }
+ _show_handles = show;
+}
+
+/** Set the visibility of outline. */
+void PathManipulator::showOutline(bool show)
+{
+ if (show == _show_outline) return;
+ _show_outline = show;
+ _updateOutline();
+}
+
+void PathManipulator::showPathDirection(bool show)
+{
+ if (show == _show_path_direction) return;
+ _show_path_direction = show;
+ _updateOutline();
+}
+
+void PathManipulator::setLiveOutline(bool set)
+{
+ _live_outline = set;
+}
+
+void PathManipulator::setLiveObjects(bool set)
+{
+ _live_objects = set;
+}
+
+void PathManipulator::updateHandles()
+{
+ for (auto & _subpath : _subpaths) {
+ for (auto & j : *_subpath) {
+ j.updateHandles();
+ }
+ }
+}
+
+void PathManipulator::setControlsTransform(Geom::Affine const &tnew)
+{
+ Geom::Affine delta = _i2d_transform.inverse() * _edit_transform.inverse() * tnew * _i2d_transform;
+ _edit_transform = tnew;
+ for (auto & _subpath : _subpaths) {
+ for (auto & j : *_subpath) {
+ j.transform(delta);
+ }
+ }
+ _createGeometryFromControlPoints();
+}
+
+/** Hide the curve drag point until the next motion event.
+ * This should be called at the beginning of every method that can delete nodes.
+ * Otherwise the invalidated iterator in the dragpoint can cause crashes. */
+void PathManipulator::hideDragPoint()
+{
+ _dragpoint->setVisible(false);
+ _dragpoint->setIterator(NodeList::iterator());
+}
+
+/** Insert a node in the segment beginning with the supplied iterator,
+ * at the given time value */
+NodeList::iterator PathManipulator::subdivideSegment(NodeList::iterator first, double t)
+{
+ if (!first) throw std::invalid_argument("Subdivide after invalid iterator");
+ NodeList &list = NodeList::get(first);
+ NodeList::iterator second = first.next();
+ if (!second) throw std::invalid_argument("Subdivide after last node in open path");
+ if (first->type() == NODE_SYMMETRIC)
+ first->setType(NODE_SMOOTH, false);
+ if (second->type() == NODE_SYMMETRIC)
+ second->setType(NODE_SMOOTH, false);
+
+ // We need to insert the segment after 'first'. We can't simply use 'second'
+ // as the point of insertion, because when 'first' is the last node of closed path,
+ // the new node will be inserted as the first node instead.
+ NodeList::iterator insert_at = first;
+ ++insert_at;
+
+ NodeList::iterator inserted;
+ if (first->front()->isDegenerate() && second->back()->isDegenerate()) {
+ // for a line segment, insert a cusp node
+ Node *n = new Node(_multi_path_manipulator._path_data.node_data,
+ Geom::lerp(t, first->position(), second->position()));
+ n->setType(NODE_CUSP, false);
+ inserted = list.insert(insert_at, n);
+ } else {
+ // build bezier curve and subdivide
+ Geom::CubicBezier temp(first->position(), first->front()->position(),
+ second->back()->position(), second->position());
+ std::pair<Geom::CubicBezier, Geom::CubicBezier> div = temp.subdivide(t);
+ std::vector<Geom::Point> seg1 = div.first.controlPoints(), seg2 = div.second.controlPoints();
+
+ // set new handle positions
+ Node *n = new Node(_multi_path_manipulator._path_data.node_data, seg2[0]);
+ if(!_isBSpline()){
+ n->back()->setPosition(seg1[2]);
+ n->front()->setPosition(seg2[1]);
+ n->setType(NODE_SMOOTH, false);
+ } else {
+ Geom::D2< Geom::SBasis > sbasis_inside_nodes;
+ std::unique_ptr<SPCurve> line_inside_nodes(new SPCurve());
+ if(second->back()->isDegenerate()){
+ line_inside_nodes->moveto(n->position());
+ line_inside_nodes->lineto(second->position());
+ sbasis_inside_nodes = line_inside_nodes->first_segment()->toSBasis();
+ Geom::Point next = sbasis_inside_nodes.valueAt(DEFAULT_START_POWER);
+ next = Geom::Point(next[Geom::X] + HANDLE_CUBIC_GAP,next[Geom::Y] + HANDLE_CUBIC_GAP);
+ line_inside_nodes->reset();
+ n->front()->setPosition(next);
+ }else{
+ n->front()->setPosition(seg2[1]);
+ }
+ if(first->front()->isDegenerate()){
+ line_inside_nodes->moveto(n->position());
+ line_inside_nodes->lineto(first->position());
+ sbasis_inside_nodes = line_inside_nodes->first_segment()->toSBasis();
+ Geom::Point previous = sbasis_inside_nodes.valueAt(DEFAULT_START_POWER);
+ previous = Geom::Point(previous[Geom::X] + HANDLE_CUBIC_GAP,previous[Geom::Y] + HANDLE_CUBIC_GAP);
+ n->back()->setPosition(previous);
+ }else{
+ n->back()->setPosition(seg1[2]);
+ }
+ n->setType(NODE_CUSP, false);
+ }
+ inserted = list.insert(insert_at, n);
+
+ first->front()->move(seg1[1]);
+ second->back()->move(seg2[2]);
+ }
+ return inserted;
+}
+
+/** Find the node that is closest/farthest from the origin
+ * @param origin Point of reference
+ * @param search_selected Consider selected nodes
+ * @param search_unselected Consider unselected nodes
+ * @param closest If true, return closest node, if false, return farthest
+ * @return The matching node, or an empty iterator if none found
+ */
+NodeList::iterator PathManipulator::extremeNode(NodeList::iterator origin, bool search_selected,
+ bool search_unselected, bool closest)
+{
+ NodeList::iterator match;
+ double extr_dist = closest ? HUGE_VAL : -HUGE_VAL;
+ if (_selection.empty() && !search_unselected) return match;
+
+ for (auto & _subpath : _subpaths) {
+ for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) {
+ if(j->selected()) {
+ if (!search_selected) continue;
+ } else {
+ if (!search_unselected) continue;
+ }
+ double dist = Geom::distance(*j, *origin);
+ bool cond = closest ? (dist < extr_dist) : (dist > extr_dist);
+ if (cond) {
+ match = j;
+ extr_dist = dist;
+ }
+ }
+ }
+ return match;
+}
+
+/* Called 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: {
+ SPPath *path = dynamic_cast<SPPath *>(_path);
+ if (path) {
+ Geom::Affine i2d_change = _d2i_transform;
+ _i2d_transform = path->i2dt_affine();
+ _d2i_transform = _i2d_transform.inverse();
+ i2d_change *= _i2d_transform;
+ for (auto & _subpath : _subpaths) {
+ for (auto & j : *_subpath) {
+ j.transform(i2d_change);
+ }
+ }
+ _updateOutline();
+ }
+ } break;
+ default: break;
+ }
+}
+
+/** Create nodes and handles based on the XML of the edited path. */
+void PathManipulator::_createControlPointsFromGeometry()
+{
+ clear();
+
+ // sanitize pathvector and store it in SPCurve,
+ // so that _updateDragPoint doesn't crash on paths with naked movetos
+ Geom::PathVector pathv = pathv_to_linear_and_cubic_beziers(_spcurve->get_pathvector());
+ for (Geom::PathVector::iterator i = pathv.begin(); i != pathv.end(); ) {
+ // NOTE: this utilizes the fact that Geom::PathVector is an std::vector.
+ // When we erase an element, the next one slides into position,
+ // so we do not increment the iterator even though it is theoretically invalidated.
+ if (i->empty()) {
+ i = pathv.erase(i);
+ } else {
+ ++i;
+ }
+ }
+ if (pathv.empty()) {
+ return;
+ }
+ _spcurve->set_pathvector(pathv);
+
+ pathv *= (_edit_transform * _i2d_transform);
+
+ // in this loop, we know that there are no zero-segment subpaths
+ for (auto & pit : pathv) {
+ // prepare new subpath
+ SubpathPtr subpath(new NodeList(_subpaths));
+ _subpaths.push_back(subpath);
+
+ Node *previous_node = new Node(_multi_path_manipulator._path_data.node_data, pit.initialPoint());
+ subpath->push_back(previous_node);
+
+ bool closed = pit.closed();
+
+ for (Geom::Path::iterator cit = pit.begin(); cit != pit.end(); ++cit) {
+ Geom::Point pos = cit->finalPoint();
+ Node *current_node;
+ // if the closing segment is degenerate and the path is closed, we need to move
+ // the handle of the first node instead of creating a new one
+ if (closed && cit == --(pit.end())) {
+ current_node = subpath->begin().get_pointer();
+ } else {
+ /* regardless of segment type, create a new node at the end
+ * of this segment (unless this is the last segment of a closed path
+ * with a degenerate closing segment */
+ current_node = new Node(_multi_path_manipulator._path_data.node_data, pos);
+ subpath->push_back(current_node);
+ }
+ // if this is a bezier segment, move handles appropriately
+ // TODO: I don't know why the dynamic cast below doesn't want to work
+ // when I replace BezierCurve with CubicBezier. Might be a bug
+ // somewhere in pathv_to_linear_and_cubic_beziers
+ Geom::BezierCurve const *bezier = dynamic_cast<Geom::BezierCurve const*>(&*cit);
+ if (bezier && bezier->order() == 3)
+ {
+ previous_node->front()->setPosition((*bezier)[1]);
+ current_node ->back() ->setPosition((*bezier)[2]);
+ }
+ previous_node = current_node;
+ }
+ // If the path is closed, make the list cyclic
+ if (pit.closed()) subpath->setClosed(true);
+ }
+
+ // we need to set the nodetypes after all the handles are in place,
+ // so that pickBestType works correctly
+ // TODO maybe migrate to inkscape:node-types?
+ // TODO move this into SPPath - do not manipulate directly
+
+ //XML Tree being used here directly while it shouldn't be.
+ gchar const *nts_raw = _path ? _path->getRepr()->attribute(_nodetypesKey().data()) : nullptr;
+ /* Calculate the needed length of the nodetype string.
+ * For closed paths, the entry is duplicated for the starting node,
+ * so we can just use the count of segments including the closing one
+ * to include the extra end node. */
+ /* pad the string to required length with a bogus value.
+ * 'b' and any other letter not recognized by the parser causes the best fit to be set
+ * as the node type */
+ auto const *tsi = nts_raw ? nts_raw : "";
+ for (auto & _subpath : _subpaths) {
+ for (auto & j : *_subpath) {
+ char nodetype = (*tsi) ? (*tsi++) : 'b';
+ j.setType(Node::parse_nodetype(nodetype), false);
+ }
+ if (_subpath->closed() && *tsi) {
+ // STUPIDITY ALERT: it seems we need to use the duplicate type symbol instead of
+ // the first one to remain backward compatible.
+ _subpath->begin()->setType(Node::parse_nodetype(*tsi++), false);
+ }
+ }
+}
+
+//determines if the trace has a bspline effect and the number of steps that it takes
+int PathManipulator::_bsplineGetSteps() const {
+
+ LivePathEffect::LPEBSpline const *lpe_bsp = nullptr;
+
+ SPLPEItem * path = dynamic_cast<SPLPEItem *>(_path);
+ if (path){
+ if(path->hasPathEffect()){
+ Inkscape::LivePathEffect::Effect const *this_effect =
+ path->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(){
+ SPPath *path = dynamic_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){
+ std::unique_ptr<SPCurve> line_inside_nodes(new SPCurve());
+ line_inside_nodes->moveto(n->position());
+ line_inside_nodes->lineto(next_node->position());
+ if(!are_near(h->position(), n->position())){
+ pos = Geom::nearest_time(Geom::Point(h->position()[X] - HANDLE_CUBIC_GAP, h->position()[Y] - HANDLE_CUBIC_GAP), *line_inside_nodes->first_segment());
+ }
+ }
+ if (pos == NO_POWER && check_other){
+ return _bsplineHandlePosition(h->other(), false);
+ }
+ return pos;
+}
+
+// give the location for the handler in the corresponding position
+Geom::Point PathManipulator::_bsplineHandleReposition(Handle *h, bool check_other)
+{
+ double pos = this->_bsplineHandlePosition(h, check_other);
+ return _bsplineHandleReposition(h,pos);
+}
+
+// give the location for the handler to the specified position
+Geom::Point PathManipulator::_bsplineHandleReposition(Handle *h,double pos){
+ using Geom::X;
+ using Geom::Y;
+ Geom::Point ret = h->position();
+ Node *n = h->parent();
+ Geom::D2< Geom::SBasis > sbasis_inside_nodes;
+ std::unique_ptr<SPCurve> line_inside_nodes(new SPCurve());
+ Node * next_node = nullptr;
+ next_node = n->nodeToward(h);
+ if(next_node && pos != NO_POWER){
+ line_inside_nodes->moveto(n->position());
+ line_inside_nodes->lineto(next_node->position());
+ sbasis_inside_nodes = line_inside_nodes->first_segment()->toSBasis();
+ ret = sbasis_inside_nodes.valueAt(pos);
+ ret = Geom::Point(ret[X] + HANDLE_CUBIC_GAP, ret[Y] + HANDLE_CUBIC_GAP);
+ }else{
+ if(pos == NO_POWER){
+ ret = n->position();
+ }
+ }
+ return ret;
+}
+
+/** Construct the geometric representation of nodes and handles, update the outline
+ * and display
+ * \param alert_LPE if true, first the LPE is warned what the new path is going to be before updating it
+ */
+void PathManipulator::_createGeometryFromControlPoints(bool alert_LPE)
+{
+ Geom::PathBuilder builder;
+ //Refresh if is bspline some times -think on path change selection, this value get lost
+ _recalculateIsBSpline();
+ for (std::list<SubpathPtr>::iterator spi = _subpaths.begin(); spi != _subpaths.end(); ) {
+ SubpathPtr subpath = *spi;
+ if (subpath->empty()) {
+ _subpaths.erase(spi++);
+ continue;
+ }
+ NodeList::iterator prev = subpath->begin();
+ builder.moveTo(prev->position());
+ for (NodeList::iterator i = ++subpath->begin(); i != subpath->end(); ++i) {
+ build_segment(builder, prev.ptr(), i.ptr());
+ prev = i;
+ }
+ if (subpath->closed()) {
+ // Here we link the last and first node if the path is closed.
+ // If the last segment is Bezier, we add it.
+ if (!prev->front()->isDegenerate() || !subpath->begin()->back()->isDegenerate()) {
+ build_segment(builder, prev.ptr(), subpath->begin().ptr());
+ }
+ // if that segment is linear, we just call closePath().
+ builder.closePath();
+ }
+ ++spi;
+ }
+ builder.flush();
+ Geom::PathVector pathv = builder.peek() * (_edit_transform * _i2d_transform).inverse();
+ for (Geom::PathVector::iterator i = pathv.begin(); i != pathv.end(); ) {
+ // NOTE: this utilizes the fact that Geom::PathVector is an std::vector.
+ // When we erase an element, the next one slides into position,
+ // so we do not increment the iterator even though it is theoretically invalidated.
+ if (i->empty()) {
+ i = pathv.erase(i);
+ } else {
+ ++i;
+ }
+ }
+ if (pathv.empty()) {
+ return;
+ }
+
+ if (_spcurve->get_pathvector() == pathv) {
+ return;
+ }
+ _spcurve->set_pathvector(pathv);
+ if (alert_LPE) {
+ /// \todo note that _path can be an Inkscape::LivePathEffect::Effect* too, kind of confusing, rework member naming?
+ SPPath *path = dynamic_cast<SPPath *>(_path);
+ if (path && path->hasPathEffect()) {
+ Inkscape::LivePathEffect::Effect *this_effect =
+ path->getFirstPathEffectOfType(Inkscape::LivePathEffect::POWERSTROKE);
+ if(this_effect){
+ LivePathEffect::LPEPowerStroke *lpe_pwr = dynamic_cast<LivePathEffect::LPEPowerStroke*>(this_effect->getLPEObj()->get_lpe());
+ if (lpe_pwr) {
+ lpe_pwr->adjustForNewPath(pathv);
+ }
+ }
+ }
+ }
+ if (_live_outline) {
+ _updateOutline();
+ }
+ if (_live_objects) {
+ _setGeometry();
+ }
+}
+
+/** Build one segment of the geometric representation.
+ * @relates PathManipulator */
+void build_segment(Geom::PathBuilder &builder, Node *prev_node, Node *cur_node)
+{
+ if (cur_node->back()->isDegenerate() && prev_node->front()->isDegenerate())
+ {
+ // NOTE: It seems like the renderer cannot correctly handle vline / hline segments,
+ // and trying to display a path using them results in funny artifacts.
+ builder.lineTo(cur_node->position());
+ } else {
+ // this is a bezier segment
+ builder.curveTo(
+ prev_node->front()->position(),
+ cur_node->back()->position(),
+ cur_node->position());
+ }
+}
+
+/** Construct a node type string to store in the sodipodi:nodetypes attribute. */
+std::string PathManipulator::_createTypeString()
+{
+ // precondition: no single-node subpaths
+ std::stringstream tstr;
+ for (auto & _subpath : _subpaths) {
+ for (auto & j : *_subpath) {
+ tstr << j.type();
+ }
+ // nodestring format peculiarity: first node is counted twice for closed paths
+ if (_subpath->closed()) tstr << _subpath->begin()->type();
+ }
+ return tstr.str();
+}
+
+/** Update the path outline. */
+void PathManipulator::_updateOutline()
+{
+ if (!_show_outline) {
+ _outline->hide();
+ return;
+ }
+
+ Geom::PathVector pv = _spcurve->get_pathvector();
+ pv *= (_edit_transform * _i2d_transform);
+ // This SPCurve thing has to be killed with extreme prejudice
+ auto _hc = std::make_unique<SPCurve>();
+ if (_show_path_direction) {
+ // To show the direction, we append additional subpaths which consist of a single
+ // linear segment that starts at the time value of 0.5 and extends for 10 pixels
+ // at an angle 150 degrees from the unit tangent. This creates the appearance
+ // of little 'harpoons' that show the direction of the subpaths.
+ auto rot_scale_w2d = Geom::Rotate(210.0 / 180.0 * M_PI) * Geom::Scale(10.0) * _desktop->w2d();
+ Geom::PathVector arrows;
+ for (auto & path : pv) {
+ for (Geom::Path::iterator j = path.begin(); j != path.end_default(); ++j) {
+ Geom::Point at = j->pointAt(0.5);
+ Geom::Point ut = j->unitTangentAt(0.5);
+ Geom::Point arrow_end = at + (Geom::unit_vector(_desktop->d2w(ut)) * rot_scale_w2d);
+
+ Geom::Path arrow(at);
+ arrow.appendNew<Geom::LineSegment>(arrow_end);
+ arrows.push_back(arrow);
+ }
+ }
+ pv.insert(pv.end(), arrows.begin(), arrows.end());
+ }
+ _hc->set_pathvector(pv);
+ _outline->set_bpath(_hc.get());
+ _outline->show();
+}
+
+/** Retrieve the geometry of the edited object from the object tree */
+void PathManipulator::_getGeometry()
+{
+ using namespace Inkscape::LivePathEffect;
+ LivePathEffectObject *lpeobj = dynamic_cast<LivePathEffectObject *>(_path);
+ SPPath *path = dynamic_cast<SPPath *>(_path);
+ if (lpeobj) {
+ Effect *lpe = lpeobj->get_lpe();
+ if (lpe) {
+ PathParam *pathparam = dynamic_cast<PathParam *>(lpe->getParameter(_lpe_key.data()));
+ _spcurve.reset(new SPCurve(pathparam->get_pathvector()));
+ }
+ } else if (path) {
+ _spcurve = SPCurve::copy(path->curveForEdit());
+ // never allow NULL to sneak in here!
+ if (_spcurve == nullptr) {
+ _spcurve.reset(new SPCurve());
+ }
+ }
+}
+
+/** Set the geometry of the edited object in the object tree, but do not commit to XML */
+void PathManipulator::_setGeometry()
+{
+ using namespace Inkscape::LivePathEffect;
+ LivePathEffectObject *lpeobj = dynamic_cast<LivePathEffectObject *>(_path);
+ SPPath *path = dynamic_cast<SPPath *>(_path);
+ if (lpeobj) {
+ // copied from nodepath.cpp
+ // NOTE: if we are editing an LPE param, _path is not actually an SPPath, it is
+ // a LivePathEffectObject. (mad laughter)
+ Effect *lpe = lpeobj->get_lpe();
+ if (lpe) {
+ PathParam *pathparam = dynamic_cast<PathParam *>(lpe->getParameter(_lpe_key.data()));
+ if (pathparam->get_pathvector() == _spcurve->get_pathvector()) {
+ return; //False we dont update LPE
+ }
+ pathparam->set_new_value(_spcurve->get_pathvector(), false);
+ lpeobj->requestModified(SP_OBJECT_MODIFIED_FLAG);
+ }
+ } else if (path) {
+ // return true to leave the decision on empty to the caller.
+ // Maybe the path become empty and we want to update to empty
+ if (empty()) return;
+ if (path->curveBeforeLPE()) {
+ path->setCurveBeforeLPE(_spcurve.get());
+ if (!path->hasPathEffectOfTypeRecursive(Inkscape::LivePathEffect::SLICE)) {
+ sp_lpe_item_update_patheffect(path, true, false);
+ } else {
+ path->setCurve(_spcurve.get());
+ }
+ } else {
+ path->setCurve(_spcurve.get());
+ }
+ }
+}
+
+/** Figure out in what attribute to store the nodetype string. */
+Glib::ustring PathManipulator::_nodetypesKey()
+{
+ LivePathEffectObject *lpeobj = dynamic_cast<LivePathEffectObject *>(_path);
+ if (!lpeobj) {
+ return "sodipodi:nodetypes";
+ } else {
+ return _lpe_key + "-nodetypes";
+ }
+}
+
+/** Return the XML node we are editing.
+ * This method is wrong but necessary at the moment. */
+Inkscape::XML::Node *PathManipulator::_getXMLNode()
+{
+ //XML Tree being used here directly while it shouldn't be.
+ LivePathEffectObject *lpeobj = dynamic_cast<LivePathEffectObject *>(_path);
+ if (!lpeobj)
+ return _path->getRepr();
+ //XML Tree being used here directly while it shouldn't be.
+ return lpeobj->getRepr();
+}
+
+bool PathManipulator::_nodeClicked(Node *n, GdkEventButton *event)
+{
+ if (event->button != 1) return false;
+ if (held_alt(*event) && held_control(*event)) {
+ // Ctrl+Alt+click: delete nodes
+ hideDragPoint();
+ NodeList::iterator iter = NodeList::get_iterator(n);
+ NodeList &nl = iter->nodeList();
+
+ if (nl.size() <= 1 || (nl.size() <= 2 && !nl.closed())) {
+ // Removing last node of closed path - delete it
+ nl.kill();
+ } else {
+ // In other cases, delete the node under cursor
+ _deleteStretch(iter, iter.next(), true);
+ }
+
+ if (!empty()) {
+ update(true);
+ }
+
+ // We need to call MPM's method because it could have been our last node
+ _multi_path_manipulator._doneWithCleanup(_("Delete node"));
+
+ return true;
+ } else if (held_control(*event)) {
+ // Ctrl+click: cycle between node types
+ if (!n->isEndNode()) {
+ n->setType(static_cast<NodeType>((n->type() + 1) % NODE_LAST_REAL_TYPE));
+ update();
+ _commit(_("Cycle node type"));
+ }
+ return true;
+ }
+ return false;
+}
+
+void PathManipulator::_handleGrabbed()
+{
+ _selection.hideTransformHandles();
+}
+
+void PathManipulator::_handleUngrabbed()
+{
+ _selection.restoreTransformHandles();
+ _commit(_("Drag handle"));
+}
+
+bool PathManipulator::_handleClicked(Handle *h, GdkEventButton *event)
+{
+ // retracting by Ctrl+click
+ if (event->button == 1 && held_control(*event)) {
+ h->move(h->parent()->position());
+ update();
+ _commit(_("Retract handle"));
+ return true;
+ }
+ return false;
+}
+
+void PathManipulator::_selectionChangedM(std::vector<SelectableControlPoint *> pvec, bool selected) {
+ for (auto & n : pvec) {
+ _selectionChanged(n, selected);
+ }
+}
+
+void PathManipulator::_selectionChanged(SelectableControlPoint *p, bool selected)
+{
+ // don't do anything if we do not show handles
+ if (!_show_handles) return;
+
+ // only do something if a node changed selection state
+ Node *node = dynamic_cast<Node*>(p);
+ if (!node) return;
+
+ // update handle display
+ NodeList::iterator iters[5];
+ iters[2] = NodeList::get_iterator(node);
+ iters[1] = iters[2].prev();
+ iters[3] = iters[2].next();
+ if (selected) {
+ // selection - show handles on this node and adjacent ones
+ node->showHandles(true);
+ if (iters[1]) iters[1]->showHandles(true);
+ if (iters[3]) iters[3]->showHandles(true);
+ } else {
+ /* Deselection is more complex.
+ * The change might affect 3 nodes - this one and two adjacent.
+ * If the node and both its neighbors are deselected, hide handles.
+ * Otherwise, leave as is. */
+ if (iters[1]) iters[0] = iters[1].prev();
+ if (iters[3]) iters[4] = iters[3].next();
+ bool nodesel[5];
+ for (int i = 0; i < 5; ++i) {
+ nodesel[i] = iters[i] && iters[i]->selected();
+ }
+ for (int i = 1; i < 4; ++i) {
+ if (iters[i] && !nodesel[i-1] && !nodesel[i] && !nodesel[i+1]) {
+ iters[i]->showHandles(false);
+ }
+ }
+ }
+}
+
+/** Removes all nodes belonging to this manipulator from the control point selection */
+void PathManipulator::_removeNodesFromSelection()
+{
+ // remove this manipulator's nodes from selection
+ for (auto & _subpath : _subpaths) {
+ for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) {
+ _selection.erase(j.get_pointer());
+ }
+ }
+}
+
+/** Update the XML representation and put the specified annotation on the undo stack */
+void PathManipulator::_commit(Glib::ustring const &annotation)
+{
+ writeXML();
+ if (_desktop) {
+ DocumentUndo::done(_desktop->getDocument(), 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 = _edit_transform * _i2d_transform;
+ 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
+ * (_edit_transform * _i2d_transform).descrim() // scale to desktop coords
+ * _desktop->current_zoom(); // == _d2w.descrim() - scale to window coords
+ }
+ return ret;
+}
+
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/tool/path-manipulator.h b/src/ui/tool/path-manipulator.h
new file mode 100644
index 0000000..ca861c6
--- /dev/null
+++ b/src/ui/tool/path-manipulator.h
@@ -0,0 +1,186 @@
+// 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"
+
+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;
+};
+
+/**
+ * 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(bool keep_shape = true);
+ void deleteSegments();
+ void reverseSubpaths(bool selected_only);
+ void setSegmentType(SegmentType);
+
+ void scaleHandle(Node *n, int which, int dir, bool pixel);
+ void rotateHandle(Node *n, int which, int dir, bool pixel);
+
+ void showOutline(bool show);
+ void showHandles(bool show);
+ void showPathDirection(bool show);
+ void setLiveOutline(bool set);
+ void setLiveObjects(bool set);
+ void updateHandles();
+ void 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, bool keep_shape);
+ std::string _createTypeString();
+ void _updateOutline();
+ //void _setOutline(Geom::PathVector const &);
+ void _getGeometry();
+ void _setGeometry();
+ Glib::ustring _nodetypesKey();
+ Inkscape::XML::Node *_getXMLNode();
+
+ void _selectionChangedM(std::vector<SelectableControlPoint *> pvec, bool selected);
+ void _selectionChanged(SelectableControlPoint * p, bool selected);
+ bool _nodeClicked(Node *, GdkEventButton *);
+ void _handleGrabbed();
+ bool _handleClicked(Handle *, GdkEventButton *);
+ void _handleUngrabbed();
+
+ void _externalChange(unsigned type);
+ void _removeNodesFromSelection();
+ void _commit(Glib::ustring const &annotation);
+ void _commit(Glib::ustring const &annotation, gchar const *key);
+ Geom::Coord _updateDragPoint(Geom::Point const &);
+ void _updateOutlineOnZoomChange();
+ double _getStrokeTolerance();
+ Handle *_chooseHandle(Node *n, int which);
+
+ SubpathList _subpaths;
+ MultiPathManipulator &_multi_path_manipulator;
+ SPObject *_path; ///< can be an SPPath or an Inkscape::LivePathEffect::Effect !!!
+ std::unique_ptr<SPCurve> _spcurve; // in item coordinates
+ Inkscape::CanvasItemBpath *_outline = nullptr;
+ 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/selector.cpp b/src/ui/tool/selector.cpp
new file mode 100644
index 0000000..5b2fd45
--- /dev/null
+++ b/src/ui/tool/selector.cpp
@@ -0,0 +1,153 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Selector component (click and rubberband)
+ */
+/* Authors:
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gdk/gdkkeysyms.h>
+
+#include "control-point.h"
+#include "desktop.h"
+
+#include "display/control/canvas-item-rect.h"
+
+#include "ui/tools/tool-base.h"
+#include "ui/tool/event-utils.h"
+#include "ui/tool/selector.h"
+
+namespace Inkscape {
+namespace UI {
+
+/** A hidden control point used for rubberbanding and selection.
+ * It uses a clever hack: the canvas item is hidden and only receives events when they
+ * are passed to it using Selector's event() function. When left mouse button
+ * is pressed, it grabs events and handles drags and clicks in the usual way. */
+class SelectorPoint : public ControlPoint {
+public:
+ SelectorPoint(SPDesktop *d, Inkscape::CanvasItemGroup *group, Selector *s) :
+ ControlPoint(d, Geom::Point(0,0), SP_ANCHOR_CENTER,
+ Inkscape::CANVAS_ITEM_CTRL_TYPE_INVISIPOINT,
+ invisible_cset, group),
+ _selector(s),
+ _cancel(false)
+ {
+ _canvas_item_ctrl->set_name("CanvasItemCtrl:SelectorPoint");
+ setVisible(false);
+ _rubber = new Inkscape::CanvasItemRect(_desktop->getCanvasControls());
+ _rubber->set_name("CanavasItemRect:SelectorPoint:Rubberband");
+ _rubber->set_stroke(0x8080ffff);
+ _rubber->set_inverted(true);
+ _rubber->hide();
+ }
+
+ ~SelectorPoint() override {
+ delete _rubber;
+ }
+
+ SPDesktop *desktop() { return _desktop; }
+
+ bool event(Inkscape::UI::Tools::ToolBase *ec, GdkEvent *e) {
+ return _eventHandler(ec, e);
+ }
+
+protected:
+ bool _eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) override {
+ if (event->type == GDK_KEY_PRESS &&
+ shortcut_key(event->key) == GDK_KEY_Escape &&
+ _rubber->is_visible() )
+ {
+ _cancel = true;
+ _rubber->hide();
+ return true;
+ }
+ return ControlPoint::_eventHandler(event_context, event);
+ }
+
+private:
+ bool grabbed(GdkEventMotion *) override {
+ _cancel = false;
+ _start = position();
+ _rubber->show();
+ return false;
+ }
+
+ void dragged(Geom::Point &new_pos, GdkEventMotion *) override {
+ if (_cancel) return;
+ Geom::Rect sel(_start, new_pos);
+ _rubber->set_rect(sel);
+ }
+
+ void ungrabbed(GdkEventButton *event) override {
+ if (_cancel) return;
+ _rubber->hide();
+ Geom::Rect sel(_start, position());
+ _selector->signal_area.emit(sel, event);
+ }
+
+ bool clicked(GdkEventButton *event) override {
+ if (event->button != 1) return false;
+ _selector->signal_point.emit(position(), event);
+ return true;
+ }
+
+ Inkscape::CanvasItemRect *_rubber;
+ Selector *_selector;
+ Geom::Point _start;
+ bool _cancel;
+};
+
+
+Selector::Selector(SPDesktop *desktop)
+ : Manipulator(desktop)
+ , _dragger(new SelectorPoint(desktop, desktop->getCanvasControls(), this))
+{
+ _dragger->setVisible(false);
+}
+
+Selector::~Selector()
+{
+ delete _dragger;
+}
+
+bool Selector::event(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event)
+{
+ // The hidden control point will capture all events after it obtains the grab,
+ // but it relies on this function to initiate it. If we pass only first button
+ // press events here, it won't interfere with any other event handling.
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ // Do not pass button presses other than left button to the control point.
+ // This way middle click and right click can be handled in ToolBase.
+ if (event->button.button == 1 && !event_context->is_space_panning()) {
+ _dragger->setPosition(_desktop->w2d(event_point(event->motion)));
+ return _dragger->event(event_context, event);
+ }
+ break;
+ default: break;
+ }
+ return false;
+}
+
+bool Selector::doubleClicked() {
+ return _dragger->doubleClicked();
+}
+
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/tool/selector.h b/src/ui/tool/selector.h
new file mode 100644
index 0000000..7ad0c3b
--- /dev/null
+++ b/src/ui/tool/selector.h
@@ -0,0 +1,59 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Selector component (click and rubberband)
+ */
+/* Authors:
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_TOOL_SELECTOR_H
+#define SEEN_UI_TOOL_SELECTOR_H
+
+#include <memory>
+#include <gdk/gdk.h>
+#include <2geom/rect.h>
+#include "ui/tool/manipulator.h"
+
+class SPDesktop;
+
+namespace Inkscape {
+
+class CanvasItemRect;
+
+namespace UI {
+
+class SelectorPoint;
+
+class Selector : public Manipulator {
+public:
+ Selector(SPDesktop *d);
+ ~Selector() override;
+ bool event(Inkscape::UI::Tools::ToolBase *, GdkEvent *) override;
+ virtual bool doubleClicked();
+
+ sigc::signal<void, Geom::Rect const &, GdkEventButton*> signal_area;
+ sigc::signal<void, Geom::Point const &, GdkEventButton*> signal_point;
+private:
+ SelectorPoint *_dragger;
+ Geom::Point _start;
+ friend class SelectorPoint;
+};
+
+} // namespace UI
+} // namespace Inkscape
+
+#endif
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/tool/shape-record.h b/src/ui/tool/shape-record.h
new file mode 100644
index 0000000..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..4b6877c
--- /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..b3493e8
--- /dev/null
+++ b/src/ui/toolbar/arc-toolbar.cpp
@@ -0,0 +1,559 @@
+// 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"
+
+#include "xml/node-event-vector.h"
+
+using Inkscape::UI::Widget::UnitTracker;
+using Inkscape::DocumentUndo;
+using Inkscape::Util::Quantity;
+using Inkscape::Util::unit_table;
+
+
+static Inkscape::XML::NodeEventVector arc_tb_repr_events = {
+ nullptr, /* child_added */
+ nullptr, /* child_removed */
+ Inkscape::UI::Toolbar::ArcToolbar::event_attr_changed,
+ nullptr, /* content_changed */
+ nullptr /* order_changed */
+};
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+ArcToolbar::ArcToolbar(SPDesktop *desktop) :
+ Toolbar(desktop),
+ _tracker(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR)),
+ _freeze(false),
+ _repr(nullptr)
+{
+ 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->removeListenerByData(this);
+ GC::release(_repr);
+ _repr = nullptr;
+ }
+}
+
+GtkWidget *
+ArcToolbar::create(SPDesktop *desktop)
+{
+ auto toolbar = new ArcToolbar(desktop);
+ return GTK_WIDGET(toolbar->gobj());
+}
+
+void
+ArcToolbar::value_changed(Glib::RefPtr<Gtk::Adjustment>& adj,
+ gchar const *value_name)
+{
+ // Per SVG spec "a [radius] value of zero disables rendering of the element".
+ // However our implementation does not allow a setting of zero in the UI (not even in the XML editor)
+ // and ugly things happen if it's forced here, so better leave the properties untouched.
+ if (!adj->get_value()) {
+ return;
+ }
+
+ Unit const *unit = _tracker->getActiveUnit();
+ g_return_if_fail(unit != nullptr);
+
+ SPDocument* document = _desktop->getDocument();
+
+ if (DocumentUndo::getUndoSensitive(document)) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble(Glib::ustring("/tools/shapes/arc/") + value_name,
+ Quantity::convert(adj->get_value(), unit, "px"));
+ }
+
+ // quit if run by the attr_changed listener
+ if (_freeze || _tracker->isUpdating()) {
+ return;
+ }
+
+ // in turn, prevent listener from responding
+ _freeze = true;
+
+ bool modmade = false;
+ Inkscape::Selection *selection = _desktop->getSelection();
+ auto itemlist= selection->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ SPItem *item = *i;
+ if (SP_IS_GENERICELLIPSE(item)) {
+
+ SPGenericEllipse *ge = SP_GENERICELLIPSE(item);
+
+ if (!strcmp(value_name, "rx")) {
+ ge->setVisibleRx(Quantity::convert(adj->get_value(), unit, "px"));
+ } else {
+ ge->setVisibleRy(Quantity::convert(adj->get_value(), unit, "px"));
+ }
+
+ ge->normalize();
+ 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 (SP_IS_GENERICELLIPSE(item)) {
+
+ SPGenericEllipse *ge = SP_GENERICELLIPSE(item);
+
+ if (!strcmp(value_name, "start")) {
+ ge->start = (adj->get_value() * M_PI)/ 180;
+ } else {
+ ge->end = (adj->get_value() * M_PI)/ 180;
+ }
+
+ ge->normalize();
+ 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 (SP_IS_GENERICELLIPSE(item)) {
+ Inkscape::XML::Node *repr = item->getRepr();
+ repr->setAttribute("sodipodi:open", (open?"true":nullptr) );
+ repr->setAttribute("sodipodi:arc-type", arc_type);
+ item->updateRepr();
+ modmade = true;
+ }
+ }
+
+ if (modmade) {
+ DocumentUndo::done(_desktop->getDocument(), _("Arc: Changed 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->removeListenerByData(this);
+ Inkscape::GC::release(_repr);
+ _repr = nullptr;
+ }
+ }
+ }
+}
+
+void
+ArcToolbar::selection_changed(Inkscape::Selection *selection)
+{
+ int n_selected = 0;
+ Inkscape::XML::Node *repr = nullptr;
+
+ if ( _repr ) {
+ _item = nullptr;
+ _repr->removeListenerByData(this);
+ GC::release(_repr);
+ _repr = nullptr;
+ }
+
+ SPItem *item = nullptr;
+
+ for(auto i : selection->items()){
+ if (SP_IS_GENERICELLIPSE(i)) {
+ n_selected++;
+ item = i;
+ repr = item->getRepr();
+ }
+ }
+
+ _single = false;
+ if (n_selected == 0) {
+ _mode_item->set_markup(_("<b>New:</b>"));
+ } else if (n_selected == 1) {
+ _single = true;
+ _mode_item->set_markup(_("<b>Change:</b>"));
+ _rx_item->set_sensitive(true);
+ _ry_item->set_sensitive(true);
+
+ if (repr) {
+ _repr = repr;
+ _item = item;
+ Inkscape::GC::anchor(_repr);
+ _repr->addListener(&arc_tb_repr_events, this);
+ _repr->synthesizeEvents(&arc_tb_repr_events, this);
+ }
+ } else {
+ // FIXME: implement averaging of all parameters for multiple selected
+ //gtk_label_set_markup(GTK_LABEL(l), _("<b>Average:</b>"));
+ _mode_item->set_markup(_("<b>Change:</b>"));
+ sensitivize( 1, 0 );
+ }
+}
+
+void
+ArcToolbar::event_attr_changed(Inkscape::XML::Node *repr, gchar const * /*name*/,
+ gchar const * /*old_value*/, gchar const * /*new_value*/,
+ bool /*is_interactive*/, gpointer data)
+{
+ auto toolbar = reinterpret_cast<ArcToolbar *>(data);
+
+ // quit if run by the _changed callbacks
+ if (toolbar->_freeze) {
+ return;
+ }
+
+ // in turn, prevent callbacks from responding
+ toolbar->_freeze = true;
+
+ if (toolbar->_item && SP_IS_GENERICELLIPSE(toolbar->_item)) {
+ SPGenericEllipse *ge = SP_GENERICELLIPSE(toolbar->_item);
+
+ Unit const *unit = toolbar->_tracker->getActiveUnit();
+ g_return_if_fail(unit != nullptr);
+
+ gdouble rx = ge->getVisibleRx();
+ gdouble ry = ge->getVisibleRy();
+ toolbar->_rx_adj->set_value(Quantity::convert(rx, "px", unit));
+ toolbar->_ry_adj->set_value(Quantity::convert(ry, "px", unit));
+ }
+
+ gdouble start = repr->getAttributeDouble("sodipodi:start", 0.0);;
+ gdouble end = repr->getAttributeDouble("sodipodi:end", 0.0);
+
+ toolbar->_start_adj->set_value(mod360((start * 180)/M_PI));
+ toolbar->_end_adj->set_value(mod360((end * 180)/M_PI));
+
+ toolbar->sensitivize(toolbar->_start_adj->get_value(), toolbar->_end_adj->get_value());
+
+ char const *arctypestr = nullptr;
+ arctypestr = repr->attribute("sodipodi:arc-type");
+ if (!arctypestr) { // For old files.
+ char const *openstr = nullptr;
+ openstr = repr->attribute("sodipodi:open");
+ arctypestr = (openstr ? "arc" : "slice");
+ }
+
+ if (!strcmp(arctypestr,"slice")) {
+ toolbar->_type_buttons[0]->set_active();
+ } else if (!strcmp(arctypestr,"arc")) {
+ toolbar->_type_buttons[1]->set_active();
+ } else {
+ toolbar->_type_buttons[2]->set_active();
+ }
+
+ toolbar->_freeze = false;
+}
+
+}
+}
+}
+
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/toolbar/arc-toolbar.h b/src/ui/toolbar/arc-toolbar.h
new file mode 100644
index 0000000..b0b0450
--- /dev/null
+++ b/src/ui/toolbar/arc-toolbar.h
@@ -0,0 +1,116 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_ARC_TOOLBAR_H
+#define SEEN_ARC_TOOLBAR_H
+
+/**
+ * @file
+ * 3d box aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbar.h"
+
+#include <gtkmm/adjustment.h>
+
+class SPDesktop;
+class SPItem;
+
+namespace Gtk {
+class RadioToolButton;
+class ToolButton;
+}
+
+namespace Inkscape {
+class Selection;
+
+namespace XML {
+class Node;
+}
+
+namespace UI {
+namespace Tools {
+class ToolBase;
+}
+
+namespace Widget {
+class LabelToolItem;
+class SpinButtonToolItem;
+class UnitTracker;
+}
+
+namespace Toolbar {
+class ArcToolbar : public Toolbar {
+private:
+ UI::Widget::UnitTracker *_tracker;
+
+ UI::Widget::SpinButtonToolItem *_rx_item;
+ UI::Widget::SpinButtonToolItem *_ry_item;
+
+ UI::Widget::LabelToolItem *_mode_item;
+
+ std::vector<Gtk::RadioToolButton *> _type_buttons;
+ Gtk::ToolButton *_make_whole;
+
+ Glib::RefPtr<Gtk::Adjustment> _rx_adj;
+ Glib::RefPtr<Gtk::Adjustment> _ry_adj;
+ Glib::RefPtr<Gtk::Adjustment> _start_adj;
+ Glib::RefPtr<Gtk::Adjustment> _end_adj;
+
+ bool _freeze;
+ bool _single;
+
+ XML::Node *_repr;
+ SPItem *_item;
+
+ void value_changed(Glib::RefPtr<Gtk::Adjustment>& adj,
+ gchar const *value_name);
+ void startend_value_changed(Glib::RefPtr<Gtk::Adjustment>& adj,
+ gchar const *value_name,
+ Glib::RefPtr<Gtk::Adjustment>& other_adj);
+ void type_changed( int type );
+ void defaults();
+ void sensitivize( double v1, double v2 );
+ void check_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec);
+ void selection_changed(Inkscape::Selection *selection);
+
+ sigc::connection _changed;
+
+protected:
+ ArcToolbar(SPDesktop *desktop);
+ ~ArcToolbar() override;
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+ static void event_attr_changed(Inkscape::XML::Node *repr,
+ gchar const *name,
+ gchar const *old_value,
+ gchar const *new_value,
+ bool is_interactive,
+ gpointer data);
+};
+
+}
+}
+}
+
+#endif /* !SEEN_ARC_TOOLBAR_H */
diff --git a/src/ui/toolbar/box3d-toolbar.cpp b/src/ui/toolbar/box3d-toolbar.cpp
new file mode 100644
index 0000000..e594240
--- /dev/null
+++ b/src/ui/toolbar/box3d-toolbar.cpp
@@ -0,0 +1,428 @@
+// 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"
+
+#include "xml/node-event-vector.h"
+
+using Inkscape::DocumentUndo;
+
+static Inkscape::XML::NodeEventVector box3d_persp_tb_repr_events =
+{
+ nullptr, /* child_added */
+ nullptr, /* child_removed */
+ Inkscape::UI::Toolbar::Box3DToolbar::event_attr_changed,
+ nullptr, /* content_changed */
+ nullptr /* order_changed */
+};
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+Box3DToolbar::Box3DToolbar(SPDesktop *desktop)
+ : Toolbar(desktop),
+ _repr(nullptr),
+ _freeze(false)
+{
+ auto prefs = Inkscape::Preferences::get();
+ auto document = desktop->getDocument();
+ auto persp_impl = document->getCurrentPersp3DImpl();
+
+ /* Angle X */
+ {
+ std::vector<double> values = {-90, -60, -30, 0, 30, 60, 90};
+ auto angle_x_val = prefs->getDouble("/tools/shapes/3dbox/box3d_angle_x", 30);
+ _angle_x_adj = Gtk::Adjustment::create(angle_x_val, -360.0, 360.0, 1.0, 10.0);
+ _angle_x_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("box3d-angle-x", _("Angle X:"), _angle_x_adj));
+ // TRANSLATORS: PL is short for 'perspective line'
+ _angle_x_item->set_tooltip_text(_("Angle of PLs in X direction"));
+ _angle_x_item->set_custom_numeric_menu_data(values);
+ _angle_x_item->set_focus_widget(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 (SP_IS_BOX3D_CONTEXT(ec)) {
+ _changed = desktop->getSelection()->connectChanged(sigc::mem_fun(*this, &Box3DToolbar::selection_changed));
+ selection_changed(desktop->getSelection());
+ } else {
+ if (_changed)
+ _changed.disconnect();
+
+ if (_repr) { // remove old listener
+ _repr->removeListenerByData(this);
+ Inkscape::GC::release(_repr);
+ _repr = nullptr;
+ }
+ }
+}
+
+Box3DToolbar::~Box3DToolbar()
+{
+ if (_repr) { // remove old listener
+ _repr->removeListenerByData(this);
+ Inkscape::GC::release(_repr);
+ _repr = nullptr;
+ }
+}
+
+/**
+ * \param selection Should not be NULL.
+ */
+// FIXME: This should rather be put into persp3d-reference.cpp or something similar so that it reacts upon each
+// Change of the perspective, and not of the current selection (but how to refer to the toolbar then?)
+void
+Box3DToolbar::selection_changed(Inkscape::Selection *selection)
+{
+ // Here the following should be done: If all selected boxes have finite VPs in a certain direction,
+ // disable the angle entry fields for this direction (otherwise entering a value in them should only
+ // update the perspectives with infinite VPs and leave the other ones untouched).
+
+ Inkscape::XML::Node *persp_repr = nullptr;
+
+ if (_repr) { // remove old listener
+ _repr->removeListenerByData(this);
+ Inkscape::GC::release(_repr);
+ _repr = nullptr;
+ }
+
+ SPItem *item = selection->singleItem();
+ SPBox3D *box = dynamic_cast<SPBox3D *>(item);
+ if (box) {
+ // FIXME: Also deal with multiple selected boxes
+ Persp3D *persp = 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->addListener(&box3d_persp_tb_repr_events, this);
+ _repr->synthesizeEvents(&box3d_persp_tb_repr_events, 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_print ("No perspective given to box3d_resync_toolbar().\n");
+ return;
+ }
+
+ Persp3D *persp = Persp3D::get_from_repr(persp_repr);
+ if (!persp) {
+ // Hmm, is it an error if this happens?
+ return;
+ }
+ set_button_and_adjustment(persp, Proj::X,
+ _angle_x_adj,
+ _angle_x_item,
+ _vp_x_state_item);
+ set_button_and_adjustment(persp, Proj::Y,
+ _angle_y_adj,
+ _angle_y_item,
+ _vp_y_state_item);
+ set_button_and_adjustment(persp, Proj::Z,
+ _angle_z_adj,
+ _angle_z_item,
+ _vp_z_state_item);
+}
+
+void
+Box3DToolbar::set_button_and_adjustment(Persp3D *persp,
+ Proj::Axis axis,
+ Glib::RefPtr<Gtk::Adjustment>& adj,
+ UI::Widget::SpinButtonToolItem *spin_btn,
+ Gtk::ToggleToolButton *toggle_btn)
+{
+ // TODO: Take all selected perspectives into account but don't touch the state button if not all of them
+ // have the same state (otherwise a call to box3d_vp_z_state_changed() is triggered and the states
+ // are reset).
+ bool is_infinite = !Persp3D::VP_is_finite(persp->perspective_impl, axis);
+
+ if (is_infinite) {
+ toggle_btn->set_active(true);
+ spin_btn->set_sensitive(true);
+
+ double angle = 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::event_attr_changed(Inkscape::XML::Node *repr,
+ gchar const * /*name*/,
+ gchar const * /*old_value*/,
+ gchar const * /*new_value*/,
+ bool /*is_interactive*/,
+ gpointer data)
+{
+ auto toolbar = reinterpret_cast<Box3DToolbar*>(data);
+
+ // quit if run by the attr_changed or selection changed listener
+ if (toolbar->_freeze) {
+ return;
+ }
+
+ // set freeze so that it can be caught in box3d_angle_z_value_changed() (to avoid calling
+ // SPDocumentUndo::maybeDone() when the document is undo insensitive)
+ toolbar->_freeze = true;
+
+ // TODO: Only update the appropriate part of the toolbar
+// if (!strcmp(name, "inkscape:vp_z")) {
+ toolbar->resync_toolbar(repr);
+// }
+
+ Persp3D *persp = Persp3D::get_from_repr(repr);
+ if (persp) {
+ persp->update_box_reprs();
+ }
+
+ toolbar->_freeze = false;
+}
+
+/**
+ * \brief normalize angle so that it lies in the interval [0,360]
+ *
+ * TODO: Isn't there something in 2Geom or cmath that does this?
+ */
+double
+Box3DToolbar::normalize_angle(double a) {
+ double angle = a + ((int) (a/360.0))*360;
+ if (angle < 0) {
+ angle += 360.0;
+ }
+ return angle;
+}
+
+}
+}
+}
+
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/toolbar/box3d-toolbar.h b/src/ui/toolbar/box3d-toolbar.h
new file mode 100644
index 0000000..8ba7872
--- /dev/null
+++ b/src/ui/toolbar/box3d-toolbar.h
@@ -0,0 +1,108 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_BOX3D_TOOLBAR_H
+#define SEEN_BOX3D_TOOLBAR_H
+
+/**
+ * @file
+ * 3d box aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbar.h"
+
+#include "axis-manip.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:
+ UI::Widget::SpinButtonToolItem *_angle_x_item;
+ UI::Widget::SpinButtonToolItem *_angle_y_item;
+ UI::Widget::SpinButtonToolItem *_angle_z_item;
+
+ Glib::RefPtr<Gtk::Adjustment> _angle_x_adj;
+ Glib::RefPtr<Gtk::Adjustment> _angle_y_adj;
+ Glib::RefPtr<Gtk::Adjustment> _angle_z_adj;
+
+ Gtk::ToggleToolButton *_vp_x_state_item;
+ Gtk::ToggleToolButton *_vp_y_state_item;
+ Gtk::ToggleToolButton *_vp_z_state_item;
+
+ XML::Node *_repr;
+ bool _freeze;
+
+ void angle_value_changed(Glib::RefPtr<Gtk::Adjustment> &adj,
+ Proj::Axis axis);
+ void vp_state_changed(Proj::Axis axis);
+ void check_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec);
+ void selection_changed(Inkscape::Selection *selection);
+ void resync_toolbar(Inkscape::XML::Node *persp_repr);
+ void set_button_and_adjustment(Persp3D *persp,
+ Proj::Axis axis,
+ Glib::RefPtr<Gtk::Adjustment>& adj,
+ UI::Widget::SpinButtonToolItem *spin_btn,
+ Gtk::ToggleToolButton *toggle_btn);
+ double normalize_angle(double a);
+
+ sigc::connection _changed;
+
+protected:
+ Box3DToolbar(SPDesktop *desktop);
+ ~Box3DToolbar() override;
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+ static void event_attr_changed(Inkscape::XML::Node *repr,
+ gchar const *name,
+ gchar const *old_value,
+ gchar const *new_value,
+ bool is_interactive,
+ gpointer data);
+
+};
+}
+}
+}
+#endif /* !SEEN_BOX3D_TOOLBAR_H */
diff --git a/src/ui/toolbar/calligraphy-toolbar.cpp b/src/ui/toolbar/calligraphy-toolbar.cpp
new file mode 100644
index 0000000..05af968
--- /dev/null
+++ b/src/ui/toolbar/calligraphy-toolbar.cpp
@@ -0,0 +1,628 @@
+// 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 map_item : _widget_map) {
+ auto widget_name = map_item.first;
+ auto widget = map_item.second;
+
+ if (widget) {
+ if (GTK_IS_ADJUSTMENT(widget)) {
+ GtkAdjustment* adj = GTK_ADJUSTMENT(widget);
+ prefs->setDouble(save_path + "/" + widget_name, gtk_adjustment_get_value(adj));
+ //std::cout << "wrote adj " << widget_name << ": " << v << "\n";
+ } else if (GTK_IS_TOGGLE_TOOL_BUTTON(widget)) {
+ auto toggle = GTK_TOGGLE_TOOL_BUTTON(widget);
+ prefs->setBool(save_path + "/" + widget_name, gtk_toggle_tool_button_get_active(toggle));
+ //std::cout << "wrote tog " << widget_name << ": " << v << "\n";
+ } else {
+ g_warning("Unknown widget type for preset: %s\n", widget_name.c_str());
+ }
+ } else {
+ g_warning("Bad key when writing preset: %s\n", widget_name.c_str());
+ }
+ }
+ prefs->setString(save_path + "/name", new_profile_name);
+
+ _presets_blocked = true;
+ build_presets_list();
+}
+
+}
+}
+}
+
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/toolbar/calligraphy-toolbar.h b/src/ui/toolbar/calligraphy-toolbar.h
new file mode 100644
index 0000000..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..305698b
--- /dev/null
+++ b/src/ui/toolbar/connector-toolbar.cpp
@@ -0,0 +1,431 @@
+// 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"
+
+#include "xml/node-event-vector.h"
+
+using Inkscape::DocumentUndo;
+
+static Inkscape::XML::NodeEventVector connector_tb_repr_events = {
+ nullptr, /* child_added */
+ nullptr, /* child_removed */
+ Inkscape::UI::Toolbar::ConnectorToolbar::event_attr_changed,
+ nullptr, /* content_changed */
+ nullptr /* order_changed */
+};
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+ConnectorToolbar::ConnectorToolbar(SPDesktop *desktop)
+ : Toolbar(desktop),
+ _freeze(false),
+ _repr(nullptr)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ {
+ auto avoid_item = Gtk::manage(new Gtk::ToolButton(_("Avoid")));
+ avoid_item->set_tooltip_text(_("Make connectors avoid selected objects"));
+ avoid_item->set_icon_name(INKSCAPE_ICON("connector-avoid"));
+ avoid_item->signal_clicked().connect(sigc::mem_fun(*this, &ConnectorToolbar::path_set_avoid));
+ add(*avoid_item);
+ }
+
+ {
+ auto ignore_item = Gtk::manage(new Gtk::ToolButton(_("Ignore")));
+ ignore_item->set_tooltip_text(_("Make connectors ignore selected objects"));
+ ignore_item->set_icon_name(INKSCAPE_ICON("connector-ignore"));
+ ignore_item->signal_clicked().connect(sigc::mem_fun(*this, &ConnectorToolbar::path_set_ignore));
+ add(*ignore_item);
+ }
+
+ // Orthogonal connectors toggle button
+ {
+ _orthogonal = add_toggle_button(_("Orthogonal"),
+ _("Make connector orthogonal or polyline"));
+ _orthogonal->set_icon_name(INKSCAPE_ICON("connector-orthogonal"));
+
+ bool tbuttonstate = prefs->getBool("/tools/connector/orthogonal");
+ _orthogonal->set_active(( tbuttonstate ? TRUE : FALSE ));
+ _orthogonal->signal_toggled().connect(sigc::mem_fun(*this, &ConnectorToolbar::orthogonal_toggled));
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ // Curvature spinbox
+ auto curvature_val = prefs->getDouble("/tools/connector/curvature", defaultConnCurvature);
+ _curvature_adj = Gtk::Adjustment::create(curvature_val, 0, 100, 1.0, 10.0);
+ auto curvature_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("inkscape:connector-curvature", _("Curvature:"), _curvature_adj, 1, 0));
+ curvature_item->set_tooltip_text(_("The amount of connectors curvature"));
+ curvature_item->set_focus_widget(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->removeListenerByData(this);
+ Inkscape::GC::release(_repr);
+ _repr = nullptr;
+ }
+
+ if (repr) {
+ _repr = repr;
+ Inkscape::GC::anchor(_repr);
+ _repr->addListener(&connector_tb_repr_events, this);
+ _repr->synthesizeEvents(&connector_tb_repr_events, this);
+ }
+
+ show_all();
+}
+
+GtkWidget *
+ConnectorToolbar::create( SPDesktop *desktop)
+{
+ auto toolbar = new ConnectorToolbar(desktop);
+ return GTK_WIDGET(toolbar->gobj());
+} // end of ConnectorToolbar::prep()
+
+void
+ConnectorToolbar::path_set_avoid()
+{
+ Inkscape::UI::Tools::cc_selection_set_avoid(_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;
+
+ std::vector<SPItem *> items;
+ items = get_avoided_items(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 (SP_IS_PATH(item))
+ {
+ gdouble curvature = SP_PATH(item)->connEndPair.getCurvature();
+ bool is_orthog = SP_PATH(item)->connEndPair.isOrthogonal();
+ _orthogonal->set_active(is_orthog);
+ _curvature_adj->set_value(curvature);
+ }
+
+}
+
+void
+ConnectorToolbar::nooverlaps_graph_layout_toggled()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/tools/connector/avoidoverlaplayout",
+ _overlap_item->get_active());
+}
+
+void
+ConnectorToolbar::event_attr_changed(Inkscape::XML::Node *repr,
+ gchar const *name,
+ gchar const * /*old_value*/,
+ gchar const * /*new_value*/,
+ bool /*is_interactive*/,
+ gpointer data)
+{
+ auto toolbar = reinterpret_cast<ConnectorToolbar *>(data);
+
+ if ( !toolbar->_freeze
+ && (strcmp(name, "inkscape:connector-spacing") == 0) ) {
+ gdouble spacing = repr->getAttributeDouble("inkscape:connector-spacing", defaultConnSpacing);
+
+ toolbar->_spacing_adj->set_value(spacing);
+
+ if (toolbar->_desktop->canvas) {
+ toolbar->_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..66df79e
--- /dev/null
+++ b/src/ui/toolbar/connector-toolbar.h
@@ -0,0 +1,93 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CONNECTOR_TOOLBAR_H
+#define SEEN_CONNECTOR_TOOLBAR_H
+
+/**
+ * @file
+ * Connector aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbar.h"
+
+#include <gtkmm/adjustment.h>
+
+class SPDesktop;
+
+namespace Gtk {
+class ToolButton;
+}
+
+namespace Inkscape {
+class Selection;
+
+namespace XML {
+class Node;
+}
+
+namespace UI {
+namespace Toolbar {
+class ConnectorToolbar : public Toolbar {
+private:
+ Gtk::ToggleToolButton *_orthogonal;
+ Gtk::ToggleToolButton *_directed_item;
+ Gtk::ToggleToolButton *_overlap_item;
+
+ Glib::RefPtr<Gtk::Adjustment> _curvature_adj;
+ Glib::RefPtr<Gtk::Adjustment> _spacing_adj;
+ Glib::RefPtr<Gtk::Adjustment> _length_adj;
+
+ bool _freeze;
+
+ Inkscape::XML::Node *_repr;
+
+ void path_set_avoid();
+ void path_set_ignore();
+ void orthogonal_toggled();
+ void graph_layout();
+ void directed_graph_layout_toggled();
+ void nooverlaps_graph_layout_toggled();
+ void curvature_changed();
+ void spacing_changed();
+ void length_changed();
+ void selection_changed(Inkscape::Selection *selection);
+
+protected:
+ ConnectorToolbar(SPDesktop *desktop);
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+
+ static void event_attr_changed(Inkscape::XML::Node *repr,
+ gchar const *name,
+ gchar const * /*old_value*/,
+ gchar const * /*new_value*/,
+ bool /*is_interactive*/,
+ gpointer data);
+};
+
+}
+}
+}
+
+#endif /* !SEEN_CONNECTOR_TOOLBAR_H */
diff --git a/src/ui/toolbar/dropper-toolbar.cpp b/src/ui/toolbar/dropper-toolbar.cpp
new file mode 100644
index 0000000..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..c993e46
--- /dev/null
+++ b/src/ui/toolbar/eraser-toolbar.h
@@ -0,0 +1,101 @@
+// 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;
+extern EraserToolMode const DEFAULT_ERASER_MODE;
+} // 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..d2faeb1
--- /dev/null
+++ b/src/ui/toolbar/gradient-toolbar.cpp
@@ -0,0 +1,1173 @@
+// 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())
+ //&& SP_IS_GRADIENT(isFill ? style->getFillPaintServer() : style->getStrokePaintServer()) ) {
+ && (isFill ? SP_IS_GRADIENT(style->getFillPaintServer()) : SP_IS_GRADIENT(style->getStrokePaintServer())) ) {
+ SPPaintServer *server = isFill ? style->getFillPaintServer() : style->getStrokePaintServer();
+ if ( SP_IS_LINEARGRADIENT(server) ) {
+ sp_item_set_gradient(item, gr, SP_GRADIENT_TYPE_LINEAR, mode);
+ } else if ( SP_IS_RADIALGRADIENT(server) ) {
+ sp_item_set_gradient(item, gr, SP_GRADIENT_TYPE_RADIAL, mode);
+ }
+ }
+ else if (initialMode == mode)
+ {
+ sp_item_set_gradient(item, gr, initialType, mode);
+ }
+}
+
+/**
+Applies gradient vector gr to the gradients attached to the selected dragger of drag, or if none,
+to all objects in selection. If there was no previous gradient on an item, uses gradient type and
+fill/stroke setting from preferences to create new default (linear: left/right; radial: centered)
+gradient.
+*/
+void gr_apply_gradient(Inkscape::Selection *selection, GrDrag *drag, SPGradient *gr)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ SPGradientType initialType = static_cast<SPGradientType>(prefs->getInt("/tools/gradient/newgradient", SP_GRADIENT_TYPE_LINEAR));
+ Inkscape::PaintTarget initialMode = (prefs->getInt("/tools/gradient/newfillorstroke", 1) != 0) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE;
+
+ // GRADIENTFIXME: make this work for multiple selected draggers.
+
+ // First try selected dragger
+ if (drag && !drag->selected.empty()) {
+ GrDragger *dragger = *(drag->selected.begin());
+ for(auto draggable : dragger->draggables) { //for all draggables of dragger
+ gr_apply_gradient_to_item(draggable->item, gr, initialType, initialMode, draggable->fill_or_stroke);
+ }
+ return;
+ }
+
+ // If no drag or no dragger selected, act on selection
+ auto itemlist= selection->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ gr_apply_gradient_to_item(*i, gr, initialType, initialMode, initialMode);
+ }
+}
+
+int gr_vector_list(Glib::RefPtr<Gtk::ListStore> store, SPDesktop *desktop,
+ bool selection_empty, SPGradient *gr_selected, bool gr_multi)
+{
+ int selected = -1;
+
+ if (!blocked) {
+ std::cerr << "gr_vector_list: should be blocked!" << std::endl;
+ }
+
+ // Get list of gradients in document.
+ SPDocument *document = desktop->getDocument();
+ std::vector<SPObject *> gl;
+ std::vector<SPObject *> gradients = document->getResourceList( "gradient" );
+ for (auto gradient : gradients) {
+ SPGradient *grad = SP_GRADIENT(gradient);
+ if ( grad->hasStops() && !grad->isSolid() ) {
+ gl.push_back(gradient);
+ }
+ }
+
+ store->clear();
+
+ Inkscape::UI::Widget::ComboToolItemColumns columns;
+ Gtk::TreeModel::Row row;
+
+ if (gl.empty()) {
+ // The document has no gradients
+
+ row = *(store->append());
+ row[columns.col_label ] = _("No gradient");
+ row[columns.col_tooltip ] = "";
+ row[columns.col_icon ] = "NotUsed";
+ row[columns.col_data ] = nullptr;
+ row[columns.col_sensitive] = true;
+
+ } else if (selection_empty) {
+ // Document has gradients, but nothing is currently selected.
+
+ row = *(store->append());
+ row[columns.col_label ] = _("Nothing Selected");
+ row[columns.col_tooltip ] = "";
+ row[columns.col_icon ] = "NotUsed";
+ row[columns.col_data ] = nullptr;
+ row[columns.col_sensitive] = true;
+
+ } else {
+
+ if (gr_selected == nullptr) {
+ row = *(store->append());
+ row[columns.col_label ] = _("No gradient");
+ row[columns.col_tooltip ] = "";
+ row[columns.col_icon ] = "NotUsed";
+ row[columns.col_data ] = nullptr;
+ row[columns.col_sensitive] = true;
+ }
+
+ if (gr_multi) {
+ row = *(store->append());
+ row[columns.col_label ] = _("Multiple gradients");
+ row[columns.col_tooltip ] = "";
+ row[columns.col_icon ] = "NotUsed";
+ row[columns.col_data ] = nullptr;
+ row[columns.col_sensitive] = true;
+ }
+
+ int idx = 0;
+ for (auto it : gl) {
+ SPGradient *gradient = SP_GRADIENT(it);
+
+ Glib::ustring label = gr_prepare_label(gradient);
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf = sp_gradient_to_pixbuf_ref(gradient, 64, 16);
+
+ row = *(store->append());
+ row[columns.col_label ] = label;
+ row[columns.col_tooltip ] = "";
+ row[columns.col_icon ] = "NotUsed";
+ row[columns.col_pixbuf ] = pixbuf;
+ row[columns.col_data ] = gradient;
+ row[columns.col_sensitive] = true;
+
+ if (gradient == gr_selected) {
+ selected = idx;
+ }
+ idx ++;
+ }
+
+ if (gr_multi) {
+ selected = 0; // This will show "Multiple Gradients"
+ }
+ }
+
+ return selected;
+}
+
+/*
+ * Get the gradient of the selected desktop item
+ * This is gradient containing the repeat settings, not the underlying "getVector" href linked gradient.
+ */
+void gr_get_dt_selected_gradient(Inkscape::Selection *selection, SPGradient *&gr_selected)
+{
+ SPGradient *gradient = nullptr;
+
+ auto itemlist= selection->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ SPItem *item = *i;// get the items gradient, not the getVector() version
+ SPStyle *style = item->style;
+ SPPaintServer *server = nullptr;
+
+ if (style && (style->fill.isPaintserver())) {
+ server = item->style->getFillPaintServer();
+ }
+ if (style && (style->stroke.isPaintserver())) {
+ server = item->style->getStrokePaintServer();
+ }
+
+ if ( SP_IS_GRADIENT(server) ) {
+ gradient = SP_GRADIENT(server);
+ }
+ }
+
+ if (gradient && gradient->isSolid()) {
+ gradient = nullptr;
+ }
+
+ if (gradient) {
+ gr_selected = gradient;
+ }
+}
+
+/*
+ * Get the current selection and dragger status from the desktop
+ */
+void gr_read_selection( Inkscape::Selection *selection,
+ GrDrag *drag,
+ SPGradient *&gr_selected,
+ bool &gr_multi,
+ SPGradientSpread &spr_selected,
+ bool &spr_multi )
+{
+ if (drag && !drag->selected.empty()) {
+ // GRADIENTFIXME: make this work for more than one selected dragger?
+ GrDragger *dragger = *(drag->selected.begin());
+ for(auto draggable : dragger->draggables) { //for all draggables of dragger
+ SPGradient *gradient = sp_item_gradient_get_vector(draggable->item, draggable->fill_or_stroke);
+ SPGradientSpread spread = sp_item_gradient_get_spread(draggable->item, draggable->fill_or_stroke);
+
+ if (gradient && gradient->isSolid()) {
+ gradient = nullptr;
+ }
+
+ if (gradient && (gradient != gr_selected)) {
+ if (gr_selected) {
+ gr_multi = true;
+ } else {
+ gr_selected = gradient;
+ }
+ }
+ if (spread != spr_selected) {
+ if (spr_selected != SP_GRADIENT_SPREAD_UNDEFINED) {
+ spr_multi = true;
+ } else {
+ spr_selected = spread;
+ }
+ }
+ }
+ return;
+ }
+
+ // If no selected dragger, read desktop selection
+ auto itemlist= selection->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ SPItem *item = *i;
+ SPStyle *style = item->style;
+
+ if (style && (style->fill.isPaintserver())) {
+ SPPaintServer *server = item->style->getFillPaintServer();
+ if ( SP_IS_GRADIENT(server) ) {
+ SPGradient *gradient = SP_GRADIENT(server)->getVector();
+ SPGradientSpread spread = SP_GRADIENT(server)->fetchSpread();
+
+ if (gradient && gradient->isSolid()) {
+ gradient = nullptr;
+ }
+
+ if (gradient && (gradient != gr_selected)) {
+ if (gr_selected) {
+ gr_multi = true;
+ } else {
+ gr_selected = gradient;
+ }
+ }
+ if (spread != spr_selected) {
+ if (spr_selected != SP_GRADIENT_SPREAD_UNDEFINED) {
+ spr_multi = true;
+ } else {
+ spr_selected = spread;
+ }
+ }
+ }
+ }
+ if (style && (style->stroke.isPaintserver())) {
+ SPPaintServer *server = item->style->getStrokePaintServer();
+ if ( SP_IS_GRADIENT(server) ) {
+ SPGradient *gradient = SP_GRADIENT(server)->getVector();
+ SPGradientSpread spread = SP_GRADIENT(server)->fetchSpread();
+
+ if (gradient && gradient->isSolid()) {
+ gradient = nullptr;
+ }
+
+ if (gradient && (gradient != gr_selected)) {
+ if (gr_selected) {
+ gr_multi = true;
+ } else {
+ gr_selected = gradient;
+ }
+ }
+ if (spread != spr_selected) {
+ if (spr_selected != SP_GRADIENT_SPREAD_UNDEFINED) {
+ spr_multi = true;
+ } else {
+ spr_selected = spread;
+ }
+ }
+ }
+ }
+ }
+ }
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+GradientToolbar::GradientToolbar(SPDesktop *desktop)
+ : Toolbar(desktop)
+{
+ auto prefs = Inkscape::Preferences::get();
+
+ /* New gradient linear or radial */
+ {
+ add_label(_("New:"));
+
+ Gtk::RadioToolButton::Group new_type_group;
+
+ auto linear_button = Gtk::manage(new Gtk::RadioToolButton(new_type_group, _("linear")));
+ linear_button->set_tooltip_text(_("Create linear gradient"));
+ linear_button->set_icon_name(INKSCAPE_ICON("paint-gradient-linear"));
+ _new_type_buttons.push_back(linear_button);
+
+ auto radial_button = Gtk::manage(new Gtk::RadioToolButton(new_type_group, _("radial")));
+ radial_button->set_tooltip_text(_("Create radial (elliptic or circular) gradient"));
+ radial_button->set_icon_name(INKSCAPE_ICON("paint-gradient-radial"));
+ _new_type_buttons.push_back(radial_button);
+
+ gint mode = prefs->getInt("/tools/gradient/newgradient", SP_GRADIENT_TYPE_LINEAR);
+ _new_type_buttons[ mode == SP_GRADIENT_TYPE_LINEAR ? 0 : 1 ]->set_active(); // linear == 1, radial == 2
+
+ int btn_index = 0;
+ for (auto btn : _new_type_buttons)
+ {
+ btn->set_sensitive(true);
+ btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &GradientToolbar::new_type_changed), btn_index++));
+ add(*btn);
+ }
+ }
+
+ /* New gradient on fill or stroke*/
+ {
+ Gtk::RadioToolButton::Group new_fillstroke_group;
+
+ auto fill_btn = Gtk::manage(new Gtk::RadioToolButton(new_fillstroke_group, _("fill")));
+ fill_btn->set_tooltip_text(_("Create gradient in the fill"));
+ fill_btn->set_icon_name(INKSCAPE_ICON("object-fill"));
+ _new_fillstroke_buttons.push_back(fill_btn);
+
+ auto stroke_btn = Gtk::manage(new Gtk::RadioToolButton(new_fillstroke_group, _("stroke")));
+ stroke_btn->set_tooltip_text(_("Create gradient in the stroke"));
+ stroke_btn->set_icon_name(INKSCAPE_ICON("object-stroke"));
+ _new_fillstroke_buttons.push_back(stroke_btn);
+
+ auto fsmode = (prefs->getInt("/tools/gradient/newfillorstroke", 1) != 0) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE;
+ _new_fillstroke_buttons[ fsmode == Inkscape::FOR_FILL ? 0 : 1 ]->set_active();
+
+ auto btn_index = 0;
+ for (auto btn : _new_fillstroke_buttons)
+ {
+ btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &GradientToolbar::new_fillstroke_changed), btn_index++));
+ btn->set_sensitive();
+ add(*btn);
+ }
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ /* Gradient Select list*/
+ {
+ UI::Widget::ComboToolItemColumns columns;
+
+ auto store = Gtk::ListStore::create(columns);
+
+ Gtk::TreeModel::Row row;
+
+ row = *(store->append());
+ row[columns.col_label ] = _("No gradient");
+ row[columns.col_tooltip ] = "";
+ row[columns.col_icon ] = "NotUsed";
+ row[columns.col_sensitive] = true;
+
+ _select_cb = UI::Widget::ComboToolItem::create(_("Select"), // Label
+ "", // Tooltip
+ "Not Used", // Icon
+ store ); // Tree store
+
+ _select_cb->use_icon( false );
+ _select_cb->use_pixbuf( true );
+ _select_cb->use_group_label( true );
+ _select_cb->set_active( 0 );
+ _select_cb->set_sensitive( false );
+
+ add(*_select_cb);
+ _select_cb->signal_changed().connect(sigc::mem_fun(*this, &GradientToolbar::gradient_changed));
+ }
+
+ // Gradients Linked toggle
+ {
+ _linked_item = add_toggle_button(_("Link gradients"),
+ _("Link gradients to change all related gradients"));
+ _linked_item->set_icon_name(INKSCAPE_ICON("object-unlocked"));
+ _linked_item->signal_toggled().connect(sigc::mem_fun(*this, &GradientToolbar::linked_changed));
+
+ bool linkedmode = prefs->getBool("/options/forkgradientvectors/value", true);
+ _linked_item->set_active(!linkedmode);
+ }
+
+ /* Reverse */
+ {
+ _stops_reverse_item = Gtk::manage(new Gtk::ToolButton(_("Reverse")));
+ _stops_reverse_item->set_tooltip_text(_("Reverse the direction of the gradient"));
+ _stops_reverse_item->set_icon_name(INKSCAPE_ICON("object-flip-horizontal"));
+ _stops_reverse_item->signal_clicked().connect(sigc::mem_fun(*this, &GradientToolbar::reverse));
+ add(*_stops_reverse_item);
+ _stops_reverse_item->set_sensitive(false);
+ }
+
+ // Gradient Spread type (how a gradient is drawn outside its nominal area)
+ {
+ UI::Widget::ComboToolItemColumns columns;
+ Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns);
+
+ std::vector<gchar*> spread_dropdown_items_list = {
+ const_cast<gchar *>(C_("Gradient repeat type", "None")),
+ _("Reflected"),
+ _("Direct")
+ };
+
+ for (auto item: spread_dropdown_items_list) {
+ Gtk::TreeModel::Row row = *(store->append());
+ row[columns.col_label ] = item;
+ row[columns.col_sensitive] = true;
+ }
+
+ _spread_cb = Gtk::manage(UI::Widget::ComboToolItem::create(_("Repeat"),
+ // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/pservers.html#LinearGradientSpreadMethodAttribute
+ _("Whether to fill with flat color beyond the ends of the gradient vector "
+ "(spreadMethod=\"pad\"), or repeat the gradient in the same direction "
+ "(spreadMethod=\"repeat\"), or repeat the gradient in alternating opposite "
+ "directions (spreadMethod=\"reflect\")"),
+ "Not Used", store));
+ _spread_cb->use_group_label(true);
+
+ _spread_cb->set_active(0);
+ _spread_cb->set_sensitive(false);
+
+ _spread_cb->signal_changed().connect(sigc::mem_fun(*this, &GradientToolbar::spread_changed));
+ add(*_spread_cb);
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ /* Gradient Stop list */
+ {
+ UI::Widget::ComboToolItemColumns columns;
+
+ auto store = Gtk::ListStore::create(columns);
+
+ Gtk::TreeModel::Row row;
+
+ row = *(store->append());
+ row[columns.col_label ] = _("No stops");
+ row[columns.col_tooltip ] = "";
+ row[columns.col_icon ] = "NotUsed";
+ row[columns.col_sensitive] = true;
+
+ _stop_cb =
+ UI::Widget::ComboToolItem::create(_("Stops" ), // Label
+ "", // Tooltip
+ "Not Used", // Icon
+ store ); // Tree store
+
+ _stop_cb->use_icon( false );
+ _stop_cb->use_pixbuf( true );
+ _stop_cb->use_group_label( true );
+ _stop_cb->set_active( 0 );
+ _stop_cb->set_sensitive( false );
+
+ add(*_stop_cb);
+ _stop_cb->signal_changed().connect(sigc::mem_fun(*this, &GradientToolbar::stop_changed));
+ }
+
+ /* Offset */
+ {
+ auto offset_val = prefs->getDouble("/tools/gradient/stopoffset", 0);
+ _offset_adj = Gtk::Adjustment::create(offset_val, 0.0, 1.0, 0.01, 0.1);
+ _offset_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("gradient-stopoffset", C_("Gradient", "Offset:"), _offset_adj, 0.01, 2));
+ _offset_item->set_tooltip_text(_("Offset of selected stop"));
+ _offset_item->set_focus_widget(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();
+ SPGradient *gradient = nullptr;
+ gr_get_dt_selected_gradient(selection, gradient);
+
+ if (gradient) {
+ SPGradientSpread spread = (SPGradientSpread) active;
+ gradient->setSpread(spread);
+ gradient->updateRepr();
+
+ DocumentUndo::done(_desktop->getDocument(), _("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;
+ }
+ bool isEndStop = false;
+
+ SPStop *prev = nullptr;
+ prev = stop->getPrevStop();
+ if (prev != nullptr ) {
+ _offset_adj->set_lower(prev->offset);
+ } else {
+ isEndStop = true;
+ _offset_adj->set_lower(0);
+ }
+
+ SPStop *next = nullptr;
+ next = stop->getNextStop();
+ if (next != nullptr ) {
+ _offset_adj->set_upper(next->offset);
+ } else {
+ isEndStop = true;
+ _offset_adj->set_upper(1.0);
+ }
+
+ _offset_adj->set_value(stop->offset);
+ _offset_item->set_sensitive( !isEndStop );
+}
+
+/**
+ * \brief User changed the offset
+ */
+void
+GradientToolbar::stop_offset_adjustment_changed()
+{
+ if (blocked) {
+ return;
+ }
+
+ blocked = true;
+
+ SPStop *stop = get_selected_stop();
+ if (stop) {
+ stop->offset = _offset_adj->get_value();
+ 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;
+
+ blocked = true;
+
+ if (!_desktop) {
+ return;
+ }
+
+ Inkscape::Selection *selection = _desktop->getSelection(); // take from desktop, not from args
+ if (selection) {
+
+ ToolBase *ev = _desktop->getEventContext();
+ GrDrag *drag = nullptr;
+ if (ev) {
+ drag = ev->get_drag();
+ }
+
+ SPGradient *gr_selected = nullptr;
+ SPGradientSpread spr_selected = SP_GRADIENT_SPREAD_UNDEFINED;
+ bool gr_multi = false;
+ bool spr_multi = false;
+
+ gr_read_selection(selection, drag, gr_selected, gr_multi, spr_selected, spr_multi);
+
+ // Gradient selection menu
+ auto store = _select_cb->get_store();
+ int gradient = gr_vector_list (store, _desktop, selection->isEmpty(), gr_selected, gr_multi);
+
+ if (gradient < 0) {
+ // No selection or no gradients
+ _select_cb->set_active( 0 );
+ _select_cb->set_sensitive (false);
+ } else {
+ // Single gradient or multiple gradients
+ _select_cb->set_active( gradient );
+ _select_cb->set_sensitive (true);
+ }
+
+ // Spread menu
+ _spread_cb->set_sensitive( gr_selected && !gr_multi );
+ _spread_cb->set_active( gr_selected ? (int)spr_selected : 0 );
+
+ _stops_add_item->set_sensitive((gr_selected && !gr_multi && drag && !drag->selected.empty()));
+ _stops_delete_item->set_sensitive((gr_selected && !gr_multi && drag && !drag->selected.empty()));
+ _stops_reverse_item->set_sensitive((gr_selected!= nullptr));
+
+ _stop_cb->set_sensitive( gr_selected && !gr_multi);
+
+ update_stop_list (gr_selected, nullptr, gr_multi);
+ select_stop_by_draggers(gr_selected, ev);
+ }
+
+ blocked = false;
+}
+
+/**
+ * \brief Construct stop list
+ */
+int
+GradientToolbar::update_stop_list( SPGradient *gradient, SPStop *new_stop, bool gr_multi)
+{
+ if (!blocked) {
+ std::cerr << "update_stop_list should be blocked!" << std::endl;
+ }
+
+ int selected = -1;
+
+ auto store = _stop_cb->get_store();
+
+ if (!store) {
+ return selected;
+ }
+
+ store->clear();
+
+ UI::Widget::ComboToolItemColumns columns;
+ Gtk::TreeModel::Row row;
+
+ if (!SP_IS_GRADIENT(gradient)) {
+ // No valid gradient
+
+ row = *(store->append());
+ row[columns.col_label ] = _("No gradient");
+ row[columns.col_tooltip ] = "";
+ row[columns.col_icon ] = "NotUsed";
+ row[columns.col_data ] = nullptr;
+ row[columns.col_sensitive] = true;
+
+ } else if (!gradient->hasStops()) {
+ // Has gradient but it has no stops
+
+ row = *(store->append());
+ row[columns.col_label ] = _("No stops in gradient");
+ row[columns.col_tooltip ] = "";
+ row[columns.col_icon ] = "NotUsed";
+ row[columns.col_data ] = nullptr;
+ row[columns.col_sensitive] = true;
+
+ } else {
+ // Gradient has stops
+
+ // Get list of stops
+ for (auto& ochild: gradient->children) {
+ if (SP_IS_STOP(&ochild)) {
+
+ SPStop *stop = SP_STOP(&ochild);
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf = sp_gradstop_to_pixbuf_ref (stop, 32, 16);
+
+ Inkscape::XML::Node *repr = reinterpret_cast<SPItem *>(&ochild)->getRepr();
+ Glib::ustring label = gr_ellipsize_text(repr->attribute("id"), 25);
+
+ row = *(store->append());
+ row[columns.col_label ] = label;
+ row[columns.col_tooltip ] = "";
+ row[columns.col_icon ] = "NotUsed";
+ row[columns.col_pixbuf ] = pixbuf;
+ row[columns.col_data ] = stop;
+ row[columns.col_sensitive] = true;
+ }
+ }
+ }
+
+ if (new_stop != nullptr) {
+ selected = select_stop_in_list (gradient, new_stop);
+ }
+
+ return selected;
+}
+
+/**
+ * \brief Find position of new_stop in menu.
+ */
+int
+GradientToolbar::select_stop_in_list(SPGradient *gradient, SPStop *new_stop)
+{
+ int i = 0;
+ for (auto& ochild: gradient->children) {
+ if (SP_IS_STOP(&ochild)) {
+ if (&ochild == new_stop) {
+ return i;
+ }
+ i++;
+ }
+ }
+ return -1;
+}
+
+/**
+ * \brief Set stop in menu to match stops selected by draggers
+ */
+void
+GradientToolbar::select_stop_by_draggers(SPGradient *gradient, ToolBase *ev)
+{
+ if (!blocked) {
+ std::cerr << "select_stop_by_draggers should be blocked!" << std::endl;
+ }
+
+ if (!ev || !gradient)
+ return;
+
+ SPGradient *vector = gradient->getVector();
+ if (!vector)
+ return;
+
+ GrDrag *drag = ev->get_drag();
+
+ if (!drag || drag->selected.empty()) {
+ _stop_cb->set_active(0);
+ stop_set_offset();
+ return;
+ }
+
+ gint n = 0;
+ SPStop *stop = nullptr;
+ int selected = -1;
+
+ // For all selected draggers
+ for(auto dragger : drag->selected) {
+
+ // For all draggables of dragger
+ for(auto draggable : dragger->draggables) {
+
+ if (draggable->point_type != POINT_RG_FOCUS) {
+ n++;
+ if (n > 1) break;
+ }
+
+ stop = vector->getFirstStop();
+
+ switch (draggable->point_type) {
+ case POINT_LG_MID:
+ case POINT_RG_MID1:
+ case POINT_RG_MID2:
+ stop = sp_get_stop_i(vector, draggable->point_i);
+ break;
+ case POINT_LG_END:
+ case POINT_RG_R1:
+ case POINT_RG_R2:
+ stop = sp_last_stop(vector);
+ break;
+ default:
+ break;
+ }
+ }
+ if (n > 1) break;
+ }
+
+ if (n > 1) {
+ // Multiple stops selected
+ if (_offset_item) {
+ _offset_item->set_sensitive(false);
+ }
+
+ // Stop list always updated first... reinsert "Multiple stops" as first entry.
+ UI::Widget::ComboToolItemColumns columns;
+ auto store = _stop_cb->get_store();
+
+ auto row = *(store->prepend());
+ row[columns.col_label ] = _("Multiple stops");
+ row[columns.col_tooltip ] = "";
+ row[columns.col_icon ] = "NotUsed";
+ row[columns.col_sensitive] = true;
+ selected = 0;
+
+ } else {
+ selected = select_stop_in_list(gradient, stop);
+ }
+
+ if (selected < 0) {
+ _stop_cb->set_active (0);
+ _stop_cb->set_sensitive (false);
+ } else {
+ _stop_cb->set_active (selected);
+ _stop_cb->set_sensitive (true);
+ stop_set_offset();
+ }
+}
+
+void
+GradientToolbar::selection_modified(Inkscape::Selection *selection, guint /*flags*/)
+{
+ selection_changed(selection);
+}
+
+void
+GradientToolbar::drag_selection_changed(gpointer /*dragger*/)
+{
+ selection_changed(nullptr);
+}
+
+void
+GradientToolbar::defs_release(SPObject * /*defs*/)
+{
+ selection_changed(nullptr);
+}
+
+void
+GradientToolbar::defs_modified(SPObject * /*defs*/, guint /*flags*/)
+{
+ selection_changed(nullptr);
+}
+
+}
+}
+}
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/ui/toolbar/gradient-toolbar.h b/src/ui/toolbar/gradient-toolbar.h
new file mode 100644
index 0000000..96beb0f
--- /dev/null
+++ b/src/ui/toolbar/gradient-toolbar.h
@@ -0,0 +1,105 @@
+// 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;
+
+ 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..df45e22
--- /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->selection;
+
+ auto bbox = selection->visualBounds();
+
+ if (bbox) {
+ Geom::Point A(bbox->min());
+ Geom::Point B(bbox->max());
+
+ A *= _desktop->doc2dt();
+ B *= _desktop->doc2dt();
+
+ // TODO: should we provide a way to store points in prefs?
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setDouble("/tools/lpetool/bbox_upperleftx", A[Geom::X]);
+ prefs->setDouble("/tools/lpetool/bbox_upperlefty", A[Geom::Y]);
+ prefs->setDouble("/tools/lpetool/bbox_lowerrightx", B[Geom::X]);
+ prefs->setDouble("/tools/lpetool/bbox_lowerrighty", B[Geom::Y]);
+
+ lpetool_context_reset_limiting_bbox(SP_LPETOOL_CONTEXT(_desktop->event_context));
+ }
+
+ _bbox_from_selection_item->set_active(false);
+}
+
+void
+LPEToolbar::change_line_segment_type(int mode)
+{
+ using namespace Inkscape::LivePathEffect;
+
+ // quit if run by the attr_changed listener
+ if (_freeze) {
+ return;
+ }
+
+ // in turn, prevent listener from responding
+ _freeze = true;
+ auto line_seg = dynamic_cast<LPELineSegment *>(_currentlpe);
+
+ if (_currentlpeitem && line_seg) {
+ line_seg->end_type.param_set_value(static_cast<Inkscape::LivePathEffect::EndType>(mode));
+ sp_lpe_item_update_patheffect(_currentlpeitem, true, true);
+ }
+
+ _freeze = false;
+}
+
+void
+LPEToolbar::toggle_show_measuring_info()
+{
+ 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 && SP_IS_LPE_ITEM(item) && lpetool_item_has_construction(lc, item)) {
+
+ SPLPEItem *lpeitem = SP_LPE_ITEM(item);
+ Effect* lpe = lpeitem->getCurrentLPE();
+ if (lpe && lpe->effectType() == LINE_SEGMENT) {
+ LPELineSegment *lpels = static_cast<LPELineSegment*>(lpe);
+ _currentlpe = lpe;
+ _currentlpeitem = lpeitem;
+ _line_segment_combo->set_sensitive(true);
+ _line_segment_combo->set_active( lpels->end_type.get_value() );
+ } else {
+ _currentlpe = nullptr;
+ _currentlpeitem = nullptr;
+ _line_segment_combo->set_sensitive(false);
+ }
+
+ } else {
+ _currentlpe = nullptr;
+ _currentlpeitem = nullptr;
+ _line_segment_combo->set_sensitive(false);
+ }
+}
+
+}
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 :
diff --git a/src/ui/toolbar/lpe-toolbar.h b/src/ui/toolbar/lpe-toolbar.h
new file mode 100644
index 0000000..903d9da
--- /dev/null
+++ b/src/ui/toolbar/lpe-toolbar.h
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_LPE_TOOLBAR_H
+#define SEEN_LPE_TOOLBAR_H
+
+/**
+ * @file
+ * LPE aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbar.h"
+
+class SPDesktop;
+class SPLPEItem;
+
+namespace Gtk {
+class RadioToolButton;
+}
+
+namespace Inkscape {
+class Selection;
+
+namespace LivePathEffect {
+class Effect;
+}
+
+namespace UI {
+namespace Tools {
+class ToolBase;
+}
+
+namespace Widget {
+class ComboToolItem;
+class UnitTracker;
+}
+
+namespace Toolbar {
+class LPEToolbar : public Toolbar {
+private:
+ std::unique_ptr<UI::Widget::UnitTracker> _tracker;
+ std::vector<Gtk::RadioToolButton *> _mode_buttons;
+ Gtk::ToggleToolButton *_show_bbox_item;
+ Gtk::ToggleToolButton *_bbox_from_selection_item;
+ Gtk::ToggleToolButton *_measuring_item;
+ Gtk::ToggleToolButton *_open_lpe_dialog_item;
+ UI::Widget::ComboToolItem *_line_segment_combo;
+ UI::Widget::ComboToolItem *_units_item;
+
+ bool _freeze;
+
+ LivePathEffect::Effect *_currentlpe;
+ SPLPEItem *_currentlpeitem;
+
+ sigc::connection c_selection_modified;
+ sigc::connection c_selection_changed;
+
+ void mode_changed(int mode);
+ void unit_changed(int not_used);
+ void sel_modified(Inkscape::Selection *selection, guint flags);
+ void sel_changed(Inkscape::Selection *selection);
+ void change_line_segment_type(int mode);
+ void watch_ec(SPDesktop* desktop, UI::Tools::ToolBase* ec);
+
+ void toggle_show_bbox();
+ void toggle_set_bbox();
+ void toggle_show_measuring_info();
+ void open_lpe_dialog();
+
+protected:
+ LPEToolbar(SPDesktop *desktop);
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+ void set_mode(int mode);
+};
+
+}
+}
+}
+
+#endif /* !SEEN_LPE_TOOLBAR_H */
diff --git a/src/ui/toolbar/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..a1ed631
--- /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();
+ SPMeshGradient *mesh = dynamic_cast<SPMeshGradient *>(server);
+ if (mesh) {
+ ms_selected.push_back(mesh);
+ }
+ }
+
+ if (edit_stroke && style->stroke.isPaintserver()) {
+ SPPaintServer *server = item->style->getStrokePaintServer();
+ SPMeshGradient *mesh = dynamic_cast<SPMeshGradient *>(server);
+ if (mesh) {
+ ms_selected.push_back(mesh);
+ }
+ }
+ }
+
+ }
+ return ms_selected;
+}
+
+
+/*
+ * Get the current selection status from the desktop
+ */
+void ms_read_selection( Inkscape::Selection *selection,
+ SPMeshGradient *&ms_selected,
+ bool &ms_selected_multi,
+ SPMeshType &ms_type,
+ bool &ms_type_multi )
+{
+ ms_selected = nullptr;
+ ms_selected_multi = false;
+ ms_type = SP_MESH_TYPE_COONS;
+ ms_type_multi = false;
+
+ bool first = true;
+
+ // Read desktop selection, taking into account fill/stroke toggles
+ std::vector<SPMeshGradient *> meshes = ms_get_dt_selected_gradients( selection );
+ for (auto & meshe : meshes) {
+ if (first) {
+ ms_selected = meshe;
+ ms_type = meshe->type;
+ first = false;
+ } else {
+ if (ms_selected != meshe) {
+ ms_selected_multi = true;
+ }
+ if (ms_type != meshe->type) {
+ ms_type_multi = true;
+ }
+ }
+ }
+}
+
+
+/*
+ * Callback functions for user actions
+ */
+
+
+/** Temporary hack: Returns the mesh tool in the active desktop.
+ * Will go away during tool refactoring. */
+static MeshTool *get_mesh_tool()
+{
+ MeshTool *tool = nullptr;
+ if (SP_ACTIVE_DESKTOP ) {
+ Inkscape::UI::Tools::ToolBase *ec = SP_ACTIVE_DESKTOP->event_context;
+ if (SP_IS_MESH_CONTEXT(ec)) {
+ tool = static_cast<MeshTool*>(ec);
+ }
+ }
+ return tool;
+}
+
+
+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..d3be665
--- /dev/null
+++ b/src/ui/toolbar/node-toolbar.cpp
@@ -0,0 +1,663 @@
+// 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 "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(_("Make selected segments lines"));
+ line_item->set_icon_name(INKSCAPE_ICON("node-segment-line"));
+ line_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_toline));
+ add(*line_item);
+ }
+
+ {
+ auto curve_item = Gtk::manage(new Gtk::ToolButton(_("Node Curve")));
+ curve_item->set_tooltip_text(_("Make selected segments curves"));
+ curve_item->set_icon_name(INKSCAPE_ICON("node-segment-curve"));
+ curve_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_tocurve));
+ add(*curve_item);
+ }
+
+ add(* Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ {
+ auto 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];
+ Geom::Point delta(0,0);
+ delta[d] = val - oldval;
+ nt->_multipath->move(delta);
+ }
+
+ _freeze = false;
+}
+
+void
+NodeToolbar::sel_changed(Inkscape::Selection *selection)
+{
+ SPItem *item = selection->singleItem();
+ if (item && SP_IS_LPE_ITEM(item)) {
+ if (SP_LPE_ITEM(item)->hasPathEffect()) {
+ _nodes_lpeedit_item->set_sensitive(true);
+ } else {
+ _nodes_lpeedit_item->set_sensitive(false);
+ }
+ } else {
+ _nodes_lpeedit_item->set_sensitive(false);
+ }
+}
+
+void
+NodeToolbar::watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec)
+{
+ if (INK_IS_NODE_TOOL(ec)) {
+ // watch selection
+ c_selection_changed = desktop->getSelection()->connectChanged(sigc::mem_fun(*this, &NodeToolbar::sel_changed));
+ c_selection_modified = desktop->getSelection()->connectModified(sigc::mem_fun(*this, &NodeToolbar::sel_modified));
+ c_subselection_changed = desktop->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();
+
+ 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..d845872
--- /dev/null
+++ b/src/ui/toolbar/page-toolbar.cpp
@@ -0,0 +1,351 @@
+// 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 "io/resource.h"
+#include "object/sp-namedview.h"
+#include "object/sp-page.h"
+#include "ui/icon-names.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 {
+
+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_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);
+
+ if (text_page_label) {
+ text_page_label->signal_changed().connect(sigc::mem_fun(*this, &PageToolbar::labelEdited));
+ }
+
+ if (combo_page_sizes) {
+ combo_page_sizes->signal_changed().connect(sigc::mem_fun(*this, &PageToolbar::sizeChoose));
+ 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->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* focus){
+ entry_page_sizes->set_text("");
+ return false;
+ });
+ }
+ auto& page_sizes = Inkscape::PaperSize::getPageSizes();
+ for (int i = 0; i < page_sizes.size(); i++) {
+ combo_page_sizes->append(std::to_string(i), page_sizes[i].getDescription(false));
+ }
+ }
+
+ // 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;
+}
+
+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::sizeChoose()
+{
+ auto &pm = _document->getPageManager();
+ try {
+ auto p_rect = pm.getSelectedPageRect();
+ bool landscape = p_rect.width() > p_rect.height();
+
+ auto page_id = std::stoi(combo_page_sizes->get_active_id());
+ auto& page_sizes = Inkscape::PaperSize::getPageSizes();
+ if (page_id >= 0 && page_id < page_sizes.size()) {
+ auto&& ps = page_sizes[page_id];
+ // Keep page orientation while selecting size
+ auto width = ps.unit->convert(ps.size[landscape], "px");
+ auto height = ps.unit->convert(ps.size[!landscape], "px");
+ pm.resizePage(width, height);
+ setSizeText();
+ DocumentUndo::maybeDone(_document, "page-resize", _("Resize Page"), INKSCAPE_ICON("tool-pages"));
+ }
+ } catch (std::invalid_argument const &e) {
+ // Ignore because user is typing into Entry
+ }
+}
+
+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)) {
+ 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)
+{
+ if (!page)
+ page = _document->getPageManager().getSelected();
+
+ auto unit = _document->getDisplayUnit();
+ double width = _document->getWidth().value(unit);
+ double height = _document->getHeight().value(unit);
+ if (page) {
+ auto px = Inkscape::Util::unit_table.getUnit("px");
+ auto rect = page->getDesktopRect();
+ width = px->convert(rect.width(), unit);
+ height = px->convert(rect.height(), unit);
+ }
+ // Orientation button
+ std::string icon = width > height ? "page-landscape" : "page-portrait";
+ if (width == 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 (auto page_size = Inkscape::PaperSize::findPaperSize(width, height, unit)) {
+ entry_page_sizes->set_text(page_size->getDescription(width > height));
+ } else {
+ entry_page_sizes->set_text(Inkscape::PaperSize::toDescription(_("Custom"), width, height, unit));
+ }
+ // Select text if box is currently in focus.
+ if (entry_page_sizes->has_focus()) {
+ entry_page_sizes->select_region(0, -1);
+ }
+}
+
+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"));
+
+ // 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 = dynamic_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)
+{
+ Glib::ustring page_toolbar_builder_file = get_filename(UIS, "toolbar-page.ui");
+ PageToolbar *toolbar = nullptr;
+
+ try {
+ auto builder = Gtk::Builder::create_from_file(page_toolbar_builder_file);
+ builder->get_widget_derived("page-toolbar", toolbar, desktop);
+
+ if (!toolbar) {
+ std::cerr << "InkscapeWindow: Failed to load page toolbar!" << std::endl;
+ return nullptr;
+ }
+ // Usually we should be packing this widget into a parent before the builder
+ // is destroyed, but the create method expects a blind widget so this widget
+ // contains a special keep-alive pattern which can be removed when refactoring.
+ } catch (const Glib::Error &ex) {
+ std::cerr << "PageToolbar: " << page_toolbar_builder_file << " file not read! " << ex.what().raw() << std::endl;
+ }
+ 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..8a2ec08
--- /dev/null
+++ b/src/ui/toolbar/page-toolbar.h
@@ -0,0 +1,91 @@
+// 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 "toolbar.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 sizeChoose();
+ void sizeChanged();
+ void setSizeText(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;
+
+ 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_label;
+ 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;
+
+ 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..95de66a
--- /dev/null
+++ b/src/ui/toolbar/pencil-toolbar.cpp
@@ -0,0 +1,692 @@
+// 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) {
+ _repr->removeListenerByData(this);
+ GC::release(_repr);
+ _repr = nullptr;
+ }
+}
+
+void
+PencilToolbar::mode_changed(int mode)
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setInt(freehand_tool_name() + "/freehand-mode", mode);
+
+ if (mode == 1 || mode == 2) {
+ _flatten_spiro_bspline->set_visible(true);
+ } else {
+ _flatten_spiro_bspline->set_visible(false);
+ }
+
+ bool visible = (mode != 2);
+
+ if (_simplify) {
+ _simplify->set_visible(visible);
+ if (_flatten_simplify) {
+ _flatten_simplify->set_visible(visible && _simplify->get_active());
+ }
+ }
+
+ // 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 = dynamic_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 = dynamic_cast<SPLPEItem*>(*it);
+ if (lpeitem && lpeitem->hasPathEffect()){
+ PathEffectList lpelist = lpeitem->getEffectList();
+ PathEffectList::iterator i;
+ for (i = lpelist.begin(); i != lpelist.end(); ++i) {
+ LivePathEffectObject *lpeobj = (*i)->lpeobject;
+ if (lpeobj) {
+ Inkscape::LivePathEffect::Effect *lpe = lpeobj->get_lpe();
+ if (dynamic_cast<Inkscape::LivePathEffect::LPESimplify *>(lpe)) {
+ SPShape * shape = dynamic_cast<SPShape *>(lpeitem);
+ if(shape){
+ auto c = SPCurve::copy(shape->curveForEdit());
+ lpe->doEffect(c.get());
+ 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 = dynamic_cast<SPLPEItem*>(*it);
+ if (lpeitem && lpeitem->hasPathEffect()){
+ PathEffectList lpelist = lpeitem->getEffectList();
+ PathEffectList::iterator i;
+ for (i = lpelist.begin(); i != lpelist.end(); ++i) {
+ LivePathEffectObject *lpeobj = (*i)->lpeobject;
+ if (lpeobj) {
+ Inkscape::LivePathEffect::Effect *lpe = lpeobj->get_lpe();
+ if (dynamic_cast<Inkscape::LivePathEffect::LPEBSpline *>(lpe) ||
+ dynamic_cast<Inkscape::LivePathEffect::LPESpiro *>(lpe))
+ {
+ SPShape * shape = dynamic_cast<SPShape *>(lpeitem);
+ if(shape){
+ auto c = SPCurve::copy(shape->curveForEdit());
+ lpe->doEffect(c.get());
+ 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){
+ SPLPEItem* lpeitem = dynamic_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);
+ SPShape *sp_shape = dynamic_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..0e59f1e
--- /dev/null
+++ b/src/ui/toolbar/rect-toolbar.cpp
@@ -0,0 +1,416 @@
+// 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"
+
+#include "xml/node-event-vector.h"
+
+using Inkscape::UI::Widget::UnitTracker;
+using Inkscape::DocumentUndo;
+using Inkscape::Util::Unit;
+using Inkscape::Util::Quantity;
+using Inkscape::Util::unit_table;
+
+static Inkscape::XML::NodeEventVector rect_tb_repr_events = {
+ nullptr, /* child_added */
+ nullptr, /* child_removed */
+ Inkscape::UI::Toolbar::RectToolbar::event_attr_changed,
+ nullptr, /* content_changed */
+ nullptr /* order_changed */
+};
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+
+RectToolbar::RectToolbar(SPDesktop *desktop)
+ : Toolbar(desktop),
+ _tracker(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR)),
+ _freeze(false),
+ _single(true),
+ _repr(nullptr),
+ _mode_item(Gtk::manage(new UI::Widget::LabelToolItem(_("<b>New:</b>"))))
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ // rx/ry units menu: create
+ //tracker->addUnit( SP_UNIT_PERCENT, 0 );
+ // fixme: add % meaning per cent of the width/height
+ 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->removeListenerByData(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 (SP_IS_RECT(*i)) {
+ if (adj->get_value() != 0) {
+ (SP_RECT(*i)->*setter)(Quantity::convert(adj->get_value(), unit, "px"));
+ } else {
+ (*i)->removeAttribute(value_name);
+ }
+ modmade = true;
+ }
+ }
+
+ sensitivize();
+
+ if (modmade) {
+ DocumentUndo::done(_desktop->getDocument(), _("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->removeListenerByData(this);
+ Inkscape::GC::release(_repr);
+ _repr = nullptr;
+ }
+ }
+ }
+}
+
+/**
+ * \param selection should not be NULL.
+ */
+void
+RectToolbar::selection_changed(Inkscape::Selection *selection)
+{
+ int n_selected = 0;
+ Inkscape::XML::Node *repr = nullptr;
+ SPItem *item = nullptr;
+
+ if (_repr) { // remove old listener
+ _item = nullptr;
+ _repr->removeListenerByData(this);
+ Inkscape::GC::release(_repr);
+ _repr = nullptr;
+ }
+
+ auto itemlist= selection->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ if (SP_IS_RECT(*i)) {
+ n_selected++;
+ item = *i;
+ repr = item->getRepr();
+ }
+ }
+
+ _single = false;
+
+ if (n_selected == 0) {
+ _mode_item->set_markup(_("<b>New:</b>"));
+ _width_item->set_sensitive(false);
+ _height_item->set_sensitive(false);
+ } else if (n_selected == 1) {
+ _mode_item->set_markup(_("<b>Change:</b>"));
+ _single = true;
+ _width_item->set_sensitive(true);
+ _height_item->set_sensitive(true);
+
+ if (repr) {
+ _repr = repr;
+ _item = item;
+ Inkscape::GC::anchor(_repr);
+ _repr->addListener(&rect_tb_repr_events, this);
+ _repr->synthesizeEvents(&rect_tb_repr_events, this);
+ }
+ } else {
+ // FIXME: implement averaging of all parameters for multiple selected
+ //gtk_label_set_markup(GTK_LABEL(l), _("<b>Average:</b>"));
+ _mode_item->set_markup(_("<b>Change:</b>"));
+ sensitivize();
+ }
+}
+
+void RectToolbar::event_attr_changed(Inkscape::XML::Node * /*repr*/, gchar const * /*name*/,
+ gchar const * /*old_value*/, gchar const * /*new_value*/,
+ bool /*is_interactive*/, gpointer data)
+{
+ auto toolbar = reinterpret_cast<RectToolbar*>(data);
+
+ // quit if run by the _changed callbacks
+ if (toolbar->_freeze) {
+ return;
+ }
+
+ // in turn, prevent callbacks from responding
+ toolbar->_freeze = true;
+
+ Unit const *unit = toolbar->_tracker->getActiveUnit();
+ g_return_if_fail(unit != nullptr);
+
+ if (toolbar->_item && SP_IS_RECT(toolbar->_item)) {
+ {
+ gdouble rx = SP_RECT(toolbar->_item)->getVisibleRx();
+ toolbar->_rx_adj->set_value(Quantity::convert(rx, "px", unit));
+ }
+
+ {
+ gdouble ry = SP_RECT(toolbar->_item)->getVisibleRy();
+ toolbar->_ry_adj->set_value(Quantity::convert(ry, "px", unit));
+ }
+
+ {
+ gdouble width = SP_RECT(toolbar->_item)->getVisibleWidth();
+ toolbar->_width_adj->set_value(Quantity::convert(width, "px", unit));
+ }
+
+ {
+ gdouble height = SP_RECT(toolbar->_item)->getVisibleHeight();
+ toolbar->_height_adj->set_value(Quantity::convert(height, "px", unit));
+ }
+ }
+
+ toolbar->sensitivize();
+ toolbar->_freeze = false;
+}
+
+}
+}
+}
+
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 :
diff --git a/src/ui/toolbar/rect-toolbar.h b/src/ui/toolbar/rect-toolbar.h
new file mode 100644
index 0000000..4937673
--- /dev/null
+++ b/src/ui/toolbar/rect-toolbar.h
@@ -0,0 +1,115 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_RECT_TOOLBAR_H
+#define SEEN_RECT_TOOLBAR_H
+
+/**
+ * @file
+ * Rect aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbar.h"
+
+#include <gtkmm/adjustment.h>
+
+class SPDesktop;
+class SPItem;
+class SPRect;
+
+namespace Gtk {
+class Toolbutton;
+}
+
+namespace Inkscape {
+class Selection;
+
+namespace XML {
+class Node;
+}
+
+namespace UI {
+namespace Tools {
+class ToolBase;
+}
+
+namespace Widget {
+class LabelToolItem;
+class SpinButtonToolItem;
+class UnitTracker;
+}
+
+namespace Toolbar {
+class RectToolbar : public Toolbar {
+private:
+ UI::Widget::UnitTracker *_tracker;
+
+ XML::Node *_repr;
+ SPItem *_item;
+
+ UI::Widget::LabelToolItem *_mode_item;
+ UI::Widget::SpinButtonToolItem *_width_item;
+ UI::Widget::SpinButtonToolItem *_height_item;
+ UI::Widget::SpinButtonToolItem *_rx_item;
+ UI::Widget::SpinButtonToolItem *_ry_item;
+ Gtk::ToolButton *_not_rounded;
+
+ Glib::RefPtr<Gtk::Adjustment> _width_adj;
+ Glib::RefPtr<Gtk::Adjustment> _height_adj;
+ Glib::RefPtr<Gtk::Adjustment> _rx_adj;
+ Glib::RefPtr<Gtk::Adjustment> _ry_adj;
+
+ bool _freeze;
+ bool _single;
+
+ void value_changed(Glib::RefPtr<Gtk::Adjustment>& adj,
+ gchar const *value_name,
+ void (SPRect::*setter)(gdouble));
+
+ void sensitivize();
+ void defaults();
+ void watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec);
+ void selection_changed(Inkscape::Selection *selection);
+
+ sigc::connection _changed;
+
+protected:
+ RectToolbar(SPDesktop *desktop);
+ ~RectToolbar() override;
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+
+ static void event_attr_changed(Inkscape::XML::Node *repr,
+ gchar const *name,
+ gchar const *old_value,
+ gchar const *new_value,
+ bool is_interactive,
+ gpointer data);
+
+};
+
+}
+}
+}
+
+#endif /* !SEEN_RECT_TOOLBAR_H */
diff --git a/src/ui/toolbar/select-toolbar.cpp b/src/ui/toolbar/select-toolbar.cpp
new file mode 100644
index 0000000..528b885
--- /dev/null
+++ b/src/ui/toolbar/select-toolbar.cpp
@@ -0,0 +1,631 @@
+// 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 "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;
+
+ SPDesktop *desktop = _desktop;
+ Inkscape::Selection *selection = desktop->getSelection();
+ SPDocument *document = desktop->getDocument();
+
+ 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);
+
+ 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);
+
+ 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) {
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ 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);
+
+ 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);
+ _selection_seq++;
+ }
+}
+
+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 + std::to_string(_selection_seq);
+ 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..3d8a2d6
--- /dev/null
+++ b/src/ui/toolbar/select-toolbar.h
@@ -0,0 +1,94 @@
+// 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::uint64_t _selection_seq = 0; ///< Increment to prevent coalescing of consecutive undo events
+ 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..73ea79e
--- /dev/null
+++ b/src/ui/toolbar/spiral-toolbar.cpp
@@ -0,0 +1,295 @@
+// 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"
+
+#include "xml/node-event-vector.h"
+
+using Inkscape::DocumentUndo;
+
+static Inkscape::XML::NodeEventVector spiral_tb_repr_events = {
+ nullptr, /* child_added */
+ nullptr, /* child_removed */
+ Inkscape::UI::Toolbar::SpiralToolbar::event_attr_changed,
+ nullptr, /* content_changed */
+ nullptr /* order_changed */
+};
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+SpiralToolbar::SpiralToolbar(SPDesktop *desktop) :
+ Toolbar(desktop),
+ _freeze(false),
+ _repr(nullptr)
+{
+ auto prefs = Inkscape::Preferences::get();
+
+ {
+ _mode_item = Gtk::manage(new UI::Widget::LabelToolItem(_("<b>New:</b>")));
+ _mode_item->set_use_markup(true);
+ add(*_mode_item);
+ }
+
+ /* Revolution */
+ {
+ std::vector<Glib::ustring> labels = {_("just a curve"), "", _("one full revolution"), "", "", "", "", "", "", ""};
+ std::vector<double> values = { 0.01, 0.5, 1, 2, 3, 5, 10, 20, 50, 100};
+ auto revolution_val = prefs->getDouble("/tools/shapes/spiral/revolution", 3.0);
+ _revolution_adj = Gtk::Adjustment::create(revolution_val, 0.01, 1024.0, 0.1, 1.0);
+ _revolution_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("spiral-revolutions", _("Turns:"), _revolution_adj, 1, 2));
+ _revolution_item->set_tooltip_text(_("Number of revolutions"));
+ _revolution_item->set_custom_numeric_menu_data(values, labels);
+ _revolution_item->set_focus_widget(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->removeListenerByData(this);
+ GC::release(_repr);
+ _repr = nullptr;
+ }
+
+ if(_connection) {
+ _connection->disconnect();
+ }
+}
+
+GtkWidget *
+SpiralToolbar::create(SPDesktop *desktop)
+{
+ auto toolbar = new SpiralToolbar(desktop);
+ return GTK_WIDGET(toolbar->gobj());
+}
+
+void
+SpiralToolbar::value_changed(Glib::RefPtr<Gtk::Adjustment> &adj,
+ Glib::ustring const &value_name)
+{
+ if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble("/tools/shapes/spiral/" + value_name,
+ adj->get_value());
+ }
+
+ // quit if run by the attr_changed listener
+ if (_freeze) {
+ return;
+ }
+
+ // in turn, prevent listener from responding
+ _freeze = true;
+
+ gchar* namespaced_name = g_strconcat("sodipodi:", value_name.data(), nullptr);
+
+ bool modmade = false;
+ auto itemlist= _desktop->getSelection()->items();
+ for(auto i=itemlist.begin();i!=itemlist.end(); ++i){
+ SPItem *item = *i;
+ if (SP_IS_SPIRAL(item)) {
+ Inkscape::XML::Node *repr = item->getRepr();
+ 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->removeListenerByData(this);
+ GC::release(_repr);
+ _repr = nullptr;
+ }
+
+ auto itemlist= selection->items();
+ for(auto i=itemlist.begin();i!=itemlist.end(); ++i){
+ SPItem *item = *i;
+ if (SP_IS_SPIRAL(item)) {
+ n_selected++;
+ repr = item->getRepr();
+ }
+ }
+
+ if (n_selected == 0) {
+ _mode_item->set_markup(_("<b>New:</b>"));
+ } else if (n_selected == 1) {
+ _mode_item->set_markup(_("<b>Change:</b>"));
+
+ if (repr) {
+ _repr = repr;
+ Inkscape::GC::anchor(_repr);
+ _repr->addListener(&spiral_tb_repr_events, this);
+ _repr->synthesizeEvents(&spiral_tb_repr_events, this);
+ }
+ } else {
+ // FIXME: implement averaging of all parameters for multiple selected
+ //gtk_label_set_markup(GTK_LABEL(l), _("<b>Average:</b>"));
+ _mode_item->set_markup(_("<b>Change:</b>"));
+ }
+}
+
+void
+SpiralToolbar::event_attr_changed(Inkscape::XML::Node *repr,
+ gchar const * /*name*/,
+ gchar const * /*old_value*/,
+ gchar const * /*new_value*/,
+ bool /*is_interactive*/,
+ gpointer data)
+{
+ auto toolbar = reinterpret_cast<SpiralToolbar *>(data);
+
+ // quit if run by the _changed callbacks
+ if (toolbar->_freeze) {
+ return;
+ }
+
+ // in turn, prevent callbacks from responding
+ toolbar->_freeze = true;
+
+ double revolution = repr->getAttributeDouble("sodipodi:revolution", 3.0);
+ toolbar->_revolution_adj->set_value(revolution);
+
+ double expansion = repr->getAttributeDouble("sodipodi:expansion", 1.0);
+ toolbar->_expansion_adj->set_value(expansion);
+
+ double t0 = repr->getAttributeDouble("sodipodi:t0", 0.0);
+ toolbar->_t0_adj->set_value(t0);
+
+ toolbar->_freeze = false;
+}
+
+}
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/toolbar/spiral-toolbar.h b/src/ui/toolbar/spiral-toolbar.h
new file mode 100644
index 0000000..9c27eb5
--- /dev/null
+++ b/src/ui/toolbar/spiral-toolbar.h
@@ -0,0 +1,98 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_SPIRAL_TOOLBAR_H
+#define SEEN_SPIRAL_TOOLBAR_H
+
+/**
+ * @file
+ * Spiral aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbar.h"
+
+#include <gtkmm/adjustment.h>
+
+class SPDesktop;
+
+namespace Gtk {
+class ToolButton;
+}
+
+namespace Inkscape {
+class Selection;
+
+namespace XML {
+class Node;
+}
+
+namespace UI {
+namespace Widget {
+class LabelToolItem;
+class SpinButtonToolItem;
+}
+
+namespace Toolbar {
+class SpiralToolbar : public Toolbar {
+private:
+ UI::Widget::LabelToolItem *_mode_item;
+
+ UI::Widget::SpinButtonToolItem *_revolution_item;
+ UI::Widget::SpinButtonToolItem *_expansion_item;
+ UI::Widget::SpinButtonToolItem *_t0_item;
+
+ Gtk::ToolButton *_reset_item;
+
+ Glib::RefPtr<Gtk::Adjustment> _revolution_adj;
+ Glib::RefPtr<Gtk::Adjustment> _expansion_adj;
+ Glib::RefPtr<Gtk::Adjustment> _t0_adj;
+
+ bool _freeze;
+
+ XML::Node *_repr;
+
+ void value_changed(Glib::RefPtr<Gtk::Adjustment> &adj,
+ Glib::ustring const &value_name);
+ void defaults();
+ void selection_changed(Inkscape::Selection *selection);
+
+ std::unique_ptr<sigc::connection> _connection;
+
+protected:
+ SpiralToolbar(SPDesktop *desktop);
+ ~SpiralToolbar() override;
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+
+ static void event_attr_changed(Inkscape::XML::Node *repr,
+ gchar const *name,
+ gchar const *old_value,
+ gchar const *new_value,
+ bool is_interactive,
+ gpointer data);
+};
+}
+}
+}
+
+#endif /* !SEEN_SPIRAL_TOOLBAR_H */
diff --git a/src/ui/toolbar/spray-toolbar.cpp b/src/ui/toolbar/spray-toolbar.cpp
new file mode 100644
index 0000000..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..6fc15b6
--- /dev/null
+++ b/src/ui/toolbar/star-toolbar.cpp
@@ -0,0 +1,569 @@
+// 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"
+
+#include "xml/node-event-vector.h"
+
+using Inkscape::DocumentUndo;
+
+static Inkscape::XML::NodeEventVector star_tb_repr_events =
+{
+ nullptr, /* child_added */
+ nullptr, /* child_removed */
+ Inkscape::UI::Toolbar::StarToolbar::event_attr_changed,
+ nullptr, /* content_changed */
+ nullptr /* order_changed */
+};
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+StarToolbar::StarToolbar(SPDesktop *desktop) :
+ Toolbar(desktop),
+ _mode_item(Gtk::manage(new UI::Widget::LabelToolItem(_("<b>New:</b>")))),
+ _repr(nullptr),
+ _freeze(false)
+{
+ _mode_item->set_use_markup(true);
+ add(*_mode_item);
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool isFlatSided = prefs->getBool("/tools/shapes/star/isflatsided", false);
+
+ /* Flatsided checkbox */
+ {
+ Gtk::RadioToolButton::Group flat_item_group;
+
+ auto flat_polygon_button = Gtk::manage(new Gtk::RadioToolButton(flat_item_group, _("Polygon")));
+ flat_polygon_button->set_tooltip_text(_("Regular polygon (with one handle) instead of a star"));
+ flat_polygon_button->set_icon_name(INKSCAPE_ICON("draw-polygon"));
+ _flat_item_buttons.push_back(flat_polygon_button);
+
+ auto flat_star_button = Gtk::manage(new Gtk::RadioToolButton(flat_item_group, _("Star")));
+ flat_star_button->set_tooltip_text(_("Star instead of a regular polygon (with one handle)"));
+ flat_star_button->set_icon_name(INKSCAPE_ICON("draw-star"));
+ _flat_item_buttons.push_back(flat_star_button);
+
+ _flat_item_buttons[ isFlatSided ? 0 : 1 ]->set_active();
+
+ int btn_index = 0;
+
+ for (auto btn : _flat_item_buttons)
+ {
+ add(*btn);
+ btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &StarToolbar::side_mode_changed), btn_index++));
+ }
+ }
+
+ add(*Gtk::manage(new Gtk::SeparatorToolItem()));
+
+ /* Magnitude */
+ {
+ std::vector<Glib::ustring> labels = {"",
+ _("triangle/tri-star"),
+ _("square/quad-star"),
+ _("pentagon/five-pointed star"),
+ _("hexagon/six-pointed star"),
+ "",
+ "",
+ "",
+ "",
+ ""};
+ std::vector<double> values = {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->removeListenerByData(this);
+ Inkscape::GC::release(_repr);
+ _repr = nullptr;
+ }
+}
+
+GtkWidget *
+StarToolbar::create(SPDesktop *desktop)
+{
+ auto toolbar = new StarToolbar(desktop);
+ return GTK_WIDGET(toolbar->gobj());
+}
+
+void
+StarToolbar::side_mode_changed(int mode)
+{
+ bool flat = (mode == 0);
+
+ if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool( "/tools/shapes/star/isflatsided", flat );
+ }
+
+ // quit if run by the attr_changed listener
+ if (_freeze) {
+ return;
+ }
+
+ // in turn, prevent listener from responding
+ _freeze = true;
+
+ Inkscape::Selection *selection = _desktop->getSelection();
+ bool modmade = false;
+
+ if (_spoke_item) {
+ _spoke_item->set_visible(!flat);
+ }
+
+ auto itemlist= selection->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ SPItem *item = *i;
+ if (SP_IS_STAR(item)) {
+ Inkscape::XML::Node *repr = item->getRepr();
+ 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 (SP_IS_STAR(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 (SP_IS_STAR(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 (SP_IS_STAR(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 (SP_IS_STAR(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->removeListenerByData(this);
+ Inkscape::GC::release(_repr);
+ _repr = nullptr;
+ }
+
+ auto itemlist= selection->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ SPItem *item = *i;
+ if (SP_IS_STAR(item)) {
+ n_selected++;
+ repr = item->getRepr();
+ }
+ }
+
+ if (n_selected == 0) {
+ _mode_item->set_markup(_("<b>New:</b>"));
+ } else if (n_selected == 1) {
+ _mode_item->set_markup(_("<b>Change:</b>"));
+
+ if (repr) {
+ _repr = repr;
+ Inkscape::GC::anchor(_repr);
+ _repr->addListener(&star_tb_repr_events, this);
+ _repr->synthesizeEvents(&star_tb_repr_events, this);
+ }
+ } else {
+ // FIXME: implement averaging of all parameters for multiple selected stars
+ //gtk_label_set_markup(GTK_LABEL(l), _("<b>Average:</b>"));
+ //gtk_label_set_markup(GTK_LABEL(l), _("<b>Change:</b>"));
+ }
+}
+
+void
+StarToolbar::event_attr_changed(Inkscape::XML::Node *repr, gchar const *name,
+ gchar const * /*old_value*/, gchar const * /*new_value*/,
+ bool /*is_interactive*/, gpointer dataPointer)
+{
+ auto toolbar = reinterpret_cast<StarToolbar *>(dataPointer);
+
+ // quit if run by the _changed callbacks
+ if (toolbar->_freeze) {
+ return;
+ }
+
+ // in turn, prevent callbacks from responding
+ toolbar->_freeze = true;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool isFlatSided = prefs->getBool("/tools/shapes/star/isflatsided", false);
+
+ if (!strcmp(name, "inkscape:randomized")) {
+ double randomized = repr->getAttributeDouble("inkscape:randomized", 0.0);
+ toolbar->_randomization_adj->set_value(randomized);
+ } else if (!strcmp(name, "inkscape:rounded")) {
+ double rounded = repr->getAttributeDouble("inkscape:rounded", 0.0);
+ toolbar->_roundedness_adj->set_value(rounded);
+ } else if (!strcmp(name, "inkscape:flatsided")) {
+ char const *flatsides = repr->attribute("inkscape:flatsided");
+ if ( flatsides && !strcmp(flatsides,"false") ) {
+ toolbar->_flat_item_buttons[1]->set_active();
+ toolbar->_spoke_item->set_visible(true);
+ toolbar->_magnitude_adj->set_lower(2);
+ } else {
+ toolbar->_flat_item_buttons[0]->set_active();
+ toolbar->_spoke_item->set_visible(false);
+ toolbar->_magnitude_adj->set_lower(3);
+ }
+ } else if ((!strcmp(name, "sodipodi:r1") || !strcmp(name, "sodipodi:r2")) && (!isFlatSided) ) {
+ gdouble r1 = repr->getAttributeDouble("sodipodi:r1", 1.0);
+ gdouble r2 = repr->getAttributeDouble("sodipodi:r2", 1.0);
+
+
+ if (r2 < r1) {
+ toolbar->_spoke_adj->set_value(r2/r1);
+ } else {
+ toolbar->_spoke_adj->set_value(r1/r2);
+ }
+ } else if (!strcmp(name, "sodipodi:sides")) {
+ int sides = repr->getAttributeInt("sodipodi:sides", 0);
+ toolbar->_magnitude_adj->set_value(sides);
+ }
+
+ toolbar->_freeze = false;
+}
+
+}
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 :
diff --git a/src/ui/toolbar/star-toolbar.h b/src/ui/toolbar/star-toolbar.h
new file mode 100644
index 0000000..c44caab
--- /dev/null
+++ b/src/ui/toolbar/star-toolbar.h
@@ -0,0 +1,108 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_STAR_TOOLBAR_H
+#define SEEN_STAR_TOOLBAR_H
+
+/**
+ * @file
+ * Star aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbar.h"
+
+#include <gtkmm/adjustment.h>
+
+class SPDesktop;
+
+namespace Gtk {
+class RadioToolButton;
+class ToolButton;
+}
+
+namespace Inkscape {
+class Selection;
+
+namespace XML {
+class Node;
+}
+
+namespace UI {
+namespace Tools {
+class ToolBase;
+}
+
+namespace Widget {
+class LabelToolItem;
+class SpinButtonToolItem;
+}
+
+namespace Toolbar {
+class StarToolbar : public Toolbar {
+private:
+ UI::Widget::LabelToolItem *_mode_item;
+ std::vector<Gtk::RadioToolButton *> _flat_item_buttons;
+ UI::Widget::SpinButtonToolItem *_magnitude_item;
+ UI::Widget::SpinButtonToolItem *_spoke_item;
+ UI::Widget::SpinButtonToolItem *_roundedness_item;
+ UI::Widget::SpinButtonToolItem *_randomization_item;
+ Gtk::ToolButton *_reset_item;
+
+ XML::Node *_repr;
+
+ Glib::RefPtr<Gtk::Adjustment> _magnitude_adj;
+ Glib::RefPtr<Gtk::Adjustment> _spoke_adj;
+ Glib::RefPtr<Gtk::Adjustment> _roundedness_adj;
+ Glib::RefPtr<Gtk::Adjustment> _randomization_adj;
+
+ bool _freeze;
+ sigc::connection _changed;
+
+ void side_mode_changed(int mode);
+ void magnitude_value_changed();
+ void proportion_value_changed();
+ void rounded_value_changed();
+ void randomized_value_changed();
+ void defaults();
+ void watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec);
+ void selection_changed(Inkscape::Selection *selection);
+
+protected:
+ StarToolbar(SPDesktop *desktop);
+ ~StarToolbar() override;
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+
+ static void event_attr_changed(Inkscape::XML::Node *repr,
+ gchar const *name,
+ gchar const *old_value,
+ gchar const *new_value,
+ bool is_interactive,
+ gpointer dataPointer);
+};
+
+}
+}
+}
+
+#endif /* !SEEN_SELECT_TOOLBAR_H */
diff --git a/src/ui/toolbar/text-toolbar.cpp b/src/ui/toolbar/text-toolbar.cpp
new file mode 100644
index 0000000..7a6144e
--- /dev/null
+++ b/src/ui/toolbar/text-toolbar.cpp
@@ -0,0 +1,2576 @@
+// 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/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 "widgets/style-utils.h"
+
+using Inkscape::DocumentUndo;
+using Inkscape::Util::Unit;
+using Inkscape::Util::Quantity;
+using Inkscape::Util::unit_table;
+using Inkscape::UI::Widget::UnitTracker;
+
+//#define DEBUG_TEXT
+
+//########################
+//## Text Toolbox ##
+//########################
+
+// Functions for debugging:
+#ifdef DEBUG_TEXT
+static void sp_print_font(SPStyle *query)
+{
+
+
+ bool family_set = query->font_family.set;
+ bool style_set = query->font_style.set;
+ bool fontspec_set = query->font_specification.set;
+
+ std::cout << " Family set? " << family_set
+ << " Style set? " << style_set
+ << " FontSpec set? " << fontspec_set
+ << std::endl;
+}
+
+static void sp_print_fontweight( SPStyle *query ) {
+ const gchar* names[] = {"100", "200", "300", "400", "500", "600", "700", "800", "900",
+ "NORMAL", "BOLD", "LIGHTER", "BOLDER", "Out of range"};
+ // Missing book = 380
+ int index = query->font_weight.computed;
+ if (index < 0 || index > 13)
+ index = 13;
+ std::cout << " Weight: " << names[ index ]
+ << " (" << query->font_weight.computed << ")" << std::endl;
+}
+
+static void sp_print_fontstyle( SPStyle *query ) {
+
+ const gchar* names[] = {"NORMAL", "ITALIC", "OBLIQUE", "Out of range"};
+ int index = query->font_style.computed;
+ if( index < 0 || index > 3 ) index = 3;
+ std::cout << " Style: " << names[ index ] << std::endl;
+
+}
+#endif
+
+static bool is_relative( Unit const *unit ) {
+ return (unit->abbr == "" || unit->abbr == "em" || unit->abbr == "ex" || unit->abbr == "%");
+}
+
+static bool is_relative(SPCSSUnit const unit)
+{
+ return (unit == SP_CSS_UNIT_NONE || unit == SP_CSS_UNIT_EM || unit == SP_CSS_UNIT_EX ||
+ unit == SP_CSS_UNIT_PERCENT);
+}
+
+// Set property for object, but unset all descendents
+// Should probably be moved to desktop_style.cpp
+static void recursively_set_properties(SPObject *object, SPCSSAttr *css, bool unset_descendents = true)
+{
+ object->changeCSS (css, "style");
+
+ SPCSSAttr *css_unset = sp_repr_css_attr_unset_all( css );
+ std::vector<SPObject *> children = object->childList(false);
+ for (auto i: children) {
+ recursively_set_properties(i, unset_descendents ? css_unset : css);
+ }
+ sp_repr_css_attr_unref (css_unset);
+}
+
+/*
+ * Set the default list of font sizes, scaled to the users preferred unit
+ */
+static void sp_text_set_sizes(GtkListStore* model_size, int unit)
+{
+ gtk_list_store_clear(model_size);
+
+ // List of font sizes for dropchange-down menu
+ int sizes[] = {
+ 4, 6, 8, 9, 10, 11, 12, 13, 14, 16, 18, 20, 22, 24, 28,
+ 32, 36, 40, 48, 56, 64, 72, 144
+ };
+
+ // Array must be same length as SPCSSUnit in style.h
+ float ratios[] = {1, 1, 1, 10, 4, 40, 100, 16, 8, 0.16};
+
+ for(int i : sizes) {
+ GtkTreeIter iter;
+ Glib::ustring size = Glib::ustring::format(i / (float)ratios[unit]);
+ gtk_list_store_append( model_size, &iter );
+ gtk_list_store_set( model_size, &iter, 0, size.c_str(), -1 );
+ }
+}
+
+
+// TODO: possibly share with font-selector by moving most code to font-lister (passing family name)
+static void sp_text_toolbox_select_cb( GtkEntry* entry, GtkEntryIconPosition /*position*/, GdkEvent /*event*/, gpointer /*data*/ ) {
+
+ Glib::ustring family = gtk_entry_get_text ( entry );
+ //std::cout << "text_toolbox_missing_font_cb: selecting: " << family << std::endl;
+
+ // Get all items with matching font-family set (not inherited!).
+ std::vector<SPItem*> selectList;
+
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ SPDocument *document = desktop->getDocument();
+ std::vector<SPItem*> x,y;
+ std::vector<SPItem*> allList = get_all_items(x, document->getRoot(), desktop, false, false, true, y);
+ for(std::vector<SPItem*>::const_reverse_iterator i=allList.rbegin();i!=allList.rend(); ++i){
+ SPItem *item = *i;
+ SPStyle *style = item->style;
+
+ if (style) {
+
+ Glib::ustring family_style;
+ if (style->font_family.set) {
+ family_style = style->font_family.value();
+ //std::cout << " family style from font_family: " << family_style << std::endl;
+ }
+ else if (style->font_specification.set) {
+ family_style = style->font_specification.value();
+ //std::cout << " family style from font_spec: " << family_style << std::endl;
+ }
+
+ if (family_style.compare( family ) == 0 ) {
+ //std::cout << " found: " << item->getId() << std::endl;
+ selectList.push_back(item);
+ }
+ }
+ }
+
+ // Update selection
+ Inkscape::Selection *selection = desktop->getSelection();
+ selection->clear();
+ //std::cout << " list length: " << g_slist_length ( selectList ) << std::endl;
+ selection->setList(selectList);
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Toolbar {
+
+TextToolbar::TextToolbar(SPDesktop *desktop)
+ : Toolbar(desktop)
+ , _freeze(false)
+ , _text_style_from_prefs(false)
+ , _outer(true)
+ , _updating(false)
+ , _tracker(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR))
+ , _tracker_fs(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR))
+ , _cusor_numbers(0)
+{
+ /* Line height unit tracker */
+ _tracker->prependUnit(unit_table.getUnit("")); // Ratio
+ _tracker->addUnit(unit_table.getUnit("%"));
+ _tracker->addUnit(unit_table.getUnit("em"));
+ _tracker->addUnit(unit_table.getUnit("ex"));
+ _tracker->setActiveUnit(unit_table.getUnit(""));
+ // We change only the display value
+ _tracker->changeLabel("lines", 0, true);
+ _tracker_fs->setActiveUnit(unit_table.getUnit("mm"));
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ /* Font family */
+ {
+ // Font list
+ Inkscape::FontLister* fontlister = Inkscape::FontLister::get_instance();
+ fontlister->update_font_list(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( sigc::mem_fun(*this, &TextToolbar::fontfamily_value_changed) );
+ add(*_font_family_item);
+
+ // Change style of drop-down from menu to list
+ auto css_provider = gtk_css_provider_new();
+ gtk_css_provider_load_from_data(css_provider,
+ "#TextFontFamilyAction_combobox {\n"
+ " -GtkComboBox-appears-as-list: true;\n"
+ "}\n",
+ -1, nullptr);
+
+ auto screen = gdk_screen_get_default();
+ _font_family_item->focus_on_click(false);
+ gtk_style_context_add_provider_for_screen(screen,
+ GTK_STYLE_PROVIDER(css_provider),
+ GTK_STYLE_PROVIDER_PRIORITY_USER);
+ }
+
+ /* Font styles */
+ {
+ Inkscape::FontLister* fontlister = Inkscape::FontLister::get_instance();
+ Glib::RefPtr<Gtk::ListStore> store = fontlister->get_style_list();
+ GtkListStore* model_style = store->gobj();
+
+ _font_style_item =
+ Gtk::manage(new UI::Widget::ComboBoxEntryToolItem( "TextFontStyleAction",
+ _("Font Style"),
+ _("Font style"),
+ GTK_TREE_MODEL(model_style),
+ 12, // Width in characters
+ 0, // Extra list width
+ nullptr, // Cell layout
+ nullptr, // Separator
+ GTK_WIDGET(desktop->getCanvas()->gobj()))); // Focus widget
+
+ _font_style_item->signal_changed().connect(sigc::mem_fun(*this, &TextToolbar::fontstyle_value_changed));
+ _font_style_item->focus_on_click(false);
+ add(*_font_style_item);
+ }
+
+ add_separator();
+
+ /* Font size */
+ {
+ // List of font sizes for drop-down menu
+ GtkListStore* model_size = gtk_list_store_new( 1, G_TYPE_STRING );
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT);
+
+ sp_text_set_sizes(model_size, unit);
+
+ auto unit_str = sp_style_get_css_unit_string(unit);
+ Glib::ustring tooltip = Glib::ustring::format(_("Font size"), " (", unit_str, ")");
+
+ _font_size_item =
+ Gtk::manage(new UI::Widget::ComboBoxEntryToolItem( "TextFontSizeAction",
+ _("Font Size"),
+ tooltip,
+ GTK_TREE_MODEL(model_size),
+ 8, // Width in characters
+ 0, // Extra list width
+ nullptr, // Cell layout
+ nullptr, // Separator
+ GTK_WIDGET(desktop->getCanvas()->gobj()))); // Focus widget
+
+ _font_size_item->signal_changed().connect(sigc::mem_fun(*this, &TextToolbar::fontsize_value_changed));
+ _font_size_item->focus_on_click(false);
+ add(*_font_size_item);
+ }
+ /* Font_ size units */
+ {
+ _font_size_units_item = _tracker_fs->create_tool_item(_("Units"), (""));
+ _font_size_units_item->signal_changed_after().connect(
+ sigc::mem_fun(*this, &TextToolbar::fontsize_unit_changed));
+ _font_size_units_item->focus_on_click(false);
+ add(*_font_size_units_item);
+ }
+ {
+ // Drop down menu
+ std::vector<Glib::ustring> labels = {_("Smaller spacing"), "", "", "", "", C_("Text tool", "Normal"), "", "", "", "", "", _("Larger spacing")};
+ std::vector<double> values = { 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 2.0};
+
+ auto line_height_val = 1.25;
+ _line_height_adj = Gtk::Adjustment::create(line_height_val, 0.0, 1000.0, 0.1, 1.0);
+ _line_height_item =
+ Gtk::manage(new UI::Widget::SpinButtonToolItem("text-line-height", "", _line_height_adj, 0.1, 2));
+ _line_height_item->set_tooltip_text(_("Spacing between baselines"));
+ _line_height_item->set_custom_numeric_menu_data(values, labels);
+ _line_height_item->set_focus_widget(desktop->getCanvas());
+ _line_height_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TextToolbar::lineheight_value_changed));
+ //_tracker->addAdjustment(_line_height_adj->gobj()); // (Alex V) Why is this commented out?
+ _line_height_item->set_sensitive(true);
+ _line_height_item->set_icon(INKSCAPE_ICON("text_line_spacing"));
+ add(*_line_height_item);
+ }
+ /* Line height units */
+ {
+ _line_height_units_item = _tracker->create_tool_item( _("Units"), (""));
+ _line_height_units_item->signal_changed_after().connect(sigc::mem_fun(*this, &TextToolbar::lineheight_unit_changed));
+ _line_height_units_item->focus_on_click(false);
+ add(*_line_height_units_item);
+ }
+
+ /* 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 i : desktop->getSelection()->items()) {
+ SPItem *item = dynamic_cast<SPItem *>(i);
+ if (dynamic_cast<SPText *>(item) || dynamic_cast<SPFlowtext *>(item)) {
+ // Scale by inverse of accumulated parent transform
+ SPCSSAttr *css_set = sp_repr_css_attr_new();
+ sp_repr_css_merge(css_set, css);
+ Geom::Affine const local(item->i2doc_affine());
+ double const ex(local.descrim());
+ if ((ex != 0.0) && (ex != 1.0)) {
+ sp_css_attr_scale(css_set, 1 / ex);
+ }
+ recursively_set_properties(item, css_set);
+ sp_repr_css_attr_unref(css_set);
+ }
+ }
+ } else {
+ // Apply css to selected inner objects.
+ sp_desktop_set_style (desktop, css, true, false);
+ }
+}
+
+void
+TextToolbar::fontfamily_value_changed()
+{
+#ifdef DEBUG_TEXT
+ std::cout << std::endl;
+ std::cout << "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" << std::endl;
+ std::cout << "sp_text_fontfamily_value_changed: " << std::endl;
+#endif
+
+ // quit if run by the _changed callbacks
+ if (_freeze) {
+#ifdef DEBUG_TEXT
+ std::cout << "sp_text_fontfamily_value_changed: frozen... return" << std::endl;
+ std::cout << "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n" << std::endl;
+#endif
+ return;
+ }
+ _freeze = true;
+
+ 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 );
+
+ SPDesktop *desktop = _desktop;
+ if( desktop->getSelection()->isEmpty() ) {
+ // Update default
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->mergeStyle("/tools/text/style", css);
+ } else {
+ // If there is a selection, update
+ sp_desktop_set_style (desktop, css, true, true); // Results in selection change called twice.
+ DocumentUndo::done(desktop->getDocument(), _("Text: Change font family"), INKSCAPE_ICON("draw-text"));
+ }
+ sp_repr_css_attr_unref (css);
+ }
+
+ // unfreeze
+ _freeze = false;
+
+#ifdef DEBUG_TEXT
+ std::cout << "sp_text_toolbox_fontfamily_changes: exit" << std::endl;
+ std::cout << "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" << std::endl;
+ std::cout << std::endl;
+#endif
+}
+
+GtkWidget *
+TextToolbar::create(SPDesktop *desktop)
+{
+ auto tb = Gtk::manage(new TextToolbar(desktop));
+ return GTK_WIDGET(tb->gobj());
+}
+
+void
+TextToolbar::fontsize_value_changed()
+{
+ // quit if run by the _changed callbacks
+ if (_freeze) {
+ return;
+ }
+ _freeze = true;
+
+ 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 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)
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->mergeStyle("/tools/text/style", css);
+ } else {
+ // Save for undo
+ sp_desktop_set_style (_desktop, css, true, true);
+ 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 no selected objects, set default.
+ SPStyle query(_desktop->getDocument());
+ int result_style =
+ sp_desktop_query_style (desktop, &query, QUERY_STYLE_PROPERTY_FONTSTYLE);
+ if (result_style == QUERY_STYLE_NOTHING) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->mergeStyle("/tools/text/style", css);
+ } else {
+ // Save for undo
+ DocumentUndo::done(desktop->getDocument(), _("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) {
+ SPText *text = dynamic_cast<SPText *>(i);
+ // SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i);
+ if (text) {
+ SPItem *item = i;
+
+ unsigned writing_mode = item->style->writing_mode.value;
+ // below, variable names suggest horizontal move, but we check the writing direction
+ // and move in the corresponding axis
+ Geom::Dim2 axis;
+ if (writing_mode == SP_CSS_WRITING_MODE_LR_TB || writing_mode == SP_CSS_WRITING_MODE_RL_TB) {
+ axis = Geom::X;
+ } else {
+ axis = Geom::Y;
+ }
+
+ Geom::OptRect bbox = item->geometricBounds();
+ if (!bbox)
+ continue;
+ double width = bbox->dimensions()[axis];
+ // If you want to align within some frame, other than the text's own bbox, calculate
+ // the left and right (or top and bottom for tb text) slacks of the text inside that
+ // frame (currently unused)
+ double left_slack = 0;
+ double right_slack = 0;
+ unsigned old_align = item->style->text_align.value;
+ double move = 0;
+ if (old_align == SP_CSS_TEXT_ALIGN_START || old_align == SP_CSS_TEXT_ALIGN_LEFT) {
+ switch (mode) {
+ case 0:
+ move = -left_slack;
+ break;
+ case 1:
+ move = width/2 + (right_slack - left_slack)/2;
+ break;
+ case 2:
+ move = width + right_slack;
+ break;
+ }
+ } else if (old_align == SP_CSS_TEXT_ALIGN_CENTER) {
+ switch (mode) {
+ case 0:
+ move = -width/2 - left_slack;
+ break;
+ case 1:
+ move = (right_slack - left_slack)/2;
+ break;
+ case 2:
+ move = width/2 + right_slack;
+ break;
+ }
+ } else if (old_align == SP_CSS_TEXT_ALIGN_END || old_align == SP_CSS_TEXT_ALIGN_RIGHT) {
+ switch (mode) {
+ case 0:
+ move = -width - left_slack;
+ break;
+ case 1:
+ move = -width/2 + (right_slack - left_slack)/2;
+ break;
+ case 2:
+ move = right_slack;
+ break;
+ }
+ }
+ Geom::Point XY = SP_TEXT(item)->attributes.firstXY();
+ if (axis == Geom::X) {
+ XY = XY + Geom::Point (move, 0);
+ } else {
+ XY = XY + Geom::Point (0, move);
+ }
+ SP_TEXT(item)->attributes.setFirstXY(XY);
+ item->updateRepr();
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ }
+ }
+
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ switch (mode)
+ {
+ case 0:
+ {
+ sp_repr_css_set_property (css, "text-anchor", "start");
+ sp_repr_css_set_property (css, "text-align", "start");
+ break;
+ }
+ case 1:
+ {
+ sp_repr_css_set_property (css, "text-anchor", "middle");
+ sp_repr_css_set_property (css, "text-align", "center");
+ break;
+ }
+
+ case 2:
+ {
+ sp_repr_css_set_property (css, "text-anchor", "end");
+ sp_repr_css_set_property (css, "text-align", "end");
+ break;
+ }
+
+ case 3:
+ {
+ sp_repr_css_set_property (css, "text-anchor", "start");
+ sp_repr_css_set_property (css, "text-align", "justify");
+ break;
+ }
+ }
+
+ SPStyle query(_desktop->getDocument());
+ int result_numbers =
+ sp_desktop_query_style (_desktop, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS);
+
+ // If querying returned nothing, update default style.
+ if (result_numbers == QUERY_STYLE_NOTHING)
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->mergeStyle("/tools/text/style", css);
+ }
+
+ sp_desktop_set_style (desktop, css, true, true);
+ if (result_numbers != QUERY_STYLE_NOTHING)
+ {
+ DocumentUndo::done(_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;
+ }
+ }
+
+ SPStyle query(_desktop->getDocument());
+ int result_numbers =
+ sp_desktop_query_style (_desktop, &query, QUERY_STYLE_PROPERTY_WRITINGMODES);
+
+ // If querying returned nothing, update default style.
+ if (result_numbers == QUERY_STYLE_NOTHING)
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->mergeStyle("/tools/text/style", css);
+ }
+
+ sp_desktop_set_style (_desktop, css, true, true);
+ if(result_numbers != QUERY_STYLE_NOTHING)
+ {
+ 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;
+ }
+ }
+
+ SPStyle query(_desktop->getDocument());
+ int result_numbers =
+ sp_desktop_query_style (_desktop, &query, QUERY_STYLE_PROPERTY_WRITINGMODES);
+
+ // If querying returned nothing, update default style.
+ if (result_numbers == QUERY_STYLE_NOTHING)
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->mergeStyle("/tools/text/style", css);
+ }
+
+ sp_desktop_set_style (_desktop, css, true, true);
+ if(result_numbers != QUERY_STYLE_NOTHING)
+ {
+ 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;
+ }
+ }
+
+ SPStyle query(_desktop->getDocument());
+ int result_numbers =
+ sp_desktop_query_style (_desktop, &query, QUERY_STYLE_PROPERTY_WRITINGMODES);
+
+ // If querying returned nothing, update default style.
+ if (result_numbers == QUERY_STYLE_NOTHING)
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->mergeStyle("/tools/text/style", css);
+ }
+
+ sp_desktop_set_style (_desktop, css, true, true);
+ if(result_numbers != QUERY_STYLE_NOTHING)
+ {
+ 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 {
+ SPItem *parent = dynamic_cast<SPItem *>(*itemlist.begin());
+ SPStyle *parent_style = parent->style;
+ SPCSSAttr *parent_cssatr = sp_css_attr_from_style(parent_style, SP_STYLE_FLAG_IFSET);
+ Glib::ustring parent_lineheight = sp_repr_css_property(parent_cssatr, "line-height", "1.25");
+ SPCSSAttr *cssfit = sp_repr_css_attr_new();
+ sp_repr_css_set_property(cssfit, "line-height", parent_lineheight.c_str());
+ double minheight = 0;
+ if (parent_style) {
+ minheight = parent_style->line_height.computed;
+ }
+ if (minheight) {
+ for (auto i : parent->childList(false)) {
+ SPItem *child = dynamic_cast<SPItem *>(i);
+ if (!child) {
+ continue;
+ }
+ recursively_set_properties(child, cssfit);
+ }
+ }
+ sp_repr_css_set_property(cssfit, "line-height", "0");
+ parent->changeCSS(cssfit, "style");
+ subselection_wrap_toggle(true);
+ sp_desktop_set_style(desktop, css, true, true);
+ subselection_wrap_toggle(false);
+ sp_repr_css_attr_unref(cssfit);
+ }
+ // Only need to save for undo if a text item has been changed.
+ itemlist = selection->items();
+ bool modmade = false;
+ for (auto i : itemlist) {
+ SPText *text = dynamic_cast<SPText *>(i);
+ SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i);
+ if (text || flowtext) {
+ modmade = true;
+ break;
+ }
+ }
+
+ // Save for undo
+ if (modmade) {
+ // Call ensureUpToDate() causes rebuild of text layout (with all proper style
+ // cascading, etc.). For multi-line text with sodipodi::role="line", we must explicitly
+ // save new <tspan> 'x' and 'y' attribute values by calling updateRepr().
+ // Partial fix for bug #1590141.
+
+ desktop->getDocument()->ensureUpToDate();
+ for (auto i : itemlist) {
+ SPText *text = dynamic_cast<SPText *>(i);
+ SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i);
+ if (text || flowtext) {
+ (i)->updateRepr();
+ }
+ }
+ if (!_outer) {
+ prepare_inner();
+ }
+ DocumentUndo::maybeDone(desktop->getDocument(), "ttb:line-height", _("Text: Change line-height"), INKSCAPE_ICON("draw-text"));
+ }
+
+ // 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)
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->mergeStyle("/tools/text/style", css);
+ }
+
+ sp_repr_css_attr_unref (css);
+
+ _freeze = false;
+}
+
+void
+TextToolbar::lineheight_unit_changed(int /* Not Used */)
+{
+ // quit if run by the _changed callbacks or is not text tool
+ if (_freeze || !SP_IS_TEXT_CONTEXT(_desktop->event_context)) {
+ return;
+ }
+ _freeze = true;
+
+ // Get old saved unit
+ int old_unit = _lineheight_unit;
+
+ // Get user selected unit and save as preference
+ Unit const *unit = _tracker->getActiveUnit();
+ g_return_if_fail(unit != nullptr);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ // This nonsense is to get SP_CSS_UNIT_xx value corresponding to unit.
+ SPILength temp_length;
+ Inkscape::CSSOStringStream temp_stream;
+ temp_stream << 1 << unit->abbr;
+ temp_length.read(temp_stream.str().c_str());
+ prefs->setInt("/tools/text/lineheight/display_unit", temp_length.unit);
+ if (old_unit == temp_length.unit) {
+ _freeze = false;
+ return;
+ } 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;
+ bool has_flow = false;
+
+ for (auto i : itemlist) {
+ SPText *text = dynamic_cast<SPText *>(i);
+ SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i);
+ if (text || flowtext) {
+ doc_scale = Geom::Affine(i->i2dt_affine()).descrim();
+ font_size += i->style->font_size.computed * doc_scale;
+ ++count;
+ }
+ if (flowtext) {
+ has_flow = true;
+ }
+ }
+ if (count > 0) {
+ font_size /= count;
+ } else {
+ // 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 : dynamic_cast<SPItem *>(*itemlist.begin());
+ 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 (dynamic_cast<SPText *>(*i) || dynamic_cast<SPFlowtext *>(*i)) {
+ SPItem *item = *i;
+ // Scale by inverse of accumulated parent transform
+ SPCSSAttr *css_set = sp_repr_css_attr_new();
+ sp_repr_css_merge(css_set, css);
+ Geom::Affine const local(item->i2doc_affine());
+ double const ex(local.descrim());
+ if ((ex != 0.0) && (ex != 1.0)) {
+ sp_css_attr_scale(css_set, 1 / ex);
+ }
+ recursively_set_properties(item, css_set);
+ sp_repr_css_attr_unref(css_set);
+ }
+ }
+ } else {
+ 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)) {
+ SPItem *child = dynamic_cast<SPItem *>(i);
+ if (!child) {
+ continue;
+ }
+ recursively_set_properties(child, cssfit);
+ }
+ }
+ sp_repr_css_set_property(cssfit, "line-height", "0");
+ parent->changeCSS(cssfit, "style");
+ subselection_wrap_toggle(true);
+ sp_desktop_set_style(desktop, css, true, true);
+ subselection_wrap_toggle(false);
+ sp_repr_css_attr_unref(cssfit);
+ }
+ itemlist= selection->items();
+ // Only need to save for undo if a text item has been changed.
+ bool modmade = false;
+ for (auto i : itemlist) {
+ SPText *text = dynamic_cast<SPText *>(i);
+ SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i);
+ if (text || flowtext) {
+ modmade = true;
+ break;
+ }
+ }
+ // Save for undo
+ if(modmade) {
+ // Call ensureUpToDate() causes rebuild of text layout (with all proper style
+ // cascading, etc.). For multi-line text with sodipodi::role="line", we must explicitly
+ // save new <tspan> 'x' and 'y' attribute values by calling updateRepr().
+ // Partial fix for bug #1590141.
+
+ desktop->getDocument()->ensureUpToDate();
+ for (auto i : itemlist) {
+ SPText *text = dynamic_cast<SPText *>(i);
+ SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i);
+ if (text || flowtext) {
+ (i)->updateRepr();
+ }
+ }
+ if (_outer) {
+ prepare_inner();
+ }
+ DocumentUndo::maybeDone(_desktop->getDocument(), "ttb:line-height", _("Text: Change line-height unit"), INKSCAPE_ICON("draw-text"));
+ }
+
+ // 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)
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->mergeStyle("/tools/text/style", css);
+ }
+
+ sp_repr_css_attr_unref (css);
+
+ _freeze = false;
+}
+
+void TextToolbar::fontsize_unit_changed(int /* Not Used */)
+{
+ // quit if run by the _changed callbacks
+ Unit const *unit = _tracker_fs->getActiveUnit();
+ g_return_if_fail(unit != nullptr);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ // This nonsense is to get SP_CSS_UNIT_xx value corresponding to unit.
+ SPILength temp_size;
+ Inkscape::CSSOStringStream temp_size_stream;
+ temp_size_stream << 1 << unit->abbr;
+ temp_size.read(temp_size_stream.str().c_str());
+ prefs->setInt("/options/font/unitType", temp_size.unit);
+ selection_changed(_desktop->selection);
+}
+
+void
+TextToolbar::wordspacing_value_changed()
+{
+ // quit if run by the _changed callbacks
+ if (_freeze) {
+ return;
+ }
+ _freeze = true;
+
+ // At the moment this handles only numerical values (i.e. no em unit).
+ // Set css word-spacing
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ Inkscape::CSSOStringStream osfs;
+ osfs << _word_spacing_adj->get_value() << "px"; // For now always use px
+ sp_repr_css_set_property (css, "word-spacing", osfs.str().c_str());
+ text_outer_set_style(css);
+
+ // If no selected objects, set default.
+ SPStyle query(_desktop->getDocument());
+ int result_numbers =
+ sp_desktop_query_style (_desktop, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS);
+ if (result_numbers == QUERY_STYLE_NOTHING)
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->mergeStyle("/tools/text/style", css);
+ } else {
+ // Save for undo
+ 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 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)
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->mergeStyle("/tools/text/style", css);
+ }
+ else
+ {
+ // Save for undo
+ 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) {
+ SPText *text = dynamic_cast<SPText *>(i);
+ SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i);
+ if (text || flowtext) {
+ to_work.push_back(i);
+ }
+ if (flowtext ||
+ (text && text->style && text->style->shape_inside.set)) {
+ isFlow = true;
+ }
+ }
+ bool outside = false;
+ if (selection && to_work.size() == 0) {
+ outside = true;
+ }
+
+ Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance();
+ fontlister->selection_update();
+ // Update font list, but only if widget already created.
+ if (_font_family_item->get_combobox() != nullptr) {
+ _font_family_item->set_active_text(fontlister->get_font_family().c_str(), fontlister->get_font_family_row());
+ _font_style_item->set_active_text(fontlister->get_font_style().c_str());
+ }
+
+ /*
+ * Query from current selection:
+ * Font family (font-family)
+ * Style (font-weight, font-style, font-stretch, font-variant, font-align)
+ * Numbers (font-size, letter-spacing, word-spacing, line-height, text-anchor, writing-mode)
+ * Font specification (Inkscape private attribute)
+ */
+ SPStyle query(document);
+ SPStyle query_fallback(document);
+ int result_family = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_FONTFAMILY);
+ int result_style = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_FONTSTYLE);
+ int result_baseline = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_BASELINES);
+ int result_wmode = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_WRITINGMODES);
+
+ // Calling sp_desktop_query_style will result in a call to TextTool::_styleQueried().
+ // This returns the style of the selected text inside the <text> element... which
+ // is often the style of one or more <tspan>s. If we want the style of the outer
+ // <text> objects then we need to bypass the call to TextTool::_styleQueried().
+ // The desktop selection never includes the elements inside the <text> element.
+ int result_numbers = 0;
+ int result_numbers_fallback = 0;
+ if (!outside) {
+ if (_outer && this->_sub_active_item) {
+ std::vector<SPItem *> qactive{ this->_sub_active_item };
+ SPItem *parent = dynamic_cast<SPItem *>(this->_sub_active_item->parent);
+ std::vector<SPItem *> qparent{ parent };
+ result_numbers =
+ sp_desktop_query_style_from_list(qactive, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS);
+ result_numbers_fallback =
+ sp_desktop_query_style_from_list(qparent, &query_fallback, QUERY_STYLE_PROPERTY_FONTNUMBERS);
+ } else if (_outer) {
+ result_numbers = sp_desktop_query_style_from_list(to_work, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS);
+ } else {
+ result_numbers = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS);
+ }
+ } else {
+ result_numbers =
+ sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS);
+ }
+
+ /*
+ * If no text in selection (querying returned nothing), read the style from
+ * the /tools/text preferences (default style for new texts). Return if
+ * tool bar already set to these preferences.
+ */
+ if (result_family == QUERY_STYLE_NOTHING ||
+ result_style == QUERY_STYLE_NOTHING ||
+ result_numbers == QUERY_STYLE_NOTHING ||
+ result_wmode == QUERY_STYLE_NOTHING ) {
+ // There are no texts in selection, read from preferences.
+ query.readFromPrefs("/tools/text");
+#ifdef DEBUG_TEXT
+ std::cout << " read style from prefs:" << std::endl;
+ sp_print_font( &query );
+#endif
+ if (_text_style_from_prefs) {
+ // Do not reset the toolbar style from prefs if we already did it last time
+ _freeze = false;
+#ifdef DEBUG_TEXT
+ std::cout << " text_style_from_prefs: toolbar already set" << std:: endl;
+ std::cout << "sp_text_toolbox_selection_changed: exit " << count << std::endl;
+ std::cout << "&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&" << std::endl;
+ std::cout << std::endl;
+#endif
+ return;
+ }
+
+ // To ensure the value of the combobox is properly set on start-up, only mark
+ // the prefs set if the combobox has already been constructed.
+ if( _font_family_item->get_combobox() != nullptr ) {
+ _text_style_from_prefs = true;
+ }
+ } else {
+ _text_style_from_prefs = false;
+ }
+
+ // If we have valid query data for text (font-family, font-specification) set toolbar accordingly.
+ {
+ // Size (average of text selected)
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT);
+ double size = 0;
+ if (!size && _cusor_numbers != QUERY_STYLE_NOTHING) {
+ size = sp_style_css_size_px_to_units(_query_cursor.font_size.computed, unit);
+ }
+ if (!size && result_numbers != QUERY_STYLE_NOTHING) {
+ size = sp_style_css_size_px_to_units(query.font_size.computed, unit);
+ }
+ if (!size && result_numbers_fallback != QUERY_STYLE_NOTHING) {
+ size = sp_style_css_size_px_to_units(query_fallback.font_size.computed, unit);
+ }
+ 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;
+ }
+ SPDocument *doc = _desktop->getDocument();
+ SPObject *spobject = dynamic_cast<SPObject *>(tc->text);
+ SPItem *spitem = dynamic_cast<SPItem *>(tc->text);
+ SPText *text = dynamic_cast<SPText *>(tc->text);
+ SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(tc->text);
+ Inkscape::XML::Document *xml_doc = doc->getReprDoc();
+ if (!spobject) {
+ return;
+ }
+
+ // We check for external files with text nodes direct children of text 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) {
+ SPString *spstring = dynamic_cast<SPString *>(child);
+ if (spstring) {
+ Glib::ustring content = spstring->string;
+ if (content != "\n") {
+ Inkscape::XML::Node *rstring = xml_doc->createTextNode(content.c_str());
+ Inkscape::XML::Node *rtspan = xml_doc->createElement("svg:tspan");
+ //Inkscape::XML::Node *rnl = xml_doc->createTextNode("\n");
+ rtspan->setAttribute("sodipodi:role", "line");
+ rtspan->addChild(rstring, nullptr);
+ text->getRepr()->addChild(rtspan, child->getRepr());
+ Inkscape::GC::release(rstring);
+ Inkscape::GC::release(rtspan);
+ text->getRepr()->removeChild(spstring->getRepr());
+ 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) {
+ SPString *spstring = dynamic_cast<SPString *>(child);
+ SPFlowtspan *flowtspan = dynamic_cast<SPFlowtspan *>(child);
+ SPTSpan *tspan = dynamic_cast<SPTSpan *>(child);
+ // we need to upper all flowtspans to container level
+ // to 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::cout << "error on TextToolbar.cpp::2433" << std::endl;
+ return repr;
+}
+
+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)) {
+ SPItem *item = dynamic_cast<SPItem *>(child);
+ if (item && counter == startline) {
+ this->_sub_active_item = item;
+ int origin_selection = layout->iteratorToCharIndex(start_selection);
+ Inkscape::Text::Layout::iterator next = layout->charIndexToIterator(origin_selection + 1);
+ Inkscape::Text::Layout::iterator prev = layout->charIndexToIterator(origin_selection - 1);
+ //TODO: find a better way to init
+ _updating = true;
+ SPStyle query(_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..b91166e
--- /dev/null
+++ b/src/ui/toolbar/text-toolbar.h
@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_TEXT_TOOLBAR_H
+#define SEEN_TEXT_TOOLBAR_H
+
+/**
+ * @file
+ * Text aux toolbar
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2011 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "object/sp-item.h"
+#include "object/sp-object.h"
+#include "toolbar.h"
+#include "text-editing.h"
+#include "style.h"
+#include <gtkmm/adjustment.h>
+#include <gtkmm/box.h>
+#include <gtkmm/popover.h>
+#include <gtkmm/separatortoolitem.h>
+#include <sigc++/connection.h>
+
+class SPDesktop;
+
+namespace Gtk {
+class ComboBoxText;
+class ToggleToolButton;
+}
+
+namespace Inkscape {
+class Selection;
+
+namespace UI {
+namespace Tools {
+class ToolBase;
+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;
+
+ UI::Widget::ComboBoxEntryToolItem *_font_family_item;
+ UI::Widget::ComboBoxEntryToolItem *_font_size_item;
+ UI::Widget::ComboToolItem *_font_size_units_item;
+ UI::Widget::ComboBoxEntryToolItem *_font_style_item;
+ UI::Widget::ComboToolItem *_line_height_units_item;
+ UI::Widget::SpinButtonToolItem *_line_height_item;
+ Gtk::ToggleToolButton *_superscript_item;
+ Gtk::ToggleToolButton *_subscript_item;
+
+ UI::Widget::ComboToolItem *_align_item;
+ UI::Widget::ComboToolItem *_writing_mode_item;
+ UI::Widget::ComboToolItem *_orientation_item;
+ UI::Widget::ComboToolItem *_direction_item;
+
+ UI::Widget::SpinButtonToolItem *_word_spacing_item;
+ UI::Widget::SpinButtonToolItem *_letter_spacing_item;
+ UI::Widget::SpinButtonToolItem *_dx_item;
+ UI::Widget::SpinButtonToolItem *_dy_item;
+ UI::Widget::SpinButtonToolItem *_rotation_item;
+
+ Glib::RefPtr<Gtk::Adjustment> _line_height_adj;
+ Glib::RefPtr<Gtk::Adjustment> _word_spacing_adj;
+ Glib::RefPtr<Gtk::Adjustment> _letter_spacing_adj;
+ Glib::RefPtr<Gtk::Adjustment> _dx_adj;
+ Glib::RefPtr<Gtk::Adjustment> _dy_adj;
+ Glib::RefPtr<Gtk::Adjustment> _rotation_adj;
+ bool _outer;
+ SPItem *_sub_active_item;
+ int _lineheight_unit;
+ Inkscape::Text::Layout::iterator wrap_start;
+ Inkscape::Text::Layout::iterator wrap_end;
+ bool _updating;
+ int _cusor_numbers;
+ SPStyle _query_cursor;
+ double selection_fontsize;
+ sigc::connection c_selection_changed;
+ sigc::connection c_selection_modified;
+ sigc::connection c_selection_modified_select_tool;
+ sigc::connection c_subselection_changed;
+ void text_outer_set_style(SPCSSAttr *css);
+ void fontfamily_value_changed();
+ void fontsize_value_changed();
+ void subselection_wrap_toggle(bool start);
+ void fontstyle_value_changed();
+ void script_changed(Gtk::ToggleToolButton *btn);
+ void align_mode_changed(int mode);
+ void writing_mode_changed(int mode);
+ void orientation_changed(int mode);
+ void direction_changed(int mode);
+ void lineheight_value_changed();
+ void lineheight_unit_changed(int not_used);
+ void wordspacing_value_changed();
+ void letterspacing_value_changed();
+ void dx_value_changed();
+ void dy_value_changed();
+ void prepare_inner();
+ void focus_text();
+ void rotation_value_changed();
+ void fontsize_unit_changed(int not_used);
+ void selection_changed(Inkscape::Selection *selection);
+ void selection_modified(Inkscape::Selection *selection, guint flags);
+ void selection_modified_select_tool(Inkscape::Selection *selection, guint flags);
+ void subselection_changed(Inkscape::UI::Tools::TextTool* texttool);
+ void watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec);
+ void set_sizes(int unit);
+ Inkscape::XML::Node *unindent_node(Inkscape::XML::Node *repr, Inkscape::XML::Node *before);
+
+ protected:
+ TextToolbar(SPDesktop *desktop);
+
+public:
+ static GtkWidget * create(SPDesktop *desktop);
+};
+}
+}
+}
+
+#endif /* !SEEN_TEXT_TOOLBAR_H */
diff --git a/src/ui/toolbar/toolbar.cpp b/src/ui/toolbar/toolbar.cpp
new file mode 100644
index 0000000..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..56422cc
--- /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 << " 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..12143bf
--- /dev/null
+++ b/src/ui/tools/arc-tool.cpp
@@ -0,0 +1,455 @@
+// 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"
+#include "xml/node-event-vector.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 (!this->within_tolerance) {
+ // we've been dragging, finish the arc
+ this->finishItem();
+ } else if (this->item_to_select) {
+ // no dragging, select clicked item if any
+ if (event->button.state & GDK_SHIFT_MASK) {
+ selection->toggle(this->item_to_select);
+ } else {
+ selection->set(this->item_to_select);
+ }
+ } else {
+ // click in an empty space
+ selection->clear();
+ }
+
+ this->xp = 0;
+ this->yp = 0;
+ this->item_to_select = nullptr;
+ handled = true;
+ }
+ 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 = SP_GENERICELLIPSE(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/box3d-tool.cpp b/src/ui/tools/box3d-tool.cpp
new file mode 100644
index 0000000..3efe20f
--- /dev/null
+++ b/src/ui/tools/box3d-tool.cpp
@@ -0,0 +1,566 @@
+// 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"
+
+#include "xml/node-event-vector.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) {
+ SPDefs *defs = document->getDefs();
+
+ bool has_persp = false;
+ for (auto& child: defs->children) {
+ if (SP_IS_PERSP3D(&child)) {
+ has_persp = true;
+ break;
+ }
+ }
+
+ if (!has_persp) {
+ document->setCurrentPersp3D(Persp3D::create_xml_element (document));
+ }
+}
+
+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 ( 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->selection->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..17bec3a
--- /dev/null
+++ b/src/ui/tools/box3d-tool.h
@@ -0,0 +1,103 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __SP_BOX3D_CONTEXT_H__
+#define __SP_BOX3D_CONTEXT_H__
+
+/*
+ * 3D box drawing context
+ *
+ * Author:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ *
+ * Copyright (C) 2000 Lauris Kaplinski
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ * Copyright (C) 2002 Lauris Kaplinski
+ * Copyright (C) 2007 Maximilian Albert <Anhalter42@gmx.de>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cstddef>
+
+#include <2geom/point.h>
+#include <sigc++/connection.h>
+
+#include "proj_pt.h"
+#include "vanishing-point.h"
+
+#include "ui/tools/tool-base.h"
+
+class SPItem;
+class SPBox3D;
+
+namespace Box3D {
+ struct VPDrag;
+}
+
+namespace Inkscape {
+ class Selection;
+}
+
+#define SP_BOX3D_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::Box3dTool*>((Inkscape::UI::Tools::ToolBase*)obj))
+#define SP_IS_BOX3D_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::Box3dTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL)
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+class Box3dTool : public ToolBase {
+public:
+ Box3dTool(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..aced95d
--- /dev/null
+++ b/src/ui/tools/calligraphic-tool.cpp
@@ -0,0 +1,1192 @@
+// 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 "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;
+
+ this->accumulated.reset(new SPCurve());
+ this->currentcurve.reset(new SPCurve());
+
+ this->cal1.reset(new SPCurve());
+ this->cal2.reset(new SPCurve());
+
+ currentshape = new Inkscape::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 = new Inkscape::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()
+{
+ if (hatch_area) {
+ delete hatch_area;
+ hatch_area = nullptr;
+ }
+ if (currentshape) {
+ delete currentshape;
+ currentshape = nullptr;
+ }
+}
+
+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();
+
+ // Ensure drawing up-to-date. (Is this really necessary?)
+ drawing->update();
+
+ // Get average color.
+ double R, G, B, A;
+ drawing->average_color(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 */
+ for (auto segment :this->segments) {
+ delete segment;
+ }
+ this->segments.clear();
+
+ /* reset accumulated curve */
+ this->accumulated->reset();
+ this->clear_current();
+
+ if (this->repr) {
+ this->repr = nullptr;
+ }
+}
+
+bool CalligraphicTool::root_handler(GdkEvent* event) {
+ gint ret = FALSE;
+ 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;
+ }
+
+ this->accumulated->reset();
+
+ if (this->repr) {
+ this->repr = nullptr;
+ }
+
+ /* initialize first point */
+ this->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 && (SP_IS_SHAPE(selected) || SP_IS_TEXT(selected))) {
+ // One item selected, and it's a path;
+ // let's try to track it as a guide
+
+ if (selected != this->hatch_item) {
+ this->hatch_item = selected;
+ if (this->hatch_livarot_path)
+ delete this->hatch_livarot_path;
+ this->hatch_livarot_path = Path_for_item (this->hatch_item, true, true);
+ 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(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(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(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(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 */
+ for (auto segment : this->segments) {
+ delete segment;
+ }
+ this->segments.clear();
+
+ /* Create object */
+ this->fit_and_split(true);
+ if (this->accumulate())
+ this->set_to_accumulated(event->button.state & GDK_SHIFT_MASK, event->button.state & GDK_MOD1_MASK); // performs document_done
+ else
+ g_warning ("Failed to create path: invalid data in dc->cal1 or dc->cal2");
+
+ /* reset accumulated curve */
+ this->accumulated->reset();
+
+ this->clear_current();
+ if (this->repr) {
+ this->repr = nullptr;
+ }
+
+ if (!this->hatch_pointer_past.empty()) this->hatch_pointer_past.clear();
+ if (!this->hatch_nearest_past.empty()) this->hatch_nearest_past.clear();
+ if (!this->inertia_vectors.empty()) this->inertia_vectors.clear();
+ if (!this->hatch_vectors.empty()) this->hatch_vectors.clear();
+ this->hatch_last_nearest = Geom::Point(0,0);
+ this->hatch_last_pointer = Geom::Point(0,0);
+ this->hatch_escaped = false;
+ this->hatch_item = nullptr;
+ this->hatch_livarot_path = nullptr;
+ this->just_started_drawing = false;
+
+ if (this->hatch_spacing != 0 && !this->keep_selected) {
+ // we do not select the newly drawn path, so increase spacing by step
+ if (this->hatch_spacing_step == 0) {
+ this->hatch_spacing_step = this->hatch_spacing;
+ }
+ this->hatch_spacing += this->hatch_spacing_step;
+ }
+
+ this->message_context->clear();
+ ret = TRUE;
+ } else if (!this->dragging
+ && event->button.button == 1
+ && 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 */
+ this->currentshape->set_bpath(nullptr);
+
+ /* reset curve */
+ this->currentcurve->reset();
+ this->cal1->reset();
+ this->cal2->reset();
+
+ /* reset points */
+ this->npoints = 0;
+}
+
+void CalligraphicTool::set_to_accumulated(bool unionize, bool subtract) {
+ if (!this->accumulated->is_empty()) {
+ if (!this->repr) {
+ /* Create object */
+ Inkscape::XML::Document *xml_doc = _desktop->doc()->getReprDoc();
+ Inkscape::XML::Node *repr = xml_doc->createElement("svg:path");
+
+ /* Set style */
+ sp_desktop_apply_style_tool(_desktop, repr, "/tools/calligraphic", false);
+
+ this->repr = repr;
+
+ auto layer = currentLayer();
+ SPItem *item=SP_ITEM(layer->appendChildRepr(this->repr));
+ Inkscape::GC::release(this->repr);
+ item->transform = layer->i2doc_affine().inverse();
+ item->updateRepr();
+ }
+
+ Geom::PathVector pathv = this->accumulated->get_pathvector() * _desktop->dt2doc();
+ this->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.
+ SPItem *result = SP_ITEM(_desktop->doc()->getObjectByRepr(this->repr));
+
+ if (result == nullptr) {
+ // The boolean operation succeeded.
+ // Now we fetch the single item, that has been set as selected by the boolean op.
+ // This is its result.
+ result = _desktop->getSelection()->singleItem();
+ }
+ result->doWriteTransform(result->transform, nullptr, true);
+ } else {
+ if (this->repr) {
+ sp_repr_unparent(this->repr);
+ }
+
+ this->repr = nullptr;
+ }
+
+ DocumentUndo::done(_desktop->getDocument(), _("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 (
+ this->cal1->is_empty() ||
+ this->cal2->is_empty() ||
+ (this->cal1->get_segment_count() <= 0) ||
+ this->cal1->first_path()->closed()
+ ) {
+
+ this->cal1->reset();
+ this->cal2->reset();
+
+ return false; // failure
+ }
+
+ auto rev_cal2 = this->cal2->create_reverse();
+
+ if ((rev_cal2->get_segment_count() <= 0) || rev_cal2->first_path()->closed()) {
+ this->cal1->reset();
+ this->cal2->reset();
+
+ return false; // failure
+ }
+
+ Geom::Curve const * dc_cal1_firstseg = this->cal1->first_segment();
+ Geom::Curve const * rev_cal2_firstseg = rev_cal2->first_segment();
+ Geom::Curve const * dc_cal1_lastseg = this->cal1->last_segment();
+ Geom::Curve const * rev_cal2_lastseg = rev_cal2->last_segment();
+
+ this->accumulated->reset(); /* Is this required ?? */
+
+ this->accumulated->append(*cal1);
+
+ add_cap(*accumulated, dc_cal1_lastseg->finalPoint(), rev_cal2_firstseg->initialPoint(), cap_rounding);
+
+ this->accumulated->append(*rev_cal2, true);
+
+ add_cap(*accumulated, rev_cal2_lastseg->finalPoint(), dc_cal1_firstseg->initialPoint(), cap_rounding);
+
+ this->accumulated->closepath();
+
+ this->cal1->reset();
+ this->cal2->reset();
+
+ return true; // success
+}
+
+static double square(double const x)
+{
+ return x * x;
+}
+
+void CalligraphicTool::fit_and_split(bool release) {
+ double const tolerance_sq = square(_desktop->w2d().descrim() * TOLERANCE_CALLIGRAPHIC);
+
+#ifdef DYNA_DRAW_VERBOSE
+ g_print("[F&S:R=%c]", release?'T':'F');
+#endif
+
+ if (!( this->npoints > 0 && this->npoints < SAMPLING_SIZE )) {
+ return; // just clicked
+ }
+
+ if ( this->npoints == SAMPLING_SIZE - 1 || release ) {
+#define BEZIER_SIZE 4
+#define BEZIER_MAX_BEZIERS 8
+#define BEZIER_MAX_LENGTH ( BEZIER_SIZE * BEZIER_MAX_BEZIERS )
+
+#ifdef DYNA_DRAW_VERBOSE
+ g_print("[F&S:#] dc->npoints:%d, release:%s\n",
+ this->npoints, release ? "TRUE" : "FALSE");
+#endif
+
+ /* Current calligraphic */
+ if ( this->cal1->is_empty() || this->cal2->is_empty() ) {
+ /* dc->npoints > 0 */
+ /* g_print("calligraphics(1|2) reset\n"); */
+ this->cal1->reset();
+ this->cal2->reset();
+
+ this->cal1->moveto(this->point1[0]);
+ this->cal2->moveto(this->point2[0]);
+ }
+
+ Geom::Point b1[BEZIER_MAX_LENGTH];
+ gint const nb1 = Geom::bezier_fit_cubic_r(b1, this->point1, this->npoints,
+ tolerance_sq, BEZIER_MAX_BEZIERS);
+ g_assert( nb1 * BEZIER_SIZE <= gint(G_N_ELEMENTS(b1)) );
+
+ Geom::Point b2[BEZIER_MAX_LENGTH];
+ gint const nb2 = Geom::bezier_fit_cubic_r(b2, this->point2, this->npoints,
+ tolerance_sq, BEZIER_MAX_BEZIERS);
+ g_assert( nb2 * BEZIER_SIZE <= gint(G_N_ELEMENTS(b2)) );
+
+ if ( nb1 != -1 && nb2 != -1 ) {
+ /* Fit and draw and reset state */
+#ifdef DYNA_DRAW_VERBOSE
+ g_print("nb1:%d nb2:%d\n", nb1, nb2);
+#endif
+ /* CanvasShape */
+ if (! release) {
+ this->currentcurve->reset();
+ this->currentcurve->moveto(b1[0]);
+ for (Geom::Point *bp1 = b1; bp1 < b1 + BEZIER_SIZE * nb1; bp1 += BEZIER_SIZE) {
+ this->currentcurve->curveto(bp1[1], bp1[2], bp1[3]);
+ }
+ this->currentcurve->lineto(b2[BEZIER_SIZE*(nb2-1) + 3]);
+ for (Geom::Point *bp2 = b2 + BEZIER_SIZE * ( nb2 - 1 ); bp2 >= b2; bp2 -= BEZIER_SIZE) {
+ this->currentcurve->curveto(bp2[2], bp2[1], bp2[0]);
+ }
+ // FIXME: dc->segments is always NULL at this point??
+ if (this->segments.empty()) { // first segment
+ add_cap(*currentcurve, b2[0], b1[0], cap_rounding);
+ }
+ this->currentcurve->closepath();
+ currentshape->set_bpath(currentcurve.get(), true);
+ }
+
+ /* Current calligraphic */
+ for (Geom::Point *bp1 = b1; bp1 < b1 + BEZIER_SIZE * nb1; bp1 += BEZIER_SIZE) {
+ this->cal1->curveto(bp1[1], bp1[2], bp1[3]);
+ }
+ for (Geom::Point *bp2 = b2; bp2 < b2 + BEZIER_SIZE * nb2; bp2 += BEZIER_SIZE) {
+ this->cal2->curveto(bp2[1], bp2[2], bp2[3]);
+ }
+ } else {
+ /* fixme: ??? */
+#ifdef DYNA_DRAW_VERBOSE
+ g_print("[fit_and_split] failed to fit-cubic.\n");
+#endif
+ this->draw_temporary_box();
+
+ for (gint i = 1; i < this->npoints; i++) {
+ this->cal1->lineto(this->point1[i]);
+ }
+ for (gint i = 1; i < this->npoints; i++) {
+ this->cal2->lineto(this->point2[i]);
+ }
+ }
+
+ /* Fit and draw and copy last point */
+#ifdef DYNA_DRAW_VERBOSE
+ g_print("[%d]Yup\n", this->npoints);
+#endif
+ if (!release) {
+ g_assert(!this->currentcurve->is_empty());
+
+ 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(), 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));
+
+ this->segments.push_back(cbp);
+ }
+
+ this->point1[0] = this->point1[this->npoints - 1];
+ this->point2[0] = this->point2[this->npoints - 1];
+ this->npoints = 1;
+ } else {
+ this->draw_temporary_box();
+ }
+}
+
+void CalligraphicTool::draw_temporary_box() {
+ this->currentcurve->reset();
+
+ this->currentcurve->moveto(this->point2[this->npoints-1]);
+
+ for (gint i = this->npoints-2; i >= 0; i--) {
+ this->currentcurve->lineto(this->point2[i]);
+ }
+
+ for (gint i = 0; i < this->npoints; i++) {
+ this->currentcurve->lineto(this->point1[i]);
+ }
+
+ if (this->npoints >= 2) {
+ add_cap(*currentcurve, point1[npoints - 1], point2[npoints - 1], cap_rounding);
+ }
+
+ this->currentcurve->closepath();
+ currentshape->set_bpath(currentcurve.get(), 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..8f43a4f
--- /dev/null
+++ b/src/ui/tools/calligraphic-tool.h
@@ -0,0 +1,100 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SP_DYNA_DRAW_CONTEXT_H_SEEN
+#define SP_DYNA_DRAW_CONTEXT_H_SEEN
+
+/*
+ * Handwriting-like drawing mode
+ *
+ * Authors:
+ * Mitsuru Oka <oka326@parkcity.ne.jp>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ *
+ * The original dynadraw code:
+ * Paul Haeberli <paul@sgi.com>
+ *
+ * Copyright (C) 1998 The Free Software Foundation
+ * Copyright (C) 1999-2002 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <list>
+#include <string>
+
+#include <2geom/point.h>
+
+#include "ui/tools/dynamic-base.h"
+
+class SPItem;
+class Path;
+
+#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;
+ Inkscape::CanvasItemBpath *hatch_area = nullptr;
+ 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..48302bd
--- /dev/null
+++ b/src/ui/tools/connector-tool.cpp
@@ -0,0 +1,1383 @@
+// 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 "svg/svg.h"
+
+#include "xml/node-event-vector.h"
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+static void cc_clear_active_knots(SPKnotList k);
+
+static void shape_event_attr_deleted(Inkscape::XML::Node *repr,
+ Inkscape::XML::Node *child, Inkscape::XML::Node *ref, gpointer data);
+
+static void shape_event_attr_changed(Inkscape::XML::Node *repr, gchar const *name,
+ gchar const *old_value, gchar const *new_value, bool is_interactive,
+ gpointer data);
+
+static void cc_select_handle(SPKnot* knot);
+static void cc_deselect_handle(SPKnot* knot);
+static bool cc_item_is_shape(SPItem *item);
+
+/*static Geom::Point connector_drag_origin_w(0, 0);
+static bool connector_within_tolerance = false;*/
+
+static Inkscape::XML::NodeEventVector shape_repr_events = {
+ nullptr, /* child_added */
+ nullptr, /* child_added */
+ shape_event_attr_changed,
+ nullptr, /* content_changed */
+ nullptr /* order_changed */
+};
+
+static Inkscape::XML::NodeEventVector layer_repr_events = {
+ nullptr, /* child_added */
+ shape_event_attr_deleted,
+ nullptr, /* child_added */
+ nullptr, /* content_changed */
+ nullptr /* order_changed */
+};
+
+ConnectorTool::ConnectorTool(SPDesktop *desktop)
+ : ToolBase(desktop, "/tools/connector", "connector.svg")
+ , selection(nullptr)
+ , npoints(0)
+ , state(SP_CONNECTOR_CONTEXT_IDLE)
+ , red_bpath(nullptr)
+ , red_curve(nullptr)
+ , red_color(0xff00007f)
+ , green_curve(nullptr)
+ , newconn(nullptr)
+ , newConnRef(nullptr)
+ , curvature(0.0)
+ , isOrthogonal(false)
+ , active_shape(nullptr)
+ , active_shape_repr(nullptr)
+ , active_shape_layer_repr(nullptr)
+ , active_conn(nullptr)
+ , active_conn_repr(nullptr)
+ , active_handle(nullptr)
+ , selected_handle(nullptr)
+ , clickeditem(nullptr)
+ , clickedhandle(nullptr)
+ , shref(nullptr)
+ , sub_shref(nullptr)
+ , ehref(nullptr)
+ , sub_ehref(nullptr)
+{
+ 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 */
+ this->red_curve = std::make_unique<SPCurve>();
+
+ /* Create green curve */
+ green_curve = std::make_unique<SPCurve>();
+
+ // Notice the initial selection.
+ //cc_selection_changed(this->selection, (gpointer) this);
+ this->_selectionChanged(this->selection);
+
+ this->within_tolerance = false;
+
+ sp_event_context_read(this, "curvature");
+ sp_event_context_read(this, "orthogonal");
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/tools/connector/selcue", false)) {
+ this->enableSelectionCue();
+ }
+
+ // Make sure we see all enter events for canvas items,
+ // even if a mouse button is depressed.
+ 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) {
+ sp_repr_remove_listener_by_data(this->active_shape_repr, this);
+ Inkscape::GC::release(this->active_shape_repr);
+ this->active_shape_repr = nullptr;
+
+ sp_repr_remove_listener_by_data(this->active_shape_layer_repr, this);
+ Inkscape::GC::release(this->active_shape_layer_repr);
+ this->active_shape_layer_repr = nullptr;
+ }
+
+ cc_clear_active_knots(this->knots);
+}
+
+static void cc_clear_active_knots(SPKnotList k)
+{
+ // Hide the connection points if they exist.
+ if (k.size()) {
+ for (auto & it : k) {
+ it.first->hide();
+ }
+ }
+}
+
+void ConnectorTool::cc_clear_active_conn()
+{
+ if (this->active_conn == nullptr) {
+ return;
+ }
+ g_assert( this->active_conn_repr );
+
+ this->active_conn = nullptr;
+
+ if (this->active_conn_repr) {
+ sp_repr_remove_listener_by_data(this->active_conn_repr, this);
+ Inkscape::GC::release(this->active_conn_repr);
+ this->active_conn_repr = nullptr;
+ }
+
+ // Hide the endpoint handles.
+ for (auto & i : this->endpt_handle) {
+ if (i) {
+ i->hide();
+ }
+ }
+}
+
+
+bool ConnectorTool::_ptHandleTest(Geom::Point& p, gchar **href, 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( SP_IS_PATH(this->clickeditem));
+
+ m.setup(_desktop);
+ m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE);
+ m.unSetup();
+
+ // Update the hidden path
+ Geom::Affine i2d ( (this->clickeditem)->i2dt_affine() );
+ Geom::Affine d2i = i2d.inverse();
+ SPPath *path = SP_PATH(this->clickeditem);
+ SPCurve *curve = path->curve();
+ if (this->clickedhandle == this->endpt_handle[0]) {
+ Geom::Point o = this->endpt_handle[1]->pos;
+ curve->stretch_endpoints(p * d2i, o * d2i);
+ } else {
+ Geom::Point o = this->endpt_handle[0]->pos;
+ curve->stretch_endpoints(o * d2i, p * d2i);
+ }
+ sp_conn_reroute_path_immediate(path);
+
+ // Copy this to the temporary visible path
+ this->red_curve = SPCurve::copy(path->curveForEdit());
+ this->red_curve->transform(i2d);
+
+ red_bpath->set_bpath(red_curve.get());
+
+ 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(SP_PATH(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.
+ recreateCurve(red_curve.get(), this->newConnRef, this->curvature);
+ this->red_curve->transform(_desktop->doc2dt());
+ red_bpath->set_bpath(red_curve.get(), 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_unique<SPCurve>();
+ std::swap(c, green_curve);
+
+ this->red_curve->reset();
+ red_bpath->set_bpath(nullptr);
+
+ if (c->is_empty()) {
+ return;
+ }
+
+ this->_flushWhite(c.get());
+}
+
+
+/*
+ * 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 && !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 = SP_ITEM(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(SP_PATH(this->newconn));
+ this->newconn->updateRepr();
+ }
+
+ this->newconn->doWriteTransform(this->newconn->transform, nullptr, true);
+
+ // Only set the selection after we are finished with creating the attributes of
+ // the connector. Otherwise, the selection change may alter the defaults for
+ // values like curvature in the connector context, preventing subsequent lookup
+ // of their original values.
+ this->selection->set(repr);
+ Inkscape::GC::release(repr);
+ }
+
+ 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 = SPCurve::copy(path->curveForEdit());
+ Geom::Affine i2d = (cc->clickeditem)->i2dt_affine();
+ cc->red_curve->transform(i2d);
+ cc->red_bpath->set_bpath(cc->red_curve.get(), 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) {
+ SPUse *use = dynamic_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) {
+ sp_repr_remove_listener_by_data(this->active_shape_repr, this);
+ Inkscape::GC::release(this->active_shape_repr);
+
+ sp_repr_remove_listener_by_data(this->active_shape_layer_repr, this);
+ Inkscape::GC::release(this->active_shape_layer_repr);
+ }
+
+ // Listen in case the active shape changes
+ this->active_shape_repr = item->getRepr();
+ if (this->active_shape_repr) {
+ Inkscape::GC::anchor(this->active_shape_repr);
+ sp_repr_add_listener(this->active_shape_repr, &shape_repr_events, this);
+
+ this->active_shape_layer_repr = this->active_shape_repr->parent();
+ Inkscape::GC::anchor(this->active_shape_layer_repr);
+ sp_repr_add_listener(this->active_shape_layer_repr, &layer_repr_events, this);
+ }
+
+ cc_clear_active_knots(this->knots);
+
+ // The idea here is to try and add a group's children to solidify
+ // connection handling. We react to path objects with only one node.
+ for (auto& child: item->children) {
+ if(child.getAttribute("inkscape:connector")) {
+ this->_activeShapeAddKnot((SPItem *) &child, nullptr);
+ }
+ }
+ // Special connector points in a symbol
+ SPUse *use = dynamic_cast<SPUse *>(item);
+ if(use) {
+ 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( SP_IS_PATH(item) );
+
+ const SPCurve *curve = SP_PATH(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) {
+ sp_repr_remove_listener_by_data(this->active_conn_repr, this);
+ Inkscape::GC::release(this->active_conn_repr);
+ this->active_conn_repr = nullptr;
+ }
+
+ // Listen in case the active conn changes
+ this->active_conn_repr = item->getRepr();
+ if (this->active_conn_repr) {
+ Inkscape::GC::anchor(this->active_conn_repr);
+ sp_repr_add_listener(this->active_conn_repr, &shape_repr_events, this);
+ }
+
+ for (int i = 0; i < 2; ++i) {
+ // Create the handle if it doesn't exist
+ if ( this->endpt_handle[i] == nullptr ) {
+ SPKnot *knot = new SPKnot(_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 = dynamic_cast<SPPath const *>(item)) {
+ SPCurve const *curve = path->curve();
+ if ( curve && !(curve->is_closed()) ) {
+ // Open paths are connectors.
+ return false;
+ }
+ } else if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item)) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/tools/connector/ignoretext", true)) {
+ // Don't count text as a shape we can connect connector to.
+ return false;
+ }
+ }
+ return true;
+}
+
+
+bool cc_item_is_connector(SPItem *item)
+{
+ if (SP_IS_PATH(item)) {
+ bool closed = SP_PATH(item)->curveForEdit()->is_closed();
+ if (SP_PATH(item)->connEndPair.isAutoRoutingConn() && !closed) {
+ // To be considered a connector, an object must be a non-closed
+ // path that is marked with a "inkscape:connector-type" attribute.
+ return true;
+ }
+ }
+ return false;
+}
+
+
+void cc_selection_set_avoid(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);
+ }
+}
+
+static void shape_event_attr_deleted(Inkscape::XML::Node */*repr*/, Inkscape::XML::Node *child,
+ Inkscape::XML::Node */*ref*/, gpointer data)
+{
+ g_assert(data);
+ ConnectorTool *cc = SP_CONNECTOR_CONTEXT(data);
+
+ if (child == cc->active_shape_repr) {
+ // The active shape has been deleted. Clear active shape.
+ cc->cc_clear_active_shape();
+ }
+}
+
+
+static void shape_event_attr_changed(Inkscape::XML::Node *repr, gchar const *name,
+ gchar const */*old_value*/, gchar const */*new_value*/, bool /*is_interactive*/, gpointer data)
+{
+ g_assert(data);
+ ConnectorTool *cc = SP_CONNECTOR_CONTEXT(data);
+
+ // Look for changes that result in onscreen movement.
+ if (!strcmp(name, "d") || !strcmp(name, "x") || !strcmp(name, "y") ||
+ !strcmp(name, "width") || !strcmp(name, "height") ||
+ !strcmp(name, "transform")) {
+ if (repr == cc->active_shape_repr) {
+ // Active shape has moved. Clear active shape.
+ cc->cc_clear_active_shape();
+ } else if (repr == cc->active_conn_repr) {
+ // The active conn has been moved.
+ // Set it again, which just sets new handle positions.
+ cc->cc_set_active_conn(cc->active_conn);
+ }
+ }
+}
+
+}
+}
+}
+
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/tools/connector-tool.h b/src/ui/tools/connector-tool.h
new file mode 100644
index 0000000..8c35ccb
--- /dev/null
+++ b/src/ui/tools/connector-tool.h
@@ -0,0 +1,164 @@
+// 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 <memory>
+#include <string>
+
+#include <2geom/point.h>
+#include <sigc++/connection.h>
+
+#include "ui/tools/tool-base.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
+};
+
+typedef std::map<SPKnot *, int> SPKnotList;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+class ConnectorTool : public ToolBase {
+public:
+ ConnectorTool(SPDesktop *desktop);
+ ~ConnectorTool() override;
+
+ Inkscape::Selection *selection;
+ Geom::Point p[5];
+
+ /** \invar npoints in {0, 2}. */
+ gint npoints;
+ unsigned int state : 4;
+
+ // Red curve
+ Inkscape::CanvasItemBpath *red_bpath;
+ std::unique_ptr<SPCurve> red_curve;
+ guint32 red_color;
+
+ // Green curve
+ std::unique_ptr<SPCurve> green_curve;
+
+ // The new connector
+ SPItem *newconn;
+ Avoid::ConnRef *newConnRef;
+ gdouble curvature;
+ bool isOrthogonal;
+
+ // The active shape
+ SPItem *active_shape;
+ Inkscape::XML::Node *active_shape_repr;
+ Inkscape::XML::Node *active_shape_layer_repr;
+
+ // Same as above, but for the active connector
+ SPItem *active_conn;
+ Inkscape::XML::Node *active_conn_repr;
+ sigc::connection sel_changed_connection;
+
+ // The activehandle
+ SPKnot *active_handle;
+
+ // The selected handle, used in editing mode
+ SPKnot *selected_handle;
+
+ SPItem *clickeditem;
+ SPKnot *clickedhandle;
+
+ SPKnotList knots;
+ SPKnot *endpt_handle[2]{};
+ sigc::connection endpt_handler_connection[2];
+ gchar *shref;
+ gchar *sub_shref;
+ gchar *ehref;
+ gchar *sub_ehref;
+
+ 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);
+};
+
+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);
+
+}
+}
+}
+
+#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..e7ff4fb
--- /dev/null
+++ b/src/ui/tools/dropper-tool.cpp
@@ -0,0 +1,402 @@
+// 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 = new Inkscape::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();
+
+ if (this->area) {
+ delete this->area;
+ this->area = nullptr;
+ }
+}
+
+/**
+ * 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(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();
+
+ // Ensure drawing up-to-date. (Is this really necessary?)
+ drawing->update();
+
+ // Get average color.
+ double R, G, B, A;
+ drawing->average_color(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..4584ae9
--- /dev/null
+++ b/src/ui/tools/dropper-tool.h
@@ -0,0 +1,93 @@
+// 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 "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
+ Inkscape::CanvasItemBpath* area = nullptr; ///< 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..de0ab07
--- /dev/null
+++ b/src/ui/tools/dynamic-base.cpp
@@ -0,0 +1,155 @@
+// 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)
+ , accumulated(nullptr)
+ , currentshape(nullptr)
+ , currentcurve(nullptr)
+ , cal1(nullptr)
+ , cal2(nullptr)
+ , point1()
+ , point2()
+ , npoints(0)
+ , repr(nullptr)
+ , cur(0, 0)
+ , vel(0, 0)
+ , vel_max(0)
+ , acc(0, 0)
+ , ang(0, 0)
+ , last(0, 0)
+ , del(0, 0)
+ , pressure(DEFAULT_PRESSURE)
+ , xtilt(0)
+ , ytilt(0)
+ , dragging(false)
+ , usepressure(false)
+ , usetilt(false)
+ , mass(0.3)
+ , drag(DRAG_DEFAULT)
+ , angle(30.0)
+ , width(0.2)
+ , vel_thin(0.1)
+ , flatness(0.9)
+ , tremor(0)
+ , cap_rounding(0)
+ , is_drawing(false)
+ , abs_width(false)
+{
+}
+
+DynamicBase::~DynamicBase() {
+ for (auto segment : segments) {
+ delete segment;
+ }
+ segments.clear();
+
+ if (this->currentshape) {
+ delete currentshape;
+ }
+}
+
+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..ce62166
--- /dev/null
+++ b/src/ui/tools/dynamic-base.h
@@ -0,0 +1,134 @@
+// 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 <memory>
+
+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 */
+ std::unique_ptr<SPCurve> accumulated;
+
+ /** canvas items for "committed" segments */
+ std::vector<Inkscape::CanvasItemBpath *> segments;
+
+ /** canvas item for red "leading" segment */
+ Inkscape::CanvasItemBpath *currentshape;
+
+ /** shape of red "leading" segment */
+ std::unique_ptr<SPCurve> currentcurve;
+
+ /** left edge of the stroke; combined to get accumulated */
+ std::unique_ptr<SPCurve> cal1;
+
+ /** right edge of the stroke; combined to get accumulated */
+ std::unique_ptr<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..5b9cbab
--- /dev/null
+++ b/src/ui/tools/eraser-tool.cpp
@@ -0,0 +1,1229 @@
+// 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 "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 {
+
+extern EraserToolMode const DEFAULT_ERASER_MODE = EraserToolMode::CUT;
+
+EraserTool::EraserTool(SPDesktop *desktop)
+ : DynamicBase(desktop, "/tools/eraser", "eraser.svg")
+{
+ accumulated.reset(new SPCurve());
+ currentcurve.reset(new SPCurve());
+
+ cal1.reset(new SPCurve());
+ cal2.reset(new SPCurve());
+
+ currentshape = new Inkscape::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
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/tools/eraser/selcue", false) != 0) {
+ enableSelectionCue();
+ }
+ _updateMode();
+
+ // TODO temp force:
+ enableSelectionCue();
+}
+
+EraserTool::~EraserTool()
+{
+ delete currentshape;
+ currentshape = nullptr;
+}
+
+/** Reads the current Eraser mode from Preferences and sets `mode` accordingly. */
+void EraserTool::_updateMode()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (!prefs) {
+ return;
+ }
+
+ int mode_idx = prefs->getInt("/tools/eraser/mode", 1); // Cut mode is default
+
+ // 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();
+
+ _removeTemporarySegments();
+
+ /* reset accumulated curve */
+ accumulated->reset();
+ _clearCurrent();
+ repr = nullptr;
+}
+
+/** Removes all temporary line segments */
+void EraserTool::_removeTemporarySegments()
+{
+ for (auto segment : segments) {
+ delete segment;
+ }
+ segments.clear();
+}
+
+bool EraserTool::root_handler(GdkEvent* event)
+{
+ bool ret = false;
+ _updateMode();
+ 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);
+ _removeTemporarySegments();
+
+ /* Create object */
+ _fitAndSplit(true);
+ _accumulate();
+ _setToAccumulated(); // performs document_done
+
+ /* 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;
+}
+
+void EraserTool::_clearCurrent()
+{
+ // reset bpath
+ currentshape->set_bpath(nullptr);
+
+ // reset curve
+ currentcurve->reset();
+ cal1->reset();
+ cal2->reset();
+
+ // reset points
+ npoints = 0;
+}
+
+void EraserTool::_setToAccumulated()
+{
+ bool work_done = false;
+ SPDocument *document = _desktop->doc();
+
+ if (!accumulated->is_empty()) {
+ if (!repr) {
+ /* Create object */
+ Inkscape::XML::Document *xml_doc = document->getReprDoc();
+ Inkscape::XML::Node *eraser_repr = xml_doc->createElement("svg:path");
+
+ /* Set style */
+ sp_desktop_apply_style_tool(_desktop, eraser_repr, "/tools/eraser", false);
+
+ repr = eraser_repr;
+ }
+ SPObject *top_layer = _desktop->layerManager().currentRoot();
+ SPItem *item_repr = SP_ITEM(top_layer->appendChildRepr(repr));
+ Inkscape::GC::release(repr);
+ item_repr->updateRepr();
+ Geom::PathVector pathv = accumulated->get_pathvector() * _desktop->dt2doc();
+ pathv *= item_repr->i2doc_affine().inverse();
+ repr->setAttribute("d", sp_svg_write_path(pathv));
+ Geom::OptRect eraser_bbox;
+ if (repr) {
+ bool was_selection = false;
+ Inkscape::Selection *selection = _desktop->getSelection();
+ _updateMode();
+ SPItem *acid = SP_ITEM(document->getObjectByRepr(repr));
+ eraser_bbox = acid->documentVisualBounds();
+ std::vector<SPItem *> remaining_items;
+ std::vector<SPItem *> to_work_on;
+ if (selection->isEmpty()) {
+ if (mode == EraserToolMode::CUT || mode == EraserToolMode::CLIP) {
+ to_work_on = document->getItemsPartiallyInBox(_desktop->dkey, *eraser_bbox,
+ false, false, false, true);
+ } else {
+ Inkscape::Rubberband *r = Inkscape::Rubberband::get(_desktop);
+ to_work_on = document->getItemsAtPoints(_desktop->dkey, r->getPoints());
+ }
+ to_work_on.erase(std::remove(to_work_on.begin(), to_work_on.end(), acid), to_work_on.end());
+ } else {
+ if (mode == EraserToolMode::DELETE) {
+ Inkscape::Rubberband *r = Inkscape::Rubberband::get(_desktop);
+ auto selected_items = selection->items();
+ std::vector<SPItem *> touched = document->getItemsAtPoints(_desktop->dkey, r->getPoints());
+ for (auto i : selected_items) {
+ if (std::find(touched.begin(), touched.end(), i) == touched.end()) {
+ remaining_items.push_back(i);
+ } else {
+ to_work_on.push_back(i);
+ }
+ }
+ } else {
+ to_work_on.insert(to_work_on.end(), selection->items().begin(), selection->items().end());
+ }
+ was_selection = true;
+ }
+
+ if (to_work_on.empty()) {
+ _clearStatusBar();
+ } else {
+ selection->clear();
+ if (mode == EraserToolMode::CUT) {
+ Error status = ALL_GOOD;
+ for (auto item : to_work_on) {
+ Error retval = _cutErase(item, eraser_bbox, remaining_items);
+ if (retval == ALL_GOOD) {
+ work_done = true;
+ } else {
+ status |= retval;
+ }
+ }
+
+ status &= ~(NOT_IN_BOUNDS | NON_EXISTENT); // Clear flags not handled at the moment
+ if (status == ALL_GOOD) {
+ _clearStatusBar();
+ } else { // Something went wrong during the cut operation
+ if (status & RASTER_IMAGE) {
+ _setStatusBarMessage(_("Cannot cut out from a bitmap, use <b>Clip</b> mode "
+ "instead."));
+ } else if (status & ERROR_GROUP) {
+ _setStatusBarMessage(_("Cannot cut out from a group, ungroup the objects "
+ "first."));
+ } else if (status & NO_AREA_PATH) {
+ _setStatusBarMessage(_("Cannot cut out from a path with zero area, use "
+ "<b>Clip</b> mode instead."));
+ }
+ }
+ } else if (mode == EraserToolMode::CLIP) {
+ if (!nowidth) {
+ for (auto item : to_work_on) {
+ _clipErase(item, item_repr->parent, eraser_bbox);
+ }
+ if (was_selection) {
+ remaining_items = to_work_on;
+ }
+ work_done = true;
+ }
+ _clearStatusBar();
+ } else if (mode == EraserToolMode::DELETE) {
+ for (auto item : to_work_on) {
+ item->deleteObject(true);
+ }
+ work_done = true;
+ _clearStatusBar();
+ }
+
+ if (was_selection && !remaining_items.empty()) {
+ selection->add(remaining_items.begin(), remaining_items.end());
+ }
+ }
+ // Remove the eraser stroke itself:
+ sp_repr_unparent(repr);
+ repr = nullptr;
+ }
+ } else if (repr) {
+ sp_repr_unparent(repr);
+ repr = nullptr;
+ }
+
+ if (work_done) {
+ DocumentUndo::done(document, _("Draw eraser stroke"), INKSCAPE_ICON("draw-eraser"));
+ } else {
+ DocumentUndo::cancel(document);
+ }
+}
+
+/**
+ * @brief Erases from a shape by cutting
+ * @param item - the item to be erased
+ * @param eraser_bbox - bounding box of the eraser stroke
+ * @param survivers - items that survived the erase operation will be added to this vector
+ * @return type of error encountered
+ */
+EraserTool::Error EraserTool::_cutErase(SPItem* item, Geom::OptRect const &eraser_bbox,
+ std::vector<SPItem *> &survivers)
+{
+ // If the item cannot be cut, preserve it
+ if (Error error = EraserTool::_uncuttableItemType(item)) {
+ survivers.push_back(item);
+ return error;
+ }
+
+ Geom::OptRect bbox = item->documentVisualBounds();
+ if (!bbox || !bbox->intersects(eraser_bbox)) {
+ survivers.push_back(item);
+ return NOT_IN_BOUNDS;
+ }
+
+ // If the item is a clone, we check if the original is cuttable before unlinking it
+ if (SPUse *use = dynamic_cast<SPUse *>(item)) {
+ int depth = use->cloneDepth();
+ if (depth < 0) {
+ survivers.push_back(item);
+ return NON_EXISTENT;
+ }
+ // We recurse into the chain of uses until we reach the original item
+ SPItem *original_item = item;
+ for (int i = 0; i < depth; ++i) {
+ SPUse *intermediate_clone = dynamic_cast<SPUse *>(original_item);
+ original_item = intermediate_clone->get_original();
+ }
+ if (Error error = EraserTool::_uncuttableItemType(original_item)) {
+ survivers.push_back(item);
+ return error;
+ }
+ item = use->unlink();
+ }
+
+ _booleanErase(item, survivers);
+ return ALL_GOOD;
+}
+
+/** 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 (dynamic_cast<SPGroup *>(item)) {
+ return ERROR_GROUP; // TODO: handle groups in the future
+ } else if (dynamic_cast<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 operation
+ * @param erasee - the item to be erased
+ * @param survivers - items that survived the erase operation will be added to this vector
+ */
+void EraserTool::_booleanErase(SPItem *erasee, std::vector<SPItem *> &survivers) const
+{
+ XML::Document *xml_doc = _desktop->doc()->getReprDoc();
+ XML::Node *duplicate_stroke = repr->duplicate(xml_doc);
+ repr->parent()->appendChild(duplicate_stroke);
+ GC::release(duplicate_stroke); // parent takes over
+ ObjectSet operands(_desktop);
+ operands.set(duplicate_stroke);
+ if (!nowidth) {
+ operands.pathUnion(true, true);
+ }
+ operands.add(erasee);
+ operands.removeLPESRecursive(true);
+
+ _handleStrokeStyle(erasee);
+
+ if (nowidth) {
+ operands.pathCut(true, true);
+ } else {
+ operands.pathDiff(true, true);
+ }
+
+ auto *prefs = Preferences::get();
+ bool break_apart = prefs->getBool("/tools/eraser/break_apart", false);
+ if (!break_apart) {
+ operands.combine(true, true);
+ } else if (!nowidth) {
+ operands.breakApart(true, false, true);
+ }
+ survivers.insert(survivers.end(), operands.items().begin(), operands.items().end());
+}
+
+/** Handles the "evenodd" stroke style */
+void EraserTool::_handleStrokeStyle(SPItem *item) const
+{
+ if (item->style->fill_rule.value == SP_WIND_RULE_EVENODD) {
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_set_property(css, "fill-rule", "evenodd");
+ sp_desktop_set_style(_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, SPObject *parent, Geom::OptRect &eraser_box)
+{
+ 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;
+ if (bbox && bbox->intersects(*eraser_box)) {
+ SPClipPath *clip_path = item->getClipObject();
+ if (clip_path) {
+ std::vector<SPItem *> selected;
+ selected.push_back(SP_ITEM(clip_path->firstChild()));
+ std::vector<Inkscape::XML::Node *> to_select;
+ std::vector<SPItem *> items(selected);
+ sp_item_list_to_curves(items, selected, to_select);
+ Inkscape::XML::Node *clip_data = SP_ITEM(clip_path->firstChild())->getRepr();
+ if (!clip_data && !to_select.empty()) {
+ clip_data = *(to_select.begin());
+ }
+ if (clip_data) {
+ Inkscape::XML::Node *dup_clip = clip_data->duplicate(xml_doc);
+ if (dup_clip) {
+ SPItem *dup_clip_obj = SP_ITEM(parent->appendChildRepr(dup_clip));
+ Inkscape::GC::release(dup_clip);
+ if (dup_clip_obj) {
+ dup_clip_obj->transform *= item->getRelativeTransform(SP_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);
+ SPRect *rect = SP_RECT(parent->appendChildRepr(rect_repr));
+ Inkscape::GC::release(rect_repr);
+ rect->setPosition(bbox->left(), bbox->top(), bbox->width(), bbox->height());
+ rect->transform = SP_ITEM(rect->parent)->i2doc_affine().inverse();
+
+ rect->updateRepr();
+ rect->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ w_selection.raiseToTop(true);
+ w_selection.add(rect);
+ w_selection.pathDiff(true, true);
+ }
+ w_selection.raiseToTop(true);
+ w_selection.add(item);
+ w_selection.setMask(true, false, true);
+ if (delete_old_clip_path) {
+ clip_path->deleteObject(true);
+ }
+ } else {
+ SPItem *erase_clip = w_selection.singleItem();
+ if (erase_clip) {
+ erase_clip->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)
+{
+ SPPath *as_path = dynamic_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->create_reverse();
+
+ 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();
+}
+
+void EraserTool::_fitAndSplit(bool releasing)
+{
+ double const tolerance_sq = square(_desktop->w2d().descrim() * tolerance);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ nowidth = (prefs->getDouble("/tools/eraser/width", 1) == 0);
+
+#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.get(), 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(), 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.push_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.get(), 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..2c9377a
--- /dev/null
+++ b/src/ui/tools/eraser-tool.h
@@ -0,0 +1,132 @@
+// 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"
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+enum class EraserToolMode
+{
+ DELETE,
+ CUT,
+ CLIP
+};
+extern EraserToolMode const DEFAULT_ERASER_MODE;
+
+class EraserTool : public DynamicBase {
+
+private:
+ // non-static data:
+ EraserToolMode mode = DEFAULT_ERASER_MODE;
+ bool nowidth = false;
+ std::vector<MessageId> _our_messages;
+
+ // static data:
+ inline static guint32 const trace_color_rgba = 0xff0000ff; // RGBA red
+ inline static SPWindRule const trace_wind_rule = SP_WIND_RULE_EVENODD;
+
+ inline static double const tolerance = 0.1;
+
+ inline static double const epsilon = 0.5e-6;
+ inline static double const epsilon_start = 0.5e-2;
+ inline static double const vel_start = 1e-5;
+
+ inline static double const drag_default = 1.0;
+ inline static double const drag_min = 0.0;
+ inline static double const drag_max = 1.0;
+
+ inline static double const min_pressure = 0.0;
+ inline static double const max_pressure = 1.0;
+ inline static double const default_pressure = 1.0;
+
+ inline static double const min_tilt = -1.0;
+ inline static double const max_tilt = 1.0;
+ inline static double const default_tilt = 0.0;
+
+public:
+ // public member functions
+ EraserTool(SPDesktop *desktop);
+ ~EraserTool() override;
+ bool root_handler(GdkEvent *event) final;
+
+ using Error = std::uint64_t;
+ inline static Error const ALL_GOOD = 0x0;
+ inline static Error const NOT_IN_BOUNDS = 0x1 << 0;
+ inline static Error const NON_EXISTENT = 0x1 << 1;
+ inline static Error const NO_AREA_PATH = 0x1 << 2;
+ inline static Error const RASTER_IMAGE = 0x1 << 3;
+ inline static Error const ERROR_GROUP = 0x1 << 4;
+
+private:
+ // private member functions
+ void _reset(Geom::Point p);
+ void _extinput(GdkEvent *event);
+ bool _apply(Geom::Point p);
+ void _brush();
+ void _cancel();
+ void _clearCurrent();
+ void _setToAccumulated();
+ void _accumulate();
+ void _fitAndSplit(bool releasing);
+ void _drawTemporaryBox();
+ bool _handleKeypress(GdkEventKey const *key);
+ void _completeBezier(double tolerance_sq, bool releasing);
+ void _failedBezierFallback();
+ void _fitDrawLastPoint();
+ void _clipErase(SPItem *item, SPObject *parent, Geom::OptRect &eraser_box);
+ Error _cutErase(SPItem *item, Geom::OptRect const &eraser_bbox, std::vector<SPItem *> &survivers);
+ void _booleanErase(SPItem *erasee, std::vector<SPItem*> &survivers) const;
+ void _handleStrokeStyle(SPItem *item) const;
+ void _updateMode();
+ void _removeTemporarySegments();
+ void _setStatusBarMessage(char *message);
+ void _clearStatusBar();
+
+ 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);
+};
+
+} // 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..62ef634
--- /dev/null
+++ b/src/ui/tools/flood-tool.cpp
@@ -0,0 +1,1239 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Bucket fill drawing context, works by bitmap filling an area on a rendered version
+ * of the current display and then tracing the result using potrace.
+ */
+/* Author:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * John Bintz <jcoswell@coswellproductions.org>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl>
+ * Copyright (C) 2000-2005 authors
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "flood-tool.h"
+
+#include <cmath>
+#include <queue>
+
+#include <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.h>
+
+#include <2geom/pathvector.h>
+
+#include "color.h"
+#include "context-fns.h"
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "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
+
+#include "xml/node-event-vector.h"
+
+using Inkscape::DocumentUndo;
+
+using Inkscape::Display::ExtractARGB32;
+using Inkscape::Display::ExtractRGB32;
+using Inkscape::Display::AssembleARGB32;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+// 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;
+
+ GrayMap *gray_map = GrayMapCreate((max_x - min_x + 1), (max_y - min_y + 1));
+ if (!gray_map) {
+ desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("Failed mid-operation, no objects created."));
+ return;
+ }
+ unsigned int gray_map_y = 0;
+ for (unsigned int y = min_y; y <= max_y; y++) {
+ unsigned long *gray_map_t = gray_map->rows[gray_map_y];
+
+ trace_t = get_trace_pixel(trace_px, min_x, y, bci.width);
+ for (unsigned int x = min_x; x <= max_x; x++) {
+ *gray_map_t = is_pixel_colored(trace_t) ? GRAYMAP_BLACK : GRAYMAP_WHITE;
+ gray_map_t++;
+ trace_t++;
+ }
+ gray_map_y++;
+ }
+
+ Inkscape::Trace::Potrace::PotraceTracingEngine pte;
+ pte.keepGoing = 1;
+ std::vector<Inkscape::Trace::TracingEngineResult> results = pte.traceGrayMap(gray_map);
+ gray_map->destroy(gray_map);
+
+ //XML Tree being used here directly while it shouldn't be...."
+ Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc();
+
+ long totalNodeCount = 0L;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double offset = prefs->getDouble("/tools/paintbucket/offset", 0.0);
+
+ for (auto result : results) {
+ totalNodeCount += result.getNodeCount();
+
+ Inkscape::XML::Node *pathRepr = xml_doc->createElement("svg:path");
+ /* Set style */
+ sp_desktop_apply_style_tool (desktop, pathRepr, "/tools/paintbucket", false);
+
+ Geom::PathVector pathv = sp_svg_read_pathv(result.getPathData().c_str());
+ Path *path = new Path;
+ path->LoadPathVector(pathv);
+
+ if (offset != 0) {
+
+ Shape *path_shape = new Shape();
+
+ path->ConvertWithBackData(0.03);
+ path->Fill(path_shape, 0);
+ delete path;
+
+ Shape *expanded_path_shape = new Shape();
+
+ expanded_path_shape->ConvertToShape(path_shape, fill_nonZero);
+ path_shape->MakeOffset(expanded_path_shape, offset * desktop->current_zoom(), join_round, 4);
+ expanded_path_shape->ConvertToShape(path_shape, fill_positive);
+
+ Path *expanded_path = new Path();
+
+ expanded_path->Reset();
+ expanded_path_shape->ConvertToForme(expanded_path);
+ expanded_path->ConvertEvenLines(1.0);
+ expanded_path->Simplify(1.0);
+
+ delete path_shape;
+ delete expanded_path_shape;
+
+ gchar *str = expanded_path->svg_dump_path();
+ if (str && *str) {
+ pathRepr->setAttribute("d", str);
+ g_free(str);
+ } else {
+ desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("<b>Too much inset</b>, the result is empty."));
+ Inkscape::GC::release(pathRepr);
+ g_free(str);
+ return;
+ }
+
+ delete expanded_path;
+
+ } else {
+ gchar *str = path->svg_dump_path();
+ delete path;
+ pathRepr->setAttribute("d", str);
+ g_free(str);
+ }
+
+ auto layer = desktop->layerManager().currentLayer();
+ layer->addChild(pathRepr, nullptr);
+
+ SPObject *reprobj = document->getObjectByRepr(pathRepr);
+ if (reprobj) {
+ SP_ITEM(reprobj)->doWriteTransform(transform);
+
+ // premultiply the item transform by the accumulated parent transform in the paste layer
+ Geom::Affine local (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.",
+ SP_PATH(reprobj)->nodesInPath()), SP_PATH(reprobj)->nodesInPath() );
+ selection->add(reprobj);
+ selection->pathUnion(true);
+ } else {
+ desktop->messageStack()->flashF( Inkscape::WARNING_MESSAGE,
+ ngettext("Area filled, path with <b>%d</b> node created.","Area filled, path with <b>%d</b> nodes created.",
+ SP_PATH(reprobj)->nodesInPath()), SP_PATH(reprobj)->nodesInPath() );
+ selection->set(reprobj);
+ }
+
+ }
+
+ Inkscape::GC::release(pathRepr);
+
+ }
+}
+
+/**
+ * The possible return states of perform_bitmap_scanline_check().
+ */
+enum ScanlineCheckResult {
+ SCANLINE_CHECK_OK,
+ SCANLINE_CHECK_ABORTED,
+ SCANLINE_CHECK_BOUNDARY
+};
+
+/**
+ * Determine if the provided coordinates are within the pixel buffer limits.
+ * @param x The X coordinate.
+ * @param y The Y coordinate.
+ * @param bci The bitmap_coords_info structure.
+ */
+inline static bool coords_in_range(unsigned int x, unsigned int y, bitmap_coords_info bci) {
+ return (x < bci.width) &&
+ (y < bci.height);
+}
+
+#define PAINT_DIRECTION_LEFT 1
+#define PAINT_DIRECTION_RIGHT 2
+#define PAINT_DIRECTION_UP 4
+#define PAINT_DIRECTION_DOWN 8
+#define PAINT_DIRECTION_ALL 15
+
+/**
+ * Paint a pixel or a square (if autogap is enabled) on the trace pixel buffer.
+ * @param px The rendered pixel buffer to check.
+ * @param trace_px The trace pixel buffer.
+ * @param orig_color The original selected pixel to use as the fill target color.
+ * @param bci The bitmap_coords_info structure.
+ * @param original_point_trace_t The original pixel in the trace pixel buffer to check.
+ */
+inline static unsigned int paint_pixel(guchar *px, guchar *trace_px, guint32 orig_color, bitmap_coords_info bci, unsigned char *original_point_trace_t) {
+ if (bci.radius == 0) {
+ mark_pixel_colored(original_point_trace_t);
+ return PAINT_DIRECTION_ALL;
+ } else {
+ unsigned char *trace_t;
+
+ bool can_paint_up = true;
+ bool can_paint_down = true;
+ bool can_paint_left = true;
+ bool can_paint_right = true;
+
+ for (unsigned int ty = bci.y - bci.radius; ty <= bci.y + bci.radius; ty++) {
+ for (unsigned int tx = bci.x - bci.radius; tx <= bci.x + bci.radius; tx++) {
+ if (coords_in_range(tx, ty, bci)) {
+ trace_t = get_trace_pixel(trace_px, tx, ty, bci.width);
+ if (!is_pixel_colored(trace_t)) {
+ if (check_if_pixel_is_paintable(px, trace_t, tx, ty, orig_color, bci)) {
+ mark_pixel_colored(trace_t);
+ } else {
+ if (tx < bci.x) { can_paint_left = false; }
+ if (tx > bci.x) { can_paint_right = false; }
+ if (ty < bci.y) { can_paint_up = false; }
+ if (ty > bci.y) { can_paint_down = false; }
+ }
+ }
+ }
+ }
+ }
+
+ unsigned int paint_directions = 0;
+ if (can_paint_left) { paint_directions += PAINT_DIRECTION_LEFT; }
+ if (can_paint_right) { paint_directions += PAINT_DIRECTION_RIGHT; }
+ if (can_paint_up) { paint_directions += PAINT_DIRECTION_UP; }
+ if (can_paint_down) { paint_directions += PAINT_DIRECTION_DOWN; }
+
+ return paint_directions;
+ }
+}
+
+/**
+ * Push a point to be checked onto the bottom of the rendered pixel buffer check queue.
+ * @param fill_queue The fill queue to add the point to.
+ * @param max_queue_size The maximum size of the fill queue.
+ * @param trace_t The trace pixel buffer pixel.
+ * @param x The X coordinate.
+ * @param y The Y coordinate.
+ */
+static void push_point_onto_queue(std::deque<Geom::Point> *fill_queue, unsigned int max_queue_size, unsigned char *trace_t, unsigned int x, unsigned int y) {
+ if (!is_pixel_queued(trace_t)) {
+ if ((fill_queue->size() < max_queue_size)) {
+ fill_queue->push_back(Geom::Point(x, y));
+ mark_pixel_queued(trace_t);
+ }
+ }
+}
+
+/**
+ * Shift a point to be checked onto the top of the rendered pixel buffer check queue.
+ * @param fill_queue The fill queue to add the point to.
+ * @param max_queue_size The maximum size of the fill queue.
+ * @param trace_t The trace pixel buffer pixel.
+ * @param x The X coordinate.
+ * @param y The Y coordinate.
+ */
+static void shift_point_onto_queue(std::deque<Geom::Point> *fill_queue, unsigned int max_queue_size, unsigned char *trace_t, unsigned int x, unsigned int y) {
+ if (!is_pixel_queued(trace_t)) {
+ if ((fill_queue->size() < max_queue_size)) {
+ fill_queue->push_front(Geom::Point(x, y));
+ mark_pixel_queued(trace_t);
+ }
+ }
+}
+
+/**
+ * Scan a row in the rendered pixel buffer and add points to the fill queue as necessary.
+ * @param fill_queue The fill queue to add the point to.
+ * @param px The rendered pixel buffer.
+ * @param trace_px The trace pixel buffer.
+ * @param orig_color The original selected pixel to use as the fill target color.
+ * @param bci The bitmap_coords_info structure.
+ */
+static ScanlineCheckResult perform_bitmap_scanline_check(std::deque<Geom::Point> *fill_queue, guchar *px, guchar *trace_px, guint32 orig_color, bitmap_coords_info bci, unsigned int *min_x, unsigned int *max_x) {
+ bool aborted = false;
+ bool reached_screen_boundary = false;
+ bool ok;
+
+ bool keep_tracing;
+ bool initial_paint = true;
+
+ unsigned char *current_trace_t = get_trace_pixel(trace_px, bci.x, bci.y, bci.width);
+ unsigned int paint_directions;
+
+ bool currently_painting_top = false;
+ bool currently_painting_bottom = false;
+
+ unsigned int top_ty = (bci.y > 0) ? bci.y - 1 : 0;
+ unsigned int bottom_ty = bci.y + 1;
+
+ bool can_paint_top = (top_ty > 0);
+ bool can_paint_bottom = (bottom_ty < bci.height);
+
+ Geom::Point front_of_queue = fill_queue->empty() ? Geom::Point() : fill_queue->front();
+
+ do {
+ ok = false;
+ if (bci.is_left) {
+ keep_tracing = (bci.x != 0);
+ } else {
+ keep_tracing = (bci.x < bci.width);
+ }
+
+ *min_x = MIN(*min_x, bci.x);
+ *max_x = MAX(*max_x, bci.x);
+
+ if (keep_tracing) {
+ if (check_if_pixel_is_paintable(px, current_trace_t, bci.x, bci.y, orig_color, bci)) {
+ paint_directions = paint_pixel(px, trace_px, orig_color, bci, current_trace_t);
+ if (bci.radius == 0) {
+ mark_pixel_checked(current_trace_t);
+ if ((!fill_queue->empty()) &&
+ (front_of_queue[Geom::X] == bci.x) &&
+ (front_of_queue[Geom::Y] == bci.y)) {
+ fill_queue->pop_front();
+ front_of_queue = fill_queue->empty() ? Geom::Point() : fill_queue->front();
+ }
+ }
+
+ if (can_paint_top) {
+ if (paint_directions & PAINT_DIRECTION_UP) {
+ unsigned char *trace_t = current_trace_t - bci.width;
+ if (!is_pixel_queued(trace_t)) {
+ bool ok_to_paint = check_if_pixel_is_paintable(px, trace_t, bci.x, top_ty, orig_color, bci);
+
+ if (initial_paint) { currently_painting_top = !ok_to_paint; }
+
+ if (ok_to_paint && (!currently_painting_top)) {
+ currently_painting_top = true;
+ push_point_onto_queue(fill_queue, bci.max_queue_size, trace_t, bci.x, top_ty);
+ }
+ if ((!ok_to_paint) && currently_painting_top) {
+ currently_painting_top = false;
+ }
+ }
+ }
+ }
+
+ if (can_paint_bottom) {
+ if (paint_directions & PAINT_DIRECTION_DOWN) {
+ unsigned char *trace_t = current_trace_t + bci.width;
+ if (!is_pixel_queued(trace_t)) {
+ bool ok_to_paint = check_if_pixel_is_paintable(px, trace_t, bci.x, bottom_ty, orig_color, bci);
+
+ if (initial_paint) { currently_painting_bottom = !ok_to_paint; }
+
+ if (ok_to_paint && (!currently_painting_bottom)) {
+ currently_painting_bottom = true;
+ push_point_onto_queue(fill_queue, bci.max_queue_size, trace_t, bci.x, bottom_ty);
+ }
+ if ((!ok_to_paint) && currently_painting_bottom) {
+ currently_painting_bottom = false;
+ }
+ }
+ }
+ }
+
+ if (bci.is_left) {
+ if (paint_directions & PAINT_DIRECTION_LEFT) {
+ bci.x--; current_trace_t--;
+ ok = true;
+ }
+ } else {
+ if (paint_directions & PAINT_DIRECTION_RIGHT) {
+ bci.x++; current_trace_t++;
+ ok = true;
+ }
+ }
+
+ initial_paint = false;
+ }
+ } else {
+ if (bci.bbox.min()[Geom::X] > bci.screen.min()[Geom::X]) {
+ aborted = true; break;
+ } else {
+ reached_screen_boundary = true;
+ }
+ }
+ } while (ok);
+
+ if (aborted) { return SCANLINE_CHECK_ABORTED; }
+ if (reached_screen_boundary) { return SCANLINE_CHECK_BOUNDARY; }
+ return SCANLINE_CHECK_OK;
+}
+
+/**
+ * Sort the rendered pixel buffer check queue vertically.
+ */
+static bool sort_fill_queue_vertical(Geom::Point a, Geom::Point b) {
+ return a[Geom::Y] > b[Geom::Y];
+}
+
+/**
+ * Sort the rendered pixel buffer check queue horizontally.
+ */
+static bool sort_fill_queue_horizontal(Geom::Point a, Geom::Point b) {
+ return a[Geom::X] > b[Geom::X];
+}
+
+/**
+ * Perform a flood fill operation.
+ * @param 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 is 0xrrggbbaa, we need 0xaarrggbb
+ dtc = (bgcolor >> 8) | (bgcolor << 24);
+
+ dc.setSource(bgcolor);
+ dc.setOperator(CAIRO_OPERATOR_SOURCE);
+ dc.paint();
+ dc.setOperator(CAIRO_OPERATOR_OVER);
+
+ drawing.render(dc, final_bbox);
+
+ //cairo_surface_write_to_png( s, "cairo.png" );
+
+ cairo_surface_flush(s);
+ cairo_surface_destroy(s);
+
+ // Hide items
+ document->getRoot()->invoke_hide(dkey);
+ }
+
+ // {
+ // // Dump data to png
+ // cairo_surface_t *s = cairo_image_surface_create_for_data(
+ // px, CAIRO_FORMAT_ARGB32, width, height, stride);
+ // cairo_surface_write_to_png( s, "cairo2.png" );
+ // std::cout << " Wrote cairo2.png" << std::endl;
+ // }
+
+ guchar *trace_px = g_new(guchar, width * height);
+ memset(trace_px, 0x00, width * height);
+
+ std::deque<Geom::Point> fill_queue;
+ std::queue<Geom::Point> color_queue;
+
+ std::vector<Geom::Point> fill_points;
+
+ bool aborted = false;
+ int y_limit = height - 1;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ PaintBucketChannels method = (PaintBucketChannels) prefs->getInt("/tools/paintbucket/channels", 0);
+ int threshold = prefs->getIntLimited("/tools/paintbucket/threshold", 1, 0, 100);
+
+ switch(method) {
+ case FLOOD_CHANNELS_ALPHA:
+ case FLOOD_CHANNELS_RGB:
+ case FLOOD_CHANNELS_R:
+ case FLOOD_CHANNELS_G:
+ case FLOOD_CHANNELS_B:
+ threshold = (255 * threshold) / 100;
+ break;
+ case FLOOD_CHANNELS_H:
+ case FLOOD_CHANNELS_S:
+ case FLOOD_CHANNELS_L:
+ break;
+ }
+
+ bitmap_coords_info bci;
+
+ bci.y_limit = y_limit;
+ bci.width = width;
+ bci.height = height;
+ bci.stride = stride;
+ bci.threshold = threshold;
+ bci.method = method;
+ bci.bbox = *bbox;
+ bci.screen = screen;
+ bci.dtc = dtc;
+ bci.radius = prefs->getIntLimited("/tools/paintbucket/autogap", 0, 0, 3);
+ bci.max_queue_size = (width * height) / 4;
+ bci.current_step = 0;
+
+ if (is_point_fill) {
+ fill_points.emplace_back(event->button.x, event->button.y);
+ } else {
+ Inkscape::Rubberband *r = Inkscape::Rubberband::get(desktop);
+ fill_points = r->getPoints();
+ }
+
+ auto const img_max_indices = Geom::Rect::from_xywh(0, 0, width - 1, height - 1);
+
+ for (unsigned int i = 0; i < fill_points.size(); i++) {
+ Geom::Point pw = fill_points[i] * world2img;
+
+ pw = img_max_indices.clamp(pw);
+
+ if (is_touch_fill) {
+ if (i == 0) {
+ color_queue.push(pw);
+ } else {
+ unsigned char *trace_t = get_trace_pixel(trace_px, (int)pw[Geom::X], (int)pw[Geom::Y], width);
+ push_point_onto_queue(&fill_queue, bci.max_queue_size, trace_t, (int)pw[Geom::X], (int)pw[Geom::Y]);
+ }
+ } else {
+ color_queue.push(pw);
+ }
+ }
+
+ bool reached_screen_boundary = false;
+
+ bool first_run = true;
+
+ unsigned long sort_size_threshold = 5;
+
+ unsigned int min_y = height;
+ unsigned int max_y = 0;
+ unsigned int min_x = width;
+ unsigned int max_x = 0;
+
+ while (!color_queue.empty() && !aborted) {
+ Geom::Point color_point = color_queue.front();
+ color_queue.pop();
+
+ int cx = (int)color_point[Geom::X];
+ int cy = (int)color_point[Geom::Y];
+
+ guint32 orig_color = get_pixel(px, cx, cy, stride);
+ bci.merged_orig_pixel = compose_onto(orig_color, dtc);
+
+ unsigned char *trace_t = get_trace_pixel(trace_px, cx, cy, width);
+ if (!is_pixel_checked(trace_t) && !is_pixel_colored(trace_t)) {
+ if (check_if_pixel_is_paintable(px, trace_px, cx, cy, orig_color, bci)) {
+ shift_point_onto_queue(&fill_queue, bci.max_queue_size, trace_t, cx, cy);
+
+ if (!first_run) {
+ for (unsigned int y = 0; y < height; y++) {
+ trace_t = get_trace_pixel(trace_px, 0, y, width);
+ for (unsigned int x = 0; x < width; x++) {
+ clear_pixel_paintability(trace_t);
+ trace_t++;
+ }
+ }
+ }
+ first_run = false;
+ }
+ }
+
+ unsigned long old_fill_queue_size = fill_queue.size();
+
+ while (!fill_queue.empty() && !aborted) {
+ Geom::Point cp = fill_queue.front();
+
+ if (bci.radius == 0) {
+ unsigned long new_fill_queue_size = fill_queue.size();
+
+ /*
+ * To reduce the number of points in the fill queue, periodically
+ * resort all of the points in the queue so that scanline checks
+ * can complete more quickly. A point cannot be checked twice
+ * in a normal scanline checks, so forcing scanline checks to start
+ * from one corner of the rendered area as often as possible
+ * will reduce the number of points that need to be checked and queued.
+ */
+ if (new_fill_queue_size > sort_size_threshold) {
+ if (new_fill_queue_size > old_fill_queue_size) {
+ std::sort(fill_queue.begin(), fill_queue.end(), sort_fill_queue_vertical);
+
+ std::deque<Geom::Point>::iterator start_sort = fill_queue.begin();
+ std::deque<Geom::Point>::iterator end_sort = fill_queue.begin();
+ unsigned int sort_y = (unsigned int)cp[Geom::Y];
+ unsigned int current_y;
+
+ for (std::deque<Geom::Point>::iterator i = fill_queue.begin(); i != fill_queue.end(); ++i) {
+ Geom::Point current = *i;
+ current_y = (unsigned int)current[Geom::Y];
+ if (current_y != sort_y) {
+ if (start_sort != end_sort) {
+ std::sort(start_sort, end_sort, sort_fill_queue_horizontal);
+ }
+ sort_y = current_y;
+ start_sort = i;
+ }
+ end_sort = i;
+ }
+ if (start_sort != end_sort) {
+ std::sort(start_sort, end_sort, sort_fill_queue_horizontal);
+ }
+
+ cp = fill_queue.front();
+ }
+ }
+
+ old_fill_queue_size = new_fill_queue_size;
+ }
+
+ fill_queue.pop_front();
+
+ int x = (int)cp[Geom::X];
+ int y = (int)cp[Geom::Y];
+
+ min_y = MIN((unsigned int)y, min_y);
+ max_y = MAX((unsigned int)y, max_y);
+
+ unsigned char *trace_t = get_trace_pixel(trace_px, x, y, width);
+ if (!is_pixel_checked(trace_t)) {
+ mark_pixel_checked(trace_t);
+
+ if (y == 0) {
+ if (bbox->min()[Geom::Y] > screen.min()[Geom::Y]) {
+ aborted = true; break;
+ } else {
+ reached_screen_boundary = true;
+ }
+ }
+
+ if (y == y_limit) {
+ if (bbox->max()[Geom::Y] < screen.max()[Geom::Y]) {
+ aborted = true; break;
+ } else {
+ reached_screen_boundary = true;
+ }
+ }
+
+ bci.is_left = true;
+ bci.x = x;
+ bci.y = y;
+
+ ScanlineCheckResult result = perform_bitmap_scanline_check(&fill_queue, px, trace_px, orig_color, bci, &min_x, &max_x);
+
+ switch (result) {
+ case SCANLINE_CHECK_ABORTED:
+ aborted = true;
+ break;
+ case SCANLINE_CHECK_BOUNDARY:
+ reached_screen_boundary = true;
+ break;
+ default:
+ break;
+ }
+
+ if (bci.x < width) {
+ trace_t++;
+ if (!is_pixel_checked(trace_t) && !is_pixel_queued(trace_t)) {
+ mark_pixel_checked(trace_t);
+ bci.is_left = false;
+ bci.x = x + 1;
+
+ result = perform_bitmap_scanline_check(&fill_queue, px, trace_px, orig_color, bci, &min_x, &max_x);
+
+ switch (result) {
+ case SCANLINE_CHECK_ABORTED:
+ aborted = true;
+ break;
+ case SCANLINE_CHECK_BOUNDARY:
+ reached_screen_boundary = true;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ }
+
+ bci.current_step++;
+
+ if (bci.current_step > bci.max_queue_size) {
+ aborted = true;
+ }
+ }
+ }
+
+ g_free(px);
+
+ if (aborted) {
+ g_free(trace_px);
+ desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("<b>Area is not bounded</b>, cannot fill."));
+ return;
+ }
+
+ if (reached_screen_boundary) {
+ desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("<b>Only the visible part of the bounded area was filled.</b> If you want to fill all of the area, undo, zoom out, and fill again."));
+ }
+
+ unsigned int trace_padding = bci.radius + 1;
+ if (min_y > trace_padding) { min_y -= trace_padding; }
+ if (max_y < (y_limit - trace_padding)) { max_y += trace_padding; }
+ if (min_x > trace_padding) { min_x -= trace_padding; }
+ if (max_x < (width - 1 - trace_padding)) { max_x += trace_padding; }
+
+ Geom::Affine inverted_affine = Geom::Translate(min_x, min_y) * doc2img.inverse();
+
+ do_trace(bci, trace_px, desktop, inverted_affine, min_x, max_x, min_y, max_y, union_with_selection);
+
+ g_free(trace_px);
+
+ DocumentUndo::done(document, _("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..8b5b56c
--- /dev/null
+++ b/src/ui/tools/freehand-base.cpp
@@ -0,0 +1,1077 @@
+// 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"
+#include "ui/tools/pen-tool.h"
+#include "ui/tools/pencil-tool.h"
+
+#define MIN_PRESSURE 0.0
+#define MAX_PRESSURE 1.0
+#define DEFAULT_PRESSURE 1.0
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+static void spdc_selection_changed(Inkscape::Selection *sel, FreehandBase *dc);
+static void spdc_selection_modified(Inkscape::Selection *sel, guint flags, FreehandBase *dc);
+
+static void spdc_attach_selection(FreehandBase *dc, Inkscape::Selection *sel);
+
+/**
+ * Flushes white curve(s) and additional curve into object.
+ *
+ * No cleaning of colored curves - this has to be done by caller
+ * No rereading of white data, so if you cannot rely on ::modified, do it in caller
+ */
+static void spdc_flush_white(FreehandBase *dc, SPCurve *gc);
+
+static void spdc_reset_white(FreehandBase *dc);
+static void spdc_free_colors(FreehandBase *dc);
+
+FreehandBase::FreehandBase(SPDesktop *desktop, std::string prefs_path, const std::string &cursor_filename)
+ : ToolBase(desktop, prefs_path, cursor_filename)
+ , selection(nullptr)
+ , attach(false)
+ , red_color(0xff00007f)
+ , blue_color(0x0000ff7f)
+ , green_color(0x00ff007f)
+ , highlight_color(0x0000007f)
+ , red_bpath(nullptr)
+ , red_curve(nullptr)
+ , blue_bpath(nullptr)
+ , blue_curve(nullptr)
+ , green_curve(nullptr)
+ , green_anchor(nullptr)
+ , green_closed(false)
+ , white_item(nullptr)
+ , sa_overwrited(nullptr)
+ , sa(nullptr)
+ , ea(nullptr)
+ , waiting_LPE_type(Inkscape::LivePathEffect::INVALID_LPE)
+ , red_curve_is_valid(false)
+ , anchor_statusbar(false)
+ , tablet_enabled(false)
+ , is_tablet(false)
+ , pressure(DEFAULT_PRESSURE)
+{
+ this->selection = desktop->getSelection();
+
+ // Connect signals to track selection changes
+ this->sel_changed_connection = this->selection->connectChanged(
+ sigc::bind(sigc::ptr_fun(&spdc_selection_changed), this)
+ );
+ this->sel_modified_connection = this->selection->connectModified(
+ sigc::bind(sigc::ptr_fun(&spdc_selection_modified), this)
+ );
+
+ // Create red bpath
+ this->red_bpath = new Inkscape::CanvasItemBpath(desktop->getCanvasSketch());
+ this->red_bpath->set_stroke(this->red_color);
+ this->red_bpath->set_fill(0x0, SP_WIND_RULE_NONZERO);
+
+ // Create red curve
+ this->red_curve.reset(new SPCurve());
+
+ // Create blue bpath
+ this->blue_bpath = new Inkscape::CanvasItemBpath(desktop->getCanvasSketch());
+ this->blue_bpath->set_stroke(this->blue_color);
+ this->blue_bpath->set_fill(0x0, SP_WIND_RULE_NONZERO);
+
+ // Create blue curve
+ this->blue_curve.reset(new SPCurve());
+
+ // Create green curve
+ this->green_curve.reset(new 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());
+
+ this->attach = TRUE;
+ spdc_attach_selection(this, this->selection);
+}
+
+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 Glib::ustring const tool_name(FreehandBase *dc)
+{
+ return ( SP_IS_PEN_CONTEXT(dc)
+ ? "/tools/freehand/pen"
+ : "/tools/freehand/pencil" );
+}
+
+static void spdc_paste_curve_as_freehand_shape(Geom::PathVector const &newpath, FreehandBase *dc, SPItem *item)
+{
+ using namespace Inkscape::LivePathEffect;
+
+ // TODO: Don't paste path if nothing is on the clipboard
+ SPDocument *document = dc->getDesktop()->doc();
+ Effect::createAndApply(PATTERN_ALONG_PATH, document, item);
+ Effect* lpe = SP_LPE_ITEM(item)->getCurrentLPE();
+ static_cast<LPEPatternAlongPath*>(lpe)->pattern.set_new_value(newpath,true);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double scale = prefs->getDouble("/live_effects/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,
+ gint maxrecursion = 0)
+{
+ 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");
+ SPItem *successor = dynamic_cast<SPItem *>(elemref);
+ sp_desktop_apply_style_tool(desktop, successor->getRepr(),
+ Glib::ustring("/tools/freehand/pencil").data(), false);
+ spdc_apply_style(successor);
+ sp_object_ref(item);
+ item->deleteObject(false);
+ item->setSuccessor(successor);
+ sp_object_unref(item);
+ item = dynamic_cast<SPItem *>(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 = SP_LPE_ITEM(item)->getCurrentLPE();
+
+ static_cast<LPEPowerStroke*>(lpe)->offset_points.param_set_and_write_new_value(points);
+
+ // write powerstroke parameters:
+ lpe->getRepr()->setAttribute("start_linecap_type", "zerowidth");
+ lpe->getRepr()->setAttribute("end_linecap_type", "zerowidth");
+ lpe->getRepr()->setAttribute("sort_points", "true");
+ lpe->getRepr()->setAttribute("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;
+ SPUse *use = dynamic_cast<SPUse *>(item);
+ if ( use ) {
+ return;
+ }
+ SPDesktop *desktop = dc->getDesktop();
+ SPDocument *document = desktop->getDocument();
+ if (!document || !desktop) {
+ return;
+ }
+ if (!SP_IS_LPE_ITEM(item)) {
+ return;
+ }
+ if(!SP_LPE_ITEM(item)->hasPathEffectOfType(BEND_PATH)){
+ Effect::createAndApply(BEND_PATH, document, item);
+ }
+ Effect* lpe = SP_LPE_ITEM(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 = SP_LPE_ITEM(item)->getCurrentLPE();
+ // write simplify parameters:
+ lpe->getRepr()->setAttribute("steps", "1");
+ lpe->getRepr()->setAttributeOrRemoveIfEmpty("threshold", threshold);
+ lpe->getRepr()->setAttribute("smooth_angles", "360");
+ lpe->getRepr()->setAttribute("helper_size", "0");
+ lpe->getRepr()->setAttribute("simplify_individual_paths", "false");
+ lpe->getRepr()->setAttribute("simplify_just_coalesce", "false");
+}
+
+static shapeType previous_shape_type = NONE;
+
+static void spdc_check_for_and_apply_waiting_LPE(FreehandBase *dc, SPItem *item, SPCurve *curve, bool is_bend)
+{
+ using namespace Inkscape::LivePathEffect;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ auto *desktop = dc->getDesktop();
+
+ if (item && SP_IS_LPE_ITEM(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(tool_name(dc) + "/shape", 0);
+ if (previous_shape_type == NONE) {
+ previous_shape_type = shape;
+ }
+ if(shape == LAST_APPLIED){
+ shape = previous_shape_type;
+ if(shape == CLIPBOARD || shape == BEND_CLIPBOARD){
+ shape = LAST_APPLIED;
+ }
+ }
+ Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get();
+ if (is_bend &&
+ (shape == BEND_CLIPBOARD || (shape == LAST_APPLIED && previous_shape_type != CLIPBOARD)) &&
+ cm->paste(desktop, true))
+ {
+ bend_item = dc->selection->singleItem();
+ if(!bend_item || (!SP_IS_SHAPE(bend_item) && !SP_IS_GROUP(bend_item))){
+ previous_shape_type = NONE;
+ return;
+ }
+ } else if(is_bend) {
+ return;
+ }
+ if (!is_bend && previous_shape_type == BEND_CLIPBOARD && shape == BEND_CLIPBOARD) {
+ return;
+ }
+ bool shape_applied = false;
+ bool simplify = prefs->getInt(tool_name(dc) + "/simplify", 0);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ guint mode = prefs->getInt("/tools/freehand/pencil/freehand-mode", 0);
+ if(simplify && mode != 2){
+ double tol = prefs->getDoubleLimited("/tools/freehand/pencil/tolerance", 10.0, 1.0, 100.0);
+ tol = tol/(100.0*(102.0-tol));
+ std::ostringstream ss;
+ ss << tol;
+ spdc_apply_simplify(ss.str(), dc, item);
+ sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), true, false);
+ }
+ if (prefs->getInt(tool_name(dc) + "/freehand-mode", 0) == 1) {
+ Effect::createAndApply(SPIRO, dc->getDesktop()->getDocument(), item);
+ }
+
+ if (prefs->getInt(tool_name(dc) + "/freehand-mode", 0) == 2) {
+ Effect::createAndApply(BSPLINE, dc->getDesktop()->getDocument(), item);
+ }
+ SPShape *sp_shape = dynamic_cast<SPShape *>(item);
+ if (sp_shape) {
+ curve = sp_shape->curve();
+ }
+ auto curveref = curve->ref();
+ 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);
+ 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"
+ guint curve_length = curveref->get_segment_count();
+ std::vector<Geom::Point> points(1);
+ points[0] = Geom::Point(0, swidth);
+ //points[0] *= i2anc_affine(static_cast<SPItem *>(item->parent), NULL).inverse();
+ points[0][Geom::X] = (double)curve_length;
+ spdc_apply_powerstroke_shape(points, dc, item);
+
+ shape_applied = false;
+ break;
+ }
+ case ELLIPSE:
+ {
+ // "ellipse"
+ auto c = std::make_unique<SPCurve>();
+ const double C1 = 0.552;
+ c->moveto(0, SHAPE_HEIGHT/2);
+ c->curveto(0, (1 - C1) * SHAPE_HEIGHT/2, (1 - C1) * SHAPE_LENGTH/2, 0, SHAPE_LENGTH/2, 0);
+ c->curveto((1 + C1) * SHAPE_LENGTH/2, 0, SHAPE_LENGTH, (1 - C1) * SHAPE_HEIGHT/2, SHAPE_LENGTH, SHAPE_HEIGHT/2);
+ c->curveto(SHAPE_LENGTH, (1 + C1) * SHAPE_HEIGHT/2, (1 + C1) * SHAPE_LENGTH/2, SHAPE_HEIGHT, SHAPE_LENGTH/2, SHAPE_HEIGHT);
+ c->curveto((1 - C1) * SHAPE_LENGTH/2, SHAPE_HEIGHT, 0, (1 + C1) * SHAPE_HEIGHT/2, 0, SHAPE_HEIGHT/2);
+ c->closepath();
+ spdc_paste_curve_as_freehand_shape(c->get_pathvector(), dc, item);
+
+ shape_applied = true;
+ break;
+ }
+ case CLIPBOARD:
+ {
+ // take shape from clipboard;
+ Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get();
+ if(cm->paste(desktop,true)){
+ SPItem * pasted_clipboard = dc->selection->singleItem();
+ dc->selection->toCurves(true);
+ pasted_clipboard = dc->selection->singleItem();
+ if(pasted_clipboard){
+ Inkscape::XML::Node *pasted_clipboard_root = pasted_clipboard->getRepr();
+ Inkscape::XML::Node *path = sp_repr_lookup_name(pasted_clipboard_root, "svg:path", -1); // unlimited search depth
+ if ( path != nullptr ) {
+ gchar const *svgd = path->attribute("d");
+ dc->selection->remove(pasted_clipboard);
+ previous_shape_pathv = sp_svg_read_pathv(svgd);
+ previous_shape_pathv *= pasted_clipboard->transform;
+ spdc_paste_curve_as_freehand_shape(previous_shape_pathv, dc, item);
+
+ shape = CLIPBOARD;
+ shape_applied = true;
+ pasted_clipboard->deleteObject();
+ } else {
+ shape = NONE;
+ }
+ } else {
+ shape = NONE;
+ }
+ } else {
+ shape = NONE;
+ }
+ break;
+ }
+ case BEND_CLIPBOARD:
+ {
+ gchar const *svgd = item->getRepr()->attribute("d");
+ if(bend_item && (SP_IS_SHAPE(bend_item) || SP_IS_GROUP(bend_item))){
+ // If item is a SPRect, convert it to path first:
+ if ( dynamic_cast<SPRect *>(bend_item) ) {
+ 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
+ */
+
+static void spdc_selection_changed(Inkscape::Selection *sel, FreehandBase *dc)
+{
+ if (dc->attach) {
+ spdc_attach_selection(dc, sel);
+ }
+}
+
+/* fixme: We have to ensure this is not delayed (Lauris) */
+
+static void spdc_selection_modified(Inkscape::Selection *sel, guint /*flags*/, FreehandBase *dc)
+{
+ if (dc->attach) {
+ spdc_attach_selection(dc, sel);
+ }
+}
+
+static void spdc_attach_selection(FreehandBase *dc, Inkscape::Selection */*sel*/)
+{
+ // We reset white and forget white/start/end anchors
+ spdc_reset_white(dc);
+ dc->sa = nullptr;
+ dc->ea = nullptr;
+
+ SPItem *item = dc->selection ? dc->selection->singleItem() : nullptr;
+
+ if ( item && SP_IS_PATH(item) ) {
+ // Create new white data
+ // Item
+ dc->white_item = item;
+
+ // Curve list
+ // We keep it in desktop coordinates to eliminate calculation errors
+ auto path = static_cast<SPPath *>(item);
+ auto norm = SPCurve::copy(path->curveForEdit());
+ g_return_if_fail( norm != nullptr );
+ norm->transform((dc->white_item)->i2dt_affine());
+ dc->white_curves = norm->split();
+
+ // Anchor list
+ for (auto const &c_smart_ptr : dc->white_curves) {
+ auto *c = c_smart_ptr.get();
+ g_return_if_fail( c->get_segment_count() > 0 );
+ if ( !c->is_closed() ) {
+ std::unique_ptr<SPDrawAnchor> a =
+ std::make_unique<SPDrawAnchor>(dc, c, TRUE, *(c->first_point()));
+ if (a)
+ dc->white_anchors.push_back(std::move(a));
+ a = std::make_unique<SPDrawAnchor>(dc, c, FALSE, *(c->last_point()));
+ if (a)
+ dc->white_anchors.push_back(std::move(a));
+ }
+ }
+ // 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_unique<SPCurve>();
+ std::swap(c, dc->green_curve);
+ for (auto path : dc->green_bpaths) {
+ delete path;
+ }
+ dc->green_bpaths.clear();
+
+ // Blue
+ c->append_continuous(*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, c.get());
+ 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 = e->create_reverse();
+ }
+ if(prefs->getInt(tool_name(dc) + "/freehand-mode", 0) == 1 ||
+ prefs->getInt(tool_name(dc) + "/freehand-mode", 0) == 2){
+ e = e->create_reverse();
+ Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*e->last_segment());
+ if(cubic){
+ auto lastSeg = std::make_unique<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 = e->create_reverse();
+ }
+ c->append_continuous(*e);
+ }
+ if (forceclosed)
+ {
+ dc->getDesktop()->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Path is closed."));
+ c->closepath_current();
+ }
+ spdc_flush_white(dc, c.get());
+}
+
+static void spdc_flush_white(FreehandBase *dc, SPCurve *gc)
+{
+ std::unique_ptr<SPCurve> c;
+
+ if (! dc->white_curves.empty()) {
+ g_assert(dc->white_item);
+
+ // c = concat(white_curves)
+ c = std::make_unique<SPCurve>();
+ for (auto const &wc : dc->white_curves) {
+ c->append(*wc);
+ }
+
+ dc->white_curves.clear();
+ if (gc) {
+ c->append(*gc);
+ }
+ } else if (gc) {
+ c = gc->ref();
+ } 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 && !c->is_empty() ) {
+ // We actually have something to write
+
+ bool has_lpe = false;
+ Inkscape::XML::Node *repr;
+
+ if (dc->white_item) {
+ repr = dc->white_item->getRepr();
+ has_lpe = SP_LPE_ITEM(dc->white_item)->hasPathEffectRecursive();
+ } else {
+ repr = xml_doc->createElement("svg:path");
+ // Set style
+ sp_desktop_apply_style_tool(desktop, repr, tool_name(dc).data(), false);
+ }
+
+ 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 = SP_ITEM(layer->appendChildRepr(repr));
+ }
+ spdc_check_for_and_apply_waiting_LPE(dc, dc->white_item, c.get(), false);
+ }
+ if (!dc->white_item) {
+ // Attach repr
+ SPItem *item = dynamic_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);
+ }
+ }
+ SPLPEItem *lpeitem = dynamic_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.
+ spdc_selection_modified(desktop->getSelection(), 0, dc);
+ }
+
+ // 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_reset_white(FreehandBase *dc)
+{
+ if (dc->white_item) {
+ // We do not hold refcount
+ dc->white_item = nullptr;
+ }
+ dc->white_curves.clear();
+ dc->white_anchors.clear();
+}
+
+static void spdc_free_colors(FreehandBase *dc)
+{
+ // Red
+ if (dc->red_bpath) {
+ delete dc->red_bpath;
+ dc->red_bpath = nullptr;
+ }
+ dc->red_curve.reset();
+
+ // Blue
+ if (dc->blue_bpath) {
+ delete dc->blue_bpath;
+ dc->blue_bpath = nullptr;
+ }
+ dc->blue_curve.reset();
+
+ // Overwrite start anchor curve
+ dc->sa_overwrited.reset();
+ // Green
+ for (auto path : dc->green_bpaths) {
+ delete path;
+ }
+ 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();
+ SPItem *item = SP_ITEM(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..e01af7a
--- /dev/null
+++ b/src/ui/tools/freehand-base.h
@@ -0,0 +1,157 @@
+// 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"
+
+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;
+
+ bool attach;
+
+ guint32 red_color;
+ guint32 blue_color;
+ guint32 green_color;
+ guint32 highlight_color;
+
+ // Red - Last segment as it's drawn.
+ Inkscape::CanvasItemBpath *red_bpath;
+ std::unique_ptr<SPCurve> red_curve;
+ std::optional<Geom::Point> red_curve_get_last_point();
+
+ // Blue - New path after LPE as it's drawn.
+ Inkscape::CanvasItemBpath *blue_bpath;
+ std::unique_ptr<SPCurve> blue_curve;
+
+ // Green - New path as it's drawn.
+ std::vector<Inkscape::CanvasItemBpath *> green_bpaths;
+ std::unique_ptr<SPCurve> green_curve;
+ std::unique_ptr<SPDrawAnchor> green_anchor;
+ gboolean green_closed; // a flag meaning we hit the green anchor, so close the path on itself
+
+ // White
+ SPItem *white_item;
+ std::list<std::unique_ptr<SPCurve>> white_curves;
+ std::vector<std::unique_ptr<SPDrawAnchor>> white_anchors;
+
+ // Temporary modified curve when start anchor
+ std::unique_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;
+
+protected:
+ bool root_handler(GdkEvent* event) override;
+};
+
+/**
+ * Returns FIRST active anchor (the activated one).
+ */
+SPDrawAnchor *spdc_test_inside(FreehandBase *dc, Geom::Point p);
+
+/**
+ * Concats red, blue and green.
+ * If any anchors are defined, process these, optionally removing curves from white list
+ * Invoke _flush_white to write result back to object.
+ */
+void spdc_concat_colors_and_flush(FreehandBase *dc, gboolean forceclosed);
+
+/**
+ * Snaps node or handle to PI/rotationsnapsperpi degree increments.
+ *
+ * @param dc draw context.
+ * @param p cursor point (to be changed by snapping).
+ * @param o origin point.
+ * @param state keyboard state to check if ctrl or shift was pressed.
+ */
+void spdc_endpoint_snap_rotation(ToolBase* const 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..fff39e8
--- /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)
+ , node_added(false)
+// TODO: Why are these connections stored as pointers?
+ , selcon(nullptr)
+ , subselcon(nullptr)
+{
+ // TODO: This value is overwritten in the root handler
+ this->tolerance = 6;
+
+ 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, 1.0);
+}
+
+void GradientTool::select_prev()
+{
+ g_assert(_grdrag);
+ GrDragger *d = _grdrag->select_prev();
+ _desktop->scroll_to_point(d->point, 1.0);
+}
+
+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 curve : _grdrag->item_curves) {
+ if (curve->contains(event_p, tolerance)) {
+ return curve->get_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 (SP_IS_GRADIENT (parent)) {
+ doc = parent->document;
+ SPStop *new_stop = sp_vector_add_stop (SP_GRADIENT (parent), this_stop, next_stop, offset);
+ new_stops.push_back(new_stop);
+ SP_GRADIENT(parent)->ensureVector();
+ }
+ }
+
+ if (!these_stops.empty() && doc) {
+ DocumentUndo::done(doc, _("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);
+ }
+ _desktop->redrawDesktop();;
+ 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) {
+ 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) {
+ ret = TRUE;
+ break;
+ }
+
+ if (!this->within_tolerance) {
+ // we've been dragging, either do nothing (grdrag handles that),
+ // or rubberband-select if we have rubberband
+ Inkscape::Rubberband *r = Inkscape::Rubberband::get(_desktop);
+
+ if (r->is_started() && !this->within_tolerance) {
+ // this was a rubberband drag
+ if (r->getMode() == RUBBERBAND_MODE_RECT) {
+ Geom::OptRect const b = r->getRectangle();
+ drag->selectRect(*b);
+ }
+ }
+ } else if (this->item_to_select) {
+ if (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..6bd2972
--- /dev/null
+++ b/src/ui/tools/gradient-tool.h
@@ -0,0 +1,78 @@
+// 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;
+ bool node_added;
+
+ 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..efb7d35
--- /dev/null
+++ b/src/ui/tools/lpe-tool.cpp
@@ -0,0 +1,473 @@
+// 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));
+
+ this->shape_editor = new 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()
+{
+ delete this->shape_editor;
+
+ if (canvas_bbox) {
+ delete canvas_bbox;
+ }
+
+ lpetool_delete_measuring_items(this);
+ measuring_items.clear();
+
+ this->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 (!SP_IS_LPE_ITEM(item)) {
+ return -1;
+ }
+
+ Inkscape::LivePathEffect::Effect* lpe = SP_LPE_ITEM(item)->getCurrentLPE();
+ if (!lpe) {
+ return -1;
+ }
+ return lpetool_mode_to_index(lpe->effectType());
+}
+
+/*
+ * Attempts to perform the construction of the given type (i.e., to apply the corresponding LPE) to
+ * a single selected item. Returns whether we succeeded.
+ */
+bool
+lpetool_try_construction(LpeTool *lc, Inkscape::LivePathEffect::EffectType const type)
+{
+ Inkscape::Selection *selection = lc->getDesktop()->getSelection();
+ SPItem *item = selection->singleItem();
+
+ // TODO: should we check whether type represents a valid geometric construction?
+ if (item && SP_IS_LPE_ITEM(item) && Inkscape::LivePathEffect::Effect::acceptsNumClicks(type) == 0) {
+ Inkscape::LivePathEffect::Effect::createAndApply(type, lc->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)
+{
+ if (lc->canvas_bbox) {
+ delete lc->canvas_bbox;
+ lc->canvas_bbox = nullptr;
+ }
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (!prefs->getBool("/tools/lpetool/show_bbox", true))
+ return;
+
+ SPDocument *document = lc->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 = new Inkscape::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::CanvasItemText *canvas_text;
+ 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) {
+ SPPath *path = dynamic_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;
+
+ canvas_text = new Inkscape::CanvasItemText(tmpgrp, Geom::Point(0,0), arc_length);
+ set_pos_and_anchor(canvas_text, pwd2, 0.5, 10);
+ if (!show) {
+ canvas_text->hide();
+ }
+
+ (lc->measuring_items)[path] = canvas_text;
+ }
+ }
+}
+
+void
+lpetool_delete_measuring_items(LpeTool *lc)
+{
+ for (auto& i : lc->measuring_items) {
+ delete i.second;
+ }
+ 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(arc_length);
+ set_pos_and_anchor(i.second, 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..fd77a13
--- /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;
+
+ ShapeEditor* shape_editor = nullptr;
+ Inkscape::CanvasItemRect *canvas_bbox = nullptr;
+ Inkscape::LivePathEffect::EffectType mode;
+
+ std::map<SPPath *, Inkscape::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..e2e2631
--- /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) {
+ SPShape* shape = dynamic_cast<SPShape*>(item);
+
+ if(shape && shape->hasMarkers() && (editMarkerMode != -1)) {
+ SPObject *obj = shape->_marker[editMarkerMode];
+
+ if(obj) {
+
+ SPMarker *sp_marker = dynamic_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(dynamic_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..d9ba7b1
--- /dev/null
+++ b/src/ui/tools/measure-tool.cpp
@@ -0,0 +1,1470 @@
+// 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 "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,
+ std::vector<Inkscape::CanvasItem *> &measure_phantom_items,
+ std::vector<Inkscape::CanvasItem *> &measure_tmp_items,
+ 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->set_z_position(0);
+ 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);
+
+ for (auto & measure_tmp_item : measure_tmp_items) {
+ delete measure_tmp_item;
+ }
+ measure_tmp_items.clear();
+
+ for (auto & idx : measure_item) {
+ delete idx;
+ }
+ measure_item.clear();
+
+ for (auto & measure_phantom_item : measure_phantom_items) {
+ delete measure_phantom_item;
+ }
+ measure_phantom_items.clear();
+}
+
+Geom::Point MeasureTool::readMeasurePoint(bool is_start) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring measure_point = is_start ? "/tools/measure/measure-start" : "/tools/measure/measure-end";
+ return prefs->getPoint(measure_point, Geom::Point(Geom::infinity(),Geom::infinity()));
+}
+
+void MeasureTool::writeMeasurePoint(Geom::Point point, bool is_start) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring measure_point = is_start ? "/tools/measure/measure-start" : "/tools/measure/measure-end";
+ prefs->setPoint(measure_point, point);
+}
+
+//This function is used to reverse the Measure, I do it in two steps because when
+//we move the knot the start_ or the end_p are overwritten so I need the original values.
+void MeasureTool::reverseKnots()
+{
+ Geom::Point start = start_p;
+ Geom::Point end = end_p;
+ this->knot_start->moveto(end);
+ this->knot_start->show();
+ this->knot_end->moveto(start);
+ this->knot_end->show();
+ start_p = end;
+ end_p = start;
+ this->showCanvasItems();
+}
+
+void MeasureTool::knotClickHandler(SPKnot *knot, guint state)
+{
+ if (state & GDK_SHIFT_MASK) {
+ 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,
+ std::unique_ptr<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();
+ for (auto & idx : measure_item) {
+ delete idx;
+ }
+ measure_item.clear();
+
+ ret = TRUE;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+ Geom::Point const motion_w(event->motion.x, event->motion.y);
+ if ( within_tolerance) {
+ if ( Geom::LInfty( motion_w - start_p ) < tolerance) {
+ return FALSE; // Do not drag if we're within tolerance from origin.
+ }
+ }
+ // Once the user has moved farther than tolerance from the original location
+ // (indicating they intend to move the object, not click), then always process the
+ // motion notify coordinates as given (no snapping back to origin)
+ within_tolerance = false;
+ if(event->motion.time == 0 || !last_end || Geom::LInfty( motion_w - *last_end ) > (tolerance/4.0)) {
+ Geom::Point const motion_dt(_desktop->w2d(motion_w));
+ end_p = motion_dt;
+
+ if (event->motion.state & GDK_CONTROL_MASK) {
+ spdc_endpoint_snap_rotation(this, end_p, start_p, event->motion.state);
+ } else if (!(event->motion.state & GDK_SHIFT_MASK)) {
+ SnapManager &snap_manager = _desktop->namedview->snap_manager;
+ snap_manager.setup(_desktop);
+ Inkscape::SnapCandidatePoint scp(end_p, Inkscape::SNAPSOURCE_OTHER_HANDLE);
+ scp.addOrigin(start_p);
+ Inkscape::SnappedPoint sp = snap_manager.freeSnap(scp);
+ end_p = sp.getPoint();
+ snap_manager.unSetup();
+ }
+ showCanvasItems();
+ last_end = motion_w ;
+ }
+ gobble_motion_events(GDK_BUTTON1_MASK);
+ }
+ break;
+ }
+ case GDK_BUTTON_RELEASE: {
+ 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;");
+ SPItem *marker = SP_ITEM(defs->appendChildRepr(rmarker));
+ Inkscape::GC::release(rmarker);
+ marker->updateRepr();
+ Inkscape::XML::Node *rpath;
+ rpath = xml_doc->createElement("svg:path");
+ rpath->setAttribute("d", "M 8.72,4.03 L -2.21,0.02 L 8.72,-4.00 C 6.97,-1.63 6.98,1.62 8.72,4.03 z");
+ rpath->setAttribute("id", isStart ? "Arrow2SstartPath" : "Arrow2SendPath");
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_set_property (css, "stroke", "none");
+ sp_repr_css_set_property (css, "fill", "#000000");
+ sp_repr_css_set_property (css, "fill-opacity", "1");
+ Glib::ustring css_str;
+ sp_repr_css_write_string(css,css_str);
+ rpath->setAttribute("style", css_str);
+ sp_repr_css_attr_unref (css);
+ rpath->setAttribute("transform", isStart ? "scale(0.3) translate(-2.3,0)" : "scale(0.3) rotate(180) translate(-2.3,0)");
+ SPItem *path = SP_ITEM(marker->appendChildRepr(rpath));
+ Inkscape::GC::release(rpath);
+ path->updateRepr();
+}
+
+void MeasureTool::toGuides()
+{
+ 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();
+
+ for (auto & measure_phantom_item : measure_phantom_items) {
+ delete measure_phantom_item;
+ }
+ measure_phantom_items.clear();
+
+ for (auto & measure_tmp_item : measure_tmp_items) {
+ delete measure_tmp_item;
+ }
+ 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);
+ SPItem *measure_item = SP_ITEM(_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 = dynamic_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);
+ SPItem *text_item_box = SP_ITEM(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();
+
+ for (auto & measure_tmp_item : measure_tmp_items) {
+ delete measure_tmp_item;
+ }
+ measure_tmp_items.clear();
+}
+
+void MeasureTool::setMeasureCanvasText(bool is_angle, double precision, double amount, double fontsize,
+ Glib::ustring unit_name, Geom::Point position, guint32 background,
+ 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.push_back(canvas_tooltip);
+ } else {
+ measure_tmp_items.push_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->set_z_position(0);
+ 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->set_z_position(0);
+ 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.push_back(canvas_tooltip);
+}
+
+void MeasureTool::showInfoBox(Geom::Point cursor, bool into_groups)
+{
+ using Inkscape::Util::Quantity;
+
+ for (auto & idx : measure_item) {
+ delete(idx);
+ }
+ 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);
+ 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 = dynamic_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);
+ Geom::Point rel_position = Geom::Point(origin, origin);
+ Geom::Point pos = _desktop->w2d(cursor);
+ double gap = Quantity::convert(7 + fontsize, "px", unit->abbr);
+ double yaxisdir = _desktop->yaxisdir();
+
+ if (selected) {
+ showItemInfoText(pos - (yaxisdir * rel_position * zoom), _desktop->getSelection()->includes(over) ? _("Selected") : _("Not selected"), fontsize);
+ rel_position = Geom::Point(rel_position[Geom::X], rel_position[Geom::Y] + gap);
+ }
+
+ if (SP_IS_SHAPE(over)) {
+
+ precision_str << _("Length") << ": %." << precision << "f %s";
+ measure_str = g_strdup_printf(precision_str.str().c_str(), item_length, unit_name.c_str());
+ precision_str.str("");
+ showItemInfoText(pos - (yaxisdir * rel_position * zoom), measure_str, fontsize);
+ rel_position = Geom::Point(rel_position[Geom::X], rel_position[Geom::Y] + gap);
+
+ } else if (SP_IS_GROUP(over)) {
+
+ measure_str = _("Press 'CTRL' to measure into group");
+ showItemInfoText(pos - (yaxisdir * rel_position * zoom), measure_str, fontsize);
+ rel_position = Geom::Point(rel_position[Geom::X], rel_position[Geom::Y] + gap);
+
+ }
+
+ precision_str << "Y: %." << precision << "f %s";
+ measure_str = g_strdup_printf(precision_str.str().c_str(), item_y, unit_name.c_str());
+ precision_str.str("");
+ showItemInfoText(pos - (yaxisdir * rel_position * zoom), measure_str, fontsize);
+ rel_position = Geom::Point(rel_position[Geom::X], rel_position[Geom::Y] + gap);
+
+ precision_str << "X: %." << precision << "f %s";
+ measure_str = g_strdup_printf(precision_str.str().c_str(), item_x, unit_name.c_str());
+ precision_str.str("");
+ showItemInfoText(pos - (yaxisdir * rel_position * zoom), measure_str, fontsize);
+ rel_position = Geom::Point(rel_position[Geom::X], rel_position[Geom::Y] + gap);
+
+ precision_str << _("Height") << ": %." << precision << "f %s";
+ measure_str = g_strdup_printf(precision_str.str().c_str(), item_height, unit_name.c_str());
+ precision_str.str("");
+ showItemInfoText(pos - (yaxisdir * rel_position * zoom), measure_str, fontsize);
+ rel_position = Geom::Point(rel_position[Geom::X], rel_position[Geom::Y] + gap);
+
+ precision_str << _("Width") << ": %." << precision << "f %s";
+ measure_str = g_strdup_printf(precision_str.str().c_str(), item_width, unit_name.c_str());
+ precision_str.str("");
+ showItemInfoText(pos - (yaxisdir * rel_position * zoom), measure_str, fontsize);
+ g_free(measure_str);
+}
+
+void MeasureTool::showCanvasItems(bool to_guides, bool to_item, bool to_phantom, Inkscape::XML::Node *measure_repr)
+{
+ if (!_desktop || !start_p.isFinite() || !end_p.isFinite() || start_p == end_p) {
+ return;
+ }
+ writeMeasurePoint(start_p, true);
+ writeMeasurePoint(end_p, false);
+
+ //clear previous canvas items, we'll draw new ones
+ for (auto & measure_tmp_item : measure_tmp_items) {
+ delete measure_tmp_item;
+ }
+ measure_tmp_items.clear();
+
+ //TODO:Calculate the measure area for current length and origin
+ // and use canvas->requestRedraw. In the calculation need a gap for outside text
+ // maybe this remove the trash lines on measure use
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool show_in_between = prefs->getBool("/tools/measure/show_in_between", true);
+ bool all_layers = prefs->getBool("/tools/measure/all_layers", true);
+ dimension_offset = 70;
+ Geom::PathVector lineseg;
+ Geom::Path p;
+ Geom::Point start_p_doc = start_p * _desktop->dt2doc();
+ Geom::Point end_p_doc = end_p * _desktop->dt2doc();
+ p.start(start_p_doc);
+ p.appendNew<Geom::LineSegment>(end_p_doc);
+ lineseg.push_back(p);
+
+ double angle = atan2(end_p - start_p);
+ double baseAngle = 0;
+
+ if (explicit_base) {
+ baseAngle = atan2(*explicit_base - 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 = dynamic_cast<SPShape const *>(item)) {
+ calculate_intersections(_desktop, item, lineseg, SPCurve::copy(shape->curve()), intersection_times);
+ } else {
+ if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item)) {
+ Inkscape::Text::Layout::iterator iter = te_get_layout(item)->begin();
+ do {
+ Inkscape::Text::Layout::iterator iter_next = iter;
+ iter_next.nextGlyph(); // iter_next is one glyph ahead from iter
+ if (iter == iter_next) {
+ break;
+ }
+
+ // get path from iter to iter_next:
+ auto curve = te_get_layout(item)->convertToCurves(iter, iter_next);
+ iter = iter_next; // shift to next glyph
+ if (!curve) {
+ continue; // error converting this glyph
+ }
+ if (curve->is_empty()) { // whitespace glyph?
+ 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_phantom_items, measure_tmp_items, measure_repr);
+ }
+
+ if (intersections.size() > 2) {
+ setMeasureCanvasControlLine(_desktop->doc2dt(intersections[0]) + normal * dimension_offset, _desktop->doc2dt(intersections[intersections.size() - 1]) + normal * dimension_offset, to_item, to_phantom, 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 {
+ SPItem *item = SP_ITEM(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..bdaf53a
--- /dev/null
+++ b/src/ui/tools/measure-tool.h
@@ -0,0 +1,128 @@
+// 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"
+
+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,
+ std::vector<Inkscape::CanvasItem *> &measure_phantom_items,
+ std::vector<Inkscape::CanvasItem *> &measure_tmp_items,
+ 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<Inkscape::CanvasItem *> measure_tmp_items;
+ std::vector<Inkscape::CanvasItem *> measure_phantom_items;
+ std::vector<Inkscape::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..deff50c
--- /dev/null
+++ b/src/ui/tools/mesh-tool.cpp
@@ -0,0 +1,973 @@
+// 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, 1.0);
+}
+
+void MeshTool::select_prev()
+{
+ g_assert(_grdrag);
+ GrDragger *d = _grdrag->select_prev();
+ _desktop->scroll_to_point(d->point, 1.0);
+}
+
+/**
+ * Returns vector of control curves mouse is over. Returns only first if 'first' is true.
+ * event_p is in canvas (world) units.
+ */
+std::vector<CanvasItemCurve *> 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<CanvasItemCurve *> selected;
+
+ for (auto curve : _grdrag->item_curves) {
+ if (curve->contains(event_p, tolerance)) {
+ selected.push_back(&*curve);
+ 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
+ SPMeshGradient *gradient = SP_MESHGRADIENT( getGradient (d->item, d->fill_or_stroke) );
+
+ // Collect points together for same gradient
+ points[gradient].push_back( d->point_i );
+ items[gradient] = d->item;
+ fill_or_stroke[gradient] = d->fill_or_stroke ? Inkscape::FOR_FILL: Inkscape::FOR_STROKE;
+ }
+ }
+
+ // Loop over meshes.
+ for( std::map<SPMeshGradient*, std::vector<guint> >::const_iterator iter = points.begin(); iter != points.end(); ++iter) {
+ SPMeshGradient *mg = iter->first;
+ if( iter->second.size() > 0 ) {
+ guint noperation = 0;
+ switch (operation) {
+
+ case MG_CORNER_SIDE_TOGGLE:
+ // std::cout << "SIDE_TOGGLE" << std::endl;
+ noperation += mg->array.side_toggle( iter->second );
+ break;
+
+ case MG_CORNER_SIDE_ARC:
+ // std::cout << "SIDE_ARC" << std::endl;
+ noperation += mg->array.side_arc( iter->second );
+ break;
+
+ case MG_CORNER_TENSOR_TOGGLE:
+ // std::cout << "TENSOR_TOGGLE" << std::endl;
+ noperation += mg->array.tensor_toggle( iter->second );
+ break;
+
+ case MG_CORNER_COLOR_SMOOTH:
+ // std::cout << "COLOR_SMOOTH" << std::endl;
+ noperation += mg->array.color_smooth( iter->second );
+ break;
+
+ case MG_CORNER_COLOR_PICK:
+ // std::cout << "COLOR_PICK" << std::endl;
+ noperation += mg->array.color_pick( iter->second, items[iter->first] );
+ break;
+
+ case MG_CORNER_INSERT:
+ // std::cout << "INSERT" << std::endl;
+ noperation += mg->array.insert( iter->second );
+ break;
+
+ default:
+ std::cout << "sp_mesh_corner_operation: unknown operation" << std::endl;
+ }
+
+ if( noperation > 0 ) {
+ mg->array.write( mg );
+ mg->requestModified(SP_OBJECT_MODIFIED_FLAG);
+ doc = mg->document;
+
+ switch (operation) {
+
+ case MG_CORNER_SIDE_TOGGLE:
+ DocumentUndo::done(doc, _("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::cout << "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 ( SP_IS_MESHGRADIENT(server) ) {
+
+ Geom::OptRect item_bbox = item->geometricBounds();
+ SPMeshGradient *gradient = SP_MESHGRADIENT(server);
+ if (gradient->array.fill_box( item_bbox )) {
+ changed = true;
+ }
+ }
+ }
+
+ if (style->stroke.isPaintserver()) {
+ SPPaintServer *server = item->style->getStrokePaintServer();
+ if ( SP_IS_MESHGRADIENT(server) ) {
+
+ Geom::OptRect item_bbox = item->visualBounds();
+ SPMeshGradient *gradient = SP_MESHGRADIENT(server);
+ if (gradient->array.fill_box( item_bbox )) {
+ changed = true;
+ }
+ }
+ }
+
+ }
+ }
+ if (changed) {
+ DocumentUndo::done(_desktop->getDocument(), _("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 && SP_IS_MESHGRADIENT(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) {
+ SPItem *item = it->get_item();
+ Inkscape::PaintTarget fill_or_stroke =
+ it->get_is_fill() ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE;
+ GrDragger* dragger0 = _grdrag->getDraggerFor(item, POINT_MG_CORNER, it->get_corner0(), fill_or_stroke);
+ GrDragger* dragger1 = _grdrag->getDraggerFor(item, POINT_MG_CORNER, it->get_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 && SP_IS_MESHGRADIENT(server))
+ has_mesh = true;
+ }
+ }
+
+ if (has_mesh) {
+ Inkscape::Rubberband::get(_desktop)->start(_desktop, button_dt);
+ }
+
+ // remember clicked item, disregarding groups, honoring Alt; do nothing with Crtl to
+ // enable Ctrl+doubleclick of exactly the selected item(s)
+ if (!(event->button.state & GDK_CONTROL_MASK)) {
+ this->item_to_select = sp_event_context_find_item (_desktop, button_w, event->button.state & GDK_MOD1_MASK, TRUE);
+ }
+
+ if (!selection->isEmpty()) {
+ SnapManager &m = _desktop->namedview->snap_manager;
+ m.setup(_desktop);
+ m.freeSnapReturnByRef(button_dt, Inkscape::SNAPSOURCE_NODE_HANDLE);
+ m.unSetup();
+ }
+
+ this->origin = button_dt;
+
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_MOTION_NOTIFY:
+ // Mouse move
+ if ( dragging && ( event->motion.state & GDK_BUTTON1_MASK ) ) {
+
+#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]->get_item(), this->mousepoint_doc, 0);
+ ret = TRUE;
+ }
+ } else {
+ dragging = false;
+
+ // unless clicked with Ctrl (to enable Ctrl+doubleclick).
+ if (event->button.state & GDK_CONTROL_MASK) {
+ ret = TRUE;
+ break;
+ }
+
+ if (!this->within_tolerance) {
+
+ // Check if object already has mesh... if it does,
+ // don't create new mesh with click-drag.
+ bool has_mesh = false;
+ if (!selection->isEmpty()) {
+ SPStyle *style = selection->items().front()->style;
+ if (style) {
+ SPPaintServer *server =
+ (fill_or_stroke_pref == Inkscape::FOR_FILL) ?
+ style->getFillPaintServer():
+ style->getStrokePaintServer();
+ if (server && SP_IS_MESHGRADIENT(server))
+ has_mesh = true;
+ }
+ }
+
+ if (!has_mesh) {
+ 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 = SP_IS_TEXT(*i);
+ sp_style_set_property_url(*i,
+ ((fill_or_stroke_pref == Inkscape::FOR_FILL) ? "fill":"stroke"),
+ mg, isText);
+
+ (*i)->requestModified(SP_OBJECT_MODIFIED_FLAG|SP_OBJECT_STYLE_MODIFIED_FLAG);
+ }
+
+ if (css) {
+ sp_repr_css_attr_unref(css);
+ css = nullptr;
+ }
+
+ DocumentUndo::done(_desktop->getDocument(), _("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..de13eb1
--- /dev/null
+++ b/src/ui/tools/mesh-tool.h
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_SP_MESH_CONTEXT_H
+#define SEEN_SP_MESH_CONTEXT_H
+
+/*
+ * Mesh drawing and editing tool
+ *
+ * Authors:
+ * bulia byak <buliabyak@users.sf.net>
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ * Jon A. Cruz <jon@joncruz.org.
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2012 Tavmjong Bah
+ * Copyright (C) 2007 Johan Engelen
+ * Copyright (C) 2005,2010 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cstddef>
+#include <sigc++/sigc++.h>
+#include "ui/tools/tool-base.h"
+
+#include "object/sp-mesh-array.h"
+
+#define SP_MESH_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::MeshTool*>((Inkscape::UI::Tools::ToolBase*)obj))
+#define SP_IS_MESH_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::MeshTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL)
+
+class GrDrag;
+
+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<CanvasItemCurve *> 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..92b6e27
--- /dev/null
+++ b/src/ui/tools/node-tool.cpp
@@ -0,0 +1,808 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * New node tool - implementation.
+ */
+/* Authors:
+ * Krzysztof Kosiński <tweenk@gmail.com>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2009 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <iomanip>
+
+#include <glibmm/ustring.h>
+#include <glib/gi18n.h>
+#include <gdk/gdkkeysyms.h>
+
+
+
+#include "desktop.h"
+#include "document.h"
+#include "message-context.h"
+#include "selection-chemistry.h"
+#include "selection.h"
+#include "snap.h"
+
+#include "display/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/shape-editor.h" // temporary!
+#include "ui/tool/control-point-selection.h"
+#include "ui/tool/curve-drag-point.h"
+#include "ui/tool/event-utils.h"
+#include "ui/tool/multi-path-manipulator.h"
+#include "ui/tool/path-manipulator.h"
+#include "ui/tool/selector.h"
+#include "ui/tools/node-tool.h"
+
+/** @struct NodeTool
+ *
+ * Node tool event context.
+ *
+ * @par Architectural overview of the tool
+ * @par
+ * Here's a breakdown of what each object does.
+ * - Handle: shows a handle and keeps the node type constraint (smooth / symmetric) by updating
+ * the other handle's position when dragged. Its move() method cannot violate the constraints.
+ * - Node: keeps node type constraints for auto nodes and smooth nodes at ends of linear segments.
+ * Its move() method cannot violate constraints. Handles linear grow and dispatches spatial grow
+ * to MultiPathManipulator. Keeps a reference to its NodeList.
+ * - NodeList: exposes an iterator-based interface to nodes. It is possible to obtain an iterator
+ * to a node from the node. Keeps a reference to its SubpathList.
+ * - SubpathList: list of NodeLists that represents an editable pathvector. Keeps a reference
+ * to its PathManipulator.
+ * - PathManipulator: performs most of the single-path actions like reverse subpaths,
+ * delete segment, shift selection, etc. Keeps a reference to MultiPathManipulator.
+ * - MultiPathManipulator: performs additional operations for actions that are not per-path,
+ * for example node joins and segment joins. Tracks the control transforms for PMs that edit
+ * clipping paths and masks. It is more or less equivalent to ShapeEditor and in the future
+ * it might handle all shapes. Handles XML commit of actions that affect all paths or
+ * the node selection and removes PathManipulators that have no nodes left after e.g. node
+ * deletes.
+ * - ControlPointSelection: keeps track of node selection and a set of nodes that can potentially
+ * be selected. There can be more than one selection. Performs actions that require no
+ * knowledge about the path, only about the nodes, like dragging and transforms. It is not
+ * specific to nodes and can accommodate any control point derived from SelectableControlPoint.
+ * Transforms nodes in response to transform handle events.
+ * - TransformHandleSet: displays nodeset transform handles and emits transform events. The aim
+ * is to eventually use a common class for object and control point transforms.
+ * - SelectableControlPoint: base for any type of selectable point. It can belong to only one
+ * selection.
+ *
+ * @par Functionality that resides in weird places
+ * @par
+ *
+ * This list is probably incomplete.
+ * - Curve dragging: CurveDragPoint, controlled by PathManipulator
+ * - Single handle shortcuts: MultiPathManipulator::event(), ModifierTracker
+ * - Linear and spatial grow: Node, spatial grow routed to ControlPointSelection
+ * - Committing handle actions performed with the mouse: PathManipulator
+ * - Sculpting: ControlPointSelection
+ *
+ * @par Plans for the future
+ * @par
+ * - MultiPathManipulator should become a generic shape editor that manages all active manipulator,
+ * more or less like the old ShapeEditor.
+ * - Knotholder should be rewritten into one manipulator class per shape, using the control point
+ * classes. Interesting features like dragging rectangle sides could be added along the way.
+ * - Better handling of clip and mask editing, particularly in response to undo.
+ * - High level refactoring of the event context hierarchy. All aspects of tools, like toolbox
+ * controls, icons, event handling should be collected in one class, though each aspect
+ * of a tool might be in an separate class for better modularity. The long term goal is to allow
+ * tools to be defined in extensions or shared library plugins.
+ */
+
+
+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;
+
+ // selector has to be created here, so that its hidden control point is on the bottom
+ this->_selector = new Inkscape::UI::Selector(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->_selector->signal_point.connect(sigc::mem_fun(this, &NodeTool::select_point));
+ this->_selector->signal_area.connect(sigc::mem_fun(this, &NodeTool::select_area));
+
+ this->_multipath->signal_coords_changed.connect([=](){
+ 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->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;
+ delete this->_selector;
+
+ Inkscape::UI::PathSharedData &data = *this->_path_data;
+ delete data.node_data.node_group;
+ delete data.node_data.handle_group;
+ delete data.node_data.handle_line_group;
+ delete data.outline_group;
+ delete data.dragpoint_group;
+ delete _transform_handle_group;
+}
+
+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) {
+ SPLPEItem *lpeitem = dynamic_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());
+ auto c = std::make_unique<SPCurve>();
+ 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(), 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->selection);
+ } else if (entry_name == "edit_masks") {
+ this->edit_masks = value.getBool();
+ this->selection_changed(_desktop->selection);
+ } else {
+ ToolBase::set(value);
+ }
+}
+
+/** Recursively collect ShapeRecords */
+static
+void gather_items(NodeTool *nt, SPItem *base, SPObject *obj, Inkscape::UI::ShapeRole role,
+ std::set<Inkscape::UI::ShapeRecord> &s)
+{
+ using namespace Inkscape::UI;
+
+ if (!obj) {
+ return;
+ }
+
+ //XML Tree being used directly here while it shouldn't be.
+ if (role != SHAPE_ROLE_NORMAL && (SP_IS_GROUP(obj) || SP_IS_OBJECTGROUP(obj))) {
+ for (auto& c: obj->children) {
+ gather_items(nt, base, &c, role, s);
+ }
+ } else if (SP_IS_ITEM(obj)) {
+ SPObject *object = obj;
+ SPItem *item = dynamic_cast<SPItem *>(obj);
+ ShapeRecord r;
+ r.object = object;
+ // TODO add support for objectBoundingBox
+ r.edit_transform = base ? base->i2doc_affine() : Geom::identity();
+ r.role = role;
+
+ if (s.insert(r).second) {
+ // this item was encountered the first time
+ if (nt->edit_clipping_paths) {
+ gather_items(nt, item, item->getClipObject(), SHAPE_ROLE_CLIPPING_PATH, s);
+ }
+
+ if (nt->edit_masks) {
+ gather_items(nt, item, item->getMaskObject(), SHAPE_ROLE_MASK, s);
+ }
+ }
+ }
+}
+
+void NodeTool::selection_changed(Inkscape::Selection *sel) {
+ using namespace Inkscape::UI;
+
+ std::set<ShapeRecord> shapes;
+
+ auto items= sel->items();
+ for(auto i=items.begin();i!=items.end();++i){
+ SPItem *item = *i;
+ 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 = dynamic_cast<SPObject *>(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(SP_ITEM(r.object)) == this->_shape_editors.end()) {
+ auto si = std::make_unique<ShapeEditor>(_desktop, r.edit_transform);
+ SPItem *item = SP_ITEM(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->selection;
+ static Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (this->_multipath->event(this, event)) {
+ return true;
+ }
+
+ if (this->_selector->event(this, event)) {
+ return true;
+ }
+
+ if (this->_selected_nodes->event(this, event)) {
+ return true;
+ }
+
+ switch (event->type)
+ {
+
+ case GDK_MOTION_NOTIFY: {
+ sp_update_helperpath(_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));
+
+ 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->selection->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")) {
+ // We want to reset flashed item to can highligh again previous one
+ if (!over_item && this->flashed_item) {
+ this->flashed_item = nullptr;
+ break;
+ }
+ if (!over_item || over_item == this->flashed_item) {
+ break;
+ }
+
+ if (!prefs->getBool("/tools/nodes/pathflash_selected") && over_item && selection->includes(over_item)) {
+ break;
+ }
+
+ if (this->flash_tempitem) {
+ _desktop->remove_temporary_canvasitem(this->flash_tempitem);
+ this->flash_tempitem = nullptr;
+ this->flashed_item = nullptr;
+ }
+
+ auto shape = dynamic_cast<SPShape const *>(over_item);
+ if (!shape) {
+ break; // for now, handle only shapes
+ }
+
+ this->flashed_item = over_item;
+ auto c = SPCurve::copy(shape->curveForEdit());
+
+ if (!c) {
+ break; // break out when curve doesn't exist
+ }
+
+ c->transform(over_item->i2dt_affine());
+
+ auto flash = new Inkscape::CanvasItemBpath(_desktop->getCanvasTemp(), c.get(), 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_RELEASE:
+ if (this->_selector->doubleClicked()) {
+ // If the selector received the doubleclick event, then we're at some distance from
+ // the path; otherwise, the doubleclick event would have been received by
+ // CurveDragPoint; we will insert nodes into the path anyway but only if we can snap
+ // to the path. Otherwise the position would not be very well defined.
+ if (!(event->motion.state & GDK_SHIFT_MASK)) {
+ Geom::Point const motion_w(event->motion.x, event->motion.y);
+ Geom::Point const motion_dt(_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()));
+ }
+ }
+ }
+ 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);
+}
+
+void NodeTool::update_tip(GdkEvent *event) {
+ using namespace Inkscape::UI;
+ if (event && (event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE)) {
+ unsigned new_state = state_after_event(event);
+
+ if (new_state == event->key.state) {
+ return;
+ }
+
+ if (state_held_shift(new_state)) {
+ if (this->_last_over) {
+ this->message_context->set(Inkscape::NORMAL_MESSAGE,
+ C_("Node tool tip", "<b>Shift</b>: drag to add nodes to the selection, "
+ "click to toggle object selection"));
+ } else {
+ this->message_context->set(Inkscape::NORMAL_MESSAGE,
+ C_("Node tool tip", "<b>Shift</b>: drag to add nodes to the selection"));
+ }
+
+ return;
+ }
+ }
+
+ unsigned sz = this->_selected_nodes->size();
+ unsigned total = this->_selected_nodes->allPoints().size();
+
+ if (sz != 0) {
+ // TODO: Use Glib::ustring::compose and remove the useless copy after string freeze
+ char *nodestring_temp = g_strdup_printf(
+ ngettext("<b>%u of %u</b> node selected.", "<b>%u of %u</b> nodes selected.", total),
+ sz, total);
+ Glib::ustring nodestring(nodestring_temp);
+ g_free(nodestring_temp);
+
+ if (sz == 2) {
+ // if there are only two nodes selected, display the angle
+ // of a line going through them relative to the X axis.
+ Inkscape::UI::ControlPointSelection::Set &selection_nodes = this->_selected_nodes->allPoints();
+ std::vector<Geom::Point> positions;
+ for (auto selection_node : selection_nodes) {
+ if (selection_node->selected()) {
+ Inkscape::UI::Node *n = dynamic_cast<Inkscape::UI::Node *>(selection_node);
+ positions.push_back(n->position());
+ }
+ }
+ g_assert(positions.size() == 2);
+ const double angle = Geom::deg_from_rad(Geom::Line(positions[0], positions[1]).angle());
+ nodestring += " ";
+ nodestring += Glib::ustring::compose(_("Angle: %1°."),
+ Glib::ustring::format(std::fixed, std::setprecision(2), angle));
+ }
+
+ if (this->_last_over) {
+ // TRANSLATORS: The %s below is where the "%u of %u nodes selected" sentence gets put
+ char *dyntip = g_strdup_printf(C_("Node tool tip",
+ "%s Drag to select nodes, click to edit only this object (more: Shift)"),
+ nodestring.c_str());
+ this->message_context->set(Inkscape::NORMAL_MESSAGE, dyntip);
+ g_free(dyntip);
+ } else {
+ char *dyntip = g_strdup_printf(C_("Node tool tip",
+ "%s Drag to select nodes, click clear the selection"),
+ nodestring.c_str());
+ this->message_context->set(Inkscape::NORMAL_MESSAGE, dyntip);
+ g_free(dyntip);
+ }
+ } else if (!this->_multipath->empty()) {
+ if (this->_last_over) {
+ this->message_context->set(Inkscape::NORMAL_MESSAGE, C_("Node tool tip",
+ "Drag to select nodes, click to edit only this object"));
+ } else {
+ this->message_context->set(Inkscape::NORMAL_MESSAGE, C_("Node tool tip",
+ "Drag to select nodes, click to clear the selection"));
+ }
+ } else {
+ if (this->_last_over) {
+ this->message_context->set(Inkscape::NORMAL_MESSAGE, C_("Node tool tip",
+ "Drag to select objects to edit, click to edit this object (more: Shift)"));
+ } else {
+ this->message_context->set(Inkscape::NORMAL_MESSAGE, C_("Node tool tip",
+ "Drag to select objects to edit"));
+ }
+ }
+}
+
+/**
+ * @param sel Area in desktop coordinates
+ */
+void NodeTool::select_area(Geom::Rect const &sel, GdkEventButton *event) {
+ using namespace Inkscape::UI;
+
+ if (this->_multipath->empty()) {
+ // if multipath is empty, select rubberbanded items rather than nodes
+ Inkscape::Selection *selection = _desktop->selection;
+ auto sel_doc = _desktop->dt2doc() * sel;
+ 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(sel, true);
+ } else {
+ // A/B/C. Adds nodes under box to existing selection.
+ this->_selected_nodes->selectArea(sel);
+ if (ctrl) {
+ // C. Selects the inverse of all nodes under the box.
+ this->_selected_nodes->invertSelection();
+ }
+ }
+ }
+}
+
+void NodeTool::select_point(Geom::Point const &/*sel*/, GdkEventButton *event) {
+ using namespace Inkscape::UI; // pull in event helpers
+
+ if (!event) {
+ return;
+ }
+
+ if (event->button != 1) {
+ return;
+ }
+
+ Inkscape::Selection *selection = _desktop->selection;
+
+ 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 {
+ 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..8a4fbcc
--- /dev/null
+++ b/src/ui/tools/node-tool.h
@@ -0,0 +1,110 @@
+// 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;
+ }
+}
+
+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;
+ void deleteSelected();
+private:
+ 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::Rect const &sel, GdkEventButton *event);
+ void select_point(Geom::Point const &sel, GdkEventButton *event);
+ void mouseover_changed(Inkscape::UI::ControlPoint *p);
+ void update_tip(GdkEvent *event);
+ void handleControlUiStyleChange();
+};
+void sp_update_helperpath(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..095ca96
--- /dev/null
+++ b/src/ui/tools/pages-tool.cpp
@@ -0,0 +1,593 @@
+// 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/widget/canvas.h"
+
+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->selection->setBackup();
+ desktop->selection->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));
+ 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"));
+ }
+ resize_knots.push_back(knot);
+ }
+ }
+
+ if (!visual_box) {
+ visual_box = new Inkscape::CanvasItemRect(desktop->getCanvasControls());
+ visual_box->set_stroke(0x0000ff7f);
+ visual_box->hide();
+ }
+ if (!drag_group) {
+ drag_group = new Inkscape::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->selection->restoreBackup();
+
+ if (visual_box) {
+ delete visual_box;
+ visual_box = nullptr;
+ }
+
+ for (auto knot : resize_knots) {
+ delete knot;
+ }
+ resize_knots.clear();
+
+ if (drag_group) {
+ delete drag_group;
+ drag_group = nullptr;
+ 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::resizeKnotMoved(SPKnot *knot, Geom::Point const &ppointer, guint state)
+{
+ Geom::Rect rect;
+
+ 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());
+ }
+
+ 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 = Geom::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) {
+ if (!page || page->isViewportPage()) {
+ // Adjust viewport so it's scroll adjustment is correct
+ *on_screen_rect *= document->dt2doc();
+ }
+ document->getPageManager().fitToRect(*on_screen_rect, page);
+ Inkscape::DocumentUndo::done(document, "Resize page", INKSCAPE_ICON("tool-pages"));
+ on_screen_rect = {};
+ }
+ visual_box->hide();
+ mouse_is_pressed = false;
+}
+
+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);
+ } 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->dt2doc();
+ document->fitToRect(*rect * affine, 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_CORNER, -1);
+ snap_manager.snapprefs.setTargetMask(SNAPTARGET_ALIGNMENT_PAGE_CENTER, -1);
+ snap_manager.snapprefs.setTargetMask(SNAPTARGET_PAGE_CORNER, -1);
+ snap_manager.snapprefs.setTargetMask(SNAPTARGET_PAGE_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, 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) {
+ delete shape;
+ }
+ 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();
+ }
+ }
+
+ // 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()));
+ });
+ resizeKnotSet(*(doc->preferredBounds()));
+ }
+ }
+}
+
+void PagesTool::pageModified(SPObject *object, guint /*flags*/)
+{
+ if (auto page = dynamic_cast<SPPage *>(object)) {
+ resizeKnotSet(page->getDesktopRect());
+ }
+}
+
+} // 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..29c44d9
--- /dev/null
+++ b/src/ui/tools/pages-tool.h
@@ -0,0 +1,91 @@
+// 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"
+
+#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 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;
+ SPPage *highlight_item = nullptr;
+ SPPage *dragging_item = nullptr;
+ std::optional<Geom::Rect> on_screen_rect;
+ Inkscape::CanvasItemRect *visual_box = nullptr;
+ Inkscape::CanvasItemGroup *drag_group = nullptr;
+ std::vector<Inkscape::CanvasItemBpath *> drag_shapes;
+
+ std::vector<Inkscape::SnapCandidatePoint> _bbox_points;
+};
+
+} // 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..d85a3b7
--- /dev/null
+++ b/src/ui/tools/pen-tool.cpp
@@ -0,0 +1,2027 @@
+// 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"
+
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+static Geom::Point pen_drag_origin_w(0, 0);
+static bool pen_within_tolerance = false;
+const double HANDLE_CUBIC_GAP = 0.001;
+
+PenTool::PenTool(SPDesktop *desktop, std::string prefs_path, const std::string &cursor_filename)
+ : FreehandBase(desktop, prefs_path, cursor_filename)
+{
+ tablet_enabled = false;
+
+ // Pen indicators (temporary handles shown when adding a new node).
+ c0 = new Inkscape::CanvasItemCtrl(desktop->getCanvasControls(), Inkscape::CANVAS_ITEM_CTRL_TYPE_ROTATE);
+ c1 = new Inkscape::CanvasItemCtrl(desktop->getCanvasControls(), Inkscape::CANVAS_ITEM_CTRL_TYPE_ROTATE);
+ c0->set_fill(0x0);
+ c1->set_fill(0x0);
+ c0->hide();
+ c1->hide();
+
+ cl0 = new Inkscape::CanvasItemCurve(desktop->getCanvasControls());
+ cl1 = new Inkscape::CanvasItemCurve(desktop->getCanvasControls());
+ 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);
+ }
+ }
+
+ if (this->c0) {
+ delete c0;
+ }
+ if (this->c1) {
+ delete c1;
+ }
+
+ if (this->cl0) {
+ delete cl0;
+ }
+ if (this->cl1) {
+ delete cl1;
+ }
+
+ 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->num_clicks = 0;
+ this->state = PenTool::STOP;
+ this->_resetColors();
+ c0->hide();
+ c1->hide();
+ cl0->hide();
+ cl1->hide();
+ this->message_context->clear();
+ this->message_context->flash(Inkscape::NORMAL_MESSAGE, _("Drawing cancelled"));
+}
+
+/**
+ * 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
+
+ this->sa = anchor;
+ if (anchor) {
+ //Put the start overwrite curve always on the same direction
+ if (anchor->start) {
+ this->sa_overwrited = this->sa->curve->create_reverse();
+ } else {
+ this->sa_overwrited = this->sa->curve->copy();
+ }
+ this->_bsplineSpiroStartAnchor(bevent.state & GDK_SHIFT_MASK);
+ }
+ if (anchor && (!this->hasWaitingLPE()|| this->bspline || this->spiro)) {
+ // Adjust point to anchor if needed; if we have a waiting LPE, we need
+ // a fresh path to be created so don't continue an existing one
+ p = anchor->dp;
+ _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Continuing selected path"));
+ } else {
+ // This is the first click of a new curve; deselect item so that
+ // this curve is not combined with it (unless it is drawn from its
+ // anchor, which is handled by the sibling branch above)
+ Inkscape::Selection * const selection = _desktop->getSelection();
+ if (!(bevent.state & GDK_SHIFT_MASK) || this->hasWaitingLPE()) {
+ // if we have a waiting LPE, we need a fresh path to be created
+ // so don't append to an existing one
+ selection->clear();
+ _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Creating new path"));
+ } else if (selection->singleItem() && SP_IS_PATH(selection->singleItem())) {
+ _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Appending to selected path"));
+ }
+
+ // Create green anchor
+ p = event_dt;
+ this->_endpointSnap(p, bevent.state);
+ this->green_anchor.reset(new SPDrawAnchor(this, this->green_curve.get(), 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){
+ c1->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){
+ c1->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(SP_PATH(selection->singleItem()));
+ selection->add(this->waiting_item);
+ this->waiting_LPE = nullptr;
+ } else {
+ // the case that we need to create a new LPE and apply it to the just-drawn path is
+ // handled in spdc_check_for_and_apply_waiting_LPE() in draw-context.cpp
+ }
+ }
+
+ return ret;
+}
+
+bool PenTool::_handle2ButtonPress(GdkEventButton const &bevent) {
+ bool ret = false;
+ // only end on LMB double click. Otherwise horizontal scrolling causes ending of the path
+ if (this->npoints != 0 && bevent.button == 1 && this->state != PenTool::CLOSE) {
+ this->_finish(false);
+ ret = true;
+ }
+ return ret;
+}
+
+void PenTool::_redrawAll() {
+ // green
+ if (! this->green_bpaths.empty()) {
+ // remove old piecewise green canvasitems
+ for (auto path : this->green_bpaths) {
+ delete path;
+ }
+ this->green_bpaths.clear();
+
+ // one canvas bpath for all of green_curve
+ auto canvas_shape = new Inkscape::CanvasItemBpath(_desktop->getCanvasSketch(), green_curve.get(), true);
+ canvas_shape->set_stroke(green_color);
+ canvas_shape->set_fill(0x0, SP_WIND_RULE_NONZERO);
+ this->green_bpaths.push_back(canvas_shape);
+ }
+ if (this->green_anchor) {
+ this->green_anchor->ctrl->set_position(this->green_anchor->dp);
+ }
+
+ this->red_curve->reset();
+ this->red_curve->moveto(this->p[0]);
+ this->red_curve->curveto(this->p[1], this->p[2], this->p[3]);
+ red_bpath->set_bpath(red_curve.get(), true);
+
+ // handles
+ // hide the handlers in bspline and spiro modes
+ if (this->p[0] != this->p[1] && !this->spiro && !this->bspline) {
+ c1->set_position(p[1]);
+ c1->show();
+ cl1->set_coords(p[0], p[1]);
+ cl1->show();
+ } else {
+ c1->hide();
+ 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];
+ c0->set_position(p2);
+ c0->show();
+ cl0->set_coords(p2, p[0]);
+ cl0->show();
+ } else {
+ c0->hide();
+ 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_unique<SPCurve>();
+ previous->moveto(A);
+ previous->curveto(B, C, D);
+ if ( this->green_curve->get_segment_count() == 1) {
+ this->green_curve = std::move(previous);
+ } else {
+ //we eliminate the last segment
+ this->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_unique<SPCurve>();
+ Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const *>( this->green_curve->last_segment() );
+ if ( cubic ) {
+ A = this->green_curve->last_segment()->initialPoint();
+ B = (*cubic)[1];
+ C = *this->green_curve->last_point();
+ D = C;
+ } else {
+ //We obtain the last segment 4 points in the previous curve
+ A = this->green_curve->last_segment()->initialPoint();
+ B = A;
+ C = *this->green_curve->last_point();
+ D = C;
+ }
+ previous->moveto(A);
+ previous->curveto(B, C, D);
+ if( this->green_curve->get_segment_count() == 1){
+ this->green_curve = std::move(previous);
+ }else{
+ //we eliminate the last segment
+ this->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(true);
+ }
+ }
+
+ this->p[1] = this->p[0];
+ this->_redrawAll();
+}
+
+
+bool PenTool::_handleKeyPress(GdkEvent *event) {
+ bool ret = false;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ gdouble const nudge = prefs->getDoubleLimited("/options/nudgedistance/value", 2, 0, 1000, "px"); // in px
+
+ // Check for undo if we have started drawing a path. User might have change shortcut.
+ if (this->npoints > 0) {
+ Gtk::AccelKey shortcut = Inkscape::Shortcuts::get_from_event((GdkEventKey*)event);
+ auto app = InkscapeApplication::instance();
+ auto gapp = app->gtk_app();
+ auto actions = gapp->get_actions_for_accel(shortcut.get_abbrev());
+ if (std::find(actions.begin(), actions.end(), "win.undo") != actions.end()) {
+ return _undoLastPoint();
+ }
+ }
+
+ switch (get_latin_keyval (&event->key)) {
+ case GDK_KEY_Left: // move last point left
+ case GDK_KEY_KP_Left:
+ if (!MOD__CTRL(event)) { // not ctrl
+ if (MOD__ALT(event)) { // alt
+ if (MOD__SHIFT(event)) {
+ this->_lastpointMoveScreen(-10, 0); // shift
+ }
+ else {
+ this->_lastpointMoveScreen(-1, 0); // no shift
+ }
+ }
+ else { // no alt
+ if (MOD__SHIFT(event)) {
+ this->_lastpointMove(-10*nudge, 0); // shift
+ }
+ else {
+ this->_lastpointMove(-nudge, 0); // no shift
+ }
+ }
+ ret = true;
+ }
+ break;
+ case GDK_KEY_Up: // move last point up
+ case GDK_KEY_KP_Up:
+ if (!MOD__CTRL(event)) { // not ctrl
+ if (MOD__ALT(event)) { // alt
+ if (MOD__SHIFT(event)) {
+ this->_lastpointMoveScreen(0, 10); // shift
+ }
+ else {
+ this->_lastpointMoveScreen(0, 1); // no shift
+ }
+ }
+ else { // no alt
+ if (MOD__SHIFT(event)) {
+ this->_lastpointMove(0, 10*nudge); // shift
+ }
+ else {
+ this->_lastpointMove(0, nudge); // no shift
+ }
+ }
+ ret = true;
+ }
+ break;
+ case GDK_KEY_Right: // move last point right
+ case GDK_KEY_KP_Right:
+ if (!MOD__CTRL(event)) { // not ctrl
+ if (MOD__ALT(event)) { // alt
+ if (MOD__SHIFT(event)) {
+ this->_lastpointMoveScreen(10, 0); // shift
+ }
+ else {
+ this->_lastpointMoveScreen(1, 0); // no shift
+ }
+ }
+ else { // no alt
+ if (MOD__SHIFT(event)) {
+ this->_lastpointMove(10*nudge, 0); // shift
+ }
+ else {
+ this->_lastpointMove(nudge, 0); // no shift
+ }
+ }
+ ret = true;
+ }
+ break;
+ case GDK_KEY_Down: // move last point down
+ case GDK_KEY_KP_Down:
+ if (!MOD__CTRL(event)) { // not ctrl
+ if (MOD__ALT(event)) { // alt
+ if (MOD__SHIFT(event)) {
+ this->_lastpointMoveScreen(0, -10); // shift
+ }
+ else {
+ this->_lastpointMoveScreen(0, -1); // no shift
+ }
+ }
+ else { // no alt
+ if (MOD__SHIFT(event)) {
+ this->_lastpointMove(0, -10*nudge); // shift
+ }
+ else {
+ this->_lastpointMove(0, -nudge); // no shift
+ }
+ }
+ ret = true;
+ }
+ break;
+
+/*TODO: this is not yet enabled?? looks like some traces of the Geometry tool
+ case GDK_KEY_P:
+ case GDK_KEY_p:
+ if (MOD__SHIFT_ONLY(event)) {
+ sp_pen_context_wait_for_LPE_mouse_clicks(pc, Inkscape::LivePathEffect::PARALLEL, 2);
+ ret = true;
+ }
+ break;
+
+ case GDK_KEY_C:
+ case GDK_KEY_c:
+ if (MOD__SHIFT_ONLY(event)) {
+ sp_pen_context_wait_for_LPE_mouse_clicks(pc, Inkscape::LivePathEffect::CIRCLE_3PTS, 3);
+ ret = true;
+ }
+ break;
+
+ case GDK_KEY_B:
+ case GDK_KEY_b:
+ if (MOD__SHIFT_ONLY(event)) {
+ sp_pen_context_wait_for_LPE_mouse_clicks(pc, Inkscape::LivePathEffect::PERP_BISECTOR, 2);
+ ret = true;
+ }
+ break;
+
+ case GDK_KEY_A:
+ case GDK_KEY_a:
+ if (MOD__SHIFT_ONLY(event)) {
+ sp_pen_context_wait_for_LPE_mouse_clicks(pc, Inkscape::LivePathEffect::ANGLE_BISECTOR, 3);
+ ret = true;
+ }
+ break;
+*/
+
+ case GDK_KEY_U:
+ case GDK_KEY_u:
+ if (MOD__SHIFT_ONLY(event)) {
+ this->_lastpointToCurve();
+ ret = true;
+ }
+ break;
+ case GDK_KEY_L:
+ case GDK_KEY_l:
+ if (MOD__SHIFT_ONLY(event)) {
+ this->_lastpointToLine();
+ ret = true;
+ }
+ break;
+
+ case GDK_KEY_Return:
+ case GDK_KEY_KP_Enter:
+ if (this->npoints != 0) {
+ this->ea = nullptr; // unset end anchor if set (otherwise crashes)
+ if(MOD__SHIFT_ONLY(event)) {
+ // All this is needed to stop the last control
+ // point dispeating and stop making an n-1 shape.
+ Geom::Point const p(0, 0);
+ if(this->red_curve->is_unset()) {
+ this->red_curve->moveto(p);
+ }
+ this->_finishSegment(p, 0);
+ this->_finish(true);
+ } else {
+ this->_finish(false);
+ }
+ ret = true;
+ }
+ break;
+ case GDK_KEY_Escape:
+ if (this->npoints != 0) {
+ // if drawing, cancel, otherwise pass it up for deselecting
+ this->_cancel ();
+ ret = true;
+ }
+ break;
+ case GDK_KEY_g:
+ case GDK_KEY_G:
+ if (MOD__SHIFT_ONLY(event)) {
+ _desktop->selection->toGuides();
+ ret = true;
+ }
+ break;
+ case GDK_KEY_BackSpace:
+ case GDK_KEY_Delete:
+ case GDK_KEY_KP_Delete:
+ ret = _undoLastPoint();
+ break;
+ case GDK_KEY_Z:
+ case GDK_KEY_z:
+ if (event->key.state & INK_GDK_PRIMARY_MASK) {
+ ret = _undoLastPoint();
+ }
+ break;
+ default:
+ break;
+ }
+ return ret;
+}
+
+void PenTool::_resetColors() {
+ // Red
+ this->red_curve->reset();
+ this->red_bpath->set_bpath(nullptr);
+
+ // Blue
+ this->blue_curve->reset();
+ this->blue_bpath->set_bpath(nullptr);
+
+ // Green
+ for (auto path : this->green_bpaths) {
+ delete path;
+ }
+ 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
+ for (auto path : this->green_bpaths) {
+ delete path;
+ }
+ this->green_bpaths.clear();
+
+ // one canvas bpath for all of green_curve
+ auto canvas_shape = new Inkscape::CanvasItemBpath(_desktop->getCanvasSketch(), green_curve.get(), true);
+ canvas_shape->set_stroke(green_color);
+ canvas_shape->set_fill(0x0, SP_WIND_RULE_NONZERO);
+ this->green_bpaths.push_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()){
+ using Geom::X;
+ using Geom::Y;
+ this->npoints = 5;
+ this->p[0] = *this->red_curve->first_point();
+ this->p[3] = this->red_curve->first_segment()->finalPoint();
+ this->p[2] = this->p[3] + (1./3)*(this->p[0] - this->p[3]);
+ this->p[2] = Geom::Point(this->p[2][X] + HANDLE_CUBIC_GAP,this->p[2][Y] + HANDLE_CUBIC_GAP);
+ }
+}
+
+void PenTool::_bsplineSpiroOff()
+{
+ if(!this->red_curve->is_unset()){
+ this->npoints = 5;
+ this->p[0] = *this->red_curve->first_point();
+ this->p[3] = this->red_curve->first_segment()->finalPoint();
+ this->p[2] = this->p[3];
+ }
+}
+
+void PenTool::_bsplineSpiroStartAnchor(bool shift)
+{
+ if(this->sa->curve->is_unset()){
+ return;
+ }
+
+ LivePathEffect::LPEBSpline *lpe_bsp = nullptr;
+
+ if (SP_IS_LPE_ITEM(this->white_item) && SP_LPE_ITEM(this->white_item)->hasPathEffect()){
+ Inkscape::LivePathEffect::Effect *thisEffect =
+ SP_LPE_ITEM(this->white_item)->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 (SP_IS_LPE_ITEM(this->white_item) && SP_LPE_ITEM(this->white_item)->hasPathEffect()){
+ Inkscape::LivePathEffect::Effect *thisEffect =
+ SP_LPE_ITEM(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_unique<SPCurve>();
+ Geom::Point point_a = this->sa_overwrited->last_segment()->initialPoint();
+ Geom::Point point_d = *this->sa_overwrited->last_point();
+ Geom::Point point_c = point_d + (1./3)*(point_a - point_d);
+ point_c = Geom::Point(point_c[X] + HANDLE_CUBIC_GAP, point_c[Y] + HANDLE_CUBIC_GAP);
+ if(cubic){
+ last_segment->moveto(point_a);
+ last_segment->curveto((*cubic)[1],point_c,point_d);
+ }else{
+ last_segment->moveto(point_a);
+ last_segment->curveto(point_a,point_c,point_d);
+ }
+ if( this->sa_overwrited->get_segment_count() == 1){
+ this->sa_overwrited = 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_unique<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;
+ std::unique_ptr<SPCurve> tmp_curve(new SPCurve());
+ this->p[2] = this->p[3] + (1./3)*(this->p[0] - this->p[3]);
+ this->p[2] = Geom::Point(this->p[2][X] + HANDLE_CUBIC_GAP,this->p[2][Y] + HANDLE_CUBIC_GAP);
+ if (this->green_curve->is_unset() && !this->sa) {
+ this->p[1] = this->p[0] + (1./3)*(this->p[3] - this->p[0]);
+ this->p[1] = Geom::Point(this->p[1][X] + HANDLE_CUBIC_GAP, this->p[1][Y] + HANDLE_CUBIC_GAP);
+ if(shift){
+ this->p[2] = this->p[3];
+ }
+ } else if (!this->green_curve->is_unset()){
+ tmp_curve = this->green_curve->copy();
+ } else {
+ tmp_curve = this->sa_overwrited->copy();
+ }
+ if ((state & GDK_MOD1_MASK ) && previous != Geom::Point(0,0)) { //ALT drag
+ this->p[0] = this->p[0] + (this->p[3] - previous);
+ }
+ if(!tmp_curve ->is_unset()){
+ Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*tmp_curve ->last_segment());
+ if ((state & GDK_MOD1_MASK ) &&
+ !Geom::are_near(*tmp_curve ->last_point(), this->p[0], 0.1))
+ {
+ auto previous_weight_power = std::make_unique<SPCurve>();
+ Geom::D2< Geom::SBasis > SBasisweight_power;
+ previous_weight_power->moveto(tmp_curve ->last_segment()->initialPoint());
+ previous_weight_power->lineto(this->p[0]);
+ SBasisweight_power = previous_weight_power->first_segment()->toSBasis();
+ if( tmp_curve ->get_segment_count() == 1){
+ Geom::Point initial = tmp_curve ->last_segment()->initialPoint();
+ tmp_curve->reset();
+ tmp_curve->moveto(initial);
+ }else{
+ tmp_curve->backspace();
+ }
+ if(this->bspline && cubic && !Geom::are_near((*cubic)[2],(*cubic)[3])){
+ tmp_curve->curveto(SBasisweight_power.valueAt(0.33334), SBasisweight_power.valueAt(0.66667), this->p[0]);
+ } else if(this->bspline && cubic) {
+ tmp_curve->curveto(SBasisweight_power.valueAt(0.33334), this->p[0], this->p[0]);
+ } else if (cubic && !Geom::are_near((*cubic)[2],(*cubic)[3])) {
+ tmp_curve->curveto((*cubic)[1], (*cubic)[2] + (this->p[3] - previous), this->p[0]);
+ } else if (cubic){
+ tmp_curve->curveto((*cubic)[1], this->p[0], this->p[0]);
+ } else {
+ tmp_curve->lineto(this->p[0]);
+ }
+ cubic = dynamic_cast<Geom::CubicBezier const*>(&*tmp_curve ->last_segment());
+ if (this->sa && this->green_curve->is_unset()) {
+ this->sa_overwrited = tmp_curve->copy();
+ }
+ this->green_curve = tmp_curve->copy();
+ }
+ if (cubic) {
+ if (this->bspline) {
+ auto weight_power = std::make_unique<SPCurve>();
+ Geom::D2< Geom::SBasis > SBasisweight_power;
+ weight_power->moveto(this->red_curve->last_segment()->initialPoint());
+ weight_power->lineto(*this->red_curve->last_point());
+ SBasisweight_power = weight_power->first_segment()->toSBasis();
+ this->p[1] = SBasisweight_power.valueAt(0.33334);
+ if(!Geom::are_near(this->p[1],this->p[0])){
+ this->p[1] = Geom::Point(this->p[1][X] + HANDLE_CUBIC_GAP,this->p[1][Y] + HANDLE_CUBIC_GAP);
+ } else {
+ this->p[1] = this->p[0];
+ }
+ if (shift) {
+ this->p[2] = this->p[3];
+ }
+ if(Geom::are_near((*cubic)[3], (*cubic)[2])) {
+ this->p[1] = this->p[0];
+ }
+ } else {
+ this->p[1] = (*cubic)[3] + ((*cubic)[3] - (*cubic)[2] );
+ }
+ } else {
+ this->p[1] = this->p[0];
+ if (shift) {
+ this->p[2] = this->p[3];
+ }
+ }
+ previous = *this->red_curve->last_point();
+ auto red = std::make_unique<SPCurve>();
+ red->moveto(this->p[0]);
+ red->curveto(this->p[1],this->p[2],this->p[3]);
+ this->red_bpath->set_bpath(red.get(), true);
+ }
+
+ if(this->anchor_statusbar && !this->red_curve->is_unset()){
+ if(shift){
+ this->_bsplineSpiroEndAnchorOff();
+ }else{
+ this->_bsplineSpiroEndAnchorOn();
+ }
+ }
+
+ // remove old piecewise green canvasitems
+ for (auto path: this->green_bpaths) {
+ delete path;
+ }
+ this->green_bpaths.clear();
+
+ // one canvas bpath for all of green_curve
+ auto canvas_shape = new Inkscape::CanvasItemBpath(_desktop->getCanvasSketch(), green_curve.get(), true);
+ canvas_shape->set_stroke(green_color);
+ canvas_shape->set_fill(0x0, SP_WIND_RULE_NONZERO);
+ this->green_bpaths.push_back(canvas_shape);
+
+ this->_bsplineSpiroBuild();
+}
+
+void PenTool::_bsplineSpiroEndAnchorOn()
+{
+
+ using Geom::X;
+ using Geom::Y;
+ this->p[2] = this->p[3] + (1./3)*(this->p[0] - this->p[3]);
+ this->p[2] = Geom::Point(this->p[2][X] + HANDLE_CUBIC_GAP,this->p[2][Y] + HANDLE_CUBIC_GAP);
+ std::unique_ptr<SPCurve> tmp_curve(new SPCurve());
+ std::unique_ptr<SPCurve> last_segment(new SPCurve());
+ Geom::Point point_c(0,0);
+ if( this->green_anchor && this->green_anchor->active ){
+ tmp_curve = this->green_curve->create_reverse();
+ if(this->green_curve->get_segment_count()==0){
+ return;
+ }
+ } else if(this->sa){
+ tmp_curve = this->sa_overwrited->create_reverse();
+ }else{
+ return;
+ }
+ Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*tmp_curve ->last_segment());
+ if(this->bspline){
+ point_c = *tmp_curve ->last_point() + (1./3)*(tmp_curve ->last_segment()->initialPoint() - *tmp_curve ->last_point());
+ point_c = Geom::Point(point_c[X] + HANDLE_CUBIC_GAP, point_c[Y] + HANDLE_CUBIC_GAP);
+ }else{
+ point_c = this->p[3] + this->p[3] - this->p[2];
+ }
+ if(cubic){
+ last_segment->moveto((*cubic)[0]);
+ last_segment->curveto((*cubic)[1],point_c,(*cubic)[3]);
+ }else{
+ last_segment->moveto(tmp_curve ->last_segment()->initialPoint());
+ last_segment->lineto(*tmp_curve ->last_point());
+ }
+ if( tmp_curve ->get_segment_count() == 1){
+ tmp_curve = std::move(last_segment);
+ }else{
+ //we eliminate the last segment
+ tmp_curve ->backspace();
+ //and we add it again with the recreation
+ tmp_curve ->append_continuous(*last_segment);
+ }
+ tmp_curve = tmp_curve->create_reverse();
+ if( this->green_anchor && this->green_anchor->active )
+ {
+ this->green_curve->reset();
+ this->green_curve = std::move(tmp_curve);
+ }else{
+ this->sa_overwrited->reset();
+ this->sa_overwrited = std::move(tmp_curve);
+ }
+}
+
+void PenTool::_bsplineSpiroEndAnchorOff()
+{
+
+ std::unique_ptr<SPCurve> tmp_curve(new SPCurve());
+ std::unique_ptr<SPCurve> last_segment(new SPCurve());
+ this->p[2] = this->p[3];
+ if( this->green_anchor && this->green_anchor->active ){
+ tmp_curve = this->green_curve->create_reverse();
+ if(this->green_curve->get_segment_count()==0){
+ return;
+ }
+ } else if(this->sa){
+ tmp_curve = this->sa_overwrited->create_reverse();
+ }else{
+ return;
+ }
+ Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*tmp_curve ->last_segment());
+ if(cubic){
+ last_segment->moveto((*cubic)[0]);
+ last_segment->curveto((*cubic)[1],(*cubic)[3],(*cubic)[3]);
+ }else{
+ last_segment->moveto(tmp_curve ->last_segment()->initialPoint());
+ last_segment->lineto(*tmp_curve ->last_point());
+ }
+ if( tmp_curve ->get_segment_count() == 1){
+ tmp_curve = std::move(last_segment);
+ }else{
+ //we eliminate the last segment
+ tmp_curve ->backspace();
+ //and we add it again with the recreation
+ tmp_curve ->append_continuous(*last_segment);
+ }
+ tmp_curve = tmp_curve->create_reverse();
+
+ if( this->green_anchor && this->green_anchor->active )
+ {
+ this->green_curve->reset();
+ this->green_curve = std::move(tmp_curve);
+ }else{
+ this->sa_overwrited->reset();
+ this->sa_overwrited = std::move(tmp_curve);
+ }
+}
+
+//prepares the curves for its transformation into BSpline curve.
+void PenTool::_bsplineSpiroBuild()
+{
+ if(!this->spiro && !this->bspline){
+ return;
+ }
+
+ //We create the base curve
+ auto curve = std::make_unique<SPCurve>();
+ //If we continuate the existing curve we add it at the start
+ if(this->sa && !this->sa->curve->is_unset()){
+ curve = this->sa_overwrited->copy();
+ }
+
+ if (!this->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.get(), 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 (this->bspline) {
+ Geom::PathVector hp;
+ LivePathEffect::sp_bspline_do_effect(curve.get(), 0, hp);
+ } else {
+ LivePathEffect::sp_spiro_do_effect(curve.get());
+ }
+
+ blue_bpath->set_bpath(curve.get(), true);
+ blue_bpath->set_stroke(blue_color);
+ blue_bpath->show();
+
+ this->blue_curve->reset();
+ //We hide the holders that doesn't contribute anything
+ if (this->spiro){
+ c1->set_position(p[0]);
+ c1->show();
+ } else {
+ c1->hide();
+ }
+ c0->hide();
+ 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.get(), 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.
+ c1->show();
+ cl1->show();
+
+ if ( this->npoints == 2 ) {
+ this->p[1] = q;
+ c0->hide();
+ cl0->hide();
+ c1->set_position(p[1]);
+ 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;
+ c0->show();
+ 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.get(), true);
+ }
+ c0->set_position(this->p[2]);
+ cl0->set_coords(this->p[3], this->p[2]);
+ c1->set_position(this->p[4]);
+ 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);
+ }
+
+ ++num_clicks;
+
+
+ if (!this->red_curve->is_unset()) {
+ this->_bsplineSpiro(state & GDK_SHIFT_MASK);
+ if(!this->green_curve->is_unset() &&
+ !Geom::are_near(*this->green_curve->last_point(),this->p[0]))
+ {
+ std::unique_ptr<SPCurve> lsegment(new SPCurve());
+ Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*this->green_curve->last_segment());
+ if (cubic) {
+ lsegment->moveto((*cubic)[0]);
+ lsegment->curveto((*cubic)[1], this->p[0] - ((*cubic)[2] - (*cubic)[3]), *this->red_curve->first_point());
+ this->green_curve->backspace();
+ green_curve->append_continuous(*lsegment);
+ }
+ }
+ this->green_curve->append_continuous(*red_curve);
+ auto curve = this->red_curve->copy();
+
+ /// \todo fixme:
+ auto canvas_shape = new Inkscape::CanvasItemBpath(_desktop->getCanvasSketch(), curve.get(), true);
+ canvas_shape->set_stroke(green_color);
+ canvas_shape->set_fill(0x0, SP_WIND_RULE_NONZERO);
+ this->green_bpaths.push_back(canvas_shape);
+
+ this->p[0] = this->p[3];
+ this->p[1] = this->p[4];
+ this->npoints = 2;
+
+ this->red_curve->reset();
+ }
+}
+
+// Partial fix for https://bugs.launchpad.net/inkscape/+bug/171990
+// TODO: implement the redo feature
+bool PenTool::_undoLastPoint() {
+ bool ret = false;
+
+ if ( this->green_curve->is_unset() || (this->green_curve->last_segment() == nullptr) ) {
+ if (!this->red_curve->is_unset()) {
+ this->_cancel ();
+ ret = true;
+ } else {
+ // do nothing; this event should be handled upstream
+ }
+ } else {
+ // Reset red curve
+ this->red_curve->reset();
+ // Get last segment
+ if ( this->green_curve->is_unset() ) {
+ g_warning("pen_handle_key_press, case GDK_KP_Delete: Green curve is empty");
+ return false;
+ }
+ // The code below assumes that this->green_curve has only ONE path !
+ Geom::Curve const * crv = this->green_curve->last_segment();
+ this->p[0] = crv->initialPoint();
+ if ( Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const *>(crv)) {
+ this->p[1] = (*cubic)[1];
+
+ } else {
+ this->p[1] = this->p[0];
+ }
+
+ // assign the value in a third of the distance of the last segment.
+ if (this->bspline){
+ this->p[1] = this->p[0] + (1./3)*(this->p[3] - this->p[0]);
+ }
+
+ Geom::Point const pt( (this->npoints < 4) ? crv->finalPoint() : this->p[3] );
+
+ this->npoints = 2;
+ // delete the last segment of the green curve and green bpath
+ if (this->green_curve->get_segment_count() == 1) {
+ this->npoints = 5;
+ if (!this->green_bpaths.empty()) {
+ delete this->green_bpaths.back();
+ this->green_bpaths.pop_back();
+ }
+ this->green_curve->reset();
+ } else {
+ this->green_curve->backspace();
+ if (this->green_bpaths.size() > 1) {
+ delete this->green_bpaths.back();
+ 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];
+ c1->set_position(this->p[0]);
+ } else {
+ this->p[1] = this->p[0];
+ }
+ }
+
+ c0->hide();
+ c1->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;
+}
+
+void PenTool::_finish(gboolean const closed) {
+ if (this->expecting_clicks_for_LPE > 1) {
+ // don't let the path be finished before we have collected the required number of mouse clicks
+ return;
+ }
+
+
+ this->num_clicks = 0;
+
+ this->_disableEvents();
+
+ this->message_context->clear();
+
+ _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Drawing finished"));
+
+ // cancelate line without a created segment
+ this->red_curve->reset();
+ spdc_concat_colors_and_flush(this, closed);
+ this->sa = nullptr;
+ this->ea = nullptr;
+
+ this->npoints = 0;
+ this->state = PenTool::POINT;
+
+ c0->hide();
+ c1->hide();
+ cl0->hide();
+ cl1->hide();
+
+ this->green_anchor.reset();
+
+ 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..bd48ce7
--- /dev/null
+++ b/src/ui/tools/pen-tool.h
@@ -0,0 +1,165 @@
+// 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 <sigc++/sigc++.h>
+
+#include "ui/tools/freehand-base.h"
+#include "live_effects/effect.h"
+
+#define SP_PEN_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::PenTool*>((Inkscape::UI::Tools::ToolBase*)obj))
+#define SP_IS_PEN_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::PenTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL)
+
+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?
+ int num_clicks = 0;;
+
+ 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;
+
+ Inkscape::CanvasItemCtrl *c0 = nullptr; // Start point of path.
+ Inkscape::CanvasItemCtrl *c1 = nullptr; // End point of path.
+
+ Inkscape::CanvasItemCurve *cl0 = nullptr;
+ Inkscape::CanvasItemCurve *cl1 = nullptr;
+
+ 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();
+
+ 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;
+};
+
+}
+}
+}
+
+#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..055f581
--- /dev/null
+++ b/src/ui/tools/pencil-tool.cpp
@@ -0,0 +1,1189 @@
+// 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; }
+const double HANDLE_CUBIC_GAP = 0.01;
+
+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)
+ , _pressure_curve(nullptr)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/tools/freehand/pencil/selcue")) {
+ this->enableSelectionCue();
+ }
+ this->_pressure_curve = std::make_unique<SPCurve>();
+ 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) {
+ this->sa_overwrited = anchor->curve->create_reverse();
+ } else {
+ this->sa_overwrited = anchor->curve->copy();
+ }
+ _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Continuing selected path"));
+ } else {
+ m.setup(_desktop, true);
+ if (tablet_enabled) {
+ // This is the first click of a new curve; deselect item so that
+ // this curve is not combined with it (unless it is drawn from its
+ // anchor, which is handled by the sibling branch above)
+ selection->clear();
+ _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Creating new path"));
+ } else if (!(bevent.state & GDK_SHIFT_MASK)) {
+ // This is the first click of a new curve; deselect item so that
+ // this curve is not combined with it (unless it is drawn from its
+ // anchor, which is handled by the sibling branch above)
+ selection->clear();
+ _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Creating new path"));
+ m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_NODE_HANDLE);
+ } else if (selection->singleItem() && SP_IS_PATH(selection->singleItem())) {
+ _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Appending to selected path"));
+ m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_NODE_HANDLE);
+ }
+ m.unSetup();
+ }
+ if (!tablet_enabled) {
+ this->sa = anchor;
+ }
+ this->_setStartpoint(p);
+ ret = true;
+ break;
+ }
+
+ set_high_motion_precision();
+ this->_is_drawing = true;
+ }
+ return ret;
+}
+
+bool PencilTool::_handleMotionNotify(GdkEventMotion const &mevent) {
+ if ((mevent.state & GDK_CONTROL_MASK) && (mevent.state & GDK_BUTTON1_MASK)) {
+ // mouse was accidentally moved during Ctrl+click;
+ // ignore the motion and create a single point
+ this->_is_drawing = false;
+ return true;
+ }
+ bool ret = false;
+
+ if ((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 ( !this->sa && !this->green_anchor ) {
+ /* Create green anchor */
+ this->green_anchor.reset(new SPDrawAnchor(this, this->green_curve.get(), 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 (SP_IS_LPE_ITEM(item)) {
+ Effect* lpe = SP_LPE_ITEM(item)->getCurrentLPE();
+ if (lpe) {
+ LPEPowerStroke* ps = static_cast<LPEPowerStroke*>(lpe);
+ if (ps) {
+ _desktop->selection->clear();
+ _desktop->selection->add(item);
+ }
+ }
+ }
+ }
+ }
+ break;
+ case SP_PENCIL_CONTEXT_ADDLINE:
+ /* Finish segment now */
+ if (anchor) {
+ p = anchor->dp;
+ } else {
+ this->_endpointSnap(p, revent.state);
+ }
+ this->ea = anchor;
+ this->_setEndpoint(p);
+ this->_finishEndpoint();
+ this->_state = SP_PENCIL_CONTEXT_IDLE;
+ 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) {
+ this->_addFreehandPoint(p_end, revent.state, true);
+ this->_pressure_curve->reset();
+ } else {
+ this->_endpointSnap(p_end, revent.state);
+ if (p_end != p) {
+ // then we must have snapped!
+ this->_addFreehandPoint(p_end, revent.state, true);
+ }
+ }
+ }
+
+ this->ea = anchor;
+ /* Write curves to object */
+ _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Finishing freehand"));
+ this->_interpolate();
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (tablet_enabled) {
+ gint shapetype = prefs->getInt("/tools/freehand/pencil/shape", 0);
+ gint simplify = prefs->getInt("/tools/freehand/pencil/simplify", 0);
+ gint mode = prefs->getInt("/tools/freehand/pencil/freehand-mode", 0);
+ prefs->setInt("/tools/freehand/pencil/shape", 0);
+ prefs->setInt("/tools/freehand/pencil/simplify", 0);
+ prefs->setInt("/tools/freehand/pencil/freehand-mode", 0);
+ spdc_concat_colors_and_flush(this, FALSE);
+ prefs->setInt("/tools/freehand/pencil/freehand-mode", mode);
+ prefs->setInt("/tools/freehand/pencil/simplify", simplify);
+ prefs->setInt("/tools/freehand/pencil/shape", shapetype);
+ } else {
+ spdc_concat_colors_and_flush(this, FALSE);
+ }
+ this->points.clear();
+ this->sa = nullptr;
+ this->ea = nullptr;
+ this->ps.clear();
+ this->_wps.clear();
+ 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.get());
+
+ for (auto path : this->green_bpaths) {
+ delete path;
+ }
+ 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->selection->toGuides();
+ ret = true;
+ }
+ break;
+ case GDK_KEY_Alt_L:
+ case GDK_KEY_Alt_R:
+ case GDK_KEY_Meta_L:
+ case GDK_KEY_Meta_R:
+ if (this->_state == SP_PENCIL_CONTEXT_IDLE) {
+ _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.get());
+ }
+ }
+}
+
+/**
+ * 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);
+ auto curvepressure = std::make_unique<SPCurve>();
+ int const n_segs = Geom::bezier_fit_cubic_r(b.data(), this->ps.data(), n_points, tolerance_sq, max_segs);
+ if (n_segs > 0) {
+ /* Fit and draw and reset state */
+ curvepressure->moveto(b[0]);
+ for (int c = 0; c < n_segs; c++) {
+ curvepressure->curveto(b[4 * c + 1], b[4 * c + 2], b[4 * c + 3]);
+ }
+ }
+ Geom::Affine transform_coordinate = currentLayer()->i2dt_affine().inverse();
+ curvepressure->transform(transform_coordinate);
+ Geom::Path path = curvepressure->get_pathvector()[0];
+
+ if (!path.empty()) {
+ Inkscape::XML::Document *xml_doc = document->getReprDoc();
+ Inkscape::XML::Node *pp = nullptr;
+ pp = xml_doc->createElement("svg:path");
+ pp->setAttribute("d", sp_svg_write_path(path));
+ pp->setAttribute("id", "power_stroke_preview");
+ Inkscape::GC::release(pp);
+
+ SPShape *powerpreview = SP_SHAPE(currentLayer()->appendChildRepr(pp));
+ SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(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 = this->_pressure_curve->get_pathvector();
+ if (!pressure_path.empty() && !previous_presure.empty()) {
+ pressure_path = sp_pathvector_boolop(pressure_path, previous_presure, bool_op_union, fill_nonZero, fill_nonZero);
+ }
+ this->_pressure_curve->set_pathvector(pressure_path);
+ red_bpath->set_bpath(_pressure_curve.get());
+ }
+ 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]);
+ point_at1 = Geom::Point(point_at1[X] + HANDLE_CUBIC_GAP, point_at1[Y] + HANDLE_CUBIC_GAP);
+ Geom::Point point_at2 = b[4 * c + 3] + (1./3) * (b[4 * c + 0] - b[4 * c + 3]);
+ point_at2 = Geom::Point(point_at2[X] + HANDLE_CUBIC_GAP, point_at2[Y] + HANDLE_CUBIC_GAP);
+ this->green_curve->curveto(point_at1,point_at2,b[4*c+3]);
+ } else {
+ if (!tablet_enabled || c != n_segs - 1) {
+ this->green_curve->curveto(b[4 * c + 1], b[4 * c + 2], b[4 * c + 3]);
+ } else {
+ 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]);
+ point_at1 = Geom::Point(point_at1[X] + HANDLE_CUBIC_GAP, point_at1[Y] + HANDLE_CUBIC_GAP);
+ Geom::Point point_at2 = b[3] + (1./3)*(b[0] - b[3]);
+ point_at2 = Geom::Point(point_at2[X] + HANDLE_CUBIC_GAP, point_at2[Y] + HANDLE_CUBIC_GAP);
+ this->red_curve->curveto(point_at1,point_at2,b[3]);
+ }else{
+ this->red_curve->curveto(b[1], b[2], b[3]);
+ }
+ if (!tablet_enabled) {
+ red_bpath->set_bpath(red_curve.get());
+ }
+ 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);
+ auto curve = this->red_curve->copy();
+
+ /// \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(), curve.get(), true);
+ cshape->set_stroke(green_color);
+ cshape->set_fill(0x0, SP_WIND_RULE_NONZERO);
+
+ this->green_bpaths.push_back(cshape);
+
+ this->red_curve_is_valid = false;
+ }
+}
+
+}
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/tools/pencil-tool.h b/src/ui/tools/pencil-tool.h
new file mode 100644
index 0000000..e9443ea
--- /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;
+ std::unique_ptr<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..cbbbbab
--- /dev/null
+++ b/src/ui/tools/rect-tool.cpp
@@ -0,0 +1,465 @@
+// 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"
+
+#include "xml/node-event-vector.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 (!this->within_tolerance) {
+ // we've been dragging, finish the rect
+ this->finishItem();
+ } else if (this->item_to_select) {
+ // no dragging, select clicked item if any
+ if (event->button.state & GDK_SHIFT_MASK) {
+ selection->toggle(this->item_to_select);
+ } else {
+ selection->set(this->item_to_select);
+ }
+ } else {
+ // click in an empty space
+ selection->clear();
+ }
+
+ this->item_to_select = nullptr;
+ ret = TRUE;
+ 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->selection->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 = SP_RECT(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..0e80c3d
--- /dev/null
+++ b/src/ui/tools/select-tool.cpp
@@ -0,0 +1,1146 @@
+// 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/cursor-utils.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->selection,
+ 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;
+ SPGroup *current_group = dynamic_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 (dynamic_cast<SPGroup *>(clicked_item) && !dynamic_cast<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));
+
+ {
+ SPGroup *selGroup = dynamic_cast<SPGroup *>(selection->single());
+ if (selGroup && (selGroup->layerMode() == SPGroup::LAYER)) {
+ group_at_point = selGroup;
+ }
+ }
+
+ // group-at-point is meant to be topmost item if it's a group,
+ // not topmost group of all items at point
+ if (group_at_point != item_in_group &&
+ !(group_at_point && item_at_point &&
+ group_at_point->isAncestorOf(item_at_point))) {
+ group_at_point = nullptr;
+ }
+
+ // if neither a group nor an item (possibly in a group) at point are selected, set selection to the item at point
+ if ((!item_in_group || !selection->includes(item_in_group)) &&
+ (!group_at_point || !selection->includes(group_at_point)) && !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->scroll_to_point(p);
+ 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();
+ SPGroup *singleGroup = dynamic_cast<SPGroup *>(single);
+ // without shift, increase state (i.e. toggle scale/rotation handles)
+ if (selection->includes(this->item)) {
+ _seltrans->increaseState();
+ } else if (singleGroup && (singleGroup->layerMode() == SPGroup::LAYER) && single->isAncestorOf(this->item)) {
+ _seltrans->increaseState();
+ } else {
+ _seltrans->resetState();
+ selection->set(this->item);
+ }
+ }
+ } else { // simple or shift click, no previous selection
+ _seltrans->resetState();
+ selection->set(this->item);
+ }
+ }
+
+ this->dragging = FALSE;
+
+ auto window = _desktop->getCanvas()->get_window();
+
+ 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 (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 (get_latin_keyval (&event->key)) {
+ case GDK_KEY_Left: // move selection left
+ case GDK_KEY_KP_Left:
+ if (!MOD__CTRL(event)) { // not ctrl
+ gint mul = 1 + gobble_key_events( get_latin_keyval(&event->key), 0); // with any mask
+
+ if (MOD__ALT(event)) { // alt
+ if (MOD__SHIFT(event)) {
+ _desktop->getSelection()->moveScreen(mul*-10, 0); // shift
+ } else {
+ _desktop->getSelection()->moveScreen(mul*-1, 0); // no shift
+ }
+ } else { // no alt
+ if (MOD__SHIFT(event)) {
+ _desktop->getSelection()->move(mul*-10*nudge, 0); // shift
+ } else {
+ _desktop->getSelection()->move(mul*-nudge, 0); // no shift
+ }
+ }
+
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Up: // move selection up
+ case GDK_KEY_KP_Up:
+ if (!MOD__CTRL(event)) { // not ctrl
+ gint mul = 1 + gobble_key_events(get_latin_keyval(&event->key), 0); // with any mask
+ mul *= -y_dir;
+
+ if (MOD__ALT(event)) { // alt
+ if (MOD__SHIFT(event)) {
+ _desktop->getSelection()->moveScreen(0, mul*10); // shift
+ } else {
+ _desktop->getSelection()->moveScreen(0, mul*1); // no shift
+ }
+ } else { // no alt
+ if (MOD__SHIFT(event)) {
+ _desktop->getSelection()->move(0, mul*10*nudge); // shift
+ } else {
+ _desktop->getSelection()->move(0, mul*nudge); // no shift
+ }
+ }
+
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Right: // move selection right
+ case GDK_KEY_KP_Right:
+ if (!MOD__CTRL(event)) { // not ctrl
+ gint mul = 1 + gobble_key_events(get_latin_keyval(&event->key), 0); // with any mask
+
+ if (MOD__ALT(event)) { // alt
+ if (MOD__SHIFT(event)) {
+ _desktop->getSelection()->moveScreen(mul*10, 0); // shift
+ } else {
+ _desktop->getSelection()->moveScreen(mul*1, 0); // no shift
+ }
+ } else { // no alt
+ if (MOD__SHIFT(event)) {
+ _desktop->getSelection()->move(mul*10*nudge, 0); // shift
+ } else {
+ _desktop->getSelection()->move(mul*nudge, 0); // no shift
+ }
+ }
+
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Down: // move selection down
+ case GDK_KEY_KP_Down:
+ if (!MOD__CTRL(event)) { // not ctrl
+ gint mul = 1 + gobble_key_events(get_latin_keyval(&event->key), 0); // with any mask
+ mul *= -y_dir;
+
+ if (MOD__ALT(event)) { // alt
+ if (MOD__SHIFT(event)) {
+ _desktop->getSelection()->moveScreen(0, mul*-10); // shift
+ } else {
+ _desktop->getSelection()->moveScreen(0, mul*-1); // no shift
+ }
+ } else { // no alt
+ if (MOD__SHIFT(event)) {
+ _desktop->getSelection()->move(0, mul*-10*nudge); // shift
+ } else {
+ _desktop->getSelection()->move(0, mul*-nudge); // no shift
+ }
+ }
+
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Escape:
+ if (!this->sp_select_context_abort()) {
+ selection->clear();
+ }
+
+ ret = TRUE;
+ break;
+
+ case GDK_KEY_a:
+ case GDK_KEY_A:
+ if (MOD__CTRL_ONLY(event)) {
+ sp_edit_select_all(_desktop);
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_space:
+ /* stamping mode: show outline mode moving */
+ /* FIXME: Is next condition ok? (lauris) */
+ if (this->dragging && this->grabbed) {
+ _seltrans->stamp();
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_x:
+ case GDK_KEY_X:
+ if (MOD__ALT_ONLY(event)) {
+ _desktop->setToolboxFocusTo("select-x");
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_bracketleft:
+ if (MOD__ALT(event)) {
+ gint mul = 1 + gobble_key_events(get_latin_keyval(&event->key), 0); // with any mask
+ selection->rotateScreen(-mul * y_dir);
+ } else if (MOD__CTRL(event)) {
+ selection->rotate(-90 * y_dir);
+ } else if (snaps) {
+ selection->rotate(-180.0/snaps * y_dir);
+ }
+
+ ret = TRUE;
+ break;
+
+ case GDK_KEY_bracketright:
+ if (MOD__ALT(event)) {
+ gint mul = 1 + gobble_key_events(get_latin_keyval(&event->key), 0); // with any mask
+ selection->rotateScreen(mul * y_dir);
+ } else if (MOD__CTRL(event)) {
+ selection->rotate(90 * y_dir);
+ } else if (snaps) {
+ selection->rotate(180.0/snaps * y_dir);
+ }
+
+ ret = TRUE;
+ break;
+
+ case GDK_KEY_Return:
+ if (MOD__CTRL_ONLY(event)) {
+ if (selection->singleItem()) {
+ SPItem *clicked_item = selection->singleItem();
+ SPGroup *clickedGroup = dynamic_cast<SPGroup *>(clicked_item);
+ if ( (clickedGroup && (clickedGroup->layerMode() != SPGroup::LAYER)) || dynamic_cast<SPBox3D *>(clicked_item)) { // enter group or a 3D box
+ _desktop->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->selection->toGuides();
+ ret = true;
+ }
+ break;
+
+ default:
+ break;
+ }
+ break;
+ }
+ case GDK_KEY_RELEASE: {
+ guint keyval = get_latin_keyval(&event->key);
+ if (key_is_a_modifier (keyval)) {
+ this->defaultMessageContext()->clear();
+ }
+
+ bool alt = ( MOD__ALT(event)
+ || (keyval == GDK_KEY_Alt_L)
+ || (keyval == GDK_KEY_Alt_R)
+ || (keyval == GDK_KEY_Meta_L)
+ || (keyval == GDK_KEY_Meta_R));
+
+ if (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;
+}
+
+}
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 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..51ed41a
--- /dev/null
+++ b/src/ui/tools/select-tool.h
@@ -0,0 +1,79 @@
+// 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;
+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..f79eab7
--- /dev/null
+++ b/src/ui/tools/spiral-tool.cpp
@@ -0,0 +1,411 @@
+// 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"
+
+#include "xml/node-event-vector.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 (!this->within_tolerance) {
+ // we've been dragging, finish the spiral
+ this->finishItem();
+ } else if (this->item_to_select) {
+ // no dragging, select clicked item if any
+ if (event->button.state & GDK_SHIFT_MASK) {
+ selection->toggle(this->item_to_select);
+ } else {
+ selection->set(this->item_to_select);
+ }
+ } else {
+ // click in an empty space
+ selection->clear();
+ }
+
+ this->item_to_select = nullptr;
+ ret = TRUE;
+ 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 = SP_SPIRAL(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..705b129
--- /dev/null
+++ b/src/ui/tools/spray-tool.cpp
@@ -0,0 +1,1538 @@
+// 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"
+
+
+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)
+ , dilate_area(nullptr)
+ , no_overlap(false)
+ , picker(false)
+ , pick_center(true)
+ , pick_inverse_value(false)
+ , pick_fill(false)
+ , pick_stroke(false)
+ , pick_no_overlap(false)
+ , over_transparent(true)
+ , over_no_transparent(true)
+ , offset(0)
+ , pick(0)
+ , do_trace(false)
+ , pick_to_size(false)
+ , pick_to_presence(false)
+ , pick_to_color(false)
+ , pick_to_opacity(false)
+ , invert_picked(false)
+ , gamma_picked(0)
+ , rand_picked(0)
+{
+ dilate_area = new Inkscape::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();
+
+ if (this->dilate_area) {
+ delete this->dilate_area;
+ this->dilate_area = nullptr;
+ }
+}
+
+void SprayTool::update_cursor(bool /*with_shift*/) {
+ guint num = 0;
+ gchar *sel_message = nullptr;
+
+ if (!_desktop->selection->isEmpty()) {
+ num = (guint)boost::distance(_desktop->selection->items());
+ sel_message = g_strdup_printf(ngettext("<b>%i</b> object selected","<b>%i</b> objects selected",num), num);
+ } else {
+ sel_message = g_strdup_printf("%s", _("<b>Nothing</b> selected"));
+ }
+
+ switch (this->mode) {
+ case SPRAY_MODE_COPY:
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag, click or click and scroll to spray <b>copies</b> of the initial selection."), sel_message);
+ break;
+ case SPRAY_MODE_CLONE:
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag, click or click and scroll to spray <b>clones</b> of the initial selection."), sel_message);
+ break;
+ case SPRAY_MODE_SINGLE_PATH:
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag, click or click and scroll to spray in a <b>single path</b> of the initial selection."), sel_message);
+ break;
+ default:
+ break;
+ }
+ 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();
+
+ // Ensure drawing up-to-date. (Is this really necessary?)
+ drawing->update();
+
+ // Get average color.
+ double R, G, B, A;
+ drawing->average_color(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.
+ SPBox3D *box = dynamic_cast<SPBox3D *>(item);
+ if (box) {
+ desktop->getSelection()->remove(dynamic_cast<SPObject *>(item));
+ set->remove(item);
+ item = box->convert_to_group();
+ set->add(item);
+ desktop->getSelection()->add(dynamic_cast<SPObject *>(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 = dynamic_cast<SPItem *>(new_obj); // Conversion object->item
+ sp_spray_scale_rel(center,desktop, item_copied, Geom::Scale(_scale));
+ sp_spray_scale_rel(center,desktop, item_copied, Geom::Scale(scale));
+ sp_spray_rotate_rel(center,desktop,item_copied, Geom::Rotate(angle));
+ // Move the cursor p
+ item_copied->move_rel(Geom::Translate(move * desktop->doc2dt().withoutTranslation()));
+ Inkscape::GC::release(copy);
+ if(picker){
+ sp_desktop_apply_css_recursive(item_copied, css, true);
+ }
+ did = true;
+ }
+ }
+#ifdef ENABLE_SPRAY_MODE_SINGLE_PATH
+ } else if (mode == SPRAY_MODE_SINGLE_PATH) {
+ 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);
+ SPItem *item_copied = dynamic_cast<SPItem *>(new_obj);
+
+ // Move around the cursor
+ Geom::Point move = (Geom::Point(cos(tilt)*cos(dp)*dr/(1-ratio)+sin(tilt)*sin(dp)*dr/(1+ratio), -sin(tilt)*cos(dp)*dr/(1-ratio)+cos(tilt)*sin(dp)*dr/(1+ratio)))+(p-a->midpoint());
+
+ Geom::Point center = 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 (dynamic_cast<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 = dynamic_cast<SPItem *>(clone_object);
+ sp_spray_scale_rel(center, desktop, item_copied, Geom::Scale(_scale, _scale));
+ sp_spray_scale_rel(center, desktop, item_copied, Geom::Scale(scale, scale));
+ sp_spray_rotate_rel(center, desktop, item_copied, Geom::Rotate(angle));
+ item_copied->move_rel(Geom::Translate(move * desktop->doc2dt().withoutTranslation()));
+ if(picker){
+ sp_desktop_apply_css_recursive(item_copied, css, true);
+ }
+ Inkscape::GC::release(clone);
+ did = true;
+ }
+ }
+ }
+
+ return did;
+}
+
+static bool sp_spray_dilate(SprayTool *tc, Geom::Point /*event_p*/, Geom::Point p, Geom::Point vector, bool reverse)
+{
+ SPDesktop *desktop = tc->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->selection->isEmpty()) {
+ num = (guint)boost::distance(_desktop->selection->items());
+ }
+ if (num == 0) {
+ this->message_context->flash(Inkscape::ERROR_MESSAGE, _("<b>Nothing selected!</b> Select objects to spray."));
+ }
+
+ // Dilating:
+ if (this->is_drawing && ( event->motion.state & GDK_BUTTON1_MASK )) {
+ sp_spray_dilate(this, motion_w, motion_doc, motion_doc - this->last_push, event->button.state & GDK_SHIFT_MASK? true : false);
+ //this->last_push = motion_doc;
+ this->has_dilated = true;
+
+ // It's slow, so prevent clogging up with events
+ gobble_motion_events(GDK_BUTTON1_MASK);
+ return TRUE;
+ }
+ }
+ break;
+ /* Spray with the scroll */
+ case GDK_SCROLL: {
+ if (event->scroll.state & GDK_BUTTON1_MASK) {
+ double temp ;
+ temp = this->population;
+ this->population = 1.0;
+ _desktop->setToolboxAdjustmentValue("population", this->population * 100);
+ Geom::Point const scroll_w(event->button.x, event->button.y);
+ Geom::Point const scroll_dt = _desktop->point();;
+
+ switch (event->scroll.direction) {
+ case GDK_SCROLL_DOWN:
+ case GDK_SCROLL_UP:
+ case GDK_SCROLL_SMOOTH: {
+ if (Inkscape::have_viable_layer(_desktop, defaultMessageContext()) == false) {
+ return TRUE;
+ }
+ this->last_push = _desktop->dt2doc(scroll_dt);
+ sp_spray_extinput(this, event);
+ 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..8216e2d
--- /dev/null
+++ b/src/ui/tools/spray-tool.h
@@ -0,0 +1,147 @@
+// 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"
+
+#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;
+ Inkscape::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..bbdd9cb
--- /dev/null
+++ b/src/ui/tools/star-tool.cpp
@@ -0,0 +1,430 @@
+// 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"
+
+#include "xml/node-event-vector.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 (!this->within_tolerance) {
+ // we've been dragging, finish the star
+ this->finishItem();
+ } else if (this->item_to_select) {
+ // no dragging, select clicked item if any
+ if (event->button.state & GDK_SHIFT_MASK) {
+ selection->toggle(this->item_to_select);
+ } else {
+ selection->set(this->item_to_select);
+ }
+ } else {
+ // click in an empty space
+ selection->clear();
+ }
+
+ this->item_to_select = nullptr;
+ ret = TRUE;
+ 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 = SP_STAR(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..12f143e
--- /dev/null
+++ b/src/ui/tools/text-tool.cpp
@@ -0,0 +1,1933 @@
+// 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/node-event-vector.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 = new Inkscape::CanvasItemCurve(desktop->getCanvasControls());
+ cursor->set_stroke(0x000000ff);
+ cursor->hide();
+
+ // The rectangle box tightly wrapping text object when selected or under cursor.
+ indicator = new Inkscape::CanvasItemRect(desktop->getCanvasControls());
+ indicator->set_stroke(0x0000ff7f);
+ indicator->set_shadow(0xffffff7f, 1);
+ indicator->hide();
+
+ // The shape that the text is flowing into
+ frame = new Inkscape::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 = new Inkscape::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 && (SP_IS_FLOWTEXT(item) || SP_IS_TEXT(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;
+ }
+
+ if (cursor) {
+ delete cursor;
+ cursor = nullptr;
+ }
+
+ if (this->indicator) {
+ delete indicator;
+ this->indicator = nullptr;
+ }
+
+ if (this->frame) {
+ delete frame;
+ this->frame = nullptr;
+ }
+
+ if (this->padding_frame) {
+ delete padding_frame;
+ this->padding_frame = nullptr;
+ }
+
+ for (auto & text_selection_quad : text_selection_quads) {
+ text_selection_quad->hide();
+ delete text_selection_quad;
+ }
+ 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 (SP_IS_TEXT(item_ungrouped) || SP_IS_FLOWTEXT(item_ungrouped)) {
+ _desktop->getSelection()->set(item_ungrouped);
+ if (this->text) {
+ // find out click point in document coordinates
+ Geom::Point p = _desktop->w2d(Geom::Point(event->button.x, event->button.y));
+ // set the cursor closest to that point
+ if (event->button.state & GDK_SHIFT_MASK) {
+ this->text_sel_start = old_start;
+ this->text_sel_end = sp_te_get_position_by_coords(this->text, p);
+ } else {
+ this->text_sel_start = this->text_sel_end = sp_te_get_position_by_coords(this->text, p);
+ }
+ // update display
+ sp_text_context_update_cursor(this);
+ sp_text_context_update_text_selection(this);
+ this->dragging = 1;
+ }
+ ret = TRUE;
+ }
+ }
+ break;
+ case GDK_2BUTTON_PRESS:
+ if (event->button.button == 1 && this->text && this->dragging) {
+ Inkscape::Text::Layout const *layout = te_get_layout(this->text);
+ if (layout) {
+ if (!layout->isStartOfWord(this->text_sel_start))
+ this->text_sel_start.prevStartOfWord();
+ if (!layout->isEndOfWord(this->text_sel_end))
+ this->text_sel_end.nextEndOfWord();
+ sp_text_context_update_cursor(this);
+ sp_text_context_update_text_selection(this);
+ this->dragging = 2;
+ ret = TRUE;
+ }
+ }
+ break;
+ case GDK_3BUTTON_PRESS:
+ if (event->button.button == 1 && this->text && this->dragging) {
+ this->text_sel_start.thisStartOfLine();
+ this->text_sel_end.thisEndOfLine();
+ sp_text_context_update_cursor(this);
+ sp_text_context_update_text_selection(this);
+ this->dragging = 3;
+ ret = TRUE;
+ }
+ break;
+ case GDK_BUTTON_RELEASE:
+ if (event->button.button == 1 && this->dragging) {
+ this->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);
+ SPItem *text_item = SP_ITEM(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 (SP_IS_TEXT(item_ungrouped) || SP_IS_FLOWTEXT(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 (SP_IS_TEXT(item_ungrouped)) {
+ _desktop->event_context->defaultMessageContext()->set(
+ Inkscape::NORMAL_MESSAGE,
+ _("<b>Click</b> to edit the text, <b>drag</b> to select part of the text."));
+ } else {
+ _desktop->event_context->defaultMessageContext()->set(
+ Inkscape::NORMAL_MESSAGE,
+ _("<b>Click</b> to edit the flowed text, <b>drag</b> to select part of the text."));
+ }
+ this->over_text = true;
+ } else {
+ // 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>
+ }
+
+ SPText* text_element = dynamic_cast<SPText*>(text);
+ if (text_element && (text_element->has_shape_inside() || text_element->has_inline_size())) {
+ // Handle new line like any other character.
+ this->text_sel_start = this->text_sel_end = sp_te_insert(this->text, this->text_sel_start, "\n");
+ } else {
+ // Replace new line by either <tspan sodipodi:role="line" or <flowPara>.
+ iterator_pair enter_pair;
+ bool success = sp_te_delete(this->text, this->text_sel_start, this->text_sel_end, enter_pair);
+ (void)success; // TODO cleanup
+ this->text_sel_start = this->text_sel_end = enter_pair.first;
+ this->text_sel_start = this->text_sel_end = sp_te_insert_line(this->text, this->text_sel_start);
+ }
+
+ sp_text_context_update_cursor(this);
+ sp_text_context_update_text_selection(this);
+ DocumentUndo::done(_desktop->getDocument(), _("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;
+ SPText *textitem = dynamic_cast<SPText *>(tc->text);
+ if (textitem) {
+ is_svg2 = textitem->has_shape_inside() /*|| textitem->has_inline_size()*/; // Do now since hiding messes this up.
+ textitem->hide_shape_inside();
+ }
+
+ SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(tc->text);
+ if (flowtext) {
+ flowtext->fix_overflow_flowregion(false);
+ }
+
+ // Fix for 244940
+ // The XML standard defines the following as valid characters
+ // (Extensible Markup Language (XML) 1.0 (Fourth Edition) paragraph 2.2)
+ // char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
+ // Since what comes in off the paste buffer will go right into XML, clean
+ // the text here.
+ Glib::ustring text(clip_text);
+ Glib::ustring::iterator itr = text.begin();
+ gunichar paste_string_uchar;
+
+ while(itr != text.end())
+ {
+ paste_string_uchar = *itr;
+
+ // Make sure we don't have a control character. We should really check
+ // for the whole range above... Add the rest of the invalid cases from
+ // above if we find additional issues
+ if(paste_string_uchar >= 0x00000020 ||
+ paste_string_uchar == 0x00000009 ||
+ paste_string_uchar == 0x0000000A ||
+ paste_string_uchar == 0x0000000D) {
+ ++itr;
+ } else {
+ itr = text.erase(itr);
+ }
+ }
+
+ if (!tc->text) { // create text if none (i.e. if nascent_object)
+ sp_text_context_setup_text(tc);
+ tc->nascent_object = false; // we don't need it anymore, having created a real <text>
+ }
+
+ // using indices is slow in ustrings. Whatever.
+ Glib::ustring::size_type begin = 0;
+ for ( ; ; ) {
+ Glib::ustring::size_type end = text.find('\n', begin);
+
+ if (end == Glib::ustring::npos || is_svg2) {
+ // Paste everything
+ if (begin != text.length())
+ tc->text_sel_start = tc->text_sel_end = sp_te_replace(tc->text, tc->text_sel_start, tc->text_sel_end, text.substr(begin).c_str());
+ break;
+ }
+
+ // Paste up to new line, add line, repeat.
+ tc->text_sel_start = tc->text_sel_end = sp_te_replace(tc->text, tc->text_sel_start, tc->text_sel_end, text.substr(begin, end - begin).c_str());
+ tc->text_sel_start = tc->text_sel_end = sp_te_insert_line(tc->text, tc->text_sel_start);
+ begin = end + 1;
+ }
+ if (textitem) {
+ textitem->show_shape_inside();
+ }
+ if (flowtext) {
+ flowtext->fix_overflow_flowregion(true);
+ }
+ DocumentUndo::done(ec->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 (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(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.
+ SPText* sptext = dynamic_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 ( SP_IS_STRING(pos_obj) ) {
+ pos_obj = pos_obj->parent; // SPStrings don't have style
+ }
+ styles_list.insert(styles_list.begin(),(SPItem*)pos_obj);
+ }
+
+ int result = sp_desktop_query_style_from_list (styles_list, style, property);
+
+ return result;
+}
+
+static void sp_text_context_validate_cursor_iterators(TextTool *tc)
+{
+ if (tc->text == nullptr)
+ return;
+ Inkscape::Text::Layout const *layout = te_get_layout(tc->text);
+ if (layout) { // undo can change the text length without us knowing it
+ layout->validateIterator(&tc->text_sel_start);
+ layout->validateIterator(&tc->text_sel_end);
+ }
+}
+
+static void sp_text_context_update_cursor(TextTool *tc, bool scroll_to_see)
+{
+ // due to interruptible display, tc may already be destroyed during a display update before
+ // the cursor update (can't do both atomically, alas)
+ if (!tc->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 (SP_IS_TEXT(tc->text)) {
+ Geom::OptRect opt_frame = SP_TEXT(tc->text)->get_frame();
+ if (opt_frame && (!opt_frame->contains(p0))) {
+ scroll = false;
+ }
+ } else if (SP_IS_FLOWTEXT(tc->text)) {
+ SPItem *frame = SP_FLOWTEXT(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, 1.0);
+ else
+ desktop->scroll_to_point(d1, 1.0);
+ }
+ }
+
+ 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;
+ Shape *exclusion_shape = nullptr;
+ double padding = 0.0;
+
+ // Frame around text
+ if (SP_IS_FLOWTEXT(tc->text)) {
+ SPItem *frame = SP_FLOWTEXT(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 = dynamic_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 = dynamic_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 = dynamic_cast<SPShape const *>(shape_item)) {
+ auto c = SPCurve::copy(shape->curve());
+ if (c) {
+ c->transform(shape->transform);
+ curve.append(*c);
+ }
+ }
+ }
+
+ if (!curve.is_empty()) {
+ bool has_padding = std::fabs(padding) > 1e-12;
+ bool has_exlusions = exclusion_shape;
+
+ if (has_padding || has_exlusions) {
+ // 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, const_cast<Shape*>(exclusion_shape), 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;
+
+ for (auto & text_selection_quad : tc->text_selection_quads) {
+ text_selection_quad->hide();
+ delete text_selection_quad;
+ }
+ tc->text_selection_quads.clear();
+
+ std::vector<Geom::Point> quads;
+ if (tc->text != nullptr)
+ quads = sp_te_create_selection_quads(tc->text, tc->text_sel_start, tc->text_sel_end, (tc->text)->i2dt_affine());
+ for (unsigned i = 0 ; i < quads.size() ; i += 4) {
+ 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.push_back(quad);
+ }
+
+ if (tc->shape_editor != nullptr) {
+ if (tc->shape_editor->knotholder) {
+ tc->shape_editor->knotholder->update_knots();
+ }
+ }
+}
+
+static gint sp_text_context_timeout(TextTool *tc)
+{
+ if (tc->show) {
+ 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 ((SP_IS_TEXT(ti) || SP_IS_FLOWTEXT(ti)) && sp_te_input_is_empty(ti)) {
+ Inkscape::XML::Node *text_repr = ti->getRepr();
+ // the repr may already have been unparented
+ // if we were called e.g. as the result of
+ // an undo or the element being removed from
+ // the XML editor
+ if ( text_repr && text_repr->parent() ) {
+ sp_repr_unparent(text_repr);
+ SPDocumentUndo::done(tc->desktop->getDocument(), _("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()->selection->set (text);
+ tc->text_sel_start = tc->text_sel_end = where;
+ sp_text_context_update_cursor(tc);
+ sp_text_context_update_text_selection(tc);
+}
+
+void sp_text_context_place_cursor_at (TextTool *tc, SPObject *text, Geom::Point const p)
+{
+ tc->getDesktop()->selection->set (text);
+ sp_text_context_place_cursor (tc, text, sp_te_get_position_by_coords(tc->text, p));
+}
+
+Inkscape::Text::Layout::iterator *sp_text_context_get_cursor_position(TextTool *tc, SPObject *text)
+{
+ if (text != tc->text)
+ return nullptr;
+ return &(tc->text_sel_end);
+}
+
+}
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/tools/text-tool.h b/src/ui/tools/text-tool.h
new file mode 100644
index 0000000..12a4cee
--- /dev/null
+++ b/src/ui/tools/text-tool.h
@@ -0,0 +1,121 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __SP_TEXT_CONTEXT_H__
+#define __SP_TEXT_CONTEXT_H__
+
+/*
+ * TextTool
+ *
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ *
+ * Copyright (C) 1999-2005 authors
+ * Copyright (C) 2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <sigc++/connection.h>
+
+#include "ui/tools/tool-base.h"
+#include <2geom/point.h>
+#include "libnrtype/Layout-TNG.h"
+
+#define SP_TEXT_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::TextTool*>((Inkscape::UI::Tools::ToolBase*)obj))
+#define SP_IS_TEXT_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::TextTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL)
+
+typedef struct _GtkIMContext GtkIMContext;
+
+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 ---
+ Inkscape::CanvasItemCurve *cursor = nullptr;
+ Inkscape::CanvasItemRect *indicator = nullptr;
+ Inkscape::CanvasItemBpath *frame = nullptr; // Highlighting flowtext shapes or textpath path
+ Inkscape::CanvasItemBpath *padding_frame = nullptr; // Highlighting flowtext padding
+ std::vector<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..30e3651
--- /dev/null
+++ b/src/ui/tools/tool-base.cpp
@@ -0,0 +1,1649 @@
+// 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/canvas-item-rotate.h"
+#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"
+
+#include "xml/node-event-vector.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 gdouble scroll_multiply = 1;
+static guint scroll_keyval = 0;
+
+// globals for key processing
+static bool latin_keys_group_valid = FALSE;
+static gint latin_keys_group;
+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)
+ : _desktop(desktop)
+ , _prefs_path(std::move(prefs_path))
+ , _cursor_default(std::move(cursor_filename))
+ , _cursor_filename("none")
+ , _uses_snap(uses_snap)
+{
+ pref_observer = new ToolPrefObserver(_prefs_path, this);
+ Inkscape::Preferences::get()->addObserver(*(this->pref_observer));
+ this->set_cursor(_cursor_default);
+ _desktop->getCanvas()->grab_focus();
+
+ message_context = std::unique_ptr<Inkscape::MessageContext>(new 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() {
+ this->enableSelectionCue(false);
+
+ if (this->pref_observer) {
+ delete this->pref_observer;
+ }
+
+ if (this->_delayed_snap_event) {
+ delete this->_delayed_snap_event;
+ }
+}
+
+/**
+ * Called by our pref_observer if a preference has been changed.
+ */
+void ToolBase::set(const Inkscape::Preferences::Entry& /*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 filename)
+{
+ 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);
+ }
+}
+
+/**
+ * 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 gdouble accelerate_scroll(GdkEvent *event, gdouble acceleration)
+{
+ guint32 time_diff = ((GdkEventKey *) event)->time - scroll_event_time;
+
+ /* key pressed within 500ms ? (1/2 second) */
+ if (time_diff > 500 || event->key.keyval != scroll_keyval) {
+ scroll_multiply = 1; // abort acceleration
+ } else {
+ scroll_multiply += acceleration; // continue acceleration
+ }
+
+ scroll_event_time = ((GdkEventKey *) event)->time;
+ scroll_keyval = event->key.keyval;
+
+ return scroll_multiply;
+}
+
+/** Moves the selected points along the supplied unit vector according to
+ * the modifier state of the supplied event. */
+bool ToolBase::_keyboardMove(GdkEventKey const &event, Geom::Point const &dir)
+{
+ if (held_control(event)) return false;
+ unsigned num = 1 + 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");
+ gint ret = FALSE;
+
+ 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 = (gint) event->button.x;
+ yp = (gint) event->button.y;
+ within_tolerance = true;
+
+ button_w = Geom::Point(event->button.x, event->button.y);
+
+ switch (event->button.button) {
+ case 1:
+ // 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 (this->is_space_panning()) {
+ // When starting panning, make sure there are no snap events pending because these might disable the panning again
+ if (_uses_snap) {
+ this->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()) {
+ // On screen canvas rotation preview
+
+ // Grab background before doing anything else
+ _desktop->getCanvasRotate()->start(_desktop);
+ _desktop->getCanvasRotate()->show();
+
+ // CanvasItemRotate code takes over!
+ ungrabCanvasEvents();
+
+ _desktop->getCanvasRotate()->grab(Gdk::KEY_PRESS_MASK |
+ Gdk::KEY_RELEASE_MASK |
+ Gdk::BUTTON_RELEASE_MASK |
+ Gdk::POINTER_MOTION_MASK,
+ nullptr); // Cursor is null.
+
+ } 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) {
+ this->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) {
+ this->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)) {
+ this->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 && (abs((gint) event->motion.x - xp)
+ < tolerance * 3) && (abs((gint) event->motion.y - yp)
+ < tolerance * 3)) {
+ // do not drag if we're within tolerance from origin
+ break;
+ }
+
+ // Once the user has moved farther than tolerance from
+ // the original location (indicating they intend to move
+ // the object, not click), then always process the motion
+ // notify coordinates as given (no snapping back to origin)
+ within_tolerance = false;
+
+ // gobble subsequent motion events to prevent "sticking"
+ // when scrolling is slow
+ gobble_motion_events(panning == 2 ? GDK_BUTTON2_MASK : (panning
+ == 1 ? GDK_BUTTON1_MASK : GDK_BUTTON3_MASK));
+
+ if (panning_cursor == 0) {
+ panning_cursor = 1;
+ auto display = _desktop->getCanvas()->get_display();
+ auto window = _desktop->getCanvas()->get_window();
+ auto cursor = Gdk::Cursor::create(display, "move");
+ window->set_cursor(cursor);
+ }
+
+ Geom::Point const motion_w(event->motion.x, event->motion.y);
+ Geom::Point const moved_w(motion_w - button_w);
+ _desktop->scroll_relative(moved_w, true); // we're still scrolling, do not redraw
+ ret = TRUE;
+ }
+ } else if (zoom_rb) {
+ if (within_tolerance && (abs((gint) event->motion.x - xp)
+ < tolerance) && (abs((gint) event->motion.y - yp)
+ < tolerance)) {
+ break; // do not drag if we're within tolerance from origin
+ }
+
+ // Once the user has moved farther than tolerance from the original location
+ // (indicating they intend to move the object, not click), then always process the
+ // motion notify coordinates as given (no snapping back to origin)
+ within_tolerance = false;
+
+ if (Inkscape::Rubberband::get(_desktop)->is_started()) {
+ 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);
+ } 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.
+ Geom::Point const motion_w(xp, yp);
+ Geom::Point 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);
+ }
+ }
+ 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 (middle_mouse_zoom && within_tolerance && (panning || zoom_rb)) {
+ zoom_rb = 0;
+
+ if (panning) {
+ panning = PANNING_NONE;
+ ungrabCanvasEvents();
+ }
+
+ Geom::Point const event_w(event->button.x, event->button.y);
+ Geom::Point const event_dt(_desktop->w2d(event_w));
+
+ double const zoom_inc = prefs->getDoubleLimited(
+ "/options/zoomincrement/value", M_SQRT2, 1.01, 10);
+
+ _desktop->zoom_relative(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)
+ Geom::Point const motion_w(event->button.x, event->button.y);
+ Geom::Point 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;
+
+ case GDK_KEY_Q:
+ case GDK_KEY_q:
+ if (_desktop->quick_zoomed()) {
+ ret = TRUE;
+ }
+ if (!MOD__SHIFT(event) && !MOD__CTRL(event) && !MOD__ALT(event)) {
+ _desktop->zoom_quick(true);
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_W:
+ case GDK_KEY_w:
+ case GDK_KEY_F4:
+ /* Close view */
+ if (MOD__CTRL_ONLY(event)) {
+ sp_ui_close_view(nullptr);
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Left: // Ctrl Left
+ case GDK_KEY_KP_Left:
+ case GDK_KEY_KP_4:
+ if (MOD__CTRL_ONLY(event)) {
+ int i = (int) floor(key_scroll * accelerate_scroll(event, acceleration));
+
+ 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 = (int) 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 = (int) 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 = (int) 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:
+ this->menu_popup(event);
+ ret = TRUE;
+ break;
+
+ case GDK_KEY_F10:
+ if (MOD__SHIFT_ONLY(event)) {
+ this->menu_popup(event);
+ ret = TRUE;
+ }
+ break;
+
+ case GDK_KEY_space:
+ within_tolerance = true;
+ xp = yp = 0;
+ if (!allow_panning) break;
+ panning = PANNING_SPACE;
+ this->message_context->set(Inkscape::INFORMATION_MESSAGE,
+ _("<b>Space+mouse move</b> to pan canvas"));
+
+ ret = TRUE;
+ break;
+
+ case GDK_KEY_z:
+ case GDK_KEY_Z:
+ if (MOD__ALT_ONLY(event)) {
+ _desktop->zoom_grab_focus();
+ ret = TRUE;
+ }
+ break;
+
+ default:
+ break;
+ }
+ }
+ break;
+
+ case GDK_KEY_RELEASE:
+ // Stop panning on any key release
+ if (this->is_space_panning()) {
+ this->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;
+
+ 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)
+ gdouble delta_x = 0;
+ gdouble 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 = CLAMP(delta_y, -1.0, 1.0); // values > 1 result in excessive rotating
+ rotate_inc = rotate_inc * -delta_y_clamped;
+ break;
+ }
+
+ default:
+ rotate_inc = 0.0;
+ break;
+ }
+
+ if (rotate_inc != 0.0) {
+ Geom::Point const scroll_dt = _desktop->point();
+ _desktop->rotate_relative_keep_point(scroll_dt, rotate_inc);
+ 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 = CLAMP(std::abs(delta_y), 0.0, 1.0); // values > 1 result in excessive zooming
+ double zoom_inc_scaled = (zoom_inc-1) * delta_y_clamped + 1;
+ if (delta_y < 0) {
+ rel_zoom = zoom_inc_scaled;
+ } else {
+ rel_zoom = 1 / zoom_inc_scaled;
+ }
+ break;
+ }
+
+ default:
+ rel_zoom = 0.0;
+ break;
+ }
+
+ if (rel_zoom != 0.0) {
+ Geom::Point const scroll_dt = _desktop->point();
+ _desktop->zoom_relative(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:
+ this->_button1on = true;
+ break;
+ case 2:
+ this->_button2on = true;
+ break;
+ case 3:
+ this->_button3on = true;
+ break;
+ }
+ break;
+ case GDK_BUTTON_RELEASE:
+ switch (event->button.button) {
+ case 1:
+ this->_button1on = false;
+ break;
+ case 2:
+ this->_button2on = false;
+ break;
+ case 3:
+ this->_button3on = false;
+ break;
+ }
+ break;
+ case GDK_MOTION_NOTIFY:
+ if (event->motion.state & Gdk::ModifierType::BUTTON1_MASK) {
+ this->_button1on = true;
+ } else {
+ this->_button1on = false;
+ }
+ if (event->motion.state & Gdk::ModifierType::BUTTON2_MASK) {
+ this->_button2on = true;
+ } else {
+ this->_button2on = false;
+ }
+ if (event->motion.state & Gdk::ModifierType::BUTTON3_MASK) {
+ this->_button3on = true;
+ } else {
+ this->_button3on = false;
+ }
+ }
+}
+
+bool ToolBase::are_buttons_1_and_3_on() const
+{
+ return this->_button1on && this->_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) {
+ int ret = FALSE;
+
+ switch (event->type) {
+ case 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))) {
+ this->menu_popup(event);
+ ret = TRUE;
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ return ret;
+}
+
+/**
+ * Returns true if we're hovering above a knot (needed because we don't want to pre-snap in that case).
+ */
+bool ToolBase::sp_event_context_knot_mouseover() const {
+ if (this->shape_editor) {
+ return this->shape_editor->knot_mouseover();
+ }
+
+ return false;
+}
+
+/**
+ * Enables/disables the ToolBase's SelCue.
+ */
+void ToolBase::enableSelectionCue(bool enable) {
+ if (enable) {
+ if (!_selcue) {
+ _selcue = new Inkscape::SelCue(_desktop);
+ }
+ } else {
+ delete _selcue;
+ _selcue = nullptr;
+ }
+}
+
+/*
+ * Enables/disables the ToolBase's GrDrag.
+ */
+void ToolBase::enableGrDrag(bool enable) {
+ if (enable) {
+ if (!_grdrag) {
+ _grdrag = new GrDrag(_desktop);
+ }
+ } else {
+ if (_grdrag) {
+ delete _grdrag;
+ _grdrag = nullptr;
+ }
+ }
+}
+
+/**
+ * Delete a selected GrDrag point
+ */
+bool ToolBase::deleteSelectedDrag(bool just_one)
+{
+ if (_grdrag && !_grdrag->selected.empty()) {
+ _grdrag->deleteSelected(just_one);
+ return true;
+ }
+ return false;
+}
+
+/**
+ * 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, nullptr); // 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) {
+ Glib::RefPtr<Gdk::Window> window = _desktop->getToplevel()->get_window();
+
+ if (window) {
+ window->set_event_compression(!high_precision);
+ }
+}
+
+Geom::Point ToolBase::setup_for_drag_start(GdkEvent *ev)
+{
+ this->xp = static_cast<gint>(ev->button.x);
+ this->yp = static_cast<gint>(ev->button.y);
+ this->within_tolerance = true;
+
+ Geom::Point const p(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);
+}
+
+/**
+ * Discard and count matching key events from top of event bucket.
+ * Convenience function that just passes request to canvas.
+ */
+int ToolBase::gobble_key_events(guint keyval, guint mask) const
+{
+ return _desktop->canvas->gobble_key_events(keyval, mask);
+}
+
+/**
+ * Discard matching motion events from top of event bucket.
+ * Convenience function that just passes request to canvas.
+ */
+void ToolBase::gobble_motion_events(guint mask) const
+{
+ _desktop->canvas->gobble_motion_events(mask);
+}
+
+/**
+ * Calls virtual set() function of ToolBase.
+ */
+void sp_event_context_read(ToolBase *ec, gchar const *key) {
+ g_return_if_fail(ec != nullptr);
+ g_return_if_fail(key != nullptr);
+
+ 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 this->tool_root_handler(event);
+ }
+
+ switch (event->type) {
+ case GDK_MOTION_NOTIFY:
+ sp_event_context_snap_delay_handler(this, nullptr, nullptr,
+ (GdkEventMotion *) event,
+ DelayedSnapEvent::EVENTCONTEXT_ROOT_HANDLER);
+ break;
+ case GDK_BUTTON_RELEASE:
+ if (_delayed_snap_event) {
+ // If we have any pending snapping action, then invoke it now
+ sp_event_context_snap_watchdog_callback(_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->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 = false;
+
+ // Just set the on buttons for now. later, behave as intended.
+ this->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 (this->is_panning()) {
+ ret = ToolBase::root_handler(event);
+ } else {
+ ret = this->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 this->virtual_item_handler(item, event);
+ }
+
+ switch (event->type) {
+ case GDK_MOTION_NOTIFY:
+ sp_event_context_snap_delay_handler(this, (gpointer) item, nullptr, (GdkEventMotion *) event, DelayedSnapEvent::EVENTCONTEXT_ITEM_HANDLER);
+ break;
+ case GDK_BUTTON_RELEASE:
+ if (_delayed_snap_event) {
+ // If we have any pending snapping action, then invoke it now
+ sp_event_context_snap_watchdog_callback(_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.
+ this->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 = this->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;
+ }
+
+ Geom::Point const button_w(event->button.x, event->button.y);
+ Geom::Point const button_dt(desktop->w2d(button_w));
+ desktop->set_coordinate_status(button_dt);
+}
+
+//-------------------------------------------------------------------
+/**
+ * Create popup menu and tell Gtk to show it.
+ */
+void 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);
+ }
+ }
+
+ ContextMenu* 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, gchar const *ctrl_tip, gchar const *shift_tip,
+ gchar const *alt_tip) {
+ guint keyval = get_latin_keyval(&event->key);
+
+ bool ctrl = ctrl_tip && (MOD__CTRL(event) || (keyval == GDK_KEY_Control_L) || (keyval
+ == GDK_KEY_Control_R));
+ bool shift = shift_tip && (MOD__SHIFT(event) || (keyval == GDK_KEY_Shift_L) || (keyval
+ == GDK_KEY_Shift_R));
+ bool alt = alt_tip && (MOD__ALT(event) || (keyval == GDK_KEY_Alt_L) || (keyval
+ == GDK_KEY_Alt_R) || (keyval == GDK_KEY_Meta_L) || (keyval == GDK_KEY_Meta_R));
+
+ gchar *tip = g_strdup_printf("%s%s%s%s%s", (ctrl ? ctrl_tip : ""), (ctrl
+ && (shift || alt) ? "; " : ""), (shift ? shift_tip : ""), ((ctrl
+ || shift) && alt ? "; " : ""), (alt ? alt_tip : ""));
+
+ if (strlen(tip) > 0) {
+ message_context->flash(Inkscape::INFORMATION_MESSAGE, tip);
+ }
+
+ g_free(tip);
+}
+
+/**
+ * Try to determine the keys group of Latin layout.
+ * Check available keymap entries for Latin 'a' key and find the minimal integer value.
+ */
+static void update_latin_keys_group() {
+ GdkKeymapKey* keys;
+ gint n_keys;
+
+ latin_keys_group_valid = FALSE;
+ latin_keys_groups.clear();
+
+ if (gdk_keymap_get_entries_for_keyval(Gdk::Display::get_default()->get_keymap(), GDK_KEY_a, &keys, &n_keys)) {
+ for (gint i = 0; i < n_keys; i++) {
+ 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), NULL);
+ update_latin_keys_group();
+}
+
+/**
+ * Return the keyval corresponding to the key event in Latin group.
+ *
+ * Use this instead of simply event->keyval, so that your keyboard shortcuts
+ * work regardless of layouts (e.g., in Cyrillic).
+ */
+guint get_latin_keyval(GdkEventKey const *event, guint *consumed_modifiers /*= NULL*/) {
+ guint keyval = 0;
+ GdkModifierType modifiers;
+ gint group = latin_keys_group_valid ? latin_keys_group : event->group;
+
+ 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;
+ }
+ 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;
+ }
+ return keyval;
+}
+
+/**
+ * Returns item at point p in desktop.
+ *
+ * If state includes alt key mask, cyclically selects under; honors
+ * into_groups.
+ */
+SPItem *sp_event_context_find_item(SPDesktop *desktop, Geom::Point const &p,
+ bool select_under, bool into_groups)
+{
+ SPItem *item = nullptr;
+
+ if (select_under) {
+ auto tmp = desktop->selection->items();
+ std::vector<SPItem *> vec(tmp.begin(), tmp.end());
+ SPItem *selected_at_point = desktop->getItemFromListAtPointBottom(vec, p);
+ item = desktop->getItemAtPoint(p, into_groups, selected_at_point);
+ if (item == nullptr) { // we may have reached bottom, flip over to the top
+ item = desktop->getItemAtPoint(p, into_groups, nullptr);
+ }
+ } else {
+ item = desktop->getItemAtPoint(p, into_groups, nullptr);
+ }
+
+ return item;
+}
+
+/**
+ * Returns item if it is under point p in desktop, at any depth; otherwise returns NULL.
+ *
+ * Honors into_groups.
+ */
+SPItem *
+sp_event_context_over_item(SPDesktop *desktop, SPItem *item,
+ Geom::Point const &p) {
+ std::vector<SPItem*> temp;
+ temp.push_back(item);
+ SPItem *item_at_point = desktop->getItemFromListAtPointBottom(temp, p);
+ return item_at_point;
+}
+
+ShapeEditor *
+sp_event_context_get_shape_editor(ToolBase *ec) {
+ return ec->shape_editor;
+}
+
+
+/**
+ * Analyses the current event, calculates the mouse speed, turns snapping off (temporarily) if the
+ * mouse speed is above a threshold, and stores the current event such that it can be re-triggered when needed
+ * (re-triggering is controlled by a watchdog timer).
+ *
+ * @param ec Pointer to the event context.
+ * @param dse_item Pointer that store a reference to a canvas or to an item.
+ * @param dse_item2 Another pointer, storing a reference to a knot or controlpoint.
+ * @param event Pointer to the motion event.
+ * @param origin Identifier (enum) specifying where the delay (and the call to this method) were initiated.
+ */
+void sp_event_context_snap_delay_handler(ToolBase *ec,
+ gpointer const dse_item, gpointer const dse_item2, GdkEventMotion *event,
+ DelayedSnapEvent::DelayedSnapEventOrigin origin)
+{
+ static guint32 prev_time;
+ static std::optional<Geom::Point> prev_pos;
+
+ if (!ec->_uses_snap || ec->_dse_callback_in_process) {
+ return;
+ }
+
+ // Snapping occurs when dragging with the left mouse button down, or when hovering e.g. in the pen tool with left mouse button up
+ bool const c1 = event->state & GDK_BUTTON2_MASK; // We shouldn't hold back any events when other mouse buttons have been
+ bool const c2 = event->state & GDK_BUTTON3_MASK; // pressed, e.g. when scrolling with the middle mouse button; if we do then
+ // Inkscape will get stuck in an unresponsive state
+ bool const c3 = dynamic_cast<Inkscape::UI::Tools::CalligraphicTool *>(ec);
+ // The snap delay will repeat the last motion event, which will lead to
+ // erroneous points in the calligraphy context. And because we don't snap
+ // in this context, we might just as well disable the snap delay all together
+ bool const c4 = ec->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
+ ec->discard_delayed_snap_event();
+ } else if (ec->getDesktop() &&
+ ec->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.
+ ec->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;
+ gdouble speed = delta_t > 0 ? dist / delta_t : 1000;
+ //std::cout << "Mouse speed = " << speed << " px/msec " << std::endl;
+ if (speed > 0.02) { // Jitter threshold, might be needed for tablets
+ // We're moving fast, so postpone any snapping until the next GDK_MOTION_NOTIFY event. We
+ // will keep on postponing the snapping as long as the speed is high.
+ // We must snap at some point in time though, so set a watchdog timer at some time from
+ // now, just in case there's no future motion event that drops under the speed limit (when
+ // stopping abruptly)
+ delete ec->_delayed_snap_event;
+ ec->_delayed_snap_event = new DelayedSnapEvent(ec, dse_item, dse_item2, event, origin); // watchdog is reset, i.e. pushed forward in time
+ // If the watchdog expires before a new motion event is received, we will snap (as explained
+ // above). This means however that when the timer is too short, we will always snap and that the
+ // speed threshold is ineffective. In the extreme case the delay is set to zero, and snapping will
+ // be immediate, as it used to be in the old days ;-).
+ } else { // Speed is very low, so we're virtually at stand still
+ // But if we're really standing still, then we should snap now. We could use some low-pass filtering,
+ // otherwise snapping occurs for each jitter movement. For this filtering we'll leave the watchdog to expire,
+ // snap, and set a new watchdog again.
+ if (ec->_delayed_snap_event == nullptr) { // no watchdog has been set
+ // it might have already expired, so we'll set a new one; the snapping frequency will be limited this way
+ ec->_delayed_snap_event = new DelayedSnapEvent(ec, dse_item, dse_item2, event, origin);
+ } // else: watchdog has been set before and we'll wait for it to expire
+ }
+ } else {
+ // This is the first GDK_MOTION_NOTIFY event, so postpone snapping and set the watchdog
+ g_assert(ec->_delayed_snap_event == nullptr);
+ ec->_delayed_snap_event = new DelayedSnapEvent(ec, dse_item, dse_item2, event, origin);
+ }
+
+ prev_pos = event_pos;
+ prev_time = event_t;
+ }
+}
+
+/**
+ * When the snap delay watchdog timer barks, this method will be called and will re-inject the last motion
+ * event in an appropriate place, with snapping being turned on again.
+ */
+gboolean sp_event_context_snap_watchdog_callback(gpointer data) {
+ // Snap NOW! For this the "postponed" flag will be reset and the last motion event will be repeated
+ DelayedSnapEvent *dse = reinterpret_cast<DelayedSnapEvent*> (data);
+
+ if (dse == nullptr) {
+ // This might occur when this method is called directly, i.e. not through the timer
+ // E.g. on GDK_BUTTON_RELEASE in start_root_handler()
+ return FALSE;
+ }
+
+ ToolBase *ec = dse->getEventContext();
+ if (ec == nullptr) {
+ delete dse;
+ return false;
+ }
+
+ const SPDesktop *dt = ec->getDesktop();
+ if (dt == nullptr) {
+ ec->_delayed_snap_event = nullptr;
+ delete dse;
+ return false;
+ }
+
+ ec->_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 it's origin
+ // The switch below takes care of that and prepares the relevant parameters
+ switch (dse->getOrigin()) {
+ case DelayedSnapEvent::EVENTCONTEXT_ROOT_HANDLER:
+ ec->tool_root_handler(dse->getEvent());
+ break;
+ case DelayedSnapEvent::EVENTCONTEXT_ITEM_HANDLER: {
+ auto item = static_cast<SPItem *>(dse->getItem());
+ if (item) {
+ ec->virtual_item_handler(item, dse->getEvent());
+ }
+ }
+ break;
+ case DelayedSnapEvent::KNOT_HANDLER: {
+ gpointer knot = dse->getItem2();
+ check_if_knot_deleted(knot);
+ if (knot && SP_IS_KNOT(knot)) {
+ bool was_grabbed = SP_KNOT(knot)->is_grabbed();
+ SP_KNOT(knot)->setFlag(SP_KNOT_GRABBED, true); // Must be grabbed for Inkscape::SelTrans::handleRequest() to pass
+ sp_knot_handler_request_position(dse->getEvent(), SP_KNOT(knot));
+ SP_KNOT(knot)->setFlag(SP_KNOT_GRABBED, was_grabbed);
+ }
+ }
+ break;
+ case DelayedSnapEvent::CONTROL_POINT_HANDLER: {
+ using Inkscape::UI::ControlPoint;
+ gpointer pitem2 = dse->getItem2();
+ if (!pitem2)
+ {
+ ec->_delayed_snap_event = nullptr;
+ delete dse;
+ return false;
+ }
+ ControlPoint *point = reinterpret_cast<ControlPoint*> (pitem2);
+ if (point) {
+ if (point->position().isFinite() && (dt == point->_desktop)) {
+ point->_eventHandler(ec, dse->getEvent());
+ }
+ else {
+ //workaround:
+ //[Bug 781893] Crash after moving a Bezier node after Knot path effect?
+ // --> at some time, some point with X = 0 and Y = nan (not a number) is created ...
+ // even so, the desktop pointer is invalid and equal to 0xff
+ g_warning ("encountered non finite point when evaluating snapping callback");
+ }
+ }
+ }
+ break;
+ case DelayedSnapEvent::GUIDE_HANDLER: {
+ auto item = static_cast<CanvasItemGuideLine *>(dse->getItem());
+ auto item2 = static_cast<SPGuide *>(dse->getItem2());
+ if (item && item2) {
+ sp_dt_guide_event(dse->getEvent(), item, item2);
+ }
+ }
+ break;
+ case DelayedSnapEvent::GUIDE_HRULER:
+ case DelayedSnapEvent::GUIDE_VRULER: {
+ gpointer item = dse->getItem();
+ auto item2 = static_cast<Gtk::Widget *>(dse->getItem2());
+ if (item && item2) {
+ g_assert(GTK_IS_WIDGET(item));
+ if (dse->getOrigin() == DelayedSnapEvent::GUIDE_HRULER) {
+ SPDesktopWidget::ruler_event(GTK_WIDGET(item), dse->getEvent(), SP_DESKTOP_WIDGET(item2), true);
+ } else {
+ SPDesktopWidget::ruler_event(GTK_WIDGET(item), dse->getEvent(), SP_DESKTOP_WIDGET(item2), false);
+ }
+ }
+ }
+ break;
+ default:
+ g_warning("Origin of snap-delay event has not been defined!;");
+ break;
+ }
+
+ ec->_delayed_snap_event = nullptr;
+ delete dse;
+
+ ec->_dse_callback_in_process = false;
+
+ return FALSE; //Kills the timer and stops it from executing this callback over and over again.
+}
+
+void ToolBase::discard_delayed_snap_event()
+{
+ delete _delayed_snap_event;
+ _delayed_snap_event = nullptr;
+ _desktop->namedview->snap_manager.snapprefs.setSnapPostponedGlobally(false);
+}
+}
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+ */
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/tools/tool-base.h b/src/ui/tools/tool-base.h
new file mode 100644
index 0000000..c384d09
--- /dev/null
+++ b/src/ui/tools/tool-base.h
@@ -0,0 +1,309 @@
+// 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 <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 Inkscape {
+namespace UI {
+
+class ShapeEditor;
+
+namespace Tools {
+
+class ToolBase;
+
+gboolean sp_event_context_snap_watchdog_callback(gpointer data);
+
+class DelayedSnapEvent {
+public:
+ enum DelayedSnapEventOrigin {
+ UNDEFINED_HANDLER = 0,
+ EVENTCONTEXT_ROOT_HANDLER,
+ EVENTCONTEXT_ITEM_HANDLER,
+ KNOT_HANDLER,
+ CONTROL_POINT_HANDLER,
+ GUIDE_HANDLER,
+ GUIDE_HRULER,
+ GUIDE_VRULER
+ };
+
+ DelayedSnapEvent(ToolBase *event_context, gpointer const dse_item, gpointer dse_item2, GdkEventMotion const *event, DelayedSnapEvent::DelayedSnapEventOrigin const origin)
+ : _timer_id(0)
+ , _event(nullptr)
+ , _item(dse_item)
+ , _item2(dse_item2)
+ , _origin(origin)
+ , _event_context(event_context)
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double value = prefs->getDoubleLimited("/options/snapdelay/value", 0, 0, 1000);
+
+ // We used to have this specified in milliseconds; this has changed to seconds now for consistency's sake
+ if (value > 1) { // Apparently we have an old preference file, this value must have been in milliseconds;
+ value = value / 1000.0; // now convert this value to seconds
+ }
+
+ _timer_id = g_timeout_add(value*1000.0, &sp_event_context_snap_watchdog_callback, this);
+ _event = gdk_event_copy((GdkEvent*) event);
+
+ ((GdkEventMotion *)_event)->time = GDK_CURRENT_TIME;
+ }
+
+ ~DelayedSnapEvent() {
+ if (_timer_id > 0) g_source_remove(_timer_id); // Kill the watchdog
+ if (_event != nullptr) gdk_event_free(_event); // Remove the copy of the original event
+ }
+
+ ToolBase* getEventContext() {
+ return _event_context;
+ }
+
+ DelayedSnapEventOrigin getOrigin() {
+ return _origin;
+ }
+
+ GdkEvent* getEvent() {
+ return _event;
+ }
+
+ gpointer getItem() {
+ return _item;
+ }
+
+ gpointer getItem2() {
+ return _item2;
+ }
+
+private:
+ guint _timer_id;
+ GdkEvent* _event;
+ gpointer _item;
+ gpointer _item2;
+ DelayedSnapEventOrigin _origin;
+ ToolBase* _event_context;
+};
+
+void sp_event_context_snap_delay_handler(ToolBase *ec, gpointer const dse_item, gpointer const dse_item2, GdkEventMotion *event, DelayedSnapEvent::DelayedSnapEventOrigin origin);
+
+
+/**
+ * Base class for Event processors.
+ *
+ * This is per desktop object, which (its derivatives) implements
+ * different actions bound to mouse events.
+ *
+ * ToolBase is an abstract base class of all tools. As the name
+ * indicates, event context implementations process UI events (mouse
+ * movements and keypresses) and take actions (like creating or modifying
+ * objects). There is one event context implementation for each tool,
+ * plus few abstract base classes. Writing a new tool involves
+ * subclassing ToolBase.
+ */
+class ToolBase : public sigc::trackable
+{
+public:
+ ToolBase(SPDesktop *desktop, std::string prefs_path, std::string cursor_filename, bool uses_snap = true);
+
+ virtual ~ToolBase();
+
+ ToolBase(const ToolBase&) = delete;
+ ToolBase& operator=(const ToolBase&) = delete;
+
+ 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);
+
+ 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 getPrefsPath() { return _prefs_path; };
+ void enableSelectionCue (bool enable=true);
+
+ Inkscape::MessageContext *defaultMessageContext() const {
+ return message_context.get();
+ }
+
+ SPDesktop *getDesktop() { 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();
+
+ /**
+ * An observer that relays pref changes to the derived classes.
+ */
+ class ToolPrefObserver: public Inkscape::Preferences::Observer {
+ public:
+ ToolPrefObserver(Glib::ustring const &path, ToolBase *ec)
+ : Inkscape::Preferences::Observer(path)
+ , ec(ec)
+ {
+ }
+
+ void notify(Inkscape::Preferences::Entry const &val) override {
+ ec->set(val);
+ }
+
+ private:
+ ToolBase * const ec;
+ };
+
+private:
+ Inkscape::Preferences::Observer *pref_observer = nullptr;
+ std::string _prefs_path;
+
+protected:
+ Glib::RefPtr<Gdk::Cursor> _cursor;
+ std::string _cursor_filename = "select.svg";
+ std::string _cursor_default = "select.svg";
+
+ gint xp = 0; ///< where drag started
+ gint yp = 0; ///< where drag started
+ gint 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;
+
+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; }
+
+ bool rotating_mode = false;;
+
+ std::unique_ptr<Inkscape::MessageContext> message_context;
+ Inkscape::SelCue *_selcue = nullptr;
+
+ GrDrag *_grdrag = nullptr;
+
+ ShapeEditor* shape_editor = nullptr;
+
+ bool _dse_callback_in_process = false;
+
+ bool _uses_snap = false;
+ DelayedSnapEvent *_delayed_snap_event = nullptr;
+
+ void discard_delayed_snap_event();
+ 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 filename);
+ 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);
+
+ int gobble_key_events(guint keyval, guint mask) const;
+ void gobble_motion_events(guint mask) const;
+
+ SPDesktop *_desktop = nullptr;
+
+private:
+
+ bool _keyboardMove(GdkEventKey const &event, Geom::Point const &dir);
+};
+
+void sp_event_context_read(ToolBase *ec, gchar const *key);
+
+
+void sp_event_root_menu_popup(SPDesktop *desktop, SPItem *item, GdkEvent *event);
+
+void sp_event_show_modifier_tip(Inkscape::MessageContext *message_context, GdkEvent *event,
+ gchar const *ctrl_tip, gchar const *shift_tip, gchar const *alt_tip);
+
+void init_latin_keys_group();
+guint get_latin_keyval(GdkEventKey const *event, guint *consumed_modifiers = nullptr);
+
+SPItem *sp_event_context_find_item (SPDesktop *desktop, Geom::Point const &p, bool select_under, bool into_groups);
+SPItem *sp_event_context_over_item (SPDesktop *desktop, SPItem *item, Geom::Point const &p);
+
+void sp_toggle_dropper(SPDesktop *dt);
+
+bool sp_event_context_knot_mouseover(ToolBase *ec);
+
+} // namespace Tools
+} // namespace UI
+} // namespace Inkscape
+
+#endif // SEEN_SP_EVENT_CONTEXT_H
+
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/tools/tweak-tool.cpp b/src/ui/tools/tweak-tool.cpp
new file mode 100644
index 0000000..d87d3f2
--- /dev/null
+++ b/src/ui/tools/tweak-tool.cpp
@@ -0,0 +1,1489 @@
+// 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)
+ , dilate_area(nullptr)
+ , do_h(true)
+ , do_s(true)
+ , do_l(true)
+ , do_o(false)
+{
+ dilate_area = new Inkscape::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");
+
+ this->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() {
+ this->enableGrDrag(false);
+
+ this->style_set_connection.disconnect();
+
+ if (this->dilate_area) {
+ delete this->dilate_area;
+ this->dilate_area = nullptr;
+ }
+}
+
+static bool is_transform_mode (gint mode)
+{
+ return (mode == TWEAK_MODE_MOVE ||
+ mode == TWEAK_MODE_MOVE_IN_OUT ||
+ mode == TWEAK_MODE_MOVE_JITTER ||
+ mode == TWEAK_MODE_SCALE ||
+ mode == TWEAK_MODE_ROTATE ||
+ mode == TWEAK_MODE_MORELESS);
+}
+
+static bool is_color_mode (gint mode)
+{
+ return (mode == TWEAK_MODE_COLORPAINT || mode == TWEAK_MODE_COLORJITTER || mode == TWEAK_MODE_BLUR);
+}
+
+void TweakTool::update_cursor (bool with_shift) {
+ guint num = 0;
+ gchar *sel_message = nullptr;
+
+ if (!_desktop->selection->isEmpty()) {
+ num = (guint)boost::distance(_desktop->selection->items());
+ sel_message = g_strdup_printf(ngettext("<b>%i</b> object selected","<b>%i</b> objects selected",num), num);
+ } else {
+ sel_message = g_strdup_printf("%s", _("<b>Nothing</b> selected"));
+ }
+
+ switch (this->mode) {
+ case TWEAK_MODE_MOVE:
+ this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag to <b>move</b>."), sel_message);
+ this->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;
+
+ {
+ SPBox3D *box = dynamic_cast<SPBox3D *>(item);
+ if (box && !is_transform_mode(mode) && !is_color_mode(mode)) {
+ // convert 3D boxes to ordinary groups before tweaking their shapes
+ item = box->convert_to_group();
+ selection->add(item);
+ }
+ }
+
+ if (dynamic_cast<SPText *>(item) || dynamic_cast<SPFlowtext *>(item)) {
+ std::vector<SPItem*> items;
+ items.push_back(item);
+ std::vector<SPItem*> selected;
+ std::vector<Inkscape::XML::Node*> to_select;
+ SPDocument *doc = item->document;
+ sp_item_list_to_curves (items, selected, to_select);
+ SPObject* newObj = doc->getObjectByRepr(to_select[0]);
+ item = dynamic_cast<SPItem *>(newObj);
+ g_assert(item != nullptr);
+ selection->add(item);
+ }
+
+ if (dynamic_cast<SPGroup *>(item) && !dynamic_cast<SPBox3D *>(item)) {
+ std::vector<SPItem *> children;
+ for (auto& child: item->children) {
+ if (dynamic_cast<SPItem *>(&child)) {
+ children.push_back(dynamic_cast<SPItem *>(&child));
+ }
+ }
+
+ for (auto i = children.rbegin(); i!= children.rend(); ++i) {
+ SPItem *child = *i;
+ g_assert(child != nullptr);
+ if (sp_tweak_dilate_recursive (selection, child, p, vector, mode, radius, force, fidelity, reverse)) {
+ did = true;
+ }
+ }
+ } else {
+ if (mode == TWEAK_MODE_MOVE) {
+
+ Geom::OptRect a = item->documentVisualBounds();
+ if (a) {
+ double x = Geom::L2(a->midpoint() - p)/radius;
+ if (a->contains(p)) x = 0;
+ if (x < 1) {
+ Geom::Point move = force * 0.5 * (cos(M_PI * x) + 1) * vector;
+ item->move_rel(Geom::Translate(move * selection->desktop()->doc2dt().withoutTranslation()));
+ did = true;
+ }
+ }
+
+ } else if (mode == TWEAK_MODE_MOVE_IN_OUT) {
+
+ Geom::OptRect a = item->documentVisualBounds();
+ if (a) {
+ double x = Geom::L2(a->midpoint() - p)/radius;
+ if (a->contains(p)) x = 0;
+ if (x < 1) {
+ Geom::Point move = force * 0.5 * (cos(M_PI * x) + 1) *
+ (reverse? (a->midpoint() - p) : (p - a->midpoint()));
+ item->move_rel(Geom::Translate(move * selection->desktop()->doc2dt().withoutTranslation()));
+ did = true;
+ }
+ }
+
+ } else if (mode == TWEAK_MODE_MOVE_JITTER) {
+
+ Geom::OptRect a = item->documentVisualBounds();
+ if (a) {
+ double dp = g_random_double_range(0, M_PI*2);
+ double dr = g_random_double_range(0, radius);
+ double x = Geom::L2(a->midpoint() - p)/radius;
+ if (a->contains(p)) x = 0;
+ if (x < 1) {
+ Geom::Point move = force * 0.5 * (cos(M_PI * x) + 1) * Geom::Point(cos(dp)*dr, sin(dp)*dr);
+ item->move_rel(Geom::Translate(move * selection->desktop()->doc2dt().withoutTranslation()));
+ did = true;
+ }
+ }
+
+ } else if (mode == TWEAK_MODE_SCALE) {
+
+ Geom::OptRect a = item->documentVisualBounds();
+ if (a) {
+ double x = Geom::L2(a->midpoint() - p)/radius;
+ if (a->contains(p)) x = 0;
+ if (x < 1) {
+ double scale = 1 + (reverse? force : -force) * 0.05 * (cos(M_PI * x) + 1);
+ item->scale_rel(Geom::Scale(scale, scale));
+ did = true;
+ }
+ }
+
+ } else if (mode == TWEAK_MODE_ROTATE) {
+
+ Geom::OptRect a = item->documentVisualBounds();
+ if (a) {
+ double x = Geom::L2(a->midpoint() - p)/radius;
+ if (a->contains(p)) x = 0;
+ if (x < 1) {
+ double angle = (reverse? force : -force) * 0.05 * (cos(M_PI * x) + 1) * M_PI;
+ angle *= -selection->desktop()->yaxisdir();
+ item->rotate_rel(Geom::Rotate(angle));
+ did = true;
+ }
+ }
+
+ } else if (mode == TWEAK_MODE_MORELESS) {
+
+ Geom::OptRect a = item->documentVisualBounds();
+ if (a) {
+ double x = Geom::L2(a->midpoint() - p)/radius;
+ if (a->contains(p)) x = 0;
+ if (x < 1) {
+ double prob = force * 0.5 * (cos(M_PI * x) + 1);
+ double chance = g_random_double_range(0, 1);
+ if (chance <= prob) {
+ if (reverse) { // delete
+ item->deleteObject(true, true);
+ } else { // duplicate
+ SPDocument *doc = item->document;
+ Inkscape::XML::Document* xml_doc = doc->getReprDoc();
+ Inkscape::XML::Node *old_repr = item->getRepr();
+ SPObject *old_obj = doc->getObjectByRepr(old_repr);
+ Inkscape::XML::Node *parent = old_repr->parent();
+ Inkscape::XML::Node *copy = old_repr->duplicate(xml_doc);
+ parent->appendChild(copy);
+ SPObject *new_obj = doc->getObjectByRepr(copy);
+ if (selection->includes(old_obj)) {
+ selection->add(new_obj);
+ }
+ Inkscape::GC::release(copy);
+ }
+ did = true;
+ }
+ }
+ }
+
+ } else if (dynamic_cast<SPPath *>(item) || dynamic_cast<SPShape *>(item)) {
+
+ Inkscape::XML::Node *newrepr = nullptr;
+ gint pos = 0;
+ Inkscape::XML::Node *parent = nullptr;
+ char const *id = nullptr;
+ if (!dynamic_cast<SPPath *>(item)) {
+ newrepr = sp_selected_item_to_curved_repr(item, 0);
+ if (!newrepr) {
+ return false;
+ }
+
+ // remember the position of the item
+ pos = item->getRepr()->position();
+ // remember parent
+ parent = item->getRepr()->parent();
+ // remember id
+ id = item->getRepr()->attribute("id");
+ }
+
+ // skip those paths whose bboxes are entirely out of reach with our radius
+ Geom::OptRect bbox = item->documentVisualBounds();
+ if (bbox) {
+ bbox->expandBy(radius);
+ if (!bbox->contains(p)) {
+ return false;
+ }
+ }
+
+ Path *orig = Path_for_item(item, false);
+ if (orig == nullptr) {
+ return false;
+ }
+
+ Path *res = new Path;
+ res->SetBackData(false);
+
+ Shape *theShape = new Shape;
+ Shape *theRes = new Shape;
+ Geom::Affine i2doc(item->i2doc_affine());
+
+ orig->ConvertWithBackData((0.08 - (0.07 * fidelity)) / i2doc.descrim()); // default 0.059
+ orig->Fill(theShape, 0);
+
+ SPCSSAttr *css = sp_repr_css_attr(item->getRepr(), "style");
+ gchar const *val = sp_repr_css_property(css, "fill-rule", nullptr);
+ if (val && strcmp(val, "nonzero") == 0) {
+ theRes->ConvertToShape(theShape, fill_nonZero);
+ } else if (val && strcmp(val, "evenodd") == 0) {
+ theRes->ConvertToShape(theShape, fill_oddEven);
+ } else {
+ theRes->ConvertToShape(theShape, fill_nonZero);
+ }
+
+ if (Geom::L2(vector) != 0) {
+ vector = 1/Geom::L2(vector) * vector;
+ }
+
+ bool did_this = false;
+ if (mode == TWEAK_MODE_SHRINK_GROW) {
+ if (theShape->MakeTweak(tweak_mode_grow, theRes,
+ reverse? force : -force,
+ join_straight, 4.0,
+ true, p, Geom::Point(0,0), radius, &i2doc) == 0) // 0 means the shape was actually changed
+ did_this = true;
+ } else if (mode == TWEAK_MODE_ATTRACT_REPEL) {
+ if (theShape->MakeTweak(tweak_mode_repel, theRes,
+ reverse? force : -force,
+ join_straight, 4.0,
+ true, p, Geom::Point(0,0), radius, &i2doc) == 0)
+ did_this = true;
+ } else if (mode == TWEAK_MODE_PUSH) {
+ if (theShape->MakeTweak(tweak_mode_push, theRes,
+ 1.0,
+ join_straight, 4.0,
+ true, p, force*2*vector, radius, &i2doc) == 0)
+ did_this = true;
+ } else if (mode == TWEAK_MODE_ROUGHEN) {
+ if (theShape->MakeTweak(tweak_mode_roughen, theRes,
+ force,
+ join_straight, 4.0,
+ true, p, Geom::Point(0,0), radius, &i2doc) == 0)
+ did_this = true;
+ }
+
+ // the rest only makes sense if we actually changed the path
+ if (did_this) {
+ theRes->ConvertToShape(theShape, fill_positive);
+
+ res->Reset();
+ theRes->ConvertToForme(res);
+
+ double th_max = (0.6 - 0.59*sqrt(fidelity)) / i2doc.descrim();
+ double threshold = MAX(th_max, th_max*force);
+ res->ConvertEvenLines(threshold);
+ res->Simplify(threshold / (selection->desktop()->current_zoom()));
+
+ if (newrepr) { // converting to path, need to replace the repr
+ bool is_selected = selection->includes(item);
+ if (is_selected) {
+ selection->remove(item);
+ }
+
+ // It's going to resurrect, so we delete without notifying listeners.
+ item->deleteObject(false);
+
+ // restore id
+ newrepr->setAttribute("id", id);
+ // add the new repr to the parent
+ // move to the saved position
+ parent->addChildAtPos(newrepr, pos);
+
+ if (is_selected)
+ selection->add(newrepr);
+ }
+
+ if (res->descr_cmd.size() > 1) {
+ gchar *str = res->svg_dump_path();
+ if (newrepr) {
+ newrepr->setAttribute("d", str);
+ } else {
+ SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item);
+ if (lpeitem && lpeitem->hasPathEffectRecursive()) {
+ item->setAttribute("inkscape:original-d", str);
+ } else {
+ item->setAttribute("d", str);
+ }
+ }
+ g_free(str);
+ } else {
+ // TODO: if there's 0 or 1 node left, delete this path altogether
+ }
+
+ if (newrepr) {
+ Inkscape::GC::release(newrepr);
+ newrepr = nullptr;
+ }
+ }
+
+ delete theShape;
+ delete theRes;
+ delete orig;
+ delete res;
+
+ if (did_this) {
+ did = true;
+ }
+ }
+
+ }
+
+ return did;
+}
+
+ static void
+tweak_colorpaint (float *color, guint32 goal, double force, bool do_h, bool do_s, bool do_l)
+{
+ float rgb_g[3];
+
+ if (!do_h || !do_s || !do_l) {
+ float hsl_g[3];
+ SPColor::rgb_to_hsl_floatv (hsl_g, SP_RGBA32_R_F(goal), SP_RGBA32_G_F(goal), SP_RGBA32_B_F(goal));
+ float hsl_c[3];
+ SPColor::rgb_to_hsl_floatv (hsl_c, color[0], color[1], color[2]);
+ if (!do_h) {
+ hsl_g[0] = hsl_c[0];
+ }
+ if (!do_s) {
+ hsl_g[1] = hsl_c[1];
+ }
+ if (!do_l) {
+ hsl_g[2] = hsl_c[2];
+ }
+ SPColor::hsl_to_rgb_floatv (rgb_g, hsl_g[0], hsl_g[1], hsl_g[2]);
+ } else {
+ rgb_g[0] = SP_RGBA32_R_F(goal);
+ rgb_g[1] = SP_RGBA32_G_F(goal);
+ rgb_g[2] = SP_RGBA32_B_F(goal);
+ }
+
+ for (int i = 0; i < 3; i++) {
+ double d = rgb_g[i] - color[i];
+ color[i] += d * force;
+ }
+}
+
+ static void
+tweak_colorjitter (float *color, double force, bool do_h, bool do_s, bool do_l)
+{
+ float hsl_c[3];
+ SPColor::rgb_to_hsl_floatv (hsl_c, color[0], color[1], color[2]);
+
+ if (do_h) {
+ hsl_c[0] += g_random_double_range(-0.5, 0.5) * force;
+ if (hsl_c[0] > 1) {
+ hsl_c[0] -= 1;
+ }
+ if (hsl_c[0] < 0) {
+ hsl_c[0] += 1;
+ }
+ }
+ if (do_s) {
+ hsl_c[1] += g_random_double_range(-hsl_c[1], 1 - hsl_c[1]) * force;
+ }
+ if (do_l) {
+ hsl_c[2] += g_random_double_range(-hsl_c[2], 1 - hsl_c[2]) * force;
+ }
+
+ SPColor::hsl_to_rgb_floatv (color, hsl_c[0], hsl_c[1], hsl_c[2]);
+}
+
+ static void
+tweak_color (guint mode, float *color, guint32 goal, double force, bool do_h, bool do_s, bool do_l)
+{
+ if (mode == TWEAK_MODE_COLORPAINT) {
+ tweak_colorpaint (color, goal, force, do_h, do_s, do_l);
+ } else if (mode == TWEAK_MODE_COLORJITTER) {
+ tweak_colorjitter (color, force, do_h, do_s, do_l);
+ }
+}
+
+ static void
+tweak_opacity (guint mode, SPIScale24 *style_opacity, double opacity_goal, double force)
+{
+ double opacity = SP_SCALE24_TO_FLOAT (style_opacity->value);
+
+ if (mode == TWEAK_MODE_COLORPAINT) {
+ double d = opacity_goal - opacity;
+ opacity += d * force;
+ } else if (mode == TWEAK_MODE_COLORJITTER) {
+ opacity += g_random_double_range(-opacity, 1 - opacity) * force;
+ }
+
+ style_opacity->value = SP_SCALE24_FROM_FLOAT(opacity);
+}
+
+
+ static double
+tweak_profile (double dist, double radius)
+{
+ if (radius == 0) {
+ return 0;
+ }
+ double x = dist / radius;
+ double alpha = 1;
+ if (x >= 1) {
+ return 0;
+ } else if (x <= 0) {
+ return 1;
+ } else {
+ return (0.5 * cos (M_PI * (pow(x, alpha))) + 0.5);
+ }
+}
+
+static void tweak_colors_in_gradient(SPItem *item, Inkscape::PaintTarget fill_or_stroke,
+ guint32 const rgb_goal, Geom::Point p_w, double radius, double force, guint mode,
+ bool do_h, bool do_s, bool do_l, bool /*do_o*/)
+{
+ SPGradient *gradient = getGradient(item, fill_or_stroke);
+
+ if (!gradient || !dynamic_cast<SPGradient *>(gradient)) {
+ return;
+ }
+
+ Geom::Affine i2d (item->i2doc_affine ());
+ Geom::Point p = p_w * i2d.inverse();
+ p *= (gradient->gradientTransform).inverse();
+ // now p is in gradient's original coordinates
+
+ SPLinearGradient *lg = dynamic_cast<SPLinearGradient *>(gradient);
+ SPRadialGradient *rg = dynamic_cast<SPRadialGradient *>(gradient);
+ if (lg || rg) {
+
+ double pos = 0;
+ double r = 0;
+
+ if (lg) {
+ Geom::Point p1(lg->x1.computed, lg->y1.computed);
+ Geom::Point p2(lg->x2.computed, lg->y2.computed);
+ Geom::Point pdiff(p2 - p1);
+ double vl = Geom::L2(pdiff);
+
+ // This is the matrix which moves and rotates the gradient line
+ // so it's oriented along the X axis:
+ Geom::Affine norm = Geom::Affine(Geom::Translate(-p1)) *
+ Geom::Affine(Geom::Rotate(-atan2(pdiff[Geom::Y], pdiff[Geom::X])));
+
+ // Transform the mouse point by it to find out its projection onto the gradient line:
+ Geom::Point pnorm = p * norm;
+
+ // Scale its X coordinate to match the length of the gradient line:
+ pos = pnorm[Geom::X] / vl;
+ // Calculate radius in length-of-gradient-line units
+ r = radius / vl;
+
+ }
+ if (rg) {
+ Geom::Point c (rg->cx.computed, rg->cy.computed);
+ pos = Geom::L2(p - c) / rg->r.computed;
+ r = radius / rg->r.computed;
+ }
+
+ // Normalize pos to 0..1, taking into account gradient spread:
+ double pos_e = pos;
+ if (gradient->getSpread() == SP_GRADIENT_SPREAD_PAD) {
+ if (pos > 1) {
+ pos_e = 1;
+ }
+ if (pos < 0) {
+ pos_e = 0;
+ }
+ } else if (gradient->getSpread() == SP_GRADIENT_SPREAD_REPEAT) {
+ if (pos > 1 || pos < 0) {
+ pos_e = pos - floor(pos);
+ }
+ } else if (gradient->getSpread() == SP_GRADIENT_SPREAD_REFLECT) {
+ if (pos > 1 || pos < 0) {
+ bool odd = ((int)(floor(pos)) % 2 == 1);
+ pos_e = pos - floor(pos);
+ if (odd) {
+ pos_e = 1 - pos_e;
+ }
+ }
+ }
+
+ SPGradient *vector = sp_gradient_get_forked_vector_if_necessary(gradient, false);
+
+ double offset_l = 0;
+ double offset_h = 0;
+ SPObject *child_prev = nullptr;
+ for (auto& child: vector->children) {
+ SPStop *stop = dynamic_cast<SPStop *>(&child);
+ if (!stop) {
+ continue;
+ }
+
+ offset_h = stop->offset;
+
+ if (child_prev) {
+ SPStop *prevStop = dynamic_cast<SPStop *>(child_prev);
+ g_assert(prevStop != nullptr);
+
+ if (offset_h - offset_l > r && pos_e >= offset_l && pos_e <= offset_h) {
+ // the summit falls in this interstop, and the radius is small,
+ // so it only affects the ends of this interstop;
+ // distribute the force between the two endstops so that they
+ // get all the painting even if they are not touched by the brush
+ tweak_color (mode, stop->getColor().v.c, rgb_goal,
+ force * (pos_e - offset_l) / (offset_h - offset_l),
+ do_h, do_s, do_l);
+ tweak_color(mode, prevStop->getColor().v.c, rgb_goal,
+ force * (offset_h - pos_e) / (offset_h - offset_l),
+ do_h, do_s, do_l);
+ stop->updateRepr();
+ child_prev->updateRepr();
+ break;
+ } else {
+ // wide brush, may affect more than 2 stops,
+ // paint each stop by the force from the profile curve
+ if (offset_l <= pos_e && offset_l > pos_e - r) {
+ tweak_color(mode, prevStop->getColor().v.c, rgb_goal,
+ force * tweak_profile (fabs (pos_e - offset_l), r),
+ do_h, do_s, do_l);
+ child_prev->updateRepr();
+ }
+
+ if (offset_h >= pos_e && offset_h < pos_e + r) {
+ tweak_color (mode, stop->getColor().v.c, rgb_goal,
+ force * tweak_profile (fabs (pos_e - offset_h), r),
+ do_h, do_s, do_l);
+ stop->updateRepr();
+ }
+ }
+ }
+
+ offset_l = offset_h;
+ child_prev = &child;
+ }
+ } else {
+ // Mesh
+ SPMeshGradient *mg = dynamic_cast<SPMeshGradient *>(gradient);
+ if (mg) {
+ SPMeshGradient *mg_array = dynamic_cast<SPMeshGradient *>(mg->getArray());
+ SPMeshNodeArray *array = &(mg_array->array);
+ // Every third node is a corner node
+ for( unsigned i=0; i < array->nodes.size(); i+=3 ) {
+ for( unsigned j=0; j < array->nodes[i].size(); j+=3 ) {
+ SPStop *stop = array->nodes[i][j]->stop;
+ double distance = Geom::L2(Geom::Point(p - array->nodes[i][j]->p));
+ tweak_color (mode, stop->getColor().v.c, rgb_goal,
+ force * tweak_profile (distance, radius), do_h, do_s, do_l);
+ stop->updateRepr();
+ }
+ }
+ }
+ }
+}
+
+ static bool
+sp_tweak_color_recursive (guint mode, SPItem *item, SPItem *item_at_point,
+ guint32 fill_goal, bool do_fill,
+ guint32 stroke_goal, bool do_stroke,
+ float opacity_goal, bool do_opacity,
+ bool do_blur, bool reverse,
+ Geom::Point p, double radius, double force,
+ bool do_h, bool do_s, bool do_l, bool do_o)
+{
+ bool did = false;
+
+ if (dynamic_cast<SPGroup *>(item)) {
+ for (auto& child: item->children) {
+ SPItem *childItem = dynamic_cast<SPItem *>(&child);
+ if (childItem) {
+ if (sp_tweak_color_recursive (mode, childItem, item_at_point,
+ fill_goal, do_fill,
+ stroke_goal, do_stroke,
+ opacity_goal, do_opacity,
+ do_blur, reverse,
+ p, radius, force, do_h, do_s, do_l, do_o)) {
+ did = true;
+ }
+ }
+ }
+
+ } else {
+ SPStyle *style = item->style;
+ if (!style) {
+ return false;
+ }
+ Geom::OptRect bbox = item->documentGeometricBounds();
+ if (!bbox) {
+ return false;
+ }
+
+ Geom::Rect brush(p - Geom::Point(radius, radius), p + Geom::Point(radius, radius));
+
+ Geom::Point center = bbox->midpoint();
+ double this_force;
+
+ // if item == item_at_point, use max force
+ if (item == item_at_point) {
+ this_force = force;
+ // else if no overlap of bbox and brush box, skip:
+ } else if (!bbox->intersects(brush)) {
+ return false;
+ //TODO:
+ // else if object > 1.5 brush: test 4/8/16 points in the brush on hitting the object, choose max
+ //} else if (bbox->maxExtent() > 3 * radius) {
+ //}
+ // else if object > 0.5 brush: test 4 corners of bbox and center on being in the brush, choose max
+ // else if still smaller, then check only the object center:
+ } else {
+ this_force = force * tweak_profile (Geom::L2 (p - center), radius);
+ }
+
+ if (this_force > 0.002) {
+
+ if (do_blur) {
+ Geom::OptRect bbox = item->documentGeometricBounds();
+ if (!bbox) {
+ return did;
+ }
+
+ double blur_now = 0;
+ Geom::Affine i2dt = item->i2dt_affine ();
+ if (style->filter.set && style->getFilter()) {
+ //cycle through filter primitives
+ for (auto& primitive_obj: style->getFilter()->children) {
+ SPFilterPrimitive *primitive = dynamic_cast<SPFilterPrimitive *>(&primitive_obj);
+ if (primitive) {
+ //if primitive is gaussianblur
+ SPGaussianBlur * spblur = dynamic_cast<SPGaussianBlur *>(primitive);
+ if (spblur) {
+ float num = spblur->stdDeviation.getNumber();
+ blur_now += num * i2dt.descrim(); // sum all blurs in the filter
+ }
+ }
+ }
+ }
+ double perimeter = bbox->dimensions()[Geom::X] + bbox->dimensions()[Geom::Y];
+ blur_now = blur_now / perimeter;
+
+ double blur_new;
+ if (reverse) {
+ blur_new = blur_now - 0.06 * force;
+ } else {
+ blur_new = blur_now + 0.06 * force;
+ }
+ if (blur_new < 0.0005 && blur_new < blur_now) {
+ blur_new = 0;
+ }
+ if (blur_new == 0) {
+ remove_filter(item, false);
+ } else {
+ double radius = blur_new * perimeter;
+ SPFilter *filter = modify_filter_gaussian_blur_from_item(item->document, item, radius);
+ sp_style_set_property_url(item, "filter", filter, false);
+ }
+ return true; // do not do colors, blur is a separate mode
+ }
+
+ if (do_fill) {
+ if (style->fill.isPaintserver()) {
+ tweak_colors_in_gradient(item, Inkscape::FOR_FILL, fill_goal, p, radius, this_force, mode, do_h, do_s, do_l, do_o);
+ did = true;
+ } else if (style->fill.isColor()) {
+ tweak_color (mode, style->fill.value.color.v.c, fill_goal, this_force, do_h, do_s, do_l);
+ item->updateRepr();
+ did = true;
+ }
+ }
+ if (do_stroke) {
+ if (style->stroke.isPaintserver()) {
+ tweak_colors_in_gradient(item, Inkscape::FOR_STROKE, stroke_goal, p, radius, this_force, mode, do_h, do_s, do_l, do_o);
+ did = true;
+ } else if (style->stroke.isColor()) {
+ tweak_color (mode, style->stroke.value.color.v.c, stroke_goal, this_force, do_h, do_s, do_l);
+ item->updateRepr();
+ did = true;
+ }
+ }
+ if (do_opacity && do_o) {
+ tweak_opacity (mode, &style->opacity, opacity_goal, this_force);
+ }
+ }
+}
+
+return did;
+}
+
+
+ static bool
+sp_tweak_dilate (TweakTool *tc, Geom::Point event_p, Geom::Point p, Geom::Point vector, bool reverse)
+{
+ 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->selection->isEmpty()) {
+ num = (guint)boost::distance(_desktop->selection->items());
+ }
+ if (num == 0) {
+ this->message_context->flash(Inkscape::ERROR_MESSAGE, _("<b>Nothing selected!</b> Select objects to tweak."));
+ }
+
+ // dilating:
+ if (this->is_drawing && ( event->motion.state & GDK_BUTTON1_MASK )) {
+ sp_tweak_dilate (this, motion_w, motion_doc, motion_doc - this->last_push, event->button.state & GDK_SHIFT_MASK? true : false);
+ //this->last_push = motion_doc;
+ this->has_dilated = true;
+ // it's slow, so prevent clogging up with events
+ gobble_motion_events(GDK_BUTTON1_MASK);
+ return TRUE;
+ }
+
+ }
+ break;
+ case GDK_BUTTON_RELEASE:
+ {
+ Geom::Point const motion_w(event->button.x, event->button.y);
+ Geom::Point const motion_dt(_desktop->w2d(motion_w));
+
+ 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..d185666
--- /dev/null
+++ b/src/ui/tools/tweak-tool.h
@@ -0,0 +1,104 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __SP_TWEAK_CONTEXT_H__
+#define __SP_TWEAK_CONTEXT_H__
+
+/*
+ * tweaking paths without node editing
+ *
+ * Authors:
+ * bulia byak
+ *
+ * Copyright (C) 2007 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/tools/tool-base.h"
+#include <2geom/point.h>
+
+#define SAMPLING_SIZE 8 /* fixme: ?? */
+
+#define TC_MIN_PRESSURE 0.0
+#define TC_MAX_PRESSURE 1.0
+#define TC_DEFAULT_PRESSURE 0.35
+
+namespace Inkscape {
+
+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 */
+ gdouble pressure;
+
+ /* attributes */
+ bool dragging; /* mouse state: mouse is dragging */
+ bool usepressure;
+ bool usetilt;
+
+ double width;
+ double force;
+ double fidelity;
+
+ gint mode;
+
+ bool is_drawing;
+
+ bool is_dilating;
+ bool has_dilated;
+ Geom::Point last_push;
+ Inkscape::CanvasItemBpath *dilate_area;
+
+ bool do_h;
+ bool do_s;
+ bool do_l;
+ bool do_o;
+
+ sigc::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..7ce5e32
--- /dev/null
+++ b/src/ui/util.cpp
@@ -0,0 +1,93 @@
+// 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 <gtkmm.h>
+
+/*
+ * Ellipse text if longer than maxlen, "50% start text + ... + ~50% end text"
+ * Text should be > length 8 or just return the original text
+ */
+Glib::ustring ink_ellipsize_text(Glib::ustring const &src, size_t maxlen)
+{
+ if (src.length() > maxlen && maxlen > 8) {
+ size_t p1 = (size_t) maxlen / 2;
+ size_t p2 = (size_t) src.length() - (maxlen - p1 - 1);
+ return src.substr(0, p1) + "…" + src.substr(p2);
+ }
+ return src;
+}
+
+/**
+ * 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* 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 {
+namespace UI {
+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);
+ }
+}
+}
+}
+
+
+Gdk::RGBA get_background_color(const Glib::RefPtr<Gtk::StyleContext> &context,
+ Gtk::StateFlags state) {
+ GdkRGBA *c;
+
+ gtk_style_context_get(context->gobj(),
+ static_cast<GtkStateFlags>(state),
+ GTK_STYLE_PROPERTY_BACKGROUND_COLOR, &c,
+ nullptr);
+ auto bg_color = Glib::wrap(c);
+
+ return bg_color;
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ 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..67bf25b
--- /dev/null
+++ b/src/ui/util.h
@@ -0,0 +1,59 @@
+// 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 <gdkmm/rgba.h>
+#include <gtkmm/stylecontext.h>
+
+namespace Glib {
+class ustring;
+}
+
+namespace Gtk {
+class Revealer;
+class Widget;
+}
+
+
+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* widget);
+
+namespace Inkscape {
+namespace UI {
+// Utility function to ensure correct sizing after adding child widgets
+void resize_widget_children(Gtk::Widget *widget);
+}
+}
+
+// Get the background-color style property for a given StyleContext
+Gdk::RGBA get_background_color(const Glib::RefPtr<Gtk::StyleContext> &context,
+ Gtk::StateFlags state = static_cast<Gtk::StateFlags>(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/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..45755e2
--- /dev/null
+++ b/src/ui/view/svg-view-widget.cpp
@@ -0,0 +1,261 @@
+// 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()
+{
+ if (_document) {
+ _document = nullptr;
+ }
+}
+
+void
+SVGViewWidget::setDocument(SPDocument* document)
+{
+ // Clear old document
+ if (_document) {
+ _document->getRoot()->invoke_hide(_dkey); // Removed from display tree
+ }
+
+ // Add new document
+ if (document) {
+ _document = document;
+
+ Inkscape::DrawingItem *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..2d5b9ab
--- /dev/null
+++ b/src/ui/view/view.cpp
@@ -0,0 +1,124 @@
+// 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 {
+
+static void
+_onResized (double x, double y, View* v)
+{
+ v->onResized (x,y);
+}
+
+static void
+_onRedrawRequested (View* v)
+{
+ v->onRedrawRequested();
+}
+
+static void
+_onStatusMessage (Inkscape::MessageType type, gchar const *message, View* v)
+{
+ v->onStatusMessage (type, message);
+}
+
+static void
+_onDocumentFilenameSet (gchar const* filename, View* v)
+{
+ v->onDocumentFilenameSet (filename);
+}
+
+//--------------------------------------------------------------------
+View::View()
+: _doc(nullptr)
+{
+ _message_stack = std::make_shared<Inkscape::MessageStack>();
+ _tips_message_context = std::unique_ptr<Inkscape::MessageContext>(new Inkscape::MessageContext(_message_stack));
+
+ _resized_connection = _resized_signal.connect (sigc::bind (sigc::ptr_fun (&_onResized), this));
+ _redraw_requested_connection = _redraw_requested_signal.connect (sigc::bind (sigc::ptr_fun (&_onRedrawRequested), this));
+
+ _message_changed_connection = _message_stack->connectChanged (sigc::bind (sigc::ptr_fun (&_onStatusMessage), this));
+}
+
+View::~View()
+{
+ _close();
+}
+
+void View::_close() {
+ _message_changed_connection.disconnect();
+
+ _tips_message_context = nullptr;
+
+ _message_stack = nullptr;
+
+ if (_doc) {
+ _document_uri_set_connection.disconnect();
+ if (INKSCAPE.remove_document(_doc)) {
+ // this was the last view of this document, so delete it
+ // delete _doc; Delete now handled in Inkscape::Application
+ }
+ _doc = nullptr;
+ }
+}
+
+void View::emitResized (double width, double height)
+{
+ _resized_signal.emit (width, height);
+}
+
+void View::requestRedraw()
+{
+ _redraw_requested_signal.emit();
+}
+
+void View::setDocument(SPDocument *doc) {
+ g_return_if_fail(doc != nullptr);
+
+ if (_doc) {
+ _document_uri_set_connection.disconnect();
+ if (INKSCAPE.remove_document(_doc)) {
+ // this was the last view of this document, so delete it
+ // delete _doc; Delete now handled in Inkscape::Application
+ }
+ }
+
+ INKSCAPE.add_document(doc);
+
+ _doc = doc;
+ _document_uri_set_connection = _doc->connectFilenameSet(sigc::bind(sigc::ptr_fun(&_onDocumentFilenameSet), this));
+ _document_filename_set_signal.emit( _doc->getDocumentFilename() );
+}
+
+}}}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 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..3e3545a
--- /dev/null
+++ b/src/ui/view/view.h
@@ -0,0 +1,145 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_UI_VIEW_VIEW_H
+#define INKSCAPE_UI_VIEW_VIEW_H
+/*
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ *
+ * Copyright (C) 2001-2002 Lauris Kaplinski
+ * Copyright (C) 2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gdk/gdk.h>
+#include <cstddef>
+#include <memory>
+#include <sigc++/connection.h>
+#include "message.h"
+#include "inkgc/gc-managed.h"
+#include "gc-finalized.h"
+#include "gc-anchored.h"
+#include <2geom/forward.h>
+
+/**
+ * Iterates until true or returns false.
+ * When used as signal accumulator, stops emission if one slot returns true.
+ */
+struct StopOnTrue {
+ typedef bool result_type;
+
+ template<typename T_iterator>
+ result_type operator()(T_iterator first, T_iterator last) const{
+ for (; first != last; ++first)
+ if (*first) return true;
+ return false;
+ }
+};
+
+/**
+ * Iterates until nonzero or returns 0.
+ * When used as signal accumulator, stops emission if one slot returns nonzero.
+ */
+struct StopOnNonZero {
+ typedef int result_type;
+
+ template<typename T_iterator>
+ result_type operator()(T_iterator first, T_iterator last) const{
+ for (; first != last; ++first)
+ if (*first) return *first;
+ return 0;
+ }
+};
+
+class SPDocument;
+
+namespace Inkscape {
+ class MessageContext;
+ class MessageStack;
+ namespace UI {
+ namespace View {
+
+/**
+ * View is an abstract base class of all UI document views. This
+ * includes both the editing window and the SVG preview, but does not
+ * include the non-UI RGBA buffer-based Inkscape::Drawing nor the XML editor or
+ * similar views. The View base class has very little functionality of
+ * its own.
+ */
+class View : public GC::Managed<>,
+ public GC::Finalized,
+ public GC::Anchored
+{
+public:
+
+ View();
+
+ /**
+ * Deletes and nulls all View message stacks and disconnects it from signals.
+ */
+ ~View() override;
+
+ void close() { _close(); }
+
+ /// Returns a pointer to the view's document.
+ SPDocument *doc() const
+ { return _doc; }
+ /// Returns a pointer to the view's message stack.
+ std::shared_ptr<Inkscape::MessageStack> messageStack() const
+ { return _message_stack; }
+ /// Returns a pointer to the view's tipsMessageContext.
+ Inkscape::MessageContext *tipsMessageContext() const
+ { return _tips_message_context.get(); }
+
+ void emitResized(gdouble width, gdouble height);
+ void requestRedraw();
+
+ virtual void onResized (double, double) {};
+ virtual void onRedrawRequested() {};
+ virtual void onStatusMessage (Inkscape::MessageType type, gchar const *message) {};
+ virtual void 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;
+ sigc::signal<void> _redraw_requested_signal;
+
+private:
+ sigc::connection _resized_connection;
+ sigc::connection _redraw_requested_connection;
+ sigc::connection _message_changed_connection; // foreign
+ sigc::connection _document_uri_set_connection; // foreign
+};
+
+}}}
+
+#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..5bcf0fa
--- /dev/null
+++ b/src/ui/widget/alignment-selector.h
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * anchor-selector.h
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef ANCHOR_SELECTOR_H_
+#define ANCHOR_SELECTOR_H_
+
+#include <gtkmm/bin.h>
+#include <gtkmm/button.h>
+#include <gtkmm/grid.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class AlignmentSelector : public Gtk::Bin
+{
+private:
+ Gtk::Button _buttons[9];
+ Gtk::Grid _container;
+
+ sigc::signal<void, int> _alignmentClicked;
+
+ void setupButton(const Glib::ustring &icon, Gtk::Button &button);
+ void btn_activated(int index);
+
+public:
+
+ sigc::signal<void, int> &on_alignmentClicked() { return _alignmentClicked; }
+
+ AlignmentSelector();
+ ~AlignmentSelector() override;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif /* ANCHOR_SELECTOR_H_ */
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/ui/widget/anchor-selector.cpp b/src/ui/widget/anchor-selector.cpp
new file mode 100644
index 0000000..b151a81
--- /dev/null
+++ b/src/ui/widget/anchor-selector.cpp
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * anchor-selector.cpp
+ *
+ * Created on: Mar 22, 2012
+ * Author: denis
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#include "ui/widget/anchor-selector.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+
+#include <gtkmm/image.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+void AnchorSelector::setupButton(const Glib::ustring& icon, Gtk::ToggleButton& button) {
+ Gtk::Image *buttonIcon = Gtk::manage(sp_get_icon_image(icon, Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ buttonIcon->show();
+
+ button.set_relief(Gtk::RELIEF_NONE);
+ button.show();
+ button.add(*buttonIcon);
+ button.set_can_focus(false);
+}
+
+AnchorSelector::AnchorSelector()
+ : _container()
+{
+ set_halign(Gtk::ALIGN_CENTER);
+ setupButton(INKSCAPE_ICON("boundingbox_top_left"), _buttons[0]);
+ setupButton(INKSCAPE_ICON("boundingbox_top"), _buttons[1]);
+ setupButton(INKSCAPE_ICON("boundingbox_top_right"), _buttons[2]);
+ setupButton(INKSCAPE_ICON("boundingbox_left"), _buttons[3]);
+ setupButton(INKSCAPE_ICON("boundingbox_center"), _buttons[4]);
+ setupButton(INKSCAPE_ICON("boundingbox_right"), _buttons[5]);
+ setupButton(INKSCAPE_ICON("boundingbox_bottom_left"), _buttons[6]);
+ setupButton(INKSCAPE_ICON("boundingbox_bottom"), _buttons[7]);
+ setupButton(INKSCAPE_ICON("boundingbox_bottom_right"), _buttons[8]);
+
+ _container.set_row_homogeneous();
+ _container.set_column_homogeneous(true);
+
+ for (int i = 0; i < 9; ++i) {
+ _buttons[i].signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &AnchorSelector::btn_activated), i));
+
+ _container.attach(_buttons[i], i % 3, i / 3, 1, 1);
+ }
+ _selection = 4;
+ _buttons[4].set_active();
+
+ this->add(_container);
+}
+
+AnchorSelector::~AnchorSelector()
+{
+ // TODO Auto-generated destructor stub
+}
+
+void AnchorSelector::btn_activated(int index)
+{
+ if (_selection == index && _buttons[index].get_active() == false) {
+ _buttons[index].set_active(true);
+ }
+ else if (_selection != index && _buttons[index].get_active()) {
+ int old_selection = _selection;
+ _selection = index;
+ _buttons[old_selection].set_active(false);
+ _selectionChanged.emit();
+ }
+}
+
+void AnchorSelector::setAlignment(int horizontal, int vertical)
+{
+ int index = 3 * vertical + horizontal;
+ if (index >= 0 && index < 9) {
+ _buttons[index].set_active(!_buttons[index].get_active());
+ }
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/ui/widget/anchor-selector.h b/src/ui/widget/anchor-selector.h
new file mode 100644
index 0000000..49ce0b2
--- /dev/null
+++ b/src/ui/widget/anchor-selector.h
@@ -0,0 +1,62 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * anchor-selector.h
+ *
+ * Created on: Mar 22, 2012
+ * Author: denis
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef ANCHOR_SELECTOR_H_
+#define ANCHOR_SELECTOR_H_
+
+#include <gtkmm/bin.h>
+#include <gtkmm/togglebutton.h>
+#include <gtkmm/grid.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class AnchorSelector : public Gtk::Bin
+{
+private:
+ Gtk::ToggleButton _buttons[9];
+ int _selection;
+ Gtk::Grid _container;
+
+ sigc::signal<void> _selectionChanged;
+
+ void setupButton(const Glib::ustring &icon, Gtk::ToggleButton &button);
+ void btn_activated(int index);
+
+public:
+
+ int getHorizontalAlignment() { return _selection % 3; }
+ int getVerticalAlignment() { return _selection / 3; }
+
+ sigc::signal<void> &on_selectionChanged() { return _selectionChanged; }
+
+ void setAlignment(int horizontal, int vertical);
+
+ AnchorSelector();
+ ~AnchorSelector() override;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif /* ANCHOR_SELECTOR_H_ */
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/ui/widget/attr-widget.h b/src/ui/widget/attr-widget.h
new file mode 100644
index 0000000..ce5fe12
--- /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..19b21d0
--- /dev/null
+++ b/src/ui/widget/canvas-grid.cpp
@@ -0,0 +1,318 @@
+// 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 "ui/icon-loader.h"
+#include "ui/widget/canvas.h"
+#include "ui/widget/ink-ruler.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 = Gtk::manage(new 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
+
+ _canvas_overlay.add(*_canvas);
+ _canvas_overlay.add_overlay(*_command_palette.get_base_widget());
+
+ // 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::manage(new Gtk::Scrollbar(_hadj, Gtk::ORIENTATION_HORIZONTAL));
+ _hscrollbar->set_name("CanvasScrollbar");
+ _hscrollbar->set_hexpand(true);
+ _hscrollbar->set_no_show_all();
+
+ // 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::manage(new Gtk::Scrollbar(_vadj, Gtk::ORIENTATION_VERTICAL));
+ _vscrollbar->set_name("CanvasScrollbar");
+ _vscrollbar->set_vexpand(true);
+ _vscrollbar->set_no_show_all();
+
+ // Horizontal Ruler
+ _hruler = Gtk::manage(new Inkscape::UI::Widget::Ruler(Gtk::ORIENTATION_HORIZONTAL));
+ _hruler->add_track_widget(*_canvas);
+ _hruler->set_hexpand(true);
+ // Tooltip/Unit set elsewhere.
+
+ // For creating guides, etc.
+ _hruler->signal_button_press_event().connect(
+ sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_button_press_event), _hruler, true));
+ _hruler->signal_button_release_event().connect(
+ sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_button_release_event), _hruler, true));
+ _hruler->signal_motion_notify_event().connect(
+ sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_motion_notify_event), _hruler, true));
+
+ // Vertical Ruler
+ _vruler = Gtk::manage(new Inkscape::UI::Widget::Ruler(Gtk::ORIENTATION_VERTICAL));
+ _vruler->add_track_widget(*_canvas);
+ _vruler->set_vexpand(true);
+ // Tooltip/Unit set elsewhere.
+
+ // For creating guides, etc.
+ _vruler->signal_button_press_event().connect(
+ sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_button_press_event), _vruler, false));
+ _vruler->signal_button_release_event().connect(
+ sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_button_release_event), _vruler, false));
+ _vruler->signal_motion_notify_event().connect(
+ sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_motion_notify_event), _vruler, false));
+
+ // Guide Lock
+ auto image1 = Gtk::manage(new Gtk::Image("object-locked", Gtk::ICON_SIZE_MENU));
+ _guide_lock = Gtk::manage(new Gtk::ToggleButton());
+ _guide_lock->set_name("LockGuides");
+ _guide_lock->add(*image1);
+ _guide_lock->set_no_show_all();
+ // 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"));
+
+ // CMS Adjust
+ auto image2 = Gtk::manage(new Gtk::Image("color-management", Gtk::ICON_SIZE_MENU));
+ _cms_adjust = Gtk::manage(new Gtk::ToggleButton());
+ _cms_adjust->set_name("CMS_Adjust");
+ _cms_adjust->add(*image2);
+ // 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"));
+ _cms_adjust->set_no_show_all();
+
+ // Sticky Zoom
+ auto image3 = Gtk::manage(sp_get_icon_image("zoom-original", Gtk::ICON_SIZE_MENU));
+ _sticky_zoom = Gtk::manage(new Gtk::ToggleButton());
+ _sticky_zoom->set_name("StickyZoom");
+ _sticky_zoom->add(*image3);
+ // To be replaced by Gio::Action:
+ _sticky_zoom->signal_toggled().connect(sigc::mem_fun(_dtw, &SPDesktopWidget::sticky_zoom_toggled));
+ _sticky_zoom->set_tooltip_text(_("Zoom drawing if window size changes"));
+ _sticky_zoom->set_no_show_all();
+
+ // Top row
+ attach(*_guide_lock, 0, 0, 1, 1);
+ attach(*_hruler, 1, 0, 1, 1);
+ attach(*_sticky_zoom, 2, 0, 1, 1);
+
+ // Middle row
+ attach(*_vruler, 0, 1, 1, 1);
+ attach(_canvas_overlay, 1, 1, 1, 1);
+ attach(*_vscrollbar, 2, 1, 1, 1);
+
+ // Bottom row
+ attach(*_hscrollbar, 1, 2, 1, 1);
+ attach(*_cms_adjust, 2, 2, 1, 1);
+
+ // Update rulers on size change.
+ signal_size_allocate().connect(sigc::mem_fun(*this, &CanvasGrid::OnSizeAllocate));
+
+ show_all();
+}
+
+CanvasGrid::~CanvasGrid() {
+}
+
+// _dt2r should be a member of _canvas.
+// get_display_area should be a member of _canvas.
+void
+CanvasGrid::UpdateRulers()
+{
+ Geom::Rect viewbox = _dtw->desktop->get_display_area().bounds();
+ // Use integer values of the canvas for calculating the display area, similar
+ // to the integer values used for positioning the grid lines. (see Canvas::scrollTo(),
+ // where ix and iy are rounded integer values; these values are stored in CanvasItemBuffer->rect,
+ // and used for drawing the grid). By using the integer values here too, the ruler ticks
+ // will be perfectly aligned to the grid
+ double _dt2r = _dtw->_dt2r;
+ Geom::Point _ruler_origin = _dtw->_ruler_origin;
+
+ double lower_x = _dt2r * (viewbox.left() - _ruler_origin[Geom::X]);
+ double upper_x = _dt2r * (viewbox.right() - _ruler_origin[Geom::X]);
+ _hruler->set_range(lower_x, upper_x);
+
+ double lower_y = _dt2r * (viewbox.bottom() - _ruler_origin[Geom::Y]);
+ double upper_y = _dt2r * (viewbox.top() - _ruler_origin[Geom::Y]);
+ if (_dtw->desktop->is_yaxisdown()) {
+ std::swap(lower_y, upper_y);
+ }
+ _vruler->set_range(lower_y, upper_y);
+}
+
+void
+CanvasGrid::ShowScrollbars(bool state)
+{
+ _show_scrollbars = state;
+
+ if (_show_scrollbars) {
+ // Show scrollbars
+ _hscrollbar->show();
+ _vscrollbar->show();
+ _cms_adjust->show();
+ _cms_adjust->show_all_children();
+ _sticky_zoom->show();
+ _sticky_zoom->show_all_children();
+ } else {
+ // Hide scrollbars
+ _hscrollbar->hide();
+ _vscrollbar->hide();
+ _cms_adjust->hide();
+ _sticky_zoom->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)
+{
+ // Sticky zoom button is always shown. We must adjust canvas when rulers are toggled or canvas
+ // won't expand fully.
+ _show_rulers = state;
+
+ if (_show_rulers) {
+ // Show rulers
+ _hruler->show();
+ _vruler->show();
+ _guide_lock->show();
+ _guide_lock->show_all_children();
+ remove(_canvas_overlay);
+ attach(_canvas_overlay, 1, 1, 1, 1);
+ } else {
+ // Hide rulers
+ _hruler->hide();
+ _vruler->hide();
+ _guide_lock->hide();
+ remove(_canvas_overlay);
+ attach(_canvas_overlay, 1, 0, 1, 2);
+ }
+}
+
+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::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::OnSizeAllocate(Gtk::Allocation& 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) {
+ if (event->button.state & GDK_SHIFT_MASK) {
+ _dtw->desktop->getCanvasDrawing()->set_sticky(true);
+ } else {
+ _dtw->desktop->getCanvasDrawing()->set_sticky(false);
+ }
+ }
+
+ // 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..17a986f
--- /dev/null
+++ b/src/ui/widget/canvas-grid.h
@@ -0,0 +1,113 @@
+// 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 "ui/dialog/command-palette.h"
+
+class SPCanvas;
+class SPDesktopWidget;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class Canvas;
+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
+{
+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();
+
+ Inkscape::UI::Widget::Canvas *GetCanvas() { return _canvas; };
+
+ // Hopefully temp.
+ Inkscape::UI::Widget::Ruler *GetHRuler() { return _vruler; };
+ Inkscape::UI::Widget::Ruler *GetVRuler() { return _hruler; };
+ Glib::RefPtr<Gtk::Adjustment> GetHAdj() { return _hadj; };
+ Glib::RefPtr<Gtk::Adjustment> GetVAdj() { return _vadj; };
+ Gtk::ToggleButton *GetGuideLock() { return _guide_lock; }
+ Gtk::ToggleButton *GetCmsAdjust() { return _cms_adjust; }
+ Gtk::ToggleButton *GetStickyZoom() { return _sticky_zoom; };
+
+private:
+
+ // Signal callbacks
+ void OnSizeAllocate(Gtk::Allocation& allocation);
+ bool SignalEvent(GdkEvent *event);
+
+ // The Widgets
+ Inkscape::UI::Widget::Canvas *_canvas;
+
+ Gtk::Overlay _canvas_overlay;
+
+ Dialog::CommandPalette _command_palette;
+
+ Glib::RefPtr<Gtk::Adjustment> _hadj;
+ Glib::RefPtr<Gtk::Adjustment> _vadj;
+ Gtk::Scrollbar *_hscrollbar;
+ Gtk::Scrollbar *_vscrollbar;
+
+ Inkscape::UI::Widget::Ruler *_hruler;
+ Inkscape::UI::Widget::Ruler *_vruler;
+
+ Gtk::ToggleButton *_guide_lock;
+ Gtk::ToggleButton *_cms_adjust;
+ Gtk::ToggleButton *_sticky_zoom;
+
+ // To be replaced by stateful Gio::Actions
+ bool _show_scrollbars = true;
+ bool _show_rulers = true;
+
+ // Hopefully temp
+ SPDesktopWidget *_dtw;
+
+ // Store allocation so we don't redraw too often.
+ Gtk::Allocation _allocation;
+};
+
+} // 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.cpp b/src/ui/widget/canvas.cpp
new file mode 100644
index 0000000..8e6de8c
--- /dev/null
+++ b/src/ui/widget/canvas.cpp
@@ -0,0 +1,2797 @@
+// 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 <glibmm/i18n.h>
+
+#include <2geom/rect.h>
+
+#include "canvas.h"
+#include "canvas-grid.h"
+
+#include "color.h" // Background color
+#include "cms-system.h" // Color correction
+#include "desktop.h"
+#include "preferences.h"
+
+#include "display/cairo-utils.h" // Checkerboard background
+#include "display/drawing.h"
+#include "display/control/canvas-item-group.h"
+#include "display/control/snap-indicator.h"
+
+#include "ui/tools/tool-base.h" // Default cursor
+
+#include "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 {
+namespace UI {
+namespace Widget {
+
+/*
+ * GDK event utilities
+ */
+
+// GdkEvents can only be safely copied using gdk_event_copy. However, this function allocates. Therefore, 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(const GdkEvent &ev) {return GdkEventUniqPtr(gdk_event_copy(&ev));}
+
+/*
+ * Preferences
+ */
+
+template<typename T>
+struct Pref {};
+
+template<typename T>
+struct PrefBase
+{
+ const char *path;
+ T t, def;
+ std::unique_ptr<Preferences::PreferencesObserver> obs;
+ std::function<void()> action;
+ operator T() const {return t;}
+ PrefBase(const char *path, T def) : path(path), def(def) {}
+ void act() {if (action) action();}
+ void enable() {t = static_cast<Pref<T>*>(this)->read(); act(); obs = Inkscape::Preferences::get()->createObserver(path, [this] (const Preferences::Entry &e) {t = static_cast<Pref<T>*>(this)->changed(e); act();});}
+ void disable() {t = def; act(); obs.reset();}
+ void set_enabled(bool enabled) {enabled ? enable() : disable();}
+};
+
+template<>
+struct Pref<bool> : PrefBase<bool>
+{
+ Pref(const char *path, bool def = false) : PrefBase(path, def) {enable();}
+ bool read() {return Inkscape::Preferences::get()->getBool(path, def);}
+ bool changed(const Preferences::Entry &e) {return e.getBool(def);}
+};
+
+template<>
+struct Pref<int> : PrefBase<int>
+{
+ int min, max;
+ Pref(const char *path, int def, int min, int max) : PrefBase(path, def), min(min), max(max) {enable();}
+ int read() {return Inkscape::Preferences::get()->getIntLimited(path, def, min, max);}
+ int changed(const Preferences::Entry &e) {return e.getIntLimited(def, min, max);}
+};
+
+template<>
+struct Pref<double> : PrefBase<double>
+{
+ double min, max;
+ Pref(const char *path, double def, double min, double max) : PrefBase(path, def), min(min), max(max) {enable();}
+ double read() {return Inkscape::Preferences::get()->getDoubleLimited(path, def, min, max);}
+ double changed(const Preferences::Entry &e) {return e.getDoubleLimited(def, min, max);}
+};
+
+struct Prefs
+{
+ // Original parameters
+ Pref<int> tile_size = Pref<int> ("/options/rendering/tile-size", 16, 1, 10000);
+ Pref<int> tile_multiplier = Pref<int> ("/options/rendering/tile-multiplier", 16, 1, 512);
+ Pref<int> x_ray_radius = Pref<int> ("/options/rendering/xray-radius", 100, 1, 1500);
+ Pref<bool> from_display = Pref<bool> ("/options/displayprofile/from_display");
+ Pref<int> grabsize = Pref<int> ("/options/grabsize/value", 3, 1, 15);
+ Pref<int> outline_overlay_opacity = Pref<int> ("/options/rendering/outline-overlay-opacity", 50, 1, 100);
+
+ // New parameters
+ Pref<int> update_strategy = Pref<int> ("/options/rendering/update_strategy", 3, 1, 3);
+ Pref<int> render_time_limit = Pref<int> ("/options/rendering/render_time_limit", 1000, 100, 1000000);
+ Pref<bool> use_new_bisector = Pref<bool> ("/options/rendering/use_new_bisector", true);
+ Pref<int> new_bisector_size = Pref<int> ("/options/rendering/new_bisector_size", 500, 1, 10000);
+ Pref<double> max_affine_diff = Pref<double>("/options/rendering/max_affine_diff", 1.8, 0.0, 100.0);
+ Pref<int> pad = Pref<int> ("/options/rendering/pad", 200, 0, 1000);
+ Pref<int> coarsener_min_size = Pref<int> ("/options/rendering/coarsener_min_size", 200, 0, 1000);
+ Pref<int> coarsener_glue_size = Pref<int> ("/options/rendering/coarsener_glue_size", 80, 0, 1000);
+ Pref<double> coarsener_min_fullness = Pref<double>("/options/rendering/coarsener_min_fullness", 0.3, 0.0, 1.0);
+
+ // Debug switches
+ Pref<bool> debug_framecheck = Pref<bool> ("/options/rendering/debug_framecheck");
+ Pref<bool> debug_logging = Pref<bool> ("/options/rendering/debug_logging");
+ Pref<bool> debug_slow_redraw = Pref<bool> ("/options/rendering/debug_slow_redraw");
+ Pref<int> debug_slow_redraw_time = Pref<int> ("/options/rendering/debug_slow_redraw_time", 50, 0, 1000000);
+ Pref<bool> debug_show_redraw = Pref<bool> ("/options/rendering/debug_show_redraw");
+ Pref<bool> debug_show_unclean = Pref<bool> ("/options/rendering/debug_show_unclean");
+ Pref<bool> debug_show_snapshot = Pref<bool> ("/options/rendering/debug_show_snapshot");
+ Pref<bool> debug_show_clean = Pref<bool> ("/options/rendering/debug_show_clean");
+ Pref<bool> debug_disable_redraw = Pref<bool> ("/options/rendering/debug_disable_redraw");
+ Pref<bool> debug_sticky_decoupled = Pref<bool> ("/options/rendering/debug_sticky_decoupled");
+
+ // Developer mode
+ Pref<bool> devmode = Pref<bool>("/options/rendering/devmode");
+ void set_devmode(bool on);
+};
+
+void Prefs::set_devmode(bool on)
+{
+ tile_size.set_enabled(on);
+ render_time_limit.set_enabled(on);
+ use_new_bisector.set_enabled(on);
+ new_bisector_size.set_enabled(on);
+ max_affine_diff.set_enabled(on);
+ pad.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_slow_redraw.set_enabled(on);
+ debug_slow_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);
+}
+
+/*
+ * Conversion functions
+ */
+
+auto geom_to_cairo(Geom::IntRect rect)
+{
+ return Cairo::RectangleInt{rect.left(), rect.top(), rect.width(), rect.height()};
+}
+
+auto cairo_to_geom(Cairo::RectangleInt rect)
+{
+ return Geom::IntRect::from_xywh(rect.x, rect.y, rect.width, rect.height);
+}
+
+auto geom_to_cairo(Geom::Affine affine)
+{
+ return Cairo::Matrix(affine[0], affine[1], affine[2], affine[3], affine[4], affine[5]);
+}
+
+auto geom_act(Geom::Affine a, Geom::IntPoint p)
+{
+ Geom::Point p2 = p;
+ p2 *= a;
+ return Geom::IntPoint(std::round(p2.x()), std::round(p2.y()));
+}
+
+void region_to_path(const Cairo::RefPtr<Cairo::Context> &cr, const Cairo::RefPtr<Cairo::Region> &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);
+ }
+}
+
+/*
+ * Update strategy
+ */
+
+// A class hierarchy for controlling what order to update invalidated regions.
+class Updater
+{
+public:
+ // The subregion of the store with up-to-date content.
+ Cairo::RefPtr<Cairo::Region> clean_region;
+
+ Updater(Cairo::RefPtr<Cairo::Region> clean_region) : clean_region(std::move(clean_region)) {}
+
+ virtual void reset() {clean_region = Cairo::Region::create();} // Reset the clean region to empty.
+ virtual void intersect (const Geom::IntRect &rect) {clean_region->intersect(geom_to_cairo(rect));} // Called when the store changes position; clip everything to the new store rectangle.
+ virtual void mark_dirty(const Geom::IntRect &rect) {clean_region->subtract (geom_to_cairo(rect));} // Called on every invalidate event.
+ virtual void mark_clean(const Geom::IntRect &rect) {clean_region->do_union (geom_to_cairo(rect));} // Called on every rectangle redrawn.
+
+ virtual Cairo::RefPtr<Cairo::Region> get_next_clean_region() {return clean_region;}; // Called by on_idle to determine what regions to consider clean for the current redraw.
+ virtual bool report_finished () {return false;} // Called in on_idle if the redraw has finished. Returns true to indicate that further redraws are required with a different clean region.
+ virtual void frame () {} // Called by on_draw to notify the updater of the display of the frame.
+ virtual ~Updater() = default;
+};
+
+// Responsive updater: As soon as a region is invalidated, redraw it.
+using ResponsiveUpdater = Updater;
+
+// Full redraw updater: When a region is invalidated, delay redraw until after the current redraw is completed.
+class FullredrawUpdater : public Updater
+{
+ // 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:
+
+ FullredrawUpdater(Cairo::RefPtr<Cairo::Region> clean_region) : Updater(std::move(clean_region)) {}
+
+ void reset() override
+ {
+ Updater::reset();
+ inprogress = false;
+ old_clean_region.clear();
+ }
+
+ void intersect(const Geom::IntRect &rect) override
+ {
+ Updater::intersect(rect);
+ if (old_clean_region) old_clean_region->intersect(geom_to_cairo(rect));
+ }
+
+ void mark_dirty(const Geom::IntRect &rect) override
+ {
+ if (inprogress && !old_clean_region) old_clean_region = clean_region->copy();
+ Updater::mark_dirty(rect);
+ }
+
+ void mark_clean(const Geom::IntRect &rect) override
+ {
+ Updater::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;
+ }
+ }
+};
+
+// Multiscale updater: Updates tiles near the mouse faster. Gives the best of both.
+class MultiscaleUpdater : public Updater
+{
+ // 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:
+
+ MultiscaleUpdater(Cairo::RefPtr<Cairo::Region> clean_region) : Updater(std::move(clean_region)) {}
+
+ void reset() override
+ {
+ Updater::reset();
+ inprogress = activated = false;
+ }
+
+ void intersect(const Geom::IntRect &rect) override
+ {
+ Updater::intersect(rect);
+ if (activated) {
+ for (auto &reg : blocked) {
+ reg->intersect(geom_to_cairo(rect));
+ }
+ }
+ }
+
+ void mark_dirty(const Geom::IntRect &rect) override
+ {
+ Updater::mark_dirty(rect);
+ if (inprogress && !activated) {
+ counter = scale = elapsed = 0;
+ blocked = {Cairo::Region::create()};
+ activated = true;
+ }
+ }
+
+ void mark_clean(const Geom::IntRect &rect) override
+ {
+ Updater::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 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]);
+ }
+ }
+};
+
+std::unique_ptr<Updater>
+make_updater(int type, Cairo::RefPtr<Cairo::Region> clean_region = Cairo::Region::create())
+{
+ switch (type) {
+ case 1: return std::make_unique<ResponsiveUpdater>(std::move(clean_region));
+ case 2: return std::make_unique<FullredrawUpdater>(std::move(clean_region));
+ default:
+ case 3: return std::make_unique<MultiscaleUpdater>(std::move(clean_region));
+ }
+}
+
+/*
+ * Implementation class
+ */
+
+class CanvasPrivate
+{
+public:
+
+ friend class Canvas;
+ Canvas *q;
+ CanvasPrivate(Canvas *q) : q(q) {}
+
+ // Lifecycle
+ bool active = false;
+ void update_active();
+ void activate();
+ void deactivate();
+
+ // Preferences
+ Prefs prefs;
+
+ // Update strategy; tracks the unclean region and decides how to redraw it.
+ std::unique_ptr<Updater> updater;
+
+ // Event processor. Events that interact with the Canvas are buffered here until the start of the next frame. They are processed by a separate object so that deleting the Canvas mid-event can be done safely.
+ struct EventProcessor
+ {
+ std::vector<GdkEventUniqPtr> events;
+ int pos;
+ GdkEvent *ignore = nullptr;
+ CanvasPrivate *canvasprivate; // Nulled on destruction.
+ bool in_processing = false; // For handling recursion due to nested GTK main loops.
+ void process();
+ int gobble_key_events(guint keyval, guint mask);
+ void gobble_motion_events(guint mask);
+ };
+ std::shared_ptr<EventProcessor> eventprocessor; // Usually held by CanvasPrivate, but temporarily also held by itself while processing so that it is not deleted mid-event.
+ bool add_to_bucket(GdkEvent*);
+ bool process_bucketed_event(const GdkEvent&);
+ bool pick_current_item(const GdkEvent&);
+ bool emit_event(const GdkEvent&);
+ Inkscape::CanvasItem *pre_scroll_grabbed_item;
+
+ // State for determining when to run event processor.
+ bool pending_draw = false;
+ sigc::connection bucket_emptier;
+ std::optional<guint> bucket_emptier_tick_callback;
+ void schedule_bucket_emptier();
+
+ // Idle system. The high priority idle ensures at least one idle cycle between add_idle and on_draw.
+ void add_idle();
+ sigc::connection hipri_idle;
+ sigc::connection lopri_idle;
+ bool on_hipri_idle();
+ bool on_lopri_idle();
+ bool idle_running = false;
+
+ // Important global properties of all the stores. If these change, all the stores must be recreated.
+ int _device_scale = 1;
+ bool _store_solid_background;
+
+ // The backing store.
+ Geom::IntRect _store_rect;
+ Geom::Affine _store_affine;
+ Cairo::RefPtr<Cairo::ImageSurface> _backing_store, _outline_store;
+
+ // The snapshot store. Used to mask redraw delay on zoom/rotate.
+ Geom::IntRect _snapshot_rect;
+ Geom::Affine _snapshot_affine;
+ Geom::IntPoint _snapshot_static_offset = {0, 0};
+ Cairo::RefPtr<Cairo::ImageSurface> _snapshot_store, _snapshot_outline_store;
+ Cairo::RefPtr<Cairo::Region> _snapshot_clean_region;
+
+ Geom::Affine geom_affine; // The affine the geometry was last imbued with.
+ bool decoupled_mode = false;
+
+ bool solid_background; // Whether the last background set is solid.
+ bool need_outline_store() const {return q->_split_mode != Inkscape::SplitMode::NORMAL || q->_render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY;}
+
+ // Drawing
+ bool on_idle();
+ void paint_rect_internal(Geom::IntRect const &rect);
+ void paint_single_buffer(Geom::IntRect const &paint_rect, Cairo::RefPtr<Cairo::ImageSurface> const &store, bool is_backing_store, bool outline_overlay_pass);
+ std::optional<Geom::Dim2> old_bisector(const Geom::IntRect &rect);
+ std::optional<Geom::Dim2> new_bisector(const Geom::IntRect &rect);
+
+ // Trivial overload of GtkWidget function.
+ void queue_draw_area(Geom::IntRect &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::Point> last_mouse;
+};
+
+/*
+ * 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 );
+
+ // Set up EventProcessor
+ d->eventprocessor = std::make_shared<CanvasPrivate::EventProcessor>();
+ d->eventprocessor->canvasprivate = d.get();
+
+ // Updater
+ d->updater = make_updater(d->prefs.update_strategy);
+
+ // Preferences
+ d->prefs.grabsize.action = [=] {_canvas_item_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->add_idle();};
+ d->prefs.debug_sticky_decoupled.action = [=] {d->add_idle();};
+ d->prefs.update_strategy.action = [=] {d->updater = make_updater(d->prefs.update_strategy, std::move(d->updater->clean_region));};
+ d->prefs.outline_overlay_opacity.action = [=] {queue_draw();};
+
+ // Developer mode master switch
+ d->prefs.devmode.action = [=] {d->prefs.set_devmode(d->prefs.devmode);};
+ d->prefs.devmode.action();
+
+ // Cavas item root
+ _canvas_item_root = new Inkscape::CanvasItemGroup(nullptr);
+ _canvas_item_root->set_name("CanvasItemGroup:Root");
+ _canvas_item_root->set_canvas(this);
+
+ // Background
+ _background = Cairo::SolidPattern::create_rgb(1.0, 1.0, 1.0);
+ d->solid_background = true;
+}
+
+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->_drawing_disabled = false;
+ q->_need_update = true;
+
+ // Split view
+ q->_split_direction = Inkscape::SplitDirection::EAST;
+ q->_split_position = {-1, -1}; // initialize with off-canvas coordinates
+ q->_hover_direction = Inkscape::SplitDirection::NONE;
+ q->_split_dragging = false;
+
+ add_idle();
+}
+
+void CanvasPrivate::deactivate()
+{
+ // Disconnect signals and timeouts. (Note: They will never be rescheduled while inactive.)
+ hipri_idle.disconnect();
+ lopri_idle.disconnect();
+ bucket_emptier.disconnect();
+ if (bucket_emptier_tick_callback) q->remove_tick_callback(*bucket_emptier_tick_callback);
+}
+
+Canvas::~Canvas()
+{
+ // Not necessary as GTK guarantees realization is always followed by unrealization. But just in case that invariant breaks, we deal with it.
+ if (d->active) {
+ std::cerr << "Canvas destructed while realized!" << std::endl;
+ d->deactivate();
+ }
+
+ // Disconnect from EventProcessor.
+ d->eventprocessor->canvasprivate = nullptr;
+
+ // Remove entire CanvasItem tree.
+ delete _canvas_item_root;
+}
+
+void CanvasPrivate::update_active()
+{
+ bool new_active = q->_drawing && q->get_realized();
+ if (new_active != active) {
+ active = new_active;
+ active ? activate() : deactivate();
+ }
+}
+
+void Canvas::set_drawing(Drawing *drawing)
+{
+ _drawing = drawing;
+ d->update_active();
+}
+
+void
+Canvas::on_realize()
+{
+ parent_type::on_realize();
+ assert(get_realized());
+ d->update_active();
+}
+
+void Canvas::on_unrealize()
+{
+ parent_type::on_unrealize();
+ assert(!get_realized());
+ d->update_active();
+}
+
+/*
+ * Events system
+ */
+
+// The following protected functions of Canvas are where all incoming events initially arrive.
+// Those that do not interact with the Canvas are processed instantaneously, while the rest are
+// delayed by placing them into the bucket.
+
+bool
+Canvas::on_scroll_event(GdkEventScroll *scroll_event)
+{
+ return d->add_to_bucket(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)
+{
+ 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.
+ switch (button_event->type) {
+ case GDK_BUTTON_PRESS:
+ if (_hover_direction != Inkscape::SplitDirection::NONE) {
+ _split_dragging = true;
+ _split_drag_start = Geom::Point(button_event->x, button_event->y);
+ 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:
+ _split_dragging = false;
+ break;
+ }
+
+ // Otherwise, handle as a delayed event.
+ return d->add_to_bucket(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->add_to_bucket(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->add_to_bucket(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->add_to_bucket(reinterpret_cast<GdkEvent*>(key_event));
+}
+
+bool
+Canvas::on_key_release_event(GdkEventKey *key_event)
+{
+ return d->add_to_bucket(reinterpret_cast<GdkEvent*>(key_event));
+}
+
+bool
+Canvas::on_motion_notify_event(GdkEventMotion *motion_event)
+{
+ // Record the last mouse position.
+ d->last_mouse = Geom::Point(motion_event->x, motion_event->y);
+
+ // Handle interactions with the split view controller.
+ Geom::IntPoint cursor_position = Geom::IntPoint(motion_event->x, motion_event->y);
+
+ // Check if we are near the edge. If so, revert to normal mode.
+ if (_split_mode == Inkscape::SplitMode::SPLIT && _split_dragging) {
+ 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_mode = Inkscape::SplitMode::NORMAL;
+ _split_position = Geom::Point(-1, -1);
+ _hover_direction = Inkscape::SplitDirection::NONE;
+ set_cursor();
+ queue_draw();
+
+ // 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((int)Inkscape::SplitMode::NORMAL);
+
+ return true;
+ }
+ }
+
+ if (_split_mode == Inkscape::SplitMode::XRAY) {
+ _split_position = cursor_position;
+ queue_draw();
+ }
+
+ if (_split_mode == Inkscape::SplitMode::SPLIT) {
+ Inkscape::SplitDirection hover_direction = Inkscape::SplitDirection::NONE;
+ Geom::Point difference(cursor_position - _split_position);
+
+ // Move controller
+ if (_split_dragging) {
+ Geom::Point delta = cursor_position - _split_drag_start; // We don't use _split_position
+ if (_hover_direction == Inkscape::SplitDirection::HORIZONTAL) {
+ _split_position += Geom::Point(0, delta.y());
+ } else if (_hover_direction == Inkscape::SplitDirection::VERTICAL) {
+ _split_position += Geom::Point(delta.x(), 0);
+ } else {
+ _split_position += delta;
+ }
+ _split_drag_start = cursor_position;
+ queue_draw();
+ return true;
+ }
+
+ if (Geom::distance(cursor_position, _split_position) < 20 * d->_device_scale) {
+ // We're hovering over circle, figure out which direction we are in.
+ if (difference.y() - difference.x() > 0) {
+ if (difference.y() + difference.x() > 0) {
+ hover_direction = Inkscape::SplitDirection::SOUTH;
+ } else {
+ hover_direction = Inkscape::SplitDirection::WEST;
+ }
+ } else {
+ if (difference.y() + difference.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(difference.y()) < 3 * d->_device_scale) {
+ // We're hovering over horizontal line
+ hover_direction = Inkscape::SplitDirection::HORIZONTAL;
+ }
+ } else {
+ if (std::abs(difference.x()) < 3 * d->_device_scale) {
+ // We're hovering over 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;
+ }
+ }
+
+ // Otherwise, handle as a delayed event.
+ return d->add_to_bucket(reinterpret_cast<GdkEvent*>(motion_event));
+}
+
+// Most events end up here. We store them in the bucket, and process them as soon as possible after
+// the next 'on_draw'. If 'on_draw' isn't pending, we use the 'tick_callback' signal to process them
+// when 'on_draw' would have run anyway. If 'on_draw' later becomes pending, we remove this signal.
+
+// Add an event to the bucket and ensure it will be emptied in the near future.
+bool
+CanvasPrivate::add_to_bucket(GdkEvent *event)
+{
+ framecheck_whole_function(this)
+
+ if (!active) {
+ std::cerr << "Canvas::add_to_bucket: Called while not active!" << std::endl;
+ return false;
+ }
+
+ // Prevent re-fired events from going through again.
+ if (event == eventprocessor->ignore) {
+ return false;
+ }
+
+ // If this is the first event, ensure event processing will run on the main loop as soon as possible after the next frame has started.
+ if (eventprocessor->events.empty() && !pending_draw) {
+#ifndef NDEBUG
+ if (bucket_emptier_tick_callback) {
+ g_warning("bucket_emptier_tick_callback not empty");
+ }
+#endif
+ bucket_emptier_tick_callback = q->add_tick_callback([this] (const Glib::RefPtr<Gdk::FrameClock>&) {
+ assert(active);
+ bucket_emptier_tick_callback.reset();
+ schedule_bucket_emptier();
+ return false;
+ });
+ }
+
+ // Add a copy to the queue.
+ eventprocessor->events.emplace_back(gdk_event_copy(event));
+
+ // Tell GTK the event was handled.
+ return true;
+}
+
+void CanvasPrivate::schedule_bucket_emptier()
+{
+ if (!active) {
+ std::cerr << "Canvas::schedule_bucket_emptier: Called while not active!" << std::endl;
+ return;
+ }
+
+ if (!bucket_emptier.connected()) {
+ bucket_emptier = Glib::signal_idle().connect([this] {
+ assert(active);
+ eventprocessor->process();
+ return false;
+ }, G_PRIORITY_HIGH_IDLE + 14); // before hipri_idle
+ }
+}
+
+// The following functions run at the start of the next frame on the GTK main loop.
+// (Note: It is crucial that it runs on the main loop and not in any frame clock tick callbacks. GTK does not allow widgets to be deleted in the latter; only the former.)
+
+// Process bucketed events.
+void
+CanvasPrivate::EventProcessor::process()
+{
+ framecheck_whole_function(canvasprivate)
+
+ // Ensure the EventProcessor continues to live even if the Canvas is destroyed during event processing.
+ auto self = canvasprivate->eventprocessor;
+
+ // Check if toplevel or recursive. (Recursive calls happen if processing an event starts its own nested GTK main loop.)
+ bool toplevel = !in_processing;
+ in_processing = true;
+
+ // If toplevel, initialise the iteration index. It may be incremented externally by gobblers or recursive calls.
+ if (toplevel) {
+ pos = 0;
+ }
+
+ while (pos < events.size()) {
+ // Extract next event.
+ auto event = std::move(events[pos]);
+ pos++;
+
+ // Fire the event at the CanvasItems and see if it was handled.
+ bool handled = canvasprivate->process_bucketed_event(*event);
+
+ if (!handled) {
+ // Re-fire the event at the window, and ignore it when it comes back here again.
+ ignore = event.get();
+ canvasprivate->q->get_toplevel()->event(event.get());
+ ignore = nullptr;
+ }
+
+ // If the Canvas was destroyed or deactivated during event processing, exit now.
+ if (!canvasprivate || !canvasprivate->active) return;
+ }
+
+ // Otherwise, clear the list of events that was just processed.
+ events.clear();
+
+ // Reset the variable to track recursive calls.
+ if (toplevel) {
+ in_processing = false;
+ }
+}
+
+// Called during event processing by some tools to batch backlogs of key events that may have built up after a freeze.
+int
+Canvas::gobble_key_events(guint keyval, guint mask)
+{
+ return d->eventprocessor->gobble_key_events(keyval, mask);
+}
+
+int
+CanvasPrivate::EventProcessor::gobble_key_events(guint keyval, guint mask)
+{
+ int count = 0;
+
+ while (pos < events.size()) {
+ auto &event = events[pos];
+ if ((event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE) && event->key.keyval == keyval && (!mask || (event->key.state & mask))) {
+ // Discard event and continue.
+ if (event->type == GDK_KEY_PRESS) count++;
+ pos++;
+ }
+ else {
+ // Stop discarding.
+ break;
+ }
+ }
+
+ if (count > 0 && canvasprivate->prefs.debug_logging) std::cout << "Gobbled " << count << " key press(es)" << std::endl;
+
+ return count;
+}
+
+// Called during event processing by some tools to ignore backlogs of motion events that may have built up after a freeze.
+void
+Canvas::gobble_motion_events(guint mask)
+{
+ d->eventprocessor->gobble_motion_events(mask);
+}
+
+void
+CanvasPrivate::EventProcessor::gobble_motion_events(guint mask)
+{
+ int count = 0;
+
+ while (pos < events.size()) {
+ auto &event = events[pos];
+ if (event->type == GDK_MOTION_NOTIFY && (event->motion.state & mask)) {
+ // Discard event and continue.
+ count++;
+ pos++;
+ }
+ else {
+ // Stop discarding.
+ break;
+ }
+ }
+
+ if (count > 0 && canvasprivate->prefs.debug_logging) std::cout << "Gobbled " << count << " motion event(s)" << std::endl;
+}
+
+// From now on Inkscape's regular event processing logic takes place. The only thing to remember is that
+// all of this happens at a slight delay after the original GTK events. Therefore, it's important to make
+// sure that stateful variables like '_current_canvas_item' and friends are ONLY read/written within these
+// functions, not during the earlier GTK event handlers. Otherwise state confusion will ensue.
+
+bool
+CanvasPrivate::process_bucketed_event(const GdkEvent &event)
+{
+ 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();
+ bool retval = emit_event(event);
+
+ return retval;
+ }
+
+ 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);
+
+ 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_bucketed_event' to manipulate the state variables relating
+// to the current object under the mouse, for example, to generate enter and leave events.
+// (A more detailed explanation by Tavmjong follows.)
+// --------
+// This routine reacts to events from the canvas. It's 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->_canvas_item_root->update(geom_affine);
+ q->_need_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.
+ // (Todo: With C++20, can reduce the code repetition here using a templated lambda.)
+ 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 && q->_canvas_item_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;
+ }
+
+ // If in split mode, look at where cursor is to see if one should pick with outline mode.
+ if (q->_split_mode == Inkscape::SplitMode::SPLIT && q->_render_mode != Inkscape::RenderMode::OUTLINE_OVERLAY) {
+ if ((q->_split_direction == Inkscape::SplitDirection::NORTH && y > q->_split_position.y()) ||
+ (q->_split_direction == Inkscape::SplitDirection::SOUTH && y < q->_split_position.y()) ||
+ (q->_split_direction == Inkscape::SplitDirection::WEST && x > q->_split_position.x()) ||
+ (q->_split_direction == Inkscape::SplitDirection::EAST && x < q->_split_position.x()) ) {
+ q->_drawing->setRenderMode(Inkscape::RenderMode::OUTLINE);
+ }
+ }
+ // Convert to world coordinates.
+ auto p = Geom::Point(x, y) + q->_pos;
+ if (decoupled_mode) {
+ p *= _store_affine * q->_affine.inverse();
+ }
+
+ q->_current_canvas_item_new = q->_canvas_item_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;
+ // }
+
+ // Reset the drawing back to the requested render mode.
+ q->_drawing->setRenderMode(q->_render_mode);
+ }
+
+ 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 (decoupled_mode) {
+ p *= _store_affine * q->_affine.inverse();
+ }
+ 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
+{
+ Gtk::Allocation allocation = get_allocation();
+ return {allocation.get_width(), allocation.get_height()};
+}
+
+/**
+ * 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());
+}
+
+/**
+ * Set the affine for the canvas.
+ */
+void
+Canvas::set_affine(Geom::Affine const &affine)
+{
+ if (_affine == affine) {
+ return;
+ }
+
+ _affine = affine;
+
+ d->add_idle();
+ queue_draw();
+}
+
+void CanvasPrivate::queue_draw_area(Geom::IntRect &rect)
+{
+ 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->updater->reset(); // Empty region (i.e. everything is dirty).
+ d->add_idle();
+ 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;
+ }
+
+ auto rect = Geom::IntRect::from_xywh(x0, y0, x1 - x0, y1 - y0);
+ d->updater->mark_dirty(rect);
+ d->add_idle();
+ 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 &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 idle process to perform the update.
+ d->add_idle();
+}
+
+/**
+ * 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->add_idle();
+ queue_draw();
+
+ if (auto grid = dynamic_cast<Inkscape::UI::Widget::CanvasGrid*>(get_parent())) {
+ grid->UpdateRulers();
+ }
+}
+
+/**
+ * Set canvas background color (display only).
+ */
+void
+Canvas::set_background_color(guint32 rgba)
+{
+ double r = SP_RGBA32_R_F(rgba);
+ double g = SP_RGBA32_G_F(rgba);
+ double b = SP_RGBA32_B_F(rgba);
+
+ _background = Cairo::SolidPattern::create_rgb(r, g, b);
+ d->solid_background = true;
+
+ redraw_all();
+}
+
+/**
+ * Set canvas background to a checkerboard pattern.
+ */
+void
+Canvas::set_background_checkerboard(guint32 rgba, bool use_alpha)
+{
+ auto pattern = ink_cairo_pattern_create_checkerboard(rgba, use_alpha);
+ _background = Cairo::RefPtr<Cairo::Pattern>(new Cairo::Pattern(pattern));
+ d->solid_background = false;
+ redraw_all();
+}
+
+void Canvas::set_drawing_disabled(bool disable)
+{
+ _drawing_disabled = disable;
+ if (!disable) {
+ d->add_idle();
+ }
+}
+
+void
+Canvas::set_render_mode(Inkscape::RenderMode mode)
+{
+ if (_render_mode != mode) {
+ _render_mode = mode;
+ _drawing->setRenderMode(_render_mode);
+ redraw_all();
+ }
+ if (_desktop) {
+ _desktop->setWindowTitle(); // Mode is listed in title.
+ }
+}
+
+void
+Canvas::set_color_mode(Inkscape::ColorMode mode)
+{
+ if (_color_mode != mode) {
+ _color_mode = mode;
+ redraw_all();
+ }
+ if (_desktop) {
+ _desktop->setWindowTitle(); // Mode is listed in title.
+ }
+}
+
+void
+Canvas::set_split_mode(Inkscape::SplitMode mode)
+{
+ if (_split_mode != mode) {
+ _split_mode = mode;
+ redraw_all();
+ }
+}
+
+Cairo::RefPtr<Cairo::ImageSurface>
+Canvas::get_backing_store() const
+{
+ return d->_backing_store;
+}
+
+/**
+ * Clear current and grabbed items.
+ */
+void
+Canvas::canvas_item_destructed(Inkscape::CanvasItem* item)
+{
+ 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;
+ }
+}
+
+// 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)
+{
+ parent_type::on_size_allocate(allocation);
+ assert(allocation == get_allocation());
+ d->add_idle(); // Trigger the size update to be applied to the stores before the next call to on_draw.
+}
+
+/*
+ * Drawing
+ */
+
+/*
+ * The on_draw() function is called whenever Gtk wants to update the window. This function:
+ *
+ * 1. Ensures that if the idle process was started, at least one cycle has run.
+ *
+ * 2. Blits the store(s) onto the canvas, clipping the outline store as required.
+ * (Or composites them with the transformed snapshot store(s) in decoupled mode.)
+ *
+ * 3. Draws the "controller" in the 'split' split mode.
+ */
+bool
+Canvas::on_draw(const Cairo::RefPtr<::Cairo::Context> &cr)
+{
+ auto f = FrameCheck::Event();
+
+ if (!d->active) {
+ std::cerr << "Canvas::on_draw: Called while not active!" << std::endl;
+ return true;
+ }
+
+ // sp_canvas_item_recursive_print_tree(0, _root);
+ // canvas_item_print_tree(_canvas_item_root);
+
+ assert(_drawing);
+
+ // Although hipri_idle 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.
+ // Here we ensure that that call has taken place. This is problematic because if hipri_idle does rendering, enlarging the damage rect, then our drawing will still be clipped to the old
+ // damage rect. It was precisely this problem that lead to the introduction of hipri_idle. Fortunately, the following failsafe only seems to execute once during initialisation, and
+ // once on further resize events. Both these events seem to trigger a full damage, hence we are ok.
+ if (d->hipri_idle.connected()) {
+ d->hipri_idle.disconnect();
+ d->on_hipri_idle();
+ }
+
+ // Blit background if not solid. (If solid, it is baked into the stores.)
+ if (!d->solid_background) {
+ if (d->prefs.debug_framecheck) f = FrameCheck::Event("background");
+ cr->save();
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->set_source(_background);
+ cr->paint();
+ cr->restore();
+ }
+
+ auto draw_store = [&, this] (const Cairo::RefPtr<Cairo::ImageSurface> &store, const Cairo::RefPtr<Cairo::ImageSurface> &snapshot_store, bool is_backing_store) {
+ if (!d->decoupled_mode) {
+ // Blit store to screen.
+ if (d->prefs.debug_framecheck) f = FrameCheck::Event("draw");
+ cr->save();
+ cr->set_operator(is_backing_store && d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER);
+ cr->set_source(store, d->_store_rect.left() - _pos.x(), d->_store_rect.top() - _pos.y());
+ cr->paint();
+ cr->restore();
+ } else {
+ // Turn off anti-aliasing for huge performance gains. Only applies to this compositing step.
+ cr->set_antialias(Cairo::ANTIALIAS_NONE);
+
+ // Blit untransformed snapshot store to complement of transformed snapshot clean region.
+ if (d->prefs.debug_framecheck) f = FrameCheck::Event("composite", 2);
+ cr->save();
+ cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD);
+ cr->rectangle(0, 0, get_allocation().get_width(), get_allocation().get_height());
+ cr->translate(-_pos.x(), -_pos.y());
+ cr->transform(geom_to_cairo(_affine * d->_snapshot_affine.inverse()));
+ region_to_path(cr, d->_snapshot_clean_region);
+ cr->clip();
+ cr->transform(geom_to_cairo(d->_snapshot_affine * _affine.inverse()));
+ cr->translate(_pos.x(), _pos.y());
+ cr->set_operator(is_backing_store && d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER);
+ cr->set_source(snapshot_store, d->_snapshot_static_offset.x(), d->_snapshot_static_offset.y());
+ cr->paint();
+ cr->restore();
+
+ // Draw transformed snapshot, clipped to its clean region and the complement of the store's clean region.
+ if (d->prefs.debug_framecheck) f = FrameCheck::Event("composite", 1);
+ cr->save();
+ cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD);
+ cr->rectangle(0, 0, get_allocation().get_width(), get_allocation().get_height());
+ cr->translate(-_pos.x(), -_pos.y());
+ cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse()));
+ region_to_path(cr, d->updater->clean_region);
+ cr->clip();
+ cr->transform(geom_to_cairo(d->_store_affine * d->_snapshot_affine.inverse()));
+ region_to_path(cr, d->_snapshot_clean_region);
+ cr->clip();
+ cr->set_source(snapshot_store, d->_snapshot_rect.left(), d->_snapshot_rect.top());
+ cr->set_operator(is_backing_store && d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER);
+ Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST);
+ cr->paint();
+ if (d->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 clean region.
+ if (d->prefs.debug_framecheck) f = FrameCheck::Event("composite", 0);
+ cr->save();
+ cr->translate(-_pos.x(), -_pos.y());
+ cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse()));
+ region_to_path(cr, d->updater->clean_region);
+ cr->clip();
+ cr->set_source(store, d->_store_rect.left(), d->_store_rect.top());
+ cr->set_operator(is_backing_store && d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER);
+ Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST);
+ cr->paint();
+ cr->restore();
+ }
+ };
+
+ // Draw the backing store.
+ draw_store(d->_backing_store, d->_snapshot_store, true);
+
+ // Draw overlay if required.
+ if (_render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY) {
+ assert(d->_outline_store);
+
+ double outline_overlay_opacity = 1.0 - d->prefs.outline_overlay_opacity / 100.0;
+
+ // Partially obscure drawing by painting semi-transparent white.
+ cr->set_source_rgb(1.0, 1.0, 1.0);
+ cr->paint_with_alpha(outline_overlay_opacity);
+
+ // Overlay outline.
+ draw_store(d->_outline_store, d->_snapshot_outline_store, false);
+ }
+
+ // Draw split if required.
+ if (_split_mode != Inkscape::SplitMode::NORMAL) {
+ assert(d->_outline_store);
+
+ // Move split position to center if not in canvas.
+ auto const rect = Geom::Rect(Geom::Point(), get_dimensions());
+ if (!rect.contains(_split_position)) {
+ _split_position = rect.midpoint();
+ }
+
+ // Add clipping path and blit background.
+ cr->save();
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->set_source(_background);
+ add_clippath(cr);
+ cr->paint();
+ cr->restore();
+
+ // Add clipping path and draw outline store.
+ cr->save();
+ add_clippath(cr);
+ draw_store(d->_outline_store, d->_snapshot_outline_store, false);
+ cr->restore();
+ }
+
+ // Paint unclean regions in red.
+ if (d->prefs.debug_show_unclean) {
+ if (d->prefs.debug_framecheck) f = FrameCheck::Event("paint_unclean");
+ auto reg = Cairo::Region::create(geom_to_cairo(d->_store_rect));
+ reg->subtract(d->updater->clean_region);
+ cr->save();
+ cr->translate(-_pos.x(), -_pos.y());
+ if (d->decoupled_mode) {
+ cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse()));
+ }
+ cr->set_source_rgba(1, 0, 0, 0.2);
+ region_to_path(cr, reg);
+ cr->fill();
+ cr->restore();
+ }
+
+ // Paint internal edges of clean region in green.
+ if (d->prefs.debug_show_clean) {
+ if (d->prefs.debug_framecheck) f = FrameCheck::Event("paint_clean");
+ cr->save();
+ cr->translate(-_pos.x(), -_pos.y());
+ if (d->decoupled_mode) {
+ cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse()));
+ }
+ cr->set_source_rgba(0, 0.7, 0, 0.4);
+ region_to_path(cr, d->updater->clean_region);
+ cr->stroke();
+ cr->restore();
+ }
+
+ if (_split_mode == Inkscape::SplitMode::SPLIT) {
+ // Add dividing line.
+ cr->save();
+ cr->set_source_rgb(0, 0, 0);
+ cr->set_line_width(1);
+ if (_split_direction == Inkscape::SplitDirection::EAST ||
+ _split_direction == Inkscape::SplitDirection::WEST) {
+ cr->move_to((int)_split_position.x() + 0.5, 0);
+ cr->line_to((int)_split_position.x() + 0.5, get_dimensions().y());
+ cr->stroke();
+ } else {
+ cr->move_to( 0, (int)_split_position.y() + 0.5);
+ cr->line_to(get_dimensions().x(), (int)_split_position.y() + 0.5);
+ cr->stroke();
+ }
+ cr->restore();
+
+ // Add controller image.
+ double a = _hover_direction == Inkscape::SplitDirection::NONE ? 0.5 : 1.0;
+ cr->save();
+ cr->set_source_rgba(0.2, 0.2, 0.2, a);
+ cr->arc(_split_position.x(), _split_position.y(), 20 * d->_device_scale, 0, 2 * M_PI);
+ cr->fill();
+ cr->restore();
+
+ cr->save();
+ 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.0);
+
+ // Draw triangle.
+ cr->move_to(-5 * d->_device_scale, 8 * d->_device_scale);
+ cr->line_to( 0, 18 * d->_device_scale);
+ cr->line_to( 5 * d->_device_scale, 8 * d->_device_scale);
+ 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();
+ }
+ cr->restore();
+ }
+
+ // Process bucketed events as soon as possible after draw. We cannot process them now, because we have
+ // a frame to get out as soon as possible, and processing events may take a while. Instead, we schedule
+ // it with a signal callback on the main loop that runs as soon as this function is completed.
+ if (!d->eventprocessor->events.empty()) d->schedule_bucket_emptier();
+
+ // Record the fact that a draw is no longer pending.
+ d->pending_draw = false;
+
+ // Notify the update strategy that another frame has passed.
+ d->updater->frame();
+
+ // Just-for-1.2 flicker "prevention": save the last offset the store was drawn at outside of decoupled mode,
+ // so we can continue to draw a static snapshot upon next going into decoupled mode.
+ if (!d->decoupled_mode) {
+ d->_snapshot_static_offset = d->_store_rect.min() - _pos;
+ }
+
+ return true;
+}
+
+// Sets clip path for Split and X-Ray modes.
+void
+Canvas::add_clippath(const Cairo::RefPtr<Cairo::Context>& cr)
+{
+ double width = get_allocation().get_width();
+ double height = get_allocation().get_height();
+ double sx = _split_position.x();
+ double sy = _split_position.y();
+
+ if (_split_mode == Inkscape::SplitMode::SPLIT) {
+ // We're clipping the outline region... so it's backwards.
+ switch (_split_direction) {
+ case Inkscape::SplitDirection::SOUTH:
+ cr->rectangle(0, 0, width, sy);
+ break;
+ case Inkscape::SplitDirection::NORTH:
+ cr->rectangle(0, sy, width, height - sy);
+ break;
+ case Inkscape::SplitDirection::EAST:
+ cr->rectangle(0, 0, sx, height );
+ break;
+ case Inkscape::SplitDirection::WEST:
+ cr->rectangle(sx, 0, width - sx, height );
+ break;
+ default:
+ // no clipping (for NONE, HORIZONTAL, VERTICAL)
+ break;
+ }
+ } else {
+ cr->arc(sx, sy, d->prefs.x_ray_radius, 0, 2 * M_PI);
+ }
+
+ cr->clip();
+}
+
+void
+CanvasPrivate::add_idle()
+{
+ framecheck_whole_function(this)
+
+ if (!active) {
+ // We can safely discard events until active, because we will run add_idle on activation later in initialisation.
+ return;
+ }
+
+ if (!hipri_idle.connected()) {
+ hipri_idle = Glib::signal_idle().connect(sigc::mem_fun(this, &CanvasPrivate::on_hipri_idle), G_PRIORITY_HIGH_IDLE + 15); // after resize, before draw
+ }
+
+ if (!lopri_idle.connected()) {
+ lopri_idle = Glib::signal_idle().connect(sigc::mem_fun(this, &CanvasPrivate::on_lopri_idle), G_PRIORITY_DEFAULT_IDLE);
+ }
+
+ idle_running = true;
+}
+
+auto
+distSq(const Geom::IntPoint pt, const Geom::IntRect &rect)
+{
+ auto v = rect.clamp(pt) - pt;
+ return v.x() * v.x() + v.y() * v.y();
+}
+
+auto
+calc_affine_diff(const Geom::Affine &a, const Geom::Affine &b) {
+ auto c = a.inverse() * b;
+ return std::abs(c[0] - 1) + std::abs(c[1]) + std::abs(c[2]) + std::abs(c[3] - 1);
+}
+
+// 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;
+}
+
+std::optional<Geom::Dim2>
+CanvasPrivate::old_bisector(const Geom::IntRect &rect)
+{
+ int bw = rect.width();
+ int bh = rect.height();
+
+ /*
+ * Determine redraw strategy:
+ *
+ * bw < bh (strips mode): Draw horizontal strips starting from cursor position.
+ * Seems to be faster for drawing many smaller objects zoomed out.
+ *
+ * bw > hb (chunks mode): Splits across the larger dimension of the rectangle, painting
+ * in almost square chunks (from the cursor.
+ * Seems to be faster for drawing a few blurred objects across the entire screen.
+ * Seems to be somewhat psychologically faster.
+ *
+ * Default is for strips mode.
+ */
+
+ int max_pixels;
+ if (q->_render_mode != Inkscape::RenderMode::OUTLINE) {
+ // Can't be too small or large gradient will be rerendered too many times!
+ max_pixels = 65536 * prefs.tile_multiplier;
+ } else {
+ // Paths only. 1M is catched buffer and we need four channels.
+ max_pixels = 262144;
+ }
+
+ if (bw * bh > max_pixels) {
+ if (bw < bh || bh < 2 * prefs.tile_size) {
+ return Geom::X;
+ } else {
+ return Geom::Y;
+ }
+ }
+
+ return {};
+}
+
+std::optional<Geom::Dim2>
+CanvasPrivate::new_bisector(const Geom::IntRect &rect)
+{
+ 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 > prefs.new_bisector_size) {
+ return Geom::X;
+ }
+ } else {
+ if (bh > prefs.new_bisector_size) {
+ return Geom::Y;
+ }
+ }
+
+ return {};
+}
+
+bool
+CanvasPrivate::on_hipri_idle()
+{
+ assert(active);
+ if (idle_running) {
+ idle_running = on_idle();
+ }
+ return false;
+}
+
+bool
+CanvasPrivate::on_lopri_idle()
+{
+ assert(active);
+ if (idle_running) {
+ idle_running = on_idle();
+ }
+ return idle_running;
+}
+
+bool
+CanvasPrivate::on_idle()
+{
+ framecheck_whole_function(this)
+
+ assert(q->_canvas_item_root);
+
+ // Quit idle process if not supposed to be drawing.
+ if (!q->_drawing || q->_drawing_disabled) {
+ return false;
+ }
+
+ const Geom::IntPoint pad(prefs.pad, prefs.pad);
+ auto recreate_store = [&, this] {
+ // Recreate the store at the current affine so that it covers the visible region.
+ _store_rect = q->get_area_world();
+ _store_rect.expandBy(pad);
+ Geom::IntRect expanded = _store_rect;
+ Geom::IntPoint expansion(expanded.width()/2, expanded.height()/2);
+ expanded.expandBy(expansion);
+ q->_drawing->setCacheLimit(expanded);
+ _store_affine = q->_affine;
+ int desired_width = _store_rect.width() * _device_scale;
+ int desired_height = _store_rect.height() * _device_scale;
+ if (!_backing_store || _backing_store->get_width() != desired_width || _backing_store->get_height() != desired_height) {
+ _backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, desired_width, desired_height);
+ cairo_surface_set_device_scale(_backing_store->cobj(), _device_scale, _device_scale); // No C++ API!
+ }
+ auto cr = Cairo::Context::create(_backing_store);
+ if (solid_background) {
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->set_source(q->_background);
+ } else {
+ cr->set_operator(Cairo::OPERATOR_CLEAR);
+ }
+ cr->paint();
+ if (need_outline_store()) {
+ if (!_outline_store || _outline_store->get_width() != desired_width || _outline_store->get_height() != desired_height) {
+ _outline_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, desired_width, desired_height);
+ cairo_surface_set_device_scale(_outline_store->cobj(), _device_scale, _device_scale); // No C++ API!
+ }
+ auto cr = Cairo::Context::create(_outline_store);
+ cr->set_operator(Cairo::OPERATOR_CLEAR);
+ cr->paint();
+ }
+ updater->reset();
+ if (prefs.debug_show_unclean) q->queue_draw();
+ };
+
+ // Determine whether the rendering parameters have changed, and reset if so.
+ if (!_backing_store || (need_outline_store() && !_outline_store) || _device_scale != q->get_scale_factor() || _store_solid_background != solid_background) {
+ _device_scale = q->get_scale_factor();
+ _store_solid_background = solid_background;
+ recreate_store();
+ decoupled_mode = false;
+ if (prefs.debug_logging) std::cout << "Full reset" << std::endl;
+ }
+
+ // Make sure to clear the outline store when not in use, so we don't accidentally re-use it when it is required again.
+ if (!need_outline_store()) {
+ _outline_store.clear();
+ }
+
+ auto shift_store = [&, this] {
+ // Recreate the store, but keep re-usable content from the old store.
+ auto store_rect = q->get_area_world();
+ store_rect.expandBy(pad);
+ Geom::IntRect expanded = _store_rect;
+ Geom::IntPoint expansion(expanded.width()/2, expanded.height()/2);
+ expanded.expandBy(expansion);
+ q->_drawing->setCacheLimit(expanded);
+ auto backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, store_rect.width() * _device_scale, store_rect.height() * _device_scale);
+ cairo_surface_set_device_scale(backing_store->cobj(), _device_scale, _device_scale); // No C++ API!
+
+ // Determine the geometry of the shift.
+ auto shift = store_rect.min() - _store_rect.min();
+ auto reuse_rect = store_rect & _store_rect;
+ assert(reuse_rect); // Should not be called if there is no overlap.
+ auto cr = Cairo::Context::create(backing_store);
+
+ // Paint background into region not covered by next operation.
+ if (solid_background) {
+ auto reg = Cairo::Region::create(geom_to_cairo(store_rect));
+ reg->subtract(geom_to_cairo(*reuse_rect));
+ reg->translate(-store_rect.left(), -store_rect.top());
+ cr->save();
+ if (solid_background) {
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->set_source(q->_background);
+ }
+ region_to_path(cr, reg);
+ cr->fill();
+ cr->restore();
+ }
+
+ // Copy re-usuable contents of old store into new store, shifted.
+ cr->rectangle(reuse_rect->left() - store_rect.left(), reuse_rect->top() - store_rect.top(), reuse_rect->width(), reuse_rect->height());
+ cr->clip();
+ cr->set_source(_backing_store, -shift.x(), -shift.y());
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->paint();
+
+ // Set the result as the new backing store.
+ _store_rect = store_rect;
+ assert(_store_affine == q->_affine); // Should not be called if the affine has changed.
+ _backing_store = std::move(backing_store);
+
+ // Do the same for the outline store
+ if (_outline_store) {
+ auto outline_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, store_rect.width() * _device_scale, store_rect.height() * _device_scale);
+ cairo_surface_set_device_scale(outline_store->cobj(), _device_scale, _device_scale); // No C++ API!
+ auto cr = Cairo::Context::create(outline_store);
+ cr->rectangle(reuse_rect->left() - store_rect.left(), reuse_rect->top() - store_rect.top(), reuse_rect->width(), reuse_rect->height());
+ cr->clip();
+ cr->set_source(_outline_store, -shift.x(), -shift.y());
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->paint();
+ _outline_store = std::move(outline_store);
+ }
+
+ updater->intersect(_store_rect);
+ if (prefs.debug_show_unclean) q->queue_draw();
+ };
+
+ auto take_snapshot = [&, this] {
+ // Copy the backing store to the snapshot, leaving us temporarily in an invalid state.
+ std::swap(_snapshot_store, _backing_store); // This will re-use the old snapshot store later if possible.
+ _snapshot_rect = _store_rect;
+ _snapshot_affine = _store_affine;
+ _snapshot_clean_region = updater->clean_region->copy();
+
+ // Do the same for the outline store
+ std::swap(_snapshot_outline_store, _outline_store);
+
+ // Recreate the backing store, making the state valid again.
+ recreate_store();
+ };
+
+ // Handle transitions and actions in response to viewport changes.
+ if (!decoupled_mode) {
+ // Enter decoupled mode if the affine has changed from what the backing store was drawn at.
+ if (q->_affine != _store_affine) {
+ // Snapshot and reset the backing store.
+ take_snapshot();
+
+ // Enter decoupled mode.
+ if (prefs.debug_logging) std::cout << "Entering decoupled mode" << std::endl;
+ decoupled_mode = true;
+
+ // Note: If redrawing is fast enough to finish during the frame, then going into decoupled mode, drawing, and leaving
+ // it again performs exactly the same rendering operations as if we had not gone into it at all. Also, no extra copies
+ // or blits are performed, and the drawing operations done on the screen are the same. Hence this feature comes at zero cost.
+ } else {
+ // Get visible rectangle in canvas coordinates.
+ auto const visible = q->get_area_world();
+ if (!_store_rect.intersects(visible)) {
+ // If the store has gone completely off-screen, recreate it.
+ recreate_store();
+ if (prefs.debug_logging) std::cout << "Recreated store" << std::endl;
+ } else if (!_store_rect.contains(visible)) {
+ // If the store has gone partially off-screen, shift it.
+ shift_store();
+ if (prefs.debug_logging) std::cout << "Shifted store" << std::endl;
+ }
+ // After these operations, the store should now be fully on-screen.
+ assert(_store_rect.contains(visible));
+ }
+ } else { // if (decoupled_mode)
+ // Completely cancel the previous redraw and start again if the viewing parameters have changed too much.
+ if (!prefs.debug_sticky_decoupled) {
+ auto pl = Geom::Parallelogram(q->get_area_world());
+ pl *= _store_affine * q->_affine.inverse();
+ if (!pl.intersects(_store_rect)) {
+ // Store has gone off the screen.
+ recreate_store();
+ if (prefs.debug_logging) std::cout << "Restarting redraw (store off-screen)" << std::endl;
+ } else {
+ auto diff = calc_affine_diff(q->_affine, _store_affine);
+ if (diff > prefs.max_affine_diff) {
+ // Affine has changed too much.
+ recreate_store();
+ if (prefs.debug_logging) std::cout << "Restarting redraw (affine changed too much)" << std::endl;
+ }
+ }
+ }
+ }
+
+ // Assert that _clean_region is a subregion of _store_rect.
+ #ifndef NDEBUG
+ auto tmp = updater->clean_region->copy();
+ tmp->subtract(geom_to_cairo(_store_rect));
+ assert(tmp->empty());
+ #endif
+
+ // Ensure the geometry is up-to-date and in the right place.
+ auto affine = decoupled_mode ? _store_affine : q->_affine;
+ if (q->_need_update || geom_affine != affine) {
+ q->_canvas_item_root->update(affine);
+ geom_affine = affine;
+ q->_need_update = false;
+ }
+
+ // If asked to, don't paint anything and instead halt the idle process.
+ if (prefs.debug_disable_redraw) {
+ return false;
+ }
+
+ // Get the subrectangle of store that is visible.
+ Geom::OptIntRect visible_rect;
+ if (!decoupled_mode) {
+ // By a previous assertion, this always lies within the store.
+ visible_rect = q->get_area_world();
+ } else {
+ // Get the window rectangle transformed into canvas space.
+ auto pl = Geom::Parallelogram(q->get_area_world());
+ pl *= _store_affine * q->_affine.inverse();
+
+ // Get its bounding box, rounded outwards.
+ auto b = pl.bounds();
+ auto bi = Geom::IntRect(b.min().floor(), b.max().ceil());
+
+ // The visible rect is the intersection of this with the store
+ visible_rect = bi & _store_rect;
+ }
+ // The visible rectangle must be a subrectangle of store.
+ assert(_store_rect.contains(visible_rect));
+
+ // Get the mouse position in screen space.
+ Geom::IntPoint mouse_loc = (last_mouse ? *last_mouse : Geom::Point(q->get_dimensions()) / 2).round();
+
+ // Map the mouse to canvas space.
+ mouse_loc += q->_pos;
+ if (decoupled_mode) {
+ mouse_loc = geom_act(_store_affine * q->_affine.inverse(), mouse_loc);
+ }
+
+ // Begin processing redraws.
+ auto start_time = g_get_monotonic_time();
+ while (true) {
+ // Get the clean region for the next redraw as reported by the updater.
+ auto clean_region = updater->get_next_clean_region();
+
+ // Get the region to paint, which is the visible rectangle minus the clean region (both subregions of store).
+ Cairo::RefPtr<Cairo::Region> paint_region;
+ if (visible_rect) {
+ paint_region = Cairo::Region::create(geom_to_cairo(*visible_rect));
+ paint_region->subtract(clean_region);
+ } else {
+ paint_region = Cairo::Region::create();
+ }
+
+ Geom::OptIntRect dragged = Geom::OptIntRect();
+ if (q->_grabbed_canvas_item) {
+ dragged = q->_grabbed_canvas_item->get_bounds().roundOutwards();
+ if (dragged) {
+ (*dragged).expandBy(prefs.pad);
+ dragged = dragged & visible_rect;
+ if (dragged) {
+ paint_region->subtract(geom_to_cairo(*dragged));
+ }
+ }
+ }
+ // Get the list of rectangles to paint, coarsened to avoid fragmentation.
+ auto rects = coarsen(paint_region,
+ std::min<int>(prefs.coarsener_min_size, prefs.new_bisector_size / 2),
+ std::min<int>(prefs.coarsener_glue_size, prefs.new_bisector_size / 2),
+ prefs.coarsener_min_fullness);
+ if (dragged) {
+ // this become the first after look for cursor
+ rects.push_back(*dragged);
+ }
+ // Ensure that all the rectangles lie within the visible rect (and therefore within the store).
+ #ifndef NDEBUG
+ for (auto &rect : rects) {
+ assert(visible_rect.contains(rect));
+ }
+ #endif
+
+ // Put the rectangles into a heap sorted by distance from mouse.
+ auto cmp = [&] (const Geom::IntRect &a, const Geom::IntRect &b) {
+ return distSq(mouse_loc, a) > distSq(mouse_loc, b);
+ };
+ std::make_heap(rects.begin(), rects.end(), cmp);
+
+ // Process rectangles until none left or timed out.
+ bool start = true;
+ while (!rects.empty()) {
+ // Extract the closest rectangle to the mouse.
+ std::pop_heap(rects.begin(), rects.end(), cmp);
+ auto rect = rects.back();
+ rects.pop_back();
+
+ // Cull empty rectangles.
+ if (rect.width() == 0 || rect.height() == 0) {
+ start = false;
+ 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 (clean_region->contains_rectangle(geom_to_cairo(rect)) == Cairo::REGION_OVERLAP_IN) {
+ start = false;
+ continue;
+ }
+
+ // Lambda to add a rectangle to the heap.
+ auto add_rect = [&] (const Geom::IntRect &rect) {
+ rects.emplace_back(rect);
+ std::push_heap(rects.begin(), rects.end(), cmp);
+ };
+
+ // If the rectangle needs bisecting, bisect it and put it back on the heap.
+ auto axis = prefs.use_new_bisector ? new_bisector(rect) : old_bisector(rect);
+ if (axis && !(dragged && start)) {
+ 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);
+ start = false;
+ continue;
+ }
+ start = false;
+ // Paint the rectangle.
+ paint_rect_internal(rect);
+
+ // Check for timeout.
+ auto now = g_get_monotonic_time();
+ auto elapsed = now - start_time;
+ bool fixchoping = false;
+ #ifdef _WIN32
+ fixchoping = true;
+ #elif defined(__APPLE__)
+ fixchoping = true;
+ #endif
+ if (elapsed > ((fixchoping && prefs.render_time_limit == 1000) ? 80000 : prefs.render_time_limit)) {
+ // Timed out. Temporarily return to GTK main loop, and come back here when next idle.
+ if (prefs.debug_logging) std::cout << "Timed out: " << g_get_monotonic_time() - start_time << " us" << std::endl;
+ framecheckobj.subtype = 1;
+ return true;
+ }
+ }
+
+ // Report the redraw as finished. Exit if there's no more redraws to process.
+ bool keep_going = updater->report_finished();
+ if (!keep_going) break;
+ }
+
+ // Finished drawing. Handle transitions out of decoupled mode, by checking if we need to do a final redraw at the correct affine.
+ if (decoupled_mode) {
+ if (prefs.debug_sticky_decoupled) {
+ // Debug feature: quit idle process, but stay in decoupled mode.
+ return false;
+ } else if (_store_affine == q->_affine) {
+ // Content is rendered at the correct affine - exit decoupled mode and quit idle process.
+ if (prefs.debug_logging) std::cout << "Finished drawing - exiting decoupled mode" << std::endl;
+ // Exit decoupled mode.
+ decoupled_mode = false;
+ // Quit idle process.
+ return false;
+ } else {
+ // Content is rendered at the wrong affine - take a new snapshot and continue idle process to continue rendering at the new affine.
+ if (prefs.debug_logging) std::cout << "Scheduling final redraw" << std::endl;
+ // Snapshot and reset the backing store.
+ take_snapshot();
+ // Continue idle process.
+ return true;
+ }
+ } else {
+ // All done, quit the idle process.
+ framecheckobj.subtype = 3;
+ return false;
+ }
+}
+
+void
+CanvasPrivate::paint_rect_internal(Geom::IntRect const &rect)
+{
+ // Paint the rectangle.
+ q->_drawing->setColorMode(q->_color_mode);
+ paint_single_buffer(rect, _backing_store, true, false);
+
+ if (_outline_store) {
+ q->_drawing->setRenderMode(Inkscape::RenderMode::OUTLINE);
+ paint_single_buffer(rect, _outline_store, false, q->_render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY);
+ q->_drawing->setRenderMode(q->_render_mode); // Leave the drawing in the requested render mode.
+ }
+
+ // Introduce an artificial delay for each rectangle.
+ if (prefs.debug_slow_redraw) g_usleep(prefs.debug_slow_redraw_time);
+
+ // Mark the rectangle as clean.
+ updater->mark_clean(rect);
+
+ // Mark the screen dirty.
+ if (!decoupled_mode) {
+ // Get rectangle needing repaint
+ auto repaint_rect = rect - q->_pos;
+
+ // Assert that a repaint actually occurs (guaranteed because we are only asked to paint fully on-screen rectangles)
+ auto screen_rect = Geom::IntRect(0, 0, q->get_allocation().get_width(), q->get_allocation().get_height());
+ assert(repaint_rect & screen_rect);
+
+ // Schedule repaint
+ queue_draw_area(repaint_rect); // Guarantees on_draw will be called in the future.
+ if (bucket_emptier_tick_callback) {q->remove_tick_callback(*bucket_emptier_tick_callback); bucket_emptier_tick_callback.reset();}
+ pending_draw = true;
+ } else {
+ // Get rectangle needing repaint (transform into screen space, take bounding box, round outwards)
+ auto pl = Geom::Parallelogram(rect);
+ pl *= q->_affine * _store_affine.inverse();
+ pl *= Geom::Translate(-q->_pos);
+ auto b = pl.bounds();
+ auto repaint_rect = Geom::IntRect(b.min().floor(), b.max().ceil());
+
+ // Check if repaint is necessary - some rectangles could be entirely off-screen.
+ auto screen_rect = Geom::IntRect(0, 0, q->get_allocation().get_width(), q->get_allocation().get_height());
+ if (repaint_rect & screen_rect) {
+ // Schedule repaint
+ queue_draw_area(repaint_rect);
+ if (bucket_emptier_tick_callback) {q->remove_tick_callback(*bucket_emptier_tick_callback); bucket_emptier_tick_callback.reset();}
+ pending_draw = true;
+ }
+ }
+}
+
+void
+CanvasPrivate::paint_single_buffer(Geom::IntRect const &paint_rect, const Cairo::RefPtr<Cairo::ImageSurface> &store, bool is_backing_store, bool outline_overlay_pass)
+{
+ // Make sure the following code does not go outside of store's data.
+ assert(store);
+ assert(store->get_format() == Cairo::FORMAT_ARGB32);
+ assert(_store_rect.contains(paint_rect)); // FIXME: Observed to fail once when hitting Ctrl+O while Canvas was busy. Haven't managed to reproduce it. Doesn't mean it's fixed.
+
+ // Create temporary surface that draws directly to store.
+ store->flush();
+ unsigned char *data = store->get_data();
+ int stride = store->get_stride();
+
+ // Check we are using the correct device scale.
+ double x_scale = 1.0;
+ double y_scale = 1.0;
+ cairo_surface_get_device_scale(store->cobj(), &x_scale, &y_scale); // No C++ API!
+ assert (_device_scale == (int) x_scale);
+ assert (_device_scale == (int) y_scale);
+
+ // Move to the correct row.
+ data += stride * (paint_rect.top() - _store_rect.top()) * (int)y_scale;
+ // Move to the correct column.
+ data += 4 * (paint_rect.left() - _store_rect.left()) * (int)x_scale;
+ auto imgs = Cairo::ImageSurface::create(data, Cairo::FORMAT_ARGB32,
+ paint_rect.width() * _device_scale,
+ paint_rect.height() * _device_scale,
+ stride);
+
+ cairo_surface_set_device_scale(imgs->cobj(), _device_scale, _device_scale); // No C++ API!
+
+ auto cr = Cairo::Context::create(imgs);
+
+ // Clear background
+ cr->save();
+ if (is_backing_store && solid_background) {
+ cr->set_source(q->_background);
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ } else {
+ cr->set_operator(Cairo::OPERATOR_CLEAR);
+ }
+ cr->paint();
+ cr->restore();
+
+ // Render drawing on top of background.
+ if (q->_canvas_item_root->is_visible()) {
+ auto buf = Inkscape::CanvasItemBuffer{ paint_rect, _device_scale, outline_overlay_pass, cr };
+ q->_canvas_item_root->render(&buf);
+ }
+
+ // Paint over newly drawn content with a translucent random colour.
+ if (prefs.debug_show_redraw) {
+ cr->set_source_rgba((rand() % 255) / 255.0, (rand() % 255) / 255.0, (rand() % 255) / 255.0, 0.2);
+ cr->set_operator(Cairo::OPERATOR_OVER);
+ cr->rectangle(0, 0, imgs->get_width(), imgs->get_height());
+ cr->fill();
+ }
+
+ if (q->_cms_active) {
+ auto transf = prefs.from_display
+ ? Inkscape::CMSSystem::getDisplayPer(q->_cms_key)
+ : Inkscape::CMSSystem::getDisplayTransform();
+
+ if (transf) {
+ imgs->flush();
+ auto px = imgs->get_data();
+ int stride = imgs->get_stride();
+ for (int i = 0; i < paint_rect.height(); i++) {
+ auto row = px + i * stride;
+ Inkscape::CMSSystem::doTransform(transf, row, row, paint_rect.width());
+ }
+ imgs->mark_dirty();
+ }
+ }
+
+ store->mark_dirty();
+}
+
+} // 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.h b/src/ui/widget/canvas.h
new file mode 100644
index 0000000..f2a434a
--- /dev/null
+++ b/src/ui/widget/canvas.h
@@ -0,0 +1,234 @@
+// 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"
+
+class SPDesktop;
+
+namespace Inkscape {
+
+class CanvasItem;
+class CanvasItemGroup;
+class Drawing;
+
+namespace UI {
+namespace Widget {
+
+class CanvasPrivate;
+
+/**
+ * A Gtk::DrawingArea widget for Inkscape's canvas.
+ */
+class Canvas : public Gtk::DrawingArea
+{
+ using parent_type = Gtk::DrawingArea;
+
+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 { return _canvas_item_root; }
+
+ // 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;}
+
+ // Background
+ void set_background_color(guint32 rgba);
+ void set_background_checkerboard(guint32 rgba = 0xC4C4C4FF, bool use_alpha = false);
+
+ // 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; }
+
+ // CMS
+ void set_cms_key(std::string key) {
+ _cms_key = std::move(key);
+ _cms_active = !_cms_key.empty();
+ }
+ 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;
+
+ // State
+ bool is_dragging() const { return _is_dragging; } // selection-chemistry.cpp
+
+ // Encapsulation leaks
+ Cairo::RefPtr<Cairo::ImageSurface> get_backing_store() const; // canvas-item-rotate.cpp
+ Cairo::RefPtr<Cairo::Pattern> get_background_pattern() const { return _background; } // canvas-item-rect.cpp, canvas-item-ctrl.cpp
+
+ /* Methods */
+
+ // Invalidation
+ void redraw_all(); // Mark everything as having changed.
+ void redraw_area(Geom::Rect& 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.
+
+ // Gobblers (tool-base.cpp)
+ int gobble_key_events(guint keyval, guint mask);
+ void gobble_motion_events(guint mask);
+
+ // 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_drawing_disabled(bool disable); // Disable during path ops, etc.
+ void set_all_enter_events(bool on) { _all_enter_events = on; }
+
+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;
+ bool on_draw(const Cairo::RefPtr<Cairo::Context>&) override;
+
+private:
+
+ /* Configuration */
+
+ // Desktop
+ SPDesktop *_desktop = nullptr;
+
+ // Drawing
+ Inkscape::Drawing *_drawing = nullptr;
+
+ // Canvas item root
+ CanvasItemGroup *_canvas_item_root = nullptr;
+
+ // Geometry
+ Geom::IntPoint _pos; ///< Coordinates of top-left pixel of canvas view within canvas.
+ Geom::Affine _affine; ///< The affine that we have been requested to draw at.
+
+ // Background
+ Cairo::RefPtr<Cairo::Pattern> _background; ///< The background of the widget.
+
+ // 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 _drawing_disabled = false; ///< Disable drawing during critical operations
+ bool _need_update = true; // Set true so setting CanvasItem bounds are calculated at least once.
+
+ // Split view
+ Inkscape::SplitDirection _split_direction;
+ Geom::Point _split_position;
+ Inkscape::SplitDirection _hover_direction;
+ bool _split_dragging;
+ Geom::Point _split_drag_start;
+
+ void add_clippath(const Cairo::RefPtr<Cairo::Context>&);
+ 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/color-entry.cpp b/src/ui/widget/color-entry.cpp
new file mode 100644
index 0000000..804350c
--- /dev/null
+++ b/src/ui/widget/color-entry.cpp
@@ -0,0 +1,157 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Entry widget for typing color value in css form
+ *//*
+ * Authors:
+ * Tomasz Boczkowski <penginsbacon@gmail.com>
+ *
+ * Copyright (C) 2014 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#include <glibmm.h>
+#include <glibmm/i18n.h>
+#include <iomanip>
+
+#include "color-entry.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+ColorEntry::ColorEntry(SelectedColor &color)
+ : _color(color)
+ , _updating(false)
+ , _updatingrgba(false)
+ , _prevpos(0)
+ , _lastcolor(0)
+{
+ _color_changed_connection = color.signal_changed.connect(sigc::mem_fun(this, &ColorEntry::_onColorChanged));
+ _color_dragged_connection = color.signal_dragged.connect(sigc::mem_fun(this, &ColorEntry::_onColorChanged));
+ signal_activate().connect(sigc::mem_fun(this, &ColorEntry::_onColorChanged));
+ get_buffer()->signal_inserted_text().connect(sigc::mem_fun(this, &ColorEntry::_inputCheck));
+ _onColorChanged();
+
+ // add extra character for pasting a hash, '#11223344'
+ set_max_length(9);
+ set_width_chars(8);
+ set_tooltip_text(_("Hexadecimal RGBA value of the color"));
+}
+
+ColorEntry::~ColorEntry()
+{
+ _color_changed_connection.disconnect();
+ _color_dragged_connection.disconnect();
+}
+
+void ColorEntry::_inputCheck(guint pos, const gchar * /*chars*/, guint n_chars)
+{
+ // remember position of last character, so we can remove it.
+ // we only overflow by 1 character at most.
+ _prevpos = pos + n_chars - 1;
+}
+
+void ColorEntry::on_changed()
+{
+ if (_updating) {
+ return;
+ }
+ if (_updatingrgba) {
+ return; // Typing text into entry box
+ }
+
+ Glib::ustring text = get_text();
+ bool changed = false;
+
+ // Coerce the value format to hexadecimal
+ for (auto it = text.begin(); it != text.end(); /*++it*/) {
+ if (!g_ascii_isxdigit(*it)) {
+ text.erase(it);
+ changed = true;
+ } else {
+ ++it;
+ }
+ }
+
+ if (text.size() > 8) {
+ text.erase(_prevpos, 1);
+ changed = true;
+ }
+
+ // autofill rules
+ gchar *str = g_strdup(text.c_str());
+ gchar *end = nullptr;
+ guint64 rgba = g_ascii_strtoull(str, &end, 16);
+ ptrdiff_t len = end - str;
+ if (len < 8) {
+ if (len == 0) {
+ rgba = _lastcolor;
+ } else if (len <= 2) {
+ if (len == 1) {
+ rgba *= 17;
+ }
+ rgba = (rgba << 24) + (rgba << 16) + (rgba << 8);
+ } else if (len <= 4) {
+ // display as rrggbbaa
+ rgba = rgba << (4 * (4 - len));
+ guint64 r = rgba & 0xf000;
+ guint64 g = rgba & 0x0f00;
+ guint64 b = rgba & 0x00f0;
+ guint64 a = rgba & 0x000f;
+ rgba = 17 * ((r << 12) + (g << 8) + (b << 4) + a);
+ } else {
+ rgba = rgba << (4 * (8 - len));
+ }
+
+ if (len == 7) {
+ rgba = (rgba & 0xfffffff0) + (_lastcolor & 0x00f);
+ } else if (len == 5) {
+ rgba = (rgba & 0xfffff000) + (_lastcolor & 0xfff);
+ } else if (len != 4 && len != 8) {
+ rgba = (rgba & 0xffffff00) + (_lastcolor & 0x0ff);
+ }
+ }
+
+ _updatingrgba = true;
+ if (changed) {
+ set_text(str);
+ }
+ SPColor color(rgba);
+ _color.setColorAlpha(color, SP_RGBA32_A_F(rgba));
+ _updatingrgba = false;
+
+ g_free(str);
+}
+
+
+void ColorEntry::_onColorChanged()
+{
+ if (_updatingrgba) {
+ return;
+ }
+
+ SPColor color = _color.color();
+ gdouble alpha = _color.alpha();
+
+ _lastcolor = color.toRGBA32(alpha);
+ Glib::ustring text = Glib::ustring::format(std::hex, std::setw(8), std::setfill(L'0'), _lastcolor);
+
+ Glib::ustring old_text = get_text();
+ if (old_text != text) {
+ _updating = true;
+ set_text(text);
+ _updating = false;
+ }
+}
+}
+}
+}
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/widget/color-entry.h b/src/ui/widget/color-entry.h
new file mode 100644
index 0000000..4df80de
--- /dev/null
+++ b/src/ui/widget/color-entry.h
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Entry widget for typing color value in css form
+ *//*
+ * Authors:
+ * Tomasz Boczkowski <penginsbacon@gmail.com>
+ *
+ * Copyright (C) 2014 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_COLOR_ENTRY_H
+#define SEEN_COLOR_ENTRY_H
+
+#include <gtkmm/entry.h>
+#include "ui/selected-color.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class ColorEntry : public Gtk::Entry
+{
+public:
+ ColorEntry(SelectedColor &color);
+ ~ColorEntry() override;
+
+protected:
+ void on_changed() override;
+
+private:
+ void _onColorChanged();
+ void _inputCheck(guint pos, const gchar * /*chars*/, guint /*n_chars*/);
+
+ SelectedColor &_color;
+ sigc::connection _color_changed_connection;
+ sigc::connection _color_dragged_connection;
+ bool _updating;
+ bool _updatingrgba;
+ guint32 _lastcolor;
+ int _prevpos;
+};
+
+}
+}
+}
+
+#endif
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/widget/color-icc-selector.cpp b/src/ui/widget/color-icc-selector.cpp
new file mode 100644
index 0000000..8c8456a
--- /dev/null
+++ b/src/ui/widget/color-icc-selector.cpp
@@ -0,0 +1,1025 @@
+// 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 "document.h"
+#include "inkscape.h"
+#include "profile-manager.h"
+
+#include "svg/svg-icc-color.h"
+
+#include "ui/dialog-events.h"
+#include "ui/util.h"
+#include "ui/widget/color-icc-selector.h"
+#include "ui/widget/color-scales.h"
+#include "ui/widget/color-slider.h"
+#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(SVGICCColor *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)
+ : _impl(nullptr)
+{
+ _impl = new ColorICCSelectorImpl(this, color);
+ init();
+ color.signal_changed.connect(sigc::mem_fun(this, &ColorICCSelector::_colorChanged));
+ // color.signal_dragged.connect(sigc::mem_fun(this, &ColorICCSelector::_colorChanged));
+}
+
+ColorICCSelector::~ColorICCSelector()
+{
+ if (_impl) {
+ delete _impl;
+ _impl = nullptr;
+ }
+}
+
+
+
+ColorICCSelectorImpl::ColorICCSelectorImpl(ColorICCSelector *owner, SelectedColor &color)
+ : _owner(owner)
+ , _color(color)
+ , _updating(FALSE)
+ , _dragging(FALSE)
+ , _fixupNeeded(0)
+ , _fixupBtn(nullptr)
+ , _profileSel(nullptr)
+ , _compUI()
+ , _adj(nullptr)
+ , _slider(nullptr)
+ , _sbtn(nullptr)
+ , _label(nullptr)
+ , _profileName()
+ , _prof(nullptr)
+ , _profChannelCount(0)
+ , _profChangedID(0)
+{
+}
+
+ColorICCSelectorImpl::~ColorICCSelectorImpl()
+{
+ _sbtn = nullptr;
+ _label = nullptr;
+}
+
+void ColorICCSelector::init()
+{
+ gint row = 0;
+
+ _impl->_updating = FALSE;
+ _impl->_dragging = FALSE;
+
+ GtkWidget *t = GTK_WIDGET(gobj());
+
+ _impl->_compUI.clear();
+
+ // Create components
+ row = 0;
+
+
+ _impl->_fixupBtn = gtk_button_new_with_label(_("Fix"));
+ g_signal_connect(G_OBJECT(_impl->_fixupBtn), "clicked", G_CALLBACK(ColorICCSelectorImpl::_fixupHit),
+ (gpointer)_impl);
+ gtk_widget_set_sensitive(_impl->_fixupBtn, FALSE);
+ gtk_widget_set_tooltip_text(_impl->_fixupBtn, _("Fix RGB fallback to match icc-color() value."));
+ gtk_widget_show(_impl->_fixupBtn);
+
+ attachToGridOrTable(t, _impl->_fixupBtn, 0, row, 1, 1);
+
+ // Combobox and store with 2 columns : label (0) and full name (1)
+ GtkListStore *store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_STRING);
+ _impl->_profileSel = _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, _("<none>"), -1);
+
+ gtk_widget_show(_impl->_profileSel);
+ gtk_combo_box_set_active(GTK_COMBO_BOX(_impl->_profileSel), 0);
+
+ attachToGridOrTable(t, _impl->_profileSel, 1, row, 1, 1);
+
+ _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);
+
+ 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) {
+ if (tmp.icc && tmp.icc->colorProfile == name) {
+#ifdef DEBUG_LCMS
+ g_message("Already at name [%s]", name);
+#endif // DEBUG_LCMS
+ }
+ else {
+#ifdef DEBUG_LCMS
+ g_message("Need to switch to profile [%s]", name);
+#endif // DEBUG_LCMS
+ if (tmp.icc) {
+ tmp.icc->colors.clear();
+ }
+ else {
+ tmp.icc = new SVGICCColor();
+ }
+ tmp.icc->colorProfile = name;
+ Inkscape::ColorProfile *newProf = SP_ACTIVE_DOCUMENT->getProfileManager()->find(name);
+ if (newProf) {
+ cmsHTRANSFORM trans = newProf->getTransfFromSRGB8();
+ if (trans) {
+ guint32 val = _color.color().toRGBA32(0);
+ guchar pre[4] = {
+ static_cast<guchar>(SP_RGBA32_R_U(val)),
+ static_cast<guchar>(SP_RGBA32_G_U(val)),
+ static_cast<guchar>(SP_RGBA32_B_U(val)),
+ 255};
+#ifdef DEBUG_LCMS
+ g_message("Shoving in [%02x] [%02x] [%02x]", pre[0], pre[1], pre[2]);
+#endif // DEBUG_LCMS
+ cmsUInt16Number post[4] = { 0, 0, 0, 0 };
+ cmsDoTransform(trans, pre, post, 1);
+#ifdef DEBUG_LCMS
+ g_message("got on out [%04x] [%04x] [%04x] [%04x]", post[0], post[1], post[2], post[3]);
+#endif // DEBUG_LCMS
+ guint count = cmsChannelsOf(asICColorSpaceSig(newProf->getColorSpace()));
+
+ std::vector<colorspace::Component> things =
+ colorspace::getColorSpaceInfo(asICColorSpaceSig(newProf->getColorSpace()));
+
+ for (guint i = 0; i < count; i++) {
+ gdouble val =
+ (((gdouble)post[i]) / 65535.0) * (gdouble)((i < things.size()) ? things[i].scale : 1);
+#ifdef DEBUG_LCMS
+ g_message(" scaled %d by %d to be %f", i, ((i < things.size()) ? things[i].scale : 1), val);
+#endif // DEBUG_LCMS
+ tmp.icc->colors.push_back(val);
+ }
+ cmsHTRANSFORM retrans = newProf->getTransfToSRGB8();
+ if (retrans) {
+ cmsDoTransform(retrans, post, pre, 1);
+#ifdef DEBUG_LCMS
+ g_message(" back out [%02x] [%02x] [%02x]", pre[0], pre[1], pre[2]);
+#endif // DEBUG_LCMS
+ tmp.set(SP_RGBA32_U_COMPOSE(pre[0], pre[1], pre[2], 0xff));
+ }
+
+ dirty = true;
+ }
+ }
+ }
+ }
+ else {
+#ifdef DEBUG_LCMS
+ g_message("NUKE THE ICC");
+#endif // DEBUG_LCMS
+ if (tmp.icc) {
+ delete tmp.icc;
+ tmp.icc = nullptr;
+ dirty = true;
+ _fixupHit(nullptr, this);
+ }
+ else {
+#ifdef DEBUG_LCMS
+ g_message("No icc to nuke");
+#endif // DEBUG_LCMS
+ }
+ }
+
+ if (dirty) {
+#ifdef DEBUG_LCMS
+ g_message("+----------------");
+ g_message("+ new color is [%s]", tmp.toString().c_str());
+#endif // DEBUG_LCMS
+ _setProfile(tmp.icc);
+ //_adjustmentChanged( _compUI[0]._adj, SP_COLOR_ICC_SELECTOR(_csel) );
+ _color.setColor(tmp);
+#ifdef DEBUG_LCMS
+ g_message("+_________________");
+#endif // DEBUG_LCMS
+ }
+}
+
+struct _cmp {
+ bool operator()(const SPObject * const & a, const SPObject * const & b)
+ {
+ const Inkscape::ColorProfile &a_prof = reinterpret_cast<const Inkscape::ColorProfile &>(*a);
+ const Inkscape::ColorProfile &b_prof = reinterpret_cast<const Inkscape::ColorProfile &>(*b);
+ gchar *a_name_casefold = g_utf8_casefold(a_prof.name, -1 );
+ gchar *b_name_casefold = g_utf8_casefold(b_prof.name, -1 );
+ int result = g_strcmp0(a_name_casefold, b_name_casefold);
+ g_free(a_name_casefold);
+ g_free(b_name_casefold);
+ return result < 0;
+ }
+};
+
+template <typename From, typename To>
+struct static_caster { To * operator () (From * value) const { return static_cast<To *>(value); } };
+
+void ColorICCSelectorImpl::_profilesChanged(std::string const &name)
+{
+ GtkComboBox *combo = GTK_COMBO_BOX(_profileSel);
+
+ g_signal_handler_block(G_OBJECT(_profileSel), _profChangedID);
+
+ GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(combo));
+ gtk_list_store_clear(store);
+
+ GtkTreeIter iter;
+ gtk_list_store_append(store, &iter);
+ gtk_list_store_set(store, &iter, 0, _("<none>"), 1, _("<none>"), -1);
+
+ gtk_combo_box_set_active(combo, 0);
+
+ int index = 1;
+ std::vector<SPObject *> current = SP_ACTIVE_DOCUMENT->getResourceList("iccprofile");
+
+ std::set<Inkscape::ColorProfile *> _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;
+// sp_color_icc_set_color( SP_COLOR_ICC( _icc ), &color );
+
+#ifdef DEBUG_LCMS
+ g_message("/^^^^^^^^^ %p::_colorChanged(%08x:%s)", this, _impl->_color.color().toRGBA32(_impl->_color.alpha()),
+ ((_impl->_color.color().icc) ? _impl->_color.color().icc->colorProfile.c_str() : "<null>"));
+#endif // DEBUG_LCMS
+
+#ifdef DEBUG_LCMS
+ g_message("FLIPPIES!!!! %p '%s'", _impl->_color.color().icc,
+ (_impl->_color.color().icc ? _impl->_color.color().icc->colorProfile.c_str() : "<null>"));
+#endif // DEBUG_LCMS
+
+ _impl->_profilesChanged((_impl->_color.color().icc) ? _impl->_color.color().icc->colorProfile : std::string(""));
+ ColorScales<>::setScaled(_impl->_adj, _impl->_color.alpha());
+
+ _impl->_setProfile(_impl->_color.color().icc);
+ _impl->_fixupNeeded = 0;
+ gtk_widget_set_sensitive(_impl->_fixupBtn, FALSE);
+
+ if (_impl->_prof) {
+ if (_impl->_prof->getTransfToSRGB8()) {
+ cmsUInt16Number tmp[4];
+ for (guint i = 0; i < _impl->_profChannelCount; i++) {
+ gdouble val = 0.0;
+ if (_impl->_color.color().icc->colors.size() > i) {
+ if (_impl->_compUI[i]._component.scale == 256) {
+ val = (_impl->_color.color().icc->colors[i] + 128.0) /
+ static_cast<gdouble>(_impl->_compUI[i]._component.scale);
+ }
+ else {
+ val = _impl->_color.color().icc->colors[i] /
+ static_cast<gdouble>(_impl->_compUI[i]._component.scale);
+ }
+ }
+ tmp[i] = val * 0x0ffff;
+ }
+ guchar post[4] = { 0, 0, 0, 0 };
+ cmsHTRANSFORM trans = _impl->_prof->getTransfToSRGB8();
+ if (trans) {
+ cmsDoTransform(trans, tmp, post, 1);
+ guint32 other = SP_RGBA32_U_COMPOSE(post[0], post[1], post[2], 255);
+ if (other != _impl->_color.color().toRGBA32(255)) {
+ _impl->_fixupNeeded = other;
+ gtk_widget_set_sensitive(_impl->_fixupBtn, TRUE);
+#ifdef DEBUG_LCMS
+ g_message("Color needs to change 0x%06x to 0x%06x", _color.toRGBA32(255) >> 8, other >> 8);
+#endif // DEBUG_LCMS
+ }
+ }
+ }
+ }
+ _impl->_updateSliders(-1);
+
+
+ _impl->_updating = FALSE;
+#ifdef DEBUG_LCMS
+ g_message("\\_________ %p::_colorChanged()", this);
+#endif // DEBUG_LCMS
+}
+
+void ColorICCSelectorImpl::_setProfile(SVGICCColor *profile)
+{
+#ifdef DEBUG_LCMS
+ g_message("/^^^^^^^^^ %p::_setProfile(%s)", this, ((profile) ? profile->colorProfile.c_str() : "<null>"));
+#endif // DEBUG_LCMS
+ bool profChanged = false;
+ if (_prof && (!profile || (_profileName != profile->colorProfile))) {
+ // Need to clear out the prior one
+ profChanged = true;
+ _profileName.clear();
+ _prof = nullptr;
+ _profChannelCount = 0;
+ }
+ else if (profile && !_prof) {
+ profChanged = true;
+ }
+
+ for (auto & i : _compUI) {
+ gtk_widget_hide(i._label);
+ i._slider->hide();
+ gtk_widget_hide(i._btn);
+ }
+
+ if (profile) {
+ _prof = SP_ACTIVE_DOCUMENT->getProfileManager()->find(profile->colorProfile.c_str());
+ if (_prof && (asICColorProfileClassSig(_prof->getProfileClass()) != cmsSigNamedColorClass)) {
+ _profChannelCount = cmsChannelsOf(asICColorSpaceSig(_prof->getColorSpace()));
+
+ if (profChanged) {
+ std::vector<colorspace::Component> things =
+ colorspace::getColorSpaceInfo(asICColorSpaceSig(_prof->getColorSpace()));
+ for (size_t i = 0; (i < things.size()) && (i < _profChannelCount); ++i) {
+ _compUI[i]._component = things[i];
+ }
+
+ for (guint i = 0; i < _profChannelCount; i++) {
+ gtk_label_set_text_with_mnemonic(GTK_LABEL(_compUI[i]._label),
+ (i < things.size()) ? things[i].name.c_str() : "");
+
+ _compUI[i]._slider->set_tooltip_text((i < things.size()) ? things[i].tip.c_str() : "");
+ gtk_widget_set_tooltip_text(_compUI[i]._btn, (i < things.size()) ? things[i].tip.c_str() : "");
+
+ _compUI[i]._slider->setColors(SPColor(0.0, 0.0, 0.0).toRGBA32(0xff),
+ SPColor(0.5, 0.5, 0.5).toRGBA32(0xff),
+ SPColor(1.0, 1.0, 1.0).toRGBA32(0xff));
+ /*
+ _compUI[i]._adj = GTK_ADJUSTMENT( gtk_adjustment_new( val, 0.0, _fooScales[i],
+ step, page, page ) );
+ g_signal_connect( G_OBJECT( _compUI[i]._adj ), "value_changed", G_CALLBACK(
+ _adjustmentChanged ), _csel );
+
+ sp_color_slider_set_adjustment( SP_COLOR_SLIDER(_compUI[i]._slider),
+ _compUI[i]._adj );
+ gtk_spin_button_set_adjustment( GTK_SPIN_BUTTON(_compUI[i]._btn),
+ _compUI[i]._adj );
+ gtk_spin_button_set_digits( GTK_SPIN_BUTTON(_compUI[i]._btn), digits );
+ */
+ gtk_widget_show(_compUI[i]._label);
+ _compUI[i]._slider->show();
+ gtk_widget_show(_compUI[i]._btn);
+ // gtk_adjustment_set_value( _compUI[i]._adj, 0.0 );
+ // gtk_adjustment_set_value( _compUI[i]._adj, val );
+ }
+ for (size_t i = _profChannelCount; i < _compUI.size(); i++) {
+ gtk_widget_hide(_compUI[i]._label);
+ _compUI[i]._slider->hide();
+ gtk_widget_hide(_compUI[i]._btn);
+ }
+ }
+ }
+ else {
+ // Give up for now on named colors
+ _prof = nullptr;
+ }
+ }
+
+#ifdef DEBUG_LCMS
+ g_message("\\_________ %p::_setProfile()", this);
+#endif // DEBUG_LCMS
+}
+
+void ColorICCSelectorImpl::_updateSliders(gint ignore)
+{
+ if (_color.color().icc) {
+ for (guint i = 0; i < _profChannelCount; i++) {
+ gdouble val = 0.0;
+ if (_color.color().icc->colors.size() > i) {
+ if (_compUI[i]._component.scale == 256) {
+ val = (_color.color().icc->colors[i] + 128.0) / static_cast<gdouble>(_compUI[i]._component.scale);
+ }
+ else {
+ val = _color.color().icc->colors[i] / static_cast<gdouble>(_compUI[i]._component.scale);
+ }
+ }
+ _compUI[i]._adj->set_value(val);
+ }
+
+ if (_prof) {
+ if (_prof->getTransfToSRGB8()) {
+ for (guint i = 0; i < _profChannelCount; i++) {
+ if (static_cast<gint>(i) != ignore) {
+ cmsUInt16Number *scratch = getScratch();
+ cmsUInt16Number filler[4] = { 0, 0, 0, 0 };
+ for (guint j = 0; j < _profChannelCount; j++) {
+ filler[j] = 0x0ffff * ColorScales<>::getScaled(_compUI[j]._adj);
+ }
+
+ cmsUInt16Number *p = scratch;
+ for (guint x = 0; x < 1024; x++) {
+ for (guint j = 0; j < _profChannelCount; j++) {
+ if (j == i) {
+ *p++ = x * 0x0ffff / 1024;
+ }
+ else {
+ *p++ = filler[j];
+ }
+ }
+ }
+
+ cmsHTRANSFORM trans = _prof->getTransfToSRGB8();
+ if (trans) {
+ cmsDoTransform(trans, scratch, _compUI[i]._map, 1024);
+ if (_compUI[i]._slider)
+ {
+ _compUI[i]._slider->setMap(_compUI[i]._map);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ 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);
+ }
+
+ SPColor other(SP_RGBA32_U_COMPOSE(post[0], post[1], post[2], 255));
+ other.icc = new SVGICCColor();
+ if (iccSelector->_impl->_color.color().icc) {
+ other.icc->colorProfile = iccSelector->_impl->_color.color().icc->colorProfile;
+ }
+
+ guint32 prior = iccSelector->_impl->_color.color().toRGBA32(255);
+ guint32 newer = other.toRGBA32(255);
+
+ if (prior != newer) {
+#ifdef DEBUG_LCMS
+ g_message("Transformed color from 0x%08x to 0x%08x", prior, newer);
+ g_message(" ~~~~ FLIP");
+#endif // DEBUG_LCMS
+ newColor = other;
+ newColor.icc->colors.clear();
+ for (guint i = 0; i < iccSelector->_impl->_profChannelCount; i++) {
+ gdouble val = ColorScales<>::getScaled(iccSelector->_impl->_compUI[i]._adj);
+ val *= iccSelector->_impl->_compUI[i]._component.scale;
+ if (iccSelector->_impl->_compUI[i]._component.scale == 256) {
+ val -= 128;
+ }
+ newColor.icc->colors.push_back(val);
+ }
+ }
+ }
+ iccSelector->_impl->_color.setColorAlpha(newColor, scaled);
+ // iccSelector->_updateInternals( newColor, scaled, iccSelector->_impl->_dragging );
+ iccSelector->_impl->_updateSliders(match);
+
+ iccSelector->_impl->_updating = FALSE;
+#ifdef DEBUG_LCMS
+ g_message("\\_________ %p::_adjustmentChanged()", cs);
+#endif // DEBUG_LCMS
+}
+
+void ColorICCSelectorImpl::_sliderGrabbed()
+{
+ // ColorICCSelector* iccSelector = dynamic_cast<ColorICCSelector*>(SP_COLOR_SELECTOR(cs)->base);
+ // if (!iccSelector->_dragging) {
+ // iccSelector->_dragging = TRUE;
+ // iccSelector->_grabbed();
+ // iccSelector->_updateInternals( iccSelector->_color, ColorScales<>::getScaled( iccSelector->_impl->_adj ),
+ // iccSelector->_dragging );
+ // }
+}
+
+void ColorICCSelectorImpl::_sliderReleased()
+{
+ // ColorICCSelector* iccSelector = dynamic_cast<ColorICCSelector*>(SP_COLOR_SELECTOR(cs)->base);
+ // if (iccSelector->_dragging) {
+ // iccSelector->_dragging = FALSE;
+ // iccSelector->_released();
+ // iccSelector->_updateInternals( iccSelector->_color, ColorScales<>::getScaled( iccSelector->_adj ),
+ // iccSelector->_dragging );
+ // }
+}
+
+#ifdef DEBUG_LCMS
+void ColorICCSelectorImpl::_sliderChanged(SPColorSlider *slider, SPColorICCSelector *cs)
+#else
+void ColorICCSelectorImpl::_sliderChanged()
+#endif // DEBUG_LCMS
+{
+#ifdef DEBUG_LCMS
+ g_message("Changed %p and %p", slider, cs);
+#endif // DEBUG_LCMS
+ // ColorICCSelector* iccSelector = dynamic_cast<ColorICCSelector*>(SP_COLOR_SELECTOR(cs)->base);
+
+ // iccSelector->_updateInternals( iccSelector->_color, ColorScales<>::getScaled( iccSelector->_adj ),
+ // iccSelector->_dragging );
+}
+
+Gtk::Widget *ColorICCSelectorFactory::createWidget(Inkscape::UI::SelectedColor &color) const
+{
+ Gtk::Widget *w = Gtk::manage(new ColorICCSelector(color));
+ return w;
+}
+
+Glib::ustring ColorICCSelectorFactory::modeName() const { return gettext(ColorICCSelector::MODE_NAME); }
+}
+}
+}
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/widget/color-icc-selector.h b/src/ui/widget/color-icc-selector.h
new file mode 100644
index 0000000..2c5ec41
--- /dev/null
+++ b/src/ui/widget/color-icc-selector.h
@@ -0,0 +1,75 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef SEEN_SP_COLOR_ICC_SELECTOR_H
+#define SEEN_SP_COLOR_ICC_SELECTOR_H
+
+#include <gtkmm/widget.h>
+#include <gtkmm/grid.h>
+
+#include "ui/selected-color.h"
+
+namespace Inkscape {
+
+class ColorProfile;
+
+namespace UI {
+namespace Widget {
+
+class ColorICCSelectorImpl;
+
+class ColorICCSelector
+ : public Gtk::Grid
+ {
+ public:
+ static const gchar *MODE_NAME;
+
+ ColorICCSelector(SelectedColor &color);
+ ~ColorICCSelector() override;
+
+ virtual void init();
+
+ protected:
+ void on_show() override;
+
+ virtual void _colorChanged();
+
+ void _recalcColor(gboolean changing);
+
+ private:
+ friend class ColorICCSelectorImpl;
+
+ // By default, disallow copy constructor and assignment operator
+ ColorICCSelector(const ColorICCSelector &obj);
+ ColorICCSelector &operator=(const ColorICCSelector &obj);
+
+ ColorICCSelectorImpl *_impl;
+};
+
+
+class ColorICCSelectorFactory : public ColorSelectorFactory {
+ public:
+ Gtk::Widget *createWidget(SelectedColor &color) const override;
+ Glib::ustring modeName() const override;
+};
+}
+}
+}
+#endif // SEEN_SP_COLOR_ICC_SELECTOR_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/widget/color-notebook.cpp b/src/ui/widget/color-notebook.cpp
new file mode 100644
index 0000000..e0dcd16
--- /dev/null
+++ b/src/ui/widget/color-notebook.cpp
@@ -0,0 +1,347 @@
+// 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 "ui/widget/color-wheel-selector.h"
+//#include "ui/widget/color-wheel-hsluv-selector.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)
+ : Gtk::Grid()
+ , _selected_color(color)
+{
+ set_name("ColorNotebook");
+
+ _available_pages.push_back(new Page(new ColorScalesFactory<SPColorScalesMode::HSL>, "color-selector-hsx"));
+ _available_pages.push_back(new Page(new ColorScalesFactory<SPColorScalesMode::HSV>, "color-selector-hsx"));
+ _available_pages.push_back(new Page(new ColorScalesFactory<SPColorScalesMode::RGB>, "color-selector-rgb"));
+ _available_pages.push_back(new Page(new ColorScalesFactory<SPColorScalesMode::CMYK>, "color-selector-cmyk"));
+ _available_pages.push_back(new Page(new ColorScalesFactory<SPColorScalesMode::HSLUV>, "color-selector-hsluv"));
+ //_available_pages.push_back(new Page(new ColorWheelSelectorFactory, "color-selector-wheel"));
+ _available_pages.push_back(new Page(new ColorICCSelectorFactory, "color-selector-cms"));
+
+ _initUI();
+
+ _selected_color.signal_changed.connect(sigc::mem_fun(this, &ColorNotebook::_onSelectedColorChanged));
+ _selected_color.signal_dragged.connect(sigc::mem_fun(this, &ColorNotebook::_onSelectedColorChanged));
+}
+
+ColorNotebook::~ColorNotebook()
+{
+ if (_onetimepick)
+ _onetimepick.disconnect();
+}
+
+ColorNotebook::Page::Page(Inkscape::UI::ColorSelectorFactory *selector_factory, const char* icon)
+ : selector_factory(selector_factory), icon_name(icon)
+{
+}
+
+void ColorNotebook::set_label(const Glib::ustring& label) {
+ _label->set_markup(label);
+}
+
+void ColorNotebook::_initUI()
+{
+ 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&& page : _available_pages) {
+ _addPage(page);
+ }
+
+ _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();
+ _setCurrentPage(prefs->getInt("/colorselector/page", 0), true);
+ row++;
+
+ auto switcher_path = Glib::ustring("/colorselector/switcher");
+ auto choose_switch = [=](bool compact) {
+ if (compact) {
+ _switcher->hide();
+ _buttonbox->show();
+ }
+ else {
+ _buttonbox->hide();
+ _switcher->show();
+ }
+ };
+
+ _observer = prefs->createObserver(switcher_path, [=](const Preferences::Entry& new_value) {
+ choose_switch(new_value.getBool());
+ });
+
+ choose_switch(prefs->getBool(switcher_path));
+
+ 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);
+
+#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::_onPageSwitched(int page_num)
+{
+ if (get_visible()) {
+ // remember the page we switched to
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setInt("/colorselector/page", page_num);
+ }
+}
+
+
+// TODO pass in param so as to avoid the need for SP_ACTIVE_DOCUMENT
+void ColorNotebook::_updateICCButtons()
+{
+ SPColor color = _selected_color.color();
+ gfloat alpha = _selected_color.alpha();
+
+ g_return_if_fail((0.0 <= alpha) && (alpha <= 1.0));
+
+ /* update color management icon*/
+ gtk_widget_set_sensitive(_box_colormanaged, color.icc != nullptr);
+
+ /* update out-of-gamut icon */
+ gtk_widget_set_sensitive(_box_outofgamut, false);
+ if (color.icc) {
+ Inkscape::ColorProfile *target_profile =
+ SP_ACTIVE_DOCUMENT->getProfileManager()->find(color.icc->colorProfile.c_str());
+ if (target_profile)
+ gtk_widget_set_sensitive(_box_outofgamut, target_profile->GamutCheck(color));
+ }
+
+ /* update too-much-ink icon */
+ gtk_widget_set_sensitive(_box_toomuchink, false);
+ if (color.icc) {
+ Inkscape::ColorProfile *prof = SP_ACTIVE_DOCUMENT->getProfileManager()->find(color.icc->colorProfile.c_str());
+ if (prof && CMSSystem::isPrintColorSpace(prof)) {
+ gtk_widget_show(GTK_WIDGET(_box_toomuchink));
+ double ink_sum = 0;
+ for (double i : color.icc->colors) {
+ ink_sum += i;
+ }
+
+ /* Some literature states that when the sum of paint values exceed 320%, it is considered to be a satured color,
+ which means the paper can get too wet due to an excessive amount of ink. This may lead to several issues
+ such as misalignment and poor quality of printing in general.*/
+ if (ink_sum > 3.2)
+ gtk_widget_set_sensitive(_box_toomuchink, true);
+ }
+ else {
+ gtk_widget_hide(GTK_WIDGET(_box_toomuchink));
+ }
+ }
+}
+
+void ColorNotebook::_setCurrentPage(int i, bool sync_combo)
+{
+ const auto pages = _book->get_children();
+ if (i >= 0 && i < pages.size()) {
+ _book->set_visible_child(*pages[i]);
+ if (sync_combo) {
+ _combo->set_active_by_id(i);
+ }
+ _onPageSwitched(i);
+ }
+}
+
+void ColorNotebook::_addPage(Page &page)
+{
+ if (auto selector_widget = page.selector_factory->createWidget(_selected_color)) {
+ selector_widget->show();
+
+ 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);
+ }
+}
+
+}
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ 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..938ab87
--- /dev/null
+++ b/src/ui/widget/color-notebook.h
@@ -0,0 +1,100 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * A notebook with RGB, CMYK, CMS, HSL, and Wheel pages
+ *//*
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Tomasz Boczkowski <penginsbacon@gmail.com> (c++-sification)
+ *
+ * Copyright (C) 2001-2014 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef SEEN_SP_COLOR_NOTEBOOK_H
+#define SEEN_SP_COLOR_NOTEBOOK_H
+
+#ifdef HAVE_CONFIG_H
+# include "config.h" // only include where actually required!
+#endif
+
+#include <boost/ptr_container/ptr_vector.hpp>
+#include <gtkmm/grid.h>
+#include <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);
+ ~ColorNotebook() override;
+
+ void set_label(const Glib::ustring& label);
+
+protected:
+ struct Page {
+ Page(Inkscape::UI::ColorSelectorFactory *selector_factory, const char* icon);
+
+ std::unique_ptr<Inkscape::UI::ColorSelectorFactory> selector_factory;
+ Glib::ustring icon_name;
+ };
+
+ virtual void _initUI();
+ void _addPage(Page &page);
+
+ void _pickColor(ColorRGBA *color);
+ static void _onPickerClicked(GtkWidget *widget, ColorNotebook *colorbook);
+ void _onPageSwitched(int page_num);
+ virtual void _onSelectedColorChanged();
+
+ 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 */
+ boost::ptr_vector<Page> _available_pages;
+ sigc::connection _onetimepick;
+ IconComboBox* _combo = nullptr;
+
+private:
+ // By default, disallow copy constructor and assignment operator
+ ColorNotebook(const ColorNotebook &obj) = delete;
+ ColorNotebook &operator=(const ColorNotebook &obj) = delete;
+
+ PrefObserver _observer;
+};
+
+}
+}
+}
+#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..a760f71
--- /dev/null
+++ b/src/ui/widget/color-palette.cpp
@@ -0,0 +1,618 @@
+// 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"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+ColorPalette::ColorPalette():
+ _builder(create_builder("color-palette.glade")),
+ _flowbox(get_widget<Gtk::FlowBox>(_builder, "flow-box")),
+ _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();
+ });
+ update_checkbox();
+
+ 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();
+
+ _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);
+ _flowbox.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);
+ }
+}
+
+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;
+ _flowbox.set_halign(enable ? Gtk::ALIGN_FILL : Gtk::ALIGN_START);
+ update_stretch();
+ set_up_scrolling();
+}
+
+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");
+
+ 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);
+ _flowbox.set_valign(Gtk::ALIGN_END);
+
+ if (_rows == 1 && _force_scrollbar) {
+ // horizontal scrolling with single row
+ _flowbox.set_max_children_per_line(_count);
+ _flowbox.set_min_children_per_line(_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);
+ _flowbox.set_min_children_per_line(1);
+ _flowbox.set_max_children_per_line(_count);
+ _scroll_left.hide();
+ _scroll_right.hide();
+ _scroll_btn.show();
+ }
+ }
+ 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();
+
+ _flowbox.set_valign(Gtk::ALIGN_START);
+ _flowbox.set_min_children_per_line(1);
+ _flowbox.set_max_children_per_line(_count);
+
+ _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::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);
+ }
+
+ _flowbox.set_column_spacing(_border);
+ _flowbox.set_row_spacing(_border);
+
+ int width = get_tile_width();
+ int height = get_tile_height();
+ _flowbox.foreach([=](Gtk::Widget& w){
+ w.set_size_request(width, height);
+ });
+}
+
+void ColorPalette::free() {
+ for (auto widget : _flowbox.get_children()) {
+ if (widget) {
+ _flowbox.remove(*widget);
+ delete widget;
+ }
+ }
+}
+
+void ColorPalette::set_colors(const std::vector<Gtk::Widget*>& swatches) {
+ _flowbox.freeze_notify();
+ _flowbox.freeze_child_notify();
+
+ free();
+
+ int count = 0;
+ for (auto widget : swatches) {
+ if (widget) {
+ _flowbox.add(*widget);
+ ++count;
+ }
+ }
+
+ _flowbox.show_all();
+ _count = std::max(1, count);
+ _flowbox.set_max_children_per_line(_count);
+
+ // resize();
+ set_up_scrolling();
+
+ _flowbox.thaw_child_notify();
+ _flowbox.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..bc4c47f
--- /dev/null
+++ b/src/ui/widget/color-palette.h
@@ -0,0 +1,111 @@
+// 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 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(const std::vector<Gtk::Widget*>& 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);
+
+ 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);
+
+ 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;
+
+ 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();
+ 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);
+ 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;
+
+ Glib::RefPtr<Gtk::Builder> _builder;
+ Gtk::FlowBox& _flowbox;
+ 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;
+ int _count = 1;
+ 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
+};
+
+}}} // 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..009ec8e
--- /dev/null
+++ b/src/ui/widget/color-picker.cpp
@@ -0,0 +1,169 @@
+// 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));
+ 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;
+ _changed_signal.emit(rgba);
+ _rgba = 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);
+}
+
+}//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..c7147ae
--- /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);
+
+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..5662656
--- /dev/null
+++ b/src/ui/widget/color-scales.cpp
@@ -0,0 +1,1062 @@
+// 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 <gtkmm/adjustment.h>
+#include <gtkmm/spinbutton.h>
+#include <gtkmm/grid.h>
+#include <glibmm/i18n.h>
+#include <functional>
+
+#include "ui/dialog-events.h"
+#include "ui/widget/color-scales.h"
+#include "ui/widget/color-slider.h"
+#include "ui/widget/scrollprotected.h"
+#include "ui/icon-loader.h"
+#include "preferences.h"
+
+#include "ui/widget/ink-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);
+
+
+template <SPColorScalesMode MODE>
+gchar const *ColorScales<MODE>::SUBMODE_NAMES[] = { N_("None"), N_("RGB"), N_("HSL"),
+ N_("CMYK"), N_("HSV"), N_("HSLuv") };
+
+
+// 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 <SPColorScalesMode MODE>
+ColorScales<MODE>::ColorScales(SelectedColor &color)
+ : 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();
+
+ _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()
+{
+ set_orientation(Gtk::ORIENTATION_VERTICAL);
+
+ Gtk::Expander *wheel_frame = nullptr;
+
+ if constexpr (
+ MODE == SPColorScalesMode::HSL ||
+ MODE == SPColorScalesMode::HSV ||
+ MODE == SPColorScalesMode::HSLUV)
+ {
+ /* Create wheel */
+ if constexpr (MODE == SPColorScalesMode::HSLUV) {
+ _wheel = Gtk::manage(new Inkscape::UI::Widget::ColorWheelHSLuv());
+ } 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();
+
+ if constexpr (
+ MODE == SPColorScalesMode::HSL ||
+ MODE == SPColorScalesMode::HSV ||
+ MODE == SPColorScalesMode::HSLUV)
+ {
+ // 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)
+ {
+ _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.preserveICC();
+ _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 {
+ 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>
+gfloat ColorScales<MODE>::getScaled(Glib::RefPtr<Gtk::Adjustment> const &a)
+{
+ gfloat val = a->get_value() / a->get_upper();
+ return val;
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::setScaled(Glib::RefPtr<Gtk::Adjustment> &a, gfloat v, bool constrained)
+{
+ auto upper = a->get_upper();
+ gfloat val = v * upper;
+ if (constrained) {
+ // TODO: do we want preferences for these?
+ if (upper == 255) {
+ val = round(val/16) * 16;
+ } else {
+ val = round(val/10) * 10;
+ }
+ }
+ 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 {
+ 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::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()
+{
+ gfloat rgba[4];
+ gfloat c[4];
+
+ 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"));
+ _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"));
+
+ _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"));
+
+ _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"));
+
+ _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"));
+
+ _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 {
+ g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__);
+ }
+}
+
+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.preserveICC();
+ _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<gfloat, 4> const adj = [this]() -> std::array<gfloat, 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 {
+ 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;
+ }
+}
+
+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) const
+{
+ Gtk::Widget *w = Gtk::manage(new ColorScales<MODE>(color));
+ 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 {
+ 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 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>;
+
+} // 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..6e5b9a2
--- /dev/null
+++ b/src/ui/widget/color-scales.h
@@ -0,0 +1,127 @@
+// 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 "ui/selected-color.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class ColorSlider;
+class ColorWheel;
+
+enum class SPColorScalesMode {
+ NONE,
+ RGB,
+ HSL,
+ CMYK,
+ HSV,
+ HSLUV
+};
+
+template <SPColorScalesMode MODE = SPColorScalesMode::NONE>
+class ColorScales
+ : public Gtk::Box
+{
+public:
+ static gchar const *SUBMODE_NAMES[];
+
+ static gfloat getScaled(Glib::RefPtr<Gtk::Adjustment> const &a);
+ static void setScaled(Glib::RefPtr<Gtk::Adjustment> &a, gfloat v, bool constrained = false);
+
+ ColorScales(SelectedColor &color);
+ ~ColorScales() override;
+
+ void setupMode();
+ 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;
+
+ virtual void _initUI();
+
+ 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;
+
+private:
+ // 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) const override;
+ Glib::ustring modeName() const override;
+};
+
+} // 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..6ffd266
--- /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..963e107
--- /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..865d827
--- /dev/null
+++ b/src/ui/widget/combo-box-entry-tool-item.cpp
@@ -0,0 +1,707 @@
+// 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 <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),
+ _isload(true),
+ _markup(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);
+ }
+ gtk_cell_renderer_set_fixed_size(_cell, -1, height);
+ g_signal_connect(G_OBJECT(comboBoxEntry), "popup", G_CALLBACK(combo_box_popup_cb), this);
+ gtk_cell_layout_clear( GTK_CELL_LAYOUT( comboBoxEntry ) );
+ gtk_cell_layout_pack_start( GTK_CELL_LAYOUT( comboBoxEntry ), _cell, true );
+ gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT( comboBoxEntry ), _cell,
+ GtkCellLayoutDataFunc (_cell_data_func), 0, nullptr );
+ }
+
+ // Optionally widen the combobox width... which widens the drop-down list in list mode.
+ if( _extra_width > 0 ) {
+ GtkRequisition req;
+ gtk_widget_get_preferred_size(GTK_WIDGET(_combobox), &req, nullptr);
+ gtk_widget_set_size_request( GTK_WIDGET( _combobox ),
+ req.width + _extra_width, -1 );
+ }
+
+ // Get reference to GtkEntry and fiddle a bit with it.
+ GtkWidget *child = gtk_bin_get_child( GTK_BIN(comboBoxEntry) );
+
+ // Name it so we can muck with it using an RC file
+ gtk_widget_set_name( child, entry_name );
+ g_free( entry_name );
+
+ if( child && GTK_IS_ENTRY( child ) ) {
+
+ _entry = GTK_ENTRY(child);
+
+ // Change width
+ if( _entry_width > 0 ) {
+ gtk_entry_set_width_chars (GTK_ENTRY (child), _entry_width );
+ }
+
+ // Add pop-up entry completion if required
+ if( _popup ) {
+ popup_enable();
+ }
+
+ // Add 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();
+ }
+}
+
+gboolean ComboBoxEntryToolItem::combo_box_popup_cb(ComboBoxEntryToolItem *widget, gpointer data)
+{
+ auto w = reinterpret_cast<ComboBoxEntryToolItem *>(data);
+ GtkComboBox *comboBoxEntry = GTK_COMBO_BOX(w->_combobox);
+ if (!w->_isload && !w->_markup && w->_cell_data_func) {
+ // first click is always displaying something wrong.
+ // Second loading of the screen should have preallocated space, and only has to render the text now
+ gtk_cell_layout_set_cell_data_func(GTK_CELL_LAYOUT(comboBoxEntry), w->_cell,
+ GtkCellLayoutDataFunc(w->_cell_data_func), widget, nullptr);
+ w->_markup = true;
+ }
+ w->_isload = false;
+ return true;
+}
+
+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..978f2ef
--- /dev/null
+++ b/src/ui/widget/combo-box-entry-tool-item.h
@@ -0,0 +1,154 @@
+// 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;
+ gboolean _isload;
+ gboolean _markup;
+
+ // 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 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..4647a00
--- /dev/null
+++ b/src/ui/widget/combo-enums.h
@@ -0,0 +1,224 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Nicholas Bishop <nicholasbishop@gmail.com>
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ *
+ * Copyright (C) 2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_COMBO_ENUMS_H
+#define INKSCAPE_UI_WIDGET_COMBO_ENUMS_H
+
+#include "ui/widget/labelled.h"
+#include <gtkmm/combobox.h>
+#include <gtkmm/liststore.h>
+#include "attr-widget.h"
+#include "util/enums.h"
+#include <glibmm/i18n.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Simplified management of enumerations in the UI as combobox.
+ */
+template<typename E> class ComboBoxEnum : public Gtk::ComboBox, public AttrWidget
+{
+private:
+ int on_sort_compare( const Gtk::TreeModel::iterator & a, const Gtk::TreeModel::iterator & b)
+ {
+ Glib::ustring an=(*a)[_columns.label];
+ Glib::ustring bn=(*b)[_columns.label];
+ return an.compare(bn);
+ }
+
+ bool _sort;
+
+public:
+ ComboBoxEnum(E default_value, const Util::EnumDataConverter<E>& c, const SPAttr a = SPAttr::INVALID, bool sort = true)
+ : AttrWidget(a, (unsigned int)default_value), setProgrammatically(false), _converter(c)
+ {
+ _sort = sort;
+
+ signal_changed().connect(signal_attr_changed().make_slot());
+ gtk_widget_add_events(GTK_WIDGET(gobj()), GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK);
+ signal_scroll_event().connect(sigc::mem_fun(*this, &ComboBoxEnum<E>::on_scroll_event));
+ _model = Gtk::ListStore::create(_columns);
+ set_model(_model);
+
+ pack_start(_columns.label);
+
+ // Initialize list
+ for(int i = 0; i < static_cast<int>(_converter._length); ++i) {
+ Gtk::TreeModel::Row row = *_model->append();
+ const Util::EnumData<E>* data = &_converter.data(i);
+ row[_columns.data] = data;
+ row[_columns.label] = _( _converter.get_label(data->id).c_str() );
+ }
+ set_active_by_id(default_value);
+
+ // Sort the list
+ if (sort) {
+ _model->set_default_sort_func(sigc::mem_fun(*this, &ComboBoxEnum<E>::on_sort_compare));
+ _model->set_sort_column(_columns.label, Gtk::SORT_ASCENDING);
+ }
+ }
+
+ ComboBoxEnum(const Util::EnumDataConverter<E>& c, const SPAttr a = SPAttr::INVALID, bool sort = true)
+ : AttrWidget(a, (unsigned int) 0), setProgrammatically(false), _converter(c)
+ {
+ _sort = sort;
+
+ signal_changed().connect(signal_attr_changed().make_slot());
+ gtk_widget_add_events(GTK_WIDGET(gobj()), GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK);
+ signal_scroll_event().connect(sigc::mem_fun(*this, &ComboBoxEnum<E>::on_scroll_event));
+
+ _model = Gtk::ListStore::create(_columns);
+ set_model(_model);
+
+ pack_start(_columns.label);
+
+ // Initialize list
+ for(unsigned int i = 0; i < _converter._length; ++i) {
+ Gtk::TreeModel::Row row = *_model->append();
+ const Util::EnumData<E>* data = &_converter.data(i);
+ row[_columns.data] = data;
+ row[_columns.label] = _( _converter.get_label(data->id).c_str() );
+ }
+ set_active(0);
+
+ // Sort the list
+ if (_sort) {
+ _model->set_default_sort_func(sigc::mem_fun(*this, &ComboBoxEnum<E>::on_sort_compare));
+ _model->set_sort_column(_columns.label, Gtk::SORT_ASCENDING);
+ }
+ }
+
+ Glib::ustring get_as_attribute() const override
+ {
+ return get_active_data()->key;
+ }
+
+ void set_from_attribute(SPObject* o) override
+ {
+ setProgrammatically = true;
+ const gchar* val = attribute_value(o);
+ if(val)
+ set_active_by_id(_converter.get_id_from_key(val));
+ else
+ set_active(get_default()->as_uint());
+ }
+
+ const Util::EnumData<E>* get_active_data() const
+ {
+ Gtk::TreeModel::iterator i = this->get_active();
+ if(i)
+ return (*i)[_columns.data];
+ return nullptr;
+ }
+
+ void add_row(const Glib::ustring& s)
+ {
+ Gtk::TreeModel::Row row = *_model->append();
+ row[_columns.data] = 0;
+ row[_columns.label] = s;
+ }
+
+ void remove_row(E id) {
+ Gtk::TreeModel::iterator i;
+
+ for(i = _model->children().begin(); i != _model->children().end(); ++i) {
+ const Util::EnumData<E>* data = (*i)[_columns.data];
+
+ if(data->id == id)
+ break;
+ }
+
+ if(i != _model->children().end())
+ _model->erase(i);
+ }
+
+ void set_active_by_id(E id) {
+ setProgrammatically = true;
+ for(Gtk::TreeModel::iterator i = _model->children().begin();
+ i != _model->children().end(); ++i)
+ {
+ const Util::EnumData<E>* data = (*i)[_columns.data];
+ if(data->id == id) {
+ set_active(i);
+ break;
+ }
+ }
+ };
+
+ bool on_scroll_event(GdkEventScroll *event) override { return false; }
+
+ void set_active_by_key(const Glib::ustring& key) {
+ setProgrammatically = true;
+ set_active_by_id( _converter.get_id_from_key(key) );
+ };
+
+ bool setProgrammatically;
+
+private:
+ class Columns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ Columns()
+ {
+ add(data);
+ add(label);
+ }
+
+ Gtk::TreeModelColumn<const Util::EnumData<E>*> data;
+ Gtk::TreeModelColumn<Glib::ustring> label;
+ };
+
+ Columns _columns;
+ Glib::RefPtr<Gtk::ListStore> _model;
+ const Util::EnumDataConverter<E>& _converter;
+};
+
+
+/**
+ * Simplified management of enumerations in the UI as combobox.
+ */
+template<typename E> class LabelledComboBoxEnum : public Labelled
+{
+public:
+ LabelledComboBoxEnum( Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ const Util::EnumDataConverter<E>& c,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true,
+ bool sorted = true)
+ : Labelled(label, tooltip, new ComboBoxEnum<E>(c, 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..1fc8b00
--- /dev/null
+++ b/src/ui/widget/combo-tool-item.h
@@ -0,0 +1,136 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_COMBO_TOOL_ITEM
+#define SEEN_COMBO_TOOL_ITEM
+
+/*
+ * Authors:
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2017 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+/**
+ A combobox that can be displayed in a toolbar
+*/
+
+#include <gtkmm/toolitem.h>
+#include <gtkmm/liststore.h>
+#include <sigc++/sigc++.h>
+#include <vector>
+
+namespace Gtk {
+class Box;
+class ComboBox;
+class Label;
+class MenuItem;
+class RadioMenuItem;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+class ComboToolItemColumns : public Gtk::TreeModel::ColumnRecord {
+public:
+ ComboToolItemColumns() {
+ add (col_label);
+ add (col_value);
+ add (col_icon);
+ add (col_pixbuf);
+ add (col_data); // Used to store a pointer
+ add (col_tooltip);
+ add (col_sensitive);
+ }
+ Gtk::TreeModelColumn<Glib::ustring> col_label;
+ Gtk::TreeModelColumn<Glib::ustring> col_value;
+ Gtk::TreeModelColumn<Glib::ustring> col_icon;
+ Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf> > col_pixbuf;
+ Gtk::TreeModelColumn<void *> col_data;
+ Gtk::TreeModelColumn<Glib::ustring> col_tooltip;
+ Gtk::TreeModelColumn<bool> col_sensitive;
+};
+
+
+class ComboToolItem : public Gtk::ToolItem {
+
+public:
+ static ComboToolItem* create(const Glib::ustring &label,
+ const Glib::ustring &tooltip,
+ const Glib::ustring &stock_id,
+ Glib::RefPtr<Gtk::ListStore> store,
+ bool has_entry = false);
+
+ /* Style of combobox */
+ void use_label( bool use_label );
+ void use_icon( bool use_icon );
+ void focus_on_click( bool focus_on_click );
+ void use_pixbuf( bool use_pixbuf );
+ void use_group_label( bool use_group_label ); // Applies to tool item only
+
+ gint get_active() { return _active; }
+ Glib::ustring get_active_text();
+ void set_active( gint active );
+ void set_icon_size( Gtk::BuiltinIconSize size ) { _icon_size = size; }
+
+ Glib::RefPtr<Gtk::ListStore> get_store() { return _store; }
+
+ sigc::signal<void, int> signal_changed() { return _changed; }
+ sigc::signal<void, int> signal_changed_after() { return _changed_after; }
+
+protected:
+ bool on_create_menu_proxy() override;
+ void populate_combobox();
+
+ /* Signals */
+ sigc::signal<void, int> _changed;
+ sigc::signal<void, int> _changed_after; // Needed for unit tracker which eats _changed.
+
+private:
+
+ Glib::ustring _group_label;
+ Glib::ustring _tooltip;
+ Glib::ustring _stock_id;
+ Glib::RefPtr<Gtk::ListStore> _store;
+
+ gint _active; /* Active menu item/button */
+
+ /* Style */
+ bool _use_label;
+ bool _use_icon; // Applies to menu item only
+ bool _use_pixbuf;
+ Gtk::BuiltinIconSize _icon_size;
+
+ /* Combobox in tool */
+ Gtk::ComboBox* _combobox;
+ Gtk::Label* _group_label_widget;
+ Gtk::Box* _container;
+
+ Gtk::MenuItem* _menuitem;
+ std::vector<Gtk::RadioMenuItem*> _radiomenuitems;
+
+ /* Internal Callbacks */
+ void on_changed_combobox();
+ void on_toggled_radiomenu(int n);
+
+ ComboToolItem(Glib::ustring group_label,
+ Glib::ustring tooltip,
+ Glib::ustring stock_id,
+ Glib::RefPtr<Gtk::ListStore> store,
+ bool has_entry = false);
+};
+}
+}
+}
+#endif /* SEEN_COMBO_TOOL_ITEM */
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/widget/dash-selector.cpp b/src/ui/widget/dash-selector.cpp
new file mode 100644
index 0000000..c3f40ab
--- /dev/null
+++ b/src/ui/widget/dash-selector.cpp
@@ -0,0 +1,259 @@
+// 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
+ surface = sp_text_to_pixbuf((char *)"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..f98bb50
--- /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..2e5c40b
--- /dev/null
+++ b/src/ui/widget/entity-entry.cpp
@@ -0,0 +1,208 @@
+// 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)
+{
+ const char *text = rdf_get_work_entity (doc, _entity);
+ // If RDF title is not set, get the document's <title> and set the RDF:
+ if ( !text && !strcmp(_entity->name, "title") && doc->getRoot() ) {
+ text = doc->getRoot()->title();
+ rdf_set_work_entity(doc, _entity, text);
+ }
+ static_cast<Gtk::Entry*>(_packable)->set_text (text ? text : "");
+}
+
+
+void EntityLineEntry::load_from_preferences()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring text = prefs->getString(PREFS_METADATA + Glib::ustring(_entity->name));
+ if (text.length() > 0) {
+ static_cast<Gtk::Entry*>(_packable)->set_text (text.c_str());
+ }
+}
+
+void
+EntityLineEntry::on_changed()
+{
+ if (_wr->isUpdating() || !_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);
+}
+
+EntityMultiLineEntry::EntityMultiLineEntry (rdf_work_entity_t* ent, Registry& wr)
+: EntityEntry (ent, wr)
+{
+ Gtk::ScrolledWindow *s = new Gtk::ScrolledWindow;
+ s->set_policy (Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ s->set_shadow_type (Gtk::SHADOW_IN);
+ _packable = s;
+ _v.set_size_request (-1, 35);
+ _v.set_wrap_mode (Gtk::WRAP_WORD);
+ _v.set_accepts_tab (false);
+ s->add (_v);
+ _v.set_tooltip_text (_(ent->tip));
+ _changed_connection = _v.get_buffer()->signal_changed().connect (sigc::mem_fun (*this, &EntityMultiLineEntry::on_changed));
+}
+
+EntityMultiLineEntry::~EntityMultiLineEntry()
+{
+ delete static_cast<Gtk::ScrolledWindow*>(_packable);
+}
+
+void EntityMultiLineEntry::update(SPDocument *doc)
+{
+ const char *text = rdf_get_work_entity (doc, _entity);
+ // If RDF title is not set, get the document's <title> and set the RDF:
+ if ( !text && !strcmp(_entity->name, "title") && doc->getRoot() ) {
+ text = doc->getRoot()->title();
+ rdf_set_work_entity(doc, _entity, text);
+ }
+ Gtk::ScrolledWindow *s = static_cast<Gtk::ScrolledWindow*>(_packable);
+ Gtk::TextView *tv = static_cast<Gtk::TextView*>(s->get_child());
+ tv->get_buffer()->set_text (text ? text : "");
+}
+
+
+void EntityMultiLineEntry::load_from_preferences()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring text = prefs->getString(PREFS_METADATA + Glib::ustring(_entity->name));
+ if (text.length() > 0) {
+ Gtk::ScrolledWindow *s = static_cast<Gtk::ScrolledWindow*>(_packable);
+ Gtk::TextView *tv = static_cast<Gtk::TextView*>(s->get_child());
+ tv->get_buffer()->set_text (text.c_str());
+ }
+}
+
+
+void
+EntityMultiLineEntry::on_changed()
+{
+ if (_wr->isUpdating() || !_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..3168e4c
--- /dev/null
+++ b/src/ui/widget/entity-entry.h
@@ -0,0 +1,85 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ *
+ * Copyright (C) 2005 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_ENTITY_ENTRY__H
+#define INKSCAPE_UI_WIDGET_ENTITY_ENTRY__H
+
+#include <gtkmm/textview.h>
+
+struct rdf_work_entity_t;
+class SPDocument;
+
+namespace Gtk {
+class TextBuffer;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class Registry;
+
+class EntityEntry {
+public:
+ static EntityEntry* create (rdf_work_entity_t* ent, Registry& wr);
+ virtual ~EntityEntry() = 0;
+ virtual void update (SPDocument *doc) = 0;
+ virtual void on_changed() = 0;
+ virtual void load_from_preferences() = 0;
+ void save_to_preferences(SPDocument *doc);
+ Gtk::Label _label;
+ Gtk::Widget *_packable;
+
+protected:
+ EntityEntry (rdf_work_entity_t* ent, Registry& wr);
+ sigc::connection _changed_connection;
+ rdf_work_entity_t *_entity;
+ Registry *_wr;
+};
+
+class EntityLineEntry : public EntityEntry {
+public:
+ EntityLineEntry (rdf_work_entity_t* ent, Registry& wr);
+ ~EntityLineEntry() override;
+ void update (SPDocument *doc) override;
+ void load_from_preferences() override;
+
+protected:
+ void on_changed() override;
+};
+
+class EntityMultiLineEntry : public EntityEntry {
+public:
+ EntityMultiLineEntry (rdf_work_entity_t* ent, Registry& wr);
+ ~EntityMultiLineEntry() override;
+ void update (SPDocument *doc) override;
+ void load_from_preferences() override;
+
+protected:
+ void on_changed() override;
+ Gtk::TextView _v;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_ENTITY_ENTRY__H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/widget/entry.cpp b/src/ui/widget/entry.cpp
new file mode 100644
index 0000000..e9a63c5
--- /dev/null
+++ b/src/ui/widget/entry.cpp
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Johan Engelen <goejendaagh@zonnet.nl>
+ *
+ * Copyright (C) 2006 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "entry.h"
+
+#include <gtkmm/entry.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Entry::Entry( Glib::ustring const &label, Glib::ustring const &tooltip,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Labelled(label, tooltip, new Gtk::Entry(), suffix, icon, mnemonic)
+{
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
diff --git a/src/ui/widget/entry.h b/src/ui/widget/entry.h
new file mode 100644
index 0000000..3674d51
--- /dev/null
+++ b/src/ui/widget/entry.h
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Johan Engelen <goejendaagh@zonnet.nl>
+ *
+ * Copyright (C) 2006 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_ENTRY__H
+#define INKSCAPE_UI_WIDGET_ENTRY__H
+
+#include "labelled.h"
+
+namespace Gtk {
+class Entry;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Helperclass for Gtk::Entry widgets.
+ */
+class Entry : public Labelled
+{
+public:
+ Entry( Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ // TO DO: add methods to access Gtk::Entry widget
+
+ Gtk::Entry* getEntry() {return (Gtk::Entry*)(_widget);};
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_ENTRY__H
diff --git a/src/ui/widget/export-lists.cpp b/src/ui/widget/export-lists.cpp
new file mode 100644
index 0000000..ba1da1a
--- /dev/null
+++ b/src/ui/widget/export-lists.cpp
@@ -0,0 +1,287 @@
+// 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"
+
+using Inkscape::Util::unit_table;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+ExtensionList::ExtensionList()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ _watch_pref = prefs->createObserver("/dialogs/export/show_all_extensions", [=]() { setup(); });
+}
+
+ExtensionList::ExtensionList(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade)
+ : Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBoxText>(cobject, refGlade)
+{
+ // This duplication is silly, but needed because C++ can't
+ // both deligate the constructor, and construct for glade
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ _watch_pref = prefs->createObserver("/dialogs/export/show_all_extensions", [=]() { setup(); });
+}
+
+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, 1, 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);
+
+ // 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 ? extension_cb->getExtension() : nullptr;
+}
+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..5bea6b3
--- /dev/null
+++ b/src/ui/widget/export-lists.h
@@ -0,0 +1,102 @@
+// 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 {};
+
+public:
+ void setup();
+ Glib::ustring getFileExtension();
+ void setExtensionFromFilename(Glib::ustring const &filename);
+ void removeExtension(Glib::ustring &filename);
+ void createList();
+ Inkscape::Extension::Output *getExtension();
+
+private:
+ PrefObserver _watch_pref;
+ std::map<std::string, Inkscape::Extension::Output *> ext_to_mod;
+};
+
+class ExportList : public Gtk::Grid
+{
+public:
+ ExportList(){};
+ ExportList(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade)
+ : 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 _dpi_col = 2;
+ int _delete_col = 3;
+};
+
+} // 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..008caff
--- /dev/null
+++ b/src/ui/widget/export-preview.cpp
@@ -0,0 +1,235 @@
+// 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 <glibmm/i18n.h>
+#include <glibmm/main.h>
+#include <glibmm/timer.h>
+#include <gtkmm.h>
+
+#include "display/cairo-utils.h"
+#include "inkscape.h"
+#include "object/sp-defs.h"
+#include "object/sp-item.h"
+#include "object/sp-namedview.h"
+#include "object/sp-root.h"
+#include "util/preview.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+void ExportPreview::resetPixels()
+{
+ clear();
+ show();
+}
+
+ExportPreview::~ExportPreview()
+{
+ if (drawing) {
+ if (_document) {
+ _document->getRoot()->invoke_hide(visionkey);
+ }
+ delete drawing;
+ drawing = nullptr;
+ }
+ if (timer) {
+ timer->stop();
+ delete timer;
+ timer = nullptr;
+ }
+ if (renderTimer) {
+ renderTimer->stop();
+ delete renderTimer;
+ renderTimer = nullptr;
+ }
+ _item = nullptr;
+ _document = nullptr;
+}
+
+void ExportPreview::setItem(SPItem *item)
+{
+ _item = item;
+ _dbox = Geom::OptRect();
+}
+void ExportPreview::setDbox(double x0, double x1, double y0, double y1)
+{
+ if (!_document) {
+ return;
+ }
+ if ((x1 - x0 == 0) || (y1 - y0) == 0) {
+ return;
+ }
+ _item = nullptr;
+ _dbox = Geom::Rect(Geom::Point(x0, y0), Geom::Point(x1, y1)) * _document->dt2doc();
+}
+
+void ExportPreview::setDocument(SPDocument *document)
+{
+ if (drawing) {
+ if (_document) {
+ _document->getRoot()->invoke_hide(visionkey);
+ }
+ delete drawing;
+ drawing = nullptr;
+ }
+ _document = document;
+ if (_document) {
+ drawing = new Inkscape::Drawing();
+ visionkey = SPItem::display_key_new(1);
+ DrawingItem *ai = _document->getRoot()->invoke_show(*drawing, visionkey, SP_ITEM_SHOW_DISPLAY);
+ if (ai) {
+ drawing->setRoot(ai);
+ }
+ }
+}
+
+void ExportPreview::refreshHide(const std::vector<SPItem *> &list)
+{
+ _hidden_excluded = std::vector<SPItem *>(list.begin(), list.end());
+ _hidden_requested = true;
+}
+
+void ExportPreview::performHide(const std::vector<SPItem *> *list)
+{
+ if (_document) {
+ if (isLastHide) {
+ if (drawing) {
+ if (_document) {
+ _document->getRoot()->invoke_hide(visionkey);
+ }
+ delete drawing;
+ drawing = nullptr;
+ }
+ drawing = new Inkscape::Drawing();
+ visionkey = SPItem::display_key_new(1);
+ DrawingItem *ai = _document->getRoot()->invoke_show(*drawing, visionkey, SP_ITEM_SHOW_DISPLAY);
+ if (ai) {
+ drawing->setRoot(ai);
+ }
+ isLastHide = false;
+ }
+ if (list && !list->empty()) {
+ hide_other_items_recursively(_document->getRoot(), *list);
+ isLastHide = true;
+ }
+ }
+}
+
+void ExportPreview::hide_other_items_recursively(SPObject *o, const std::vector<SPItem *> &list)
+{
+ if (SP_IS_ITEM(o) && !SP_IS_DEFS(o) && !SP_IS_ROOT(o) && !SP_IS_GROUP(o) &&
+ list.end() == find(list.begin(), list.end(), o)) {
+ SP_ITEM(o)->invoke_hide(visionkey);
+ }
+
+ // recurse
+ if (list.end() == find(list.begin(), list.end(), o)) {
+ for (auto &child : o->children) {
+ hide_other_items_recursively(&child, list);
+ }
+ }
+}
+
+void ExportPreview::queueRefresh()
+{
+ if (drawing == nullptr) {
+ return;
+ }
+ if (!pending) {
+ pending = true;
+ if (!timer) {
+ timer = new Glib::Timer();
+ }
+ Glib::signal_idle().connect(sigc::mem_fun(this, &ExportPreview::refreshCB), Glib::PRIORITY_DEFAULT_IDLE);
+ }
+}
+
+bool ExportPreview::refreshCB()
+{
+ bool callAgain = true;
+ if (!timer) {
+ timer = new Glib::Timer();
+ }
+ if (timer->elapsed() > minDelay) {
+ callAgain = false;
+ refreshPreview();
+ pending = false;
+ }
+ return callAgain;
+}
+
+void ExportPreview::refreshPreview()
+{
+ auto document = _document;
+ if (!timer) {
+ timer = new Glib::Timer();
+ }
+ if (timer->elapsed() < minDelay) {
+ // Do not refresh too quickly
+ queueRefresh();
+ } else if (document) {
+ renderPreview();
+ timer->reset();
+ }
+}
+
+/*
+This is main function which finally render preview. Call this after setting document, item and dbox.
+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 simply we do nothing.
+*/
+void ExportPreview::renderPreview()
+{
+ if (!renderTimer) {
+ renderTimer = new Glib::Timer();
+ }
+ renderTimer->reset();
+ if (drawing == nullptr) {
+ return;
+ }
+
+ if (_hidden_requested) {
+ this->performHide(&_hidden_excluded);
+ _hidden_requested = false;
+ }
+ if (_document) {
+ GdkPixbuf *pb = nullptr;
+ if (_item) {
+ pb = Inkscape::UI::PREVIEW::render_preview(_document, *drawing, _item, size, size);
+ } else if (_dbox) {
+ pb = Inkscape::UI::PREVIEW::render_preview(_document, *drawing, nullptr, size, size, &_dbox);
+ }
+ if (pb) {
+ set(Glib::wrap(pb));
+ show();
+ }
+ }
+
+ renderTimer->stop();
+ minDelay = std::max(0.1, renderTimer->elapsed() * 3.0);
+}
+
+} // 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..ad7f49c
--- /dev/null
+++ b/src/ui/widget/export-preview.h
@@ -0,0 +1,87 @@
+// 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_PREVIEW_H
+#define SP_EXPORT_PREVIEW_H
+
+#include <gtkmm.h>
+
+#include "desktop.h"
+#include "document.h"
+
+class SPObject;
+class SPItem;
+
+namespace Glib {
+class Timer;
+}
+
+namespace Inkscape {
+class Drawing;
+namespace UI {
+namespace Dialog {
+
+class ExportPreview : public Gtk::Image
+{
+public:
+ ExportPreview() {};
+ ~ExportPreview() override;
+
+ ExportPreview(BaseObjectType* cobject, const Glib::RefPtr<Gtk::Builder>& refGlade):Gtk::Image(cobject){};
+private:
+ int size = 128; // size of preview image
+ bool isLastHide = false;
+ SPDocument *_document = nullptr;
+ SPItem *_item = nullptr;
+ Geom::OptRect _dbox;
+
+ Drawing *drawing = nullptr;
+ unsigned int visionkey = 0;
+ Glib::Timer *timer = nullptr;
+ Glib::Timer *renderTimer = nullptr;
+ bool pending = false;
+ gdouble minDelay = 0.1;
+
+ std::vector<SPItem *> _hidden_excluded;
+ bool _hidden_requested = false;
+public:
+ void setDocument(SPDocument *document);
+ void refreshHide(const std::vector<SPItem *> &list = {});
+ void hide_other_items_recursively(SPObject *o, const std::vector<SPItem *> &list);
+ void setItem(SPItem *item);
+ void setDbox(double x0, double x1, double y0, double y1);
+ void queueRefresh();
+ void resetPixels();
+
+ void setSize(int newSize)
+ {
+ size = newSize;
+ resetPixels();
+ }
+private:
+ void refreshPreview();
+ void renderPreview();
+ bool refreshCB();
+ void performHide(const std::vector<SPItem *> *list);
+};
+} // 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/fill-style.cpp b/src/ui/widget/fill-style.cpp
new file mode 100644
index 0000000..b5b9d9a
--- /dev/null
+++ b/src/ui/widget/fill-style.cpp
@@ -0,0 +1,714 @@
+// 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 "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 "ui/dialog/dialog-base.h"
+#include "style.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); }
+ });
+
+ 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->selection) {
+ // subselChangedConn =
+ // desktop->connectToolSubselectionChanged(sigc::hide(sigc::mem_fun(*this, &FillNStroke::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 = dynamic_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 (_desktop->selection != nullptr) {
+ std::vector<SPItem*> vec(_desktop->selection->items().begin(), _desktop->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 (SP_IS_GRADIENT(server) && SP_GRADIENT(server)->getVector()->isSwatch()) {
+ SPGradient *vector = SP_GRADIENT(server)->getVector();
+ _psel->setSwatch(vector);
+ } else if (SP_IS_LINEARGRADIENT(server)) {
+ SPGradient *vector = SP_GRADIENT(server)->getVector();
+ SPLinearGradient *lg = SP_LINEARGRADIENT(server);
+ _psel->setGradientLinear(vector, lg, stop);
+
+ _psel->setGradientProperties(lg->getUnits(), lg->getSpread());
+ } else if (SP_IS_RADIALGRADIENT(server)) {
+ SPGradient *vector = SP_GRADIENT(server)->getVector();
+ SPRadialGradient *rg = SP_RADIALGRADIENT(server);
+ _psel->setGradientRadial(vector, rg, stop);
+
+ _psel->setGradientProperties(rg->getUnits(), rg->getSpread());
+#ifdef WITH_MESH
+ } else if (SP_IS_MESHGRADIENT(server)) {
+ SPGradient *array = SP_GRADIENT(server)->getArray();
+ _psel->setGradientMesh(SP_MESHGRADIENT(array));
+ _psel->updateMeshList(SP_MESHGRADIENT(array));
+#endif
+ } else if (SP_IS_PATTERN(server)) {
+ SPPattern *pat = SP_PATTERN(server)->rootPattern();
+ _psel->updatePatternList(pat);
+ }
+ }
+ }
+ 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;
+}
+
+/**
+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);
+
+ 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");
+ }
+
+ 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, createSwatch);
+ if (vector && createSwatch) {
+ vector->setSwatch();
+ }
+ }
+
+ for (auto item : items) {
+ // FIXME: see above
+ if (kind == FILL) {
+ sp_repr_css_change_recursive(item->getRepr(), css, "style");
+ }
+
+ if (!vector) {
+ auto gr = sp_gradient_vector_for_object(
+ document, _desktop, item,
+ (kind == FILL) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE, createSwatch);
+ if (gr && createSwatch) {
+ gr->setSwatch();
+ }
+ 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) {
+ // FIXME: see above
+ if (kind == FILL) {
+ sp_repr_css_change_recursive(item->getRepr(), css, "style");
+ }
+
+ SPGradient *gr = sp_item_set_gradient(
+ item, vector, gradient_type, (kind == FILL) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE);
+ _psel->pushAttrsToGradient(gr);
+ }
+ }
+
+ if (css) {
+ sp_repr_css_attr_unref(css);
+ css = nullptr;
+ }
+
+ 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 && SP_IS_MESHGRADIENT(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 = SP_IS_TEXT(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 = SP_IS_TEXT(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 {
+ Inkscape::XML::Node *patrepr = 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 (SP_IS_PATTERN(server) && SP_PATTERN(server)->rootPattern() == 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");
+ }
+ }
+
+ 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()) {
+ 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..16c02a3
--- /dev/null
+++ b/src/ui/widget/filter-effect-chooser.cpp
@@ -0,0 +1,203 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Filter effect selection selection widget
+ *
+ * Author:
+ * Nicholas Bishop <nicholasbishop@gmail.com>
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2007, 2017 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "filter-effect-chooser.h"
+
+#include "document.h"
+
+namespace Inkscape {
+
+const EnumData<SPBlendMode> SPBlendModeData[SP_CSS_BLEND_ENDMODE] = {
+ { SP_CSS_BLEND_NORMAL, _("Normal"), "normal" },
+ { SP_CSS_BLEND_MULTIPLY, _("Multiply"), "multiply" },
+ { SP_CSS_BLEND_SCREEN, _("Screen"), "screen" },
+ { SP_CSS_BLEND_DARKEN, _("Darken"), "darken" },
+ { SP_CSS_BLEND_LIGHTEN, _("Lighten"), "lighten" },
+ // New in Compositing and Blending Level 1
+ { SP_CSS_BLEND_OVERLAY, _("Overlay"), "overlay" },
+ { SP_CSS_BLEND_COLORDODGE, _("Color Dodge"), "color-dodge" },
+ { SP_CSS_BLEND_COLORBURN, _("Color Burn"), "color-burn" },
+ { SP_CSS_BLEND_HARDLIGHT, _("Hard Light"), "hard-light" },
+ { SP_CSS_BLEND_SOFTLIGHT, _("Soft Light"), "soft-light" },
+ { SP_CSS_BLEND_DIFFERENCE, _("Difference"), "difference" },
+ { SP_CSS_BLEND_EXCLUSION, _("Exclusion"), "exclusion" },
+ { SP_CSS_BLEND_HUE, _("Hue"), "hue" },
+ { SP_CSS_BLEND_SATURATION, _("Saturation"), "saturation" },
+ { SP_CSS_BLEND_COLOR, _("Color"), "color" },
+ { SP_CSS_BLEND_LUMINOSITY, _("Luminosity"), "luminosity" }
+};
+const EnumDataConverter<SPBlendMode> SPBlendModeConverter(SPBlendModeData, SP_CSS_BLEND_ENDMODE);
+
+
+namespace UI {
+namespace Widget {
+
+SimpleFilterModifier::SimpleFilterModifier(int flags)
+ : Gtk::Box(Gtk::ORIENTATION_VERTICAL)
+ , _flags(flags)
+ , _lb_blend(_("Blend mode:"))
+ , _lb_isolation("Isolate") // Translate for 1.1
+ , _blend(SPBlendModeConverter, SPAttr::INVALID, false)
+ , _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(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..f0b07b3
--- /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-selector-toolbar.cpp b/src/ui/widget/font-selector-toolbar.cpp
new file mode 100644
index 0000000..68c5e79
--- /dev/null
+++ b/src/ui/widget/font-selector-toolbar.cpp
@@ -0,0 +1,302 @@
+// 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 (sigc::mem_fun(*this, &FontSelectorToolbar::on_family_changed));
+ style_combo.signal_changed().connect (sigc::mem_fun(*this, &FontSelectorToolbar::on_style_changed));
+
+ show_all_children();
+
+ // Initialize font family lists. (May already be done.) Should be done on document change.
+ font_lister->update_font_list(SP_ACTIVE_DESKTOP->getDocument());
+
+ // When FontLister is changed, update family and style shown in GUI.
+ font_lister->connectUpdate(sigc::mem_fun(*this, &FontSelectorToolbar::update_font));
+}
+
+
+// Update GUI based on font-selector values.
+void
+FontSelectorToolbar::update_font ()
+{
+ if (signal_block) return;
+
+ signal_block = true;
+
+ Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+ Gtk::TreeModel::Row row;
+
+ // Set font family.
+ try {
+ row = font_lister->get_row_for_font ();
+ family_combo.set_active (row);
+ } catch (...) {
+ std::cerr << "FontSelectorToolbar::update_font: Couldn't find row for family: "
+ << font_lister->get_font_family() << std::endl;
+ }
+
+ // Set style.
+ try {
+ row = font_lister->get_row_for_style ();
+ style_combo.set_active (row);
+ } catch (...) {
+ std::cerr << "FontSelectorToolbar::update_font: Couldn't find row for style: "
+ << font_lister->get_font_style() << std::endl;
+ }
+
+ // Check for missing fonts.
+ Glib::ustring missing_fonts = get_missing_fonts();
+
+ // Add an icon to end of entry.
+ Gtk::Entry* entry = family_combo.get_entry();
+ if (missing_fonts.empty()) {
+ // If no missing fonts, add icon for selecting all objects with this font-family.
+ entry->set_icon_from_icon_name (INKSCAPE_ICON("edit-select-all"), Gtk::ENTRY_ICON_SECONDARY);
+ entry->set_icon_tooltip_text (_("Select all text with this text family"), Gtk::ENTRY_ICON_SECONDARY);
+ } else {
+ // If missing fonts, add warning icon.
+ Glib::ustring warning = _("Font not found on system: ") + missing_fonts;
+ entry->set_icon_from_icon_name (INKSCAPE_ICON("dialog-warning"), Gtk::ENTRY_ICON_SECONDARY);
+ entry->set_icon_tooltip_text (warning, Gtk::ENTRY_ICON_SECONDARY);
+ }
+
+ signal_block = false;
+}
+
+// Get comma separated list of fonts in font-family that are not on system.
+// To do, move to font-lister.
+Glib::ustring
+FontSelectorToolbar::get_missing_fonts ()
+{
+ // Get font list in text entry which may be a font stack (with fallbacks).
+ Glib::ustring font_list = family_combo.get_entry_text();
+ Glib::ustring missing_font_list;
+ Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+
+ std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("\\s*,\\s*", font_list);
+
+ for (auto token: tokens) {
+ bool found = false;
+ Gtk::TreeModel::Children children = font_lister->get_font_list()->children();
+ for (auto iter2: children) {
+ Gtk::TreeModel::Row row2 = *iter2;
+ Glib::ustring family2 = row2[font_lister->FontList.family];
+ bool onSystem2 = row2[font_lister->FontList.onSystem];
+ // CSS dictates that font family names are case insensitive.
+ // This should really implement full Unicode case unfolding.
+ if (onSystem2 && token.casefold().compare(family2.casefold()) == 0) {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ missing_font_list += token;
+ missing_font_list += ", ";
+ }
+ }
+
+ // Remove extra comma and space from end.
+ if (missing_font_list.size() >= 2) {
+ missing_font_list.resize(missing_font_list.size() - 2);
+ }
+
+ return missing_font_list;
+}
+
+
+// Callbacks
+
+// Need to update style list
+void
+FontSelectorToolbar::on_family_changed() {
+
+ if (signal_block) return;
+ signal_block = true;
+
+ Glib::ustring family = family_combo.get_entry_text();
+
+ Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance();
+ fontlister->set_font_family (family);
+
+ signal_block = false;
+
+ // Let world know
+ changed_emit();
+}
+
+void
+FontSelectorToolbar::on_style_changed() {
+
+ if (signal_block) return;
+ signal_block = true;
+
+ Glib::ustring style = style_combo.get_entry_text();
+
+ Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance();
+ fontlister->set_font_style (style);
+
+ signal_block = false;
+
+ // Let world know
+ changed_emit();
+}
+
+void
+FontSelectorToolbar::on_icon_pressed (Gtk::EntryIconPosition icon_position, const GdkEventButton* event) {
+ std::cout << "FontSelectorToolbar::on_entry_icon_pressed" << std::endl;
+ std::cout << " .... Should select all items with same font-family. FIXME" << std::endl;
+ // Call equivalent of sp_text_toolbox_select_cb() in text-toolbar.cpp
+ // Should be action! (Maybe: select_all_fontfamily( Glib::ustring font_family );).
+ // Check how Find dialog works.
+}
+
+// bool
+// FontSelectorToolbar::on_match_selected (const Gtk::TreeModel::iterator& iter)
+// {
+// std::cout << "on_match_selected" << std::endl;
+// std::cout << " FIXME" << std::endl;
+// Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+// Glib::ustring family = (*iter)[font_lister->FontList.family];
+// std::cout << " family: " << family << std::endl;
+// return false; // Leave it to default handler to set entry text.
+// }
+
+// Return focus to canvas.
+bool
+FontSelectorToolbar::on_key_press_event (GdkEventKey* key_event)
+{
+ bool consumed = false;
+
+ unsigned int key = 0;
+ gdk_keymap_translate_keyboard_state( Gdk::Display::get_default()->get_keymap(),
+ key_event->hardware_keycode,
+ (GdkModifierType)key_event->state,
+ 0, &key, nullptr, nullptr, nullptr );
+
+ switch ( key ) {
+
+ case GDK_KEY_Escape:
+ case GDK_KEY_Return:
+ case GDK_KEY_KP_Enter:
+ {
+ // Defocus
+ std::cerr << "FontSelectorToolbar::on_key_press_event: Defocus: FIXME" << std::endl;
+ consumed = true;
+ }
+ break;
+ }
+
+ return consumed; // Leave it to default handler if false.
+}
+
+void
+FontSelectorToolbar::changed_emit() {
+ signal_block = true;
+ changed_signal.emit ();
+ signal_block = false;
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 :
diff --git a/src/ui/widget/font-selector-toolbar.h b/src/ui/widget/font-selector-toolbar.h
new file mode 100644
index 0000000..53cdcea
--- /dev/null
+++ b/src/ui/widget/font-selector-toolbar.h
@@ -0,0 +1,120 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2018 Tavmong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ *
+ *
+ * The routines here create and manage a font selector widget with two parts,
+ * one each for font-family and font-style.
+ *
+ * This is essentially a toolbar version of the 'FontSelector' widget. Someday
+ * this may be merged with it.
+ *
+ * The main functions are:
+ * Create the font-selector toolbar widget.
+ * Update the lists when a new text selection is made.
+ * Update the Style list when a new font-family is selected, highlighting the
+ * best match to the original font style (as not all fonts have the same style options).
+ * Update the on-screen text.
+ * Provide the currently selected values.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_FONT_SELECTOR_TOOLBAR_H
+#define INKSCAPE_UI_WIDGET_FONT_SELECTOR_TOOLBAR_H
+
+#include <gtkmm/grid.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/comboboxtext.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A container of widgets for selecting font faces.
+ *
+ * It is used by Text tool toolbar. The FontSelectorToolbar class utilizes the
+ * FontLister class to obtain a list of font-families and their associated styles for fonts either
+ * on the system or in the document. The FontLister class is also used by the Text toolbar. Fonts
+ * are kept track of by their "fontspecs" which are the same as the strings that Pango generates.
+ *
+ * The main functions are:
+ * Create the font-selector widget.
+ * Update the child widgets when a new text selection is made.
+ * Update the Style list when a new font-family is selected, highlighting the
+ * best match to the original font style (as not all fonts have the same style options).
+ * Emit a signal when any change is made to a child widget.
+ */
+class FontSelectorToolbar : public Gtk::Grid
+{
+
+public:
+
+ /**
+ * Constructor
+ */
+ FontSelectorToolbar ();
+
+protected:
+
+ // Font family
+ Gtk::ComboBox family_combo;
+ Gtk::CellRendererText family_cell;
+
+ // Font style
+ Gtk::ComboBoxText style_combo;
+ Gtk::CellRendererText style_cell;
+
+private:
+
+ // Make a list of missing fonts for tooltip and for warning icon.
+ Glib::ustring get_missing_fonts ();
+
+ // Signal handlers
+ void on_family_changed();
+ void on_style_changed();
+ void on_icon_pressed (Gtk::EntryIconPosition icon_position, const GdkEventButton* event);
+ // bool on_match_selected (const Gtk::TreeModel::iterator& iter);
+ bool on_key_press_event (GdkEventKey* key_event) override;
+
+ // Signals
+ sigc::signal<void> changed_signal;
+ void changed_emit();
+ bool signal_block;
+
+public:
+
+ /**
+ * Update GUI based on font-selector values.
+ */
+ void update_font ();
+
+ /**
+ * Let others know that user has changed GUI settings.
+ */
+ sigc::connection connectChanged(sigc::slot<void> slot) {
+ return changed_signal.connect(slot);
+ }
+};
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_FONT_SETTINGS_TOOLBAR_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 :
diff --git a/src/ui/widget/font-selector.cpp b/src/ui/widget/font-selector.cpp
new file mode 100644
index 0000000..0617e87
--- /dev/null
+++ b/src/ui/widget/font-selector.cpp
@@ -0,0 +1,471 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2018 Tavmong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+#include <glibmm/markup.h>
+
+#include "font-selector.h"
+
+#include "libnrtype/font-lister.h"
+#include "libnrtype/font-instance.h"
+
+// For updating from selection
+#include "inkscape.h"
+#include "desktop.h"
+#include "object/sp-text.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+FontSelector::FontSelector (bool with_size, bool with_variations)
+ : Gtk::Grid ()
+ , family_frame (_("Font family"))
+ , style_frame (C_("Font selector", "Style"))
+ , size_label (_("Font size"))
+ , size_combobox (true) // With entry
+ , signal_block (false)
+ , font_size (18)
+{
+
+ Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+ Glib::RefPtr<Gtk::TreeModel> model = font_lister->get_font_list();
+ 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);
+ // Font family
+ family_treecolumn.pack_start (family_cell, false);
+ 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_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);
+ }
+
+ // Add signals
+ family_treeview.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &FontSelector::on_family_changed));
+ style_treeview.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &FontSelector::on_style_changed));
+ size_combobox.signal_changed().connect(sigc::mem_fun(*this, &FontSelector::on_size_changed));
+ font_variations.connectChanged(sigc::mem_fun(*this, &FontSelector::on_variations_changed));
+
+ show_all_children();
+
+ // Initialize font family lists. (May already be done.) Should be done on document change.
+ font_lister->update_font_list(SP_ACTIVE_DESKTOP->getDocument());
+}
+
+void
+FontSelector::set_sizes ()
+{
+ size_combobox.remove_all();
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT);
+
+ int sizes[] = {
+ 4, 6, 8, 9, 10, 11, 12, 13, 14, 16, 18, 20, 22, 24, 28,
+ 32, 36, 40, 48, 56, 64, 72, 144
+ };
+
+ // Array must be same length as SPCSSUnit in style-internal.h
+ // PX PT PC MM CM IN EM EX %
+ double ratios[] = {1, 1, 1, 10, 4, 40, 100, 16, 8, 0.16};
+
+ for (int i : sizes)
+ {
+ double size = i/ratios[unit];
+ size_combobox.append( Glib::ustring::format(size) );
+ }
+}
+
+void
+FontSelector::set_fontsize_tooltip()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT);
+ Glib::ustring tooltip = Glib::ustring::format(_("Font size"), " (", sp_style_get_css_unit_string(unit), ")");
+ size_combobox.set_tooltip_text (tooltip);
+}
+
+// Update GUI.
+// We keep a private copy of the style list as the font-family in widget is only temporary
+// until the "Apply" button is set so the style list can be different from that in
+// FontLister.
+void
+FontSelector::update_font ()
+{
+ signal_block = true;
+
+ Inkscape::FontLister *font_lister = Inkscape::FontLister::get_instance();
+ Gtk::TreePath path;
+ Glib::ustring family = font_lister->get_font_family();
+ Glib::ustring style = font_lister->get_font_style();
+
+ // Set font family
+ try {
+ path = font_lister->get_row_for_font (family);
+ } catch (...) {
+ std::cerr << "FontSelector::update_font: Couldn't find row for font-family: "
+ << family << std::endl;
+ path.clear();
+ path.push_back(0);
+ }
+
+ Gtk::TreePath currentPath;
+ Gtk::TreeViewColumn *currentColumn;
+ family_treeview.get_cursor(currentPath, currentColumn);
+ if (currentPath.empty() || !font_lister->is_path_for_font(currentPath, family)) {
+ family_treeview.set_cursor (path);
+ family_treeview.scroll_to_row (path);
+ }
+
+ // Get font-lister style list for selected family
+ Gtk::TreeModel::Row row = *(family_treeview.get_model()->get_iter (path));
+ GList *styles;
+ row.get_value(1, styles);
+
+ // Copy font-lister style list to private list store, searching for match.
+ Gtk::TreeModel::iterator match;
+ FontLister::FontStyleListClass FontStyleList;
+ Glib::RefPtr<Gtk::ListStore> local_style_list_store = Gtk::ListStore::create(FontStyleList);
+ for ( ; styles; styles = styles->next ) {
+ Gtk::TreeModel::iterator treeModelIter = local_style_list_store->append();
+ (*treeModelIter)[FontStyleList.cssStyle] = ((StyleNames *)styles->data)->CssName;
+ (*treeModelIter)[FontStyleList.displayStyle] = ((StyleNames *)styles->data)->DisplayName;
+ if (style == ((StyleNames*)styles->data)->CssName) {
+ match = treeModelIter;
+ }
+ }
+
+ // Attach store to tree view and select row.
+ style_treeview.set_model (local_style_list_store);
+ if (match) {
+ style_treeview.get_selection()->select (match);
+ }
+
+ Glib::ustring fontspec = font_lister->get_fontspec();
+ update_variations(fontspec);
+
+ signal_block = false;
+}
+
+void
+FontSelector::update_size (double size)
+{
+ signal_block = true;
+
+ // Set font size
+ std::stringstream ss;
+ ss << size;
+ size_combobox.get_entry()->set_text( ss.str() );
+ font_size = size; // Store value
+ set_fontsize_tooltip();
+
+ signal_block = false;
+}
+
+
+// If use_variations is true (default), we get variation values from variations widget otherwise we
+// get values from CSS widget (we need to be able to keep the two widgets synchronized both ways).
+Glib::ustring
+FontSelector::get_fontspec(bool use_variations) {
+
+ // Build new fontspec from GUI settings
+ Glib::ustring family = "Sans"; // Default...family list may not have been constructed.
+ Gtk::TreeModel::iterator iter = family_treeview.get_selection()->get_selected();
+ if (iter) {
+ (*iter).get_value(0, family);
+ }
+
+ Glib::ustring style = "Normal";
+ iter = style_treeview.get_selection()->get_selected();
+ if (iter) {
+ (*iter).get_value(0, style);
+ }
+
+ if (family.empty()) {
+ std::cerr << "FontSelector::get_fontspec: empty family!" << std::endl;
+ }
+
+ if (style.empty()) {
+ std::cerr << "FontSelector::get_fontspec: empty style!" << std::endl;
+ }
+
+ Glib::ustring fontspec = family + ", ";
+
+ if (use_variations) {
+ // Clip any font_variation data in 'style' as we'll replace it.
+ auto pos = style.find('@');
+ if (pos != Glib::ustring::npos) {
+ style.erase (pos, style.length()-1);
+ }
+
+ Glib::ustring variations = font_variations.get_pango_string();
+
+ if (variations.empty()) {
+ fontspec += style;
+ } else {
+ fontspec += variations;
+ }
+ } else {
+ fontspec += style;
+ }
+
+ return fontspec;
+}
+
+void
+FontSelector::style_cell_data_func (Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter)
+{
+ Glib::ustring family = "Sans"; // Default...family list may not have been constructed.
+ Gtk::TreeModel::iterator iter_family = family_treeview.get_selection()->get_selected();
+ if (iter_family) {
+ (*iter_family).get_value(0, family);
+ }
+
+ Glib::ustring style = "Normal";
+ (*iter).get_value(1, style);
+
+ Glib::ustring style_escaped = Glib::Markup::escape_text( style );
+ Glib::ustring font_desc = Glib::Markup::escape_text( family + ", " + style );
+ Glib::ustring markup;
+
+ markup = "<span font='" + font_desc + "'>" + style_escaped + "</span>";
+
+ // std::cout << " markup: " << markup << " (" << name << ")" << std::endl;
+
+ renderer->set_property("markup", markup);
+}
+
+
+// Callbacks
+
+// Need to update style list
+void
+FontSelector::on_family_changed() {
+
+ if (signal_block) return;
+ signal_block = true;
+
+ Glib::RefPtr<Gtk::TreeModel> model;
+ Gtk::TreeModel::iterator iter = family_treeview.get_selection()->get_selected(model);
+
+ if (!iter) {
+ // This can happen just after the family list is recreated.
+ signal_block = false;
+ return;
+ }
+
+ Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance();
+ fontlister->ensureRowStyles(model, iter);
+
+ Gtk::TreeModel::Row row = *iter;
+
+ // Get family name
+ Glib::ustring family;
+ row.get_value(0, family);
+
+ // Get style list (TO DO: Get rid of GList)
+ GList *styles;
+ row.get_value(1, styles);
+
+ // Find best style match for selected family with current style (e.g. of selected text).
+ Glib::ustring style = fontlister->get_font_style();
+ Glib::ustring best = fontlister->get_best_style_match (family, style);
+
+ // Create are own store of styles for selected font-family (the font-family selected
+ // in the dialog may not be the same as stored in the font-lister class until the
+ // "Apply" button is triggered).
+ Gtk::TreeModel::iterator it_best;
+ FontLister::FontStyleListClass FontStyleList;
+ Glib::RefPtr<Gtk::ListStore> local_style_list_store = Gtk::ListStore::create(FontStyleList);
+
+ // Build list and find best match.
+ for ( ; styles; styles = styles->next ) {
+ Gtk::TreeModel::iterator treeModelIter = local_style_list_store->append();
+ (*treeModelIter)[FontStyleList.cssStyle] = ((StyleNames *)styles->data)->CssName;
+ (*treeModelIter)[FontStyleList.displayStyle] = ((StyleNames *)styles->data)->DisplayName;
+ if (best == ((StyleNames*)styles->data)->CssName) {
+ it_best = treeModelIter;
+ }
+ }
+
+ // Attach store to tree view and select row.
+ style_treeview.set_model (local_style_list_store);
+ if (it_best) {
+ style_treeview.get_selection()->select (it_best);
+ }
+
+ signal_block = false;
+
+ // Let world know
+ changed_emit();
+}
+
+void
+FontSelector::on_style_changed() {
+ if (signal_block) return;
+
+ // Update variations widget if new style selected from style widget.
+ signal_block = true;
+ Glib::ustring fontspec = get_fontspec( false );
+ update_variations(fontspec);
+ signal_block = false;
+
+ // Let world know
+ changed_emit();
+}
+
+void
+FontSelector::on_size_changed() {
+
+ if (signal_block) return;
+
+ double size;
+ Glib::ustring input = size_combobox.get_active_text();
+ try {
+ size = std::stod (input);
+ }
+ catch (std::invalid_argument) {
+ std::cerr << "FontSelector::on_size_changed: Invalid input: " << input << std::endl;
+ size = -1;
+ }
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ // Arbitrary: Text and Font preview freezes with huge font sizes.
+ int max_size = prefs->getInt("/dialogs/textandfont/maxFontSize", 10000);
+
+ if (size <= 0) {
+ return;
+ }
+ if (size > max_size)
+ size = max_size;
+
+ if (fabs(font_size - size) > 0.001) {
+ font_size = size;
+ // Let world know
+ changed_emit();
+ }
+}
+
+void
+FontSelector::on_variations_changed() {
+
+ if (signal_block) return;
+
+ // Let world know
+ changed_emit();
+}
+
+void
+FontSelector::changed_emit() {
+ signal_block = true;
+ signal_changed.emit (get_fontspec());
+ 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_markup);
+ }
+ 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..e1d358e
--- /dev/null
+++ b/src/ui/widget/font-selector.h
@@ -0,0 +1,165 @@
+// 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);
+
+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);
+
+public:
+
+ /**
+ * Update GUI based on fontspec
+ */
+ void update_font ();
+ void update_size (double size);
+
+ /**
+ * Get fontspec based on current settings. (Does not handle size, yet.)
+ */
+ Glib::ustring get_fontspec(bool use_variations = true);
+
+ /**
+ * Get font size. Could be merged with fontspec.
+ */
+ double get_fontsize() { return font_size; };
+
+ /**
+ * Let others know that user has changed GUI settings.
+ * (Used to enable 'Apply' and 'Default' buttons.)
+ */
+ sigc::connection connectChanged(sigc::slot<void, Glib::ustring> slot) {
+ return signal_changed.connect(slot);
+ }
+};
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_FONT_SETTINGS_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 :
diff --git a/src/ui/widget/font-variants.cpp b/src/ui/widget/font-variants.cpp
new file mode 100644
index 0000000..3ec77c8
--- /dev/null
+++ b/src/ui/widget/font-variants.cpp
@@ -0,0 +1,1459 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2015, 2018 Tavmong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm.h>
+#include <glibmm/i18n.h>
+
+#include <libnrtype/font-instance.h>
+
+#include "font-variants.h"
+
+// For updating from selection
+#include "desktop.h"
+#include "object/sp-text.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+ // A simple class to handle UI for one feature. We could of derived this from Gtk::HBox but by
+ // attaching widgets directly to Gtk::Grid, we keep columns lined up (which may or may not be a
+ // good thing).
+ class Feature
+ {
+ public:
+ Feature( const Glib::ustring& name, OTSubstitution& glyphs, int options, Glib::ustring family, Gtk::Grid& grid, int &row, FontVariants* parent)
+ : _name (name)
+ {
+ 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.
+ font_instance* res = font_factory::Default()->FaceFromFontSpecification( font_spec.c_str() );
+ if( res ) {
+
+ std::map<Glib::ustring, OTSubstitution>::iterator it;
+
+ if((it = res->openTypeTables.find("liga"))!= res->openTypeTables.end() ||
+ (it = res->openTypeTables.find("clig"))!= res->openTypeTables.end()) {
+ _ligatures_common.set_sensitive();
+ } else {
+ _ligatures_common.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("dlig"))!= res->openTypeTables.end()) {
+ _ligatures_discretionary.set_sensitive();
+ } else {
+ _ligatures_discretionary.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("hlig"))!= res->openTypeTables.end()) {
+ _ligatures_historical.set_sensitive();
+ } else {
+ _ligatures_historical.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("calt"))!= res->openTypeTables.end()) {
+ _ligatures_contextual.set_sensitive();
+ } else {
+ _ligatures_contextual.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("subs"))!= res->openTypeTables.end()) {
+ _position_sub.set_sensitive();
+ } else {
+ _position_sub.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("sups"))!= res->openTypeTables.end()) {
+ _position_super.set_sensitive();
+ } else {
+ _position_super.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("smcp"))!= res->openTypeTables.end()) {
+ _caps_small.set_sensitive();
+ } else {
+ _caps_small.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("c2sc"))!= res->openTypeTables.end() &&
+ (it = res->openTypeTables.find("smcp"))!= res->openTypeTables.end()) {
+ _caps_all_small.set_sensitive();
+ } else {
+ _caps_all_small.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("pcap"))!= res->openTypeTables.end()) {
+ _caps_petite.set_sensitive();
+ } else {
+ _caps_petite.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("c2sc"))!= res->openTypeTables.end() &&
+ (it = res->openTypeTables.find("pcap"))!= res->openTypeTables.end()) {
+ _caps_all_petite.set_sensitive();
+ } else {
+ _caps_all_petite.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("unic"))!= res->openTypeTables.end()) {
+ _caps_unicase.set_sensitive();
+ } else {
+ _caps_unicase.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("titl"))!= res->openTypeTables.end()) {
+ _caps_titling.set_sensitive();
+ } else {
+ _caps_titling.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("lnum"))!= res->openTypeTables.end()) {
+ _numeric_lining.set_sensitive();
+ } else {
+ _numeric_lining.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("onum"))!= res->openTypeTables.end()) {
+ _numeric_old_style.set_sensitive();
+ } else {
+ _numeric_old_style.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("pnum"))!= res->openTypeTables.end()) {
+ _numeric_proportional.set_sensitive();
+ } else {
+ _numeric_proportional.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("tnum"))!= res->openTypeTables.end()) {
+ _numeric_tabular.set_sensitive();
+ } else {
+ _numeric_tabular.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("frac"))!= res->openTypeTables.end()) {
+ _numeric_diagonal.set_sensitive();
+ } else {
+ _numeric_diagonal.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("afrac"))!= res->openTypeTables.end()) {
+ _numeric_stacked.set_sensitive();
+ } else {
+ _numeric_stacked.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("ordn"))!= res->openTypeTables.end()) {
+ _numeric_ordinal.set_sensitive();
+ } else {
+ _numeric_ordinal.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("zero"))!= res->openTypeTables.end()) {
+ _numeric_slashed_zero.set_sensitive();
+ } else {
+ _numeric_slashed_zero.set_sensitive( false );
+ }
+
+ // East-Asian
+ if((it = res->openTypeTables.find("jp78"))!= res->openTypeTables.end()) {
+ _asian_jis78.set_sensitive();
+ } else {
+ _asian_jis78.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("jp83"))!= res->openTypeTables.end()) {
+ _asian_jis83.set_sensitive();
+ } else {
+ _asian_jis83.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("jp90"))!= res->openTypeTables.end()) {
+ _asian_jis90.set_sensitive();
+ } else {
+ _asian_jis90.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("jp04"))!= res->openTypeTables.end()) {
+ _asian_jis04.set_sensitive();
+ } else {
+ _asian_jis04.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("smpl"))!= res->openTypeTables.end()) {
+ _asian_simplified.set_sensitive();
+ } else {
+ _asian_simplified.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("trad"))!= res->openTypeTables.end()) {
+ _asian_traditional.set_sensitive();
+ } else {
+ _asian_traditional.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("fwid"))!= res->openTypeTables.end()) {
+ _asian_full_width.set_sensitive();
+ } else {
+ _asian_full_width.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("pwid"))!= res->openTypeTables.end()) {
+ _asian_proportional_width.set_sensitive();
+ } else {
+ _asian_proportional_width.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("ruby"))!= res->openTypeTables.end()) {
+ _asian_ruby.set_sensitive();
+ } else {
+ _asian_ruby.set_sensitive( false );
+ }
+
+ // List available ligatures
+ Glib::ustring markup_liga;
+ Glib::ustring markup_dlig;
+ Glib::ustring markup_hlig;
+ Glib::ustring markup_calt;
+
+ for (auto table: res->openTypeTables) {
+
+ if (table.first == "liga" ||
+ table.first == "clig" ||
+ table.first == "dlig" ||
+ table.first == "hgli" ||
+ table.first == "calt") {
+
+ Glib::ustring markup;
+ markup += "<span font_family='";
+ markup += sp_font_description_get_family(res->descr);
+ markup += "'>";
+ markup += Glib::Markup::escape_text(table.second.output);
+ markup += "</span>";
+
+ if (table.first == "liga") markup_liga += markup;
+ if (table.first == "clig") markup_liga += markup;
+ if (table.first == "dlig") markup_dlig += markup;
+ if (table.first == "hlig") markup_hlig += markup;
+ if (table.first == "calt") markup_calt += markup;
+ }
+ }
+
+ _ligatures_label_common.set_markup ( markup_liga.c_str() );
+ _ligatures_label_discretionary.set_markup ( markup_dlig.c_str() );
+ _ligatures_label_historical.set_markup ( markup_hlig.c_str() );
+ _ligatures_label_contextual.set_markup ( markup_calt.c_str() );
+
+ // List available numeric variants
+ Glib::ustring markup_lnum;
+ Glib::ustring markup_onum;
+ Glib::ustring markup_pnum;
+ Glib::ustring markup_tnum;
+ Glib::ustring markup_frac;
+ Glib::ustring markup_afrc;
+ Glib::ustring markup_ordn;
+ Glib::ustring markup_zero;
+
+ for (auto table: res->openTypeTables) {
+
+ Glib::ustring markup;
+ markup += "<span font_family='";
+ markup += sp_font_description_get_family(res->descr);
+ markup += "' font_features='";
+ markup += table.first;
+ markup += "'>";
+ if (table.first == "lnum" ||
+ table.first == "onum" ||
+ table.first == "pnum" ||
+ table.first == "tnum") markup += "0123456789";
+ if (table.first == "zero") markup += "0";
+ if (table.first == "ordn") markup += "[" + table.second.before + "]" + table.second.output;
+ if (table.first == "frac" ||
+ table.first == "afrc" ) markup += "1/2 2/3 3/4 4/5 5/6"; // Can we do better?
+ markup += "</span>";
+
+ if (table.first == "lnum") markup_lnum += markup;
+ if (table.first == "onum") markup_onum += markup;
+ if (table.first == "pnum") markup_pnum += markup;
+ if (table.first == "tnum") markup_tnum += markup;
+ if (table.first == "frac") markup_frac += markup;
+ if (table.first == "afrc") markup_afrc += markup;
+ if (table.first == "ordn") markup_ordn += markup;
+ if (table.first == "zero") markup_zero += markup;
+ }
+
+ _numeric_lining_label.set_markup ( markup_lnum.c_str() );
+ _numeric_old_style_label.set_markup ( markup_onum.c_str() );
+ _numeric_proportional_label.set_markup ( markup_pnum.c_str() );
+ _numeric_tabular_label.set_markup ( markup_tnum.c_str() );
+ _numeric_diagonal_label.set_markup ( markup_frac.c_str() );
+ _numeric_stacked_label.set_markup ( markup_afrc.c_str() );
+ _numeric_ordinal_label.set_markup ( markup_ordn.c_str() );
+ _numeric_slashed_zero_label.set_markup ( markup_zero.c_str() );
+
+ // Make list of tables not handled above.
+ std::map<Glib::ustring, OTSubstitution> table_copy = res->openTypeTables;
+ if( (it = table_copy.find("liga")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("clig")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("dlig")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("hlig")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("calt")) != table_copy.end() ) table_copy.erase( it );
+
+ if( (it = table_copy.find("subs")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("sups")) != table_copy.end() ) table_copy.erase( it );
+
+ if( (it = table_copy.find("smcp")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("c2sc")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("pcap")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("c2pc")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("unic")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("titl")) != table_copy.end() ) table_copy.erase( it );
+
+ if( (it = table_copy.find("lnum")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("onum")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("pnum")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("tnum")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("frac")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("afrc")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("ordn")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("zero")) != table_copy.end() ) table_copy.erase( it );
+
+ if( (it = table_copy.find("jp78")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("jp83")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("jp90")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("jp04")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("smpl")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("trad")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("fwid")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("pwid")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("ruby")) != table_copy.end() ) table_copy.erase( it );
+
+ // An incomplete list of tables that should not be exposed to the user:
+ if( (it = table_copy.find("abvf")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("abvs")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("akhn")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("blwf")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("blws")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("ccmp")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("cjct")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("dnom")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("dtls")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("fina")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("half")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("haln")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("init")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("isol")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("locl")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("medi")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("nukt")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("numr")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("pref")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("pres")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("pstf")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("psts")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("rlig")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("rkrf")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("rphf")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("rtlm")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("ssty")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("vatu")) != table_copy.end() ) table_copy.erase( it );
+
+ // Clear out old features
+ auto children = _feature_grid.get_children();
+ for (auto child: children) {
+ _feature_grid.remove (*child);
+ }
+ _features.clear();
+
+ std::string markup;
+ int grid_row = 0;
+
+ // GSUB lookup type 1 (1 to 1 mapping).
+ for (auto table: res->openTypeTables) {
+ if (table.first == "case" ||
+ table.first == "hist" ||
+ (table.first[0] == 's' && table.first[1] == 's' && !(table.first[2] == 't'))) {
+
+ if( (it = table_copy.find(table.first)) != table_copy.end() ) table_copy.erase( it );
+
+ _features[table.first] = new Feature (table.first, table.second, 2,
+ sp_font_description_get_family(res->descr),
+ _feature_grid, grid_row, this);
+ grid_row++;
+ }
+ }
+
+ // GSUB lookup type 3 (1 to many mapping). Optionally type 1.
+ for (auto table: res->openTypeTables) {
+ if (table.first == "salt" ||
+ table.first == "swsh" ||
+ table.first == "cwsh" ||
+ table.first == "ornm" ||
+ table.first == "nalt" ||
+ (table.first[0] == 'c' && table.first[1] == 'v')) {
+
+ if (table.second.input.length() == 0) {
+ // This can happen if a table is not in the 'DFLT' script and 'dflt' language.
+ // We should be using the 'lang' attribute to find the correct tables.
+ // std::cerr << "FontVariants::open_type_update: "
+ // << table.first << " has no entries!" << std::endl;
+ continue;
+ }
+
+ if( (it = table_copy.find(table.first)) != table_copy.end() ) table_copy.erase( it );
+
+ // Our lame attempt at determining number of alternative glyphs for one glyph:
+ int number = table.second.output.length() / table.second.input.length();
+ if (number < 1) {
+ number = 1; // Must have at least on/off, see comment above about 'lang' attribute.
+ // std::cout << table.first << " "
+ // << table.second.output.length() << "/"
+ // << table.second.input.length() << "="
+ // << number << std::endl;
+ }
+
+ _features[table.first] = new Feature (table.first, table.second, number+1,
+ sp_font_description_get_family(res->descr),
+ _feature_grid, grid_row, this);
+ grid_row++;
+ }
+ }
+
+ _feature_grid.show_all();
+
+ _feature_substitutions.set_markup ( markup.c_str() );
+
+ std::string ott_list = "OpenType tables not included above: ";
+ for(it = table_copy.begin(); it != table_copy.end(); ++it) {
+ ott_list += it->first;
+ ott_list += ", ";
+ }
+
+ if (table_copy.size() > 0) {
+ ott_list.pop_back();
+ ott_list.pop_back();
+ _feature_list.set_text( ott_list.c_str() );
+ } else {
+ _feature_list.set_text( "" );
+ }
+
+ } else {
+ std::cerr << "FontVariants::update(): Couldn't find font_instance for: "
+ << font_spec << std::endl;
+ }
+
+ _ligatures_changed = false;
+ _position_changed = false;
+ _caps_changed = false;
+ _numeric_changed = false;
+ _feature_changed = false;
+ }
+
+ void
+ FontVariants::fill_css( SPCSSAttr *css ) {
+
+ // Ligatures
+ bool common = _ligatures_common.get_active();
+ bool discretionary = _ligatures_discretionary.get_active();
+ bool historical = _ligatures_historical.get_active();
+ bool contextual = _ligatures_contextual.get_active();
+
+ if( !common && !discretionary && !historical && !contextual ) {
+ sp_repr_css_set_property(css, "font-variant-ligatures", "none" );
+ } else if ( common && !discretionary && !historical && contextual ) {
+ sp_repr_css_set_property(css, "font-variant-ligatures", "normal" );
+ } else {
+ Glib::ustring css_string;
+ if ( !common )
+ css_string += "no-common-ligatures ";
+ if ( discretionary )
+ css_string += "discretionary-ligatures ";
+ if ( historical )
+ css_string += "historical-ligatures ";
+ if ( !contextual )
+ css_string += "no-contextual ";
+ sp_repr_css_set_property(css, "font-variant-ligatures", css_string.c_str() );
+ }
+
+ // Position
+ {
+ unsigned position_new = SP_CSS_FONT_VARIANT_POSITION_NORMAL;
+ Glib::ustring css_string;
+ if( _position_normal.get_active() ) {
+ css_string = "normal";
+ } else if( _position_sub.get_active() ) {
+ css_string = "sub";
+ position_new = SP_CSS_FONT_VARIANT_POSITION_SUB;
+ } else if( _position_super.get_active() ) {
+ css_string = "super";
+ position_new = SP_CSS_FONT_VARIANT_POSITION_SUPER;
+ }
+
+ // 'if' may not be necessary... need to test.
+ if( (_position_all != position_new) || ((_position_mix != 0) && _position_changed) ) {
+ sp_repr_css_set_property(css, "font-variant-position", css_string.c_str() );
+ }
+ }
+
+ // Caps
+ {
+ //unsigned caps_new;
+ Glib::ustring css_string;
+ if( _caps_normal.get_active() ) {
+ css_string = "normal";
+ // caps_new = SP_CSS_FONT_VARIANT_CAPS_NORMAL;
+ } else if( _caps_small.get_active() ) {
+ css_string = "small-caps";
+ // caps_new = SP_CSS_FONT_VARIANT_CAPS_SMALL;
+ } else if( _caps_all_small.get_active() ) {
+ css_string = "all-small-caps";
+ // caps_new = SP_CSS_FONT_VARIANT_CAPS_ALL_SMALL;
+ } else if( _caps_petite.get_active() ) {
+ css_string = "petite";
+ // caps_new = SP_CSS_FONT_VARIANT_CAPS_PETITE;
+ } else if( _caps_all_petite.get_active() ) {
+ css_string = "all-petite";
+ // caps_new = SP_CSS_FONT_VARIANT_CAPS_ALL_PETITE;
+ } else if( _caps_unicase.get_active() ) {
+ css_string = "unicase";
+ // caps_new = SP_CSS_FONT_VARIANT_CAPS_UNICASE;
+ } else if( _caps_titling.get_active() ) {
+ css_string = "titling";
+ // caps_new = SP_CSS_FONT_VARIANT_CAPS_TITLING;
+ //} else {
+ // caps_new = SP_CSS_FONT_VARIANT_CAPS_NORMAL;
+ }
+
+ // May not be necessary... need to test.
+ //if( (_caps_all != caps_new) || ((_caps_mix != 0) && _caps_changed) ) {
+ sp_repr_css_set_property(css, "font-variant-caps", css_string.c_str() );
+ //}
+ }
+
+ // Numeric
+ bool default_style = _numeric_default_style.get_active();
+ bool lining = _numeric_lining.get_active();
+ bool old_style = _numeric_old_style.get_active();
+
+ bool default_width = _numeric_default_width.get_active();
+ bool proportional = _numeric_proportional.get_active();
+ bool tabular = _numeric_tabular.get_active();
+
+ bool default_fractions = _numeric_default_fractions.get_active();
+ bool diagonal = _numeric_diagonal.get_active();
+ bool stacked = _numeric_stacked.get_active();
+
+ bool ordinal = _numeric_ordinal.get_active();
+ bool slashed_zero = _numeric_slashed_zero.get_active();
+
+ if (default_style & default_width & default_fractions & !ordinal & !slashed_zero) {
+ sp_repr_css_set_property(css, "font-variant-numeric", "normal");
+ } else {
+ Glib::ustring css_string;
+ if ( lining )
+ css_string += "lining-nums ";
+ if ( old_style )
+ css_string += "oldstyle-nums ";
+ if ( proportional )
+ css_string += "proportional-nums ";
+ if ( tabular )
+ css_string += "tabular-nums ";
+ if ( diagonal )
+ css_string += "diagonal-fractions ";
+ if ( stacked )
+ css_string += "stacked-fractions ";
+ if ( ordinal )
+ css_string += "ordinal ";
+ if ( slashed_zero )
+ css_string += "slashed-zero ";
+ sp_repr_css_set_property(css, "font-variant-numeric", css_string.c_str() );
+ }
+
+ // East Asian
+ bool 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..b581aa5
--- /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..66af8e7
--- /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 "font-variations.h"
+
+// For updating from selection
+#include "desktop.h"
+#include "object/sp-text.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+FontVariationAxis::FontVariationAxis (Glib::ustring name, OTVarAxis& axis)
+ : name (name)
+{
+
+ // std::cout << "FontVariationAxis::FontVariationAxis:: "
+ // << " name: " << name
+ // << " min: " << axis.minimum
+ // << " def: " << axis.def
+ // << " max: " << axis.maximum
+ // << " val: " << axis.set_val << std::endl;
+
+ label = Gtk::manage( new Gtk::Label( name ) );
+ add( *label );
+
+ precision = 2 - int( log10(axis.maximum - axis.minimum));
+ if (precision < 0) precision = 0;
+
+ scale = Gtk::manage( new Gtk::Scale() );
+ scale->set_range (axis.minimum, axis.maximum);
+ scale->set_value (axis.set_val);
+ scale->set_digits (precision);
+ scale->set_hexpand(true);
+ add( *scale );
+
+ 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 (const Glib::ustring& font_spec) {
+
+ font_instance* res = font_factory::Default()->FaceFromFontSpecification (font_spec.c_str());
+
+ auto children = get_children();
+ for (auto child: children) {
+ remove ( *child );
+ }
+ axes.clear();
+
+ for (auto a: res->openTypeVarAxes) {
+ // std::cout << "Creating axis: " << a.first << std::endl;
+ FontVariationAxis* axis = Gtk::manage( new FontVariationAxis( a.first, a.second ));
+ axes.push_back( axis );
+ add( *axis );
+ size_group->add_widget( *(axis->get_label()) ); // Keep labels the same width
+ axis->get_scale()->signal_value_changed().connect(
+ sigc::mem_fun(*this, &FontVariations::on_variations_change)
+ );
+ }
+
+ show_all_children();
+}
+
+void
+FontVariations::fill_css( SPCSSAttr *css ) {
+
+ // Eventually will want to favor using 'font-weight', etc. but at the moment these
+ // can't handle "fractional" values. See CSS Fonts Module Level 4.
+ sp_repr_css_set_property(css, "font-variation-settings", get_css_string().c_str());
+}
+
+Glib::ustring
+FontVariations::get_css_string() {
+
+ Glib::ustring css_string;
+
+ for (auto axis: axes) {
+ Glib::ustring name = axis->get_name();
+
+ // Translate the "named" axes. (Additional names in 'stat' table, may need to handle them.)
+ if (name == "Width") name = "wdth"; // 'font-stretch'
+ if (name == "Weight") name = "wght"; // 'font-weight'
+ if (name == "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..e3c09aa
--- /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& 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/framecheck.cpp b/src/ui/widget/framecheck.cpp
new file mode 100644
index 0000000..27b3d5b
--- /dev/null
+++ b/src/ui/widget/framecheck.cpp
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <fstream>
+#include <iostream>
+#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 {
+namespace FrameCheck {
+
+std::ostream &logfile()
+{
+ static std::ofstream f;
+
+ if (!f.is_open()) {
+ try {
+ auto path = fs::temp_directory_path() / "framecheck.txt";
+ auto mode = std::ios_base::out | std::ios_base::app | std::ios_base::binary;
+ f.open(path.string(), mode);
+ } catch (...) {
+ std::cerr << "failed to create framecheck logfile" << std::endl;
+ }
+ }
+
+ return f;
+}
+
+} // namespace FrameCheck
+} // namespace Inkscape
diff --git a/src/ui/widget/framecheck.h b/src/ui/widget/framecheck.h
new file mode 100644
index 0000000..36eeea1
--- /dev/null
+++ b/src/ui/widget/framecheck.h
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Functions for logging timing events.
+ * Copyright (C) 2022 PBS <pbs3141@gmail.com>
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef FRAMECHECK_H
+#define FRAMECHECK_H
+
+#include <ostream>
+#include <glib.h>
+
+namespace Inkscape {
+namespace FrameCheck {
+
+extern std::ostream &logfile();
+
+// RAII object that logs a timing event for the duration of its lifetime.
+struct Event
+{
+ gint64 start;
+ const char *name;
+ int subtype;
+
+ Event() : start(-1) {}
+
+ Event(const char *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;
+ }
+
+ void movefrom(Event &p)
+ {
+ start = p.start;
+ name = p.name;
+ subtype = p.subtype;
+ p.start = -1;
+ }
+
+ void finish()
+ {
+ if (start != -1) {
+ logfile() << name << ' ' << start << ' ' << g_get_monotonic_time() << ' ' << subtype << '\n';
+ }
+ }
+};
+
+} // namespace FrameCheck
+} // namespace Inkscape
+
+#endif // FRAMECHECK_H
diff --git a/src/ui/widget/gradient-editor.cpp b/src/ui/widget/gradient-editor.cpp
new file mode 100644
index 0000000..df1b51b
--- /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 (SP_IS_STOP(&child)) {
+ auto stop = SP_STOP(&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..6b62164
--- /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..662082b
--- /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..e6bfa7e
--- /dev/null
+++ b/src/ui/widget/gradient-selector.cpp
@@ -0,0 +1,612 @@
+// 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) {
+ row[_columns->name] = gr_prepare_label(obj);
+ if (!new_text.empty() && new_text != row[_columns->name]) {
+ rename_id(obj, new_text);
+ Inkscape::DocumentUndo::done(obj->document, _("Rename gradient"), INKSCAPE_ICON("color-gradient"));
+ }
+ }
+ }
+ }
+}
+
+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 || SP_IS_GRADIENT(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_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 = SP_GRADIENT(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..f76949d
--- /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..ac6624c
--- /dev/null
+++ b/src/ui/widget/gradient-vector-selector.cpp
@@ -0,0 +1,327 @@
+// 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 || SP_IS_GRADIENT(gr));
+ 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) {
+ SPGradient* grad = SP_GRADIENT(gradient);
+ if ( grad->hasStops() && (grad->isSwatch() == _swatched) ) {
+ gl.push_back(SP_GRADIENT(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..6ce8edb
--- /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..6c1770c
--- /dev/null
+++ b/src/ui/widget/icon-combobox.h
@@ -0,0 +1,73 @@
+// 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>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class IconComboBox : public Gtk::ComboBox {
+public:
+ IconComboBox() {
+ _model = Gtk::ListStore::create(_columns);
+ set_model(_model);
+
+ 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);
+ }
+
+ 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;
+ }
+
+ void set_active_by_id(int id) {
+ for (auto i = _model->children().begin(); i != _model->children().end(); ++i) {
+ const int data = (*i)[_columns.id];
+ if (data == id) {
+ set_active(i);
+ 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);
+ }
+
+ Gtk::TreeModelColumn<Glib::ustring> icon_name;
+ Gtk::TreeModelColumn<Glib::ustring> label;
+ Gtk::TreeModelColumn<int> id;
+ };
+
+ Columns _columns;
+ Glib::RefPtr<Gtk::ListStore> _model;
+ 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..27c389b
--- /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/imagetoggler.cpp b/src/ui/widget/imagetoggler.cpp
new file mode 100644
index 0000000..7a0c295
--- /dev/null
+++ b/src/ui/widget/imagetoggler.cpp
@@ -0,0 +1,135 @@
+// 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_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::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);
+ }
+
+ // Hide when not being used.
+ double alpha = 1.0;
+ bool visible = _property_activatable.get_value()
+ || _property_active.get_value();
+ 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 = _property_pixbuf_on.get_value();
+ } 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..8ec406b
--- /dev/null
+++ b/src/ui/widget/imagetoggler.h
@@ -0,0 +1,92 @@
+// 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< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_on();
+ Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_off();
+
+protected:
+ void render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags ) override;
+
+ void get_preferred_width_vfunc(Gtk::Widget& widget,
+ int& min_w,
+ int& nat_w) const override;
+
+ void get_preferred_height_vfunc(Gtk::Widget& widget,
+ int& min_h,
+ int& nat_h) const override;
+
+ bool activate_vfunc(GdkEvent *event,
+ Gtk::Widget &widget,
+ const Glib::ustring &path,
+ const Gdk::Rectangle &background_area,
+ const Gdk::Rectangle &cell_area,
+ Gtk::CellRendererState flags) override;
+
+
+private:
+ int _size;
+ Glib::ustring _pixOnName;
+ Glib::ustring _pixOffName;
+
+ 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;
+
+ 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..4d6cc8c
--- /dev/null
+++ b/src/ui/widget/ink-color-wheel.cpp
@@ -0,0 +1,1507 @@
+// 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 "ink-color-wheel.h"
+
+#include <cstring>
+#include <algorithm>
+
+#include "hsluv.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;
+
+using Hsluv::Line;
+
+class ColorPoint {
+public:
+ 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(double red, double green, double blue);
+
+ double x;
+ double y;
+ double r;
+ double g;
+ double b;
+};
+
+/* FIXME: replace with Geom::Point */
+class Point {
+public:
+ Point();
+ Point(double x, double y);
+
+ double x;
+ double y;
+};
+
+class Intersection {
+public:
+ Intersection();
+ Intersection (int line1, int line2, Point intersectionPoint,
+ double intersectionPointAngle, double relativeAngle);
+
+ int line1;
+ int line2;
+ Point intersectionPoint;
+ double intersectionPointAngle;
+ double relativeAngle;
+};
+
+static Point intersect_line_line(Line a, Line b);
+static double distance_from_origin(Point point);
+static double distance_line_from_origin(Line line);
+static double angle_from_origin(Point point);
+static double normalize_angle(double angle);
+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 Point to_pixel_coordinate(Point const &point, double scale, double resize);
+static Point from_pixel_coordinate(Point const &point, double scale, double resize);
+static std::vector<Point> to_pixel_coordinate( std::vector<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 {
+
+/**
+ * Used to represent the in RGB gamut colors polygon of the color wheel.
+ *
+ * @struct
+ */
+struct PickerGeometry {
+ std::vector<Line> lines;
+ /** Ordered such that 1st vertex is intersection between first and second
+ * line, 2nd vertex between second and third line etc. */
+ std::vector<Point> vertices;
+ /** Angles from origin to corresponding vertex, in radians */
+ std::vector<double> angles;
+ /** Smallest circle with center at origin such that polygon fits inside */
+ double outerCircleRadius;
+ /** Largest circle with center at origin such that it fits inside polygon */
+ double innerCircleRadius;
+};
+
+/**
+ * Update the passed in PickerGeometry structure to the given lightness value.
+ *
+ * @param[out] pickerGeometry The PickerGeometry instance to update.
+ * @param lightness The lightness value.
+ */
+static void get_picker_geometry(PickerGeometry *pickerGeometry, double lightness);
+
+/* 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 / 360.0;
+
+ 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()
+ : _scale(1.0)
+ , _cache_width(0)
+ , _cache_height(0)
+ , _square_size(1)
+{
+ _picker_geometry = new PickerGeometry;
+ setHsluv(0.0, 100.0, 50.0);
+}
+
+ColorWheelHSLuv::~ColorWheelHSLuv()
+{
+ delete _picker_geometry;
+}
+
+void ColorWheelHSLuv::setRgb(double r, double g, double b, bool /*overrideHue*/)
+{
+ double h, s ,l;
+ Hsluv::rgb_to_hsluv(r, g, b, &h, &s, &l);
+
+ setHue(h);
+ setSaturation(s);
+ setLightness(l);
+}
+
+void ColorWheelHSLuv::getRgb(double *r, double *g, double *b) const
+{
+ Hsluv::hsluv_to_rgb(_values[0], _values[1], _values[2], r, g, b);
+}
+
+void ColorWheelHSLuv::getRgbV(double *rgb) const
+{
+ Hsluv::hsluv_to_rgb(_values[0], _values[1], _values[2], &rgb[0], &rgb[1], &rgb[2]);
+}
+
+guint32 ColorWheelHSLuv::getRgb() const
+{
+ double r, g, b;
+ Hsluv::hsluv_to_rgb(_values[0], _values[1], _values[2], &r, &g, &b);
+
+ return (
+ (static_cast<guint32>(r * 255.0) << 16) |
+ (static_cast<guint32>(g * 255.0) << 8) |
+ (static_cast<guint32>(b * 255.0) )
+ );
+}
+
+void ColorWheelHSLuv::setHsluv(double h, double s, double l)
+{
+ setHue(h);
+ setSaturation(s);
+ setLightness(l);
+}
+
+void ColorWheelHSLuv::setLightness(double l)
+{
+ _values[2] = std::clamp(l, MIN_LIGHTNESS, MAX_LIGHTNESS);
+
+ // Update polygon
+ get_picker_geometry(_picker_geometry, _values[2]);
+ _scale = OUTER_CIRCLE_RADIUS / _picker_geometry->outerCircleRadius;
+ _update_polygon();
+
+ queue_draw();
+}
+
+void ColorWheelHSLuv::getHsluv(double *h, double *s, double *l) const
+{
+ getValues(h, s, l);
+}
+
+bool ColorWheelHSLuv::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;
+
+ double const resize = std::min(width, height) / static_cast<double>(SIZE);
+
+ int const marginX = std::max(0.0, (width - height) / 2.0);
+ int const marginY = std::max(0.0, (height - width) / 2.0);
+
+ std::vector<Point> shapePointsPixel =
+ to_pixel_coordinate(_picker_geometry->vertices, _scale, resize);
+ for (Point &point : shapePointsPixel) {
+ point.x += marginX;
+ point.y += marginY;
+ }
+
+ // Detect if we're at the top or bottom vertex of the color space
+ bool is_vertex = (_values[2] < VERTEX_EPSILON || _values[2] > 100.0 - VERTEX_EPSILON);
+
+ cr->set_antialias(Cairo::ANTIALIAS_SUBPIXEL);
+
+ if (width > _square_size && height > _square_size) {
+ if (_cache_width != width || _cache_height != height) {
+ _update_polygon();
+ }
+ if (!is_vertex) {
+ // Paint with surface, clipping to polygon
+ cr->save();
+ cr->set_source(_surface_polygon, 0, 0);
+ cr->move_to(shapePointsPixel[0].x, shapePointsPixel[0].y);
+ for (size_t i = 1; i < shapePointsPixel.size(); i++) {
+ Point const &point = shapePointsPixel[i];
+ cr->line_to(point.x, point.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(cx, cy, _scale * resize * _picker_geometry->outerCircleRadius, 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(cx, cy, _scale * resize * _picker_geometry->outerCircleRadius, 0, 2 * M_PI);
+ cr->stroke();
+ cr->unset_dash();
+
+ // Contrast
+ double a = (_values[2] > 70.0) ? 0.0 : 1.0;
+ cr->set_source_rgb(a, a, a);
+
+ // Pastel circle
+ double const innerRadius = is_vertex ? 0.01 : _picker_geometry->innerCircleRadius;
+ cr->set_line_width(2);
+ cr->begin_new_path();
+ cr->arc(cx, cy, _scale * resize * innerRadius, 0, 2 * M_PI);
+ cr->stroke();
+
+ // Center
+ cr->begin_new_path();
+ cr->arc(cx, cy, 2, 0, 2 * M_PI);
+ cr->fill();
+
+ // Draw marker
+ double l, u, v;
+ Hsluv::hsluv_to_luv(_values[0], _values[1], _values[2], &l, &u, &v);
+ Point mp = to_pixel_coordinate(Point(u, v), _scale, resize);
+ mp.x += marginX;
+ mp.y += marginY;
+
+ cr->set_line_width(2);
+ cr->begin_new_path();
+ cr->arc(mp.x, mp.y, 4, 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.x-4, mp.y-4, 8, 8);
+
+ cr->set_line_width(0.5);
+ cr->set_source_rgb(1-a, 1-a, 1-a);
+ cr->begin_new_path();
+ cr->arc(mp.x, mp.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);
+
+ int const margin_x = std::max(0.0, (width - height) / 2.0);
+ int const margin_y = std::max(0.0, (height - width) / 2.0);
+
+ Point const p = from_pixel_coordinate(Point(
+ x - margin_x,
+ y - margin_y
+ ), _scale, resize);
+
+ double h, s, l;
+ Hsluv::luv_to_hsluv(_values[2], p.x, p.y, &h, &s, &l);
+
+ setHue(h);
+ setSaturation(s);
+
+ _signal_color_changed.emit();
+ queue_draw();
+}
+
+void ColorWheelHSLuv::_update_polygon()
+{
+ Gtk::Allocation allocation = get_allocation();
+ int const width = allocation.get_width();
+ int const height = allocation.get_height();
+ int const size = std::min(width, height);
+
+ // Update square size
+ _square_size = std::max(1, static_cast<int>(size/50));
+
+ if (width < _square_size || height < _square_size) {
+ return;
+ }
+
+ _cache_width = width;
+ _cache_height = height;
+
+ double const resize = size / static_cast<double>(SIZE);
+
+ int const marginX = std::max(0.0, (width - height) / 2.0);
+ int const marginY = std::max(0.0, (height - width) / 2.0);
+
+ std::vector<Point> shapePointsPixel =
+ to_pixel_coordinate(_picker_geometry->vertices, _scale, resize);
+
+ for (Point &point : shapePointsPixel) {
+ point.x += marginX;
+ point.y += marginY;
+ }
+
+ std::vector<double> xs;
+ std::vector<double> ys;
+
+ for (Point const &point : shapePointsPixel) {
+ xs.emplace_back(point.x);
+ ys.emplace_back(point.y);
+ }
+
+ int const xmin = std::floor(*std::min_element(xs.begin(), xs.end()) / _square_size);
+ int const ymin = std::floor(*std::min_element(ys.begin(), ys.end()) / _square_size);
+ int const xmax = std::ceil(*std::max_element(xs.begin(), xs.end()) / _square_size);
+ int const ymax = std::ceil(*std::max_element(ys.begin(), ys.end()) / _square_size);
+
+ int const stride =
+ Cairo::ImageSurface::format_stride_for_width(Cairo::FORMAT_RGB24, width);
+
+ _buffer_polygon.resize(height * stride / 4);
+ std::vector<guint32> buffer_line;
+ buffer_line.resize(stride / 4);
+
+ ColorPoint clr;
+
+ // Set the color of each pixel/square
+ for (int y = ymin; y < ymax; y++) {
+ for (int x = xmin; x < xmax; x++) {
+ double px = x * _square_size;
+ double py = y * _square_size;
+ Point point = from_pixel_coordinate(Point(
+ px + (_square_size / 2) - marginX,
+ py + (_square_size / 2) - marginY
+ ), _scale, resize);
+
+ double r, g ,b;
+ Hsluv::luv_to_rgb(_values[2], point.x, point.y, &r, &g, &b); // safe with _values[2] == 0
+
+ r = std::clamp(r, 0.0, 1.0);
+ g = std::clamp(g, 0.0, 1.0);
+ b = std::clamp(b, 0.0, 1.0);
+
+ clr.set_color(r, g, b);
+
+ 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 Y = y * _square_size;
+ for (int i = 0; i < _square_size; i++) {
+ guint32 *t = _buffer_polygon.data() + (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, width, height, stride
+ );
+}
+
+bool ColorWheelHSLuv::on_button_press_event(GdkEventButton* event)
+{
+ double const x = event->x;
+ double const y = event->y;
+
+ Gtk::Allocation allocation = get_allocation();
+ int const width = allocation.get_width();
+ int const height = allocation.get_height();
+
+ int const margin_x = std::max(0.0, (width - height) / 2.0);
+ int const margin_y = std::max(0.0, (height - width) / 2.0);
+ int const size = std::min(width, height);
+
+ if (x > margin_x && x < (margin_x+size) && y > margin_y && y < (margin_y+size)) {
+ _adjusting = true;
+ grab_focus();
+ _set_from_xy(x, y);
+ 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; }
+
+ double x = event->x;
+ double y = event->y;
+
+ _set_from_xy(x, 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
+ double l, u, v;
+ Hsluv::hsluv_to_luv(_values[0], _values[1], _values[2], &l, &u, &v);
+
+ double const marker_move = 1.0 / _scale;
+
+ switch (key) {
+ case GDK_KEY_Up:
+ case GDK_KEY_KP_Up:
+ v += marker_move;
+ consumed = true;
+ break;
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Down:
+ v -= marker_move;
+ consumed = true;
+ break;
+ case GDK_KEY_Left:
+ case GDK_KEY_KP_Left:
+ u -= marker_move;
+ consumed = true;
+ break;
+ case GDK_KEY_Right:
+ case GDK_KEY_KP_Right:
+ u += marker_move;
+ consumed = true;
+ break;
+ }
+
+ if (consumed) {
+ double h, s, l;
+ Hsluv::luv_to_hsluv(_values[2], u, v, &h, &s, &l);
+
+ setHue(h);
+ setSaturation(s);
+
+ _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)
+ );
+};
+
+void ColorPoint::set_color(double red, double green, double blue)
+{
+ r = red;
+ g = green;
+ b = blue;
+};
+
+/* Point */
+Point::Point() : x(0), y(0)
+{}
+
+Point::Point(double x, double y) : x(x), y(y)
+{}
+
+/* Intersection */
+Intersection::Intersection() : line1(0), line2(0)
+{}
+
+Intersection::Intersection(int line1, int line2, Point intersectionPoint,
+ double intersectionPointAngle, double relativeAngle)
+ : line1(line1)
+ , line2(line2)
+ , intersectionPoint(intersectionPoint)
+ , intersectionPointAngle(intersectionPointAngle)
+ , relativeAngle(relativeAngle)
+{}
+
+/* FIXME: replace these utility functions with calls into lib2geom */
+/* Utility functions */
+static Point intersect_line_line(Line a, Line b)
+{
+ double x = (a.intercept - b.intercept) / (b.slope - a.slope);
+ double y = a.slope * x + a.intercept;
+ return {x, y};
+}
+
+static double distance_from_origin(Point point)
+{
+ return std::sqrt(std::pow(point.x, 2) + std::pow(point.y, 2));
+}
+
+static double distance_line_from_origin(Line line)
+{
+ // https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
+ return std::abs(line.intercept) / std::sqrt(std::pow(line.slope, 2) + 1);
+}
+
+static double angle_from_origin(Point point)
+{
+ return std::atan2(point.y, point.x);
+}
+
+static double normalize_angle(double angle)
+{
+ double m = 2 * M_PI;
+ return std::fmod(std::fmod(angle, m) + m, m);
+}
+
+static double lerp(double v0, double v1, double t0, double t1, double t)
+{
+ double s = 0;
+
+ if (t0 != t1) {
+ s = (t - t0) / (t1 - t0);
+ }
+
+ return (1.0 - s) * v0 + s * 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 the vertice of the in 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 Point to_pixel_coordinate(Point const &point, double scale, double resize)
+{
+ return Point(
+ point.x * scale * resize + (SIZE * resize / 2.0),
+ (SIZE * resize / 2.0) - point.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 Point from_pixel_coordinate(Point const &point, double scale, double resize)
+{
+ return Point(
+ (point.x - (SIZE * resize / 2.0)) / (scale * resize),
+ ((SIZE * resize / 2.0) - point.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<Point> to_pixel_coordinate(std::vector<Point> const &points,
+ double scale, double resize)
+{
+ std::vector<Point> result;
+
+ for (Point 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;
+ }
+ }
+}
+
+static void Inkscape::UI::Widget::get_picker_geometry(PickerGeometry *pickerGeometry, double lightness)
+{
+ // Add a lambda to avoid overlapping intersections
+ lightness = std::clamp(lightness + 0.01, 0.1, 99.9);
+
+ // Array of lines
+ std::array<Line, 6> const lines = Hsluv::getBounds(lightness);
+ int numLines = lines.size();
+ double outerCircleRadius = 0.0;
+
+ // Find the line closest to origin
+ int closestIndex2 = -1;
+ double closestLineDistance = -1;
+
+ for (int i = 0; i < numLines; i++) {
+ double d = distance_line_from_origin(lines[i]);
+ if (closestLineDistance < 0 || d < closestLineDistance) {
+ closestLineDistance = d;
+ closestIndex2 = i;
+ }
+ }
+
+ Line closestLine = lines[closestIndex2];
+ Line perpendicularLine (0 - (1 / closestLine.slope), 0.0);
+
+ Point intersectionPoint = intersect_line_line(closestLine,
+ perpendicularLine);
+ double startingAngle = angle_from_origin(intersectionPoint);
+
+ std::vector<Intersection> intersections;
+ double intersectionPointAngle;
+ double relativeAngle;
+
+ for (int i = 0; i < numLines - 1; i++) {
+ for (int j = i + 1; j < numLines; j++) {
+ intersectionPoint = intersect_line_line(lines[i], lines[j]);
+ intersectionPointAngle = angle_from_origin(intersectionPoint);
+ relativeAngle = normalize_angle(
+ intersectionPointAngle - startingAngle);
+ intersections.emplace_back(i, j, intersectionPoint,
+ intersectionPointAngle, relativeAngle);
+ }
+ }
+
+ std::sort(intersections.begin(), intersections.end(),
+ [] (Intersection const &lhs, Intersection const &rhs)
+ {
+ return lhs.relativeAngle >= rhs.relativeAngle;
+ });
+
+ std::vector<Line> orderedLines;
+ std::vector<Point> orderedVertices;
+ std::vector<double> orderedAngles;
+
+ int nextIndex;
+ double intersectionPointDistance;
+ int currentIndex = closestIndex2;
+
+ for (Intersection intersection : intersections) {
+ nextIndex = -1;
+
+ if (intersection.line1 == currentIndex) {
+ nextIndex = intersection.line2;
+ }
+ else if (intersection.line2 == currentIndex) {
+ nextIndex = intersection.line1;
+ }
+
+ if (nextIndex > -1) {
+ currentIndex = nextIndex;
+
+ orderedLines.emplace_back(lines[nextIndex]);
+ orderedVertices.emplace_back(intersection.intersectionPoint);
+ orderedAngles.emplace_back(intersection.intersectionPointAngle);
+
+ intersectionPointDistance = distance_from_origin(intersection.intersectionPoint);
+ if (intersectionPointDistance > outerCircleRadius) {
+ outerCircleRadius = intersectionPointDistance;
+ }
+ }
+ }
+
+ pickerGeometry->lines = orderedLines;
+ pickerGeometry->vertices = orderedVertices;
+ pickerGeometry->angles = orderedAngles;
+ pickerGeometry->outerCircleRadius = outerCircleRadius;
+ pickerGeometry->innerCircleRadius = closestLineDistance;
+}
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace .0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ 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..57af896
--- /dev/null
+++ b/src/ui/widget/ink-color-wheel.h
@@ -0,0 +1,152 @@
+// 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>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+struct PickerGeometry;
+
+/**
+ * @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;
+
+ 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;
+
+protected:
+ bool on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) override;
+
+private:
+ void _set_from_xy(double const x, double const y) override;
+ void _update_polygon();
+
+ // 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;
+ PickerGeometry *_picker_geometry;
+ std::vector<guint32> _buffer_polygon;
+ Cairo::RefPtr<::Cairo::ImageSurface> _surface_polygon;
+ int _cache_width, _cache_height;
+ int _square_size;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INK_COLORWHEEL_HSLUV_H
diff --git a/src/ui/widget/ink-flow-box.cpp b/src/ui/widget/ink-flow-box.cpp
new file mode 100644
index 0000000..86eb8a0
--- /dev/null
+++ b/src/ui/widget/ink-flow-box.cpp
@@ -0,0 +1,144 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Inkflow-box widget.
+ * This widget allow pack widgets in a flowbox with a controller to show-hide
+ *
+ * Author:
+ * Jabier Arraiza <jabier.arraiza@marker.es>
+ *
+ * Copyright (C) 2018 Jabier Arraiza
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "preferences.h"
+#include "ui/icon-loader.h"
+#include "ui/widget/ink-flow-box.h"
+#include <gtkmm/adjustment.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+InkFlowBox::InkFlowBox(const gchar *name)
+: Gtk::Box(Gtk::ORIENTATION_VERTICAL)
+{
+ set_name(name);
+ this->pack_start(_controller, false, false, 0);
+ this->pack_start(_flowbox, true, true, 0);
+ _flowbox.set_activate_on_single_click(true);
+ Gtk::ToggleButton *tbutton = new Gtk::ToggleButton("", false);
+ tbutton->set_always_show_image(true);
+ _flowbox.set_selection_mode(Gtk::SelectionMode::SELECTION_NONE);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool(Glib::ustring("/dialogs/") + get_name() + Glib::ustring("/flowbox/lock"), false);
+ tbutton->set_active(prefs->getBool(Glib::ustring("/dialogs/") + get_name() + Glib::ustring("/flowbox/lock"), true));
+ Glib::ustring iconname = "object-unlocked";
+ if (tbutton->get_active()) {
+ iconname = "object-locked";
+ }
+ tbutton->set_image(*sp_get_icon_image(iconname, Gtk::ICON_SIZE_MENU));
+ tbutton->signal_toggled().connect(
+ sigc::bind<Gtk::ToggleButton *>(sigc::mem_fun(*this, &InkFlowBox::on_global_toggle), tbutton));
+ _controller.pack_start(*tbutton);
+ tbutton->hide();
+ tbutton->set_no_show_all(true);
+ showing = 0;
+ sensitive = true;
+}
+
+InkFlowBox::~InkFlowBox() = default;
+
+Glib::ustring InkFlowBox::getPrefsPath(gint pos)
+{
+ return Glib::ustring("/dialogs/") + get_name() + Glib::ustring("/flowbox/index_") + std::to_string(pos);
+}
+
+bool InkFlowBox::on_filter(Gtk::FlowBoxChild *child)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool(getPrefsPath(child->get_index()), true)) {
+ showing++;
+ return true;
+ }
+ return false;
+}
+
+void InkFlowBox::on_toggle(gint pos, Gtk::ToggleButton *tbutton)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool global = prefs->getBool(Glib::ustring("/dialogs/") + get_name() + Glib::ustring("/flowbox/lock"), true);
+ if (global && sensitive) {
+ sensitive = false;
+ bool active = true;
+ for (auto child : tbutton->get_parent()->get_children()) {
+ if (tbutton != child) {
+ static_cast<Gtk::ToggleButton *>(child)->set_active(active);
+ active = false;
+ }
+ }
+ prefs->setBool(getPrefsPath(pos), true);
+ tbutton->set_active(true);
+ sensitive = true;
+ } else {
+ prefs->setBool(getPrefsPath(pos), tbutton->get_active());
+ }
+ showing = 0;
+ _flowbox.set_filter_func(sigc::mem_fun(*this, &InkFlowBox::on_filter));
+ _flowbox.set_max_children_per_line(showing);
+}
+
+void InkFlowBox::on_global_toggle(Gtk::ToggleButton *tbutton)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool(Glib::ustring("/dialogs/") + get_name() + Glib::ustring("/flowbox/lock"), tbutton->get_active());
+ sensitive = true;
+ if (tbutton->get_active()) {
+ sensitive = false;
+ bool active = true;
+ for (auto child : tbutton->get_parent()->get_children()) {
+ if (tbutton != child) {
+ static_cast<Gtk::ToggleButton *>(child)->set_active(active);
+ active = false;
+ }
+ }
+ }
+ Glib::ustring iconname = "object-unlocked";
+ if (tbutton->get_active()) {
+ iconname = "object-locked";
+ }
+ tbutton->set_image(*sp_get_icon_image(iconname, Gtk::ICON_SIZE_MENU));
+ sensitive = true;
+}
+
+void InkFlowBox::insert(Gtk::Widget *widget, Glib::ustring label, gint pos, bool active, int minwidth)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Gtk::ToggleButton *tbutton = new Gtk::ToggleButton(label, true);
+ tbutton->set_active(prefs->getBool(getPrefsPath(pos), active));
+ tbutton->signal_toggled().connect(
+ sigc::bind<gint, Gtk::ToggleButton *>(sigc::mem_fun(*this, &InkFlowBox::on_toggle), pos, tbutton));
+ _controller.pack_start(*tbutton);
+ tbutton->show();
+ prefs->setBool(getPrefsPath(pos), prefs->getBool(getPrefsPath(pos), active));
+ widget->set_size_request(minwidth, -1);
+ _flowbox.insert(*widget, pos);
+ showing = 0;
+ _flowbox.set_filter_func(sigc::mem_fun(*this, &InkFlowBox::on_filter));
+ _flowbox.set_max_children_per_line(showing);
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/widget/ink-flow-box.h b/src/ui/widget/ink-flow-box.h
new file mode 100644
index 0000000..9db1d21
--- /dev/null
+++ b/src/ui/widget/ink-flow-box.h
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Inkflow-box widget.
+ * This widget allow pack widgets in a flowbox with a controller to show-hide
+ *
+ * Author:
+ * Jabier Arraiza <jabier.arraiza@marker.es>
+ *
+ * Copyright (C) 2018 Jabier Arraiza
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_INK_FLOW_BOX_H
+#define INKSCAPE_INK_FLOW_BOX_H
+
+#include <gtkmm/actionbar.h>
+#include <gtkmm/box.h>
+#include <gtkmm/flowbox.h>
+#include <gtkmm/flowboxchild.h>
+#include <gtkmm/togglebutton.h>
+#include <sigc++/signal.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A flowbox widget with filter controller for dialogs.
+ */
+
+class InkFlowBox : public Gtk::Box {
+ public:
+ InkFlowBox(const gchar *name);
+ ~InkFlowBox() override;
+ void insert(Gtk::Widget *widget, Glib::ustring label, gint pos, bool active, int minwidth);
+ void on_toggle(gint pos, Gtk::ToggleButton *tbutton);
+ void on_global_toggle(Gtk::ToggleButton *tbutton);
+ void set_visible(gint pos, bool visible);
+ bool on_filter(Gtk::FlowBoxChild *child);
+ Glib::ustring getPrefsPath(gint pos);
+ /**
+ * Construct a InkFlowBox.
+ */
+
+ private:
+ Gtk::FlowBox _flowbox;
+ Gtk::ActionBar _controller;
+ gint showing;
+ bool sensitive;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_INK_FLOW_BOX_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/widget/ink-ruler.cpp b/src/ui/widget/ink-ruler.cpp
new file mode 100644
index 0000000..535eddf
--- /dev/null
+++ b/src/ui/widget/ink-ruler.cpp
@@ -0,0 +1,476 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Ruler widget. Indicates horizontal or vertical position of a cursor in a specified widget.
+ *
+ * Copyright (C) 2019 Tavmjong Bah
+ *
+ * Rewrite of the 'C' ruler code which came originally from Gimp.
+ *
+ * The contents of this file may be used under the GNU General Public License Version 2 or later.
+ *
+ */
+
+#include "ink-ruler.h"
+
+#include <iostream>
+#include <cmath>
+
+#include "util/units.h"
+
+struct SPRulerMetric
+{
+ gdouble ruler_scale[16];
+ gint subdivide[5];
+};
+
+// Ruler metric for general use.
+static SPRulerMetric const ruler_metric_general = {
+ { 1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 25000, 50000, 100000 },
+ { 1, 5, 10, 50, 100 }
+};
+
+// Ruler metric for inch scales.
+static SPRulerMetric const ruler_metric_inches = {
+ { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768 },
+ { 1, 2, 4, 8, 16 }
+};
+
+// Half width of pointer triangle.
+static double half_width = 5.0;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Ruler::Ruler(Gtk::Orientation orientation)
+ : _orientation(orientation)
+ , _backing_store(nullptr)
+ , _lower(0)
+ , _upper(1000)
+ , _max_size(1000)
+ , _unit(nullptr)
+ , _backing_store_valid(false)
+ , _rect()
+ , _position(0)
+{
+ set_name("InkRuler");
+
+ set_events(Gdk::POINTER_MOTION_MASK |
+ Gdk::BUTTON_PRESS_MASK | // For guide creation
+ Gdk::BUTTON_RELEASE_MASK );
+
+ signal_motion_notify_event().connect(sigc::mem_fun(*this, &Ruler::draw_marker_callback));
+ set_no_show_all();
+}
+
+// Set display unit for ruler.
+void
+Ruler::set_unit(Inkscape::Util::Unit const *unit)
+{
+ if (_unit != unit) {
+ _unit = unit;
+
+ _backing_store_valid = false;
+ queue_draw();
+ }
+}
+
+// Set range for ruler, update ticks.
+void
+Ruler::set_range(const double& lower, const double& upper)
+{
+ if (_lower != lower || _upper != upper) {
+
+ _lower = lower;
+ _upper = upper;
+ _max_size = _upper - _lower;
+ if (_max_size == 0) {
+ _max_size = 1;
+ }
+
+ _backing_store_valid = false;
+ queue_draw();
+ }
+}
+
+// Add a widget (i.e. canvas) to monitor. Note, we don't worry about removing this signal as
+// our ruler is tied tightly to the canvas, if one is destroyed, so is the other.
+void
+Ruler::add_track_widget(Gtk::Widget& widget)
+{
+ widget.signal_motion_notify_event().connect(sigc::mem_fun(*this, &Ruler::draw_marker_callback), false); // false => connect first
+}
+
+
+// Draws marker in response to motion events from canvas. Position is defined in ruler pixel
+// coordinates. The routine assumes that the ruler is the same width (height) as the canvas. If
+// not, one could use Gtk::Widget::translate_coordinates() to convert the coordinates.
+bool
+Ruler::draw_marker_callback(GdkEventMotion* motion_event)
+{
+ double position = 0;
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ position = motion_event->x;
+ } else {
+ position = motion_event->y;
+ }
+
+ if (position != _position) {
+
+ _position = position;
+
+ // Find region to repaint (old and new marker positions).
+ Cairo::RectangleInt new_rect = marker_rect();
+ Cairo::RefPtr<Cairo::Region> region = Cairo::Region::create(new_rect);
+ region->do_union(_rect);
+
+ // Queue repaint
+ queue_draw_region(region);
+
+ _rect = new_rect;
+ }
+
+ return false;
+}
+
+
+// Find smallest dimension of ruler based on font size.
+void
+Ruler::size_request (Gtk::Requisition& requisition) const
+{
+ // Get border size
+ Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context();
+ Gtk::Border border = style_context->get_border(get_state_flags());
+
+ // Get font size
+ Pango::FontDescription font = style_context->get_font(get_state_flags());
+ int font_size = font.get_size();
+ if (!font.get_size_is_absolute()) {
+ font_size /= Pango::SCALE;
+ }
+
+ int size = 2 + font_size * 2.0; // Room for labels and ticks
+
+ int width = border.get_left() + border.get_right();
+ int height = border.get_top() + border.get_bottom();
+
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ width += 1;
+ height += size;
+ } else {
+ width += size;
+ height += 1;
+ }
+
+ // Only valid for orientation in question (smallest dimension)!
+ requisition.width = width;
+ requisition.height = height;
+}
+
+void
+Ruler::get_preferred_width_vfunc (int& minimum_width, int& natural_width) const
+{
+ Gtk::Requisition requisition;
+ size_request(requisition);
+ minimum_width = natural_width = requisition.width;
+}
+
+void
+Ruler::get_preferred_height_vfunc (int& minimum_height, int& natural_height) const
+{
+ Gtk::Requisition requisition;
+ size_request(requisition);
+ minimum_height = natural_height = requisition.height;
+}
+
+// Update backing store when scale changes.
+// Note: in principle, there should not be a border (ruler ends should match canvas ends). If there
+// is a border, we calculate tick position ignoring border width at ends of ruler but move the
+// ticks and position marker inside the border.
+bool
+Ruler::draw_scale(const::Cairo::RefPtr<::Cairo::Context>& cr_in)
+{
+
+ // Get style information
+ Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context();
+ Gtk::Border border = style_context->get_border(get_state_flags());
+ Gdk::RGBA foreground = style_context->get_color(get_state_flags());
+
+ Pango::FontDescription font = style_context->get_font(get_state_flags());
+ int font_size = font.get_size();
+ if (!font.get_size_is_absolute()) {
+ font_size /= Pango::SCALE;
+ }
+
+ Gtk::Allocation allocation = get_allocation();
+ int awidth = allocation.get_width();
+ int aheight = allocation.get_height();
+
+ // if (allocation.get_x() != 0 || allocation.get_y() != 0) {
+ // std::cerr << "Ruler::draw_scale: maybe we do have to handle allocation x and y! "
+ // << " x: " << allocation.get_x() << " y: " << allocation.get_y() << std::endl;
+ // }
+
+ // Create backing store (need surface_in to get scale factor correct).
+ Cairo::RefPtr<Cairo::Surface> surface_in = cr_in->get_target();
+ _backing_store = Cairo::Surface::create(surface_in, Cairo::CONTENT_COLOR_ALPHA, awidth, aheight);
+
+ // Get context
+ Cairo::RefPtr<::Cairo::Context> cr = ::Cairo::Context::create(_backing_store);
+ style_context->render_background(cr, 0, 0, awidth, aheight);
+
+ cr->set_line_width(1.0);
+ Gdk::Cairo::set_source_rgba(cr, foreground);
+
+ // Ruler size (only smallest dimension used later).
+ int rwidth = awidth - (border.get_left() + border.get_right());
+ int rheight = aheight - (border.get_top() + border.get_bottom());
+
+ // Draw bottom/right line of ruler
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ cr->rectangle( 0, aheight - border.get_bottom() - 1, awidth, 1);
+ } else {
+ cr->rectangle( awidth - border.get_left() - 1, 0, 1, aheight);
+ std::swap(awidth, aheight);
+ std::swap(rwidth, rheight);
+ }
+ cr->fill();
+
+ // From here on, awidth is the longest dimension of the ruler, rheight is the shortest.
+
+ // Figure out scale. Largest ticks must be far enough apart to fit largest text in vertical ruler.
+ // We actually require twice the distance.
+ unsigned int scale = std::ceil (_max_size); // Largest number
+ Glib::ustring scale_text = std::to_string(scale);
+ unsigned int digits = scale_text.length() + 1; // Add one for negative sign.
+ unsigned int minimum = digits * font_size * 2;
+
+ double pixels_per_unit = awidth/_max_size; // pixel per distance
+
+ SPRulerMetric ruler_metric = ruler_metric_general;
+ if (_unit == Inkscape::Util::unit_table.getUnit("in")) {
+ ruler_metric = ruler_metric_inches;
+ }
+
+ unsigned scale_index;
+ for (scale_index = 0; scale_index < G_N_ELEMENTS (ruler_metric.ruler_scale)-1; ++scale_index) {
+ if (ruler_metric.ruler_scale[scale_index] * std::abs (pixels_per_unit) > minimum) break;
+ }
+
+ // Now we find out what is the subdivide index for the closest ticks we can draw
+ unsigned divide_index;
+ for (divide_index = 0; divide_index < G_N_ELEMENTS (ruler_metric.subdivide)-1; ++divide_index) {
+ if (ruler_metric.ruler_scale[scale_index] * std::abs (pixels_per_unit) < 5 * ruler_metric.subdivide[divide_index+1]) break;
+ }
+
+ // We'll loop over all ticks.
+ double pixels_per_tick = pixels_per_unit *
+ ruler_metric.ruler_scale[scale_index] / ruler_metric.subdivide[divide_index];
+
+ double units_per_tick = pixels_per_tick/pixels_per_unit;
+ double ticks_per_unit = 1.0/units_per_tick;
+
+ // Find first and last ticks
+ int start = 0;
+ int end = 0;
+ if (_lower < _upper) {
+ start = std::floor (_lower * ticks_per_unit);
+ end = std::ceil (_upper * ticks_per_unit);
+ } else {
+ start = std::floor (_upper * ticks_per_unit);
+ end = std::ceil (_lower * ticks_per_unit);
+ }
+
+ // std::cout << " start: " << start
+ // << " end: " << end
+ // << " pixels_per_unit: " << pixels_per_unit
+ // << " pixels_per_tick: " << pixels_per_tick
+ // << std::endl;
+
+ // Loop over all ticks
+ for (int i = start; i < end+1; ++i) {
+
+ // Position of tick (add 0.5 to center tick on pixel).
+ double position = std::floor(i*pixels_per_tick - _lower*pixels_per_unit) + 0.5;
+
+ // Height of tick
+ int height = rheight;
+ for (int j = divide_index; j > 0; --j) {
+ if (i%ruler_metric.subdivide[j] == 0) break;
+ height = height/2 + 1;
+ }
+
+ // Draw text for major ticks.
+ if (i%ruler_metric.subdivide[divide_index] == 0) {
+
+ int label_value = std::round(i*units_per_tick);
+ Glib::ustring label = std::to_string(label_value);
+
+ Glib::RefPtr<Pango::Layout> layout = create_pango_layout("");
+ layout->set_font_description(font);
+
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ layout->set_text(label);
+ cr->move_to (position+4, border.get_top()); // Magic number offset lables
+ layout->show_in_cairo_context(cr);
+ } else {
+ cr->move_to (border.get_left(), position);
+ int n = 0;
+ for (char const &c : label) {
+ std::string s(1, c);
+ layout->set_text(s);
+ int text_width;
+ int text_height;
+ layout->get_pixel_size(text_width, text_height);
+ cr->move_to(border.get_left() + (aheight-text_width)/2.0 - 1,
+ position + n*0.8*text_height + 1);
+ layout->show_in_cairo_context(cr);
+ ++n;
+ }
+ // Glyphs are not centered in vertical text... should specify fixed width numbers.
+ // Glib::RefPtr<Pango::Context> context = layout->get_context();
+ // if (_orientation == Gtk::ORIENTATION_VERTICAL) {
+ // context->set_base_gravity(Pango::GRAVITY_EAST);
+ // context->set_gravity_hint(Pango::GRAVITY_HINT_STRONG);
+ // cr->move_to(...)
+ // cr->save();
+ // cr->rotate(M_PI_2);
+ // layout->show_in_cairo_context(cr);
+ // cr->restore();
+ // }
+ }
+ }
+
+ // Draw ticks
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ cr->move_to(position, rheight + border.get_top() - height);
+ cr->line_to(position, rheight + border.get_top());
+ } else {
+ cr->move_to(rheight + border.get_left() - height, position);
+ cr->line_to(rheight + border.get_left(), position);
+ }
+ cr->stroke();
+ }
+
+ _backing_store_valid = true;
+
+ return true;
+}
+
+// Draw position marker, we use doubles here.
+void
+Ruler::draw_marker(const Cairo::RefPtr<::Cairo::Context>& cr)
+{
+
+ // Get style information
+ Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context();
+ Gtk::Border border = style_context->get_border(get_state_flags());
+ Gdk::RGBA foreground = style_context->get_color(get_state_flags());
+
+ Gtk::Allocation allocation = get_allocation();
+ const int awidth = allocation.get_width();
+ const int aheight = allocation.get_height();
+
+ // Temp (to verify our redraw rectangle encloses position marker).
+ // Cairo::RectangleInt rect = marker_rect();
+ // cr->set_source_rgb(0, 1.0, 0);
+ // cr->rectangle (rect.x, rect.y, rect.width, rect.height);
+ // cr->fill();
+
+ Gdk::Cairo::set_source_rgba(cr, foreground);
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ double offset = aheight - border.get_bottom();
+ cr->move_to(_position, offset);
+ cr->line_to(_position - half_width, offset - half_width);
+ cr->line_to(_position + half_width, offset - half_width);
+ cr->close_path();
+ } else {
+ double offset = awidth - border.get_right();
+ cr->move_to(offset, _position);
+ cr->line_to(offset - half_width, _position - half_width);
+ cr->line_to(offset - half_width, _position + half_width);
+ cr->close_path();
+ }
+ cr->fill();
+}
+
+// This is a pixel aligned integer rectangle that encloses the position marker. Used to define the
+// redraw area.
+Cairo::RectangleInt
+Ruler::marker_rect()
+{
+ // Get border size
+ Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context();
+ Gtk::Border border = style_context->get_border(get_state_flags());
+
+ Gtk::Allocation allocation = get_allocation();
+ const int awidth = allocation.get_width();
+ const int aheight = allocation.get_height();
+
+ int rwidth = awidth - border.get_left() - border.get_right();
+ int rheight = aheight - border.get_top() - border.get_bottom();
+
+ Cairo::RectangleInt rect;
+ rect.x = 0;
+ rect.y = 0;
+ rect.width = 0;
+ rect.height = 0;
+
+ // Find size of rectangle to enclose triangle.
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ rect.x = std::floor(_position - half_width);
+ rect.y = std::floor(border.get_top() + rheight - half_width);
+ rect.width = std::ceil(half_width * 2.0 + 1);
+ rect.height = std::ceil(half_width);
+ } else {
+ rect.x = std::floor(border.get_left() + rwidth - half_width);
+ rect.y = std::floor(_position - half_width);
+ rect.width = std::ceil(half_width);
+ rect.height = std::ceil(half_width * 2.0 + 1);
+ }
+
+ return rect;
+}
+
+// Draw the ruler using the tick backing store.
+bool
+Ruler::on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) {
+
+ if (!_backing_store_valid) {
+ draw_scale (cr);
+ }
+
+ cr->set_source (_backing_store, 0, 0);
+ cr->paint();
+
+ draw_marker (cr);
+
+ return true;
+}
+
+// Update ruler on style change (font-size, etc.)
+void
+Ruler::on_style_updated() {
+
+ Gtk::DrawingArea::on_style_updated();
+
+ _backing_store_valid = false; // If font-size changed we need to regenerate store.
+
+ queue_resize();
+ queue_draw();
+}
+
+} // Namespace Inkscape
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/widget/ink-ruler.h b/src/ui/widget/ink-ruler.h
new file mode 100644
index 0000000..f34960b
--- /dev/null
+++ b/src/ui/widget/ink-ruler.h
@@ -0,0 +1,79 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Ruler widget. Indicates horizontal or vertical position of a cursor in a specified widget.
+ *
+ * Copyright (C) 2019 Tavmjong Bah
+ *
+ * The contents of this file may be used under the GNU General Public License Version 2 or later.
+ *
+ */
+
+#ifndef INK_RULER_H
+#define INK_RULER_H
+
+/* Rewrite of the C Ruler. */
+
+#include <gtkmm.h>
+
+namespace Inkscape {
+namespace Util {
+class Unit;
+}
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class Ruler : public Gtk::DrawingArea
+{
+public:
+ Ruler(Gtk::Orientation orientation);
+
+ void set_unit(Inkscape::Util::Unit const *unit);
+ void set_range(const double& lower, const double& upper);
+
+ void add_track_widget(Gtk::Widget& widget);
+ bool draw_marker_callback(GdkEventMotion* motion_event);
+
+ void size_request(Gtk::Requisition& requisition) const;
+ void get_preferred_width_vfunc( int& minimum_width, int& natural_width ) const override;
+ void get_preferred_height_vfunc(int& minimum_height, int& natural_height) const override;
+
+protected:
+ bool draw_scale(const Cairo::RefPtr<::Cairo::Context>& cr);
+ void draw_marker(const Cairo::RefPtr<::Cairo::Context>& cr);
+ Cairo::RectangleInt marker_rect();
+ bool on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) override;
+ void on_style_updated() override;
+
+private:
+ Gtk::Orientation _orientation;
+
+ Inkscape::Util::Unit const* _unit;
+ double _lower;
+ double _upper;
+ double _position;
+ double _max_size;
+
+ bool _backing_store_valid;
+
+ Cairo::RefPtr<::Cairo::Surface> _backing_store;
+ Cairo::RectangleInt _rect;
+};
+
+} // Namespace Inkscape
+}
+}
+#endif // INK_RULER_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/widget/ink-spinscale.cpp b/src/ui/widget/ink-spinscale.cpp
new file mode 100644
index 0000000..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..3b47e84
--- /dev/null
+++ b/src/ui/widget/labelled.cpp
@@ -0,0 +1,105 @@
+// 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)
+{
+ g_assert(g_utf8_validate(icon.c_str(), -1, nullptr));
+ if (icon != "") {
+ _icon = Gtk::manage(sp_get_icon_image(icon, Gtk::ICON_SIZE_LARGE_TOOLBAR));
+ pack_start(*_icon, Gtk::PACK_SHRINK);
+ }
+
+ set_spacing(6);
+ // Setting margins separately allows for more control over them
+ set_margin_start(6);
+ set_margin_end(6);
+ pack_start(*Gtk::manage(_label), Gtk::PACK_SHRINK);
+ pack_start(*Gtk::manage(_widget), Gtk::PACK_SHRINK);
+ if (mnemonic) {
+ _label->set_mnemonic_widget(*_widget);
+ }
+ widget->set_tooltip_text(tooltip);
+}
+
+
+void Labelled::setWidgetSizeRequest(int width, int height)
+{
+ if (_widget)
+ _widget->set_size_request(width, height);
+
+
+}
+
+Gtk::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..d6ed8ff
--- /dev/null
+++ b/src/ui/widget/licensor.cpp
@@ -0,0 +1,156 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * bulia byak <buliabyak@users.sf.net>
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Jon Phillips <jon@rejon.org>
+ * Ralf Stephan <ralf@ark.in-berlin.de> (Gtkmm)
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2000 - 2005 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "licensor.h"
+
+#include <gtkmm/entry.h>
+#include <gtkmm/radiobutton.h>
+
+#include "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 */
+ struct rdf_license_t * license = rdf_get_license (doc);
+
+ if (license) {
+ int i;
+ for (i=0; rdf_licenses[i].name; i++)
+ if (license == &rdf_licenses[i])
+ break;
+ static_cast<LicenseItem*>(get_children()[i+1])->set_active();
+ }
+ else {
+ static_cast<LicenseItem*>(get_children()[0])->set_active();
+ }
+
+ /* update the URI */
+ _eentry->update (doc);
+}
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/widget/licensor.h b/src/ui/widget/licensor.h
new file mode 100644
index 0000000..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..a8ea110
--- /dev/null
+++ b/src/ui/widget/marker-combo-box.cpp
@@ -0,0 +1,1033 @@
+// 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 "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"
+
+#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 = 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);
+ }
+ };
+
+ _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([=]() { set_scale(true); });
+ _scale_y.signal_value_changed().connect([=]() { 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 (_document) {
+ modified_connection.disconnect();
+ }
+}
+
+void MarkerComboBox::update_widgets_from_marker(SPMarker* marker) {
+ _input_grid.set_sensitive(marker != nullptr);
+
+ if (marker) {
+ marker->updateRepr();
+
+ _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;
+ }
+
+ _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 (SP_IS_MARKER(&child)) {
+ auto marker = SP_MARKER(&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());
+
+ // collect orphaned markers, so they are not listed; if they are listed then
+ // they disappear upon selection leaving dangling URLs
+ if (_document) _document->collectOrphans();
+
+ /*
+ * 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 = dynamic_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 = dynamic_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 (SP_IS_MARKER(&child)) {
+ auto marker = SP_MARKER(&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.push_back(item);
+ }
+ else {
+ _stock_items.push_back(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)
+{
+ // Retrieve the marker named 'mname' from the source SVG document
+ SPObject const *marker = source->getObjectById(mname);
+ if (marker == nullptr) {
+ g_warning("bad mname: %s", mname);
+ return g_bad_marker;
+ }
+
+ SPObject *oldmarker = _sandbox->getObjectById("sample");
+ if (oldmarker) {
+ oldmarker->deleteObject(false);
+ }
+
+ // Create a copy repr of the marker with id="sample"
+ Inkscape::XML::Document *xml_doc = _sandbox->getReprDoc();
+ Inkscape::XML::Node *mrepr = marker->getRepr()->duplicate(xml_doc);
+ mrepr->setAttribute("id", "sample");
+
+ // Replace the old sample in the sandbox by the new one
+ Inkscape::XML::Node *defsrepr = _sandbox->getObjectById("defs")->getRepr();
+
+ // TODO - This causes a SIGTRAP on windows
+ defsrepr->appendChild(mrepr);
+
+ Inkscape::GC::release(mrepr);
+
+ // If the marker color is a url link to a pattern or gradient copy that too
+ SPObject *mk = source->getObjectById(mname);
+ SPCSSAttr *css_marker = sp_css_attr_from_object(mk->firstChild(), SP_STYLE_FLAG_ALWAYS);
+ //const char *mfill = sp_repr_css_property(css_marker, "fill", "none");
+ const char *mstroke = sp_repr_css_property(css_marker, "fill", "none");
+
+ if (!strncmp (mstroke, "url(", 4)) {
+ SPObject *linkObj = getMarkerObj(mstroke, source);
+ if (linkObj) {
+ Inkscape::XML::Node *grepr = linkObj->getRepr()->duplicate(xml_doc);
+ SPObject *oldmarker = _sandbox->getObjectById(linkObj->getId());
+ if (oldmarker) {
+ oldmarker->deleteObject(false);
+ }
+ defsrepr->appendChild(grepr);
+ Inkscape::GC::release(grepr);
+
+ if (SP_IS_GRADIENT(linkObj)) {
+ SPGradient *vector = sp_gradient_get_forked_vector_if_necessary (SP_GRADIENT(linkObj), false);
+ if (vector) {
+ Inkscape::XML::Node *grepr = vector->getRepr()->duplicate(xml_doc);
+ SPObject *oldmarker = _sandbox->getObjectById(vector->getId());
+ if (oldmarker) {
+ oldmarker->deleteObject(false);
+ }
+ defsrepr->appendChild(grepr);
+ Inkscape::GC::release(grepr);
+ }
+ }
+ }
+ }
+
+// Uncomment this to get the sandbox documents saved (useful for debugging)
+ // FILE *fp = fopen (g_strconcat(combo_id, mname, ".svg", nullptr), "w");
+ // sp_repr_save_stream(_sandbox->getReprDoc(), fp);
+ // fclose (fp);
+
+ // object to render; note that the id is the same as that of the combo we're building
+ SPObject *object = _sandbox->getObjectById(_combo_id);
+
+ if (object == nullptr || !SP_IS_ITEM(object)) {
+ g_warning("no obj: %s", _combo_id.c_str());
+ return g_bad_marker;
+ }
+
+ auto context = get_style_context();
+ Gdk::RGBA fg = context->get_color(get_state_flags());
+ auto fgcolor = rgba_to_css_color(fg);
+ fg.set_red(1 - fg.get_red());
+ fg.set_green(1 - fg.get_green());
+ fg.set_blue(1 - fg.get_blue());
+ auto bgcolor = rgba_to_css_color(fg);
+ auto objects = _sandbox->getObjectsBySelector(".colors");
+ for (auto el : objects) {
+ if (SPCSSAttr* css = sp_repr_css_attr(el->getRepr(), "style")) {
+ sp_repr_css_set_property(css, "fill", bgcolor.c_str());
+ sp_repr_css_set_property(css, "stroke", fgcolor.c_str());
+ el->changeCSS(css, "style");
+ sp_repr_css_attr_unref(css);
+ }
+ }
+
+ auto cross = _sandbox->getObjectsBySelector(".cross");
+ double stroke = 0.5;
+ for (auto el : cross) {
+ if (SPCSSAttr* css = sp_repr_css_attr(el->getRepr(), "style")) {
+ sp_repr_css_set_property(css, "display", checkerboard ? "block" : "none");
+ sp_repr_css_set_property_double(css, "stroke-width", stroke);
+ el->changeCSS(css, "style");
+ sp_repr_css_attr_unref(css);
+ }
+ }
+
+ SPDocument::install_reference_document scoped(_sandbox.get(), marker->document);
+
+ _sandbox->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ _sandbox->ensureUpToDate();
+
+ SPItem *item = SP_ITEM(object);
+ // Find object's bbox in document
+ Geom::OptRect dbox = item->documentVisualBounds();
+
+ if (!dbox) {
+ g_warning("no dbox");
+ return g_bad_marker;
+ }
+
+ if (auto measure = dynamic_cast<SPItem*>(_sandbox->getObjectById("measure-marker"))) {
+ if (auto box = measure->documentVisualBounds()) {
+ // check size of the marker applied to a path with stroke of 1px
+ auto size = std::max(box->width(), box->height());
+ const double small = 5.0;
+ // if too small, then scale up; clip needs to be enabled for scale to work
+ if (size > 0 && size < small) {
+ auto factor = 1 + small - size;
+ scale *= factor;
+ no_clip = false;
+
+ // adjust cross stroke
+ stroke /= factor;
+ for (auto el : cross) {
+ if (SPCSSAttr* css = sp_repr_css_attr(el->getRepr(), "style")) {
+ sp_repr_css_set_property_double(css, "stroke-width", stroke);
+ el->changeCSS(css, "style");
+ sp_repr_css_attr_unref(css);
+ }
+ }
+
+ _sandbox->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ _sandbox->ensureUpToDate();
+ }
+ }
+ }
+
+ /* Update to renderable state */
+ const double device_scale = get_scale_factor();
+ auto surface = render_surface(drawing, scale, *dbox, pixel_size, device_scale, checkerboard ? &_background_color : nullptr, no_clip);
+ cairo_surface_set_device_scale(surface, device_scale, device_scale);
+ return Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(surface, false));
+}
+
+// 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();
+ }
+}
+
+/*TODO: background for custom markers
+ <filter id="softGlow" height="1.2" width="1.2" x="0.0" y="0.0">
+ <!-- <feMorphology operator="dilate" radius="1" in="SourceAlpha" result="thicken" id="feMorphology2" /> -->
+ <!-- Use a gaussian blur to create the soft blurriness of the glow -->
+ <feGaussianBlur in="SourceAlpha" stdDeviation="3" result="blurred" id="feGaussianBlur4" />
+ <!-- Change the color -->
+ <feFlood flood-color="rgb(255,255,255)" result="glowColor" id="feFlood6" flood-opacity="0.70" />
+ <!-- Color in the glows -->
+ <feComposite in="glowColor" in2="blurred" operator="in" result="softGlow_colored" id="feComposite8" />
+ <!-- Layer the effects together -->
+ <feMerge id="feMerge14">
+ <feMergeNode in="softGlow_colored" id="feMergeNode10" />
+ <feMergeNode in="SourceGraphic" id="feMergeNode12" />
+ </feMerge>
+ </filter>
+*/
+
+/**
+ * Returns a new document containing default start, mid, and end markers.
+ * Note 1: group IDs are matched against "_combo_id" to render correct preview object.
+ * Note 2: paths/lines are kept outside of groups, so they don't inflate visible bounds
+ * Note 3: invisible rects inside groups keep visual bounds from getting too small, so we can see relative marker sizes
+ */
+std::unique_ptr<SPDocument> MarkerComboBox::ink_markers_preview_doc(const Glib::ustring& group_id)
+{
+gchar const *buffer = R"A(
+ <svg xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ id="MarkerSample">
+
+ <defs id="defs">
+ <filter id="softGlow" height="1.2" width="1.2" x="0.0" y="0.0">
+ <!-- <feMorphology operator="dilate" radius="1" in="SourceAlpha" result="thicken" id="feMorphology2" /> -->
+ <!-- Use a gaussian blur to create the soft blurriness of the glow -->
+ <feGaussianBlur in="SourceAlpha" stdDeviation="3" result="blurred" id="feGaussianBlur4" />
+ <!-- Change the color -->
+ <feFlood flood-color="rgb(255,255,255)" result="glowColor" id="feFlood6" flood-opacity="0.70" />
+ <!-- Color in the glows -->
+ <feComposite in="glowColor" in2="blurred" operator="in" result="softGlow_colored" id="feComposite8" />
+ <!-- Layer the effects together -->
+ <feMerge id="feMerge14">
+ <feMergeNode in="softGlow_colored" id="feMergeNode10" />
+ <feMergeNode in="SourceGraphic" id="feMergeNode12" />
+ </feMerge>
+ </filter>
+ </defs>
+
+ <!-- cross at the end of the line to help position marker -->
+ <symbol id="cross" width="25" height="25" viewBox="0 0 25 25">
+ <path class="cross" style="mix-blend-mode:difference;stroke:#7ff;stroke-opacity:1;fill:none;display:block" d="M 0,0 M 25,25 M 10,10 15,15 M 10,15 15,10" />
+ <!-- <path class="cross" style="mix-blend-mode:difference;stroke:#7ff;stroke-width:1;stroke-opacity:1;fill:none;display:block;-inkscape-stroke:hairline" d="M 0,0 M 25,25 M 10,10 15,15 M 10,15 15,10" /> -->
+ </symbol>
+
+ <!-- very short path with 1px stroke used to measure size of marker -->
+ <path id="measure-marker" style="stroke-width:1.0;stroke-opacity:0.01;marker-start:url(#sample)" d="M 0,9999 m 0,0.1" />
+
+ <path id="line-marker-start" class="line colors" style="stroke-width:2;stroke-opacity:0.2" d="M 12.5,12.5 l 1000,0" />
+ <!-- <g id="marker-start" class="group" style="filter:url(#softGlow)"> -->
+ <g id="marker-start" class="group">
+ <path class="colors" style="stroke-width:2;stroke-opacity:0;marker-start:url(#sample)"
+ d="M 12.5,12.5 L 25,12.5"/>
+ <rect x="0" y="0" width="25" height="25" style="fill:none;stroke:none"/>
+ <use xlink:href="#cross" width="25" height="25" />
+ </g>
+
+ <path id="line-marker-mid" class="line colors" style="stroke-width:2;stroke-opacity:0.2" d="M -1000,12.5 L 1000,12.5" />
+ <g id="marker-mid" class="group">
+ <path class="colors" style="stroke-width:2;stroke-opacity:0;marker-mid:url(#sample)"
+ d="M 0,12.5 L 12.5,12.5 L 25,12.5"/>
+ <rect x="0" y="0" width="25" height="25" style="fill:none;stroke:none"/>
+ <use xlink:href="#cross" width="25" height="25" />
+ </g>
+
+ <path id="line-marker-end" class="line colors" style="stroke-width:2;stroke-opacity:0.2" d="M -1000,12.5 L 12.5,12.5" />
+ <g id="marker-end" class="group">
+ <path class="colors" style="stroke-width:2;stroke-opacity:0;marker-end:url(#sample)"
+ d="M 0,12.5 L 12.5,12.5"/>
+ <rect x="0" y="0" width="25" height="25" style="fill:none;stroke:none"/>
+ <use xlink:href="#cross" width="25" height="25" />
+ </g>
+
+ </svg>
+)A";
+
+ auto document = std::unique_ptr<SPDocument>(SPDocument::createNewDocFromMem(buffer, strlen(buffer), false));
+ // only leave requested group, so nothing else gets rendered
+ for (auto&& group : document->getObjectsByClass("group")) {
+ assert(group->getId());
+ if (group->getId() != group_id) {
+ group->deleteObject();
+ }
+ }
+ auto id = "line-" + group_id;
+ for (auto&& line : document->getObjectsByClass("line")) {
+ assert(line->getId());
+ if (line->getId() != id) {
+ line->deleteObject();
+ }
+ }
+ return document;
+}
+
+} // 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..46391a4
--- /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);
+ std::unique_ptr<SPDocument> ink_markers_preview_doc(const Glib::ustring& group_id);
+ 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;
+};
+
+} // 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..9331e71
--- /dev/null
+++ b/src/ui/widget/object-composite-settings.cpp
@@ -0,0 +1,312 @@
+// 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 (!SP_IS_ITEM(i)) {
+ continue;
+ }
+ SPItem * item = SP_ITEM(i);
+ SPStyle *style = item->style;
+ g_assert(style != nullptr);
+ bool change_blend = (item->style->mix_blend_mode.set ? item->style->mix_blend_mode.value : SP_CSS_BLEND_NORMAL) != _filter_modifier.get_blend_mode();
+ // < 1.0 filter based blend removal
+ if (!item->style->mix_blend_mode.set && item->style->filter.set && item->style->getFilter()) {
+ remove_filter_legacy_blend(item);
+ }
+ item->style->mix_blend_mode.set = TRUE;
+ if (item->style->isolation.value == SP_CSS_ISOLATION_ISOLATE) {
+ item->style->mix_blend_mode.value = SP_CSS_BLEND_NORMAL;
+ } else {
+ item->style->mix_blend_mode.value = _filter_modifier.get_blend_mode();
+ }
+
+ if (radius == 0 && item->style->filter.set && 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) { //we do blend so we need update display style
+ item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT);
+ } else {
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
+ }
+ }
+
+ DocumentUndo::maybeDone(document, _blur_tag.c_str(), _("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/page-properties.cpp b/src/ui/widget/page-properties.cpp
new file mode 100644
index 0000000..effc6b2
--- /dev/null
+++ b/src/ui/widget/page-properties.cpp
@@ -0,0 +1,515 @@
+// 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 "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))
+
+class PagePropertiesBox : public PageProperties {
+public:
+ PagePropertiesBox() :
+ _builder(create_builder("page-properties.glade")),
+ GET(_main_grid, "main-grid"),
+ GET(_left_grid, "left-grid"),
+ GET(_page_width, "page-width"),
+ GET(_page_height, "page-height"),
+ GET(_portrait, "page-portrait"),
+ GET(_landscape, "page-landscape"),
+ GET(_scale_x, "scale-x"),
+ GET(_doc_units, "user-units"),
+ GET(_unsupported_size, "unsupported"),
+ GET(_nonuniform_scale, "nonuniform-scale"),
+ GET(_viewbox_x, "viewbox-x"),
+ GET(_viewbox_y, "viewbox-y"),
+ GET(_viewbox_width, "viewbox-width"),
+ GET(_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(_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
+
+ _backgnd_color_picker = std::make_unique<ColorPicker>(
+ _("Background color"), "", 0xffffff00, true,
+ &get_widget<Gtk::Button>(_builder, "background-color"));
+
+ _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}) {
+ 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 : _("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;
+
+ 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;
+ Gtk::SpinButton& _page_width;
+ Gtk::SpinButton& _page_height;
+ Gtk::RadioButton& _portrait;
+ Gtk::RadioButton& _landscape;
+ Gtk::SpinButton& _scale_x;
+ Gtk::Label& _unsupported_size;
+ Gtk::Label& _nonuniform_scale;
+ Gtk::Label& _doc_units;
+ Gtk::SpinButton& _viewbox_x;
+ Gtk::SpinButton& _viewbox_y;
+ Gtk::SpinButton& _viewbox_width;
+ Gtk::SpinButton& _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::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..37779e5
--- /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 };
+ 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..baa258b
--- /dev/null
+++ b/src/ui/widget/page-selector.cpp
@@ -0,0 +1,196 @@
+// 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)
+{
+ _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);
+ return;
+ }
+ }
+ }
+}
+
+/**
+ * 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..40e2ea5
--- /dev/null
+++ b/src/ui/widget/page-size-preview.cpp
@@ -0,0 +1,185 @@
+// 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..ed67d46
--- /dev/null
+++ b/src/ui/widget/paint-selector.cpp
@@ -0,0 +1,1476 @@
+// 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 "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/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));
+ /*
+ guint32 rgba = 0;
+
+ if ( sp_color_get_colorspace_type(color) == SP_COLORSPACE_TYPE_CMYK )
+ {
+ #ifdef SP_PS_VERBOSE
+ g_print("PaintSelector set CMYKA\n");
+ #endif
+ sp_paint_selector_set_mode(psel, MODE_COLOR_CMYK);
+ }
+ else
+ */
+ {
+#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;
+ // rgba = color.toRGBA32( alpha );
+}
+
+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 (SP_IS_MESHGRADIENT(meshe) && SP_GRADIENT(meshe) == SP_GRADIENT(meshe)->getArray()) { // only if this is a
+ // root mesh
+ pl.push_back(SP_MESHGRADIENT(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 && SP_IS_MESHGRADIENT(mesh_obj)) {
+ mesh = SP_MESHGRADIENT(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(); }
+
+
+/**
+ * Returns a list of patterns in the defs of the given source document as a vector
+ */
+static std::vector<SPPattern *> ink_pattern_list_get(SPDocument *source)
+{
+ std::vector<SPPattern *> pl;
+ if (source == nullptr)
+ return pl;
+
+ std::vector<SPObject *> patterns = source->getResourceList("pattern");
+ for (auto pattern : patterns) {
+ if (SP_PATTERN(pattern) == SP_PATTERN(pattern)->rootPattern()) { // only if this is a root pattern
+ pl.push_back(SP_PATTERN(pattern));
+ }
+ }
+
+ return pl;
+}
+
+/**
+ * Adds menu items for pattern list - derived from marker code, left hb etc in to make addition of previews easier at
+ * some point.
+ */
+static void sp_pattern_menu_build(GtkWidget *combo, std::vector<SPPattern *> &pl, SPDocument * /*source*/)
+{
+ GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo)));
+ GtkTreeIter iter;
+
+ for (auto i = pl.rbegin(); i != pl.rend(); ++i) {
+
+ Inkscape::XML::Node *repr = (*i)->getRepr();
+
+ // label for combobox
+ gchar const *label;
+ if (repr->attribute("inkscape:stockid")) {
+ label = _(repr->attribute("inkscape:stockid"));
+ } else {
+ label = _(repr->attribute("id"));
+ }
+
+ gchar const *patid = repr->attribute("id");
+
+ gboolean stockid = false;
+ if (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_PATTERN, patid,
+ COMBO_COL_SEP, FALSE, -1);
+ }
+}
+
+/**
+ * Pick up all patterns from source, except those that are in
+ * current_doc (if non-NULL), and add items to the pattern menu.
+ */
+static void sp_pattern_list_from_doc(GtkWidget *combo, SPDocument * /*current_doc*/, SPDocument *source,
+ SPDocument * /*pattern_doc*/)
+{
+ std::vector<SPPattern *> pl = ink_pattern_list_get(source);
+ sp_pattern_menu_build(combo, pl, source);
+}
+
+
+static void ink_pattern_menu_populate_menu(GtkWidget *combo, SPDocument *doc)
+{
+ static SPDocument *patterns_doc = nullptr;
+
+ // find and load patterns.svg
+ if (patterns_doc == nullptr) {
+ using namespace Inkscape::IO::Resource;
+ auto patterns_source = get_path_string(SYSTEM, PAINT, "patterns.svg");
+ if (Glib::file_test(patterns_source, Glib::FILE_TEST_IS_REGULAR)) {
+ patterns_doc = SPDocument::createNewDoc(patterns_source.c_str(), false);
+ }
+ }
+
+ // suck in from current doc
+ sp_pattern_list_from_doc(combo, nullptr, doc, patterns_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_PATTERN, "",
+ COMBO_COL_SEP, true, -1);
+ }
+
+ // suck in from patterns.svg
+ if (patterns_doc) {
+ doc->ensureUpToDate();
+ sp_pattern_list_from_doc(combo, doc, patterns_doc, nullptr);
+ }
+}
+
+
+static GtkWidget *ink_pattern_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_PATTERN, "", COMBO_COL_SEP, false, -1);
+ gtk_widget_set_sensitive(combo, FALSE);
+
+ } else {
+
+ ink_pattern_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 pattern list*/
+void PaintSelector::updatePatternList(SPPattern *pattern)
+{
+ if (_update) {
+ return;
+ }
+ g_assert(_patternmenu != nullptr);
+
+ /* Clear existing menu if any */
+ auto store = gtk_combo_box_get_model(GTK_COMBO_BOX(_patternmenu));
+ gtk_list_store_clear(GTK_LIST_STORE(store));
+
+ ink_pattern_menu(_patternmenu);
+
+ /* Set history */
+
+ if (pattern && !_patternmenu_update) {
+ _patternmenu_update = true;
+ gchar const *patname = pattern->getRepr()->attribute("id");
+
+ // Find this pattern and set it active in the combo_box
+ GtkTreeIter iter;
+ gchar *patid = nullptr;
+ bool valid = gtk_tree_model_get_iter_first(store, &iter);
+ if (!valid) {
+ return;
+ }
+ gtk_tree_model_get(store, &iter, COMBO_COL_PATTERN, &patid, -1);
+ while (valid && strcmp(patid, patname) != 0) {
+ valid = gtk_tree_model_iter_next(store, &iter);
+ g_free(patid);
+ patid = nullptr;
+ gtk_tree_model_get(store, &iter, COMBO_COL_PATTERN, &patid, -1);
+ }
+ g_free(patid);
+
+ if (valid) {
+ gtk_combo_box_set_active_iter(GTK_COMBO_BOX(_patternmenu), &iter);
+ }
+
+ _patternmenu_update = false;
+ }
+}
+
+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) {
+ /* Create vbox */
+ _selector_pattern = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4));
+ _selector_pattern->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 pattern, 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);
+ _patternmenu = _scrollprotected_combo_box_new_with_model(GTK_TREE_MODEL(store));
+ gtk_combo_box_set_row_separator_func(GTK_COMBO_BOX(_patternmenu), 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(_patternmenu), renderer, TRUE);
+ gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(_patternmenu), renderer, "text", COMBO_COL_LABEL, nullptr);
+
+ ink_pattern_menu(_patternmenu);
+ g_signal_connect(G_OBJECT(_patternmenu), "changed", G_CALLBACK(PaintSelector::pattern_change), this);
+ g_signal_connect(G_OBJECT(_patternmenu), "destroy", G_CALLBACK(PaintSelector::pattern_destroy), this);
+ g_object_ref(G_OBJECT(_patternmenu));
+
+ gtk_container_add(GTK_CONTAINER(hb->gobj()), _patternmenu);
+ _selector_pattern->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>Node tool</b> to adjust position, scale, and rotation of the pattern on canvas. Use "
+ "<b>Object &gt; Pattern &gt; Objects to Pattern</b> to create a new pattern from selection."));
+ l->set_line_wrap(true);
+ l->set_size_request(180, -1);
+ hb2->pack_start(*l, true, true, AUX_BETWEEN_BUTTON_GROUPS);
+ _selector_pattern->pack_start(*hb2, false, false, AUX_BETWEEN_BUTTON_GROUPS);
+ _selector_pattern->show_all();
+ _frame->add(*_selector_pattern);
+ }
+
+ _selector_pattern->show();
+ _label->set_markup(_("<b>Pattern fill</b>"));
+ }
+#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;
+}
+
+SPPattern *PaintSelector::getPattern()
+{
+ SPPattern *pat = nullptr;
+ g_return_val_if_fail(_mode == MODE_PATTERN, nullptr);
+
+ /* no pattern menu if we were just selected */
+ if (!_patternmenu) {
+ return nullptr;
+ }
+
+ auto store = gtk_combo_box_get_model(GTK_COMBO_BOX(_patternmenu));
+
+ /* Get the selected pattern */
+ GtkTreeIter iter;
+ if (!gtk_combo_box_get_active_iter(GTK_COMBO_BOX(_patternmenu), &iter) ||
+ !gtk_list_store_iter_is_valid(GTK_LIST_STORE(store), &iter)) {
+ return nullptr;
+ }
+
+ gchar *patid = nullptr;
+ gboolean stockid = FALSE;
+ // gchar *label = nullptr;
+ gtk_tree_model_get(store, &iter,
+ // COMBO_COL_LABEL, &label,
+ COMBO_COL_STOCK, &stockid, COMBO_COL_PATTERN, &patid, -1);
+ // g_free(label);
+ if (patid == nullptr) {
+ return nullptr;
+ }
+
+ if (strcmp(patid, "none") != 0) {
+ gchar *paturn;
+
+ if (stockid) {
+ paturn = g_strconcat("urn:inkscape:pattern:", patid, nullptr);
+ } else {
+ paturn = g_strdup(patid);
+ }
+ SPObject *pat_obj = get_stock_item(paturn);
+ if (pat_obj) {
+ pat = SP_PATTERN(pat_obj);
+ }
+ g_free(paturn);
+ } else {
+ SPDocument *doc = SP_ACTIVE_DOCUMENT;
+ SPObject *pat_obj = doc->getObjectById(patid);
+
+ if (pat_obj && SP_IS_PATTERN(pat_obj)) {
+ pat = SP_PATTERN(pat_obj)->rootPattern();
+ }
+ }
+
+ g_free(patid);
+
+ return pat;
+}
+
+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(),
+ (SP_IS_GRADIENT(server) ? "Y" : "n"),
+ (SP_IS_GRADIENT(server) && SP_GRADIENT(server)->getVector()->isSwatch() ? "Y" : "n"));
+#endif // SP_PS_VERBOSE
+
+
+ if (server && SP_IS_GRADIENT(server) && SP_GRADIENT(server)->getVector()->isSwatch()) {
+ mode = MODE_SWATCH;
+ } else if (SP_IS_LINEARGRADIENT(server)) {
+ mode = MODE_GRADIENT_LINEAR;
+ } else if (SP_IS_RADIALGRADIENT(server)) {
+ mode = MODE_GRADIENT_RADIAL;
+#ifdef WITH_MESH
+ } else if (SP_IS_MESHGRADIENT(server)) {
+ mode = MODE_GRADIENT_MESH;
+#endif
+ } else if (SP_IS_PATTERN(server)) {
+ mode = MODE_PATTERN;
+ } else if (SP_IS_HATCH(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..ad069d8
--- /dev/null
+++ b/src/ui/widget/paint-selector.h
@@ -0,0 +1,226 @@
+// 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 "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;
+
+/**
+ * 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;
+ Gtk::Box *_selector_pattern = nullptr;
+ SwatchSelector *_selector_swatch = 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;
+
+ 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; }
+
+ 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();
+};
+
+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/point.cpp b/src/ui/widget/point.cpp
new file mode 100644
index 0000000..5bc50ab
--- /dev/null
+++ b/src/ui/widget/point.cpp
@@ -0,0 +1,180 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Johan Engelen <j.b.c.engelen@utwente.nl>
+ * Carl Hetherington <inkscape@carlh.net>
+ * Derek P. Moore <derekm@hackunix.org>
+ * Bryce Harrington <bryce@bryceharrington.org>
+ *
+ * Copyright (C) 2007 Authors
+ * Copyright (C) 2004 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/widget/point.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Point::Point(Glib::ustring const &label, Glib::ustring const &tooltip,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Labelled(label, tooltip, new Gtk::Box(Gtk::ORIENTATION_VERTICAL), suffix, icon, mnemonic),
+ xwidget("X:",""),
+ ywidget("Y:","")
+{
+ 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)
+{
+ 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)
+{
+ 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..e19ebe3
--- /dev/null
+++ b/src/ui/widget/preferences-widget.cpp
@@ -0,0 +1,1118 @@
+// 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();
+ 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());
+ Gdk::RGBA bg;
+ bg.set_grey(0.5);
+ if (auto wnd = dynamic_cast<Gtk::Window*>(this->get_toplevel())) {
+ auto sc = wnd->get_style_context();
+ bg = get_background_color(sc);
+ }
+
+ cr->set_source_rgb(bg.get_red(), bg.get_green(), bg.get_blue());
+ cr->set_fill_rule(Cairo::FILL_RULE_WINDING);
+ cr->rectangle(0, 0, w, _height + _border*2);
+ cr->fill();
+
+ cr->set_source_rgb(0.0, 0.0, 0.0);
+ cr->set_line_width(0.5);
+
+ cr->translate(_border, _border); // so that we have a small white border around the ruler
+ cr->move_to (0, _height);
+ cr->line_to (_drawing_width, _height);
+
+ cr->set_source_rgb(fg.get_red(), fg.get_green(), fg.get_blue());
+
+ 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 labels[], int values[], int num_items, int default_value)
+{
+ _prefs_path = prefs_path;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int row = 0;
+ int value = prefs->getInt(_prefs_path, default_value);
+
+ for (int i = 0 ; i < num_items; ++i)
+ {
+ this->append(labels[i]);
+ _values.push_back(values[i]);
+ if (value == values[i])
+ row = i;
+ }
+ this->set_active(row);
+}
+
+void PrefCombo::init(Glib::ustring const &prefs_path,
+ Glib::ustring labels[], Glib::ustring values[], int num_items, Glib::ustring default_value)
+{
+ _prefs_path = prefs_path;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int row = 0;
+ Glib::ustring value = prefs->getString(_prefs_path);
+ if(value.empty())
+ {
+ value = default_value;
+ }
+
+ for (int i = 0 ; i < num_items; ++i)
+ {
+ this->append(labels[i]);
+ _ustr_values.push_back(values[i]);
+ if (value == values[i])
+ row = i;
+ }
+ this->set_active(row);
+}
+
+void PrefCombo::init(Glib::ustring const &prefs_path, std::vector<Glib::ustring> labels, std::vector<int> values,
+ int default_value)
+{
+ size_t labels_size = labels.size();
+ size_t values_size = values.size();
+ if (values_size != labels_size) {
+ std::cout << "PrefCombo::"
+ << "Different number of values/labels in " << prefs_path << std::endl;
+ return;
+ }
+ _prefs_path = prefs_path;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int row = 0;
+ int value = prefs->getInt(_prefs_path, default_value);
+
+ for (int i = 0; i < labels_size; ++i) {
+ this->append(labels[i]);
+ _values.push_back(values[i]);
+ if (value == values[i])
+ row = i;
+ }
+ this->set_active(row);
+}
+
+void PrefCombo::init(Glib::ustring const &prefs_path, std::vector<Glib::ustring> labels,
+ std::vector<Glib::ustring> values, Glib::ustring default_value)
+{
+ size_t labels_size = labels.size();
+ size_t values_size = values.size();
+ if (values_size != labels_size) {
+ std::cout << "PrefCombo::"
+ << "Different number of values/labels in " << prefs_path << std::endl;
+ return;
+ }
+ _prefs_path = prefs_path;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int row = 0;
+ Glib::ustring value = prefs->getString(_prefs_path);
+ if (value.empty()) {
+ value = default_value;
+ }
+
+ for (int i = 0; i < labels_size; ++i) {
+ this->append(labels[i]);
+ _ustr_values.push_back(values[i]);
+ if (value == values[i])
+ row = i;
+ }
+ this->set_active(row);
+}
+
+void PrefCombo::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 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..07fee61
--- /dev/null
+++ b/src/ui/widget/preferences-widget.h
@@ -0,0 +1,345 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Widgets for Inkscape Preferences dialog.
+ */
+/*
+ * Authors:
+ * Marco Scholten
+ * Bruno Dilly <bruno.dilly@gmail.com>
+ *
+ * Copyright (C) 2004, 2006, 2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_INKSCAPE_PREFERENCES_H
+#define INKSCAPE_UI_WIDGET_INKSCAPE_PREFERENCES_H
+
+#include <iostream>
+#include <vector>
+
+#include <gtkmm/filechooserbutton.h>
+#include "ui/widget/spinbutton.h"
+#include <cstddef>
+#include <sigc++/sigc++.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/textview.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/drawingarea.h>
+#include <gtkmm/grid.h>
+
+#include "ui/widget/color-picker.h"
+#include "ui/widget/unit-menu.h"
+#include "ui/widget/spinbutton.h"
+#include "ui/widget/scalar-unit.h"
+
+namespace Gtk {
+class Scale;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class PrefCheckButton : public Gtk::CheckButton
+{
+public:
+ void init(Glib::ustring const &label, Glib::ustring const &prefs_path,
+ bool default_value);
+ sigc::signal<void, bool> changed_signal;
+protected:
+ Glib::ustring _prefs_path;
+ void on_toggled() override;
+};
+
+class PrefRadioButton : public Gtk::RadioButton
+{
+public:
+ void init(Glib::ustring const &label, Glib::ustring const &prefs_path,
+ int int_value, bool default_value, PrefRadioButton* group_member);
+ void init(Glib::ustring const &label, Glib::ustring const &prefs_path,
+ Glib::ustring const &string_value, bool default_value, PrefRadioButton* group_member);
+ sigc::signal<void, bool> changed_signal;
+protected:
+ Glib::ustring _prefs_path;
+ Glib::ustring _string_value;
+ int _value_type;
+ enum
+ {
+ VAL_INT,
+ VAL_STRING
+ };
+ int _int_value;
+ void on_toggled() override;
+};
+
+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 labels[], int values[], int num_items, int default_value);
+
+ /**
+ * Initialize a combo box.
+ * second form uses strings as key values.
+ */
+ void init(Glib::ustring const &prefs_path,
+ Glib::ustring labels[], Glib::ustring values[], int num_items, Glib::ustring default_value);
+ /**
+ * Initialize a combo box.
+ * with vectors.
+ */
+ void init(Glib::ustring const &prefs_path, std::vector<Glib::ustring> labels, std::vector<int> values,
+ int default_value);
+
+ void init(Glib::ustring const &prefs_path, std::vector<Glib::ustring> labels, std::vector<Glib::ustring> values,
+ Glib::ustring default_value);
+
+ /**
+ * 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 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/preview.cpp b/src/ui/widget/preview.cpp
new file mode 100644
index 0000000..663d4b8
--- /dev/null
+++ b/src/ui/widget/preview.cpp
@@ -0,0 +1,511 @@
+// SPDX-License-Identifier: GPL-2.0-or-later OR MPL-1.1 OR LGPL-2.1-or-later
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Eek Preview Stuffs.
+ *
+ * The Initial Developer of the Original Code is
+ * Jon A. Cruz.
+ * Portions created by the Initial Developer are Copyright (C) 2005
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#include <algorithm>
+#include <gdkmm/general.h>
+#include "preview.h"
+#include "preferences.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+#define PRIME_BUTTON_MAGIC_NUMBER 1
+
+/* Keep in sync with last value in eek-preview.h */
+#define PREVIEW_SIZE_LAST PREVIEW_SIZE_HUGE
+#define PREVIEW_SIZE_NEXTFREE (PREVIEW_SIZE_HUGE + 1)
+
+#define PREVIEW_MAX_RATIO 500
+
+void
+Preview::set_color(int r, int g, int b )
+{
+ _r = r;
+ _g = g;
+ _b = b;
+
+ queue_draw();
+}
+
+
+void
+Preview::set_pixbuf(const Glib::RefPtr<Gdk::Pixbuf> &pixbuf)
+{
+ _previewPixbuf = pixbuf;
+
+ queue_draw();
+
+ if (_scaled)
+ {
+ _scaled.reset();
+ }
+
+ _scaledW = _previewPixbuf->get_width();
+ _scaledH = _previewPixbuf->get_height();
+}
+
+static gboolean setupDone = FALSE;
+static GtkRequisition sizeThings[PREVIEW_SIZE_NEXTFREE];
+
+void
+Preview::set_size_mappings( guint count, GtkIconSize const* sizes )
+{
+ gint width = 0;
+ gint height = 0;
+ gint smallest = 512;
+ gint largest = 0;
+ guint i = 0;
+ guint delta = 0;
+
+ for ( i = 0; i < count; ++i ) {
+ gboolean worked = gtk_icon_size_lookup( sizes[i], &width, &height );
+ if ( worked ) {
+ if ( width < smallest ) {
+ smallest = width;
+ }
+ if ( width > largest ) {
+ largest = width;
+ }
+ }
+ }
+
+ smallest = (smallest * 3) / 4;
+
+ delta = largest - smallest;
+
+ for ( i = 0; i < G_N_ELEMENTS(sizeThings); ++i ) {
+ guint val = smallest + ( (i * delta) / (G_N_ELEMENTS(sizeThings) - 1) );
+ sizeThings[i].width = val;
+ sizeThings[i].height = val;
+ }
+
+ setupDone = TRUE;
+}
+
+void Preview::set_freesize(bool en) {
+ _freesize = en;
+}
+
+void
+Preview::size_request(GtkRequisition* req) const
+{
+ if (_freesize) {
+ req->width = req->height = 1;
+ return;
+ }
+
+ int width = 0;
+ int height = 0;
+
+ if ( !setupDone ) {
+ GtkIconSize sizes[] = {
+ GTK_ICON_SIZE_MENU,
+ GTK_ICON_SIZE_SMALL_TOOLBAR,
+ GTK_ICON_SIZE_LARGE_TOOLBAR,
+ GTK_ICON_SIZE_BUTTON,
+ GTK_ICON_SIZE_DIALOG
+ };
+ set_size_mappings( G_N_ELEMENTS(sizes), sizes );
+ }
+
+ width = sizeThings[_size].width;
+ height = sizeThings[_size].height;
+
+ if ( _view == VIEW_TYPE_LIST ) {
+ width *= 3;
+ }
+
+ if ( _ratio != 100 ) {
+ width = (width * _ratio) / 100;
+ if ( width < 0 ) {
+ width = 1;
+ }
+ }
+
+ req->width = width;
+ req->height = height;
+}
+
+void
+Preview::get_preferred_width_vfunc(int &minimal_width, int &natural_width) const
+{
+ GtkRequisition requisition;
+ size_request(&requisition);
+ minimal_width = natural_width = requisition.width;
+}
+
+void
+Preview::get_preferred_height_vfunc(int &minimal_height, int &natural_height) const
+{
+ GtkRequisition requisition;
+ size_request(&requisition);
+ minimal_height = natural_height = requisition.height;
+}
+
+bool
+Preview::on_draw(const Cairo::RefPtr<Cairo::Context> &cr)
+{
+ auto allocation = get_allocation();
+
+ gint insetTop = 0, insetBottom = 0;
+ gint insetLeft = 0, insetRight = 0;
+
+ if (_border == BORDER_SOLID) {
+ insetTop = 1;
+ insetLeft = 1;
+ }
+ if (_border == BORDER_SOLID_LAST_ROW) {
+ insetTop = insetBottom = 1;
+ insetLeft = 1;
+ }
+ if (_border == BORDER_WIDE) {
+ insetTop = insetBottom = 1;
+ insetLeft = insetRight = 1;
+ }
+
+ auto context = get_style_context();
+
+ context->render_frame(cr,
+ 0, 0,
+ allocation.get_width(), allocation.get_height());
+
+ context->render_background(cr,
+ 0, 0,
+ allocation.get_width(), allocation.get_height());
+
+ // Border
+ if (_border != BORDER_NONE) {
+ cr->set_source_rgb(0.0, 0.0, 0.0);
+ cr->rectangle(0, 0, allocation.get_width(), allocation.get_height());
+ cr->fill();
+ }
+
+ cr->set_source_rgb(_r/65535.0, _g/65535.0, _b/65535.0 );
+ cr->rectangle(insetLeft, insetTop, allocation.get_width() - (insetLeft + insetRight), allocation.get_height() - (insetTop + insetBottom));
+ cr->fill();
+
+ if (_previewPixbuf )
+ {
+ if ((allocation.get_width() != _scaledW) || (allocation.get_height() != _scaledH)) {
+ if (_scaled)
+ {
+ _scaled.reset();
+ }
+
+ _scaledW = allocation.get_width() - (insetLeft + insetRight);
+ _scaledH = allocation.get_height() - (insetTop + insetBottom);
+
+ _scaled = _previewPixbuf->scale_simple(_scaledW,
+ _scaledH,
+ Gdk::INTERP_BILINEAR);
+ }
+
+ Glib::RefPtr<Gdk::Pixbuf> pix = (_scaled) ? _scaled : _previewPixbuf;
+
+ // Border
+ if (_border != BORDER_NONE) {
+ cr->set_source_rgb(0.0, 0.0, 0.0);
+ cr->rectangle(0, 0, allocation.get_width(), allocation.get_height());
+ cr->fill();
+ }
+
+ Gdk::Cairo::set_source_pixbuf(cr, pix, insetLeft, insetTop);
+ cr->paint();
+ }
+
+ if (_linked)
+ {
+ /* Draw arrow */
+ GdkRectangle possible = {insetLeft,
+ insetTop,
+ (allocation.get_width() - (insetLeft + insetRight)),
+ (allocation.get_height() - (insetTop + insetBottom))
+ };
+
+ GdkRectangle area = {possible.x,
+ possible.y,
+ possible.width / 2,
+ possible.height / 2 };
+
+ /* Make it square */
+ if ( area.width > area.height )
+ area.width = area.height;
+ if ( area.height > area.width )
+ area.height = area.width;
+
+ /* Center it horizontally */
+ if ( area.width < possible.width ) {
+ int diff = (possible.width - area.width) / 2;
+ area.x += diff;
+ }
+
+ if (_linked & PREVIEW_LINK_IN)
+ {
+ context->render_arrow(cr,
+ G_PI, // Down-pointing arrow
+ area.x, area.y,
+ std::min(area.width, area.height)
+ );
+ }
+
+ if (_linked & PREVIEW_LINK_OUT)
+ {
+ GdkRectangle otherArea = {area.x, area.y, area.width, area.height};
+ if ( otherArea.height < possible.height ) {
+ otherArea.y = possible.y + (possible.height - otherArea.height);
+ }
+
+ context->render_arrow(cr,
+ G_PI, // Down-pointing arrow
+ otherArea.x, otherArea.y,
+ std::min(otherArea.width, otherArea.height)
+ );
+ }
+
+ if (_linked & PREVIEW_LINK_OTHER)
+ {
+ GdkRectangle otherArea = {insetLeft, area.y, area.width, area.height};
+ if ( otherArea.height < possible.height ) {
+ otherArea.y = possible.y + (possible.height - otherArea.height) / 2;
+ }
+
+ context->render_arrow(cr,
+ 1.5*G_PI, // Left-pointing arrow
+ otherArea.x, otherArea.y,
+ std::min(otherArea.width, otherArea.height)
+ );
+ }
+
+
+ if (_linked & PREVIEW_FILL)
+ {
+ GdkRectangle otherArea = {possible.x + ((possible.width / 4) - (area.width / 2)),
+ area.y,
+ area.width, area.height};
+ if ( otherArea.height < possible.height ) {
+ otherArea.y = possible.y + (possible.height - otherArea.height) / 2;
+ }
+ context->render_check(cr,
+ otherArea.x, otherArea.y,
+ otherArea.width, otherArea.height );
+ }
+
+ if (_linked & PREVIEW_STROKE)
+ {
+ GdkRectangle otherArea = {possible.x + (((possible.width * 3) / 4) - (area.width / 2)),
+ area.y,
+ area.width, area.height};
+ if ( otherArea.height < possible.height ) {
+ otherArea.y = possible.y + (possible.height - otherArea.height) / 2;
+ }
+ // This should be a diamond too?
+ context->render_check(cr,
+ otherArea.x, otherArea.y,
+ otherArea.width, otherArea.height );
+ }
+ }
+
+
+ if ( has_focus() ) {
+ allocation = get_allocation();
+
+ context->render_focus(cr,
+ 0 + 1, 0 + 1,
+ allocation.get_width() - 2, allocation.get_height() - 2 );
+ }
+
+ return false;
+}
+
+
+bool
+Preview::on_enter_notify_event(GdkEventCrossing* event )
+{
+ _within = true;
+ set_state_flags(_hot ? Gtk::STATE_FLAG_ACTIVE : Gtk::STATE_FLAG_PRELIGHT, false);
+
+ return false;
+}
+
+bool
+Preview::on_leave_notify_event(GdkEventCrossing* event)
+{
+ _within = false;
+ set_state_flags(Gtk::STATE_FLAG_NORMAL, false);
+
+ return false;
+}
+
+bool
+Preview::on_button_press_event(GdkEventButton *event)
+{
+ if (_takesFocus && !has_focus() )
+ {
+ grab_focus();
+ }
+
+ if ( event->button == PRIME_BUTTON_MAGIC_NUMBER ||
+ event->button == 2 )
+ {
+ _hot = true;
+
+ if ( _within )
+ {
+ set_state_flags(Gtk::STATE_FLAG_ACTIVE, false);
+ }
+ }
+
+ return false;
+}
+
+bool
+Preview::on_button_release_event(GdkEventButton* event)
+{
+ _hot = false;
+ set_state_flags(Gtk::STATE_FLAG_NORMAL, false);
+
+ if (_within &&
+ (event->button == PRIME_BUTTON_MAGIC_NUMBER ||
+ event->button == 2))
+ {
+ gboolean isAlt = ( ((event->state & GDK_SHIFT_MASK) == GDK_SHIFT_MASK) ||
+ (event->button == 2));
+
+ if ( isAlt )
+ {
+ _signal_alt_clicked(2);
+ }
+ else
+ {
+ _signal_clicked.emit();
+ }
+ }
+
+ return false;
+}
+
+void
+Preview::set_linked(LinkType link)
+{
+ link = (LinkType)(link & PREVIEW_LINK_ALL);
+
+ if (link != _linked)
+ {
+ _linked = link;
+
+ queue_draw();
+ }
+}
+
+LinkType
+Preview::get_linked() const
+{
+ return (LinkType)_linked;
+}
+
+void
+Preview::set_details(ViewType view,
+ PreviewSize size,
+ guint ratio,
+ guint border)
+{
+ _view = view;
+
+ if ( size > PREVIEW_SIZE_LAST )
+ {
+ size = PREVIEW_SIZE_LAST;
+ }
+
+ _size = size;
+
+ if ( ratio > PREVIEW_MAX_RATIO )
+ {
+ ratio = PREVIEW_MAX_RATIO;
+ }
+
+ _ratio = ratio;
+ _border = border;
+
+ queue_draw();
+}
+
+Preview::Preview()
+ : _r(0x80),
+ _g(0x80),
+ _b(0xcc),
+ _scaledW(0),
+ _scaledH(0),
+ _hot(false),
+ _within(false),
+ _takesFocus(false),
+ _view(VIEW_TYPE_LIST),
+ _size(PREVIEW_SIZE_SMALL),
+ _ratio(100),
+ _border(BORDER_NONE),
+ _previewPixbuf(nullptr),
+ _scaled(nullptr),
+ _linked(PREVIEW_LINK_NONE)
+{
+ set_can_focus(true);
+ set_receives_default(true);
+
+ set_sensitive(true);
+
+ add_events(Gdk::BUTTON_PRESS_MASK
+ |Gdk::BUTTON_RELEASE_MASK
+ |Gdk::KEY_PRESS_MASK
+ |Gdk::KEY_RELEASE_MASK
+ |Gdk::FOCUS_CHANGE_MASK
+ |Gdk::ENTER_NOTIFY_MASK
+ |Gdk::LEAVE_NOTIFY_MASK );
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/widget/preview.h b/src/ui/widget/preview.h
new file mode 100644
index 0000000..d352a0d
--- /dev/null
+++ b/src/ui/widget/preview.h
@@ -0,0 +1,165 @@
+// SPDX-License-Identifier: GPL-2.0-or-later OR MPL-1.1 OR LGPL-2.1-or-later
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Eek Preview Stuffs.
+ *
+ * The Initial Developer of the Original Code is
+ * Jon A. Cruz.
+ * Portions created by the Initial Developer are Copyright (C) 2005-2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#ifndef SEEN_EEK_PREVIEW_H
+#define SEEN_EEK_PREVIEW_H
+
+#include <gtkmm/drawingarea.h>
+
+/**
+ * @file
+ * Generic implementation of an object that can be shown by a preview.
+ */
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+enum PreviewStyle {
+ PREVIEW_STYLE_ICON = 0,
+ PREVIEW_STYLE_PREVIEW,
+ PREVIEW_STYLE_NAME,
+ PREVIEW_STYLE_BLURB,
+ PREVIEW_STYLE_ICON_NAME,
+ PREVIEW_STYLE_ICON_BLURB,
+ PREVIEW_STYLE_PREVIEW_NAME,
+ PREVIEW_STYLE_PREVIEW_BLURB
+};
+
+enum ViewType {
+ VIEW_TYPE_LIST = 0,
+ VIEW_TYPE_GRID
+};
+
+enum PreviewSize {
+ PREVIEW_SIZE_TINY = 0,
+ PREVIEW_SIZE_SMALL,
+ PREVIEW_SIZE_MEDIUM,
+ PREVIEW_SIZE_BIG,
+ PREVIEW_SIZE_BIGGER,
+ PREVIEW_SIZE_HUGE
+};
+
+enum LinkType {
+ PREVIEW_LINK_NONE = 0,
+ PREVIEW_LINK_IN = 1,
+ PREVIEW_LINK_OUT = 2,
+ PREVIEW_LINK_OTHER = 4,
+ PREVIEW_FILL = 8,
+ PREVIEW_STROKE = 16,
+ PREVIEW_LINK_ALL = 31
+};
+
+enum BorderStyle {
+ BORDER_NONE = 0,
+ BORDER_SOLID,
+ BORDER_WIDE,
+ BORDER_SOLID_LAST_ROW,
+};
+
+class Preview : public Gtk::DrawingArea {
+private:
+ int _scaledW;
+ int _scaledH;
+
+ int _r;
+ int _g;
+ int _b;
+
+ bool _hot;
+ bool _within;
+ bool _takesFocus; ///< flag to grab focus when clicked
+ ViewType _view;
+ PreviewSize _size;
+ unsigned int _ratio;
+ LinkType _linked;
+ unsigned int _border;
+ bool _freesize = false;
+
+ Glib::RefPtr<Gdk::Pixbuf> _previewPixbuf;
+ Glib::RefPtr<Gdk::Pixbuf> _scaled;
+
+ // signals
+ sigc::signal<void> _signal_clicked;
+ sigc::signal<void, int> _signal_alt_clicked;
+
+ void size_request(GtkRequisition *req) const;
+
+protected:
+ void get_preferred_width_vfunc(int &minimal_width, int &natural_width) const override;
+ void get_preferred_height_vfunc(int &minimal_height, int &natural_height) const override;
+ bool on_draw(const Cairo::RefPtr<Cairo::Context> &cr) override;
+ bool on_button_press_event(GdkEventButton *button_event) override;
+ bool on_button_release_event(GdkEventButton *button_event) override;
+ bool on_enter_notify_event(GdkEventCrossing* event ) override;
+ bool on_leave_notify_event(GdkEventCrossing* event ) override;
+
+public:
+ Preview();
+ bool get_focus_on_click() const {return _takesFocus;}
+ void set_focus_on_click(bool focus_on_click) {_takesFocus = focus_on_click;}
+ LinkType get_linked() const;
+ void set_linked(LinkType link);
+ void set_details(ViewType view,
+ PreviewSize size,
+ guint ratio,
+ guint border);
+ void set_color(int r, int g, int b);
+ void set_pixbuf(const Glib::RefPtr<Gdk::Pixbuf> &pixbuf);
+ void set_freesize(bool enable);
+ static void set_size_mappings(guint count, GtkIconSize const* sizes);
+
+ decltype(_signal_clicked) signal_clicked() {return _signal_clicked;}
+ decltype(_signal_alt_clicked) signal_alt_clicked() {return _signal_alt_clicked;}
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif /* SEEN_EEK_PREVIEW_H */
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/widget/random.cpp b/src/ui/widget/random.cpp
new file mode 100644
index 0000000..495a778
--- /dev/null
+++ b/src/ui/widget/random.cpp
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Carl Hetherington <inkscape@carlh.net>
+ * Derek P. Moore <derekm@hackunix.org>
+ * Bryce Harrington <bryce@bryceharrington.org>
+ *
+ * Copyright (C) 2004 Carl Hetherington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "random.h"
+#include "ui/icon-loader.h"
+#include <glibmm/i18n.h>
+
+#include <gtkmm/button.h>
+#include <gtkmm/image.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Random::Random(Glib::ustring const &label, Glib::ustring const &tooltip,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Scalar(label, tooltip, suffix, icon, mnemonic)
+{
+ startseed = 0;
+ addReseedButton();
+}
+
+Random::Random(Glib::ustring const &label, Glib::ustring const &tooltip,
+ unsigned digits,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Scalar(label, tooltip, digits, suffix, icon, mnemonic)
+{
+ startseed = 0;
+ addReseedButton();
+}
+
+Random::Random(Glib::ustring const &label, Glib::ustring const &tooltip,
+ Glib::RefPtr<Gtk::Adjustment> &adjust,
+ unsigned digits,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Scalar(label, tooltip, adjust, digits, suffix, icon, mnemonic)
+{
+ startseed = 0;
+ addReseedButton();
+}
+
+long Random::getStartSeed() const
+{
+ return startseed;
+}
+
+void Random::setStartSeed(long newseed)
+{
+ startseed = newseed;
+}
+
+void Random::addReseedButton()
+{
+ Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("randomize", Gtk::ICON_SIZE_BUTTON));
+ Gtk::Button * pButton = Gtk::manage(new Gtk::Button());
+ pButton->set_relief(Gtk::RELIEF_NONE);
+ pIcon->show();
+ pButton->add(*pIcon);
+ pButton->show();
+ pButton->signal_clicked().connect(sigc::mem_fun(*this, &Random::onReseedButtonClick));
+ pButton->set_tooltip_text(_("Reseed the random number generator; this creates a different sequence of random numbers."));
+
+ pack_start(*pButton, Gtk::PACK_SHRINK, 0);
+}
+
+void
+Random::onReseedButtonClick()
+{
+ startseed = g_random_int();
+ signal_reseeded.emit();
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/widget/random.h b/src/ui/widget/random.h
new file mode 100644
index 0000000..2648cb2
--- /dev/null
+++ b/src/ui/widget/random.h
@@ -0,0 +1,125 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ *
+ * Copyright (C) 2007 Author
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_RANDOM_H
+#define INKSCAPE_UI_WIDGET_RANDOM_H
+
+#include "scalar.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A labelled text box, with spin buttons and optional
+ * icon or suffix, for entering arbitrary number values. It adds an extra
+ * number called "startseed", that is not UI edittable, but should be put in SVG.
+ * This does NOT generate a random number, but provides merely the saving of
+ * the startseed value.
+ */
+class Random : public Scalar
+{
+public:
+
+ /**
+ * Construct a Random scalar Widget.
+ *
+ * @param label Label.
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to false).
+ */
+ Random(Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Construct a Random Scalar Widget.
+ *
+ * @param label Label.
+ * @param digits Number of decimal digits to display.
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to false).
+ */
+ Random(Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ unsigned digits,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Construct a Random Scalar Widget.
+ *
+ * @param label Label.
+ * @param adjust Adjustment to use for the SpinButton.
+ * @param digits Number of decimal digits to display (defaults to 0).
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to true).
+ */
+ Random(Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ Glib::RefPtr<Gtk::Adjustment> &adjust,
+ unsigned digits = 0,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Gets the startseed.
+ */
+ long getStartSeed() const;
+
+ /**
+ * Sets the startseed number.
+ */
+ void setStartSeed(long newseed);
+
+ sigc::signal <void> signal_reseeded;
+
+protected:
+ long startseed;
+
+private:
+
+ /**
+ * Add reseed button to the widget.
+ */
+ void addReseedButton();
+
+ void onReseedButtonClick();
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_RANDOM_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/widget/registered-enums.h b/src/ui/widget/registered-enums.h
new file mode 100644
index 0000000..b0cc199
--- /dev/null
+++ b/src/ui/widget/registered-enums.h
@@ -0,0 +1,99 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ *
+ * Copyright (C) 2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_REGISTERED_ENUMS_H
+#define INKSCAPE_UI_WIDGET_REGISTERED_ENUMS_H
+
+#include "ui/widget/combo-enums.h"
+#include "ui/widget/registered-widget.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Simplified management of enumerations in the UI as combobox.
+ */
+template<typename E> class RegisteredEnum : public RegisteredWidget< LabelledComboBoxEnum<E> >
+{
+public:
+ ~RegisteredEnum() override {
+ _changed_connection.disconnect();
+ }
+
+ RegisteredEnum ( const Glib::ustring& label,
+ const Glib::ustring& tip,
+ const Glib::ustring& key,
+ const Util::EnumDataConverter<E>& c,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr,
+ bool sorted = true )
+ : RegisteredWidget< LabelledComboBoxEnum<E> >(label, tip, c, (const Glib::ustring &)"", (const Glib::ustring &)"", true, sorted)
+ {
+ RegisteredWidget< LabelledComboBoxEnum<E> >::init_parent(key, wr, repr_in, doc_in);
+ _changed_connection = combobox()->signal_changed().connect (sigc::mem_fun (*this, &RegisteredEnum::on_changed));
+ }
+
+ void set_active_by_id (E id) {
+ combobox()->set_active_by_id(id);
+ };
+
+ void set_active_by_key (const Glib::ustring& key) {
+ combobox()->set_active_by_key(key);
+ }
+
+ inline const Util::EnumData<E>* get_active_data() {
+ combobox()->get_active_data();
+ }
+
+ ComboBoxEnum<E> * combobox() {
+ return LabelledComboBoxEnum<E>::getCombobox();
+ }
+
+ sigc::connection _changed_connection;
+
+protected:
+ void on_changed() {
+ if (combobox()->setProgrammatically) {
+ combobox()->setProgrammatically = false;
+ return;
+ }
+
+ if (RegisteredWidget< LabelledComboBoxEnum<E> >::_wr->isUpdating())
+ return;
+
+ RegisteredWidget< LabelledComboBoxEnum<E> >::_wr->setUpdating (true);
+
+ const Util::EnumData<E>* data = combobox()->get_active_data();
+ if (data) {
+ RegisteredWidget< LabelledComboBoxEnum<E> >::write_to_xml(data->key.c_str());
+ }
+
+ RegisteredWidget< LabelledComboBoxEnum<E> >::_wr->setUpdating (false);
+ }
+};
+
+}
+}
+}
+
+#endif
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/widget/registered-widget.cpp b/src/ui/widget/registered-widget.cpp
new file mode 100644
index 0000000..aaea668
--- /dev/null
+++ b/src/ui/widget/registered-widget.cpp
@@ -0,0 +1,843 @@
+// 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()
+{
+ _toggled_connection.disconnect();
+}
+
+RegisteredCheckButton::RegisteredCheckButton (const Glib::ustring& label, const Glib::ustring& tip, const Glib::ustring& key, Registry& wr, bool right, Inkscape::XML::Node* repr_in, SPDocument *doc_in, char const *active_str, char const *inactive_str)
+ : RegisteredWidget<Gtk::CheckButton>()
+ , _active_str(active_str)
+ , _inactive_str(inactive_str)
+{
+ init_parent(key, wr, repr_in, doc_in);
+
+ setProgrammatically = false;
+
+ set_tooltip_text (tip);
+ Gtk::Label *l = new Gtk::Label();
+ l->set_markup(label);
+ l->set_use_underline (true);
+ add (*manage (l));
+
+ if(right) set_halign(Gtk::ALIGN_END);
+ else set_halign(Gtk::ALIGN_START);
+
+ set_valign(Gtk::ALIGN_CENTER);
+ _toggled_connection = signal_toggled().connect (sigc::mem_fun (*this, &RegisteredCheckButton::on_toggled));
+}
+
+void
+RegisteredCheckButton::setActive (bool b)
+{
+ setProgrammatically = true;
+ set_active (b);
+ //The slave button is greyed out if the master button is unchecked
+ for (std::list<Gtk::Widget*>::const_iterator i = _slavewidgets.begin(); i != _slavewidgets.end(); ++i) {
+ (*i)->set_sensitive(b);
+ }
+ setProgrammatically = false;
+}
+
+void
+RegisteredCheckButton::on_toggled()
+{
+ if (setProgrammatically) {
+ setProgrammatically = false;
+ return;
+ }
+
+ if (_wr->isUpdating())
+ return;
+ _wr->setUpdating (true);
+
+ write_to_xml(get_active() ? _active_str : _inactive_str);
+ //The slave button is greyed out if the master button is unchecked
+ for (std::list<Gtk::Widget*>::const_iterator i = _slavewidgets.begin(); i != _slavewidgets.end(); ++i) {
+ (*i)->set_sensitive(get_active());
+ }
+
+ _wr->setUpdating (false);
+}
+
+/*#########################################
+ * Registered TOGGLEBUTTON
+ */
+
+RegisteredToggleButton::~RegisteredToggleButton()
+{
+ _toggled_connection.disconnect();
+}
+
+RegisteredToggleButton::RegisteredToggleButton (const Glib::ustring& /*label*/, const Glib::ustring& tip, const Glib::ustring& key, Registry& wr, bool right, Inkscape::XML::Node* repr_in, SPDocument *doc_in, char const *icon_active, char const *icon_inactive)
+ : RegisteredWidget<Gtk::ToggleButton>()
+{
+ init_parent(key, wr, repr_in, doc_in);
+ setProgrammatically = false;
+ set_tooltip_text (tip);
+
+ if(right) set_halign(Gtk::ALIGN_END);
+ else set_halign(Gtk::ALIGN_START);
+
+ set_valign(Gtk::ALIGN_CENTER);
+ _toggled_connection = signal_toggled().connect (sigc::mem_fun (*this, &RegisteredToggleButton::on_toggled));
+}
+
+void
+RegisteredToggleButton::setActive (bool b)
+{
+ setProgrammatically = true;
+ set_active (b);
+ //The slave button is greyed out if the master button is untoggled
+ for (std::list<Gtk::Widget*>::const_iterator i = _slavewidgets.begin(); i != _slavewidgets.end(); ++i) {
+ (*i)->set_sensitive(b);
+ }
+ setProgrammatically = false;
+}
+
+void
+RegisteredToggleButton::on_toggled()
+{
+ if (setProgrammatically) {
+ setProgrammatically = false;
+ return;
+ }
+
+ if (_wr->isUpdating())
+ return;
+ _wr->setUpdating (true);
+
+ write_to_xml(get_active() ? "true" : "false");
+ //The slave button is greyed out if the master button is untoggled
+ for (std::list<Gtk::Widget*>::const_iterator i = _slavewidgets.begin(); i != _slavewidgets.end(); ++i) {
+ (*i)->set_sensitive(get_active());
+ }
+
+ _wr->setUpdating (false);
+}
+
+/*#########################################
+ * Registered UNITMENU
+ */
+
+RegisteredUnitMenu::~RegisteredUnitMenu()
+{
+ _changed_connection.disconnect();
+}
+
+RegisteredUnitMenu::RegisteredUnitMenu (const Glib::ustring& label, const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, SPDocument *doc_in)
+ : RegisteredWidget<Labelled> (label, "" /*tooltip*/, new UnitMenu())
+{
+ init_parent(key, wr, repr_in, doc_in);
+
+ getUnitMenu()->setUnitType (UNIT_TYPE_LINEAR);
+ _changed_connection = getUnitMenu()->signal_changed().connect (sigc::mem_fun (*this, &RegisteredUnitMenu::on_changed));
+}
+
+void
+RegisteredUnitMenu::setUnit (Glib::ustring unit)
+{
+ getUnitMenu()->setUnit(unit);
+}
+
+void
+RegisteredUnitMenu::on_changed()
+{
+ if (_wr->isUpdating())
+ return;
+
+ Inkscape::SVGOStringStream os;
+ os << getUnitMenu()->getUnitAbbr();
+
+ _wr->setUpdating (true);
+
+ write_to_xml(os.str().c_str());
+
+ _wr->setUpdating (false);
+}
+
+
+/*#########################################
+ * Registered SCALARUNIT
+ */
+
+RegisteredScalarUnit::~RegisteredScalarUnit()
+{
+ _value_changed_connection.disconnect();
+}
+
+RegisteredScalarUnit::RegisteredScalarUnit (const Glib::ustring& label, const Glib::ustring& tip, const Glib::ustring& key, const RegisteredUnitMenu &rum, Registry& wr, Inkscape::XML::Node* repr_in, SPDocument *doc_in, RSU_UserUnits user_units)
+ : RegisteredWidget<ScalarUnit>(label, tip, UNIT_TYPE_LINEAR, "", "", rum.getUnitMenu()),
+ _um(nullptr)
+{
+ init_parent(key, wr, repr_in, doc_in);
+
+ setProgrammatically = false;
+
+ initScalar (-1e6, 1e6);
+ setUnit (rum.getUnitMenu()->getUnitAbbr());
+ setDigits (2);
+ _um = rum.getUnitMenu();
+ _user_units = user_units;
+ _value_changed_connection = signal_value_changed().connect (sigc::mem_fun (*this, &RegisteredScalarUnit::on_value_changed));
+}
+
+
+void
+RegisteredScalarUnit::on_value_changed()
+{
+ if (setProgrammatically) {
+ setProgrammatically = false;
+ return;
+ }
+
+ if (_wr->isUpdating())
+ return;
+
+ _wr->setUpdating (true);
+
+ Inkscape::SVGOStringStream os;
+ if (_user_units != RSU_none) {
+ // Output length in 'user units', taking into account scale in 'x' or 'y'.
+ double scale = 1.0;
+ if (doc) {
+ SPRoot *root = doc->getRoot();
+ if (root->viewBox_set) {
+ // check to see if scaling is uniform
+ if(Geom::are_near((root->viewBox.width() * root->height.computed) / (root->width.computed * root->viewBox.height()), 1.0, Geom::EPSILON)) {
+ scale = (root->viewBox.width() / root->width.computed + root->viewBox.height() / root->height.computed)/2.0;
+ } else if (_user_units == RSU_x) {
+ scale = root->viewBox.width() / root->width.computed;
+ } else {
+ scale = root->viewBox.height() / root->height.computed;
+ }
+ }
+ }
+ os << getValue("px") * scale;
+ } else {
+ // Output using unit identifiers.
+ os << getValue("");
+ if (_um)
+ os << _um->getUnitAbbr();
+ }
+
+ write_to_xml(os.str().c_str());
+ _wr->setUpdating (false);
+}
+
+
+/*#########################################
+ * Registered SCALAR
+ */
+
+RegisteredScalar::~RegisteredScalar()
+{
+ _value_changed_connection.disconnect();
+}
+
+RegisteredScalar::RegisteredScalar ( const Glib::ustring& label, const Glib::ustring& tip,
+ const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in,
+ SPDocument * doc_in )
+ : RegisteredWidget<Scalar>(label, tip)
+{
+ init_parent(key, wr, repr_in, doc_in);
+
+ setProgrammatically = false;
+ setRange (-1e6, 1e6);
+ setDigits (2);
+ setIncrements(0.1, 1.0);
+ _value_changed_connection = signal_value_changed().connect (sigc::mem_fun (*this, &RegisteredScalar::on_value_changed));
+}
+
+void
+RegisteredScalar::on_value_changed()
+{
+ if (setProgrammatically) {
+ setProgrammatically = false;
+ return;
+ }
+ if (_wr->isUpdating()) {
+ return;
+ }
+ _wr->setUpdating (true);
+
+ Inkscape::SVGOStringStream os;
+ //Force exact 0 if decimals over to 6
+ double val = getValue() < 1e-6 && getValue() > -1e-6?0.0:getValue();
+ os << val;
+ //TODO: Test is ok remove this sensitives
+ //also removed in registered text and in registered random
+ //set_sensitive(false);
+ write_to_xml(os.str().c_str());
+ //set_sensitive(true);
+ _wr->setUpdating (false);
+}
+
+
+/*#########################################
+ * Registered TEXT
+ */
+
+RegisteredText::~RegisteredText()
+{
+ _activate_connection.disconnect();
+}
+
+RegisteredText::RegisteredText ( const Glib::ustring& label, const Glib::ustring& tip,
+ const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in,
+ SPDocument * doc_in )
+ : RegisteredWidget<Text>(label, tip)
+{
+ init_parent(key, wr, repr_in, doc_in);
+
+ setProgrammatically = false;
+ _activate_connection = signal_activate().connect (sigc::mem_fun (*this, &RegisteredText::on_activate));
+}
+
+void
+RegisteredText::on_activate()
+{
+ if (setProgrammatically) {
+ setProgrammatically = false;
+ return;
+ }
+
+ if (_wr->isUpdating()) {
+ return;
+ }
+ _wr->setUpdating (true);
+ Glib::ustring str(getText());
+ Inkscape::SVGOStringStream os;
+ os << str;
+ write_to_xml(os.str().c_str());
+ _wr->setUpdating (false);
+}
+
+
+/*#########################################
+ * Registered COLORPICKER
+ */
+
+RegisteredColorPicker::RegisteredColorPicker(const Glib::ustring& label,
+ const Glib::ustring& title,
+ const Glib::ustring& tip,
+ const Glib::ustring& ckey,
+ const Glib::ustring& akey,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in,
+ SPDocument *doc_in)
+ : RegisteredWidget<LabelledColorPicker> (label, title, tip, 0, true)
+{
+ init_parent("", wr, repr_in, doc_in);
+
+ _ckey = ckey;
+ _akey = akey;
+ _changed_connection = connectChanged (sigc::mem_fun (*this, &RegisteredColorPicker::on_changed));
+}
+
+RegisteredColorPicker::~RegisteredColorPicker()
+{
+ _changed_connection.disconnect();
+}
+
+void
+RegisteredColorPicker::setRgba32 (guint32 rgba)
+{
+ LabelledColorPicker::setRgba32 (rgba);
+}
+
+void
+RegisteredColorPicker::closeWindow()
+{
+ LabelledColorPicker::closeWindow();
+}
+
+void
+RegisteredColorPicker::on_changed (guint32 rgba)
+{
+ if (_wr->isUpdating())
+ return;
+
+ _wr->setUpdating (true);
+
+ // Use local repr here. When repr is specified, use that one, but
+ // if repr==NULL, get the repr of namedview of active desktop.
+ Inkscape::XML::Node *local_repr = repr;
+ SPDocument *local_doc = doc;
+ if (!local_repr) {
+ 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);
+ }
+ bool saved = DocumentUndo::getUndoSensitive(local_doc);
+ DocumentUndo::setUndoSensitive(local_doc, false);
+ local_repr->setAttribute(_ckey, c);
+ local_repr->setAttributeCssDouble(_akey.c_str(), (rgba & 0xff) / 255.0);
+ DocumentUndo::setUndoSensitive(local_doc, saved);
+
+ 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..f843540
--- /dev/null
+++ b/src/ui/widget/registered-widget.h
@@ -0,0 +1,455 @@
+// 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();
+ }
+
+ bool saved = DocumentUndo::getUndoSensitive(local_doc);
+ DocumentUndo::setUndoSensitive(local_doc, false);
+ const char * svgstr_old = local_repr->attribute(_key.c_str());
+ if (!write_undo) {
+ local_repr->setAttribute(_key, svgstr);
+ }
+ DocumentUndo::setUndoSensitive(local_doc, saved);
+ if (svgstr_old && svgstr && strcmp(svgstr_old,svgstr)) {
+ local_doc->setModifiedSinceSave();
+ }
+
+ if (write_undo) {
+ local_repr->setAttribute(_key, svgstr);
+ DocumentUndo::done(local_doc, event_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;
+ 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;
+ sigc::connection _toggled_connection;
+ void on_toggled() override;
+};
+
+class RegisteredToggleButton : public RegisteredWidget<Gtk::ToggleButton> {
+public:
+ ~RegisteredToggleButton() override;
+ RegisteredToggleButton (const Glib::ustring& label, const Glib::ustring& tip, const Glib::ustring& key, Registry& wr, bool right=true, Inkscape::XML::Node* repr_in=nullptr, SPDocument *doc_in=nullptr, char const *icon_active = "true", char const *icon_inactive = "false");
+
+ void setActive (bool);
+
+ std::list<Gtk::Widget*> _slavewidgets;
+
+ // a slave button is only sensitive when the master button is active
+ // i.e. a slave button is greyed-out when the master button is not checked
+
+ void setSlaveWidgets(std::list<Gtk::Widget*> 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:
+ sigc::connection _toggled_connection;
+ void on_toggled() override;
+};
+
+class RegisteredUnitMenu : public RegisteredWidget<Labelled> {
+public:
+ ~RegisteredUnitMenu() override;
+ RegisteredUnitMenu ( const Glib::ustring& label,
+ const Glib::ustring& key,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr );
+
+ void setUnit (const Glib::ustring);
+ Unit const * getUnit() const { return static_cast<UnitMenu*>(_widget)->getUnit(); };
+ UnitMenu* getUnitMenu() const { return static_cast<UnitMenu*>(_widget); };
+ sigc::connection _changed_connection;
+
+protected:
+ void on_changed();
+};
+
+// Allow RegisteredScalarUnit to output lengths in 'user units' (which may have direction dependent
+// scale factors).
+enum RSU_UserUnits {
+ RSU_none,
+ RSU_x,
+ RSU_y
+};
+
+class RegisteredScalarUnit : public RegisteredWidget<ScalarUnit> {
+public:
+ ~RegisteredScalarUnit() override;
+ RegisteredScalarUnit ( const Glib::ustring& label,
+ const Glib::ustring& tip,
+ const Glib::ustring& key,
+ const RegisteredUnitMenu &rum,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr,
+ RSU_UserUnits _user_units = RSU_none );
+
+protected:
+ sigc::connection _value_changed_connection;
+ UnitMenu *_um;
+ void on_value_changed();
+ RSU_UserUnits _user_units;
+};
+
+class RegisteredScalar : public RegisteredWidget<Scalar> {
+public:
+ ~RegisteredScalar() override;
+ RegisteredScalar (const Glib::ustring& label,
+ const Glib::ustring& tip,
+ const Glib::ustring& key,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr );
+protected:
+ sigc::connection _value_changed_connection;
+ void on_value_changed();
+};
+
+class RegisteredText : public RegisteredWidget<Text> {
+public:
+ ~RegisteredText() override;
+ RegisteredText (const Glib::ustring& label,
+ const Glib::ustring& tip,
+ const Glib::ustring& key,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr );
+
+protected:
+ sigc::connection _activate_connection;
+ void on_activate();
+};
+
+class RegisteredColorPicker : public RegisteredWidget<LabelledColorPicker> {
+public:
+ ~RegisteredColorPicker() override;
+
+ RegisteredColorPicker (const Glib::ustring& label,
+ const Glib::ustring& title,
+ const Glib::ustring& tip,
+ const Glib::ustring& ckey,
+ const Glib::ustring& akey,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr);
+
+ void setRgba32 (guint32);
+ void closeWindow();
+
+protected:
+ Glib::ustring _ckey, _akey;
+ void on_changed (guint32);
+ sigc::connection _changed_connection;
+};
+
+class RegisteredSuffixedInteger : public RegisteredWidget<Scalar> {
+public:
+ ~RegisteredSuffixedInteger() override;
+ RegisteredSuffixedInteger ( const Glib::ustring& label,
+ const Glib::ustring& tip,
+ const Glib::ustring& suffix,
+ const Glib::ustring& key,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr );
+
+ bool setProgrammatically; // true if the value was set by setValue, not changed by the user;
+ // if a callback checks it, it must reset it back to false
+
+protected:
+ sigc::connection _changed_connection;
+ void on_value_changed();
+};
+
+class RegisteredRadioButtonPair : public RegisteredWidget<Gtk::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..7f32823
--- /dev/null
+++ b/src/ui/widget/rotateable.cpp
@@ -0,0 +1,179 @@
+// 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);
+ }
+ }
+ 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..d6766fe
--- /dev/null
+++ b/src/ui/widget/scalar.cpp
@@ -0,0 +1,186 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Carl Hetherington <inkscape@carlh.net>
+ * Derek P. Moore <derekm@hackunix.org>
+ * Bryce Harrington <bryce@bryceharrington.org>
+ * Johan Engelen <j.b.c.engelen@alumnus.utwente.nl>
+ *
+ * Copyright (C) 2004-2011 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "scalar.h"
+#include "spinbutton.h"
+#include <gtkmm/scale.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Scalar::Scalar(Glib::ustring const &label, Glib::ustring const &tooltip,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Labelled(label, tooltip, new SpinButton(), suffix, icon, mnemonic),
+ setProgrammatically(false)
+{
+}
+
+Scalar::Scalar(Glib::ustring const &label, Glib::ustring const &tooltip,
+ unsigned digits,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Labelled(label, tooltip, new SpinButton(0.0, digits), suffix, icon, mnemonic),
+ setProgrammatically(false)
+{
+}
+
+Scalar::Scalar(Glib::ustring const &label, Glib::ustring const &tooltip,
+ Glib::RefPtr<Gtk::Adjustment> &adjust,
+ unsigned digits,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Labelled(label, tooltip, new SpinButton(adjust, 0.0, digits), suffix, icon, mnemonic),
+ setProgrammatically(false)
+{
+}
+
+unsigned Scalar::getDigits() const
+{
+ g_assert(_widget != nullptr);
+ return static_cast<SpinButton*>(_widget)->get_digits();
+}
+
+double Scalar::getStep() const
+{
+ g_assert(_widget != nullptr);
+ double step, page;
+ static_cast<SpinButton*>(_widget)->get_increments(step, page);
+ return step;
+}
+
+double Scalar::getPage() const
+{
+ g_assert(_widget != nullptr);
+ double step, page;
+ static_cast<SpinButton*>(_widget)->get_increments(step, page);
+ return page;
+}
+
+double Scalar::getRangeMin() const
+{
+ g_assert(_widget != nullptr);
+ double min, max;
+ static_cast<SpinButton*>(_widget)->get_range(min, max);
+ return min;
+}
+
+double Scalar::getRangeMax() const
+{
+ g_assert(_widget != nullptr);
+ double min, max;
+ static_cast<SpinButton*>(_widget)->get_range(min, max);
+ return max;
+}
+
+double Scalar::getValue() const
+{
+ g_assert(_widget != nullptr);
+ return static_cast<SpinButton*>(_widget)->get_value();
+}
+
+int Scalar::getValueAsInt() const
+{
+ g_assert(_widget != nullptr);
+ return static_cast<SpinButton*>(_widget)->get_value_as_int();
+}
+
+
+void Scalar::setDigits(unsigned digits)
+{
+ g_assert(_widget != nullptr);
+ static_cast<SpinButton*>(_widget)->set_digits(digits);
+}
+
+void Scalar::setIncrements(double step, double /*page*/)
+{
+ g_assert(_widget != nullptr);
+ static_cast<SpinButton*>(_widget)->set_increments(step, 0);
+}
+
+void Scalar::setRange(double min, double max)
+{
+ g_assert(_widget != nullptr);
+ static_cast<SpinButton*>(_widget)->set_range(min, max);
+}
+
+void Scalar::setValue(double value, bool setProg)
+{
+ g_assert(_widget != nullptr);
+ if (setProg) {
+ setProgrammatically = true; // callback is supposed to reset back, if it cares
+ }
+ static_cast<SpinButton*>(_widget)->set_value(value);
+}
+
+void Scalar::setWidthChars(unsigned chars)
+{
+ g_assert(_widget != NULL);
+ static_cast<SpinButton*>(_widget)->set_width_chars(chars);
+}
+
+void Scalar::update()
+{
+ g_assert(_widget != nullptr);
+ static_cast<SpinButton*>(_widget)->update();
+}
+
+void Scalar::addSlider()
+{
+ auto scale = new Gtk::Scale(static_cast<SpinButton*>(_widget)->get_adjustment());
+ scale->set_draw_value(false);
+ 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..0ef8d35
--- /dev/null
+++ b/src/ui/widget/scalar.h
@@ -0,0 +1,191 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Carl Hetherington <inkscape@carlh.net>
+ * Derek P. Moore <derekm@hackunix.org>
+ * Bryce Harrington <bryce@bryceharrington.org>
+ *
+ * Copyright (C) 2004 Carl Hetherington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_SCALAR_H
+#define INKSCAPE_UI_WIDGET_SCALAR_H
+
+#include "labelled.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A labelled text box, with spin buttons and optional
+ * icon or suffix, for entering arbitrary number values.
+ */
+class Scalar : public Labelled
+{
+public:
+ /**
+ * Construct a Scalar Widget.
+ *
+ * @param label Label.
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to false).
+ */
+ Scalar(Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Construct a Scalar Widget.
+ *
+ * @param label Label.
+ * @param digits Number of decimal digits to display.
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to false).
+ */
+ Scalar(Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ unsigned digits,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Construct a Scalar Widget.
+ *
+ * @param label Label.
+ * @param adjust Adjustment to use for the SpinButton.
+ * @param digits Number of decimal digits to display (defaults to 0).
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to true).
+ */
+ Scalar(Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ Glib::RefPtr<Gtk::Adjustment> &adjust,
+ unsigned digits = 0,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Fetches the precision of the spin button.
+ */
+ unsigned getDigits() const;
+
+ /**
+ * Gets the current step increment used by the spin button.
+ */
+ double getStep() const;
+
+ /**
+ * Gets the current page increment used by the spin button.
+ */
+ double getPage() const;
+
+ /**
+ * Gets the minimum range value allowed for the spin button.
+ */
+ double getRangeMin() const;
+
+ /**
+ * Gets the maximum range value allowed for the spin button.
+ */
+ double getRangeMax() const;
+
+ bool getSnapToTicks() const;
+
+ /**
+ * Get the value in the spin_button.
+ */
+ double getValue() const;
+
+ /**
+ * Get the value spin_button represented as an integer.
+ */
+ int getValueAsInt() const;
+
+ /**
+ * Sets the precision to be displayed by the spin button.
+ */
+ void setDigits(unsigned digits);
+
+ /**
+ * Sets the step and page increments for the spin button.
+ * @todo Remove the second parameter - deprecated
+ */
+ void setIncrements(double step, double page);
+
+ /**
+ * Sets the minimum and maximum range allowed for the spin button.
+ */
+ void setRange(double min, double max);
+
+ /**
+ * Sets the value of the spin button.
+ */
+ void setValue(double value, bool setProg = true);
+
+ /**
+ * Sets the width of the spin button by number of characters.
+ */
+ void setWidthChars(unsigned chars);
+
+ /**
+ * Manually forces an update of the spin button.
+ */
+ void update();
+
+ /**
+ * Adds a slider (HScale) to the left of the spinbox.
+ */
+ void addSlider();
+
+ /**
+ * Signal raised when the spin button's value changes.
+ */
+ Glib::SignalProxy0<void> signal_value_changed();
+
+ /**
+ * Signal raised when the spin button's pressed.
+ */
+ Glib::SignalProxy1<bool, GdkEventButton*> signal_button_release_event();
+
+ /**
+ * true if the value was set by setValue, not changed by the user;
+ * if a callback checks it, it must reset it back to false.
+ */
+ bool setProgrammatically;
+
+ // 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..433112c
--- /dev/null
+++ b/src/ui/widget/selected-style.cpp
@@ -0,0 +1,1435 @@
+// 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/canvas.h" // Forced redraws.
+#include "ui/widget/color-preview.h"
+#include "ui/widget/gradient-image.h"
+
+#include "widgets/ege-paint-def.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
+};
+
+//TODO: warning: deprecated conversion from string constant to ‘gchar*’
+//
+//Turn out to be warnings that we should probably leave in place. The
+// pointers/types used need to be read-only. So until we correct the using
+// code, those warnings are actually desired. They say "Hey! Fix this". We
+// definitely don't want to hide/ignore them. --JonCruz
+static const GtkTargetEntry ui_drop_target_entries [] = {
+ {g_strdup("application/x-oswb-color"), 0, APP_OSWB_COLOR}
+};
+
+static guint nui_drop_target_entries = G_N_ELEMENTS(ui_drop_target_entries);
+
+/* convenience function */
+static Dialog::FillAndStroke *get_fill_and_stroke_panel(SPDesktop *desktop);
+
+SelectedStyle::SelectedStyle(bool /*layout*/)
+ : 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) {
+ ege::PaintDef color;
+ worked = color.fromMIMEData("application/x-oswb-color",
+ reinterpret_cast<char const *>(gtk_selection_data_get_data(data)),
+ gtk_selection_data_get_length(data),
+ gtk_selection_data_get_format(data));
+ if (worked) {
+ if (color.getType() == ege::PaintDef::CLEAR) {
+ colorspec = ""; // TODO check if this is sufficient
+ } else if (color.getType() == ege::PaintDef::NONE) {
+ colorspec = "none";
+ } else {
+ unsigned int r = color.getR();
+ unsigned int g = color.getG();
+ unsigned int b = color.getB();
+
+ gchar* tmp = g_strdup_printf("#%02x%02x%02x", r, g, b);
+ colorspec = tmp;
+ g_free(tmp);
+ }
+ }
+ }
+ if (worked) {
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_set_property(css, (tracker->item == SS_FILL) ? "fill":"stroke", colorspec.c_str());
+
+ sp_desktop_set_style(tracker->parent->_desktop, css);
+ sp_repr_css_attr_unref(css);
+ DocumentUndo::done(tracker->parent->_desktop->getDocument(), _("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] ) {
+ gtk_drag_dest_unset( GTK_WIDGET((i==SS_FILL) ? _fill_place.gobj():_stroke_place.gobj()) );
+ _dropEnabled[i] = false;
+ }
+ break;
+ case QUERY_STYLE_SINGLE:
+ case QUERY_STYLE_MULTIPLE_AVERAGED:
+ case QUERY_STYLE_MULTIPLE_SAME:
+ if ( !_dropEnabled[i] ) {
+ gtk_drag_dest_set( GTK_WIDGET( (i==SS_FILL) ? _fill_place.gobj():_stroke_place.gobj()),
+ GTK_DEST_DEFAULT_ALL,
+ ui_drop_target_entries,
+ nui_drop_target_entries,
+ GdkDragAction(GDK_ACTION_COPY | GDK_ACTION_MOVE) );
+ _dropEnabled[i] = true;
+ }
+ SPIPaint *paint;
+ if (i == SS_FILL) {
+ paint = &(query.fill);
+ } else {
+ paint = &(query.stroke);
+ }
+ if (paint->set && paint->isPaintserver()) {
+ SPPaintServer *server = (i == SS_FILL)? SP_STYLE_FILL_SERVER (&query) : SP_STYLE_STROKE_SERVER (&query);
+ if ( server ) {
+ Inkscape::XML::Node *srepr = server->getRepr();
+ _paintserver_id[i] += "url(#";
+ _paintserver_id[i] += srepr->attribute("id");
+ _paintserver_id[i] += ")";
+
+ if (SP_IS_LINEARGRADIENT(server)) {
+ SPGradient *vector = SP_GRADIENT(server)->getVector();
+ _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 (SP_IS_RADIALGRADIENT(server)) {
+ SPGradient *vector = SP_GRADIENT(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 (SP_IS_MESHGRADIENT(server)) {
+ SPGradient *array = SP_GRADIENT(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 (SP_IS_PATTERN(server)) {
+ place->add(_pattern[i]);
+ place->set_tooltip_text(__pattern[i]);
+ _mode[i] = SS_PATTERN;
+ } else if (SP_IS_HATCH(server)) {
+ place->add(_hatch[i]);
+ place->set_tooltip_text(__hatch[i]);
+ _mode[i] = SS_HATCH;
+ }
+ } else {
+ g_warning ("file %s: line %d: Unknown paint server", __FILE__, __LINE__);
+ }
+ } else if (paint->set && paint->isColor()) {
+ guint32 color = paint->value.color.toRGBA32(
+ SP_SCALE24_TO_FLOAT ((i == SS_FILL)? query.fill_opacity.value : query.stroke_opacity.value));
+ _lastselected[i] = _thisselected[i];
+ _thisselected[i] = color; // include opacity
+ ((Inkscape::UI::Widget::ColorPreview*)_color_preview[i])->setRgba32 (color);
+ _color_preview[i]->show_all();
+ place->add(*_color_preview[i]);
+ gchar c_string[64];
+ g_snprintf (c_string, 64, "%06x/%.3g", color >> 8, SP_RGBA32_A_F(color));
+ place->set_tooltip_text(__color[i] + ": " + c_string + _(", drag to adjust, middle-click to remove"));
+ _mode[i] = SS_COLOR;
+ _popup_copy[i].set_sensitive(true);
+
+ } else if (paint->set && paint->isNone()) {
+ place->add(_none[i]);
+ place->set_tooltip_text(__none[i]);
+ _mode[i] = SS_NONE;
+ } else if (!paint->set) {
+ place->add(_unset[i]);
+ place->set_tooltip_text(__unset[i]);
+ _mode[i] = SS_UNSET;
+ }
+ if (result == QUERY_STYLE_MULTIPLE_AVERAGED) {
+ flag_place->add(_averaged[i]);
+ flag_place->set_tooltip_text(__averaged[i]);
+ } else if (result == QUERY_STYLE_MULTIPLE_SAME) {
+ flag_place->add(_multiple[i]);
+ flag_place->set_tooltip_text(__multiple[i]);
+ }
+ break;
+ case QUERY_STYLE_MULTIPLE_DIFFERENT:
+ place->add(_many[i]);
+ place->set_tooltip_text(__many[i]);
+ _mode[i] = SS_MANY;
+ break;
+ default:
+ break;
+ }
+ }
+
+// Now query opacity
+ clearTooltip(_opacity_place);
+ clearTooltip(_opacity_sb);
+
+ int result = sp_desktop_query_style (_desktop, &query, QUERY_STYLE_PROPERTY_MASTEROPACITY);
+
+ switch (result) {
+ case QUERY_STYLE_NOTHING:
+ _opacity_place.set_tooltip_text(_("Nothing selected"));
+ _opacity_sb.set_tooltip_text(_("Nothing selected"));
+ _opacity_sb.set_sensitive(false);
+ break;
+ case QUERY_STYLE_SINGLE:
+ case QUERY_STYLE_MULTIPLE_AVERAGED:
+ case QUERY_STYLE_MULTIPLE_SAME:
+ _opacity_place.set_tooltip_text(_("Opacity (%)"));
+ _opacity_sb.set_tooltip_text(_("Opacity (%)"));
+ if (_opacity_blocked) break;
+ _opacity_blocked = true;
+ _opacity_sb.set_sensitive(true);
+ _opacity_adjustment->set_value(SP_SCALE24_TO_FLOAT(query.opacity.value) * 100);
+ _opacity_blocked = false;
+ break;
+ }
+
+// Now query stroke_width
+ int result_sw = sp_desktop_query_style (_desktop, &query, QUERY_STYLE_PROPERTY_STROKEWIDTH);
+ switch (result_sw) {
+ case QUERY_STYLE_NOTHING:
+ _stroke_width.set_markup("");
+ current_stroke_width = 0;
+ break;
+ case QUERY_STYLE_SINGLE:
+ case QUERY_STYLE_MULTIPLE_AVERAGED:
+ case QUERY_STYLE_MULTIPLE_SAME:
+ {
+ 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..6b7a9d9
--- /dev/null
+++ b/src/ui/widget/shapeicon.cpp
@@ -0,0 +1,117 @@
+// 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 "color.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)
+{
+ std::string shape_type = _property_shape_type.get_value();
+ std::string highlight = SPColor(_property_color.get_value()).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 (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);
+ }
+ }
+
+}
+
+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;
+}
+
+
+} // 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..b5ca033
--- /dev/null
+++ b/src/ui/widget/shapeicon.h
@@ -0,0 +1,101 @@
+// 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
+};
+
+/* 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)
+ {
+ 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();
+ }
+
+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;
+
+private:
+
+ 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;
+};
+
+} // 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..21aa525
--- /dev/null
+++ b/src/ui/widget/spin-scale.cpp
@@ -0,0 +1,231 @@
+// 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>
+
+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.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),
+ //TRANSLATORS: "Link" means to _link_ two sliders together
+ _link(C_("Sliders", "Link"))
+{
+ set_name("DualSpinScale");
+ signal_value_changed().connect(signal_attr_changed().make_slot());
+
+ _s1.get_adjustment()->signal_value_changed().connect(_signal_value_changed.make_slot());
+ _s2.get_adjustment()->signal_value_changed().connect(_signal_value_changed.make_slot());
+ _s1.get_adjustment()->signal_value_changed().connect(sigc::mem_fun(*this, &DualSpinScale::update_linked));
+
+ _link.signal_toggled().connect(sigc::mem_fun(*this, &DualSpinScale::link_toggled));
+
+ Gtk::Box* vb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
+ vb->add(_s1);
+ vb->add(_s2);
+ pack_start(*vb);
+ pack_start(_link, false, false);
+ _link.set_active(true);
+
+ show_all();
+}
+
+Glib::ustring DualSpinScale::get_as_attribute() const
+{
+ if(_link.get_active())
+ return _s1.get_as_attribute();
+ else
+ return _s1.get_as_attribute() + " " + _s2.get_as_attribute();
+}
+
+void DualSpinScale::set_from_attribute(SPObject* o)
+{
+ const gchar* val = attribute_value(o);
+ if(val) {
+ // Split val into parts
+ gchar** toks = g_strsplit(val, " ", 2);
+
+ if(toks) {
+ double v1 = 0.0, v2 = 0.0;
+ if(toks[0])
+ v1 = v2 = Glib::Ascii::strtod(toks[0]);
+ if(toks[1])
+ v2 = Glib::Ascii::strtod(toks[1]);
+
+ _link.set_active(toks[1] == nullptr);
+
+ _s1.get_adjustment()->set_value(v1);
+ _s2.get_adjustment()->set_value(v2);
+
+ g_strfreev(toks);
+ }
+ }
+}
+
+sigc::signal<void>& DualSpinScale::signal_value_changed()
+{
+ return _signal_value_changed;
+}
+
+const SpinScale& DualSpinScale::get_SpinScale1() const
+{
+ return _s1;
+}
+
+SpinScale& DualSpinScale::get_SpinScale1()
+{
+ return _s1;
+}
+
+const SpinScale& DualSpinScale::get_SpinScale2() const
+{
+ return _s2;
+}
+
+SpinScale& DualSpinScale::get_SpinScale2()
+{
+ return _s2;
+}
+
+void DualSpinScale::link_toggled()
+{
+ _s2.set_sensitive(!_link.get_active());
+ update_linked();
+}
+
+void DualSpinScale::update_linked()
+{
+ if(_link.get_active())
+ _s2.set_value(_s1.get_value());
+}
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/widget/spin-scale.h b/src/ui/widget/spin-scale.h
new file mode 100644
index 0000000..b2e478d
--- /dev/null
+++ b/src/ui/widget/spin-scale.h
@@ -0,0 +1,114 @@
+// 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();
+ sigc::signal<void> _signal_value_changed;
+ SpinScale _s1, _s2;
+ Gtk::ToggleButton _link;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_SPIN_SCALE_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/widget/spinbutton.cpp b/src/ui/widget/spinbutton.cpp
new file mode 100644
index 0000000..423ffe5
--- /dev/null
+++ b/src/ui/widget/spinbutton.cpp
@@ -0,0 +1,126 @@
+// 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 {
+
+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();
+ return true; // I consumed the event
+ 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..e8e5628
--- /dev/null
+++ b/src/ui/widget/spinbutton.h
@@ -0,0 +1,112 @@
+// 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;
+
+/**
+ * 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..53b50dd
--- /dev/null
+++ b/src/ui/widget/stroke-style.cpp
@@ -0,0 +1,1232 @@
+// 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.
+ */
+
+#define noSP_SS_VERBOSE
+
+#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());
+
+ Inkscape::Selection *selection = desktop->getSelection();
+ auto itemlist= selection->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ SPItem *item = *i;
+ if (!SP_IS_SHAPE(item)) {
+ continue;
+ }
+ Inkscape::XML::Node *selrepr = item->getRepr();
+ if (selrepr) {
+ 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);
+
+ DocumentUndo::done(document, _("Set markers"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ }
+
+ /* edit marker mode - update */
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+
+ if (desktop) {
+ Inkscape::UI::Tools::MarkerTool *mt = dynamic_cast<Inkscape::UI::Tools::MarkerTool*>(desktop->event_context);
+
+ if(mt) {
+ 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 (SP_IS_GROUP(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 (!SP_IS_TEXT(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..d88c6da
--- /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..a79f9d9
--- /dev/null
+++ b/src/ui/widget/style-swatch.cpp
@@ -0,0 +1,386 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Static style swatch (fill, stroke, opacity).
+ */
+/* Authors:
+ * buliabyak@gmail.com
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2005-2008 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "style-swatch.h"
+
+#include <glibmm/i18n.h>
+#include <gtkmm/grid.h>
+
+#include "inkscape.h"
+#include "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::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);
+
+ _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);
+
+ 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 (SP_IS_LINEARGRADIENT (server)) {
+ _value[i].set_markup(_("L Gradient"));
+ place->add(_value[i]);
+ place->set_tooltip_text((i == SS_FILL)? (_("Linear gradient (fill)")) : (_("Linear gradient (stroke)")));
+ } else if (SP_IS_RADIALGRADIENT (server)) {
+ _value[i].set_markup(_("R Gradient"));
+ place->add(_value[i]);
+ place->set_tooltip_text((i == SS_FILL)? (_("Radial gradient (fill)")) : (_("Radial gradient (stroke)")));
+ } else if (SP_IS_PATTERN (server)) {
+ _value[i].set_markup(_("Pattern"));
+ place->add(_value[i]);
+ place->set_tooltip_text((i == SS_FILL)? (_("Pattern (fill)")) : (_("Pattern (stroke)")));
+ }
+
+ } else if (paint->set && paint->isColor()) {
+ guint32 color = paint->value.color.toRGBA32( SP_SCALE24_TO_FLOAT ((i == SS_FILL)? query->fill_opacity.value : query->stroke_opacity.value) );
+ ((Inkscape::UI::Widget::ColorPreview*)_color_preview[i])->setRgba32 (color);
+ _color_preview[i]->show_all();
+ place->add(*_color_preview[i]);
+ gchar *tip;
+ if (i == SS_FILL) {
+ tip = g_strdup_printf (_("Fill: %06x/%.3g"), color >> 8, SP_RGBA32_A_F(color));
+ } else {
+ tip = g_strdup_printf (_("Stroke: %06x/%.3g"), color >> 8, SP_RGBA32_A_F(color));
+ }
+ place->set_tooltip_text(tip);
+ g_free (tip);
+ } else if (paint->set && paint->isNone()) {
+ _value[i].set_markup(C_("Fill and stroke", "<i>None</i>"));
+ place->add(_value[i]);
+ place->set_tooltip_text((i == SS_FILL)? (C_("Fill and stroke", "No fill")) : (C_("Fill and stroke", "No stroke")));
+ if (i == SS_STROKE) has_stroke = false;
+ } else if (!paint->set) {
+ _value[i].set_markup(_("<b>Unset</b>"));
+ place->add(_value[i]);
+ place->set_tooltip_text((i == SS_FILL)? (_("Unset fill")) : (_("Unset stroke")));
+ if (i == SS_STROKE) has_stroke = false;
+ }
+ }
+
+// Now query stroke_width
+ if (has_stroke) {
+ 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..31165f5
--- /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);
+
+ ~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..edff1d5
--- /dev/null
+++ b/src/ui/widget/swatch-selector.cpp
@@ -0,0 +1,148 @@
+// 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 "svg/css-ostringstream.h"
+#include "svg/svg-color.h"
+
+#include "ui/icon-names.h"
+#include "ui/widget/color-notebook.h"
+#include "ui/widget/gradient-selector.h"
+
+#include "xml/node.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+SwatchSelector::SwatchSelector() :
+ Gtk::Box(Gtk::ORIENTATION_VERTICAL),
+ _gsel(nullptr),
+ _updating_color(false)
+{
+ using Inkscape::UI::Widget::ColorNotebook;
+
+ _gsel = Gtk::manage(new GradientSelector());
+ _gsel->setMode(GradientSelector::MODE_SWATCH);
+
+ _gsel->show();
+
+ pack_start(*_gsel);
+
+ auto color_selector = Gtk::manage(new ColorNotebook(_selected_color));
+ color_selector->set_label(_("Swatch color"));
+ color_selector->show();
+ pack_start(*color_selector);
+
+ //_selected_color.signal_grabbed.connect(sigc::mem_fun(this, &SwatchSelector::_grabbedCb));
+ _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));
+}
+
+SwatchSelector::~SwatchSelector()
+{
+ _gsel = nullptr;
+}
+
+GradientSelector *SwatchSelector::getGradientSelector()
+{
+ return _gsel;
+}
+
+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();
+
+
+ SPStop* stop = ngr->getFirstStop();
+ if (stop) {
+ SPColor color = _selected_color.color();
+ gfloat alpha = _selected_color.alpha();
+ guint32 rgb = color.toRGBA32( 0x00 );
+
+ // TODO replace with generic shared code that also handles icc-color
+ Inkscape::CSSOStringStream os;
+ gchar c[64];
+ sp_svg_write_color(c, sizeof(c), rgb);
+ os << "stop-color:" << c << ";stop-opacity:" << static_cast<gdouble>(alpha) <<";";
+ stop->setAttribute("style", os.str());
+
+ DocumentUndo::done(ngr->document, _("Change swatch color"), INKSCAPE_ICON("color-gradient"));
+ }
+ }
+}
+
+void SwatchSelector::connectchangedHandler( GCallback handler, void *data )
+{
+ GObject* obj = G_OBJECT(_gsel);
+ g_signal_connect( obj, "changed", handler, data );
+}
+
+void SwatchSelector::setVector(SPDocument */*doc*/, SPGradient *vector)
+{
+ //GtkVBox * box = gobj();
+ _gsel->setVector((vector) ? vector->document : nullptr, vector);
+
+ if ( vector && vector->isSolid() ) {
+ SPStop* stop = vector->getFirstStop();
+
+ guint32 const colorVal = stop->get_rgba32();
+ _updating_color = true;
+ _selected_color.setValue(colorVal);
+ _updating_color = false;
+ // gtk_widget_show_all( GTK_WIDGET(_csel) );
+ } else {
+ //gtk_widget_hide( GTK_WIDGET(_csel) );
+ }
+
+/*
+*/
+}
+
+} // 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..67486a5
--- /dev/null
+++ b/src/ui/widget/swatch-selector.h
@@ -0,0 +1,65 @@
+// 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();
+ ~SwatchSelector() override;
+
+ void connectchangedHandler( GCallback handler, void *data );
+
+ void setVector(SPDocument *doc, SPGradient *vector);
+
+ GradientSelector *getGradientSelector();
+
+private:
+ void _grabbedCb();
+ void _draggedCb();
+ void _releasedCb();
+ void _changedCb();
+
+ GradientSelector *_gsel;
+ Inkscape::UI::SelectedColor _selected_color;
+ bool _updating_color;
+};
+
+
+} // 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/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..817e783
--- /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();
+ bool saved = DocumentUndo::getUndoSensitive(doc);
+ DocumentUndo::setUndoSensitive(doc, false);
+ Inkscape::XML::Node *repr = dt->getNamedView()->getRepr();
+ repr->setAttribute(_key, os.str());
+ DocumentUndo::setUndoSensitive(doc, saved);
+
+ doc->setModifiedSinceSave();
+
+ _wr->setUpdating (false);
+}
+
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/widget/tolerance-slider.h b/src/ui/widget/tolerance-slider.h
new file mode 100644
index 0000000..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