]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/menu/item/listbox.qc
Merge branch 'master' into Mirio/balance
[xonotic/xonotic-data.pk3dir.git] / qcsrc / menu / item / listbox.qc
1 #ifndef ITEM_LISTBOX_H
2         #define ITEM_LISTBOX_H
3         #include "../item.qc"
4         CLASS(ListBox, Item)
5                 METHOD(ListBox, resizeNotify, void(entity, vector, vector, vector, vector));
6                 METHOD(ListBox, configureListBox, void(entity, float, float));
7                 METHOD(ListBox, draw, void(entity));
8                 METHOD(ListBox, keyDown, float(entity, float, float, float));
9                 METHOD(ListBox, mouseMove, float(entity, vector));
10                 METHOD(ListBox, mousePress, float(entity, vector));
11                 METHOD(ListBox, mouseDrag, float(entity, vector));
12                 METHOD(ListBox, mouseRelease, float(entity, vector));
13                 METHOD(ListBox, focusLeave, void(entity));
14                 ATTRIB(ListBox, focusable, float, 1)
15                 ATTRIB(ListBox, focusedItem, int, -1)
16                 ATTRIB(ListBox, focusedItemAlpha, float, 0.3)
17                 METHOD(ListBox, setFocusedItem, void(entity, int));
18                 ATTRIB(ListBox, mouseMoveOffset, float, -1)  // let know where the cursor is when the list scrolls without moving the cursor
19                 ATTRIB(ListBox, allowFocusSound, float, 1)
20                 ATTRIB(ListBox, selectedItem, int, 0)
21                 ATTRIB(ListBox, size, vector, '0 0 0')
22                 ATTRIB(ListBox, origin, vector, '0 0 0')
23                 ATTRIB(ListBox, scrollPos, float, 0)  // measured in window heights, fixed when needed
24                 ATTRIB(ListBox, scrollPosTarget, float, 0)
25                 METHOD(ListBox, isScrolling, bool(entity));
26                 ATTRIB(ListBox, needScrollToItem, float, -1)
27                 METHOD(ListBox, scrollToItem, void(entity, int));
28                 ATTRIB(ListBox, previousValue, float, 0)
29                 ATTRIB(ListBox, pressed, float, 0)  // 0 = normal, 1 = scrollbar dragging, 2 = item dragging, 3 = released
30                 ATTRIB(ListBox, pressOffset, float, 0)
31
32                 METHOD(ListBox, updateControlTopBottom, void(entity));
33                 ATTRIB(ListBox, controlTop, float, 0)
34                 ATTRIB(ListBox, controlBottom, float, 0)
35                 ATTRIB(ListBox, controlWidth, float, 0)
36                 ATTRIB(ListBox, dragScrollPos, vector, '0 0 0')
37                 ATTRIB(ListBox, selectionDoesntMatter, bool, false) // improves scrolling by keys for lists that don't need to show an active selection
38
39                 ATTRIB(ListBox, src, string, string_null)           // scrollbar
40                 ATTRIB(ListBox, color, vector, '1 1 1')
41                 ATTRIB(ListBox, color2, vector, '1 1 1')
42                 ATTRIB(ListBox, colorC, vector, '1 1 1')
43                 ATTRIB(ListBox, colorF, vector, '1 1 1')
44                 ATTRIB(ListBox, tolerance, vector, '0 0 0') // drag tolerance
45                 ATTRIB(ListBox, scrollbarWidth, float, 0)   // pixels
46                 ATTRIB(ListBox, nItems, float, 42) // FIXME: why?!?
47                 ATTRIB(ListBox, itemHeight, float, 0)
48                 ATTRIB(ListBox, colorBG, vector, '0 0 0')
49                 ATTRIB(ListBox, alphaBG, float, 0)
50
51                 ATTRIB(ListBox, lastClickedItem, float, -1)
52                 ATTRIB(ListBox, lastClickedTime, float, 0)
53
54                 METHOD(ListBox, drawListBoxItem, void(entity, int, vector, bool, bool)); // item number, width/height, isSelected, isFocused
55                 METHOD(ListBox, clickListBoxItem, void(entity, float, vector));          // item number, relative clickpos
56                 METHOD(ListBox, doubleClickListBoxItem, void(entity, float, vector));    // item number, relative clickpos
57                 METHOD(ListBox, setSelected, void(entity, float));
58                 METHOD(ListBox, focusedItemChangeNotify, void(entity));
59
60                 METHOD(ListBox, getLastFullyVisibleItemAtScrollPos, float(entity, float));
61                 METHOD(ListBox, getFirstFullyVisibleItemAtScrollPos, float(entity, float));
62
63                 // NOTE: override these four methods if you want variable sized list items
64                 METHOD(ListBox, getTotalHeight, float(entity));
65                 METHOD(ListBox, getItemAtPos, float(entity, float));
66                 METHOD(ListBox, getItemStart, float(entity, float));
67                 METHOD(ListBox, getItemHeight, float(entity, float));
68                 // NOTE: if getItemAt* are overridden, it may make sense to cache the
69                 // start and height of the last item returned by getItemAtPos and fast
70                 // track returning their properties for getItemStart and getItemHeight.
71                 // The "hot" code path calls getItemAtPos first, then will query
72                 // getItemStart and getItemHeight on it soon.
73                 // When overriding, the following consistency rules must hold:
74                 // getTotalHeight() == SUM(getItemHeight(i), i, 0, me.nItems-1)
75                 // getItemStart(i+1) == getItemStart(i) + getItemHeight(i)
76                 //   for 0 <= i < me.nItems-1
77                 // getItemStart(0) == 0
78                 // getItemStart(getItemAtPos(p)) <= p
79                 //   if p >= 0
80                 // getItemAtPos(p) == 0
81                 //   if p < 0
82                 // getItemStart(getItemAtPos(p)) + getItemHeight(getItemAtPos(p)) > p
83                 //   if p < getTotalHeigt()
84                 // getItemAtPos(p) == me.nItems - 1
85                 //   if p >= getTotalHeight()
86         ENDCLASS(ListBox)
87 #endif
88
89 #ifdef IMPLEMENTATION
90         bool ListBox_isScrolling(entity me)
91         {
92                 return me.scrollPos != me.scrollPosTarget;
93         }
94
95         void ListBox_scrollToItem(entity me, int i)
96         {
97                 // scroll doesn't work properly until itemHeight is set to the correct value
98                 // at the first resizeNotify call
99                 if (me.itemHeight == 1)  // initial temporary value of itemHeight is 1
100                 {
101                         me.needScrollToItem = i;
102                         return;
103                 }
104
105                 i = bound(0, i, me.nItems - 1);
106
107                 // scroll the list to make sure the selected item is visible
108                 // (even if the selected item doesn't change).
109                 if (i < me.getFirstFullyVisibleItemAtScrollPos(me, me.scrollPos))
110                 {
111                         // above visible area
112                         me.scrollPosTarget = me.getItemStart(me, i);
113                 }
114                 else if (i > me.getLastFullyVisibleItemAtScrollPos(me, me.scrollPos))
115                 {
116                         // below visible area
117                         if (i == me.nItems - 1) me.scrollPosTarget = me.getTotalHeight(me) - 1;
118                         else me.scrollPosTarget = me.getItemStart(me, i + 1) - 1;
119                 }
120         }
121
122         void ListBox_setSelected(entity me, float i)
123         {
124                 i = bound(0, i, me.nItems - 1);
125                 me.scrollToItem(me, i);
126                 me.selectedItem = i;
127         }
128         void ListBox_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
129         {
130                 SUPER(ListBox).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
131                 me.controlWidth = me.scrollbarWidth / absSize.x;
132         }
133         void ListBox_configureListBox(entity me, float theScrollbarWidth, float theItemHeight)
134         {
135                 me.scrollbarWidth = theScrollbarWidth;
136                 me.itemHeight = theItemHeight;
137         }
138
139         float ListBox_getTotalHeight(entity me)
140         {
141                 return me.nItems * me.itemHeight;
142         }
143         float ListBox_getItemAtPos(entity me, float pos)
144         {
145                 return floor(pos / me.itemHeight);
146         }
147         float ListBox_getItemStart(entity me, float i)
148         {
149                 return me.itemHeight * i;
150         }
151         float ListBox_getItemHeight(entity me, float i)
152         {
153                 return me.itemHeight;
154         }
155
156         float ListBox_getLastFullyVisibleItemAtScrollPos(entity me, float pos)
157         {
158                 return me.getItemAtPos(me, pos + 0.999) - 1;
159         }
160         float ListBox_getFirstFullyVisibleItemAtScrollPos(entity me, float pos)
161         {
162                 return me.getItemAtPos(me, pos + 0.001) + 1;
163         }
164         float ListBox_keyDown(entity me, float key, float ascii, float shift)
165         {
166                 if (key == K_MWHEELUP)
167                 {
168                         me.scrollPosTarget = max(me.scrollPosTarget - 0.5, 0);
169                 }
170                 else if (key == K_MWHEELDOWN)
171                 {
172                         me.scrollPosTarget = min(me.scrollPosTarget + 0.5, max(0, me.getTotalHeight(me) - 1));
173                 }
174                 else if (key == K_PGUP || key == K_KP_PGUP)
175                 {
176                         if (me.selectionDoesntMatter)
177                         {
178                                 me.scrollPosTarget = max(me.scrollPosTarget - 0.5, 0);
179                                 return 1;
180                         }
181
182                         float i = me.selectedItem;
183                         float a = me.getItemHeight(me, i);
184                         for ( ; ; )
185                         {
186                                 --i;
187                                 if (i < 0) break;
188                                 a += me.getItemHeight(me, i);
189                                 if (a >= 1) break;
190                         }
191                         me.setSelected(me, i + 1);
192                 }
193                 else if (key == K_PGDN || key == K_KP_PGDN)
194                 {
195                         if (me.selectionDoesntMatter)
196                         {
197                                 me.scrollPosTarget = min(me.scrollPosTarget + 0.5, me.nItems * me.itemHeight - 1);
198                                 return 1;
199                         }
200
201                         float i = me.selectedItem;
202                         float a = me.getItemHeight(me, i);
203                         for ( ; ; )
204                         {
205                                 ++i;
206                                 if (i >= me.nItems) break;
207                                 a += me.getItemHeight(me, i);
208                                 if (a >= 1) break;
209                         }
210                         me.setSelected(me, i - 1);
211                 }
212                 else if (key == K_UPARROW || key == K_KP_UPARROW)
213                 {
214                         if (me.selectionDoesntMatter)
215                         {
216                                 me.scrollPosTarget = max(me.scrollPosTarget - me.itemHeight, 0);
217                                 return 1;
218                         }
219
220                         me.setSelected(me, me.selectedItem - 1);
221                 }
222                 else if (key == K_DOWNARROW || key == K_KP_DOWNARROW)
223                 {
224                         if (me.selectionDoesntMatter)
225                         {
226                                 me.scrollPosTarget = min(me.scrollPosTarget + me.itemHeight, me.nItems * me.itemHeight - 1);
227                                 return 1;
228                         }
229
230                         me.setSelected(me, me.selectedItem + 1);
231                 }
232                 else if (key == K_HOME || key == K_KP_HOME)
233                 {
234                         me.setSelected(me, 0);
235                 }
236                 else if (key == K_END || key == K_KP_END)
237                 {
238                         me.setSelected(me, me.nItems - 1);
239                 }
240                 else
241                 {
242                         return 0;
243                 }
244                 return 1;
245         }
246         float ListBox_mouseMove(entity me, vector pos)
247         {
248                 me.mouseMoveOffset = -1;
249                 if (pos_x < 0) return 0;
250                 if (pos_y < 0) return 0;
251                 if (pos_x >= 1) return 0;
252                 if (pos_y >= 1) return 0;
253                 if (pos_x < 1 - me.controlWidth)
254                 {
255                         me.mouseMoveOffset = pos.y;
256                 }
257                 else
258                 {
259                         me.setFocusedItem(me, -1);
260                         me.mouseMoveOffset = -1;
261                 }
262                 return 1;
263         }
264         float ListBox_mouseDrag(entity me, vector pos)
265         {
266                 float hit;
267                 me.updateControlTopBottom(me);
268                 me.dragScrollPos = pos;
269                 if (me.pressed == 1)
270                 {
271                         hit = 1;
272                         if (pos.x < 1 - me.controlWidth - me.tolerance.y * me.controlWidth) hit = 0;
273                         if (pos.y < 0 - me.tolerance.x) hit = 0;
274                         if (pos.x >= 1 + me.tolerance.y * me.controlWidth) hit = 0;
275                         if (pos.y >= 1 + me.tolerance.x) hit = 0;
276                         if (hit)
277                         {
278                                 // calculate new pos to v
279                                 float d;
280                                 d = (pos.y - me.pressOffset) / (1 - (me.controlBottom - me.controlTop)) * (me.getTotalHeight(me) - 1);
281                                 me.scrollPosTarget = me.previousValue + d;
282                         }
283                         else
284                         {
285                                 me.scrollPosTarget = me.previousValue;
286                         }
287                         me.scrollPosTarget = min(me.scrollPosTarget, me.getTotalHeight(me) - 1);
288                         me.scrollPosTarget = max(me.scrollPosTarget, 0);
289                 }
290                 else if (me.pressed == 2)
291                 {
292                         me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
293                         me.setFocusedItem(me, me.selectedItem);
294                         me.mouseMoveOffset = -1;
295                 }
296                 return 1;
297         }
298         float ListBox_mousePress(entity me, vector pos)
299         {
300                 if (pos.x < 0) return 0;
301                 if (pos.y < 0) return 0;
302                 if (pos.x >= 1) return 0;
303                 if (pos.y >= 1) return 0;
304                 me.dragScrollPos = pos;
305                 me.updateControlTopBottom(me);
306                 if (pos.x >= 1 - me.controlWidth)
307                 {
308                         // if hit, set me.pressed, otherwise scroll by one page
309                         if (pos.y < me.controlTop)
310                         {
311                                 // page up
312                                 me.scrollPosTarget = max(me.scrollPosTarget - 1, 0);
313                         }
314                         else if (pos.y > me.controlBottom)
315                         {
316                                 // page down
317                                 me.scrollPosTarget = min(me.scrollPosTarget + 1, me.getTotalHeight(me) - 1);
318                         }
319                         else
320                         {
321                                 me.pressed = 1;
322                                 me.pressOffset = pos.y;
323                                 me.previousValue = me.scrollPos;
324                         }
325                 }
326                 else
327                 {
328                         // continue doing that while dragging (even when dragging outside). When releasing, forward the click to the then selected item.
329                         me.pressed = 2;
330                         // an item has been clicked. Select it, ...
331                         me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
332                         me.setFocusedItem(me, me.selectedItem);
333                 }
334                 return 1;
335         }
336         void ListBox_setFocusedItem(entity me, int item)
337         {
338                 float focusedItem_save = me.focusedItem;
339                 me.focusedItem = (item < me.nItems) ? item : -1;
340                 if (focusedItem_save != me.focusedItem)
341                 {
342                         me.focusedItemChangeNotify(me);
343                         if (me.focusedItem >= 0) me.focusedItemAlpha = SKINALPHA_LISTBOX_FOCUSED;
344                 }
345         }
346         float ListBox_mouseRelease(entity me, vector pos)
347         {
348                 if (me.pressed == 1)
349                 {
350                         // slider dragging mode
351                         // in that case, nothing happens on releasing
352                 }
353                 else if (me.pressed == 2)
354                 {
355                         me.pressed = 3;  // do that here, so setSelected can know the mouse has been released
356                         // item dragging mode
357                         // select current one one last time...
358                         me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
359                         me.setFocusedItem(me, me.selectedItem);
360                         // and give it a nice click event
361                         if (me.nItems > 0)
362                         {
363                                 vector where = globalToBox(pos, eY * (me.getItemStart(me, me.selectedItem) - me.scrollPos), eX * (1 - me.controlWidth) + eY * me.getItemHeight(me, me.selectedItem));
364
365                                 if ((me.selectedItem == me.lastClickedItem) && (time < me.lastClickedTime + 0.3)) me.doubleClickListBoxItem(me, me.selectedItem, where);
366                                 else me.clickListBoxItem(me, me.selectedItem, where);
367
368                                 me.lastClickedItem = me.selectedItem;
369                                 me.lastClickedTime = time;
370                         }
371                 }
372                 me.pressed = 0;
373                 return 1;
374         }
375         void ListBox_focusLeave(entity me)
376         {
377                 // Reset the var pressed in case listbox loses focus
378                 // by a mouse click on an item of the list
379                 // for example showing a dialog on right click
380                 me.pressed = 0;
381                 me.setFocusedItem(me, -1);
382                 me.mouseMoveOffset = -1;
383         }
384         void ListBox_updateControlTopBottom(entity me)
385         {
386                 float f;
387                 // scrollPos is in 0..1 and indicates where the "page" currently shown starts.
388                 if (me.getTotalHeight(me) <= 1)
389                 {
390                         // we don't need no stinkin' scrollbar, we don't need no view control...
391                         me.controlTop = 0;
392                         me.controlBottom = 1;
393                         me.scrollPos = 0;
394                 }
395                 else
396                 {
397                         // if scroll pos is below end of list, fix it
398                         me.scrollPos = min(me.scrollPos, me.getTotalHeight(me) - 1);
399                         // if scroll pos is above beginning of list, fix it
400                         me.scrollPos = max(me.scrollPos, 0);
401                         // now that we know where the list is scrolled to, find out where to draw the control
402                         me.controlTop = max(0, me.scrollPos / me.getTotalHeight(me));
403                         me.controlBottom = min((me.scrollPos + 1) / me.getTotalHeight(me), 1);
404
405                         float minfactor;
406                         minfactor = 2 * me.controlWidth / me.size.y * me.size.x;
407                         f = me.controlBottom - me.controlTop;
408                         if (f < minfactor)  // FIXME good default?
409                         {
410                                 // f * X + 1 * (1-X) = minfactor
411                                 // (f - 1) * X + 1 = minfactor
412                                 // (f - 1) * X = minfactor - 1
413                                 // X = (minfactor - 1) / (f - 1)
414                                 f = (minfactor - 1) / (f - 1);
415                                 me.controlTop = me.controlTop * f + 0 * (1 - f);
416                                 me.controlBottom = me.controlBottom * f + 1 * (1 - f);
417                         }
418                 }
419         }
420         AUTOCVAR(menu_scroll_averaging_time, float, 0.16, "smooth scroll averaging time");
421 // scroll faster while dragging the scrollbar
422         AUTOCVAR(menu_scroll_averaging_time_pressed, float, 0.06, "smooth scroll averaging time when dragging the scrollbar");
423         void ListBox_draw(entity me)
424         {
425                 float i;
426                 vector absSize, fillSize = '0 0 0';
427                 vector oldshift, oldscale;
428
429                 // we can't do this in mouseMove as the list can scroll without moving the cursor
430                 if (me.mouseMoveOffset != -1) me.setFocusedItem(me, me.getItemAtPos(me, me.scrollPos + me.mouseMoveOffset));
431
432                 if (me.needScrollToItem >= 0)
433                 {
434                         me.scrollToItem(me, me.needScrollToItem);
435                         me.needScrollToItem = -1;
436                 }
437                 if (me.scrollPos != me.scrollPosTarget)
438                 {
439                         float averaging_time = (me.pressed == 1)
440                             ? autocvar_menu_scroll_averaging_time_pressed
441                                 : autocvar_menu_scroll_averaging_time;
442                         // this formula works with whatever framerate
443                         float f = averaging_time ? exp(-frametime / averaging_time) : 0;
444                         me.scrollPos = me.scrollPos * f + me.scrollPosTarget * (1 - f);
445                         if (fabs(me.scrollPos - me.scrollPosTarget) < 0.001) me.scrollPos = me.scrollPosTarget;
446                 }
447
448                 if (me.pressed == 2) me.mouseDrag(me, me.dragScrollPos);  // simulate mouseDrag event
449                 me.updateControlTopBottom(me);
450                 fillSize.x = (1 - me.controlWidth);
451                 if (me.alphaBG) draw_Fill('0 0 0', '0 1 0' + fillSize, me.colorBG, me.alphaBG);
452                 if (me.controlWidth)
453                 {
454                         draw_VertButtonPicture(eX * (1 - me.controlWidth), strcat(me.src, "_s"), eX * me.controlWidth + eY, me.color2, 1);
455                         if (me.getTotalHeight(me) > 1)
456                         {
457                                 vector o, s;
458                                 o = eX * (1 - me.controlWidth) + eY * me.controlTop;
459                                 s = eX * me.controlWidth + eY * (me.controlBottom - me.controlTop);
460                                 if (me.pressed == 1) draw_VertButtonPicture(o, strcat(me.src, "_c"), s, me.colorC, 1);
461                                 else if (me.focused) draw_VertButtonPicture(o, strcat(me.src, "_f"), s, me.colorF, 1);
462                                 else draw_VertButtonPicture(o, strcat(me.src, "_n"), s, me.color, 1);
463                         }
464                 }
465                 draw_SetClip();
466                 oldshift = draw_shift;
467                 oldscale = draw_scale;
468
469                 float y;
470                 i = me.getItemAtPos(me, me.scrollPos);
471                 y = me.getItemStart(me, i) - me.scrollPos;
472                 for ( ; i < me.nItems && y < 1; ++i)
473                 {
474                         draw_shift = boxToGlobal(eY * y, oldshift, oldscale);
475                         vector relSize = eX * (1 - me.controlWidth) + eY * me.getItemHeight(me, i);
476                         absSize = boxToGlobalSize(relSize, me.size);
477                         draw_scale = boxToGlobalSize(relSize, oldscale);
478                         me.drawListBoxItem(me, i, absSize, (me.selectedItem == i), (me.focusedItem == i));
479                         y += relSize.y;
480                 }
481                 draw_ClearClip();
482
483                 draw_shift = oldshift;
484                 draw_scale = oldscale;
485                 SUPER(ListBox).draw(me);
486         }
487
488         void ListBox_focusedItemChangeNotify(entity me)
489         {}
490
491         void ListBox_clickListBoxItem(entity me, float i, vector where)
492         {
493                 // template method
494         }
495
496         void ListBox_doubleClickListBoxItem(entity me, float i, vector where)
497         {
498                 // template method
499         }
500
501         void ListBox_drawListBoxItem(entity me, int i, vector absSize, bool isSelected, bool isFocused)
502         {
503                 draw_Text('0 0 0', sprintf(_("Item %d"), i), eX * (8 / absSize.x) + eY * (8 / absSize.y), (isSelected ? '0 1 0' : '1 1 1'), 1, 0);
504         }
505 #endif