]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blobdiff - qcsrc/menu/item/listbox.qc
Menu: don't allow customization of drag tolerance for slider and listbox in the skins...
[xonotic/xonotic-data.pk3dir.git] / qcsrc / menu / item / listbox.qc
index f7c17fa6bd888a73e580f825c81c077467d230a5..97f08c98113e520e6d55457356befca52319f28b 100644 (file)
-#ifndef ITEM_LISTBOX_H
-#define ITEM_LISTBOX_H
-#include "../item.qc"
-CLASS(ListBox, Item)
-       METHOD(ListBox, resizeNotify, void(entity, vector, vector, vector, vector))
-       METHOD(ListBox, configureListBox, void(entity, float, float))
-       METHOD(ListBox, draw, void(entity))
-       METHOD(ListBox, keyDown, float(entity, float, float, float))
-       METHOD(ListBox, mouseMove, float(entity, vector))
-       METHOD(ListBox, mousePress, float(entity, vector))
-       METHOD(ListBox, mouseDrag, float(entity, vector))
-       METHOD(ListBox, mouseRelease, float(entity, vector))
-       METHOD(ListBox, focusLeave, void(entity))
-       ATTRIB(ListBox, focusable, float, 1)
-       ATTRIB(ListBox, focusedItem, int, -1)
-       ATTRIB(ListBox, focusedItemAlpha, float, 0.3)
-       ATTRIB(ListBox, allowFocusSound, float, 1)
-       ATTRIB(ListBox, selectedItem, int, 0)
-       ATTRIB(ListBox, size, vector, '0 0 0')
-       ATTRIB(ListBox, origin, vector, '0 0 0')
-       ATTRIB(ListBox, scrollPos, float, 0) // measured in window heights, fixed when needed
-       ATTRIB(ListBox, previousValue, float, 0)
-       ATTRIB(ListBox, pressed, float, 0) // 0 = normal, 1 = scrollbar dragging, 2 = item dragging, 3 = released
-       ATTRIB(ListBox, pressOffset, float, 0)
+#include "listbox.qh"
 
-       METHOD(ListBox, updateControlTopBottom, void(entity))
-       ATTRIB(ListBox, controlTop, float, 0)
-       ATTRIB(ListBox, controlBottom, float, 0)
-       ATTRIB(ListBox, controlWidth, float, 0)
-       ATTRIB(ListBox, dragScrollTimer, float, 0)
-       ATTRIB(ListBox, dragScrollPos, vector, '0 0 0')
-
-       ATTRIB(ListBox, src, string, string_null) // scrollbar
-       ATTRIB(ListBox, color, vector, '1 1 1')
-       ATTRIB(ListBox, color2, vector, '1 1 1')
-       ATTRIB(ListBox, colorC, vector, '1 1 1')
-       ATTRIB(ListBox, colorF, vector, '1 1 1')
-       ATTRIB(ListBox, tolerance, vector, '0 0 0') // drag tolerance
-       ATTRIB(ListBox, scrollbarWidth, float, 0) // pixels
-       ATTRIB(ListBox, nItems, float, 42)
-       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, int, vector, bool, bool)) // item number, width/height, isSelected, isFocused
-       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))
+       bool ListBox_isScrolling(entity me)
+       {
+               return me.scrollPos != me.scrollPosTarget;
+       }
 
-       // 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
+       void ListBox_scrollToItem(entity me, int i)
+       {
+               // scroll doesn't work properly until itemHeight is set to the correct value
+               // at the first resizeNotify call
+               if (me.itemHeight == 1)  // initial temporary value of itemHeight is 1
+               {
+                       me.needScrollToItem = i;
+                       return;
+               }
 
-#ifdef IMPLEMENTATION
-void ListBox_setSelected(entity me, float i)
-{
-       me.selectedItem = bound(0, i, me.nItems - 1);
-}
-void ListBox_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
-{
-       SUPER(ListBox).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
-       me.controlWidth = me.scrollbarWidth / absSize.x;
-}
-void ListBox_configureListBox(entity me, float theScrollbarWidth, float theItemHeight)
-{
-       me.scrollbarWidth = theScrollbarWidth;
-       me.itemHeight = theItemHeight;
-}
+               i = bound(0, i, me.nItems - 1);
 
-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;
-}
+               // scroll the list to make sure the selected item is visible
+               // (even if the selected item doesn't change).
+               if (i < me.getFirstFullyVisibleItemAtScrollPos(me, me.scrollPos))
+               {
+                       // above visible area
+                       me.scrollPosTarget = me.getItemStart(me, i);
+               }
+               else if (i > me.getLastFullyVisibleItemAtScrollPos(me, me.scrollPos))
+               {
+                       // below visible area
+                       if (i == me.nItems - 1) me.scrollPosTarget = me.getTotalHeight(me) - 1;
+                       else me.scrollPosTarget = me.getItemStart(me, i + 1) - 1;
+               }
+       }
 
-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)
+       void ListBox_setSelected(entity me, float i)
        {
-               me.scrollPos = max(me.scrollPos - 0.5, 0);
-               me.setSelected(me, min(me.selectedItem, me.getLastFullyVisibleItemAtScrollPos(me, me.scrollPos)));
+               i = bound(0, i, me.nItems - 1);
+               me.scrollToItem(me, i);
+               me.selectedItem = i;
        }
-       else if(key == K_MWHEELDOWN)
+       void ListBox_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
        {
-               me.scrollPos = min(me.scrollPos + 0.5, me.getTotalHeight(me) - 1);
-               me.setSelected(me, max(me.selectedItem, me.getFirstFullyVisibleItemAtScrollPos(me, me.scrollPos)));
+               SUPER(ListBox).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
+               me.controlWidth = me.scrollbarWidth / absSize.x;
        }
-       else if(key == K_PGUP || key == K_KP_PGUP)
+       void ListBox_configureListBox(entity me, float theScrollbarWidth, float theItemHeight)
        {
-               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);
+               me.scrollbarWidth = theScrollbarWidth;
+               me.itemHeight = theItemHeight;
        }
-       else if(key == K_PGDN || key == K_KP_PGDN)
+
+       float ListBox_getTotalHeight(entity me)
        {
-               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);
+               return me.nItems * me.itemHeight;
+       }
+       float ListBox_getItemAtPos(entity me, float pos)
+       {
+               return floor(pos / me.itemHeight);
        }
-       else if(key == K_UPARROW || key == K_KP_UPARROW)
-               me.setSelected(me, me.selectedItem - 1);
-       else if(key == K_DOWNARROW || key == K_KP_DOWNARROW)
-               me.setSelected(me, me.selectedItem + 1);
-       else if(key == K_HOME || key == K_KP_HOME)
+       float ListBox_getItemStart(entity me, float i)
        {
-               me.scrollPos = 0;
-               me.setSelected(me, 0);
+               return me.itemHeight * i;
        }
-       else if(key == K_END || key == K_KP_END)
+       float ListBox_getItemHeight(entity me, float i)
        {
-               me.scrollPos = max(0, me.getTotalHeight(me) - 1);
-               me.setSelected(me, me.nItems - 1);
+               return me.itemHeight;
        }
-       else
-               return 0;
-       return 1;
-}
-float ListBox_mouseMove(entity me, vector pos)
-{
-       if(pos_x < 0) return 0;
-       if(pos_y < 0) return 0;
-       if(pos_x >= 1) return 0;
-       if(pos_y >= 1) return 0;
-       if(pos_x < 1 - me.controlWidth)
+
+       float ListBox_getLastFullyVisibleItemAtScrollPos(entity me, float pos)
+       {
+               return me.getItemAtPos(me, pos + 0.999) - 1;
+       }
+       float ListBox_getFirstFullyVisibleItemAtScrollPos(entity me, float pos)
        {
-               float x;
-               x = me.focusedItem;
-               me.focusedItem = me.getItemAtPos(me, me.scrollPos + pos.y);
-               if(x != me.focusedItem)
-                       me.focusedItemAlpha = SKINALPHA_LISTBOX_FOCUSED;
+               return me.getItemAtPos(me, pos + 0.001) + 1;
        }
-       return 1;
-}
-float ListBox_mouseDrag(entity me, vector pos)
-{
-       float hit;
-       float i;
-       me.updateControlTopBottom(me);
-       me.dragScrollPos = pos;
-       if(me.pressed == 1)
+       float ListBox_keyDown(entity me, float key, float ascii, float shift)
        {
-               hit = 1;
-               if(pos.x < 1 - me.controlWidth - me.tolerance.y * me.controlWidth) hit = 0;
-               if(pos.y < 0 - me.tolerance.x) hit = 0;
-               if(pos.x >= 1 + me.tolerance.y * me.controlWidth) hit = 0;
-               if(pos.y >= 1 + me.tolerance.x) hit = 0;
-               if(hit)
+               if (key == K_MWHEELUP)
+               {
+                       me.scrollPosTarget = max(me.scrollPosTarget - 0.5, 0);
+               }
+               else if (key == K_MWHEELDOWN)
+               {
+                       me.scrollPosTarget = min(me.scrollPosTarget + 0.5, max(0, me.getTotalHeight(me) - 1));
+               }
+               else if (key == K_PGUP || key == K_KP_PGUP)
                {
-                       // calculate new pos to v
-                       float d;
-                       d = (pos.y - me.pressOffset) / (1 - (me.controlBottom - me.controlTop)) * (me.getTotalHeight(me) - 1);
-                       me.scrollPos = me.previousValue + d;
+                       if (me.selectionDoesntMatter)
+                       {
+                               me.scrollPosTarget = max(me.scrollPosTarget - 0.5, 0);
+                               return 1;
+                       }
+
+                       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)
+               {
+                       if (me.selectionDoesntMatter)
+                       {
+                               me.scrollPosTarget = min(me.scrollPosTarget + 0.5, me.nItems * me.itemHeight - 1);
+                               return 1;
+                       }
+
+                       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)
+               {
+                       if (me.selectionDoesntMatter)
+                       {
+                               me.scrollPosTarget = max(me.scrollPosTarget - me.itemHeight, 0);
+                               return 1;
+                       }
+
+                       me.setSelected(me, me.selectedItem - 1);
+               }
+               else if (key == K_DOWNARROW || key == K_KP_DOWNARROW)
+               {
+                       if (me.selectionDoesntMatter)
+                       {
+                               me.scrollPosTarget = min(me.scrollPosTarget + me.itemHeight, me.nItems * me.itemHeight - 1);
+                               return 1;
+                       }
+
+                       me.setSelected(me, me.selectedItem + 1);
+               }
+               else if (key == K_HOME || key == K_KP_HOME)
+               {
+                       me.setSelected(me, 0);
+               }
+               else if (key == K_END || key == K_KP_END)
+               {
+                       me.setSelected(me, me.nItems - 1);
                }
                else
-                       me.scrollPos = me.previousValue;
-               me.scrollPos = min(me.scrollPos, me.getTotalHeight(me) - 1);
-               me.scrollPos = max(me.scrollPos, 0);
-               i = min(me.selectedItem, me.getLastFullyVisibleItemAtScrollPos(me, me.scrollPos));
-               i = max(i, ListBox_getFirstFullyVisibleItemAtScrollPos(me, me.scrollPos));
-               me.setSelected(me, i);
+               {
+                       return 0;
+               }
+               return 1;
        }
-       else if(me.pressed == 2)
+       float ListBox_mouseMove(entity me, vector pos)
        {
-               me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
+               me.mouseMoveOffset = -1;
+               if (pos_x < 0) return 0;
+               if (pos_y < 0) return 0;
+               if (pos_x >= 1) return 0;
+               if (pos_y >= 1) return 0;
+               if (pos_x < 1 - me.controlWidth)
+               {
+                       me.mouseMoveOffset = pos.y;
+               }
+               else
+               {
+                       me.setFocusedItem(me, -1);
+                       me.mouseMoveOffset = -1;
+               }
+               return 1;
        }
-       return 1;
-}
-float ListBox_mousePress(entity me, vector pos)
-{
-       if(pos.x < 0) return 0;
-       if(pos.y < 0) return 0;
-       if(pos.x >= 1) return 0;
-       if(pos.y >= 1) return 0;
-       me.dragScrollPos = pos;
-       me.updateControlTopBottom(me);
-       me.dragScrollTimer = time;
-       if(pos.x >= 1 - me.controlWidth)
+       float ListBox_mouseDrag(entity me, vector pos)
        {
-               // if hit, set me.pressed, otherwise scroll by one page
-               if(pos.y < me.controlTop)
+               float hit;
+               me.updateControlTopBottom(me);
+               me.dragScrollPos = pos;
+               if (me.pressed == 1)
+               {
+                       hit = 1;
+                       if (pos.x < 1 - me.controlWidth - me.tolerance.x * me.controlWidth) hit = 0;
+                       if (pos.y < 0 - me.tolerance.y) hit = 0;
+                       if (pos.x >= 1 + me.tolerance.x * me.controlWidth) hit = 0;
+                       if (pos.y >= 1 + me.tolerance.y) hit = 0;
+                       if (hit)
+                       {
+                               // calculate new pos to v
+                               float d;
+                               d = (pos.y - me.pressOffset) / (1 - (me.controlBottom - me.controlTop)) * (me.getTotalHeight(me) - 1);
+                               me.scrollPosTarget = me.previousValue + d;
+                       }
+                       else
+                       {
+                               me.scrollPosTarget = me.previousValue;
+                       }
+                       me.scrollPosTarget = min(me.scrollPosTarget, me.getTotalHeight(me) - 1);
+                       me.scrollPosTarget = max(me.scrollPosTarget, 0);
+               }
+               else if (me.pressed == 2)
                {
-                       // page up
-                       me.scrollPos = max(me.scrollPos - 1, 0);
-                       me.setSelected(me, min(me.selectedItem, ListBox_getLastFullyVisibleItemAtScrollPos(me, me.scrollPos)));
+                       me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
+                       me.setFocusedItem(me, me.selectedItem);
+                       me.mouseMoveOffset = -1;
                }
-               else if(pos.y > me.controlBottom)
+               return 1;
+       }
+       METHOD(ListBox, mousePress, bool(ListBox this, vector pos))
+       {
+               if (pos.x < 0) return false;
+               if (pos.y < 0) return false;
+               if (pos.x >= 1) return false;
+               if (pos.y >= 1) return false;
+               this.dragScrollPos = pos;
+               this.updateControlTopBottom(this);
+               if (pos.x >= 1 - this.controlWidth)
                {
-                       // page down
-                       me.scrollPos = min(me.scrollPos + 1, me.getTotalHeight(me) - 1);
-                       me.setSelected(me, max(me.selectedItem, ListBox_getFirstFullyVisibleItemAtScrollPos(me, me.scrollPos)));
+                       // if hit, set this.pressed, otherwise scroll by one page
+                       if (pos.y < this.controlTop)
+                       {
+                               // page up
+                               this.scrollPosTarget = max(this.scrollPosTarget - 1, 0);
+                       }
+                       else if (pos.y > this.controlBottom)
+                       {
+                               // page down
+                               this.scrollPosTarget = min(this.scrollPosTarget + 1, this.getTotalHeight(this) - 1);
+                       }
+                       else
+                       {
+                               this.pressed = 1;
+                               this.pressOffset = pos.y;
+                               this.previousValue = this.scrollPos;
+                       }
                }
                else
                {
-                       me.pressed = 1;
-                       me.pressOffset = pos.y;
-                       me.previousValue = me.scrollPos;
+                       // continue doing that while dragging (even when dragging outside). When releasing, forward the click to the then selected item.
+                       this.pressed = 2;
+                       // an item has been clicked. Select it, ...
+                       this.setSelected(this, this.getItemAtPos(this, this.scrollPos + pos.y));
+                       this.setFocusedItem(this, this.selectedItem);
                }
+               return true;
        }
-       else
+       void ListBox_setFocusedItem(entity me, int item)
        {
-               // 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, me.getItemAtPos(me, me.scrollPos + pos.y));
-       }
-       return 1;
-}
-float ListBox_mouseRelease(entity me, vector pos)
-{
-       if(me.pressed == 1)
-       {
-               // slider dragging mode
-               // in that case, nothing happens on releasing
+               float focusedItem_save = me.focusedItem;
+               me.focusedItem = (item < me.nItems) ? item : -1;
+               if (focusedItem_save != me.focusedItem)
+               {
+                       me.focusedItemChangeNotify(me);
+                       if (me.focusedItem >= 0) me.focusedItemAlpha = SKINALPHA_LISTBOX_FOCUSED;
+               }
        }
-       else if(me.pressed == 2)
+       float ListBox_mouseRelease(entity me, vector pos)
        {
-               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, me.getItemAtPos(me, me.scrollPos + pos.y));
-               // and give it a nice click event
-               if(me.nItems > 0)
+               if (me.pressed == 1)
                {
-                       vector where = globalToBox(pos, eY * (me.getItemStart(me, me.selectedItem) - me.scrollPos), eX * (1 - me.controlWidth) + eY * me.getItemHeight(me, me.selectedItem));
+                       // slider dragging mode
+                       // in that case, nothing happens on releasing
+               }
+               else if (me.pressed == 2)
+               {
+                       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, me.getItemAtPos(me, me.scrollPos + pos.y));
+                       me.setFocusedItem(me, me.selectedItem);
+                       // and give it a nice click event
+                       if (me.nItems > 0)
+                       {
+                               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);
+                               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.lastClickedItem = me.selectedItem;
+                               me.lastClickedTime = time;
+                       }
                }
+               me.pressed = 0;
+               return 1;
        }
-       me.pressed = 0;
-       return 1;
-}
-void ListBox_focusLeave(entity me)
-{
-       // Reset the var pressed in case listbox loses focus
-       // by a mouse click on an item of the list
-       // for example showing a dialog on right click
-       me.pressed = 0;
-       me.focusedItem = -1;
-}
-void ListBox_updateControlTopBottom(entity me)
-{
-       float f;
-       // scrollPos is in 0..1 and indicates where the "page" currently shown starts.
-       if(me.getTotalHeight(me) <= 1)
+       void ListBox_focusLeave(entity me)
        {
-               // we don't need no stinkin' scrollbar, we don't need no view control...
-               me.controlTop = 0;
-               me.controlBottom = 1;
-               me.scrollPos = 0;
+               // Reset the var pressed in case listbox loses focus
+               // by a mouse click on an item of the list
+               // for example showing a dialog on right click
+               me.pressed = 0;
+               me.setFocusedItem(me, -1);
+               me.mouseMoveOffset = -1;
        }
-       else
+       void ListBox_updateControlTopBottom(entity me)
        {
-               if(frametime) // only do this in draw frames
+               float f;
+               // scrollPos is in 0..1 and indicates where the "page" currently shown starts.
+               if (me.getTotalHeight(me) <= 1)
                {
-                       if(me.dragScrollTimer < time)
+                       // we don't need no stinkin' scrollbar, we don't need no view control...
+                       me.controlTop = 0;
+                       me.controlBottom = 1;
+                       me.scrollPos = 0;
+               }
+               else
+               {
+                       // if scroll pos is below end of list, fix it
+                       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.getTotalHeight(me));
+                       me.controlBottom = min((me.scrollPos + 1) / me.getTotalHeight(me), 1);
+
+                       float minfactor;
+                       minfactor = 2 * me.controlWidth / me.size.y * me.size.x;
+                       f = me.controlBottom - me.controlTop;
+                       if (f < minfactor)  // FIXME good default?
                        {
-                               float save;
-                               save = me.scrollPos;
-                               // if selected item is below listbox, increase scrollpos so it is in
-                               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.getItemStart(me, me.selectedItem));
-                               if(me.scrollPos != save)
-                                       me.dragScrollTimer = time + 0.2;
+                               // f * X + 1 * (1-X) = minfactor
+                               // (f - 1) * X + 1 = minfactor
+                               // (f - 1) * X = minfactor - 1
+                               // X = (minfactor - 1) / (f - 1)
+                               f = (minfactor - 1) / (f - 1);
+                               me.controlTop = me.controlTop * f + 0 * (1 - f);
+                               me.controlBottom = me.controlBottom * f + 1 * (1 - f);
                        }
                }
-               // if scroll pos is below end of list, fix it
-               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.getTotalHeight(me));
-               me.controlBottom = min((me.scrollPos + 1) / me.getTotalHeight(me), 1);
+       }
+       AUTOCVAR(menu_scroll_averaging_time, float, 0.16, "smooth scroll averaging time");
+// scroll faster while dragging the scrollbar
+       AUTOCVAR(menu_scroll_averaging_time_pressed, float, 0.06, "smooth scroll averaging time when dragging the scrollbar");
+       void ListBox_draw(entity me)
+       {
+               vector fillSize = '0 0 0';
+
+               // we can't do this in mouseMove as the list can scroll without moving the cursor
+               if (me.mouseMoveOffset != -1) me.setFocusedItem(me, me.getItemAtPos(me, me.scrollPos + me.mouseMoveOffset));
 
-               float minfactor;
-               minfactor = 2 * me.controlWidth / me.size.y * me.size.x;
-               f = me.controlBottom - me.controlTop;
-               if(f < minfactor) // FIXME good default?
+               if (me.needScrollToItem >= 0)
                {
-                       // f * X + 1 * (1-X) = minfactor
-                       // (f - 1) * X + 1 = minfactor
-                       // (f - 1) * X = minfactor - 1
-                       // X = (minfactor - 1) / (f - 1)
-                       f = (minfactor - 1) / (f - 1);
-                       me.controlTop = me.controlTop * f + 0 * (1 - f);
-                       me.controlBottom = me.controlBottom * f + 1 * (1 - f);
+                       me.scrollToItem(me, me.needScrollToItem);
+                       me.needScrollToItem = -1;
                }
-       }
-}
-void ListBox_draw(entity me)
-{
-       float i;
-       vector absSize, fillSize = '0 0 0';
-       vector oldshift, oldscale;
-       if(me.pressed == 2)
-               me.mouseDrag(me, me.dragScrollPos); // simulate mouseDrag event
-       me.updateControlTopBottom(me);
-       fillSize.x = (1 - me.controlWidth);
-       if(me.alphaBG)
-               draw_Fill('0 0 0', '0 1 0' + fillSize, me.colorBG, me.alphaBG);
-       if(me.controlWidth)
-       {
-               draw_VertButtonPicture(eX * (1 - me.controlWidth), strcat(me.src, "_s"), eX * me.controlWidth + eY, me.color2, 1);
-               if(me.getTotalHeight(me) > 1)
+               if (me.scrollPos != me.scrollPosTarget)
                {
-                       vector o, s;
-                       o = eX * (1 - me.controlWidth) + eY * me.controlTop;
-                       s = eX * me.controlWidth + eY * (me.controlBottom - me.controlTop);
-                       if(me.pressed == 1)
-                               draw_VertButtonPicture(o, strcat(me.src, "_c"), s, me.colorC, 1);
-                       else if(me.focused)
-                               draw_VertButtonPicture(o, strcat(me.src, "_f"), s, me.colorF, 1);
-                       else
-                               draw_VertButtonPicture(o, strcat(me.src, "_n"), s, me.color, 1);
+                       float averaging_time = (me.pressed == 1)
+                           ? autocvar_menu_scroll_averaging_time_pressed
+                               : autocvar_menu_scroll_averaging_time;
+                       // this formula works with whatever framerate
+                       float f = averaging_time ? exp(-frametime / averaging_time) : 0;
+                       me.scrollPos = me.scrollPos * f + me.scrollPosTarget * (1 - f);
+                       if (fabs(me.scrollPos - me.scrollPosTarget) < 0.001) me.scrollPos = me.scrollPosTarget;
                }
-       }
-       draw_SetClip();
-       oldshift = draw_shift;
-       oldscale = draw_scale;
 
-       float y;
-       i = me.getItemAtPos(me, me.scrollPos);
-       y = me.getItemStart(me, i) - me.scrollPos;
-       for (; i < me.nItems && y < 1; ++i)
-       {
-               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), (me.focusedItem == i));
-               y += relSize.y;
+               if (me.pressed == 2) me.mouseDrag(me, me.dragScrollPos);  // simulate mouseDrag event
+               me.updateControlTopBottom(me);
+               fillSize.x = (1 - me.controlWidth);
+               if (me.alphaBG) draw_Fill('0 0 0', '0 1 0' + fillSize, me.colorBG, me.alphaBG);
+               if (me.controlWidth)
+               {
+                       draw_VertButtonPicture(eX * (1 - me.controlWidth), strcat(me.src, "_s"), eX * me.controlWidth + eY, me.color2, 1);
+                       if (me.getTotalHeight(me) > 1)
+                       {
+                               vector o, s;
+                               o = eX * (1 - me.controlWidth) + eY * me.controlTop;
+                               s = eX * me.controlWidth + eY * (me.controlBottom - me.controlTop);
+                               if (me.pressed == 1) draw_VertButtonPicture(o, strcat(me.src, "_c"), s, me.colorC, 1);
+                               else if (me.focused) draw_VertButtonPicture(o, strcat(me.src, "_f"), s, me.colorF, 1);
+                               else draw_VertButtonPicture(o, strcat(me.src, "_n"), s, me.color, 1);
+                       }
+               }
+               draw_SetClip();
+               vector oldshift = draw_shift;
+               vector oldscale = draw_scale;
+
+               int i = me.getItemAtPos(me, me.scrollPos);
+               float y = me.getItemStart(me, i) - me.scrollPos;
+               for ( ; i < me.nItems && y < 1; ++i)
+               {
+                       draw_shift = boxToGlobal(eY * y, oldshift, oldscale);
+                       vector relSize = eX * (1 - me.controlWidth) + eY * me.getItemHeight(me, i);
+                       vector absSize = boxToGlobalSize(relSize, me.size);
+                       draw_scale = boxToGlobalSize(relSize, oldscale);
+                       me.drawListBoxItem(me, i, absSize, (me.selectedItem == i), (me.focusedItem == i));
+                       y += relSize.y;
+               }
+               draw_ClearClip();
+
+               draw_shift = oldshift;
+               draw_scale = oldscale;
+               SUPER(ListBox).draw(me);
        }
-       draw_ClearClip();
 
-       draw_shift = oldshift;
-       draw_scale = oldscale;
-       SUPER(ListBox).draw(me);
-}
+       void ListBox_focusedItemChangeNotify(entity me)
+       {}
 
-void ListBox_clickListBoxItem(entity me, float i, vector where)
-{
-       // template method
-}
+       void ListBox_clickListBoxItem(entity me, float i, vector where)
+       {
+               // template method
+       }
 
-void ListBox_doubleClickListBoxItem(entity me, float i, vector where)
-{
-       // template method
-}
+       void ListBox_doubleClickListBoxItem(entity me, float i, vector where)
+       {
+               // template method
+       }
 
-void ListBox_drawListBoxItem(entity me, int i, vector absSize, bool isSelected, bool isFocused)
-{
-       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);
-}
-#endif
+       void ListBox_drawListBoxItem(entity me, int i, vector absSize, bool isSelected, bool isFocused)
+       {
+               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);
+       }