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,19 +215,35 @@ 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 @@ selectionChanged(); init(); 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 @@ -681,7 +681,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"); @@ -762,7 +763,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 = []; } /** @@ -771,17 +777,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(); saveSettingAndWriteToUserConfig("lobby.buddies", g_Buddies.filter(nick => nick).join(g_BuddyListDelimiter) || g_BuddyListDelimiter); @@ -813,24 +818,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(); @@ -986,8 +997,9 @@ let gamesBox = Engine.GetGUIObjectByName("gamesBox"); let sortBy = gamesBox.selected_column; let sortOrder = gamesBox.selected_column_order; - - if (gamesBox.selected > -1) + let selected = gamesBox.multiSelected.length ? gamesBox.multiSelected[0] : -1; + + if (selected > -1) { g_SelectedGameIP = g_GameList[gamesBox.selected].ip; g_SelectedGamePort = g_GameList[gamesBox.selected].port; @@ -1107,6 +1119,10 @@ gamesBox.auto_scroll = false; gamesBox.selected = selectedGameIndex; + if (selectedGameIndex > -1) + gamesBox.multiSelected = [selectedGameIndex]; + else + gamesBox.multiSelected = []; updateGameSelection(); } @@ -1165,10 +1181,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 = []; } /** @@ -250,19 +254,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: binaries/data/mods/public/gui/savegame/save.js =================================================================== --- binaries/data/mods/public/gui/savegame/save.js +++ binaries/data/mods/public/gui/savegame/save.js @@ -4,8 +4,10 @@ function selectDescription() { let gameSelection = Engine.GetGUIObjectByName("gameSelection"); - let gameID = gameSelection.list_data[gameSelection.selected]; + let index = gameSelection.multiSelected.length ? gameSelection.multiSelected[0] : -1; + let gameID = gameSelection.list_data[index]; Engine.GetGUIObjectByName("deleteGameButton").enabled = !!gameID; + Engine.GetGUIObjectByName("saveGameButton").enabled = gameSelection.multiSelected.length < 2; if (!gameID) return; @@ -29,6 +31,7 @@ { gameSelection.list = [translate("No saved games found")]; gameSelection.selected = -1; + gameSelection.multiSelected = []; return; } @@ -40,15 +43,19 @@ gameSelection.list = savedGames.map(game => generateSavegameLabel(game.metadata, engineInfo)); gameSelection.list_data = savedGames.map(game => game.id); gameSelection.selected = Math.min(gameSelection.selected, gameSelection.list.length - 1); - + if (gameSelection.selected == -1) + gameSelection.multiSelected = []; + else + gameSelection.multiSelected = [gameSelection.selected]; Engine.GetGUIObjectByName("deleteGameButton").tooltip = deleteTooltip(); + selectDescription(); } function saveGame() { let gameSelection = Engine.GetGUIObjectByName("gameSelection"); - let gameLabel = gameSelection.list[gameSelection.selected]; - let gameID = gameSelection.list_data[gameSelection.selected]; + let gameLabel = gameSelection.list[gameSelection.multiSelected[0]]; + let gameID = gameSelection.list_data[gameSelection.multiSelected[0]]; let desc = Engine.GetGUIObjectByName("saveGameDesc").caption; let name = gameID || "savegame"; Index: binaries/data/mods/public/gui/savegame/save.xml =================================================================== --- binaries/data/mods/public/gui/savegame/save.xml +++ binaries/data/mods/public/gui/savegame/save.xml @@ -17,7 +17,7 @@ selectDescription(); @@ -42,7 +42,7 @@ deleteGame(); - + Save saveGame(); 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 @@ -1,4 +1,4 @@ -/* Copyright (C) 2017 Wildfire Games. +/* 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 @@ -19,9 +19,10 @@ #define INCLUDED_CLIST #include "IGUIScrollBar.h" +#include "CIntList.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. @@ -76,7 +77,6 @@ virtual void SelectPrevElement(); virtual void SelectFirstElement(); virtual void SelectLastElement(); - /** * Handle the \ tag. */ @@ -85,9 +85,14 @@ // Called every time the auto-scrolling should be checked. void UpdateAutoScroll(); + /** + * 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. @@ -107,6 +112,7 @@ virtual int GetHoveredItem(); + std::vector m_SelectedItems; private: // Whether the list's items have been modified since last handling a message. bool m_Modified; @@ -116,6 +122,26 @@ // Last time a click on an item was issued double m_LastItemClickTime; + + // 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; + + /** + * Control multiselection + * If two items remain in multiselection and one is removed, select is changed to the not removed one + */ + void MultiSelectionControl(int& select); + + /** + * Control selection when multiselection is disabled + */ + void SingleSelectionControl(int& select); }; #endif // INCLUDED_CLIST Index: source/gui/CList.cpp =================================================================== --- source/gui/CList.cpp +++ source/gui/CList.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2018 Wildfire Games. +/* 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 @@ -23,12 +23,13 @@ #include "lib/external_libraries/libsdl.h" #include "ps/CLogger.h" +#include "ps/Globals.h" #include "ps/Profile.h" #include "soundmanager/ISoundManager.h" CList::CList() - : m_Modified(false), m_PrevSelectedItem(-1), m_LastItemClickTime(0) + : m_Modified(false), m_PrevSelectedItem(-1), m_LastItemClickTime(0), m_AddMultiSelection(false), m_IsMultiSelecting(false) { // Add sprite_disabled! TODO @@ -45,6 +46,8 @@ AddSetting(GUIST_CColor, "textcolor"); AddSetting(GUIST_CColor, "textcolor_selected"); AddSetting(GUIST_int, "selected"); // Index selected. -1 is none. + AddSetting(GUIST_CIntList, "multiSelected"); + AddSetting(GUIST_bool, "multiSelection_enabled"); AddSetting(GUIST_bool, "auto_scroll"); AddSetting(GUIST_int, "hovered"); AddSetting(GUIST_CStrW, "tooltip"); @@ -146,6 +149,91 @@ } } +void CList::SingleSelectionControl(int& select) +{ + CIntList sel; + GUI::GetSetting(this, "multiSelected", sel); + m_SelectedItems = sel.m_Items; + if (!m_SelectedItems.empty()) + m_SelectedItems.at(0) = select; + else + m_SelectedItems.push_back(select); + sel.m_Items = m_SelectedItems; + GUI::SetSetting(this, "multiSelected", sel); +} + +void CList::MultiSelectionControl(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; + GUI::GetSetting(this, "multiSelected", sel); + 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; + GUI::SetSetting(this, "multiSelected", sel); + } + // Select everything from last clicked item to current + else if (m_IsMultiSelecting) + { + if (m_MultiSelectionFromItem == select) + return; + + CIntList sel; + GUI::GetSetting(this, "multiSelected", sel); + m_SelectedItems = sel.m_Items; + int firstId = m_MultiSelectionFromItem; + int lastId = select; + + if (select < m_MultiSelectionFromItem) + { + firstId = select; + lastId = m_MultiSelectionFromItem; + } + + m_SelectedItems.clear(); + for (size_t itemId = firstId; itemId < lastId + 1; ++itemId) + m_SelectedItems.push_back(itemId); + + sel.m_Items = m_SelectedItems; + GUI::SetSetting(this, "multiSelected", sel); + } + // Clear multi selection + else + { + CIntList sel; + GUI::GetSetting(this, "multiSelected", sel); + m_SelectedItems = sel.m_Items; + m_SelectedItems.clear(); + m_SelectedItems.push_back(select); + sel.m_Items = m_SelectedItems; + m_MultiSelectionFromItem = select; + GUI::SetSetting(this, "multiSelected", sel); + } +} void CList::HandleMessage(SGUIMessage& Message) { IGUIScrollBarOwner::HandleMessage(Message); @@ -205,6 +293,15 @@ int hovered = GetHoveredItem(); if (hovered == -1) break; + + bool multiSelectionEnabled; + GUI::GetSetting(this, "multiSelection_enabled", multiSelectionEnabled); + + if (multiSelectionEnabled) + MultiSelectionControl(hovered); + else + SingleSelectionControl(hovered); + GUI::SetSetting(this, "selected", hovered); UpdateAutoScroll(); @@ -212,7 +309,8 @@ if (g_SoundManager && GUI::GetSetting(this, "sound_selected", soundPath) == PSRETURN_OK && !soundPath.empty()) g_SoundManager->PlayAsUI(soundPath.c_str(), false); - if (timer_Time() - m_LastItemClickTime < SELECT_DBLCLICK_RATE && hovered == m_PrevSelectedItem) + if (timer_Time() - m_LastItemClickTime < SELECT_DBLCLICK_RATE && hovered == m_PrevSelectedItem && + (!multiSelectionEnabled || (!m_IsMultiSelecting && !m_AddMultiSelection))) this->SendEvent(GUIM_MOUSE_DBLCLICK_LEFT_ITEM, "mouseleftdoubleclickitem"); else this->SendEvent(GUIM_MOUSE_PRESS_LEFT_ITEM, "mouseleftclickitem"); @@ -266,7 +364,6 @@ InReaction CList::ManuallyHandleEvent(const SDL_Event_* ev) { InReaction result = IN_PASS; - if (ev->ev.type == SDL_KEYDOWN) { int szChar = ev->ev.key.keysym.sym; @@ -311,19 +408,49 @@ result = IN_PASS; } } - return result; } void CList::Draw() { - int selected; - GUI::GetSetting(this, "selected", selected); + DrawList(-1, "sprite", "sprite_selectarea", "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 (!GetGUI()) + return; + if (selected < 0 || selected + 1 >= (int)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; + } - DrawList(selected, "sprite", "sprite_selectarea", "textcolor"); + GetGUI()->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) +void CList::DrawList(const int selected, const CStr& _sprite, const CStr& _sprite_selected, const CStr& _textcolor) { float bz = GetBufferedZ(); @@ -354,35 +481,15 @@ 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; - } - - GetGUI()->DrawSprite(*sprite_selectarea, cell_id, bz+0.05f, rect_sel); - } - } + CIntList sel; + GUI::GetSetting(this, "multiSelected", sel); + 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); + else if (selected != -1) + DrawSelection(selected, scrollbar, scroll, *sprite_selectarea, cell_id, bz, rect); CColor color; GUI::GetSetting(this, _textcolor, color); @@ -454,12 +561,20 @@ if (selected != (int)pList->m_Items.size()-1) { + m_PrevSelectedItem = selected; ++selected; GUI::SetSetting(this, "selected", selected); CStrW soundPath; if (g_SoundManager && GUI::GetSetting(this, "sound_selected", soundPath) == PSRETURN_OK && !soundPath.empty()) g_SoundManager->PlayAsUI(soundPath.c_str(), false); + + bool multiSelectionEnabled; + GUI::GetSetting(this, "multiSelection_enabled", multiSelectionEnabled); + if (multiSelectionEnabled) + MultiSelectionControl(selected); + else + SingleSelectionControl(selected); } } @@ -470,12 +585,20 @@ if (selected > 0) { + m_PrevSelectedItem = selected; --selected; GUI::SetSetting(this, "selected", selected); CStrW soundPath; if (g_SoundManager && GUI::GetSetting(this, "sound_selected", soundPath) == PSRETURN_OK && !soundPath.empty()) g_SoundManager->PlayAsUI(soundPath.c_str(), false); + + bool multiSelectionEnabled; + GUI::GetSetting(this, "multiSelection_enabled", multiSelectionEnabled); + if (multiSelectionEnabled) + MultiSelectionControl(selected); + else + SingleSelectionControl(selected); } } @@ -484,8 +607,17 @@ int selected; GUI::GetSetting(this, "selected", selected); - if (selected >= 0) - GUI::SetSetting(this, "selected", 0); + if (selected >= 0) { + m_PrevSelectedItem = selected; + selected = 0; + GUI::SetSetting(this, "selected", selected); + bool multiSelectionEnabled; + GUI::GetSetting(this, "multiSelection_enabled", multiSelectionEnabled); + if (multiSelectionEnabled) + MultiSelectionControl(selected); + else + SingleSelectionControl(selected); + } } void CList::SelectLastElement() @@ -496,8 +628,17 @@ CGUIList* pList; GUI::GetSettingPointer(this, "list", pList); - if (selected != (int)pList->m_Items.size()-1) - GUI::SetSetting(this, "selected", (int)pList->m_Items.size()-1); + if (selected != (int)pList->m_Items.size() - 1) { + m_PrevSelectedItem = selected; + selected = (int)pList->m_Items.size() - 1; + GUI::SetSetting(this, "selected", selected); + bool multiSelectionEnabled; + GUI::GetSetting(this, "multiSelection_enabled", multiSelectionEnabled); + if (multiSelectionEnabled) + MultiSelectionControl(selected); + else + SingleSelectionControl(selected); + } } void CList::UpdateAutoScroll() Index: source/gui/COList.h =================================================================== --- source/gui/COList.h +++ source/gui/COList.h @@ -56,7 +56,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 @@ -311,7 +311,7 @@ } } -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) { float bz = GetBufferedZ(); @@ -342,39 +342,15 @@ 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 - GetGUI()->DrawSprite(*sprite_selectarea, cell_id, bz+0.05f, rect_sel); - } - } + CIntList sel; + GUI::GetSetting(this, "multiSelected", sel); + m_SelectedItems = sel.m_Items; + + 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); + else if (selected != -1) + DrawSelection(selected, scrollbar, scroll, *sprite_selectarea, cell_id, bz, rect); // Draw line above column header CGUISpriteInstance* sprite_heading = NULL; Index: source/gui/GUI.h =================================================================== --- source/gui/GUI.h +++ source/gui/GUI.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2015 Wildfire Games. +/* 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 @@ -44,6 +44,7 @@ #include "GUIbase.h" #include "GUItext.h" #include "GUIutil.h" +#include "CIntList.h" #include "IGUIButtonBehavior.h" #include "IGUIObject.h" #include "IGUIScrollBarOwner.h" // Required by IGUIScrollBar Index: source/gui/GUItypes.h =================================================================== --- source/gui/GUItypes.h +++ source/gui/GUItypes.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2016 Wildfire Games. +/* 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 @@ -42,3 +42,4 @@ TYPE(CPos) TYPE(CGUIList) TYPE(CGUISeries) +TYPE(CIntList) Index: source/gui/GUIutil.cpp =================================================================== --- source/gui/GUIutil.cpp +++ source/gui/GUIutil.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2016 Wildfire Games. +/* 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 @@ -266,6 +266,11 @@ return false; } +template <> +bool __ParseString(const CStrW& UNUSED(Value), CIntList& UNUSED(Output)) +{ + return false; +} //-------------------------------------------------------- CMatrix3D GetDefaultGuiMatrix() Index: source/gui/scripting/JSInterface_IGUIObject.cpp =================================================================== --- source/gui/scripting/JSInterface_IGUIObject.cpp +++ source/gui/scripting/JSInterface_IGUIObject.cpp @@ -295,7 +295,13 @@ ScriptInterface::ToJSVal(cx, vp, value.m_Items); break; } - + case GUIST_CIntList: + { + CIntList value; + GUI::GetSetting(e, propName, value); + ScriptInterface::ToJSVal(cx, vp, value.m_Items); + break; + } case GUIST_CGUISeries: { CGUISeries value; @@ -586,6 +592,19 @@ break; } + case GUIST_CIntList: + { + CIntList list; + if (ScriptInterface::FromJSVal(cx, vp, list.m_Items)) + GUI::SetSetting(e, propName, list); + else + { + JS_ReportError(cx, "Failed to get list '%s'", propName.c_str()); + return false; + } + break; + } + case GUIST_CGUISeries: { CGUISeries series;