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