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

- old
+ new

@@ -1,56 +1,87 @@ # Wx::SF::GridShape - grid shape class # Copyright (c) M.J.N. Corino, The Netherlands require 'wx/shapes/shapes/rect_shape' +require 'wx/shapes/shapes/manager_shape' module Wx::SF # Class encapsulates a rectangular shape derived from Wx::SF::RectShape class which acts as a grid-based # container able to manage other assigned child shapes (it can control their position). The managed # shapes are aligned into defined grid with a behaviour similar to classic Wx::GridSizer class. class GridShape < RectShape + include ManagerShape + # default values module DEFAULT - # Default value of GridShape @rows data member. - ROWS = 3 # Default value of GridShape @cols data member. - COLS = 3 + COLUMNS = 3 # Default value of GridShape @cell_space data member. CELLSPACE = 5 end - property :rows, :cols, :cell_space, :cells + class << self - # @overload initialize() - # Default constructor. - # @overload initialize(pos, size, rows, cols, cell_space, diagram) - # User constructor. - # @param [Wx::RealPoint] pos Initial position - # @param [Wx::RealPoint] size Initial size - # @param [Integer] cols Number of grid rows - # @param [Integer] rows Number of grid columns - # @param [Integer] cell_space Additional space between managed shapes - # @param [Wx::SF::Diagram] diagram parent diagram - def initialize(*args) - if args.empty? - super() - @rows = DEFAULT::ROWS - @cols = DEFAULT::COLS - @cell_space = DEFAULT::CELLSPACE - else - pos, size, rows, cols, cell_space, diagram = args - super(pos, size, diagram) - @rows = rows || 0 - @cols = cols || 0 - @cell_space = cell_space || 0 + # Returns the minimum size for *empty* grids + # @return [Wx::Size] + def get_min_size + @min_size ||= Wx::Size.new(20, 20) end + alias :min_size :get_min_size + + # Sets the minimum size for *empty* grids + # @overload set_min_size(sz) + # @param [Wx::Size] sz + # @overload set_min_size(w, h) + # @param [Integer] w + # @param [Integer] h + def set_min_size(arg1, arg2 = nil) + @min_size = if arg2.nil? + raise ArgumentError, 'Expected Wx::Size' unless Wx::Size === arg1 + arg1 + else + Wx::Size.new(arg1, arg2) + end + end + alias :min_size= :set_min_size + + end + + property :cols, :max_rows, :cell_space, :cells + + # Constructor. + # @param [Wx::RealPoint,Wx::Point] pos Initial position + # @param [Wx::RealPoint,Wx::Size,Wx::Point] size Initial size + # @param [Integer] cols Number of grid columns + # @param [Integer] max_rows Maximum number of grid rows + # @param [Integer] cell_space Additional space between managed shapes + # @param [Wx::SF::Diagram] diagram parent diagram + def initialize(pos = Shape::DEFAULT::POSITION, size = RectShape::DEFAULT::SIZE, + cols: DEFAULT::COLUMNS, max_rows: 0, cell_space: DEFAULT::CELLSPACE, diagram: nil) + super(pos, size, diagram: diagram) + @cols = [1, cols.to_i].max # at least one column + @max_rows = [0, max_rows.to_i].max # no or >=1 max rows + @cell_space = [0, cell_space.to_i].max + @rows = 1 @cells = [] remove_style(Shape::STYLE::SIZE_CHANGE) end + attr_reader :max_rows + + # Sets the maximum number of rows for the grid (by default there this value is 0 == no maximum). + # In case the number of already managed cells exceeds the new maximum no change is made. + # @return [Integer] the active maximum + def set_max_rows(num) + # only change as long as this does not invalidate already managed cells + @max_rows = num unless (num * @cols) < @cells.size + @max_rows + end + alias :max_rows= :set_max_rows + # Set grid dimensions. # @param [Integer] rows Number of rows # @param [Integer] cols Number of columns def set_dimensions(rows, cols) return if (new_size = rows * cols) == 0 @@ -65,10 +96,17 @@ # @return [Array(Integer,Integer)] row and col numbers def get_dimensions [@rows, @cols] end + # Get number of available grid cells + # @return [Integer] + def get_cell_count + @rows * @cols + end + alias :cell_count :get_cell_count + # Set space between grid cells (managed shapes). # @param [Integer] cellspace Cellspace size def set_cell_space(cellspace) @cell_space = cellspace end @@ -79,18 +117,18 @@ def get_cell_space @cell_space end alias :cell_space :get_cell_space - # Iterate all cells. If a block is given passes row, col and id for each cell to block. + # Iterate all cells. If a block is given passes row, col and shape (if any) for each cell to block. # Returns Enumerator if no block given. # @overload each_cell() # @return [Enumerator] # @overload each_cell(&block) # @yieldparam [Integer] row # @yieldparam [Integer] col - # @yieldparam [Wx::SF::Serializable::ID,nil] id + # @yieldparam [shape,nil] shape # @return [Object] def each_cell(&block) if block @rows.times do |row| @cols.times do |col| @@ -106,44 +144,44 @@ end end end end + # Clear the cell at given row and column index + # @param [Integer] row + # @param [Integer] col + # @return [Boolean] true if cell existed, false otherwise + # Note that this function doesn't remove managed (child) shapes from the parent grid shape + # (they are still its child shapes but aren't managed anymore). def clear_cell(row, col) if row>=0 && row<@rows && col>=0 && col<@cols @cells[row*@cols + col] = nil + true + else + false end end - def get_cell(row, col) - if row>=0 && row<@rows && col>=0 && col<@cols - @cells[row*@cols + col] - end - end - # Get managed shape specified by lexicographic cell index. # @overload get_managed_shape(index) # @param [Integer] index Lexicographic index of requested shape - # @return [Shape] shape object of given cell index if exists, otherwise nil + # @return [Shape, nil] shape object of given cell index if exists, otherwise nil # @overload get_managed_shape(row, col) # @param [Integer] row Zero-base row index # @param [Integer] col Zero-based column index - # @return [Shape] shape object stored in specified grid cell if exists, otherwise nil + # @return [Shape, nil] shape object stored in specified grid cell if exists, otherwise nil def get_managed_shape(*args) index = args.size == 1 ? args.first : (args[0]*@cols)+args[1] - if index>=0 && index<@cells.size && @cells[index] - return @child_shapes.find { |child| @cells[index] == child.id } - end - nil + @cells[index] end - # Clear information about managed shapes and set number of rows and columns to zero. + # Clear information about managed shapes and set number of rows to 1 (number of columns does not change). # # Note that this function doesn't remove managed (child) shapes from the parent grid shape # (they are still its child shapes but aren't managed anymore). def clear_grid - @rows = @cols = 0 + @rows = 1 @cells = [] end # Append given shape to the grid at the last managed position. # @param [Shape] shape shape to append @@ -153,237 +191,297 @@ insert_to_grid(row, col, shape) end # Insert given shape to the grid at the given position. + # In case a shape is inserted in a cell already occupied the cells at that position and following will + # be shifted to the next lexicographic position. + # A maximum row setting may prevent a new shape of being inserted. # @overload insert_to_grid(row, col, shape) # Note that the grid can grow in a vertical direction only, so if the user specifies a desired # horizontal position bigger than the current number of columns is then this function exits with # an error (false) return value. If specified vertical position exceeds the number or grid rows than - # the grid is resized. Any occupied grid cells at given position or beyond will be shifted to the next - # lexicographic position. + # the grid is resized. # @param [Integer] row Vertical position # @param [Integer] col Horizontal position # @param [Shape] shape shape to insert # @return [Boolean] true on success, otherwise false # @overload insert_to_grid(index, shape) # Note that the given index is a lexicographic position of inserted shape. The given shape is inserted before # the existing item 'index', thus insert_to_grid(0, something) will insert an item in such way that it will become - # the first grid element. Any occupied grid cells at given position or beyond will be shifted to the next - # lexicographic position. - # @param [Integer] index Lexicographic position of inserted shape + # the first grid element. + # @param [Integer] index Lexicographic position of inserted shape (>= 0) # @param [Shape] shape shape to insert # @return [Boolean] true on success, otherwise false def insert_to_grid(*args) if args.size > 2 row, col, shape = args if shape && shape.is_a?(Shape) && is_child_accepted(shape.class) # protect duplicated occurrences - return false if @cells.index(shape.id) + return false if @cells.index(shape) # protect unbounded horizontal index (grid can grow in a vertical direction only) return false if col >= @cols + # protect maximum rows + index = row * @cols + col + return false if @max_rows > 0 && + (row >= @max_rows || # cannot insert beyond max_rows + (@cells[index] && @cells.size >= (@max_rows * @cols))) # cannot grow beyond max_rows # add the shape to the children list if necessary unless @child_shapes.include?(shape) if @diagram @diagram.reparent_shape(shape, self) else shape.set_parent_shape(self) end end - @cells.insert(row * @cols + col, shape.id) - - # adjust row count if necessary - if @cells.size > (@rows * @cols) - @rows = @cells.size / @cols + if @cells[index] + @cells.insert(row * @cols + col, shape) + else + @cells[index] = shape end + # adjust row count + update_rows + return true end else index, shape = args if shape && shape.is_a?(Shape) && is_child_accepted(shape.class) # protect duplicated occurrences - return false if @cells.index(shape.id) + return false if @cells.index(shape) # protect unbounded index - return false if index >= (@rows * @cols) + max_size = @cols * @max_rows + return false if index < 0 || + (@max_rows > 0 && + (index >= max_size || # cannot insert beyond max_rows + (@cells[index] && @cells.size >= (@cols * @max_rows)))) # cannot grow beyond max_rows # add the shape to the children list if necessary unless @child_shapes.include?(shape) if @diagram @diagram.reparent_shape(shape, self) else shape.set_parent_shape(self) end end - @cells.insert(index, shape.id) - - # adjust row count if necessary - if @cells.size > (@rows * @cols) - @rows = @cells.size / @cols + if @cells[index] + @cells.insert(index, shape) + else + @cells[index] = shape end + # adjust row count + update_rows + return true end end false end - # Remove shape with given ID from the grid. - # Shifts any occupied cells beyond the cell containing the given id to the previous lexicographic position. - # @param [Serializable::ID] id ID of shape which should be removed + # Remove given shape from the grid. + # Shifts any occupied cells beyond the cell containing the given shape to the previous lexicographic position. + # @param [Shape] shape shape which should be removed + # @return [Shape,nil] removed shape or nil if not found # @note Note this does *not* remove the shape as a child shape. - def remove_from_grid(id) - @cells.delete(id) + def remove_from_grid(shape) + if @cells.delete(shape) + # remove trailing empty cells + @cells.pop until @cells.last + # update row count + @rows = @cells.size / @cols + @rows += 1 if (@cells.size % @cols) > 0 + return shape + end + nil end - # Update shape (align all child shapes an resize it to fit them) - def update + # Update shape (align all child shapes and resize it to fit them) + def update(recurse= true) # check for existence of de-assigned shapes - @cells.delete_if do |id| - @child_shapes.find { |child| child.id == id }.nil? + @cells.delete_if do |shape| + shape && !@child_shapes.include?(shape) end - # check whether all child shapes' IDs are present in the cells array... + # check whether all child shapes are present in the cells array... @child_shapes.each do |child| - @cells << child.id unless @cells.include?(child.id) + unless @cells.include?(child) + # see if we can match the position of the new child with the position of another + # (previously assigned) managed shape + position_child_cell(child) + end end # do self-alignment do_alignment - - # do alignment of shape's children - do_children_layout - + # fit the shape to its children fit_to_children unless has_style?(STYLE::NO_FIT_TO_CHILDREN) # do it recursively on all parent shapes - get_parent_shape.update if get_parent_shape + get_parent_shape.update(recurse) if recurse && get_parent_shape end # Resize the shape to bound all child shapes. The function can be overridden if necessary. def fit_to_children - # HINT: overload it for custom actions... - - # get bounding box of the shape and children set be inside it + # get bounding box of the shape and children set to be inside it abs_pos = get_absolute_position ch_bb = Wx::Rect.new(abs_pos.to_point, [0, 0]) @child_shapes.each do |child| - child.get_complete_bounding_box(ch_bb, BBMODE::SELF | BBMODE::CHILDREN) if child.has_style?(STYLE::ALWAYS_INSIDE) + ch_bb = child.get_complete_bounding_box(ch_bb, BBMODE::SELF | BBMODE::CHILDREN) if child.has_style?(STYLE::ALWAYS_INSIDE) end - - # do not let the grid shape 'disappear' due to zero sizes... - if (ch_bb.width == 0 || ch_bb.height == 0) && @cell_space == 0 - ch_bb.set_width(10) - ch_bb.set_height(10) + + if @child_shapes.empty? + # do not let the empty grid shape 'disappear' due to zero sizes... + ch_bb.size = GridShape.min_size - @cell_space end - - @rect_size = Wx::RealPoint.new(ch_bb.width + 2*@cell_space, ch_bb.height + 2*@cell_space) + + @rect_size = Wx::RealPoint.new(ch_bb.width + @cell_space, ch_bb.height + @cell_space) end + # Event handler called when any shape is dropped above this shape (and the dropped + # shape is accepted as a child of this shape). The function can be overridden if necessary. + # + # The function is called by the framework (by the shape canvas). + # @param [Wx::RealPoint] _pos Relative position of dropped shape + # @param [Shape] child dropped shape + def on_child_dropped(_pos, child) + # see if we can match the position of the new child with the position of another + # (previously assigned) managed shape + if child && !child.is_a?(LineShape) + # insert child based on it's current (possibly dropped) position + position_child_cell(child) + end + end + + protected + # Do layout of assigned child shapes def do_children_layout return if @cols == 0 || @rows == 0 - - max_rect = Wx::Rect.new(0,0,0,0) - - # get maximum size of all managed (child) shapes - @child_shapes.each do |shape| - curr_rect = shape.get_bounding_box - max_rect.set_width(curr_rect.width) if shape.get_h_align != HALIGN::EXPAND && curr_rect.width > max_rect.width - max_rect.set_height(curr_rect.height) if shape.get_v_align != VALIGN::EXPAND && curr_rect.height > max_rect.height - end + max_size = get_max_child_size - @cells.each_with_index do |id, i| - if id - shape = @child_shapes[id] + @cells.each_with_index do |shape, i| + if shape col = (i % @cols) row = (i / @cols) - fit_shape_to_rect(shape, Wx::Rect.new(col*max_rect.width + (col+1)*@cell_space, - row*max_rect.height + (row+1)*@cell_space, - max_rect.width, max_rect.height)) + fit_shape_to_rect(shape, Wx::Rect.new(col*max_size.width + (col+1)*@cell_space, + row*max_size.height + (row+1)*@cell_space, + max_size.width, max_size.height)) end end end - # Event handler called when any shape is dropped above this shape (and the dropped - # shape is accepted as a child of this shape). The function can be overridden if necessary. - # - # The function is called by the framework (by the shape canvas). - # @param [Wx::RealPoint] _pos Relative position of dropped shape - # @param [Shape] child dropped shape - def on_child_dropped(_pos, child) - append_to_grid(child) if child && !child.is_a?(LineShape) + # called after the shape has been newly imported/pasted/dropped + # checks the cells for stale links + def on_import + # check for existence of non-included shapes + @cells.delete_if do |shape| + shape && !@child_shapes.include?(shape) + end end - protected + # update row count + def update_rows + # remove trailing empty cells (if any) + @cells.pop until @cells.last + @rows = @cells.size / @cols + @rows += 1 if (@cells.size % @cols) > 0 + end - # Move and resize given shape so it will fit the given bounding rectangle. - # - # The shape is aligned inside the given bounding rectangle in accordance to the shape's - # valign and halign flags. - # @param [Shape] shape modified shape - # @param [Wx::Rect] rct Bounding rectangle - # @see Shape#set_v_align - # @see Shape#set_h_align - def fit_shape_to_rect(shape, rct) - shape_bb = shape.get_bounding_box - prev_pos = shape.get_relative_position - - # do vertical alignment - case shape.get_v_align - when VALIGN::TOP - shape.set_relative_position(prev_pos.x, rct.top + shape.get_v_border) - when VALIGN::MIDDLE - shape.set_relative_position(prev_pos.x, rct.top + (rct.height/2 - shape_bb.height/2)) - when VALIGN::BOTTOM - shape.set_relative_position(prev_pos.x, rct.bottom - shape_bb.height - shape.get_v_border) - when VALIGN::EXPAND - if shape.has_style?(STYLE::SIZE_CHANGE) - shape.set_relative_position(prev_pos.x, rct.top + shape.get_v_border) - shape.scale(1.0, (rct.height - 2*shape.get_v_border).to_f/shape_bb.height) + # returns maximum size of all managed (child) shapes + # @return [Wx::Size] + def get_max_child_size + @child_shapes.inject(Wx::Size.new(0, 0)) do |max_size, shape| + child_rect = shape.get_bounding_box + + max_size.set_width(child_rect.width) if shape.get_h_align != HALIGN::EXPAND && child_rect.width > max_size.width + max_size.set_height(child_rect.height) if shape.get_v_align != VALIGN::EXPAND && child_rect.height > max_size.height + max_size + end + end + + def find_cell(child_rect) + max_size = get_max_child_size + child_centre = child_rect.get_position + child_centre.x += child_rect.width/2 + child_centre.y += child_rect.height/2 + # find the cell index where the new or dragged child is positioned above and in front of + offset = get_bounding_box.top_left + cell_count.times.find do |cell| + col = (cell % @cols) + row = (cell / @cols) + cell_rct = Wx::Rect.new(col*max_size.width + (col+1)*@cell_space, + row*max_size.height + (row+1)*@cell_space, + max_size.width, max_size.height).offset!(offset) + child_centre.x <= cell_rct.right && child_centre.y <= cell_rct.bottom + end + end + + def position_child_cell(child) + crct = child.get_bounding_box + # see if the child already had a cell in this grid (moving a child) + child_index = @cells.index(child) + # if the child intersects this box shape we look + # for the cell it should go into (if any) + if @cells.size>0 && intersects?(crct) + # find the cell index where the new child is positioned above and in front of + index = find_cell(crct) + # now see where to put the new/moved child + if index # found a matching cell? + # if the child being inserted already had a slot + if child_index + # if the newly found index equals the existing index there is nothing to do + return if child_index == index + # else clear the child's current cell; this provides support for reordering child shapes by dragging + @cells[child_index] = nil + end + # insert/move the child + unless insert_to_grid(index, child) + # if failed to insert (max rows exceeded?) restore the child to it's previous cell + if child_index + @cells[child_index] = child + else # or make the child a toplevel shape + # move relative to current parent (if any) + child.move_by(child.get_parent_shape.get_absolute_position) if child.get_parent_shape + diagram.reparent_shape(child, nil) + end + end + return # done end - else - shape.set_relative_position(prev_pos.x, rct.top) end - - prev_pos = shape.get_relative_position - - # do horizontal alignment - case shape.get_h_align - when HALIGN::LEFT - shape.set_relative_position(rct.left + shape.get_h_border, prev_pos.y) - when HALIGN::CENTER - shape.set_relative_position(rct.left + (rct.width/2 - shape_bb.width/2), prev_pos.y) - when HALIGN::RIGHT - shape.set_relative_position(rct.right - shape_bb.width - shape.get_h_border, prev_pos.y) - when HALIGN::EXPAND - if shape.has_style?(STYLE::SIZE_CHANGE) - shape.set_relative_position(rct.left + shape.get_h_border, prev_pos.y) - shape.scale((rct.width - 2*shape.get_h_border).to_f/shape_bb.width, 1.9) + # otherwise append + # clear the child's current cell if already part of grid + @cells[child_index] = nil if child_index + # append + unless append_to_grid(child) + # if failed to append (max rows exceeded?) restore the child to it's previous cell + if child_index + @cells[child_index] = child + else # or make the child a toplevel shape + # move relative to current parent (if any) + child.move_by(child.get_parent_shape.get_absolute_position) if child.get_parent_shape + diagram.reparent_shape(child, nil) end - else - shape.set_relative_position(rct.left, prev_pos.y) end end private - # Deserialization only. + # (de-)serialization only. - def get_rows - @rows - end - def set_rows(num) - @rows = num + # default deserialization finalizer + def create + update_rows end def get_cols @cols end