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