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