1 /** @file listwidget.cpp UI widget for a selectable list of items.
2 *
3 * @authors Copyright © 2005-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
4 * @authors Copyright © 2005-2014 Daniel Swanson <danij@dengine.net>
5 *
6 * @par License
7 * GPL: http://www.gnu.org/licenses/gpl.html
8 *
9 * <small>This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by the
11 * Free Software Foundation; either version 2 of the License, or (at your
12 * option) any later version. This program is distributed in the hope that it
13 * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
14 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
15 * Public License for more details. You should have received a copy of the GNU
16 * General Public License along with this program; if not, write to the Free
17 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
18 * 02110-1301 USA</small>
19 */
20
21 #include <QtAlgorithms>
22 #include "common.h"
23 #include "menu/widgets/listwidget.h"
24
25 #include "hu_menu.h" // menu sounds
26 #include "menu/page.h"
27
28 using namespace de;
29
30 namespace common {
31 namespace menu {
32
Item(String const & text,int userValue)33 ListWidget::Item::Item(String const &text, int userValue)
34 {
35 setText(text);
36 setUserValue(userValue);
37 }
38
setText(String const & newText)39 void ListWidget::Item::setText(String const &newText)
40 {
41 _text = newText;
42 }
43
text() const44 String ListWidget::Item::text() const
45 {
46 return _text;
47 }
48
setUserValue(int newUserValue)49 void ListWidget::Item::setUserValue(int newUserValue)
50 {
51 _userValue = newUserValue;
52 }
53
userValue() const54 int ListWidget::Item::userValue() const
55 {
56 return _userValue;
57 }
58
DENG2_PIMPL_NOREF(ListWidget)59 DENG2_PIMPL_NOREF(ListWidget)
60 {
61 Items items;
62 int selection = 0; ///< Selected item (-1 if none).
63 int first = 0; ///< First visible item.
64 int numvis = 0;
65 bool reorderEnabled = false;
66
67 ~Impl() { qDeleteAll(items); }
68 };
69
ListWidget()70 ListWidget::ListWidget()
71 : Widget()
72 , d(new Impl)
73 {
74 setFont(MENU_FONT1);
75 setColor(MENU_COLOR1);
76 }
77
~ListWidget()78 ListWidget::~ListWidget()
79 {}
80
addItem(Item * item)81 ListWidget &ListWidget::addItem(Item *item)
82 {
83 if(item) d->items << item;
84 return *this;
85 }
86
addItems(Items const & itemsToAdd)87 ListWidget &ListWidget::addItems(Items const &itemsToAdd)
88 {
89 for(Item *item : itemsToAdd) addItem(item);
90 return *this;
91 }
92
items() const93 ListWidget::Items const &ListWidget::items() const
94 {
95 return d->items;
96 }
97
updateGeometry()98 void ListWidget::updateGeometry()
99 {
100 geometry().setSize(Vector2ui(0, 0));
101 FR_PushAttrib();
102 FR_SetFont(page().predefinedFont(mn_page_fontid_t(font())));
103
104 RectRaw itemGeometry{};
105 for(int i = 0; i < itemCount(); ++i)
106 {
107 Item *item = d->items[i];
108
109 FR_TextSize(&itemGeometry.size, item->text().toUtf8().constData());
110 if(i != itemCount() - 1)
111 {
112 itemGeometry.size.height *= 1 + MNDATA_LIST_LEADING;
113 }
114
115 geometry() |= Rectanglei::fromSize(Vector2i(itemGeometry.origin.xy), Vector2ui(itemGeometry.size.width, itemGeometry.size.height));
116
117 itemGeometry.origin.y += itemGeometry.size.height;
118 }
119 FR_PopAttrib();
120 }
121
draw() const122 void ListWidget::draw() const
123 {
124 bool const flashSelection = (isActive() && selectionIsVisible());
125 Vector4f const &textColor = mnRendState->textColors[color()];
126 float t = flashSelection? 1 : 0;
127
128 Vector4f flashColor = textColor;
129
130 if (flashSelection) /* && cfg.common.menuTextFlashSpeed > 0)
131 {
132 float const speed = cfg.common.menuTextFlashSpeed / 2.f;
133 t = (1 + sin(page().timer() / (float)TICSPERSEC * speed * DD_PI)) / 2;
134 }*/
135 {
136 flashColor = selectionFlashColor(flashColor);
137 }
138
139
140 // Vector4f const flashColor = de::lerp(textColor, Vector4f(Vector3f(cfg.common.menuTextFlashColor), textColor.w), t);
141 Vector4f const dimColor = Vector4f(Vector3f(textColor) * MNDATA_LIST_NONSELECTION_LIGHT, textColor.w);
142
143 if(d->first < d->items.count() && d->numvis > 0)
144 {
145 DGL_Enable(DGL_TEXTURE_2D);
146 FR_SetFont(mnRendState->textFonts[font()]);
147
148 Vector2i origin = geometry().topLeft;
149 int itemIdx = d->first;
150 do
151 {
152 const Item *item = d->items[itemIdx];
153
154 const Vector4f &color =
155 d->selection == itemIdx ? (flashSelection ? flashColor : textColor) : dimColor;
156
157 const int itemHeight =
158 FR_TextHeight(item->text().toUtf8().constData()) * (1 + MNDATA_LIST_LEADING);
159
160 FR_SetColorAndAlpha(color.x,
161 color.y,
162 color.z,
163 color.w * scrollingFadeout(origin.y, origin.y + itemHeight));
164 FR_DrawTextXY3(item->text().toUtf8().constData(), origin.x, origin.y, ALIGN_TOPLEFT, Hu_MenuMergeEffectWithDrawTextFlags(0));
165 origin.y += itemHeight;
166 } while (++itemIdx < d->items.count() && itemIdx < d->first + d->numvis);
167
168 DGL_Disable(DGL_TEXTURE_2D);
169 }
170 }
171
handleCommand(menucommand_e cmd)172 int ListWidget::handleCommand(menucommand_e cmd)
173 {
174 switch (cmd)
175 {
176 case MCMD_NAV_DOWN:
177 case MCMD_NAV_UP:
178 if (isActive())
179 {
180 int oldSelection = d->selection;
181 if(MCMD_NAV_DOWN == cmd)
182 {
183 if(d->selection < itemCount() - 1)
184 selectItem(d->selection + 1);
185 }
186 else
187 {
188 if(d->selection > 0)
189 selectItem(d->selection - 1);
190 }
191
192 if(d->selection != oldSelection)
193 {
194 S_LocalSound(cmd == MCMD_NAV_DOWN? SFX_MENU_NAV_DOWN : SFX_MENU_NAV_UP, NULL);
195 execAction(Modified);
196 }
197 return true;
198 }
199 return false; // Not eaten.
200
201 case MCMD_NAV_OUT:
202 if (isActive())
203 {
204 S_LocalSound(SFX_MENU_CANCEL, NULL);
205 setFlags(Active, UnsetFlags);
206 execAction(Closed);
207 return true;
208 }
209 return false; // Not eaten.
210
211 case MCMD_SELECT:
212 if (!isActive())
213 {
214 S_LocalSound(SFX_MENU_ACCEPT, NULL);
215 setFlags(Active);
216 execAction(Activated);
217 }
218 else
219 {
220 S_LocalSound(SFX_MENU_ACCEPT, NULL);
221 setFlags(Active, UnsetFlags);
222 execAction(Deactivated);
223 }
224 return true;
225
226 case MCMD_NAV_LEFT:
227 case MCMD_NAV_RIGHT:
228 if (d->reorderEnabled && isActive())
229 {
230 if (reorder(selection(), cmd == MCMD_NAV_LEFT ? -1 : +1))
231 {
232 S_LocalSound(SFX_MENU_SLIDER_MOVE, NULL);
233 execAction(Modified);
234 }
235 }
236 return true;
237
238 default: return false; // Not eaten.
239 }
240 }
241
selection() const242 int ListWidget::selection() const
243 {
244 return d->selection;
245 }
246
first() const247 int ListWidget::first() const
248 {
249 return d->first;
250 }
251
selectionIsVisible() const252 bool ListWidget::selectionIsVisible() const
253 {
254 return (d->selection >= d->first && d->selection < d->first + d->numvis);
255 }
256
updateVisibleSelection()257 void ListWidget::updateVisibleSelection()
258 {
259 d->numvis = itemCount();
260 if(d->selection >= 0)
261 {
262 if(d->selection < d->first)
263 d->first = d->selection;
264 if(d->selection > d->first + d->numvis - 1)
265 d->first = d->selection - d->numvis + 1;
266 }
267 }
268
pageActivated()269 void ListWidget::pageActivated()
270 {
271 Widget::pageActivated();
272
273 // Determine number of potentially visible items.
274 updateVisibleSelection();
275 }
276
itemData(int index) const277 int ListWidget::itemData(int index) const
278 {
279 if (index >= 0 && index < itemCount())
280 {
281 return d->items[index]->userValue();
282 }
283 return 0;
284 }
285
findItem(int userValue) const286 int ListWidget::findItem(int userValue) const
287 {
288 for(int i = 0; i < d->items.count(); ++i)
289 {
290 Item *item = d->items[i];
291 if(item->userValue() == userValue)
292 {
293 return i;
294 }
295 }
296 return -1;
297 }
298
selectItem(int itemIndex,int flags)299 bool ListWidget::selectItem(int itemIndex, int flags)
300 {
301 if(itemIndex >= 0 && itemIndex < itemCount())
302 {
303 if(d->selection != itemIndex)
304 {
305 d->selection = itemIndex;
306 if(!(flags & MNLIST_SIF_NO_ACTION))
307 {
308 execAction(Modified);
309 }
310 return true;
311 }
312 }
313 return false;
314 }
315
selectItemByValue(int userValue,int flags)316 bool ListWidget::selectItemByValue(int userValue, int flags)
317 {
318 return selectItem(findItem(userValue), flags);
319 }
320
reorder(int itemIndex,int indexOffset)321 bool ListWidget::reorder(int itemIndex, int indexOffset)
322 {
323 if (itemIndex + indexOffset < 0 || itemIndex + indexOffset >= d->items.size())
324 {
325 return false; // Would go out of bounds.
326 }
327
328 if (d->selection == itemIndex)
329 {
330 d->selection += indexOffset;
331 }
332
333 while (indexOffset < 0)
334 {
335 std::swap(d->items[itemIndex - 1], d->items[itemIndex]);
336 --itemIndex;
337 ++indexOffset;
338 }
339 while (indexOffset > 0)
340 {
341 std::swap(d->items[itemIndex + 1], d->items[itemIndex]);
342 ++itemIndex;
343 --indexOffset;
344 }
345 return true;
346 }
347
setReorderingEnabled(bool reorderEnabled)348 ListWidget &ListWidget::setReorderingEnabled(bool reorderEnabled)
349 {
350 d->reorderEnabled = reorderEnabled;
351 return *this;
352 }
353
354 } // namespace menu
355 } // namespace common
356