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