glimmer-dsl-opal 0.5.0 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -0
  3. data/README.md +390 -29
  4. data/VERSION +1 -1
  5. data/app/assets/images/glimmer/images/calendar.gif +0 -0
  6. data/app/assets/images/glimmer/images/ui-icons_222222_256x240.png +0 -0
  7. data/app/assets/images/glimmer/images/ui-icons_444444_256x240.png +0 -0
  8. data/app/assets/images/glimmer/images/ui-icons_555555_256x240.png +0 -0
  9. data/app/assets/images/glimmer/images/ui-icons_777620_256x240.png +0 -0
  10. data/app/assets/images/glimmer/images/ui-icons_777777_256x240.png +0 -0
  11. data/app/assets/images/glimmer/images/ui-icons_cc0000_256x240.png +0 -0
  12. data/app/assets/images/glimmer/images/ui-icons_ffffff_256x240.png +0 -0
  13. data/app/assets/stylesheets/glimmer/glimmer.css +15 -0
  14. data/app/assets/stylesheets/glimmer/jquery-ui.css +1312 -0
  15. data/app/assets/stylesheets/glimmer/jquery-ui.structure.css +886 -0
  16. data/app/assets/stylesheets/glimmer/jquery-ui.theme.css +443 -0
  17. data/app/assets/stylesheets/glimmer/jquery.ui.timepicker.css +57 -0
  18. data/lib/glimmer-dsl-opal.rb +73 -8
  19. data/lib/glimmer-dsl-opal/ext/date.rb +49 -3
  20. data/lib/glimmer-dsl-opal/samples/elaborate/tic_tac_toe.rb +23 -0
  21. data/lib/glimmer-dsl-opal/samples/hello/hello_combo.rb +5 -5
  22. data/lib/glimmer-dsl-opal/samples/hello/hello_date_time.rb +63 -0
  23. data/lib/glimmer-dsl-opal/samples/hello/hello_list_single_selection.rb +1 -1
  24. data/lib/glimmer-dsl-opal/samples/hello/hello_table.rb +283 -0
  25. data/lib/glimmer-dsl-opal/vendor/jquery-ui-timepicker/GPL-LICENSE.txt +278 -0
  26. data/lib/glimmer-dsl-opal/vendor/jquery-ui-timepicker/MIT-LICENSE.txt +20 -0
  27. data/lib/glimmer-dsl-opal/vendor/jquery-ui-timepicker/jquery.ui.timepicker.css +57 -0
  28. data/lib/glimmer-dsl-opal/vendor/jquery-ui-timepicker/jquery.ui.timepicker.js +1496 -0
  29. data/lib/glimmer-dsl-opal/vendor/jquery-ui/AUTHORS.txt +333 -0
  30. data/lib/glimmer-dsl-opal/vendor/jquery-ui/LICENSE.txt +43 -0
  31. data/lib/glimmer-dsl-opal/vendor/jquery-ui/images/ui-icons_444444_256x240.png +0 -0
  32. data/lib/glimmer-dsl-opal/vendor/jquery-ui/images/ui-icons_555555_256x240.png +0 -0
  33. data/lib/glimmer-dsl-opal/vendor/jquery-ui/images/ui-icons_777620_256x240.png +0 -0
  34. data/lib/glimmer-dsl-opal/vendor/jquery-ui/images/ui-icons_777777_256x240.png +0 -0
  35. data/lib/glimmer-dsl-opal/vendor/jquery-ui/images/ui-icons_cc0000_256x240.png +0 -0
  36. data/lib/glimmer-dsl-opal/vendor/jquery-ui/images/ui-icons_ffffff_256x240.png +0 -0
  37. data/lib/glimmer-dsl-opal/vendor/jquery-ui/jquery-ui.min.css +7 -0
  38. data/lib/glimmer-dsl-opal/vendor/jquery-ui/jquery-ui.min.js +13 -0
  39. data/lib/glimmer-dsl-opal/vendor/jquery-ui/jquery-ui.structure.min.css +5 -0
  40. data/lib/glimmer-dsl-opal/vendor/jquery-ui/jquery-ui.theme.min.css +5 -0
  41. data/lib/glimmer-dsl-opal/vendor/jquery-ui/package.json +74 -0
  42. data/lib/glimmer-dsl-swt.rb +20 -35
  43. data/lib/glimmer/data_binding/table_items_binding.rb +31 -18
  44. data/lib/glimmer/dsl/opal/block_property_expression.rb +41 -0
  45. data/lib/glimmer/dsl/opal/custom_widget_expression.rb +1 -1
  46. data/lib/glimmer/dsl/opal/dsl.rb +2 -0
  47. data/lib/glimmer/dsl/opal/shell_expression.rb +7 -2
  48. data/lib/glimmer/dsl/opal/widget_expression.rb +10 -1
  49. data/lib/glimmer/engine.rb +9 -0
  50. data/lib/glimmer/swt.rb +3 -3
  51. data/lib/glimmer/swt/button_proxy.rb +5 -5
  52. data/lib/glimmer/swt/checkbox_proxy.rb +1 -0
  53. data/lib/glimmer/swt/color_proxy.rb +45 -45
  54. data/lib/glimmer/swt/combo_proxy.rb +2 -2
  55. data/lib/glimmer/swt/composite_proxy.rb +7 -3
  56. data/lib/glimmer/swt/control_editor.rb +53 -0
  57. data/lib/glimmer/swt/date_time_proxy.rb +145 -0
  58. data/lib/glimmer/swt/display_proxy.rb +6 -2
  59. data/lib/glimmer/swt/fill_layout_proxy.rb +1 -1
  60. data/lib/glimmer/swt/label_proxy.rb +2 -2
  61. data/lib/glimmer/swt/layout_data_proxy.rb +8 -8
  62. data/lib/glimmer/swt/layout_proxy.rb +5 -5
  63. data/lib/glimmer/swt/list_proxy.rb +2 -2
  64. data/lib/glimmer/swt/make_shift_shell_proxy.rb +4 -4
  65. data/lib/glimmer/swt/message_box_proxy.rb +10 -8
  66. data/lib/glimmer/swt/property_owner.rb +2 -2
  67. data/lib/glimmer/swt/radio_proxy.rb +1 -0
  68. data/lib/glimmer/swt/row_layout_proxy.rb +1 -1
  69. data/lib/glimmer/swt/shell_proxy.rb +8 -0
  70. data/lib/glimmer/swt/tab_folder_proxy.rb +5 -5
  71. data/lib/glimmer/swt/tab_item_proxy.rb +7 -7
  72. data/lib/glimmer/swt/table_column_proxy.rb +71 -12
  73. data/lib/glimmer/swt/table_editor.rb +65 -0
  74. data/lib/glimmer/swt/table_item_proxy.rb +42 -7
  75. data/lib/glimmer/swt/table_proxy.rb +575 -20
  76. data/lib/glimmer/swt/text_proxy.rb +50 -2
  77. data/lib/glimmer/swt/widget_proxy.rb +120 -23
  78. data/lib/glimmer/ui/custom_widget.rb +8 -8
  79. data/lib/net/http.rb +1 -5
  80. data/lib/uri.rb +3 -3
  81. metadata +45 -9
  82. data/lib/glimmer/data_binding/ext/observable_model.rb +0 -40
@@ -3,6 +3,7 @@ require 'glimmer/swt/widget_proxy'
3
3
  module Glimmer
4
4
  module SWT
5
5
  class RadioProxy < WidgetProxy
6
+ # TODO add a create method that ensures passing :radio style in if not there
6
7
  STYLE=<<~CSS
7
8
  .radio {
8
9
  display: inline;
@@ -12,7 +12,7 @@ module Glimmer
12
12
  }
13
13
 
14
14
  .row-layout-pack-false {
15
- display: initial;
15
+ align-items: stretch;
16
16
  }
17
17
 
18
18
  .row-layout-horizontal {
@@ -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
@@ -2,17 +2,17 @@ require 'glimmer/swt/widget_proxy'
2
2
 
3
3
  module Glimmer
4
4
  module SWT
5
- class TabFolderProxy < WidgetProxy
5
+ class TabFolderProxy < WidgetProxy
6
6
  attr_reader :tabs
7
7
 
8
- def initialize(parent, args)
9
- super(parent, args)
8
+ def initialize(parent, args, block)
9
+ super(parent, args, block)
10
10
  @tabs = []
11
11
  end
12
12
 
13
- def add_child(child)
13
+ def post_initialize_child(child)
14
14
  unless @children.include?(child)
15
- @children << child
15
+ @children << child
16
16
  tabs_dom_element.append(child.tab_dom)
17
17
  child.render
18
18
  end
@@ -4,10 +4,10 @@ module Glimmer
4
4
  module SWT
5
5
  class TabItemProxy < CompositeProxy
6
6
  include Glimmer
7
- attr_reader :text, :content_visible
7
+ attr_reader :text, :content_visible
8
8
 
9
- def initialize(parent, args)
10
- super(parent, args)
9
+ def initialize(parent, args, block)
10
+ super(parent, args, block)
11
11
  content {
12
12
  on_widget_selected {
13
13
  @parent.hide_all_tab_content
@@ -35,7 +35,7 @@ module Glimmer
35
35
 
36
36
  def selector
37
37
  super + '-tab'
38
- end
38
+ end
39
39
 
40
40
  def observation_request_to_event_mapping
41
41
  {
@@ -47,7 +47,7 @@ module Glimmer
47
47
 
48
48
  def listener_path
49
49
  tab_path
50
- end
50
+ end
51
51
 
52
52
  def tab_path
53
53
  "#{parent.tabs_path} > ##{tab_id}"
@@ -72,13 +72,13 @@ module Glimmer
72
72
 
73
73
  # This contains the tab content
74
74
  def dom
75
- tab_item_id = id
75
+ tab_item_id = id
76
76
  tab_item_class_string = [name, 'hide'].join(' ')
77
77
  @dom ||= html {
78
78
  div(id: tab_item_id, class: tab_item_class_string) {
79
79
  }
80
80
  }.to_s
81
- end
81
+ end
82
82
  end
83
83
  end
84
84
  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
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', '90%') # just a good enough approximation
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
@@ -132,7 +167,7 @@ module Glimmer
132
167
  tr(id: table_item_id, style: table_item_id_style, class: table_item_css_classes.to_a.join(' ')) {
133
168
  table_item_text_array.each_with_index do |table_item_text, column_index|
134
169
  td('data-column-index' => column_index) {
135
- if @edit_column_index == column_index
170
+ if @edit_column_index == column_index
136
171
  input(type: 'text', value: table_item_text, style: "max-width: #{table_item_max_width - 11}px;")
137
172
  else
138
173
  table_item_text
@@ -1,28 +1,285 @@
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.minimumHeight = 10
47
+ table_editor_widget_proxy = text(*args) {
48
+ text model.send(property)
49
+ focus true
50
+ on_focus_lost {
51
+ table_proxy.finish_edit!
52
+ }
53
+ on_key_pressed { |key_event|
54
+ if key_event.keyCode == swt(:cr)
55
+ table_proxy.finish_edit!
56
+ elsif key_event.keyCode == swt(:esc)
57
+ table_proxy.cancel_edit!
58
+ end
59
+ }
60
+ }
61
+ # table_editor_widget_proxy.swt_widget.selectAll # TODO select all
62
+ table_editor_widget_proxy
63
+ end,
64
+ },
65
+ combo: {
66
+ widget_value_property: :text,
67
+ editor_gui: lambda do |args, model, property, table_proxy|
68
+ first_time = true
69
+ table_proxy.table_editor.minimumHeight = 18
70
+ table_editor_widget_proxy = combo(*args) {
71
+ items model.send("#{property}_options")
72
+ text model.send(property)
73
+ focus true
74
+ on_focus_lost {
75
+ table_proxy.finish_edit!
76
+ }
77
+ on_key_pressed { |key_event|
78
+ if key_event.keyCode == swt(:cr)
79
+ table_proxy.finish_edit!
80
+ elsif key_event.keyCode == swt(:esc)
81
+ table_proxy.cancel_edit!
82
+ end
83
+ }
84
+ on_widget_selected {
85
+ if !OS.windows? || !first_time || first_time && model.send(property) != table_editor_widget_proxy.text
86
+ table_proxy.finish_edit!
87
+ end
88
+ }
89
+ }
90
+ table_editor_widget_proxy
91
+ end,
92
+ },
93
+ checkbox: {
94
+ widget_value_property: :selection,
95
+ editor_gui: lambda do |args, model, property, table_proxy|
96
+ first_time = true
97
+ table_proxy.table_editor.minimumHeight = 25
98
+ checkbox(*args) {
99
+ selection model.send(property)
100
+ focus true
101
+ on_widget_selected {
102
+ table_proxy.finish_edit!
103
+ }
104
+ on_focus_lost {
105
+ table_proxy.finish_edit!
106
+ }
107
+ on_key_pressed { |key_event|
108
+ if key_event.keyCode == swt(:cr)
109
+ table_proxy.finish_edit!
110
+ elsif key_event.keyCode == swt(:esc)
111
+ table_proxy.cancel_edit!
112
+ end
113
+ }
114
+ }
115
+ end,
116
+ },
117
+ date: {
118
+ widget_value_property: :date_time,
119
+ editor_gui: lambda do |args, model, property, table_proxy|
120
+ first_time = true
121
+ table_proxy.table_editor.minimumHeight = 25
122
+ date(*args) {
123
+ date_time model.send(property)
124
+ focus true
125
+ on_focus_lost {
126
+ table_proxy.finish_edit!
127
+ }
128
+ on_key_pressed { |key_event|
129
+ if key_event.keyCode == swt(:cr)
130
+ table_proxy.finish_edit!
131
+ elsif key_event.keyCode == swt(:esc)
132
+ table_proxy.cancel_edit!
133
+ end
134
+ }
135
+ }
136
+ end,
137
+ },
138
+ date_drop_down: {
139
+ widget_value_property: :date_time,
140
+ editor_gui: lambda do |args, model, property, table_proxy|
141
+ first_time = true
142
+ table_proxy.table_editor.minimumHeight = 25
143
+ date_drop_down(*args) {
144
+ date_time model.send(property)
145
+ focus true
146
+ on_focus_lost {
147
+ table_proxy.finish_edit!
148
+ }
149
+ on_key_pressed { |key_event|
150
+ if key_event.keyCode == swt(:cr)
151
+ table_proxy.finish_edit!
152
+ elsif key_event.keyCode == swt(:esc)
153
+ table_proxy.cancel_edit!
154
+ end
155
+ }
156
+ }
157
+ end,
158
+ },
159
+ time: {
160
+ widget_value_property: :date_time,
161
+ editor_gui: lambda do |args, model, property, table_proxy|
162
+ first_time = true
163
+ table_proxy.table_editor.minimumHeight = 25
164
+ time(*args) {
165
+ date_time model.send(property)
166
+ focus true
167
+ on_focus_lost {
168
+ table_proxy.finish_edit!
169
+ }
170
+ on_key_pressed { |key_event|
171
+ if key_event.keyCode == swt(:cr)
172
+ table_proxy.finish_edit!
173
+ elsif key_event.keyCode == swt(:esc)
174
+ table_proxy.cancel_edit!
175
+ end
176
+ }
177
+ }
178
+ end,
179
+ },
180
+ radio: {
181
+ widget_value_property: :selection,
182
+ editor_gui: lambda do |args, model, property, table_proxy|
183
+ first_time = true
184
+ table_proxy.table_editor.minimumHeight = 25
185
+ radio(*args) {
186
+ selection model.send(property)
187
+ focus true
188
+ on_widget_selected {
189
+ table_proxy.finish_edit!
190
+ }
191
+ on_focus_lost {
192
+ table_proxy.finish_edit!
193
+ }
194
+ on_key_pressed { |key_event|
195
+ if key_event.keyCode == swt(:cr)
196
+ table_proxy.finish_edit!
197
+ elsif key_event.keyCode == swt(:esc)
198
+ table_proxy.cancel_edit!
199
+ end
200
+ }
201
+ }
202
+ end,
203
+ },
204
+ spinner: {
205
+ widget_value_property: :selection,
206
+ editor_gui: lambda do |args, model, property, table_proxy|
207
+ first_time = true
208
+ table_proxy.table_editor.minimumHeight = 25
209
+ table_editor_widget_proxy = spinner(*args) {
210
+ selection model.send(property)
211
+ focus true
212
+ on_focus_lost {
213
+ table_proxy.finish_edit!
214
+ }
215
+ on_key_pressed { |key_event|
216
+ if key_event.keyCode == swt(:cr)
217
+ table_proxy.finish_edit!
218
+ elsif key_event.keyCode == swt(:esc)
219
+ table_proxy.cancel_edit!
220
+ end
221
+ }
222
+ }
223
+ table_editor_widget_proxy
224
+ end,
225
+ },
226
+ }
227
+ end
228
+ end
229
+
230
+ def initialize(parent, args, block)
231
+ super(parent, args, block)
13
232
  @columns = []
14
233
  @children = []
234
+ @editors = []
15
235
  @selection = []
236
+ @table_editor = TableEditor.new(self)
237
+ @table_editor.horizontalAlignment = SWTProxy[:left]
238
+ @table_editor.grabHorizontal = true
239
+ @table_editor.minimumHeight = 20
240
+ if editable?
241
+ on_mouse_up { |event|
242
+ edit_table_item(event.table_item, event.column_index)
243
+ }
244
+ end
16
245
  end
17
246
 
18
247
  # Only table_columns may be added as children
19
- def add_child(child)
248
+ def post_initialize_child(child)
20
249
  if child.is_a?(TableColumnProxy)
21
250
  @columns << child
251
+ child.render
252
+ elsif child.is_a?(TableItemProxy)
253
+ @children << child
254
+ child.render
22
255
  else
23
- @children << child
256
+ @editors << child
24
257
  end
25
- child.redraw
258
+ end
259
+
260
+ # Executes for the parent of a child that just got disposed
261
+ def post_dispose_child(child)
262
+ if child.is_a?(TableColumnProxy)
263
+ @columns&.delete(child)
264
+ elsif child.is_a?(TableItemProxy)
265
+ @children&.delete(child)
266
+ else
267
+ @editors&.delete(child)
268
+ end
269
+ end
270
+
271
+ def post_add_content
272
+ return if @initially_sorted
273
+ initial_sort!
274
+ @initially_sorted = true
275
+ end
276
+
277
+ def default_layout
278
+ nil
279
+ end
280
+
281
+ def get_data(key=nil)
282
+ data
26
283
  end
27
284
 
28
285
  def remove_all
@@ -30,20 +287,39 @@ module Glimmer
30
287
  redraw
31
288
  end
32
289
 
290
+ def editable?
291
+ args.include?(:editable)
292
+ end
293
+ alias editable editable?
294
+
295
+ def selection
296
+ @selection.to_a
297
+ end
298
+
33
299
  def selection=(new_selection)
34
- changed = (@selection + new_selection) - (@selection & new_selection)
300
+ new_selection = new_selection.to_a
301
+ changed = (selection + new_selection) - (selection & new_selection)
35
302
  @selection = new_selection
36
303
  changed.each(&:redraw)
37
304
  end
38
305
 
39
- def items=(new_items)
306
+ def items=(new_items)
40
307
  @children = new_items
41
308
  redraw
42
309
  end
43
310
 
311
+ def item_count=(value)
312
+ @item_count = value
313
+ redraw_empty_items
314
+ end
315
+
316
+ def cells_for(model)
317
+ column_properties.map {|property| model.send(property)}
318
+ end
319
+
44
320
  def search(&condition)
45
321
  items.select {|item| condition.nil? || condition.call(item)}
46
- end
322
+ end
47
323
 
48
324
  def index_of(item)
49
325
  items.index(item)
@@ -61,8 +337,252 @@ module Glimmer
61
337
  self.selection = new_selection
62
338
  end
63
339
 
64
- def edit_table_item(table_item, column_index)
65
- table_item.edit(column_index)
340
+ def sort_block=(comparator)
341
+ @sort_block = comparator
342
+ end
343
+
344
+ def sort_by_block=(property_picker)
345
+ @sort_by_block = property_picker
346
+ end
347
+
348
+ def sort_property=(new_sort_property)
349
+ @sort_property = [new_sort_property].flatten.compact
350
+ end
351
+
352
+ def detect_sort_type
353
+ @sort_type = sort_property.size.times.map { String }
354
+ array = model_binding.evaluate_property
355
+ sort_property.each_with_index do |a_sort_property, i|
356
+ values = array.map { |object| object.send(a_sort_property) }
357
+ value_classes = values.map(&:class).uniq
358
+ if value_classes.size == 1
359
+ @sort_type[i] = value_classes.first
360
+ elsif value_classes.include?(Integer)
361
+ @sort_type[i] = Integer
362
+ elsif value_classes.include?(Float)
363
+ @sort_type[i] = Float
364
+ end
365
+ end
366
+ end
367
+
368
+ def column_sort_properties
369
+ column_properties.zip(columns.map(&:sort_property)).map do |pair|
370
+ [pair.compact.last].flatten.compact
371
+ end
372
+ end
373
+
374
+ def sort_direction
375
+ @sort_direction == :ascending ? SWTProxy[:up] : SWTProxy[:down]
376
+ end
377
+
378
+ def sort_direction=(value)
379
+ @sort_direction = value == SWTProxy[:up] ? :ascending : :descending
380
+ end
381
+
382
+ # Sorts by specified TableColumnProxy object. If nil, it uses the table default sort instead.
383
+ def sort_by_column!(table_column_proxy=nil)
384
+ index = columns.to_a.index(table_column_proxy) unless table_column_proxy.nil?
385
+ new_sort_property = table_column_proxy.nil? ? @sort_property : table_column_proxy.sort_property || [column_properties[index]]
386
+
387
+ return if table_column_proxy.nil? && new_sort_property.nil? && @sort_block.nil? && @sort_by_block.nil?
388
+ if new_sort_property && table_column_proxy.nil? && new_sort_property.size == 1 && (index = column_sort_properties.index(new_sort_property))
389
+ table_column_proxy = columns[index]
390
+ end
391
+ if new_sort_property && new_sort_property.size == 1 && !additional_sort_properties.to_a.empty?
392
+ selected_additional_sort_properties = additional_sort_properties.clone
393
+ if selected_additional_sort_properties.include?(new_sort_property.first)
394
+ selected_additional_sort_properties.delete(new_sort_property.first)
395
+ new_sort_property += selected_additional_sort_properties
396
+ else
397
+ new_sort_property += additional_sort_properties
398
+ end
399
+ end
400
+
401
+ new_sort_property = [new_sort_property].flatten.compact unless new_sort_property.is_a?(Array)
402
+ @sort_direction = @sort_direction.nil? || @sort_property.first != new_sort_property.first || @sort_direction == :descending ? :ascending : :descending
403
+
404
+ @sort_property = new_sort_property
405
+ table_column_index = column_properties.index(new_sort_property.to_s.to_sym)
406
+ table_column_proxy ||= columns[table_column_index] if table_column_index
407
+ @sort_column = table_column_proxy if table_column_proxy
408
+
409
+ if table_column_proxy
410
+ @sort_by_block = nil
411
+ @sort_block = nil
412
+ end
413
+ @sort_type = nil
414
+ if table_column_proxy&.sort_by_block
415
+ @sort_by_block = table_column_proxy.sort_by_block
416
+ elsif table_column_proxy&.sort_block
417
+ @sort_block = table_column_proxy.sort_block
418
+ else
419
+ detect_sort_type
420
+ end
421
+
422
+ sort!
423
+ end
424
+
425
+ def initial_sort!
426
+ sort_by_column!
427
+ end
428
+
429
+ def sort!
430
+ return unless sort_property && (sort_type || sort_block || sort_by_block)
431
+ array = model_binding.evaluate_property
432
+ array = array.sort_by(&:hash) # this ensures consistent subsequent sorting in case there are equivalent sorts to avoid an infinite loop
433
+ # Converting value to_s first to handle nil cases. Should work with numeric, boolean, and date fields
434
+ if sort_block
435
+ sorted_array = array.sort(&sort_block)
436
+ elsif sort_by_block
437
+ sorted_array = array.sort_by(&sort_by_block)
438
+ else
439
+ sorted_array = array.sort_by do |object|
440
+ sort_property.each_with_index.map do |a_sort_property, i|
441
+ value = object.send(a_sort_property)
442
+ # handle nil and difficult to compare types gracefully
443
+ if sort_type[i] == Integer
444
+ value = value.to_i
445
+ elsif sort_type[i] == Float
446
+ value = value.to_f
447
+ elsif sort_type[i] == String
448
+ value = value.to_s
449
+ end
450
+ value
451
+ end
452
+ end
453
+ end
454
+ sorted_array = sorted_array.reverse if @sort_direction == :descending
455
+ model_binding.call(sorted_array)
456
+ end
457
+
458
+ def additional_sort_properties=(*args)
459
+ @additional_sort_properties = args unless args.empty?
460
+ end
461
+
462
+ def editor=(args)
463
+ @editor = args
464
+ end
465
+
466
+ # Indicates if table is in edit mode, thus displaying a text widget for a table item cell
467
+ def edit_mode?
468
+ !!@edit_mode
469
+ end
470
+
471
+ def cancel_edit!
472
+ @cancel_edit&.call if @edit_mode
473
+ end
474
+
475
+ def finish_edit!
476
+ @finish_edit&.call if @edit_mode
477
+ end
478
+
479
+ # 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.
480
+ # It is set to false once change is saved to model
481
+ def edit_in_progress?
482
+ !!@edit_in_progress
483
+ end
484
+
485
+ def edit_selected_table_item(column_index, before_write: nil, after_write: nil, after_cancel: nil)
486
+ edit_table_item(selection.first, column_index, before_write: before_write, after_write: after_write, after_cancel: after_cancel)
487
+ end
488
+
489
+ # TODO migrate the following to the next method
490
+ # def edit_table_item(table_item, column_index)
491
+ # table_item&.edit(column_index) unless column_index.nil?
492
+ # end
493
+
494
+ def edit_table_item(table_item, column_index, before_write: nil, after_write: nil, after_cancel: nil)
495
+ return if table_item.nil? || (@edit_mode && @edit_table_item == table_item && @edit_column_index == column_index)
496
+ @edit_column_index = column_index
497
+ @edit_table_item = table_item
498
+ column_index = column_index.to_i
499
+ model = table_item.data
500
+ property = column_properties[column_index]
501
+ cancel_edit!
502
+ return unless columns[column_index].editable?
503
+ action_taken = false
504
+ @edit_mode = true
505
+
506
+ editor_config = columns[column_index].editor || editor
507
+ editor_config = [editor_config].flatten.compact
508
+ editor_widget_options = editor_config.last.is_a?(Hash) ? editor_config.last : {}
509
+ editor_widget_arg_last_index = editor_config.last.is_a?(Hash) ? -2 : -1
510
+ editor_widget = (editor_config[0] || :text).to_sym
511
+ editor_widget_args = editor_config[1..editor_widget_arg_last_index]
512
+ model_editing_property = editor_widget_options[:property] || property
513
+ widget_value_property = TableProxy::editors.symbolize_keys[editor_widget][:widget_value_property]
514
+
515
+ @cancel_edit = lambda do |event=nil|
516
+ @cancel_in_progress = true
517
+ @table_editor.cancel!
518
+ @table_editor_widget_proxy&.dispose
519
+ @table_editor_widget_proxy = nil
520
+ after_cancel&.call
521
+ @edit_in_progress = false
522
+ @cancel_in_progress = false
523
+ @cancel_edit = nil
524
+ @edit_table_item = @edit_column_index = nil if (@edit_mode && @edit_table_item == table_item && @edit_column_index == column_index)
525
+ @edit_mode = false
526
+ end
527
+
528
+ @finish_edit = lambda do |event=nil|
529
+ new_value = @table_editor_widget_proxy&.send(widget_value_property)
530
+ if table_item.disposed?
531
+ @cancel_edit.call
532
+ elsif !new_value.nil? && !action_taken && !@edit_in_progress && !@cancel_in_progress
533
+ action_taken = true
534
+ @edit_in_progress = true
535
+ if new_value == model.send(model_editing_property)
536
+ @cancel_edit.call
537
+ else
538
+ before_write&.call
539
+ @table_editor.save!(widget_value_property: widget_value_property)
540
+ model.send("#{model_editing_property}=", new_value) # makes table update itself, so must search for selected table item again
541
+ # Table refresh happens here because of model update triggering observers, so must retrieve table item again
542
+ edited_table_item = search { |ti| ti.data == model }.first
543
+ show_item(edited_table_item)
544
+ @table_editor_widget_proxy&.dispose
545
+ @table_editor_widget_proxy = nil
546
+ after_write&.call(edited_table_item)
547
+ @edit_in_progress = false
548
+ @edit_table_item = @edit_column_index = nil
549
+ end
550
+ end
551
+ end
552
+
553
+ content {
554
+ @table_editor_widget_proxy = TableProxy::editors.symbolize_keys[editor_widget][:editor_gui].call(editor_widget_args, model, model_editing_property, self)
555
+ }
556
+ @table_editor.set_editor(@table_editor_widget_proxy, table_item, column_index)
557
+ rescue => e
558
+ Glimmer::Config.logger.error {e.full_message}
559
+ raise e
560
+ end
561
+
562
+ def show_item(table_item)
563
+ table_item.dom_element.focus
564
+ end
565
+
566
+ def add_listener(underscored_listener_name, &block)
567
+ enhanced_block = lambda do |event|
568
+ event.extend(TableListenerEvent)
569
+ block.call(event)
570
+ end
571
+ super(underscored_listener_name, &enhanced_block)
572
+ end
573
+
574
+
575
+ def header_visible=(value)
576
+ @header_visible = value
577
+ if @header_visible
578
+ thead_dom_element.remove_class('hide')
579
+ else
580
+ thead_dom_element.add_class('hide')
581
+ end
582
+ end
583
+
584
+ def header_visible
585
+ @header_visible
66
586
  end
67
587
 
68
588
  def selector
@@ -84,9 +604,9 @@ module Glimmer
84
604
  event.singleton_class.send(:define_method, :column_index) do
85
605
  (table_data || event.target).attr('data-column-index')
86
606
  end
87
- event_listener.call(event)
607
+ event_listener.call(event)
88
608
  }
89
- }
609
+ }
90
610
 
91
611
  {
92
612
  'on_mouse_down' => {
@@ -96,13 +616,26 @@ module Glimmer
96
616
  'on_mouse_up' => {
97
617
  event: 'mouseup',
98
618
  event_handler: mouse_handler,
619
+ },
620
+ 'on_widget_selected' => {
621
+ event: 'mouseup',
99
622
  }
100
623
  }
101
624
  end
102
625
 
103
626
  def redraw
104
627
  super()
105
- @columns.to_a.each(&:redraw)
628
+ @columns.to_a.each(&:redraw)
629
+ redraw_empty_items
630
+ end
631
+
632
+ def redraw_empty_items
633
+ if @children&.size.to_i < item_count.to_i
634
+ item_count.to_i.times do
635
+ empty_columns = column_properties&.size.to_i.times.map { |i| "<td data-column-index='#{i}'></td>" }
636
+ items_dom_element.append("<tr class='table-item empty-table-item'>#{empty_columns}</tr>")
637
+ end
638
+ end
106
639
  end
107
640
 
108
641
  def element
@@ -125,7 +658,7 @@ module Glimmer
125
658
  Document.find(items_path)
126
659
  end
127
660
 
128
- def columns_dom
661
+ def columns_dom
129
662
  tr {
130
663
  }
131
664
  end
@@ -134,9 +667,13 @@ module Glimmer
134
667
  thead {
135
668
  columns_dom
136
669
  }
137
- end
670
+ end
671
+
672
+ def thead_dom_element
673
+ dom_element.find('thead')
674
+ end
138
675
 
139
- def items_dom
676
+ def items_dom
140
677
  tbody {
141
678
  }
142
679
  end
@@ -145,7 +682,8 @@ module Glimmer
145
682
  table_id = id
146
683
  table_id_style = css
147
684
  table_id_css_classes = css_classes
148
- table_id_css_classes << 'table'
685
+ table_id_css_classes << 'table' unless table_id_css_classes.include?('table')
686
+ table_id_css_classes << 'editable' if editable? && !table_id_css_classes.include?('editable')
149
687
  table_id_css_classes_string = table_id_css_classes.to_a.join(' ')
150
688
  @dom ||= html {
151
689
  table(id: table_id, style: table_id_style, class: table_id_css_classes_string) {
@@ -154,6 +692,23 @@ module Glimmer
154
692
  }
155
693
  }.to_s
156
694
  end
695
+
696
+ private
697
+
698
+ def property_type_converters
699
+ super.merge({
700
+ selection: lambda do |value|
701
+ if value.is_a?(Array)
702
+ search {|ti| value.include?(ti.get_data) }
703
+ else
704
+ search {|ti| ti.get_data == value}
705
+ end
706
+ end,
707
+ })
708
+ end
709
+
157
710
  end
711
+
158
712
  end
713
+
159
714
  end