AndyObtiva/glimmer-dsl-swt

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

Summary

Maintainability
A
1 hr
Test Coverage
A
90%
# 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/custom/drawable'

module Glimmer
  module SWT
    # Proxy for org.eclipse.swt.widgets.Display
    #
    # Maintains a singleton instance since SWT only supports
    # a single active display at a time.
    #
    # Supports SWT Display's very useful asyncExec and syncExec methods
    # to support proper multi-threaded manipulation of SWT UI objects
    #
    # Invoking `#swt_display` returns the SWT Display object wrapped by this proxy
    #
    # Follows the Proxy Design Pattern
    class DisplayProxy
      include_package 'org.eclipse.swt.widgets'
      
      include Custom::Drawable
      
      OBSERVED_MENU_ITEMS = ['about', 'preferences', 'quit']
      
      class ConcreteListener
        include org.eclipse.swt.widgets.Listener
        
        def initialize(&listener_block)
          @listener_block = listener_block
        end
        
        def handleEvent(event)
          @listener_block.call(event)
        end
      end

      class << self
        # Returns singleton instance
        def instance(*args)
          if @instance.nil? || @instance.swt_display.nil? || @instance.swt_display.isDisposed
            @thread = Thread.current
            @instance = new(*args)
          end
          @instance
        end
        
        def thread
          instance # ensure instance
          @thread
        end
        
        # Current custom widgets, shells, and shapes being rendered. Useful to yoke all observers evaluated during rendering of their custom widgets/shells/shapes for automatical disposal on_widget_disposed/on_shape_disposed
        def current_custom_widgets_and_shapes
          @current_custom_widgets_and_shapes ||= []
        end
      end

      # SWT Display object wrapped
      attr_reader :swt_display

      def initialize(*args)
        Display.app_name ||= 'Glimmer'
        @swt_display = Display.new(*args)
        @swt_display.set_data('proxy', self)
        @execs_in_progress = {}
        on_swt_Dispose {
          clear_shapes
        }
      end
      
      def content(&block)
        Glimmer::DSL::Engine.add_content(self, Glimmer::DSL::SWT::DisplayExpression.new, 'display', &block)
      end
      
      # asynchronously executes the block (required from threads other than first GUI thread)
      # does not return the value produced by the block since it is async, running after the return
      def async_exec(&block)
        @swt_display.asyncExec do
          execs_in_progress << :async_exec
          begin
            result = block.call
          ensure
            execs_in_progress.pop
          end
        end
      end

      # synchronously executes the block (required from threads other than first GUI thread)
      # returns the value produced by the block
      def sync_exec(&block)
        result = nil
        @swt_display.syncExec do
          execs_in_progress << :sync_exec
          begin
            result = block.call
          ensure
            execs_in_progress.pop
          end
        end
        result
      end

      def timer_exec(delay_in_millis, &block)
        @swt_display.timerExec(delay_in_millis, &block)
      end
      
      # Indicates whether `sync_exec` is required because of running in a different thread from the GUI thread
      # `async_exec` could be used as an alternative to `sync_exec` when required.
      def sync_exec_required?
        Thread.current != DisplayProxy.thread
      end
      
      def async_exec_in_progress?
        execs_in_progress.last == :async_exec
      end
      
      def sync_exec_in_progress?
        execs_in_progress.include?(:sync_exec)
      end
      
      def execs_in_progress
        @execs_in_progress[Thread.current] ||= []
      end
      
      # Invoke block with `sync_exec` only when necessary (running from a separate thread)
      # Override sync_exec as `true` to force using or `false` to force avoiding
      # Override async_exec as `true` to force using or `:unless_in_progress` to force using only if no `async_exec` is in progress
      # Disable auto execution of `sync_exec` via `Glimmer::Config.auto_sync_exec = false`
      # Otherwise, runs normally, thus allowing SWT to decide how to batch/optimize GUI updates
      def auto_exec(override_sync_exec: nil, override_async_exec: nil, &block)
        if override_sync_exec || override_sync_exec.nil? && !override_async_exec && sync_exec_required? && Config.auto_sync_exec? && !sync_exec_in_progress? && !async_exec_in_progress?
          sync_exec(&block)
        elsif override_async_exec || override_async_exec.to_s == 'unless_in_progress' && !async_exec_in_progress?
          async_exec(&block)
        else
          block.call
        end
      end

      def on_widget_disposed(&block)
        on_swt_Dispose(&block)
      end
      
      def disposed?
        @swt_display.isDisposed
      end

      def method_missing(method_name, *args, &block)
        if block && can_handle_observation_request?(method_name)
          handle_observation_request(method_name, &block)
        else
          swt_display.send(method_name, *args, &block)
        end
      rescue => e
        Glimmer::Config.logger.debug {"Neither DisplayProxy nor #{swt_display.class.name} can handle the method ##{method_name}"}
        super
      end
      
      def respond_to?(method_name, *args, &block)
        super ||
          can_handle_observation_request?(method_name) ||
          swt_display.respond_to?(method_name, *args, &block)
      end

      def can_handle_observation_request?(observation_request)
        observation_request = observation_request.to_s
        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_name = observation_request.sub(/^on_/, '')
          OBSERVED_MENU_ITEMS.include?(event_name)
        else
          false
        end
      end

      def handle_observation_request(observation_request, &block)
        observation_request = observation_request.to_s
        if observation_request.start_with?('on_swt_')
          constant_name = observation_request.sub(/^on_swt_/, '')
          swt_event_reg = add_swt_event_filter(constant_name, &block)
          DisplayProxy.current_custom_widgets_and_shapes.last&.observer_registrations&.push(swt_event_reg)
          swt_event_reg
        elsif observation_request.start_with?('on_')
          event_name = observation_request.sub(/^on_/, '')
          if OBSERVED_MENU_ITEMS.include?(event_name) && OS.mac?
            auto_exec do
              system_menu = swt_display.getSystemMenu
              menu_item = system_menu.getItems.find {|menu_item| menu_item.getID == SWTProxy["ID_#{event_name.upcase}"]}
              listener = ConcreteListener.new(&block)
              display_mac_event_registration = menu_item.addListener(SWTProxy[:Selection], listener)
              DisplayProxy.current_custom_widgets_and_shapes.last&.observer_registrations&.push(display_mac_event_registration)
              display_mac_event_registration
            end
          end
        end
      end

      def add_swt_event_filter(swt_constant, &block)
        event_type = SWTProxy[swt_constant]
        swt_listener = ConcreteListener.new(&block)
        @swt_display.addFilter(event_type, swt_listener)
        #WidgetListenerProxy.new(@swt_display.getListeners(event_type).last)
        WidgetListenerProxy.new(
          swt_display: @swt_display,
          event_type: event_type,
          filter: true,
          swt_listener: swt_listener,
          widget_add_listener_method: 'addFilter',
          swt_listener_class:  ConcreteListener,
          swt_listener_method: 'handleEvent'
        )
      end
    end
  end
end