glimmer-dsl-web 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1058 @@
1
+ # Copyright (c) 2020-2022 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/web/event_listener_proxy'
23
+ require 'glimmer/web/property_owner'
24
+
25
+ # TODO implement menu (which delays building it till render using add_content_on_render)
26
+
27
+ module Glimmer
28
+ module Web
29
+ class ElementProxy
30
+ class << self
31
+ # Factory Method that translates a Glimmer DSL keyword into a ElementProxy object
32
+ def for(keyword, parent, args, block)
33
+ element_type(keyword).new(keyword, parent, args, block)
34
+ end
35
+
36
+ # returns Ruby proxy class (type) that would handle this keyword
37
+ def element_type(keyword)
38
+ class_name_main = "#{keyword.camelcase(:upper)}Proxy"
39
+ Glimmer::Web::ElementProxy.const_get(class_name_main.to_sym)
40
+ rescue NameError => e
41
+ Glimmer::Web::ElementProxy
42
+ end
43
+
44
+ def next_id_number_for(name)
45
+ @max_id_numbers[name] = max_id_number_for(name) + 1
46
+ end
47
+
48
+ def max_id_number_for(name)
49
+ @max_id_numbers[name] = max_id_numbers[name] || 0
50
+ end
51
+
52
+ def max_id_numbers
53
+ @max_id_numbers ||= reset_max_id_numbers!
54
+ end
55
+
56
+ def reset_max_id_numbers!
57
+ @max_id_numbers = {}
58
+ end
59
+
60
+ def underscored_widget_name(widget_proxy)
61
+ widget_proxy.class.name.split(/::|\./).last.sub(/Proxy$/, '').underscore
62
+ end
63
+
64
+ def widget_handling_listener
65
+ @@widget_handling_listener
66
+ end
67
+ end
68
+
69
+ include Glimmer
70
+ include PropertyOwner
71
+
72
+ Event = Struct.new(:widget, keyword_init: true)
73
+
74
+ attr_reader :keyword, :parent, :args, :options, :path, :children, :enabled, :foreground, :background, :focus, :removed?, :rendered
75
+ alias rendered? rendered
76
+
77
+ def initialize(keyword, parent, args, block)
78
+ @keyword = keyword
79
+ @parent = parent
80
+ @options = args.last.is_a?(Hash) ? args.last.symbolize_keys : {}
81
+ @args = args
82
+ @block = block
83
+ @children = []
84
+ @parent&.post_initialize_child(self)
85
+ end
86
+
87
+ # Executes for the parent of a child that just got added
88
+ def post_initialize_child(child)
89
+ @children << child
90
+ child.render
91
+ end
92
+
93
+ # Executes for the parent of a child that just got removed
94
+ def post_remove_child(child)
95
+ @children&.delete(child)
96
+ end
97
+
98
+ # Executes at the closing of a parent widget curly braces after all children/properties have been added/set
99
+ def post_add_content
100
+ # No Op
101
+ end
102
+
103
+ def css_classes
104
+ dom_element.attr('class').to_s.split
105
+ end
106
+
107
+ def remove
108
+ remove_all_listeners
109
+ Document.find(path).remove
110
+ parent&.post_remove_child(self)
111
+ # children.each(:remove) # TODO enable this safely
112
+ @removed = true
113
+ listeners_for('widget_removed').each {|listener| listener.call(Event.new(widget: self))}
114
+ end
115
+
116
+ def remove_all_listeners
117
+ effective_observation_request_to_event_mapping.keys.each do |keyword|
118
+ effective_observation_request_to_event_mapping[keyword].to_collection.each do |mapping|
119
+ observation_requests[keyword].to_a.each do |event_listener|
120
+ event = mapping[:event]
121
+ event_handler = mapping[:event_handler]
122
+ event_element_css_selector = mapping[:event_element_css_selector]
123
+ the_listener_dom_element = event_element_css_selector ? Element[event_element_css_selector] : listener_dom_element
124
+ the_listener_dom_element.off(event, event_listener)
125
+ # TODO improve to precisely remove the listeners that were added, no more no less. (or use the event_listener_proxies method instead or in collaboration)
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ def path
132
+ "#{parent_path} #{element}##{id}.#{name}"
133
+ end
134
+ alias widget_path path # pure path without subchildren modifications
135
+
136
+ # Root element representing widget. Must be overridden by subclasses if different from div
137
+ def element
138
+ keyword
139
+ end
140
+
141
+ def shell
142
+ current_widget = self
143
+ current_widget = current_widget.parent until current_widget.parent.nil?
144
+ current_widget
145
+ end
146
+
147
+ def parents
148
+ parents_array = []
149
+ current_widget = self
150
+ until current_widget.parent.nil?
151
+ current_widget = current_widget.parent
152
+ parents_array << current_widget
153
+ end
154
+ parents_array
155
+ end
156
+
157
+ def dialog_ancestor
158
+ parents.detect {|p| p.is_a?(DialogProxy)}
159
+ end
160
+
161
+ def print
162
+ `window.print()`
163
+ true
164
+ end
165
+
166
+ def enabled=(value)
167
+ @enabled = value
168
+ dom_element.prop('disabled', !@enabled)
169
+ end
170
+
171
+ def foreground=(value)
172
+ value = ColorProxy.new(value) if value.is_a?(String)
173
+ @foreground = value
174
+ dom_element.css('color', foreground.to_css) unless foreground.nil?
175
+ end
176
+
177
+ def background=(value)
178
+ value = ColorProxy.new(value) if value.is_a?(String)
179
+ @background = value
180
+ dom_element.css('background-color', background.to_css) unless background.nil?
181
+ end
182
+
183
+ def focus=(value)
184
+ @focus = value
185
+ dom_element.focus # TODO consider if a delay or async_exec is needed here
186
+ end
187
+
188
+ def set_focus
189
+ self.focus = true
190
+ end
191
+ alias setFocus set_focus
192
+
193
+ def parent_path
194
+ @parent&.path
195
+ end
196
+
197
+ def parent_dom_element
198
+ if parent_path
199
+ Document.find(parent_path)
200
+ elsif options[:root]
201
+ Document.find(options[:root])
202
+ end
203
+ end
204
+
205
+ def render(custom_parent_dom_element: nil, brand_new: false)
206
+ the_parent_dom_element = custom_parent_dom_element || parent_dom_element
207
+ old_element = dom_element
208
+ brand_new = @dom.nil? || old_element.empty? || brand_new
209
+ build_dom(layout: !custom_parent_dom_element) # TODO handle custom parent layout by passing parent instead of parent dom element
210
+ if brand_new
211
+ # TODO make a method attach to allow subclasses to override if needed
212
+ attach(the_parent_dom_element)
213
+ else
214
+ reattach(old_element)
215
+ end
216
+ observation_requests&.each do |keyword, event_listener_set|
217
+ event_listener_set.each do |event_listener|
218
+ handle_observation_request(keyword, event_listener)
219
+ end
220
+ end
221
+ children.each do |child|
222
+ child.render
223
+ end
224
+ @rendered = true
225
+ unless skip_content_on_render_blocks?
226
+ content_on_render_blocks.each do |content_block|
227
+ content(&content_block)
228
+ end
229
+ end
230
+ end
231
+ alias redraw render
232
+
233
+ def attach(the_parent_dom_element)
234
+ the_parent_dom_element.append(@dom)
235
+ end
236
+
237
+ def reattach(old_element)
238
+ old_element.replace_with(@dom)
239
+ end
240
+
241
+ def add_text_content(text)
242
+ dom_element.append(text)
243
+ end
244
+
245
+ def content_on_render_blocks
246
+ @content_on_render_blocks ||= []
247
+ end
248
+
249
+ def skip_content_on_render_blocks?
250
+ false
251
+ end
252
+
253
+ def add_content_on_render(&content_block)
254
+ if rendered?
255
+ content_block.call
256
+ else
257
+ content_on_render_blocks << content_block
258
+ end
259
+ end
260
+
261
+ def build_dom(layout: true)
262
+ # TODO consider passing parent element instead and having table item include a table cell widget only for opal
263
+ @dom = nil
264
+ @dom = dom # TODO unify how to build dom for most widgets based on element, id, and name (class)
265
+ @dom = @parent.get_layout.dom(@dom) if @parent.respond_to?(:layout) && @parent.get_layout
266
+ @dom
267
+ end
268
+
269
+ def dom
270
+ body_id = id
271
+ body_class = ([name] + css_classes.to_a).join(' ')
272
+ html_options = options.dup
273
+ html_options[:id] ||= body_id
274
+ html_options[:class] ||= ''
275
+ html_options[:class] = "#{html_options[:class]} #{body_class}".strip
276
+ @dom ||= html {
277
+ send(keyword, html_options) {
278
+ # TODO consider supporting the idea of dynamic CSS building on close of shell that adds only as much CSS as needed for widgets that were mentioned
279
+ # style(class: 'common-style') {
280
+ # style_dom_css
281
+ # }
282
+ # [LayoutProxy, WidgetProxy].map(&:descendants).reduce(:+).each do |style_class|
283
+ # if style_class.constants.include?('STYLE')
284
+ # style(class: "#{style_class.name.split(':').last.underscore.gsub('_', '-').sub(/-proxy$/, '')}-style") {
285
+ # style_class::STYLE
286
+ # }
287
+ # end
288
+ # end
289
+ }
290
+ }.to_s
291
+ end
292
+
293
+ def content(&block)
294
+ Glimmer::DSL::Engine.add_content(self, Glimmer::DSL::Web::ElementExpression.new, keyword, &block)
295
+ end
296
+
297
+ # Subclasses must override with their own mappings
298
+ def observation_request_to_event_mapping
299
+ {}
300
+ end
301
+
302
+ def effective_observation_request_to_event_mapping
303
+ default_observation_request_to_event_mapping.merge(observation_request_to_event_mapping)
304
+ end
305
+
306
+ def default_observation_request_to_event_mapping
307
+ myself = self
308
+ mouse_event_handler = -> (event_listener) {
309
+ -> (event) {
310
+ # TODO generalize this solution to all widgets that support key presses
311
+ event.define_singleton_method(:widget) {myself}
312
+ event.define_singleton_method(:button, &event.method(:which))
313
+ event.define_singleton_method(:count) {1} # TODO support double-click count of 2 in the future by using ondblclick
314
+ event.define_singleton_method(:x, &event.method(:page_x))
315
+ event.define_singleton_method(:y, &event.method(:page_y))
316
+ doit = true
317
+ event.define_singleton_method(:doit=) do |value|
318
+ doit = value
319
+ end
320
+ event.define_singleton_method(:doit) { doit }
321
+
322
+ if event.which == 1
323
+ # event.prevent # TODO consider if this is needed
324
+ event_listener.call(event)
325
+ end
326
+
327
+ # TODO Imlement doit properly for all different kinds of events
328
+ # unless doit
329
+ # event.prevent
330
+ # event.stop
331
+ # event.stop_immediate
332
+ # end
333
+ }
334
+ }
335
+ mouse_move_event_handler = -> (event_listener) {
336
+ -> (event) {
337
+ # TODO generalize this solution to all widgets that support key presses
338
+ event.define_singleton_method(:widget) {myself}
339
+ event.define_singleton_method(:button, &event.method(:which))
340
+ event.define_singleton_method(:count) {1} # TODO support double-click count of 2 in the future by using ondblclick
341
+ event.define_singleton_method(:x, &event.method(:page_x))
342
+ event.define_singleton_method(:y, &event.method(:page_y))
343
+ doit = true
344
+ event.define_singleton_method(:doit=) do |value|
345
+ doit = value
346
+ end
347
+ event.define_singleton_method(:doit) { doit }
348
+
349
+ event_listener.call(event)
350
+
351
+ # TODO Imlement doit properly for all different kinds of events
352
+ # unless doit
353
+ # event.prevent
354
+ # event.stop
355
+ # event.stop_immediate
356
+ # end
357
+ }
358
+ }
359
+ context_menu_handler = -> (event_listener) {
360
+ -> (event) {
361
+ # TODO generalize this solution to all widgets that support key presses
362
+ event.define_singleton_method(:widget) {myself}
363
+ event.define_singleton_method(:button, &event.method(:which))
364
+ event.define_singleton_method(:count) {1} # TODO support double-click count of 2 in the future by using ondblclick
365
+ event.define_singleton_method(:x, &event.method(:page_x))
366
+ event.define_singleton_method(:y, &event.method(:page_y))
367
+ doit = true
368
+ event.define_singleton_method(:doit=) do |value|
369
+ doit = value
370
+ end
371
+ event.define_singleton_method(:doit) { doit }
372
+
373
+ if event.which == 3
374
+ event.prevent
375
+ event_listener.call(event)
376
+ end
377
+ # TODO Imlement doit properly for all different kinds of events
378
+ # unless doit
379
+ # event.prevent
380
+ # event.stop
381
+ # event.stop_immediate
382
+ # end
383
+ }
384
+ }
385
+ {
386
+ 'on_focus_gained' => {
387
+ event: 'focus',
388
+ },
389
+ 'on_focus_lost' => {
390
+ event: 'blur',
391
+ },
392
+ 'on_mouse_move' => [
393
+ {
394
+ event: 'mousemove',
395
+ event_handler: mouse_move_event_handler,
396
+ },
397
+ ],
398
+ 'on_mouse_up' => [
399
+ {
400
+ event: 'mouseup',
401
+ event_handler: mouse_event_handler,
402
+ },
403
+ {
404
+ event: 'contextmenu',
405
+ event_handler: context_menu_handler,
406
+ },
407
+ ],
408
+ 'on_mouse_down' => [
409
+ {
410
+ event: 'mousedown',
411
+ event_handler: mouse_event_handler,
412
+ },
413
+ {
414
+ event: 'contextmenu',
415
+ event_handler: context_menu_handler,
416
+ },
417
+ ],
418
+ 'on_swt_mouseup' => [
419
+ {
420
+ event: 'mouseup',
421
+ event_handler: mouse_event_handler,
422
+ },
423
+ {
424
+ event: 'contextmenu',
425
+ event_handler: context_menu_handler,
426
+ },
427
+ ],
428
+ 'on_swt_mousedown' => [
429
+ {
430
+ event: 'mousedown',
431
+ event_handler: mouse_event_handler,
432
+ },
433
+ {
434
+ event: 'contextmenu',
435
+ event_handler: context_menu_handler,
436
+ },
437
+ ],
438
+ 'on_key_pressed' => {
439
+ event: 'keypress',
440
+ event_handler: -> (event_listener) {
441
+ -> (event) {
442
+ event.define_singleton_method(:widget) {myself}
443
+ event.define_singleton_method(:keyLocation) do
444
+ location = `#{event.to_n}.originalEvent.location`
445
+ JS_LOCATION_TO_SWT_KEY_LOCATION_MAP[location] || location
446
+ end
447
+ event.define_singleton_method(:key_location, &event.method(:keyLocation))
448
+ event.define_singleton_method(:keyCode) {
449
+ JS_KEY_CODE_TO_SWT_KEY_CODE_MAP[event.which] || event.which
450
+ }
451
+ event.define_singleton_method(:key_code, &event.method(:keyCode))
452
+ event.define_singleton_method(:character) {event.which.chr}
453
+ event.define_singleton_method(:stateMask) do
454
+ state_mask = 0
455
+ state_mask |= SWTProxy[:alt] if event.alt_key
456
+ state_mask |= SWTProxy[:ctrl] if event.ctrl_key
457
+ state_mask |= SWTProxy[:shift] if event.shift_key
458
+ state_mask |= SWTProxy[:command] if event.meta_key
459
+ state_mask
460
+ end
461
+ event.define_singleton_method(:state_mask, &event.method(:stateMask))
462
+ doit = true
463
+ event.define_singleton_method(:doit=) do |value|
464
+ doit = value
465
+ end
466
+ event.define_singleton_method(:doit) { doit }
467
+ event_listener.call(event)
468
+
469
+ # TODO Fix doit false, it's not stopping input
470
+ unless doit
471
+ event.prevent
472
+ event.prevent_default
473
+ event.stop_propagation
474
+ event.stop_immediate_propagation
475
+ end
476
+
477
+ doit
478
+ }
479
+ } },
480
+ 'on_key_released' => {
481
+ event: 'keyup',
482
+ event_handler: -> (event_listener) {
483
+ -> (event) {
484
+ event.define_singleton_method(:keyLocation) do
485
+ location = `#{event.to_n}.originalEvent.location`
486
+ JS_LOCATION_TO_SWT_KEY_LOCATION_MAP[location] || location
487
+ end
488
+ event.define_singleton_method(:key_location, &event.method(:keyLocation))
489
+ event.define_singleton_method(:widget) {myself}
490
+ event.define_singleton_method(:keyCode) {
491
+ JS_KEY_CODE_TO_SWT_KEY_CODE_MAP[event.which] || event.which
492
+ }
493
+ event.define_singleton_method(:key_code, &event.method(:keyCode))
494
+ event.define_singleton_method(:character) {event.which.chr}
495
+ event.define_singleton_method(:stateMask) do
496
+ state_mask = 0
497
+ state_mask |= SWTProxy[:alt] if event.alt_key
498
+ state_mask |= SWTProxy[:ctrl] if event.ctrl_key
499
+ state_mask |= SWTProxy[:shift] if event.shift_key
500
+ state_mask |= SWTProxy[:command] if event.meta_key
501
+ state_mask
502
+ end
503
+ event.define_singleton_method(:state_mask, &event.method(:stateMask))
504
+ doit = true
505
+ event.define_singleton_method(:doit=) do |value|
506
+ doit = value
507
+ end
508
+ event.define_singleton_method(:doit) { doit }
509
+ event_listener.call(event)
510
+
511
+ # TODO Fix doit false, it's not stopping input
512
+ unless doit
513
+ event.prevent
514
+ event.prevent_default
515
+ event.stop_propagation
516
+ event.stop_immediate_propagation
517
+ end
518
+
519
+ doit
520
+ }
521
+ }
522
+ },
523
+ 'on_swt_keydown' => [
524
+ {
525
+ event: 'keypress',
526
+ event_handler: -> (event_listener) {
527
+ -> (event) {
528
+ event.define_singleton_method(:keyLocation) do
529
+ location = `#{event.to_n}.originalEvent.location`
530
+ JS_LOCATION_TO_SWT_KEY_LOCATION_MAP[location] || location
531
+ end
532
+ event.define_singleton_method(:key_location, &event.method(:keyLocation))
533
+ event.define_singleton_method(:keyCode) {
534
+ JS_KEY_CODE_TO_SWT_KEY_CODE_MAP[event.which] || event.which
535
+ }
536
+ event.define_singleton_method(:key_code, &event.method(:keyCode))
537
+ event.define_singleton_method(:widget) {myself}
538
+ event.define_singleton_method(:character) {event.which.chr}
539
+ event.define_singleton_method(:stateMask) do
540
+ state_mask = 0
541
+ state_mask |= SWTProxy[:alt] if event.alt_key
542
+ state_mask |= SWTProxy[:ctrl] if event.ctrl_key
543
+ state_mask |= SWTProxy[:shift] if event.shift_key
544
+ state_mask |= SWTProxy[:command] if event.meta_key
545
+ state_mask
546
+ end
547
+ event.define_singleton_method(:state_mask, &event.method(:stateMask))
548
+ doit = true
549
+ event.define_singleton_method(:doit=) do |value|
550
+ doit = value
551
+ end
552
+ event.define_singleton_method(:doit) { doit }
553
+ event_listener.call(event)
554
+
555
+ # TODO Fix doit false, it's not stopping input
556
+ unless doit
557
+ event.prevent
558
+ event.prevent_default
559
+ event.stop_propagation
560
+ event.stop_immediate_propagation
561
+ end
562
+
563
+ doit
564
+ }
565
+ }
566
+ },
567
+ {
568
+ event: 'keydown',
569
+ event_handler: -> (event_listener) {
570
+ -> (event) {
571
+ event.define_singleton_method(:keyLocation) do
572
+ location = `#{event.to_n}.originalEvent.location`
573
+ JS_LOCATION_TO_SWT_KEY_LOCATION_MAP[location] || location
574
+ end
575
+ event.define_singleton_method(:key_location, &event.method(:keyLocation))
576
+ event.define_singleton_method(:keyCode) {
577
+ JS_KEY_CODE_TO_SWT_KEY_CODE_MAP[event.which] || event.which
578
+ }
579
+ event.define_singleton_method(:key_code, &event.method(:keyCode))
580
+ event.define_singleton_method(:widget) {myself}
581
+ event.define_singleton_method(:character) {event.which.chr}
582
+ event.define_singleton_method(:stateMask) do
583
+ state_mask = 0
584
+ state_mask |= SWTProxy[:alt] if event.alt_key
585
+ state_mask |= SWTProxy[:ctrl] if event.ctrl_key
586
+ state_mask |= SWTProxy[:shift] if event.shift_key
587
+ state_mask |= SWTProxy[:command] if event.meta_key
588
+ state_mask
589
+ end
590
+ event.define_singleton_method(:state_mask, &event.method(:stateMask))
591
+ doit = true
592
+ event.define_singleton_method(:doit=) do |value|
593
+ doit = value
594
+ end
595
+ event.define_singleton_method(:doit) { doit }
596
+ event_listener.call(event) if event.which != 13 && (event.which == 127 || event.which <= 40)
597
+
598
+ # TODO Fix doit false, it's not stopping input
599
+ unless doit
600
+ event.prevent
601
+ event.prevent_default
602
+ event.stop_propagation
603
+ event.stop_immediate_propagation
604
+ end
605
+ doit
606
+ }
607
+ }
608
+ }
609
+ ],
610
+ 'on_swt_keyup' => {
611
+ event: 'keyup',
612
+ event_handler: -> (event_listener) {
613
+ -> (event) {
614
+ event.define_singleton_method(:keyLocation) do
615
+ location = `#{event.to_n}.originalEvent.location`
616
+ JS_LOCATION_TO_SWT_KEY_LOCATION_MAP[location] || location
617
+ end
618
+ event.define_singleton_method(:key_location, &event.method(:keyLocation))
619
+ event.define_singleton_method(:widget) {myself}
620
+ event.define_singleton_method(:keyCode) {
621
+ JS_KEY_CODE_TO_SWT_KEY_CODE_MAP[event.which] || event.which
622
+ }
623
+ event.define_singleton_method(:key_code, &event.method(:keyCode))
624
+ event.define_singleton_method(:character) {event.which.chr}
625
+ event.define_singleton_method(:stateMask) do
626
+ state_mask = 0
627
+ state_mask |= SWTProxy[:alt] if event.alt_key
628
+ state_mask |= SWTProxy[:ctrl] if event.ctrl_key
629
+ state_mask |= SWTProxy[:shift] if event.shift_key
630
+ state_mask |= SWTProxy[:command] if event.meta_key
631
+ state_mask
632
+ end
633
+ event.define_singleton_method(:state_mask, &event.method(:stateMask))
634
+ doit = true
635
+ event.define_singleton_method(:doit=) do |value|
636
+ doit = value
637
+ end
638
+ event.define_singleton_method(:doit) { doit }
639
+ event_listener.call(event)
640
+
641
+ # TODO Fix doit false, it's not stopping input
642
+ unless doit
643
+ event.prevent
644
+ event.prevent_default
645
+ event.stop_propagation
646
+ event.stop_immediate_propagation
647
+ end
648
+
649
+ doit
650
+ }
651
+ }
652
+ },
653
+ }
654
+ end
655
+
656
+ def name
657
+ self.class.name.split('::').last.underscore.sub(/_proxy$/, '').gsub('_', '-')
658
+ end
659
+
660
+ def id
661
+ return options[:id] if options.include?(:id)
662
+ @id ||= "#{name}-#{ElementProxy.next_id_number_for(name)}"
663
+ end
664
+
665
+ # Sets id explicitly. Useful in cases of wanting to maintain a stable id
666
+ def id=(value)
667
+ # TODO delete this method if it is not needed and isn't accurate in what it does
668
+ @id = value
669
+ end
670
+
671
+ # Subclasses can override with their own selector
672
+ def selector
673
+ "#{name}##{id}"
674
+ end
675
+
676
+ def add_css_class(css_class)
677
+ dom_element.add_class(css_class)
678
+ end
679
+
680
+ def add_css_classes(css_classes_to_add)
681
+ css_classes_to_add.each {|css_class| add_css_class(css_class)}
682
+ end
683
+
684
+ def remove_css_class(css_class)
685
+ dom_element.remove_class(css_class)
686
+ end
687
+
688
+ def remove_css_classes(css_classes_to_remove)
689
+ css_classes_to_remove.each {|css_class| remove_css_class(css_class)}
690
+ end
691
+
692
+ def clear_css_classes
693
+ css_classes.each {|css_class| remove_css_class(css_class)}
694
+ end
695
+
696
+ def has_style?(symbol)
697
+ @args.include?(symbol) # not a very solid implementation. Bring SWT constants eventually
698
+ end
699
+
700
+ def dom_element
701
+ # TODO consider making this pick an element in relation to its parent, allowing unhooked dom elements to be built if needed (unhooked to the visible page dom)
702
+ Document.find(path)
703
+ end
704
+
705
+ # TODO consider adding a default #dom method implementation for the common case, automatically relying on #element and other methods to build the dom html
706
+
707
+ def style_element
708
+ style_element_id = "#{id}-style"
709
+ style_element_selector = "style##{style_element_id}"
710
+ element = dom_element.find(style_element_selector)
711
+ if element.empty?
712
+ new_element = Element.new(:style)
713
+ new_element.attr('id', style_element_id)
714
+ new_element.attr('class', "#{name.gsub('_', '-')}-instance-style widget-instance-style")
715
+ dom_element.prepend(new_element)
716
+ element = dom_element.find(style_element_selector)
717
+ end
718
+ element
719
+ end
720
+
721
+ def listener_path
722
+ path
723
+ end
724
+
725
+ def listener_dom_element
726
+ Document.find(listener_path)
727
+ end
728
+
729
+ def observation_requests
730
+ @observation_requests ||= {}
731
+ end
732
+
733
+ def event_listener_proxies
734
+ @event_listener_proxies ||= []
735
+ end
736
+
737
+ def suspend_event_handling
738
+ @event_handling_suspended = true
739
+ end
740
+
741
+ def resume_event_handling
742
+ @event_handling_suspended = false
743
+ end
744
+
745
+ def event_handling_suspended?
746
+ @event_handling_suspended
747
+ end
748
+
749
+ def listeners
750
+ @listeners ||= {}
751
+ end
752
+
753
+ def listeners_for(listener_event)
754
+ listeners[listener_event.to_s] ||= []
755
+ end
756
+
757
+ def can_handle_observation_request?(observation_request)
758
+ # TODO sort this out for Opal
759
+ observation_request = observation_request.to_s
760
+ if observation_request.start_with?('on_swt_')
761
+ constant_name = observation_request.sub(/^on_swt_/, '')
762
+ SWTProxy.has_constant?(constant_name)
763
+ elsif observation_request.start_with?('on_')
764
+ # event = observation_request.sub(/^on_/, '')
765
+ # can_add_listener?(event) || can_handle_drag_observation_request?(observation_request) || can_handle_drop_observation_request?(observation_request)
766
+ true # TODO filter by valid listeners only in the future
767
+ end
768
+ end
769
+
770
+ def handle_observation_request(keyword, original_event_listener)
771
+ case keyword
772
+ when 'on_widget_removed'
773
+ listeners_for(keyword.sub(/^on_/, '')) << original_event_listener.to_proc
774
+ else
775
+ handle_javascript_observation_request(keyword, original_event_listener)
776
+ end
777
+ end
778
+
779
+ def handle_javascript_observation_request(keyword, original_event_listener)
780
+ return unless effective_observation_request_to_event_mapping.keys.include?(keyword)
781
+ event = nil
782
+ delegate = nil
783
+ effective_observation_request_to_event_mapping[keyword].to_collection.each do |mapping|
784
+ observation_requests[keyword] ||= Set.new
785
+ observation_requests[keyword] << original_event_listener
786
+ event = mapping[:event]
787
+ event_handler = mapping[:event_handler]
788
+ event_element_css_selector = mapping[:event_element_css_selector]
789
+ potential_event_listener = event_handler&.call(original_event_listener)
790
+ event_listener = potential_event_listener || original_event_listener
791
+ async_event_listener = proc do |event|
792
+ # TODO look into the issue with using async::task.new here. maybe put it in event listener (like not being able to call preventDefault or return false successfully )
793
+ # maybe consider pushing inside the widget classes instead where needed only or implement universal doit support correctly to bypass this issue
794
+ # Async::Task.new do
795
+ @@widget_handling_listener = self
796
+ # TODO also make sure to disable all widgets for suspension
797
+ event_listener.call(event) unless dialog_ancestor&.event_handling_suspended?
798
+ @widget_handling_listener = nil
799
+ # end
800
+ end
801
+ the_listener_dom_element = event_element_css_selector ? Element[event_element_css_selector] : listener_dom_element
802
+ unless the_listener_dom_element.empty?
803
+ the_listener_dom_element.on(event, &async_event_listener)
804
+ # TODO ensure uniqueness of insertion (perhaps adding equals/hash method to event listener proxy)
805
+
806
+ event_listener_proxies << EventListenerProxy.new(element_proxy: self, selector: selector, dom_element: the_listener_dom_element, event: event, listener: async_event_listener, original_event_listener: original_event_listener)
807
+ end
808
+ end
809
+ end
810
+
811
+ def remove_event_listener_proxies
812
+ event_listener_proxies.each do |event_listener_proxy|
813
+ event_listener_proxy.unregister
814
+ end
815
+ event_listener_proxies.clear
816
+ end
817
+
818
+ def add_observer(observer, property_name)
819
+ property_listener_installers = self.class&.ancestors&.to_a.map {|ancestor| widget_property_listener_installers[ancestor]}.compact
820
+ widget_listener_installers = property_listener_installers.map{|installer| installer[property_name.to_s.to_sym]}.compact if !property_listener_installers.empty?
821
+ widget_listener_installers.to_a.each do |widget_listener_installer|
822
+ widget_listener_installer.call(observer)
823
+ end
824
+ end
825
+
826
+ def set_attribute(attribute_name, *args)
827
+ apply_property_type_converters(attribute_name, args)
828
+ super(attribute_name, *args) # PropertyOwner
829
+ end
830
+
831
+ def method_missing(method, *args, &block)
832
+ if method.to_s.start_with?('on_')
833
+ handle_observation_request(method, block)
834
+ else
835
+ super(method, *args, &block)
836
+ end
837
+ end
838
+
839
+ def swt_widget
840
+ # only added for compatibility/adaptibility with Glimmer DSL for SWT
841
+ self
842
+ end
843
+
844
+ def apply_property_type_converters(attribute_name, args)
845
+ if args.count == 1
846
+ value = args.first
847
+ converter = property_type_converters[attribute_name.to_sym]
848
+ args[0] = converter.call(value) if converter
849
+ end
850
+ # if args.count == 1 && args.first.is_a?(ColorProxy)
851
+ # g_color = args.first
852
+ # args[0] = g_color.swt_color
853
+ # end
854
+ end
855
+
856
+ def property_type_converters
857
+ color_converter = proc do |value|
858
+ if value.is_a?(Symbol) || value.is_a?(String)
859
+ ColorProxy.new(value)
860
+ else
861
+ value
862
+ end
863
+ end
864
+ @property_type_converters ||= {
865
+ :background => color_converter,
866
+ # :background_image => proc do |value|
867
+ # if value.is_a?(String)
868
+ # if value.start_with?('uri:classloader')
869
+ # value = value.sub(/^uri\:classloader\:\//, '')
870
+ # object = java.lang.Object.new
871
+ # value = object.java_class.resource_as_stream(value)
872
+ # value = java.io.BufferedInputStream.new(value)
873
+ # end
874
+ # image_data = ImageData.new(value)
875
+ # on_event_Resize do |resize_event|
876
+ # new_image_data = image_data.scaledTo(@swt_widget.getSize.x, @swt_widget.getSize.y)
877
+ # @swt_widget.getBackgroundImage&.remove
878
+ # @swt_widget.setBackgroundImage(Image.new(@swt_widget.getDisplay, new_image_data))
879
+ # end
880
+ # Image.new(@swt_widget.getDisplay, image_data)
881
+ # else
882
+ # value
883
+ # end
884
+ # end,
885
+ :foreground => color_converter,
886
+ # :font => proc do |value|
887
+ # if value.is_a?(Hash)
888
+ # font_properties = value
889
+ # FontProxy.new(self, font_properties).swt_font
890
+ # else
891
+ # value
892
+ # end
893
+ # end,
894
+ :text => proc do |value|
895
+ # if swt_widget.is_a?(Browser)
896
+ # value.to_s
897
+ # else
898
+ value.to_s
899
+ # end
900
+ end,
901
+ # :visible => proc do |value|
902
+ # !!value
903
+ # end,
904
+ }
905
+ end
906
+
907
+ def widget_property_listener_installers
908
+ @swt_widget_property_listener_installers ||= {
909
+ # WidgetProxy => {
910
+ # :focus => proc do |observer|
911
+ # on_focus_gained { |focus_event|
912
+ # observer.call(true)
913
+ # }
914
+ # on_focus_lost { |focus_event|
915
+ # observer.call(false)
916
+ # }
917
+ # end,
918
+ # },
919
+ MenuItemProxy => {
920
+ :selection => proc do |observer|
921
+ on_widget_selected { |selection_event|
922
+ # TODO look into validity of this and perhaps move toggle logic to MenuItemProxy
923
+ if check?
924
+ observer.call(!selection)
925
+ else
926
+ observer.call(selection)
927
+ end
928
+ }
929
+ end
930
+ },
931
+ ScaleProxy => {
932
+ :selection => proc do |observer|
933
+ on_widget_selected { |selection_event|
934
+ observer.call(selection)
935
+ }
936
+ end
937
+ },
938
+ SliderProxy => {
939
+ :selection => proc do |observer|
940
+ on_widget_selected { |selection_event|
941
+ observer.call(selection)
942
+ }
943
+ end
944
+ },
945
+ SpinnerProxy => {
946
+ :selection => proc do |observer|
947
+ on_widget_selected { |selection_event|
948
+ observer.call(selection)
949
+ }
950
+ end
951
+ },
952
+ TextProxy => {
953
+ :text => proc do |observer|
954
+ on_modify_text { |modify_event|
955
+ observer.call(text)
956
+ }
957
+ end,
958
+ # :caret_position => proc do |observer|
959
+ # on_event_keydown { |event|
960
+ # observer.call(getCaretPosition)
961
+ # }
962
+ # on_event_keyup { |event|
963
+ # observer.call(getCaretPosition)
964
+ # }
965
+ # on_event_mousedown { |event|
966
+ # observer.call(getCaretPosition)
967
+ # }
968
+ # on_event_mouseup { |event|
969
+ # observer.call(getCaretPosition)
970
+ # }
971
+ # end,
972
+ # :selection => proc do |observer|
973
+ # on_event_keydown { |event|
974
+ # observer.call(getSelection)
975
+ # }
976
+ # on_event_keyup { |event|
977
+ # observer.call(getSelection)
978
+ # }
979
+ # on_event_mousedown { |event|
980
+ # observer.call(getSelection)
981
+ # }
982
+ # on_event_mouseup { |event|
983
+ # observer.call(getSelection)
984
+ # }
985
+ # end,
986
+ # :selection_count => proc do |observer|
987
+ # on_event_keydown { |event|
988
+ # observer.call(getSelectionCount)
989
+ # }
990
+ # on_event_keyup { |event|
991
+ # observer.call(getSelectionCount)
992
+ # }
993
+ # on_event_mousedown { |event|
994
+ # observer.call(getSelectionCount)
995
+ # }
996
+ # on_event_mouseup { |event|
997
+ # observer.call(getSelectionCount)
998
+ # }
999
+ # end,
1000
+ # :top_index => proc do |observer|
1001
+ # @last_top_index = getTopIndex
1002
+ # on_paint_control { |event|
1003
+ # if getTopIndex != @last_top_index
1004
+ # @last_top_index = getTopIndex
1005
+ # observer.call(@last_top_index)
1006
+ # end
1007
+ # }
1008
+ # end,
1009
+ },
1010
+ # Java::OrgEclipseSwtCustom::StyledText => {
1011
+ # :text => proc do |observer|
1012
+ # on_modify_text { |modify_event|
1013
+ # observer.call(getText)
1014
+ # }
1015
+ # end,
1016
+ # },
1017
+ DateTimeProxy => {
1018
+ :date_time => proc do |observer|
1019
+ on_widget_selected { |selection_event|
1020
+ observer.call(date_time)
1021
+ }
1022
+ end
1023
+ },
1024
+ RadioProxy => { #radio?
1025
+ :selection => proc do |observer|
1026
+ on_widget_selected { |selection_event|
1027
+ observer.call(selection)
1028
+ }
1029
+ end
1030
+ },
1031
+ TableProxy => {
1032
+ :selection => proc do |observer|
1033
+ on_widget_selected { |selection_event|
1034
+ observer.call(selection_event.table_item.get_data) # TODO ensure selection doesn't conflict with editing
1035
+ }
1036
+ end,
1037
+ },
1038
+ # Java::OrgEclipseSwtWidgets::MenuItem => {
1039
+ # :selection => proc do |observer|
1040
+ # on_widget_selected { |selection_event|
1041
+ # observer.call(getSelection)
1042
+ # }
1043
+ # end
1044
+ # },
1045
+ }
1046
+ end
1047
+
1048
+ private
1049
+
1050
+ def css_cursor
1051
+ SWT_CURSOR_TO_CSS_CURSOR_MAP[@cursor]
1052
+ end
1053
+
1054
+ end
1055
+ end
1056
+ end
1057
+
1058
+ require 'glimmer/dsl/web/element_expression'