__init__.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. # PyMsgBox - A simple, cross-platform, pure Python module for JavaScript-like message boxes.
  2. from __future__ import annotations
  3. from typing import Optional, Any
  4. # By Al Sweigart al@inventwithpython.com
  5. __version__ = "1.0.9"
  6. __all__ = [
  7. "alert",
  8. "confirm",
  9. "prompt",
  10. "password",
  11. "OK_TEXT",
  12. "CANCEL_TEXT",
  13. "YES_TEXT",
  14. "NO_TEXT",
  15. "RETRY_TEXT",
  16. "ABORT_TEXT",
  17. "IGNORE_TEXT",
  18. "TRY_AGAIN_TEXT",
  19. "CONTINUE_TEXT",
  20. "TIMEOUT_RETURN_VALUE",
  21. ]
  22. # Modified BSD License
  23. # Derived from Stephen Raymond Ferg's EasyGui http://easygui.sourceforge.net/
  24. """
  25. The four functions in PyMsgBox:
  26. - alert(text='', title='', button='OK')
  27. Displays a simple message box with text and a single OK button. Returns the text of the button clicked on.
  28. - confirm(text='', title='', buttons=['OK', 'Cancel'])
  29. Displays a message box with OK and Cancel buttons. Number and text of buttons can be customized. Returns the text of the button clicked on.
  30. - prompt(text='', title='' , default='')
  31. Displays a message box with text input, and OK & Cancel buttons. Returns the text entered, or None if Cancel was clicked.
  32. - password(text='', title='', default='', mask='*')
  33. Displays a message box with text input, and OK & Cancel buttons. Typed characters appear as *. Returns the text entered, or None if Cancel was clicked.
  34. """
  35. """
  36. TODO Roadmap:
  37. - Be able to specify a custom icon in the message box.
  38. - Be able to place the message box at an arbitrary position (including on multi screen layouts)
  39. - Add mouse clicks to unit testing.
  40. - progress() function to display a progress bar
  41. - Maybe other types of dialog: open, save, file/folder picker, etc.
  42. - I18N support (internationalization)
  43. """
  44. import sys
  45. # Because PyAutoGUI requires PyMsgBox but might be installed on systems
  46. # without tkinter, we don't want a lack of tkinter to cause installation
  47. # to fail. So exceptions won't be raised until the PyMsgBox functions
  48. # are actually called.
  49. TKINTER_IMPORT_SUCCEEDED = True
  50. try:
  51. import tkinter as tk
  52. rootWindowPosition = "+300+200"
  53. if tk.TkVersion < 8.0:
  54. raise RuntimeError(
  55. "You are running Tk version: "
  56. + str(tk.TkVersion)
  57. + "You must be using Tk version 8.0 or greater to use PyMsgBox."
  58. )
  59. except ImportError:
  60. TKINTER_IMPORT_SUCCEEDED = False
  61. PROPORTIONAL_FONT_FAMILY = ("MS", "Sans", "Serif")
  62. MONOSPACE_FONT_FAMILY = "Courier"
  63. PROPORTIONAL_FONT_SIZE = 10
  64. MONOSPACE_FONT_SIZE = (
  65. 9
  66. ) # a little smaller, because it it more legible at a smaller size
  67. TEXT_ENTRY_FONT_SIZE = 12 # a little larger makes it easier to see
  68. STANDARD_SELECTION_EVENTS = ["Return", "Button-1", "space"]
  69. # constants for strings: (TODO: for internationalization, change these)
  70. OK_TEXT = "OK"
  71. CANCEL_TEXT = "Cancel"
  72. YES_TEXT = "Yes"
  73. NO_TEXT = "No"
  74. RETRY_TEXT = "Retry"
  75. ABORT_TEXT = "Abort"
  76. IGNORE_TEXT = "Ignore"
  77. TRY_AGAIN_TEXT = "Try Again"
  78. CONTINUE_TEXT = "Continue"
  79. TIMEOUT_RETURN_VALUE = "Timeout"
  80. from typing import Optional, Any
  81. def _alertTkinter(
  82. text: str = "",
  83. title: str = "",
  84. button: str = OK_TEXT,
  85. root: Optional[Any] = None,
  86. timeout: Optional[int] = None
  87. ) -> str:
  88. """Displays a simple message box with text and a single OK button. Returns the text of the button clicked on."""
  89. assert TKINTER_IMPORT_SUCCEEDED, "Tkinter is required for pymsgbox"
  90. text = str(text)
  91. retVal = _buttonbox(
  92. msg=text, title=title, choices=[str(button)], root=root, timeout=timeout
  93. )
  94. if retVal is None:
  95. return button
  96. else:
  97. return retVal
  98. alert = _alertTkinter
  99. def _confirmTkinter(
  100. text: str = "",
  101. title: str = "",
  102. buttons: tuple = (OK_TEXT, CANCEL_TEXT),
  103. root: Optional[Any] = None,
  104. timeout: Optional[int] = None
  105. ) -> Optional[str]:
  106. """Displays a message box with OK and Cancel buttons. Number and text of buttons can be customized. Returns the text of the button clicked on, or None if the dialog box was closed."""
  107. assert TKINTER_IMPORT_SUCCEEDED, "Tkinter is required for pymsgbox"
  108. return _buttonbox(
  109. msg=str(text),
  110. title=title,
  111. choices=[str(b) for b in buttons],
  112. root=root,
  113. timeout=timeout,
  114. )
  115. confirm = _confirmTkinter
  116. def _promptTkinter(
  117. text: str = "",
  118. title: str = "",
  119. default: str = "",
  120. root: Optional[Any] = None,
  121. timeout: Optional[int] = None
  122. ) -> Optional[str]:
  123. """Displays a message box with text input, and OK & Cancel buttons. Returns the text entered, or None if Cancel was clicked."""
  124. assert TKINTER_IMPORT_SUCCEEDED, "Tkinter is required for pymsgbox"
  125. text = str(text)
  126. return __fillablebox(
  127. text, title, default=default, mask=None, root=root, timeout=timeout
  128. )
  129. prompt = _promptTkinter
  130. def _passwordTkinter(
  131. text: str = "",
  132. title: str = "",
  133. default: str = "",
  134. mask: str = "*",
  135. root: Optional[Any] = None,
  136. timeout: Optional[int] = None
  137. ) -> Optional[str]:
  138. """Displays a message box with text input, and OK & Cancel buttons. Typed characters appear as *. Returns the text entered, or None if Cancel was clicked."""
  139. assert TKINTER_IMPORT_SUCCEEDED, "Tkinter is required for pymsgbox"
  140. text = str(text)
  141. return __fillablebox(text, title, default, mask=mask, root=root, timeout=timeout)
  142. password = _passwordTkinter
  143. # Load the native versions of the alert/confirm/prompt/password functions, if available:
  144. if sys.platform == "win32":
  145. from . import _native_win
  146. NO_ICON = 0
  147. STOP = 0x10
  148. QUESTION = 0x20
  149. WARNING = 0x30
  150. INFO = 0x40
  151. alert = _native_win.alert
  152. confirm = _native_win.confirm
  153. def timeoutBoxRoot():
  154. global boxRoot, __replyButtonText, __enterboxText
  155. boxRoot.destroy()
  156. __replyButtonText = TIMEOUT_RETURN_VALUE
  157. __enterboxText = TIMEOUT_RETURN_VALUE
  158. def _buttonbox(msg, title, choices, root=None, timeout=None):
  159. """
  160. Display a msg, a title, and a set of buttons.
  161. The buttons are defined by the members of the choices list.
  162. Return the text of the button that the user selected.
  163. @arg msg: the msg to be displayed.
  164. @arg title: the window title
  165. @arg choices: a list or tuple of the choices to be displayed
  166. """
  167. global boxRoot, __replyButtonText, buttonsFrame, __firstWidget
  168. # Initialize __replyButtonText to the first choice.
  169. # This is what will be used if the window is closed by the close button.
  170. __replyButtonText = choices[0]
  171. if root:
  172. root.withdraw()
  173. boxRoot = tk.Toplevel(master=root)
  174. boxRoot.withdraw()
  175. else:
  176. boxRoot = tk.Tk()
  177. boxRoot.withdraw()
  178. boxRoot.title(title)
  179. boxRoot.iconname("Dialog")
  180. boxRoot.geometry(rootWindowPosition)
  181. boxRoot.minsize(400, 100)
  182. # ------------- define the messageFrame ---------------------------------
  183. messageFrame = tk.Frame(master=boxRoot)
  184. messageFrame.pack(side=tk.TOP, fill=tk.BOTH)
  185. # ------------- define the buttonsFrame ---------------------------------
  186. buttonsFrame = tk.Frame(master=boxRoot)
  187. buttonsFrame.pack(side=tk.TOP, fill=tk.BOTH)
  188. # -------------------- place the widgets in the frames -----------------------
  189. messageWidget = tk.Message(messageFrame, text=msg, width=400)
  190. messageWidget.configure(font=(PROPORTIONAL_FONT_FAMILY, PROPORTIONAL_FONT_SIZE)) # pyright: ignore[reportArgumentType]
  191. messageWidget.pack(side=tk.TOP, expand=tk.YES, fill=tk.X, padx="3m", pady="3m")
  192. __put_buttons_in_buttonframe(choices)
  193. # -------------- the action begins -----------
  194. # put the focus on the first button
  195. __firstWidget.focus_force()
  196. boxRoot.deiconify()
  197. if timeout is not None:
  198. boxRoot.after(timeout, timeoutBoxRoot)
  199. boxRoot.mainloop()
  200. try:
  201. boxRoot.destroy()
  202. except tk.TclError:
  203. if __replyButtonText != TIMEOUT_RETURN_VALUE:
  204. __replyButtonText = None
  205. if root:
  206. root.deiconify()
  207. return __replyButtonText
  208. def __put_buttons_in_buttonframe(choices):
  209. """Put the buttons in the buttons frame"""
  210. global __widgetTexts, __firstWidget, buttonsFrame
  211. __widgetTexts = {}
  212. i = 0
  213. for buttonText in choices:
  214. tempButton = tk.Button(buttonsFrame, takefocus=1, text=buttonText)
  215. _bindArrows(tempButton)
  216. tempButton.pack(
  217. expand=tk.YES, side=tk.LEFT, padx="1m", pady="1m", ipadx="2m", ipady="1m"
  218. )
  219. # remember the text associated with this widget
  220. __widgetTexts[tempButton] = buttonText
  221. # remember the first widget, so we can put the focus there
  222. if i == 0:
  223. __firstWidget = tempButton
  224. i = 1
  225. # for the commandButton, bind activation events to the activation event handler
  226. commandButton = tempButton
  227. handler = __buttonEvent
  228. for selectionEvent in STANDARD_SELECTION_EVENTS:
  229. commandButton.bind("<%s>" % selectionEvent, handler)
  230. if CANCEL_TEXT in choices:
  231. commandButton.bind("<Escape>", __cancelButtonEvent)
  232. def _bindArrows(widget, skipArrowKeys=False):
  233. widget.bind("<Down>", _tabRight)
  234. widget.bind("<Up>", _tabLeft)
  235. if not skipArrowKeys:
  236. widget.bind("<Right>", _tabRight)
  237. widget.bind("<Left>", _tabLeft)
  238. def _tabRight(event):
  239. boxRoot.event_generate("<Tab>")
  240. def _tabLeft(event):
  241. boxRoot.event_generate("<Shift-Tab>")
  242. def __buttonEvent(event):
  243. """
  244. Handle an event that is generated by a person clicking a button.
  245. """
  246. global boxRoot, __widgetTexts, __replyButtonText
  247. __replyButtonText = __widgetTexts[event.widget]
  248. boxRoot.quit() # quit the main loop
  249. def __cancelButtonEvent(event):
  250. """Handle pressing Esc by clicking the Cancel button."""
  251. global boxRoot, __widgetTexts, __replyButtonText
  252. __replyButtonText = CANCEL_TEXT
  253. boxRoot.quit()
  254. def __fillablebox(msg, title="", default="", mask=None, root=None, timeout=None):
  255. """
  256. Show a box in which a user can enter some text.
  257. You may optionally specify some default text, which will appear in the
  258. enterbox when it is displayed.
  259. Returns the text that the user entered, or None if he cancels the operation.
  260. """
  261. global boxRoot, __enterboxText, __enterboxDefaultText
  262. global cancelButton, entryWidget, okButton
  263. if title is None:
  264. title = ""
  265. if default is None:
  266. default = ""
  267. __enterboxDefaultText = default
  268. __enterboxText = __enterboxDefaultText
  269. if root:
  270. root.withdraw()
  271. boxRoot = tk.Toplevel(master=root)
  272. boxRoot.withdraw()
  273. else:
  274. boxRoot = tk.Tk()
  275. boxRoot.withdraw()
  276. boxRoot.title(title)
  277. boxRoot.iconname("Dialog")
  278. boxRoot.geometry(rootWindowPosition)
  279. boxRoot.bind("<Escape>", __enterboxCancel)
  280. # ------------- define the messageFrame ---------------------------------
  281. messageFrame = tk.Frame(master=boxRoot)
  282. messageFrame.pack(side=tk.TOP, fill=tk.BOTH)
  283. # ------------- define the buttonsFrame ---------------------------------
  284. buttonsFrame = tk.Frame(master=boxRoot)
  285. buttonsFrame.pack(side=tk.TOP, fill=tk.BOTH)
  286. # ------------- define the entryFrame ---------------------------------
  287. entryFrame = tk.Frame(master=boxRoot)
  288. entryFrame.pack(side=tk.TOP, fill=tk.BOTH)
  289. # ------------- define the buttonsFrame ---------------------------------
  290. buttonsFrame = tk.Frame(master=boxRoot)
  291. buttonsFrame.pack(side=tk.TOP, fill=tk.BOTH)
  292. # -------------------- the msg widget ----------------------------
  293. messageWidget = tk.Message(messageFrame, width="4.5i", text=msg)
  294. messageWidget.configure(font=(PROPORTIONAL_FONT_FAMILY, PROPORTIONAL_FONT_SIZE)) # pyright: ignore[reportArgumentType]
  295. messageWidget.pack(side=tk.RIGHT, expand=1, fill=tk.BOTH, padx="3m", pady="3m")
  296. # --------- entryWidget ----------------------------------------------
  297. entryWidget = tk.Entry(entryFrame, width=40)
  298. _bindArrows(entryWidget, skipArrowKeys=True)
  299. entryWidget.configure(font=(PROPORTIONAL_FONT_FAMILY, TEXT_ENTRY_FONT_SIZE)) # pyright: ignore[reportArgumentType]
  300. if mask:
  301. entryWidget.configure(show=mask)
  302. entryWidget.pack(side=tk.LEFT, padx="3m")
  303. entryWidget.bind("<Return>", __enterboxGetText)
  304. entryWidget.bind("<Escape>", __enterboxCancel)
  305. # put text into the entryWidget and have it pre-highlighted
  306. if __enterboxDefaultText != "":
  307. entryWidget.insert(0, __enterboxDefaultText)
  308. entryWidget.select_range(0, tk.END)
  309. # ------------------ ok button -------------------------------
  310. okButton = tk.Button(buttonsFrame, takefocus=1, text=OK_TEXT)
  311. _bindArrows(okButton)
  312. okButton.pack(expand=1, side=tk.LEFT, padx="3m", pady="3m", ipadx="2m", ipady="1m")
  313. # for the commandButton, bind activation events to the activation event handler
  314. commandButton = okButton
  315. handler = __enterboxGetText
  316. for selectionEvent in STANDARD_SELECTION_EVENTS:
  317. commandButton.bind("<%s>" % selectionEvent, handler)
  318. # ------------------ cancel button -------------------------------
  319. cancelButton = tk.Button(buttonsFrame, takefocus=1, text=CANCEL_TEXT)
  320. _bindArrows(cancelButton)
  321. cancelButton.pack(
  322. expand=1, side=tk.RIGHT, padx="3m", pady="3m", ipadx="2m", ipady="1m"
  323. )
  324. # for the commandButton, bind activation events to the activation event handler
  325. commandButton = cancelButton
  326. handler = __enterboxCancel
  327. for selectionEvent in STANDARD_SELECTION_EVENTS:
  328. commandButton.bind("<%s>" % selectionEvent, handler)
  329. # ------------------- time for action! -----------------
  330. entryWidget.focus_force() # put the focus on the entryWidget
  331. boxRoot.deiconify()
  332. if timeout is not None:
  333. boxRoot.after(timeout, timeoutBoxRoot)
  334. boxRoot.mainloop() # run it!
  335. # -------- after the run has completed ----------------------------------
  336. if root:
  337. root.deiconify()
  338. try:
  339. boxRoot.destroy() # button_click didn't destroy boxRoot, so we do it now
  340. except tk.TclError:
  341. if __enterboxText != TIMEOUT_RETURN_VALUE:
  342. return None
  343. return __enterboxText
  344. def __enterboxGetText(event):
  345. global __enterboxText
  346. __enterboxText = entryWidget.get()
  347. boxRoot.quit()
  348. def __enterboxRestore(event):
  349. global entryWidget
  350. entryWidget.delete(0, len(entryWidget.get()))
  351. entryWidget.insert(0, __enterboxDefaultText)
  352. def __enterboxCancel(event):
  353. global __enterboxText
  354. __enterboxText = None
  355. boxRoot.quit()