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