using System; using System.Collections.Generic; using System.IO; namespace Clipman { internal enum ClipMergeKind { Text, Files } internal sealed class ClipMergeObservation { public ClipMergeKind Kind { get; set; } public string Signature { get; set; } public string SourceApplication { get; set; } public string Operation { get; set; } public string HistoryId { get; set; } public uint ChangeIdentifier { get; set; } public object Payload { get; set; } public ClipMergeObservation() { Signature = string.Empty; SourceApplication = string.Empty; Operation = string.Empty; HistoryId = string.Empty; } } internal sealed class ClipMergeDecision { public bool ShouldMerge { get; private set; } public bool SuppressDuplicate { get; private set; } public ClipMergeObservation Base { get; private set; } public ClipMergeObservation FirstTap { get; private set; } private ClipMergeDecision() { } public static ClipMergeDecision Replace() { return new ClipMergeDecision(); } public static ClipMergeDecision Merge(ClipMergeObservation baseItem, ClipMergeObservation firstTap) { return new ClipMergeDecision { ShouldMerge = true, Base = baseItem, FirstTap = firstTap }; } public static ClipMergeDecision Duplicate() { return new ClipMergeDecision { SuppressDuplicate = true }; } } internal sealed class ClipMergeDetector { public const int DefaultWindowMilliseconds = 500; public const int MinimumWindowMilliseconds = 200; public const int MaximumWindowMilliseconds = 2000; private ClipMergeObservation current; private ClipMergeObservation candidateBase; private ClipMergeObservation candidateFirstTap; private long candidateStartedMilliseconds; public ClipMergeDecision Observe(ClipMergeObservation incoming, long nowMilliseconds, bool enabled, int windowMilliseconds, bool deliberate) { if (incoming == null) throw new ArgumentNullException("incoming"); windowMilliseconds = NormalizeWindow(windowMilliseconds); if (!enabled || deliberate || string.IsNullOrWhiteSpace(incoming.SourceApplication)) { candidateBase = null; candidateFirstTap = null; current = incoming; return ClipMergeDecision.Replace(); } var elapsed = nowMilliseconds - candidateStartedMilliseconds; if (candidateFirstTap != null && SameTap(candidateFirstTap, incoming) && IsDuplicateNotification(candidateFirstTap, incoming, elapsed)) { candidateStartedMilliseconds = nowMilliseconds; return ClipMergeDecision.Duplicate(); } if (candidateFirstTap != null && candidateBase != null && elapsed >= 0 && elapsed <= windowMilliseconds && SameTap(candidateFirstTap, incoming) && Compatible(candidateBase, incoming)) { var decision = ClipMergeDecision.Merge(candidateBase, candidateFirstTap); candidateBase = null; candidateFirstTap = null; return decision; } candidateBase = current; candidateFirstTap = incoming; candidateStartedMilliseconds = nowMilliseconds; current = incoming; return ClipMergeDecision.Replace(); } public void SetCurrentHistoryId(string historyId) { if (current != null) current.HistoryId = historyId ?? string.Empty; if (candidateFirstTap != null && ReferenceEquals(current, candidateFirstTap)) { candidateFirstTap.HistoryId = historyId ?? string.Empty; } } public void CompleteMerge(ClipMergeObservation merged, string historyId) { if (merged == null) throw new ArgumentNullException("merged"); merged.HistoryId = historyId ?? string.Empty; current = merged; candidateBase = null; candidateFirstTap = null; } public void RetainFirstTap(ClipMergeObservation firstTap) { current = firstTap; candidateBase = null; candidateFirstTap = null; } public void Reset() { current = null; candidateBase = null; candidateFirstTap = null; candidateStartedMilliseconds = 0; } public static int NormalizeWindow(int value) { return Math.Max(MinimumWindowMilliseconds, Math.Min(MaximumWindowMilliseconds, value)); } public static string ResolveSeparator(string mode, string custom) { switch ((mode ?? string.Empty).Trim().ToUpperInvariant()) { case "BLANKLINE": return Environment.NewLine + Environment.NewLine; case "SPACE": return " "; case "COMMASPACE": return ", "; case "CUSTOM": return DecodeCustomSeparator(custom); case "NEWLINE": default: return Environment.NewLine; } } private static string DecodeCustomSeparator(string value) { return (value ?? string.Empty) .Replace("\\r\\n", "\r\n") .Replace("\\n", "\n") .Replace("\\r", "\r") .Replace("\\t", "\t"); } private static bool SameTap(ClipMergeObservation left, ClipMergeObservation right) { return left.Kind == right.Kind && string.Equals(left.Signature, right.Signature, StringComparison.Ordinal) && string.Equals(left.SourceApplication, right.SourceApplication, StringComparison.OrdinalIgnoreCase) && string.Equals(left.Operation, right.Operation, StringComparison.OrdinalIgnoreCase); } private static bool IsDuplicateNotification(ClipMergeObservation left, ClipMergeObservation right, long elapsedMilliseconds) { var compatibility = ClipboardApplicationCompatibility.ForProcess(left.SourceApplication); if (left.ChangeIdentifier != 0 && right.ChangeIdentifier != 0) { if (left.ChangeIdentifier == right.ChangeIdentifier) return true; return compatibility.AcceptChangingSequenceIdentifiers && elapsedMilliseconds >= 0 && elapsedMilliseconds < compatibility.DuplicateNotificationMilliseconds; } return elapsedMilliseconds >= 0 && elapsedMilliseconds < compatibility.DuplicateNotificationMilliseconds; } private static bool Compatible(ClipMergeObservation baseItem, ClipMergeObservation incoming) { return baseItem.Kind == incoming.Kind && !string.Equals(baseItem.Signature, incoming.Signature, StringComparison.Ordinal) && (incoming.Kind != ClipMergeKind.Files || string.Equals(baseItem.Operation, incoming.Operation, StringComparison.OrdinalIgnoreCase)); } } internal static class ClipMergeFilePolicy { public static bool AreSourcesAvailable(IEnumerable paths) { if (paths == null) return false; var found = false; foreach (var value in paths) { var path = (value ?? string.Empty).Trim(); if (path.Length == 0) return false; found = true; if (!File.Exists(path) && !Directory.Exists(path)) return false; } return found; } } }