AndyObtiva/glimmer-dsl-swt

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

Summary

Maintainability
F
1 wk
Test Coverage
B
82%
# Copyright (c) 2007-2024 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 'glimmer/swt/widget_listener_proxy'
require 'glimmer/swt/color_proxy'
require 'glimmer/swt/font_proxy'
require 'glimmer/swt/swt_proxy'
require 'glimmer/swt/display_proxy'
require 'glimmer/swt/dnd_proxy'
require 'glimmer/swt/image_proxy'
require 'glimmer/swt/proxy_properties'
require 'glimmer/swt/custom/drawable'

# TODO refactor to make file smaller and extract sub-widget-proxies out of this

module Glimmer
  module SWT
    # Proxy for SWT Widget objects
    #
    # Sets default SWT styles to widgets upon inititalizing as
    # per DEFAULT_STYLES
    #
    # Also, auto-initializes widgets as per initializer blocks
    # in DEFAULT_INITIALIZERS  (e.g. setting Composite default layout)
    #
    # Follows the Proxy Design Pattern
    class WidgetProxy
      include Packages
      include ProxyProperties
      include Custom::Drawable

      DEFAULT_STYLES = {
        'arrow'               => [:arrow, :down],
        'browser'              => ([:edge] if OS.windows?),
        'button'              => [:push],
        'canvas'              => ([:double_buffered] unless OS.mac?),
        'ccombo'              => [:border],
        'checkbox'            => [:check],
        'check'               => [:check],
        'drag_source'         => [:drop_copy],
        'drop_target'         => [:drop_copy],
        'expand_bar'          => [:v_scroll],
        'list'                => [:border, :v_scroll],
        'menu_item'           => [:push],
        'radio'               => [:radio],
        'scrolled_composite'  => [:border, :h_scroll, :v_scroll],
        'spinner'             => [:border],
        'styled_text'         => [:border, :multi, :v_scroll, :h_scroll],
        'table'               => [:virtual, :border, :full_selection],
        'text'                => [:border],
        'toggle'              => [:toggle],
        'tool_bar'            => [:push],
        'tool_item'           => [:push],
        'tree'                => [:virtual, :border, :h_scroll, :v_scroll],
        'date_drop_down'      => [:date, :drop_down],
        'time'                => [:time],
        'calendar'            => [:calendar],
      }

      DEFAULT_INITIALIZERS = {
        composite: lambda do |composite|
          composite.layout = GridLayout.new if composite.get_layout.nil?
        end,
        canvas: lambda do |canvas|
          canvas.layout = nil if canvas.respond_to?('layout=') && !canvas.get_layout.nil?
        end,
        scrolled_composite: lambda do |scrolled_composite|
          scrolled_composite.expand_horizontal = true
          scrolled_composite.expand_vertical = true
        end,
        table: lambda do |table|
          table.setHeaderVisible(true)
          table.setLinesVisible(true)
        end,
        table_column: lambda do |table_column|
          table_column.setWidth(80)
        end,
        group: lambda do |group|
          group.layout = GridLayout.new if group.get_layout.nil?
        end,
      }
      
      KEYWORD_ALIASES = {
        'arrow'          => 'button',
        'checkbox'       => 'button',
        'check'          => 'button',
        'radio'          => 'button',
        'toggle'         => 'button',
        'date'           => 'date_time',
        'date_drop_down' => 'date_time',
        'time'           => 'date_time',
        'calendar'       => 'date_time',
      }
      
      class << self
        # Instantiates the right WidgetProxy subclass for passed in keyword
        # Args are: keyword, parent, swt_widget_args (including styles)
        def create(*init_args, swt_widget: nil)
          DisplayProxy.instance.auto_exec do
            return swt_widget.get_data('proxy') if swt_widget&.get_data('proxy')
            keyword, parent, args = init_args
            selected_widget_proxy_class = widget_proxy_class(keyword || underscored_widget_name(swt_widget))
            if init_args.empty?
              selected_widget_proxy_class.new(swt_widget: swt_widget)
            else
              selected_widget_proxy_class.new(*init_args)
            end
          end
        end
        
        def widget_proxy_class(keyword)
          begin
            keyword = KEYWORD_ALIASES[keyword] if KEYWORD_ALIASES[keyword]
            class_name = "#{keyword.camelcase(:upper)}Proxy".to_sym
            Glimmer::SWT.const_get(class_name)
          rescue
            Glimmer::SWT::WidgetProxy
          end
        end
        
        def underscored_widget_name(swt_widget)
          swt_widget.class.name.split(/::|\./).last.underscore
        end
      end

      attr_reader :parent_proxy, :swt_widget, :drag_source_proxy, :drop_target_proxy, :drag_source_style, :drag_source_transfer, :drop_target_transfer, :finished_add_content
      alias finished_add_content? finished_add_content
      
      # Initializes a new SWT Widget
      #
      # It is preferred to use `::create` method instead since it instantiates the
      # right subclass per widget keyword
      #
      # keyword, parent, swt_widget_args (including styles)
      #
      # Styles is a comma separate list of symbols representing SWT styles in lower case
      def initialize(*init_args, swt_widget: nil)
        auto_exec do
          @image_double_buffered = !!(init_args&.last&.include?(:image_double_buffered) && init_args&.last&.delete(:image_double_buffered))
          if swt_widget.nil?
            underscored_widget_name, parent, args = init_args
            @parent_proxy = parent
            styles, extra_options = extract_args(underscored_widget_name, args)
            swt_widget_class = self.class.swt_widget_class_for(underscored_widget_name)
            @swt_widget = swt_widget_class.new(@parent_proxy.swt_widget, style(underscored_widget_name, styles), *extra_options)
          else
            @swt_widget = swt_widget
            underscored_widget_name = self.class.underscored_widget_name(@swt_widget)
            parent_proxy_class = self.class.widget_proxy_class(self.class.underscored_widget_name(@swt_widget.parent))
            parent = swt_widget.parent
            @parent_proxy = parent&.get_data('proxy') || parent_proxy_class.new(swt_widget: parent)
          end
          shell_proxy # populates @shell_proxy attribute
          if @swt_widget&.get_data('proxy').nil?
            @swt_widget.set_data('proxy', self)
            DEFAULT_INITIALIZERS[underscored_widget_name.to_s.to_sym]&.call(@swt_widget)
            @parent_proxy.post_initialize_child(self)
          end
          @keyword = underscored_widget_name.to_s
          if respond_to?(:on_widget_disposed)
            on_widget_disposed {
              unless shell_proxy.last_shell_closing?
                clear_shapes
                deregister_shape_painting
              end
            }
          end
        end
      end
      
      # Subclasses may override to perform post initialization work on an added child
      def post_initialize_child(child)
        # No Op by default
      end

      # Subclasses may override to perform post add_content work.
      # Make sure its logic detects if it ran before since it could run multiple times
      # when adding content multiple times post creation.
      def post_add_content
        # No Op by default
      end
      
      def finish_add_content!
        @finished_add_content = true
      end
      
      def shell_proxy
        if @swt_widget.respond_to?(:shell) && !@swt_widget.is_disposed
          @shell_proxy = @swt_widget.shell.get_data('proxy')
        elsif @parent_proxy&.shell_proxy
          @shell_proxy = @parent_proxy&.shell_proxy
        else
          @shell_proxy
        end
      end
      
      def extract_args(underscored_widget_name, args)
        @arg_extractor_mapping ||= {
          'menu_item' => lambda do |args|
            index = args.delete(args.last) if args.last.is_a?(Numeric)
            extra_options = [index].compact
            styles = args
            [styles, extra_options]
          end,
        }
        if @arg_extractor_mapping[underscored_widget_name]
          @arg_extractor_mapping[underscored_widget_name].call(args)
        else
          extra_options = []
          style_args = args.select {|arg| arg.is_a?(Symbol) || arg.is_a?(String)}
          if style_args.any?
            style_arg_start_index = args.index(style_args.first)
            style_arg_last_index = args.index(style_args.last)
            extra_options = args[style_arg_last_index+1..-1]
            args = args[style_arg_start_index..style_arg_last_index]
          elsif args.first.is_a?(Integer)
            extra_options = args[1..-1]
            args = args[0..0]
          end
          [args, extra_options]
        end
      end
      
      def proxy_source_object
        @swt_widget
      end

      def has_attribute?(attribute_name, *args)
        # TODO test that attribute getter responds too
        widget_custom_attribute = widget_custom_attribute_mapping[attribute_name.to_s]
        property_type_converter = property_type_converters[attribute_name.to_s.to_sym]
        auto_exec do
          if widget_custom_attribute
            @swt_widget.respond_to?(widget_custom_attribute[:setter][:name])
          elsif property_type_converter
            true
          else
            super
          end
        end
      end

      def set_attribute(attribute_name, *args)
        # TODO Think about widget subclasses overriding set_attribute to add more attributes vs adding as Ruby attributes directly
        widget_custom_attribute = widget_custom_attribute_mapping[attribute_name.to_s]
        auto_exec do
          apply_property_type_converters(normalized_attribute(attribute_name), args)
        end
        swt_widget_operation = false
        result = nil
        auto_exec do
          result = if widget_custom_attribute
            swt_widget_operation = true
            widget_custom_attribute[:setter][:invoker].call(@swt_widget, args)
          end
        end
        result = super unless swt_widget_operation
        result
      end

      def get_attribute(attribute_name)
        widget_custom_attribute = widget_custom_attribute_mapping[attribute_name.to_s]
        swt_widget_operation = false
        result = nil
        auto_exec do
          result = if widget_custom_attribute
            swt_widget_operation = true
            if widget_custom_attribute[:getter][:invoker]
              widget_custom_attribute[:getter][:invoker].call(@swt_widget, [])
            else
              @swt_widget.send(widget_custom_attribute[:getter][:name])
            end
          end
        end
        result = super unless swt_widget_operation
        result
      end

      def pack_same_size
        auto_exec do
          bounds = @swt_widget.getBounds
          listener = on_control_resized {
            @swt_widget.setSize(bounds.width, bounds.height)
            @swt_widget.setLocation(bounds.x, bounds.y)
          }
          if @swt_widget.is_a?(Composite)
            @swt_widget.layout(true, true)
          else
            @swt_widget.pack(true)
          end
          @swt_widget.removeControlListener(listener.swt_listener)
        end
      end
      
      def x
        bounds.x
      end

      def y
        bounds.y
      end
      
      def width
        bounds.width
      end

      def height
        bounds.height
      end

      # these work in tandem with the property_type_converters
      # sometimes, they are specified in subclasses instead
      def widget_property_listener_installers
        @swt_widget_property_listener_installers ||= {
          Java::OrgEclipseSwtWidgets::Control => {
            :focus => lambda do |observer|
              on_focus_gained { |focus_event|
                observer.call(true)
              }
              on_focus_lost { |focus_event|
                observer.call(false)
              }
            end,
            :selection => lambda do |observer|
              on_widget_selected { |selection_event|
                observer.call(@swt_widget.getSelection)
              } if can_handle_observation_request?(:on_widget_selected)
            end,
            :text => lambda do |observer|
              on_modify_text { |modify_event|
                observer.call(@swt_widget.getText)
              } if can_handle_observation_request?(:on_modify_text)
            end,
          },
          Java::OrgEclipseSwtWidgets::Combo => {
            :text => lambda do |observer|
              on_modify_text { |modify_event|
                observer.call(@swt_widget.getText)
              }
            end,
          },
          Java::OrgEclipseSwtCustom::CCombo => {
            :text => lambda do |observer|
              on_modify_text { |modify_event|
                observer.call(@swt_widget.getText)
              }
            end,
          },
          Java::OrgEclipseSwtWidgets::Table => {
            :selection => lambda do |observer|
              on_widget_selected { |selection_event|
                if has_style?(:multi)
                  observer.call(@swt_widget.getSelection.map(&:get_data))
                else
                  observer.call(@swt_widget.getSelection.first&.get_data)
                end
              }
            end,
          },
          Java::OrgEclipseSwtWidgets::Tree => {
            :selection => lambda do |observer|
              on_widget_selected { |selection_event|
                if has_style?(:multi)
                  observer.call(@swt_widget.getSelection.map(&:get_data))
                else
                  observer.call(@swt_widget.getSelection.first&.get_data)
                end
              }
            end,
          },
          Java::OrgEclipseSwtWidgets::Text => {
            :text => lambda do |observer|
              on_modify_text { |modify_event|
                observer.call(@swt_widget.getText)
              }
            end,
            :caret_position => lambda do |observer|
              on_swt_keydown { |event|
                observer.call(@swt_widget.getCaretPosition)
              }
              on_swt_keyup { |event|
                observer.call(@swt_widget.getCaretPosition)
              }
              on_swt_mousedown { |event|
                observer.call(@swt_widget.getCaretPosition)
              }
              on_swt_mouseup { |event|
                observer.call(@swt_widget.getCaretPosition)
              }
            end,
            :selection => lambda do |observer|
              on_swt_keydown { |event|
                observer.call(@swt_widget.getSelection)
              }
              on_swt_keyup { |event|
                observer.call(@swt_widget.getSelection)
              }
              on_swt_mousedown { |event|
                observer.call(@swt_widget.getSelection)
              }
              on_swt_mouseup { |event|
                observer.call(@swt_widget.getSelection)
              }
            end,
            :selection_count => lambda do |observer|
              on_swt_keydown { |event|
                observer.call(@swt_widget.getSelectionCount)
              }
              on_swt_keyup { |event|
                observer.call(@swt_widget.getSelectionCount)
              }
              on_swt_mousedown { |event|
                observer.call(@swt_widget.getSelectionCount)
              }
              on_swt_mouseup { |event|
                observer.call(@swt_widget.getSelectionCount)
              }
            end,
            :top_index => lambda do |observer|
              @last_top_index = @swt_widget.getTopIndex
              on_paint_control { |event|
                if @swt_widget.getTopIndex != @last_top_index
                  @last_top_index = @swt_widget.getTopIndex
                  observer.call(@last_top_index)
                end
              }
            end,
          },
          Java::OrgEclipseSwtCustom::StyledText => {
            :text => lambda do |observer|
              on_modify_text { |modify_event|
                observer.call(@swt_widget.getText)
              }
            end,
            :caret_position => lambda do |observer|
              on_caret_moved { |event|
                observer.call(@swt_widget.getSelection.x) unless @swt_widget.getCaretOffset == 0 && @last_modify_text != text
              }
              on_swt_keyup { |event|
                observer.call(@swt_widget.getSelection.x) unless @swt_widget.getCaretOffset == 0 && @last_modify_text != text
              }
              on_swt_mouseup { |event|
                observer.call(@swt_widget.getSelection.x) unless @swt_widget.getCaretOffset == 0 && @last_modify_text != text
              }
            end,
            :caret_offset => lambda do |observer|
              on_caret_moved { |event|
                observer.call(@swt_widget.getCaretOffset) unless @swt_widget.getCaretOffset == 0 && @last_modify_text != text
              }
            end,
            :selection => lambda do |observer|
              on_widget_selected { |event|
                observer.call(@swt_widget.getSelection) unless @swt_widget.getCaretOffset == 0 && @last_modify_text != text
              }
              on_swt_keyup { |event|
                observer.call(@swt_widget.getSelection) unless @swt_widget.getCaretOffset == 0 && @last_modify_text != text
              }
              on_swt_mouseup { |event|
                observer.call(@swt_widget.getSelection) unless @swt_widget.getCaretOffset == 0 && @last_modify_text != text
              }
            end,
            :selection_count => lambda do |observer|
              on_widget_selected { |event|
                observer.call(@swt_widget.getSelectionCount) unless @swt_widget.getCaretOffset == 0 && @last_modify_text != text
              }
              on_swt_keyup { |event|
                observer.call(@swt_widget.getSelectionCount) unless @swt_widget.getCaretOffset == 0 && @last_modify_text != text
              }
              on_swt_mouseup { |event|
                observer.call(@swt_widget.getSelectionCount) unless @swt_widget.getCaretOffset == 0 && @last_modify_text != text
              }
            end,
            :selection_range => lambda do |observer|
              on_widget_selected { |event|
                observer.call(@swt_widget.getSelectionRange) unless @swt_widget.getCaretOffset == 0 && @last_modify_text != text
              }
              on_swt_keyup { |event|
                observer.call(@swt_widget.getSelectionRange) unless @swt_widget.getCaretOffset == 0 && @last_modify_text != text
              }
              on_swt_mouseup { |event|
                observer.call(@swt_widget.getSelectionRange) unless @swt_widget.getCaretOffset == 0 && @last_modify_text != text
              }
            end,
            :top_index => lambda do |observer|
              @last_top_index = @swt_widget.getTopIndex
              on_paint_control { |event|
                if @swt_widget.getTopIndex != @last_top_index
                  @last_top_index = @swt_widget.getTopIndex
                  observer.call(@last_top_index)
                end
              }
            end,
            :top_pixel => lambda do |observer|
              @last_top_pixel = @swt_widget.getTopPixel
              on_paint_control { |event|
                if @swt_widget.getTopPixel != @last_top_pixel
                  @last_top_pixel = @swt_widget.getTopPixel
                  observer.call(@last_top_pixel)
                end
              }
            end,
          },
          Java::OrgEclipseSwtWidgets::Button => {
            :selection => lambda do |observer|
              on_widget_selected { |selection_event|
                observer.call(@swt_widget.getSelection)
              }
            end
          },
          Java::OrgEclipseSwtWidgets::MenuItem => {
            :selection => lambda do |observer|
              on_widget_selected { |selection_event|
                observer.call(@swt_widget.getSelection)
              }
            end
          },
          Java::OrgEclipseSwtWidgets::Spinner => {
            :selection => lambda do |observer|
              on_widget_selected { |selection_event|
                observer.call(@swt_widget.getSelection)
              }
            end
          },
          Java::OrgEclipseSwtWidgets::DateTime =>
            [:year, :month, :day, :hours, :minutes, :seconds, :date_time, :date, :time].reduce({}) do |hash, attribute|
              hash.merge(
                attribute => lambda do |observer|
                  on_widget_selected { |selection_event|
                    observer.call(get_attribute(attribute))
                  }
                end
              )
            end,
        }
      end

      def self.widget_exists?(underscored_widget_name)
        !!swt_widget_class_for(underscored_widget_name)
      end
      
      # Manual entries of SWT widget classes that conflict with Ruby classes
      def self.swt_widget_class_manual_entries
        {
          'date_time' => Java::OrgEclipseSwtWidgets::DateTime
        }
      end

      # This supports widgets in and out of basic SWT
      def self.swt_widget_class_for(underscored_widget_name)
        # TODO clear memoization for a keyword if a custom widget was defined with that keyword
        unless flyweight_swt_widget_classes.keys.include?(underscored_widget_name)
          begin
            underscored_widget_name = KEYWORD_ALIASES[underscored_widget_name] if KEYWORD_ALIASES[underscored_widget_name]
            swt_widget_name = underscored_widget_name.camelcase(:upper)
            swt_widget_class = eval(swt_widget_name)
            # TODO fix issue with not detecting DateTime because it's conflicting with the Ruby DateTime
            unless swt_widget_class.ancestors.include?(org.eclipse.swt.widgets.Widget)
              swt_widget_class = swt_widget_class_manual_entries[underscored_widget_name]
              if swt_widget_class.nil?
                Glimmer::Config.logger.debug {"Class #{swt_widget_class} matching #{underscored_widget_name} is not a subclass of org.eclipse.swt.widgets.Widget"}
                return nil
              end
            end
            flyweight_swt_widget_classes[underscored_widget_name] = swt_widget_class
          rescue SyntaxError, NameError => e
            Glimmer::Config.logger.debug {e.full_message}
            nil
          rescue => e
            Glimmer::Config.logger.debug {e.full_message}
            nil
          end
        end
        flyweight_swt_widget_classes[underscored_widget_name]
      end
      
      # Flyweight Design Pattern memoization cache. Can be cleared if memory is needed.
      def self.flyweight_swt_widget_classes
        @flyweight_swt_widget_classes ||= {}
      end

      # delegates to DisplayProxy
      def async_exec(&block)
        DisplayProxy.instance.async_exec(&block)
      end

      # delegates to DisplayProxy
      def sync_exec(&block)
        DisplayProxy.instance.sync_exec(&block)
      end

      # delegates to DisplayProxy
      def timer_exec(delay_in_millis, &block)
        DisplayProxy.instance.timer_exec(delay_in_millis, &block)
      end

      # delegates to DisplayProxy
      def auto_exec(override_sync_exec: nil, override_async_exec: nil, &block)
        DisplayProxy.instance.auto_exec(override_sync_exec: override_sync_exec, override_async_exec: override_async_exec, &block)
      end

      def has_style?(style)
        comparison = interpret_style(style)
        auto_exec do
          (@swt_widget.style & comparison) == comparison
        end
      end
      
      def print(gc=nil, job_name: nil)
        if gc.is_a?(org.eclipse.swt.graphics.GC)
          @swt_widget.print(gc)
        else
          image = Image.new(DisplayProxy.instance.swt_display, bounds)
          gc = org.eclipse.swt.graphics.GC.new(image)
          success = print(gc)
          if success
            printer_data = DialogProxy.new('print_dialog', shell.get_data('proxy')).open
            if printer_data
              printer = Printer.new(printer_data)
              job_name ||= 'Glimmer'
              if printer.start_job(job_name)
                printer_gc = org.eclipse.swt.graphics.GC.new(printer)
                if printer.start_page
                  printer_gc.drawImage(image, 0, 0)
                  printer.end_page
                else
                  success = false
                end
                printer_gc.dispose
                printer.end_job
              else
                success = false
              end
              printer.dispose
              gc.dispose
              image.dispose
            end
          end
          success
        end
      end

      def dispose
        auto_exec do
          @swt_widget.dispose
        end
      end

      def disposed?
        @swt_widget.isDisposed
      end

      # TODO Consider renaming these methods as they are mainly used for data-binding

      def can_add_observer?(property_name)
        auto_exec do
          @swt_widget.class.ancestors.map {|ancestor| widget_property_listener_installers[ancestor]}.compact.map(&:keys).flatten.map(&:to_s).include?(property_name.to_s)
        end
      end

      # Used for data-binding only. Consider renaming or improving to avoid the confusion it causes
      def add_observer(observer, property_name)
        if !observer.respond_to?(:binding_options) || !observer.binding_options[:read_only]
          auto_exec do
            property_listener_installers = @swt_widget.class.ancestors.map {|ancestor| widget_property_listener_installers[ancestor]}.compact
            widget_listener_installers = property_listener_installers.map{|installer| installer[property_name.to_s.to_sym]}.compact if !property_listener_installers.empty?
            widget_listener_installers.to_a.first&.call(observer)
          end
        end
      end

      def remove_observer(observer, property_name)
        # TODO consider implementing if remove_observer is needed (consumers can remove listener via SWT API)
      end

      def ensure_drag_source_proxy(style=[])
        @drag_source_proxy ||= self.class.new('drag_source', self, style).tap do |proxy|
          proxy.set_attribute(:transfer, :text)
        end
      end
      
      def ensure_drop_target_proxy(style=[])
        @drop_target_proxy ||= WidgetProxy.new('drop_target', self, style).tap do |proxy|
          proxy.set_attribute(:transfer, :text)
          proxy.on_drag_enter { |event|
            event.detail = DNDProxy[:drop_copy]
          }
        end
      end
      
      # TODO eliminate duplication in the following methods perhaps by relying on exceptions

      def can_handle_observation_request?(observation_request)
        observation_request = normalize_observation_request(observation_request)
        if observation_request.start_with?('on_swt_')
          constant_name = observation_request.sub(/^on_swt_/, '')
          SWTProxy.has_constant?(constant_name)
        elsif observation_request.start_with?('on_')
          event = observation_request.sub(/^on_/, '')
          can_add_listener?(event) || can_handle_drag_observation_request?(observation_request) || can_handle_drop_observation_request?(observation_request)
        end
      end
      
      def can_handle_drag_observation_request?(observation_request)
        auto_exec do
          return false unless swt_widget.is_a?(Control)
          potential_drag_source = @drag_source_proxy.nil?
          ensure_drag_source_proxy
          @drag_source_proxy.can_handle_observation_request?(observation_request).tap do |result|
            if potential_drag_source && !result
              @drag_source_proxy.swt_widget.dispose
              @drag_source_proxy = nil
            end
          end
        end
      rescue => e
        Glimmer::Config.logger.debug {e.full_message}
        false
      end

      def can_handle_drop_observation_request?(observation_request)
        auto_exec do
          return false unless swt_widget.is_a?(Control)
          potential_drop_target = @drop_target_proxy.nil?
          ensure_drop_target_proxy
          @drop_target_proxy.can_handle_observation_request?(observation_request).tap do |result|
            if potential_drop_target && !result
              @drop_target_proxy.swt_widget.dispose
              @drop_target_proxy = nil
            end
          end
        end
      end

      def handle_observation_request(observation_request, &block)
        observation_request = normalize_observation_request(observation_request)
        if observation_request.start_with?('on_drag_enter')
          original_block = block
          block = Proc.new do |event|
            event.detail = DNDProxy[:drop_copy]
            original_block.call(event)
          end
        end
        if observation_request.start_with?('on_swt_')
          constant_name = observation_request.sub(/^on_swt_/, '')
          add_swt_event_listener(constant_name, &block)
        elsif observation_request.start_with?('on_')
          event = observation_request.sub(/^on_/, '')
          if can_add_listener?(event)
            event = observation_request.sub(/^on_/, '')
            add_listener(event, &block)
          elsif can_handle_drag_observation_request?(observation_request)
            @drag_source_proxy&.handle_observation_request(observation_request, &block)
          elsif can_handle_drop_observation_request?(observation_request)
            @drop_target_proxy&.handle_observation_request(observation_request, &block)
          end
        end
      end
      
      def widget_bindings
        @widget_bindings ||= []
      end

      def content(&block)
        auto_exec do
          Glimmer::DSL::Engine.add_content(self, Glimmer::DSL::SWT::WidgetExpression.new, @keyword, &block)
        end
      end

      def method_missing(method_name, *args, &block)
        # TODO push most of this logic down to Properties (and perhaps create Listeners module as well)
        if block && can_handle_observation_request?(method_name)
          handle_observation_request(method_name, &block)
        else
          super
        end
      end
      
      def respond_to?(method_name, *args, &block)
        result = super
        return true if result
        auto_exec do
          can_handle_observation_request?(method_name)
        end
      end
      
      private

      def style(underscored_widget_name, styles)
        styles = [styles].flatten.compact
        styles = default_style(underscored_widget_name) if styles.empty?
        interpret_style(*styles)
      end
      
      def interpret_style(*styles)
        SWTProxy[*styles] rescue DNDProxy[*styles]
      end

      def default_style(underscored_widget_name)
        DEFAULT_STYLES[underscored_widget_name] || [:none]
      end

      # TODO refactor following methods to eliminate duplication
      # perhaps consider relying on raising an exception to avoid checking first
      # unless that gives obscure SWT errors
      # Otherwise, consider caching results from can_add_lsitener and using them in
      # add_listener knowing it will be called for sure afterwards

      def can_add_listener?(underscored_listener_name)
        auto_exec do
          @swt_widget && !self.class.find_listener(@swt_widget.getClass, underscored_listener_name).empty?
        end
      end

      def add_listener(underscored_listener_name, &block)
        auto_exec do
          widget_add_listener_method, listener_class, listener_method = self.class.find_listener(@swt_widget.getClass, underscored_listener_name)
          widget_listener_proxy = nil
          safe_block = lambda do |*args|
            begin
              block.call(*args) unless @swt_widget.isDisposed
            rescue => e
              Glimmer::Config.logger.error {e}
            end
          end
          listener = listener_class.new(listener_method => safe_block)
          @swt_widget.send(widget_add_listener_method, listener)
          WidgetListenerProxy.new(swt_widget: @swt_widget, swt_listener: listener, widget_add_listener_method: widget_add_listener_method, swt_listener_class: listener_class, swt_listener_method: listener_method)
        end
      end

      # Looks through SWT class add***Listener methods till it finds one for which
      # the argument is a listener class that has an event method matching
      # underscored_listener_name
      def self.find_listener(swt_widget_class, underscored_listener_name)
        @listeners ||= {}
        listener_key = [swt_widget_class.name, underscored_listener_name]
        unless @listeners.has_key?(listener_key)
          listener_method_name = underscored_listener_name.camelcase(:lower)
          swt_widget_class.getMethods.each do |widget_add_listener_method|
            if widget_add_listener_method.getName.match(/add.*Listener/)
              widget_add_listener_method.getParameterTypes.each do |listener_type|
                listener_type.getMethods.each do |listener_method|
                  if (listener_method.getName == listener_method_name)
                    @listeners[listener_key] = [widget_add_listener_method.getName, listener_class(listener_type), listener_method.getName]
                    return @listeners[listener_key]
                  end
                end
              end
            end
          end
          @listeners[listener_key] = []
        end
        @listeners[listener_key]
      end

      # Returns a Ruby class that implements listener type Java interface with ability to easily
      # install a block that gets called upon calling a listener event method
      def self.listener_class(listener_type)
        @listener_classes ||= {}
        listener_class_key = listener_type.name
        unless @listener_classes.has_key?(listener_class_key)
          @listener_classes[listener_class_key] = Class.new(Object).tap do |listener_class|
            listener_class.send :include, (eval listener_type.name.sub("interface", ""))
            listener_class.define_method('initialize') do |event_method_block_mapping|
              @event_method_block_mapping = event_method_block_mapping
            end
            listener_type.getMethods.each do |event_method|
              listener_class.define_method(event_method.getName) do |*args|
                @event_method_block_mapping[event_method.getName]&.call(*args)
              end
            end
          end
        end
        @listener_classes[listener_class_key]
      end

      def add_swt_event_listener(swt_constant, &block)
        auto_exec do
          event_type = SWTProxy[swt_constant]
          widget_listener_proxy = nil
          safe_block = lambda { |*args| block.call(*args) unless @swt_widget.isDisposed }
          @swt_widget.addListener(event_type, &safe_block)
          WidgetListenerProxy.new(swt_widget: @swt_widget, swt_listener: @swt_widget.getListeners(event_type).last, event_type: event_type, swt_constant: swt_constant)
        end
      end

      def widget_custom_attribute_mapping
        # TODO scope per widget class type just like other mappings
        @swt_widget_custom_attribute_mapping ||= {
          'drag_source' => {
            getter: {name: 'getShell', invoker: lambda { |widget, args|
              @drag_source
            }},
            setter: {name: 'getShell', invoker: lambda { |widget, args|
              @drag_source = args.first
              if @drag_source
                case @swt_widget
                when List
                  on_drag_set_data do |event|
                    drag_widget = event.widget.control
                    event.data = drag_widget.selection.first
                  end
                when Label
                  on_drag_set_data do |event|
                    drag_widget = event.widget.control
                    event.data = drag_widget.text
                  end
                when Text
                  on_drag_set_data do |event|
                    drag_widget = event.widget.control
                    event.data = drag_widget.selection_text
                  end
                when Spinner
                  on_drag_set_data do |event|
                    drag_widget = event.widget.control
                    event.data = drag_widget.selection.to_s
                  end
                end
              end
            }},
          },
          'drop_target' => {
            getter: {name: 'getShell', invoker: lambda { |widget, args|
              @drop_target
            }},
            setter: {name: 'getShell', invoker: lambda { |widget, args|
              @drop_target = args.first
              if @drop_target
                case @swt_widget
                when List
                  on_drop do |event|
                    drop_widget = event.widget.control
                    drop_widget.add(event.data) unless @drop_target == :unique && drop_widget.items.include?(event.data)
                    drop_widget.select(drop_widget.items.count - 1)
                  end
                when Label
                  on_drop do |event|
                    drop_widget = event.widget.control
                    drop_widget.text = event.data
                  end
                when Text
                  on_drop do |event|
                    drop_widget = event.widget.control
                    if @drop_target == :replace
                      drop_widget.text = event.data
                    else
                      drop_widget.insert(event.data)
                    end
                  end
                when Spinner
                  on_drop do |event|
                    drop_widget = event.widget.control
                    drop_widget.selection = event.data.to_f
                  end
                end
              end
            }},
          },
          'window' => {
            getter: {name: 'getShell'},
            setter: {name: 'getShell', invoker: lambda { |widget, args| @swt_widget.getShell }}, # No Op
          },
          'focus' => {
            getter: {name: 'isFocusControl'},
            setter: {name: 'setFocus', invoker: lambda { |widget, args| @swt_widget.setFocus if args.first }},
          },
          'caret_position' => {
            getter: {name: 'getCaretPosition', invoker: lambda { |widget, args| @swt_widget.respond_to?(:getCaretPosition) ? @swt_widget.getCaretPosition : @swt_widget.getSelection.x}},
            setter: {name: 'setSelection', invoker: lambda { |widget, args| @swt_widget.setSelection(args.first, args.first + @swt_widget.getSelectionCount) if args.first }},
          },
          'selection_count' => {
            getter: {name: 'getSelectionCount'},
            setter: {name: 'setSelection', invoker: lambda { |widget, args|
              if args.first
                caret_position = @swt_widget.respond_to?(:getCaretPosition) ? @swt_widget.getCaretPosition : @swt_widget.getSelection.x
                # TODO handle negative length
                @swt_widget.setSelection(caret_position, caret_position + args.first)
              end
            }},
          },
        }
      end

      def drag_source_style=(style)
        ensure_drag_source_proxy(style)
      end

      def drop_target_style=(style)
        ensure_drop_target_proxy(style)
      end

      def drag_source_transfer=(args)
        args = args.first if !args.empty? && args.first.is_a?(ArrayJavaProxy)
        ensure_drag_source_proxy
        @drag_source_proxy.set_attribute(:transfer, args)
      end

      def drop_target_transfer=(args)
        args = args.first if !args.empty? && args.first.is_a?(ArrayJavaProxy)
        ensure_drop_target_proxy
        @drop_target_proxy.set_attribute(:transfer, args)
      end

      def drag_source_effect=(args)
        args = args.first if args.is_a?(Array)
        ensure_drag_source_proxy
        @drag_source_proxy.set_attribute(:drag_source_effect, args)
      end

      def drop_target_effect=(args)
        args = args.first if args.is_a?(Array)
        ensure_drop_target_proxy
        @drop_target_proxy.set_attribute(:drop_target_effect, args)
      end
      
      def normalize_observation_request(observation_request)
        observation_request = observation_request.to_s
        if @swt_widget.is_a?(Shell) && observation_request.downcase.include?('window')
          observation_request = observation_request.sub('window', 'shell').sub('Window', 'Shell')
        end
        observation_request
      end

      def apply_property_type_converters(attribute_name, args)
        value = args
        converter = property_type_converters[attribute_name.to_sym]
        args[0..-1] = [converter.call(*value)] if converter
        if args.count == 1 && args.first.is_a?(ColorProxy)
          g_color = args.first
          args[0] = g_color.swt_color
        end
      end

      def property_type_converters
        color_converter = lambda do |value|
          if value.is_a?(Symbol) || value.is_a?(String)
            ColorProxy.new(value).swt_color
          elsif value.is_a?(RGB)
            ColorProxy.new(value.red, value.green, value.blue).swt_color
          else
            value
          end
        end
        # TODO consider detecting type on widget method and automatically invoking right converter (e.g. :to_s for String, :to_i for Integer)
        @property_type_converters ||= {
          accelerator: lambda { |*value|
            SWTProxy[*value]
          },
          alignment: lambda { |*value|
            SWTProxy[*value]
          },
          background: color_converter,
          background_image: lambda do |*value|
            image_proxy = ImageProxy.create(*value)
            
            if image_proxy&.file_path&.end_with?('.gif')
              image = image_proxy.swt_image
              width = image.get_bounds.width.to_i
              height = image.get_bounds.height.to_i
              image_number = 0
              loader = ImageLoader.new
              loader.load(image_proxy.input_stream)
              image.dispose
              image = org.eclipse.swt.graphics.Image.new(DisplayProxy.instance.swt_display,loader.data[0].scaledTo(width, height))
              gc = org.eclipse.swt.graphics.GC.new(image)
              on_paint_control { |event|
                image_number = (image_number == loader.data.length - 1) ? 0 : image_number + 1
                next_frame_data = loader.data[image_number]
                image = org.eclipse.swt.graphics.Image.new(DisplayProxy.instance.swt_display, next_frame_data.scaledTo(width, height))
                event.gc.drawImage(image, 0, 0, width, height, 0, 0, width, height)
                image.dispose
              }
              Thread.new {
                last_image_number = -1
                while last_image_number != image_number
                  last_image_number = image_number
                  sync_exec {
                    redraw unless disposed?
                  }
                  delayTime = loader.data[image_number].delayTime.to_f / 100.0
                  sleep(delayTime)
                end
              };
              image_proxy = nil
            else
              on_swt_Resize do |resize_event|
                image_proxy.scale_to(@swt_widget.getSize.x, @swt_widget.getSize.y)
                @swt_widget.setBackgroundImage(image_proxy.swt_image)
              end
            end
            
            image_proxy&.swt_image
          end,
          cursor: lambda do |value|
            cursor_proxy = nil
            if value.is_a?(CursorProxy)
              cursor_proxy = value
            elsif value.is_a?(Symbol) || value.is_a?(String) || value.is_a?(Integer)
              cursor_proxy = CursorProxy.new(value)
            end
            cursor_proxy ? cursor_proxy.swt_cursor : value
          end,
          :enabled => lambda do |value|
            !!value
          end,
          foreground: color_converter,
          selection_foreground: color_converter,
          link_foreground: color_converter,
          font: lambda do |value|
            if value.is_a?(Hash) || value.is_a?(FontData)
              font_properties = value
              FontProxy.new(self, font_properties).swt_font
            else
              value
            end
          end,
          image: lambda do |*value|
            ImageProxy.create(*value).swt_image
          end,
          disabled_image: lambda do |*value|
            ImageProxy.create(*value).swt_image
          end,
          hot_image: lambda do |*value|
            ImageProxy.create(*value).swt_image
          end,
          images: lambda do |array|
            array.to_a.map do |value|
              ImageProxy.create(value).swt_image
            end.to_java(Image)
          end,
          items: lambda do |value|
            value.map(&:to_s).to_java :string
          end,
          maximized_control: lambda do |value|
            value = (value.respond_to?(:swt_widget) ? value.swt_widget : value) if swt_widget.is_a?(SashForm)
            value
          end,
          orientation: lambda do |value|
            value = SWTProxy[value] if swt_widget.is_a?(SashForm)
            value
          end,
          selection: lambda do |value|
            value = value.to_f if swt_widget.is_a?(Spinner)
            value
          end,
          text: lambda do |value|
            value.to_s
          end,
          transfer: lambda do |value|
            value = value.first if value.is_a?(Array) && value.size == 1 && value.first.is_a?(Array)
            transfer_object_extrapolator = lambda do |transfer_name|
              transfer_type = "#{transfer_name.to_s.camelcase(:upper)}Transfer".to_sym
              transfer_type_alternative = "#{transfer_name.to_s.upcase}Transfer".to_sym
              transfer_class = org.eclipse.swt.dnd.const_get(transfer_type) rescue org.eclipse.swt.dnd.const_get(transfer_type_alternative)
              transfer_class.getInstance
            end
            result = value
            if value.is_a?(Symbol) || value.is_a?(String)
              result = [transfer_object_extrapolator.call(value)]
            elsif value.is_a?(Array)
              result = value.map do |transfer_name|
                transfer_object_extrapolator.call(transfer_name)
              end
            end
            result = result.to_java(Transfer) unless result.is_a?(ArrayJavaProxy)
            result
          end,
          visible: lambda do |value|
            !!value
          end,
          weights: lambda do |value|
            value.to_java(:int)
          end,
        }
      end
    end
  end
end