// // Application.cs // // Author: // Michael Becker // // Copyright (c) 2020 Mike Becker's Software // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WAR+RANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . using System; using System.Collections.Generic; using System.Text; using MBS.Framework.Collections.Generic; namespace MBS.Framework { public class Application { /// /// Implements a which notifies /// the containing of context addition and /// removal. /// public class ApplicationContextCollection : Context.ContextCollection { protected override void ClearItems() { for (int i = 0; i < this.Count; i++) { Instance.RemoveContext(this[i]); } base.ClearItems(); } protected override void InsertItem(int index, Context item) { base.InsertItem(index, item); Instance.AddContext(item); } protected override void RemoveItem(int index) { Instance.RemoveContext(this[index]); base.RemoveItem(index); } } /// /// Gets or sets the currently-running /// instance. /// /// /// The currently-running instance. /// public static Application Instance { get; set; } = null; /// /// Searches for the file with the given and /// returns all matching fully-qualified file names. /// /// The files. /// Filename. /// Options. public string[] FindFiles(string filename, FindFileOptions options = FindFileOptions.All) { if (filename.StartsWith("~/")) { filename = filename.Substring(2); } List files = new List(); string[] paths = EnumerateDataPaths(options); foreach (string path in paths) { string file = System.IO.Path.Combine(new string[] { path, filename }); if (System.IO.File.Exists(file)) { files.Add(file); } } return files.ToArray(); } /// /// Searches for the file with the given and /// returns the first matching fully-qualified file name. /// /// The files. /// Filename. /// Options. public string FindFile(string filename, FindFileOptions options = FindFileOptions.All) { string[] files = FindFiles(filename, options); if (files.Length > 0) { return files[0]; } if ((options & FindFileOptions.Create) == FindFileOptions.Create) { string[] paths = EnumerateDataPaths(options); return System.IO.Path.Combine(paths[0], filename); } return null; } /// /// Gets a collection of s registered for this /// . /// /// The event filters. public EventFilter.EventFilterCollection EventFilters { get; } = new EventFilter.EventFilterCollection(); /// /// Finds the command with the given . /// protected virtual Command FindCommandInternal(string commandID) { return null; } /// /// Finds the command with the given , /// searching across application-global commands as well as commands /// defined in the currently-loaded s. /// /// /// The command with the given , or /// if the command was not found. /// /// Command identifier. public Command FindCommand(string commandID) { Command cmd = Commands[commandID]; if (cmd != null) return cmd; cmd = FindCommandInternal(commandID); if (cmd != null) return cmd; foreach (Context ctx in Contexts) { cmd = ctx.Commands[commandID]; if (cmd != null) return cmd; } return null; } /// /// Finds the with the given /// . /// protected virtual Context FindContextInternal(Guid contextID) { return null; } /// /// Finds the with the given /// . /// public Context FindContext(Guid contextID) { Context ctx = FindContextInternal(contextID); if (ctx != null) return ctx; ctx = Contexts[contextID]; if (ctx != null) return ctx; return null; } public Guid ID { get; set; } = Guid.Empty; protected virtual void EnableDisableCommandInternal(Command command, bool enable) { } internal void _EnableDisableCommand(Command command, bool enable) { EnableDisableCommandInternal(command, enable); } private string _UniqueName = null; public string UniqueName { get { if (_UniqueName == null) { return ShortName; } return _UniqueName; } set { _UniqueName = value; } } private string mvarBasePath = null; public string BasePath { get { if (mvarBasePath == null) { // Set up the base path for the current application. Should this be able to be // overridden with a switch (/basepath:...) ? mvarBasePath = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); } return mvarBasePath; } } protected virtual string DataDirectoryName => ShortName; public string[] EnumerateDataPaths() => EnumerateDataPaths(FindFileOptions.All); public string[] EnumerateDataPaths(FindFileOptions options) { List list = new List(); if ((options & FindFileOptions.All) == FindFileOptions.All) { // first look in the application root directory since this will override everything else list.Add(BasePath); if (Environment.OSVersion.Platform == PlatformID.Unix) { // if we are on Unix or Mac OS X, look in /etc/... list.Add(String.Join(System.IO.Path.DirectorySeparatorChar.ToString(), new string[] { String.Empty, // *nix root directory "etc", DataDirectoryName })); } // then look in /usr/share/universal-editor or C:\ProgramData\Mike Becker's Software\Universal Editor list.Add(String.Join(System.IO.Path.DirectorySeparatorChar.ToString(), new string[] { System.Environment.GetFolderPath(System.Environment.SpecialFolder.CommonApplicationData), DataDirectoryName })); // then look in ~/.local/share/universal-editor or C:\Users\USERNAME\AppData\Local\Mike Becker's Software\Universal Editor list.Add(String.Join(System.IO.Path.DirectorySeparatorChar.ToString(), new string[] { System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData), DataDirectoryName })); } //fixme: addd finddfileoption.userconfig, localdata, etc. // now for the user-writable locations... // then look in ~/.universal-editor or C:\Users\USERNAME\AppData\Roaming\Mike Becker's Software\Universal Editor list.Add(String.Join(System.IO.Path.DirectorySeparatorChar.ToString(), new string[] { System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData), DataDirectoryName })); return list.ToArray(); } public string ShortName { get; set; } public string Title { get; set; } = String.Empty; public int ExitCode { get; protected set; } = 0; protected virtual InstallationStatus GetInstallationStatusInternal() { return InstallationStatus.Unknown; } public InstallationStatus InstallationStatus { get { return GetInstallationStatusInternal(); } } public CommandLine CommandLine { get; } = new CommandLine(); private Dictionary> _CommandEventHandlers = new Dictionary>(); public Command.CommandCollection Commands { get; } = new Command.CommandCollection(); /// /// Attachs the specified to handle the with the given . /// /// true, if the command was found, false otherwise. /// Command identifier. /// Handler. public bool AttachCommandEventHandler(string commandID, EventHandler handler) { Command cmd = Commands[commandID]; if (cmd != null) { cmd.Executed += handler; return true; } Console.WriteLine("attempted to attach handler for unknown command '" + commandID + "'"); // handle command event handlers attached without a Command instance if (!_CommandEventHandlers.ContainsKey(commandID)) { _CommandEventHandlers.Add(commandID, new List()); } if (!_CommandEventHandlers[commandID].Contains(handler)) { _CommandEventHandlers[commandID].Add(handler); } return false; } /// /// Executes the command with the given . /// /// Identifier. /// Named parameters. public void ExecuteCommand(string id, KeyValuePair[] namedParameters = null) { Command cmd = Commands[id]; // handle command event handlers attached without a Command instance if (_CommandEventHandlers.ContainsKey(id)) { List c = _CommandEventHandlers[id]; for (int i = 0; i < c.Count; i++) { c[i](this, new CommandEventArgs(cmd, namedParameters)); } return; } // handle command event handlers attached in a context, most recently added first for (int i = Contexts.Count - 1; i >= 0; i--) { if (Contexts[i].ExecuteCommand(id)) return; } if (cmd == null) return; cmd.Execute(); } protected virtual void InitializeInternal() { } public bool Initialized { get; private set; } = false; [System.Diagnostics.DebuggerNonUserCode()] public void Initialize() { if (Initialized) return; if (ShortName == null) throw new ArgumentException("must specify a ShortName for the application"); Console.CancelKeyPress += Console_CancelKeyPress; InitializeInternal(); Initialized = true; } protected virtual void OnCancelKeyPress(ConsoleCancelEventArgs e) { } private void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e) { OnCancelKeyPress(e); } public event ApplicationActivatedEventHandler BeforeActivated; protected virtual void OnBeforeActivated(ApplicationActivatedEventArgs e) { BeforeActivated?.Invoke(this, e); } public event ApplicationActivatedEventHandler Activated; protected virtual void OnActivated(ApplicationActivatedEventArgs e) { Activated?.Invoke(this, e); } public event ApplicationActivatedEventHandler AfterActivated; protected virtual void OnAfterActivated(ApplicationActivatedEventArgs e) { AfterActivated?.Invoke(this, e); } protected virtual int StartInternal() { CommandLine cline = new CommandLine(); string[] args = CommandLine.Arguments; if (args.Length > 0) { int i = 0; for (i = 0; i < args.Length; i++) { if (ParseOption(args, ref i, cline.Options, CommandLine.Options)) break; } // we have finished parsing the first set of options ("global" options) // now we see if we have commands if (CommandLine.Commands.Count > 0 && i < args.Length) { // we support commands like git and apt, "appname --app-global-options --command-options" CommandLineCommand cmd = CommandLine.Commands[args[i]]; if (cmd != null) { for (i++; i < args.Length; i++) { if (ParseOption(args, ref i, cline.Options, cmd.Options)) break; } cline.Command = cmd; } else { // assume filename } } for (/* intentionally left blank */; i < args.Length; i++) { cline.FileNames.Add(args[i]); } } ApplicationActivatedEventArgs e = new ApplicationActivatedEventArgs(true, ApplicationActivationType.CommandLineLaunch, cline); if (CommandLine.Options["help"]?.Value is bool && ((bool)CommandLine.Options["help"]?.Value) == true) { if (ShowCommandLineHelp(out int resultCode)) { return resultCode; } } if (cline.Command != null && cline.Command.ActivationDelegate != null) { OnBeforeActivated(e); if (!e.Success) { Console.WriteLine(String.Format("Try '{0} --help' for more information.", ShortName)); return e.ExitCode; } // use the activation delegate instead of calling OnActivated cline.Command.ActivationDelegate(e); if (!e.Success) { Console.WriteLine(String.Format("Try '{0} --help' for more information.", ShortName)); return e.ExitCode; } OnAfterActivated(e); if (!e.Success) { Console.WriteLine(String.Format("Try '{0} --help' for more information.", ShortName)); return e.ExitCode; } } else { OnBeforeActivated(e); if (!e.Success) { Console.WriteLine(String.Format("Try '{0} --help' for more information.", ShortName)); return e.ExitCode; } OnActivated(e); if (!e.Success) { Console.WriteLine(String.Format("Try '{0} --help' for more information.", ShortName)); return e.ExitCode; } OnAfterActivated(e); if (!e.Success) { Console.WriteLine(String.Format("Try '{0} --help' for more information.", ShortName)); return e.ExitCode; } } return e.ExitCode; } protected void PrintUsageStatement(CommandLineCommand command = null) { string shortOptionPrefix = CommandLine.ShortOptionPrefix ?? "-"; string longOptionPrefix = CommandLine.LongOptionPrefix ?? "--"; Console.Write("usage: {0} ", ShortName); foreach (CommandLineOption option in CommandLine.Options) { PrintUsageStatementOption(option); } if (CommandLine.HelpTextPrefix != null) { Console.WriteLine(); Console.WriteLine(); Console.WriteLine(CommandLine.HelpTextPrefix); } Console.WriteLine(); bool printDescriptions = true; if (printDescriptions) { foreach (CommandLineOption option in CommandLine.Options) { Console.WriteLine(" {0}{1}", option.Abbreviation != '\0' ? String.Format("{0}{1}, {2}{3}", shortOptionPrefix, option.Abbreviation, longOptionPrefix, option.Name) : String.Format("{0}{1}", longOptionPrefix, option.Name), option.Description == null ? null : String.Format(" - {0}", option.Description)); } } // Console.Write("[]"); if (command != null) { Console.WriteLine(" {0}{1}", command.Name, command.Description == null ? null : String.Format(" - {0}", command.Description)); foreach (CommandLineOption option in command.Options) { PrintUsageStatementOption(option); } } else { if (CommandLine.Commands.Count > 0) { Console.WriteLine(" []"); Console.WriteLine(); List commands = new List(CommandLine.Commands); commands.Sort((x, y) => { return x.Name.CompareTo(y.Name); }); foreach (CommandLineCommand command1 in commands) { Console.WriteLine(" {0}{1}", command1.Name, command1.Description == null ? null : String.Format(" - {0}", command1.Description)); } } } if (CommandLine.HelpTextSuffix != null) { Console.WriteLine(); Console.WriteLine(CommandLine.HelpTextSuffix); } } private void PrintUsageStatementOption(CommandLineOption option) { Console.Write(" "); if (option.Optional) { Console.Write('['); } string shortOptionPrefix = CommandLine.ShortOptionPrefix ?? "-"; string longOptionPrefix = CommandLine.LongOptionPrefix ?? "--"; if (option.Abbreviation != '\0') { Console.Write("{0}{1}", shortOptionPrefix, option.Abbreviation); if (option.Type == CommandLineOptionValueType.Single) { Console.Write(" "); } else if (option.Type == CommandLineOptionValueType.Multiple) { Console.Write(" [,,...]"); } Console.Write(" | {0}{1}", longOptionPrefix, option.Name); if (option.Type == CommandLineOptionValueType.Single) { Console.Write("="); } else if (option.Type == CommandLineOptionValueType.Multiple) { Console.Write("=[,,...]"); } } else { Console.Write(longOptionPrefix ?? "--"); Console.Write("{0}", option.Name); if (option.Type == CommandLineOptionValueType.Single) { Console.Write("="); } else if (option.Type == CommandLineOptionValueType.Multiple) { Console.Write("=[,,...]"); } } if (option.Optional) { Console.Write(']'); } // Console.WriteLine(); } /// /// Parses the option. /// /// true, if option was parsed, false otherwise. /// Arguments. /// Index. /// The list into which to add the option if it has been specified. /// The set of available options. private bool ParseOption(string[] args, ref int index, IList list, CommandLineOption.CommandLineOptionCollection optionSet) { string longOptionPrefix = "--", shortOptionPrefix = "-"; bool breakout = false; if (args[index].StartsWith(shortOptionPrefix) && args[index].Length == (shortOptionPrefix.Length + 1)) { char shortOptionChar = args[index][args[index].Length - 1]; CommandLineOption option = optionSet[shortOptionChar]; if (option != null) { if (option.Abbreviation == shortOptionChar) { if (option.Type != CommandLineOptionValueType.None) { index++; option.Value = args[index]; } else { option.Value = true; } if (list != null) list.Add(option); } } else { list.Add(new CommandLineOption() { Abbreviation = shortOptionChar }); } } else if (args[index].StartsWith(longOptionPrefix)) { // long option format is --name[=value] string name = args[index].Substring(longOptionPrefix.Length); string value = null; if (name.Contains("=")) { int idx = name.IndexOf('='); value = name.Substring(idx + 1); name = name.Substring(0, idx); } CommandLineOption option = optionSet[name]; if (option != null) { if (option.Type != CommandLineOptionValueType.None) { // index++; // option.Value = args[index]; if (!name.Contains("=")) { // already taken care of the true case above if (index + 1 < args.Length) { index++; value = args[index]; } } option.Value = value; } else { option.Value = true; } list.Add(option); } else { list.Add(new CommandLineOption() { Name = name }); } } else { // we have reached a non-option return true; } return false; } public int Start() { if (Application.Instance == null) Application.Instance = this; Initialize(); int exitCode = StartInternal(); return exitCode; } // CONTEXTS /// /// Gets a collection of objects representing /// system, application, user, and custom contexts for settings and other /// items. /// /// /// A collection of objects representing system, /// application, user, and custom contexts for settings and other items. /// public ApplicationContextCollection Contexts { get; } = new ApplicationContextCollection(); /// /// The event raised when a is added to the /// . /// public ContextChangedEventHandler ContextAdded; /// /// Called when a is added to the /// . The default behavior is to simply fire /// the event. Implementations should handle /// adding context-specific behaviors and UI elements in this method. /// /// Event arguments. protected virtual void OnContextAdded(ContextChangedEventArgs e) { ContextAdded?.Invoke(this, e); } /// /// The event raised when a is removed from the /// . /// public ContextChangedEventHandler ContextRemoved; /// /// Called when a is removed from the /// . The default behavior is to simply fire /// the event. Implementations should handle /// removing context-specific behaviors and UI elements in this method. /// /// Event arguments. protected virtual void OnContextRemoved(ContextChangedEventArgs e) { ContextRemoved?.Invoke(this, e); } /// /// Handles updating the menus, toolbars, keyboard shortcuts, and other /// UI elements associated with the application . /// private void AddContext(Context ctx) { OnContextAdded(new ContextChangedEventArgs(ctx)); } /// /// Handles updating the menus, toolbars, keyboard shortcuts, and other /// UI elements associated with the application . /// private void RemoveContext(Context ctx) { OnContextRemoved(new ContextChangedEventArgs(ctx)); } /// /// Log the specified message. /// /// Message. public void Log(string message) { Log(null, 0, message); } /// /// Log the specified message, including information about the relevant /// object instance or static class , line number of /// the corresponding source code. /// /// Object. /// Line number. /// Message. public void Log(object obj, int lineNumber, string message) { Type type = (obj is Type ? ((Type)obj) : (obj != null ? obj.GetType() : null)); StringBuilder sb = new StringBuilder(); if (type != null) { sb.Append('['); sb.Append(type.Assembly.GetName().Name); sb.Append("] "); sb.Append(type.FullName); sb.Append('('); sb.Append(lineNumber); sb.Append(')'); sb.Append(": "); } sb.Append(message); System.Diagnostics.Debug.WriteLine(sb); } private Language mvarDefaultLanguage = null; /// /// The default used to display translatable text in this application. /// public Language DefaultLanguage { get { return mvarDefaultLanguage; } set { mvarDefaultLanguage = value; } } private Language.LanguageCollection mvarLanguages = new Language.LanguageCollection(); /// /// The languages defined for this application. Translations can be added through XML files in the ~/Languages folder. /// public Language.LanguageCollection Languages { get { return mvarLanguages; } } /// /// Gets a value indicating whether this is /// currently in the process of shutting down. /// /// true if stopping; otherwise, false. public bool Stopping { get; private set; } = false; protected virtual void OnStopping(System.ComponentModel.CancelEventArgs e) { } protected virtual void OnStopped(EventArgs e) { } /// /// The event raised when the method is called, before /// the application is stopped. /// public event System.ComponentModel.CancelEventHandler BeforeShutdown; /// /// Event handler for event. Called when the /// method is called, before the application is stopped. /// /// Event arguments. protected virtual void OnBeforeShutdown(System.ComponentModel.CancelEventArgs e) { BeforeShutdown?.Invoke(this, e); } public event EventHandler Shutdown; private void OnShutdown(EventArgs e) { Shutdown?.Invoke(this, e); } public void Restart() { Stop(); Start(); } /// /// Informs the underlying system backend that it is to begin the process /// of application shutdown, gracefully ending the main loop before /// returning the specified to the operating /// system. /// /// /// The exit code to return to the operating system. /// protected virtual void StopInternal(int exitCode) { } /// /// Shuts down the application gracefully, calling any event handlers /// attached to the shutdown event to give listeners the opportunity to /// cancel the shutdown and passing the specified /// to the operating system. /// /// /// The exit code to return to the operating system. /// public void Stop(int exitCode = 0) { if (Stopping) return; Stopping = true; System.ComponentModel.CancelEventArgs ce = new System.ComponentModel.CancelEventArgs(); OnBeforeShutdown(ce); if (ce.Cancel) { Stopping = false; return; } ce = new System.ComponentModel.CancelEventArgs(); // OnStopping called after setting Stopping to True, otherwise there is no real difference OnStopping(ce); if (ce.Cancel) { Stopping = false; return; } StopInternal(exitCode); OnShutdown(EventArgs.Empty); OnStopped(EventArgs.Empty); Stopping = false; } private Dictionary _settings = new Dictionary(); public T GetSetting(Guid id, T defaultValue = default(T)) { object value = GetSetting(id, (object)defaultValue); if (value is T) { return (T)value; } else if (value is string && ((string)value).TryParse(typeof(T), out object val)) { return (T)val; } return defaultValue; } public object GetSetting(Guid id, object defaultValue = null) { if (_settings.ContainsKey(id)) return _settings[id]; return defaultValue; } public void SetSetting(Guid id, T value) { _settings[id] = value; } protected virtual bool ShowCommandLineHelp(out int resultCode) { // bash: cd returns 2 if --help is specified OR invalid option selected, 1 if file not found PrintUsageStatement(); resultCode = 2; return true; } protected virtual Plugin[] GetAdditionalPluginsInternal() { return new Plugin[0]; } public Plugin[] GetAdditionalPlugins() { return GetAdditionalPluginsInternal(); } } }