├── multiclip.ui ├── AutoBinder.cs ├── Resources │ ├── app.ico │ ├── tray_icon.ico │ ├── tray_icon.black.ico │ ├── dark_theme_preview.png │ └── light_theme_preview.png ├── tray_icon_soft.ico ├── Logo │ ├── Clipboard16.png │ ├── Clipboard24.png │ ├── Clipboard32.png │ ├── Clipboard48.png │ ├── Settings16.png │ ├── Settings24.png │ ├── Settings32.png │ ├── Clipboard128.png │ └── Clipboard256.png ├── SettingsViewModel.cs ├── app.config ├── Properties │ ├── Settings.settings │ ├── DesignTimeResources.xaml │ ├── Settings.Designer.cs │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ └── Resources.resx ├── App.xaml ├── Utils │ ├── HotKeysView.xaml │ ├── HotKeysView.xaml.cs │ ├── DibHelper.cs │ ├── HotKeyEditorViewModel.cs │ ├── HotKeyEditor.xaml.cs │ ├── TrimmingTextBlock.cs │ ├── AutoBinder.cs │ └── HotKeyEditor.xaml ├── HistoryViewModel.cs ├── HistoryItemViewModel.cs ├── Rosources.xaml ├── App.xaml.cs ├── SettingsView.xaml.cs ├── HistoryView.xaml.cs ├── Hosting │ └── TrayIcon.cs ├── WinHook.cs ├── Bootstrapper.cs ├── HistoryView.xaml ├── Utils.cs ├── multiclip.ui.csproj └── ClipboardMonitor.cs ├── docs ├── menu.png ├── config.png ├── selection.png ├── reading_error.png └── defender_exclusion.png ├── multiclip.ui.ico ├── multiclip ├── tray_icon.black.ico ├── app.config ├── Properties │ ├── AssemblyVersion.cs │ └── AssemblyInfo.cs ├── Program.cs └── multiclip.csproj ├── .gitignore ├── choco ├── tools │ ├── chocolateyUninstall.ps1 │ ├── chocolateyBeforeModify.ps1 │ ├── chocolateyInstall.ps1.template │ └── chocolateyInstall.ps1 ├── build.cmd ├── readme.txt ├── multiclip.nuspec └── update_package.cs ├── multiclip.server ├── app.config ├── Properties │ └── AssemblyInfo.cs ├── Globals.cs ├── ClipboardView.cs ├── MultiClip.Server.csproj ├── Program.cs ├── ClipboardWatcher.resx ├── ClipboardWatcher.cs ├── Utils.cs ├── Clipboard.cs └── ClipboardHistory.cs ├── LICENSE ├── multiclip.hotkeys ├── multiclip.sln └── README.md /multiclip.ui/AutoBinder.cs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-shilo/multiclip/HEAD/docs/menu.png -------------------------------------------------------------------------------- /docs/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-shilo/multiclip/HEAD/docs/config.png -------------------------------------------------------------------------------- /multiclip.ui.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-shilo/multiclip/HEAD/multiclip.ui.ico -------------------------------------------------------------------------------- /docs/selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-shilo/multiclip/HEAD/docs/selection.png -------------------------------------------------------------------------------- /docs/reading_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-shilo/multiclip/HEAD/docs/reading_error.png -------------------------------------------------------------------------------- /docs/defender_exclusion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-shilo/multiclip/HEAD/docs/defender_exclusion.png -------------------------------------------------------------------------------- /multiclip.ui/Resources/app.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-shilo/multiclip/HEAD/multiclip.ui/Resources/app.ico -------------------------------------------------------------------------------- /multiclip.ui/tray_icon_soft.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-shilo/multiclip/HEAD/multiclip.ui/tray_icon_soft.ico -------------------------------------------------------------------------------- /multiclip/tray_icon.black.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-shilo/multiclip/HEAD/multiclip/tray_icon.black.ico -------------------------------------------------------------------------------- /multiclip.ui/Logo/Clipboard16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-shilo/multiclip/HEAD/multiclip.ui/Logo/Clipboard16.png -------------------------------------------------------------------------------- /multiclip.ui/Logo/Clipboard24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-shilo/multiclip/HEAD/multiclip.ui/Logo/Clipboard24.png -------------------------------------------------------------------------------- /multiclip.ui/Logo/Clipboard32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-shilo/multiclip/HEAD/multiclip.ui/Logo/Clipboard32.png -------------------------------------------------------------------------------- /multiclip.ui/Logo/Clipboard48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-shilo/multiclip/HEAD/multiclip.ui/Logo/Clipboard48.png -------------------------------------------------------------------------------- /multiclip.ui/Logo/Settings16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-shilo/multiclip/HEAD/multiclip.ui/Logo/Settings16.png -------------------------------------------------------------------------------- /multiclip.ui/Logo/Settings24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-shilo/multiclip/HEAD/multiclip.ui/Logo/Settings24.png -------------------------------------------------------------------------------- /multiclip.ui/Logo/Settings32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-shilo/multiclip/HEAD/multiclip.ui/Logo/Settings32.png -------------------------------------------------------------------------------- /multiclip.ui/SettingsViewModel.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-shilo/multiclip/HEAD/multiclip.ui/SettingsViewModel.cs -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.gitignore 2 | /.vs 3 | *.csdat 4 | **/bin/Debug/** 5 | **/bin/Release/** 6 | **/obj/** 7 | /MultiClip/*.user 8 | -------------------------------------------------------------------------------- /multiclip.ui/Logo/Clipboard128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-shilo/multiclip/HEAD/multiclip.ui/Logo/Clipboard128.png -------------------------------------------------------------------------------- /multiclip.ui/Logo/Clipboard256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-shilo/multiclip/HEAD/multiclip.ui/Logo/Clipboard256.png -------------------------------------------------------------------------------- /multiclip.ui/Resources/tray_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-shilo/multiclip/HEAD/multiclip.ui/Resources/tray_icon.ico -------------------------------------------------------------------------------- /multiclip.ui/Resources/tray_icon.black.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-shilo/multiclip/HEAD/multiclip.ui/Resources/tray_icon.black.ico -------------------------------------------------------------------------------- /multiclip.ui/Resources/dark_theme_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-shilo/multiclip/HEAD/multiclip.ui/Resources/dark_theme_preview.png -------------------------------------------------------------------------------- /multiclip.ui/Resources/light_theme_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-shilo/multiclip/HEAD/multiclip.ui/Resources/light_theme_preview.png -------------------------------------------------------------------------------- /choco/tools/chocolateyUninstall.ps1: -------------------------------------------------------------------------------- 1 | $packageName = 'multiclip' 2 | 3 | # no actions at this stage but any future 4 | ## cleanup steps should go here -------------------------------------------------------------------------------- /choco/build.cmd: -------------------------------------------------------------------------------- 1 | echo off 2 | echo ******************* 3 | echo * must be admin * 4 | echo ******************* 5 | 6 | choco pack 7 | REM choco install multiclip -s '%cd%' --force -------------------------------------------------------------------------------- /multiclip.ui/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /multiclip/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /multiclip.server/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /multiclip.ui/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /choco/tools/chocolateyBeforeModify.ps1: -------------------------------------------------------------------------------- 1 | $packageName = 'multiclip' 2 | 3 | # stop "multiclip" first so it does not restart the server assuming it crashed 4 | Stop-Process -Name "multiclip" -ErrorAction SilentlyContinue 5 | Stop-Process -Name "multiclip.server" -ErrorAction SilentlyContinue -------------------------------------------------------------------------------- /multiclip.ui/App.xaml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /multiclip.ui/Properties/DesignTimeResources.xaml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /multiclip/Properties/AssemblyVersion.cs: -------------------------------------------------------------------------------- 1 | // Version information for an assembly consists of the following four values: 2 | // 3 | // Major Version 4 | // Minor Version 5 | // Build Number 6 | // Revision 7 | // 8 | // You can specify all the values or you can default the Build and Revision Numbers 9 | // by using the '*' as shown below: 10 | // [assembly: AssemblyVersion("1.0.*")] 11 | using System.Reflection; 12 | 13 | [assembly: AssemblyVersion("1.4.5.0")] 14 | [assembly: AssemblyFileVersion("1.4.5.0")] -------------------------------------------------------------------------------- /choco/readme.txt: -------------------------------------------------------------------------------- 1 | # Build 2 | 1. 7Zip 'multiclip.exe' 3 | 4 | # Package 5 | 1. Update update_package.cs with URL and the latest version 6 | 2. Run update_package.cs 7 | 3. Update *.nuspec with the latest version number 8 | 4. Update *.nuspec with the latest release notes 9 | 5. Update publish.cmd with the latest version 10 | 11 | MUST be CMD but not PS !!!! 12 | 13 | 5. Run build.cmd or "choco pack" 14 | 5.a If required run "choco install multiclip -y -s '%cd%'" or "choco upgrade multiclip -y -s '%cd%'" 15 | 6. Run publish.cmd 16 | -------------------------------------------------------------------------------- /multiclip.ui/Utils/HotKeysView.xaml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /choco/tools/chocolateyInstall.ps1.template: -------------------------------------------------------------------------------- 1 | $packageName = 'multiclip' 2 | $url = ??? 3 | 4 | # In order to avoid multiclip app popping up message boxes need to indicate that 5 | # we are running under choco runtime by setting `UNDER_CHOCO` environment variable 6 | # that is to be consumed by all child processes of choco. 7 | # Cannot use `Install-ChocolateyEnvironmentVariable` as the variable should not be 8 | # persisted but only set for the choco installation process. So using .NET API 9 | 10 | [System.Environment]::SetEnvironmentVariable('UNDER_CHOCO', 'yes') 11 | 12 | Stop-Process -Name "multiclip" -ErrorAction SilentlyContinue 13 | Stop-Process -Name "multiclip.server" -ErrorAction SilentlyContinue 14 | 15 | $installDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)" 16 | 17 | $checksum = ??? 18 | $checksumType = "sha256" 19 | 20 | # Download and unpack a zip file 21 | Install-ChocolateyZipPackage "$packageName" "$url" "$installDir" -checksum $checksum -checksumType $checksumType 22 | -------------------------------------------------------------------------------- /multiclip/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("multiclip.schim")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("multiclip.schim")] 13 | [assembly: AssemblyCopyright("Copyright © 2022")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("7b23e52a-bb74-4b36-b57e-858b7d256ce7")] -------------------------------------------------------------------------------- /multiclip.server/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("MultiClip.Server")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("MultiClip")] 13 | [assembly: AssemblyCopyright("Copyright © 2015-2020")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("6da643c6-e46f-4713-87a1-8db62eb66c9a")] -------------------------------------------------------------------------------- /choco/tools/chocolateyInstall.ps1: -------------------------------------------------------------------------------- 1 | $packageName = 'multiclip' 2 | $url = 'https://github.com/oleg-shilo/multiclip/releases/download/v1.4.5.0/multiclip.v1.4.5.0.7z' 3 | 4 | # In order to avoid multiclip app popping up message boxes need to indicate that 5 | # we are running under choco runtime by setting `UNDER_CHOCO` environment variable 6 | # that is to be consumed by all child processes of choco. 7 | # Cannot use `Install-ChocolateyEnvironmentVariable` as the variable should not be 8 | # persisted but only set for the choco installation process. So using .NET API 9 | 10 | [System.Environment]::SetEnvironmentVariable('UNDER_CHOCO', 'yes') 11 | 12 | Stop-Process -Name "multiclip" -ErrorAction SilentlyContinue 13 | Stop-Process -Name "multiclip.server" -ErrorAction SilentlyContinue 14 | 15 | $installDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)" 16 | 17 | $checksum = 'BBD16CCF29E49EF7DE9CE265A52EFEBC678C9201B0A9716AF0D451871CEDB204' 18 | $checksumType = "sha256" 19 | 20 | # Download and unpack a zip file 21 | Install-ChocolateyZipPackage "$packageName" "$url" "$installDir" -checksum $checksum -checksumType $checksumType 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Oleg Shilo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /multiclip.ui/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace MultiClip.UI.Properties { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.8.0.0")] 16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 19 | 20 | public static Settings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /multiclip.hotkeys: -------------------------------------------------------------------------------- 1 | ; 2 | ; [name] 3 | ; [|argument0...[|argumentN]] 4 | ; Example: notepad.exe|C:\Users\osh\AppData\Local\MultiClip.History\Data\..\multiclip.hotkeys 5 | ;--------------------------------------- 6 | Control+Oem3 7 | 8 | 9 | Control+Shift+T 10 | 11 | 12 | Control+Shift+K 13 | 14 | 15 | Control+Shift+Q 16 | 17 | 18 | Control+Shift+F7 19 | Spell Checker 20 | E:\My Utils\SpellChecker\ClipboardSpellChecker.exe| 21 | 22 | Control+Alt+T 23 | Translit Mapper 24 | csws.exe|Q:\Extras\Utils\TranslitMapper.cs 25 | 26 | Control+Shift+X 27 | Format Clipboard XML 28 | csws.exe|Q:\Extras\Utils\FormatClipboardXml.cs 29 | 30 | Control+F3 31 | Search Everything 32 | Everything.exe| 33 | 34 | Control+Shift+I 35 | Ukrainian I 36 | csws.exe|Q:\Extras\Utils\TranslitMapper.cs char ukr-i 37 | 38 | Control+Shift+Oemtilde 39 | Ukrainian Є 40 | csws.exe|Q:\Extras\Utils\TranslitMapper.cs char ukr-e 41 | 42 | Control+Shift+Alt+I 43 | Ukrainian Ї 44 | csws.exe|Q:\Extras\Utils\TranslitMapper.cs char ukr-ii 45 | 46 | Control+Shift+E 47 | Restart Explorer 48 | csws.exe|Q:\Extras\Utils\RestartExplorer.cs 49 | 50 | -------------------------------------------------------------------------------- /multiclip.ui/HistoryViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using System.ComponentModel; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | 7 | namespace MultiClip.UI 8 | { 9 | class HistoryViewModel 10 | { 11 | public static string DataDir = Globals.DataDir; 12 | 13 | public ObservableCollection Items { get; set; } = new ObservableCollection(); 14 | 15 | public HistoryItemViewModel Selected { get; set; } 16 | 17 | public object PreviewImage { get; set; } 18 | 19 | public void Remove(HistoryItemViewModel item) 20 | { 21 | if (item != null) 22 | { 23 | item.Location.TryDeleteDir(); 24 | Items.Remove(item); 25 | } 26 | } 27 | 28 | public void RemoveAll() 29 | { 30 | Items.ForEach(item=>item.Location.TryDeleteDir()); 31 | Items.Clear(); 32 | } 33 | 34 | public void Reset() 35 | { 36 | Items.Clear(); 37 | var sw = new Stopwatch(); 38 | foreach (string dir in Directory.GetDirectories(DataDir).Reverse()) 39 | { 40 | sw.Start(); 41 | 42 | Items.Add(HistoryItemViewModel.LoadFrom(dir)); 43 | Debug.WriteLine(sw.Elapsed + " - " + dir); 44 | sw.Reset(); 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /multiclip.server/Globals.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | 5 | namespace MultiClip 6 | { 7 | class Globals 8 | { 9 | static Globals() 10 | { 11 | Directory.CreateDirectory(DataDir); 12 | } 13 | 14 | public static string DataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MultiClip.History", "Data"); 15 | public static string CloseRequestName = "multiclip_CloseRequest"; 16 | public static string ClipboardWatcherWindow = "MultiClip_ClipboardWatcherWindow"; 17 | public const int WM_USER = 0x0400; 18 | public const int WM_MULTICLIPTEST = WM_USER + 100; 19 | } 20 | 21 | // hardcodded configuration (not persisted) 22 | class Config 23 | { 24 | public static int MaxHistoryDepth = 35; 25 | public static bool RestoreHistoryAtStartup = true; 26 | public static bool EncryptData = true; 27 | public static int CacheEncryptDataMinSize = 1024 * 5; 28 | public static bool AsyncProcessing = true; 29 | public static bool RestartingIsEnabled => !File.Exists(Path.Combine(Globals.DataDir, "..", "disable-reset")); // can be a property getter at it will be checked only every 3 mins 30 | public static bool RemoveDuplicates = !File.Exists(Path.Combine(Globals.DataDir, "..", "disable-remove-duplicates")); // has to be initialized once as it is checked only every time clipboard is changed 31 | } 32 | } -------------------------------------------------------------------------------- /multiclip.ui/HistoryItemViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using Win32; 4 | 5 | namespace MultiClip.UI 6 | { 7 | class HistoryItemViewModel 8 | { 9 | public static HistoryItemViewModel LoadFrom(string dir) 10 | { 11 | var model = new ClipboardView(dir); 12 | 13 | return new HistoryItemViewModel 14 | { 15 | Location = model.Location, 16 | Title = model.Title, 17 | Timestamp = model.Timestamp, 18 | ViewFormat = model.MainFormat, 19 | PreviewText = model.PreviewText, 20 | PreviewImageData = model.PreviewImage 21 | }; 22 | } 23 | 24 | public ClipboardView.ViewFormat ViewFormat { get; set; } 25 | public DateTime Timestamp { get; set; } 26 | public string Location { get; set; } 27 | public string Title { get; set; } 28 | 29 | byte[] PreviewImageData; 30 | object previewImage; 31 | 32 | public object PreviewImage 33 | { 34 | get 35 | { 36 | if (previewImage == null && PreviewImageData != null) 37 | { 38 | previewImage = DibHelper.ImageFromDIBBytes(PreviewImageData); 39 | if (previewImage == null) 40 | PreviewImageData = null; 41 | } 42 | return previewImage; 43 | } 44 | 45 | } 46 | public object PreviewText { get; set; } 47 | } 48 | } -------------------------------------------------------------------------------- /multiclip/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace multiclip.schim 11 | { 12 | internal class Program 13 | { 14 | static byte[] Scrumble(bool toScramble, string inFile) 15 | { 16 | var bytes = File.ReadAllBytes(inFile); 17 | for (int i = 0; i < bytes.Length; i++) 18 | if ((i % 2) == 0) 19 | bytes[i] = (byte)(toScramble ? bytes[i] - 1 : bytes[i] + 1); 20 | return bytes; 21 | } 22 | 23 | static void Scrumble(bool toScramble, string inFile, string outFile) 24 | => File.WriteAllBytes(outFile, Scrumble(toScramble, inFile)); 25 | 26 | static Assembly asm; 27 | 28 | [STAThread] 29 | static void Main(string[] args) 30 | { 31 | if (args.Any()) 32 | { 33 | bool toScramble = (args.First() == "-s"); 34 | string inFile = args[1]; 35 | string outFile = args[2]; 36 | 37 | Scrumble(toScramble, inFile, outFile); 38 | } 39 | else 40 | { 41 | // doing scrambling/unscrambling simply because Chocolatey triggers false-positives on RESX files 42 | AppDomain.CurrentDomain.ResourceResolve += CurrentDomain_ResourceResolve; 43 | var scambledAsm = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "multiclip.ui"); 44 | asm = Assembly.Load(Scrumble(toScramble: false, scambledAsm)); 45 | var app = asm.GetType("MultiClip.UI.App").GetMethod("Main"); 46 | app.Invoke(null, new object[0]); 47 | } 48 | } 49 | 50 | private static Assembly CurrentDomain_ResourceResolve(object sender, ResolveEventArgs args) 51 | { 52 | return asm; 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /multiclip.ui/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | using System.Windows; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("MultiClip")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("Oleg Shilo")] 12 | [assembly: AssemblyProduct("MultiClip")] 13 | [assembly: AssemblyCopyright("Copyright © 2015-2023")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | //In order to begin building localizable applications, set 23 | //CultureYouAreCodingWith in your .csproj file 24 | //inside a . For example, if you are using US english 25 | //in your source files, set the to en-US. Then uncomment 26 | //the NeutralResourceLanguage attribute below. Update the "en-US" in 27 | //the line below to match the UICulture setting in the project file. 28 | 29 | //[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] 30 | 31 | [assembly: ThemeInfo( 32 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 33 | //(used if a resource is not found in the page, 34 | // or application resource dictionaries) 35 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 36 | //(used if a resource is not found in the page, 37 | // app, or any theme specific resource dictionaries) 38 | )] -------------------------------------------------------------------------------- /multiclip.ui/Utils/HotKeysView.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows; 3 | using System.Windows.Controls; 4 | using System.Windows.Input; 5 | 6 | namespace MultiClip.UI.Utils 7 | { 8 | /// 9 | /// Interaction logic for HotKeysView.xaml 10 | /// 11 | public partial class HotKeysView : Window 12 | { 13 | public class Item 14 | { 15 | public string Name; 16 | public Action Action; 17 | 18 | public override string ToString() 19 | { 20 | return Name; 21 | } 22 | } 23 | 24 | public HotKeysView() 25 | { 26 | InitializeComponent(); 27 | 28 | var map = HotKeysMapping.ToKeyHandlersView(); 29 | foreach (var key in map.Keys) 30 | mappingList.Items.Add(new Item { Name = key, Action = map[key] }); 31 | } 32 | 33 | public static string PopupActionName = ""; 34 | 35 | static public void Popup() 36 | { 37 | new HotKeysView().ShowDialog(); 38 | } 39 | 40 | void Window_PreviewKeyDown(object sender, KeyEventArgs e) 41 | { 42 | if (e.Key == Key.Escape || e.Key == Key.Return) 43 | { 44 | Close(); 45 | if (e.Key == Key.Return) 46 | (mappingList.SelectedItem as Item)?.Action(); 47 | } 48 | } 49 | 50 | void Window_Deactivated(object sender, EventArgs e) 51 | { 52 | try 53 | { 54 | Close(); 55 | } 56 | catch { } 57 | } 58 | 59 | void Window_Loaded(object sender, RoutedEventArgs e) 60 | { 61 | // to ensure the keyboard input focus 62 | this.Activate(); 63 | mappingList.SelectedIndex = 0; 64 | (mappingList.ItemContainerGenerator 65 | .ContainerFromItem(mappingList.SelectedItem) as ListBoxItem)? 66 | .Focus(); 67 | } 68 | 69 | private void mappingList_MouseDoubleClick(object sender, MouseButtonEventArgs e) 70 | { 71 | (mappingList.SelectedItem as Item)?.Action(); 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /multiclip.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.32014.148 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "multiclip.server", "multiclip.server\multiclip.server.csproj", "{6DA643C6-E46F-4713-87A1-8DB62EB66C9A}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "multiclip.ui", "multiclip.ui\multiclip.ui.csproj", "{C1CE4AD5-CB3A-4BCC-A379-9A9515BE86DB}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "multiclip", "multiclip\multiclip.csproj", "{7B23E52A-BB74-4B36-B57E-858B7D256CE7}" 11 | ProjectSection(ProjectDependencies) = postProject 12 | {6DA643C6-E46F-4713-87A1-8DB62EB66C9A} = {6DA643C6-E46F-4713-87A1-8DB62EB66C9A} 13 | {C1CE4AD5-CB3A-4BCC-A379-9A9515BE86DB} = {C1CE4AD5-CB3A-4BCC-A379-9A9515BE86DB} 14 | EndProjectSection 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {6DA643C6-E46F-4713-87A1-8DB62EB66C9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {6DA643C6-E46F-4713-87A1-8DB62EB66C9A}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {6DA643C6-E46F-4713-87A1-8DB62EB66C9A}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {6DA643C6-E46F-4713-87A1-8DB62EB66C9A}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {C1CE4AD5-CB3A-4BCC-A379-9A9515BE86DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {C1CE4AD5-CB3A-4BCC-A379-9A9515BE86DB}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {C1CE4AD5-CB3A-4BCC-A379-9A9515BE86DB}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {C1CE4AD5-CB3A-4BCC-A379-9A9515BE86DB}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {7B23E52A-BB74-4B36-B57E-858B7D256CE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {7B23E52A-BB74-4B36-B57E-858B7D256CE7}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {7B23E52A-BB74-4B36-B57E-858B7D256CE7}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {7B23E52A-BB74-4B36-B57E-858B7D256CE7}.Release|Any CPU.Build.0 = Release|Any CPU 34 | EndGlobalSection 35 | GlobalSection(SolutionProperties) = preSolution 36 | HideSolutionNode = FALSE 37 | EndGlobalSection 38 | GlobalSection(ExtensibilityGlobals) = postSolution 39 | SolutionGuid = {4655BF5A-14F8-4E24-9F4A-8DBCD697DFE9} 40 | EndGlobalSection 41 | EndGlobal 42 | -------------------------------------------------------------------------------- /multiclip.ui/Rosources.xaml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 32 | 33 | 53 | 54 | -------------------------------------------------------------------------------- /multiclip.server/ClipboardView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Drawing; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace Win32 8 | { 9 | public class ClipboardView 10 | { 11 | public enum ViewFormat : int 12 | { 13 | Custom = 0, 14 | PlainText = 1, 15 | UnicodeText = 13, 16 | Files = 15, 17 | Image = 8, 18 | } 19 | 20 | public ViewFormat MainFormat; 21 | public string Title; 22 | public string Location; 23 | public byte[] PreviewImage; 24 | public string PreviewText; 25 | public DateTime Timestamp; 26 | 27 | public ClipboardView(string location) 28 | { 29 | Location = location; 30 | Title = ""; 31 | PreviewText = ""; 32 | try 33 | { 34 | Timestamp = ClipboardHistory.ToTimestamp(location); 35 | 36 | byte[] data = null; 37 | if ((data = ClipboardHistory.ReadFormatData(location, (int)ViewFormat.Image)) != null) 38 | { 39 | MainFormat = ViewFormat.Image; 40 | Title = ""; 41 | PreviewImage = data; 42 | PreviewText = ""; 43 | } 44 | else if ((data = ClipboardHistory.ReadFormatData(location, (int)ViewFormat.Files)) != null) 45 | { 46 | MainFormat = ViewFormat.Files; 47 | 48 | string[] files = Clipboard.GetDropFiles(data); 49 | Title = files.FirstOrDefault() ?? ""; 50 | PreviewText = string.Join("\n", files); 51 | } 52 | else if ((data = ClipboardHistory.ReadFormatData(location, (int)ViewFormat.UnicodeText)) != null) 53 | { 54 | MainFormat = ViewFormat.UnicodeText; 55 | 56 | Title = 57 | PreviewText = data.ToUnicodeTitle(); 58 | } 59 | else if ((data = ClipboardHistory.ReadFormatData(location, (int)ViewFormat.PlainText)) != null) 60 | { 61 | MainFormat = ViewFormat.PlainText; 62 | 63 | Title = 64 | PreviewText = data.ToAsciiTitle(); 65 | } 66 | } 67 | catch { } 68 | 69 | Title = Title.Replace("\r\n", " ") 70 | .Replace("\r\n", " ") 71 | .Trim(); 72 | 73 | int titleMaxLength = 100; 74 | if (Title.Length > titleMaxLength) 75 | Title = Title.Substring(0, titleMaxLength) + "..."; 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /choco/multiclip.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Multiclip 5 | 1.4.5.0 6 | Multiclip 7 | Oleg Shilo 8 | Oleg Shilo 9 | http://opensource.org/licenses/MIT 10 | https://github.com/oleg-shilo/multiclip 11 | https://github.com/oleg-shilo/multiclip 12 | https://github.com/oleg-shilo/multiclip/issues 13 | https://github.com/oleg-shilo/multiclip/wiki 14 | https://cdn.statically.io/img/raw.githubusercontent.com/oleg-shilo/multiclip/master/multiclip.ui/Resources/app.ico 15 | 16 | false 17 | MultiClip is a non-obsructive clipboard manager. There are quite a few very good products exist in this category. Though MultiClip is a clipboard manager that puts an extremely strong emphasis on the user experience particularly tailored for the programmers. MultiClip is noting else but an application that allows you to switch the clipboard content to any item from the interactive clipboard history dialog. 18 | Multiclip allows restoring a clipboard content (with all its formats) from the history. 19 | _Your antivirus may report Multiclip a false positive due to the fact that the solution uses low level keyboard API. Thus you may need to to customize your scanning configuration. See this [article](https://github.com/oleg-shilo/multiclip/tree/v1.4.0.0#important) for more details _ 20 | 21 | - Support capturing and restoring of all clipboard formats. 22 | - Auto-completion IDE-style user experience. 23 | - System-wide hot-key triggering. 24 | - Non-obstructive immediate preview. 25 | - Full history encryption. 26 | - General purpose system-wide hotkey mapping for to running any executable. 27 | - Fully configurable 28 | - Restoring the clipboard history after system restart 29 | - Start with Windows 30 | - Dark/Light theme 31 | - Zero-deployment. One executable is all that you need. 32 | 33 | Multiclip 34 | 35 | # Release v1.4.5.0 36 | - Server restarting algorithm has been made consistent across the all failure detecting routines thorough the app. 37 | 38 | Oleg Shilo 39 | clipboard C# UI UX User-Experience 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /multiclip.ui/Utils/DibHelper.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using fiLE=System.IO.File; 3 | using System.Linq; 4 | using System.Runtime.InteropServices; 5 | using System.Windows.Media; 6 | using System.Windows.Media.Imaging; 7 | 8 | namespace MultiClip.UI 9 | { 10 | /// 11 | /// All credit to Thomas Levesque (http://www.thomaslevesque.com/2009/02/05/wpf-paste-an-image-from-the-clipboard/) 12 | /// 13 | class DibHelper 14 | { 15 | public static ImageSource ImageFromDIBBytes(byte[] dibBuffer) 16 | { 17 | if (dibBuffer == null || dibBuffer.Length == 0) 18 | return null; 19 | 20 | try 21 | { 22 | BITMAPINFOHEADER infoHeader = dibBuffer.MarshalTo(); 23 | 24 | int fileHeaderSize = Marshal.SizeOf(typeof(BITMAPFILEHEADER)); 25 | int infoHeaderSize = infoHeader.biSize; 26 | int fileSize = fileHeaderSize + infoHeader.biSize + infoHeader.biSizeImage; 27 | 28 | var fileHeader = new BITMAPFILEHEADER 29 | { 30 | bfType = BITMAPFILEHEADER.BM, 31 | bfSize = fileSize, 32 | bfReserved1 = 0, 33 | bfReserved2 = 0, 34 | bfOffBits = fileHeaderSize + infoHeaderSize + infoHeader.biClrUsed * 4 35 | }; 36 | 37 | byte[] fileHeaderBytes = fileHeader.UnmarshalTo(); 38 | 39 | //Do not dispose here as BitmapFrame still needs an active stream copy. 40 | //GC should eventually dispose the stream when the ImageSource is disposed. 41 | //http://code.logos.com/blog/2008/04/memory_leak_with_bitmapimage_and_memorystream.html 42 | var msBitmap = new MemoryStream(); 43 | msBitmap.Write(fileHeaderBytes, 0, fileHeaderSize); 44 | msBitmap.Write(dibBuffer, 0, dibBuffer.Length); 45 | msBitmap.Seek(0, SeekOrigin.Begin); 46 | 47 | return BitmapFrame.Create(msBitmap); 48 | } 49 | catch { } 50 | return null; 51 | } 52 | 53 | [StructLayout(LayoutKind.Sequential, Pack = 2)] 54 | private struct BITMAPFILEHEADER 55 | { 56 | public static readonly short BM = 0x4d42; // BM 57 | 58 | public short bfType; 59 | public int bfSize; 60 | public short bfReserved1; 61 | public short bfReserved2; 62 | public int bfOffBits; 63 | } 64 | 65 | [StructLayout(LayoutKind.Sequential)] 66 | private struct BITMAPINFOHEADER 67 | { 68 | public int biSize; 69 | public int biWidth; 70 | public int biHeight; 71 | public short biPlanes; 72 | public short biBitCount; 73 | public int biCompression; 74 | public int biSizeImage; 75 | public int biXPelsPerMeter; 76 | public int biYPelsPerMeter; 77 | public int biClrUsed; 78 | public int biClrImportant; 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /multiclip.ui/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Threading; 7 | using System.Windows; 8 | 9 | //TODO 10 | // Rebind hotkeys when the config file is updated 11 | // 12 | namespace MultiClip.UI 13 | { 14 | public partial class App : Application 15 | { 16 | Mutex mutex; 17 | 18 | void Application_Startup(object sender, StartupEventArgs e) 19 | { 20 | // Debug.Assert(false); 21 | 22 | if (Environment.GetCommandLineArgs().Contains("-kill")) 23 | { 24 | var runningGui = Process.GetProcessesByName(Path.GetFileNameWithoutExtension(Assembly.GetExecutingAssembly().Location)) 25 | .Where(p => p.Id != Process.GetCurrentProcess().Id); 26 | 27 | foreach (var server in runningGui) 28 | try 29 | { 30 | server.Kill(); 31 | } 32 | catch { } 33 | ClipboardMonitor.KillAllServers(); 34 | 35 | Shutdown(); 36 | } 37 | else 38 | { 39 | Log.WriteLine($"=============== Started ================="); 40 | Log.WriteLine(Assembly.GetExecutingAssembly().Location); 41 | //new SettingsView().ShowDialog();return; 42 | //IMPORTANT: Do not release the mutex. OS will release the mutex on app exit automatically. 43 | mutex = new Mutex(true, "multiclip.history"); 44 | if (mutex.WaitOne(0)) 45 | { 46 | StartApp(); 47 | } 48 | else 49 | { 50 | Operations.MsgBox("Another instance of the application is already running.", "MultiClip"); 51 | Shutdown(); 52 | } 53 | } 54 | } 55 | 56 | void StartApp() 57 | { 58 | //The app must be hosted as x86 otherwise the Clipboard some operations can lead to 59 | //the CLR crash. Shocking! 60 | // 61 | //Very tempting to use Caliburn.Micro but for such a simple UI it's a bit overkill. 62 | //But more importantly the current CB.M depends on .NET v4.5 at least. It also requires 63 | //System.Windows.Interactivity, which is distributed by MS individually. Thus the deployment pressure 64 | //is to much for such a simple app as this one. 65 | // 66 | //Packing of MultiClip.exe into MultiClip.UI.exe resources is also motivated by the deployment considerations 67 | 68 | //SettingsView.Popup(); 69 | //return; 70 | AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; 71 | 72 | ClipboardMonitor.Restart(); 73 | 74 | new Bootstrapper().Run(); 75 | } 76 | 77 | void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) 78 | { 79 | #if DEBUG 80 | Operations.MsgBox(e.ExceptionObject.ToString(), "MultiClip critical error"); 81 | #else 82 | Debug.Assert(false, e.ToString()); 83 | #endif 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /choco/update_package.cs: -------------------------------------------------------------------------------- 1 | //css_dir %WINDOWS_DESKTOP_APP% 2 | ///css_ac 3 | 4 | using System.IO; 5 | using System.Net; 6 | using System.Text; 7 | using System.Diagnostics; 8 | using System; 9 | 10 | //void main() 11 | //{ 12 | Console.WriteLine("Starting..."); 13 | 14 | ServicePointManager.Expect100Continue = true; 15 | ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; 16 | 17 | var url = "https://github.com/oleg-shilo/multiclip/releases/download/v1.4.5.0/multiclip.v1.4.5.0.7z"; 18 | 19 | var installScript = @"tools\chocolateyInstall.ps1"; 20 | 21 | var checksum = calcChecksum(url); 22 | // var cheksum = "E1809AD6433A91B2FF4803E7F4B15AE0FA88905A28949EAC5590F7D9FD9BE9C3"; 23 | Console.WriteLine(checksum); 24 | 25 | var code = File.ReadAllText(installScript + ".template") 26 | .Replace("$url = ???", "$url = '" + url + "'") 27 | .Replace("$checksum = ???", "$checksum = '" + checksum + "'"); 28 | 29 | File.WriteAllText(installScript, code); 30 | Console.WriteLine("--------------"); 31 | Console.WriteLine(code); 32 | Console.WriteLine("--------------"); 33 | Console.WriteLine(); 34 | Console.WriteLine("Done..."); 35 | //} 36 | 37 | string calcChecksum(string url) 38 | { 39 | var file = "multiclip.7z"; 40 | DownloadBinary(url, file, (step, total) => Console.Write("\r{0}%\r", (int)(step * 100.0 / total))); 41 | Console.WriteLine(); 42 | 43 | var checksum = run(@"C:\ProgramData\chocolatey\tools\checksum.exe", "-t sha256 -f \"" + file + "\"", echo: false).Trim(); 44 | return checksum; 45 | } 46 | 47 | void DownloadBinary(string url, string destinationPath, Action onProgress = null) 48 | { 49 | var sb = new StringBuilder(); 50 | byte[] buf = new byte[1024 * 4]; 51 | 52 | var request = WebRequest.Create(url); 53 | var response = (HttpWebResponse)request.GetResponse(); 54 | 55 | if (File.Exists(destinationPath)) 56 | File.Delete(destinationPath); 57 | 58 | using (var destStream = new FileStream(destinationPath, FileMode.CreateNew)) 59 | using (var resStream = response.GetResponseStream()) 60 | { 61 | int totalCount = 0; 62 | int count = 0; 63 | 64 | while (0 < (count = resStream.Read(buf, 0, buf.Length))) 65 | { 66 | destStream.Write(buf, 0, count); 67 | 68 | totalCount += count; 69 | if (onProgress != null) 70 | onProgress(totalCount, response.ContentLength); 71 | } 72 | } 73 | 74 | if (File.ReadAllText(destinationPath).Contains("Error 404")) 75 | throw new Exception($"Resource {url} cannot be downloaded."); 76 | } 77 | 78 | string run(string app, string args, bool echo = true) 79 | { 80 | StringBuilder sb = new StringBuilder(); 81 | Process myProcess = new Process(); 82 | myProcess.StartInfo.FileName = app; 83 | myProcess.StartInfo.Arguments = args; 84 | myProcess.StartInfo.UseShellExecute = false; 85 | myProcess.StartInfo.RedirectStandardOutput = true; 86 | myProcess.StartInfo.CreateNoWindow = true; 87 | myProcess.Start(); 88 | 89 | string line = null; 90 | 91 | while (null != (line = myProcess.StandardOutput.ReadLine())) 92 | { 93 | Console.WriteLine(line); 94 | sb.Append(line); 95 | } 96 | myProcess.WaitForExit(); 97 | return sb.ToString(); 98 | } -------------------------------------------------------------------------------- /multiclip/multiclip.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {7B23E52A-BB74-4B36-B57E-858B7D256CE7} 8 | WinExe 9 | multiclip 10 | multiclip 11 | v4.8 12 | 512 13 | true 14 | true 15 | 16 | 17 | 18 | AnyCPU 19 | true 20 | full 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | prompt 25 | 4 26 | 27 | 28 | AnyCPU 29 | pdbonly 30 | true 31 | bin\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | 36 | 37 | 38 | 39 | 40 | ..\..\multiclip\multiclip.ui\Resources\tray_icon.black.ico 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | md "$(SolutionDir)publish" 66 | 67 | "$(TargetPath)" -s "$(SolutionDir)multiclip.ui\$(OutDir)multiclip.ui.exe" "$(TargetDir)multiclip.ui" 68 | copy "$(SolutionDir)multiclip.server\$(OutDir)multiclip.server.exe" "$(TargetDir)multiclip.server.exe" 69 | 70 | copy "$(TargetDir)multiclip.server.exe" "$(SolutionDir)publish\multiclip.server.exe" 71 | copy "$(TargetPath)" "$(SolutionDir)publish\multiclip.exe" 72 | copy "$(TargetDir)multiclip.ui" "$(SolutionDir)publish\multiclip.ui" 73 | echo Publised: "$(SolutionDir)publish\$(TargetFileName)" 74 | 75 | -------------------------------------------------------------------------------- /multiclip.ui/SettingsView.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Windows; 8 | using System.Windows.Controls; 9 | using System.Windows.Controls.Primitives; 10 | using System.Windows.Data; 11 | using System.Windows.Media; 12 | 13 | namespace MultiClip.UI 14 | { 15 | /// 16 | /// Interaction logic for SettingsView.xaml 17 | /// 18 | public partial class SettingsView : Window 19 | { 20 | SettingsViewModel viewModel; 21 | 22 | public static bool EnsureDefaults() 23 | { 24 | return SettingsViewModel.EnsureDefaults(); 25 | } 26 | 27 | static SettingsView activeView; 28 | 29 | public static void Popup() 30 | { 31 | if (activeView == null) 32 | { 33 | try 34 | { 35 | HotKeys.Instance.UnregisterAll(); 36 | 37 | (activeView = new SettingsView()).ShowDialog(); 38 | } 39 | catch { } 40 | finally 41 | { 42 | activeView = null; 43 | 44 | HotKeysMapping.Bind(HotKeys.Instance, TrayIcon.InvokeMenu); 45 | } 46 | } 47 | } 48 | 49 | public static void CloseIfAny() 50 | { 51 | try 52 | { 53 | if (activeView != null) 54 | activeView.Close(); 55 | } 56 | catch { } 57 | } 58 | 59 | public SettingsView() 60 | { 61 | InitializeComponent(); 62 | 63 | viewModel = SettingsViewModel.Load(); 64 | AutoBinder.BindOnLoad(this, viewModel); 65 | } 66 | 67 | void Window_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e) 68 | { 69 | if (e.Key == System.Windows.Input.Key.Escape) 70 | Close(); 71 | } 72 | 73 | void Window_Closed(object sender, EventArgs e) 74 | { 75 | viewModel.Process(); 76 | viewModel.Save(); 77 | HotKeysMapping.Save(HotKeyEditor.viewModel.HotKeys); 78 | } 79 | 80 | void EditHotKeys_Click(object sender, RoutedEventArgs e) 81 | { 82 | Close(); 83 | } 84 | 85 | void ClearHistory_Click(object sender, RoutedEventArgs e) 86 | { 87 | viewModel.ClearHistory(); 88 | } 89 | 90 | void HotKeyEditorHide_Click(object sender, RoutedEventArgs e) 91 | { 92 | HotKeyEditor.viewModel.UpdateCurrentSelection(); //to capture changes if any 93 | HotKeysMapping.Save(HotKeyEditor.viewModel.HotKeys); 94 | viewModel.RefreshHotKeysView(); 95 | } 96 | 97 | void PurgeHistory_Click(object sender, RoutedEventArgs e) 98 | { 99 | PurgeHistory.IsEnabled = false; 100 | this.Dispatcher.InUIThread(() => 101 | { 102 | viewModel.PurgeHistory(); 103 | PurgeHistory.IsEnabled = true; 104 | }); 105 | } 106 | 107 | private void StartWithWindows_Click(object sender, RoutedEventArgs e) 108 | { 109 | Process.Start("explorer", Environment.GetFolderPath(Environment.SpecialFolder.Startup)); 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /multiclip.server/MultiClip.Server.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {6DA643C6-E46F-4713-87A1-8DB62EB66C9A} 8 | Exe 9 | Properties 10 | MultiClip 11 | multiclip.server 12 | v4.8 13 | 512 14 | 15 | 16 | 17 | x86 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | false 26 | false 27 | 28 | 29 | x86 30 | pdbonly 31 | true 32 | bin\Release\ 33 | TRACE 34 | prompt 35 | 4 36 | false 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | Properties\AssemblyVersion.cs 49 | 50 | 51 | 52 | 53 | Form 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | ClipboardWatcher.cs 67 | 68 | 69 | 70 | 71 | copy "$(TargetPath)" "$(TargetDir)..\..\..\multiclip.ui\Resources\$(TargetFileName)" 72 | 73 | 80 | -------------------------------------------------------------------------------- /multiclip.ui/Utils/HotKeyEditorViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.ObjectModel; 3 | using System.Diagnostics; 4 | using System.Windows; 5 | using static MultiClip.UI.HotKeysMapping; 6 | 7 | namespace MultiClip.UI.Utils 8 | { 9 | public class HotKeyEditorViewModel : NotifyPropertyChangedBase 10 | { 11 | public void DoSomeTest(RoutedEventArgs e) 12 | { 13 | } 14 | 15 | public static HotKeyEditorViewModel Load() 16 | { 17 | return new HotKeyEditorViewModel(); 18 | } 19 | 20 | public HotKeyEditorViewModel() 21 | { 22 | Reset(); 23 | } 24 | 25 | public void Add() 26 | { 27 | var newBinding = new HotKeyBinding { Name = "New Binding" }; 28 | HotKeys.Add(newBinding); 29 | SelectedHotKey = newBinding; 30 | } 31 | 32 | public void Remove() 33 | { 34 | if (NonBuiltInCommand) 35 | { 36 | var index = HotKeys.IndexOf(SelectedHotKey); 37 | HotKeys.RemoveAt(index); 38 | index--; 39 | SelectedHotKey = HotKeys[Math.Max(index, 0)]; 40 | } 41 | } 42 | 43 | void Reset() 44 | { 45 | HotKeys.Clear(); 46 | 47 | foreach (HotKeyBinding item in HotKeysMapping.Load()) 48 | HotKeys.Add(item); 49 | } 50 | 51 | public string EnteredHotKey { get; set; } 52 | 53 | public string CurrentItemHotKey { get; set; } 54 | 55 | public bool NonBuiltInCommand 56 | { 57 | get { return !(SelectedHotKey?.Name ?? "").StartsWith("<"); } 58 | } 59 | 60 | public bool IsErrorSet { get { return !LastError.IsEmpty(); } } 61 | 62 | string lastError; 63 | 64 | public string LastError 65 | { 66 | get { return lastError; } 67 | 68 | set 69 | { 70 | lastError = value; 71 | OnPropertyChanged(() => this.LastError); 72 | OnPropertyChanged(() => this.IsErrorSet); 73 | } 74 | } 75 | 76 | public void UpdateCurrentSelection() 77 | { 78 | if (selectedHotKey != null) 79 | { 80 | if (!EnteredHotKey.IsEmpty()) 81 | selectedHotKey.HotKey = EnteredHotKey.ToMachineHotKey(); 82 | } 83 | } 84 | 85 | HotKeyBinding selectedHotKey; 86 | 87 | public HotKeyBinding SelectedHotKey 88 | { 89 | get { return selectedHotKey; } 90 | 91 | set 92 | { 93 | if (selectedHotKey != value) 94 | UpdateCurrentSelection(); 95 | 96 | selectedHotKey = value; 97 | 98 | if (selectedHotKey != null) 99 | { 100 | CurrentItemHotKey = selectedHotKey.HotKey.ToReadableHotKey(); 101 | LastError = null; 102 | } 103 | else 104 | { 105 | CurrentItemHotKey = 106 | LastError = null; 107 | } 108 | 109 | OnPropertyChanged(() => this.CurrentItemHotKey); 110 | OnPropertyChanged(() => this.SelectedHotKey); 111 | OnPropertyChanged(() => this.NonBuiltInCommand); 112 | } 113 | } 114 | 115 | public ObservableCollection HotKeys { get; set; } = new ObservableCollection(); 116 | } 117 | } -------------------------------------------------------------------------------- /multiclip.ui/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace MultiClip.UI.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MultiClip.UI.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). 65 | /// 66 | internal static System.Drawing.Icon tray_icon { 67 | get { 68 | object obj = ResourceManager.GetObject("tray_icon", resourceCulture); 69 | return ((System.Drawing.Icon)(obj)); 70 | } 71 | } 72 | 73 | /// 74 | /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). 75 | /// 76 | internal static System.Drawing.Icon tray_icon_black { 77 | get { 78 | object obj = ResourceManager.GetObject("tray_icon_black", resourceCulture); 79 | return ((System.Drawing.Icon)(obj)); 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /multiclip.ui/HistoryView.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows; 3 | using System.Windows.Controls; 4 | using System.Windows.Controls.Primitives; 5 | using System.Windows.Input; 6 | 7 | namespace MultiClip.UI 8 | { 9 | public partial class HistoryView : Window 10 | { 11 | private HistoryViewModel ViewModel 12 | { 13 | get { return (HistoryViewModel)DataContext; } 14 | set { DataContext = value; } 15 | } 16 | 17 | public HistoryView() 18 | { 19 | InitializeComponent(); 20 | ViewModel = new HistoryViewModel(); 21 | Closed += (s, e) => IsClosed = true; 22 | } 23 | 24 | private bool IsClosed = false; 25 | 26 | private void Window_PreviewKeyDown(object sender, KeyEventArgs e) 27 | { 28 | if (e.Key == Key.Escape) 29 | Hide(); 30 | 31 | if (e.Key == Key.Return) 32 | { 33 | Operations.SetClipboardTo(ViewModel.Selected.Location); 34 | Hide(); 35 | } 36 | } 37 | 38 | private void Window_Deactivated(object sender, EventArgs e) 39 | { 40 | ViewModel.Items.Clear(); 41 | //Hide(); 42 | CloseIfAny(); 43 | 44 | // ClipboardMonitor.Restart(); // has nasty artifacts 45 | } 46 | 47 | private static HistoryView activeView; 48 | 49 | public static string PopupActionName = ""; 50 | 51 | public static void Popup() 52 | { 53 | if (activeView == null || activeView.IsClosed) 54 | activeView = new HistoryView(); 55 | 56 | try 57 | { 58 | activeView.ViewModel.Reset(); 59 | activeView.History.SelectedIndex = 0; 60 | 61 | activeView.Show(); 62 | 63 | //activeView.CentreOnActiveScreen(); 64 | 65 | activeView.Activate(); 66 | } 67 | catch (InvalidOperationException) 68 | { 69 | activeView = null; 70 | } 71 | catch (Exception) 72 | { 73 | } 74 | } 75 | 76 | public static void CloseIfAny() 77 | { 78 | try 79 | { 80 | if (activeView != null) 81 | activeView.Close(); 82 | } 83 | catch { } 84 | } 85 | 86 | private void History_MouseDoubleClick(object sender, MouseButtonEventArgs e) 87 | { 88 | Operations.SetClipboardTo(ViewModel.Selected.Location); 89 | 90 | sender.GetParent()?.Close(); 91 | } 92 | 93 | private void History_Loaded(object sender, RoutedEventArgs e) 94 | { 95 | if (History.SelectedIndex == -1) 96 | History.SelectedIndex = 0; 97 | 98 | History.ItemContainerGenerator.StatusChanged += (s, a) => 99 | { 100 | if (History.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated) 101 | { 102 | var index = History.SelectedIndex; 103 | if (index >= 0) 104 | { 105 | var item = History.ItemContainerGenerator.ContainerFromIndex(index) as ListBoxItem; 106 | item?.Focus(); 107 | } 108 | } 109 | }; 110 | } 111 | 112 | private void Remove_Click(object sender, RoutedEventArgs e) 113 | { 114 | ViewModel.Remove(History.SelectedItem as HistoryItemViewModel); 115 | } 116 | 117 | private void Clear_Click(object sender, RoutedEventArgs e) 118 | { 119 | ViewModel.RemoveAll(); 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /multiclip.ui/Hosting/TrayIcon.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Diagnostics; 4 | using System.Drawing; 5 | using System.Globalization; 6 | using System.IO; 7 | using System.Reflection; 8 | using System.Resources; 9 | using System.Threading; 10 | using System.Windows.Forms; 11 | using MultiClip.UI.Properties; 12 | using ContextMenuStrip = System.Windows.Forms.ContextMenuStrip; 13 | using NotifyIcon = System.Windows.Forms.NotifyIcon; 14 | 15 | namespace MultiClip.UI 16 | { 17 | // Using RESX file immediately adds 17 false-positives ton VirusTotal (used by choco). 18 | // Even if RESX file is EMPTY!!!! Shocking. 19 | // Meaning antiviruses treat any custom resource sections as a threat. Madness!!! 20 | // using resources embedded as part of XAML compilation seems fine. 21 | // So piggybacking on XAML resources. 22 | public static class AppResources 23 | { 24 | static ResourceManager resourceMan => new ResourceManager("multiclip.ui.g", Assembly.GetExecutingAssembly()); 25 | 26 | static public Icon GetIcon(string name) 27 | { 28 | // if we need to explore the names in the resource section 29 | // foreach (DictionaryEntry item in resourceMan.GetResourceSet(CultureInfo.CurrentUICulture, true, true)) 30 | // Trace.WriteLine(item.Key); 31 | 32 | using (var stream = (MemoryStream)resourceMan.GetObject(name, CultureInfo.CurrentUICulture)) 33 | return new Icon(stream); 34 | } 35 | 36 | public static Icon tray_icon_black => GetIcon("resources/tray_icon.black.ico"); 37 | public static Icon tray_icon => GetIcon("resources/tray_icon.ico"); 38 | } 39 | 40 | class TrayIcon 41 | { 42 | static public EventHandler Rehook; 43 | static public EventHandler Test; 44 | static public EventHandler ShowSettings; 45 | static public EventHandler ShowHistory; 46 | static public EventHandler Exit; 47 | static public ToolStripMenuItem InvokeMenu; 48 | 49 | static NotifyIcon ni; 50 | 51 | static public void RefreshIcon() 52 | { 53 | // works but interferes with the mouse cursor and menus 54 | var icon = ni.Icon; 55 | ni.Icon = null; 56 | Application.DoEvents(); 57 | ni.Icon = icon; 58 | } 59 | 60 | static public void SetIcon(Icon icon) 61 | { 62 | if (ni != null) 63 | ni.Icon = icon; 64 | } 65 | 66 | static public void Close() 67 | { 68 | if (ni != null) 69 | { 70 | ni.Visible = false; 71 | ni.Dispose(); 72 | } 73 | } 74 | 75 | static public void Init() 76 | { 77 | ni = new NotifyIcon(); 78 | 79 | ni.Text = "MultiClip"; 80 | ni.Visible = true; 81 | ni.DoubleClick += ShowHistory; 82 | 83 | ni.ContextMenuStrip = new ContextMenuStrip(); 84 | ni.ContextMenuStrip.Items.Add("Open", null, ShowHistory); 85 | ni.ContextMenuStrip.Items.Add("Settings", null, ShowSettings); 86 | ni.ContextMenuStrip.Items.Add("-"); 87 | InvokeMenu = (ToolStripMenuItem)ni.ContextMenuStrip.Items.Add("Invoke"); 88 | // invoke.DropDownItems.Add(); 89 | // #if DEBUG 90 | ni.ContextMenuStrip.Items.Add("-"); 91 | ni.ContextMenuStrip.Items.Add("Reset", null, Rehook); 92 | #if DEBUG 93 | ni.ContextMenuStrip.Items.Add("Test", null, Test); 94 | #endif 95 | // #endif 96 | ni.ContextMenuStrip.Items.Add("-"); 97 | ni.ContextMenuStrip.Items.Add("Exit", null, Exit); 98 | 99 | ni.ContextMenuStrip.Items[0].Font = new Font(ni.ContextMenuStrip.Items[0].Font, FontStyle.Bold); 100 | 101 | SetIcon(AppResources.tray_icon); 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /multiclip.ui/Utils/HotKeyEditor.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using System.Windows; 6 | using System.Windows.Controls; 7 | using System.Windows.Input; 8 | using System.Windows.Media.Animation; 9 | 10 | namespace MultiClip.UI.Utils 11 | { 12 | /// 13 | /// Interaction logic for HotKeyEditor.xaml 14 | /// 15 | public partial class HotKeyEditor : UserControl 16 | { 17 | public HotKeyEditorViewModel viewModel; 18 | 19 | public HotKeyEditor() 20 | { 21 | InitializeComponent(); 22 | viewModel = HotKeyEditorViewModel.Load(); 23 | AutoBinder.BindOnLoad(this, viewModel); 24 | 25 | viewModel.PropertyChanged += ViewModel_PropertyChanged; 26 | HotKeys.SelectionChanged += HotKeys_SelectionChanged; 27 | 28 | viewModel.SelectedHotKey = viewModel.HotKeys.FirstOrDefault(); 29 | } 30 | 31 | void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) 32 | { 33 | if (e.PropertyName == nameof(viewModel.CurrentItemHotKey)) 34 | { 35 | HotKeyValue.Text = 36 | hotkey = viewModel.CurrentItemHotKey.ToReadableHotKey(); 37 | } 38 | else if (e.PropertyName == nameof(viewModel.IsErrorSet)) 39 | { 40 | if (viewModel.IsErrorSet) 41 | ShowError(); 42 | } 43 | } 44 | 45 | void HotKeys_SelectionChanged(object sender, SelectionChangedEventArgs e) 46 | { 47 | HotKeys.Items.Refresh(); //will update the item title if the bound items are changed 48 | HotKeys.ScrollIntoView(HotKeys.SelectedItem); 49 | } 50 | 51 | string hotkey; 52 | 53 | void KeysView_PreviewKeyDown(object sender, KeyEventArgs e) 54 | { 55 | var oldHotkey = hotkey; 56 | 57 | Key wpfKey = e.Key == Key.System ? e.SystemKey : e.Key; 58 | var formsKey = (System.Windows.Forms.Keys)KeyInterop.VirtualKeyFromKey(wpfKey); 59 | 60 | var key = formsKey.ToString(); 61 | var modifiers = ""; 62 | 63 | if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) 64 | modifiers += "Ctrl+"; 65 | 66 | if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)) 67 | modifiers += "Shift+"; 68 | 69 | if (Keyboard.IsKeyDown(Key.LeftAlt) || Keyboard.IsKeyDown(Key.RightAlt)) 70 | modifiers += "Alt+"; 71 | 72 | if (modifiers == "" || key.EndsWith("ShiftKey") || key.EndsWith("ControlKey") || key.EndsWith("Menu")) 73 | hotkey = ""; 74 | else 75 | hotkey = modifiers + key; 76 | 77 | hotkey = hotkey.ToReadableHotKey(); 78 | 79 | if (!hotkey.IsEmpty()) 80 | if (viewModel.HotKeys.Where(x => x != viewModel.SelectedHotKey).Any(x => x.HotKey.ToReadableHotKey() == hotkey)) 81 | { 82 | viewModel.LastError = "The hot key combination is already taken"; 83 | hotkey = ""; 84 | } 85 | 86 | viewModel.EnteredHotKey = 87 | HotKeyValue.Text = hotkey; 88 | } 89 | 90 | void KeysView_TextChanged(object sender, TextChangedEventArgs e) 91 | { 92 | HotKeyValue.Text = hotkey; 93 | } 94 | 95 | void EditFile_Click(object sender, RoutedEventArgs e) 96 | { 97 | this.GetParent().Close(); 98 | HotKeysMapping.Edit(); 99 | } 100 | 101 | void ShowError_Click(object sender, RoutedEventArgs e) 102 | { 103 | ShowError(); 104 | } 105 | 106 | void ShowError() 107 | { 108 | (FindResource("ShowError") as Storyboard)?.Begin(); 109 | } 110 | 111 | private void Test_Click(object sender, RoutedEventArgs e) 112 | { 113 | HotKeys.Items.Refresh(); 114 | } 115 | 116 | void TextBox_TextChanged(object sender, TextChangedEventArgs e) 117 | { 118 | this.InUIThread(100, HotKeys.Items.Refresh); 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /multiclip.server/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Security.Cryptography; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using System.Windows.Forms; 11 | using MultiClip.Server; 12 | 13 | namespace MultiClip 14 | { 15 | static class Program 16 | { 17 | [STAThread] 18 | static void Main(string[] args) 19 | { 20 | AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; 21 | // Debug.Assert(false); 22 | 23 | if (args.Contains("-start")) 24 | Start(); 25 | 26 | if (args.Contains("-purge")) 27 | ClipboardHistory.Purge(); 28 | 29 | if (args.Contains("-showdup")) 30 | ClipboardHistory.Purge(showOnly: true); 31 | 32 | if (args.Contains("-capture")) 33 | new ClipboardHistory().MakeSnapshot(); 34 | 35 | if (args.Contains("-toplaintext")) 36 | Win32.Clipboard.ToPlainText(); 37 | 38 | if (args.Contains("-clearall")) 39 | ClearAll(); 40 | 41 | var loadBuffer = args.FirstOrDefault(x => x.StartsWith("-load:")); 42 | 43 | if (loadBuffer.IsNotEmpty()) 44 | { 45 | // Debug.Assert(false); 46 | LoadSnapshot(loadBuffer.Replace("-load:", "")); 47 | } 48 | } 49 | 50 | private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) 51 | { 52 | Log.WriteLine($"{nameof(CurrentDomain_UnhandledException)}: {e}"); 53 | } 54 | 55 | static void KillAll() 56 | { 57 | var runningServers = Process.GetProcessesByName("multiclip.server"); 58 | if (runningServers.Any()) 59 | { 60 | using (var closeRequest = new EventWaitHandle(false, EventResetMode.ManualReset, Globals.CloseRequestName)) 61 | closeRequest.Set(); 62 | 63 | Parallel.ForEach(runningServers, server => 64 | { 65 | try 66 | { 67 | if (server.Id != Process.GetCurrentProcess().Id) 68 | { 69 | server.WaitForExit(500); 70 | if (!server.HasExited) 71 | server.Kill(); 72 | } 73 | } 74 | catch { } 75 | } 76 | ); 77 | Thread.Sleep(200); 78 | } 79 | } 80 | 81 | static void ClearAll() 82 | { 83 | ClipboardHistory.ClearAll(); 84 | } 85 | 86 | static void LoadSnapshot(string bufferLocation) 87 | { 88 | ClipboardHistory.LoadSnapshot(bufferLocation); 89 | } 90 | 91 | static void Start() 92 | { 93 | try 94 | { 95 | Log.WriteLine("================== Started =================="); 96 | 97 | var monitor = new ClipboardHistory(); 98 | 99 | var closeRequest = new EventWaitHandle(false, EventResetMode.ManualReset, Globals.CloseRequestName); 100 | ClipboardWatcher.OnClipboardChanged = () => 101 | { 102 | var p = new Process(); 103 | 104 | p.StartInfo.FileName = Assembly.GetExecutingAssembly().Location; 105 | p.StartInfo.Arguments = "-capture"; 106 | p.StartInfo.UseShellExecute = false; 107 | p.StartInfo.RedirectStandardOutput = true; 108 | p.StartInfo.CreateNoWindow = true; 109 | p.Start(); 110 | }; 111 | 112 | ClipboardWatcher.Enabled = true; 113 | 114 | Task.Factory.StartNew(() => 115 | { 116 | Console.WriteLine("Press 'Enter' to exit"); 117 | Console.ReadLine(); 118 | closeRequest.Set(); 119 | }); 120 | 121 | closeRequest.WaitOne(); 122 | } 123 | finally 124 | { 125 | ClipboardWatcher.Enabled = false; 126 | } 127 | } 128 | } 129 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![stand with Ukraine](https://img.shields.io/badge/stand_with-ukraine-ffd700.svg?labelColor=0057b7)](https://stand-with-ukraine.pp.ua) 2 | # MultiClip 3 | 4 | ## Overview 5 | 6 | [![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://oleg-shilo.github.io/cs-script/Donation.html) 7 | 8 | MultiClip is not a unique clipboard manager. There are quite a few very good products exist in this category. Though MultiClip is a clipboard manager that puts an extremely strong emphasis on the user experience particularly tailored for the programmers. 9 | 10 | MultiClip is noting else but an application that allows you to switch the clipboard content to any item from the interactive clipboard history dialog. 11 | 12 | These some of the strong MultiClip features: 13 | - **Support for all clipboard formats** 14 | _The format of the clipboard content is indicated by the corresponding icon on the selection dialog_ 15 | 16 | - **auto-completion style user experience** 17 | _The popup selection dialog mimics auto-completion feature present in many IDEs._ 18 | 19 | - **System-wide hot-key** 20 | _The selection dialog can be invoked by pressing configurable hotkey combination regardless of the current application focus. You can also MultiClip as a generic application launcher by binding an additional hot-keys to the custom application._ 21 | 22 | - **Preview of the clipboard history content** 23 | _Non-obstructive immediate preview of the text and image content_ 24 | 25 | - **Indication of the history item "age"** 26 | _The left side colour bar indicates how old the item in the history list is._ 27 | 28 | - **Full history encryption** 29 | _The history is encrypted and stored in the user profile._ 30 | 31 | - **Fully configurable** 32 | - Restoring the clipboard history after system restart 33 | - Start with Windows 34 | - Dark/Light theme 35 | 36 | - **Zero-deployment** 37 | The whole product is a single file 38 | 39 | ## Installation 40 | 41 | - Run _multiclip.exe_ 42 | 43 | - You can alsoinstall MultiClip form Chocolatey: 44 | ``` 45 | choco install multiclip 46 | ``` 47 | 48 | --- 49 | 50 | ### Important 51 | 52 | Be aware MS _**Windows Defender**_ may identify multiclip as an application containing `Trojan:Script/Wacatac.B!ml` (or oter) virus. This is the case at least for for MultiClip v1.2.0-1.3.0. 53 | 54 | While false positives are not so uncommon, it is rather puzzeling. This case the application that has passed multiple antivirus tests during Chocolayey moderation (virus screening and SHA protection) and yet, when deployed on the target PC it is flagged as dangerous by _Windows Defender_. 55 | 56 | _Windows Defender_ is in fact so aggressive that it deletes that executable even if you just build it in Visual Studio and start debugging it (with F5). 57 | The executable not even have any tird-party dependency so the only possible trigger for this can be the use of low-level Win API for intercepting system wide hot-key combinations. 58 | 59 | You can address this problem by adding the Windows Defender exclusion for `C:\ProgramData\chocolatey\lib\Multiclip` folder (see [here](https://github.com/oleg-shilo/multiclip/raw/master/docs/defender_exclusion.png)). 60 | 61 | --- 62 | 63 | ## Usage 64 | 65 | - Press `` CTRL+` `` to popup clipboard history selection dialog 66 | 67 | ![](https://github.com/oleg-shilo/multiclip/blob/master/docs/selection.png) 68 | 69 | - Select `settings` in the tray icon menu to popup the settings dialog: 70 | 71 | ![](https://github.com/oleg-shilo/multiclip/blob/master/docs/menu.png) 72 | 73 | ![](https://github.com/oleg-shilo/multiclip/blob/master/docs/config.png) 74 | 75 | ## Limitations 76 | 77 | - Clipboard API is one of the oldest WIN32 API domain. While it works quite well it is suffering from a few nasty flaws.

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 | } --------------------------------------------------------------------------------