glimmer-dsl-opal 0.6.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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