]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/menu/item/listbox.qc
Merge branch 'master' into terencehill/menu_gametype_tooltips_2
[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)
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)
118                         me.scrollPosTarget = me.getTotalHeight(me) - 1;
119                 else
120                         me.scrollPosTarget = me.getItemStart(me, i + 1) - 1;
121         }
122 }
123
124 void ListBox_setSelected(entity me, float i)
125 {
126         i = bound(0, i, me.nItems - 1);
127         me.scrollToItem(me, i);
128         me.selectedItem = i;
129 }
130 void ListBox_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
131 {
132         SUPER(ListBox).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
133         me.controlWidth = me.scrollbarWidth / absSize.x;
134 }
135 void ListBox_configureListBox(entity me, float theScrollbarWidth, float theItemHeight)
136 {
137         me.scrollbarWidth = theScrollbarWidth;
138         me.itemHeight = theItemHeight;
139 }
140
141 float ListBox_getTotalHeight(entity me)
142 {
143         return me.nItems * me.itemHeight;
144 }
145 float ListBox_getItemAtPos(entity me, float pos)
146 {
147         return floor(pos / me.itemHeight);
148 }
149 float ListBox_getItemStart(entity me, float i)
150 {
151         return me.itemHeight * i;
152 }
153 float ListBox_getItemHeight(entity me, float i)
154 {
155         return me.itemHeight;
156 }
157
158 float ListBox_getLastFullyVisibleItemAtScrollPos(entity me, float pos)
159 {
160         return me.getItemAtPos(me, pos + 0.999) - 1;
161 }
162 float ListBox_getFirstFullyVisibleItemAtScrollPos(entity me, float pos)
163 {
164         return me.getItemAtPos(me, pos + 0.001) + 1;
165 }
166 float ListBox_keyDown(entity me, float key, float ascii, float shift)
167 {
168         if(key == K_MWHEELUP)
169         {
170                 me.scrollPosTarget = max(me.scrollPosTarget - 0.5, 0);
171         }
172         else if(key == K_MWHEELDOWN)
173         {
174                 me.scrollPosTarget = min(me.scrollPosTarget + 0.5, max(0, me.getTotalHeight(me) - 1));
175         }
176         else if(key == K_PGUP || key == K_KP_PGUP)
177         {
178                 if(me.selectionDoesntMatter)
179                 {
180                         me.scrollPosTarget = max(me.scrollPosTarget - 0.5, 0);
181                         return 1;
182                 }
183
184                 float i = me.selectedItem;
185                 float a = me.getItemHeight(me, i);
186                 for (;;)
187                 {
188                         --i;
189                         if (i < 0)
190                                 break;
191                         a += me.getItemHeight(me, i);
192                         if (a >= 1)
193                                 break;
194                 }
195                 me.setSelected(me, i + 1);
196         }
197         else if(key == K_PGDN || key == K_KP_PGDN)
198         {
199                 if(me.selectionDoesntMatter)
200                 {
201                         me.scrollPosTarget = min(me.scrollPosTarget + 0.5, me.nItems * me.itemHeight - 1);
202                         return 1;
203                 }
204
205                 float i = me.selectedItem;
206                 float a = me.getItemHeight(me, i);
207                 for (;;)
208                 {
209                         ++i;
210                         if (i >= me.nItems)
211                                 break;
212                         a += me.getItemHeight(me, i);
213                         if (a >= 1)
214                                 break;
215                 }
216                 me.setSelected(me, i - 1);
217         }
218         else if(key == K_UPARROW || key == K_KP_UPARROW)
219         {
220                 if(me.selectionDoesntMatter)
221                 {
222                         me.scrollPosTarget = max(me.scrollPosTarget - me.itemHeight, 0);
223                         return 1;
224                 }
225
226                 me.setSelected(me, me.selectedItem - 1);
227         }
228         else if(key == K_DOWNARROW || key == K_KP_DOWNARROW)
229         {
230                 if(me.selectionDoesntMatter)
231                 {
232                         me.scrollPosTarget = min(me.scrollPosTarget + me.itemHeight, me.nItems * me.itemHeight - 1);
233                         return 1;
234                 }
235
236                 me.setSelected(me, me.selectedItem + 1);
237         }
238         else if(key == K_HOME || key == K_KP_HOME)
239                 me.setSelected(me, 0);
240         else if(key == K_END || key == K_KP_END)
241                 me.setSelected(me, me.nItems - 1);
242         else
243                 return 0;
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                 me.mouseMoveOffset = pos.y;
255         else
256         {
257                 me.setFocusedItem(me, -1);
258                 me.mouseMoveOffset = -1;
259         }
260         return 1;
261 }
262 float ListBox_mouseDrag(entity me, vector pos)
263 {
264         float hit;
265         me.updateControlTopBottom(me);
266         me.dragScrollPos = pos;
267         if(me.pressed == 1)
268         {
269                 hit = 1;
270                 if(pos.x < 1 - me.controlWidth - me.tolerance.y * me.controlWidth) hit = 0;
271                 if(pos.y < 0 - me.tolerance.x) hit = 0;
272                 if(pos.x >= 1 + me.tolerance.y * me.controlWidth) hit = 0;
273                 if(pos.y >= 1 + me.tolerance.x) hit = 0;
274                 if(hit)
275                 {
276                         // calculate new pos to v
277                         float d;
278                         d = (pos.y - me.pressOffset) / (1 - (me.controlBottom - me.controlTop)) * (me.getTotalHeight(me) - 1);
279                         me.scrollPosTarget = me.previousValue + d;
280                 }
281                 else
282                         me.scrollPosTarget = me.previousValue;
283                 me.scrollPosTarget = min(me.scrollPosTarget, me.getTotalHeight(me) - 1);
284                 me.scrollPosTarget = max(me.scrollPosTarget, 0);
285         }
286         else if(me.pressed == 2)
287         {
288                 me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
289                 me.setFocusedItem(me, me.selectedItem);
290                 me.mouseMoveOffset = -1;
291         }
292         return 1;
293 }
294 float ListBox_mousePress(entity me, vector pos)
295 {
296         if(pos.x < 0) return 0;
297         if(pos.y < 0) return 0;
298         if(pos.x >= 1) return 0;
299         if(pos.y >= 1) return 0;
300         me.dragScrollPos = pos;
301         me.updateControlTopBottom(me);
302         if(pos.x >= 1 - me.controlWidth)
303         {
304                 // if hit, set me.pressed, otherwise scroll by one page
305                 if(pos.y < me.controlTop)
306                 {
307                         // page up
308                         me.scrollPosTarget = max(me.scrollPosTarget - 1, 0);
309                 }
310                 else if(pos.y > me.controlBottom)
311                 {
312                         // page down
313                         me.scrollPosTarget = min(me.scrollPosTarget + 1, me.getTotalHeight(me) - 1);
314                 }
315                 else
316                 {
317                         me.pressed = 1;
318                         me.pressOffset = pos.y;
319                         me.previousValue = me.scrollPos;
320                 }
321         }
322         else
323         {
324                 // continue doing that while dragging (even when dragging outside). When releasing, forward the click to the then selected item.
325                 me.pressed = 2;
326                 // an item has been clicked. Select it, ...
327                 me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
328                 me.setFocusedItem(me, me.selectedItem);
329         }
330         return 1;
331 }
332 void ListBox_setFocusedItem(entity me, int item)
333 {
334         float focusedItem_save = me.focusedItem;
335         me.focusedItem = (item < me.nItems) ? item : -1;
336         if(focusedItem_save != me.focusedItem)
337         {
338                 me.focusedItemChangeNotify(me);
339                 if(me.focusedItem >= 0)
340                         me.focusedItemAlpha = SKINALPHA_LISTBOX_FOCUSED;
341         }
342 }
343 float ListBox_mouseRelease(entity me, vector pos)
344 {
345         if(me.pressed == 1)
346         {
347                 // slider dragging mode
348                 // in that case, nothing happens on releasing
349         }
350         else if(me.pressed == 2)
351         {
352                 me.pressed = 3; // do that here, so setSelected can know the mouse has been released
353                 // item dragging mode
354                 // select current one one last time...
355                 me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
356                 me.setFocusedItem(me, me.selectedItem);
357                 // and give it a nice click event
358                 if(me.nItems > 0)
359                 {
360                         vector where = globalToBox(pos, eY * (me.getItemStart(me, me.selectedItem) - me.scrollPos), eX * (1 - me.controlWidth) + eY * me.getItemHeight(me, me.selectedItem));
361
362                         if((me.selectedItem == me.lastClickedItem) && (time < me.lastClickedTime + 0.3))
363                                 me.doubleClickListBoxItem(me, me.selectedItem, where);
364                         else
365                                 me.clickListBoxItem(me, me.selectedItem, where);
366
367                         me.lastClickedItem = me.selectedItem;
368                         me.lastClickedTime = time;
369                 }
370         }
371         me.pressed = 0;
372         return 1;
373 }
374 void ListBox_focusLeave(entity me)
375 {
376         // Reset the var pressed in case listbox loses focus
377         // by a mouse click on an item of the list
378         // for example showing a dialog on right click
379         me.pressed = 0;
380         me.setFocusedItem(me, -1);
381         me.mouseMoveOffset = -1;
382 }
383 void ListBox_updateControlTopBottom(entity me)
384 {
385         float f;
386         // scrollPos is in 0..1 and indicates where the "page" currently shown starts.
387         if(me.getTotalHeight(me) <= 1)
388         {
389                 // we don't need no stinkin' scrollbar, we don't need no view control...
390                 me.controlTop = 0;
391                 me.controlBottom = 1;
392                 me.scrollPos = 0;
393         }
394         else
395         {
396                 // if scroll pos is below end of list, fix it
397                 me.scrollPos = min(me.scrollPos, me.getTotalHeight(me) - 1);
398                 // if scroll pos is above beginning of list, fix it
399                 me.scrollPos = max(me.scrollPos, 0);
400                 // now that we know where the list is scrolled to, find out where to draw the control
401                 me.controlTop = max(0, me.scrollPos / me.getTotalHeight(me));
402                 me.controlBottom = min((me.scrollPos + 1) / me.getTotalHeight(me), 1);
403
404                 float minfactor;
405                 minfactor = 2 * me.controlWidth / me.size.y * me.size.x;
406                 f = me.controlBottom - me.controlTop;
407                 if(f < minfactor) // FIXME good default?
408                 {
409                         // f * X + 1 * (1-X) = minfactor
410                         // (f - 1) * X + 1 = minfactor
411                         // (f - 1) * X = minfactor - 1
412                         // X = (minfactor - 1) / (f - 1)
413                         f = (minfactor - 1) / (f - 1);
414                         me.controlTop = me.controlTop * f + 0 * (1 - f);
415                         me.controlBottom = me.controlBottom * f + 1 * (1 - f);
416                 }
417         }
418 }
419 AUTOCVAR(menu_scroll_averaging_time, float, 0.16, "smooth scroll averaging time");
420 // scroll faster while dragging the scrollbar
421 AUTOCVAR(menu_scroll_averaging_time_pressed, float, 0.06, "smooth scroll averaging time when dragging the scrollbar");
422 void ListBox_draw(entity me)
423 {
424         float i;
425         vector absSize, fillSize = '0 0 0';
426         vector oldshift, oldscale;
427
428         // we can't do this in mouseMove as the list can scroll without moving the cursor
429         if(me.mouseMoveOffset != -1)
430                 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)
446                         me.scrollPos = me.scrollPosTarget;
447         }
448
449         if(me.pressed == 2)
450                 me.mouseDrag(me, me.dragScrollPos); // simulate mouseDrag event
451         me.updateControlTopBottom(me);
452         fillSize.x = (1 - me.controlWidth);
453         if(me.alphaBG)
454                 draw_Fill('0 0 0', '0 1 0' + fillSize, me.colorBG, me.alphaBG);
455         if(me.controlWidth)
456         {
457                 draw_VertButtonPicture(eX * (1 - me.controlWidth), strcat(me.src, "_s"), eX * me.controlWidth + eY, me.color2, 1);
458                 if(me.getTotalHeight(me) > 1)
459                 {
460                         vector o, s;
461                         o = eX * (1 - me.controlWidth) + eY * me.controlTop;
462                         s = eX * me.controlWidth + eY * (me.controlBottom - me.controlTop);
463                         if(me.pressed == 1)
464                                 draw_VertButtonPicture(o, strcat(me.src, "_c"), s, me.colorC, 1);
465                         else if(me.focused)
466                                 draw_VertButtonPicture(o, strcat(me.src, "_f"), s, me.colorF, 1);
467                         else
468                                 draw_VertButtonPicture(o, strcat(me.src, "_n"), s, me.color, 1);
469                 }
470         }
471         draw_SetClip();
472         oldshift = draw_shift;
473         oldscale = draw_scale;
474
475         float y;
476         i = me.getItemAtPos(me, me.scrollPos);
477         y = me.getItemStart(me, i) - me.scrollPos;
478         for (; i < me.nItems && y < 1; ++i)
479         {
480                 draw_shift = boxToGlobal(eY * y, oldshift, oldscale);
481                 vector relSize = eX * (1 - me.controlWidth) + eY * me.getItemHeight(me, i);
482                 absSize = boxToGlobalSize(relSize, me.size);
483                 draw_scale = boxToGlobalSize(relSize, oldscale);
484                 me.drawListBoxItem(me, i, absSize, (me.selectedItem == i), (me.focusedItem == i));
485                 y += relSize.y;
486         }
487         draw_ClearClip();
488
489         draw_shift = oldshift;
490         draw_scale = oldscale;
491         SUPER(ListBox).draw(me);
492 }
493
494 void ListBox_focusedItemChangeNotify(entity me)
495 {
496 }
497
498 void ListBox_clickListBoxItem(entity me, float i, vector where)
499 {
500         // template method
501 }
502
503 void ListBox_doubleClickListBoxItem(entity me, float i, vector where)
504 {
505         // template method
506 }
507
508 void ListBox_drawListBoxItem(entity me, int i, vector absSize, bool isSelected, bool isFocused)
509 {
510         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);
511 }
512 #endif