]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/menu/item/listbox.qc
Require semicolon or definition following `METHOD`
[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 averaging_time = 0.16;
422                 if(me.pressed == 1)
423                         averaging_time = 0.06; // scroll faster while dragging the scrollbar
424                 // this formula works with whatever framerate
425                 float f = exp(-frametime / averaging_time);
426                 me.scrollPos = me.scrollPos * f + me.scrollPosTarget * (1 - f);
427                 if(fabs(me.scrollPos - me.scrollPosTarget) < 0.001)
428                         me.scrollPos = me.scrollPosTarget;
429         }
430
431         if(me.pressed == 2)
432                 me.mouseDrag(me, me.dragScrollPos); // simulate mouseDrag event
433         me.updateControlTopBottom(me);
434         fillSize.x = (1 - me.controlWidth);
435         if(me.alphaBG)
436                 draw_Fill('0 0 0', '0 1 0' + fillSize, me.colorBG, me.alphaBG);
437         if(me.controlWidth)
438         {
439                 draw_VertButtonPicture(eX * (1 - me.controlWidth), strcat(me.src, "_s"), eX * me.controlWidth + eY, me.color2, 1);
440                 if(me.getTotalHeight(me) > 1)
441                 {
442                         vector o, s;
443                         o = eX * (1 - me.controlWidth) + eY * me.controlTop;
444                         s = eX * me.controlWidth + eY * (me.controlBottom - me.controlTop);
445                         if(me.pressed == 1)
446                                 draw_VertButtonPicture(o, strcat(me.src, "_c"), s, me.colorC, 1);
447                         else if(me.focused)
448                                 draw_VertButtonPicture(o, strcat(me.src, "_f"), s, me.colorF, 1);
449                         else
450                                 draw_VertButtonPicture(o, strcat(me.src, "_n"), s, me.color, 1);
451                 }
452         }
453         draw_SetClip();
454         oldshift = draw_shift;
455         oldscale = draw_scale;
456
457         float y;
458         i = me.getItemAtPos(me, me.scrollPos);
459         y = me.getItemStart(me, i) - me.scrollPos;
460         for (; i < me.nItems && y < 1; ++i)
461         {
462                 draw_shift = boxToGlobal(eY * y, oldshift, oldscale);
463                 vector relSize = eX * (1 - me.controlWidth) + eY * me.getItemHeight(me, i);
464                 absSize = boxToGlobalSize(relSize, me.size);
465                 draw_scale = boxToGlobalSize(relSize, oldscale);
466                 me.drawListBoxItem(me, i, absSize, (me.selectedItem == i), (me.focusedItem == i));
467                 y += relSize.y;
468         }
469         draw_ClearClip();
470
471         draw_shift = oldshift;
472         draw_scale = oldscale;
473         SUPER(ListBox).draw(me);
474 }
475
476 void ListBox_clickListBoxItem(entity me, float i, vector where)
477 {
478         // template method
479 }
480
481 void ListBox_doubleClickListBoxItem(entity me, float i, vector where)
482 {
483         // template method
484 }
485
486 void ListBox_drawListBoxItem(entity me, int i, vector absSize, bool isSelected, bool isFocused)
487 {
488         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);
489 }
490 #endif