-
Notifications
You must be signed in to change notification settings - Fork 5
/
WritingMyOwnModel.pier
224 lines (176 loc) · 11.7 KB
/
WritingMyOwnModel.pier
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
!! Creating new basic widgets
''Spec'' provides for a large amount and wide variety of basic widgets.
In the rare case that a basic widget is missing, the ''Spec'' framework will need to be extended to add this new widget.
In this section we will explain how to create such a new basic widget.
We will first explain the applicable part of how the widget build process is performed.
This will reveal the different actors in the process and provide a clearer understanding of their responsibilities.
We then present the three steps of widget creation: writing a new model, writing an adapter, and updating or creating an individual UI framework binding.
!!! One step in the building process of a widget
The UI building process does not make a distinction between basic and composed widgets.
At a specific point in the building process of a basic widget the default spec method of the widget is called, just as if it would be a composed widget.
However in this case, instead of providing a layout for multiple widgets that comprise the UI, this method builds an adapter to the underlying UI framework.
Depending of the underlying UI framework that is currently used, this method can provide different kind of adapters, for example an adapter for Morphic, or an adapter for Seaside, etc.
The adapter, when instantiated by the UI model, will in turn instantiate a widget that is specific to the UI framework being used.
For example, when using a List in the Morphic UI, the adaptor will be a MorphicListAdapter and it will contain a PluggableListMorph.
This is this framework-specific widget that will be added to the widget container.
Figure *model_adapter_uielement* shows the relationship between those objects.
+Relationship between the model, the adapter, and the UI element>file://figures/Model-Adapter-UIElement.png|width=50|label=model_adapter_uielement+
!!! The Model
The new model needs to be a subclass of ""AbstractWidgetModel"" and its name should be composed of the new basic widget concept, e.g. list or button, and of the word ''Model''.
The responsibility of the model is to store all the state of the widget.
Examples of widget-specific state are:
- the index of a list
- the label of a button
- the action block for when a text is validated in a text field
The state is wrapped in value holders and kept in instance variables.
For example, the code in *ex_value_holder* shows how to wrap the state ==0== in a value holder and keep it as an instance variable.
Value holders are needed because they are later used to propagate state changes and thus create the interaction flow of the user interface, as discussed in Section *sec_heart_of_spec*.
[[[label=ex_value_holder|caption=Storing state wrapped in a Value Holder in an instance variable|language=Smalltalk
index := 0 asValueHolder.
]]]
For each instance variable that holds state three methods should be defined: the getter, the setter, and the registration method.
The first two should be classified in the protocol named ''protocol'' while the registration method should be in ''protocol-events''.
For example, the code in *ex_mutators* shows the methods for the example code in *ex_value_holder*.
[[[label=ex_mutators|caption=Example of mutators for index|language=Smalltalk
"in protocol: protocol"
index
^index value
"in protocol: protocol"
index: anInteger
index value: anInteger
"in protocol: protocol-events"
whenIndexChanged: aBlock
index whenChangedDo: aBlock
]]]
The last step to define a new model is to implement a method ==adapterName== on the class side.
The method should be in the protocol named ''spec'' and should return a symbol.
The symbol should be composed of the basic concept of the widget, e.g. list or button, and the word ''Adapter'', like ""ListAdapter"".
The communication from the UI model to the adapter is performed using the dependents mechanism.
This mechanism is used to to handle the fact that a same model can have multiple UI elements concurrently displayed.
The message ==changed: with: == is used to send the message ''selector'' with the arguments ''aCollection'' to the adapters.
Each adapter can then convert this ''Spec'' message into a framework specific message.
For example, the method ==#filterWith:== sent by ""TreeModel"" via ==changed: with:== is implemented as shown in *ex_filter_with* in MorphicTreeAdapter
[[[label=ex_filter_with|caption=Implementation of MorphicTreeAdapter>>#filterWith:|language=Smalltalk
filterWith: aFilter
self widgetDo: [ :w || nodes |
nodes := w model rootNodes.
nodes do: [:r | r nodeModel updateAccordingTo: aFilter].
self removeRootsSuchAs: [:n | (aFilter keepTreeNode: n) not and: [n isEmpty]].
self changed: #rootNodes ].
]]]
!!! The Adapter
An adapter must be a subclass of ""AbstractAdapter"".
The adapter name should be composed of the UI framework name, e.g. Morphic, and the name of the adapter it is implementing, e.g. ListAdapter.
The adapter is an object used to connect a UI framework specific element to the framework independent model.
The only mandatory method for an adapter is ==defaultSpec== on the class side.
This method has the responsibility to instantiate the corresponding UI element.
The example *ex_adapter_instanciation* shows how ""MorphicButtonAdapter"" instantiates its UI element.
[[[label=ex_adapter_instanciation|caption=How ""MorphicButtonAdapter"" instantiates its UI element|language=Smalltalk
defaultSpec
<spec>
^ #(PluggableButtonMorph
#color: #(model color)
#on:getState:action:label:menu: #model #state #action #label nil
#getEnabledSelector: #enabled
#getMenuSelector: #menu:
#hResizing: #spaceFill
#vResizing: #spaceFill
#borderWidth: #(model borderWidth)
#borderColor: #(model borderColor)
#askBeforeChanging: #(model askBeforeChanging)
#setBalloonText: #(model help)
#dragEnabled: #(model dragEnabled)
#dropEnabled: #(model dropEnabled)
#eventHandler: #(EventHandler on:send:to: keyStroke keyStroke:fromMorph: model))
]]]
Since the adapter is bridging the gap between the element of the UI framework and the model, the adapter also needs to forward the queries from the UI element to the model.
Seen from the other way around: since the model is holding the state, the adapter is used to update the UI element state of the model.
The methods involved in the communication from the model to the adapter as well as the methods involved in the communication from the adapter to the UI model should be in the protocol ''spec protocol''.
On the other hand the methods involved in the communication from the adapter to the UI element and vice versa should be categorized in the protocol ''widget API''.
To communicate with the UI element, the adapter methods use the method ==widgetDo:==.
This method executes the block provided as argument, which will only happen after the UI element has already been created.
The example *ex_emphasis* shows how ""MorphicLabelAdapter"" propagates the modification of the emphasis from the adapter to the UI element.
[[[label=ex_emphasis|caption=How MorphicLabelAdapter propagates the emphasis changes|language=Smalltalk
emphasis: aTextEmphasis
self widgetDo: [ :w | w emphasis: aTextEmphasis ]
]]]
!!! The UI Framework binding
The binding is an object that is used to resolve the name of the adapter at run time.
This allows for the same model to be used with several UI frameworks.
Adding the new adapter to the default binding is quite simple.
It requires to update two methods: ==initializeBindings== in ""SpecAdapterBindings"" and ==initializeBindings== in the framework specific adapter class, e.g. ""MorphicAdapterBindings"" for Morphic.
The method ==SpecAdapterBindings>>#initializeBindings== is present only to expose the whole set of adapters required.
It fills a dictionary, as shown in the code *ex_adapter_init*.
[[[label=ex_adapter_init|caption=Implementation of SpecAdapterBindings>>#initializeBindings|language=Smalltalk
initializeBindings
"This implementation is stupid, but it exposes all the containers which need to be bound"
bindings
at: #ButtonAdapter put: #ButtonAdapter;
at: #CheckBoxAdapter put: #CheckBoxAdapter;
at: #ContainerAdapter put: #ContainerAdapter;
at: #DiffAdapter put: #MorphicDiffAdapter;
at: #ImageAdapter put: #ImageAdapter;
at: #LabelAdapter put: #LabelAdapter;
at: #ListAdapter put: #ListAdapter;
at: #IconListAdapter put: #IconListAdapter;
at: #DropListAdapter put: #DropListAdapter;
at: #MultiColumnListAdapter put: #MultiColumnListAdapter;
at: #MenuAdapter put: #MenuAdapter;
at: #MenuGroupAdapter put: #MenuGroupAdapter;
at: #MenuItemAdapter put: #MenuItemAdapter;
at: #NewListAdapter put: #NewListAdapter;
at: #RadioButtonAdapter put: #RadioButtonAdapter;
at: #SliderAdapter put: #SliderAdapter;
at: #TabManagerAdapter put: #TabManagerAdapter;
at: #TabAdapter put: #TabAdapter;
at: #TextAdapter put: #TextAdapter;
at: #TextInputFieldAdapter put: #TextInputFieldAdapter;
at: #TreeAdapter put: #TreeAdapter;
at: #TreeColumnAdapter put: #TreeColumnAdapter;
at: #TreeNodeAdapter put: #TreeNodeAdapter;
at: #WindowAdapter put: #WindowAdapter;
at: #DialogWindowAdapter put: #DialogWindowAdapter;
yourself
]]]
Each UI framework-specific adapter set should define its own bindings.
To implement a new binding, a subclass of ""SpecAdapterBindings"" must be defined that overrides the method ==initializeBindings==.
This method must bind ''Spec'' adapter names with framework specific adapter class names.
The example *ex_morphic_bindings* shows how the morphic binding implements the method ==initializeBindings==.
[[[label=ex_morphic_bindings|caption=Definition of Morphic specific bindings|language=Smalltalk
initializeBindings
bindings
at: #ButtonAdapter put: #MorphicButtonAdapter;
at: #CheckBoxAdapter put: #MorphicCheckBoxAdapter;
at: #ContainerAdapter put: #MorphicContainerAdapter;
at: #DiffAdapter put: #MorphicDiffAdapter;
at: #DropListAdapter put: #MorphicDropListAdapter;
at: #LabelAdapter put: #MorphicLabelAdapter;
at: #ListAdapter put: #MorphicListAdapter;
at: #IconListAdapter put: #MorphicIconListAdapter;
at: #ImageAdapter put: #MorphicImageAdapter;
at: #MultiColumnListAdapter put: #MorphicMultiColumnListAdapter;
at: #MenuAdapter put: #MorphicMenuAdapter;
at: #MenuGroupAdapter put: #MorphicMenuGroupAdapter;
at: #MenuItemAdapter put: #MorphicMenuItemAdapter;
at: #NewListAdapter put: #MorphicNewListAdapter;
at: #RadioButtonAdapter put: #MorphicRadioButtonAdapter;
at: #SliderAdapter put: #MorphicSliderAdapter;
at: #TabManagerAdapter put: #MorphicTabManagerAdapter;
at: #TabAdapter put: #MorphicTabAdapter;
at: #TextAdapter put: #MorphicTextAdapter;
at: #TextInputFieldAdapter put: #MorphicTextInputFieldAdapter;
at: #TreeAdapter put: #MorphicTreeAdapter;
at: #TreeColumnAdapter put: #MorphicTreeColumnAdapter;
at: #TreeNodeAdapter put: #MorphicTreeNodeAdapter;
at: #WindowAdapter put: #MorphicWindowAdapter;
at: #DialogWindowAdapter put: #MorphicDialogWindowAdapter;
yourself
]]]
Once this is done, the bindings should be re-initialized by running the following snippet of code: ==SpecInterpreter hardResetBindings==.
Then during the process of computing a ''Spec'' model and its layout into a framework specific UI element, the binding can be changed to change the output framework.
The binding is managed by the ""SpecInterpreter"".
The example *ex_setting_bindings* shows how to do change the binding to use the ""MyOwnBindingClass"" class.
[[[label=ex_setting_bindings|caption=How to set custom bindings|language=Smalltalk
SpecInterpreter bindings: MyOwnBindingClass new.
]]]
Note that the ""SpecInterpreter"" bindings are reset after each execution.