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