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