lib/glimmer/swt/table_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_proxy'
module Glimmer
module SWT
class TableProxy < Glimmer::SWT::WidgetProxy
include Glimmer
module TableListenerEvent
def table_item
table_item_and_column_index[:table_item]
end
def column_index
table_item_and_column_index[:column_index]
end
private
def table_item_and_column_index
@table_item_and_column_index ||= find_table_item_and_column_index
end
def find_table_item_and_column_index
{}.tap do |result|
if respond_to?(:x) && respond_to?(:y)
result[:table_item] = widget.items.detect do |ti|
result[:column_index] = widget.column_count.times.to_a.detect do |ci|
ti.getBounds(ci).contains(x, y)
end
end
end
end
end
end
class << self
def editors
@editors ||= {
# ensure editor can work with string keys not just symbols (leave one string in for testing)
'text' => {
widget_value_property: :text,
editor_gui: lambda do |args, model, property, table_proxy|
table_proxy.table_editor.minimumHeight = 20
table_editor_widget_proxy = text(*args) {
text model.send(property)
focus true
on_focus_lost {
table_proxy.finish_edit!
}
on_key_pressed { |key_event|
if key_event.keyCode == swt(:cr)
table_proxy.finish_edit!
elsif key_event.keyCode == swt(:esc)
table_proxy.cancel_edit!
end
}
}
table_editor_widget_proxy.swt_widget.selectAll
table_editor_widget_proxy
end,
},
combo: {
widget_value_property: :text,
editor_gui: lambda do |args, model, property, table_proxy|
first_time = true
table_proxy.table_editor.minimumHeight = 25
table_editor_widget_proxy = combo(*args) {
items model.send("#{property}_options")
text model.send(property)
focus true
on_focus_lost {
table_proxy.finish_edit!
}
on_key_pressed { |key_event|
if key_event.keyCode == swt(:cr)
table_proxy.finish_edit!
elsif key_event.keyCode == swt(:esc)
table_proxy.cancel_edit!
end
}
on_widget_selected {
if !OS.windows? || !first_time || first_time && model.send(property) != table_editor_widget_proxy.swt_widget.text
table_proxy.finish_edit!
end
}
}
table_editor_widget_proxy
end,
},
checkbox: {
widget_value_property: :selection,
editor_gui: lambda do |args, model, property, table_proxy|
first_time = true
table_proxy.table_editor.minimumHeight = 25
checkbox(*args) {
selection model.send(property)
focus true
on_widget_selected {
table_proxy.finish_edit!
}
on_focus_lost {
table_proxy.finish_edit!
}
on_key_pressed { |key_event|
if key_event.keyCode == swt(:cr)
table_proxy.finish_edit!
elsif key_event.keyCode == swt(:esc)
table_proxy.cancel_edit!
end
}
}
end,
},
date: {
widget_value_property: :date_time,
editor_gui: lambda do |args, model, property, table_proxy|
first_time = true
table_proxy.table_editor.minimumHeight = 25
date(*args) {
date_time model.send(property)
focus true
on_focus_lost {
table_proxy.finish_edit!
}
on_key_pressed { |key_event|
if key_event.keyCode == swt(:cr)
table_proxy.finish_edit!
elsif key_event.keyCode == swt(:esc)
table_proxy.cancel_edit!
end
}
}
end,
},
date_drop_down: {
widget_value_property: :date_time,
editor_gui: lambda do |args, model, property, table_proxy|
first_time = true
table_proxy.table_editor.minimumHeight = 25
date_drop_down(*args) {
date_time model.send(property)
focus true
on_focus_lost {
table_proxy.finish_edit!
}
on_key_pressed { |key_event|
if key_event.keyCode == swt(:cr)
table_proxy.finish_edit!
elsif key_event.keyCode == swt(:esc)
table_proxy.cancel_edit!
end
}
}
end,
},
time: {
widget_value_property: :date_time,
editor_gui: lambda do |args, model, property, table_proxy|
first_time = true
table_proxy.table_editor.minimumHeight = 25
time(*args) {
date_time model.send(property)
focus true
on_focus_lost {
table_proxy.finish_edit!
}
on_key_pressed { |key_event|
if key_event.keyCode == swt(:cr)
table_proxy.finish_edit!
elsif key_event.keyCode == swt(:esc)
table_proxy.cancel_edit!
end
}
}
end,
},
radio: {
widget_value_property: :selection,
editor_gui: lambda do |args, model, property, table_proxy|
first_time = true
table_proxy.table_editor.minimumHeight = 25
radio(*args) {
selection model.send(property)
focus true
on_widget_selected {
table_proxy.finish_edit!
}
on_focus_lost {
table_proxy.finish_edit!
}
on_key_pressed { |key_event|
if key_event.keyCode == swt(:cr)
table_proxy.finish_edit!
elsif key_event.keyCode == swt(:esc)
table_proxy.cancel_edit!
end
}
}
end,
},
spinner: {
widget_value_property: :selection,
editor_gui: lambda do |args, model, property, table_proxy|
first_time = true
table_proxy.table_editor.minimumHeight = 25
table_editor_widget_proxy = spinner(*args) {
selection model.send(property)
focus true
on_focus_lost {
table_proxy.finish_edit!
}
on_key_pressed { |key_event|
if key_event.keyCode == swt(:cr)
table_proxy.finish_edit!
elsif key_event.keyCode == swt(:esc)
table_proxy.cancel_edit!
end
}
}
table_editor_widget_proxy
end,
},
}
end
end
attr_reader :table_editor, :table_editor_widget_proxy, :sort_property, :sort_direction, :sort_block, :sort_type, :sort_by_block, :additional_sort_properties, :editor, :editable
attr_writer :column_properties
attr_accessor :table_items_binding, :sort_strategy
alias column_attributes= column_properties=
alias editable? editable
def initialize(underscored_widget_name, parent, args)
editable_style = args.delete(:editable)
super
@table_editor = TableEditor.new(swt_widget)
@table_editor.horizontalAlignment = SWTProxy[:left]
@table_editor.grabHorizontal = true
@table_editor.minimumHeight = 20
self.editable = editable_style
end
def column_properties
if @column_properties.nil?
swt_widget.columns.to_a.map(&:text).map(&:underscore)
elsif @column_properties.is_a?(Hash)
@column_properties = swt_widget.columns.to_a.map(&:text).map do |column_name|
@column_properties[column_name] || column_name.underscore
end
else
@column_properties
end
end
alias column_attributes column_properties
def items
auto_exec do
swt_widget.get_items
end
end
def model_binding
auto_exec do
swt_widget.data
end
end
def table_items_binding
auto_exec do
swt_widget.get_data('table_items_binding')
end
end
def editable=(value)
@editable = value
if @editable
content {
@editable_on_mouse_up = on_mouse_up { |event|
edit_table_item(event.table_item, event.column_index)
}
}
else
@editable_on_mouse_up.deregister if @editable_on_mouse_up
end
end
def sort_block=(comparator)
@sort_block = comparator
end
def sort_by_block=(property_picker)
@sort_by_block = property_picker
end
def sort_property=(new_sort_property)
@sort_property = [new_sort_property].flatten.compact
end
def detect_sort_type
@sort_type = sort_property.size.times.map { String }
array = model_binding.evaluate_property
sort_property.each_with_index do |a_sort_property, i|
values = array.map { |object| object.send(a_sort_property) }
value_classes = values.map(&:class).uniq
if value_classes.size == 1
@sort_type[i] = value_classes.first
elsif value_classes.include?(Integer)
@sort_type[i] = Integer
elsif value_classes.include?(Float)
@sort_type[i] = Float
end
end
end
def column_sort_properties
column_properties.zip(table_column_proxies.map(&:sort_property)).map do |pair|
[pair.compact.last].flatten.compact
end
end
# Sorts by specified TableColumnProxy object. If nil, it uses the table default sort instead.
def sort_by_column!(table_column_proxy=nil)
index = nil
auto_exec do
index = swt_widget.columns.to_a.index(table_column_proxy.swt_widget) unless table_column_proxy.nil?
end
new_sort_property = table_column_proxy.nil? ? @sort_property : table_column_proxy.sort_property || [column_properties[index]]
return if table_column_proxy.nil? && new_sort_property.nil? && @sort_block.nil? && @sort_by_block.nil?
if new_sort_property && table_column_proxy.nil? && new_sort_property.size == 1 && (index = column_sort_properties.index(new_sort_property))
table_column_proxy = table_column_proxies[index]
end
if new_sort_property && new_sort_property.size == 1 && !additional_sort_properties.to_a.empty?
selected_additional_sort_properties = additional_sort_properties.clone
if selected_additional_sort_properties.include?(new_sort_property.first)
selected_additional_sort_properties.delete(new_sort_property.first)
new_sort_property += selected_additional_sort_properties
else
new_sort_property += additional_sort_properties
end
end
@sort_direction = @sort_direction.nil? || @sort_property.first != new_sort_property.first || @sort_direction == :descending ? :ascending : :descending
auto_exec do
swt_widget.sort_direction = @sort_direction == :ascending ? SWTProxy[:up] : SWTProxy[:down]
end
@sort_property = [new_sort_property].flatten.compact
table_column_index = column_properties.index(new_sort_property.to_s.to_sym)
table_column_proxy ||= table_column_proxies[table_column_index] if table_column_index
auto_exec do
swt_widget.sort_column = table_column_proxy.swt_widget if table_column_proxy
end
if table_column_proxy
@sort_by_block = nil
@sort_block = nil
end
@sort_type = nil
if table_column_proxy&.sort_by_block
@sort_by_block = table_column_proxy.sort_by_block
elsif table_column_proxy&.sort_block
@sort_block = table_column_proxy.sort_block
else
detect_sort_type
end
sort!
end
def initial_sort!
sort_by_column!
end
def additional_sort_properties=(args)
@additional_sort_properties = args unless args.empty?
end
def sort!(internal_sort: false)
return unless sort_property && (sort_type || sort_block || sort_by_block)
if sort_strategy
sort_strategy.call
else
original_array = array = model_binding.evaluate_property
array = array.sort_by(&:hash) # this ensures consistent subsequent sorting in case there are equivalent sorts to avoid an infinite loop
# Converting value to_s first to handle nil cases. Should work with numeric, boolean, and date fields
if sort_block
sorted_array = array.sort(&sort_block)
elsif sort_by_block
sorted_array = array.sort_by(&sort_by_block)
else
sorted_array = array.sort_by do |object|
sort_property.each_with_index.map do |a_sort_property, i|
value = object.send(a_sort_property)
# handle nil and difficult to compare types gracefully
if sort_type[i] == Integer
value = value.to_i
elsif sort_type[i] == Float
value = value.to_f
elsif sort_type[i] == String
value = value.to_s.downcase
end
value
end
end
end
sorted_array = sorted_array.reverse if sort_direction == :descending
if model_binding.binding_options.symbolize_keys[:read_only_sort]
table_items_binding.call(sorted_array, internal_sort: true) unless internal_sort
else
model_binding.call(sorted_array)
end
sorted_array
end
end
def no_sort=(value)
table_column_proxies.each do |table_column_proxy|
table_column_proxy.no_sort = value
end
end
def editor=(args)
@editor = args
end
def cells_for(model, table_item_property: :text)
if table_item_property.to_s == 'text'
column_properties.map {|column_property| model.send(column_property)}
else
column_properties.map do |column_property|
model.send("#{column_property}_#{table_item_property}") if model.respond_to?("#{column_property}_#{table_item_property}")
end
end
end
def cells
column_count = @table.column_properties.size
auto_exec do
swt_widget.items.map {|item| column_count.times.map {|i| item.get_text(i)} }
end
end
# Performs a search for table items matching block condition
# If no condition block is passed, returns all table items
# Returns a Java TableItem array to easily set as selection on org.eclipse.swt.Table if needed
def search(&condition)
auto_exec do
swt_widget.getItems.select {|item| condition.nil? || condition.call(item)}.to_java(TableItem)
end
end
# Returns all table items including descendants
def all_table_items
search
end
def post_initialize_child(table_column_proxy)
table_column_proxies << table_column_proxy
end
def post_add_content
return if @initially_sorted
initial_sort!
@initially_sorted = true
end
def table_column_proxies
@table_column_proxies ||= []
end
# Indicates if table is in edit mode, thus displaying a text widget for a table item cell
def edit_mode?
!!@edit_mode
end
def cancel_edit!
@cancel_edit&.call if @edit_mode
end
def finish_edit!
@finish_edit&.call if @edit_mode
end
# Indicates if table is editing a table item because the user hit ENTER or focused out after making a change in edit mode to a table item cell.
# It is set to false once change is saved to model
def edit_in_progress?
!!@edit_in_progress
end
def edit_selected_table_item(column_index, before_write: nil, after_write: nil, after_cancel: nil)
auto_exec do
edit_table_item(swt_widget.getSelection.first, column_index, before_write: before_write, after_write: after_write, after_cancel: after_cancel)
end
end
def edit_table_item(table_item, column_index, before_write: nil, after_write: nil, after_cancel: nil, write_on_cancel: false)
return if table_item.nil?
model = table_item.data
property = column_properties[column_index]
cancel_edit!
return unless table_column_proxies[column_index].editable?
action_taken = false
@edit_mode = true
editor_config = table_column_proxies[column_index].editor || editor
editor_config = editor_config.to_a
editor_widget_options = editor_config.last.is_a?(Hash) ? editor_config.last : {}
editor_widget_arg_last_index = editor_config.last.is_a?(Hash) ? -2 : -1
editor_widget = (editor_config[0] || :text).to_sym
editor_widget_args = editor_config[1..editor_widget_arg_last_index]
model_editing_property = editor_widget_options[:property] || property
widget_value_property = TableProxy::editors.symbolize_keys[editor_widget][:widget_value_property]
@cancel_edit = lambda do |event=nil|
if write_on_cancel
@finish_edit.call(event)
else
@cancel_in_progress = true
@table_editor_widget_proxy&.swt_widget&.dispose
@table_editor_widget_proxy = nil
if after_cancel&.arity == 0
after_cancel&.call
else
after_cancel&.call(table_item)
end
@edit_in_progress = false
@cancel_in_progress = false
@cancel_edit = nil
@edit_mode = false
end
end
@finish_edit = lambda do |event=nil|
new_value = @table_editor_widget_proxy&.send(widget_value_property)
if table_item.isDisposed
@cancel_edit.call unless write_on_cancel
elsif !new_value.nil? && !action_taken && !@edit_in_progress && !@cancel_in_progress
action_taken = true
@edit_in_progress = true
if new_value == model.send(model_editing_property)
@cancel_edit.call unless write_on_cancel
else
if before_write&.arity == 0
before_write&.call
else
before_write&.call(edited_table_item)
end
model.send("#{model_editing_property}=", new_value) # makes table update itself, so must search for selected table item again
# Table refresh happens here because of model update triggering observers, so must retrieve table item again
edited_table_item = search { |ti| ti.getData == model }.first
auto_exec do
swt_widget.showItem(edited_table_item)
end
@table_editor_widget_proxy&.swt_widget&.dispose
@table_editor_widget_proxy = nil
if after_write&.arity == 0
after_write&.call
else
after_write&.call(edited_table_item)
end
@edit_in_progress = false
end
end
end
content {
@table_editor_widget_proxy = TableProxy::editors.symbolize_keys[editor_widget][:editor_gui].call(editor_widget_args, model, model_editing_property, self)
}
@table_editor.setEditor(@table_editor_widget_proxy.swt_widget, table_item, column_index)
rescue => e
Glimmer::Config.logger.error {e.full_message}
raise e
end
def add_listener(underscored_listener_name, &block)
enhanced_block = lambda do |event|
event.extend(TableListenerEvent)
block.call(event)
end
super(underscored_listener_name, &enhanced_block)
end
private
def property_type_converters
super.merge({
selection: lambda do |value|
if value.is_a?(Array)
search {|ti| value.include?(ti.getData) }
else
search {|ti| ti.getData == value}
end
end,
})
end
end
end
end