78 | Thus one of the biggest surprises is the fact that despite all the exception handling you can possibly implement access to clipboard can trigger the native exceptions that is not handled properly by neither user code nor CLR itself. This can lead to the situation when the whole CLR goes down and kills the parent process. That's why Multiclip is implemented as a two process system where `multiclip.server.exe` ai constantly monitored by `multiclip.exe`, which restarts the server if it dies while accessing the clipboard content.
79 | This in turn can lead to the situation when occasionally Multiplip can miss and not record in the clipboard history a clipboard changes that triggered the failure. This problem has higher occurrence when triggered by highly diverse clipboard formats. Thus it has not been seen with the clipboard content that is a plain text only (e.g. copy text from Notepad).
80 |
81 | - Some of the native MS application use Clipboard in a very exotic and way that affect other Clipboard clients (e.g. Multiclip). Thus Excel is the biggest offender. A single act of copying of a cell content can trigger multiple clipboard writing operations that are not transactional. Worth yet, some of the pure Excel clipboard formats are not even accessible from other processes. This can lead to the occasional reading (by Multiclip) failures that do not affect functionality and handled internally. But if the clipboard content cannot be read at all the Multiclip puts a string containing the location of the error log file so it can be used for troubleshooting:
82 | ```
83 | "MultiClip Error: "
84 | ```
85 |
86 | Thus you may see this item in the clipboard history list from time to time.
87 |
--------------------------------------------------------------------------------
/multiclip.ui/WinHook.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Runtime.InteropServices;
4 | using System.Windows.Forms;
5 | using Microsoft.Win32;
6 |
7 | namespace CSScriptNpp
8 | {
9 | public class WinHook : LocalWindowsHook, IDisposable where T : new()
10 | {
11 | static T instance;
12 |
13 | public static T Instance
14 | {
15 | get
16 | {
17 | if (instance == null)
18 | instance = new T();
19 | return instance;
20 | }
21 | }
22 |
23 | protected WinHook()
24 | : base(HookType.WH_DEBUG)
25 | {
26 | m_filterFunc = this.Proc;
27 | }
28 |
29 | ~WinHook()
30 | {
31 | Dispose(false);
32 | }
33 |
34 | protected void Dispose(bool disposing)
35 | {
36 | if (IsInstalled)
37 | Uninstall();
38 |
39 | if (disposing)
40 | GC.SuppressFinalize(this);
41 | }
42 |
43 | protected void Install(HookType type)
44 | {
45 | base.m_hookType = type;
46 | base.Install();
47 | }
48 |
49 | public void Dispose()
50 | {
51 | Dispose(true);
52 | }
53 |
54 | protected int Proc(int code, IntPtr wParam, IntPtr lParam)
55 | {
56 | if (code == 0) //Win32.HC_ACTION
57 | if (HandleHookEvent(wParam, lParam))
58 | return 1;
59 |
60 | return CallNextHookEx(m_hhook, code, wParam, lParam);
61 | }
62 |
63 | virtual protected bool HandleHookEvent(IntPtr wParam, IntPtr lParam)
64 | {
65 | throw new NotSupportedException();
66 | }
67 | }
68 |
69 | public struct Modifiers
70 | {
71 | public bool IsCtrl;
72 | public bool IsShift;
73 | public bool IsAlt;
74 | }
75 |
76 | public partial class KeyInterceptor : WinHook
77 | {
78 | [DllImport("USER32.dll")]
79 | static extern short GetKeyState(int nVirtKey);
80 |
81 | public static bool IsPressed(Keys key)
82 | {
83 | const int KEY_PRESSED = 0x8000;
84 | return Convert.ToBoolean(GetKeyState((int)key) & KEY_PRESSED);
85 | }
86 |
87 | public static Modifiers GetModifiers()
88 | {
89 | return new Modifiers
90 | {
91 | IsCtrl = KeyInterceptor.IsPressed(Keys.ControlKey),
92 | IsShift = KeyInterceptor.IsPressed(Keys.ShiftKey),
93 | IsAlt = KeyInterceptor.IsPressed(Keys.Menu)
94 | };
95 | }
96 |
97 | public delegate void KeyDownHandler(Keys key, int repeatCount, ref bool handled);
98 |
99 | public List KeysToIntercept = new List();
100 |
101 | public new void Install()
102 | {
103 | base.Install(HookType.WH_KEYBOARD);
104 | }
105 |
106 | public event KeyDownHandler KeyDown;
107 |
108 | public void Add(params Keys[] keys)
109 | {
110 | foreach (int key in keys)
111 | KeysToIntercept.Add(key);
112 | }
113 |
114 | public void Remove(params Keys[] keys)
115 | {
116 | foreach (int key in keys)
117 | {
118 | //ignore for now as anyway the extra invoke will not do any harm
119 | //but eventually it needs to be ref counting based
120 | //KeysToIntercept.RemoveAll(k => k == key);
121 | }
122 | }
123 |
124 | public const int KF_UP = 0x8000;
125 | public const long KB_TRANSITION_FLAG = 0x80000000;
126 |
127 | override protected bool HandleHookEvent(IntPtr wParam, IntPtr lParam)
128 | {
129 | int key = (int)wParam;
130 | int context = (int)lParam;
131 |
132 | if (KeysToIntercept.Contains(key))
133 | {
134 | bool down = ((context & KB_TRANSITION_FLAG) != KB_TRANSITION_FLAG);
135 | int repeatCount = (context & 0xFF00);
136 | if (down && KeyDown != null)
137 | {
138 | bool handled = false;
139 | KeyDown((Keys)key, repeatCount, ref handled);
140 | return handled;
141 | }
142 | }
143 | return false;
144 | }
145 | }
146 |
147 | public partial class KeyInterceptor //CS-Script plugin specific functionality
148 | {
149 | public static bool IsShortcutPressed(ShortcutKey key)
150 | {
151 | Keys expectedKey = (Keys)key._key;
152 | bool expectedAlt = (key._isAlt != 0);
153 | bool expectedCtrl = (key._isCtrl != 0);
154 | bool expectedShift = (key._isShift != 0);
155 |
156 | if (!KeyInterceptor.IsPressed(expectedKey))
157 | return false;
158 |
159 | if (KeyInterceptor.IsPressed(Keys.ControlKey) == expectedCtrl)
160 | return false;
161 |
162 | if (KeyInterceptor.IsPressed(Keys.ShiftKey) == expectedShift)
163 | return false;
164 |
165 | if (KeyInterceptor.IsPressed(Keys.Menu) == expectedAlt)
166 | return false;
167 |
168 | return true;
169 | }
170 | }
171 | }
--------------------------------------------------------------------------------
/multiclip.ui/Bootstrapper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Runtime.InteropServices;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 | using System.Windows;
9 | using Microsoft.Win32;
10 | using MultiClip.UI.Utils;
11 |
12 | namespace MultiClip.UI
13 | {
14 | public class Bootstrapper
15 | {
16 | private HotKeys hotKeys = HotKeys.Instance;
17 |
18 | public void Run()
19 | {
20 | bool justCreated = SettingsView.EnsureDefaults();
21 |
22 | Func clearAtStartup = () => !SettingsViewModel.Load().RestoreHistoryAtStartup;
23 | ClipboardMonitor.Start(clearAtStartup);
24 |
25 | TrayIcon.ShowHistory = (s, a) => HistoryView.Popup();
26 | TrayIcon.ShowSettings = (s, a) => SettingsView.Popup();
27 | TrayIcon.Rehook = (s, a) => { ClipboardMonitor.Restart(); TrayIcon.RefreshIcon(); };
28 | TrayIcon.Test = (s, a) => ClipboardMonitor.RestartIfFaulty();
29 | TrayIcon.Exit = (s, a) => this.Close();
30 | TrayIcon.Init();
31 |
32 | hotKeys.Start();
33 |
34 | HotKeysMapping.EmbeddedHandlers[HistoryView.PopupActionName] = HistoryView.Popup;
35 | HotKeysMapping.EmbeddedHandlers[ClipboardMonitor.ToPlainTextActionName] = ClipboardMonitor.ToPlainText;
36 | HotKeysMapping.EmbeddedHandlers[HotKeysView.PopupActionName] = HotKeysView.Popup;
37 | HotKeysMapping.EmbeddedHandlers[ClipboardMonitor.RestartActionName] = () => ClipboardMonitor.Restart();
38 |
39 | HotKeysMapping.Bind(hotKeys, TrayIcon.InvokeMenu);
40 |
41 | var timer = new System.Windows.Threading.DispatcherTimer();
42 | var lastCheck = DateTime.Now;
43 |
44 | timer.Tick += (s, e) =>
45 | {
46 |
47 | try
48 | {
49 | // to test the clipboard monitor
50 | var restarted = ClipboardMonitor.RestartIfFaulty();
51 |
52 | if (!restarted)
53 | {
54 | // keeping all three checks even though some of them overlap with each other
55 | var needToRestart = false;
56 |
57 | // if there was no keyboard input for a long time, we can assume that the user is not using the app
58 | // ideally it should be any key press input but at the moment it's actually the time since the last hot key registered
59 | if (HotKeys.Instance.LastKeyInputTime.IntervalFromNow() > TimeSpan.FromMinutes(3))
60 | {
61 | needToRestart = true;
62 | }
63 |
64 | // ensure that after a long sleep we are restarting
65 | if (lastCheck.IntervalFromNow() > TimeSpan.FromMinutes(2))
66 | {
67 | needToRestart = true;
68 | }
69 |
70 | // restart every 3 minutes because... why not? :o)
71 | // this is a bit of a hack to ensure that the app is running
72 | if (Config.RestartingIsEnabled && ClipboardMonitor.LastRestart.IntervalFromNow() > TimeSpan.FromMinutes(3))
73 | {
74 | needToRestart = true;
75 | }
76 |
77 | if (needToRestart)
78 | {
79 | ClipboardMonitor.Restart(true);
80 | }
81 |
82 | // refreshing the icon works but I am not convinced it is beneficial enough to be released
83 | // it also creates a short flickering effect every minute.
84 | // TrayIcon.RefreshIcon();
85 | }
86 | }
87 | catch (Exception ex)
88 | {
89 | Log.WriteLine(ex.ToString());
90 | }
91 | finally
92 | {
93 | lastCheck = DateTime.Now;
94 | }
95 |
96 | };
97 |
98 | timer.Interval = TimeSpan.FromSeconds(30);
99 |
100 | timer.Start();
101 |
102 | if (justCreated)
103 | SettingsView.Popup(); //can pop it up without any side effect only after all messaging is initialized
104 |
105 | SystemEvents.PowerModeChanged += OnPowerChange;
106 | }
107 |
108 | private void OnPowerChange(object s, PowerModeChangedEventArgs e)
109 | {
110 | switch (e.Mode)
111 | {
112 | case PowerModes.Resume:
113 | new Task(() =>
114 | {
115 | Thread.Sleep(5000);
116 | ClipboardMonitor.Restart();
117 | }).Start();
118 | break;
119 | }
120 | }
121 |
122 | private void Close()
123 | {
124 | try
125 | {
126 | ClipboardMonitor.Stop(shutdown: true);
127 | SettingsView.CloseIfAny();
128 | HistoryView.CloseIfAny();
129 | hotKeys.Stop();
130 | TrayIcon.Close();
131 | }
132 | catch { }
133 | }
134 | }
135 | }
--------------------------------------------------------------------------------
/multiclip.ui/HistoryView.xaml:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
24 |
25 |
28 |
29 |
30 |
31 |
32 |
33 |
42 |
43 |
44 |
50 |
51 |
55 |
56 |
57 |
58 |
62 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
83 |
84 |
85 |
86 |
87 |
88 |
94 |
100 |
105 |
106 |
107 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
--------------------------------------------------------------------------------
/multiclip.server/ClipboardWatcher.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
--------------------------------------------------------------------------------
/multiclip.ui/Utils/TrimmingTextBlock.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using System.Windows;
4 | using System.Windows.Controls;
5 | using System.Windows.Media;
6 |
7 | namespace MultiClip.UI
8 | {
9 | ///
10 | /// A TextBlock like control that provides special text trimming logic
11 | /// designed for a file or folder path.
12 | /// Based on Smorgg comment of http://www.codeproject.com/Tips/467054/WPF-PathTrimmingTextBlock.
13 | /// It is extended to dynamically make decision to trim either the end or the middle
14 | ///
15 | ///
16 | public class TrimmingTextBlock : UserControl
17 | {
18 | TextBlock textBlock;
19 |
20 | public TrimmingTextBlock()
21 | {
22 | textBlock = new TextBlock();
23 | AddChild(textBlock);
24 | }
25 |
26 | protected override Size MeasureOverride(Size constraint)
27 | {
28 | if (constraint.Width == 0)
29 | return base.MeasureOverride(constraint);
30 |
31 | base.MeasureOverride(constraint);
32 | // This is where the control requests to be as large
33 | // as is needed while fitting within the given bounds
34 | var meas = TrimToFit(RawText, constraint);
35 |
36 | // Update the text
37 | textBlock.Text = meas.Item1;
38 |
39 | return meas.Item2;
40 | }
41 |
42 | ///
43 | /// Trims the given path until it fits within the given constraints.
44 | ///
45 | /// The path to trim.
46 | /// The size constraint.
47 | /// The trimmed path and its size.
48 | Tuple TrimToFit(string path, Size constraint)
49 | {
50 | if (path == null)
51 | path = "";
52 |
53 | // If the path does not need to be trimmed
54 | // then return immediately
55 | Size size = MeasureString(path);
56 | if (size.Width < constraint.Width)
57 | {
58 | return new Tuple(path, size);
59 | }
60 |
61 | bool trimMiddle = false;
62 |
63 | try
64 | {
65 | if (!path.HasInvalidPathCharacters())
66 | trimMiddle = System.IO.Path.IsPathRooted(path);
67 | }
68 | catch { }
69 |
70 | // Do not perform trimming if the path is not valid
71 | // because the below algorithm will not work
72 | // if we cannot separate the filename from the directory
73 | string rightSide = null;
74 | string leftSide = null;
75 |
76 | if (!trimMiddle)
77 | {
78 | leftSide = path;
79 | }
80 | else
81 | {
82 | try
83 | {
84 | rightSide = System.IO.Path.GetFileName(path) ?? "";
85 | leftSide = System.IO.Path.GetDirectoryName(path) ?? "";
86 | if (leftSide == null)
87 | {
88 | leftSide = path;
89 | trimMiddle = false;
90 | }
91 | }
92 | catch (Exception)
93 | {
94 | return new Tuple(path, size);
95 | }
96 | }
97 |
98 | while (true)
99 | {
100 | if (trimMiddle)
101 | path = $"{leftSide}...\\{rightSide}";
102 | else
103 | path = leftSide + "...";
104 |
105 | size = MeasureString(path);
106 |
107 | if (size.Width <= constraint.Width)
108 | {
109 | // If size is within constraints
110 | // then stop trimming
111 | break;
112 | }
113 |
114 | // Shorten the directory component of the path
115 | // and continue
116 | if (leftSide.Length > 0)
117 | leftSide = leftSide.Substring(0, leftSide.Length - 1);
118 |
119 | if (trimMiddle)
120 | {
121 | // If the directory component is completely gone
122 | // then replace it with ellipses and stop
123 | if (leftSide.Length == 0)
124 | {
125 | path = @"...\" + rightSide;
126 | size = MeasureString(path);
127 | break;
128 | }
129 | }
130 | else
131 | {
132 | if (leftSide.Length <= 5) //5 - something practical
133 | {
134 | path = leftSide + @"...\";
135 | size = MeasureString(path);
136 | break;
137 | }
138 | }
139 | }
140 |
141 | return new Tuple(path, size);
142 | }
143 |
144 | ///
145 | /// Returns the size of the given string if it were to be rendered.
146 | ///
147 | /// The string to measure.
148 | /// The size of the string.
149 | Size MeasureString(string str)
150 | {
151 | var typeFace = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
152 | var text = new FormattedText(str, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeFace, FontSize, Foreground);
153 |
154 | return new Size(text.Width, text.Height);
155 | }
156 |
157 | ///
158 | /// Gets or sets the path to display.
159 | /// The text that is actually displayed will be trimmed appropriately.
160 | ///
161 | public string RawText
162 | {
163 | get { return (string)GetValue(RawTextProperty); }
164 | set { SetValue(RawTextProperty, value); }
165 | }
166 |
167 | public static readonly DependencyProperty RawTextProperty = DependencyProperty.Register("RawText", typeof(string), typeof(TrimmingTextBlock), new UIPropertyMetadata("", OnRawTextChanged));
168 |
169 | static void OnRawTextChanged(DependencyObject o, DependencyPropertyChangedEventArgs args)
170 | {
171 | TrimmingTextBlock @this = (TrimmingTextBlock)o;
172 |
173 | // This element will be re-measured
174 | // The text will be updated during that process
175 | @this.InvalidateMeasure();
176 | }
177 | }
178 | }
--------------------------------------------------------------------------------
/multiclip.ui/Properties/Resources.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
121 |
122 |
123 | ..\Resources\tray_icon.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
124 |
125 |
126 | ..\Resources\tray_icon.black.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
127 |
128 |
--------------------------------------------------------------------------------
/multiclip.server/ClipboardWatcher.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.Drawing;
4 | using System.Runtime.InteropServices;
5 | using System.Text;
6 | using System.Threading;
7 | using System.Windows.Forms;
8 | using Microsoft.Win32;
9 | using MultiClip;
10 |
11 | ///
12 | /// it has to be window (Form) in order to allow access to the WinProc
13 | ///
14 | internal class ClipboardWatcher : Form
15 | {
16 | IntPtr nextClipboardViewer;
17 | static public Action OnClipboardChanged;
18 |
19 | static ClipboardWatcher dialog;
20 |
21 | static public void InUiThread(Action action)
22 | {
23 | if (dialog == null)
24 | action();
25 | else
26 | dialog.Invoke(action);
27 | }
28 |
29 | static bool enabled;
30 |
31 | static new public bool Enabled
32 | {
33 | get
34 | {
35 | return enabled;
36 | }
37 |
38 | set
39 | {
40 | if (enabled != value)
41 | {
42 | enabled = value;
43 | if (enabled)
44 | Start();
45 | else
46 | Stop();
47 | }
48 | }
49 | }
50 |
51 | public static IntPtr WindowHandle;
52 |
53 | static bool started = false;
54 |
55 | static void Start()
56 | {
57 | lock (typeof(ClipboardWatcher))
58 | {
59 | if (!started)
60 | {
61 | started = true;
62 | ThreadPool.QueueUserWorkItem(_ =>
63 | {
64 | try
65 | {
66 | Debug.Assert(dialog == null);
67 | dialog = new ClipboardWatcher();
68 | dialog.Activated += delegate
69 | {
70 | WindowHandle = dialog.Handle;
71 | };
72 | dialog.ShowDialog();
73 | }
74 | catch { }
75 | });
76 | }
77 | }
78 | }
79 |
80 | static void Stop()
81 | {
82 | lock (typeof(ClipboardWatcher))
83 | {
84 | try
85 | {
86 | if (started && dialog != null)
87 | {
88 | InUiThread(dialog.Close);
89 | dialog = null;
90 | started = false;
91 | }
92 | }
93 | catch { }
94 | }
95 | }
96 |
97 | public ClipboardWatcher()
98 | {
99 | var h = Win32.Desktop.GetForegroundWindow();
100 |
101 | this.InitializeComponent();
102 |
103 | this.Load += (s, e) =>
104 | {
105 | Left = -8200;
106 | Init();
107 | };
108 |
109 | this.GotFocus += (s, e) =>
110 | {
111 | Win32.Desktop.SetForegroundWindow(h); //it is important to return the focus back to the desktop window as this one always steals it at startup
112 | };
113 |
114 | this.FormClosed += (s, e) =>
115 | {
116 | Uninit();
117 | };
118 | }
119 |
120 | [DllImport("User32.dll", CharSet = CharSet.Auto)]
121 | public static extern bool ChangeClipboardChain(IntPtr hWndRemove, IntPtr hWndNewNext);
122 |
123 | static internal int ChangesCount = 0;
124 | static internal bool IsTestingMode = false;
125 |
126 | void NotifyChanged()
127 | {
128 | try
129 | {
130 | ChangesCount++;
131 | Console.WriteLine("OnClipboardChanged");
132 | if (!IsTestingMode && OnClipboardChanged != null)
133 | OnClipboardChanged();
134 | }
135 | catch
136 | {
137 | //Debug.Assert(false);
138 | }
139 | }
140 |
141 | protected override void Dispose(bool disposing)
142 | {
143 | try
144 | {
145 | ChangeClipboardChain(base.Handle, this.nextClipboardViewer);
146 | Console.WriteLine("Exited");
147 | base.Dispose(disposing);
148 | }
149 | catch { }
150 | }
151 |
152 | void InitializeComponent()
153 | {
154 | this.SuspendLayout();
155 | //
156 | // ClipboardWatcher
157 | //
158 | this.ClientSize = new System.Drawing.Size(284, 195);
159 | this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
160 | this.Name = "ClipboardWatcher";
161 | this.ShowInTaskbar = false;
162 | this.Text = "MultiClip_ClipboardWatcherWindow";
163 | this.ResumeLayout(false);
164 | }
165 |
166 | [DllImport("user32.dll", CharSet = CharSet.Auto)]
167 | public static extern int SendMessage(IntPtr hwnd, int wMsg, IntPtr wParam, IntPtr lParam);
168 |
169 | [DllImport("User32.dll")]
170 | protected static extern int SetClipboardViewer(int hWndNewViewer);
171 |
172 | void Init()
173 | {
174 | nextClipboardViewer = (IntPtr)SetClipboardViewer((int)Handle);
175 | }
176 |
177 | void Uninit()
178 | {
179 | ChangeClipboardChain(Handle, nextClipboardViewer);
180 | }
181 |
182 | protected override void WndProc(ref Message m)
183 | {
184 | const int WM_DRAWCLIPBOARD = 0x308;
185 | const int WM_CHANGECBCHAIN = 0x030D;
186 | const int WM_ENDSESSION = 0x16;
187 | //const int WM_QUERYENDSESSION = 0x11;
188 |
189 | switch (m.Msg)
190 | {
191 | //case WM_IDLE:
192 | // if (m.WParam == nextClipboardViewer)
193 | case Globals.WM_MULTICLIPTEST:
194 | {
195 | // Debug.Assert(false);
196 |
197 | if (ClipboardHistory.lastSnapshopHash != 0 && ClipboardHistory.lastSnapshopHash != Win32.Clipboard.GetClipboard().GetContentHash())
198 | m.Result = IntPtr.Zero; // stopped receiving clipboard notifications
199 | else
200 | m.Result = (IntPtr)Environment.TickCount;
201 |
202 | break;
203 | }
204 | case WM_ENDSESSION:
205 | Application.Exit();
206 | break;
207 |
208 | case WM_DRAWCLIPBOARD:
209 | NotifyChanged();
210 | SendMessage(nextClipboardViewer, m.Msg, m.WParam, m.LParam);
211 | break;
212 |
213 | case WM_CHANGECBCHAIN:
214 | if (m.WParam == nextClipboardViewer)
215 | {
216 | nextClipboardViewer = m.LParam;
217 | }
218 | else
219 | {
220 | SendMessage(nextClipboardViewer, m.Msg, m.WParam, m.LParam);
221 | }
222 | break;
223 |
224 | default:
225 | base.WndProc(ref m);
226 | break;
227 | }
228 | }
229 | }
--------------------------------------------------------------------------------
/multiclip.ui/Utils.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Reflection;
6 | using System.Runtime.InteropServices;
7 | using System.Text;
8 | using System.Threading;
9 |
10 | namespace MultiClip.Server
11 | {
12 | static class Log
13 | {
14 | static string logFile = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "server.log");
15 |
16 | public static void WriteLine(string message)
17 | {
18 | if (File.Exists(logFile) && new FileInfo(logFile).Length > 100 * 1024) // > 100K
19 | {
20 | if (File.Exists(logFile + ".bak"))
21 | File.Delete(logFile + ".bak");
22 | File.Move(logFile, logFile + ".bak");
23 | }
24 |
25 | File.AppendAllText(logFile, $"{DateTime.Now.ToString("s")}: {message}{Environment.NewLine}");
26 | }
27 | }
28 | }
29 |
30 | class BytesHash
31 | {
32 | public BytesHash()
33 | {
34 | unchecked
35 | {
36 | hash = (int)2166136261;
37 | }
38 | }
39 |
40 | int hash;
41 | const int p = 16777619;
42 |
43 | public BytesHash Add(params byte[] data)
44 | {
45 | unchecked
46 | {
47 | for (int i = 0; i < data.Length; i++)
48 | hash = (hash ^ data[i]) * p;
49 | }
50 | return this;
51 | }
52 |
53 | public int HashCode
54 | {
55 | get
56 | {
57 | hash += hash << 13;
58 | hash ^= hash >> 7;
59 | hash += hash << 3;
60 | hash ^= hash >> 17;
61 | hash += hash << 5;
62 | return hash;
63 | }
64 | }
65 |
66 | public override string ToString()
67 | {
68 | return HashCode.ToString();
69 | }
70 | }
71 |
72 | public static class RenderingExtensions
73 | {
74 | //public static Size MeasureString(string str)
75 | //{
76 | //var typeFace = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
77 | //var text = new FormattedText(str, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeFace, FontSize, Foreground);
78 |
79 | //return new Size(text.Width, text.Height);
80 | //}
81 | //}
82 | }
83 |
84 | //Thread based task that can be canceled without the task action/body processing the cancellation token
85 | public class Async
86 | {
87 | public Thread thread;
88 |
89 | static public Async Run(ThreadStart action)
90 | {
91 | var result = new Async { thread = new Thread(action) };
92 | result.thread.Start();
93 | return result;
94 | }
95 |
96 | public Async WaitFor(int timeout, Action onTimeout = null)
97 | {
98 | if (!thread.Join(timeout))
99 | {
100 | try
101 | {
102 | thread.Abort();
103 | }
104 | catch
105 | {
106 | onTimeout?.Invoke();
107 | }
108 | }
109 | return this;
110 | }
111 | }
112 |
113 | public static class ClipboardExtensions
114 | {
115 | static public IEnumerable ForEach(this IEnumerable collection, Action action)
116 | {
117 | foreach (var item in collection)
118 | {
119 | action(item);
120 | }
121 |
122 | return collection;
123 | }
124 |
125 | public static void TryDeleteDir(this string directory)
126 | {
127 | try
128 | {
129 | Directory.Delete(directory, true);
130 | }
131 | catch { }
132 | }
133 |
134 | public static bool HasInvalidPathCharacters(this string path)
135 | {
136 | return (!string.IsNullOrEmpty(path) && path.IndexOfAny(System.IO.Path.GetInvalidPathChars()) >= 0);
137 | }
138 |
139 | public static string ToAsciiTitle(this byte[] bytes, int max_length = 300)
140 | {
141 | var title = Encoding.ASCII.GetString(bytes.TrimAsciiEnd());
142 | if (title.Length > max_length)
143 | title = title.Substring(0, max_length) + "...";
144 |
145 | title = title.Replace("\n", "").Replace("\r", "");
146 | return title;
147 | }
148 |
149 | public static string ToUnicodeTitle(this byte[] bytes, int max_length = 300)
150 | {
151 | var title = Encoding.Unicode.GetString(bytes.TrimUnicodeEnd());
152 | if (title.Length > max_length)
153 | title = title.Substring(0, max_length) + "...";
154 |
155 | title = title.Replace("\n", "").Replace("\r", "");
156 | return title;
157 | }
158 |
159 | public static byte[] TrimUnicodeEnd(this byte[] bytes)
160 | {
161 | if (bytes.Length > 4 &&
162 | bytes[bytes.Length - 1 - 1] == 0 &&
163 | bytes[bytes.Length - 1 - 2] == 0 &&
164 | bytes[bytes.Length - 1 - 3] == 0 &&
165 | bytes[bytes.Length - 1 - 4] == 0)
166 | return bytes.Take(bytes.Length - 4).ToArray();
167 | else if (bytes.Length > 2 &&
168 | bytes[bytes.Length - 1 - 1] == 0 &&
169 | bytes[bytes.Length - 1 - 2] == 0)
170 | return bytes.Take(bytes.Length - 2).ToArray();
171 | else
172 | return bytes;
173 | }
174 |
175 | public static byte[] TrimAsciiEnd(this byte[] bytes)
176 | {
177 | if (bytes.Length > 2 &&
178 | bytes[bytes.Length - 1 - 1] == 0 &&
179 | bytes[bytes.Length - 1 - 2] == 0)
180 | return bytes.Take(bytes.Length - 2).ToArray();
181 | else
182 | return bytes;
183 | }
184 |
185 | public static string ToReadableHotKey(this string text)
186 | {
187 | if (text.IsEmpty())
188 | return text;
189 | else
190 | return text.Replace("Control", "Ctrl")
191 | .Replace("PrintScreen", "PrtScr")
192 | .Replace("Oem3", "Tilde")
193 | .Replace("Oemtilde", "Tilde");
194 | }
195 |
196 | public static string ToMachineHotKey(this string text)
197 | {
198 | if (text.IsEmpty())
199 | return text;
200 | else
201 | return text.Replace("Ctrl", "Control")
202 | .Replace("PrtScr", "PrintScreen")
203 | .Replace("Tilde", "Oemtilde"); //don't bother with Oem3
204 | }
205 |
206 | public static bool IsEmpty(this string text)
207 | {
208 | return string.IsNullOrWhiteSpace(text);
209 | }
210 |
211 | public static bool IsNotEmpty(this string text)
212 | {
213 | return !string.IsNullOrWhiteSpace(text);
214 | }
215 |
216 | public static bool SameAs(this string text, string text2, bool ignoreCase = false)
217 | {
218 | return string.Equals(text, text2, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.CurrentCulture);
219 | }
220 |
221 | public static string FormatWith(this string text, params object[] args)
222 | {
223 | return string.Format(text, args);
224 | }
225 |
226 | public static int GetHash(this byte[] bytes)
227 | {
228 | return new BytesHash().Add(bytes)
229 | .HashCode;
230 | }
231 |
232 | public static string ToFormatName(this uint format)
233 | {
234 | return System.Windows.Forms.DataFormats.GetFormat((int)format).Name;
235 | }
236 | }
--------------------------------------------------------------------------------
/multiclip.ui/multiclip.ui.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {C1CE4AD5-CB3A-4BCC-A379-9A9515BE86DB}
8 | WinExe
9 | Properties
10 | MultiClip.UI
11 | multiclip.ui
12 | v4.8
13 | 512
14 | {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
15 | 4
16 |
17 |
18 |
19 | x86
20 | true
21 | full
22 | false
23 | bin\Debug\
24 | DEBUG;TRACE
25 | prompt
26 | 4
27 | false
28 | false
29 |
30 |
31 | x86
32 | pdbonly
33 | true
34 | bin\Release\
35 | TRACE
36 | prompt
37 | 4
38 | false
39 |
40 |
41 | Resources\tray_icon.black.ico
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | 4.0
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | MSBuild:Compile
65 | Designer
66 |
67 |
68 | Globals.cs
69 |
70 |
71 | Properties\AssemblyVersion.cs
72 |
73 |
74 |
75 |
76 |
77 |
78 | HotKeyEditor.xaml
79 |
80 |
81 |
82 | HotKeysView.xaml
83 |
84 |
85 |
86 |
87 |
88 | SettingsView.xaml
89 |
90 |
91 |
92 |
93 | MSBuild:Compile
94 | Designer
95 |
96 |
97 | App.xaml
98 | Code
99 |
100 |
101 |
102 | HistoryView.xaml
103 | Code
104 |
105 |
106 | Designer
107 | MSBuild:Compile
108 | true
109 |
110 |
111 | Designer
112 | MSBuild:Compile
113 |
114 |
115 | Designer
116 | MSBuild:Compile
117 |
118 |
119 | Designer
120 | MSBuild:Compile
121 |
122 |
123 | Designer
124 | MSBuild:Compile
125 |
126 |
127 |
128 |
129 |
130 | Code
131 |
132 |
133 | True
134 | Settings.settings
135 | True
136 |
137 |
138 |
139 | SettingsSingleFileGenerator
140 | Settings.Designer.cs
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 | {6da643c6-e46f-4713-87a1-8db62eb66c9a}
162 | MultiClip.Server
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
177 |
--------------------------------------------------------------------------------
/multiclip.server/Utils.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Reflection;
6 | using System.Runtime.InteropServices;
7 | using System.Text;
8 | using System.Threading;
9 |
10 | namespace MultiClip.Server
11 | {
12 | static class Log
13 | {
14 | static string DataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MultiClip.History", "Data");
15 | static string logFile = Path.Combine(DataDir, @"..\server.log");
16 |
17 | public static void WriteLine(string message)
18 | {
19 | if (File.Exists(logFile) && new FileInfo(logFile).Length > 100 * 1024) // > 100K
20 | {
21 | if (File.Exists(logFile + ".bak"))
22 | File.Delete(logFile + ".bak");
23 | File.Move(logFile, logFile + ".bak");
24 | }
25 |
26 | File.AppendAllText(logFile, $"{DateTime.Now.ToString("s")}: {message}{Environment.NewLine}");
27 | }
28 | }
29 |
30 | static class Operations
31 | {
32 | public static void MsgBox(string message, string caption)
33 | {
34 | if (Environment.GetEnvironmentVariable("UNDER_CHOCO").IsEmpty())
35 | System.Windows.Forms.MessageBox.Show(message, caption);
36 | }
37 | }
38 | }
39 |
40 | class BytesHash
41 | {
42 | public BytesHash()
43 | {
44 | unchecked
45 | {
46 | hash = (int)2166136261;
47 | }
48 | }
49 |
50 | int hash;
51 | const int p = 16777619;
52 |
53 | public BytesHash Add(params byte[] data)
54 | {
55 | unchecked
56 | {
57 | for (int i = 0; i < data.Length; i++)
58 | hash = (hash ^ data[i]) * p;
59 | }
60 | return this;
61 | }
62 |
63 | public int HashCode
64 | {
65 | get
66 | {
67 | hash += hash << 13;
68 | hash ^= hash >> 7;
69 | hash += hash << 3;
70 | hash ^= hash >> 17;
71 | hash += hash << 5;
72 | return hash;
73 | }
74 | }
75 |
76 | public override string ToString()
77 | {
78 | return HashCode.ToString();
79 | }
80 | }
81 |
82 | //Thread based task that can be canceled without the task action/body processing the cancellation token
83 | public class Async
84 | {
85 | public Thread thread;
86 |
87 | static public Async Run(ThreadStart action)
88 | {
89 | var result = new Async { thread = new Thread(action) };
90 | result.thread.Start();
91 | return result;
92 | }
93 |
94 | public Async WaitFor(int timeout, Action onTimeout = null)
95 | {
96 | if (!thread.Join(timeout))
97 | {
98 | try
99 | {
100 | thread.Abort();
101 | }
102 | catch
103 | {
104 | onTimeout?.Invoke();
105 | }
106 | }
107 | return this;
108 | }
109 | }
110 |
111 | public static class ClipboardExtensions
112 | {
113 | public static ulong GetContentHash(this Dictionary data)
114 | {
115 | ulong checksum = 0;
116 |
117 | unchecked
118 | {
119 | foreach (uint i in data.Keys)
120 | checksum = checksum ^ i;
121 |
122 | foreach (byte[] value in data.Values)
123 | for (int i = 0; i < value.Length; i++)
124 | checksum = checksum ^ value[i];
125 | return checksum;
126 | }
127 | }
128 |
129 | static public IEnumerable ForEach(this IEnumerable collection, Action action)
130 | {
131 | foreach (var item in collection)
132 | {
133 | action(item);
134 | }
135 |
136 | return collection;
137 | }
138 |
139 | public static void TryDeleteDir(this string directory)
140 | {
141 | try
142 | {
143 | Directory.Delete(directory, true);
144 | }
145 | catch { }
146 | }
147 |
148 | public static bool HasInvalidPathCharacters(this string path)
149 | {
150 | return (!string.IsNullOrEmpty(path) && path.IndexOfAny(System.IO.Path.GetInvalidPathChars()) >= 0);
151 | }
152 |
153 | public static string ToAsciiTitle(this byte[] bytes, int max_length = 300)
154 | {
155 | var title = Encoding.ASCII.GetString(bytes.TrimAsciiEnd());
156 | if (title.Length > max_length)
157 | title = title.Substring(0, max_length) + "...";
158 |
159 | title = title.Replace("\n", "").Replace("\r", "");
160 | return title;
161 | }
162 |
163 | public static string ToUnicodeTitle(this byte[] bytes, int max_length = 300)
164 | {
165 | var title = Encoding.Unicode.GetString(bytes.TrimUnicodeEnd());
166 | if (title.Length > max_length)
167 | title = title.Substring(0, max_length) + "...";
168 |
169 | title = title.Replace("\n", "").Replace("\r", "");
170 | return title;
171 | }
172 |
173 | public static byte[] TrimUnicodeEnd(this byte[] bytes)
174 | {
175 | if (bytes.Length > 4 &&
176 | bytes[bytes.Length - 1 - 1] == 0 &&
177 | bytes[bytes.Length - 1 - 2] == 0 &&
178 | bytes[bytes.Length - 1 - 3] == 0 &&
179 | bytes[bytes.Length - 1 - 4] == 0)
180 | return bytes.Take(bytes.Length - 4).ToArray();
181 | else if (bytes.Length > 2 &&
182 | bytes[bytes.Length - 1 - 1] == 0 &&
183 | bytes[bytes.Length - 1 - 2] == 0)
184 | return bytes.Take(bytes.Length - 2).ToArray();
185 | else
186 | return bytes;
187 | }
188 |
189 | public static byte[] TrimAsciiEnd(this byte[] bytes)
190 | {
191 | if (bytes.Length > 2 &&
192 | bytes[bytes.Length - 1 - 1] == 0 &&
193 | bytes[bytes.Length - 1 - 2] == 0)
194 | return bytes.Take(bytes.Length - 2).ToArray();
195 | else
196 | return bytes;
197 | }
198 |
199 | public static string ToReadableHotKey(this string text)
200 | {
201 | if (text.IsEmpty())
202 | return text;
203 | else
204 | return text.Replace("Control", "Ctrl")
205 | .Replace("PrintScreen", "PrtScr")
206 | .Replace("Oem3", "Tilde")
207 | .Replace("Oemtilde", "Tilde");
208 | }
209 |
210 | public static string ToMachineHotKey(this string text)
211 | {
212 | if (text.IsEmpty())
213 | return text;
214 | else
215 | return text.Replace("Ctrl", "Control")
216 | .Replace("PrtScr", "PrintScreen")
217 | .Replace("Tilde", "Oemtilde"); //don't bother with Oem3
218 | }
219 |
220 | public static bool IsEmpty(this string text)
221 | {
222 | return string.IsNullOrWhiteSpace(text);
223 | }
224 | public static string EscapePath(this string text)
225 | {
226 | Path.GetInvalidFileNameChars().ForEach(c => text = text.Replace(c.ToString(), "_"));
227 | return text;
228 | }
229 |
230 | public static bool IsNotEmpty(this string text)
231 | {
232 | return !string.IsNullOrWhiteSpace(text);
233 | }
234 |
235 | public static void DeleteIfDiExists(this string path)
236 | {
237 | if (path.IsNotEmpty() && Directory.Exists(path))
238 | Directory.Delete(path, true);
239 | }
240 |
241 | public static bool SameAs(this string text, string text2, bool ignoreCase = false)
242 | {
243 | return string.Equals(text, text2, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.CurrentCulture);
244 | }
245 |
246 | public static string FormatWith(this string text, params object[] args)
247 | {
248 | return string.Format(text, args);
249 | }
250 |
251 | public static int GetHash(this byte[] bytes)
252 | {
253 | return new BytesHash().Add(bytes)
254 | .HashCode;
255 | }
256 |
257 | public static string ToFormatName(this uint format)
258 | {
259 | return System.Windows.Forms.DataFormats.GetFormat((int)format).Name;
260 | }
261 | }
--------------------------------------------------------------------------------
/multiclip.ui/ClipboardMonitor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Reflection;
6 | using System.Runtime.ExceptionServices;
7 | using System.Runtime.InteropServices;
8 | using System.Text;
9 | using System.Threading;
10 | using System.Threading.Tasks;
11 | using System.Windows;
12 | using System.Windows.Forms;
13 | using Microsoft.Win32;
14 |
15 | namespace MultiClip.UI
16 | {
17 | internal class ClipboardMonitor
18 | {
19 | private static string multiClipServerExe = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "multiclip.server.exe");
20 |
21 | static ClipboardMonitor()
22 | {
23 | SystemEvents.PowerModeChanged += OnPowerChange;
24 | SystemEvents.SessionEnded += SystemEvents_SessionEnded;
25 | SystemEvents.SessionEnding += SystemEvents_SessionEnding;
26 |
27 | try
28 | {
29 | Stop();
30 | }
31 | catch { }
32 | }
33 |
34 | private static void SystemEvents_SessionEnded(object sender, SessionEndedEventArgs e)
35 | {
36 | Log.WriteLine("SystemEvents_SessionEnded");
37 | OnSysShutdown(null, null);
38 | }
39 |
40 | private static void SystemEvents_SessionEnding(object sender, SessionEndingEventArgs e)
41 | {
42 | Log.WriteLine("SystemEvents_SessionEnding");
43 | OnSysShutdown(null, null);
44 | }
45 |
46 | public static int ServerRecoveryDelay = 3000;
47 |
48 | static bool firstRun = true;
49 |
50 | public static void Start(Func clear)
51 | {
52 | KillAllServers();
53 | Thread.Sleep(1000);
54 | Task.Factory.StartNew(() =>
55 | {
56 | while (!stopping && !shutdownRequested)
57 | {
58 | try
59 | {
60 | if (firstRun)
61 | {
62 | firstRun = false;
63 | var clearHistory = clear();
64 | Debug.Assert(clearHistory == false, "Multiclip UI is requesting clearing the history.");
65 | StartServer("-start " + (clear() ? "-clearall" : "")).WaitForExit();
66 | }
67 | else
68 | {
69 | StartServer("-start").WaitForExit();
70 | }
71 |
72 | if (IsScheduledRestart)
73 | {
74 | Log.WriteLine($"Restart is requested.");
75 | IsScheduledRestart = false;
76 | }
77 | else
78 | {
79 | Log.WriteLine($"Unexpected server exit.");
80 | }
81 |
82 | // KillAllServers();
83 |
84 | // if the server exited because of the system shutdown
85 | // let some time so UI also processes shutdown event.
86 | Thread.Sleep(ServerRecoveryDelay);
87 |
88 | //it crashed or was killed so resurrect it in the next loop
89 | }
90 | catch { }
91 | }
92 | });
93 | }
94 |
95 | private static Process StartServer(string args)
96 | {
97 | Log.WriteLine($"Starting server");
98 | var p = new Process();
99 |
100 | p.StartInfo.FileName = multiClipServerExe;
101 | p.StartInfo.Arguments = args;
102 | p.StartInfo.UseShellExecute = false;
103 | p.StartInfo.RedirectStandardOutput = true;
104 | p.StartInfo.CreateNoWindow = true;
105 | p.Start();
106 |
107 | return p;
108 | }
109 |
110 | private static bool shutdownRequested = false;
111 | private static bool stopping = false;
112 |
113 | public static void Stop(bool shutdown = false)
114 | {
115 | stopping = true;
116 | shutdownRequested = shutdown;
117 |
118 | Log.WriteLine($"Stop(shutdown: {shutdown})");
119 |
120 | if (shutdown)
121 | SystemEvents.PowerModeChanged -= OnPowerChange;
122 |
123 | var runningServers = Process.GetProcessesByName(Path.GetFileNameWithoutExtension(multiClipServerExe));
124 | if (runningServers.Any())
125 | {
126 | using (var closeRequest = new EventWaitHandle(false, EventResetMode.ManualReset, Globals.CloseRequestName))
127 | closeRequest.Set();
128 |
129 | Parallel.ForEach(runningServers, server =>
130 | {
131 | try
132 | {
133 | server.WaitForExit(200);
134 | if (!server.HasExited)
135 | server.Kill();
136 | }
137 | catch { }
138 | });
139 |
140 | Thread.Sleep(200);
141 | }
142 | stopping = false;
143 | }
144 |
145 | public static void KillAllServers()
146 | {
147 | var runningServers = Process.GetProcessesByName(Path.GetFileNameWithoutExtension(multiClipServerExe));
148 | if (runningServers.Any())
149 | foreach (var server in runningServers)
150 | {
151 | try
152 | {
153 | using (var closeRequest = new EventWaitHandle(false, EventResetMode.ManualReset, Globals.CloseRequestName))
154 | closeRequest.Set();
155 |
156 | server.WaitForExit(200);
157 | if (!server.HasExited)
158 | server.Kill();
159 | }
160 | catch { }
161 | }
162 | }
163 |
164 | public static string ToPlainTextActionName = "";
165 | public static string RestartActionName = "";
166 |
167 | public static void ToPlainText()
168 | {
169 | Task.Factory.StartNew(() =>
170 | {
171 | try
172 | {
173 | StartServer("-toplaintext");
174 | }
175 | catch { }
176 | });
177 | }
178 |
179 | internal static bool IsScheduledRestart = false;
180 |
181 | public static void Restart(bool isScheduledRestart = false)
182 | {
183 | IsScheduledRestart = isScheduledRestart;
184 | LastRestart = DateTime.Now;
185 | try
186 | {
187 | Log.Enabled = false;
188 | // it will kill any active instance and the monitor loop will
189 | // restart the server automatically.
190 | KillAllServers();
191 |
192 | Task.Factory.StartNew(() =>
193 | {
194 | Thread.Sleep(ServerRecoveryDelay + 1000);
195 | Log.Enabled = true;
196 | });
197 | }
198 | catch { }
199 | }
200 |
201 | [DllImport("user32.dll", SetLastError = true)]
202 | public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
203 |
204 | [DllImport("user32.dll", CharSet = CharSet.Auto)]
205 | public static extern int SendMessage(IntPtr hwnd, int wMsg, IntPtr wParam, IntPtr lParam);
206 |
207 | public static DateTime LastRestart = DateTime.Now;
208 |
209 | public static bool RestartIfFaulty()
210 | {
211 | bool restarted = false;
212 |
213 | var wnd = FindWindow(null, Globals.ClipboardWatcherWindow);
214 | if (wnd != null)
215 | {
216 | // prime the clipboard monitor channel with a test content
217 | bool success = TestClipboard(wnd);
218 |
219 | if (!success)
220 | {
221 | Restart();
222 | restarted = true;
223 | }
224 | }
225 |
226 | return restarted;
227 | }
228 |
229 | private static bool TestClipboard(IntPtr wnd)
230 | {
231 | var success = (0 != SendMessage(wnd, Globals.WM_MULTICLIPTEST, IntPtr.Zero, IntPtr.Zero));
232 | // or do some test clipboard read/write
233 | return success;
234 | }
235 |
236 | public static void ClearAll()
237 | {
238 | Directory.GetDirectories(Globals.DataDir, "*", SearchOption.TopDirectoryOnly)
239 | .ForEach(dir => dir.TryDeleteDir());
240 | }
241 |
242 | public static void ClearDuplicates()
243 | {
244 | try
245 | {
246 | StartServer("-purge");
247 | }
248 | catch { }
249 | }
250 |
251 | public static void LoadSnapshot(string bufferLocation)
252 | {
253 | try
254 | {
255 | StartServer($"\"-load:{bufferLocation}").WaitForExit();
256 |
257 | if (SettingsViewModel.Load().PasteAfterSelection ||
258 | System.Windows.Input.Keyboard.IsKeyDown(System.Windows.Input.Key.LeftCtrl))
259 | Task.Run(() =>
260 | {
261 | Thread.Sleep(100);
262 |
263 | Desktop.FireKeyInput(System.Windows.Forms.Keys.V, System.Windows.Forms.Keys.ControlKey);
264 | });
265 | }
266 | catch
267 | {
268 | }
269 | }
270 |
271 | private static void OnPowerChange(object s, PowerModeChangedEventArgs e)
272 | {
273 | switch (e.Mode)
274 | {
275 | case PowerModes.Resume:
276 | Restart();
277 | break;
278 | }
279 | }
280 |
281 | private static void OnSysShutdown(object s, SessionEndedEventArgs e)
282 | {
283 | Log.WriteLine($"System shutdown detected. Stopping the server.");
284 | Stop(shutdown: true);
285 | }
286 | }
287 | }
288 |
289 | class Desktop
290 | {
291 | [DllImport("user32.dll")]
292 | internal static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);
293 |
294 | internal const int KEYEVENTF_KEYUP = 0x02;
295 | internal const int KEYEVENTF_KEYDOWN = 0x00;
296 |
297 | public static void FireKeyInput(Keys key, params Keys[] modifiers)
298 | {
299 | foreach (Keys k in modifiers)
300 | keybd_event((byte)k, 0x45, KEYEVENTF_KEYDOWN, UIntPtr.Zero);
301 |
302 | keybd_event((byte)key, 0x45, KEYEVENTF_KEYDOWN, UIntPtr.Zero);
303 |
304 | Thread.Sleep(10);
305 |
306 | keybd_event((byte)key, 0x45, KEYEVENTF_KEYUP, UIntPtr.Zero);
307 |
308 | foreach (Keys k in modifiers)
309 | keybd_event((byte)k, 0x45, KEYEVENTF_KEYUP, UIntPtr.Zero);
310 | }
311 | }
--------------------------------------------------------------------------------
/multiclip.server/Clipboard.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Reflection;
6 | using System.Runtime.ExceptionServices;
7 | using System.Runtime.InteropServices;
8 | using System.Security;
9 | using System.Text;
10 | using NClipboard = System.Windows.Forms.Clipboard;
11 |
12 | namespace Win32
13 | {
14 | public class Desktop
15 | {
16 | [DllImport("user32.dll")]
17 | public static extern IntPtr SetForegroundWindow(IntPtr hWnd);
18 |
19 | [DllImport("user32.dll")]
20 | public static extern IntPtr GetForegroundWindow();
21 | }
22 |
23 | public static class Clipboard
24 | {
25 | [DllImport("user32.dll")]
26 | static extern bool OpenClipboard(IntPtr hWndNewOwner);
27 |
28 | [DllImport("user32.dll")]
29 | static extern bool CloseClipboard();
30 |
31 | [DllImport("user32.dll")]
32 | static extern bool EmptyClipboard();
33 |
34 | [DllImport("user32.dll")]
35 | static extern uint EnumClipboardFormats(uint format);
36 |
37 | [DllImport("user32.dll")]
38 | static extern IntPtr GetClipboardData(uint uFormat);
39 |
40 | [DllImport("user32.dll")]
41 | static extern IntPtr SetClipboardData(uint uFormat, IntPtr hMem);
42 |
43 | [DllImport("kernel32.dll")]
44 | static extern IntPtr GlobalAlloc(uint uFlags, UIntPtr dwBytes);
45 |
46 | [DllImport("kernel32.dll")]
47 | static extern IntPtr GlobalLock(IntPtr hMem);
48 |
49 | [DllImport("kernel32.dll")]
50 | static extern IntPtr GlobalUnlock(IntPtr hMem);
51 |
52 | [DllImport("kernel32.dll")]
53 | static extern IntPtr GlobalFree(IntPtr hMem);
54 |
55 | [DllImport("kernel32.dll")]
56 | static extern UIntPtr GlobalSize(IntPtr hMem);
57 |
58 | const uint GMEM_DDESHARE = 0x2000;
59 | const uint GMEM_MOVEABLE = 0x2;
60 |
61 | public static void WithFormats(Action handler)
62 | {
63 | uint num = 0u;
64 | while ((num = EnumClipboardFormats(num)) != 0)
65 | {
66 | handler(num);
67 | }
68 | }
69 |
70 | public class LastSessionErrorDetectedException : Exception
71 | {
72 | public LastSessionErrorDetectedException()
73 | {
74 | }
75 |
76 | public LastSessionErrorDetectedException(string message) : base(message)
77 | {
78 | }
79 | }
80 |
81 | // UI will use this static class for browsing history and `GetExecutingAssembly` may return invalid path (GAC)
82 | // but because server is always invoked as a process then it is safe to use `GetEntryAssembly`.
83 | public static string DataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MultiClip.History", "Data");
84 |
85 | static string ErrorLog = Path.Combine(DataDir, @"..\reading.log");
86 |
87 | static void BackupLog(string logFile)
88 | {
89 | if (File.Exists(logFile))
90 | {
91 | bool hasReadingErrors = File.ReadAllLines(logFile)
92 | .Any(x => x.EndsWith("started -> ")
93 | || x.EndsWith("Error"));
94 |
95 | if (hasReadingErrors)
96 | {
97 | PurgeErrors(logFile);
98 |
99 | var lastErrorLog = logFile + Guid.NewGuid() + ".error.log";
100 | File.Move(logFile, lastErrorLog);
101 |
102 | throw new LastSessionErrorDetectedException(lastErrorLog);
103 | }
104 |
105 | if (!ErrorLogIsPurged)
106 | {
107 | ErrorLogIsPurged = true;
108 | PurgeErrors(logFile);
109 | }
110 | }
111 | }
112 |
113 | static void PurgeErrors(string logFile)
114 | {
115 | try
116 | {
117 | foreach (var oldLog in Directory
118 | .GetFiles(Path.GetDirectoryName(logFile), "*.error.log")
119 | .OrderByDescending(x => x)
120 | .Skip(10))
121 | {
122 | try
123 | {
124 | File.Delete(oldLog);
125 | }
126 | catch { }
127 | }
128 | }
129 | catch { }
130 | }
131 |
132 | static bool ErrorLogIsPurged = false;
133 |
134 | static uint[] ignoreCipboardFormats = new uint[]
135 | {
136 | 49466, // (InShellDragLoop)
137 | 50417, // (PowerPoint 12.0 Internal Theme)
138 | 50418, // (PowerPoint 12.0 Internal Color Scheme)
139 | 50416, // (Art::Text ClipFormat)
140 | 50378, // (Art::Table ClipFormat)
141 | 49171, // (Ole Private Data) // not sure if not having this one is 100% acceptable
142 | 14, // (EnhancedMetafile)
143 | 3, // CF_METAFILEPICT - upsets Excel
144 | };
145 |
146 | public static Dictionary GetClipboard()
147 | {
148 | BackupLog(ErrorLog);
149 |
150 | using (var readingLog = new StreamWriter(ErrorLog))
151 | {
152 | readingLog.WriteLine(ErrorLog);
153 |
154 | var result = new Dictionary();
155 | try
156 | {
157 | if (OpenClipboard(ClipboardWatcher.WindowHandle))
158 | {
159 | try
160 | {
161 | WithFormats(delegate (uint format)
162 | {
163 | // zos
164 | // skipping nasty formats as well as delaying the making snapshot (ClipboardHistory.cs:78)
165 | // seems to help with unhanded Win32 exceptions
166 | if (ignoreCipboardFormats.Contains(format))
167 | return;
168 |
169 | try
170 | {
171 | readingLog.Write($"Reading {format} ({format.ToFormatName()}): started -> ");
172 | readingLog.Flush();
173 | byte[] bytes = GetBytes(format);
174 | if (bytes != null)
175 | {
176 | result[format] = bytes;
177 | }
178 | readingLog.WriteLine("OK");
179 | }
180 | catch
181 | {
182 | readingLog.WriteLine("Error");
183 | }
184 | try { readingLog.Flush(); } catch { }
185 | });
186 | }
187 | finally
188 | {
189 | CloseClipboard();
190 | try { readingLog.Flush(); } catch { }
191 | }
192 | }
193 | }
194 | catch
195 | {
196 | }
197 |
198 | return result;
199 | }
200 | }
201 |
202 | public static string[] GetDropFiles(byte[] bytes)
203 | {
204 | var result = new List();
205 |
206 | var buf = new StringBuilder(bytes.Length);
207 |
208 | IntPtr mem = Marshal.AllocHGlobal(bytes.Length);
209 | try
210 | {
211 | Marshal.Copy(bytes, 0, mem, bytes.Length);
212 |
213 | var count = DragQueryFile(mem, uint.MaxValue, null, 0);
214 | for (uint i = 0; i < count; i++)
215 | if (0 < DragQueryFile(mem, i, buf, buf.Capacity))
216 | result.Add(buf.ToString());
217 | }
218 | finally
219 | {
220 | Marshal.FreeHGlobal(mem);
221 | }
222 | return result.ToArray();
223 | }
224 |
225 | public static byte[] GetBytes(uint format)
226 | {
227 | IntPtr pos = IntPtr.Zero;
228 | try
229 | {
230 | pos = GetClipboardData(format);
231 | if (pos != IntPtr.Zero)
232 | {
233 | IntPtr gLock = GlobalLock(pos);
234 | if (gLock == IntPtr.Zero)
235 | return null;
236 |
237 | var length = (int)GlobalSize(pos);
238 | if (length > 0)
239 | {
240 | var buffer = new byte[length];
241 | Marshal.Copy(gLock, buffer, 0, length);
242 | return buffer;
243 | }
244 | }
245 | return null;
246 | }
247 | finally
248 | {
249 | if (pos != IntPtr.Zero)
250 | try { GlobalUnlock(pos); }
251 | catch { }
252 | }
253 | }
254 |
255 | public static IEnumerable GetFormats()
256 | {
257 | var result = new List();
258 | uint format = 0;
259 | while ((format = EnumClipboardFormats(format)) != 0)
260 | result.Add(format);
261 | return result;
262 | }
263 |
264 | static public void SetBytes(uint format, byte[] data)
265 | {
266 | if (data.Length > 0)
267 | {
268 | IntPtr alloc = GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, (UIntPtr)data.Length);
269 | if (alloc != IntPtr.Zero)
270 | {
271 | IntPtr gLock = GlobalLock(alloc);
272 | if (gLock != IntPtr.Zero)
273 | {
274 | Marshal.Copy(data, 0, gLock, data.Length);
275 | GlobalUnlock(alloc);
276 |
277 | SetClipboardData(format, alloc);
278 |
279 | GlobalFree(alloc);
280 | }
281 | }
282 | }
283 | }
284 |
285 | [DllImport("shell32.dll", CharSet = CharSet.Auto)]
286 | private static extern int DragQueryFile(IntPtr hDrop, uint iFile, StringBuilder lpszFile, int cch);
287 |
288 | static public void SetText(string text)
289 | {
290 | try
291 | {
292 | NClipboard.SetText(text);
293 | }
294 | catch { }
295 | }
296 |
297 | static public void ToPlainText()
298 | {
299 | try
300 | {
301 | if (NClipboard.ContainsText())
302 | {
303 | string text = NClipboard.GetText();
304 | NClipboard.SetText(text); //all formatting (e.g. RTF) will be removed
305 | }
306 | }
307 | catch { }
308 | }
309 |
310 | static public void SetClipboard(Dictionary data)
311 | {
312 | try
313 | {
314 | if (OpenClipboard(IntPtr.Zero))
315 | {
316 | EmptyClipboard();
317 | foreach (var format in data.Keys)
318 | SetBytes(format, data[format]);
319 |
320 | CloseClipboard();
321 | }
322 | }
323 | catch { }
324 | }
325 | }
326 | }
--------------------------------------------------------------------------------
/multiclip.ui/Utils/AutoBinder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.ComponentModel;
3 | using System.Linq;
4 | using System.Linq.Expressions;
5 | using System.Reflection;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 | using System.Windows;
9 | using System.Windows.Controls;
10 | using System.Windows.Controls.Primitives;
11 | using System.Windows.Data;
12 | using System.Windows.Media;
13 | using System.Windows.Threading;
14 |
15 | namespace MultiClip.UI
16 | {
17 | ///
18 | /// Extremely simplistic Claiburn.Micro binder replacement. Deployment pressure is too strong to justify
19 | /// introducing Caliburn and other decencies
20 | ///
21 | static class AutoBinder
22 | {
23 | public static void BindOnLoad(FrameworkElement element, object model)
24 | {
25 | //Note: Trying to call Bind for Window may yield 0 visual children
26 | //if you it is called from the constructor (even after InitializeComponent()).
27 | //This is because the visual children aren't loaded yet.
28 |
29 | if (element.IsLoaded)
30 | Bind((DependencyObject)element, model); //typecast to avoid re-entrance
31 | else
32 | element.Loaded += (s, e) => BindOnLoad(element, model);
33 | }
34 |
35 | public static void Bind(DependencyObject element, object model)
36 | {
37 | if (element != null)
38 | {
39 | if (element is FrameworkElement)
40 | (element as FrameworkElement).DataContext = model;
41 |
42 | for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
43 | {
44 | DependencyObject child = VisualTreeHelper.GetChild(element, i);
45 | if (child != null && child is FrameworkElement)
46 | BindElement((FrameworkElement)child, model);
47 | Bind(child, model);
48 | }
49 | }
50 | }
51 |
52 | public static void BindElement(FrameworkElement element, object model)
53 | {
54 | if (string.IsNullOrEmpty(element.Name))
55 | return;
56 |
57 | MemberInfo modelMember = model.GetType().GetMember(element.Name).FirstOrDefault();
58 |
59 | if (modelMember == null)
60 | return;
61 |
62 | MemberInfo modelPropEnabled = model.GetType()
63 | .GetMembers()
64 | .OfType()
65 | .Where(x => x.PropertyType == typeof(bool) && (x.Name == "Can" + element.Name || x.Name == element.Name + "Enabled"))
66 | .FirstOrDefault();
67 |
68 | string singularName = element.Name.TrimEnd('s');
69 |
70 | MemberInfo modelPropSelected = model.GetType()
71 | .GetMembers()
72 | .OfType()
73 | .Where(x => (x.Name == "Current" + singularName || x.Name == "Selected" + singularName))
74 | .FirstOrDefault();
75 |
76 | var modelMethod = modelMember as MethodInfo;
77 | var modelProp = modelMember as PropertyInfo;
78 |
79 | if (element is ItemsControl)
80 | {
81 | var selector = element as Selector;
82 | var control = element as ItemsControl;
83 | var prop = ItemsControl.ItemsSourceProperty;
84 |
85 | if (modelProp != null && element.GetBindingExpression(prop) == null)
86 | control.SetBinding(prop, new Binding(modelProp.Name) { Source = model });
87 |
88 | if (selector != null && modelPropSelected != null && element.GetBindingExpression(Selector.SelectedItemProperty) == null)
89 | control.SetBinding(Selector.SelectedItemProperty, new Binding(modelPropSelected.Name) { Source = model, Mode = BindingMode.TwoWay });
90 |
91 | if (modelPropEnabled != null && element.GetBindingExpression(UIElement.IsEnabledProperty) == null)
92 | control.SetBinding(UIElement.IsEnabledProperty, new Binding(modelPropEnabled.Name) { Source = model });
93 | }
94 |
95 | else if (element is TextBlock)
96 | {
97 | var control = element as TextBlock;
98 | var prop = TextBlock.TextProperty;
99 |
100 | if (modelProp != null && element.GetBindingExpression(prop) == null)
101 | control.SetBinding(prop, new Binding(modelProp.Name) { Source = model });
102 |
103 | if (modelPropEnabled != null && element.GetBindingExpression(UIElement.IsEnabledProperty) == null)
104 | control.SetBinding(UIElement.IsEnabledProperty, new Binding(modelPropEnabled.Name) { Source = model });
105 | }
106 | else if (element is TextBox)
107 | {
108 | var control = element as TextBox;
109 | var prop = TextBox.TextProperty;
110 |
111 | if (modelProp != null && element.GetBindingExpression(prop) == null)
112 | control.SetBinding(prop, new Binding(modelProp.Name) { Source = model, Mode = BindingMode.TwoWay });
113 |
114 | if (modelPropEnabled != null && element.GetBindingExpression(UIElement.IsEnabledProperty) == null)
115 | control.SetBinding(UIElement.IsEnabledProperty, new Binding(modelPropEnabled.Name) { Source = model, Mode = BindingMode.OneWay});
116 | }
117 | else if (element is CheckBox)
118 | {
119 | var control = element as CheckBox;
120 | var prop = CheckBox.IsCheckedProperty;
121 |
122 | if (modelProp != null && element.GetBindingExpression(prop) == null)
123 | control.SetBinding(prop, new Binding(modelProp.Name) { Source = model, Mode = BindingMode.TwoWay });
124 |
125 | if (modelPropEnabled != null && element.GetBindingExpression(UIElement.IsEnabledProperty) == null)
126 | control.SetBinding(UIElement.IsEnabledProperty, new Binding(modelPropEnabled.Name) { Source = model });
127 | }
128 | else if (element is RadioButton)
129 | {
130 | var control = element as RadioButton;
131 | var prop = RadioButton.IsCheckedProperty;
132 |
133 | if (modelProp != null && element.GetBindingExpression(prop) == null)
134 | control.SetBinding(prop, new Binding(modelProp.Name) { Source = model, Mode = BindingMode.TwoWay });
135 |
136 | if (modelPropEnabled != null && element.GetBindingExpression(UIElement.IsEnabledProperty) == null)
137 | control.SetBinding(UIElement.IsEnabledProperty, new Binding(modelPropEnabled.Name) { Source = model });
138 | }
139 | else if (element is ButtonBase) //important to have it as the last 'if clause' as otherwise it will collect all check boxes and radio buttons
140 | {
141 | var control = element as ButtonBase;
142 |
143 | if (modelMethod != null)
144 | {
145 | ParameterInfo[] paramsInfo = modelMethod.GetParameters();
146 |
147 | control.Click += (sender, e) =>
148 | {
149 | object[] @params = new object[paramsInfo.Length];
150 |
151 | for (int i = 0; i < paramsInfo.Length; i++)
152 | {
153 | var info = paramsInfo[i];
154 |
155 | if (info.ParameterType.IsAssignableFrom(sender.GetType()) && info.Name.SameAs("sender", ignoreCase:true))
156 | @params[i] = sender;
157 | else if (info.ParameterType.IsAssignableFrom(e.GetType()))
158 | @params[i] = e;
159 | }
160 |
161 | modelMethod.Invoke(model, @params);
162 | };
163 | }
164 |
165 | if (modelPropEnabled != null && element.GetBindingExpression(UIElement.IsEnabledProperty) == null)
166 | control.SetBinding(UIElement.IsEnabledProperty, new Binding(modelPropEnabled.Name) { Source = model, Mode = BindingMode.TwoWay });
167 | }
168 | }
169 | }
170 |
171 | public class NotifyPropertyChangedBase : INotifyPropertyChanged
172 | {
173 | public event PropertyChangedEventHandler PropertyChanged;
174 |
175 | public void OnPropertyChanged(Expression> expression)
176 | {
177 | if (PropertyChanged != null)
178 | this.InUIThread(() => PropertyChanged(this, new PropertyChangedEventArgs(Reflect.NameOf(expression))));
179 | }
180 | }
181 |
182 | public static class Reflect
183 | {
184 | public static void InUIThread(this object obj, int withDelay, System.Action action)
185 | {
186 | Task.Factory.StartNew(() =>
187 | {
188 | Thread.Sleep(withDelay);
189 | obj.InUIThread(action);
190 | });
191 | }
192 |
193 | public static void InUIThread(this object obj, System.Action action)
194 | {
195 | if (Application.Current != null) //to handle exit
196 | {
197 | if (Application.Current.Dispatcher.CheckAccess())
198 | action();
199 | else
200 | Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, action);
201 | }
202 | }
203 | ///
204 | /// Gets the Member name of the lambda expression.
205 | /// For example "()=>FileName" will return string "FileName".
206 | ///
207 | /// The expression.
208 | ///
209 | public static string GetMemberName(System.Linq.Expressions.Expression expression)
210 | {
211 | switch (expression.NodeType)
212 | {
213 | case ExpressionType.MemberAccess:
214 | var memberExpression = (MemberExpression)expression;
215 |
216 | string supername = null;
217 | if (memberExpression.Expression != null)
218 | supername = GetMemberName(memberExpression.Expression);
219 |
220 | if (String.IsNullOrEmpty(supername))
221 | return memberExpression.Member.Name;
222 |
223 | return String.Concat(supername, '.', memberExpression.Member.Name);
224 |
225 | case ExpressionType.Call:
226 | var callExpression = (MethodCallExpression)expression;
227 | return callExpression.Method.Name;
228 |
229 | case ExpressionType.Convert:
230 | var unaryExpression = (UnaryExpression)expression;
231 | return GetMemberName(unaryExpression.Operand);
232 |
233 | case ExpressionType.Constant:
234 | case ExpressionType.Parameter:
235 | return "";
236 |
237 | default:
238 | throw new ArgumentException("The expression is not a member access or method call expression");
239 | }
240 | }
241 |
242 | ///
243 | /// Gets the Member name of the lambda expression.
244 | /// For example "()=>FileName" will return string "FileName".
245 | ///
246 | /// The expression.
247 | ///
248 | public static string NameOf(Expression> expression)
249 | {
250 | return GetMemberName(expression.Body).Split('.').Last();
251 | }
252 | }
253 | }
--------------------------------------------------------------------------------
/multiclip.ui/Utils/HotKeyEditor.xaml:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
59 |
69 |
70 |
71 |
72 |
77 |
87 |
88 |
89 |
94 |
103 |
104 |
105 |
110 |
119 |
120 |
126 |
132 |
146 |
147 |
163 |
164 |
181 |
182 |
197 |
198 |
199 |
218 |
219 |
231 |
232 |
233 |
234 |
235 |
236 |
241 |
242 |
243 |
--------------------------------------------------------------------------------
/multiclip.server/ClipboardHistory.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Globalization;
5 | using System.IO;
6 | using System.Linq;
7 | using System.Security.Cryptography;
8 | using System.Text;
9 | using System.Threading;
10 | using System.Threading.Tasks;
11 | using MultiClip;
12 | using MultiClip.Server;
13 | using Clipboard = Win32.Clipboard;
14 |
15 | internal class ClipboardHistory
16 | {
17 | ManualResetEvent clipboardChanged = new ManualResetEvent(false);
18 |
19 | public string NextItemId()
20 | {
21 | return DateTime.Now.ToUniversalTime().Ticks.ToString("X8");
22 | }
23 |
24 | static public DateTime ToTimestamp(string dir)
25 | {
26 | try
27 | {
28 | var name = Path.GetFileName(dir);
29 | var ticks = long.Parse(name, System.Globalization.NumberStyles.HexNumber);
30 | var date = new DateTime(ticks);
31 | return date.ToLocalTime();
32 | }
33 | catch
34 | {
35 | return Directory.GetLastWriteTime(dir);
36 | }
37 | }
38 |
39 | internal static Dictionary Cache = new Dictionary();
40 |
41 | static void ClearCaheHistoryOf(string dir)
42 | {
43 | foreach (var file in Directory.GetFiles(dir, "*.cbd"))
44 | Cache.Remove(file);
45 | dir.TryDeleteDir();
46 | }
47 |
48 | public void MakeSnapshot()
49 | {
50 | lock (typeof(ClipboardHistory))
51 | {
52 | try
53 | {
54 | //Some applications (e.g. IE) like setting clipboard multiple times to the same content
55 | // Thread.Sleep(300); //dramatically helps with some 'air' in message queue
56 | string hashFile = SaveSnapshot();
57 |
58 | if (hashFile != null)
59 | {
60 | string hash = Path.GetFileName(hashFile);
61 |
62 | var hashFiles = Directory.GetFiles(Globals.DataDir, "*.hash", SearchOption.AllDirectories);
63 |
64 | if (Config.RemoveDuplicates)
65 | {
66 | //delete older snapshots with the same content (same hash)
67 | var duplicates = hashFiles.Where(file => file.EndsWith(hash) && file != hashFile).ToArray();
68 |
69 | duplicates.ForEach(file => ClearCaheHistoryOf(Path.GetDirectoryName(file)));
70 |
71 | hashFiles = hashFiles.Except(duplicates).ToArray();
72 | }
73 |
74 | //purge snapshots history excess
75 | var excess = hashFiles.Select(Path.GetDirectoryName)
76 | .OrderByDescending(x => x)
77 | .Skip(Config.MaxHistoryDepth)
78 | .ToArray();
79 | Task.Run(() =>
80 | {
81 | Thread.Sleep(1000); //give some time for all hash files to be created
82 | lock (typeof(ClipboardHistory))
83 | {
84 | var orphantDirs = Directory.GetDirectories(Globals.DataDir, "*", SearchOption.TopDirectoryOnly)
85 | .Where(d => !Directory.GetFiles(d, "*.hash").Any())
86 | .ToArray();
87 | if (orphantDirs.Any())
88 | {
89 | Debug.Assert(false, "Multiclip Server is about to clear orphans from the history.");
90 | orphantDirs.ForEach(ClearCaheHistoryOf);
91 | }
92 | }
93 | });
94 |
95 | excess.ForEach(ClearCaheHistoryOf);
96 | }
97 | }
98 | catch { }
99 | finally
100 | {
101 | //Debug.WriteLine("Snapshot End");
102 | clipboardChanged.Reset();
103 | }
104 | }
105 | }
106 |
107 | public static void ClearAll()
108 | {
109 | Debug.Assert(false, "Multiclip Server is about to clear the history.");
110 | Directory.GetDirectories(Globals.DataDir, "*", SearchOption.TopDirectoryOnly)
111 | .ForEach(ClearCaheHistoryOf);
112 | }
113 |
114 | static Dictionary uniqunessFormats = (
115 | // "0000C009.DataObject," +
116 | // "0000C003.OwnerLink," +
117 | // "0000C013.Ole Private Data," +
118 | // "0000C00E.Object Descriptor," +
119 | // "0000C004.Native," +
120 | "0000C007.FileNameW," +
121 | // "00000007.00000010.Locale," +
122 | "0000000D.UnicodeText," +
123 | // "0000C00B.Embed Source," +
124 | "00000008.DeviceIndependentBitmap," +
125 | "00000001.Text," +
126 | // "0000C07E.Rich Text Format," +
127 | // "00000003.MetaFilePict," + //always different even for the virtually same clipboard content
128 | "00000007.OEMText," +
129 | "0000C140.HTML Format")
130 | .Split(',')
131 | .ToDictionary(x => uint.Parse(x.Split('.').First(), NumberStyles.HexNumber));
132 |
133 | public static void Purge(bool showOnly = false)
134 | {
135 | foreach (var dir in Directory.GetDirectories(Globals.DataDir, "???????????????").OrderBy(x => x))
136 | {
137 | if (!Directory.GetFiles(dir, "*.hash").Any())
138 | {
139 | Console.WriteLine("Deleting hash-less data dir: " + dir);
140 | if (!showOnly)
141 | dir.TryDeleteDir();
142 | }
143 | }
144 |
145 | var titles = new Dictionary();
146 |
147 | foreach (var file in Directory.GetFiles(Globals.DataDir, "*.hash", SearchOption.AllDirectories).OrderByDescending(x => x))
148 | {
149 | var snapshot_dir = Path.GetDirectoryName(file);
150 | var title = "";
151 | var hash = new BytesHash();
152 |
153 | foreach (var data_file in Directory.GetFiles(snapshot_dir, "*.cbd").OrderBy(x => x))
154 | {
155 | // For example: 00000001.Text.cbd or 0000000D.UnicodeText.cbd
156 |
157 | string format = Path.GetFileNameWithoutExtension(data_file);
158 | if (uniqunessFormats.ContainsValue(format))
159 | {
160 | var bytes = new byte[0];
161 | try
162 | {
163 | // File.ReadAllBytes(data_file) will not work because even the same data encrypted
164 | // twice (e.g. to different location) will produce different data. Thus need to decrypt it first
165 | bytes = ReadPrivateData(data_file);
166 | if (showOnly && true)
167 | {
168 | if (format == "0000000D.UnicodeText")
169 | {
170 | title = bytes.ToUnicodeTitle(20);
171 | Console.WriteLine($"{Path.GetFileName(snapshot_dir)}: {title}");
172 | }
173 | }
174 | }
175 | catch { }
176 |
177 | hash.Add(bytes);
178 | }
179 | }
180 |
181 | titles[snapshot_dir] = title;
182 |
183 | foreach (var old_text_ash in Directory.GetFiles(snapshot_dir, "*.text_hash"))
184 | File.Delete(old_text_ash);
185 |
186 | string crcFile = Path.Combine(snapshot_dir, hash + ".text_hash");
187 | File.WriteAllText(crcFile, "");
188 | }
189 |
190 | var duplicates = Directory.GetFiles(Globals.DataDir, "*.text_hash", SearchOption.AllDirectories)
191 | .GroupBy(Path.GetFileName)
192 | .Select(x => new { Hash = x.Key, Files = x.ToArray() })
193 | .ForEach(x =>
194 | {
195 | Debug.WriteLine("");
196 | Debug.WriteLine($"{x.Hash}");
197 | var snapshot = Path.GetDirectoryName(x.Files.First());
198 | if (titles.ContainsKey(snapshot))
199 | Debug.WriteLine($"{titles[snapshot]}");
200 | foreach (var item in x.Files)
201 | Debug.WriteLine(" " + item);
202 | });
203 |
204 | var dirs_to_purge = duplicates.Where(x => x.Files.Count() > 1).ToArray();
205 | Debug.WriteLine(">>> Duplicates: " + dirs_to_purge.Length);
206 | foreach (var item in dirs_to_purge)
207 | item.Files.Skip(1).ForEach(x =>
208 | {
209 | var dir = Path.GetDirectoryName(x);
210 | Console.WriteLine("Deleting " + dir);
211 | if (titles.ContainsKey(dir))
212 | Debug.WriteLine(titles[dir]);
213 | if (!showOnly)
214 | dir.TryDeleteDir();
215 | });
216 | }
217 |
218 | static internal ulong lastSnapshopHash;
219 |
220 | public string SaveSnapshot()
221 | {
222 | string snapshotDir = Path.Combine(Globals.DataDir, NextItemId());
223 | try
224 | {
225 | Log.WriteLine($"SaveSnapshot");
226 |
227 | Dictionary clipboard = Win32.Clipboard.GetClipboard();
228 |
229 | if (clipboard?.Any() == true)
230 | {
231 | lastSnapshopHash = clipboard.GetContentHash();
232 |
233 | Directory.CreateDirectory(snapshotDir);
234 |
235 | var bytesHash = new BytesHash();
236 |
237 | foreach (uint item in clipboard.Keys.OrderBy(x => x))
238 | {
239 | string formatFile = Path.Combine(snapshotDir, $"{item:X8}.{item.ToFormatName().EscapePath()}.cbd");
240 |
241 | var array = new byte[0];
242 | try
243 | {
244 | array = clipboard[item];
245 | }
246 | catch { }
247 |
248 | if (array.Any())
249 | {
250 | // cache large data chunks only to speedup the processing (e.g. compare, load) of the captured encrypted content
251 | if (Config.EncryptData && Config.CacheEncryptDataMinSize < array.Length)
252 | {
253 | // It's OK to cache raw data here as it is a clipboard content that is available to every user app here anyway.
254 | // Though the data will always be encrypted when it hit's the permanent storage.
255 | Cache[formatFile] = array;
256 | }
257 | try
258 | {
259 | var writtenData = WritePrivateData(formatFile, array);
260 | if (uniqunessFormats.ContainsKey(item))
261 | bytesHash.Add(array);
262 |
263 | }
264 | catch (Exception e) { }
265 | }
266 | }
267 |
268 | string shapshotHashFile = Path.Combine(snapshotDir, bytesHash + ".hash");
269 | File.WriteAllText(shapshotHashFile, "");
270 | return shapshotHashFile;
271 | }
272 | }
273 | catch (Clipboard.LastSessionErrorDetectedException ex)
274 | {
275 | snapshotDir.DeleteIfDiExists();
276 |
277 | if (Environment.GetEnvironmentVariable("MULTICLIP_SHOW_ERRORS") != null)
278 | {
279 | var newThread = new Thread(() =>
280 | {
281 | try
282 | {
283 | Thread.Sleep(1000);
284 | Clipboard.SetText($"MultiClip Error: {ex.Message}");
285 | }
286 | catch { }
287 | });
288 | newThread.IsBackground = true;
289 | newThread.SetApartmentState(ApartmentState.STA);
290 | newThread.Start();
291 | }
292 | }
293 | catch
294 | {
295 | snapshotDir.DeleteIfDiExists();
296 | }
297 | return null;
298 | }
299 |
300 | public static void LoadSnapshot(string dir)
301 | {
302 | Log.WriteLine(nameof(LoadSnapshot));
303 |
304 | lock (typeof(ClipboardHistory))
305 | {
306 | try
307 | {
308 | var data = new Dictionary();
309 | foreach (string file in Directory.GetFiles(dir, "*.cbd"))
310 | {
311 | try
312 | {
313 | uint format = uint.Parse(Path.GetFileName(file).Split('.').First(), NumberStyles.HexNumber);
314 | var bytes = new byte[0];
315 |
316 | try
317 | {
318 | if (Cache.ContainsKey(file))
319 | bytes = Cache[file];
320 | else
321 | bytes = ReadPrivateData(file);
322 | }
323 | catch { }
324 |
325 | if (bytes.Any())
326 | data[format] = bytes;
327 | }
328 | catch { }
329 | }
330 |
331 | if (data.Any())
332 | Win32.Clipboard.SetClipboard(data);
333 | }
334 | catch { }
335 | }
336 | }
337 |
338 | static byte[] entropy = Encoding.Unicode.GetBytes("MultiClip");
339 |
340 | static internal byte[] ReadPrivateData(string file)
341 | {
342 | if (Cache.ContainsKey(file))
343 | {
344 | return Cache[file];
345 | }
346 | else
347 | {
348 | var bytes = File.ReadAllBytes(file);
349 | if (Config.EncryptData)
350 | {
351 | bytes = ProtectedData.Unprotect(bytes, entropy, DataProtectionScope.CurrentUser);
352 |
353 | if (Config.CacheEncryptDataMinSize < bytes.Length)
354 | Cache[file] = bytes;
355 | }
356 | return bytes;
357 | }
358 | }
359 |
360 | static byte[] WritePrivateData(string file, byte[] data)
361 | {
362 | if (Config.EncryptData)
363 | {
364 | var bytes = ProtectedData.Protect(data, entropy, DataProtectionScope.CurrentUser);
365 | File.WriteAllBytes(file, bytes);
366 | return bytes;
367 | }
368 | else
369 | {
370 | File.WriteAllBytes(file, data);
371 | return data;
372 | }
373 | }
374 |
375 | public static byte[] ReadFormatData(string dir, int format)
376 | {
377 | var file = Directory.GetFiles(dir, "{0:X8}.*.cbd".FormatWith(format)).FirstOrDefault();
378 | if (file != null)
379 | return ReadPrivateData(file);
380 | else
381 | return null;
382 | }
383 | }
--------------------------------------------------------------------------------