diff options
Diffstat (limited to 'src/ui/mobile.c')
-rw-r--r-- | src/ui/mobile.c | 704 |
1 files changed, 704 insertions, 0 deletions
diff --git a/src/ui/mobile.c b/src/ui/mobile.c new file mode 100644 index 00000000..f93d4352 --- /dev/null +++ b/src/ui/mobile.c | |||
@@ -0,0 +1,704 @@ | |||
1 | /* Copyright 2021 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
23 | #include "mobile.h" | ||
24 | |||
25 | #include "app.h" | ||
26 | #include "command.h" | ||
27 | #include "defs.h" | ||
28 | #include "inputwidget.h" | ||
29 | #include "labelwidget.h" | ||
30 | #include "root.h" | ||
31 | #include "text.h" | ||
32 | #include "widget.h" | ||
33 | |||
34 | #if defined (iPlatformAppleMobile) | ||
35 | # include "ios.h" | ||
36 | #endif | ||
37 | |||
38 | static void updatePanelSheetMetrics_(iWidget *sheet) { | ||
39 | iWidget *navi = findChild_Widget(sheet, "panel.navi"); | ||
40 | iWidget *naviPad = child_Widget(navi, 0); | ||
41 | int naviHeight = lineHeight_Text(defaultBig_FontId) + 4 * gap_UI; | ||
42 | #if defined (iPlatformAppleMobile) | ||
43 | float left, right, top, bottom; | ||
44 | safeAreaInsets_iOS(&left, &top, &right, &bottom); | ||
45 | setPadding_Widget(sheet, left, 0, right, 0); | ||
46 | navi->rect.pos = init_I2(left, top); | ||
47 | iConstForEach(PtrArray, i, findChildren_Widget(sheet, "panel.toppad")) { | ||
48 | iWidget *pad = *i.value; | ||
49 | setFixedSize_Widget(pad, init1_I2(naviHeight)); | ||
50 | } | ||
51 | #endif | ||
52 | setFixedSize_Widget(navi, init_I2(-1, naviHeight)); | ||
53 | } | ||
54 | |||
55 | static iWidget *findDetailStack_(iWidget *topPanel) { | ||
56 | return findChild_Widget(parent_Widget(topPanel), "detailstack"); | ||
57 | } | ||
58 | |||
59 | static iBool topPanelHandler_(iWidget *topPanel, const char *cmd) { | ||
60 | if (equal_Command(cmd, "panel.open")) { | ||
61 | iWidget *button = pointer_Command(cmd); | ||
62 | iWidget *panel = userData_Object(button); | ||
63 | // openMenu_Widget(panel, innerToWindow_Widget(panel, zero_I2())); | ||
64 | // setFlags_Widget(panel, hidden_WidgetFlag, iFalse); | ||
65 | iForEach(ObjectList, i, children_Widget(findDetailStack_(topPanel))) { | ||
66 | iWidget *child = i.object; | ||
67 | setFlags_Widget(child, hidden_WidgetFlag | disabled_WidgetFlag, child != panel); | ||
68 | } | ||
69 | return iTrue; | ||
70 | } | ||
71 | if (equal_Command(cmd, "mouse.clicked") && arg_Command(cmd) && | ||
72 | argLabel_Command(cmd, "button") == SDL_BUTTON_X1) { | ||
73 | postCommand_App("panel.close"); | ||
74 | return iTrue; | ||
75 | } | ||
76 | if (equal_Command(cmd, "panel.close")) { | ||
77 | iBool wasClosed = iFalse; | ||
78 | if (isPortrait_App()) { | ||
79 | iForEach(ObjectList, i, children_Widget(parent_Widget(topPanel))) { | ||
80 | iWidget *child = i.object; | ||
81 | if (!cmp_String(id_Widget(child), "panel") && isVisible_Widget(child)) { | ||
82 | // closeMenu_Widget(child); | ||
83 | setFlags_Widget(child, hidden_WidgetFlag | disabled_WidgetFlag, iTrue); | ||
84 | setFocus_Widget(NULL); | ||
85 | updateTextCStr_LabelWidget(findWidget_App("panel.back"), "Back"); | ||
86 | wasClosed = iTrue; | ||
87 | } | ||
88 | } | ||
89 | } | ||
90 | if (!wasClosed) { | ||
91 | postCommand_App("prefs.dismiss"); | ||
92 | } | ||
93 | return iTrue; | ||
94 | } | ||
95 | if (equal_Command(cmd, "document.changed")) { | ||
96 | postCommand_App("prefs.dismiss"); | ||
97 | return iFalse; | ||
98 | } | ||
99 | if (equal_Command(cmd, "window.resized")) { | ||
100 | // sheet > mdsplit > panel.top | ||
101 | updatePanelSheetMetrics_(parent_Widget(parent_Widget(topPanel))); | ||
102 | } | ||
103 | return iFalse; | ||
104 | } | ||
105 | |||
106 | static iBool isTwoColumnPage_(iWidget *d) { | ||
107 | if (cmp_String(id_Widget(d), "dialogbuttons") == 0 || | ||
108 | cmp_String(id_Widget(d), "prefs.tabs") == 0) { | ||
109 | return iFalse; | ||
110 | } | ||
111 | if (class_Widget(d) == &Class_Widget && childCount_Widget(d) == 2) { | ||
112 | return class_Widget(child_Widget(d, 0)) == &Class_Widget && | ||
113 | class_Widget(child_Widget(d, 1)) == &Class_Widget; | ||
114 | } | ||
115 | return iFalse; | ||
116 | } | ||
117 | |||
118 | static iBool isOmittedPref_(const iString *id) { | ||
119 | static const char *omittedPrefs[] = { | ||
120 | "prefs.smoothscroll", | ||
121 | "prefs.imageloadscroll", | ||
122 | "prefs.pinsplit", | ||
123 | "prefs.retainwindow", | ||
124 | "prefs.ca.file", | ||
125 | "prefs.ca.path", | ||
126 | }; | ||
127 | iForIndices(i, omittedPrefs) { | ||
128 | if (cmp_String(id, omittedPrefs[i]) == 0) { | ||
129 | return iTrue; | ||
130 | } | ||
131 | } | ||
132 | return iFalse; | ||
133 | } | ||
134 | |||
135 | enum iPrefsElement { | ||
136 | panelTitle_PrefsElement, | ||
137 | heading_PrefsElement, | ||
138 | toggle_PrefsElement, | ||
139 | dropdown_PrefsElement, | ||
140 | radioButton_PrefsElement, | ||
141 | textInput_PrefsElement, | ||
142 | }; | ||
143 | |||
144 | static iAnyObject *addPanelChild_(iWidget *panel, iAnyObject *child, int64_t flags, | ||
145 | enum iPrefsElement elementType, | ||
146 | enum iPrefsElement precedingElementType) { | ||
147 | /* Erase redundant/unused headings. */ | ||
148 | if (precedingElementType == heading_PrefsElement && | ||
149 | (!child || (elementType == heading_PrefsElement || elementType == radioButton_PrefsElement))) { | ||
150 | iRelease(removeChild_Widget(panel, lastChild_Widget(panel))); | ||
151 | if (!cmp_String(id_Widget(constAs_Widget(lastChild_Widget(panel))), "padding")) { | ||
152 | iRelease(removeChild_Widget(panel, lastChild_Widget(panel))); | ||
153 | } | ||
154 | } | ||
155 | if (child) { | ||
156 | /* Insert padding between different element types. */ | ||
157 | if (precedingElementType != panelTitle_PrefsElement) { | ||
158 | if (elementType == heading_PrefsElement || | ||
159 | (elementType == toggle_PrefsElement && | ||
160 | precedingElementType != toggle_PrefsElement && | ||
161 | precedingElementType != heading_PrefsElement) || | ||
162 | (elementType == dropdown_PrefsElement && | ||
163 | precedingElementType != dropdown_PrefsElement && | ||
164 | precedingElementType != heading_PrefsElement) || | ||
165 | (elementType == textInput_PrefsElement && | ||
166 | precedingElementType != textInput_PrefsElement && | ||
167 | precedingElementType != heading_PrefsElement)) { | ||
168 | addChild_Widget(panel, iClob(makePadding_Widget(lineHeight_Text(defaultBig_FontId)))); | ||
169 | } | ||
170 | } | ||
171 | if ((elementType == toggle_PrefsElement && precedingElementType != toggle_PrefsElement) || | ||
172 | (elementType == textInput_PrefsElement && precedingElementType != textInput_PrefsElement)) { | ||
173 | flags |= borderTop_WidgetFlag; | ||
174 | } | ||
175 | return addChildFlags_Widget(panel, child, flags); | ||
176 | } | ||
177 | return NULL; | ||
178 | } | ||
179 | |||
180 | static void stripTrailingColon_(iLabelWidget *label) { | ||
181 | const iString *text = text_LabelWidget(label); | ||
182 | if (endsWith_String(text, ":")) { | ||
183 | iString *mod = copy_String(text); | ||
184 | removeEnd_String(mod, 1); | ||
185 | updateText_LabelWidget(label, mod); | ||
186 | delete_String(mod); | ||
187 | } | ||
188 | } | ||
189 | |||
190 | static iLabelWidget *makePanelButton_(const char *text, const char *command) { | ||
191 | iLabelWidget *btn = new_LabelWidget(text, command); | ||
192 | setFlags_Widget(as_Widget(btn), | ||
193 | borderBottom_WidgetFlag | alignLeft_WidgetFlag | | ||
194 | frameless_WidgetFlag | extraPadding_WidgetFlag, | ||
195 | iTrue); | ||
196 | checkIcon_LabelWidget(btn); | ||
197 | setFont_LabelWidget(btn, defaultBig_FontId); | ||
198 | setTextColor_LabelWidget(btn, uiTextStrong_ColorId); | ||
199 | setBackgroundColor_Widget(as_Widget(btn), uiBackgroundSidebar_ColorId); | ||
200 | return btn; | ||
201 | } | ||
202 | |||
203 | static iWidget *makeValuePadding_(iWidget *value) { | ||
204 | iInputWidget *input = isInstance_Object(value, &Class_InputWidget) ? (iInputWidget *) value : NULL; | ||
205 | if (input) { | ||
206 | setFont_InputWidget(input, defaultBig_FontId); | ||
207 | setContentPadding_InputWidget(input, 3 * gap_UI, 3 * gap_UI); | ||
208 | } | ||
209 | iWidget *pad = new_Widget(); | ||
210 | setBackgroundColor_Widget(pad, uiBackgroundSidebar_ColorId); | ||
211 | setPadding_Widget(pad, 0, 1 * gap_UI, 0, 1 * gap_UI); | ||
212 | addChild_Widget(pad, iClob(value)); | ||
213 | setFlags_Widget(pad, | ||
214 | borderBottom_WidgetFlag | | ||
215 | arrangeVertical_WidgetFlag | | ||
216 | resizeToParentWidth_WidgetFlag | | ||
217 | resizeWidthOfChildren_WidgetFlag | | ||
218 | arrangeHeight_WidgetFlag, | ||
219 | iTrue); | ||
220 | return pad; | ||
221 | } | ||
222 | |||
223 | static iWidget *makeValuePaddingWithHeading_(iLabelWidget *heading, iWidget *value) { | ||
224 | iWidget *div = new_Widget(); | ||
225 | setFlags_Widget(div, | ||
226 | borderBottom_WidgetFlag | arrangeHeight_WidgetFlag | | ||
227 | resizeWidthOfChildren_WidgetFlag | | ||
228 | arrangeHorizontal_WidgetFlag, iTrue); | ||
229 | setBackgroundColor_Widget(div, uiBackgroundSidebar_ColorId); | ||
230 | setPadding_Widget(div, gap_UI, gap_UI, 4 * gap_UI, gap_UI); | ||
231 | addChildFlags_Widget(div, iClob(heading), 0); | ||
232 | //setFixedSize_Widget(as_Widget(heading), init_I2(-1, height_Widget(value))); | ||
233 | setFont_LabelWidget(heading, defaultBig_FontId); | ||
234 | setTextColor_LabelWidget(heading, uiTextStrong_ColorId); | ||
235 | if (isInstance_Object(value, &Class_InputWidget)) { | ||
236 | addChildFlags_Widget(div, iClob(value), expand_WidgetFlag); | ||
237 | } | ||
238 | else if (isInstance_Object(value, &Class_LabelWidget) && | ||
239 | cmp_String(command_LabelWidget((iLabelWidget *) value), "toggle")) { | ||
240 | addChildFlags_Widget(div, iClob(value), expand_WidgetFlag); | ||
241 | /* TODO: This doesn't work? */ | ||
242 | // setCommand_LabelWidget(heading, | ||
243 | // collectNewFormat_String("!%s ptr:%p", | ||
244 | // cstr_String(command_LabelWidget((iLabelWidget *) value)), | ||
245 | // value)); | ||
246 | } | ||
247 | else { | ||
248 | addChildFlags_Widget(div, iClob(new_Widget()), expand_WidgetFlag); | ||
249 | addChild_Widget(div, iClob(value)); | ||
250 | } | ||
251 | return div; | ||
252 | } | ||
253 | |||
254 | static iWidget *addChildPanel_(iWidget *parent, iLabelWidget *panelButton, | ||
255 | const iString *titleText) { | ||
256 | iWidget *panel = new_Widget(); | ||
257 | setId_Widget(panel, "panel"); | ||
258 | setUserData_Object(panelButton, panel); | ||
259 | setBackgroundColor_Widget(panel, uiBackground_ColorId); | ||
260 | setId_Widget(addChild_Widget(panel, iClob(makePadding_Widget(0))), "panel.toppad"); | ||
261 | if (titleText) { | ||
262 | iLabelWidget *title = | ||
263 | addChildFlags_Widget(panel, | ||
264 | iClob(new_LabelWidget(cstr_String(titleText), NULL)), | ||
265 | alignLeft_WidgetFlag | frameless_WidgetFlag); | ||
266 | setFont_LabelWidget(title, uiLabelLargeBold_FontId); | ||
267 | setTextColor_LabelWidget(title, uiHeading_ColorId); | ||
268 | } | ||
269 | addChildFlags_Widget(parent, | ||
270 | iClob(panel), | ||
271 | focusRoot_WidgetFlag | hidden_WidgetFlag | disabled_WidgetFlag | | ||
272 | arrangeVertical_WidgetFlag | resizeWidthOfChildren_WidgetFlag | | ||
273 | arrangeHeight_WidgetFlag | overflowScrollable_WidgetFlag | | ||
274 | //horizontalOffset_WidgetFlag | edgeDraggable_WidgetFlag | | ||
275 | commandOnClick_WidgetFlag); | ||
276 | return panel; | ||
277 | } | ||
278 | |||
279 | void finalizeSheet_Mobile(iWidget *sheet) { | ||
280 | /* The sheet contents are completely rearranged and restyled on a phone. | ||
281 | We'll set up a linear fullscreen arrangement of the widgets. Sheets are already | ||
282 | scrollable so they can be taller than the display. In hindsight, it may have been | ||
283 | easier to create phone versions of each dialog, but at least this works with any | ||
284 | future changes to the UI (..."works"). At least this way it is possible to enforce | ||
285 | a consistent styling. */ | ||
286 | if (deviceType_App() == phone_AppDeviceType && parent_Widget(sheet) == root_Widget(sheet)) { | ||
287 | if (~flags_Widget(sheet) & keepOnTop_WidgetFlag) { | ||
288 | /* Already finalized. */ | ||
289 | arrange_Widget(sheet); | ||
290 | postRefresh_App(); | ||
291 | return; | ||
292 | } | ||
293 | /* Landscape Layout Portrait Layout | ||
294 | |||
295 | ┌─────────┬──────Detail─Stack─────┐ ┌─────────┬ ─ ─ ─ ─ ┐ | ||
296 | │ │┌───────────────────┐ │ │ │Detail | ||
297 | │ ││┌──────────────────┴┐ │ │ │Stack │ | ||
298 | │ │││┌──────────────────┴┐│ │ │┌──────┐ | ||
299 | │ ││││ ││ │ ││┌─────┴┐│ | ||
300 | │ ││││ ││ │ │││ │ | ||
301 | │Top Panel││││ ││ │Top Panel│││ ││ | ||
302 | │ ││││ Panels ││ │ │││Panels│ | ||
303 | │ ││││ ││ │ │││ ││ | ||
304 | │ │└┤│ ││ │ │││ │ | ||
305 | │ │ └┤ ││ │ │└┤ ││ | ||
306 | │ │ └───────────────────┘│ │ │ └──────┘ | ||
307 | └─────────┴───────────────────────┘ └─────────┴ ─ ─ ─ ─ ┘ | ||
308 | offscreen | ||
309 | */ | ||
310 | /* Modify the top sheet to act as a fullscreen background. */ | ||
311 | setPadding1_Widget(sheet, 0); | ||
312 | setBackgroundColor_Widget(sheet, uiBackground_ColorId); | ||
313 | setFlags_Widget(sheet, | ||
314 | keepOnTop_WidgetFlag | | ||
315 | parentCannotResize_WidgetFlag | | ||
316 | arrangeSize_WidgetFlag | | ||
317 | centerHorizontal_WidgetFlag | | ||
318 | arrangeVertical_WidgetFlag | | ||
319 | arrangeHorizontal_WidgetFlag | | ||
320 | overflowScrollable_WidgetFlag, | ||
321 | iFalse); | ||
322 | setFlags_Widget(sheet, | ||
323 | frameless_WidgetFlag | | ||
324 | resizeWidthOfChildren_WidgetFlag | | ||
325 | edgeDraggable_WidgetFlag | | ||
326 | commandOnClick_WidgetFlag, | ||
327 | iTrue); | ||
328 | iPtrArray * contents = collect_PtrArray(new_PtrArray()); /* two-column pages */ | ||
329 | iPtrArray * panelButtons = collect_PtrArray(new_PtrArray()); | ||
330 | iWidget * prefsTabs = findChild_Widget(sheet, "prefs.tabs"); | ||
331 | iWidget * dialogHeading = (prefsTabs ? NULL : child_Widget(sheet, 0)); | ||
332 | const iBool isPrefs = (prefsTabs != NULL); | ||
333 | const int64_t panelButtonFlags = borderBottom_WidgetFlag | alignLeft_WidgetFlag | | ||
334 | frameless_WidgetFlag | extraPadding_WidgetFlag; | ||
335 | iWidget *mainDetailSplit = makeHDiv_Widget(); | ||
336 | setFlags_Widget(mainDetailSplit, resizeHeightOfChildren_WidgetFlag, iFalse); | ||
337 | setId_Widget(mainDetailSplit, "mdsplit"); | ||
338 | iWidget *topPanel = new_Widget(); { | ||
339 | setId_Widget(topPanel, "panel.top"); | ||
340 | setCommandHandler_Widget(topPanel, topPanelHandler_); | ||
341 | setFlags_Widget(topPanel, | ||
342 | arrangeVertical_WidgetFlag | | ||
343 | resizeWidthOfChildren_WidgetFlag | | ||
344 | arrangeHeight_WidgetFlag | | ||
345 | overflowScrollable_WidgetFlag | | ||
346 | commandOnClick_WidgetFlag, | ||
347 | iTrue); | ||
348 | addChild_Widget(mainDetailSplit, iClob(topPanel)); | ||
349 | } | ||
350 | iWidget *detailStack = new_Widget(); { | ||
351 | setId_Widget(detailStack, "detailstack"); | ||
352 | setFlags_Widget(detailStack, resizeWidthOfChildren_WidgetFlag, iTrue); | ||
353 | addChild_Widget(mainDetailSplit, iClob(detailStack)); | ||
354 | } | ||
355 | //setFlags_Widget(topPanel, topPanelOffset_WidgetFlag, iTrue); /* slide with children */ | ||
356 | addChild_Widget(topPanel, iClob(makePadding_Widget(lineHeight_Text(defaultBig_FontId)))); | ||
357 | if (prefsTabs) { | ||
358 | iRelease(removeChild_Widget(sheet, child_Widget(sheet, 0))); /* heading */ | ||
359 | iRelease(removeChild_Widget(sheet, findChild_Widget(sheet, "dialogbuttons"))); | ||
360 | /* Pull out the pages and make them panels. */ | ||
361 | iWidget *pages = findChild_Widget(prefsTabs, "tabs.pages"); | ||
362 | size_t pageCount = tabCount_Widget(prefsTabs); | ||
363 | for (size_t i = 0; i < pageCount; i++) { | ||
364 | iString *text = copy_String(text_LabelWidget(tabPageButton_Widget(prefsTabs, tabPage_Widget(prefsTabs, 0)))); | ||
365 | iWidget *page = removeTabPage_Widget(prefsTabs, 0); | ||
366 | iWidget *pageContent = child_Widget(page, 1); /* surrounded by padding widgets */ | ||
367 | pushBack_PtrArray(contents, ref_Object(pageContent)); | ||
368 | iLabelWidget *panelButton; | ||
369 | pushBack_PtrArray(panelButtons, | ||
370 | addChildFlags_Widget(topPanel, | ||
371 | iClob(panelButton = makePanelButton_( | ||
372 | i == 1 ? "${heading.prefs.userinterface}" : cstr_String(text), | ||
373 | "panel.open")), | ||
374 | (i == 0 ? borderTop_WidgetFlag : 0) | | ||
375 | chevron_WidgetFlag)); | ||
376 | const iChar icons[] = { | ||
377 | 0x02699, /* gear */ | ||
378 | 0x1f4f1, /* mobile phone */ | ||
379 | 0x1f3a8, /* palette */ | ||
380 | 0x1f523, | ||
381 | 0x1f5a7, /* computer network */ | ||
382 | }; | ||
383 | setIcon_LabelWidget(panelButton, icons[i]); | ||
384 | // setFont_LabelWidget(panelButton, defaultBig_FontId); | ||
385 | // setBackgroundColor_Widget(as_Widget(panelButton), uiBackgroundSidebar_ColorId); | ||
386 | iRelease(page); | ||
387 | delete_String(text); | ||
388 | } | ||
389 | destroy_Widget(prefsTabs); | ||
390 | } | ||
391 | iForEach(ObjectList, i, children_Widget(sheet)) { | ||
392 | iWidget *child = i.object; | ||
393 | if (isTwoColumnPage_(child)) { | ||
394 | pushBack_PtrArray(contents, removeChild_Widget(sheet, child)); | ||
395 | } | ||
396 | else { | ||
397 | removeChild_Widget(sheet, child); | ||
398 | addChild_Widget(topPanel, child); | ||
399 | iRelease(child); | ||
400 | } | ||
401 | } | ||
402 | const iBool useSlidePanels = (size_PtrArray(contents) == size_PtrArray(panelButtons)); | ||
403 | addChild_Widget(sheet, iClob(mainDetailSplit)); | ||
404 | iForEach(PtrArray, j, contents) { | ||
405 | iWidget *owner = topPanel; | ||
406 | if (useSlidePanels) { | ||
407 | /* Create a new child panel. */ | ||
408 | iLabelWidget *button = at_PtrArray(panelButtons, index_PtrArrayIterator(&j)); | ||
409 | owner = addChildPanel_(detailStack, button, | ||
410 | collect_String(upper_String(text_LabelWidget(button)))); | ||
411 | } | ||
412 | iWidget *pageContent = j.ptr; | ||
413 | iWidget *headings = child_Widget(pageContent, 0); | ||
414 | iWidget *values = child_Widget(pageContent, 1); | ||
415 | enum iPrefsElement prevElement = panelTitle_PrefsElement; | ||
416 | /* Identify the types of controls in the dialog and restyle/organize them. */ | ||
417 | while (!isEmpty_ObjectList(children_Widget(headings))) { | ||
418 | iWidget *heading = child_Widget(headings, 0); | ||
419 | iWidget *value = child_Widget(values, 0); | ||
420 | removeChild_Widget(headings, heading); | ||
421 | removeChild_Widget(values, value); | ||
422 | /* Can we ignore these widgets? */ | ||
423 | if (isOmittedPref_(id_Widget(value)) || | ||
424 | (class_Widget(heading) == &Class_Widget && | ||
425 | class_Widget(value) == &Class_Widget) /* just padding */) { | ||
426 | iRelease(heading); | ||
427 | iRelease(value); | ||
428 | continue; | ||
429 | } | ||
430 | enum iPrefsElement element = toggle_PrefsElement; | ||
431 | iLabelWidget *headingLabel = NULL; | ||
432 | iLabelWidget *valueLabel = NULL; | ||
433 | iInputWidget *valueInput = NULL; | ||
434 | const iBool isMenuButton = findChild_Widget(value, "menu") != NULL; | ||
435 | if (isInstance_Object(heading, &Class_LabelWidget)) { | ||
436 | headingLabel = (iLabelWidget *) heading; | ||
437 | stripTrailingColon_(headingLabel); | ||
438 | } | ||
439 | if (isInstance_Object(value, &Class_LabelWidget)) { | ||
440 | valueLabel = (iLabelWidget *) value; | ||
441 | setFont_LabelWidget(valueLabel, defaultBig_FontId); | ||
442 | } | ||
443 | if (isInstance_Object(value, &Class_InputWidget)) { | ||
444 | valueInput = (iInputWidget *) value; | ||
445 | setFlags_Widget(value, borderBottom_WidgetFlag, iFalse); | ||
446 | element = textInput_PrefsElement; | ||
447 | } | ||
448 | if (childCount_Widget(value) >= 2) { | ||
449 | if (isInstance_Object(child_Widget(value, 0), &Class_InputWidget)) { | ||
450 | element = textInput_PrefsElement; | ||
451 | setPadding_Widget(value, 0, 0, gap_UI, 0); | ||
452 | valueInput = child_Widget(value, 0); | ||
453 | } | ||
454 | } | ||
455 | if (valueInput) { | ||
456 | setFont_InputWidget(valueInput, defaultBig_FontId); | ||
457 | setContentPadding_InputWidget(valueInput, 3 * gap_UI, 0); | ||
458 | } | ||
459 | /* Toggles have the button on the right. */ | ||
460 | if (valueLabel && cmp_String(command_LabelWidget(valueLabel), "toggle") == 0) { | ||
461 | element = toggle_PrefsElement; | ||
462 | addPanelChild_(owner, | ||
463 | iClob(makeValuePaddingWithHeading_(headingLabel, value)), | ||
464 | 0, | ||
465 | element, | ||
466 | prevElement); | ||
467 | } | ||
468 | else if (valueLabel && isEmpty_String(text_LabelWidget(valueLabel))) { | ||
469 | element = heading_PrefsElement; | ||
470 | iRelease(value); | ||
471 | addPanelChild_(owner, iClob(heading), 0, element, prevElement); | ||
472 | setFont_LabelWidget(headingLabel, uiLabel_FontId); | ||
473 | } | ||
474 | else if (isMenuButton) { | ||
475 | element = dropdown_PrefsElement; | ||
476 | setFlags_Widget(value, | ||
477 | alignRight_WidgetFlag | noBackground_WidgetFlag | | ||
478 | frameless_WidgetFlag, iTrue); | ||
479 | setFlags_Widget(value, alignLeft_WidgetFlag, iFalse); | ||
480 | iWidget *pad = addPanelChild_(owner, iClob(makeValuePaddingWithHeading_(headingLabel, value)), 0, | ||
481 | element, prevElement); | ||
482 | pad->padding[2] = gap_UI; | ||
483 | } | ||
484 | else if (valueInput) { | ||
485 | addPanelChild_(owner, iClob(makeValuePaddingWithHeading_(headingLabel, value)), 0, | ||
486 | element, prevElement); | ||
487 | } | ||
488 | else { | ||
489 | if (childCount_Widget(value) >= 2) { | ||
490 | element = radioButton_PrefsElement; | ||
491 | /* Always padding before radio buttons. */ | ||
492 | addChild_Widget(owner, iClob(makePadding_Widget(lineHeight_Text(defaultBig_FontId)))); | ||
493 | } | ||
494 | addChildFlags_Widget(owner, iClob(heading), borderBottom_WidgetFlag); | ||
495 | if (headingLabel) { | ||
496 | setTextColor_LabelWidget(headingLabel, uiSubheading_ColorId); | ||
497 | setText_LabelWidget(headingLabel, | ||
498 | collect_String(upper_String(text_LabelWidget(headingLabel)))); | ||
499 | } | ||
500 | addPanelChild_(owner, iClob(value), 0, element, prevElement); | ||
501 | /* Radio buttons expand to fill the space. */ | ||
502 | if (element == radioButton_PrefsElement) { | ||
503 | setBackgroundColor_Widget(value, uiBackgroundSidebar_ColorId); | ||
504 | setPadding_Widget(value, 4 * gap_UI, 2 * gap_UI, 4 * gap_UI, 2 * gap_UI); | ||
505 | setFlags_Widget(value, arrangeWidth_WidgetFlag, iFalse); | ||
506 | setFlags_Widget(value, | ||
507 | borderBottom_WidgetFlag | | ||
508 | resizeToParentWidth_WidgetFlag | | ||
509 | resizeWidthOfChildren_WidgetFlag, | ||
510 | iTrue); | ||
511 | iForEach(ObjectList, sub, children_Widget(value)) { | ||
512 | if (isInstance_Object(sub.object, &Class_LabelWidget)) { | ||
513 | iLabelWidget *opt = sub.object; | ||
514 | setFont_LabelWidget(opt, defaultMedium_FontId); | ||
515 | setFlags_Widget(as_Widget(opt), noBackground_WidgetFlag, iTrue); | ||
516 | } | ||
517 | } | ||
518 | } | ||
519 | } | ||
520 | prevElement = element; | ||
521 | } | ||
522 | addPanelChild_(owner, NULL, 0, 0, prevElement); | ||
523 | destroy_Widget(pageContent); | ||
524 | setFlags_Widget(owner, drawBackgroundToBottom_WidgetFlag, iTrue); | ||
525 | } | ||
526 | destroyPending_Root(sheet->root); | ||
527 | /* Additional elements for preferences. */ | ||
528 | if (isPrefs) { | ||
529 | addChild_Widget(topPanel, iClob(makePadding_Widget(lineHeight_Text(defaultBig_FontId)))); | ||
530 | addChildFlags_Widget(topPanel, | ||
531 | iClob(makePanelButton_(info_Icon " ${menu.help}", "!open url:about:help")), | ||
532 | borderTop_WidgetFlag); | ||
533 | iLabelWidget *aboutButton = addChildFlags_Widget(topPanel, | ||
534 | iClob(makePanelButton_(planet_Icon " ${menu.about}", "panel.open")), | ||
535 | chevron_WidgetFlag); | ||
536 | /* The About panel. */ { | ||
537 | iWidget *panel = addChildPanel_(detailStack, aboutButton, NULL); | ||
538 | iString *msg = collectNew_String(); | ||
539 | setCStr_String(msg, "Lagrange " LAGRANGE_APP_VERSION); | ||
540 | #if defined (iPlatformAppleMobile) | ||
541 | appendCStr_String(msg, " (" LAGRANGE_IOS_VERSION ")"); | ||
542 | #endif | ||
543 | addChildFlags_Widget(panel, iClob(new_LabelWidget(cstr_String(msg), NULL)), | ||
544 | frameless_WidgetFlag); | ||
545 | addChildFlags_Widget(panel, | ||
546 | iClob(makePanelButton_(globe_Icon " By @jk@skyjake.fi", | ||
547 | "!open url:https://skyjake.fi/@jk")), | ||
548 | borderTop_WidgetFlag); | ||
549 | addChildFlags_Widget(panel, | ||
550 | iClob(makePanelButton_(clock_Icon " ${menu.releasenotes}", | ||
551 | "!open url:about:version")), | ||
552 | 0); | ||
553 | addChildFlags_Widget(panel, | ||
554 | iClob(makePanelButton_(info_Icon " ${menu.aboutpages}", | ||
555 | "!open url:about:about")), | ||
556 | 0); | ||
557 | addChildFlags_Widget(panel, | ||
558 | iClob(makePanelButton_(bug_Icon " ${menu.debug}", | ||
559 | "!open url:about:debug")), | ||
560 | 0); | ||
561 | } | ||
562 | } | ||
563 | else { | ||
564 | setFlags_Widget(topPanel, overflowScrollable_WidgetFlag, iTrue); | ||
565 | /* Update heading style. */ | ||
566 | setFont_LabelWidget((iLabelWidget *) dialogHeading, uiLabelLargeBold_FontId); | ||
567 | setFlags_Widget(dialogHeading, alignLeft_WidgetFlag, iTrue); | ||
568 | } | ||
569 | if (findChild_Widget(sheet, "valueinput.prompt")) { | ||
570 | iWidget *prompt = findChild_Widget(sheet, "valueinput.prompt"); | ||
571 | setFlags_Widget(prompt, alignLeft_WidgetFlag, iTrue); | ||
572 | iInputWidget *input = findChild_Widget(sheet, "input"); | ||
573 | removeChild_Widget(parent_Widget(input), input); | ||
574 | addChild_Widget(topPanel, iClob(makeValuePadding_(as_Widget(input)))); | ||
575 | } | ||
576 | /* Top padding for each panel, to account for the overlaid navbar. */ { | ||
577 | setId_Widget(addChildPos_Widget(topPanel, | ||
578 | iClob(makePadding_Widget(0)), front_WidgetAddPos), | ||
579 | "panel.toppad"); | ||
580 | } | ||
581 | /* Navbar. */ { | ||
582 | iWidget *navi = new_Widget(); | ||
583 | setId_Widget(navi, "panel.navi"); | ||
584 | setBackgroundColor_Widget(navi, uiBackground_ColorId); | ||
585 | addChild_Widget(navi, iClob(makePadding_Widget(0))); | ||
586 | iLabelWidget *back = addChildFlags_Widget(navi, | ||
587 | iClob(new_LabelWidget(leftAngle_Icon " ${panel.back}", "panel.close")), | ||
588 | noBackground_WidgetFlag | frameless_WidgetFlag | | ||
589 | alignLeft_WidgetFlag | extraPadding_WidgetFlag); | ||
590 | checkIcon_LabelWidget(back); | ||
591 | setId_Widget(as_Widget(back), "panel.back"); | ||
592 | setFont_LabelWidget(back, defaultBig_FontId); | ||
593 | if (!isPrefs) { | ||
594 | /* Pick up the dialog buttons for the navbar. */ | ||
595 | iWidget *buttons = findChild_Widget(sheet, "dialogbuttons"); | ||
596 | iLabelWidget *cancel = findMenuItem_Widget(buttons, "cancel"); | ||
597 | // if (!cancel) { | ||
598 | // cancel = findMenuItem_Widget(buttons, "translation.cancel"); | ||
599 | // } | ||
600 | if (cancel) { | ||
601 | updateText_LabelWidget(back, text_LabelWidget(cancel)); | ||
602 | setCommand_LabelWidget(back, command_LabelWidget(cancel)); | ||
603 | } | ||
604 | iLabelWidget *def = (iLabelWidget *) lastChild_Widget(buttons); | ||
605 | if (def && !cancel) { | ||
606 | updateText_LabelWidget(back, text_LabelWidget(def)); | ||
607 | setCommand_LabelWidget(back, command_LabelWidget(def)); | ||
608 | setFlags_Widget(as_Widget(back), alignLeft_WidgetFlag, iFalse); | ||
609 | setFlags_Widget(as_Widget(back), alignRight_WidgetFlag, iTrue); | ||
610 | setIcon_LabelWidget(back, 0); | ||
611 | setFont_LabelWidget(back, defaultBigBold_FontId); | ||
612 | } | ||
613 | else if (def != cancel) { | ||
614 | removeChild_Widget(buttons, def); | ||
615 | setFont_LabelWidget(def, defaultBigBold_FontId); | ||
616 | setFlags_Widget(as_Widget(def), | ||
617 | frameless_WidgetFlag | extraPadding_WidgetFlag | | ||
618 | noBackground_WidgetFlag, iTrue); | ||
619 | addChildFlags_Widget(as_Widget(back), iClob(def), moveToParentRightEdge_WidgetFlag); | ||
620 | updateSize_LabelWidget(def); | ||
621 | } | ||
622 | /* Action buttons are added in the bottom as extra buttons. */ { | ||
623 | iBool isFirstAction = iTrue; | ||
624 | iForEach(ObjectList, i, children_Widget(buttons)) { | ||
625 | if (isInstance_Object(i.object, &Class_LabelWidget) && | ||
626 | i.object != cancel && i.object != def) { | ||
627 | iLabelWidget *item = i.object; | ||
628 | setBackgroundColor_Widget(i.object, uiBackgroundSidebar_ColorId); | ||
629 | setFont_LabelWidget(item, defaultBig_FontId); | ||
630 | removeChild_Widget(buttons, item); | ||
631 | addChildFlags_Widget(topPanel, iClob(item), panelButtonFlags | | ||
632 | (isFirstAction ? borderTop_WidgetFlag : 0)); | ||
633 | updateSize_LabelWidget(item); | ||
634 | isFirstAction = iFalse; | ||
635 | } | ||
636 | } | ||
637 | } | ||
638 | iRelease(removeChild_Widget(parent_Widget(buttons), buttons)); | ||
639 | /* Styling for remaining elements. */ | ||
640 | iForEach(ObjectList, i, children_Widget(topPanel)) { | ||
641 | if (isInstance_Object(i.object, &Class_LabelWidget) && | ||
642 | isEmpty_String(command_LabelWidget(i.object)) && | ||
643 | isEmpty_String(id_Widget(i.object))) { | ||
644 | setFlags_Widget(i.object, alignLeft_WidgetFlag, iTrue); | ||
645 | if (font_LabelWidget(i.object) == uiLabel_FontId) { | ||
646 | setFont_LabelWidget(i.object, uiContent_FontId); | ||
647 | } | ||
648 | } | ||
649 | } | ||
650 | } | ||
651 | addChildFlags_Widget(sheet, iClob(navi), | ||
652 | drawBackgroundToVerticalSafeArea_WidgetFlag | | ||
653 | arrangeHeight_WidgetFlag | resizeWidthOfChildren_WidgetFlag | | ||
654 | resizeToParentWidth_WidgetFlag | arrangeVertical_WidgetFlag); | ||
655 | } | ||
656 | updatePanelSheetMetrics_(sheet); | ||
657 | iAssert(sheet->parent); | ||
658 | arrange_Widget(sheet->parent); | ||
659 | postCommand_App("widget.overflow"); /* with the correct dimensions */ | ||
660 | //puts("---- MOBILE LAYOUT ----"); | ||
661 | //printTree_Widget(sheet); | ||
662 | } | ||
663 | else { | ||
664 | arrange_Widget(sheet); | ||
665 | } | ||
666 | postRefresh_App(); | ||
667 | } | ||
668 | |||
669 | void setupMenuTransition_Mobile(iWidget *sheet, iBool isIncoming) { | ||
670 | if (deviceType_App() != phone_AppDeviceType) { | ||
671 | return; | ||
672 | } | ||
673 | const iBool isSlidePanel = (flags_Widget(sheet) & horizontalOffset_WidgetFlag) != 0; | ||
674 | if (isSlidePanel && isLandscape_App()) { | ||
675 | return; | ||
676 | } | ||
677 | if (isIncoming) { | ||
678 | setVisualOffset_Widget(sheet, isSlidePanel ? width_Widget(sheet) : height_Widget(sheet), 0, 0); | ||
679 | setVisualOffset_Widget(sheet, 0, 330, easeOut_AnimFlag | softer_AnimFlag); | ||
680 | } | ||
681 | else { | ||
682 | const iBool wasDragged = iAbs(value_Anim(&sheet->visualOffset) - 0) > 1; | ||
683 | setVisualOffset_Widget(sheet, | ||
684 | isSlidePanel ? width_Widget(sheet) : height_Widget(sheet), | ||
685 | wasDragged ? 100 : 200, | ||
686 | wasDragged ? 0 : easeIn_AnimFlag | softer_AnimFlag); | ||
687 | } | ||
688 | } | ||
689 | |||
690 | void setupSheetTransition_Mobile(iWidget *sheet, iBool isIncoming) { | ||
691 | if (deviceType_App() != phone_AppDeviceType || isLandscape_App()) { | ||
692 | return; | ||
693 | } | ||
694 | if (isIncoming) { | ||
695 | setFlags_Widget(sheet, horizontalOffset_WidgetFlag, iTrue); | ||
696 | setVisualOffset_Widget(sheet, size_Root(sheet->root).x, 0, 0); | ||
697 | setVisualOffset_Widget(sheet, 0, 200, easeOut_AnimFlag); | ||
698 | } | ||
699 | else { | ||
700 | const iBool wasDragged = iAbs(value_Anim(&sheet->visualOffset)) > 0; | ||
701 | setVisualOffset_Widget(sheet, size_Root(sheet->root).x, wasDragged ? 100 : 200, | ||
702 | wasDragged ? 0 : easeIn_AnimFlag); | ||
703 | } | ||
704 | } | ||