-
Notifications
You must be signed in to change notification settings - Fork 5
/
SpecTheDynamic.pier
239 lines (181 loc) · 10.2 KB
/
SpecTheDynamic.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
!! Spec the Dynamic
@sec_spec_the_dynamic
Having an user interface with a well known number of sub widgets and a static layout is not always sufficient.
A user interface is often more than just that, for example here are two situations where more is needed:
First, when the layout of the user interface needs to be changed at runtime to match the execution context of the software.
Second, sub widgets are added or removed at runtime and therefore the programmer needs to be able to parametrize those new sub widgets on the fly.
''Spec'' also provides support for such dynamic user interfaces.
In this section we show how to use ''Spec'' in these situations.
First, we talk about making dynamic modifications of the layout of widgets, and second we discuss the dynamic adding and removing of subwidgets.
Third and last we show how the dynamic features can be used to quickly prototype a user interface.
!!! Dynamic modification of the layout
Changing the layout of widgets at runtime is straightforward, as we will see here.
Such changes basically consist of three steps:
# creating the new layout,
# setting a flag to prohibit the creation of a new UI element (and instead reuse the existing one),
# building the UI again with the newly created layout.
The code in *rebuildDynamically* is an example of rebuilding a widget with a new layout.
First, a helper method is used to obtain a ==SpecLayout== object that determines the new layout.
Second, the ==needRebuild== flag is set to ==false== such that the existing UI element is reused.
Third, the rebuilding of the user interface is performed.
[[[label=rebuildDynamically|caption=Rebuild a widget at run time|language=Smalltalk
rebuildWithNewLayout
| newLayout |
newLayout := self newLayoutCreatedDynamically.
self needRebuild: false. "tells the interpreter to keep my current UI element"
self buildWithSpecLayout: newLayout. "rebuilds me with the new layout"
]]]
One widget can also keep the UI elements of its sub widgets which do not need to be rebuilt.
The message ==needRebuild: false== needs to be sent to any of those sub widgets.
For example, if a model comprising a ''button'' and a ''list'' just wants to rearrange the position of these UI elements, there is no need to rebuild them, i.e. instantiate new UI elements for them.
To prevent this, the message ==needRebuild: false== should be send to them, as shown in the example *ex_needRebuild*.
[[[label=ex_needRebuild|caption=How to need rebuild sub widgets|language=Smalltalk
rebuildWithNewLayout
| newLayout |
newLayout := self newLayoutCreatedDynamically.
self needRebuild: false.
theButton needRebuild: false.
theList needRebuild: false.
self buildWithSpecLayout: newLayout.
]]]
!!! Dynamic adding and removal of subwidgets
If a user interface needs a varying number of subwidgets, the amount of which cannot be established at compilation time, then another approach is needed.
In this scenario, ==DynamicComposableModel== is the model that needs to be subclassed, as this class provides support for the required kind of dynamic behavior.
Amongst others, this class adds the method ==assign:to:==, which takes a model instance as a first argument, and a unique symbol as a second argument.
This method is used to assign an already instantiated model as sub widget, instead of the method ==instantiateModels:== that takes a class name as argument and instantiates a new model.
When using ==DynamicComposableModel==, the instantiation of the sub widgets is a bit different from normal use.
In the ==initializeWidgets== method, instead of instantiating each widget separately, ==instantiateModels:== should be used to instantiate them.
This method takes as argument an array of pairs, where each pair is composed of the unique name of the widget as key, and the name of the widget class as value.
This allows for a widget to be accessed by sending a message whose selector is the widget name to the model.
By example, if a widget named ==button== is created, then this widget can be accessed by calling ==self button== as shown in the example *ex_dynamic_creation*.
[[[label=ex_dynamic_creation|caption=Dynamic creation of a widget|language=Smalltalk
self instantiateModels: #( button ButtonModel ).
self button label: 'Click me'.
]]]
!!! Examples: Prototyping a UI
Thanks to the capability of ''Spec'' to dynamically instantiate widgets, it is also possible to prototype a user interface from within any workspace.
The following examples show how ''Spec'' can be used to quickly prototype a user interace.
The first example explains how to design by prototyping a user interface.
The second example introduces the composition of dynamic models.
!!!! Designing a pop up
This example shows how to easily and quickly design a popup window asking for an input.
First we create a simple model with two sub widgets, a label and a text field, as shown by the snippet *ex_widget_creation*.
[[[label=ex_widget_creation|caption=Create a widget|language=Smalltalk
view := DynamicComposableModel new
instantiateModels: #(label LabelModel text TextInputFieldModel);
yourself.
]]]
We can then specify the title and the initial size of the widget, adding the code in *ex_set_title*.
[[[label=ex_set_title|caption=Specify the title and the initial size|language=Smalltalk
view extent: 300@90;
title: 'Choose your project'.
]]]
Then we specify the UI element layout.
It will be only one row with the label and the text field.
The snippet *ex_layout* shows the layout definition.
[[[label=ex_layout|caption=Define the layout|language=Smalltalk
layout := SpecLayout composed
newRow: [ :r | r add: #label width: 75; add: #text ];
yourself.
]]]
To have a first idea of the resulting UI, we can already open it as follows: ==view openWithSpecLayout: layout==.
This UI however does not have any sub widget state or behavior so there is not much to see or do at this point.
The next step is to set up the sub widget state and behavior.
We set the text of the label as well as the ghost text of the textfield.
We also specify here that the text field should automatically accept the text on each keystroke, such that it does not show the yellow 'edited' triangle on the top right.
This is shown in the code in *ex_setup_subwidget*.
[[[label=ex_setup_subwidget|caption=Setting up the sub widgets|language=Smalltalk
view label text: 'Packages:'.
view text
autoAccept: true;
entryCompletion: nil;
ghostText: '.*'.
]]]
Opening the UI again (==view openWithSpecLayout: layout==) now shows the text of the label and the ghost text of the text field.
As we want the widget to be a popup with a single button ''Ok'', the toolbar to use should be defined explicitly (the default toolbar has an ''Ok'' button and a ''Cancel'' button).
We also set the toolbar action for when ''Ok'' is clicked: the current text of the text field will be saved in the instance variable ''regex''.
The code in *ex_toolbar* shows how to do it.
[[[label=ex_toolbar|caption=Instantiating the toolbar|language=Smalltalk
toolbar := OkToolbar new
okAction: [ regex := view text text ];
yourself.
]]]
We can also add a shortcut to the text field on ''Enter'' to simulate the click on ''Ok''.
The code *ex_shortcut* illustrates how to set up such a shortcut.
[[[label=ex_shortcut|caption=Add a shortcut|language=Smalltalk
view text
bindKeyCombination: Character cr asKeyCombination
toAction: [ toolbar triggerOkAction ].
]]]
This completes the specification of the UI.
As a final step, when opening it we pass it the toolbar and configure it to be centered in the Pharo window and modal.
The code in *ex_final* shows the final version of the code
[[[label=ex_final|caption=Open the widget|language=Smalltalk
view := DynamicComposableModel new
instantiateModels: #(label LabelModel text TextInputFieldModel);
extent: 300@90;
title: 'Choose your project'
yourself.
view label text: 'Packages:'.
layout := SpecLayout composed
newRow: [ :r | r add: #label width: 75; add: #text ];
yourself.
view text
autoAccept: true;
entryCompletion: nil;
ghostText: '.*'.
toolbar := OkToolbar new
okAction: [ regex := view text text ];
yourself.
view text
bindKeyCombination: Character cr asKeyCombination
toAction: [ toolbar triggerOkAction ].
(view openDialogWithSpecLayout: layout)
toolbar: toolbar;
centered;
modalRelativeTo: World.
]]]
The result can be seen in Figure *fig_popup*.
+Prototype of a popup>file://figures/Popup.png|width=50|label=fig_popup+
!!!! Composing dynamic models
This exemple shows in three parts how to buid a simple code browser.
First a simple list widget is created displaying all the subclasses of AstractWidgetModel.
[[[label=ex_dyn_list|caption=Defining a list widget|language=Smalltalk
m := DynamicComposableModel new.
m instantiateModels: #( list ListModel ).
m list items: (AbstractWidgetModel allSubclasses sorted: [:a :b | a name < b name ]).
m layout: (SpecLayout composed
add: #list;
yourself).
m openWithSpec.
]]]
Then the list widget is reused to build a viewer widget displaying the protocol methods of the selected class.
[[[label=ex_dyn_protocols|caption=Definition of a protocol methods viewer|language=Smalltalk
m2 := DynamicComposableModel new.
m2 assign: m to: #classes.
m2 instantiateModels: #( methods ListModel ).
m list whenSelectedItemChanged: [ :item |
item
ifNil: [ m2 methods: #() ]
ifNotNil: [ m2 methods items: ((item selectorsInProtocol: 'protocol') sorted) ] ].
m2 layout: (SpecLayout composed
newRow: [ :r | r add: #classes; add: #methods ];
yourself).
m2 openWithSpec.
]]]
Finally the last widget is defined with the previously created viewer.
In addition, a text zone is added to show the selected methods source code.
[[[label=ex_dyn_browser|caption=Definition of a Protocol Browser|language=Smalltalk
m3 := DynamicComposableModel new.
m3 assign: m2 to: #top.
m3 instantiateModels: #( text TextModel ).
m2 methods whenSelectedItemChanged: [ :selector |
selector ifNil: [ m3 text text: '' ] ifNotNil: [ m3 text text: (m list selectedItem >> selector ) sourceCode ] ].
m3 layout: (SpecLayout composed
newColumn: [ :c | c add: #top; add: #text ];
yourself).
m3 openWithSpec.
m3 title: 'Protocol browser'
]]]
The final result looks like the Figure *ex_browser*.
+Prototype of Protocol Browser>file://figures/Dyn_Protocol_Browser.png|width=50|label=ex_browser+