# Copyright (c) 2007-2023 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