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