From a380f87b5fb08627348504249da830f0065ea836 Mon Sep 17 00:00:00 2001 From: Frank Skare Date: Mon, 19 Jul 2021 15:22:36 +0200 Subject: [PATCH] Profile selection in the context menu --- docs/Changelog.md | 3 +- src/Misc/JSONParser.cs | 415 +++++++++++++++++++++++++++++++++++ src/Resources/input.conf.txt | 2 + src/WinForms/MainForm.cs | 32 +++ src/mpv.net.csproj | 2 + 5 files changed, 453 insertions(+), 1 deletion(-) create mode 100644 src/Misc/JSONParser.cs diff --git a/docs/Changelog.md b/docs/Changelog.md index a3fa718..b91ab7b 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -1,11 +1,12 @@ 5.4.9.2 Beta (2021-??-??) -- Manual translated to simplified Chinese (hooke007) +- Manual translated to simplified Chinese. (hooke007) - Showing the playlist selects the currently played file/stream in the playlist. - Properties are shown in the command palette instead of the text editor making it very easy to find a property and show/print its value. - Support for --keep-open=no. +- Profile selection in the context menu. 5.4.9.1 Beta (2021-06-23) diff --git a/src/Misc/JSONParser.cs b/src/Misc/JSONParser.cs new file mode 100644 index 0000000..3e2f365 --- /dev/null +++ b/src/Misc/JSONParser.cs @@ -0,0 +1,415 @@ + +// https://github.com/zanders3/json + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.Serialization; +using System.Text; + +namespace mpvnet +{ + public static class JSONParser + { + [ThreadStatic] static Stack> splitArrayPool; + [ThreadStatic] static StringBuilder stringBuilder; + [ThreadStatic] static Dictionary> fieldInfoCache; + [ThreadStatic] static Dictionary> propertyInfoCache; + + public static T FromJson(this string json) + { + // Initialize, if needed, the ThreadStatic variables + if (propertyInfoCache == null) + propertyInfoCache = new Dictionary>(); + + if (fieldInfoCache == null) + fieldInfoCache = new Dictionary>(); + + if (stringBuilder == null) + stringBuilder = new StringBuilder(); + + if (splitArrayPool == null) + splitArrayPool = new Stack>(); + + //Remove all whitespace not within strings to make parsing simpler + stringBuilder.Length = 0; + + for (int i = 0; i < json.Length; i++) + { + char c = json[i]; + + if (c == '"') + { + i = AppendUntilStringEnd(true, i, json); + continue; + } + + if (char.IsWhiteSpace(c)) + continue; + + stringBuilder.Append(c); + } + + //Parse the thing! + return (T)ParseValue(typeof(T), stringBuilder.ToString()); + } + + static int AppendUntilStringEnd(bool appendEscapeCharacter, int startIdx, string json) + { + stringBuilder.Append(json[startIdx]); + + for (int i = startIdx + 1; i < json.Length; i++) + { + if (json[i] == '\\') + { + if (appendEscapeCharacter) + stringBuilder.Append(json[i]); + stringBuilder.Append(json[i + 1]); + i++;//Skip next character as it is escaped + } + else if (json[i] == '"') + { + stringBuilder.Append(json[i]); + return i; + } + else + stringBuilder.Append(json[i]); + } + + return json.Length - 1; + } + + //Splits { :, : } and [ , ] into a list of strings + static List Split(string json) + { + List splitArray = splitArrayPool.Count > 0 ? splitArrayPool.Pop() : new List(); + splitArray.Clear(); + + if (json.Length == 2) + return splitArray; + + int parseDepth = 0; + stringBuilder.Length = 0; + + for (int i = 1; i < json.Length - 1; i++) + { + switch (json[i]) + { + case '[': + case '{': + parseDepth++; + break; + case ']': + case '}': + parseDepth--; + break; + case '"': + i = AppendUntilStringEnd(true, i, json); + continue; + case ',': + case ':': + if (parseDepth == 0) + { + splitArray.Add(stringBuilder.ToString()); + stringBuilder.Length = 0; + continue; + } + break; + } + + stringBuilder.Append(json[i]); + } + + splitArray.Add(stringBuilder.ToString()); + + return splitArray; + } + + internal static object ParseValue(Type type, string json) + { + if (type == typeof(string)) + { + if (json.Length <= 2) + return string.Empty; + + StringBuilder parseStringBuilder = new StringBuilder(json.Length); + + for (int i = 1; i < json.Length - 1; ++i) + { + if (json[i] == '\\' && i + 1 < json.Length - 1) + { + int j = "\"\\nrtbf/".IndexOf(json[i + 1]); + + if (j >= 0) + { + parseStringBuilder.Append("\"\\\n\r\t\b\f/"[j]); + ++i; + continue; + } + if (json[i + 1] == 'u' && i + 5 < json.Length - 1) + { + uint c = 0; + + if (uint.TryParse(json.Substring(i + 2, 4), System.Globalization.NumberStyles.AllowHexSpecifier, null, out c)) + { + parseStringBuilder.Append((char)c); + i += 5; + continue; + } + } + } + + parseStringBuilder.Append(json[i]); + } + + return parseStringBuilder.ToString(); + } + + if (type.IsPrimitive) + { + var result = Convert.ChangeType(json, type, System.Globalization.CultureInfo.InvariantCulture); + return result; + } + + if (type == typeof(decimal)) + { + decimal result; + decimal.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out result); + return result; + } + + if (json == "null") + return null; + + if (type.IsEnum) + { + if (json[0] == '"') + json = json.Substring(1, json.Length - 2); + + try + { + return Enum.Parse(type, json, false); + } + catch + { + return 0; + } + } + + if (type.IsArray) + { + Type arrayType = type.GetElementType(); + + if (json[0] != '[' || json[json.Length - 1] != ']') + return null; + + List elems = Split(json); + Array newArray = Array.CreateInstance(arrayType, elems.Count); + + for (int i = 0; i < elems.Count; i++) + newArray.SetValue(ParseValue(arrayType, elems[i]), i); + + splitArrayPool.Push(elems); + return newArray; + } + + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) + { + Type listType = type.GetGenericArguments()[0]; + + if (json[0] != '[' || json[json.Length - 1] != ']') + return null; + + List elems = Split(json); + var list = (IList)type.GetConstructor(new Type[] { typeof(int) }).Invoke(new object[] { elems.Count }); + + for (int i = 0; i < elems.Count; i++) + list.Add(ParseValue(listType, elems[i])); + + splitArrayPool.Push(elems); + return list; + } + + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) + { + Type keyType, valueType; + { + Type[] args = type.GetGenericArguments(); + keyType = args[0]; + valueType = args[1]; + } + + //Refuse to parse dictionary keys that aren't of type string + if (keyType != typeof(string)) + return null; + + //Must be a valid dictionary element + if (json[0] != '{' || json[json.Length - 1] != '}') + return null; + + //The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON + List elems = Split(json); + + if (elems.Count % 2 != 0) + return null; + + var dictionary = (IDictionary)type.GetConstructor(new Type[] { typeof(int) }).Invoke(new object[] { elems.Count / 2 }); + + for (int i = 0; i < elems.Count; i += 2) + { + if (elems[i].Length <= 2) + continue; + string keyValue = elems[i].Substring(1, elems[i].Length - 2); + object val = ParseValue(valueType, elems[i + 1]); + dictionary[keyValue] = val; + } + + return dictionary; + } + + if (type == typeof(object)) + return ParseAnonymousValue(json); + + if (json[0] == '{' && json[json.Length - 1] == '}') + return ParseObject(type, json); + + return null; + } + + static object ParseAnonymousValue(string json) + { + if (json.Length == 0) + return null; + + if (json[0] == '{' && json[json.Length - 1] == '}') + { + List elems = Split(json); + + if (elems.Count % 2 != 0) + return null; + + var dict = new Dictionary(elems.Count / 2); + + for (int i = 0; i < elems.Count; i += 2) + dict[elems[i].Substring(1, elems[i].Length - 2)] = ParseAnonymousValue(elems[i + 1]); + + return dict; + } + + if (json[0] == '[' && json[json.Length - 1] == ']') + { + List items = Split(json); + var finalList = new List(items.Count); + + for (int i = 0; i < items.Count; i++) + finalList.Add(ParseAnonymousValue(items[i])); + + return finalList; + } + + if (json[0] == '"' && json[json.Length - 1] == '"') + { + string str = json.Substring(1, json.Length - 2); + return str.Replace("\\", string.Empty); + } + + if (char.IsDigit(json[0]) || json[0] == '-') + { + if (json.Contains(".")) + { + double result; + double.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out result); + return result; + } + else + { + int result; + int.TryParse(json, out result); + return result; + } + } + + if (json == "true") + return true; + + if (json == "false") + return false; + + // handles json == "null" as well as invalid JSON + return null; + } + + static Dictionary CreateMemberNameDictionary(T[] members) where T : MemberInfo + { + Dictionary nameToMember = new Dictionary(StringComparer.OrdinalIgnoreCase); + + for (int i = 0; i < members.Length; i++) + { + T member = members[i]; + + if (member.IsDefined(typeof(IgnoreDataMemberAttribute), true)) + continue; + + string name = member.Name; + + if (member.IsDefined(typeof(DataMemberAttribute), true)) + { + DataMemberAttribute dataMemberAttribute = (DataMemberAttribute)Attribute.GetCustomAttribute(member, typeof(DataMemberAttribute), true); + + if (!string.IsNullOrEmpty(dataMemberAttribute.Name)) + name = dataMemberAttribute.Name; + } + + nameToMember.Add(name, member); + } + + return nameToMember; + } + + static object ParseObject(Type type, string json) + { + object instance = FormatterServices.GetUninitializedObject(type); + + //The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON + List elems = Split(json); + + if (elems.Count % 2 != 0) + return instance; + + Dictionary nameToField; + Dictionary nameToProperty; + + if (!fieldInfoCache.TryGetValue(type, out nameToField)) + { + nameToField = CreateMemberNameDictionary(type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy)); + fieldInfoCache.Add(type, nameToField); + } + + if (!propertyInfoCache.TryGetValue(type, out nameToProperty)) + { + nameToProperty = CreateMemberNameDictionary(type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy)); + propertyInfoCache.Add(type, nameToProperty); + } + + for (int i = 0; i < elems.Count; i += 2) + { + if (elems[i].Length <= 2) + continue; + + string key = elems[i].Substring(1, elems[i].Length - 2); + string value = elems[i + 1]; + + FieldInfo fieldInfo; + PropertyInfo propertyInfo; + + if (nameToField.TryGetValue(key, out fieldInfo)) + fieldInfo.SetValue(instance, ParseValue(fieldInfo.FieldType, value)); + else if (nameToProperty.TryGetValue(key, out propertyInfo)) + propertyInfo.SetValue(instance, ParseValue(propertyInfo.PropertyType, value), null); + } + + return instance; + } + } +} diff --git a/src/Resources/input.conf.txt b/src/Resources/input.conf.txt index c9663a8..e5f4f0d 100644 --- a/src/Resources/input.conf.txt +++ b/src/Resources/input.conf.txt @@ -158,6 +158,8 @@ _ script-message mpv.net show-protocols #menu: View > Show Protocols F9 show-text ${track-list} 5000 #menu: View > Show Tracks Ctrl+m script-message mpv.net show-media-info #menu: View > Show Media Info +_ ignore #menu: Profile + c script-message mpv.net show-conf-editor #menu: Settings > Show Config Editor Ctrl+i script-message mpv.net show-input-editor #menu: Settings > Show Input Editor Ctrl+f script-message mpv.net open-conf-folder #menu: Settings > Open Config Folder diff --git a/src/WinForms/MainForm.cs b/src/WinForms/MainForm.cs index 24968e4..49adbb4 100644 --- a/src/WinForms/MainForm.cs +++ b/src/WinForms/MainForm.cs @@ -302,6 +302,38 @@ namespace mpvnet () => Core.SetBluRayTitle(item.Index)); } } + + MenuItem profiles = FindMenuItem("Profile"); + + if (profiles != null) + { + profiles.DropDownItems.Clear(); + + foreach (string profile in ProfileNames) + if (!profile.StartsWith("extension.")) + MenuItem.Add(profiles.DropDownItems, profile, + () => { + Core.CommandV("show-text", profile); + Core.CommandV("apply-profile", profile); + }); + } + } + + private string[] _ProfileNames; + + public string[] ProfileNames { + get { + if (_ProfileNames == null) + { + string[] ignore = { "builtin-pseudo-gui", "encoding", "libmpv", "pseudo-gui", "default" }; + string profileList = Core.GetPropertyString("profile-list"); + var json = profileList.FromJson>>(); + _ProfileNames = json.Select(i => i["name"].ToString()) + .Where(i => !ignore.Contains(i)).ToArray(); + } + + return _ProfileNames; + } } MenuItem FindMenuItem(string text, ToolStripItemCollection items) diff --git a/src/mpv.net.csproj b/src/mpv.net.csproj index 1c3a03b..5995c1d 100644 --- a/src/mpv.net.csproj +++ b/src/mpv.net.csproj @@ -69,6 +69,7 @@ ..\packages\Microsoft.PowerShell.5.ReferenceAssemblies.1.1.0\lib\net4\System.Management.Automation.dll + @@ -78,6 +79,7 @@ +