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