text: """
  #############################################################################
  utility windows - may be useful in other programs
  #############################################################################
  """
  
  from tkinter import *
  from PP4E.Gui.Tools.windows import PopupWindow
  
  
  class HelpPopup(PopupWindow):
      """
      custom Toplevel that shows help text as scrolled text
      source button runs a passed-in callback handler
      3.0 alternative: use HTML file and webbrowser module
      """
      myfont = 'system'  # customizable
      mywidth = 78       # 3.0: start width
  
      def __init__(self, appname, helptext, iconfile=None, showsource=lambda:0):
          PopupWindow.__init__(self, appname, 'Help', iconfile)
          from tkinter.scrolledtext import ScrolledText    # a nonmodal dialog
          bar  = Frame(self)                               # pack first=clip last
          bar.pack(side=BOTTOM, fill=X)
          code = Button(bar, bg='beige', text="Source", command=showsource)
          quit = Button(bar, bg='beige', text="Cancel", command=self.destroy)
          code.pack(pady=1, side=LEFT)
          quit.pack(pady=1, side=LEFT)
          text = ScrolledText(self)                   # add Text + scrollbar
          text.config(font=self.myfont)
          text.config(width=self.mywidth)             # too big for showinfo
          text.config(bg='steelblue', fg='white')     # erase on btn or return
          text.insert('0.0', helptext)
          text.pack(expand=YES, fill=BOTH)
          self.bind("<Return>", (lambda event: self.destroy()))
  
  
  def askPasswordWindow(appname, prompt):
      """
      modal dialog to input password string     
      getpass.getpass uses stdin, not GUI
      tkSimpleDialog.askstring echos input
      """
      win = PopupWindow(appname, 'Prompt')               # a configured Toplevel
      Label(win, text=prompt).pack(side=LEFT)
      entvar = StringVar(win)
      ent = Entry(win, textvariable=entvar, show='*')    # display * for input
      ent.pack(side=RIGHT, expand=YES, fill=X)
      ent.bind('<Return>', lambda event: win.destroy())
      ent.focus_set(); win.grab_set(); win.wait_window()
      win.update()                                       # update forces redraw
      return entvar.get()                                # ent widget is now gone
  
  
  class BusyBoxWait(PopupWindow):
      """
      pop up blocking wait message box: thread waits
      main GUI event thread stays alive during wait
      but GUI is inoperable during this wait state;
      uses quit redef here because lower, not leftmost;
      """
      def __init__(self, appname, message):
          PopupWindow.__init__(self, appname, 'Busy')
          self.protocol('WM_DELETE_WINDOW', lambda:0)        # ignore deletes
          label = Label(self, text=message + '...')          # win.quit() to erase
          label.config(height=10, width=40, cursor='watch')  # busy cursor
          label.pack()
          self.makeModal()
          self.message, self.label = message, label
      def makeModal(self):
          self.focus_set()                                   # grab application
          self.grab_set()                                    # wait for threadexit
      def changeText(self, newtext):
          self.label.config(text=self.message + ': ' + newtext)
      def quit(self):
          self.destroy()                                     # don't verify quit
  
  class BusyBoxNowait(BusyBoxWait):
      """
      pop up nonblocking wait window
      call changeText to show progress, quit to close
      """
      def makeModal(self):
          pass
  
  if __name__ == '__main__':
      HelpPopup('spam', 'See figure 1...\n')
      print(askPasswordWindow('spam', 'enter password'))
      input('Enter to exit')  # pause if clicked
  