]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blobdiff - qcsrc/menu/item/listbox.qc
Merge branch 'master' into terencehill/menu_listbox_changes
[xonotic/xonotic-data.pk3dir.git] / qcsrc / menu / item / listbox.qc
index b92beb3d1f7b954f6cf72193a2aef44b7f3ab70b..1940e324f27b0fa3174bbaf1db2652cf13292c25 100644 (file)
@@ -1,18 +1,28 @@
-#ifdef INTERFACE
-CLASS(ListBox) EXTENDS(Item)
+#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, selectedItem, float, 0)
+       ATTRIB(ListBox, focusedItem, int, -1)
+       ATTRIB(ListBox, focusedItemAlpha, float, 0.3)
+       ATTRIB(ListBox, mouseMoveOffset, float, -1) // let know where the cursor is when the list scrolls without moving the cursor
+       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, scrollPosTarget, float, 0)
+       ATTRIB(ListBox, needScrollToItem, float, -1)
+       METHOD(ListBox, scrollToItem, void(entity, int))
        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)
@@ -21,8 +31,8 @@ CLASS(ListBox) EXTENDS(Item)
        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, selectionDoesntMatter, bool, false) // improves scrolling by keys for lists that don't need to show an active selection
 
        ATTRIB(ListBox, src, string, string_null) // scrollbar
        ATTRIB(ListBox, color, vector, '1 1 1')
@@ -39,7 +49,7 @@ CLASS(ListBox) EXTENDS(Item)
        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, 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))
@@ -74,14 +84,45 @@ ENDCLASS(ListBox)
 #endif
 
 #ifdef IMPLEMENTATION
+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;
+       }
+
+       i = bound(0, i, me.nItems - 1);
+
+       // 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;
+       }
+}
+
 void ListBox_setSelected(entity me, float i)
 {
-       me.selectedItem = bound(0, i, me.nItems - 1);
+       i = bound(0, i, me.nItems - 1);
+       me.scrollToItem(me, i);
+       me.selectedItem = i;
 }
 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;
+       me.controlWidth = me.scrollbarWidth / absSize.x;
 }
 void ListBox_configureListBox(entity me, float theScrollbarWidth, float theItemHeight)
 {
@@ -108,30 +149,33 @@ float ListBox_getItemHeight(entity me, float i)
 
 float ListBox_getLastFullyVisibleItemAtScrollPos(entity me, float pos)
 {
-       return me.getItemAtPos(me, pos + 1.001) - 1;
+       return me.getItemAtPos(me, pos + 0.999) - 1;
 }
 float ListBox_getFirstFullyVisibleItemAtScrollPos(entity me, float pos)
 {
-       return me.getItemAtPos(me, pos - 0.001) + 1;
+       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, me.getLastFullyVisibleItemAtScrollPos(me, me.scrollPos)));
+               me.scrollPosTarget = max(me.scrollPosTarget - 0.5, 0);
        }
        else if(key == K_MWHEELDOWN)
        {
-               me.scrollPos = min(me.scrollPos + 0.5, me.getTotalHeight(me) - 1);
-               me.setSelected(me, max(me.selectedItem, me.getFirstFullyVisibleItemAtScrollPos(me, me.scrollPos)));
+               me.scrollPosTarget = min(me.scrollPosTarget + 0.5, me.getTotalHeight(me) - 1);
        }
        else if(key == K_PGUP || key == K_KP_PGUP)
        {
+               if(me.selectionDoesntMatter)
+               {
+                       me.scrollPosTarget = max(me.scrollPosTarget - 0.5, 0);
+                       return 1;
+               }
+
                float i = me.selectedItem;
                float a = me.getItemHeight(me, i);
-               for(0;;)
+               for (;;)
                {
                        --i;
                        if (i < 0)
@@ -144,9 +188,15 @@ float ListBox_keyDown(entity me, float key, float ascii, float shift)
        }
        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(0;;)
+               for (;;)
                {
                        ++i;
                        if (i >= me.nItems)
@@ -158,85 +208,106 @@ float ListBox_keyDown(entity me, float key, float ascii, float shift)
                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.scrollPos = 0;
                me.setSelected(me, 0);
-       }
        else if(key == K_END || key == K_KP_END)
-       {
-               me.scrollPos = max(0, me.getTotalHeight(me) - 1);
                me.setSelected(me, me.nItems - 1);
-       }
        else
                return 0;
        return 1;
 }
+float ListBox_mouseMove(entity me, vector pos)
+{
+       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.focusedItem = -1;
+               me.mouseMoveOffset = -1;
+       }
+       return 1;
+}
 float ListBox_mouseDrag(entity me, vector pos)
 {
        float hit;
-       float i;
        me.updateControlTopBottom(me);
        me.dragScrollPos = pos;
        if(me.pressed == 1)
        {
                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(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)
                {
                        // 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;
+                       d = (pos.y - me.pressOffset) / (1 - (me.controlBottom - me.controlTop)) * (me.getTotalHeight(me) - 1);
+                       me.scrollPosTarget = me.previousValue + d;
                }
                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);
+                       me.scrollPosTarget = me.previousValue;
+               me.scrollPosTarget = min(me.scrollPosTarget, me.getTotalHeight(me) - 1);
+               me.scrollPosTarget = max(me.scrollPosTarget, 0);
        }
        else if(me.pressed == 2)
        {
-               me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos_y));
+               me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
+               me.focusedItem = me.selectedItem;
+               me.mouseMoveOffset = -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;
+       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)
+       if(pos.x >= 1 - me.controlWidth)
        {
                // if hit, set me.pressed, otherwise scroll by one page
-               if(pos_y < me.controlTop)
+               if(pos.y < me.controlTop)
                {
                        // page up
-                       me.scrollPos = max(me.scrollPos - 1, 0);
-                       me.setSelected(me, min(me.selectedItem, ListBox_getLastFullyVisibleItemAtScrollPos(me, me.scrollPos)));
+                       me.scrollPosTarget = max(me.scrollPosTarget - 1, 0);
                }
-               else if(pos_y > me.controlBottom)
+               else if(pos.y > me.controlBottom)
                {
                        // page down
-                       me.scrollPos = min(me.scrollPos + 1, me.getTotalHeight(me) - 1);
-                       me.setSelected(me, max(me.selectedItem, ListBox_getFirstFullyVisibleItemAtScrollPos(me, me.scrollPos)));
+                       me.scrollPosTarget = min(me.scrollPosTarget + 1, me.getTotalHeight(me) - 1);
                }
                else
                {
                        me.pressed = 1;
-                       me.pressOffset = pos_y;
+                       me.pressOffset = pos.y;
                        me.previousValue = me.scrollPos;
                }
        }
@@ -245,7 +316,8 @@ float ListBox_mousePress(entity me, vector pos)
                // 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));
+               me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
+               me.focusedItem = me.selectedItem;
        }
        return 1;
 }
@@ -261,7 +333,8 @@ 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));
+               me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
+               me.focusedItem = me.selectedItem;
                // and give it a nice click event
                if(me.nItems > 0)
                {
@@ -285,6 +358,8 @@ void ListBox_focusLeave(entity me)
        // by a mouse click on an item of the list
        // for example showing a dialog on right click
        me.pressed = 0;
+       me.focusedItem = -1;
+       me.mouseMoveOffset = -1;
 }
 void ListBox_updateControlTopBottom(entity me)
 {
@@ -299,20 +374,6 @@ void ListBox_updateControlTopBottom(entity me)
        }
        else
        {
-               if(frametime) // only do this in draw frames
-               {
-                       if(me.dragScrollTimer < time)
-                       {
-                               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;
-                       }
-               }
                // 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
@@ -322,7 +383,7 @@ void ListBox_updateControlTopBottom(entity me)
                me.controlBottom = min((me.scrollPos + 1) / me.getTotalHeight(me), 1);
 
                float minfactor;
-               minfactor = 2 * 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?
                {
@@ -341,10 +402,37 @@ void ListBox_draw(entity me)
        float i;
        vector absSize, fillSize = '0 0 0';
        vector oldshift, oldscale;
+
+       // we can't do this in mouseMove as the list can scroll without moving the cursor
+       float focusedItem_save = me.focusedItem;
+       if(me.mouseMoveOffset != -1)
+               me.focusedItem = me.getItemAtPos(me, me.scrollPos + me.mouseMoveOffset);
+       if(me.focusedItem >= 0)
+       if(focusedItem_save != me.focusedItem)
+               me.focusedItemAlpha = SKINALPHA_LISTBOX_FOCUSED;
+
+       if(me.needScrollToItem >= 0)
+       {
+               me.scrollToItem(me, me.needScrollToItem);
+               me.needScrollToItem = -1;
+       }
+       if(me.scrollPos != me.scrollPosTarget)
+       {
+               float PI = 3.1415926535897932384626433832795028841971693993751058209749445923;
+               float exp_factor = 0.65;
+               if(me.pressed == 1)
+                       exp_factor = 0.45; // scroll faster while dragging the scrollbar
+               // this formula works with whatever framerate
+               float f = sin(PI / 2 * pow(frametime, exp_factor));
+               me.scrollPos = me.scrollPos * (1 - f) + me.scrollPosTarget * f;
+               if(fabs(me.scrollPos - me.scrollPosTarget) < 0.001)
+                       me.scrollPos = me.scrollPosTarget;
+       }
+
        if(me.pressed == 2)
                me.mouseDrag(me, me.dragScrollPos); // simulate mouseDrag event
        me.updateControlTopBottom(me);
-       fillSize_x = (1 - me.controlWidth);
+       fillSize.x = (1 - me.controlWidth);
        if(me.alphaBG)
                draw_Fill('0 0 0', '0 1 0' + fillSize, me.colorBG, me.alphaBG);
        if(me.controlWidth)
@@ -366,17 +454,18 @@ void ListBox_draw(entity me)
        draw_SetClip();
        oldshift = draw_shift;
        oldscale = draw_scale;
+
        float y;
        i = me.getItemAtPos(me, me.scrollPos);
        y = me.getItemStart(me, i) - me.scrollPos;
-       for(0; i < me.nItems && y < 1; ++i)
+       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));
-               y += relSize_y;
+               me.drawListBoxItem(me, i, absSize, (me.selectedItem == i), (me.focusedItem == i));
+               y += relSize.y;
        }
        draw_ClearClip();
 
@@ -395,8 +484,8 @@ void ListBox_doubleClickListBoxItem(entity me, float i, vector where)
        // template method
 }
 
-void ListBox_drawListBoxItem(entity me, float i, vector absSize, float selected)
+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), (selected ? '0 1 0' : '1 1 1'), 1, 0);
+       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