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)
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')
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)
39 ATTRIB(ListBox, lastClickedItem, float, -1)
40 ATTRIB(ListBox, lastClickedTime, float, 0)
42 METHOD(ListBox, drawListBoxItem, void(entity, float, vector, float)) // item number, width/height, selected
43 METHOD(ListBox, clickListBoxItem, void(entity, float, vector)) // item number, relative clickpos
44 METHOD(ListBox, doubleClickListBoxItem, void(entity, float, vector)) // item number, relative clickpos
45 METHOD(ListBox, setSelected, void(entity, float))
47 METHOD(ListBox, getLastFullyVisibleItemAtScrollPos, float(entity, float))
48 METHOD(ListBox, getFirstFullyVisibleItemAtScrollPos, float(entity, float))
50 // NOTE: override these four methods if you want variable sized list items
51 METHOD(ListBox, getTotalHeight, float(entity))
52 METHOD(ListBox, getItemAtPos, float(entity, float))
53 METHOD(ListBox, getItemStart, float(entity, float))
54 METHOD(ListBox, getItemHeight, float(entity, float))
55 // NOTE: if getItemAt* are overridden, it may make sense to cache the
56 // start and height of the last item returned by getItemAtPos and fast
57 // track returning their properties for getItemStart and getItemHeight.
58 // The "hot" code path calls getItemAtPos first, then will query
59 // getItemStart and getItemHeight on it soon.
60 // When overriding, the following consistency rules must hold:
61 // getTotalHeight() == SUM(getItemHeight(i), i, 0, me.nItems-1)
62 // getItemStart(i+1) == getItemStart(i) + getItemHeight(i)
63 // for 0 <= i < me.nItems-1
64 // getItemStart(0) == 0
65 // getItemStart(getItemAtPos(p)) <= p
67 // getItemAtPos(p) == 0
69 // getItemStart(getItemAtPos(p)) + getItemHeight(getItemAtPos(p)) > p
70 // if p < getTotalHeigt()
71 // getItemAtPos(p) == me.nItems - 1
72 // if p >= getTotalHeight()
77 void ListBox_setSelected(entity me, float i)
79 me.selectedItem = bound(0, i, me.nItems - 1);
81 void ListBox_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
83 SUPER(ListBox).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
84 me.controlWidth = me.scrollbarWidth / absSize.x;
86 void ListBox_configureListBox(entity me, float theScrollbarWidth, float theItemHeight)
88 me.scrollbarWidth = theScrollbarWidth;
89 me.itemHeight = theItemHeight;
92 float ListBox_getTotalHeight(entity me)
94 return me.nItems * me.itemHeight;
96 float ListBox_getItemAtPos(entity me, float pos)
98 return floor(pos / me.itemHeight);
100 float ListBox_getItemStart(entity me, float i)
102 return me.itemHeight * i;
104 float ListBox_getItemHeight(entity me, float i)
106 return me.itemHeight;
109 float ListBox_getLastFullyVisibleItemAtScrollPos(entity me, float pos)
111 return me.getItemAtPos(me, pos + 1.001) - 1;
113 float ListBox_getFirstFullyVisibleItemAtScrollPos(entity me, float pos)
115 return me.getItemAtPos(me, pos - 0.001) + 1;
117 float ListBox_keyDown(entity me, float key, float ascii, float shift)
119 me.dragScrollTimer = time;
120 if(key == K_MWHEELUP)
122 me.scrollPos = max(me.scrollPos - 0.5, 0);
123 me.setSelected(me, min(me.selectedItem, me.getLastFullyVisibleItemAtScrollPos(me, me.scrollPos)));
125 else if(key == K_MWHEELDOWN)
127 me.scrollPos = min(me.scrollPos + 0.5, me.getTotalHeight(me) - 1);
128 me.setSelected(me, max(me.selectedItem, me.getFirstFullyVisibleItemAtScrollPos(me, me.scrollPos)));
130 else if(key == K_PGUP || key == K_KP_PGUP)
132 float i = me.selectedItem;
133 float a = me.getItemHeight(me, i);
139 a += me.getItemHeight(me, i);
143 me.setSelected(me, i + 1);
145 else if(key == K_PGDN || key == K_KP_PGDN)
147 float i = me.selectedItem;
148 float a = me.getItemHeight(me, i);
154 a += me.getItemHeight(me, i);
158 me.setSelected(me, i - 1);
160 else if(key == K_UPARROW || key == K_KP_UPARROW)
161 me.setSelected(me, me.selectedItem - 1);
162 else if(key == K_DOWNARROW || key == K_KP_DOWNARROW)
163 me.setSelected(me, me.selectedItem + 1);
164 else if(key == K_HOME || key == K_KP_HOME)
167 me.setSelected(me, 0);
169 else if(key == K_END || key == K_KP_END)
171 me.scrollPos = max(0, me.getTotalHeight(me) - 1);
172 me.setSelected(me, me.nItems - 1);
178 float ListBox_mouseDrag(entity me, vector pos)
182 me.updateControlTopBottom(me);
183 me.dragScrollPos = pos;
187 if(pos.x < 1 - me.controlWidth - me.tolerance.y * me.controlWidth) hit = 0;
188 if(pos.y < 0 - me.tolerance.x) hit = 0;
189 if(pos.x >= 1 + me.tolerance.y * me.controlWidth) hit = 0;
190 if(pos.y >= 1 + me.tolerance.x) hit = 0;
193 // calculate new pos to v
195 d = (pos.y - me.pressOffset) / (1 - (me.controlBottom - me.controlTop)) * (me.getTotalHeight(me) - 1);
196 me.scrollPos = me.previousValue + d;
199 me.scrollPos = me.previousValue;
200 me.scrollPos = min(me.scrollPos, me.getTotalHeight(me) - 1);
201 me.scrollPos = max(me.scrollPos, 0);
202 i = min(me.selectedItem, me.getLastFullyVisibleItemAtScrollPos(me, me.scrollPos));
203 i = max(i, ListBox_getFirstFullyVisibleItemAtScrollPos(me, me.scrollPos));
204 me.setSelected(me, i);
206 else if(me.pressed == 2)
208 me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
212 float ListBox_mousePress(entity me, vector pos)
214 if(pos.x < 0) return 0;
215 if(pos.y < 0) return 0;
216 if(pos.x >= 1) return 0;
217 if(pos.y >= 1) return 0;
218 me.dragScrollPos = pos;
219 me.updateControlTopBottom(me);
220 me.dragScrollTimer = time;
221 if(pos.x >= 1 - me.controlWidth)
223 // if hit, set me.pressed, otherwise scroll by one page
224 if(pos.y < me.controlTop)
227 me.scrollPos = max(me.scrollPos - 1, 0);
228 me.setSelected(me, min(me.selectedItem, ListBox_getLastFullyVisibleItemAtScrollPos(me, me.scrollPos)));
230 else if(pos.y > me.controlBottom)
233 me.scrollPos = min(me.scrollPos + 1, me.getTotalHeight(me) - 1);
234 me.setSelected(me, max(me.selectedItem, ListBox_getFirstFullyVisibleItemAtScrollPos(me, me.scrollPos)));
239 me.pressOffset = pos.y;
240 me.previousValue = me.scrollPos;
245 // continue doing that while dragging (even when dragging outside). When releasing, forward the click to the then selected item.
247 // an item has been clicked. Select it, ...
248 me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
252 float ListBox_mouseRelease(entity me, vector pos)
256 // slider dragging mode
257 // in that case, nothing happens on releasing
259 else if(me.pressed == 2)
261 me.pressed = 3; // do that here, so setSelected can know the mouse has been released
262 // item dragging mode
263 // select current one one last time...
264 me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
265 // and give it a nice click event
268 vector where = globalToBox(pos, eY * (me.getItemStart(me, me.selectedItem) - me.scrollPos), eX * (1 - me.controlWidth) + eY * me.getItemHeight(me, me.selectedItem));
270 if((me.selectedItem == me.lastClickedItem) && (time < me.lastClickedTime + 0.3))
271 me.doubleClickListBoxItem(me, me.selectedItem, where);
273 me.clickListBoxItem(me, me.selectedItem, where);
275 me.lastClickedItem = me.selectedItem;
276 me.lastClickedTime = time;
282 void ListBox_focusLeave(entity me)
284 // Reset the var pressed in case listbox loses focus
285 // by a mouse click on an item of the list
286 // for example showing a dialog on right click
289 void ListBox_updateControlTopBottom(entity me)
292 // scrollPos is in 0..1 and indicates where the "page" currently shown starts.
293 if(me.getTotalHeight(me) <= 1)
295 // we don't need no stinkin' scrollbar, we don't need no view control...
297 me.controlBottom = 1;
302 if(frametime) // only do this in draw frames
304 if(me.dragScrollTimer < time)
308 // if selected item is below listbox, increase scrollpos so it is in
309 me.scrollPos = max(me.scrollPos, me.getItemStart(me, me.selectedItem) + me.getItemHeight(me, me.selectedItem) - 1);
310 // if selected item is above listbox, decrease scrollpos so it is in
311 me.scrollPos = min(me.scrollPos, me.getItemStart(me, me.selectedItem));
312 if(me.scrollPos != save)
313 me.dragScrollTimer = time + 0.2;
316 // if scroll pos is below end of list, fix it
317 me.scrollPos = min(me.scrollPos, me.getTotalHeight(me) - 1);
318 // if scroll pos is above beginning of list, fix it
319 me.scrollPos = max(me.scrollPos, 0);
320 // now that we know where the list is scrolled to, find out where to draw the control
321 me.controlTop = max(0, me.scrollPos / me.getTotalHeight(me));
322 me.controlBottom = min((me.scrollPos + 1) / me.getTotalHeight(me), 1);
325 minfactor = 2 * me.controlWidth / me.size.y * me.size.x;
326 f = me.controlBottom - me.controlTop;
327 if(f < minfactor) // FIXME good default?
329 // f * X + 1 * (1-X) = minfactor
330 // (f - 1) * X + 1 = minfactor
331 // (f - 1) * X = minfactor - 1
332 // X = (minfactor - 1) / (f - 1)
333 f = (minfactor - 1) / (f - 1);
334 me.controlTop = me.controlTop * f + 0 * (1 - f);
335 me.controlBottom = me.controlBottom * f + 1 * (1 - f);
339 void ListBox_draw(entity me)
342 vector absSize, fillSize = '0 0 0';
343 vector oldshift, oldscale;
345 me.mouseDrag(me, me.dragScrollPos); // simulate mouseDrag event
346 me.updateControlTopBottom(me);
347 fillSize_x = (1 - me.controlWidth);
349 draw_Fill('0 0 0', '0 1 0' + fillSize, me.colorBG, me.alphaBG);
352 draw_VertButtonPicture(eX * (1 - me.controlWidth), strcat(me.src, "_s"), eX * me.controlWidth + eY, me.color2, 1);
353 if(me.getTotalHeight(me) > 1)
356 o = eX * (1 - me.controlWidth) + eY * me.controlTop;
357 s = eX * me.controlWidth + eY * (me.controlBottom - me.controlTop);
359 draw_VertButtonPicture(o, strcat(me.src, "_c"), s, me.colorC, 1);
361 draw_VertButtonPicture(o, strcat(me.src, "_f"), s, me.colorF, 1);
363 draw_VertButtonPicture(o, strcat(me.src, "_n"), s, me.color, 1);
367 oldshift = draw_shift;
368 oldscale = draw_scale;
370 i = me.getItemAtPos(me, me.scrollPos);
371 y = me.getItemStart(me, i) - me.scrollPos;
372 for(0; i < me.nItems && y < 1; ++i)
374 draw_shift = boxToGlobal(eY * y, oldshift, oldscale);
375 vector relSize = eX * (1 - me.controlWidth) + eY * me.getItemHeight(me, i);
376 absSize = boxToGlobalSize(relSize, me.size);
377 draw_scale = boxToGlobalSize(relSize, oldscale);
378 me.drawListBoxItem(me, i, absSize, (me.selectedItem == i));
383 draw_shift = oldshift;
384 draw_scale = oldscale;
385 SUPER(ListBox).draw(me);
388 void ListBox_clickListBoxItem(entity me, float i, vector where)
393 void ListBox_doubleClickListBoxItem(entity me, float i, vector where)
398 void ListBox_drawListBoxItem(entity me, float i, vector absSize, float selected)
400 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);