From aa9f4bf57972cf391e8598f8ba4e2872d0b8b87c Mon Sep 17 00:00:00 2001 From: w4po Date: Sat, 23 Dec 2023 11:04:31 +0200 Subject: [PATCH] =?UTF-8?q?=E2=84=B9=EF=B8=8F=20Major=20Update=20v1.3.0:?= =?UTF-8?q?=20Enhancing=20Functionality=20and=20UX=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🔍 Overview: This update focuses on improving functionality and user experience, introducing the all-new `HotKeyProfiles` feature for efficient management of hotkeys and windows. 🔄 Changes: 1️⃣ Created a new `MainForm` class to serve as the application's main form, replacing the now-removed `TrayIcon.cs` file. 2️⃣ Implemented customizable `HotKeyProfiles`: - Users can now assign custom actions, such as opening a specific path, to hotkeys like Win + E or any of your choice. - Users can now duplicate the current tab when pressing for example CTRL + D. - closes #5. 3️⃣ The `HotKeyProfileControl` UserControl: - Handles the visual representation and management of hotkey profiles. - Includes methods for initializing controls, updating states, paths, scopes, etc. 4️⃣ Added new interfaces, classes, and enums: - `IHook`, `ClipboardManager`, `HookManager`, `InteractionManager`, `RegistryManager`, `SettingsManager`, `HotKeyAction`, `HotKeyProfile`, `HotkeyScope`, `InteractionMethod`, and `WindowHeaderElements`. 5️⃣ Utilized third-party libraries for improved aesthetics and functionality: - [MaterialSkin.2](https://github.com/leocb/MaterialSkin) for a modern and visually appealing form design. - [H.Hooks](https://github.com/HavenDV/H.Hooks) for keyboard key hooking, enabling efficient detection of hotkeys. 🌐 Impact: These changes contribute to a more streamlined and customizable user experience, providing greater control over hotkey assignments and window interactions. 👏 Thanks you! --- .github/ISSUE_TEMPLATE/bug-report.yml | 4 +- .github/ISSUE_TEMPLATE/feature_request.yml | 1 - ExplorerTabUtility/App.config | 13 +- ExplorerTabUtility/ExplorerTabUtility.csproj | 6 +- .../Forms/HotKeyProfileControl.Designer.cs | 455 +++++++++++ .../Forms/HotKeyProfileControl.cs | 193 +++++ .../Forms/HotKeyProfileControl.resx | 123 +++ ExplorerTabUtility/Forms/MainForm.Designer.cs | 211 +++++ ExplorerTabUtility/Forms/MainForm.cs | 374 +++++++++ ExplorerTabUtility/Forms/MainForm.resx | 763 ++++++++++++++++++ ExplorerTabUtility/Forms/TrayIcon.cs | 275 ------- ExplorerTabUtility/Helpers/Constants.cs | 3 + ExplorerTabUtility/Helpers/Helper.cs | 123 ++- ExplorerTabUtility/Hooks/IHook.cs | 10 + ExplorerTabUtility/Hooks/Keyboard.cs | 173 ++-- ExplorerTabUtility/Hooks/Shell32.cs | 69 +- ExplorerTabUtility/Hooks/UiAutomation.cs | 142 ++-- .../Managers/ClipboardManager.cs | 231 ++++++ ExplorerTabUtility/Managers/HookManager.cs | 38 + .../Managers/InteractionManager.cs | 183 +++++ .../Managers/RegistryManager.cs | 43 + .../Managers/SettingsManager.cs | 52 ++ ExplorerTabUtility/Models/HotKeyAction.cs | 7 + ExplorerTabUtility/Models/HotKeyProfile.cs | 26 + ExplorerTabUtility/Models/HotkeyScope.cs | 7 + .../Models/InteractionMethod.cs | 7 + ExplorerTabUtility/Models/Window.cs | 22 +- .../Models/WindowHeaderElements.cs | 11 + ExplorerTabUtility/Models/WindowHookVia.cs | 7 - ExplorerTabUtility/Program.cs | 10 +- .../Properties/Settings.Designer.cs | 38 +- .../Properties/Settings.settings | 13 +- ExplorerTabUtility/WinAPI/Delegates.cs | 6 +- ExplorerTabUtility/WinAPI/WinApi.cs | 166 +++- 34 files changed, 3271 insertions(+), 534 deletions(-) create mode 100644 ExplorerTabUtility/Forms/HotKeyProfileControl.Designer.cs create mode 100644 ExplorerTabUtility/Forms/HotKeyProfileControl.cs create mode 100644 ExplorerTabUtility/Forms/HotKeyProfileControl.resx create mode 100644 ExplorerTabUtility/Forms/MainForm.Designer.cs create mode 100644 ExplorerTabUtility/Forms/MainForm.cs create mode 100644 ExplorerTabUtility/Forms/MainForm.resx delete mode 100644 ExplorerTabUtility/Forms/TrayIcon.cs create mode 100644 ExplorerTabUtility/Hooks/IHook.cs create mode 100644 ExplorerTabUtility/Managers/ClipboardManager.cs create mode 100644 ExplorerTabUtility/Managers/HookManager.cs create mode 100644 ExplorerTabUtility/Managers/InteractionManager.cs create mode 100644 ExplorerTabUtility/Managers/RegistryManager.cs create mode 100644 ExplorerTabUtility/Managers/SettingsManager.cs create mode 100644 ExplorerTabUtility/Models/HotKeyAction.cs create mode 100644 ExplorerTabUtility/Models/HotKeyProfile.cs create mode 100644 ExplorerTabUtility/Models/HotkeyScope.cs create mode 100644 ExplorerTabUtility/Models/InteractionMethod.cs create mode 100644 ExplorerTabUtility/Models/WindowHeaderElements.cs delete mode 100644 ExplorerTabUtility/Models/WindowHookVia.cs diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index c8c4f62..7838670 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -15,8 +15,7 @@ body: description: Increase the chances of your issue being accepted by ensuring it has not been raised before. options: - label: I have checked "open" AND "closed" issues and this is not a duplicate - validations: - required: true + required: true - type: textarea id: description attributes: @@ -60,7 +59,6 @@ body: options: - "No" - "Yes" - default: 0 validations: required: true - type: textarea diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 9e41d2c..2f96d25 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -26,7 +26,6 @@ body: options: - "No" - "Yes" - default: 0 validations: required: false - type: textarea diff --git a/ExplorerTabUtility/App.config b/ExplorerTabUtility/App.config index 9d29ca9..832a850 100644 --- a/ExplorerTabUtility/App.config +++ b/ExplorerTabUtility/App.config @@ -7,17 +7,20 @@ - - True + + 0 True - + True - - False + + [{"Name":"Home","HotKeys":[91,69],"Scope":0,"Action":0,"Path":"","IsHandled":true,"IsEnabled":true,"Delay":0},{"Name":"Duplicate","HotKeys":[17,68],"Scope":1,"Action":1,"Path":null,"IsHandled":true,"IsEnabled":true,"Delay":0}] + + + True diff --git a/ExplorerTabUtility/ExplorerTabUtility.csproj b/ExplorerTabUtility/ExplorerTabUtility.csproj index f872794..1160fb6 100644 --- a/ExplorerTabUtility/ExplorerTabUtility.csproj +++ b/ExplorerTabUtility/ExplorerTabUtility.csproj @@ -4,10 +4,9 @@ WinExe net7.0-windows;net481 enable - true True Icon.ico - 1.2.0 + 1.3.0 latest Explorer Tab Utility w4po @@ -24,7 +23,10 @@ + + + diff --git a/ExplorerTabUtility/Forms/HotKeyProfileControl.Designer.cs b/ExplorerTabUtility/Forms/HotKeyProfileControl.Designer.cs new file mode 100644 index 0000000..eaa0280 --- /dev/null +++ b/ExplorerTabUtility/Forms/HotKeyProfileControl.Designer.cs @@ -0,0 +1,455 @@ +namespace ExplorerTabUtility.Forms; + +partial class HotKeyProfileControl +{ + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + components = new System.ComponentModel.Container(); + pnlMain = new System.Windows.Forms.Panel(); + btnCollapse = new MaterialSkin.Controls.MaterialButton(); + splitter5 = new System.Windows.Forms.Splitter(); + cbAction = new MaterialSkin.Controls.MaterialComboBox(); + splitter4 = new System.Windows.Forms.Splitter(); + cbScope = new MaterialSkin.Controls.MaterialComboBox(); + splitter3 = new System.Windows.Forms.Splitter(); + txtHotKeys = new MaterialSkin.Controls.MaterialTextBox(); + splitter2 = new System.Windows.Forms.Splitter(); + txtName = new MaterialSkin.Controls.MaterialTextBox(); + splitter1 = new System.Windows.Forms.Splitter(); + cbEnabled = new MaterialSkin.Controls.MaterialSwitch(); + pnlMore = new System.Windows.Forms.Panel(); + btnDelete = new MaterialSkin.Controls.MaterialButton(); + splitter8 = new System.Windows.Forms.Splitter(); + cbHandled = new MaterialSkin.Controls.MaterialSwitch(); + splitter7 = new System.Windows.Forms.Splitter(); + sDelay = new MaterialSkin.Controls.MaterialSlider(); + splitter6 = new System.Windows.Forms.Splitter(); + txtPath = new MaterialSkin.Controls.MaterialTextBox(); + toolTip = new System.Windows.Forms.ToolTip(components); + pnlMain.SuspendLayout(); + pnlMore.SuspendLayout(); + SuspendLayout(); + // + // pnlMain + // + pnlMain.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + pnlMain.Controls.Add(btnCollapse); + pnlMain.Controls.Add(splitter5); + pnlMain.Controls.Add(cbAction); + pnlMain.Controls.Add(splitter4); + pnlMain.Controls.Add(cbScope); + pnlMain.Controls.Add(splitter3); + pnlMain.Controls.Add(txtHotKeys); + pnlMain.Controls.Add(splitter2); + pnlMain.Controls.Add(txtName); + pnlMain.Controls.Add(splitter1); + pnlMain.Controls.Add(cbEnabled); + pnlMain.Location = new System.Drawing.Point(0, 0); + pnlMain.Margin = new System.Windows.Forms.Padding(0); + pnlMain.Name = "pnlMain"; + pnlMain.Padding = new System.Windows.Forms.Padding(10, 0, 0, 0); + pnlMain.Size = new System.Drawing.Size(725, 37); + pnlMain.TabIndex = 0; + // + // btnCollapse + // + btnCollapse.AutoSize = false; + btnCollapse.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; + btnCollapse.Density = MaterialSkin.Controls.MaterialButton.MaterialButtonDensity.Default; + btnCollapse.Depth = 0; + btnCollapse.Dock = System.Windows.Forms.DockStyle.Left; + btnCollapse.HighEmphasis = true; + btnCollapse.Icon = null; + btnCollapse.Location = new System.Drawing.Point(687, 0); + btnCollapse.Margin = new System.Windows.Forms.Padding(5, 7, 5, 7); + btnCollapse.MouseState = MaterialSkin.MouseState.HOVER; + btnCollapse.Name = "btnCollapse"; + btnCollapse.NoAccentTextColor = System.Drawing.Color.Empty; + btnCollapse.Size = new System.Drawing.Size(36, 37); + btnCollapse.TabIndex = 4; + btnCollapse.Text = "ᐯ"; + toolTip.SetToolTip(btnCollapse, "Show more."); + btnCollapse.Type = MaterialSkin.Controls.MaterialButton.MaterialButtonType.Outlined; + btnCollapse.UseAccentColor = true; + btnCollapse.UseVisualStyleBackColor = true; + btnCollapse.Click += BtnCollapse_Click; + // + // splitter5 + // + splitter5.Location = new System.Drawing.Point(682, 0); + splitter5.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + splitter5.MinSize = 120; + splitter5.Name = "splitter5"; + splitter5.Size = new System.Drawing.Size(5, 37); + splitter5.TabIndex = 12; + splitter5.TabStop = false; + // + // cbAction + // + cbAction.AutoResize = false; + cbAction.BackColor = System.Drawing.Color.FromArgb(255, 255, 255); + cbAction.Depth = 0; + cbAction.Dock = System.Windows.Forms.DockStyle.Left; + cbAction.DrawMode = System.Windows.Forms.DrawMode.OwnerDrawVariable; + cbAction.DropDownHeight = 118; + cbAction.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + cbAction.DropDownWidth = 121; + cbAction.Font = new System.Drawing.Font("Microsoft Sans Serif", 14F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Pixel); + cbAction.ForeColor = System.Drawing.Color.FromArgb(222, 0, 0, 0); + cbAction.FormattingEnabled = true; + cbAction.Hint = "Action"; + cbAction.IntegralHeight = false; + cbAction.ItemHeight = 29; + cbAction.Items.AddRange(new object[] { "Open", "ReopenClosed", "Duplicate", "Write" }); + cbAction.Location = new System.Drawing.Point(529, 0); + cbAction.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + cbAction.MaxDropDownItems = 4; + cbAction.MouseState = MaterialSkin.MouseState.OUT; + cbAction.Name = "cbAction"; + cbAction.Size = new System.Drawing.Size(153, 35); + cbAction.StartIndex = 0; + cbAction.TabIndex = 3; + toolTip.SetToolTip(cbAction, "What to do if the HotKeys got pressed."); + cbAction.UseTallSize = false; + cbAction.SelectedIndexChanged += CbAction_SelectedIndexChanged; + // + // splitter4 + // + splitter4.Location = new System.Drawing.Point(525, 0); + splitter4.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + splitter4.MinSize = 120; + splitter4.Name = "splitter4"; + splitter4.Size = new System.Drawing.Size(4, 37); + splitter4.TabIndex = 13; + splitter4.TabStop = false; + // + // cbScope + // + cbScope.AutoResize = false; + cbScope.BackColor = System.Drawing.Color.FromArgb(255, 255, 255); + cbScope.Depth = 0; + cbScope.Dock = System.Windows.Forms.DockStyle.Left; + cbScope.DrawMode = System.Windows.Forms.DrawMode.OwnerDrawVariable; + cbScope.DropDownHeight = 118; + cbScope.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + cbScope.DropDownWidth = 121; + cbScope.Font = new System.Drawing.Font("Microsoft Sans Serif", 14F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Pixel); + cbScope.ForeColor = System.Drawing.Color.FromArgb(222, 0, 0, 0); + cbScope.FormattingEnabled = true; + cbScope.Hint = "Scope"; + cbScope.IntegralHeight = false; + cbScope.ItemHeight = 29; + cbScope.Items.AddRange(new object[] { "Global", "FileExplorer" }); + cbScope.Location = new System.Drawing.Point(390, 0); + cbScope.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + cbScope.MaxDropDownItems = 4; + cbScope.MouseState = MaterialSkin.MouseState.OUT; + cbScope.Name = "cbScope"; + cbScope.Size = new System.Drawing.Size(135, 35); + cbScope.StartIndex = 0; + cbScope.TabIndex = 2; + toolTip.SetToolTip(cbScope, "Scope of the hotkeys, whether it's Global or only if the FileExplorer is focused."); + cbScope.UseTallSize = false; + cbScope.SelectedIndexChanged += CbScope_SelectedIndexChanged; + // + // splitter3 + // + splitter3.Location = new System.Drawing.Point(386, 0); + splitter3.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + splitter3.MinSize = 120; + splitter3.Name = "splitter3"; + splitter3.Size = new System.Drawing.Size(4, 37); + splitter3.TabIndex = 14; + splitter3.TabStop = false; + // + // txtHotKeys + // + txtHotKeys.AnimateReadOnly = true; + txtHotKeys.BorderStyle = System.Windows.Forms.BorderStyle.None; + txtHotKeys.Depth = 0; + txtHotKeys.DetectUrls = false; + txtHotKeys.Dock = System.Windows.Forms.DockStyle.Left; + txtHotKeys.Font = new System.Drawing.Font("Roboto", 16F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Pixel); + txtHotKeys.Hint = "HotKeys"; + txtHotKeys.LeadingIcon = null; + txtHotKeys.LeaveOnEnterKey = true; + txtHotKeys.Location = new System.Drawing.Point(231, 0); + txtHotKeys.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + txtHotKeys.MaxLength = 50; + txtHotKeys.MouseState = MaterialSkin.MouseState.OUT; + txtHotKeys.Multiline = false; + txtHotKeys.Name = "txtHotKeys"; + txtHotKeys.ReadOnly = true; + txtHotKeys.ShortcutsEnabled = false; + txtHotKeys.Size = new System.Drawing.Size(155, 36); + txtHotKeys.TabIndex = 1; + txtHotKeys.Text = ""; + toolTip.SetToolTip(txtHotKeys, "HotKeys to listen for."); + txtHotKeys.TrailingIcon = null; + txtHotKeys.UseTallSize = false; + txtHotKeys.Enter += TxtHotKeys_Enter; + txtHotKeys.KeyDown += TxtHotKeys_KeyDown; + txtHotKeys.Leave += TxtHotKeys_Leave; + // + // splitter2 + // + splitter2.Location = new System.Drawing.Point(227, 0); + splitter2.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + splitter2.MinSize = 120; + splitter2.Name = "splitter2"; + splitter2.Size = new System.Drawing.Size(4, 37); + splitter2.TabIndex = 15; + splitter2.TabStop = false; + // + // txtName + // + txtName.AnimateReadOnly = false; + txtName.BorderStyle = System.Windows.Forms.BorderStyle.None; + txtName.Depth = 0; + txtName.Dock = System.Windows.Forms.DockStyle.Left; + txtName.Font = new System.Drawing.Font("Roboto", 16F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Pixel); + txtName.Hint = "Name"; + txtName.LeadingIcon = null; + txtName.LeaveOnEnterKey = true; + txtName.Location = new System.Drawing.Point(72, 0); + txtName.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + txtName.MaxLength = 50; + txtName.MouseState = MaterialSkin.MouseState.OUT; + txtName.Multiline = false; + txtName.Name = "txtName"; + txtName.Size = new System.Drawing.Size(155, 36); + txtName.TabIndex = 0; + txtName.Text = ""; + toolTip.SetToolTip(txtName, "Name of the profile."); + txtName.TrailingIcon = null; + txtName.UseTallSize = false; + txtName.TextChanged += TxtName_TextChanged; + // + // splitter1 + // + splitter1.Location = new System.Drawing.Point(68, 0); + splitter1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + splitter1.MinSize = 50; + splitter1.Name = "splitter1"; + splitter1.Size = new System.Drawing.Size(4, 37); + splitter1.TabIndex = 16; + splitter1.TabStop = false; + // + // cbEnabled + // + cbEnabled.AutoSize = true; + cbEnabled.Checked = true; + cbEnabled.CheckState = System.Windows.Forms.CheckState.Checked; + cbEnabled.Depth = 0; + cbEnabled.Dock = System.Windows.Forms.DockStyle.Left; + cbEnabled.Location = new System.Drawing.Point(10, 0); + cbEnabled.Margin = new System.Windows.Forms.Padding(0); + cbEnabled.MouseLocation = new System.Drawing.Point(-1, -1); + cbEnabled.MouseState = MaterialSkin.MouseState.HOVER; + cbEnabled.Name = "cbEnabled"; + cbEnabled.Ripple = true; + cbEnabled.Size = new System.Drawing.Size(58, 37); + cbEnabled.TabIndex = 8; + toolTip.SetToolTip(cbEnabled, "Enable the profile."); + cbEnabled.UseVisualStyleBackColor = true; + cbEnabled.CheckedChanged += CbEnabled_CheckedChanged; + // + // pnlMore + // + pnlMore.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + pnlMore.Controls.Add(btnDelete); + pnlMore.Controls.Add(splitter8); + pnlMore.Controls.Add(cbHandled); + pnlMore.Controls.Add(splitter7); + pnlMore.Controls.Add(sDelay); + pnlMore.Controls.Add(splitter6); + pnlMore.Controls.Add(txtPath); + pnlMore.Location = new System.Drawing.Point(0, 45); + pnlMore.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + pnlMore.Name = "pnlMore"; + pnlMore.Padding = new System.Windows.Forms.Padding(72, 0, 0, 0); + pnlMore.Size = new System.Drawing.Size(725, 37); + pnlMore.TabIndex = 8; + // + // btnDelete + // + btnDelete.AutoSize = false; + btnDelete.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; + btnDelete.Density = MaterialSkin.Controls.MaterialButton.MaterialButtonDensity.Default; + btnDelete.Depth = 0; + btnDelete.Dock = System.Windows.Forms.DockStyle.Left; + btnDelete.HighEmphasis = true; + btnDelete.Icon = null; + btnDelete.Location = new System.Drawing.Point(650, 0); + btnDelete.Margin = new System.Windows.Forms.Padding(4, 6, 4, 6); + btnDelete.MouseState = MaterialSkin.MouseState.HOVER; + btnDelete.Name = "btnDelete"; + btnDelete.NoAccentTextColor = System.Drawing.Color.Empty; + btnDelete.Size = new System.Drawing.Size(63, 37); + btnDelete.TabIndex = 21; + btnDelete.Text = "Delete"; + toolTip.SetToolTip(btnDelete, "Delete current profile."); + btnDelete.Type = MaterialSkin.Controls.MaterialButton.MaterialButtonType.Text; + btnDelete.UseAccentColor = true; + btnDelete.UseVisualStyleBackColor = true; + btnDelete.Click += BtnDelete_Click; + // + // splitter8 + // + splitter8.Location = new System.Drawing.Point(646, 0); + splitter8.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + splitter8.MinSize = 80; + splitter8.Name = "splitter8"; + splitter8.Size = new System.Drawing.Size(4, 37); + splitter8.TabIndex = 22; + splitter8.TabStop = false; + // + // cbHandled + // + cbHandled.AutoSize = true; + cbHandled.Checked = true; + cbHandled.CheckState = System.Windows.Forms.CheckState.Checked; + cbHandled.Depth = 0; + cbHandled.Dock = System.Windows.Forms.DockStyle.Left; + cbHandled.Location = new System.Drawing.Point(529, 0); + cbHandled.Margin = new System.Windows.Forms.Padding(0); + cbHandled.MouseLocation = new System.Drawing.Point(-1, -1); + cbHandled.MouseState = MaterialSkin.MouseState.HOVER; + cbHandled.Name = "cbHandled"; + cbHandled.Ripple = true; + cbHandled.Size = new System.Drawing.Size(117, 37); + cbHandled.TabIndex = 7; + cbHandled.Text = "Handled"; + toolTip.SetToolTip(cbHandled, "Prevent further processing of the hotkeys in other applications."); + cbHandled.UseVisualStyleBackColor = true; + cbHandled.CheckedChanged += CbHandled_CheckedChanged; + // + // splitter7 + // + splitter7.Location = new System.Drawing.Point(525, 0); + splitter7.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + splitter7.MinSize = 100; + splitter7.Name = "splitter7"; + splitter7.Size = new System.Drawing.Size(4, 37); + splitter7.TabIndex = 19; + splitter7.TabStop = false; + // + // sDelay + // + sDelay.Depth = 0; + sDelay.Dock = System.Windows.Forms.DockStyle.Left; + sDelay.Font = new System.Drawing.Font("Roboto", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Pixel); + sDelay.FontType = MaterialSkin.MaterialSkinManager.fontType.Caption; + sDelay.ForeColor = System.Drawing.Color.FromArgb(222, 0, 0, 0); + sDelay.Location = new System.Drawing.Point(390, 0); + sDelay.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + sDelay.MouseState = MaterialSkin.MouseState.HOVER; + sDelay.Name = "sDelay"; + sDelay.RangeMax = 10000; + sDelay.ShowText = false; + sDelay.Size = new System.Drawing.Size(135, 40); + sDelay.TabIndex = 6; + sDelay.Text = "Delay"; + toolTip.SetToolTip(sDelay, "Delay before doing the action."); + sDelay.Value = 0; + sDelay.ValueSuffix = " MS"; + sDelay.onValueChanged += SDelay_ValueChanged; + // + // splitter6 + // + splitter6.Location = new System.Drawing.Point(386, 0); + splitter6.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + splitter6.MinExtra = 450; + splitter6.MinSize = 155; + splitter6.Name = "splitter6"; + splitter6.Size = new System.Drawing.Size(4, 37); + splitter6.TabIndex = 20; + splitter6.TabStop = false; + // + // txtPath + // + txtPath.AnimateReadOnly = false; + txtPath.BorderStyle = System.Windows.Forms.BorderStyle.None; + txtPath.Depth = 0; + txtPath.Dock = System.Windows.Forms.DockStyle.Left; + txtPath.Font = new System.Drawing.Font("Roboto", 16F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Pixel); + txtPath.Hint = "Path"; + txtPath.LeadingIcon = null; + txtPath.LeaveOnEnterKey = true; + txtPath.Location = new System.Drawing.Point(72, 0); + txtPath.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + txtPath.MaxLength = 50; + txtPath.MouseState = MaterialSkin.MouseState.OUT; + txtPath.Multiline = false; + txtPath.Name = "txtPath"; + txtPath.Size = new System.Drawing.Size(314, 36); + txtPath.TabIndex = 5; + txtPath.Text = ""; + toolTip.SetToolTip(txtPath, "Folder path to open."); + txtPath.TrailingIcon = null; + txtPath.UseTallSize = false; + txtPath.TextChanged += TxtPath_TextChanged; + // + // HotKeyProfileControl + // + AutoScaleMode = System.Windows.Forms.AutoScaleMode.None; + Controls.Add(pnlMore); + Controls.Add(pnlMain); + Margin = new System.Windows.Forms.Padding(3, 3, 3, 10); + Name = "HotKeyProfileControl"; + Size = new System.Drawing.Size(725, 37); + pnlMain.ResumeLayout(false); + pnlMain.PerformLayout(); + pnlMore.ResumeLayout(false); + pnlMore.PerformLayout(); + ResumeLayout(false); + } + + #endregion + + private System.Windows.Forms.Panel pnlMain; + private MaterialSkin.Controls.MaterialSwitch cbEnabled; + private System.Windows.Forms.Splitter splitter1; + private MaterialSkin.Controls.MaterialTextBox txtName; + private MaterialSkin.Controls.MaterialTextBox txtHotKeys; + private System.Windows.Forms.Splitter splitter2; + private System.Windows.Forms.Splitter splitter3; + private MaterialSkin.Controls.MaterialComboBox cbScope; + private MaterialSkin.Controls.MaterialComboBox cbAction; + private System.Windows.Forms.Splitter splitter4; + private System.Windows.Forms.Splitter splitter5; + private MaterialSkin.Controls.MaterialButton btnCollapse; + private System.Windows.Forms.Panel pnlMore; + private System.Windows.Forms.Splitter splitter6; + private MaterialSkin.Controls.MaterialTextBox txtPath; + private MaterialSkin.Controls.MaterialSlider sDelay; + private MaterialSkin.Controls.MaterialSwitch cbHandled; + private System.Windows.Forms.Splitter splitter7; + private System.Windows.Forms.ToolTip toolTip; + private MaterialSkin.Controls.MaterialButton btnDelete; + private System.Windows.Forms.Splitter splitter8; +} \ No newline at end of file diff --git a/ExplorerTabUtility/Forms/HotKeyProfileControl.cs b/ExplorerTabUtility/Forms/HotKeyProfileControl.cs new file mode 100644 index 0000000..f09bf9a --- /dev/null +++ b/ExplorerTabUtility/Forms/HotKeyProfileControl.cs @@ -0,0 +1,193 @@ +using System; +using System.Linq; +using System.Windows.Forms; +using ExplorerTabUtility.Models; +using H.Hooks; + +namespace ExplorerTabUtility.Forms; + +public partial class HotKeyProfileControl : UserControl +{ + // Fields + private bool _isCollapsed; + private readonly int _collapsedHeight; + private readonly HotKeyProfile _profile; + private readonly Action? _removeAction; + private readonly Action? _keyboardHookStarted; + private readonly Action? _keyboardHookStopped; + private LowLevelKeyboardHook? _lowLevelKeyboardHook; + // Constants + private const int ExpandedHeight = 76; + // Properties + public bool IsEnabled + { + get => cbEnabled.Checked; + set + { + if (cbEnabled.Checked == value) return; + cbEnabled.Checked = value; + } + } + public bool IsCollapsed + { + get => _isCollapsed; + set + { + if (_isCollapsed == value) return; + _isCollapsed = value; + ToggleCollapse(); + } + } + + // Constructor + public HotKeyProfileControl(HotKeyProfile profile, Action? removeAction = default, Action? keyboardHookStarted = default, Action? keyboardHookStopped = default) + { + InitializeComponent(); + Tag = profile; + _profile = profile; + _collapsedHeight = Height; + _removeAction = removeAction; + _keyboardHookStarted = keyboardHookStarted; + _keyboardHookStopped = keyboardHookStopped; + InitializeControls(); + } + // Initialize controls with hot key profile data + private void InitializeControls() + { + cbEnabled.Checked = _profile.IsEnabled; + txtName.Text = _profile.Name ?? string.Empty; + + if (_profile.HotKeys != null) + txtHotKeys.Text = string.Join(" + ", _profile.HotKeys.Select(k => k.ToFixedString())); + + SetComboBoxDataSourceQuietly(cbScope, Enum.GetValues(typeof(HotkeyScope)), CbScope_SelectedIndexChanged); + SetComboBoxDataSourceQuietly(cbAction, Enum.GetValues(typeof(HotKeyAction)), CbAction_SelectedIndexChanged); + cbScope.SelectedItem = _profile.Scope; + cbAction.SelectedItem = _profile.Action; + txtPath.Text = _profile.Path ?? string.Empty; + sDelay.Value = _profile.Delay; + cbHandled.Checked = _profile.IsHandled; + } + + // Event handlers + private void CbEnabled_CheckedChanged(object? _, EventArgs __) => UpdateControlsEnabledState(); + private void TxtName_TextChanged(object? _, EventArgs __) => _profile.Name = txtName.Text; + private void CbScope_SelectedIndexChanged(object? _, EventArgs __) => _profile.Scope = (HotkeyScope)cbScope.SelectedItem; + private void CbAction_SelectedIndexChanged(object? _, EventArgs __) => UpdateAction(); + private void TxtPath_TextChanged(object? _, EventArgs __) => _profile.Path = txtPath.Text; + private void CbHandled_CheckedChanged(object? _, EventArgs __) => _profile.IsHandled = cbHandled.Checked; + private void SDelay_ValueChanged(object? _, int newValue) => _profile.Delay = newValue; + private void BtnCollapse_Click(object? _, EventArgs __) => IsCollapsed = !_isCollapsed; + private void BtnDelete_Click(object? _, EventArgs __) => _removeAction?.Invoke(_profile); + private void TxtHotKeys_KeyDown(object? _, KeyEventArgs e) => e.SuppressKeyPress = true; + private void TxtHotKeys_Enter(object? _, EventArgs __) => InitializeKeyboardHook(); + private void TxtHotKeys_Leave(object? _, EventArgs __) + { + DisposeKeyboardHook(); + + // If the name is empty, set it to the hotkey. + if (string.IsNullOrEmpty(txtName.Text)) + txtName.Text = txtHotKeys.Text; + } + private void LowLevelKeyboardHook_Down(object? _, KeyboardEventArgs e) + { + // Backspace removes the hotkey. + if (e.Keys.Are(Key.Back)) + { + Invoke(() => txtHotKeys.Text = string.Empty); + _profile.HotKeys = null; + return; + } + + // Two or more keys and must have a modifier key. (CTRL, ALT, SHIFT, WIN) + if (e.Keys.Values.Count < 2 || !e.Keys.Values.Any(k => k is Key.Ctrl or Key.Alt or Key.Shift or Key.LWin or Key.RWin)) + return; + + // Prevent the key from being handled by other applications. + e.IsHandled = true; + + // Order the keys by modifier keys first, then by key. + var keys = e.Keys.Values + .OrderByDescending(key => key is Key.LWin or Key.RWin) + .ThenBy(key => key) + .ToArray(); + + _profile.HotKeys = keys; + Invoke(() => txtHotKeys.Text = string.Join(" + ", keys.Select(k => k.ToFixedString()))); + } + + // Methods + private static void SetComboBoxDataSourceQuietly(ComboBox comboBox, object datasource, EventHandler eventHandler) + { + comboBox.SelectedIndexChanged -= eventHandler; + comboBox.DataSource = datasource; + comboBox.SelectedIndexChanged += eventHandler; + } + private void ToggleCollapse() + { + switch (_isCollapsed) + { + case true: + Height = ExpandedHeight; + btnCollapse.Text = @"ᐱ"; + break; + default: + Height = _collapsedHeight; + btnCollapse.Text = @"ᐯ"; + break; + } + } + private void UpdateControlsEnabledState() + { + var isEnabled = cbEnabled.Checked; + _profile.IsEnabled = isEnabled; + + // Disable all controls if cbEnabled is unchecked, except for cbEnabled and btnCollapse. + txtName.Enabled = isEnabled; + txtHotKeys.Enabled = isEnabled; + cbScope.Enabled = isEnabled; + cbAction.Enabled = isEnabled; + txtPath.Enabled = isEnabled; + sDelay.Enabled = isEnabled; + cbHandled.Enabled = isEnabled; + } + private void UpdateAction() + { + var selectedAction = (HotKeyAction)cbAction.SelectedItem; + _profile.Action = selectedAction; + + switch (selectedAction) + { + case HotKeyAction.Open: + { + txtPath.Enabled = true; + txtPath.Text = _profile.Path ?? string.Empty; + break; + } + case HotKeyAction.Duplicate: + { + txtPath.Enabled = false; + cbScope.SelectedIndex = cbScope.FindStringExact(nameof(HotkeyScope.FileExplorer)); + cbScope.Invalidate(); + break; + } + } + } + private void InitializeKeyboardHook() + { + DisposeKeyboardHook(false); + _keyboardHookStarted?.Invoke(); + _lowLevelKeyboardHook = new LowLevelKeyboardHook { Handling = true }; + _lowLevelKeyboardHook.Down += LowLevelKeyboardHook_Down; + _lowLevelKeyboardHook.Start(); + } + private void DisposeKeyboardHook(bool inform = true) + { + if (_lowLevelKeyboardHook == default) return; + _lowLevelKeyboardHook.Stop(); + _lowLevelKeyboardHook.Dispose(); + + if (inform) + _keyboardHookStopped?.Invoke(); + } +} \ No newline at end of file diff --git a/ExplorerTabUtility/Forms/HotKeyProfileControl.resx b/ExplorerTabUtility/Forms/HotKeyProfileControl.resx new file mode 100644 index 0000000..1f052d5 --- /dev/null +++ b/ExplorerTabUtility/Forms/HotKeyProfileControl.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + \ No newline at end of file diff --git a/ExplorerTabUtility/Forms/MainForm.Designer.cs b/ExplorerTabUtility/Forms/MainForm.Designer.cs new file mode 100644 index 0000000..63549e1 --- /dev/null +++ b/ExplorerTabUtility/Forms/MainForm.Designer.cs @@ -0,0 +1,211 @@ +using ExplorerTabUtility.Models; + +namespace ExplorerTabUtility.Forms +{ + partial class MainForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + components = new System.ComponentModel.Container(); + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm)); + label1 = new System.Windows.Forms.Label(); + flpProfiles = new System.Windows.Forms.FlowLayoutPanel(); + btnNewProfile = new MaterialSkin.Controls.MaterialButton(); + btnImport = new MaterialSkin.Controls.MaterialButton(); + btnExport = new MaterialSkin.Controls.MaterialButton(); + btnSave = new MaterialSkin.Controls.MaterialButton(); + cbSaveProfilesOnExit = new MaterialSkin.Controls.MaterialCheckbox(); + toolTip = new System.Windows.Forms.ToolTip(components); + SuspendLayout(); + // + // label1 + // + label1.AutoSize = true; + label1.BackColor = System.Drawing.Color.Transparent; + label1.Font = new System.Drawing.Font("Microsoft Sans Serif", 14F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Pixel); + label1.ForeColor = System.Drawing.SystemColors.ControlLightLight; + label1.Location = new System.Drawing.Point(4, 2); + label1.Name = "label1"; + label1.Size = new System.Drawing.Size(127, 17); + label1.TabIndex = 0; + label1.Text = "Explorer Tab Utility"; + // + // flpProfiles + // + flpProfiles.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + flpProfiles.AutoScroll = true; + flpProfiles.FlowDirection = System.Windows.Forms.FlowDirection.TopDown; + flpProfiles.Location = new System.Drawing.Point(7, 112); + flpProfiles.Name = "flpProfiles"; + flpProfiles.Size = new System.Drawing.Size(748, 283); + flpProfiles.TabIndex = 2; + flpProfiles.WrapContents = false; + flpProfiles.Resize += FlpProfiles_Resize; + // + // btnNewProfile + // + btnNewProfile.AutoSize = false; + btnNewProfile.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; + btnNewProfile.Density = MaterialSkin.Controls.MaterialButton.MaterialButtonDensity.Default; + btnNewProfile.Depth = 0; + btnNewProfile.HighEmphasis = true; + btnNewProfile.Icon = null; + btnNewProfile.Location = new System.Drawing.Point(12, 68); + btnNewProfile.Margin = new System.Windows.Forms.Padding(4, 6, 4, 6); + btnNewProfile.MouseState = MaterialSkin.MouseState.HOVER; + btnNewProfile.Name = "btnNewProfile"; + btnNewProfile.NoAccentTextColor = System.Drawing.Color.Empty; + btnNewProfile.Size = new System.Drawing.Size(76, 36); + btnNewProfile.TabIndex = 5; + btnNewProfile.Text = "New"; + toolTip.SetToolTip(btnNewProfile, "New profile."); + btnNewProfile.Type = MaterialSkin.Controls.MaterialButton.MaterialButtonType.Text; + btnNewProfile.UseAccentColor = true; + btnNewProfile.UseVisualStyleBackColor = true; + btnNewProfile.Click += BtnNewProfile_Click; + // + // btnImport + // + btnImport.AutoSize = false; + btnImport.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; + btnImport.Density = MaterialSkin.Controls.MaterialButton.MaterialButtonDensity.Default; + btnImport.Depth = 0; + btnImport.HighEmphasis = true; + btnImport.Icon = null; + btnImport.Location = new System.Drawing.Point(94, 68); + btnImport.Margin = new System.Windows.Forms.Padding(4, 6, 4, 6); + btnImport.MouseState = MaterialSkin.MouseState.HOVER; + btnImport.Name = "btnImport"; + btnImport.NoAccentTextColor = System.Drawing.Color.Empty; + btnImport.Size = new System.Drawing.Size(76, 36); + btnImport.TabIndex = 6; + btnImport.Text = "Import"; + toolTip.SetToolTip(btnImport, "Import profiles."); + btnImport.Type = MaterialSkin.Controls.MaterialButton.MaterialButtonType.Text; + btnImport.UseAccentColor = true; + btnImport.UseVisualStyleBackColor = true; + btnImport.Click += BtnImport_Click; + // + // btnExport + // + btnExport.AutoSize = false; + btnExport.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; + btnExport.Density = MaterialSkin.Controls.MaterialButton.MaterialButtonDensity.Default; + btnExport.Depth = 0; + btnExport.HighEmphasis = true; + btnExport.Icon = null; + btnExport.Location = new System.Drawing.Point(176, 68); + btnExport.Margin = new System.Windows.Forms.Padding(4, 6, 4, 6); + btnExport.MouseState = MaterialSkin.MouseState.HOVER; + btnExport.Name = "btnExport"; + btnExport.NoAccentTextColor = System.Drawing.Color.Empty; + btnExport.Size = new System.Drawing.Size(76, 36); + btnExport.TabIndex = 7; + btnExport.Text = "Export"; + toolTip.SetToolTip(btnExport, "Export profiles."); + btnExport.Type = MaterialSkin.Controls.MaterialButton.MaterialButtonType.Text; + btnExport.UseAccentColor = true; + btnExport.UseVisualStyleBackColor = true; + btnExport.Click += BtnExport_Click; + // + // btnSave + // + btnSave.AutoSize = false; + btnSave.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; + btnSave.Density = MaterialSkin.Controls.MaterialButton.MaterialButtonDensity.Default; + btnSave.Depth = 0; + btnSave.HighEmphasis = true; + btnSave.Icon = null; + btnSave.Location = new System.Drawing.Point(258, 68); + btnSave.Margin = new System.Windows.Forms.Padding(4, 6, 4, 6); + btnSave.MouseState = MaterialSkin.MouseState.HOVER; + btnSave.Name = "btnSave"; + btnSave.NoAccentTextColor = System.Drawing.Color.Empty; + btnSave.Size = new System.Drawing.Size(76, 36); + btnSave.TabIndex = 8; + btnSave.Text = "Save"; + toolTip.SetToolTip(btnSave, "Persist profiles for next time you open the app."); + btnSave.Type = MaterialSkin.Controls.MaterialButton.MaterialButtonType.Text; + btnSave.UseAccentColor = true; + btnSave.UseVisualStyleBackColor = true; + btnSave.Click += BtnSave_Click; + // + // cbSaveProfilesOnExit + // + cbSaveProfilesOnExit.AutoSize = true; + cbSaveProfilesOnExit.Checked = true; + cbSaveProfilesOnExit.CheckState = System.Windows.Forms.CheckState.Checked; + cbSaveProfilesOnExit.Depth = 0; + cbSaveProfilesOnExit.Location = new System.Drawing.Point(610, 68); + cbSaveProfilesOnExit.Margin = new System.Windows.Forms.Padding(0); + cbSaveProfilesOnExit.MouseLocation = new System.Drawing.Point(-1, -1); + cbSaveProfilesOnExit.MouseState = MaterialSkin.MouseState.HOVER; + cbSaveProfilesOnExit.Name = "cbSaveProfilesOnExit"; + cbSaveProfilesOnExit.ReadOnly = false; + cbSaveProfilesOnExit.Ripple = true; + cbSaveProfilesOnExit.Size = new System.Drawing.Size(121, 37); + cbSaveProfilesOnExit.TabIndex = 9; + cbSaveProfilesOnExit.Text = "Save on exit"; + toolTip.SetToolTip(cbSaveProfilesOnExit, "Automatically saves your profiles on exit.\r\nTo persist profiles for next time you open the app."); + cbSaveProfilesOnExit.UseVisualStyleBackColor = true; + cbSaveProfilesOnExit.CheckedChanged += CbSaveProfilesOnExit_CheckedChanged; + // + // MainForm + // + AutoScaleMode = System.Windows.Forms.AutoScaleMode.None; + ClientSize = new System.Drawing.Size(762, 402); + Controls.Add(cbSaveProfilesOnExit); + Controls.Add(btnSave); + Controls.Add(btnExport); + Controls.Add(btnImport); + Controls.Add(btnNewProfile); + Controls.Add(flpProfiles); + Controls.Add(label1); + Icon = (System.Drawing.Icon)resources.GetObject("$this.Icon"); + MinimumSize = new System.Drawing.Size(762, 225); + Name = "MainForm"; + Padding = new System.Windows.Forms.Padding(3, 55, 3, 3); + Text = "Settings"; + Deactivate += MainForm_Deactivate; + FormClosing += MainForm_FormClosing; + Resize += MainForm_Resize; + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private System.Windows.Forms.Label label1; + private System.Windows.Forms.FlowLayoutPanel flpProfiles; + private MaterialSkin.Controls.MaterialButton btnNewProfile; + private MaterialSkin.Controls.MaterialButton btnImport; + private MaterialSkin.Controls.MaterialButton btnExport; + private MaterialSkin.Controls.MaterialButton btnSave; + private MaterialSkin.Controls.MaterialCheckbox cbSaveProfilesOnExit; + private System.Windows.Forms.ToolTip toolTip; + } +} \ No newline at end of file diff --git a/ExplorerTabUtility/Forms/MainForm.cs b/ExplorerTabUtility/Forms/MainForm.cs new file mode 100644 index 0000000..c9561b6 --- /dev/null +++ b/ExplorerTabUtility/Forms/MainForm.cs @@ -0,0 +1,374 @@ +using System; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Windows.Forms; +using System.Collections.Generic; +using System.Diagnostics; +using MaterialSkin; +using MaterialSkin.Controls; +using ExplorerTabUtility.Models; +using ExplorerTabUtility.WinAPI; +using ExplorerTabUtility.Helpers; +using ExplorerTabUtility.Managers; + +namespace ExplorerTabUtility.Forms; + +public partial class MainForm : MaterialForm +{ + private readonly List _hotKeyProfiles = []; + private bool _allowVisible; + private NotifyIcon _notifyIcon = null!; + private readonly HookManager _hookManager; + + public MainForm() + { + Application.ApplicationExit += OnApplicationExit; + InteractionManager.InteractionMethod = (InteractionMethod)SettingsManager.InteractionMethod; + _hookManager = new HookManager(_hotKeyProfiles, InteractionManager.OnHotKeyProfileTriggered, InteractionManager.OnNewWindow); + + InitializeComponent(); + SetupMaterialSkin(); + InitializeNotifyIcon(); + StartHooks(); + } + + private void SetupMaterialSkin() + { + var materialSkinManager = MaterialSkinManager.Instance; + materialSkinManager.EnforceBackcolorOnAllComponents = false; + materialSkinManager.AddFormToManage(this); + materialSkinManager.Theme = MaterialSkinManager.Themes.DARK; + materialSkinManager.ColorScheme = new ColorScheme(Primary.Blue800, Primary.Blue900, Primary.LightBlue600, Accent.LightBlue200, TextShade.WHITE); + } + private void InitializeNotifyIcon() + { + var importedList = DeserializeHotKeyProfiles(SettingsManager.HotKeyProfiles); + if (importedList != default) AddProfiles(importedList); + + _notifyIcon = new NotifyIcon + { + Icon = Helper.GetIcon(), + Text = Constants.NotifyIconText, + + ContextMenuStrip = CreateContextMenuStrip(), + Visible = true + }; + + // Show the form when the user double-clicks on the notify icon. + _notifyIcon.MouseDoubleClick += (_, e) => + { + if (e.Button != MouseButtons.Left) return; + ShowForm(); + }; + } + private void StartHooks() + { + if (SettingsManager.IsWindowHookActive) _hookManager.StartWindowHook(); + if (SettingsManager.IsKeyboardHookActive) _hookManager.StartKeyboardHook(); + } + + private ContextMenuStrip CreateContextMenuStrip() + { + var strip = new ContextMenuStrip(); + + // Interaction Menu + strip.Items.Add(CreateInteractionMethodMenuItem()); + + // KeyboardHook Menu + strip.Items.Add(CreateKeyboardHookMenuItem()); + + // WindowHook + strip.Items.Add(CreateMenuItem("Window Hook", SettingsManager.IsWindowHookActive, ToggleWindowHook)); + + // Separator + strip.Items.Add(new ToolStripSeparator()); + + // Startup + strip.Items.Add(CreateMenuItem("Add to startup", RegistryManager.IsInStartup(), static (_, _) => RegistryManager.ToggleStartup())); + + // Settings + strip.Items.Add(CreateMenuItem("Settings", false, (_, _) => ShowForm(), checkOnClick: false)); + + // Separator + strip.Items.Add(new ToolStripSeparator()); + + // Exit + strip.Items.Add(CreateMenuItem("Exit", false, static (_, _) => Application.Exit())); + + return strip; + } + private ToolStripMenuItem CreateKeyboardHookMenuItem() + { + var menuItem = CreateMenuItem("Keyboard Hook", SettingsManager.IsKeyboardHookActive, ToggleKeyboardHook, "KeyboardHookMenu"); + + AddProfilesToMenuItem(menuItem); + return menuItem; + } + private void UpdateKeyboardHookMenu() + { + var menuItem = (ToolStripMenuItem?)_notifyIcon.ContextMenuStrip?.Items["KeyboardHookMenu"]; + if (menuItem == default) return; + + menuItem.DropDownItems.Clear(); + AddProfilesToMenuItem(menuItem); + } + private void AddProfilesToMenuItem(ToolStripMenuItem menuItem) + { + foreach (var profile in _hotKeyProfiles) + { + var profileMenuItem = CreateMenuItem(profile.Name ?? string.Empty, profile.IsEnabled, eventHandler: KeyboardHookProfileItemClick); + profileMenuItem.Tag = profile; + menuItem.DropDownItems.Add(profileMenuItem); + } + } + private ToolStripMenuItem CreateInteractionMethodMenuItem() + { + var isUiAutomation = InteractionManager.InteractionMethod == InteractionMethod.UiAutomation; + + var windowHookMenuItem = CreateMenuItem("Interaction Method", checkOnClick: false, + dropDownItems: + [ + CreateMenuItem("UIAutomation (Recommended)", isUiAutomation, InteractionMethodChanged, nameof(InteractionMethod.UiAutomation), false), + CreateMenuItem("Keyboard", !isUiAutomation, InteractionMethodChanged, nameof(InteractionMethod.Keyboard), false) + ]); + + return windowHookMenuItem; + } + private static ToolStripMenuItem CreateMenuItem(string text, bool isChecked = default, EventHandler? eventHandler = default, + string? name = default, bool checkOnClick = true, params ToolStripItem[] dropDownItems) + { + var item = new ToolStripMenuItem + { + Text = text, + Checked = isChecked, + CheckOnClick = checkOnClick + }; + + if (name != default) + item.Name = name; + + if (eventHandler != default) + item.Click += eventHandler; + + if (dropDownItems.Length > 0) + item.DropDownItems.AddRange(dropDownItems); + + return item; + } + + private void UpdateMenuAndSaveProfiles() + { + // Remove profiles that don't have any hotkeys. + _hotKeyProfiles.FindAll(p => p.HotKeys?.Any() != true).ForEach(RemoveProfile); + + UpdateKeyboardHookMenu(); + + SettingsManager.HotKeyProfiles = JsonSerializer.Serialize(_hotKeyProfiles); + } + private void AddProfiles(List profiles, bool clear = false) + { + flpProfiles.SuspendLayout(); + flpProfiles.SuspendDrawing(); + + if (clear) ClearProfiles(); + + profiles.ForEach(AddProfile); + + flpProfiles.ResumeDrawing(); + flpProfiles.ResumeLayout(); + } + private void AddProfile(HotKeyProfile? profile = default) + { + _hotKeyProfiles.Add(profile ??= new HotKeyProfile()); + flpProfiles.Controls.Add(new HotKeyProfileControl(profile, RemoveProfile, ControlStartedKeyboardHook, ControlStoppedKeyboardHook)); + } + private void ClearProfiles() + { + _hotKeyProfiles.Clear(); + flpProfiles.Controls.Clear(); + } + private void RemoveProfile(HotKeyProfile profile) + { + _hotKeyProfiles.Remove(profile); + + var control = FindControlByProfile(profile); + if (control != default) + flpProfiles.Controls.Remove(control); + } + private HotKeyProfileControl? FindControlByProfile(HotKeyProfile profile) + { + return flpProfiles.Controls + .OfType() + .FirstOrDefault(c => c.Tag?.Equals(profile) == true); + } + private static List? DeserializeHotKeyProfiles(string jsonString) + { + try + { + return JsonSerializer.Deserialize>(jsonString); + } + catch + { + return default; + } + } + + private void ControlStartedKeyboardHook() + { + Debug.WriteLine($"{nameof(ControlStartedKeyboardHook)}"); + if (!SettingsManager.IsKeyboardHookActive) return; + _hookManager.StopKeyboardHook(); + } + private void ControlStoppedKeyboardHook() + { + Debug.WriteLine($"{nameof(ControlStoppedKeyboardHook)}"); + if (!SettingsManager.IsKeyboardHookActive) return; + _hookManager.StartKeyboardHook(); + } + private static void InteractionMethodChanged(object? sender, EventArgs _) + { + if (sender is not ToolStripMenuItem item) return; + if (!Enum.TryParse(item.Name, out InteractionMethod method)) return; + InteractionManager.InteractionMethod = method; + + foreach (ToolStripMenuItem radio in item.GetCurrentParent().Items) + radio.Checked = radio == item; + + SettingsManager.InteractionMethod = (int)method; + } + private void KeyboardHookProfileItemClick(object? sender, EventArgs _) + { + if (sender is not ToolStripMenuItem item || item.OwnerItem is not ToolStripMenuItem parent) return; + + // Toggle the HotKeyProfileControl's Enabled state. + if (item.Tag is HotKeyProfile profile) + { + var control = FindControlByProfile(profile); + if (control != null) control.IsEnabled = item.Checked; + } + + // Uncheck parent if all sub items are unchecked. + parent.Checked = parent.DropDownItems.OfType().Any(c => c.Checked); + if (!parent.Checked) + ToggleKeyboardHook(parent, EventArgs.Empty); + } + private void ToggleKeyboardHook(object? sender, EventArgs _) + { + if (sender is not ToolStripMenuItem item) return; + + SettingsManager.IsKeyboardHookActive = item.Checked; + + foreach (ToolStripItem subItem in item.DropDownItems) + subItem.Enabled = item.Checked; + + if (item.Checked) + { + // If all sub items are not checked, click the first item. + if (_hotKeyProfiles.TrueForAll(h => !h.IsEnabled)) + item.DropDownItems[0].PerformClick(); + + _hookManager.StartKeyboardHook(); + } + else + _hookManager.StopKeyboardHook(); + } + private void ToggleWindowHook(object? sender, EventArgs _) + { + if (sender is not ToolStripMenuItem item) return; + + SettingsManager.IsWindowHookActive = item.Checked; + + if (item.Checked) + _hookManager.StartWindowHook(); + else + _hookManager.StopWindowHook(); + } + private void BtnNewProfile_Click(object _, EventArgs __) => AddProfile(); + private void BtnImport_Click(object _, EventArgs __) + { + using var ofd = new OpenFileDialog(); + ofd.FileName = Constants.HotKeyProfilesFileName; + ofd.Filter = Constants.JsonFileFilter; + if (ofd.ShowDialog() != DialogResult.OK) return; + + var jsonString = System.IO.File.ReadAllText(ofd.FileName); + var importedList = DeserializeHotKeyProfiles(jsonString); + if (importedList == default) return; + + AddProfiles(importedList, true); + } + private void BtnExport_Click(object _, EventArgs __) + { + using var sfd = new SaveFileDialog(); + sfd.FileName = Constants.HotKeyProfilesFileName; + sfd.Filter = Constants.JsonFileFilter; + if (sfd.ShowDialog() != DialogResult.OK) return; + + using var openFile = sfd.OpenFile(); + var bytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(_hotKeyProfiles)); + openFile.Write(bytes, 0, bytes.Length); + } + private void BtnSave_Click(object _, EventArgs __) => UpdateMenuAndSaveProfiles(); + private void CbSaveProfilesOnExit_CheckedChanged(object _, EventArgs __) => SettingsManager.SaveProfilesOnExit = cbSaveProfilesOnExit.Checked; + private void FlpProfiles_Resize(object _, EventArgs __) + { + foreach (Control c in flpProfiles.Controls) + c.Width = flpProfiles.Width - 20; + } + private void MainForm_Resize(object _, EventArgs __) + { + if (WindowState == FormWindowState.Normal) + { + _allowVisible = true; + } + else if (WindowState == FormWindowState.Minimized) + { + WindowState = FormWindowState.Normal; + _allowVisible = false; + Hide(); + + if (!cbSaveProfilesOnExit.Checked) return; + + // Save + UpdateMenuAndSaveProfiles(); + } + } + private void MainForm_FormClosing(object _, FormClosingEventArgs e) + { + if (e.CloseReason != CloseReason.UserClosing) return; + + e.Cancel = true; + _allowVisible = false; + + // Hide the form instead of closing it when the close button is clicked + Hide(); + + if (!cbSaveProfilesOnExit.Checked) return; + + // Save + UpdateMenuAndSaveProfiles(); + } + private void MainForm_Deactivate(object _, EventArgs __) => flpProfiles.Focus(); + private void OnApplicationExit(object? _, EventArgs __) + { + _notifyIcon.Visible = false; + _hookManager.Dispose(); + } + + private void ShowForm() + { + _allowVisible = true; + WindowState = FormWindowState.Normal; + Show(); + } + protected override void SetVisibleCore(bool value) + { + if (!_allowVisible) + { + value = false; + if (!IsHandleCreated) CreateHandle(); + } + base.SetVisibleCore(value); + } +} \ No newline at end of file diff --git a/ExplorerTabUtility/Forms/MainForm.resx b/ExplorerTabUtility/Forms/MainForm.resx new file mode 100644 index 0000000..0cb0265 --- /dev/null +++ b/ExplorerTabUtility/Forms/MainForm.resx @@ -0,0 +1,763 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + + + + AAABAAEAYGAAAAEAIAColAAAFgAAACgAAABgAAAAwAAAAAEAIAAAAAAAAJAAAMMOAADDDgAAAAAAAAAA + AADpuAb/57oH/+e5B//ouAb/6LkG/+e5B//nuAj/6LgI/+m4B//ouQb/6LgH/+i4B//puAj/6bgH/+m5 + B//puQf/6LkH/+a5B//puAj/6LcH/+m4Bv/puQb/57kG/+e5Bv/muQb/57gH/+a3B//muAb/6LkG/+m5 + B//puQf/6bkH/+i4Bv/nuAj/57kI/+e5B//nuAj/57gH/+e5B//puAj/6LcG/+m5B//puQb/6bkG/+i4 + Bv/puQf/6bkH/+m5B//nugf/6LkH/+m5B//puQb/6LkH/+a4B//otwf/57cH/+a4CP/nuAj/6LgI/+i3 + B//nuAj/57kH/+e5Bv/nuAj/57gI/+a3B//muQf/6LkH/+m4Bv/puQf/6LgG/+i4Bv/puQf/6LkH/+e5 + B//nuQf/6LkH/+m5B//puQf/6bkH/+m5Bv/puQf/6bkH/+m5B//puQf/6bkH/+m5B//nuQf/57kH/+e5 + B//nuQf/6LkH/+m5B//pugX/6bkG/+m5B//quAf/6boI/+m6CP/qugj/6roJ/+m6Cf/puQr/6bkJ/+q5 + Cf/quQn/6rgJ/+q4Cv/quQn/6rkJ/+q5Cf/qugj/6boI/+m6CP/puQn/6bkJ/+m6Cf/rugn/6boJ/+i6 + Cf/pugj/6boJ/+i5Cf/ougj/6roJ/+q6Cf/qugn/6rkI/+m6Cf/ouQn/6LoJ/+m7Cf/pugr/6bkJ/+m5 + CP/puQn/6rkJ/+q6Cf/quAn/6rkI/+q6Cf/pugn/6roJ/+q6Cf/pugr/6roJ/+u6Cf/quQr/6bkK/+i5 + Cf/quQn/6bkJ/+i5Cf/puQn/6bkJ/+m5Cf/puQn/6LkJ/+i5Cv/ouQn/6boK/+i5Cf/ougn/6boJ/+m5 + Cv/pugn/6roJ/+q5CP/qugj/6roI/+q7Cf/ougn/6boJ/+q6Cf/quwn/6roJ/+u6Cf/rugn/67oJ/+q6 + CP/quwn/6roJ/+q6Cf/ougn/6boJ/+m6Cf/ougn/6rsJ/+u7Cf/qugj/6roJ/+q5Cf/quAf/6rkI/+q6 + CP/pugj/6rkI/+q5Cf/quAr/6rgK/+q5Cf/puQj/6rkJ/+q5Cf/puAj/6bgJ/+q4Cv/quQj/6roI/+q6 + CP/ouQn/6LkJ/+i5Cf/quwj/6rsJ/+m7CP/ruwn/6roJ/+i6Cf/ougj/6boJ/+q5Cf/quQn/6bkJ/+i6 + Cv/ouQn/6LoJ/+i6CP/ouQn/6bkJ/+q5Cf/nuAn/6bkJ/+q5Cf/quQn/6rkJ/+q5Cf/ouQr/6LoK/+e5 + Cf/pugn/6boK/+q5Cv/ouQn/6LoJ/+m7CP/quQn/6boJ/+m6Cv/ruQr/6boJ/+i5Cf/quQj/6bkJ/+i5 + Cf/ouQn/6LkJ/+m6Cv/ouQn/6boK/+i6Cf/ougr/6boK/+q5Cf/qugj/6rsJ/+q6Cf/pugr/6LkJ/+i6 + Cf/ougn/6boI/+u7Cf/quQn/6roJ/+q7CP/puwn/6roJ/+q5Cv/puQr/6bkK/+m6Cv/puwn/6rsJ/+q6 + Cf/qugj/6roI/+q6CP/puQf/6boI/+i6CP/puQf/6rkI/+q5Cf/quAj/6bgI/+e4CP/qugj/6boI/+i6 + CP/ouQn/6LkJ/+m5Cf/ouQn/6LkJ/+i5Cf/ouQn/6LoJ/+i6CP/puwn/6boI/+q6CP/ougj/6LoI/+e5 + B//ougj/6LoI/+i6CP/pugj/6boI/+m7Cf/pugr/6LkI/+e5B//nugf/6LoJ/+i5Cf/ouAn/6LkJ/+i5 + Cf/ouQn/6LkI/+i6CP/ouQn/6boJ/+i6CP/ouQn/6LkJ/+i5Cf/ougj/6boJ/+i5Cf/ouQn/6LkI/+m7 + CP/ougj/6LoI/+m7Cf/pugn/6LoI/+i6CP/ougj/6LoJ/+i5Cf/ouQn/6LoJ/+m7CP/puwn/6boJ/+m6 + Cf/ougj/6LoI/+i6CP/ouQn/6boJ/+i6CP/pugr/6bkK/+i5Cf/pugj/6bsH/+m8Bv/puwn/6bsJ/+m7 + Cf/ouQn/6LkJ/+m6Cv/ougj/6LoI/+m6CP/qugf/6rkI/+q4Cf/puAb/6boI/+i6CP/ougf/6bkI/+m5 + Cf/puQn/6LgJ/+e4CP/ouQf/6LkI/+e5CP/ouQn/6LkJ/+i5Cf/ouQn/6LkJ/+i5Cf/ouQn/6LkJ/+i5 + CP/ougj/6LoI/+m5CP/ouQj/6LkI/+i5CP/ougj/6LoJ/+i6Cf/ouQj/6LkI/+i5CP/pugr/6LkJ/+i5 + CP/ougn/6LkJ/+i5Cf/nuQj/6LkJ/+i5Cf/ouQn/57kI/+i6CP/ouQn/6LoJ/+i6CP/ougn/6LoJ/+i6 + Cf/ougj/6LoI/+i6Cf/pugr/6boJ/+m6Cf/ougn/6LoI/+m7Cf/puwn/6LoI/+i6CP/ougj/6boJ/+i5 + Cf/ouQj/6LoJ/+m7CP/ougj/6LoI/+i6CP/ouwn/6LoI/+i6Cf/ouQj/6boJ/+i6Cf/ouQn/6boJ/+i6 + Cf/pugn/6LoI/+i6B//ougn/6LoJ/+i6Cf/ouQn/6boJ/+i6Cf/ougj/57kI/+i5CP/ouQj/6bkI/+q5 + CP/puQf/6boI/+e6CP/ougj/6LkI/+i5Cf/ouQr/57gJ/+e4CP/nuQf/6LkI/+i5Cf/ouQn/57gI/+i5 + Cf/ouQn/6LkJ/+i5Cf/ouQn/6LgJ/+i5Cf/ougj/57kI/+i5Cf/ouQn/6LkJ/+i5Cf/puwn/6LoJ/+i5 + Cf/ouQn/6LkJ/+i5Cf/ouQn/6LkJ/+i5Cf/ouQn/6LkJ/+i5Cf/ougj/6LkI/+i5Cf/ouQn/6LkI/+i6 + CP/ouQn/6LoJ/+m7CP/puwj/6LoI/+i6CP/ougj/6LoI/+i7CP/pugn/6LkJ/+m6Cv/pugr/6bsK/+m7 + Cf/ougj/6LoI/+i6CP/ougj/6LoJ/+m6Cv/ougj/6LoI/+m7Cf/puwn/6bsJ/+m7Cf/ougj/6LkJ/+i5 + Cf/ougf/6boJ/+i5Cv/ougn/6LoJ/+m7Cf/pugr/6LkJ/+m6Cv/pugr/6boK/+i5Cf/ouQn/6boJ/+i6 + CP/ougj/6LkI/+i5Cf/ouQn/6boJ/+q6B//puAj/6boJ/+e6CP/ouQn/6LkI/+i6CP/ouQn/57gI/+i5 + Cf/ouQn/57gI/+e4CP/ouQn/57gI/+i5Cf/ouQn/6LkI/+e5CP/ouQn/6LkJ/+i5Cf/ougj/57oH/+i6 + CP/ougj/6LkI/+i5Cf/ouQn/6LkJ/+i5Cf/ouQn/6LkJ/+i5Cf/ouAn/6LkI/+i6CP/ouQn/6LkJ/+i5 + Cf/ouQn/57kI/+i6CP/nuQf/6LoI/+i6CP/ouQn/6LoJ/+i6CP/pugr/6boJ/+i6CP/ougj/6LoI/+i6 + CP/ougj/6LoI/+i6CP/ougj/6LoI/+i6CP/ougj/6LoI/+i6CP/ougj/6LkJ/+i5Cf/ougj/6LsI/+m7 + Cf/puwn/6LoI/+i6CP/ougj/6boJ/+i5Cv/ougj/6LoI/+i6CP/ougj/6bsJ/+i7CP/puwj/6boJ/+i5 + Cf/pugr/6LkJ/+m6Cv/puQr/6LoJ/+m7Cf/pugn/6LkJ/+i5Cf/qugj/6rkI/+q5Cf/otwf/6boJ/+i6 + CP/ouQn/6LkI/+i6CP/ouQn/6LkJ/+i5Cf/ouQn/57gI/+e4CP/ouQn/6LkI/+i5CP/ouQn/6LoJ/+i6 + CP/ouQj/6LkI/+i5Cf/ouQj/6LkI/+i5CP/nuQj/6LkI/+i5Cf/ouQn/6LkJ/+i5Cf/ouQn/6LkJ/+i5 + Cf/ouQj/6LkI/+i6B//ouQj/6LoJ/+i5CP/ouQj/6LoI/+i6CP/ougn/6LoI/+m7Cf/ougn/6LoJ/+m7 + Cf/pugn/6LoJ/+i7CP/ouwj/6LoI/+i6CP/puwn/6bsJ/+i6CP/ougj/6LoI/+i6CP/ougj/6LoI/+i6 + CP/puwn/6boJ/+i6Cf/ougj/6bsJ/+m7Cf/ougj/6LoI/+i6CP/puwn/6LoI/+m6Cf/ougj/6LoI/+i6 + CP/ougj/6LoJ/+i6Cf/pugn/6boJ/+m6Cv/pugn/6LoJ/+i5Cf/pugn/6boJ/+m7Cf/ouQn/6LkK/+i5 + Cf/pugj/6roI/+q5CP/otwf/6bkI/+i6CP/ouQn/6LkI/+i6CP/ouQn/6LkJ/+i5Cf/nuAn/57gI/+i5 + CP/nuAj/6LkI/+e5B//ouQn/6LkI/+i6CP/ougj/6LkI/+i4Cf/ouQn/6LkJ/+i5Cf/ouAn/6LgJ/+i5 + Cf/nuAj/6LkJ/+i5Cf/ouQn/6LkJ/+i5Cf/ougj/6LoI/+i6CP/ougj/6LoI/+i6CP/ougj/6LoI/+i6 + CP/ouQn/6boJ/+m7Cf/ougj/6bsJ/+m7CP/puwn/6bsJ/+m6Cf/puwn/6bsJ/+m7Cf/pugn/6bsJ/+m7 + Cf/ougn/6bsJ/+m7Cf/puwn/6bsJ/+m7Cf/puwn/6bsJ/+m7Cf/puwn/6LoI/+m7Cf/puwn/6bsJ/+i6 + CP/puwn/6bsJ/+i7CP/puwn/6bsJ/+m7Cf/puwn/6LoJ/+i5Cf/pugr/6boK/+m6Cv/puwn/6LoJ/+i5 + Cf/puwn/6bsJ/+i6CP/ouQn/6LkJ/+i5Cf/nugj/6boI/+m5B//otwf/6LkI/+e5B//nuAj/6LkJ/+e4 + CP/nuAj/57gJ/+e4Cf/nuAn/57gI/+e4CP/nuQf/6LkI/+i5Cf/ouQn/6LkJ/+i5CP/puAj/6bkJ/+i5 + Cf/ouQn/6LkJ/+i5Cf/ouQn/6LkJ/+i5Cf/ouQn/6LkJ/+i5CP/ougj/57kH/+e5B//ouQn/6LkJ/+i5 + Cf/ouQn/6LkJ/+i5Cf/ougj/6LkI/+i5CP/ougj/6boJ/+i5Cf/pugr/6LoJ/+m7Cf/puwn/6bsK/+m6 + Cv/pugv/6boL/+m6C//pugv/6rsK/+m7Cv/puwn/6bsJ/+m7Cf/puwn/6rwK/+m8Cf/pvAf/6bwJ/+m7 + Cv/puwn/6bsJ/+m7Cf/puwn/6LoJ/+i5Cf/ougn/6LoI/+m7Cf/pugr/6LoJ/+m7Cf/puwn/6LoI/+i6 + Cf/puQr/6LoJ/+m7Cf/puwn/6LoI/+m7Cf/pugr/6LkJ/+m6Cv/pugr/6boK/+i5Cf/nuQj/6boI/+m5 + B//otwf/6bkI/+i4B//mtwf/57gI/+e4CP/mtwn/57gJ/+e4Cf/ntwn/57gI/+e4CP/nuAf/6LkI/+i4 + Cf/nuAj/57gI/+i4CP/puQn/6LkJ/+i5Cf/nuAn/6LkI/+e5CP/ouQn/6LkJ/+e4CP/ouQn/6LkJ/+i5 + Cf/ouQj/57kI/+e5CP/ouQn/6LkJ/+i5Cf/ouQn/6LkJ/+e5CP/ouQj/57kI/+i5Cf/puwn/6boK/+m6 + Cv/ouQn/6LkI/+m5B//puQf/6bkI/+q6CP/qugj/6roI/+q6CP/qugn/6bwJ/+q8Cv/puwr/6bsL/+q8 + Cv/qvAr/6rwL/+q8Cf/qvAn/6rwJ/+m8CP/qvAn/6bwI/+m8CP/puwn/6LoJ/+i6Cf/puwn/6bsJ/+m6 + Cf/pugr/6boJ/+m7Cf/puwn/6LoI/+i6CP/ougn/6LoJ/+i6Cf/ougn/6boJ/+i6CP/pugn/6boJ/+m6 + Cv/pugr/6LoJ/+i5CP/pugj/6bkI/+m4B//otwf/6bgI/+i3B//mtwj/5rcI/+a3CP/mtgr/5rcJ/+a3 + CP/mtwj/5rcI/+e4CP/nuAj/57gI/+e4CP/nuAj/57gI/+i4CP/nuAj/6LkJ/+e4CP/nuAj/57kI/+e5 + B//nuAj/6LgI/+i5Cf/ouQn/6LkJ/+i5Cf/nuAj/6LkJ/+i5Cf/ouQn/6boK/+i5Cf/ouQn/6LkJ/+i6 + Cf/pugr/6bkK/+i5CP/puQj/6bgG/+i5CP/ouwz/6bsQ/+m9Ff/pvRj/6r0a/+m9F//pvhf/6r8X/+q+ + FP/qvhD/6r0N/+q8Cv/quwn/67sH/+u7CP/quwn/6rwL/+q8C//qvAv/6r0J/+q9CP/qvQj/6r0I/+q9 + CP/puwn/6bsJ/+i7CP/puwn/6bsK/+m6Cv/puwn/6bsJ/+i6CP/puwn/6boJ/+i6CP/ougj/6boJ/+m5 + Cv/ouQn/6LoJ/+i6CP/puwn/6boJ/+i5Cv/ouQr/6LoI/+i6B//qugj/6rkJ/+m4CP/ntQn/57YJ/+a2 + Cf/ltQn/5bUJ/+W0Cv/ltQn/5LUI/+W1Cf/ltgj/5rcI/+a3CP/mtwj/5rcI/+e4Cf/nuAn/57gI/+e4 + CP/nuAj/6LkJ/+i5Cf/nuAj/57gI/+i5Cf/nuAj/6LkI/+i6CP/ouQn/6LkJ/+i5Cf/ouQn/6LkJ/+i5 + Cf/ouQn/6boK/+i5Cv/ouQr/6boJ/+m5B//pugf/6boK/+m8EP/pvRr/6MIl/+nAKf/muyT/5roe/+e8 + Hv/nwCX/58Y0/+jJP//nyT//5sk9/+fJP//pyjz/6Mk7/+nINv/pxi//6sQm/+rCGP/rvg//670I/+y7 + B//rvAn/6r0K/+q+Cf/qvQj/6r0I/+q9CP/qvAj/6bwH/+m8B//puwn/6bsK/+m7Cf/puwn/6bsJ/+m7 + Cf/puwn/6LsI/+m7Cf/puwj/6boJ/+m6Cv/ouQn/6LkJ/+m6Cv/pugn/6LkJ/+m6Cv/pugr/6LoJ/+i6 + CP/quQn/6rgJ/+m4CP/mtAj/5rUJ/+OzCP/lswn/5LMI/+SyCP/ksgj/5LMI/+WzCf/ksgr/5bMJ/+W1 + B//ltQj/5rYJ/+a3Cf/mtwj/5rcI/+e4CP/nuAj/57gI/+e4CP/ouQn/57gI/+i4CP/nuAj/57gI/+i5 + CP/ouQn/57kJ/+i5Cf/ouQj/6LkI/+i5Cf/ougr/6LoK/+i5Cf/nuQb/6boL/+m9Fv/owCP/58Ux/+fI + Pf/nxTr/5bop/+SwFv/jrg7/4KkN/92hC//ZoAv/26MQ/96wIP/iwzj/5spG/+jKRv/oy0X/6MtH/+fK + Sf/mzEf/58xJ/+jMRv/oyz//6Mgy/+nDIf/qvxL/67wH/+q9CP/rvQr/6r0K/+u+Cf/rvgn/6bwH/+m8 + B//qvAn/6bwI/+m7CP/puwn/6bsJ/+i6CP/puwn/6boJ/+i6Cf/ougn/6boJ/+m6Cv/pugn/6boJ/+m6 + Cv/pugr/6boK/+i5Cf/ouQn/6boJ/+i6Cf/puQj/6rkI/+m4B//lsQr/5LIK/+OxCf/ksAr/5LAK/+Sw + Cf/ksAr/5LAK/+SvCv/krwr/5bAK/+WyCv/ksgn/5LMI/+W1Cf/ltgf/5bYH/+a3Cf/mtwj/57gI/+e4 + CP/nuAn/57gI/+e4CP/nuAj/57gI/+i4Cf/ouQn/6LkJ/+e4CP/ougj/6LkJ/+i5Cv/puQj/6bkI/+i9 + E//nwSP/5sQy/+bGPf/myEL/5sZA/+O6K//irhL/4qsI/92jC//Wlw7/1ZIM/9aWCv/Zmwr/3qIK/+Kp + CP/krxD/4bco/+XHQ//nzU7/6MtL/+jLSf/oy0r/6cxK/+nMS//nzUn/581L/+bNS//nzET/6Mcx/+rB + Gf/svgr/7b4I/+u/Cv/rvwr/674J/+q9CP/qvQj/6r0I/+m8B//puwr/6bsJ/+m7Cf/puwn/6bsK/+m5 + Cv/pugr/6LkK/+i5Cf/puwn/6LoJ/+i5Cf/pugr/6boK/+m6Cv/pugr/6boK/+m6Cf/qugj/6roI/+q6 + CP/irQr/4q0K/+GsCP/gqwn/4KoJ/+CqCv/gqwn/4asJ/+KtCf/jrgn/4q0K/+KuCv/jrwn/47AI/+Oz + CP/ktAj/5LUI/+W2Cf/mtgr/5rcJ/+e4CP/nuAn/57gI/+e4B//nuAj/57gI/+i5Cf/ouQn/6LkJ/+i5 + Cf/pugr/6LkJ/+i5CP/pvRP/6MIn/+fFOP/lxj//5shD/+fHP//luy7/4q4V/+KrB//dowr/zowQ/8h/ + Dv++cw//pVcW/44/HP+ANR//hDkg/5hTGf/Dhg//5rIH/+i3E//hvjH/5ctM/+nOU//pzU//6s1O/+rO + T//pzU3/6M1M/+jNTP/ozkz/6c5O/+rNSf/pyjj/6cMd/+y/Cv/tvwj/7L8L/+u/Cv/rvgn/674J/+q9 + Cf/qvAr/6rwK/+m7Cf/puwn/6boJ/+m5Cv/pugr/6LkK/+m6Cv/pugr/6LoJ/+i6CP/ougn/6boJ/+m6 + Cf/ouQn/6LoJ/+i6CP/ouQn/6bkJ/+m4CP/Ymgv/1pkM/9SXDP/SlQz/0JIK/8+QC//Ojgz/z48M/9OU + DP/ZoAv/36kK/+KsC//gqwr/4KwK/+GvCf/isQn/47II/+WzCP/ltQn/5rYI/+W3B//mtwj/5rcI/+e4 + CP/nuAj/57gI/+i5Cf/ouQn/6LkK/+i6Cv/ouQj/6bwS/+fCKf/mxTn/5cU+/+fIQP/nx0H/5r4x/+Ku + GP/iqwr/4KYL/9CODv+8cRP/vG4U/6BPGf9qFib/VgQt/1UBL/9VAi//VAEv/1MAL/9XCCz/hT8e/8aO + Dv/stwf/6bsZ/+PCOv/nzVP/6dBY/+nPVP/pz1P/6c9S/+jPUP/oz1H/6M9P/+fOT//ozlH/581O/+jJ + Of/rwxf/7r8I/+2/C//svwv/7L4J/+u+Cf/rvwn/674J/+q9Cf/pvAj/6bsJ/+m6Cf/pugn/6boJ/+m6 + Cv/pugn/6boJ/+m7Cf/puwj/6bsJ/+i6CP/ougn/6LoJ/+i6CP/ougn/6bkI/+m4CP/Wlgv/05UM/9GT + DP/QkQv/zo0L/8yLDP/LiQv/yYUM/8aDDP/Egg3/y4oN/9aZC//dpwv/3qoL/96qCf/frAn/4q4J/+Sv + Cv/jsgn/5LQJ/+S0CP/mtgj/5rcI/+a3CP/muAj/57gI/+i5Cf/ouQv/6LoJ/+q7C//owCD/5sU2/+bF + Pf/mxkD/58hA/+W/M//irxv/4qkL/+CnCv/SkhD/vnQS/7JkFf+zaBX/l0Qe/10HK/9ZCC3/Xg0s/14M + Lf9hDCz/Xwws/18NLf9eCy7/VAEv/1gKK/+ISB3/zpgN/+26C//nvR//48VC/+jRWv/p0Vz/6dBY/+nQ + V//p0Vb/6NBV/+fPVP/nzlP/6c5S/+jOUv/nzk3/6ckt/+3CDf/twAr/7cEN/+y/Cv/rvgn/6r4J/+q9 + CP/qvQj/6rwJ/+m7Cf/puwn/6boJ/+m6Cv/puwj/6LsI/+i6CP/puwn/6LoJ/+m7Cf/pugj/6LoI/+m6 + Cf/ougj/6boI/+q6B//UlQv/05QK/9GRC//Ojwn/zYsL/8uIC//Jhgv/x4QK/8aCC//Ffw3/w3wM/8N8 + Df/JhQz/1ZgL/96mCv/gqgr/4KoJ/+GsCf/jrgr/47AK/+OyCf/ktAj/5bUJ/+a2Cf/mtwf/57gI/+i5 + Cf/ouQn/6L0U/+jBL//kxj3/5cc+/+bHPv/kwDb/4bEc/9+pDP/hqAv/1ZcO/8B3Ef+xZRP/rWEV/65h + F/+VQB//Xwks/1wJLP9hCi3/YAkt/2EKLf9gCS3/XgYs/1wFK/9bBiv/Xwwt/18LL/9TAC//Ww0r/5BR + G//Unwz/7r0M/+jAJf/lyk3/6dFh/+rRXf/p0Vn/6dBZ/+nRWv/o0Fn/6M9W/+fPU//mz1L/5tBV/+jN + Qf/rxRn/7cEJ/+zBDf/qwAv/678K/+y+Cf/rvgj/6r0J/+q7Cv/puwn/6bsJ/+i6Cf/puwn/6bsJ/+m7 + Cf/puwn/6bsJ/+i6Cf/ouQn/6LkJ/+i5Cf/quQn/6rkI/+m6B//UlAv/05ML/9CPC//OjAv/zIoK/8mH + C//IhAv/xoEM/8R+DP/DfQz/w30M/8J7Df/AeQ3/wHgO/8aCDv/Ulwv/3qYJ/+CqCv/gqQr/4KsK/+Ct + Cf/isAn/47IJ/+S1CP/mtwf/6LgI/+i5Cf/lvRj/5MIz/+XFPP/kwjX/47ko/9+wG//fqA3/36cJ/9ea + Df/DfRH/s2cR/65iE/+pXRb/pVYb/5RAIv9lDCz/Xggu/2AKLf9gCS3/YAou/1wHLP9hCiv/bBov/3Mi + Nf9tHDP/YAos/1wEKv9gDC7/XQkv/1IALv9eEyn/nF0a/92pCv/xwQ7/6cIv/+XNVf/m02T/6dJe/+rS + Wv/p01r/6dJa/+nRWP/p0Ff/6M9U/+fPVv/nz1H/68ko/+3CCf/twQz/7MAM/+y/Cv/rvgn/674J/+q9 + Cf/qvAr/6bsJ/+m7Cf/puwn/6LoI/+m7Cf/puwn/6LoI/+i6CP/ougn/6boJ/+i5Cf/puQn/6bkI/+m5 + CP/Ukgv/05IL/9CPC//Nigz/y4kK/8iFCv/Hgwv/xX8N/8R9DP/Cewz/wXkM/8B4Df++eA3/vncO/7tz + Dv+9cw3/xX4N/9OVDP/dpgn/3qgL/92oCv/fqwr/4K0K/+KxCP/jswj/5rUK/+W7Gf/ivy//48At/+O0 + H//eqBH/3aQK/96oCP/ZnQ3/xoAQ/7RoEv+uYxP/rGAU/6ZVGf+hTh7/kj4j/2gRLP9eBy7/YQks/18I + LP9hCi3/XAYs/28dL/+RSTv/nFdE/6BdSP+hXUz/lU1G/3YnN/9dCSv/WgYq/2ENLf9dCS//UgAw/2Ma + Kv+nbBf/5bII//DFEf/oxDj/589b/+rUZf/q01//69Nd/+vSXP/q0V3/6dFa/+jRVv/nz1f/5tBX/+nL + Nf/swwz/68IM/+zBDf/swAv/678J/+u+Cf/qvAr/6bsJ/+m7Cf/puwn/6LoI/+m7Cf/ougj/6bsJ/+i6 + CP/ouwj/6LoJ/+i5Cf/ouQn/6bkJ/+q5Cf/Skgv/0pIL/8+OCv/Oiwv/y4gL/8iEC//GgQv/xH4M/8J8 + DP/BeQ3/wHgL/793DP++dQz/vHQM/7tzDf+6cQ7/t20P/7huEP/BfA//0ZUM/9ymC//dqQr/3akJ/96q + Cv/grQv/4rIO/+C0Fv/gsBf/3agO/9qjCP/epgv/2qAN/8uEEP+4ahH/r2IS/6xgFP+mWBj/oE8Z/5pI + H/+POiT/ahMr/14ILP9hCi3/Xwgs/2AJLf9bBiv/bx4u/5dSPv+bWUX/nFhG/5xVRP+dVUT/o11J/6dk + UP+VTkf/cyQ0/1sHK/9bBS3/Yg0u/1wHL/9PADD/bCUl/7R6E//quAj/8cUZ/+bIQf/m0WD/6NVm/+nT + Yf/q0mD/6tJd/+vSW//p0Vn/6NBY/+bQWv/pzT3/7cMO/+zCDP/swQz/6r8K/+q/Cv/rvgn/674J/+q9 + CP/puwn/6bsJ/+m7Cf/puwn/6bsJ/+m7Cf/puwn/6LoI/+i6CP/quQn/6rkJ/+m4CP/Tkwv/0pIK/9CP + Cv/NjAr/yogK/8iFCv/GgQr/w30K/8F7C//AeA3/v3cM/751DP+8cg3/unEN/7hvDv+4bg//t2wP/7Zr + D/+zZhH/tGcR/795D//Slgv/3KYJ/96qCv/eqgv/2qQN/9abCv/WmQf/2Z8K/9ifDP/NiA//uW0R/69i + Ev+sYBP/p1sV/59RGf+ZSBz/kj4j/4UxKf9pFCv/Xgct/2EKLP9fCCz/YAot/1wGLP9rGC3/lE8+/5lV + Qv+QRDX/iz45/5JPT/+dX17/nVxV/5pPQf+iW0f/p2VS/5JLR/9vIDP/WgYr/1wIK/9hDi7/WQUv/1IA + L/90LyT/vIUU/+u+C//wyCD/6MtK/+fSZ//q1Wb/6tNg/+rSX//q0l3/6tFd/+nQWP/o0lr/6c0//+zE + D//sxAz/7MEM/+u/Cv/svwr/674J/+u9CP/qvAr/6bsJ/+m7Cf/puwn/6bsJ/+m7Cf/pugn/6LoJ/+i6 + CP/puQn/6bkI/+m4B//TlQr/05QK/9CQCv/NjQn/yokK/8iGCv/Hggr/xH4L/8J7DP/AeA3/vnUM/7xz + DP+5cQz/uXAN/7dtDv+1ag7/s2YQ/7JlEP+zZBH/r2IS/6teFP+tXxT/uHAQ/8OADf/JhQv/y4YK/8yI + Cf/Jhw3/wnsQ/7ZpEv+tXhT/qV0U/6ZZFv+fUBr/mkgd/5I+I/+IMyj/fCoq/2kULf9eCSz/YQot/2AJ + Lf9hCiz/XQYs/2cSLf+RSzz/mFVC/4k+Mv+fbnT/y8DK/9vh6v/e6PP/3OLt/8m0uf+mbmb/nFFC/6Zg + Tv+nZlX/jkhG/2sbMv9ZBCn/XAkr/2AOLv9WAzL/UgMv/3s5Iv/EkRL/8sQN/+/KKf/lzlT/59Vq/+rT + Zv/r02H/69Jg/+rSXP/p0Vn/59Jb/+nOQP/sxA//7cIO/+zBDP/svwr/7L8K/+u+Cf/qvAr/6rwK/+m7 + Cf/puwn/6bsJ/+m6Cf/pugr/6LoJ/+m7Cf/ouQn/6bkI/+m5B//Vlwr/1JYK/9KSC//Pjwr/zIoL/8mH + C//IhAv/xYEK/8N8DP/AeQv/vXYL/7pzDP+5cA7/t24N/7ZrD/+1aA//smUR/65hEv+rXhP/qVwU/6lc + Fv+nWRf/pVQZ/6RSGP+lVRj/qFgX/6pYFv+nVhf/pFUY/6NWF/+iUxf/nk0b/5lIHv+SPyL/hzQl/30p + Kv90IC7/ZxQt/18KLP9gCS3/YAkt/2EKLf9eBy3/Yw4s/5BGO/+bWEP/hjsy/7GRmf/r+P//6PX7/+Lu + 9v/j8fn/4u/3/+b3///h7vj/xLC0/6VqYv+dU0X/p2NS/6hlWP+MQ0b/aRcy/1kEKv9eCS7/YAww/1UB + Mf9WBi//hEQi/82cD//zyA//7s01/+bRYv/n02v/6tNk/+rTYv/q01//6dFa/+jSXv/pzDn/7cQL/+zE + D//swgv/68AK/+q+Cv/rvgn/6r0J/+m7Cv/puwn/6bsJ/+i6CP/puwn/6boJ/+m6Cv/ouQn/6boI/+m5 + B//Xmgn/1pgJ/9SUDP/QkQv/zY4K/8uKC//Jhwv/xoMK/8R/C//Cegv/vncM/7t0Df+5cA7/t20O/7Vp + D/+yZg//sGMR/61gE/+pXBT/pVkV/6JVGP+iUhr/oVEb/6BQG/+fUBr/n08b/59PHP+eTRz/m0sc/5dH + Hv+UQiD/kDwi/4kzJv9+Kir/dSEu/2wYMf9kDzD/Xwot/2AKLf9gCS3/YAkt/14ILv9gCyz/i0E4/5xa + Q/+HOzD/rIeM/+36///h6e//4ev0/+Xy+P/c4+n/5fb8/+Pz+//h7PX/5fr//9/r8//Cqq7/pGZf/59W + SP+raFf/pWVY/4Y+RP9mEzD/WQUq/14LLf9hDC//UwAx/1cLLf+PUR//2KcL//XPF//s1Ez/5tJs/+nT + ZP/p1GH/6tJg/+rQXP/p0l3/68ws/+7ECf/sxA3/7MIL/+vACv/rvgn/670J/+q8Cf/pvAj/6bwI/+i6 + CP/puwn/6bsJ/+i6Cf/ouQn/6bkJ/+q5B//ZnQv/2JwK/9WYCv/SlQr/0JIK/86OC//Ligr/yYYL/8eC + DP/Dfgv/wXsL/714DP+7cwz/uG8O/7VpEP+xZhH/rmES/6xeE/+oWxX/pFcW/6FSGv+cThv/mUsc/5dH + H/+WRSD/lkMi/5NBIf+RPyD/jTsh/4g2Jv+EMCn/fikq/3YiLf9vGTD/aBMw/2EOL/9eCi3/Xwot/2AJ + Lf9gCS3/Xgku/1wJKv+FPTT/m1pE/4k+MP+keX//6vb7/+Hq8P/g7fb/6Pb1/6d9eP+UWVj/s4yK/9fT + 1v/o+///4vL6/9/x9//m/P//3efw/76ipv+iZFz/oFpM/6lrW/+iYlr/gThD/2IQLv9aBSr/YA0t/14L + L/9SADD/WxEs/5dfGf/ftAr/9do+/+rXav/o0WT/6tRj/+rSYP/q0l3/6dJa/+7KHv/sxAr/7MQN/+zC + Cv/swAr/674J/+q9CP/pvAf/6bwI/+m7Cf/puwn/6bsJ/+i6CP/ouQn/6bkJ/+q4CP/boAr/2qAK/9ic + C//VmQr/0pUK/9CRCv/Ojgv/zIoL/8mHC//Hgwr/w38M/797DP+9dwv/unMN/7duDv+zaQ//rmQQ/6pe + E/+nWRb/olUY/55RG/+bTR3/lkcf/5JBIv+PPSX/jDol/4g0Jv+DMSf/gCso/3slK/91IC3/bhow/2cU + Mv9iEDL/YA0v/14KLf9gCS3/YAkt/2AJLf9fCi3/XAYt/4A2M/+bWUT/jEEy/51rcf/m8ff/4u3x/+Dr + 9f/p+Pr/qn91/4E+N/+WVlP/mlpY/5hbVv+3j47/3Nzf/+f9///f8ff/4vX7/+f+///b4ur/vZue/6Jg + W/+iXVD/q2xf/6BfWP9+M0D/YA0t/1kEKv9iDS7/Xgov/1EAMf9hGSr/pGwX/+PNUP/x4Gv/59Fk/+jU + Zf/p02H/59Jg/+rRT//uxxD/7sUN/+vDDP/swQz/68AL/+q+Cv/rvgn/6bwJ/+m7Cf/puwn/6bsJ/+i6 + CP/ougj/6rsJ/+q5B//dowj/3aQK/9qhCv/YnQv/1poK/9OWCv/Rkwz/zo8L/82MC//LiAv/x4QL/8N/ + C//BfAv/vncM/7pyDf+3bQ//smgP/61hEv+pXBX/o1YY/59QG/+ZSh7/mEgg/5ZDIf+MOCb/gy8n/4Ev + Lf95JS7/cxwv/2sZL/9qFTL/ZREz/2APMf9fDi//Xgot/18JLf9gCS3/YAkt/18KLv9bBiz/fy8z/5xZ + RP+PRDP/mGFj/+Po8f/j7vT/3+n0/+v7/P+thXv/hEA4/5JVVf+tkKj/q4ub/6Fnbf+aWln/nmFf/7ue + mv/g7fL/4fP6/97v9f/i9f3/5vz//9nd5P+2kpX/oV1Y/6RfU/+rbmD/nVxW/3kvPv9eCy3/WgYq/2EO + L/9cCTL/TwAu/2QiLP+5mUz/8N9o/+fSZv/o02X/6NJf/+jSYv/rzjn/7sUK/+zED//swgz/68EL/+u/ + Cv/rvgn/6r0J/+m7Cf/qvAr/6bsK/+i6Cf/ougj/6LoI/+m5CP/epgj/3qYJ/92lCf/bogv/2aAJ/9ec + Cv/VmAr/0pQK/8+QC//OjQv/y4kL/8iGC//GgQv/wXwL/714DP+7cw3/t24N/7JoEP+sYRT/p1sW/6JU + Gf+eUB3/izoi/3AdJ/9eCCX/YA0s/2AJIP9wGCj/cx40/2gVMv9lETH/Yg8w/18NMP9fCy7/Xwkt/2AJ + Lf9gCS3/YAsu/1kFK/94KTL/mlhD/5JIN/+RV1j/3uHq/+Tw9v/g6vT/6fv9/6+Mf/+FQDn/k1NP/7KU + r//EvNf/w7jU/72pxv+uiZz/oWlz/6Nkb//c3eT/5Pv//+f7/f/l+v7/5Pn+/+P4/f/m/f//1dje/7KM + jP+gXlf/pGJY/6tvY/+bWVf/dio7/10IK/9aByv/YQ8v/1oHMP9QACn/soxM//Hfav/m0mX/6tRk/+rR + X//o013/7soi/+7FDP/rxA7/7MIM/+vAC//rvwn/670K/+m7Cv/puwn/6bsK/+i5Cf/ougj/6LoJ/+e4 + CP/ksgb/4awH/96nCf/dpgr/3KQK/9uhC//Yngv/1psK/9WYC//Skwv/z48L/8yLC//KiAr/x4ML/8N9 + DP++eQz/u3QN/7hvD/+zaQ//rGIT/6tgF/+LNiL/XQku/1EAJP+DU2T/08nD/554hv9mFzH/XwMc/20W + Lv9sGTT/ZREw/18LLf9gCS3/YQkt/2AJLf9eCy3/WgUs/3MjMP+ZVUH/k0o5/45QT//Y2OL/5fL5/97o + 8f/k8fj/t5KI/4c8N/+STU3/sI+n/8O41P/Uzt3/19Xh/8W/2f/FwOD/uaTB/8u7y//m/v//4e3s/6l4 + ff+whov/09HT/+D1///T5vr/ydv6/8nc/v/FwtT/sYSG/6JeVf+mZVr/rW9l/5hUVv9wJTn/WQgr/14L + Lv9cCDP/ZB8u/+TOYP/s12r/6dRm/+jTY//q0mT/6tFK/+7HDv/txQ7/68MN/+rCDP/qwAr/6r0K/+q8 + Cv/qvAr/6bsJ/+m7Cf/puwn/6boI/+m5B//puAf/6LgH/+SyCP/hqwn/3qYK/92lCv/cowr/2aEI/9ie + Cv/Xmwv/1JcL/9GSC//Ojgr/zIoM/8mGDP/FgQz/wXwM/713DP+5cQ7/tWwR/61gF/9kEin/XQsw/1MC + If+1n53/6+nd/+jn3f/UzMr/lWp3/2ESKf9gByD/aBIw/2AJK/9gCCz/YQkt/2AKLP9bBSv/bx0u/5hT + QP+TTDr/jEhG/9PO2f/m9Pn/3ujv/+Py+v/S1d//fTA3/49DQf+ujKL/w7rT/9XR3P/p8fL/1dDf/83G + 2//Bs9L/0s7m/+X9///n9fL/qGlq/41HUP+hZ23/l11n/515jf+vq9H/vs78/73N/v+/0/7/yd39/8S9 + zP+tf3//oV5W/6hoX/+rb2b/kk9U/2wgOf9cCi3/UgIn/8CcU//z4m7/6NNn/+nWZv/q02D/6dRi/+3N + Kv/vxgv/7MUP/+vDDP/qwQv/6r8K/+q9Cv/qvQn/6bwI/+m7Cf/ougj/6rsJ/+q5CP/otwf/6LkI/+e5 + CP/mtwn/5LIJ/+CrCf/dpgr/3KQJ/9uiCv/aoAr/2Z4K/9ecCf/Ulwr/0JIK/82NC//LiAv/x4UM/8OA + DP++eA7/v3YQ/6peF/9cDCr/YA0w/1QEI/+3oJ7/5eHY/9rRzf/i3dj/6/Hs/87JzP+IV2T/XAYg/18I + K/9gCCz/YAks/1wFLP9rGS7/lFA9/5ZOPP+JQT3/zcPN/+n1+//g6O//4ez0/9/s8//i8/7/vK6+/6R/ + lP/EutH/wbPK/8rF2P/Sz97/xrrS/8Sz0P/Qyd//4vX7/+j69v+qcnP/kEtR/5xgbv+jf5r/n3OI/5Ra + bP+KTmL/mXOR/7a22v/B2P//v9T9/8Ha///K4Pv/w7nH/616e/+iYFf/rG1j/61wav+TWWb/Vwo2/5Fb + Pf/25m//6dVr/+rWav/r1GX/6NRm/+rST//vyBD/7sYQ/+zEDf/qwgz/6sAL/+q+Cf/qvQj/6b0H/+m7 + CP/ougn/6boJ/+q5Cf/ptwf/6bkJ/+e4CP/nuAj/57kI/+e4CP/jsgn/4KoK/96lCf/cpAr/3KMJ/9qh + CP/ZoAn/150I/9SYCf/QkQv/zY0L/8qLC//GhA3/x4IQ/65lF/9bCyr/YQ4x/1MEJP+2oaH/6Off/9XK + xv/Eo6L/08O+/+bj4//k6Oj/n2l3/2IKKf9fCSz/XQYt/2cTLf+QSzv/llE//4U9N//Htr//6vb7/9/m + 7v/g6/P/3+v0/9/r9v/e7fj/4/j//9rq9P/Mytz/xrzU/8K21P/Bu9f/w7rY/83H4P/h9vz/6P38/6x4 + ev+OTEv/l09a/5dcb/+ec4f/m2d9/5Vedf+TXHb/j05m/4tNZP+cepL/ur3f/8Td///A2f7/xuH//83h + +v/AtcD/rXh4/6ZhXP+5iYn/iFl//2YeJ//r12z/7dlv/+nWbf/q1Wn/6dNl/+nVY//tzCj/7scM/+zG + D//rww3/6sEL/+q/Cv/qvQn/6r0H/+m8B//puwj/6bsJ/+i6CP/puAj/6bkI/+i6Cf/ouQj/6LkI/+e4 + CP/ouQn/57gJ/+OyCf/fqwn/3acK/92mCf/dpQj/26MJ/9mhCv/Ynwr/1p0J/9GWCv/Mjg3/0JAN/7Zw + Ff9aCin/YQ4x/1IEJP+3oqL/6uji/9XLyP++mpr/wJuY/9XIxv/f4OL/2tTY/349Uv9XACP/ZhAt/4tB + N/+YUT//hDkz/76or//s9vz/4Obs/+Lq8f/g6vL/3uny/9zn8f/Y3er/z83f/8fE2//IxN7/wrXP/7eh + uP+7p7v/tZ+z/7Ocr/+8sL7/n3F//4hHSf+WUVn/l1Vo/5pkdf+aYXP/nmh7/59uhf+faoX/p4Ce/6OB + nv+WYXn/k151/6GFoP+8xef/w9r8/8HZ+v/H4///zeD2/8Gxuv+ygIP/pHWV/1cOL//IqVj/8+R1/+jW + b//p12v/6tVn/+jVZ//r0Ef/78gN/+3HEf/sxA7/68EL/+u/Cv/qvgn/6bwI/+m7CP/ouwj/6bsJ/+m6 + CP/puAj/6bkI/+i6CP/ougf/6LkI/+i5Cf/ouQn/6LkK/+i6Cv/ouQn/5bUJ/+CtCf/fqQf/3agI/9yp + Cf/dqQr/3qoI/92oCP/Xnwv/2J0N/756Fv9bCyr/YQ4w/1EEI/+4oqL/6+nk/9XKyf/AnZ7/xaGf/9jN + y//Z2d3/4+fo/72uu/9TAyX/cB8u/5NKPP+HOjL/t5mh/+v1+//g5ez/4unw/+Ho8P/f5/D/2+Hr/9jY + 4//S0uL/y8rh/8a40f+phaH/czRW/2UXQP9wKUr/i1Jo/5xref+JSmD/byA5/4hAUP+UV2b/m2R1/5xl + eP+bYXb/j09n/4E/Wv94MFH/eDxg/39Sd/+CSm3/j1x4/5hpf/+fb4P/ur/e/8Xf///K5P7/xd///9jw + ///l8fP/y77Q/2AbR/+dbD//9+p4/+fVcf/p2G3/6dZq/+jUZP/p013/7ssc/+/HD//sxQ7/68IM/+u/ + C//pvgn/6bwJ/+q8Cv/puwn/6rsJ/+q6CP/puQf/6roJ/+i5Cf/ougj/6boJ/+i5Cf/ouQn/6boK/+i5 + Cv/puwn/670K/+u9C//mtwj/4q8J/+GrB//jsAf/47IL/+KxCf/frQj/5LAK/8aIFv9aCir/YQ4w/1EE + I/+4o6P/6Orm/9bPz//Bnp7/wpua/9rPzv/f4eb/29zf/97i6P9wNE7/Xwwn/3wrLP+meH//6vP7/97k + 6v/h5+7/4efu/+Dl7v/b3uj/2Nzo/9bb6f/JwNH/s5Gk/5xgd/9dDTD/Ywsb/4QrF/92IBn/Xwgo/3Aj + Rv+ERF7/eTlO/3AoQP90LUb/mF1t/4dEXP9oGjn/VAIk/0wAGf9PABr/TgAb/04AGP9MABj/TQAa/2Ia + Ov+GVGv/u8He/8LZ+f+kpMf/w9Pn/+v7/v/m9fj/8f///45si/93Myn/8uR5/+jZdP/p2W//6dZq/+nV + Zf/p1Wj/7c8z/+/HDv/rxg//7MMM/+zAC//qvwr/6r4J/+q8CP/puwn/6bsK/+i6CP/ouAf/6LoJ/+i6 + Cf/ougj/6LoJ/+i6Cf/ougn/6LoJ/+m6Cf/puwn/6rwK/+u+Cv/qwQr/68EL/+e/FP/mvBj/5rUG/+a2 + CP/lswz/6rsL/82UFP9bCir/YQ4v/1EEI/+4pKX/5ujl/9zZ2f/Xzs7/zri4/9vQ0f/f4+j/3N3h/+jy + 9P+bfY3/VAAb/20gMP/Owsn/4unw/97i6v/f5ez/3+Ps/9re6P/Y4Ov/0M7c/7iap/+fbHr/mV1v/2sd + Pv9lECL/mkUU/7FbDf+8aAv/o0wP/3gcGP9iCCv/biRH/2ISMP9XACL/Zhg0/1cGJ/9QAB7/dyxD/6Bx + fP+4nKf/wbS9/8Gzwf+zmq3/j2aA/2MfPf9IABD/ZidE/2wxUf9IABX/Yyg9/93a2//u+fz/7////52N + pv9pKCf/8ON6/+rbdv/p2XL/6dhu/+rVaf/o1Wj/6tJL//DJEf/tyBH/7cQO/+zCDP/qwAv/6r4J/+q9 + CP/qvAj/6rwJ/+i6CP/nuAj/6LoJ/+i7CP/puwn/6LsI/+i6CP/puwj/6LoI/+m7Cf/qvAr/6r0J/+y/ + Cv/swAz/7MIJ/+rLM//m0VT/58s6/+i/HP/otgr/7r8L/9GZGP9bCiv/YQ4w/1IEJP+4pKb/6Ovp/9TN + z//Fqaz/1MnJ/97g4f/h5uv/3uTq/+Lp7//S0t3/XxYw/1sIH//Qw8v/4ujx/9vc5P/d4ur/29/p/9fc + 6P/CtcD/qHiD/51jcf+eZ3f/gT1W/10GJv+POBf/rFUO/7hjDf+4ZQv/vWoK/7xlDf+fRhD/dBca/1sE + KP9qJ0T/Uw4s/2UYL/+8lZj/7/Lw//v////5/v7/+P////n////9/v7/+P///+Pt9P+qlKn/Xxw6/1QC + IP+JTmj/WBQv/2YrO//r7uz/6f7//2UtWP+MWD7/9uyC/+nZd//p2nX/6dhx/+nWbf/o1Gr/6dRd//DM + Gv/vyBH/7MUP/+vDDP/rwAv/678K/+q9CP/pvQf/6bwI/+i6CP/puQf/6rsJ/+i6CP/puwn/6bsJ/+m7 + Cf/puwn/6bsI/+m8B//rvQj/6r4J/+vAC//qwQv/68QO/+nOR//n0Vj/6NRc/+nUXP/nzEb/8Moh/9Sb + Fv9bCyr/YQ4x/1MFI/+4pab/6evq/9TP0v+/n6X/waKl/9HK0P/RxtH/3OHn/9/m8P/k8/7/tau8/1QB + Hf+DSVT/5Obq/+Dp8f/Y2eX/19vn/8/L2f+lcXz/oWp0/51peP+VWm3/YRAz/3ohHf+lThD/t2AN/7tl + DP+7ZQv/umUL/7pjDf/CbQ7/pE4Z/1AAIP+heoP/u622/9bHyf//////4u/y/62isP+KX3L/fUhd/4JQ + Zf+dfYz/zcvN//f+/f//////4eXs/9HH0//7////xLvK/04BI/9+WGj/elFz/1YNIv/XxXP/8OWA/+nc + ef/r3Hb/6tlz/+jXbv/q1Wv/6dVo/+7PK//uyA//7McP/+vDDv/swQz/678K/+q9CP/pvAf/6bwI/+i6 + CP/puQf/6rsJ/+m7Cf/puwn/6bsJ/+m7Cf/puwn/6bwJ/+q8Cf/qvQn/6r8K/+vADP/rwwv/7Mca/+fS + VP/o0Vr/6dFc/+rTX//n1GT/7uBj/9KtRv9aCSj/Yg8x/1MFI/+4pab/6ezr/9PO0//Bo6n/xais/9HI + zv/Bqrb/xLG9/9DL1v/a3+3/5fj//8jF1v9zOFD/cio6/7ecpf/c4un/2+Tx/8e6zP+hbHf/n2p1/51n + d/95M0//YAoj/5lBE/+xWA7/vGYM/7tlC/+7ZQr/u2cL/7pkDv/BaA7/dyci/1YKKv/CmJD/8Pf0/+3/ + //+unqz/ZyNB/1IBJf9pI0b/cDdc/2osUf9aDi//VwYg/4dUZv/T0NH/9f/+//P9/v/o9/b//v///5qD + m/9AABX/YiEs/864cv/w6Yj/6N2A/+zdff/r3Hf/6tp1/+jYcP/p1mv/59dr/+vRPf/vyQ//7ccR/+zE + D//rwgz/68AL/+u+Cf/qvQj/6bwI/+m7Cf/puQf/6bsI/+m7Cf/pugn/6bsJ/+m7Cf/puwn/6bsJ/+q8 + Cv/pvgn/6r8K/+vBDP/rwwn/7Moq/+jRXf/o01z/6tRi/+nVbP/n02v/791p/9GzYv9aCir/Yg8x/1MF + I/+4pKb/6e3u/9PP1P+/n6T/xqis/9TJ0P/FsL3/w7C9/8Gvvv/Dtsb/0NDg/+L1/v/k+///oo2h/14Q + JP93PEn/vKqy/8vCz/+hbHr/nmV1/5VZbv9iETT/eyYb/6hQDv+5Ygz/vGYL/7xmDP+8Zwv/u2gN/71n + CP+ybzD/Ugop/5BXX//6+vD/8f3+//T///9/WHL/UgAZ/10KL/9yPmv/qJnB/7vD6f+7w+v/hV2C/0sA + Ev9tMED/6Ojn//H8///p/v7/nYig/1cONP+IUEj/5tqH//Lsjv/o34b/6uGC/+3cff/s3Hn/6tt3/+jZ + cv/p12z/6NZr/+vSS//wyhL/7cgS/+zFD//qwwz/68EL/+y+Cv/qvQj/6bwI/+m7Cf/puQf/6rsI/+m7 + B//pugn/6boK/+m6Cf/puwn/6rwJ/+q9CP/qvwr/678L/+3BDf/sxAr/68w3/+fSXv/o1F7/6tVj/+rX + av/m1nv/7t51/9OzYf9aCyv/YxAy/1MFI/+4pqj/5evs/9rc4f/OvsX/xqmu/9LHzv/EsL3/xbTB/8O2 + x//BscP/zMjX/9zt+f/b7fn/4PT8/6mInP9/QVf/YBct/3Y3TP+PV2j/onB7/4E/Wf9eCCT/lj8S/7FZ + Df+8Zwv/u2YL/7xnC/+8aAz/umMH/8Z6JP/Ap5H/RgAe/2UfR/91NV7/cC5W/3M2XP9lI1H/WQ0n/7yH + KP+ELRX/VwAn/3A/a/+tps7/eEVs/1MAHP+8np//+f///+Dx9P91TGz/UQAg/62GXv/y75H/7eiO/+jg + if/q4oj/6uCE/+vfiv/s3H//69x4/+nadP/q2G//6tZs/+rTWP/uyxf/7ckS/+vGEP/rww3/68EL/+q+ + Cv/qvQj/6rwJ/+m6Cf/puQf/6bsH/+i7Bv/pugn/6boJ/+m6Cf/puwn/6rwI/+u+Cf/qvwr/68EM/+zD + Dv/txQ//7M9G/+jTXv/p1F//6dVl/+vXZ//o1Gn/7t9w/9G1Zv9ZDCv/YQ8w/1UEJP+5pqn/5ejs/9vi + 6P/i7PP/2Nrk/9nV3//HtcD/wrC9/8S3yP/Etcf/z8vb/9/x/P/c7vr/2eb4/7GWsP+nfY//oXuO/38/ + WP9iEy//byY8/2QXOf90Hh7/o0sP/7VfDf+8Zgv/u2UL/7xnC/+9aQ//uV0A/9u2if/8+Hf/u5Mn/6Rx + Lf+lcSj/p3Mq/6ZzKP+jcC7/yqo8////Rv/87kX/zZQu/34iEv9ZCCv/Vggs/24sQP/s8e3/9f///4xq + hv9dDyX/3c2F//P1mf/o5JD/6eOO/+vkjf/o44n/6uKZ/+nhlv/s3n//7Nx5/+radf/q2HD/6ddr/+rW + Yf/uzB7/7ckR/+zGEf/rxA7/68EM/+q/Cv/rvgn/6r0I/+m7CP/puQf/6rsI/+m8B//puwj/6LsI/+m7 + Cf/puwn/6rwJ/+u+Cf/qvwr/68EM/+vEDf/sxhP/69BO/+nTYP/q02L/6NZm/+rXaf/o1W3/8OBy/9K1 + Z/9ZDCr/YRAw/1YDJP+4pan/5erw/9PP2P/Uztf/5vT6/+b0/P/c4+7/0MzZ/8e6y//DsMT/z8va/9/x + /f/e8Pz/1uHz/7ilwv+XX3P/n26A/6mAkv+fcIP/fT5U/14OMf9zHx//p1UR/79oCP+4YQr/u2YL/7xo + Df+8YgX/w3o1//fuqv/+6zH///w8///+QP///0P///9E////SP///0v///xL//vvSv/99VD///9Y//vt + Tv/Ffi3/WwEe/3U+Uv/z+/f/8P///3ZEY/9fESf/xbN2/+/qk//p45L/6+SR/+vjj//s44z/6uKI/+rg + gP/r3n7/7N17/+vbd//p2HL/6Nhs/+rWZv/uzif/7soS/+zHEf/sxA7/68EM/+u/Cv/rvgn/6r0H/+m8 + B//puQf/6rsJ/+i7CP/puwn/6bsJ/+m7Cf/puwr/6rwJ/+u+Cf/rvwr/68IM/+zEDf/uxxb/6tJV/+rU + Yf/p1WP/6dZn/+rXav/o1W//7uFz/9K2af9ZCyv/YRAw/1MEJP+4pKj/5e7x/6R7iP+GP1b/j152/7Wl + t//k8fj/5/j//9vm8//Rzd3/09Hf/9/y+//f8vz/1d/w/8TB3P+hdYf/jU1b/5JXav+ebH//mWR7/2kb + PP9VBCz/ayQp/6VhGf/AcQv/vGII/7tkEP+4XQD/27aH//31a//76TX/+upD//rrQv/860T/++tI//ns + TP/57E7/+u9P//zzUP/98lP//O9Y//r1W////2f/k11R/1UKLv/k4N3//////7e2xv9TCi3/UwYj/8y5 + ev/x7Zf/6OKP/+nikv/q447/6uKJ/+nhhf/r34H/6958/+rbd//p2XT/6dht/+rXaf/uzy7/8MoS/+3J + Ef/sxQ3/68IM/+vAC//rvgn/6r0J/+m7Cf/quQj/6rsI/+m7Cf/puwn/6bsJ/+m7Cf/puwr/6r0J/+u/ + Cv/rwAv/7MEN/+zFDf/tyhz/6tNb/+rTYf/p1WT/6tZo/+rXbP/o1nD/7uF1/9K3af9aCyz/YQ8x/1QE + I/+4pKr/4+ru/5xtev9tHzv/XAAa/2cPG/93PFj/u66+/+b2/P/l+f//3Ov2/93u9//g9P3/097v/8TA + 3v+yn7b/j1Jf/41PYf+QVGT/aiBA/2AKIf9tGiX/UwQw/1MNLv+BRSP/tnIX/8FqCf+7ah7/7uOi//3r + QP/97UH//O5C//3wRf/98Uf//PFM//zyT//88k///fJT//3zVv/881v/+/Bd///9XP/c0oD/ZiNA/0sA + F/+IXGv/9vz6///////S2uP/Uwwz/6SAXf/29pv/5+OT/+rlnv/q44//6uOJ/+rhhf/r34L/6919/+vb + ef/p2nb/6Nlu/+rYa//t0DP/7soT/+3JEv/sxg7/68IM/+u/Cv/qvgn/6r0J/+m7CP/quQj/6rwI/+i6 + Cf/puwn/6bsJ/+m8Cf/puwn/6r0K/+q/Cv/rwAv/7MIN/+zEDP/tyx//6dNb/+rTYP/q1Gb/6tdq/+rY + bv/o1nD/7+F3/9O3av9bCyz/YA8x/1UEI/+4pKr/4+rv/51wff9sHj3/bhci/6pRFv+VQBb/YAYa/3hB + Yf/Cu8n/6fv+/+Dy+v/f8fz/2uv3/8rN5v/Audj/ll9z/49QYf+RUmL/YBE5/3sgHf+oTAz/hS8Z/18L + J/9MAC3/XyIv/41RHf/Nomb///1///vmL//66Uf//e9H//zxRv/78Uv//PJQ//zzUP/981T//fNX//30 + Wf/5813//vpb/+3ne/9fHjv/bzVS/5hmfv9QABn/bzhM/7mttv+ZhZn/TAIl/8axef/x8Zz/6OST/+vl + kv/r5JD/6uOL/+vhiP/s4IT/6959/+zbe//q2nf/6Nlv/+nYbf/t0Db/7soU/+zJFP/sxw7/7MMN/+vA + C//qvgn/6r0I/+m8B//qugj/6rsK/+m7Cf/puwf/6bwH/+m8B//qvQj/6r0I/+y/Cv/swAv/7MIN/+3F + DP/tyyL/6tRf/+jVZf/q1GX/69Zk/+rYb//n13H/7+J4/9K4bP9ZCyz/YQ8x/1MEIv+5pKr/4unu/59x + fP9sIDv/bhQg/6hQEv/txiH/5bAg/5Q8EP9fBRz/h1ds/9zn6//h8/3/4PL6/9/z+v/a7vz/ro+i/45K + W/+OTWD/Xw40/4ApGf+uVQ3/umcK/6ZODv96Jhz/Vgcq/0oGLf9sOEj/vqNp//jyY///+Uf/++tB//nt + S//78k///fNR//30U//89Ff//PVb//v1Xv/79Vv//f10/3pCTf9jITv/4u7u/9Lh9/+tjaz/cjNM/1IH + KP9XCir/rIZn/+7wnf/q5pf/6eaT/+rljv/s44n/6eOE/+vghv/s34P/695+/+rbe//p2nf/6tlw/+rY + bv/t0Dn/7csX/+7JFf/txhD/7MMO/+zAC//rvgn/6r0I/+q9B//pugj/6rwI/+m8CP/puwj/6bwH/+q9 + B//qvQj/674J/+y/Cv/swAv/7MMO/+3FDP/tyyT/6tRf/+jXcP/o3ZP/6NuL/+rYb//n2HL/7+J5/9G4 + bP9YDCv/YQ8x/1QEI/+4pav/4+zx/6B3f/9tITz/bRcf/6lSEP/ouhv//+gh//3mJf/jrCT/chgO/4tr + eP/r////3u/4/9/x+v/k/P//0tzl/51qfP+MUGP/Xgsu/4syFv+xWA3/uWYL/71rCf+8Zgr/oEYS/3Eb + Hf9OABv/SQMt/3RBUv/Fr3X/+/hr///7Tv/67kr/+/BU//31V//99ln//fVf//rxYP///2z/qoZt/1AB + J//WztH/9v///9jr9v/H3v//w67A/7eNjv/YyZX/9vWe/+nnlf/s5pn/6uie/+vpqP/s6rP/6+u7/+vs + vv/q5Jv/699//+vdff/q23f/6dpx/+nYb//u0Tr/7ssW/+3JFv/txxD/7MMN/+zADP/svwr/6r0J/+m8 + CP/ouQn/6bwH/+m8B//puwn/6bwI/+q9CP/qvQj/674J/+y/Cv/swQz/7MMO/+3GDf/syyX/6dRg/+nV + Y//p2W3/59p8/+jZcP/o2HP/7+J5/9G5bf9ZDCv/YQ8x/1QEJP+3pKr/5vL5/6WHkP9rHzn/bBgf/6lU + Ef/swBz//N4h//TZJP///y//ro9T/2o4T//q/v//3/H5/+Dz+//h8/r/4PX8/66QoP+TWWz/Xgss/5E4 + Ff+0Wwz/vGgL/7xmDP+9aA3/v2kF/8FyI/+zeUv/dB0Z/0cAIf9FBi//eUpd/828fv/+/XH///pX//vv + U//88l7/+/Nl///9Y//k2YD/UgIs/6eFj//6////6Pb3/+T1+v/D1vP/uJuu/93Pmv/v8KP/5+ms/+zt + t//s7sD/7e/D/+vvv//s67T/6+im/+vllv/q4Yn/6uCC/+vcfP/r23j/6Nlx/+jYb//t0Tr/78oV/+zK + Fv/sxxD/7cMM/+zADP/rvwn/6r0J/+m7Cv/nuAn/6boJ/+m7Cf/puwn/6bsJ/+q8Cf/qvQj/6r4J/+q/ + Cv/rwAv/7MMO/+3GDf/tyyT/6NRg/+fVZf/o12j/6Nhq/+fZcf/o13T/8ON6/9K5bv9bDCv/Yg8x/1MD + JP+2par/6PT7/8O7w/9pITn/bxkf/6dSD//pvRz//uEj//faJf//8zL/q4hZ/2w5UP/p/f//3/H5/+Dz + +v/g8vn/5Pz//8O7x/+MT2X/YAor/5Q7Ff+zXA3/vWgM/71oDP/Aag7/u2AA/8uRVf///5T/784s/7hr + Kv9zFx//RgAh/0kINf+EVWP/18aE////dv/++l//+u5c////eP+MXV//ZR4+//P39v/s+/3/6/39/+r/ + ///H3fj/t6Cy/97Snf/r7qP/6eqn/+rro//q6J7/6uaX/+rmj//q5Iv/6eOJ/+viif/p4Yb/7N+C/+ze + ff/q23n/6dly/+nYcP/t0Tj/7swX/+vKFf/sxhD/7cMO/+vAC//rvwn/670K/+m7Cv/puQj/6rsJ/+m8 + CP/puwn/6bsJ/+q8Cv/rvgn/678K/+rAC//swAv/7MMO/+3FDf/uyyD/6NRe/+jVZf/p12n/6tht/+jZ + cf/o13P/8ON5/9K5bv9bDCv/Yg4x/1IDI/+3p6z/4+vy/+Lt9f+BVmn/XwUQ/7FbGP/y0CD//N0f//XW + Kv//9jb/rYhZ/204Uf/q/f//3/H4/+H0+v/h8/r/4fj+/9fn7v+NVmz/Xwcl/5c/FP+0XQ3/vWkM/71o + DP/Baw7/u18A/9Wqbf/89Xf//PQ/////TP/wzj3/tmUp/20SIP9CACT/TQ04/41gZf/bzoj///96/+bd + i/9PAS3/sZeg//r////r/P7/z8vS/8m+yP/H2/f/t6S3/9vNm//s7qH/6Oib/+npm//q553/6uec/+rn + l//r5ZP/6+OP/+vijP/q4Yj/7N+C/+vefv/q23n/6dly/+nYbv/u0Db/7cwZ/+zKFf/sxg//7MMN/+zA + C//svwr/6r0J/+m7Cv/quQf/6rwI/+m9B//puwn/6bsJ/+q8Cf/qvQn/6r4J/+q/Cv/rwAv/7MIN/+7F + Dv/uyhv/6dNc/+jVZv/p1mj/69dt/+nZcf/o13j/7+J6/9K5bf9bDCv/Yg8w/1MFI/+4p6z/4+vy/+Hq + 8f/GyNX/Yh04/2YQG/+9m1z/++tR//3mL///8zH/rYVc/283Uv/r/f//3/L5/+H0+//h9Pv/3/L5/+X/ + //+bdIf/XQEe/5lCFP+1Xgv/vWkL/71oDf/Baw7/u18A/929gv//+Gb/+uxC//rvTv/9/FP///9W/+/I + Qf+uWin/Zw8g/0EAJf9QDDn/oH14/5hrbP9fGzv/7O/u/+/6+//v////09Tb/62Hk/+2mqj/tJet/9jJ + nP/r76H/5+mj/+npnv/p5pf/6eWS/+vlkP/r5JH/7eSQ/+vjjf/q4Yr/6+CD/+rdf//q23n/6dlz/+nX + bP/vzzH/7cwa/+zKFf/sxw//7MMN/+zAC//rvwn/6r0J/+m7Cv/quwb/6rwI/+m8B//qvQf/6rwJ/+m7 + Cf/qvQj/6r4I/+u/Cv/swQr/7MMN/+3FD//tyhb/6tNX/+jVZv/o1mn/6Ndv/+jZcv/n2Xv/7+J7/9K5 + bv9bDCv/Yg4x/1MFIv+5qKz/5+30/9rh6f/i7vf/0dXm/4xnhv9WCTL/dDE6/8enZv///2P/spNj/281 + U//r/P//3/L5/+L0+v/h9Pv/3/H4/+n///+upLL/WgAa/5hCFf+zXA7/vWgL/7xpDf++agz/u2EF/+XL + kP//913//PBF//zyT//88VL//PJV//79Xf///lz/6cJF/6ZYKP9iDCD/SQAm/1IAJ/95PlX/8PTx//z/ + ///u+vz/8P///9Xf7v+8qLr/y6mV/+fjof/q66H/6uyz/+zwx//r8tD/6vDI/+jqsP/q5Zj/6uKK/+zf + hf/r34T/69+D/+zdf//q23j/6th0/+nXaP/vzyv/7cwb/+vJFP/rxhD/7cMN/+zAC//rvwn/670K/+m7 + Cf/qugf/6rwI/+m8B//pvAj/6bwJ/+m7Cf/pvAn/6r0J/+y/Cv/swAv/7MMN/+zFD//tyRL/7NJP/+jU + Z//o1mj/6ddu/+nacf/n2HL/7+N4/9K5bf9bDCv/YQ4x/1MEJP+5pqv/5+3y/9zj7P/d5O//4ez3/+j5 + ///Mz+L/hFx7/1ACK/+CTE7/gVBS/3E3TP/q/v//3/H5/+L0+//h9fv/4PP5/+b8///K1d7/Ww4n/5I7 + FP+yWw3/vWcM/7xoDP++agz/umUL/+jUmP//9lf//PFJ//3zUf/+81T//vNZ//zyW//79GD//v5o///8 + ZP/owEb/lkwx/1oJMv9eGDr/ZyQ8/7WdpP/0+vn/7vn7/+f5/f/G3Pn/y8C6/9/Omf/q66D/6eqg/+jr + pP/p7bL/6/LM/+v48//q+PT/6fHa/+rrvf/q5Jz/69+C/+vcfP/q23n/6tl2/+nWYf/u0CX/7cwc/+zJ + FP/sxhD/7MMN/+vAC//rvgn/670K/+m7Cf/qugj/67wJ/+m8B//puwn/6bsJ/+m7Cf/quwr/670K/+y/ + Cv/swQz/7MMN/+3FEP/tyQ3/7NFD/+jUaP/p1mf/6tds/+jZb//o13L/7+N2/9G5bP9cDCv/YhAy/1IC + JP+8qq7/5uzx/9rg6v/e5/H/3ujy/9zn8//j8fz/5/z//8XK3f+BVXL/YiE8/7ajrv/n/f//3/D4/+Dz + +v/h9fr/4fX6/+D0+f/k/P//cDxU/3EXEP+1XhH/v2YK/7plDP+9aQv/u2gQ/+3dmf//9lT//PJM//30 + Uv/+81b//fRb//32Xf/99WH//fJk//v0Zv///3L/yLB+/1MIK/+6zNz/e1F6/0oAD/+ymZ//+////+/+ + /P/X6/z/v9f//723zP/ayJz/6uyi/+jpof/o6Jz/5uaT/+nutf/t+vj/6fv//+j7///o+fr/5fTl/+ng + kf/q2nT/6Np3/+vUV//uziL/7c0c/+zJFP/sxQ//68IN/+u/Cv/rvgn/6r0J/+q8Cv/quQn/6rwH/+m8 + B//puwn/6rwK/+m7Cf/quwr/6r0K/+q/Cv/rwAv/7MMN/+zFD//vxwz/7M81/+nUZ//p1mX/6ddt/+nY + b//o13b/7+F5/9K5av9cDSz/Yg8y/1IAI/+Yb3n/7Pb1/+Lt8//e5O7/3ufx/97r9f/d6vb/3Or2/+T3 + /v/p/f//5fT9/+X4/f/g8Pr/4vT7/+L1+//i8/v/4vT7/+Hz+f/o////wcPO/1kHJf92HhX/uHMa/8Fv + Cv+5Ygj/u2gZ/+7gnP/+9FT//fJO//70U//+9Ff//fVc//z2X//99mL//fZo//vzZf///33/lWtr/2cm + P//3////jn2d/18HLf+Va37/z8jQ/8zI0v/O2+z/w+D//7/J5P/Kr5//6+ug/+fpn//p6Z7/6eif/+nn + mP/q66b/6vjy/+n6/v/m+/z/5Pv//+jjnf/p2nP/59h2/+zSSv/tzSL/7Msb/+zJEv/rxQ//68IN/+q/ + Cv/rvgn/674J/+m8B//qugn/670I/+m8B//qvAj/6rwJ/+m7Cf/qvAn/6r0J/+q/Cv/rwAv/68EM/+3E + D//uxg3/7c4l/+rUY//o1Wb/6dhs/+nbe//m2X7/7997/9K5av9bDS3/Xw0w/1wNMP9RBB7/eUNR/8K2 + u//o8/n/5fH7/9vn8v/c6vX/3u35/9zs+P/d7fn/4PH7/9/v+P/h8/v/3e74/9/u9//j9/z/4vX6/+L1 + +v/g8/n/6P///76+zP9pJUr/XA0b/55eI//CeBT/wWcW/+/fnf/88lb//PFT//30Vv/99lr//fZd//32 + Yv/99mX//fZq//z3Zv///4r/cTtL/4dWZ//+////gGuO/3AqPP/RvaP/x7Ca/76enP+4mKT/vKq8/7uj + sv/Uwp//7O2h/+fpn//n55z/6eaY/+vmm//q5pP/6u60/+r48v/n9Ob/6Oqz/+zfgf/p33v/5+St/+3S + SP/tzSL/7Msb/+zJEv/rxg7/7MIM/+q/Cv/rvgn/674J/+m8B//qugj/6rwJ/+m8B//pvAf/6bwI/+m7 + Cf/pvAf/6r4J/+q/Cv/rwAv/7MEM/+7DDv/uxg//78sY/+nUWP/n1Wf/6tZq/+rZbv/m2Hf/7eB4/9G3 + a/9ZDCz/Xw0w/1gJK/+SXWn/dCs//1MAF/+ASlj/x77F/+b3/f/i9P7/2+n2/9zs9//f7/v/3/D6/+Hx + +v/h9f3/1OPx/8jG3//W3+7/4fP5/+X4/P/j9/v/3vL5/+f+///f8vj/kXSO/1AEKP9wLCD/s3U4//ry + rP//+VD/+e1R//vzWP/891v//Pde//32ZP/992f//fZt//77a//295L/Whw5/6WDjv//////fGqM/3c3 + Pf/y+bD/7O+n/+jsqP/e2KT/1MGf/9nIoP/r6KL/6eqh/+jpn//q6qj/6eip/+rlmv/r5ZT/6+WN/+rl + l//s4o//6d9+/+nefv/p3Xr/6dx3/+7SPP/s0CT/7Mwa/+zIEv/rxg7/7MIM/+q/Cv/rvgn/6r0I/+m8 + B//ouQn/6rwI/+m7Cf/pvAf/6bwI/+m7Cf/qvQj/6r0I/+u+Cf/qvwr/7MEM/+3DDf/txhH/8MkR/+rS + Rv/o1Gj/6dVo/+jXbf/m1m//7eF2/9G3av9YDCv/Xg8x/1UFKP+qkJb/zLa5/6uCh/9xKjv/VgEZ/4JP + Xv/Iwcv/6Pr//+P2/f/c7Pb/3u73/+Dx+v/h9f3/1uXy/8XC3v/ExOL/yc7o/9bi8v/h8vr/5Pj8/+Dz + +f/k+Pv/6////8PG1/9xQGX/Uwcn/5BgWf/o3n3///9m//z1U//58Vv//PZh//72Zf/9+Gr//PZv//// + b//o45L/UQwv/76prv/+////fGqM/3Y3Of/t9LD/5euv/+jtrv/s76f/7PGh/+vuof/p6qP/6eqh/+rp + n//p6qD/6Omk/+nmnP/r5JD/6+OR/+rjiv/q4Yf/6OCF/+rdff/p23r/7NdX/+3RMP/szyT/7MwZ/+zJ + Ev/rxQ//68IM/+q/Cv/rvgn/6r0I/+q9CP/ougj/6rwJ/+m7Cf/puwj/6bwJ/+m7Cf/pvAj/6r0I/+u+ + Cf/qvwr/68AL/+3CDf/sxRD/7scP/+/QMP/o1mT/6dZl/+nXbP/n1nD/7uJ2/9C3av9YDCr/YBAx/1QF + KP+pjJD/uqGr/3c3Tf+ykJn/qX6H/28gM/9VAxv/hlNk/8vHzv/o/f//4/j+/93u9v/g9Pr/1+Py/8bE + 3//Iy+b/xsro/8PI6v/I0e7/1+T1/+T0+v/m+Pz/4fX6/+j+/v/m+///sqrA/2MmTP9VCCv/oHpi//Hs + f////2n//PZc//rzZf/89m//+/Zy////c//XyYv/TQYr/9HGx//7////e2uM/3U3Ov/w9rD/6eys/+ru + uf/p8MT/6e/F/+jss//o6KD/6eaY/+vnmv/q55z/6ueZ/+nmlv/r5JL/7OON/+rji//p4If/6d6B/+nc + e//p2XL/7dVG/+3QLf/szSL/7MoY/+zHE//rxQ//68EM/+q/Cv/rvgn/6r0I/+m8B//nuQf/6bsJ/+m7 + Cf/puwn/6bsJ/+m7Cf/pvAj/6r0I/+u+Cf/qvwr/68AL/+zCDP/sxQ//7cYQ/+7MG//q1Fr/6NZm/+rW + av/o1m//7uBz/9bAav9cECv/YhAx/1IBJf+jhIz/r5Sc/2EKH/9sMUH/yL3D/7+os/+idYH/axsw/1UD + Gv+JV2j/zszV/+r////l+///1+Xy/8nE3//Gx+X/yMzp/8jP7f/Fzu//w8zw/8nU8//Z5vf/5fb7/+X3 + +v/k9/v/6v///+H3/P+klq3/WhdB/1gSMv+wkWz/+faD////bf/89Wv/9+9w////e/+2mXf/Uw0x/+bn + 5f/3////fWyO/3Y5PP/x9rD/6uqm/+rrp//p7an/6fC6/+vz0P/r9eD/6fDN/+nprf/p5Jn/6+OQ/+rk + j//s5JH/7eSN/+riif/p34X/691//+ncff/n2nj/7NJA/+zPK//szB//7MoV/+3HEf/sxA7/6sEL/+q+ + Cv/rvgn/6r0I/+m8B//ougj/6bsJ/+m7Cf/puwn/6bsJ/+m7Cf/qvAr/6rwJ/+u+Cf/qvwr/68AK/+zC + C//sxA7/7scS/+7KEP/r0UD/59Vo/+nWZ//q123/6dhw/+rcdP9xLzb/Wgos/1wKLP90OlL/0MjL/4JO + Y/9fIjX/way0/2snR/+BTWT/xbe//5xufP9kFyz/VwUe/4lebv/P0dj/6f///+H0+//P1+v/xsro/8fM + 7P/H0PH/x9Hy/8bQ9P/Cz/X/x9b2/9jo+f/l9vv/5vf6/+X5/P/u////3u/2/5iAmv9RCjf/ZSM4/8Wr + d//9/Yj///99//r5jf9rJj3/gE1k//v////x////fmuO/3Y5O//x9rH/6uqo/+rrp//p6qj/6eqj/+np + oP/p8Ln/6vr5/+r6/f/p9ef/6e7G/+vlov/q4Yn/69+D/+zfhP/s3oP/6918/+rceP/q23T/6NRT/+zO + KP/szRz/7ckT/+3GD//qww3/6sAL/+q+Cf/qvgj/6r0J/+m7Cf/ouwf/6bwJ/+m7Cv/puwn/6bsJ/+q8 + Cv/qvAn/6r0J/+u+Cf/rvwr/68AK/+vCCv/sxAz/7cYQ/+7IEP/tzyP/6NVi/+nVaP/q12z/59Vw//Dl + d/++nVz/VAIj/18PMf9YCSv/i19z/9TKzP/QvsH/yrK9/3Y4U/9TBxv/qI+b/9LI1f/Drbn/nWx6/2QW + Lf9WByD/imNz/9DX3P/u////4vT7/9DY7v/Gzu3/xs/y/8jT9P/G1fb/w9P2/8DR9//G2vf/2Or5/+b3 + +//n+Pr/6fv8/+/////V5O3/hmeE/0wDLP90M0H/nHBa/2slN/9YDy//2NPV//X////w////gGyP/3c5 + O//w9q//6uqo/+rrpv/p66f/6eqm/+rqpv/q6Z//7PXb/+z7///q+/3/6Pz//+j6+v/m8tz/6eix/+nf + i//q23r/69t4/+zab//u1kP/7NIw/+3OJf/tzBj/7cgR/+3FD//rwg3/6sAL/+m+Cf/rvgn/6r0I/+m7 + CP/ouwb/6LsH/+m7Cf/puwn/6bsJ/+m7Cf/qvAj/6r0I/+q9CP/rvgn/68AK/+vCCv/rxAr/7MUP/+7H + Ev/vzBL/69RJ/+jWaf/p1mv/6dhx/+fXc//x5nj/tpJZ/2EWLv9UAyv/UgEl/2ooQ/+mipX/2tDU/8Cn + tf9tNkz/tZqo/8q4xf/GuMb/zcLR/8avv/+bZnf/YREq/1MKIf+Oa3n/1t7i//D////i9Pz/z9vy/8fR + 8v/E0vX/xtb2/8bX9//C1fn/v9T5/8ba+f/a6/r/6ff7/+r4+f/s/P3/9P///8zT3f+MZX//dDVY/4pa + cv/Z0tb/9v///+329v/x////f2yP/3c4O//w9q7/6eqk/+rspv/q7KX/6eqk/+rqpf/q6p7/7fXV/+z8 + ///q+/r/6Pv6/+f7+//l+///6fDM/+rqsP/n5Jr/6dx+/+vXW//u0jf/69Es/+zNH//tyhf/7McQ/+vE + Df/swQz/678K/+m+Cf/rvgn/6r0I/+m8B//nugj/6bsI/+m8B//puwn/6bsI/+m8B//qvQj/6r0J/+q9 + C//rvgn/6r8K/+vBC//twwv/7MQO/+3HEf/wyRL/7s4n/+jVZP/q1mn/6tdu/+nYc//o13P/8+h6/97K + c/+aalD/Yhgz/08AJv9RASL/bS9L/7CYof/d2t7/1MbO/8Ouu//Fs8D/x7fH/8m+0P/Nxdn/xq/B/5pm + e/9fDyn/Vw0m/5Fxfv/Y4eP/8f///+P1/P/Q3fT/w9P1/8PT+P/E1/j/xdj6/8LX+/+/1fr/yd36/97s + +v/q9/r/6/n6//L+///4////9/////r////0/f7/7vr7//D5+P/x////f22O/3Y3Of/v97H/6eus/+vq + n//p66P/6uqh/+rpov/o6Jz/7O+2/+77/v/q+/z/6Pv8/+b7/f/m/P//7OWi/+3he//p4of/6d5+/+vY + W//r0j//7c4p/+3MHP/tyRX/7cYQ/+vDDf/qwAv/678K/+u+Cf/qvQj/6r0J/+m7Cf/ouQf/6rwI/+m8 + B//puwn/6bsJ/+m8CP/qvAn/6rwJ/+q8Cv/rvgn/674J/+zAC//swgv/7MMN/+3GEP/uyBP/8MoU/+zS + Rv/o1mv/6thq/+nYcv/o2nX/6dd0/+/gd//37X//2sd2/5ZmUf9fFTP/UgAl/1EBI/9xNU7/rpih/9jR + 1f/TxtD/xbPF/8a4yP/HvM7/ycLX/8/I3//CrsX/lWB5/10NKf9WDyX/lneB/93m5//y////4vX8/87d + 9v/D1fb/wdb6/8XY+//F2vv/wtj7/8DX+v/K3vr/3O76/+z4+v/u+Pn/7Pj7/+z3+v/u+fn/8Pz9/+/4 + +v/x////fm6O/3Y4Ov/x9qj/6+6+/+vrsv/q6Zr/6uqg/+rqnf/o6Z//6ema/+vzzv/q/P//6fv6/+f5 + 8//o8dX/7OGM/+vfgP/p3Hf/6dph/+zVSP/q0zr/7c8o/+3MGv/tyRP/7cUP/+vDDf/qwAv/674J/+u+ + Cf/qvQn/6r0J/+q7CP/qugj/6rwI/+m8B//puwn/6bsJ/+m7Cf/pvAr/6rwK/+q8Cv/rvgn/7L8K/+3A + Cv/swQv/7MMM/+zED//uxxD/78gU/+7QIv/q1l//6dZq/+nZb//r2nb/6dyB/+fchv/p2Hn/8eF5//bt + h//Vw3n/kF9R/1wRMf9RACT/UwQm/3c+VP+0oqj/29bb/9PJ1v/Gt8r/x7zO/8nA1f/Ix93/zMvl/8Cu + yv+SXHr/Wgsn/1oTKP+cfof/4urp//X////i9Pz/y972/8LW+f/D2Pr/xdv6/8Tb+v/C2vv/wdn6/8zg + +v/f7/v/7/r8//H9/f/v+/v/8Pz9/+/5+//y////f2+P/3U4O//y9qj/6Oyh/+vxyv/q7LX/6+eX/+zo + nv/r55z/6uab/+vol//q7LH/6uut/+vmlP/r44j/6+CA/+veff/q3Gz/7ddO/+3UOf/t0in/7c0f/+zL + Fv/txxD/7cQP/+rCDP/qwAr/678K/+q9CP/qvAr/67wJ/+u8Bv/pugj/6rwK/+m7Cf/puwn/6bsJ/+m7 + Cf/qvAr/6rwK/+q8Cf/qvQj/674J/+y/Cv/swQr/68IM/+vEDv/txg//7cgU/+/MFP/u0jr/59Zq/+nX + av/r2nD/6dx5/+nfhv/o56r/6OKo/+fZgP/v5ID/9O+L/8+9e/+KVlH/XA0x/1EAI/9VBSj/e0Nc/7mo + sv/a2eH/0crc/8W80v/Gv9X/x8Lb/8nK5P/Lz+z/vq3M/41Zef9ZByX/XRYq/6CDjP/j7e3/9v///+L1 + /P/M3/b/wdf5/8LZ+//F2/v/xdz7/8La/P/B2vz/zOL7/+Hx+v/w/Pv/8Pz8/+/5+f/z////gG6O/3U5 + Of/x9qj/6eqg/+jtoP/q8sv/6+u0/+vmk//s55n/6+aX/+rmlv/r5Yz/6eOL/+njif/p4ob/6t9+/+va + cf/q2Fz/7NVC/+7TMv/vzyT/7c0a/+3KEv/txw//7MUL/+zCCv/qvwr/6r4K/+u9C//qvAr/6bsJ/+m7 + Cf/ougj/6bwJ/+m8CP/puwn/6bsK/+m7Cv/pvAj/6bwJ/+q8Cv/qvQj/6r0I/+u/Cf/rwAr/68EL/+zD + DP/txQ7/7cgS/+7KFv/wzRr/6tVQ/+jVbf/q2G7/6tp2/+vbbf/p56H/5/b//+fw4//m5rn/6N6R//Ho + h//z743/zbh7/4ZRT/9ZCy7/UAAk/1cIKv9/TWH/va+4/9vd5//PzOD/xL7Y/8fD2//HxuD/yc7q/8vQ + 8P+6rND/iVR2/1cGJP9hGS3/p4yS/+jx8P/1////4fT7/8zg9//C2vn/wtr8/8bc/P/G3fz/wNr8/8jh + +v/w+/v/8fz9/+/5+v/y////f26O/3Y6Ov/y96j/6eqg/+nrn//q7J//6fHH/+nqrf/r5pL/7OaU/+vl + kf/s5JD/6+KO/+rhif/p333/6eWe/+rhkv/v0kP/7dM9/+7QKf/vzh3/7MwU/+vJD//sxQ7/68ML/+vB + Cv/rvwr/674K/+u9Cv/qvQn/6rwJ/+i6Cf/ougj/6rwJ/+m8B//puwn/6bsK/+m6Cv/pvAf/6bwI/+q8 + Cv/qvQj/670J/+u+Cf/qvgn/68AK/+3DC//sxA3/7cYP/+3IEv/vyxX/8NIj/+nWX//o1m3/7Ndx/+ra + df/q337/6fHY/+f4///n9///5vPy/+Xry//o4qD/8uyM//PtkP/HsHr/gElL/1cIL/9QACT/Vwss/4NT + aP/AtsH/2d/s/87N5v/Fwt7/yMbh/8bJ5v/I0fD/y9P0/7asz/+DUHP/VwUi/2UdMf+rk5j/7PTy//n/ + ///j9Pv/zeH4/8Pb+v/D2/z/xNv8/8vj+//w+/v/8fz9/+74+v/x////fm6O/3U5Of/y9af/6umf/+rq + n//q6Z3/6eqb/+jtt//q55z/7eWO/+zkj//q5Iv/6uKJ/+nhhf/s3Xf/6eio/+P3/P/s2WX/7tAt/+3P + Jf/uzRj/7MoS/+3GD//swwz/68EL/+u/C//svwr/674J/+q9CP/qvQj/6bwI/+i6CP/qugj/6rsJ/+m7 + Cf/puwn/6bsJ/+m7Cf/puwn/6bsJ/+m7Cf/qvQj/6r0I/+u+Cf/rvgn/68AK/+vCCv/rwwz/7MQO/+7H + Ef/vyRX/8MwY/+3SMP/o1mb/69Zt/+nZc//q23T/695//+vqt//o9fX/5fb8/+X3///k9vv/5e3a/+jm + rP/z75X/8e+X/8Kqef98QUf/VgYs/04AJf9ZDjD/h1tw/8W+yf/Z4fP/zM7q/8XF4//HyOf/xszr/8nU + 9v/I1vr/s6rQ/4JOcv9VAyL/ZyI2/7CZoP/t9/X/+P///+P0+//O4vf/wtr6/8zh+f/w+/r/7fz7/+35 + +P/x////f22O/3Y4OP/x86T/6uid/+ronf/r6Jr/6+eY/+rnl//o55r/6+SO/+vjiv/q4Yn/6uGH/+ve + gv/t23H/6uWb/+L5///o5qr/780g/+7OH//syxT/7ckP/+7FDf/swwv/68EL/+vAC//qvwr/6r4J/+q9 + CP/qvAr/6rwK/+m7Cf/pugf/6rwJ/+m7Cf/puwn/6bsJ/+m7Cf/puwr/6bsK/+m7Cf/qvAn/6r0J/+u9 + Cv/rvgr/674L/+vBCf/swwr/7MQL/+3GDv/vyBH/78oY//HNF//t1Dv/59Zr/+nXbf/q2nL/7Nt2/+ze + df/q8d3/5vb//+b29v/k9/n/4/j9/+T6///l7dL/6uCH//Tzl//v7Zf/u6F0/3g5Rv9WBCz/UAAk/1wS + MP+NZXX/x8XO/9ni9//Lz+//xMfo/8bM7P/Gz/D/yNj6/8nY/f+xrND/f0pv/1YCIv9rJjv/tKGo//L6 + +f/5////4vP6/9bn9v/u+vv/7/z8/+75+f/w////fmyO/3U3Nv/w86L/6uaX/+znmP/s6Jb/7OeV/+3m + lP/r5Y//6uOM/+vhiP/r34b/6d+B/+reff/u11r/7OGC/+L6///n45r/8MwZ/+3NGP/tyRP/7McO/+zD + Df/rwQv/68AK/+u/Cv/qvgn/6r4I/+q9CP/quwr/6rsK/+i7CP/ouwb/6rwJ/+m7Cf/puwn/6rwK/+q7 + Cv/pugr/6bsK/+m7Cf/qvAr/6rwK/+q8Cv/qvAv/7L4L/+3ACf/swQr/7MMK/+zFC//uxhD/7skT/+7L + GP/wzhr/7NRF/+fXbf/p2G7/6tl1/+zbdP/s557/5/j7/+b4/v/k+f//5/To/+vsuf/o8tb/6eu5/+jh + l//o45H/8/Oa/+3pl/+3mXL/dTRD/1QELP9RACT/YBYz/5JufP/KzNf/1+X6/8nS8//Dye7/xc7v/8bR + 8//G2/3/xtv//6+r0P98SW7/VQEi/24sQ/+6qq//8/z6//n////u+vv/7Pj5/+34+v/w////fW2P/3Y2 + NP/x9KX/6Oak/+vlk//s5pT/7OWR/+zlkP/r5I3/6uKK/+vfh//s3oH/6t99/+zaZf/u1kv/79lQ/+jj + i//s00D/7s0Z/+7LFf/uxxH/7MUN/+vCDf/rwAz/7L8K/+y+Cf/rvgn/6r0I/+q9CP/qvAn/6rwK/+i6 + CP/ougj/6rwJ/+m8B//qvAn/6bsJ/+m7Cf/puwn/6bsJ/+m7Cf/puwn/6rwK/+q8Cv/qvQj/674I/+y/ + Cv/rwAv/68IM/+vEDP/txQ7/7ccR/+7JFP/vyxr/8M4c/+vWS//o1nH/6ddu/+nadv/t3HT/7eei/+nz + 4P/o8+f/6+er/+zhgv/q5Iz/6umj/+nmo//q45D/6OKS/+nllP/z85z/6eaX/7KSb/9wLUH/VAEs/1EA + I/9iGjf/lnaD/8zS3f/W5/7/yNP2/8PM8P/F0PT/w9P3/8bd/v/F3P//rqrR/3lFav9WASP/cjJI/76v + tf/0/Pv/+f///+z3+P/u////fW2P/3c4N//y85n/6emp/+rkmP/s5I//6+SP/+rjjP/r4Yr/6uCH/+ne + gv/r3YD/7dpk/+3YR//s1j//7dM2/+7QIP/uzhz/7cwW/+7IEv/txg//7MIM/+zBDP/rwAv/678K/+u+ + Cf/rvQv/670L/+q8Cv/qvAr/6bsJ/+m7Cf/pugf/6rwI/+q8Cf/puwn/6bsJ/+m7Cf/puwn/6bsJ/+m7 + Cf/qvAr/6rwK/+q8Cf/qvQn/674J/+u+Cf/svwr/68AL/+vCDP/sxA3/7MUP/+3HEf/tyhP/7cwZ/+/Q + Hf/s1kn/59Zv/+nYb//r2nX/6ttz/+reev/s34D/6+B//+rhiP/r4on/6+OI/+njjP/p5JD/6eSQ/+nl + lP/o4Zf/6uaU//P2nP/m4pT/q4hr/2wnPv9SASv/UgAk/2UhOf+bfIn/ztbg/9bo///G1Pr/wc/1/8LT + 9v/D1Pn/xd7//8Xc//+qqM7/dkBm/1QAIv91N0v/xbe4//X8+f/4////fG2P/3Y5N//08Zj/6+aV/+nm + mv/s5I3/6uOK/+nhiP/r34f/6t6C/+ndf//q22X/8NZF/+3VO//u0zD/7tAp/+7OIv/uyxn/7skS/+zH + EP/sxA7/68IM/+vAC//rvwr/674J/+u+Cf/qvAr/6rwK/+q8Cv/qvAr/6bsK/+m7Cf/quwX/6rwI/+m7 + Cf/puwn/6bsJ/+m7Cf/puwn/6bsJ/+m7Cf/puwn/6rwK/+q8Cv/qvAr/670K/+u+Cf/rvgn/678K/+vB + DP/rwwz/7MQN/+zFD//tyBD/78oV/+7MGP/yzxz/7NVG/+fXbP/q12//6tl1/+vbd//r3Hv/6t1//+rf + gf/q4YT/6uKI/+rji//r44z/6+OK/+zlnf/s5Jb/6uWS/+fik//r55b/9vWd/+PdkP+lgWX/aSI8/1IA + Kv9RACX/ZyI//52Ej//P3Of/0+n//8TU+v+/0Pb/w9P4/8PV+v/G3///xNz//6qkzP90PmP/VAMh/3o8 + TP/IztL/fW+S/3c4Nf/z8Zb/6+SQ/+rkjv/s5Iv/6+KJ/+rfhv/r34L/6d1+/+3aYf/v10L/7tQ7/+3T + L//u0SX/7s4f/+3MGf/uyhL/7scP/+zFDv/rww3/6sEL/+vAC//svwr/674J/+u+Cf/qvAr/6rwK/+q8 + Cv/qvAr/6rwK/+m7Cf/qugj/67wK/+m7Cf/puwn/6bsK/+m6Cv/puwn/6bsJ/+m7Cf/puwn/6rwK/+q8 + Cv/qvAr/6r0J/+q9CP/rvgn/678K/+rAC//swQz/7MMN/+vEDf/sxg7/7cgR/+7KFP/tzBj/788a/+zU + P//o1mn/6tZv/+nZcv/r23f/69t6/+3dff/r33//6uCC/+vhhv/q4of/6+SL/+zknP/q44v/6+SP/+zl + kv/q45H/6eGR/+3olv/09Zv/4deO/6J5Y/9mHzv/UQAr/1EAJv9qKEL/ooyX/9Hf6//S6f//wdT7/7/R + +P/D1Pj/w9f7/8Xg///E3f3/q6TM/3E3X/9VCyj/Vw81/4FEQv/w7JH/6+OM/+vjiv/q4Yn/6t+F/+rf + gv/r3Xr/7dlZ/+7XP//v1Df/7tMs/+3QI//tzhz/7cwW/+vKE//tyA//7MUN/+vDDP/rwAv/7MAL/+y/ + Cv/rvgn/670K/+u9C//qvAv/6rwK/+q8Cv/qvAr/6bsJ/+m7Cf/pugj/6rwK/+m7Cf/puwn/6bsJ/+m7 + Cv/puwn/6bsJ/+m7Cf/puwn/6rwK/+m8Cf/qvAr/6r0J/+q9CP/rvQr/674K/+q/Cf/rwAv/7MEM/+zD + DP/sxQz/7MYO/+3HEf/tyRP/7csY//HNGf/t0jP/6tZh/+nYcP/p2XD/6tt2/+zdef/s3X3/7N9+/+zf + gf/r4YP/6+KI/+rhif/q4Yr/6uOL/+vki//q5JL/6uSV/+vjkv/p4ZH/7eqU//T2mv/f04v/m3Bf/2Ma + Ov9TACv/UwEn/2wtRv+mkp3/0ePw/9Do///C1Pv/vtL4/8HW+f/B1vv/x+L+/8zq//+sp8z/ZSNQ/31B + Pv/w7I//6uGK/+rhiP/p34X/6d6F/+vccv/t2U//7tY7/+7TNv/u0Sz/7tAi/+7OG//uyxX/7skR/+3I + Dv/sxg//68MM/+vBC//qwAv/678K/+y+Cf/rvgn/6r0J/+q9Cf/rvQr/6rwK/+q8Cv/qvAr/6bsK/+m6 + Cf/ougj/6rwK/+m7Cf/puwn/6bsJ/+m7Cf/puwn/6bsJ/+m7Cf/puwn/6bsJ/+m7Cf/puwn/6bwJ/+q9 + CP/qvAr/670K/+u+Cf/qvwr/68AK/+zCC//qwwn/7MQM/+7FDv/vxxD/7skT/+3LFv/vzRf/8M8o/+vV + Uv/q12//6tly/+rcc//s3Hf/7N16/+vdf//s3oD/6uCC/+nhg//r4Yb/6+KJ/+ziiP/q5Zb/6eOY/+vj + if/s5JD/6+OR/+jgj//u6ZT/9vWZ/9nNhv+Ya1r/YRc4/1AAKv9TAij/bjJL/6mYo//T5PH/zun//8DU + +//A1v3/u8rp/5Nief/L6P//h3Gf/3g4Mv/y7Yz/6d+E/+jghf/q34D/7dpm/+/XRP/v1Tn/7dMy/+3R + Kv/v0CH/784b/+/MFP/uyBL/7McQ/+zGDv/rww3/68EM/+vAC//qvwr/6r4J/+u+Cf/qvQj/6r0I/+q9 + CP/qvQf/6r0J/+q8Cv/puwn/6bsK/+i5Cf/ougj/6bwI/+m8B//puwn/6bsJ/+m7Cf/puwn/6bsJ/+m7 + Cf/puwn/6bsJ/+m7Cf/puwn/6bsJ/+q8Cf/qvQj/6r0I/+q9CP/rvgn/678K/+vAC//swQv/7cIN/+3E + C//txgz/7cgO/+7JEf/tyxb/78wZ//DOHv/u0z3/69dl/+rYcv/r2nP/69t1/+vbeP/r3Xr/6t59/+vf + gP/s4IL/6+GF/+rhiP/r4ob/6uOH/+rjlP/r4oz/7OOM/+zjjf/q4pD/6eGN/+/qkP/185X/1saB/5Ji + VP9gEzf/UQAq/1MDKP9wNk//rJ6p/9Lo9f/M5///vdP7/3hFZf+1u9b/h3Sj/3g4L//z6on/6t6C/+rd + cv/w2FL/8Nc8/+3UN//u0i//79En/+/PIP/uzRr/7MsW/+7JEv/uxxD/7MUO/+vDDf/swQz/6sAL/+q/ + Cv/svwr/674J/+q9CP/qvQr/6r0J/+q9CP/qvAn/6rwK/+q7Cv/qvAr/6rwK/+m7Cf/ougf/6bwI/+m7 + CP/puwf/6bsI/+m7Cf/puwn/6bsJ/+m7Cf/qvAr/6bsJ/+q8Cv/puwn/6bsJ/+m7Cf/pvAf/6r0I/+q9 + CP/qvQj/674J/+u/Cv/qvwr/7MEM/+3DC//rxAv/7cYM/+7IDf/tyRD/7ssU/+7MGf/wzRr/79En/+vV + TP/q2Gn/69lz/+nac//q23T/7Nx4/+3de//s3n3/696A/+rfgf/r4IL/6uKE/+njlv/r4Yr/6uKJ/+ri + if/q4ov/6uOL/+rhi//o34r/8OqM//TykP/TwHv/jVtQ/1wQNf9TACv/VgQq/3U8VP+vpK//0un3/83o + /f/O7///hWqW/3k3Mv/z53v/7Ndb//DXQv/v1jj/7tQy/+3SLP/uzyT/7s4e/+/MGf/tyxb/7MkS/+3H + D//txQ7/7MMN/+vCDP/swAv/7L8L/+q+Cf/rvgn/674K/+u9Cv/rvQv/6r0K/+q9Cf/qvAr/6bwJ/+m8 + CP/puwn/6bsJ/+m7Cf/ouwb/6rwJ/+m6Cf/pvAf/6bwI/+m7Cf/puwn/6bsJ/+m7Cf/puwn/6bsJ/+m7 + Cf/puwn/6bsJ/+m7Cf/pvAj/6r0I/+q9CP/qvQj/674J/+u+Cf/qvwr/68EL/+zCC//rwgz/7MQM/+3F + DP/uxw//7sgQ/+7JFP/tyxn/7s0Z//DPHP/u0y7/7NZO/+nYaf/q2XL/69p1/+rbdf/r3Hb/69x6/+vd + ff/q3oD/69+A/+regP/r4IL/6+GD/+rghf/r4IX/6uGG/+nhh//r4Ij/6d6I/+jdhv/v6Yn/8u6K/864 + dP+IUUv/Wwwz/1IAKf9YBCr/eT1V/7Cirv+3utL/Xx1T/5VgMv/45Ej/7dE4/+/TNf/v0y3/7tEm/+/P + IP/vzRr/7swV/+3KE//tyg//7cgO/+zGDv/rwwz/68EL/+zAC//twAv/7L8K/+u+Cf/rvgn/674L/+q8 + C//qvAr/6rwK/+q8Cv/quwr/6bwI/+q9B//puwn/6bsK/+m7Cf/qugj/67wJ/+m8B//pugn/6boK/+m6 + Cv/puwn/6bsJ/+m7Cf/puwn/6bsJ/+m7Cf/puwn/6bsJ/+m7Cf/pvAf/6r0I/+q9CP/qvQj/6r0I/+u+ + Cf/svwr/7L8K/+zACv/rwAv/7cMN/+3DDP/sxQv/7ccO/+3IEP/tyRP/7ssV/+3MGv/vzhv/8M8f/+7S + Lv/s1Eb/69de/+rZbv/p2nb/6tt2/+vbd//r23r/6916/+vee//q3nz/695+/+vef//q3n//69+A/+re + gf/p34L/6d6B/+rfgf/p3YH/6Nx///Hohf/06Yb/ya5s/4NJSP9aDDX/TwAp/00BJf9QASf/fDso/+PH + Nv/w1jP/7NIu/+3RKP/vzyL/7s8d/+/MGP/tyxT/7soS/+7IEP/txg7/7MQN/+vEDf/swQz/7MAL/+3A + C//svwr/674J/+u+Cf/rvQv/670L/+u9Cv/qvAr/6rwK/+q8Cv/puwn/6bwI/+m8B//puwn/6bsJ/+i6 + CP/qugj/67wJ/+m8CP/pugr/6boK/+i6Cf/pugr/6boJ/+m7Cf/puwn/6bsJ/+q8Cv/puwn/6bsJ/+m7 + Cf/pvAf/6bwH/+m8B//qvQj/6r0I/+u+Cf/rvgn/674J/+y/Cv/qvwr/68AK/+zCC//sxAv/7cUM/+3G + Df/tyA7/7sgQ/+/JFP/tyxf/7swb/+7NHP/vzx7/8NEm/+3TN//r1E3/69df/+rZbP/q2nT/6dp3/+rb + ef/q23r/6tx6/+rcfP/p3Xz/6d19/+rdff/o3n7/6tx+/+rdfv/q3n//6t2A/+nafv/q23b/9OZv//Xk + Xv/Lpz7/qHQ1/616Mv/KoDD/7tMw//DVLP/t0Cj/7tAi/+7PHv/uzRr/7MwX/+3LFP/tyRL/7McQ/+3F + Dv/sxA3/68MN/+zCDP/rwAv/7L8K/+y/Cv/rvgr/674K/+u9Cv/rvQr/6rwK/+u9C//qvAr/6rwK/+m7 + Cf/puwn/6bwI/+m8B//puwn/6bsJ/+m6Cf/puQf/6rsJ/+i6Cf/pugn/6bsK/+m7Cf/pugr/6bsK/+m7 + Cf/puwn/6bsJ/+m7Cf/puwn/6bsJ/+m7Cf/pvAf/6r0I/+q9CP/qvQj/6r0I/+q9CP/qvQj/674J/+u+ + Cf/pvgn/6r8J/+vBCv/swgz/7cQL/+3ECv/txgz/7scO/+3ID//syRD/7soT/+/LFv/uzBr/780d/+7O + Hf/vzyD/79Am/+7RMP/u0zv/7dVI/+rWU//p11z/6thi/+rZZv/q2Wr/69pr/+vaa//q2mj/69pm/+vZ + Yf/s2Vv/7ddS/+7XRv/w1D//7dE2/+3SMP/12zP/+OAx//neLf/22yj/7s8l/+3PIP/uzh7/8M0Z/+/N + F//vzBP/7ckR/+3HEf/txg//7MUN/+zDDf/rww3/68EM/+vAC//qvwr/678K/+u+Cf/rvQv/670L/+u9 + C//rvQv/670L/+q8Cv/qvAr/6bsJ/+m7Cf/puwn/6bwI/+m8B//pvAn/6boK/+m5Cv/puQf/6rsJ/+m7 + Cf/puwn/6bsK/+m6Cv/pugr/6bsK/+m7Cf/puwn/6bsJ/+m7Cf/puwn/6bsJ/+m7Cf/puwn/6bsJ/+m7 + Cf/qvAr/670K/+q8Cv/qvQj/6r0I/+u+Cf/rvgn/674J/+y/Cv/twAv/7MEL/+zDC//twwz/7cUM/+3G + DP/uxw3/7sgN/+/JEf/uyRP/7soW/+7MGP/uzBv/780d/+/NH//wzh//8M8g/+/QI//u0Cb/79Ep//DS + LP/w0y3/79Mt/+7TLf/w0jD/8NIu/+/SK//w0Sz/8NEr/+/SK//u0Sv/7tEr/+3QKv/tzSj/7Msm/+3M + If/tzR3/7s4b/+7NGf/tzBb/7coT/+7IEv/vyRD/7ccQ/+zFD//rxAz/7MMN/+3CDf/swQz/68AL/+q/ + Cv/svwr/670K/+u9C//rvQv/670K/+q9CP/qvAr/6rwK/+q8Cv/qvAr/6rwJ/+m7Cf/puwn/6bsJ/+m7 + Cf/puwn/6bsK/+m6Cv/qugj/6rsJ/+m7Cf/puwn/6boJ/+m6Cf/pugn/6bsJ/+m7Cf/puwn/6bsJ/+m7 + Cf/puwn/6bsJ/+m7Cf/puwn/6bsJ/+m7Cf/qvAr/6rwK/+q8Cv/qvQj/6r0I/+q9CP/rvgn/674J/+y+ + Cv/svwr/7MAK/+zBC//swgv/7MML/+zEC//txAz/7cYM/+3HDv/tyBD/7ckQ/+3JEv/uyhP/7soW/+7K + Gf/uzBv/7c0c/+3NHP/uzh3/784g/+/OIP/vzyH/788i/+7PI//uziP/7s8k/+/PJP/vziX/7s4j/+7P + I//uzyH/7s4d/+/PHP/uzR3/7s0b/+3NGP/vzBb/78sV/+7KE//vyhH/7sgQ/+zHEP/sxQ7/68QO/+vD + Df/rwgz/68EM/+zBDP/rwAv/68AL/+u/Cv/rvgn/674K/+u9Cv/qvQn/6r0J/+q9Cf/qvAr/6rwK/+m7 + Cf/qvAn/6rsK/+m7Cf/puwn/6bsJ/+m7Cf/ouwj/6boJ/+i5Cf/puQf/6rsJ/+m7Cf/puwn/6bsJ/+m7 + Cf/puwn/6bsJ/+i6CP/puwn/6bsJ/+i6CP/puwn/6bsJ/+m7Cf/puwn/6bsJ/+m7Cf/puwn/6bsJ/+m8 + Cf/qvQn/6r0I/+q9CP/qvQj/6r0I/+q9CP/rvgn/7L8K/+y/Cv/rwQr/7MEK/+zCC//twgz/7MML/+3G + DP/txg7/7ccP/+7HD//tyBD/7MkR/+zKEf/uyxL/7swT/+3ME//uyxX/7ssY/+/MGP/uzBn/7swZ/+7M + Gv/vzRr/780a/+7NGv/uzRr/7s0Z/+7NGP/szRf/7cwW/+7MFf/tyxT/7ssT/+7KEv/uyRP/7cgR/+zH + D//txw//7MYO/+zFDf/sww3/68MN/+vCDP/swQz/68AL/+vAC//qvwr/678K/+y+Cf/rvgn/674J/+q9 + CP/qvQj/6r0J/+q8Cv/qvAr/6rwK/+m7Cf/puwj/6bsK/+m8Cf/puwn/6bsJ/+m7Cf/puwn/6boJ/+i5 + Cf/qugj/6rwI/+m8B//puwn/6bsJ/+m7Cf/pugr/6boJ/+m7Cf/puwn/6bsJ/+q8Cv/puwn/6bwJ/+m7 + Cf/pugr/6rsK/+m7CP/puwn/6rwK/+m7Cf/pvAn/6rwK/+q8Cv/qvAr/6r0J/+q9CP/rvgj/674J/+y/ + Cv/twAv/68AL/+vAC//swQz/7MEM/+3CDf/tww3/7cUM/+zGDP/rxw3/7MgN/+zIDf/vyQ7/7skQ/+3J + Ef/tyRH/7coS/+7KE//uyhL/7ssS/+7LEv/uyxL/7ssT/+3KEv/uyxL/7soS/+3KEf/tyhH/7ssS/+7K + Ef/vyRH/78kQ/+7ID//tyBD/7cYP/+zFDv/sxA3/7MMN/+3CDf/swg3/7MEM/+zBDP/qwAr/6r8K/+q/ + Cv/rvwn/674J/+u+Cf/rvQv/6rwK/+q8Cv/qvAr/6rwK/+q8Cv/pvAf/6bwI/+m7Cf/puwn/6bsJ/+m7 + Cf/puwn/6bsJ/+m7Cf/puwn/6bsJ/+i6CP/pugj/6bsI/+m7CP/puwn/6bsJ/+i6Cf/pugr/6boJ/+m7 + Cf/ougn/6boJ/+q7Cv/puwr/6bsK/+m7Cv/puwr/6bsJ/+m7Cf/pvAn/6bsK/+m7Cv/puwn/6bsK/+m7 + Cf/qvAr/6rwK/+q9Cf/rvQr/674J/+u+Cf/svgr/678K/+u/Cv/rwAv/68AL/+vAC//swgv/7MML/+zE + C//txAz/7cQM/+3FDf/txg3/7MYO/+3HD//txxD/7ccP/+7ID//uyQ3/7skN/+7IDf/vyQ//7sgP/+7I + D//tyBD/7ccQ/+3HD//txxD/7ccQ/+3HD//txw//7MUO/+zFDv/rxQz/7MMM/+zCDf/rwgv/68EL/+zB + DP/swQv/68AL/+vAC//rvwv/678L/+u/Cv/rvgr/674K/+u+Cv/rvQv/6rwK/+q8Cv/qvAr/6rwK/+q8 + Cv/pvAj/6bwJ/+m7Cv/puwn/6bsJ/+m7Cf/qvAr/6bwJ/+m7Cf/puwn/6boJ/+i5CP/puwn/6rwK/+m7 + Cf/ougj/6LoI/+m7Cf/pugr/6boJ/+i6Cf/pugr/6boK/+m6Cv/puQr/6boK/+m6Cv/puwn/6bsJ/+m7 + Cf/puwn/6rsK/+m6Cv/puwn/6bsJ/+q7Cv/qvAr/6rwK/+q8Cv/qvAv/670K/+q9CP/qvQn/674J/+u+ + Cf/qvwr/6r8K/+vAC//rwQr/68EK/+zCC//twgz/7MMM/+vDDP/rwwz/7MMN/+zEDf/txA7/7cUN/+3G + DP/txg3/7cYN/+3GDf/txg//7MYN/+zGDP/sxg7/7MUO/+3EDv/txA7/7MQN/+3EDf/sxA3/68MN/+vD + Df/rwg3/68EM/+zADP/rwA3/68AM/+vAC//rvwv/678L/+q/Cv/rvQz/674K/+u+Cf/rvQv/67wL/+u9 + C//qvAr/6rwK/+q8Cv/puwr/6rwK/+q8Cv/puwr/6bsK/+q8Cv/puwn/6bsJ/+m7Cf/puwn/6bsJ/+m7 + Cf/puwn/6boJ/+i5Cf/puwj/6rwJ/+m8Cf/ougj/6boJ/+m6Cf/ouQn/6LoJ/+i6CP/ougn/6boK/+i6 + Cf/ouQn/6boJ/+m6Cv/pugr/6boK/+m6Cv/puwn/6bsJ/+m7Cf/puwn/6bsJ/+m7Cv/qvAr/6rwK/+m8 + Cf/qvAr/6rwK/+q8Cv/qvQj/674J/+u+CP/rvgn/6r4J/+m+Cf/qvwr/6r8K/+vACv/rwAr/68AL/+vB + C//swQv/7MEL/+zBDP/twwv/7MMN/+vDDf/rww7/68MM/+vDDP/rww3/68MN/+vDDf/rxA3/68MN/+vD + DP/swgz/7MEM/+zBDP/qwgz/6sEL/+vAC//rwAz/68AL/+vAC//qwA3/678M/+y+DP/svgz/674L/+u9 + C//pvgv/6r4L/+u9Cv/qvAr/6rwK/+q8Cv/qvAr/6bsJ/+q7Cv/quwr/6rsL/+q7C//quwr/6boK/+m6 + Cv/puwn/6bsJ/+m6Cf/pugr/6boJ/+m6Cf/pugn/6boJ/+i5Cf/ouQr/6LoK/+i5Cf/ouQn/6boJ/+i5 + Cf/ouAr/6LkK/+i5Cf/ouQr/6LgK/+i4Cv/ouAr/6LkK/+i5Cf/ouQn/6LkJ/+i5Cv/ouQn/6LoJ/+i6 + Cf/ougn/6LoJ/+m6Cv/pugr/6boL/+m7C//quwr/6bsK/+m7Cf/qvAn/6rwJ/+q8Cf/qvAr/6bwK/+m9 + Cv/qvQr/6b0J/+m+Cf/qvgv/6r4L/+q+C//qvgz/6r8M/+q/DP/rwAz/68AM/+rBDP/rwQz/6sEL/+rB + C//qwQz/6sEL/+vBDP/qwQz/6sAL/+rAC//rwAz/68AM/+q/C//pwAz/6r8L/+q/Cv/rvwr/6r4K/+q+ + C//qvgv/6r0L/+u8C//rvAv/67wL/+u8C//pvAv/6rwL/+q7C//puwr/6boK/+m6Cv/pugr/6boK/+m6 + C//puQv/6bkK/+m6C//puQv/6LkK/+m5Cv/ouQr/6LkK/+i5Cv/puQr/6LkK/+i5Cv/ouQr/6LkL/+i4 + Cv/ouQv/6boL/+e5CP/nugf/6LkJ/+i5Cv/ouAr/6LkJ/+i5Cf/ouQn/6LkI/+i5Cf/ouQj/6LkJ/+i5 + Cv/ouQn/6LkJ/+i5Cf/ougf/6LoI/+i6CP/ougj/6LsI/+i7CP/pugr/6bsL/+m6Cv/puwn/6bsK/+m7 + Cv/puwr/6bsK/+m7Cv/puwn/6rwK/+q8Cv/quwv/6bwK/+i9Cv/ovQr/6L0K/+m+Cv/pvgv/6b4L/+q+ + C//pvgr/6r4L/+q/C//qvwz/6r8M/+q/DP/qwAr/6sAJ/+q/Cv/qvwz/6r8L/+q/C//qvwv/6b8J/+rA + Cf/qvwr/6b4K/+m+C//qvQr/6rwJ/+q8Cf/rvAv/6rwL/+q8Cv/qvAr/6rwK/+q8Cv/qvAn/6bsJ/+m7 + Cf/puwn/6boJ/+i6Cf/pugr/6LoJ/+i6Cf/ougn/6LoJ/+i6Cf/pugn/6LoK/+i5Cf/ouQn/6LoJ/+i6 + Cf/nuQj/6LoJ/+i5Cf/nuQj/6LkK/+i5Cv8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + + + \ No newline at end of file diff --git a/ExplorerTabUtility/Forms/TrayIcon.cs b/ExplorerTabUtility/Forms/TrayIcon.cs deleted file mode 100644 index 03fe09f..0000000 --- a/ExplorerTabUtility/Forms/TrayIcon.cs +++ /dev/null @@ -1,275 +0,0 @@ -using System; -using System.Linq; -using System.Threading; -using System.Diagnostics; -using System.Windows.Forms; -using System.Threading.Tasks; -using System.Collections.Generic; -using FlaUI.Core.AutomationElements; -using Microsoft.Win32; -using ExplorerTabUtility.Hooks; -using ExplorerTabUtility.Models; -using ExplorerTabUtility.WinAPI; -using ExplorerTabUtility.Helpers; -using Window = ExplorerTabUtility.Models.Window; - -namespace ExplorerTabUtility.Forms; - -public class TrayIcon : ApplicationContext -{ - private static IntPtr _mainWindowHandle = IntPtr.Zero; - private static readonly NotifyIcon NotifyIcon; - private static readonly Keyboard KeyboardHook; - private static readonly Shell32 WindowHook; - private static readonly SemaphoreSlim Limiter; - private static WindowHookVia _windowHookVia; - - static TrayIcon() - { - Limiter = new SemaphoreSlim(1); - WindowHook = new Shell32(OnNewWindow); - KeyboardHook = new Keyboard(OnNewWindow); - - var isKeyboardHookActive = Properties.Settings.Default.KeyboardHook; - var isWindowHookActive = Properties.Settings.Default.WindowHook; - _windowHookVia = Properties.Settings.Default.WindowViaUi - ? WindowHookVia.Ui - : WindowHookVia.Keys; - - NotifyIcon = new NotifyIcon - { - Icon = Helper.GetIcon(), - Text = "Explorer Tab Utility: Force new windows to tabs.", - - ContextMenuStrip = CreateContextMenuStrip(isKeyboardHookActive, isWindowHookActive), - Visible = true - }; - - if (isKeyboardHookActive) KeyboardHook.StartHook(); - if (isWindowHookActive) WindowHook.StartHook(); - - Application.ApplicationExit += OnApplicationExit; - } - private static ContextMenuStrip CreateContextMenuStrip(bool isKeyboardHookActive, bool isWindowHookActive) - { - var strip = new ContextMenuStrip(); - - strip.Items.Add(CreateToolStripMenuItem("Keyboard (Win + E)", isKeyboardHookActive, ToggleKeyboardHook)); - strip.Items.Add(CreateWindowHookMenuItem(isWindowHookActive)); - - strip.Items.Add(new ToolStripSeparator()); - strip.Items.Add(CreateToolStripMenuItem("Add to startup", IsInStartup(), ToggleStartup)); - - strip.Items.Add(new ToolStripSeparator()); - strip.Items.Add(CreateToolStripMenuItem("Exit", false, static (_, _) => Application.Exit())); - - return strip; - } - private static ToolStripMenuItem CreateWindowHookMenuItem(bool isWindowHookActive) - { - var windowHookMenuItem = CreateToolStripMenuItem("All Windows", isWindowHookActive, ToggleWindowHook); - - windowHookMenuItem.DropDownItems.Add( - CreateToolStripMenuItem("UI (Recommended)", _windowHookVia == WindowHookVia.Ui, WindowHookViaChanged, "WindowViaUi")); - - windowHookMenuItem.DropDownItems.Add( - CreateToolStripMenuItem("Keys", _windowHookVia == WindowHookVia.Keys, WindowHookViaChanged, "WindowViaKeys")); - - return windowHookMenuItem; - } - private static ToolStripMenuItem CreateToolStripMenuItem(string text, bool isChecked, EventHandler eventHandler, string? name = default) - { - var item = new ToolStripMenuItem - { - Text = text, - Checked = isChecked - }; - - if (name != default) - item.Name = name; - - item.Click += eventHandler; - return item; - } - - private static bool IsInStartup() - { - var executablePath = Process.GetCurrentProcess().MainModule?.FileName; - if (string.IsNullOrWhiteSpace(executablePath)) return false; - - using var key = Registry.CurrentUser.OpenSubKey(Constants.RunRegistryKeyPath, false); - if (key == default) return false; - - var value = key.GetValue(Constants.AppName) as string; - return string.Equals(value, executablePath, StringComparison.OrdinalIgnoreCase); - } - private static void ToggleStartup(object? sender, EventArgs _) - { - if (sender is not ToolStripMenuItem item) return; - - var executablePath = Process.GetCurrentProcess().MainModule?.FileName; - if (string.IsNullOrWhiteSpace(executablePath)) return; - - using var key = Registry.CurrentUser.OpenSubKey(Constants.RunRegistryKeyPath, true); - if (key == default) return; - - // If already set in startup - if (string.Equals(key.GetValue(Constants.AppName) as string, executablePath, StringComparison.OrdinalIgnoreCase)) - { - // Remove from startup - key.DeleteValue(Constants.AppName, false); - item.Checked = false; - } - else - { - // Add to startup - key.SetValue(Constants.AppName, executablePath); - item.Checked = true; - } - } - private static void ToggleKeyboardHook(object? sender, EventArgs _) - { - if (sender is not ToolStripMenuItem item) return; - - item.Checked = !item.Checked; - - Properties.Settings.Default.KeyboardHook = item.Checked; - Properties.Settings.Default.Save(); - - if (item.Checked) - KeyboardHook.StartHook(); - else - KeyboardHook.StopHook(); - } - private static void ToggleWindowHook(object? sender, EventArgs _) - { - if (sender is not ToolStripMenuItem item) return; - - item.Checked = !item.Checked; - - Properties.Settings.Default.WindowHook = item.Checked; - Properties.Settings.Default.Save(); - - if (item.Checked) - WindowHook.StartHook(); - else - WindowHook.StopHook(); - - foreach (ToolStripItem subItem in item.DropDownItems) - subItem.Enabled = item.Checked; - } - private static void WindowHookViaChanged(object? sender, EventArgs _) - { - if (sender is not ToolStripMenuItem item) return; - - var container = item.GetCurrentParent(); - foreach (ToolStripMenuItem radio in container.Items) - { - radio.Checked = !radio.Checked; - - if (radio.Name == "WindowViaUi") - Properties.Settings.Default.WindowViaUi = radio.Checked; - else if (radio.Name == "WindowViaKeys") - Properties.Settings.Default.WindowViaKeys = radio.Checked; - } - - Properties.Settings.Default.Save(); - - _windowHookVia = Properties.Settings.Default.WindowViaUi - ? WindowHookVia.Ui - : WindowHookVia.Keys; - } - - private static async Task OnNewWindow(Window window) - { - var windowHandle = IntPtr.Zero; - try - { - await Limiter.WaitAsync().ConfigureAwait(false); - - windowHandle = GetMainWindowHWnd(window.OldWindowHandle); - if (windowHandle == default) return; - - var windowElement = UiAutomation.FromHandle(windowHandle); - if (windowElement == default) return; - - // Store currently opened Tabs, before we open a new one. - var oldTabs = WinApi.GetAllExplorerTabs(); - - // Add new tab. - AddNewTab(windowElement); - - // If it is just a new (This PC | Home), return. - if (string.IsNullOrWhiteSpace(window.Path)) return; - - // Get newly created tab's handle (That is not in 'oldTabs') - var newTabHandle = WinApi.ListenForNewExplorerTab(oldTabs); - if (newTabHandle == 0) return; - - // Get the tab element out of that handle. - var newTabElement = UiAutomation.FromHandle(newTabHandle); - if (newTabElement == default) return; - - // Navigate to the target location - Navigate(windowElement, newTabHandle, window.Path); - - if (window.SelectedItems is not { Count: > 0 } selectedItems) return; - - // Select items - SelectItems(newTabElement, selectedItems); - } - finally - { - if (windowHandle != default) - WinApi.RestoreWindowToForeground(windowHandle); - - Limiter.Release(); - } - } - - private static IntPtr GetMainWindowHWnd(IntPtr otherThan) - { - if (WinApi.IsWindowHasClassName(_mainWindowHandle, "CabinetWClass")) - return _mainWindowHandle; - - var allWindows = WinApi.FindAllWindowsEx(); - - // Get another handle other than the newly created one. (In case if it still alive.) - _mainWindowHandle = allWindows.FirstOrDefault(t => t != otherThan); - - return _mainWindowHandle; - } - private static void AddNewTab(AutomationElement window) - { - // if via UI is selected try to add a new tab with UI Automation. - if (_windowHookVia == WindowHookVia.Ui && UiAutomation.AddNewTab(window)) - return; - - // Via Keys is selected or UI Automation fails. - Keyboard.AddNewTab(window.Properties.NativeWindowHandle.Value); - } - private static void Navigate(AutomationElement window, nint tabHandle, string location) - { - // if via UI is selected try to Navigate with UI Automation. - if (_windowHookVia == WindowHookVia.Ui && UiAutomation.Navigate(window, tabHandle, location)) - return; - - // Via Keys is selected or UI Automation fails. - Keyboard.Navigate(window.Properties.NativeWindowHandle.Value, tabHandle, location); - } - private static void SelectItems(AutomationElement tab, ICollection names) - { - // if via UI is selected try to Select with UI Automation. - if (_windowHookVia == WindowHookVia.Ui && UiAutomation.SelectItems(tab, names)) - return; - - // Via Keys is selected or UI Automation fails. - Keyboard.SelectItems(tab.Properties.NativeWindowHandle.Value, names); - } - private static void OnApplicationExit(object? _, EventArgs __) - { - NotifyIcon.Visible = false; - KeyboardHook.Dispose(); - WindowHook.Dispose(); - } -} \ No newline at end of file diff --git a/ExplorerTabUtility/Helpers/Constants.cs b/ExplorerTabUtility/Helpers/Constants.cs index 4fca603..aa53b9d 100644 --- a/ExplorerTabUtility/Helpers/Constants.cs +++ b/ExplorerTabUtility/Helpers/Constants.cs @@ -5,4 +5,7 @@ internal class Constants internal const string AppName = "ExplorerTabUtility"; internal const string MutexId = $"__{AppName}Hook__Mutex"; internal const string RunRegistryKeyPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run"; + internal const string NotifyIconText = @"Explorer Tab Utility: Force new windows to tabs."; + internal const string HotKeyProfilesFileName = "HotKeyProfiles.json"; + internal const string JsonFileFilter = @"JSON files (*.json)|*.json|All Files|*.*"; } \ No newline at end of file diff --git a/ExplorerTabUtility/Helpers/Helper.cs b/ExplorerTabUtility/Helpers/Helper.cs index 911aebf..8b8fd32 100644 --- a/ExplorerTabUtility/Helpers/Helper.cs +++ b/ExplorerTabUtility/Helpers/Helper.cs @@ -3,11 +3,37 @@ using System.Diagnostics; using System.Drawing; using System.Threading; +using System.Threading.Tasks; namespace ExplorerTabUtility.Helpers; public static class Helper { + public static Task DoDelayedBackgroundAsync(Action action, int delayMs = 2_000, CancellationToken cancellationToken = default) + { + return Task.Run(async () => + { + await Task.Delay(delayMs, cancellationToken).ConfigureAwait(false); + action(); + }, cancellationToken); + } + public static Task DoDelayedBackgroundAsync(Func action, int delayMs = 2_000, CancellationToken cancellationToken = default) + { + return Task.Run(async () => + { + await Task.Delay(delayMs, cancellationToken).ConfigureAwait(false); + await action().ConfigureAwait(false); + }, cancellationToken); + } + public static Task DoDelayedBackgroundAsync(Func> action, int delayMs = 2_000, CancellationToken cancellationToken = default) + { + return Task.Run(async () => + { + await Task.Delay(delayMs, cancellationToken).ConfigureAwait(false); + return await action().ConfigureAwait(false); + }, cancellationToken); + } + public static T DoUntilNotDefault(Func action, int timeMs = 500, int sleepMs = 20, CancellationToken cancellationToken = default) { return DoUntilCondition( @@ -64,6 +90,86 @@ public static void DoIfCondition(Action action, Func predicate, bool justO Thread.Sleep(sleepMs); } } + public static Task DoUntilNotDefaultAsync(Func> action, int timeMs = 500, int sleepMs = 20, CancellationToken cancellationToken = default) + { + return DoUntilConditionAsync( + action, + result => !EqualityComparer.Default.Equals(result, default), + timeMs, + sleepMs, + cancellationToken); + } + public static Task DoUntilNotDefaultAsync(Func action, int timeMs = 500, int sleepMs = 20, CancellationToken cancellationToken = default) + { + return DoUntilConditionAsync( + action, + result => !EqualityComparer.Default.Equals(result, default), + timeMs, + sleepMs, + cancellationToken); + } + public static Task DoUntilTimeEndAsync(Func action, int timeMs = 500, int sleepMs = 20, CancellationToken cancellationToken = default) + { + return DoUntilConditionAsync(action, static () => false, timeMs, sleepMs, cancellationToken); + } + public static async Task DoUntilConditionAsync(Func action, Func predicate, int timeMs = 500, int sleepMs = 20, CancellationToken cancellationToken = default) + { + var startTicks = Stopwatch.GetTimestamp(); + + while (!cancellationToken.IsCancellationRequested && !IsTimeUp(startTicks, timeMs)) + { + await action().ConfigureAwait(false); + if (predicate()) + return; + + await Task.Delay(sleepMs).ConfigureAwait(false); + } + } + public static async Task DoUntilConditionAsync(Func action, Predicate predicate, int timeMs = 500, int sleepMs = 20, CancellationToken cancellationToken = default) + { + var startTicks = Stopwatch.GetTimestamp(); + + while (!cancellationToken.IsCancellationRequested && !IsTimeUp(startTicks, timeMs)) + { + var result = action(); + if (predicate(result)) + return result; + + await Task.Delay(sleepMs).ConfigureAwait(false); + } + + return action(); + } + public static async Task DoUntilConditionAsync(Func> action, Predicate predicate, int timeMs = 500, int sleepMs = 20, CancellationToken cancellationToken = default) + { + var startTicks = Stopwatch.GetTimestamp(); + + while (!cancellationToken.IsCancellationRequested && !IsTimeUp(startTicks, timeMs)) + { + var result = await action().ConfigureAwait(false); + if (predicate(result)) + return result; + + await Task.Delay(sleepMs).ConfigureAwait(false); + } + + return await action().ConfigureAwait(false); + } + public static async Task DoIfConditionAsync(Func action, Func predicate, bool justOnce = false, int timeMs = 500, int sleepMs = 20, CancellationToken cancellationToken = default) + { + var startTicks = Stopwatch.GetTimestamp(); + + while (!cancellationToken.IsCancellationRequested && !IsTimeUp(startTicks, timeMs)) + { + if (predicate()) + { + await action().ConfigureAwait(false); + + if (justOnce) return; + } + await Task.Delay(sleepMs).ConfigureAwait(false); + } + } public static bool IsTimeUp(long startTicks, int timeMs) { @@ -81,15 +187,18 @@ public static TimeSpan GetElapsedTime(long startTicks) var tickFrequency = (double)10_000_000 / Stopwatch.Frequency; return new TimeSpan((long)((Stopwatch.GetTimestamp() - startTicks) * tickFrequency)); } + public static T Clamp(this T val, T min, T max) where T : IComparable + { + if (val.CompareTo(min) < 0) return min; + if (val.CompareTo(max) > 0) return max; + return val; + } + + public static Icon? GetIcon() => Icon.ExtractAssociatedIcon(GetExecutablePath()); - public static Icon? GetIcon() + public static string GetExecutablePath() { var processName = Process.GetCurrentProcess().MainModule?.FileName; - - var location = string.IsNullOrWhiteSpace(processName) - ? $"{AppDomain.CurrentDomain.FriendlyName}.exe" - : processName; - - return Icon.ExtractAssociatedIcon(location); + return processName is { Length: > 0 } ? processName : $"{AppDomain.CurrentDomain.FriendlyName}.exe"; } } \ No newline at end of file diff --git a/ExplorerTabUtility/Hooks/IHook.cs b/ExplorerTabUtility/Hooks/IHook.cs new file mode 100644 index 0000000..7fed91e --- /dev/null +++ b/ExplorerTabUtility/Hooks/IHook.cs @@ -0,0 +1,10 @@ +using System; + +namespace ExplorerTabUtility.Hooks; + +public interface IHook : IDisposable +{ + public bool IsHookActive { get; } + public void StartHook(); + public void StopHook(); +} \ No newline at end of file diff --git a/ExplorerTabUtility/Hooks/Keyboard.cs b/ExplorerTabUtility/Hooks/Keyboard.cs index e1f1719..67090ab 100644 --- a/ExplorerTabUtility/Hooks/Keyboard.cs +++ b/ExplorerTabUtility/Hooks/Keyboard.cs @@ -1,124 +1,131 @@ -using WindowsInput; -using System; +using System; using System.Linq; using System.Threading.Tasks; using System.Collections.Generic; -using System.Runtime.InteropServices; +using ExplorerTabUtility.Managers; +using WindowsInput; +using H.Hooks; using ExplorerTabUtility.Models; using ExplorerTabUtility.WinAPI; -using System.Threading; +using ExplorerTabUtility.Helpers; namespace ExplorerTabUtility.Hooks; -public class Keyboard : IDisposable +public class Keyboard : IHook { - private nint _hookId = 0; - private nint _user32LibraryHandle = 0; - private bool _isWinKeyDown; - private HookProc? _keyboardHookCallback; // We have to keep a reference because of GC - private readonly Func _onNewWindow; + private readonly LowLevelKeyboardHook _lowLevelKeyboardHook; + private readonly IReadOnlyCollection _hotkeyProfiles; + private readonly Action _onHotKeyProfileTriggered; private static readonly IKeyboardSimulator KeyboardSimulator = new InputSimulator().Keyboard; + public bool IsHookActive => _lowLevelKeyboardHook.IsStarted; - public Keyboard(Func onNewWindow) + public Keyboard(IReadOnlyCollection hotkeyProfiles, Action onHotKeyProfileTriggered) { - _onNewWindow = onNewWindow; + _hotkeyProfiles = hotkeyProfiles; + _onHotKeyProfileTriggered = onHotKeyProfileTriggered; + _lowLevelKeyboardHook = new LowLevelKeyboardHook { Handling = true }; + _lowLevelKeyboardHook.Down += LowLevelKeyboardHook_Down; } - public void StartHook() - { - _keyboardHookCallback = KeyboardHookCallback; - _user32LibraryHandle = WinApi.LoadLibrary("User32"); - _hookId = WinApi.SetWindowsHookEx(WinHookType.WH_KEYBOARD_LL, _keyboardHookCallback, _user32LibraryHandle, 0); - } + public void StartHook() => _lowLevelKeyboardHook.Start(); + public void StopHook() => _lowLevelKeyboardHook.Stop(); - public void StopHook() + private void LowLevelKeyboardHook_Down(object? sender, KeyboardEventArgs e) { - Dispose(); - } + var isFileExplorerForeground = WinApi.IsFileExplorerForeground(out _); + + foreach (var profile in _hotkeyProfiles) + { + // Skip if the profile is disabled or if it doesn't have any hotkeys. + if (!profile.IsEnabled || profile.HotKeys?.Any() != true) continue; + + // Skip if the profile is for File Explorer but File Explorer is not the foreground window. + if (profile.Scope == HotkeyScope.FileExplorer && !isFileExplorerForeground) continue; + + // Skip if the hotkeys don't match. + if (!e.Keys.Are(profile.HotKeys)) continue; - private nint KeyboardHookCallback(int nCode, nint wParam, nint lParam) + // Set handled value. + e.IsHandled = profile.IsHandled; + + // Invoke the profile action in the background in order for `IsHandled` to successfully prevent further processing. + Task.Run(() => _onHotKeyProfileTriggered(profile)); + } + } + public static async Task GetCurrentTabLocationAsync(nint windowHandle, bool restoreToForeground = true) { - if (nCode < 0) - return WinApi.CallNextHookEx(_hookId, nCode, wParam, lParam); + // Restore the window to foreground. + if (restoreToForeground) + { + WinApi.RestoreWindowToForeground(windowHandle); + await Task.Delay(350).ConfigureAwait(false); + } - // Read key - var vkCode = Marshal.ReadInt32(lParam); + // Send CTRL + L to activate the address bar + KeyboardSimulator.ModifiedKeyStroke(VirtualKeyCode.CONTROL, VirtualKeyCode.VK_L); + await Task.Delay(150).ConfigureAwait(false); - // Windows key - if (vkCode == WinApi.VK_WIN) - _isWinKeyDown = wParam == WinApi.WM_KEYDOWN; //DOWN or UP + // Store the current clipboard data + var backup = ClipboardManager.GetClipboardData(); - if (!_isWinKeyDown || vkCode != WinApi.VK_E || wParam != WinApi.WM_KEYDOWN) - return WinApi.CallNextHookEx(_hookId, nCode, wParam, lParam); + // Clear the clipboard. + ClipboardManager.ClearClipboard(); - // No Explorer windows, Continue with normal flow. - if (!WinApi.FindAllWindowsEx().Take(1).Any()) - return WinApi.CallNextHookEx(_hookId, nCode, wParam, lParam); + // Send CTRL + C to copy the address location. + KeyboardSimulator.ModifiedKeyStroke(VirtualKeyCode.CONTROL, VirtualKeyCode.VK_C); - // It is better not to wait for the invocation, otherwise the normal flow might open a new window - Task.Run(() => _onNewWindow.Invoke(new Window(string.Empty))); + // Get the text from the clipboard. + var addressLocation = await Helper.DoUntilConditionAsync( + action: ClipboardManager.GetClipboardText, + predicate: l => !string.IsNullOrWhiteSpace(l)) + .ConfigureAwait(false); + + // Give the focus to the window to close the address bar. + WinApi.PostMessage(windowHandle, WinApi.WM_SETFOCUS, 0, 0); + + // Restore the previous clipboard data. + ClipboardManager.SetClipboardData(backup); - // Return dummy value to prevent normal flow. - return 1; + return addressLocation; } - public static void AddNewTab(nint windowHandle) + public static async Task AddNewTabAsync(nint windowHandle) { // Restore the window to foreground. WinApi.RestoreWindowToForeground(windowHandle); - // Give the focus to the folder view. - WinApi.PostMessage(windowHandle, WinApi.WM_SETFOCUS, 0, 0); + await Task.Delay(200).ConfigureAwait(false); // Send CTRL + T - KeyboardSimulator - .Sleep(300) - .ModifiedKeyStroke(VirtualKeyCode.CONTROL, VirtualKeyCode.VK_T); + KeyboardSimulator.ModifiedKeyStroke(VirtualKeyCode.CONTROL, VirtualKeyCode.VK_T); } - public static void Navigate(nint windowHandle, nint tabHandle, string location) + public static async Task NavigateAsync(nint windowHandle, string location) { // Restore the window to foreground. WinApi.RestoreWindowToForeground(windowHandle); - // Give the keyboard focus to the tab. - WinApi.PostMessage(tabHandle, WinApi.WM_SETFOCUS, 0, 0); + await Task.Delay(300).ConfigureAwait(false); // Send CTRL + L to activate the address bar - KeyboardSimulator - .Sleep(300) - .ModifiedKeyStroke(VirtualKeyCode.CONTROL, VirtualKeyCode.VK_L); + KeyboardSimulator.ModifiedKeyStroke(VirtualKeyCode.CONTROL, VirtualKeyCode.VK_L); - // Type the location. - KeyboardSimulator - .Sleep(300) - .TextEntry(location) - .Sleep(250 + location.Length * 5) // Longer locations require longer wait time. - .KeyPress(VirtualKeyCode.RETURN); // Press Enter - - // Do in the background - Task.Run(async () => - { - // for ~1250 Milliseconds (25 * 50) - for (var i = 0; i < 25; i++) - { - await Task.Delay(50); + await Task.Delay(300).ConfigureAwait(false); - var popupHandle = WinApi.GetWindow(windowHandle, WinApi.GW_ENABLEDPOPUP); + // Type the location. + KeyboardSimulator.TextEntry(location); - // If the suggestion popup is not visible, continue. - if (popupHandle == 0) continue; + // Longer locations require longer wait time. + await Task.Delay(270 + location.Length * 5).ConfigureAwait(false); - // Hide the suggestion popup. - WinApi.ShowWindow(popupHandle, WinApi.SW_HIDE); - } - }); + // Press Enter + KeyboardSimulator.KeyPress(VirtualKeyCode.RETURN); } - public static void SelectItems(nint tabHandle, ICollection names) + public static async Task SelectItemsAsync(nint tabHandle, ICollection names) { // Restore the window to foreground. WinApi.RestoreWindowToForeground(tabHandle); - Thread.Sleep(500); + await Task.Delay(500).ConfigureAwait(false); // Type the first name. KeyboardSimulator.TextEntry(names.First()); @@ -126,21 +133,9 @@ public static void SelectItems(nint tabHandle, ICollection names) public void Dispose() { - if (_hookId != IntPtr.Zero) - { - WinApi.UnhookWindowsHookEx(_hookId); - _hookId = IntPtr.Zero; - } - - _keyboardHookCallback = null; - if (_user32LibraryHandle == IntPtr.Zero) return; - - // reduces reference to library by 1. - WinApi.FreeLibrary(_user32LibraryHandle); - _user32LibraryHandle = IntPtr.Zero; - } - ~Keyboard() - { - Dispose(); + StopHook(); + _lowLevelKeyboardHook.Dispose(); } + + ~Keyboard() => Dispose(); } \ No newline at end of file diff --git a/ExplorerTabUtility/Hooks/Shell32.cs b/ExplorerTabUtility/Hooks/Shell32.cs index 6231979..62d931f 100644 --- a/ExplorerTabUtility/Hooks/Shell32.cs +++ b/ExplorerTabUtility/Hooks/Shell32.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; @@ -9,34 +10,31 @@ namespace ExplorerTabUtility.Hooks; -public class Shell32 : IDisposable +public class Shell32(Action onNewWindow) : IHook { - private IntPtr _hookId = IntPtr.Zero; + private nint _hookId; private WinEventDelegate? _eventHookCallback; // We have to keep a reference because of GC - private readonly Func _onNewWindow; private static object? _shell; private static Type? _shellType; private static Type? _windowType; - - public Shell32(Func onNewWindow) - { - _onNewWindow = onNewWindow; - } + public bool IsHookActive { get; private set; } public void StartHook() { _eventHookCallback = OnWindowOpenHandler; - _hookId = WinApi.SetWinEventHook(WinApi.EVENT_OBJECT_CREATE, WinApi.EVENT_OBJECT_CREATE, IntPtr.Zero, _eventHookCallback, 0, 0, 0); + _hookId = WinApi.SetWinEventHook(WinApi.EVENT_OBJECT_CREATE, WinApi.EVENT_OBJECT_CREATE, default, _eventHookCallback, 0, 0, 0); + IsHookActive = true; } public void StopHook() { - Dispose(); + WinApi.UnhookWinEvent(_hookId); + IsHookActive = false; } - private void OnWindowOpenHandler(IntPtr hWinEventHook, uint eventType, IntPtr hWnd, int idObject, int idChild, uint dwEventThread, uint dWmsEventTime) + private void OnWindowOpenHandler(nint hWinEventHook, uint eventType, nint hWnd, int idObject, int idChild, uint dwEventThread, uint dWmsEventTime) { if (!WinApi.IsWindowHasClassName(hWnd, "CabinetWClass")) return; - if (WinApi.FindAllWindowsEx().Take(2).Count() < 2) return; + if (WinApi.FindAllWindowsEx("CabinetWClass").Take(2).Count() < 2) return; var originalRect = WinApi.HideWindow(hWnd); var showAgain = true; @@ -69,7 +67,7 @@ private void OnWindowOpenHandler(IntPtr hWinEventHook, uint eventType, IntPtr hW { // Move the back to the screen (Show) if (showAgain) - WinApi.SetWindowPos(hWnd, IntPtr.Zero, originalRect.Left, originalRect.Top, 0, 0, WinApi.SWP_NOSIZE | WinApi.SWP_NOZORDER); + WinApi.SetWindowPos(hWnd, default, originalRect.Left, originalRect.Top, 0, 0, WinApi.SWP_NOSIZE | WinApi.SWP_NOZORDER); } } @@ -81,7 +79,7 @@ private void OnWindowOpenHandler(IntPtr hWinEventHook, uint eventType, IntPtr hW _shell ??= Activator.CreateInstance(_shellType); if (_shell == default) return default; - var windows = _shellType.InvokeMember("Windows", BindingFlags.InvokeMethod, null, _shell, Array.Empty()); + var windows = _shellType.InvokeMember("Windows", BindingFlags.InvokeMethod, null, _shell, []); _windowType ??= windows?.GetType(); return windows; @@ -110,7 +108,7 @@ public static void NavigateToLocation(object? window, string location) { if (window == default || _windowType == default) return; - _windowType.InvokeMember("Navigate", BindingFlags.InvokeMethod, null, window, new object?[] { location }); + _windowType.InvokeMember("Navigate", BindingFlags.InvokeMethod, null, window, [location]); } private static int GetCount(object? item) @@ -120,20 +118,20 @@ private static int GetCount(object? item) var obj = _windowType.InvokeMember("Count", BindingFlags.GetProperty, null, item, null); return obj is int count ? count : default; } - public static object? GetWindowByHandle(object? windows, IntPtr hWnd) + public static object? GetWindowByHandle(object? windows, nint hWnd) { if (hWnd == default || windows == default || _windowType == default) return default; var count = GetCount(windows); for (var i = 0; i < count; i++) { - var window = _windowType.InvokeMember("Item", BindingFlags.InvokeMethod, null, windows, new object[] { i }); + var window = _windowType.InvokeMember("Item", BindingFlags.InvokeMethod, null, windows, [i]); if (window == default) continue; var itemHWnd = _windowType.InvokeMember("HWND", BindingFlags.GetProperty, null, window, null); if (itemHWnd == default) continue; - if ((long)itemHWnd == hWnd.ToInt64()) + if ((long)itemHWnd == hWnd) return window; } @@ -162,7 +160,7 @@ private static int GetCount(object? item) var selectedList = new List(count); for (var i = 0; i < count; i++) { - var selectedItem = _windowType.InvokeMember("Item", BindingFlags.InvokeMethod, null, selectedItems, new object[] { i }); + var selectedItem = _windowType.InvokeMember("Item", BindingFlags.InvokeMethod, null, selectedItems, [i]); if (selectedItem == default) return default; if (_windowType.InvokeMember("Name", BindingFlags.GetProperty, null, selectedItem, null) is string selectedItemName) @@ -198,7 +196,7 @@ public static void SelectItems(object? window, ICollection? names) var selectedCount = 0; for (var i = 0; i < count; i++) { - var item = _windowType.InvokeMember("Item", BindingFlags.InvokeMethod, null, files, new object[] { i }); + var item = _windowType.InvokeMember("Item", BindingFlags.InvokeMethod, null, files, [i]); if (item == default) return; var name = _windowType.InvokeMember("Name", BindingFlags.GetProperty, null, item, null) as string; @@ -206,7 +204,7 @@ public static void SelectItems(object? window, ICollection? names) if (!names.Any(n => n.Equals(name, StringComparison.OrdinalIgnoreCase))) continue; - _windowType.InvokeMember("SelectItem", BindingFlags.InvokeMethod, null, document, new[] { item, 1 }); + _windowType.InvokeMember("SelectItem", BindingFlags.InvokeMethod, null, document, [item, 1]); if (++selectedCount >= names.Count) return; } @@ -224,15 +222,34 @@ private void CloseAndNotifyNewWindow(object? item, Window window) CloseWindow(item); - Task.Run(() => _onNewWindow.Invoke(window)); + Task.Run(() => onNewWindow.Invoke(window, false)); } - public void Dispose() + public static void OpenNewWindowAndSelectItems(string folderPath, ICollection? names) { - WinApi.UnhookWinEvent(_hookId); + // If there are no names, add an empty string to the list, Otherwise the parent folder will open instead. + if (names == default || names.Count == 0) + names = new[] { string.Empty }; + + var folderPidl = WinApi.ILCreateFromPathW(folderPath); + if (folderPidl == default) return; + + var filesApidl = GetFilesPidls(folderPath, names); + WinApi.SHOpenFolderAndSelectItems(folderPidl, (uint)filesApidl.Length, filesApidl, 0); + + WinApi.ILFree(folderPidl); + if (filesApidl.Length == 0) return; + + foreach (var pidl in filesApidl) WinApi.ILFree(pidl); } - ~Shell32() + private static nint[] GetFilesPidls(string folderPath, IEnumerable fileNames) { - Dispose(); + return fileNames + .Select(file => WinApi.ILCreateFromPathW(Path.Combine(folderPath, file))) + .Where(nativeFile => nativeFile != default) + .ToArray(); } + + public void Dispose() => StopHook(); + ~Shell32() => Dispose(); } \ No newline at end of file diff --git a/ExplorerTabUtility/Hooks/UiAutomation.cs b/ExplorerTabUtility/Hooks/UiAutomation.cs index caec918..5717335 100644 --- a/ExplorerTabUtility/Hooks/UiAutomation.cs +++ b/ExplorerTabUtility/Hooks/UiAutomation.cs @@ -14,32 +14,29 @@ namespace ExplorerTabUtility.Hooks; -public class UiAutomation : IDisposable +public class UiAutomation(Func onNewWindow) : IHook { - private readonly Func _onNewWindow; private readonly HWndCache _cache = new(5_000); private static readonly UIA3Automation Automation = new(); - - public UiAutomation(Func onNewWindow) - { - _onNewWindow = onNewWindow; - } + public bool IsHookActive { get; private set; } public void StartHook() { Automation .GetDesktop() .RegisterAutomationEvent(Automation.EventLibrary.Window.WindowOpenedEvent, TreeScope.Children, OnWindowOpenHandler); + IsHookActive = true; } public void StopHook() { Automation.UnregisterAllEvents(); + IsHookActive = false; } - private void OnWindowOpenHandler(AutomationElement element, EventId _) + private async void OnWindowOpenHandler(AutomationElement element, EventId _) { if (element.ClassName != "CabinetWClass") return; - if (WinApi.FindAllWindowsEx().Take(2).Count() < 2) return; + if (WinApi.FindAllWindowsEx("CabinetWClass").Take(2).Count() < 2) return; var hWnd = element.Properties.NativeWindowHandle.Value; @@ -54,23 +51,21 @@ private void OnWindowOpenHandler(AutomationElement element, EventId _) // A new "This PC" window (unless the opened folder is called "This PC"?) if (string.Equals(element.Name, "This PC")) { - CloseAndNotifyNewWindow(element, new Window(string.Empty, oldWindowHandle: hWnd)); + await CloseAndNotifyNewWindowAsync(element, new Window(string.Empty, oldWindowHandle: hWnd)).ConfigureAwait(false); showAgain = false; return; } - GetHeaderElements(element, out var suggestBox, out var searchBox, out var addressBar); - if (suggestBox == default || searchBox == default || addressBar == default) return; + var header = await GetHeaderElementsAsync(element).ConfigureAwait(false); + if (header?.SuggestBox == null || header.AddressBar == default) return; - // We have to invoke the suggestBox to populate the address bar :( - suggestBox.Patterns.Invoke.Pattern.Invoke(); + // We have to invoke the suggestBox to populate the address bar + header.SuggestBox.Patterns.Invoke.Pattern.Invoke(); - // Invoke searchBox to hide the suggestPopup window. - searchBox.Patterns.Invoke.Pattern.Invoke(); - - var location = Helper.DoUntilCondition( - action: () => addressBar.Patterns.Value.Pattern.Value.Value, - predicate: l => !string.IsNullOrWhiteSpace(l)); + var location = await Helper.DoUntilConditionAsync( + action: () => header.AddressBar.Patterns.Value.Pattern.Value.Value, + predicate: l => !string.IsNullOrWhiteSpace(l)) + .ConfigureAwait(false); if (string.IsNullOrWhiteSpace(location)) return; @@ -78,7 +73,7 @@ private void OnWindowOpenHandler(AutomationElement element, EventId _) // ("Home") For English version. if (string.Equals(location, "Home")) { - CloseAndNotifyNewWindow(element, new Window(string.Empty, oldWindowHandle: hWnd)); + await CloseAndNotifyNewWindowAsync(element, new Window(string.Empty, oldWindowHandle: hWnd)).ConfigureAwait(false); showAgain = false; return; } @@ -88,11 +83,11 @@ private void OnWindowOpenHandler(AutomationElement element, EventId _) var folderView = tab?.FindFirstDescendant(c => c.ByClassName("UIItemsView")); if (folderView == default) { - // ("Home") For non English versions. + // ("Home") For non-English versions. var home = tab?.FindFirstDescendant(c => c.ByClassName("HomeListView")); if (home == default) return; - CloseAndNotifyNewWindow(element, new Window(string.Empty, oldWindowHandle: hWnd)); + await CloseAndNotifyNewWindowAsync(element, new Window(string.Empty, oldWindowHandle: hWnd)).ConfigureAwait(false); showAgain = false; return; } @@ -103,17 +98,42 @@ private void OnWindowOpenHandler(AutomationElement element, EventId _) var tabHWnd = tab!.Properties.NativeWindowHandle.Value; - CloseAndNotifyNewWindow(element, new Window(location, selectedNames, hWnd, tabHWnd)); + await CloseAndNotifyNewWindowAsync(element, new Window(location, selectedNames, hWnd, tabHWnd)).ConfigureAwait(false); showAgain = false; } finally { // Move the back to the screen (Show) if (showAgain) - WinApi.SetWindowPos(hWnd, IntPtr.Zero, originalRect.Left, originalRect.Top, 0, 0, WinApi.SWP_NOSIZE | WinApi.SWP_NOZORDER); + WinApi.SetWindowPos(hWnd, default, originalRect.Left, originalRect.Top, 0, 0, WinApi.SWP_NOSIZE | WinApi.SWP_NOZORDER); } } - public static AutomationElement? FromHandle(IntPtr hWnd) => Automation.FromHandle(hWnd); + + public static AutomationElement? FromHandle(nint hWnd) => Automation.FromHandle(hWnd); + public static async Task GetCurrentTabLocationAsync(AutomationElement window) + { + var header = await GetHeaderElementsAsync(window).ConfigureAwait(false); + if (header?.SuggestBox == default || header.AddressBar == default) return default; + + // Give the address bar focus to update its value with the current location. + header.AddressBar.Focus(); + + var windowHandle = window.Properties.NativeWindowHandle.Value; + var tabHandle = window + .FindFirstChild(c => c.ByClassName("ShellTabWindowClass")) + .Properties.NativeWindowHandle.Value; + + if (tabHandle == default) + tabHandle = windowHandle; + + // Wait in the background for 700ms and give the focus to the tab to close the address bar. + _ = Helper.DoDelayedBackgroundAsync(() => WinApi.PostMessage(tabHandle, WinApi.WM_SETFOCUS, 0, 0), 700); + + return await Helper.DoUntilConditionAsync( + action: () => header.AddressBar.Patterns.Value.Pattern.Value.Value, + predicate: l => !string.IsNullOrWhiteSpace(l)) + .ConfigureAwait(false); + } public static bool AddNewTab(AutomationElement window) { var addButton = GetAddNewTabButton(window); @@ -130,43 +150,31 @@ public static bool AddNewTab(AutomationElement window) return addButton; } - public static bool Navigate(AutomationElement window, nint tabHandle, string location) + public static async Task NavigateAsync(AutomationElement window, nint tabHandle, string location) { - GetHeaderElements(window, out var suggestBox, out var searchBox, out var addressBar); - if (suggestBox == default || searchBox == default || addressBar == default) + var header = await GetHeaderElementsAsync(window).ConfigureAwait(false); + if (header?.SuggestBox == default || header.AddressBar == default) return false; // Set the location. - addressBar.Patterns.Value.Pattern.SetValue(location); + header.AddressBar.Patterns.Value.Pattern.SetValue(location); // We have to invoke the suggestBox to Navigate :( - suggestBox.Patterns.Invoke.Pattern.Invoke(); - - // Wait in the background - Task.Run(async () => - { - await Task.Delay(700).ConfigureAwait(false); - - var popupHandle = WinApi.GetWindow(window.Properties.NativeWindowHandle.Value, WinApi.GW_ENABLEDPOPUP); + header.SuggestBox.Patterns.Invoke.Pattern.Invoke(); - // If for some reason the address bar doesn't have the focus anymore, return. - if (popupHandle == 0) return; - - // Hide Suggestion popup. - WinApi.ShowWindow(popupHandle, WinApi.SW_HIDE); - - // Give the focus to the tab to close the address bar. - WinApi.PostMessage(tabHandle, WinApi.WM_SETFOCUS, 0, 0); - }); + // Wait in the background for 700ms and give the focus to the tab to close the address bar. + _ = Helper.DoDelayedBackgroundAsync(() => WinApi.PostMessage(tabHandle, WinApi.WM_SETFOCUS, 0, 0), 700); return true; } - public static bool SelectItems(AutomationElement tab, ICollection names) + public static async Task SelectItemsAsync(AutomationElement tab, ICollection names) { if (names.Count == 0) return false; var condition = new PropertyCondition(Automation.PropertyLibrary.Element.ClassName, "UIItemsView"); - var itemsView = Helper.DoUntilNotDefault(() => tab.FindFirstWithOptions(TreeScope.Subtree, condition, TreeTraversalOptions.Default, tab)); + var itemsView = await Helper.DoUntilNotDefaultAsync( + () => tab.FindFirstWithOptions(TreeScope.Subtree, condition, TreeTraversalOptions.Default, tab)) + .ConfigureAwait(false); var files = itemsView?.FindAllChildren(); if (files == default) return false; @@ -185,12 +193,15 @@ public static bool SelectItems(AutomationElement tab, ICollection names) // At least one is selected. return selectedCount > 0; } - public static bool CloseRandomTabOfAWindow(AutomationElement window) + public static async Task CloseRandomTabOfAWindowAsync(AutomationElement window) { - var headerBar = Helper.DoUntilNotDefault(() => window.FindFirstChild(c => c.ByClassName("Microsoft.UI.Content.DesktopChildSiteBridge"))); + var headerBar = await Helper.DoUntilNotDefaultAsync( + () => window.FindFirstChild(c => c.ByClassName("Microsoft.UI.Content.DesktopChildSiteBridge"))) + .ConfigureAwait(false); + if (headerBar == default) return false; - var closeButton = Helper.DoUntilNotDefault(() => headerBar.FindFirstDescendant("CloseButton")); + var closeButton = await Helper.DoUntilNotDefaultAsync(() => headerBar.FindFirstDescendant("CloseButton")).ConfigureAwait(false); var invokePattern = closeButton?.Patterns.Invoke.Pattern; if (invokePattern == default) @@ -200,33 +211,32 @@ public static bool CloseRandomTabOfAWindow(AutomationElement window) return true; } - private static void GetHeaderElements(AutomationElement window, out AutomationElement? suggestBox, out AutomationElement? searchBox, out AutomationElement? addressBar) + public static async Task GetHeaderElementsAsync(AutomationElement window) { - suggestBox = default; - searchBox = default; - addressBar = default; + var headerBar = await Helper.DoUntilNotDefaultAsync( + action: () => window.FindFirstChild(c => c.ByClassName("Microsoft.UI.Content.DesktopChildSiteBridge"))) + .ConfigureAwait(false); - var headerBar = Helper.DoUntilNotDefault(() => window.FindFirstChild(c => c.ByClassName("Microsoft.UI.Content.DesktopChildSiteBridge"))); - if (headerBar == default) return; + if (headerBar == default) return default; - searchBox = Helper.DoUntilNotDefault(() => headerBar.FindFirstChild("FileExplorerSearchBox")); - if (searchBox == default) return; + var suggestBox = headerBar.FindFirstChild("PART_AutoSuggestBox"); + var addressBar = suggestBox?.FindFirstChild(c => c.ByName("Address Bar")); - suggestBox = headerBar.FindFirstChild("PART_AutoSuggestBox"); - addressBar = suggestBox?.FindFirstChild(c => c.ByName("Address Bar")); + return new WindowHeaderElements(suggestBox, addressBar); } - private void CloseAndNotifyNewWindow(AutomationElement element, Window window) + private async Task CloseAndNotifyNewWindowAsync(AutomationElement element, Window window) { // the window suppose to have only one tab, so we should be okay. - CloseRandomTabOfAWindow(element); + await CloseRandomTabOfAWindowAsync(element).ConfigureAwait(false); - _onNewWindow.Invoke(window); + _ = onNewWindow.Invoke(window); } public void Dispose() { - Automation.UnregisterAllEvents(); + StopHook(); Automation.Dispose(); + GC.SuppressFinalize(this); } ~UiAutomation() { diff --git a/ExplorerTabUtility/Managers/ClipboardManager.cs b/ExplorerTabUtility/Managers/ClipboardManager.cs new file mode 100644 index 0000000..20005a4 --- /dev/null +++ b/ExplorerTabUtility/Managers/ClipboardManager.cs @@ -0,0 +1,231 @@ +using System.Runtime.InteropServices; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +// ReSharper disable ArrangeTypeMemberModifiers +// ReSharper disable InconsistentNaming + +namespace ExplorerTabUtility.Managers; + +public static class ClipboardManager +{ + // P/Invoke declarations + [DllImport("user32.dll")] + private static extern bool OpenClipboard(nint hWndNewOwner); + [DllImport("user32.dll")] + private static extern bool CloseClipboard(); + [DllImport("user32.dll")] + private static extern nint GetClipboardData(uint uFormat); + [DllImport("user32.dll")] + private static extern nint SetClipboardData(uint uFormat, nint hMem); + [DllImport("user32.dll")] + private static extern bool EmptyClipboard(); + [DllImport("user32.dll")] + private static extern uint EnumClipboardFormats(uint format); + [DllImport("kernel32.dll")] + private static extern nint GlobalLock(nint hMem); + [DllImport("kernel32.dll")] + private static extern uint GlobalSize(nint hMem); + [DllImport("kernel32.dll")] + private static extern bool GlobalUnlock(nint hMem); + [DllImport("kernel32.dll")] + private static extern nint GlobalAlloc(uint uFlags, nuint dwBytes); + [DllImport("kernel32.dll")] + private static extern nint GlobalFree(nint hMem); + + // Flags for GlobalAlloc + private const uint GMEM_MOVEABLE = 0x0002; + private const uint GMEM_ZEROINIT = 0x0040; + private const uint GHND = GMEM_MOVEABLE | GMEM_ZEROINIT; + + // Clipboard formats + private const uint CF_BITMAP = 2; + private const uint CF_UNICODETEXT = 13; + private const uint CF_ENHMETAFILE = 14; + + public static string GetClipboardText() + { + try + { + if (!OpenClipboard(default)) + return string.Empty; + + var handle = GetClipboardData(CF_UNICODETEXT); + if (handle == default) + return string.Empty; + + var lockedData = GlobalLock(handle); + if (lockedData == default) + return string.Empty; + + var size = GlobalSize(handle); + if (size == 0) + { + GlobalUnlock(handle); + return string.Empty; + } + + var buffer = new byte[size]; + Marshal.Copy(lockedData, buffer, 0, (int)size); + GlobalUnlock(handle); + + return Encoding.Unicode.GetString(buffer).TrimEnd('\0'); + } + catch (Exception e) + { + Debug.WriteLine($"Error getting clipboard text: {e}"); + return string.Empty; + } + finally + { + CloseClipboard(); + } + } + + public static void SetClipboardText(string text) + { + try + { + if (!OpenClipboard(default)) + return; + + EmptyClipboard(); + + text = $"{text.TrimEnd('\0')}\0"; + var buffer = Encoding.Unicode.GetBytes(text); + var size = (uint)buffer.Length; + + var handle = GlobalAlloc(GHND, size); + if (handle == default) + return; + + var pointer = GlobalLock(handle); + if (pointer == default) + { + GlobalFree(handle); + return; + } + + Marshal.Copy(buffer, 0, pointer, (int)size); + SetClipboardData(CF_UNICODETEXT, handle); + GlobalUnlock(handle); + } + catch (Exception e) + { + Debug.WriteLine($"Error setting clipboard text: {e}"); + } + finally + { + CloseClipboard(); + } + } + + // Get the current clipboard data as a dictionary of format and data pairs + public static Dictionary GetClipboardData() + { + var data = new Dictionary(); + try + { + if (!OpenClipboard(default)) + return data; + + uint format = 0; + while ((format = EnumClipboardFormats(format)) != 0) + { + if (format is CF_BITMAP or CF_ENHMETAFILE) continue; // Ignore these formats (might cause problems) + + var handle = GetClipboardData(format); + if (handle == default) + continue; + + var lockedData = GlobalLock(handle); + if (lockedData == default) + continue; + + var size = GlobalSize(handle); + if (size == 0) + { + GlobalUnlock(handle); + continue; + } + + var buffer = new byte[size]; + Marshal.Copy(lockedData, buffer, 0, (int)size); + data.Add(format, buffer); + GlobalUnlock(handle); + } + } + catch (Exception e) + { + Debug.WriteLine($"Error getting clipboard data: {e}"); + } + finally + { + CloseClipboard(); + } + return data; + } + + // Set the clipboard data from a dictionary of format and data pairs + public static void SetClipboardData(Dictionary data) + { + if (data.Count == 0) return; + + try + { + if (!OpenClipboard(default)) + return; + + EmptyClipboard(); + foreach (var pair in data) + { + var format = pair.Key; + var buffer = pair.Value; + if (buffer == default || buffer.Length == 0) continue; + + var handle = GlobalAlloc(GHND, (nuint)buffer.Length); + if (handle == default) continue; + + var pointer = GlobalLock(handle); + if (pointer == default) + { + GlobalFree(handle); + continue; + } + + Marshal.Copy(buffer, 0, pointer, buffer.Length); + SetClipboardData(format, handle); + GlobalUnlock(handle); + } + } + catch (Exception e) + { + Debug.WriteLine($"Error setting clipboard data: {e}"); + } + finally + { + CloseClipboard(); + } + } + + // Clear the clipboard + public static void ClearClipboard() + { + try + { + if (!OpenClipboard(default)) return; + + EmptyClipboard(); + } + catch (Exception e) + { + Debug.WriteLine($"Error clearing clipboard: {e}"); + } + finally + { + CloseClipboard(); + } + } +} \ No newline at end of file diff --git a/ExplorerTabUtility/Managers/HookManager.cs b/ExplorerTabUtility/Managers/HookManager.cs new file mode 100644 index 0000000..9220973 --- /dev/null +++ b/ExplorerTabUtility/Managers/HookManager.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using ExplorerTabUtility.Models; +using ExplorerTabUtility.Hooks; + +namespace ExplorerTabUtility.Managers; + +public class HookManager( + IReadOnlyCollection hotKeyProfiles, + Action onHotKeyProfileTriggered, + Action onNewWindow) +{ + private readonly IHook _keyboardHook = new Keyboard(hotKeyProfiles, onHotKeyProfileTriggered); + private readonly IHook _windowHook = new Shell32(onNewWindow); + + public bool IsKeyboardHookStarted => _keyboardHook.IsHookActive; + public bool IsWindowHookStarted => _windowHook.IsHookActive; + + public void StartKeyboardHook() => ChangeHookStatus(_keyboardHook, true); + public void StopKeyboardHook() => ChangeHookStatus(_keyboardHook, false); + public void StartWindowHook() => ChangeHookStatus(_windowHook, true); + public void StopWindowHook() => ChangeHookStatus(_windowHook, false); + + public void Dispose() + { + _keyboardHook.Dispose(); + _windowHook.Dispose(); + } + private static void ChangeHookStatus(IHook hook, bool isActive) + { + if (hook.IsHookActive == isActive) return; + + if (isActive) + hook.StartHook(); + else + hook.StopHook(); + } +} \ No newline at end of file diff --git a/ExplorerTabUtility/Managers/InteractionManager.cs b/ExplorerTabUtility/Managers/InteractionManager.cs new file mode 100644 index 0000000..b0fe71f --- /dev/null +++ b/ExplorerTabUtility/Managers/InteractionManager.cs @@ -0,0 +1,183 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using FlaUI.Core.AutomationElements; +using ExplorerTabUtility.Hooks; +using ExplorerTabUtility.Models; +using ExplorerTabUtility.WinAPI; +using Keyboard = ExplorerTabUtility.Hooks.Keyboard; +using Window = ExplorerTabUtility.Models.Window; + +namespace ExplorerTabUtility.Managers; + +public static class InteractionManager +{ + public static InteractionMethod InteractionMethod { get; set; } + private static readonly SemaphoreSlim Limiter = new(1); + private static nint _mainWindowHandle; + + public static async void OnHotKeyProfileTriggered(HotKeyProfile profile) + { + // If the interaction method is via Keyboard, + // Allow a brief delay for the user to release the keys, + // as not doing so might result in unexpected behavior. + if (InteractionMethod == InteractionMethod.Keyboard) + await Task.Delay(250).ConfigureAwait(false); + + switch (profile.Action) + { + case HotKeyAction.Open: await ShortcutOpenNewTab(profile.Path, profile.Delay).ConfigureAwait(false); break; + case HotKeyAction.Duplicate: await ShortcutDuplicateCurrentTab(profile.Delay).ConfigureAwait(false); break; + default: throw new ArgumentOutOfRangeException(nameof(profile), profile.Action, @"Invalid profile action"); + } + } + + public static async void OnNewWindow(Window window, bool openNewIfNonExist = false) + { + var windowHandle = IntPtr.Zero; + try + { + await Limiter.WaitAsync().ConfigureAwait(false); + + windowHandle = GetMainWindowHWnd(window.OldWindowHandle); + if (windowHandle == default && !openNewIfNonExist) return; + if (windowHandle == default) + { + Shell32.OpenNewWindowAndSelectItems(window.Path, window.SelectedItems); + return; + } + + var windowElement = UiAutomation.FromHandle(windowHandle); + if (windowElement == default) return; + + // Hide the popup (suggestion window). + var popupWindow = WinApi.FindPopupWindow(windowHandle, "PopupHost"); + WinApi.SetWindowTransparency(popupWindow, 0); + + // Store currently opened Tabs, before we open a new one. + var oldTabs = WinApi.GetAllExplorerTabs(); + + // Add new tab. + await AddNewTabAsync(windowElement).ConfigureAwait(false); + + // If it is just a new (This PC | Home), return. + if (string.IsNullOrWhiteSpace(window.Path)) return; + + // Get newly created tab's handle (That is not in 'oldTabs') + var newTabHandle = await WinApi.ListenForNewExplorerTabAsync(oldTabs).ConfigureAwait(false); + if (newTabHandle == default) return; + + // Navigate to the target location + await NavigateAsync(windowElement, newTabHandle, window.Path).ConfigureAwait(false); + + if (window.SelectedItems is not { Count: > 0 } selectedItems) return; + + // Select items + await SelectItemsAsync(newTabHandle, selectedItems).ConfigureAwait(false); + } + finally + { + if (windowHandle != default) + WinApi.RestoreWindowToForeground(windowHandle); + + Limiter.Release(); + } + } + private static nint GetMainWindowHWnd(nint otherThan) + { + if (WinApi.IsWindowHasClassName(_mainWindowHandle, "CabinetWClass")) + return _mainWindowHandle; + + var allWindows = WinApi.FindAllWindowsEx("CabinetWClass"); + + // Get another handle other than the newly created one. (In case if it is still alive.) + _mainWindowHandle = allWindows.FirstOrDefault(t => t != otherThan); + + return _mainWindowHandle; + } + private static Task AddNewTabAsync(AutomationElement window) + { + // if via UI is selected try to add a new tab with UI Automation. + if (InteractionMethod == InteractionMethod.UiAutomation && UiAutomation.AddNewTab(window)) + return Task.CompletedTask; + + // Via Keys is selected or UI Automation fails. + return Keyboard.AddNewTabAsync(window.Properties.NativeWindowHandle.Value); + } + private static async Task NavigateAsync(AutomationElement window, nint tabHandle, string location) + { + // if via UI is selected try to Navigate with UI Automation. + if (InteractionMethod == InteractionMethod.UiAutomation && + await UiAutomation.NavigateAsync(window, tabHandle, location).ConfigureAwait(false)) + return; + + // Via Keys is selected or UI Automation fails. + var windowHandle = window.Properties.NativeWindowHandle.Value; + await Keyboard.NavigateAsync(windowHandle, location).ConfigureAwait(false); + } + private static async Task SelectItemsAsync(nint tabHandle, ICollection names) + { + // if via UI is selected try to Select with UI Automation. + if (InteractionMethod == InteractionMethod.UiAutomation) + { + var tabElement = UiAutomation.FromHandle(tabHandle); + if (tabElement != default && await UiAutomation.SelectItemsAsync(tabElement, names).ConfigureAwait(false)) + return; + } + + // Via Keys is selected or UI Automation fails. + await Keyboard.SelectItemsAsync(tabHandle, names).ConfigureAwait(false); + } + + private static async Task GetCurrentForegroundTabLocation() + { + if (!WinApi.IsFileExplorerForeground(out var foregroundWindow)) + return default; + + // Hide the popup (suggestion window). + var popupWindow = WinApi.FindPopupWindow(foregroundWindow, "PopupHost"); + WinApi.SetWindowTransparency(popupWindow, 0); + + string? currentTabLocation; + if (InteractionMethod == InteractionMethod.UiAutomation) + { + var windowElement = UiAutomation.FromHandle(foregroundWindow); + if (windowElement == default) return default; + + currentTabLocation = await UiAutomation.GetCurrentTabLocationAsync(windowElement).ConfigureAwait(false); + if (currentTabLocation != default) return currentTabLocation; + } + + currentTabLocation = await Keyboard.GetCurrentTabLocationAsync(foregroundWindow, false).ConfigureAwait(false); + + return currentTabLocation; + } + private static async Task ShortcutOpenNewTab(string? path, int delay) + { + if (delay > 0) + await Task.Delay(delay).ConfigureAwait(false); + + OnNewWindow(new Window(path ?? string.Empty), true); + } + private static async Task ShortcutDuplicateCurrentTab(int delay) + { + string? currentTabLocation; + try + { + await Limiter.WaitAsync().ConfigureAwait(false); + currentTabLocation = await GetCurrentForegroundTabLocation().ConfigureAwait(false); + if (currentTabLocation == default) return; + } + finally + { + Limiter.Release(); + } + + if (delay > 0) + await Task.Delay(delay).ConfigureAwait(false); + + OnNewWindow(new Window(currentTabLocation), true); + } +} \ No newline at end of file diff --git a/ExplorerTabUtility/Managers/RegistryManager.cs b/ExplorerTabUtility/Managers/RegistryManager.cs new file mode 100644 index 0000000..f270b22 --- /dev/null +++ b/ExplorerTabUtility/Managers/RegistryManager.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.Win32; +using ExplorerTabUtility.Helpers; + +namespace ExplorerTabUtility.Managers; + +public static class RegistryManager +{ + public static bool IsInStartup() + { + var executablePath = Helper.GetExecutablePath(); + if (string.IsNullOrWhiteSpace(executablePath)) return false; + + using var key = OpenCurrentUserKey(Constants.RunRegistryKeyPath, false); + if (key == default) return false; + + var value = key.GetValue(Constants.AppName) as string; + return string.Equals(value, executablePath, StringComparison.OrdinalIgnoreCase); + } + + public static void ToggleStartup() + { + var executablePath = Helper.GetExecutablePath(); + if (string.IsNullOrWhiteSpace(executablePath)) return; + + using var key = OpenCurrentUserKey(Constants.RunRegistryKeyPath, true); + if (key == default) return; + + // If already set in startup + if (string.Equals(key.GetValue(Constants.AppName) as string, executablePath, StringComparison.OrdinalIgnoreCase)) + { + // Remove from startup + key.DeleteValue(Constants.AppName, false); + } + else + { + // Add to startup + key.SetValue(Constants.AppName, executablePath); + } + } + + private static RegistryKey? OpenCurrentUserKey(string name, bool writable) => Registry.CurrentUser.OpenSubKey(name, writable); +} \ No newline at end of file diff --git a/ExplorerTabUtility/Managers/SettingsManager.cs b/ExplorerTabUtility/Managers/SettingsManager.cs new file mode 100644 index 0000000..4345cba --- /dev/null +++ b/ExplorerTabUtility/Managers/SettingsManager.cs @@ -0,0 +1,52 @@ +namespace ExplorerTabUtility.Managers; + +public static class SettingsManager +{ + public static int InteractionMethod + { + get => Properties.Settings.Default.InteractionMethod; + set + { + Properties.Settings.Default.InteractionMethod = value; + SaveSettings(); + } + } + public static bool IsKeyboardHookActive + { + get => Properties.Settings.Default.KeyboardHook; + set + { + Properties.Settings.Default.KeyboardHook = value; + SaveSettings(); + } + } + public static bool IsWindowHookActive + { + get => Properties.Settings.Default.WindowHook; + set + { + Properties.Settings.Default.WindowHook = value; + SaveSettings(); + } + } + public static string HotKeyProfiles + { + get => Properties.Settings.Default.HotKeyProfiles; + set + { + Properties.Settings.Default.HotKeyProfiles = value; + SaveSettings(); + } + } + public static bool SaveProfilesOnExit + { + get => Properties.Settings.Default.SaveProfilesOnExit; + set + { + Properties.Settings.Default.SaveProfilesOnExit = value; + SaveSettings(); + } + } + + public static void SaveSettings() => Properties.Settings.Default.Save(); +} diff --git a/ExplorerTabUtility/Models/HotKeyAction.cs b/ExplorerTabUtility/Models/HotKeyAction.cs new file mode 100644 index 0000000..3c575ce --- /dev/null +++ b/ExplorerTabUtility/Models/HotKeyAction.cs @@ -0,0 +1,7 @@ +namespace ExplorerTabUtility.Models; + +public enum HotKeyAction +{ + Open, + Duplicate +} \ No newline at end of file diff --git a/ExplorerTabUtility/Models/HotKeyProfile.cs b/ExplorerTabUtility/Models/HotKeyProfile.cs new file mode 100644 index 0000000..ee97e2f --- /dev/null +++ b/ExplorerTabUtility/Models/HotKeyProfile.cs @@ -0,0 +1,26 @@ +using H.Hooks; + +namespace ExplorerTabUtility.Models; + +public class HotKeyProfile +{ + public string? Name { get; set; } + public Key[]? HotKeys { get; set; } + public HotkeyScope Scope { get; set; } + public HotKeyAction Action { get; set; } + public string? Path { get; set; } + public bool IsHandled { get; set; } = true; + public bool IsEnabled { get; set; } = true; + public int Delay { get; set; } + + public HotKeyProfile() { } + public HotKeyProfile(string name, Key[] hotKeys, HotKeyAction action, string? path = default, HotkeyScope scope = HotkeyScope.Global, int delay = 0) + { + Name = name; + HotKeys = hotKeys; + Action = action; + Path = path; + Scope = scope; + Delay = delay; + } +} \ No newline at end of file diff --git a/ExplorerTabUtility/Models/HotkeyScope.cs b/ExplorerTabUtility/Models/HotkeyScope.cs new file mode 100644 index 0000000..8d24ca8 --- /dev/null +++ b/ExplorerTabUtility/Models/HotkeyScope.cs @@ -0,0 +1,7 @@ +namespace ExplorerTabUtility.Models; + +public enum HotkeyScope +{ + Global, + FileExplorer +} \ No newline at end of file diff --git a/ExplorerTabUtility/Models/InteractionMethod.cs b/ExplorerTabUtility/Models/InteractionMethod.cs new file mode 100644 index 0000000..1d79a7a --- /dev/null +++ b/ExplorerTabUtility/Models/InteractionMethod.cs @@ -0,0 +1,7 @@ +namespace ExplorerTabUtility.Models; + +public enum InteractionMethod +{ + UiAutomation, + Keyboard +} \ No newline at end of file diff --git a/ExplorerTabUtility/Models/Window.cs b/ExplorerTabUtility/Models/Window.cs index 9f24fbf..f81e9a9 100644 --- a/ExplorerTabUtility/Models/Window.cs +++ b/ExplorerTabUtility/Models/Window.cs @@ -2,18 +2,14 @@ namespace ExplorerTabUtility.Models; -public class Window +public class Window( + string path, + IList? selectedItems = default, + nint oldWindowHandle = 0, + nint oldTabHandle = 0) { - public nint OldWindowHandle { get; set; } - public nint OldTabHandle { get; set; } - public string Path { get; set; } - public IList? SelectedItems { get; set; } - - public Window(string path, IList? selectedItems = default, nint oldWindowHandle = 0, nint oldTabHandle = 0) - { - Path = path; - SelectedItems = selectedItems; - OldWindowHandle = oldWindowHandle; - OldTabHandle = oldTabHandle; - } + public nint OldWindowHandle { get; set; } = oldWindowHandle; + public nint OldTabHandle { get; set; } = oldTabHandle; + public string Path { get; set; } = path; + public IList? SelectedItems { get; set; } = selectedItems; } \ No newline at end of file diff --git a/ExplorerTabUtility/Models/WindowHeaderElements.cs b/ExplorerTabUtility/Models/WindowHeaderElements.cs new file mode 100644 index 0000000..994d6c3 --- /dev/null +++ b/ExplorerTabUtility/Models/WindowHeaderElements.cs @@ -0,0 +1,11 @@ +using FlaUI.Core.AutomationElements; + +namespace ExplorerTabUtility.Models; + +public class WindowHeaderElements( + AutomationElement? suggestBox = default, + AutomationElement? addressBar = default) +{ + public AutomationElement? SuggestBox { get; set; } = suggestBox; + public AutomationElement? AddressBar { get; set; } = addressBar; +} \ No newline at end of file diff --git a/ExplorerTabUtility/Models/WindowHookVia.cs b/ExplorerTabUtility/Models/WindowHookVia.cs deleted file mode 100644 index c2734be..0000000 --- a/ExplorerTabUtility/Models/WindowHookVia.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ExplorerTabUtility.Models; - -public enum WindowHookVia -{ - Ui, - Keys -} \ No newline at end of file diff --git a/ExplorerTabUtility/Program.cs b/ExplorerTabUtility/Program.cs index 6f2f89d..43cc7d8 100644 --- a/ExplorerTabUtility/Program.cs +++ b/ExplorerTabUtility/Program.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System; +using System.Threading; using System.Windows.Forms; using ExplorerTabUtility.Forms; using ExplorerTabUtility.Helpers; @@ -7,18 +8,19 @@ namespace ExplorerTabUtility; internal class Program { + [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); - + using var mutex = new Mutex(false, Constants.MutexId); if (!mutex.WaitOne(0, false)) { - MessageBox.Show("Another instance is already running.\nCheck in System Tray Icons.", Constants.AppName); + MessageBox.Show(@"Another instance is already running.\nCheck in System Tray Icons.", Constants.AppName); return; } - Application.Run(new TrayIcon()); + Application.Run(new MainForm()); } } \ No newline at end of file diff --git a/ExplorerTabUtility/Properties/Settings.Designer.cs b/ExplorerTabUtility/Properties/Settings.Designer.cs index 427135a..804e0b0 100644 --- a/ExplorerTabUtility/Properties/Settings.Designer.cs +++ b/ExplorerTabUtility/Properties/Settings.Designer.cs @@ -12,7 +12,7 @@ namespace ExplorerTabUtility.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.8.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.9.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); @@ -25,13 +25,13 @@ public static Settings Default { [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("True")] - public bool KeyboardHook { + [global::System.Configuration.DefaultSettingValueAttribute("0")] + public int InteractionMethod { get { - return ((bool)(this["KeyboardHook"])); + return ((int)(this["InteractionMethod"])); } set { - this["KeyboardHook"] = value; + this["InteractionMethod"] = value; } } @@ -50,24 +50,38 @@ public bool WindowHook { [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("True")] - public bool WindowViaUi { + public bool KeyboardHook { + get { + return ((bool)(this["KeyboardHook"])); + } + set { + this["KeyboardHook"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("[{\"Name\":\"Home\",\"HotKeys\":[91,69],\"Scope\":0,\"Action\":0,\"Path\":\"\",\"IsHandled\":true" + + ",\"IsEnabled\":true,\"Delay\":0},{\"Name\":\"Duplicate\",\"HotKeys\":[17,68],\"Scope\":1,\"Ac" + + "tion\":1,\"Path\":null,\"IsHandled\":true,\"IsEnabled\":true,\"Delay\":0}]")] + public string HotKeyProfiles { get { - return ((bool)(this["WindowViaUi"])); + return ((string)(this["HotKeyProfiles"])); } set { - this["WindowViaUi"] = value; + this["HotKeyProfiles"] = value; } } [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("False")] - public bool WindowViaKeys { + [global::System.Configuration.DefaultSettingValueAttribute("True")] + public bool SaveProfilesOnExit { get { - return ((bool)(this["WindowViaKeys"])); + return ((bool)(this["SaveProfilesOnExit"])); } set { - this["WindowViaKeys"] = value; + this["SaveProfilesOnExit"] = value; } } } diff --git a/ExplorerTabUtility/Properties/Settings.settings b/ExplorerTabUtility/Properties/Settings.settings index d8eabcd..c421822 100644 --- a/ExplorerTabUtility/Properties/Settings.settings +++ b/ExplorerTabUtility/Properties/Settings.settings @@ -2,17 +2,20 @@ - - True + + 0 True - + True - - False + + [{"Name":"Home","HotKeys":[91,69],"Scope":0,"Action":0,"Path":"","IsHandled":true,"IsEnabled":true,"Delay":0},{"Name":"Duplicate","HotKeys":[17,68],"Scope":1,"Action":1,"Path":null,"IsHandled":true,"IsEnabled":true,"Delay":0}] + + + True \ No newline at end of file diff --git a/ExplorerTabUtility/WinAPI/Delegates.cs b/ExplorerTabUtility/WinAPI/Delegates.cs index 3db019d..759e90d 100644 --- a/ExplorerTabUtility/WinAPI/Delegates.cs +++ b/ExplorerTabUtility/WinAPI/Delegates.cs @@ -1,9 +1,7 @@ // ReSharper disable IdentifierTypo -using System; - namespace ExplorerTabUtility.WinAPI; -public delegate nint EnumWindowsProc(IntPtr hWnd, nint lParam); +public delegate bool EnumWindowsProc(nint hWnd, nint lParam); public delegate nint HookProc(int nCode, nint wParam, nint lParam); -public delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, nint hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime); \ No newline at end of file +public delegate void WinEventDelegate(nint hWinEventHook, uint eventType, nint hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime); \ No newline at end of file diff --git a/ExplorerTabUtility/WinAPI/WinApi.cs b/ExplorerTabUtility/WinAPI/WinApi.cs index e69812a..27014fb 100644 --- a/ExplorerTabUtility/WinAPI/WinApi.cs +++ b/ExplorerTabUtility/WinAPI/WinApi.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Collections.Generic; using System.Runtime.InteropServices; +using System.Threading.Tasks; using ExplorerTabUtility.Helpers; namespace ExplorerTabUtility.WinAPI; @@ -14,18 +15,22 @@ public static class WinApi { public const int EVENT_OBJECT_CREATE = 0x8000; - public const int VK_WIN = 0x5B; // Windows key code - public const int VK_E = 0x45; // E key code - - public const int WM_KEYDOWN = 0x0100; // Key down flag public const int WM_SETFOCUS = 0x0007; // Set Keyboard focus + public const int WM_SETREDRAW = 0xB; // Allow or prevent changes in a window from being redrawn + public const int WM_GETTEXT = 0x000D; // Get the text of a window public const int SW_HIDE = 0; // Hide window public const int SW_SHOWNOACTIVATE = 4; // Show window but not activated public const int SWP_NOSIZE = 0x0001; // Retains the current size public const int SWP_NOZORDER = 0x0004; // Retains the current Z order - public const int GW_ENABLEDPOPUP = 6; // Get the popup window owned by the specified window + public const int GW_OWNER = 4; // Get the specified window's owner window, if any + public const int GW_ENABLEDPOPUP = 6; // Get the enabled popup window owned by the specified window + public const int GWL_STYLE = -16; // window style. + public const int GWL_EXSTYLE = -20; // Extended window style. + public const int WS_EX_LAYERED = 0x80000; // Layered window. + public const uint WS_POPUP = 0x80000000; // popup. + public const int LWA_ALPHA = 0x2; // Determine the opacity of a layered window [DllImport("kernel32.dll")] public static extern nint LoadLibrary(string lpFileName); @@ -34,13 +39,13 @@ public static class WinApi public static extern bool FreeLibrary(nint hModule); [DllImport("user32.dll")] - public static extern nint SetWinEventHook(uint eventMin, uint eventMax, nint hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags); + public static extern nint SetWinEventHook(uint eventMin, uint eventMax, nint hModWinEventProc, WinEventDelegate lPfnWinEventProc, uint idProcess, uint idThread, uint dwFlags); [DllImport("user32.dll")] public static extern bool UnhookWinEvent(nint hWinEventHook); [DllImport("user32.dll", SetLastError = true)] - public static extern nint SetWindowsHookEx(WinHookType HookType, HookProc lpfn, nint hMod, uint dwThreadId); + public static extern nint SetWindowsHookEx(WinHookType HookType, HookProc lPfn, nint hMod, uint dwThreadId); [DllImport("user32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] @@ -55,15 +60,30 @@ public static class WinApi [DllImport("user32.dll", SetLastError = true)] public static extern nint FindWindowEx(nint parentHandle, nint childAfter, string className, string? windowTitle); + [DllImport("user32.dll", SetLastError = true)] + public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, nint lParam); + + [DllImport("user32.dll")] + public static extern bool ShowWindow(nint handle, int nCmdShow); + [DllImport("user32.dll")] public static extern nint GetWindow(nint hWnd, uint uCmd); [DllImport("user32.dll")] - public static extern bool ShowWindow(nint handle, int nCmdShow); + public static extern nint GetForegroundWindow(); [DllImport("user32.dll", SetLastError = true)] public static extern bool SetForegroundWindow(nint hWnd); + [DllImport("user32.dll", SetLastError = true)] + public static extern int GetWindowLong(nint hWnd, int nIndex); + + [DllImport("user32.dll")] + public static extern int SetWindowLong(nint hWnd, int nIndex, int dwNewLong); + + [DllImport("user32.dll")] + public static extern bool SetLayeredWindowAttributes(nint hWnd, uint crKey, byte bAlpha, uint dwFlags); + [DllImport("user32.dll")] public static extern bool SetWindowPos(nint hWnd, nint hWndInsertAfter, int x, int Y, int cx, int cy, uint wFlags); @@ -76,15 +96,35 @@ public static class WinApi [DllImport("user32.dll")] public static extern uint RealGetWindowClass(nint hwnd, StringBuilder pszType, uint cchType); + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern nint SendMessage(nint hWnd, uint Msg, nint wParam, nint lParam); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern nint SendMessage(nint hWnd, uint Msg, nint wParam, StringBuilder lParam); + [return: MarshalAs(UnmanagedType.Bool)] [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern bool PostMessage(nint hWnd, uint Msg, nint wParam, nint lParam); + [DllImport("shell32.dll", CharSet = CharSet.Unicode)] + public static extern nint ILCreateFromPathW(string pszPath); + + [DllImport("shell32.dll")] + public static extern void ILFree(nint pidl); + + [DllImport("shell32.dll", SetLastError = true)] + public static extern int SHOpenFolderAndSelectItems(nint pidlFolder, uint cIdl, [In, MarshalAs(UnmanagedType.LPArray)] nint[] apidl, uint dwFlags); + + public static bool IsFileExplorerForeground(out nint foregroundWindow) + { + foregroundWindow = GetForegroundWindow(); + return IsWindowHasClassName(foregroundWindow, "CabinetWClass"); + } public static nint GetAnotherExplorerWindow(nint currentWindow) { return currentWindow == default ? FindWindow("CabinetWClass", default) - : FindAllWindowsEx() + : FindAllWindowsEx("CabinetWClass") .FirstOrDefault(window => window != currentWindow); } public static nint ListenForNewExplorerTab(IReadOnlyCollection currentTabs, int searchTimeMs = 1000) @@ -95,16 +135,24 @@ public static nint ListenForNewExplorerTab(IReadOnlyCollection currentTabs .FirstOrDefault(), searchTimeMs); } + public static Task ListenForNewExplorerTabAsync(IReadOnlyCollection currentTabs, int searchTimeMs = 1000) + { + return Helper.DoUntilNotDefaultAsync(() => + GetAllExplorerTabs() + .Except(currentTabs) + .FirstOrDefault(), + searchTimeMs); + } public static List GetAllExplorerTabs() { var tabs = new List(); - foreach (var window in FindAllWindowsEx()) + foreach (var window in FindAllWindowsEx("CabinetWClass")) tabs.AddRange(FindAllWindowsEx("ShellTabWindowClass", window)); return tabs; } - public static IEnumerable FindAllWindowsEx(string className = "CabinetWClass", nint parent = 0, string? windowTitle = default) + public static IEnumerable FindAllWindowsEx(string className, nint parent = 0, string? windowTitle = default) { var handle = IntPtr.Zero; do @@ -118,6 +166,52 @@ public static IEnumerable FindAllWindowsEx(string className = "CabinetWCla } while (handle != default); } + /// + /// Finds a popup window associated with a specified owner window. + /// + /// The handle of the owner window. + /// Optional. The caption of the popup window. If not specified, the method will return the first popup window found. + /// Optional. The class name of the popup window. If not specified, the method will return the first popup window found. + /// The handle of the popup window if found, otherwise returns default value. + public static nint FindPopupWindow(nint owner, string? popupCaption = default, string? popupClassName = default) + { + nint popupHWnd = default; + EnumWindows((hWnd, _) => + { + var style = GetWindowLong(hWnd, GWL_STYLE); + var isPopup = (style & WS_POPUP) == WS_POPUP; + if (!isPopup) return true; + + var windowOwner = GetWindow(hWnd, GW_OWNER); + if (windowOwner != owner) return true; + + // If the caption is specified, check if it matches + if (popupCaption?.Length > 0) + { + var caption = GetWindowCaption(hWnd, popupCaption.Length); + + // If the caption doesn't match, continue enumerating windows + if (!string.Equals(caption, popupCaption, StringComparison.OrdinalIgnoreCase)) + return true; + } + + // If the class name is specified, check if it matches + if (popupClassName?.Length > 0) + { + var windowClassName = GetWindowClassName(hWnd, popupClassName.Length); + + // If the class name doesn't match, continue enumerating windows + if (!string.Equals(windowClassName, popupClassName, StringComparison.OrdinalIgnoreCase)) + return true; + } + + popupHWnd = hWnd; + return false; // Stop enumerating windows + }, default); + + return popupHWnd; + } + /// /// Hides the specified window by moving it outside the visible screen area. /// @@ -141,7 +235,7 @@ public static void RestoreWindowToForeground(nint window) //If Minimized if (IsIconic(window)) { - // Show the window but don't activate it (otherwise won't respond to hot-keys), SetForegroundWindow gonna activate it. + // Show the window but don't activate it (otherwise won't respond to hot-keys), SetForegroundWindow going to activate it. ShowWindow(window, SW_SHOWNOACTIVATE); } @@ -149,12 +243,44 @@ public static void RestoreWindowToForeground(nint window) SetForegroundWindow(window); } + /// + /// Sets the transparency of the specified window. + /// + /// A handle to the window to set the transparency for. + /// The transparency value to set for the window. + /// A value of 0 makes the window completely transparent, and a value of 255 makes the window opaque. + public static void SetWindowTransparency(nint hWnd, byte alpha = 128) + { + if (hWnd == default) return; + + // Clamp the alpha value between 0 and 255 + alpha = alpha.Clamp(byte.MinValue, byte.MaxValue); + + // Get the current extended window style + var extendedStyle = GetWindowLong(hWnd, GWL_EXSTYLE); + + // Make the window layered + SetWindowLong(hWnd, GWL_EXSTYLE, extendedStyle | WS_EX_LAYERED); + + // Set the transparency (alpha value) of the window (0 = transparent, 255 = opaque) + SetLayeredWindowAttributes(hWnd, 0, alpha, LWA_ALPHA); + } + + public static string GetWindowCaption(nint hWnd, int maxCaptionLength = 254) + { + if (hWnd == default) return string.Empty; + + var caption = new StringBuilder(maxCaptionLength + 1); + SendMessage(hWnd, WM_GETTEXT, caption.Capacity, caption); + + return caption.ToString(); + } public static string GetWindowClassName(nint hWnd, int maxClassNameLength = 254) { - if (hWnd == IntPtr.Zero) return string.Empty; + if (hWnd == default) return string.Empty; - var className = new StringBuilder(maxClassNameLength); - _ = RealGetWindowClass(hWnd, className, (uint)(maxClassNameLength + 1)); + var className = new StringBuilder(maxClassNameLength + 1); + RealGetWindowClass(hWnd, className, (uint)className.Capacity); return className.ToString(); } @@ -164,4 +290,14 @@ public static bool IsWindowHasClassName(nint hWnd, string className, StringCompa return string.Equals(currentClassName, className, comparison); } + + public static void SuspendDrawing(this System.Windows.Forms.Control target) + { + SendMessage(target.Handle, WM_SETREDRAW, 0, 0); + } + public static void ResumeDrawing(this System.Windows.Forms.Control target, bool redraw = true) + { + SendMessage(target.Handle, WM_SETREDRAW, 1, 0); + if (redraw) target.Refresh(); + } } \ No newline at end of file