Ctrl K
ComunidadGuías…

src/ClipmanApplicationContext.cs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Windows.Forms;

namespace Clipman
{
    internal sealed class ClipmanApplicationContext : ApplicationContext
    {
        private const int ShowHotkeyId = 1001;
        private const int ToggleHotkeyId = 1002;
        private const int SaveCurrentClipboardHotkeyId = 1003;
        private const int ToggleHotkeyAlternateId = 1004;
        private const int QuickClipHotkeyId = 1005;
        private const int QuickCopyHotkeyBaseId = 2000;
        private const int SecretHotkeyBaseId = 3000;
        private const int MaximumClipboardSnapshotTextLength = 32 * 1024 * 1024;
        private const int MaximumClipboardSnapshotStreamLength = 64 * 1024 * 1024;

        private readonly string appDirectory;
        private readonly SettingsStore settingsStore;
        private SoundService sounds;
        private readonly MessageWindow messageWindow;
        private readonly Control invoker;
        private readonly NotifyIcon notifyIcon;
        private readonly EventWaitHandle closeEvent;
        private readonly EventWaitHandle showEvent;
        private readonly EventWaitHandle recoverEvent;
        private readonly EventWaitHandle pauseEvent;
        private readonly EventWaitHandle resumeEvent;
        private readonly EventWaitHandle toggleEvent;
        private readonly EventWaitHandle historyChangedEvent;
        private readonly EventWaitHandle historyAddedEvent;
        private readonly Thread closeThread;
        private readonly Thread showThread;
        private readonly Thread recoverThread;
        private readonly Thread pauseThread;
        private readonly Thread resumeThread;
        private readonly Thread toggleThread;
        private readonly Thread historyChangedThread;
        private readonly Thread historyAddedThread;
        private readonly ClipboardFloodGuardRegistry clipboardFloodGuards = new ClipboardFloodGuardRegistry();
        private readonly ClipboardNotificationState clipboardNotifications = new ClipboardNotificationState();
        private readonly ClipMergeDetector clipMergeDetector = new ClipMergeDetector();
        private readonly WebsiteTitleDiagnostics websiteTitleDiagnostics = new WebsiteTitleDiagnostics();
        private readonly object automaticWebsiteTitleLock = new object();
        private readonly Queue<AutomaticWebsiteTitleRequest> automaticWebsiteTitleQueue = new Queue<AutomaticWebsiteTitleRequest>();
        private readonly HashSet<string> automaticWebsiteTitleEntryIds = new HashSet<string>(StringComparer.Ordinal);
        private bool automaticWebsiteTitleWorkerRunning;
        private FileSystemWatcher sharedStateWatcher;
        private FileSystemWatcher executableWatcher;
        private System.Threading.Timer sharedStateTimer;
        private System.Threading.Timer updateCheckTimer;
        private readonly System.Windows.Forms.Timer clipboardFloodRecoveryTimer;
        private AppSettings settings;
        private ClipStore store;
        private FileClipboardEventStore fileEventStore;
        private SecretStore secretStore;
        private HistoryForm historyForm;
        private PreferencesForm preferencesForm;
        private SecretsForm secretsForm;
        private bool showHotkeyRegistered;
        private bool toggleHotkeyRegistered;
        private bool saveCurrentClipboardHotkeyRegistered;
        private bool quickClipHotkeyRegistered;
        private bool toggleAlternateHotkeyRegistered;
        private readonly Dictionary<int, string> quickCopyHotkeyEntryIds = new Dictionary<int, string>();
        private readonly Dictionary<int, string> secretHotkeyEntryIds = new Dictionary<int, string>();
        private int quickCopyHotkeysRegistered;
        private int secretHotkeysRegistered;
        private string lastClipboardPrivacySignal = "None";
        private string lastHandledCloseRequestId = string.Empty;
        private string lastAutoCopiedRemoteEntryId = string.Empty;
        private long lastAutoCopiedRemoteEntryStamp;
        private string databasePassword = string.Empty;
        private IntPtr previousForegroundWindow = IntPtr.Zero;
        private string lastReceivedHistoryTab = HistoryTabs.Text;
        private bool receivedHistoryTabPending;
        private int storageRetryInProgress;

        private sealed class AutomaticWebsiteTitleRequest
        {
            public string EntryId { get; set; }
            public string OriginalText { get; set; }
            public Uri Uri { get; set; }
        }

        public ClipmanApplicationContext()
        {
            RichImageFileDropData.Cleanup();
            appDirectory = AppDomain.CurrentDomain.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar);
            settingsStore = new SettingsStore(appDirectory, Program.WriteRuntimeLog);
            settings = settingsStore.Load();
            ResolveDatabaseLocation();
            ResolveDatabasePassword();
            SeedServerCacheFromConfiguredDatabase();
            SharedUpdateStateStore.PublishCurrentBuild(SharedExecutableSettingsDirectory(), Application.ExecutablePath);
            sounds = new SoundService(appDirectory, settingsStore.SettingsDirectory);
            store = new ClipStore(EffectiveTextHistoryDatabasePath(), CurrentDatabasePassword, CurrentDeviceName());
            store.Changed += StoreChanged;
            store.StorageStateChanged += StoreStorageStateChanged;
            ConfigureTextHistoryServerStorage();
            ResetRemoteAutoCopyBaseline();
            fileEventStore = new FileClipboardEventStore(settingsStore.DefaultFileHistoryDatabasePath(), CurrentDatabasePassword);
            fileEventStore.Changed += FileEventStoreChanged;
            secretStore = new SecretStore(DefaultSecretsDatabasePath(), CurrentDatabasePassword);

            invoker = new Control();
            invoker.CreateControl();

            clipboardFloodRecoveryTimer = new System.Windows.Forms.Timer
            {
                Interval = ClipboardFloodGuard.DefaultQuietMilliseconds
            };
            clipboardFloodRecoveryTimer.Tick += ClipboardFloodRecoveryTimerTick;

            messageWindow = new MessageWindow(this);
            NativeMethods.AddClipboardFormatListener(messageWindow.Handle);

            notifyIcon = new NotifyIcon
            {
                Text = TrayText(),
                Icon = BuildIcon(settings.Active),
                Visible = true,
                ContextMenuStrip = BuildTrayMenu()
            };
            notifyIcon.DoubleClick += (s, e) => ToggleHistoryWindow();
            if (settings.CaptureClipboardOnStartup)
            {
                TryCaptureClipboardOnStartup();
            }

            RegisterHotkeys();
            ApplyStartupRegistration(false);
            PlayLaunchStateSound();

            closeEvent = new EventWaitHandle(false, EventResetMode.ManualReset, Program.CloseEventName);
            showEvent = new EventWaitHandle(false, EventResetMode.ManualReset, Program.ShowEventName);
            recoverEvent = new EventWaitHandle(false, EventResetMode.ManualReset, Program.RecoverEventName);
            pauseEvent = new EventWaitHandle(false, EventResetMode.ManualReset, Program.PauseEventName);
            resumeEvent = new EventWaitHandle(false, EventResetMode.ManualReset, Program.ResumeEventName);
            toggleEvent = new EventWaitHandle(false, EventResetMode.ManualReset, Program.ToggleEventName);
            historyChangedEvent = new EventWaitHandle(false, EventResetMode.ManualReset, Program.HistoryChangedEventName);
            historyAddedEvent = new EventWaitHandle(false, EventResetMode.ManualReset, Program.HistoryAddedEventName);
            closeThread = new Thread(WaitForClose) { IsBackground = true, Name = "Clipman close event listener" };
            showThread = new Thread(WaitForShow) { IsBackground = true, Name = "Clipman show event listener" };
            recoverThread = new Thread(WaitForRecover) { IsBackground = true, Name = "Clipman storage recovery listener" };
            pauseThread = new Thread(() => WaitForState(pauseEvent, false)) { IsBackground = true, Name = "Clipman pause event listener" };
            resumeThread = new Thread(() => WaitForState(resumeEvent, true)) { IsBackground = true, Name = "Clipman resume event listener" };
            toggleThread = new Thread(WaitForToggle) { IsBackground = true, Name = "Clipman toggle event listener" };
            historyChangedThread = new Thread(WaitForHistoryChanged) { IsBackground = true, Name = "Clipman history-change event listener" };
            historyAddedThread = new Thread(WaitForHistoryAdded) { IsBackground = true, Name = "Clipman history-add event listener" };
            closeThread.Start();
            showThread.Start();
            recoverThread.Start();
            pauseThread.Start();
            resumeThread.Start();
            toggleThread.Start();
            historyChangedThread.Start();
            historyAddedThread.Start();
            if (InstanceStateStore.HasPendingCommandEntry())
            {
                historyAddedEvent.Set();
            }
            StartSharedUpdateWatchers();
            ScheduleSharedUpdateCheck(5000);
            ScheduleUpdateChecks();
            WarnAboutPasswordlessServerConfiguration();
        }

        private void WarnAboutPasswordlessServerConfiguration()
        {
            if (!IsServerStorageEnabled() ||
                string.IsNullOrWhiteSpace(settings.ServerUrl) ||
                string.IsNullOrWhiteSpace(settings.ServerToken) ||
                !string.IsNullOrEmpty(CurrentDatabasePassword()) ||
                invoker == null ||
                !invoker.IsHandleCreated)
            {
                return;
            }

            invoker.BeginInvoke(new Action(() =>
            {
                MessageBox.Show(
                    "Clipman Server now requires a history password. Server synchronization has not started, and your local cached history has not been changed.\r\n\r\nOpen Storage and Password preferences and enter a unique password shared only with your own Clipman clients.",
                    "Clipman Server history password",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Information);
                ShowPreferencesFromTray();
            }));
        }

        public void ShowHistory()
        {
            if (historyForm != null && !historyForm.IsDisposed && historyForm.RestoreQuickClip()) return;
            var historyWasVisible = historyForm != null && !historyForm.IsDisposed && historyForm.Visible;
            if (!historyWasVisible)
            {
                RememberPreviousForegroundWindow();
            }
            var created = false;
            if (historyForm == null || historyForm.IsDisposed)
            {
                historyForm = new HistoryForm(store, settings, SaveSettings, RegisterHotkeys, CopyEntryToClipboard, CopyEntriesToClipboard, CopyPlainTextToClipboard, PasteClipboardIntoPreviousApplication, SaveCurrentClipboardToHistory, GetRecentClipboardEvents, DeleteRecentClipboardEvents, ClearRecentClipboardEvents, RemoveUnavailableRecentClipboardEvents, ToggleRecentClipboardEventPinned, MoveRecentClipboardEvents, ClearTextHistory, ShowPreferences, ShowSecrets, ToggleActive, ExitThread, () => sounds.Skip(settings.SoundsEnabled), BuildDiagnosticsText, HistorySteadyStatusText);
                created = true;
            }

            if (historyForm.WindowState == FormWindowState.Minimized)
            {
                historyForm.WindowState = FormWindowState.Normal;
            }
            if (settings.DynamicHistoryMode && receivedHistoryTabPending)
            {
                historyForm.SelectHistoryTabForOpen(lastReceivedHistoryTab);
                receivedHistoryTabPending = false;
            }
            var handle = historyForm.Handle;
            historyForm.Show();
            NativeMethods.ShowWindow(handle, NativeMethods.SW_SHOWNORMAL);
            historyForm.BringToFront();
            historyForm.Activate();
            NativeMethods.SetForegroundWindow(handle);
            if (created)
            {
                historyForm.TopMost = true;
                historyForm.TopMost = false;
            }
            historyForm.FocusHistoryList(created);
        }

        public void ShowQuickClip()
        {
            if (historyForm != null && !historyForm.IsDisposed && historyForm.ToggleQuickClip()) return;
            var historyWasVisible = historyForm != null && !historyForm.IsDisposed && historyForm.Visible;
            ShowHistory();
            historyForm.ShowQuickClip(() =>
            {
                if (!historyWasVisible && !historyForm.IsDisposed) historyForm.Hide();
            });
        }

        public void ToggleHistoryWindow()
        {
            if (historyForm != null && !historyForm.IsDisposed && historyForm.Visible && historyForm.WindowState != FormWindowState.Minimized)
            {
                historyForm.HideWithQuickClip();
                return;
            }

            ShowHistory();
        }

        public void ToggleActive()
        {
            SetActive(!settings.Active, true);
        }

        private void SetActive(bool active, bool playSound)
        {
            settings.Active = active;
            SaveSettings();
            UpdateTray();
            if (preferencesForm != null && !preferencesForm.IsDisposed)
            {
                preferencesForm.SetActiveChecked(settings.Active);
            }
            if (playSound)
            {
                if (settings.Active) sounds.On(settings.SoundsEnabled); else sounds.Off(settings.SoundsEnabled);
            }
        }

        public void ShowPreferences()
        {
            ShowPreferences(true);
        }

        private void ShowPreferencesFromTray()
        {
            ShowPreferences(false);
        }

        private void ShowPreferences(bool showHistoryIfHidden)
        {
            if (preferencesForm != null && !preferencesForm.IsDisposed)
            {
                FocusPreferencesForm();
                return;
            }

            if (historyForm == null || historyForm.IsDisposed || !historyForm.Visible)
            {
                if (showHistoryIfHidden)
                {
                    ShowHistory();
                }
            }

            preferencesForm = new PreferencesForm(settings, ApplyPreferences, CopySensitiveTextToClipboard, OpenSyncRulesEditor);
            preferencesForm.FormClosed += (s, e) => preferencesForm = null;
            if (historyForm != null && !historyForm.IsDisposed && historyForm.Visible)
            {
                preferencesForm.ShowDialog(historyForm);
            }
            else
            {
                preferencesForm.ShowDialog();
            }
            preferencesForm = null;
        }

        /// <summary>
        /// Opens the modal sync rules editor from Preferences. Returns whether it applied a change,
        /// so Preferences can announce success the same way other preference changes do.
        /// </summary>
        private bool OpenSyncRulesEditor()
        {
            using (var form = new SyncRulesForm(store, CurrentDeviceName()))
            {
                if (preferencesForm != null && !preferencesForm.IsDisposed)
                {
                    form.ShowDialog(preferencesForm);
                }
                else
                {
                    form.ShowDialog();
                }

                if (form.Applied) UpdateTray();
                return form.Applied;
            }
        }

        private void FocusPreferencesForm()
        {
            if (preferencesForm == null || preferencesForm.IsDisposed) return;
            FocusPreferencesFormNow();
            BeginDelayedPreferencesFocus(80);
            BeginDelayedPreferencesFocus(250);
            BeginDelayedPreferencesFocus(600);
        }

        private void BeginDelayedPreferencesFocus(int delayMilliseconds)
        {
            var timer = new System.Windows.Forms.Timer { Interval = delayMilliseconds };
            timer.Tick += (s, e) =>
            {
                timer.Stop();
                timer.Dispose();
                FocusPreferencesFormNow();
            };
            timer.Start();
        }

        private void FocusPreferencesFormNow()
        {
            if (preferencesForm == null || preferencesForm.IsDisposed) return;
            if (preferencesForm.WindowState == FormWindowState.Minimized)
            {
                preferencesForm.WindowState = FormWindowState.Normal;
            }
            if (!preferencesForm.Visible)
            {
                return;
            }
            var handle = preferencesForm.Handle;
            if (handle != IntPtr.Zero)
            {
                NativeMethods.ShowWindow(handle, NativeMethods.SW_RESTORE);
                NativeMethods.SetForegroundWindow(handle);
            }
            preferencesForm.Activate();
            preferencesForm.BringToFront();
            preferencesForm.Focus();
            preferencesForm.BeginInvoke(new Action(() =>
            {
                if (preferencesForm == null || preferencesForm.IsDisposed) return;
                if (preferencesForm.WindowState == FormWindowState.Minimized)
                {
                    preferencesForm.WindowState = FormWindowState.Normal;
                }
                var delayedHandle = preferencesForm.Handle;
                if (delayedHandle != IntPtr.Zero)
                {
                    NativeMethods.ShowWindow(delayedHandle, NativeMethods.SW_RESTORE);
                    NativeMethods.SetForegroundWindow(delayedHandle);
                }
                preferencesForm.Activate();
                preferencesForm.BringToFront();
                preferencesForm.Focus();
            }));
        }

        private void ApplyPreferences(AppSettings updated)
        {
            var nextDatabasePassword = databasePassword;
            if (updated.PasswordClearRequested)
            {
                nextDatabasePassword = string.Empty;
            }
            else if (!string.IsNullOrEmpty(updated.PlainDatabasePassword))
            {
                nextDatabasePassword = updated.PlainDatabasePassword;
            }
            if (!updated.DatabaseEncryptionEnabled)
            {
                nextDatabasePassword = string.Empty;
                updated.ProtectedDatabasePassword = string.Empty;
                updated.RememberDatabasePassword = false;
            }
            else if (updated.RememberDatabasePassword &&
                     string.IsNullOrWhiteSpace(updated.ProtectedDatabasePassword) &&
                     !string.IsNullOrEmpty(nextDatabasePassword))
            {
                updated.ProtectedDatabasePassword = DatabasePasswordProtector.Protect(nextDatabasePassword);
            }
            else if (!updated.RememberDatabasePassword)
            {
                updated.ProtectedDatabasePassword = string.Empty;
            }

            var databaseChanged =
                !string.Equals(settings.DatabasePath, updated.DatabasePath, StringComparison.OrdinalIgnoreCase) ||
                !string.Equals(settings.StorageMode, updated.StorageMode, StringComparison.OrdinalIgnoreCase) ||
                !string.Equals(settings.ServerUrl, updated.ServerUrl, StringComparison.OrdinalIgnoreCase) ||
                !string.Equals(settings.ServerToken, updated.ServerToken, StringComparison.Ordinal) ||
                !string.Equals(settings.ServerCaCertPem, updated.ServerCaCertPem, StringComparison.Ordinal) ||
                !string.Equals(settings.ServerCaHost, updated.ServerCaHost, StringComparison.OrdinalIgnoreCase);
            var wasServerStorageEnabled = IsServerStorageEnabled();
            var serverCacheBeforeStorageChange = wasServerStorageEnabled ? ServerCacheDatabasePath() : string.Empty;
            var activeChanged = settings.Active != updated.Active;
            var sendToChanged = settings.SendToEnabled != updated.SendToEnabled;
            var encryptionChanged =
                settings.DatabaseEncryptionEnabled != updated.DatabaseEncryptionEnabled ||
                settings.RememberDatabasePassword != updated.RememberDatabasePassword ||
                !string.Equals(settings.ProtectedDatabasePassword, updated.ProtectedDatabasePassword, StringComparison.Ordinal) ||
                !string.IsNullOrEmpty(updated.PlainDatabasePassword) ||
                updated.PasswordClearRequested;
            var startupChanged = settings.RunAtStartup != updated.RunAtStartup;
            var updatePolicyChanged =
                !string.Equals(settings.UpdateCheckFrequency, updated.UpdateCheckFrequency, StringComparison.OrdinalIgnoreCase) ||
                settings.InstallUpdatesSilently != updated.InstallUpdatesSilently;
            var autoRemoteCopyTurnedOn = !settings.AutoCopyLatestRemoteText && updated.AutoCopyLatestRemoteText;
            var saveListPositionTurnedOff = settings.SaveListPosition && !updated.SaveListPosition;
            var linksHistoryVisibilityChanged = settings.LinksHistoryEnabled != updated.LinksHistoryEnabled;
            var richTextHistoryVisibilityChanged = settings.RichTextHistoryEnabled != updated.RichTextHistoryEnabled;
            var oldSettingsDirectory = settingsStore.SettingsDirectory;
            settingsStore.Save(updated);
            databasePassword = nextDatabasePassword;
            settings.ShowHistoryHotkey = updated.ShowHistoryHotkey;
            settings.ToggleActiveHotkey = updated.ToggleActiveHotkey;
            settings.SaveCurrentClipboardHotkey = updated.SaveCurrentClipboardHotkey;
            settings.QuickClipHotkey = updated.QuickClipHotkey;
            settings.QuickCopyHotkeys = updated.QuickCopyHotkeys == null
                ? new List<QuickCopyBinding>()
                : updated.QuickCopyHotkeys.Select(b => new QuickCopyBinding { EntryId = b.EntryId, Hotkey = b.Hotkey, Mode = QuickPasteModes.Normalize(b.Mode) }).ToList();
            settings.AutoCopyLatestRemoteText = updated.AutoCopyLatestRemoteText;
            settings.PasteAfterEnter = updated.PasteAfterEnter;
            settings.DynamicHistoryMode = updated.DynamicHistoryMode;
            settings.RemoveDuplicates = updated.RemoveDuplicates;
            settings.SoundsEnabled = updated.SoundsEnabled;
            settings.ClipMergeEnabled = updated.ClipMergeEnabled;
            settings.ClipMergeWindowMilliseconds = ClipMergeDetector.NormalizeWindow(updated.ClipMergeWindowMilliseconds);
            settings.ClipMergeSeparatorMode = updated.ClipMergeSeparatorMode;
            settings.ClipMergeCustomSeparator = updated.ClipMergeCustomSeparator;
            settings.MultipleEntrySeparatorMode = updated.MultipleEntrySeparatorMode;
            settings.MultipleEntryCustomSeparator = updated.MultipleEntryCustomSeparator;
            settings.SaveListPosition = updated.SaveListPosition;
            settings.Active = updated.Active;
            settings.DatabasePath = updated.DatabasePath;
            settings.UseDefaultDatabasePath = updated.UseDefaultDatabasePath;
            settings.StorageMode = updated.StorageMode;
            settings.ServerUrl = updated.ServerUrl;
            settings.ServerToken = updated.ServerToken;
            settings.ServerCaCertPem = updated.ServerCaCertPem;
            settings.ServerCaHost = updated.ServerCaHost;
            settings.MaxHistoryEntries = updated.MaxHistoryEntries;
            settings.MaxHistoryDays = updated.MaxHistoryDays;
            settings.IgnoredProcesses = updated.IgnoredProcesses;
            settings.SortMode = updated.SortMode;
            settings.SortDescending = updated.SortDescending;
            settings.SendToEnabled = updated.SendToEnabled;
            settings.ShowHistoryAfterSendTo = updated.ShowHistoryAfterSendTo;
            settings.GroupFilter = updated.GroupFilter;
            settings.HistoryFilterType = updated.HistoryFilterType;
            settings.DeviceFilter = updated.DeviceFilter;
            settings.DeviceName = updated.DeviceName;
            settings.ConfirmDeletions = updated.ConfirmDeletions;
            settings.ConfirmSingleModifierHotkeys = updated.ConfirmSingleModifierHotkeys;
            settings.ConfirmWebsiteTitleRequests = updated.ConfirmWebsiteTitleRequests;
            settings.AutoNameCopiedWebsiteLinks = updated.AutoNameCopiedWebsiteLinks;
            settings.DuplicateMode = updated.DuplicateMode;
            settings.AutoGroupByApp = updated.AutoGroupByApp;
            settings.AutoRemoveUrlTracking = updated.AutoRemoveUrlTracking;
            settings.LinksHistoryEnabled = updated.LinksHistoryEnabled;
            settings.RichTextHistoryEnabled = updated.RichTextHistoryEnabled;
            settings.IncludeImagesInRichText = updated.RichTextHistoryEnabled && updated.IncludeImagesInRichText;
            settings.AutoAddImageFilesToRichText = settings.IncludeImagesInRichText && updated.AutoAddImageFilesToRichText;
            settings.LastSelectedHistoryTab = HistoryTabs.Normalize(updated.LastSelectedHistoryTab, settings.LinksHistoryEnabled, settings.RichTextHistoryEnabled);
            settings.HistoryTabOrder = HistoryTabs.NormalizeOrder(updated.HistoryTabOrder);
            settings.AutoRemoveUnavailableFileHistoryEvents = updated.AutoRemoveUnavailableFileHistoryEvents;
            settings.DiagnosticsFileHistoryLimit = updated.DiagnosticsFileHistoryLimit;
            settings.SensitiveDataMode = SensitiveDataExclusion.NormalizeMode(updated.SensitiveDataMode);
            settings.SensitiveDataPresetIds = updated.SensitiveDataPresetIds == null ? new List<string>() : new List<string>(updated.SensitiveDataPresetIds);
            settings.RunAtStartup = updated.RunAtStartup;
            settings.CaptureClipboardOnStartup = updated.CaptureClipboardOnStartup;
            settings.UpdateCheckFrequency = updated.UpdateCheckFrequency;
            settings.InstallUpdatesSilently = updated.InstallUpdatesSilently;
            settings.DatabaseEncryptionEnabled = updated.DatabaseEncryptionEnabled;
            settings.RememberDatabasePassword = updated.RememberDatabasePassword;
            settings.ProtectedDatabasePassword = updated.ProtectedDatabasePassword;
            settings.LastPreferencesTab = updated.LastPreferencesTab;
            store.SetMachineName(CurrentDeviceName());
            if (saveListPositionTurnedOff)
            {
                settings.LastSelectedIndex = -1;
            }
            var settingsDirectoryChanged = !string.Equals(oldSettingsDirectory, settingsStore.SettingsDirectory, StringComparison.OrdinalIgnoreCase);
            if (settingsDirectoryChanged)
            {
                SharedUpdateStateStore.PublishCurrentBuild(SharedExecutableSettingsDirectory(), Application.ExecutablePath);
                RestartSharedUpdateWatchers();
                sounds = new SoundService(appDirectory, settingsStore.SettingsDirectory);
                ReopenFileHistoryStore();
                ReopenSecretStore();
            }
            if (sendToChanged)
            {
                try
                {
                    SendToInstaller.SetInstalled(settings.SendToEnabled);
                }
                catch (Exception ex)
                {
                    MessageBox.Show("Could not update the Send To shortcut.\r\n\r\n" + ex.Message, "Clipman Send To", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                }
            }
            if (databaseChanged)
            {
                if (wasServerStorageEnabled && !IsServerStorageEnabled())
                {
                    MergeServerCacheIntoConfiguredDatabase(serverCacheBeforeStorageChange);
                }
                ResolveDatabasePassword();
                ResolveDatabaseLocation();
                SeedServerCacheFromConfiguredDatabase();
                store.SetDatabasePath(EffectiveTextHistoryDatabasePath(), CurrentDatabasePassword);
                ConfigureTextHistoryServerStorage();
                ResetRemoteAutoCopyBaseline();
                ReopenSecretStore();
            }
            else if (encryptionChanged)
            {
                if (!updated.PasswordClearRequested)
                {
                    ResolveDatabasePassword();
                }
                store.ChangeDatabasePassword();
                ConfigureTextHistoryServerStorage();
                if (!settingsDirectoryChanged)
                {
                    fileEventStore.ChangeDatabasePassword();
                }
                if (secretStore != null)
                {
                    secretStore.ChangeDatabasePassword();
                }
                ReopenSecretStore();
            }
            RegisterHotkeys();
            if (startupChanged)
            {
                ApplyStartupRegistration(true);
            }
            if (updatePolicyChanged)
            {
                ScheduleUpdateChecks();
            }
            if (autoRemoteCopyTurnedOn)
            {
                ResetRemoteAutoCopyBaseline();
            }
            UpdateTray();
            if (activeChanged)
            {
                if (settings.Active) sounds.On(settings.SoundsEnabled); else sounds.Off(settings.SoundsEnabled);
            }
            if (historyForm != null && !historyForm.IsDisposed && (databaseChanged || linksHistoryVisibilityChanged || richTextHistoryVisibilityChanged))
            {
                historyForm.RefreshTabsAndReload();
            }
        }

        private void ShowSecrets()
        {
            var password = CurrentDatabasePassword();
            if (string.IsNullOrEmpty(password))
            {
                MessageBox.Show(
                    "Secrets require a history password. Open Preferences, set a history password, and enable Remember history password on this computer if you want secrets available after restart.",
                    "Clipman Secrets",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Warning);
                return;
            }
            if (!ConfirmSecretsPassword(password))
            {
                return;
            }

            if (secretStore == null)
            {
                secretStore = new SecretStore(DefaultSecretsDatabasePath(), CurrentDatabasePassword);
            }

            if (secretsForm == null || secretsForm.IsDisposed)
            {
                secretsForm = new SecretsForm(secretStore, RegisterHotkeys, QuickPasteSecret);
                secretsForm.FormClosed += (s, e) => secretsForm = null;
            }
            if (historyForm != null && !historyForm.IsDisposed && historyForm.Visible)
            {
                secretsForm.ShowDialog(historyForm);
            }
            else
            {
                secretsForm.ShowDialog();
            }
            secretsForm = null;
        }

        private bool ConfirmSecretsPassword(string currentPassword)
        {
            var entered = PasswordPromptForm.Ask(
                "Unlock Clipman Secrets",
                "Enter the current history password to open Secrets.");
            if (entered.Length == 0) return false;
            if (string.Equals(entered, currentPassword, StringComparison.Ordinal)) return true;
            MessageBox.Show(
                "The history password did not match. Secrets were not opened.",
                "Clipman Secrets",
                MessageBoxButtons.OK,
                MessageBoxIcon.Warning);
            return false;
        }

        public void CopyEntryToClipboard(ClipEntry entry)
        {
            if (entry == null) return;
            SetClipboardIgnoringNotification(() => SetEntryClipboard(entry));
            store.MarkUsed(entry.Id);
            sounds.Copy(settings.SoundsEnabled);
        }

        private void QueueAutomaticWebsiteTitle(ClipEntry entry, bool deliberate)
        {
            if (deliberate || !settings.AutoNameCopiedWebsiteLinks || entry == null ||
                !string.IsNullOrWhiteSpace(entry.Name) || string.IsNullOrWhiteSpace(entry.Id)) return;
            Uri uri;
            string reason;
            if (!LinkPresentation.TryGetUri(entry, out uri) || !LinkTitleFetcher.CanOffer(uri, out reason)) return;

            var startWorker = false;
            lock (automaticWebsiteTitleLock)
            {
                if (automaticWebsiteTitleEntryIds.Contains(entry.Id) || automaticWebsiteTitleQueue.Count >= 8) return;
                automaticWebsiteTitleEntryIds.Add(entry.Id);
                automaticWebsiteTitleQueue.Enqueue(new AutomaticWebsiteTitleRequest
                {
                    EntryId = entry.Id,
                    OriginalText = entry.Text ?? string.Empty,
                    Uri = uri
                });
                if (!automaticWebsiteTitleWorkerRunning)
                {
                    automaticWebsiteTitleWorkerRunning = true;
                    startWorker = true;
                }
            }
            if (startWorker) ThreadPool.QueueUserWorkItem(_ => ProcessAutomaticWebsiteTitles());
        }

        private void ProcessAutomaticWebsiteTitles()
        {
            while (true)
            {
                AutomaticWebsiteTitleRequest request;
                lock (automaticWebsiteTitleLock)
                {
                    if (automaticWebsiteTitleQueue.Count == 0)
                    {
                        automaticWebsiteTitleWorkerRunning = false;
                        return;
                    }
                    request = automaticWebsiteTitleQueue.Dequeue();
                }

                if (!settings.AutoNameCopiedWebsiteLinks)
                {
                    lock (automaticWebsiteTitleLock) automaticWebsiteTitleEntryIds.Remove(request.EntryId);
                    continue;
                }

                var result = LinkTitleFetcher.Fetch(request.Uri);
                BeginInvokeIfReady(() => CompleteAutomaticWebsiteTitle(request, result));
            }
        }

        private void CompleteAutomaticWebsiteTitle(AutomaticWebsiteTitleRequest request, LinkTitleFetchResult result)
        {
            lock (automaticWebsiteTitleLock) automaticWebsiteTitleEntryIds.Remove(request.EntryId);
            if (!settings.AutoNameCopiedWebsiteLinks) return;
            if (!result.Success)
            {
                websiteTitleDiagnostics.RecordFailure(result.Error);
                return;
            }
            if (!store.TrySetNameIfUnchanged(request.EntryId, request.OriginalText, result.Title))
            {
                websiteTitleDiagnostics.RecordStaleResult();
            }
        }

        public void CopyEntriesToClipboard(List<ClipEntry> entries)
        {
            if (entries == null || entries.Count == 0) return;
            var data = new DataObject();
            data.SetText(
                string.Join(
                    MultipleEntrySeparator.Resolve(settings.MultipleEntrySeparatorMode, settings.MultipleEntryCustomSeparator),
                    entries.Select(ResolvedEntryText)),
                TextDataFormat.UnicodeText);
            data.SetData(ClipmanClipboardData.EntriesFormat, ClipmanClipboardData.SerializeEntries(entries));
            SetClipboardIgnoringNotification(() => Clipboard.SetDataObject(data, true));
            store.MarkUsedMany(entries.Select(entry => entry.Id));
            sounds.Copy(settings.SoundsEnabled);
        }

        private bool CopyPlainTextToClipboard(string text, List<ClipEntry> entries)
        {
            try
            {
                SetClipboardIgnoringNotification(() => Clipboard.SetText(text ?? string.Empty, TextDataFormat.UnicodeText));
                store.MarkUsedMany((entries ?? new List<ClipEntry>()).Where(entry => entry != null).Select(entry => entry.Id));
                sounds.Copy(settings.SoundsEnabled);
                return true;
            }
            catch (Exception ex)
            {
                sounds.Skip(settings.SoundsEnabled);
                Program.WriteRuntimeLog("Clipman could not copy plain text to the clipboard.", ex);
                return false;
            }
        }

        private void CopySensitiveTextToClipboard(string text)
        {
            SetClipboardIgnoringNotification(() => Clipboard.SetText(text ?? string.Empty, TextDataFormat.UnicodeText));
        }

        internal void HandleHotkey(int id)
        {
            if (id == ShowHotkeyId)
            {
                ToggleHistoryWindow();
            }
            else if (id == ToggleHotkeyId || id == ToggleHotkeyAlternateId)
            {
                ToggleActive();
            }
            else if (id == SaveCurrentClipboardHotkeyId)
            {
                SaveCurrentClipboardToHistory();
            }
            else if (id == QuickClipHotkeyId)
            {
                ShowQuickClip();
            }
            else if (quickCopyHotkeyEntryIds.ContainsKey(id))
            {
                QuickPasteEntry(quickCopyHotkeyEntryIds[id]);
            }
            else if (secretHotkeyEntryIds.ContainsKey(id))
            {
                QuickPasteSecret(secretStore.GetEntryById(secretHotkeyEntryIds[id]));
            }
        }

        private void SaveCurrentClipboardToHistory()
        {
            HandleClipboardUpdate(true);
        }

        private void TryCaptureClipboardOnStartup()
        {
            try
            {
                HandleClipboardUpdate(startupCapture: true);
            }
            catch (ExternalException ex)
            {
                if ((uint)ex.ErrorCode != 0x800401D0U) throw;
                Program.WriteRuntimeLog("The clipboard was busy during startup; Clipman will retry the initial capture.", ex);
                clipboardFloodRecoveryTimer.Stop();
                clipboardFloodRecoveryTimer.Start();
            }
        }

        internal void HandleClipboardUpdate(bool deliberate = false, uint clipboardSequence = 0, bool recovery = false, bool startupCapture = false, long? observedMilliseconds = null)
        {
            var captureMilliseconds = observedMilliseconds ?? ClipboardFloodGuard.MonotonicMilliseconds();
            if (!deliberate && clipboardSequence == 0)
            {
                clipboardSequence = NativeMethods.GetClipboardSequenceNumber();
            }
            if (!deliberate && !clipboardNotifications.ShouldProcess(clipboardSequence, recovery))
            {
                return;
            }
            var sourceProcessName = ClipboardOwnerProcessName();
            if (string.IsNullOrWhiteSpace(sourceProcessName))
            {
                sourceProcessName = ForegroundProcessName();
            }

            if (!deliberate && IsClipmanProcess(sourceProcessName))
            {
                return;
            }

            if (!deliberate)
            {
                var floodDecision = clipboardFloodGuards.Observe(
                    NormalizeProcessName(sourceProcessName),
                    ClipboardFloodGuard.MonotonicMilliseconds());
                if (floodDecision != ClipboardFloodDecision.Allow)
                {
                    clipboardFloodRecoveryTimer.Stop();
                    clipboardFloodRecoveryTimer.Start();
                    return;
                }
                clipboardFloodRecoveryTimer.Stop();
            }

            if (!deliberate && !settings.Active)
            {
                clipMergeDetector.Reset();
                sounds.Skip(settings.SoundsEnabled);
                return;
            }

            if (!deliberate && IsIgnoredProcess(sourceProcessName))
            {
                clipMergeDetector.Reset();
                sounds.Skip(settings.SoundsEnabled);
                return;
            }

            var privacySignal = ClipboardPrivacySignals.Detect();
            if (privacySignal != null)
            {
                clipMergeDetector.Reset();
                lastClipboardPrivacySignal = privacySignal.Reason;
                sounds.Exclude(settings.SoundsEnabled);
                return;
            }

            lastClipboardPrivacySignal = "None";

            if (settings.RichTextHistoryEnabled && settings.IncludeImagesInRichText &&
                !Clipboard.ContainsText(TextDataFormat.UnicodeText) && RichImageData.ClipboardHasStandaloneImage())
            {
                clipMergeDetector.Reset();
                var imageCapture = RichImageData.CaptureFromClipboard(FriendlyProcessName(sourceProcessName));
                if (imageCapture == null)
                {
                    sounds.Skip(settings.SoundsEnabled);
                    return;
                }
                if (store.EmbeddedImageByteCount() + imageCapture.ImageBytes > RichImageData.MaximumDatabaseImageBytes)
                {
                    Program.WriteRuntimeLog("Clipman did not store a clipboard image because the 8 MiB history image budget is full.", null);
                    sounds.Skip(settings.SoundsEnabled);
                    return;
                }
                var imageGroup = settings.AutoGroupByApp ? FriendlyProcessName(sourceProcessName) : string.Empty;
                store.AddText(imageCapture.Text, DuplicateModeForCapture(startupCapture, settings.DuplicateMode), settings.MaxHistoryEntries, settings.MaxHistoryDays, imageGroup, imageCapture.RichText);
                if (IsStorageUnavailable())
                {
                    sounds.Skip(settings.SoundsEnabled);
                    UpdateTray();
                    return;
                }
                RememberReceivedHistoryTab(HistoryTabs.RichText);
                sounds.Copy(settings.SoundsEnabled);
                return;
            }

            var hasUnicodeText = Clipboard.ContainsText(TextDataFormat.UnicodeText);
            string urlOnlyText = null;
            if (!hasUnicodeText)
            {
                try { urlOnlyText = ClipboardUrlData.TryRead(Clipboard.GetDataObject()); }
                catch { }
            }

            ClipboardEventSummary fileSummary = null;
            try
            {
                fileSummary = ReadClipboardEventSummary(sourceProcessName);
            }
            catch
            {
            }
            if (fileSummary != null && urlOnlyText == null)
            {
                if (startupCapture && fileEventStore.Contains(fileSummary))
                {
                    return;
                }
                HandleCapturedFileEvent(fileSummary, sourceProcessName, deliberate, clipboardSequence, captureMilliseconds);
                if (settings.RichTextHistoryEnabled && settings.IncludeImagesInRichText && settings.AutoAddImageFilesToRichText)
                {
                    string imageFilePath;
                    string ignoredFailureMessage;
                    if (RichImageData.TryGetSingleClipboardImageFile(out imageFilePath, out ignoredFailureMessage))
                    {
                        AddCopiedImageFileToRichTextAsync(imageFilePath, sourceProcessName);
                    }
                }
                return;
            }

            if (!hasUnicodeText && urlOnlyText == null)
            {
                clipMergeDetector.Reset();
                sounds.Skip(settings.SoundsEnabled);
                return;
            }

            var text = urlOnlyText;
            try
            {
                if (hasUnicodeText) text = Clipboard.GetText(TextDataFormat.UnicodeText);
                if (string.IsNullOrEmpty(text))
                {
                    text = ClipboardUrlData.TryRead(Clipboard.GetDataObject());
                }
            }
            catch
            {
                return;
            }

            if (string.IsNullOrEmpty(text))
            {
                clipMergeDetector.Reset();
                if (deliberate)
                {
                    sounds.Skip(settings.SoundsEnabled);
                }
                return;
            }

            var richText = settings.RichTextHistoryEnabled ? RichTextData.CaptureFromClipboard() : null;
            var rawText = text;

            if (!deliberate && store.HasRecentlyTouchedRemoteText(text, CurrentDeviceName(), 90000))
            {
                return;
            }

            var sensitiveMatch = deliberate ? null : SensitiveDataExclusion.FindMatch(text, settings);
            if (sensitiveMatch != null)
            {
                clipMergeDetector.Reset();
                sounds.Exclude(settings.SoundsEnabled);
                return;
            }

            if (!deliberate && settings.AutoRemoveUrlTracking)
            {
                var cleaned = UrlTrackingCleaner.CleanText(text);
                if (!string.Equals(cleaned, text, StringComparison.Ordinal))
                {
                    text = cleaned;
                    richText = null;
                    try
                    {
                        SetClipboardIgnoringNotification(() => Clipboard.SetText(text, TextDataFormat.UnicodeText));
                    }
                    catch
                    {
                    }
                }
            }

            var group = settings.AutoGroupByApp ? FriendlyProcessName(sourceProcessName) : string.Empty;
            var textObservation = new ClipMergeObservation
            {
                Kind = ClipMergeKind.Text,
                Signature = rawText,
                SourceApplication = NormalizeProcessName(sourceProcessName),
                ChangeIdentifier = clipboardSequence,
                Payload = text
            };
            var mergeDecision = clipMergeDetector.Observe(
                textObservation,
                captureMilliseconds,
                settings.ClipMergeEnabled,
                settings.ClipMergeWindowMilliseconds,
                deliberate);
            if (mergeDecision.SuppressDuplicate)
            {
                return;
            }
            ClipEntry storedEntry;
            if (mergeDecision.ShouldMerge)
            {
                var baseText = Convert.ToString(mergeDecision.Base.Payload) ?? string.Empty;
                var mergedText = baseText + ClipMergeDetector.ResolveSeparator(settings.ClipMergeSeparatorMode, settings.ClipMergeCustomSeparator) + text;
                storedEntry = store.MergeCapturedText(
                    mergeDecision.Base.HistoryId,
                    mergeDecision.FirstTap.HistoryId,
                    mergedText,
                    settings.MaxHistoryEntries,
                    settings.MaxHistoryDays,
                    group);
                if (storedEntry != null)
                {
                    try
                    {
                        SetClipboardIgnoringNotification(() => Clipboard.SetText(mergedText, TextDataFormat.UnicodeText));
                    }
                    catch
                    {
                    }
                    textObservation.Signature = mergedText;
                    textObservation.Payload = mergedText;
                    clipMergeDetector.CompleteMerge(textObservation, storedEntry.Id);
                    RememberReceivedHistoryTab(LinkClassifier.IsLinkOnlyText(mergedText) ? HistoryTabs.Links : HistoryTabs.Text);
                    sounds.Merge(settings.SoundsEnabled);
                    return;
                }
            }

            storedEntry = store.AddText(text, DuplicateModeForCapture(startupCapture, settings.DuplicateMode), settings.MaxHistoryEntries, settings.MaxHistoryDays, group, richText);
            clipMergeDetector.SetCurrentHistoryId(storedEntry == null ? string.Empty : storedEntry.Id);
            if (IsStorageUnavailable())
            {
                sounds.Skip(settings.SoundsEnabled);
                UpdateTray();
                return;
            }

            RememberReceivedHistoryTab(richText != null ? HistoryTabs.RichText : LinkClassifier.IsLinkOnlyText(text) ? HistoryTabs.Links : HistoryTabs.Text);

            sounds.Copy(settings.SoundsEnabled);
            QueueAutomaticWebsiteTitle(storedEntry, deliberate);
        }

        internal static string DuplicateModeForCapture(bool startupCapture, string configuredMode)
        {
            return startupCapture ? "Ignore" : configuredMode;
        }

        private void AddCopiedImageFileToRichTextAsync(string path, string sourceProcessName)
        {
            ThreadPool.QueueUserWorkItem(_ =>
            {
                var capture = RichImageData.CaptureFromFile(path);
                if (capture == null)
                {
                    Program.WriteRuntimeLog("Clipman could not add the copied image file to Rich Text history because it was unavailable or invalid.", null);
                    return;
                }
                BeginInvokeIfReady(() =>
                {
                    if (!settings.RichTextHistoryEnabled || !settings.IncludeImagesInRichText || !settings.AutoAddImageFilesToRichText) return;
                    if (store.EmbeddedImageByteCount() + capture.ImageBytes > RichImageData.MaximumDatabaseImageBytes)
                    {
                        Program.WriteRuntimeLog("Clipman did not add a copied image file to Rich Text history because the 8 MiB history image budget is full.", null);
                        return;
                    }
                    var group = settings.AutoGroupByApp ? FriendlyProcessName(sourceProcessName) : string.Empty;
                    store.AddText(capture.Text, settings.DuplicateMode, settings.MaxHistoryEntries, settings.MaxHistoryDays, group, capture.RichText);
                    if (IsStorageUnavailable()) UpdateTray();
                });
            });
        }

        private void ClipboardFloodRecoveryTimerTick(object sender, EventArgs e)
        {
            clipboardFloodRecoveryTimer.Stop();
            try
            {
                HandleClipboardUpdate(false, NativeMethods.GetClipboardSequenceNumber(), true);
            }
            catch (ExternalException ex)
            {
                if ((uint)ex.ErrorCode != 0x800401D0U) throw;
                clipboardFloodRecoveryTimer.Start();
            }
        }

        private void RememberReceivedHistoryTab(string tabId)
        {
            if (!settings.DynamicHistoryMode) return;
            lastReceivedHistoryTab = tabId;
            receivedHistoryTabPending = true;
        }

        private void HandleCapturedFileEvent(ClipboardEventSummary summary, string sourceProcessName, bool deliberate, uint clipboardSequence, long observedMilliseconds)
        {
            if (summary == null || summary.Files == null || summary.Files.Count == 0) return;
            if (string.IsNullOrWhiteSpace(summary.Operation)) summary.Operation = "Copy";
            var observation = new ClipMergeObservation
            {
                Kind = ClipMergeKind.Files,
                Signature = FileMergeSignature(summary.Files, summary.Operation),
                SourceApplication = NormalizeProcessName(sourceProcessName),
                Operation = summary.Operation,
                ChangeIdentifier = clipboardSequence,
                Payload = CloneClipboardEvent(summary)
            };
            var decision = clipMergeDetector.Observe(
                observation,
                observedMilliseconds,
                settings.ClipMergeEnabled,
                settings.ClipMergeWindowMilliseconds,
                deliberate);

            if (decision.SuppressDuplicate)
            {
                return;
            }

            if (decision.ShouldMerge)
            {
                var baseSummary = decision.Base.Payload as ClipboardEventSummary;
                if (baseSummary != null)
                {
                    if (string.Equals(summary.Operation, "Move", StringComparison.OrdinalIgnoreCase) &&
                        (!ClipMergeFilePolicy.AreSourcesAvailable(baseSummary.Files) ||
                         !ClipMergeFilePolicy.AreSourcesAvailable(summary.Files)))
                    {
                        clipMergeDetector.RetainFirstTap(decision.FirstTap);
                        return;
                    }
                    var mergedFiles = baseSummary.Files
                        .Concat(summary.Files)
                        .Where(path => !string.IsNullOrWhiteSpace(path))
                        .Select(path => path.Trim())
                        .Distinct(StringComparer.OrdinalIgnoreCase)
                        .ToList();
                    var merged = new ClipboardEventSummary
                    {
                        CapturedAt = DateTime.Now,
                        Source = summary.Source,
                        Operation = summary.Operation,
                        SourceMachine = CurrentDeviceName(),
                        ContainsText = true,
                        FileCount = mergedFiles.Count,
                        Files = mergedFiles,
                        Formats = baseSummary.Formats.Concat(summary.Formats).Distinct(StringComparer.OrdinalIgnoreCase).ToList()
                    };
                    var saved = fileEventStore.MergeCapturedEvents(decision.Base.HistoryId, decision.FirstTap.HistoryId, merged);
                    if (saved != null && string.IsNullOrWhiteSpace(fileEventStore.LastStorageError))
                    {
                        WriteMergedFilesToClipboard(mergedFiles, merged.Operation);
                        observation.Signature = FileMergeSignature(mergedFiles, merged.Operation);
                        observation.Payload = CloneClipboardEvent(merged);
                        clipMergeDetector.CompleteMerge(observation, saved.Id);
                        RememberReceivedHistoryTab(HistoryTabs.Files);
                        sounds.Merge(settings.SoundsEnabled);
                        return;
                    }
                }
            }

            var stored = fileEventStore.Add(summary);
            clipMergeDetector.SetCurrentHistoryId(stored == null ? string.Empty : stored.Id);
            if (settings.AutoRemoveUnavailableFileHistoryEvents)
            {
                fileEventStore.RemoveUnavailableEvents();
            }
            if (string.IsNullOrWhiteSpace(fileEventStore.LastStorageError))
            {
                RememberReceivedHistoryTab(HistoryTabs.Files);
                sounds.Copy(settings.SoundsEnabled);
            }
            else
            {
                sounds.Skip(settings.SoundsEnabled);
            }
        }

        private void WriteMergedFilesToClipboard(IEnumerable<string> paths, string operation)
        {
            var files = (paths ?? Enumerable.Empty<string>()).Where(path => !string.IsNullOrWhiteSpace(path)).ToList();
            if (files.Count == 0) return;
            var fileList = new StringCollection();
            fileList.AddRange(files.ToArray());
            var data = new DataObject();
            data.SetFileDropList(fileList);
            data.SetText(string.Join(Environment.NewLine, files), TextDataFormat.UnicodeText);
            var effect = string.Equals(operation, "Move", StringComparison.OrdinalIgnoreCase) ? 2 : 1;
            data.SetData("Preferred DropEffect", false, new MemoryStream(BitConverter.GetBytes(effect)));
            try
            {
                SetClipboardIgnoringNotification(() => Clipboard.SetDataObject(data, true));
            }
            catch
            {
            }
        }

        private static string FileMergeSignature(IEnumerable<string> paths, string operation)
        {
            return (operation ?? string.Empty).Trim().ToUpperInvariant() + "\n" + string.Join("\n",
                (paths ?? Enumerable.Empty<string>())
                    .Where(path => !string.IsNullOrWhiteSpace(path))
                    .Select(path => path.Trim().ToUpperInvariant())
                    .OrderBy(path => path, StringComparer.Ordinal));
        }

        private List<ClipboardEventSummary> GetRecentClipboardEvents()
        {
            return fileEventStore.GetEvents(settings.FileHistorySortMode, settings.FileHistorySortDescending);
        }

        private int DeleteRecentClipboardEvents(List<string> ids)
        {
            return fileEventStore.DeleteMany(ids);
        }

        private int ClearRecentClipboardEvents()
        {
            return fileEventStore.Clear();
        }

        private int RemoveUnavailableRecentClipboardEvents()
        {
            return fileEventStore.RemoveUnavailableEvents();
        }

        private bool ToggleRecentClipboardEventPinned(string id)
        {
            return fileEventStore.TogglePinned(id);
        }

        private void MoveRecentClipboardEvents(List<string> ids, int direction)
        {
            fileEventStore.MoveEvents(ids, direction);
        }

        private bool ClearTextHistory()
        {
            var password = CurrentDatabasePassword();
            if (!string.IsNullOrEmpty(password))
            {
                var entered = PasswordPromptForm.Ask(
                    "Clear Clipman history",
                    "Enter the history password to clear all saved text clipboard entries.");
                if (!string.Equals(entered, password, StringComparison.Ordinal))
                {
                    MessageBox.Show("The history password did not match. Clipboard history was not cleared.", "Clipman", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                    return false;
                }
            }

            var message =
                "Clear all saved text clipboard history?" +
                Environment.NewLine +
                Environment.NewLine +
                "This will remove every text entry from the current Clipman database. A timestamped backup will be created first.";
            if (MessageBox.Show(message, "Clear Clipman history", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) != DialogResult.Yes)
            {
                return false;
            }

            BackupDatabase(settings.DatabasePath, "before-clear-history");
            store.ReplaceAll(new List<ClipEntry>());
            return true;
        }

        private static void BackupDatabase(string databasePath, string reason)
        {
            if (string.IsNullOrWhiteSpace(databasePath) || !File.Exists(databasePath)) return;
            var directory = Path.GetDirectoryName(databasePath);
            var name = Path.GetFileNameWithoutExtension(databasePath);
            var extension = Path.GetExtension(databasePath);
            if (string.IsNullOrEmpty(directory)) directory = AppDomain.CurrentDomain.BaseDirectory;
            if (string.IsNullOrEmpty(name)) name = "clipman-history";
            if (string.IsNullOrEmpty(extension)) extension = ".clipdb";
            var safeReason = string.IsNullOrWhiteSpace(reason) ? "backup" : reason.Trim();
            var stamp = DateTime.Now.ToString("yyyyMMdd-HHmmss");
            var backupPath = Path.Combine(directory, name + "." + safeReason + "-" + stamp + extension);
            File.Copy(databasePath, backupPath, false);
        }

        private static ClipboardEventSummary CloneClipboardEvent(ClipboardEventSummary source)
        {
            if (source == null) return null;
            return new ClipboardEventSummary
            {
                Id = source.Id,
                CapturedAt = source.CapturedAt,
                Source = source.Source ?? string.Empty,
                Operation = source.Operation ?? string.Empty,
                SourceMachine = source.SourceMachine ?? string.Empty,
                ContainsText = source.ContainsText,
                FileCount = source.FileCount,
                Files = source.Files == null ? new List<string>() : source.Files.ToList(),
                Formats = source.Formats == null ? new List<string>() : source.Formats.ToList(),
                Pinned = source.Pinned,
                ManualOrder = source.ManualOrder
            };
        }

        private ClipboardEventSummary ReadClipboardEventSummary(string sourceProcessName)
        {
            var data = Clipboard.GetDataObject();
            if (data == null) return null;
            var formats = data.GetFormats(false) ?? new string[0];
            var hasText = data.GetDataPresent(DataFormats.UnicodeText, false) || data.GetDataPresent(DataFormats.Text, false);
            var hasFiles = data.GetDataPresent(DataFormats.FileDrop, false);
            if (!hasFiles && hasText)
            {
                return null;
            }

            var summary = new ClipboardEventSummary
            {
                CapturedAt = DateTime.Now,
                Source = FriendlyProcessName(sourceProcessName),
                SourceMachine = CurrentDeviceName(),
                ContainsText = hasText,
                Formats = formats.ToList(),
                Operation = ClipboardDropEffect(data)
            };

            if (hasFiles)
            {
                var files = data.GetData(DataFormats.FileDrop, false) as string[];
                if (files != null)
                {
                    summary.Files = files.ToList();
                    summary.FileCount = files.Length;
                }
            }

            return summary;
        }

        private static string ClipboardDropEffect(IDataObject data)
        {
            try
            {
                if (!data.GetDataPresent("Preferred DropEffect", false)) return string.Empty;
                var stream = data.GetData("Preferred DropEffect", false) as MemoryStream;
                if (stream == null || stream.Length < 4) return string.Empty;
                var bytes = stream.ToArray();
                var value = BitConverter.ToInt32(bytes, 0);
                return DescribeDropEffect(value);
            }
            catch
            {
                return string.Empty;
            }
        }

        internal static string DescribeDropEffect(int value)
        {
            var parts = new List<string>();
            if ((value & 1) == 1) parts.Add("Copy");
            if ((value & 2) == 2) parts.Add("Move");
            if ((value & 4) == 4) parts.Add("Link");
            var knownBits = value & 7;
            if (parts.Count > 0 && knownBits == value)
            {
                return string.Join(" or ", parts);
            }

            switch (value)
            {
                case 0: return string.Empty;
                default: return "Unknown operation " + value;
            }
        }

        private ContextMenuStrip BuildTrayMenu()
        {
            var menu = new ContextMenuStrip();
            var storageMessage = StorageUnavailableMessage();
            if (!string.IsNullOrWhiteSpace(storageMessage))
            {
                var unavailable = new ToolStripMenuItem("Storage unavailable")
                {
                    Enabled = false,
                    ToolTipText = storageMessage
                };
                menu.Items.Add(unavailable);
                menu.Items.Add("&Retry storage", null, (s, e) => RetryStorage());
                menu.Items.Add("-");
            }

            menu.Items.Add("&Show or hide history\t" + settings.ShowHistoryHotkey, null, (s, e) => ToggleHistoryWindow());
            menu.Items.Add((settings.Active ? "Turn &off" : "Turn &on") + "\t" + settings.ToggleActiveHotkey, null, (s, e) => ToggleActive());
            var saveClipboardText = "Save current &clipboard to history";
            if (!string.IsNullOrWhiteSpace(settings.SaveCurrentClipboardHotkey))
            {
                saveClipboardText += "\t" + settings.SaveCurrentClipboardHotkey;
            }
            menu.Items.Add(saveClipboardText, null, (s, e) => SaveCurrentClipboardToHistory());
            var quickClipText = "New &Quick Clip";
            if (!string.IsNullOrWhiteSpace(settings.QuickClipHotkey))
            {
                quickClipText += "\t" + settings.QuickClipHotkey;
            }
            menu.Items.Add(quickClipText, null, (s, e) => ShowQuickClip());
            menu.Items.Add("&Secrets...\tCtrl+Shift+E", null, (s, e) => ShowSecrets());
            menu.Items.Add("&Preferences...", null, (s, e) => ShowPreferencesFromTray());
            menu.Items.Add("Open &settings folder\tCtrl+Shift+O", null, (s, e) => OpenSettingsFolder());
            menu.Items.Add("-");
            menu.Items.Add("E&xit", null, (s, e) => ExitThread());
            return menu;
        }

        private void OpenSettingsFolder()
        {
            try
            {
                Directory.CreateDirectory(settingsStore.SettingsDirectory);
                Process.Start(new ProcessStartInfo
                {
                    FileName = settingsStore.SettingsDirectory,
                    UseShellExecute = true
                });
            }
            catch (Exception ex)
            {
                MessageBox.Show("Clipman could not open the settings folder.\r\n\r\n" + ex.Message, "Clipman", MessageBoxButtons.OK, MessageBoxIcon.Warning);
            }
        }

        private void RetryStorage()
        {
            if (Interlocked.CompareExchange(ref storageRetryInProgress, 1, 0) != 0) return;

            ThreadPool.QueueUserWorkItem(delegate
            {
                var result = StorageRetryOperation.Execute(store.Reload, fileEventStore.Reload);
                if (result.TextHistoryError != null)
                {
                    store.RecordServerRetryFailure(result.TextHistoryError);
                }
                if (!result.Succeeded) Program.WriteRuntimeLog("The explicit storage retry failed.", result.Error);

                Interlocked.Exchange(ref storageRetryInProgress, 0);
                try
                {
                    if (invoker != null && invoker.IsHandleCreated)
                    {
                        invoker.BeginInvoke(new Action(delegate
                        {
                            UpdateTray();
                            if (!result.Succeeded)
                            {
                                MessageBox.Show(
                                    "Clipman could not reconnect to storage. It will keep using the local cache and retry automatically.\r\n\r\n" + result.Error.Message,
                                    "Clipman storage unavailable",
                                    MessageBoxButtons.OK,
                                    MessageBoxIcon.Warning);
                            }
                        }));
                    }
                }
                catch (InvalidOperationException)
                {
                }
            });
        }

        private void RegisterHotkeys()
        {
            NativeMethods.UnregisterHotKey(messageWindow.Handle, ShowHotkeyId);
            NativeMethods.UnregisterHotKey(messageWindow.Handle, ToggleHotkeyId);
            NativeMethods.UnregisterHotKey(messageWindow.Handle, SaveCurrentClipboardHotkeyId);
            NativeMethods.UnregisterHotKey(messageWindow.Handle, ToggleHotkeyAlternateId);
            NativeMethods.UnregisterHotKey(messageWindow.Handle, QuickClipHotkeyId);
            foreach (var hotkeyId in quickCopyHotkeyEntryIds.Keys.ToList())
            {
                NativeMethods.UnregisterHotKey(messageWindow.Handle, hotkeyId);
            }
            foreach (var hotkeyId in secretHotkeyEntryIds.Keys.ToList())
            {
                NativeMethods.UnregisterHotKey(messageWindow.Handle, hotkeyId);
            }
            quickCopyHotkeyEntryIds.Clear();
            secretHotkeyEntryIds.Clear();
            showHotkeyRegistered = false;
            toggleHotkeyRegistered = false;
            saveCurrentClipboardHotkeyRegistered = false;
            quickClipHotkeyRegistered = false;
            toggleAlternateHotkeyRegistered = false;
            quickCopyHotkeysRegistered = 0;
            secretHotkeysRegistered = 0;
            PruneInvalidQuickPasteBindings();

            HotkeyDefinition show;
            if (HotkeyDefinition.TryParse(settings.ShowHistoryHotkey, out show))
            {
                showHotkeyRegistered = NativeMethods.RegisterHotKey(messageWindow.Handle, ShowHotkeyId, GlobalHotkeyModifiers(show.Modifiers), show.Key);
            }

            HotkeyDefinition toggle;
            if (HotkeyDefinition.TryParse(settings.ToggleActiveHotkey, out toggle))
            {
                toggleHotkeyRegistered = NativeMethods.RegisterHotKey(messageWindow.Handle, ToggleHotkeyId, GlobalHotkeyModifiers(toggle.Modifiers), toggle.Key);
                if (settings.ToggleActiveHotkey.Trim().Equals("Ctrl+Alt+`", StringComparison.OrdinalIgnoreCase))
                {
                    toggleAlternateHotkeyRegistered = NativeMethods.RegisterHotKey(messageWindow.Handle, ToggleHotkeyAlternateId, GlobalHotkeyModifiers(toggle.Modifiers), Keys.Oem8);
                }
            }

            HotkeyDefinition saveCurrentClipboard;
            if (HotkeyDefinition.TryParse(settings.SaveCurrentClipboardHotkey, out saveCurrentClipboard))
            {
                saveCurrentClipboardHotkeyRegistered = NativeMethods.RegisterHotKey(messageWindow.Handle, SaveCurrentClipboardHotkeyId, GlobalHotkeyModifiers(saveCurrentClipboard.Modifiers), saveCurrentClipboard.Key);
            }

            HotkeyDefinition quickClip;
            if (HotkeyDefinition.TryParse(settings.QuickClipHotkey, out quickClip))
            {
                quickClipHotkeyRegistered = NativeMethods.RegisterHotKey(messageWindow.Handle, QuickClipHotkeyId, GlobalHotkeyModifiers(quickClip.Modifiers), quickClip.Key);
            }

            var quickCopyId = QuickCopyHotkeyBaseId;
            var usedHotkeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            usedHotkeys.Add((settings.ShowHistoryHotkey ?? string.Empty).Trim());
            usedHotkeys.Add((settings.ToggleActiveHotkey ?? string.Empty).Trim());
            if (!string.IsNullOrWhiteSpace(settings.SaveCurrentClipboardHotkey))
            {
                usedHotkeys.Add(settings.SaveCurrentClipboardHotkey.Trim());
            }
            if (!string.IsNullOrWhiteSpace(settings.QuickClipHotkey))
            {
                usedHotkeys.Add(settings.QuickClipHotkey.Trim());
            }
            foreach (var binding in (settings.QuickCopyHotkeys ?? new List<QuickCopyBinding>())
                .Where(b => b != null && !string.IsNullOrWhiteSpace(b.EntryId) && !string.IsNullOrWhiteSpace(b.Hotkey))
                .OrderBy(b => b.EntryId, StringComparer.OrdinalIgnoreCase))
            {
                var normalizedHotkey = binding.Hotkey.Trim();
                if (!usedHotkeys.Add(normalizedHotkey)) continue;

                HotkeyDefinition quickCopy;
                if (!HotkeyDefinition.TryParse(normalizedHotkey, out quickCopy)) continue;

                while (quickCopyHotkeyEntryIds.ContainsKey(quickCopyId))
                {
                    quickCopyId++;
                }

                if (NativeMethods.RegisterHotKey(messageWindow.Handle, quickCopyId, GlobalHotkeyModifiers(quickCopy.Modifiers), quickCopy.Key))
                {
                    quickCopyHotkeyEntryIds[quickCopyId] = binding.EntryId.Trim();
                    quickCopyHotkeysRegistered++;
                }
                quickCopyId++;
            }

            var secretId = SecretHotkeyBaseId;
            foreach (var secret in GetSecretEntriesSafe()
                .Where(s => s != null && !string.IsNullOrWhiteSpace(s.Id) && !string.IsNullOrWhiteSpace(s.Hotkey))
                .OrderBy(s => s.Id, StringComparer.OrdinalIgnoreCase))
            {
                var normalizedHotkey = secret.Hotkey.Trim();
                if (!usedHotkeys.Add(normalizedHotkey)) continue;

                HotkeyDefinition secretHotkey;
                if (!HotkeyDefinition.TryParse(normalizedHotkey, out secretHotkey)) continue;

                while (secretHotkeyEntryIds.ContainsKey(secretId))
                {
                    secretId++;
                }

                if (NativeMethods.RegisterHotKey(messageWindow.Handle, secretId, GlobalHotkeyModifiers(secretHotkey.Modifiers), secretHotkey.Key))
                {
                    secretHotkeyEntryIds[secretId] = secret.Id.Trim();
                    secretHotkeysRegistered++;
                }
                secretId++;
            }
        }

        private static NativeMethods.Modifiers GlobalHotkeyModifiers(NativeMethods.Modifiers modifiers)
        {
            return modifiers | NativeMethods.Modifiers.NoRepeat;
        }

        private void PruneInvalidQuickPasteBindings()
        {
            if (settings.QuickCopyHotkeys == null)
            {
                settings.QuickCopyHotkeys = new List<QuickCopyBinding>();
                return;
            }

            var cleaned = new List<QuickCopyBinding>();
            var usedEntryIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            var usedHotkeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            var changed = false;

            foreach (var binding in settings.QuickCopyHotkeys)
            {
                var entryId = binding == null ? string.Empty : (binding.EntryId ?? string.Empty).Trim();
                var hotkey = binding == null ? string.Empty : (binding.Hotkey ?? string.Empty).Trim();
                HotkeyDefinition parsed;
                var entry = string.IsNullOrWhiteSpace(entryId) ? null : store.GetEntryById(entryId);

                if (string.IsNullOrWhiteSpace(entryId) ||
                    string.IsNullOrWhiteSpace(hotkey) ||
                    !HotkeyDefinition.TryParse(hotkey, out parsed) ||
                    entry == null ||
                    string.IsNullOrEmpty(entry.Text) ||
                    !usedEntryIds.Add(entryId) ||
                    !usedHotkeys.Add(hotkey))
                {
                    changed = true;
                    continue;
                }

                if (!string.Equals(binding.EntryId, entryId, StringComparison.Ordinal) ||
                    !string.Equals(binding.Hotkey, hotkey, StringComparison.Ordinal))
                {
                    changed = true;
                }

                cleaned.Add(new QuickCopyBinding { EntryId = entryId, Hotkey = hotkey, Mode = QuickPasteModes.Normalize(binding.Mode) });
            }

            if (changed || cleaned.Count != settings.QuickCopyHotkeys.Count)
            {
                settings.QuickCopyHotkeys = cleaned;
                settingsStore.Save(settings);
            }
        }

        private void SaveSettings()
        {
            settingsStore.Save(settings);
        }

        private void PlayLaunchStateSound()
        {
            if (settings.Active)
            {
                sounds.On(settings.SoundsEnabled);
            }
            else
            {
                sounds.Off(settings.SoundsEnabled);
            }
        }

        private string ForegroundProcessName()
        {
            try
            {
                uint processId;
                var hwnd = NativeMethods.GetForegroundWindow();
                if (hwnd == IntPtr.Zero) return string.Empty;
                NativeMethods.GetWindowThreadProcessId(hwnd, out processId);
                if (processId == 0) return string.Empty;
                if ((int)processId == Process.GetCurrentProcess().Id) return "clipman";
                var process = Process.GetProcessById((int)processId);
                return NormalizeProcessName(process.ProcessName);
            }
            catch
            {
                return string.Empty;
            }
        }

        private string ClipboardOwnerProcessName()
        {
            try
            {
                uint processId;
                var hwnd = NativeMethods.GetClipboardOwner();
                if (hwnd == IntPtr.Zero) return string.Empty;
                NativeMethods.GetWindowThreadProcessId(hwnd, out processId);
                if (processId == 0) return string.Empty;
                if ((int)processId == Process.GetCurrentProcess().Id) return "clipman";
                var process = Process.GetProcessById((int)processId);
                return NormalizeProcessName(process.ProcessName);
            }
            catch
            {
                return string.Empty;
            }
        }

        private bool IsIgnoredProcess(string processName)
        {
            processName = NormalizeProcessName(processName);
            if (IsClipmanProcess(processName)) return true;
            if (settings.IgnoredProcesses == null || settings.IgnoredProcesses.Count == 0) return false;
            return settings.IgnoredProcesses.Any(p =>
                IgnoredProcessMatches(NormalizeProcessName(p), processName));
        }

        private static bool IgnoredProcessMatches(string ignoredProcessName, string processName)
        {
            if (string.IsNullOrWhiteSpace(ignoredProcessName) || string.IsNullOrWhiteSpace(processName)) return false;
            if (string.Equals(ignoredProcessName, processName, StringComparison.OrdinalIgnoreCase)) return true;
            return processName.StartsWith(ignoredProcessName + ".", StringComparison.OrdinalIgnoreCase) ||
                processName.StartsWith(ignoredProcessName + "-", StringComparison.OrdinalIgnoreCase) ||
                processName.StartsWith(ignoredProcessName + "_", StringComparison.OrdinalIgnoreCase) ||
                processName.StartsWith(ignoredProcessName + " ", StringComparison.OrdinalIgnoreCase);
        }

        private static bool IsClipmanProcess(string processName)
        {
            return string.Equals(NormalizeProcessName(processName), "clipman", StringComparison.OrdinalIgnoreCase);
        }

        private static string NormalizeProcessName(string processName)
        {
            if (string.IsNullOrWhiteSpace(processName)) return string.Empty;
            var trimmed = processName.Trim();
            return trimmed.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)
                ? Path.GetFileNameWithoutExtension(trimmed)
                : trimmed;
        }

        private static string FriendlyProcessName(string processName)
        {
            var normalized = NormalizeProcessName(processName);
            if (string.IsNullOrWhiteSpace(normalized)) return string.Empty;
            return normalized.Substring(0, 1).ToUpperInvariant() + (normalized.Length > 1 ? normalized.Substring(1) : string.Empty);
        }

        private string ClipboardFloodProtectionStatus(long nowMilliseconds)
        {
            var activeSources = clipboardFloodGuards.ActiveSources(nowMilliseconds)
                .Select(FriendlyProcessName)
                .Where(name => !string.IsNullOrWhiteSpace(name))
                .OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
                .ToList();
            return activeSources.Count == 0
                ? "ready"
                : "suppressing rapid updates from " + string.Join(", ", activeSources);
        }

        private static string FormatDiagnosticTime(long unixMs)
        {
            if (unixMs <= 0) return "Never";
            return TimeUtil.FromUnixMs(unixMs).ToString("yyyy-MM-dd HH:mm:ss");
        }

        private string BuildDiagnosticsText()
        {
            var diagnosticNowMilliseconds = ClipboardFloodGuard.MonotonicMilliseconds();
            var sharedExecutableSettingsDirectory = SharedExecutableSettingsDirectory();
            var sharedUpdateCoordinationEnabled = SharedUpdateStateStore.CanCoordinateExecutable(
                sharedExecutableSettingsDirectory,
                Application.ExecutablePath);
            var sharedState = sharedUpdateCoordinationEnabled
                ? SharedUpdateStateStore.Load(sharedExecutableSettingsDirectory)
                : new SharedUpdateState();
            var serverStatus = store.GetServerSyncStatus();
            var entries = store.GetEntries();
            var richEntries = entries.Where(e => RichTextData.Normalize(e.RichText) != null).ToList();
            var richHtmlEntries = richEntries.Count(e => !string.IsNullOrEmpty(e.RichText.HtmlFragment));
            var richRtfEntries = richEntries.Count(e => !string.IsNullOrEmpty(e.RichText.RtfBase64));
            long richBytes = richEntries.Sum(e => (long)Encoding.UTF8.GetByteCount(e.RichText.HtmlFragment ?? string.Empty) + RichTextData.DecodedRtfByteCount(e.RichText.RtfBase64));
            var ignored = settings.IgnoredProcesses == null || settings.IgnoredProcesses.Count == 0
                ? "None"
                : string.Join(", ", settings.IgnoredProcesses);
            return
                "Clipman diagnostics\r\n\r\n" +
                "Active: " + settings.Active + "\r\n" +
                "Sounds enabled: " + settings.SoundsEnabled + "\r\n" +
                "Storage mode: " + settings.StorageMode + "\r\n" +
                "Server host: " + (string.IsNullOrWhiteSpace(settings.ServerUrl) ? "not set" : ServerSettingsSanitizer.CleanTransportUrl(settings.ServerUrl)) + "\r\n" +
                "Server sync enabled: " + serverStatus.Enabled + "\r\n" +
                "Server sync configured: " + serverStatus.Configured + "\r\n" +
                "Server sync revision: " + (string.IsNullOrWhiteSpace(serverStatus.Revision) ? "None" : serverStatus.Revision) + "\r\n" +
                "Server sync last poll: " + FormatDiagnosticTime(serverStatus.LastPollUnixMs) + "\r\n" +
                "Server sync last success: " + FormatDiagnosticTime(serverStatus.LastSuccessUnixMs) + "\r\n" +
                "Server sync last upload: " + FormatDiagnosticTime(serverStatus.LastUploadUnixMs) + "\r\n" +
                "Server sync next retry: " + FormatDiagnosticTime(serverStatus.NextPollUnixMs) + "\r\n" +
                "Server sync consecutive failures: " + serverStatus.ConsecutiveFailures + "\r\n" +
                "Database path: " + settings.DatabasePath + "\r\n" +
                "Database path type: " + (settings.UseDefaultDatabasePath ? "Default beside Clipman" : "Explicit") + "\r\n" +
                "Database storage status: " + (string.IsNullOrWhiteSpace(store.LastStorageError) ? "OK" : "Unavailable: " + store.LastStorageError) + "\r\n" +
                "Entries: " + entries.Count + "\r\n" +
                "Rich text history: " + (settings.RichTextHistoryEnabled ? "enabled" : "disabled") + "\r\n" +
                "Rich text images: " + (settings.IncludeImagesInRichText ? "enabled" : "disabled") + "\r\n" +
                "Automatically add copied image files to Rich Text: " + (settings.AutoAddImageFilesToRichText ? "enabled" : "disabled") + "\r\n" +
                "Rich text payloads: " + richEntries.Count + " entries (HTML " + richHtmlEntries + ", RTF " + richRtfEntries + ", " + richBytes + " bytes)\r\n" +
                "File history path: " + fileEventStore.DatabasePath + "\r\n" +
                "File history storage status: " + (string.IsNullOrWhiteSpace(fileEventStore.LastStorageError) ? "OK" : "Unavailable: " + fileEventStore.LastStorageError) + "\r\n" +
                "File history events: " + fileEventStore.GetEvents().Count + "\r\n" +
                "File history diagnostics limit: " + settings.DiagnosticsFileHistoryLimit + "\r\n" +
                "Auto remove unavailable file history events: " + settings.AutoRemoveUnavailableFileHistoryEvents + "\r\n" +
                "Show history hotkey: " + settings.ShowHistoryHotkey + " (" + (showHotkeyRegistered ? "registered" : "not registered") + ")\r\n" +
                "Toggle hotkey: " + settings.ToggleActiveHotkey + " (" + (toggleHotkeyRegistered ? "registered" : "not registered") + ")\r\n" +
                "Save current clipboard hotkey: " + (string.IsNullOrWhiteSpace(settings.SaveCurrentClipboardHotkey) ? "Not assigned" : settings.SaveCurrentClipboardHotkey + " (" + (saveCurrentClipboardHotkeyRegistered ? "registered" : "not registered") + ")") + "\r\n" +
                "Quick Clip hotkey: " + (string.IsNullOrWhiteSpace(settings.QuickClipHotkey) ? "Not assigned" : settings.QuickClipHotkey + " (" + (quickClipHotkeyRegistered ? "registered" : "not registered") + ")") + "\r\n" +
                "Toggle alternate UK key: " + (toggleAlternateHotkeyRegistered ? "registered" : "not registered or not needed") + "\r\n" +
                "Quick Paste bindings: " + ((settings.QuickCopyHotkeys == null ? 0 : settings.QuickCopyHotkeys.Count) + " configured, " + quickCopyHotkeysRegistered + " registered") + "\r\n" +
                "Secrets: " + (GetSecretEntriesSafe().Count + " configured, " + secretHotkeysRegistered + " hotkeys registered") + "\r\n" +
                "Auto-copy received and command text: " + (settings.AutoCopyLatestRemoteText ? "on" : "off") + "\r\n" +
                "Paste after Enter: " + (settings.PasteAfterEnter ? "on" : "off") + "\r\n" +
                "Dynamic history mode: " + (settings.DynamicHistoryMode ? "on" : "off") + "\r\n" +
                "Build stamp: " + BuildInfo.BuildStampUtcMs + "\r\n" +
                "Executable hash: " + SharedUpdateStateStore.CurrentExeHash() + "\r\n" +
                "Shared executable update coordination: " + (sharedUpdateCoordinationEnabled ? "enabled for install-local Settings" : "disabled for separately stored settings") + "\r\n" +
                "Shared update state path: " + (sharedUpdateCoordinationEnabled ? SharedUpdateStateStore.StatePath(sharedExecutableSettingsDirectory) : "Not used") + "\r\n" +
                "Shared update state build stamp: " + (sharedState == null ? 0 : sharedState.BuildStampUtcMs) + "\r\n" +
                "Remove duplicates: " + settings.RemoveDuplicates + "\r\n" +
                "Duplicate mode: " + settings.DuplicateMode + "\r\n" +
                "Auto group by app: " + settings.AutoGroupByApp + "\r\n" +
                "Auto remove URL tracking: " + settings.AutoRemoveUrlTracking + "\r\n" +
                "Sensitive data mode: " + SensitiveDataExclusion.NormalizeMode(settings.SensitiveDataMode) + "\r\n" +
                "Sensitive data presets: " + SensitiveDataPresetSummary() + "\r\n" +
                "Last clipboard privacy signal: " + lastClipboardPrivacySignal + "\r\n" +
                "Clipboard flood protection: " + ClipboardFloodProtectionStatus(diagnosticNowMilliseconds) + "\r\n" +
                "Clipboard flood protection activations: " + clipboardFloodGuards.SuppressionCount + "\r\n" +
                "Clipboard flood events suppressed: " + clipboardFloodGuards.SuppressedEventCount + "\r\n" +
                "Run at startup: " + settings.RunAtStartup + "\r\n" +
                "Add clipboard item on startup: " + settings.CaptureClipboardOnStartup + "\r\n" +
                "Startup registration present: " + StartupRegistration.IsEnabled() + "\r\n" +
                "Update check frequency: " + settings.UpdateCheckFrequency + "\r\n" +
                "Install updates silently: " + settings.InstallUpdatesSilently + "\r\n" +
                "Database encryption enabled: " + settings.DatabaseEncryptionEnabled + "\r\n" +
                "Runtime crash log: " + Program.RuntimeLogPath() + "\r\n" +
                websiteTitleDiagnostics.Summary() + "\r\n" +
                "User sound override folder: " + Path.Combine(settingsStore.SettingsDirectory, "sounds") + "\r\n" +
                "Group filter: " + settings.GroupFilter + "\r\n" +
                "Foreground process: " + FriendlyProcessName(ForegroundProcessName()) + "\r\n" +
                "Clipboard owner process: " + FriendlyProcessName(ClipboardOwnerProcessName()) + "\r\n" +
                "Maximum entries: " + (settings.MaxHistoryEntries <= 0 ? "No limit" : settings.MaxHistoryEntries.ToString()) + "\r\n" +
                "Maximum age: " + (settings.MaxHistoryDays <= 0 ? "No limit" : settings.MaxHistoryDays + " days") + "\r\n" +
                "Ignored applications: " + ignored + "\r\n\r\n" +
                BuildRecentClipboardEventsText();
        }

        private string SensitiveDataPresetSummary()
        {
            if (settings.SensitiveDataPresetIds == null || settings.SensitiveDataPresetIds.Count == 0)
            {
                return "None";
            }

            var names = SensitiveDataExclusion.BuiltInPresets
                .Where(p => settings.SensitiveDataPresetIds.Any(id => string.Equals(id, p.Id, StringComparison.OrdinalIgnoreCase)))
                .Select(p => p.Name)
                .ToList();
            return names.Count == 0 ? "None" : string.Join(", ", names);
        }

        private string BuildRecentClipboardEventsText()
        {
            var snapshots = fileEventStore.GetEvents();
            var total = snapshots.Count;
            var limit = settings.DiagnosticsFileHistoryLimit;
            if (limit < 0) limit = 0;
            if (limit > 200) limit = 200;

            if (total == 0)
            {
                return "File and non-text clipboard events: none recorded.";
            }

            if (limit == 0)
            {
                return "File and non-text clipboard events: " + total + " recorded. Details omitted by diagnostics preference.";
            }

            var lines = new List<string> { "File and non-text clipboard events: showing " + Math.Min(limit, total) + " of " + total + "." };
            foreach (var item in snapshots.Take(limit))
            {
                var title = item.CapturedAt.ToString("yyyy-MM-dd HH:mm:ss") +
                    " | Source: " + (string.IsNullOrWhiteSpace(item.Source) ? "Unknown" : item.Source);
                if (!string.IsNullOrWhiteSpace(item.Operation))
                {
                    title += " | Operation: " + item.Operation;
                }
                if (item.FileCount > 0)
                {
                    title += " | Files: " + item.FileCount;
                }
                if (item.ContainsText)
                {
                    title += " | Also contains text";
                }
                lines.Add(title);

                if (item.Files != null && item.Files.Count > 0)
                {
                    foreach (var file in item.Files.Take(20))
                    {
                        lines.Add("  " + file);
                    }
                    if (item.FileCount > 20)
                    {
                        lines.Add("  ... " + (item.FileCount - 20) + " more file(s)");
                    }
                }
                else if (item.Formats != null && item.Formats.Count > 0)
                {
                    lines.Add("  Formats: " + string.Join(", ", item.Formats.Take(12)));
                }
            }
            if (total > limit)
            {
                lines.Add("... " + (total - limit) + " more event(s) omitted by diagnostics preference.");
            }

            return string.Join("\r\n", lines);
        }

        private void StoreChanged(object sender, EventArgs e)
        {
            if (store.LastChangeWasExternal)
            {
                if (invoker != null && invoker.IsHandleCreated)
                {
                    invoker.BeginInvoke(new Action(MaybeAutoCopyLatestRemoteEntry));
                }
            }
            if (historyForm != null && !historyForm.IsDisposed)
            {
                historyForm.BeginInvoke(new Action(() => historyForm.Reload()));
            }
        }

        private void QuickPasteEntry(string entryId)
        {
            var entry = store.GetEntryById(entryId);
            if (entry == null || string.IsNullOrEmpty(entry.Text))
            {
                sounds.Skip(settings.SoundsEnabled);
                return;
            }

            try
            {
                var mode = QuickPasteModeForEntry(entryId);
                if (mode == QuickPasteModes.CopyOnly)
                {
                    SetClipboardIgnoringNotification(() => SetEntryClipboard(entry));
                    store.MarkUsed(entry.Id);
                    sounds.Copy(settings.SoundsEnabled);
                    return;
                }

                IDataObject previousClipboard = null;
                if (mode == QuickPasteModes.PasteRestore)
                {
                    try
                    {
                        previousClipboard = SnapshotClipboardData();
                    }
                    catch
                    {
                        previousClipboard = null;
                    }
                }

                SetClipboardIgnoringNotification(() => SetEntryClipboard(entry));
                store.MarkUsed(entry.Id);
                sounds.Copy(settings.SoundsEnabled);
                if (mode == QuickPasteModes.PasteKeep)
                {
                    BeginPasteOnly();
                }
                else
                {
                    BeginPasteThenRestore(previousClipboard);
                }
            }
            catch
            {
                sounds.Skip(settings.SoundsEnabled);
            }
        }

        private string QuickPasteModeForEntry(string entryId)
        {
            if (string.IsNullOrWhiteSpace(entryId) || settings.QuickCopyHotkeys == null) return QuickPasteModes.PasteRestore;
            var binding = settings.QuickCopyHotkeys.FirstOrDefault(b =>
                b != null && string.Equals(b.EntryId, entryId, StringComparison.OrdinalIgnoreCase));
            return binding == null ? QuickPasteModes.PasteRestore : QuickPasteModes.Normalize(binding.Mode);
        }

        private void QuickPasteSecret(SecretEntry secret)
        {
            if (secret == null || string.IsNullOrEmpty(secret.Value))
            {
                sounds.Skip(settings.SoundsEnabled);
                return;
            }

            try
            {
                IDataObject previousClipboard = null;
                try
                {
                    previousClipboard = SnapshotClipboardData();
                }
                catch
                {
                    previousClipboard = null;
                }

                SetClipboardIgnoringNotification(() => Clipboard.SetText(secret.Value, TextDataFormat.UnicodeText));
                sounds.Copy(settings.SoundsEnabled);
                BeginPasteThenRestore(previousClipboard);
            }
            catch
            {
                sounds.Skip(settings.SoundsEnabled);
            }
        }

        private List<SecretEntry> GetSecretEntriesSafe()
        {
            if (secretStore == null) return new List<SecretEntry>();
            try
            {
                return secretStore.GetEntries();
            }
            catch (Exception ex)
            {
                Debug.WriteLine("Could not read secrets store: " + ex);
                return new List<SecretEntry>();
            }
        }

        private void BeginPasteOnly()
        {
            ThreadPool.QueueUserWorkItem(_ =>
            {
                WaitForHotkeyModifiersReleased();
                BeginInvokeIfReady(SendCtrlVPaste);
            });
        }

        private void RememberPreviousForegroundWindow()
        {
            var foreground = NativeMethods.GetForegroundWindow();
            if (foreground == IntPtr.Zero) return;

            uint processId;
            NativeMethods.GetWindowThreadProcessId(foreground, out processId);
            if (processId == (uint)Process.GetCurrentProcess().Id) return;
            previousForegroundWindow = foreground;
        }

        private void PasteClipboardIntoPreviousApplication()
        {
            var target = previousForegroundWindow;
            previousForegroundWindow = IntPtr.Zero;
            if (target == IntPtr.Zero || !NativeMethods.IsWindow(target))
            {
                sounds.Skip(settings.SoundsEnabled);
                return;
            }

            ThreadPool.QueueUserWorkItem(_ =>
            {
                WaitForHotkeyModifiersReleased();
                BeginInvokeIfReady(() => ActivatePreviousWindowAndPaste(target, 2));
            });
        }

        private void ActivatePreviousWindowAndPaste(IntPtr target, int attemptsRemaining)
        {
            if (!NativeMethods.IsWindow(target))
            {
                sounds.Skip(settings.SoundsEnabled);
                return;
            }

            NativeMethods.SetForegroundWindow(target);
            var timer = new System.Windows.Forms.Timer { Interval = 100 };
            timer.Tick += (sender, args) =>
            {
                timer.Stop();
                timer.Dispose();
                if (NativeMethods.GetForegroundWindow() == target)
                {
                    SendCtrlVPaste();
                }
                else if (attemptsRemaining > 0)
                {
                    ActivatePreviousWindowAndPaste(target, attemptsRemaining - 1);
                }
                else
                {
                    sounds.Skip(settings.SoundsEnabled);
                }
            };
            timer.Start();
        }

        private void BeginPasteThenRestore(IDataObject previousClipboard)
        {
            ThreadPool.QueueUserWorkItem(_ =>
            {
                WaitForHotkeyModifiersReleased();
                BeginInvokeIfReady(SendCtrlVPaste);
                Thread.Sleep(600);
                BeginInvokeIfReady(() => RestoreClipboard(previousClipboard));
            });
        }

        private static IDataObject SnapshotClipboardData()
        {
            return SnapshotClipboardData(Clipboard.GetDataObject());
        }

        internal static IDataObject SnapshotClipboardData(IDataObject source)
        {
            if (source == null) return null;

            var snapshot = new DataObject();
            var copied = false;

            copied |= SnapshotText(source, snapshot, DataFormats.UnicodeText, TextDataFormat.UnicodeText);
            copied |= SnapshotText(source, snapshot, DataFormats.Text, TextDataFormat.Text);
            copied |= SnapshotText(source, snapshot, DataFormats.OemText, TextDataFormat.Text);
            copied |= SnapshotText(source, snapshot, DataFormats.Rtf, TextDataFormat.Rtf);
            copied |= SnapshotText(source, snapshot, DataFormats.Html, TextDataFormat.Html);
            copied |= SnapshotText(source, snapshot, DataFormats.CommaSeparatedValue, TextDataFormat.CommaSeparatedValue);

            var url = ClipboardUrlData.TryRead(source);
            if (url != null)
            {
                if (!snapshot.GetDataPresent(DataFormats.UnicodeText, false))
                {
                    snapshot.SetText(url, TextDataFormat.UnicodeText);
                }
                snapshot.SetData("UniformResourceLocatorW", false,
                    new MemoryStream(Encoding.Unicode.GetBytes(url + "\0")));
                copied = true;
            }

            try
            {
                if (source.GetDataPresent(DataFormats.FileDrop, false))
                {
                    var files = source.GetData(DataFormats.FileDrop, false) as string[];
                    if (files != null && files.Length > 0)
                    {
                        var fileList = new StringCollection();
                        fileList.AddRange(files);
                        snapshot.SetFileDropList(fileList);
                        copied = true;
                    }
                }
            }
            catch
            {
            }

            copied |= SnapshotImage(source, snapshot);
            copied |= SnapshotStream(source, snapshot, DataFormats.WaveAudio, MaximumClipboardSnapshotStreamLength);

            // Explorer uses this small stream to distinguish a cut from a copy. It is
            // independently cloneable, unlike delayed OLE formats such as metafiles.
            copied |= SnapshotStream(source, snapshot, "Preferred DropEffect", 64);

            return copied ? snapshot : null;
        }

        private static bool SnapshotImage(IDataObject source, DataObject snapshot)
        {
            try
            {
                if (!source.GetDataPresent(DataFormats.Bitmap, false)) return false;
                var image = source.GetData(DataFormats.Bitmap, false) as Image;
                if (image == null) return false;
                snapshot.SetImage(new Bitmap(image));
                return true;
            }
            catch
            {
                return false;
            }
        }

        private static bool SnapshotStream(IDataObject source, DataObject snapshot, string format, int maximumLength)
        {
            try
            {
                if (!source.GetDataPresent(format, false)) return false;
                var data = source.GetData(format, false);
                var stream = data as Stream;
                if (stream != null)
                {
                    if (stream.CanSeek && stream.Length > maximumLength) return false;
                    var copy = new MemoryStream();
                    var originalPosition = stream.CanSeek ? stream.Position : 0;
                    try
                    {
                        if (stream.CanSeek) stream.Position = 0;
                        CopyBounded(stream, copy, maximumLength);
                    }
                    finally
                    {
                        if (stream.CanSeek) stream.Position = originalPosition;
                    }
                    copy.Position = 0;
                    snapshot.SetData(format, false, copy);
                    return true;
                }

                var bytes = data as byte[];
                if (bytes != null && bytes.Length <= maximumLength)
                {
                    snapshot.SetData(format, false, bytes.ToArray());
                    return true;
                }
            }
            catch
            {
            }
            return false;
        }

        private static bool SnapshotText(IDataObject source, DataObject snapshot, string dataFormat, TextDataFormat textFormat)
        {
            try
            {
                if (!source.GetDataPresent(dataFormat, false)) return false;
                var text = source.GetData(dataFormat, false) as string;
                if (text == null || text.Length > MaximumClipboardSnapshotTextLength) return false;
                snapshot.SetText(text, textFormat);
                return true;
            }
            catch
            {
                return false;
            }
        }

        private static void CopyBounded(Stream source, Stream destination, int maximumLength)
        {
            var buffer = new byte[81920];
            var total = 0;
            while (true)
            {
                var read = source.Read(buffer, 0, Math.Min(buffer.Length, maximumLength - total + 1));
                if (read <= 0) return;
                total += read;
                if (total > maximumLength) throw new InvalidDataException("Clipboard format exceeds the snapshot limit.");
                destination.Write(buffer, 0, read);
            }
        }

        private void BeginInvokeIfReady(Action action)
        {
            if (action == null) return;
            if (invoker == null || invoker.IsDisposed || !invoker.IsHandleCreated) return;
            try
            {
                invoker.BeginInvoke(action);
            }
            catch
            {
            }
        }

        private static void WaitForHotkeyModifiersReleased()
        {
            var deadline = DateTime.UtcNow.AddMilliseconds(1200);
            while (DateTime.UtcNow < deadline && AnyHotkeyModifierDown())
            {
                Thread.Sleep(20);
            }
            Thread.Sleep(40);
        }

        private static bool AnyHotkeyModifierDown()
        {
            return IsKeyDown(NativeMethods.VK_CONTROL) ||
                   IsKeyDown(NativeMethods.VK_SHIFT) ||
                   IsKeyDown(NativeMethods.VK_MENU) ||
                   IsKeyDown(NativeMethods.VK_LWIN) ||
                   IsKeyDown(NativeMethods.VK_RWIN);
        }

        private static bool IsKeyDown(int virtualKey)
        {
            return (NativeMethods.GetAsyncKeyState(virtualKey) & unchecked((short)0x8000)) != 0;
        }

        private static void SendCtrlVPaste()
        {
            if (!KeyboardInput.SendControlVPaste())
            {
                throw new InvalidOperationException("Windows did not accept the paste keyboard input.");
            }
        }

        private void RestoreClipboard(IDataObject previousClipboard)
        {
            try
            {
                if (previousClipboard == null)
                {
                    SetClipboardIgnoringNotification(Clipboard.Clear);
                }
                else
                {
                    SetClipboardIgnoringNotification(() => Clipboard.SetDataObject(previousClipboard, true));
                }
            }
            catch
            {
            }
        }

        private void StoreStorageStateChanged(object sender, StorageStateChangedEventArgs e)
        {
            Program.WriteRuntimeLog(
                e.Unavailable
                    ? "Text-history storage became unavailable: " + e.Error
                    : "Text-history storage recovered.",
                null);
            try
            {
                if (invoker != null && invoker.IsHandleCreated)
                {
                    invoker.BeginInvoke(new Action(UpdateTray));
                }
            }
            catch (InvalidOperationException)
            {
            }
        }

        private void SetClipboardIgnoringNotification(Action setClipboard)
        {
            if (setClipboard == null) return;
            clipMergeDetector.Reset();
            setClipboard();
            clipboardNotifications.Ignore(NativeMethods.GetClipboardSequenceNumber());
        }

        private void ResetRemoteAutoCopyBaseline()
        {
            var entry = store == null ? null : store.GetNewestRemoteEntry(CurrentDeviceName());
            if (entry == null)
            {
                lastAutoCopiedRemoteEntryId = string.Empty;
                lastAutoCopiedRemoteEntryStamp = 0;
                return;
            }

            lastAutoCopiedRemoteEntryId = entry.Id ?? string.Empty;
            lastAutoCopiedRemoteEntryStamp = entry.CreatedUnixMs;
        }

        private void MaybeAutoCopyLatestRemoteEntry()
        {
            if (!settings.AutoCopyLatestRemoteText) return;

            var entry = store.GetNewestRemoteEntry(CurrentDeviceName());
            if (entry == null || string.IsNullOrEmpty(entry.Text)) return;

            var stamp = entry.CreatedUnixMs;
            if (stamp < lastAutoCopiedRemoteEntryStamp)
            {
                return;
            }
            if (stamp == lastAutoCopiedRemoteEntryStamp &&
                string.Equals(entry.Id ?? string.Empty, lastAutoCopiedRemoteEntryId, StringComparison.Ordinal))
            {
                return;
            }

            lastAutoCopiedRemoteEntryId = entry.Id ?? string.Empty;
            lastAutoCopiedRemoteEntryStamp = stamp;
            SetClipboardIgnoringNotification(() => SetEntryClipboard(entry));
            sounds.Remote(settings.SoundsEnabled);
        }

        private void MaybeCopyCommandEntry(string entryId)
        {
            if (!settings.AutoCopyLatestRemoteText) return;

            var entry = store.GetEntryById(entryId);
            if (entry == null || string.IsNullOrEmpty(entry.Text)) return;

            try
            {
                SetClipboardIgnoringNotification(() => SetEntryClipboard(entry));
                sounds.Copy(settings.SoundsEnabled);
            }
            catch (Exception ex)
            {
                Program.WriteRuntimeLog("Could not put a command-line history addition on the clipboard.", ex);
                sounds.Skip(settings.SoundsEnabled);
            }
        }

        private static void SetEntryClipboard(ClipEntry entry)
        {
            var text = ResolvedEntryText(entry);
            var data = new DataObject();
            data.SetText(text, TextDataFormat.UnicodeText);
            if (entry != null && !entry.IsTemplate && string.Equals(text, entry.Text ?? string.Empty, StringComparison.Ordinal))
            {
                RichTextData.AddToDataObject(data, entry.RichText, entry);
            }
            Clipboard.SetDataObject(data, true);
        }

        private static string ResolvedEntryText(ClipEntry entry)
        {
            if (entry == null) return string.Empty;
            var text = entry.Text ?? string.Empty;
            return entry.IsTemplate ? TemplateResolver.Resolve(text) : text;
        }

        private void FileEventStoreChanged(object sender, EventArgs e)
        {
            if (historyForm != null && !historyForm.IsDisposed)
            {
                historyForm.BeginInvoke(new Action(() => historyForm.RefreshFileClipboardEvents()));
            }
        }

        private string TrayText()
        {
            var storageMessage = StorageUnavailableMessage();
            if (!string.IsNullOrWhiteSpace(storageMessage))
            {
                return ShortTrayText("Clipman: storage unavailable");
            }

            return settings.Active ? "Clipman: on" : "Clipman: off";
        }

        private bool IsStorageUnavailable()
        {
            return !string.IsNullOrWhiteSpace(StorageUnavailableMessage());
        }

        private string StorageUnavailableMessage()
        {
            var parts = new List<string>();
            if (!string.IsNullOrWhiteSpace(store.LastStorageError))
            {
                parts.Add("Text history: " + store.LastStorageError);
            }

            if (!string.IsNullOrWhiteSpace(fileEventStore.LastStorageError))
            {
                parts.Add("File history: " + fileEventStore.LastStorageError);
            }

            return string.Join("; ", parts);
        }

        private static string ShortTrayText(string text)
        {
            if (string.IsNullOrEmpty(text)) return string.Empty;
            return text.Length <= 63 ? text : text.Substring(0, 63);
        }

        private void UpdateTray()
        {
            notifyIcon.Text = TrayText();
            notifyIcon.Icon = BuildIcon(settings.Active);
            notifyIcon.ContextMenuStrip = BuildTrayMenu();
            if (historyForm != null && !historyForm.IsDisposed && historyForm.IsHandleCreated)
            {
                historyForm.BeginInvoke(new Action(historyForm.RefreshSteadyStatus));
            }
        }

        private string HistorySteadyStatusText()
        {
            var monitoring = settings.Active ? string.Empty : "Monitoring off. ";
            var fileStorageError = fileEventStore == null ? string.Empty : fileEventStore.LastStorageError;
            // The channel notice is good news about one entry; it must never stand in for a storage
            // or connectivity problem, so it is only offered where the status would say "Ready".
            var channelNotice = store == null ? string.Empty : store.ChannelAnnouncement();
            var sync = store == null ? null : store.GetServerSyncStatus();
            if (IsServerStorageEnabled())
            {
                if (sync == null || !sync.Enabled || !sync.Configured)
                {
                    return monitoring + "Server is not configured; using local cache.";
                }
                if (!string.IsNullOrWhiteSpace(sync.LastError))
                {
                    return monitoring + "Server unavailable; using local cache.";
                }
                if (sync.LastSuccessUnixMs <= 0)
                {
                    return monitoring + "Connecting to Clipman Server.";
                }
                if (!string.IsNullOrWhiteSpace(fileStorageError))
                {
                    return monitoring + "File history storage unavailable.";
                }
                if (!string.IsNullOrEmpty(channelNotice))
                {
                    return monitoring + channelNotice;
                }
                return settings.Active ? "Ready. Server sync connected." : "Monitoring off. Server sync connected.";
            }
            if ((store != null && !string.IsNullOrWhiteSpace(store.LastStorageError)) || !string.IsNullOrWhiteSpace(fileStorageError))
            {
                return monitoring + "Storage unavailable.";
            }
            if (!string.IsNullOrEmpty(channelNotice))
            {
                return monitoring + channelNotice;
            }
            return settings.Active ? "Ready. Using local or shared-folder history." : "Monitoring off. Using local or shared-folder history.";
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (closeEvent != null) closeEvent.Dispose();
                if (showEvent != null) showEvent.Dispose();
                if (recoverEvent != null) recoverEvent.Dispose();
                if (pauseEvent != null) pauseEvent.Dispose();
                if (resumeEvent != null) resumeEvent.Dispose();
                if (toggleEvent != null) toggleEvent.Dispose();
                if (historyChangedEvent != null) historyChangedEvent.Dispose();
                if (historyAddedEvent != null) historyAddedEvent.Dispose();
                if (invoker != null) invoker.Dispose();
                if (sharedStateWatcher != null) sharedStateWatcher.Dispose();
                if (executableWatcher != null) executableWatcher.Dispose();
                if (sharedStateTimer != null) sharedStateTimer.Dispose();
                if (updateCheckTimer != null) updateCheckTimer.Dispose();
                if (clipboardFloodRecoveryTimer != null) clipboardFloodRecoveryTimer.Dispose();
                NativeMethods.RemoveClipboardFormatListener(messageWindow.Handle);
                NativeMethods.UnregisterHotKey(messageWindow.Handle, ShowHotkeyId);
                NativeMethods.UnregisterHotKey(messageWindow.Handle, ToggleHotkeyId);
                NativeMethods.UnregisterHotKey(messageWindow.Handle, ToggleHotkeyAlternateId);
                NativeMethods.UnregisterHotKey(messageWindow.Handle, SaveCurrentClipboardHotkeyId);
                NativeMethods.UnregisterHotKey(messageWindow.Handle, QuickClipHotkeyId);
                foreach (var hotkeyId in quickCopyHotkeyEntryIds.Keys.ToList())
                {
                    NativeMethods.UnregisterHotKey(messageWindow.Handle, hotkeyId);
                }
                notifyIcon.Visible = false;
                notifyIcon.Dispose();
                store.StorageStateChanged -= StoreStorageStateChanged;
                store.Dispose();
                fileEventStore.Dispose();
                messageWindow.Dispose();
                messageWindow.DestroyHandle();
            }
            base.Dispose(disposing);
        }

        private void WaitForClose()
        {
            try
            {
                closeEvent.WaitOne();
                if (invoker != null && invoker.IsHandleCreated)
                {
                    invoker.BeginInvoke(new Action(ExitThread));
                }
            }
            catch
            {
            }
        }

        private void WaitForShow()
        {
            while (true)
            {
                try
                {
                    showEvent.WaitOne();
                    showEvent.Reset();
                    if (invoker != null && invoker.IsHandleCreated)
                    {
                        invoker.BeginInvoke(new Action(ShowHistory));
                    }
                }
                catch
                {
                    return;
                }
            }
        }

        private void WaitForState(EventWaitHandle ev, bool active)
        {
            while (true)
            {
                try
                {
                    ev.WaitOne();
                    ev.Reset();
                    if (invoker != null && invoker.IsHandleCreated)
                    {
                        invoker.BeginInvoke(new Action(() => SetActive(active, true)));
                    }
                }
                catch
                {
                    return;
                }
            }
        }

        private void WaitForToggle()
        {
            while (true)
            {
                try
                {
                    toggleEvent.WaitOne();
                    toggleEvent.Reset();
                    if (invoker != null && invoker.IsHandleCreated)
                    {
                        invoker.BeginInvoke(new Action(ToggleActive));
                    }
                }
                catch
                {
                    return;
                }
            }
        }

        private void StartSharedUpdateWatchers()
        {
            var sharedExecutableSettingsDirectory = SharedExecutableSettingsDirectory();
            if (!SharedUpdateStateStore.CanCoordinateExecutable(sharedExecutableSettingsDirectory, Application.ExecutablePath)) return;
            try
            {
                Directory.CreateDirectory(sharedExecutableSettingsDirectory);
                sharedStateTimer = new System.Threading.Timer(delegate
                {
                    try
                    {
                        CheckSharedUpdateState();
                    }
                    catch (Exception ex)
                    {
                        Program.WriteRuntimeLog("Shared executable update coordination check failed.", ex);
                    }
                }, null, Timeout.Infinite, Timeout.Infinite);

                sharedStateWatcher = new FileSystemWatcher(sharedExecutableSettingsDirectory, "clipman-shared-state*.json")
                {
                    NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size | NotifyFilters.FileName | NotifyFilters.CreationTime
                };
                sharedStateWatcher.Changed += SharedUpdateStateChanged;
                sharedStateWatcher.Created += SharedUpdateStateChanged;
                sharedStateWatcher.Renamed += SharedUpdateStateChanged;
                sharedStateWatcher.EnableRaisingEvents = true;

                executableWatcher = new FileSystemWatcher(appDirectory, "clipman.exe")
                {
                    NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size | NotifyFilters.FileName | NotifyFilters.CreationTime
                };
                executableWatcher.Changed += SharedUpdateStateChanged;
                executableWatcher.Created += SharedUpdateStateChanged;
                executableWatcher.Renamed += SharedUpdateStateChanged;
                executableWatcher.EnableRaisingEvents = true;
            }
            catch
            {
            }
        }

        private void RestartSharedUpdateWatchers()
        {
            try
            {
                if (sharedStateWatcher != null)
                {
                    sharedStateWatcher.Dispose();
                    sharedStateWatcher = null;
                }
                if (executableWatcher != null)
                {
                    executableWatcher.Dispose();
                    executableWatcher = null;
                }
                if (sharedStateTimer != null)
                {
                    sharedStateTimer.Dispose();
                    sharedStateTimer = null;
                }
                StartSharedUpdateWatchers();
                ScheduleSharedUpdateCheck(5000);
            }
            catch
            {
            }
        }

        private void ReopenFileHistoryStore()
        {
            try
            {
                if (fileEventStore != null)
                {
                    fileEventStore.Changed -= FileEventStoreChanged;
                    fileEventStore.Dispose();
                }
            }
            catch
            {
            }

            fileEventStore = new FileClipboardEventStore(settingsStore.DefaultFileHistoryDatabasePath(), CurrentDatabasePassword);
            fileEventStore.Changed += FileEventStoreChanged;
        }

        private void ApplyStartupRegistration(bool showErrors)
        {
            try
            {
                StartupRegistration.SetEnabled(settings.RunAtStartup);
            }
            catch (Exception ex)
            {
                if (showErrors)
                {
                    MessageBox.Show("Could not update the Windows startup entry.\r\n\r\n" + ex.Message, "Clipman startup", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                }
            }
        }

        private void ScheduleUpdateChecks()
        {
            try
            {
                if (updateCheckTimer != null)
                {
                    updateCheckTimer.Dispose();
                    updateCheckTimer = null;
                }

                var frequency = settings.UpdateCheckFrequency ?? "Never";
                if (string.Equals(frequency, "Never", StringComparison.OrdinalIgnoreCase))
                {
                    return;
                }

                var period = Timeout.Infinite;
                if (string.Equals(frequency, "Hourly", StringComparison.OrdinalIgnoreCase))
                {
                    period = (int)TimeSpan.FromHours(1).TotalMilliseconds;
                }
                else if (string.Equals(frequency, "Daily", StringComparison.OrdinalIgnoreCase))
                {
                    period = (int)TimeSpan.FromDays(1).TotalMilliseconds;
                }

                updateCheckTimer = new System.Threading.Timer(delegate { CheckForUpdatesAutomatically(); }, null, 30000, period);
            }
            catch
            {
            }
        }

        private void CheckForUpdatesAutomatically()
        {
            try
            {
                if (invoker != null && invoker.IsHandleCreated)
                {
                    invoker.BeginInvoke(new Action(() =>
                        UpdateService.CheckForUpdatesAutomatic(UpdateWindowOwner(), AppVersion(), ExitThread, settings.InstallUpdatesSilently)));
                }
            }
            catch
            {
            }
        }

        private void ResolveDatabasePassword()
        {
            ResolveDatabasePassword(EffectiveTextHistoryDatabasePath(), message => PasswordPromptForm.Ask(
                "Clipman history password", message));
        }

        private void ResolveDatabasePassword(string activeDatabasePath, Func<string, string> requestPassword)
        {
            // Server mode opens its cache, or seeds it from the configured file if absent.
            var historyPath = File.Exists(activeDatabasePath) ? activeDatabasePath : settings.DatabasePath;
            var encryptedPaths = new[]
            {
                historyPath,
                settingsStore.DefaultFileHistoryDatabasePath(),
                settingsStore.DefaultSecretsDatabasePath()
            }.Where(path => !string.IsNullOrWhiteSpace(path) && ClipDatabaseFile.IsEncryptedFile(path))
                .Distinct(StringComparer.OrdinalIgnoreCase).ToList();
            var encryptedHistoryExists = encryptedPaths.Count > 0;
            if (!encryptedHistoryExists && !settings.DatabaseEncryptionEnabled && string.IsNullOrWhiteSpace(settings.ProtectedDatabasePassword))
            {
                databasePassword = string.Empty;
                settings.DatabaseEncryptionEnabled = false;
                settings.RememberDatabasePassword = false;
                settings.ProtectedDatabasePassword = string.Empty;
                return;
            }

            if (settings.RememberDatabasePassword && !string.IsNullOrWhiteSpace(settings.ProtectedDatabasePassword))
            {
                try
                {
                    var password = settingsStore.DatabasePassword(settings);
                    if (!string.IsNullOrEmpty(password))
                    {
                        databasePassword = password;
                        settings.DatabaseEncryptionEnabled = true;
                        return;
                    }
                }
                catch
                {
                }
            }

            if (!string.IsNullOrEmpty(databasePassword))
            {
                settings.DatabaseEncryptionEnabled = true;
                if (settings.RememberDatabasePassword && string.IsNullOrWhiteSpace(settings.ProtectedDatabasePassword))
                {
                    settings.ProtectedDatabasePassword = DatabasePasswordProtector.Protect(databasePassword);
                    settingsStore.Save(settings);
                }
                return;
            }

            if (!encryptedHistoryExists)
            {
                settings.DatabaseEncryptionEnabled = false;
                settings.RememberDatabasePassword = false;
                settings.ProtectedDatabasePassword = string.Empty;
                settingsStore.Save(settings);
                return;
            }

            string entered;
            var prompt = "This machine needs the password for the encrypted Clipman history database.";
            while (true)
            {
                entered = requestPassword(prompt);
                if (string.IsNullOrEmpty(entered))
                {
                    throw new OperationCanceledException("Clipman history password was not provided.");
                }
                try
                {
                    // Validate before remembering the password; never rewrite encrypted data here.
                    foreach (var path in encryptedPaths)
                    {
                        ClipDatabaseFile.Load<Dictionary<string, object>>(path, entered);
                    }
                    break;
                }
                catch (Exception ex)
                {
                    if (!(ex is DatabasePasswordRequiredException) && !(ex is System.Security.Cryptography.CryptographicException)) throw;
                    prompt = "Could not unlock history. Check the password and try again, or Cancel. Your data has not been changed.";
                }
            }
            databasePassword = entered;
            settings.DatabaseEncryptionEnabled = true;
            if (settings.RememberDatabasePassword)
            {
                settings.ProtectedDatabasePassword = DatabasePasswordProtector.Protect(entered);
            }
            else
            {
                settings.ProtectedDatabasePassword = string.Empty;
            }
            settingsStore.Save(settings);
        }

        private void ResolveDatabaseLocation()
        {
            if (IsServerStorageEnabled())
            {
                if (settings.UseDefaultDatabasePath || string.IsNullOrWhiteSpace(settings.DatabasePath))
                {
                    settings.UseDefaultDatabasePath = true;
                    settings.DatabasePath = settingsStore.DefaultDatabasePath();
                    settingsStore.Save(settings);
                }
                return;
            }

            if (settings.UseDefaultDatabasePath || string.IsNullOrWhiteSpace(settings.DatabasePath))
            {
                settings.UseDefaultDatabasePath = true;
                settings.DatabasePath = settingsStore.DefaultDatabasePath();
                settingsStore.Save(settings);
                return;
            }

            if (File.Exists(settings.DatabasePath))
            {
                return;
            }

            var message =
                "Clipman cannot find the configured history database:" +
                Environment.NewLine +
                Environment.NewLine +
                settings.DatabasePath +
                Environment.NewLine +
                Environment.NewLine +
                "Choose Yes to browse for the data folder that contains the database." +
                Environment.NewLine +
                "Choose No to use the default database beside Clipman." +
                Environment.NewLine +
                "Choose Cancel to stop Clipman.";
            var choice = MessageBox.Show(
                message,
                "Clipman database not found",
                MessageBoxButtons.YesNoCancel,
                MessageBoxIcon.Warning,
                MessageBoxDefaultButton.Button1);

            if (choice == DialogResult.Cancel)
            {
                throw new OperationCanceledException("Clipman database location was not selected.");
            }

            if (choice == DialogResult.Yes)
            {
                using (var dialog = new FolderBrowserDialog())
                {
                    dialog.Description = "Choose the Clipman data folder. Clipman will use clipman-history.clipdb inside this folder.";
                    dialog.ShowNewFolderButton = true;
                    var oldDir = Path.GetDirectoryName(settings.DatabasePath);
                    if (!string.IsNullOrWhiteSpace(oldDir) && Directory.Exists(oldDir))
                    {
                        dialog.SelectedPath = oldDir;
                    }

                    if (dialog.ShowDialog() != DialogResult.OK)
                    {
                        throw new OperationCanceledException("Clipman database location was not selected.");
                    }

                    settings.DatabasePath = Path.Combine(dialog.SelectedPath, "clipman-history.clipdb");
                    settings.UseDefaultDatabasePath = settingsStore.IsCurrentDefaultDatabasePath(settings.DatabasePath);
                    settingsStore.Save(settings);
                    return;
                }
            }

            settings.UseDefaultDatabasePath = true;
            settings.DatabasePath = settingsStore.DefaultDatabasePath();
            settingsStore.Save(settings);
        }

        private string EffectiveTextHistoryDatabasePath()
        {
            return settingsStore.EffectiveTextHistoryDatabasePath(settings);
        }

        private void WaitForRecover()
        {
            while (true)
            {
                try
                {
                    recoverEvent.WaitOne();
                    recoverEvent.Reset();
                    if (invoker != null && invoker.IsHandleCreated)
                    {
                        invoker.BeginInvoke(new Action(delegate
                        {
                            if (IsStorageUnavailable()) RetryStorage();
                        }));
                    }
                }
                catch
                {
                    return;
                }
            }
        }

        private void WaitForHistoryChanged()
        {
            while (true)
            {
                try
                {
                    historyChangedEvent.WaitOne();
                    historyChangedEvent.Reset();
                    store.ReloadExternalChangeAndSync();
                }
                catch (ObjectDisposedException)
                {
                    return;
                }
                catch (Exception ex)
                {
                    Program.WriteRuntimeLog("Could not reload and synchronize an external history change.", ex);
                }
            }
        }

        private void WaitForHistoryAdded()
        {
            while (true)
            {
                try
                {
                    historyAddedEvent.WaitOne();
                    historyAddedEvent.Reset();
                    var entryId = InstanceStateStore.TakePendingCommandEntry((long)TimeSpan.FromMinutes(5).TotalMilliseconds);
                    store.ReloadExternalChange();
                    if (!string.IsNullOrEmpty(entryId) && invoker != null && invoker.IsHandleCreated)
                    {
                        invoker.BeginInvoke(new Action(() => MaybeCopyCommandEntry(entryId)));
                    }
                    store.SyncExternalChange();
                }
                catch (ObjectDisposedException)
                {
                    return;
                }
                catch (Exception ex)
                {
                    Program.WriteRuntimeLog("Could not process and synchronize a command-line history addition.", ex);
                }
            }
        }

        private string ServerCacheDatabasePath()
        {
            return settingsStore.EffectiveTextHistoryDatabasePath(new AppSettings { StorageMode = "Server" });
        }

        private void MergeServerCacheIntoConfiguredDatabase(string serverCachePath)
        {
            if (string.IsNullOrWhiteSpace(serverCachePath) || !File.Exists(serverCachePath)) return;
            if (string.IsNullOrWhiteSpace(settings.DatabasePath)) return;
            try
            {
                var targetPath = settings.DatabasePath;
                if (string.Equals(Path.GetFullPath(serverCachePath), Path.GetFullPath(targetPath), StringComparison.OrdinalIgnoreCase)) return;

                var targetDatabase = File.Exists(targetPath)
                    ? ClipDatabaseFile.Load(targetPath, CurrentDatabasePassword())
                    : new ClipDatabase();
                var cacheDatabase = ClipDatabaseFile.Load(serverCachePath, CurrentDatabasePassword());
                SyncConflictResolver.MergeInto(targetDatabase, cacheDatabase);

                var dir = Path.GetDirectoryName(targetPath);
                if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
                ClipDatabaseFile.SaveAtomic(targetPath, targetDatabase, CurrentDatabasePassword());
            }
            catch (Exception ex)
            {
                Debug.WriteLine("Could not merge the server cache into the selected local database: " + ex.Message);
            }
        }

        private void SeedServerCacheFromConfiguredDatabase()
        {
            if (!IsServerStorageEnabled()) return;
            var cachePath = EffectiveTextHistoryDatabasePath();
            if (File.Exists(cachePath)) return;
            if (string.IsNullOrWhiteSpace(settings.DatabasePath)) return;
            if (!File.Exists(settings.DatabasePath)) return;
            if (string.Equals(Path.GetFullPath(cachePath), Path.GetFullPath(settings.DatabasePath), StringComparison.OrdinalIgnoreCase)) return;

            try
            {
                var dir = Path.GetDirectoryName(cachePath);
                if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
                File.Copy(settings.DatabasePath, cachePath, false);
            }
            catch
            {
            }
        }

        private string CurrentDatabasePassword()
        {
            return databasePassword ?? string.Empty;
        }

        private string CurrentDeviceName()
        {
            var name = settings == null ? string.Empty : (settings.DeviceName ?? string.Empty).Trim();
            return name.Length == 0 ? (Environment.MachineName ?? string.Empty).Trim() : name;
        }

        private string DefaultSecretsDatabasePath()
        {
            return settingsStore.DefaultSecretsDatabasePath();
        }

        private void ReopenSecretStore()
        {
            try
            {
                secretStore = new SecretStore(DefaultSecretsDatabasePath(), CurrentDatabasePassword);
                if (secretsForm != null && !secretsForm.IsDisposed)
                {
                    secretsForm.Close();
                    secretsForm = null;
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine("Could not reopen secrets store: " + ex);
                secretStore = null;
            }
        }

        private bool IsServerStorageEnabled()
        {
            return string.Equals(settings.StorageMode, "Server", StringComparison.OrdinalIgnoreCase);
        }

        private void ConfigureTextHistoryServerStorage()
        {
            if (store == null) return;
            store.ConfigureServerStorage(
                IsServerStorageEnabled(),
                settings.ServerUrl,
                settings.ServerToken,
                settings.ServerCaCertPem,
                settings.ServerCaHost);
        }

        private IWin32Window UpdateWindowOwner()
        {
            return historyForm != null && !historyForm.IsDisposed ? historyForm : null;
        }

        private static string AppVersion()
        {
            var version = typeof(ClipmanApplicationContext).Assembly.GetName().Version;
            return version == null ? "1.1.0" : version.Major + "." + version.Minor + "." + version.Build;
        }

        private string SharedExecutableSettingsDirectory()
        {
            return Path.Combine(appDirectory, "Settings");
        }

        private void SharedUpdateStateChanged(object sender, FileSystemEventArgs e)
        {
            ScheduleSharedUpdateCheck(10000);
        }

        private void ScheduleSharedUpdateCheck(int delayMs)
        {
            try
            {
                if (sharedStateTimer != null)
                {
                    sharedStateTimer.Change(delayMs, Timeout.Infinite);
                }
            }
            catch
            {
            }
        }

        private void CheckSharedUpdateState()
        {
            var sharedExecutableSettingsDirectory = SharedExecutableSettingsDirectory();
            if (!SharedUpdateStateStore.CanCoordinateExecutable(sharedExecutableSettingsDirectory, Application.ExecutablePath)) return;
            SharedUpdateState closeState;
            if (SharedUpdateStateStore.HasActiveCloseRequest(sharedExecutableSettingsDirectory, Application.ExecutablePath, lastHandledCloseRequestId, out closeState))
            {
                lastHandledCloseRequestId = closeState.CloseRequestId ?? string.Empty;
                if (invoker != null && invoker.IsHandleCreated)
                {
                    invoker.BeginInvoke(new Action(StartStandDownRestartHelper));
                }
                return;
            }

            SharedUpdateState state;
            string reason;
            if (!SharedUpdateStateStore.ShouldRestartForState(sharedExecutableSettingsDirectory, Application.ExecutablePath, out state, out reason))
            {
                if (SharedUpdateStateStore.IsNewerStateFromAnotherMachine(state))
                {
                    if (invoker != null && invoker.IsHandleCreated)
                    {
                        invoker.BeginInvoke(new Action(StartStandDownRestartHelper));
                    }
                    return;
                }

                if (!string.IsNullOrWhiteSpace(reason))
                {
                    ScheduleSharedUpdateCheck(15000);
                }
                else
                {
                    ScheduleSharedUpdateCheck(60000);
                }
                return;
            }

            if (invoker != null && invoker.IsHandleCreated)
            {
                invoker.BeginInvoke(new Action(RestartForSharedUpdate));
            }
        }

        private void RestartForSharedUpdate()
        {
            try
            {
                Process.Start(new ProcessStartInfo
                {
                    FileName = Application.ExecutablePath,
                    WorkingDirectory = appDirectory,
                    UseShellExecute = true
                });
            }
            catch
            {
                return;
            }

            ExitThread();
        }

        private void StartStandDownRestartHelper()
        {
            var sharedExecutableSettingsDirectory = SharedExecutableSettingsDirectory();
            if (!SharedUpdateStateStore.CanCoordinateExecutable(sharedExecutableSettingsDirectory, Application.ExecutablePath)) return;
            try
            {
                var tempRoot = Path.Combine(Path.GetTempPath(), "ClipmanRestart-" + Guid.NewGuid().ToString("N"));
                Directory.CreateDirectory(tempRoot);
                var helperExe = Path.Combine(tempRoot, "clipman-restart-helper.exe");
                File.Copy(Application.ExecutablePath, helperExe, true);
                Process.Start(new ProcessStartInfo
                {
                    FileName = helperExe,
                    Arguments =
                        "--wait-restart" +
                        " --restart-exe " + Quote(Application.ExecutablePath) +
                        " --restart-working-dir " + Quote(appDirectory) +
                        " --restart-state " + Quote(SharedUpdateStateStore.StatePath(sharedExecutableSettingsDirectory)) +
                        " --restart-current-build " + BuildInfo.BuildStampUtcMs +
                        " --restart-wait-pid " + Process.GetCurrentProcess().Id +
                        " --restart-timeout-ms 120000",
                    WorkingDirectory = tempRoot,
                    UseShellExecute = false,
                    CreateNoWindow = true
                });
                ExitThread();
            }
            catch
            {
            }
        }

        private static string Quote(string value)
        {
            if (value == null) return "\"\"";
            return "\"" + value.Replace("\"", "\\\"") + "\"";
        }

        private static Icon BuildIcon(bool active)
        {
            var bitmap = new Bitmap(16, 16);
            using (var g = Graphics.FromImage(bitmap))
            using (var back = new SolidBrush(active ? Color.ForestGreen : Color.DarkRed))
            using (var pen = new Pen(Color.White, 2))
            {
                g.Clear(Color.Transparent);
                g.FillRectangle(back, 1, 1, 14, 14);
                g.DrawRectangle(Pens.Black, 1, 1, 14, 14);
                if (active)
                {
                    g.DrawLine(pen, 4, 8, 7, 11);
                    g.DrawLine(pen, 7, 11, 12, 4);
                }
                else
                {
                    g.DrawLine(pen, 5, 5, 11, 11);
                    g.DrawLine(pen, 11, 5, 5, 11);
                }
            }

            return Icon.FromHandle(bitmap.GetHicon());
        }

        private sealed class MessageWindow : NativeWindow, IDisposable
        {
            private const int ClipboardSettleMilliseconds = 40;
            private readonly ClipmanApplicationContext app;
            private readonly System.Windows.Forms.Timer clipboardSettleTimer;

            public MessageWindow(ClipmanApplicationContext app)
            {
                this.app = app;
                clipboardSettleTimer = new System.Windows.Forms.Timer { Interval = ClipboardSettleMilliseconds };
                clipboardSettleTimer.Tick += ClipboardSettleTimerTick;
                CreateHandle(new CreateParams());
            }

            public void Dispose()
            {
                clipboardSettleTimer.Stop();
                clipboardSettleTimer.Tick -= ClipboardSettleTimerTick;
                clipboardSettleTimer.Dispose();
            }

            private void ClipboardSettleTimerTick(object sender, EventArgs e)
            {
                clipboardSettleTimer.Stop();
                long? observedMilliseconds;
                var sequence = app.clipboardNotifications.TakePending(out observedMilliseconds);
                app.HandleClipboardUpdate(false, sequence, observedMilliseconds: observedMilliseconds);
            }

            protected override void WndProc(ref Message m)
            {
                if (m.Msg == NativeMethods.WM_HOTKEY)
                {
                    app.HandleHotkey(m.WParam.ToInt32());
                    return;
                }

                if (m.Msg == NativeMethods.WM_CLIPBOARDUPDATE)
                {
                    app.clipboardNotifications.Observe(NativeMethods.GetClipboardSequenceNumber(),
                        ClipboardNotificationState.MessageObservedMilliseconds(NativeMethods.GetMessageTime(),
                            Environment.TickCount, ClipboardFloodGuard.MonotonicMilliseconds()));
                    clipboardSettleTimer.Stop();
                    clipboardSettleTimer.Start();
                    return;
                }

                base.WndProc(ref m);
            }
        }

    }
}