METHOD(ListBox, mouseRelease, float(entity, vector))
METHOD(ListBox, focusLeave, void(entity))
ATTRIB(ListBox, focusable, float, 1)
+ ATTRIB(ListBox, allowFocusSound, float, 1)
ATTRIB(ListBox, selectedItem, float, 0)
ATTRIB(ListBox, size, vector, '0 0 0')
ATTRIB(ListBox, origin, vector, '0 0 0')
ATTRIB(ListBox, itemHeight, float, 0)
ATTRIB(ListBox, colorBG, vector, '0 0 0')
ATTRIB(ListBox, alphaBG, float, 0)
+
+ ATTRIB(ListBox, lastClickedItem, float, -1)
+ ATTRIB(ListBox, lastClickedTime, float, 0)
+
METHOD(ListBox, drawListBoxItem, void(entity, float, vector, float)) // item number, width/height, selected
METHOD(ListBox, clickListBoxItem, void(entity, float, vector)) // item number, relative clickpos
+ METHOD(ListBox, doubleClickListBoxItem, void(entity, float, vector)) // item number, relative clickpos
METHOD(ListBox, setSelected, void(entity, float))
+
+ METHOD(ListBox, getLastFullyVisibleItemAtScrollPos, float(entity, float))
+ METHOD(ListBox, getFirstFullyVisibleItemAtScrollPos, float(entity, float))
+
+ // NOTE: override these four methods if you want variable sized list items
+ METHOD(ListBox, getTotalHeight, float(entity))
+ METHOD(ListBox, getItemAtPos, float(entity, float))
+ METHOD(ListBox, getItemStart, float(entity, float))
+ METHOD(ListBox, getItemHeight, float(entity, float))
+ // NOTE: if getItemAt* are overridden, it may make sense to cache the
+ // start and height of the last item returned by getItemAtPos and fast
+ // track returning their properties for getItemStart and getItemHeight.
+ // The "hot" code path calls getItemAtPos first, then will query
+ // getItemStart and getItemHeight on it soon.
+ // When overriding, the following consistency rules must hold:
+ // getTotalHeight() == SUM(getItemHeight(i), i, 0, me.nItems-1)
+ // getItemStart(i+1) == getItemStart(i) + getItemHeight(i)
+ // for 0 <= i < me.nItems-1
+ // getItemStart(0) == 0
+ // getItemStart(getItemAtPos(p)) <= p
+ // if p >= 0
+ // getItemAtPos(p) == 0
+ // if p < 0
+ // getItemStart(getItemAtPos(p)) + getItemHeight(getItemAtPos(p)) > p
+ // if p < getTotalHeigt()
+ // getItemAtPos(p) == me.nItems - 1
+ // if p >= getTotalHeight()
ENDCLASS(ListBox)
#endif
#ifdef IMPLEMENTATION
void ListBox_setSelected(entity me, float i)
{
- me.selectedItem = floor(0.5 + bound(0, i, me.nItems - 1));
+ me.selectedItem = bound(0, i, me.nItems - 1);
}
void ListBox_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
{
me.scrollbarWidth = theScrollbarWidth;
me.itemHeight = theItemHeight;
}
+
+float ListBox_getTotalHeight(entity me)
+{
+ return me.nItems * me.itemHeight;
+}
+float ListBox_getItemAtPos(entity me, float pos)
+{
+ return floor(pos / me.itemHeight);
+}
+float ListBox_getItemStart(entity me, float i)
+{
+ return me.itemHeight * i;
+}
+float ListBox_getItemHeight(entity me, float i)
+{
+ return me.itemHeight;
+}
+
+float ListBox_getLastFullyVisibleItemAtScrollPos(entity me, float pos)
+{
+ return me.getItemAtPos(me, pos + 1.001) - 1;
+}
+float ListBox_getFirstFullyVisibleItemAtScrollPos(entity me, float pos)
+{
+ return me.getItemAtPos(me, pos - 0.001) + 1;
+}
float ListBox_keyDown(entity me, float key, float ascii, float shift)
{
me.dragScrollTimer = time;
if(key == K_MWHEELUP)
{
me.scrollPos = max(me.scrollPos - 0.5, 0);
- me.setSelected(me, min(me.selectedItem, floor((me.scrollPos + 1) / me.itemHeight - 1)));
+ me.setSelected(me, min(me.selectedItem, me.getLastFullyVisibleItemAtScrollPos(me, me.scrollPos)));
}
else if(key == K_MWHEELDOWN)
{
- me.scrollPos = min(me.scrollPos + 0.5, me.nItems * me.itemHeight - 1);
- me.setSelected(me, max(me.selectedItem, ceil(me.scrollPos / me.itemHeight)));
+ me.scrollPos = min(me.scrollPos + 0.5, me.getTotalHeight(me) - 1);
+ me.setSelected(me, max(me.selectedItem, me.getFirstFullyVisibleItemAtScrollPos(me, me.scrollPos)));
}
else if(key == K_PGUP || key == K_KP_PGUP)
- me.setSelected(me, me.selectedItem - 1 / me.itemHeight);
+ {
+ float i = me.selectedItem;
+ float a = me.getItemHeight(me, i);
+ for(;;)
+ {
+ --i;
+ if (i < 0)
+ break;
+ a += me.getItemHeight(me, i);
+ if (a >= 1)
+ break;
+ }
+ me.setSelected(me, i + 1);
+ }
else if(key == K_PGDN || key == K_KP_PGDN)
- me.setSelected(me, me.selectedItem + 1 / me.itemHeight);
+ {
+ float i = me.selectedItem;
+ float a = me.getItemHeight(me, i);
+ for(;;)
+ {
+ ++i;
+ if (i >= me.nItems)
+ break;
+ a += me.getItemHeight(me, i);
+ if (a >= 1)
+ break;
+ }
+ me.setSelected(me, i - 1);
+ }
else if(key == K_UPARROW || key == K_KP_UPARROW)
me.setSelected(me, me.selectedItem - 1);
else if(key == K_DOWNARROW || key == K_KP_DOWNARROW)
}
else if(key == K_END || key == K_KP_END)
{
- me.scrollPos = max(0, me.nItems * me.itemHeight - 1);
+ me.scrollPos = max(0, me.getTotalHeight(me) - 1);
me.setSelected(me, me.nItems - 1);
}
else
{
// calculate new pos to v
float d;
- d = (pos_y - me.pressOffset) / (1 - (me.controlBottom - me.controlTop)) * (me.nItems * me.itemHeight - 1);
+ d = (pos_y - me.pressOffset) / (1 - (me.controlBottom - me.controlTop)) * (me.getTotalHeight(me) - 1);
me.scrollPos = me.previousValue + d;
}
else
me.scrollPos = me.previousValue;
- me.scrollPos = min(me.scrollPos, me.nItems * me.itemHeight - 1);
+ me.scrollPos = min(me.scrollPos, me.getTotalHeight(me) - 1);
me.scrollPos = max(me.scrollPos, 0);
- i = min(me.selectedItem, floor((me.scrollPos + 1) / me.itemHeight - 1));
- i = max(i, ceil(me.scrollPos / me.itemHeight));
+ i = min(me.selectedItem, me.getLastFullyVisibleItemAtScrollPos(me, me.scrollPos));
+ i = max(i, ListBox_getFirstFullyVisibleItemAtScrollPos(me, me.scrollPos));
me.setSelected(me, i);
}
else if(me.pressed == 2)
{
- me.setSelected(me, floor((me.scrollPos + pos_y) / me.itemHeight));
+ me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos_y));
}
return 1;
}
{
// page up
me.scrollPos = max(me.scrollPos - 1, 0);
- me.setSelected(me, min(me.selectedItem, floor((me.scrollPos + 1) / me.itemHeight - 1)));
+ me.setSelected(me, min(me.selectedItem, ListBox_getLastFullyVisibleItemAtScrollPos(me, me.scrollPos)));
}
else if(pos_y > me.controlBottom)
{
// page down
- me.scrollPos = min(me.scrollPos + 1, me.nItems * me.itemHeight - 1);
- me.setSelected(me, max(me.selectedItem, ceil(me.scrollPos / me.itemHeight)));
+ me.scrollPos = min(me.scrollPos + 1, me.getTotalHeight(me) - 1);
+ me.setSelected(me, max(me.selectedItem, ListBox_getFirstFullyVisibleItemAtScrollPos(me, me.scrollPos)));
}
else
{
// continue doing that while dragging (even when dragging outside). When releasing, forward the click to the then selected item.
me.pressed = 2;
// an item has been clicked. Select it, ...
- me.setSelected(me, floor((me.scrollPos + pos_y) / me.itemHeight));
+ me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos_y));
}
return 1;
}
float ListBox_mouseRelease(entity me, vector pos)
{
- vector absSize;
if(me.pressed == 1)
{
// slider dragging mode
me.pressed = 3; // do that here, so setSelected can know the mouse has been released
// item dragging mode
// select current one one last time...
- me.setSelected(me, floor((me.scrollPos + pos_y) / me.itemHeight));
+ me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos_y));
// and give it a nice click event
if(me.nItems > 0)
{
- absSize = boxToGlobalSize(me.size, eX * (1 - me.controlWidth) + eY * me.itemHeight);
- me.clickListBoxItem(me, me.selectedItem, globalToBox(pos, eY * (me.selectedItem * me.itemHeight - me.scrollPos), eX * (1 - me.controlWidth) + eY * me.itemHeight));
+ vector where = globalToBox(pos, eY * (me.getItemStart(me, me.selectedItem) - me.scrollPos), eX * (1 - me.controlWidth) + eY * me.getItemHeight(me, me.selectedItem));
+
+ if((me.selectedItem == me.lastClickedItem) && (time < me.lastClickedTime + 0.3))
+ me.doubleClickListBoxItem(me, me.selectedItem, where);
+ else
+ me.clickListBoxItem(me, me.selectedItem, where);
+
+ me.lastClickedItem = me.selectedItem;
+ me.lastClickedTime = time;
}
}
me.pressed = 0;
{
float f;
// scrollPos is in 0..1 and indicates where the "page" currently shown starts.
- if(me.nItems * me.itemHeight <= 1)
+ if(me.getTotalHeight(me) <= 1)
{
// we don't need no stinkin' scrollbar, we don't need no view control...
me.controlTop = 0;
float save;
save = me.scrollPos;
// if selected item is below listbox, increase scrollpos so it is in
- me.scrollPos = max(me.scrollPos, me.selectedItem * me.itemHeight - 1 + me.itemHeight);
+ me.scrollPos = max(me.scrollPos, me.getItemStart(me, me.selectedItem) + me.getItemHeight(me, me.selectedItem) - 1);
// if selected item is above listbox, decrease scrollpos so it is in
- me.scrollPos = min(me.scrollPos, me.selectedItem * me.itemHeight);
+ me.scrollPos = min(me.scrollPos, me.getItemStart(me, me.selectedItem));
if(me.scrollPos != save)
me.dragScrollTimer = time + 0.2;
}
}
// if scroll pos is below end of list, fix it
- me.scrollPos = min(me.scrollPos, me.nItems * me.itemHeight - 1);
+ me.scrollPos = min(me.scrollPos, me.getTotalHeight(me) - 1);
// if scroll pos is above beginning of list, fix it
me.scrollPos = max(me.scrollPos, 0);
// now that we know where the list is scrolled to, find out where to draw the control
- me.controlTop = max(0, me.scrollPos / (me.nItems * me.itemHeight));
- me.controlBottom = min((me.scrollPos + 1) / (me.nItems * me.itemHeight), 1);
+ me.controlTop = max(0, me.scrollPos / me.getTotalHeight(me));
+ me.controlBottom = min((me.scrollPos + 1) / me.getTotalHeight(me), 1);
float minfactor;
- minfactor = 1 * me.controlWidth / me.size_y * me.size_x;
+ minfactor = 2 * me.controlWidth / me.size_y * me.size_x;
f = me.controlBottom - me.controlTop;
if(f < minfactor) // FIXME good default?
{
void ListBox_draw(entity me)
{
float i;
- vector absSize, fillSize;
+ vector absSize, fillSize = '0 0 0';
vector oldshift, oldscale;
if(me.pressed == 2)
me.mouseDrag(me, me.dragScrollPos); // simulate mouseDrag event
if(me.controlWidth)
{
draw_VertButtonPicture(eX * (1 - me.controlWidth), strcat(me.src, "_s"), eX * me.controlWidth + eY, me.color2, 1);
- if(me.nItems * me.itemHeight > 1)
+ if(me.getTotalHeight(me) > 1)
{
vector o, s;
o = eX * (1 - me.controlWidth) + eY * me.controlTop;
draw_SetClip();
oldshift = draw_shift;
oldscale = draw_scale;
- absSize = boxToGlobalSize(me.size, eX * (1 - me.controlWidth) + eY * me.itemHeight);
- draw_scale = boxToGlobalSize(eX * (1 - me.controlWidth) + eY * me.itemHeight, oldscale);
- for(i = floor(me.scrollPos / me.itemHeight); i < me.nItems; ++i)
+ float y;
+ i = me.getItemAtPos(me, me.scrollPos);
+ y = me.getItemStart(me, i) - me.scrollPos;
+ for(; i < me.nItems && y < 1; ++i)
{
- float y;
- y = i * me.itemHeight - me.scrollPos;
- if(y >= 1)
- break;
draw_shift = boxToGlobal(eY * y, oldshift, oldscale);
+ vector relSize = eX * (1 - me.controlWidth) + eY * me.getItemHeight(me, i);
+ absSize = boxToGlobalSize(relSize, me.size);
+ draw_scale = boxToGlobalSize(relSize, oldscale);
me.drawListBoxItem(me, i, absSize, (me.selectedItem == i));
+ y += relSize_y;
}
draw_ClearClip();
void ListBox_clickListBoxItem(entity me, float i, vector where)
{
- // itemclick, itemclick, does whatever itemclick does
+ // template method
+}
+
+void ListBox_doubleClickListBoxItem(entity me, float i, vector where)
+{
+ // template method
}
void ListBox_drawListBoxItem(entity me, float i, vector absSize, float selected)