glimmer-dsl-opal 0.6.0 → 0.7.3

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