File: ui_unitsandscale.markdown

package info (click to toggle)
brewtarget 4.2.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 35,468 kB
  • sloc: cpp: 56,958; xml: 19,031; python: 1,266; sh: 183; makefile: 11
file content (319 lines) | stat: -rw-r--r-- 14,352 bytes parent folder | download | duplicates (4)
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
314
315
316
317
318
319
How It Works
============

The Short Form
--------------

If you don't want to read or don't care about the magic, follow these basic
steps in the Qt Designer to make the units & scales stuff work.

1. Create your label and line edit field. I am asking that people follow the
   basic naming convention of:
   label\_fieldname for labels
   lineEdit\_fieldname for line Edits
2. Make sure the nearest parent that can have a dynamic property has one named
   "configSection" defined. Things like groupBoxes can, things like
   verticalLayouts cannot. Most existing UI elements should already have this
   set. If not, create it. I have been using the name of the form for the
   value.
3. Right click on the new label and find the Promote To list. Note that you
   want the drop down list, not the dialog. If that list isn't available, open
   another form like mainWindow.ui or brewNote.ui first. This will prepopulate
   a lot of things and save you some typing later.
 * BtColorLabel for color
 * BtDateLabel for dates like 2014-12-13 -- there is no BtDateEdit
 * BtDensityLabel for specific gravity/plato
 * BtMassLabel for weights
 * BtMixedLabel for "mixed" fields like Amount on the misc editor.
 * BtTemperatureLabel for temperatures
 * BtTimeLabel for time
 * BtVolumeLabel for volumes
4. Once you have promoted the label, change the "contextMenuPolicy" to
   "CustomContextMenu" in the property editor.
5. Edit the "buddy" attribute to the name of the associated lineEdit.
6. On some very rare occassions, you may need to add a dynamic attribute
   called "editField" on a Label. For example, BtDateLabels have no
   BtDateEdit, so would need the editField attribute defined. For more
   examples, you will need the gory details.
7. Right click on the associated LineEdit and promote it to the equivalent
   BtLineEdit type.
 * BtColorEdit for color
 * BtDensityEdit for specific gravity/plato
 * BtMassEdit for weights
 * BtMixedEdit for mixed fields like Amount in the misc editor
 * BtTemperatureEdit for temperatures
 * BtTimeEdit for time
 * BtVolumeEdit for volumes
8. Add a custom string property named "editField". This should normally be the
   name of the property defined in the associated BeerXML object (e.g.,
   "carbMax\_vol" for the miscEditor's lineEdit\_carbMax ). There are
   exceptions to this rule, but this is the tl;dr version.
9. Still using the Designer, add a Signal/Slot
10. Change the sender to the label's name, set the signal to
   "labelChanged(unitDisplay)", set the receiver to the lineEdit and the slot
   to "lineChanged(unitDisplay)"
11. Save your changes and compile.

The Magic Explained
-------------------

The magic relies on a two Classes and a signal.

The BtLabel class knows what kind of label it is and what menu to pop. When
somebody right clicks on a BtLabel, a menu is found and displayed showing
units and scales, as appropriate. When the user selects a unit or scale, the
BtLabel accesses the configSection and the editField dynamic property to
determine the proper section and attribute name, and then stores that choice
in the configuration file.

Once the property is stored, a signal is generated using the previous value of
that property.

The BtLineEdit also knows what kind of field it is and the two dynamic
properties. When the BtLabel emits its signal, the BtLineEdit redisplays its
contents. The signal carries the old unit/scale with it, so BtLineEdit first
converts that to SI and then from SI to the new unit/scale as read from the
config file. This works because setText does not emit a signal and we can muck
with the contents without having to recalculate anything.

When a BtLineEdit fields interfaces with a BeerXMLElement, it is
really important that the value of the editField attribute be the same as the
attribute being edited. For example, the BatchSize field on the main window
has an editField value of batchSize\_l. This allows some of the very nice
setText() syntax shown later.

### BtLabels

The first part are the labels. The labels are responsible for popping the
necessary context menu, setting the appropriate attribute in the config file
and signalling that something has changed.

BtLabels know five things about themselves:
 * what kind of label they are
 * who their parent object is
 * the section name in the configuration file
 * the attribute name in the configuration file. This should also be the name of the attribute in the BeerXMLElement
 * what menu it should pop

#### Constructor
The first two items are set in the constructor, along with connecting the
customContextMenuRequested() signal to the proper slot. Unfortunately, the
dynamic properties are not available at this point, so the other three pieces of
information have to wait. NOTE: I need to check this against Qt5. It may have
changed.

#### initializeSection
This method tries to find the name of the section in the configuration file.
If the work has already been done, it just returns the cached value.

Otherwise, it uses a multi-step logic tree to find this name. If a name is
found at any time, evaluation stops and that name is used. The order is:

1. A dynamic property called configSection set on the label itself.
2. The configured buddy has a dynamic property called configSection
3. The parent object has a dynamic property called configSection
4. The name of the parent object.

In the course of development, I started with labels and lineEdits defining the
configSection every time. I really cannot express how repetitive that was. I
then found the buddy attribute, which reduced the typing by half. I then
realized I could use the parent name, but sometimes the names were very
unhelpful, like "groupBox" which would have lead to all sorts of
name collisions. So I finally decided to put the dynamic property on the
parent and then leave the decision tree in place just in case you need
to override it.

#### initializeProperty
This method tries to figure out the property name to use. It works like
initializeSection, but uses a shorter decision tree:

1. A dynamic property called editField is set on the label itself
2. A dynamic property called editField is set on the buddy lineEdit
3. If neither is found, nothing is done. I am not sure if this is a good idea
or not

This allows the label to override what is on the lineEdit, and I do make use of
this in a few places like the style editor. I need to think of a better null
behavior than a qDebug() line, but that is what I have at the moment.

#### initializeMenu
The final initialize method gets the menu. This has to be called late, since
we need to know the property and section. Based on the type of the label, the
proper Brewtarget::setupMenu() method is called. The Mixed labels use the
volume menu, which is weird but it mostly works.

#### popContextMenu
This does all the hard work.

The property, configSection and menu are initialized if required. The menu is
then executed.

If the unit menu is what returns, the requested unit is set using the property
name and config section to generate the name. Otherwise, the scale is set.

Some special handling is done when the property name is og, fg or color\_srm.
For these three property names, we also set a min and max attribute. This is
required to make the sliders work properly, and unexpectedly made the Style
editor actually work.

The final bit of processing is to switch the text of the label on
BtColorLabels so that they show the unit being used.

When all of this is done, a labelChanged() signal is emitted, using the
unit and scale of the field before it was changed. This basically signals the
associated BtLineEdit that the unit or scale has changed and the LineEdit
needs to redisplay.

#### Everything else
Everything past that is to simply initialize each label as the proper type.

### BtLineEdit
This class is a bit trickier than the last. This class extends the QLineEdit
class to handle a bunch of different things. The ultimate goal of this class
is to change *everyplace* in brewtarget that says
setText(Brewtarget::displayAmount()) to simply say setText(QString). And I am
very, very close.

Please note that I have over loaded setText() four times.

#### Constructor
BtLineEdit knows five things:

* Its parent
* What kind of BtLineEdit it is
* What the default Unit is (e.g., Units::kilograms)
* The configSection that holds its unit and scale
* The property name that defines it unit and scale

Again, my reliance on dynamic properties means it knows the first three during
construction and we have to figure the last two out later.

The only other thing the constructor does is to connect the editingFinished()
signal to the lineChanged() slot.

#### lineChanged()
This method simply calls the more complex lineChanged(unitDisplay,unitScale) with
noUnit and noScale. It has to be there so that the signature of this slot
matches the signature of the signal.

#### initializeProperty()
This is very similar to the same method in BtLabel. If the value is not known,
it looks for the dynamic property called "editField". Note, there is no
complex series of guesses like there is in BtLabel. "editField" on a
BtLineEdit is required for the magic to work.

#### initializeSection()
Again, similar to the same method in BtLabel. The hierarchy looks like:

1. A dynamic property called "configSection" on the BtLineEdit itself
2. A dynamic property called "configSection" on its parent object
3. The name of its parent

All of the caveats from BtLabel apply. I strong recommend setting this on the
nearest parent that it can be set on. It saves a lot of typing.

#### lineChanged(unitDisplay,unitScale)
One of the things I noticed while working on this code is how frequently
finishedEditing() fires. If focus leaves the window, leaves the field, etc.
this signal is sent. So the first check looks to see who signaled and return
if the BtLineEdit signaled and nothing had actually changed.

If something else signaled (that is, a BtLabel) a boolean is set to ensure we
treat the units correctly. It gets weird a bit further in.

The configSection and property name are discovered, and then we get the unit
and scale as written in the config file. It is somewhat important to
understand that the BtLabel has already written the new values to the config
file at this point. So the unit and scale we get is the *new* unit and scale.

Given a BtLineEdit of mass, volume, temperature or time, we convert the
current value to SI using the previous unit. We then setText to the new unit
and scale via a call to displayAmount.

Color and density work the same way but use a different default precision.

Finally, generic types and the default just go to double and display it.

If force wasn't set, it means we have modified the value not just the display.
Under those circumstances, we emit a textModified signal that is used by
upstream processes to redo their caclulations.

#### toSI(unitDisplay,unitScale,boolean)
Given a unitDisplay and a unitScale, this method finds the appropriate unit
system and calls its qstringToSI() method.

If the boolean is true, we will override the provided unit and scale and get
the current values out of the config file. This changes depending on who
calls lineChanged() -- input in the field will use the config file, a
lineChanged emitted by a BtLabel will not.

Once we figure out which units and scale to use, we use
Brewtarget::findUnitSystem to find the proper UnitSystem for the new unit. If
we find a UnitSystem, we then find the proper Unit for the provided scale. If
we cannot find the scale, we use the default scale for that UnitSystem (eg,
Unit::kilogram for siWeightUnitSystem).

Assuming that all works, we invoke the proper toSI() method for that
UnitSystem.

If we cannot find an approproate UnitSystem and the BtLineEdit is a STRING
type, we just return 0.

If all else fails, we just return the value of the text() in the BtLineEdit
converted to a double.

#### displayAmount(double,double)
I got tired of having to find the unitDisplay and unitScale each time I called
Brewtarget::displayAmount. So this method simply isolates all that work for
me.

#### setText(double,double)
This is the base. Given two doubles (amount and precision), the text is set to
the displayAmount() using QLineEdit::setText().

#### setText(BeerXMLElement\*,double)
Given a BeerXMLElement\* and an optional double, this version will use the
editField attribute on the BtLineEdit to get the value from the
BeerXMLElement.  displayAmount(double,double) is then called using the found
value and the provided precision. Finally, QLineEdit::setText() is called to
display the results.

#### setText(QString,double)
Given a QString and a double, the QString is converted to a double,  and
displayAmount(double,double) is then called using the converted
value and the provided precision. The results are finally displayed with a
call to QLineEdit::setText().

#### setText(QVariant,double)
Given a QVariant and a precision, the QVariant is converted either to a string
if the BtLineEdit is a STRING or a double otherwise. Once the conversion is
done, we call displayAmount() and QLineEdit::setText()

### Everything else
After that, it is all configuration work, except for....

### BtMixedEdit
The problem was how to handle fields, like the amounts on miscellaneous items,
that can represent either masses or volumes. After a lot of thought, it
occurred to me that all I had to do was fudge the class a little.

As the comments suggest, this class is kind of evil. Well, actually it is
pretty much all evil and I strongly suspect I will come to regret this
decision.

#### constructor()
So the constructor lies. It sets itself as a VOLUME and a default unit of
Units::liters.

#### setIsWeight(boolean)
And this is the evil. All of the check boxes that mark if something is a mass
or volume trigger this slot.

If the checkbox is marked, it means the associated field is now a Mass. This
will cause the BtMixedEdit to change its type to Mass and its default unit to
Units::kilograms. If the box is unchecked, it change its type back to Volume
and the default to Units::liters. We then call lineChanged() to do its magic.

It works astonishingly well, but you do get some odd results if you change the
field from one to the other with a value already in the line edit. 2 lb will
suddenly become 2 gallons.