mirror of
https://git.do.srb2.org/STJr/UltimateZoneBuilder.git
synced 2024-11-26 05:41:45 +00:00
Added command palette that allows the user to search for (and execute) commands at any time. Unbound by default. Resolves #555
This commit is contained in:
parent
91d79e12be
commit
9c7526a80c
8 changed files with 773 additions and 9 deletions
|
@ -1,5 +1,5 @@
|
|||
URL http://devbuilds.drdteam.org/ultimatedoombuilder/
|
||||
FileName Builder.exe
|
||||
UpdateName UltimateDoomBuilder-r[REVNUM]-x64.7z
|
||||
InstallerName UltimateDoomBuilder-Setup-R[REVNUM]-x64.exe
|
||||
UpdaterName UDB_Updater-x64.7z
|
||||
UpdateName UltimateDoomBuilder-r[REVNUM]-x86.7z
|
||||
InstallerName UltimateDoomBuilder-Setup-R[REVNUM]-x86.exe
|
||||
UpdaterName UDB_Updater-x86.7z
|
|
@ -180,6 +180,12 @@
|
|||
<Compile Include="Controls\ArgumentBox.Designer.cs">
|
||||
<DependentUpon>ArgumentBox.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Controls\CommandPaletteControl.cs">
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Controls\CommandPaletteControl.Designer.cs">
|
||||
<DependentUpon>CommandPaletteControl.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Controls\ExternalCommandControl.cs">
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
|
@ -669,6 +675,9 @@
|
|||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Controls\CommandPaletteControl.resx">
|
||||
<DependentUpon>CommandPaletteControl.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Controls\ExternalCommandControl.resx">
|
||||
<DependentUpon>ExternalCommandControl.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
|
|
|
@ -177,6 +177,12 @@
|
|||
<Compile Include="Controls\ArgumentBox.Designer.cs">
|
||||
<DependentUpon>ArgumentBox.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Controls\CommandPaletteControl.cs">
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Controls\CommandPaletteControl.Designer.cs">
|
||||
<DependentUpon>CommandPaletteControl.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Controls\ExternalCommandControl.cs">
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
|
@ -661,6 +667,9 @@
|
|||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Controls\CommandPaletteControl.resx">
|
||||
<DependentUpon>CommandPaletteControl.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Controls\ExternalCommandControl.resx">
|
||||
<DependentUpon>ExternalCommandControl.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
|
|
136
Source/Core/Controls/CommandPaletteControl.Designer.cs
generated
Normal file
136
Source/Core/Controls/CommandPaletteControl.Designer.cs
generated
Normal file
|
@ -0,0 +1,136 @@
|
|||
|
||||
namespace CodeImp.DoomBuilder.Controls
|
||||
{
|
||||
partial class CommandPaletteControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Component Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
System.Windows.Forms.ListViewGroup listViewGroup1 = new System.Windows.Forms.ListViewGroup("Recent", System.Windows.Forms.HorizontalAlignment.Left);
|
||||
System.Windows.Forms.ListViewGroup listViewGroup2 = new System.Windows.Forms.ListViewGroup("Usable actions", System.Windows.Forms.HorizontalAlignment.Left);
|
||||
System.Windows.Forms.ListViewGroup listViewGroup3 = new System.Windows.Forms.ListViewGroup("Not usable in this context", System.Windows.Forms.HorizontalAlignment.Left);
|
||||
this.commandsearch = new System.Windows.Forms.TextBox();
|
||||
this.noresults = new System.Windows.Forms.Label();
|
||||
this.commandlist = new CodeImp.DoomBuilder.Controls.OptimizedListView();
|
||||
this.columnHeader1 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
|
||||
this.columnHeader3 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
|
||||
this.columnHeader2 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// commandsearch
|
||||
//
|
||||
this.commandsearch.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.commandsearch.Location = new System.Drawing.Point(3, 2);
|
||||
this.commandsearch.Name = "commandsearch";
|
||||
this.commandsearch.Size = new System.Drawing.Size(864, 20);
|
||||
this.commandsearch.TabIndex = 2;
|
||||
this.commandsearch.TextChanged += new System.EventHandler(this.commandsearch_TextChanged);
|
||||
this.commandsearch.KeyDown += new System.Windows.Forms.KeyEventHandler(this.commandsearch_KeyDown);
|
||||
//
|
||||
// noresults
|
||||
//
|
||||
this.noresults.AutoSize = true;
|
||||
this.noresults.Location = new System.Drawing.Point(6, 28);
|
||||
this.noresults.Name = "noresults";
|
||||
this.noresults.Size = new System.Drawing.Size(84, 13);
|
||||
this.noresults.TabIndex = 4;
|
||||
this.noresults.Text = "No results found";
|
||||
this.noresults.Visible = false;
|
||||
//
|
||||
// commandlist
|
||||
//
|
||||
this.commandlist.Activation = System.Windows.Forms.ItemActivation.OneClick;
|
||||
this.commandlist.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
| System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.commandlist.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
|
||||
this.columnHeader1,
|
||||
this.columnHeader3,
|
||||
this.columnHeader2});
|
||||
this.commandlist.FullRowSelect = true;
|
||||
listViewGroup1.Header = "Recent";
|
||||
listViewGroup1.Name = "recent";
|
||||
listViewGroup2.Header = "Usable actions";
|
||||
listViewGroup2.Name = "usableactions";
|
||||
listViewGroup3.Header = "Not usable in this context";
|
||||
listViewGroup3.Name = "notusableactions";
|
||||
this.commandlist.Groups.AddRange(new System.Windows.Forms.ListViewGroup[] {
|
||||
listViewGroup1,
|
||||
listViewGroup2,
|
||||
listViewGroup3});
|
||||
this.commandlist.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.None;
|
||||
this.commandlist.Location = new System.Drawing.Point(3, 25);
|
||||
this.commandlist.MultiSelect = false;
|
||||
this.commandlist.Name = "commandlist";
|
||||
this.commandlist.Size = new System.Drawing.Size(864, 173);
|
||||
this.commandlist.TabIndex = 3;
|
||||
this.commandlist.TabStop = false;
|
||||
this.commandlist.UseCompatibleStateImageBehavior = false;
|
||||
this.commandlist.View = System.Windows.Forms.View.Details;
|
||||
this.commandlist.ItemActivate += new System.EventHandler(this.commandlist_ItemActivate);
|
||||
//
|
||||
// columnHeader1
|
||||
//
|
||||
this.columnHeader1.Text = "Action";
|
||||
this.columnHeader1.Width = 275;
|
||||
//
|
||||
// columnHeader3
|
||||
//
|
||||
this.columnHeader3.Text = "Section";
|
||||
this.columnHeader3.Width = 196;
|
||||
//
|
||||
// columnHeader2
|
||||
//
|
||||
this.columnHeader2.Text = "Key";
|
||||
this.columnHeader2.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
|
||||
this.columnHeader2.Width = 117;
|
||||
//
|
||||
// CommandPaletteControl
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.Controls.Add(this.noresults);
|
||||
this.Controls.Add(this.commandlist);
|
||||
this.Controls.Add(this.commandsearch);
|
||||
this.DoubleBuffered = true;
|
||||
this.Name = "CommandPaletteControl";
|
||||
this.Size = new System.Drawing.Size(870, 201);
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private OptimizedListView commandlist;
|
||||
private System.Windows.Forms.ColumnHeader columnHeader1;
|
||||
private System.Windows.Forms.ColumnHeader columnHeader2;
|
||||
private System.Windows.Forms.TextBox commandsearch;
|
||||
private System.Windows.Forms.ColumnHeader columnHeader3;
|
||||
private System.Windows.Forms.Label noresults;
|
||||
}
|
||||
}
|
461
Source/Core/Controls/CommandPaletteControl.cs
Normal file
461
Source/Core/Controls/CommandPaletteControl.cs
Normal file
|
@ -0,0 +1,461 @@
|
|||
#region ================== Copyright (c) 2022 Boris Iwanski
|
||||
|
||||
/*
|
||||
* 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 WARRANTY; 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<http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#endregion
|
||||
|
||||
#region ================== Namespaces
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Windows.Forms;
|
||||
using CodeImp.DoomBuilder.Windows;
|
||||
|
||||
#endregion
|
||||
|
||||
namespace CodeImp.DoomBuilder.Controls
|
||||
{
|
||||
public partial class CommandPaletteControl : UserControl
|
||||
{
|
||||
#region ================== Constants
|
||||
|
||||
private const int MAX_ITEMS = 20;
|
||||
private const int MAX_RECENT_ACTIONS = 5;
|
||||
private const int GROUP_RECENT = 0;
|
||||
private const int GROUP_USABLE = 1;
|
||||
private const int GROUP_UNUSABLE = 2;
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region ================== Variables
|
||||
|
||||
private readonly List<Actions.Action> recentactions;
|
||||
|
||||
#endregion
|
||||
|
||||
#region ================== Constructor
|
||||
|
||||
public CommandPaletteControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
recentactions = new List<Actions.Action>();
|
||||
|
||||
Enabled = false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ================== Methods
|
||||
|
||||
/// <summary>
|
||||
/// Hides the palette. Disabled it and sends it to the background.
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender</param>
|
||||
/// <param name="e">The event args</param>
|
||||
private void HidePalette(object sender, EventArgs e)
|
||||
{
|
||||
commandsearch.LostFocus -= HidePalette;
|
||||
Enabled = false;
|
||||
|
||||
if (Parent is MainForm mf)
|
||||
{
|
||||
mf.Resize -= Reposition;
|
||||
|
||||
mf.Controls.SetChildIndex(this, 0xffff);
|
||||
mf.ActiveControl = null;
|
||||
mf.Focus();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the color of the currently selected item.
|
||||
/// </summary>
|
||||
private void HighlightSelectedItem()
|
||||
{
|
||||
if (commandlist.SelectedItems.Count > 0)
|
||||
{
|
||||
commandlist.SelectedItems[0].BackColor = SystemColors.Highlight;
|
||||
commandlist.SelectedItems[0].ForeColor = SystemColors.HighlightText;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the palette
|
||||
/// </summary>
|
||||
public void MakeVisible()
|
||||
{
|
||||
if (Parent is MainForm mf)
|
||||
{
|
||||
// Reset everything to a blank slate
|
||||
commandsearch.Text = string.Empty;
|
||||
// commandsearch_TextChanged(this, EventArgs.Empty);
|
||||
FillCommandList(withrecent: true);
|
||||
HighlightSelectedItem();
|
||||
|
||||
// Set the width of each column to the max width of its fields
|
||||
commandlist.Columns[0].Width = -1;
|
||||
commandlist.Columns[1].Width = -1;
|
||||
commandlist.Columns[2].Width = -1;
|
||||
|
||||
// Compute the new width. It's the width of the columns, the vertical scroll bar and some buffer
|
||||
Width = commandlist.Columns[0].Width + commandlist.Columns[1].Width + commandlist.Columns[2].Width + SystemInformation.VerticalScrollBarWidth + commandlist.Location.X * 4;
|
||||
|
||||
// Center the control at the top middle
|
||||
Location = new Point(mf.Display.Width / 2 - Width / 2, mf.Display.Location.Y + 5);
|
||||
|
||||
Enabled = true;
|
||||
|
||||
commandsearch.Focus();
|
||||
|
||||
// We want to hide the control when the focus is lost
|
||||
commandsearch.LostFocus += HidePalette;
|
||||
|
||||
// Bring it to the foreground
|
||||
mf.Controls.SetChildIndex(this, 0);
|
||||
|
||||
// Always keep the control in the center
|
||||
mf.Resize += Reposition;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Keeps the control positioned in the top middle of the window when it is rezied.
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender</param>
|
||||
/// <param name="e">The event args</param>
|
||||
private void Reposition(object sender, EventArgs e)
|
||||
{
|
||||
// Center the control at the top middle
|
||||
if (Parent is MainForm mf)
|
||||
Location = new Point(mf.Display.Width / 2 - Width / 2, mf.Display.Location.Y + 5);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects the item before or after the current item in the command list.
|
||||
/// </summary>
|
||||
/// <param name="changeindexby">By how much the index should be changed. Positive numbers mean that it will scroll up, negative numbers will scroll down.</param>
|
||||
/// <param name="wraparound">If the selection should wrap around to the opposite side if the top or bottom of the list is reached</param>
|
||||
private void SetSelectedItem(int changeindexby, bool wraparound)
|
||||
{
|
||||
if (commandlist.Items.Count > 1)
|
||||
{
|
||||
int newindex = commandlist.SelectedIndices[0] + changeindexby;
|
||||
|
||||
if (newindex >= commandlist.Items.Count)
|
||||
{
|
||||
if (wraparound)
|
||||
newindex = 0;
|
||||
else
|
||||
newindex = commandlist.Items.Count - 1;
|
||||
}
|
||||
else if (newindex < 0)
|
||||
{
|
||||
if (wraparound)
|
||||
newindex = commandlist.Items.Count - 1;
|
||||
else
|
||||
newindex = 0;
|
||||
}
|
||||
|
||||
// Reset the colors of the currently selected item to the defaults
|
||||
commandlist.SelectedItems[0].BackColor = SystemColors.Window;
|
||||
commandlist.SelectedItems[0].ForeColor = SystemColors.WindowText;
|
||||
|
||||
// Set the new item, scroll the list to it, and set the highlight color
|
||||
commandlist.Items[newindex].Selected = true;
|
||||
commandlist.EnsureVisible(newindex);
|
||||
HighlightSelectedItem();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a search string matches a text. It replicates the behavior of Visual Stuido Code.
|
||||
/// At first it tries to match the whole search string. If that didn't produce a result it'll try to match as much of the search
|
||||
/// string at the *beginning* of a word in the text. If that worked the matching characters are removed from the search text and
|
||||
/// all words in the text up to (including) the found word are removed. This is repeated until all characters in the search string
|
||||
/// are gone. This means:
|
||||
/// "le cl" matches "Toggle classic rendering"
|
||||
/// ^^^^^
|
||||
/// "tore" matches "Toggle classic rendering"
|
||||
/// ^^ ^^
|
||||
/// "tcl" matches "Toggle classic rendering"
|
||||
/// ^ ^ ^
|
||||
/// "tof" matches "Toggle Full Brightness"
|
||||
/// ^^ ^
|
||||
/// "Align Floor Textures to Front Side"
|
||||
/// ^^ ^
|
||||
/// "Reset Texture Offsets"
|
||||
/// ^ ^^
|
||||
/// (and a couple other)
|
||||
/// </summary>
|
||||
/// <param name="text">The string to search in</param>
|
||||
/// <param name="search">The string to search for</param>
|
||||
/// <returns></returns>
|
||||
private bool MatchText(string text, string search)
|
||||
{
|
||||
text = text.ToLowerInvariant().Trim();
|
||||
text = Regex.Replace(text, @"\s+", " ");
|
||||
|
||||
search = search.ToLowerInvariant().Trim();
|
||||
search = Regex.Replace(search, @"\s+", " ");
|
||||
|
||||
// Check if the search string is empty or the whole search string is in the text to search
|
||||
if (string.IsNullOrWhiteSpace(search) || text.Contains(search))
|
||||
return true;
|
||||
|
||||
// No match yet, so let's check if all search tokens are at the beginning of a text token. This is the same(ish?) behavior as Visual Studio Code.
|
||||
// This means that searching for "op ma" will match "Open Map", but not "Open Command Palette", because the "ma" in "Command" is not in the beginning.
|
||||
List<string> textitems = text.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||
string[] searchitems = search.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
for(int i=0; i < searchitems.Length; i++)
|
||||
{
|
||||
string si = searchitems[i];
|
||||
|
||||
// If the search item is empty it means we processed all its characters, so go to the next search item
|
||||
if (string.IsNullOrEmpty(si))
|
||||
continue;
|
||||
|
||||
string result = null;
|
||||
|
||||
// Search token not found, so try to match parts of the search token
|
||||
while (si.Length > 0)
|
||||
{
|
||||
// Try to find the first text token that starts with the search token
|
||||
result = textitems.FirstOrDefault(ti => ti.StartsWith(si));
|
||||
|
||||
// We found something, so remove the matching part of the search token and prepare processing this search token again
|
||||
if (result != null)
|
||||
{
|
||||
searchitems[i] = searchitems[i].Remove(0, si.Length);
|
||||
i--;
|
||||
break;
|
||||
}
|
||||
|
||||
// Nothing found, so remove the last character and keep going
|
||||
si = si.Remove(si.Length - 1);
|
||||
}
|
||||
|
||||
// Nothing found, so abort
|
||||
if (result == null)
|
||||
return false;
|
||||
|
||||
// We found a search token (or part of it), so remove all text tokens up to including the found text token
|
||||
int index = textitems.IndexOf(result);
|
||||
textitems.RemoveRange(0, index + 1);
|
||||
}
|
||||
|
||||
// We didn't return yet, so we must have found everything
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an action to the command list, either in the "usable" or "unsuable" group.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to add</param>
|
||||
private void AddActionToList(Actions.Action action, bool isrecent = false)
|
||||
{
|
||||
string actiontitle = action.Title;
|
||||
string catname = string.Empty;
|
||||
bool isbound = action.BeginBound || action.EndBound;
|
||||
|
||||
if (General.Actions.Categories.ContainsKey(action.Category))
|
||||
catname = General.Actions.Categories[action.Category];
|
||||
|
||||
ListViewItem item = commandlist.Items.Add(action.Name, actiontitle, 0);
|
||||
|
||||
// Store the action in the tag, so we can invoke the action later
|
||||
item.Tag = action;
|
||||
|
||||
// Add the item to the appropriate group, either the "usable" (0) or "unusable" (1) one
|
||||
if (isrecent)
|
||||
item.Group = commandlist.Groups[GROUP_RECENT];
|
||||
else
|
||||
item.Group = commandlist.Groups[isbound ? GROUP_USABLE : GROUP_UNUSABLE];
|
||||
|
||||
item.SubItems.Add(catname);
|
||||
item.SubItems.Add(Actions.Action.GetShortcutKeyDesc(action.ShortcutKey));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs an action and adds it to the list of recent actions
|
||||
/// </summary>
|
||||
/// <param name="action"></param>
|
||||
private void RunAction(Actions.Action action)
|
||||
{
|
||||
// Remove the action (if it's in the list) and then insert it at the beginning
|
||||
recentactions.Remove(action);
|
||||
recentactions.Insert(0, action);
|
||||
|
||||
// Remove all actions that exceed the limit of the max number of recent actions
|
||||
if (recentactions.Count > MAX_RECENT_ACTIONS)
|
||||
recentactions.RemoveRange(4, recentactions.Count - MAX_RECENT_ACTIONS);
|
||||
|
||||
General.Actions.InvokeAction(action.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills the control, filtering it so that only the actions that match the search string are shown.
|
||||
/// </summary>
|
||||
/// <param name="searchtext">Text to search for in the action name</param>
|
||||
/// <param name="withrecent">If recently shown actions should be shown or not</param>
|
||||
private void FillCommandList(string searchtext = "", bool withrecent = false)
|
||||
{
|
||||
List<Actions.Action> usableactions = new List<Actions.Action>();
|
||||
List<Actions.Action> unusableactions = new List<Actions.Action>();
|
||||
|
||||
commandlist.BeginUpdate();
|
||||
commandlist.Items.Clear();
|
||||
|
||||
Actions.Action[] actions = General.Actions.GetAllActions();
|
||||
|
||||
// Crawl through all actions and check if they are usable or not in the current context
|
||||
foreach (Actions.Action a in actions)
|
||||
{
|
||||
if (MatchText(a.Title, searchtext))
|
||||
{
|
||||
if (a.BeginBound || a.EndBound)
|
||||
usableactions.Add(a);
|
||||
else
|
||||
unusableactions.Add(a);
|
||||
}
|
||||
}
|
||||
|
||||
// If there are matching actions we have to change the control's height and set the default selection
|
||||
if (usableactions.Count + unusableactions.Count > 0)
|
||||
{
|
||||
noresults.Visible = false;
|
||||
commandlist.Visible = true;
|
||||
|
||||
if (withrecent)
|
||||
foreach (Actions.Action a in recentactions) if (a != null) AddActionToList(a, true);
|
||||
|
||||
// We have to do the sorting on our own, because otherwise the groups will screw with the selection logic when pressing the up/down keys
|
||||
foreach (Actions.Action a in usableactions.OrderBy(o => o.Title)) AddActionToList(a);
|
||||
foreach (Actions.Action a in unusableactions.OrderBy(o => o.Title)) AddActionToList(a);
|
||||
|
||||
// We want to show at most MAX_ITEMS items before having a scroll bar
|
||||
int numitems = commandlist.Items.Count > MAX_ITEMS ? MAX_ITEMS : commandlist.Items.Count;
|
||||
|
||||
// Get the height of a row
|
||||
int itemheight = commandlist.Items[0].GetBounds(ItemBoundsPortion.Entire).Height;
|
||||
|
||||
// Get the number of shown groups
|
||||
int numgroups = (usableactions.Count == 0 ? 0 : 1) + (unusableactions.Count == 0 ? 0 : 1);
|
||||
|
||||
// Set the new height, which is the number of items times the row height, the groups, the search textbox and some buffer
|
||||
Height = itemheight * numitems + commandsearch.Height + numgroups * (int)(itemheight * 1.4) + commandlist.Location.X * 5;
|
||||
|
||||
// Select the topmost item and highlight it
|
||||
commandlist.Items[0].Selected = true;
|
||||
HighlightSelectedItem();
|
||||
|
||||
noresults.Visible = false;
|
||||
}
|
||||
else // No matching actions, hide line command list and tell the user that there are no matches
|
||||
{
|
||||
commandlist.Visible = false;
|
||||
noresults.Visible = true;
|
||||
|
||||
Height = noresults.Location.Y + noresults.Height + noresults.Margin.Left * 2;
|
||||
}
|
||||
|
||||
commandlist.EndUpdate();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ================== Events
|
||||
|
||||
private void commandsearch_TextChanged(object sender, EventArgs e)
|
||||
{
|
||||
string searchtext = commandsearch.Text.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(searchtext))
|
||||
FillCommandList(withrecent: true);
|
||||
else
|
||||
FillCommandList(searchtext);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles certain special keys. Esc will close the palette, the Up and Down keys will change the selection, and Enter will start the command.
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender</param>
|
||||
/// <param name="e">The event args</param>
|
||||
private void commandsearch_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
switch(e.KeyCode)
|
||||
{
|
||||
case Keys.Escape:
|
||||
case Keys.Down:
|
||||
case Keys.Up:
|
||||
case Keys.PageDown:
|
||||
case Keys.PageUp:
|
||||
//case Keys.End:
|
||||
//case Keys.Home:
|
||||
case Keys.Enter:
|
||||
e.Handled = true;
|
||||
e.SuppressKeyPress = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (e.KeyCode == Keys.Escape)
|
||||
HidePalette(this, EventArgs.Empty);
|
||||
else if (e.KeyCode == Keys.Down)
|
||||
SetSelectedItem(1, true);
|
||||
else if (e.KeyCode == Keys.Up)
|
||||
SetSelectedItem(-1, true);
|
||||
else if (e.KeyCode == Keys.PageDown)
|
||||
SetSelectedItem(MAX_ITEMS - 1, false);
|
||||
else if (e.KeyCode == Keys.PageUp)
|
||||
SetSelectedItem(-MAX_ITEMS + 1, false);
|
||||
//else if (e.KeyCode == Keys.End)
|
||||
// SetSelectedItem(commandlist.Items.Count, false);
|
||||
//else if (e.KeyCode == Keys.Home)
|
||||
// SetSelectedItem(0, false);
|
||||
else if (e.KeyCode == Keys.Enter)
|
||||
{
|
||||
if (commandlist.Items.Count > 0)
|
||||
{
|
||||
HidePalette(this, EventArgs.Empty);
|
||||
RunAction((Actions.Action)commandlist.SelectedItems[0].Tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run the command that was clicked on
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender</param>
|
||||
/// <param name="e">The event args</param>
|
||||
private void commandlist_ItemActivate(object sender, EventArgs e)
|
||||
{
|
||||
HidePalette(this, EventArgs.Empty);
|
||||
|
||||
RunAction((Actions.Action)commandlist.SelectedItems[0].Tag);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
120
Source/Core/Controls/CommandPaletteControl.resx
Normal file
120
Source/Core/Controls/CommandPaletteControl.resx
Normal file
|
@ -0,0 +1,120 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
|
@ -1443,3 +1443,13 @@ placethingatcursor
|
|||
allowscroll = false;
|
||||
default = 131076;
|
||||
}
|
||||
|
||||
opencommandpalette
|
||||
{
|
||||
title = "Open Command Palette";
|
||||
category = "tools";
|
||||
description = "Opens the command palette.";
|
||||
allowkeys = true;
|
||||
allowmouse = true;
|
||||
allowscroll = true;
|
||||
}
|
|
@ -159,6 +159,8 @@ namespace CodeImp.DoomBuilder.Windows
|
|||
|
||||
//mxd. Misc drawing
|
||||
private Graphics graphics;
|
||||
|
||||
private CommandPaletteControl commandpalette;
|
||||
|
||||
#endregion
|
||||
|
||||
|
@ -465,11 +467,28 @@ namespace CodeImp.DoomBuilder.Windows
|
|||
|
||||
this.Update();
|
||||
}
|
||||
|
||||
|
||||
// We're doing it in EndAction because it'll otherwise screw with the stored keys
|
||||
[EndAction("opencommandpalette")]
|
||||
public void OpenCommandPalette()
|
||||
{
|
||||
if (commandpalette == null)
|
||||
{
|
||||
// We have to add the command palette control manually because trying to use the designer will make the form explode
|
||||
commandpalette = new CommandPaletteControl();
|
||||
Controls.Add(commandpalette);
|
||||
|
||||
// Send it somewhere to the background
|
||||
Controls.SetChildIndex(commandpalette, 0xffff);
|
||||
}
|
||||
|
||||
commandpalette.MakeVisible();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region ================== Window
|
||||
|
||||
|
||||
// This locks the window for updating
|
||||
internal void LockUpdate()
|
||||
{
|
||||
|
@ -1162,7 +1181,7 @@ namespace CodeImp.DoomBuilder.Windows
|
|||
{
|
||||
General.Plugins.OnEditMouseEnter(e);
|
||||
General.Editing.Mode.OnMouseEnter(e);
|
||||
if(Application.OpenForms.Count == 1 || editformopen) display.Focus(); //mxd
|
||||
if((Application.OpenForms.Count == 1 || editformopen) && (commandpalette == null ? true : !commandpalette.Visible)) display.Focus(); //mxd
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1424,7 +1443,7 @@ namespace CodeImp.DoomBuilder.Windows
|
|||
if(alt) mod |= (int)Keys.Alt;
|
||||
if(shift) mod |= (int)Keys.Shift;
|
||||
if(ctrl) mod |= (int)Keys.Control;
|
||||
|
||||
|
||||
// Don't process any keys when they are meant for other input controls
|
||||
if((e.KeyData != Keys.None) && ((ActiveControl == null) || (ActiveControl == display)))
|
||||
{
|
||||
|
@ -1480,7 +1499,7 @@ namespace CodeImp.DoomBuilder.Windows
|
|||
private void MainForm_KeyUp(object sender, KeyEventArgs e)
|
||||
{
|
||||
int mod = 0;
|
||||
|
||||
|
||||
// Keep key modifiers
|
||||
alt = e.Alt;
|
||||
shift = e.Shift;
|
||||
|
|
Loading…
Reference in a new issue