glimmer-dsl-libui 0.7.2 → 0.7.4

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.
@@ -0,0 +1,314 @@
1
+ require 'glimmer-dsl-libui'
2
+
3
+ class BasicTableSelection
4
+ TableColumnPresenter = Struct.new(:name,
5
+ :column,
6
+ :sort_indicator,
7
+ :table_presenter,
8
+ keyword_init: true) do
9
+ def toggle_sort_indicator
10
+ self.sort_indicator = self.sort_indicator != :ascending ? :ascending : :descending
11
+ end
12
+
13
+ def sort
14
+ selected_data = table_presenter.selection_mode == :zero_or_many ? table_presenter.selected_rows : table_presenter.selected_row
15
+ toggle_sort_indicator
16
+ table_presenter.data.sort_by! { |row_data| row_data[column] }
17
+ table_presenter.data.reverse! if sort_indicator == :descending
18
+ table_presenter.selection = table_presenter.selection_mode == :zero_or_many ? selected_data&.map { |selected_row| table_presenter.data.index(selected_row) } : table_presenter.data.index(selected_data)
19
+ end
20
+ end
21
+
22
+ TablePresenter = Struct.new(:data,
23
+ :column_names,
24
+ :selection_mode,
25
+ :selection,
26
+ :header_visible,
27
+ keyword_init: true) do
28
+ def selection_items
29
+ data.size.times.map { |row| "Row #{row} Selection" }
30
+ end
31
+
32
+ def toggle_header_visible
33
+ self.header_visible = !(header_visible.nil? || header_visible)
34
+ end
35
+
36
+ def column_presenters
37
+ @column_presenters ||= column_names.each_with_index.map do |column_name, column|
38
+ TableColumnPresenter.new(name: column_name, column: column, table_presenter: self)
39
+ end
40
+ end
41
+
42
+ def selected_row
43
+ selection && data[selection]
44
+ end
45
+
46
+ def selected_rows
47
+ selection && selection.is_a?(Array) && selection.map { |row| data[row] }
48
+ end
49
+ end
50
+
51
+ include Glimmer::LibUI::Application
52
+
53
+ before_body do
54
+ data = [
55
+ %w[cat meow],
56
+ %w[dog woof],
57
+ %w[chicken cock-a-doodle-doo],
58
+ %w[horse neigh],
59
+ %w[cow moo]
60
+ ]
61
+ @one_table_presenter = TablePresenter.new(
62
+ data: data.dup,
63
+ column_names: ['Name', 'Description'],
64
+ selection_mode: :one, # other values are :zero_or_many , :zero_or_one, :none (default is :zero_or_one if not specified)
65
+ selection: 2, # initial selection row index (could be nil too or just left off, defaulting to 0)
66
+ header_visible: nil, # defaults to true
67
+ )
68
+ @zero_or_one_table_presenter = TablePresenter.new(
69
+ data: data.dup,
70
+ column_names: ['Name', 'Description'],
71
+ selection_mode: :zero_or_one, # other values are :zero_or_many , :one, :none (default is :zero_or_one if not specified)
72
+ selection: nil, # initial selection row index (could be an integer too or just left off, defaulting to nil)
73
+ header_visible: nil, # defaults to true
74
+ )
75
+ @zero_or_many_table_presenter = TablePresenter.new(
76
+ data: data.dup,
77
+ column_names: ['Name', 'Description'],
78
+ selection_mode: :zero_or_many, # other values are :zero_or_many , :one, :none (default is :zero_or_one if not specified)
79
+ selection: [0, 2, 4], # initial selection row index (could be an integer too or just left off, defaulting to nil)
80
+ header_visible: nil, # defaults to true
81
+ )
82
+ @none_table_presenter = TablePresenter.new(
83
+ data: data.dup,
84
+ column_names: ['Name', 'Description'],
85
+ selection_mode: :none, # other values are :zero_or_many , :zero_or_one, :one (default is :zero_or_one if not specified)
86
+ selection: nil, # defaults to nil
87
+ header_visible: nil, # defaults to true
88
+ )
89
+ end
90
+
91
+ body {
92
+ window('Basic Table Selection', 400, 300) {
93
+ tab {
94
+ tab_item('One') {
95
+ vertical_box {
96
+ vertical_box {
97
+ stretchy false
98
+
99
+ @one_table_selection_radio_buttons = radio_buttons {
100
+ items @one_table_presenter.selection_items
101
+ selected <=> [@one_table_presenter, :selection]
102
+ }
103
+ }
104
+
105
+ button('Toggle Table Header Visibility') {
106
+ stretchy false
107
+
108
+ on_clicked do
109
+ @one_table_presenter.toggle_header_visible
110
+ end
111
+ }
112
+
113
+ @one_table = table {
114
+ @one_table_presenter.column_presenters.each do |column_presenter|
115
+ text_column(column_presenter.name) {
116
+ sort_indicator <=> [column_presenter, :sort_indicator]
117
+
118
+ on_clicked do |tc, column|
119
+ puts "Clicked column #{column}: #{tc.name}"
120
+ column_presenter.sort
121
+ end
122
+ }
123
+ end
124
+
125
+ cell_rows @one_table_presenter.data
126
+ selection_mode <= [@one_table_presenter, :selection_mode]
127
+ selection <=> [@one_table_presenter, :selection]
128
+ header_visible <= [@one_table_presenter, :header_visible]
129
+ sortable false # disable default sorting behavior to demonstrate manual sorting
130
+
131
+ on_row_clicked do |t, row|
132
+ puts "Row Clicked: #{row}"
133
+ end
134
+
135
+ on_row_double_clicked do |t, row|
136
+ puts "Row Double Clicked: #{row}"
137
+ end
138
+
139
+ on_selection_changed do |t, selection, added_selection, removed_selection|
140
+ # selection is an array or nil if selection mode is zero_or_many
141
+ # otherwise, selection is a single index integer or nil when not selected
142
+ puts "Selection Changed: #{selection.inspect}"
143
+ puts "Added Selection: #{added_selection.inspect}"
144
+ puts "Removed Selection: #{removed_selection.inspect}"
145
+ end
146
+ }
147
+ }
148
+ }
149
+
150
+ tab_item('Zero-Or-One') {
151
+ vertical_box {
152
+ vertical_box {
153
+ stretchy false
154
+
155
+ @zero_or_one_table_selection_radio_buttons = radio_buttons {
156
+ items @zero_or_one_table_presenter.selection_items
157
+ selected <=> [@zero_or_one_table_presenter, :selection]
158
+ }
159
+ }
160
+
161
+ button('Toggle Table Header Visibility') {
162
+ stretchy false
163
+
164
+ on_clicked do
165
+ @zero_or_one_table_presenter.toggle_header_visible
166
+ end
167
+ }
168
+
169
+ @zero_or_one_table = table {
170
+ @zero_or_one_table_presenter.column_presenters.each do |column_presenter|
171
+ text_column(column_presenter.name) {
172
+ sort_indicator <=> [column_presenter, :sort_indicator]
173
+
174
+ on_clicked do |tc, column|
175
+ puts "Clicked column #{column}: #{tc.name}"
176
+ column_presenter.sort
177
+ end
178
+ }
179
+ end
180
+
181
+ cell_rows @zero_or_one_table_presenter.data
182
+ selection_mode <= [@zero_or_one_table_presenter, :selection_mode]
183
+ selection <=> [@zero_or_one_table_presenter, :selection]
184
+ header_visible <= [@zero_or_one_table_presenter, :header_visible]
185
+ sortable false # disable default sorting behavior to demonstrate manual sorting
186
+
187
+ on_row_clicked do |t, row|
188
+ puts "Row Clicked: #{row}"
189
+ end
190
+
191
+ on_row_double_clicked do |t, row|
192
+ puts "Row Double Clicked: #{row}"
193
+ end
194
+
195
+ on_selection_changed do |t, selection, added_selection, removed_selection|
196
+ # selection is an array or nil if selection mode is zero_or_many
197
+ # otherwise, selection is a single index integer or nil when not selected
198
+ puts "Selection Changed: #{selection.inspect}"
199
+ puts "Added Selection: #{added_selection.inspect}"
200
+ puts "Removed Selection: #{removed_selection.inspect}"
201
+ end
202
+ }
203
+ }
204
+ }
205
+
206
+ tab_item('Zero-Or-Many') {
207
+ vertical_box {
208
+ vertical_box {
209
+ stretchy false
210
+
211
+ @zero_or_many_table_selection_checkboxes = @zero_or_many_table_presenter.data.size.times.map do |row|
212
+ checkbox("Row #{row} Selection") {
213
+ checked <=> [@zero_or_many_table_presenter, :selection,
214
+ on_read: ->(selection_rows) {selection_rows.to_a.include?(row)},
215
+ on_write: ->(checked_value) {
216
+ checked_value ?
217
+ (@zero_or_many_table_presenter.selection.to_a + [row]).uniq :
218
+ @zero_or_many_table_presenter.selection.to_a.reject {|v| v == row }
219
+ },
220
+ ]
221
+ }
222
+ end
223
+ }
224
+
225
+ button('Toggle Table Header Visibility') {
226
+ stretchy false
227
+
228
+ on_clicked do
229
+ @zero_or_many_table_presenter.toggle_header_visible
230
+ end
231
+ }
232
+
233
+ @zero_or_many_table = table {
234
+ @zero_or_many_table_presenter.column_presenters.each do |column_presenter|
235
+ text_column(column_presenter.name) {
236
+ sort_indicator <=> [column_presenter, :sort_indicator]
237
+
238
+ on_clicked do |tc, column|
239
+ puts "Clicked column #{column}: #{tc.name}"
240
+ column_presenter.sort
241
+ end
242
+ }
243
+ end
244
+
245
+ cell_rows @zero_or_many_table_presenter.data
246
+ selection_mode <= [@zero_or_many_table_presenter, :selection_mode]
247
+ selection <=> [@zero_or_many_table_presenter, :selection]
248
+ header_visible <= [@zero_or_many_table_presenter, :header_visible]
249
+ sortable false # disable default sorting behavior to demonstrate manual sorting
250
+
251
+ on_row_clicked do |t, row|
252
+ puts "Row Clicked: #{row}"
253
+ end
254
+
255
+ on_row_double_clicked do |t, row|
256
+ puts "Row Double Clicked: #{row}"
257
+ end
258
+
259
+ on_selection_changed do |t, selection, added_selection, removed_selection|
260
+ # selection is an array or nil if selection mode is zero_or_many
261
+ # otherwise, selection is a single index integer or nil when not selected
262
+ puts "Selection Changed: #{selection.inspect}"
263
+ puts "Added Selection: #{added_selection.inspect}"
264
+ puts "Removed Selection: #{removed_selection.inspect}"
265
+ end
266
+ }
267
+ }
268
+ }
269
+
270
+ tab_item('None') {
271
+ vertical_box {
272
+ button('Toggle Table Header Visibility') {
273
+ stretchy false
274
+
275
+ on_clicked do
276
+ @none_table_presenter.toggle_header_visible
277
+ end
278
+ }
279
+
280
+ @none_table = table {
281
+ @none_table_presenter.column_presenters.each do |column_presenter|
282
+ text_column(column_presenter.name) {
283
+ sort_indicator <=> [column_presenter, :sort_indicator]
284
+
285
+ on_clicked do |tc, column|
286
+ puts "Clicked column #{column}: #{tc.name}"
287
+ column_presenter.sort
288
+ end
289
+ }
290
+ end
291
+
292
+ cell_rows @none_table_presenter.data
293
+ selection_mode <= [@none_table_presenter, :selection_mode]
294
+ selection <=> [@none_table_presenter, :selection]
295
+ header_visible <= [@none_table_presenter, :header_visible]
296
+ sortable false # disable default sorting behavior to demonstrate manual sorting
297
+
298
+ on_row_clicked do |t, row|
299
+ puts "Row Clicked: #{row}"
300
+ end
301
+
302
+ on_row_double_clicked do |t, row|
303
+ puts "Row Double Clicked: #{row}"
304
+ end
305
+ }
306
+ }
307
+ }
308
+
309
+ }
310
+ }
311
+ }
312
+ end
313
+
314
+ BasicTableSelection.launch
@@ -0,0 +1,307 @@
1
+ require 'glimmer-dsl-libui'
2
+
3
+ class BasicTableSelection
4
+ include Glimmer::LibUI::Application
5
+
6
+ before_body do
7
+ data = [
8
+ %w[cat meow],
9
+ %w[dog woof],
10
+ %w[chicken cock-a-doodle-doo],
11
+ %w[horse neigh],
12
+ %w[cow moo]
13
+ ]
14
+ @one_table_data = data.dup
15
+ @zero_or_one_table_data = data.dup
16
+ @zero_or_many_table_data = data.dup
17
+ @none_table_data = data.dup
18
+ end
19
+
20
+ body {
21
+ window('Basic Table Selection', 400, 300) {
22
+ tab {
23
+ tab_item('One') {
24
+ vertical_box {
25
+ vertical_box {
26
+ stretchy false
27
+
28
+ @one_table_selection_radio_buttons = radio_buttons {
29
+ items @one_table_data.size.times.map { |row| "Row #{row} Selection" }
30
+
31
+ on_selected do |rb|
32
+ @one_table.selection = [rb.selected]
33
+ end
34
+ }
35
+ }
36
+
37
+ button('Toggle Table Header Visibility') {
38
+ stretchy false
39
+
40
+ on_clicked do
41
+ @one_table.header_visible = !@one_table.header_visible
42
+ end
43
+ }
44
+
45
+ @one_table = table {
46
+ text_column('Animal') {
47
+ # sort_indicator :descending # (optional) can be :ascending, :descending, or nil (default)
48
+
49
+ on_clicked do |tc, column|
50
+ sort_one_table_column(tc, column)
51
+ end
52
+ }
53
+ text_column('Description') {
54
+ # sort_indicator :descending # (optional) can be :ascending, :descending, or nil (default)
55
+
56
+ on_clicked do |tc, column|
57
+ sort_one_table_column(tc, column)
58
+ end
59
+ }
60
+
61
+ cell_rows @one_table_data
62
+ selection_mode :one # other values are :zero_or_many , :zero_or_one, :none (default is :zero_or_one if not specified)
63
+ selection 2 # initial selection row index (could be nil too or just left off, defaulting to 0)
64
+ # header_visible true # default
65
+ sortable false # disable default sorting behavior to demonstrate manual sorting
66
+
67
+ on_row_clicked do |t, row|
68
+ puts "Row Clicked: #{row}"
69
+ end
70
+
71
+ on_row_double_clicked do |t, row|
72
+ puts "Row Double Clicked: #{row}"
73
+ end
74
+
75
+ on_selection_changed do |t, selection, added_selection, removed_selection|
76
+ # selection is an array or nil if selection mode is zero_or_many
77
+ # otherwise, selection is a single index integer or nil when not selected
78
+ puts "Selection Changed: #{selection.inspect}"
79
+ puts "Added Selection: #{added_selection.inspect}"
80
+ puts "Removed Selection: #{removed_selection.inspect}"
81
+ @one_table_selection_radio_buttons.selected = selection
82
+ end
83
+ }
84
+ }
85
+ }
86
+
87
+ tab_item('Zero-Or-One') {
88
+ vertical_box {
89
+ vertical_box {
90
+ stretchy false
91
+
92
+ @zero_or_one_table_selection_radio_buttons = radio_buttons {
93
+ items @zero_or_one_table_data.size.times.map { |row| "Row #{row} Selection" }
94
+
95
+ on_selected do |rb|
96
+ @zero_or_one_table.selection = [rb.selected]
97
+ end
98
+ }
99
+ }
100
+
101
+ button('Toggle Table Header Visibility') {
102
+ stretchy false
103
+
104
+ on_clicked do
105
+ @zero_or_one_table.header_visible = !@zero_or_one_table.header_visible
106
+ end
107
+ }
108
+
109
+ @zero_or_one_table = table {
110
+ text_column('Animal') {
111
+ # sort_indicator :descending # (optional) can be :ascending, :descending, or nil (default)
112
+
113
+ on_clicked do |tc, column|
114
+ sort_zero_or_one_table_column(tc, column)
115
+ end
116
+ }
117
+ text_column('Description') {
118
+ # sort_indicator :descending # (optional) can be :ascending, :descending, or nil (default)
119
+
120
+ on_clicked do |tc, column|
121
+ sort_zero_or_one_table_column(tc, column)
122
+ end
123
+ }
124
+
125
+ cell_rows @zero_or_one_table_data
126
+ selection_mode :zero_or_one # other values are :zero_or_many , :one, :none (default is :zero_or_one if not specified)
127
+ # selection 0 # initial selection row index (could be nil too or just left off)
128
+ # header_visible true # default
129
+ sortable false # disable default sorting behavior to demonstrate manual sorting
130
+
131
+ on_row_clicked do |t, row|
132
+ puts "Row Clicked: #{row}"
133
+ end
134
+
135
+ on_row_double_clicked do |t, row|
136
+ puts "Row Double Clicked: #{row}"
137
+ end
138
+
139
+ on_selection_changed do |t, selection, added_selection, removed_selection|
140
+ # selection is an array or nil if selection mode is zero_or_many
141
+ # otherwise, selection is a single index integer or nil when not selected
142
+ puts "Selection Changed: #{selection.inspect}"
143
+ puts "Added Selection: #{added_selection.inspect}"
144
+ puts "Removed Selection: #{removed_selection.inspect}"
145
+ @zero_or_one_table_selection_radio_buttons.selected = selection
146
+ end
147
+ }
148
+ }
149
+ }
150
+
151
+ tab_item('Zero-Or-Many') {
152
+ vertical_box {
153
+ vertical_box {
154
+ stretchy false
155
+
156
+ @zero_or_many_table_selection_checkboxes = @zero_or_many_table_data.size.times.map do |row|
157
+ checkbox("Row #{row} Selection") {
158
+ on_toggled do |c|
159
+ table_selection = @zero_or_many_table.selection.to_a
160
+ if c.checked?
161
+ table_selection << row unless table_selection.include?(row)
162
+ else
163
+ table_selection.delete(row) if table_selection.include?(row)
164
+ end
165
+ @zero_or_many_table.selection = table_selection
166
+ end
167
+ }
168
+ end
169
+ }
170
+
171
+ button('Toggle Table Header Visibility') {
172
+ stretchy false
173
+
174
+ on_clicked do
175
+ @zero_or_many_table.header_visible = !@zero_or_many_table.header_visible
176
+ end
177
+ }
178
+
179
+ @zero_or_many_table = table {
180
+ text_column('Animal') {
181
+ # sort_indicator :descending # (optional) can be :ascending, :descending, or nil (default)
182
+
183
+ on_clicked do |tc, column|
184
+ sort_zero_or_many_table_column(tc, column)
185
+ end
186
+ }
187
+ text_column('Description') {
188
+ # sort_indicator :descending # (optional) can be :ascending, :descending, or nil (default)
189
+
190
+ on_clicked do |tc, column|
191
+ sort_zero_or_many_table_column(tc, column)
192
+ end
193
+ }
194
+
195
+ cell_rows @zero_or_many_table_data
196
+ selection_mode :zero_or_many # other values are :none , :zero_or_one , and :one (default is :zero_or_one if not specified)
197
+ selection 0, 2, 4 # initial selection row indexes (could be empty array too or just left off)
198
+ # header_visible true # default
199
+ sortable false # disable default sorting behavior to demonstrate manual sorting
200
+
201
+ on_row_clicked do |t, row|
202
+ puts "Row Clicked: #{row}"
203
+ end
204
+
205
+ on_row_double_clicked do |t, row|
206
+ puts "Row Double Clicked: #{row}"
207
+ end
208
+
209
+ on_selection_changed do |t, selection, added_selection, removed_selection|
210
+ # selection is an array or nil if selection mode is zero_or_many
211
+ # otherwise, selection is a single index integer or nil when not selected
212
+ puts "Selection Changed: #{selection.inspect}"
213
+ puts "Added Selection: #{added_selection.inspect}"
214
+ puts "Removed Selection: #{removed_selection.inspect}"
215
+ removed_selection&.each do |selected_row|
216
+ @zero_or_many_table_selection_checkboxes[selected_row].checked = false
217
+ end
218
+ added_selection&.each do |selected_row|
219
+ @zero_or_many_table_selection_checkboxes[selected_row].checked = true
220
+ end
221
+ end
222
+ }
223
+ }
224
+ }
225
+
226
+ tab_item('None') {
227
+ vertical_box {
228
+ button('Toggle Table Header Visibility') {
229
+ stretchy false
230
+
231
+ on_clicked do
232
+ @none_table.header_visible = !@none_table.header_visible
233
+ end
234
+ }
235
+
236
+ @none_table = table {
237
+ text_column('Animal') {
238
+ # sort_indicator :descending # (optional) can be :ascending, :descending, or nil (default)
239
+
240
+ on_clicked do |tc, column|
241
+ sort_none_table_column(tc, column)
242
+ end
243
+ }
244
+ text_column('Description') {
245
+ # sort_indicator :descending # (optional) can be :ascending, :descending, or nil (default)
246
+
247
+ on_clicked do |tc, column|
248
+ sort_none_table_column(tc, column)
249
+ end
250
+ }
251
+
252
+ cell_rows @none_table_data
253
+ selection_mode :none # other values are :zero_or_many , :zero_or_one, :one (default is :zero_or_one if not specified)
254
+ # header_visible true # default
255
+ sortable false # disable default sorting behavior to demonstrate manual sorting
256
+
257
+ on_row_clicked do |t, row|
258
+ puts "Row Clicked: #{row}"
259
+ end
260
+
261
+ on_row_double_clicked do |t, row|
262
+ puts "Row Double Clicked: #{row}"
263
+ end
264
+ }
265
+ }
266
+ }
267
+
268
+ }
269
+ }
270
+ }
271
+
272
+ def sort_one_table_column(tc, column)
273
+ puts "Clicked column #{column}: #{tc.name}"
274
+ selected_row = @one_table.selection && @one_table_data[@one_table.selection]
275
+ tc.toggle_sort_indicator
276
+ @one_table_data.sort_by! { |row_data| row_data[column] }
277
+ @one_table_data.reverse! if tc.sort_indicator == :descending
278
+ @one_table.selection = @one_table_data.index(selected_row)
279
+ end
280
+
281
+ def sort_zero_or_one_table_column(tc, column)
282
+ puts "Clicked column #{column}: #{tc.name}"
283
+ selected_row = @zero_or_one_table.selection && @zero_or_one_table_data[@zero_or_one_table.selection]
284
+ tc.toggle_sort_indicator
285
+ @zero_or_one_table_data.sort_by! { |row_data| row_data[column] }
286
+ @zero_or_one_table_data.reverse! if tc.sort_indicator == :descending
287
+ @zero_or_one_table.selection = @zero_or_one_table_data.index(selected_row)
288
+ end
289
+
290
+ def sort_zero_or_many_table_column(tc, column)
291
+ puts "Clicked column #{column}: #{tc.name}"
292
+ selected_rows = @zero_or_many_table.selection&.map { |row| @zero_or_many_table_data[row] }
293
+ tc.toggle_sort_indicator
294
+ @zero_or_many_table_data.sort_by! { |row_data| row_data[column] }
295
+ @zero_or_many_table_data.reverse! if tc.sort_indicator == :descending
296
+ @zero_or_many_table.selection = selected_rows&.map {|row_data| @zero_or_many_table_data.index(row_data) }
297
+ end
298
+
299
+ def sort_none_table_column(tc, column)
300
+ puts "Clicked column #{column}: #{tc.name}"
301
+ tc.toggle_sort_indicator
302
+ @none_table_data.sort_by! { |row_data| row_data[column] }
303
+ @none_table_data.reverse! if tc.sort_indicator == :descending
304
+ end
305
+ end
306
+
307
+ BasicTableSelection.launch
Binary file
@@ -85,6 +85,15 @@ module Glimmer
85
85
  self.sort_indicator = value
86
86
  end
87
87
  end
88
+
89
+ def data_bind_write(property, model_binding)
90
+ case property
91
+ when 'sort_indicator'
92
+ Glimmer::DataBinding::Observer.proc do
93
+ model_binding.call(sort_indicator)
94
+ end.observe(self, :sort_indicator)
95
+ end
96
+ end
88
97
 
89
98
  def can_handle_listener?(listener_name)
90
99
  listener_name == 'on_clicked'
@@ -92,6 +101,12 @@ module Glimmer
92
101
 
93
102
  def handle_listener(listener_name, &listener)
94
103
  column_listeners_for(listener_name) << listener
104
+ # TODO fix this by adding a `on_button_clicked` listener in the future to separate it from `on_clicked` on the column header
105
+ begin
106
+ super # attempt to handle listener natively if this column supports it (button_column)
107
+ rescue => e
108
+ # No Op
109
+ end
95
110
  end
96
111
 
97
112
  def column_listeners