5 METHOD(ListBox, resizeNotify, void(entity, vector, vector, vector, vector))
6 METHOD(ListBox, configureListBox, void(entity, float, float))
7 METHOD(ListBox, draw, void(entity))
8 METHOD(ListBox, keyDown, float(entity, float, float, float))
9 METHOD(ListBox, mousePress, float(entity, vector))
10 METHOD(ListBox, mouseDrag, float(entity, vector))
11 METHOD(ListBox, mouseRelease, float(entity, vector))
12 METHOD(ListBox, focusLeave, void(entity))
13 ATTRIB(ListBox, focusable, float, 1)
14 ATTRIB(ListBox, allowFocusSound, float, 1)
15 ATTRIB(ListBox, selectedItem, int, 0)
16 ATTRIB(ListBox, size, vector, '0 0 0')
17 ATTRIB(ListBox, origin, vector, '0 0 0')
18 ATTRIB(ListBox, scrollPos, float, 0) // measured in window heights, fixed when needed
19 ATTRIB(ListBox, previousValue, float, 0)
20 ATTRIB(ListBox, pressed, float, 0) // 0 = normal, 1 = scrollbar dragging, 2 = item dragging, 3 = released
21 ATTRIB(ListBox, pressOffset, float, 0)
23 METHOD(ListBox, updateControlTopBottom, void(entity))
24 ATTRIB(ListBox, controlTop, float, 0)
25 ATTRIB(ListBox, controlBottom, float, 0)
26 ATTRIB(ListBox, controlWidth, float, 0)
27 ATTRIB(ListBox, dragScrollTimer, float, 0)
28 ATTRIB(ListBox, dragScrollPos, vector, '0 0 0')
30 ATTRIB(ListBox, src, string, string_null) // scrollbar
31 ATTRIB(ListBox, color, vector, '1 1 1')
32 ATTRIB(ListBox, color2, vector, '1 1 1')
33 ATTRIB(ListBox, colorC, vector, '1 1 1')
34 ATTRIB(ListBox, colorF, vector, '1 1 1')
35 ATTRIB(ListBox, tolerance, vector, '0 0 0') // drag tolerance
36 ATTRIB(ListBox, scrollbarWidth, float, 0) // pixels
37 ATTRIB(ListBox, nItems, float, 42)
38 ATTRIB(ListBox, itemHeight, float, 0)
39 ATTRIB(ListBox, colorBG, vector, '0 0 0')
40 ATTRIB(ListBox, alphaBG, float, 0)
42 ATTRIB(ListBox, lastClickedItem, float, -1)
43 ATTRIB(ListBox, lastClickedTime, float, 0)
45 METHOD(ListBox, drawListBoxItem, void(entity, float, vector, float)) // item number, width/height, selected
46 METHOD(ListBox, clickListBoxItem, void(entity, float, vector)) // item number, relative clickpos
47 METHOD(ListBox, doubleClickListBoxItem, void(entity, float, vector)) // item number, relative clickpos
48 METHOD(ListBox, setSelected, void(entity, float))
50 METHOD(ListBox, getLastFullyVisibleItemAtScrollPos, float(entity, float))
51 METHOD(ListBox, getFirstFullyVisibleItemAtScrollPos, float(entity, float))
53 // NOTE: override these four methods if you want variable sized list items
54 METHOD(ListBox, getTotalHeight, float(entity))
55 METHOD(ListBox, getItemAtPos, float(entity, float))
56 METHOD(ListBox, getItemStart, float(entity, float))
57 METHOD(ListBox, getItemHeight, float(entity, float))
58 // NOTE: if getItemAt* are overridden, it may make sense to cache the
59 // start and height of the last item returned by getItemAtPos and fast
60 // track returning their properties for getItemStart and getItemHeight.
61 // The "hot" code path calls getItemAtPos first, then will query
62 // getItemStart and getItemHeight on it soon.
63 // When overriding, the following consistency rules must hold:
64 // getTotalHeight() == SUM(getItemHeight(i), i, 0, me.nItems-1)
65 // getItemStart(i+1) == getItemStart(i) + getItemHeight(i)
66 // for 0 <= i < me.nItems-1
67 // getItemStart(0) == 0
68 // getItemStart(getItemAtPos(p)) <= p
70 // getItemAtPos(p) == 0
72 // getItemStart(getItemAtPos(p)) + getItemHeight(getItemAtPos(p)) > p
73 // if p < getTotalHeigt()
74 // getItemAtPos(p) == me.nItems - 1
75 // if p >= getTotalHeight()
80 void ListBox_setSelected(entity me, float i)
82 me.selectedItem = bound(0, i, me.nItems - 1);
84 void ListBox_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
86 SUPER(ListBox).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
87 me.controlWidth = me.scrollbarWidth / absSize.x;
89 void ListBox_configureListBox(entity me, float theScrollbarWidth, float theItemHeight)
91 me.scrollbarWidth = theScrollbarWidth;
92 me.itemHeight = theItemHeight;
95 float ListBox_getTotalHeight(entity me)
97 return me.nItems * me.itemHeight;
99 float ListBox_getItemAtPos(entity me, float pos)
101 return floor(pos / me.itemHeight);
103 float ListBox_getItemStart(entity me, float i)
105 return me.itemHeight * i;
107 float ListBox_getItemHeight(entity me, float i)
109 return me.itemHeight;
112 float ListBox_getLastFullyVisibleItemAtScrollPos(entity me, float pos)
114 return me.getItemAtPos(me, pos + 1.001) - 1;
116 float ListBox_getFirstFullyVisibleItemAtScrollPos(entity me, float pos)
118 return me.getItemAtPos(me, pos - 0.001) + 1;
120 float ListBox_keyDown(entity me, float key, float ascii, float shift)
122 me.dragScrollTimer = time;
123 if(key == K_MWHEELUP)
125 me.scrollPos = max(me.scrollPos - 0.5, 0);
126 me.setSelected(me, min(me.selectedItem, me.getLastFullyVisibleItemAtScrollPos(me, me.scrollPos)));
128 else if(key == K_MWHEELDOWN)
130 me.scrollPos = min(me.scrollPos + 0.5, me.getTotalHeight(me) - 1);
131 me.setSelected(me, max(me.selectedItem, me.getFirstFullyVisibleItemAtScrollPos(me, me.scrollPos)));
133 else if(key == K_PGUP || key == K_KP_PGUP)
135 float i = me.selectedItem;
136 float a = me.getItemHeight(me, i);
142 a += me.getItemHeight(me, i);
146 me.setSelected(me, i + 1);
148 else if(key == K_PGDN || key == K_KP_PGDN)
150 float i = me.selectedItem;
151 float a = me.getItemHeight(me, i);
157 a += me.getItemHeight(me, i);
161 me.setSelected(me, i - 1);
163 else if(key == K_UPARROW || key == K_KP_UPARROW)
164 me.setSelected(me, me.selectedItem - 1);
165 else if(key == K_DOWNARROW || key == K_KP_DOWNARROW)
166 me.setSelected(me, me.selectedItem + 1);
167 else if(key == K_HOME || key == K_KP_HOME)
170 me.setSelected(me, 0);
172 else if(key == K_END || key == K_KP_END)
174 me.scrollPos = max(0, me.getTotalHeight(me) - 1);
175 me.setSelected(me, me.nItems - 1);
181 float ListBox_mouseDrag(entity me, vector pos)
185 me.updateControlTopBottom(me);
186 me.dragScrollPos = pos;
190 if(pos.x < 1 - me.controlWidth - me.tolerance.y * me.controlWidth) hit = 0;
191 if(pos.y < 0 - me.tolerance.x) hit = 0;
192 if(pos.x >= 1 + me.tolerance.y * me.controlWidth) hit = 0;
193 if(pos.y >= 1 + me.tolerance.x) hit = 0;
196 // calculate new pos to v
198 d = (pos.y - me.pressOffset) / (1 - (me.controlBottom - me.controlTop)) * (me.getTotalHeight(me) - 1);
199 me.scrollPos = me.previousValue + d;
202 me.scrollPos = me.previousValue;
203 me.scrollPos = min(me.scrollPos, me.getTotalHeight(me) - 1);
204 me.scrollPos = max(me.scrollPos, 0);
205 i = min(me.selectedItem, me.getLastFullyVisibleItemAtScrollPos(me, me.scrollPos));
206 i = max(i, ListBox_getFirstFullyVisibleItemAtScrollPos(me, me.scrollPos));
207 me.setSelected(me, i);
209 else if(me.pressed == 2)
211 me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
215 float ListBox_mousePress(entity me, vector pos)
217 if(pos.x < 0) return 0;
218 if(pos.y < 0) return 0;
219 if(pos.x >= 1) return 0;
220 if(pos.y >= 1) return 0;
221 me.dragScrollPos = pos;
222 me.updateControlTopBottom(me);
223 me.dragScrollTimer = time;
224 if(pos.x >= 1 - me.controlWidth)
226 // if hit, set me.pressed, otherwise scroll by one page
227 if(pos.y < me.controlTop)
230 me.scrollPos = max(me.scrollPos - 1, 0);
231 me.setSelected(me, min(me.selectedItem, ListBox_getLastFullyVisibleItemAtScrollPos(me, me.scrollPos)));
233 else if(pos.y > me.controlBottom)
236 me.scrollPos = min(me.scrollPos + 1, me.getTotalHeight(me) - 1);
237 me.setSelected(me, max(me.selectedItem, ListBox_getFirstFullyVisibleItemAtScrollPos(me, me.scrollPos)));
242 me.pressOffset = pos.y;
243 me.previousValue = me.scrollPos;
248 // continue doing that while dragging (even when dragging outside). When releasing, forward the click to the then selected item.
250 // an item has been clicked. Select it, ...
251 me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
255 float ListBox_mouseRelease(entity me, vector pos)
259 // slider dragging mode
260 // in that case, nothing happens on releasing
262 else if(me.pressed == 2)
264 me.pressed = 3; // do that here, so setSelected can know the mouse has been released
265 // item dragging mode
266 // select current one one last time...
267 me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
268 // and give it a nice click event
271 vector where = globalToBox(pos, eY * (me.getItemStart(me, me.selectedItem) - me.scrollPos), eX * (1 - me.controlWidth) + eY * me.getItemHeight(me, me.selectedItem));
273 if((me.selectedItem == me.lastClickedItem) && (time < me.lastClickedTime + 0.3))
274 me.doubleClickListBoxItem(me, me.selectedItem, where);
276 me.clickListBoxItem(me, me.selectedItem, where);
278 me.lastClickedItem = me.selectedItem;
279 me.lastClickedTime = time;
285 void ListBox_focusLeave(entity me)
287 // Reset the var pressed in case listbox loses focus
288 // by a mouse click on an item of the list
289 // for example showing a dialog on right click
292 void ListBox_updateControlTopBottom(entity me)
295 // scrollPos is in 0..1 and indicates where the "page" currently shown starts.
296 if(me.getTotalHeight(me) <= 1)
298 // we don't need no stinkin' scrollbar, we don't need no view control...
300 me.controlBottom = 1;
305 if(frametime) // only do this in draw frames
307 if(me.dragScrollTimer < time)
311 // if selected item is below listbox, increase scrollpos so it is in
312 me.scrollPos = max(me.scrollPos, me.getItemStart(me, me.selectedItem) + me.getItemHeight(me, me.selectedItem) - 1);
313 // if selected item is above listbox, decrease scrollpos so it is in
314 me.scrollPos = min(me.scrollPos, me.getItemStart(me, me.selectedItem));
315 if(me.scrollPos != save)
316 me.dragScrollTimer = time + 0.2;
319 // if scroll pos is below end of list, fix it
320 me.scrollPos = min(me.scrollPos, me.getTotalHeight(me) - 1);
321 // if scroll pos is above beginning of list, fix it
322 me.scrollPos = max(me.scrollPos, 0);
323 // now that we know where the list is scrolled to, find out where to draw the control
324 me.controlTop = max(0, me.scrollPos / me.getTotalHeight(me));
325 me.controlBottom = min((me.scrollPos + 1) / me.getTotalHeight(me), 1);
328 minfactor = 2 * me.controlWidth / me.size.y * me.size.x;
329 f = me.controlBottom - me.controlTop;
330 if(f < minfactor) // FIXME good default?
332 // f * X + 1 * (1-X) = minfactor
333 // (f - 1) * X + 1 = minfactor
334 // (f - 1) * X = minfactor - 1
335 // X = (minfactor - 1) / (f - 1)
336 f = (minfactor - 1) / (f - 1);
337 me.controlTop = me.controlTop * f + 0 * (1 - f);
338 me.controlBottom = me.controlBottom * f + 1 * (1 - f);
342 void ListBox_draw(entity me)
345 vector absSize, fillSize = '0 0 0';
346 vector oldshift, oldscale;
348 me.mouseDrag(me, me.dragScrollPos); // simulate mouseDrag event
349 me.updateControlTopBottom(me);
350 fillSize.x = (1 - me.controlWidth);
352 draw_Fill('0 0 0', '0 1 0' + fillSize, me.colorBG, me.alphaBG);
355 draw_VertButtonPicture(eX * (1 - me.controlWidth), strcat(me.src, "_s"), eX * me.controlWidth + eY, me.color2, 1);
356 if(me.getTotalHeight(me) > 1)
359 o = eX * (1 - me.controlWidth) + eY * me.controlTop;
360 s = eX * me.controlWidth + eY * (me.controlBottom - me.controlTop);
362 draw_VertButtonPicture(o, strcat(me.src, "_c"), s, me.colorC, 1);
364 draw_VertButtonPicture(o, strcat(me.src, "_f"), s, me.colorF, 1);
366 draw_VertButtonPicture(o, strcat(me.src, "_n"), s, me.color, 1);
370 oldshift = draw_shift;
371 oldscale = draw_scale;
373 i = me.getItemAtPos(me, me.scrollPos);
374 y = me.getItemStart(me, i) - me.scrollPos;
375 for (; i < me.nItems && y < 1; ++i)
377 draw_shift = boxToGlobal(eY * y, oldshift, oldscale);
378 vector relSize = eX * (1 - me.controlWidth) + eY * me.getItemHeight(me, i);
379 absSize = boxToGlobalSize(relSize, me.size);
380 draw_scale = boxToGlobalSize(relSize, oldscale);
381 me.drawListBoxItem(me, i, absSize, (me.selectedItem == i));
386 draw_shift = oldshift;
387 draw_scale = oldscale;
388 SUPER(ListBox).draw(me);
391 void ListBox_clickListBoxItem(entity me, float i, vector where)
396 void ListBox_doubleClickListBoxItem(entity me, float i, vector where)
401 void ListBox_drawListBoxItem(entity me, float i, vector absSize, float selected)
403 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);