1
0
forked from mirrors/0ad

Map browser, used in the gamesetup and in the main menu.

This grid-based system allows browsing all available maps at a glance,
encouraging more diversity and making it nicer to pick a map.

Moves the mapCache and the map filters controller to the gui maps/
folders, and include that folder where it is used, instead of them being
in common/ or the gamesetup.

Comments By: Freagarach
Patch By: Nani (reworked by elexis then wraitii)
Fixes #5315 (though further work, such as proper scrolling, would be
nice).

Differential Revision: https://code.wildfiregames.com/D1703
This was SVN commit r24459.
This commit is contained in:
wraitii
2020-12-27 15:26:19 +00:00
parent f31443eccb
commit ae9ea5b859
37 changed files with 1110 additions and 2 deletions
+3
View File
@@ -288,6 +288,9 @@ offscreen = Alt ; Include offscreen units in selection
8 = 8, Num8
9 = 9, Num9
[hotkey.gamesetup]
mapbrowser.open = "M"
[hotkey.session]
kill = Delete, Backspace ; Destroy selected units
stop = "H" ; Stop the current action
@@ -0,0 +1,25 @@
/**
* This class is implemented by gamesettings that are controlled by a button.
*/
class GameSettingControlButton extends GameSettingControl
{
setControl(gameSettingControlManager)
{
let row = gameSettingControlManager.getNextRow("buttonSettingFrame");
this.frame = Engine.GetGUIObjectByName("buttonSettingFrame[" + row + "]");
this.button = Engine.GetGUIObjectByName("buttonSettingControl[" + row + "]");
this.button.onPress = this.onPress.bind(this);
if (this.Caption)
this.button.caption = this.Caption;
}
setControlTooltip(tooltip)
{
this.button.tooltip = tooltip;
}
setControlHidden(hidden)
{
this.button.hidden = hidden;
}
}
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<object name="buttonSettingFrame[n]" size="0 2 100% 32" hidden="true">
<object
name="buttonSettingControl[n]"
type="button"
size="175 0 100% 28"
style="ModernButtonRed"
tooltip_style="onscreenToolTip"
hidden="true"
z="1"
/>
</object>
@@ -9,6 +9,7 @@ var g_GameSettingsLayout = [
"MapType",
"MapFilter",
"MapSelection",
"MapBrowser",
"MapSize",
"TeamPlacement",
"Landscape",
@@ -0,0 +1,29 @@
GameSettingControls.MapBrowser = class extends GameSettingControlButton
{
constructor(...args)
{
super(...args);
this.button.tooltip = colorizeHotkey(this.HotkeyTooltip, this.HotkeyConfig);
Engine.SetGlobalHotkey(this.HotkeyConfig, "Press", this.onPress.bind(this));
}
setControlHidden()
{
this.button.hidden = false;
}
onPress()
{
this.setupWindow.pages.MapBrowserPage.openPage();
}
};
GameSettingControls.MapBrowser.prototype.HotkeyConfig =
"gamesetup.mapbrowser.open";
GameSettingControls.MapBrowser.prototype.Caption =
translate("Browse Maps");
GameSettingControls.MapBrowser.prototype.HotkeyTooltip =
translate("Press %(hotkey)s to view the list of available maps.");
@@ -6,6 +6,7 @@
<script directory="gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/"/>
<script directory="gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/"/>
<script directory="gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/"/>
<script directory="gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Buttons/"/>
<script directory="gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Checkboxes/"/>
<script directory="gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Dropdowns/"/>
<script directory="gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Sliders/"/>
@@ -5,6 +5,13 @@
<object name="settingsPanel" size="0 5 100% 100%-5" z="1">
<!-- There is currently only one button, so don't add un-necessary objects. -->
<repeat count="1" var="n">
<object>
<include file="gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingControlButton.xml"/>
</object>
</repeat>
<repeat count="40" var="n">
<object>
<include file="gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingControlDropdown.xml"/>
@@ -2,16 +2,24 @@ class MapPreview
{
constructor(setupWindow)
{
this.setupWindow = setupWindow;
this.gameSettingsControl = setupWindow.controls.gameSettingsControl;
this.mapCache = setupWindow.controls.mapCache;
this.mapInfoName = Engine.GetGUIObjectByName("mapInfoName");
this.mapPreview = Engine.GetGUIObjectByName("mapPreview");
this.mapPreview.onMouseLeftPress = this.onPress.bind(this); // TODO: Why does onPress not work? CGUI.cpp seems to support it
this.mapPreview.tooltip = this.Tooltip;
this.gameSettingsControl.registerMapChangeHandler(this.onMapChange.bind(this));
this.gameSettingsControl.registerGameAttributesBatchChangeHandler(this.onGameAttributesBatchChange.bind(this));
}
onPress()
{
this.setupWindow.pages.MapBrowserPage.openPage();
}
onMapChange(mapData)
{
let preview = mapData && mapData.settings && mapData.settings.Preview;
@@ -34,3 +42,6 @@ class MapPreview
this.mapCache.getMapPreview(g_GameAttributes.mapType, g_GameAttributes.map, g_GameAttributes);
}
}
MapPreview.prototype.Tooltip =
translate("Click to view the list of available maps.");
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<object type="image" sprite="ModernDarkBoxGold" name="gamePreviewBox">
<object name="mapPreview" type="image" size="1 1 401 294"/>
<object name="mapPreview" type="image" size="1 1 401 294" tooltip_style="onscreenToolTip"/>
<object name="mapInfoName" type="text" size="5 100%-20 100% 100%-1" style="ModernLeftLabelText"/>
</object>
@@ -0,0 +1,22 @@
SetupWindowPages.MapBrowserPage = class extends MapBrowser
{
constructor(setupWindow)
{
super(setupWindow.controls.mapCache, setupWindow.controls.mapFilters, setupWindow);
this.mapBrowserPage.hidden = true;
}
openPage()
{
super.openPage();
this.mapBrowserPage.hidden = false;
}
closePage()
{
super.closePage();
this.mapBrowserPage.hidden = true;
}
};
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<object>
<include file="gui/maps/mapbrowser/MapBrowser.xml"/>
<script directory="gui/gamesetup/Pages/MapBrowserPage/"/>
</object>
@@ -2,6 +2,7 @@
<objects>
<script directory="gui/common/"/>
<script directory="gui/maps/"/>
<script directory="gui/gamesetup/"/>
<script directory="gui/gamesetup/Controls/"/>
<script directory="gui/gamesetup/NetMessages/"/>
@@ -15,6 +16,7 @@
<include file="gui/gamesetup/Pages/LoadingPage/LoadingPage.xml"/>
<include file="gui/gamesetup/Pages/GameSetupPage/GameSetupPage.xml"/>
<include file="gui/gamesetup/Pages/AIConfigPage/AIConfigPage.xml"/>
<include file="gui/gamesetup/Pages/MapBrowserPage/MapBrowserPage.xml"/>
</object>
</objects>
@@ -3,6 +3,7 @@
<objects>
<script directory="gui/common/"/>
<script directory="gui/maps/"/>
<script directory="gui/loadgame/"/>
<!-- Add a translucent black background to fade out the menu page -->
@@ -3,6 +3,7 @@
<objects>
<script directory="gui/common/"/>
<script directory="gui/maps/"/>
<script directory="gui/lobby/"/>
<object>
@@ -69,7 +69,7 @@ class MapFilters
}
Engine.ProfileStop();
return existence ? false : maps;
return existence ? false : maps.sort((a, b) => a.name.localeCompare(b.name));
}
}
@@ -0,0 +1,48 @@
class MapBrowser
{
constructor(mapCache, mapFilters, setupWindow = undefined)
{
this.openPageHandlers = new Set();
this.closePageHandlers = new Set();
this.mapCache = mapCache;
this.mapFilters = mapFilters;
this.mapBrowserPage = Engine.GetGUIObjectByName("mapBrowserPage");
this.mapBrowserPageDialog = Engine.GetGUIObjectByName("mapBrowserPageDialog");
this.gridBrowser = new MapGridBrowser(this, setupWindow);
this.controls = new MapBrowserPageControls(this, this.gridBrowser);
this.open = false;
}
// TODO: this is mostly gamesetup specific stuff.
registerOpenPageHandler(handler)
{
this.openPageHandlers.add(handler);
}
registerClosePageHandler(handler)
{
this.closePageHandlers.add(handler);
}
openPage()
{
if (this.open)
return;
for (let handler of this.openPageHandlers)
handler();
this.open = true;
}
closePage()
{
if (!this.open)
return;
for (let handler of this.closePageHandlers)
handler();
this.open = false;
}
}
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<object name="mapBrowserPage" z="300">
<script file="gui/maps/mapbrowser/MapBrowser.js"/>
<script directory="gui/maps/mapbrowser/controls/"/>
<script directory="gui/maps/mapbrowser/grid/"/>
<script directory="gui/maps/mapbrowser/utils/"/>
<object type="image" sprite="ModernFade"/>
<object name="mapBrowserPageDialog" type="image" style="ModernDialog" size="5% 5% 95% 95%">
<object style="ModernLabelText" type="text" size="50%-128 -18 50%+128 14" z="200">
<translatableAttribute id="caption">Map Browser</translatableAttribute>
</object>
<object name="mapBrowserTopPanel" size="15 15 100%-10 55">
</object>
<object name="mapBrowserLeftPanel" size="15 15 100%-400 100%-15">
<include file="gui/maps/mapbrowser/grid/MapGridBrowser.xml"/>
</object>
<object name="mapBrowserRightPanel" size="100%-390 15 100%-15 100%-15">
<include file="gui/maps/mapbrowser/controls/MapBrowserControls.xml"/>
</object>
</object>
</object>
@@ -0,0 +1,20 @@
/**
* TODO: better global state handling in the GUI.
* In particular a bunch of those shadow gamesetup stuff.
*/
const g_IsController = false;
const g_GameAttributes = {
"mapType": "skirmish",
"mapFilter": "default",
};
const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes);
var g_SetupWindow;
function init()
{
let cache = new MapCache();
let filters = new MapFilters(cache);
let browser = new MapBrowser(cache, filters);
browser.registerClosePageHandler(() => Engine.PopGuiPage());
browser.openPage();
}
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<objects>
<script directory="gui/common/"/>
<script directory="gui/maps/"/>
<object>
<include file="gui/maps/mapbrowser/MapBrowser.xml"/>
</object>
<script file="gui/maps/mapbrowser/MapBrowserPage.js"/>
</objects>
@@ -0,0 +1,58 @@
class MapBrowserPageControls
{
constructor(mapBrowserPage, gridBrowser)
{
for (let name in this)
this[name] = new this[name](mapBrowserPage, gridBrowser);
this.mapBrowserPage = mapBrowserPage;
this.gridBrowser = gridBrowser;
this.setupButtons();
}
setupButtons()
{
this.pickRandom = Engine.GetGUIObjectByName("mapBrowserPagePickRandom");
if (!g_IsController)
this.pickRandom.hidden = true;
this.pickRandom.onPress = () => {
let index = randIntInclusive(0, this.gridBrowser.itemCount - 1);
this.gridBrowser.setSelectedIndex(index);
this.gridBrowser.goToPageOfSelected();
};
this.select = Engine.GetGUIObjectByName("mapBrowserPageSelect");
this.select.onPress = () => this.onSelect();
this.close = Engine.GetGUIObjectByName("mapBrowserPageClose");
if (g_SetupWindow)
this.close.tooltip = colorizeHotkey(
translate("%(hotkey)s: Close map browser and discard the selection."), "cancel");
else
{
this.close.caption = translate("Close");
this.close.tooltip = colorizeHotkey(
translate("%(hotkey)s: Close map browser."), "cancel");
}
this.close.onPress = () => this.mapBrowserPage.closePage();
this.select.hidden = !g_IsController;
if (!g_IsController)
this.close.size = this.select.size;
this.gridBrowser.registerSelectionChangeHandler(() => this.onSelectionChange());
}
onSelectionChange()
{
this.select.enabled = this.gridBrowser.selected != -1;
}
onSelect()
{
this.gridBrowser.submitMapSelection();
this.mapBrowserPage.closePage();
}
}
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<object>
<object type="image" sprite="ModernFade" size="-5 0 100%+5 100%"/>
<!-- Page controls -->
<object>
<object name="mapBrowserPageStatus" type="text" style="ModernLabelText" size="0 0 180 30"/>
<object size="180 0 100%-60 30">
<object name="mapBrowserPreviousButton" type="button" style="ModernButtonRed" size="0 0 50% 100%"/>
<object name="mapBrowserNextButton" type="button" style="ModernButtonRed" size="50% 0 100% 100%"/>
</object>
<object size="100%-60 0 100% 30">
<object name="mapsZoomIn" type="button" style="ModernButtonRed" size="0 0 50% 100%">
<translatableAttribute id="caption" context="zoom in">+</translatableAttribute>
</object>
<object name="mapsZoomOut" type="button" style="ModernButtonRed" size="50% 0 100% 100%">
<translatableAttribute id="caption" context="zoom out"></translatableAttribute>
</object>
</object>
</object>
<object size="0 35 100% 100%-60">
<object size="0 0 100% 40">
<object name="mapBrowserSearchBoxLabel" type="text" style="ModernLeftLabelText" size="2 0 0 35">
<translatableAttribute id="caption">Search Map:</translatableAttribute>
</object>
<object name="mapBrowserSearchBoxControl" type="input" size="0 5 100% 30" style="ModernInput" font="sans-16"/>
</object>
<object size="0 40 100% 80">
<object name="mapBrowserMapTypeLabel" type="text" size="2 0 0 35" style="ModernLeftLabelText">
<translatableAttribute id="caption">Map Type:</translatableAttribute>
</object>
<object name="mapBrowserMapTypeControl" type="dropdown" size="0 5 100% 30" style="ModernDropDown"/>
</object>
<object size="0 80 100% 120">
<object name="mapBrowserMapFilterLabel" type="text" size="2 0 0 35" style="ModernLeftLabelText">
<translatableAttribute id="caption">Map Filter:</translatableAttribute>
</object>
<object name="mapBrowserMapFilterControl" type="dropdown" size="0 5 100% 30" style="ModernDropDown"/>
</object>
<object size="0 120 100% 100%">
<include file="gui/maps/mapbrowser/controls/MapDescription.xml"/>
</object>
</object>
<object name="mapBrowserPagePickRandom" type="button" style="ModernButtonRed" size="0 100%-60 100% 100%-30">
<translatableAttribute id="caption">Pick Random Map</translatableAttribute>
</object>
<object name="mapBrowserPageClose" type="button" style="ModernButtonRed" hotkey="cancel" size="0 100%-30 50% 100%">
<translatableAttribute id="caption">Cancel</translatableAttribute>
</object>
<object name="mapBrowserPageSelect" type="button" style="ModernButtonRed" size="50% 100%-30 100% 100%">
<translatableAttribute id="caption" context="map selection dialog">Select</translatableAttribute>
</object>
</object>
@@ -0,0 +1,49 @@
MapBrowserPageControls.prototype.MapDescription = class
{
constructor(mapBrowserPage, gridBrowser)
{
this.ImageRatio = 4 / 3;
this.mapBrowserPage = mapBrowserPage;
this.gridBrowser = gridBrowser;
this.mapCache = mapBrowserPage.mapCache;
this.mapBrowserSelectedName = Engine.GetGUIObjectByName("mapBrowserSelectedName");
this.mapBrowserSelectedPreview = Engine.GetGUIObjectByName("mapBrowserSelectedPreview");
this.mapBrowserSelectedDescription = Engine.GetGUIObjectByName("mapBrowserSelectedDescription");
let computedSize = this.mapBrowserSelectedPreview.getComputedSize();
let top = this.mapBrowserSelectedName.size.bottom;
let height = Math.floor((computedSize.right - computedSize.left) / this.ImageRatio);
{
let size = this.mapBrowserSelectedPreview.size;
size.top = top;
size.bottom = top + height;
this.mapBrowserSelectedPreview.size = size;
}
{
let size = this.mapBrowserSelectedDescription.size;
size.top = top + height + 10;
this.mapBrowserSelectedDescription.size = size;
}
gridBrowser.registerSelectionChangeHandler(this.onSelectionChange.bind(this));
}
onSelectionChange()
{
let map = this.gridBrowser.mapList[this.gridBrowser.selected];
if (!map)
return;
this.mapBrowserSelectedName.caption = map ? map.name : "";
this.mapBrowserSelectedDescription.caption = map ? map.description : "";
this.mapBrowserSelectedPreview.sprite =
this.mapCache.getMapPreview(
this.mapBrowserPage.controls.MapFiltering.getSelectedMapType(),
map.file);
}
};
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<object>
<object name="mapBrowserSelectedPreview" type="image" size="0 0 100% 0">
<object name="mapBrowserSelectedName" type="text" style="ModernLabelText" text_valign="bottom" font="sans-bold-16" size="0 0 100% 100%"/>
</object>
<object name="mapBrowserSelectedDescription" type="text" style="ModernText" size="10 0 100%-10 100%-40"/>
</object>
@@ -0,0 +1,70 @@
MapBrowserPageControls.prototype.MapFiltering = class
{
constructor(mapBrowserPage, gridBrowser)
{
this.mapBrowserPage = mapBrowserPage;
this.gridBrowser = gridBrowser;
this.mapFilters = mapBrowserPage.mapFilters;
this.searchBox = new LabelledInput("mapBrowserSearchBox")
.setupEvents(() => this.onChange());
this.mapType = new LabelledDropdown("mapBrowserMapType")
.setupEvents(() => this.onMapTypeChange());
this.mapFilter = new LabelledDropdown("mapBrowserMapFilter")
.setupEvents(() => this.onChange());
mapBrowserPage.registerOpenPageHandler(() => this.onOpenPage());
mapBrowserPage.registerClosePageHandler(() => this.onClosePage());
this.searchBox.blur();
}
onOpenPage()
{
// setTimeout avoids having the hotkey key inserted into the input text.
setTimeout(() => this.searchBox.focus(), 0);
this.renderMapFilter();
this.mapFilter.select(g_GameAttributes.mapFilter);
this.mapType.render(g_MapTypes.Title, g_MapTypes.Name);
this.mapType.select(g_GameAttributes.mapType);
}
onClosePage()
{
this.searchBox.blur();
}
onMapTypeChange()
{
this.renderMapFilter();
this.onChange();
}
onChange()
{
this.gridBrowser.updateMapList();
this.gridBrowser.goToPageOfSelected();
}
renderMapFilter()
{
let filters = this.mapFilters.getAvailableMapFilters(this.getSelectedMapType());
this.mapFilter.render(filters.map(f => f.Title), filters.map(f => f.Name));
}
// TODO: would be nicer to store this state somewhere else.
getSearchText()
{
return this.searchBox.getText() || "";
}
getSelectedMapType()
{
return this.mapType.getSelected() || g_GameAttributes.mapType;
}
getSelectedMapFilter()
{
return this.mapFilter.getSelected() || g_GameAttributes.mapFilter;
}
};
@@ -0,0 +1,55 @@
MapBrowserPageControls.prototype.Pagination = class
{
constructor(mapBrowserPage, gridBrowser)
{
this.status = Engine.GetGUIObjectByName("mapBrowserPageStatus");
this.previous = Engine.GetGUIObjectByName("mapBrowserPreviousButton");
this.next = Engine.GetGUIObjectByName("mapBrowserNextButton");
this.zoomIn = Engine.GetGUIObjectByName("mapsZoomIn");
this.zoomOut = Engine.GetGUIObjectByName("mapsZoomOut");
this.gridBrowser = gridBrowser;
this.gridBrowser.registerPageChangeHandler(() => this.render());
this.gridBrowser.registerGridResizeHandler(() => this.render());
this.setup();
this.render();
}
setup()
{
this.previous.onPress = () => this.gridBrowser.previousPage();
this.next.onPress = () => this.gridBrowser.nextPage();
this.previous.caption = "←";
this.next.caption = "→";
this.previous.tooltip = translate("Go to the previous page.");
this.next.tooltip = translate("Go to the next page.");
this.zoomIn.onPress = () => this.gridBrowser.increaseColumnCount(-1);
this.zoomOut.onPress = () => this.gridBrowser.increaseColumnCount(1);
this.zoomIn.tooltip = translate("Increase map preview size.");
this.zoomOut.tooltip = translate("Decrease map preview size.");
}
render()
{
this.status.caption =
sprintf(translate("Maps: %(mapCount)s"), {
"mapCount": this.gridBrowser.itemCount
}) +
" " +
sprintf(translate("Page: %(currentPage)s/%(maxPage)s"), {
"currentPage": this.gridBrowser.currentPage + 1,
"maxPage": Math.max(1, this.gridBrowser.pageCount)
});
this.previous.enabled = this.gridBrowser.pageCount > 1;
this.next.enabled = this.gridBrowser.pageCount > 1;
this.zoomIn.enabled = this.gridBrowser.columnCount > 0;
this.zoomOut.enabled = this.gridBrowser.columnCount < this.gridBrowser.maxColumns;
}
};
@@ -0,0 +1,145 @@
/**
* Class that arranges a grid of items using paging.
*
* Needs an object as container with items and a object to display the page numbering (if not
* make hidden object and assign it to that).
*/
class GridBrowser
{
constructor(container)
{
this.container = container;
// These properties may be read from publicly.
this.pageCount = undefined;
this.currentPage = undefined;
this.columnCount = undefined;
this.minColumns = undefined;
this.maxColumns = undefined;
this.rowCount = undefined;
this.itemCount = undefined;
this.itemsPerPage = undefined;
this.selected = undefined;
this.gridResizeHandlers = new Set();
this.pageChangeHandlers = new Set();
this.selectionChangeHandlers = new Set();
}
registerGridResizeHandler(handler)
{
this.gridResizeHandlers.add(handler);
}
registerPageChangeHandler(handler)
{
this.pageChangeHandlers.add(handler);
}
registerSelectionChangeHandler(handler)
{
this.selectionChangeHandlers.add(handler);
}
// Inheriting classes must subscribe to this event.
onWindowResized()
{
this.resizeGrid();
this.goToPageOfSelected();
}
setSelectedIndex(index)
{
this.selected = index;
for (let handler of this.selectionChangeHandlers)
handler();
}
goToPage(pageNumber)
{
if (!Number.isInteger(pageNumber))
throw new Error("Given argument is not a number");
this.currentPage = pageNumber;
for (let handler of this.pageChangeHandlers)
handler();
}
nextPage(wrapAround = true)
{
let numberPages = Math.max(1, this.pageCount);
if (!wrapAround)
this.goToPage(Math.min(this.currentPage + 1, numberPages - 1));
else
this.goToPage((this.currentPage + 1) % numberPages);
}
previousPage(wrapAround = true)
{
let numberPages = Math.max(1, this.pageCount);
if (!wrapAround)
this.goToPage(Math.max(this.currentPage - 1, 0));
else
this.goToPage((this.currentPage + numberPages - 1) % numberPages);
}
goToPageOfSelected()
{
this.goToPage(
Math.max(Math.min(
Math.floor(this.selected / this.itemsPerRow) - Math.floor(this.rowCount / 2),
this.pageCount-1),
0)
);
}
increaseColumnCount(diff)
{
let isSelectedInPage =
this.selected !== undefined &&
Math.floor(this.selected / this.itemsPerRow) >= this.currentPage &&
Math.floor(this.selected / this.itemsPerRow) < this.currentPage + this.rowCount;
this.columnCount += diff;
this.resizeGrid();
if (isSelectedInPage)
this.goToPageOfSelected();
else
this.goToPage(Math.min(this.currentPage, Math.max(0, this.pageCount - 1)));
}
resizeGrid()
{
let size = this.container.getComputedSize();
let width = size.right - size.left;
let height = size.bottom - size.top;
let maxColumns = Math.floor(width / this.MinItemWidth);
if (maxColumns <= 0)
return;
if (this.columnCount === undefined)
this.columnCount = Math.floor(width / this.DefaultItemWidth);
this.minColumns = Math.ceil(width / (height * this.ItemRatio));
this.maxColumns = maxColumns;
this.columnCount = Math.min(this.maxColumns, Math.max(this.minColumns, this.columnCount));
this.itemWidth = Math.floor(width / this.columnCount);
this.itemHeight = Math.floor(this.itemWidth / this.ItemRatio);
this.rowCount = Math.floor((size.bottom - size.top) / this.itemHeight);
this.itemsPerRow = Math.min(this.columnCount, this.items.length);
this.itemsPerPage = Math.min(this.columnCount * this.rowCount, this.items.length);
// NB: pages only change by one row, so items are in several pages.
this.pageCount = Math.ceil(this.itemCount / this.itemsPerRow) - this.rowCount + 1;
for (let handler of this.gridResizeHandlers)
handler();
}
}
@@ -0,0 +1,44 @@
class GridBrowserItem
{
constructor(gridBrowser, imageObject, itemIndex)
{
this.gridBrowser = gridBrowser;
this.itemIndex = itemIndex;
this.imageObject = imageObject;
imageObject.onMouseLeftPress = this.select.bind(this);
imageObject.onMouseWheelDown = () => gridBrowser.nextPage(false);
imageObject.onMouseWheelUp = () => gridBrowser.previousPage(false);
gridBrowser.registerGridResizeHandler(this.onGridResize.bind(this));
gridBrowser.registerPageChangeHandler(this.updateVisibility.bind(this));
}
updateVisibility()
{
this.imageObject.hidden =
this.itemIndex >= Math.min(
this.gridBrowser.itemsPerPage,
this.gridBrowser.itemCount - this.gridBrowser.currentPage * this.gridBrowser.itemsPerRow);
}
onGridResize()
{
let gridBrowser = this.gridBrowser;
let x = this.itemIndex % gridBrowser.columnCount;
let y = Math.floor(this.itemIndex / gridBrowser.columnCount);
let size = this.imageObject.size;
size.left = gridBrowser.itemWidth * x;
size.right = gridBrowser.itemWidth * (x + 1);
size.top = gridBrowser.itemHeight * y;
size.bottom = gridBrowser.itemHeight * (y + 1);
this.imageObject.size = size;
this.updateVisibility();
}
select()
{
this.gridBrowser.setSelectedIndex(
this.itemIndex + this.gridBrowser.currentPage * this.gridBrowser.itemsPerRow);
}
}
@@ -0,0 +1,102 @@
class MapGridBrowser extends GridBrowser
{
constructor(mapBrowserPage, setupWindow)
{
super(Engine.GetGUIObjectByName("mapBrowserContainer"));
this.setupWindow = setupWindow;
this.mapBrowserPage = mapBrowserPage;
this.mapCache = mapBrowserPage.mapCache;
this.mapFilters = mapBrowserPage.mapFilters;
this.mapList = [];
this.items = this.container.children.map((imageObject, itemIndex) =>
new MapGridBrowserItem(mapBrowserPage, this, imageObject, itemIndex));
this.mapBrowserPage.registerOpenPageHandler(this.onOpenPage.bind(this));
this.mapBrowserPage.registerClosePageHandler(this.onClosePage.bind(this));
this.mapBrowserPage.mapBrowserPageDialog.onMouseWheelUp = this.nextPage.bind(this);
this.mapBrowserPage.mapBrowserPageDialog.onMouseWheelDown = this.previousPage.bind(this);
}
onOpenPage()
{
this.updateMapList();
this.setSelectedIndex(this.mapList.findIndex(map => map.file == g_GameAttributes.map));
this.goToPageOfSelected();
this.container.onWindowResized = this.onWindowResized.bind(this);
Engine.SetGlobalHotkey(this.HotkeyConfigNext, "Press", this.nextPage.bind(this));
Engine.SetGlobalHotkey(this.HotkeyConfigPrevious, "Press", this.previousPage.bind(this));
}
onClosePage()
{
delete this.container.onWindowResized;
Engine.UnsetGlobalHotkey(this.HotkeyConfigNext, "Press");
Engine.UnsetGlobalHotkey(this.HotkeyConfigPrevious, "Press");
}
updateMapList()
{
let selectedMap =
this.mapList[this.selected] &&
this.mapList[this.selected].file || undefined;
let mapList = this.mapFilters.getFilteredMaps(
this.mapBrowserPage.controls.MapFiltering.getSelectedMapType(),
this.mapBrowserPage.controls.MapFiltering.getSelectedMapFilter());
let filterText = this.mapBrowserPage.controls.MapFiltering.getSearchText();
if (filterText)
{
mapList = MatchSort.get(filterText, mapList, "name");
if (!mapList.length)
{
let filter = "all";
for (let type of g_MapTypes.Name)
for (let map of this.mapFilters.getFilteredMaps(type, filter))
mapList.push(Object.assign({ "type": type, "filter": filter }, map));
mapList = MatchSort.get(filterText, mapList, "name");
}
}
if (this.mapBrowserPage.controls.MapFiltering.getSelectedMapType() == "random")
{
mapList = [{
"file": "random",
"name": "Random",
"description": "Pick a map at random.",
}, ...mapList];
}
this.mapList = mapList;
this.itemCount = this.mapList.length;
this.resizeGrid();
this.setSelectedIndex(this.mapList.findIndex(map => map.file == selectedMap));
}
submitMapSelection()
{
if (!g_IsController)
return;
let map = this.mapList[this.selected] || undefined;
if (!map)
return;
g_GameAttributes.mapType = map.type ? map.type :
this.mapBrowserPage.controls.MapFiltering.getSelectedMapType();
g_GameAttributes.mapFilter = map.filter ? map.filter :
this.mapBrowserPage.controls.MapFiltering.getSelectedMapFilter();
g_GameAttributes.map = map.file;
this.setupWindow.controls.gameSettingsControl.updateGameAttributes();
}
}
MapGridBrowser.prototype.ItemRatio = 4 / 3;
MapGridBrowser.prototype.DefaultItemWidth = 200;
MapGridBrowser.prototype.MinItemWidth = 100;
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<object name="mapBrowserContainer">
<repeat count="100" var="n">
<object
name="map[n]"
type="button"
size="0 0 0 0"
sprite_over="color: 255 0 0 100"
sprite_pressed="color: 255 0 0 150"
>
<object ghost="true" type="image" sprite="ModernDarkBoxGold" size="3 3 100%-3 100%-3">
<object
name="mapPreview[n]"
style="ModernLabelText"
type="button"
size="1 1 100%-1 100%-1"
text_align="center"
text_valign="bottom"
buffer_zone="10"
/>
</object>
</object>
</repeat>
</object>
@@ -0,0 +1,64 @@
class MapGridBrowserItem extends GridBrowserItem
{
constructor(mapBrowserPage, mapGridBrowser, imageObject, itemIndex)
{
super(mapGridBrowser, imageObject, itemIndex);
this.mapBrowserPage = mapBrowserPage;
this.mapCache = mapBrowserPage.mapCache;
this.mapPreview = Engine.GetGUIObjectByName("mapPreview[" + itemIndex + "]");
mapGridBrowser.registerSelectionChangeHandler(this.onSelectionChange.bind(this));
mapGridBrowser.registerPageChangeHandler(this.onGridResize.bind(this));
if (g_IsController)
this.imageObject.onMouseLeftDoubleClick = this.onMouseLeftDoubleClick.bind(this);
}
onSelectionChange()
{
this.updateSprite();
}
onGridResize()
{
super.onGridResize();
this.updateMapAssignment();
this.updateSprite();
}
updateSprite()
{
this.imageObject.sprite =
this.gridBrowser.selected == this.itemIndex + this.gridBrowser.currentPage * this.gridBrowser.itemsPerRow ?
this.SelectedSprite :
"";
}
updateMapAssignment()
{
let map = this.gridBrowser.mapList[
this.itemIndex + this.gridBrowser.currentPage * this.gridBrowser.itemsPerRow] || undefined;
if (!map)
return;
this.mapPreview.caption = map.name;
this.imageObject.tooltip =
map.description + "\n" +
this.gridBrowser.container.tooltip;
this.mapPreview.sprite =
this.mapCache.getMapPreview(this.mapBrowserPage.controls.MapFiltering.getSelectedMapType(), map.file);
}
onMouseLeftDoubleClick()
{
this.gridBrowser.submitMapSelection();
this.mapBrowserPage.closePage();
}
}
MapGridBrowserItem.prototype.SelectedSprite = "color: 120 0 0 255";
@@ -0,0 +1,92 @@
/**
* Take ownership of a Control/Label setup, and resize them horizontally
* depending on the size of the label.
* TODO: we should let JS components like that generate their XML
* so they can be easily reused, and then move this to another folder.
*/
class LabelledControl
{
constructor(guiObjectName)
{
this.control = Engine.GetGUIObjectByName(guiObjectName + "Control");
this.label = Engine.GetGUIObjectByName(guiObjectName + "Label");
this.resizeLabel();
}
setupEvents()
{
return this;
}
resizeLabel()
{
let labelWidth = Engine.GetTextWidth(this.label.font, this.label.caption) + 15;
{
let size = this.label.size;
size.right = labelWidth;
this.label.size = size;
}
{
let size = this.control.size;
size.left = labelWidth;
this.control.size = size;
}
}
}
class LabelledDropdown extends LabelledControl
{
setupEvents(onSelectionChange)
{
this.control.onSelectionChange = onSelectionChange;
return this;
}
render(names, data)
{
let selected = this.getSelected();
this.control.list = names;
this.control.list_data = data;
this.select(selected);
}
select(data)
{
this.control.selected = this.control.list_data.indexOf(data);
}
getSelected()
{
if (this.control.selected === -1)
return undefined;
return this.control.list_data[this.control.selected];
}
}
class LabelledInput extends LabelledControl
{
setupEvents(onTextEdit, onTab = () => this.blur())
{
this.control.onTab = onTab;
this.control.onTextEdit = onTextEdit;
return this;
}
focus()
{
this.control.focus();
// focus resets cursor position
this.control.buffer_position = this.control.caption.length;
}
blur()
{
this.control.blur();
}
getText()
{
return this.control.caption.trim();
}
}
@@ -0,0 +1,85 @@
const MatchSort = (function() {
const Highscore = -10E7;
return class
{
/**
* Returns a new list filtered and sorted by the similarity with the input text
* Order of sorting:
* 1. Exact match
* 2. Exact lowercase match
* 3. Starting letters match and sorted alphabetically
* 4. By similarity score (lookahead match)
* 5. Entry is discarded if one of the previous don't apply
*
* @param {string} input text to seach for
* @param {string[] | object[]} list
* @param {string} [key] text to use if the list is made up of objects
*/
static get(input, list, key)
{
input = input.toLowerCase();
let result = [];
for (let obj of list)
{
let text = key == null ? obj : obj[key];
let score = MatchSort.scoreText(input, text);
if (score !== undefined)
result.push([obj, score, text, text.startsWith(input)]);
}
return result.sort(MatchSort.sort).map(v => v[0]);
}
static sort([o1, s1, t1, a1], [o2, s2, t2, a2])
{
if (a1 && a2)
return t1.localeCompare(t2);
if (a1)
return -1;
if (a2)
return 1;
return s1 - s2;
}
/**
* The lower the score the better the match.
*/
static scoreText(input, text)
{
// Exact match.
if (input == text)
return Highscore;
text = text.toLowerCase();
// Exact match relaxed.
if (input == text)
return Highscore / 2;
let score = 0;
let offset = -1;
for (let i = 0; i < input.length; ++i)
{
let offsetNext = text.indexOf(input[i], offset + 1);
// No match.
if (offsetNext == -1)
return undefined;
// Lower score increase if consecutive index.
let isConsecutive = offsetNext == offset + 1 ? 0 : 1;
score += offsetNext + isConsecutive * offsetNext;
offset = offsetNext;
}
return score;
}
};
})();
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<page>
<include>common/modern/setup.xml</include>
<include>common/modern/styles.xml</include>
<include>common/modern/sprites.xml</include>
<include>common/setup.xml</include>
<include>common/sprites.xml</include>
<include>common/styles.xml</include>
<include>maps/mapbrowser/MapBrowserPage.xml</include>
<!-- Work around a render bug:
Needs to be drawn last, as it should overlay everything -->
<include>common/global.xml</include>
</page>
@@ -43,6 +43,13 @@ var g_MainMenuItems = [
};
Engine.PushGuiPage("page_civinfo.xml", {}, callback);
}
},
{
"caption": translate("Map Overview"),
"tooltip": translate("View the different maps featured in 0 A.D."),
"onPress": () => {
Engine.PushGuiPage("page_mapbrowser.xml");
},
}
]
},
@@ -3,6 +3,7 @@
<objects>
<script directory="gui/common/"/>
<script directory="gui/maps/"/>
<script directory="gui/replaymenu/" />
<!-- Everything displayed in the replay menu. -->
@@ -3,6 +3,7 @@
<objects>
<script directory="gui/common/"/>
<script directory="gui/maps/"/>
<script directory="gui/session/"/>
<script directory="gui/session/chat/"/>
<script directory="gui/session/developer_overlay/"/>