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