Index: ps/trunk/binaries/data/mods/public/gui/hotkeys/HotkeyMetadata.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/hotkeys/HotkeyMetadata.js +++ ps/trunk/binaries/data/mods/public/gui/hotkeys/HotkeyMetadata.js @@ -0,0 +1,67 @@ +/** + * + */ +class HotkeyMetadata +{ + constructor() + { + this.DEFAULT_CATEGORY = "other"; + this.categories = {}; + this.hotkeys = {}; + + this.parseSpec(); + } + + parseSpec() + { + let files = this.getFiles(); + let hotkey_i = 0; + let categories = { + [this.DEFAULT_CATEGORY]: { + "name": markForTranslation("Other Hotkeys"), + "desc": markForTranslation("Other Hotkeys"), + } + }; + for (let file of files) + { + let data = Engine.ReadJSONFile(file); + if (data.categories) + for (let cat in data.categories) + categories[cat] = data.categories[cat]; + if (data.hotkeys) + for (let hotkey in data.hotkeys) + { + this.hotkeys[hotkey] = data.hotkeys[hotkey]; + this.hotkeys[hotkey].order = hotkey_i++; + this.hotkeys[hotkey].categories = data.hotkeys[hotkey].categories || [this.DEFAULT_CATEGORY]; + } + if (data.mapped_hotkeys) + for (let cat in data.mapped_hotkeys) + for (let hotkey in data.mapped_hotkeys[cat]) + { + if (data.mapped_hotkeys[cat][hotkey].categories) + warn("Categories will be overwritten for mapped hotkey " + hotkey); + this.hotkeys[hotkey] = data.mapped_hotkeys[cat][hotkey]; + this.hotkeys[hotkey].order = hotkey_i++; + this.hotkeys[hotkey].categories = [cat]; + } + } + // Sort categories (JS objects are (in this case) sorted by insertion order). + this.categories = {}; + let keys = Object.keys(categories).sort((a, b) => { + if (a === this.DEFAULT_CATEGORY || b === this.DEFAULT_CATEGORY) + return a === this.DEFAULT_CATEGORY ? 1 : -1; + if (categories[a].order === undefined || categories[b].order === undefined) + return categories[a].order === undefined ? -1 : 1; // Likely to keep alphabetical order. + return categories[a].order - categories[b].order; + }); + for (let key of keys) + this.categories[key] = categories[key]; + // TODO: validate that categories exist. + } + + getFiles() + { + return Engine.ListDirectoryFiles("gui/hotkeys/spec/", "*.json"); + } +} Index: ps/trunk/binaries/data/mods/public/gui/hotkeys/HotkeyPicker.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/hotkeys/HotkeyPicker.js +++ ps/trunk/binaries/data/mods/public/gui/hotkeys/HotkeyPicker.js @@ -4,8 +4,9 @@ */ class HotkeyPicker { - constructor(onClose, name, combinations) + constructor(metadata, onClose, name, combinations) { + this.metadata = metadata; this.name = name; this.combinations = combinations; this.window = Engine.GetGUIObjectByName("hotkeyPicker"); @@ -13,7 +14,16 @@ this.enteringInput = -1; - Engine.GetGUIObjectByName("hotkeyPickerTitle").caption = translate(this.name); + if (this.metadata.hotkeys[name]) + { + Engine.GetGUIObjectByName("hotkeyPickerTitle").caption = translate(this.metadata.hotkeys[name].name); + Engine.GetGUIObjectByName("hotkeyPickerDescHotkey").caption = translate(this.metadata.hotkeys[name].desc); + } + else + { + Engine.GetGUIObjectByName("hotkeyPickerTitle").caption = this.name; + Engine.GetGUIObjectByName("hotkeyPickerDescHotkey").hidden = true; + } this.setupCombinations(); this.render(); @@ -41,8 +51,8 @@ for (let i = 0; i < 4; ++i) { let s = Engine.GetGUIObjectByName("combination[" + i + "]").size; - s.top = +i * 60 + 90; - s.bottom = +i * 60 + 120; + s.top = +i * 60 + 120; + s.bottom = +i * 60 + 150; Engine.GetGUIObjectByName("combination[" + i + "]").size = s; Engine.GetGUIObjectByName("combNb[" + i + "]").caption = sprintf(translate("#%i"), i); Index: ps/trunk/binaries/data/mods/public/gui/hotkeys/HotkeysPage.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/hotkeys/HotkeysPage.js +++ ps/trunk/binaries/data/mods/public/gui/hotkeys/HotkeysPage.js @@ -1,16 +1,33 @@ class HotkeysPage { - constructor() + constructor(metadata) { + this.metadata = metadata; + Engine.GetGUIObjectByName("hotkeyList").onMouseLeftDoubleClickItem = () => { let idx = Engine.GetGUIObjectByName("hotkeyList").selected; let picker = new HotkeyPicker( + this.metadata, this.onHotkeyPicked.bind(this), Engine.GetGUIObjectByName("hotkeyList").list_data[idx], clone(this.hotkeys[Engine.GetGUIObjectByName("hotkeyList").list_data[idx]]) ); }; + Engine.GetGUIObjectByName("hotkeyList").onHoverChange = () => this.onHoverChange(); + Engine.GetGUIObjectByName("hotkeyFilter").onSelectionChange = () => this.setupHotkeyList(); + Engine.GetGUIObjectByName("hotkeyFilter").onHoverChange = () => { + let dropdown = Engine.GetGUIObjectByName("hotkeyFilter"); + if (dropdown.hovered === -1) + dropdown.tooltip = ""; + else + { + if (dropdown.list_data[dropdown.hovered] == -1) + dropdown.tooltip = ""; + else + dropdown.tooltip = translate(this.categories[dropdown.list_data[dropdown.hovered]].desc); + } + }; Engine.GetGUIObjectByName("hotkeyTextFilter").onTextEdit = () => this.setupHotkeyList(); @@ -29,12 +46,39 @@ this.setupHotkeyList(); } + setupHotkeyData() + { + let hotkeydata = Engine.GetHotkeyMap(); + this.hotkeys = hotkeydata; + let categories = clone(this.metadata.categories); + for (let name in categories) + categories[name].hotkeys = []; + for (let hotkeyName in this.hotkeys) + { + if (this.metadata.hotkeys[hotkeyName]) + for (let cat of this.metadata.hotkeys[hotkeyName].categories) + categories[cat].hotkeys.push(hotkeyName); + else + categories[this.metadata.DEFAULT_CATEGORY].hotkeys.push(hotkeyName); + } + for (let cat in categories) + categories[cat].hotkeys.sort((a, b) => { + if (!this.metadata.hotkeys[a] || !this.metadata.hotkeys[b]) + return !this.metadata.hotkeys[a] ? 1 : -1; + return this.metadata.hotkeys[a].order - this.metadata.hotkeys[b].order; + }); + for (let cat in categories) + if (categories[cat].hotkeys.length === 0) + delete categories[cat]; + this.categories = categories; + } + setupFilters() { let dropdown = Engine.GetGUIObjectByName("hotkeyFilter"); let names = []; for (let cat in this.categories) - names.push(this.categories[cat].label); + names.push(translate(this.categories[cat].name)); dropdown.list = [translate("All Hotkeys")].concat(names); dropdown.list_data = [-1].concat(Object.keys(this.categories)); dropdown.selected = 0; @@ -45,28 +89,34 @@ let hotkeyList = Engine.GetGUIObjectByName("hotkeyList"); hotkeyList.selected = -1; let textFilter = Engine.GetGUIObjectByName("hotkeyTextFilter").caption; + + let hotkeys; let dropdown = Engine.GetGUIObjectByName("hotkeyFilter"); if (dropdown.selected && dropdown.selected !== 0) - { - let category = this.categories[dropdown.list_data[dropdown.selected]]; - // This is inefficient but it seems fast enough. - let hotkeys = category.hotkeys.filter(x => translate(x[0]).indexOf(textFilter) !== -1); - hotkeyList.list_name = hotkeys.map(x => translate(x[0])); - hotkeyList.list_mapping = hotkeys.map(x => formatHotkeyCombinations(x[1])); - hotkeyList.list = hotkeys.map(() => 0); - hotkeyList.list_data = hotkeys.map(x => x[0]); - } + hotkeys = this.categories[dropdown.list_data[dropdown.selected]].hotkeys; + else + hotkeys = Object.values(this.categories).map(x => x.hotkeys).flat(); + hotkeys = hotkeys.filter(x => { + return x.indexOf(textFilter) !== -1 || + translate(this.metadata.hotkeys[x]?.name || x).indexOf(textFilter) !== -1; + }); + + hotkeyList.list_name = hotkeys.map(x => translate(this.metadata.hotkeys[x]?.name || x)); + hotkeyList.list_mapping = hotkeys.map(x => formatHotkeyCombinations(this.hotkeys[x])); + hotkeyList.list = hotkeys.map(() => 0); + hotkeyList.list_data = hotkeys.map(x => x); + } + + onHoverChange() + { + let hotkeyList = Engine.GetGUIObjectByName("hotkeyList"); + if (hotkeyList.hovered === -1) + hotkeyList.tooltip = ""; else { - // TODO SM62+ : refactor using flat() - let flattened = []; - for (let cat in this.categories) - flattened = flattened.concat(this.categories[cat].hotkeys); - flattened = flattened.filter(x => translate(x[0]).indexOf(textFilter) !== -1); - hotkeyList.list_name = flattened.map(x => translate(x[0])); - hotkeyList.list_mapping = flattened.map(x => formatHotkeyCombinations(x[1])); - hotkeyList.list = flattened.map(() => 0); - hotkeyList.list_data = flattened.map(x => x[0]); + let hotkey = hotkeyList.list_data[hotkeyList.hovered]; + let tooltip = this.metadata.hotkeys[hotkey]?.desc || markForTranslation("No tooltip available"); + hotkeyList.tooltip = translate(tooltip); } } @@ -94,48 +144,6 @@ this.setupHotkeyList(); } - setupHotkeyData() - { - let hotkeydata = Engine.GetHotkeyMap(); - this.hotkeys = hotkeydata; - - let categories = { - "other": { - "label": translate("Other hotkeys"), - "hotkeys": [] - } - }; - let n_categories = 1; - for (let hotkeyName in this.hotkeys) - { - let category = "other"; - let firstdot = hotkeyName.indexOf('.'); - if (firstdot !== -1) - category = hotkeyName.substr(0, firstdot); - if (!(category in categories)) - { - if (n_categories > 18) - category = "other"; - categories[category] = { - "label": category, - "hotkeys": [] - }; - } - categories[category].hotkeys.push([hotkeyName, this.hotkeys[hotkeyName]]); - } - // Remove categories that are too small to deserve a tab. - for (let cat of Object.keys(categories)) - if (categories[cat].hotkeys.length < 3) - { - categories.other.hotkeys = categories.other.hotkeys.concat(categories[cat].hotkeys); - delete categories[cat]; - } - for (let cat in categories) - categories[cat].hotkeys = categories[cat].hotkeys.sort(); - - this.categories = categories; - } - resetUserHotkeys() { messageBox( @@ -177,7 +185,7 @@ } -function init(data) +function init() { - let hotkeyPage = new HotkeysPage(data); + let hotkeyPage = new HotkeysPage(new HotkeyMetadata()); } Index: ps/trunk/binaries/data/mods/public/gui/hotkeys/hotkeys.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/hotkeys/hotkeys.xml +++ ps/trunk/binaries/data/mods/public/gui/hotkeys/hotkeys.xml @@ -66,13 +66,17 @@