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