__init__.py 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901
  1. # MouseInfo by Al Sweigart al@inventwithpython.com
  2. # Note: how to specify where a tkintr window opens:
  3. # https://stackoverflow.com/questions/14910858/how-to-specify-where-a-tkinter-window-opens
  4. """
  5. Features we should consider adding:
  6. * Register a global hotkey for copying/logging info. (Should this hotkey be configurable?)
  7. Features that have been considered and rejected:
  8. * The Save Log/Save Screenshot buttons should open a file dialog box.
  9. * The Save Log button should append text, instead of overwrite it.
  10. * The log text area should prepopulate itself with the contents of the given filename.
  11. * The button delay should be configurable instead of just set to 3 seconds.
  12. """
  13. __version__ = '0.1.3'
  14. import pyperclip, sys, os, platform, webbrowser
  15. #from enum import Enum
  16. from ctypes import (
  17. c_bool, c_int32, c_int64, c_size_t, c_uint16, c_uint32, c_void_p,
  18. cdll, util,
  19. )
  20. # =========================================================================
  21. # Originally, these functions were pulled in from PyAutoGUI. However, to
  22. # make this module independent of PyAutoGUI, the code for these functions
  23. # has been copy/pasted into the following section:
  24. # NOTE: Any bug fixes for these functions in PyAutoGUI will have to be
  25. # manually merged into MouseInfo.
  26. #from pyautogui import position, screenshot, size
  27. # =========================================================================
  28. # Alternatively, this code makes this application not dependent on PyAutoGUI
  29. # by copying the code for the position() and screenshot() functions into this
  30. # source code file.
  31. import datetime, subprocess
  32. try:
  33. from PIL import Image
  34. _PILLOW_INSTALLED = True
  35. except ImportError:
  36. _PILLOW_INSTALLED = False
  37. if sys.platform == 'win32':
  38. import ctypes
  39. if _PILLOW_INSTALLED:
  40. from PIL import ImageGrab
  41. # Makes this process aware of monitor scaling so the screenshots are correctly sized:
  42. try:
  43. ctypes.windll.user32.SetProcessDPIAware()
  44. except AttributeError:
  45. pass # Windows XP doesn't support this, so just do nothing.
  46. dc = ctypes.windll.user32.GetDC(0)
  47. class POINT(ctypes.Structure):
  48. _fields_ = [('x', ctypes.c_long),
  49. ('y', ctypes.c_long)]
  50. def _winPosition():
  51. cursor = POINT()
  52. ctypes.windll.user32.GetCursorPos(ctypes.byref(cursor))
  53. return (cursor.x, cursor.y)
  54. position = _winPosition
  55. def _winScreenshot(filename=None):
  56. # TODO - Use the winapi to get a screenshot, and compare performance with ImageGrab.grab()
  57. # https://stackoverflow.com/a/3586280/1893164
  58. try:
  59. im = ImageGrab.grab()
  60. if filename is not None:
  61. im.save(filename)
  62. except NameError:
  63. raise ImportError('Pillow module must be installed to use screenshot functions on Windows.')
  64. return im
  65. screenshot = _winScreenshot
  66. def _winSize():
  67. return (ctypes.windll.user32.GetSystemMetrics(0), ctypes.windll.user32.GetSystemMetrics(1))
  68. size = _winSize
  69. def _winGetPixel(x, y):
  70. colorRef = ctypes.windll.gdi32.GetPixel(dc, x, y) # A COLORREF value as 0x00bbggrr. See https://docs.microsoft.com/en-us/windows/win32/gdi/colorref
  71. red = colorRef % 256
  72. colorRef //= 256
  73. green = colorRef % 256
  74. colorRef //= 256
  75. blue = colorRef
  76. return (red, green, blue)
  77. getPixel = _winGetPixel
  78. elif sys.platform == 'darwin':
  79. from rubicon.objc import ObjCClass, CGPoint
  80. from rubicon.objc.types import register_preferred_encoding
  81. #####################################################################
  82. appkit = cdll.LoadLibrary(util.find_library('AppKit'))
  83. NSEvent = ObjCClass('NSEvent')
  84. NSEvent.declare_class_property('mouseLocation')
  85. # NSSystemDefined = ObjCClass('NSSystemDefined')
  86. #####################################################################
  87. core_graphics = cdll.LoadLibrary(util.find_library('CoreGraphics'))
  88. CGDirectDisplayID = c_uint32
  89. CGEventRef = c_void_p
  90. register_preferred_encoding(b'^{__CGEvent=}', CGEventRef)
  91. CGEventSourceRef = c_void_p
  92. register_preferred_encoding(b'^{__CGEventSource=}', CGEventSourceRef)
  93. CGEventTapLocation = c_uint32
  94. CGEventType = c_uint32
  95. CGEventField = c_uint32
  96. CGKeyCode = c_uint16
  97. CGMouseButton = c_uint32
  98. CGScrollEventUnit = c_uint32
  99. # size_t CGDisplayPixelsWide(CGDirectDisplayID display);
  100. core_graphics.CGDisplayPixelsWide.argtypes = [CGDirectDisplayID]
  101. core_graphics.CGDisplayPixelsWide.restype = c_size_t
  102. # CGEventRef CGEventCreateKeyboardEvent(CGEventSourceRef source, CGKeyCode virtualKey, bool keyDown);
  103. core_graphics.CGEventCreateKeyboardEvent.argtypes = [CGEventSourceRef, CGKeyCode, c_bool]
  104. core_graphics.CGEventCreateKeyboardEvent.restype = CGEventRef
  105. # CGEventRef CGEventCreateMouseEvent(
  106. # CGEventSourceRef source, CGEventType mouseType, CGPoint mouseCursorPosition, CGMouseButton mouseButton);
  107. core_graphics.CGEventCreateMouseEvent.argtypes = [CGEventSourceRef, CGEventType, CGPoint, CGMouseButton]
  108. core_graphics.CGEventCreateMouseEvent.restype = CGEventRef
  109. # CGEventRef CGEventCreateScrollWheelEvent(
  110. # CGEventSourceRef source, CGScrollEventUnit units, uint32_t wheelCount, int32_t wheel1, ...);
  111. core_graphics.CGEventCreateScrollWheelEvent.argtypes = [CGEventSourceRef, CGScrollEventUnit, c_uint32, c_int32]
  112. core_graphics.CGEventCreateScrollWheelEvent.restype = CGEventRef
  113. # void CGEventSetIntegerValueField(CGEventRef event, CGEventField field, int64_t value);
  114. core_graphics.CGEventSetIntegerValueField.argtypes = [CGEventRef, CGEventField, c_int64]
  115. core_graphics.CGEventSetIntegerValueField.restype = None
  116. # void CGEventSetType(CGEventRef event, CGEventType type);
  117. core_graphics.CGEventSetType.argtype = [CGEventRef, CGEventType]
  118. core_graphics.CGEventSetType.restype = None
  119. # void CGEventPost(CGEventTapLocation tap, CGEventRef event);
  120. core_graphics.CGEventPost.argtypes = [CGEventTapLocation, CGEventRef]
  121. core_graphics.CGEventPost.restype = None
  122. # CGDirectDisplayID CGMainDisplayID(void);
  123. core_graphics.CGMainDisplayID.argtypes = []
  124. core_graphics.CGMainDisplayID.restype = CGDirectDisplayID
  125. def _macPosition():
  126. loc = NSEvent.mouseLocation
  127. return int(loc.x), int(core_graphics.CGDisplayPixelsHigh(0) - loc.y)
  128. position = _macPosition
  129. def _macScreenshot(filename=None):
  130. if filename is not None:
  131. tmpFilename = filename
  132. else:
  133. tmpFilename = 'screenshot%s.png' % (datetime.datetime.now().strftime('%Y-%m%d_%H-%M-%S-%f'))
  134. subprocess.call(['screencapture', '-x', tmpFilename])
  135. im = Image.open(tmpFilename)
  136. # force loading before unlinking, Image.open() is lazy
  137. im.load()
  138. if filename is None:
  139. os.unlink(tmpFilename)
  140. return im
  141. screenshot = _macScreenshot
  142. def _macSize():
  143. return (
  144. core_graphics.CGDisplayPixelsWide(core_graphics.CGMainDisplayID()),
  145. core_graphics.CGDisplayPixelsHigh(core_graphics.CGMainDisplayID())
  146. )
  147. size = _macSize
  148. def _macGetPixel(x, y):
  149. rgbValue = screenshot().getpixel((x, y))
  150. return rgbValue[0], rgbValue[1], rgbValue[2]
  151. getPixel = _macGetPixel
  152. elif platform.system() == 'Linux':
  153. from Xlib.display import Display
  154. import errno
  155. scrotExists = False
  156. try:
  157. whichProc = subprocess.Popen(
  158. ['which', 'scrot'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  159. scrotExists = whichProc.wait() == 0
  160. except OSError as ex:
  161. if ex.errno == errno.ENOENT:
  162. # if there is no "which" program to find scrot, then assume there
  163. # is no scrot.
  164. pass
  165. else:
  166. raise
  167. _display = Display(os.environ['DISPLAY'])
  168. def _linuxPosition():
  169. coord = _display.screen().root.query_pointer()._data
  170. return coord["root_x"], coord["root_y"]
  171. position = _linuxPosition
  172. def _linuxScreenshot(filename=None):
  173. if not scrotExists:
  174. raise NotImplementedError('"scrot" must be installed to use screenshot functions in Linux. Run: sudo apt-get install scrot')
  175. if filename is not None:
  176. tmpFilename = filename
  177. else:
  178. tmpFilename = '.screenshot%s.png' % (datetime.datetime.now().strftime('%Y-%m%d_%H-%M-%S-%f'))
  179. if scrotExists:
  180. subprocess.call(['scrot', '-z', tmpFilename])
  181. im = Image.open(tmpFilename)
  182. # force loading before unlinking, Image.open() is lazy
  183. im.load()
  184. if filename is None:
  185. os.unlink(tmpFilename)
  186. return im
  187. else:
  188. raise Exception('The scrot program must be installed to take a screenshot with PyScreeze on Linux. Run: sudo apt-get install scrot')
  189. screenshot = _linuxScreenshot
  190. def _linuxSize():
  191. return _display.screen().width_in_pixels, _display.screen().height_in_pixels
  192. size = _linuxSize
  193. def _linuxGetPixel(x, y):
  194. rgbValue = screenshot().getpixel((x, y))
  195. return rgbValue[0], rgbValue[1], rgbValue[2]
  196. getPixel = _linuxGetPixel
  197. # =========================================================================
  198. RUNNING_PYTHON_2 = sys.version_info[0] == 2
  199. if platform.system() == 'Linux':
  200. if RUNNING_PYTHON_2:
  201. try:
  202. import Tkinter as tkinter
  203. ttk = tkinter
  204. from Tkinter import Event
  205. except ImportError:
  206. sys.exit('NOTE: You must install tkinter on Linux to use MouseInfo. Run the following: sudo apt-get install python-tk python-dev')
  207. else:
  208. # Running Python 3+:
  209. try:
  210. import tkinter
  211. from tkinter import ttk
  212. from tkinter import Event
  213. except ImportError:
  214. sys.exit('NOTE: You must install tkinter on Linux to use MouseInfo. Run the following: sudo apt-get install python3-tk python3-dev')
  215. else:
  216. # Running Windows or macOS:
  217. if RUNNING_PYTHON_2:
  218. import Tkinter as tkinter
  219. ttk = tkinter
  220. from Tkinter import Event
  221. else:
  222. # Running Python 3+:
  223. import tkinter
  224. from tkinter import ttk
  225. from tkinter import Event
  226. MOUSE_INFO_BUTTON_WIDTH = 16 # A standard width for the buttons in the MouseInfo window.
  227. class MouseInfoWindow:
  228. def _updateMouseInfoTextFields(self):
  229. # Update the XY and RGB text fields in the MouseInfo window.
  230. # Get the XY coordinates of the current mouse position:
  231. x, y = position()
  232. self.xyTextboxSV.set('%s,%s' % (x - self.xOrigin, y - self.yOrigin))
  233. # MouseInfo currently only works on the primary monitor, and doesn't
  234. # support multi-monitor setups. The color information isn't reliable
  235. # when the mouse is not on the primary monitor, so display an error instead.
  236. width, height = size()
  237. if not _PILLOW_INSTALLED:
  238. self.rgbSV.set('NA_Pillow_unsupported')
  239. elif sys.platform == 'darwin':
  240. # TODO - Until I can get screenshots without the mouse cursor, this feature doesn't work on mac.
  241. self.rgbSV.set('NA_on_macOS')
  242. elif not (0 <= x < width and 0 <= y < height):
  243. self.rgbSV.set('NA_on_multimonitor_setups')
  244. else:
  245. # Get the RGB color value of the pixel currently under the mouse:
  246. # NOTE: On Windows & Linux, Pillow's getpixel() returns a 3-integer tuple, but on macOS it returns a 4-integer tuple.
  247. r, g, b = getPixel(x, y)
  248. self.rgbSV.set('%s,%s,%s' % (r, g, b))
  249. if not _PILLOW_INSTALLED:
  250. self.rgbHexSV.set('NA_Pillow_unsupported')
  251. elif sys.platform == 'darwin':
  252. # TODO - Until I can get screenshots without the mouse cursor, this feature doesn't work on mac.
  253. self.rgbHexSV.set('NA_on_macOS')
  254. elif not (0 <= x < width and 0 <= y < height):
  255. self.rgbHexSV.set('NA_on_multimonitor_setups')
  256. else:
  257. # Convert this RGB value into a hex RGB value:
  258. rHex = hex(r)[2:].upper().rjust(2, '0')
  259. gHex = hex(g)[2:].upper().rjust(2, '0')
  260. bHex = hex(b)[2:].upper().rjust(2, '0')
  261. hexColor = '#%s%s%s' % (rHex, gHex, bHex)
  262. self.rgbHexSV.set(hexColor)
  263. if (not _PILLOW_INSTALLED) or (sys.platform == 'darwin') or (not (0 <= x < width and 0 <= y < height)):
  264. self.colorFrame.configure(background='black')
  265. else:
  266. # Update the color panel:
  267. self.colorFrame.configure(background=hexColor)
  268. # As long as the self.isRunning variable is True,
  269. # schedule this function to be called again in 100 milliseconds.
  270. # NOTE: Previously this if-else code was at the top of the function
  271. # so that I could avoid the "invalid command name" message that
  272. # was popping up (this didn't work though), but it was also causing
  273. # a weird bug where the text fields weren't populated until I moved
  274. # the tkinter window. I have no idea why that behavior was happening.
  275. # You can reproduce it by moving this if-else code to the top of this
  276. # function.
  277. if self.isRunning:
  278. self._updateMouseInfoJob = self.root.after(100, self._updateMouseInfoTextFields)
  279. else:
  280. return # MouseInfo window has been closed, so return immediately.
  281. def _copyText(self, textToCopy):
  282. try:
  283. pyperclip.copy(textToCopy)
  284. self.statusbarSV.set('Copied ' + textToCopy)
  285. except pyperclip.PyperclipException as e:
  286. if platform.system() == 'Linux':
  287. self.statusbarSV.set('Copy failed. Run "sudo apt-get install xsel".')
  288. else:
  289. self.statusbarSV.set('Clipboard error: ' + str(e))
  290. def _copyXyMouseInfo(self, *args):
  291. # Copy the contents of the XY coordinate text field in the MouseInfo
  292. # window to the clipboard.
  293. if len(args) > 0 and isinstance(args[0], Event):
  294. args = () # When the hotkey is pressed, an Event object is in args. Let's just get rid of it and let the rest of the code run as normal.
  295. if self.delayEnabledSV.get() == 'on' and len(args) == 0:
  296. # Start countdown by having after() call this function in 1 second:
  297. self.root.after(1000, self._copyXyMouseInfo, 2)
  298. self.xyCopyButtonSV.set('Copy in 3')
  299. elif len(args) == 1 and args[0] == 2:
  300. # Continue countdown by having after() call this function in 1 second:
  301. self.root.after(1000, self._copyXyMouseInfo, 1)
  302. self.xyCopyButtonSV.set('Copy in 2')
  303. elif len(args) == 1 and args[0] == 1:
  304. # Continue countdown by having after() call this function in 1 second:
  305. self.root.after(1000, self._copyXyMouseInfo, 0)
  306. self.xyCopyButtonSV.set('Copy in 1')
  307. else:
  308. # Delay disabled or countdown has finished:
  309. self._copyText(self.xyTextboxSV.get())
  310. self.xyCopyButtonSV.set('Copy XY')
  311. def _copyRgbMouseInfo(self, *args):
  312. # Copy the contents of the RGB color text field in the MouseInfo
  313. # window to the clipboard.
  314. if len(args) > 0 and isinstance(args[0], Event):
  315. args = () # When the hotkey is pressed, an Event object is in args. Let's just get rid of it and let the rest of the code run as normal.
  316. if self.delayEnabledSV.get() == 'on' and len(args) == 0:
  317. # Start countdown by having after() call this function in 1 second:
  318. self.root.after(1000, self._copyRgbMouseInfo, 2)
  319. self.rgbCopyButtonSV.set('Copy in 3')
  320. elif len(args) == 1 and args[0] == 2:
  321. # Continue countdown by having after() call this function in 1 second:
  322. self.root.after(1000, self._copyRgbMouseInfo, 1)
  323. self.rgbCopyButtonSV.set('Copy in 2')
  324. elif len(args) == 1 and args[0] == 1:
  325. # Continue countdown by having after() call this function in 1 second:
  326. self.root.after(1000, self._copyRgbMouseInfo, 0)
  327. self.rgbCopyButtonSV.set('Copy in 1')
  328. else:
  329. # Delay disabled or countdown has finished:
  330. self._copyText(self.rgbSV.get())
  331. self.rgbCopyButtonSV.set('Copy RGB')
  332. def _copyRgbHexMouseInfo(self, *args):
  333. # Copy the contents of the RGB hex color text field in the MouseInfo
  334. # window to the clipboard.
  335. if len(args) > 0 and isinstance(args[0], Event):
  336. args = () # When the hotkey is pressed, an Event object is in args. Let's just get rid of it and let the rest of the code run as normal.
  337. if self.delayEnabledSV.get() == 'on' and len(args) == 0:
  338. # Start countdown by having after() call this function in 1 second:
  339. self.root.after(1000, self._copyRgbHexMouseInfo, 2)
  340. self.rgbHexCopyButtonSV.set('Copy in 3')
  341. elif len(args) == 1 and args[0] == 2:
  342. # Continue countdown by having after() call this function in 1 second:
  343. self.root.after(1000, self._copyRgbHexMouseInfo, 1)
  344. self.rgbHexCopyButtonSV.set('Copy in 2')
  345. elif len(args) == 1 and args[0] == 1:
  346. # Continue countdown by having after() call this function in 1 second:
  347. self.root.after(1000, self._copyRgbHexMouseInfo, 0)
  348. self.rgbHexCopyButtonSV.set('Copy in 1')
  349. else:
  350. # Delay disabled or countdown has finished:
  351. self._copyText(self.rgbHexSV.get())
  352. self.rgbHexCopyButtonSV.set('Copy RGB Hex')
  353. def _copyAllMouseInfo(self, *args):
  354. # Copy the contents of the XY coordinate and RGB color text fields in the
  355. # MouseInfo window to the log text field.
  356. textFieldContents = '%s %s %s' % (self.xyTextboxSV.get(),
  357. self.rgbSV.get(),
  358. self.rgbHexSV.get())
  359. #self._copyText(textFieldContents)
  360. if len(args) > 0 and isinstance(args[0], Event):
  361. args = () # When the hotkey is pressed, an Event object is in args. Let's just get rid of it and let the rest of the code run as normal.
  362. if self.delayEnabledSV.get() == 'on' and len(args) == 0:
  363. # Start countdown by having after() call this function in 1 second:
  364. self.root.after(1000, self._copyAllMouseInfo, 2)
  365. self.allCopyButtonSV.set('Copy in 3')
  366. elif len(args) == 1 and args[0] == 2:
  367. # Continue countdown by having after() call this function in 1 second:
  368. self.root.after(1000, self._copyAllMouseInfo, 1)
  369. self.allCopyButtonSV.set('Copy in 2')
  370. elif len(args) == 1 and args[0] == 1:
  371. # Continue countdown by having after() call this function in 1 second:
  372. self.root.after(1000, self._copyAllMouseInfo, 0)
  373. self.allCopyButtonSV.set('Copy in 1')
  374. else:
  375. # Delay disabled or countdown has finished:
  376. textFieldContents = '%s %s %s' % (self.xyTextboxSV.get(),
  377. self.rgbSV.get(),
  378. self.rgbHexSV.get())
  379. self._copyText(textFieldContents)
  380. self.allCopyButtonSV.set('Copy All')
  381. def _logXyMouseInfo(self, *args):
  382. # Log the contents of the XY coordinate text field in the MouseInfo
  383. # window to the log text field.
  384. if len(args) > 0 and isinstance(args[0], Event):
  385. args = () # When the hotkey is pressed, an Event object is in args. Let's just get rid of it and let the rest of the code run as normal.
  386. if self.delayEnabledSV.get() == 'on' and len(args) == 0:
  387. # Start countdown by having after() call this function in 1 second:
  388. self.root.after(1000, self._logXyMouseInfo, 2)
  389. self.xyLogButtonSV.set('Log in 3')
  390. elif len(args) == 1 and args[0] == 2:
  391. # Continue countdown by having after() call this function in 1 second:
  392. self.root.after(1000, self._logXyMouseInfo, 1)
  393. self.xyLogButtonSV.set('Log in 2')
  394. elif len(args) == 1 and args[0] == 1:
  395. # Continue countdown by having after() call this function in 1 second:
  396. self.root.after(1000, self._logXyMouseInfo, 0)
  397. self.xyLogButtonSV.set('Log in 1')
  398. else:
  399. # Delay disabled or countdown has finished:
  400. logContents = self.logTextarea.get('1.0', 'end-1c') + '%s\n' % (self.xyTextboxSV.get()) # 'end-1c' doesn't include the final newline
  401. self.logTextboxSV.set(logContents)
  402. self._setLogTextAreaContents(logContents)
  403. self.statusbarSV.set('Logged ' + self.xyTextboxSV.get())
  404. self.xyLogButtonSV.set('Log XY')
  405. def _logRgbMouseInfo(self, *args):
  406. # Log the contents of the RGB color text field in the MouseInfo
  407. # window to the log text field.
  408. if len(args) > 0 and isinstance(args[0], Event):
  409. args = () # When the hotkey is pressed, an Event object is in args. Let's just get rid of it and let the rest of the code run as normal.
  410. if self.delayEnabledSV.get() == 'on' and len(args) == 0:
  411. # Start countdown by having after() call this function in 1 second:
  412. self.root.after(1000, self._logRgbMouseInfo, 2)
  413. self.rgbLogButtonSV.set('Log in 3')
  414. elif len(args) == 1 and args[0] == 2:
  415. # Continue countdown by having after() call this function in 1 second:
  416. self.root.after(1000, self._logRgbMouseInfo, 1)
  417. self.rgbLogButtonSV.set('Log in 2')
  418. elif len(args) == 1 and args[0] == 1:
  419. # Continue countdown by having after() call this function in 1 second:
  420. self.root.after(1000, self._logRgbMouseInfo, 0)
  421. self.rgbLogButtonSV.set('Log in 1')
  422. else:
  423. # Delay disabled or countdown has finished:
  424. logContents = self.logTextarea.get('1.0', 'end-1c') + '%s\n' % (self.rgbSV.get()) # 'end-1c' doesn't include the final newline
  425. self.logTextboxSV.set(logContents)
  426. self._setLogTextAreaContents(logContents)
  427. self.statusbarSV.set('Logged ' + self.rgbSV.get())
  428. self.rgbLogButtonSV.set('Log RGB')
  429. def _logRgbHexMouseInfo(self, *args):
  430. # Log the contents of the RGB hex color text field in the MouseInfo
  431. # window to the log text field.
  432. if len(args) > 0 and isinstance(args[0], Event):
  433. args = () # When the hotkey is pressed, an Event object is in args. Let's just get rid of it and let the rest of the code run as normal.
  434. if self.delayEnabledSV.get() == 'on' and len(args) == 0:
  435. # Start countdown by having after() call this function in 1 second:
  436. self.root.after(1000, self._logRgbHexMouseInfo, 2)
  437. self.rgbHexLogButtonSV.set('Log in 3')
  438. elif len(args) == 1 and args[0] == 2:
  439. # Continue countdown by having after() call this function in 1 second:
  440. self.root.after(1000, self._logRgbHexMouseInfo, 1)
  441. self.rgbHexLogButtonSV.set('Log in 2')
  442. elif len(args) == 1 and args[0] == 1:
  443. # Continue countdown by having after() call this function in 1 second:
  444. self.root.after(1000, self._logRgbHexMouseInfo, 0)
  445. self.rgbHexLogButtonSV.set('Log in 1')
  446. else:
  447. # Delay disabled or countdown has finished:
  448. logContents = self.logTextarea.get('1.0', 'end-1c') + '%s\n' % (self.rgbHexSV.get()) # 'end-1c' doesn't include the final newline
  449. self.logTextboxSV.set(logContents)
  450. self._setLogTextAreaContents(logContents)
  451. self.statusbarSV.set('Logged ' + self.rgbHexSV.get())
  452. self.rgbHexLogButtonSV.set('Log RGB Hex')
  453. def _logAllMouseInfo(self, *args):
  454. # Log the contents of the XY coordinate and RGB color text fields in the
  455. # MouseInfo window to the log text field.
  456. if len(args) > 0 and isinstance(args[0], Event):
  457. args = () # When the hotkey is pressed, an Event object is in args. Let's just get rid of it and let the rest of the code run as normal.
  458. if self.delayEnabledSV.get() == 'on' and len(args) == 0:
  459. # Start countdown by having after() call this function in 1 second:
  460. self.root.after(1000, self._logAllMouseInfo, 2)
  461. self.allLogButtonSV.set('Log in 3')
  462. elif len(args) == 1 and args[0] == 2:
  463. # Continue countdown by having after() call this function in 1 second:
  464. self.root.after(1000, self._logAllMouseInfo, 1)
  465. self.allLogButtonSV.set('Log in 2')
  466. elif len(args) == 1 and args[0] == 1:
  467. # Continue countdown by having after() call this function in 1 second:
  468. self.root.after(1000, self._logAllMouseInfo, 0)
  469. self.allLogButtonSV.set('Log in 1')
  470. else:
  471. # Delay disabled or countdown has finished:
  472. textFieldContents = '%s %s %s' % (self.xyTextboxSV.get(),
  473. self.rgbSV.get(),
  474. self.rgbHexSV.get())
  475. logContents = self.logTextarea.get('1.0', 'end-1c') + '%s\n' % (textFieldContents) # 'end-1c' doesn't include the final newline
  476. self.logTextboxSV.set(logContents)
  477. self._setLogTextAreaContents(logContents)
  478. self.statusbarSV.set('Logged ' + textFieldContents)
  479. self.allLogButtonSV.set('Log All')
  480. def _xyOriginChanged(self, sv):
  481. contents = sv.get()
  482. if len(contents.split(',')) != 2:
  483. return # Do nothing if the text is invalid
  484. x, y = contents.split(',')
  485. x = x.strip()
  486. y = y.strip()
  487. if not x.isdecimal() or not y.isdecimal():
  488. return # Do nothing.
  489. self.xOrigin = int(x)
  490. self.yOrigin = int(y)
  491. self.statusbarSV.set('Set XY Origin to ' + str(self.xOrigin) + ', ' + str(self.yOrigin))
  492. def _setLogTextAreaContents(self, logContents):
  493. if RUNNING_PYTHON_2:
  494. self.logTextarea.delete('1.0', tkinter.END)
  495. self.logTextarea.insert(tkinter.END, logContents)
  496. else:
  497. self.logTextarea.replace('1.0', tkinter.END, logContents)
  498. # Scroll to the bottom of the text area:
  499. topOfTextArea, bottomOfTextArea = self.logTextarea.yview()
  500. self.logTextarea.yview_moveto(bottomOfTextArea)
  501. def _saveLogFile(self, *args):
  502. # Save the current contents of the log file text field. Automatically
  503. # overwrites the file if it exists. Displays an error message in the
  504. # status bar if there is a problem.
  505. try:
  506. with open(self.logFilenameSV.get(), 'w') as fo:
  507. fo.write(self.logTextboxSV.get())
  508. except Exception as e:
  509. self.statusbarSV.set('ERROR: ' + str(e))
  510. else:
  511. self.statusbarSV.set('Log file saved to ' + self.logFilenameSV.get())
  512. def _saveScreenshotFile(self, *args):
  513. # Saves a screenshot. Automatically overwrites the file if it exists.
  514. # Displays an error message in the status bar if there is a problem.
  515. if not _PILLOW_INSTALLED:
  516. self.statusbarSV.set('ERROR: NA_Pillow_unsupported')
  517. return
  518. try:
  519. screenshot(self.screenshotFilenameSV.get())
  520. except Exception as e:
  521. self.statusbarSV.set('ERROR: ' + str(e))
  522. else:
  523. self.statusbarSV.set('Screenshot file saved to ' + self.screenshotFilenameSV.get())
  524. def __init__(self):
  525. """Launches the MouseInfo window, which displays XY coordinate and RGB
  526. color information for the mouse's current position."""
  527. self.isRunning = True # While True, the text fields will update.
  528. # Create the MouseInfo window:
  529. self.root = tkinter.Tk()
  530. self.root.title('MouseInfo ' + __version__)
  531. self.root.minsize(400, 100)
  532. # Create the main frame in the MouseInfo window:
  533. if RUNNING_PYTHON_2:
  534. mainframe = tkinter.Frame(self.root)
  535. else:
  536. mainframe = ttk.Frame(self.root, padding='3 3 12 12')
  537. # Set up the grid for the MouseInfo window's widgets:
  538. mainframe.grid(column=0, row=0, sticky=(tkinter.N, tkinter.W, tkinter.E, tkinter.S))
  539. mainframe.columnconfigure(0, weight=1)
  540. mainframe.rowconfigure(0, weight=1)
  541. # WIDGETS ON ROW 1:
  542. CUR_ROW = 1 # I'm using a variable because it's easier to make changes to the source code this way.
  543. # Set up the instructional text label:
  544. #ttk.Label(mainframe, text='Tab over the buttons and press Enter to\n"click" them as you move the mouse around.').grid(column=1, row=1, columnspan=2, sticky=tkinter.W)
  545. self.delayEnabledSV = tkinter.StringVar()
  546. self.delayEnabledSV.set('on')
  547. delayCheckbox = ttk.Checkbutton(mainframe, text='3 Sec. Button Delay', variable=self.delayEnabledSV, onvalue='on', offvalue='off')
  548. delayCheckbox.grid(column=1, row=CUR_ROW, columnspan=2, sticky=tkinter.W)
  549. # Set up the button to copy the XY coordinates to the clipboard:
  550. self.allCopyButtonSV = tkinter.StringVar()
  551. self.allCopyButtonSV.set('Copy All (F1)')
  552. self.allCopyButton = ttk.Button(mainframe, textvariable=self.allCopyButtonSV, width=MOUSE_INFO_BUTTON_WIDTH, command=self._copyAllMouseInfo)
  553. self.allCopyButton.grid(column=3, row=CUR_ROW, sticky=tkinter.W)
  554. self.allCopyButton.bind('<Return>', self._copyAllMouseInfo)
  555. # Set up the button to copy the XY coordinates to the clipboard:
  556. self.allLogButtonSV = tkinter.StringVar()
  557. self.allLogButtonSV.set('Log All (F5)')
  558. self.allLogButton = ttk.Button(mainframe, textvariable=self.allLogButtonSV, width=MOUSE_INFO_BUTTON_WIDTH, command=self._logAllMouseInfo)
  559. self.allLogButton.grid(column=4, row=CUR_ROW, sticky=tkinter.W)
  560. self.allLogButton.bind('<Return>', self._logAllMouseInfo)
  561. # Set up the variables for the content of the MouseInfo window's text fields:
  562. self.xyTextboxSV = tkinter.StringVar() # The str contents of the xy text field.
  563. self.rgbSV = tkinter.StringVar() # The str contents of the rgb text field.
  564. self.rgbHexSV = tkinter.StringVar() # The str contents of the rgb hex text field.
  565. self.xyOriginSV = tkinter.StringVar() # The str contents of the xy origin field.
  566. self.logTextboxSV = tkinter.StringVar() # The str contents of the log text area.
  567. self.logFilenameSV = tkinter.StringVar() # The str contents of the log filename text field.
  568. self.screenshotFilenameSV = tkinter.StringVar() # The str contents of the screenshot filename text field.
  569. self.statusbarSV = tkinter.StringVar() # The str contents of the status bar at the bottom of the window.
  570. # WIDGETS ON ROW 3:
  571. CUR_ROW += 1
  572. # Set up the XY coordinate text field and label:
  573. self.xyInfoTextbox = ttk.Entry(mainframe, width=16, textvariable=self.xyTextboxSV)
  574. self.xyInfoTextbox.grid(column=2, row=CUR_ROW, sticky=(tkinter.W, tkinter.E))
  575. ttk.Label(mainframe, text='XY Position').grid(column=1, row=CUR_ROW, sticky=tkinter.W)
  576. # Set up the button to copy the XY coordinates to the clipboard:
  577. self.xyCopyButtonSV = tkinter.StringVar()
  578. self.xyCopyButtonSV.set('Copy XY (F2)')
  579. self.xyCopyButton = ttk.Button(mainframe, textvariable=self.xyCopyButtonSV, width=MOUSE_INFO_BUTTON_WIDTH, command=self._copyXyMouseInfo)
  580. self.xyCopyButton.grid(column=3, row=CUR_ROW, sticky=tkinter.W)
  581. self.xyCopyButton.bind('<Return>', self._copyXyMouseInfo)
  582. # Set up the button to log the XY coordinates:
  583. self.xyLogButtonSV = tkinter.StringVar()
  584. self.xyLogButtonSV.set('Log XY (F6)')
  585. self.xyLogButton = ttk.Button(mainframe, textvariable=self.xyLogButtonSV, width=MOUSE_INFO_BUTTON_WIDTH, command=self._logXyMouseInfo)
  586. self.xyLogButton.grid(column=4, row=CUR_ROW, sticky=tkinter.W)
  587. self.xyLogButton.bind('<Return>', self._logXyMouseInfo)
  588. # WIDGETS ON ROW 4:
  589. CUR_ROW += 1
  590. # Set up the RGB color text field and label:
  591. self.rgbSV_entry = ttk.Entry(mainframe, width=16, textvariable=self.rgbSV)
  592. self.rgbSV_entry.grid(column=2, row=CUR_ROW, sticky=(tkinter.W, tkinter.E))
  593. ttk.Label(mainframe, text='RGB Color').grid(column=1, row=CUR_ROW, sticky=tkinter.W)
  594. # Set up the button to copy the RGB color to the clipboard:
  595. self.rgbCopyButtonSV = tkinter.StringVar()
  596. self.rgbCopyButtonSV.set('Copy RGB (F3)')
  597. self.rgbCopyButton = ttk.Button(mainframe, textvariable=self.rgbCopyButtonSV, width=MOUSE_INFO_BUTTON_WIDTH, command=self._copyRgbMouseInfo)
  598. self.rgbCopyButton.grid(column=3, row=CUR_ROW, sticky=tkinter.W)
  599. self.rgbCopyButton.bind('<Return>', self._copyRgbMouseInfo)
  600. # Set up the button to log the XY coordinates:
  601. self.rgbLogButtonSV = tkinter.StringVar()
  602. self.rgbLogButtonSV.set('Log RGB (F7)')
  603. self.rgbLogButton = ttk.Button(mainframe, textvariable=self.rgbLogButtonSV, width=MOUSE_INFO_BUTTON_WIDTH, command=self._logRgbMouseInfo)
  604. self.rgbLogButton.grid(column=4, row=CUR_ROW, sticky=tkinter.W)
  605. self.rgbLogButton.bind('<Return>', self._logRgbMouseInfo)
  606. # WIDGETS ON ROW 5:
  607. CUR_ROW += 1
  608. # Set up the RGB hex color text field and label:
  609. self.rgbHexSV_entry = ttk.Entry(mainframe, width=16, textvariable=self.rgbHexSV)
  610. self.rgbHexSV_entry.grid(column=2, row=CUR_ROW, sticky=(tkinter.W, tkinter.E))
  611. ttk.Label(mainframe, text='RGB as Hex').grid(column=1, row=CUR_ROW, sticky=tkinter.W)
  612. # Set up the button to copy the RGB hex color to the clipboard:
  613. self.rgbHexCopyButtonSV = tkinter.StringVar()
  614. self.rgbHexCopyButtonSV.set('Copy RGB Hex (F4)')
  615. self.rgbHexCopyButton = ttk.Button(mainframe, textvariable=self.rgbHexCopyButtonSV, width=MOUSE_INFO_BUTTON_WIDTH, command=self._copyRgbHexMouseInfo)
  616. self.rgbHexCopyButton.grid(column=3, row=CUR_ROW, sticky=tkinter.W)
  617. self.rgbHexCopyButton.bind('<Return>', self._copyRgbHexMouseInfo)
  618. # Set up the button to log the XY coordinates:
  619. self.rgbHexLogButtonSV = tkinter.StringVar()
  620. self.rgbHexLogButtonSV.set('Log RGB Hex (F8)')
  621. self.rgbHexLogButton = ttk.Button(mainframe, textvariable=self.rgbHexLogButtonSV, width=MOUSE_INFO_BUTTON_WIDTH, command=self._logRgbHexMouseInfo)
  622. self.rgbHexLogButton.grid(column=4, row=CUR_ROW, sticky=tkinter.W)
  623. self.rgbHexLogButton.bind('<Return>', self._logRgbHexMouseInfo)
  624. # WIDGETS ON ROW 6:
  625. CUR_ROW += 1
  626. # Set up the frame that displays the color of the pixel currently under the mouse cursor:
  627. self.colorFrame = tkinter.Frame(mainframe, width=50, height=50)
  628. self.colorFrame.grid(column=2, row=CUR_ROW, sticky=(tkinter.W, tkinter.E))
  629. ttk.Label(mainframe, text='Color').grid(column=1, row=CUR_ROW, sticky=tkinter.W)
  630. # WIDGETS ON ROW 7:
  631. CUR_ROW += 1
  632. # Set up the XY origin text field and label:
  633. self.xOrigin = 0
  634. self.yOrigin = 0
  635. self.xyOriginSV.set('0, 0')
  636. ttk.Label(mainframe, text='XY Origin').grid(column=1, row=CUR_ROW, sticky=tkinter.W)
  637. self.xyOriginSV.trace("w", lambda name, index, mode, sv=self.xyOriginSV: self._xyOriginChanged(sv))
  638. self.xyOriginSV_entry = ttk.Entry(mainframe, width=16, textvariable=self.xyOriginSV)
  639. self.xyOriginSV_entry.grid(column=2, row=CUR_ROW, sticky=(tkinter.W, tkinter.E))
  640. # WIDGETS ON ROW 8:
  641. CUR_ROW += 1
  642. # Set up the multiline text widget where the log info appears:
  643. self.logTextarea = tkinter.Text(mainframe, width=20, height=6)
  644. self.logTextarea.grid(column=1, row=CUR_ROW, columnspan=4, sticky=(tkinter.W, tkinter.E, tkinter.N, tkinter.S))
  645. self.logTextareaScrollbar = ttk.Scrollbar(mainframe, orient=tkinter.VERTICAL, command=self.logTextarea.yview)
  646. self.logTextareaScrollbar.grid(column=5, row=CUR_ROW, sticky=(tkinter.N, tkinter.S))
  647. self.logTextarea['yscrollcommand'] = self.logTextareaScrollbar.set
  648. # WIDGETS ON ROW 9:
  649. CUR_ROW += 1
  650. self.logFilenameTextbox = ttk.Entry(mainframe, width=16, textvariable=self.logFilenameSV)
  651. self.logFilenameTextbox.grid(column=1, row=CUR_ROW, columnspan=3, sticky=(tkinter.W, tkinter.E))
  652. self.saveLogButton = ttk.Button(mainframe, text='Save Log', width=MOUSE_INFO_BUTTON_WIDTH, command=self._saveLogFile)
  653. self.saveLogButton.grid(column=4, row=CUR_ROW, sticky=tkinter.W)
  654. self.saveLogButton.bind('<Return>', self._saveLogFile)
  655. self.logFilenameSV.set(os.path.join(os.getcwd(), 'mouseInfoLog.txt'))
  656. # WIDGETS ON ROW 10:
  657. CUR_ROW += 1
  658. G_MOUSE_INFO_SCREENSHOT_FILENAME_entry = ttk.Entry(mainframe, width=16, textvariable=self.screenshotFilenameSV)
  659. G_MOUSE_INFO_SCREENSHOT_FILENAME_entry.grid(column=1, row=CUR_ROW, columnspan=3, sticky=(tkinter.W, tkinter.E))
  660. self.saveScreenshotButton = ttk.Button(mainframe, text='Save Screenshot', width=MOUSE_INFO_BUTTON_WIDTH, command=self._saveScreenshotFile)
  661. self.saveScreenshotButton.grid(column=4, row=CUR_ROW, sticky=tkinter.W)
  662. self.saveScreenshotButton.bind('<Return>', self._saveScreenshotFile)
  663. self.screenshotFilenameSV.set(os.path.join(os.getcwd(), 'mouseInfoScreenshot.png'))
  664. # WIDGETS ON ROW 11:
  665. CUR_ROW += 1
  666. statusbar = ttk.Label(mainframe, relief=tkinter.SUNKEN, textvariable=self.statusbarSV)
  667. statusbar.grid(column=1, row=CUR_ROW, columnspan=5, sticky=(tkinter.W, tkinter.E))
  668. # Add padding to all of the widgets:
  669. for child in mainframe.winfo_children():
  670. # Ensure the scrollbar and text area don't have padding in between them:
  671. if child == self.logTextareaScrollbar:
  672. child.grid_configure(padx=0, pady=3)
  673. elif child == self.logTextarea:
  674. child.grid_configure(padx=(3, 0), pady=3)
  675. elif child == statusbar:
  676. child.grid_configure(padx=0, pady=(3, 0))
  677. else:
  678. # All other widgets have a standard padding of 3:
  679. child.grid_configure(padx=3, pady=3)
  680. # Add keyboard hotkeys for the Copy/Log buttons:
  681. self.root.option_add('*tearOff', tkinter.FALSE) # Disable tkinter's ugly tear-off menus which are enabled by default.
  682. menu = tkinter.Menu(self.root)
  683. self.root.config(menu=menu)
  684. copyMenu = tkinter.Menu(menu)
  685. copyMenu.add_command(label='Copy All', command=self._copyAllMouseInfo, accelerator='F1', underline=5)
  686. copyMenu.add_command(label='Copy XY', command=self._copyXyMouseInfo, accelerator='F2', underline=5)
  687. copyMenu.add_command(label='Copy RGB', command=self._copyRgbMouseInfo, accelerator='F3', underline=5)
  688. copyMenu.add_command(label='Copy RGB as Hex', command=self._copyRgbHexMouseInfo, accelerator='F4', underline=12)
  689. menu.add_cascade(label='Copy', menu=copyMenu, underline=0)
  690. logMenu = tkinter.Menu(menu)
  691. logMenu.add_command(label='Log All', command=self._logAllMouseInfo, accelerator='F5', underline=4)
  692. logMenu.add_command(label='Log XY', command=self._logXyMouseInfo, accelerator='F6', underline=4)
  693. logMenu.add_command(label='Log RGB', command=self._logRgbMouseInfo, accelerator='F7', underline=4)
  694. logMenu.add_command(label='Log RGB as Hex', command=self._logRgbHexMouseInfo, accelerator='F8', underline=11)
  695. menu.add_cascade(label='Log', menu=logMenu, underline=0)
  696. helpMenu = tkinter.Menu(menu)
  697. helpMenu.add_command(label='Online Documentation', command=lambda: webbrowser.open('https://mouseinfo.readthedocs.io'), underline=6)
  698. menu.add_cascade(label='Help', menu=helpMenu, underline=0)
  699. self.root.bind_all('<F1>', self._copyAllMouseInfo)
  700. self.root.bind_all('<F2>', self._copyXyMouseInfo)
  701. self.root.bind_all('<F3>', self._copyRgbMouseInfo)
  702. self.root.bind_all('<F4>', self._copyRgbHexMouseInfo)
  703. self.root.bind_all('<F5>', self._logAllMouseInfo)
  704. self.root.bind_all('<F6>', self._logXyMouseInfo)
  705. self.root.bind_all('<F7>', self._logRgbMouseInfo)
  706. self.root.bind_all('<F8>', self._logRgbHexMouseInfo)
  707. self.root.resizable(False, False) # Prevent the window from being resized.
  708. self.xyInfoTextbox.focus() # Put the focus on the XY coordinate text field to start.
  709. self._updateMouseInfoJob = self.root.after(100, self._updateMouseInfoTextFields) # Begin updating the text fields.
  710. # Make the mouse info window "always on top".
  711. self.root.attributes('-topmost', True)
  712. self.root.update()
  713. # Start the application:
  714. self.root.mainloop()
  715. # Application has closed, set isRunning to False and cancel any "after" commands already queued:
  716. self.root.after_cancel(self._updateMouseInfoJob)
  717. self.isRunning = False
  718. # Destroy the tkinter root widget:
  719. try:
  720. self.root.destroy()
  721. except tkinter.TclError:
  722. pass
  723. def mouseInfo():
  724. """
  725. Launch the MouseInfo application in a new window.
  726. This exists as a shortcut instead of running MouseInfoWindow() because
  727. PyAutoGUI (which imports mouseinfo) is set up with a simple mouseInfo()
  728. function and I'd like to keep this consistent with that.
  729. """
  730. MouseInfoWindow()
  731. if __name__ == '__main__':
  732. MouseInfoWindow()