]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/menu/item/listbox.qc
Don't display any tooltip while a list is scrolling
[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, 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;
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 void ListBox_draw(entity me)
420 {
421         float i;
422         vector absSize, fillSize = '0 0 0';
423         vector oldshift, oldscale;
424
425         // we can't do this in mouseMove as the list can scroll without moving the cursor
426         if(me.mouseMoveOffset != -1)
427                 me.setFocusedItem(me, me.getItemAtPos(me, me.scrollPos + me.mouseMoveOffset));
428
429         if(me.needScrollToItem >= 0)
430         {
431                 me.scrollToItem(me, me.needScrollToItem);
432                 me.needScrollToItem = -1;
433         }
434         if(me.scrollPos != me.scrollPosTarget)
435         {
436                 float averaging_time = 0.16;
437                 if(me.pressed == 1)
438                         averaging_time = 0.06; // scroll faster while dragging the scrollbar
439                 // this formula works with whatever framerate
440                 float f = exp(-frametime / averaging_time);
441                 me.scrollPos = me.scrollPos * f + me.scrollPosTarget * (1 - f);
442                 if(fabs(me.scrollPos - me.scrollPosTarget) < 0.001)
443                         me.scrollPos = me.scrollPosTarget;
444         }
445
446         if(me.pressed == 2)
447                 me.mouseDrag(me, me.dragScrollPos); // simulate mouseDrag event
448         me.updateControlTopBottom(me);
449         fillSize.x = (1 - me.controlWidth);
450         if(me.alphaBG)
451                 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)
461                                 draw_VertButtonPicture(o, strcat(me.src, "_c"), s, me.colorC, 1);
462                         else if(me.focused)
463                                 draw_VertButtonPicture(o, strcat(me.src, "_f"), s, me.colorF, 1);
464                         else
465                                 draw_VertButtonPicture(o, strcat(me.src, "_n"), s, me.color, 1);
466                 }
467         }
468         draw_SetClip();
469         oldshift = draw_shift;
470         oldscale = draw_scale;
471
472         float y;
473         i = me.getItemAtPos(me, me.scrollPos);
474         y = me.getItemStart(me, i) - me.scrollPos;
475         for (; i < me.nItems && y < 1; ++i)
476         {
477                 draw_shift = boxToGlobal(eY * y, oldshift, oldscale);
478                 vector relSize = eX * (1 - me.controlWidth) + eY * me.getItemHeight(me, i);
479                 absSize = boxToGlobalSize(relSize, me.size);
480                 draw_scale = boxToGlobalSize(relSize, oldscale);
481                 me.drawListBoxItem(me, i, absSize, (me.selectedItem == i), (me.focusedItem == i));
482                 y += relSize.y;
483         }
484         draw_ClearClip();
485
486         draw_shift = oldshift;
487         draw_scale = oldscale;
488         SUPER(ListBox).draw(me);
489 }
490
491 void ListBox_focusedItemChangeNotify(entity me)
492 {
493 }
494
495 void ListBox_clickListBoxItem(entity me, float i, vector where)
496 {
497         // template method
498 }
499
500 void ListBox_doubleClickListBoxItem(entity me, float i, vector where)
501 {
502         // template method
503 }
504
505 void ListBox_drawListBoxItem(entity me, int i, vector absSize, bool isSelected, bool isFocused)
506 {
507         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);
508 }
509 #endif