-
Notifications
You must be signed in to change notification settings - Fork 5
/
TheHeartOfSpec.pier
313 lines (246 loc) · 15 KB
/
TheHeartOfSpec.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
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
!! The heart of Spec
@sec_heart_of_spec
All user interfaces in ''Spec'' are constructed through the composition of existing user interfaces.
To define a user interface, it is sufficient to define the model of the user interface.
The UI elements that correspond to this model are instantiated by ''Spec'', depending on the underlying UI framework.
It is the composition of this model and these UI elements that makes up the resulting widget that is shown to the user, i.e. the resulting user interface.
Hence, since all UIs are constructed through ""composition"" of other UI's, and it is sufficient to define the ""model"" to define the UI, the root class of all UIs is named ==ComposableModel==.
So, to define a new user interface, a subclass of ==ComposableModel== needs to be created.
As said above, Spec is inspired by the MVP pattern.
It is built around three axes that materialize themselves as the following three methods in ==ComposableModel==: ==initializeWidgets==, ==initializePresenter==, and ==defaultSpec==.
These methods are hence typically found in the model for each user interface.
In this section we describe the responsibility for each method, i.e. how these three work together to build the overall UI.
!!! the ''initializeWidgets'' method
@subsec_initializeWidgets
This method is used to instantiate the models for the different widgets that will be part of the UI and store them in their respective instance variables.
Instantiation of the models will in turn result in the instantiation and initialization of the different widgets that make up the UI.
Consequently, configuration and default values of each widget are specified here as well, which is why this method is called ""initializeWidgets"".
This focus in this method is to specify what the widgets will look like and what their self-contained behavior is.
The behavior to update model state, e.g. when pressing a ==Save== button, is described in this method as well.
It is explicitly ""not"" the responsibility of this method to define the interactions ""between"" the widgets.
In general the ==initializeWidgets== method should follow the pattern:
- widgets instantiation
- widgets configuration specification
- specification of order of focus
The last step is not mandatory but ""highly"" recommended.
Indeed, without this final step keyboard navigation will not work at all.
The code in figure *pattern* shows an example of an ==initializeWidgets== method.
It first instantiates a button and a list widget, storing each in an instance variable.
It second configures the button by setting its label.
Third it specifies the focus order of all the widgets: first the button and then the list.
[[[label=pattern|caption=Example of initializeWidgets|
initializeWidgets
theButton := self newButton.
theList := self newList.
theButton label: 'I am a button'.
self focusOrder
add: theButton;
add: theList.
]]]
[[[caption:Hint|
Specifying this method is mandatory, as without it the UI would have no widgets.
]]]
!!!! Widget instantiation
The instantiation of the model for a widget (and hence the widget) can be done in two ways: through the use of an creation method or through the use of the ==instantiate:== method.
Considering the first option, the framework provides unary messages for the creation of all basic widgets.
The format of these messages is ==new[Widget]==, for example ==newButton== creates a button widget, and ==newList== creates a list widget, as we have seen above.
The complete list of available widget creation methods can be found in the class ''ComposableModel'' in the protocol ''widgets''.
Considering the second option, to reuse any composite widgets, i.e. a subclass of ''ComposableModel'', the widget needs to be initialized using the ==instantiate:== method.
For example, to reuse a ''MessageBrowser'' widget, the code is == self instantiate: MessageBrowser.==
!!! The ''initializePresenter'' method
@subsec_initializePresenter
This method takes care of the interactions between the different widgets.
By linking the behavior of the different widgets it specifies the overall presentation, i.e. how the overall UI responds to interactions by the user.
Usually this method consists of specifications of actions to perform when a certain event is received by a widget.
From the propagation of those events the whole interaction flow of the UI emerges.
In ""Spec"", the different UI models are contained in value holders, and the event mechanism relies on the announcements of these value holders to manage the interactions between widgets.
Value holders provide a single method ==whenChangedDo:== that is used to register a block to perform on change.
In addition to this primitive ==whenChangedDo:== method, the basic widgets provide more specific hooks, e.g. when an item in a list is selected or deselected.
The example *ex_button* shows how to use one of the registration methods of the list widget to change the label of the button according to the selection in the list.
[[[label=ex_button|caption=How to change a button label according to a list selection|language=Smalltalk
theList whenSelectedItemChanged: [ :item |
item
ifNil: [ theButton text: 'No selected item' ]
ifNotNil: [ theButton text: 'An item is selected'] ]
]]]
The whole event API of the basic widgets is described in the section *sec_where_to_find_what_I_want*.
[[[caption:Hint|
If a programmer wants his or her widgets to be reused,
they should provide a comprehensive API.
]]]
[[[caption:Hint|
This method is optional. Without it, the different widgets in the UI will simply not respond to changes in each others' state.
]]]
!!! the ''layout'' method
@subsec_layout
This method specifies the layout of the different widgets in the UI.
It also specifies how a widget reacts when the window is resized.
For the same UI multiple layouts can be described, and when the UI is built a specific layout to use can be specified.
If no such specific layout is given, the following lookup mechanism will be used to obtain the layout method:
# Search on class side, throughout the whole class hierarchy, for a method with the pragma ==<spec: #default>==.
# If multiple such methods exist, the first one found is used.
# If none such methods exist and if there is exactly one method with the pragma ==<spec>==, this method is used.
# No layout method is found, an error is raised.
This method is on class side because it returns a value that usually is the same for all the instances.
Put differently, usually all the instances of the same user interface have the same layout and hence this can be considered as being a class-side accessor for a class variable.
Note that the lookup for the spec method to use starts on instance side, which allows a UI to have a more specific layout depending on the state of the instance.
The simpliest example of such a method is laying out just one widget.
Example *ex_layout1* presents such a layout.
It returns a layout in which just one widget is added: the widget contained in ==theList== instance variable.
[[[label=ex_layout1|caption=Layout with only one widget|language=Smalltalk
^ SpecLayout composed
add: #theList;
yourself
]]]
The symbol ==theList== refers to an instance side method returning a widget.
This is because instance variables are private, so the layout class needs to use an accessor to obtain it when building the UI.
Note that by default, a widget will take all the space available in its container.
This method is ''not'' restricted to laying out sub widgets.
It can also refer to sub widgets contained in sub widgets, i.e. when
reusing an existing UI, specify a new layout for the sub widgets that
comprise this UI.
To do this, instead of giving a symbol, an array with 2 symbols must
be given.
The first symbol identifies the UI being reused and the second the sub
widget within this UI whose new layout position is being specified.
We have seen an example of this reuse in *subsec_protocol_editor*.
As said above, multiple layouts can be described for the same user interface.
In order to retrieve the correct method to apply, these methods need to be flagged with a pragma.
The pragma can be either ==<spec: default>== for the layout to use by default, or ==<spec>== for the other layouts.
[[[caption:Hint|
Specifying this method is mandatory, as without it the UI would show no widgets to the user.
]]]
!!!! Layout Examples
As layouts can become quite complex, this section provides a list of examples of the construction of layouts.
First two examples are given of the use of *rows and columns>layout_rows_and_column_layout*.
This is followed by two examples that explain how to set a *fixed size>layout_set_size_pixels* for rows and columns.
Next is an example that explains how to specify a widget *proportionally>layout_percentage*.
The last example presents the *expert>layout_expert* mode in case everything else fails.
To conclude, this section ends with a little *explanation>layout_specify_layout* of how to specify which layout to use when a model defines multiple layouts.
[[[caption:Hint|
All the methods for adding sub widgets can be found in the ''commands'' and ''commands-advanced'' protocols of ""SpecLayout"".
]]]
@layout_rows_and_column_layout
Often the layout of user interfaces can be described in rows and columns, and ""Spec"" provides for an easy way to specify such layouts.
The example *ex_layout_row* shows how to build a row of widgets.
[[[label=ex_layout_row|caption=Row of widgets|language=Smalltalk
^ SpecLayout composed
newRow: [ :row |
row
add: #theList;
add: #theButton
];
yourself
]]]
Having the widgets rendered as a column is similar, as shown in the example *ex_layout_column*
[[[label=ex_layout_column|caption=Column of widgets|language=Smalltalk
^ SpecLayout composed
newColumn: [ :column |
column
add: #theList;
add: #theButton
];
yourself
]]]
Rows and columns can be combined to build more complex layouts, and splitters between cells can be added.
The example *ex_three_columns* shows how to create a 3 columns layout, containing three buttons in each column.
This example also shows the ==addSplitter== message, which adds a splitter between the element added before it and the element added after.
[[[label=ex_three_columns|caption=3-column layout|language=Smalltalk
^ SpecLayout composed
newRow: [ :row |
row
newColumn: [ :column |
column
add: #button1;
add: #button2;
add: #button3
];
addSplitter;
newColumn: [ :column |
column
add: #button4;
add: #button5;
add: #button6
];
addSplitter;
newColumn: [ :column |
column
add: #button7;
add: #button8;
add: #button9
];
];
yourself
]]]
_
@layout_set_size_pixels
The height of rows as well as the width of columns can be specified, to prevent them to take all the available space.
The example *ex_row_height* shows how to specify the height of a row in pixels while the example *ex_column_width* shows how to specify the column width.
[[[label=ex_row_height|caption=Row of 30 pixels|language=Smalltalk
^ SpecLayout composed
newRow: [ :row |
row
add: #theList;
add: #theButton
] height: 30;
yourself
]]]
[[[label=ex_column_width|caption=Column of 30 pixels|language=Smalltalk
^ SpecLayout composed
newColumn: [ :column |
column
add: #theList;
add: #theButton
] width: 30;
yourself
]]]
Note that it is generally considered a bad habit to hardcode the size of the widgets.
Methods are available on ''ComposableModel'' providing sensible default sizes, like the width of a button.
When specifying custom widget sizes, care should be taken to take in account the current font size.
_
@layout_percentage
It is also possible to specify the percentage of the container, e.g. the window, that a widget should occupy.
As a result of this, the widget size will change accordingly when the container is resized.
To do so, the proportional position of the four sides of a widget can be specified, as shown in the example *ex_layout_proportional*.
For each edge, the proportion indicates at what percentage of the overall container the edge should be placed.
Zero percent is the container edge, 100 percent is the opposite container edge.
For example, for the top edge, the percentage is counted from the top down: 0 is the top edge, and 1 is the bottom edge.
[[[label=ex_layout_proportional|caption=A Button centered in, and half the size of its container|language=Smalltalk
^ SpecLayout composed
add: #theButton top: 0.25 bottom: 0.25 left: 0.25 right: 0.25;
yourself
]]]
Also, the argument can be an integer if the offset has to be a fixed number of pixels.
The number of pixels should be positive, as it indicates a distance from the corresponding edge, going to the opposite edge.
_
@layout_expert
The previous examples should cover most of the cases of layout of widgets.
For the remaining cases there is a last way to specify a widget by specifying its position.
The method ==add: origin: corner: == of ==SpecLayout== specifies the layout of a widget, percentage-wise from the origin point to the corner point.
These two points represent respectively the top left corner and the bottom right corner of the widget.
The arguments express a percentage of the container, so these __must__ be between ''0@0'' and ''1@1'' .
In addition to those points, two offsets can be also be specified, using the method ==add: origin: corner: offsetOrigin: offsetCorner: ==.
The offsets specify the number of pixels that the origin and the corner should be moved.
Contrary to the previous way to define layouts, while using ==add: origin: corner: offsetOrigin: offsetCorner: == the offset can be negative.
The offset expresses the number of pixels from the corresponding corner, in the classical computer graphics coordinate system where the origin is in the top left corner.
Note that this approach is similar to the ProportionalLayout of ""Morphic"".
The example *ex_layout_expert* shows how to add a widget as a toolbar.
It specifies that the widget in the ==toolbar== instance variable should take all the window width, but should be only 30 pixels in height.
[[[label=ex_layout_expert|caption=Using expert mode to specify a toolbar|language=Smalltalk
^ SpecLayout composed
add: #toolbar origin: 0@0 corner: 1@0 offsetOrigin: 0@0 offsetCorner: 0@30;
yourself
]]]
_
@layout_specify_layout
As explained in the section *subsec_layout*, a UI can have multiple layouts.
So when the layout of a widget that is composed of multiple sub-widgets is defined, and this widget contains multiple layout methods that determine the layout of its sub-widgets, the layout method to use can be specified.
All the methods seen in the previous examples come with a variant used to specify which selector to use for the layout method.
By example, for the ==add:== method the variant is ==add:withSpec:==.
For example, consider a widget ""MyWidget"" defining a first layout method ==firstLayout== as the default layout and another layout method called ==anotherLayout==.
The example *ex_specify_layout* shows how to add an instance of ""MyWidget"" using its ==anotherLayout== layout method.
[[[label=ex_specify_layout|caption=How to specify an alternative layout|language=Smalltalk
^ SpecLayout composed
add: #myWidget withSpec: #anotherLayout;
yourself
]]]