You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

181 lines
7.1 KiB

7 years ago
  1. """
  2. Dialogs that query users and verify the answer before accepting.
  3. Use ttk widgets, limiting use to tcl/tk 8.5+, as in IDLE 3.6+.
  4. Query is the generic base class for a popup dialog.
  5. The user must either enter a valid answer or close the dialog.
  6. Entries are validated when <Return> is entered or [Ok] is clicked.
  7. Entries are ignored when [Cancel] or [X] are clicked.
  8. The 'return value' is .result set to either a valid answer or None.
  9. Subclass SectionName gets a name for a new config file section.
  10. Configdialog uses it for new highlight theme and keybinding set names.
  11. Subclass ModuleName gets a name for File => Open Module.
  12. Subclass HelpSource gets menu item and path for additions to Help menu.
  13. """
  14. # Query and Section name result from splitting GetCfgSectionNameDialog
  15. # of configSectionNameDialog.py (temporarily config_sec.py) into
  16. # generic and specific parts. 3.6 only, July 2016.
  17. # ModuleName.entry_ok came from editor.EditorWindow.load_module.
  18. # HelpSource was extracted from configHelpSourceEdit.py (temporarily
  19. # config_help.py), with darwin code moved from ok to path_ok.
  20. import importlib.util
  21. import os
  22. from sys import executable, platform # Platform is set for one test.
  23. from tkinter import Toplevel, StringVar, W, E, S, Tk
  24. from tkinter.ttk import Frame, Button, Entry, Label
  25. from tkinter import filedialog
  26. from tkinter.font import Font
  27. class Query(Toplevel):
  28. """Base class for getting verified answer from a user.
  29. For this base class, accept any non-blank string.
  30. """
  31. def __init__(self, title, message, *, text0='', used_names={},
  32. _htest=False, _utest=False):
  33. """Create popup, do not return until tk widget destroyed.
  34. Additional subclass init must be done before calling this
  35. unless _utest=True is passed to suppress wait_window().
  36. title - string, title of popup dialog
  37. message - string, informational message to display
  38. text0 - initial value for entry
  39. used_names - names already in use
  40. _htest - bool, change box location when running htest
  41. _utest - bool, leave window hidden and not modal
  42. """
  43. Toplevel.__init__(self)
  44. self.withdraw() # Hide while configuring, especially geometry.
  45. # self.parent = parent
  46. self.title(title)
  47. self.message = message
  48. self.text0 = text0
  49. self.used_names = used_names
  50. # self.transient(parent)
  51. self.grab_set()
  52. windowingsystem = self.tk.call('tk', 'windowingsystem')
  53. if windowingsystem == 'aqua':
  54. try:
  55. self.tk.call('::tk::unsupported::MacWindowStyle', 'style',
  56. self._w, 'moveableModal', '')
  57. except:
  58. pass
  59. self.bind("<Command-.>", self.cancel)
  60. self.bind('<Key-Escape>', self.cancel)
  61. self.protocol("WM_DELETE_WINDOW", self.cancel)
  62. self.bind('<Key-Return>', self.ok)
  63. self.bind("<KP_Enter>", self.ok)
  64. self.resizable(height=False, width=False)
  65. self.create_widgets()
  66. self.update_idletasks() # Needed here for winfo_reqwidth below.
  67. self.geometry( # Center dialog over parent (or below htest box).
  68. "+150+150"
  69. )
  70. if not _utest:
  71. self.deiconify() # Unhide now that geometry set.
  72. self.wait_window()
  73. def create_widgets(self): # Call from override, if any.
  74. # Bind to self widgets needed for entry_ok or unittest.
  75. self.frame = frame = Frame(self, padding=10)
  76. frame.grid(column=0, row=0, sticky='news')
  77. frame.grid_columnconfigure(0, weight=1)
  78. entrylabel = Label(frame, anchor='w', justify='left',
  79. text=self.message)
  80. self.entryvar = StringVar(self, self.text0)
  81. self.entry = Entry(frame, width=30, textvariable=self.entryvar)
  82. self.entry.focus_set()
  83. self.error_font = Font(name='TkCaptionFont',
  84. exists=True, )
  85. self.entry_error = Label(frame, text=' ', foreground='red',
  86. font=self.error_font)
  87. self.button_ok = Button(
  88. frame, text='OK', default='active', command=self.ok)
  89. self.button_cancel = Button(
  90. frame, text='Cancel', command=self.cancel)
  91. entrylabel.grid(column=0, row=0, columnspan=3, padx=5, sticky=W)
  92. self.entry.grid(column=0, row=1, columnspan=3, padx=5, sticky=W+E,
  93. pady=[10,0])
  94. self.entry_error.grid(column=0, row=2, columnspan=3, padx=5,
  95. sticky=W+E)
  96. self.button_ok.grid(column=1, row=99, padx=5)
  97. self.button_cancel.grid(column=2, row=99, padx=5)
  98. def showerror(self, message, widget=None):
  99. #self.bell(displayof=self)
  100. (widget or self.entry_error)['text'] = 'ERROR: ' + message
  101. def entry_ok(self): # Example: usually replace.
  102. "Return non-blank entry or None."
  103. self.entry_error['text'] = ''
  104. entry = self.entry.get().strip()
  105. if not entry:
  106. self.showerror('blank line.')
  107. return None
  108. return entry
  109. def ok(self, event=None): # Do not replace.
  110. '''If entry is valid, bind it to 'result' and destroy tk widget.
  111. Otherwise leave dialog open for user to correct entry or cancel.
  112. '''
  113. entry = self.entry_ok()
  114. if entry is not None:
  115. self.result = entry
  116. self.destroy()
  117. else:
  118. # [Ok] moves focus. (<Return> does not.) Move it back.
  119. self.entry.focus_set()
  120. def cancel(self, event=None): # Do not replace.
  121. "Set dialog result to None and destroy tk widget."
  122. self.result = None
  123. self.destroy()
  124. class ModuleName(Query):
  125. "Get a module name for Open Module menu entry."
  126. # Used in open_module (editor.EditorWindow until move to iobinding).
  127. def __init__(self, title, message, text0,
  128. *, _htest=False, _utest=False):
  129. super().__init__( title, message, text0=text0,
  130. _htest=_htest, _utest=_utest)
  131. def entry_ok(self):
  132. "Return entered module name as file path or None."
  133. self.entry_error['text'] = ''
  134. name = self.entry.get().strip()
  135. if not name:
  136. self.showerror('no name specified.')
  137. return None
  138. # XXX Ought to insert current file's directory in front of path.
  139. try:
  140. spec = importlib.util.find_spec(name)
  141. except (ValueError, ImportError) as msg:
  142. self.showerror(str(msg))
  143. return None
  144. if spec is None:
  145. self.showerror("module not found")
  146. return None
  147. if not isinstance(spec.loader, importlib.abc.SourceLoader):
  148. self.showerror("not a source-based module")
  149. return None
  150. try:
  151. file_path = spec.loader.get_filename(name)
  152. except AttributeError:
  153. self.showerror("loader does not support get_filename",
  154. parent=self)
  155. return None
  156. return file_path
  157. if __name__ == "__main__":
  158. root = Tk()
  159. root.withdraw()
  160. result = ModuleName('Module','Enter module name','').result
  161. print(result)
  162. root.destroy()