#region ================== Copyright (c) 2020 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.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Windows.Forms;
using CodeImp.DoomBuilder.Controls;
namespace CodeImp.DoomBuilder.UDBScript
{
public partial class ScriptDockerControl : UserControl
{
#region ================== Variables
private ImageList images;
private ContextMenuStrip filecontextmenu;
private ContextMenuStrip foldercontextmenu;
ToolStripItem[] slotitems;
#endregion
#region ================== Properties
public ImageList Images { get { return images; } }
#endregion
#region ================== Constructor
public ScriptDockerControl(string foldername)
{
InitializeComponent();
images = new ImageList();
images.Images.Add("Folder", Properties.Resources.Folder);
images.Images.Add("Script", Properties.Resources.Script);
filetree.ImageList = images;
}
#endregion
#region ================== Methods
///
/// Creates the context menu for file items.
///
private void CreateFileContextMenu()
{
ToolStripMenuItem edititem = new ToolStripMenuItem("Edit");
edititem.Click += EditScriptClicked;
ToolStripMenuItem deleteitem = new ToolStripMenuItem("Clear slot");
deleteitem.Tag = "deleteitem";
slotitems = new ToolStripItem[BuilderPlug.NUM_SCRIPT_SLOTS + 2];
slotitems[0] = deleteitem;
slotitems[1] = new ToolStripSeparator();
for (int i=0; i < BuilderPlug.NUM_SCRIPT_SLOTS; i++)
{
slotitems[i+2] = new ToolStripMenuItem("Slot " + (i + 1));
slotitems[i+2].Tag = i + 1;
}
ToolStripMenuItem setslot = new ToolStripMenuItem("Set slot");
setslot.DropDownItems.AddRange(slotitems);
setslot.DropDownItemClicked += ItemClicked;
filecontextmenu = new ContextMenuStrip();
filecontextmenu.Items.AddRange(new ToolStripItem[]
{
edititem,
setslot
});
}
///
/// Creates the context menu for folder items.
///
private void CreateFolderContextMenu()
{
ToolStripMenuItem openitem = new ToolStripMenuItem("Open in Explorer");
openitem.Click += (s, e) => { try { Process.Start("explorer.exe", ((ScriptDirectoryStructure)filetree.SelectedNodes[0].Tag).Path); } catch { } };
foldercontextmenu = new ContextMenuStrip();
foldercontextmenu.Items.AddRange(new ToolStripItem[]
{
openitem
});
}
///
/// Returns the hotkey text for a script s lot.
///
/// Slot to get the hotkey text for
/// The hotkey text
private string GetHotkeyText(int slot)
{
string actionname = "udbscript_udbscriptexecuteslot" + slot;
string keytext = "no hotkey";
Actions.Action action = General.Actions.GetActionByName(actionname);
if (action.ShortcutKey != 0)
keytext = Actions.Action.GetShortcutKeyDesc(actionname);
return keytext;
}
///
/// Updates the context menu of the slots so that the items show the script name and hotkey (if applicable)
///
private void UpdateFileContextMenu()
{
for (int i = 0; i < BuilderPlug.NUM_SCRIPT_SLOTS; i++)
{
ScriptInfo si = BuilderPlug.Me.GetScriptSlot(i + 1);
if (si != null)
slotitems[i+2].Text = "Slot " + (i+1) + ": " + si.Name + " [" + GetHotkeyText(i+1) + "]";
else
slotitems[i+2].Text = "Slot " + (i+1) + ": not assigned [" + GetHotkeyText(i + 1) + "]";
}
}
///
/// Recursively updates the tree, so that the items show the hotkey (if applicable)
///
///
private void UpdateTree(TreeNode node)
{
ScriptInfo si = node.Tag as ScriptInfo;
// Update the item
if(si != null)
{
int slot = BuilderPlug.Me.GetScriptSlotByScriptInfo(si);
if (slot == 0) // Not assigned to a slot, just set the name
node.Text = si.Name;
else // It's assigned to a slot, so set the name and the hotkey
node.Text = si.Name + " [" + GetHotkeyText(slot) + "]";
}
// Update all children
foreach(TreeNode childnode in node.Nodes)
UpdateTree(childnode);
}
///
/// Assignes a script to a slot.
///
///
///
private void ItemClicked(object sender, ToolStripItemClickedEventArgs e)
{
ScriptInfo si = filetree.SelectedNodes[0].Tag as ScriptInfo;
if (si == null)
return;
if (e.ClickedItem.Tag is string && (string)e.ClickedItem.Tag == "deleteitem")
{
int slot = BuilderPlug.Me.GetScriptSlotByScriptInfo(si);
if(slot != 0)
BuilderPlug.Me.SetScriptSlot(slot, null);
}
else
{
if (e.ClickedItem.Tag is int)
BuilderPlug.Me.SetScriptSlot((int)e.ClickedItem.Tag, si);
}
UpdateFileContextMenu();
foreach (TreeNode node in filetree.Nodes)
UpdateTree(node);
}
///
/// Edits a script.
///
///
///
private void EditScriptClicked(object sender, EventArgs e)
{
ScriptInfo si = filetree.SelectedNodes[0].Tag as ScriptInfo;
if (si == null)
return;
//MessageBox.Show("Edit script " + si.ScriptFile);
BuilderPlug.Me.EditScript(si.ScriptFile);
}
///
/// Fills the tree of all available scripts. Tries to re-select a previously selected script
///
public void FillTree()
{
string previousscriptfile = string.Empty;
NodesCollection nc = filetree.SelectedNodes;
string filtertext = tbFilter.Text.ToLowerInvariant().Trim();
scriptoptions.ParametersView.Rows.Clear();
scriptoptions.ParametersView.Refresh();
if(nc.Count > 0 && nc[0].Tag is ScriptInfo)
previousscriptfile = ((ScriptInfo)nc[0].Tag).ScriptFile;
filetree.BeginUpdate();
filetree.Nodes.Clear();
filetree.Nodes.AddRange(AddToTree(filtertext, BuilderPlug.Me.ScriptDirectoryStructure));
//filetree.ExpandAll();
foreach(TreeNode node in filetree.Nodes)
{
TreeNode result = FindScriptTreeNode(previousscriptfile, node);
if (result != null)
{
filetree.SelectedNodes.Add(result);
break;
}
}
filetree.EndUpdate();
}
///
/// Recursively tries to find a tree node by the script file name. Based on https://stackoverflow.com/a/19227024 by user "King King"
///
/// Script file name to look for
/// TreeNode node to start looking at
/// Found TreeNode or null
private TreeNode FindScriptTreeNode(string name, TreeNode root)
{
foreach (TreeNode node in root.Nodes)
{
if (node.Tag is ScriptInfo && ((ScriptInfo)node.Tag).ScriptFile == name)
return node;
TreeNode next = FindScriptTreeNode(name, node);
if (next != null)
return next;
}
return null;
}
///
/// Recursively adds nodes to the tree. Optionally filters script, showing only the ones containing the filter text in
/// the script name or description.
///
/// Text to filter by. null or an empty string will not filter at all
/// Directory structur or crawl through
///
private TreeNode[] AddToTree(string filtertext, ScriptDirectoryStructure sds)
{
List newnodes = new List();
if (sds == null)
return newnodes.ToArray();
// Go through folders and add files (and other folders) recusrively
foreach (ScriptDirectoryStructure subsds in sds.Directories.OrderBy(s => s.Name))
{
TreeNode[] children = AddToTree(filtertext, subsds);
TreeNode tn = new TreeNode(subsds.Name, AddToTree(filtertext, subsds));
tn.Tag = subsds;
tn.SelectedImageKey = tn.ImageKey = "Folder";
tn.ContextMenuStrip = foldercontextmenu;
if (subsds.Expanded)
tn.Expand();
newnodes.Add(tn);
}
// Add the scripts in to folder to the tree
foreach(ScriptInfo si in sds.Scripts.OrderBy(s => s.Name))
{
// Check if there's a text to filter scripts by, and if there is, skip scripts that do not contain
// the filter in the script name or description
if (!string.IsNullOrWhiteSpace(filtertext))
{
if (!si.Name.ToLowerInvariant().Contains(filtertext) && !si.Description.ToLowerInvariant().Contains(filtertext))
continue;
}
TreeNode tn = new TreeNode();
int slot = BuilderPlug.Me.GetScriptSlotByScriptInfo(si);
if (slot == 0) // Not assigned to a slot, just set the name
tn.Text = si.Name;
else // It's assigned to a slot, so set the name and the hotkey
tn.Text = si.Name + " [" + GetHotkeyText(slot) + "]";
tn.Tag = si;
tn.SelectedImageKey = tn.ImageKey = "Script";
tn.ContextMenuStrip = filecontextmenu;
newnodes.Add(tn);
}
return newnodes.ToArray();
}
///
/// Ends editing the currently edited grid view cell. This is required so that the value is applied before running the script if the cell is currently
/// being editing (i.e. typing in a value, then running the script without clicking somewhere else first)
///
public void EndEdit()
{
scriptoptions.EndEdit();
}
#endregion
#region ================== Events
///
/// Sets up the the script options control for the currently selected script
///
/// the sender
/// the event
private void filetree_AfterSelect(object sender, TreeViewEventArgs e)
{
if (e.Node.Tag == null)
{
tbDescription.Text = string.Empty;
scriptoptions.ParametersView.Rows.Clear();
return;
}
if(e.Node.Tag is ScriptInfo)
{
BuilderPlug.Me.CurrentScript = (ScriptInfo)e.Node.Tag;
scriptoptions.ParametersView.Rows.Clear();
foreach (ScriptOption so in ((ScriptInfo)e.Node.Tag).Options)
{
int index = scriptoptions.ParametersView.Rows.Add();
scriptoptions.ParametersView.Rows[index].Tag = so;
scriptoptions.ParametersView.Rows[index].Cells["Value"].Value = so.value;
scriptoptions.ParametersView.Rows[index].Cells["Description"].Value = so.description;
}
scriptoptions.EndAddingOptions();
tbDescription.Text = ((ScriptInfo)e.Node.Tag).Description;
}
else
{
scriptoptions.ParametersView.Rows.Clear();
scriptoptions.ParametersView.Refresh();
}
}
///
/// Runs the currently selected script immediately
///
/// the sender
/// the event
private void btnRunScript_Click(object sender, EventArgs e)
{
BuilderPlug.Me.ScriptExecute();
}
///
/// Resets all options of the currently selected script to their default values
///
/// the sender
/// the event
private void btnResetToDefaults_Click(object sender, EventArgs e)
{
foreach (DataGridViewRow row in scriptoptions.ParametersView.Rows)
{
if (row.Tag is ScriptOption)
{
ScriptOption so = (ScriptOption)row.Tag;
row.Cells["Value"].Value = so.defaultvalue.ToString();
so.typehandler.SetValue(so.defaultvalue);
General.Settings.DeletePluginSetting("scriptoptions." + BuilderPlug.Me.CurrentScript.GetScriptPathHash() + "." + so.name);
}
}
}
///
/// Sets the node that was clicked on as the selected node. This is needed for the context menu, because it's the easiest
/// way to know which node the context menu was opened for.
///
/// The sender
/// The event
private void filetree_NodeMouseClick(object sender, TreeNodeMouseClickEventArgs e)
{
((TreeView)sender).SelectedNode = e.Node;
}
private void ScriptDockerControl_VisibleChanged(object sender, EventArgs e)
{
if (!Visible || Disposing)
return;
CreateFileContextMenu();
UpdateFileContextMenu();
CreateFolderContextMenu();
FillTree();
}
private void btnClearFilter_Click(object sender, EventArgs e)
{
tbFilter.Clear();
}
private void tbFilter_TextChanged(object sender, EventArgs e)
{
// TreeNodes can't by dynamically hidden, so it's easier to just fill the tholw tree again
FillTree();
}
#endregion
private void filetree_BeforeCollapse(object sender, TreeViewCancelEventArgs e)
{
ScriptDirectoryStructure sds;
if(e.Node.Tag is ScriptDirectoryStructure)
{
sds = (ScriptDirectoryStructure)e.Node.Tag;
sds.Expanded = false;
}
// Immedeiately save the status, otherwise folders will be expanded/collapsed incorrectly on hot reload
BuilderPlug.Me.SaveScriptDirectoryExpansionStatus(BuilderPlug.Me.ScriptDirectoryStructure);
}
private void filetree_BeforeExpand(object sender, TreeViewCancelEventArgs e)
{
ScriptDirectoryStructure sds;
if (e.Node.Tag is ScriptDirectoryStructure)
{
sds = (ScriptDirectoryStructure)e.Node.Tag;
sds.Expanded = true;
}
// Immedeiately save the status, otherwise folders will be expanded/collapsed incorrectly on hot reload
BuilderPlug.Me.SaveScriptDirectoryExpansionStatus(BuilderPlug.Me.ScriptDirectoryStructure);
}
}
}