lib/glimmer/swt/display_proxy.rb
# 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