4 METHOD(ListBox, resizeNotify, void(entity, vector, vector, vector, vector))
5 METHOD(ListBox, configureListBox, void(entity, float, float))
6 METHOD(ListBox, draw, void(entity))
7 METHOD(ListBox, keyDown, float(entity, float, float, float))
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, allowFocusSound, float, 1)
14 ATTRIB(ListBox, selectedItem, int, 0)
15 ATTRIB(ListBox, size, vector, '0 0 0')
16 ATTRIB(ListBox, origin, vector, '0 0 0')
17 ATTRIB(ListBox, scrollPos, float, 0) // measured in window heights, fixed when needed
18 ATTRIB(ListBox, previousValue, float, 0)
19 ATTRIB(ListBox, pressed, float, 0) // 0 = normal, 1 = scrollbar dragging, 2 = item dragging, 3 = released
20 ATTRIB(ListBox, pressOffset, float, 0)
22 METHOD(ListBox, updateControlTopBottom, void(entity))
23 ATTRIB(ListBox, controlTop, float, 0)
24 ATTRIB(ListBox, controlBottom, float, 0)
25 ATTRIB(ListBox, controlWidth, float, 0)
26 ATTRIB(ListBox, dragScrollTimer, float, 0)
27 ATTRIB(ListBox, dragScrollPos, vector, '0 0 0')
29 ATTRIB(ListBox, src, string, string_null) // scrollbar
30 ATTRIB(ListBox, color, vector, '1 1 1')
31 ATTRIB(ListBox, color2, vector, '1 1 1')
32 ATTRIB(ListBox, colorC, vector, '1 1 1')
33 ATTRIB(ListBox, colorF, vector, '1 1 1')
34 ATTRIB(ListBox, tolerance, vector, '0 0 0') // drag tolerance
35 ATTRIB(ListBox, scrollbarWidth, float, 0) // pixels
36 ATTRIB(ListBox, nItems, float, 42)
37 ATTRIB(ListBox, itemHeight, float, 0)
38 ATTRIB(ListBox, colorBG, vector, '0 0 0')
39 ATTRIB(ListBox, alphaBG, float, 0)
41 ATTRIB(ListBox, lastClickedItem, float, -1)
42 ATTRIB(ListBox, lastClickedTime, float, 0)
44 METHOD(ListBox, drawListBoxItem, void(entity, float, vector, float)) // item number, width/height, selected
45 METHOD(ListBox, clickListBoxItem, void(entity, float, vector)) // item number, relative clickpos
46 METHOD(ListBox, doubleClickListBoxItem, void(entity, float, vector)) // item number, relative clickpos
47 METHOD(ListBox, setSelected, void(entity, float))
49 METHOD(ListBox, getLastFullyVisibleItemAtScrollPos, float(entity, float))
50 METHOD(ListBox, getFirstFullyVisibleItemAtScrollPos, float(entity, float))
52 // NOTE: override these four methods if you want variable sized list items
53 METHOD(ListBox, getTotalHeight, float(entity))
54 METHOD(ListBox, getItemAtPos, float(entity, float))
55 METHOD(ListBox, getItemStart, float(entity, float))
56 METHOD(ListBox, getItemHeight, float(entity, float))
57 // NOTE: if getItemAt* are overridden, it may make sense to cache the
58 // start and height of the last item returned by getItemAtPos and fast
59 // track returning their properties for getItemStart and getItemHeight.
60 // The "hot" code path calls getItemAtPos first, then will query
61 // getItemStart and getItemHeight on it soon.
62 // When overriding, the following consistency rules must hold:
63 // getTotalHeight() == SUM(getItemHeight(i), i, 0, me.nItems-1)
64 // getItemStart(i+1) == getItemStart(i) + getItemHeight(i)
65 // for 0 <= i < me.nItems-1
66 // getItemStart(0) == 0
67 // getItemStart(getItemAtPos(p)) <= p
69 // getItemAtPos(p) == 0
71 // getItemStart(getItemAtPos(p)) + getItemHeight(getItemAtPos(p)) > p
72 // if p < getTotalHeigt()
73 // getItemAtPos(p) == me.nItems - 1
74 // if p >= getTotalHeight()
79 void ListBox_setSelected(entity me, float i)
81 me.selectedItem = bound(0, i, me.nItems - 1);
83 void ListBox_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
85 SUPER(ListBox).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
86 me.controlWidth = me.scrollbarWidth / absSize.x;
88 void ListBox_configureListBox(entity me, float theScrollbarWidth, float theItemHeight)
90 me.scrollbarWidth = theScrollbarWidth;
91 me.itemHeight = theItemHeight;
94 float ListBox_getTotalHeight(entity me)
96 return me.nItems * me.itemHeight;
98 float ListBox_getItemAtPos(entity me, float pos)
100 return floor(pos / me.itemHeight);
102 float ListBox_getItemStart(entity me, float i)
104 return me.itemHeight * i;
106 float ListBox_getItemHeight(entity me, float i)
108 return me.itemHeight;
111 float ListBox_getLastFullyVisibleItemAtScrollPos(entity me, float pos)
113 return me.getItemAtPos(me, pos + 1.001) - 1;
115 float ListBox_getFirstFullyVisibleItemAtScrollPos(entity me, float pos)
117 return me.getItemAtPos(me, pos - 0.001) + 1;
119 float ListBox_keyDown(entity me, float key, float ascii, float shift)
121 me.dragScrollTimer = time;
122 if(key == K_MWHEELUP)
124 me.scrollPos = max(me.scrollPos - 0.5, 0);
125 me.setSelected(me, min(me.selectedItem, me.getLastFullyVisibleItemAtScrollPos(me, me.scrollPos)));
127 else if(key == K_MWHEELDOWN)
129 me.scrollPos = min(me.scrollPos + 0.5, me.getTotalHeight(me) - 1);
130 me.setSelected(me, max(me.selectedItem, me.getFirstFullyVisibleItemAtScrollPos(me, me.scrollPos)));
132 else if(key == K_PGUP || key == K_KP_PGUP)
134 float i = me.selectedItem;
135 float a = me.getItemHeight(me, i);
141 a += me.getItemHeight(me, i);
145 me.setSelected(me, i + 1);
147 else if(key == K_PGDN || key == K_KP_PGDN)
149 float i = me.selectedItem;
150 float a = me.getItemHeight(me, i);
156 a += me.getItemHeight(me, i);
160 me.setSelected(me, i - 1);
162 else if(key == K_UPARROW || key == K_KP_UPARROW)
163 me.setSelected(me, me.selectedItem - 1);
164 else if(key == K_DOWNARROW || key == K_KP_DOWNARROW)
165 me.setSelected(me, me.selectedItem + 1);
166 else if(key == K_HOME || key == K_KP_HOME)
169 me.setSelected(me, 0);
171 else if(key == K_END || key == K_KP_END)
173 me.scrollPos = max(0, me.getTotalHeight(me) - 1);
174 me.setSelected(me, me.nItems - 1);
180 float ListBox_mouseDrag(entity me, vector pos)
184 me.updateControlTopBottom(me);
185 me.dragScrollPos = pos;
189 if(pos.x < 1 - me.controlWidth - me.tolerance.y * me.controlWidth) hit = 0;
190 if(pos.y < 0 - me.tolerance.x) hit = 0;
191 if(pos.x >= 1 + me.tolerance.y * me.controlWidth) hit = 0;
192 if(pos.y >= 1 + me.tolerance.x) hit = 0;
195 // calculate new pos to v
197 d = (pos.y - me.pressOffset) / (1 - (me.controlBottom - me.controlTop)) * (me.getTotalHeight(me) - 1);
198 me.scrollPos = me.previousValue + d;
201 me.scrollPos = me.previousValue;
202 me.scrollPos = min(me.scrollPos, me.getTotalHeight(me) - 1);
203 me.scrollPos = max(me.scrollPos, 0);
204 i = min(me.selectedItem, me.getLastFullyVisibleItemAtScrollPos(me, me.scrollPos));
205 i = max(i, ListBox_getFirstFullyVisibleItemAtScrollPos(me, me.scrollPos));
206 me.setSelected(me, i);
208 else if(me.pressed == 2)
210 me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
214 float ListBox_mousePress(entity me, vector pos)
216 if(pos.x < 0) return 0;
217 if(pos.y < 0) return 0;
218 if(pos.x >= 1) return 0;
219 if(pos.y >= 1) return 0;
220 me.dragScrollPos = pos;
221 me.updateControlTopBottom(me);
222 me.dragScrollTimer = time;
223 if(pos.x >= 1 - me.controlWidth)
225 // if hit, set me.pressed, otherwise scroll by one page
226 if(pos.y < me.controlTop)
229 me.scrollPos = max(me.scrollPos - 1, 0);
230 me.setSelected(me, min(me.selectedItem, ListBox_getLastFullyVisibleItemAtScrollPos(me, me.scrollPos)));
232 else if(pos.y > me.controlBottom)
235 me.scrollPos = min(me.scrollPos + 1, me.getTotalHeight(me) - 1);
236 me.setSelected(me, max(me.selectedItem, ListBox_getFirstFullyVisibleItemAtScrollPos(me, me.scrollPos)));
241 me.pressOffset = pos.y;
242 me.previousValue = me.scrollPos;
247 // continue doing that while dragging (even when dragging outside). When releasing, forward the click to the then selected item.
249 // an item has been clicked. Select it, ...
250 me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
254 float ListBox_mouseRelease(entity me, vector pos)
258 // slider dragging mode
259 // in that case, nothing happens on releasing
261 else if(me.pressed == 2)
263 me.pressed = 3; // do that here, so setSelected can know the mouse has been released
264 // item dragging mode
265 // select current one one last time...
266 me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
267 // and give it a nice click event
270 vector where = globalToBox(pos, eY * (me.getItemStart(me, me.selectedItem) - me.scrollPos), eX * (1 - me.controlWidth) + eY * me.getItemHeight(me, me.selectedItem));
272 if((me.selectedItem == me.lastClickedItem) && (time < me.lastClickedTime + 0.3))
273 me.doubleClickListBoxItem(me, me.selectedItem, where);
275 me.clickListBoxItem(me, me.selectedItem, where);
277 me.lastClickedItem = me.selectedItem;
278 me.lastClickedTime = time;
284 void ListBox_focusLeave(entity me)
286 // Reset the var pressed in case listbox loses focus
287 // by a mouse click on an item of the list
288 // for example showing a dialog on right click
291 void ListBox_updateControlTopBottom(entity me)
294 // scrollPos is in 0..1 and indicates where the "page" currently shown starts.
295 if(me.getTotalHeight(me) <= 1)
297 // we don't need no stinkin' scrollbar, we don't need no view control...
299 me.controlBottom = 1;
304 if(frametime) // only do this in draw frames
306 if(me.dragScrollTimer < time)
310 // if selected item is below listbox, increase scrollpos so it is in
311 me.scrollPos = max(me.scrollPos, me.getItemStart(me, me.selectedItem) + me.getItemHeight(me, me.selectedItem) - 1);
312 // if selected item is above listbox, decrease scrollpos so it is in
313 me.scrollPos = min(me.scrollPos, me.getItemStart(me, me.selectedItem));
314 if(me.scrollPos != save)
315 me.dragScrollTimer = time + 0.2;
318 // if scroll pos is below end of list, fix it
319 me.scrollPos = min(me.scrollPos, me.getTotalHeight(me) - 1);
320 // if scroll pos is above beginning of list, fix it
321 me.scrollPos = max(me.scrollPos, 0);
322 // now that we know where the list is scrolled to, find out where to draw the control
323 me.controlTop = max(0, me.scrollPos / me.getTotalHeight(me));
324 me.controlBottom = min((me.scrollPos + 1) / me.getTotalHeight(me), 1);
327 minfactor = 2 * me.controlWidth / me.size.y * me.size.x;
328 f = me.controlBottom - me.controlTop;
329 if(f < minfactor) // FIXME good default?
331 // f * X + 1 * (1-X) = minfactor
332 // (f - 1) * X + 1 = minfactor
333 // (f - 1) * X = minfactor - 1
334 // X = (minfactor - 1) / (f - 1)
335 f = (minfactor - 1) / (f - 1);
336 me.controlTop = me.controlTop * f + 0 * (1 - f);
337 me.controlBottom = me.controlBottom * f + 1 * (1 - f);
341 void ListBox_draw(entity me)
344 vector absSize, fillSize = '0 0 0';
345 vector oldshift, oldscale;
347 me.mouseDrag(me, me.dragScrollPos); // simulate mouseDrag event
348 me.updateControlTopBottom(me);
349 fillSize.x = (1 - me.controlWidth);
351 draw_Fill('0 0 0', '0 1 0' + fillSize, me.colorBG, me.alphaBG);
354 draw_VertButtonPicture(eX * (1 - me.controlWidth), strcat(me.src, "_s"), eX * me.controlWidth + eY, me.color2, 1);
355 if(me.getTotalHeight(me) > 1)
358 o = eX * (1 - me.controlWidth) + eY * me.controlTop;
359 s = eX * me.controlWidth + eY * (me.controlBottom - me.controlTop);
361 draw_VertButtonPicture(o, strcat(me.src, "_c"), s, me.colorC, 1);
363 draw_VertButtonPicture(o, strcat(me.src, "_f"), s, me.colorF, 1);
365 draw_VertButtonPicture(o, strcat(me.src, "_n"), s, me.color, 1);
369 oldshift = draw_shift;
370 oldscale = draw_scale;
372 i = me.getItemAtPos(me, me.scrollPos);
373 y = me.getItemStart(me, i) - me.scrollPos;
374 for (; i < me.nItems && y < 1; ++i)
376 draw_shift = boxToGlobal(eY * y, oldshift, oldscale);
377 vector relSize = eX * (1 - me.controlWidth) + eY * me.getItemHeight(me, i);
378 absSize = boxToGlobalSize(relSize, me.size);
379 draw_scale = boxToGlobalSize(relSize, oldscale);
380 me.drawListBoxItem(me, i, absSize, (me.selectedItem == i));
385 draw_shift = oldshift;
386 draw_scale = oldscale;
387 SUPER(ListBox).draw(me);
390 void ListBox_clickListBoxItem(entity me, float i, vector where)
395 void ListBox_doubleClickListBoxItem(entity me, float i, vector where)
400 void ListBox_drawListBoxItem(entity me, float i, vector absSize, float selected)
402 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);