wxruby3-shapes 0.9.0.pre.beta.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +12 -0
  3. data/CREDITS.md +18 -0
  4. data/INSTALL.md +39 -0
  5. data/LICENSE +21 -0
  6. data/README.md +118 -0
  7. data/assets/screenshot.png +0 -0
  8. data/bin/wx-shapes +9 -0
  9. data/lib/wx/shapes/arrow_base.rb +86 -0
  10. data/lib/wx/shapes/arrows/circle_arrow.rb +39 -0
  11. data/lib/wx/shapes/arrows/diamond_arrow.rb +33 -0
  12. data/lib/wx/shapes/arrows/open_arrow.rb +56 -0
  13. data/lib/wx/shapes/arrows/solid_arrow.rb +69 -0
  14. data/lib/wx/shapes/art/shape_canvas/page.xpm +73 -0
  15. data/lib/wx/shapes/auto_layout.rb +358 -0
  16. data/lib/wx/shapes/base.rb +33 -0
  17. data/lib/wx/shapes/canvas_history.rb +84 -0
  18. data/lib/wx/shapes/connection_point.rb +238 -0
  19. data/lib/wx/shapes/core.rb +19 -0
  20. data/lib/wx/shapes/diagram.rb +659 -0
  21. data/lib/wx/shapes/events.rb +389 -0
  22. data/lib/wx/shapes/printout.rb +136 -0
  23. data/lib/wx/shapes/serializable.rb +440 -0
  24. data/lib/wx/shapes/serialize/core.rb +40 -0
  25. data/lib/wx/shapes/serialize/id.rb +82 -0
  26. data/lib/wx/shapes/serialize/wx.rb +104 -0
  27. data/lib/wx/shapes/serializer/json.rb +258 -0
  28. data/lib/wx/shapes/serializer/yaml.rb +125 -0
  29. data/lib/wx/shapes/shape.rb +2129 -0
  30. data/lib/wx/shapes/shape_canvas.rb +3285 -0
  31. data/lib/wx/shapes/shape_data_object.rb +43 -0
  32. data/lib/wx/shapes/shape_handle.rb +287 -0
  33. data/lib/wx/shapes/shape_list.rb +161 -0
  34. data/lib/wx/shapes/shapes/bitmap_shape.rb +257 -0
  35. data/lib/wx/shapes/shapes/circle_shape.rb +136 -0
  36. data/lib/wx/shapes/shapes/control_shape.rb +483 -0
  37. data/lib/wx/shapes/shapes/curve_shape.rb +231 -0
  38. data/lib/wx/shapes/shapes/diamond_shape.rb +62 -0
  39. data/lib/wx/shapes/shapes/edit_text_shape.rb +317 -0
  40. data/lib/wx/shapes/shapes/ellipse_shape.rb +106 -0
  41. data/lib/wx/shapes/shapes/flex_grid_shape.rb +78 -0
  42. data/lib/wx/shapes/shapes/grid_shape.rb +404 -0
  43. data/lib/wx/shapes/shapes/line_shape.rb +907 -0
  44. data/lib/wx/shapes/shapes/multi_sel_rect.rb +214 -0
  45. data/lib/wx/shapes/shapes/ortho_shape.rb +357 -0
  46. data/lib/wx/shapes/shapes/polygon_shape.rb +294 -0
  47. data/lib/wx/shapes/shapes/rect_shape.rb +378 -0
  48. data/lib/wx/shapes/shapes/round_ortho_shape.rb +131 -0
  49. data/lib/wx/shapes/shapes/round_rect_shape.rb +142 -0
  50. data/lib/wx/shapes/shapes/square_shape.rb +119 -0
  51. data/lib/wx/shapes/shapes/text_shape.rb +324 -0
  52. data/lib/wx/shapes/thumbnail.rb +234 -0
  53. data/lib/wx/shapes/version.rb +12 -0
  54. data/lib/wx/shapes/wx.rb +29 -0
  55. data/lib/wx/shapes.rb +18 -0
  56. data/lib/wx/wx-shapes/base.rb +87 -0
  57. data/lib/wx/wx-shapes/cmd/sampler.rb +58 -0
  58. data/lib/wx/wx-shapes/cmd/test.rb +27 -0
  59. data/rakelib/yard/templates/default/fulldoc/html/css/wxruby3.css +7 -0
  60. data/rakelib/yard/templates/default/layout/html/setup.rb +5 -0
  61. data/rakelib/yard/yard/relative_markdown_links/version.rb +8 -0
  62. data/rakelib/yard/yard/relative_markdown_links.rb +39 -0
  63. data/rakelib/yard/yard-custom-templates.rb +2 -0
  64. data/rakelib/yard/yard-relative_markdown_links.rb +4 -0
  65. data/samples/demo/art/AlignBottom.xpm +35 -0
  66. data/samples/demo/art/AlignCenter.xpm +35 -0
  67. data/samples/demo/art/AlignLeft.xpm +35 -0
  68. data/samples/demo/art/AlignMiddle.xpm +35 -0
  69. data/samples/demo/art/AlignRight.xpm +35 -0
  70. data/samples/demo/art/AlignTop.xpm +35 -0
  71. data/samples/demo/art/Bitmap.xpm +25 -0
  72. data/samples/demo/art/Circle.xpm +22 -0
  73. data/samples/demo/art/Curve.xpm +21 -0
  74. data/samples/demo/art/Diamond.xpm +22 -0
  75. data/samples/demo/art/EditText.xpm +21 -0
  76. data/samples/demo/art/Ellipse.xpm +22 -0
  77. data/samples/demo/art/FixedRect.xpm +22 -0
  78. data/samples/demo/art/FlexGrid.xpm +22 -0
  79. data/samples/demo/art/GC.xpm +23 -0
  80. data/samples/demo/art/Grid.xpm +22 -0
  81. data/samples/demo/art/Line.xpm +21 -0
  82. data/samples/demo/art/NoSource.xpm +69 -0
  83. data/samples/demo/art/OrthoLine.xpm +21 -0
  84. data/samples/demo/art/Rect.xpm +22 -0
  85. data/samples/demo/art/RoundOrthoLine.xpm +21 -0
  86. data/samples/demo/art/RoundRect.xpm +22 -0
  87. data/samples/demo/art/Shadow.xpm +23 -0
  88. data/samples/demo/art/StandAloneLine.xpm +22 -0
  89. data/samples/demo/art/Text.xpm +21 -0
  90. data/samples/demo/art/Tool.xpm +23 -0
  91. data/samples/demo/art/sample.xpm +251 -0
  92. data/samples/demo/demo.rb +658 -0
  93. data/samples/demo/frame_canvas.rb +422 -0
  94. data/samples/demo/images/motyl.bmp +0 -0
  95. data/samples/demo/images/motyl2.bmp +0 -0
  96. data/samples/sample1/art/sample.xpm +251 -0
  97. data/samples/sample1/sample.rb +263 -0
  98. data/samples/sample2/art/sample.xpm +251 -0
  99. data/samples/sample2/sample.rb +133 -0
  100. data/samples/sample2/sample_canvas.rb +35 -0
  101. data/samples/sample2/sample_shape.rb +108 -0
  102. data/samples/sample3/art/sample.xpm +251 -0
  103. data/samples/sample3/sample.rb +281 -0
  104. data/samples/sample4/art/sample.xpm +251 -0
  105. data/samples/sample4/sample.rb +180 -0
  106. data/tests/art/motyl.bmp +0 -0
  107. data/tests/lib/wxapp_runner.rb +64 -0
  108. data/tests/serializer_tests.rb +521 -0
  109. data/tests/test_grid_shapes.rb +42 -0
  110. data/tests/test_serialize.rb +7 -0
  111. data/tests/test_serialize_yaml.rb +17 -0
  112. metadata +242 -0
@@ -0,0 +1,3285 @@
1
+ # Wx::SF::ShapeCanvas - shape canvas class
2
+ # Copyright (c) M.J.N. Corino, The Netherlands
3
+
4
+ require 'wx/shapes/shape_data_object'
5
+ require 'wx/shapes/canvas_history'
6
+ require 'wx/shapes/printout'
7
+
8
+ require 'tempfile'
9
+ require 'fileutils'
10
+
11
+ module Wx::SF
12
+
13
+ if Wx.has_feature?(:USE_DRAG_AND_DROP)
14
+
15
+ # Auxiliary class encapsulating shape drop target.
16
+ class CanvasDropTarget < Wx::DropTarget
17
+
18
+ # @param [Wx::DataObject] data
19
+ # @param [Wx::SF::ShapeCanvas] parent
20
+ def initialize(data, parent)
21
+ super(data)
22
+ @parent_canvas = parent
23
+ end
24
+
25
+ # @param [Integer] x
26
+ # @param [Integer] y
27
+ # @param [Wx::DragResult] deflt
28
+ # @return [Wx::DragResult]
29
+ def on_data(x, y, deflt)
30
+ return Wx::DragResult::DragNone unless get_data
31
+
32
+ @parent_canvas.__send__(:_on_drop, x, y, deflt, get_data_object)
33
+ deflt
34
+ end
35
+
36
+ end
37
+ end
38
+
39
+ # Class encapsulating a Shape canvas. The shape canvas is window control
40
+ # which extends the Wx::ScrolledWindow and is responsible for displaying of shapes diagrams.
41
+ # It also supports clipboard and drag&drop operations, undo/redo operations,
42
+ # and graphics exporting functions.
43
+ #
44
+ # This class is a core framework class and provides many member functions suitable for adding,
45
+ # removing, moving, resizing and drawing of shape objects. It can be used as it is or as a base class
46
+ # if necessary. In that case, the default class functionality can be enhanced by overriding
47
+ # its methods or by manual events handling. In both cases the user is responsible
48
+ # for invoking of default event handlers/virtual functions otherwise the
49
+ # built in functionality wont be available.
50
+ # @see Wx::SF::Diagram
51
+ class ShapeCanvas < Wx::ScrolledWindow
52
+
53
+ # Working modes
54
+ class MODE < Wx::Enum
55
+ # The shape canvas is in ready state (no operation is pending)
56
+ READY = self.new(0)
57
+ # Some shape handle is dragged
58
+ HANDLEMOVE = self.new(1)
59
+ # Handle of multiselection tool is dragged
60
+ MULTIHANDLEMOVE = self.new(2)
61
+ # Some shape/s is/are dragged
62
+ SHAPEMOVE = self.new(3)
63
+ # Multiple shape selection is in progress
64
+ MULTISELECTION = self.new(4)
65
+ # Interactive connection creation is in progress
66
+ CREATECONNECTION = self.new(5)
67
+ # Canvas is in the Drag&Drop mode
68
+ DND = self.new(6)
69
+ end
70
+
71
+ # Selection modes
72
+ class SELECTIONMODE < Wx::Enum
73
+ NORMAL = self.new(0)
74
+ ADD = self.new(1)
75
+ REMOVE = self.new(2)
76
+ end
77
+
78
+ # Search mode flags for GetShapeAtPosition function
79
+ class SEARCHMODE < Wx::Enum
80
+ # Search for selected shapes only
81
+ SELECTED = self.new(0)
82
+ # Search for unselected shapes only
83
+ UNSELECTED = self.new(1)
84
+ # Search for both selected and unselected shapes
85
+ BOTH = self.new(2)
86
+ end
87
+
88
+ # Flags for AlignSelected function
89
+ class VALIGN < Wx::Enum
90
+ NONE = self.new(0)
91
+ TOP = self.new(1)
92
+ MIDDLE = self.new(2)
93
+ BOTTOM = self.new(3)
94
+ end
95
+
96
+ # Flags for AlignSelected function
97
+ class HALIGN < Wx::Enum
98
+ NONE = self.new(0)
99
+ LEFT = self.new(1)
100
+ CENTER = self.new(2)
101
+ RIGHT = self.new(3)
102
+ end
103
+
104
+ # Style flags
105
+ class STYLE < Wx::Enum
106
+ # Allow multiselection box.
107
+ MULTI_SELECTION = self.new(1)
108
+ # Allow shapes' size change done via the multiselection box.
109
+ MULTI_SIZE_CHANGE = self.new(2)
110
+ # Show grid.
111
+ GRID_SHOW = self.new(4)
112
+ # Use grid.
113
+ GRID_USE = self.new(8)
114
+ # Enable Drag & Drop operations.
115
+ DND = self.new(16)
116
+ # Enable Undo/Redo operations.
117
+ UNDOREDO = self.new(32)
118
+ # Enable the clipboard.
119
+ CLIPBOARD = self.new(64)
120
+ # Enable mouse hovering
121
+ HOVERING = self.new(128)
122
+ # Enable highlighting of shapes able to accept dragged shape(s).
123
+ HIGHLIGHTING = self.new(256)
124
+ # Use gradient color for the canvas background.
125
+ GRADIENT_BACKGROUND = self.new(512)
126
+ # Print also canvas background.
127
+ PRINT_BACKGROUND = self.new(1024)
128
+ # Process mouse wheel by the canvas (canvas scale will be changed).
129
+ PROCESS_MOUSEWHEEL = self.new(2048)
130
+ # Default canvas style.
131
+ DEFAULT_CANVAS_STYLE = MULTI_SELECTION | MULTI_SIZE_CHANGE | DND | UNDOREDO | CLIPBOARD | HOVERING | HIGHLIGHTING
132
+ end
133
+
134
+ # Flags for ShowShadow function.
135
+ class SHADOWMODE < Wx::Enum
136
+ # Show/hide shadow under topmost shapes only.
137
+ TOPMOST = self.new(0)
138
+ # Show/hide shadow under all shapes in the diagram.
139
+ ALL = self.new(1)
140
+ end
141
+
142
+ # Change mode flags
143
+ class CHANGE < Wx::Enum
144
+ SET_SCALE = self.new(0)
145
+ RESCALED = self.new(1)
146
+ VIRTUAL_SIZE = self.new(2)
147
+ FOCUS = self.new(3)
148
+ end
149
+
150
+ # Printing modes used by SetPrintMode() function.
151
+ class PRINTMODE < Wx::Enum
152
+ # This sets the user scale and origin of the DC so that the image fits
153
+ # within the paper rectangle (but the edges could be cut off by printers
154
+ # that can't print to the edges of the paper -- which is most of them. Use
155
+ # this if your image already has its own margins.
156
+ FIT_TO_PAPER = self.new(0)
157
+ # This sets the user scale and origin of the DC so that the image fits
158
+ # within the page rectangle, which is the printable area on Mac and MSW
159
+ # and is the entire page on other platforms.
160
+ FIT_TO_PAGE = self.new(1)
161
+ # This sets the user scale and origin of the DC so that the image fits
162
+ # within the page margins as specified by g_PageSetupData, which you can
163
+ # change (on some platforms, at least) in the Page Setup dialog. Note that
164
+ # on Mac, the native Page Setup dialog doesn't let you change the margins
165
+ # of a Wx::PageSetupDialogData object, so you'll have to write your own dialog or
166
+ # use the Mac-only Wx::MacPageMarginsDialog, as we do in this program.
167
+ FIT_TO_MARGINS = self.new(2)
168
+ # This sets the user scale and origin of the DC so that you could map the
169
+ # screen image to the entire paper at the same size as it appears on screen.
170
+ MAP_TO_PAPER = self.new(3)
171
+ # This sets the user scale and origin of the DC so that the image appears
172
+ # on the paper at the same size that it appears on screen (i.e., 10-point
173
+ # type on screen is 10-point on the printed page).
174
+ MAP_TO_PAGE = self.new(4)
175
+ # This sets the user scale and origin of the DC so that you could map the
176
+ # screen image to the page margins specified by the native Page Setup dialog at the same
177
+ # size as it appears on screen.
178
+ MAP_TO_MARGINS = self.new(5)
179
+ # This sets the user scale and origin of the DC so that you can to do you own
180
+ # scaling in order to draw objects at full native device resolution.
181
+ MAP_TO_DEVICE = self.new(6)
182
+ end
183
+
184
+ class PRECON_FINISH_STATE < Wx::Enum
185
+ # Finish line connection.
186
+ OK = self.new(0)
187
+ # Cancel line connection and abort the interactive connection.
188
+ FAILED_AND_CANCEL_LINE = self.new(1)
189
+ # Cancel line connection and continue with the interactive connection.
190
+ FAILED_AND_CONTINUE_EDIT = self.new(2)
191
+ end
192
+
193
+ # Default values
194
+ module DEFAULT
195
+ # Default value of Wx::SF::CanvasSettings @background_color data member
196
+ BACKGROUNDCOLOR = Wx::Colour.new(240, 240, 240) if Wx::App.is_main_loop_running
197
+ Wx.add_delayed_constant(self, :BACKGROUNDCOLOR) { Wx::Colour.new(240, 240, 240) }
198
+ # Default value of Wx::SF::CanvasSettings @grid_size data member
199
+ GRIDSIZE = Wx::Size.new(10, 10)
200
+ # Default value of Wx::SF::CanvasSettings @grid_line_mult data member
201
+ GRIDLINEMULT = 1
202
+ # Default value of Wx::SF::CanvasSettings @grid_color data member
203
+ GRIDCOLOR = Wx::Colour.new(200, 200, 200) if Wx::App.is_main_loop_running
204
+ Wx.add_delayed_constant(self, :GRIDCOLOR) { Wx::Colour.new(200, 200, 200) }
205
+ # Default value of Wx::SF::CanvasSettings @grid_style data member
206
+ GRIDSTYLE = Wx::PenStyle::PENSTYLE_SOLID
207
+ # Default value of Wx::SF::CanvasSettings @common_hover_color data member
208
+ HOVERCOLOR = Wx::Colour.new(120, 120, 255) if Wx::App.is_main_loop_running
209
+ Wx.add_delayed_constant(self, :HOVERCOLOR) { Wx::Colour.new(120, 120, 255) }
210
+ # Default value of Wx::SF::CanvasSettings @gradient_from data member
211
+ GRADIENT_FROM = Wx::Colour.new(240, 240, 240) if Wx::App.is_main_loop_running
212
+ Wx.add_delayed_constant(self, :GRADIENT_FROM) { Wx::Colour.new(240, 240, 240) }
213
+ # Default value of Wx::SF::CanvasSettings @gradient_to data member
214
+ GRADIENT_TO = Wx::Colour.new(200, 200, 255) if Wx::App.is_main_loop_running
215
+ Wx.add_delayed_constant(self, :GRADIENT_TO) { Wx::Colour.new(200, 200, 255) }
216
+ # Default value of Wx::SF::CanvasSettings @style data member
217
+ CANVAS_STYLE = STYLE::DEFAULT_CANVAS_STYLE
218
+ # Default value of Wx::SF::CanvasSettings @shadow_offset data member
219
+ SHADOWOFFSET = Wx::RealPoint.new(4, 4)
220
+ # Default shadow colour
221
+ SHADOWCOLOR = Wx::Colour.new(150, 150, 150, 128) if Wx::App.is_main_loop_running
222
+ Wx.add_delayed_constant(self, :SHADOWCOLOR) { Wx::Colour.new(150, 150, 150, 128) }
223
+ # Default value of Wx::SF::CanvasSettings @shadow_fill data member
224
+ SHADOWBRUSH = Wx::Brush.new(SHADOWCOLOR.call, Wx::BrushStyle::BRUSHSTYLE_SOLID) if Wx::App.is_main_loop_running
225
+ Wx.add_delayed_constant(self, :SHADOWBRUSH) { Wx::Brush.new(Wx::Colour.new(150, 150, 150, 128), Wx::BrushStyle::BRUSHSTYLE_SOLID) }
226
+ # Default value of Wx::SF::CanvasSettings @print_h_align data member
227
+ PRINT_HALIGN = HALIGN::CENTER
228
+ # Default value of Wx::SF::CanvasSettings @print_v_align data member
229
+ PRINT_VALIGN = VALIGN::MIDDLE
230
+ # Default value of Wx::SF::CanvasSettings @print_mode data member
231
+ PRINT_MODE = PRINTMODE::FIT_TO_MARGINS
232
+ # Default value of Wx::SF::CanvasSettings @min_scale data member
233
+ SCALE_MIN = 0.1
234
+ # Default value of Wx::SF::CanvasSettings @max_scale data member
235
+ SCALE_MAX = 5.0
236
+ end
237
+
238
+ class << self
239
+
240
+ def gc_enabled?
241
+ if @gc_enabled.nil?
242
+ @gc_enabled = Wx.has_feature?(:USE_GRAPHICS_CONTEXT)
243
+ end
244
+ @gc_enabled
245
+ end
246
+
247
+ def enable_gc(f = true)
248
+ if Wx.has_feature?(:USE_GRAPHICS_CONTEXT)
249
+ @gc_enabled = f
250
+ else
251
+ @gc_enabled = false
252
+ Wx.log_warning(%Q{Couldn't enable Graphics context due to missing USE_GRAPHICS_CONTEXT})
253
+ end
254
+ end
255
+
256
+ TLS_LOADING_VERSION_KEY = :loading_version.freeze
257
+ private_constant :TLS_LOADING_VERSION_KEY
258
+
259
+ def compat_loading?
260
+ !!::Thread::current[TLS_LOADING_VERSION_KEY]
261
+ end
262
+
263
+ def compat_loading_version
264
+ ::Thread::current[TLS_LOADING_VERSION_KEY]
265
+ end
266
+
267
+ def set_compat_loading(ver_info)
268
+ ::Thread::current[TLS_LOADING_VERSION_KEY] = ver_info
269
+ end
270
+
271
+ def reset_compat_loading
272
+ ::Thread::current[TLS_LOADING_VERSION_KEY] = nil
273
+ end
274
+
275
+ def print_data
276
+ _init_printing unless @print_data
277
+ @print_data
278
+ end
279
+
280
+ def print_data=(pd)
281
+ @print_data = pd
282
+ end
283
+
284
+ def page_setup_data
285
+ _init_printing unless @page_setup_data
286
+ @page_setup_data
287
+ end
288
+
289
+ def page_setup_data=(psd)
290
+ @page_setup_data = psd
291
+ end
292
+
293
+ def _init_printing
294
+ @print_data = Wx::PRT::PrintData.new
295
+ # You could set an initial paper size here
296
+ # @print_data.set_paper_id(Wx::PaperSize::PAPER_LETTER) # for Americans
297
+ @print_data.set_paper_id(Wx::PaperSize::PAPER_A4) # for everyone else
298
+
299
+ # copy over initial paper size from print record
300
+ @page_setup_data = Wx::PRT::PageSetupDialogData.new(@print_data)
301
+ # Set some initial page margins in mm.
302
+ @page_setup_data.set_margin_top_left([15, 15])
303
+ @page_setup_data.set_margin_bottom_right([15, 15])
304
+ end
305
+ private :_init_printing
306
+
307
+ end
308
+
309
+ # Auxiliary serializable class encapsulating canvas version info
310
+ # and providing version check on loading.
311
+ class Version
312
+
313
+ class Exception < SFException; end
314
+
315
+ VersionInfo = ::Struct.new(:major, :minor, :release) do
316
+ def to_s
317
+ "#{major}.#{minor}.#{release}"
318
+ end
319
+ end
320
+
321
+ include Serializable
322
+
323
+ property :version_info
324
+
325
+ def initialize
326
+ # get version numbers as [major, minor, release]
327
+ @version_info = VersionInfo.new(*Wx::SF::VERSION.split(/\D/).shift(3).collect { |s| s.to_i })
328
+ end
329
+
330
+ attr_reader :version_info
331
+
332
+ # Deserialization only.
333
+ def set_version_info(ver_info)
334
+ if @version_info.major < ver_info.major ||
335
+ (@version_info.major == ver_info.major && @version_info.minor < ver_info.minor)
336
+ ::Kernel.raise Version::Exception, "Incompatible Wx::SF diagram version #{ver_info} cannot be loaded (current Wx::SF version #{@version_info})."
337
+ elsif @version_info.major > ver_info.major ||
338
+ (@version_info.major == ver_info.major && @version_info.minor > ver_info.minor)
339
+ # this should normally work but may give trouble
340
+ # set compat loading info
341
+ ShapeCanvas.set_compat_loading(ver_info)
342
+ else
343
+ # this should never give any trouble
344
+ end
345
+ end
346
+ private :set_version_info
347
+
348
+ end
349
+
350
+ # Auxiliary serializable class encapsulating the canvas properties.
351
+ class Settings
352
+
353
+ include Serializable
354
+
355
+ include DEFAULT
356
+
357
+ property :scale, :min_scale, :max_scale, :background_color, :common_hover_color,
358
+ :grid_size, :grid_line_mult, :grid_color, :grid_style,
359
+ :gradient_from, :gradient_to, :style, :shadow_offset, :shadow_fill,
360
+ :print_h_align, :print_v_align, :print_mode
361
+
362
+ def initialize
363
+ @scale = 1.0
364
+ @min_scale = SCALE_MIN
365
+ @max_scale = SCALE_MAX
366
+ @background_color = BACKGROUNDCOLOR
367
+ @common_hover_color = HOVERCOLOR
368
+ @grid_size = GRIDSIZE.dup
369
+ @grid_line_mult = GRIDLINEMULT
370
+ @grid_color = GRIDCOLOR
371
+ @grid_style = GRIDSTYLE
372
+ @gradient_from = GRADIENT_FROM
373
+ @gradient_to = GRADIENT_TO
374
+ @style = CANVAS_STYLE
375
+ @shadow_offset = SHADOWOFFSET.dup
376
+ @shadow_fill = SHADOWBRUSH
377
+ @print_h_align = PRINT_HALIGN
378
+ @print_v_align = PRINT_VALIGN
379
+ @print_mode = PRINT_MODE
380
+ end
381
+
382
+ attr_accessor :scale, :min_scale, :max_scale, :background_color, :common_hover_color,
383
+ :grid_size, :grid_line_mult, :grid_color, :grid_style,
384
+ :gradient_from, :gradient_to, :style, :shadow_offset, :shadow_fill,
385
+ :print_h_align, :print_v_align, :print_mode
386
+
387
+ end
388
+
389
+ # @overload initialize()
390
+ # Default constructor
391
+ # @overload initialize(diagram, parent, id = Wx::ID_ANY, pos = Wx::DEFAULT_POSITION, size = Wx::DEFAULT_SIZE, style = Wx::HSCROLL | Wx::VSCROLL)
392
+ # Constructor
393
+ # @param [Wx::SF::Diagram] diagram shape diagram
394
+ # @param [Wx::Window] parent Parent window
395
+ # @param [Integer] id Window ID
396
+ # @param [Wx::Point] pos Initial position
397
+ # @param [Wx::Size] size Initial size
398
+ # @param [Integer] style Window style
399
+ def initialize(diagram = nil, *mixed_args)
400
+ super()
401
+
402
+ @dnd_started_here = false
403
+ @dnd_started_at = nil
404
+ @can_save_state_on_mouse_up = false
405
+ @working_mode = MODE::READY
406
+ @selection_mode = SELECTIONMODE::NORMAL
407
+ @selected_handle = nil
408
+ @selection_start = Wx::RealPoint.new
409
+ @new_line_shape = nil
410
+ @unselected_shape_under_cursor = nil
411
+ @selected_shape_under_cursor = nil
412
+ @topmost_shape_under_cursor = nil
413
+ @current_shapes = []
414
+ @invalidate_rect = nil
415
+
416
+ @prev_mouse_pos = Wx::Point.new
417
+ @prev_positions = {}
418
+
419
+ @settings = Settings.new
420
+ @canvas_history = CanvasHistory.new
421
+
422
+ if diagram
423
+ parent = mixed_args.first.is_a?(Wx::Window) ? mixed_args.shift : nil
424
+ real_args = []
425
+ begin
426
+ real_args = [parent] + Wx::ScrolledWindow.args_as_list(*mixed_args)
427
+ create(*real_args)
428
+ rescue => err
429
+ msg = "Error initializing #{self.inspect}\n" +
430
+ " : #{err.message} \n" +
431
+ "Provided are #{real_args} \n" +
432
+ "Correct parameters for #{self.class.name}.new are:\n" +
433
+ self.class.describe_constructor
434
+
435
+ new_err = err.class.new(msg)
436
+ new_err.set_backtrace(caller)
437
+ Kernel.raise new_err
438
+ end
439
+
440
+ self.diagram = diagram
441
+
442
+ save_canvas_state
443
+ end
444
+
445
+ # set up event handlers
446
+ evt_paint :_on_paint
447
+ evt_erase_background :_on_erase_background
448
+ evt_left_down :_on_left_down
449
+ evt_left_up :_on_left_up
450
+ evt_right_down :_on_right_down
451
+ evt_right_up :_on_right_up
452
+ evt_left_dclick :_on_left_double_click
453
+ evt_right_dclick :_on_right_double_click
454
+ evt_motion :_on_mouse_move
455
+ evt_mousewheel :_on_mouse_wheel
456
+ evt_key_down :_on_key_down
457
+ evt_enter_window :_on_enter_window
458
+ evt_leave_window :_on_leave_window
459
+ evt_size :_on_resize
460
+ end
461
+
462
+ # Creates the window in two-step construction mode. set_diagram() function must also be called to complete the canvas initialization.
463
+ # @param [Wx::Window] parent Parent window
464
+ # @param [Integer] id Window ID
465
+ # @param [Wx::Point] pos Initial position
466
+ # @param [Wx::Size] size Initial size
467
+ # @param [Integer] style Window style
468
+ # @param [String] name Window name
469
+ def create(parent, id = -1, pos = Wx::DEFAULT_POSITION, size = Wx::DEFAULT_SIZE, style = (Wx::HSCROLL | Wx::VSCROLL), name = "Wx::ScrolledWindow")
470
+ # NOTE: user must call Wx::SF::ShapeCanvas#set_diagram() to complete
471
+ # canvas initialization!
472
+
473
+ # perform basic window initialization
474
+ super
475
+
476
+ # set drop target
477
+ if Wx.has_feature?(:USE_DRAG_AND_DROP)
478
+ set_drop_target(Wx::SF::CanvasDropTarget.new(Wx::SF::ShapeDataObject.new, self))
479
+ end
480
+
481
+ # initialize selection rectangle
482
+ @shp_selection = MultiSelRect.new
483
+ @shp_selection.send(:set_id, nil)
484
+ @shp_selection.create_handles
485
+ @shp_selection.select(true)
486
+ @shp_selection.show(false)
487
+ @shp_selection.show_handles(true)
488
+
489
+ # initialize multi-edit rectangle
490
+ @shp_multi_edit = MultiSelRect.new
491
+ @shp_multi_edit.send(:set_id, nil)
492
+ @shp_multi_edit.create_handles
493
+ @shp_multi_edit.select(true)
494
+ @shp_multi_edit.show(false)
495
+ @shp_multi_edit.show_handles(true)
496
+
497
+ set_scrollbars(5, 5, 100, 100)
498
+ set_background_style(Wx::BG_STYLE_PAINT)
499
+
500
+ true
501
+ end
502
+
503
+ attr_reader :settings
504
+
505
+ # Returns the shape diagram which shapes are displayed on this canvas.
506
+ # @return [Wx::SF::Diagram]
507
+ def get_diagram
508
+ @diagram
509
+ end
510
+ alias :diagram :get_diagram
511
+
512
+ # Set the shape diagram to display on this canvas
513
+ # @param [Wx::SF::Diagram] diagram
514
+ def set_diagram(diagram)
515
+ @diagram = diagram
516
+ @shp_selection.set_diagram(@diagram)
517
+ @shp_multi_edit.set_diagram(@diagram)
518
+ @diagram.shape_canvas = self if @diagram
519
+ clear_temporaries
520
+ @diagram.update_all
521
+ end
522
+
523
+ # Load serialized canvas content (diagrams).
524
+ # @overload load_canvas(file)
525
+ # @param [String] file Full file name
526
+ # @return [self]
527
+ # @overload load_canvas(io)
528
+ # @param [IO] io IO object
529
+ # @return [self]
530
+ def load_canvas(io)
531
+ # get IO stream to read from
532
+ ios = io.is_a?(::String) ? File.open(io, 'r') : io
533
+ begin
534
+ _, @settings, diagram = Serializable.deserialize(ios)
535
+ rescue SFException
536
+ ::Kernel.raise
537
+ rescue ::Exception
538
+ ::Kernel.raise SFException, "Failed to load canvas: #{$!.message}"
539
+ ensure
540
+ ShapeCanvas.reset_compat_loading
541
+ ios.close if io.is_a?(::String) && ios
542
+ end
543
+ set_diagram(diagram)
544
+ clear_canvas_history
545
+ save_canvas_state
546
+ set_scale(@settings.scale)
547
+ update_virtual_size
548
+ refresh(false)
549
+
550
+ @diagram.set_modified(false)
551
+
552
+ self
553
+ end
554
+
555
+ # Save canvas content (diagrams).
556
+ # @overload save_canvas(file, compact: true)
557
+ # @param [String] file Full file name
558
+ # @param [Boolean] compact specifies whether to write content in compact mode (true) or not (false)
559
+ # @return [self]
560
+ # @overload save_canvas(io, compact: true)
561
+ # @param [IO] io IO object
562
+ # @param [Boolean] compact specifies whether to write content in compact mode (true) or not (false)
563
+ # @return [self]
564
+ def save_canvas(io, compact: true)
565
+ return self unless @diagram
566
+ # get IO stream to write to
567
+ ios = io.is_a?(::String) ? Tempfile.new(File.basename(io, '.*')) : io
568
+ # write canvas data to temp file
569
+ begin
570
+ [Version.new, @settings, @diagram].serialize(ios, pretty: !compact)
571
+ rescue SFException
572
+ ::Kernel.raise
573
+ rescue Exception
574
+ ::Kernel.raise SFException, "Error writing canvas: #{$!.message}"
575
+ end
576
+ if io.is_a?(::String)
577
+ ios.close(false) # close but keep temp file
578
+ full_path = File.absolute_path(io)
579
+ if File.exist?(full_path)
580
+ # create temporary backup
581
+ ftmp = Tempfile.new(File.basename(io))
582
+ ftmp_name = ftmp.path.dup
583
+ ftmp.close(true) # close AND unlink
584
+ FileUtils::mv(full_path, ftmp_name) # backup existing file
585
+ # replace original
586
+ begin
587
+ # rename newly generated file
588
+ FileUtils.mv(ios.path, full_path)
589
+ # preserve file mode
590
+ FileUtils.chmod(File.lstat(ftmp_name).mode, full_path)
591
+ rescue Exception
592
+ # restore backup
593
+ FileUtils.mv(ftmp_name, full_path)
594
+ ::Kernel.raise SFException, "Unable to save canvas file #{io}: #{$!.message}"
595
+ end
596
+ # remove backup
597
+ FileUtils.rm_f(ftmp_name)
598
+ else
599
+ begin
600
+ # rename newly generated file
601
+ FileUtils.mv(ios.path, full_path)
602
+ rescue Exception
603
+ ::Kernel.raise SFException, "Unable to save canvas file #{io}: #{$!.message}"
604
+ end
605
+ end
606
+ end
607
+
608
+ @diagram.set_modified(false)
609
+
610
+ self
611
+ end
612
+
613
+ # @overload save_canvas_to_image(type: Wx::BITMAP_TYPE_BMP, background: true, scale: -1.0)
614
+ # Export canvas content to image.
615
+ # @param [Wx::BitmapType] type Image type. See Wx::BitmapType for more details. Default type is Wx::BITMAP_TYPE_BMP.
616
+ # @param [Boolean] background Export also diagram background
617
+ # @param [Float] scale Image scale. If -1 then current canvas scale id used.
618
+ # @return [Wx::Bitmap,nil] exported canvas image or nil if failed to create bitmap
619
+ # @overload save_canvas_to_image(file, type: Wx::BITMAP_TYPE_BMP, background: true, scale: -1.0)
620
+ # Export canvas content to image file.
621
+ # @param [String] file Full file name
622
+ # @param [Wx::BitmapType] type Image type. See Wx::BitmapType for more details. Default type is Wx::BITMAP_TYPE_BMP.
623
+ # @param [Boolean] background Export also diagram background
624
+ # @param [Float] scale Image scale. If -1 then current canvas scale id used.
625
+ # @return [Boolean] true if saving the image to file succeeded, false otherwise
626
+ def save_canvas_to_image(file = nil, type: Wx::BITMAP_TYPE_BMP, background: true, scale: -1.0)
627
+ # create memory DC a draw the canvas content into
628
+
629
+ prev_scale = get_scale
630
+ scale = prev_scale if scale == -1
631
+
632
+ bmp_bb = get_total_bounding_box
633
+
634
+ bmp_bb.left = (bmp_bb.left * scale).to_i
635
+ bmp_bb.top = (bmp_bb.top * scale).to_i
636
+ bmp_bb.width = (bmp_bb.width * scale).to_i
637
+ bmp_bb.height = (bmp_bb.height * scale).to_i
638
+
639
+ bmp_bb.inflate!(@settings.grid_size * scale)
640
+
641
+ outbmp = Wx::Bitmap.new(bmp_bb.width, bmp_bb.height)
642
+ Wx::MemoryDC.draw_on(outbmp) do |mdc|
643
+
644
+ Wx::ScaledDC.draw_on(mdc, scale) do |outdc|
645
+
646
+ if outdc.ok?
647
+ set_scale(scale) if scale != prev_scale
648
+
649
+ outdc.set_device_origin(-bmp_bb.left, -bmp_bb.top)
650
+
651
+ prev_style = get_style
652
+ prev_colour = get_canvas_colour
653
+
654
+ unless background
655
+ remove_style(STYLE::GRADIENT_BACKGROUND)
656
+ remove_style(STYLE::GRID_SHOW)
657
+ set_canvas_colour(Wx::WHITE)
658
+ end
659
+
660
+ draw_background(outdc, NOT_FROM_PAINT)
661
+ draw_content(outdc, NOT_FROM_PAINT)
662
+ draw_foreground( outdc, NOT_FROM_PAINT)
663
+
664
+ unless background
665
+ set_style(prev_style)
666
+ set_canvas_colour(prev_colour)
667
+ end
668
+
669
+ set_scale(prev_scale) if scale != prev_scale
670
+
671
+ if file
672
+ return outbmp.save_file(file, type)
673
+ else
674
+ return outbmp
675
+ end
676
+ elsif file
677
+ Wx.message_box('Could not create output bitmap.', 'wxRuby ShapeFramework', Wx::OK | Wx::ICON_ERROR)
678
+ end
679
+ end
680
+ end
681
+ nil
682
+ end
683
+
684
+ def _start_interactive_connection(lpos, src_shape_id, cpt)
685
+ if @new_line_shape
686
+ @working_mode = MODE::CREATECONNECTION
687
+ @new_line_shape.send(:set_line_mode, LineShape::LINEMODE::UNDERCONSTRUCTION)
688
+
689
+ @new_line_shape.set_src_shape_id(src_shape_id)
690
+
691
+ # switch on the "under-construction" mode
692
+ @new_line_shape.send(:set_unfinished_point, lpos)
693
+ # assign starting point of new line shapes to the nearest connection point of
694
+ # connected shape if exists
695
+ @new_line_shape.set_starting_connection_point(cpt)
696
+ ERRCODE::OK
697
+ else
698
+ ERRCODE::NOT_CREATED
699
+ end
700
+ end
701
+ private :_start_interactive_connection
702
+
703
+ # Start interactive connection creation.
704
+ #
705
+ # This function switches the canvas to a mode in which a new shape connection
706
+ # can be created interactively (by mouse operations). Every connection must
707
+ # start and finish in some shape object or another connection. At the end of the
708
+ # process the on_connection_finished event handler is invoked so the user can
709
+ # set needed connection properties immediately.
710
+ #
711
+ # Function must be called from mouse event handler and the event must be passed
712
+ # to the function.
713
+ # @overload start_interactive_connection(shape_info, pos)
714
+ # @param [Class] shape_info Connection type
715
+ # @param [Wx::Point] pos Position where to start
716
+ # @return [Wx::SF::ERRCODE] operation result
717
+ # @overload start_interactive_connection(shape, pos)
718
+ # @param [Wx::SF::LineShape] shape existing line shape object which will be used as a connection.
719
+ # @param [Wx::Point] pos Position where to start
720
+ # @return [Wx::SF::ERRCODE] err operation result
721
+ # @overload start_interactive_connection(shape, connection_point, pos)
722
+ # @param [Wx::SF::LineShape] shape existing line shape object which will be used as a connection.
723
+ # @param [Wx::SF::ConnectionPoint] connection_point Initial connection point
724
+ # @param [Wx::Point] pos Position where to start
725
+ # @return [Wx::SF::ERRCODE] err operation result
726
+ # @see create_connection
727
+ def start_interactive_connection(*args)
728
+ return ERRCODE::INVALID_INPUT unless @diagram
729
+
730
+ shape_info = shape = pos = connection_point = nil
731
+ shape_klass = nil
732
+ case args.first
733
+ when Wx::SF::LineShape
734
+ shape = args.shift
735
+ shape_klass = shape.class.name
736
+ if args.first.is_a?(Wx::SF::ConnectionPoint)
737
+ connection_point = args.shift
738
+ end
739
+ pos = args.shift.to_point
740
+ when ::Class
741
+ shape_info = args.shift
742
+ pos = args.shift.to_point
743
+ shape_klass = shape_info.name
744
+ end
745
+ ::Kernel.raise ArgumentError, "Invalid arguments #{args}" unless args.empty?
746
+ return ERRCODE::INVALID_INPUT unless pos
747
+
748
+ lpos = dp2lp(pos)
749
+
750
+ if @working_mode == MODE::READY && ((shape_info && shape_info <= Wx::SF::LineShape) || (shape.is_a?(Wx::SF::LineShape)))
751
+
752
+ if connection_point
753
+
754
+ if @diagram.contains?(shape)
755
+ @new_line_shape = shape
756
+ else
757
+ @new_line_shape = @diagram.add_shape(shape, nil, Wx::DEFAULT_POSITION, INITIALIZE, DONT_SAVE_STATE)
758
+ end
759
+ return _start_interactive_connection(lpos, connection_point.get_parent_shape.id, connection_point)
760
+
761
+ else
762
+
763
+ shape_under = get_shape_at_position(lpos)
764
+ if shape_info
765
+ # propagate request for interactive connection if requested
766
+ shape_under = shape_under.get_parent_shape while shape_under &&
767
+ shape_under.has_style?(Shape::STYLE::PROPAGATE_INTERACTIVE_CONNECTION)
768
+ end
769
+
770
+ # start the connection's creation process if possible
771
+ if shape_under&.id && shape_under.is_connection_accepted(shape_klass)
772
+ if shape && @diagram.contains?(shape)
773
+ @new_line_shape = shape
774
+ else
775
+ if shape
776
+ err = @diagram.add_shape(shape, nil, Wx::DEFAULT_POSITION, INITIALIZE, DONT_SAVE_STATE)
777
+ else
778
+ err, shape = @diagram.create_shape(shape_info, DONT_SAVE_STATE)
779
+ end
780
+ @new_line_shape = (err == ERRCODE::OK ? shape : nil)
781
+ end
782
+ return _start_interactive_connection(lpos, shape_under.id, shape_under.get_nearest_connection_point(lpos.to_real))
783
+ else
784
+ return ERRCODE::NOT_ACCEPTED
785
+ end
786
+
787
+ end
788
+ end
789
+ ERRCODE::INVALID_INPUT
790
+ end
791
+
792
+ # Abort interactive connection creation process
793
+ def abort_interactive_connection
794
+ return unless @diagram
795
+
796
+ if @new_line_shape
797
+ @diagram.remove_shape(@new_line_shape)
798
+ @new_line_shape = nil
799
+ on_connection_finished(nil)
800
+ end
801
+ @working_mode = MODE::READY
802
+ refresh(false)
803
+ end
804
+
805
+ # Select all shapes in the canvas
806
+ def select_all
807
+ return unless @diagram
808
+
809
+ shapes = @diagram.get_shapes
810
+
811
+ unless shapes.empty?
812
+ shapes.each { |shape| shape.select(true) }
813
+
814
+ validate_selection(get_selected_shapes)
815
+
816
+ hide_all_handles
817
+ update_multiedit_size
818
+ @shp_multi_edit.show(true)
819
+ @shp_multi_edit.show_handles(true)
820
+
821
+ refresh(false)
822
+ end
823
+ end
824
+
825
+ # Deselect all shapes
826
+ def deselect_all
827
+ return unless @diagram
828
+
829
+ @diagram.get_shapes.each { |shape| shape.select(false) }
830
+
831
+ @shp_multi_edit.show(false)
832
+ end
833
+
834
+ # Hide handles of all shapes
835
+ def hide_all_handles
836
+ return unless @diagram
837
+
838
+ @diagram.get_shapes.each { |shape| shape.show_handles(false) }
839
+ end
840
+
841
+ # Repaint the shape canvas.
842
+ # @param [Boolean] erase true if the canvas should be erased before repainting
843
+ # @param [Wx::Rect] rct Refreshed region (rectangle)
844
+ def refresh_canvas(erase, rct)
845
+ lpos = dp2lp(Wx::Point.new(0, 0))
846
+
847
+ upd_rct = rct.inflate((20/@settings.scale).to_i, (20/@settings.scale).to_i)
848
+ upd_rct.offset!(-lpos.x, -lpos.y)
849
+
850
+ refresh_rect(Wx::Rect.new((upd_rct.x*@settings.scale).to_i,
851
+ (upd_rct.y*@settings.scale).to_i,
852
+ (upd_rct.width*@settings.scale).to_i,
853
+ (upd_rct.height*@settings.scale).to_i),
854
+ erase)
855
+ end
856
+
857
+ # Mark given rectangle as an invalidated one, i.e. as a rectangle which should
858
+ # be refreshed (by using Wx::SF::ShapeCanvas::refresh_invalidated_rect).
859
+ # @param [Wx::Rect] rct Rectangle to be invalidated
860
+ def invalidate_rect(rct)
861
+ if @invalidate_rect.nil?
862
+ @invalidate_rect = rct.dup
863
+ else
864
+ @invalidate_rect.union!(rct)
865
+ end
866
+ end
867
+
868
+ # Mark whole visible canvas portion as an invalidated rectangle.
869
+ def invalidate_visible_rect
870
+ invalidate_rect(dp2lp(get_client_rect))
871
+ end
872
+
873
+ # Refresh all canvas rectangles marked as invalidated.
874
+ # @see Wx::SF::ShapeCanvas::invalidate_rect
875
+ def refresh_invalidated_rect
876
+ unless @invalidate_rect.nil? || @invalidate_rect.empty?
877
+ refresh_canvas(false, @invalidate_rect)
878
+ @invalidate_rect = nil
879
+ end
880
+ end
881
+
882
+ # Show shapes shadows (only current diagram shapes are affected).
883
+ #
884
+ # The functions sets/unsets SHOW_SHADOW flag for all shapes currently included in the diagram.
885
+ # @param [Boolean] show true if the shadow should be shown, otherwise false
886
+ # @param [SHADOWMODE] style Shadow style
887
+ # @see SHADOWMODE
888
+ def show_shadows(show, style)
889
+ return unless @diagram
890
+
891
+ shapes = @diagram.get_shapes
892
+
893
+ shapes.each do |shape|
894
+ shape.remove_style(Shape::STYLE::SHOW_SHADOW) if show
895
+
896
+ case style
897
+ when SHADOWMODE::TOPMOST
898
+ unless shape.get_parent_shape
899
+ if show
900
+ shape.add_style(Shape::STYLE::SHOW_SHADOW)
901
+ else
902
+ shape.remove_style(Shape::STYLE::SHOW_SHADOW)
903
+ end
904
+ end
905
+
906
+ when SHADOWMODE::ALL
907
+ if show
908
+ shape.add_style(Shape::STYLE::SHOW_SHADOW)
909
+ else
910
+ shape.remove_style(Shape::STYLE::SHOW_SHADOW)
911
+ end
912
+ end
913
+ end
914
+ end
915
+
916
+ if Wx.has_feature?(:USE_DRAG_AND_DROP)
917
+
918
+ # Start Drag&Drop operation with shapes included in the given list.
919
+ # @param [Array<Wx::SF::Shape>] shapes List of shapes which should be dragged
920
+ # @param [Wx::Point] start A point where the dragging operation has started
921
+ # @return [Wx::DragResult] Drag result
922
+ def do_drag_drop(shapes, start = Wx::Point.new(-1, -1))
923
+ return Wx::DragNone unless has_style?(STYLE::DND)
924
+
925
+ @working_mode = MODE::DND
926
+
927
+ result = Wx::DragNone
928
+
929
+ validate_selection_for_clipboard(shapes, true)
930
+
931
+ unless shapes.empty?
932
+ deselect_all
933
+
934
+ @dnd_started_here = true
935
+ @dnd_started_at = start.to_point
936
+
937
+ data_obj = Wx::SF::ShapeDataObject.new(shapes)
938
+
939
+ dnd_src = if Wx::PLATFORM == 'WXGTK'
940
+ Wx::DropSource.new(data_obj, self, Wx::Icon(:page), Wx::Icon(:page), Wx::Icon(:page))
941
+ else
942
+ Wx::DropSource.new(data_obj)
943
+ end
944
+
945
+ result = dnd_src.do_drag_drop(Wx::Drag_AllowMove)
946
+ case result
947
+ when Wx::DragResult::DragMove
948
+ @diagram.remove_shapes(shapes)
949
+ end
950
+
951
+ @dnd_started_here = false
952
+
953
+ restore_prev_positions
954
+
955
+ move_shapes_from_negatives
956
+ update_virtual_size
957
+
958
+ save_canvas_state
959
+ refresh(false)
960
+ end
961
+
962
+ @working_mode = MODE::READY
963
+
964
+ result
965
+ end
966
+
967
+ end # if Wx.has_feature?(:USE_DRAG_AND_DROP)
968
+
969
+ # Copy selected shapes to the clipboard
970
+ def copy
971
+ return unless has_style?(STYLE::CLIPBOARD)
972
+ return unless @diagram
973
+
974
+ # copy selected shapes to the clipboard
975
+ Wx::Clipboard.open do |clipboard|
976
+ lst_selection = get_selected_shapes
977
+
978
+ validate_selection_for_clipboard(lst_selection,true)
979
+
980
+ unless lst_selection.empty?
981
+ data_obj = Wx::SF::ShapeDataObject.new(lst_selection)
982
+ clipboard.place(data_obj)
983
+
984
+ restore_prev_positions
985
+ end
986
+ end
987
+ end
988
+
989
+ # Copy selected shapes to the clipboard and remove them from the canvas
990
+ def cut
991
+ return unless has_style?(STYLE::CLIPBOARD)
992
+ return unless @diagram
993
+
994
+ copy
995
+
996
+ clear_temporaries
997
+
998
+ # remove selected shapes
999
+ lst_selection = get_selected_shapes
1000
+
1001
+ validate_selection_for_clipboard(lst_selection,false)
1002
+
1003
+ unless lst_selection.empty?
1004
+ @diagram.remove_shapes(lst_selection)
1005
+ @shp_multi_edit.show(false)
1006
+ save_canvas_state
1007
+ refresh(false)
1008
+ end
1009
+ end
1010
+
1011
+ # Paste shapes stored in the clipboard to the canvas
1012
+ def paste
1013
+ return unless has_style?(STYLE::CLIPBOARD)
1014
+ return unless @diagram
1015
+
1016
+ Wx::Clipboard.open do |clipboard|
1017
+ # read data object from the clipboard
1018
+ data_obj = Wx::SF::ShapeDataObject.new
1019
+ if clipboard.fetch(data_obj)
1020
+
1021
+ # deserialize shapes
1022
+ new_shapes = Wx::SF::Serializable.deserialize(data_obj.get_data_here)
1023
+ # add new shapes to diagram and remove those that are not accepted
1024
+ new_shapes.select! do |shape|
1025
+ ERRCODE::OK == @diagram.add_shape(shape, nil, shape.get_relative_position, INITIALIZE, DONT_SAVE_STATE)
1026
+ end
1027
+
1028
+ # verify newly added shapes (may remove shapes from list)
1029
+ @diagram.send(:check_new_shapes, new_shapes)
1030
+
1031
+ update_virtual_size # update for new shapes
1032
+
1033
+ # call user-defined handler
1034
+ on_paste(new_shapes)
1035
+
1036
+ save_canvas_state
1037
+ refresh(false)
1038
+ end
1039
+ end
1040
+ end
1041
+
1042
+ # Perform Undo operation (if available)
1043
+ def undo
1044
+ return unless has_style?(STYLE::UNDOREDO)
1045
+
1046
+ clear_temporaries
1047
+
1048
+ restore_canvas_state(@canvas_history.restore_older_state)
1049
+ @shp_multi_edit.show(false)
1050
+ end
1051
+
1052
+ # Perform Redo operation (if available)
1053
+ def redo
1054
+ return unless has_style?(STYLE::UNDOREDO)
1055
+
1056
+ clear_temporaries
1057
+
1058
+ restore_canvas_state(@canvas_history.restore_newer_state)
1059
+ @shp_multi_edit.show(false)
1060
+ end
1061
+
1062
+ # Function returns true if some shapes can be copied to the clipboard (it means they are selected)
1063
+ # @return [Boolean]
1064
+ def can_copy
1065
+ return false unless has_style?(STYLE::CLIPBOARD)
1066
+
1067
+ !get_selected_shapes.empty?
1068
+ end
1069
+ alias :can_copy? :can_copy
1070
+
1071
+ # Function returns true if some shapes can be cut to the clipboard (it means they are selected)
1072
+ # @return [Boolean]
1073
+ def can_cut
1074
+ can_copy
1075
+ end
1076
+ alias :can_cut? :can_cut
1077
+
1078
+ # Function returns true if some shapes can be copied from the clipboard to the canvas
1079
+ # (it means the clipboard contains stored shapes)
1080
+ # @return [Boolean]
1081
+ def can_paste
1082
+ return false unless has_style?(STYLE::CLIPBOARD)
1083
+
1084
+ Wx::Clipboard.open do |clipboard|
1085
+ return clipboard.supported?(Wx::DataFormat.new(Wx::SF::ShapeDataObject::DataFormatID))
1086
+ end
1087
+ end
1088
+ alias :can_paste? :can_paste
1089
+
1090
+ # Function returns true if undo operation can be done
1091
+ # @return [Boolean]
1092
+ def can_undo
1093
+ return false unless has_style?(STYLE::UNDOREDO)
1094
+
1095
+ @canvas_history.can_undo
1096
+ end
1097
+ alias :can_undo? :can_undo
1098
+
1099
+ # Function returns TRUE if Redo operation can be done
1100
+ # @return [Boolean]
1101
+ def can_redo
1102
+ return false unless has_style?(STYLE::UNDOREDO)
1103
+
1104
+ @canvas_history.can_redo
1105
+ end
1106
+ alias :can_redo? :can_redo
1107
+
1108
+ # Function returns true if align_selected function can be invoked (if more than
1109
+ # @return [Boolean]
1110
+ def can_align_selected
1111
+ @shp_multi_edit.visible? && @working_mode == MODE::READY
1112
+ end
1113
+ alias :can_align_selected? :can_align_selected
1114
+
1115
+ # Save current canvas state (for Undo/Redo operations)
1116
+ def save_canvas_state
1117
+ return unless has_style?(STYLE::UNDOREDO)
1118
+
1119
+ @canvas_history.save_canvas_state(@diagram.serialize)
1120
+ end
1121
+
1122
+ # Clear all stored canvas states (no Undo/Redo operations will be available)
1123
+ def clear_canvas_history
1124
+ @canvas_history.clear
1125
+ end
1126
+
1127
+ # Restores given canvas state (unless nil given)
1128
+ # @param [String,nil] state to restore
1129
+ def restore_canvas_state(state)
1130
+ return unless state
1131
+ set_diagram(Wx::SF::Serializable.deserialize(state))
1132
+ update_virtual_size
1133
+ @diagram.set_modified
1134
+ refresh(false)
1135
+ end
1136
+ protected :restore_canvas_state
1137
+
1138
+ # @!group Print methods
1139
+
1140
+ # Print current canvas content.
1141
+ # @overload print(prompt = PROMPT)
1142
+ # @param [Boolean] prompt If true (PROMPT) then the the native print dialog will be displayed before printing
1143
+ # @overload print(printout, prompt = PROMPT)
1144
+ # @param [Wx::SF::Printout] printout user-defined printout object (inherited from Wx::SF::Printout class) for printing.
1145
+ # @param [Boolean] prompt If true (PROMPT) then the the native print dialog will be displayed before printing
1146
+ # @see Wx::SF::Printout
1147
+ def print(*args)
1148
+ if args.first.is_a?(Wx::PRT::Printout)
1149
+ printout, prompt = args
1150
+ else
1151
+ printout = Printout.new('wxRuby SF Printout', self)
1152
+ prompt = args.shift
1153
+ end
1154
+ prompt = PROMPT if prompt.nil?
1155
+
1156
+ print_dialog_data = Wx::PRT::PrintDialogData.new(ShapeCanvas.print_data)
1157
+ printer = Wx::PRT::Printer.new(print_dialog_data)
1158
+
1159
+ deselect_all
1160
+
1161
+ if !printer.print(self, printout, prompt)
1162
+ if Wx::PRT::Printer.get_last_error == Wx::PRT::PrinterError::PRINTER_ERROR
1163
+ Wx.message_box("There was a problem printing.\nPerhaps your current printer is not set correctly?",
1164
+ 'wxRuby SF Printing',
1165
+ Wx::OK | Wx::ICON_ERROR)
1166
+ end
1167
+ else
1168
+ ShapeCanvas.print_data = printer.get_print_dialog_data.get_print_data
1169
+ end
1170
+ end
1171
+
1172
+ # Show print preview.
1173
+ # @overload print_preview()
1174
+ # @overload print_preview(preview, printout = nil)
1175
+ # @param [Wx::SF::Printout] preview user-defined printout object (inherited from Wx::SF::Printout class) used for print preview.
1176
+ # @param [Wx::SF::Printout] printout user-defined printout class (inherited from Wx::SF::Printout class) used for printing.
1177
+ # This parameter can be nil (in this case a print button will not be available in the print preview window).
1178
+ # @see Wx::SF::Printout
1179
+ def print_preview(*args)
1180
+ if args.empty?
1181
+ preview = Printout.new('wxRuby SF Preview', self)
1182
+ printout = Printout.new('wxRuby SF Printout', self)
1183
+ else
1184
+ preview, printout = args
1185
+ end
1186
+
1187
+ deselect_all
1188
+
1189
+ # Pass two printout objects: for preview, and possible printing.
1190
+ print_dialog_data = Wx::PRT::PrintDialogData.new(ShapeCanvas.print_data)
1191
+ prn_preview = Wx::PRT::PrintPreview.new(preview, printout, print_dialog_data)
1192
+ unless prn_preview.ok?
1193
+ Wx.message_box("There was a problem previewing.\nPerhaps your current printer is not set correctly?",
1194
+ 'wxRuby SF Previewing',
1195
+ Wx::OK | Wx::ICON_ERROR)
1196
+ return
1197
+ end
1198
+
1199
+ frame = Wx::PRT::PreviewFrame.new(prn_preview, self, 'wxRuby SF Print Preview', [100, 100], [800, 700])
1200
+ frame.centre(Wx::BOTH)
1201
+ frame.init
1202
+ frame.show
1203
+ end
1204
+
1205
+ # Show page setup dialog for printing.
1206
+ def page_setup
1207
+ ShapeCanvas.page_setup_data.set_print_data(ShapeCanvas.print_data)
1208
+
1209
+ Wx::PRT::PageSetupDialog(self, ShapeCanvas.page_setup_data) do |dlg|
1210
+ dlg.show_modal
1211
+ ShapeCanvas.print_data = dlg.get_page_setup_data.get_print_data
1212
+ ShapeCanvas.page_setup_data = dlg.get_page_setup_data
1213
+ end
1214
+ end
1215
+
1216
+ # @!endgroup
1217
+
1218
+ # Convert device position to logical position.
1219
+ #
1220
+ # The function returns unscrolled unscaled canvas position.
1221
+ # @overload dp2lp(pos)
1222
+ # @param [Wx::Point] pos Device position (for example mouse position)
1223
+ # @return [Wx::Point] Logical position
1224
+ # @overload dp2lp(rct)
1225
+ # @param [Wx::Rect] rct Device position (for example mouse position)
1226
+ # @return [Wx::Rect] Logical position
1227
+ def dp2lp(arg)
1228
+ if arg.is_a?(Wx::Rect)
1229
+ x, y = calc_unscrolled_position(arg.x, arg.y)
1230
+ Wx::Rect.new((x/@settings.scale).to_i, (y/@settings.scale).to_i,
1231
+ (arg.width/@settings.scale).to_i, (arg.height/@settings.scale).to_i)
1232
+ else
1233
+ arg = arg.to_point
1234
+ x, y = calc_unscrolled_position(arg.x, arg.y)
1235
+ Wx::Point.new((x/@settings.scale).to_i, (y/@settings.scale).to_i)
1236
+ end
1237
+ end
1238
+
1239
+ # Convert logical position to device position.
1240
+ #
1241
+ # The function returns scrolled scaled canvas position.
1242
+ # @overload lp2dp(pos)
1243
+ # @param [Wx::Point] pos Logical position (for example shape position)
1244
+ # @return [Wx::Point] Device position
1245
+ # @overload lp2dp(rct)
1246
+ # @param [Wx::Rect] rct Logical position (for example shape position)
1247
+ # @return [Wx::Rect] Device position
1248
+ def lp2dp(arg)
1249
+ if arg.is_a?(Wx::Rect)
1250
+ x, y = calc_unscrolled_position(arg.x, arg.y)
1251
+ Wx::Rect.new((x*@settings.scale).to_i, (y*@settings.scale).to_i,
1252
+ (arg.width*@settings.scale).to_i, (arg.height*@settings.scale).to_i)
1253
+ else
1254
+ arg = arg.to_point
1255
+ x, y = calc_unscrolled_position(arg.x, arg.y)
1256
+ Wx::Point.new((x*@settings.scale).to_i, (y*@settings.scale).to_i)
1257
+ end
1258
+ end
1259
+
1260
+ # Search for any shape located at the (mouse cursor) position (result used by #get_shape_under_cursor)
1261
+ # @param [Wx::Point] lpos
1262
+ def update_shape_under_cursor_cache(lpos)
1263
+ sel_shape = unsel_shape = top_shape = nil
1264
+ sel_line = unsel_line = top_line = nil
1265
+ lpos = lpos.to_point
1266
+
1267
+ @topmost_shape_under_cursor = nil
1268
+
1269
+ @current_shapes.replace(@diagram.get_all_shapes) if @diagram
1270
+
1271
+ @current_shapes.reverse_each do |shape|
1272
+ if shape.visible? && shape.active? && shape.contains?(lpos)
1273
+ if shape.is_a?(Wx::SF::LineShape)
1274
+ top_line ||= shape
1275
+ if shape.selected?
1276
+ sel_line ||= shape
1277
+ else
1278
+ unsel_line ||= shape
1279
+ end
1280
+ else
1281
+ top_shape ||= shape
1282
+ if shape.selected?
1283
+ sel_shape ||= shape
1284
+ else
1285
+ unsel_shape ||= shape
1286
+ end
1287
+ end
1288
+ end
1289
+ end
1290
+
1291
+ # set reference to logically topmost selected and unselected shape under the mouse cursor
1292
+ @topmost_shape_under_cursor = top_line ? top_line : top_shape
1293
+
1294
+ @selected_shape_under_cursor = sel_line ? sel_line : sel_shape
1295
+
1296
+ @unselected_shape_under_cursor = unsel_line ? unsel_line : unsel_shape
1297
+ end
1298
+
1299
+ # Get shape under current mouse cursor position (fast implementation - use everywhere
1300
+ # it is possible instead of much slower GetShapeAtPosition()).
1301
+ # @param [SEARCHMODE] mode Search mode
1302
+ # @return [Wx::SF::Shape,nil] shape if found, otherwise nil
1303
+ # @see SEARCHMODE, Wx::SF::ShapeCanvas#dp2lp, Wx::SF::ShapeCanvas#get_shape_at_position
1304
+ def get_shape_under_cursor(mode = SEARCHMODE::BOTH)
1305
+ case mode
1306
+ when SEARCHMODE::BOTH
1307
+ @topmost_shape_under_cursor
1308
+ when SEARCHMODE::SELECTED
1309
+ @selected_shape_under_cursor
1310
+ when SEARCHMODE::UNSELECTED
1311
+ @unselected_shape_under_cursor
1312
+ else
1313
+ nil
1314
+ end
1315
+ end
1316
+
1317
+ # Get shape at given logical position
1318
+ # @param [Wx::Point] pos Logical position
1319
+ # @param [Integer] zorder Z-order of searched shape (useful if several shapes are located
1320
+ # at the given position)
1321
+ # @param [SEARCHMODE] mode Search mode
1322
+ # @return [Wx::SF::Shape,nil] shape if found, otherwise nil
1323
+ # @see SEARCHMODE, Wx::SF::ShapeCanvas#dp2lp, Wx::SF::ShapeCanvas#get_shape_under_cursor
1324
+ def get_shape_at_position(pos, zorder = 1, mode = SEARCHMODE::BOTH)
1325
+ return nil unless @diagram
1326
+
1327
+ @diagram.get_shape_at_position(pos, zorder, mode)
1328
+ end
1329
+
1330
+ # Get topmost handle at given position
1331
+ # @param [Wx::Point] pos Logical position
1332
+ # @return [Wx::SF::Shape::Handle,nil] shape handle if found, otherwise nil
1333
+ # @see Wx::SF::ShapeCanvas#dp2lp
1334
+ def get_topmost_handle_at_position(pos)
1335
+ return nil unless @diagram
1336
+
1337
+ pos = pos.to_point
1338
+ # first test multiedit handles...
1339
+ if @shp_multi_edit.visible?
1340
+ @shp_multi_edit.handles.each do |handle|
1341
+ return handle if handle.visible? && handle.contains?(pos)
1342
+ end
1343
+ end
1344
+
1345
+ # ... then test normal handles
1346
+ @diagram.get_shapes.each do |shape|
1347
+ # iterate through all shape's handles
1348
+ if shape.has_style?(Shape::STYLE::SIZE_CHANGE)
1349
+ shape.handles.each do |handle|
1350
+ return handle if handle.visible? && handle.contains?(pos)
1351
+ end
1352
+ end
1353
+ end
1354
+
1355
+ nil
1356
+ end
1357
+
1358
+ # Get list of all shapes located at given position
1359
+ # @param [Wx::Point] pos Logical position
1360
+ # @param [Array<Wx::SF::Shape>] shapes shape list where pointers to all found shapes will be stored
1361
+ # @return [Array<Wx::SF::Shape>] shapes shape list
1362
+ # @see Wx::SF::ShapeCanvas#dp2lp
1363
+ def get_shapes_at_position(pos, shapes = [])
1364
+ @diagram.get_shapes_at_position(pos, shapes) if @diagram
1365
+ shapes
1366
+ end
1367
+
1368
+ # Get list of shapes located inside given rectangle
1369
+ # @param [Wx::Rect] rct Examined rectangle
1370
+ # @param [Array<Wx::SF::Shape>] shapes shape list where pointers to all found shapes will be stored
1371
+ # @return [Array<Wx::SF::Shape>] shapes shape list
1372
+ def get_shapes_inside(rct, shapes = [])
1373
+ @diagram.get_shapes_inside(rct, shapes) if @diagram
1374
+ shapes
1375
+ end
1376
+
1377
+ # Get list of selected shapes.
1378
+ # @param [Array<Wx::SF::Shape>] selection shape list where pointers to all selected shapes will be stored
1379
+ # @return [Array<Wx::SF::Shape>] shapes shape list
1380
+ def get_selected_shapes(selection = [])
1381
+ return selection unless @diagram
1382
+
1383
+ selection.clear
1384
+ @diagram.get_shapes.each do |shape|
1385
+ selection << shape if shape.selected?
1386
+ end
1387
+ selection
1388
+ end
1389
+
1390
+ # Get box bounding all shapes in the canvas.
1391
+ # @return [Wx::Rect] Total bounding box
1392
+ def get_total_bounding_box
1393
+ virt_rct = nil
1394
+ if @diagram
1395
+ # calculate total bounding box (includes all shapes)
1396
+ @diagram.get_shapes.each_with_index do |shape, ix|
1397
+ if ix == 0
1398
+ virt_rct = shape.get_bounding_box
1399
+ else
1400
+ virt_rct.union!(shape.get_bounding_box)
1401
+ end
1402
+ end
1403
+ end
1404
+ virt_rct || Wx::Rect.new
1405
+ end
1406
+
1407
+ # Get bounding box of all selected shapes.
1408
+ # @return [Wx::Rect] Selection bounding box
1409
+ def get_selection_bb
1410
+ bb_rct = Wx::Rect.new
1411
+ # get selected shapes
1412
+ get_selected_shapes.each do |shape|
1413
+ shape.get_complete_bounding_box(
1414
+ bb_rct,
1415
+ Shape::BBMODE::SELF | Shape::BBMODE::CHILDREN | Shape::BBMODE::CONNECTIONS | Shape::BBMODE::SHADOW)
1416
+ end
1417
+ bb_rct
1418
+ end
1419
+
1420
+ # Align selected shapes in given directions.
1421
+ #
1422
+ # Shapes will be aligned according to most far shape in appropriate direction.
1423
+ # @param [HALIGN] halign Horizontal alignment
1424
+ # @param [VALIGN] valign Vertical alignment
1425
+ def align_selected(halign, valign)
1426
+ cnt = 0
1427
+ min_pos = max_pos = nil
1428
+
1429
+ lst_selection = get_selected_shapes
1430
+
1431
+ upd_rct = get_selection_bb
1432
+ upd_rct.inflate!(DEFAULT_ME_OFFSET, DEFAULT_ME_OFFSET)
1433
+
1434
+ # find most distant position
1435
+ lst_selection.each do |shape|
1436
+ unless shape.is_a?(LineShape)
1437
+ pos = shape.get_absolute_position
1438
+ shape_bb = shape.get_bounding_box
1439
+
1440
+ if cnt == 0
1441
+ min_pos = pos
1442
+ max_pos = Wx::RealPoint.new(pos.x + shape_bb.width, pos.y + shape_bb.height)
1443
+ else
1444
+ min_pos.x = pos.x if pos.x < min_pos.x
1445
+ min_pos.y = pos.y if pos.y < min_pos.y
1446
+ max_pos.x = pos.x + shape_bb.width if (pos.x + shape_bb.width) > max_pos.x
1447
+ max_pos.y = pos.y + shape_bb.height if (pos.y + shape_bb.height) > max_pos.y
1448
+ end
1449
+
1450
+ cnt += 1
1451
+ end
1452
+ end
1453
+
1454
+ # if only one non-line shape is in the selection then alignment has no sense so exit...
1455
+ return if cnt < 2
1456
+
1457
+ # set new positions
1458
+ lst_selection.each do |shape|
1459
+ unless shape.is_a?(LineShape)
1460
+ pos = shape.get_absolute_position
1461
+ shape_bb = shape.get_bounding_box
1462
+
1463
+ case halign
1464
+ when HALIGN::LEFT
1465
+ shape.move_to(min_pos.x, pos.y)
1466
+
1467
+ when HALIGN::RIGHT
1468
+ shape.move_to(max_pos.x - shape_bb.width, pos.y)
1469
+
1470
+ when HALIGN::CENTER
1471
+ shape.move_to((max_pos.x + min_pos.x)/2 - shape_bb.width/2, pos.y)
1472
+ end
1473
+
1474
+ case valign
1475
+ when VALIGN::TOP
1476
+ shape.move_to(pos.x, min_pos.y)
1477
+
1478
+ when VALIGN::BOTTOM
1479
+ shape.move_to(pos.x, max_pos.y - shape_bb.height)
1480
+
1481
+ when VALIGN::MIDDLE
1482
+ shape.move_to(pos.x, (max_pos.y + min_pos.y)/2 - shape_bb.height/2)
1483
+ end
1484
+
1485
+ # update the shape and its parent
1486
+ shape.update
1487
+ parent = shape.get_parent_shape
1488
+ parent.update if parent
1489
+ end
1490
+ end
1491
+
1492
+ unless upd_rct.empty?
1493
+ update_multiedit_size
1494
+ save_canvas_state
1495
+ refresh_canvas(false, upd_rct)
1496
+ end
1497
+ end
1498
+
1499
+ # @!group Style accessors
1500
+
1501
+ # Set canvas style.
1502
+ #
1503
+ # Default value is STYLE::MULTI_SELECTION | STYLE::MULTI_SIZE_CHANGE | STYLE::DND | STYLE::UNDOREDO | STYLE::CLIPBOARD | STYLE::HOVERING | STYLE::HIGHLIGHTING
1504
+ # @param [STYLE] style Combination of the canvas styles
1505
+ # @see STYLE
1506
+ def set_style(style)
1507
+ @settings.style = style
1508
+ end
1509
+ alias :style= :set_style
1510
+
1511
+ # Get current canvas style.
1512
+ def get_style
1513
+ @settings.style
1514
+ end
1515
+ alias :style :get_style
1516
+
1517
+ # Add new style flag.
1518
+ # @param [STYLE] style canvas style to add
1519
+ def add_style(style)
1520
+ @settings.style |= style
1521
+ end
1522
+
1523
+ # Remove given style flag.
1524
+ # @param [STYLE] style canvas style to remove
1525
+ def remove_style(style)
1526
+ @settings.style &= ~style
1527
+ end
1528
+
1529
+ # Check whether given style flag is used.
1530
+ # @param [STYLE] style canvas style to check
1531
+ def contains_style(style)
1532
+ (@settings.style & style) != 0
1533
+ end
1534
+ alias :contains_style? :contains_style
1535
+ alias :has_style? :contains_style
1536
+
1537
+ # @!endgroup
1538
+
1539
+ # @!group Public attribute accessors
1540
+
1541
+ # Set canvas background color.
1542
+ # @param [Wx::Colour] col Background color
1543
+ def set_canvas_colour(col)
1544
+ @settings.background_color = col
1545
+ end
1546
+ alias :canvas_colour= :set_canvas_colour
1547
+
1548
+ # Get canvas background color.
1549
+ # @return [Wx::Colour] Background color
1550
+ def get_canvas_colour
1551
+ @settings.background_color
1552
+ end
1553
+ alias :canvas_colour :get_canvas_colour
1554
+
1555
+ # Set starting gradient color.
1556
+ # @param [Wx::Colour] col Color
1557
+ def set_gradient_from(col)
1558
+ @settings.gradient_from = col
1559
+ end
1560
+ alias :gradient_from= :set_gradient_from
1561
+
1562
+ # Get starting gradient color.
1563
+ # @return [Wx::Colour] Color
1564
+ def get_gradient_from
1565
+ @settings.gradient_from
1566
+ end
1567
+ alias :gradient_from :get_gradient_from
1568
+
1569
+ # Set ending gradient color.
1570
+ # @param [Wx::Colour] col Color
1571
+ def set_gradient_to(col)
1572
+ @settings.gradient_to = col
1573
+ end
1574
+ alias :gradient_to= :set_gradient_to
1575
+
1576
+ # Get ending gradient color.
1577
+ # @return [Wx::Colour] Color
1578
+ def get_gradient_to
1579
+ @settings.gradient_to
1580
+ end
1581
+ alias :gradient_to :get_gradient_to
1582
+
1583
+ # Get grid size.
1584
+ # @return [Wx::Size] Grid size
1585
+ def get_grid_size
1586
+ @settings.grid_size
1587
+ end
1588
+ alias :grid_size :get_grid_size
1589
+
1590
+ # Set grid size.
1591
+ # @param [Wx::Size] grid Grid size
1592
+ def set_grid_size(grid)
1593
+ @settings.grid_size = grid.to_size
1594
+ end
1595
+ alias :grid_size= :set_grid_size
1596
+
1597
+ # Set grid line multiple.
1598
+ #
1599
+ # Grid lines will be drawn in a distance calculated as grid size multiplicated by this value.
1600
+ # Default value is 1.
1601
+ # @param [Integer] multiple Multiple value
1602
+ def set_grid_line_mult(multiple)
1603
+ @settings.grid_line_mult = multiple
1604
+ end
1605
+ alias :grid_line_mult= :set_grid_line_mult
1606
+
1607
+ # Get grid line multiple.
1608
+ # @return [Integer] Value by which a grid size will be multiplicated to determine grid lines distance
1609
+ def get_grid_line_mult
1610
+ @settings.grid_line_mult
1611
+ end
1612
+ alias :grid_line_mult :get_grid_line_mult
1613
+
1614
+ # Set grid color.
1615
+ # @param [Wx::Colour] col Grid color
1616
+ def set_grid_colour(col)
1617
+ @settings.grid_color = col
1618
+ end
1619
+ alias :grid_colour= :set_grid_colour
1620
+
1621
+ # Get grid color.
1622
+ # @return [Wx::Colour] Grid color
1623
+ def get_grid_colour
1624
+ @settings.grid_color
1625
+ end
1626
+ alias :grid_colour :get_grid_colour
1627
+
1628
+ # Set grid line style.
1629
+ # @param [Wx::PenStyle] style Line style
1630
+ def set_grid_style(style)
1631
+ @settings.grid_style = style
1632
+ end
1633
+ alias :grid_style= :set_grid_style
1634
+
1635
+ # Get grid line style.
1636
+ # @return [Wx::PenStyle] Line style
1637
+ def get_grid_style
1638
+ @settings.grid_style
1639
+ end
1640
+ alias :grid_style :get_grid_style
1641
+
1642
+ # Set shadow offset.
1643
+ # @param [Wx::RealPoint] offset Shadow offset
1644
+ def set_shadow_offset(offset)
1645
+ @settings.shadow_offset = offset.to_real_point
1646
+ end
1647
+ alias :shadow_offset= :set_shadow_offset
1648
+
1649
+ # Get shadow offset.
1650
+ # @return [Wx::RealPoint] Shadow offset
1651
+ def get_shadow_offset
1652
+ @settings.shadow_offset
1653
+ end
1654
+ alias :shadow_offset :get_shadow_offset
1655
+
1656
+ # Set shadow fill (used for shadows of non-text shapes only).
1657
+ # @param [Wx::Brush] brush Reference to brush object
1658
+ def set_shadow_fill(brush)
1659
+ @settings.shadow_fill = brush
1660
+ end
1661
+ alias :shadow_fill= :set_shadow_fill
1662
+
1663
+ # Get shadow fill.
1664
+ # @return [Wx::Brush] Current shadow brush
1665
+ def get_shadow_fill
1666
+ @settings.shadow_fill
1667
+ end
1668
+ alias :shadow_fill :get_shadow_fill
1669
+
1670
+ # Set horizontal align of printed drawing.
1671
+ # @param [HALIGN] val Horizontal align
1672
+ # @see HALIGN
1673
+ def set_print_h_align(val)
1674
+ @settings.print_h_align = val
1675
+ end
1676
+ alias :print_h_align= :set_print_h_align
1677
+
1678
+ # Get horizontal align of printed drawing.
1679
+ # @return [HALIGN] Current horizontal align
1680
+ # @see HALIGN
1681
+ def get_print_h_align
1682
+ @settings.print_h_align
1683
+ end
1684
+ alias :print_h_align :get_print_h_align
1685
+
1686
+ # Set vertical align of printed drawing.
1687
+ # @param [VALIGN] val Vertical align
1688
+ # @see VALIGN
1689
+ def set_print_v_align(val)
1690
+ @settings.print_v_align = val
1691
+ end
1692
+ alias :print_v_align= :set_print_v_align
1693
+
1694
+ # Get vertical align of printed drawing.
1695
+ # @return [VALIGN] Current vertical align
1696
+ # @see VALIGN
1697
+ def get_print_v_align
1698
+ @settings.print_v_align
1699
+ end
1700
+ alias :print_v_align :get_print_v_align
1701
+
1702
+ # Set printing mode for this canvas.
1703
+ # @param [PRINTMODE] mode Printing mode
1704
+ # @see PRINTMODE
1705
+ def set_print_mode(mode)
1706
+ @settings.print_mode = mode
1707
+ end
1708
+ alias :print_mode= :set_print_mode
1709
+
1710
+ # Get printing mode for this canvas.
1711
+ # @return [PRINTMODE] Current printing mode
1712
+ # #see PRINTMODE
1713
+ def get_print_mode
1714
+ @settings.print_mode
1715
+ end
1716
+ alias :print_mode :get_print_mode
1717
+
1718
+ # Set canvas scale.
1719
+ # @param [Float] scale Scale value
1720
+ def set_scale(scale)
1721
+ return unless @diagram
1722
+
1723
+ if scale != 1.0
1724
+ # query shapes
1725
+ msg = ''
1726
+ unless _query_canvas_change(CHANGE::SET_SCALE, msg)
1727
+ Wx.message_box("Cannot change scale of shape canvas: #{msg}.", 'wxRuby ShapeFramework', Wx::ICON_WARNING | Wx::OK)
1728
+ scale = 1.0
1729
+ end
1730
+ end
1731
+
1732
+ @settings.scale = scale != 0.0 ? scale : 1.0
1733
+
1734
+ # inform shapes
1735
+ _notify_canvas_change(CHANGE::RESCALED)
1736
+
1737
+ update_virtual_size
1738
+ end
1739
+ alias :scale= :set_scale
1740
+
1741
+ # Set minimal allowed scale (for mouse wheel scale change).
1742
+ # @param [Float] scale Minimal scale
1743
+ def set_min_scale(scale)
1744
+ @settings.min_scale = scale
1745
+ end
1746
+ alias :min_scale= :set_min_scale
1747
+
1748
+ # Get minimal allowed scale (for mouse wheel scale change).
1749
+ # @return [Float] Minimal scale
1750
+ def get_min_scale
1751
+ @settings.min_scale
1752
+ end
1753
+ alias :min_scale :get_min_scale
1754
+
1755
+ # Set maximal allowed scale (for mouse wheel scale change).
1756
+ # @param [Float] scale Maximal scale
1757
+ def set_max_scale(scale)
1758
+ @settings.max_scale = scale
1759
+ end
1760
+ alias :max_scale= :set_max_scale
1761
+
1762
+ # Set maximal allowed scale (for mouse wheel scale change).
1763
+ # @return [FLOAT] Maximal scale
1764
+ def get_max_scale
1765
+ @settings.max_scale
1766
+ end
1767
+ alias :max_scale :get_max_scale
1768
+
1769
+ # Get the canvas scale.
1770
+ # @return [Float] Canvas scale
1771
+ def get_scale
1772
+ @settings.scale
1773
+ end
1774
+ alias :scale :get_scale
1775
+
1776
+ # @!endgroup
1777
+
1778
+ # Set the canvas scale so a whole diagram is visible.
1779
+ def set_scale_to_view_all
1780
+ phys_rct = get_client_size
1781
+ virt_rct = get_total_bounding_box
1782
+
1783
+ hz = phys_rct.width.to_f / virt_rct.right
1784
+ vz = phys_rct.height.to_f / virt_rct.bottom
1785
+
1786
+ if hz < vz
1787
+ set_scale(hz < 1 ? hz : 1.0)
1788
+ else
1789
+ set_scale(vz < 1 ? vz : 1.0)
1790
+ end
1791
+ end
1792
+
1793
+ # Scroll the shape canvas so the given shape will be located in its center.
1794
+ # @param [Wx::SF::Shape] shape Pointer to focused shape
1795
+ def scroll_to_shape(shape)
1796
+ if shape
1797
+ ux, uy = get_scroll_pixels_per_unit
1798
+ sz_canvas = get_client_size
1799
+ pt_pos = shape.center
1800
+
1801
+ scroll(((pt_pos.x * @settings.scale) - sz_canvas.x/2)/ux, ((pt_pos.y * @settings.scale) - sz_canvas.y/2)/uy)
1802
+ end
1803
+ end
1804
+
1805
+ # Get canvas working mode.
1806
+ # @return [MODE] Working mode
1807
+ # @see MODE
1808
+ def get_mode
1809
+ @working_mode
1810
+ end
1811
+ alias :mode :get_mode
1812
+
1813
+ # Set default hover color.
1814
+ # @param [Wx::Colour] col Hover color.
1815
+ def set_hover_colour(col)
1816
+ return unless @diagram
1817
+
1818
+ @settings.common_hover_color = col
1819
+
1820
+ # update Hover color in all existing shapes
1821
+ @diagram.get_shapes.each { |shape| shape.set_hover_colour(col) }
1822
+ end
1823
+ alias :hover_colour= :set_hover_colour
1824
+
1825
+ # Get default hover colour.
1826
+ # @return [Wx::Colour] Hover colour
1827
+ def get_hover_colour
1828
+ @settings.common_hover_color
1829
+ end
1830
+ alias :hover_colour :get_hover_colour
1831
+
1832
+ # Get canvas history manager.
1833
+ # @return [Wx::SF::CanvasHistory] the canvas history manager
1834
+ # @see Wx::SF::CanvasHistory
1835
+ def get_history_manager
1836
+ @canvas_history
1837
+ end
1838
+ alias :history_manager :get_history_manager
1839
+
1840
+ # Update given position so it will fit canvas grid (if enabled).
1841
+ # @param [Wx::Point] pos Position which should be updated
1842
+ # @return [Wx::Point] Updated position
1843
+ def fit_position_to_grid(pos)
1844
+ pos = pos.to_point
1845
+ if has_style?(STYLE::GRID_USE)
1846
+ Wx::Point.new(pos.x / @settings.grid_size.x * @settings.grid_size.x,
1847
+ pos.y / @settings.grid_size.y * @settings.grid_size.y)
1848
+ else
1849
+ pos
1850
+ end
1851
+ end
1852
+
1853
+ # Update size of multi selection rectangle
1854
+ def update_multiedit_size
1855
+ # calculate bounding box
1856
+ union_rct = nil
1857
+ get_selected_shapes.each_with_index do |shape, ix|
1858
+ if ix == 0
1859
+ union_rct = shape.get_bounding_box
1860
+ else
1861
+ union_rct.union!(shape.get_bounding_box)
1862
+ end
1863
+ end
1864
+ union_rct ||= Wx::Rect.new
1865
+ union_rct.inflate!([DEFAULT_ME_OFFSET, DEFAULT_ME_OFFSET])
1866
+
1867
+ # draw rectangle
1868
+ @shp_multi_edit.set_relative_position(Wx::RealPoint.new(union_rct.x.to_f, union_rct.y.to_f))
1869
+ @shp_multi_edit.set_rect_size(Wx::RealPoint.new(union_rct.width.to_f, union_rct.height.to_f))
1870
+ end
1871
+
1872
+ # Update scroll window virtual size so it can display all shape canvas
1873
+ def update_virtual_size
1874
+ virt_rct = get_total_bounding_box
1875
+
1876
+ # allow user to modify calculated virtual canvas size
1877
+ on_update_virtual_size(virt_rct)
1878
+
1879
+ # update virtual area of the scrolled window if necessary
1880
+ if virt_rct.empty?
1881
+ set_virtual_size(500, 500)
1882
+ else
1883
+ set_virtual_size((virt_rct.right*@settings.scale).to_i, (virt_rct.bottom*@settings.scale).to_i)
1884
+ end
1885
+ _notify_canvas_change(CHANGE::VIRTUAL_SIZE)
1886
+ end
1887
+
1888
+ # Move all shapes so none of it will be located in negative position
1889
+ def move_shapes_from_negatives
1890
+ @diagram.move_shapes_from_negatives if @diagram
1891
+ end
1892
+
1893
+ # Center diagram in accordance to the shape canvas extent.
1894
+ def center_shapes
1895
+ rct_prev_bb = get_total_bounding_box
1896
+
1897
+ rct_bb = rct_prev_bb.center_in(Wx::Rect.new(Wx::Point.new(0, 0), get_size))
1898
+
1899
+ dx = (rct_bb.left - rct_prev_bb.left).to_f
1900
+ dy = (rct_bb.top - rct_prev_bb.top).to_f
1901
+
1902
+ @current_shapes.each do |shape|
1903
+ shape.move_by(dx, dy) unless shape.get_parent_shape
1904
+ end
1905
+
1906
+ move_shapes_from_negatives
1907
+ end
1908
+
1909
+ # Validate selection (remove redundantly selected shapes etc...).
1910
+ # @param [Array<Wx::SF::Shape>] selection List of selected shapes that should be validated
1911
+ def validate_selection(selection)
1912
+ return unless @diagram
1913
+
1914
+ # find child shapes that have parents in the list and deselect and remove those
1915
+ # so we only have regular toplevel shapes and orphaned child shapes
1916
+ selection.select! do |shape|
1917
+ if selection.include?(shape.get_parent_shape)
1918
+ shape.select(false)
1919
+ false
1920
+ else
1921
+ true
1922
+ end
1923
+ end
1924
+
1925
+ # move selected (toplevel) shapes to the back of the shapes list in the diagram
1926
+ # this gives a higher Z-order so they will float on top of other shapes when dragging
1927
+ selection.each do |shape|
1928
+ # in case of child shapes find the toplevel parent it belongs to and move that one
1929
+ shape = shape.get_parent_shape while shape.get_parent_shape
1930
+ @diagram.move_to_end(shape)
1931
+ end
1932
+ end
1933
+
1934
+ # Function responsible for drawing of the canvas's content to given DC. The default
1935
+ # implementation draws actual objects managed by assigned diagram manager.
1936
+ # @param [Wx::DC] dc device context where the shapes will be drawn to
1937
+ # @param [Boolean] from_paint Set the argument to true if the dc argument refers to the Wx::PaintDC instance
1938
+ # or derived classes (i.e. the function is called as a response to Wx::EVT_PAINT event)
1939
+ def draw_content(dc, from_paint)
1940
+ return unless @diagram
1941
+
1942
+ if from_paint
1943
+ # wxRect updRct
1944
+ bb_rct = Wx::Rect.new
1945
+ #
1946
+ # ShapeList m_lstToDraw
1947
+ lst_lines_to_draw = []
1948
+
1949
+ # get all existing shapes
1950
+ lst_to_draw = @diagram.get_shapes(Shape, Shape::SEARCHMODE::DFS)
1951
+
1952
+ upd_rct = nil
1953
+ # get the update rect list
1954
+ Wx::RegionIterator.for_region(get_update_region) do |region_it|
1955
+ # combine updated rectangles
1956
+ region_it.each do |rct|
1957
+ if upd_rct.nil?
1958
+ upd_rct = dp2lp(rct.inflate(5, 5))
1959
+ else
1960
+ upd_rct.union!(dp2lp(rct.inflate(5, 5)))
1961
+ end
1962
+ end
1963
+ end
1964
+ upd_rct ||= Wx::Rect.new
1965
+
1966
+ if @working_mode == MODE::SHAPEMOVE
1967
+ # draw unselected non line-based shapes first...
1968
+ lst_to_draw.each do |shape|
1969
+ parent_shape = shape.get_parent_shape
1970
+
1971
+ if !shape.is_a?(LineShape) || shape.stand_alone?
1972
+ if shape.intersects?(upd_rct)
1973
+ if parent_shape
1974
+ shape.draw(dc, WITHOUTCHILDREN) if !parent_shape.is_a?(LineShape) || parent_shape.stand_alone?
1975
+ else
1976
+ shape.draw(dc, WITHOUTCHILDREN)
1977
+ end
1978
+ end
1979
+ else
1980
+ lst_lines_to_draw << shape
1981
+ end
1982
+ end
1983
+
1984
+ # ... and draw connections
1985
+ lst_lines_to_draw.each do |line|
1986
+ line.get_complete_bounding_box(bb_rct, Shape::BBMODE::SELF | Shape::BBMODE::CHILDREN | Shape::BBMODE::SHADOW)
1987
+ line.draw(dc, line.get_line_mode == LineShape::LINEMODE::READY) if bb_rct.intersects(upd_rct)
1988
+ end
1989
+ else
1990
+ # draw parent shapes (children are processed by parent objects)
1991
+ lst_to_draw.each do |shape|
1992
+ parent_shape = shape.get_parent_shape
1993
+
1994
+ if !shape.is_a?(LineShape) || shape.stand_alone?
1995
+ if shape.intersects?(upd_rct)
1996
+ if parent_shape
1997
+ shape.draw(dc, WITHOUTCHILDREN) if !parent_shape.is_a?(LineShape) || shape.stand_alone?
1998
+ else
1999
+ shape.draw(dc, WITHOUTCHILDREN)
2000
+ end
2001
+ end
2002
+ else
2003
+ lst_lines_to_draw << shape
2004
+ end
2005
+ end
2006
+
2007
+ # draw connections
2008
+ lst_lines_to_draw.each do |line|
2009
+ line.get_complete_bounding_box(bb_rct, Shape::BBMODE::SELF | Shape::BBMODE::CHILDREN)
2010
+ line.draw(dc, line.get_line_mode == LineShape::LINEMODE::READY) if bb_rct.intersects(upd_rct)
2011
+ end
2012
+ end
2013
+
2014
+ # draw multiselection if necessary
2015
+ @shp_selection.draw(dc) if @shp_selection.visible?
2016
+ @shp_multi_edit.draw(dc) if @shp_multi_edit.visible?
2017
+ else
2018
+ # draw parent shapes (children are processed by parent objects)
2019
+ @diagram.get_top_shapes.each do |shape|
2020
+ shape.draw(dc) if !shape.is_a?(LineShape) || shape.stand_alone?
2021
+ end
2022
+
2023
+ # draw connections
2024
+ @diagram.get_top_shapes.each do |shape|
2025
+ shape.draw(dc) if shape.is_a?(LineShape) && !shape.stand_alone?
2026
+ end
2027
+ end
2028
+ end
2029
+
2030
+ # Function responsible for drawing of the canvas's background to given DC. The default
2031
+ # implementation draws canvas background and grid.
2032
+ # @param [Wx::DC] dc device context where the shapes will be drawn to
2033
+ # @param [Boolean] _from_paint Set the argument to true if the dc argument refers to the Wx::PaintDC instance
2034
+ # or derived classes (i.e. the function is called as a response to Wx::EVT_PAINT event)
2035
+ def draw_background(dc, _from_paint)
2036
+ # erase background
2037
+ if has_style?(STYLE::GRADIENT_BACKGROUND)
2038
+ bcg_size = @settings.grid_size + get_virtual_size
2039
+ if @settings.scale != 1.0
2040
+ dc.gradient_fill_linear(Wx::Rect.new([0, 0], [(bcg_size.x/@settings.scale).to_i, (bcg_size.y/@settings.scale).to_i]),
2041
+ @settings.gradient_from, @settings.gradient_to, Wx::SOUTH)
2042
+ else
2043
+ dc.gradient_fill_linear(Wx::Rect.new(Wx::Point.new(0, 0), bcg_size),
2044
+ @settings.gradient_from, @settings.gradient_to, Wx::SOUTH)
2045
+ end
2046
+ else
2047
+ dc.set_background(Wx::Brush.new(@settings.background_color))
2048
+ dc.clear
2049
+ end
2050
+
2051
+ # show grid
2052
+ if has_style?(STYLE::GRID_SHOW)
2053
+ linedist = @settings.grid_size.x * @settings.grid_line_mult
2054
+
2055
+ if (linedist * @settings.scale) > 3.0
2056
+ grid_rct = Wx::Rect.new([0, 0], @settings.grid_size + get_virtual_size)
2057
+ max_x = (grid_rct.right/@settings.scale).to_i
2058
+ max_y = (grid_rct.bottom/@settings.scale).to_i
2059
+
2060
+ dc.set_pen(Wx::Pen.new(@settings.grid_color, 1, @settings.grid_style))
2061
+ (grid_rct.left..max_x).step(linedist) do |x|
2062
+ dc.draw_line(x, 0, x, max_y)
2063
+ end
2064
+ (grid_rct.top..max_y).step(linedist) do |y|
2065
+ dc.draw_line(0, y, max_x, y)
2066
+ end
2067
+ end
2068
+ end
2069
+ end
2070
+
2071
+ # Function responsible for drawing of the canvas's foreground to given DC. The default
2072
+ # do nothing.
2073
+ # @param [Wx::DC] _dc device context where the shapes will be drawn to
2074
+ # @param [Boolean] _from_paint Set the argument to true if the dc argument refers to the Wx::PaintDC instance
2075
+ # or derived classes (i.e. the function is called as a response to Wx::EVT_PAINT event)
2076
+ def draw_foreground(_dc, _from_paint)
2077
+ # do nothing here...
2078
+ end
2079
+
2080
+ # Get reference to multiselection box
2081
+ # @return [Wx::SF::MultiSelRect] multiselection box object
2082
+ def get_multiselection_box
2083
+ @shp_multi_edit
2084
+ end
2085
+
2086
+ # @!group Public event handlers
2087
+
2088
+ # Event handler called when the canvas is clicked by
2089
+ # the left mouse button. The function can be overridden if necessary.
2090
+ #
2091
+ # The function is called by the framework and provides basic functionality
2092
+ # needed for proper management of displayed shape. It is necessary to call
2093
+ # this function from overridden methods if the default canvas behaviour
2094
+ # should be preserved.
2095
+ # @param [Wx::MouseEvent] event Mouse event
2096
+ # @see _on_left_down
2097
+ def on_left_down(event)
2098
+ # HINT: override it for custom actions...
2099
+ return unless @diagram
2100
+
2101
+ _notify_canvas_change(CHANGE::FOCUS)
2102
+ set_focus
2103
+
2104
+ lpos = dp2lp(event.get_position)
2105
+
2106
+ @can_save_state_on_mouse_up = false
2107
+
2108
+ case @working_mode
2109
+ when MODE::READY
2110
+ @selected_handle = get_topmost_handle_at_position(lpos)
2111
+
2112
+ if event.control_down && event.shift_down
2113
+ @selection_mode = SELECTIONMODE::REMOVE
2114
+ elsif event.shift_down
2115
+ @selection_mode = SELECTIONMODE::ADD
2116
+ else
2117
+ @selection_mode = SELECTIONMODE::NORMAL
2118
+ end
2119
+
2120
+ if @selected_handle.nil?
2121
+ selected_shape = get_shape_at_position(lpos)
2122
+
2123
+ selected_top_shape = selected_shape
2124
+ while selected_top_shape && selected_top_shape.has_style?(Shape::STYLE::PROPAGATE_SELECTION)
2125
+ selected_top_shape = selected_top_shape.get_parent_shape
2126
+ end
2127
+
2128
+ if selected_shape
2129
+ # perform selection
2130
+ lst_selection = get_selected_shapes
2131
+
2132
+ # cancel previous selections if necessary...
2133
+ if @selection_mode == SELECTIONMODE::NORMAL && (selected_top_shape.nil? || !lst_selection.include?(selected_top_shape))
2134
+ deselect_all
2135
+ end
2136
+ selected_top_shape.select(@selection_mode != SELECTIONMODE::REMOVE) if selected_top_shape
2137
+
2138
+ get_selected_shapes(lst_selection)
2139
+
2140
+ # remove child shapes from the selection
2141
+ validate_selection(lst_selection)
2142
+
2143
+ if lst_selection.size > 1
2144
+ hide_all_handles
2145
+ elsif @selection_mode == SELECTIONMODE::REMOVE && lst_selection.size == 1
2146
+ lst_selection.first.select(true)
2147
+ end
2148
+
2149
+ fit_pos = fit_position_to_grid(lpos)
2150
+
2151
+ # call user defined actions
2152
+ selected_shape.on_left_click(fit_pos)
2153
+
2154
+ # inform selected shapes about begin of dragging...
2155
+ lst_connections = []
2156
+
2157
+ lst_selection.each do |shape|
2158
+ shape.send(:_on_begin_drag, fit_pos)
2159
+
2160
+ # inform also connections assigned to the shape and its children
2161
+ lst_connections.clear
2162
+ append_assigned_connections(shape, lst_connections, true)
2163
+
2164
+ lst_connections.each do |line|
2165
+ line.send(:_on_begin_drag, fit_pos)
2166
+ end
2167
+ end
2168
+
2169
+ if @selection_mode == SELECTIONMODE::NORMAL
2170
+ @shp_multi_edit.show(false)
2171
+ @working_mode = MODE::SHAPEMOVE
2172
+ else
2173
+ if lst_selection.size > 1
2174
+ @shp_multi_edit.show(true)
2175
+ @shp_multi_edit.show_handles(true)
2176
+ else
2177
+ @shp_multi_edit.show(false)
2178
+ end
2179
+ @working_mode = MODE::READY
2180
+ end
2181
+ else
2182
+ if has_style?(STYLE::MULTI_SELECTION)
2183
+ deselect_all if @selection_mode == SELECTIONMODE::NORMAL
2184
+ @selection_start = Wx::RealPoint.new(lpos.x, lpos.y)
2185
+ @shp_selection.show(true)
2186
+ @shp_selection.show_handles(false)
2187
+ @shp_selection.set_relative_position(@selection_start)
2188
+ @shp_selection.set_rect_size(Wx::RealPoint.new(0, 0))
2189
+ @working_mode = MODE::MULTISELECTION
2190
+ else
2191
+ deselect_all
2192
+ @working_mode = MODE::READY
2193
+ end
2194
+ end
2195
+
2196
+ # update canvas
2197
+ invalidate_visible_rect
2198
+ else
2199
+ if @selected_handle.get_parent_shape == @shp_multi_edit
2200
+ if has_style?(STYLE::MULTI_SIZE_CHANGE)
2201
+ @working_mode = MODE::MULTIHANDLEMOVE
2202
+ else
2203
+ @working_mode = MODE::READY
2204
+ end
2205
+ else
2206
+ @working_mode = MODE::HANDLEMOVE
2207
+ case @selected_handle.get_type
2208
+ when Shape::Handle::TYPE::LINESTART
2209
+ line = @selected_handle.get_parent_shape
2210
+ line.send(:set_line_mode, LineShape::LINEMODE::SRCCHANGE)
2211
+ line.send(:set_unfinished_point, lpos)
2212
+
2213
+ when Shape::Handle::TYPE::LINEEND
2214
+ line = @selected_handle.get_parent_shape
2215
+ line.send(:set_line_mode, LineShape::LINEMODE::TRGCHANGE)
2216
+ line.send(:set_unfinished_point, lpos)
2217
+ end
2218
+ end
2219
+ @selected_handle.send(:_on_begin_drag, fit_position_to_grid(lpos))
2220
+ end
2221
+
2222
+ when MODE::CREATECONNECTION
2223
+ # update the line shape being created
2224
+ if @new_line_shape
2225
+ shape_under = get_shape_under_cursor
2226
+ # propagate request for interactive connection if requested
2227
+ while shape_under && shape_under.has_style?(Shape::STYLE::PROPAGATE_INTERACTIVE_CONNECTION)
2228
+ shape_under = shape_under.get_parent_shape
2229
+ end
2230
+ # finish connection's creation process if possible
2231
+ if shape_under && !event.control_down
2232
+ if @new_line_shape.get_trg_shape_id.nil? && (shape_under != @new_line_shape) &&
2233
+ shape_under.get_id && (shape_under.is_connection_accepted(@new_line_shape.class))
2234
+ # find out whether the target shape can be connected to the source shape
2235
+ source_shape = @diagram.find_shape(@new_line_shape.get_src_shape_id)
2236
+
2237
+ if source_shape &&
2238
+ shape_under.is_src_neighbour_accepted(source_shape.class) &&
2239
+ source_shape.is_trg_neighbour_accepted(shape_under.class)
2240
+ @new_line_shape.set_trg_shape_id(shape_under.get_id)
2241
+ @new_line_shape.set_ending_connection_point(shape_under.get_nearest_connection_point(lpos.to_real))
2242
+
2243
+ # inform user that the line is completed
2244
+ case on_pre_connection_finished(@new_line_shape)
2245
+ when PRECON_FINISH_STATE::OK
2246
+ when PRECON_FINISH_STATE::FAILED_AND_CANCEL_LINE
2247
+ @new_line_shape.set_trg_shape_id(nil)
2248
+ @diagram.remove_shape(@new_line_shape)
2249
+ @working_mode = MODE::READY
2250
+ @new_line_shape = nil
2251
+ return
2252
+ when PRECON_FINISH_STATE::FAILED_AND_CONTINUE_EDIT
2253
+ @new_line_shape.set_trg_shape_id(nil)
2254
+ return
2255
+ end
2256
+ @new_line_shape.create_handles
2257
+
2258
+ # switch off the "under-construction" mode
2259
+ @new_line_shape.send(:set_line_mode, LineShape::LINEMODE::READY)
2260
+
2261
+ on_connection_finished(@new_line_shape)
2262
+
2263
+ @new_line_shape.update
2264
+ @new_line_shape.refresh(DELAYED)
2265
+
2266
+ @working_mode = MODE::READY
2267
+ @new_line_shape = nil
2268
+
2269
+ save_canvas_state
2270
+ end
2271
+ end
2272
+ else
2273
+ if @new_line_shape.get_src_shape_id
2274
+ fit_pos = fit_position_to_grid(lpos)
2275
+ @new_line_shape.get_control_points << Wx::RealPoint.new(fit_pos.x, fit_pos.y)
2276
+ end
2277
+ end
2278
+ end
2279
+
2280
+ else
2281
+ @working_mode = MODE::READY
2282
+ end
2283
+
2284
+ refresh_invalidated_rect
2285
+ end
2286
+
2287
+ # Event handler called when the canvas is double-clicked by
2288
+ # the left mouse button. The function can be overridden if necessary.
2289
+ #
2290
+ # The function is called by the framework and provides basic functionality
2291
+ # needed for proper management of displayed shape. It is necessary to call
2292
+ # this function from overridden methods if the default canvas behaviour
2293
+ # should be preserved.
2294
+ # @param [Wx::MouseEvent] event Mouse event
2295
+ # @see _on_left_double_click
2296
+ def on_left_double_click(event)
2297
+ # HINT: override it for custom actions...
2298
+
2299
+ _notify_canvas_change(CHANGE::FOCUS)
2300
+ set_focus
2301
+
2302
+ lpos = dp2lp(event.get_position)
2303
+
2304
+ if @working_mode == MODE::READY
2305
+ shape = get_shape_under_cursor
2306
+ if shape
2307
+ shape.on_left_double_click(lpos)
2308
+
2309
+ # double click onto a line shape always change its set of
2310
+ # control points so the canvas state should be saved now...
2311
+ save_canvas_state if shape.is_a?(LineShape)
2312
+ end
2313
+ end
2314
+
2315
+ refresh_invalidated_rect
2316
+ end
2317
+
2318
+ # Event handler called when the left mouse button is released.
2319
+ # The function can be overridden if necessary.
2320
+ #
2321
+ # The function is called by the framework and provides basic functionality
2322
+ # needed for proper management of displayed shape. It is necessary to call
2323
+ # this function from overridden methods if the default canvas behaviour
2324
+ # should be preserved.
2325
+ # @param [Wx::MouseEvent] event Mouse event
2326
+ # @see _on_left_up
2327
+ def on_left_up(event)
2328
+ # HINT: override it for custom actions...
2329
+ lpos = dp2lp(event.get_position)
2330
+
2331
+ case @working_mode
2332
+ when MODE::MULTIHANDLEMOVE, MODE::HANDLEMOVE
2333
+ # resize parent shape to fit all its children if necessary
2334
+ if @selected_handle.get_parent_shape.get_parent_shape
2335
+ @selected_handle.get_parent_shape.get_parent_shape.update
2336
+ end
2337
+
2338
+ # if the handle is line handle then return the line to normal state
2339
+ # and re-assign line's source/target shape
2340
+ case @selected_handle.get_type
2341
+ when Shape::Handle::TYPE::LINESTART, Shape::Handle::TYPE::LINEEND
2342
+ line = @selected_handle.get_parent_shape
2343
+ line.send(:set_line_mode, LineShape::LINEMODE::READY)
2344
+
2345
+ parent_shape = get_shape_under_cursor
2346
+
2347
+ if parent_shape && (parent_shape != line) && (parent_shape.is_connection_accepted(line.class))
2348
+ if @selected_handle.get_type == Shape::Handle::TYPE::LINESTART
2349
+ trg_shape = @diagram.find_shape(line.get_trg_shape_id)
2350
+ if trg_shape && parent_shape.is_trg_neighbour_accepted(trg_shape.class)
2351
+ line.set_src_shape_id(parent_shape.get_id)
2352
+ end
2353
+ else
2354
+ src_shape = @diagram.find_shape(line.get_src_shape_id)
2355
+ if src_shape && parent_shape.is_src_neighbour_accepted(src_shape.class)
2356
+ line.set_trg_shape_id(parent_shape.get_id)
2357
+ end
2358
+ end
2359
+ end
2360
+ end
2361
+
2362
+ @selected_handle.send(:_on_end_drag, lpos)
2363
+
2364
+ @selected_handle = nil
2365
+ save_canvas_state if @can_save_state_on_mouse_up
2366
+
2367
+ when MODE::SHAPEMOVE
2368
+ lst_selection = get_selected_shapes
2369
+
2370
+ lst_selection.each do |shape|
2371
+ # notify shape
2372
+ shape.send(:_on_end_drag, lpos)
2373
+ # reparent based on new position
2374
+ reparent_shape(shape, lpos)
2375
+ end
2376
+
2377
+ if lst_selection.size>1
2378
+ @shp_multi_edit.show(true)
2379
+ @shp_multi_edit.show_handles(true)
2380
+ else
2381
+ @shp_multi_edit.show(false)
2382
+ end
2383
+
2384
+ move_shapes_from_negatives
2385
+
2386
+ save_canvas_state if @can_save_state_on_mouse_up
2387
+
2388
+ when MODE::MULTISELECTION
2389
+ lst_selection = get_selected_shapes
2390
+
2391
+ sel_rect = @shp_selection.get_bounding_box
2392
+ @current_shapes.each do |shape|
2393
+ if shape.active? && sel_rect.contains?(shape.get_bounding_box)
2394
+ shape = shape.get_parent_shape while shape && shape.has_style?(Shape::STYLE::PROPAGATE_SELECTION)
2395
+ if shape
2396
+ shape.select(@selection_mode != SELECTIONMODE::REMOVE)
2397
+ shape_pos = lst_selection.index(shape)
2398
+ if @selection_mode != SELECTIONMODE::REMOVE && shape_pos.nil?
2399
+ lst_selection << shape
2400
+ elsif @selection_mode == SELECTIONMODE::REMOVE && shape_pos
2401
+ lst_selection.delete_at(shape_pos)
2402
+ end
2403
+ end
2404
+ end
2405
+ end
2406
+
2407
+ validate_selection(lst_selection)
2408
+
2409
+ if lst_selection.empty?
2410
+ @shp_multi_edit.show(false)
2411
+ else
2412
+ hide_all_handles
2413
+ @shp_multi_edit.show(true)
2414
+ @shp_multi_edit.show_handles(true)
2415
+ end
2416
+
2417
+ @shp_selection.show(false)
2418
+ end
2419
+
2420
+ if @working_mode != MODE::CREATECONNECTION
2421
+ # update canvas
2422
+ @working_mode = MODE::READY
2423
+ update_multiedit_size
2424
+ update_virtual_size
2425
+ refresh(false)
2426
+ else
2427
+ refresh_invalidated_rect
2428
+ end
2429
+ end
2430
+
2431
+ # Event handler called when the canvas is clicked by
2432
+ # the right mouse button. The function can be overridden if necessary.
2433
+ #
2434
+ # The function is called by the framework and provides basic functionality
2435
+ # needed for proper management of displayed shape. It is necessary to call
2436
+ # this function from overridden methods if the default canvas behaviour
2437
+ # should be preserved.
2438
+ # @param [Wx::MouseEvent] event Mouse event
2439
+ # @see _on_right_down
2440
+ def on_right_down(event)
2441
+ # HINT: override it for custom actions...
2442
+
2443
+ _notify_canvas_change(CHANGE::FOCUS)
2444
+ set_focus
2445
+
2446
+ lpos = dp2lp(event.get_position)
2447
+
2448
+ if @working_mode == MODE::READY
2449
+ deselect_all
2450
+
2451
+ shape = get_shape_under_cursor
2452
+ if shape
2453
+ shape.select(true)
2454
+ shape.on_right_click(lpos)
2455
+ end
2456
+ end
2457
+
2458
+ refresh(false)
2459
+ end
2460
+
2461
+ # Event handler called when the canvas is double-clicked by
2462
+ # the right mouse button. The function can be overridden if necessary.
2463
+ #
2464
+ # The function is called by the framework and provides basic functionality
2465
+ # needed for proper management of displayed shape. It is necessary to call
2466
+ # this function from overridden methods if the default canvas behaviour
2467
+ # should be preserved.
2468
+ # @param [Wx::MouseEvent] event Mouse event
2469
+ # @see _on_right_double_click
2470
+ def on_right_double_click(event)
2471
+ # HINT: override it for custom actions...
2472
+
2473
+ _notify_canvas_change(CHANGE::FOCUS)
2474
+ set_focus
2475
+
2476
+ lpos = dp2lp(event.get_position)
2477
+
2478
+ if @working_mode == MODE::READY
2479
+ shape = get_shape_under_cursor
2480
+ shape.on_right_double_click(lpos) if shape
2481
+ end
2482
+
2483
+ refresh_invalidated_rect
2484
+ end
2485
+
2486
+ # Event handler called when the right mouse button is released.
2487
+ # The function can be overridden if necessary.
2488
+ #
2489
+ # The function is called by the framework and provides basic functionality
2490
+ # needed for proper management of displayed shape. It is necessary to call
2491
+ # this function from overridden methods if the default canvas behaviour
2492
+ # should be preserved.
2493
+ # @param [Wx::MouseEvent] _event Mouse event
2494
+ # @see _on_right_up
2495
+ def on_right_up(_event)
2496
+ # HINT: override it for custom actions...
2497
+ end
2498
+
2499
+ # Event handler called when the mouse pointer is moved.
2500
+ # The function can be overridden if necessary.
2501
+ #
2502
+ # The function is called by the framework and provides basic functionality
2503
+ # needed for proper management of displayed shape. It is necessary to call
2504
+ # this function from overridden methods if the default canvas behaviour
2505
+ # should be preserved.
2506
+ # @param [Wx::MouseEvent] event Mouse event
2507
+ # @see _on_mouse_move
2508
+ def on_mouse_move(event)
2509
+ # HINT: override it for custom actions...
2510
+ return unless @diagram
2511
+
2512
+ lpos = dp2lp(event.get_position)
2513
+
2514
+ case @working_mode
2515
+ when MODE::READY, MODE::CREATECONNECTION
2516
+ unless event.dragging
2517
+ # send event to multiedit shape
2518
+ @shp_multi_edit.send(:_on_mouse_move, lpos) if @shp_multi_edit.visible?
2519
+
2520
+ # send event to all user shapes
2521
+ @current_shapes.each { |shape| shape.send(:_on_mouse_move, lpos) }
2522
+
2523
+ # update unfinished line if any
2524
+ if @new_line_shape
2525
+ line_rct = Wx::Rect.new
2526
+ upd_line_rct = Wx::Rect.new
2527
+ @new_line_shape.get_complete_bounding_box(line_rct, Shape::BBMODE::SELF | Shape::BBMODE::CHILDREN)
2528
+
2529
+ @new_line_shape.send(:set_unfinished_point, fit_position_to_grid(lpos))
2530
+ @new_line_shape.update
2531
+
2532
+ @new_line_shape.get_complete_bounding_box(upd_line_rct, Shape::BBMODE::SELF | Shape::BBMODE::CHILDREN)
2533
+
2534
+ line_rct.union!(upd_line_rct)
2535
+
2536
+ invalidate_rect(line_rct)
2537
+ end
2538
+ end
2539
+
2540
+ when MODE::HANDLEMOVE, MODE::MULTIHANDLEMOVE, MODE::SHAPEMOVE
2541
+ if @working_mode != MODE::SHAPEMOVE
2542
+ if event.dragging
2543
+ @selected_handle.send(:_on_dragging, fit_position_to_grid(lpos)) if @selected_handle
2544
+ update_multiedit_size if @working_mode == MODE::MULTIHANDLEMOVE
2545
+ @can_save_state_on_mouse_up = true
2546
+ else
2547
+ @selected_handle.send(:_on_end_drag, lpos) if @selected_handle
2548
+ @selected_handle = nil
2549
+ @working_mode = MODE::READY
2550
+ end
2551
+ end
2552
+ unless @working_mode == MODE::MULTIHANDLEMOVE
2553
+ if event.dragging
2554
+ if has_style?(STYLE::GRID_USE)
2555
+ return if (event.get_position.x - @prev_mouse_pos.x).abs < @settings.grid_size.x &&
2556
+ (event.get_position.y - @prev_mouse_pos.y).abs < @settings.grid_size.y
2557
+ end
2558
+ @prev_mouse_pos = event.get_position
2559
+
2560
+ if event.control_down || event.shift_down
2561
+ lst_selection = get_selected_shapes
2562
+ deselect_all
2563
+ if Wx.has_feature?(:USE_DRAG_AND_DROP)
2564
+ do_drag_drop(lst_selection, lpos)
2565
+ end
2566
+ else
2567
+ lst_connections = []
2568
+ @current_shapes.each do |shape|
2569
+ if shape.selected? && @working_mode == MODE::SHAPEMOVE
2570
+ shape.send(:_on_dragging, fit_position_to_grid(lpos))
2571
+
2572
+ # move also connections assigned to this shape and its children
2573
+ lst_connections.clear
2574
+
2575
+ append_assigned_connections(shape, lst_connections,true)
2576
+
2577
+ lst_connections.each { |line| line.send(:_on_dragging, fit_position_to_grid(lpos)) }
2578
+
2579
+ # update connections assigned to this shape
2580
+ lst_connections = @diagram.get_assigned_connections(shape, LineShape, Shape::CONNECTMODE::BOTH)
2581
+ lst_connections.each { |line| line.update }
2582
+ else
2583
+ shape.send(:_on_mouse_move, lpos)
2584
+ end
2585
+ end
2586
+
2587
+ @can_save_state_on_mouse_up = true
2588
+ end
2589
+ else
2590
+ @working_mode = MODE::READY
2591
+ end
2592
+ end
2593
+
2594
+ when MODE::MULTISELECTION
2595
+ selection_pos = Wx::RealPoint.new(*@selection_start.to_ary)
2596
+ selection_size = Wx::RealPoint.new(lpos.x - @selection_start.x, lpos.y - @selection_start.y)
2597
+ if selection_size.x < 0
2598
+ selection_pos.x += selection_size.x
2599
+ selection_size.x = -selection_size.x
2600
+ end
2601
+ if selection_size.y < 0
2602
+ selection_pos.y += selection_size.y
2603
+ selection_size.y = -selection_size.y
2604
+ end
2605
+ @shp_selection.set_relative_position(selection_pos)
2606
+ @shp_selection.set_rect_size(selection_size)
2607
+
2608
+ invalidate_visible_rect
2609
+ end
2610
+
2611
+ refresh_invalidated_rect
2612
+ end
2613
+
2614
+ # Event handler called when the mouse wheel position is changed.
2615
+ # The function can be overridden if necessary.
2616
+ #
2617
+ # The function is called by the framework and provides basic functionality
2618
+ # needed for proper management of displayed shape. It is necessary to call
2619
+ # this function from overridden methods if the default canvas behaviour
2620
+ # should be preserved.
2621
+ # @param [Wx::MouseEvent] event Mouse event
2622
+ def on_mouse_wheel(event)
2623
+ # HINT: override it for custom actions...
2624
+
2625
+ if event.control_down
2626
+ scale = get_scale
2627
+ scale += (event.get_wheel_rotation/(event.get_wheel_delta*10)).to_f
2628
+
2629
+ scale = @settings.min_scale if scale < @settings.min_scale
2630
+ scale = @settings.max_scale if scale > @settings.max_scale
2631
+
2632
+ set_scale(scale)
2633
+ refresh(false)
2634
+ end
2635
+
2636
+ event.skip
2637
+ end
2638
+
2639
+ # Event handler called when any key is pressed.
2640
+ # The function can be overridden if necessary.
2641
+ #
2642
+ # The function is called by the framework and provides basic functionality
2643
+ # needed for proper management of displayed shape. It is necessary to call
2644
+ # this function from overridden methods if the default canvas behaviour
2645
+ # should be preserved.
2646
+ # @param [Wx::KeyEvent] event Keyboard event
2647
+ # @see _on_key_down
2648
+ def on_key_down(event)
2649
+ # HINT: override it for custom actions...
2650
+ return unless @diagram
2651
+
2652
+ lst_selection = get_selected_shapes
2653
+
2654
+ case event.get_key_code
2655
+ when Wx::K_DELETE
2656
+ # send event to selected shapes
2657
+ lst_selection.delete_if do |shape|
2658
+ if shape.has_style?(Shape::STYLE::PROCESS_DEL)
2659
+ shape.send(:_on_key, event.get_key_code)
2660
+ true
2661
+ else
2662
+ false
2663
+ end
2664
+ end
2665
+
2666
+ clear_temporaries
2667
+
2668
+ # delete selected shapes
2669
+ @diagram.remove_shapes(lst_selection)
2670
+ @shp_multi_edit.show(false)
2671
+ save_canvas_state
2672
+ refresh(false)
2673
+
2674
+ when Wx::K_ESCAPE
2675
+ case @working_mode
2676
+ when MODE::CREATECONNECTION
2677
+ abort_interactive_connection
2678
+
2679
+ when MODE::HANDLEMOVE
2680
+ if @selected_handle && @selected_handle.get_parent_shape.is_a?(LineShape)
2681
+ @selected_handle.send(:_on_end_drag, Wx::Point.new(0, 0))
2682
+
2683
+ line = @selected_handle.get_parent_shape
2684
+ line.send(:set_line_mode, LineShape::LINEMODE::READY)
2685
+ @selected_handle = nil
2686
+ end
2687
+
2688
+ else
2689
+ # send event to selected shapes
2690
+ lst_selection.each { |shape| shape.send(:_on_key, event.get_key_code) }
2691
+ end
2692
+ @working_mode = MODE::READY
2693
+ refresh(false)
2694
+
2695
+ when Wx::K_LEFT, Wx::K_RIGHT, Wx::K_UP, Wx::K_DOWN
2696
+ lst_connections = []
2697
+ lst_selection.each do |shape|
2698
+ shape.send(:_on_key, event.get_key_code)
2699
+
2700
+ # inform also connections assigned to this shape
2701
+ lst_connections.clear
2702
+ append_assigned_connections(shape, lst_connections, true)
2703
+
2704
+ lst_connections.each do |line|
2705
+ line.send(:_on_key, event.get_key_code) unless line.selected?
2706
+ end
2707
+ end
2708
+
2709
+ # send the event to multiedit ctrl if displayed
2710
+ @shp_multi_edit.send(:_on_key, event.get_key_code) if @shp_multi_edit.visible?
2711
+
2712
+ refresh_invalidated_rect
2713
+ save_canvas_state
2714
+
2715
+ else
2716
+ lst_selection.each { |shape| shape.send(:_on_key, event.get_key_code) }
2717
+ update_multiedit_size if @shp_multi_edit.visible?
2718
+ end
2719
+ end
2720
+
2721
+ # Event handler called when any editable text shape is changed.
2722
+ # The function can be overridden if necessary.
2723
+ # The function is called by the framework and its default implementation
2724
+ # generates Wx::SF::EVT_SF_TEXT_CHANGE event.
2725
+ # @param [Wx::SF::EditTextShape] shape Changed Wx::SF::EditTextShape object
2726
+ # @see Wx::SF::EditTextShape#edit_label
2727
+ # @see Wx::SF::ShapeTextEvent
2728
+ def on_text_change(shape)
2729
+ # HINT: override it for custom actions...
2730
+
2731
+ # ... standard implementation generates the Wx::SF::EVT_SF_TEXT_CHANGE event.
2732
+ id = shape ? shape.get_id : nil
2733
+
2734
+ event = ShapeTextEvent.new(Wx::SF::EVT_SF_TEXT_CHANGE, id)
2735
+ event.set_shape(shape)
2736
+ event.set_text(shape.get_text)
2737
+ process_event(event)
2738
+ end
2739
+
2740
+ # Event handler called after (successful or cancelled) connection creation. The function
2741
+ # can be overridden if necessary. The default implementation
2742
+ # generates Wx::SF::EVT_SF_LINE_DONE event.
2743
+ # @param [Wx::SF::LineShape,nil] connection new connection object (nil if cancelled)
2744
+ # @see start_interactive_connection
2745
+ # @see Wx::SF::ShapeEvent
2746
+ def on_connection_finished(connection)
2747
+ # HINT: override to perform user-defined actions...
2748
+
2749
+ # ... standard implementation generates the Wx::SF::EVT_SF_LINE_DONE event.
2750
+ id = connection ? connection.get_id : -1
2751
+
2752
+ event = ShapeEvent.new(Wx::SF::EVT_SF_LINE_DONE, id)
2753
+ event.set_shape(connection)
2754
+ process_event(event)
2755
+ end
2756
+
2757
+ # Event handler called after successful connection creation in
2758
+ # order to allow developer to perform some kind of checks
2759
+ # before the connection is really added to the diagram. The function
2760
+ # can be overridden if necessary. The default implementation
2761
+ # generates Wx::SF::EVT_SF_LINE_DONE event.
2762
+ # @param [Wx::SF::LineShape] connection new connection object
2763
+ # @return [PRECON_FINISH_STATE] PRECONNECTIONFINISHEDSTATE::OK if the connection is accepted, otherwise
2764
+ # if the generated event has been vetoed the connection creation is cancelled
2765
+ # @see start_interactive_connection
2766
+ # @see Wx::SF::ShapeEvent
2767
+ def on_pre_connection_finished(connection)
2768
+ # HINT: override to perform user-defined actions...
2769
+
2770
+ # ... standard implementation generates the Wx::SF::EVT_SF_LINE_DONE event.
2771
+ id = connection ? connection.get_id : -1
2772
+
2773
+ event = ShapeEvent.new(Wx::SF::EVT_SF_LINE_BEFORE_DONE, id)
2774
+ event.set_shape(connection)
2775
+ process_event(event)
2776
+
2777
+ return PRECON_FINISH_STATE::FAILED_AND_CANCEL_LINE if event.vetoed?
2778
+
2779
+ PRECON_FINISH_STATE::OK
2780
+ end
2781
+
2782
+ if Wx.has_feature?(:USE_DRAG_AND_DROP)
2783
+
2784
+ # Event handler called by the framework after any dragged shapes
2785
+ # are dropped to the canvas. The default implementation
2786
+ # generates Wx::SF::EVT_SF_ON_DROP event.
2787
+ # @param [Integer] x X-coordinate of a position the data was dropped to
2788
+ # @param [Integer] y Y-coordinate of a position the data was dropped to
2789
+ # @param [Wx::DragResult] deflt Drag result
2790
+ # @param [Array<Wx::SF::Shape>] dropped a list containing the dropped data
2791
+ # @see Wx::SF::CanvasDropTarget
2792
+ # @see Wx::SF::ShapeDropEvent
2793
+ def on_drop(x, y, deflt, dropped)
2794
+ # HINT: override it for custom actions...
2795
+
2796
+ # ... standard implementation generates the Wx::SF::EVT_SF_ON_DROP event.
2797
+ return unless has_style?(STYLE::DND)
2798
+
2799
+ # create the drop event and process it
2800
+ event = ShapeDropEvent.new(Wx::SF::EVT_SF_ON_DROP, x, y, self, deflt, Wx::ID_ANY)
2801
+ event.set_dropped_shapes(dropped)
2802
+ process_event(event)
2803
+ end
2804
+
2805
+ end
2806
+
2807
+ # Event handler called by the framework after pasting of shapes
2808
+ # from the clipboard to the canvas. The default implementation
2809
+ # generates Wx::SF::EVT_SF_ON_PASTE event.
2810
+ # @param [Array<Wx::SF::Shape>] pasted a list containing the pasted data
2811
+ # @see Wx::SF::ShapeCanvas#paste
2812
+ # @see Wx::SF::ShapePasteEvent
2813
+ def on_paste(pasted)
2814
+ # HINT: override it for custom actions...
2815
+
2816
+ # ... standard implementation generates the Wx::SF::EVT_SF_ON_PASTE event.
2817
+ return unless has_style?(STYLE::CLIPBOARD)
2818
+
2819
+ # create the drop event and process it
2820
+ event = ShapePasteEvent.new(Wx::SF::EVT_SF_ON_PASTE, self, Wx::ID_ANY)
2821
+ event.set_pasted_shapes(pasted)
2822
+ process_event(event)
2823
+ end
2824
+
2825
+ # Event handler called if canvas virtual size is going to be updated.
2826
+ # The default implementation does nothing but the function can be overridden by
2827
+ # a user to modify calculated virtual canvas size.
2828
+ # @param [Wx::Rect] _virtrct Calculated canvas virtual size
2829
+ def on_update_virtual_size(_virtrct)
2830
+ # HINT: override it for custom actions...
2831
+ end
2832
+
2833
+ # @!endgroup
2834
+
2835
+ def inspect
2836
+ "#<Wx::SF::ShapeCanvas:#{object_id}>"
2837
+ end
2838
+
2839
+ private
2840
+
2841
+ # Validate selection so the shapes in the given list can be processed by the clipboard functions
2842
+ # @param [Array<Wx::SF::Shape>] selection
2843
+ # @param [Boolean] storeprevpos
2844
+ def validate_selection_for_clipboard(selection, storeprevpos)
2845
+ selection.dup.each do |shape|
2846
+ if shape.get_parent_shape
2847
+ # remove child shapes without parent in the selection and without STYLE::PARENT_CHANGE style
2848
+ # defined from the selection
2849
+ if !shape.has_style?(Shape::STYLE::PARENT_CHANGE) && !selection.include?(shape.get_parent_shape)
2850
+ selection.delete(shape)
2851
+ else
2852
+ # convert relative position to absolute position if the shape is copied
2853
+ # without its parent
2854
+ unless selection.include?(shape.get_parent_shape)
2855
+ store_prev_position(shape) if storeprevpos
2856
+ shape.set_relative_position(shape.get_absolute_position)
2857
+ end
2858
+ end
2859
+ end
2860
+
2861
+ append_assigned_connections(shape, selection, false)
2862
+ end
2863
+ end
2864
+
2865
+ # Append connections assigned to shapes in given list to this list as well
2866
+ # @param [Wx::SF::Shape] shape
2867
+ # @param [Array<Wx::SF::Shape>] selection
2868
+ # @param [Boolean] childrenonly
2869
+ def append_assigned_connections(shape, selection, childrenonly)
2870
+ # add connections assigned to copied topmost shapes and their children to the copy list
2871
+ lst_children = shape.get_child_shapes(ANY, RECURSIVE)
2872
+
2873
+ # get connections assigned to the parent shape
2874
+ lst_connections = @diagram.get_assigned_connections(shape, LineShape, Shape::CONNECTMODE::BOTH) unless childrenonly
2875
+ lst_connections ||= []
2876
+ # get connections assigned to its child shape
2877
+ lst_children.each do |child|
2878
+ # get connections assigned to the child shape
2879
+ @diagram.get_assigned_connections(child, LineShape, Shape::CONNECTMODE::BOTH, lst_connections)
2880
+ end
2881
+
2882
+ # insert connections to the copy list
2883
+ lst_connections.each do |line|
2884
+ selection << line unless selection.include?(line)
2885
+ end
2886
+ end
2887
+
2888
+ # Remove given shape for temporary containers
2889
+ # @param [Wx::SF::Shape] shape
2890
+ def remove_from_temporaries(shape)
2891
+ if shape
2892
+ @current_shapes.delete(shape)
2893
+ @new_line_shape = nil if @new_line_shape == shape
2894
+ @unselected_shape_under_cursor = nil if @unselected_shape_under_cursor == shape
2895
+ @selected_shape_under_cursor = nil if @selected_shape_under_cursor == shape
2896
+ @topmost_shape_under_cursor = nil if @topmost_shape_under_cursor == shape
2897
+ end
2898
+ end
2899
+
2900
+ # Clear all temporary containers
2901
+ def clear_temporaries
2902
+ @current_shapes.clear
2903
+ @new_line_shape = nil
2904
+ @unselected_shape_under_cursor = nil
2905
+ @selected_shape_under_cursor = nil
2906
+ @topmost_shape_under_cursor = nil
2907
+ end
2908
+
2909
+ # Assign give shape to parent at given location (if exists)
2910
+ # @param [Wx::SF::Shape] shape
2911
+ # @param [Wx::Point] parentpos
2912
+ def reparent_shape(shape, parentpos)
2913
+ return unless @diagram
2914
+ # is shape dropped into accepting shape?
2915
+ parent_shape = get_shape_at_position(parentpos, 1, SEARCHMODE::UNSELECTED)
2916
+
2917
+ parent_shape = nil if parent_shape && !parent_shape.is_child_accepted(shape.class)
2918
+
2919
+ # set new parent
2920
+ if shape.has_style?(Shape::STYLE::PARENT_CHANGE) && !shape.is_a?(LineShape)
2921
+ prev_parent = shape.get_parent_shape
2922
+
2923
+ if parent_shape
2924
+ if parent_shape.get_parent_shape != shape
2925
+ # update relative position to new parent
2926
+ apos = shape.get_absolute_position - parent_shape.get_absolute_position
2927
+ shape.set_relative_position(apos)
2928
+ # reparent
2929
+ @diagram.reparent_shape(shape, parent_shape)
2930
+ # notify the parent shape about dropped child
2931
+ parent_shape.on_child_dropped(apos, shape)
2932
+ end
2933
+ else
2934
+ if @diagram.is_top_shape_accepted(shape.class)
2935
+ # move relative to previous parent
2936
+ shape.move_by(prev_parent.get_absolute_position) if prev_parent
2937
+ # reparent
2938
+ @diagram.reparent_shape(shape, parent_shape)
2939
+ end
2940
+ end
2941
+
2942
+ prev_parent.update if prev_parent
2943
+ parent_shape.update if parent_shape
2944
+ end
2945
+ end
2946
+
2947
+ # Store previous shape's position modified in validate_selection_for_clipboard() function
2948
+ # @param [Wx::SF::Shape] shape
2949
+ def store_prev_position(shape)
2950
+ @prev_positions[shape] = shape.get_relative_position.dup
2951
+ end
2952
+
2953
+ # Restore previously stored shape positions and clear the storage
2954
+ def restore_prev_positions
2955
+ @prev_positions.each_pair { |shape, pos| shape.set_relative_position(pos) }
2956
+ @prev_positions.clear
2957
+ end
2958
+
2959
+ # private event handlers
2960
+
2961
+ # Event handler called when the canvas should be repainted.
2962
+ # @param [Wx::PaintEvent] _event Paint event
2963
+ def _on_paint(_event)
2964
+ paint_buffered do |paint_dc|
2965
+ if ShapeCanvas.gc_enabled?
2966
+ Wx::GCDC.draw_on(paint_dc) do |gdc|
2967
+ prepare_dc(gdc)
2968
+
2969
+ # scale GC
2970
+ gc = gdc.get_graphics_context
2971
+ gc.scale(@settings.scale, @settings.scale)
2972
+
2973
+ draw_background(gdc, FROM_PAINT)
2974
+ draw_content(gdc, FROM_PAINT)
2975
+ draw_foreground(gdc, FROM_PAINT)
2976
+ end
2977
+ else
2978
+ Wx::ScaledDC.draw_on(paint_dc, @settings.scale) do |dc|
2979
+ prepare_dc(dc)
2980
+ draw_background(dc, FROM_PAINT)
2981
+ draw_content(dc, FROM_PAINT)
2982
+ draw_foreground(dc, FROM_PAINT)
2983
+ end
2984
+ end
2985
+ end
2986
+ end
2987
+
2988
+ # Event handler called when the canvas should be erased.
2989
+ # @param [Wx::EraseEvent] _event Erase event
2990
+ def _on_erase_background(_event)
2991
+ # do nothing to suppress window flickering
2992
+ end
2993
+
2994
+ # Event handler called when the mouse pointer leaves the canvas window.
2995
+ # @param [Wx::MouseEvent] event Mouse event
2996
+ def _on_leave_window(event)
2997
+ case @working_mode
2998
+ when MODE::MULTISELECTION
2999
+ when MODE::SHAPEMOVE
3000
+ when MODE::CREATECONNECTION
3001
+ when MODE::HANDLEMOVE
3002
+ when MODE::MULTIHANDLEMOVE
3003
+ else
3004
+ @working_mode = MODE::READY
3005
+ end
3006
+
3007
+ event.skip
3008
+ end
3009
+
3010
+ # Event handler called when the mouse pointer enters the canvas window.
3011
+ # @param [Wx::MouseEvent] event Mouse event
3012
+ def _on_enter_window(event)
3013
+ @prev_mouse_pos = event.get_position
3014
+
3015
+ lpos = dp2lp(event.get_position)
3016
+
3017
+ case @working_mode
3018
+ when MODE::MULTISELECTION
3019
+ unless event.left_is_down
3020
+ update_multiedit_size
3021
+ @shp_multi_edit.show(false)
3022
+ @working_mode = MODE::READY
3023
+
3024
+ invalidate_visible_rect
3025
+ end
3026
+
3027
+ when MODE::HANDLEMOVE
3028
+ unless event.left_is_down
3029
+ if @selected_handle
3030
+ if @selected_handle.get_parent_shape.is_a?(LineShape)
3031
+ @selected_handle.get_parent_shape.send(:set_line_mode, LineShape::LINEMODE::READY)
3032
+ end
3033
+
3034
+ @selected_handle.send(:_on_end_drag, lpos)
3035
+
3036
+ save_canvas_state
3037
+ @working_mode = MODE::READY
3038
+ @selected_handle = nil
3039
+
3040
+ invalidate_visible_rect
3041
+ end
3042
+ end
3043
+
3044
+ when MODE::MULTIHANDLEMOVE
3045
+ unless event.left_is_down
3046
+ if @selected_handle
3047
+ @selected_handle.send(:_on_end_drag, lpos)
3048
+
3049
+ save_canvas_state
3050
+ @working_mode = MODE::READY
3051
+
3052
+ invalidate_visible_rect
3053
+ end
3054
+ end
3055
+
3056
+ when MODE::SHAPEMOVE
3057
+ unless event.left_is_down
3058
+ lst_selection = get_selected_shapes
3059
+
3060
+ move_shapes_from_negatives
3061
+ update_virtual_size
3062
+
3063
+ if lst_selection.size > 1
3064
+ update_multiedit_size
3065
+ @shp_multi_edit.show(true)
3066
+ @shp_multi_edit.show_handles(true)
3067
+ end
3068
+
3069
+ lst_selection.each { |shape| shape.send(:_on_end_drag, lpos) }
3070
+
3071
+ @working_mode = MODE::READY
3072
+
3073
+ invalidate_visible_rect
3074
+ end
3075
+ end
3076
+
3077
+ refresh_invalidated_rect
3078
+
3079
+ event.skip
3080
+ end
3081
+
3082
+ # Event handler called when the canvas size has changed.
3083
+ # @param [Wx::SizeEvent] event Size event
3084
+ def _on_resize(event)
3085
+ refresh(false) if has_style?(STYLE::GRADIENT_BACKGROUND)
3086
+
3087
+ event.skip
3088
+ end
3089
+
3090
+ # original private event handlers
3091
+
3092
+ # Original private event handler called when the canvas is clicked by
3093
+ # left mouse button. The handler calls user-overridable event handler function
3094
+ # and skips the event for next possible processing.
3095
+ # @param [Wx::MouseEvent] event Mouse event
3096
+ # @see Wx::SF::ShapeCanvas#on_left_down
3097
+ def _on_left_down(event)
3098
+ on_left_down(event)
3099
+
3100
+ event.skip
3101
+ end
3102
+
3103
+ # Original private event handler called when the canvas is double-clicked by
3104
+ # left mouse button. The handler calls user-overridable event handler function
3105
+ # and skips the event for next possible processing.
3106
+ # @param [Wx::MouseEvent] event Mouse event
3107
+ # @see Wx::SF::ShapeCanvas#on_left_double_click
3108
+ def _on_left_double_click(event)
3109
+ on_left_double_click(event)
3110
+
3111
+ event.skip
3112
+ end
3113
+
3114
+ # Original private event handler called when the left mouse button
3115
+ # is release above the canvas. The handler calls user-overridable event handler function
3116
+ # and skips the event for next possible processing.
3117
+ # @param [Wx::MouseEvent] event Mouse event
3118
+ # @see Wx::SF::ShapeCanvas#on_left_up
3119
+ def _on_left_up(event)
3120
+ on_left_up(event)
3121
+
3122
+ event.skip
3123
+ end
3124
+
3125
+ # Original private event handler called when the canvas is clicked by
3126
+ # right mouse button. The handler calls user-overridable event handler function
3127
+ # and skips the event for next possible processing.
3128
+ # @param [Wx::MouseEvent] event Mouse event
3129
+ # @see Wx::SF::ShapeCanvas#on_right_down
3130
+ def _on_right_down(event)
3131
+ on_right_down(event)
3132
+
3133
+ event.skip
3134
+ end
3135
+
3136
+ # Original private event handler called when the canvas is double-clicked by
3137
+ # right mouse button. The handler calls user-overridable event handler function
3138
+ # and skips the event for next possible processing.
3139
+ # @param [Wx::MouseEvent] event Mouse event
3140
+ # @see Wx::SF::ShapeCanvas#on_right_double_click
3141
+ def _on_right_double_click(event)
3142
+ on_right_double_click(event)
3143
+
3144
+ event.skip
3145
+ end
3146
+
3147
+ # Original private event handler called when the right mouse button
3148
+ # is release above the canvas. The handler calls user-overridable event handler function
3149
+ # and skips the event for next possible processing.
3150
+ # @param [Wx::MouseEvent] event Mouse event
3151
+ # @see Wx::SF::ShapeCanvas#on_right_up
3152
+ def _on_right_up(event)
3153
+ on_right_up(event)
3154
+
3155
+ event.skip
3156
+ end
3157
+
3158
+ # Original private event handler called when the mouse pointer is moving above
3159
+ # the canvas. The handler calls user-overridable event handler function
3160
+ # and skips the event for next possible processing.
3161
+ # @param [Wx::MouseEvent] event Mouse event
3162
+ # @see Wx::SF::ShapeCanvas#on_mouse_move
3163
+ def _on_mouse_move(event)
3164
+ lpos = dp2lp(event.get_position)
3165
+
3166
+ update_shape_under_cursor_cache(lpos)
3167
+
3168
+ # call user event handler
3169
+ on_mouse_move(event)
3170
+
3171
+ event.skip
3172
+ end
3173
+
3174
+ # Original private event handler called when the mouse wheel position is changed.
3175
+ # The handler calls user-overridable event handler function and skips the event
3176
+ # for next possible processing.
3177
+ # @param [Wx::MouseEvent] event Mouse event
3178
+ # @see Wx::SF::ShapeCanvas#on_mouse_wheel
3179
+ def _on_mouse_wheel(event)
3180
+ on_mouse_wheel(event) if has_style?(STYLE::PROCESS_MOUSEWHEEL)
3181
+
3182
+ event.skip
3183
+ end
3184
+
3185
+ # Original private event handler called when any key is pressed.
3186
+ # The handler calls user-overridable event handler function
3187
+ # and skips the event for next possible processing.
3188
+ # @param [Wx::KeyEvent] event Keyboard event
3189
+ # @see Wx::SF::ShapeCanvas#on_key_down
3190
+ def _on_key_down(event)
3191
+ on_key_down(event)
3192
+
3193
+ event.skip
3194
+ end
3195
+
3196
+ if Wx.has_feature?(:USE_DRAG_AND_DROP)
3197
+
3198
+ # Function is called by associated wxSFCanvasDropTarget after any dragged shapes
3199
+ # are dropped to the canvas.
3200
+ # @param [Integer] x X-coordinate of a position the data was dropped to
3201
+ # @param [Integer] y Y-coordinate of a position the data was dropped to
3202
+ # @param [Wx::DragResult] deflt Drag result
3203
+ # @param [Wx::ShapeDataObject] data a data object encapsulating dropped data
3204
+ # @see Wx::SF::CanvasDropTarget
3205
+ def _on_drop(x, y, deflt, data)
3206
+ if data && Wx::SF::ShapeDataObject === data
3207
+ lst_new_content = Wx::SF::Serializable.deserialize(data.get_data_here)
3208
+ if lst_new_content && !lst_new_content.empty?
3209
+ lst_parents_to_update = []
3210
+ lpos = dp2lp(Wx::Point.new(x, y))
3211
+
3212
+ dx = 0
3213
+ dy = 0
3214
+ if @dnd_started_here
3215
+ dx = lpos.x - @dnd_started_at.x
3216
+ dy = lpos.y - @dnd_started_at.y
3217
+ end
3218
+
3219
+ parent = @diagram.get_shape_at_position(lpos, 1, SEARCHMODE::UNSELECTED)
3220
+
3221
+ # add each shape to diagram keeping only those that are accepted
3222
+ lst_new_content.select! do |shape|
3223
+ shape.move_by(dx, dy)
3224
+ # do not reparent connection lines
3225
+ rc = if shape.is_a?(LineShape) && !shape.stand_alone?
3226
+ @diagram.add_shape(shape,
3227
+ nil,
3228
+ lp2dp(shape.get_absolute_position.to_point),
3229
+ INITIALIZE,
3230
+ DONT_SAVE_STATE)
3231
+ else
3232
+ @diagram.add_shape(shape,
3233
+ parent,
3234
+ lp2dp((shape.get_absolute_position - parent.get_absolute_position).to_point),
3235
+ INITIALIZE,
3236
+ DONT_SAVE_STATE)
3237
+ end
3238
+ rc == ERRCODE::OK # keep or remove?
3239
+ end
3240
+
3241
+ # verify newly added shapes (may remove shapes from list)
3242
+ @diagram.send(:check_new_shapes, lst_new_content)
3243
+
3244
+ update_virtual_size # update for new shapes
3245
+
3246
+ # notify parents and collect for update
3247
+ lst_new_content.each do |shape|
3248
+ if (parent_shape = shape.get_parent_shape)
3249
+ parent_shape.on_child_dropped(shape.get_absolute_position - parent_shape.get_absolute_position,
3250
+ shape)
3251
+ lst_parents_to_update << parent_shape unless lst_parents_to_update.include?(parent_shape)
3252
+ end
3253
+ end
3254
+
3255
+ deselect_all
3256
+
3257
+ lst_parents_to_update.each { |shape| shape.update }
3258
+
3259
+ unless @dnd_started_here
3260
+ save_canvas_state
3261
+ refresh(false)
3262
+ end
3263
+
3264
+ # call user-defined drop handler
3265
+ on_drop(x, y, deflt, lst_new_content)
3266
+ end
3267
+ end
3268
+ end
3269
+
3270
+ end
3271
+
3272
+ def _notify_canvas_change(change, *args)
3273
+ @diagram.get_all_shapes.each { |shape| shape.send(:_on_canvas, change, *args) } if @diagram
3274
+ end
3275
+
3276
+ def _query_canvas_change(change, *args)
3277
+ return true unless @diagram
3278
+ @diagram.get_all_shapes.all? { |shape| shape.send(:_on_canvas, change, *args) }
3279
+ end
3280
+
3281
+ end # class ShapeCanvas
3282
+
3283
+ end
3284
+
3285
+ # module Wx::SF