Implement sortable columns for the gui, and use them in the lobby. Patch by Vladislav. Fixes #2405.

This was SVN commit r16781.
This commit is contained in:
sanderd17 2015-06-17 09:10:50 +00:00
parent 2ff4c60859
commit b8fce56821
10 changed files with 262 additions and 50 deletions

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:230a6850ce851613b5397b6522135c1ce201d6b515a9ca485a3047e6423cb7e6
size 292

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:17ab3802fbf0947f2dabbecd68bdb36d59535f51b61907f663c0bd7e0607a953
size 351

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3ecb1a3a4a10be5f0015be13c54f863fb6122ce5f08dcd00f5fa2d06cb4bb29f
size 284

View file

@ -585,6 +585,27 @@
size="0 6 16 22"
/>
</sprite>
<sprite name = "ModernNotSorted">
<image texture = "global/modern/arrow-up-down.png"
real_texture_placement = "0 0 16 16"
texture_size="0 0 16 16"
size="0 6 16 22"
/>
</sprite>
<sprite name = "ModernArrowUp">
<image texture = "global/modern/arrow-up.png"
real_texture_placement = "0 0 16 16"
texture_size="0 0 16 16"
size="0 6 16 22"
/>
</sprite>
<sprite name = "ModernArrowDown">
<image texture = "global/modern/arrow-down.png"
real_texture_placement = "0 0 16 16"
texture_size="0 0 16 16"
size="0 6 16 22"
/>
</sprite>
<sprite name = "ModernDropDownArrowHighlight">
<image texture = "global/modern/dropdown-arrow.png"
real_texture_placement = "0 0 16 16"

View file

@ -52,6 +52,7 @@ ex_settings =
attribute button_width { xsd:decimal }?&
attribute checked { bool }?&
attribute clip { bool }?&
attribute default_column { text }?&
attribute dropdown_size { xsd:decimal }?&
attribute dropdown_buffer { xsd:decimal }?&
attribute enabled { bool }?&
@ -70,6 +71,7 @@ ex_settings =
attribute scrollbar { bool }?&
attribute scrollbar_style { text }?&
attribute scroll_bottom { bool }?&
attribute sortable { bool }?&
attribute sound_closed { text }?&
attribute sound_disabled { text }?&
attribute sound_enter { text }?&
@ -79,12 +81,15 @@ ex_settings =
attribute sound_selected { text }?&
attribute sprite { text }?&
attribute sprite2 { text }?&
attribute sprite_asc { text }?&
attribute sprite_heading { text }?&
attribute sprite_bar { text }?&
attribute sprite_background { text }?&
attribute sprite_desc { text }?&
attribute sprite_disabled { text }?&
attribute sprite_list { text }?&
attribute sprite2_disabled { text }?&
attribute sprite_not_sorted { text }?&
attribute sprite_over { text }?&
attribute sprite2_over { text }?&
attribute sprite_pressed { text }?&

View file

@ -199,6 +199,9 @@
<ref name="bool"/>
</attribute>
</optional>
<optional>
<attribute name="default_column"/>
</optional>
<optional>
<attribute name="dropdown_size">
<data type="decimal"/>
@ -286,6 +289,9 @@
<ref name="bool"/>
</attribute>
</optional>
<optional>
<attribute name="sortable"/>
</optional>
<optional>
<attribute name="sound_closed"/>
</optional>
@ -346,6 +352,15 @@
<optional>
<attribute name="sprite_selectarea"/>
</optional>
<optional>
<attribute name="sprite_asc"/>
</optional>
<optional>
<attribute name="sprite_desc"/>
</optional>
<optional>
<attribute name="sprite_not_sorted"/>
</optional>
<optional>
<attribute name="square_side">
<data type="decimal"/>

View file

@ -1,6 +1,10 @@
var g_ChatMessages = [];
var g_Name = "unknown";
var g_GameList = {};
var g_GameList = {}
var g_GameListSortBy = "name";
var g_PlayerListSortBy = "name";
var g_GameListOrder = 1; // 1 for ascending sort, and -1 for descending
var g_PlayerListOrder = 1;
var g_specialKey = Math.random();
// This object looks like {"name":[numMessagesSinceReset, lastReset, timeBlocked]} when in use.
var g_spamMonitor = {};
@ -71,25 +75,37 @@ function lobbyDisconnect()
}
////////////////////////////////////////////////////////////////////////////////////////////////
// Update functions
////////////////////////////////////////////////////////////////////////////////////////////////
function resetFilters()
{
// Reset states of gui objects
// Update functions
////////////////////////////////////////////////////////////////////////////////////////////////
function updateGameListOrderSelection()
{
g_GameListSortBy = Engine.GetGUIObjectByName("gamesBox").selected_column;
g_GameListOrder = Engine.GetGUIObjectByName("gamesBox").selected_column_order;
applyFilters();
}
function updatePlayerListOrderSelection()
{
g_PlayerListSortBy = Engine.GetGUIObjectByName("playersBox").selected_column;
g_PlayerListOrder = Engine.GetGUIObjectByName("playersBox").selected_column_order;
updatePlayerList();
}
function resetFilters()
{
// Reset states of gui objects
Engine.GetGUIObjectByName("mapSizeFilter").selected = 0
Engine.GetGUIObjectByName("playersNumberFilter").selected = 0;
Engine.GetGUIObjectByName("mapTypeFilter").selected = 0;
Engine.GetGUIObjectByName("showFullFilter").checked = false;
// Update the list of games
updateGameList();
// Update info box about the game currently selected
updateGameSelection();
}
function applyFilters()
Engine.GetGUIObjectByName("mapTypeFilter").selected = 0;
Engine.GetGUIObjectByName("showFullFilter").checked = false;
applyFilters();
}
function applyFilters()
{
// Update the list of games
updateGameList();
@ -146,13 +162,13 @@ function updateSubject(newSubject)
subjectBox.hidden = false;
logo.size = "50%-110 40 50%+110 140";
}
}
/**
* Do a full update of the player listing, including ratings from C++.
*
* @return Array containing the player, presence, nickname, and rating listings.
*/
}
/**
* Do a full update of the player listing, including ratings from cached C++ information.
*
* @return Array containing the player, presence, nickname, and rating listings.
*/
function updatePlayerList()
{
var playersBox = Engine.GetGUIObjectByName("playersBox");
@ -162,12 +178,36 @@ function updatePlayerList()
var ratingList = [];
var cleanPlayerList = Engine.GetPlayerList();
// Sort the player list, ignoring case.
cleanPlayerList.sort(function(a,b)
cleanPlayerList.sort(function(a,b)
{
var aName = a.name.toLowerCase();
var bName = b.name.toLowerCase();
return ((aName > bName) ? 1 : (bName > aName) ? -1 : 0);
} );
switch (g_PlayerListSortBy)
{
case 'rating':
if (a.rating < b.rating)
return -g_PlayerListOrder;
else if (a.rating > b.rating)
return g_PlayerListOrder;
return 0;
case 'status':
let order = ["available", "away", "playing", "gone", "offline"];
let presenceA = order.indexOf(a.presence);
let presenceB = order.indexOf(b.presence);
if (presenceA < presenceB)
return -g_PlayerListOrder;
else if (presenceA > presenceB)
return g_PlayerListOrder;
return 0;
case 'name':
default:
var aName = a.name.toLowerCase();
var bName = b.name.toLowerCase();
if (aName < bName)
return -g_PlayerListOrder;
else if (aName > bName)
return g_PlayerListOrder;
return 0;
}
});
for (var i = 0; i < cleanPlayerList.length; i++)
{
// Identify current user's rating.
@ -362,20 +402,47 @@ function updateGameList()
// to update the game info panel.
g_GameList = gameList;
// Sort the list of games to that games 'waiting' are displayed at the top, followed by 'init', followed by 'running'.
// Sort the list of games to that games 'waiting' are displayed at the top, followed by 'init', followed by 'running'.
var gameStatuses = ['waiting', 'init', 'running'];
g_GameList.sort(function (a,b) {
if (gameStatuses.indexOf(a.state) < gameStatuses.indexOf(b.state))
return -1;
else if (gameStatuses.indexOf(a.state) > gameStatuses.indexOf(b.state))
return 1;
switch (g_GameListSortBy)
{
case 'name':
case 'mapSize':
// mapSize contains the number of tiles for random maps
// scenario maps always display default size
case 'mapType':
if (a[g_GameListSortBy] < b[g_GameListSortBy])
return -g_GameListOrder;
else if (a[g_GameListSortBy] > b[g_GameListSortBy])
return g_GameListOrder;
return 0;
case 'mapName':
if (translate(a.niceMapName) < translate(b.niceMapName))
return -g_GameListOrder;
else if (translate(a.niceMapName) > translate(b.niceMapName))
return g_GameListOrder;
return 0;
case 'nPlayers':
// Numerical comparison of player count ratio.
if (a.nbp * b.tnbp < b.nbp * a.tnbp) // ratio a = a.nbp / a.tnbp, ratio b = b.nbp / b.tnbp
return -g_GameListOrder;
else if (a.nbp * b.tnbp > b.nbp * a.tnbp)
return g_GameListOrder;
return 0;
default:
if (gameStatuses.indexOf(a.state) < gameStatuses.indexOf(b.state))
return -1;
else if (gameStatuses.indexOf(a.state) > gameStatuses.indexOf(b.state))
return 1;
// Alphabetical comparison of names as tiebreaker.
if (a.name < b.name)
return -1;
else if (a.name > b.name)
return 1;
return 0;
// Alphabetical comparison of names as tiebreaker.
if (a.name < b.name)
return -1;
else if (a.name > b.name)
return 1;
return 0;
}
});
var list_name = [];

View file

@ -20,19 +20,22 @@
<!-- Left panel: Player list. -->
<object name="leftPanel" size="20 30 20% 100%-280">
<object name="playersBox" style="ModernList" type="olist" size="0 0 100% 100%" font="sans-bold-stroke-13">
<object name="playersBox" style="ModernList" sprite_asc="ModernArrowDown" default_column="name" sprite_desc="ModernArrowUp" sprite_not_sorted="ModernNotSorted" type="olist" sortable="true" size="0 0 100% 100%" font="sans-bold-stroke-13">
<def id="status" width="26%">
<translatableAttribute id="heading">Status</translatableAttribute>
</def>
<def id="name" width="50%">
<def id="name" width="48%">
<translatableAttribute id="heading">Name</translatableAttribute>
</def>
<def id="rating" width="24%">
<def id="rating" width="26%">
<translatableAttribute id="heading">Rating</translatableAttribute>
</def>
<action on="SelectionChange">
displayProfile("lobbylist");
</action>
<action on="SelectionColumnChange">
updatePlayerListOrderSelection();
</action>
</object>
</object>
@ -169,8 +172,9 @@
<!-- Middle panel: Filters, game list, chat box. -->
<object name="middlePanel" size="20%+5 5% 100%-255 97.2%">
<object name="gamesBox" style="ModernList" type="olist" size="0 25 100% 48%" font="sans-stroke-13">
<object name="gamesBox" style="ModernList" sprite_asc="ModernArrowDown" default_column="name" sprite_desc="ModernArrowUp" sprite_not_sorted="ModernNotSorted" type="olist" sortable="true" size="0 25 100% 48%" font="sans-stroke-13">
<action on="SelectionChange">updateGameSelection();</action>
<action on="SelectionColumnChange">updateGameListOrderSelection();</action>
<def id="name" color="0 60 0" width="27%">
<translatableAttribute id="heading">Name</translatableAttribute>
</def>

View file

@ -19,10 +19,24 @@
#include "i18n/L10n.h"
#include "ps/CLogger.h"
#include "soundmanager/ISoundManager.h"
COList::COList() : CList(),m_HeadingHeight(30.f)
COList::COList() : CList(),m_HeadingHeight(30.f),m_SelectedDef(-1),m_SelectedColumnOrder(1)
{
AddSetting(GUIST_CGUISpriteInstance, "sprite_heading");
AddSetting(GUIST_bool, "sortable"); // The actual sorting is done in JS for more versatility
AddSetting(GUIST_CStr, "selected_column");
AddSetting(GUIST_int, "selected_column_order");
AddSetting(GUIST_CStr, "default_column");
AddSetting(GUIST_int, "selected_def");
AddSetting(GUIST_CGUISpriteInstance, "sprite_asc"); // Show the order of sorting
AddSetting(GUIST_CGUISpriteInstance, "sprite_desc");
AddSetting(GUIST_CGUISpriteInstance, "sprite_not_sorted");
// Nothing is selected by default.
GUI<CStr>::SetSetting(this, "selected_column", "");
GUI<int>::SetSetting(this, "selected_column_order", 1);
GUI<int>::SetSetting(this, "selected_def", -1);
}
void COList::SetupText()
@ -66,6 +80,9 @@ void COList::SetupText()
float buffer_zone=0.f;
GUI<float>::GetSetting(this, "buffer_zone", buffer_zone);
CStr defaultColumn;
GUI<CStr>::GetSetting(this, "default_column", defaultColumn);
defaultColumn = "list_" + defaultColumn;
for (unsigned int c=0; c<m_ObjectsDefs.size(); ++c)
{
@ -74,6 +91,9 @@ void COList::SetupText()
gui_string.SetValue(m_ObjectsDefs[c].m_Heading);
*text = GetGUI()->GenerateText(gui_string, font, width, buffer_zone, this);
AddText(text);
if (m_SelectedDef == -1 && defaultColumn == m_ObjectsDefs[c].m_Id)
m_SelectedDef = c;
}
@ -119,6 +139,58 @@ CRect COList::GetListRect() const
void COList::HandleMessage(SGUIMessage &Message)
{
CList::HandleMessage(Message);
switch (Message.type)
{
// If somebody clicks on the column heading
case GUIM_MOUSE_PRESS_LEFT:
{
bool sortable;
GUI<bool>::GetSetting(this, "sortable", sortable);
if (!sortable)
return;
CPos mouse = GetMousePos();
if (!m_CachedActualSize.PointInside(mouse))
return;
float xpos = 0;
for (unsigned int def = 0; def < m_ObjectsDefs.size(); ++def)
{
float width = m_ObjectsDefs[def].m_Width;
// Check if it's a decimal value, and if so, assume relative positioning.
if (m_ObjectsDefs[def].m_Width < 1 && m_ObjectsDefs[def].m_Width > 0)
width *= m_TotalAvalibleColumnWidth;
CPos leftTopCorner = m_CachedActualSize.TopLeft() + CPos(xpos, 4);
if (mouse.x >= leftTopCorner.x &&
mouse.x < leftTopCorner.x + width &&
mouse.y < leftTopCorner.y + m_HeadingHeight)
{
if (static_cast<int> (def) != m_SelectedDef)
{
m_SelectedColumnOrder = 1;
m_SelectedDef = def;
}
else
m_SelectedColumnOrder = -m_SelectedColumnOrder;
GUI<CStr>::SetSetting(this, "selected_column", m_ObjectsDefs[def].m_Id.substr(5));
GUI<int>::SetSetting(this, "selected_column_order", m_SelectedColumnOrder);
GUI<int>::SetSetting(this, "selected_def", def);
ScriptEvent("selectioncolumnchange");
CStrW soundPath;
if (g_SoundManager && GUI<CStrW>::GetSetting(this, "sound_selected", soundPath) == PSRETURN_OK && !soundPath.empty())
g_SoundManager->PlayAsUI(soundPath.c_str(), false);
return;
}
xpos += width;
}
return;
}
default:
return;
}
}
bool COList::HandleAdditionalChildren(const XMBElement& child, CXeromyces* pFile)
@ -307,16 +379,33 @@ void COList::DrawList(const int &selected,
CRect rect_head(m_CachedActualSize.left, m_CachedActualSize.top, m_CachedActualSize.right,
m_CachedActualSize.top + m_HeadingHeight);
GetGUI()->DrawSprite(*sprite_heading, cell_id, bz, rect_head);
CGUISpriteInstance *sprite_order, *sprite_not_sorted;
if (m_SelectedColumnOrder != -1)
GUI<CGUISpriteInstance>::GetSettingPointer(this, "sprite_asc", sprite_order);
else
GUI<CGUISpriteInstance>::GetSettingPointer(this, "sprite_desc", sprite_order);
GUI<CGUISpriteInstance>::GetSettingPointer(this, "sprite_not_sorted", sprite_not_sorted);
float xpos = 0;
for (unsigned int def=0; def< m_ObjectsDefs.size(); ++def)
{
DrawText(def, color, m_CachedActualSize.TopLeft() + CPos(xpos, 4), bz+0.1f, rect_head);
// Check if it's a decimal value, and if so, assume relative positioning.
float width = m_ObjectsDefs[def].m_Width;
if (m_ObjectsDefs[def].m_Width < 1 && m_ObjectsDefs[def].m_Width > 0)
xpos += m_ObjectsDefs[def].m_Width * m_TotalAvalibleColumnWidth;
width *= m_TotalAvalibleColumnWidth;
CPos leftTopCorner = m_CachedActualSize.TopLeft() + CPos(xpos, 0);
CGUISpriteInstance *sprite;
// If the list sorted by current column
if (m_SelectedDef == static_cast<int> (def))
sprite = sprite_order;
else
xpos += m_ObjectsDefs[def].m_Width;
sprite = sprite_not_sorted;
GetGUI()->DrawSprite(*sprite, cell_id, bz + 0.1f, CRect(leftTopCorner + CPos(width - 16, 0), leftTopCorner + CPos(width, 16)));
DrawText(def, color, leftTopCorner + CPos(0, 4), bz + 0.1f, rect_head);
xpos += width;
}
for (int i=0; i<(int)pList->m_Items.size(); ++i)

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2014 Wildfire Games.
/* Copyright (C) 2015 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -70,6 +70,8 @@ protected:
virtual CRect GetListRect() const;
std::vector<ObjectDef> m_ObjectsDefs;
int m_SelectedDef;
int m_SelectedColumnOrder;
private:
float m_HeadingHeight;