diff --git a/src/g_cvars.cpp b/src/g_cvars.cpp index 3d1a9167d..433b88888 100644 --- a/src/g_cvars.cpp +++ b/src/g_cvars.cpp @@ -51,6 +51,9 @@ CVAR(Int, developer, 0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) // [RH] Feature control cvars CVAR(Bool, var_friction, true, CVAR_SERVERINFO); +// Option Search +CVAR(Bool, os_isanyof, true, CVAR_ARCHIVE); + @@ -138,4 +141,3 @@ CUSTOM_CVAR(String, language, "auto", CVAR_ARCHIVE | CVAR_NOINITCALL | CVAR_GLOB if (Level->info != nullptr) Level->LevelName = Level->info->LookupLevelName(); } } - diff --git a/wadsrc/static/menudef.txt b/wadsrc/static/menudef.txt index 403123db7..5c4b02bf6 100644 --- a/wadsrc/static/menudef.txt +++ b/wadsrc/static/menudef.txt @@ -378,6 +378,8 @@ OptionMenu "OptionsMenu" protected Submenu "$OPTMNU_DISPLAY", "VideoOptions" Submenu "$OPTMNU_VIDEO", "VideoModeMenu" StaticText " " + Submenu "$OS_TITLE", "os_Menu" + StaticText " " SafeCommand "$OPTMNU_DEFAULTS", "reset2defaults" SafeCommand "$OPTMNU_RESETTOSAVED", "reset2saved" Command "$OPTMNU_CONSOLE", "menuconsole" @@ -2606,3 +2608,21 @@ OptionString "LanguageOptions" "ptb", "Português do Brasil (PTB)" "rus", "Русский (RU)" } + +/*======================================= + * + * Option Search menu + * + *=======================================*/ + +OptionMenu "os_Menu" +{ + Class "os_Menu" + Title "$OS_TITLE" +} + +OptionValue "os_isanyof_values" +{ + 0, "$OS_ALL" + 1, "$OS_ANY" +} diff --git a/wadsrc/static/zscript.txt b/wadsrc/static/zscript.txt index 705f29798..54e5f00b8 100644 --- a/wadsrc/static/zscript.txt +++ b/wadsrc/static/zscript.txt @@ -253,6 +253,10 @@ version "3.8" #include "zscript/ui/menu/reverbedit.zs" #include "zscript/ui/menu/textentermenu.zs" +#include "zscript/ui/menu/search/menu.zs" +#include "zscript/ui/menu/search/searchfield.zs" +#include "zscript/ui/menu/search/query.zs" + #include "zscript/ui/statscreen/statscreen.zs" #include "zscript/ui/statscreen/statscreen_coop.zs" #include "zscript/ui/statscreen/statscreen_dm.zs" diff --git a/wadsrc/static/zscript/ui/menu/search/menu.zs b/wadsrc/static/zscript/ui/menu/search/menu.zs new file mode 100644 index 000000000..f8cd06e82 --- /dev/null +++ b/wadsrc/static/zscript/ui/menu/search/menu.zs @@ -0,0 +1,212 @@ +//============================================================================= +// +// Option Search Menu class. +// This menu contains search field, and is dynamically filled with search +// results. +// +//============================================================================= + +class os_Menu : OptionMenu +{ + override void Init(Menu parent, OptionMenuDescriptor desc) + { + Super.Init(parent, desc); + + mDesc.mItems.clear(); + + addSearchField(); + + mDesc.mScrollPos = 0; + mDesc.mSelectedItem = 0; + mDesc.CalcIndent(); + } + + void search(os_Query query) + { + mDesc.mItems.clear(); + + addSearchField(query.getText()); + + string path = StringTable.Localize("$OPTMNU_TITLE"); + bool isAnyTermMatches = mIsAnyOfItem.mCVar.GetBool(); + bool found = listOptions(mDesc, "MainMenu", query, path, isAnyTermMatches); + + if (!found) { addNoResultsItem(mDesc); } + + mDesc.CalcIndent(); + } + + private void addSearchField(string query = "") + { + string searchLabel = StringTable.Localize("$OS_LABEL"); + let searchField = new("os_SearchField").Init(searchLabel, self, query); + + mIsAnyOfItem = new("OptionMenuItemOption").Init("", "os_isanyof", "os_isanyof_values"); + + mDesc.mItems.push(searchField); + mDesc.mItems.push(mIsAnyOfItem); + addEmptyLine(mDesc); + } + + private static bool listOptions(OptionMenuDescriptor targetDesc, + string menuName, + os_Query query, + string path, + bool isAnyTermMatches) + { + let desc = MenuDescriptor.GetDescriptor(menuName); + let listMenuDesc = ListMenuDescriptor(desc); + + if (listMenuDesc) + { + return listOptionsListMenu(listMenuDesc, targetDesc, query, path, isAnyTermMatches); + } + + let optionMenuDesc = OptionMenuDescriptor(desc); + + if (optionMenuDesc) + { + return listOptionsOptionMenu(optionMenuDesc, targetDesc, query, path, isAnyTermMatches); + } + + return false; + } + + private static bool listOptionsListMenu(ListMenuDescriptor sourceDesc, + OptionMenuDescriptor targetDesc, + os_Query query, + string path, + bool isAnyTermMatches) + { + int nItems = sourceDesc.mItems.size(); + bool found = false; + + for (int i = 0; i < nItems; ++i) + { + let item = sourceDesc.mItems[i]; + string actionN = item.GetAction(); + string newPath = (actionN == "Optionsmenu") + ? StringTable.Localize("$OPTMNU_TITLE") + : StringTable.Localize("$OS_MAIN"); + + found |= listOptions(targetDesc, actionN, query, newPath, isAnyTermMatches); + } + + return found; + } + + private static bool listOptionsOptionMenu(OptionMenuDescriptor sourceDesc, + OptionMenuDescriptor targetDesc, + os_Query query, + string path, + bool isAnyTermMatches) + { + if (sourceDesc == targetDesc) { return false; } + + int nItems = sourceDesc.mItems.size(); + bool first = true; + bool found = false; + + for (int i = 0; i < nItems; ++i) + { + let item = sourceDesc.mItems[i]; + + if (item is "OptionMenuItemStaticText") { continue; } + + string label = StringTable.Localize(item.mLabel); + + if (!query.matches(label, isAnyTermMatches)) { continue; } + + found = true; + + if (first) + { + addEmptyLine(targetDesc); + addPathItem(targetDesc, path); + + first = false; + } + + let itemOptionBase = OptionMenuItemOptionBase(item); + + if (itemOptionBase) + { + itemOptionBase.mCenter = false; + } + + targetDesc.mItems.push(item); + } + + for (int i = 0; i < nItems; ++i) + { + let item = sourceDesc.mItems[i]; + string label = StringTable.Localize(item.mLabel); + string optionSearchTitle = StringTable.Localize("$OS_TITLE"); + + if (label == optionSearchTitle) { continue; } + + if (item is "OptionMenuItemSubMenu") + { + string newPath = makePath(path, label); + + found |= listOptions(targetDesc, item.GetAction(), query, newPath, isAnyTermMatches); + } + } + + return found; + } + + private static string makePath(string path, string label) + { + int pathWidth = SmallFont.StringWidth(path .. "/" .. label); + int screenWidth = Screen.GetWidth(); + bool isTooWide = (pathWidth > screenWidth / 3); + string newPath = isTooWide + ? path .. "/" .. "\n" .. label + : path .. "/" .. label; + + return newPath; + } + + private static void addPathItem(OptionMenuDescriptor desc, string path) + { + Array lines; + path.split(lines, "\n"); + + int nLines = lines.size(); + + for (int i = 0; i < nLines; ++i) + { + OptionMenuItemStaticText text = new("OptionMenuItemStaticText").Init(lines[i], 1); + + desc.mItems.push(text); + } + } + + private static void addEmptyLine(OptionMenuDescriptor desc) + { + int nItems = desc.mItems.size(); + + if (nItems > 0) + { + let staticText = OptionMenuItemStaticText(desc.mItems[nItems - 1]); + + if (staticText != null && staticText.mLabel == "") { return; } + } + + let item = new("OptionMenuItemStaticText").Init(""); + + desc.mItems.push(item); + } + + private static void addNoResultsItem(OptionMenuDescriptor desc) + { + string noResults = StringTable.Localize("$OS_NO_RESULTS"); + let text = new("OptionMenuItemStaticText").Init(noResults, 0); + + addEmptyLine(desc); + desc.mItems.push(text); + } + + private OptionMenuItemOption mIsAnyOfItem; +} diff --git a/wadsrc/static/zscript/ui/menu/search/query.zs b/wadsrc/static/zscript/ui/menu/search/query.zs new file mode 100644 index 000000000..75e1d8395 --- /dev/null +++ b/wadsrc/static/zscript/ui/menu/search/query.zs @@ -0,0 +1,78 @@ +//============================================================================= +// +// Option Search Query class represents a search query. +// A search query consists constists of one or more terms (words). +// +// Query matching deponds on "os_is_any_of" variable. +// If this variable is "true", the text matches the query if any of the terms +// matches the query. +// If this variable is "false", the text matches the query only if all the +// terms match the query. +// +//============================================================================= + +class os_Query +{ + static os_Query fromString(string str) + { + let query = new("os_Query"); + + str.Split(query.mQueryParts, " ", TOK_SKIPEMPTY); + + query.mText = str; + + return query; + } + + bool matches(string text, bool isSearchForAny) + { + return isSearchForAny + ? matchesAny(text) + : matchesAll(text); + } + + string getText() { return mText; } + + // private: ////////////////////////////////////////////////////////////////// + + private bool matchesAny(string text) + { + int nParts = mQueryParts.size(); + + for (int i = 0; i < nParts; ++i) + { + string queryPart = mQueryParts[i]; + + if (contains(text, queryPart)) { return true; } + } + + return false; + } + + private bool matchesAll(string text) + { + int nParts = mQueryParts.size(); + + for (int i = 0; i < nParts; ++i) + { + string queryPart = mQueryParts[i]; + + if (!contains(text, queryPart)) { return false; } + } + + return true; + } + + private static bool contains(string str, string substr) + { + str .toLower(); + substr.toLower(); + + bool contains = (str.IndexOf(substr) != -1); + + return contains; + } + + private string mText; + private Array mQueryParts; +} diff --git a/wadsrc/static/zscript/ui/menu/search/searchfield.zs b/wadsrc/static/zscript/ui/menu/search/searchfield.zs new file mode 100644 index 000000000..194501022 --- /dev/null +++ b/wadsrc/static/zscript/ui/menu/search/searchfield.zs @@ -0,0 +1,51 @@ +//============================================================================= +// +// Option Search Field class. +// +// When the search query is entered, makes Search Menu perform a search. +// +//============================================================================= + +class os_SearchField : OptionMenuItemTextField +{ + os_SearchField Init(String label, os_Menu menu, string query) + { + Super.Init(label, ""); + + mMenu = menu; + + mText = query; + + return self; + } + + override bool MenuEvent(int mkey, bool fromcontroller) + { + if (mkey == Menu.MKEY_Enter) + { + Menu.MenuSound("menu/choose"); + mEnter = TextEnterMenu.OpenTextEnter(Menu.GetCurrentMenu(), SmallFont, mText, -1, fromcontroller); + mEnter.ActivateMenu(); + return true; + } + if (mkey == Menu.MKEY_Input) + { + string text = mEnter.GetText(); + let query = os_Query.fromString(text); + + mMenu.search(query); + } + + return Super.MenuEvent(mkey, fromcontroller); + } + + override String Represent() + { + return mEnter + ? mEnter.GetText() .. SmallFont.GetCursor() + : mText; + } + + private os_Menu mMenu; + private string mText; +}