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