/// <summary> /// Scrolls all the visible terminal windows to the bottom. /// </summary> public void ScrollAllToBottom() { GameTerminal.ScrollToLastLine(); Terminal1.ScrollToLastLine(); Terminal2.ScrollToLastLine(); Terminal3.ScrollToLastLine(); }
/// <summary> /// Handles the event when the custom tab selection changes. /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void TabCustom_SelectionChanged(object sender, SelectionChangedEventArgs e) { // When the game tab gets the focus always put the focus into the input box. if (CustomTab1.IsSelected && TextInput.Editor != null) { Dispatcher.BeginInvoke(new Action(() => { // Reset the value to 0, it's now been shown. CustomTab1Badge.Value = 0; // In order to get the focus in this instance an UpdateLayout() call has to be called first. UpdateLayout(); Terminal1.ScrollToEnd(); // When the app is first loaded the Editor was coming up null so we'll just check the nulls // and then default the caret position to 0 if that's the case. TextInput.Editor.CaretIndex = TextInput?.Editor?.Text?.Length ?? 0; TextInput.Editor.Focus(); })); } else if (CustomTab2.IsSelected && TextInput.Editor != null) { Dispatcher.BeginInvoke(new Action(() => { // Reset the value to 0, it's now been shown. CustomTab2Badge.Value = 0; // In order to get the focus in this instance an UpdateLayout() call has to be called first. UpdateLayout(); Terminal2.ScrollToEnd(); // When the app is first loaded the Editor was coming up null so we'll just check the nulls // and then default the caret position to 0 if that's the case. TextInput.Editor.CaretIndex = TextInput?.Editor?.Text?.Length ?? 0; TextInput.Editor.Focus(); })); } else if (CustomTab3.IsSelected && TextInput.Editor != null) { Dispatcher.BeginInvoke(new Action(() => { // Reset the value to 0, it's now been shown. CustomTab3Badge.Value = 0; // In order to get the focus in this instance an UpdateLayout() call has to be called first. UpdateLayout(); Terminal3.ScrollToEnd(); // When the app is first loaded the Editor was coming up null so we'll just check the nulls // and then default the caret position to 0 if that's the case. TextInput.Editor.CaretIndex = TextInput?.Editor?.Text?.Length ?? 0; TextInput.Editor.Focus(); })); } }
/// <summary> /// Updates any properties that need to be updated after the settings are updated. This deals primarily /// with properties that aren't bound through dependency properties. /// </summary> public void UpdateUISettings() { // Terminal font FontFamily font; switch (App.Settings.AvalonSettings.TerminalFont) { case AvalonSettings.TerminalFonts.Consolas: this.ViewModel.TerminalFontFamily = new FontFamily("Consolas"); break; case AvalonSettings.TerminalFonts.CourierNew: this.ViewModel.TerminalFontFamily = new FontFamily("Courier New"); break; default: this.ViewModel.TerminalFontFamily = new FontFamily("Consolas"); break; } this.ViewModel.SpellCheckEnabled = App.Settings.ProfileSettings.SpellChecking; // Scroll everything to the last line in case heights/widths/wrapping has changed. GameTerminal.ScrollToLastLine(); Terminal1.ScrollToLastLine(); Terminal2.ScrollToLastLine(); Terminal3.ScrollToLastLine(); // This will allow the main window to go maximize and not cover the task bar on the main window // but will maximize over the task bar on 2nd monitors. // TODO - Make this work on all monitors this.MaxHeight = SystemParameters.MaximizedPrimaryScreenHeight; this.MaxWidth = SystemParameters.MaximizedPrimaryScreenWidth; // The number of seconds in between batch writes for the the database wrapper. SqlTasks.SetInterval(App.Settings.AvalonSettings.DatabaseWriteInterval); // Apply the border settings by trigger the SizeChanged event. this.MainPage_SizeChanged(null, null); // Grid Layout LoadGridState(); }
/// <summary> /// Checks a line to see if any Triggers should fire and if so executes those triggers. /// </summary> /// <param name="line"></param> public async void CheckTriggers(Line line) { // Don't process if the user has disabled triggers. if (!App.Settings.ProfileSettings.TriggersEnabled) { return; } // Don't process if the line is blank. if (string.IsNullOrWhiteSpace(line.Text)) { return; } // Replacement triggers come, they actually alter the line in the terminal. this.ProcessReplacementTriggers(line); // Go through the immutable system triggers, system triggers are silent in that // they won't echo to the terminal window, they also don't adhere to attributes like // character or enabled. These can and will have CLR implementations and can be loaded // from other DLL's as plugins. System triggers are also unique in that they are designed // to be loaded from a plugin and they don't save their state in the profile. foreach (var item in App.InstanceGlobals.SystemTriggers) { // Skip it if it's not enabled. if (!item.Enabled) { continue; } if (item.IsMatch(line.Text)) { // Run any CLR that might exist. item.Execute(); if (item.ExecuteAs == ExecuteType.Command && !string.IsNullOrEmpty(item.Command)) { // If it has text but it's not lua, send it to the interpreter. await Interp.Send(item.Command, item.IsSilent, false); } else if ((item.IsLua || item.ExecuteAs == ExecuteType.LuaMoonsharp) && !string.IsNullOrEmpty(item.Command)) { var paramList = new string[item.Match.Groups.Count]; paramList[0] = line.Text; for (int i = 1; i < item.Match.Groups.Count; i++) { paramList[i] = item.Match.Groups[i].Value; } string luaResult = Interp.ScriptHost.MoonSharp.ExecuteFunction <string>(item.FunctionName, paramList); } // Check if we're supposed to move this line somewhere else. if (item.MoveTo != TerminalTarget.None) { // Create a brand new line (not a shared reference) where this can be shown in the communication window. var commLine = new Line { FormattedText = $"[{Utilities.Utilities.Timestamp()}]: {line.FormattedText}\r\n" }; if (item.MoveTo == TerminalTarget.Terminal1) { Terminal1.Append(commLine); if (!App.MainWindow.CustomTab1.IsSelected) { App.MainWindow.CustomTab1Badge.Value++; } else if (App.MainWindow.CustomTab1.IsSelected && App.MainWindow.CustomTab1Badge.Value != 0) { // Only setting this if the value isn't 0 so it doesn't trigger UI processing. App.MainWindow.CustomTab1Badge.Value = 0; } } else if (item.MoveTo == TerminalTarget.Terminal2) { Terminal2.Append(commLine); if (!App.MainWindow.CustomTab2.IsSelected) { App.MainWindow.CustomTab2Badge.Value++; } else if (App.MainWindow.CustomTab2.IsSelected && App.MainWindow.CustomTab2Badge.Value != 0) { // Only setting this if the value isn't 0 so it doesn't trigger UI processing. App.MainWindow.CustomTab2Badge.Value = 0; } } else if (item.MoveTo == TerminalTarget.Terminal3) { Terminal3.Append(commLine); if (!App.MainWindow.CustomTab3.IsSelected) { App.MainWindow.CustomTab3Badge.Value++; } else if (App.MainWindow.CustomTab3.IsSelected && App.MainWindow.CustomTab3Badge.Value != 0) { // Only setting this if the value isn't 0 so it doesn't trigger UI processing. App.MainWindow.CustomTab3Badge.Value = 0; } } } // This breaks instead of returning, no more system triggers would be processed but user // ones might. if (item.StopProcessing) { // To help with debugging. if (App.Settings.AvalonSettings.Debug) { App.Conveyor.EchoLog("System trigger matched that stops the processing of the rest of the trigger list.", LogType.Debug); } break; } } } // Go through the TriggerList which are user defined triggers. foreach (var item in App.Settings.ProfileSettings.TriggerList.EnabledEnumerable()) { // Skip it if it's not global or for this character. if (!string.IsNullOrWhiteSpace(item.Character) && item.Character != App.Conveyor.GetVariable("Character")) { continue; } if (item.IsMatch(line.Text)) { // Run any CLR that might exist. item.Execute(); // Increment the counter. item.Count++; // Line Highlighting if the trigger is supposed to. Insert the ANSI color at the start of the line. if (item.HighlightLine) { // TODO - Allow the highlighted color to be set for each trigger. int start = GameTerminal.Document.Text.LastIndexOf(line.FormattedText, StringComparison.Ordinal); GameTerminal.Document.Insert(start, AnsiColors.DarkCyan); } // Only send if it has something in it. Use the processed command. if (item.ExecuteAs == ExecuteType.Command && !string.IsNullOrEmpty(item.ProcessedCommand)) { // If it has text but it's not lua, send it to the interpreter. await Interp.Send(item.ProcessedCommand, false, false); } else if ((item.IsLua || item.ExecuteAs == ExecuteType.LuaMoonsharp) && !string.IsNullOrWhiteSpace(item.Command)) { var paramList = new string[item.Match.Groups.Count]; paramList[0] = line.Text; for (int i = 1; i < item.Match.Groups.Count; i++) { paramList[i] = item.Match.Groups[i].Value; } // Not sure why the try/catch calling CheckTriggers wasn't catching Lua errors. Eat // the error here and allow the script host to process it's exception handler which will // echo it to the terminal. try { // We'll send the function we want to call but also the code, if the code has changed // it nothing will be reloaded thus saving memory and calls. This is why replacing %1 // variables is problematic here and why we are forcing the use of Lua varargs (...) _ = await Interp.ScriptHost.MoonSharp.ExecuteFunctionAsync <object>(item.FunctionName, paramList); } catch { } } // Check if we're supposed to move this line somewhere else. if (item.MoveTo != TerminalTarget.None) { // Create a brand new line (not a shared reference) where this can be shown in the communication window. var commLine = new Line { FormattedText = $"[{Utilities.Utilities.Timestamp()}]: {line.FormattedText}\r\n" }; if (item.MoveTo == TerminalTarget.Terminal1) { Terminal1.Append(commLine); if (!App.MainWindow.CustomTab1.IsSelected) { App.MainWindow.CustomTab1Badge.Value++; } else if (App.MainWindow.CustomTab1.IsSelected && App.MainWindow.CustomTab1Badge.Value != 0) { // Only setting this if the value isn't 0 so it doesn't trigger UI processing. App.MainWindow.CustomTab1Badge.Value = 0; } } else if (item.MoveTo == TerminalTarget.Terminal2) { Terminal2.Append(commLine); if (!App.MainWindow.CustomTab2.IsSelected) { App.MainWindow.CustomTab2Badge.Value++; } else if (App.MainWindow.CustomTab2.IsSelected && App.MainWindow.CustomTab2Badge.Value != 0) { // Only setting this if the value isn't 0 so it doesn't trigger UI processing. App.MainWindow.CustomTab2Badge.Value = 0; } } else if (item.MoveTo == TerminalTarget.Terminal3) { Terminal3.Append(commLine); if (!App.MainWindow.CustomTab3.IsSelected) { App.MainWindow.CustomTab3Badge.Value++; } else if (App.MainWindow.CustomTab3.IsSelected && App.MainWindow.CustomTab3Badge.Value != 0) { // Only setting this if the value isn't 0 so it doesn't trigger UI processing. App.MainWindow.CustomTab3Badge.Value = 0; } } } // So, if this trigger matches and i has StopProcessing set it will not process any trigger // thereafter. This lets a savvy user setup a very efficient trigger processing pipeline but // can potentially cause issues if they have something that stops processing but didn't intend // for it (since it would not fire any triggers after). All triggers are set to process by // default. if (item.StopProcessing) { // To help with debugging. if (App.Settings.AvalonSettings.Debug) { App.Conveyor.EchoLog("Regular trigger matched that stops the processing of the rest of the trigger list.", LogType.Debug); } return; } } } }
/// <summary> /// The PreviewKeyDown event for the input text box used to setup special behavior from that box. /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void Editor_PreviewKeyDown(object sender, KeyEventArgs e) { // So they key's for macros aren't entered into the text box. foreach (var item in App.Settings.ProfileSettings.MacroList) { if ((int)e.Key == item.Key) { e.Handled = true; return; } } switch (e.Key) { case Key.Enter: e.Handled = true; // When a command is entered into the input box. // Make sure the newline didn't make it into the text input, then select all in the box so it can be cleared quickly. TextInput.Editor.Text = TextInput.Editor.Text.RemoveLineEndings(); TextInput.Editor.SelectAll(); Interp.Send(TextInput.Editor.Text); // If the user wants the input box to clear after a command, make it so. if (App.Settings.AvalonSettings.InputBoxClearAfterCommand) { TextInput.Editor.Text = ""; } // Set the history count to the end Interp.InputHistoryPosition = -1; break; case Key.Up: e.Handled = false; // If the drop down is open allow the up and down keys to work for it, not history. if (TextInput.IsDropDownOpen) { return; } // Go to the previous item in the history. TextInput.Editor.Text = Interp.InputHistoryNext(); TextInput.Editor.SelectionStart = (TextInput.Editor.Text.Length); TextInput.Editor.SelectionLength = 0; break; case Key.Down: e.Handled = false; // If the drop down is open allow the up and down keys to work for it, not history. if (TextInput.IsDropDownOpen) { return; } // Go to the next item in the history. TextInput.Editor.Text = Interp.InputHistoryPrevious(); TextInput.Editor.SelectionStart = (TextInput.Editor.Text.Length); TextInput.Editor.SelectionLength = 0; break; case Key.PageUp: e.Handled = true; // Back buffer is collapsed, show it, scroll to the bottom of it. if (GameBackBufferTerminal.Visibility == System.Windows.Visibility.Collapsed) { GameBackBufferTerminal.Visibility = System.Windows.Visibility.Visible; // Scroll via the vertical offset, if not done for some reason the first time the window is shown // it will be at the top. GameBackBufferTerminal.ScrollToLastLine(true); // Since the height of this changed scroll it to the bottom. GameTerminal.ScrollToLastLine(); break; } // If it was already visible, then we PageUp() GameBackBufferTerminal.PageUp(); break; case Key.PageDown: e.Handled = true; // Back buffer is visible then execute a PageDown() if (GameBackBufferTerminal.Visibility == System.Windows.Visibility.Visible) { GameBackBufferTerminal.PageDown(); } // Now, if the last line in the back buffer is visible then we can just collapse the // back buffer because the main terminal shows everything at the end. if (GameBackBufferTerminal.IsLastLineVisible()) { GameBackBufferTerminal.Visibility = System.Windows.Visibility.Collapsed; TextInput.Editor.Focus(); } break; case Key.Oem5: case Key.OemBackslash: TextInput.Editor.SelectAll(); e.Handled = true; break; case Key.Escape: // Collapse the back buffer so it hides it and reclaims the space for the main terminal. GameBackBufferTerminal.Visibility = System.Windows.Visibility.Collapsed; // Setting to see if the comm windows should scroll to the bottom on escape. if (App.Settings.AvalonSettings.EscapeScrollsAllTerminalsToBottom) { Terminal1.ScrollToLastLine(); Terminal2.ScrollToLastLine(); Terminal3.ScrollToLastLine(); GameTerminal.ScrollToLastLine(); } // Hide the auto completion box if it's open. if (PopupAutoComplete.IsOpen) { PopupAutoComplete.IsOpen = false; } // Reset the input history to the default position and clear the text in the input box. Interp.InputHistoryPosition = -1; TextInput.Editor.Text = ""; break; case Key.Tab: // Auto Complete from command history and/or aliases. e.Handled = true; string command = null; bool ctrl = ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control); bool shift = ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift); // If control is down search aliases, if it's not search the history if (ctrl && !string.IsNullOrWhiteSpace(TextInput.Editor.Text)) { // Aliases var alias = App.Settings.ProfileSettings.AliasList.FirstOrDefault(x => x.AliasExpression.StartsWith(TextInput.Editor.Text, System.StringComparison.OrdinalIgnoreCase)); if (alias != null) { command = alias.AliasExpression; } // If the alias isn't null, put it in the text editor and leave, we're done. If it is, we'll go ahead and // continue on to the history as a fall back. if (!string.IsNullOrWhiteSpace(command)) { TextInput.Editor.Text = command; TextInput.Editor.SelectionStart = TextInput.Editor.Text.Length; return; } } // If shift is down search past input data, if it's not search the history if (shift && App.Settings.AvalonSettings.AutoCompleteWord && !string.IsNullOrWhiteSpace(TextInput.Editor.Text)) { try { TextInput.Editor.SelectCurrentWord(); string word = TextInput.Editor.SelectedText; App.MainWindow.PopupAutoComplete.IsOpen = false; App.MainWindow.PopupAutoComplete.PlacementTarget = TextInput.Editor; App.MainWindow.PopupAutoComplete.Height = 200; App.MainWindow.PopupAutoComplete.Width = 200; App.MainWindow.PopupAutoComplete.HorizontalOffset = App.MainWindow.PopupAutoComplete.Width; App.MainWindow.PopupAutoComplete.VerticalOffset = -200; App.MainWindow.PopupAutoComplete.IsOpen = true; // Get a list of words that match, then reverse it so the newest entries come first. var list = Interp.InputAutoCompleteKeywords.Where(x => x.StartsWith(word, StringComparison.OrdinalIgnoreCase)).Reverse(); PopupAutoCompleteListBox.ItemsSource = list; // Select the first item in the list, then focus the list. PopupAutoCompleteListBox.Focus(); if (PopupAutoCompleteListBox.Items.Count > 0) { PopupAutoCompleteListBox.SelectedIndex = 0; } } catch (Exception ex) { App.Conveyor.EchoLog(ex.Message, LogType.Error); } return; } // If there is no input in the text editor, get the last entered command otherwise search the input // history for the command (searching the latest entries backwards). if (string.IsNullOrWhiteSpace(TextInput.Editor.Text)) { command = Interp.InputHistory.Last(); } else { command = Interp.InputHistory.FindLast(x => x.StartsWith(TextInput.Editor.Text, System.StringComparison.OrdinalIgnoreCase)); } if (!string.IsNullOrWhiteSpace(command)) { TextInput.Editor.Text = command; TextInput.Editor.SelectionStart = TextInput.Editor.Text.Length; } break; } }
/// <summary> /// Shared preview key down logic between the game terminal and it's back buffer. If they have focus this /// will implement page up and page down so that the back buffer will show all of the paging, once it gets /// to the bottom it will disappear. The escape key will send the focus back to the input box hiding the /// back buffer. /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void GameTerminal_PreviewKeyDown(object sender, KeyEventArgs e) { switch (e.Key) { case Key.PageUp: e.Handled = true; // Back buffer is collapsed, show it, scroll to the bottom of it. if (GameBackBufferTerminal.Visibility == System.Windows.Visibility.Collapsed) { GameBackBufferTerminal.Visibility = System.Windows.Visibility.Visible; // Scroll via the vertical offset, if not done for some reason the first time the window is shown // it will be at the top. GameBackBufferTerminal.ScrollToLastLine(true); // Since the height of this changed scroll it to the bottom. GameTerminal.ScrollToLastLine(); break; } // If it was already visible, then we PageUp() GameBackBufferTerminal.PageUp(); break; case Key.PageDown: e.Handled = true; // Back buffer is visible then execute a PageDown() if (GameBackBufferTerminal.Visibility == System.Windows.Visibility.Visible) { GameBackBufferTerminal.PageDown(); } // Now, if the last line in the back buffer is visible then we can just collapse the // back buffer because the main terminal shows everything at the end. Set the focus // back to the input box if it's not already there. if (GameBackBufferTerminal.IsLastLineVisible()) { GameBackBufferTerminal.Visibility = System.Windows.Visibility.Collapsed; TextInput.Editor.Focus(); } break; case Key.Escape: // Collapse the back buffer so it hides it and reclaims the space for the main terminal. GameBackBufferTerminal.Visibility = System.Windows.Visibility.Collapsed; // Setting to see if the comm windows should scroll to the bottom on escape. if (App.Settings.AvalonSettings.EscapeScrollsAllTerminalsToBottom) { GameTerminal.ScrollToLastLine(); Terminal1.ScrollToLastLine(); Terminal2.ScrollToLastLine(); Terminal3.ScrollToLastLine(); } // Reset the input history to the default position and clear the text in the input box. Interp.InputHistoryPosition = -1; TextInput.Editor.Text = ""; TextInput.Editor.Focus(); break; } }