glimmer 0.9.1 → 0.9.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +15 -14
- data/VERSION +1 -1
- data/lib/glimmer/data_binding/model_binding.rb +248 -0
- data/lib/glimmer/data_binding/observable.rb +21 -0
- data/lib/glimmer/data_binding/observable_array.rb +86 -0
- data/lib/glimmer/data_binding/observable_model.rb +104 -0
- data/lib/glimmer/data_binding/observer.rb +127 -0
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2d387850f085c102bcaf51d9f9f1250cf02124372beed08612bc2dde2766f387
|
4
|
+
data.tar.gz: 828032e8a3d6779e47d63c054cf15211ec8b989a58af47ba1aedfed36e696b67
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5e7d349b48f19af94bab354eceef736e6c58e9d4e608582fb12c1677c453dfff5ee02c74a968133e985239a18f0058f66b35a4199fc4c3ac63b29cb0d80a7f91
|
7
|
+
data.tar.gz: 2d4f7f4600517ca0f6e9f08946b37f7aaff9740dc3f49b1844147d6635cd4cce053af200e1c58c0a5eac8523d4cce2f9601ead454665049919954498d697c32e
|
data/README.md
CHANGED
@@ -1,12 +1,19 @@
|
|
1
|
-
# Glimmer - Ruby Desktop Development GUI Library
|
1
|
+
# <img src="https://raw.githubusercontent.com/AndyObtiva/glimmer/master/images/glimmer-logo-hi-res.png" height=105 /> Glimmer - Ruby Desktop Development GUI Library
|
2
2
|
[![Gem Version](https://badge.fury.io/rb/glimmer.svg)](http://badge.fury.io/rb/glimmer)
|
3
|
-
[![Travis CI](https://travis-ci.com/AndyObtiva/glimmer.svg?branch=master)](https://travis-ci.com/github/AndyObtiva/glimmer)
|
3
|
+
[![Travis CI](https://travis-ci.com/AndyObtiva/glimmer.svg?branch=master)](https://travis-ci.com/github/AndyObtiva/glimmer)
|
4
|
+
[![Join the chat at https://gitter.im/AndyObtiva/glimmer](https://badges.gitter.im/AndyObtiva/glimmer.svg)](https://gitter.im/AndyObtiva/glimmer?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
4
5
|
|
5
6
|
Glimmer is a native-GUI cross-platform desktop development library written in Ruby. Glimmer's main innovation is a JRuby DSL that enables productive and efficient authoring of desktop application user-interfaces while relying on the robust Eclipse SWT library. Glimmer additionally innovates by having built-in data-binding support to greatly facilitate synchronizing the GUI with domain models. As a result, that achieves true decoupling of object oriented components, enabling developers to solve business problems without worrying about GUI concerns, or alternatively drive development GUI-first, and then write clean business models test-first afterwards.
|
6
7
|
|
7
8
|
[<img src="https://covers.oreillystatic.com/images/9780596519650/lrg.jpg" width=105 /><br />
|
8
9
|
Featured in<br />JRuby Cookbook](http://shop.oreilly.com/product/9780596519650.do)
|
9
10
|
|
11
|
+
Glimmer DSL gems:
|
12
|
+
- [glimmer-dsl-swt](https://github.com/AndyObtiva/glimmer-dsl-swt): Glimmer DSL for SWT (Desktop GUI)
|
13
|
+
- [glimmer-dsl-opal](https://github.com/AndyObtiva/glimmer-dsl-opal): Glimmer DSL for Opal (Web GUI Adapter for Desktop Apps)
|
14
|
+
- [glimmer-dsl-xml](https://github.com/AndyObtiva/glimmer-dsl-xml): Glimmer DSL for XML (& HTML)
|
15
|
+
- [glimmer-dsl-css](https://github.com/AndyObtiva/glimmer-dsl-css): Glimmer DSL for CSS (Cascading Style Sheets)
|
16
|
+
|
10
17
|
## Examples
|
11
18
|
|
12
19
|
### Hello, World!
|
@@ -199,13 +206,13 @@ Glimmer might still work on lower versions of Java, JRuby and SWT, but there are
|
|
199
206
|
|
200
207
|
## Setup
|
201
208
|
|
202
|
-
Please follow these instructions to make the `glimmer` command available on your system.
|
209
|
+
Please follow these instructions to make the `glimmer` command available on your system via the [`glimmer-dsl-swt`](https://github.com/AndyObtiva/glimmer-dsl-swt) gem.
|
203
210
|
|
204
211
|
### Option 1: Direct Install
|
205
212
|
|
206
213
|
Run this command to install directly:
|
207
214
|
```
|
208
|
-
jgem install glimmer-dsl-swt -v 0.1.
|
215
|
+
jgem install glimmer-dsl-swt -v 0.1.2
|
209
216
|
```
|
210
217
|
|
211
218
|
`jgem` is JRuby's version of `gem` command.
|
@@ -216,7 +223,7 @@ Otherwise, you may also run `jruby -S gem install ...`
|
|
216
223
|
|
217
224
|
Add the following to `Gemfile`:
|
218
225
|
```
|
219
|
-
gem 'glimmer-dsl-swt', '~> 0.1.
|
226
|
+
gem 'glimmer-dsl-swt', '~> 0.1.2'
|
220
227
|
```
|
221
228
|
|
222
229
|
And, then run:
|
@@ -224,7 +231,7 @@ And, then run:
|
|
224
231
|
jruby -S bundle install
|
225
232
|
```
|
226
233
|
|
227
|
-
You may learn more about other Glimmer related gems at [Multi-DSL Support](#multi-dsl-support)
|
234
|
+
You may learn more about other Glimmer related gems ([`glimmer-dsl-opal`](https://github.com/AndyObtiva/glimmer-dsl-opal), [`glimmer-dsl-xml`](https://github.com/AndyObtiva/glimmer-dsl-xml), and [`glimmer-dsl-css`](https://github.com/AndyObtiva/glimmer-dsl-css)) at [Multi-DSL Support](#multi-dsl-support)
|
228
235
|
|
229
236
|
## Glimmer Command
|
230
237
|
|
@@ -2391,10 +2398,6 @@ By the way, keep in mind that during normal operation, it does also indicate a f
|
|
2391
2398
|
Exec failed with code 2 command [[/usr/bin/SetFile, -c, icnC, /var/folders/4_/g1sw__tx6mjdgyh3mky7vydc0000gp/T/fxbundler4076750801763032201/images/MathBowling/.VolumeIcon.icns] in unspecified directory
|
2392
2399
|
```
|
2393
2400
|
|
2394
|
-
## Limitations
|
2395
|
-
|
2396
|
-
- Glimmer apps have a long startup up time and can take anywhere between 12 and 30 seconds to start depending on the app and the machine. Once started, Glimmer apps run very fast and perform very responsively since Ruby is more than fast and capable for handling desktop GUIs. In any case, one idea to address this limitation is to include a background service (daemon) that can launch Glimmer apps instantly or near instantly by keeping a JRuby runtime started and ready at all times.
|
2397
|
-
|
2398
2401
|
## Resources
|
2399
2402
|
|
2400
2403
|
* [Code Master Blog](http://andymaleh.blogspot.com/search/label/Glimmer)
|
@@ -2412,11 +2415,9 @@ You may submit [issues](https://github.com/AndyObtiva/glimmer/issues) on [GitHub
|
|
2412
2415
|
|
2413
2416
|
[Click here to submit an issue.](https://github.com/AndyObtiva/glimmer/issues)
|
2414
2417
|
|
2415
|
-
###
|
2416
|
-
|
2417
|
-
If you need live help, try the [#glimmer](http://widget.mibbit.com/?settings=7514b8a196f8f1de939a351245db7aa8&server=irc.mibbit.net&channel=%23glimmer) IRC channel on [irc.mibbit.net](http://widget.mibbit.com/?settings=7514b8a196f8f1de939a351245db7aa8&server=irc.mibbit.net&channel=%23glimmer). If no one was available, you may [leave a GitHub issue](https://github.com/AndyObtiva/glimmer/issues) to schedule a meetup on IRC.
|
2418
|
+
### Chat
|
2418
2419
|
|
2419
|
-
|
2420
|
+
If you need live help, try to [![Join the chat at https://gitter.im/AndyObtiva/glimmer](https://badges.gitter.im/AndyObtiva/glimmer.svg)](https://gitter.im/AndyObtiva/glimmer?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
2420
2421
|
|
2421
2422
|
## Feature Suggestions
|
2422
2423
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.9.
|
1
|
+
0.9.2
|
@@ -0,0 +1,248 @@
|
|
1
|
+
require 'glimmer/data_binding/observable'
|
2
|
+
require 'glimmer/data_binding/observer'
|
3
|
+
|
4
|
+
module Glimmer
|
5
|
+
module DataBinding
|
6
|
+
class ModelBinding
|
7
|
+
include Observable
|
8
|
+
include Observer
|
9
|
+
|
10
|
+
attr_reader :binding_options
|
11
|
+
|
12
|
+
def initialize(base_model, property_name_expression, binding_options = nil)
|
13
|
+
@base_model = base_model
|
14
|
+
@property_name_expression = property_name_expression
|
15
|
+
@binding_options = binding_options || {}
|
16
|
+
if computed?
|
17
|
+
@computed_model_bindings = computed_by.map do |computed_by_property_expression|
|
18
|
+
self.class.new(base_model, computed_by_property_expression)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def model
|
24
|
+
nested_property? ? nested_model : base_model
|
25
|
+
end
|
26
|
+
|
27
|
+
# e.g. person.address.state returns [person, person.address]
|
28
|
+
def nested_models
|
29
|
+
@nested_models = [base_model]
|
30
|
+
model_property_names.reduce(base_model) do |reduced_model, nested_model_property_name|
|
31
|
+
if reduced_model.nil?
|
32
|
+
nil
|
33
|
+
else
|
34
|
+
invoke_property_reader(reduced_model, nested_model_property_name).tap do |new_reduced_model|
|
35
|
+
@nested_models << new_reduced_model
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
@nested_models
|
40
|
+
end
|
41
|
+
|
42
|
+
def nested_model
|
43
|
+
nested_models.last
|
44
|
+
end
|
45
|
+
|
46
|
+
def base_model
|
47
|
+
@base_model
|
48
|
+
end
|
49
|
+
|
50
|
+
def property_name
|
51
|
+
nested_property? ? nested_property_name : property_name_expression
|
52
|
+
end
|
53
|
+
|
54
|
+
def convert_on_read(value)
|
55
|
+
apply_converter(@binding_options[:on_read], value)
|
56
|
+
end
|
57
|
+
|
58
|
+
def convert_on_write(value)
|
59
|
+
apply_converter(@binding_options[:on_write], value)
|
60
|
+
end
|
61
|
+
|
62
|
+
def apply_converter(converter, value)
|
63
|
+
if converter.nil?
|
64
|
+
value
|
65
|
+
elsif converter.is_a?(String) || converter.is_a?(Symbol)
|
66
|
+
if value.respond_to?(converter)
|
67
|
+
value.send(converter)
|
68
|
+
else
|
69
|
+
raise Glimmer::Error, "Unsupported bind converter: #{converter.inspect}"
|
70
|
+
end
|
71
|
+
elsif converter.respond_to?(:call, value)
|
72
|
+
converter.call(value)
|
73
|
+
else
|
74
|
+
raise Glimmer::Error, "Unsupported bind converter: #{converter.inspect}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# All nested property names
|
79
|
+
# e.g. property name expression "address.state" gives ['address', 'state']
|
80
|
+
# If there are any indexed property names, this returns indexes as properties.
|
81
|
+
# e.g. property name expression "addresses[1].state" gives ['addresses', '[1]', 'state']
|
82
|
+
def nested_property_names
|
83
|
+
@nested_property_names ||= property_name_expression.split(".").map {|pne| pne.match(/([^\[]+)(\[[^\]]+\])?/).to_a.drop(1)}.flatten.compact
|
84
|
+
end
|
85
|
+
|
86
|
+
# Final nested property name
|
87
|
+
# e.g. property name expression "address.state" gives :state
|
88
|
+
def nested_property_name
|
89
|
+
nested_property_names.last
|
90
|
+
end
|
91
|
+
|
92
|
+
# Model representing nested property names
|
93
|
+
# e.g. property name expression "address.state" gives [:address]
|
94
|
+
def model_property_names
|
95
|
+
nested_property_names[0...-1]
|
96
|
+
end
|
97
|
+
|
98
|
+
def nested_property?
|
99
|
+
property_name_expression.match(/[.\[]/)
|
100
|
+
end
|
101
|
+
|
102
|
+
def property_name_expression
|
103
|
+
@property_name_expression
|
104
|
+
end
|
105
|
+
|
106
|
+
def computed?
|
107
|
+
!computed_by.empty?
|
108
|
+
end
|
109
|
+
|
110
|
+
def computed_by
|
111
|
+
[@binding_options[:computed_by]].flatten.compact
|
112
|
+
end
|
113
|
+
|
114
|
+
def nested_property_observers_for(observer)
|
115
|
+
@nested_property_observers_collection ||= {}
|
116
|
+
unless @nested_property_observers_collection.has_key?(observer)
|
117
|
+
@nested_property_observers_collection[observer] = nested_property_names.reduce({}) do |output, property_name|
|
118
|
+
output.merge(
|
119
|
+
property_name => Observer.proc do |new_value|
|
120
|
+
# Ensure reattaching observers when a higher level nested property is updated (e.g. person.address changes reattaches person.address.street observer)
|
121
|
+
add_observer(observer)
|
122
|
+
observer.call(evaluate_property)
|
123
|
+
end
|
124
|
+
)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
@nested_property_observers_collection[observer]
|
128
|
+
end
|
129
|
+
|
130
|
+
def add_observer(observer)
|
131
|
+
if computed?
|
132
|
+
add_computed_observers(observer)
|
133
|
+
elsif nested_property?
|
134
|
+
add_nested_observers(observer)
|
135
|
+
else
|
136
|
+
model_binding_observer = Observer.proc do |new_value|
|
137
|
+
observer.call(evaluate_property)
|
138
|
+
end
|
139
|
+
observer_registration = model_binding_observer.observe(model, property_name)
|
140
|
+
my_registration = observer.registration_for(self)
|
141
|
+
observer.add_dependent(my_registration => observer_registration)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def remove_observer(observer)
|
146
|
+
if computed?
|
147
|
+
@computed_model_bindings.each do |computed_model_binding|
|
148
|
+
computed_observer_for(observer).unobserve(computed_model_binding)
|
149
|
+
end
|
150
|
+
@computed_observer_collection.delete(observer)
|
151
|
+
elsif nested_property?
|
152
|
+
nested_property_observers_for(observer).clear
|
153
|
+
else
|
154
|
+
observer.unobserve(model, property_name)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def computed_observer_for(observer)
|
159
|
+
@computed_observer_collection ||= {}
|
160
|
+
unless @computed_observer_collection.has_key?(observer)
|
161
|
+
@computed_observer_collection[observer] = Observer.proc do |new_value|
|
162
|
+
observer.call(evaluate_property)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
@computed_observer_collection[observer]
|
166
|
+
end
|
167
|
+
|
168
|
+
def add_computed_observers(observer)
|
169
|
+
@computed_model_bindings.each do |computed_model_binding|
|
170
|
+
observer_registration = computed_observer_for(observer).observe(computed_model_binding)
|
171
|
+
my_registration = observer.registration_for(self)
|
172
|
+
observer.add_dependent(my_registration => observer_registration)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def add_nested_observers(observer)
|
177
|
+
nested_property_observers = nested_property_observers_for(observer)
|
178
|
+
nested_models.zip(nested_property_names).each_with_index do |zip, i|
|
179
|
+
model, property_name = zip
|
180
|
+
nested_property_observer = nested_property_observers[property_name]
|
181
|
+
previous_index = i - 1
|
182
|
+
parent_model = previous_index.negative? ? self : nested_models[previous_index]
|
183
|
+
parent_property_name = previous_index.negative? ? nil : nested_property_names[previous_index]
|
184
|
+
parent_observer = previous_index.negative? ? observer : nested_property_observers[parent_property_name]
|
185
|
+
parent_property_name = nil if parent_property_name.to_s.start_with?('[')
|
186
|
+
unless model.nil?
|
187
|
+
if property_indexed?(property_name)
|
188
|
+
# TODO figure out a way to deal with this more uniformly
|
189
|
+
observer_registration = nested_property_observer.observe(model)
|
190
|
+
else
|
191
|
+
observer_registration = nested_property_observer.observe(model, property_name)
|
192
|
+
end
|
193
|
+
parent_registration = parent_observer.registration_for(parent_model, parent_property_name)
|
194
|
+
parent_observer.add_dependent(parent_registration => observer_registration)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def call(value)
|
200
|
+
return if model.nil?
|
201
|
+
converted_value = value
|
202
|
+
invoke_property_writer(model, "#{property_name}=", converted_value) unless evaluate_property == converted_value
|
203
|
+
end
|
204
|
+
|
205
|
+
def evaluate_property
|
206
|
+
value = nil
|
207
|
+
value = invoke_property_reader(model, property_name) unless model.nil?
|
208
|
+
convert_on_read(value)
|
209
|
+
end
|
210
|
+
|
211
|
+
def evaluate_options_property
|
212
|
+
model.send(options_property_name) unless model.nil?
|
213
|
+
end
|
214
|
+
|
215
|
+
def options_property_name
|
216
|
+
self.property_name + "_options"
|
217
|
+
end
|
218
|
+
|
219
|
+
def property_indexed?(property_expression)
|
220
|
+
property_expression.to_s.start_with?('[')
|
221
|
+
end
|
222
|
+
|
223
|
+
def invoke_property_reader(object, property_expression)
|
224
|
+
if property_indexed?(property_expression)
|
225
|
+
property_method = '[]'
|
226
|
+
property_argument = property_expression[1...-1]
|
227
|
+
property_argument = property_argument.to_i if property_argument.match(/\d+/)
|
228
|
+
object.send(property_method, property_argument)
|
229
|
+
else
|
230
|
+
object.send(property_expression)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def invoke_property_writer(object, property_expression, value)
|
235
|
+
return if @binding_options[:read_only]
|
236
|
+
value = convert_on_write(value)
|
237
|
+
if property_indexed?(property_expression)
|
238
|
+
property_method = '[]='
|
239
|
+
property_argument = property_expression[1...-2]
|
240
|
+
property_argument = property_argument.to_i if property_argument.match(/\d+/)
|
241
|
+
object.send(property_method, property_argument, value)
|
242
|
+
else
|
243
|
+
object.send(property_expression, value)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'glimmer/error'
|
2
|
+
|
3
|
+
module Glimmer
|
4
|
+
module DataBinding
|
5
|
+
module Observable
|
6
|
+
# TODO rename methods to observe/unobserve
|
7
|
+
def add_observer(observer, property_or_properties=nil)
|
8
|
+
raise Error, 'Not implemented!'
|
9
|
+
end
|
10
|
+
|
11
|
+
def remove_observer(observer, property_or_properties=nil)
|
12
|
+
raise Error, 'Not implemented!'
|
13
|
+
end
|
14
|
+
|
15
|
+
# Overriding inspect to avoid printing very long observer hierarchies
|
16
|
+
def inspect
|
17
|
+
"#<#{self.class.name}:0x#{self.hash.to_s(16)}>"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
require 'glimmer/data_binding/observable'
|
4
|
+
|
5
|
+
module Glimmer
|
6
|
+
module DataBinding
|
7
|
+
# TODO prefix utility methods with double-underscore
|
8
|
+
module ObservableArray
|
9
|
+
include Observable
|
10
|
+
|
11
|
+
def add_observer(observer, element_properties=nil)
|
12
|
+
return observer if has_observer?(observer) && element_properties.nil?
|
13
|
+
property_observer_list << observer
|
14
|
+
[element_properties].flatten.compact.each do |property|
|
15
|
+
each do |element|
|
16
|
+
observer.observe(element, property)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
observer
|
20
|
+
end
|
21
|
+
|
22
|
+
def remove_observer(observer, element_properties=nil)
|
23
|
+
property_observer_list.delete(observer)
|
24
|
+
[element_properties].flatten.compact.each do |property|
|
25
|
+
each do |element|
|
26
|
+
observer.unobserve(element, property)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
observer
|
30
|
+
end
|
31
|
+
|
32
|
+
def has_observer?(observer)
|
33
|
+
property_observer_list.include?(observer)
|
34
|
+
end
|
35
|
+
|
36
|
+
def property_observer_list
|
37
|
+
@property_observer_list ||= Set.new
|
38
|
+
end
|
39
|
+
|
40
|
+
def notify_observers
|
41
|
+
property_observer_list.each {|observer| observer.call}
|
42
|
+
end
|
43
|
+
|
44
|
+
def <<(element)
|
45
|
+
super(element)
|
46
|
+
notify_observers
|
47
|
+
end
|
48
|
+
|
49
|
+
def []=(index, value)
|
50
|
+
old_value = self[index]
|
51
|
+
unregister_dependent_observers(old_value)
|
52
|
+
super(index, value)
|
53
|
+
notify_observers
|
54
|
+
end
|
55
|
+
|
56
|
+
def delete(element)
|
57
|
+
unregister_dependent_observers(element)
|
58
|
+
super(element)
|
59
|
+
notify_observers
|
60
|
+
end
|
61
|
+
|
62
|
+
def delete_at(index)
|
63
|
+
old_value = self[index]
|
64
|
+
unregister_dependent_observers(old_value)
|
65
|
+
super(index)
|
66
|
+
notify_observers
|
67
|
+
end
|
68
|
+
|
69
|
+
def clear
|
70
|
+
each do |old_value|
|
71
|
+
unregister_dependent_observers(old_value)
|
72
|
+
end
|
73
|
+
super()
|
74
|
+
notify_observers
|
75
|
+
end
|
76
|
+
|
77
|
+
def unregister_dependent_observers(old_value)
|
78
|
+
# TODO look into optimizing this
|
79
|
+
return unless old_value.is_a?(ObservableModel) || old_value.is_a?(ObservableArray)
|
80
|
+
property_observer_list.each do |observer|
|
81
|
+
observer.unregister_dependents_with_observable(observer.registration_for(self), old_value)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'glimmer/data_binding/observable'
|
2
|
+
require 'glimmer/data_binding/observer'
|
3
|
+
|
4
|
+
module Glimmer
|
5
|
+
module DataBinding
|
6
|
+
# TODO prefix utility methods with double-underscore
|
7
|
+
module ObservableModel
|
8
|
+
include Observable
|
9
|
+
|
10
|
+
class Notifier
|
11
|
+
include Observer
|
12
|
+
def initialize(observable_model, property_name)
|
13
|
+
@observable_model = observable_model
|
14
|
+
@property_name = property_name
|
15
|
+
end
|
16
|
+
def call(new_value=nil)
|
17
|
+
@observable_model.notify_observers(@property_name)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_observer(observer, property_name)
|
22
|
+
return observer if has_observer?(observer, property_name)
|
23
|
+
property_observer_list(property_name) << observer
|
24
|
+
add_property_writer_observers(property_name)
|
25
|
+
observer
|
26
|
+
end
|
27
|
+
|
28
|
+
def remove_observer(observer, property_name)
|
29
|
+
property_observer_list(property_name).delete(observer)
|
30
|
+
end
|
31
|
+
|
32
|
+
def has_observer?(observer, property_name)
|
33
|
+
property_observer_list(property_name).include?(observer)
|
34
|
+
end
|
35
|
+
|
36
|
+
def has_observer_for_any_property?(observer)
|
37
|
+
property_observer_hash.values.map(&:to_a).sum.include?(observer)
|
38
|
+
end
|
39
|
+
|
40
|
+
def property_observer_hash
|
41
|
+
@property_observers ||= Hash.new
|
42
|
+
end
|
43
|
+
|
44
|
+
def property_observer_list(property_name)
|
45
|
+
property_observer_hash[property_name.to_sym] = Set.new unless property_observer_hash[property_name.to_sym]
|
46
|
+
property_observer_hash[property_name.to_sym]
|
47
|
+
end
|
48
|
+
|
49
|
+
def notify_observers(property_name)
|
50
|
+
property_observer_list(property_name).each {|observer| observer.call(send(property_name))}
|
51
|
+
end
|
52
|
+
#TODO upon updating values, make sure dependent observers are cleared (not added as dependents here)
|
53
|
+
|
54
|
+
def add_property_writer_observers(property_name)
|
55
|
+
property_writer_name = "#{property_name}="
|
56
|
+
method(property_writer_name)
|
57
|
+
ensure_array_object_observer(property_name, send(property_name))
|
58
|
+
begin
|
59
|
+
method("__original_#{property_writer_name}")
|
60
|
+
rescue
|
61
|
+
old_method = self.class.instance_method(property_writer_name)
|
62
|
+
define_singleton_method("__original_#{property_writer_name}", old_method)
|
63
|
+
define_singleton_method(property_writer_name) do |value|
|
64
|
+
old_value = self.send(property_name)
|
65
|
+
unregister_dependent_observers(property_name, old_value)
|
66
|
+
self.send("__original_#{property_writer_name}", value)
|
67
|
+
notify_observers(property_name)
|
68
|
+
ensure_array_object_observer(property_name, value, old_value)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
rescue => e
|
72
|
+
# ignore writing if no property writer exists
|
73
|
+
Glimmer::Config.logger&.debug "No need to observe property writer: #{property_writer_name}\n#{e.message}\n#{e.backtrace.join("\n")}"
|
74
|
+
end
|
75
|
+
|
76
|
+
def unregister_dependent_observers(property_name, old_value)
|
77
|
+
# TODO look into optimizing this
|
78
|
+
return unless old_value.is_a?(ObservableModel) || old_value.is_a?(ObservableArray)
|
79
|
+
property_observer_list(property_name).each do |observer|
|
80
|
+
observer.unregister_dependents_with_observable(observer.registration_for(self, property_name), old_value)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def ensure_array_object_observer(property_name, object, old_object = nil)
|
85
|
+
return unless object.is_a?(Array)
|
86
|
+
array_object_observer = array_object_observer_for(property_name)
|
87
|
+
array_observer_registration = array_object_observer.observe(object)
|
88
|
+
property_observer_list(property_name).each do |observer|
|
89
|
+
my_registration = observer.registration_for(self, property_name) # TODO eliminate repetition
|
90
|
+
observer.add_dependent(my_registration => array_observer_registration)
|
91
|
+
end
|
92
|
+
array_object_observer_for(property_name).unregister(old_object) if old_object.is_a?(ObservableArray)
|
93
|
+
end
|
94
|
+
|
95
|
+
def array_object_observer_for(property_name)
|
96
|
+
@array_object_observers ||= {}
|
97
|
+
unless @array_object_observers.has_key?(property_name)
|
98
|
+
@array_object_observers[property_name] = ObservableModel::Notifier.new(self, property_name)
|
99
|
+
end
|
100
|
+
@array_object_observers[property_name]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'glimmer/error'
|
2
|
+
|
3
|
+
module Glimmer
|
4
|
+
module DataBinding
|
5
|
+
# Mixin representing Observer trait from Observer Design Pattern
|
6
|
+
# Allows classes to include without interfering with their
|
7
|
+
# inheritance hierarchy.
|
8
|
+
#
|
9
|
+
# Includes a default implementation that can receive an observer block
|
10
|
+
# Example: Observer.proc {|new_value| puts new_value}
|
11
|
+
# Subclasses may override
|
12
|
+
module Observer
|
13
|
+
# Observer Proc default implementation that takes an observer block to process updates
|
14
|
+
# via call method
|
15
|
+
class Proc
|
16
|
+
include Observer
|
17
|
+
|
18
|
+
def initialize(&observer_block)
|
19
|
+
@observer_block = observer_block
|
20
|
+
end
|
21
|
+
|
22
|
+
# Called by observables once updates occur sending in the new_value if any
|
23
|
+
def call(new_value=nil)
|
24
|
+
@observer_block.call(new_value)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Registration < Struct.new(:observer, :observable, :property, keyword_init: true)
|
29
|
+
def unregister
|
30
|
+
observer.unobserve(observable, property)
|
31
|
+
end
|
32
|
+
alias unobserve unregister
|
33
|
+
end
|
34
|
+
|
35
|
+
class << self
|
36
|
+
def proc(&observer_block)
|
37
|
+
Proc.new(&observer_block)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def registrations
|
42
|
+
@registrations ||= Set.new
|
43
|
+
end
|
44
|
+
|
45
|
+
def registration_for(observable, property = nil)
|
46
|
+
Registration.new(observer: self, observable: observable, property: property)
|
47
|
+
end
|
48
|
+
|
49
|
+
# mapping of registrations to dependents
|
50
|
+
# {[observable, property] => [[dependent, dependent_observable, dependent_property], ...]}
|
51
|
+
def dependents
|
52
|
+
@dependents ||= Hash.new
|
53
|
+
end
|
54
|
+
|
55
|
+
def dependents_for(registration)
|
56
|
+
dependents[registration] ||= Set.new
|
57
|
+
end
|
58
|
+
|
59
|
+
# registers observer in an observable on a property (optional)
|
60
|
+
# observer maintains registration list to unregister later
|
61
|
+
def register(observable, property = nil)
|
62
|
+
unless observable.is_a?(Observable)
|
63
|
+
# TODO refactor code to be more smart/polymorphic/automated and honor open/closed principle
|
64
|
+
if observable.is_a?(Array)
|
65
|
+
observable.extend(ObservableArray)
|
66
|
+
else
|
67
|
+
observable.extend(ObservableModel)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
observable.add_observer(*[self, property].compact)
|
71
|
+
registration_for(observable, property).tap do |registration|
|
72
|
+
self.registrations << registration
|
73
|
+
end
|
74
|
+
end
|
75
|
+
alias observe register
|
76
|
+
|
77
|
+
def unregister(observable, property = nil)
|
78
|
+
# TODO optimize performance in the future via indexing and/or making a registration official object/class
|
79
|
+
observable.remove_observer(*[self, property].compact)
|
80
|
+
registration = registration_for(observable, property)
|
81
|
+
dependents_for(registration).each do |dependent|
|
82
|
+
dependent.unregister
|
83
|
+
remove_dependent(registration => dependent)
|
84
|
+
end
|
85
|
+
registrations.delete(registration)
|
86
|
+
end
|
87
|
+
alias unobserve unregister
|
88
|
+
|
89
|
+
def unregister_dependents_with_observable(registration, dependent_observable)
|
90
|
+
thedependents = dependents_for(registration).select do |thedependent|
|
91
|
+
thedependent.observable == dependent_observable
|
92
|
+
end
|
93
|
+
thedependents.each do |thedependent|
|
94
|
+
thedependent.unregister
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# cleans up all registrations in observables
|
99
|
+
def unregister_all_observables
|
100
|
+
registrations.each do |registration|
|
101
|
+
registration.unregister
|
102
|
+
end
|
103
|
+
end
|
104
|
+
alias unobserve_all_observables unregister_all_observables
|
105
|
+
|
106
|
+
# add dependent observer to unregister when unregistering observer
|
107
|
+
def add_dependent(parent_to_dependent_hash)
|
108
|
+
registration = parent_to_dependent_hash.keys.first
|
109
|
+
dependent = parent_to_dependent_hash.values.first
|
110
|
+
dependents_for(registration) << dependent
|
111
|
+
end
|
112
|
+
|
113
|
+
def remove_dependent(parent_to_dependent_hash)
|
114
|
+
registration = parent_to_dependent_hash.keys.first
|
115
|
+
dependent = parent_to_dependent_hash.values.first
|
116
|
+
dependents_for(registration).delete(dependent)
|
117
|
+
end
|
118
|
+
|
119
|
+
def call(new_value)
|
120
|
+
raise Error, 'Not implemented!'
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
require 'glimmer/data_binding/observable_model'
|
127
|
+
require 'glimmer/data_binding/observable_array'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: glimmer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.9.
|
4
|
+
version: 0.9.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- AndyMaleh
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-06-
|
11
|
+
date: 2020-06-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
@@ -145,6 +145,11 @@ files:
|
|
145
145
|
- VERSION
|
146
146
|
- lib/glimmer.rb
|
147
147
|
- lib/glimmer/config.rb
|
148
|
+
- lib/glimmer/data_binding/model_binding.rb
|
149
|
+
- lib/glimmer/data_binding/observable.rb
|
150
|
+
- lib/glimmer/data_binding/observable_array.rb
|
151
|
+
- lib/glimmer/data_binding/observable_model.rb
|
152
|
+
- lib/glimmer/data_binding/observer.rb
|
148
153
|
- lib/glimmer/dsl/engine.rb
|
149
154
|
- lib/glimmer/dsl/expression.rb
|
150
155
|
- lib/glimmer/dsl/expression_handler.rb
|