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