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, mouseMove, float(entity, vector))
10 METHOD(ListBox, mousePress, float(entity, vector))
11 METHOD(ListBox, mouseDrag, float(entity, vector))
12 METHOD(ListBox, mouseRelease, float(entity, vector))
13 METHOD(ListBox, focusLeave, void(entity))
14 ATTRIB(ListBox, focusable, float, 1)
15 ATTRIB(ListBox, focusedItem, int, -1)
16 ATTRIB(ListBox, focusedItemAlpha, float, 0.3)
17 ATTRIB(ListBox, mouseMoveOffset, float, -1) // let know where the cursor is when the list scrolls without moving the cursor
18 ATTRIB(ListBox, allowFocusSound, float, 1)
19 ATTRIB(ListBox, selectedItem, int, 0)
20 ATTRIB(ListBox, size, vector, '0 0 0')
21 ATTRIB(ListBox, origin, vector, '0 0 0')
22 ATTRIB(ListBox, scrollPos, float, 0) // measured in window heights, fixed when needed
23 ATTRIB(ListBox, scrollPosTarget, float, 0)
24 ATTRIB(ListBox, needScrollToItem, float, -1)
25 METHOD(ListBox, scrollToItem, void(entity, int))
26 ATTRIB(ListBox, previousValue, float, 0)
27 ATTRIB(ListBox, pressed, float, 0) // 0 = normal, 1 = scrollbar dragging, 2 = item dragging, 3 = released
28 ATTRIB(ListBox, pressOffset, float, 0)
30 METHOD(ListBox, updateControlTopBottom, void(entity))
31 ATTRIB(ListBox, controlTop, float, 0)
32 ATTRIB(ListBox, controlBottom, float, 0)
33 ATTRIB(ListBox, controlWidth, float, 0)
34 ATTRIB(ListBox, dragScrollPos, vector, '0 0 0')
35 ATTRIB(ListBox, selectionDoesntMatter, bool, false) // improves scrolling by keys for lists that don't need to show an active selection
37 ATTRIB(ListBox, src, string, string_null) // scrollbar
38 ATTRIB(ListBox, color, vector, '1 1 1')
39 ATTRIB(ListBox, color2, vector, '1 1 1')
40 ATTRIB(ListBox, colorC, vector, '1 1 1')
41 ATTRIB(ListBox, colorF, vector, '1 1 1')
42 ATTRIB(ListBox, tolerance, vector, '0 0 0') // drag tolerance
43 ATTRIB(ListBox, scrollbarWidth, float, 0) // pixels
44 ATTRIB(ListBox, nItems, float, 42)
45 ATTRIB(ListBox, itemHeight, float, 0)
46 ATTRIB(ListBox, colorBG, vector, '0 0 0')
47 ATTRIB(ListBox, alphaBG, float, 0)
49 ATTRIB(ListBox, lastClickedItem, float, -1)
50 ATTRIB(ListBox, lastClickedTime, float, 0)
52 METHOD(ListBox, drawListBoxItem, void(entity, int, vector, bool, bool)) // item number, width/height, isSelected, isFocused
53 METHOD(ListBox, clickListBoxItem, void(entity, float, vector)) // item number, relative clickpos
54 METHOD(ListBox, doubleClickListBoxItem, void(entity, float, vector)) // item number, relative clickpos
55 METHOD(ListBox, setSelected, void(entity, float))
57 METHOD(ListBox, getLastFullyVisibleItemAtScrollPos, float(entity, float))
58 METHOD(ListBox, getFirstFullyVisibleItemAtScrollPos, float(entity, float))
60 // NOTE: override these four methods if you want variable sized list items
61 METHOD(ListBox, getTotalHeight, float(entity))
62 METHOD(ListBox, getItemAtPos, float(entity, float))
63 METHOD(ListBox, getItemStart, float(entity, float))
64 METHOD(ListBox, getItemHeight, float(entity, float))
65 // NOTE: if getItemAt* are overridden, it may make sense to cache the
66 // start and height of the last item returned by getItemAtPos and fast
67 // track returning their properties for getItemStart and getItemHeight.
68 // The "hot" code path calls getItemAtPos first, then will query
69 // getItemStart and getItemHeight on it soon.
70 // When overriding, the following consistency rules must hold:
71 // getTotalHeight() == SUM(getItemHeight(i), i, 0, me.nItems-1)
72 // getItemStart(i+1) == getItemStart(i) + getItemHeight(i)
73 // for 0 <= i < me.nItems-1
74 // getItemStart(0) == 0
75 // getItemStart(getItemAtPos(p)) <= p
77 // getItemAtPos(p) == 0
79 // getItemStart(getItemAtPos(p)) + getItemHeight(getItemAtPos(p)) > p
80 // if p < getTotalHeigt()
81 // getItemAtPos(p) == me.nItems - 1
82 // if p >= getTotalHeight()
87 void ListBox_scrollToItem(entity me, int i)
89 // scroll doesn't work properly until itemHeight is set to the correct value
90 // at the first resizeNotify call
91 if(me.itemHeight == 1) // initial temporary value of itemHeight is 1
93 me.needScrollToItem = i;
97 i = bound(0, i, me.nItems - 1);
99 // scroll the list to make sure the selected item is visible
100 // (even if the selected item doesn't change).
101 if(i < me.getFirstFullyVisibleItemAtScrollPos(me, me.scrollPos))
103 // above visible area
104 me.scrollPosTarget = me.getItemStart(me, i);
106 else if(i > me.getLastFullyVisibleItemAtScrollPos(me, me.scrollPos))
108 // below visible area
109 if(i == me.nItems - 1)
110 me.scrollPosTarget = me.getTotalHeight(me) - 1;
112 me.scrollPosTarget = me.getItemStart(me, i + 1) - 1;
116 void ListBox_setSelected(entity me, float i)
118 i = bound(0, i, me.nItems - 1);
119 me.scrollToItem(me, i);
122 void ListBox_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
124 SUPER(ListBox).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
125 me.controlWidth = me.scrollbarWidth / absSize.x;
127 void ListBox_configureListBox(entity me, float theScrollbarWidth, float theItemHeight)
129 me.scrollbarWidth = theScrollbarWidth;
130 me.itemHeight = theItemHeight;
133 float ListBox_getTotalHeight(entity me)
135 return me.nItems * me.itemHeight;
137 float ListBox_getItemAtPos(entity me, float pos)
139 return floor(pos / me.itemHeight);
141 float ListBox_getItemStart(entity me, float i)
143 return me.itemHeight * i;
145 float ListBox_getItemHeight(entity me, float i)
147 return me.itemHeight;
150 float ListBox_getLastFullyVisibleItemAtScrollPos(entity me, float pos)
152 return me.getItemAtPos(me, pos + 0.999) - 1;
154 float ListBox_getFirstFullyVisibleItemAtScrollPos(entity me, float pos)
156 return me.getItemAtPos(me, pos + 0.001) + 1;
158 float ListBox_keyDown(entity me, float key, float ascii, float shift)
160 if(key == K_MWHEELUP)
162 me.scrollPosTarget = max(me.scrollPosTarget - 0.5, 0);
164 else if(key == K_MWHEELDOWN)
166 me.scrollPosTarget = min(me.scrollPosTarget + 0.5, me.getTotalHeight(me) - 1);
168 else if(key == K_PGUP || key == K_KP_PGUP)
170 if(me.selectionDoesntMatter)
172 me.scrollPosTarget = max(me.scrollPosTarget - 0.5, 0);
176 float i = me.selectedItem;
177 float a = me.getItemHeight(me, i);
183 a += me.getItemHeight(me, i);
187 me.setSelected(me, i + 1);
189 else if(key == K_PGDN || key == K_KP_PGDN)
191 if(me.selectionDoesntMatter)
193 me.scrollPosTarget = min(me.scrollPosTarget + 0.5, me.nItems * me.itemHeight - 1);
197 float i = me.selectedItem;
198 float a = me.getItemHeight(me, i);
204 a += me.getItemHeight(me, i);
208 me.setSelected(me, i - 1);
210 else if(key == K_UPARROW || key == K_KP_UPARROW)
212 if(me.selectionDoesntMatter)
214 me.scrollPosTarget = max(me.scrollPosTarget - me.itemHeight, 0);
218 me.setSelected(me, me.selectedItem - 1);
220 else if(key == K_DOWNARROW || key == K_KP_DOWNARROW)
222 if(me.selectionDoesntMatter)
224 me.scrollPosTarget = min(me.scrollPosTarget + me.itemHeight, me.nItems * me.itemHeight - 1);
228 me.setSelected(me, me.selectedItem + 1);
230 else if(key == K_HOME || key == K_KP_HOME)
231 me.setSelected(me, 0);
232 else if(key == K_END || key == K_KP_END)
233 me.setSelected(me, me.nItems - 1);
238 float ListBox_mouseMove(entity me, vector pos)
240 me.mouseMoveOffset = -1;
241 if(pos_x < 0) return 0;
242 if(pos_y < 0) return 0;
243 if(pos_x >= 1) return 0;
244 if(pos_y >= 1) return 0;
245 if(pos_x < 1 - me.controlWidth)
246 me.mouseMoveOffset = pos.y;
250 me.mouseMoveOffset = -1;
254 float ListBox_mouseDrag(entity me, vector pos)
257 me.updateControlTopBottom(me);
258 me.dragScrollPos = pos;
262 if(pos.x < 1 - me.controlWidth - me.tolerance.y * me.controlWidth) hit = 0;
263 if(pos.y < 0 - me.tolerance.x) hit = 0;
264 if(pos.x >= 1 + me.tolerance.y * me.controlWidth) hit = 0;
265 if(pos.y >= 1 + me.tolerance.x) hit = 0;
268 // calculate new pos to v
270 d = (pos.y - me.pressOffset) / (1 - (me.controlBottom - me.controlTop)) * (me.getTotalHeight(me) - 1);
271 me.scrollPosTarget = me.previousValue + d;
274 me.scrollPosTarget = me.previousValue;
275 me.scrollPosTarget = min(me.scrollPosTarget, me.getTotalHeight(me) - 1);
276 me.scrollPosTarget = max(me.scrollPosTarget, 0);
278 else if(me.pressed == 2)
280 me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
281 me.focusedItem = me.selectedItem;
282 me.mouseMoveOffset = -1;
286 float ListBox_mousePress(entity me, vector pos)
288 if(pos.x < 0) return 0;
289 if(pos.y < 0) return 0;
290 if(pos.x >= 1) return 0;
291 if(pos.y >= 1) return 0;
292 me.dragScrollPos = pos;
293 me.updateControlTopBottom(me);
294 if(pos.x >= 1 - me.controlWidth)
296 // if hit, set me.pressed, otherwise scroll by one page
297 if(pos.y < me.controlTop)
300 me.scrollPosTarget = max(me.scrollPosTarget - 1, 0);
302 else if(pos.y > me.controlBottom)
305 me.scrollPosTarget = min(me.scrollPosTarget + 1, me.getTotalHeight(me) - 1);
310 me.pressOffset = pos.y;
311 me.previousValue = me.scrollPos;
316 // continue doing that while dragging (even when dragging outside). When releasing, forward the click to the then selected item.
318 // an item has been clicked. Select it, ...
319 me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
320 me.focusedItem = me.selectedItem;
324 float ListBox_mouseRelease(entity me, vector pos)
328 // slider dragging mode
329 // in that case, nothing happens on releasing
331 else if(me.pressed == 2)
333 me.pressed = 3; // do that here, so setSelected can know the mouse has been released
334 // item dragging mode
335 // select current one one last time...
336 me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
337 me.focusedItem = me.selectedItem;
338 // and give it a nice click event
341 vector where = globalToBox(pos, eY * (me.getItemStart(me, me.selectedItem) - me.scrollPos), eX * (1 - me.controlWidth) + eY * me.getItemHeight(me, me.selectedItem));
343 if((me.selectedItem == me.lastClickedItem) && (time < me.lastClickedTime + 0.3))
344 me.doubleClickListBoxItem(me, me.selectedItem, where);
346 me.clickListBoxItem(me, me.selectedItem, where);
348 me.lastClickedItem = me.selectedItem;
349 me.lastClickedTime = time;
355 void ListBox_focusLeave(entity me)
357 // Reset the var pressed in case listbox loses focus
358 // by a mouse click on an item of the list
359 // for example showing a dialog on right click
362 me.mouseMoveOffset = -1;
364 void ListBox_updateControlTopBottom(entity me)
367 // scrollPos is in 0..1 and indicates where the "page" currently shown starts.
368 if(me.getTotalHeight(me) <= 1)
370 // we don't need no stinkin' scrollbar, we don't need no view control...
372 me.controlBottom = 1;
377 // if scroll pos is below end of list, fix it
378 me.scrollPos = min(me.scrollPos, me.getTotalHeight(me) - 1);
379 // if scroll pos is above beginning of list, fix it
380 me.scrollPos = max(me.scrollPos, 0);
381 // now that we know where the list is scrolled to, find out where to draw the control
382 me.controlTop = max(0, me.scrollPos / me.getTotalHeight(me));
383 me.controlBottom = min((me.scrollPos + 1) / me.getTotalHeight(me), 1);
386 minfactor = 2 * me.controlWidth / me.size.y * me.size.x;
387 f = me.controlBottom - me.controlTop;
388 if(f < minfactor) // FIXME good default?
390 // f * X + 1 * (1-X) = minfactor
391 // (f - 1) * X + 1 = minfactor
392 // (f - 1) * X = minfactor - 1
393 // X = (minfactor - 1) / (f - 1)
394 f = (minfactor - 1) / (f - 1);
395 me.controlTop = me.controlTop * f + 0 * (1 - f);
396 me.controlBottom = me.controlBottom * f + 1 * (1 - f);
400 void ListBox_draw(entity me)
403 vector absSize, fillSize = '0 0 0';
404 vector oldshift, oldscale;
406 // we can't do this in mouseMove as the list can scroll without moving the cursor
407 float focusedItem_save = me.focusedItem;
408 if(me.mouseMoveOffset != -1)
409 me.focusedItem = me.getItemAtPos(me, me.scrollPos + me.mouseMoveOffset);
410 if(me.focusedItem >= 0)
411 if(focusedItem_save != me.focusedItem)
412 me.focusedItemAlpha = SKINALPHA_LISTBOX_FOCUSED;
414 if(me.needScrollToItem >= 0)
416 me.scrollToItem(me, me.needScrollToItem);
417 me.needScrollToItem = -1;
419 if(me.scrollPos != me.scrollPosTarget)
421 float PI = 3.1415926535897932384626433832795028841971693993751058209749445923;
422 float exp_factor = 0.65;
424 exp_factor = 0.45; // scroll faster while dragging the scrollbar
425 // this formula works with whatever framerate
426 float f = sin(PI / 2 * pow(frametime, exp_factor));
427 me.scrollPos = me.scrollPos * (1 - f) + me.scrollPosTarget * f;
428 if(fabs(me.scrollPos - me.scrollPosTarget) < 0.001)
429 me.scrollPos = me.scrollPosTarget;
433 me.mouseDrag(me, me.dragScrollPos); // simulate mouseDrag event
434 me.updateControlTopBottom(me);
435 fillSize.x = (1 - me.controlWidth);
437 draw_Fill('0 0 0', '0 1 0' + fillSize, me.colorBG, me.alphaBG);
440 draw_VertButtonPicture(eX * (1 - me.controlWidth), strcat(me.src, "_s"), eX * me.controlWidth + eY, me.color2, 1);
441 if(me.getTotalHeight(me) > 1)
444 o = eX * (1 - me.controlWidth) + eY * me.controlTop;
445 s = eX * me.controlWidth + eY * (me.controlBottom - me.controlTop);
447 draw_VertButtonPicture(o, strcat(me.src, "_c"), s, me.colorC, 1);
449 draw_VertButtonPicture(o, strcat(me.src, "_f"), s, me.colorF, 1);
451 draw_VertButtonPicture(o, strcat(me.src, "_n"), s, me.color, 1);
455 oldshift = draw_shift;
456 oldscale = draw_scale;
459 i = me.getItemAtPos(me, me.scrollPos);
460 y = me.getItemStart(me, i) - me.scrollPos;
461 for (; i < me.nItems && y < 1; ++i)
463 draw_shift = boxToGlobal(eY * y, oldshift, oldscale);
464 vector relSize = eX * (1 - me.controlWidth) + eY * me.getItemHeight(me, i);
465 absSize = boxToGlobalSize(relSize, me.size);
466 draw_scale = boxToGlobalSize(relSize, oldscale);
467 me.drawListBoxItem(me, i, absSize, (me.selectedItem == i), (me.focusedItem == i));
472 draw_shift = oldshift;
473 draw_scale = oldscale;
474 SUPER(ListBox).draw(me);
477 void ListBox_clickListBoxItem(entity me, float i, vector where)
482 void ListBox_doubleClickListBoxItem(entity me, float i, vector where)
487 void ListBox_drawListBoxItem(entity me, int i, vector absSize, bool isSelected, bool isFocused)
489 draw_Text('0 0 0', sprintf(_("Item %d"), i), eX * (8 / absSize.x) + eY * (8 / absSize.y), (isSelected ? '0 1 0' : '1 1 1'), 1, 0);