From f3a7b5c97a34c5cb52089b9f4e2ad3ed554c1951 Mon Sep 17 00:00:00 2001 From: FenPhoenix Date: Mon, 28 Sep 2020 10:54:51 -0700 Subject: [PATCH] Large cleanup and organization in MainForm --- AngelLoader/Forms/ControlExtensions.cs | 10 +- .../DataGridViewCustom/Menu_FM.cs | 4 - AngelLoader/Forms/IView.cs | 2 +- AngelLoader/Forms/MainForm.cs | 4518 +++++++++-------- AngelLoader/Forms/MainForm_InitManual.cs | 4 +- AngelLoader/Forms/MainForm_Progress.cs | 3 - 6 files changed, 2277 insertions(+), 2264 deletions(-) diff --git a/AngelLoader/Forms/ControlExtensions.cs b/AngelLoader/Forms/ControlExtensions.cs index 48568f4cb..1eb7d34b5 100644 --- a/AngelLoader/Forms/ControlExtensions.cs +++ b/AngelLoader/Forms/ControlExtensions.cs @@ -1,5 +1,7 @@ -using System.Drawing; +using System; +using System.Drawing; using System.Windows.Forms; +using AngelLoader.WinAPI; using JetBrains.Annotations; using static AngelLoader.WinAPI.InteropMisc; @@ -145,5 +147,11 @@ internal static void RemoveAndSelectNearest(this ListBox listBox) } internal static bool EqualsIfNotNull(this object? sender, object? equals) => sender != null && equals != null && sender == equals; + + internal static void HideFocusRectangle(this Control control) => SendMessage( + control.Handle, + WM_CHANGEUISTATE, + new IntPtr(SetControlFocusToHidden), + new IntPtr(0)); } } diff --git a/AngelLoader/Forms/CustomControls/DataGridViewCustom/Menu_FM.cs b/AngelLoader/Forms/CustomControls/DataGridViewCustom/Menu_FM.cs index 5b6f68564..38ad1d56b 100644 --- a/AngelLoader/Forms/CustomControls/DataGridViewCustom/Menu_FM.cs +++ b/AngelLoader/Forms/CustomControls/DataGridViewCustom/Menu_FM.cs @@ -38,8 +38,6 @@ public sealed partial class DataGridViewCustom #region FM context menu fields -#pragma warning disable IDE0069 // Disposable fields should be disposed - // These are disposed by adding them to an array and iterating through it in Dispose() // TODO: This probably doesn't even need to happen, as they prolly get dumped with everything else on app exit @@ -96,8 +94,6 @@ public sealed partial class DataGridViewCustom private ToolStripMenuItem? WebSearchMenuItem; -#pragma warning restore IDE0069 // Disposable fields should be disposed - #endregion #region Private methods diff --git a/AngelLoader/Forms/IView.cs b/AngelLoader/Forms/IView.cs index d315c40cc..9b1363014 100644 --- a/AngelLoader/Forms/IView.cs +++ b/AngelLoader/Forms/IView.cs @@ -8,7 +8,7 @@ namespace AngelLoader.Forms { - internal interface IView : ILocalizable, IEventDisabler + internal interface IView : ILocalizable, IEventDisabler, IKeyPressDisabler, IMessageFilter { #region Progress box diff --git a/AngelLoader/Forms/MainForm.cs b/AngelLoader/Forms/MainForm.cs index 78f7f2b43..603e66ae1 100644 --- a/AngelLoader/Forms/MainForm.cs +++ b/AngelLoader/Forms/MainForm.cs @@ -53,11 +53,74 @@ namespace AngelLoader.Forms { - public sealed partial class MainForm : Form, IView, IKeyPressDisabler, IMessageFilter + public sealed partial class MainForm : Form, IView { - // We don't need to dispose anything on here really, because the app closes when the form closes, so - // Windows will dispose it all anyway -#pragma warning disable IDE0069 // Disposable fields should be disposed + #region Private fields + + private FormWindowState _nominalWindowState; + private Size _nominalWindowSize; + private Point _nominalWindowLocation; + + private float _fMsListDefaultFontSizeInPoints; + private int _rMsListDefaultRowHeight; + + // To order them such that we can just look them up with an index + private readonly TabPage[] _gameTabsInOrder; + private readonly ToolStripButtonCustom[] _filterByGameButtonsInOrder; + private readonly TabPage[] _topRightTabsInOrder; + + private readonly Control[] _filterLabels; + private readonly ToolStripItem[] _filtersToolStripSeparatedItems; + private readonly Control[] _bottomAreaSeparatedItems; + + private readonly Component[][] _hideableFilterControls; + + private enum KeepSel { False, True, TrueNearest } + + private enum ZoomFMsDGVType + { + ZoomIn, + ZoomOut, + ResetZoom, + ZoomTo, + ZoomToHeightOnly + } + + // Set these beforehand and don't set autosize on any column! Or else it explodes everything because + // FMsDGV tries to refresh when it shouldn't and all kinds of crap. Phew. + private const int _ratingImageColumnWidth = 73; + private const int _finishedColumnWidth = 91; + + #region Bitmaps + + // We need to grab these images every time a cell is shown on the DataGridView, and pulling them from + // Resources every time is enormously expensive, causing laggy scrolling and just generally wasting good + // cycles. So we copy them only once to these local bitmaps, and voila, instant scrolling performance. + private readonly Bitmap?[] GameIcons = new Bitmap?[SupportedGameCount]; + + private Bitmap? BlankIcon; + private Bitmap? CheckIcon; + private Bitmap? RedQuestionMarkIcon; + + private Bitmap[]? StarIcons; + private Bitmap[]? FinishedOnIcons; + private Bitmap? FinishedOnUnknownIcon; + + #endregion + + private DataGridViewImageColumn? RatingImageColumn; + + public bool EventsDisabled { get; set; } + public bool KeyPressesDisabled { get; set; } + + // Needed for Rating column swap to prevent a possible exception when CellValueNeeded is called in the + // middle of the operation + private bool _cellValueNeededDisabled; + + private TransparentPanel? ViewBlockingPanel; + private bool _viewBlocked; + + #endregion #region Test / debug @@ -107,431 +170,121 @@ private void Test2Button_Click(object sender, EventArgs e) #endif - #endregion - - #region IView implementations - - #region Invoke - - public object InvokeSync(Delegate method) => Invoke(method); - //public object InvokeSync(Delegate method, params object[] args) => Invoke(method, args); +#if DEBUG || (Release_Testing && !RT_StartupOnly) + public string GetDebug1Text() => DebugLabel.Text; + public string GetDebug2Text() => DebugLabel2.Text; + public void SetDebug1Text(string value) => DebugLabel.Text = value; + public void SetDebug2Text(string value) => DebugLabel2.Text = value; +#endif #endregion - public Column GetCurrentSortedColumnIndex() => FMsDGV.CurrentSortedColumn; - public SortOrder GetCurrentSortDirection() => FMsDGV.CurrentSortDirection; - public bool GetShowRecentAtTop() => FilterShowRecentAtTopButton.Checked; + #region Message handling - public void Block(bool block) + protected override void WndProc(ref Message m) { - if (ViewBlockingPanel == null) - { - ViewBlockingPanel = new TransparentPanel { Visible = false }; - Controls.Add(ViewBlockingPanel); - ViewBlockingPanel.Dock = DockStyle.Fill; - } - - try - { - // Doesn't help the RichTextBox, it happily flickers like it always does. Oh well. - this.SuspendDrawing(); - _viewBlocked = block; - ViewBlockingPanel.Visible = block; - ViewBlockingPanel.BringToFront(); - } - finally + // A second instance has been started and told us to show ourselves, so do it here (nicer UX). + // This has to be in WndProc, not PreFilterMessage(). Shrug. + if (m.Msg == InteropMisc.WM_SHOWFIRSTINSTANCE) { - this.ResumeDrawing(); + if (WindowState == FormWindowState.Minimized) WindowState = _nominalWindowState; + Activate(); } + base.WndProc(ref m); } - public SelectedFM? GetSelectedFMPosInfo() => FMsDGV.RowSelected() ? FMsDGV.GetSelectedFMPosInfo() : null; - - public Filter GetFilter() => FMsDGV.Filter; - public string GetTitleFilter() => FilterTitleTextBox.Text; - public string GetAuthorFilter() => FilterAuthorTextBox.Text; - - public bool[] GetGameFiltersEnabledStates() + public bool PreFilterMessage(ref Message m) { - bool[] gamesChecked = new bool[SupportedGameCount]; + // So I don't forget what the return values do + const bool BlockMessage = true; + const bool PassMessageOn = false; - for (int i = 0; i < SupportedGameCount; i++) + static bool TryGetHWndFromMousePos(Message msg, out IntPtr result) { - gamesChecked[i] = _filterByGameButtonsInOrder[i].Checked; + Point pos = new Point(msg.LParam.ToInt32() & 0xffff, msg.LParam.ToInt32() >> 16); + result = InteropMisc.WindowFromPoint(pos); + return result != IntPtr.Zero && Control.FromHandle(result) != null; } - return gamesChecked; - } + // Note: CanFocus will be false if there are modal windows open - public bool GetFinishedFilter() => FilterByFinishedButton.Checked; - public bool GetUnfinishedFilter() => FilterByUnfinishedButton.Checked; - public bool GetShowUnsupportedFilter() => FilterShowUnsupportedButton.Checked; - public List GetFilterShownIndexList() => FMsDGV.FilterShownIndexList; + // This allows controls to be scrolled with the mousewheel when the mouse is over them, without + // needing to actually be focused. Vital for a good user experience. + #region Mouse + if (m.Msg == InteropMisc.WM_MOUSEWHEEL) + { + // IMPORTANT (PreFilterMessage): + // Do this check inside each if block rather than above, because the message may not + // be a mousemove message, and in that case we'd be trying to get a window point from a random + // value, and that causes the min,max,close button flickering. + if (!TryGetHWndFromMousePos(m, out IntPtr hWnd)) return PassMessageOn; -#if DEBUG || (Release_Testing && !RT_StartupOnly) - public string GetDebug1Text() => DebugLabel.Text; - public string GetDebug2Text() => DebugLabel2.Text; - public void SetDebug1Text(string value) => DebugLabel.Text = value; - public void SetDebug2Text(string value) => DebugLabel2.Text = value; -#endif + if (_viewBlocked || CursorOutsideAddTagsDropDownArea()) return BlockMessage; - public void Localize() => Localize(startup: false); + int wParam = (int)m.WParam; + int delta = wParam >> 16; + if (CanFocus && CursorOverControl(FilterBarFLP) && !CursorOverControl(FMsDGV)) + { + // Allow the filter bar to be mousewheel-scrolled with the buttons properly appearing + // and disappearing as appropriate + if (delta != 0) + { + int direction = delta > 0 ? InteropMisc.SB_LINELEFT : InteropMisc.SB_LINERIGHT; + int origSmallChange = FilterBarFLP.HorizontalScroll.SmallChange; - public void ChangeReadmeBoxFont(bool useFixed) => ReadmeRichTextBox.SetFontType(useFixed); + FilterBarFLP.HorizontalScroll.SmallChange = 45; - public void ShowInstallUninstallButton(bool enabled) - { - if (enabled) - { - if (!InstallUninstallFMLLButton.Constructed) + InteropMisc.SendMessage(FilterBarFLP.Handle, InteropMisc.WM_SCROLL, (IntPtr)direction, IntPtr.Zero); + + FilterBarFLP.HorizontalScroll.SmallChange = origSmallChange; + } + } + else if (CanFocus && CursorOverControl(FMsDGV) && (wParam & 0xFFFF) == InteropMisc.MK_CONTROL) { - InstallUninstallFMLLButton.Construct(this); - InstallUninstallFMLLButton.Localize(false); + if (delta != 0) ZoomFMsDGV(delta > 0 ? ZoomFMsDGVType.ZoomIn : ZoomFMsDGVType.ZoomOut); } - InstallUninstallFMLLButton.Show(); - } - else - { - InstallUninstallFMLLButton.Hide(); - } - } - - public void ChangeGameOrganization(bool startup = false) - { - if (Config.GameOrganization == GameOrganization.OneList) - { - Config.SelFM.DeepCopyTo(FMsDGV.CurrentSelFM); + else + { + // Stupid hack to fix "send mousewheel to underlying control and block further messages" + // functionality still not being fully reliable. We need to focus the parent control sometimes + // inexplicably. Sure. Whole point is to avoid having to do that, but sure. + if (CursorOverControl(TopSplitContainer.Panel2)) + { + TopSplitContainer.Panel2.Focus(); + } + else if (CursorOverControl(MainSplitContainer.Panel2)) + { + MainSplitContainer.Panel2.Focus(); + } + InteropMisc.SendMessage(hWnd, m.Msg, m.WParam, m.LParam); + } + return BlockMessage; } - else // ByTab + else if (m.Msg == InteropMisc.WM_MOUSEHWHEEL) { - // In case they don't match - Config.Filter.Games = GameIndexToGame(Config.GameTab); - - Config.GameTabsState.DeepCopyTo(FMsDGV.GameTabsState); + if (!TryGetHWndFromMousePos(m, out _)) return PassMessageOn; - FMsDGV.GameTabsState.GetSelectedFM(Config.GameTab).DeepCopyTo(FMsDGV.CurrentSelFM); - FMsDGV.GameTabsState.GetFilter(Config.GameTab).DeepCopyTo(FMsDGV.Filter); + if (_viewBlocked) return BlockMessage; - using (new DisableEvents(this)) + if (CanFocus && CursorOverControl(FMsDGV)) { - GamesTabControl.SelectedIndex = (int)Config.GameTab; + int delta = (int)m.WParam >> 16; + if (delta != 0) + { + int offset = FMsDGV.HorizontalScrollingOffset; + offset = delta < 0 ? (offset - 15).ClampToZero() : offset + 15; + FMsDGV.HorizontalScrollingOffset = offset; + return BlockMessage; + } } } - - // Do these even if we're not in startup, because we may have changed the game organization mode - for (int i = 0; i < SupportedGameCount; i++) + // Just handle the NC* messages and presto, we don't even need the mouse hook anymore! + // NC = Non-Client, ie. the mouse was in a non-client area of the control + else if (m.Msg == InteropMisc.WM_MOUSEMOVE || m.Msg == InteropMisc.WM_NCMOUSEMOVE) { - var game = GameIndexToGame((GameIndex)i); - _filterByGameButtonsInOrder[i].Checked = Config.Filter.Games.HasFlagFast(game); - } - - if (!startup) ChangeFilterControlsForGameType(); - } + if (!CanFocus) return PassMessageOn; - public void ShowFMsListZoomButtons(bool visible) - { - Lazy_FMsListZoomButtons.SetVisible(this, visible); - SetFilterBarWidth(); - } - - public void ClearUIAndCurrentInternalFilter() - { - using (new DisableEvents(this)) - { - FilterBarFLP.SuspendDrawing(); - try - { - bool oneList = Config.GameOrganization == GameOrganization.OneList; - if (oneList) - { - for (int i = 0; i < SupportedGameCount; i++) - { - _filterByGameButtonsInOrder[i].Checked = false; - } - } - FilterTitleTextBox.Text = ""; - FilterAuthorTextBox.Text = ""; - - FilterByReleaseDateButton.Checked = false; - Lazy_ToolStripLabels.Hide(Lazy_ToolStripLabel.FilterByReleaseDate); - - FilterByLastPlayedButton.Checked = false; - Lazy_ToolStripLabels.Hide(Lazy_ToolStripLabel.FilterByLastPlayed); - - FilterByTagsButton.Checked = false; - FilterByFinishedButton.Checked = false; - FilterByUnfinishedButton.Checked = false; - - FilterByRatingButton.Checked = false; - Lazy_ToolStripLabels.Hide(Lazy_ToolStripLabel.FilterByRating); - - FilterShowUnsupportedButton.Checked = false; - - // NOTE: Here is the line where the internal filter is cleared. It does in fact happen! - FMsDGV.Filter.Clear(oneList); - } - finally - { - FilterBarFLP.ResumeDrawing(); - } - } - } - - #region Messageboxes - - public bool AskToContinue(string message, string title, bool noIcon = false) => - MessageBox.Show( - message, - title, - MessageBoxButtons.YesNo, - noIcon ? MessageBoxIcon.None : MessageBoxIcon.Warning) == DialogResult.Yes; - - public (bool Cancel, bool Continue, bool DontAskAgain) - AskToContinueWithCancelCustomStrings(string message, string title, TaskDialogIcon? icon, bool showDontAskAgain, - string yes, string no, string cancel, ButtonType? defaultButton = null) - { - var yesButton = new TaskDialogButton(yes); - var noButton = new TaskDialogButton(no); - var cancelButton = new TaskDialogButton(cancel); - - using var d = new TaskDialog( - title: title, - message: message, - buttons: new[] { yesButton, noButton, cancelButton }, - defaultButton: defaultButton switch - { - ButtonType.No => noButton, - ButtonType.Cancel => cancelButton, - _ => yesButton - }, - verificationText: showDontAskAgain ? LText.AlertMessages.DontAskAgain : null, - mainIcon: icon); - - TaskDialogButton? buttonClicked = d.ShowDialog(); - bool canceled = buttonClicked == null || buttonClicked == cancelButton; - bool cont = buttonClicked == yesButton; - bool dontAskAgain = d.IsVerificationChecked; - return (canceled, cont, dontAskAgain); - } - - public (bool Cancel, bool DontAskAgain) - AskToContinueYesNoCustomStrings(string message, string title, TaskDialogIcon? icon, bool showDontAskAgain, - string? yes, string? no, ButtonType? defaultButton = null) - { - var yesButton = yes != null ? new TaskDialogButton(yes) : new TaskDialogButton(ButtonType.Yes); - var noButton = no != null ? new TaskDialogButton(no) : new TaskDialogButton(ButtonType.No); - - using var d = new TaskDialog( - title: title, - message: message, - buttons: new[] { yesButton, noButton }, - defaultButton: defaultButton == ButtonType.No ? noButton : yesButton, - verificationText: showDontAskAgain ? LText.AlertMessages.DontAskAgain : null, - mainIcon: icon); - - bool cancel = d.ShowDialog() != yesButton; - bool dontAskAgain = d.IsVerificationChecked; - return (cancel, dontAskAgain); - } - - public void ShowAlert(string message, string title) => MessageBox.Show(message, title, MessageBoxButtons.OK, MessageBoxIcon.Warning); - - #endregion - - public void ChangeGameTabNameShortness(bool useShort, bool refreshFilterBarPositionIfNeeded) - { - for (int i = 0; i < SupportedGameCount; i++) - { - _gameTabsInOrder[i].Text = useShort - ? GetShortLocalizedGameName((GameIndex)i) - : GetLocalizedGameName((GameIndex)i); - } - - // Prevents the couple-pixel-high tab page from extending out too far and becoming visible - var lastGameTabsRect = GamesTabControl.GetTabRect(GamesTabControl.TabCount - 1); - GamesTabControl.Width = lastGameTabsRect.X + lastGameTabsRect.Width + 5; - - if (refreshFilterBarPositionIfNeeded && Config.GameOrganization == GameOrganization.ByTab) - { - PositionFilterBarAfterTabs(); - } - } - - #endregion - - #region Private fields - - private FormWindowState _nominalWindowState; - private Size _nominalWindowSize; - private Point _nominalWindowLocation; - - private float _fMsListDefaultFontSizeInPoints; - private int _rMsListDefaultRowHeight; - - // To order them such that we can just look them up with an index - private readonly TabPage[] _gameTabsInOrder; - private readonly ToolStripButtonCustom[] _filterByGameButtonsInOrder; - private readonly TabPage[] _topRightTabsInOrder; - - private readonly Control[] _filterLabels; - private readonly ToolStripItem[] _filtersToolStripSeparatedItems; - private readonly Control[] _bottomAreaSeparatedItems; - - private readonly Component[][] _hideableFilterControls; - - private enum KeepSel { False, True, TrueNearest } - - private enum ZoomFMsDGVType - { - ZoomIn, - ZoomOut, - ResetZoom, - ZoomTo, - ZoomToHeightOnly - } - - // Set these beforehand and don't set autosize on any column! Or else it explodes everything because - // FMsDGV tries to refresh when it shouldn't and all kinds of crap. Phew. - private const int _ratingImageColumnWidth = 73; - private const int _finishedColumnWidth = 91; - - #region Bitmaps - - // We need to grab these images every time a cell is shown on the DataGridView, and pulling them from - // Resources every time is enormously expensive, causing laggy scrolling and just generally wasting good - // cycles. So we copy them only once to these local bitmaps, and voila, instant scrolling performance. - private readonly Bitmap?[] GameIcons = new Bitmap?[SupportedGameCount]; - - private Bitmap? BlankIcon; - private Bitmap? CheckIcon; - private Bitmap? RedQuestionMarkIcon; - - private Bitmap[]? StarIcons; - private Bitmap[]? FinishedOnIcons; - private Bitmap? FinishedOnUnknownIcon; - - #endregion - - private DataGridViewImageColumn? RatingImageColumn; - - public bool EventsDisabled { get; set; } - public bool KeyPressesDisabled { get; set; } - - // Needed for Rating column swap to prevent a possible exception when CellValueNeeded is called in the - // middle of the operation - private bool _cellValueNeededDisabled; - - private TransparentPanel? ViewBlockingPanel; - private bool _viewBlocked; - - #endregion - - #region Message handling - - protected override void WndProc(ref Message m) - { - // A second instance has been started and told us to show ourselves, so do it here (nicer UX). - // This has to be in WndProc, not PreFilterMessage(). Shrug. - if (m.Msg == InteropMisc.WM_SHOWFIRSTINSTANCE) - { - if (WindowState == FormWindowState.Minimized) WindowState = _nominalWindowState; - Activate(); - } - base.WndProc(ref m); - } - - public bool PreFilterMessage(ref Message m) - { - // So I don't forget what the return values do - const bool BlockMessage = true; - const bool PassMessageOn = false; - - static bool TryGetHWndFromMousePos(Message msg, out IntPtr result) - { - Point pos = new Point(msg.LParam.ToInt32() & 0xffff, msg.LParam.ToInt32() >> 16); - result = InteropMisc.WindowFromPoint(pos); - return result != IntPtr.Zero && Control.FromHandle(result) != null; - } - - // Note: CanFocus will be false if there are modal windows open - - // This allows controls to be scrolled with the mousewheel when the mouse is over them, without - // needing to actually be focused. Vital for a good user experience. - #region Mouse - if (m.Msg == InteropMisc.WM_MOUSEWHEEL) - { - // IMPORTANT (PreFilterMessage): - // Do this check inside each if block rather than above, because the message may not - // be a mousemove message, and in that case we'd be trying to get a window point from a random - // value, and that causes the min,max,close button flickering. - if (!TryGetHWndFromMousePos(m, out IntPtr hWnd)) return PassMessageOn; - - if (_viewBlocked || CursorOutsideAddTagsDropDownArea()) return BlockMessage; - - int wParam = (int)m.WParam; - int delta = wParam >> 16; - if (CanFocus && CursorOverControl(FilterBarFLP) && !CursorOverControl(FMsDGV)) - { - // Allow the filter bar to be mousewheel-scrolled with the buttons properly appearing - // and disappearing as appropriate - if (delta != 0) - { - int direction = delta > 0 ? InteropMisc.SB_LINELEFT : InteropMisc.SB_LINERIGHT; - int origSmallChange = FilterBarFLP.HorizontalScroll.SmallChange; - - FilterBarFLP.HorizontalScroll.SmallChange = 45; - - InteropMisc.SendMessage(FilterBarFLP.Handle, InteropMisc.WM_SCROLL, (IntPtr)direction, IntPtr.Zero); - - FilterBarFLP.HorizontalScroll.SmallChange = origSmallChange; - } - } - else if (CanFocus && CursorOverControl(FMsDGV) && (wParam & 0xFFFF) == InteropMisc.MK_CONTROL) - { - if (delta != 0) ZoomFMsDGV(delta > 0 ? ZoomFMsDGVType.ZoomIn : ZoomFMsDGVType.ZoomOut); - } - else - { - // Stupid hack to fix "send mousewheel to underlying control and block further messages" - // functionality still not being fully reliable. We need to focus the parent control sometimes - // inexplicably. Sure. Whole point is to avoid having to do that, but sure. - if (CursorOverControl(TopSplitContainer.Panel2)) - { - TopSplitContainer.Panel2.Focus(); - } - else if (CursorOverControl(MainSplitContainer.Panel2)) - { - MainSplitContainer.Panel2.Focus(); - } - InteropMisc.SendMessage(hWnd, m.Msg, m.WParam, m.LParam); - } - return BlockMessage; - } - else if (m.Msg == InteropMisc.WM_MOUSEHWHEEL) - { - if (!TryGetHWndFromMousePos(m, out _)) return PassMessageOn; - - if (_viewBlocked) return BlockMessage; - - if (CanFocus && CursorOverControl(FMsDGV)) - { - int delta = (int)m.WParam >> 16; - if (delta != 0) - { - int offset = FMsDGV.HorizontalScrollingOffset; - offset = delta < 0 ? (offset - 15).ClampToZero() : offset + 15; - FMsDGV.HorizontalScrollingOffset = offset; - return BlockMessage; - } - } - } - // Just handle the NC* messages and presto, we don't even need the mouse hook anymore! - // NC = Non-Client, ie. the mouse was in a non-client area of the control - else if (m.Msg == InteropMisc.WM_MOUSEMOVE || m.Msg == InteropMisc.WM_NCMOUSEMOVE) - { - if (!CanFocus) return PassMessageOn; - - if (CursorOutsideAddTagsDropDownArea() || _viewBlocked) return BlockMessage; + if (CursorOutsideAddTagsDropDownArea() || _viewBlocked) return BlockMessage; ShowReadmeControls(CursorOverReadmeArea()); } @@ -631,12 +384,6 @@ bool AnyControlFocusedInTabPage(TabPage tabPage) => #endregion - private static void HideFocusRectangle(Control control) => InteropMisc.SendMessage( - control.Handle, - InteropMisc.WM_CHANGEUISTATE, - new IntPtr(InteropMisc.SetControlFocusToHidden), - new IntPtr(0)); - #region Init / load / show // InitializeComponent() (and stuff that doesn't do anything) only - for everything else use the init @@ -655,7 +402,7 @@ public MainForm() InitComponentManual(); #endif - HideFocusRectangle(MainMenuButton); + MainMenuButton.HideFocusRectangle(); #if DEBUG || (Release_Testing && !RT_StartupOnly) #region Init debug-only controls @@ -1251,6 +998,10 @@ private void MainForm_FormClosing(object sender, FormClosingEventArgs e) #endregion + #region Localize + + public void Localize() => Localize(startup: false); + private void Localize(bool startup) { // Certain controls' text depends on FM state. Because this could be run after startup, we need to @@ -1523,41 +1274,143 @@ private void Localize(bool startup) ChooseReadmeLLPanel.ResumePanelLayout(); } - // We can't do this while the layout is suspended, because then it won't have the right dimensions - // for centering - ViewHTMLReadmeLLButton.Center(MainSplitContainer.Panel2); + // We can't do this while the layout is suspended, because then it won't have the right dimensions + // for centering + ViewHTMLReadmeLLButton.Center(MainSplitContainer.Panel2); + } + + // To refresh the FM size column strings to localized + // We don't need to refresh on startup because we already will later + if (!startup) RefreshFMsListKeepSelection(); + } + + #endregion + + #region Helpers & misc + + #region Invoke + + public object InvokeSync(Delegate method) => Invoke(method); + //public object InvokeSync(Delegate method, params object[] args) => Invoke(method, args); + + #endregion + + #region Messageboxes + + public bool AskToContinue(string message, string title, bool noIcon = false) => + MessageBox.Show( + message, + title, + MessageBoxButtons.YesNo, + noIcon ? MessageBoxIcon.None : MessageBoxIcon.Warning) == DialogResult.Yes; + + public (bool Cancel, bool Continue, bool DontAskAgain) + AskToContinueWithCancelCustomStrings(string message, string title, TaskDialogIcon? icon, bool showDontAskAgain, + string yes, string no, string cancel, ButtonType? defaultButton = null) + { + var yesButton = new TaskDialogButton(yes); + var noButton = new TaskDialogButton(no); + var cancelButton = new TaskDialogButton(cancel); + + using var d = new TaskDialog( + title: title, + message: message, + buttons: new[] { yesButton, noButton, cancelButton }, + defaultButton: defaultButton switch + { + ButtonType.No => noButton, + ButtonType.Cancel => cancelButton, + _ => yesButton + }, + verificationText: showDontAskAgain ? LText.AlertMessages.DontAskAgain : null, + mainIcon: icon); + + TaskDialogButton? buttonClicked = d.ShowDialog(); + bool canceled = buttonClicked == null || buttonClicked == cancelButton; + bool cont = buttonClicked == yesButton; + bool dontAskAgain = d.IsVerificationChecked; + return (canceled, cont, dontAskAgain); + } + + public (bool Cancel, bool DontAskAgain) + AskToContinueYesNoCustomStrings(string message, string title, TaskDialogIcon? icon, bool showDontAskAgain, + string? yes, string? no, ButtonType? defaultButton = null) + { + var yesButton = yes != null ? new TaskDialogButton(yes) : new TaskDialogButton(ButtonType.Yes); + var noButton = no != null ? new TaskDialogButton(no) : new TaskDialogButton(ButtonType.No); + + using var d = new TaskDialog( + title: title, + message: message, + buttons: new[] { yesButton, noButton }, + defaultButton: defaultButton == ButtonType.No ? noButton : yesButton, + verificationText: showDontAskAgain ? LText.AlertMessages.DontAskAgain : null, + mainIcon: icon); + + bool cancel = d.ShowDialog() != yesButton; + bool dontAskAgain = d.IsVerificationChecked; + return (cancel, dontAskAgain); + } + + public void ShowAlert(string message, string title) => MessageBox.Show(message, title, MessageBoxButtons.OK, MessageBoxIcon.Warning); + + #endregion + + #region Show menu + + private enum MenuPos { LeftUp, LeftDown, TopLeft, TopRight, RightUp, RightDown, BottomLeft, BottomRight } + + private static void ShowMenu(ContextMenuStrip menu, Control control, MenuPos pos, + int xOffset = 0, int yOffset = 0, bool unstickMenu = false) + { + int x = pos == MenuPos.LeftUp || pos == MenuPos.LeftDown || pos == MenuPos.TopRight || pos == MenuPos.BottomRight + ? 0 + : control.Width; + + int y = pos == MenuPos.LeftDown || pos == MenuPos.TopLeft || pos == MenuPos.TopRight || pos == MenuPos.RightDown + ? 0 + : control.Height; + + var direction = + pos == MenuPos.LeftUp || pos == MenuPos.TopLeft ? ToolStripDropDownDirection.AboveLeft : + pos == MenuPos.RightUp || pos == MenuPos.TopRight ? ToolStripDropDownDirection.AboveRight : + pos == MenuPos.LeftDown || pos == MenuPos.BottomLeft ? ToolStripDropDownDirection.BelowLeft : + ToolStripDropDownDirection.BelowRight; + + if (unstickMenu) + { + // If menu is stuck to a submenu or something, we need to show and hide it once to get it unstuck, + // then carry on with the final show below + menu.Show(); + menu.Hide(); } - // To refresh the FM size column strings to localized - // We don't need to refresh on startup because we already will later - if (!startup) RefreshFMsListKeepSelection(); + menu.Show(control, new Point(x + xOffset, y + yOffset), direction); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int TopBarXZero() => MainMenuButton.Left + MainMenuButton.Width + 8; + #endregion - // Separate so we can call it from _Load on startup (because it needs the form to be loaded to layout the - // controls properly) but keep the rest of the work before load - private void ChangeFilterControlsForGameType() + public void Block(bool block) { - if (Config.GameOrganization == GameOrganization.OneList) + if (ViewBlockingPanel == null) { - GamesTabControl.Hide(); - // Don't inline this var - it stores the X value to persist it through a change - int plusWidth = FilterBarFLP.Location.X - TopBarXZero(); - FilterBarFLP.Location = new Point(TopBarXZero(), FilterBarFLP.Location.Y); - FilterBarFLP.Width += plusWidth; - FilterGameButtonsToolStrip.Show(); + ViewBlockingPanel = new TransparentPanel { Visible = false }; + Controls.Add(ViewBlockingPanel); + ViewBlockingPanel.Dock = DockStyle.Fill; } - else // ByTab - { - PositionFilterBarAfterTabs(); - FilterGameButtonsToolStrip.Hide(); - GamesTabControl.Show(); + try + { + // Doesn't help the RichTextBox, it happily flickers like it always does. Oh well. + this.SuspendDrawing(); + _viewBlocked = block; + ViewBlockingPanel.Visible = block; + ViewBlockingPanel.BringToFront(); + } + finally + { + this.ResumeDrawing(); } - - SetFilterBarScrollButtons(); } private void UpdateConfig() @@ -1661,2408 +1514,2620 @@ private bool CursorOverControl(Control control, bool fullArea = false) #endregion - #region FMsDGV-related + #endregion - public void SetRowCount(int count) => FMsDGV.RowCount = count; + #region Main menu - private void ZoomFMsDGV(ZoomFMsDGVType type, float? zoomFontSize = null) + private void MainMenuButton_Click(object sender, EventArgs e) { - // No goal escapes me, mate + MainLLMenu.Construct(this, components); + ShowMenu(MainLLMenu.Menu, MainMenuButton, MenuPos.BottomRight, xOffset: -2, yOffset: 2); + } - SelectedFM? selFM = FMsDGV.RowSelected() ? FMsDGV.GetSelectedFMPosInfo() : null; + [SuppressMessage("ReSharper", "MemberCanBeMadeStatic.Global")] + internal void MainMenu_GameVersionsMenuItem_Click(object sender, EventArgs e) + { + using var f = new GameVersionsForm(); + f.ShowDialog(); + } - Font f = FMsDGV.DefaultCellStyle.Font; + private void MainMenuButton_Enter(object sender, EventArgs e) => MainMenuButton.HideFocusRectangle(); - // Set zoom level - float fontSize = - type == ZoomFMsDGVType.ZoomIn ? f.SizeInPoints + 1.0f : - type == ZoomFMsDGVType.ZoomOut ? f.SizeInPoints - 1.0f : - type == ZoomFMsDGVType.ZoomTo && zoomFontSize != null ? (float)zoomFontSize : - type == ZoomFMsDGVType.ZoomToHeightOnly && zoomFontSize != null ? (float)zoomFontSize : - _fMsListDefaultFontSizeInPoints; + #endregion - // Clamp zoom level - if (fontSize < Math.Round(1.00f, 2)) fontSize = 1.00f; - if (fontSize > Math.Round(41.25f, 2)) fontSize = 41.25f; - fontSize = (float)Math.Round(fontSize, 2); + #region Filter bar - // Set new font size - Font newF = new Font(f.FontFamily, fontSize, f.Style, f.Unit, f.GdiCharSet, f.GdiVerticalFont); + public void ClearUIAndCurrentInternalFilter() + { + using (new DisableEvents(this)) + { + FilterBarFLP.SuspendDrawing(); + try + { + bool oneList = Config.GameOrganization == GameOrganization.OneList; + if (oneList) + { + for (int i = 0; i < SupportedGameCount; i++) + { + _filterByGameButtonsInOrder[i].Checked = false; + } + } + FilterTitleTextBox.Text = ""; + FilterAuthorTextBox.Text = ""; - // Set row height based on font plus some padding - int rowHeight = type == ZoomFMsDGVType.ResetZoom ? _rMsListDefaultRowHeight : newF.Height + 9; + FilterByReleaseDateButton.Checked = false; + Lazy_ToolStripLabels.Hide(Lazy_ToolStripLabel.FilterByReleaseDate); - // If we're on startup, then the widths will already have been restored (to zoomed size) from the - // config - bool heightOnly = type == ZoomFMsDGVType.ZoomToHeightOnly; + FilterByLastPlayedButton.Checked = false; + Lazy_ToolStripLabels.Hide(Lazy_ToolStripLabel.FilterByLastPlayed); - // Must be done first, else we get wrong values - List widthMul = new List(); - foreach (DataGridViewColumn c in FMsDGV.Columns) - { - Size size = c.HeaderCell.Size; - widthMul.Add((double)size.Width / size.Height); - } + FilterByTagsButton.Checked = false; + FilterByFinishedButton.Checked = false; + FilterByUnfinishedButton.Checked = false; - // Set font on cells - FMsDGV.DefaultCellStyle.Font = newF; + FilterByRatingButton.Checked = false; + Lazy_ToolStripLabels.Hide(Lazy_ToolStripLabel.FilterByRating); - // Set font on headers - FMsDGV.ColumnHeadersDefaultCellStyle.Font = newF; + FilterShowUnsupportedButton.Checked = false; - // Set height on all rows (but it won't take effect yet) - FMsDGV.RowTemplate.Height = rowHeight; + // NOTE: Here is the line where the internal filter is cleared. It does in fact happen! + FMsDGV.Filter.Clear(oneList); + } + finally + { + FilterBarFLP.ResumeDrawing(); + } + } + } - // Save previous selection - int selIndex = FMsDGV.RowSelected() ? FMsDGV.SelectedRows[0].Index : -1; - using (new DisableEvents(this)) + public void ChangeGameOrganization(bool startup = false) + { + if (Config.GameOrganization == GameOrganization.OneList) { - // Force a regeneration of rows (height will take effect here) - int rowCount = FMsDGV.RowCount; - FMsDGV.RowCount = 0; - FMsDGV.RowCount = rowCount; + Config.SelFM.DeepCopyTo(FMsDGV.CurrentSelFM); + } + else // ByTab + { + // In case they don't match + Config.Filter.Games = GameIndexToGame(Config.GameTab); - // Restore previous selection (no events will be fired, due to being in a DisableEvents block) - if (selIndex > -1) + Config.GameTabsState.DeepCopyTo(FMsDGV.GameTabsState); + + FMsDGV.GameTabsState.GetSelectedFM(Config.GameTab).DeepCopyTo(FMsDGV.CurrentSelFM); + FMsDGV.GameTabsState.GetFilter(Config.GameTab).DeepCopyTo(FMsDGV.Filter); + + using (new DisableEvents(this)) { - FMsDGV.Rows[selIndex].Selected = true; - FMsDGV.SelectProperly(); + GamesTabControl.SelectedIndex = (int)Config.GameTab; } + } - // Set column widths (keeping ratio to height) - for (int i = 0; i < FMsDGV.Columns.Count; i++) - { - DataGridViewColumn c = FMsDGV.Columns[i]; + // Do these even if we're not in startup, because we may have changed the game organization mode + for (int i = 0; i < SupportedGameCount; i++) + { + var game = GameIndexToGame((GameIndex)i); + _filterByGameButtonsInOrder[i].Checked = Config.Filter.Games.HasFlagFast(game); + } - // Complicated gobbledegook for handling different options and also special-casing the - // non-resizable columns - bool reset = type == ZoomFMsDGVType.ResetZoom; - if (c != RatingImageColumn && c != FinishedColumn) - { - c.MinimumWidth = reset ? Defaults.MinColumnWidth : rowHeight + 3; - } + if (!startup) ChangeFilterControlsForGameType(); + } - if (heightOnly) - { - if (c == RatingImageColumn || c == FinishedColumn) - { - c.Width = (int)Math.Round(c.HeaderCell.Size.Height * widthMul[i]); - } - } - else - { - if (reset && c == RatingImageColumn) - { - c.Width = _ratingImageColumnWidth; - } - else if (reset && c == FinishedColumn) - { - c.Width = _finishedColumnWidth; - } - else - { - // The ever-present rounding errors creep in here, but meh. I should figure out - // how to not have those - ensure scaling always happens in integral pixel counts - // somehow? - c.Width = reset && Math.Abs(Config.FMsListFontSizeInPoints - _fMsListDefaultFontSizeInPoints) < 0.1 - ? Config.Columns[i].Width - : (int)Math.Ceiling(c.HeaderCell.Size.Height * widthMul[i]); - } - } + #region Game tabs + + public void ChangeGameTabNameShortness(bool useShort, bool refreshFilterBarPositionIfNeeded) + { + for (int i = 0; i < SupportedGameCount; i++) + { + _gameTabsInOrder[i].Text = useShort + ? GetShortLocalizedGameName((GameIndex)i) + : GetLocalizedGameName((GameIndex)i); + } + + // Prevents the couple-pixel-high tab page from extending out too far and becoming visible + var lastGameTabsRect = GamesTabControl.GetTabRect(GamesTabControl.TabCount - 1); + GamesTabControl.Width = lastGameTabsRect.X + lastGameTabsRect.Width + 5; + + if (refreshFilterBarPositionIfNeeded && Config.GameOrganization == GameOrganization.ByTab) + { + PositionFilterBarAfterTabs(); + } + } + + private (SelectedFM GameSelFM, Filter GameFilter) + GetGameSelFMAndFilter(TabPage tabPage) + { + // NULL_TODO: Null so I can assert + SelectedFM? gameSelFM = null; + Filter? gameFilter = null; + for (int i = 0; i < SupportedGameCount; i++) + { + if (_gameTabsInOrder[i] == tabPage) + { + gameSelFM = FMsDGV.GameTabsState.GetSelectedFM((GameIndex)i); + gameFilter = FMsDGV.GameTabsState.GetFilter((GameIndex)i); + break; } } - // Keep selected FM in the center of the list vertically where possible (UX nicety) - if (selIndex > -1 && selFM != null) CenterSelectedFM(); + AssertR(gameSelFM != null, "gameSelFM is null: Selected tab is not being handled"); + AssertR(gameFilter != null, "gameFilter is null: Selected tab is not being handled"); + + return (gameSelFM!, gameFilter!); + } - // And that's how you do it + private void SaveCurrentTabSelectedFM(TabPage tabPage) + { + var (gameSelFM, gameFilter) = GetGameSelFMAndFilter(tabPage); + SelectedFM selFM = FMsDGV.GetSelectedFMPosInfo(); + selFM.DeepCopyTo(gameSelFM); + FMsDGV.Filter.DeepCopyTo(gameFilter); } - private void CenterSelectedFM() + private void GamesTabControl_Deselecting(object sender, TabControlCancelEventArgs e) { - try - { - FMsDGV.FirstDisplayedScrollingRowIndex = - (FMsDGV.SelectedRows[0].Index - (FMsDGV.DisplayedRowCount(true) / 2)) - .Clamp(0, FMsDGV.RowCount - 1); - } - catch - { - // no room is available to display rows - } + if (EventsDisabled) return; + if (GamesTabControl.Visible) SaveCurrentTabSelectedFM(e.TabPage); } - private void SortFMsDGV(Column column, SortOrder sortDirection) + private async void GamesTabControl_SelectedIndexChanged(object sender, EventArgs e) { - FMsDGV.CurrentSortedColumn = column; - FMsDGV.CurrentSortDirection = sortDirection; + if (EventsDisabled) return; - Core.SortFMsViewList(column, sortDirection); + var (gameSelFM, gameFilter) = GetGameSelFMAndFilter(GamesTabControl.SelectedTab); - // Perf: doing it this way is significantly faster than the old method of indiscriminately setting - // all columns to None and then setting the current one back to the CurrentSortDirection glyph again - int intCol = (int)column; - for (int i = 0; i < FMsDGV.Columns.Count; i++) + for (int i = 0; i < SupportedGameCount; i++) { - DataGridViewColumn c = FMsDGV.Columns[i]; - if (i == intCol && c.HeaderCell.SortGlyphDirection != FMsDGV.CurrentSortDirection) - { - c.HeaderCell.SortGlyphDirection = FMsDGV.CurrentSortDirection; - } - else if (i != intCol && c.HeaderCell.SortGlyphDirection != SortOrder.None) - { - c.HeaderCell.SortGlyphDirection = SortOrder.None; - } + _filterByGameButtonsInOrder[i].Checked = gameSelFM == FMsDGV.GameTabsState.GetSelectedFM((GameIndex)i); } + + gameSelFM.DeepCopyTo(FMsDGV.CurrentSelFM); + gameFilter.DeepCopyTo(FMsDGV.Filter); + + SetUIFilterValues(gameFilter); + + await SortAndSetFilter(gameTabSwitch: true); } - /// - /// Pass selectedFM only if you need to store it BEFORE this method runs, like for RefreshFromDisk() - /// - /// - /// - /// - /// - /// - public async Task SortAndSetFilter(SelectedFM? selectedFM = null, bool forceDisplayFM = false, - bool keepSelection = true, bool gameTabSwitch = false) + #endregion + + public Filter GetFilter() => FMsDGV.Filter; + public string GetTitleFilter() => FilterTitleTextBox.Text; + public string GetAuthorFilter() => FilterAuthorTextBox.Text; + + public bool[] GetGameFiltersEnabledStates() { - bool selFMWasPassedIn = selectedFM != null; + bool[] gamesChecked = new bool[SupportedGameCount]; - FanMission? oldSelectedFM = FMsDGV.RowSelected() ? FMsDGV.GetSelectedFM() : null; + for (int i = 0; i < SupportedGameCount; i++) + { + gamesChecked[i] = _filterByGameButtonsInOrder[i].Checked; + } - selectedFM ??= keepSelection && !gameTabSwitch && FMsDGV.RowSelected() - ? FMsDGV.GetSelectedFMPosInfo() - : null; + return gamesChecked; + } - KeepSel keepSel = - selectedFM != null ? KeepSel.TrueNearest : - keepSelection || gameTabSwitch ? KeepSel.True : KeepSel.False; + public bool GetFinishedFilter() => FilterByFinishedButton.Checked; + public bool GetUnfinishedFilter() => FilterByUnfinishedButton.Checked; + public bool GetShowUnsupportedFilter() => FilterShowUnsupportedButton.Checked; + public bool GetShowRecentAtTop() => FilterShowRecentAtTopButton.Checked; - // Fix: in RefreshFMsList, CurrentSelFM was being used when coming from no FMs listed to some FMs listed - if (!gameTabSwitch && !selFMWasPassedIn && oldSelectedFM == null) keepSel = KeepSel.False; + public List GetFilterShownIndexList() => FMsDGV.FilterShownIndexList; - if (gameTabSwitch) forceDisplayFM = true; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int TopBarXZero() => MainMenuButton.Left + MainMenuButton.Width + 8; - SortFMsDGV(FMsDGV.CurrentSortedColumn, FMsDGV.CurrentSortDirection); + // Separate so we can call it from _Load on startup (because it needs the form to be loaded to layout the + // controls properly) but keep the rest of the work before load + private void ChangeFilterControlsForGameType() + { + if (Config.GameOrganization == GameOrganization.OneList) + { + GamesTabControl.Hide(); + // Don't inline this var - it stores the X value to persist it through a change + int plusWidth = FilterBarFLP.Location.X - TopBarXZero(); + FilterBarFLP.Location = new Point(TopBarXZero(), FilterBarFLP.Location.Y); + FilterBarFLP.Width += plusWidth; + FilterGameButtonsToolStrip.Show(); + } + else // ByTab + { + PositionFilterBarAfterTabs(); - Core.SetFilter(); - if (RefreshFMsList(selectedFM, keepSelection: keepSel)) + FilterGameButtonsToolStrip.Hide(); + GamesTabControl.Show(); + } + + SetFilterBarScrollButtons(); + } + + private void SetUIFilterValues(Filter filter) + { + using (new DisableEvents(this)) { - // DEBUG: Keep this in for testing this because the whole thing is irrepressibly finicky - //Trace.WriteLine(nameof(keepSelection) + ": " + keepSelection); - //Trace.WriteLine("selectedFM != null: " + (selectedFM != null)); - //Trace.WriteLine("!selectedFM.InstalledName.IsEmpty(): " + (selectedFM != null && !selectedFM.InstalledName.IsEmpty())); - //Trace.WriteLine("selectedFM.InstalledName != FMsDGV.GetSelectedFM().InstalledDir: " + (selectedFM != null && selectedFM.InstalledName != FMsDGV.GetSelectedFM().InstalledDir)); + FilterBarFLP.SuspendDrawing(); + try + { + FilterTitleTextBox.Text = filter.Title; + FilterAuthorTextBox.Text = filter.Author; + FilterShowUnsupportedButton.Checked = filter.ShowUnsupported; - // Optimization in case we land on the same as FM as before, don't reload it - // And whaddaya know, I still ended up having to have this eyes-glazing-over stuff here. - if (forceDisplayFM || - (keepSelection && - selectedFM != null && !selectedFM.InstalledName.IsEmpty() && - selectedFM.InstalledName != FMsDGV.GetSelectedFM().InstalledDir) || - (!keepSelection && - (oldSelectedFM == null || - (FMsDGV.RowSelected() && !oldSelectedFM.Equals(FMsDGV.GetSelectedFM())))) || - // Fix: when resetting release date filter the readme wouldn't load for the selected FM - oldSelectedFM == null) + FilterByTagsButton.Checked = !filter.Tags.IsEmpty(); + + FilterByFinishedButton.Checked = filter.Finished.HasFlagFast(FinishedState.Finished); + FilterByUnfinishedButton.Checked = filter.Finished.HasFlagFast(FinishedState.Unfinished); + + FilterByRatingButton.Checked = !(filter.RatingFrom == -1 && filter.RatingTo == 10); + UpdateRatingLabel(suspendResume: false); + + FilterByReleaseDateButton.Checked = filter.ReleaseDateFrom != null || filter.ReleaseDateTo != null; + UpdateDateLabel(lastPlayed: false, suspendResume: false); + + FilterByLastPlayedButton.Checked = filter.LastPlayedFrom != null || filter.LastPlayedTo != null; + UpdateDateLabel(lastPlayed: true, suspendResume: false); + } + finally { - await DisplaySelectedFM(); + FilterBarFLP.ResumeDrawing(); } } } - #region FMsDGV event handlers - - // Coloring the recent rows here because if we do it in _CellValueNeeded, we get a brief flash of the - // default while-background cell color before it changes. - private void FMsDGV_RowPrePaint(object sender, DataGridViewRowPrePaintEventArgs e) + private void PositionFilterBarAfterTabs() { - if (_cellValueNeededDisabled) return; + int filterBarAfterTabsX; + // In case I decide to allow a variable number of tabs based on which games are defined + if (GamesTabControl.TabCount == 0) + { + filterBarAfterTabsX = TopBarXZero(); + } + else + { + var lastRect = GamesTabControl.GetTabRect(GamesTabControl.TabCount - 1); + filterBarAfterTabsX = TopBarXZero() + lastRect.X + lastRect.Width + 5; + } - if (FMsDGV.FilterShownIndexList.Count == 0) return; + FilterBarFLP.Location = new Point(filterBarAfterTabsX, FilterBarFLP.Location.Y); + SetFilterBarWidth(); + } - var fm = FMsDGV.GetFMFromIndex(e.RowIndex); + private void SetFilterBarWidth() => FilterBarFLP.Width = (RefreshAreaToolStrip.Location.X - 4) - FilterBarFLP.Location.X; - FMsDGV.Rows[e.RowIndex].DefaultCellStyle.BackColor = fm.MarkedRecent ? Color.LightGoldenrodYellow : SystemColors.Window; - } + #region Filter bar controls - private void FMsDGV_CellValueNeeded_Initial(object sender, DataGridViewCellValueEventArgs e) + // A ton of things in one event handler to cut down on async/awaits + private async void FilterWindowOpenButtons_Click(object sender, EventArgs e) { - if (_cellValueNeededDisabled) return; + if (sender == FilterByReleaseDateButton || sender == FilterByLastPlayedButton) + { + var button = (ToolStripButtonCustom)sender; - // Lazy-load these in an attempt to save some kind of startup time - // @LAZYLOAD: Try lazy-loading these at a more granular level - // The arrays are obstacles to lazy-loading, but see if we still get good scrolling perf when we look - // them up and load the individual images as needed, rather than all at once here + bool lastPlayed = button == FilterByLastPlayedButton; + DateTime? fromDate = lastPlayed ? FMsDGV.Filter.LastPlayedFrom : FMsDGV.Filter.ReleaseDateFrom; + DateTime? toDate = lastPlayed ? FMsDGV.Filter.LastPlayedTo : FMsDGV.Filter.ReleaseDateTo; + string title = lastPlayed ? LText.DateFilterBox.LastPlayedTitleText : LText.DateFilterBox.ReleaseDateTitleText; - // @GENGAMES (Game icons for FMs list): Begin - // We would prefer to put these in an array, but see Images class for why we can't really do that - GameIcons[(int)Thief1] = Images.Thief1_21; - GameIcons[(int)Thief2] = Images.Thief2_21; - GameIcons[(int)Thief3] = Images.Thief3_21; - GameIcons[(int)SS2] = Images.Shock2_21; - // @GENGAMES (Game icons for FMs list): End + using (var f = new FilterDateForm(title, fromDate, toDate)) + { + f.Location = FilterBarFLP.PointToScreen(new Point( + FilterIconButtonsToolStrip.Location.X + button.Bounds.X, + FilterIconButtonsToolStrip.Location.Y + button.Bounds.Y + button.Height)); - BlankIcon = new Bitmap(1, 1, PixelFormat.Format32bppPArgb); - CheckIcon = Resources.CheckCircle; - RedQuestionMarkIcon = Resources.QuestionMarkCircleRed; - // @LAZYLOAD: Have these be wrapper objects so we can put them in the list without them loading - // Then grab the internal object down below when we go to display them - StarIcons = Images.GetRatingImages(); + if (f.ShowDialog() != DialogResult.OK) return; - FinishedOnIcons = Images.GetFinishedOnImages(BlankIcon); - FinishedOnUnknownIcon = Images.FinishedOnUnknown; + FMsDGV.Filter.SetDateFromAndTo(lastPlayed, f.DateFrom, f.DateTo); + + button.Checked = f.DateFrom != null || f.DateTo != null; + } + + UpdateDateLabel(lastPlayed); + } + else if (sender == FilterByTagsButton) + { + using var tf = new FilterTagsForm(GlobalTags, FMsDGV.Filter.Tags); + if (tf.ShowDialog() != DialogResult.OK) return; + + tf.TagsFilter.DeepCopyTo(FMsDGV.Filter.Tags); + FilterByTagsButton.Checked = !FMsDGV.Filter.Tags.IsEmpty(); + } + else if (sender == FilterByRatingButton) + { + bool outOfFive = Config.RatingDisplayStyle == RatingDisplayStyle.FMSel; + using (var f = new FilterRatingForm(FMsDGV.Filter.RatingFrom, FMsDGV.Filter.RatingTo, outOfFive)) + { + f.Location = + FilterBarFLP.PointToScreen(new Point( + FilterIconButtonsToolStrip.Location.X + + FilterByRatingButton.Bounds.X, + FilterIconButtonsToolStrip.Location.Y + + FilterByRatingButton.Bounds.Y + + FilterByRatingButton.Height)); + + if (f.ShowDialog() != DialogResult.OK) return; + FMsDGV.Filter.SetRatingFromAndTo(f.RatingFrom, f.RatingTo); + FilterByRatingButton.Checked = + !(FMsDGV.Filter.RatingFrom == -1 && FMsDGV.Filter.RatingTo == 10); + } + + UpdateRatingLabel(); + } - // Prevents having to check the bool again forevermore even after we've already set the images. - // Taking an extremely minor technique from a data-oriented design talk, heck yeah! - FMsDGV.CellValueNeeded -= FMsDGV_CellValueNeeded_Initial; - FMsDGV.CellValueNeeded += FMsDGV_CellValueNeeded; - FMsDGV_CellValueNeeded(sender, e); + await SortAndSetFilter(); } - private void FMsDGV_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e) + private void UpdateDateLabel(bool lastPlayed, bool suspendResume = true) { - if (_cellValueNeededDisabled) return; - - if (FMsDGV.FilterShownIndexList.Count == 0) return; - - var fm = FMsDGV.GetFMFromIndex(e.RowIndex); + var button = lastPlayed ? FilterByLastPlayedButton : FilterByReleaseDateButton; + DateTime? fromDate = lastPlayed ? FMsDGV.Filter.LastPlayedFrom : FMsDGV.Filter.ReleaseDateFrom; + DateTime? toDate = lastPlayed ? FMsDGV.Filter.LastPlayedTo : FMsDGV.Filter.ReleaseDateTo; - // PERF: ~0.14ms per FM for en-US Long Date format - // PERF_TODO: Test with custom - dt.ToString() might be slow? - static string FormatDate(DateTime dt) => Config.DateFormat switch + // Normally you can see the re-layout kind of "sequentially happen", this stops that and makes it + // snappy + if (suspendResume) FilterBarFLP.SuspendDrawing(); + try { - DateFormat.CurrentCultureShort => dt.ToShortDateString(), - DateFormat.CurrentCultureLong => dt.ToLongDateString(), - _ => dt.ToString(Config.DateCustomFormatString) - }; - - static string FormatSize(ulong size) => - size == 0 - ? "" - : size < ByteSize.MB - ? Math.Round(size / 1024f).ToString(CultureInfo.CurrentCulture) + " " + LText.Global.KilobyteShort - : size >= ByteSize.MB && size < ByteSize.GB - ? Math.Round(size / 1024f / 1024f).ToString(CultureInfo.CurrentCulture) + " " + LText.Global.MegabyteShort - : Math.Round(size / 1024f / 1024f / 1024f, 2).ToString(CultureInfo.CurrentCulture) + " " + LText.Global.GigabyteShort; + if (button.Checked) + { + string from = fromDate == null ? "" : fromDate.Value.ToShortDateString(); + string to = toDate == null ? "" : toDate.Value.ToShortDateString(); - switch ((Column)e.ColumnIndex) + Lazy_ToolStripLabels.Show(this, + lastPlayed + ? Lazy_ToolStripLabel.FilterByLastPlayed + : Lazy_ToolStripLabel.FilterByReleaseDate, from + " - " + to); + } + else + { + Lazy_ToolStripLabels.Hide(lastPlayed + ? Lazy_ToolStripLabel.FilterByLastPlayed + : Lazy_ToolStripLabel.FilterByReleaseDate); + } + } + finally { - case Column.Game: - e.Value = - GameIsKnownAndSupported(fm.Game) ? GameIcons[(int)GameToGameIndex(fm.Game)] : - fm.Game == Game.Unsupported ? RedQuestionMarkIcon : - // Can't say null, or else it sets an ugly red-x image - BlankIcon; - break; + if (suspendResume) FilterBarFLP.ResumeDrawing(); + } + } - case Column.Installed: - e.Value = fm.Installed ? CheckIcon : BlankIcon; - break; + #region Filter bar right-hand controls - case Column.Title: - if (Config.EnableArticles && Config.MoveArticlesToEnd) - { - string title = fm.Title; - for (int i = 0; i < Config.Articles.Count; i++) - { - string a = Config.Articles[i]; - if (fm.Title.StartsWithI(a + " ")) - { - // Take the actual article from the name so as to preserve casing - title = fm.Title.Substring(a.Length + 1) + ", " + fm.Title.Substring(0, a.Length); - break; - } - } - e.Value = title; - } - else - { - e.Value = fm.Title; - } - break; + internal void FMsListZoomInButton_Click(object sender, EventArgs e) => ZoomFMsDGV(ZoomFMsDGVType.ZoomIn); - case Column.Archive: - e.Value = fm.Archive; - break; + internal void FMsListZoomOutButton_Click(object sender, EventArgs e) => ZoomFMsDGV(ZoomFMsDGVType.ZoomOut); - case Column.Author: - e.Value = fm.Author; - break; + internal void FMsListResetZoomButton_Click(object sender, EventArgs e) => ZoomFMsDGV(ZoomFMsDGVType.ResetZoom); - case Column.Size: - // This conversion takes like 1ms over the entire 1545 set, so no problem - e.Value = FormatSize(fm.SizeBytes); - break; + // A ton of things in one event handler to cut down on async/awaits + private async void SortAndSetFiltersButtons_Click(object sender, EventArgs e) + { + if (sender == RefreshFromDiskButton) + { + await Core.RefreshFMsListFromDisk(); + } + else + { + bool senderIsTextBox = sender == FilterTitleTextBox || + sender == FilterAuthorTextBox; + bool senderIsGameButton = _filterByGameButtonsInOrder.Contains(sender); - case Column.Rating: - if (Config.RatingDisplayStyle == RatingDisplayStyle.NewDarkLoader) - { - e.Value = fm.Rating == -1 ? "" : fm.Rating.ToString(); - } - else - { - if (Config.RatingUseStars) - { - e.Value = fm.Rating == -1 ? BlankIcon : StarIcons![fm.Rating]; - } - else - { - e.Value = fm.Rating == -1 ? "" : (fm.Rating / 2.0).ToString(CultureInfo.CurrentCulture); - } - } - break; + if ((senderIsTextBox || senderIsGameButton) && EventsDisabled) + { + return; + } - case Column.Finished: - e.Value = fm.FinishedOnUnknown ? FinishedOnUnknownIcon : FinishedOnIcons![fm.FinishedOn]; - break; + if (sender == ClearFiltersButton) ClearUIAndCurrentInternalFilter(); - case Column.ReleaseDate: - e.Value = fm.ReleaseDate.DateTime != null ? FormatDate((DateTime)fm.ReleaseDate.DateTime) : ""; - break; + // Don't keep selection for these ones, cause you want to end up on the FM you typed as soon as possible + bool keepSel = sender != FilterShowRecentAtTopButton && !senderIsTextBox; + await SortAndSetFilter(keepSelection: keepSel); + } + } - case Column.LastPlayed: - e.Value = fm.LastPlayed.DateTime != null ? FormatDate((DateTime)fm.LastPlayed.DateTime) : ""; - break; + #endregion - case Column.DateAdded: - // IMPORTANT (Convert to local time): We don't do it earlier for startup perf reasons. - e.Value = fm.DateAdded != null ? FormatDate(((DateTime)fm.DateAdded).ToLocalTime()) : ""; - break; + #region Filter bar scroll RepeatButtons - case Column.DisabledMods: - e.Value = fm.DisableAllMods ? LText.FMsList.AllModsDisabledMessage : fm.DisabledMods; - break; + // TODO: Make this use a timer or something? + // The thread is fine but the speed accumulates if you click a bunch. Not a big deal I guess but hey. + // Single-threading it would also allow it to be packed away in a custom control. + private bool _repeatButtonRunning; - case Column.Comment: - e.Value = fm.CommentSingleLine; - break; - } + private void FilterBarScrollButtons_Click(object sender, EventArgs e) + { + if (_repeatButtonRunning) return; + int direction = sender == FilterBarScrollLeftButton ? InteropMisc.SB_LINELEFT : InteropMisc.SB_LINERIGHT; + InteropMisc.SendMessage(FilterBarFLP.Handle, InteropMisc.WM_SCROLL, (IntPtr)direction, IntPtr.Zero); } - private async void FMsDGV_ColumnHeaderMouseClick(object sender, DataGridViewCellMouseEventArgs e) + private void FilterBarScrollButtons_MouseDown(object sender, MouseEventArgs e) { if (e.Button != MouseButtons.Left) return; + RunRepeatButton(sender == FilterBarScrollLeftButton ? InteropMisc.SB_LINELEFT : InteropMisc.SB_LINERIGHT); + } - SelectedFM? selFM = FMsDGV.RowSelected() ? FMsDGV.GetSelectedFMPosInfo() : null; - - var newSortDirection = - e.ColumnIndex == (int)FMsDGV.CurrentSortedColumn && FMsDGV.CurrentSortDirection == SortOrder.Ascending - ? SortOrder.Descending - : SortOrder.Ascending; - - SortFMsDGV((Column)e.ColumnIndex, newSortDirection); - - Core.SetFilter(); - if (RefreshFMsList(selFM, keepSelection: KeepSel.TrueNearest, fromColumnClick: true)) + private void RunRepeatButton(int direction) + { + if (_repeatButtonRunning) return; + _repeatButtonRunning = true; + Task.Run(() => { - if (selFM != null && FMsDGV.RowSelected() && - selFM.InstalledName != FMsDGV.GetSelectedFM().InstalledDir) + while (_repeatButtonRunning) { - await DisplaySelectedFM(); + Invoke(new Action(() => InteropMisc.SendMessage(FilterBarFLP.Handle, InteropMisc.WM_SCROLL, (IntPtr)direction, IntPtr.Zero))); + Thread.Sleep(150); } - } + }); } - private void FMsDGV_MouseDown(object sender, MouseEventArgs e) - { - if (e.Button != MouseButtons.Right) return; + private void FilterBarScrollButtons_EnabledChanged(object sender, EventArgs e) => _repeatButtonRunning = false; - var ht = FMsDGV.HitTest(e.X, e.Y); + private void FilterBarScrollLeftButton_MouseUp(object sender, MouseEventArgs e) => _repeatButtonRunning = false; - #region Right-click menu + private void FilterBarScrollButtons_VisibleChanged(object sender, EventArgs e) + { + var senderButton = (Button)sender; + var otherButton = senderButton == FilterBarScrollLeftButton ? FilterBarScrollRightButton : FilterBarScrollLeftButton; + if (!senderButton.Visible && otherButton.Visible) _repeatButtonRunning = false; + } - if (ht.Type == DataGridViewHitTestType.ColumnHeader || ht.Type == DataGridViewHitTestType.None) - { - FMsDGV.SetContextMenuToColumnHeader(); - } - else if (ht.Type == DataGridViewHitTestType.Cell && ht.ColumnIndex > -1 && ht.RowIndex > -1) - { - FMsDGV.SetContextMenuToFM(); - FMsDGV.Rows[ht.RowIndex].Selected = true; - // We don't need to call SelectProperly() here because the mousedown will select it properly - } - else - { - FMsDGV.SetContextMenuToNone(); - } + private void FilterBarFLP_SizeChanged(object sender, EventArgs e) => SetFilterBarScrollButtons(); - #endregion - } + private void FilterBarFLP_Scroll(object sender, ScrollEventArgs e) => SetFilterBarScrollButtons(); - // Okay, boys and girls. We get the glitched last row on keyboard-scroll if we don't do this idiot thing. - // No, we can't do any of the normal things you'd think would work in RefreshFMsList() itself. I tried. - // Everything is stupid. Whatever. - private bool _fmsListOneTimeHackRefreshDone; - private async void FMsDGV_SelectionChanged(object sender, EventArgs e) + // PERF_TODO: This is still called too many times on startup. + // Even though it has checks to prevent any real work from being done if not needed, I should still take + // a look at this and see if I can't make it be called only once max on startup. + // TODO: Something about the Construct() calls in this method causes the anchoring issue (when we lazy-load). + // If we just construct once at the top, it works fine. But we can't do that because then it would always + // load right away, defeating the purpose of lazy loading. Look into this. If we can solve it, that's a + // bit more time shaved off of startup. + // 2019-07-17: Lazy loading these is disabled for the moment. + private void SetFilterBarScrollButtons() { - if (EventsDisabled) return; + // Don't run this a zillion gatrillion times during init + if (EventsDisabled || !Visible) return; - if (!FMsDGV.RowSelected()) + void ShowLeft() + { + FilterBarScrollLeftButton.Location = new Point(FilterBarFLP.Location.X, FilterBarFLP.Location.Y + 1); + FilterBarScrollLeftButton.Show(); + } + + void ShowRight() { - ClearShownData(); + // Don't set it based on the filter bar width and location, otherwise it gets it slightly wrong + // the first time + FilterBarScrollRightButton.Location = new Point( + RefreshAreaToolStrip.Location.X - FilterBarScrollRightButton.Width - 4, + FilterBarFLP.Location.Y + 1); + FilterBarScrollRightButton.Show(); } - else - { - FMsDGV.SelectProperly(); - if (!_fmsListOneTimeHackRefreshDone) + var hs = FilterBarFLP.HorizontalScroll; + if (!hs.Visible) + { + if (FilterBarScrollLeftButton.Visible || FilterBarScrollRightButton.Visible) { - RefreshFMsList(FMsDGV.GetSelectedFMPosInfo(), startup: false, KeepSel.TrueNearest); - _fmsListOneTimeHackRefreshDone = true; + FilterBarScrollLeftButton.Hide(); + FilterBarScrollRightButton.Hide(); } - - await DisplaySelectedFM(); } - } - - #region Crappy hack for basic go-to-first-typed-letter - - // TODO: Make this into a working, polished, documented feature - - private void FMsDGV_KeyPress(object sender, KeyPressEventArgs e) - { - if (e.KeyChar.IsAsciiAlpha()) + // Keep order: Show, Hide + // Otherwise there's a small hiccup with the buttons + else if (hs.Value == 0) { - int rowIndex = -1; - - for (int i = 0; i < FMsDGV.RowCount; i++) + ShowRight(); + FilterBarScrollLeftButton.Hide(); + using (new DisableEvents(this)) { - if (FMsDGV.Rows[i].Cells[(int)Column.Title].Value.ToString().StartsWithI(e.KeyChar.ToString())) + // Disgusting! But necessary to patch up heisenbuggy behavior with this crap. This is really + // bad in general anyway, but how else am I supposed to have show-and-hide scroll buttons with + // WinForms? Argh! + for (int i = 0; i < 8; i++) { - rowIndex = i; - break; + InteropMisc.SendMessage(FilterBarFLP.Handle, InteropMisc.WM_SCROLL, (IntPtr)InteropMisc.SB_LINELEFT, IntPtr.Zero); } } - - if (rowIndex > -1) + } + else if (hs.Value >= (hs.Maximum + 1) - hs.LargeChange) + { + ShowLeft(); + FilterBarScrollRightButton.Hide(); + using (new DisableEvents(this)) { - FMsDGV.Rows[rowIndex].Selected = true; - FMsDGV.SelectProperly(); - FMsDGV.FirstDisplayedScrollingRowIndex = FMsDGV.SelectedRows[0].Index; + // Ditto the above + for (int i = 0; i < 8; i++) + { + InteropMisc.SendMessage(FilterBarFLP.Handle, InteropMisc.WM_SCROLL, (IntPtr)InteropMisc.SB_LINERIGHT, IntPtr.Zero); + } } } + else + { + ShowLeft(); + ShowRight(); + } } #endregion - private void FMsDGV_KeyDown(object sender, KeyEventArgs e) - { - // This is in here because it doesn't really work right if we put it in MainForm_KeyDown anyway - if (e.KeyCode == Keys.Apps) - { - FMsDGV.SetContextMenuToFM(); - } - } + #endregion - private async void FMsDGV_CellDoubleClick(object sender, DataGridViewCellEventArgs e) + private void ResetLayoutButton_Click(object sender, EventArgs e) { - FanMission fm; - if (e.RowIndex < 0 || !FMsDGV.RowSelected() || !GameIsKnownAndSupported((fm = FMsDGV.GetSelectedFM()).Game)) - { - return; - } - - await FMInstallAndPlay.InstallIfNeededAndPlay(fm, askConfIfRequired: true); + MainSplitContainer.ResetSplitterPercent(); + TopSplitContainer.ResetSplitterPercent(); + if (FilterBarScrollRightButton.Visible) SetFilterBarScrollButtons(); } #endregion - #endregion - - #region Bottom bar - - #region Left side + #region Filter controls visibility menu - #region Install/Play buttons + private void FilterControlsShowHideButton_Click(object sender, EventArgs e) + { + FilterControlsLLMenu.Construct(this, components); + ShowMenu(FilterControlsLLMenu.Menu, + FilterIconButtonsToolStrip, + MenuPos.RightDown, + -FilterControlsShowHideButton.Width, + FilterIconButtonsToolStrip.Height); + } - internal async void InstallUninstall_Play_Buttons_Click(object sender, EventArgs e) + internal void FilterControlsMenuItems_Click(object sender, EventArgs e) { - if (sender.EqualsIfNotNull(InstallUninstallFMLLButton.Button)) + var s = (ToolStripMenuItem)sender; + + try { - await FMInstallAndPlay.InstallOrUninstall(FMsDGV.GetSelectedFM()); + FilterBarFLP.SuspendDrawing(); + + var filterItems = _hideableFilterControls[(int)s.Tag]; + for (int i = 0; i < filterItems.Length; i++) + { + switch (filterItems[i]) + { + case Control control: + control.Visible = s.Checked; + break; + case ToolStripItem toolStripItem: + toolStripItem.Visible = s.Checked; + break; + } + } } - else if (sender == PlayFMButton) + finally { - await FMInstallAndPlay.InstallIfNeededAndPlay(FMsDGV.GetSelectedFM()); + FilterBarFLP.ResumeDrawing(); } } - #region Play original game + #endregion - // @GENGAMES (Play original game menu event handlers): Begin - // Because of the T2MP menu item breaking up the middle there, we can't array/index these menu items. - // Just gonna have to leave this part as-is. - private void PlayOriginalGameButton_Click(object sender, EventArgs e) + #region Refresh FMs list + + public void RefreshSelectedFM(bool rowOnly = false) { - PlayOriginalGameLLMenu.Construct(this, components); + FMsDGV.InvalidateRow(FMsDGV.SelectedRows[0].Index); - PlayOriginalGameLLMenu.Thief1MenuItem.Enabled = !Config.GetGameExe(Thief1).IsEmpty(); - PlayOriginalGameLLMenu.Thief2MenuItem.Enabled = !Config.GetGameExe(Thief2).IsEmpty(); - PlayOriginalGameLLMenu.Thief2MPMenuItem.Visible = Config.T2MPDetected; - PlayOriginalGameLLMenu.Thief3MenuItem.Enabled = !Config.GetGameExe(Thief3).IsEmpty(); - PlayOriginalGameLLMenu.SS2MenuItem.Enabled = !Config.GetGameExe(SS2).IsEmpty(); + if (rowOnly) return; - ShowMenu(PlayOriginalGameLLMenu.Menu, PlayOriginalGameButton, MenuPos.TopRight); + UpdateAllFMUIDataExceptReadme(FMsDGV.GetSelectedFM()); } - [SuppressMessage("ReSharper", "MemberCanBeMadeStatic.Global")] - internal void PlayOriginalGameMenuItem_Click(object sender, EventArgs e) + /// + /// Returns false if the list is empty and ClearShownData() has been called, otherwise true + /// + /// + /// + /// + /// + /// + private bool RefreshFMsList(SelectedFM? selectedFM, bool startup = false, KeepSel keepSelection = KeepSel.False, + bool fromColumnClick = false) { - var item = (ToolStripMenuItem)sender; + using (new DisableEvents(this)) + { + // A small but measurable perf increase from this. Also prevents flickering when switching game + // tabs. + if (!startup) + { + FMsDGV.SuspendDrawing(); + // So, I'm sorry, I thought the line directly above this one said to suspend drawing. I just + // thought I saw a suspend drawing command, and since drawing cells constitutes drawing, I + // just assumed you would understand that to suspend drawing means not to draw cells. I must + // be mistaken. No no, please. + _cellValueNeededDisabled = true; + } - GameIndex game = - item == PlayOriginalGameLLMenu.Thief1MenuItem ? Thief1 : - item == PlayOriginalGameLLMenu.Thief2MenuItem || item == PlayOriginalGameLLMenu.Thief2MPMenuItem ? Thief2 : - item == PlayOriginalGameLLMenu.Thief3MenuItem ? Thief3 : - SS2; + // Prevents: + // -a glitched row from being drawn at the end in certain situations + // -the subsequent row count set from being really slow + FMsDGV.Rows.Clear(); - bool playMP = item == PlayOriginalGameLLMenu.Thief2MPMenuItem; + FMsDGV.RowCount = FMsDGV.FilterShownIndexList.Count; - FMInstallAndPlay.PlayOriginalGame(game, playMP); + if (FMsDGV.RowCount == 0) + { + if (!startup) FMsDGV.ResumeDrawing(); + ClearShownData(); + return false; + } + else + { + int row; + if (keepSelection == KeepSel.False) + { + row = 0; + FMsDGV.FirstDisplayedScrollingRowIndex = 0; + } + else + { + SelectedFM selFM = selectedFM ?? FMsDGV.CurrentSelFM; + bool findNearest = keepSelection == KeepSel.TrueNearest && selectedFM != null; + row = FMsDGV.GetIndexFromInstalledName(selFM.InstalledName, findNearest).ClampToZero(); + try + { + if (fromColumnClick) + { + if (FMsDGV.CurrentSortDirection == SortOrder.Ascending) + { + FMsDGV.FirstDisplayedScrollingRowIndex = row.ClampToZero(); + } + else if (FMsDGV.CurrentSortDirection == SortOrder.Descending) + { + FMsDGV.FirstDisplayedScrollingRowIndex = (row - FMsDGV.DisplayedRowCount(true)).ClampToZero(); + } + } + else + { + FMsDGV.FirstDisplayedScrollingRowIndex = (row - selFM.IndexFromTop).ClampToZero(); + } + } + catch + { + // no room is available to display rows + } + } + + // Events will be re-enabled at the end of the enclosing using block + if (keepSelection != KeepSel.False) EventsDisabled = true; + FMsDGV.Rows[row].Selected = true; + FMsDGV.SelectProperly(suspendResume: startup); + + // Resume drawing before loading the readme; that way the list will update instantly even + // if the readme doesn't. The user will see delays in the "right place" (the readme box) + // and understand why it takes a sec. Otherwise, it looks like merely changing tabs brings + // a significant delay, and that's annoying because it doesn't seem like it should happen. + if (!startup) + { + _cellValueNeededDisabled = false; + FMsDGV.ResumeDrawing(); + } + } + } + + return true; + } + + public void RefreshFMsListKeepSelection() + { + if (FMsDGV.RowCount == 0) return; + + int selectedRow = FMsDGV.SelectedRows[0].Index; + + using (new DisableEvents(this)) + { + FMsDGV.Refresh(); + FMsDGV.Rows[selectedRow].Selected = true; + FMsDGV.SelectProperly(); + } } - // @GENGAMES (Play original game menu event handlers): End #endregion - #endregion + #region Top-right area - private async void ScanAllFMsButton_Click(object sender, EventArgs e) + // Hook them all up to one event handler to avoid extraneous async/awaits + private async void FieldScanButtons_Click(object sender, EventArgs e) { - if (FMsViewList.Count == 0) return; - - FMScanner.ScanOptions? scanOptions = null; - bool noneSelected; - using (var f = new ScanAllFMsForm()) + if (sender == EditFMScanForReadmesButton) { - if (f.ShowDialog() != DialogResult.OK) return; - noneSelected = f.NoneSelected; - if (!noneSelected) - { - scanOptions = FMScanner.ScanOptions.FalseDefault( - scanTitle: f.ScanOptions.ScanTitle, - scanAuthor: f.ScanOptions.ScanAuthor, - scanGameType: f.ScanOptions.ScanGameType, - scanCustomResources: f.ScanOptions.ScanCustomResources, - scanSize: f.ScanOptions.ScanSize, - scanReleaseDate: f.ScanOptions.ScanReleaseDate, - scanTags: f.ScanOptions.ScanTags); - } + Ini.WriteFullFMDataIni(); + await DisplaySelectedFM(refreshCache: true); } - - if (noneSelected) + else { - MessageBox.Show(LText.ScanAllFMsBox.NothingWasScanned, LText.AlertMessages.Alert); - return; - } + var scanOptions = + sender == EditFMScanTitleButton ? FMScanner.ScanOptions.FalseDefault(scanTitle: true) : + sender == EditFMScanAuthorButton ? FMScanner.ScanOptions.FalseDefault(scanAuthor: true) : + sender == EditFMScanReleaseDateButton ? FMScanner.ScanOptions.FalseDefault(scanReleaseDate: true) : + //sender == StatsScanCustomResourcesButton + FMScanner.ScanOptions.FalseDefault(scanCustomResources: true); - bool success = await FMScan.ScanFMs(FMsViewList, scanOptions!); - if (success) await SortAndSetFilter(forceDisplayFM: true); + if (await FMScan.ScanFMs(new List { FMsDGV.GetSelectedFM() }, scanOptions, hideBoxIfZip: true)) + { + RefreshSelectedFM(); + } + } } - private void WebSearchButton_Click(object sender, EventArgs e) => Core.OpenWebSearchUrl(FMsDGV.GetSelectedFM().Title); - - #endregion + #region Edit FM tab - #region Right side + private void EditFMAltTitlesArrowButtonClick(object sender, EventArgs e) + { + AltTitlesLLMenu.Construct(components); + FillAltTitlesMenu(FMsDGV.GetSelectedFM().AltTitles); + ShowMenu(AltTitlesLLMenu.Menu, EditFMAltTitlesArrowButton, MenuPos.BottomLeft); + } - private void ImportButton_Click(object sender, EventArgs e) + private void EditFMAltTitlesMenuItems_Click(object sender, EventArgs e) { - ImportFromLLMenu.Construct(this, components); - ShowMenu(ImportFromLLMenu.ImportFromMenu, ImportButton, MenuPos.TopLeft); + EditFMTitleTextBox.Text = ((ToolStripMenuItem)sender).Text; + Ini.WriteFullFMDataIni(); } - [SuppressMessage("ReSharper", "MemberCanBeMadeStatic.Global")] - internal async void ImportMenuItems_Click(object sender, EventArgs e) + private void EditFMTitleTextBox_TextChanged(object sender, EventArgs e) { - ImportType importType = - sender == ImportFromLLMenu.ImportFromDarkLoaderMenuItem - ? ImportType.DarkLoader - : sender == ImportFromLLMenu.ImportFromFMSelMenuItem - ? ImportType.FMSel - : ImportType.NewDarkLoader; + if (EventsDisabled) return; + FMsDGV.GetSelectedFM().Title = EditFMTitleTextBox.Text; + RefreshSelectedFM(rowOnly: true); + } - await Import.ImportFrom(importType); + private void EditFMTitleTextBox_Leave(object sender, EventArgs e) + { + if (EventsDisabled) return; + Ini.WriteFullFMDataIni(); } - private async void SettingsButton_Click(object sender, EventArgs e) + private void EditFMAuthorTextBox_TextChanged(object sender, EventArgs e) { - var ret = Core.OpenSettings(); - if (ret.Canceled) return; + if (EventsDisabled) return; + FMsDGV.GetSelectedFM().Author = EditFMAuthorTextBox.Text; + RefreshSelectedFM(rowOnly: true); + } - if (ret.FMsViewListUnscanned?.Count > 0) await FMScan.ScanNewFMs(ret.FMsViewListUnscanned); - // TODO: forceDisplayFM is always true so that this always works, but it could be smarter - // If I store the selected FM up above the Find(), I can make the FM not have to reload if - // it's still selected - if (ret.SortAndSetFilter) await SortAndSetFilter(keepSelection: ret.KeepSel, forceDisplayFM: true); + private void EditFMAuthorTextBox_Leave(object sender, EventArgs e) + { + if (EventsDisabled) return; + Ini.WriteFullFMDataIni(); } - #endregion + private void EditFMReleaseDateCheckBox_CheckedChanged(object sender, EventArgs e) + { + if (EventsDisabled) return; + EditFMReleaseDateDateTimePicker.Visible = EditFMReleaseDateCheckBox.Checked; - #endregion + FMsDGV.GetSelectedFM().ReleaseDate.DateTime = EditFMReleaseDateCheckBox.Checked + ? EditFMReleaseDateDateTimePicker.Value + : (DateTime?)null; - #region Update displayed rating + RefreshSelectedFM(rowOnly: true); + Ini.WriteFullFMDataIni(); + } - public void UpdateRatingDisplayStyle(RatingDisplayStyle style, bool startup) + private void EditFMReleaseDateDateTimePicker_ValueChanged(object sender, EventArgs e) { - UpdateRatingListsAndColumn(style == RatingDisplayStyle.FMSel, startup); - UpdateRatingLabel(); + if (EventsDisabled) return; + FMsDGV.GetSelectedFM().ReleaseDate.DateTime = EditFMReleaseDateDateTimePicker.Value; + RefreshSelectedFM(rowOnly: true); + Ini.WriteFullFMDataIni(); } - private void UpdateRatingListsAndColumn(bool fmSelStyle, bool startup) + private void EditFMLastPlayedCheckBox_CheckedChanged(object sender, EventArgs e) { - #region Update rating lists - - // Just in case, since changing a ComboBox item's text counts as a selected index change maybe? Argh! - using (new DisableEvents(this)) - { - for (int i = 0; i <= 10; i++) - { - string num = (fmSelStyle ? i / 2.0 : i).ToString(CultureInfo.CurrentCulture); - EditFMRatingComboBox.Items[i + 1] = num; - } - } + if (EventsDisabled) return; + EditFMLastPlayedDateTimePicker.Visible = EditFMLastPlayedCheckBox.Checked; - FMsDGV.UpdateRatingList(fmSelStyle); + FMsDGV.GetSelectedFM().LastPlayed.DateTime = EditFMLastPlayedCheckBox.Checked + ? EditFMLastPlayedDateTimePicker.Value + : (DateTime?)null; - #endregion + RefreshSelectedFM(rowOnly: true); + Ini.WriteFullFMDataIni(); + } - #region Update rating column + private void EditFMLastPlayedDateTimePicker_ValueChanged(object sender, EventArgs e) + { + if (EventsDisabled) return; + FMsDGV.GetSelectedFM().LastPlayed.DateTime = EditFMLastPlayedDateTimePicker.Value; + RefreshSelectedFM(rowOnly: true); + Ini.WriteFullFMDataIni(); + } - var newRatingColumn = - Config.RatingDisplayStyle == RatingDisplayStyle.FMSel && Config.RatingUseStars - ? (DataGridViewColumn)RatingImageColumn! - : RatingTextColumn; + private void EditFMDisabledModsTextBox_TextChanged(object sender, EventArgs e) + { + if (EventsDisabled) return; + FMsDGV.GetSelectedFM().DisabledMods = EditFMDisabledModsTextBox.Text; + RefreshSelectedFM(rowOnly: true); + } - if (!startup) - { - var oldRatingColumn = FMsDGV.Columns[(int)Column.Rating]; - newRatingColumn!.Width = newRatingColumn == RatingTextColumn - ? oldRatingColumn.Width - // To set the ratio back to exact on zoom reset - : FMsDGV.RowTemplate.Height == 22 - ? _ratingImageColumnWidth - : (FMsDGV.DefaultCellStyle.Font.Height + 9) * (_ratingImageColumnWidth / 22); - newRatingColumn.Visible = oldRatingColumn.Visible; - newRatingColumn.DisplayIndex = oldRatingColumn.DisplayIndex; - } + private void EditFMDisabledModsTextBox_Leave(object sender, EventArgs e) + { + if (EventsDisabled) return; + Ini.WriteFullFMDataIni(); + } - if (!startup || newRatingColumn != RatingTextColumn) - { - using (new DisableEvents(this)) - { - _cellValueNeededDisabled = true; - try - { - FMsDGV.Columns.RemoveAt((int)Column.Rating); - FMsDGV.Columns.Insert((int)Column.Rating, newRatingColumn!); - } - finally - { - _cellValueNeededDisabled = false; - } - } - if (FMsDGV.CurrentSortedColumn == Column.Rating) - { - FMsDGV.Columns[(int)Column.Rating].HeaderCell.SortGlyphDirection = FMsDGV.CurrentSortDirection; - } - } + private void EditFMDisableAllModsCheckBox_CheckedChanged(object sender, EventArgs e) + { + if (EventsDisabled) return; + EditFMDisabledModsTextBox.Enabled = !EditFMDisableAllModsCheckBox.Checked; - if (!startup) - { - FMsDGV.SetColumnData(FMsDGV.GetColumnData()); - RefreshFMsListKeepSelection(); - } + FMsDGV.GetSelectedFM().DisableAllMods = EditFMDisableAllModsCheckBox.Checked; + RefreshSelectedFM(rowOnly: true); + Ini.WriteFullFMDataIni(); + } - #endregion + private void EditFMRatingComboBox_SelectedIndexChanged(object sender, EventArgs e) + { + if (EventsDisabled) return; + int rating = EditFMRatingComboBox.SelectedIndex - 1; + FMsDGV.GetSelectedFM().Rating = rating; + FMsDGV.SetRatingMenuItemChecked(rating); + RefreshSelectedFM(rowOnly: true); + Ini.WriteFullFMDataIni(); } - private void UpdateRatingLabel(bool suspendResume = true) + private void EditFMLanguageComboBox_SelectedIndexChanged(object sender, EventArgs e) { - // For snappy visual layout performance - if (suspendResume) FilterBarFLP.SuspendDrawing(); - try - { - if (FilterByRatingButton.Checked) - { - bool ndl = Config.RatingDisplayStyle == RatingDisplayStyle.NewDarkLoader; - int rFrom = FMsDGV.Filter.RatingFrom; - int rTo = FMsDGV.Filter.RatingTo; - var curCulture = CultureInfo.CurrentCulture; + if (EventsDisabled || !FMsDGV.RowSelected()) return; - string from = rFrom == -1 ? LText.Global.None : (ndl ? rFrom : rFrom / 2.0).ToString(curCulture); - string to = rTo == -1 ? LText.Global.None : (ndl ? rTo : rTo / 2.0).ToString(curCulture); + FMsDGV.GetSelectedFM().SelectedLang = EditFMLanguageComboBox.SelectedIndex > -1 + ? EditFMLanguageComboBox.SelectedBackingItem() + : FMLanguages.DefaultLangKey; + Ini.WriteFullFMDataIni(); + } - Lazy_ToolStripLabels.Show(this, Lazy_ToolStripLabel.FilterByRating, from + " - " + to); - } - else - { - Lazy_ToolStripLabels.Hide(Lazy_ToolStripLabel.FilterByRating); - } - } - finally - { - if (suspendResume) FilterBarFLP.ResumeDrawing(); - } + private void EditFMFinishedOnButton_Click(object sender, EventArgs e) + { + ShowMenu(FMsDGV.GetFinishedOnMenu(), EditFMFinishedOnButton, MenuPos.BottomRight, unstickMenu: true); + } + + private void EditFMScanLanguagesButton_Click(object sender, EventArgs e) + { + ScanAndFillLanguagesBox(FMsDGV.GetSelectedFM(), forceScan: true); + Ini.WriteFullFMDataIni(); } #endregion - #region Refresh FMs list + #region Comment tab - public void RefreshSelectedFM(bool rowOnly = false) + private void CommentTextBox_TextChanged(object sender, EventArgs e) { - FMsDGV.InvalidateRow(FMsDGV.SelectedRows[0].Index); + if (EventsDisabled) return; - if (rowOnly) return; + if (!FMsDGV.RowSelected()) return; - Update_FMMenu_TopRight_And_BottomBar(FMsDGV.GetSelectedFM()); + var fm = FMsDGV.GetSelectedFM(); + + // Converting a multiline comment to single line: + // DarkLoader copies up to the first linebreak or the 40 char mark, whichever comes first. + // I'm doing the same, but bumping the cutoff point to 100 chars, which is still plenty fast. + // fm.Comment.ToEscapes() is unbounded, but I measure tenths to hundredths of a millisecond even for + // 25,000+ character strings with nothing but slashes and linebreaks in them. + fm.Comment = CommentTextBox.Text.ToRNEscapes(); + fm.CommentSingleLine = CommentTextBox.Text.ToSingleLineComment(100); + + RefreshSelectedFM(rowOnly: true); } - /// - /// Returns false if the list is empty and ClearShownData() has been called, otherwise true - /// - /// - /// - /// - /// - /// - private bool RefreshFMsList(SelectedFM? selectedFM, bool startup = false, KeepSel keepSelection = KeepSel.False, - bool fromColumnClick = false) + private void CommentTextBox_Leave(object sender, EventArgs e) { - using (new DisableEvents(this)) - { - // A small but measurable perf increase from this. Also prevents flickering when switching game - // tabs. - if (!startup) - { - FMsDGV.SuspendDrawing(); - // So, I'm sorry, I thought the line directly above this one said to suspend drawing. I just - // thought I saw a suspend drawing command, and since drawing cells constitutes drawing, I - // just assumed you would understand that to suspend drawing means not to draw cells. I must - // be mistaken. No no, please. - _cellValueNeededDisabled = true; - } + if (EventsDisabled) return; + Ini.WriteFullFMDataIni(); + } - // Prevents: - // -a glitched row from being drawn at the end in certain situations - // -the subsequent row count set from being really slow - FMsDGV.Rows.Clear(); + #endregion - FMsDGV.RowCount = FMsDGV.FilterShownIndexList.Count; + #region Tags tab - if (FMsDGV.RowCount == 0) - { - if (!startup) FMsDGV.ResumeDrawing(); - ClearShownData(); - return false; - } - else - { - int row; - if (keepSelection == KeepSel.False) - { - row = 0; - FMsDGV.FirstDisplayedScrollingRowIndex = 0; - } - else - { - SelectedFM selFM = selectedFM ?? FMsDGV.CurrentSelFM; - bool findNearest = keepSelection == KeepSel.TrueNearest && selectedFM != null; - row = FMsDGV.GetIndexFromInstalledName(selFM.InstalledName, findNearest).ClampToZero(); - try - { - if (fromColumnClick) - { - if (FMsDGV.CurrentSortDirection == SortOrder.Ascending) - { - FMsDGV.FirstDisplayedScrollingRowIndex = row.ClampToZero(); - } - else if (FMsDGV.CurrentSortDirection == SortOrder.Descending) - { - FMsDGV.FirstDisplayedScrollingRowIndex = (row - FMsDGV.DisplayedRowCount(true)).ClampToZero(); - } - } - else - { - FMsDGV.FirstDisplayedScrollingRowIndex = (row - selFM.IndexFromTop).ClampToZero(); - } - } - catch - { - // no room is available to display rows - } - } + // Robustness for if the user presses tab to get away, rather than clicking + internal void AddTagTextBoxOrListBox_Leave(object sender, EventArgs e) + { + if ((sender == AddTagTextBox && !AddTagLLDropDown.Focused) || + (AddTagLLDropDown.Constructed && + sender == AddTagLLDropDown.ListBox && !AddTagTextBox.Focused)) + { + AddTagLLDropDown.HideAndClear(); + } + } - // Events will be re-enabled at the end of the enclosing using block - if (keepSelection != KeepSel.False) EventsDisabled = true; - FMsDGV.Rows[row].Selected = true; - FMsDGV.SelectProperly(suspendResume: startup); + private void AddTagTextBox_TextChanged(object sender, EventArgs e) + { + if (EventsDisabled) return; - // Resume drawing before loading the readme; that way the list will update instantly even - // if the readme doesn't. The user will see delays in the "right place" (the readme box) - // and understand why it takes a sec. Otherwise, it looks like merely changing tabs brings - // a significant delay, and that's annoying because it doesn't seem like it should happen. - if (!startup) - { - _cellValueNeededDisabled = false; - FMsDGV.ResumeDrawing(); - } - } + var list = FMTags.GetMatchingTagsList(AddTagTextBox.Text); + if (list.Count == 0) + { + AddTagLLDropDown.HideAndClear(); + } + else + { + AddTagLLDropDown.SetItemsAndShow(this, list); } - - return true; } - public void RefreshFMsListKeepSelection() + internal void AddTagTextBoxOrListBox_KeyDown(object sender, KeyEventArgs e) { - if (FMsDGV.RowCount == 0) return; - - int selectedRow = FMsDGV.SelectedRows[0].Index; + AddTagLLDropDown.Construct(this); + var box = AddTagLLDropDown.ListBox; - using (new DisableEvents(this)) + switch (e.KeyCode) { - FMsDGV.Refresh(); - FMsDGV.Rows[selectedRow].Selected = true; - FMsDGV.SelectProperly(); + case Keys.Up when box.Items.Count > 0: + // We can't do a switch expression on the second one, so keep them both the same for consistency + // ReSharper disable once ConvertConditionalTernaryExpressionToSwitchExpression + box.SelectedIndex = + box.SelectedIndex == -1 ? box.Items.Count - 1 : + box.SelectedIndex == 0 ? -1 : + box.SelectedIndex - 1; + e.Handled = true; + break; + case Keys.Down when box.Items.Count > 0: + box.SelectedIndex = + box.SelectedIndex == -1 ? 0 : + box.SelectedIndex == box.Items.Count - 1 ? -1 : + box.SelectedIndex + 1; + e.Handled = true; + break; + case Keys.Enter: + string catAndTag = box.SelectedIndex == -1 ? AddTagTextBox.Text : box.SelectedItem.ToString(); + AddTagOperation(FMsDGV.GetSelectedFM(), catAndTag); + break; + default: + if (sender == AddTagLLDropDown.ListBox) AddTagTextBox.Focus(); + break; } } - #endregion + internal void AddTagListBox_SelectedIndexChanged(object sender, EventArgs e) + { + var lb = AddTagLLDropDown.ListBox; + if (lb.SelectedIndex == -1) return; - #region FM display + var tb = AddTagTextBox; - // Perpetual TODO: Make sure this clears everything including the top right tab stuff - private void ClearShownData() - { - if (FMsViewList.Count == 0) ScanAllFMsButton.Enabled = false; + using (new DisableEvents(this)) tb.Text = lb.SelectedItem.ToString(); - FMsDGV.SetInstallUninstallMenuItemText(true); - FMsDGV.SetInstallUninstallMenuItemEnabled(false); - FMsDGV.SetDeleteFMMenuItemEnabled(false); - FMsDGV.SetOpenInDromEdMenuItemText(false); + if (tb.Text.Length > 0) tb.SelectionStart = tb.Text.Length; + } - InstallUninstallFMLLButton.SetSayInstall(true); - InstallUninstallFMLLButton.SetEnabled(false); + private void RemoveTagButton_Click(object sender, EventArgs e) + { + if (!FMsDGV.RowSelected()) return; - FMsDGV.SetPlayFMMenuItemEnabled(false); - PlayFMButton.Enabled = false; + var fm = FMsDGV.GetSelectedFM(); + var tv = TagsTreeView; - FMsDGV.SetPlayFMInMPMenuItemVisible(false); + bool success = FMTags.RemoveTagFromFM(fm, tv.SelectedNode?.Parent?.Text ?? "", tv.SelectedNode?.Text ?? ""); + if (!success) return; - FMsDGV.SetOpenInDromEdVisible(false); + DisplayFMTags(fm.Tags); + } - FMsDGV.SetExportFMIniFromFMMenuItemEnabled(false); + internal void AddTagListBox_MouseUp(object sender, MouseEventArgs e) + { + if (e.Button != MouseButtons.Left) return; - FMsDGV.SetScanFMMenuItemEnabled(false); + if (AddTagLLDropDown.ListBox.SelectedIndex > -1) + { + AddTagOperation(FMsDGV.GetSelectedFM(), AddTagLLDropDown.ListBox.SelectedItem.ToString()); + } + } - FMsDGV.SetConvertAudioRCSubMenuEnabled(false); + private void AddTagOperation(FanMission fm, string catAndTag) + { + if (!catAndTag.CharCountIsAtLeast(':', 2) && !catAndTag.IsWhiteSpace()) + { + FMTags.AddTagToFM(fm, catAndTag); + DisplayFMTags(fm.Tags); + } - // Hide instead of clear to avoid zoom factor pain - SetReadmeVisible(false); + AddTagTextBox.Clear(); + AddTagLLDropDown.HideAndClear(); + } - ChooseReadmeLLPanel.ShowPanel(false); - ViewHTMLReadmeLLButton.Hide(); - WebSearchButton.Enabled = false; + private void AddTagButton_Click(object sender, EventArgs e) => AddTagOperation(FMsDGV.GetSelectedFM(), AddTagTextBox.Text); - BlankStatsPanelWithMessage(LText.StatisticsTab.NoFMSelected); - StatsScanCustomResourcesButton.Hide(); + private void AddTagFromListButton_Click(object sender, EventArgs e) + { + GlobalTags.SortAndMoveMiscToEnd(); - AltTitlesLLMenu.ClearItems(); + AddTagLLMenu.Construct(this, components); + AddTagLLMenu.Menu.Items.Clear(); - using (new DisableEvents(this)) + var addTagMenuItems = new List(GlobalTags.Count); + foreach (GlobalCatAndTags catAndTag in GlobalTags) { - EditFMRatingComboBox.SelectedIndex = 0; + if (catAndTag.Tags.Count == 0) + { + var catItem = new ToolStripMenuItem(catAndTag.Category + ":"); + catItem.Click += AddTagMenuEmptyItem_Click; + addTagMenuItems.Add(catItem); + } + else + { + var catItem = new ToolStripMenuItem(catAndTag.Category.Name); + addTagMenuItems.Add(catItem); - EditFMLanguageComboBox.ClearFullItems(); - EditFMLanguageComboBox.AddFullItem(FMLanguages.DefaultLangKey, LText.EditFMTab.DefaultLanguage); - EditFMLanguageComboBox.SelectedIndex = 0; + var last = addTagMenuItems[addTagMenuItems.Count - 1]; - foreach (Control c in EditFMTabPage.Controls) - { - switch (c) + if (catAndTag.Category.Name != "misc") { - case TextBox tb: - tb.Text = ""; - break; - case DateTimePicker dtp: - dtp.Value = DateTime.Now; - dtp.Hide(); - break; - case CheckBox chk: - chk.Checked = false; - break; + var customItem = new ToolStripMenuItem(LText.Global.CustomTagInCategory); + customItem.Click += AddTagMenuCustomItem_Click; + ((ToolStripMenuItem)last).DropDownItems.Add(customItem); + ((ToolStripMenuItem)last).DropDownItems.Add(new ToolStripSeparator()); } - c.Enabled = false; - } - - FMsDGV.ClearFinishedOnMenuItemChecks(); + foreach (GlobalCatOrTag tag in catAndTag.Tags) + { + var tagItem = new ToolStripMenuItem(tag.Name); - CommentTextBox.Text = ""; - CommentTextBox.Enabled = false; - AddTagTextBox.Text = ""; + if (catAndTag.Category.Name == "misc") + { + tagItem.Click += AddTagMenuMiscItem_Click; + } + else + { + tagItem.Click += AddTagMenuItem_Click; + } - TagsTreeView.Nodes.Clear(); + ((ToolStripMenuItem)last).DropDownItems.Add(tagItem); + } + } + } - foreach (Control c in TagsTabPage.Controls) c.Enabled = false; + AddTagLLMenu.Menu.Items.AddRange(addTagMenuItems.ToArray()); - ShowPatchSection(enable: false); - } + ShowMenu(AddTagLLMenu.Menu, AddTagFromListButton, MenuPos.LeftDown); } - private void HidePatchSectionWithMessage(string message) + private void AddTagMenuItem_Click(object sender, EventArgs e) { - PatchDMLsListBox.Items.Clear(); - PatchMainPanel.Hide(); - PatchFMNotInstalledLabel.Text = message; - PatchFMNotInstalledLabel.CenterHV(PatchTabPage); - PatchFMNotInstalledLabel.Show(); - } + var item = (ToolStripMenuItem)sender; + if (item.HasDropDownItems) return; - private void ShowPatchSection(bool enable) - { - PatchDMLsListBox.Items.Clear(); - PatchMainPanel.Show(); - PatchFMNotInstalledLabel.CenterHV(PatchTabPage); - PatchFMNotInstalledLabel.Hide(); - PatchMainPanel.Enabled = enable; + var cat = item.OwnerItem; + if (cat == null) return; + + AddTagOperation(FMsDGV.GetSelectedFM(), cat.Text + ": " + item.Text); } - private void BlankStatsPanelWithMessage(string message) + private void AddTagMenuCustomItem_Click(object sender, EventArgs e) { - CustomResourcesLabel.Text = message; - foreach (CheckBox cb in StatsCheckBoxesPanel.Controls) cb.Checked = false; - StatsCheckBoxesPanel.Hide(); + var item = (ToolStripMenuItem)sender; + + var cat = item.OwnerItem; + if (cat == null) return; + + AddTagTextBox.SetTextAndMoveCursorToEnd(cat.Text + ": "); } - public void UpdateRatingMenus(int rating, bool disableEvents = false) + private void AddTagMenuMiscItem_Click(object sender, EventArgs e) => AddTagTextBox.SetTextAndMoveCursorToEnd(((ToolStripMenuItem)sender).Text); + + private void AddTagMenuEmptyItem_Click(object sender, EventArgs e) => AddTagTextBox.SetTextAndMoveCursorToEnd(((ToolStripMenuItem)sender).Text + " "); + + // Just to keep things in a known state (clearing items also removes their event hookups, which is + // convenient) + [SuppressMessage("ReSharper", "MemberCanBeMadeStatic.Global")] + internal void AddTagMenu_Closed(object sender, ToolStripDropDownClosedEventArgs e) { - using (disableEvents ? new DisableEvents(this) : null) - { - FMsDGV.SetRatingMenuItemChecked(rating); - EditFMRatingComboBox.SelectedIndex = rating + 1; - } + // This handler will only be hooked up after construction, so we don't need to call Construct() + AddTagLLMenu.Menu.Items.Clear(); } - // @GENGAMES: Lots of game-specific code in here, but I don't see much to be done about it. - private void Update_FMMenu_TopRight_And_BottomBar(FanMission fm) + #endregion + + #region Patch tab + + private void PatchRemoveDMLButton_Click(object sender, EventArgs e) { - bool fmIsT3 = fm.Game == Game.Thief3; - bool fmIsSS2 = fm.Game == Game.SS2; + var lb = PatchDMLsListBox; + if (lb.SelectedIndex == -1) return; - #region Toggles + bool success = Core.RemoveDML(FMsDGV.GetSelectedFM(), lb.SelectedItem.ToString()); + if (!success) return; - // We should never get here when FMsList.Count == 0, but hey - if (FMsViewList.Count > 0) ScanAllFMsButton.Enabled = true; + lb.RemoveAndSelectNearest(); + } - FMsDGV.SetGameSpecificFinishedOnMenuItemsText(fm.Game); - // FinishedOnUnknownMenuItem text stays the same + private void PatchAddDMLButton_Click(object sender, EventArgs e) + { + var lb = PatchDMLsListBox; - bool gameIsSupported = GameIsKnownAndSupported(fm.Game); + var dmlFiles = new List(); - FMsDGV.SetInstallUninstallMenuItemEnabled(gameIsSupported); - FMsDGV.SetInstallUninstallMenuItemText(!fm.Installed); - FMsDGV.SetDeleteFMMenuItemEnabled(true); - FMsDGV.SetOpenInDromEdMenuItemText(fmIsSS2); + using (var d = new OpenFileDialog()) + { + d.Multiselect = true; + d.Filter = LText.BrowseDialogs.DMLFiles + "|*.dml"; + if (d.ShowDialog() != DialogResult.OK || d.FileNames.Length == 0) return; + dmlFiles.AddRange(d.FileNames); + } - // Sneaky Upgrade's FMSel allows exporting fm.ini files, so I guess Thief 3 can have those too - FMsDGV.SetExportFMIniFromFMMenuItemEnabled(gameIsSupported); + foreach (string f in dmlFiles) + { + if (f.IsEmpty()) continue; - FMsDGV.SetOpenInDromEdVisible(GameIsDark(fm.Game) && Config.GetGameEditorDetectedUnsafe(fm.Game)); + bool success = Core.AddDML(FMsDGV.GetSelectedFM(), f); + if (!success) return; - FMsDGV.SetPlayFMInMPMenuItemVisible(fm.Game == Game.Thief2 && Config.T2MPDetected); + string dmlFileName = Path.GetFileName(f); + if (!lb.Items.Cast().ToArray().ContainsI(dmlFileName)) + { + lb.Items.Add(dmlFileName); + } + } + } - InstallUninstallFMLLButton.SetEnabled(gameIsSupported); - InstallUninstallFMLLButton.SetSayInstall(!fm.Installed); + private void PatchOpenFMFolderButton_Click(object sender, EventArgs e) => Core.OpenFMFolder(FMsDGV.GetSelectedFM()); - FMsDGV.SetPlayFMMenuItemEnabled(gameIsSupported); - PlayFMButton.Enabled = gameIsSupported; + #endregion - FMsDGV.SetScanFMMenuItemEnabled(true); + private void TopRightCollapseButton_Click(object sender, EventArgs e) + { + TopSplitContainer.ToggleFullScreen(); + SetTopRightCollapsedState(); + } - FMsDGV.SetConvertAudioRCSubMenuEnabled(GameIsDark(fm.Game) && fm.Installed); + private void SetTopRightCollapsedState() + { + bool collapsed = TopSplitContainer.FullScreen; + TopRightTabControl.Enabled = !collapsed; + TopRightCollapseButton.ArrowDirection = collapsed ? Direction.Left : Direction.Right; + } - WebSearchButton.Enabled = true; + private void TopRightMenuButton_Click(object sender, EventArgs e) + { + TopRightLLMenu.Construct(this, components); + ShowMenu(TopRightLLMenu.Menu, TopRightMenuButton, MenuPos.BottomLeft); + } - foreach (Control c in EditFMTabPage.Controls) + internal void TopRightMenu_MenuItems_Click(object sender, EventArgs e) + { + var s = (ToolStripMenuItem)sender; + + // NULL_TODO: Null so I can assert + TabPage? tab = null; + for (int i = 0; i < TopRightTabsData.Count; i++) { - if (c == EditFMLanguageLabel || - c == EditFMLanguageComboBox || - c == EditFMScanLanguagesButton) - { - c.Enabled = !fmIsT3; - } - else + if (s == (ToolStripMenuItem)TopRightLLMenu.Menu.Items[i]) { - c.Enabled = true; + tab = _topRightTabsInOrder[i]; + break; } } - CommentTextBox.Enabled = true; - foreach (Control c in TagsTabPage.Controls) c.Enabled = true; - - PatchMainPanel.Enabled = true; + AssertR(tab != null, nameof(tab) + " is null - tab does not have a corresponding menu item"); - if (fm.Installed) - { - ShowPatchSection(enable: true); - } - else + if (!s.Checked && TopRightTabControl.TabCount == 1) { - HidePatchSectionWithMessage(LText.PatchTab.FMNotInstalled); + s.Checked = true; + return; } - PatchDMLsPanel.Enabled = GameIsDark(fm.Game); - - #endregion + TopRightTabControl.ShowTab(tab!, s.Checked); + } - #region FinishedOn + #endregion - FMsDGV.SetFinishedOnMenuItemsChecked((Difficulty)fm.FinishedOn, fm.FinishedOnUnknown); + #region FMs list - #endregion + public SelectedFM? GetSelectedFMPosInfo() => FMsDGV.RowSelected() ? FMsDGV.GetSelectedFMPosInfo() : null; - #region Custom resources + public void SetRowCount(int count) => FMsDGV.RowCount = count; - if (fmIsT3) - { - BlankStatsPanelWithMessage(LText.StatisticsTab.CustomResourcesNotSupportedForThief3); - StatsScanCustomResourcesButton.Hide(); - } - else if (!fm.ResourcesScanned) - { - BlankStatsPanelWithMessage(LText.StatisticsTab.CustomResourcesNotScanned); - StatsScanCustomResourcesButton.Show(); - } - else - { - CustomResourcesLabel.Text = LText.StatisticsTab.CustomResources; + public void ShowFMsListZoomButtons(bool visible) + { + Lazy_FMsListZoomButtons.SetVisible(this, visible); + SetFilterBarWidth(); + } - CR_MapCheckBox.Checked = FMHasResource(fm, CustomResources.Map); - CR_AutomapCheckBox.Checked = FMHasResource(fm, CustomResources.Automap); - CR_ScriptsCheckBox.Checked = FMHasResource(fm, CustomResources.Scripts); - CR_TexturesCheckBox.Checked = FMHasResource(fm, CustomResources.Textures); - CR_SoundsCheckBox.Checked = FMHasResource(fm, CustomResources.Sounds); - CR_ObjectsCheckBox.Checked = FMHasResource(fm, CustomResources.Objects); - CR_CreaturesCheckBox.Checked = FMHasResource(fm, CustomResources.Creatures); - CR_MotionsCheckBox.Checked = FMHasResource(fm, CustomResources.Motions); - CR_MoviesCheckBox.Checked = FMHasResource(fm, CustomResources.Movies); - CR_SubtitlesCheckBox.Checked = FMHasResource(fm, CustomResources.Subtitles); + private void ZoomFMsDGV(ZoomFMsDGVType type, float? zoomFontSize = null) + { + // No goal escapes me, mate - StatsCheckBoxesPanel.Show(); - StatsScanCustomResourcesButton.Show(); - } + SelectedFM? selFM = FMsDGV.RowSelected() ? FMsDGV.GetSelectedFMPosInfo() : null; - #endregion + Font f = FMsDGV.DefaultCellStyle.Font; - #region Other tabs + // Set zoom level + float fontSize = + type == ZoomFMsDGVType.ZoomIn ? f.SizeInPoints + 1.0f : + type == ZoomFMsDGVType.ZoomOut ? f.SizeInPoints - 1.0f : + type == ZoomFMsDGVType.ZoomTo && zoomFontSize != null ? (float)zoomFontSize : + type == ZoomFMsDGVType.ZoomToHeightOnly && zoomFontSize != null ? (float)zoomFontSize : + _fMsListDefaultFontSizeInPoints; - using (new DisableEvents(this)) - { - EditFMTitleTextBox.Text = fm.Title; + // Clamp zoom level + if (fontSize < Math.Round(1.00f, 2)) fontSize = 1.00f; + if (fontSize > Math.Round(41.25f, 2)) fontSize = 41.25f; + fontSize = (float)Math.Round(fontSize, 2); - FillAltTitlesMenu(fm.AltTitles); + // Set new font size + Font newF = new Font(f.FontFamily, fontSize, f.Style, f.Unit, f.GdiCharSet, f.GdiVerticalFont); - EditFMAuthorTextBox.Text = fm.Author; + // Set row height based on font plus some padding + int rowHeight = type == ZoomFMsDGVType.ResetZoom ? _rMsListDefaultRowHeight : newF.Height + 9; - EditFMReleaseDateCheckBox.Checked = fm.ReleaseDate.DateTime != null; - EditFMReleaseDateDateTimePicker.Value = fm.ReleaseDate.DateTime ?? DateTime.Now; - EditFMReleaseDateDateTimePicker.Visible = fm.ReleaseDate.DateTime != null; + // If we're on startup, then the widths will already have been restored (to zoomed size) from the + // config + bool heightOnly = type == ZoomFMsDGVType.ZoomToHeightOnly; - EditFMLastPlayedCheckBox.Checked = fm.LastPlayed.DateTime != null; - EditFMLastPlayedDateTimePicker.Value = fm.LastPlayed.DateTime ?? DateTime.Now; - EditFMLastPlayedDateTimePicker.Visible = fm.LastPlayed.DateTime != null; + // Must be done first, else we get wrong values + List widthMul = new List(); + foreach (DataGridViewColumn c in FMsDGV.Columns) + { + Size size = c.HeaderCell.Size; + widthMul.Add((double)size.Width / size.Height); + } - EditFMDisableAllModsCheckBox.Checked = fm.DisableAllMods; - EditFMDisabledModsTextBox.Text = fm.DisabledMods; - EditFMDisabledModsTextBox.Enabled = !fm.DisableAllMods; + // Set font on cells + FMsDGV.DefaultCellStyle.Font = newF; - UpdateRatingMenus(fm.Rating, disableEvents: false); + // Set font on headers + FMsDGV.ColumnHeadersDefaultCellStyle.Font = newF; - ScanAndFillLanguagesBox(fm, disableEvents: false); + // Set height on all rows (but it won't take effect yet) + FMsDGV.RowTemplate.Height = rowHeight; - CommentTextBox.Text = fm.Comment.FromRNEscapes(); + // Save previous selection + int selIndex = FMsDGV.RowSelected() ? FMsDGV.SelectedRows[0].Index : -1; + using (new DisableEvents(this)) + { + // Force a regeneration of rows (height will take effect here) + int rowCount = FMsDGV.RowCount; + FMsDGV.RowCount = 0; + FMsDGV.RowCount = rowCount; - AddTagTextBox.Text = ""; + // Restore previous selection (no events will be fired, due to being in a DisableEvents block) + if (selIndex > -1) + { + FMsDGV.Rows[selIndex].Selected = true; + FMsDGV.SelectProperly(); + } - if (GameIsDark(fm.Game) && fm.Installed) + // Set column widths (keeping ratio to height) + for (int i = 0; i < FMsDGV.Columns.Count; i++) { - PatchMainPanel.Show(); - PatchFMNotInstalledLabel.Hide(); - PatchDMLsListBox.Items.Clear(); - (bool success, List dmlFiles) = Core.GetDMLFiles(fm); - if (success) + DataGridViewColumn c = FMsDGV.Columns[i]; + + // Complicated gobbledegook for handling different options and also special-casing the + // non-resizable columns + bool reset = type == ZoomFMsDGVType.ResetZoom; + if (c != RatingImageColumn && c != FinishedColumn) { - foreach (string f in dmlFiles) + c.MinimumWidth = reset ? Defaults.MinColumnWidth : rowHeight + 3; + } + + if (heightOnly) + { + if (c == RatingImageColumn || c == FinishedColumn) { - if (!f.IsEmpty()) PatchDMLsListBox.Items.Add(f); + c.Width = (int)Math.Round(c.HeaderCell.Size.Height * widthMul[i]); + } + } + else + { + if (reset && c == RatingImageColumn) + { + c.Width = _ratingImageColumnWidth; + } + else if (reset && c == FinishedColumn) + { + c.Width = _finishedColumnWidth; + } + else + { + // The ever-present rounding errors creep in here, but meh. I should figure out + // how to not have those - ensure scaling always happens in integral pixel counts + // somehow? + c.Width = reset && Math.Abs(Config.FMsListFontSizeInPoints - _fMsListDefaultFontSizeInPoints) < 0.1 + ? Config.Columns[i].Width + : (int)Math.Ceiling(c.HeaderCell.Size.Height * widthMul[i]); } } } } - DisplayFMTags(fm.Tags); + // Keep selected FM in the center of the list vertically where possible (UX nicety) + if (selIndex > -1 && selFM != null) CenterSelectedFM(); - #endregion + // And that's how you do it } - private async Task DisplaySelectedFM(bool refreshCache = false) + private void CenterSelectedFM() { - FanMission fm = FMsDGV.GetSelectedFM(); - - if (fm.Game == Game.Null || (GameIsKnownAndSupported(fm.Game) && !fm.MarkedScanned)) + try { - using (new DisableKeyPresses(this)) - { - if (await FMScan.ScanFMs(new List { fm }, hideBoxIfZip: true)) - { - RefreshSelectedFM(rowOnly: true); - } - } + FMsDGV.FirstDisplayedScrollingRowIndex = + (FMsDGV.SelectedRows[0].Index - (FMsDGV.DisplayedRowCount(true) / 2)) + .Clamp(0, FMsDGV.RowCount - 1); } - - Update_FMMenu_TopRight_And_BottomBar(fm); - - var cacheData = await FMCache.GetCacheableData(fm, refreshCache); - - #region Readme - - var readmeFiles = cacheData.Readmes; - readmeFiles.Sort(); - - if (!readmeFiles.PathContainsI(fm.SelectedReadme)) fm.SelectedReadme = ""; - - using (new DisableEvents(this)) ChooseReadmeComboBox.ClearFullItems(); - - if (!fm.SelectedReadme.IsEmpty()) + catch { - if (readmeFiles.Count > 1) - { - ReadmeComboBoxFillAndSelect(readmeFiles, fm.SelectedReadme); - } - else - { - ChooseReadmeComboBox.Hide(); - } + // no room is available to display rows } - else // if fm.SelectedReadme is empty - { - if (readmeFiles.Count == 0) - { - ReadmeRichTextBox.SetText(LText.ReadmeArea.NoReadmeFound); - - ChooseReadmeLLPanel.ShowPanel(false); - ChooseReadmeComboBox.Hide(); - ViewHTMLReadmeLLButton.Hide(); - SetReadmeVisible(true); - - return; - } - else if (readmeFiles.Count > 1) - { - string safeReadme = Core.DetectSafeReadme(readmeFiles, fm.Title); + } - if (!safeReadme.IsEmpty()) - { - fm.SelectedReadme = safeReadme; - // @DIRSEP: Pass only fm.SelectedReadme, otherwise we might end up with un-normalized dirseps - ReadmeComboBoxFillAndSelect(readmeFiles, fm.SelectedReadme); - } - else - { - SetReadmeVisible(false); - ViewHTMLReadmeLLButton.Hide(); + #region FMs list sorting - ChooseReadmeLLPanel.Construct(this, MainSplitContainer.Panel2); + public Column GetCurrentSortedColumnIndex() => FMsDGV.CurrentSortedColumn; - ChooseReadmeLLPanel.ListBox.ClearFullItems(); - foreach (string f in readmeFiles) ChooseReadmeLLPanel.ListBox.AddFullItem(f, f.GetFileNameFast()); + public SortOrder GetCurrentSortDirection() => FMsDGV.CurrentSortDirection; - ShowReadmeControls(false); + private void SortFMsDGV(Column column, SortOrder sortDirection) + { + FMsDGV.CurrentSortedColumn = column; + FMsDGV.CurrentSortDirection = sortDirection; - ChooseReadmeLLPanel.ShowPanel(true); + Core.SortFMsViewList(column, sortDirection); - return; - } + // Perf: doing it this way is significantly faster than the old method of indiscriminately setting + // all columns to None and then setting the current one back to the CurrentSortDirection glyph again + int intCol = (int)column; + for (int i = 0; i < FMsDGV.Columns.Count; i++) + { + DataGridViewColumn c = FMsDGV.Columns[i]; + if (i == intCol && c.HeaderCell.SortGlyphDirection != FMsDGV.CurrentSortDirection) + { + c.HeaderCell.SortGlyphDirection = FMsDGV.CurrentSortDirection; } - else if (readmeFiles.Count == 1) + else if (i != intCol && c.HeaderCell.SortGlyphDirection != SortOrder.None) { - fm.SelectedReadme = readmeFiles[0]; - - ChooseReadmeComboBox.Hide(); + c.HeaderCell.SortGlyphDirection = SortOrder.None; } } - - ChooseReadmeLLPanel.ShowPanel(false); - - LoadReadme(fm); - - #endregion } - private void ScanAndFillLanguagesBox(FanMission fm, bool forceScan = false, bool disableEvents = true) + /// + /// Pass selectedFM only if you need to store it BEFORE this method runs, like for RefreshFromDisk() + /// + /// + /// + /// + /// + /// + public async Task SortAndSetFilter(SelectedFM? selectedFM = null, bool forceDisplayFM = false, + bool keepSelection = true, bool gameTabSwitch = false) { - using (disableEvents ? new DisableEvents(this) : null) - { - EditFMLanguageComboBox.ClearFullItems(); - EditFMLanguageComboBox.AddFullItem(FMLanguages.DefaultLangKey, LText.EditFMTab.DefaultLanguage); + bool selFMWasPassedIn = selectedFM != null; - if (!GameIsDark(fm.Game)) - { - EditFMLanguageComboBox.SelectedIndex = 0; - fm.SelectedLang = FMLanguages.DefaultLangKey; - return; - } + FanMission? oldSelectedFM = FMsDGV.RowSelected() ? FMsDGV.GetSelectedFM() : null; - bool doScan = forceScan || !fm.LangsScanned; + selectedFM ??= keepSelection && !gameTabSwitch && FMsDGV.RowSelected() + ? FMsDGV.GetSelectedFMPosInfo() + : null; - if (doScan) FMLanguages.FillFMSupportedLangs(fm); + KeepSel keepSel = + selectedFM != null ? KeepSel.TrueNearest : + keepSelection || gameTabSwitch ? KeepSel.True : KeepSel.False; - var langs = fm.Langs.Split(CA_Comma, StringSplitOptions.RemoveEmptyEntries).ToList(); - var sortedLangs = doScan ? langs : FMLanguages.SortLangsToSpec(langs); - fm.Langs = ""; - for (int i = 0; i < sortedLangs.Count; i++) - { - string langLower = sortedLangs[i].ToLowerInvariant(); - EditFMLanguageComboBox.AddFullItem(langLower, FMLanguages.Translated[langLower]); + // Fix: in RefreshFMsList, CurrentSelFM was being used when coming from no FMs listed to some FMs listed + if (!gameTabSwitch && !selFMWasPassedIn && oldSelectedFM == null) keepSel = KeepSel.False; + + if (gameTabSwitch) forceDisplayFM = true; - // Rewrite the FM's lang string for cleanliness, in case it contains unsupported langs or - // other nonsense - if (!langLower.EqualsI(FMLanguages.DefaultLangKey)) - { - if (!fm.Langs.IsEmpty()) fm.Langs += ","; - fm.Langs += langLower; - } - } + SortFMsDGV(FMsDGV.CurrentSortedColumn, FMsDGV.CurrentSortDirection); - if (fm.SelectedLang.EqualsI(FMLanguages.DefaultLangKey)) - { - EditFMLanguageComboBox.SelectedIndex = 0; - fm.SelectedLang = FMLanguages.DefaultLangKey; - } - else - { - int index = EditFMLanguageComboBox.BackingItems.FindIndex(x => x.EqualsI(fm.SelectedLang)); - EditFMLanguageComboBox.SelectedIndex = index == -1 ? 0 : index; + Core.SetFilter(); + if (RefreshFMsList(selectedFM, keepSelection: keepSel)) + { + // DEBUG: Keep this in for testing this because the whole thing is irrepressibly finicky + //Trace.WriteLine(nameof(keepSelection) + ": " + keepSelection); + //Trace.WriteLine("selectedFM != null: " + (selectedFM != null)); + //Trace.WriteLine("!selectedFM.InstalledName.IsEmpty(): " + (selectedFM != null && !selectedFM.InstalledName.IsEmpty())); + //Trace.WriteLine("selectedFM.InstalledName != FMsDGV.GetSelectedFM().InstalledDir: " + (selectedFM != null && selectedFM.InstalledName != FMsDGV.GetSelectedFM().InstalledDir)); - fm.SelectedLang = EditFMLanguageComboBox.SelectedIndex > -1 - ? EditFMLanguageComboBox.SelectedBackingItem() - : FMLanguages.DefaultLangKey; + // Optimization in case we land on the same as FM as before, don't reload it + // And whaddaya know, I still ended up having to have this eyes-glazing-over stuff here. + if (forceDisplayFM || + (keepSelection && + selectedFM != null && !selectedFM.InstalledName.IsEmpty() && + selectedFM.InstalledName != FMsDGV.GetSelectedFM().InstalledDir) || + (!keepSelection && + (oldSelectedFM == null || + (FMsDGV.RowSelected() && !oldSelectedFM.Equals(FMsDGV.GetSelectedFM())))) || + // Fix: when resetting release date filter the readme wouldn't load for the selected FM + oldSelectedFM == null) + { + await DisplaySelectedFM(); } } } - private void ReadmeComboBoxFillAndSelect(List readmeFiles, string readme) + #endregion + + #region FMsDGV event handlers + + // Coloring the recent rows here because if we do it in _CellValueNeeded, we get a brief flash of the + // default while-background cell color before it changes. + private void FMsDGV_RowPrePaint(object sender, DataGridViewRowPrePaintEventArgs e) { - using (new DisableEvents(this)) - { - // @DIRSEP: To backslashes for each file, to prevent selection misses. - // I thought I accounted for this with backslashing the selected readme, but they all need to be. - foreach (string f in readmeFiles) ChooseReadmeComboBox.AddFullItem(f.ToBackSlashes(), f.GetFileNameFast()); - ChooseReadmeComboBox.SelectBackingIndexOf(readme); - } + if (_cellValueNeededDisabled) return; + + if (FMsDGV.FilterShownIndexList.Count == 0) return; + + var fm = FMsDGV.GetFMFromIndex(e.RowIndex); + + FMsDGV.Rows[e.RowIndex].DefaultCellStyle.BackColor = fm.MarkedRecent ? Color.LightGoldenrodYellow : SystemColors.Window; } - private void LoadReadme(FanMission fm) + private void FMsDGV_CellValueNeeded_Initial(object sender, DataGridViewCellValueEventArgs e) { - try - { - (string path, ReadmeType fileType) = Core.GetReadmeFileAndType(fm); - #region Debug + if (_cellValueNeededDisabled) return; - // Tells me whether a readme got reloaded more than once, which should never be allowed to happen - // due to performance concerns. -#if DEBUG || (Release_Testing && !RT_StartupOnly) - DebugLabel.Text = int.TryParse(DebugLabel.Text, out int result) ? (result + 1).ToString() : "1"; -#endif + // Lazy-load these in an attempt to save some kind of startup time + // @LAZYLOAD: Try lazy-loading these at a more granular level + // The arrays are obstacles to lazy-loading, but see if we still get good scrolling perf when we look + // them up and load the individual images as needed, rather than all at once here - #endregion + // @GENGAMES (Game icons for FMs list): Begin + // We would prefer to put these in an array, but see Images class for why we can't really do that + GameIcons[(int)Thief1] = Images.Thief1_21; + GameIcons[(int)Thief2] = Images.Thief2_21; + GameIcons[(int)Thief3] = Images.Thief3_21; + GameIcons[(int)SS2] = Images.Shock2_21; + // @GENGAMES (Game icons for FMs list): End - if (fileType == ReadmeType.HTML) - { - ViewHTMLReadmeLLButton.Show(this); - SetReadmeVisible(false); - // In case the cursor is over the scroll bar area - if (CursorOverReadmeArea()) ShowReadmeControls(true); - } - else - { - SetReadmeVisible(true); - ViewHTMLReadmeLLButton.Hide(); + BlankIcon = new Bitmap(1, 1, PixelFormat.Format32bppPArgb); + CheckIcon = Resources.CheckCircle; + RedQuestionMarkIcon = Resources.QuestionMarkCircleRed; + // @LAZYLOAD: Have these be wrapper objects so we can put them in the list without them loading + // Then grab the internal object down below when we go to display them + StarIcons = Images.GetRatingImages(); - ReadmeRichTextBox.LoadContent(path, fileType); - } - } - catch (Exception ex) - { - Log(nameof(LoadReadme) + " failed.", ex); + FinishedOnIcons = Images.GetFinishedOnImages(BlankIcon); + FinishedOnUnknownIcon = Images.FinishedOnUnknown; - ViewHTMLReadmeLLButton.Hide(); - SetReadmeVisible(true); - ReadmeRichTextBox.SetText(LText.ReadmeArea.UnableToLoadReadme); - } + // Prevents having to check the bool again forevermore even after we've already set the images. + // Taking an extremely minor technique from a data-oriented design talk, heck yeah! + FMsDGV.CellValueNeeded -= FMsDGV_CellValueNeeded_Initial; + FMsDGV.CellValueNeeded += FMsDGV_CellValueNeeded; + FMsDGV_CellValueNeeded(sender, e); } - private void FillAltTitlesMenu(List fmAltTitles) + private void FMsDGV_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e) { - if (!AltTitlesLLMenu.Constructed) return; + if (_cellValueNeededDisabled) return; - AltTitlesLLMenu.ClearItems(); + if (FMsDGV.FilterShownIndexList.Count == 0) return; - if (fmAltTitles.Count == 0) - { - EditFMAltTitlesArrowButton.Enabled = false; - } - else - { - List altTitlesMenuItems = new List(fmAltTitles.Count); - foreach (string altTitle in fmAltTitles) - { - var item = new ToolStripMenuItem { Text = altTitle }; - item.Click += EditFMAltTitlesMenuItems_Click; - altTitlesMenuItems.Add(item); - } - AltTitlesLLMenu.AddRange(altTitlesMenuItems); + var fm = FMsDGV.GetFMFromIndex(e.RowIndex); - EditFMAltTitlesArrowButton.Enabled = true; - } - } + // PERF: ~0.14ms per FM for en-US Long Date format + // PERF_TODO: Test with custom - dt.ToString() might be slow? + static string FormatDate(DateTime dt) => Config.DateFormat switch + { + DateFormat.CurrentCultureShort => dt.ToShortDateString(), + DateFormat.CurrentCultureLong => dt.ToLongDateString(), + _ => dt.ToString(Config.DateCustomFormatString) + }; - private void DisplayFMTags(CatAndTagsList fmTags) - { - var tv = TagsTreeView; + static string FormatSize(ulong size) => + size == 0 + ? "" + : size < ByteSize.MB + ? Math.Round(size / 1024f).ToString(CultureInfo.CurrentCulture) + " " + LText.Global.KilobyteShort + : size >= ByteSize.MB && size < ByteSize.GB + ? Math.Round(size / 1024f / 1024f).ToString(CultureInfo.CurrentCulture) + " " + LText.Global.MegabyteShort + : Math.Round(size / 1024f / 1024f / 1024f, 2).ToString(CultureInfo.CurrentCulture) + " " + LText.Global.GigabyteShort; - try + switch ((Column)e.ColumnIndex) { - tv.SuspendDrawing(); - tv.Nodes.Clear(); - - if (fmTags.Count == 0) return; + case Column.Game: + e.Value = + GameIsKnownAndSupported(fm.Game) ? GameIcons[(int)GameToGameIndex(fm.Game)] : + fm.Game == Game.Unsupported ? RedQuestionMarkIcon : + // Can't say null, or else it sets an ugly red-x image + BlankIcon; + break; - fmTags.SortAndMoveMiscToEnd(); + case Column.Installed: + e.Value = fm.Installed ? CheckIcon : BlankIcon; + break; - foreach (CatAndTags item in fmTags) - { - tv.Nodes.Add(item.Category); - var last = tv.Nodes[tv.Nodes.Count - 1]; - foreach (string tag in item.Tags) last.Nodes.Add(tag); - } + case Column.Title: + if (Config.EnableArticles && Config.MoveArticlesToEnd) + { + string title = fm.Title; + for (int i = 0; i < Config.Articles.Count; i++) + { + string a = Config.Articles[i]; + if (fm.Title.StartsWithI(a + " ")) + { + // Take the actual article from the name so as to preserve casing + title = fm.Title.Substring(a.Length + 1) + ", " + fm.Title.Substring(0, a.Length); + break; + } + } + e.Value = title; + } + else + { + e.Value = fm.Title; + } + break; - tv.ExpandAll(); - } - finally - { - tv.ResumeDrawing(); - } - } + case Column.Archive: + e.Value = fm.Archive; + break; - #endregion + case Column.Author: + e.Value = fm.Author; + break; - #region Game tabs + case Column.Size: + // This conversion takes like 1ms over the entire 1545 set, so no problem + e.Value = FormatSize(fm.SizeBytes); + break; - private (SelectedFM GameSelFM, Filter GameFilter) - GetGameSelFMAndFilter(TabPage tabPage) - { - // NULL_TODO: Null so I can assert - SelectedFM? gameSelFM = null; - Filter? gameFilter = null; - for (int i = 0; i < SupportedGameCount; i++) - { - if (_gameTabsInOrder[i] == tabPage) - { - gameSelFM = FMsDGV.GameTabsState.GetSelectedFM((GameIndex)i); - gameFilter = FMsDGV.GameTabsState.GetFilter((GameIndex)i); + case Column.Rating: + if (Config.RatingDisplayStyle == RatingDisplayStyle.NewDarkLoader) + { + e.Value = fm.Rating == -1 ? "" : fm.Rating.ToString(); + } + else + { + if (Config.RatingUseStars) + { + e.Value = fm.Rating == -1 ? BlankIcon : StarIcons![fm.Rating]; + } + else + { + e.Value = fm.Rating == -1 ? "" : (fm.Rating / 2.0).ToString(CultureInfo.CurrentCulture); + } + } break; - } - } - - AssertR(gameSelFM != null, "gameSelFM is null: Selected tab is not being handled"); - AssertR(gameFilter != null, "gameFilter is null: Selected tab is not being handled"); - return (gameSelFM!, gameFilter!); - } + case Column.Finished: + e.Value = fm.FinishedOnUnknown ? FinishedOnUnknownIcon : FinishedOnIcons![fm.FinishedOn]; + break; - private void SaveCurrentTabSelectedFM(TabPage tabPage) - { - var (gameSelFM, gameFilter) = GetGameSelFMAndFilter(tabPage); - SelectedFM selFM = FMsDGV.GetSelectedFMPosInfo(); - selFM.DeepCopyTo(gameSelFM); - FMsDGV.Filter.DeepCopyTo(gameFilter); - } + case Column.ReleaseDate: + e.Value = fm.ReleaseDate.DateTime != null ? FormatDate((DateTime)fm.ReleaseDate.DateTime) : ""; + break; - private void GamesTabControl_Deselecting(object sender, TabControlCancelEventArgs e) - { - if (EventsDisabled) return; - if (GamesTabControl.Visible) SaveCurrentTabSelectedFM(e.TabPage); - } + case Column.LastPlayed: + e.Value = fm.LastPlayed.DateTime != null ? FormatDate((DateTime)fm.LastPlayed.DateTime) : ""; + break; - private async void GamesTabControl_SelectedIndexChanged(object sender, EventArgs e) - { - if (EventsDisabled) return; + case Column.DateAdded: + // IMPORTANT (Convert to local time): We don't do it earlier for startup perf reasons. + e.Value = fm.DateAdded != null ? FormatDate(((DateTime)fm.DateAdded).ToLocalTime()) : ""; + break; - var (gameSelFM, gameFilter) = GetGameSelFMAndFilter(GamesTabControl.SelectedTab); + case Column.DisabledMods: + e.Value = fm.DisableAllMods ? LText.FMsList.AllModsDisabledMessage : fm.DisabledMods; + break; - for (int i = 0; i < SupportedGameCount; i++) - { - _filterByGameButtonsInOrder[i].Checked = gameSelFM == FMsDGV.GameTabsState.GetSelectedFM((GameIndex)i); + case Column.Comment: + e.Value = fm.CommentSingleLine; + break; } + } - gameSelFM.DeepCopyTo(FMsDGV.CurrentSelFM); - gameFilter.DeepCopyTo(FMsDGV.Filter); - - SetUIFilterValues(gameFilter); + private async void FMsDGV_ColumnHeaderMouseClick(object sender, DataGridViewCellMouseEventArgs e) + { + if (e.Button != MouseButtons.Left) return; - await SortAndSetFilter(gameTabSwitch: true); - } + SelectedFM? selFM = FMsDGV.RowSelected() ? FMsDGV.GetSelectedFMPosInfo() : null; - #endregion + var newSortDirection = + e.ColumnIndex == (int)FMsDGV.CurrentSortedColumn && FMsDGV.CurrentSortDirection == SortOrder.Ascending + ? SortOrder.Descending + : SortOrder.Ascending; - #region Top-right area + SortFMsDGV((Column)e.ColumnIndex, newSortDirection); - // Hook them all up to one event handler to avoid extraneous async/awaits - private async void FieldScanButtons_Click(object sender, EventArgs e) - { - if (sender == EditFMScanForReadmesButton) - { - Ini.WriteFullFMDataIni(); - await DisplaySelectedFM(refreshCache: true); - } - else + Core.SetFilter(); + if (RefreshFMsList(selFM, keepSelection: KeepSel.TrueNearest, fromColumnClick: true)) { - var scanOptions = - sender == EditFMScanTitleButton ? FMScanner.ScanOptions.FalseDefault(scanTitle: true) : - sender == EditFMScanAuthorButton ? FMScanner.ScanOptions.FalseDefault(scanAuthor: true) : - sender == EditFMScanReleaseDateButton ? FMScanner.ScanOptions.FalseDefault(scanReleaseDate: true) : - //sender == StatsScanCustomResourcesButton - FMScanner.ScanOptions.FalseDefault(scanCustomResources: true); - - if (await FMScan.ScanFMs(new List { FMsDGV.GetSelectedFM() }, scanOptions, hideBoxIfZip: true)) + if (selFM != null && FMsDGV.RowSelected() && + selFM.InstalledName != FMsDGV.GetSelectedFM().InstalledDir) { - RefreshSelectedFM(); + await DisplaySelectedFM(); } } } - #region Edit FM tab - - private void EditFMAltTitlesArrowButtonClick(object sender, EventArgs e) - { - AltTitlesLLMenu.Construct(components); - FillAltTitlesMenu(FMsDGV.GetSelectedFM().AltTitles); - ShowMenu(AltTitlesLLMenu.Menu, EditFMAltTitlesArrowButton, MenuPos.BottomLeft); - } - - private void EditFMAltTitlesMenuItems_Click(object sender, EventArgs e) + private void FMsDGV_MouseDown(object sender, MouseEventArgs e) { - EditFMTitleTextBox.Text = ((ToolStripMenuItem)sender).Text; - Ini.WriteFullFMDataIni(); - } + if (e.Button != MouseButtons.Right) return; - private void EditFMTitleTextBox_TextChanged(object sender, EventArgs e) - { - if (EventsDisabled) return; - FMsDGV.GetSelectedFM().Title = EditFMTitleTextBox.Text; - RefreshSelectedFM(rowOnly: true); - } + var ht = FMsDGV.HitTest(e.X, e.Y); - private void EditFMTitleTextBox_Leave(object sender, EventArgs e) - { - if (EventsDisabled) return; - Ini.WriteFullFMDataIni(); - } + #region Right-click menu - private void EditFMAuthorTextBox_TextChanged(object sender, EventArgs e) - { - if (EventsDisabled) return; - FMsDGV.GetSelectedFM().Author = EditFMAuthorTextBox.Text; - RefreshSelectedFM(rowOnly: true); - } + if (ht.Type == DataGridViewHitTestType.ColumnHeader || ht.Type == DataGridViewHitTestType.None) + { + FMsDGV.SetContextMenuToColumnHeader(); + } + else if (ht.Type == DataGridViewHitTestType.Cell && ht.ColumnIndex > -1 && ht.RowIndex > -1) + { + FMsDGV.SetContextMenuToFM(); + FMsDGV.Rows[ht.RowIndex].Selected = true; + // We don't need to call SelectProperly() here because the mousedown will select it properly + } + else + { + FMsDGV.SetContextMenuToNone(); + } - private void EditFMAuthorTextBox_Leave(object sender, EventArgs e) - { - if (EventsDisabled) return; - Ini.WriteFullFMDataIni(); + #endregion } - private void EditFMReleaseDateCheckBox_CheckedChanged(object sender, EventArgs e) + // Okay, boys and girls. We get the glitched last row on keyboard-scroll if we don't do this idiot thing. + // No, we can't do any of the normal things you'd think would work in RefreshFMsList() itself. I tried. + // Everything is stupid. Whatever. + private bool _fmsListOneTimeHackRefreshDone; + private async void FMsDGV_SelectionChanged(object sender, EventArgs e) { if (EventsDisabled) return; - EditFMReleaseDateDateTimePicker.Visible = EditFMReleaseDateCheckBox.Checked; - FMsDGV.GetSelectedFM().ReleaseDate.DateTime = EditFMReleaseDateCheckBox.Checked - ? EditFMReleaseDateDateTimePicker.Value - : (DateTime?)null; + if (!FMsDGV.RowSelected()) + { + ClearShownData(); + } + else + { + FMsDGV.SelectProperly(); - RefreshSelectedFM(rowOnly: true); - Ini.WriteFullFMDataIni(); - } + if (!_fmsListOneTimeHackRefreshDone) + { + RefreshFMsList(FMsDGV.GetSelectedFMPosInfo(), startup: false, KeepSel.TrueNearest); + _fmsListOneTimeHackRefreshDone = true; + } - private void EditFMReleaseDateDateTimePicker_ValueChanged(object sender, EventArgs e) - { - if (EventsDisabled) return; - FMsDGV.GetSelectedFM().ReleaseDate.DateTime = EditFMReleaseDateDateTimePicker.Value; - RefreshSelectedFM(rowOnly: true); - Ini.WriteFullFMDataIni(); + await DisplaySelectedFM(); + } } - private void EditFMLastPlayedCheckBox_CheckedChanged(object sender, EventArgs e) - { - if (EventsDisabled) return; - EditFMLastPlayedDateTimePicker.Visible = EditFMLastPlayedCheckBox.Checked; - - FMsDGV.GetSelectedFM().LastPlayed.DateTime = EditFMLastPlayedCheckBox.Checked - ? EditFMLastPlayedDateTimePicker.Value - : (DateTime?)null; + #region Crappy hack for basic go-to-first-typed-letter - RefreshSelectedFM(rowOnly: true); - Ini.WriteFullFMDataIni(); - } + // TODO: Make this into a working, polished, documented feature - private void EditFMLastPlayedDateTimePicker_ValueChanged(object sender, EventArgs e) + private void FMsDGV_KeyPress(object sender, KeyPressEventArgs e) { - if (EventsDisabled) return; - FMsDGV.GetSelectedFM().LastPlayed.DateTime = EditFMLastPlayedDateTimePicker.Value; - RefreshSelectedFM(rowOnly: true); - Ini.WriteFullFMDataIni(); - } + if (e.KeyChar.IsAsciiAlpha()) + { + int rowIndex = -1; - private void EditFMDisabledModsTextBox_TextChanged(object sender, EventArgs e) - { - if (EventsDisabled) return; - FMsDGV.GetSelectedFM().DisabledMods = EditFMDisabledModsTextBox.Text; - RefreshSelectedFM(rowOnly: true); - } + for (int i = 0; i < FMsDGV.RowCount; i++) + { + if (FMsDGV.Rows[i].Cells[(int)Column.Title].Value.ToString().StartsWithI(e.KeyChar.ToString())) + { + rowIndex = i; + break; + } + } - private void EditFMDisabledModsTextBox_Leave(object sender, EventArgs e) - { - if (EventsDisabled) return; - Ini.WriteFullFMDataIni(); + if (rowIndex > -1) + { + FMsDGV.Rows[rowIndex].Selected = true; + FMsDGV.SelectProperly(); + FMsDGV.FirstDisplayedScrollingRowIndex = FMsDGV.SelectedRows[0].Index; + } + } } - private void EditFMDisableAllModsCheckBox_CheckedChanged(object sender, EventArgs e) - { - if (EventsDisabled) return; - EditFMDisabledModsTextBox.Enabled = !EditFMDisableAllModsCheckBox.Checked; - - FMsDGV.GetSelectedFM().DisableAllMods = EditFMDisableAllModsCheckBox.Checked; - RefreshSelectedFM(rowOnly: true); - Ini.WriteFullFMDataIni(); - } + #endregion - private void EditFMRatingComboBox_SelectedIndexChanged(object sender, EventArgs e) + private void FMsDGV_KeyDown(object sender, KeyEventArgs e) { - if (EventsDisabled) return; - int rating = EditFMRatingComboBox.SelectedIndex - 1; - FMsDGV.GetSelectedFM().Rating = rating; - FMsDGV.SetRatingMenuItemChecked(rating); - RefreshSelectedFM(rowOnly: true); - Ini.WriteFullFMDataIni(); + // This is in here because it doesn't really work right if we put it in MainForm_KeyDown anyway + if (e.KeyCode == Keys.Apps) + { + FMsDGV.SetContextMenuToFM(); + } } - private void EditFMLanguageComboBox_SelectedIndexChanged(object sender, EventArgs e) + private async void FMsDGV_CellDoubleClick(object sender, DataGridViewCellEventArgs e) { - if (EventsDisabled || !FMsDGV.RowSelected()) return; + FanMission fm; + if (e.RowIndex < 0 || !FMsDGV.RowSelected() || !GameIsKnownAndSupported((fm = FMsDGV.GetSelectedFM()).Game)) + { + return; + } - FMsDGV.GetSelectedFM().SelectedLang = EditFMLanguageComboBox.SelectedIndex > -1 - ? EditFMLanguageComboBox.SelectedBackingItem() - : FMLanguages.DefaultLangKey; - Ini.WriteFullFMDataIni(); + await FMInstallAndPlay.InstallIfNeededAndPlay(fm, askConfIfRequired: true); } - private void EditFMFinishedOnButton_Click(object sender, EventArgs e) + #endregion + + #endregion + + #region Update displayed rating + + public void UpdateRatingDisplayStyle(RatingDisplayStyle style, bool startup) { - ShowMenu(FMsDGV.GetFinishedOnMenu(), EditFMFinishedOnButton, MenuPos.BottomRight, unstickMenu: true); + UpdateRatingListsAndColumn(style == RatingDisplayStyle.FMSel, startup); + UpdateRatingLabel(); } - private void EditFMScanLanguagesButton_Click(object sender, EventArgs e) + private void UpdateRatingListsAndColumn(bool fmSelStyle, bool startup) { - ScanAndFillLanguagesBox(FMsDGV.GetSelectedFM(), forceScan: true); - Ini.WriteFullFMDataIni(); - } + #region Update rating lists - #endregion + // Just in case, since changing a ComboBox item's text counts as a selected index change maybe? Argh! + using (new DisableEvents(this)) + { + for (int i = 0; i <= 10; i++) + { + string num = (fmSelStyle ? i / 2.0 : i).ToString(CultureInfo.CurrentCulture); + EditFMRatingComboBox.Items[i + 1] = num; + } + } - #region Comment tab + FMsDGV.UpdateRatingList(fmSelStyle); - private void CommentTextBox_TextChanged(object sender, EventArgs e) - { - if (EventsDisabled) return; + #endregion - if (!FMsDGV.RowSelected()) return; + #region Update rating column - var fm = FMsDGV.GetSelectedFM(); + var newRatingColumn = + Config.RatingDisplayStyle == RatingDisplayStyle.FMSel && Config.RatingUseStars + ? (DataGridViewColumn)RatingImageColumn! + : RatingTextColumn; - // Converting a multiline comment to single line: - // DarkLoader copies up to the first linebreak or the 40 char mark, whichever comes first. - // I'm doing the same, but bumping the cutoff point to 100 chars, which is still plenty fast. - // fm.Comment.ToEscapes() is unbounded, but I measure tenths to hundredths of a millisecond even for - // 25,000+ character strings with nothing but slashes and linebreaks in them. - fm.Comment = CommentTextBox.Text.ToRNEscapes(); - fm.CommentSingleLine = CommentTextBox.Text.ToSingleLineComment(100); + if (!startup) + { + var oldRatingColumn = FMsDGV.Columns[(int)Column.Rating]; + newRatingColumn!.Width = newRatingColumn == RatingTextColumn + ? oldRatingColumn.Width + // To set the ratio back to exact on zoom reset + : FMsDGV.RowTemplate.Height == 22 + ? _ratingImageColumnWidth + : (FMsDGV.DefaultCellStyle.Font.Height + 9) * (_ratingImageColumnWidth / 22); + newRatingColumn.Visible = oldRatingColumn.Visible; + newRatingColumn.DisplayIndex = oldRatingColumn.DisplayIndex; + } - RefreshSelectedFM(rowOnly: true); + if (!startup || newRatingColumn != RatingTextColumn) + { + using (new DisableEvents(this)) + { + _cellValueNeededDisabled = true; + try + { + FMsDGV.Columns.RemoveAt((int)Column.Rating); + FMsDGV.Columns.Insert((int)Column.Rating, newRatingColumn!); + } + finally + { + _cellValueNeededDisabled = false; + } + } + if (FMsDGV.CurrentSortedColumn == Column.Rating) + { + FMsDGV.Columns[(int)Column.Rating].HeaderCell.SortGlyphDirection = FMsDGV.CurrentSortDirection; + } + } + + if (!startup) + { + FMsDGV.SetColumnData(FMsDGV.GetColumnData()); + RefreshFMsListKeepSelection(); + } + + #endregion } - private void CommentTextBox_Leave(object sender, EventArgs e) + private void UpdateRatingLabel(bool suspendResume = true) { - if (EventsDisabled) return; - Ini.WriteFullFMDataIni(); + // For snappy visual layout performance + if (suspendResume) FilterBarFLP.SuspendDrawing(); + try + { + if (FilterByRatingButton.Checked) + { + bool ndl = Config.RatingDisplayStyle == RatingDisplayStyle.NewDarkLoader; + int rFrom = FMsDGV.Filter.RatingFrom; + int rTo = FMsDGV.Filter.RatingTo; + var curCulture = CultureInfo.CurrentCulture; + + string from = rFrom == -1 ? LText.Global.None : (ndl ? rFrom : rFrom / 2.0).ToString(curCulture); + string to = rTo == -1 ? LText.Global.None : (ndl ? rTo : rTo / 2.0).ToString(curCulture); + + Lazy_ToolStripLabels.Show(this, Lazy_ToolStripLabel.FilterByRating, from + " - " + to); + } + else + { + Lazy_ToolStripLabels.Hide(Lazy_ToolStripLabel.FilterByRating); + } + } + finally + { + if (suspendResume) FilterBarFLP.ResumeDrawing(); + } } #endregion - #region Tags tab + #region Readme - // Robustness for if the user presses tab to get away, rather than clicking - internal void AddTagTextBoxOrListBox_Leave(object sender, EventArgs e) + #region Choose readme + + internal void ChooseReadmeButton_Click(object sender, EventArgs e) { - if ((sender == AddTagTextBox && !AddTagLLDropDown.Focused) || - (AddTagLLDropDown.Constructed && - sender == AddTagLLDropDown.ListBox && !AddTagTextBox.Focused)) + // This is only hooked up after construction, so no Construct() call needed + + if (ChooseReadmeLLPanel.ListBox.Items.Count == 0 || ChooseReadmeLLPanel.ListBox.SelectedIndex == -1) { - AddTagLLDropDown.HideAndClear(); + return; } - } - private void AddTagTextBox_TextChanged(object sender, EventArgs e) - { - if (EventsDisabled) return; + var fm = FMsDGV.GetSelectedFM(); + fm.SelectedReadme = ChooseReadmeLLPanel.ListBox.SelectedBackingItem(); + ChooseReadmeLLPanel.ShowPanel(false); - var list = FMTags.GetMatchingTagsList(AddTagTextBox.Text); - if (list.Count == 0) + if (fm.SelectedReadme.ExtIsHtml()) { - AddTagLLDropDown.HideAndClear(); + ViewHTMLReadmeLLButton.Show(this); } else { - AddTagLLDropDown.SetItemsAndShow(this, list); + SetReadmeVisible(true); + } + + if (ChooseReadmeLLPanel.ListBox.Items.Count > 1) + { + ReadmeComboBoxFillAndSelect(ChooseReadmeLLPanel.ListBox.BackingItems, fm.SelectedReadme); + ShowReadmeControls(CursorOverReadmeArea()); } + else + { + using (new DisableEvents(this)) ChooseReadmeComboBox.ClearFullItems(); + ChooseReadmeComboBox.Hide(); + } + + LoadReadme(fm); } - internal void AddTagTextBoxOrListBox_KeyDown(object sender, KeyEventArgs e) + private void ChooseReadmeComboBox_SelectedIndexChanged(object sender, EventArgs e) { - AddTagLLDropDown.Construct(this); - var box = AddTagLLDropDown.ListBox; + if (EventsDisabled) return; - switch (e.KeyCode) - { - case Keys.Up when box.Items.Count > 0: - // We can't do a switch expression on the second one, so keep them both the same for consistency - // ReSharper disable once ConvertConditionalTernaryExpressionToSwitchExpression - box.SelectedIndex = - box.SelectedIndex == -1 ? box.Items.Count - 1 : - box.SelectedIndex == 0 ? -1 : - box.SelectedIndex - 1; - e.Handled = true; - break; - case Keys.Down when box.Items.Count > 0: - box.SelectedIndex = - box.SelectedIndex == -1 ? 0 : - box.SelectedIndex == box.Items.Count - 1 ? -1 : - box.SelectedIndex + 1; - e.Handled = true; - break; - case Keys.Enter: - string catAndTag = box.SelectedIndex == -1 ? AddTagTextBox.Text : box.SelectedItem.ToString(); - AddTagOperation(FMsDGV.GetSelectedFM(), catAndTag); - break; - default: - if (sender == AddTagLLDropDown.ListBox) AddTagTextBox.Focus(); - break; - } + var fm = FMsDGV.GetSelectedFM(); + fm.SelectedReadme = ChooseReadmeComboBox.SelectedBackingItem(); + // Just load the readme; don't call DisplaySelectedFM() because that will re-get readmes and screw + // things up + LoadReadme(fm); } - internal void AddTagListBox_SelectedIndexChanged(object sender, EventArgs e) + private void ChooseReadmeComboBox_DropDownClosed(object sender, EventArgs e) { - var lb = AddTagLLDropDown.ListBox; - if (lb.SelectedIndex == -1) return; + if (!CursorOverReadmeArea()) ShowReadmeControls(false); + } - var tb = AddTagTextBox; + #endregion - using (new DisableEvents(this)) tb.Text = lb.SelectedItem.ToString(); + // Allows the readme controls to hide when the mouse moves directly from the readme area onto another + // window. General-case showing and hiding is still handled by PreFilterMessage() for reliability. + // Note: ChooseReadmePanel doesn't need this, because the readme controls aren't shown when it's visible. + internal void ReadmeArea_MouseLeave(object sender, EventArgs e) + { + IntPtr hWnd = InteropMisc.WindowFromPoint(Cursor.Position); + if (hWnd == IntPtr.Zero || Control.FromHandle(hWnd) == null) ShowReadmeControls(false); + } - if (tb.Text.Length > 0) tb.SelectionStart = tb.Text.Length; + [SuppressMessage("ReSharper", "MemberCanBeMadeStatic.Local")] + private void ReadmeRichTextBox_LinkClicked(object sender, LinkClickedEventArgs e) => Core.OpenLink(e.LinkText); + + private void ReadmeZoomInButton_Click(object sender, EventArgs e) => ReadmeRichTextBox.ZoomIn(); + + private void ReadmeZoomOutButton_Click(object sender, EventArgs e) => ReadmeRichTextBox.ZoomOut(); + + private void ReadmeResetZoomButton_Click(object sender, EventArgs e) => ReadmeRichTextBox.ResetZoomFactor(); + + private void ReadmeFullScreenButton_Click(object sender, EventArgs e) + { + MainSplitContainer.ToggleFullScreen(); + ShowReadmeControls(CursorOverReadmeArea()); } - private void RemoveTagButton_Click(object sender, EventArgs e) + private void SetReadmeVisible(bool enabled) { - if (!FMsDGV.RowSelected()) return; + ReadmeRichTextBox.Visible = enabled; + ReadmeZoomInButton.BackColor = enabled ? SystemColors.Window : SystemColors.Control; + ReadmeZoomOutButton.BackColor = enabled ? SystemColors.Window : SystemColors.Control; + ReadmeResetZoomButton.BackColor = enabled ? SystemColors.Window : SystemColors.Control; + ReadmeFullScreenButton.BackColor = enabled ? SystemColors.Window : SystemColors.Control; - var fm = FMsDGV.GetSelectedFM(); - var tv = TagsTreeView; + // In case the cursor is already over the readme when we do this + // (cause it won't show automatically if it is) + ShowReadmeControls(enabled && CursorOverReadmeArea()); + } + + private void ShowReadmeControls(bool enabled) + { + ReadmeZoomInButton.Visible = enabled; + ReadmeZoomOutButton.Visible = enabled; + ReadmeResetZoomButton.Visible = enabled; + ReadmeFullScreenButton.Visible = enabled; + ChooseReadmeComboBox.Visible = enabled && ChooseReadmeComboBox.Items.Count > 0; + } + + internal void ViewHTMLReadmeButton_Click(object sender, EventArgs e) => Core.ViewHTMLReadme(FMsDGV.GetSelectedFM()); + + public void ChangeReadmeBoxFont(bool useFixed) => ReadmeRichTextBox.SetFontType(useFixed); + + #endregion + + #region Bottom bar - bool success = FMTags.RemoveTagFromFM(fm, tv.SelectedNode?.Parent?.Text ?? "", tv.SelectedNode?.Text ?? ""); - if (!success) return; + #region Left side - DisplayFMTags(fm.Tags); - } + #region Install/Play buttons - internal void AddTagListBox_MouseUp(object sender, MouseEventArgs e) + public void ShowInstallUninstallButton(bool enabled) { - if (e.Button != MouseButtons.Left) return; - - if (AddTagLLDropDown.ListBox.SelectedIndex > -1) + if (enabled) { - AddTagOperation(FMsDGV.GetSelectedFM(), AddTagLLDropDown.ListBox.SelectedItem.ToString()); + if (!InstallUninstallFMLLButton.Constructed) + { + InstallUninstallFMLLButton.Construct(this); + InstallUninstallFMLLButton.Localize(false); + } + InstallUninstallFMLLButton.Show(); + } + else + { + InstallUninstallFMLLButton.Hide(); } } - private void AddTagOperation(FanMission fm, string catAndTag) + internal async void InstallUninstall_Play_Buttons_Click(object sender, EventArgs e) { - if (!catAndTag.CharCountIsAtLeast(':', 2) && !catAndTag.IsWhiteSpace()) + if (sender.EqualsIfNotNull(InstallUninstallFMLLButton.Button)) { - FMTags.AddTagToFM(fm, catAndTag); - DisplayFMTags(fm.Tags); + await FMInstallAndPlay.InstallOrUninstall(FMsDGV.GetSelectedFM()); + } + else if (sender == PlayFMButton) + { + await FMInstallAndPlay.InstallIfNeededAndPlay(FMsDGV.GetSelectedFM()); } - - AddTagTextBox.Clear(); - AddTagLLDropDown.HideAndClear(); } - private void AddTagButton_Click(object sender, EventArgs e) => AddTagOperation(FMsDGV.GetSelectedFM(), AddTagTextBox.Text); + #region Play original game - private void AddTagFromListButton_Click(object sender, EventArgs e) + // @GENGAMES (Play original game menu event handlers): Begin + // Because of the T2MP menu item breaking up the middle there, we can't array/index these menu items. + // Just gonna have to leave this part as-is. + private void PlayOriginalGameButton_Click(object sender, EventArgs e) { - GlobalTags.SortAndMoveMiscToEnd(); + PlayOriginalGameLLMenu.Construct(this, components); - AddTagLLMenu.Construct(this, components); - AddTagLLMenu.Menu.Items.Clear(); + PlayOriginalGameLLMenu.Thief1MenuItem.Enabled = !Config.GetGameExe(Thief1).IsEmpty(); + PlayOriginalGameLLMenu.Thief2MenuItem.Enabled = !Config.GetGameExe(Thief2).IsEmpty(); + PlayOriginalGameLLMenu.Thief2MPMenuItem.Visible = Config.T2MPDetected; + PlayOriginalGameLLMenu.Thief3MenuItem.Enabled = !Config.GetGameExe(Thief3).IsEmpty(); + PlayOriginalGameLLMenu.SS2MenuItem.Enabled = !Config.GetGameExe(SS2).IsEmpty(); - var addTagMenuItems = new List(GlobalTags.Count); - foreach (GlobalCatAndTags catAndTag in GlobalTags) - { - if (catAndTag.Tags.Count == 0) - { - var catItem = new ToolStripMenuItem(catAndTag.Category + ":"); - catItem.Click += AddTagMenuEmptyItem_Click; - addTagMenuItems.Add(catItem); - } - else - { - var catItem = new ToolStripMenuItem(catAndTag.Category.Name); - addTagMenuItems.Add(catItem); + ShowMenu(PlayOriginalGameLLMenu.Menu, PlayOriginalGameButton, MenuPos.TopRight); + } - var last = addTagMenuItems[addTagMenuItems.Count - 1]; + [SuppressMessage("ReSharper", "MemberCanBeMadeStatic.Global")] + internal void PlayOriginalGameMenuItem_Click(object sender, EventArgs e) + { + var item = (ToolStripMenuItem)sender; - if (catAndTag.Category.Name != "misc") - { - var customItem = new ToolStripMenuItem(LText.Global.CustomTagInCategory); - customItem.Click += AddTagMenuCustomItem_Click; - ((ToolStripMenuItem)last).DropDownItems.Add(customItem); - ((ToolStripMenuItem)last).DropDownItems.Add(new ToolStripSeparator()); - } + GameIndex game = + item == PlayOriginalGameLLMenu.Thief1MenuItem ? Thief1 : + item == PlayOriginalGameLLMenu.Thief2MenuItem || item == PlayOriginalGameLLMenu.Thief2MPMenuItem ? Thief2 : + item == PlayOriginalGameLLMenu.Thief3MenuItem ? Thief3 : + SS2; - foreach (GlobalCatOrTag tag in catAndTag.Tags) - { - var tagItem = new ToolStripMenuItem(tag.Name); + bool playMP = item == PlayOriginalGameLLMenu.Thief2MPMenuItem; - if (catAndTag.Category.Name == "misc") - { - tagItem.Click += AddTagMenuMiscItem_Click; - } - else - { - tagItem.Click += AddTagMenuItem_Click; - } + FMInstallAndPlay.PlayOriginalGame(game, playMP); + } + // @GENGAMES (Play original game menu event handlers): End - ((ToolStripMenuItem)last).DropDownItems.Add(tagItem); - } + #endregion + + #endregion + + private async void ScanAllFMsButton_Click(object sender, EventArgs e) + { + if (FMsViewList.Count == 0) return; + + FMScanner.ScanOptions? scanOptions = null; + bool noneSelected; + using (var f = new ScanAllFMsForm()) + { + if (f.ShowDialog() != DialogResult.OK) return; + noneSelected = f.NoneSelected; + if (!noneSelected) + { + scanOptions = FMScanner.ScanOptions.FalseDefault( + scanTitle: f.ScanOptions.ScanTitle, + scanAuthor: f.ScanOptions.ScanAuthor, + scanGameType: f.ScanOptions.ScanGameType, + scanCustomResources: f.ScanOptions.ScanCustomResources, + scanSize: f.ScanOptions.ScanSize, + scanReleaseDate: f.ScanOptions.ScanReleaseDate, + scanTags: f.ScanOptions.ScanTags); } } - AddTagLLMenu.Menu.Items.AddRange(addTagMenuItems.ToArray()); + if (noneSelected) + { + MessageBox.Show(LText.ScanAllFMsBox.NothingWasScanned, LText.AlertMessages.Alert); + return; + } - ShowMenu(AddTagLLMenu.Menu, AddTagFromListButton, MenuPos.LeftDown); + bool success = await FMScan.ScanFMs(FMsViewList, scanOptions!); + if (success) await SortAndSetFilter(forceDisplayFM: true); } - private void AddTagMenuItem_Click(object sender, EventArgs e) - { - var item = (ToolStripMenuItem)sender; - if (item.HasDropDownItems) return; + private void WebSearchButton_Click(object sender, EventArgs e) => Core.OpenWebSearchUrl(FMsDGV.GetSelectedFM().Title); - var cat = item.OwnerItem; - if (cat == null) return; + #endregion - AddTagOperation(FMsDGV.GetSelectedFM(), cat.Text + ": " + item.Text); - } + #region Right side - private void AddTagMenuCustomItem_Click(object sender, EventArgs e) + private void ImportButton_Click(object sender, EventArgs e) { - var item = (ToolStripMenuItem)sender; - - var cat = item.OwnerItem; - if (cat == null) return; - - AddTagTextBox.SetTextAndMoveCursorToEnd(cat.Text + ": "); + ImportFromLLMenu.Construct(this, components); + ShowMenu(ImportFromLLMenu.ImportFromMenu, ImportButton, MenuPos.TopLeft); } - private void AddTagMenuMiscItem_Click(object sender, EventArgs e) => AddTagTextBox.SetTextAndMoveCursorToEnd(((ToolStripMenuItem)sender).Text); - - private void AddTagMenuEmptyItem_Click(object sender, EventArgs e) => AddTagTextBox.SetTextAndMoveCursorToEnd(((ToolStripMenuItem)sender).Text + " "); - - // Just to keep things in a known state (clearing items also removes their event hookups, which is - // convenient) [SuppressMessage("ReSharper", "MemberCanBeMadeStatic.Global")] - internal void AddTagMenu_Closed(object sender, ToolStripDropDownClosedEventArgs e) + internal async void ImportMenuItems_Click(object sender, EventArgs e) { - // This handler will only be hooked up after construction, so we don't need to call Construct() - AddTagLLMenu.Menu.Items.Clear(); - } - - #endregion + ImportType importType = + sender == ImportFromLLMenu.ImportFromDarkLoaderMenuItem + ? ImportType.DarkLoader + : sender == ImportFromLLMenu.ImportFromFMSelMenuItem + ? ImportType.FMSel + : ImportType.NewDarkLoader; - #region Patch tab + await Import.ImportFrom(importType); + } - private void PatchRemoveDMLButton_Click(object sender, EventArgs e) + private async void SettingsButton_Click(object sender, EventArgs e) { - var lb = PatchDMLsListBox; - if (lb.SelectedIndex == -1) return; - - bool success = Core.RemoveDML(FMsDGV.GetSelectedFM(), lb.SelectedItem.ToString()); - if (!success) return; + var ret = Core.OpenSettings(); + if (ret.Canceled) return; - lb.RemoveAndSelectNearest(); + if (ret.FMsViewListUnscanned?.Count > 0) await FMScan.ScanNewFMs(ret.FMsViewListUnscanned); + // TODO: forceDisplayFM is always true so that this always works, but it could be smarter + // If I store the selected FM up above the Find(), I can make the FM not have to reload if + // it's still selected + if (ret.SortAndSetFilter) await SortAndSetFilter(keepSelection: ret.KeepSel, forceDisplayFM: true); } - private void PatchAddDMLButton_Click(object sender, EventArgs e) - { - var lb = PatchDMLsListBox; + #endregion - var dmlFiles = new List(); + #endregion - using (var d = new OpenFileDialog()) - { - d.Multiselect = true; - d.Filter = LText.BrowseDialogs.DMLFiles + "|*.dml"; - if (d.ShowDialog() != DialogResult.OK || d.FileNames.Length == 0) return; - dmlFiles.AddRange(d.FileNames); - } + #region FM display - foreach (string f in dmlFiles) - { - if (f.IsEmpty()) continue; + // Perpetual TODO: Make sure this clears everything including the top right tab stuff + private void ClearShownData() + { + if (FMsViewList.Count == 0) ScanAllFMsButton.Enabled = false; - bool success = Core.AddDML(FMsDGV.GetSelectedFM(), f); - if (!success) return; + FMsDGV.SetInstallUninstallMenuItemText(true); + FMsDGV.SetInstallUninstallMenuItemEnabled(false); + FMsDGV.SetDeleteFMMenuItemEnabled(false); + FMsDGV.SetOpenInDromEdMenuItemText(false); - string dmlFileName = Path.GetFileName(f); - if (!lb.Items.Cast().ToArray().ContainsI(dmlFileName)) - { - lb.Items.Add(dmlFileName); - } - } - } + InstallUninstallFMLLButton.SetSayInstall(true); + InstallUninstallFMLLButton.SetEnabled(false); - private void PatchOpenFMFolderButton_Click(object sender, EventArgs e) => Core.OpenFMFolder(FMsDGV.GetSelectedFM()); + FMsDGV.SetPlayFMMenuItemEnabled(false); + PlayFMButton.Enabled = false; - #endregion + FMsDGV.SetPlayFMInMPMenuItemVisible(false); - private void TopRightCollapseButton_Click(object sender, EventArgs e) - { - TopSplitContainer.ToggleFullScreen(); - SetTopRightCollapsedState(); - } + FMsDGV.SetOpenInDromEdVisible(false); - private void SetTopRightCollapsedState() - { - bool collapsed = TopSplitContainer.FullScreen; - TopRightTabControl.Enabled = !collapsed; - TopRightCollapseButton.ArrowDirection = collapsed ? Direction.Left : Direction.Right; - } + FMsDGV.SetExportFMIniFromFMMenuItemEnabled(false); - private void TopRightMenuButton_Click(object sender, EventArgs e) - { - TopRightLLMenu.Construct(this, components); - ShowMenu(TopRightLLMenu.Menu, TopRightMenuButton, MenuPos.BottomLeft); - } + FMsDGV.SetScanFMMenuItemEnabled(false); + + FMsDGV.SetConvertAudioRCSubMenuEnabled(false); - internal void TopRightMenu_MenuItems_Click(object sender, EventArgs e) - { - var s = (ToolStripMenuItem)sender; + // Hide instead of clear to avoid zoom factor pain + SetReadmeVisible(false); - // NULL_TODO: Null so I can assert - TabPage? tab = null; - for (int i = 0; i < TopRightTabsData.Count; i++) - { - if (s == (ToolStripMenuItem)TopRightLLMenu.Menu.Items[i]) - { - tab = _topRightTabsInOrder[i]; - break; - } - } + ChooseReadmeLLPanel.ShowPanel(false); + ViewHTMLReadmeLLButton.Hide(); + WebSearchButton.Enabled = false; - AssertR(tab != null, nameof(tab) + " is null - tab does not have a corresponding menu item"); + BlankStatsPanelWithMessage(LText.StatisticsTab.NoFMSelected); + StatsScanCustomResourcesButton.Hide(); - if (!s.Checked && TopRightTabControl.TabCount == 1) - { - s.Checked = true; - return; - } + AltTitlesLLMenu.ClearItems(); - TopRightTabControl.ShowTab(tab!, s.Checked); - } + using (new DisableEvents(this)) + { + EditFMRatingComboBox.SelectedIndex = 0; - #endregion + EditFMLanguageComboBox.ClearFullItems(); + EditFMLanguageComboBox.AddFullItem(FMLanguages.DefaultLangKey, LText.EditFMTab.DefaultLanguage); + EditFMLanguageComboBox.SelectedIndex = 0; - #region Readme + foreach (Control c in EditFMTabPage.Controls) + { + switch (c) + { + case TextBox tb: + tb.Text = ""; + break; + case DateTimePicker dtp: + dtp.Value = DateTime.Now; + dtp.Hide(); + break; + case CheckBox chk: + chk.Checked = false; + break; + } - #region Choose readme + c.Enabled = false; + } - internal void ChooseReadmeButton_Click(object sender, EventArgs e) - { - // This is only hooked up after construction, so no Construct() call needed + FMsDGV.ClearFinishedOnMenuItemChecks(); - if (ChooseReadmeLLPanel.ListBox.Items.Count == 0 || ChooseReadmeLLPanel.ListBox.SelectedIndex == -1) - { - return; - } + CommentTextBox.Text = ""; + CommentTextBox.Enabled = false; + AddTagTextBox.Text = ""; - var fm = FMsDGV.GetSelectedFM(); - fm.SelectedReadme = ChooseReadmeLLPanel.ListBox.SelectedBackingItem(); - ChooseReadmeLLPanel.ShowPanel(false); + TagsTreeView.Nodes.Clear(); - if (fm.SelectedReadme.ExtIsHtml()) - { - ViewHTMLReadmeLLButton.Show(this); - } - else - { - SetReadmeVisible(true); - } + foreach (Control c in TagsTabPage.Controls) c.Enabled = false; - if (ChooseReadmeLLPanel.ListBox.Items.Count > 1) - { - ReadmeComboBoxFillAndSelect(ChooseReadmeLLPanel.ListBox.BackingItems, fm.SelectedReadme); - ShowReadmeControls(CursorOverReadmeArea()); - } - else - { - using (new DisableEvents(this)) ChooseReadmeComboBox.ClearFullItems(); - ChooseReadmeComboBox.Hide(); + ShowPatchSection(enable: false); } - - LoadReadme(fm); } - private void ChooseReadmeComboBox_SelectedIndexChanged(object sender, EventArgs e) + private void HidePatchSectionWithMessage(string message) { - if (EventsDisabled) return; - - var fm = FMsDGV.GetSelectedFM(); - fm.SelectedReadme = ChooseReadmeComboBox.SelectedBackingItem(); - // Just load the readme; don't call DisplaySelectedFM() because that will re-get readmes and screw - // things up - LoadReadme(fm); + PatchDMLsListBox.Items.Clear(); + PatchMainPanel.Hide(); + PatchFMNotInstalledLabel.Text = message; + PatchFMNotInstalledLabel.CenterHV(PatchTabPage); + PatchFMNotInstalledLabel.Show(); } - private void ChooseReadmeComboBox_DropDownClosed(object sender, EventArgs e) + private void ShowPatchSection(bool enable) { - if (!CursorOverReadmeArea()) ShowReadmeControls(false); + PatchDMLsListBox.Items.Clear(); + PatchMainPanel.Show(); + PatchFMNotInstalledLabel.CenterHV(PatchTabPage); + PatchFMNotInstalledLabel.Hide(); + PatchMainPanel.Enabled = enable; } - #endregion - - // Allows the readme controls to hide when the mouse moves directly from the readme area onto another - // window. General-case showing and hiding is still handled by PreFilterMessage() for reliability. - // Note: ChooseReadmePanel doesn't need this, because the readme controls aren't shown when it's visible. - internal void ReadmeArea_MouseLeave(object sender, EventArgs e) + private void BlankStatsPanelWithMessage(string message) { - IntPtr hWnd = InteropMisc.WindowFromPoint(Cursor.Position); - if (hWnd == IntPtr.Zero || Control.FromHandle(hWnd) == null) ShowReadmeControls(false); + CustomResourcesLabel.Text = message; + foreach (CheckBox cb in StatsCheckBoxesPanel.Controls) cb.Checked = false; + StatsCheckBoxesPanel.Hide(); } - [SuppressMessage("ReSharper", "MemberCanBeMadeStatic.Local")] - private void ReadmeRichTextBox_LinkClicked(object sender, LinkClickedEventArgs e) => Core.OpenLink(e.LinkText); + public void UpdateRatingMenus(int rating, bool disableEvents = false) + { + using (disableEvents ? new DisableEvents(this) : null) + { + FMsDGV.SetRatingMenuItemChecked(rating); + EditFMRatingComboBox.SelectedIndex = rating + 1; + } + } - private void ReadmeZoomInButton_Click(object sender, EventArgs e) => ReadmeRichTextBox.ZoomIn(); + // @GENGAMES: Lots of game-specific code in here, but I don't see much to be done about it. + private void UpdateAllFMUIDataExceptReadme(FanMission fm) + { + bool fmIsT3 = fm.Game == Game.Thief3; + bool fmIsSS2 = fm.Game == Game.SS2; - private void ReadmeZoomOutButton_Click(object sender, EventArgs e) => ReadmeRichTextBox.ZoomOut(); + #region Toggles - private void ReadmeResetZoomButton_Click(object sender, EventArgs e) => ReadmeRichTextBox.ResetZoomFactor(); + // We should never get here when FMsList.Count == 0, but hey + if (FMsViewList.Count > 0) ScanAllFMsButton.Enabled = true; - private void ReadmeFullScreenButton_Click(object sender, EventArgs e) - { - MainSplitContainer.ToggleFullScreen(); - ShowReadmeControls(CursorOverReadmeArea()); - } + FMsDGV.SetGameSpecificFinishedOnMenuItemsText(fm.Game); + // FinishedOnUnknownMenuItem text stays the same - private void SetReadmeVisible(bool enabled) - { - ReadmeRichTextBox.Visible = enabled; - ReadmeZoomInButton.BackColor = enabled ? SystemColors.Window : SystemColors.Control; - ReadmeZoomOutButton.BackColor = enabled ? SystemColors.Window : SystemColors.Control; - ReadmeResetZoomButton.BackColor = enabled ? SystemColors.Window : SystemColors.Control; - ReadmeFullScreenButton.BackColor = enabled ? SystemColors.Window : SystemColors.Control; + bool gameIsSupported = GameIsKnownAndSupported(fm.Game); - // In case the cursor is already over the readme when we do this - // (cause it won't show automatically if it is) - ShowReadmeControls(enabled && CursorOverReadmeArea()); - } + FMsDGV.SetInstallUninstallMenuItemEnabled(gameIsSupported); + FMsDGV.SetInstallUninstallMenuItemText(!fm.Installed); + FMsDGV.SetDeleteFMMenuItemEnabled(true); + FMsDGV.SetOpenInDromEdMenuItemText(fmIsSS2); - private void ShowReadmeControls(bool enabled) - { - ReadmeZoomInButton.Visible = enabled; - ReadmeZoomOutButton.Visible = enabled; - ReadmeResetZoomButton.Visible = enabled; - ReadmeFullScreenButton.Visible = enabled; - ChooseReadmeComboBox.Visible = enabled && ChooseReadmeComboBox.Items.Count > 0; - } + // Sneaky Upgrade's FMSel allows exporting fm.ini files, so I guess Thief 3 can have those too + FMsDGV.SetExportFMIniFromFMMenuItemEnabled(gameIsSupported); - internal void ViewHTMLReadmeButton_Click(object sender, EventArgs e) => Core.ViewHTMLReadme(FMsDGV.GetSelectedFM()); + FMsDGV.SetOpenInDromEdVisible(GameIsDark(fm.Game) && Config.GetGameEditorDetectedUnsafe(fm.Game)); - #endregion + FMsDGV.SetPlayFMInMPMenuItemVisible(fm.Game == Game.Thief2 && Config.T2MPDetected); - private void FiltersFlowLayoutPanel_SizeChanged(object sender, EventArgs e) => SetFilterBarScrollButtons(); + InstallUninstallFMLLButton.SetEnabled(gameIsSupported); + InstallUninstallFMLLButton.SetSayInstall(!fm.Installed); - private void FiltersFlowLayoutPanel_Scroll(object sender, ScrollEventArgs e) => SetFilterBarScrollButtons(); + FMsDGV.SetPlayFMMenuItemEnabled(gameIsSupported); + PlayFMButton.Enabled = gameIsSupported; - // PERF_TODO: This is still called too many times on startup. - // Even though it has checks to prevent any real work from being done if not needed, I should still take - // a look at this and see if I can't make it be called only once max on startup. - // TODO: Something about the Construct() calls in this method causes the anchoring issue (when we lazy-load). - // If we just construct once at the top, it works fine. But we can't do that because then it would always - // load right away, defeating the purpose of lazy loading. Look into this. If we can solve it, that's a - // bit more time shaved off of startup. - // 2019-07-17: Lazy loading these is disabled for the moment. - private void SetFilterBarScrollButtons() - { - // Don't run this a zillion gatrillion times during init - if (EventsDisabled || !Visible) return; + FMsDGV.SetScanFMMenuItemEnabled(true); - void ShowLeft() - { - FilterBarScrollLeftButton.Location = new Point(FilterBarFLP.Location.X, FilterBarFLP.Location.Y + 1); - FilterBarScrollLeftButton.Show(); - } + FMsDGV.SetConvertAudioRCSubMenuEnabled(GameIsDark(fm.Game) && fm.Installed); - void ShowRight() - { - // Don't set it based on the filter bar width and location, otherwise it gets it slightly wrong - // the first time - FilterBarScrollRightButton.Location = new Point( - RefreshAreaToolStrip.Location.X - FilterBarScrollRightButton.Width - 4, - FilterBarFLP.Location.Y + 1); - FilterBarScrollRightButton.Show(); - } + WebSearchButton.Enabled = true; - var hs = FilterBarFLP.HorizontalScroll; - if (!hs.Visible) + foreach (Control c in EditFMTabPage.Controls) { - if (FilterBarScrollLeftButton.Visible || FilterBarScrollRightButton.Visible) + if (c == EditFMLanguageLabel || + c == EditFMLanguageComboBox || + c == EditFMScanLanguagesButton) { - FilterBarScrollLeftButton.Hide(); - FilterBarScrollRightButton.Hide(); + c.Enabled = !fmIsT3; } - } - // Keep order: Show, Hide - // Otherwise there's a small hiccup with the buttons - else if (hs.Value == 0) - { - ShowRight(); - FilterBarScrollLeftButton.Hide(); - using (new DisableEvents(this)) + else { - // Disgusting! But necessary to patch up heisenbuggy behavior with this crap. This is really - // bad in general anyway, but how else am I supposed to have show-and-hide scroll buttons with - // WinForms? Argh! - for (int i = 0; i < 8; i++) - { - InteropMisc.SendMessage(FilterBarFLP.Handle, InteropMisc.WM_SCROLL, (IntPtr)InteropMisc.SB_LINELEFT, IntPtr.Zero); - } + c.Enabled = true; } } - else if (hs.Value >= (hs.Maximum + 1) - hs.LargeChange) - { - ShowLeft(); - FilterBarScrollRightButton.Hide(); - using (new DisableEvents(this)) - { - // Ditto the above - for (int i = 0; i < 8; i++) - { - InteropMisc.SendMessage(FilterBarFLP.Handle, InteropMisc.WM_SCROLL, (IntPtr)InteropMisc.SB_LINERIGHT, IntPtr.Zero); - } - } + + CommentTextBox.Enabled = true; + foreach (Control c in TagsTabPage.Controls) c.Enabled = true; + + PatchMainPanel.Enabled = true; + + if (fm.Installed) + { + ShowPatchSection(enable: true); } else { - ShowLeft(); - ShowRight(); + HidePatchSectionWithMessage(LText.PatchTab.FMNotInstalled); } - } - private void SetUIFilterValues(Filter filter) - { - using (new DisableEvents(this)) - { - FilterBarFLP.SuspendDrawing(); - try - { - FilterTitleTextBox.Text = filter.Title; - FilterAuthorTextBox.Text = filter.Author; - FilterShowUnsupportedButton.Checked = filter.ShowUnsupported; + PatchDMLsPanel.Enabled = GameIsDark(fm.Game); - FilterByTagsButton.Checked = !filter.Tags.IsEmpty(); + #endregion - FilterByFinishedButton.Checked = filter.Finished.HasFlagFast(FinishedState.Finished); - FilterByUnfinishedButton.Checked = filter.Finished.HasFlagFast(FinishedState.Unfinished); + #region FinishedOn - FilterByRatingButton.Checked = !(filter.RatingFrom == -1 && filter.RatingTo == 10); - UpdateRatingLabel(suspendResume: false); + FMsDGV.SetFinishedOnMenuItemsChecked((Difficulty)fm.FinishedOn, fm.FinishedOnUnknown); - FilterByReleaseDateButton.Checked = filter.ReleaseDateFrom != null || filter.ReleaseDateTo != null; - UpdateDateLabel(lastPlayed: false, suspendResume: false); + #endregion - FilterByLastPlayedButton.Checked = filter.LastPlayedFrom != null || filter.LastPlayedTo != null; - UpdateDateLabel(lastPlayed: true, suspendResume: false); - } - finally - { - FilterBarFLP.ResumeDrawing(); - } - } - } + #region Custom resources - private void PositionFilterBarAfterTabs() - { - int filterBarAfterTabsX; - // In case I decide to allow a variable number of tabs based on which games are defined - if (GamesTabControl.TabCount == 0) + if (fmIsT3) { - filterBarAfterTabsX = TopBarXZero(); + BlankStatsPanelWithMessage(LText.StatisticsTab.CustomResourcesNotSupportedForThief3); + StatsScanCustomResourcesButton.Hide(); } - else + else if (!fm.ResourcesScanned) { - var lastRect = GamesTabControl.GetTabRect(GamesTabControl.TabCount - 1); - filterBarAfterTabsX = TopBarXZero() + lastRect.X + lastRect.Width + 5; + BlankStatsPanelWithMessage(LText.StatisticsTab.CustomResourcesNotScanned); + StatsScanCustomResourcesButton.Show(); } + else + { + CustomResourcesLabel.Text = LText.StatisticsTab.CustomResources; - FilterBarFLP.Location = new Point(filterBarAfterTabsX, FilterBarFLP.Location.Y); - SetFilterBarWidth(); - } + CR_MapCheckBox.Checked = FMHasResource(fm, CustomResources.Map); + CR_AutomapCheckBox.Checked = FMHasResource(fm, CustomResources.Automap); + CR_ScriptsCheckBox.Checked = FMHasResource(fm, CustomResources.Scripts); + CR_TexturesCheckBox.Checked = FMHasResource(fm, CustomResources.Textures); + CR_SoundsCheckBox.Checked = FMHasResource(fm, CustomResources.Sounds); + CR_ObjectsCheckBox.Checked = FMHasResource(fm, CustomResources.Objects); + CR_CreaturesCheckBox.Checked = FMHasResource(fm, CustomResources.Creatures); + CR_MotionsCheckBox.Checked = FMHasResource(fm, CustomResources.Motions); + CR_MoviesCheckBox.Checked = FMHasResource(fm, CustomResources.Movies); + CR_SubtitlesCheckBox.Checked = FMHasResource(fm, CustomResources.Subtitles); - private void SetFilterBarWidth() => FilterBarFLP.Width = (RefreshAreaToolStrip.Location.X - 4) - FilterBarFLP.Location.X; + StatsCheckBoxesPanel.Show(); + StatsScanCustomResourcesButton.Show(); + } - #region Filter bar controls + #endregion - // A ton of things in one event handler to cut down on async/awaits - private async void FilterWindowOpenButtons_Click(object sender, EventArgs e) - { - if (sender == FilterByReleaseDateButton || sender == FilterByLastPlayedButton) + #region Other tabs + + using (new DisableEvents(this)) { - var button = (ToolStripButtonCustom)sender; + EditFMTitleTextBox.Text = fm.Title; - bool lastPlayed = button == FilterByLastPlayedButton; - DateTime? fromDate = lastPlayed ? FMsDGV.Filter.LastPlayedFrom : FMsDGV.Filter.ReleaseDateFrom; - DateTime? toDate = lastPlayed ? FMsDGV.Filter.LastPlayedTo : FMsDGV.Filter.ReleaseDateTo; - string title = lastPlayed ? LText.DateFilterBox.LastPlayedTitleText : LText.DateFilterBox.ReleaseDateTitleText; + FillAltTitlesMenu(fm.AltTitles); - using (var f = new FilterDateForm(title, fromDate, toDate)) - { - f.Location = FilterBarFLP.PointToScreen(new Point( - FilterIconButtonsToolStrip.Location.X + button.Bounds.X, - FilterIconButtonsToolStrip.Location.Y + button.Bounds.Y + button.Height)); + EditFMAuthorTextBox.Text = fm.Author; - if (f.ShowDialog() != DialogResult.OK) return; + EditFMReleaseDateCheckBox.Checked = fm.ReleaseDate.DateTime != null; + EditFMReleaseDateDateTimePicker.Value = fm.ReleaseDate.DateTime ?? DateTime.Now; + EditFMReleaseDateDateTimePicker.Visible = fm.ReleaseDate.DateTime != null; - FMsDGV.Filter.SetDateFromAndTo(lastPlayed, f.DateFrom, f.DateTo); + EditFMLastPlayedCheckBox.Checked = fm.LastPlayed.DateTime != null; + EditFMLastPlayedDateTimePicker.Value = fm.LastPlayed.DateTime ?? DateTime.Now; + EditFMLastPlayedDateTimePicker.Visible = fm.LastPlayed.DateTime != null; - button.Checked = f.DateFrom != null || f.DateTo != null; - } + EditFMDisableAllModsCheckBox.Checked = fm.DisableAllMods; + EditFMDisabledModsTextBox.Text = fm.DisabledMods; + EditFMDisabledModsTextBox.Enabled = !fm.DisableAllMods; - UpdateDateLabel(lastPlayed); - } - else if (sender == FilterByTagsButton) - { - using var tf = new FilterTagsForm(GlobalTags, FMsDGV.Filter.Tags); - if (tf.ShowDialog() != DialogResult.OK) return; + UpdateRatingMenus(fm.Rating, disableEvents: false); - tf.TagsFilter.DeepCopyTo(FMsDGV.Filter.Tags); - FilterByTagsButton.Checked = !FMsDGV.Filter.Tags.IsEmpty(); - } - else if (sender == FilterByRatingButton) - { - bool outOfFive = Config.RatingDisplayStyle == RatingDisplayStyle.FMSel; - using (var f = new FilterRatingForm(FMsDGV.Filter.RatingFrom, FMsDGV.Filter.RatingTo, outOfFive)) - { - f.Location = - FilterBarFLP.PointToScreen(new Point( - FilterIconButtonsToolStrip.Location.X + - FilterByRatingButton.Bounds.X, - FilterIconButtonsToolStrip.Location.Y + - FilterByRatingButton.Bounds.Y + - FilterByRatingButton.Height)); + ScanAndFillLanguagesBox(fm, disableEvents: false); - if (f.ShowDialog() != DialogResult.OK) return; - FMsDGV.Filter.SetRatingFromAndTo(f.RatingFrom, f.RatingTo); - FilterByRatingButton.Checked = - !(FMsDGV.Filter.RatingFrom == -1 && FMsDGV.Filter.RatingTo == 10); - } + CommentTextBox.Text = fm.Comment.FromRNEscapes(); - UpdateRatingLabel(); + AddTagTextBox.Text = ""; + + if (GameIsDark(fm.Game) && fm.Installed) + { + PatchMainPanel.Show(); + PatchFMNotInstalledLabel.Hide(); + PatchDMLsListBox.Items.Clear(); + (bool success, List dmlFiles) = Core.GetDMLFiles(fm); + if (success) + { + foreach (string f in dmlFiles) + { + if (!f.IsEmpty()) PatchDMLsListBox.Items.Add(f); + } + } + } } - await SortAndSetFilter(); + DisplayFMTags(fm.Tags); + + #endregion } - private void UpdateDateLabel(bool lastPlayed, bool suspendResume = true) + private async Task DisplaySelectedFM(bool refreshCache = false) { - var button = lastPlayed ? FilterByLastPlayedButton : FilterByReleaseDateButton; - DateTime? fromDate = lastPlayed ? FMsDGV.Filter.LastPlayedFrom : FMsDGV.Filter.ReleaseDateFrom; - DateTime? toDate = lastPlayed ? FMsDGV.Filter.LastPlayedTo : FMsDGV.Filter.ReleaseDateTo; + FanMission fm = FMsDGV.GetSelectedFM(); - // Normally you can see the re-layout kind of "sequentially happen", this stops that and makes it - // snappy - if (suspendResume) FilterBarFLP.SuspendDrawing(); - try + if (fm.Game == Game.Null || (GameIsKnownAndSupported(fm.Game) && !fm.MarkedScanned)) { - if (button.Checked) + using (new DisableKeyPresses(this)) { - string from = fromDate == null ? "" : fromDate.Value.ToShortDateString(); - string to = toDate == null ? "" : toDate.Value.ToShortDateString(); + if (await FMScan.ScanFMs(new List { fm }, hideBoxIfZip: true)) + { + RefreshSelectedFM(rowOnly: true); + } + } + } - Lazy_ToolStripLabels.Show(this, - lastPlayed - ? Lazy_ToolStripLabel.FilterByLastPlayed - : Lazy_ToolStripLabel.FilterByReleaseDate, from + " - " + to); + UpdateAllFMUIDataExceptReadme(fm); + + var cacheData = await FMCache.GetCacheableData(fm, refreshCache); + + #region Readme + + var readmeFiles = cacheData.Readmes; + readmeFiles.Sort(); + + if (!readmeFiles.PathContainsI(fm.SelectedReadme)) fm.SelectedReadme = ""; + + using (new DisableEvents(this)) ChooseReadmeComboBox.ClearFullItems(); + + if (!fm.SelectedReadme.IsEmpty()) + { + if (readmeFiles.Count > 1) + { + ReadmeComboBoxFillAndSelect(readmeFiles, fm.SelectedReadme); } else { - Lazy_ToolStripLabels.Hide(lastPlayed - ? Lazy_ToolStripLabel.FilterByLastPlayed - : Lazy_ToolStripLabel.FilterByReleaseDate); + ChooseReadmeComboBox.Hide(); } } - finally + else // if fm.SelectedReadme is empty { - if (suspendResume) FilterBarFLP.ResumeDrawing(); + if (readmeFiles.Count == 0) + { + ReadmeRichTextBox.SetText(LText.ReadmeArea.NoReadmeFound); + + ChooseReadmeLLPanel.ShowPanel(false); + ChooseReadmeComboBox.Hide(); + ViewHTMLReadmeLLButton.Hide(); + SetReadmeVisible(true); + + return; + } + else if (readmeFiles.Count > 1) + { + string safeReadme = Core.DetectSafeReadme(readmeFiles, fm.Title); + + if (!safeReadme.IsEmpty()) + { + fm.SelectedReadme = safeReadme; + // @DIRSEP: Pass only fm.SelectedReadme, otherwise we might end up with un-normalized dirseps + ReadmeComboBoxFillAndSelect(readmeFiles, fm.SelectedReadme); + } + else + { + SetReadmeVisible(false); + ViewHTMLReadmeLLButton.Hide(); + + ChooseReadmeLLPanel.Construct(this, MainSplitContainer.Panel2); + + ChooseReadmeLLPanel.ListBox.ClearFullItems(); + foreach (string f in readmeFiles) ChooseReadmeLLPanel.ListBox.AddFullItem(f, f.GetFileNameFast()); + + ShowReadmeControls(false); + + ChooseReadmeLLPanel.ShowPanel(true); + + return; + } + } + else if (readmeFiles.Count == 1) + { + fm.SelectedReadme = readmeFiles[0]; + + ChooseReadmeComboBox.Hide(); + } } - } - - #region Filter bar right-hand controls - internal void FMsListZoomInButton_Click(object sender, EventArgs e) => ZoomFMsDGV(ZoomFMsDGVType.ZoomIn); + ChooseReadmeLLPanel.ShowPanel(false); - internal void FMsListZoomOutButton_Click(object sender, EventArgs e) => ZoomFMsDGV(ZoomFMsDGVType.ZoomOut); + LoadReadme(fm); - internal void FMsListResetZoomButton_Click(object sender, EventArgs e) => ZoomFMsDGV(ZoomFMsDGVType.ResetZoom); + #endregion + } - // A ton of things in one event handler to cut down on async/awaits - private async void SortAndSetFiltersButtons_Click(object sender, EventArgs e) + private void ScanAndFillLanguagesBox(FanMission fm, bool forceScan = false, bool disableEvents = true) { - if (sender == RefreshFromDiskButton) - { - await Core.RefreshFMsListFromDisk(); - } - else + using (disableEvents ? new DisableEvents(this) : null) { - bool senderIsTextBox = sender == FilterTitleTextBox || - sender == FilterAuthorTextBox; - bool senderIsGameButton = _filterByGameButtonsInOrder.Contains(sender); + EditFMLanguageComboBox.ClearFullItems(); + EditFMLanguageComboBox.AddFullItem(FMLanguages.DefaultLangKey, LText.EditFMTab.DefaultLanguage); - if ((senderIsTextBox || senderIsGameButton) && EventsDisabled) + if (!GameIsDark(fm.Game)) { + EditFMLanguageComboBox.SelectedIndex = 0; + fm.SelectedLang = FMLanguages.DefaultLangKey; return; } - if (sender == ClearFiltersButton) ClearUIAndCurrentInternalFilter(); + bool doScan = forceScan || !fm.LangsScanned; - // Don't keep selection for these ones, cause you want to end up on the FM you typed as soon as possible - bool keepSel = sender != FilterShowRecentAtTopButton && !senderIsTextBox; - await SortAndSetFilter(keepSelection: keepSel); - } - } + if (doScan) FMLanguages.FillFMSupportedLangs(fm); - #endregion + var langs = fm.Langs.Split(CA_Comma, StringSplitOptions.RemoveEmptyEntries).ToList(); + var sortedLangs = doScan ? langs : FMLanguages.SortLangsToSpec(langs); + fm.Langs = ""; + for (int i = 0; i < sortedLangs.Count; i++) + { + string langLower = sortedLangs[i].ToLowerInvariant(); + EditFMLanguageComboBox.AddFullItem(langLower, FMLanguages.Translated[langLower]); - #region Filter bar scroll RepeatButtons + // Rewrite the FM's lang string for cleanliness, in case it contains unsupported langs or + // other nonsense + if (!langLower.EqualsI(FMLanguages.DefaultLangKey)) + { + if (!fm.Langs.IsEmpty()) fm.Langs += ","; + fm.Langs += langLower; + } + } - // TODO: Make this use a timer or something? - // The thread is fine but the speed accumulates if you click a bunch. Not a big deal I guess but hey. - // Single-threading it would also allow it to be packed away in a custom control. - private bool _repeatButtonRunning; + if (fm.SelectedLang.EqualsI(FMLanguages.DefaultLangKey)) + { + EditFMLanguageComboBox.SelectedIndex = 0; + fm.SelectedLang = FMLanguages.DefaultLangKey; + } + else + { + int index = EditFMLanguageComboBox.BackingItems.FindIndex(x => x.EqualsI(fm.SelectedLang)); + EditFMLanguageComboBox.SelectedIndex = index == -1 ? 0 : index; - private void FilterBarScrollButtons_Click(object sender, EventArgs e) - { - if (_repeatButtonRunning) return; - int direction = sender == FilterBarScrollLeftButton ? InteropMisc.SB_LINELEFT : InteropMisc.SB_LINERIGHT; - InteropMisc.SendMessage(FilterBarFLP.Handle, InteropMisc.WM_SCROLL, (IntPtr)direction, IntPtr.Zero); + fm.SelectedLang = EditFMLanguageComboBox.SelectedIndex > -1 + ? EditFMLanguageComboBox.SelectedBackingItem() + : FMLanguages.DefaultLangKey; + } + } } - private void FilterBarScrollButtons_MouseDown(object sender, MouseEventArgs e) + private void ReadmeComboBoxFillAndSelect(List readmeFiles, string readme) { - if (e.Button != MouseButtons.Left) return; - RunRepeatButton(sender == FilterBarScrollLeftButton ? InteropMisc.SB_LINELEFT : InteropMisc.SB_LINERIGHT); + using (new DisableEvents(this)) + { + // @DIRSEP: To backslashes for each file, to prevent selection misses. + // I thought I accounted for this with backslashing the selected readme, but they all need to be. + foreach (string f in readmeFiles) ChooseReadmeComboBox.AddFullItem(f.ToBackSlashes(), f.GetFileNameFast()); + ChooseReadmeComboBox.SelectBackingIndexOf(readme); + } } - private void RunRepeatButton(int direction) + private void LoadReadme(FanMission fm) { - if (_repeatButtonRunning) return; - _repeatButtonRunning = true; - Task.Run(() => + try { - while (_repeatButtonRunning) + (string path, ReadmeType fileType) = Core.GetReadmeFileAndType(fm); + #region Debug + + // Tells me whether a readme got reloaded more than once, which should never be allowed to happen + // due to performance concerns. +#if DEBUG || (Release_Testing && !RT_StartupOnly) + DebugLabel.Text = int.TryParse(DebugLabel.Text, out int result) ? (result + 1).ToString() : "1"; +#endif + + #endregion + + if (fileType == ReadmeType.HTML) { - Invoke(new Action(() => InteropMisc.SendMessage(FilterBarFLP.Handle, InteropMisc.WM_SCROLL, (IntPtr)direction, IntPtr.Zero))); - Thread.Sleep(150); + ViewHTMLReadmeLLButton.Show(this); + SetReadmeVisible(false); + // In case the cursor is over the scroll bar area + if (CursorOverReadmeArea()) ShowReadmeControls(true); } - }); - } + else + { + SetReadmeVisible(true); + ViewHTMLReadmeLLButton.Hide(); - private void FilterBarScrollButtons_EnabledChanged(object sender, EventArgs e) => _repeatButtonRunning = false; + ReadmeRichTextBox.LoadContent(path, fileType); + } + } + catch (Exception ex) + { + Log(nameof(LoadReadme) + " failed.", ex); - private void FilterBarScrollLeftButton_MouseUp(object sender, MouseEventArgs e) => _repeatButtonRunning = false; + ViewHTMLReadmeLLButton.Hide(); + SetReadmeVisible(true); + ReadmeRichTextBox.SetText(LText.ReadmeArea.UnableToLoadReadme); + } + } - private void FilterBarScrollButtons_VisibleChanged(object sender, EventArgs e) + private void FillAltTitlesMenu(List fmAltTitles) { - var senderButton = (Button)sender; - var otherButton = senderButton == FilterBarScrollLeftButton ? FilterBarScrollRightButton : FilterBarScrollLeftButton; - if (!senderButton.Visible && otherButton.Visible) _repeatButtonRunning = false; - } + if (!AltTitlesLLMenu.Constructed) return; - #endregion + AltTitlesLLMenu.ClearItems(); - #endregion + if (fmAltTitles.Count == 0) + { + EditFMAltTitlesArrowButton.Enabled = false; + } + else + { + List altTitlesMenuItems = new List(fmAltTitles.Count); + foreach (string altTitle in fmAltTitles) + { + var item = new ToolStripMenuItem { Text = altTitle }; + item.Click += EditFMAltTitlesMenuItems_Click; + altTitlesMenuItems.Add(item); + } + AltTitlesLLMenu.AddRange(altTitlesMenuItems); - private void ResetLayoutButton_Click(object sender, EventArgs e) - { - MainSplitContainer.ResetSplitterPercent(); - TopSplitContainer.ResetSplitterPercent(); - if (FilterBarScrollRightButton.Visible) SetFilterBarScrollButtons(); + EditFMAltTitlesArrowButton.Enabled = true; + } } - #region Show menu + private void DisplayFMTags(CatAndTagsList fmTags) + { + var tv = TagsTreeView; - private enum MenuPos { LeftUp, LeftDown, TopLeft, TopRight, RightUp, RightDown, BottomLeft, BottomRight } + try + { + tv.SuspendDrawing(); + tv.Nodes.Clear(); - private static void ShowMenu(ContextMenuStrip menu, Control control, MenuPos pos, - int xOffset = 0, int yOffset = 0, bool unstickMenu = false) - { - int x = pos == MenuPos.LeftUp || pos == MenuPos.LeftDown || pos == MenuPos.TopRight || pos == MenuPos.BottomRight - ? 0 - : control.Width; + if (fmTags.Count == 0) return; - int y = pos == MenuPos.LeftDown || pos == MenuPos.TopLeft || pos == MenuPos.TopRight || pos == MenuPos.RightDown - ? 0 - : control.Height; + fmTags.SortAndMoveMiscToEnd(); - var direction = - pos == MenuPos.LeftUp || pos == MenuPos.TopLeft ? ToolStripDropDownDirection.AboveLeft : - pos == MenuPos.RightUp || pos == MenuPos.TopRight ? ToolStripDropDownDirection.AboveRight : - pos == MenuPos.LeftDown || pos == MenuPos.BottomLeft ? ToolStripDropDownDirection.BelowLeft : - ToolStripDropDownDirection.BelowRight; + foreach (CatAndTags item in fmTags) + { + tv.Nodes.Add(item.Category); + var last = tv.Nodes[tv.Nodes.Count - 1]; + foreach (string tag in item.Tags) last.Nodes.Add(tag); + } - if (unstickMenu) + tv.ExpandAll(); + } + finally { - // If menu is stuck to a submenu or something, we need to show and hide it once to get it unstuck, - // then carry on with the final show below - menu.Show(); - menu.Hide(); + tv.ResumeDrawing(); } - - menu.Show(control, new Point(x + xOffset, y + yOffset), direction); } #endregion @@ -4151,62 +4216,9 @@ private void RefreshAreaToolStrip_Paint(object sender, PaintEventArgs e) #endregion - private void MainMenuButton_Click(object sender, EventArgs e) - { - MainLLMenu.Construct(this, components); - ShowMenu(MainLLMenu.Menu, MainMenuButton, MenuPos.BottomRight, xOffset: -2, yOffset: 2); - } - - [SuppressMessage("ReSharper", "MemberCanBeMadeStatic.Global")] - internal void MainMenu_GameVersionsMenuItem_Click(object sender, EventArgs e) - { - using var f = new GameVersionsForm(); - f.ShowDialog(); - } - internal void FMsListStatsMenuItem_Click(object sender, EventArgs e) { } - - private void MainMenuButton_Enter(object sender, EventArgs e) => HideFocusRectangle(MainMenuButton); - - internal void FilterControlsMenuItems_Click(object sender, EventArgs e) - { - var s = (ToolStripMenuItem)sender; - - try - { - FilterBarFLP.SuspendDrawing(); - - var filterItems = _hideableFilterControls[(int)s.Tag]; - for (int i = 0; i < filterItems.Length; i++) - { - switch (filterItems[i]) - { - case Control control: - control.Visible = s.Checked; - break; - case ToolStripItem toolStripItem: - toolStripItem.Visible = s.Checked; - break; - } - } - } - finally - { - FilterBarFLP.ResumeDrawing(); - } - } - - private void FilterControlsShowHideButton_Click(object sender, EventArgs e) - { - FilterControlsLLMenu.Construct(this, components); - ShowMenu(FilterControlsLLMenu.Menu, - FilterIconButtonsToolStrip, - MenuPos.RightDown, - -FilterControlsShowHideButton.Width, - FilterIconButtonsToolStrip.Height); - } } } diff --git a/AngelLoader/Forms/MainForm_InitManual.cs b/AngelLoader/Forms/MainForm_InitManual.cs index 71b6a6fa6..fb0aeae85 100644 --- a/AngelLoader/Forms/MainForm_InitManual.cs +++ b/AngelLoader/Forms/MainForm_InitManual.cs @@ -578,8 +578,8 @@ private void InitComponentManual() FilterBarFLP.TabIndex = 11; FilterBarFLP.WrapContents = false; - FilterBarFLP.Scroll += FiltersFlowLayoutPanel_Scroll; - FilterBarFLP.SizeChanged += FiltersFlowLayoutPanel_SizeChanged; + FilterBarFLP.Scroll += FilterBarFLP_Scroll; + FilterBarFLP.SizeChanged += FilterBarFLP_SizeChanged; FilterBarFLP.Paint += FilterBarFLP_Paint; // // FilterGameButtonsToolStrip diff --git a/AngelLoader/Forms/MainForm_Progress.cs b/AngelLoader/Forms/MainForm_Progress.cs index bde9728c7..e03e0b301 100644 --- a/AngelLoader/Forms/MainForm_Progress.cs +++ b/AngelLoader/Forms/MainForm_Progress.cs @@ -6,9 +6,6 @@ namespace AngelLoader.Forms { public sealed partial class MainForm { - // You know the drill -#pragma warning disable IDE0069 // Disposable fields should be disposed - // Not great code really, but works. private ProgressPanel? ProgressBox;