├── .gitignore ├── LICENSE ├── README.md ├── changelog.md ├── lib ├── Class_CtlColors.ahk ├── Class_ImageButton.ahk ├── Class_LB_AdjustHeight.ahk └── Gui_Functions.ahk ├── main.ahk ├── resources ├── menuclip_demo_dark_theme.jpg └── menuclip_demo_final.gif └── src ├── MenuClip.ahk ├── config ├── Config.ahk ├── ConfigGuiGeneralOptions.ahk ├── ConfigGuiThemeOptions.ahk ├── ConfigManager.ahk ├── ConfigManagerGui.ahk └── ConfigThemeManager.ahk ├── controller ├── ClipManager.ahk ├── Controller.ahk └── TrayManager.ahk ├── model ├── CacheDirManager.ahk ├── ClipStore.ahk └── Model.ahk ├── test_scenarios.md └── view ├── MenuClipsViewHandler.ahk ├── MenuGui.ahk ├── MenuSearchHandler.ahk ├── MenuWindowHandler.ahk └── View.ahk /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | *.ini 3 | nppBackup/ 4 | AHK-Studio Backup/ 5 | cache/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Except where otherwise stated, this code is licensed under: 2 | 3 | MIT License 4 | 5 | Copyright (c) 2019 takanuva15 (github.com/takanuva15) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MenuClip 2 | MenuClip is a menu-based clipboard manager written by takanuva15 in AutoHotkey. (Tested on Windows 10 with AHK v1.1.32.00) 3 | 4 | When you press a hotkey, a small context menu will show up that indicates the last x things that were copied to your clipboard. 5 | 6 | #### _This project is in maintenance mode. No new features will be added; bugs may be fixed depending on bug severity and as time permits. (Please check the issue tracker to see if your bug was already raised)_ 7 | 8 | ### Demo: 9 | MenuClip functionality demo gif 10 | 11 | Dark theme: 12 | 13 | MenuClip dark theme screenshot 14 | 15 | ### Features: 16 | - Stores text clips into memory (doesn't support images or files) 17 | - Displays stored clips in a context menu via `Ctrl+Shift+V` 18 | - Stores 100 clips by default. (Afterwards, it will delete the oldest one and insert the newest clip at the top) 19 | - Has configurable options (eg shortcut, number of clips to store, gui size) 20 | - Can specify editors that use Shift+Insert for pasting instead of Ctrl+V 21 | - Has a dark theme option (can be automatically swapped in at a specified time) 22 | - Saves clips to file so that they are restored when the script is restarted 23 | - Has a search box to filter clips list 24 | 25 | ## How to Run 26 | 27 | 1. Download a zip file of the latest [release](https://github.com/takanuva15/MenuClip/releases). 28 | 1. Extract it to a directory named "MenuClip" wherever you store your scripts. 29 | 1. Double click on `main.ahk` to run it. 30 | 31 | ## Configuring the Script 32 | The configuration can be edited by right-clicking on MenuClip's system tray icon and selecting the `Edit Configuration` option. (Alternatively, you can directly edit the `config.ini` file, which is generated when you run the script for the first time.) 33 | Note: 34 | - When changing the shortcut to open the menu, you must use the AHK shortcut syntax (see [here](https://www.autohotkey.com/docs/Hotkeys.htm#Symbols) for a glossary). Example: To use `CapsLock` + `f` as the hotkey, you should write `CapsLock & f` for the hotkey config option. 35 | - To add a certain editor to the "Shift+Insert pasting" list, add its exe filename to the Shift Ins paste list. (You must comma-separate each exe. eg: `mintty.exe,runemacs.exe` Do not use newlines.) 36 | 37 | ## Contributing 38 | Please open up an issue if you see a bug. Depending on the situation, you can also make PRs, but check with me before starting work on a PR, especially if it is out-of-scope of this application. (eg a rotating clip-paste function which [ClipJump](https://github.com/aviaryan/Clipjump) already does, or something that assigns presets like [this](https://www.autohotkey.com/boards/viewtopic.php?t=65004) already does). 39 | 40 | In addition, if you are not restricted from downloading & running exe files on your computer, you should just download [Ditto](https://ditto-cp.sourceforge.io/) which does the same thing with orders of magnitude more functionality. You can also download [ClipClip](https://clipclip.com/) for Windows. (If you're on a Mac, you can download [ClipMenu](http://www.clipmenu.com/) which has the same essential features) 41 | 42 | Random: I tried my best to structure the code OO-style. 43 | 44 | ## Notes 45 | A [similar application](https://autohotkey.com/board/topic/69834-probably-yet-another-clipboard-manager/) was posted on the AutoHotkey forum by [spg SCOTT](https://www.autohotkey.com/boards/memberlist.php?mode=viewprofile&u=66846). It also fulfills the purpose of a context-based clipboard manager. 46 | 47 | You can check out the Trello board for tracking stories on the project [here](https://trello.com/b/wD95pQRR/menuclip-kanban-board). If you have an issue or bug, feel free to post it on GitHub. 48 | 49 | You can view a UML diagram of the application [here](https://www.lucidchart.com/documents/view/8b32b807-f1e5-4cb6-afa5-1380075d861b). (Note: UML will occasionally be out-of-date) 50 | 51 | ## Credits 52 | This program was developed in [AHK Studio](https://www.autohotkey.com/boards/viewtopic.php?t=300). 53 | 54 | Special thanks to the people who've helped me with coding obstacles by answering my questions on the AutoHotkey forums. You can see my various questions and people's responses [here](https://www.autohotkey.com/boards/search.php?author_id=117081&sr=posts). 55 | 56 | I used the following libraries for this script: 57 | + [`Class_CtlColors.ahk`](https://www.autohotkey.com/boards/viewtopic.php?t=2197) 58 | + [`Class_ImageButton.ahk`](https://www.autohotkey.com/boards/viewtopic.php?t=1103) 59 | + Misc listbox functions combined together in `Class_LB_Functions.ahk`. Please see the comments within the file for relevant links. 60 | + Misc gui functions combined together in `Gui_Functions.ahk`. Please see the comments within the file for relevant links. 61 | 62 | Please note that library files may not be covered under the MIT license. If such is the case, a comment will be placed at the top of the library file indicating its status. -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### v1.15.2 4 | 2020-05-02 Fixed bug where new clips would not be pasted properly due to not being updated in the filtered list array. 5 | 2020-05-02 Fixed bug where fade animation would play when pasting by pressing Enter. 6 | 2020-05-02 Added MenuClip functionality demo gif to repo. 7 | ### v1.15.1 8 | 2020-04-04 Refactored config files into new config directory. Extracted theme calculation logic to its own class. 9 | 2020-04-04 Added logic to swap theme without reloading if user currently has clips view open. (It reloads after you quit) 10 | ## v1.15.0 11 | 2020-03-31 Added a method to clean up the cache each time the script is loaded (the cache randomly gets messed up during normal operation) 12 | ### v1.14.2 13 | 2020-03-19 Fixed issue with converting special characters in display view. Fixed dead code in clip filtering 14 | 2020-03-18 Made the save button in editconfig dark theme 15 | ### v1.14.1 16 | 2020-03-16 Removed fade animation if pasting 17 | ## v1.14.0 18 | 2020-03-16 Added option to swap the theme automatically at a specified time 19 | ### v1.13.1 20 | 2020-03-08 Added tabs to separate theme options from general options 21 | ## v1.13.0 22 | 2020-03-07 Added a fade-away animation to the gui window 23 | ### v1.12.1 24 | 2020-03-07 Changed the left-click tracking to try to mitigate bug where left-click doesn't select correct item in listbox 25 | ## v1.12.0 26 | 2020-02-29 Added option to convert tabs and newlines to [\t] and [\n] when viewing clips 27 | ### v1.11.2 28 | 2020-02-29 Fixed bug that caused search term to remain in box if you exited gui window using Esc 29 | ### v1.11.1 30 | 2020-02-23 Multiple refactorings of the code base to reduce repetition and break down large classes 31 | ## v1.11.0 32 | 2020-02-17 Added tray option to clear cache and reload script 33 | ### v1.10.1 34 | 2020-02-17 Fixed bug that caused click-drag not to work if you clicked outside of gui to close it 35 | ## v1.10.0 36 | 2020-02-16 Added search box to filter clips list 37 | ### v1.9.1 38 | 2020-02-02 Added option to config height. Fixed bug where menu would render offscreen 39 | ## v1.9.0 40 | 2020-02-02 Changed the menu display to be a gui window now. Has some bugs but not going to worry about it since I'm switching to Ditto anyways. 41 | ### v1.8.1 42 | 2020-02-01 Recolored dropdown list in config gui 43 | ## v1.8.0 44 | 2020-01-31 Added a gui window that opens when you edit config (rather than opening up the ini file directly) 45 | ### v1.7.3 46 | 2020-01-19 Converted code structure into MVC 47 | ### v1.7.2 48 | 2020-01-19 Added 1/4 sec delay before adding a clip to the menu, so if an application is rapidly changing the clipboard, only the latest clip will be saved. (Fixes issue when copying text from IntelliJ terminal) 49 | ### v1.7.1 50 | 2020-01-18 Config file is now generated if it doesn't exist 51 | ## v1.7.0 52 | 2020-01-18 Clip menu contents are now saved to a cache folder and will be restored on restart 53 | ### v1.6.2 54 | 2020-01-11 Separated the clips array into its own class 55 | ### v1.6.1 56 | 2020-01-06 Fixed bug that would show identical copies of the same string if it was copied repeatedly consecutively 57 | ## v1.6.0 58 | 2020-01-05 Added option to directly modify config from Tray Menu. (Also modified Tray Menu to custom settings) 59 | ## v1.5.0 60 | 2020-01-05 Added shortcut config option 61 | ## v1.4.0 62 | 2020-01-05 Added a dark theme config option 63 | ## v1.3.0 64 | 2020-01-04 Added support for ini files. Now all configurable variables are read from ini. 65 | ## v1.2.0 66 | 2020-01-04 Added config option to specify which exe processes use Shift+Insert for pasting, and the menu will execute that for those processes 67 | ## v1.1.0 68 | 2020-01-03 Added easily configurable variables to `main.ahk` to modify menu settings 69 | # v1.0.0 70 | 2020-01-01 Refactored menu actions to its own class. Also put in limits to how much of long clips is shown in the menu. Renamed tray tooltip. 71 | 2019-12-29 72 | - Maximum storage size implemented. Currently set to 8 clips. 73 | - Selected menu item now shifts to the top of the menu 74 | 2019-12-22 Clipboard manager now stores clips and displays them in a context menu when you press Ctrl+Shift+V or CapsLock+J. Credit to teadrinker in [this post](https://www.autohotkey.com/boards/viewtopic.php?p=306818&sid=fcabbee4a2f810fb8fe9544a7f8fa688#p306818) for developing that code! 75 | 2019-12-14 Created a UML diagram 76 | 2019-12-08 Repo created. Added a readme and license 77 | -------------------------------------------------------------------------------- /lib/Class_CtlColors.ahk: -------------------------------------------------------------------------------- 1 | ; ====================================================================================================================== 2 | ; AHK 1.1+ 3 | ; ====================================================================================================================== 4 | ; Function: Auxiliary object to color controls on WM_CTLCOLOR... notifications. 5 | ; Supported controls are: Checkbox, ComboBox, DropDownList, Edit, ListBox, Radio, Text. 6 | ; Checkboxes and Radios accept only background colors due to design. 7 | ; Namespace: CtlColors 8 | ; Tested with: 1.1.25.02 9 | ; Tested on: Win 10 (x64) 10 | ; Change log: 1.0.04.00/2017-10-30/just me - added transparent background (BkColor = "Trans"). 11 | ; 1.0.03.00/2015-07-06/just me - fixed Change() to run properly for ComboBoxes. 12 | ; 1.0.02.00/2014-06-07/just me - fixed __New() to run properly with compiled scripts. 13 | ; 1.0.01.00/2014-02-15/just me - changed class initialization. 14 | ; 1.0.00.00/2014-02-14/just me - initial release. 15 | ; ====================================================================================================================== 16 | ; This software is provided 'as-is', without any express or implied warranty. 17 | ; In no event will the authors be held liable for any damages arising from the use of this software. 18 | ; ====================================================================================================================== 19 | Class CtlColors { 20 | ; =================================================================================================================== 21 | ; Class variables 22 | ; =================================================================================================================== 23 | ; Registered Controls 24 | Static Attached := {} 25 | ; OnMessage Handlers 26 | Static HandledMessages := {Edit: 0, ListBox: 0, Static: 0} 27 | ; Message Handler Function 28 | Static MessageHandler := "CtlColors_OnMessage" 29 | ; Windows Messages 30 | Static WM_CTLCOLOR := {Edit: 0x0133, ListBox: 0x134, Static: 0x0138} 31 | ; HTML Colors (BGR) 32 | Static HTML := {AQUA: 0xFFFF00, BLACK: 0x000000, BLUE: 0xFF0000, FUCHSIA: 0xFF00FF, GRAY: 0x808080, GREEN: 0x008000 33 | , LIME: 0x00FF00, MAROON: 0x000080, NAVY: 0x800000, OLIVE: 0x008080, PURPLE: 0x800080, RED: 0x0000FF 34 | , SILVER: 0xC0C0C0, TEAL: 0x808000, WHITE: 0xFFFFFF, YELLOW: 0x00FFFF} 35 | ; Transparent Brush 36 | Static NullBrush := DllCall("GetStockObject", "Int", 5, "UPtr") 37 | ; System Colors 38 | Static SYSCOLORS := {Edit: "", ListBox: "", Static: ""} 39 | ; Error message in case of errors 40 | Static ErrorMsg := "" 41 | ; Class initialization 42 | Static InitClass := CtlColors.ClassInit() 43 | ; =================================================================================================================== 44 | ; Constructor / Destructor 45 | ; =================================================================================================================== 46 | __New() { ; You must not instantiate this class! 47 | If (This.InitClass == "!DONE!") { ; external call after class initialization 48 | This["!Access_Denied!"] := True 49 | Return False 50 | } 51 | } 52 | ; ---------------------------------------------------------------------------------------------------------------- 53 | __Delete() { 54 | If This["!Access_Denied!"] 55 | Return 56 | This.Free() ; free GDI resources 57 | } 58 | ; =================================================================================================================== 59 | ; ClassInit Internal creation of a new instance to ensure that __Delete() will be called. 60 | ; =================================================================================================================== 61 | ClassInit() { 62 | CtlColorsInst := New CtlColors 63 | Return "!DONE!" 64 | } 65 | ; =================================================================================================================== 66 | ; CheckBkColor Internal check for parameter BkColor. 67 | ; =================================================================================================================== 68 | CheckBkColor(ByRef BkColor, Class) { 69 | This.ErrorMsg := "" 70 | If (BkColor != "") && !This.HTML.HasKey(BkColor) && !RegExMatch(BkColor, "^[[:xdigit:]]{6}$") { 71 | This.ErrorMsg := "Invalid parameter BkColor: " . BkColor 72 | Return False 73 | } 74 | BkColor := BkColor = "" ? This.SYSCOLORS[Class] 75 | : This.HTML.HasKey(BkColor) ? This.HTML[BkColor] 76 | : "0x" . SubStr(BkColor, 5, 2) . SubStr(BkColor, 3, 2) . SubStr(BkColor, 1, 2) 77 | Return True 78 | } 79 | ; =================================================================================================================== 80 | ; CheckTxColor Internal check for parameter TxColor. 81 | ; =================================================================================================================== 82 | CheckTxColor(ByRef TxColor) { 83 | This.ErrorMsg := "" 84 | If (TxColor != "") && !This.HTML.HasKey(TxColor) && !RegExMatch(TxColor, "i)^[[:xdigit:]]{6}$") { 85 | This.ErrorMsg := "Invalid parameter TextColor: " . TxColor 86 | Return False 87 | } 88 | TxColor := TxColor = "" ? "" 89 | : This.HTML.HasKey(TxColor) ? This.HTML[TxColor] 90 | : "0x" . SubStr(TxColor, 5, 2) . SubStr(TxColor, 3, 2) . SubStr(TxColor, 1, 2) 91 | Return True 92 | } 93 | ; =================================================================================================================== 94 | ; Attach Registers a control for coloring. 95 | ; Parameters: HWND - HWND of the GUI control 96 | ; BkColor - HTML color name, 6-digit hexadecimal RGB value, or "" for default color 97 | ; ----------- Optional 98 | ; TxColor - HTML color name, 6-digit hexadecimal RGB value, or "" for default color 99 | ; Return values: On success - True 100 | ; On failure - False, CtlColors.ErrorMsg contains additional informations 101 | ; =================================================================================================================== 102 | Attach(HWND, BkColor, TxColor := "") { 103 | ; Names of supported classes 104 | Static ClassNames := {Button: "", ComboBox: "", Edit: "", ListBox: "", Static: ""} 105 | ; Button styles 106 | Static BS_CHECKBOX := 0x2, BS_RADIOBUTTON := 0x8 107 | ; Editstyles 108 | Static ES_READONLY := 0x800 109 | ; Default class background colors 110 | Static COLOR_3DFACE := 15, COLOR_WINDOW := 5 111 | ; Initialize default background colors on first call ------------------------------------------------------------- 112 | If (This.SYSCOLORS.Edit = "") { 113 | This.SYSCOLORS.Static := DllCall("User32.dll\GetSysColor", "Int", COLOR_3DFACE, "UInt") 114 | This.SYSCOLORS.Edit := DllCall("User32.dll\GetSysColor", "Int", COLOR_WINDOW, "UInt") 115 | This.SYSCOLORS.ListBox := This.SYSCOLORS.Edit 116 | } 117 | This.ErrorMsg := "" 118 | ; Check colors --------------------------------------------------------------------------------------------------- 119 | If (BkColor = "") && (TxColor = "") { 120 | This.ErrorMsg := "Both parameters BkColor and TxColor are empty!" 121 | Return False 122 | } 123 | ; Check HWND ----------------------------------------------------------------------------------------------------- 124 | If !(CtrlHwnd := HWND + 0) || !DllCall("User32.dll\IsWindow", "UPtr", HWND, "UInt") { 125 | This.ErrorMsg := "Invalid parameter HWND: " . HWND 126 | Return False 127 | } 128 | If This.Attached.HasKey(HWND) { 129 | This.ErrorMsg := "Control " . HWND . " is already registered!" 130 | Return False 131 | } 132 | Hwnds := [CtrlHwnd] 133 | ; Check control's class ------------------------------------------------------------------------------------------ 134 | Classes := "" 135 | WinGetClass, CtrlClass, ahk_id %CtrlHwnd% 136 | This.ErrorMsg := "Unsupported control class: " . CtrlClass 137 | If !ClassNames.HasKey(CtrlClass) 138 | Return False 139 | ControlGet, CtrlStyle, Style, , , ahk_id %CtrlHwnd% 140 | If (CtrlClass = "Edit") 141 | Classes := ["Edit", "Static"] 142 | Else If (CtrlClass = "Button") { 143 | IF (CtrlStyle & BS_RADIOBUTTON) || (CtrlStyle & BS_CHECKBOX) 144 | Classes := ["Static"] 145 | Else 146 | Return False 147 | } 148 | Else If (CtrlClass = "ComboBox") { 149 | VarSetCapacity(CBBI, 40 + (A_PtrSize * 3), 0) 150 | NumPut(40 + (A_PtrSize * 3), CBBI, 0, "UInt") 151 | DllCall("User32.dll\GetComboBoxInfo", "Ptr", CtrlHwnd, "Ptr", &CBBI) 152 | Hwnds.Insert(NumGet(CBBI, 40 + (A_PtrSize * 2, "UPtr")) + 0) 153 | Hwnds.Insert(Numget(CBBI, 40 + A_PtrSize, "UPtr") + 0) 154 | Classes := ["Edit", "Static", "ListBox"] 155 | } 156 | If !IsObject(Classes) 157 | Classes := [CtrlClass] 158 | ; Check background color ----------------------------------------------------------------------------------------- 159 | If (BkColor <> "Trans") 160 | If !This.CheckBkColor(BkColor, Classes[1]) 161 | Return False 162 | ; Check text color ----------------------------------------------------------------------------------------------- 163 | If !This.CheckTxColor(TxColor) 164 | Return False 165 | ; Activate message handling on the first call for a class -------------------------------------------------------- 166 | For I, V In Classes { 167 | If (This.HandledMessages[V] = 0) 168 | OnMessage(This.WM_CTLCOLOR[V], This.MessageHandler) 169 | This.HandledMessages[V] += 1 170 | } 171 | ; Store values for HWND ------------------------------------------------------------------------------------------ 172 | If (BkColor = "Trans") 173 | Brush := This.NullBrush 174 | Else 175 | Brush := DllCall("Gdi32.dll\CreateSolidBrush", "UInt", BkColor, "UPtr") 176 | For I, V In Hwnds 177 | This.Attached[V] := {Brush: Brush, TxColor: TxColor, BkColor: BkColor, Classes: Classes, Hwnds: Hwnds} 178 | ; Redraw control ------------------------------------------------------------------------------------------------- 179 | DllCall("User32.dll\InvalidateRect", "Ptr", HWND, "Ptr", 0, "Int", 1) 180 | This.ErrorMsg := "" 181 | Return True 182 | } 183 | ; =================================================================================================================== 184 | ; Change Change control colors. 185 | ; Parameters: HWND - HWND of the GUI control 186 | ; BkColor - HTML color name, 6-digit hexadecimal RGB value, or "" for default color 187 | ; ----------- Optional 188 | ; TxColor - HTML color name, 6-digit hexadecimal RGB value, or "" for default color 189 | ; Return values: On success - True 190 | ; On failure - False, CtlColors.ErrorMsg contains additional informations 191 | ; Remarks: If the control isn't registered yet, Add() is called instead internally. 192 | ; =================================================================================================================== 193 | Change(HWND, BkColor, TxColor := "") { 194 | ; Check HWND ----------------------------------------------------------------------------------------------------- 195 | This.ErrorMsg := "" 196 | HWND += 0 197 | If !This.Attached.HasKey(HWND) 198 | Return This.Attach(HWND, BkColor, TxColor) 199 | CTL := This.Attached[HWND] 200 | ; Check BkColor -------------------------------------------------------------------------------------------------- 201 | If (BkColor <> "Trans") 202 | If !This.CheckBkColor(BkColor, CTL.Classes[1]) 203 | Return False 204 | ; Check TxColor ------------------------------------------------------------------------------------------------ 205 | If !This.CheckTxColor(TxColor) 206 | Return False 207 | ; Store Colors --------------------------------------------------------------------------------------------------- 208 | If (BkColor <> CTL.BkColor) { 209 | If (CTL.Brush) { 210 | If (Ctl.Brush <> This.NullBrush) 211 | DllCall("Gdi32.dll\DeleteObject", "Prt", CTL.Brush) 212 | This.Attached[HWND].Brush := 0 213 | } 214 | If (BkColor = "Trans") 215 | Brush := This.NullBrush 216 | Else 217 | Brush := DllCall("Gdi32.dll\CreateSolidBrush", "UInt", BkColor, "UPtr") 218 | For I, V In CTL.Hwnds { 219 | This.Attached[V].Brush := Brush 220 | This.Attached[V].BkColor := BkColor 221 | } 222 | } 223 | For I, V In Ctl.Hwnds 224 | This.Attached[V].TxColor := TxColor 225 | This.ErrorMsg := "" 226 | DllCall("User32.dll\InvalidateRect", "Ptr", HWND, "Ptr", 0, "Int", 1) 227 | Return True 228 | } 229 | ; =================================================================================================================== 230 | ; Detach Stop control coloring. 231 | ; Parameters: HWND - HWND of the GUI control 232 | ; Return values: On success - True 233 | ; On failure - False, CtlColors.ErrorMsg contains additional informations 234 | ; =================================================================================================================== 235 | Detach(HWND) { 236 | This.ErrorMsg := "" 237 | HWND += 0 238 | If This.Attached.HasKey(HWND) { 239 | CTL := This.Attached[HWND].Clone() 240 | If (CTL.Brush) && (CTL.Brush <> This.NullBrush) 241 | DllCall("Gdi32.dll\DeleteObject", "Prt", CTL.Brush) 242 | For I, V In CTL.Classes { 243 | If This.HandledMessages[V] > 0 { 244 | This.HandledMessages[V] -= 1 245 | If This.HandledMessages[V] = 0 246 | OnMessage(This.WM_CTLCOLOR[V], "") 247 | } } 248 | For I, V In CTL.Hwnds 249 | This.Attached.Remove(V, "") 250 | DllCall("User32.dll\InvalidateRect", "Ptr", HWND, "Ptr", 0, "Int", 1) 251 | CTL := "" 252 | Return True 253 | } 254 | This.ErrorMsg := "Control " . HWND . " is not registered!" 255 | Return False 256 | } 257 | ; =================================================================================================================== 258 | ; Free Stop coloring for all controls and free resources. 259 | ; Return values: Always True. 260 | ; =================================================================================================================== 261 | Free() { 262 | For K, V In This.Attached 263 | If (V.Brush) && (V.Brush <> This.NullBrush) 264 | DllCall("Gdi32.dll\DeleteObject", "Ptr", V.Brush) 265 | For K, V In This.HandledMessages 266 | If (V > 0) { 267 | OnMessage(This.WM_CTLCOLOR[K], "") 268 | This.HandledMessages[K] := 0 269 | } 270 | This.Attached := {} 271 | Return True 272 | } 273 | ; =================================================================================================================== 274 | ; IsAttached Check if the control is registered for coloring. 275 | ; Parameters: HWND - HWND of the GUI control 276 | ; Return values: On success - True 277 | ; On failure - False 278 | ; =================================================================================================================== 279 | IsAttached(HWND) { 280 | Return This.Attached.HasKey(HWND) 281 | } 282 | } 283 | ; ====================================================================================================================== 284 | ; CtlColors_OnMessage 285 | ; This function handles CTLCOLOR messages. There's no reason to call it manually! 286 | ; ====================================================================================================================== 287 | CtlColors_OnMessage(HDC, HWND) { 288 | Critical 289 | If CtlColors.IsAttached(HWND) { 290 | CTL := CtlColors.Attached[HWND] 291 | If (CTL.TxColor != "") 292 | DllCall("Gdi32.dll\SetTextColor", "Ptr", HDC, "UInt", CTL.TxColor) 293 | If (CTL.BkColor = "Trans") 294 | DllCall("Gdi32.dll\SetBkMode", "Ptr", HDC, "UInt", 1) ; TRANSPARENT = 1 295 | Else 296 | DllCall("Gdi32.dll\SetBkColor", "Ptr", HDC, "UInt", CTL.BkColor) 297 | Return CTL.Brush 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /lib/Class_ImageButton.ahk: -------------------------------------------------------------------------------- 1 | ;Note from takanuva15: Not under MIT license. See license below for license details 2 | ;I have made small edits to this class to prevent warnings popping up about undeclared variables 3 | 4 | ; ====================================================================================================================== 5 | ; Namespace: ImageButton 6 | ; Function: Create images and assign them to pushbuttons. 7 | ; Tested with: AHK 1.1.14.03 (A32/U32/U64) 8 | ; Tested on: Win 7 (x64) 9 | ; Change history: 1.4.00.00/2014-06-07/just me - fixed bug for button caption = "0", "000", etc. 10 | ; 1.3.00.00/2014-02-28/just me - added support for ARGB colors 11 | ; 1.2.00.00/2014-02-23/just me - added borders 12 | ; 1.1.00.00/2013-12-26/just me - added rounded and bicolored buttons 13 | ; 1.0.00.00/2013-12-21/just me - initial release 14 | ; How to use: 15 | ; 1. Create a push button (e.g. "Gui, Add, Button, vMyButton hwndHwndButton, Caption") using the 'Hwnd' option 16 | ; to get its HWND. 17 | ; 2. Call ImageButton.Create() passing two parameters: 18 | ; HWND - Button's HWND. 19 | ; Options* - variadic array containing up to 6 option arrays (see below). 20 | ; --------------------------------------------------------------------------------------------------------------- 21 | ; The index of each option object determines the corresponding button state on which the bitmap will be shown. 22 | ; MSDN defines 6 states (http://msdn.microsoft.com/en-us/windows/bb775975): 23 | ; PBS_NORMAL = 1 24 | ; PBS_HOT = 2 25 | ; PBS_PRESSED = 3 26 | ; PBS_DISABLED = 4 27 | ; PBS_DEFAULTED = 5 28 | ; PBS_STYLUSHOT = 6 <- used only on tablet computers (that's false for Windows Vista and 7, see below) 29 | ; If you don't want the button to be 'animated' on themed GUIs, just pass one option object with index 1. 30 | ; On Windows Vista and 7 themed bottons are 'animated' using the images of states 5 and 6 after clicked. 31 | ; --------------------------------------------------------------------------------------------------------------- 32 | ; Each option array may contain the following values: 33 | ; Index Value 34 | ; 1 Mode mandatory: 35 | ; 0 - unicolored or bitmap 36 | ; 1 - vertical bicolored 37 | ; 2 - horizontal bicolored 38 | ; 3 - vertical gradient 39 | ; 4 - horizontal gradient 40 | ; 5 - vertical gradient using StartColor at both borders and TargetColor at the center 41 | ; 6 - horizontal gradient using StartColor at both borders and TargetColor at the center 42 | ; 7 - 'raised' style 43 | ; 2 StartColor mandatory for Option[1], higher indices will inherit the value of Option[1], if omitted: 44 | ; - ARGB integer value (0xAARRGGBB) or HTML color name ("Red"). 45 | ; - Path of an image file or HBITMAP handle for mode 0. 46 | ; 3 TargetColor mandatory for Option[1] if Mode > 0, ignored if Mode = 0. Higher indcices will inherit 47 | ; the color of Option[1], if omitted: 48 | ; - ARGB integer value (0xAARRGGBB) or HTML color name ("Red"). 49 | ; 4 TextColor optional, if omitted, the default text color will be used for Option[1], higher indices 50 | ; will inherit the color of Option[1]: 51 | ; - ARGB integer value (0xAARRGGBB) or HTML color name ("Red"). 52 | ; Default: 0xFF000000 (black) 53 | ; 5 Rounded optional: 54 | ; - Radius of the rounded corners in pixel; the letters 'H' and 'W' may be specified 55 | ; also to use the half of the button's height or width respectively. 56 | ; Default: 0 - not rounded 57 | ; 6 GuiColor optional, needed for rounded buttons if you've changed the GUI background color: 58 | ; - RGB integer value (0xRRGGBB) or HTML color name ("Red"). 59 | ; Default: AHK default GUI background color 60 | ; 7 BorderColor optional, ignored for modes 0 (bitmap) and 7, color of the border: 61 | ; - RGB integer value (0xRRGGBB) or HTML color name ("Red"). 62 | ; 8 BorderWidth optional, ignored for modes 0 (bitmap) and 7, width of the border in pixels: 63 | ; - Default: 1 64 | ; --------------------------------------------------------------------------------------------------------------- 65 | ; If the the button has a caption it will be drawn above the bitmap. 66 | ; Credits: THX tic for GDIP.AHK : http://www.autohotkey.com/forum/post-198949.html 67 | ; THX tkoi for ILBUTTON.AHK : http://www.autohotkey.com/forum/topic40468.html 68 | ; ====================================================================================================================== 69 | ; This software is provided 'as-is', without any express or implied warranty. 70 | ; In no event will the authors be held liable for any damages arising from the use of this software. 71 | ; ====================================================================================================================== 72 | ; ====================================================================================================================== 73 | ; CLASS ImageButton() 74 | ; ====================================================================================================================== 75 | Class ImageButton { 76 | ; =================================================================================================================== 77 | ; PUBLIC PROPERTIES ================================================================================================= 78 | ; =================================================================================================================== 79 | Static DefGuiColor := "" ; default GUI color (read/write) 80 | Static DefTxtColor := "Black" ; default caption color (read/write) 81 | Static LastError := "" ; will contain the last error message, if any (readonly) 82 | ; =================================================================================================================== 83 | ; PRIVATE PROPERTIES ================================================================================================ 84 | ; =================================================================================================================== 85 | Static BitMaps := [] 86 | Static GDIPDll := 0 87 | Static GDIPToken := 0 88 | Static MaxOptions := 8 89 | ; HTML colors 90 | Static HTML := {BLACK: 0x000000, GRAY: 0x808080, SILVER: 0xC0C0C0, WHITE: 0xFFFFFF, MAROON: 0x800000 91 | , PURPLE: 0x800080, FUCHSIA: 0xFF00FF, RED: 0xFF0000, GREEN: 0x008000, OLIVE: 0x808000 92 | , YELLOW: 0xFFFF00, LIME: 0x00FF00, NAVY: 0x000080, TEAL: 0x008080, AQUA: 0x00FFFF, BLUE: 0x0000FF} 93 | ; Initialize 94 | Static ClassInit := ImageButton.InitClass() 95 | ; =================================================================================================================== 96 | ; PRIVATE METHODS =================================================================================================== 97 | ; =================================================================================================================== 98 | __New(P*) { 99 | Return False 100 | } 101 | ; =================================================================================================================== 102 | InitClass() { 103 | ; ---------------------------------------------------------------------------------------------------------------- 104 | ; Get AHK's default GUI background color 105 | GuiColor := DllCall("User32.dll\GetSysColor", "Int", 15, "UInt") ; COLOR_3DFACE is used by AHK as default 106 | This.DefGuiColor := ((GuiColor >> 16) & 0xFF) | (GuiColor & 0x00FF00) | ((GuiColor & 0xFF) << 16) 107 | Return True 108 | } 109 | ; =================================================================================================================== 110 | GdiplusStartup() { 111 | This.GDIPDll := This.GDIPToken := 0 112 | If (This.GDIPDll := DllCall("Kernel32.dll\LoadLibrary", "Str", "Gdiplus.dll", "Ptr")) { 113 | VarSetCapacity(SI, 24, 0) 114 | Numput(1, SI, 0, "Int") 115 | GDIPToken := 0 116 | If !DllCall("Gdiplus.dll\GdiplusStartup", "PtrP", GDIPToken, "Ptr", &SI, "Ptr", 0) 117 | This.GDIPToken := GDIPToken 118 | Else 119 | This.GdiplusShutdown() 120 | } 121 | Return This.GDIPToken 122 | } 123 | ; =================================================================================================================== 124 | GdiplusShutdown() { 125 | If This.GDIPToken 126 | DllCall("Gdiplus.dll\GdiplusShutdown", "Ptr", This.GDIPToken) 127 | If This.GDIPDll 128 | DllCall("Kernel32.dll\FreeLibrary", "Ptr", This.GDIPDll) 129 | This.GDIPDll := This.GDIPToken := 0 130 | } 131 | ; =================================================================================================================== 132 | FreeBitmaps() { 133 | For I, HBITMAP In This.BitMaps 134 | DllCall("Gdi32.dll\DeleteObject", "Ptr", HBITMAP) 135 | This.BitMaps := [] 136 | } 137 | ; =================================================================================================================== 138 | GetARGB(RGB) { 139 | ARGB := This.HTML.HasKey(RGB) ? This.HTML[RGB] : RGB 140 | Return (ARGB & 0xFF000000) = 0 ? 0xFF000000 | ARGB : ARGB 141 | } 142 | ; =================================================================================================================== 143 | PathAddRectangle(Path, X, Y, W, H) { 144 | Return DllCall("Gdiplus.dll\GdipAddPathRectangle", "Ptr", Path, "Float", X, "Float", Y, "Float", W, "Float", H) 145 | } 146 | ; =================================================================================================================== 147 | PathAddRoundedRect(Path, X1, Y1, X2, Y2, R) { 148 | D := (R * 2), X2 -= D, Y2 -= D 149 | DllCall("Gdiplus.dll\GdipAddPathArc" 150 | , "Ptr", Path, "Float", X1, "Float", Y1, "Float", D, "Float", D, "Float", 180, "Float", 90) 151 | DllCall("Gdiplus.dll\GdipAddPathArc" 152 | , "Ptr", Path, "Float", X2, "Float", Y1, "Float", D, "Float", D, "Float", 270, "Float", 90) 153 | DllCall("Gdiplus.dll\GdipAddPathArc" 154 | , "Ptr", Path, "Float", X2, "Float", Y2, "Float", D, "Float", D, "Float", 0, "Float", 90) 155 | DllCall("Gdiplus.dll\GdipAddPathArc" 156 | , "Ptr", Path, "Float", X1, "Float", Y2, "Float", D, "Float", D, "Float", 90, "Float", 90) 157 | Return DllCall("Gdiplus.dll\GdipClosePathFigure", "Ptr", Path) 158 | } 159 | ; =================================================================================================================== 160 | SetRect(ByRef Rect, X1, Y1, X2, Y2) { 161 | VarSetCapacity(Rect, 16, 0) 162 | NumPut(X1, Rect, 0, "Int"), NumPut(Y1, Rect, 4, "Int") 163 | NumPut(X2, Rect, 8, "Int"), NumPut(Y2, Rect, 12, "Int") 164 | Return True 165 | } 166 | ; =================================================================================================================== 167 | SetRectF(ByRef Rect, X, Y, W, H) { 168 | VarSetCapacity(Rect, 16, 0) 169 | NumPut(X, Rect, 0, "Float"), NumPut(Y, Rect, 4, "Float") 170 | NumPut(W, Rect, 8, "Float"), NumPut(H, Rect, 12, "Float") 171 | Return True 172 | } 173 | ; =================================================================================================================== 174 | SetError(Msg) { 175 | This.FreeBitmaps() 176 | This.GdiplusShutdown() 177 | This.LastError := Msg 178 | Return False 179 | } 180 | ; =================================================================================================================== 181 | ; PUBLIC METHODS ==================================================================================================== 182 | ; =================================================================================================================== 183 | Create(HWND, Options*) { 184 | ; Windows constants 185 | Static BCM_SETIMAGELIST := 0x1602 186 | , BS_CHECKBOX := 0x02, BS_RADIOBUTTON := 0x04, BS_GROUPBOX := 0x07, BS_AUTORADIOBUTTON := 0x09 187 | , BS_LEFT := 0x0100, BS_RIGHT := 0x0200, BS_CENTER := 0x0300, BS_TOP := 0x0400, BS_BOTTOM := 0x0800 188 | , BS_VCENTER := 0x0C00, BS_BITMAP := 0x0080 189 | , BUTTON_IMAGELIST_ALIGN_LEFT := 0, BUTTON_IMAGELIST_ALIGN_RIGHT := 1, BUTTON_IMAGELIST_ALIGN_CENTER := 4 190 | , ILC_COLOR32 := 0x20 191 | , OBJ_BITMAP := 7 192 | , RCBUTTONS := BS_CHECKBOX | BS_RADIOBUTTON | BS_AUTORADIOBUTTON 193 | , SA_LEFT := 0x00, SA_CENTER := 0x01, SA_RIGHT := 0x02 194 | , WM_GETFONT := 0x31 195 | ; ---------------------------------------------------------------------------------------------------------------- 196 | This.LastError := "" 197 | ; ---------------------------------------------------------------------------------------------------------------- 198 | ; Check HWND 199 | If !DllCall("User32.dll\IsWindow", "Ptr", HWND) 200 | Return This.SetError("Invalid parameter HWND!") 201 | ; ---------------------------------------------------------------------------------------------------------------- 202 | ; Check Options 203 | If !(IsObject(Options)) || (Options.MinIndex() <> 1) || (Options.MaxIndex() > This.MaxOptions) 204 | Return This.SetError("Invalid parameter Options!") 205 | ; ---------------------------------------------------------------------------------------------------------------- 206 | ; Get and check control's class and styles 207 | WinGetClass, BtnClass, ahk_id %HWND% 208 | ControlGet, BtnStyle, Style, , , ahk_id %HWND% 209 | If (BtnClass != "Button") || ((BtnStyle & 0xF ^ BS_GROUPBOX) = 0) || ((BtnStyle & RCBUTTONS) > 1) 210 | Return This.SetError("The control must be a pushbutton!") 211 | ; ---------------------------------------------------------------------------------------------------------------- 212 | ; Load GdiPlus 213 | If !This.GdiplusStartup() 214 | Return This.SetError("GDIPlus could not be started!") 215 | ; ---------------------------------------------------------------------------------------------------------------- 216 | ; Get the button's font 217 | GDIPFont := 0 218 | HFONT := DllCall("User32.dll\SendMessage", "Ptr", HWND, "UInt", WM_GETFONT, "Ptr", 0, "Ptr", 0, "Ptr") 219 | DC := DllCall("User32.dll\GetDC", "Ptr", HWND, "Ptr") 220 | DllCall("Gdi32.dll\SelectObject", "Ptr", DC, "Ptr", HFONT) 221 | PFONT := 222 | DllCall("Gdiplus.dll\GdipCreateFontFromDC", "Ptr", DC, "PtrP", PFONT) 223 | DllCall("User32.dll\ReleaseDC", "Ptr", HWND, "Ptr", DC) 224 | If !(PFONT) 225 | Return This.SetError("Couldn't get button's font!") 226 | ; ---------------------------------------------------------------------------------------------------------------- 227 | ; Get the button's rectangle 228 | VarSetCapacity(RECT, 16, 0) 229 | If !DllCall("User32.dll\GetWindowRect", "Ptr", HWND, "Ptr", &RECT) 230 | Return This.SetError("Couldn't get button's rectangle!") 231 | BtnW := NumGet(RECT, 8, "Int") - NumGet(RECT, 0, "Int") 232 | BtnH := NumGet(RECT, 12, "Int") - NumGet(RECT, 4, "Int") 233 | ; ---------------------------------------------------------------------------------------------------------------- 234 | ; Get the button's caption 235 | ControlGetText, BtnCaption, , ahk_id %HWND% 236 | If (ErrorLevel) 237 | Return This.SetError("Couldn't get button's caption!") 238 | ; ---------------------------------------------------------------------------------------------------------------- 239 | ; Create the bitmap(s) 240 | This.BitMaps := [] 241 | For Index, Option In Options { 242 | If !IsObject(Option) 243 | Continue 244 | BkgColor1 := BkgColor2 := TxtColor := Mode := Rounded := GuiColor := Image := "" 245 | ; Replace omitted options with the values of Options.1 246 | Loop, % This.MaxOptions { 247 | If (Option[A_Index] = "") 248 | Option[A_Index] := Options.1[A_Index] 249 | } 250 | ; ------------------------------------------------------------------------------------------------------------- 251 | ; Check option values 252 | ; Mode 253 | Mode := SubStr(Option.1, 1 ,1) 254 | If !InStr("0123456789", Mode) 255 | Return This.SetError("Invalid value for Mode in Options[" . Index . "]!") 256 | ; StartColor & TargetColor 257 | If (Mode = 0) 258 | && (FileExist(Option.2) || (DllCall("Gdi32.dll\GetObjectType", "Ptr", Option.2, "UInt") = OBJ_BITMAP)) 259 | Image := Option.2 260 | Else { 261 | If !(Option.2 + 0) && !This.HTML.HasKey(Option.2) 262 | Return This.SetError("Invalid value for StartColor in Options[" . Index . "]!") 263 | BkgColor1 := This.GetARGB(Option.2) 264 | If (Option.3 = "") 265 | Option.3 := Option.2 266 | If !(Option.3 + 0) && !This.HTML.HasKey(Option.3) 267 | Return This.SetError("Invalid value for TargetColor in Options[" . Index . "]!") 268 | BkgColor2 := This.GetARGB(Option.3) 269 | } 270 | ; TextColor 271 | If (Option.4 = "") 272 | Option.4 := This.DefTxtColor 273 | If !(Option.4 + 0) && !This.HTML.HasKey(Option.4) 274 | Return This.SetError("Invalid value for TxtColor in Options[" . Index . "]!") 275 | TxtColor := This.GetARGB(Option.4) 276 | ; Rounded 277 | Rounded := Option.5 278 | If (Rounded = "H") 279 | Rounded := BtnH * 0.5 280 | If (Rounded = "W") 281 | Rounded := BtnW * 0.5 282 | If !(Rounded + 0) 283 | Rounded := 0 284 | ; GuiColor 285 | If (Option.6 = "") 286 | Option.6 := This.DefGuiColor 287 | If !(Option.6 + 0) && !This.HTML.HasKey(Option.6) 288 | Return This.SetError("Invalid value for GuiColor in Options[" . Index . "]!") 289 | GuiColor := This.GetARGB(Option.6) 290 | ; BorderColor 291 | BorderColor := "" 292 | If (Option.7 <> "") { 293 | If !(Option.7 + 0) && !This.HTML.HasKey(Option.7) 294 | Return This.SetError("Invalid value for BorderColor in Options[" . Index . "]!") 295 | BorderColor := 0xFF000000 | This.GetARGB(Option.7) ; BorderColor must be always opaque 296 | } 297 | ; BorderWidth 298 | BorderWidth := Option.8 ? Option.8 : 1 299 | ; ------------------------------------------------------------------------------------------------------------- 300 | ; Create a GDI+ bitmap 301 | PBITMAP := 302 | DllCall("Gdiplus.dll\GdipCreateBitmapFromScan0", "Int", BtnW, "Int", BtnH, "Int", 0 303 | , "UInt", 0x26200A, "Ptr", 0, "PtrP", PBITMAP) 304 | ; Get the pointer to its graphics 305 | PGRAPHICS := 306 | DllCall("Gdiplus.dll\GdipGetImageGraphicsContext", "Ptr", PBITMAP, "PtrP", PGRAPHICS) 307 | ; Quality settings 308 | DllCall("Gdiplus.dll\GdipSetSmoothingMode", "Ptr", PGRAPHICS, "UInt", 4) 309 | DllCall("Gdiplus.dll\GdipSetInterpolationMode", "Ptr", PGRAPHICS, "Int", 7) 310 | DllCall("Gdiplus.dll\GdipSetCompositingQuality", "Ptr", PGRAPHICS, "UInt", 4) 311 | DllCall("Gdiplus.dll\GdipSetRenderingOrigin", "Ptr", PGRAPHICS, "Int", 0, "Int", 0) 312 | DllCall("Gdiplus.dll\GdipSetPixelOffsetMode", "Ptr", PGRAPHICS, "UInt", 4) 313 | ; Clear the background 314 | DllCall("Gdiplus.dll\GdipGraphicsClear", "Ptr", PGRAPHICS, "UInt", GuiColor) 315 | ; Create the image 316 | If (Image = "") { ; Create a BitMap based on the specified colors 317 | PathX := PathY := 0, PathW := BtnW, PathH := BtnH 318 | ; Create a GraphicsPath 319 | PPATH := 320 | DllCall("Gdiplus.dll\GdipCreatePath", "UInt", 0, "PtrP", PPATH) 321 | If (Rounded < 1) ; the path is a rectangular rectangle 322 | This.PathAddRectangle(PPATH, PathX, PathY, PathW, PathH) 323 | Else ; the path is a rounded rectangle 324 | This.PathAddRoundedRect(PPATH, PathX, PathY, PathW, PathH, Rounded) 325 | ; If BorderColor and BorderWidth are specified, 'draw' the border (not for Mode 7) 326 | If (BorderColor <> "") && (BorderWidth > 0) && (Mode <> 7) { 327 | ; Create a SolidBrush 328 | PBRUSH := 329 | DllCall("Gdiplus.dll\GdipCreateSolidFill", "UInt", BorderColor, "PtrP", PBRUSH) 330 | ; Fill the path 331 | DllCall("Gdiplus.dll\GdipFillPath", "Ptr", PGRAPHICS, "Ptr", PBRUSH, "Ptr", PPATH) 332 | ; Free the brush 333 | DllCall("Gdiplus.dll\GdipDeleteBrush", "Ptr", PBRUSH) 334 | ; Reset the path 335 | DllCall("Gdiplus.dll\GdipResetPath", "Ptr", PPATH) 336 | ; Add a new 'inner' path 337 | PathX := PathY := BorderWidth, PathW -= BorderWidth, PathH -= BorderWidth, Rounded -= BorderWidth 338 | If (Rounded < 1) ; the path is a rectangular rectangle 339 | This.PathAddRectangle(PPATH, PathX, PathY, PathW - PathX, PathH - PathY) 340 | Else ; the path is a rounded rectangle 341 | This.PathAddRoundedRect(PPATH, PathX, PathY, PathW, PathH, Rounded) 342 | ; If a BorderColor has been drawn, BkgColors must be opaque 343 | BkgColor1 := 0xFF000000 | BkgColor1 344 | BkgColor2 := 0xFF000000 | BkgColor2 345 | } 346 | PathW -= PathX 347 | PathH -= PathY 348 | If (Mode = 0) { ; the background is unicolored 349 | ; Create a SolidBrush 350 | PBRUSH := 351 | DllCall("Gdiplus.dll\GdipCreateSolidFill", "UInt", BkgColor1, "PtrP", PBRUSH) 352 | ; Fill the path 353 | DllCall("Gdiplus.dll\GdipFillPath", "Ptr", PGRAPHICS, "Ptr", PBRUSH, "Ptr", PPATH) 354 | } 355 | Else If (Mode = 1) || (Mode = 2) { ; the background is bicolored 356 | ; Create a LineGradientBrush 357 | This.SetRectF(RECTF, PathX, PathY, PathW, PathH) 358 | DllCall("Gdiplus.dll\GdipCreateLineBrushFromRect", "Ptr", &RECTF 359 | , "UInt", BkgColor1, "UInt", BkgColor2, "Int", Mode & 1, "Int", 3, "PtrP", PBRUSH) 360 | DllCall("Gdiplus.dll\GdipSetLineGammaCorrection", "Ptr", PBRUSH, "Int", 1) 361 | ; Set up colors and positions 362 | This.SetRect(COLORS, BkgColor1, BkgColor1, BkgColor2, BkgColor2) ; sorry for function misuse 363 | This.SetRectF(POSITIONS, 0, 0.5, 0.5, 1) ; sorry for function misuse 364 | DllCall("Gdiplus.dll\GdipSetLinePresetBlend", "Ptr", PBRUSH 365 | , "Ptr", &COLORS, "Ptr", &POSITIONS, "Int", 4) 366 | ; Fill the path 367 | DllCall("Gdiplus.dll\GdipFillPath", "Ptr", PGRAPHICS, "Ptr", PBRUSH, "Ptr", PPATH) 368 | } 369 | Else If (Mode >= 3) && (Mode <= 6) { ; the background is a gradient 370 | ; Determine the brush's width/height 371 | W := Mode = 6 ? PathW / 2 : PathW ; horizontal 372 | H := Mode = 5 ? PathH / 2 : PathH ; vertical 373 | ; Create a LineGradientBrush 374 | This.SetRectF(RECTF, PathX, PathY, W, H) 375 | DllCall("Gdiplus.dll\GdipCreateLineBrushFromRect", "Ptr", &RECTF 376 | , "UInt", BkgColor1, "UInt", BkgColor2, "Int", Mode & 1, "Int", 3, "PtrP", PBRUSH) 377 | DllCall("Gdiplus.dll\GdipSetLineGammaCorrection", "Ptr", PBRUSH, "Int", 1) 378 | ; Fill the path 379 | DllCall("Gdiplus.dll\GdipFillPath", "Ptr", PGRAPHICS, "Ptr", PBRUSH, "Ptr", PPATH) 380 | } 381 | Else { ; raised mode 382 | DllCall("Gdiplus.dll\GdipCreatePathGradientFromPath", "Ptr", PPATH, "PtrP", PBRUSH) 383 | ; Set Gamma Correction 384 | DllCall("Gdiplus.dll\GdipSetPathGradientGammaCorrection", "Ptr", PBRUSH, "UInt", 1) 385 | ; Set surround and center colors 386 | VarSetCapacity(ColorArray, 4, 0) 387 | NumPut(BkgColor1, ColorArray, 0, "UInt") 388 | DllCall("Gdiplus.dll\GdipSetPathGradientSurroundColorsWithCount", "Ptr", PBRUSH, "Ptr", &ColorArray 389 | , "IntP", 1) 390 | DllCall("Gdiplus.dll\GdipSetPathGradientCenterColor", "Ptr", PBRUSH, "UInt", BkgColor2) 391 | ; Set the FocusScales 392 | FS := (BtnH < BtnW ? BtnH : BtnW) / 3 393 | XScale := (BtnW - FS) / BtnW 394 | YScale := (BtnH - FS) / BtnH 395 | DllCall("Gdiplus.dll\GdipSetPathGradientFocusScales", "Ptr", PBRUSH, "Float", XScale, "Float", YScale) 396 | ; Fill the path 397 | DllCall("Gdiplus.dll\GdipFillPath", "Ptr", PGRAPHICS, "Ptr", PBRUSH, "Ptr", PPATH) 398 | } 399 | ; Free resources 400 | DllCall("Gdiplus.dll\GdipDeleteBrush", "Ptr", PBRUSH) 401 | DllCall("Gdiplus.dll\GdipDeletePath", "Ptr", PPATH) 402 | } Else { ; Create a bitmap from HBITMAP or file 403 | If (Image + 0) 404 | DllCall("Gdiplus.dll\GdipCreateBitmapFromHBITMAP", "Ptr", Image, "Ptr", 0, "PtrP", PBM) 405 | Else 406 | DllCall("Gdiplus.dll\GdipCreateBitmapFromFile", "WStr", Image, "PtrP", PBM) 407 | ; Draw the bitmap 408 | DllCall("Gdiplus.dll\GdipDrawImageRectI", "Ptr", PGRAPHICS, "Ptr", PBM, "Int", 0, "Int", 0 409 | , "Int", BtnW, "Int", BtnH) 410 | ; Free the bitmap 411 | DllCall("Gdiplus.dll\GdipDisposeImage", "Ptr", PBM) 412 | } 413 | ; ------------------------------------------------------------------------------------------------------------- 414 | ; Draw the caption 415 | If (BtnCaption <> "") { 416 | ; Create a StringFormat object 417 | HFORMAT := 418 | DllCall("Gdiplus.dll\GdipStringFormatGetGenericTypographic", "PtrP", HFORMAT) 419 | ; Text color 420 | DllCall("Gdiplus.dll\GdipCreateSolidFill", "UInt", TxtColor, "PtrP", PBRUSH) 421 | ; Horizontal alignment 422 | HALIGN := (BtnStyle & BS_CENTER) = BS_CENTER ? SA_CENTER 423 | : (BtnStyle & BS_CENTER) = BS_RIGHT ? SA_RIGHT 424 | : (BtnStyle & BS_CENTER) = BS_Left ? SA_LEFT 425 | : SA_CENTER 426 | DllCall("Gdiplus.dll\GdipSetStringFormatAlign", "Ptr", HFORMAT, "Int", HALIGN) 427 | ; Vertical alignment 428 | VALIGN := (BtnStyle & BS_VCENTER) = BS_TOP ? 0 429 | : (BtnStyle & BS_VCENTER) = BS_BOTTOM ? 2 430 | : 1 431 | DllCall("Gdiplus.dll\GdipSetStringFormatLineAlign", "Ptr", HFORMAT, "Int", VALIGN) 432 | ; Set render quality to system default 433 | DllCall("Gdiplus.dll\GdipSetTextRenderingHint", "Ptr", PGRAPHICS, "Int", 0) 434 | ; Set the text's rectangle 435 | VarSetCapacity(RECT, 16, 0) 436 | NumPut(BtnW, RECT, 8, "Float") 437 | NumPut(BtnH, RECT, 12, "Float") 438 | ; Draw the text 439 | DllCall("Gdiplus.dll\GdipDrawString", "Ptr", PGRAPHICS, "WStr", BtnCaption, "Int", -1 440 | , "Ptr", PFONT, "Ptr", &RECT, "Ptr", HFORMAT, "Ptr", PBRUSH) 441 | } 442 | ; ------------------------------------------------------------------------------------------------------------- 443 | ; Create a HBITMAP handle from the bitmap and add it to the array 444 | HBITMAP := 445 | DllCall("Gdiplus.dll\GdipCreateHBITMAPFromBitmap", "Ptr", PBITMAP, "PtrP", HBITMAP, "UInt", 0X00FFFFFF) 446 | This.BitMaps[Index] := HBITMAP 447 | ; Free resources 448 | DllCall("Gdiplus.dll\GdipDisposeImage", "Ptr", PBITMAP) 449 | DllCall("Gdiplus.dll\GdipDeleteBrush", "Ptr", PBRUSH) 450 | DllCall("Gdiplus.dll\GdipDeleteStringFormat", "Ptr", HFORMAT) 451 | DllCall("Gdiplus.dll\GdipDeleteGraphics", "Ptr", PGRAPHICS) 452 | ; Add the bitmap to the array 453 | } 454 | ; Now free the font object 455 | DllCall("Gdiplus.dll\GdipDeleteFont", "Ptr", PFONT) 456 | ; ---------------------------------------------------------------------------------------------------------------- 457 | ; Create the ImageList 458 | HIL := DllCall("Comctl32.dll\ImageList_Create" 459 | , "UInt", BtnW, "UInt", BtnH, "UInt", ILC_COLOR32, "Int", 6, "Int", 0, "Ptr") 460 | Loop, % (This.BitMaps.MaxIndex() > 1 ? 6 : 1) { 461 | HBITMAP := This.BitMaps.HasKey(A_Index) ? This.BitMaps[A_Index] : This.BitMaps.1 462 | DllCall("Comctl32.dll\ImageList_Add", "Ptr", HIL, "Ptr", HBITMAP, "Ptr", 0) 463 | } 464 | ; Create a BUTTON_IMAGELIST structure 465 | VarSetCapacity(BIL, 20 + A_PtrSize, 0) 466 | NumPut(HIL, BIL, 0, "Ptr") 467 | Numput(BUTTON_IMAGELIST_ALIGN_CENTER, BIL, A_PtrSize + 16, "UInt") 468 | ; Hide buttons's caption 469 | ControlSetText, , , ahk_id %HWND% 470 | Control, Style, +%BS_BITMAP%, , ahk_id %HWND% 471 | ; Assign the ImageList to the button 472 | SendMessage, %BCM_SETIMAGELIST%, 0, 0, , ahk_id %HWND% 473 | SendMessage, %BCM_SETIMAGELIST%, 0, % &BIL, , ahk_id %HWND% 474 | ; Free the bitmaps 475 | This.FreeBitmaps() 476 | ; ---------------------------------------------------------------------------------------------------------------- 477 | ; All done successfully 478 | This.GdiplusShutdown() 479 | Return True 480 | } 481 | ; =================================================================================================================== 482 | ; Set the default GUI color 483 | SetGuiColor(GuiColor) { 484 | ; GuiColor - RGB integer value (0xRRGGBB) or HTML color name ("Red"). 485 | If !(GuiColor + 0) && !This.HTML.HasKey(GuiColor) 486 | Return False 487 | This.DefGuiColor := (This.HTML.HasKey(GuiColor) ? This.HTML[GuiColor] : GuiColor) & 0xFFFFFF 488 | Return True 489 | } 490 | ; =================================================================================================================== 491 | ; Set the default text color 492 | SetTxtColor(TxtColor) { 493 | ; TxtColor - RGB integer value (0xRRGGBB) or HTML color name ("Red"). 494 | If !(TxtColor + 0) && !This.HTML.HasKey(TxtColor) 495 | Return False 496 | This.DefTxtColor := (This.HTML.HasKey(TxtColor) ? This.HTML[TxtColor] : TxtColor) & 0xFFFFFF 497 | Return True 498 | } 499 | } -------------------------------------------------------------------------------- /lib/Class_LB_AdjustHeight.ahk: -------------------------------------------------------------------------------- 1 | ;;License note by takanuva15: The 2 functions below this line is derived from a post by "SKAN" at (https://autohotkey.com/board/topic/33383-change-1-line-in-listbox-using-guicontrol/) and is NOT under the MIT license provided in the root directory of this repository. 2 | LB_InsertItemAtIndex(HListBox, item, index) { 3 | static LB_INSERTSTRING := 0x181 4 | SendMessage, %LB_INSERTSTRING%, % index - 1, &item, , ahk_id %HListBox% 5 | Return ErrorLevel 6 | } 7 | 8 | LB_DeleteItem(HListBox, index) { 9 | static LB_DELETESTRING := 0x182 10 | SendMessage, %LB_DELETESTRING%, % index - 1, 0, , ahk_id %HListBox% 11 | Return ErrorLevel 12 | } 13 | 14 | ;;License note by takanuva15: The code below this line is authored by "Just Me" (https://autohotkey.com/board/topic/89793-set-height-of-listbox-rows/#entry568445) and is NOT under the MIT license provided in the root directory of this repository. Please see the code comments for information on its license. 15 | LB_AdjustItemHeight(HListBox, Adjust) { 16 | Return LB_SetItemHeight(HListBox, LB_GetItemHeight(HListBox) + Adjust) 17 | } 18 | 19 | LB_GetItemHeight(HListBox) { 20 | Static LB_GETITEMHEIGHT := 0x01A1 21 | SendMessage, %LB_GETITEMHEIGHT%, 0, 0, , ahk_id %HListBox% 22 | Return ErrorLevel 23 | } 24 | 25 | LB_SetItemHeight(HListBox, NewHeight) { 26 | Static LB_SETITEMHEIGHT := 0x01A0 27 | SendMessage, %LB_SETITEMHEIGHT%, 0, %NewHeight%, , ahk_id %HListBox% 28 | WinSet, Redraw, , ahk_id %HListBox% 29 | Return ErrorLevel 30 | } 31 | 32 | ;;This function is derived from the LB_InsertItemAtIndex function above. It is under MIT license except for the LB_InsertItemAtIndex function itself. 33 | PopulateLBFromArray(HListBox, arr) { 34 | GuiControl, -Redraw, HListBox 35 | for index, element in arr { 36 | LB_InsertItemAtIndex(HListBox, element, index) 37 | } 38 | GuiControl, +Redraw, HListBox 39 | } -------------------------------------------------------------------------------- /lib/Gui_Functions.ahk: -------------------------------------------------------------------------------- 1 | GetControlValue(hWnd) { 2 | GuiControlGet, tmp,, %hWnd% 3 | return tmp 4 | } 5 | 6 | ;This function came from https://autohotkey.com/board/topic/85172-solved-gui-width-height/. Not under MIT license 7 | GetGuiSize(hwnd, ByRef w, ByRef h) { 8 | VarSetCapacity(rc, 16) 9 | DllCall("GetClientRect", "uint", hwnd, "uint", &rc) 10 | w := NumGet(rc, 8, "int"), h := NumGet(rc, 12, "int") 11 | } -------------------------------------------------------------------------------- /main.ahk: -------------------------------------------------------------------------------- 1 | ;[MenuClip Clipboard Manager] 2 | 3 | #NoEnv ; Recommended for performance and compatibility with future AutoHotkey releases. 4 | #Warn ; Enable warnings to assist with detecting common errors. 5 | SendMode Input ; Recommended for new scripts due to its superior speed and reliability. 6 | SetWorkingDir %A_ScriptDir% ; Ensures a consistent starting directory. 7 | #SingleInstance, Force 8 | 9 | VERSION := "v1.15.2" 10 | 11 | #Include 12 | #Include 13 | #Include 14 | #Include 15 | #Include src\MenuClip.ahk 16 | 17 | ;configManager := new MenuClip.Config.ConfigManager(VERSION) 18 | 19 | clipManager := new MenuClip.Controller.ClipManager() 20 | 21 | trayManager := new MenuClip.Controller.TrayManager(VERSION, clipManager) 22 | trayManager.configureTrayTooltip() 23 | trayManager.configureTrayOptions() 24 | 25 | clipManager.monitorClipboardChanges() 26 | 27 | showMenu := ObjBindMethod(clipManager, "showContextMenu") 28 | Hotkey, % clipManager.configManager.getShowMenuHotkey(), % showMenu 29 | 30 | return 31 | -------------------------------------------------------------------------------- /resources/menuclip_demo_dark_theme.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takanuva15/MenuClip/490c5ad84bed20f1855db4edc6640a311e8297a7/resources/menuclip_demo_dark_theme.jpg -------------------------------------------------------------------------------- /resources/menuclip_demo_final.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takanuva15/MenuClip/490c5ad84bed20f1855db4edc6640a311e8297a7/resources/menuclip_demo_final.gif -------------------------------------------------------------------------------- /src/MenuClip.ahk: -------------------------------------------------------------------------------- 1 | ;Container class for the classes that MenuClip directly uses 2 | class MenuClip { 3 | #Include %A_ScriptDir%\src\config\Config.ahk 4 | #Include %A_ScriptDir%\src\controller\Controller.ahk 5 | #Include %A_ScriptDir%\src\model\Model.ahk 6 | #Include %A_ScriptDir%\src\view\View.ahk 7 | } 8 | -------------------------------------------------------------------------------- /src/config/Config.ahk: -------------------------------------------------------------------------------- 1 | ;Container class for the model classes that MenuClip uses 2 | class Config { 3 | #Include %A_ScriptDir%\src\config\ConfigManager.ahk 4 | #Include %A_ScriptDir%\src\config\ConfigManagerGui.ahk 5 | } -------------------------------------------------------------------------------- /src/config/ConfigGuiGeneralOptions.ahk: -------------------------------------------------------------------------------- 1 | ;Handles configuration file reading & writing 2 | class ConfigGuiGeneralOptions { 3 | static configManager 4 | static CONFIG_HANDLE_HOTKEY 5 | static CONFIG_HANDLE_MAX_CLIPS_TO_STORE 6 | static CONFIG_HANDLE_MAX_WIDTH 7 | static CONFIG_HANDLE_MAX_HEIGHT 8 | static CONFIG_HANDLE_ALT_PASTE_APPS 9 | static CONFIG_HANDLE_THEME 10 | static CONFIG_HANDLE_CONV_SPEC_CHAR 11 | __New(configManager) { 12 | this.configManager := configManager 13 | } 14 | 15 | addAllOptions() { 16 | this.addHotkeyOpt() 17 | this.addMaxClipsToStoreOpt() 18 | this.addMaxWidth() 19 | this.addMaxHeight() 20 | this.addAltPasteAppsOpt() 21 | this.addConvSpecCharOpt() 22 | } 23 | 24 | addHotkeyOpt() { 25 | Gui EditConfig:Add, Text, w170 Section, Hotkey: 26 | Gui EditConfig:Add, Edit, x+5 yp-2 w100 h17 hWndShowMenuHotkey, % this.configManager.getShowMenuHotkey() 27 | this.CONFIG_HANDLE_HOTKEY := ShowMenuHotkey 28 | } 29 | 30 | addMaxClipsToStoreOpt() { 31 | Gui EditConfig:Add, Text, xs y+10 w170, Number of clips to store: 32 | Gui EditConfig:Add, Edit, x+5 yp-2 w32 h17 hWndMaxClipsToStore, % this.configManager.getMaxClipsToStore() 33 | this.CONFIG_HANDLE_MAX_CLIPS_TO_STORE := MaxClipsToStore 34 | } 35 | 36 | addMaxWidth() { 37 | Gui EditConfig:Add, Text, xs y+10 w170, Max width: 38 | Gui EditConfig:Add, Edit, x+5 yp-2 w32 h17 hWndMaxWidth, % this.configManager.getMaxWidth() 39 | this.CONFIG_HANDLE_MAX_WIDTH := MaxWidth 40 | } 41 | 42 | addMaxHeight() { 43 | Gui EditConfig:Add, Text, xs y+10 w170, Max height (# rows): 44 | Gui EditConfig:Add, Edit, x+5 yp-2 w32 h17 hWndMaxHeight, % this.configManager.getMaxHeight() 45 | this.CONFIG_HANDLE_MAX_HEIGHT := MaxHeight 46 | } 47 | 48 | addAltPasteAppsOpt() { 49 | Gui EditConfig:Add, Text, xs y+10 w170, App exes that use Shift+Ins pasting: (comma-separated) 50 | Gui EditConfig:Add, Edit, x+5 yp-2 w100 r2 hWndAltPasteApps, % this.configManager.getAltPasteApps() 51 | this.CONFIG_HANDLE_ALT_PASTE_APPS := AltPasteApps 52 | } 53 | 54 | addConvSpecCharOpt() { 55 | Gui EditConfig:Add, Text, xs y+10 w170, Convert tabs and newlines to [\t] and [\n] in clips view 56 | Gui EditConfig:Add, CheckBox, % "x+5 yp-2 w100 r2 hWndConvSpecChar Checked" this.configManager.getConvSpecChar() 57 | this.CONFIG_HANDLE_CONV_SPEC_CHAR := ConvSpecChar 58 | } 59 | 60 | saveConfigs() { 61 | this.configManager.writeConfigToFile(this.configManager.CONFIG_NAME_HOTKEY, GetControlValue(this.CONFIG_HANDLE_HOTKEY)) 62 | this.configManager.writeConfigToFile(this.configManager.CONFIG_NAME_MAX_CLIPS_TO_STORE, GetControlValue(this.CONFIG_HANDLE_MAX_CLIPS_TO_STORE)) 63 | this.configManager.writeConfigToFile(this.configManager.CONFIG_NAME_MAX_WIDTH, GetControlValue(this.CONFIG_HANDLE_MAX_WIDTH)) 64 | this.configManager.writeConfigToFile(this.configManager.CONFIG_NAME_MAX_HEIGHT, GetControlValue(this.CONFIG_HANDLE_MAX_HEIGHT)) 65 | this.configManager.writeConfigToFile(this.configManager.CONFIG_NAME_ALT_PASTE_APPS, GetControlValue(this.CONFIG_HANDLE_ALT_PASTE_APPS)) 66 | this.configManager.writeConfigToFile(this.configManager.CONFIG_NAME_CONV_SPEC_CHAR, GetControlValue(this.CONFIG_HANDLE_CONV_SPEC_CHAR)) 67 | } 68 | } -------------------------------------------------------------------------------- /src/config/ConfigGuiThemeOptions.ahk: -------------------------------------------------------------------------------- 1 | ;Handles configuration file reading & writing 2 | class ConfigGuiThemeOptions { 3 | static configManager 4 | static themes, themeStyle 5 | static CONFIG_HANDLE_THEME 6 | static CONFIG_HANDLE_AUTO_THEME_GROUPBOX 7 | static HR_OPTIONS := "01|02|03|04|05|06|07|08|09|10|11|12" 8 | static MINUTE_OPTIONS 9 | static CONFIG_HANDLE_DARK_START_TEXT, CONFIG_HANDLE_DARK_START_HR, CONFIG_HANDLE_DARK_START_MIN, CONFIG_HANDLE_DARK_START_PM 10 | static CONFIG_HANDLE_DARK_STOP_TEXT, CONFIG_HANDLE_DARK_STOP_HR, CONFIG_HANDLE_DARK_STOP_MIN, CONFIG_HANDLE_DARK_STOP_PM 11 | __New(configManager) { 12 | this.configManager := configManager 13 | this.configThemeManager := configManager.configThemeManager 14 | this.themeStyle := this.configThemeManager.CONFIG_VAL_THEME 15 | this.generateMinuteOptions() 16 | } 17 | 18 | addAllThemeOptions() { 19 | this.addThemeOpt() 20 | this.addAutoThemeOpts() 21 | this.disableAutoThemeConfigBySelectedTheme() 22 | } 23 | 24 | addThemeOpt() { 25 | this.themes := {"light":1, "dark":2, "auto":3} 26 | Gui EditConfig:Add, Text, xs y+10 w170, Theme: 27 | Gui EditConfig:Add, DDL, % "x+5 yp-2 w50 hWndMenuTheme Choose" this.themes[this.themeStyle], light|dark|auto 28 | if(this.configManager.getTheme() = "dark") { 29 | CtlColors.Attach(MenuTheme, "43474A","CCCCCC") 30 | } 31 | this.CONFIG_HANDLE_THEME := MenuTheme 32 | disableAutoThemeConfigBySelectedThemeFn := ObjBindMethod(this, "disableAutoThemeConfigBySelectedTheme") 33 | GuiControl +g, %MenuTheme%, % disableAutoThemeConfigBySelectedThemeFn 34 | } 35 | 36 | addAutoThemeOpts() { 37 | Gui EditConfig:Add, GroupBox, xs y+10 w280 hWndAutoThemeGroupBox Section, Automatic Theme Options 38 | this.CONFIG_HANDLE_AUTO_THEME_GROUPBOX := AutoThemeGroupBox 39 | Gui EditConfig:Add, Text, xs+10 yp+20 w140 hWndDarkStartText, Activate dark theme at: 40 | this.CONFIG_HANDLE_DARK_START_TEXT := DarkStartText 41 | 42 | Gui EditConfig:Add, DDL, % "x+0 yp-2 w35 hWndDarkStartHr Choose" this.configThemeManager.CONFIG_VAL_DARK_START_HR + 0, % this.HR_OPTIONS 43 | this.CONFIG_HANDLE_DARK_START_HR := DarkStartHr 44 | 45 | Gui EditConfig:Add, DDL, % "x+5 yp w35 hWndDarkStartMin Choose" this.configThemeManager.CONFIG_VAL_DARK_START_MIN + 1, % this.MINUTE_OPTIONS 46 | this.CONFIG_HANDLE_DARK_START_MIN := DarkStartMin 47 | 48 | Gui EditConfig:Add, DDL, % "x+5 yp w40 hWndDarkStartPM Choose" this.configThemeManager.CONFIG_VAL_DARK_START_PM + 1, AM|PM 49 | this.CONFIG_HANDLE_DARK_START_PM := DarkStartPM 50 | 51 | 52 | Gui EditConfig:Add, Text, xs+10 y+8 w140 hWndDarkStopText, Deactivate dark theme at: 53 | this.CONFIG_HANDLE_DARK_STOP_TEXT := DarkStopText 54 | 55 | Gui EditConfig:Add, DDL, % "x+0 yp-2 w35 hWndDarkStopHr Choose" this.configThemeManager.CONFIG_VAL_DARK_STOP_HR + 0, % this.HR_OPTIONS 56 | this.CONFIG_HANDLE_DARK_STOP_HR := DarkStopHr 57 | 58 | Gui EditConfig:Add, DDL, % "x+5 yp w35 hWndDarkStopMin Choose" this.configThemeManager.CONFIG_VAL_DARK_STOP_MIN + 1, % this.MINUTE_OPTIONS 59 | this.CONFIG_HANDLE_DARK_STOP_MIN := DarkStopMin 60 | 61 | Gui EditConfig:Add, DDL, % "x+5 yp w40 hWndDarkStopPM Choose" this.configThemeManager.CONFIG_VAL_DARK_STOP_PM + 1, AM|PM 62 | this.CONFIG_HANDLE_DARK_STOP_PM := DarkStopPM 63 | 64 | if(this.configManager.getTheme() = "dark") { 65 | CtlColors.Attach(DarkStartHr, "43474A","CCCCCC") 66 | CtlColors.Attach(DarkStartMin, "43474A","CCCCCC") 67 | CtlColors.Attach(DarkStartPM, "43474A","CCCCCC") 68 | CtlColors.Attach(DarkStopHr, "43474A","CCCCCC") 69 | CtlColors.Attach(DarkStopMin, "43474A","CCCCCC") 70 | CtlColors.Attach(DarkStopPM, "43474A","CCCCCC") 71 | } 72 | } 73 | 74 | disableAutoThemeConfigBySelectedTheme() { 75 | selectedTheme := GetControlValue(this.CONFIG_HANDLE_THEME) 76 | if(selectedTheme != "auto") { 77 | GuiControl, EditConfig:Disable, % this.CONFIG_HANDLE_AUTO_THEME_GROUPBOX 78 | GuiControl, EditConfig:Disable, % this.CONFIG_HANDLE_DARK_START_TEXT 79 | GuiControl, EditConfig:Disable, % this.CONFIG_HANDLE_DARK_START_HR 80 | GuiControl, EditConfig:Disable, % this.CONFIG_HANDLE_DARK_START_MIN 81 | GuiControl, EditConfig:Disable, % this.CONFIG_HANDLE_DARK_START_PM 82 | GuiControl, EditConfig:Disable, % this.CONFIG_HANDLE_DARK_STOP_TEXT 83 | GuiControl, EditConfig:Disable, % this.CONFIG_HANDLE_DARK_STOP_HR 84 | GuiControl, EditConfig:Disable, % this.CONFIG_HANDLE_DARK_STOP_MIN 85 | GuiControl, EditConfig:Disable, % this.CONFIG_HANDLE_DARK_STOP_PM 86 | } else { 87 | GuiControl, EditConfig:Enable, % this.CONFIG_HANDLE_AUTO_THEME_GROUPBOX 88 | GuiControl, EditConfig:Enable, % this.CONFIG_HANDLE_DARK_START_TEXT 89 | GuiControl, EditConfig:Enable, % this.CONFIG_HANDLE_DARK_START_HR 90 | GuiControl, EditConfig:Enable, % this.CONFIG_HANDLE_DARK_START_MIN 91 | GuiControl, EditConfig:Enable, % this.CONFIG_HANDLE_DARK_START_PM 92 | GuiControl, EditConfig:Enable, % this.CONFIG_HANDLE_DARK_STOP_TEXT 93 | GuiControl, EditConfig:Enable, % this.CONFIG_HANDLE_DARK_STOP_HR 94 | GuiControl, EditConfig:Enable, % this.CONFIG_HANDLE_DARK_STOP_MIN 95 | GuiControl, EditConfig:Enable, % this.CONFIG_HANDLE_DARK_STOP_PM 96 | } 97 | } 98 | 99 | generateMinuteOptions() { 100 | t := "00|01|02|03|04|05|06|07|08|09" 101 | Loop, % loopIndex := 50 102 | t := t . "|" . (60 - loopIndex--) 103 | this.MINUTE_OPTIONS := t 104 | } 105 | 106 | saveConfigs() { 107 | this.configManager.writeConfigToFile(this.configThemeManager.CONFIG_NAME_THEME, GetControlValue(this.CONFIG_HANDLE_THEME)) 108 | this.configManager.writeConfigToFile(this.configThemeManager.CONFIG_NAME_DARK_START_HR, GetControlValue(this.CONFIG_HANDLE_DARK_START_HR)) 109 | this.configManager.writeConfigToFile(this.configThemeManager.CONFIG_NAME_DARK_START_MIN, GetControlValue(this.CONFIG_HANDLE_DARK_START_MIN)) 110 | this.configManager.writeConfigToFile(this.configThemeManager.CONFIG_NAME_DARK_START_PM, this.convertPMValToBinary(GetControlValue(this.CONFIG_HANDLE_DARK_START_PM))) 111 | this.configManager.writeConfigToFile(this.configThemeManager.CONFIG_NAME_DARK_STOP_HR, GetControlValue(this.CONFIG_HANDLE_DARK_STOP_HR)) 112 | this.configManager.writeConfigToFile(this.configThemeManager.CONFIG_NAME_DARK_STOP_MIN, GetControlValue(this.CONFIG_HANDLE_DARK_STOP_MIN)) 113 | this.configManager.writeConfigToFile(this.configThemeManager.CONFIG_NAME_DARK_STOP_PM, this.convertPMValToBinary(GetControlValue(this.CONFIG_HANDLE_DARK_STOP_PM))) 114 | } 115 | 116 | convertPMValToBinary(pmVal) { 117 | return pmVal = "PM" 118 | } 119 | } -------------------------------------------------------------------------------- /src/config/ConfigManager.ahk: -------------------------------------------------------------------------------- 1 | #Include %A_ScriptDir%\src\config\ConfigThemeManager.ahk 2 | 3 | ;Handles configuration file reading & writing 4 | class ConfigManager { 5 | static CONFIG_FILE_NAME := "config.ini" 6 | static CONFIG_SECTION := "Configuration Options" 7 | static CONFIG_NAME_HOTKEY := "SHOW_MENU_HOTKEY" 8 | static CONFIG_NAME_MAX_CLIPS_TO_STORE := "MAX_CLIPS_TO_STORE" 9 | static CONFIG_NAME_MAX_WIDTH := "MAX_WIDTH" 10 | static CONFIG_NAME_MAX_HEIGHT := "MAX_HEIGHT" 11 | static CONFIG_NAME_ALT_PASTE_APPS := "SHIFT_INS_PASTE_APPS" 12 | static CONFIG_NAME_CONV_SPEC_CHAR := "CONV_SPEC_CHAR" 13 | 14 | static CONFIG_VAL_HOTKEY 15 | static CONFIG_VAL_MAX_CLIPS_TO_STORE 16 | static CONFIG_VAL_MAX_WIDTH 17 | static CONFIG_VAL_MAX_HEIGHT 18 | static CONFIG_VAL_ALT_PASTE_APPS 19 | static CONFIG_VAL_CONV_SPEC_CHAR 20 | 21 | __New(clipManager) { 22 | this.clipManager := clipManager 23 | this.configThemeManager := new MenuClip.Config.ConfigThemeManager(this) 24 | this.readAllConfigOptionsFromFile() 25 | this.writeAllConfigOptionsToFile() 26 | 27 | this.configManagerGui := new MenuClip.Config.ConfigManagerGui(this) 28 | } 29 | 30 | readConfigFromFile(configKeyName, defaultValue) { 31 | IniRead, tmpReadInStorageVar, % this.CONFIG_FILE_NAME, % this.CONFIG_SECTION, %configKeyName%, %defaultValue% 32 | return tmpReadInStorageVar 33 | } 34 | 35 | readAllConfigOptionsFromFile() { 36 | this.CONFIG_VAL_HOTKEY := this.readConfigFromFile(this.CONFIG_NAME_HOTKEY, "^+v") 37 | this.CONFIG_VAL_MAX_CLIPS_TO_STORE := this.readConfigFromFile(this.CONFIG_NAME_MAX_CLIPS_TO_STORE, 100) 38 | this.CONFIG_VAL_MAX_WIDTH := this.readConfigFromFile(this.CONFIG_NAME_MAX_WIDTH, 350) 39 | this.CONFIG_VAL_MAX_HEIGHT := this.readConfigFromFile(this.CONFIG_NAME_MAX_HEIGHT, 12) 40 | this.CONFIG_VAL_ALT_PASTE_APPS := this.readConfigFromFile(this.CONFIG_NAME_ALT_PASTE_APPS, A_Space) 41 | this.CONFIG_VAL_CONV_SPEC_CHAR := this.readConfigFromFile(this.CONFIG_NAME_CONV_SPEC_CHAR, 1) 42 | this.configThemeManager.determineTheme() 43 | } 44 | 45 | 46 | writeConfigToFile(configKeyName, configValue) { 47 | IniWrite, %configValue%, % this.CONFIG_FILE_NAME, % this.CONFIG_SECTION, %configKeyName% 48 | } 49 | 50 | writeAllConfigOptionsToFile() { 51 | this.writeConfigToFile(this.CONFIG_NAME_HOTKEY, this.CONFIG_VAL_HOTKEY) 52 | this.writeConfigToFile(this.CONFIG_NAME_MAX_CLIPS_TO_STORE, this.CONFIG_VAL_MAX_CLIPS_TO_STORE) 53 | this.writeConfigToFile(this.CONFIG_NAME_MAX_WIDTH, this.CONFIG_VAL_MAX_WIDTH) 54 | this.writeConfigToFile(this.CONFIG_NAME_MAX_HEIGHT, this.CONFIG_VAL_MAX_HEIGHT) 55 | this.writeConfigToFile(this.CONFIG_NAME_ALT_PASTE_APPS, this.CONFIG_VAL_ALT_PASTE_APPS) 56 | this.writeConfigToFile(this.CONFIG_NAME_CONV_SPEC_CHAR, this.CONFIG_VAL_CONV_SPEC_CHAR) 57 | this.configThemeManager.writeThemeConfigs() 58 | } 59 | 60 | getShowMenuHotkey() { 61 | return this.CONFIG_VAL_HOTKEY 62 | } 63 | 64 | getMaxClipsToStore() { 65 | return this.CONFIG_VAL_MAX_CLIPS_TO_STORE 66 | } 67 | 68 | getMaxWidth() { 69 | return this.CONFIG_VAL_MAX_WIDTH 70 | } 71 | 72 | getMaxHeight() { 73 | return this.CONFIG_VAL_MAX_HEIGHT 74 | } 75 | 76 | getAltPasteApps() { 77 | return this.CONFIG_VAL_ALT_PASTE_APPS 78 | } 79 | 80 | getConvSpecChar() { 81 | return this.CONFIG_VAL_CONV_SPEC_CHAR 82 | } 83 | 84 | getTheme() { 85 | return this.configThemeManager.calculatedTheme 86 | } 87 | 88 | openEditConfigWindow() { 89 | this.configManagerGui.showGui() 90 | } 91 | } -------------------------------------------------------------------------------- /src/config/ConfigManagerGui.ahk: -------------------------------------------------------------------------------- 1 | #Include %A_ScriptDir%\src\config\ConfigGuiGeneralOptions.ahk 2 | #Include %A_ScriptDir%\src\config\ConfigGuiThemeOptions.ahk 3 | 4 | ;Handles configuration file reading & writing 5 | class ConfigManagerGui { 6 | static configManager 7 | static themeStyle 8 | static configGuiGeneralOptions, configGuiThemeOptions 9 | static CONFIG_HANDLE_BUTTON_SAVE_AND_RELOAD 10 | __New(configManager) { 11 | this.configManager := configManager 12 | this.themeStyle := this.configManager.getTheme() 13 | this.configGuiGeneralOptions := new MenuClip.Config.ConfigGuiGeneralOptions(configManager) 14 | this.configGuiThemeOptions := new MenuClip.Config.ConfigGuiThemeOptions(configManager) 15 | 16 | Gui +hWndEditConfig 17 | Gui EditConfig:-MinimizeBox -MaximizeBox 18 | Gui EditConfig:Margin, 10, 10 19 | 20 | if(this.themeStyle = "dark") { 21 | Gui EditConfig:Color, 2B2B2B, 43474A 22 | Gui EditConfig:Font, cDDDDDD 23 | } 24 | 25 | Gui EditConfig:Add, Tab3, , General|Theme 26 | Gui EditConfig:Tab, General 27 | this.configGuiGeneralOptions.addAllOptions() 28 | Gui EditConfig:Tab, Theme 29 | this.configGuiThemeOptions.addAllThemeOptions() 30 | Gui EditConfig:Tab 31 | this.addSaveAndReloadButton() 32 | } 33 | 34 | addSaveAndReloadButton() { 35 | Gui EditConfig:Add, Button, x107 y+10 w110 h26 hWndSaveAndReload +Default, Save and Reload 36 | this.CONFIG_HANDLE_BUTTON_SAVE_AND_RELOAD := SaveAndReload 37 | saveConfigsAndReloadFn := ObjBindMethod(this, "saveConfigsAndReload") 38 | GuiControl +g, %SaveAndReload%, % saveConfigsAndReloadFn 39 | 40 | if(this.themeStyle = "dark") { 41 | NORMAL_STATE := [0, 0x43474A, , 0xDDDDDD, , , "White"] 42 | HOVER_STATE := [0, "Gray", , "White", , , "White"] 43 | ImageButton.create(SaveAndReload, NORMAL_STATE, HOVER_STATE) 44 | } 45 | } 46 | 47 | saveConfigsAndReload() { 48 | this.configGuiGeneralOptions.saveConfigs() 49 | this.configGuiThemeOptions.saveConfigs() 50 | Reload 51 | } 52 | 53 | showGui() { 54 | Gui EditConfig:Show, Autosize, Edit Configuration 55 | } 56 | } -------------------------------------------------------------------------------- /src/config/ConfigThemeManager.ahk: -------------------------------------------------------------------------------- 1 | ;Handles configuration file reading & writing 2 | class ConfigThemeManager { 3 | static CONFIG_NAME_THEME := "THEME" 4 | static CONFIG_NAME_DARK_START_HR := "DARK_START_HR" 5 | static CONFIG_NAME_DARK_START_MIN := "DARK_START_MIN" 6 | static CONFIG_NAME_DARK_START_PM := "DARK_START_PM" 7 | static CONFIG_NAME_DARK_STOP_HR := "DARK_STOP_HR" 8 | static CONFIG_NAME_DARK_STOP_MIN := "DARK_STOP_MIN" 9 | static CONFIG_NAME_DARK_STOP_PM := "DARK_STOP_PM" 10 | 11 | static CONFIG_VAL_THEME, calculatedTheme 12 | static CONFIG_VAL_DARK_START_HR, CONFIG_VAL_DARK_START_MIN, CONFIG_VAL_DARK_START_PM 13 | static CONFIG_VAL_DARK_STOP_HR, CONFIG_VAL_DARK_STOP_MIN, CONFIG_VAL_DARK_STOP_PM 14 | 15 | __New(configManager) { 16 | this.configManager := configManager 17 | } 18 | 19 | determineTheme() { 20 | this.CONFIG_VAL_THEME := this.configManager.readConfigFromFile(this.CONFIG_NAME_THEME, "light") 21 | this.CONFIG_VAL_DARK_START_HR := this.configManager.readConfigFromFile(this.CONFIG_NAME_DARK_START_HR, "05") 22 | this.CONFIG_VAL_DARK_START_MIN := this.configManager.readConfigFromFile(this.CONFIG_NAME_DARK_START_MIN, "00") 23 | this.CONFIG_VAL_DARK_START_PM := this.configManager.readConfigFromFile(this.CONFIG_NAME_DARK_START_PM, "1") 24 | this.CONFIG_VAL_DARK_STOP_HR := this.configManager.readConfigFromFile(this.CONFIG_NAME_DARK_STOP_HR, "09") 25 | this.CONFIG_VAL_DARK_STOP_MIN := this.configManager.readConfigFromFile(this.CONFIG_NAME_DARK_STOP_MIN, "00") 26 | this.CONFIG_VAL_DARK_STOP_PM := this.configManager.readConfigFromFile(this.CONFIG_NAME_DARK_STOP_PM, "0") 27 | 28 | if(this.CONFIG_VAL_THEME = "auto") { 29 | darkStartHr := this.CONFIG_VAL_DARK_START_HR + (this.CONFIG_VAL_DARK_START_PM ? 12 : 0) 30 | darkStopHr := this.CONFIG_VAL_DARK_STOP_HR + (this.CONFIG_VAL_DARK_STOP_PM ? 12 : 0) 31 | 32 | darkStartTime := this.convertTimeToFullStr(darkStartHr, this.CONFIG_VAL_DARK_START_MIN) 33 | darkStopTime := this.convertTimeToFullStr(darkStopHr, this.CONFIG_VAL_DARK_STOP_MIN) 34 | 35 | if(darkStopTime < darkStartTime) { 36 | EnvAdd, darkStopTime, 1, Days 37 | } 38 | if(A_Now >= darkStartTime && A_Now < darkStopTime) { 39 | this.calculatedTheme := "dark" 40 | reloadTime := this.getNextOccurrenceOfTimeAsFullStr(darkStopHr, this.CONFIG_VAL_DARK_STOP_MIN) 41 | EnvSub, reloadTime, %A_Now%, Seconds 42 | } else { 43 | this.calculatedTheme := "light" 44 | reloadTime := this.getNextOccurrenceOfTimeAsFullStr(darkStartHr, this.CONFIG_VAL_DARK_START_MIN) 45 | EnvSub, reloadTime, %A_Now%, Seconds 46 | } 47 | triggerGuiRecolorFn := ObjBindMethod(this, "triggerGuiRecolor") 48 | SetTimer, % triggerGuiRecolorFn, % reloadTime * -1000 49 | } else { 50 | this.calculatedTheme := this.CONFIG_VAL_THEME 51 | } 52 | } 53 | 54 | writeThemeConfigs() { 55 | this.configManager.writeConfigToFile(this.CONFIG_NAME_THEME, this.CONFIG_VAL_THEME) 56 | this.configManager.writeConfigToFile(this.CONFIG_NAME_DARK_START_HR, this.CONFIG_VAL_DARK_START_HR) 57 | this.configManager.writeConfigToFile(this.CONFIG_NAME_DARK_START_MIN, this.CONFIG_VAL_DARK_START_MIN) 58 | this.configManager.writeConfigToFile(this.CONFIG_NAME_DARK_START_PM, this.CONFIG_VAL_DARK_START_PM) 59 | this.configManager.writeConfigToFile(this.CONFIG_NAME_DARK_STOP_HR, this.CONFIG_VAL_DARK_STOP_HR) 60 | this.configManager.writeConfigToFile(this.CONFIG_NAME_DARK_STOP_MIN, this.CONFIG_VAL_DARK_STOP_MIN) 61 | this.configManager.writeConfigToFile(this.CONFIG_NAME_DARK_STOP_PM, this.CONFIG_VAL_DARK_STOP_PM) 62 | } 63 | 64 | ;parts were derived from https://autohotkey.com/board/topic/19960-got-problems-while-checking-the-system-time/://autohotkey.com/board/topic/19960-got-problems-while-checking-the-system-time/page-2?& 65 | ;not under MIT license 66 | ;takes in an hour (24-hour format) and a minute. Returns the next occurrence of that time today 67 | getNextOccurrenceOfTimeAsFullStr(hour, min) { 68 | next := this.convertTimeToFullStr(hour, min) 69 | if (next < A_Now) { 70 | EnvAdd, next, 1, Days 71 | } 72 | return next 73 | } 74 | 75 | ;converts an hour (24-hour format) and a minute into a full string with YYYYMMDDHH24MISS format 76 | convertTimeToFullStr(hour, min) { 77 | hourStr := (StrLen(hour) = 1 ? "0" : "") hour ;pads single digit hour 78 | minStr := (StrLen(min) = 1 ? "0" : "") min 79 | hhmm := hourStr . minStr 80 | return % A_YYYY A_MM A_DD hhmm "00" 81 | } 82 | 83 | triggerGuiRecolor() { 84 | recolorOpts := {"light":"dark", "dark":"light"} 85 | this.configManager.clipManager.menuGui.recolorGui(recolorOpts[this.calculatedTheme]) 86 | this.configManager.clipManager.menuGui.initReloadOnMenuQuit() 87 | } 88 | } -------------------------------------------------------------------------------- /src/controller/ClipManager.ahk: -------------------------------------------------------------------------------- 1 | ;Primary clipboard manager class. Should only be one instance for the duration of the script 2 | class ClipManager { 3 | static CLIP_TYPE_TEXT := 1 4 | static clipStore 5 | static MAX_CLIPS_TO_STORE 6 | static ALT_PASTE_APPS 7 | static lastSavedClipType 8 | __New() { 9 | this.configManager := new MenuClip.Config.ConfigManager(this) 10 | this.MAX_CLIPS_TO_STORE := this.configManager.getMaxClipsToStore() 11 | this.ALT_PASTE_APPS := this.configManager.getAltPasteApps() 12 | this.clipStore := new MenuClip.Model.ClipStore(this.configManager) 13 | this.postNewClipFn := ObjBindMethod(this, "postNewClip") 14 | this.saveClipFn := ObjBindMethod(this, "saveClip") 15 | 16 | this.menuGui := new MenuClip.View.MenuGui(this.configManager, this.clipStore, ObjBindMethod(this, "pasteClip")) 17 | } 18 | 19 | monitorClipboardChanges() { 20 | OnClipboardChange(this.postNewClipFn) 21 | } 22 | 23 | postNewClip(clipType) { 24 | this.lastSavedClipType := clipType 25 | saveClipFn := this.saveClipFn 26 | SetTimer, % saveClipFn, -250 27 | } 28 | 29 | saveClip() { 30 | clipType := this.lastSavedClipType 31 | if (clipType = this.CLIP_TYPE_TEXT) { 32 | ;avoid recording consecutive identical copies 33 | if(Clipboard = this.clipStore.getAtIndex(1)) { 34 | return 35 | } else if(this.clipStore.getSize() < this.MAX_CLIPS_TO_STORE) { 36 | this.clipStore.insertAtTop(Clipboard) 37 | this.menuGui.menuClipsViewHandler.insertItemAtTop(Clipboard) 38 | } else { 39 | this.clipStore.deleteAtIndex(this.MAX_CLIPS_TO_STORE) 40 | this.menuGui.menuClipsViewHandler.deleteItemAtIndex(this.MAX_CLIPS_TO_STORE) 41 | this.clipStore.insertAtTop(Clipboard) 42 | this.menuGui.menuClipsViewHandler.insertItemAtTop(Clipboard) 43 | } 44 | } 45 | } 46 | 47 | pasteClip(posClicked) { 48 | OnClipboardChange(this.postNewClipFn, 0) 49 | Clipboard := this.clipStore.getAtIndex(posClicked) 50 | WinGet, activeWin, ProcessName, A 51 | if(InStr(this.ALT_PASTE_APPS, activeWin)) { 52 | Send, +{Insert} 53 | } else { 54 | Send, ^v 55 | } 56 | this.clipStore.moveToTop(posClicked) 57 | this.menuGui.menuClipsViewHandler.moveToTop(posClicked) 58 | OnClipboardChange(this.postNewClipFn) 59 | } 60 | 61 | showContextMenu() { 62 | this.menuGui.showGui() 63 | } 64 | 65 | __Delete() { 66 | OnClipboardChange(this.saveClipFn, 0) 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /src/controller/Controller.ahk: -------------------------------------------------------------------------------- 1 | ;Container class for the controller classes that MenuClip uses 2 | class Controller { 3 | #Include %A_ScriptDir%\src\controller\TrayManager.ahk 4 | #Include %A_ScriptDir%\src\controller\ClipManager.ahk 5 | } -------------------------------------------------------------------------------- /src/controller/TrayManager.ahk: -------------------------------------------------------------------------------- 1 | ;Handles changes to the menu 2 | class TrayManager { 3 | static configManager 4 | static clipManager 5 | static VERSION 6 | __New(version, clipManager) { 7 | this.VERSION := version 8 | this.clipManager := clipManager 9 | this.configManager := clipManager.configManager 10 | } 11 | 12 | configureTrayTooltip() { 13 | Menu, Tray, Tip, % "MenuClip " . this.VERSION 14 | } 15 | 16 | configureTrayOptions() { 17 | Menu, Tray, NoStandard 18 | showAboutMessageFn := ObjBindMethod(this, "showAboutMessage") 19 | Menu, Tray, Add, &About, % showAboutMessageFn 20 | Menu, Tray, Add 21 | editConfigFileFn := ObjBindMethod(this, "editConfigFile") 22 | Menu, Tray, Add, Edit &Configuration, % editConfigFileFn 23 | Menu, Tray, Default, Edit &Configuration 24 | reloadScriptFn := ObjBindMethod(this, "reloadScript") 25 | Menu, Tray, Add, &Reload This Script, % reloadScriptFn 26 | clearCacheAndReloadFn := ObjBindMethod(this, "clearCacheAndReload") 27 | Menu, Tray, Add, Clear Cache && Reload, % clearCacheAndReloadFn 28 | Menu, Tray, Add 29 | exitScriptFn := ObjBindMethod(this, "exitScript") 30 | Menu, Tray, Add, &Exit, % exitScriptFn 31 | } 32 | 33 | showAboutMessage() { 34 | MsgBox, 0, About MenuClip, % "MenuClip " . this.VERSION . " (c) takanuva15 `n`n" this.ABOUT_MENUCLIP 35 | } 36 | 37 | editConfigFile() { 38 | this.configManager.openEditConfigWindow() 39 | } 40 | 41 | reloadScript() { 42 | Reload 43 | } 44 | 45 | clearCacheAndReload() { 46 | this.clipManager.clipStore.cacheDirManager.clearCache() 47 | this.reloadScript() 48 | } 49 | 50 | exitScript() { 51 | ExitApp 52 | } 53 | 54 | static ABOUT_MENUCLIP := "" 55 | . "MenuClip is a free, open-source clipboard manager. " 56 | . "It watches your clipboard and stores new clips into memory, which you " 57 | . "can recall later by invoking the 'clip menu' through a hotkey.`n`n" 58 | . "See the README.md file on the MenuClip GitHub page (bit.ly/MenuClip) for more info " 59 | . "(and visit my profile at github.com/takanuva15)`n" 60 | } -------------------------------------------------------------------------------- /src/model/CacheDirManager.ahk: -------------------------------------------------------------------------------- 1 | ;Manages the cache directory which holds file backups for what's on the clip menu 2 | class CacheDirManager { 3 | static configManager 4 | static cacheDir := "cache\" 5 | static cachedClipFileNames := [] 6 | __New(configManager) { 7 | this.configManager := configManager 8 | if(!InStr(FileExist(this.cacheDir), "D")) { 9 | FileCreateDir, % this.cacheDir 10 | } 11 | this.verifyCache() 12 | } 13 | 14 | ;returns array of what was restored from cache 15 | restoreFromCache() { 16 | Loop, Files, % this.cacheDir . "*.txt" 17 | this.cachedClipFileNames.push(A_LoopFileName) 18 | 19 | tmp := [] 20 | Loop, % index := this.cachedClipFileNames.maxIndex() 21 | { 22 | FileRead, clip, % this.cacheDir . this.cachedClipFileNames[index--] 23 | tmp.insertAt(1, clip) 24 | } 25 | return tmp 26 | } 27 | 28 | insertAtTopOfCache(clip) { 29 | Loop, % index := this.cachedClipFileNames.length() 30 | { 31 | fileName := this.cachedClipFileNames[index] 32 | RegExMatch(fileName, "^(?P\d+)\.", file) 33 | FileMove, % this.cacheDir . fileName, % this.cacheDir . fileNum + 1 . ".txt" 34 | this.cachedClipFileNames[fileNum + 1] := fileNum + 1 . ".txt" 35 | index-- 36 | } 37 | this.cachedClipFileNames.removeAt(1) 38 | FileAppend, %clip%, % this.cacheDir . "1.txt" 39 | this.cachedClipFileNames.insertAt(1, "1.txt") 40 | } 41 | 42 | moveToTopOfCache(index) { 43 | tmp := this.cachedClipFileNames[index] 44 | FileMove, % this.cacheDir . tmp, % this.cacheDir . tmp . ".tmp" 45 | Loop, % loopIndex := index - 1 46 | { 47 | fileName := this.cachedClipFileNames[loopIndex--] 48 | RegExMatch(fileName, "^(?P\d+)\.", file) 49 | FileMove, % this.cacheDir . fileName, % this.cacheDir . fileNum + 1 . ".txt" 50 | this.cachedClipFileNames[fileNum + 1] := fileNum + 1 . ".txt" 51 | } 52 | FileMove, % this.cacheDir . tmp . ".tmp", % this.cacheDir . "1.txt" 53 | } 54 | 55 | deleteFromCache(index) { 56 | ;Works as of now because we only ever delete the last element in ClipManager. However, should be a loop available to rename files below this in the cache if we theoretically deleted from the middle of the menu. 57 | FileDelete, % this.cacheDir this.cachedClipFileNames[index] 58 | this.cachedClipFileNames.removeAt(index) 59 | } 60 | 61 | clearCache() { 62 | FileRemoveDir, % this.cacheDir, 1 63 | FileCreateDir, % this.cacheDir 64 | } 65 | 66 | verifyCache() { 67 | fileList := [] 68 | Loop, Files, % this.cacheDir . "*" 69 | fileList.push(A_LoopFileName) 70 | fileList := this.sortArray(fileList, "N") 71 | numFiles := fileList.maxIndex() 72 | maxFileCount := this.configManager.getMaxClipsToStore() 73 | 74 | ;Delete excess clips above maximum allowed 75 | Loop, % numFiles - maxFileCount 76 | FileDelete, % this.cacheDir . fileList[A_Index + maxFileCount] 77 | 78 | ;Temp name remaining clips prior to rename 79 | Loop, % numFiles > maxFileCount ? maxFileCount : numFiles 80 | FileMove, % this.cacheDir . fileList[A_Index], % this.cacheDir . fileList[A_Index] . ".bak" 81 | 82 | ;Rename remaining clips 83 | Loop, % index := numFiles > maxFileCount ? maxFileCount : numFiles 84 | { 85 | ;Msgbox % "Comparing " fileList[index] " with " index . ".txt" 86 | FileMove, % this.cacheDir . fileList[index] . ".bak", % this.cacheDir . index . ".txt" 87 | index-- 88 | } 89 | 90 | } 91 | 92 | ;https://autohotkey.com/board/topic/93570-sortarray/ 93 | ;Not under MIT license 94 | sortArray(arr,options="") { 95 | if !IsObject(arr) 96 | return 0 97 | new := [] 98 | if (options="Flip") { 99 | While (i := arr.MaxIndex()-A_Index+1) 100 | new.Insert(arr[i]) 101 | return new 102 | } 103 | list := "" 104 | For each, item in arr 105 | list .= item "`n" 106 | list := Trim(list,"`n") 107 | Sort, list, %options% 108 | Loop, parse, list, `n, `r 109 | new.Insert(A_LoopField) 110 | return new 111 | } 112 | } -------------------------------------------------------------------------------- /src/model/ClipStore.ahk: -------------------------------------------------------------------------------- 1 | #Include %A_ScriptDir%\src\model\CacheDirManager.ahk 2 | 3 | ;Manages the stored clips. (Basically an array manager) 4 | class ClipStore { 5 | static clips := [] 6 | static configManager 7 | static cacheDirManager 8 | __New(configManager) { 9 | this.configManager := configManager 10 | this.cacheDirManager := new MenuClip.Model.CacheDirManager(configManager) 11 | this.clips := this.cacheDirManager.restoreFromCache() 12 | } 13 | 14 | getClips() { 15 | return this.clips 16 | } 17 | 18 | getAtIndex(index) { 19 | return this.clips[index] 20 | } 21 | 22 | insertAtTop(clip) { 23 | this.clips.insertAt(1, clip) 24 | this.cacheDirManager.insertAtTopOfCache(clip) 25 | } 26 | 27 | moveToTop(index) { 28 | tmp := this.clips[index] 29 | Loop, % loopIndex := index - 1 30 | this.clips[loopIndex + 1] := this.clips[loopIndex--] 31 | this.clips[1] := tmp 32 | 33 | this.cacheDirManager.moveToTopOfCache(index) 34 | } 35 | 36 | deleteAtIndex(index) { 37 | this.clips.removeAt(index) 38 | this.cacheDirManager.deleteFromCache(index) 39 | } 40 | 41 | getSize() { 42 | ;avoids returning blank if clips empty 43 | return this.clips.maxIndex() ? this.clips.maxIndex() : 0 44 | } 45 | 46 | printClips() { 47 | s := "" 48 | for index, element in this.clips { 49 | s := s . element . ", " 50 | } 51 | 52 | t := "" 53 | for index, element in this.cachedClipFileNames { 54 | t := t . element . ", " 55 | } 56 | MsgBox % s . "`n`n" . t 57 | } 58 | } -------------------------------------------------------------------------------- /src/model/Model.ahk: -------------------------------------------------------------------------------- 1 | ;Container class for the model classes that MenuClip uses 2 | class Model { 3 | #Include %A_ScriptDir%\src\model\ClipStore.ahk 4 | } -------------------------------------------------------------------------------- /src/test_scenarios.md: -------------------------------------------------------------------------------- 1 | # Test Scenarios 2 | ### This file lists the scenarios that should be tested prior to merging to develop 3 | 4 | All scenarios assume: 5 | - MAX_CLIPS_TO_STORE=3 6 | - ALT_PASTE_APPS=mintty.exe 7 | 8 | ### Functionality Tests 9 | 1. Max limit on stored clips obeyed 10 | Copy "four", "three", "two", "one", in that order. The menu should show "one", "two", "three", in that order. 11 | 12 | 1. Correct clip is pasted on selection 13 | From previous, paste "one". Now, paste "two". 14 | 15 | 1. Selected clip shifts to top of menu 16 | From previous, menu should show "two", "one", "three" 17 | 18 | 1. Pasting is correct in editors that use Shift+Insert for paste 19 | Open up git bash. Paste "three" into it. 20 | 21 | 1. No repeated copies 22 | Copy "five" multiple times. There should only be one entry of it at the top of the menu. 23 | 24 | #### Gui-related functionality 25 | 26 | 1. Open the menu. Use the arrow keys to select a paste option and press "enter" to paste it. 27 | 28 | 1. Type some letters into the search filter. Then use the arrow keys to move the highlighted selection around. Both should work interchangeably. 29 | 30 | 1. Filter the entries by a few letters. Click on an entry and paste it. 31 | 32 | 1. Filter the entries by a few letters. Use the arrow keys to select an entry and hit "enter" to paste. 33 | 34 | 1. Ensure the search box is cleared each time you open the gui 35 | 36 | ### Storage Tests 37 | 1. Clip order is preserved 38 | * Make sure cache is empty & restart the script. Copy "four", "three", "two", "one", in that order. The cache folder should show "1.txt", "2.txt", "3.txt", in that order, with file contents "one", "two", "three", respectively. 39 | * Paste "two" from the menu. The cache folder should show the same file names, but now the file contents should be "two", "one", "three", respectively. 40 | 41 | 1. Cache contents restored on script start 42 | Exit the script and then run it. The menu should show "two", "one", "three", in that order. Paste "one". Check that the cache shows "1.txt", "2.txt", "3.txt" with file contents matching "one", "two", "three", respectively. 43 | 44 | 1. mess up the order and naming of the cache files. Reload the script. Everything should be in the proper order without exceeding the max limit of the number-of-clips-to-store config option. 45 | 46 | ### Visual Tests 47 | 1. Try to open up the paste window with your mouse at the bottom-right of the screen. The window should not render off-screen. 48 | 49 | 1. Paste a clip. There should be no fade animation. Now click outside of the clips view window. The box should fade away. 50 | 51 | ### Config File 52 | 1. Delete the config file. Restart the script and the config file should appear with default values. 53 | 54 | 1. Change the configuration keys in `config.ini`. The property that each variable defines should adjust accordingly when the script is restarted. 55 | 56 | 1. Do the previous especially for the auto theme config. Test different times and different hours, and swap them around for weird situations 57 | 58 | ### Tray Menu Options 59 | 1. Change the configurations through the GUI that appears when selecting "Edit Configuration" from the System Tray icon's menu. Saving changes on the GUI should change it in the config.ini. 60 | 61 | 1. While the script is running, change the 1st text file in the cache and save. Then click "reload script" in the tray. The 1st entry in the clip menu should reflect the change made in the file. 62 | 63 | 1. With items in the clip menu, click the "clear cache & reload" option in the tray. Then open up the menu again and it should be empty. Check the cache dir to make sure it is also empty. 64 | 65 | ### Bugfix Tests 66 | 1. Open up Terminal in IntelliJ (download it if you don't have it). Try highlighting a word in command prompt; then open the menu. There should only be one entry with the word you highlighted. 67 | 68 | 1. Open up the menu and click outside of it. The menu should close and you should be able to click-drag random text. 69 | 70 | 1. Open up the menu and type in a random search term. Exit the gui using the Escape key. Reopen the menu. There should be no search term there -------------------------------------------------------------------------------- /src/view/MenuClipsViewHandler.ahk: -------------------------------------------------------------------------------- 1 | ;Handles changes to the clips view 2 | 3 | class MenuClipsViewHandler { 4 | static menuGui 5 | __New(menuGui) { 6 | this.menuGui := menuGui 7 | } 8 | 9 | populateMenuFromArray(arr) { 10 | if(this.menuGui.configManager.getConvSpecChar()) { 11 | prettifiedArr := [] 12 | for index, element in arr { 13 | prettifiedArr.push(this.prettifyClip(element)) 14 | } 15 | } else { 16 | prettifiedArr := arr 17 | } 18 | PopulateLBFromArray(this.menuGui.HANDLE_CLIPS_VIEW, prettifiedArr) 19 | } 20 | 21 | insertItemAtTop(item) { 22 | if(this.menuGui.configManager.getConvSpecChar()) { 23 | item := this.prettifyClip(item) 24 | } 25 | LB_InsertItemAtIndex(this.menuGui.HANDLE_CLIPS_VIEW, item, 1) 26 | ;MC-45 bugfix needed for new clips being added 27 | this.menuGui.menuSearchHandler.filterClipsByStr("") 28 | } 29 | 30 | deleteItemAtIndex(index) { 31 | LB_DeleteItem(this.menuGui.HANDLE_CLIPS_VIEW, index) 32 | } 33 | 34 | moveToTop(index) { 35 | this.deleteItemAtIndex(index) 36 | this.insertItemAtTop(this.clipStore.getAtIndex(1)) 37 | } 38 | 39 | pasteSelectedClip() { 40 | selectedClipIndex := GetControlValue(this.menuGui.HANDLE_CLIPS_VIEW) 41 | origClipIndex := this.menuGui.menuSearchHandler.getOrigClipFromFilteredClipByIndex(selectedClipIndex) 42 | this.menuGui.onItemClickFn.call(origClipIndex) 43 | } 44 | 45 | prettifyClip(clip) { 46 | filteredClip := StrReplace(clip, A_Tab, "[\t]") 47 | filteredClip := StrReplace(filteredClip, "`r`n", "[\n]") 48 | filteredClip := StrReplace(filteredClip, "`r", "[\n]") 49 | filteredClip := StrReplace(filteredClip, "`n", "[\n]") 50 | return filteredClip 51 | } 52 | } -------------------------------------------------------------------------------- /src/view/MenuGui.ahk: -------------------------------------------------------------------------------- 1 | #Include %A_ScriptDir%\src\view\MenuWindowHandler.ahk 2 | #Include %A_ScriptDir%\src\view\MenuClipsViewHandler.ahk 3 | #Include %A_ScriptDir%\src\view\MenuSearchHandler.ahk 4 | 5 | ;Represents the menu gui 6 | class MenuGui { 7 | static clipStore 8 | static menuWindowHandler 9 | static menuClipsViewHandler 10 | static menuSearchHandler 11 | static HANDLE_CLIPS_VIEW 12 | static HANDLE_SEARCH_BOX 13 | static HANDLE_GUI 14 | __New(configManager, clipStore, onItemClickFn) { 15 | this.configManager := configManager 16 | this.clipStore := clipStore 17 | this.onItemClickFn := onItemClickFn 18 | this.menuWindowHandler := new MenuClip.View.MenuWindowHandler(this) 19 | this.menuClipsViewHandler := new MenuClip.View.MenuClipsViewHandler(this) 20 | this.menuSearchHandler := new MenuClip.View.MenuSearchHandler(this) 21 | 22 | Gui +hWndClipMenu 23 | Gui ClipMenu:-MinimizeBox -MaximizeBox -Caption +LastFound AlwaysOnTop 24 | this.HANDLE_GUI := WinExist() 25 | Gui ClipMenu:Margin, 5, 5 26 | Gui ClipMenu:Font, % (FontOptions := "s10") 27 | 28 | this.themeStyle := this.configManager.getTheme() 29 | if(this.themeStyle = "dark") { 30 | Gui ClipMenu:Color, 2B2B2B, 43474A 31 | Gui ClipMenu:Font, cDDDDDD 32 | } 33 | 34 | this.addClipsView() 35 | this.addSearchBox() 36 | this.addInvisibleOKButton() 37 | this.menuWindowHandler.updateGuiDims() 38 | this.menuSearchHandler.handleSearch() ;called once to prefill the filtered menu 39 | } 40 | 41 | addClipsView() { 42 | LBS_NOINTEGRALHEIGHT := 0x0100 43 | Gui ClipMenu:Add, ListBox, % "xm ym w" . this.configManager.getMaxWidth() . " r" . this.configManager.getMaxHeight() . " hWndClipsView AltSubmit +0x0100" 44 | this.HANDLE_CLIPS_VIEW := ClipsView 45 | LB_AdjustItemHeight(ClipsView, 5) 46 | } 47 | 48 | addSearchBox() { 49 | Gui ClipMenu:Add, Edit, % "xm y+5 w" . this.configManager.getMaxWidth() . " hWndSearchBox" 50 | this.HANDLE_SEARCH_BOX := SearchBox 51 | handleSearchFn := ObjBindMethod(this.menuSearchHandler, "handleSearch") 52 | GuiControl +g, %SearchBox%, % handleSearchFn 53 | } 54 | 55 | ;handles user pressing Enter on the menu 56 | addInvisibleOKButton() { 57 | Gui ClipMenu:Add, Button, h0 w0 hWndPasteSelected +Default 58 | this.HANDLE_BUTTON_PASTE := PasteSelected 59 | pasteOnEnterFn := ObjBindMethod(this, "pasteOnEnter") 60 | GuiControl +g, %PasteSelected%, % pasteOnEnterFn 61 | } 62 | 63 | pasteOnEnter() { 64 | this.menuWindowHandler.hideMenuGui(False) 65 | this.menuClipsViewHandler.pasteSelectedClip() 66 | this.menuWindowHandler.resetGuiState() 67 | } 68 | 69 | showGui() { 70 | this.menuWindowHandler.showGui() 71 | } 72 | 73 | recolorGui(themeStyle) { 74 | if(themeStyle = "light") { 75 | Gui ClipMenu:Color, Default, Default 76 | GuiControl, +cBlack, % this.HANDLE_CLIPS_VIEW 77 | GuiControl, +cBlack, % this.HANDLE_SEARCH_BOX 78 | } else { 79 | Gui ClipMenu:Color, 2B2B2B, 43474A 80 | GuiControl, +cDDDDDD, % this.HANDLE_CLIPS_VIEW 81 | GuiControl, +cDDDDDD, % this.HANDLE_SEARCH_BOX 82 | } 83 | } 84 | 85 | ;if gui is open when theme change occurs, theme will swap immediately 86 | ;once user closes gui, then we reload so that config gui reflects the 87 | ;theme change as well 88 | initReloadOnMenuQuit() { 89 | if(WinExist("ahk_id " this.HANDLE_GUI)) { 90 | this.menuWindowHandler.reloadOnHide := True 91 | } else { 92 | Reload 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /src/view/MenuSearchHandler.ahk: -------------------------------------------------------------------------------- 1 | ;Handles changes to the search box 2 | class MenuSearchHandler { 3 | static menuGui 4 | static filteredClips := [] 5 | __New(menuGui) { 6 | this.menuGui := menuGui 7 | } 8 | 9 | handleSearch() { 10 | searchStr := GetControlValue(this.menuGui.HANDLE_SEARCH_BOX) 11 | this.filterClipsByStr(searchStr) 12 | GuiControl, , % this.menuGui.HANDLE_CLIPS_VIEW, | 13 | this.menuGui.menuClipsViewHandler.populateMenuFromArray(this.convertFilteredClipsToStrArr()) 14 | GuiControl, Choose, % this.menuGui.HANDLE_CLIPS_VIEW, 1 15 | } 16 | 17 | filterClipsByStr(searchStr) { 18 | this.filteredClips := [] 19 | for index, element in this.menuGui.clipStore.getClips() { 20 | if(InStr(element, searchStr)) { 21 | this.filteredClips.push({"origIndex": index, "clip": element}) 22 | } 23 | } 24 | } 25 | 26 | convertFilteredClipsToStrArr() { 27 | filteredClipsTextOnly := [] 28 | for index, element in this.filteredClips { 29 | filteredClipsTextOnly.push(element.clip) 30 | } 31 | return filteredClipsTextOnly 32 | } 33 | 34 | getOrigClipFromFilteredClipByIndex(filteredClipIndex) { 35 | return this.filteredClips[filteredClipIndex].origIndex 36 | } 37 | } -------------------------------------------------------------------------------- /src/view/MenuWindowHandler.ahk: -------------------------------------------------------------------------------- 1 | ;Handles changes to the menu 2 | class MenuWindowHandler { 3 | static menuGui 4 | static totalScreenWidth, totalScreenHeight 5 | static guiWidth, guiHeight 6 | static reloadOnHide := False 7 | __New(menuGui) { 8 | this.menuGui := menuGui 9 | hideGuiOnOutsideClickFn := ObjBindMethod(this, "watchMouseClickAndHideGuiOnOutsideClick") 10 | Hotkey, ~LButton, % hideGuiOnOutsideClickFn 11 | Hotkey, ~LButton, Off 12 | 13 | CoordMode, Mouse, Screen 14 | SysGet, tmp, 78 15 | this.totalScreenWidth := tmp 16 | SysGet, tmp, 79 17 | this.totalScreenHeight := tmp 18 | } 19 | 20 | watchMouseClickAndHideGuiOnOutsideClick() { 21 | MouseGetPos, , , windowClicked, controlClicked 22 | if(controlClicked = "ListBox1") { 23 | Sleep, 100 ;allows time for Gui to show what was selected 24 | this.hideMenuGui(playFadeAnimation := False) 25 | this.menuGui.menuClipsViewHandler.pasteSelectedClip() 26 | this.resetGuiState() 27 | } else if(windowClicked = this.menuGui.HANDLE_GUI) { 28 | return 29 | } else { 30 | this.hideMenuGui() 31 | this.resetGuiState() 32 | } 33 | } 34 | 35 | showGui() { 36 | GuiControl, Focus, % this.menuGui.HANDLE_SEARCH_BOX 37 | GuiControl, Choose, % this.menuGui.HANDLE_CLIPS_VIEW, 1 38 | Hotkey, ~LButton, On 39 | 40 | MouseGetPos, mouseXPos, mouseYPos 41 | dispXPos := % mouseXPos + this.guiWidth > this.totalScreenWidth ? mouseXPos - this.guiWidth : mouseXPos 42 | dispYPos := % mouseYPos + this.guiHeight > this.totalScreenHeight ? mouseYPos - this.guiHeight : mouseYPos 43 | Gui ClipMenu:Show, AutoSize x%dispXPos% y%dispYPos% 44 | WinSet, Transparent, Off, % "ahk_id " this.menuGui.HANDLE_GUI 45 | this.handleKeyPresses() 46 | } 47 | 48 | handleKeyPresses() { 49 | Input, tmp, V, {Esc}{LWin}{RWin}{AppsKey}{LAlt}{RAlt}{Up}{Down} 50 | if(InStr(ErrorLevel, "EndKey:")) { 51 | if(InStr(ErrorLevel, "Up")) { 52 | GuiControl, Focus, % this.menuGui.HANDLE_CLIPS_VIEW 53 | Send, {Up} 54 | GuiControl, Focus, % this.menuGui.HANDLE_SEARCH_BOX 55 | } else if(InStr(ErrorLevel, "Down")) { 56 | GuiControl, Focus, % this.menuGui.HANDLE_CLIPS_VIEW 57 | Send, {Down} 58 | GuiControl, Focus, % this.menuGui.HANDLE_SEARCH_BOX 59 | } else { 60 | this.hideMenuGui() 61 | this.resetGuiState() 62 | return 63 | } 64 | this.handleKeyPresses() 65 | } 66 | } 67 | 68 | hideMenuGui(playFadeAnimation := True) { 69 | if(playFadeAnimation) { 70 | Loop, % loopIndex := 10 71 | { 72 | WinSet, Transparent, % loopIndex-- * 25, % "ahk_id " this.menuGui.HANDLE_GUI 73 | Sleep 25 74 | } 75 | } 76 | Gui ClipMenu:Hide 77 | if(this.reloadOnHide) { 78 | Reload 79 | } 80 | } 81 | 82 | resetGuiState() { 83 | GuiControl, , % this.menuGui.HANDLE_SEARCH_BOX 84 | Input 85 | Hotkey, ~LButton, Off 86 | Send, {Ctrl up} ;depresses Ctrl if you're using Ctrl in the showMenu shortcut 87 | } 88 | 89 | updateGuiDims() { 90 | Gui ClipMenu:Show, Hide ;Renders it once to give it dimensions for the handler to use 91 | GetGuiSize(this.menuGui.HANDLE_GUI, guiWidth, guiHeight) 92 | this.guiWidth := guiWidth 93 | this.guiHeight := guiHeight 94 | } 95 | } -------------------------------------------------------------------------------- /src/view/View.ahk: -------------------------------------------------------------------------------- 1 | ;Container class for the view classes that MenuClip uses 2 | class View { 3 | #Include %A_ScriptDir%\src\view\MenuGui.ahk 4 | } --------------------------------------------------------------------------------