using System; using System.Collections.Generic; using System.Drawing; using System.Runtime.CompilerServices; using System.Windows.Forms; namespace Clipman { internal sealed class ContextHelp : IMessageFilter, IDisposable { private const int KeyDownMessage = 0x0100; private sealed class HelpText { public string Text; } private static readonly ConditionalWeakTable Descriptions = new ConditionalWeakTable(); private static readonly Dictionary Instructions = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "Clipman sections", "Choose Text, Links, Rich Text or Files. Only enabled sections are shown. Control+Tab moves between sections; Alt+Left or Alt+Right reorders the focused tab." }, { "Text history", "Select text clips with the arrow keys. Enter copies and closes, optionally pasting according to Preferences. Control+Enter includes the name; F2 edits and F4 views." }, { "Links history", "Select web links with the arrow keys. Enter copies the destination; Control+Enter includes its name. Alt+Enter opens the link in your browser and closes History." }, { "Rich text history", "Formatted clips and embedded images. Copy preserves supported formatting. Alt+Enter opens a selected image in its default application." }, { "File history", "Device-local file events, not uploaded files. Enter restores the selected files to the clipboard. Alt+Enter reveals one selected file or folder and closes History. Clearing history never deletes the original files." }, { "Quick Paste mode", "Choose whether the shortcut pastes and restores the previous clipboard, pastes and keeps this clip, or only copies it without pasting." }, { "Pinned", "Protect this entry from normal history limits and bulk clearing. Unpin it to let those rules apply again." }, { "Delete", "Remove the selected entry or device-local secret. Removing a file-history record never deletes the original file." }, { "Add", "Create a new device-local secret with a name, private value and optional shortcut." }, { "Properties", "Edit the selected secret's name, private value and shortcut." }, { "Quick Paste", "Paste the selected secret using its configured mode without adding the value to shared history." }, { "Copy", "Copy the displayed content to the system clipboard. This does not edit the saved entry." }, { "Copy text", "Copy the displayed clip text to the system clipboard without editing the saved entry." }, { "Use current history password", "Create this export with the same encryption password as the live history. Re-enter that password before exporting." }, { "Use a new export password", "Encrypt this export using a separate password. This does not change the live history password." }, { "Current history password", "Unlock the live history before exporting it. Help never reads this field's contents." }, { "New export password", "Password used only for this encrypted export. Keep it safely; it is required to import the file elsewhere." }, { "Confirm new export password", "Retype the new export password to catch mistakes before writing the file." }, { "Clipboard monitoring active", "Record new clipboard items automatically. Turning this off leaves existing history available; explicit capture commands remain separate." }, { "Play sounds", "Play event sounds for accepted copies, sync and monitoring changes. This does not change clipboard capture." }, { "Confirm before deleting entries", "Ask before removing history entries. Disable only if you prefer immediate deletion; removing history does not delete the original files." }, { "ClipMerge window in milliseconds", "Time allowed for the deliberate second copy that starts an append. Allowed range: 200 to 2000 milliseconds. Shorter windows reduce accidental merging." }, { "ClipMerge text separator", "Choose what goes between appended selections. This affects ClipMerge, not copying several selected history entries." }, { "Multiple selected entries separator", "Choose what goes between entries copied together from history. This is independent of ClipMerge; No separator joins them directly." }, { "Automatically group new clips by source application", "Use the application that supplied the clipboard as the group for newly captured entries. Existing groups are unchanged." }, { "Automatically remove URL tracking from copied text", "Remove recognized tracking parameters from newly copied web links. This can change the saved destination; turn it off to retain the original URL." }, { "Save list position", "Remember your place in each history section when closing and reopening history." }, { "Remove duplicate entries", "Use the Duplicate handling choice when text already exists. Pinned entries remain protected." }, { "Duplicate handling", "Move to top reuses the existing entry; Ignore leaves it in place; Keep both retains separate copies." }, { "Maximum entries", "Limit retained normal text history. Zero means no count limit. Pinned entries are not removed by this limit." }, { "Maximum entry age, days", "Remove old normal text history after this many days. Zero means no age limit. Pinned entries are retained." }, { "Automatically remove unavailable file-history events", "Remove unpinned events whose referenced files are no longer available. This removes history records, never the files themselves." }, { "Diagnostics event limit", "Limit the number of file events included in diagnostics, not the number stored in history." }, { "History storage type", "Local or shared folder uses your chosen data folder. Clipman Server exchanges encrypted history with the configured server; file events and secrets remain device-local." }, { "Data folder", "Folder containing this device's settings, sounds, logs and history. Use a dedicated folder. A cloud provider must actually synchronize shared files; Clipman does not configure that provider." }, { "Browse", "Select the Clipman data folder. Review the destination before accepting a settings move." }, { "Clipman Server host", "Enter the reachable server address and port. Use HTTPS for a remote connection. A wildcard listen address such as 0.0.0.0 is not a client destination." }, { "Private certificate authority SHA-256 fingerprint", "Compare this public certificate fingerprint with the server owner's trusted copy before accepting a private authority." }, { "Show history password", "Reveal or hide the password fields on screen. This does not change encryption or whether the password is remembered." }, { "Generate password", "Generate a strong history password. Keep a safe copy and use the same password on devices that should share history." }, { "Use no password", "Remove history encryption after confirmation. Do not use this for a shared or public server." }, { "Ignored applications", "One process name per line, with or without .exe. Automatic copies from these apps are ignored; explicit imports are separate." }, { "Add running app", "Choose a currently running application to add to the ignore list." }, { "Refresh", "Refresh the list of running applications without changing the ignored applications list." }, { "Run Clipman at Windows startup", "Start the background clipboard manager when you sign into Windows. This does not open history." }, { "Update check frequency", "Choose when Clipman checks for a newer Windows client. Never disables automatic checks, but manual Check for updates remains available." }, { "Install updates silently when possible", "Allow automatic update installation without asking. Settings and history are preserved; manual update commands remain available." }, { "Add Clipman to the Windows Send To menu for text files", "Add selected text-file contents to history from Explorer's Send To menu without opening the file in an editor." }, { "Show history window after Send To imports", "Open history after a Send To import. Otherwise the import completes in the background." }, { "Sensitive data mode", "Exclude from history applies selected detection presets to automatic capture only. It does not erase the system clipboard, existing history or deliberate imports." }, { "History password", "Unlock the encrypted history. Devices using different history passwords have separate server buckets; the server token alone does not select your history." }, { "Secret name", "Label for this device-local secret. The name is visible in the secrets list; its value remains hidden." }, { "Secret value", "Private value copied or pasted by this secret. It stays in the device-local encrypted secrets database, not shared history." }, { "Confirm secret value", "Retype the secret to catch mistakes before saving. This field is never included in help or diagnostics." }, { "Paste and restore previous clipboard", "Paste the target into the previous application, then restore the prior clipboard contents." }, { "Paste and keep target on clipboard", "Paste the target into the previous application and leave it on the clipboard afterward." }, { "Copy to clipboard only", "Put the target on the clipboard without sending a paste command to another application." }, { "Channel name", "Name this sync channel using 1 to 32 letters, digits, spaces, dashes or underscores. Reserved names cannot be used." }, { "Groups that route into this channel", "Match these source groups. Multiple choices are alternatives; leave empty to match every group." }, { "Source devices this channel captures from", "Match these source devices. Leave empty to match every device. Device and group conditions must both match." }, { "All channels", "Receive every channel, including channels added later. Main history is always received." }, { "Channels received", "Select the channels this device receives. Unsubscribing hides that channel locally; it does not delete its clips." }, { "Add channel", "Create a routing channel. Rules are evaluated in list order; the first matching channel receives an entry." }, { "Edit channel", "Change the selected channel's name or conditions. Saving can relocate matching history entries." }, { "Edit subscriptions", "Choose which channels the selected device receives without deleting history from the server." }, { "Close", "Close this window and return to the previous control. In Preferences, changes are applied as you make them." }, { "Cancel", "Dismiss this dialog without accepting its pending changes." }, { "OK", "Accept this dialog's changes. Review any validation message if it remains open." }, { "Save", "Save the entry. Enter inserts a new line in the text editor; Control+Enter also saves." } }; private bool disposed; internal ContextHelp() { Application.AddMessageFilter(this); } internal static void SetDescription(Control control, string text) { Descriptions.GetValue(control, key => new HelpText()).Text = text; control.AccessibleDescription = string.Empty; } internal static void Prepare(Control control, bool moveDescriptions) { if (!string.IsNullOrEmpty(control.AccessibleDescription)) { Descriptions.GetValue(control, key => new HelpText()).Text = control.AccessibleDescription; if (moveDescriptions) control.AccessibleDescription = string.Empty; } foreach (Control child in control.Controls) Prepare(child, moveDescriptions); } internal static Control FocusedControl(Control parent) { foreach (Control child in parent.Controls) if (child.ContainsFocus) return FocusedControl(child); return parent; } internal static string ControlName(Control control) { for (var current = control; current != null && !(current is Form); current = current.Parent) { if (!string.IsNullOrWhiteSpace(current.AccessibleName)) return current.AccessibleName; if (current is ButtonBase || current is TabPage) return CleanLabel(current.Text); var index = current.Parent == null ? -1 : current.Parent.Controls.GetChildIndex(current); if (index > 0 && current.Parent.Controls[index - 1] is Label) return CleanLabel(current.Parent.Controls[index - 1].Text); } return "Current window"; } private static string CleanLabel(string text) { return (text ?? "").Replace("&", "").Trim().TrimEnd('.', ':'); } internal static string Description(Control control) { if (control is FocusableImagePreview) return "Read-only preview of the selected embedded image. Copy the history entry to retain its image data; editing its name does not change the image."; if (control.FindForm() is ExportPasswordForm && ControlName(control) == "Use no password") return "Create a readable, unencrypted export after confirmation. This does not remove encryption from the live history."; for (var current = control; current != null && !(current is Form); current = current.Parent) { if (current != control && current is TabPage) break; if (!string.IsNullOrEmpty(current.AccessibleDescription)) return current.AccessibleDescription; HelpText stored; if (Descriptions.TryGetValue(current, out stored)) return stored.Text; string instructions; if (Instructions.TryGetValue(ControlName(current), out instructions)) return instructions; } if (control is TextBoxBase) return "Enter or review text for this field. Tab moves to the next control; Shift+Tab returns to the previous one. Help never includes the field's contents."; if (control is NumericUpDown) return "Type a number or use the arrow keys within this control's allowed range."; if (control is ComboBox || control is TabControl) return "Choose an option with the arrow keys. Tab moves into the selected section or to the next control."; if (control is ListView || control is TreeView || control is ListBox) return "Navigate items with the arrow keys. Selection alone does not copy or delete an entry; use the window's commands for the selected item."; if (control is CheckBox || control is RadioButton) return "Space changes this option. Its name and current state remain available to your screen reader."; if (control is ButtonBase) return "Activate this command with Space or Enter. If another dialog opens, review it before accepting."; return "Help for this window is available in the complete manual. No clipboard contents or private field values are included here."; } public bool PreFilterMessage(ref Message message) { if (disposed || message.Msg != KeyDownMessage || (Keys)message.WParam.ToInt32() != Keys.F1 || Control.ModifierKeys != Keys.None) return false; var control = Control.FromChildHandle(message.HWnd); var owner = control == null ? null : control.FindForm(); if (owner is ContextHelpDialog) return true; for (var form = owner; form != null; form = form.Owner) if (form.GetType().Namespace == "Clipman") { Show(owner); return true; } return false; } internal static void OpenManual(Form owner) { try { var path = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Manual.html"); System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(System.IO.File.Exists(path) ? path : "https://onjlouis.github.io/clipman/manual.html") { UseShellExecute = true }); } catch { MessageBox.Show(owner, "Could not open the manual. You can also use Help, Manual from history.", "Clipman Help", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } internal static void Show(Form owner) { if (owner == null || owner is ContextHelpDialog) return; var focused = FocusedControl(owner); using (var dialog = new ContextHelpDialog(ControlName(focused), Description(focused))) dialog.ShowDialog(owner); if (!focused.IsDisposed && focused.CanFocus) focused.Focus(); } public void Dispose() { if (disposed) return; disposed = true; Application.RemoveMessageFilter(this); } } internal sealed class ContextHelpDialog : Form { internal ContextHelpDialog(string name, string instructions) { Text = "Help: " + name; StartPosition = FormStartPosition.CenterParent; Size = new Size(620, 360); MinimizeBox = false; MaximizeBox = false; ShowInTaskbar = false; var text = new TextBox { Multiline = true, ReadOnly = true, AcceptsTab = false, ScrollBars = ScrollBars.Vertical, Dock = DockStyle.Fill, Text = instructions, AccessibleName = name + " help", TabIndex = 0 }; var manual = new Button { Text = "Open &Manual", AutoSize = true, TabIndex = 1 }; manual.Click += (s, e) => ContextHelp.OpenManual(this); var close = new Button { Text = "&Close", AutoSize = true, DialogResult = DialogResult.Cancel, TabIndex = 2 }; var buttons = new FlowLayoutPanel { Dock = DockStyle.Bottom, AutoSize = true, Padding = new Padding(8) }; buttons.Controls.Add(manual); buttons.Controls.Add(close); Controls.Add(text); Controls.Add(buttons); CancelButton = close; Shown += (s, e) => { text.Focus(); text.Select(0, 0); }; } } }