lighttroupe/luz

View on GitHub
gui/gui_box.rb

Summary

Maintainability
B
4 hrs
Test Coverage
#
# GuiBox is the base level container.
#
# It does no positioning of child objects, each is simply drawn on top of the previous.
#
class GuiBox < GuiObject
    callback :contents_change

    def initialize(contents = [])
        self.contents = contents
        @selection = Set.new
        @float_left = -0.5
        @float_right = 0.5
        super()
    end

    #
    #
    #
    def count
        @contents.count
    end

    def empty?
        @contents.empty?
    end

    # finding
    def first
        @contents.first
    end

    def include?(object)
        @contents.include? object
    end

    def index(object)
        @contents.index(object)
    end

    #
    # Adding
    #
    def contents=(contents)
        if @contents && @contents.any?
            unlink!
            notify = true            # NOTE: changing from empty doesn't notify
        end
        @contents = contents
        @contents.each { |gui_object| gui_object.parent = self }
        contents_change_notify if notify
    end

    def <<(gui_object)
        @contents << gui_object
        gui_object.parent = self

        #
        # crude float:left support (add it to object, it gets stacked left by the container)
        #
        if gui_object.float == :left
            extra_spacing = (gui_object.offset_x || 0.0)
            gui_object.offset_x = @float_left + (gui_object.scale_x / 2.0) + extra_spacing
            @float_left += gui_object.scale_x + extra_spacing
        elsif gui_object.float == :right
            extra_spacing = (gui_object.offset_x || 0.0)
            gui_object.offset_x = @float_right - (gui_object.scale_x / 2.0) - extra_spacing
            @float_right -= (gui_object.scale_x + extra_spacing)
        end
        contents_change_notify
    end

    def insert(index, object)
        @contents.insert(index, object)
        object.parent = self
        contents_change_notify
    end

    def add_after_selection(object)
        if (selected_object = selection.first) && (index = @contents.index(selected_object))
            insert(index+1, object)
        else
            self << object
        end
        contents_change_notify
    end

    def prepend(gui_object)
        @contents.unshift(gui_object)
        gui_object.parent = self
        contents_change_notify
    end

    def bring_to_top(object)
        if @contents.delete(object)
            @contents << object
            contents_change_notify
        end
    end

    #
    #
    #
    def clear!
        return if empty?
        unlink!
        @contents.clear
        clear_selection!
        contents_change_notify
    end

    def remove(gui_object)
        @contents.delete(gui_object)
        contents_change_notify
    end

    def unlink!
        @contents.each { |gui_object| gui_object.parent = nil }
    end

    #
    # Selection
    #
    callback :selection_change

    def child_is_selected?(object)
        @selection.include?(object)
    end

    def add_to_selection(object)
        @selection << object
        selection_change_notify
    end

    def remove_from_selection(object)
        @selection.delete(object)
        selection_change_notify
    end

    def set_selection(object)
        return if @selection.size == 1 && child_is_selected?(object)
        @selection.clear                    # without notify
        add_to_selection(object)    # with notify
    end

    def selected_index
        selection = @selection.first
        index(selection) if selection
    end

    def clear_selection!
        return if @selection.empty?
        @selection.clear
        selection_change_notify
    end

    def select_next!(number=1)
        return if @contents.empty?
        selection = @selection.first        # TODO: support multiselection?
        index = @contents.index(selection) || -1
        index = (index + number) % @contents.size
        @selection.clear
        add_to_selection(@contents[index])
    end

    def select_previous!(number=1)
        return if @contents.empty?
        selection = @selection.first        # TODO: support multiselection?
        index = @contents.index(selection) || @contents.size
        index = (index - number) % @contents.size
        @selection.clear
        add_to_selection(@contents[index])
    end

    attr_reader :selection

    def selection_grab_focus!
        selected = selection.first
        selected.grab_keyboard_focus! if selected
    end

    #
    # Iterating
    #
    def each(&proc)
        @contents.each(&proc)
    end
    def find(&proc)
        @contents.find(&proc)
    end
    def map(&proc)
        @contents.map(&proc)
    end

    def each_with_positioning
        with_positioning {
            @contents.each { |gui_object| yield gui_object }
        }
    end

    #
    # Reordering
    #
    def move_child_up(child, number=1)
        return unless (index = @contents.index(child))
        if index > (number-1)
            @contents[index], @contents[index-number] = @contents[index-number], @contents[index]
            contents_change_notify
        end
    end

    def move_child_down(child, number=1)
        return unless (index = @contents.index(child))
        if index < (@contents.size - number)
            @contents[index], @contents[index+number] = @contents[index+number], @contents[index]
            contents_change_notify
        end
    end

    #
    # Extend GuiObject methods to pass them along to contents
    #
    def gui_tick
        return if hidden?
        @contents.each { |gui_object| gui_object.gui_tick }
        super
    end

    def gui_render
        return if hidden?
        with_positioning { gui_render_background }
        each_with_positioning { |gui_object| gui_object.gui_render }
        with_positioning { gui_render_keyboard_focus } if keyboard_focus?
    end

    def hit_test_render!
        return if hidden?
        each_with_positioning { |gui_object| gui_object.hit_test_render! }
    end
end