glimmer-dsl-opal 0.6.0 → 0.7.3

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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +51 -0
  3. data/README.md +428 -18
  4. data/VERSION +1 -1
  5. data/app/assets/stylesheets/{glimmer.css → glimmer/glimmer.css} +1 -1
  6. data/lib/display.rb +31 -0
  7. data/lib/file.rb +29 -0
  8. data/lib/glimmer-dsl-opal.rb +33 -5
  9. data/lib/glimmer-dsl-opal/ext/date.rb +11 -0
  10. data/lib/glimmer-dsl-opal/ext/struct.rb +37 -0
  11. data/lib/glimmer-dsl-opal/samples/elaborate/tic_tac_toe.rb +23 -0
  12. data/lib/glimmer-dsl-opal/samples/hello/hello_table.rb +283 -0
  13. data/lib/glimmer-dsl-swt.rb +20 -35
  14. data/lib/glimmer/data_binding/table_items_binding.rb +32 -19
  15. data/lib/glimmer/dsl/opal/block_property_expression.rb +41 -0
  16. data/lib/glimmer/dsl/opal/custom_widget_expression.rb +1 -1
  17. data/lib/glimmer/dsl/opal/dsl.rb +2 -0
  18. data/lib/glimmer/dsl/opal/widget_expression.rb +7 -3
  19. data/lib/glimmer/engine.rb +1 -1
  20. data/lib/glimmer/swt/button_proxy.rb +5 -5
  21. data/lib/glimmer/swt/color_proxy.rb +45 -45
  22. data/lib/glimmer/swt/combo_proxy.rb +42 -3
  23. data/lib/glimmer/swt/composite_proxy.rb +7 -3
  24. data/lib/glimmer/swt/control_editor.rb +54 -0
  25. data/lib/glimmer/swt/date_time_proxy.rb +71 -5
  26. data/lib/glimmer/swt/display_proxy.rb +6 -2
  27. data/lib/glimmer/swt/fill_layout_proxy.rb +1 -1
  28. data/lib/glimmer/swt/font_proxy.rb +4 -4
  29. data/lib/glimmer/swt/label_proxy.rb +2 -2
  30. data/lib/glimmer/swt/layout_data_proxy.rb +13 -10
  31. data/lib/glimmer/swt/layout_proxy.rb +5 -5
  32. data/lib/glimmer/swt/list_proxy.rb +2 -2
  33. data/lib/glimmer/swt/message_box_proxy.rb +4 -2
  34. data/lib/glimmer/swt/property_owner.rb +2 -2
  35. data/lib/glimmer/swt/shell_proxy.rb +8 -0
  36. data/lib/glimmer/swt/tab_folder_proxy.rb +2 -2
  37. data/lib/glimmer/swt/tab_item_proxy.rb +7 -7
  38. data/lib/glimmer/swt/table_column_proxy.rb +71 -12
  39. data/lib/glimmer/swt/table_editor.rb +65 -0
  40. data/lib/glimmer/swt/table_item_proxy.rb +50 -7
  41. data/lib/glimmer/swt/table_proxy.rb +581 -14
  42. data/lib/glimmer/swt/text_proxy.rb +49 -1
  43. data/lib/glimmer/swt/widget_proxy.rb +120 -22
  44. data/lib/glimmer/ui/custom_widget.rb +8 -8
  45. data/lib/net/http.rb +1 -5
  46. data/lib/os.rb +36 -0
  47. data/lib/uri.rb +3 -3
  48. metadata +31 -10
  49. data/lib/glimmer/data_binding/ext/observable_model.rb +0 -40
@@ -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
9
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
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,56 @@ 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
+
77
+ # Sets editor (e.g. combo)
78
+ def editor=(*args)
79
+ @editor = args
80
+ end
81
+
82
+ def editable?
83
+ !@editor&.include?(:none)
84
+ end
85
+
34
86
  def observation_request_to_event_mapping
35
87
  {
36
88
  'on_widget_selected' => {
37
- event: 'click'
89
+ event: 'click',
90
+ event_handler: -> (event_listener) {
91
+ -> (event) {
92
+ event_listener.call(event)
93
+ redraw_sort_direction
94
+ }
95
+ }
38
96
  },
39
97
  }
40
- end
98
+ end
41
99
 
42
100
  def dom
43
101
  table_column_text = text
44
102
  table_column_id = id
45
- table_column_id_style = css
103
+ table_column_id_style = "width: #{width}px;"
46
104
  table_column_css_classes = css_classes
47
105
  table_column_css_classes << name
48
106
  @dom ||= html {
49
107
  th(id: table_column_id, style: table_column_id_style, class: table_column_css_classes.to_a.join(' ')) {
50
- table_column_text
108
+ span {table_column_text}
109
+ span(class: sort_icon_class)
51
110
  }
52
111
  }.to_s
53
- end
112
+ end
54
113
  end
55
114
  end
56
115
  end
@@ -0,0 +1,65 @@
1
+ # Copyright (c) 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/swt/control_editor'
23
+
24
+ module Glimmer
25
+ module SWT
26
+ # Emulates SWT's native org.eclipse.swt.custom.TableEditor
27
+ class TableEditor < ControlEditor
28
+ alias table composite
29
+
30
+ def editor=(editor_widget, table_item, table_column_index)
31
+ # TODO consider making editor not gain an ID or gain a separate set of IDs to avoid clashing with standard widget predictability of ID
32
+ @table_item = table_item
33
+ @table_column_index = table_column_index
34
+ @editor_widget = editor_widget
35
+ @old_value = table_item.cell_dom_element(table_column_index).html
36
+ table_item.cell_dom_element(table_column_index).html('')
37
+ editor_widget.render(table_item.cell_dom_element(table_column_index))
38
+ # TODO tweak the width perfectly so it doesn't expand the table cell
39
+ # editor_widget.dom_element.css('width', 'calc(100% - 20px)')
40
+ editor_widget.dom_element.css('width', "#{minimumWidth}%") # TODO implement property with pixels (and perhaps derive percentage separately from pixels)
41
+ editor_widget.dom_element.css('height', "#{minimumHeight}px")
42
+ editor_widget.dom_element.add_class('table-editor')
43
+ # TODO consider relying on autofocus instead
44
+ editor_widget.dom_element.focus
45
+ # TODO consider doing the following line only for :text editor
46
+ editor_widget.dom_element.select
47
+ end
48
+ alias set_editor editor=
49
+ alias setEditor editor=
50
+
51
+ def cancel!
52
+ done!
53
+ end
54
+
55
+ def save!
56
+ done!
57
+ end
58
+
59
+ def done!
60
+ @table_item.cell_dom_element(@table_column_index).html(@old_value) unless @old_value.nil?
61
+ @old_value = nil
62
+ end
63
+ end
64
+ end
65
+ end
@@ -1,12 +1,43 @@
1
+ # Copyright (c) 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
+
1
22
  require 'glimmer/swt/widget_proxy'
2
23
 
3
24
  module Glimmer
4
25
  module SWT
5
26
  class TableItemProxy < WidgetProxy
27
+ STYLE = <<~CSS
28
+ tr.table-item td {
29
+ padding-bottom: 0;
30
+ }
31
+ tr.table-item:nth-child(even):not(.selected) {
32
+ background: rgb(243, 244, 246);
33
+ }
34
+ CSS
35
+
6
36
  attr_reader :data
7
37
 
8
- def initialize(parent, args)
9
- super(parent, args)
38
+ def initialize(parent, args, block)
39
+ super(parent, args, block)
40
+ # TODO check if there is a need to remove this observer when removing widget from table upon items update
10
41
  on_widget_selected { |event|
11
42
  parent.select(parent.index_of(self), event.meta?)
12
43
  }
@@ -53,8 +84,12 @@ module Glimmer
53
84
  'tr'
54
85
  end
55
86
 
87
+ def cell_dom_element(column_index)
88
+ dom_element.find("td:nth-child(#{column_index + 1})")
89
+ end
90
+
56
91
  def redraw
57
- super() #TODO re-enalbe and remove below lines
92
+ super() #TODO re-enable and remove below lines
58
93
 
59
94
  # TODO perhaps turn the following lambdas into methods
60
95
  table_item_edit_handler = lambda do |event, cancel = false|
@@ -70,11 +105,11 @@ module Glimmer
70
105
  redraw
71
106
  end
72
107
  end
73
- table_item_edit_cancel_handler = lambda do |event|
108
+ table_item_edit_cancel_handler = lambda do |event|
74
109
  Async::Task.new do
75
110
  table_item_edit_handler.call(event, true)
76
111
  end
77
- end
112
+ end
78
113
  table_item_edit_key_handler = lambda do |event|
79
114
  Async::Task.new do
80
115
  if event.key_code == 13
@@ -91,7 +126,7 @@ module Glimmer
91
126
  Async::Task.new do
92
127
  table_item_input.focus
93
128
  table_item_input.on('keyup', &table_item_edit_key_handler)
94
- table_item_input.on('focusout', &table_item_edit_cancel_handler)
129
+ table_item_input.on('focusout', &table_item_edit_cancel_handler)
95
130
  end
96
131
  end
97
132
  end
@@ -104,6 +139,14 @@ module Glimmer
104
139
  redraw
105
140
  end
106
141
 
142
+ def redraw_selection
143
+ if parent.selection.include?(self)
144
+ dom_element.add_class('selected')
145
+ else
146
+ dom_element.remove_class('selected')
147
+ end
148
+ end
149
+
107
150
  def on_widget_selected(&block)
108
151
  event = 'click'
109
152
  delegate = $document.on(event, selector, &block)
@@ -132,7 +175,7 @@ module Glimmer
132
175
  tr(id: table_item_id, style: table_item_id_style, class: table_item_css_classes.to_a.join(' ')) {
133
176
  table_item_text_array.each_with_index do |table_item_text, column_index|
134
177
  td('data-column-index' => column_index) {
135
- if @edit_column_index == column_index
178
+ if @edit_column_index == column_index
136
179
  input(type: 'text', value: table_item_text, style: "max-width: #{table_item_max_width - 11}px;")
137
180
  else
138
181
  table_item_text
@@ -1,28 +1,294 @@
1
+ # Copyright (c) 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
+
1
22
  require 'glimmer/swt/widget_proxy'
2
23
  require 'glimmer/swt/table_column_proxy'
24
+ require 'glimmer/swt/table_item_proxy'
25
+ require 'glimmer/swt/table_editor'
3
26
 
4
27
  module Glimmer
5
28
  module SWT
6
- class TableProxy < WidgetProxy
7
- attr_reader :columns, :selection
8
- attr_accessor :column_properties
29
+ class TableProxy < CompositeProxy
30
+ attr_reader :columns, :selection,
31
+ :sort_type, :sort_column, :sort_property, :sort_block, :sort_by_block, :additional_sort_properties,
32
+ :editor, :table_editor
33
+ attr_accessor :column_properties, :item_count, :data
9
34
  alias items children
35
+ alias model_binding data
10
36
 
11
- def initialize(parent, args)
12
- super(parent, args)
37
+ class << self
38
+ include Glimmer
39
+
40
+ def editors
41
+ @editors ||= {
42
+ # ensure editor can work with string keys not just symbols (leave one string in for testing)
43
+ text: {
44
+ widget_value_property: :text,
45
+ editor_gui: lambda do |args, model, property, table_proxy|
46
+ table_proxy.table_editor.minimumWidth = 90
47
+ table_proxy.table_editor.minimumHeight = 10
48
+ table_editor_widget_proxy = text(*args) {
49
+ text model.send(property)
50
+ focus true
51
+ on_focus_lost {
52
+ table_proxy.finish_edit!
53
+ }
54
+ on_key_pressed { |key_event|
55
+ if key_event.keyCode == swt(:cr)
56
+ table_proxy.finish_edit!
57
+ elsif key_event.keyCode == swt(:esc)
58
+ table_proxy.cancel_edit!
59
+ end
60
+ }
61
+ }
62
+ # table_editor_widget_proxy.swt_widget.selectAll # TODO select all
63
+ table_editor_widget_proxy
64
+ end,
65
+ },
66
+ combo: {
67
+ widget_value_property: :text,
68
+ editor_gui: lambda do |args, model, property, table_proxy|
69
+ first_time = true
70
+ table_proxy.table_editor.minimumWidth = 90
71
+ table_proxy.table_editor.minimumHeight = 18
72
+ table_editor_widget_proxy = combo(*args) {
73
+ items model.send("#{property}_options")
74
+ text model.send(property)
75
+ focus true
76
+ on_focus_lost {
77
+ table_proxy.finish_edit!
78
+ }
79
+ on_key_pressed { |key_event|
80
+ if key_event.keyCode == swt(:cr)
81
+ table_proxy.finish_edit!
82
+ elsif key_event.keyCode == swt(:esc)
83
+ table_proxy.cancel_edit!
84
+ end
85
+ }
86
+ on_widget_selected {
87
+ if !OS.windows? || !first_time || first_time && model.send(property) != table_editor_widget_proxy.text
88
+ table_proxy.finish_edit!
89
+ end
90
+ }
91
+ }
92
+ table_editor_widget_proxy
93
+ end,
94
+ },
95
+ checkbox: {
96
+ widget_value_property: :selection,
97
+ editor_gui: lambda do |args, model, property, table_proxy|
98
+ first_time = true
99
+ table_proxy.table_editor.minimumHeight = 25
100
+ checkbox(*args) {
101
+ selection model.send(property)
102
+ focus true
103
+ on_widget_selected {
104
+ table_proxy.finish_edit!
105
+ }
106
+ on_focus_lost {
107
+ table_proxy.finish_edit!
108
+ }
109
+ on_key_pressed { |key_event|
110
+ if key_event.keyCode == swt(:cr)
111
+ table_proxy.finish_edit!
112
+ elsif key_event.keyCode == swt(:esc)
113
+ table_proxy.cancel_edit!
114
+ end
115
+ }
116
+ }
117
+ end,
118
+ },
119
+ date: {
120
+ widget_value_property: :date_time,
121
+ editor_gui: lambda do |args, model, property, table_proxy|
122
+ first_time = true
123
+ table_proxy.table_editor.minimumWidth = 90
124
+ table_proxy.table_editor.minimumHeight = 15
125
+ date(*args) {
126
+ date_time model.send(property)
127
+ focus true
128
+ on_widget_selected {
129
+ table_proxy.finish_edit!
130
+ }
131
+ on_key_pressed { |key_event|
132
+ if key_event.keyCode == swt(:cr)
133
+ table_proxy.finish_edit!
134
+ elsif key_event.keyCode == swt(:esc)
135
+ table_proxy.cancel_edit!
136
+ end
137
+ }
138
+ }
139
+ end,
140
+ },
141
+ date_drop_down: {
142
+ widget_value_property: :date_time,
143
+ editor_gui: lambda do |args, model, property, table_proxy|
144
+ first_time = true
145
+ table_proxy.table_editor.minimumWidth = 80
146
+ table_proxy.table_editor.minimumHeight = 15
147
+ date_drop_down(*args) {
148
+ date_time model.send(property)
149
+ focus true
150
+ on_widget_selected {
151
+ table_proxy.finish_edit!
152
+ }
153
+ on_key_pressed { |key_event|
154
+ if key_event.keyCode == swt(:cr)
155
+ table_proxy.finish_edit!
156
+ elsif key_event.keyCode == swt(:esc)
157
+ table_proxy.cancel_edit!
158
+ end
159
+ }
160
+ }
161
+ end,
162
+ },
163
+ time: {
164
+ widget_value_property: :date_time,
165
+ editor_gui: lambda do |args, model, property, table_proxy|
166
+ first_time = true
167
+ table_proxy.table_editor.minimumWidth = 80
168
+ table_proxy.table_editor.minimumHeight = 15
169
+ time(*args) {
170
+ date_time model.send(property)
171
+ focus true
172
+ on_widget_selected {
173
+ table_proxy.finish_edit!
174
+ }
175
+ on_focus_lost {
176
+ table_proxy.finish_edit!
177
+ }
178
+ on_key_pressed { |key_event|
179
+ if key_event.keyCode == swt(:cr)
180
+ table_proxy.finish_edit!
181
+ elsif key_event.keyCode == swt(:esc)
182
+ table_proxy.cancel_edit!
183
+ end
184
+ }
185
+ }
186
+ end,
187
+ },
188
+ radio: {
189
+ widget_value_property: :selection,
190
+ editor_gui: lambda do |args, model, property, table_proxy|
191
+ first_time = true
192
+ table_proxy.table_editor.minimumHeight = 25
193
+ radio(*args) {
194
+ selection model.send(property)
195
+ focus true
196
+ on_widget_selected {
197
+ table_proxy.finish_edit!
198
+ }
199
+ on_focus_lost {
200
+ table_proxy.finish_edit!
201
+ }
202
+ on_key_pressed { |key_event|
203
+ if key_event.keyCode == swt(:cr)
204
+ table_proxy.finish_edit!
205
+ elsif key_event.keyCode == swt(:esc)
206
+ table_proxy.cancel_edit!
207
+ end
208
+ }
209
+ }
210
+ end,
211
+ },
212
+ spinner: {
213
+ widget_value_property: :selection,
214
+ editor_gui: lambda do |args, model, property, table_proxy|
215
+ first_time = true
216
+ table_proxy.table_editor.minimumHeight = 25
217
+ table_editor_widget_proxy = spinner(*args) {
218
+ selection model.send(property)
219
+ focus true
220
+ on_focus_lost {
221
+ table_proxy.finish_edit!
222
+ }
223
+ on_key_pressed { |key_event|
224
+ if key_event.keyCode == swt(:cr)
225
+ table_proxy.finish_edit!
226
+ elsif key_event.keyCode == swt(:esc)
227
+ table_proxy.cancel_edit!
228
+ end
229
+ }
230
+ }
231
+ table_editor_widget_proxy
232
+ end,
233
+ },
234
+ }
235
+ end
236
+ end
237
+
238
+ def initialize(parent, args, block)
239
+ super(parent, args, block)
13
240
  @columns = []
14
241
  @children = []
242
+ @editors = []
15
243
  @selection = []
244
+ @table_editor = TableEditor.new(self)
245
+ @table_editor.horizontalAlignment = SWTProxy[:left]
246
+ @table_editor.grabHorizontal = true
247
+ @table_editor.minimumWidth = 90
248
+ @table_editor.minimumHeight = 20
249
+ if editable?
250
+ on_mouse_up { |event|
251
+ edit_table_item(event.table_item, event.column_index)
252
+ }
253
+ end
16
254
  end
17
255
 
18
256
  # Only table_columns may be added as children
19
257
  def post_initialize_child(child)
20
258
  if child.is_a?(TableColumnProxy)
21
259
  @columns << child
22
- else
260
+ child.render
261
+ elsif child.is_a?(TableItemProxy)
23
262
  @children << child
263
+ child.render
264
+ else
265
+ @editors << child
266
+ end
267
+ end
268
+
269
+ # Executes for the parent of a child that just got disposed
270
+ def post_dispose_child(child)
271
+ if child.is_a?(TableColumnProxy)
272
+ @columns&.delete(child)
273
+ elsif child.is_a?(TableItemProxy)
274
+ @children&.delete(child)
275
+ else
276
+ @editors&.delete(child)
24
277
  end
25
- child.redraw
278
+ end
279
+
280
+ def post_add_content
281
+ return if @initially_sorted
282
+ initial_sort!
283
+ @initially_sorted = true
284
+ end
285
+
286
+ def default_layout
287
+ nil
288
+ end
289
+
290
+ def get_data(key=nil)
291
+ data
26
292
  end
27
293
 
28
294
  def remove_all
@@ -30,17 +296,37 @@ module Glimmer
30
296
  redraw
31
297
  end
32
298
 
299
+ def editable?
300
+ args.include?(:editable)
301
+ end
302
+ alias editable editable?
303
+
304
+ def selection
305
+ @selection.to_a
306
+ end
307
+
33
308
  def selection=(new_selection)
34
- changed = (@selection + new_selection) - (@selection & new_selection)
309
+ new_selection = new_selection.to_a
310
+ changed = (selection + new_selection) - (selection & new_selection)
35
311
  @selection = new_selection
36
- changed.each(&:redraw)
312
+ changed.each(&:redraw_selection)
37
313
  end
38
314
 
39
315
  def items=(new_items)
40
316
  @children = new_items
317
+ # TODO optimize in the future by sorting elements in DOM directly when no change to elements occur other than sort
41
318
  redraw
42
319
  end
43
320
 
321
+ def item_count=(value)
322
+ @item_count = value
323
+ redraw_empty_items
324
+ end
325
+
326
+ def cells_for(model)
327
+ column_properties.map {|property| model.send(property)}
328
+ end
329
+
44
330
  def search(&condition)
45
331
  items.select {|item| condition.nil? || condition.call(item)}
46
332
  end
@@ -61,8 +347,252 @@ module Glimmer
61
347
  self.selection = new_selection
62
348
  end
63
349
 
64
- def edit_table_item(table_item, column_index)
65
- table_item.edit(column_index)
350
+ def sort_block=(comparator)
351
+ @sort_block = comparator
352
+ end
353
+
354
+ def sort_by_block=(property_picker)
355
+ @sort_by_block = property_picker
356
+ end
357
+
358
+ def sort_property=(new_sort_property)
359
+ @sort_property = new_sort_property.to_collection
360
+ end
361
+
362
+ def detect_sort_type
363
+ @sort_type = sort_property.size.times.map { String }
364
+ array = model_binding.evaluate_property
365
+ sort_property.each_with_index do |a_sort_property, i|
366
+ values = array.map { |object| object.send(a_sort_property) }
367
+ value_classes = values.map(&:class).uniq
368
+ if value_classes.size == 1
369
+ @sort_type[i] = value_classes.first
370
+ elsif value_classes.include?(Integer)
371
+ @sort_type[i] = Integer
372
+ elsif value_classes.include?(Float)
373
+ @sort_type[i] = Float
374
+ end
375
+ end
376
+ end
377
+
378
+ def column_sort_properties
379
+ column_properties.zip(columns.map(&:sort_property)).map do |pair|
380
+ pair.compact.last.to_collection
381
+ end
382
+ end
383
+
384
+ def sort_direction
385
+ @sort_direction == :ascending ? SWTProxy[:up] : SWTProxy[:down]
386
+ end
387
+
388
+ def sort_direction=(value)
389
+ @sort_direction = value == SWTProxy[:up] ? :ascending : :descending
390
+ end
391
+
392
+ # Sorts by specified TableColumnProxy object. If nil, it uses the table default sort instead.
393
+ def sort_by_column!(table_column_proxy=nil)
394
+ index = columns.to_a.index(table_column_proxy) unless table_column_proxy.nil?
395
+ new_sort_property = table_column_proxy.nil? ? @sort_property : table_column_proxy.sort_property || [column_properties[index]]
396
+
397
+ return if table_column_proxy.nil? && new_sort_property.nil? && @sort_block.nil? && @sort_by_block.nil?
398
+ if new_sort_property && table_column_proxy.nil? && new_sort_property.size == 1 && (index = column_sort_properties.index(new_sort_property))
399
+ table_column_proxy = columns[index]
400
+ end
401
+ if new_sort_property && new_sort_property.size == 1 && !additional_sort_properties.to_a.empty?
402
+ selected_additional_sort_properties = additional_sort_properties.clone
403
+ if selected_additional_sort_properties.include?(new_sort_property.first)
404
+ selected_additional_sort_properties.delete(new_sort_property.first)
405
+ new_sort_property += selected_additional_sort_properties
406
+ else
407
+ new_sort_property += additional_sort_properties
408
+ end
409
+ end
410
+
411
+ new_sort_property = new_sort_property.to_collection unless new_sort_property.is_a?(Array)
412
+ @sort_direction = @sort_direction.nil? || @sort_property.first != new_sort_property.first || @sort_direction == :descending ? :ascending : :descending
413
+
414
+ @sort_property = new_sort_property
415
+ table_column_index = column_properties.index(new_sort_property.to_s.to_sym)
416
+ table_column_proxy ||= columns[table_column_index] if table_column_index
417
+ @sort_column = table_column_proxy if table_column_proxy
418
+
419
+ if table_column_proxy
420
+ @sort_by_block = nil
421
+ @sort_block = nil
422
+ end
423
+ @sort_type = nil
424
+ if table_column_proxy&.sort_by_block
425
+ @sort_by_block = table_column_proxy.sort_by_block
426
+ elsif table_column_proxy&.sort_block
427
+ @sort_block = table_column_proxy.sort_block
428
+ else
429
+ detect_sort_type
430
+ end
431
+
432
+ sort!
433
+ end
434
+
435
+ def initial_sort!
436
+ sort_by_column!
437
+ end
438
+
439
+ def sort!
440
+ return unless sort_property && (sort_type || sort_block || sort_by_block)
441
+ array = model_binding.evaluate_property
442
+ array = array.sort_by(&:hash) # this ensures consistent subsequent sorting in case there are equivalent sorts to avoid an infinite loop
443
+ # Converting value to_s first to handle nil cases. Should work with numeric, boolean, and date fields
444
+ if sort_block
445
+ sorted_array = array.sort(&sort_block)
446
+ elsif sort_by_block
447
+ sorted_array = array.sort_by(&sort_by_block)
448
+ else
449
+ sorted_array = array.sort_by do |object|
450
+ sort_property.each_with_index.map do |a_sort_property, i|
451
+ value = object.send(a_sort_property)
452
+ # handle nil and difficult to compare types gracefully
453
+ if sort_type[i] == Integer
454
+ value = value.to_i
455
+ elsif sort_type[i] == Float
456
+ value = value.to_f
457
+ elsif sort_type[i] == String
458
+ value = value.to_s
459
+ end
460
+ value
461
+ end
462
+ end
463
+ end
464
+ sorted_array = sorted_array.reverse if @sort_direction == :descending
465
+ model_binding.call(sorted_array)
466
+ end
467
+
468
+ def additional_sort_properties=(*args)
469
+ @additional_sort_properties = args unless args.empty?
470
+ end
471
+
472
+ def editor=(args)
473
+ @editor = args
474
+ end
475
+
476
+ # Indicates if table is in edit mode, thus displaying a text widget for a table item cell
477
+ def edit_mode?
478
+ !!@edit_mode
479
+ end
480
+
481
+ def cancel_edit!
482
+ @cancel_edit&.call if @edit_mode
483
+ end
484
+
485
+ def finish_edit!
486
+ @finish_edit&.call if @edit_mode
487
+ end
488
+
489
+ # 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.
490
+ # It is set to false once change is saved to model
491
+ def edit_in_progress?
492
+ !!@edit_in_progress
493
+ end
494
+
495
+ def edit_selected_table_item(column_index, before_write: nil, after_write: nil, after_cancel: nil)
496
+ edit_table_item(selection.first, column_index, before_write: before_write, after_write: after_write, after_cancel: after_cancel)
497
+ end
498
+
499
+ # TODO migrate the following to the next method
500
+ # def edit_table_item(table_item, column_index)
501
+ # table_item&.edit(column_index) unless column_index.nil?
502
+ # end
503
+
504
+ def edit_table_item(table_item, column_index, before_write: nil, after_write: nil, after_cancel: nil)
505
+ return if table_item.nil? || (@edit_mode && @edit_table_item == table_item && @edit_column_index == column_index)
506
+ @edit_column_index = column_index
507
+ @edit_table_item = table_item
508
+ column_index = column_index.to_i
509
+ model = table_item.data
510
+ property = column_properties[column_index]
511
+ cancel_edit!
512
+ return unless columns[column_index].editable?
513
+ action_taken = false
514
+ @edit_mode = true
515
+
516
+ editor_config = columns[column_index].editor || editor
517
+ editor_config = editor_config.to_collection
518
+ editor_widget_options = editor_config.last.is_a?(Hash) ? editor_config.last : {}
519
+ editor_widget_arg_last_index = editor_config.last.is_a?(Hash) ? -2 : -1
520
+ editor_widget = (editor_config[0] || :text).to_sym
521
+ editor_widget_args = editor_config[1..editor_widget_arg_last_index]
522
+ model_editing_property = editor_widget_options[:property] || property
523
+ widget_value_property = TableProxy::editors.symbolize_keys[editor_widget][:widget_value_property]
524
+
525
+ @cancel_edit = lambda do |event=nil|
526
+ @cancel_in_progress = true
527
+ @table_editor.cancel!
528
+ @table_editor_widget_proxy&.dispose
529
+ @table_editor_widget_proxy = nil
530
+ after_cancel&.call
531
+ @edit_in_progress = false
532
+ @cancel_in_progress = false
533
+ @cancel_edit = nil
534
+ @edit_table_item = @edit_column_index = nil if (@edit_mode && @edit_table_item == table_item && @edit_column_index == column_index)
535
+ @edit_mode = false
536
+ end
537
+
538
+ @finish_edit = lambda do |event=nil|
539
+ new_value = @table_editor_widget_proxy&.send(widget_value_property)
540
+ if table_item.disposed?
541
+ @cancel_edit.call
542
+ elsif !new_value.nil? && !action_taken && !@edit_in_progress && !@cancel_in_progress
543
+ action_taken = true
544
+ @edit_in_progress = true
545
+ if new_value == model.send(model_editing_property)
546
+ @cancel_edit.call
547
+ else
548
+ before_write&.call
549
+ @table_editor.save!(widget_value_property: widget_value_property)
550
+ model.send("#{model_editing_property}=", new_value) # makes table update itself, so must search for selected table item again
551
+ # Table refresh happens here because of model update triggering observers, so must retrieve table item again
552
+ edited_table_item = search { |ti| ti.data == model }.first
553
+ show_item(edited_table_item)
554
+ @table_editor_widget_proxy&.dispose
555
+ @table_editor_widget_proxy = nil
556
+ after_write&.call(edited_table_item)
557
+ @edit_in_progress = false
558
+ @edit_table_item = @edit_column_index = nil
559
+ end
560
+ end
561
+ end
562
+
563
+ content {
564
+ @table_editor_widget_proxy = TableProxy::editors.symbolize_keys[editor_widget][:editor_gui].call(editor_widget_args, model, model_editing_property, self)
565
+ }
566
+ @table_editor.set_editor(@table_editor_widget_proxy, table_item, column_index)
567
+ rescue => e
568
+ Glimmer::Config.logger.error {e.full_message}
569
+ raise e
570
+ end
571
+
572
+ def show_item(table_item)
573
+ table_item.dom_element.focus
574
+ end
575
+
576
+ def add_listener(underscored_listener_name, &block)
577
+ enhanced_block = lambda do |event|
578
+ event.extend(TableListenerEvent)
579
+ block.call(event)
580
+ end
581
+ super(underscored_listener_name, &enhanced_block)
582
+ end
583
+
584
+
585
+ def header_visible=(value)
586
+ @header_visible = value
587
+ if @header_visible
588
+ thead_dom_element.remove_class('hide')
589
+ else
590
+ thead_dom_element.add_class('hide')
591
+ end
592
+ end
593
+
594
+ def header_visible
595
+ @header_visible
66
596
  end
67
597
 
68
598
  def selector
@@ -84,7 +614,8 @@ module Glimmer
84
614
  event.singleton_class.send(:define_method, :column_index) do
85
615
  (table_data || event.target).attr('data-column-index')
86
616
  end
87
- event_listener.call(event)
617
+
618
+ event_listener.call(event) unless event.table_item.nil? && event.column_index.nil?
88
619
  }
89
620
  }
90
621
 
@@ -96,13 +627,27 @@ module Glimmer
96
627
  'on_mouse_up' => {
97
628
  event: 'mouseup',
98
629
  event_handler: mouse_handler,
99
- }
630
+ },
631
+ 'on_widget_selected' => {
632
+ event: 'mouseup',
633
+ event_handler: mouse_handler,
634
+ },
100
635
  }
101
636
  end
102
637
 
103
638
  def redraw
104
639
  super()
105
640
  @columns.to_a.each(&:redraw)
641
+ redraw_empty_items
642
+ end
643
+
644
+ def redraw_empty_items
645
+ if @children&.size.to_i < item_count.to_i
646
+ item_count.to_i.times do
647
+ empty_columns = column_properties&.size.to_i.times.map { |i| "<td data-column-index='#{i}'></td>" }
648
+ items_dom_element.append("<tr class='table-item empty-table-item'>#{empty_columns}</tr>")
649
+ end
650
+ end
106
651
  end
107
652
 
108
653
  def element
@@ -136,6 +681,10 @@ module Glimmer
136
681
  }
137
682
  end
138
683
 
684
+ def thead_dom_element
685
+ dom_element.find('thead')
686
+ end
687
+
139
688
  def items_dom
140
689
  tbody {
141
690
  }
@@ -145,7 +694,8 @@ module Glimmer
145
694
  table_id = id
146
695
  table_id_style = css
147
696
  table_id_css_classes = css_classes
148
- table_id_css_classes << 'table'
697
+ table_id_css_classes << 'table' unless table_id_css_classes.include?('table')
698
+ table_id_css_classes << 'editable' if editable? && !table_id_css_classes.include?('editable')
149
699
  table_id_css_classes_string = table_id_css_classes.to_a.join(' ')
150
700
  @dom ||= html {
151
701
  table(id: table_id, style: table_id_style, class: table_id_css_classes_string) {
@@ -154,6 +704,23 @@ module Glimmer
154
704
  }
155
705
  }.to_s
156
706
  end
707
+
708
+ private
709
+
710
+ def property_type_converters
711
+ super.merge({
712
+ selection: lambda do |value|
713
+ if value.is_a?(Array)
714
+ search {|ti| value.include?(ti.get_data) }
715
+ else
716
+ search {|ti| ti.get_data == value}
717
+ end
718
+ end,
719
+ })
720
+ end
721
+
157
722
  end
723
+
158
724
  end
725
+
159
726
  end