[tor-commits] [arm/master] Moving help popup into a new popup toolkit

atagar at torproject.org atagar at torproject.org
Sat May 7 04:32:51 UTC 2011


commit b3fd2f2df18ce7310c33006f1d81f10bae620010
Author: Damian Johnson <atagar at torproject.org>
Date:   Fri May 6 21:00:02 2011 -0700

    Moving help popup into a new popup toolkit
    
    Breaking the help popup out of the controller, moving it to a nice, modular
    'show popup' function. This is a drop-in replacement (besides some reordered
    help options) and also allowed us to move the help content into their
    respective panels.
    
    This is far cleaner and avoids accessing private panel parameters. Next the
    rest of the popups will be moved to this new toolkit, which will greatly clean
    up the controller.
---
 README                           |    3 +-
 src/cli/__init__.py              |    2 +-
 src/cli/configPanel.py           |   12 +++
 src/cli/connections/connPanel.py |   16 ++++
 src/cli/controller.py            |  155 ++++++++++++++++----------------------
 src/cli/graphing/graphPanel.py   |   12 +++
 src/cli/logPanel.py              |   11 +++
 src/cli/popups.py                |  110 +++++++++++++++++++++++++++
 src/cli/torrcPanel.py            |   12 +++
 src/util/panel.py                |    9 ++
 10 files changed, 250 insertions(+), 92 deletions(-)

diff --git a/README b/README
index ff03b37..e7c2e2c 100644
--- a/README
+++ b/README
@@ -157,7 +157,8 @@ Layout:
       __init__.py
       controller.py          - main display loop, handling input and layout
       headerPanel.py         - top of all pages, providing general information
-      descriptorPopup.py     - (popup) displays connection descriptor data
+      descriptorPopup.py     - displays connection descriptor data
+      popups.py              - toolkit providing display popups
       
       logPanel.py            - (page 1) displays tor, arm, and torctl events
       configPanel.py         - (page 3) editor panel for the tor configuration
diff --git a/src/cli/__init__.py b/src/cli/__init__.py
index 171af09..1564f68 100644
--- a/src/cli/__init__.py
+++ b/src/cli/__init__.py
@@ -2,5 +2,5 @@
 Panels, popups, and handlers comprising the arm user interface.
 """
 
-__all__ = ["configPanel", "controller", "descriptorPopup", "headerPanel", "logPanel", "torrcPanel"]
+__all__ = ["configPanel", "controller", "descriptorPopup", "headerPanel", "logPanel", "popups", "torrcPanel"]
 
diff --git a/src/cli/configPanel.py b/src/cli/configPanel.py
index fd6fb54..90c6191 100644
--- a/src/cli/configPanel.py
+++ b/src/cli/configPanel.py
@@ -248,6 +248,18 @@ class ConfigPanel(panel.Panel):
       self.redraw(True)
     self.valsLock.release()
   
+  def getHelp(self):
+    options = []
+    options.append(("up arrow", "scroll up a line", None))
+    options.append(("down arrow", "scroll down a line", None))
+    options.append(("page up", "scroll up a page", None))
+    options.append(("page down", "scroll down a page", None))
+    options.append(("enter", "edit configuration option", None))
+    options.append(("w", "save configuration", None))
+    options.append(("a", "toggle option filtering", None))
+    options.append(("s", "sort ordering", None))
+    return options
+  
   def draw(self, width, height):
     self.valsLock.acquire()
     
diff --git a/src/cli/connections/connPanel.py b/src/cli/connections/connPanel.py
index 5f4f036..40c479e 100644
--- a/src/cli/connections/connPanel.py
+++ b/src/cli/connections/connPanel.py
@@ -185,6 +185,22 @@ class ConnectionPanel(panel.Panel, threading.Thread):
         drawTicks = (time.time() - lastDraw) / self._config["features.connection.refreshRate"]
         lastDraw += self._config["features.connection.refreshRate"] * drawTicks
   
+  def getHelp(self):
+    resolverUtil = connections.getResolver("tor").overwriteResolver
+    if resolverUtil == None: resolverUtil = "auto"
+    
+    options = []
+    options.append(("up arrow", "scroll up a line", None))
+    options.append(("down arrow", "scroll down a line", None))
+    options.append(("page up", "scroll up a page", None))
+    options.append(("page down", "scroll down a page", None))
+    options.append(("enter", "edit configuration option", None))
+    options.append(("d", "raw consensus descriptor", None))
+    options.append(("l", "listed identity", self._listingType.lower()))
+    options.append(("s", "sort ordering", None))
+    options.append(("u", "resolving utility", resolverUtil))
+    return options
+  
   def draw(self, width, height):
     self.valsLock.acquire()
     
diff --git a/src/cli/controller.py b/src/cli/controller.py
index b8bb5cd..1501419 100644
--- a/src/cli/controller.py
+++ b/src/cli/controller.py
@@ -15,6 +15,7 @@ import curses.textpad
 import socket
 from TorCtl import TorCtl
 
+import popups
 import headerPanel
 import graphing.graphPanel
 import logPanel
@@ -30,6 +31,56 @@ import graphing.bandwidthStats
 import graphing.connStats
 import graphing.resourceStats
 
+# TODO: controller should be its own object that can be refreshed - until that
+# emulating via a 'refresh' flag
+REFRESH_FLAG = False
+
+def refresh():
+  global REFRESH_FLAG
+  REFRESH_FLAG = True
+
+# new panel params and accessors (this is part of the new controller apis)
+PANELS = {}
+STDSCR = None
+
+def getScreen():
+  return STDSCR
+
+def getPage():
+  """
+  Provides the number belonging to this page. Page numbers start at one.
+  """
+  
+  return PAGE + 1
+
+def getPanel(name):
+  """
+  Provides the panel with the given identifier.
+  
+  Arguments:
+    name - name of the panel to be fetched
+  """
+  
+  return PANELS[name]
+
+def getPanels(page = None):
+  """
+  Provides all panels or all panels from a given page.
+  
+  Arguments:
+    page - page number of the panels to be fetched, all panels if undefined
+  """
+  
+  panelSet = []
+  if page == None:
+    # fetches all panel names
+    panelSet = list(PAGE_S)
+    for pagePanels in PAGES:
+      panelSet += pagePanels
+  else: panelSet = PAGES[page - 1]
+  
+  return [getPanel(name) for name in panelSet]
+
 CONFIRM_QUIT = True
 REFRESH_RATE = 5        # seconds between redrawing screen
 MAX_REGEX_FILTERS = 5   # maximum number of previous regex filters that'll be remembered
@@ -422,6 +473,9 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
     otherwise unrecognized events)
   """
   
+  global PANELS, STDSCR, REFRESH_FLAG, PAGE
+  STDSCR = stdscr
+  
   # loads config for various interface components
   config = conf.getConfig("arm")
   config.update(CONFIG)
@@ -609,6 +663,8 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
     pluralLabel = "s" if len(missingEventTypes) > 1 else ""
     log.log(CONFIG["log.torEventTypeUnrecognized"], "arm doesn't recognize the following event type%s: %s (log 'UNKNOWN' events to see them)" % (pluralLabel, ", ".join(missingEventTypes)))
   
+  PANELS = panels
+  
   # tells revised panels to run as daemons
   panels["header"].start()
   panels["log"].start()
@@ -631,6 +687,8 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
   regexFilters = []             # previously used log regex filters
   panels["popup"].redraw(True)  # hack to make sure popup has a window instance (not entirely sure why...)
   
+  PAGE = page
+  
   # provides notice about any unused config keys
   for key in config.getUnusedKeys():
     log.log(CONFIG["log.configEntryUndefined"], "Unused configuration entry: %s" % key)
@@ -866,6 +924,8 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
         isVisible = i == page
         for entry in PAGES[i]: panels[entry].setVisible(isVisible)
       
+      PAGE = page
+      
       panels["control"].page = page + 1
       
       # TODO: this redraw doesn't seem necessary (redraws anyway after this
@@ -914,96 +974,7 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
       finally:
         panel.CURSES_LOCK.release()
     elif key == ord('h') or key == ord('H'):
-      # displays popup for current page's controls
-      panel.CURSES_LOCK.acquire()
-      try:
-        setPauseState(panels, isPaused, page, True)
-        
-        # lists commands
-        popup = panels["popup"]
-        popup.clear()
-        popup.win.box()
-        popup.addstr(0, 0, "Page %i Commands:" % (page + 1), curses.A_STANDOUT)
-        
-        pageOverrideKeys = ()
-        
-        if page == 0:
-          graphedStats = panels["graph"].currentDisplay
-          if not graphedStats: graphedStats = "none"
-          popup.addfstr(1, 2, "<b>up arrow</b>: scroll log up a line")
-          popup.addfstr(1, 41, "<b>down arrow</b>: scroll log down a line")
-          popup.addfstr(2, 2, "<b>m</b>: increase graph size")
-          popup.addfstr(2, 41, "<b>n</b>: decrease graph size")
-          popup.addfstr(3, 2, "<b>s</b>: graphed stats (<b>%s</b>)" % graphedStats)
-          popup.addfstr(3, 41, "<b>i</b>: graph update interval (<b>%s</b>)" % graphing.graphPanel.UPDATE_INTERVALS[panels["graph"].updateInterval][0])
-          popup.addfstr(4, 2, "<b>b</b>: graph bounds (<b>%s</b>)" % panels["graph"].bounds.lower())
-          popup.addfstr(4, 41, "<b>a</b>: save snapshot of the log")
-          popup.addfstr(5, 2, "<b>e</b>: change logged events")
-          
-          regexLabel = "enabled" if panels["log"].regexFilter else "disabled"
-          popup.addfstr(5, 41, "<b>f</b>: log regex filter (<b>%s</b>)" % regexLabel)
-          
-          hiddenEntryLabel = "visible" if panels["log"].showDuplicates else "hidden"
-          popup.addfstr(6, 2, "<b>u</b>: duplicate log entries (<b>%s</b>)" % hiddenEntryLabel)
-          popup.addfstr(6, 41, "<b>c</b>: clear event log")
-          
-          pageOverrideKeys = (ord('m'), ord('n'), ord('s'), ord('i'), ord('d'), ord('e'), ord('r'), ord('f'), ord('x'))
-        if page == 1:
-          popup.addfstr(1, 2, "<b>up arrow</b>: scroll up a line")
-          popup.addfstr(1, 41, "<b>down arrow</b>: scroll down a line")
-          popup.addfstr(2, 2, "<b>page up</b>: scroll up a page")
-          popup.addfstr(2, 41, "<b>page down</b>: scroll down a page")
-          
-          popup.addfstr(3, 2, "<b>enter</b>: edit configuration option")
-          popup.addfstr(3, 41, "<b>d</b>: raw consensus descriptor")
-          
-          listingType = panels["conn"]._listingType.lower()
-          popup.addfstr(4, 2, "<b>l</b>: listed identity (<b>%s</b>)" % listingType)
-          
-          popup.addfstr(4, 41, "<b>s</b>: sort ordering")
-          
-          resolverUtil = connections.getResolver("tor").overwriteResolver
-          if resolverUtil == None: resolverUtil = "auto"
-          popup.addfstr(5, 2, "<b>u</b>: resolving utility (<b>%s</b>)" % resolverUtil)
-          
-          pageOverrideKeys = (ord('d'), ord('l'), ord('s'), ord('u'))
-        elif page == 2:
-          popup.addfstr(1, 2, "<b>up arrow</b>: scroll up a line")
-          popup.addfstr(1, 41, "<b>down arrow</b>: scroll down a line")
-          popup.addfstr(2, 2, "<b>page up</b>: scroll up a page")
-          popup.addfstr(2, 41, "<b>page down</b>: scroll down a page")
-          popup.addfstr(3, 2, "<b>enter</b>: edit configuration option")
-          popup.addfstr(3, 41, "<b>w</b>: save configuration")
-          popup.addfstr(4, 2, "<b>a</b>: toggle option filtering")
-          popup.addfstr(4, 41, "<b>s</b>: sort ordering")
-        elif page == 3:
-          popup.addfstr(1, 2, "<b>up arrow</b>: scroll up a line")
-          popup.addfstr(1, 41, "<b>down arrow</b>: scroll down a line")
-          popup.addfstr(2, 2, "<b>page up</b>: scroll up a page")
-          popup.addfstr(2, 41, "<b>page down</b>: scroll down a page")
-          
-          strippingLabel = "on" if panels["torrc"].stripComments else "off"
-          popup.addfstr(3, 2, "<b>s</b>: comment stripping (<b>%s</b>)" % strippingLabel)
-          
-          lineNumLabel = "on" if panels["torrc"].showLineNum else "off"
-          popup.addfstr(3, 41, "<b>n</b>: line numbering (<b>%s</b>)" % lineNumLabel)
-          
-          popup.addfstr(4, 2, "<b>r</b>: reload torrc")
-          popup.addfstr(4, 41, "<b>x</b>: reset tor (issue sighup)")
-        
-        popup.addstr(7, 2, "Press any key...")
-        popup.refresh()
-        
-        # waits for user to hit a key, if it belongs to a command then executes it
-        curses.cbreak()
-        helpExitKey = stdscr.getch()
-        if helpExitKey in pageOverrideKeys: overrideKey = helpExitKey
-        curses.halfdelay(REFRESH_RATE * 10)
-        
-        setPauseState(panels, isPaused, page)
-        selectiveRefresh(panels, page)
-      finally:
-        panel.CURSES_LOCK.release()
+      overrideKey = popups.showHelpPopup()
     elif page == 0 and (key == ord('s') or key == ord('S')):
       # provides menu to pick stats to be graphed
       #options = ["None"] + [label for label in panels["graph"].stats.keys()]
@@ -1585,6 +1556,10 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
       panels["config"].handleKey(key)
     elif page == 3:
       panels["torrc"].handleKey(key)
+    
+    if REFRESH_FLAG:
+      REFRESH_FLAG = False
+      selectiveRefresh(panels, page)
 
 def startTorMonitor(startTime, loggedEvents, isBlindMode):
   try:
diff --git a/src/cli/graphing/graphPanel.py b/src/cli/graphing/graphPanel.py
index 238e163..d8808b3 100644
--- a/src/cli/graphing/graphPanel.py
+++ b/src/cli/graphing/graphPanel.py
@@ -253,6 +253,18 @@ class GraphPanel(panel.Panel):
     
     self.graphHeight = max(MIN_GRAPH_HEIGHT, newGraphHeight)
   
+  def getHelp(self):
+    if self.currentDisplay: graphedStats = self.currentDisplay
+    else: graphedStats = "none"
+    
+    options = []
+    options.append(("m", "increase graph size", None))
+    options.append(("n", "decrease graph size", None))
+    options.append(("s", "graphed stats", graphedStats))
+    options.append(("b", "graph bounds", self.bounds.lower()))
+    options.append(("i", "graph update interval", UPDATE_INTERVALS[self.updateInterval][0]))
+    return options
+  
   def draw(self, width, height):
     """ Redraws graph panel """
     
diff --git a/src/cli/logPanel.py b/src/cli/logPanel.py
index 6feb129..b12715e 100644
--- a/src/cli/logPanel.py
+++ b/src/cli/logPanel.py
@@ -761,6 +761,17 @@ class LogPanel(panel.Panel, threading.Thread):
       self.redraw(True)
       self.valsLock.release()
   
+  def getHelp(self):
+    options = []
+    options.append(("up arrow", "scroll log up a line", None))
+    options.append(("down arrow", "scroll log down a line", None))
+    options.append(("a", "save snapshot of the log", None))
+    options.append(("e", "change logged events", None))
+    options.append(("f", "log regex filter", "enabled" if self.regexFilter else "disabled"))
+    options.append(("u", "duplicate log entries", "visible" if self.showDuplicates else "hidden"))
+    options.append(("c", "clear event log", None))
+    return options
+  
   def draw(self, width, height):
     """
     Redraws message log. Entries stretch to use available space and may
diff --git a/src/cli/popups.py b/src/cli/popups.py
new file mode 100644
index 0000000..ce51ee8
--- /dev/null
+++ b/src/cli/popups.py
@@ -0,0 +1,110 @@
+"""
+Functions for displaying popups in the interface.
+"""
+
+import curses
+
+import controller
+
+from util import panel, uiTools
+
+def init(height = -1, width = -1):
+  """
+  Preparation for displaying a popup. This creates a popup with a valid
+  subwindow instance. If that's successful then the curses lock is acquired
+  and this returns a tuple of the...
+  (popup, draw width, draw height)
+  Otherwise this leaves curses unlocked and returns None.
+  
+  Arguments:
+    height - maximum height of the popup
+    width  - maximum width of the popup
+  """
+  
+  topSize = controller.getPanel("header").getHeight()
+  topSize += controller.getPanel("control").getHeight()
+  
+  popup = panel.Panel(controller.getScreen(), "popup", topSize, height, width)
+  popup.setVisible(True)
+  
+  # Redraws the popup to prepare a subwindow instance. If none is spawned then
+  # the panel can't be drawn (for instance, due to not being visible).
+  popup.redraw(True)
+  if popup.win != None:
+    panel.CURSES_LOCK.acquire()
+    return (popup, popup.maxX - 1, popup.maxY)
+  else: return None
+
+def finalize():
+  """
+  Cleans up after displaying a popup, releasing the cureses lock and redrawing
+  the rest of the display.
+  """
+  
+  controller.refresh()
+  panel.CURSES_LOCK.release()
+
+def showHelpPopup():
+  """
+  Presents a popup with instructions for the current page's hotkeys. This
+  returns the user input used to close the popup. If the popup didn't close
+  properly, this is an arrow, enter, or scroll key then this returns None.
+  """
+  
+  popup, width, height = init(9, 80)
+  if not popup: return
+  
+  exitKey = None
+  try:
+    pageNum = controller.getPage()
+    pagePanels = controller.getPanels(pageNum)
+    
+    # the first page is the only one with multiple panels, and it looks better
+    # with the log entries first, so reversing the order
+    pagePanels.reverse()
+    
+    helpOptions = []
+    for entry in pagePanels:
+      helpOptions += entry.getHelp()
+    
+    # test doing afterward in case of overwriting
+    popup.win.box()
+    popup.addstr(0, 0, "Page %i Commands:" % pageNum, curses.A_STANDOUT)
+    
+    for i in range(len(helpOptions)):
+      if i / 2 >= height - 2: break
+      
+      # draws entries in the form '<key>: <description>[ (<selection>)]', for
+      # instance...
+      # u: duplicate log entries (hidden)
+      key, description, selection = helpOptions[i]
+      if key: description = ": " + description
+      row = (i / 2) + 1
+      col = 2 if i % 2 == 0 else 41
+      
+      popup.addstr(row, col, key, curses.A_BOLD)
+      col += len(key)
+      popup.addstr(row, col, description)
+      col += len(description)
+      
+      if selection:
+        popup.addstr(row, col, " (")
+        popup.addstr(row, col + 2, selection, curses.A_BOLD)
+        popup.addstr(row, col + 2 + len(selection), ")")
+    
+    # tells user to press a key if the lower left is unoccupied
+    if len(helpOptions) < 13 and height == 9:
+      popup.addstr(7, 2, "Press any key...")
+    
+    popup.win.refresh()
+    curses.cbreak()
+    exitKey = controller.getScreen().getch()
+    curses.halfdelay(controller.REFRESH_RATE * 10)
+  finally: finalize()
+  
+  if not uiTools.isSelectionKey(exitKey) and \
+    not uiTools.isScrollKey(exitKey) and \
+    not exitKey in (curses.KEY_LEFT, curses.KEY_RIGHT):
+    return exitKey
+  else: return None
+
diff --git a/src/cli/torrcPanel.py b/src/cli/torrcPanel.py
index b7cad86..6d7156d 100644
--- a/src/cli/torrcPanel.py
+++ b/src/cli/torrcPanel.py
@@ -60,6 +60,18 @@ class TorrcPanel(panel.Panel):
     
     self.valsLock.release()
   
+  def getHelp(self):
+    options = []
+    options.append(("up arrow", "scroll up a line", None))
+    options.append(("down arrow", "scroll down a line", None))
+    options.append(("page up", "scroll up a page", None))
+    options.append(("page down", "scroll down a page", None))
+    options.append(("s", "comment stripping", "on" if self.stripComments else "off"))
+    options.append(("n", "line numbering", "on" if self.showLineNum else "off"))
+    options.append(("r", "reload torrc", None))
+    options.append(("x", "reset tor (issue sighup)", None))
+    return options
+  
   def draw(self, width, height):
     self.valsLock.acquire()
     
diff --git a/src/util/panel.py b/src/util/panel.py
index 9c3dd29..7387833 100644
--- a/src/util/panel.py
+++ b/src/util/panel.py
@@ -290,6 +290,15 @@ class Panel():
     if setWidth != -1: newWidth = min(newWidth, setWidth)
     return (newHeight, newWidth)
   
+  def getHelp(self):
+    """
+    Provides help information for the controls this page provides. This is a
+    list of tuples of the form...
+    (control, description, status)
+    """
+    
+    return []
+  
   def draw(self, width, height):
     """
     Draws display's content. This is meant to be overwritten by 



More information about the tor-commits mailing list