diff --git a/Flow.Launcher.Test/Flow.Launcher.Test.csproj b/Flow.Launcher.Test/Flow.Launcher.Test.csproj index 1164e5ebea6..11ccff05b05 100644 --- a/Flow.Launcher.Test/Flow.Launcher.Test.csproj +++ b/Flow.Launcher.Test/Flow.Launcher.Test.csproj @@ -39,6 +39,7 @@ + diff --git a/Flow.Launcher.Test/Plugins/CalculatorTest.cs b/Flow.Launcher.Test/Plugins/CalculatorTest.cs new file mode 100644 index 00000000000..b075813dbb6 --- /dev/null +++ b/Flow.Launcher.Test/Plugins/CalculatorTest.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Flow.Launcher.Plugin.Calculator; +using Mages.Core; +using NUnit.Framework; +using NUnit.Framework.Legacy; + +namespace Flow.Launcher.Test.Plugins +{ + [TestFixture] + public class CalculatorPluginTest + { + private readonly Main _plugin; + private readonly Settings _settings = new() + { + DecimalSeparator = DecimalSeparator.UseSystemLocale, + MaxDecimalPlaces = 10, + ShowErrorMessage = false // Make sure we return the empty results when error occurs + }; + private readonly Engine _engine = new(new Configuration + { + Scope = new Dictionary + { + { "e", Math.E }, // e is not contained in the default mages engine + } + }); + + public CalculatorPluginTest() + { + _plugin = new Main(); + + var settingField = typeof(Main).GetField("_settings", BindingFlags.NonPublic | BindingFlags.Instance); + if (settingField == null) + Assert.Fail("Could not find field '_settings' on Flow.Launcher.Plugin.Calculator.Main"); + settingField.SetValue(_plugin, _settings); + + var engineField = typeof(Main).GetField("MagesEngine", BindingFlags.NonPublic | BindingFlags.Static); + if (engineField == null) + Assert.Fail("Could not find static field 'MagesEngine' on Flow.Launcher.Plugin.Calculator.Main"); + engineField.SetValue(null, _engine); + } + + // Basic operations + [TestCase(@"1+1", "2")] + [TestCase(@"2-1", "1")] + [TestCase(@"2*2", "4")] + [TestCase(@"4/2", "2")] + [TestCase(@"2^3", "8")] + // Decimal places + [TestCase(@"10/3", "3.3333333333")] + // Parentheses + [TestCase(@"(1+2)*3", "9")] + [TestCase(@"2^(1+2)", "8")] + // Functions + [TestCase(@"pow(2,3)", "8")] + [TestCase(@"min(1,-1,-2)", "-2")] + [TestCase(@"max(1,-1,-2)", "1")] + [TestCase(@"sqrt(16)", "4")] + [TestCase(@"sin(pi)", "0.0000000000")] + [TestCase(@"cos(0)", "1")] + [TestCase(@"tan(0)", "0")] + [TestCase(@"log10(100)", "2")] + [TestCase(@"log(100)", "2")] + [TestCase(@"log2(8)", "3")] + [TestCase(@"ln(e)", "1")] + [TestCase(@"abs(-5)", "5")] + // Constants + [TestCase(@"pi", "3.1415926536")] + // Complex expressions + [TestCase(@"(2+3)*sqrt(16)-log(100)/ln(e)", "18")] + [TestCase(@"sin(pi/2)+cos(0)+tan(0)", "2")] + // Error handling (should return empty result) + [TestCase(@"10/0", "")] + [TestCase(@"sqrt(-1)", "")] + [TestCase(@"log(0)", "")] + [TestCase(@"invalid_expression", "")] + public void CalculatorTest(string expression, string result) + { + ClassicAssert.AreEqual(GetCalculationResult(expression), result); + } + + private string GetCalculationResult(string expression) + { + var results = _plugin.Query(new Plugin.Query() + { + Search = expression + }); + return results.Count > 0 ? results[0].Title : string.Empty; + } + } +} diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml index b71e5d8a0e0..b12972b1b84 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml @@ -4,7 +4,7 @@ xmlns:system="clr-namespace:System;assembly=mscorlib"> Calculator - Perform mathematical calculations (including hexadecimal values). Use ',' or '.' as thousand separator or decimal place. + Perform mathematical calculations, including hex values and advanced functions such as 'min(1,2,3)', 'sqrt(123)' and 'cos(123)'. Not a number (NaN) Expression wrong or incomplete (Did you forget some parentheses?) Copy this number to the clipboard @@ -15,4 +15,5 @@ Dot (.) Max. decimal places Copy failed, please try later + Show error message when calculation fails \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs index 6878c54b4a8..9d5e4700fff 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs @@ -13,30 +13,24 @@ namespace Flow.Launcher.Plugin.Calculator { public class Main : IPlugin, IPluginI18n, ISettingProvider { - private static readonly Regex RegValidExpressChar = MainRegexHelper.GetRegValidExpressChar(); - private static readonly Regex RegBrackets = MainRegexHelper.GetRegBrackets(); private static readonly Regex ThousandGroupRegex = MainRegexHelper.GetThousandGroupRegex(); private static readonly Regex NumberRegex = MainRegexHelper.GetNumberRegex(); + private static readonly Regex PowRegex = MainRegexHelper.GetPowRegex(); + private static readonly Regex LogRegex = MainRegexHelper.GetLogRegex(); + private static readonly Regex LnRegex = MainRegexHelper.GetLnRegex(); + private static readonly Regex FunctionRegex = MainRegexHelper.GetFunctionRegex(); private static Engine MagesEngine; private const string Comma = ","; private const string Dot = "."; + private const string IcoPath = "Images/calculator.png"; + private static readonly List EmptyResults = []; internal static PluginInitContext Context { get; set; } = null!; private Settings _settings; private SettingsViewModel _viewModel; - /// - /// Holds the formatting information for a single query. - /// This is used to ensure thread safety by keeping query state local. - /// - private class ParsingContext - { - public string InputDecimalSeparator { get; set; } - public bool InputUsesGroupSeparators { get; set; } - } - public void Init(PluginInitContext context) { Context = context; @@ -54,38 +48,98 @@ public void Init(PluginInitContext context) public List Query(Query query) { - if (!CanCalculate(query)) + if (string.IsNullOrWhiteSpace(query.Search)) { - return new List(); + return EmptyResults; } - var context = new ParsingContext(); - try { - var expression = NumberRegex.Replace(query.Search, m => NormalizeNumber(m.Value, context)); + var search = query.Search; + bool isFunctionPresent = FunctionRegex.IsMatch(search); + + // Mages is case sensitive, so we need to convert all function names to lower case. + search = FunctionRegex.Replace(search, m => m.Value.ToLowerInvariant()); + + var decimalSep = GetDecimalSeparator(); + var groupSep = GetGroupSeparator(decimalSep); + var expression = NumberRegex.Replace(search, m => NormalizeNumber(m.Value, isFunctionPresent, decimalSep, groupSep)); + + // WORKAROUND START: The 'pow' function in Mages v3.0.0 is broken. + // https://github.com/FlorianRappl/Mages/issues/132 + // We bypass it by rewriting any pow(x,y) expression to the equivalent (x^y) expression + // before the engine sees it. This loop handles nested calls. + { + string previous; + do + { + previous = expression; + expression = PowRegex.Replace(previous, PowMatchEvaluator); + } while (previous != expression); + } + // WORKAROUND END + + // WORKAROUND START: The 'log' & 'ln' function in Mages v3.0.0 are broken. + // https://github.com/FlorianRappl/Mages/issues/137 + // We bypass it by rewriting any log & ln expression to the equivalent (log10 & log) expression + // before the engine sees it. This loop handles nested calls. + { + string previous; + do + { + previous = expression; + expression = LogRegex.Replace(previous, LogMatchEvaluator); + } while (previous != expression); + } + { + string previous; + do + { + previous = expression; + expression = LnRegex.Replace(previous, LnMatchEvaluator); + } while (previous != expression); + } + // WORKAROUND END var result = MagesEngine.Interpret(expression); - if (result?.ToString() == "NaN") + if (result == null || string.IsNullOrEmpty(result.ToString())) + { + if (!_settings.ShowErrorMessage) return EmptyResults; + return + [ + new Result + { + Title = Localize.flowlauncher_plugin_calculator_expression_not_complete(), + IcoPath = IcoPath + } + ]; + } + + if (result.ToString() == "NaN") + { result = Localize.flowlauncher_plugin_calculator_not_a_number(); + } if (result is Function) + { result = Localize.flowlauncher_plugin_calculator_expression_not_complete(); + } - if (!string.IsNullOrEmpty(result?.ToString())) + if (!string.IsNullOrEmpty(result.ToString())) { decimal roundedResult = Math.Round(Convert.ToDecimal(result), _settings.MaxDecimalPlaces, MidpointRounding.AwayFromZero); - string newResult = FormatResult(roundedResult, context); + string newResult = FormatResult(roundedResult); - return new List - { + return + [ new Result { Title = newResult, - IcoPath = "Images/calculator.png", + IcoPath = IcoPath, Score = 300, - SubTitle = Localize.flowlauncher_plugin_calculator_copy_number_to_clipboard(), + // Check context nullability for unit testing + SubTitle = Context == null ? string.Empty : Localize.flowlauncher_plugin_calculator_copy_number_to_clipboard(), CopyText = newResult, Action = c => { @@ -101,118 +155,206 @@ public List Query(Query query) } } } - }; + ]; } } catch (Exception) { - // ignored + // Mages engine can throw various exceptions, for simplicity we catch them all and show a generic message. + if (!_settings.ShowErrorMessage) return EmptyResults; + return + [ + new Result + { + Title = Localize.flowlauncher_plugin_calculator_expression_not_complete(), + IcoPath = IcoPath + } + ]; } - return new List(); + return EmptyResults; } - /// - /// Parses a string representation of a number, detecting its format. It uses structural analysis - /// and falls back to system culture for truly ambiguous cases (e.g., "1,234"). - /// It populates the provided ParsingContext with the detected format for later use. - /// - /// A normalized number string with '.' as the decimal separator for the Mages engine. - private string NormalizeNumber(string numberStr, ParsingContext context) + private static string PowMatchEvaluator(Match m) { - var systemGroupSep = CultureInfo.CurrentCulture.NumberFormat.NumberGroupSeparator; - int dotCount = numberStr.Count(f => f == '.'); - int commaCount = numberStr.Count(f => f == ','); + // m.Groups[1].Value will be `(...)` with parens + var contentWithParen = m.Groups[1].Value; + // remove outer parens. `(min(2,3), 4)` becomes `min(2,3), 4` + var argsContent = contentWithParen[1..^1]; - // Case 1: Unambiguous mixed separators (e.g., "1.234,56") - if (dotCount > 0 && commaCount > 0) + var bracketCount = 0; + var splitIndex = -1; + + // Find the top-level comma that separates the two arguments of pow. + for (var i = 0; i < argsContent.Length; i++) { - context.InputUsesGroupSeparators = true; - if (numberStr.LastIndexOf('.') > numberStr.LastIndexOf(',')) - { - context.InputDecimalSeparator = Dot; - return numberStr.Replace(Comma, string.Empty); - } - else + switch (argsContent[i]) { - context.InputDecimalSeparator = Comma; - return numberStr.Replace(Dot, string.Empty).Replace(Comma, Dot); + case '(': + case '[': + bracketCount++; + break; + case ')': + case ']': + bracketCount--; + break; + case ',' when bracketCount == 0: + splitIndex = i; + break; } + + if (splitIndex != -1) + break; + } + + if (splitIndex == -1) + { + // This indicates malformed arguments for pow, e.g., pow(5) or pow(). + // Return original string to let Mages handle the error. + return m.Value; + } + + var arg1 = argsContent[..splitIndex].Trim(); + var arg2 = argsContent[(splitIndex + 1)..].Trim(); + + // Check for empty arguments which can happen with stray commas, e.g., pow(,5) + if (string.IsNullOrEmpty(arg1) || string.IsNullOrEmpty(arg2)) + { + return m.Value; } - // Case 2: Only dots - if (dotCount > 0) + return $"({arg1}^{arg2})"; + } + + private static string LogMatchEvaluator(Match m) + { + // m.Groups[1].Value will be `(...)` with parens + var contentWithParen = m.Groups[1].Value; + var argsContent = contentWithParen[1..^1]; + + // log is unary — if malformed, return original to let Mages handle it + var arg = argsContent.Trim(); + if (string.IsNullOrEmpty(arg)) return m.Value; + + // log(x) -> log10(x) (natural log) + return $"(log10({arg}))"; + } + + private static string LnMatchEvaluator(Match m) + { + // m.Groups[1].Value will be `(...)` with parens + var contentWithParen = m.Groups[1].Value; + var argsContent = contentWithParen[1..^1]; + + // ln is unary — if malformed, return original to let Mages handle it + var arg = argsContent.Trim(); + if (string.IsNullOrEmpty(arg)) return m.Value; + + // ln(x) -> log(x) (natural log) + return $"(log({arg}))"; + } + private static string NormalizeNumber(string numberStr, bool isFunctionPresent, string decimalSep, string groupSep) + { + if (isFunctionPresent) { - if (dotCount > 1) + // STRICT MODE: When functions are present, ',' is ALWAYS an argument separator. + if (numberStr.Contains(',')) { - context.InputUsesGroupSeparators = true; - return numberStr.Replace(Dot, string.Empty); + return numberStr; } - // A number is ambiguous if it has a single Dot in the thousands position, - // and does not start with a "0." or "." - bool isAmbiguous = numberStr.Length - numberStr.LastIndexOf('.') == 4 - && !numberStr.StartsWith("0.") - && !numberStr.StartsWith("."); - if (isAmbiguous) + + string processedStr = numberStr; + + // Handle group separator, with special care for ambiguous dot. + if (!string.IsNullOrEmpty(groupSep)) { - if (systemGroupSep == Dot) + if (groupSep == ".") { - context.InputUsesGroupSeparators = true; - return numberStr.Replace(Dot, string.Empty); + var parts = processedStr.Split('.'); + if (parts.Length > 1) + { + var culture = CultureInfo.CurrentCulture; + if (IsValidGrouping(parts, culture.NumberFormat.NumberGroupSizes)) + { + processedStr = processedStr.Replace(groupSep, ""); + } + // If not grouped, it's likely a decimal number, so we don't strip dots. + } } else { - context.InputDecimalSeparator = Dot; - return numberStr; + processedStr = processedStr.Replace(groupSep, ""); } } - else // Unambiguous decimal (e.g., "12.34" or "0.123" or ".123") + + // Handle decimal separator. + if (decimalSep != ".") { - context.InputDecimalSeparator = Dot; - return numberStr; + processedStr = processedStr.Replace(decimalSep, "."); } + + return processedStr; } + else + { + // LENIENT MODE: No functions are present, so we can be flexible. + string processedStr = numberStr; + if (!string.IsNullOrEmpty(groupSep)) + { + processedStr = processedStr.Replace(groupSep, ""); + } + if (decimalSep != ".") + { + processedStr = processedStr.Replace(decimalSep, "."); + } + return processedStr; + } + } + + private static bool IsValidGrouping(string[] parts, int[] groupSizes) + { + if (parts.Length <= 1) return true; + + if (groupSizes is null || groupSizes.Length == 0 || groupSizes[0] == 0) + return false; // has groups, but culture defines none. - // Case 3: Only commas - if (commaCount > 0) + var firstPart = parts[0]; + if (firstPart.StartsWith('-')) firstPart = firstPart[1..]; + if (firstPart.Length == 0) return false; // e.g. ",123" + + if (firstPart.Length > groupSizes[0]) return false; + + var lastGroupSize = groupSizes.Last(); + var canRepeatLastGroup = lastGroupSize != 0; + + int groupIndex = 0; + for (int i = parts.Length - 1; i > 0; i--) { - if (commaCount > 1) + int expectedSize; + if (groupIndex < groupSizes.Length) { - context.InputUsesGroupSeparators = true; - return numberStr.Replace(Comma, string.Empty); + expectedSize = groupSizes[groupIndex]; } - // A number is ambiguous if it has a single Comma in the thousands position, - // and does not start with a "0," or "," - bool isAmbiguous = numberStr.Length - numberStr.LastIndexOf(',') == 4 - && !numberStr.StartsWith("0,") - && !numberStr.StartsWith(","); - if (isAmbiguous) + else if(canRepeatLastGroup) { - if (systemGroupSep == Comma) - { - context.InputUsesGroupSeparators = true; - return numberStr.Replace(Comma, string.Empty); - } - else - { - context.InputDecimalSeparator = Comma; - return numberStr.Replace(Comma, Dot); - } + expectedSize = lastGroupSize; } - else // Unambiguous decimal (e.g., "12,34" or "0,123" or ",123") + else { - context.InputDecimalSeparator = Comma; - return numberStr.Replace(Comma, Dot); + return false; } + + if (parts[i].Length != expectedSize) return false; + + groupIndex++; } - // Case 4: No separators - return numberStr; + return true; } - private string FormatResult(decimal roundedResult, ParsingContext context) + private string FormatResult(decimal roundedResult) { - string decimalSeparator = context.InputDecimalSeparator ?? GetDecimalSeparator(); + string decimalSeparator = GetDecimalSeparator(); string groupSeparator = GetGroupSeparator(decimalSeparator); string resultStr = roundedResult.ToString(CultureInfo.InvariantCulture); @@ -221,7 +363,7 @@ private string FormatResult(decimal roundedResult, ParsingContext context) string integerPart = parts[0]; string fractionalPart = parts.Length > 1 ? parts[1] : string.Empty; - if (context.InputUsesGroupSeparators && integerPart.Length > 3) + if (integerPart.Length > 3) { integerPart = ThousandGroupRegex.Replace(integerPart, groupSeparator); } @@ -236,29 +378,23 @@ private string FormatResult(decimal roundedResult, ParsingContext context) private string GetGroupSeparator(string decimalSeparator) { - // This logic is now independent of the system's group separator - // to ensure consistent output for unit testing. - return decimalSeparator == Dot ? Comma : Dot; - } - - private bool CanCalculate(Query query) - { - if (query.Search.Length < 2) - { - return false; - } + var culture = CultureInfo.CurrentCulture; + var systemGroupSeparator = culture.NumberFormat.NumberGroupSeparator; - if (!RegValidExpressChar.IsMatch(query.Search)) + if (_settings.DecimalSeparator == DecimalSeparator.UseSystemLocale) { - return false; + return systemGroupSeparator; } - if (!IsBracketComplete(query.Search)) + // When a custom decimal separator is used, + // use the system's group separator unless it conflicts with the custom decimal separator. + if (decimalSeparator == systemGroupSeparator) { - return false; + // Conflict: use the opposite of the decimal separator as a fallback. + return decimalSeparator == Dot ? Comma : Dot; } - return true; + return systemGroupSeparator; } private string GetDecimalSeparator() @@ -273,25 +409,6 @@ private string GetDecimalSeparator() }; } - private static bool IsBracketComplete(string query) - { - var matchs = RegBrackets.Matches(query); - var leftBracketCount = 0; - foreach (Match match in matchs) - { - if (match.Value == "(" || match.Value == "[") - { - leftBracketCount++; - } - else - { - leftBracketCount--; - } - } - - return leftBracketCount == 0; - } - public string GetTranslatedPluginTitle() { return Localize.flowlauncher_plugin_calculator_plugin_name(); diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs b/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs index f4e2090e740..a8b582ccce5 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs @@ -4,16 +4,21 @@ namespace Flow.Launcher.Plugin.Calculator; internal static partial class MainRegexHelper { - - [GeneratedRegex(@"[\(\)\[\]]", RegexOptions.Compiled)] - public static partial Regex GetRegBrackets(); - - [GeneratedRegex(@"^(ceil|floor|exp|pi|e|max|min|det|abs|log|ln|sqrt|sin|cos|tan|arcsin|arccos|arctan|eigval|eigvec|eig|sum|polar|plot|round|sort|real|zeta|bin2dec|hex2dec|oct2dec|factorial|sign|isprime|isinfty|==|~=|&&|\|\||(?:\<|\>)=?|[ei]|[0-9]|0x[\da-fA-F]+|[\+\%\-\*\/\^\., ""]|[\(\)\|\!\[\]])+$", RegexOptions.Compiled)] - public static partial Regex GetRegValidExpressChar(); - - [GeneratedRegex(@"[\d\.,]+", RegexOptions.Compiled)] + [GeneratedRegex(@"-?[\d\.,'\u00A0\u202F]+", RegexOptions.Compiled | RegexOptions.CultureInvariant)] public static partial Regex GetNumberRegex(); [GeneratedRegex(@"\B(?=(\d{3})+(?!\d))", RegexOptions.Compiled)] public static partial Regex GetThousandGroupRegex(); + + [GeneratedRegex(@"\bpow(\((?:[^()\[\]]|\((?)|\)(?<-Depth>)|\[(?)|\](?<-Depth>))*(?(Depth)(?!))\))", RegexOptions.Compiled | RegexOptions.RightToLeft | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + public static partial Regex GetPowRegex(); + + [GeneratedRegex(@"\blog(\((?:[^()\[\]]|\((?)|\)(?<-Depth>)|\[(?)|\](?<-Depth>))*(?(Depth)(?!))\))", RegexOptions.Compiled | RegexOptions.RightToLeft | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + public static partial Regex GetLogRegex(); + + [GeneratedRegex(@"\bln(\((?:[^()\[\]]|\((?)|\)(?<-Depth>)|\[(?)|\](?<-Depth>))*(?(Depth)(?!))\))", RegexOptions.Compiled | RegexOptions.RightToLeft | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + public static partial Regex GetLnRegex(); + + [GeneratedRegex(@"\b(sqrt|pow|factorial|abs|sign|ceil|floor|round|exp|log|log2|log10|min|max|lt|eq|gt|sin|cos|tan|arcsin|arccos|arctan|isnan|isint|isprime|isinfty|rand|randi|type|is|as|length|throw|catch|eval|map|clamp|lerp|regex|shuffle)\s*\(", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + public static partial Regex GetFunctionRegex(); } diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs index 8354863b852..cac0f308016 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs @@ -1,9 +1,11 @@  -namespace Flow.Launcher.Plugin.Calculator +namespace Flow.Launcher.Plugin.Calculator; + +public class Settings { - public class Settings - { - public DecimalSeparator DecimalSeparator { get; set; } = DecimalSeparator.UseSystemLocale; - public int MaxDecimalPlaces { get; set; } = 10; - } + public DecimalSeparator DecimalSeparator { get; set; } = DecimalSeparator.UseSystemLocale; + + public int MaxDecimalPlaces { get; set; } = 10; + + public bool ShowErrorMessage { get; set; } = false; } diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/ViewModels/SettingsViewModel.cs b/Plugins/Flow.Launcher.Plugin.Calculator/ViewModels/SettingsViewModel.cs index 87ae72fb681..79236bdf8e5 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/ViewModels/SettingsViewModel.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/ViewModels/SettingsViewModel.cs @@ -1,31 +1,25 @@ using System.Collections.Generic; using System.Linq; -namespace Flow.Launcher.Plugin.Calculator.ViewModels -{ - public class SettingsViewModel : BaseModel - { - public SettingsViewModel(Settings settings) - { - Settings = settings; - } +namespace Flow.Launcher.Plugin.Calculator.ViewModels; - public Settings Settings { get; init; } +public class SettingsViewModel(Settings settings) : BaseModel +{ + public Settings Settings { get; } = settings; - public static IEnumerable MaxDecimalPlacesRange => Enumerable.Range(1, 20); + public static IEnumerable MaxDecimalPlacesRange => Enumerable.Range(1, 20); - public List AllDecimalSeparator { get; } = DecimalSeparatorLocalized.GetValues(); + public List AllDecimalSeparator { get; } = DecimalSeparatorLocalized.GetValues(); - public DecimalSeparator SelectedDecimalSeparator + public DecimalSeparator SelectedDecimalSeparator + { + get => Settings.DecimalSeparator; + set { - get => Settings.DecimalSeparator; - set + if (Settings.DecimalSeparator != value) { - if (Settings.DecimalSeparator != value) - { - Settings.DecimalSeparator = value; - OnPropertyChanged(); - } + Settings.DecimalSeparator = value; + OnPropertyChanged(); } } } diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml index 8d240ef3971..9e7549b2df1 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml @@ -15,6 +15,7 @@ + @@ -58,5 +59,14 @@ ItemsSource="{Binding MaxDecimalPlacesRange}" SelectedItem="{Binding Settings.MaxDecimalPlaces}" /> + diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml.cs index 7bc307d111c..9e75e7bfb3b 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml.cs @@ -1,22 +1,16 @@ using System.Windows.Controls; using Flow.Launcher.Plugin.Calculator.ViewModels; -namespace Flow.Launcher.Plugin.Calculator.Views +namespace Flow.Launcher.Plugin.Calculator.Views; + +public partial class CalculatorSettings : UserControl { - /// - /// Interaction logic for CalculatorSettings.xaml - /// - public partial class CalculatorSettings : UserControl - { - private readonly SettingsViewModel _viewModel; - private readonly Settings _settings; + private readonly SettingsViewModel _viewModel; - public CalculatorSettings(Settings settings) - { - _viewModel = new SettingsViewModel(settings); - _settings = _viewModel.Settings; - DataContext = _viewModel; - InitializeComponent(); - } + public CalculatorSettings(Settings settings) + { + _viewModel = new SettingsViewModel(settings); + DataContext = _viewModel; + InitializeComponent(); } } diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json b/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json index c9435e04315..93df9ec72dd 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json @@ -2,7 +2,7 @@ "ID": "CEA0FDFC6D3B4085823D60DC76F28855", "ActionKeyword": "*", "Name": "Calculator", - "Description": "Perform mathematical calculations (including hexadecimal values). Use ',' or '.' as thousand separator or decimal place.", + "Description": "Perform mathematical calculations, including hex values and advanced functions such as 'min(1,2,3)', 'sqrt(123)' and 'cos(123)'.", "Author": "cxfksword, dcog989", "Version": "1.0.0", "Language": "csharp",