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 METHOD(ListBox, setFocusedItem, void(entity, int));
18 ATTRIB(ListBox, mouseMoveOffset, float, -1) // let know where the cursor is when the list scrolls without moving the cursor
19 ATTRIB(ListBox, allowFocusSound, float, 1)
20 ATTRIB(ListBox, selectedItem, int, 0)
21 ATTRIB(ListBox, size, vector, '0 0 0')
22 ATTRIB(ListBox, origin, vector, '0 0 0')
23 ATTRIB(ListBox, scrollPos, float, 0) // measured in window heights, fixed when needed
24 ATTRIB(ListBox, scrollPosTarget, float, 0)
25 METHOD(ListBox, isScrolling, bool(entity));
26 ATTRIB(ListBox, needScrollToItem, float, -1)
27 METHOD(ListBox, scrollToItem, void(entity, int));
28 ATTRIB(ListBox, previousValue, float, 0)
29 ATTRIB(ListBox, pressed, float, 0) // 0 = normal, 1 = scrollbar dragging, 2 = item dragging, 3 = released
30 ATTRIB(ListBox, pressOffset, float, 0)
32 METHOD(ListBox, updateControlTopBottom, void(entity));
33 ATTRIB(ListBox, controlTop, float, 0)
34 ATTRIB(ListBox, controlBottom, float, 0)
35 ATTRIB(ListBox, controlWidth, float, 0)
36 ATTRIB(ListBox, dragScrollPos, vector, '0 0 0')
37 ATTRIB(ListBox, selectionDoesntMatter, bool, false) // improves scrolling by keys for lists that don't need to show an active selection
39 ATTRIB(ListBox, src, string, string_null) // scrollbar
40 ATTRIB(ListBox, color, vector, '1 1 1')
41 ATTRIB(ListBox, color2, vector, '1 1 1')
42 ATTRIB(ListBox, colorC, vector, '1 1 1')
43 ATTRIB(ListBox, colorF, vector, '1 1 1')
44 ATTRIB(ListBox, tolerance, vector, '0 0 0') // drag tolerance
45 ATTRIB(ListBox, scrollbarWidth, float, 0) // pixels
46 ATTRIB(ListBox, nItems, float, 42) // FIXME: why?!?
47 ATTRIB(ListBox, itemHeight, float, 0)
48 ATTRIB(ListBox, colorBG, vector, '0 0 0')
49 ATTRIB(ListBox, alphaBG, float, 0)
51 ATTRIB(ListBox, lastClickedItem, float, -1)
52 ATTRIB(ListBox, lastClickedTime, float, 0)
54 METHOD(ListBox, drawListBoxItem, void(entity, int, vector, bool, bool)); // item number, width/height, isSelected, isFocused
55 METHOD(ListBox, clickListBoxItem, void(entity, float, vector)); // item number, relative clickpos
56 METHOD(ListBox, doubleClickListBoxItem, void(entity, float, vector)); // item number, relative clickpos
57 METHOD(ListBox, setSelected, void(entity, float));
58 METHOD(ListBox, focusedItemChangeNotify, void(entity));
60 METHOD(ListBox, getLastFullyVisibleItemAtScrollPos, float(entity, float));
61 METHOD(ListBox, getFirstFullyVisibleItemAtScrollPos, float(entity, float));
63 // NOTE: override these four methods if you want variable sized list items
64 METHOD(ListBox, getTotalHeight, float(entity));
65 METHOD(ListBox, getItemAtPos, float(entity, float));
66 METHOD(ListBox, getItemStart, float(entity, float));
67 METHOD(ListBox, getItemHeight, float(entity, float));
68 // NOTE: if getItemAt* are overridden, it may make sense to cache the
69 // start and height of the last item returned by getItemAtPos and fast
70 // track returning their properties for getItemStart and getItemHeight.
71 // The "hot" code path calls getItemAtPos first, then will query
72 // getItemStart and getItemHeight on it soon.
73 // When overriding, the following consistency rules must hold:
74 // getTotalHeight() == SUM(getItemHeight(i), i, 0, me.nItems-1)
75 // getItemStart(i+1) == getItemStart(i) + getItemHeight(i)
76 // for 0 <= i < me.nItems-1
77 // getItemStart(0) == 0
78 // getItemStart(getItemAtPos(p)) <= p
80 // getItemAtPos(p) == 0
82 // getItemStart(getItemAtPos(p)) + getItemHeight(getItemAtPos(p)) > p
83 // if p < getTotalHeigt()
84 // getItemAtPos(p) == me.nItems - 1
85 // if p >= getTotalHeight()
90 bool ListBox_isScrolling(entity me)
92 return me.scrollPos != me.scrollPosTarget;
95 void ListBox_scrollToItem(entity me, int i)
97 // scroll doesn't work properly until itemHeight is set to the correct value
98 // at the first resizeNotify call
99 if (me.itemHeight == 1) // initial temporary value of itemHeight is 1
101 me.needScrollToItem = i;
105 i = bound(0, i, me.nItems - 1);
107 // scroll the list to make sure the selected item is visible
108 // (even if the selected item doesn't change).
109 if (i < me.getFirstFullyVisibleItemAtScrollPos(me, me.scrollPos))
111 // above visible area
112 me.scrollPosTarget = me.getItemStart(me, i);
114 else if (i > me.getLastFullyVisibleItemAtScrollPos(me, me.scrollPos))
116 // below visible area
117 if (i == me.nItems - 1) me.scrollPosTarget = me.getTotalHeight(me) - 1;
118 else me.scrollPosTarget = me.getItemStart(me, i + 1) - 1;
122 void ListBox_setSelected(entity me, float i)
124 i = bound(0, i, me.nItems - 1);
125 me.scrollToItem(me, i);
128 void ListBox_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
130 SUPER(ListBox).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
131 me.controlWidth = me.scrollbarWidth / absSize.x;
133 void ListBox_configureListBox(entity me, float theScrollbarWidth, float theItemHeight)
135 me.scrollbarWidth = theScrollbarWidth;
136 me.itemHeight = theItemHeight;
139 float ListBox_getTotalHeight(entity me)
141 return me.nItems * me.itemHeight;
143 float ListBox_getItemAtPos(entity me, float pos)
145 return floor(pos / me.itemHeight);
147 float ListBox_getItemStart(entity me, float i)
149 return me.itemHeight * i;
151 float ListBox_getItemHeight(entity me, float i)
153 return me.itemHeight;
156 float ListBox_getLastFullyVisibleItemAtScrollPos(entity me, float pos)
158 return me.getItemAtPos(me, pos + 0.999) - 1;
160 float ListBox_getFirstFullyVisibleItemAtScrollPos(entity me, float pos)
162 return me.getItemAtPos(me, pos + 0.001) + 1;
164 float ListBox_keyDown(entity me, float key, float ascii, float shift)
166 if (key == K_MWHEELUP)
168 me.scrollPosTarget = max(me.scrollPosTarget - 0.5, 0);
170 else if (key == K_MWHEELDOWN)
172 me.scrollPosTarget = min(me.scrollPosTarget + 0.5, max(0, me.getTotalHeight(me) - 1));
174 else if (key == K_PGUP || key == K_KP_PGUP)
176 if (me.selectionDoesntMatter)
178 me.scrollPosTarget = max(me.scrollPosTarget - 0.5, 0);
182 float i = me.selectedItem;
183 float a = me.getItemHeight(me, i);
188 a += me.getItemHeight(me, i);
191 me.setSelected(me, i + 1);
193 else if (key == K_PGDN || key == K_KP_PGDN)
195 if (me.selectionDoesntMatter)
197 me.scrollPosTarget = min(me.scrollPosTarget + 0.5, me.nItems * me.itemHeight - 1);
201 float i = me.selectedItem;
202 float a = me.getItemHeight(me, i);
206 if (i >= me.nItems) break;
207 a += me.getItemHeight(me, i);
210 me.setSelected(me, i - 1);
212 else if (key == K_UPARROW || key == K_KP_UPARROW)
214 if (me.selectionDoesntMatter)
216 me.scrollPosTarget = max(me.scrollPosTarget - me.itemHeight, 0);
220 me.setSelected(me, me.selectedItem - 1);
222 else if (key == K_DOWNARROW || key == K_KP_DOWNARROW)
224 if (me.selectionDoesntMatter)
226 me.scrollPosTarget = min(me.scrollPosTarget + me.itemHeight, me.nItems * me.itemHeight - 1);
230 me.setSelected(me, me.selectedItem + 1);
232 else if (key == K_HOME || key == K_KP_HOME)
234 me.setSelected(me, 0);
236 else if (key == K_END || key == K_KP_END)
238 me.setSelected(me, me.nItems - 1);
246 float ListBox_mouseMove(entity me, vector pos)
248 me.mouseMoveOffset = -1;
249 if (pos_x < 0) return 0;
250 if (pos_y < 0) return 0;
251 if (pos_x >= 1) return 0;
252 if (pos_y >= 1) return 0;
253 if (pos_x < 1 - me.controlWidth)
255 me.mouseMoveOffset = pos.y;
259 me.setFocusedItem(me, -1);
260 me.mouseMoveOffset = -1;
264 float ListBox_mouseDrag(entity me, vector pos)
267 me.updateControlTopBottom(me);
268 me.dragScrollPos = pos;
272 if (pos.x < 1 - me.controlWidth - me.tolerance.y * me.controlWidth) hit = 0;
273 if (pos.y < 0 - me.tolerance.x) hit = 0;
274 if (pos.x >= 1 + me.tolerance.y * me.controlWidth) hit = 0;
275 if (pos.y >= 1 + me.tolerance.x) hit = 0;
278 // calculate new pos to v
280 d = (pos.y - me.pressOffset) / (1 - (me.controlBottom - me.controlTop)) * (me.getTotalHeight(me) - 1);
281 me.scrollPosTarget = me.previousValue + d;
285 me.scrollPosTarget = me.previousValue;
287 me.scrollPosTarget = min(me.scrollPosTarget, me.getTotalHeight(me) - 1);
288 me.scrollPosTarget = max(me.scrollPosTarget, 0);
290 else if (me.pressed == 2)
292 me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
293 me.setFocusedItem(me, me.selectedItem);
294 me.mouseMoveOffset = -1;
298 float ListBox_mousePress(entity me, vector pos)
300 if (pos.x < 0) return 0;
301 if (pos.y < 0) return 0;
302 if (pos.x >= 1) return 0;
303 if (pos.y >= 1) return 0;
304 me.dragScrollPos = pos;
305 me.updateControlTopBottom(me);
306 if (pos.x >= 1 - me.controlWidth)
308 // if hit, set me.pressed, otherwise scroll by one page
309 if (pos.y < me.controlTop)
312 me.scrollPosTarget = max(me.scrollPosTarget - 1, 0);
314 else if (pos.y > me.controlBottom)
317 me.scrollPosTarget = min(me.scrollPosTarget + 1, me.getTotalHeight(me) - 1);
322 me.pressOffset = pos.y;
323 me.previousValue = me.scrollPos;
328 // continue doing that while dragging (even when dragging outside). When releasing, forward the click to the then selected item.
330 // an item has been clicked. Select it, ...
331 me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
332 me.setFocusedItem(me, me.selectedItem);
336 void ListBox_setFocusedItem(entity me, int item)
338 float focusedItem_save = me.focusedItem;
339 me.focusedItem = (item < me.nItems) ? item : -1;
340 if (focusedItem_save != me.focusedItem)
342 me.focusedItemChangeNotify(me);
343 if (me.focusedItem >= 0) me.focusedItemAlpha = SKINALPHA_LISTBOX_FOCUSED;
346 float ListBox_mouseRelease(entity me, vector pos)
350 // slider dragging mode
351 // in that case, nothing happens on releasing
353 else if (me.pressed == 2)
355 me.pressed = 3; // do that here, so setSelected can know the mouse has been released
356 // item dragging mode
357 // select current one one last time...
358 me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
359 me.setFocusedItem(me, me.selectedItem);
360 // and give it a nice click event
363 vector where = globalToBox(pos, eY * (me.getItemStart(me, me.selectedItem) - me.scrollPos), eX * (1 - me.controlWidth) + eY * me.getItemHeight(me, me.selectedItem));
365 if ((me.selectedItem == me.lastClickedItem) && (time < me.lastClickedTime + 0.3)) me.doubleClickListBoxItem(me, me.selectedItem, where);
366 else me.clickListBoxItem(me, me.selectedItem, where);
368 me.lastClickedItem = me.selectedItem;
369 me.lastClickedTime = time;
375 void ListBox_focusLeave(entity me)
377 // Reset the var pressed in case listbox loses focus
378 // by a mouse click on an item of the list
379 // for example showing a dialog on right click
381 me.setFocusedItem(me, -1);
382 me.mouseMoveOffset = -1;
384 void ListBox_updateControlTopBottom(entity me)
387 // scrollPos is in 0..1 and indicates where the "page" currently shown starts.
388 if (me.getTotalHeight(me) <= 1)
390 // we don't need no stinkin' scrollbar, we don't need no view control...
392 me.controlBottom = 1;
397 // if scroll pos is below end of list, fix it
398 me.scrollPos = min(me.scrollPos, me.getTotalHeight(me) - 1);
399 // if scroll pos is above beginning of list, fix it
400 me.scrollPos = max(me.scrollPos, 0);
401 // now that we know where the list is scrolled to, find out where to draw the control
402 me.controlTop = max(0, me.scrollPos / me.getTotalHeight(me));
403 me.controlBottom = min((me.scrollPos + 1) / me.getTotalHeight(me), 1);
406 minfactor = 2 * me.controlWidth / me.size.y * me.size.x;
407 f = me.controlBottom - me.controlTop;
408 if (f < minfactor) // FIXME good default?
410 // f * X + 1 * (1-X) = minfactor
411 // (f - 1) * X + 1 = minfactor
412 // (f - 1) * X = minfactor - 1
413 // X = (minfactor - 1) / (f - 1)
414 f = (minfactor - 1) / (f - 1);
415 me.controlTop = me.controlTop * f + 0 * (1 - f);
416 me.controlBottom = me.controlBottom * f + 1 * (1 - f);
420 AUTOCVAR(menu_scroll_averaging_time, float, 0.16, "smooth scroll averaging time");
421 // scroll faster while dragging the scrollbar
422 AUTOCVAR(menu_scroll_averaging_time_pressed, float, 0.06, "smooth scroll averaging time when dragging the scrollbar");
423 void ListBox_draw(entity me)
426 vector absSize, fillSize = '0 0 0';
427 vector oldshift, oldscale;
429 // we can't do this in mouseMove as the list can scroll without moving the cursor
430 if (me.mouseMoveOffset != -1) me.setFocusedItem(me, me.getItemAtPos(me, me.scrollPos + me.mouseMoveOffset));
432 if (me.needScrollToItem >= 0)
434 me.scrollToItem(me, me.needScrollToItem);
435 me.needScrollToItem = -1;
437 if (me.scrollPos != me.scrollPosTarget)
439 float averaging_time = (me.pressed == 1)
440 ? autocvar_menu_scroll_averaging_time_pressed
441 : autocvar_menu_scroll_averaging_time;
442 // this formula works with whatever framerate
443 float f = averaging_time ? exp(-frametime / averaging_time) : 0;
444 me.scrollPos = me.scrollPos * f + me.scrollPosTarget * (1 - f);
445 if (fabs(me.scrollPos - me.scrollPosTarget) < 0.001) me.scrollPos = me.scrollPosTarget;
448 if (me.pressed == 2) me.mouseDrag(me, me.dragScrollPos); // simulate mouseDrag event
449 me.updateControlTopBottom(me);
450 fillSize.x = (1 - me.controlWidth);
451 if (me.alphaBG) draw_Fill('0 0 0', '0 1 0' + fillSize, me.colorBG, me.alphaBG);
454 draw_VertButtonPicture(eX * (1 - me.controlWidth), strcat(me.src, "_s"), eX * me.controlWidth + eY, me.color2, 1);
455 if (me.getTotalHeight(me) > 1)
458 o = eX * (1 - me.controlWidth) + eY * me.controlTop;
459 s = eX * me.controlWidth + eY * (me.controlBottom - me.controlTop);
460 if (me.pressed == 1) draw_VertButtonPicture(o, strcat(me.src, "_c"), s, me.colorC, 1);
461 else if (me.focused) draw_VertButtonPicture(o, strcat(me.src, "_f"), s, me.colorF, 1);
462 else draw_VertButtonPicture(o, strcat(me.src, "_n"), s, me.color, 1);
466 oldshift = draw_shift;
467 oldscale = draw_scale;
470 i = me.getItemAtPos(me, me.scrollPos);
471 y = me.getItemStart(me, i) - me.scrollPos;
472 for ( ; i < me.nItems && y < 1; ++i)
474 draw_shift = boxToGlobal(eY * y, oldshift, oldscale);
475 vector relSize = eX * (1 - me.controlWidth) + eY * me.getItemHeight(me, i);
476 absSize = boxToGlobalSize(relSize, me.size);
477 draw_scale = boxToGlobalSize(relSize, oldscale);
478 me.drawListBoxItem(me, i, absSize, (me.selectedItem == i), (me.focusedItem == i));
483 draw_shift = oldshift;
484 draw_scale = oldscale;
485 SUPER(ListBox).draw(me);
488 void ListBox_focusedItemChangeNotify(entity me)
491 void ListBox_clickListBoxItem(entity me, float i, vector where)
496 void ListBox_doubleClickListBoxItem(entity me, float i, vector where)
501 void ListBox_drawListBoxItem(entity me, int i, vector absSize, bool isSelected, bool isFocused)
503 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);