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:
biwa 2022-04-09 11:10:41 +02:00
parent 91d79e12be
commit 9c7526a80c
8 changed files with 773 additions and 9 deletions

View file

@ -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

View file

@ -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>

View file

@ -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>

View 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;
}
}

View 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
}
}

View 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>

View file

@ -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;
}

View file

@ -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;