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