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