1 /** @file packagecontentoptionswidget.cpp  Widget for package content options.
2  *
3  * @authors Copyright (c) 2016-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
4  *
5  * @par License
6  * GPL: http://www.gnu.org/licenses/gpl.html
7  *
8  * <small>This program is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by the
10  * Free Software Foundation; either version 2 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 General
14  * Public License for more details. You should have received a copy of the GNU
15  * General Public License along with this program; if not, see:
16  * http://www.gnu.org/licenses</small>
17  */
18 
19 #include "ui/widgets/packagecontentoptionswidget.h"
20 
21 #include <de/ButtonWidget>
22 #include <de/Config>
23 #include <de/DictionaryValue>
24 #include <de/LabelWidget>
25 #include <de/MenuWidget>
26 #include <de/PackageLoader>
27 #include <de/PopupWidget>
28 #include <de/SequentialLayout>
29 #include <de/ToggleWidget>
30 #include <de/TextValue>
31 
32 using namespace de;
33 
34 DENG_GUI_PIMPL(PackageContentOptionsWidget)
35 , public ChildWidgetOrganizer::IWidgetFactory
36 {
37     /**
38      * Item representing a contained package.
39      */
40     struct Item : public ui::Item
41     {
42         bool selectedByDefault;
43         String containerPackageId;
44         String category;
45 
46         Item(String const &packageId, bool selectedByDefault, String const &containerPackageId)
47             : selectedByDefault(selectedByDefault)
48             , containerPackageId(containerPackageId)
49         {
50             setData(packageId);
51 
52             if (File const *file = PackageLoader::get().select(packageId))
53             {
54                 Record const &meta = file->objectNamespace();
55                 setLabel(meta.gets(Package::VAR_PACKAGE_TITLE));
56                 category = meta.gets(QStringLiteral("package.category"), "");
57             }
58             else
59             {
60                 setLabel(packageId);
61             }
62 
63             if (!selectedByDefault)
64             {
65                 setLabel(label() + " " _E(s)_E(b)_E(D) "ALT");
66             }
67         }
68 
69         String packageId() const
70         {
71             return data().toString();
72         }
73 
74         DictionaryValue &conf()
75         {
76             return Config::get("fs.selectedPackages").value<DictionaryValue>();
77         }
78 
79         DictionaryValue const &conf() const
80         {
81             return Config::get("fs.selectedPackages").value<DictionaryValue>();
82         }
83 
84         bool isSelected() const
85         {
86             if (conf().contains(TextValue(containerPackageId)))
87             {
88                 DictionaryValue const &sel = conf().element(TextValue(containerPackageId))
89                                              .as<DictionaryValue>();
90                 if (sel.contains(TextValue(packageId())))
91                 {
92                     return sel.element(TextValue(packageId())).isTrue();
93                 }
94             }
95             return selectedByDefault;
96         }
97 
98         void setSelected(bool selected)
99         {
100             if (!conf().contains(TextValue(containerPackageId)))
101             {
102                 conf().add(new TextValue(containerPackageId),
103                            new DictionaryValue);
104             }
105             DictionaryValue &sel = conf().element(TextValue(containerPackageId))
106                                    .as<DictionaryValue>();
107             sel.add(new TextValue(packageId()), new NumberValue(selected));
108             notifyChange();
109         }
110 
111         void reset()
112         {
113             conf().remove(TextValue(containerPackageId));
114             notifyChange();
115         }
116     };
117 
118     String packageId;
119     LabelWidget *summary;
120     MenuWidget *contents;
121 
122     Impl(Public *i, String const &packageId, Rule const &maxHeight)
123         : Base(i)
124         , packageId(packageId)
125     {
126         self().add(summary = new LabelWidget);
127         summary->setSizePolicy(ui::Fixed, ui::Expand);
128         summary->setFont("small");
129         summary->setTextColor("altaccent");
130         summary->setAlignment(ui::AlignLeft);
131 
132         auto *label = LabelWidget::newWithText(tr("Select:"), thisPublic);
133         label->setSizePolicy(ui::Expand, ui::Expand);
134         label->setFont("small");
135         label->setAlignment(ui::AlignLeft);
136 
137         auto *allButton = new ButtonWidget;
138         allButton->setSizePolicy(ui::Expand, ui::Expand);
139         allButton->setText(tr("All"));
140         allButton->setFont("small");
141         self().add(allButton);
142 
143         auto *noneButton = new ButtonWidget;
144         noneButton->setSizePolicy(ui::Expand, ui::Expand);
145         noneButton->setText(tr("None"));
146         noneButton->setFont("small");
147         self().add(noneButton);
148 
149         auto *defaultsButton = new ButtonWidget;
150         defaultsButton->setSizePolicy(ui::Expand, ui::Expand);
151         defaultsButton->setText(tr("Defaults"));
152         defaultsButton->setFont("small");
153         self().add(defaultsButton);
154 
155         contents = new MenuWidget;
156         contents->enableIndicatorDraw(true);
157         contents->setBehavior(ChildVisibilityClipping);
158         contents->setVirtualizationEnabled(true, rule(RuleBank::UNIT).value()*2 +
159                                            style().fonts().font("default").height().value());
160         contents->layout().setRowPadding(Const(0));
161         contents->organizer().setWidgetFactory(*this);
162         contents->setGridSize(1, ui::Filled, 0, ui::Fixed);
163         self().add(contents);
164 
165         // Layout.
166         auto &rect = self().rule();
167         SequentialLayout layout(rect.left() + self().margins().left(),
168                                 rect.top()  + self().margins().top());
169         layout << *label << *summary << *contents;
170         summary->rule()
171                 .setInput(Rule::Width,  rect.width() - self().margins().width());
172         contents->rule()
173                 .setInput(Rule::Left,   rect.left())
174                 .setInput(Rule::Width,  summary->rule().width() + self().margins().left())
175                 .setInput(Rule::Height, OperatorRule::minimum(contents->contentHeight(),
176                                                               maxHeight - self().margins().height() -
177                                                               label->rule().height() -
178                                                               summary->rule().height()));
179 
180         SequentialLayout(label->rule().right(), label->rule().top(), ui::Right)
181                << *defaultsButton << *noneButton << *allButton;
182         self().rule().setInput(Rule::Height, layout.height() + self().margins().bottom());
183 
184         // Configure margins.
185         for (GuiWidget *w : self().childWidgets())
186         {
187             w->margins().set("dialog.gap");
188         }
189         contents->margins().setLeftRight("gap");
190 
191         // Actions.
192         defaultsButton->setActionFn([this] ()
193         {
194             contents->items().forAll([] (ui::Item &item) {
195                 if (auto *i = maybeAs<Item>(item)) i->reset();
196                 return LoopContinue;
197             });
198         });
199         noneButton->setActionFn([this] ()
200         {
201             contents->items().forAll([] (ui::Item &item) {
202                 if (auto *i = maybeAs<Item>(item)) i->setSelected(false);
203                 return LoopContinue;
204             });
205         });
206         allButton->setActionFn([this] ()
207         {
208             contents->items().forAll([] (ui::Item &item) {
209                 if (auto *i = maybeAs<Item>(item)) i->setSelected(true);
210                 return LoopContinue;
211             });
212         });
213     }
214 
215     void populate()
216     {
217         contents->items().clear();
218 
219         File const *file = PackageLoader::get().select(packageId);
220         if (!file) return;
221 
222         Record const &meta = file->objectNamespace().subrecord(Package::VAR_PACKAGE);
223 
224         ArrayValue const &requires   = meta.geta("requires");
225         ArrayValue const &recommends = meta.geta("recommends");
226         ArrayValue const &extras     = meta.geta("extras");
227 
228         auto const totalCount    = requires.size() + recommends.size() + extras.size();
229         auto const optionalCount = recommends.size() + extras.size();
230 
231         summary->setText(tr("%1 package%2 (%3 optional)")
232                          .arg(totalCount)
233                          .arg(DENG2_PLURAL_S(totalCount))
234                          .arg(optionalCount));
235 
236         makeItems(recommends, true);
237         makeItems(extras,     false);
238 
239         // Create category headings.
240         QSet<QString> categories;
241         contents->items().forAll([&categories] (ui::Item const &i)
242         {
243             String const cat = i.as<Item>().category;
244             if (!cat.isEmpty()) categories.insert(cat);
245             return LoopContinue;
246         });
247         for (QString cat : categories)
248         {
249             contents->items() << new ui::Item(ui::Item::Separator, cat);
250         }
251 
252         // Sort all the items by category.
253         contents->items().sort([] (ui::Item const &s, ui::Item const &t)
254         {
255             if (!s.isSeparator() && !t.isSeparator())
256             {
257                 Item const &a = s.as<Item>();
258                 Item const &b = t.as<Item>();
259 
260                 int const catComp = a.category.compareWithoutCase(b.category);
261                 if (!catComp)
262                 {
263                     int const nameComp = a.label().compareWithoutCase(b.label());
264                     return nameComp < 0;
265                 }
266                 return catComp < 0;
267             }
268 
269             if (s.isSeparator() && !t.isSeparator())
270             {
271                 return s.label().compareWithoutCase(t.as<Item>().category) <= 0;
272             }
273 
274             if (!s.isSeparator() && t.isSeparator())
275             {
276                 return s.as<Item>().category.compareWithoutCase(t.label()) < 0;
277             }
278 
279             return s.label().compareWithoutCase(t.label()) < 0;
280         });
281     }
282 
283     void makeItems(ArrayValue const &ids, bool recommended)
284     {
285         for (Value const *value : ids.elements())
286         {
287             contents->items() << new Item(value->asText(), recommended, packageId);
288         }
289     }
290 
291 //- ChildWidgetOrganizer::IWidgetFactory ------------------------------------------------
292 
293     GuiWidget *makeItemWidget(ui::Item const &item, GuiWidget const *) override
294     {
295         if (item.semantics() & ui::Item::Separator)
296         {
297             auto *label = new LabelWidget;
298             label->setFont("separator.label");
299             label->setTextColor("accent");
300             label->margins().setBottom(RuleBank::UNIT);
301             label->setSizePolicy(ui::Fixed, ui::Expand);
302             label->setAlignment(ui::AlignLeft);
303             return label;
304         }
305 
306         auto *toggle = new ToggleWidget;
307         toggle->setSizePolicy(ui::Fixed, ui::Expand);
308         toggle->setAlignment(ui::AlignLeft);
309         toggle->set(Background());
310         toggle->margins().setTopBottom(RuleBank::UNIT);
311         QObject::connect(toggle, &ToggleWidget::stateChangedByUser,
312                          [&item] (ToggleWidget::ToggleState active)
313         {
314             const_cast<ui::Item &>(item).as<Item>()
315                     .setSelected(active == ToggleWidget::Active);
316         });
317         return toggle;
318     }
319 
320     void updateItemWidget(GuiWidget &widget, ui::Item const &item) override
321     {
322         LabelWidget &label = widget.as<LabelWidget>();
323         label.setText(item.label());
324 
325         if (!(item.semantics() & ui::Item::Separator))
326         {
327             widget.as<ToggleWidget>().setActive(item.as<Item>().isSelected());
328         }
329     }
330 };
331 
332 PackageContentOptionsWidget::PackageContentOptionsWidget(String const &packageId,
333                                                          Rule   const &maxHeight,
334                                                          String const &name)
335     : GuiWidget(name)
336     , d(new Impl(this, packageId, maxHeight))
337 {
338     d->populate();
339 }
340 
341 PopupWidget *PackageContentOptionsWidget::makePopup(String const &packageId,
342                                                     Rule const &width,
343                                                     Rule const &maxHeight)
344 {
345     PopupWidget *pop = new PopupWidget;
346     pop->setOutlineColor("popup.outline");
347     pop->setDeleteAfterDismissed(true);
348     //pop->setAnchorAndOpeningDirection(rule(), ui::Left);
349     /*pop->closeButton().setActionFn([this] ()
350     {
351         root().setFocus(this);
352         _optionsPopup->close();
353     });*/
354 
355     auto *opts = new PackageContentOptionsWidget(packageId, maxHeight);
356     opts->rule().setInput(Rule::Width, width);
357     pop->setContent(opts);
358     //add(_optionsPopup);
359     //_optionsPopup->open();
360     return pop;
361 }
362