Index: binaries/data/mods/mod/gui/gui.rnc =================================================================== --- binaries/data/mods/mod/gui/gui.rnc +++ binaries/data/mods/mod/gui/gui.rnc @@ -70,6 +70,8 @@ attribute max_length { xsd:nonNegativeInteger }?& attribute maxwidth { xsd:decimal }? & attribute multiline { bool }?& + attribute multiSelected { bool }?& + attribute multiSelection_enabled { bool }?& attribute offset { pos }?& attribute readonly { bool }?& attribute scrollbar { bool }?& Index: binaries/data/mods/mod/gui/gui.rng =================================================================== --- binaries/data/mods/mod/gui/gui.rng +++ binaries/data/mods/mod/gui/gui.rng @@ -297,6 +297,16 @@ + + + + + + + + + + Index: binaries/data/mods/mod/gui/modio/modio.js =================================================================== --- binaries/data/mods/mod/gui/modio/modio.js +++ binaries/data/mods/mod/gui/modio/modio.js @@ -158,7 +158,9 @@ function displayMods() { let modsAvailableList = Engine.GetGUIObjectByName("modsAvailableList"); - let selectedMod = modsAvailableList.list[modsAvailableList.selected]; + let index = modsAvailableList.multiSelected.length ? modsAvailableList.multiSelected[0] : -1; + let selectedMod = modsAvailableList.list[index]; + modsAvailableList.multiSelected = []; modsAvailableList.selected = -1; let displayedMods = clone(g_ModsAvailableOnline); @@ -187,6 +189,7 @@ function clearModList() { let modsAvailableList = Engine.GetGUIObjectByName("modsAvailableList"); + modsAvailableList.multiSelected = []; modsAvailableList.selected = -1; for (let listIdx of Object.keys(modsAvailableList).filter(key => key.startsWith("list"))) modsAvailableList[listIdx] = []; @@ -196,10 +199,11 @@ { let modsAvailableList = Engine.GetGUIObjectByName("modsAvailableList"); - if (modsAvailableList.selected == -1) + if (!modsAvailableList.multiSelected.length) return undefined; - return +modsAvailableList.list[modsAvailableList.selected]; + let index = modsAvailableList.multiSelected.length ? modsAvailableList.multiSelected[0] : -1; + return +modsAvailableList.list[index]; } function showModDescription() Index: binaries/data/mods/mod/gui/modio/modio.xml =================================================================== --- binaries/data/mods/mod/gui/modio/modio.xml +++ binaries/data/mods/mod/gui/modio/modio.xml @@ -41,6 +41,7 @@ sortable="true" selected_column="name" selected_column_order="1" + multiSelection_enabled="false" font="sans-stroke-13" > showModDescription(); Index: binaries/data/mods/mod/gui/modmod/modmod.js =================================================================== --- binaries/data/mods/mod/gui/modmod/modmod.js +++ binaries/data/mods/mod/gui/modmod/modmod.js @@ -158,7 +158,7 @@ function enableMod() { let modsDisabledList = Engine.GetGUIObjectByName("modsDisabledList"); - let pos = modsDisabledList.selected; + let pos = modsDisabledList.multiSelected.length ? modsDisabledList.multiSelected[0] : -1; if (pos == -1 || !areDependenciesMet(g_ModsDisabled[pos])) return; @@ -168,6 +168,10 @@ if (pos >= g_ModsDisabled.length) --pos; + if (pos == -1) + modsDisabledList.multiSelected = []; + else + modsDisabledList.multiSelected = [pos]; modsDisabledList.selected = pos; displayModLists(); @@ -176,7 +180,7 @@ function disableMod() { let modsEnabledList = Engine.GetGUIObjectByName("modsEnabledList"); - let pos = modsEnabledList.selected; + let pos = modsEnabledList.multiSelected.length ? modsEnabledList.multiSelected[0] : -1; if (pos == -1) return; @@ -195,7 +199,12 @@ --i; } - modsEnabledList.selected = Math.min(pos, g_ModsEnabled.length - 1); + let newPos = Math.min(pos, g_ModsEnabled.length - 1); + if (newPos == -1) + modsEnabledList.multiSelected = []; + else + modsEnabledList.multiSelected = [newPos]; + modsEnabledList.selected = newPos; displayModLists(); } @@ -206,18 +215,34 @@ let modsDisabledList = Engine.GetGUIObjectByName("modsDisabledList"); let modsEnabledList = Engine.GetGUIObjectByName("modsEnabledList"); - let selectedDisabledFolder = modsDisabledList.list_folder[modsDisabledList.selected]; - let selectedEnabledFolder = modsEnabledList.list_folder[modsEnabledList.selected]; + let selectedDisabledFolder = modsDisabledList.list_folder[modsDisabledList.multiSelected.length ? modsDisabledList.multiSelected[0] : -1]; + let selectedEnabledFolder = modsEnabledList.list_folder[modsEnabledList.multiSelected.length ? modsEnabledList.multiSelected[0] : -1]; // Remove selected rows to prevent a link to a non existing item + modsDisabledList.multiSelected = []; modsDisabledList.selected = -1; + + modsEnabledList.multiSelected = []; modsEnabledList.selected = -1; displayModLists(); // Restore previously selected rows - modsDisabledList.selected = modsDisabledList.list_folder.indexOf(selectedDisabledFolder); - modsEnabledList.selected = modsEnabledList.list_folder.indexOf(selectedEnabledFolder); + let disPos = modsDisabledList.list_folder.indexOf(selectedDisabledFolder); + let enPos = modsEnabledList.list_folder.indexOf(selectedEnabledFolder); + + if (enPos == -1) + modsEnabledList.multiSelected = []; + else + modsEnabledList.multiSelected = [enPos]; + modsEnabledList.selected = enPos; + + if (disPos == -1) + modsDisabledList.multiSelected = []; + else + modsDisabledList.multiSelected = [disPos]; + + modsDisabledList.selected = disPos; Engine.GetGUIObjectByName("globalModDescription").caption = ""; } @@ -254,7 +279,7 @@ function moveCurrItem(objectName, up) { let obj = Engine.GetGUIObjectByName(objectName); - let idx = obj.selected; + let idx = obj.multiSelected.length ? obj.multiSelected[0] : -1; if (idx == -1) return; @@ -268,6 +293,7 @@ g_ModsEnabled[idx2] = tmp; obj.list = g_ModsEnabled; + obj.multiSelected = [idx2]; obj.selected = idx2; displayModList("modsEnabledList", g_ModsEnabled); @@ -362,8 +388,9 @@ let otherListObject = Engine.GetGUIObjectByName(listObjectName == "modsDisabledList" ? "modsEnabledList" : "modsDisabledList"); - if (listObject.selected != -1) + if (listObject.multiSelected.length) { + otherListObject.multiSelected = []; otherListObject.selected = -1; Engine.GetGUIObjectByName("visitWebButton").enabled = true; let toggleModButton = Engine.GetGUIObjectByName("toggleModButton"); @@ -374,9 +401,10 @@ Engine.GetGUIObjectByName("enabledModDown").enabled = listObjectName == "modsEnabledList"; } + let index = listObject.multiSelected.length ? listObject.multiSelected[0] : -1; Engine.GetGUIObjectByName("globalModDescription").caption = - listObject.list[listObject.selected] ? - g_Mods[listObject.list[listObject.selected]].description : + listObject.list[index] ? + g_Mods[listObject.list[index]].description : '[color="' + g_ColorNoModSelected + '"]' + translate("No mod has been selected.") + '[/color]'; } @@ -385,8 +413,9 @@ let modsEnabledList = Engine.GetGUIObjectByName("modsEnabledList"); let modsDisabledList = Engine.GetGUIObjectByName("modsDisabledList"); - let list = modsEnabledList.selected == -1 ? modsDisabledList : modsEnabledList; - let folder = list.list_folder[list.selected]; + let list = !modsEnabledList.multiSelected.length ? modsDisabledList : modsEnabledList; + let index = list.multiSelected.length ? list.multiSelected[0] : -1; + let folder = list.list_folder[index]; let url = folder && g_Mods[folder] && g_Mods[folder].url; if (!url) Index: binaries/data/mods/mod/gui/modmod/modmod.xml =================================================================== --- binaries/data/mods/mod/gui/modmod/modmod.xml +++ binaries/data/mods/mod/gui/modmod/modmod.xml @@ -62,6 +62,7 @@ sortable="true" selected_column="name" selected_column_order="1" + multiSelection_enabled="false" size="0 25 100%-2 100%" font="sans-stroke-13" auto_scroll="true" @@ -103,6 +104,7 @@ { reallyDeleteGame(toDelete); }]); } function reallyDeleteGame(gameID) { - if (!Engine.DeleteSavedGame(gameID)) - error("Could not delete saved game: " + gameID); + for (let gameID of gameIDs) + if (!Engine.DeleteSavedGame(gameID)) + error("Could not delete saved game: " + gameID); updateSavegameList(); } Index: binaries/data/mods/public/gui/loadgame/load.js =================================================================== --- binaries/data/mods/public/gui/loadgame/load.js +++ binaries/data/mods/public/gui/loadgame/load.js @@ -21,8 +21,10 @@ updateSavegameList(); - // Select the most recent savegame to be loaded, or no savegame to be overwritten let gameSelection = Engine.GetGUIObjectByName("gameSelection"); + gameSelection.multiSelected = []; + + // Select the most recent savegame to be loaded, or no savegame to be overwritten if (!save && gameSelection.list.length) gameSelection.selected = 0; else @@ -121,10 +123,17 @@ function selectionChanged() { - let metadata = g_SavedGamesMetadata[Engine.GetGUIObjectByName("gameSelection").selected]; + let gameSelection = Engine.GetGUIObjectByName("gameSelection"); + let index = gameSelection.multiSelected.length ? gameSelection.multiSelected[0] : -1; + let metadata = g_SavedGamesMetadata[index]; + Engine.GetGUIObjectByName("invalidGame").hidden = !!metadata; Engine.GetGUIObjectByName("validGame").hidden = !metadata; - Engine.GetGUIObjectByName("confirmButton").enabled = !!metadata || Engine.IsGameStarted(); + + Engine.GetGUIObjectByName("confirmButton").enabled = + (!!metadata || Engine.IsGameStarted()) && + gameSelection.multiSelected.length == 1;; + Engine.GetGUIObjectByName("deleteGameButton").enabled = !!metadata; if (!metadata) @@ -153,8 +162,8 @@ function loadGame() { let gameSelection = Engine.GetGUIObjectByName("gameSelection"); - let gameId = gameSelection.list_data[gameSelection.selected]; - let metadata = g_SavedGamesMetadata[gameSelection.selected]; + let gameId = gameSelection.list_data[gameSelection.multiSelected[0]]; + let metadata = g_SavedGamesMetadata[gameSelection.multiSelected[0]]; // Check compatibility before really loading it let engineInfo = Engine.GetEngineInfo(); Index: binaries/data/mods/public/gui/loadgame/load.xml =================================================================== --- binaries/data/mods/public/gui/loadgame/load.xml +++ binaries/data/mods/public/gui/loadgame/load.xml @@ -21,6 +21,7 @@ size="24 4 100%-24 100%-70" type="olist" auto_scroll="true" + multiSelection_enabled="true" > Date / Time Index: binaries/data/mods/public/gui/lobby/lobby.js =================================================================== --- binaries/data/mods/public/gui/lobby/lobby.js +++ binaries/data/mods/public/gui/lobby/lobby.js @@ -658,7 +658,8 @@ function updateToggleBuddy() { let playerList = Engine.GetGUIObjectByName("playersBox"); - let playerName = playerList.list[playerList.selected]; + let index = playerList.multiSelected.length ? playerList.multiSelected[0] : -1; + let playerName = playerList.list[index]; let toggleBuddyButton = Engine.GetGUIObjectByName("toggleBuddyButton"); toggleBuddyButton.caption = g_Buddies.indexOf(playerName) != -1 ? translate("Unmark as Buddy") : translate("Mark as Buddy"); @@ -739,7 +740,12 @@ playersBox.list_rating = ratingList; playersBox.list = nickList; - playersBox.selected = playersBox.list.indexOf(g_SelectedPlayer); + let index = playersBox.list.indexOf(g_SelectedPlayer); + playersBox.selected = index; + if (index > -1) + playersBox.multiSelected = [index]; + else + playersBox.multiSelected = []; } /** @@ -748,17 +754,16 @@ function toggleBuddy() { let playerList = Engine.GetGUIObjectByName("playersBox"); - let name = playerList.list[playerList.selected]; + let selected = playerList.multiSelected[0]; + let name = playerList.list[selected]; if (!name || name == g_Username || name.indexOf(g_BuddyListDelimiter) != -1) return; - let index = g_Buddies.indexOf(name); if (index != -1) g_Buddies.splice(index, 1); else g_Buddies.push(name); - updateToggleBuddy(); Engine.ConfigDB_CreateAndWriteValueToFile("user", "lobby.buddies", g_Buddies.filter(nick => nick).join(g_BuddyListDelimiter) || g_BuddyListDelimiter, "config/user.cfg"); @@ -790,24 +795,30 @@ { foundAsObserver = true; gameList.selected = i; + gamelist.multiSelected = [i]; } else if (!player.Offline) { gameList.selected = i; + gamelist.multiSelected = [i]; return; } - else if (!foundAsObserver) + else if (!foundAsObserver) { gameList.selected = i; + gamelist.multiSelected = [i]; + } } } function onPlayerListSelection() { let playerList = Engine.GetGUIObjectByName("playersBox"); - if (playerList.selected == playerList.list.indexOf(g_SelectedPlayer)) + if (!playerList.multiSelected.length && playerList.list.indexOf(g_SelectedPlayer) == -1) + return; + if (playerList.multiSelected[0] == playerList.list.indexOf(g_SelectedPlayer)) return; - g_SelectedPlayer = playerList.list[playerList.selected]; + g_SelectedPlayer = playerList.list[playerList.multiSelected[0]]; lookupSelectedUserProfile("playersBox"); updateToggleBuddy(); selectGameFromPlayername(); @@ -963,8 +974,9 @@ let gamesBox = Engine.GetGUIObjectByName("gamesBox"); let sortBy = gamesBox.selected_column; let sortOrder = gamesBox.selected_column_order; + let selected = gamesBox.multiSelected.length ? gamesBox.multiSelected[0] : -1; - if (gamesBox.selected > -1) + if (selected > -1) { g_SelectedGameIP = g_GameList[gamesBox.selected].ip; g_SelectedGamePort = g_GameList[gamesBox.selected].port; @@ -1084,6 +1096,10 @@ gamesBox.auto_scroll = false; gamesBox.selected = selectedGameIndex; + if (selectedGameIndex > -1) + gamesBox.multiSelected = [selectedGameIndex]; + else + gamesBox.multiSelected = []; updateGameSelection(); } @@ -1141,10 +1157,10 @@ function selectedGame() { let gamesBox = Engine.GetGUIObjectByName("gamesBox"); - if (gamesBox.selected < 0) + if (!gamesBox.multiSelected.length) return undefined; - return g_GameList[gamesBox.list_data[gamesBox.selected]]; + return g_GameList[gamesBox.list_data[gamesBox.multiSelected[0]]]; } /** Index: binaries/data/mods/public/gui/lobby/lobby_panels.xml =================================================================== --- binaries/data/mods/public/gui/lobby/lobby_panels.xml +++ binaries/data/mods/public/gui/lobby/lobby_panels.xml @@ -15,6 +15,7 @@ selected_column="name" selected_column_order="1" type="olist" + multiSelection_enabled="false" sortable="true" size="0 0 100% 100%" font="sans-bold-stroke-13" @@ -178,6 +179,7 @@ selected_column="name" selected_column_order="1" type="olist" + multiSelection_enabled="false" sortable="true" size="0 25 100% 48%" font="sans-stroke-13" @@ -281,6 +283,7 @@ Rank Index: binaries/data/mods/public/gui/replaymenu/replay_actions.js =================================================================== --- binaries/data/mods/public/gui/replaymenu/replay_actions.js +++ binaries/data/mods/public/gui/replaymenu/replay_actions.js @@ -155,19 +155,26 @@ function deleteReplay() { // Get selected replay - var selected = Engine.GetGUIObjectByName("replaySelection").selected; - if (selected == -1) + let replaySelection = Engine.GetGUIObjectByName("replaySelection"); + if (!replaySelection.multiSelected.length) return; - - var replay = g_ReplaysFiltered[selected]; - + let directories = []; + let directoryNames = []; + for (let selected of replaySelection.multiSelected) + { + directories.push(g_ReplaysFiltered[selected].directory); + directoryNames.push(Engine.GetReplayDirectoryName(g_ReplaysFiltered[selected].directory)); + } messageBox( 500, 200, - translate("Are you sure you want to delete this replay permanently?") + "\n" + - escapeText(Engine.GetReplayDirectoryName(replay.directory)), - translate("Delete replay"), + translatePlural("Are you sure you want to delete this replay permanently?", + "Are you sure you want to delete these replays permanently?", + directories.length + ) + "\n" + + escapeText(directoryNames.join("\n")), + translatePlural("Delete replay", "Delete replays", directories.length), [translate("No"), translate("Yes")], - [null, function() { reallyDeleteReplay(replay.directory); }] + [null, function() { reallyDeleteReplay(directories); }] ); } @@ -176,23 +183,27 @@ */ function deleteReplayWithoutConfirmation() { - var selected = Engine.GetGUIObjectByName("replaySelection").selected; - if (selected > -1) - reallyDeleteReplay(g_ReplaysFiltered[selected].directory); + let replaySelection = Engine.GetGUIObjectByName("replaySelection"); + if (!replaySelection.multiSelected.length) + return; + let directories = []; + for (selected of replaySelection.multiSelected) + directories.push(g_ReplaysFiltered[selected].directory); + reallyDeleteReplay(directories); } /** * Attempts to delete the given replay directory from the disk. * - * @param replayDirectory {string} + * @param replayDirectories {string[]} */ -function reallyDeleteReplay(replayDirectory) +function reallyDeleteReplay(replayDirectories) { - var replaySelection = Engine.GetGUIObjectByName("replaySelection"); - var selectedIndex = replaySelection.selected; + let replaySelection = Engine.GetGUIObjectByName("replaySelection"); - if (!Engine.DeleteReplay(replayDirectory)) - error("Could not delete replay!"); + for (let replayDirectory of replayDirectories) + if (!Engine.DeleteReplay(replayDirectory)) + error("Could not delete replay!"); // Refresh replay list init(); Index: binaries/data/mods/public/gui/replaymenu/replay_menu.js =================================================================== --- binaries/data/mods/public/gui/replaymenu/replay_menu.js +++ binaries/data/mods/public/gui/replaymenu/replay_menu.js @@ -77,6 +77,10 @@ if (data && data.summarySelectedData) g_SummarySelectedData = data.summarySelectedData; + + let replaySelection = Engine.GetGUIObjectByName("replaySelection"); + if(replaySelection) + replaySelection.multiSelected = []; } /** @@ -196,6 +200,20 @@ Engine.GetGUIObjectByName("deleteReplayButton").tooltip = deleteTooltip(); } +function deleteTooltip() +{ + let tooltip = colorizeHotkey( + translate("Delete the selected replay using %(hotkey)s."), + "session.savedgames.delete"); + + if (tooltip) + tooltip += colorizeHotkey( + "\n" + translate("Hold %(hotkey)s to delete without confirmation."), + "session.savedgames.noconfirmation"); + + return tooltip; +} + /** * Filter g_Replays, fill the GUI list with that data and show the description of the current replay. */ @@ -250,19 +268,19 @@ */ function displayReplayDetails() { - let selected = Engine.GetGUIObjectByName("replaySelection").selected; - let replaySelected = selected > -1; + let countOfReplays = Engine.GetGUIObjectByName("replaySelection").multiSelected.length; + let singleReplaySelected = countOfReplays == 1; - Engine.GetGUIObjectByName("replayInfo").hidden = !replaySelected; - Engine.GetGUIObjectByName("replayInfoEmpty").hidden = replaySelected; - Engine.GetGUIObjectByName("startReplayButton").enabled = replaySelected; - Engine.GetGUIObjectByName("deleteReplayButton").enabled = replaySelected; - Engine.GetGUIObjectByName("replayFilename").hidden = !replaySelected; + Engine.GetGUIObjectByName("replayInfo").hidden = !singleReplaySelected; + Engine.GetGUIObjectByName("replayInfoEmpty").hidden = singleReplaySelected; + Engine.GetGUIObjectByName("startReplayButton").enabled = singleReplaySelected; + Engine.GetGUIObjectByName("deleteReplayButton").enabled = countOfReplays > 0; + Engine.GetGUIObjectByName("replayFilename").hidden = !singleReplaySelected; Engine.GetGUIObjectByName("summaryButton").hidden = true; - if (!replaySelected) + if (!singleReplaySelected) return; - + let selected = Engine.GetGUIObjectByName("replaySelection").multiSelected[0]; let replay = g_ReplaysFiltered[selected]; Engine.GetGUIObjectByName("sgMapName").caption = translate(replay.attribs.settings.Name); Index: binaries/data/mods/public/gui/replaymenu/replay_menu.xml =================================================================== --- binaries/data/mods/public/gui/replaymenu/replay_menu.xml +++ binaries/data/mods/public/gui/replaymenu/replay_menu.xml @@ -58,6 +58,7 @@ selected_column_order="-1" font="sans-stroke-13" auto_scroll="true" + multiSelection_enabled="true" > displayReplayDetails(); Index: source/gui/CDropDown.h =================================================================== --- source/gui/CDropDown.h +++ source/gui/CDropDown.h @@ -34,7 +34,7 @@ #define INCLUDED_CDROPDOWN #include "GUI.h" -#include "CList.h" +#include "CListSingle.h" /** * Drop Down @@ -45,7 +45,7 @@ * immediately appear, and not first after release * (which is the whole gist of the IGUIButtonBehavior). */ -class CDropDown : public CList +class CDropDown : public CListSingle { GUI_OBJECT(CDropDown) Index: source/gui/CDropDown.cpp =================================================================== --- source/gui/CDropDown.cpp +++ source/gui/CDropDown.cpp @@ -27,7 +27,7 @@ #include "ps/Profile.h" CDropDown::CDropDown(CGUI& pGUI) - : CList(pGUI), IGUIObject(pGUI), + : CListSingle(pGUI), IGUIObject(pGUI), m_Open(false), m_HideScrollBar(false), m_ElementHighlight(-1) { AddSetting("button_width"); Index: source/gui/CIntList.h =================================================================== --- /dev/null +++ source/gui/CIntList.h @@ -0,0 +1,32 @@ +/* Copyright (C) 2019 Wildfire Games. +* This file is part of 0 A.D. +* +* 0 A.D. is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 2 of the License, or +* (at your option) any later version. +* +* 0 A.D. is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with 0 A.D. If not, see . +*/ + +#ifndef INCLUDED_CINTLIST +#define INCLUDED_CINTLIST + +#include + +class CIntList +{ +public: + /** + * List of items (as int) + */ + std::vector m_Items; +}; + +#endif Index: source/gui/CList.h =================================================================== --- source/gui/CList.h +++ source/gui/CList.h @@ -18,10 +18,10 @@ #ifndef INCLUDED_CLIST #define INCLUDED_CLIST -#include "IGUIScrollBar.h" +#include "gui/IGUIScrollBar.h" /** - * Create a list of elements, where one can be selected + * Create a list of elements, where multiple can be selected * by the user. The control will use a pre-processed * text-object for each element, which will be managed * by the IGUITextOwner structure. @@ -85,9 +85,18 @@ // Called every time the auto-scrolling should be checked. void UpdateAutoScroll(); + virtual bool IsEnabledDoubleClick() = 0; + + virtual int GetLastSelected() = 0; + + /** + * Draw selection on item + */ + virtual void DrawSelection(const int selected, const bool scrollbar, const float scroll, CGUISpriteInstance& sprite_selectarea, const int cell_id, const float bz, CRect& rect); + // Extended drawing interface, this is so that classes built on the this one // can use other sprite names. - virtual void DrawList(const int& selected, const CStr& _sprite, const CStr& _sprite_selected, const CStr& _textcolor); + virtual void DrawList(const int selected, const CStr& _sprite, const CStr& _sprite_selected, const CStr& _textcolor); // Get the area of the list. This is so that it can easily be changed, like in CDropDown // where the area is not equal to m_CachedActualSize. @@ -116,6 +125,12 @@ // Last time a click on an item was issued double m_LastItemClickTime; + + /** + * Control selection + */ + virtual void SelectionControl(int& select) = 0; + }; #endif // INCLUDED_CLIST Index: source/gui/CList.cpp =================================================================== --- source/gui/CList.cpp +++ source/gui/CList.cpp @@ -24,6 +24,7 @@ #include "gui/CGUIScrollBarVertical.h" #include "lib/external_libraries/libsdl.h" #include "lib/timer.h" +#include "ps/Globals.h" CList::CList(CGUI& pGUI) : IGUIObject(pGUI), IGUITextOwner(pGUI), IGUIScrollBarOwner(pGUI), @@ -42,7 +43,6 @@ AddSetting("text_align"); AddSetting("textcolor"); AddSetting("textcolor_selected"); - AddSetting("selected"); // Index selected. -1 is none. AddSetting("auto_scroll"); AddSetting("hovered"); AddSetting("tooltip"); @@ -176,11 +176,15 @@ int hovered = GetHoveredItem(); if (hovered == -1) break; - SetSetting("selected", hovered, true); + + SelectionControl(hovered); + UpdateAutoScroll(); PlaySound("sound_selected"); - if (timer_Time() - m_LastItemClickTime < SELECT_DBLCLICK_RATE && hovered == m_PrevSelectedItem) + if (timer_Time() - m_LastItemClickTime < SELECT_DBLCLICK_RATE && + hovered == m_PrevSelectedItem && + IsEnabledDoubleClick()) this->SendEvent(GUIM_MOUSE_DBLCLICK_LEFT_ITEM, "mouseleftdoubleclickitem"); else this->SendEvent(GUIM_MOUSE_PRESS_LEFT_ITEM, "mouseleftclickitem"); @@ -278,10 +282,42 @@ void CList::Draw() { - DrawList(GetSetting("selected"), "sprite", "sprite_selectarea", "textcolor"); + DrawList(-1, "sprite", "sprite_selectarea", "textcolor"); } -void CList::DrawList(const int& selected, const CStr& _sprite, const CStr& _sprite_selected, const CStr& _textcolor) +void CList::DrawSelection(const int selected, const bool scrollbar, const float scroll, CGUISpriteInstance& sprite_selectarea, const int cell_id, const float bz, CRect& rect) +{ + if (selected < 0 || selected + 1 >= static_cast(m_ItemsYPositions.size())) + return; + + // Get rectangle of selection: + CRect rect_sel(rect.left, rect.top + m_ItemsYPositions[selected] - scroll, + rect.right, rect.top + m_ItemsYPositions[selected + 1] - scroll); + + if (rect_sel.top > rect.bottom || rect_sel.bottom < rect.top) + return; + + if (rect_sel.bottom > rect.bottom) + rect_sel.bottom = rect.bottom; + if (rect_sel.top < rect.top) + rect_sel.top = rect.top; + + if (scrollbar) + { + // Remove any overlapping area of the scrollbar. + if (rect_sel.right > GetScrollBar(0).GetOuterRect().left && + rect_sel.right <= GetScrollBar(0).GetOuterRect().right) + rect_sel.right = GetScrollBar(0).GetOuterRect().left; + + if (rect_sel.left >= GetScrollBar(0).GetOuterRect().left && + rect_sel.left < GetScrollBar(0).GetOuterRect().right) + rect_sel.left = GetScrollBar(0).GetOuterRect().right; + } + + m_pGUI.DrawSprite(sprite_selectarea, cell_id, bz + 0.05f, rect_sel); +} + +void CList::DrawList(const int selected, const CStr& _sprite, const CStr& _sprite_selected, const CStr& _textcolor) { float bz = GetBufferedZ(); @@ -304,35 +340,7 @@ if (scrollbar) scroll = GetScrollBar(0).GetPos(); - if (selected >= 0 && selected+1 < (int)m_ItemsYPositions.size()) - { - // Get rectangle of selection: - CRect rect_sel(rect.left, rect.top + m_ItemsYPositions[selected] - scroll, - rect.right, rect.top + m_ItemsYPositions[selected+1] - scroll); - - if (rect_sel.top <= rect.bottom && - rect_sel.bottom >= rect.top) - { - if (rect_sel.bottom > rect.bottom) - rect_sel.bottom = rect.bottom; - if (rect_sel.top < rect.top) - rect_sel.top = rect.top; - - if (scrollbar) - { - // Remove any overlapping area of the scrollbar. - if (rect_sel.right > GetScrollBar(0).GetOuterRect().left && - rect_sel.right <= GetScrollBar(0).GetOuterRect().right) - rect_sel.right = GetScrollBar(0).GetOuterRect().left; - - if (rect_sel.left >= GetScrollBar(0).GetOuterRect().left && - rect_sel.left < GetScrollBar(0).GetOuterRect().right) - rect_sel.left = GetScrollBar(0).GetOuterRect().right; - } - - m_pGUI.DrawSprite(sprite_selectarea, cell_id, bz+0.05f, rect_sel); - } - } + DrawSelectedItems(selected, scrollbar, scroll, sprite_selectarea, cell_id, bz, rect); const CGUIList& pList = GetSetting("list"); const CGUIColor& color = GetSetting(_textcolor); @@ -393,50 +401,9 @@ return false; } -void CList::SelectNextElement() -{ - int selected = GetSetting("selected"); - - const CGUIList& pList = GetSetting("list"); - - if (selected != static_cast(pList.m_Items.size()) - 1) - { - ++selected; - SetSetting("selected", selected, true); - PlaySound("sound_selected"); - } -} - -void CList::SelectPrevElement() -{ - int selected = GetSetting("selected"); - - if (selected > 0) - { - --selected; - SetSetting("selected", selected, true); - PlaySound("sound_selected"); - } -} - -void CList::SelectFirstElement() -{ - if (GetSetting("selected") >= 0) - SetSetting("selected", 0, true); -} - -void CList::SelectLastElement() -{ - const CGUIList& pList = GetSetting("list"); - const int index = static_cast(pList.m_Items.size()) - 1; - - if (GetSetting("selected") != index) - SetSetting("selected", index, true); -} - void CList::UpdateAutoScroll() { - const int selected = GetSetting("selected"); + const int selected = GetLastSelected(); const bool scrollbar = GetSetting("scrollbar"); // No scrollbar, no scrolling (at least it's not made to work properly). @@ -482,3 +449,47 @@ return -1; } + +void CList::UpdateSelection(int oldIndex, int newIndex) +{ + m_PrevSelectedItem = selected; + PlaySound("sound_selected"); + + SelectionControl(selected); +} + +void CList::SelectNextElement() +{ + int selected = GetLastSelected(); + + const CGUIList& pList = GetSetting("list"); + + if (selected != static_cast(pList.m_Items.size()) - 1) + UpdateSelection(selected, ++selected); +} + +void CList::SelectPrevElement() +{ + int selected = GetLastSelected(); + + if (selected > 0) + UpdateSelection(selected, --selected); +} + +void CList::SelectFirstElement() +{ + int selected = GetLastSelected(); + + if (selected >= 0) + UpdateSelection(selected, 0); +} + +void CList::SelectLastElement() +{ + const CGUIList& pList = GetSetting("list"); + const int index = static_cast(pList.m_Items.size()) - 1; + int selected = GetLastSelected(); + + if (selected != index) + UpdateSelection(selected, index); +} Index: source/gui/CListMultiple.h =================================================================== --- /dev/null +++ source/gui/CListMultiple.h @@ -0,0 +1,53 @@ +/* Copyright (C) 2019 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#ifndef INCLUDED_CLISTMULTIPLE +#define INCLUDED_CLISTMULTIPLE + +#include "gui/CIntList.h" +#include "gui/CList.h" + +class CListMultiple: public CList +{ + GUI_OBJECT(CListMultiple); + +public: + CListMultiple(CGUI& pGUI); + virtual ~CListMultiple(); + + virtual bool IsEnabledDoubleClick(); + + virtual void DrawSelectiedItems(const int selected, const bool scrollbar, const float scroll, CGUISpriteInstance& sprite_selectarea, const int cell_id, const float bz, CRect& rect); + + virtual int GetLastSelected(); + + std::vector m_SelectedItems; +private: + + // Select items between last two clicks + bool m_IsMultiSelecting; + + // Add/Remove one item to/from selection + bool m_AddMultiSelection; + + // Used for multiselection + int m_MultiSelectionFromItem; + + virtual void SelectionControl(int &select); +}; + +#endif // INCLUDED_CLISTMULTIPLE Index: source/gui/CListMultiple.cpp =================================================================== --- /dev/null +++ source/gui/CListMultiple.cpp @@ -0,0 +1,124 @@ +/* Copyright (C) 2019 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "precompiled.h" + +#include "CListMultiple.h" + +#include "gui/CGUI.h" +#include "gui/CGUIColor.h" +#include "gui/CGUIScrollBarVertical.h" +#include "lib/external_libraries/libsdl.h" +#include "lib/timer.h" +#include "ps/Globals.h" + +CListMultiple::CListMultiple(CGUI& pGUI) + : CList(pGUI), IGUIObject(pGUI), + m_AddMultiSelection(false), m_IsMultiSelecting(false) +{ + AddSetting("multiSelected"); +} + +CListMultiple::~CListMultiple() +{ +} + +void CListMultiple::SelectionControl(int &select) +{ + m_IsMultiSelecting = g_keys[SDLK_LSHIFT] || g_keys[SDLK_RSHIFT]; + m_AddMultiSelection = g_keys[SDLK_LCTRL] || g_keys[SDLK_RCTRL]; + + // Add current item to selection + if (m_AddMultiSelection) + { + CIntList sel = GetSetting("multiSelected"); + m_MultiSelectionFromItem = select; + m_SelectedItems = sel.m_Items; + bool add = true; + size_t pos = -1; + for (size_t it = 0; it < m_SelectedItems.size(); ++it) + if (m_SelectedItems.at(it) == select) + { + add = false; + pos = it; + break; + } + if (add) + m_SelectedItems.push_back(select); + else + { + m_SelectedItems.erase(m_SelectedItems.begin() + pos); + // Select last item in multiselection + if (m_SelectedItems.size() == 1) + select = m_SelectedItems.at(0); + } + + sel.m_Items = m_SelectedItems; + SetSetting("multiSelected", sel, true); + } + // Select everything from last clicked item to current + else if (m_IsMultiSelecting) + { + if (m_MultiSelectionFromItem == select) + return; + + CIntList sel = GetSetting("multiSelected"); + m_SelectedItems = sel.m_Items; + + m_SelectedItems.clear(); + for (size_t itemId = m_MultiSelectionFromItem; itemId < select + 1; ++itemId) + m_SelectedItems.push_back(itemId); + + sel.m_Items = m_SelectedItems; + SetSetting( "multiSelected", sel, true); + } + // Clear multi selection + else + { + CIntList sel = GetSetting("multiSelected"); + m_SelectedItems = sel.m_Items; + m_SelectedItems.clear(); + m_SelectedItems.push_back(select); + sel.m_Items = m_SelectedItems; + m_MultiSelectionFromItem = select; + SetSetting("multiSelected", sel, true); + } +} + +bool CListMultiple::IsEnabledDoubleClick() +{ + return !m_IsMultiSelecting && !m_AddMultiSelection; +} + +void CListMultiple::DrawSelectiedItems(const int selected, const bool scrollbar, const float scroll, CGUISpriteInstance& sprite_selectarea, const int cell_id, const float bz, CRect& rect) +{ + CIntList sel = GetSetting("multiSelected"); + m_SelectedItems = sel.m_Items; + + // Draw multi selection + if (!m_SelectedItems.empty()) + for (size_t selection = 0; selection < m_SelectedItems.size(); ++selection) + DrawSelection(m_SelectedItems[selection], scrollbar, scroll, sprite_selectarea, cell_id, bz, rect); +} + +int CListMultiple::GetLastSelected() +{ + CIntList sel = GetSetting("multiSelected"); + if (sel.m_Items.size() == 0) + return -1; + return sel.m_Items.at(sel.m_Items.size()-1); +} Index: source/gui/CListSingle.h =================================================================== --- /dev/null +++ source/gui/CListSingle.h @@ -0,0 +1,40 @@ +/* Copyright (C) 2019 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#ifndef INCLUDED_CLISTSINGLE +#define INCLUDED_CLISTSINGLE + +#include "gui/CList.h" + +class CListSingle: public CList +{ + GUI_OBJECT(CListSingle); + +public: + CListSingle(CGUI& pGUI); + virtual ~CListSingle(); + + virtual bool IsEnabledDoubleClick(); + + virtual void DrawSelectiedItems(const int selected, const bool scrollbar, const float scroll, CGUISpriteInstance& sprite_selectarea, const int cell_id, const float bz, CRect& rect); + + virtual int GetLastSelected(); +private: + virtual void SelectionControl(int &select); +}; + +#endif // INCLUDED_CLISTSINGLE Index: source/gui/CListSingle.cpp =================================================================== --- /dev/null +++ source/gui/CListSingle.cpp @@ -0,0 +1,58 @@ +/* Copyright (C) 2019 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "precompiled.h" + +#include "CListSingle.h" + +#include "gui/CGUI.h" +#include "gui/CGUIColor.h" +#include "gui/CGUIScrollBarVertical.h" +#include "lib/external_libraries/libsdl.h" +#include "lib/timer.h" +#include "ps/Globals.h" + +CListSingle::CListSingle(CGUI& pGUI) + : CList(pGUI), IGUIObject(pGUI) +{ + AddSetting("selected"); // Index selected. -1 is none. +} + +CListSingle::~CListSingle() +{ +} + +void CListSingle::SelectionControl(int &select) +{ + SetSetting("selected", select, true); +} + +bool CListSingle::IsEnabledDoubleClick() +{ + return true; +} + +void CListSingle::DrawSelectiedItems(const int selected, const bool scrollbar, const float scroll, CGUISpriteInstance& sprite_selectarea, const int cell_id, const float bz, CRect& rect) +{ + if (selected != -1) + DrawSelection(selected, scrollbar, scroll, sprite_selectarea, cell_id, bz, rect); +} + +int CListSingle::GetLastSelected() +{ + return GetSetting("selected"); +} Index: source/gui/COList.h =================================================================== --- source/gui/COList.h +++ source/gui/COList.h @@ -18,7 +18,7 @@ #define INCLUDED_COLIST #include "GUI.h" -#include "CList.h" +#include "CListSingle.h" /** * Represents a column. @@ -44,7 +44,7 @@ * heading is clicked. * A scroll-bar will appear when needed. */ -class COList : public CList +class COList : public CListSingle { GUI_OBJECT(COList) @@ -60,7 +60,7 @@ */ virtual bool HandleAdditionalChildren(const XMBElement& child, CXeromyces* pFile); - void DrawList(const int& selected, const CStr& _sprite, const CStr& _sprite_selected, const CStr& _textcolor); + void DrawList(const int selected, const CStr& _sprite, const CStr& _sprite_selected, const CStr& _textcolor); virtual CRect GetListRect() const; Index: source/gui/COList.cpp =================================================================== --- source/gui/COList.cpp +++ source/gui/COList.cpp @@ -28,7 +28,7 @@ const CPos COLUMN_SHIFT = CPos(0, 4); COList::COList(CGUI& pGUI) - : CList(pGUI), IGUIObject(pGUI) + : CListSingle(pGUI), IGUIObject(pGUI) { AddSetting("sprite_heading"); AddSetting("sortable"); // The actual sorting is done in JS for more versatility @@ -284,7 +284,7 @@ return false; } -void COList::DrawList(const int& selected, const CStr& _sprite, const CStr& _sprite_selected, const CStr& _textcolor) +void COList::DrawList(const int selected, const CStr& _sprite, const CStr& _sprite_selected, const CStr& _textcolor) { const float bz = GetBufferedZ(); const bool scrollbar = GetSetting("scrollbar"); @@ -305,39 +305,7 @@ if (scrollbar) scroll = GetScrollBar(0).GetPos(); - // Draw item selection - if (selected != -1) - { - ENSURE(selected >= 0 && selected+1 < (int)m_ItemsYPositions.size()); - - // Get rectangle of selection: - CRect rect_sel(rect.left, rect.top + m_ItemsYPositions[selected] - scroll, - rect.right, rect.top + m_ItemsYPositions[selected+1] - scroll); - - if (rect_sel.top <= rect.bottom && - rect_sel.bottom >= rect.top) - { - if (rect_sel.bottom > rect.bottom) - rect_sel.bottom = rect.bottom; - if (rect_sel.top < rect.top) - rect_sel.top = rect.top; - - if (scrollbar) - { - // Remove any overlapping area of the scrollbar. - if (rect_sel.right > GetScrollBar(0).GetOuterRect().left && - rect_sel.right <= GetScrollBar(0).GetOuterRect().right) - rect_sel.right = GetScrollBar(0).GetOuterRect().left; - - if (rect_sel.left >= GetScrollBar(0).GetOuterRect().left && - rect_sel.left < GetScrollBar(0).GetOuterRect().right) - rect_sel.left = GetScrollBar(0).GetOuterRect().right; - } - - // Draw item selection - m_pGUI.DrawSprite(sprite_selectarea, cell_id, bz+0.05f, rect_sel); - } - } + DrawSelectedItems(selected, scrollbar, scroll, sprite_selectarea, cell_id, bz, rect); // Draw line above column header CGUISpriteInstance& sprite_heading = GetSetting("sprite_heading"); Index: source/gui/GUI.h =================================================================== --- source/gui/GUI.h +++ source/gui/GUI.h @@ -28,6 +28,7 @@ #include "CGUIList.h" #include "CGUISeries.h" #include "CGUIText.h" +#include "CIntList.h" #include "GUIbase.h" #include "IGUIButtonBehavior.h" #include "IGUIObject.h" Index: source/gui/GUIStringConversions.cpp =================================================================== --- source/gui/GUIStringConversions.cpp +++ source/gui/GUIStringConversions.cpp @@ -19,6 +19,7 @@ #include "gui/CGUI.h" #include "gui/CGUIString.h" +#include "gui/CIntList.h" #include "ps/CLogger.h" class CGUIList; @@ -244,3 +245,9 @@ { return false; } + +template <> +bool CGUI::ParseString(const CGUI* UNUSED(pGUI), const CStrW& UNUSED(Value), CIntList& UNUSED(Output)) +{ + return false; +} Index: source/gui/GUItypes.h =================================================================== --- source/gui/GUItypes.h +++ source/gui/GUItypes.h @@ -33,6 +33,7 @@ TYPE(EAlign) TYPE(EVAlign) TYPE(CPos) +TYPE(CIntList) #endif #ifndef GUITYPE_IGNORE_NONCOPYABLE Index: source/gui/scripting/GuiScriptConversions.cpp =================================================================== --- source/gui/scripting/GuiScriptConversions.cpp +++ source/gui/scripting/GuiScriptConversions.cpp @@ -20,6 +20,7 @@ #include "gui/CGUIColor.h" #include "gui/CGUIList.h" #include "gui/CGUISeries.h" +#include "gui/CIntList.h" #include "gui/GUIbase.h" #include "gui/IGUIObject.h" #include "lib/external_libraries/libsdl.h" @@ -252,6 +253,16 @@ return FromJSVal(cx, v, out.m_Series); } +template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const CIntList& val) +{ + ToJSVal(cx, ret, val.m_Items); +} + +template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, CIntList& out) +{ + return FromJSVal(cx, v, out.m_Items); +} + template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const EVAlign& val) { std::string word;