glimmer-dsl-opal 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,26 +12,32 @@ module Glimmer
12
12
  include DataBinding::Observer
13
13
 
14
14
  def initialize(parent, model_binding, column_properties)
15
- @last_model_collection = nil
15
+ @last_populated_model_collection = nil
16
16
  @table = parent
17
17
  @model_binding = model_binding
18
18
  @column_properties = column_properties
19
- if @table.respond_to?(:column_properties=)
20
- @table.column_properties = @column_properties
21
- ##else # assume custom widget
22
- ## @table.body_root.column_properties = @column_properties
23
- end
24
- call(@model_binding.evaluate_property)
25
- model = model_binding.base_model
26
- observe(model, model_binding.property_name_expression)
19
+ @table.data = @model_binding
27
20
  ##@table.on_widget_disposed do |dispose_event| # doesn't seem needed within Opal
28
21
  ## unregister_all_observables
29
22
  ##end
23
+ if @table.respond_to?(:column_properties=)
24
+ @table.column_properties = @column_properties
25
+ else # assume custom widget
26
+ @table.body_root.column_properties = @column_properties
27
+ end
28
+ @table_observer_registration = observe(model_binding)
29
+ call
30
30
  end
31
31
 
32
32
  def call(new_model_collection=nil)
33
+ new_model_collection = @model_binding.evaluate_property # this ensures applying converters (e.g. :on_read)
34
+ table_cells = @table.items.map {|item| @table.column_properties.size.times.map {|i| item.get_text(i)} }
35
+ model_cells = new_model_collection.to_a.map {|m| @table.cells_for(m)}
36
+ return if table_cells == model_cells
33
37
  if new_model_collection and new_model_collection.is_a?(Array)
34
- observe(new_model_collection, @column_properties)
38
+ # @table_items_observer_registration&.unobserve
39
+ @table_items_observer_registration = observe(new_model_collection, @column_properties)
40
+ add_dependent(@table_observer_registration => @table_items_observer_registration)
35
41
  @model_collection = new_model_collection
36
42
  end
37
43
  populate_table(@model_collection, @table, @column_properties)
@@ -39,8 +45,8 @@ module Glimmer
39
45
  end
40
46
 
41
47
  def populate_table(model_collection, parent, column_properties)
42
- return if model_collection&.sort_by(&:hash) == @last_model_collection&.sort_by(&:hash)
43
- @last_model_collection = model_collection
48
+ return if model_collection&.sort_by(&:hash) == @last_populated_model_collection&.sort_by(&:hash)
49
+ @last_populated_model_collection = model_collection
44
50
  # TODO improve performance
45
51
  selected_table_item_models = parent.selection.map(&:get_data)
46
52
  old_items = parent.items
@@ -55,16 +61,21 @@ module Glimmer
55
61
  table_item.id = old_item_ids_per_model[model.hash] if old_item_ids_per_model[model.hash]
56
62
  end
57
63
  selected_table_items = parent.search {|item| selected_table_item_models.include?(item.get_data) }
58
- selected_table_items = [parent.items.first] if selected_table_items.empty? && !parent.items.empty?
59
- parent.selection = selected_table_items unless selected_table_items.empty?
64
+ parent.selection = selected_table_items
60
65
  parent.redraw
61
66
  end
62
67
 
63
68
  def sort_table(model_collection, parent, column_properties)
64
- return if model_collection == @last_model_collection
65
- parent.items = parent.items.sort_by { |item| model_collection.index(item.get_data) }
66
- @last_model_collection = model_collection
67
- end
69
+ return if model_collection == @last_sorted_model_collection
70
+ if model_collection == @last_populated_model_collection
71
+ # Reapply the last table sort. The model collection has just been populated since it diverged from what it was before
72
+ parent.sort!
73
+ else
74
+ # The model collection was sorted by the model, but beyond sorting, it did not change from the last populated model collection.
75
+ parent.items = parent.items.sort_by { |item| model_collection.index(item.get_data) }
76
+ @last_sorted_model_collection = @last_populated_model_collection = model_collection
77
+ end
78
+ end
68
79
  end
69
80
  end
70
81
  end
@@ -0,0 +1,41 @@
1
+ # Copyright (c) 2007-2020 Andy Maleh
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ require 'glimmer/dsl/expression'
23
+
24
+ module Glimmer
25
+ module DSL
26
+ module Opal
27
+ class BlockPropertyExpression < Expression
28
+ def can_interpret?(parent, keyword, *args, &block)
29
+ block_given? and
30
+ args.size == 0 and
31
+ parent.respond_to?("#{keyword}_block=")
32
+ end
33
+
34
+ def interpret(parent, keyword, *args, &block)
35
+ parent.send("#{keyword}_block=", block)
36
+ nil
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -25,6 +25,7 @@ require 'glimmer/dsl/opal/custom_widget_expression'
25
25
  require 'glimmer/dsl/opal/swt_expression'
26
26
  require 'glimmer/dsl/opal/radio_group_selection_data_binding_expression'
27
27
  require 'glimmer/dsl/opal/checkbox_group_selection_data_binding_expression'
28
+ require 'glimmer/dsl/opal/block_property_expression'
28
29
 
29
30
  module Glimmer
30
31
  module DSL
@@ -42,6 +43,7 @@ module Glimmer
42
43
  data_binding
43
44
  font
44
45
  layout
46
+ block_property
45
47
  property
46
48
  widget
47
49
  ]
@@ -24,6 +24,10 @@ module Glimmer
24
24
  Document
25
25
  end
26
26
 
27
+ def shells
28
+ @shells ||= []
29
+ end
30
+
27
31
  def render
28
32
  # No rendering as body is rendered as part of ShellProxy.. this class only serves as an SWT Display utility
29
33
  end
@@ -1,4 +1,5 @@
1
1
  require 'glimmer/swt/widget_proxy'
2
+ require 'glimmer/swt/display_proxy'
2
3
 
3
4
  module Glimmer
4
5
  module SWT
@@ -7,7 +8,7 @@ module Glimmer
7
8
 
8
9
  def initialize(parent, args, block)
9
10
  i = 0
10
- @parent = parent
11
+ @parent = parent || DisplayProxy.instance.shells.first
11
12
  @args = args
12
13
  @block = block
13
14
  @children = Set.new
@@ -7,7 +7,7 @@ module Glimmer
7
7
  end
8
8
 
9
9
  def set_attribute(attribute_name, *args)
10
- send(attribute_setter(attribute_name), *args) unless send(attribute_getter(attribute_name)) == args.first
10
+ send(attribute_setter(attribute_name), *args) unless args.size == 1 && send(attribute_getter(attribute_name)) == args.first
11
11
  end
12
12
 
13
13
  def attribute_setter(attribute_name)
@@ -16,7 +16,7 @@ module Glimmer
16
16
 
17
17
  def attribute_getter(attribute_name)
18
18
  attribute_name.to_s.underscore
19
- end
19
+ end
20
20
  end
21
21
  end
22
22
  end
@@ -21,6 +21,7 @@ module Glimmer
21
21
  @layout.margin_width = 0
22
22
  @layout.margin_height = 0
23
23
  self.minimum_size = Point.new(WIDTH_MIN, HEIGHT_MIN)
24
+ DisplayProxy.instance.shells << self
24
25
  end
25
26
 
26
27
  def element
@@ -207,6 +208,7 @@ module Glimmer
207
208
  body_class = ([name] + css_classes.to_a).join(' ')
208
209
  @dom ||= html {
209
210
  div(id: body_id, class: body_class) {
211
+ # TODO support the idea of dynamic CSS building on close of shell that adds only as much CSS as needed for widgets that were mentioned
210
212
  style(class: 'common-style') {
211
213
  style_dom_css
212
214
  }
@@ -246,6 +248,12 @@ module Glimmer
246
248
  style(class: 'scrolled-composite-style') {
247
249
  Glimmer::SWT::ScrolledCompositeProxy::STYLE
248
250
  }
251
+ style(class: 'table-item-style') {
252
+ Glimmer::SWT::TableItemProxy::STYLE
253
+ }
254
+ style(class: 'table-column-style') {
255
+ Glimmer::SWT::TableColumnProxy::STYLE
256
+ }
249
257
  }
250
258
  }.to_s
251
259
  end
@@ -1,12 +1,51 @@
1
1
  require 'glimmer/swt/widget_proxy'
2
+ require 'glimmer/swt/swt_proxy'
2
3
 
3
4
  module Glimmer
4
5
  module SWT
5
6
  class TableColumnProxy < WidgetProxy
7
+ STYLE = <<~CSS
8
+ th.table-column {
9
+ background: rgb(246, 246, 246);
10
+ text-align: left;
11
+ padding: 5px;
12
+ }
13
+
14
+ th.table-column .sort-direction {
15
+ float: right;
16
+ }
17
+ CSS
6
18
  include Glimmer
7
19
 
8
- attr_reader :text, :width
20
+ attr_accessor :sort_block, :sort_by_block
21
+ attr_reader :text, :width,
22
+ :no_sort, :sort_property, :editor
23
+ alias no_sort? no_sort
24
+
25
+ def initialize(parent, args, block)
26
+ @no_sort = args.delete(:no_sort)
27
+ super(parent, args, block)
28
+ unless no_sort?
29
+ content {
30
+ on_widget_selected { |event|
31
+ parent.sort_by_column!(self)
32
+ }
33
+ }
34
+ end
35
+ end
36
+
37
+ def sort_property=(args)
38
+ @sort_property = args unless args.empty?
39
+ end
40
+
41
+ def sort_direction
42
+ parent.sort_direction if parent.sort_column == self
43
+ end
9
44
 
45
+ def redraw_sort_direction
46
+ sort_icon_dom_element.attr('class', sort_icon_class)
47
+ end
48
+
10
49
  def text=(value)
11
50
  @text = value
12
51
  redraw
@@ -21,36 +60,47 @@ module Glimmer
21
60
  parent.columns_path
22
61
  end
23
62
 
24
- def css
25
- <<~CSS
26
- width: #{width}px;
27
- CSS
28
- end
29
-
30
63
  def element
31
64
  'th'
32
65
  end
33
66
 
67
+ def sort_icon_class
68
+ @sort_icon_class = 'sort-direction'
69
+ @sort_icon_class += (sort_direction == SWTProxy[:up] ? ' ui-icon ui-icon-caret-1-n' : ' ui-icon ui-icon-caret-1-s') unless sort_direction.nil?
70
+ @sort_icon_class
71
+ end
72
+
73
+ def sort_icon_dom_element
74
+ dom_element.find('.sort-direction')
75
+ end
76
+
34
77
  def observation_request_to_event_mapping
35
78
  {
36
79
  'on_widget_selected' => {
37
- event: 'click'
80
+ event: 'click',
81
+ event_handler: -> (event_listener) {
82
+ -> (event) {
83
+ event_listener.call(event)
84
+ redraw_sort_direction
85
+ }
86
+ }
38
87
  },
39
88
  }
40
- end
89
+ end
41
90
 
42
91
  def dom
43
92
  table_column_text = text
44
93
  table_column_id = id
45
- table_column_id_style = css
94
+ table_column_id_style = "width: #{width}px;"
46
95
  table_column_css_classes = css_classes
47
96
  table_column_css_classes << name
48
97
  @dom ||= html {
49
98
  th(id: table_column_id, style: table_column_id_style, class: table_column_css_classes.to_a.join(' ')) {
50
- table_column_text
99
+ span {table_column_text}
100
+ span(class: sort_icon_class)
51
101
  }
52
102
  }.to_s
53
- end
103
+ end
54
104
  end
55
105
  end
56
106
  end
@@ -3,10 +3,17 @@ require 'glimmer/swt/widget_proxy'
3
3
  module Glimmer
4
4
  module SWT
5
5
  class TableItemProxy < WidgetProxy
6
+ STYLE = <<~CSS
7
+ tr.table-item:nth-child(even):not(.selected) {
8
+ background: rgb(243, 244, 246);
9
+ }
10
+ CSS
11
+
6
12
  attr_reader :data
7
13
 
8
14
  def initialize(parent, args, block)
9
15
  super(parent, args, block)
16
+ # TODO check if there is a need to remove this observer when removing widget from table upon items update
10
17
  on_widget_selected { |event|
11
18
  parent.select(parent.index_of(self), event.meta?)
12
19
  }
@@ -54,7 +61,7 @@ module Glimmer
54
61
  end
55
62
 
56
63
  def redraw
57
- super() #TODO re-enalbe and remove below lines
64
+ super() #TODO re-enable and remove below lines
58
65
 
59
66
  # TODO perhaps turn the following lambdas into methods
60
67
  table_item_edit_handler = lambda do |event, cancel = false|
@@ -4,15 +4,22 @@ require 'glimmer/swt/table_column_proxy'
4
4
  module Glimmer
5
5
  module SWT
6
6
  class TableProxy < WidgetProxy
7
- attr_reader :columns, :selection
8
- attr_accessor :column_properties
7
+ attr_reader :columns, :selection,
8
+ :sort_type, :sort_column, :sort_property, :sort_block, :sort_by_block, :additional_sort_properties
9
+ attr_accessor :column_properties, :item_count, :data
9
10
  alias items children
11
+ alias model_binding data
10
12
 
11
13
  def initialize(parent, args, block)
12
14
  super(parent, args, block)
13
15
  @columns = []
14
16
  @children = []
15
17
  @selection = []
18
+ if editable?
19
+ on_mouse_up { |event|
20
+ edit_table_item(event.table_item, event.column_index)
21
+ }
22
+ end
16
23
  end
17
24
 
18
25
  # Only table_columns may be added as children
@@ -25,13 +32,33 @@ module Glimmer
25
32
  child.redraw
26
33
  end
27
34
 
35
+ def post_add_content
36
+ return if @initially_sorted
37
+ initial_sort!
38
+ @initially_sorted = true
39
+ end
40
+
41
+ def get_data(key=nil)
42
+ data
43
+ end
44
+
28
45
  def remove_all
29
46
  items.clear
30
47
  redraw
31
48
  end
32
49
 
50
+ def editable?
51
+ args.include?(:editable)
52
+ end
53
+ alias editable editable?
54
+
55
+ def selection
56
+ @selection.to_a
57
+ end
58
+
33
59
  def selection=(new_selection)
34
- changed = (@selection + new_selection) - (@selection & new_selection)
60
+ new_selection = new_selection.to_a
61
+ changed = (selection + new_selection) - (selection & new_selection)
35
62
  @selection = new_selection
36
63
  changed.each(&:redraw)
37
64
  end
@@ -41,6 +68,15 @@ module Glimmer
41
68
  redraw
42
69
  end
43
70
 
71
+ def item_count=(value)
72
+ @item_count = value
73
+ redraw_empty_items
74
+ end
75
+
76
+ def cells_for(model)
77
+ column_properties.map {|property| model.send(property)}
78
+ end
79
+
44
80
  def search(&condition)
45
81
  items.select {|item| condition.nil? || condition.call(item)}
46
82
  end
@@ -61,8 +97,147 @@ module Glimmer
61
97
  self.selection = new_selection
62
98
  end
63
99
 
100
+ def search(&condition)
101
+ items.select {|item| condition.nil? || condition.call(item)}
102
+ end
103
+
104
+ def sort_block=(comparator)
105
+ @sort_block = comparator
106
+ end
107
+
108
+ def sort_by_block=(property_picker)
109
+ @sort_by_block = property_picker
110
+ end
111
+
112
+ def sort_property=(new_sort_property)
113
+ @sort_property = [new_sort_property].flatten.compact
114
+ end
115
+
116
+ def detect_sort_type
117
+ @sort_type = sort_property.size.times.map { String }
118
+ array = model_binding.evaluate_property
119
+ sort_property.each_with_index do |a_sort_property, i|
120
+ values = array.map { |object| object.send(a_sort_property) }
121
+ value_classes = values.map(&:class).uniq
122
+ if value_classes.size == 1
123
+ @sort_type[i] = value_classes.first
124
+ elsif value_classes.include?(Integer)
125
+ @sort_type[i] = Integer
126
+ elsif value_classes.include?(Float)
127
+ @sort_type[i] = Float
128
+ end
129
+ end
130
+ end
131
+
132
+ def column_sort_properties
133
+ column_properties.zip(columns.map(&:sort_property)).map do |pair|
134
+ [pair.compact.last].flatten.compact
135
+ end
136
+ end
137
+
138
+ def sort_direction
139
+ @sort_direction == :ascending ? SWTProxy[:up] : SWTProxy[:down]
140
+ end
141
+
142
+ def sort_direction=(value)
143
+ @sort_direction = value == SWTProxy[:up] ? :ascending : :descending
144
+ end
145
+
146
+ # Sorts by specified TableColumnProxy object. If nil, it uses the table default sort instead.
147
+ def sort_by_column!(table_column_proxy=nil)
148
+ index = columns.to_a.index(table_column_proxy) unless table_column_proxy.nil?
149
+ new_sort_property = table_column_proxy.nil? ? @sort_property : table_column_proxy.sort_property || [column_properties[index]]
150
+
151
+ return if table_column_proxy.nil? && new_sort_property.nil? && @sort_block.nil? && @sort_by_block.nil?
152
+ if new_sort_property && table_column_proxy.nil? && new_sort_property.size == 1 && (index = column_sort_properties.index(new_sort_property))
153
+ table_column_proxy = columns[index]
154
+ end
155
+ if new_sort_property && new_sort_property.size == 1 && !additional_sort_properties.to_a.empty?
156
+ selected_additional_sort_properties = additional_sort_properties.clone
157
+ if selected_additional_sort_properties.include?(new_sort_property.first)
158
+ selected_additional_sort_properties.delete(new_sort_property.first)
159
+ new_sort_property += selected_additional_sort_properties
160
+ else
161
+ new_sort_property += additional_sort_properties
162
+ end
163
+ end
164
+
165
+ new_sort_property = [new_sort_property].flatten.compact unless new_sort_property.is_a?(Array)
166
+ @sort_direction = @sort_direction.nil? || @sort_property.first != new_sort_property.first || @sort_direction == :descending ? :ascending : :descending
167
+
168
+ @sort_property = new_sort_property
169
+ table_column_index = column_properties.index(new_sort_property.to_s.to_sym)
170
+ table_column_proxy ||= columns[table_column_index] if table_column_index
171
+ @sort_column = table_column_proxy if table_column_proxy
172
+
173
+ if table_column_proxy
174
+ @sort_by_block = nil
175
+ @sort_block = nil
176
+ end
177
+ @sort_type = nil
178
+ if table_column_proxy&.sort_by_block
179
+ @sort_by_block = table_column_proxy.sort_by_block
180
+ elsif table_column_proxy&.sort_block
181
+ @sort_block = table_column_proxy.sort_block
182
+ else
183
+ detect_sort_type
184
+ end
185
+
186
+ sort!
187
+ end
188
+
189
+ def initial_sort!
190
+ sort_by_column!
191
+ end
192
+
193
+ def sort!
194
+ return unless sort_property && (sort_type || sort_block || sort_by_block)
195
+ array = model_binding.evaluate_property
196
+ array = array.sort_by(&:hash) # this ensures consistent subsequent sorting in case there are equivalent sorts to avoid an infinite loop
197
+ # Converting value to_s first to handle nil cases. Should work with numeric, boolean, and date fields
198
+ if sort_block
199
+ sorted_array = array.sort(&sort_block)
200
+ elsif sort_by_block
201
+ sorted_array = array.sort_by(&sort_by_block)
202
+ else
203
+ sorted_array = array.sort_by do |object|
204
+ sort_property.each_with_index.map do |a_sort_property, i|
205
+ value = object.send(a_sort_property)
206
+ # handle nil and difficult to compare types gracefully
207
+ if sort_type[i] == Integer
208
+ value = value.to_i
209
+ elsif sort_type[i] == Float
210
+ value = value.to_f
211
+ elsif sort_type[i] == String
212
+ value = value.to_s
213
+ end
214
+ value
215
+ end
216
+ end
217
+ end
218
+ sorted_array = sorted_array.reverse if @sort_direction == :descending
219
+ model_binding.call(sorted_array)
220
+ end
221
+
222
+ def additional_sort_properties=(*args)
223
+ @additional_sort_properties = args unless args.empty?
224
+ end
225
+
64
226
  def edit_table_item(table_item, column_index)
65
- table_item.edit(column_index)
227
+ table_item&.edit(column_index) unless column_index.nil?
228
+ end
229
+
230
+ def header_visible=(value)
231
+ @header_visible = value
232
+ if @header_visible
233
+ thead_dom_element.remove_class('hide')
234
+ else
235
+ thead_dom_element.add_class('hide')
236
+ end
237
+ end
238
+
239
+ def header_visible
240
+ @header_visible
66
241
  end
67
242
 
68
243
  def selector
@@ -96,6 +271,9 @@ module Glimmer
96
271
  'on_mouse_up' => {
97
272
  event: 'mouseup',
98
273
  event_handler: mouse_handler,
274
+ },
275
+ 'on_widget_selected' => {
276
+ event: 'mouseup',
99
277
  }
100
278
  }
101
279
  end
@@ -103,6 +281,16 @@ module Glimmer
103
281
  def redraw
104
282
  super()
105
283
  @columns.to_a.each(&:redraw)
284
+ redraw_empty_items
285
+ end
286
+
287
+ def redraw_empty_items
288
+ if @children&.size.to_i < item_count.to_i
289
+ item_count.to_i.times do
290
+ empty_columns = column_properties&.size.to_i.times.map { |i| "<td data-column-index='#{i}'></td>" }
291
+ items_dom_element.append("<tr class='table-item empty-table-item'>#{empty_columns}</tr>")
292
+ end
293
+ end
106
294
  end
107
295
 
108
296
  def element
@@ -136,6 +324,10 @@ module Glimmer
136
324
  }
137
325
  end
138
326
 
327
+ def thead_dom_element
328
+ dom_element.find('thead')
329
+ end
330
+
139
331
  def items_dom
140
332
  tbody {
141
333
  }
@@ -154,6 +346,23 @@ module Glimmer
154
346
  }
155
347
  }.to_s
156
348
  end
349
+
350
+ private
351
+
352
+ def property_type_converters
353
+ super.merge({
354
+ selection: lambda do |value|
355
+ if value.is_a?(Array)
356
+ search {|ti| value.include?(ti.get_data) }
357
+ else
358
+ search {|ti| ti.get_data == value}
359
+ end
360
+ end,
361
+ })
362
+ end
363
+
157
364
  end
365
+
158
366
  end
367
+
159
368
  end