#strict 3 static const DT_Menu = MN7I; static const DT_Menu_MenuVar = -1; // Enums and bitfields static const Menu_KeepOpen_Not = 0x0; static const Menu_KeepOpen_Keep = 0x1; static const Menu_KeepOpen_Force = 0x2; static const Menu_KeepOpen_Permanent = 0x4; static const Menu_KeepOpen_Refresh = 0x8; static const Menu_KeepOpen_RefreshContinuously = 0x10; static const Menu_KeepOpen_Refresh_Mask = 0x18; // Menu_KeepOpen_Refresh | Menu_KeepOpen_RefreshContinuously static const DT_Menu_Type_Entry = 0; static const DT_Menu_Type_Factory = 1; static const DT_Menu_Type_Columns = 2; static const DT_Menu_Action_Normal = 0; static const DT_Menu_Action_Special2 = 1; static const DT_Menu_Action_Close = 2; static const Menu_React_Close = 1; static const Menu_React_Refresh = 2; static const Menu_React_KeepOpen = 3; static const Menu_React_Back = 4; static const Menu_React_SelectionOffset = 5; static const Menu_React_SelectionChange = 6; static const Menu_React_ShowSubMenu = 7; static const Menu_React_OverrideReaction = -1; static Menu_React_None; // constants with nil is somehow broken? // static const Menu_React_None = nil; static const Menu_React_Max = Menu_React_ShowSubMenu; global func Menu_React_OffsetSelection(int offset) { return [Menu_React_SelectionOffset, offset]; } global func Menu_React_OverrideSelection(int override) { return [Menu_React_SelectionChange, override]; } global func Menu_React_Override(override) { return [Menu_React_OverrideReaction, override]; } static const Menu_ConditionReact_Default = 0; static const Menu_ConditionReact_Show = 1; static const Menu_ConditionReact_Hide = 2; static const Menu_ConditionReact_GrayOut = 3; static const Menu_ConditionReact_CustomFormat = 4; static const Menu_ConditionReact_Max = Menu_ConditionReact_GrayOut; global func Menu_ConditionReact_CustomColor(int color) { return [Menu_ConditionReact_CustomFormat, Format("%%s", color)]; } global func Menu_ConditionReact_Format(string format) { return [Menu_ConditionReact_CustomFormat, format]; } static const Menu_Condition_Default = 0; static const Menu_Condition_AllowSelection = 1; static const Menu_Condition_DenySelection = 2; static const Menu_Condition_Max = Menu_Condition_DenySelection; static const Menu_CallbackType_None = 0x0; static const Menu_CallbackType_Special2 = 0x1; static const Menu_CallbackType_Normal = 0x2; static const Menu_CallbackType_Close = 0x4; static const Menu_CallbackType_Selection = 0x8; static const Menu_CallbackType_Deselection = 0x10; static const Menu_CallbackType_ValueChanged = 0x20; static const Menu_CallbackType_InputAborted = 0x40; static const Menu_CallbackType_Defaults = 0x3; // Menu_CallbackType_Normal | Menu_CallbackType_Special2 static const Menu_CallbackType_All = 0x7f; // Menu_CallbackType_Normal | Menu_CallbackType_Special2 | Menu_CallbackType_Close | Menu_CallbackType_Selection | Menu_CallbackType_Deselection | Menu_CallbackType_ValueChanged | Menu_CallbackType_InputAborted static const Menu_AdaptorType_Boolean = 1; static const Menu_AdaptorType_Integer = 2; static const Menu_AdaptorType_String = 3; static const Menu_AdaptorType_ID = 4; static const Menu_AdaptorType_Enum = 5; static const Menu_AdaptorType_BitField = 6; static const Menu_Adaptor_Limits_Max = 0x7fffffff; static const Menu_Adaptor_Limits_Min = 0x80000000; static const Menu_CallbackArg_All = -1; static const Menu_CallbackArg_Action = 0; static const Menu_CallbackArg_Icon = 1; static const Menu_CallbackArg_MenuObject = 2; static const Menu_CallbackArg_Args = 3; static const Menu_CallbackArg_NewSelection = 4; static const Menu_CallbackArg_OldSelection = 5; static const Menu_CallbackArg_NewValue = 6; static const Menu_CallbackArg_OldValue = 7; static const Menu_CallbackArg_FromSubMenu = 8; static const Menu_CallbackArg_Menu = 9; static const Menu_CallbackArg_Returned = 10; static const Menu_CallbackArg_Menu = 11; static const Menu_CallbackArg_EntryNumber = 12; static const Menu_CallbackArg_EntryColumn = 13; static const Menu_CallbackArg_NewSelectionColumn = 14; static const Menu_CallbackArg_OldSelectionColumn = 15; static const Menu_CallbackArg_Min = Menu_CallbackArg_All; static const Menu_CallbackArg_Max = Menu_CallbackArg_EntryNumber; static const Menu_Selection_Simple = 0; static const Menu_Selection_WithColumn = 1; static const Menu_Selection_SubMenuColumnChain = 2; static const Menu_Layout_Icon = 1; static const Menu_Layout_Description = 2; static const Menu_Layout__TextPos = 3; static const Menu_Layout__ValuePos = 4; // static const Menu_Layout__InputValuePos = 5; TODO static const Menu_Layout_Text = 4; static const Menu_Layout_Value = 8; // static const Menu_Layout_InputValue = 16; TODO static const Menu_Layout__NoFlagMask = 3; // ---------------------------------------------------------------------------- local settings; local createEntries; local currentColumn; local columnCount; local columnEntries; local columnOffsets; local currentColumnSelections; local shouldChangeColumn; local suspended; local closing; local refreshing; local msgBoardMode; local msgBoardEntry; local noSelectionCallbacks; local multiColumnMode; local subMenu; local vars; func Initialize() { currentColumnSelections = [-1]; columnEntries = []; msgBoardEntry = []; vars = []; currentColumn = 0; } func Destruction() { Close(); } func Create(map cSettings) { settings = cSettings.Settings; createEntries = cSettings.Entries; multiColumnMode = false; columnCount = 0; var actualColumnCount = GetLength(currentColumnSelections); columnOffsets = CreateArray(actualColumnCount); for(var i = 0; i < actualColumnCount; ++i) { columnOffsets[i] = 0; } if(settings.Vars) { var settingVars = settings.Vars; for(var i = 0; i < GetLength(settingVars); ++i) { if(settingVars[i]) { vars[i] = settingVars[i]; } } settings.Vars = 0; } var factoryArgs = []; factoryArgs[Menu_CallbackArg_Menu] = this; factoryArgs[Menu_CallbackArg_MenuObject] = settings.Object; var index = 0; columnEntries[0] = []; HandleEntries(createEntries, index, columnEntries[0], settings, factoryArgs, 0, true); if(columnCount > 1 && settings.Style != C4MN_Style_Context) { FatalError("CreateNewMenu: Multi column menus (and submenus as columns) are only supported with Menu_Style_Context()"); } if(settings.Parent) settings.Parent->Suspend(); if(!(settings.Object)) { FatalError("Assertion failed: CreateNewMenu: menu object is missing; assertion code: settings.Object"); } var title = settings.Title; if(!multiColumnMode) { var submenuTitle; // update submenu columns if necessary and build title for(var i = 0; i < GetLength(currentColumnSelections); ++i) { // TODO: cascade? then remove the columnSelection == -1 check; and check if shrinking menus still work correctly if(!columnEntries[i]) { currentColumnSelections[i] = -1; break; } var columnSelection = currentColumnSelections[i]; if((columnSelection == -1) || columnSelection >= GetLength(columnEntries[i])) { currentColumnSelections[i] = GetLength(columnEntries[i]) - 1; break; } var submenuEntries = columnEntries[i][columnSelection].SubMenuColumnEntries; if(submenuEntries) { columnEntries[i + 1] = submenuEntries; var submenuSettings = columnEntries[i][columnSelection].SubMenuInitColumnData.Settings; /*var */submenuTitle = submenuSettings.Title || ""; if(submenuSettings.Icon) { submenuTitle = Format("{{%i}} %s", submenuSettings.Icon, submenuTitle); } // because of the engine such long titles are not nice; maybe do "... > last SubMenu Title" // the engine makes entry columns at least as long as the title is... // title = Format("%s {{MN7I:7}} %s", title, submenuTitle); } } if(submenuTitle) { title = Format("... {{MN7I:7}} %s", submenuTitle); } } CreateMenu(settings.Icon, settings.Object, this, settings.Extra, title, settings.ExtraData, settings.Style, true, MN7I); if(settings.Size) SetMenuSize(settings.Size[0], settings.Size[1], settings.Object); AddEntries(); if(GetLength(columnEntries[0]) > 0) { if(!refreshing) { SelectEntry(settings.Selection[0], settings.Selection[1]); } } else { Log("WARNING: DT_Menu::Create: Created an empty menu"); } if(settings.Decoration) { SetMenuDecoration(settings.Decoration, settings.Object); } if(!GetEffect("Menu", this)) AddEffect("Menu", this, 1, 1, this, 0); } func LeaveSubMenuColumn() { if(!(!multiColumnMode && currentColumn > 0)) { FatalError("Assertion failed: LeaveSubMenuColumn: currentColumn is not a submenu; can't leave; assertion code: !multiColumnMode && currentColumn > 0"); } SelectEntry(currentColumnSelections[currentColumn - 1], currentColumn - 1); Refresh(GetSelection(Menu_Selection_WithColumn)); } func SelectEntry(indexOrChain, int column) { if(GetType(indexOrChain) == C4V_Array) { if(!(GetLength(indexOrChain) <= columnCount)) { FatalError("Assertion failed: SelectEntry: index chain is longer than there are columns; assertion code: GetLength(indexOrChain) <= columnCount"); } for(var i = 0; i < GetLength(indexOrChain); ++i) { SelectEntry(indexOrChain[i], i); } } else if(!indexOrChain || GetType(indexOrChain) == C4V_Int) { column ??= 0; if(!(column >= 0 && column < GetLength(columnEntries) && GetType(columnEntries[column]) == C4V_Array)) { FatalError("Assertion failed: SelectEntry: column index out of range; assertion code: column >= 0 && column < GetLength(columnEntries) && GetType(columnEntries[column]) == C4V_Array"); } indexOrChain ??= 0; var entryCount = GetLength(columnEntries[column]); if(indexOrChain < 0) indexOrChain = entryCount + indexOrChain; if(!(indexOrChain >= 0 && indexOrChain < entryCount)) { FatalError("Assertion failed: SelectEntry: selection index out of range; assertion code: indexOrChain >= 0 && indexOrChain < entryCount"); } shouldChangeColumn = true; SelectMenuItem(EncodeSelection(indexOrChain, column), settings.Object); shouldChangeColumn = false; } else { FatalError(Format("DT_Menu::SelectEntry: indexOrChain must be an integer or an array of integers but got %d", GetType(indexOrChain))); } } func SelectTopEntry(int column) { for(var i = 0; i < GetLength(columnEntries[column]); ++i) { if(columnEntries[column][i].Placeholder) { return SelectEntry(i, column); } } } func SelectBottomEntry(int column) { for(var i = GetLength(columnEntries[column]) - 1; i >= 0; --i) { if(columnEntries[column][i].Placeholder) { return SelectEntry(i, column); } } } func ActivateEntry(int action, indexOrChain, int column) { SelectEntry(indexOrChain); var index = indexOrChain; if(GetType(indexOrChain) == C4V_Array) { column = GetLength(indexOrChain) - 1; index = indexOrChain[column]; } var entry = columnEntries[column][index]; MenuItemCommand(entry.Icon, EncodeSelection(index, column), action); } func SubMenu() { return subMenu; } func &Var(int index) { return vars[index]; } global func MenuVar(int index) { return [DT_Menu_MenuVar, index]; } /*func FxMenuStart(object target, int effectNumber, int temp) { }*/ func FxMenuTimer(object target, int effectNumber, int effectTime) { if(!settings.Object) { return Close(); } else if(settings.RequireAction) { var obj = settings.Object; var requirement = settings.RequireAction; if(GetAction(obj) != requirement[0] || (requirement[1] && GetActionTarget(0, obj) != requirement[1])) { return Close(); } } if(suspended) return; if(msgBoardMode && !TestMessageBoard(GetOwner(settings.Object), true)) { var entry = columnEntries[msgBoardEntry[1]][msgBoardEntry[0]]; var args = entry.Args; var reaction = CallCallbacks(args?.Callbacks, Menu_CallbackType_InputAborted, [Menu_CallbackType_InputAborted, entry.Icon, settings.Object, entry.Args]); if(reaction != Menu_React_None) { React(reaction, msgBoardEntry); } msgBoardMode = nil; } if(!GetMenu(settings.Object)) { if(settings.KeepOpen & (Menu_KeepOpen_Refresh_Mask | Menu_KeepOpen_Force) && !settings.Closable) { Refresh(GetSelection(Menu_Selection_WithColumn)); } else { return FX_Execute_Kill; } } if(settings.KeepOpen & Menu_KeepOpen_RefreshContinuously && !(effectTime % settings.RefreshInterval)) { Refresh(GetSelection(Menu_Selection_WithColumn)); } } func FxMenuStop(object target, int effectNumber, int reason, bool temp) { if(temp) { return; } CloseMenu(settings.Object); if(settings.InstantDescription.Enable) HideInstantDescription(); if(settings.Parent) settings.Parent->Suspend(true); RemoveObject(this); } func BindCallbackArgs(array args, array binding) { var ret = CreateArray(GetLength(binding)); var i = 0; for(var arg in binding) { if(arg == Menu_CallbackArg_All) { ret[i] = args; } else { ret[i] = args[arg]; } ++i; } return ret; } func CallCallbacks(array callbacks, int type, array args, defaultRet, bool noGlobalCallbacks) { var ret = defaultRet; args[Menu_CallbackArg_Menu] = this; if(callbacks) { for(var callback in callbacks) { if(callback[1] & type) { ret = CallA(callback[0], BindCallbackArgs(args, callback[2])); break; } } } if(!(!ret || (GetType(ret) == C4V_Int && ret > 0 && ret <= Menu_React_Max) || (GetType(ret) == C4V_Array && ret[0] > 0 && ret[0] <= Menu_React_Max && (!ret[1] || GetType(ret[1]) == C4V_Int)))) { FatalError(Format("Assertion failed: CallCallbacks: invalid return value detected: %v; assertion code: !ret || (GetType(ret) == C4V_Int && ret > 0 && ret <= Menu_React_Max) || (GetType(ret) == C4V_Array && ret[0] > 0 && ret[0] <= Menu_React_Max && (!ret[1] || GetType(ret[1]) == C4V_Int))", ret)); } if(!noGlobalCallbacks) { args[Menu_CallbackArg_Returned] = ret; var globalRet = CallCallbacks(settings.Callbacks, type, args, defaultRet, true); if(GetType(globalRet) == C4V_Array && globalRet[0] == Menu_React_OverrideReaction) { ret = globalRet[1]; if(!(!ret || (GetType(ret) == C4V_Int && ret > 0 && ret <= Menu_React_Max) || (GetType(ret) == C4V_Array && ret[0] > 0 && ret[0] <= Menu_React_Max && (!ret[1] || GetType(ret[1]) == C4V_Int)))) { FatalError(Format("Assertion failed: CallCallbacks: invalid return value detected: %v; assertion code: !ret || (GetType(ret) == C4V_Int && ret > 0 && ret <= Menu_React_Max) || (GetType(ret) == C4V_Array && ret[0] > 0 && ret[0] <= Menu_React_Max && (!ret[1] || GetType(ret[1]) == C4V_Int))", ret)); } } else if(globalRet) { Log("WARNING: DT_Menu::CallCallbacks: ignoring non-zero return value of global callback because override is not specified: %v", globalRet); } } return ret; } func Close(bool closeParents) { closing = true; if(subMenu) { subMenu->Close(); } if(closeParents && settings.Parent) settings.Parent->Close(true); RemoveEffect("Menu", this); } func Suspend(bool cont) { if(suspended == !cont) return; if(suspended = !cont) { CloseMenu(settings.Object); } else if(!closing) { Refresh(GetSelection(Menu_Selection_WithColumn)); } } func HandleEntries(array factoryEntries, int& i, array& ret, map& retSettings, array& factoryArgs, int column, bool isMainColumn) { if(column >= columnCount) { columnCount = column + 1; } for(var entry in factoryEntries) { if(!(GetType(entry) == C4V_Map)) { FatalError(Format("Assertion failed: \"DT_Menu::HandleEntries: invalid entry. must be a map, got %v\"; assertion code: GetType(entry) == C4V_Map", entry)); } if(entry.Type == DT_Menu_Type_Entry) { if(multiColumnMode && isMainColumn) { FatalError("DT_Menu::HandleEntries: if Menu_Columns() is used, all entries must occur inside it"); } // is it an extra column submenu? if(entry.SubMenuInitColumnData) { if(multiColumnMode) { FatalError("DT_Menu::HandleEntries: Menu_Columns() and SubMenu columns can not be used together"); } if(!entry.SubMenuInitColumnData.Settings) { entry.SubMenuInitColumnData.Settings = {}; } var index = 0; HandleEntries(entry.SubMenuInitColumnData.Entries, index, entry.SubMenuColumnEntries = [], entry.SubMenuInitColumnData.Settings, factoryArgs, column + 1, false); } ret[i++] = entry; } else if(entry.Type == DT_Menu_Type_Factory) { for(var callback in entry.Callbacks) { factoryArgs[Menu_CallbackArg_Args] = entry.Args; factoryArgs[Menu_CallbackArg_EntryNumber] = i; factoryArgs[Menu_CallbackArg_EntryColumn] = column; var factoryResult = CallA(callback, BindCallbackArgs(factoryArgs, entry.Binding)); if(factoryResult == Menu_React_Close) { return Close(); } else { var newSettings; var newEntries; if(GetType(factoryResult) == C4V_Map) { newSettings = factoryResult.Settings; newEntries = factoryResult.Entries; } else if(GetType(factoryResult) == C4V_Array) { newEntries = factoryResult; } else { Log("WARNING: DT_Menu::HandleEntries: ignoring invalid return value of Factory: %v (%v)", factoryResult, callback); } if(newSettings) { retSettings = Extend(retSettings, newSettings, true); } if(newEntries) { HandleEntries(newEntries, i, ret, retSettings, factoryArgs, column, isMainColumn); } } } } else if(entry.Type == DT_Menu_Type_Columns) { if(multiColumnMode) { FatalError("DT_Menu::HandleEntries: only one Menu_Columns() can appear in the whole menu"); } if(columnCount > 1) { FatalError("DT_Menu::HandleEntries: Menu_Columns() and SubMenu columns can not be used together"); } if(GetLength(columnEntries[0]) > 0) { FatalError("DT_Menu::HandleEntries: if Menu_Columns() is used, all entries must occur inside it"); } multiColumnMode = true; var j = 0; for(var columnData in entry.Columns) { columnEntries[j] = []; var index = 0; var tempSettings = {}; // NOTE: First instead of tempSettings, settings was used. Seems wrong. HandleEntries(columnData, index, columnEntries[j], tempSettings, factoryArgs, j, false); ++j; if(GetLength(tempSettings) > 0) { FatalError("DT_Menu::HandleEntries: menu settings can not appear in Menu_Columns()"); } } } else { Log("WARNING: DT_Menu::HandleEntries: ignoring invalid Non-Entry/Factory: %v", entry); } } } func AddEntries() { // TODO: determine maximum count of all possible submenu columns; or just ignore because it can be changing anyway? var entriesCount = 0; for(var column in columnEntries) { if(column) { entriesCount = Max(entriesCount, GetLength(column)); } } if(columnCount > 1) { SetMenuSize(columnCount, entriesCount, settings.Object); } if(!multiColumnMode) { for(var i = 1; i < GetLength(columnEntries); ++i) { var column = columnEntries[i]; if(column) { var offset = currentColumnSelections[i - 1] + columnOffsets[i - 1]; var entryCount = GetLength(column); if(offset + entryCount > entriesCount) { offset = entriesCount - entryCount; } columnOffsets[i] = offset; } } } for(var i = 0, entryIndex = 0; i < entriesCount; ++i) { for(var j = 0; j < columnCount; ++j) { columnOffsets[j] ??= 0; var rowIndex = i - columnOffsets[j]; var entries = columnEntries[j]; if(entries && rowIndex >= 0 && rowIndex < GetLength(entries)) { var entry = entries[rowIndex]; var condition = entry.Condition, conditionRet; var text = entry.Text, noCommand = !entry.Placeholder || (entry.Placeholder != true && !entry.Callbacks); if(condition) { if(noCommand || condition.AllowDisabledSelection == Menu_Condition_DenySelection || (condition.AllowDisabledSelection == Menu_Condition_Default && !settings.Condition.AllowSelection)) { noCommand = true; } conditionRet = CallA(condition.Callback, [entry.Icon, settings.Object, entry.Args]) || settings.Condition.DisableMode; if(conditionRet == Menu_ConditionReact_Hide) { continue; } else if(conditionRet == Menu_ConditionReact_GrayOut) { text = Format("%s", text); } else if(GetType(conditionRet) == C4V_Array && conditionRet[0] == Menu_ConditionReact_CustomFormat) { text = Format(conditionRet[1], text); } else { noCommand = false; } } var icon = entry.Icon, iconIndex = 0, iconID = 0, deleteIcon = 0; if(GetType(icon) == C4V_Array) { if(GetType(icon[0]) == C4V_C4ID) { iconID = icon[0]; if(icon[2]) { icon = [CreateIconDummy()->SetIcon(iconID, icon[1])->SetColor(icon[2]), true]; } else if(icon[1]) { entry.Extra |= C4MN_Add_ImgIndexed; iconIndex = icon[1]; entry.XPar1 = icon[1]; } } if(GetType(icon[0]) == C4V_C4Object) { entry.Extra |= C4MN_Add_ImgObject; entry.XPar1 = icon[0]; if(icon[1]) { deleteIcon = icon[0]; } if(!iconID) { iconID = GetID(icon[0]); } } } else { iconID = icon; } entry.Icon = iconID; if(!entry.InstantDescriptionIcon) { entry.InstantDescriptionIcon = [iconID, iconIndex]; } if(settings.InstantDescription.Enable) { entry.Extra |= C4MN_Add_ForceNoDesc; } entry.Placeholder = !noCommand; columnEntries[j][rowIndex] = entry; var showDesc = !(entry.Extra & C4MN_Add_ForceNoDesc); if(entry.SubMenuInitColumnData && currentColumnSelections[j] == rowIndex) { text = Format("%s {{MN7I:7}}", text); } AddMenuItem(text, !noCommand && "MenuItemCommand", iconID, settings.Object, entry.Count, entryIndex, showDesc && entry.Description || nil, entry.Extra, entry.XPar1, entry.XPar2); if(deleteIcon) { RemoveObject(deleteIcon); } } else { AddMenuItem("", "DummyItemCommand", nil, settings.Object, nil, entryIndex, nil, C4MN_Add_ForceNoDesc); } ++entryIndex; } } } func React(reaction, array entryIndex, int refreshDelayed) { if(reaction == Menu_React_Close) { Close(!settings.KeepParentOnClose); } else if(reaction == Menu_React_Back) { if(currentColumn > 0 && !multiColumnMode) { LeaveSubMenuColumn(); } else { Close(); } } else if(reaction == Menu_React_Refresh || (settings.KeepOpen & Menu_KeepOpen_Refresh)) { Refresh(entryIndex, refreshDelayed); } else if(GetType(reaction) == C4V_Array) { var selection = GetSelection(Menu_Selection_WithColumn); var currentSelection = selection; if(reaction[0] == Menu_React_SelectionOffset) { selection[0] += reaction[1]; selection[0] %= GetLength(columnEntries[currentColumn]); } else if(reaction[0] == Menu_React_SelectionChange) { selection[0] = BoundBy(reaction[1], 0, GetLength(columnEntries[selection[1]]) - 1); } if(selection != currentSelection) { SelectEntry(selection[0], selection[1]); } } } func CheckCondition(map entry) { var condition = entry.Condition; return !condition || (CallA(condition.Callback, [entry.Icon, settings.Object, entry.Args]) || settings.Condition.DisableMode) == Menu_ConditionReact_Show; } func MenuItemCommand(id ID, int itemNumber, int action) { var column = DecodeSelection(itemNumber); var entry = columnEntries[column][itemNumber]; var condition = entry.Condition; action = action || Menu_CallbackType_Normal; var reaction; if(CheckCondition(entry)) { reaction = CallCallbacks(entry.Callbacks, action, [action, ID, settings.Object, entry.Args]); } else { if(condition.AllowDisabledSelection == Menu_Condition_AllowSelection) { reaction = Menu_React_KeepOpen; } else { reaction = Menu_React_Refresh; } } React(reaction, [itemNumber, column]); } // for entries that shouldn't be able to be selected // just in else if(conditionRet == Menu_ConditionReact_) they get activated through clicking func DummyItemCommand(id ID, int itemNumber, int action) { var column = DecodeSelection(itemNumber); React(Menu_React_KeepOpen, [itemNumber, column]); } func SubMenuItemCallback(int action, object menuObject, args, array allArgs) { allArgs[Menu_CallbackArg_Args] = args[1]; var reaction = CallCallbacks(args[0], action, allArgs, Menu_React_None); if(((action & Menu_CallbackType_Defaults) && reaction == Menu_React_None) || reaction == Menu_React_ShowSubMenu) { if(args[3]) { var targetSelection = columnEntries[currentColumn][currentColumnSelections[currentColumn]].SubMenuInitColumnData.Settings.Selection; SelectEntry(targetSelection && targetSelection[0], currentColumn + 1); } else { subMenu = CreateNewMenu(args[2], settings, this); } return Menu_React_None; } else { return reaction; } } func DecodeSelection(int& selection) { var column = selection % columnCount; selection /= columnCount; selection -= columnOffsets[column]; return column; } func EncodeSelection(int selection, int column) { return (selection + columnOffsets[column]) * columnCount + column; } func MenuQueryCancel(int selection, object menuObject) { var column = DecodeSelection(selection); var reaction; if(selection != -1) { var entry = columnEntries[column][selection]; if(CheckCondition(entry)) { reaction = CallCallbacks(entry.Callbacks, Menu_CallbackType_Close, [Menu_CallbackType_Close, entry.Icon, settings.Object, entry.Args]); } React(reaction, [selection, column], true); } if((settings.KeepOpen != Menu_KeepOpen_Not && settings.KeepOpen != Menu_KeepOpen_Permanent && !settings.Closable) || (reaction == Menu_React_KeepOpen)) { return true; } } func OnMenuSelection(int selection, object menuObject) { var column = DecodeSelection(selection); var oldColumnSelection = currentColumnSelections[column]; var oldColumnOldSelection = currentColumnSelections[currentColumn]; // selections can be out of range if dummy entries get selected // let's emulate some engine behavior then // TODO: Fix stack overflow when the target selection can't be selected (Placeholder) if(!columnEntries[column]) { // navigate downwards when pressing right at the right border (thus selecting a dummy) var selectedColumn = column; for(; column > -1; --column) { if(columnEntries[column]) { break; } } if(column >= 0) { var offset = 1; if(selectedColumn == columnCount - 1 && currentColumn == 0) { // but upwards if it was actually pressing left at the left border offset = 0; column = 0; } SelectEntry(BoundBy(selection - columnOffsets[column] + offset, 0, GetLength(columnEntries[column]) - 1), column); } return; } var skipHandling; if(selection < 0) { if(currentColumn == column) { if(oldColumnSelection == 0) { // wrap around if the last selection was the top already SelectBottomEntry(column); } else { // otherwise maybe wrapped around from the bottom or just hovered it with the mouse SelectTopEntry(column); } return; } skipHandling = true; } if(selection >= GetLength(columnEntries[column])) { if(currentColumn == column) { if(oldColumnSelection == GetLength(columnEntries[column]) - 1) { // wrap around if the last selection was the bottom already SelectTopEntry(column); } else { // otherwise maybe wrapped around from the top or just hovered it with the mouse SelectBottomEntry(column); } return; } else if(multiColumnMode) { SelectBottomEntry(column); return; } skipHandling = true; } if(selection != oldColumnSelection || column != currentColumn) { var oldColumn = currentColumn; currentColumn = column; if(!skipHandling) { var entry = columnEntries[oldColumn][oldColumnOldSelection]; var deselectReaction = Menu_React_None; var selectReaction = Menu_React_None; if(!noSelectionCallbacks && CheckCondition(entry) && oldColumnSelection != -1) { var args = [Menu_CallbackType_Deselection, entry.Icon, settings.Object, entry.Args, selection, oldColumnSelection]; args[Menu_CallbackArg_NewSelectionColumn] = currentColumn; args[Menu_CallbackArg_OldSelectionColumn] = oldColumn; deselectReaction = CallCallbacks(entry.Callbacks, Menu_CallbackType_Deselection, args); } var oldIsColumnSubMenu = !!entry.SubMenuInitColumnData; currentColumnSelections[column] = selection; entry = columnEntries[column][selection]; if(!noSelectionCallbacks && CheckCondition(entry)) { var args = [Menu_CallbackType_Selection, entry.Icon, settings.Object, entry.Args, selection, oldColumnSelection]; args[Menu_CallbackArg_NewSelectionColumn] = currentColumn; args[Menu_CallbackArg_OldSelectionColumn] = oldColumn; selectReaction = CallCallbacks(entry.Callbacks, Menu_CallbackType_Selection, args); } if(deselectReaction != Menu_React_None) { React(deselectReaction, GetSelection(Menu_Selection_WithColumn)); } if(selectReaction != Menu_React_None) { React(selectReaction, GetSelection(Menu_Selection_WithColumn)); } selection = currentColumnSelections[column]; } // navigating between submenu columns; not in multiColumnMode if(!multiColumnMode) { // navigated from one column to another without going over the left/right border if(selection + columnOffsets[currentColumn] == oldColumnOldSelection + columnOffsets[oldColumn]) { if(currentColumn == oldColumn + 1 && oldIsColumnSubMenu) { var targetSelection = columnEntries[currentColumn - 1][oldColumnOldSelection].SubMenuInitColumnData.Settings.Selection; targetSelection = targetSelection && targetSelection[0]; // column submenu was entered through pressing right; select the first entry // but only if it's not there yet if(selection != targetSelection) { SelectEntry(targetSelection, currentColumn); return; } } else if(currentColumn == oldColumn - 1) { // column submenu was left through pressing left; select the submenu entry // but only if it's not there yet if(selection != oldColumnSelection) { SelectEntry(oldColumnSelection, currentColumn); return; } } } else if(currentColumn != oldColumn && !shouldChangeColumn) // with going over the left/right border; we don’t want that { currentColumn = oldColumn; SelectEntry(oldColumnOldSelection, oldColumn); return; } } if(skipHandling) { currentColumn = oldColumn; return; } if(settings.InstantDescription.Enable) { ShowInstantDescription(columnEntries[currentColumn][selection]); } // update submenu column if necessary if(!multiColumnMode && selection != oldColumnSelection) { if(entry.SubMenuInitColumnData) { SetLength(columnEntries, column + 2); for(var i = column + 1; i < columnCount; ++i) { currentColumnSelections[i] = -1; } Refresh([selection, column]); return; } else { var oldEntries = columnEntries[column + 1]; if (oldEntries != nil) { SetLength(columnEntries, column + 1); currentColumnSelections[column + 1] = -1; if(oldEntries) { Refresh([selection, column]); return; } } } } } } func Refresh(array selection, bool delayed) { if(suspended) { return; } if(delayed) { ScheduleCall(this, "Refresh", 1, 0, selection); } else { var oldNoSelectionCallbacks = noSelectionCallbacks; noSelectionCallbacks = true; CloseMenu(settings.Object); var oldRefreshing = refreshing; refreshing = true; Create({ Settings = settings, Entries = createEntries }); refreshing = oldRefreshing; var column = BoundBy(selection[1], 0, columnCount); selection = selection[0]; if(GetLength(columnEntries[column]) > 0) { SelectEntry(BoundBy(selection, 0, GetLength(columnEntries[column]) - 1), column); if(settings.InstantDescription.Enable) { ShowInstantDescription(columnEntries[currentColumn][selection]); } } noSelectionCallbacks = oldNoSelectionCallbacks; } } func ShowInstantDescription(map entry) { if(entry.Description) { var icon = entry.InstantDescriptionIcon; var iconString; if(GetType(icon[1]) == C4V_String) { iconString = Format("Portrait:%i::%x::%s", icon[0], icon[2], icon[1]); } else { iconString = Format("%i:%d", icon[0] || DT_Menu_IconDummy, icon[1]); } var pos = settings.InstantDescription.Position; CustomMessage("@" .. entry.Description, 0, GetOwner(settings.Object), pos[0], pos[1], 0, settings.InstantDescription.Decoration, iconString, settings.InstantDescription.Flags); } else { HideInstantDescription(); } } func HideInstantDescription() { CustomMessage("", 0, GetOwner(settings.Object), 0, 0, 0, 0, Format("%i", DT_Menu_IconDummy), settings.InstantDescription.Flags); } // ---------------------------------------------------------------------------- global func Menu_Callback(array callback, int types, array argBinding) { types ??= Menu_CallbackType_Defaults; if(!(MN7I->CheckCallback(callback))) { FatalError("Assertion failed: Menu_Callback invalid callback used; assertion code: MN7I->CheckCallback(callback)"); } if(!((types & ~Menu_CallbackType_All) == 0)) { FatalError("Assertion failed: Menu_Callback: invalid callback type(s) used; assertion code: (types & ~Menu_CallbackType_All) == 0"); } argBinding = argBinding || [Menu_CallbackArg_Action, Menu_CallbackArg_Icon, Menu_CallbackArg_MenuObject, Menu_CallbackArg_Args, Menu_CallbackArg_NewSelection, Menu_CallbackArg_OldSelection, Menu_CallbackArg_NewValue, Menu_CallbackArg_OldValue, Menu_CallbackArg_FromSubMenu]; if(!(MN7I->ValidateMenuCallbackArgBinding(argBinding))) { FatalError("Assertion failed: Menu_Callback: invalid argBinding used; assertion code: MN7I->ValidateMenuCallbackArgBinding(argBinding)"); } return [callback, types, argBinding]; } global func Menu_Entry_VariableCondition(array scopedVar, compare, int disableMode, bool invert, int allowDisabledSelection) { if(!(MN7I->CheckScopedVar(scopedVar))) { FatalError("Assertion failed: Menu_Entry_VariableCondition: Invalid scopedVar used; assertion code: MN7I->CheckScopedVar(scopedVar)"); } if(!(!disableMode || (GetType(disableMode) == C4V_Int && disableMode >= 0 && disableMode <= Menu_ConditionReact_Max) || (GetType(disableMode) == C4V_Array && disableMode[0] == Menu_ConditionReact_CustomFormat && GetType(disableMode[1]) == C4V_String))) { FatalError("Assertion failed: Menu_ConditionDisableMode: invalid disableMode; assertion code: !disableMode || (GetType(disableMode) == C4V_Int && disableMode >= 0 && disableMode <= Menu_ConditionReact_Max) || (GetType(disableMode) == C4V_Array && disableMode[0] == Menu_ConditionReact_CustomFormat && GetType(disableMode[1]) == C4V_String)"); } if(!(!allowDisabledSelection || (GetType(allowDisabledSelection) == C4V_Int && allowDisabledSelection >= 0 && allowDisabledSelection <= Menu_Condition_Max))) { FatalError("Assertion failed: Menu_ConditionAllowSelection: invalid allowDisabledSelection value; assertion code: !allowDisabledSelection || (GetType(allowDisabledSelection) == C4V_Int && allowDisabledSelection >= 0 && allowDisabledSelection <= Menu_Condition_Max)"); } return BindCallback(MN7I->MenuObjectCallback("VariableCondition"), [Bind(scopedVar), Bind(compare), Bind(disableMode), Bind(invert)]); } func VariableCondition(array scopedVar, compare, int disableMode, bool invert) { var disable = ScopedVar(scopedVar) != compare; if(invert) { disable = !disable; } if(disable) { return disableMode; } else { return Menu_ConditionReact_Show; } } /* Menu_Entry({ Text = , Icon = , Count = , Description = , Placeholder = : true | >false<, InstantDescriptionIcon = [iconID, iconIndex] | [iconID, portraitName, color], Callbacks = [...], Args = , Condition = { Callback = , AllowDisabledSelection = >Menu_Condition_Default< | Menu_Condition_AllowSelection | Menu_Condition_DenySelection }, Extra = >Also used internally<, XPar1 = >Also used internally<, XPar2 = >Also used internally<, SubMenuInitColumnData = >Internal<: For SubMenus as columns, SubMenuColumnEntries = >Internal<: For SubMenus as columns, Type = >Internal<: DT_Menu_Type_Entry | DT_Menu_Type_Factory | DT_Menu_Type_Columns }) */ global func Menu_Entry(map entry) { if(!entry) { entry = { Placeholder = false }; } else { entry = Extend({ Text = "", Placeholder = -1, Extra = 0, }, entry, true); } if(!entry.Description) { entry.Extra |= C4MN_Add_ForceNoDesc; } entry.Type = DT_Menu_Type_Entry; if(entry.Icon && GetType(entry.Icon) != C4V_Array) { if(GetType(entry.Icon) == C4V_C4Object) { entry.Icon = [entry.Icon, true]; } else { entry.Icon = [entry.Icon]; } } return entry; } global func Menu_SubMenu(map submenuSettings, bool asColumn) { if(!(submenuSettings.Entry)) { FatalError("Assertion failed: Menu_SubMenu: Non-selectable placeholder as submenu entry doesn't make sense; assertion code: submenuSettings.Entry"); } if(!(submenuSettings && GetLength(submenuSettings) > 0)) { FatalError("Assertion failed: Menu_SubMenu: Empty submenu doesn't make sense; assertion code: submenuSettings && GetLength(submenuSettings) > 0"); } var ret = Menu_Entry(submenuSettings.Entry); ret.Args = [ret.Callbacks, ret.Args, submenuSettings, asColumn]; ret.Callbacks = [Menu_Callback(MN7I->MenuObjectCallback("SubMenuItemCallback"), Menu_CallbackType_All, [Menu_CallbackArg_Action, Menu_CallbackArg_MenuObject, Menu_CallbackArg_Args, Menu_CallbackArg_All])]; if(asColumn) { ret.SubMenuInitColumnData = submenuSettings; } return ret; } global func Menu_Factory(array callbacks, args, array binding) { if(!(callbacks)) { FatalError("Assertion failed: Menu_Factory: callbacks are mandatory; assertion code: callbacks"); } return { Type = DT_Menu_Type_Factory, Callbacks = callbacks, Args = args, Binding = binding || [Menu_CallbackArg_Args, Menu_CallbackArg_EntryNumber, Menu_CallbackArg_Menu] }; } // Menu_Columns // ([ // [ first column entries ], // [ second column entries ], // ... // ]) global func Menu_Columns(array columns) { for(var column in columns) { if(column.Settings && GetLength(column.Settings) > 0) { FatalError("DT_Menu::Menu_Columns: menu settings can not appear in Menu_Columns()"); } } return { Type = DT_Menu_Type_Columns, Columns = columns }; } global func Menu_Text(string text, bool allowSelection) { return Menu_Entry({ Text = text, Placeholder = allowSelection }); } global func Menu_Blank(bool allowSelection) { return Menu_Entry({ Placeholder = allowSelection }); } func DeclineAcceptBack(string text, icon, string callback, map settings) { var ret = Menu_Entry(Extend({ Text = text, }, settings || {}, true)); ret.Args = [ret.Callbacks, ret.Args]; ret.Callbacks = [Menu_Callback(MN7I->MenuObjectCallback(callback), Menu_CallbackType_All, [Menu_CallbackArg_Action, Menu_CallbackArg_Args, Menu_CallbackArg_All])]; ExtraIcon(ret.Text, ret.Icon, icon); return ret; } global func Menu_Decline(map settings) { return MN7I->DeclineAcceptBack("$Decline$", [MN7I, 2], "DeclineAcceptCommand", settings); } global func Menu_Accept(map settings) { return MN7I->DeclineAcceptBack("$Accept$", [MN7I, 1], "DeclineAcceptCommand", settings); } global func Menu_Back(map settings) { return MN7I->DeclineAcceptBack("$Back$", [MN7I, 5], "BackCommand", settings); } func AdaptorLayout(array& layout, array& vals, bool valuesAsSeparateLists) { if(!(vals)) { FatalError("Assertion failed: AdaptorLayout: vals are mandatory; assertion code: vals"); } layout = layout || [Menu_Layout_Value | Menu_Layout_Text]; var layoutMap = []; var index = 1; for(var val in layout) { var noFlag = val & Menu_Layout__NoFlagMask; if(noFlag) { layoutMap[noFlag] = index; } if(val & Menu_Layout_Text) { layoutMap[Menu_Layout__TextPos] = index; } if(val & Menu_Layout_Value) { layoutMap[Menu_Layout__ValuePos] = index; } // if(val & Menu_Layout_InputValue) TODO // { // layoutMap[Menu_Layout__InputValuePos] = index; // } ++index; } if(!(layoutMap[Menu_Layout__ValuePos])) { FatalError("Assertion failed: AdaptorLayout: Menu_Layout_Value is mandatory; assertion code: layoutMap[Menu_Layout__ValuePos]"); } layout = layoutMap; if(valuesAsSeparateLists) { var tempVals = vals; var vals = []; for(var i = 0; i < GetLength(tempVals[0]); ++i) { vals[i] = []; for(var j = 0; j < GetLength(tempVals); ++j) { vals[i][j] = tempVals[j][i]; } } } } /* Menu_Adaptor({ Entry = { sames as for Menu_Entry }, Adaptor = { Type = Menu_AdaptorType_Boolean | Menu_AdaptorType_Integer | Menu_AdaptorType_String | Menu_AdaptorType_ID | Menu_AdaptorType_Enum | Menu_AdaptorType_BitField, Variable = storing the value, Callbacks = [...], Args = , MessageBoardText = , Limits = [, ] for Integer-Type, StepSize = [, ] for Integer-Type, NoEmptyString = : true | >false<, EntryIndex = >Internal<: used for MessageBoard callbacks, WrapAround = : true | false , Enum = : { Values = [<[Values according to Layout]>...], Layout = [<[Menu_Layout_*, some can be combined with &]>...], ValuesAsSeparateLists = : true | >false<, Inline = : true | >false<, AllowUnknown = : true | >false<, SubMenu = : { On = : >Menu_CallbackType_Special2< | true to show as extra column | Menu_CallbackType_None | Menu_CallbackType_*, Icon = , Title = , Text = <Text to use for the individual Entries>, } }, BitField = <only for BitField-Type>: { Values = [<[Values according to Layout]>...], Layout = [<[Menu_Layout_*, some can be combined with &]>...], ValuesAsSeparateLists = <Values is a "struct of arrays" instead of "array of structs">: true | >false<, BitPositionAsValue = <Instead of the values being a bit-mask, the values denote the bit-position>: true | >false<, Mask = >Internal< } } }) */ global func Menu_Adaptor(map settings) { if(!(settings.Adaptor && GetLength(settings.Adaptor) > 0)) { FatalError("Assertion failed: Menu_Adaptor: Adaptor without settings doesn't make sense; assertion code: settings.Adaptor && GetLength(settings.Adaptor) > 0"); } if(!(settings.Entry && GetLength(settings.Entry) > 0)) { FatalError("Assertion failed: Menu_Adaptor: Non-selectable placeholder as adaptor entry doesn't make sense; assertion code: settings.Entry && GetLength(settings.Entry) > 0"); } if(!settings.Adaptor.Type) { if(settings.Adaptor.Enum) { settings.Adaptor.Type = Menu_AdaptorType_Enum; } else if(settings.Adaptor.BitField) { settings.Adaptor.Type = Menu_AdaptorType_BitField; } else { FatalError("Menu_Adaptor: Adaptor.Type is missing!"); } } settings = Extend({ Adaptor = { WrapAround = -1, Enum = { SubMenu = { On = Menu_CallbackType_Special2, } } } }, settings, true); if(settings.Adaptor.WrapAround == -1) { if(settings.Adaptor.Type == Menu_AdaptorType_Integer) { settings.Adaptor.WrapAround = false; } else if(settings.Adaptor.Type == Menu_AdaptorType_Enum) { settings.Adaptor.WrapAround = true; } } if(settings.Adaptor.Type == Menu_AdaptorType_BitField) { var layout = settings.Adaptor.BitField.Layout; var fieldVals = settings.Adaptor.BitField.Values; MN7I->AdaptorLayout(layout, fieldVals, settings.Adaptor.BitField.ValuesAsSeparateLists); if(settings.Adaptor.BitField.BitPositionAsValue) { var valuePos = layout[Menu_Layout__ValuePos] - 1; for(var i = 0; i < GetLength(fieldVals); ++i) { if(!(!fieldVals[i][valuePos] || GetType(fieldVals[i][valuePos]) == C4V_Int)) { FatalError(Format("Assertion failed: Menu_Adaptor_BitField: Bit value must be an integer; got: %v; assertion code: !fieldVals[i][valuePos] || GetType(fieldVals[i][valuePos]) == C4V_Int", fieldVals[i][valuePos])); } if(!(fieldVals[i][valuePos] >= 0 && fieldVals[i][valuePos] < 32)) { FatalError("Assertion failed: Menu_Adaptor_BitField: Bit value out of range; assertion code: fieldVals[i][valuePos] >= 0 && fieldVals[i][valuePos] < 32"); } fieldVals[i][valuePos] = 1 << fieldVals[i][valuePos]; } } settings.Adaptor.BitField.Values = fieldVals; settings.Adaptor.BitField.Layout = layout; var valuePos = layout[Menu_Layout__ValuePos] - 1; for(var fieldPart in fieldVals) { if(!(GetType(fieldPart[valuePos]) == C4V_Int)) { FatalError(Format("Assertion failed: Menu_Adaptor_BitField: Flag mask (Menu_Layout_Value) must be a non-zero integer; got %v; assertion code: GetType(fieldPart[valuePos]) == C4V_Int", fieldPart[valuePos])); } } } else if(settings.Adaptor.Type == Menu_AdaptorType_Enum) { MN7I->AdaptorLayout(settings.Adaptor.Enum.Layout, settings.Adaptor.Enum.Values, settings.Adaptor.Enum.ValuesAsSeparateLists); } return Menu_Factory([MN7I->MenuObjectCallback("AdaptorFactory")], [Menu_Entry(settings.Entry), settings], [Menu_CallbackArg_Args, Menu_CallbackArg_EntryNumber, Menu_CallbackArg_EntryColumn]); } func EnumValPos(array enumVals, array layout, val, bool allowUnknown) { for(var i = 0; i < GetLength(enumVals); ++i) { if(enumVals[i][layout[Menu_Layout__ValuePos] - 1] == val) { return i; } } if(!allowUnknown) { Log("WARNING: DT_Menu::EnumValPos: Current value %v is not in enum: %v", val, enumVals); } return -1; } func BooleanToggleText(bool val, string& text, &icon) { val = !!val; ExtraIcon(text, icon, [MN7I, 2 - val, !val && RGB(128, 128, 128)]); } func InlineIcon(string& text, icon) { if(icon[2]) { text = Format("<c %x>{{%i:%d}}</c> %s", icon[2], icon[0], icon[1], text); } else { text = Format("{{%i:%d}} %s", icon[0], icon[1], text); } } func ExtraIcon(string& text, &icon, extraIcon) { if(GetType(extraIcon) == C4V_C4ID) { extraIcon = [extraIcon]; } if(icon && extraIcon && GetType(extraIcon[0]) == C4V_C4ID) { InlineIcon(text, extraIcon); } else { icon = extraIcon; } } func EnumEntrySettings(string& text, &icon, string& description, int index, map args, map entry) { var layoutVals = args.Enum.Values; var layout = args.Enum.Layout; text = entry.Text; if(layout[Menu_Layout__TextPos]) { text = Format(text, layoutVals[index][layout[Menu_Layout__TextPos] - 1]); } if(layout[Menu_Layout_Icon]) { ExtraIcon(text, icon, layoutVals[index][layout[Menu_Layout_Icon] - 1]); } if(layout[Menu_Layout_Description]) { description = Format(description, layoutVals[index][layout[Menu_Layout_Description] - 1]); } } func AdaptorGetLimits(array limits) { var ret = CreateArray(2); if(!limits) { limits = [Menu_Adaptor_Limits_Min, Menu_Adaptor_Limits_Max]; } for(var i = 0; i < 2; ++i) { if(!limits[i] || GetType(limits[i]) == C4V_Int) { ret[i] = limits[i]; } else { ret[i] = Call(limits[i], limits[3]); if(!(!ret[i] || GetType(ret[i]) == C4V_Int)) { FatalError(Format("Assertion failed: Menu_Adaptor_Limits: Got invalid callback value for %s: %v; assertion code: !ret[i] || GetType(ret[i]) == C4V_Int", ["min", "max"][i], ret[i])); } } } return ret; } func AdaptorFactory(args, int entryIndex, int entryColumn) { var origArgs = args; var entry = args[0]; var entrySettings = args[1].Entry; args = args[1].Adaptor; var text = entry.Text; var description = entry.Description; var icon = entry.Icon; var val = ScopedVar(args.Variable); var defaultMsgboardText = "$EnterValue$"; var retSubMenu; args.Args = entry.Args; args.EntryIndex = [entryIndex, entryColumn]; args.Callbacks = args.Callbacks || []; if(args.Type == Menu_AdaptorType_Boolean) { if(!(GetType(val) == C4V_Any || GetType(val) == C4V_Bool)) { FatalError("Assertion failed: AdaptorFactory: Value of Menu_Adaptor_Variable has wrong type; assertion code: GetType(val) == C4V_Any || GetType(val) == C4V_Bool"); } BooleanToggleText(val, text, icon); } else if(args.Type == Menu_AdaptorType_String) { if(!(GetType(val) == C4V_Any || GetType(val) == C4V_String)) { FatalError("Assertion failed: AdaptorFactory: Value of Menu_Adaptor_Variable has wrong type; assertion code: GetType(val) == C4V_Any || GetType(val) == C4V_String"); } text = Format(entry.Text, val); defaultMsgboardText = "$EnterText$"; } else if(args.Type == Menu_AdaptorType_Integer) { if(!(GetType(val) == C4V_Any || GetType(val) == C4V_Int)) { FatalError("Assertion failed: AdaptorFactory: Value of Menu_Adaptor_Variable has wrong type; assertion code: GetType(val) == C4V_Any || GetType(val) == C4V_Int"); } val ??= 0; var limits = AdaptorGetLimits(args.Limits), max, min; var wrapAround = args.WrapAround; if(limits && !wrapAround) { if(val >= limits[1]) { max = true; } if(val <= limits[0]) { min = true; } } text = Format(entry.Text, val) .. " " .. ["{{MN7I:4}}", "<c 808080>{{MN7I:4}}</c>"][min]; ExtraIcon(text, icon, [MN7I, 3, max && RGB(128, 128, 128)]); defaultMsgboardText = "$EnterNumber$"; } else if(args.Type == Menu_AdaptorType_ID) { if(!(GetType(val) == C4V_Any || GetType(val) == C4V_C4ID)) { FatalError("Assertion failed: AdaptorFactory: Value of Menu_Adaptor_Variable has wrong type; assertion code: GetType(val) == C4V_Any || GetType(val) == C4V_C4ID"); } text = Format(entry.Text, val && GetName(0, val) || ""); if(val) { ExtraIcon(text, icon, val); } defaultMsgboardText = "$EnterIDOrName$"; } else if(args.Type == Menu_AdaptorType_Enum) { if(args.Enum.Inline) { return AdaptorEnumSubMenuFactory([args, entry]); } var layoutVals = args.Enum.Values; var layout = args.Enum.Layout; var index = EnumValPos(layoutVals, layout, val, args.Enum.AllowUnknown); if(index == -1) { if(!args.Enum.AllowUnknown) { ScopedVar(args.Variable) = layoutVals[0][layout[Menu_Layout__ValuePos] - 1]; } index = 0; } var submenuIcon = icon; EnumEntrySettings(text, icon, description, index, args, entry); if(args.Enum.SubMenu.On != Menu_CallbackType_None) { args.MessageBoardText = args.MessageBoardText || defaultMsgboardText; retSubMenu = [Menu_Factory([MenuObjectCallback("AdaptorEnumSubMenuFactory")], origArgs[1])]; } } else if(args.Type == Menu_AdaptorType_BitField) { return AdaptorBitFieldItemsFactory(origArgs); } args.MessageBoardText = args.MessageBoardText || defaultMsgboardText; if(!retSubMenu) { return [Menu_Entry(Extend(entrySettings, { Text = text, Callbacks = [Menu_Callback(MenuObjectCallback("AdaptorCommand"), Menu_CallbackType_All, [Menu_CallbackArg_Action, Menu_CallbackArg_MenuObject, Menu_CallbackArg_Args, Menu_CallbackArg_All])], Icon = icon, Description = description, Args = args }, true))]; } else { return [Menu_SubMenu({ Entry = Extend(entrySettings, { Text = text, Callbacks = [Menu_Callback(MenuObjectCallback("AdaptorCommand"), Menu_CallbackType_All, [Menu_CallbackArg_Action, Menu_CallbackArg_MenuObject, Menu_CallbackArg_Args, Menu_CallbackArg_All])], Icon = icon, Description = description, Args = args }, true), Entries = retSubMenu }, GetType(args.Enum.SubMenu.On) == C4V_Bool && args.Enum.SubMenu.On)]; } } func AdaptorEnumSubMenuFactory(map args) { var entry = args.Entry; args = args.Adaptor; var layoutVals = args.Enum.Values; var layout = args.Enum.Layout; var submenuIcon = []; if(args.Enum.SubMenu.Title) { entry.Text = args.Enum.SubMenu.Title; } var index = EnumValPos(layoutVals, layout, ScopedVar(args.Variable), args.Enum.AllowUnknown); var title, icon, description; EnumEntrySettings(title, icon, description, index, args, entry); var ret; if(args.Enum.Inline) { ret = { Entries = [] }; } else { ret = { Settings = { Selection = [index, 0], KeepParentOnClose = true, KeepOpen = Menu_KeepOpen_Not, Title = title }, Entries = [] }; } if(args.Enum.SubMenu.Icon) { submenuIcon = [args.Enum.SubMenu.Icon]; } else if(icon && GetType(icon[0]) == C4V_C4ID) { submenuIcon = icon; } if(submenuIcon && GetType(submenuIcon[0]) == C4V_C4ID) { ret.Settings.Icon = submenuIcon[0]; } var text; if(args.Enum.SubMenu.Text) { entry.Text = args.Enum.SubMenu.Text; } for(var i = 0; i < GetLength(layoutVals); ++i) { icon = 0; EnumEntrySettings(text, icon, description, i, args, entry); if(args.Enum.Inline) { BooleanToggleText(i == index, text, icon); } else if(i == index) { ExtraIcon(text, icon, [MN7I, 1]); } ret.Entries[] = Menu_Entry({ Text = text, Icon = icon, Description = description, Callbacks = [Menu_Callback(MenuObjectCallback("AdaptorEnumSubMenuItem"), Menu_CallbackType_Defaults, [Menu_CallbackArg_Args, Menu_CallbackArg_All])], Args = [i, args] }); } return ret; } func AdaptorEnumSubMenuItem(args, array allArgs) { var index = args[0]; args = args[1]; var val = ScopedVar(args.Variable); var oldVal = val; var enumVals = args.Enum.Values; var layout = args.Enum.Layout; ScopedVar(args.Variable) = val = enumVals[index][layout[Menu_Layout__ValuePos] - 1]; allArgs[Menu_CallbackArg_Args] = args.Args; allArgs[Menu_CallbackArg_FromSubMenu] = true; var reaction = AdaptorCommandCallChangedCallback(args.Callbacks, val, oldVal, allArgs); if(reaction != Menu_React_None) { return reaction; } if(args.Enum.Inline) { return Menu_React_Refresh; } return Menu_React_Back; } func AdaptorBitFieldItemsFactory(args) { var entry = args[0]; var entrySettings = args[1].Entry; args = args[1].Adaptor; var text = entry.Text; var description = entry.Description; var icon = entry.Icon; var fieldValue = ScopedVar(args.Variable); if(!(GetType(fieldValue) == C4V_Any || GetType(fieldValue) == C4V_Int)) { FatalError("Assertion failed: AdaptorFactory: Value of Menu_Adaptor_Variable has wrong type; assertion code: GetType(fieldValue) == C4V_Any || GetType(fieldValue) == C4V_Int"); } var layoutVals = args.BitField.Values; var layout = args.BitField.Layout; var ret = []; for(var val in layoutVals) { var mask = val[layout[Menu_Layout__ValuePos] - 1]; text = entry.Text || "%s"; if(layout[Menu_Layout__TextPos]) { text = Format(text, val[layout[Menu_Layout__TextPos] - 1]); } icon = entry.Icon; BooleanToggleText((fieldValue & mask) == mask, text, icon); if(layout[Menu_Layout_Icon]) { ExtraIcon(text, icon, val[layout[Menu_Layout_Icon] - 1]); } if(layout[Menu_Layout_Description]) { description = Format(description, val[layout[Menu_Layout_Description] - 1]); } args.BitField.Mask = mask; ret[] = Menu_Entry(Extend(entrySettings, { Text = text, Callbacks = [Menu_Callback(MenuObjectCallback("AdaptorCommand"), Menu_CallbackType_All, [Menu_CallbackArg_Action, Menu_CallbackArg_MenuObject, Menu_CallbackArg_Args, Menu_CallbackArg_All])], Icon = icon, Description = description, Args = args }, true)); } return { Entries = ret }; } func WrapOrBind(int val, array limits, bool wrap) { if(!limits) { return val; } var min = limits[0]; var max = limits[1]; if(val < min) { if(wrap) { return max; } else { return min; } } else if(val > max) { if(wrap) { return min; } else { return max; } } else { return val; } } func AdaptorCommandCallChangedCallback(callbacks, val, oldVal, array allArgs) { allArgs[Menu_CallbackArg_Action] = Menu_CallbackType_ValueChanged; allArgs[Menu_CallbackArg_NewValue] = val; allArgs[Menu_CallbackArg_OldValue] = oldVal; return CallCallbacks(callbacks, Menu_CallbackType_ValueChanged, allArgs, ...); } func AdaptorCommand(int action, object obj, args, array allArgs) { var val = ScopedVar(args.Variable); var oldVal = val; allArgs[Menu_CallbackArg_Args] = args.Args; var reaction = CallCallbacks(args.Callbacks, action, allArgs, Menu_React_Refresh, true); if(reaction != Menu_React_Refresh) { return reaction; } if(args.Type == Menu_AdaptorType_Boolean) { if(action & (Menu_CallbackType_Normal | Menu_CallbackType_Special2)) { ScopedVar(args.Variable) = val = !val; reaction = AdaptorCommandCallChangedCallback(args.Callbacks, val, oldVal, allArgs); if(reaction != Menu_React_None) { return reaction; } } else { return Menu_React_KeepOpen; } } else if(args.Type == Menu_AdaptorType_Integer) { var limits = AdaptorGetLimits(args.Limits); var wrapAround = args.WrapAround; var step = args.StepSize; var stepSize = 1; if(step) { stepSize = step[0]; } if(action == Menu_CallbackType_Special2) { if(msgBoardMode == 0) { msgBoardMode = Menu_AdaptorType_Integer + 1; msgBoardEntry = args.EntryIndex; CallMessageBoard(this, false, args.MessageBoardText, GetOwner(obj)); } return Menu_React_KeepOpen; } else if(action == Menu_CallbackType_Normal) { val += stepSize; } else if(action == Menu_CallbackType_Close) { val -= stepSize; } else { return Menu_React_KeepOpen; } val = WrapOrBind(val, limits, wrapAround); if(val != oldVal) { ScopedVar(args.Variable) = val; reaction = AdaptorCommandCallChangedCallback(args.Callbacks, val, oldVal, allArgs); if(reaction != Menu_React_None) { return reaction; } } else { return Menu_React_KeepOpen; } } else if(args.Type == Menu_AdaptorType_String) { if(action & (Menu_CallbackType_Normal | Menu_CallbackType_Special2)) { if(msgBoardMode == 0) { msgBoardMode = Menu_AdaptorType_String + 1; msgBoardEntry = args.EntryIndex; CallMessageBoard(this, false, args.MessageBoardText, GetOwner(obj)); } } if(action == Menu_CallbackType_Close) { return Menu_React_None; } else { return Menu_React_KeepOpen; } } else if(args.Type == Menu_AdaptorType_ID) { if(action & (Menu_CallbackType_Normal | Menu_CallbackType_Special2)) { if(msgBoardMode == 0) { msgBoardMode = Menu_AdaptorType_ID + 1; msgBoardEntry = args.EntryIndex; CallMessageBoard(this, false, args.MessageBoardText, GetOwner(obj)); } } return Menu_React_KeepOpen; } else if(args.Type == Menu_AdaptorType_Enum) { if(GetType(args.Enum.SubMenu.On) == C4V_Bool) { if(action & Menu_CallbackType_Defaults) { return Menu_React_ShowSubMenu; } else { return Menu_React_None; } } else if(args.Enum.SubMenu.On & action) { return Menu_React_ShowSubMenu; } var enumVals = args.Enum.Values; var layout = args.Enum.Layout; var index = EnumValPos(enumVals, layout, val, args.Enum.AllowUnknown); var wrapAround = args.WrapAround; if(action & (Menu_CallbackType_Normal | Menu_CallbackType_Special2)) { ++index; } else if(action == Menu_CallbackType_Close) { --index; } else { return Menu_React_None; } index = WrapOrBind(index, [0, GetLength(enumVals) - 1], wrapAround); ScopedVar(args.Variable) = val = enumVals[index][layout[Menu_Layout__ValuePos] - 1]; reaction = AdaptorCommandCallChangedCallback(args.Callbacks, val, oldVal, allArgs); if(reaction != Menu_React_None) { return reaction; } } else if(args.Type == Menu_AdaptorType_BitField) { if(action & (Menu_CallbackType_Normal | Menu_CallbackType_Special2)) { var mask = args.BitField.Mask; if((val & mask) == mask) { val &= ~mask; } else { val |= mask; } ScopedVar(args.Variable) = val; reaction = AdaptorCommandCallChangedCallback(args.Callbacks, val, oldVal, allArgs); if(reaction != Menu_React_None) { return reaction; } } else { return Menu_React_KeepOpen; } } return Menu_React_Refresh; } func InputCallback(string input, int plr) { var entry = columnEntries[msgBoardEntry[1]][msgBoardEntry[0]]; var args = entry.Args; var callbackArgs = args.Args; var oldVal = ScopedVar(args.Variable); var val = input; if(msgBoardMode - 1 == Menu_AdaptorType_Integer) { var int = ParseInt(input); var limits = AdaptorGetLimits(args.Limits); var step = args.StepSize; if(GetType(int) == C4V_Int) { if(step && step[1]) { var s = step[0]; if(GetType(step[1]) == C4V_Int) { s = step[1]; } int = ((int + s / 2) / s) * s; } if(limits) { int = BoundBy(int, limits[0], limits[1]); } ScopedVar(args.Variable) = val = int; } else { val = oldVal; } } else if(msgBoardMode - 1 == Menu_AdaptorType_String) { if(args.NoEmptyString && (!input || input == "")) { val = oldVal; } else { ScopedVar(args.Variable) = input; } } else if(msgBoardMode - 1 == Menu_AdaptorType_ID) { val = GetIDByName(input); // WARNING: desyncs between clients with different languages if(!val && GetLength(input) == 4) val = C4Id(input); if(!GetName(0, val)) val = 0; ScopedVar(args.Variable) = val; } msgBoardMode = 0; if(val != oldVal) { var reaction = CallCallbacks(args.Callbacks, Menu_CallbackType_ValueChanged, [Menu_CallbackType_ValueChanged, entry.Icon, settings.Object, callbackArgs, 0, 0, val, oldVal]); if(reaction != Menu_React_None) { return React(reaction, msgBoardEntry); } } Refresh(msgBoardEntry); } /* CreateNewMenu({ Settings = { Icon = <Icon>, Title = <Title>, Object = <Object for which the menu is shown>, Style = <Same as Style of CreateMenu>, Size = [<Columns>, <Rows>] passed to SetMenuSize(), Selecion = <Initial Selection>, KeepOpen = <Keep Open Mode>: Menu_KeepOpen_*, RefreshInterval = <Interval in Frames> for Menu_KeepOpen_RefreshContinuously, Callbacks = [<Menu_Callback()...>] global Callbacks, Decoration = <Decoration ID> passed to SetMenuDecoration(), RequireAction = [<Action Name>] | [<Action Name>, <Action Target>] required action (and target) for the Settings.Object; if not fulfilled the menu will close itself, KeepParentOnClose = <Should the parent menu stay open when the submenu closes>: true | >false<, Vars = [<Initial values of MenuVar() ScopedVariables>...], Closable = <Can the menu be closed by pressing dig / clicking the X in the corner>: true | >false<, InstantDescription = <If enabled, shows the description of an entry immediately in a separate CustomMessage()>: { Enable = true | >false<, Decoration = <Decoration> passed to CustomMessage(); >DT_Menu_DefaultInstantDescriptionDecoration<, Position = [<x>, <y>] passed to CustomMessage(); >[40, -60]<, Flags = <Flags> passed to CustomMessage(); >MSG_Left | MSG_Bottom< }, Extra = <Same as extra of CreateMenu>, ExtraData = <Same as extraData of CreateMenu>, Condition = { DisableMode = <How to handle disabled entries>: >Menu_ConditionReact_Hide< | Menu_ConditionReact_*, AllowSelection = <Allow disabled entries to be selected>: true | >false< }, Parent = >Internal<: Used for submenus }, Entries = [Menu_Columns()] | [<Menu_Entry() | Menu_Text() | Menu_Blank() | Menu_Decline() | Menu_Accept() | Menu_Back() | Menu_Adaptor() | | Menu_SubMenu() | Menu_Factory()>...] }) */ global func CreateNewMenu(map settings, map inheritSettings, object parentMenu) { inheritSettings = inheritSettings || {}; var menuObj = CreateObject(MN7I); settings = Extend(Extend(Extend( { Settings = { Condition = { DisableMode = Menu_ConditionReact_Hide }, InstantDescription = { Decoration = DT_Menu_DefaultInstantDescriptionDecoration, Flags = MSG_Left | MSG_Bottom, Position = [40, -60] }, KeepOpen = 0, } }, { Settings = inheritSettings }, true), { Settings = { Selection = [], // Selection can't be inherited Callbacks = [], // Global callbacks can't be inherited (maybe filter callbacks) Parent = parentMenu } }, true), settings, true); if(GetType(settings.Settings.Selection) == C4V_Int) { settings.Settings.Selection = [settings.Settings.Selection, 0]; } if(inheritSettings.Style) { // EqualItemHeight can't be inherited settings.Settings.Style &= ~C4MN_Style_EqualItemHeight; } menuObj->Create(settings); return menuObj; } func DeclineAcceptBackCommand(int action, args, int defaultAction, array allArgs) { if(args[0]) { allArgs[Menu_CallbackArg_Args] = args[1]; var reaction = CallCallbacks(args[0], action, allArgs); if(reaction != Menu_React_None) { return reaction; } } if(action & Menu_CallbackType_Defaults) { return defaultAction; } else { return Menu_React_None; } } func DeclineAcceptCommand(int action, args, array allArgs) { return DeclineAcceptBackCommand(action, args, Menu_React_Close, allArgs); } func BackCommand(int action, args, array allArgs) { return DeclineAcceptBackCommand(action, args, Menu_React_Back, allArgs); } func MenuObjectCallback(string name) { return [CallbackTarget_Scenario - 1, name]; } func CallCustom(callback, args) { if(GetType(callback) == C4V_Array && GetLength(callback) == 2 && callback[0] == CallbackTarget_Scenario - 1) { return CallA(ObjectCallback(callback[1], true), args, ...); } else { return _inherited(callback, args, ...); } } func CustomScopedVar(array variable) { if(variable && variable[0] == DT_Menu_MenuVar) { return ArrayVar(variable[1], LocalVar("vars")); } else { return _inherited(variable, ...); } } func CheckCustomScopedVar(array variable) { if(variable && variable[0] == DT_Menu_MenuVar && ((GetType(variable[1]) == C4V_Int && variable[1] > 0) || variable[1] == 0)) { return true; } else { return _inherited(variable, ...); } } func Update() { return Refresh(GetSelection(Menu_Selection_WithColumn)); } func GetSelection(int mode) { mode ??= Menu_Selection_Simple; if(mode == Menu_Selection_Simple) { return currentColumnSelections[currentColumn]; } else if(mode == Menu_Selection_WithColumn) { return [currentColumnSelections[currentColumn], currentColumn]; } else if(mode == Menu_Selection_SubMenuColumnChain) { if(multiColumnMode) { FatalError("DT_Menu::GetSelection: Menu_Selection_SubMenuColumnChain doesn't make sense when Menu_Columns() are used"); } var ret = []; for(var i = 0; i < columnEntries; ++i) { if(!columnEntries[i]) { break; } ret[] = currentColumnSelections[i]; } return ret; } } func GetObject() { return settings.Object; } func CreateIconDummy() { return CreateContents(DT_Menu_IconDummy); } // deprecated compatibility functions and constants // Symbol -> Icon static const Menu_Layout_Symbol = Menu_Layout_Icon; static const Menu_CallbackArg_Symbol = Menu_CallbackArg_Icon; func ValidateDeco(id deco) { return !deco || (deco->~FrameDecorationBackClr() || deco->~FrameDecorationBorderTop() || deco->~FrameDecorationBorderLeft() || deco->~FrameDecorationBorderRight() || deco->~FrameDecorationBorderBottom()); } func ValidateMenuCallbacks(array callbacks) { if(!callbacks) { return false; } for(var callback in callbacks) { if(!ValidateMenuCallback(callback)) { return false; } } return true; } func ValidateMenuCallbackArgBinding(array binding) { for(var arg in binding) { if(arg > Menu_CallbackArg_Max || arg < Menu_CallbackArg_Min) { return false; } } return true; } func ValidateMenuCallback(array callback) { if(!callback) { return false; } return GetLength(callback) == 3 && GetType(callback[1]) == C4V_Int && (callback[1] & ~Menu_CallbackType_All) == 0 && CheckCallback(callback[0]) && ValidateMenuCallbackArgBinding(callback[2]); } func CheckCallback(callback) { var ret = _inherited(callback, ...); if(ret && GetType(callback) == C4V_String) { Log("WARNING: DT_Menu::CheckCallback: A string only callback will not work as expected (yet): \"%s\"", callback); } return ret; } func CheckCustomCallback(callback) { if(GetType(callback) == C4V_Array && GetLength(callback) == 2 && callback[0] == (CallbackTarget_Scenario - 1) && GetType(callback[1]) == C4V_String && callback[1] != "") { return true; } return _inherited(callback, ...); }