AndyObtiva/glimmer-dsl-tk

View on GitHub
lib/glimmer/tk/widget_proxy.rb

Summary

Maintainability
F
5 days
Test Coverage
# Copyright (c) 2020-2022 Andy Maleh
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

require 'yaml'
require 'glimmer/data_binding/tk/one_time_observer'
require 'glimmer/tk/widget'
require 'glimmer/tk/draggable_and_droppable'

module Glimmer
  module Tk
    # Proxy for Tk Widget objects
    #
    # Follows the Proxy Design Pattern
    class WidgetProxy
      class << self
        def create(keyword, parent, args, &block)
          widget_proxy_class(keyword).new(keyword, parent, args, &block)
        end
        
        def widget_proxy_class(keyword)
          begin
            class_name = "#{keyword.camelcase(:upper)}Proxy".to_sym
            Glimmer::Tk.const_get(class_name)
          rescue => e
            Glimmer::Config.logger.debug {"Unable to instantiate custom class name for #{keyword} ... defaulting to Glimmer::Tk::WidgetProxy"}
            Glimmer::Config.logger.debug {e.full_message}
            Glimmer::Tk::WidgetProxy
          end
        end
        
        # This supports widgets in and out of basic Tk
        def tk_widget_class_for(underscored_widget_name)
          tk_widget_class_basename = underscored_widget_name.camelcase(:upper)
          # TODO consider exposing this via Glimmer::Config
          potential_tk_widget_class_names = [
            "::Tk::Tile::#{tk_widget_class_basename}",
            "::Tk#{tk_widget_class_basename}",
            "::Tk::#{tk_widget_class_basename}",
            "::Tk::BWidget::#{tk_widget_class_basename}",
            "::Tk::Iwidgets::#{tk_widget_class_basename}",
            "::Glimmer::Tk::#{tk_widget_class_basename}Proxy",
          ]
          tk_widget_class = nil
          potential_tk_widget_class_names.each do |tk_widget_name|
            begin
              tk_widget_class = eval(tk_widget_name)
              break
            rescue RuntimeError, SyntaxError, NameError => e
              Glimmer::Config.logger.debug {e.full_message}
            end
          end
          tk_widget_class if tk_widget_class.respond_to?(:new)
        end
      end
      
      prepend DraggableAndDroppable
      
      FONTS_PREDEFINED = %w[default text fixed menu heading caption small_caption icon tooltip]
      HEXADECIMAL_CHARACTERS = %w[0 1 2 3 4 5 6 7 8 9 a b c d e f]
      
      attr_reader :parent_proxy, :tk, :args, :keyword, :children, :bind_ids, :destroyed
      alias destroyed? destroyed

      # Initializes a new Tk Widget
      #
      # Styles is a comma separate list of symbols representing Tk styles in lower case
      def initialize(underscored_widget_name, parent_proxy, args, &block)
        @parent_proxy = parent_proxy
        @args = args
        @keyword = underscored_widget_name
        @block = block
        build_widget
        # a common widget initializer
        @parent_proxy&.post_initialize_child(self)
        initialize_defaults
        post_add_content if @block.nil?
      end
      
      def root_parent_proxy
        current = self
        current = current.parent_proxy while current.parent_proxy
        current
      end
      
      def toplevel_parent_proxy
        ancestor_proxies.find {|widget_proxy| widget_proxy.is_a?(ToplevelProxy)}
      end
      alias closest_window toplevel_parent_proxy
      
      # returns list of ancestors ordered from direct parent to root parent
      def ancestor_proxies
        ancestors = []
        current = self
        while current.parent_proxy
          ancestors << current.parent_proxy
          current = current.parent_proxy
        end
        ancestors
      end
      
      def children
        @children ||= []
      end
      
      # Subclasses may override to perform post initialization work on an added child
      def post_initialize_child(child)
        children << child
      end

      # Subclasses may override to perform post add_content work
      def post_add_content
        # No Op by default
      end

      def self.widget_exists?(underscored_widget_name)
        !!tk_widget_class_for(underscored_widget_name) || (Glimmer::Tk.constants.include?("#{underscored_widget_name.camelcase(:upper)}Proxy".to_sym) && Glimmer::Tk.const_get("#{underscored_widget_name.camelcase(:upper)}Proxy".to_sym).respond_to?(:new))
      end

      def tk_widget_has_attribute_setter?(attribute)
        return true if @tk.respond_to?(attribute) && attribute != 'focus' # TODO configure exceptions via constant if needed
        result = nil
        begin
          # TK Widget currently doesn't support respond_to? properly, so I have to resort to this trick for now
          @tk.send(attribute_setter(attribute), @tk.send(attribute))
          result = true
        rescue => e
          Glimmer::Config.logger.debug { "No tk attribute setter for #{attribute}" }
          Glimmer::Config.logger.debug { e.full_message }
          result = false
        end
        result
      end
      
      def tk_widget_has_attribute_getter_setter?(attribute)
        begin
          # TK Widget currently doesn't support respond_to? properly, so I have to resort to this trick for now
          @tk.send(attribute)
          true
        rescue => e
          Glimmer::Config.logger.debug { "No tk attribute getter setter for #{attribute}" }
          false
        end
      end
      
      def has_state?(attribute)
        attribute = attribute.to_s
        attribute = attribute.sub(/\?$/, '').sub(/=$/, '')
        if @tk.respond_to?(:tile_state)
          begin
            @tk.tile_instate(attribute)
            true
          rescue => e
            Glimmer::Config.logger.debug { "No tk state for #{attribute}" }
            false
          end
        else
          false
        end
      end
      
      def has_attributes_attribute?(attribute)
        attribute = attribute.to_s
        attribute = attribute.sub(/\?$/, '').sub(/=$/, '')
        @tk.respond_to?(:attributes) && @tk.attributes.keys.include?(attribute.to_s)
      end
      
      def has_attribute?(attribute, *args)
        (widget_custom_attribute_mapping[tk.class] and widget_custom_attribute_mapping[tk.class][attribute.to_s]) or
          tk_widget_has_attribute_setter?(attribute) or
          tk_widget_has_attribute_getter_setter?(attribute) or
          has_state?(attribute) or
          has_attributes_attribute?(attribute) or
          respond_to?(attribute_setter(attribute), args) or
          respond_to?(attribute_setter(attribute), *args, super_only: true) or
          respond_to?(attribute, *args, super_only: true)
      end

      def set_attribute(attribute, *args)
        begin
          args = normalize_attribute_arguments(attribute, args)
          widget_custom_attribute = widget_custom_attribute_mapping[tk.class] && widget_custom_attribute_mapping[tk.class][attribute.to_s]
          if respond_to?(attribute_setter(attribute), super_only: true)
            send(attribute_setter(attribute), *args)
          elsif respond_to?(attribute, super_only: true) && self.class.instance_method(attribute).parameters.size > 0
            send(attribute, *args)
          elsif widget_custom_attribute
            widget_custom_attribute[:setter][:invoker].call(@tk, args)
          elsif tk_widget_has_attribute_setter?(attribute)
            unless args.size == 1 && @tk.send(attribute) == args.first
              if args.size == 1
                @tk.send(attribute_setter(attribute), *args)
              else
                @tk.send(attribute_setter(attribute), args)
              end
            end
          elsif tk_widget_has_attribute_getter_setter?(attribute)
            @tk.send(attribute, *args)
          elsif has_state?(attribute)
            attribute = attribute.sub(/=$/, '')
            if !!args.first
              @tk.tile_state(attribute)
            else
              @tk.tile_state("!#{attribute}")
            end
          elsif has_attributes_attribute?(attribute)
            attribute = attribute.sub(/=$/, '')
            @tk.attributes(attribute, args.first)
          else
            raise "#{self} cannot handle attribute #{attribute} with args #{args.inspect}"
          end
        rescue => e
          Glimmer::Config.logger.error {"Failed to set attribute #{attribute} with args #{args.inspect}. Attempting to set through style instead..."}
          Glimmer::Config.logger.error {e.full_message}
          apply_style(attribute => args.first)
        end
      end

      def get_attribute(attribute)
        widget_custom_attribute = widget_custom_attribute_mapping[tk.class] && widget_custom_attribute_mapping[tk.class][attribute.to_s]
        if respond_to?(attribute, super_only: true)
          send(attribute)
        elsif widget_custom_attribute
          widget_custom_attribute[:getter][:invoker].call(@tk, args)
        elsif tk_widget_has_attribute_getter_setter?(attribute)
          @tk.send(attribute)
        elsif has_state?(attribute)
          @tk.tile_instate(attribute.sub(/\?$/, ''))
        elsif has_attributes_attribute?(attribute)
          result = @tk.attributes[attribute.sub(/\?$/, '')]
          result = result == 1 if result.is_a?(Integer)
          result
        else
          send(attribute)
        end
      end

      def attribute_setter(attribute)
        "#{attribute}="
      end
      
      def style=(style_or_styles)
        if style_or_styles.is_a? String
          @tk.style(style_or_styles)
        else
          style_or_styles.each do |attribute, value|
            apply_style(attribute => value)
          end
        end
      end

      def grid(options = {})
        @_visible = true
        options = options.stringify_keys
        options['rowspan'] = options.delete('row_span') if options.keys.include?('row_span')
        options['columnspan'] = options.delete('column_span') if options.keys.include?('column_span')
        options['rowweight'] = options.delete('row_weight') if options.keys.include?('row_weight')
        options['columnweight'] = options.delete('column_weight') if options.keys.include?('column_weight')
        options['columnweight'] = options['rowweight'] = options.delete('weight')  if options.keys.include?('weight')
        options['rowminsize'] = options.delete('row_minsize') if options.keys.include?('row_minsize')
        options['rowminsize'] = options.delete('minheight') if options.keys.include?('minheight')
        options['rowminsize'] = options.delete('min_height') if options.keys.include?('min_height')
        options['columnminsize'] = options.delete('column_minsize') if options.keys.include?('column_minsize')
        options['columnminsize'] = options.delete('minwidth') if options.keys.include?('minwidth')
        options['columnminsize'] = options.delete('min_width') if options.keys.include?('min_width')
        options['columnminsize'] = options['rowminsize'] = options.delete('minsize') if options.keys.include?('minsize')
        options['rowuniform'] = options.delete('row_uniform') || options.delete('rowuniform')
        options['columnuniform'] = options.delete('column_uniform') || options.delete('columnuniform')
        index_in_parent = griddable_parent_proxy&.children&.index(griddable_proxy)
        if index_in_parent
          TkGrid.rowconfigure(griddable_parent_proxy.tk, index_in_parent, 'weight'=> options.delete('rowweight')) if options.keys.include?('rowweight')
          TkGrid.rowconfigure(griddable_parent_proxy.tk, index_in_parent, 'minsize'=> options.delete('rowminsize')) if options.keys.include?('rowminsize')
          TkGrid.rowconfigure(griddable_parent_proxy.tk, index_in_parent, 'uniform'=> options.delete('rowuniform')) if options.keys.include?('rowuniform')
          TkGrid.columnconfigure(griddable_parent_proxy.tk, index_in_parent, 'weight'=> options.delete('columnweight')) if options.keys.include?('columnweight')
          TkGrid.columnconfigure(griddable_parent_proxy.tk, index_in_parent, 'minsize'=> options.delete('columnminsize')) if options.keys.include?('columnminsize')
          TkGrid.columnconfigure(griddable_parent_proxy.tk, index_in_parent, 'uniform'=> options.delete('columnuniform')) if options.keys.include?('columnuniform')
        end
        griddable_proxy&.tk&.grid(options)
      end
      
      def font=(value)
        if (value.is_a?(Symbol) || value.is_a?(String)) && FONTS_PREDEFINED.include?(value.to_s.downcase)
          @tk.font = "tk_#{value}_font".camelcase(:upper)
        else
          @tk.font = value.is_a?(TkFont) ? value : TkFont.new(value)
        end
      rescue => e
        Glimmer::Config.logger.debug {"Failed to set attribute #{attribute} with args #{args.inspect}. Attempting to set through style instead..."}
        Glimmer::Config.logger.debug {e.full_message}
        apply_style({"font" => value})
      end
      
      def destroy
        children.each(&:destroy)
        unbind_all
        @tk.destroy
        @on_destroy_procs&.each {|p| p.call(@tk)}
        @destroyed = true
      end
      
      def apply_style(options)
        @@style_number = 0 unless defined?(@@style_number)
        style = "style#{@@style_number += 1}.#{@tk.class.name.split('::').last}"
        ::Tk::Tile::Style.configure(style, options)
        @tk.style = style
      end
      
      def normalize_attribute_arguments(attribute, args)
        attribute_argument_normalizers[attribute]&.call(args) || args
      end
      
      def attribute_argument_normalizers
        color_normalizer = lambda do |args|
          if args.size > 1 || args.first.is_a?(Numeric)
            rgb = args
            rgb = rgb.map(&:to_s).map(&:to_i)
            rgb = 3.times.map { |n| rgb[n] || 0}
            hex = rgb.map { |color| color.to_s(16).ljust(2, '0') }.join
            ["##{hex}"]
          elsif args.size == 1 &&
                args.first.is_a?(String) &&
                !args.first.start_with?('#') &&
                (args.first.size == 3 || args.first.size == 6)  &&
                (args.first.chars.all? {|char| HEXADECIMAL_CHARACTERS.include?(char.downcase)})
            ["##{args.first}"]
          else
            args
          end
        end
        @attribute_argument_normalizers ||= {
          'background' => color_normalizer,
          'foreground' => color_normalizer,
        }
      end
      
      def widget_custom_attribute_mapping
        # TODO consider extracting to modules/subclasses
        @widget_custom_attribute_mapping ||= {
          ::Tk::Tile::TButton => {
            'image' => {
              getter: {name: 'image', invoker: lambda { |widget, args| @tk.image }},
              setter: {name: 'image=', invoker: lambda { |widget, args| @tk.image = image_argument(args) }},
            },
          },
          ::Tk::Tile::TCheckbutton => {
            'image' => {
              getter: {name: 'image', invoker: lambda { |widget, args| @tk.image }},
              setter: {name: 'image=', invoker: lambda { |widget, args| @tk.image = image_argument(args) }},
            },
            'variable' => {
              getter: {name: 'variable', invoker: lambda { |widget, args| @tk.variable&.value.to_s == @tk.onvalue.to_s }},
              setter: {name: 'variable=', invoker: lambda { |widget, args| @tk.variable&.value = args.first.is_a?(Integer) ? args.first : (args.first ? 1 : 0) }},
            },
          },
          ::Tk::Tile::TRadiobutton => {
            'image' => {
              getter: {name: 'image', invoker: lambda { |widget, args| @tk.image }},
              setter: {name: 'image=', invoker: lambda { |widget, args| @tk.image = image_argument(args) }},
            },
            'text' => {
              getter: {name: 'text', invoker: lambda { |widget, args| @tk.text }},
              setter: {name: 'text=', invoker: lambda { |widget, args| @tk.value = @tk.text = args.first }},
            },
            'variable' => {
              getter: {name: 'variable', invoker: lambda { |widget, args| @tk.variable&.value == @tk.value }},
              setter: {name: 'variable=', invoker: lambda { |widget, args| @tk.variable&.value = args.first ? @tk.value : @tk.variable&.value }},
            },
          },
          ::Tk::Tile::TCombobox => {
            'text' => {
              getter: {name: 'text', invoker: lambda { |widget, args| @tk.textvariable&.value }},
              setter: {name: 'text=', invoker: lambda { |widget, args| @tk.textvariable&.value = args.first }},
            },
          },
          ::Tk::Tile::TLabel => {
            'text' => {
              getter: {name: 'text', invoker: lambda { |widget, args| @tk.textvariable&.value }},
              setter: {name: 'text=', invoker: lambda { |widget, args| @tk.textvariable&.value = args.first }},
            },
            'image' => {
              getter: {name: 'image', invoker: lambda { |widget, args| @tk.image }},
              setter: {name: 'image=', invoker: lambda { |widget, args| @tk.image = image_argument(args) }},
            },
          },
          ::Tk::Tile::TEntry => {
            'text' => {
              getter: {name: 'text', invoker: lambda { |widget, args| @tk.textvariable&.value }},
              setter: {name: 'text=', invoker: lambda { |widget, args| @tk.textvariable&.value = args.first }},
            },
          },
          ::Tk::Tile::TSpinbox => {
            'text' => {
              getter: {name: 'text', invoker: lambda { |widget, args| @tk.textvariable&.value }},
              setter: {name: 'text=', invoker: lambda { |widget, args| @tk.textvariable&.value = args.first }},
            },
          },
          ::Tk::Tile::TScale => {
            'variable' => {
              getter: {name: 'variable', invoker: lambda { |widget, args| @tk.variable&.value }},
              setter: {name: 'variable=', invoker: lambda { |widget, args| @tk.variable&.value = args.first }},
            },
          },
          ::Tk::Root => {
            'text' => {
              getter: {name: 'text', invoker: lambda { |widget, args| @tk.title }},
              setter: {name: 'text=', invoker: lambda { |widget, args| @tk.title = args.first }},
            },
            'icon_photo' => {
              getter: {name: 'icon_photo', invoker: lambda { |widget, args| @tk.iconphoto }},
              setter: {name: 'icon_photo=', invoker: lambda { |widget, args| @tk.iconphoto = image_argument(args) }},
            },
          },
        }
      end
      
      def widget_attribute_listener_installers
        # TODO consider extracting to modules/subclasses
        @tk_widget_attribute_listener_installers ||= {
          ::Tk::Tile::TCheckbutton => {
            'variable' => lambda do |observer|
              @tk.command {
                observer.call(@tk.variable.value.to_s == @tk.onvalue.to_s)
              }
            end,
          },
          ::Tk::Tile::TCombobox => {
            'text' => lambda do |observer|
              if observer.is_a?(Glimmer::DataBinding::ModelBinding)
                model = observer.model
                options_model_property = observer.property_name + '_options'
                if model.respond_to?(options_model_property)
                  options_observer = Glimmer::DataBinding::Observer.proc do
                    @tk.values = model.send(options_model_property)
                  end
                  options_observer.observe(model, options_model_property)
                  options_observer.call
                end
              end
              @tk.bind('<ComboboxSelected>') {
                observer.call(@tk.textvariable.value)
              }
            end,
          },
          ::Tk::Tile::TEntry => {
            'text' => lambda do |observer|
              @tk.textvariable.trace('write') {
                observer.call(@tk.textvariable.value)
              }
            end,
          },
          ::Tk::Tile::TSpinbox => {
            'text' => lambda do |observer|
              @tk.command {
                observer.call(@tk.textvariable&.value)
              }
              @tk.validate('key')
              @tk.validatecommand { |validate_args|
                observer.call(validate_args.value)
                new_icursor = validate_args.index
                new_icursor += validate_args.string.size if validate_args.action == 1
                @tk.icursor = new_icursor
                true
              }
            end,
          },
          ::Tk::Tile::TScale => {
            'variable' => lambda do |observer|
              @tk.command {
                observer.call(@tk.variable&.value)
              }
            end,
          },
          ::Tk::Text => {
            'value' => lambda do |observer|
              handle_listener('modified') do
                observer.call(value)
              end
            end,
          },
          ::Tk::Tile::TRadiobutton => {
            'variable' => lambda do |observer|
              # tk.command is called only when the radiobutton is clicked/selected
              # it isn't called when other radiobutton in the group is clicked/selected == this radiobutton is deselected
              # so, we need to call the observers of the deselected radiobuttons in order for their variables to be reset to false
              @tk.command {
                if @tk.variable.value == @tk.value
                  sibling_radio_buttons.each do |sibling_radio_button|
                    sibling_radio_button.tk.command.call
                  end
                end
                observer.call(@tk.variable.value == @tk.value)
              }
            end,
          },
        }
      end
      
      def image_argument(args)
        if args.first.is_a?(::TkPhotoImage)
          args.first
        else
          image_args = {}
          image_args.merge!(file: args.first.to_s) if args.first.is_a?(String)
          the_image = ::TkPhotoImage.new(image_args)
          if args.last.is_a?(Hash)
            processed_image = ::TkPhotoImage.new
            processed_image.copy(the_image, args.last)
            the_image = processed_image
          end
          the_image
        end
      end
      
      def add_observer(observer, attribute)
        attribute_listener_installers = @tk.class.ancestors.map {|ancestor| widget_attribute_listener_installers[ancestor]}.compact
        widget_listener_installers = attribute_listener_installers.map{|installer| installer[attribute.to_s]}.compact if !attribute_listener_installers.empty?
        widget_listener_installers.to_a.first&.call(observer)
      end
      
      def handle_listener(listener_name, &listener)
        listener_name = listener_name.to_s
        # TODO return a listener registration object that has a deregister method
        if listener_name == 'destroy'
          # 'destroy' is a more reliable alternative listener binding to '<Destroy>'
          @on_destroy_procs ||= []
          listener.singleton_class.include(Glimmer::DataBinding::Tk::OneTimeObserver) unless listener.is_a?(Glimmer::DataBinding::Tk::OneTimeObserver)
          @on_destroy_procs << listener
          @tk.bind('<Destroy>', listener)
          parent_proxy.handle_listener(listener_name, &listener) if parent_proxy
        else
          @listeners ||= {}
          begin
            @listeners[listener_name] ||= []
            if @tk.respond_to?(listener_name)
              @tk.send(listener_name) { |*args| @listeners[listener_name].each {|l| l.call(*args)} } if @listeners[listener_name].empty?
            else
              @tk.bind(listener_name) { |*args| @listeners[listener_name].each {|l| l.call(*args)} } if @listeners[listener_name].empty?
            end
            @listeners[listener_name] << listener
          rescue => e
            @listeners.delete(listener_name)
            Glimmer::Config.logger.debug {"Unable to bind to #{listener_name} .. attempting to surround with <> ..."}
            Glimmer::Config.logger.debug {e.full_message}
            new_listener_name = listener_name
            new_listener_name = "<#{new_listener_name}" if !new_listener_name.start_with?('<')
            new_listener_name = "#{new_listener_name}>" if !new_listener_name.end_with?('>')
            @listeners[new_listener_name] ||= []
            @tk.bind(new_listener_name) { |*args| @listeners[new_listener_name].each {|l| l.call(*args)} } if @listeners[new_listener_name].empty?
            @listeners[new_listener_name] << listener
          end
        end
      end
      
      def on(listener_name, &listener)
        handle_listener(listener_name, &listener)
      end

      def event_generate(event_name, data = nil)
        data = YAML.dump(data) if data && !data.is_a?(String)
        tk.event_generate("<#{event_name}>", data: data)
      end

      def unbind_all
        @listeners&.keys&.each do |key|
          if key.to_s.downcase.include?('command')
            @tk.send(key, '')
          else
            @tk.bind_remove(key)
          end
        end
        @listeners = nil
      end

      def clear_children
        children.each(&:destroy)
        @children = []
      end

      def content(&block)
        Glimmer::DSL::Engine.add_content(self, Glimmer::DSL::Tk::WidgetExpression.new, keyword, *args, &block)
      end

      def window?
        false
      end

      def visible
        @visible = true if @visible.nil?
        @visible
      end
      alias visible? visible

      def visible=(value)
        value = !!value
        return if visible == value

        if value
          grid
        else
          tk.grid_remove
        end
        @visible = value
      end

      def hidden
        !visible
      end
      alias hidden? hidden

      def hidden=(value)
        self.visible = !value
      end

      def enabled
        tk.state == 'normal'
      end
      alias enabled? enabled

      def enabled=(value)
        tk.state = !!value ? 'normal' : 'disabled'
      end

      def disabled
        !enabled
      end
      alias disabled? disabled

      def disabled=(value)
        self.enabled = !value
      end

      def method_missing(method, *args, &block)
        method = method.to_s
        attribute_name = method.sub(/=$/, '')
        args = normalize_attribute_arguments(attribute_name, args)
        if args.empty? && block.nil? && ((widget_custom_attribute_mapping[tk.class] && widget_custom_attribute_mapping[tk.class][method]) || has_state?(method) || has_attributes_attribute?(method))
          get_attribute(method)
        elsif method.end_with?('=') && block.nil? && ((widget_custom_attribute_mapping[tk.class] && widget_custom_attribute_mapping[tk.class][method.sub(/=$/, '')]) || has_state?(method) || has_attributes_attribute?(method))
          set_attribute(attribute_name, *args)
        else
          tk.send(method, *args, &block)
        end
      rescue => e
        Glimmer::Config.logger.debug {"Neither WidgetProxy nor #{tk.class.name} can handle the method ##{method}"}
        super(method.to_sym, *args, &block)
      end
      
      def respond_to?(method, *args, super_only: false, &block)
        super(method, true) ||
          !super_only && tk.respond_to?(method, *args, &block)
      end
      
      # inspect is overridden to prevent printing very long stack traces
      def inspect
        "#{super[0, 150]}... >"
      end
      
      private
      
      # The griddable parent widget proxy to apply grid to (is different from @tk in composite widgets like notebook or scrolledframe)
      def griddable_parent_proxy
        @parent_proxy
      end
      
      # The griddable widget proxy to apply grid to (is different from @tk in composite widgets like notebook or scrolledframe)
      def griddable_proxy
        self
      end
      
      def build_widget
        tk_widget_class = self.class.tk_widget_class_for(@keyword)
        @tk = tk_widget_class.new(@parent_proxy.tk, *args).tap {|tk| tk.singleton_class.include(Glimmer::Tk::Widget); tk.proxy = self}
      end
      
      def initialize_defaults
        options = {}
        options[:sticky] = 'nsew'
        options[:column_weight] = 1 if @parent_proxy.children.count == 1
        grid(options) unless @tk.is_a?(::Tk::Toplevel) || @tk.is_a?(::Tk::Menu) || @tk.nil? # TODO refactor by adding a griddable? method that could be overriden by subclasses to consult for this call
      end
    end
  end
end

Dir[File.expand_path('./*_proxy.rb', __dir__)].each {|f| require f}