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