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