diff --git a/Cmdline/ConsoleUser.cs b/Cmdline/ConsoleUser.cs index 12a733fcf0..2d42320a49 100644 --- a/Cmdline/ConsoleUser.cs +++ b/Cmdline/ConsoleUser.cs @@ -4,32 +4,53 @@ namespace CKAN.CmdLine { - public class ConsoleUser : NullUser + /// + /// The commandline implementation of the IUser interface. + /// + public class ConsoleUser : IUser { + /// + /// A logger for this class. + /// ONLY FOR INTERNAL USE! + /// private static readonly ILog log = LogManager.GetLogger(typeof(ConsoleUser)); - private bool m_Headless = false; - public ConsoleUser(bool headless) + /// + /// Initializes a new instance of the class. + /// + /// If set to true, supress interactive dialogs like Yes/No-Dialog or SelectionDialog. + public ConsoleUser (bool headless) { - m_Headless = headless; + Headless = headless; } - public override bool Headless + /// + /// Gets a value indicating whether this is headless. + /// + /// true if headless; otherwise, false. + public bool Headless { get; } + + /// + /// Gets a value indicating whether this + /// should show confirmation prompts. Depends on . + /// + public bool ConfirmPrompt { - get - { - return m_Headless; - } + get { return !Headless; } } - protected override bool DisplayYesNoDialog(string message) + /// + /// Ask the user for a yes or no input. + /// + /// Question. + public bool RaiseYesNoDialog(string question) { - if (m_Headless) + if (Headless) { return true; } - Console.Write("{0} [Y/n] ", message); + Console.Write("{0} [Y/n] ", question); while (true) { var input = Console.In.ReadLine(); @@ -60,22 +81,20 @@ protected override bool DisplayYesNoDialog(string message) } } - protected override void DisplayMessage(string message, params object[] args) - { - Console.WriteLine(message, args); - } - - protected override void DisplayError(string message, params object[] args) - { - Console.Error.WriteLine(message, args); - } - - protected override int DisplaySelectionDialog(string message, params object[] args) + /// + /// Ask the user to select one of the elements of the array. + /// The output is index 0 based. + /// To supply a default option, make the first option an integer indicating the index of it. + /// + /// The selection dialog. + /// Message. + /// Array of available options. + public int RaiseSelectionDialog(string message, params object[] args) { const int return_cancel = -1; // Check for the headless flag. - if (m_Headless) + if (Headless) { // Return that the user cancelled the selection process. return return_cancel; @@ -227,17 +246,33 @@ protected override int DisplaySelectionDialog(string message, params object[] ar return result; } - protected override void ReportProgress(string format, int percent) + /// + /// Write an error to the console. + /// + /// Message. + /// Possible arguments to format the message. + public void RaiseError(string message, params object[] args) + { + Console.Error.WriteLine(message, args); + } + + /// + /// Write a progress message including the percentage to the console. + /// Rewrites the line, so the console is not cluttered by progress messages. + /// + /// Message. + /// Progress in percent. + public void RaiseProgress(string message, int percent) { - if (Regex.IsMatch(format, "download", RegexOptions.IgnoreCase)) + if (Regex.IsMatch(message, "download", RegexOptions.IgnoreCase)) { // In headless mode, only print a new message if the percent has changed, // to reduce clutter in Jenkins for large downloads - if (!m_Headless || percent != previousPercent) + if (!Headless || percent != previousPercent) { // The \r at the front here causes download messages to *overwrite* each other. Console.Write( - "\r{0} - {1}% ", format, percent); + "\r{0} - {1}% ", message, percent); previousPercent = percent; } } @@ -246,10 +281,23 @@ protected override void ReportProgress(string format, int percent) // The percent looks weird on non-download messages. // The leading newline makes sure we don't end up with a mess from previous // download messages. - Console.Write("\r\n{0}", format); + Console.Write("\r\n{0}", message); } } + /// + /// Needed for + /// private int previousPercent = -1; + + /// + /// Writes a message to the console. + /// + /// Message. + /// Arguments to format the message. + public void RaiseMessage(string message, params object[] args) + { + Console.WriteLine(message, args); + } } } diff --git a/ConsoleUI/Toolkit/ConsoleScreen.cs b/ConsoleUI/Toolkit/ConsoleScreen.cs index 41f3693733..62558ec7ee 100644 --- a/ConsoleUI/Toolkit/ConsoleScreen.cs +++ b/ConsoleUI/Toolkit/ConsoleScreen.cs @@ -72,13 +72,19 @@ protected virtual string MenuTip() /// protected ConsolePopupMenu mainMenu = null; - // IUser + #region IUser /// /// Tell IUser clients that we have the ability to interact with the user /// public bool Headless { get { return false; } } + /// + /// Show confirmation prompts. + /// Atm used to ask for confirmation prior to installing mods. + /// + public bool ConfirmPrompt { get { return true; } } + // These functions can be implemented the same on all screens, // so they are not virtual. @@ -207,7 +213,7 @@ public void RaiseProgress(string message, int percent) /// Value from 0 to 100 representing task completion protected virtual void Progress(string message, int percent) { } - // End IUser + #endregion IUser private void DrawSelectedHamburger() { diff --git a/Core/ModuleInstaller.cs b/Core/ModuleInstaller.cs index 3b2048af6b..4d0c9b4bf1 100644 --- a/Core/ModuleInstaller.cs +++ b/Core/ModuleInstaller.cs @@ -177,7 +177,15 @@ public void InstallList(ICollection modules, RelationshipResolverOpt } } - bool ok = User.RaiseYesNoDialog("\r\nContinue?"); + bool ok; + if (User.ConfirmPrompt) + { + ok = User.RaiseYesNoDialog("\r\nContinue?"); + } + else + { + ok = true; + } if (!ok) { @@ -770,7 +778,15 @@ public void UninstallList(IEnumerable mods) User.RaiseMessage(" * {0} {1}", module.Module.name, module.Module.version); } - bool ok = User.RaiseYesNoDialog("\r\nContinue?"); + bool ok; + if (User.ConfirmPrompt) + { + ok = User.RaiseYesNoDialog("\r\nContinue?"); + } + else + { + ok = true; + } if (!ok) { diff --git a/Core/User.cs b/Core/User.cs index 7db134e765..3dfa6595d4 100644 --- a/Core/User.cs +++ b/Core/User.cs @@ -1,80 +1,73 @@ -// Communicate with the user (status messages, yes/no questions, etc) -// This class will proxy to either the GUI or cmdline functionality. - namespace CKAN { - + /// + /// This interface holds all methods which communicate with the user in some way. + /// Every CKAN interface (GUI, cmdline, consoleUI) has an implementation of the IUser interface. + /// The implementations define HOW we interact with the user. + /// public interface IUser { bool Headless { get; } + bool ConfirmPrompt { get; } bool RaiseYesNoDialog(string question); int RaiseSelectionDialog(string message, params object[] args); void RaiseError(string message, params object[] args); void RaiseProgress(string message, int percent); - void RaiseMessage(string message, params object[] url); + void RaiseMessage(string message, params object[] args); } - //Can be used in tests to supress output or as a base class for other types of user. - //It supplies no op event handlers so that subclasses can avoid null checks. + /// + /// To be used in tests. + /// Supresses all output. + /// public class NullUser : IUser { - public static readonly IUser User = new NullUser(); - - public NullUser() { } - - public virtual bool Headless - { - get { return false; } - } - - protected virtual bool DisplayYesNoDialog(string message) + /// + /// NullUser is headless. Variable not used for NullUser. + /// + public bool Headless { - return true; + get { return true; } } - protected virtual int DisplaySelectionDialog(string message, params object[] args) - { - return 0; - } - - protected virtual void DisplayError(string message, params object[] args) - { - } - - protected virtual void ReportProgress(string format, int percent) - { - } - - protected virtual void DisplayMessage(string message, params object[] args) + /// + /// Indicates if a confirmation prompt should be shown for this type of User. + /// NullUser returns false. + /// + /// true if confirm prompt should be shown; false if not. + public bool ConfirmPrompt { + get { return false; } } + /// + /// NullUser returns true. + /// public bool RaiseYesNoDialog(string question) { - return DisplayYesNoDialog(question); + return true; } + /// + /// NullUser returns 0. + /// public int RaiseSelectionDialog(string message, params object[] args) { - return DisplaySelectionDialog(message, args); + return 0; } public void RaiseError(string message, params object[] args) { - DisplayError(message, args); } public void RaiseProgress(string message, int percent) { - ReportProgress(message, percent); } public void RaiseMessage(string message, params object[] args) { - DisplayMessage(message, args); } - } } diff --git a/GUI/CKAN-GUI.csproj b/GUI/CKAN-GUI.csproj index 03ead9787e..de268760d6 100644 --- a/GUI/CKAN-GUI.csproj +++ b/GUI/CKAN-GUI.csproj @@ -238,6 +238,12 @@ YesNoDialog.cs + + Form + + + SelectionDialog.cs + @@ -294,6 +300,9 @@ YesNoDialog.cs + + SelectionDialog.cs + diff --git a/GUI/ChooseKSPInstance.cs b/GUI/ChooseKSPInstance.cs index 90932997a1..0e6101655a 100644 --- a/GUI/ChooseKSPInstance.cs +++ b/GUI/ChooseKSPInstance.cs @@ -87,7 +87,7 @@ private void AddNewButton_Click(object sender, EventArgs e) } catch (NotKSPDirKraken k) { - GUI.user.displayError("Directory {0} is not valid KSP directory.", + GUI.user.RaiseError("Directory {0} is not valid KSP directory.", new object[] { k.path }); return; } @@ -125,7 +125,7 @@ private void UseSelectedInstance() } catch (NotKSPDirKraken k) { - GUI.user.displayError("Directory {0} is not valid KSP directory.", + GUI.user.RaiseError("Directory {0} is not valid KSP directory.", new object[] { k.path }); } } diff --git a/GUI/GUIUser.cs b/GUI/GUIUser.cs index ee32da91e7..653747a92d 100644 --- a/GUI/GUIUser.cs +++ b/GUI/GUIUser.cs @@ -2,44 +2,83 @@ namespace CKAN { - - public class GUIUser : NullUser + /// + /// The GUI implementation of the IUser interface. + /// + public class GUIUser : IUser { - public delegate bool DisplayYesNo(string message); - - public Action displayMessage; - public Action displayError; - public DisplayYesNo displayYesNo; + /// + /// A GUIUser is obviously not headless. Returns false. + /// + public bool Headless + { + get { return false; } + } - protected override bool DisplayYesNoDialog(string message) + /// + /// Indicates if a confirmation prompt should be shown for this type of User. + /// E.g. don't ask for confirmation prior to installing mods in the GUI, + /// because it's already done in a different way. + /// + /// true if confirm prompt should be shown; false if not. + public bool ConfirmPrompt { - if (displayYesNo == null) - return true; + get { return false; } + } - return displayYesNo(message); + /// + /// Shows a small form with the question. + /// User can select yes or no (ya dont say). + /// + /// true if user pressed yes, false if no. + /// Question. + public bool RaiseYesNoDialog (string question) + { + return Main.Instance.YesNoDialog(question); } - protected override void DisplayMessage(string message, params object[] args) + /// + /// Will show a small form with the message and a list to choose from. + /// + /// The index of the selection in the args array. 0-based! + /// Message. + /// Array of offered options. + public int RaiseSelectionDialog(string message, params object[] args) { - if (displayMessage != null) - { - displayMessage(message, args); - } + return Main.Instance.SelectionDialog(message, args); } - protected override void DisplayError(string message, params object[] args) + /// + /// Shows a message box containing the formatted error message. + /// + /// Message. + /// Arguments to format the message. + public void RaiseError (string message, params object[] args) { - if (displayError != null) - { - displayError(message, args); - } + Main.Instance.ErrorDialog(message, args); } - protected override void ReportProgress(string format, int percent) + /// + /// Sets the progress bars and the message box of the current WaitTabPage. + /// + /// Message. + /// Progress in percent. + public void RaiseProgress( string message, int percent) { - Main.Instance.SetDescription($"{format} - {percent}%"); + Main.Instance.SetDescription($"{message} - {percent}%"); Main.Instance.SetProgress(percent); } + + /// + /// Displays the formatted message in the lower StatusStrip. + /// Removes any newline strings. + /// + /// Message. + /// Arguments to fromat the message. + public void RaiseMessage (string message, params object[] args) + { + Main.Instance.AddStatusMessage(message, args); + } } } diff --git a/GUI/Main.cs b/GUI/Main.cs index 4d0e76d6f1..11b13d2a1b 100644 --- a/GUI/Main.cs +++ b/GUI/Main.cs @@ -170,10 +170,6 @@ public Main(string[] cmdlineArgs, KSPManager mgr, GUIUser user, bool showConsole log.Info("Starting the GUI"); commandLineArgs = cmdlineArgs; - // These are used by KSPManager's constructor to show messages about directory creation - user.displayMessage = AddStatusMessage; - user.displayError = ErrorDialog; - manager = mgr ?? new KSPManager(user); currentUser = user; @@ -389,10 +385,7 @@ protected override void OnLoad(EventArgs e) installWorker.RunWorkerCompleted += PostInstallMods; installWorker.DoWork += InstallMods; - var old_YesNoDialog = currentUser.displayYesNo; - currentUser.displayYesNo = YesNoDialog; URLHandlers.RegisterURLHandler(configuration, currentUser); - currentUser.displayYesNo = old_YesNoDialog; ApplyToolButton.Enabled = false; diff --git a/GUI/MainDialogs.cs b/GUI/MainDialogs.cs index 553f166779..298f5c9d07 100644 --- a/GUI/MainDialogs.cs +++ b/GUI/MainDialogs.cs @@ -11,6 +11,7 @@ public partial class Main private SettingsDialog settingsDialog; private PluginsDialog pluginsDialog; private YesNoDialog yesNoDialog; + private SelectionDialog selectionDialog; public void RecreateDialogs() { @@ -18,6 +19,7 @@ public void RecreateDialogs() settingsDialog = controlFactory.CreateControl(); pluginsDialog = controlFactory.CreateControl(); yesNoDialog = controlFactory.CreateControl(); + selectionDialog = controlFactory.CreateControl(); } public void AddStatusMessage(string text, params object[] args) @@ -40,6 +42,11 @@ public bool YesNoDialog(string text) return yesNoDialog.ShowYesNoDialog(text) == DialogResult.Yes; } + public int SelectionDialog(string message, params object[] args) + { + return selectionDialog.ShowSelectionDialog(message, args); + } + // Ugly Hack. Possible fix is to alter the relationship provider so we can use a loop // over reason for to find a user requested mod. Or, you know, pass in a handler to it. private readonly ConcurrentStack last_mod_to_have_install_toggled = new ConcurrentStack(); diff --git a/GUI/MainImport.cs b/GUI/MainImport.cs index 6f0bc56de6..30b8de4ce2 100644 --- a/GUI/MainImport.cs +++ b/GUI/MainImport.cs @@ -31,10 +31,7 @@ private void ImportModules() if (dlg.ShowDialog() == DialogResult.OK && dlg.FileNames.Length > 0) { - - // Set up GUI to respond to IUser calls... - GUIUser.DisplayYesNo old_YesNoDialog = currentUser.displayYesNo; - currentUser.displayYesNo = YesNoDialog; + // Show WaitTabPage (status page) and lock it. tabController.RenameTab("WaitTabPage", "Status log"); tabController.ShowTab("WaitTabPage"); tabController.SetTabLock(true); @@ -50,7 +47,6 @@ private void ImportModules() finally { // Put GUI back the way we found it - currentUser.displayYesNo = old_YesNoDialog; tabController.SetTabLock(false); tabController.HideTab("WaitTabPage"); } diff --git a/GUI/MainRepo.cs b/GUI/MainRepo.cs index f2e8181abd..ba307a7c4e 100644 --- a/GUI/MainRepo.cs +++ b/GUI/MainRepo.cs @@ -26,9 +26,6 @@ public static RepositoryList FetchMasterRepositoryList(Uri master_uri = null) public void UpdateRepo() { - var old_dialog = currentUser.displayYesNo; - currentUser.displayYesNo = YesNoDialog; - tabController.RenameTab("WaitTabPage", "Updating repositories"); CurrentInstance.ScanGameData(); @@ -37,10 +34,7 @@ public void UpdateRepo() { m_UpdateRepoWorker.RunWorkerAsync(); } - finally - { - currentUser.displayYesNo = old_dialog; - } + catch { } Util.Invoke(this, SwitchEnabledState); @@ -84,8 +78,6 @@ private void UpdateRepo(object sender, DoWorkEventArgs e) { errorDialog.ShowErrorDialog("Failed to connect to repository. Exception: " + ex.Message); } - - currentUser.displayYesNo = null; } private void PostUpdateRepo(object sender, RunWorkerCompletedEventArgs e) @@ -112,14 +104,12 @@ private void ShowRefreshQuestion() { if (!configuration.RefreshOnStartupNoNag) { - currentUser.displayYesNo = YesNoDialog; configuration.RefreshOnStartupNoNag = true; - if (!currentUser.displayYesNo("Would you like CKAN to refresh the modlist every time it is loaded? (You can always manually refresh using the button up top.)")) + if (!currentUser.RaiseYesNoDialog("Would you like CKAN to refresh the modlist every time it is loaded? (You can always manually refresh using the button up top.)")) { configuration.RefreshOnStartup = false; } configuration.Save(); - currentUser.displayYesNo = null; } } diff --git a/GUI/SelectionDialog.Designer.cs b/GUI/SelectionDialog.Designer.cs new file mode 100644 index 0000000000..7c5367d850 --- /dev/null +++ b/GUI/SelectionDialog.Designer.cs @@ -0,0 +1,129 @@ +using System; + +namespace CKAN +{ + partial class SelectionDialog + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose (bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent () + { + this.panel1 = new System.Windows.Forms.Panel(); + this.MessageLabel = new System.Windows.Forms.Label(); + this.SelectButton = new System.Windows.Forms.Button(); + this.DefaultButton = new System.Windows.Forms.Button(); + this.CancelButton = new System.Windows.Forms.Button(); + this.OptionsList = new System.Windows.Forms.ListBox(); + this.panel1.SuspendLayout(); + this.SuspendLayout(); + // + // panel1 + // + this.panel1.Controls.Add(this.MessageLabel); + this.panel1.Controls.Add(this.OptionsList); + this.panel1.Controls.Add(this.CancelButton); + this.panel1.Controls.Add(this.DefaultButton); + this.panel1.Controls.Add(this.SelectButton); + this.panel1.Location = new System.Drawing.Point(10, 10); + this.panel1.Size = new System.Drawing.Size(400, 400); + this.panel1.Name = "panel1"; + this.OptionsList.TabStop = false; + this.DefaultButton.UseVisualStyleBackColor = true; + // + // MessageLabel + // + this.MessageLabel.Location = new System.Drawing.Point(5, 5); + this.MessageLabel.Size = new System.Drawing.Size(390, 40); + this.MessageLabel.Name = "MessageLabel"; + this.OptionsList.TabStop = false; + this.MessageLabel.Text = "Please select:"; + this.MessageLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + this.DefaultButton.UseVisualStyleBackColor = true; + // + // OptionsList + // + this.OptionsList.Location = new System.Drawing.Point(5, 55); + this.OptionsList.Size = new System.Drawing.Size(390, 315); + this.OptionsList.SelectionMode = System.Windows.Forms.SelectionMode.One; + this.OptionsList.MultiColumn = false; + this.OptionsList.SelectedIndexChanged += new System.EventHandler(OptionsList_SelectedIndexChanged); + this.OptionsList.Name = "OptionsList"; + this.DefaultButton.UseVisualStyleBackColor = true; + // + // SelectButton + // + this.SelectButton.Location = new System.Drawing.Point(325, 375); + this.SelectButton.Size = new System.Drawing.Size(60, 20); + this.SelectButton.DialogResult = System.Windows.Forms.DialogResult.OK; + this.SelectButton.Name = "SelectButton"; + this.SelectButton.TabIndex = 1; + this.SelectButton.Text = "Select"; + this.SelectButton.UseVisualStyleBackColor = true; + // + // DefaultButton + // + this.DefaultButton.Location = new System.Drawing.Point(160, 375); + this.DefaultButton.Size = new System.Drawing.Size(60, 20); + this.DefaultButton.DialogResult = System.Windows.Forms.DialogResult.Yes; + this.DefaultButton.Name = "SelectButton"; + this.DefaultButton.TabIndex = 0; + this.DefaultButton.Text = "Default"; + this.DefaultButton.UseVisualStyleBackColor = true; + // + // CancelButton + // + this.CancelButton.Location = new System.Drawing.Point(5, 375); + this.CancelButton.Size = new System.Drawing.Size(60, 20); + this.CancelButton.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.CancelButton.Name = "CancelButton"; + this.CancelButton.TabIndex = 2; + this.CancelButton.Text = "Cancel"; + this.CancelButton.UseVisualStyleBackColor = true; + // + // SelectionDialog + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(420, 420); + this.Controls.Add(this.panel1); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow; + this.Name = "SelectionDialog"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "CKAN Selection Dialog"; + this.panel1.ResumeLayout(false); + this.ResumeLayout(false); + this.PerformLayout(); + } + + #endregion + + private System.Windows.Forms.Panel panel1; + private System.Windows.Forms.Label MessageLabel; + private System.Windows.Forms.Button SelectButton; + private System.Windows.Forms.Button DefaultButton; + private new System.Windows.Forms.Button CancelButton; + private System.Windows.Forms.ListBox OptionsList; + } +} diff --git a/GUI/SelectionDialog.cs b/GUI/SelectionDialog.cs new file mode 100644 index 0000000000..cb04ff19ed --- /dev/null +++ b/GUI/SelectionDialog.cs @@ -0,0 +1,122 @@ +using System; +using System.Windows.Forms; + +namespace CKAN +{ + public partial class SelectionDialog : FormCompatibility + { + int currentSelected; + + public SelectionDialog () + { + InitializeComponent(); + currentSelected = 0; + } + + /// + /// Shows the selection dialog. + /// + /// The selected index, -1 if canceled. + /// Message. + /// Array of items to select from. + public int ShowSelectionDialog (string message, params object[] args) + { + int defaultSelection = -1; + int return_cancel = -1; + + // Validate input. + if (String.IsNullOrWhiteSpace(message)) + { + throw new Kraken("Passed message string must be non-empty."); + } + + if (args.Length == 0) + { + throw new Kraken("Passed list of selection candidates must be non-empty."); + } + + // Hide the default button unless we have a default option + Util.Invoke(DefaultButton, DefaultButton.Hide); + // Clear the item list. + Util.Invoke(OptionsList, OptionsList.Items.Clear); + + // Check if we have a default option. + if (args[0] is int) + { + // Check that the default selection makes sense. + defaultSelection = (int)args[0]; + + if (defaultSelection < 0 || defaultSelection > args.Length - 1) + { + throw new Kraken("Passed default arguments is out of range of the selection candidates."); + } + + // Extract the relevant arguments. + object[] newArgs = new object[args.Length - 1]; + + for (int i = 1; i < args.Length; i++) + { + newArgs[i - 1] = args[i]; + } + + args = newArgs; + + // Show the defaultButton. + Util.Invoke(DefaultButton, DefaultButton.Show); + } + + // Further data validation. + foreach (object argument in args) + { + if (String.IsNullOrWhiteSpace(argument.ToString())) + { + throw new Kraken("Candidate may not be empty."); + } + } + + // Add all items to the OptionsList. + for (int i = 0; i < args.Length; i++) + { + if (defaultSelection == i) + { + Util.Invoke(OptionsList, () => OptionsList.Items.Add(String.Concat(args[i].ToString(), " -- Default"))); + + } + else + { + Util.Invoke(OptionsList, () => OptionsList.Items.Add(args[i].ToString())); + } + } + + // Write the message to the label. + Util.Invoke(MessageLabel, () => MessageLabel.Text = message); + + // Now show the dialog and get the return values. + DialogResult result = ShowDialog(); + if (result == DialogResult.Yes) + { + // If pressed Defaultbutton + return defaultSelection; + } + else if (result == DialogResult.Cancel) + { + // If pressed CancelButton + return return_cancel; + } + else + { + return currentSelected; + } + } + + public void HideYesNoDialog () + { + Util.Invoke(this, Close); + } + + private void OptionsList_SelectedIndexChanged(object sender, EventArgs e) + { + currentSelected = OptionsList.SelectedIndex; + } + } +} diff --git a/GUI/SelectionDialog.resx b/GUI/SelectionDialog.resx new file mode 100644 index 0000000000..1af7de150c --- /dev/null +++ b/GUI/SelectionDialog.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Netkan/ConsoleUser.cs b/Netkan/ConsoleUser.cs index 028f9d59f1..15cfa70c72 100644 --- a/Netkan/ConsoleUser.cs +++ b/Netkan/ConsoleUser.cs @@ -4,24 +4,55 @@ namespace CKAN { - public class ConsoleUser : NullUser + /// + /// The commandline implementation of the IUser interface. + /// It is exactly the same as the one of the CKAN-cmdline. + /// At least at the time of this commit (git blame is your friend). + /// + public class ConsoleUser : IUser { + /// + /// A logger for this class. + /// ONLY FOR INTERNAL USE! + /// private static readonly ILog log = LogManager.GetLogger(typeof(ConsoleUser)); - private bool m_Headless = false; - public ConsoleUser(bool headless) + /// + /// Initializes a new instance of the class. + /// + /// If set to true, supress interactive dialogs like Yes/No-Dialog or SelectionDialog. + public ConsoleUser (bool headless) { - m_Headless = headless; + Headless = headless; } - protected override bool DisplayYesNoDialog(string message) + /// + /// Gets a value indicating whether this + /// should show confirmation prompts. Depends on . + /// + public bool ConfirmPrompt { - if (m_Headless) + get { return !Headless; } + } + + /// + /// Gets a value indicating whether this is headless. + /// + /// true if headless; otherwise, false. + public bool Headless { get; } + + /// + /// Ask the user for a yes or no input. + /// + /// Question. + public bool RaiseYesNoDialog (string question) + { + if (Headless) { return true; } - Console.Write("{0} [Y/N] ", message); + Console.Write("{0} [Y/n] ", question); while (true) { var input = Console.In.ReadLine(); @@ -42,31 +73,208 @@ protected override bool DisplayYesNoDialog(string message) { return false; } + if (input.Equals(string.Empty)) + { + // User pressed enter without any text, assuming default choice. + return true; + } + Console.Write("Invalid input. Please enter yes or no"); } } - protected override void DisplayMessage(string message, params object[] args) + /// + /// Ask the user to select one of the elements of the array. + /// The output is index 0 based. + /// To supply a default option, make the first option an integer indicating the index of it. + /// + /// The selection dialog. + /// Message. + /// Array of available options. + public int RaiseSelectionDialog (string message, params object[] args) { - Console.WriteLine(message, args); + const int return_cancel = -1; + + // Check for the headless flag. + if (Headless) + { + // Return that the user cancelled the selection process. + return return_cancel; + } + + // Validate input. + if (String.IsNullOrWhiteSpace(message)) + { + throw new Kraken("Passed message string must be non-empty."); + } + + if (args.Length == 0) + { + throw new Kraken("Passed list of selection candidates must be non-empty."); + } + + // Check if we have a default selection. + int defaultSelection = -1; + + if (args[0] is int) + { + // Check that the default selection makes sense. + defaultSelection = (int)args[0]; + + if (defaultSelection < 0 || defaultSelection > args.Length - 1) + { + throw new Kraken("Passed default arguments is out of range of the selection candidates."); + } + + // Extract the relevant arguments. + object[] newArgs = new object[args.Length - 1]; + + for (int i = 1; i < args.Length; i++) + { + newArgs[i - 1] = args[i]; + } + + args = newArgs; + } + + // Further data validation. + foreach (object argument in args) + { + if (String.IsNullOrWhiteSpace(argument.ToString())) + { + throw new Kraken("Candidate may not be empty."); + } + } + + // List options. + for (int i = 0; i < args.Length; i++) + { + string CurrentRow = String.Format("{0}", i + 1); + + if (i == defaultSelection) + { + CurrentRow += "*"; + } + + CurrentRow += String.Format(") {0}", args[i]); + + RaiseMessage(CurrentRow); + } + + // Create message string. + string output = String.Format("Enter a number between {0} and {1} (To cancel press \"c\" or \"n\".", 1, args.Length); + + if (defaultSelection >= 0) + { + output += String.Format(" \"Enter\" will select {0}.", defaultSelection + 1); + } + + output += "): "; + + RaiseMessage(output); + + bool valid = false; + int result = 0; + + while (!valid) + { + // Wait for input from the command line. + string input = Console.In.ReadLine(); + + if (input == null) + { + // No console present, cancel the process. + return return_cancel; + } + + input = input.Trim().ToLower(); + + // Check for default selection. + if (String.IsNullOrEmpty(input)) + { + if (defaultSelection >= 0) + { + return defaultSelection; + } + } + + // Check for cancellation characters. + if (input == "c" || input == "n") + { + RaiseMessage("Selection cancelled."); + + return return_cancel; + } + + // Attempt to parse the input. + try + { + result = Convert.ToInt32(input); + } + catch (FormatException) + { + RaiseMessage("The input is not a number."); + continue; + } + catch (OverflowException) + { + RaiseMessage("The number in the input is too large."); + continue; + } + + // Check the input against the boundaries. + if (result > args.Length) + { + RaiseMessage("The number in the input is too large."); + RaiseMessage(output); + + continue; + } + else if (result < 1) + { + RaiseMessage("The number in the input is too small."); + RaiseMessage(output); + + continue; + } + + // The list we provide is index 1 based, but the array is index 0 based. + result--; + + // We have checked for all errors and have gotten a valid result. Stop the input loop. + valid = true; + } + + return result; } - protected override void DisplayError(string message, params object[] args) + /// + /// Write an error to the console. + /// + /// Message. + /// Possible arguments to format the message. + public void RaiseError (string message, params object[] args) { Console.Error.WriteLine(message, args); } - protected override void ReportProgress(string format, int percent) + /// + /// Write a progress message including the percentage to the console. + /// Rewrites the line, so the console is not cluttered by progress messages. + /// + /// Message. + /// Progress in percent. + public void RaiseProgress (string message, int percent) { - if (Regex.IsMatch(format, "download", RegexOptions.IgnoreCase)) + if (Regex.IsMatch(message, "download", RegexOptions.IgnoreCase)) { // In headless mode, only print a new message if the percent has changed, // to reduce clutter in Jenkins for large downloads - if (!m_Headless || percent != previousPercent) + if (!Headless || percent != previousPercent) { // The \r at the front here causes download messages to *overwrite* each other. Console.Write( - "\r{0} - {1}% ", format, percent); + "\r{0} - {1}% ", message, percent); previousPercent = percent; } } @@ -75,10 +283,23 @@ protected override void ReportProgress(string format, int percent) // The percent looks weird on non-download messages. // The leading newline makes sure we don't end up with a mess from previous // download messages. - Console.Write("\r\n{0}", format); + Console.Write("\r\n{0}", message); } } + /// + /// Needed for + /// private int previousPercent = -1; + + /// + /// Writes a message to the console. + /// + /// Message. + /// Arguments to format the message. + public void RaiseMessage (string message, params object[] args) + { + Console.WriteLine(message, args); + } } } diff --git a/Tests/Core/KSP.cs b/Tests/Core/KSP.cs index 4199c83a80..9d0a416dd3 100644 --- a/Tests/Core/KSP.cs +++ b/Tests/Core/KSP.cs @@ -12,13 +12,15 @@ public class KSP { private CKAN.KSP ksp; private string ksp_dir; + private IUser nullUser; [SetUp] public void Setup() { ksp_dir = TestData.NewTempDir(); + nullUser = new NullUser(); TestData.CopyDirectory(TestData.good_ksp_dir(), ksp_dir); - ksp = new CKAN.KSP(ksp_dir, "test", NullUser.User); + ksp = new CKAN.KSP(ksp_dir, "test", nullUser); } [TearDown] @@ -138,7 +140,7 @@ public void Valid_MissingVersionData_False() File.WriteAllText(jsonpath, compatible_ksp_versions_json); // Act - CKAN.KSP my_ksp = new CKAN.KSP(gamedir, "missing-ver-test", NullUser.User); + CKAN.KSP my_ksp = new CKAN.KSP(gamedir, "missing-ver-test", nullUser); // Assert Assert.IsFalse(my_ksp.Valid); @@ -170,7 +172,7 @@ public void Constructor_NullMainCompatVer_NoCrash() // Act & Assert Assert.DoesNotThrow(() => { - CKAN.KSP my_ksp = new CKAN.KSP(gamedir, "null-compat-ver-test", NullUser.User); + CKAN.KSP my_ksp = new CKAN.KSP(gamedir, "null-compat-ver-test", nullUser); }); } diff --git a/Tests/Core/ModuleInstaller.cs b/Tests/Core/ModuleInstaller.cs index 92b0bd7659..b6010a206d 100644 --- a/Tests/Core/ModuleInstaller.cs +++ b/Tests/Core/ModuleInstaller.cs @@ -25,6 +25,8 @@ public class ModuleInstaller private string mission_zip; private CkanModule mission_mod; + private IUser nullUser; + [SetUp] public void Setup() { @@ -40,6 +42,8 @@ public void Setup() mission_zip = TestData.MissionZip(); mission_mod = TestData.MissionModule(); + + nullUser = new NullUser(); } [Test] @@ -434,7 +438,7 @@ public void UninstallModNotFound() Assert.Throws(delegate { // This should throw, as our tidy KSP has no mods installed. - CKAN.ModuleInstaller.GetInstance(manager.CurrentInstance, manager.Cache, NullUser.User).UninstallList("Foo"); + CKAN.ModuleInstaller.GetInstance(manager.CurrentInstance, manager.Cache, nullUser).UninstallList("Foo"); }); manager.CurrentInstance = null; // I weep even more. @@ -480,7 +484,7 @@ public void CanInstallMod() // Attempt to install it. List modules = new List { TestData.DogeCoinFlag_101_module().identifier }; - CKAN.ModuleInstaller.GetInstance(ksp.KSP, manager.Cache, NullUser.User).InstallList(modules, new RelationshipResolverOptions()); + CKAN.ModuleInstaller.GetInstance(ksp.KSP, manager.Cache, nullUser).InstallList(modules, new RelationshipResolverOptions()); // Check that the module is installed. Assert.IsTrue(File.Exists(mod_file_path)); @@ -511,13 +515,13 @@ public void CanUninstallMod() List modules = new List { TestData.DogeCoinFlag_101_module().identifier }; - CKAN.ModuleInstaller.GetInstance(manager.CurrentInstance, manager.Cache, NullUser.User).InstallList(modules, new RelationshipResolverOptions()); + CKAN.ModuleInstaller.GetInstance(manager.CurrentInstance, manager.Cache, nullUser).InstallList(modules, new RelationshipResolverOptions()); // Check that the module is installed. Assert.IsTrue(File.Exists(mod_file_path)); // Attempt to uninstall it. - CKAN.ModuleInstaller.GetInstance(manager.CurrentInstance, manager.Cache, NullUser.User).UninstallList(modules); + CKAN.ModuleInstaller.GetInstance(manager.CurrentInstance, manager.Cache, nullUser).UninstallList(modules); // Check that the module is not installed. Assert.IsFalse(File.Exists(mod_file_path)); @@ -549,7 +553,7 @@ public void UninstallEmptyDirs() List modules = new List { TestData.DogeCoinFlag_101_module().identifier }; - CKAN.ModuleInstaller.GetInstance(manager.CurrentInstance, manager.Cache, NullUser.User).InstallList(modules, new RelationshipResolverOptions()); + CKAN.ModuleInstaller.GetInstance(manager.CurrentInstance, manager.Cache, nullUser).InstallList(modules, new RelationshipResolverOptions()); modules.Clear(); @@ -559,7 +563,7 @@ public void UninstallEmptyDirs() modules.Add(TestData.DogeCoinPlugin_module().identifier); - CKAN.ModuleInstaller.GetInstance(manager.CurrentInstance, manager.Cache, NullUser.User).InstallList(modules, new RelationshipResolverOptions()); + CKAN.ModuleInstaller.GetInstance(manager.CurrentInstance, manager.Cache, nullUser).InstallList(modules, new RelationshipResolverOptions()); modules.Clear(); @@ -571,7 +575,7 @@ public void UninstallEmptyDirs() modules.Add(TestData.DogeCoinFlag_101_module().identifier); modules.Add(TestData.DogeCoinPlugin_module().identifier); - CKAN.ModuleInstaller.GetInstance(manager.CurrentInstance, manager.Cache, NullUser.User).UninstallList(modules); + CKAN.ModuleInstaller.GetInstance(manager.CurrentInstance, manager.Cache, nullUser).UninstallList(modules); // Check that the directory has been deleted. Assert.IsFalse(Directory.Exists(directoryPath)); @@ -607,7 +611,7 @@ public void ModuleManagerInstancesAreDecoupled() // Attempt to install it. List modules = new List { TestData.DogeCoinFlag_101_module().identifier }; - CKAN.ModuleInstaller.GetInstance(ksp.KSP, manager.Cache, NullUser.User).InstallList(modules, new RelationshipResolverOptions()); + CKAN.ModuleInstaller.GetInstance(ksp.KSP, manager.Cache, nullUser).InstallList(modules, new RelationshipResolverOptions()); // Check that the module is installed. string mod_file_path = Path.Combine(ksp.KSP.GameData(), mod_file_name); diff --git a/Tests/Core/ModuleInstallerDirTest.cs b/Tests/Core/ModuleInstallerDirTest.cs index 08eff847ce..6cd28bd691 100644 --- a/Tests/Core/ModuleInstallerDirTest.cs +++ b/Tests/Core/ModuleInstallerDirTest.cs @@ -23,6 +23,7 @@ public class ModuleInstallerDirTest private CKAN.ModuleInstaller _installer; private CkanModule _testModule; private string _gameDataDir; + private IUser _nullUser; /// /// Prep environment by setting up a single mod in @@ -33,10 +34,12 @@ public void SetUp() { _testModule = TestData.DogeCoinFlag_101_module(); - _manager = new KSPManager(NullUser.User); + _nullUser = new NullUser(); + + _manager = new KSPManager(_nullUser); _instance = new DisposableKSP(); _registry = CKAN.RegistryManager.Instance(_instance.KSP).registry; - _installer = CKAN.ModuleInstaller.GetInstance(_instance.KSP, _manager.Cache, NullUser.User); + _installer = CKAN.ModuleInstaller.GetInstance(_instance.KSP, _manager.Cache, _nullUser); _gameDataDir = _instance.KSP.GameData(); _registry.AddAvailable(_testModule); diff --git a/Tests/Data/DisposableKSP.cs b/Tests/Data/DisposableKSP.cs index 47ba2b84fd..42bd00cacb 100644 --- a/Tests/Data/DisposableKSP.cs +++ b/Tests/Data/DisposableKSP.cs @@ -38,7 +38,7 @@ public DisposableKSP(string directoryToClone = null, string registryFile = null) File.Copy(registryFile, registryPath, true); } - KSP = new KSP(_disposableDir, "disposable", NullUser.User); + KSP = new KSP(_disposableDir, "disposable", new NullUser()); Logging.Initialize(); }