1 /** @file childwidgetorganizer.cpp Organizes widgets according to a UI context.
2 *
3 * @authors Copyright (c) 2013-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
4 *
5 * @par License
6 * LGPL: http://www.gnu.org/licenses/lgpl.html
7 *
8 * <small>This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU Lesser General Public License as published by
10 * the Free Software Foundation; either version 3 of the License, or (at your
11 * option) any later version. This program is distributed in the hope that it
12 * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
13 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
14 * General Public License for more details. You should have received a copy of
15 * the GNU Lesser General Public License along with this program; if not, see:
16 * http://www.gnu.org/licenses</small>
17 */
18
19 #include "de/ChildWidgetOrganizer"
20 #include "de/LabelWidget"
21 #include "de/ui/Item"
22
23 #include <de/App>
24 #include <QMap>
25
26 namespace de {
27
28 namespace internal
29 {
30 enum AddBehavior
31 {
32 AlwaysAppend = 0x1,
33 AlwaysPrepend = 0x2,
34 DefaultBehavior = 0,
35 };
36
37 Q_DECLARE_FLAGS(AddBehaviors, AddBehavior)
38 Q_DECLARE_OPERATORS_FOR_FLAGS(AddBehaviors)
39 }
40
41 using namespace internal;
42
43 static DefaultWidgetFactory defaultWidgetFactory;
44
DENG2_PIMPL(ChildWidgetOrganizer)45 DENG2_PIMPL(ChildWidgetOrganizer)
46 , DENG2_OBSERVES(Widget, Deletion )
47 , DENG2_OBSERVES(ui::Data, Addition )
48 , DENG2_OBSERVES(ui::Data, Removal )
49 , DENG2_OBSERVES(ui::Data, OrderChange)
50 , DENG2_OBSERVES(ui::Item, Change )
51 {
52 typedef Rangei PvsRange;
53
54 ui::Data const *dataItems = nullptr;
55 GuiWidget *container;
56 IWidgetFactory *factory;
57
58 typedef QMap<ui::Item const *, GuiWidget *> Mapping;
59 typedef QMutableMapIterator<ui::Item const *, GuiWidget *> MutableMappingIterator;
60 Mapping mapping; ///< Maps items to corresponding widgets.
61
62 bool virtualEnabled = false;
63 Rule const *virtualTop = nullptr;
64 Rule const *virtualMin = nullptr;
65 Rule const *virtualMax = nullptr;
66 ConstantRule *virtualStrut = nullptr;
67 ConstantRule *estimatedHeight = nullptr;
68 int averageItemHeight = 0;
69 PvsRange virtualPvs;
70 float lastTop = 0;
71 float totalCorrection = 0;
72 float correctionPerUnit = 0;
73
74 bool recyclingEnabled = false;
75 QList<GuiWidget *> recycledWidgets; // Not GL-deinitialized to facilitate fast reuse.
76
77 Impl(Public *i, GuiWidget *c)
78 : Base(i)
79 , container(c)
80 , factory(&defaultWidgetFactory)
81 {}
82
83 ~Impl()
84 {
85 foreach (GuiWidget *recycled, recycledWidgets)
86 {
87 GuiWidget::destroy(recycled);
88 }
89
90 releaseRef(virtualTop);
91 releaseRef(virtualMin);
92 releaseRef(virtualMax);
93 releaseRef(virtualStrut);
94 releaseRef(estimatedHeight);
95 }
96
97 void set(ui::Data const *ctx)
98 {
99 if (dataItems)
100 {
101 dataItems->audienceForAddition() -= this;
102 dataItems->audienceForRemoval() -= this;
103 dataItems->audienceForOrderChange() -= this;
104
105 clearWidgets();
106 dataItems = 0;
107 }
108
109 dataItems = ctx;
110
111 if (dataItems)
112 {
113 makeWidgets();
114
115 dataItems->audienceForAddition() += this;
116 dataItems->audienceForRemoval() += this;
117 dataItems->audienceForOrderChange() += this;
118 }
119 }
120
121 PvsRange itemRange() const
122 {
123 PvsRange range(0, dataItems->size());
124 if (virtualEnabled) range = range.intersection(virtualPvs);
125 return range;
126 }
127
128 GuiWidget *addItemWidget(ui::Data::Pos pos, AddBehaviors behavior = DefaultBehavior)
129 {
130 DENG2_ASSERT_IN_MAIN_THREAD(); // widgets should only be manipulated in UI thread
131 DENG2_ASSERT(factory != 0);
132
133 if (!itemRange().contains(pos))
134 {
135 // Outside the current potentially visible range.
136 return nullptr;
137 }
138
139 ui::Item const &item = dataItems->at(pos);
140
141 GuiWidget *w = nullptr;
142 if (recyclingEnabled && !recycledWidgets.isEmpty())
143 {
144 w = recycledWidgets.takeFirst();
145 }
146 else
147 {
148 w = factory->makeItemWidget(item, container);
149 }
150 if (!w) return nullptr; // Unpresentable.
151
152 mapping.insert(&item, w);
153
154 if (behavior.testFlag(AlwaysAppend) || pos == dataItems->size() - 1)
155 {
156 container->addLast(w);
157 }
158 else if (behavior.testFlag(AlwaysPrepend) || pos == 0)
159 {
160 container->addFirst(w);
161 }
162 else
163 {
164 if (GuiWidget *nextWidget = findNextWidget(pos))
165 {
166 container->insertBefore(w, *nextWidget);
167 }
168 else
169 {
170 container->add(w);
171 }
172 }
173
174 // Others may alter the widget in some way.
175 DENG2_FOR_PUBLIC_AUDIENCE2(WidgetCreation, i)
176 {
177 i->widgetCreatedForItem(*w, item);
178 }
179
180 // Update the widget immediately.
181 itemChanged(item);
182
183 // Observe.
184 w->audienceForDeletion() += this; // in case it's manually deleted
185 item.audienceForChange() += this;
186
187 return w;
188 }
189
190 void removeItemWidget(ui::DataPos pos)
191 {
192 ui::Item const *item = &dataItems->at(pos);
193 auto found = mapping.find(item);
194 if (found != mapping.end())
195 {
196 GuiWidget *w = found.value();
197 mapping.erase(found);
198 deleteWidget(w);
199 item->audienceForChange() -= this;
200 }
201 }
202
203 GuiWidget *findNextWidget(ui::DataPos afterPos) const
204 {
205 // Some items may not be represented as widgets, so continue looking
206 // until the next widget is found.
207 while (++afterPos < dataItems->size())
208 {
209 auto found = mapping.constFind(&dataItems->at(afterPos));
210 if (found != mapping.constEnd())
211 {
212 return found.value();
213 }
214 }
215 return nullptr;
216 }
217
218 void makeWidgets()
219 {
220 DENG2_ASSERT(dataItems != 0);
221 DENG2_ASSERT(container != 0);
222
223 for (ui::Data::Pos i = 0; i < dataItems->size(); ++i)
224 {
225 addItemWidget(i, AlwaysAppend);
226 }
227 }
228
229 void deleteWidget(GuiWidget *w)
230 {
231 //pendingStrutAdjust.remove(w);
232 w->audienceForDeletion() -= this;
233
234 if (recyclingEnabled)
235 {
236 w->orphan();
237 recycledWidgets << w;
238 }
239 else
240 {
241 GuiWidget::destroy(w);
242 }
243 }
244
245 void clearWidgets()
246 {
247 DENG2_FOR_EACH_CONST(Mapping, i, mapping)
248 {
249 i.key()->audienceForChange() -= this;
250 deleteWidget(i.value());
251 }
252 mapping.clear();
253 }
254
255 void widgetBeingDeleted(Widget &widget)
256 {
257 MutableMappingIterator iter(mapping);
258 while (iter.hasNext())
259 {
260 iter.next();
261 if (iter.value() == &widget)
262 {
263 iter.remove();
264 break;
265 }
266 }
267 }
268
269 void dataItemAdded(ui::DataPos pos, ui::Item const &)
270 {
271 if (!virtualEnabled)
272 {
273 addItemWidget(pos);
274 }
275 else
276 {
277 // Items added below the PVS can be handled purely virtually (i.e., ignored).
278 // Items above the PVS will cause the PVS range to be re-estimated.
279 if (pos < ui::DataPos(virtualPvs.end))
280 {
281 clearWidgets();
282 virtualPvs = PvsRange();
283 }
284 updateVirtualHeight();
285 }
286 }
287
288 void dataItemRemoved(ui::DataPos pos, ui::Item &item)
289 {
290 Mapping::iterator found = mapping.find(&item);
291 if (found != mapping.end())
292 {
293 found.key()->audienceForChange() -= this;
294 deleteWidget(found.value());
295 mapping.erase(found);
296 }
297
298 if (virtualEnabled)
299 {
300 if (virtualPvs.contains(pos))
301 {
302 clearWidgets();
303 virtualPvs = PvsRange();
304 }
305 // Virtual total height changes even if the item was not represented by a widget.
306 updateVirtualHeight();
307 }
308 }
309
310 void dataItemOrderChanged()
311 {
312 // Remove all widgets and put them back in the correct order.
313 DENG2_FOR_EACH_CONST(Mapping, i, mapping)
314 {
315 container->remove(*i.value());
316 }
317 for (ui::Data::Pos i = 0; i < dataItems->size(); ++i)
318 {
319 if (mapping.contains(&dataItems->at(i)))
320 {
321 container->add(mapping[&dataItems->at(i)]);
322 }
323 }
324 }
325
326 void itemChanged(ui::Item const &item)
327 {
328 if (!mapping.contains(&item))
329 {
330 // Not represented as a child widget.
331 return;
332 }
333
334 GuiWidget &w = *mapping[&item];
335 factory->updateItemWidget(w, item);
336
337 // Notify.
338 DENG2_FOR_PUBLIC_AUDIENCE2(WidgetUpdate, i)
339 {
340 i->widgetUpdatedForItem(w, item);
341 }
342 }
343
344 GuiWidget *find(ui::Item const &item) const
345 {
346 Mapping::const_iterator found = mapping.constFind(&item);
347 if (found == mapping.constEnd()) return 0;
348 return found.value();
349 }
350
351 GuiWidget *findByLabel(String const &label) const
352 {
353 DENG2_FOR_EACH_CONST(Mapping, i, mapping)
354 {
355 if (i.key()->label() == label)
356 {
357 return i.value();
358 }
359 }
360 return 0;
361 }
362
363 ui::Item const *findByWidget(GuiWidget const &widget) const
364 {
365 DENG2_FOR_EACH_CONST(Mapping, i, mapping)
366 {
367 if (i.value() == &widget)
368 {
369 return i.key();
370 }
371 }
372 return 0;
373 }
374
375 //- Child Widget Virtualization ---------------------------------------------------------
376
377 void updateVirtualHeight()
378 {
379 if (virtualEnabled)
380 {
381 estimatedHeight->set(dataItems->size() * float(averageItemHeight));
382 }
383 }
384
385 GuiWidget *firstChild() const
386 {
387 return container->childWidgets().first();
388 }
389
390 GuiWidget *lastChild() const
391 {
392 return container->childWidgets().last();
393 }
394
395 float virtualItemHeight(GuiWidget const *widget) const
396 {
397 if (float hgt = widget->rule().height().value())
398 {
399 return hgt;
400 }
401 return averageItemHeight;
402 }
403
404 float bestEstimateOfWidgetHeight(GuiWidget &w) const
405 {
406 float height = w.rule().height().value();
407 if (fequal(height, 0.f))
408 {
409 // Actual height is not yet known, so use the average.
410 height = w.estimatedHeight();
411 }
412 if (fequal(height, 0.f))
413 {
414 height = averageItemHeight;
415 }
416 return height;
417 }
418
419 void updateVirtualization()
420 {
421 if (!virtualEnabled || !virtualMin || !virtualMax || !virtualTop ||
422 virtualMin->valuei() >= virtualMax->valuei())
423 {
424 return;
425 }
426
427 PvsRange const fullRange { 0, int(dataItems->size()) };
428 PvsRange const oldPvs = virtualPvs;
429
430 // Calculate position delta to compared to last update.
431 float delta = virtualTop->value() - lastTop;
432 lastTop = virtualTop->value();
433
434 // Estimate a new PVS range based on the average item height and the visible area.
435 float const y1 = de::max(0.f, virtualMin->value() - virtualTop->value());
436 float const y2 = de::max(0.f, virtualMax->value() - virtualTop->value());
437
438 int const spareItems = 3;
439 PvsRange estimated = PvsRange(y1 / averageItemHeight - spareItems,
440 y2 / averageItemHeight + spareItems)
441 .intersection(fullRange);
442
443 // If this range is completely different than the current range, recreate
444 // all visible widgets.
445 if (oldPvs.isEmpty() ||
446 estimated.start >= oldPvs.end ||
447 estimated.end <= oldPvs.start)
448 {
449 clearWidgets();
450
451 // Set up a fully estimated strut.
452 virtualPvs = estimated;
453 virtualStrut->set(averageItemHeight * virtualPvs.start);
454 lastTop = virtualTop->value();
455 delta = 0;
456 totalCorrection = 0;
457
458 for (auto pos = virtualPvs.start; pos < virtualPvs.end; ++pos)
459 {
460 addItemWidget(pos, AlwaysAppend);
461 }
462 DENG2_ASSERT(virtualPvs.size() == int(container->childCount()));
463 }
464 else if (estimated.end > oldPvs.end) // Extend downward.
465 {
466 virtualPvs.end = estimated.end;
467 for (auto pos = oldPvs.end; pos < virtualPvs.end; ++pos)
468 {
469 addItemWidget(pos, AlwaysAppend);
470 }
471 DENG2_ASSERT(virtualPvs.size() == int(container->childCount()));
472 }
473 else if (estimated.start < oldPvs.start) // Extend upward.
474 {
475 virtualPvs.start = estimated.start;
476 for (auto pos = oldPvs.start - 1;
477 pos >= virtualPvs.start && pos < int(dataItems->size());
478 --pos)
479 {
480 GuiWidget *w = addItemWidget(pos, AlwaysPrepend);
481 DENG2_ASSERT(w != nullptr);
482
483 float height = bestEstimateOfWidgetHeight(*w);
484 // Reduce strut length to make room for new items.
485 virtualStrut->set(de::max(0.f, virtualStrut->value() - height));
486 }
487 DENG2_ASSERT(virtualPvs.size() == int(container->childCount()));
488 }
489
490 if (container->childCount() > 0)
491 {
492 // Remove excess widgets from the top and extend the strut accordingly.
493 while (virtualPvs.start < estimated.start)
494 {
495 float height = bestEstimateOfWidgetHeight(*firstChild());
496 removeItemWidget(virtualPvs.start++);
497 virtualStrut->set(virtualStrut->value() + height);
498 }
499 DENG2_ASSERT(virtualPvs.size() == int(container->childCount()));
500
501 // Remove excess widgets from the bottom.
502 while (virtualPvs.end > estimated.end)
503 {
504 removeItemWidget(--virtualPvs.end);
505 }
506 DENG2_ASSERT(virtualPvs.size() == int(container->childCount()));
507 }
508
509 DENG2_ASSERT(virtualPvs.size() == int(container->childCount()));
510
511 // Take note of how big a difference there is between the ideal distance and
512 // the virtual top of the list.
513 if (oldPvs.start != virtualPvs.start)
514 {
515 // Calculate a correction delta to be applied while the view is scrolling.
516 // This will ensure that differences in item heights will not accumulate
517 // and cause the estimated PVS to become too inaccurate.
518 float error = virtualStrut->value() - estimated.start * averageItemHeight;
519 correctionPerUnit = -error / GuiWidget::pointsToPixels(100);
520 totalCorrection = de::abs(error);
521 }
522 // Apply correction to the virtual strut.
523 if (!fequal(delta, 0.f))
524 {
525 float applied = correctionPerUnit * de::abs(delta);
526 if (de::abs(applied) > totalCorrection)
527 {
528 applied = de::sign(applied) * totalCorrection;
529 }
530 virtualStrut->set(virtualStrut->value() + applied);
531 }
532 }
533
534 DENG2_PIMPL_AUDIENCE(WidgetCreation)
535 DENG2_PIMPL_AUDIENCE(WidgetUpdate)
536 };
537
DENG2_AUDIENCE_METHOD(ChildWidgetOrganizer,WidgetCreation)538 DENG2_AUDIENCE_METHOD(ChildWidgetOrganizer, WidgetCreation)
539 DENG2_AUDIENCE_METHOD(ChildWidgetOrganizer, WidgetUpdate)
540
541 ChildWidgetOrganizer::ChildWidgetOrganizer(GuiWidget &container)
542 : d(new Impl(this, &container))
543 {}
544
setContext(ui::Data const & context)545 void ChildWidgetOrganizer::setContext(ui::Data const &context)
546 {
547 d->set(&context);
548 }
549
unsetContext()550 void ChildWidgetOrganizer::unsetContext()
551 {
552 d->set(0);
553 }
554
context() const555 ui::Data const &ChildWidgetOrganizer::context() const
556 {
557 DENG2_ASSERT(d->dataItems != 0);
558 return *d->dataItems;
559 }
560
itemWidget(ui::Data::Pos pos) const561 GuiWidget *ChildWidgetOrganizer::itemWidget(ui::Data::Pos pos) const
562 {
563 return itemWidget(context().at(pos));
564 }
565
setWidgetFactory(IWidgetFactory & factory)566 void ChildWidgetOrganizer::setWidgetFactory(IWidgetFactory &factory)
567 {
568 d->factory = &factory;
569 }
570
widgetFactory() const571 ChildWidgetOrganizer::IWidgetFactory &ChildWidgetOrganizer::widgetFactory() const
572 {
573 DENG2_ASSERT(d->factory != 0);
574 return *d->factory;
575 }
576
itemWidget(ui::Item const & item) const577 GuiWidget *ChildWidgetOrganizer::itemWidget(ui::Item const &item) const
578 {
579 return d->find(item);
580 }
581
itemWidget(String const & label) const582 GuiWidget *ChildWidgetOrganizer::itemWidget(String const &label) const
583 {
584 return d->findByLabel(label);
585 }
586
findItemForWidget(GuiWidget const & widget) const587 ui::Item const *ChildWidgetOrganizer::findItemForWidget(GuiWidget const &widget) const
588 {
589 return d->findByWidget(widget);
590 }
591
setVirtualizationEnabled(bool enabled)592 void ChildWidgetOrganizer::setVirtualizationEnabled(bool enabled)
593 {
594 d->virtualEnabled = enabled;
595 d->virtualPvs = Impl::PvsRange();
596
597 if (d->virtualEnabled)
598 {
599 d->estimatedHeight = new ConstantRule(0);
600 d->virtualStrut = new ConstantRule(0);
601 }
602 else
603 {
604 releaseRef(d->estimatedHeight);
605 releaseRef(d->virtualStrut);
606 }
607 }
608
setRecyclingEnabled(bool enabled)609 void ChildWidgetOrganizer::setRecyclingEnabled(bool enabled)
610 {
611 d->recyclingEnabled = enabled;
612 }
613
setVirtualTopEdge(Rule const & topEdge)614 void ChildWidgetOrganizer::setVirtualTopEdge(Rule const &topEdge)
615 {
616 changeRef(d->virtualTop, topEdge);
617 }
618
setVisibleArea(Rule const & minimum,Rule const & maximum)619 void ChildWidgetOrganizer::setVisibleArea(Rule const &minimum, Rule const &maximum)
620 {
621 changeRef(d->virtualMin, minimum);
622 changeRef(d->virtualMax, maximum);
623 }
624
virtualizationEnabled() const625 bool ChildWidgetOrganizer::virtualizationEnabled() const
626 {
627 return d->virtualEnabled;
628 }
629
virtualStrut() const630 Rule const &ChildWidgetOrganizer::virtualStrut() const
631 {
632 DENG2_ASSERT(d->virtualEnabled);
633 return *d->virtualStrut;
634 }
635
setAverageChildHeight(int height)636 void ChildWidgetOrganizer::setAverageChildHeight(int height)
637 {
638 d->averageItemHeight = height;
639 d->updateVirtualHeight();
640 }
641
averageChildHeight() const642 int ChildWidgetOrganizer::averageChildHeight() const
643 {
644 return d->averageItemHeight;
645 }
646
estimatedTotalHeight() const647 Rule const &ChildWidgetOrganizer::estimatedTotalHeight() const
648 {
649 return *d->estimatedHeight;
650 }
651
updateVirtualization()652 void ChildWidgetOrganizer::updateVirtualization()
653 {
654 d->updateVirtualization();
655 }
656
makeItemWidget(ui::Item const &,GuiWidget const *)657 GuiWidget *DefaultWidgetFactory::makeItemWidget(ui::Item const &, GuiWidget const *)
658 {
659 return new LabelWidget;
660 }
661
updateItemWidget(GuiWidget & widget,ui::Item const & item)662 void DefaultWidgetFactory::updateItemWidget(GuiWidget &widget, ui::Item const &item)
663 {
664 widget.as<LabelWidget>().setText(item.label());
665 }
666
667 } // namespace de
668