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