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