lib/wx/shapes/shape_canvas.rb in wxruby3-shapes-0.9.0.pre.beta.3 vs lib/wx/shapes/shape_canvas.rb in wxruby3-shapes-0.9.5

- old
+ new

@@ -48,11 +48,11 @@ # for invoking of default event handlers/virtual functions otherwise the # built in functionality wont be available. # @see Wx::SF::Diagram class ShapeCanvas < Wx::ScrolledWindow - # Working modes + # Working modes class MODE < Wx::Enum # The shape canvas is in ready state (no operation is pending) READY = self.new(0) # Some shape handle is dragged HANDLEMOVE = self.new(1) @@ -190,41 +190,60 @@ FAILED_AND_CONTINUE_EDIT = self.new(2) end # Default values module DEFAULT - # Default value of Wx::SF::CanvasSettings @background_color data member - BACKGROUNDCOLOR = Wx::Colour.new(240, 240, 240) if Wx::App.is_main_loop_running - Wx.add_delayed_constant(self, :BACKGROUNDCOLOR) { Wx::Colour.new(240, 240, 240) } + class << self + # Default value of Wx::SF::CanvasSettings @background_color data member + def background_color; @bgcolor ||= Wx::Colour.new(240, 240, 240); end + # Default value of Wx::SF::CanvasSettings @common_hover_color data member + def hover_color; @hvrcolor ||= Wx::Colour.new(120, 120, 255); end + # Default value of Wx::SF::CanvasSettings @common_border_pen data member + def border_pen; @border_pen ||= Wx::BLACK_PEN.dup; end + # Default value of Wx::SF::CanvasSettings @common_fill_brush data member + def fill_brush; @fill_brush ||= Wx::WHITE_BRUSH.dup; end + # Default value of Wx::SF::CanvasSettings @common_line_pen data member + def line_pen; @line_pen ||= border_pen; end + # Default value of Wx::SF::CanvasSettings @common_arrow_fill data member + def arrow_fill; @arrow_fill ||= fill_brush; end + # Default value of Wx::SF::CanvasSettings @common_text_color data member + def text_color; @text_color ||= Wx::BLACK.dup; end + # Default value of Wx::SF::CanvasSettings @common_text_fill data member + def text_fill; @text_fill ||= Wx::TRANSPARENT_BRUSH.dup; end + # Default value of Wx::SF::CanvasSettings @common_text_border data member + def text_border; @text_border ||= Wx::TRANSPARENT_PEN.dup; end + # Default value of Wx::SF::CanvasSettings @common_text_font data member + def text_font; begin; @text_font = Wx::SWISS_FONT.dup; @text_font.point_size = 12; end unless @text_font; @text_font; end + # Default value of Wx::SF::CanvasSettings @common_control_fill data member + def control_fill; @ctrl_fill ||= Wx::TRANSPARENT_BRUSH.dup; end + # Default value of Wx::SF::CanvasSettings @common_control_border data member + def control_border; @ctrl_border ||= Wx::TRANSPARENT_PEN.dup; end + # Default value of Wx::SF::CanvasSettings @common_control_mod_fill data member + def control_mod_fill; @ctrl_mod_fill ||= Wx::Brush.new(Wx::BLUE, Wx::BrushStyle::BRUSHSTYLE_BDIAGONAL_HATCH); end + # Default value of Wx::SF::CanvasSettings @common_control_mod_border data member + def control_mod_border; @ctrl_mod_border ||= Wx::Pen.new(Wx::BLUE, 1, Wx::PenStyle::PENSTYLE_SOLID); end + # Default value of Wx::SF::CanvasSettings @grid_color data member + def grid_color; @gridcolor ||= Wx::Colour.new(200, 200, 200); end + # Default value of Wx::SF::CanvasSettings @gradient_from data member + def gradient_from; @gradcolor_from ||= Wx::Colour.new(240, 240, 240); end + # Default value of Wx::SF::CanvasSettings @gradient_to data member + def gradient_to; @gradcolor_to ||= Wx::Colour.new(200, 200, 255); end + # Default shadow colour + def shadow_color; @shadowcolor ||= Wx::Colour.new(150, 150, 150, 128); end + # Default value of Wx::SF::CanvasSettings @shadow_fill data member + def shadow_brush; @shadowbrush ||= Wx::Brush.new(shadow_color, Wx::BrushStyle::BRUSHSTYLE_SOLID); end + end # Default value of Wx::SF::CanvasSettings @grid_size data member - GRIDSIZE = Wx::Size.new(10, 10) + GRIDSIZE = 10 # Default value of Wx::SF::CanvasSettings @grid_line_mult data member GRIDLINEMULT = 1 - # Default value of Wx::SF::CanvasSettings @grid_color data member - GRIDCOLOR = Wx::Colour.new(200, 200, 200) if Wx::App.is_main_loop_running - Wx.add_delayed_constant(self, :GRIDCOLOR) { Wx::Colour.new(200, 200, 200) } # Default value of Wx::SF::CanvasSettings @grid_style data member GRIDSTYLE = Wx::PenStyle::PENSTYLE_SOLID - # Default value of Wx::SF::CanvasSettings @common_hover_color data member - HOVERCOLOR = Wx::Colour.new(120, 120, 255) if Wx::App.is_main_loop_running - Wx.add_delayed_constant(self, :HOVERCOLOR) { Wx::Colour.new(120, 120, 255) } - # Default value of Wx::SF::CanvasSettings @gradient_from data member - GRADIENT_FROM = Wx::Colour.new(240, 240, 240) if Wx::App.is_main_loop_running - Wx.add_delayed_constant(self, :GRADIENT_FROM) { Wx::Colour.new(240, 240, 240) } - # Default value of Wx::SF::CanvasSettings @gradient_to data member - GRADIENT_TO = Wx::Colour.new(200, 200, 255) if Wx::App.is_main_loop_running - Wx.add_delayed_constant(self, :GRADIENT_TO) { Wx::Colour.new(200, 200, 255) } # Default value of Wx::SF::CanvasSettings @style data member CANVAS_STYLE = STYLE::DEFAULT_CANVAS_STYLE # Default value of Wx::SF::CanvasSettings @shadow_offset data member SHADOWOFFSET = Wx::RealPoint.new(4, 4) - # Default shadow colour - SHADOWCOLOR = Wx::Colour.new(150, 150, 150, 128) if Wx::App.is_main_loop_running - Wx.add_delayed_constant(self, :SHADOWCOLOR) { Wx::Colour.new(150, 150, 150, 128) } - # Default value of Wx::SF::CanvasSettings @shadow_fill data member - SHADOWBRUSH = Wx::Brush.new(SHADOWCOLOR.call, Wx::BrushStyle::BRUSHSTYLE_SOLID) if Wx::App.is_main_loop_running - Wx.add_delayed_constant(self, :SHADOWBRUSH) { Wx::Brush.new(Wx::Colour.new(150, 150, 150, 128), Wx::BrushStyle::BRUSHSTYLE_SOLID) } # Default value of Wx::SF::CanvasSettings @print_h_align data member PRINT_HALIGN = HALIGN::CENTER # Default value of Wx::SF::CanvasSettings @print_v_align data member PRINT_VALIGN = VALIGN::MIDDLE # Default value of Wx::SF::CanvasSettings @print_mode data member @@ -316,11 +335,11 @@ def to_s "#{major}.#{minor}.#{release}" end end - include Serializable + include FIRM::Serializable property :version_info def initialize # get version numbers as [major, minor, release] @@ -348,40 +367,72 @@ end # Auxiliary serializable class encapsulating the canvas properties. class Settings - include Serializable + include FIRM::Serializable include DEFAULT - property :scale, :min_scale, :max_scale, :background_color, :common_hover_color, - :grid_size, :grid_line_mult, :grid_color, :grid_style, + property :scale, :min_scale, :max_scale, :background_color, + :common_hover_color, :common_border_pen, :common_fill_brush, :common_line_pen, + :common_arrow_fill, :common_text_color, :common_text_fill, :common_text_border, :common_text_font, + :common_control_fill, :common_control_border, :common_control_mod_fill, :common_control_mod_border, + :grid_line_mult, :grid_color, :grid_style, :gradient_from, :gradient_to, :style, :shadow_offset, :shadow_fill, :print_h_align, :print_v_align, :print_mode + property grid_size: ->(obj, *val) { + unless val.empty? + obj.grid_size = Wx::Size === val.first ? val.first.x : val.first + end + obj.grid_size + } def initialize @scale = 1.0 @min_scale = SCALE_MIN @max_scale = SCALE_MAX - @background_color = BACKGROUNDCOLOR - @common_hover_color = HOVERCOLOR - @grid_size = GRIDSIZE.dup + @background_color = DEFAULT.background_color + + # common shape property + @common_hover_color = DEFAULT.hover_color + # common rect shape properties + @common_border_pen = DEFAULT.border_pen + @common_fill_brush = DEFAULT.fill_brush + # common line shape property + @common_line_pen = DEFAULT.line_pen + # common arrow property + @common_arrow_fill = DEFAULT.arrow_fill + # common text shape properties (fill and border overrule common rect properties) + @common_text_color = DEFAULT.text_color + @common_text_fill = DEFAULT.text_fill + @common_text_border = DEFAULT.text_border + @common_text_font = DEFAULT.text_font + # common control shape properties + @common_control_fill = DEFAULT.control_fill + @common_control_border = DEFAULT.control_border + @common_control_mod_fill = DEFAULT.control_mod_fill + @common_control_mod_border = DEFAULT.control_mod_border + + @grid_size = GRIDSIZE @grid_line_mult = GRIDLINEMULT - @grid_color = GRIDCOLOR + @grid_color = DEFAULT.grid_color @grid_style = GRIDSTYLE - @gradient_from = GRADIENT_FROM - @gradient_to = GRADIENT_TO + @gradient_from = DEFAULT.gradient_from + @gradient_to = DEFAULT.gradient_to @style = CANVAS_STYLE @shadow_offset = SHADOWOFFSET.dup - @shadow_fill = SHADOWBRUSH + @shadow_fill = DEFAULT.shadow_brush @print_h_align = PRINT_HALIGN @print_v_align = PRINT_VALIGN @print_mode = PRINT_MODE end - attr_accessor :scale, :min_scale, :max_scale, :background_color, :common_hover_color, + attr_accessor :scale, :min_scale, :max_scale, :background_color, + :common_hover_color, :common_border_pen, :common_fill_brush, :common_line_pen, + :common_arrow_fill, :common_text_color, :common_text_fill, :common_text_border, :common_text_font, + :common_control_fill, :common_control_border, :common_control_mod_fill, :common_control_mod_border, :grid_size, :grid_line_mult, :grid_color, :grid_style, :gradient_from, :gradient_to, :style, :shadow_offset, :shadow_fill, :print_h_align, :print_v_align, :print_mode end @@ -439,11 +490,11 @@ self.diagram = diagram save_canvas_state end - + # set up event handlers evt_paint :_on_paint evt_erase_background :_on_erase_background evt_left_down :_on_left_down evt_left_up :_on_left_up @@ -478,19 +529,17 @@ set_drop_target(Wx::SF::CanvasDropTarget.new(Wx::SF::ShapeDataObject.new, self)) end # initialize selection rectangle @shp_selection = MultiSelRect.new - @shp_selection.send(:set_id, nil) @shp_selection.create_handles @shp_selection.select(true) @shp_selection.show(false) @shp_selection.show_handles(true) # initialize multi-edit rectangle @shp_multi_edit = MultiSelRect.new - @shp_multi_edit.send(:set_id, nil) @shp_multi_edit.create_handles @shp_multi_edit.select(true) @shp_multi_edit.show(false) @shp_multi_edit.show_handles(true) @@ -521,55 +570,102 @@ end # Load serialized canvas content (diagrams). # @overload load_canvas(file) # @param [String] file Full file name + # @param [Symbol,nil] format specifies the serialization format to use; + # determined from file extension if not specified defaulting to :json for unknown extensions # @return [self] # @overload load_canvas(io) # @param [IO] io IO object + # @param [Symbol,nil] format specifies the serialization format to use (by default :json) # @return [self] - def load_canvas(io) + def load_canvas(io, format: nil) # get IO stream to read from - ios = io.is_a?(::String) ? File.open(io, 'r') : io + ios = if io.is_a?(::String) + format ||= case File.extname(io) + when '.json' then :json + when '.yaml', '.yml' then :yaml + when '.xml' then :xml + else + :json + end + File.open(io, 'r') + else + format ||= :json + io + end + old_diagram = @diagram + old_settings = @settings begin - _, @settings, diagram = Serializable.deserialize(ios) - rescue SFException + begin + _, @settings, diagram = FIRM.deserialize(ios, format: format) + rescue SFException + ::Kernel.raise + rescue ::Exception + $stderr.puts "#{$!}\n#{$!.backtrace.join("\n")}\n" + ::Kernel.raise SFException, "Failed to load canvas: #{$!.message}" + ensure + ShapeCanvas.reset_compat_loading + ios.close if io.is_a?(::String) && ios + end + set_diagram(diagram) + clear_canvas_history + save_canvas_state + set_scale(@settings.scale) + update_virtual_size + refresh(false) + rescue Exception + $stderr.puts "#{$!}\n#{$!.backtrace.join("\n")}\n" + # restore previous state + @settings = old_settings + set_diagram(old_diagram) + clear_canvas_history + save_canvas_state + set_scale(@settings.scale) + update_virtual_size + refresh(false) + # propagate exception ::Kernel.raise - rescue ::Exception - ::Kernel.raise SFException, "Failed to load canvas: #{$!.message}" - ensure - ShapeCanvas.reset_compat_loading - ios.close if io.is_a?(::String) && ios end - set_diagram(diagram) - clear_canvas_history - save_canvas_state - set_scale(@settings.scale) - update_virtual_size - refresh(false) @diagram.set_modified(false) self end # Save canvas content (diagrams). # @overload save_canvas(file, compact: true) # @param [String] file Full file name # @param [Boolean] compact specifies whether to write content in compact mode (true) or not (false) + # @param [Symbol,nil] format specifies the serialization format to use; + # determined from file extension if not specified defaulting to :json for unknown extensions # @return [self] # @overload save_canvas(io, compact: true) # @param [IO] io IO object # @param [Boolean] compact specifies whether to write content in compact mode (true) or not (false) + # @param [Symbol,nil] format specifies the serialization format to use (by default :json) # @return [self] - def save_canvas(io, compact: true) + def save_canvas(io, compact: true, format: nil) return self unless @diagram # get IO stream to write to - ios = io.is_a?(::String) ? Tempfile.new(File.basename(io, '.*')) : io + ios = if io.is_a?(::String) + format ||= case File.extname(io) + when '.json' then :json + when '.yaml', '.yml' then :yaml + when '.xml' then :xml + else + :json + end + Tempfile.new(File.basename(io, '.*')) + else + format ||= :json + io + end # write canvas data to temp file begin - [Version.new, @settings, @diagram].serialize(ios, pretty: !compact) + [Version.new, @settings, @diagram].serialize(ios, pretty: !compact, format: format) rescue SFException ::Kernel.raise rescue Exception ::Kernel.raise SFException, "Error writing canvas: #{$!.message}" end @@ -634,11 +730,11 @@ bmp_bb.left = (bmp_bb.left * scale).to_i bmp_bb.top = (bmp_bb.top * scale).to_i bmp_bb.width = (bmp_bb.width * scale).to_i bmp_bb.height = (bmp_bb.height * scale).to_i - bmp_bb.inflate!(@settings.grid_size * scale) + bmp_bb.inflate!(Wx::Size.new(@settings.grid_size, @settings.grid_size) * scale) outbmp = Wx::Bitmap.new(bmp_bb.width, bmp_bb.height) Wx::MemoryDC.draw_on(outbmp) do |mdc| Wx::ScaledDC.draw_on(mdc, scale) do |outdc| @@ -679,16 +775,16 @@ end end nil end - def _start_interactive_connection(lpos, src_shape_id, cpt) + def _start_interactive_connection(lpos, src_shape, cpt) if @new_line_shape @working_mode = MODE::CREATECONNECTION @new_line_shape.send(:set_line_mode, LineShape::LINEMODE::UNDERCONSTRUCTION) - @new_line_shape.set_src_shape_id(src_shape_id) + @new_line_shape.set_src_shape(src_shape) # switch on the "under-construction" mode @new_line_shape.send(:set_unfinished_point, lpos) # assign starting point of new line shapes to the nearest connection point of # connected shape if exists @@ -730,19 +826,19 @@ shape_info = shape = pos = connection_point = nil shape_klass = nil case args.first when Wx::SF::LineShape shape = args.shift - shape_klass = shape.class.name + shape_klass = shape.class if args.first.is_a?(Wx::SF::ConnectionPoint) connection_point = args.shift end pos = args.shift.to_point when ::Class shape_info = args.shift pos = args.shift.to_point - shape_klass = shape_info.name + shape_klass = shape_info end ::Kernel.raise ArgumentError, "Invalid arguments #{args}" unless args.empty? return ERRCODE::INVALID_INPUT unless pos lpos = dp2lp(pos) @@ -754,11 +850,11 @@ if @diagram.contains?(shape) @new_line_shape = shape else @new_line_shape = @diagram.add_shape(shape, nil, Wx::DEFAULT_POSITION, INITIALIZE, DONT_SAVE_STATE) end - return _start_interactive_connection(lpos, connection_point.get_parent_shape.id, connection_point) + return _start_interactive_connection(lpos, connection_point.get_parent_shape, connection_point) else shape_under = get_shape_at_position(lpos) if shape_info @@ -766,22 +862,22 @@ shape_under = shape_under.get_parent_shape while shape_under && shape_under.has_style?(Shape::STYLE::PROPAGATE_INTERACTIVE_CONNECTION) end # start the connection's creation process if possible - if shape_under&.id && shape_under.is_connection_accepted(shape_klass) + if shape_under && shape_under.is_connection_accepted(shape_klass) if shape && @diagram.contains?(shape) @new_line_shape = shape else if shape err = @diagram.add_shape(shape, nil, Wx::DEFAULT_POSITION, INITIALIZE, DONT_SAVE_STATE) else err, shape = @diagram.create_shape(shape_info, DONT_SAVE_STATE) end @new_line_shape = (err == ERRCODE::OK ? shape : nil) end - return _start_interactive_connection(lpos, shape_under.id, shape_under.get_nearest_connection_point(lpos.to_real)) + return _start_interactive_connection(lpos, shape_under, shape_under.get_nearest_connection_point(lpos.to_real)) else return ERRCODE::NOT_ACCEPTED end end @@ -804,11 +900,11 @@ # Select all shapes in the canvas def select_all return unless @diagram - shapes = @diagram.get_shapes + shapes = @diagram.get_all_shapes unless shapes.empty? shapes.each { |shape| shape.select(true) } validate_selection(get_selected_shapes) @@ -824,20 +920,20 @@ # Deselect all shapes def deselect_all return unless @diagram - @diagram.get_shapes.each { |shape| shape.select(false) } + @diagram.get_all_shapes.each { |shape| shape.select(false) } @shp_multi_edit.show(false) end # Hide handles of all shapes def hide_all_handles return unless @diagram - @diagram.get_shapes.each { |shape| shape.show_handles(false) } + @diagram.get_all_shapes.each { |shape| shape.show_handles(false) } end # Repaint the shape canvas. # @param [Boolean] erase true if the canvas should be erased before repainting # @param [Wx::Rect] rct Refreshed region (rectangle) @@ -886,11 +982,11 @@ # @param [SHADOWMODE] style Shadow style # @see SHADOWMODE def show_shadows(show, style) return unless @diagram - shapes = @diagram.get_shapes + shapes = @diagram.get_all_shapes shapes.each do |shape| shape.remove_style(Shape::STYLE::SHOW_SHADOW) if show case style @@ -977,10 +1073,11 @@ validate_selection_for_clipboard(lst_selection,true) unless lst_selection.empty? data_obj = Wx::SF::ShapeDataObject.new(lst_selection) + clipboard.place(data_obj) restore_prev_positions end end @@ -1016,19 +1113,18 @@ Wx::Clipboard.open do |clipboard| # read data object from the clipboard data_obj = Wx::SF::ShapeDataObject.new if clipboard.fetch(data_obj) - # deserialize shapes - new_shapes = Wx::SF::Serializable.deserialize(data_obj.get_data_here) + new_shapes = data_obj.get_as_shapes # add new shapes to diagram and remove those that are not accepted new_shapes.select! do |shape| ERRCODE::OK == @diagram.add_shape(shape, nil, shape.get_relative_position, INITIALIZE, DONT_SAVE_STATE) end # verify newly added shapes (may remove shapes from list) - @diagram.send(:check_new_shapes, new_shapes) + @diagram.send(:on_import, new_shapes) update_virtual_size # update for new shapes # call user-defined handler on_paste(new_shapes) @@ -1080,12 +1176,12 @@ # @return [Boolean] def can_paste return false unless has_style?(STYLE::CLIPBOARD) Wx::Clipboard.open do |clipboard| - return clipboard.supported?(Wx::DataFormat.new(Wx::SF::ShapeDataObject::DataFormatID)) - end + return clipboard.supported?(Wx::SF::ShapeDataObject::DataFormatID) + end rescue false end alias :can_paste? :can_paste # Function returns true if undo operation can be done # @return [Boolean] @@ -1126,17 +1222,28 @@ # Restores given canvas state (unless nil given) # @param [String,nil] state to restore def restore_canvas_state(state) return unless state - set_diagram(Wx::SF::Serializable.deserialize(state)) + set_diagram(FIRM.deserialize(state)) update_virtual_size @diagram.set_modified refresh(false) end protected :restore_canvas_state + # Restores current last saved canvas state. + def restore_current_state + return unless has_style?(STYLE::UNDOREDO) + + clear_temporaries + + restore_canvas_state(@canvas_history.current_state) + @shp_multi_edit.show(false) + end + protected :restore_current_state + # @!group Print methods # Print current canvas content. # @overload print(prompt = PROMPT) # @param [Boolean] prompt If true (PROMPT) then the the native print dialog will be displayed before printing @@ -1245,16 +1352,16 @@ # @overload lp2dp(rct) # @param [Wx::Rect] rct Logical position (for example shape position) # @return [Wx::Rect] Device position def lp2dp(arg) if arg.is_a?(Wx::Rect) - x, y = calc_unscrolled_position(arg.x, arg.y) + x, y = calc_scrolled_position(arg.x, arg.y) Wx::Rect.new((x*@settings.scale).to_i, (y*@settings.scale).to_i, (arg.width*@settings.scale).to_i, (arg.height*@settings.scale).to_i) else arg = arg.to_point - x, y = calc_unscrolled_position(arg.x, arg.y) + x, y = calc_scrolled_position(arg.x, arg.y) Wx::Point.new((x*@settings.scale).to_i, (y*@settings.scale).to_i) end end # Search for any shape located at the (mouse cursor) position (result used by #get_shape_under_cursor) @@ -1279,11 +1386,11 @@ end else top_shape ||= shape if shape.selected? sel_shape ||= shape - else + elsif !shape.has_selected_parent? unsel_shape ||= shape end end end end @@ -1341,11 +1448,11 @@ return handle if handle.visible? && handle.contains?(pos) end end # ... then test normal handles - @diagram.get_shapes.each do |shape| + @diagram.get_all_shapes.each do |shape| # iterate through all shape's handles if shape.has_style?(Shape::STYLE::SIZE_CHANGE) shape.handles.each do |handle| return handle if handle.visible? && handle.contains?(pos) end @@ -1379,11 +1486,11 @@ # @return [Array<Wx::SF::Shape>] shapes shape list def get_selected_shapes(selection = []) return selection unless @diagram selection.clear - @diagram.get_shapes.each do |shape| + @diagram.get_all_shapes.each do |shape| selection << shape if shape.selected? end selection end @@ -1391,11 +1498,11 @@ # @return [Wx::Rect] Total bounding box def get_total_bounding_box virt_rct = nil if @diagram # calculate total bounding box (includes all shapes) - @diagram.get_shapes.each_with_index do |shape, ix| + @diagram.get_all_shapes.each_with_index do |shape, ix| if ix == 0 virt_rct = shape.get_bounding_box else virt_rct.union!(shape.get_bounding_box) end @@ -1405,18 +1512,18 @@ end # Get bounding box of all selected shapes. # @return [Wx::Rect] Selection bounding box def get_selection_bb - bb_rct = Wx::Rect.new + bb_rct = nil # get selected shapes get_selected_shapes.each do |shape| - shape.get_complete_bounding_box( - bb_rct, - Shape::BBMODE::SELF | Shape::BBMODE::CHILDREN | Shape::BBMODE::CONNECTIONS | Shape::BBMODE::SHADOW) + bb_rct = shape.get_complete_bounding_box(bb_rct, + Shape::BBMODE::SELF | Shape::BBMODE::CHILDREN | + Shape::BBMODE::CONNECTIONS | Shape::BBMODE::SHADOW) end - bb_rct + bb_rct || Wx::Rect.new end # Align selected shapes in given directions. # # Shapes will be aligned according to most far shape in appropriate direction. @@ -1436,11 +1543,11 @@ unless shape.is_a?(LineShape) pos = shape.get_absolute_position shape_bb = shape.get_bounding_box if cnt == 0 - min_pos = pos + min_pos = pos.dup max_pos = Wx::RealPoint.new(pos.x + shape_bb.width, pos.y + shape_bb.height) else min_pos.x = pos.x if pos.x < min_pos.x min_pos.y = pos.y if pos.y < min_pos.y max_pos.x = pos.x + shape_bb.width if (pos.x + shape_bb.width) > max_pos.x @@ -1541,18 +1648,22 @@ # Set canvas background color. # @param [Wx::Colour] col Background color def set_canvas_colour(col) @settings.background_color = col end + alias :set_canvas_color :set_canvas_colour alias :canvas_colour= :set_canvas_colour + alias :canvas_color= :set_canvas_colour # Get canvas background color. # @return [Wx::Colour] Background color def get_canvas_colour @settings.background_color end + alias :get_canvas_color :get_canvas_colour alias :canvas_colour :get_canvas_colour + alias :canvas_color :get_canvas_colour # Set starting gradient color. # @param [Wx::Colour] col Color def set_gradient_from(col) @settings.gradient_from = col @@ -1578,21 +1689,22 @@ def get_gradient_to @settings.gradient_to end alias :gradient_to :get_gradient_to - # Get grid size. - # @return [Wx::Size] Grid size + # Get grid size (px). + # @return [Integer] Grid size def get_grid_size @settings.grid_size end alias :grid_size :get_grid_size - # Set grid size. - # @param [Wx::Size] grid Grid size - def set_grid_size(grid) - @settings.grid_size = grid.to_size + # Set grid size (px). + # @param [Integer] sz Grid size + def set_grid_size(sz) + raise ArgumentError, 'Grid size must be integer > 0' if sz.to_i <= 0 + @settings.grid_size = sz.to_i end alias :grid_size= :set_grid_size # Set grid line multiple. # @@ -1614,18 +1726,22 @@ # Set grid color. # @param [Wx::Colour] col Grid color def set_grid_colour(col) @settings.grid_color = col end + alias :set_grid_color :set_grid_colour alias :grid_colour= :set_grid_colour + alias :grid_color= :set_grid_colour # Get grid color. # @return [Wx::Colour] Grid color def get_grid_colour @settings.grid_color end + alias :get_grid_color :get_grid_colour alias :grid_colour :get_grid_colour + alias :grid_color :get_grid_colour # Set grid line style. # @param [Wx::PenStyle] style Line style def set_grid_style(style) @settings.grid_style = style @@ -1652,13 +1768,23 @@ @settings.shadow_offset end alias :shadow_offset :get_shadow_offset # Set shadow fill (used for shadows of non-text shapes only). - # @param [Wx::Brush] brush Reference to brush object - def set_shadow_fill(brush) - @settings.shadow_fill = brush + # @overload set_shadow_fill(brush) + # @param [Wx::Brush] brush + # @overload set_shadow_fill(color, style=Wx::BrushStyle::BRUSHSTYLE_SOLID) + # @param [Wx::Colour,Symbol,String] color brush color + # @param [Wx::BrushStyle] style + # @overload set_shadow_fill(stipple_bitmap) + # @param [Wx::Bitmap] stipple_bitmap + def set_shadow_fill(*args) + @settings.shadow_fill = if args.size == 1 && Wx::Brush === args.first + args.first + else + Wx::Brush.new(*args) + end end alias :shadow_fill= :set_shadow_fill # Get shadow fill. # @return [Wx::Brush] Current shadow brush @@ -1809,28 +1935,309 @@ @working_mode end alias :mode :get_mode # Set default hover color. - # @param [Wx::Colour] col Hover color. + # @param [Wx::Colour,Symbol,String] col Hover color. def set_hover_colour(col) - return unless @diagram - - @settings.common_hover_color = col - - # update Hover color in all existing shapes - @diagram.get_shapes.each { |shape| shape.set_hover_colour(col) } + @settings.common_hover_color = Wx::Colour === col ? col : Wx::Colour.new(col) end + alias :set_hover_color :set_hover_colour alias :hover_colour= :set_hover_colour + alias :hover_color= :set_hover_colour # Get default hover colour. # @return [Wx::Colour] Hover colour def get_hover_colour @settings.common_hover_color end + alias :get_hover_color :get_hover_colour alias :hover_colour :get_hover_colour + alias :hover_color :get_hover_colour + # Set default fill brush. + # @overload set_fill_brush(brush) + # @param [Wx::Brush] brush + # @overload set_fill_brush(color, style=Wx::BrushStyle::BRUSHSTYLE_SOLID) + # @param [Wx::Colour,Symbol,String] color brush color + # @param [Wx::BrushStyle] style + # @overload set_fill_brush(stipple_bitmap) + # @param [Wx::Bitmap] stipple_bitmap + def set_fill_brush(*args) + @settings.common_fill_brush = if args.size == 1 && Wx::Brush === args.first + args.first + else + Wx::Brush.new(*args) + end + end + alias :fill_brush= :set_fill_brush + + # Get default fill brush. + # @return [Wx::Brush] Fill brush + def get_fill_brush + @settings.common_fill_brush + end + alias :fill_brush :get_fill_brush + + # Set default border pen. + # @overload set_border_pen(pen) + # @param [Wx::Pen] pen + # @overload set_border_pen(color, width=1, style=Wx::PenStyle::PENSTYLE_SOLID) + # @param [Wx::Colour,String,Symbol] color + # @param [Integer] width + # @param [Wx::PenStyle] style + def set_border_pen(*args) + @settings.common_border_pen = if args.size == 1 && Wx::Pen === args.first + args.first + else + Wx::Pen.new(*args) + end + end + alias :border_pen= :set_border_pen + + # Get default border pen. + # @return [Wx::Pen] + def get_border_pen + @settings.common_border_pen + end + alias :border_pen :get_border_pen + + # Set default line pen. + # @overload set_line_pen(pen) + # @param [Wx::Pen] pen + # @overload set_line_pen(color, width=1, style=Wx::PenStyle::PENSTYLE_SOLID) + # @param [Wx::Colour,String,Symbol] color + # @param [Integer] width + # @param [Wx::PenStyle] style + def set_line_pen(*args) + @settings.common_line_pen = if args.size == 1 && Wx::Pen === args.first + args.first + else + Wx::Pen.new(*args) + end + end + alias :line_pen= :set_line_pen + + # Get default line pen. + # @return [Wx::Pen] + def get_line_pen + @settings.common_line_pen + end + alias :line_pen :get_line_pen + + # Set default arrow fill brush. + # @overload set_arrow_fill(brush) + # @param [Wx::Brush] brush + # @overload set_arrow_fill(color, style=Wx::BrushStyle::BRUSHSTYLE_SOLID) + # @param [Wx::Colour,Symbol,String] color brush color + # @param [Wx::BrushStyle] style + # @overload set_arrow_fill(stipple_bitmap) + # @param [Wx::Bitmap] stipple_bitmap + def set_arrow_fill(*args) + @settings.common_arrow_fill = if args.size == 1 && Wx::Brush === args.first + args.first + else + Wx::Brush.new(*args) + end + end + alias :arrow_fill= :set_arrow_fill + + # Get default arrow fill brush. + # @return [Wx::Brush] Fill brush + def get_arrow_fill + @settings.common_arrow_fill + end + alias :arrow_fill :get_arrow_fill + + # Set default text color. + # @param [Wx::Colour,Symbol,String] col text color. + def set_text_colour(col) + @settings.common_text_color = Wx::Colour === col ? col : Wx::Colour.new(col) + end + alias :set_text_color= :set_text_colour + alias :text_colour= :set_text_colour + alias :text_color= :set_text_colour + + # Get default text colour. + # @return [Wx::Colour] text colour + def get_text_colour + @settings.common_text_color + end + alias :get_text_color :get_text_colour + alias :text_colour :get_text_colour + alias :text_color :get_text_colour + + # Set default text fill brush. + # @overload set_text_fill(brush) + # @param [Wx::Brush] brush + # @overload set_text_fill(color, style=Wx::BrushStyle::BRUSHSTYLE_SOLID) + # @param [Wx::Colour,Symbol,String] color brush color + # @param [Wx::BrushStyle] style + # @overload set_text_fill(stipple_bitmap) + # @param [Wx::Bitmap] stipple_bitmap + def set_text_fill(*args) + @settings.common_text_fill = if args.size == 1 && Wx::Brush === args.first + args.first + else + Wx::Brush.new(*args) + end + end + alias :text_fill= :set_text_fill + + # Get default text fill brush. + # @return [Wx::Brush] Fill brush + def get_text_fill + @settings.common_text_fill + end + alias :text_fill :get_text_fill + + # Set default text border. + # @overload set_text_border(pen) + # @param [Wx::Pen] pen + # @overload set_text_border(color, width=1, style=Wx::PenStyle::PENSTYLE_SOLID) + # @param [Wx::Colour,String,Symbol] color + # @param [Integer] width + # @param [Wx::PenStyle] style + def set_text_border(*args) + @settings.common_text_border = if args.size == 1 && Wx::Pen === args.first + args.first + else + Wx::Pen.new(*args) + end + end + alias :text_border= :set_text_border + + # Get default text border. + # @return [Wx::Pen] + def get_text_border + @settings.common_text_border + end + alias :text_border :get_text_border + + # Set default text font. + # @overload set_text_font(font) + # @param [Wx::Font] font + # @overload set_text_font(font_info) + # @param [Wx::FontInfo] font_info + # @overload set_text_font(pointSize, family, style, weight, underline=false, faceName=(''), encoding=Wx::FontEncoding::FONTENCODING_DEFAULT) + # @param pointSize [Integer] Size in points. See {Wx::Font#initialize}. + # @param family [Wx::FontFamily] The font family. See {Wx::Font#initialize}. + # @param style [Wx::FontStyle] One of {Wx::FontStyle::FONTSTYLE_NORMAL}, {Wx::FontStyle::FONTSTYLE_SLANT} and {Wx::FontStyle::FONTSTYLE_ITALIC}. See {Wx::Font#initialize}. + # @param weight [Wx::FontWeight] Font weight. One of the {Wx::FontWeight} enumeration values. See {Wx::Font#initialize}. + # @param underline [Boolean] The value can be true or false. See {Wx::Font#initialize}. + # @param faceName [String] An optional string specifying the face name to be used. See {Wx::Font#initialize}. + # @param encoding [Wx::FontEncoding] An encoding which may be one of the enumeration values of {Wx::FontEncoding}. See {Wx::Font#initialize}. + def set_text_font(*args) + @settings.common_text_font = if args.size == 1 && Wx::Font === args.first + args.first + else + Wx::Font.new(*args) + end + end + alias :text_font= :set_text_font + + # Get default text font. + # @return [Wx::Font] + def get_text_font + @settings.common_text_font + end + alias :text_font :get_text_font + + # Set default control fill brush. + # @overload set_control_fill(brush) + # @param [Wx::Brush] brush + # @overload set_control_fill(color, style=Wx::BrushStyle::BRUSHSTYLE_SOLID) + # @param [Wx::Colour,Symbol,String] color brush color + # @param [Wx::BrushStyle] style + # @overload set_control_fill(stipple_bitmap) + # @param [Wx::Bitmap] stipple_bitmap + def set_control_fill(*args) + @settings.common_control_fill = if args.size == 1 && Wx::Brush === args.first + args.first + else + Wx::Brush.new(*args) + end + end + alias :control_fill= :set_control_fill + + # Get default control fill brush. + # @return [Wx::Brush] Fill brush + def get_control_fill + @settings.common_control_fill + end + alias :control_fill :get_control_fill + + # Set default control border. + # @overload set_control_border(pen) + # @param [Wx::Pen] pen + # @overload set_control_border(color, width=1, style=Wx::PenStyle::PENSTYLE_SOLID) + # @param [Wx::Colour,String,Symbol] color + # @param [Integer] width + # @param [Wx::PenStyle] style + def set_control_border(*args) + @settings.common_control_border = if args.size == 1 && Wx::Pen === args.first + args.first + else + Wx::Pen.new(*args) + end + end + alias :control_border= :set_control_border + + # Get default control border. + # @return [Wx::Pen] + def get_control_border + @settings.common_control_border + end + alias :control_border :get_control_border + + # Set default control modification fill brush. + # @overload set_control_mod_fill(brush) + # @param [Wx::Brush] brush + # @overload set_control_mod_fill(color, style=Wx::BrushStyle::BRUSHSTYLE_SOLID) + # @param [Wx::Colour,Symbol,String] color brush color + # @param [Wx::BrushStyle] style + # @overload set_control_mod_fill(stipple_bitmap) + # @param [Wx::Bitmap] stipple_bitmap + def set_control_mod_fill(*args) + @settings.common_control_mod_fill = if args.size == 1 && Wx::Brush === args.first + args.first + else + Wx::Brush.new(*args) + end + end + alias :control_mod_fill= :set_control_mod_fill + + # Get default control modification fill brush. + # @return [Wx::Brush] Fill brush + def get_control_mod_fill + @settings.common_control_mod_fill + end + alias :control_mod_fill :get_control_mod_fill + + # Set default control modification border. + # @overload set_control_mod_border(pen) + # @param [Wx::Pen] pen + # @overload set_control_mod_border(color, width=1, style=Wx::PenStyle::PENSTYLE_SOLID) + # @param [Wx::Colour,String,Symbol] color + # @param [Integer] width + # @param [Wx::PenStyle] style + def set_control_mod_border(*args) + @settings.common_control_mod_border = if args.size == 1 && Wx::Pen === args.first + args.first + else + Wx::Pen.new(*args) + end + end + alias :control_mod_border= :set_control_mod_border + + # Get default control modification border. + # @return [Wx::Pen] + def get_control_mod_border + @settings.common_control_mod_border + end + alias :control_mod_border :get_control_mod_border + # Get canvas history manager. # @return [Wx::SF::CanvasHistory] the canvas history manager # @see Wx::SF::CanvasHistory def get_history_manager @canvas_history @@ -1841,12 +2248,12 @@ # @param [Wx::Point] pos Position which should be updated # @return [Wx::Point] Updated position def fit_position_to_grid(pos) pos = pos.to_point if has_style?(STYLE::GRID_USE) - Wx::Point.new(pos.x / @settings.grid_size.x * @settings.grid_size.x, - pos.y / @settings.grid_size.y * @settings.grid_size.y) + Wx::Point.new(pos.x / @settings.grid_size * @settings.grid_size, + pos.y / @settings.grid_size * @settings.grid_size) else pos end end @@ -1903,11 +2310,11 @@ shape.move_by(dx, dy) unless shape.get_parent_shape end move_shapes_from_negatives end - + # Validate selection (remove redundantly selected shapes etc...). # @param [Array<Wx::SF::Shape>] selection List of selected shapes that should be validated def validate_selection(selection) return unless @diagram @@ -1929,25 +2336,53 @@ shape = shape.get_parent_shape while shape.get_parent_shape @diagram.move_to_end(shape) end end + # Draws shapes intersecting the update region + def draw_shape_updates(dc, upd_rct, lst_to_draw, exclude_selected = false) + lst_selected = exclude_selected ? [] : nil + lst_lines_to_draw = [] + # draw unselected non line-based shapes first... + lst_to_draw.each do |shape| + if exclude_selected && (shape.selected? || shape.has_selected_parent?) + lst_selected << shape + else + if !shape.is_a?(LineShape) || shape.stand_alone? + if shape.intersects?(upd_rct) + parent_shape = shape.get_parent_shape + if parent_shape + shape.draw(dc, WITHOUTCHILDREN) if !parent_shape.is_a?(LineShape) || parent_shape.stand_alone? + else + shape.draw(dc, WITHOUTCHILDREN) + end + end + else + lst_lines_to_draw << shape + end + end + end + + # ... and draw connections + bb_rct = nil + lst_lines_to_draw.each do |line| + bb_rct = line.get_complete_bounding_box(bb_rct, Shape::BBMODE::SELF | Shape::BBMODE::CHILDREN | Shape::BBMODE::SHADOW) + line.draw(dc, line.get_line_mode == LineShape::LINEMODE::READY) if bb_rct.intersects(upd_rct) + end + lst_selected + end + private :draw_shape_updates + # Function responsible for drawing of the canvas's content to given DC. The default # implementation draws actual objects managed by assigned diagram manager. # @param [Wx::DC] dc device context where the shapes will be drawn to # @param [Boolean] from_paint Set the argument to true if the dc argument refers to the Wx::PaintDC instance # or derived classes (i.e. the function is called as a response to Wx::EVT_PAINT event) def draw_content(dc, from_paint) return unless @diagram if from_paint - # wxRect updRct - bb_rct = Wx::Rect.new - # - # ShapeList m_lstToDraw - lst_lines_to_draw = [] - # get all existing shapes lst_to_draw = @diagram.get_shapes(Shape, Shape::SEARCHMODE::DFS) upd_rct = nil # get the update rect list @@ -1962,55 +2397,17 @@ end end upd_rct ||= Wx::Rect.new if @working_mode == MODE::SHAPEMOVE - # draw unselected non line-based shapes first... - lst_to_draw.each do |shape| - parent_shape = shape.get_parent_shape + # draw unselected shapes first and filter and return selected shapes + lst_selected = draw_shape_updates(dc, upd_rct, lst_to_draw, true) - if !shape.is_a?(LineShape) || shape.stand_alone? - if shape.intersects?(upd_rct) - if parent_shape - shape.draw(dc, WITHOUTCHILDREN) if !parent_shape.is_a?(LineShape) || parent_shape.stand_alone? - else - shape.draw(dc, WITHOUTCHILDREN) - end - end - else - lst_lines_to_draw << shape - end - end - - # ... and draw connections - lst_lines_to_draw.each do |line| - line.get_complete_bounding_box(bb_rct, Shape::BBMODE::SELF | Shape::BBMODE::CHILDREN | Shape::BBMODE::SHADOW) - line.draw(dc, line.get_line_mode == LineShape::LINEMODE::READY) if bb_rct.intersects(upd_rct) - end + # ... and now draw the selected shapes being moved + draw_shape_updates(dc, upd_rct, lst_selected) else - # draw parent shapes (children are processed by parent objects) - lst_to_draw.each do |shape| - parent_shape = shape.get_parent_shape - - if !shape.is_a?(LineShape) || shape.stand_alone? - if shape.intersects?(upd_rct) - if parent_shape - shape.draw(dc, WITHOUTCHILDREN) if !parent_shape.is_a?(LineShape) || shape.stand_alone? - else - shape.draw(dc, WITHOUTCHILDREN) - end - end - else - lst_lines_to_draw << shape - end - end - - # draw connections - lst_lines_to_draw.each do |line| - line.get_complete_bounding_box(bb_rct, Shape::BBMODE::SELF | Shape::BBMODE::CHILDREN) - line.draw(dc, line.get_line_mode == LineShape::LINEMODE::READY) if bb_rct.intersects(upd_rct) - end + draw_shape_updates(dc, upd_rct, lst_to_draw) end # draw multiselection if necessary @shp_selection.draw(dc) if @shp_selection.visible? @shp_multi_edit.draw(dc) if @shp_multi_edit.visible? @@ -2033,11 +2430,11 @@ # @param [Boolean] _from_paint Set the argument to true if the dc argument refers to the Wx::PaintDC instance # or derived classes (i.e. the function is called as a response to Wx::EVT_PAINT event) def draw_background(dc, _from_paint) # erase background if has_style?(STYLE::GRADIENT_BACKGROUND) - bcg_size = @settings.grid_size + get_virtual_size + bcg_size = get_virtual_size.to_size + @settings.grid_size if @settings.scale != 1.0 dc.gradient_fill_linear(Wx::Rect.new([0, 0], [(bcg_size.x/@settings.scale).to_i, (bcg_size.y/@settings.scale).to_i]), @settings.gradient_from, @settings.gradient_to, Wx::SOUTH) else dc.gradient_fill_linear(Wx::Rect.new(Wx::Point.new(0, 0), bcg_size), @@ -2048,14 +2445,14 @@ dc.clear end # show grid if has_style?(STYLE::GRID_SHOW) - linedist = @settings.grid_size.x * @settings.grid_line_mult + linedist = @settings.grid_size * @settings.grid_line_mult if (linedist * @settings.scale) > 3.0 - grid_rct = Wx::Rect.new([0, 0], @settings.grid_size + get_virtual_size) + grid_rct = Wx::Rect.new([0, 0], get_virtual_size.to_size + @settings.grid_size) max_x = (grid_rct.right/@settings.scale).to_i max_y = (grid_rct.bottom/@settings.scale).to_i dc.set_pen(Wx::Pen.new(@settings.grid_color, 1, @settings.grid_style)) (grid_rct.left..max_x).step(linedist) do |x| @@ -2098,15 +2495,15 @@ # HINT: override it for custom actions... return unless @diagram _notify_canvas_change(CHANGE::FOCUS) set_focus - + lpos = dp2lp(event.get_position) - + @can_save_state_on_mouse_up = false - + case @working_mode when MODE::READY @selected_handle = get_topmost_handle_at_position(lpos) if event.control_down && event.shift_down @@ -2127,10 +2524,14 @@ if selected_shape # perform selection lst_selection = get_selected_shapes + if @selection_mode == SELECTIONMODE::NORMAL + save_canvas_state if @canvas_history.empty? + end + # cancel previous selections if necessary... if @selection_mode == SELECTIONMODE::NORMAL && (selected_top_shape.nil? || !lst_selection.include?(selected_top_shape)) deselect_all end selected_top_shape.select(@selection_mode != SELECTIONMODE::REMOVE) if selected_top_shape @@ -2157,11 +2558,11 @@ lst_selection.each do |shape| shape.send(:_on_begin_drag, fit_pos) # inform also connections assigned to the shape and its children lst_connections.clear - append_assigned_connections(shape, lst_connections, true) + append_assigned_connections(shape, lst_connections) lst_connections.each do |line| line.send(:_on_begin_drag, fit_pos) end end @@ -2194,10 +2595,11 @@ end # update canvas invalidate_visible_rect else + save_canvas_state if @canvas_history.empty? if @selected_handle.get_parent_shape == @shp_multi_edit if has_style?(STYLE::MULTI_SIZE_CHANGE) @working_mode = MODE::MULTIHANDLEMOVE else @working_mode = MODE::READY @@ -2227,32 +2629,32 @@ while shape_under && shape_under.has_style?(Shape::STYLE::PROPAGATE_INTERACTIVE_CONNECTION) shape_under = shape_under.get_parent_shape end # finish connection's creation process if possible if shape_under && !event.control_down - if @new_line_shape.get_trg_shape_id.nil? && (shape_under != @new_line_shape) && - shape_under.get_id && (shape_under.is_connection_accepted(@new_line_shape.class)) + if @new_line_shape.get_trg_shape.nil? && (shape_under != @new_line_shape) && + (shape_under.is_connection_accepted(@new_line_shape.class)) # find out whether the target shape can be connected to the source shape - source_shape = @diagram.find_shape(@new_line_shape.get_src_shape_id) + source_shape = @new_line_shape.get_src_shape if source_shape && shape_under.is_src_neighbour_accepted(source_shape.class) && source_shape.is_trg_neighbour_accepted(shape_under.class) - @new_line_shape.set_trg_shape_id(shape_under.get_id) + @new_line_shape.set_trg_shape(shape_under) @new_line_shape.set_ending_connection_point(shape_under.get_nearest_connection_point(lpos.to_real)) # inform user that the line is completed case on_pre_connection_finished(@new_line_shape) when PRECON_FINISH_STATE::OK when PRECON_FINISH_STATE::FAILED_AND_CANCEL_LINE - @new_line_shape.set_trg_shape_id(nil) + @new_line_shape.set_trg_shape(nil) @diagram.remove_shape(@new_line_shape) @working_mode = MODE::READY @new_line_shape = nil return when PRECON_FINISH_STATE::FAILED_AND_CONTINUE_EDIT - @new_line_shape.set_trg_shape_id(nil) + @new_line_shape.set_trg_shape(nil) return end @new_line_shape.create_handles # switch off the "under-construction" mode @@ -2268,21 +2670,21 @@ save_canvas_state end end else - if @new_line_shape.get_src_shape_id + if @new_line_shape.get_src_shape fit_pos = fit_position_to_grid(lpos) @new_line_shape.get_control_points << Wx::RealPoint.new(fit_pos.x, fit_pos.y) end end end else @working_mode = MODE::READY end - + refresh_invalidated_rect end # Event handler called when the canvas is double-clicked by # the left mouse button. The function can be overridden if necessary. @@ -2325,11 +2727,11 @@ # @param [Wx::MouseEvent] event Mouse event # @see _on_left_up def on_left_up(event) # HINT: override it for custom actions... lpos = dp2lp(event.get_position) - + case @working_mode when MODE::MULTIHANDLEMOVE, MODE::HANDLEMOVE # resize parent shape to fit all its children if necessary if @selected_handle.get_parent_shape.get_parent_shape @selected_handle.get_parent_shape.get_parent_shape.update @@ -2341,52 +2743,56 @@ when Shape::Handle::TYPE::LINESTART, Shape::Handle::TYPE::LINEEND line = @selected_handle.get_parent_shape line.send(:set_line_mode, LineShape::LINEMODE::READY) parent_shape = get_shape_under_cursor + # propagate request for interactive connection if requested + while parent_shape && parent_shape.has_style?(Shape::STYLE::PROPAGATE_INTERACTIVE_CONNECTION) + parent_shape = parent_shape.get_parent_shape + end if parent_shape && (parent_shape != line) && (parent_shape.is_connection_accepted(line.class)) if @selected_handle.get_type == Shape::Handle::TYPE::LINESTART - trg_shape = @diagram.find_shape(line.get_trg_shape_id) + trg_shape = line.get_trg_shape if trg_shape && parent_shape.is_trg_neighbour_accepted(trg_shape.class) - line.set_src_shape_id(parent_shape.get_id) + line.set_src_shape(parent_shape) end else - src_shape = @diagram.find_shape(line.get_src_shape_id) + src_shape = line.get_src_shape if src_shape && parent_shape.is_src_neighbour_accepted(src_shape.class) - line.set_trg_shape_id(parent_shape.get_id) + line.set_trg_shape(parent_shape) end end end end @selected_handle.send(:_on_end_drag, lpos) @selected_handle = nil - save_canvas_state if @can_save_state_on_mouse_up + save_canvas_state if @can_save_state_on_mouse_up when MODE::SHAPEMOVE lst_selection = get_selected_shapes - + lst_selection.each do |shape| # notify shape shape.send(:_on_end_drag, lpos) # reparent based on new position - reparent_shape(shape, lpos) + reparent_dropped_shape(shape, lpos) end - + if lst_selection.size>1 @shp_multi_edit.show(true) @shp_multi_edit.show_handles(true) else @shp_multi_edit.show(false) end - + move_shapes_from_negatives save_canvas_state if @can_save_state_on_mouse_up - + when MODE::MULTISELECTION lst_selection = get_selected_shapes sel_rect = @shp_selection.get_bounding_box @current_shapes.each do |shape| @@ -2414,11 +2820,11 @@ @shp_multi_edit.show_handles(true) end @shp_selection.show(false) end - + if @working_mode != MODE::CREATECONNECTION # update canvas @working_mode = MODE::READY update_multiedit_size update_virtual_size @@ -2440,23 +2846,26 @@ def on_right_down(event) # HINT: override it for custom actions... _notify_canvas_change(CHANGE::FOCUS) set_focus - + lpos = dp2lp(event.get_position) - + if @working_mode == MODE::READY deselect_all - + shape = get_shape_under_cursor + while shape && shape.has_style?(Shape::STYLE::PROPAGATE_SELECTION) + shape = shape.get_parent_shape + end if shape shape.select(true) shape.on_right_click(lpos) end end - + refresh(false) end # Event handler called when the canvas is double-clicked by # the right mouse button. The function can be overridden if necessary. @@ -2470,13 +2879,13 @@ def on_right_double_click(event) # HINT: override it for custom actions... _notify_canvas_change(CHANGE::FOCUS) set_focus - + lpos = dp2lp(event.get_position) - + if @working_mode == MODE::READY shape = get_shape_under_cursor shape.on_right_double_click(lpos) if shape end @@ -2506,13 +2915,13 @@ # @param [Wx::MouseEvent] event Mouse event # @see _on_mouse_move def on_mouse_move(event) # HINT: override it for custom actions... return unless @diagram - + lpos = dp2lp(event.get_position) - + case @working_mode when MODE::READY, MODE::CREATECONNECTION unless event.dragging # send event to multiedit shape @shp_multi_edit.send(:_on_mouse_move, lpos) if @shp_multi_edit.visible? @@ -2520,21 +2929,17 @@ # send event to all user shapes @current_shapes.each { |shape| shape.send(:_on_mouse_move, lpos) } # update unfinished line if any if @new_line_shape - line_rct = Wx::Rect.new - upd_line_rct = Wx::Rect.new - @new_line_shape.get_complete_bounding_box(line_rct, Shape::BBMODE::SELF | Shape::BBMODE::CHILDREN) + line_rct = @new_line_shape.get_complete_bounding_box(nil, Shape::BBMODE::SELF | Shape::BBMODE::CHILDREN) @new_line_shape.send(:set_unfinished_point, fit_position_to_grid(lpos)) @new_line_shape.update - @new_line_shape.get_complete_bounding_box(upd_line_rct, Shape::BBMODE::SELF | Shape::BBMODE::CHILDREN) + line_rct = @new_line_shape.get_complete_bounding_box(line_rct, Shape::BBMODE::SELF | Shape::BBMODE::CHILDREN) - line_rct.union!(upd_line_rct) - invalidate_rect(line_rct) end end when MODE::HANDLEMOVE, MODE::MULTIHANDLEMOVE, MODE::SHAPEMOVE @@ -2550,12 +2955,12 @@ end end unless @working_mode == MODE::MULTIHANDLEMOVE if event.dragging if has_style?(STYLE::GRID_USE) - return if (event.get_position.x - @prev_mouse_pos.x).abs < @settings.grid_size.x && - (event.get_position.y - @prev_mouse_pos.y).abs < @settings.grid_size.y + return if (event.get_position.x - @prev_mouse_pos.x).abs < @settings.grid_size && + (event.get_position.y - @prev_mouse_pos.y).abs < @settings.grid_size end @prev_mouse_pos = event.get_position if event.control_down || event.shift_down lst_selection = get_selected_shapes @@ -2570,11 +2975,11 @@ shape.send(:_on_dragging, fit_position_to_grid(lpos)) # move also connections assigned to this shape and its children lst_connections.clear - append_assigned_connections(shape, lst_connections,true) + append_assigned_connections(shape, lst_connections) lst_connections.each { |line| line.send(:_on_dragging, fit_position_to_grid(lpos)) } # update connections assigned to this shape lst_connections = @diagram.get_assigned_connections(shape, LineShape, Shape::CONNECTMODE::BOTH) @@ -2605,11 +3010,11 @@ @shp_selection.set_relative_position(selection_pos) @shp_selection.set_rect_size(selection_size) invalidate_visible_rect end - + refresh_invalidated_rect end # Event handler called when the mouse wheel position is changed. # The function can be overridden if necessary. @@ -2619,22 +3024,21 @@ # this function from overridden methods if the default canvas behaviour # should be preserved. # @param [Wx::MouseEvent] event Mouse event def on_mouse_wheel(event) # HINT: override it for custom actions... - if event.control_down scale = get_scale - scale += (event.get_wheel_rotation/(event.get_wheel_delta*10)).to_f + scale += event.get_wheel_rotation.to_f/(event.get_wheel_delta*10) scale = @settings.min_scale if scale < @settings.min_scale scale = @settings.max_scale if scale > @settings.max_scale - + set_scale(scale) refresh(false) end - + event.skip end # Event handler called when any key is pressed. # The function can be overridden if necessary. @@ -2648,11 +3052,11 @@ def on_key_down(event) # HINT: override it for custom actions... return unless @diagram lst_selection = get_selected_shapes - + case event.get_key_code when Wx::K_DELETE # send event to selected shapes lst_selection.delete_if do |shape| if shape.has_style?(Shape::STYLE::PROCESS_DEL) @@ -2682,11 +3086,18 @@ line = @selected_handle.get_parent_shape line.send(:set_line_mode, LineShape::LINEMODE::READY) @selected_handle = nil end + restore_current_state + when MODE::MULTIHANDLEMOVE + restore_current_state + + when MODE::SHAPEMOVE + restore_current_state + else # send event to selected shapes lst_selection.each { |shape| shape.send(:_on_key, event.get_key_code) } end @working_mode = MODE::READY @@ -2694,20 +3105,20 @@ when Wx::K_LEFT, Wx::K_RIGHT, Wx::K_UP, Wx::K_DOWN lst_connections = [] lst_selection.each do |shape| shape.send(:_on_key, event.get_key_code) - + # inform also connections assigned to this shape lst_connections.clear - append_assigned_connections(shape, lst_connections, true) - + append_assigned_connections(shape, lst_connections) + lst_connections.each do |line| line.send(:_on_key, event.get_key_code) unless line.selected? end end - + # send the event to multiedit ctrl if displayed @shp_multi_edit.send(:_on_key, event.get_key_code) if @shp_multi_edit.visible? refresh_invalidated_rect save_canvas_state @@ -2725,13 +3136,13 @@ # @param [Wx::SF::EditTextShape] shape Changed Wx::SF::EditTextShape object # @see Wx::SF::EditTextShape#edit_label # @see Wx::SF::ShapeTextEvent def on_text_change(shape) # HINT: override it for custom actions... - + # ... standard implementation generates the Wx::SF::EVT_SF_TEXT_CHANGE event. - id = shape ? shape.get_id : nil + id = shape ? shape.object_id : nil event = ShapeTextEvent.new(Wx::SF::EVT_SF_TEXT_CHANGE, id) event.set_shape(shape) event.set_text(shape.get_text) process_event(event) @@ -2743,13 +3154,13 @@ # @param [Wx::SF::LineShape,nil] connection new connection object (nil if cancelled) # @see start_interactive_connection # @see Wx::SF::ShapeEvent def on_connection_finished(connection) # HINT: override to perform user-defined actions... - + # ... standard implementation generates the Wx::SF::EVT_SF_LINE_DONE event. - id = connection ? connection.get_id : -1 + id = connection ? connection.object_id : -1 event = ShapeEvent.new(Wx::SF::EVT_SF_LINE_DONE, id) event.set_shape(connection) process_event(event) end @@ -2764,14 +3175,14 @@ # if the generated event has been vetoed the connection creation is cancelled # @see start_interactive_connection # @see Wx::SF::ShapeEvent def on_pre_connection_finished(connection) # HINT: override to perform user-defined actions... - + # ... standard implementation generates the Wx::SF::EVT_SF_LINE_DONE event. - id = connection ? connection.get_id : -1 - + id = connection ? connection.object_id : -1 + event = ShapeEvent.new(Wx::SF::EVT_SF_LINE_BEFORE_DONE, id) event.set_shape(connection) process_event(event) return PRECON_FINISH_STATE::FAILED_AND_CANCEL_LINE if event.vetoed? @@ -2790,14 +3201,14 @@ # @param [Array<Wx::SF::Shape>] dropped a list containing the dropped data # @see Wx::SF::CanvasDropTarget # @see Wx::SF::ShapeDropEvent def on_drop(x, y, deflt, dropped) # HINT: override it for custom actions... - + # ... standard implementation generates the Wx::SF::EVT_SF_ON_DROP event. return unless has_style?(STYLE::DND) - + # create the drop event and process it event = ShapeDropEvent.new(Wx::SF::EVT_SF_ON_DROP, x, y, self, deflt, Wx::ID_ANY) event.set_dropped_shapes(dropped) process_event(event) end @@ -2810,14 +3221,14 @@ # @param [Array<Wx::SF::Shape>] pasted a list containing the pasted data # @see Wx::SF::ShapeCanvas#paste # @see Wx::SF::ShapePasteEvent def on_paste(pasted) # HINT: override it for custom actions... - + # ... standard implementation generates the Wx::SF::EVT_SF_ON_PASTE event. return unless has_style?(STYLE::CLIPBOARD) - + # create the drop event and process it event = ShapePasteEvent.new(Wx::SF::EVT_SF_ON_PASTE, self, Wx::ID_ANY) event.set_pasted_shapes(pasted) process_event(event) end @@ -2840,50 +3251,69 @@ # Validate selection so the shapes in the given list can be processed by the clipboard functions # @param [Array<Wx::SF::Shape>] selection # @param [Boolean] storeprevpos def validate_selection_for_clipboard(selection, storeprevpos) - selection.dup.each do |shape| + # first remove any shapes not eligible for copying + selection.reject! do |shape| + do_reject = false if shape.get_parent_shape # remove child shapes without parent in the selection and without STYLE::PARENT_CHANGE style # defined from the selection if !shape.has_style?(Shape::STYLE::PARENT_CHANGE) && !selection.include?(shape.get_parent_shape) - selection.delete(shape) + do_reject = true else # convert relative position to absolute position if the shape is copied # without its parent unless selection.include?(shape.get_parent_shape) store_prev_position(shape) if storeprevpos shape.set_relative_position(shape.get_absolute_position) end end + elsif LineShape === shape && !shape.stand_alone? + # remove any stand alone LineShape for which the source or target shape are not included in the selection + # (or any of it's child shapes) + unless (selection.include?(shape.get_src_shape) || + selection.any? { |selshp| selshp.include_child_shape?(shape.get_src_shape, true) }) && + (selection.include?(shape.get_trg_shape) || + selection.any? { |selshp| selshp.include_child_shape?(shape.get_trg_shape, true) }) + do_reject = true + end end - - append_assigned_connections(shape, selection, false) + do_reject end + + # now append all connections for which source AND target are included in the selection + selection.each do |shape| + append_assigned_connections(shape, selection, children_only: false, complete_only: true) + end end - # Append connections assigned to shapes in given list to this list as well - # @param [Wx::SF::Shape] shape - # @param [Array<Wx::SF::Shape>] selection - # @param [Boolean] childrenonly - def append_assigned_connections(shape, selection, childrenonly) + # Append connections assigned to shapes in given list to this list as well + # @param [Wx::SF::Shape] shape shape + # @param [Array<Wx::SF::Shape>] selection selected shapes + # @param [Boolean] children_only appends connections from child shapes only + # @param [Boolean] complete_only append complete (src and trg shapes in selection) only + def append_assigned_connections(shape, selection, children_only: true, complete_only: false) # add connections assigned to copied topmost shapes and their children to the copy list lst_children = shape.get_child_shapes(ANY, RECURSIVE) - + # get connections assigned to the parent shape - lst_connections = @diagram.get_assigned_connections(shape, LineShape, Shape::CONNECTMODE::BOTH) unless childrenonly + lst_connections = @diagram.get_assigned_connections(shape, LineShape, Shape::CONNECTMODE::BOTH) unless children_only lst_connections ||= [] - # get connections assigned to its child shape + # get connections assigned to its child shape(s) lst_children.each do |child| - # get connections assigned to the child shape + # get connections assigned to the child shape(s) @diagram.get_assigned_connections(child, LineShape, Shape::CONNECTMODE::BOTH, lst_connections) end - + # insert connections to the copy list lst_connections.each do |line| - selection << line unless selection.include?(line) + selection << line unless selection.include?(line) || + (complete_only && + !(selection.include?(line.get_src_shape) && + selection.include?(line.get_trg_shape))) end end # Remove given shape for temporary containers # @param [Wx::SF::Shape] shape @@ -2895,11 +3325,11 @@ @selected_shape_under_cursor = nil if @selected_shape_under_cursor == shape @topmost_shape_under_cursor = nil if @topmost_shape_under_cursor == shape end end - # Clear all temporary containers + # Clear all temporary containers def clear_temporaries @current_shapes.clear @new_line_shape = nil @unselected_shape_under_cursor = nil @selected_shape_under_cursor = nil @@ -2907,23 +3337,46 @@ end # Assign give shape to parent at given location (if exists) # @param [Wx::SF::Shape] shape # @param [Wx::Point] parentpos - def reparent_shape(shape, parentpos) + def reparent_dropped_shape(shape, parentpos) return unless @diagram - # is shape dropped into accepting shape? - parent_shape = get_shape_at_position(parentpos, 1, SEARCHMODE::UNSELECTED) - parent_shape = nil if parent_shape && !parent_shape.is_child_accepted(shape.class) - - # set new parent + # set new parent if possible if shape.has_style?(Shape::STYLE::PARENT_CHANGE) && !shape.is_a?(LineShape) + # is shape dropped into accepting shape? + + # get all shapes at drop position in reversed z-order + shapes_at_pos = get_shapes_at_position(parentpos).reverse + # see if we can find a non-LineShape drop target + parent_shape = shapes_at_pos.find do |s| + # consider non-LineShapes that are unselected and not the dropped shape itself or one of it's (grand-)children + !s.is_a?(Wx::SF::LineShape) && !s.selected? && shape != s && !shape.include_child_shape?(s) + end + # if none found consider line shapes + parent_shape = shapes_at_pos.find do |s| + # consider LineShapes that are unselected and not the dropped shape itself or one of it's (grand-)children + s.is_a?(Wx::SF::LineShape) && !s.selected? && shape != s && !shape.include_child_shape?(s) + end unless parent_shape + # In case the matching shape does not accept the dropped child and has style PROPAGATE_DROPPING + # see if this shape has a parent that does also matches the position and DOES accept the child. + # This allows dropping shapes onto child shapes inside a (container) shapes like + # grids and/or boxes. + while parent_shape && !parent_shape.is_child_accepted(shape.class) + parent_shape = parent_shape.has_style?(Shape::STYLE::PROPAGATE_DROPPING) ? parent_shape.parent_shape : nil + parent_shape = nil if parent_shape && !parent_shape.get_bounding_box.contains?(parentpos) + end + # parent_shape = nil if parent_shape && !parent_shape.is_child_accepted(shape.class) + prev_parent = shape.get_parent_shape - + if parent_shape - if parent_shape.get_parent_shape != shape + # in rare cases (where childs are expanded to fill a parent and have PROPAGATE_SELECTION) + # the matched drop parent may actually a child of the shape being dropped + # guard against that (since that would lead to illegal circular references) + if parent_shape != shape && !shape.include_child_shape?(parent_shape, true) # update relative position to new parent apos = shape.get_absolute_position - parent_shape.get_absolute_position shape.set_relative_position(apos) # reparent @diagram.reparent_shape(shape, parent_shape) @@ -2936,13 +3389,13 @@ shape.move_by(prev_parent.get_absolute_position) if prev_parent # reparent @diagram.reparent_shape(shape, parent_shape) end end - + prev_parent.update if prev_parent - parent_shape.update if parent_shape + parent_shape.update if parent_shape && parent_shape != prev_parent end end # Store previous shape's position modified in validate_selection_for_clipboard() function # @param [Wx::SF::Shape] shape @@ -3001,96 +3454,96 @@ when MODE::HANDLEMOVE when MODE::MULTIHANDLEMOVE else @working_mode = MODE::READY end - + event.skip end # Event handler called when the mouse pointer enters the canvas window. # @param [Wx::MouseEvent] event Mouse event def _on_enter_window(event) @prev_mouse_pos = event.get_position - + lpos = dp2lp(event.get_position) - + case @working_mode when MODE::MULTISELECTION unless event.left_is_down update_multiedit_size @shp_multi_edit.show(false) @working_mode = MODE::READY - + invalidate_visible_rect end when MODE::HANDLEMOVE unless event.left_is_down if @selected_handle if @selected_handle.get_parent_shape.is_a?(LineShape) @selected_handle.get_parent_shape.send(:set_line_mode, LineShape::LINEMODE::READY) end - + @selected_handle.send(:_on_end_drag, lpos) - + save_canvas_state @working_mode = MODE::READY @selected_handle = nil - + invalidate_visible_rect end end when MODE::MULTIHANDLEMOVE unless event.left_is_down if @selected_handle @selected_handle.send(:_on_end_drag, lpos) - + save_canvas_state @working_mode = MODE::READY - + invalidate_visible_rect end end when MODE::SHAPEMOVE unless event.left_is_down lst_selection = get_selected_shapes - + move_shapes_from_negatives update_virtual_size - + if lst_selection.size > 1 update_multiedit_size @shp_multi_edit.show(true) @shp_multi_edit.show_handles(true) end - + lst_selection.each { |shape| shape.send(:_on_end_drag, lpos) } @working_mode = MODE::READY - + invalidate_visible_rect end end - + refresh_invalidated_rect - + event.skip end # Event handler called when the canvas size has changed. # @param [Wx::SizeEvent] event Size event def _on_resize(event) refresh(false) if has_style?(STYLE::GRADIENT_BACKGROUND) event.skip end - + # original private event handlers - + # Original private event handler called when the canvas is clicked by # left mouse button. The handler calls user-overridable event handler function # and skips the event for next possible processing. # @param [Wx::MouseEvent] event Mouse event # @see Wx::SF::ShapeCanvas#on_left_down @@ -3202,11 +3655,11 @@ # @param [Wx::DragResult] deflt Drag result # @param [Wx::ShapeDataObject] data a data object encapsulating dropped data # @see Wx::SF::CanvasDropTarget def _on_drop(x, y, deflt, data) if data && Wx::SF::ShapeDataObject === data - lst_new_content = Wx::SF::Serializable.deserialize(data.get_data_here) + lst_new_content = data.get_as_shapes if lst_new_content && !lst_new_content.empty? lst_parents_to_update = [] lpos = dp2lp(Wx::Point.new(x, y)) dx = 0 @@ -3215,16 +3668,24 @@ dx = lpos.x - @dnd_started_at.x dy = lpos.y - @dnd_started_at.y end parent = @diagram.get_shape_at_position(lpos, 1, SEARCHMODE::UNSELECTED) + # In case the located shape does not accept ANY children and has style PROPAGATE_DROPPING + # see if this shape has a parent that does also match the position and DOES accept children. + # This allows dropping shapes onto child shapes inside a (container) shapes like + # grids and/or boxes. + while parent&.does_not_accept_children? + parent = parent.has_style?(Shape::STYLE::PROPAGATE_DROPPING) ? parent.parent_shape : nil + parent = nil if parent && !parent.get_bounding_box.contains?(lpos) + end # add each shape to diagram keeping only those that are accepted lst_new_content.select! do |shape| shape.move_by(dx, dy) # do not reparent connection lines - rc = if shape.is_a?(LineShape) && !shape.stand_alone? + rc = if (shape.is_a?(LineShape) && !shape.stand_alone?) || parent.nil? @diagram.add_shape(shape, nil, lp2dp(shape.get_absolute_position.to_point), INITIALIZE, DONT_SAVE_STATE) @@ -3237,11 +3698,11 @@ end rc == ERRCODE::OK # keep or remove? end # verify newly added shapes (may remove shapes from list) - @diagram.send(:check_new_shapes, lst_new_content) + @diagram.send(:on_import, lst_new_content) update_virtual_size # update for new shapes # notify parents and collect for update lst_new_content.each do |shape| @@ -3264,10 +3725,10 @@ # call user-defined drop handler on_drop(x, y, deflt, lst_new_content) end end end - + end def _notify_canvas_change(change, *args) @diagram.get_all_shapes.each { |shape| shape.send(:_on_canvas, change, *args) } if @diagram end