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, allowFocusSound, float, 1)
13 ATTRIB(ListBox, selectedItem, float, 0)
14 ATTRIB(ListBox, size, vector, '0 0 0')
15 ATTRIB(ListBox, origin, vector, '0 0 0')
16 ATTRIB(ListBox, scrollPos, float, 0) // measured in window heights, fixed when needed
17 ATTRIB(ListBox, previousValue, float, 0)
18 ATTRIB(ListBox, pressed, float, 0) // 0 = normal, 1 = scrollbar dragging, 2 = item dragging, 3 = released
19 ATTRIB(ListBox, pressOffset, float, 0)
21 METHOD(ListBox, updateControlTopBottom, void(entity))
22 ATTRIB(ListBox, controlTop, float, 0)
23 ATTRIB(ListBox, controlBottom, float, 0)
24 ATTRIB(ListBox, controlWidth, float, 0)
25 ATTRIB(ListBox, dragScrollTimer, float, 0)
26 ATTRIB(ListBox, dragScrollPos, vector, '0 0 0')
28 ATTRIB(ListBox, src, string, string_null) // scrollbar
29 ATTRIB(ListBox, color, vector, '1 1 1')
30 ATTRIB(ListBox, color2, vector, '1 1 1')
31 ATTRIB(ListBox, colorC, vector, '1 1 1')
32 ATTRIB(ListBox, colorF, vector, '1 1 1')
33 ATTRIB(ListBox, tolerance, vector, '0 0 0') // drag tolerance
34 ATTRIB(ListBox, scrollbarWidth, float, 0) // pixels
35 ATTRIB(ListBox, nItems, float, 42)
36 ATTRIB(ListBox, itemHeight, float, 0)
37 ATTRIB(ListBox, colorBG, vector, '0 0 0')
38 ATTRIB(ListBox, alphaBG, float, 0)
40 ATTRIB(ListBox, lastClickedItem, float, -1)
41 ATTRIB(ListBox, lastClickedTime, float, 0)
43 METHOD(ListBox, drawListBoxItem, void(entity, float, vector, float)) // item number, width/height, selected
44 METHOD(ListBox, clickListBoxItem, void(entity, float, vector)) // item number, relative clickpos
45 METHOD(ListBox, doubleClickListBoxItem, void(entity, float, vector)) // item number, relative clickpos
46 METHOD(ListBox, setSelected, void(entity, float))
48 METHOD(ListBox, getLastFullyVisibleItemAtScrollPos, float(entity, float))
49 METHOD(ListBox, getFirstFullyVisibleItemAtScrollPos, float(entity, float))
51 // NOTE: override these four methods if you want variable sized list items
52 METHOD(ListBox, getTotalHeight, float(entity))
53 METHOD(ListBox, getItemAtPos, float(entity, float))
54 METHOD(ListBox, getItemStart, float(entity, float))
55 METHOD(ListBox, getItemHeight, float(entity, float))
56 // NOTE: if getItemAt* are overridden, it may make sense to cache the
57 // start and height of the last item returned by getItemAtPos and fast
58 // track returning their properties for getItemStart and getItemHeight.
59 // The "hot" code path calls getItemAtPos first, then will query
60 // getItemStart and getItemHeight on it soon.
61 // When overriding, the following consistency rules must hold:
62 // getTotalHeight() == SUM(getItemHeight(i), i, 0, me.nItems-1)
63 // getItemStart(i+1) == getItemStart(i) + getItemHeight(i)
64 // for 0 <= i < me.nItems-1
65 // getItemStart(0) == 0
66 // getItemStart(getItemAtPos(p)) <= p
68 // getItemAtPos(p) == 0
70 // getItemStart(getItemAtPos(p)) + getItemHeight(getItemAtPos(p)) > p
71 // if p < getTotalHeigt()
72 // getItemAtPos(p) == me.nItems - 1
73 // if p >= getTotalHeight()
78 void ListBox_setSelected(entity me, float i)
80 me.selectedItem = bound(0, i, me.nItems - 1);
82 void ListBox_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
84 SUPER(ListBox).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
85 me.controlWidth = me.scrollbarWidth / absSize.x;
87 void ListBox_configureListBox(entity me, float theScrollbarWidth, float theItemHeight)
89 me.scrollbarWidth = theScrollbarWidth;
90 me.itemHeight = theItemHeight;
93 float ListBox_getTotalHeight(entity me)
95 return me.nItems * me.itemHeight;
97 float ListBox_getItemAtPos(entity me, float pos)
99 return floor(pos / me.itemHeight);
101 float ListBox_getItemStart(entity me, float i)
103 return me.itemHeight * i;
105 float ListBox_getItemHeight(entity me, float i)
107 return me.itemHeight;
110 float ListBox_getLastFullyVisibleItemAtScrollPos(entity me, float pos)
112 return me.getItemAtPos(me, pos + 1.001) - 1;
114 float ListBox_getFirstFullyVisibleItemAtScrollPos(entity me, float pos)
116 return me.getItemAtPos(me, pos - 0.001) + 1;
118 float ListBox_keyDown(entity me, float key, float ascii, float shift)
120 me.dragScrollTimer = time;
121 if(key == K_MWHEELUP)
123 me.scrollPos = max(me.scrollPos - 0.5, 0);
124 me.setSelected(me, min(me.selectedItem, me.getLastFullyVisibleItemAtScrollPos(me, me.scrollPos)));
126 else if(key == K_MWHEELDOWN)
128 me.scrollPos = min(me.scrollPos + 0.5, me.getTotalHeight(me) - 1);
129 me.setSelected(me, max(me.selectedItem, me.getFirstFullyVisibleItemAtScrollPos(me, me.scrollPos)));
131 else if(key == K_PGUP || key == K_KP_PGUP)
133 float i = me.selectedItem;
134 float a = me.getItemHeight(me, i);
140 a += me.getItemHeight(me, i);
144 me.setSelected(me, i + 1);
146 else if(key == K_PGDN || key == K_KP_PGDN)
148 float i = me.selectedItem;
149 float a = me.getItemHeight(me, i);
155 a += me.getItemHeight(me, i);
159 me.setSelected(me, i - 1);
161 else if(key == K_UPARROW || key == K_KP_UPARROW)
162 me.setSelected(me, me.selectedItem - 1);
163 else if(key == K_DOWNARROW || key == K_KP_DOWNARROW)
164 me.setSelected(me, me.selectedItem + 1);
165 else if(key == K_HOME || key == K_KP_HOME)
168 me.setSelected(me, 0);
170 else if(key == K_END || key == K_KP_END)
172 me.scrollPos = max(0, me.getTotalHeight(me) - 1);
173 me.setSelected(me, me.nItems - 1);
179 float ListBox_mouseDrag(entity me, vector pos)
183 me.updateControlTopBottom(me);
184 me.dragScrollPos = pos;
188 if(pos.x < 1 - me.controlWidth - me.tolerance.y * me.controlWidth) hit = 0;
189 if(pos.y < 0 - me.tolerance.x) hit = 0;
190 if(pos.x >= 1 + me.tolerance.y * me.controlWidth) hit = 0;
191 if(pos.y >= 1 + me.tolerance.x) hit = 0;
194 // calculate new pos to v
196 d = (pos.y - me.pressOffset) / (1 - (me.controlBottom - me.controlTop)) * (me.getTotalHeight(me) - 1);
197 me.scrollPos = me.previousValue + d;
200 me.scrollPos = me.previousValue;
201 me.scrollPos = min(me.scrollPos, me.getTotalHeight(me) - 1);
202 me.scrollPos = max(me.scrollPos, 0);
203 i = min(me.selectedItem, me.getLastFullyVisibleItemAtScrollPos(me, me.scrollPos));
204 i = max(i, ListBox_getFirstFullyVisibleItemAtScrollPos(me, me.scrollPos));
205 me.setSelected(me, i);
207 else if(me.pressed == 2)
209 me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
213 float ListBox_mousePress(entity me, vector pos)
215 if(pos.x < 0) return 0;
216 if(pos.y < 0) return 0;
217 if(pos.x >= 1) return 0;
218 if(pos.y >= 1) return 0;
219 me.dragScrollPos = pos;
220 me.updateControlTopBottom(me);
221 me.dragScrollTimer = time;
222 if(pos.x >= 1 - me.controlWidth)
224 // if hit, set me.pressed, otherwise scroll by one page
225 if(pos.y < me.controlTop)
228 me.scrollPos = max(me.scrollPos - 1, 0);
229 me.setSelected(me, min(me.selectedItem, ListBox_getLastFullyVisibleItemAtScrollPos(me, me.scrollPos)));
231 else if(pos.y > me.controlBottom)
234 me.scrollPos = min(me.scrollPos + 1, me.getTotalHeight(me) - 1);
235 me.setSelected(me, max(me.selectedItem, ListBox_getFirstFullyVisibleItemAtScrollPos(me, me.scrollPos)));
240 me.pressOffset = pos.y;
241 me.previousValue = me.scrollPos;
246 // continue doing that while dragging (even when dragging outside). When releasing, forward the click to the then selected item.
248 // an item has been clicked. Select it, ...
249 me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
253 float ListBox_mouseRelease(entity me, vector pos)
257 // slider dragging mode
258 // in that case, nothing happens on releasing
260 else if(me.pressed == 2)
262 me.pressed = 3; // do that here, so setSelected can know the mouse has been released
263 // item dragging mode
264 // select current one one last time...
265 me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
266 // and give it a nice click event
269 vector where = globalToBox(pos, eY * (me.getItemStart(me, me.selectedItem) - me.scrollPos), eX * (1 - me.controlWidth) + eY * me.getItemHeight(me, me.selectedItem));
271 if((me.selectedItem == me.lastClickedItem) && (time < me.lastClickedTime + 0.3))
272 me.doubleClickListBoxItem(me, me.selectedItem, where);
274 me.clickListBoxItem(me, me.selectedItem, where);
276 me.lastClickedItem = me.selectedItem;
277 me.lastClickedTime = time;
283 void ListBox_focusLeave(entity me)
285 // Reset the var pressed in case listbox loses focus
286 // by a mouse click on an item of the list
287 // for example showing a dialog on right click
290 void ListBox_updateControlTopBottom(entity me)
293 // scrollPos is in 0..1 and indicates where the "page" currently shown starts.
294 if(me.getTotalHeight(me) <= 1)
296 // we don't need no stinkin' scrollbar, we don't need no view control...
298 me.controlBottom = 1;
303 if(frametime) // only do this in draw frames
305 if(me.dragScrollTimer < time)
309 // if selected item is below listbox, increase scrollpos so it is in
310 me.scrollPos = max(me.scrollPos, me.getItemStart(me, me.selectedItem) + me.getItemHeight(me, me.selectedItem) - 1);
311 // if selected item is above listbox, decrease scrollpos so it is in
312 me.scrollPos = min(me.scrollPos, me.getItemStart(me, me.selectedItem));
313 if(me.scrollPos != save)
314 me.dragScrollTimer = time + 0.2;
317 // if scroll pos is below end of list, fix it
318 me.scrollPos = min(me.scrollPos, me.getTotalHeight(me) - 1);
319 // if scroll pos is above beginning of list, fix it
320 me.scrollPos = max(me.scrollPos, 0);
321 // now that we know where the list is scrolled to, find out where to draw the control
322 me.controlTop = max(0, me.scrollPos / me.getTotalHeight(me));
323 me.controlBottom = min((me.scrollPos + 1) / me.getTotalHeight(me), 1);
326 minfactor = 2 * me.controlWidth / me.size.y * me.size.x;
327 f = me.controlBottom - me.controlTop;
328 if(f < minfactor) // FIXME good default?
330 // f * X + 1 * (1-X) = minfactor
331 // (f - 1) * X + 1 = minfactor
332 // (f - 1) * X = minfactor - 1
333 // X = (minfactor - 1) / (f - 1)
334 f = (minfactor - 1) / (f - 1);
335 me.controlTop = me.controlTop * f + 0 * (1 - f);
336 me.controlBottom = me.controlBottom * f + 1 * (1 - f);
340 void ListBox_draw(entity me)
343 vector absSize, fillSize = '0 0 0';
344 vector oldshift, oldscale;
346 me.mouseDrag(me, me.dragScrollPos); // simulate mouseDrag event
347 me.updateControlTopBottom(me);
348 fillSize.x = (1 - me.controlWidth);
350 draw_Fill('0 0 0', '0 1 0' + fillSize, me.colorBG, me.alphaBG);
353 draw_VertButtonPicture(eX * (1 - me.controlWidth), strcat(me.src, "_s"), eX * me.controlWidth + eY, me.color2, 1);
354 if(me.getTotalHeight(me) > 1)
357 o = eX * (1 - me.controlWidth) + eY * me.controlTop;
358 s = eX * me.controlWidth + eY * (me.controlBottom - me.controlTop);
360 draw_VertButtonPicture(o, strcat(me.src, "_c"), s, me.colorC, 1);
362 draw_VertButtonPicture(o, strcat(me.src, "_f"), s, me.colorF, 1);
364 draw_VertButtonPicture(o, strcat(me.src, "_n"), s, me.color, 1);
368 oldshift = draw_shift;
369 oldscale = draw_scale;
371 i = me.getItemAtPos(me, me.scrollPos);
372 y = me.getItemStart(me, i) - me.scrollPos;
373 for (; i < me.nItems && y < 1; ++i)
375 draw_shift = boxToGlobal(eY * y, oldshift, oldscale);
376 vector relSize = eX * (1 - me.controlWidth) + eY * me.getItemHeight(me, i);
377 absSize = boxToGlobalSize(relSize, me.size);
378 draw_scale = boxToGlobalSize(relSize, oldscale);
379 me.drawListBoxItem(me, i, absSize, (me.selectedItem == i));
384 draw_shift = oldshift;
385 draw_scale = oldscale;
386 SUPER(ListBox).draw(me);
389 void ListBox_clickListBoxItem(entity me, float i, vector where)
394 void ListBox_doubleClickListBoxItem(entity me, float i, vector where)
399 void ListBox_drawListBoxItem(entity me, float i, vector absSize, float selected)
401 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);