[tor-commits] [arm/master] Alternative menu implementation

atagar at torproject.org atagar at torproject.org
Sat Jun 4 21:12:31 UTC 2011


commit c1aecf68b13bf2eb12a104b98dab9dde6a2a46f0
Author: Damian Johnson <atagar at torproject.org>
Date:   Sat Jun 4 13:52:09 2011 -0700

    Alternative menu implementation
    
    This is a smaller and (I think) simpler implementation of a menu. Its model
    and presentation logic are done so the menu can be opened and used, but
    handlers haven't been attached yet so nothing happens when a selection is
    made.
---
 README                   |    5 +
 src/cli/controller.py    |    5 +-
 src/cli/menu/__init__.py |    6 ++
 src/cli/menu/item.py     |  146 ++++++++++++++++++++++++++++++
 src/cli/menu/menu.py     |  222 ++++++++++++++++++++++++++++++++++++++++++++++
 src/cli/popups.py        |   15 ++--
 src/util/panel.py        |    2 +-
 7 files changed, 393 insertions(+), 8 deletions(-)

diff --git a/README b/README
index b20ec1c..0f26163 100644
--- a/README
+++ b/README
@@ -176,6 +176,11 @@ Layout:
         connEntry.py      - individual connections to or from the system
         entries.py        - common parent for connPanel display entries
       
+      menu/
+        __init__.py
+        menu.py           - provides an interactive menu
+        item.py           - individual items within the menu
+      
       __init__.py
       controller.py          - main display loop, handling input and layout
       headerPanel.py         - top of all pages, providing general information
diff --git a/src/cli/controller.py b/src/cli/controller.py
index fcbaed5..fac43a4 100644
--- a/src/cli/controller.py
+++ b/src/cli/controller.py
@@ -8,6 +8,7 @@ import curses
 import threading
 
 import cli.menu
+import cli.menu.menu
 import cli.popups
 import cli.headerPanel
 import cli.logPanel
@@ -510,7 +511,7 @@ def drawTorMonitor(stdscr, startTime):
       control.prevPage()
     elif key == ord('p') or key == ord('P'):
       control.setPaused(not control.isPaused())
-    elif key == ord('m') or key == ord('M'):
+    elif key == ord('n') or key == ord('N'):
       menu = cli.menu.Menu()
       menuKeys = menu.showMenu(keys=menuKeys)
       if menuKeys != []:
@@ -518,6 +519,8 @@ def drawTorMonitor(stdscr, startTime):
           if key in menuKeys:
             menuKeys.remove(key)
             overrideKey = key
+    elif key == ord('m') or key == ord('M'):
+      cli.menu.menu.showMenu()
     elif key == ord('q') or key == ord('Q'):
       # provides prompt to confirm that arm should exit
       if CONFIG["features.confirmQuit"]:
diff --git a/src/cli/menu/__init__.py b/src/cli/menu/__init__.py
new file mode 100644
index 0000000..e6b5f10
--- /dev/null
+++ b/src/cli/menu/__init__.py
@@ -0,0 +1,6 @@
+"""
+Resources for displaying the menu.
+"""
+
+__all__ = ["item", "menu"]
+
diff --git a/src/cli/menu/item.py b/src/cli/menu/item.py
new file mode 100644
index 0000000..bfc7ffd
--- /dev/null
+++ b/src/cli/menu/item.py
@@ -0,0 +1,146 @@
+"""
+Menu item, representing an option in the drop-down menu.
+"""
+
+class MenuItem():
+  """
+  Option in a drop-down menu.
+  """
+  
+  def __init__(self, label, callback):
+    self._label = label
+    self._callback = callback
+    self._parent = None
+  
+  def getLabel(self):
+    """
+    Provides a tuple of three strings representing the prefix, label, and
+    suffix for this item.
+    """
+    
+    return ("", self._label, "")
+  
+  def getParent(self):
+    """
+    Provides the Submenu we're contained within.
+    """
+    
+    return self._parent
+  
+  def getHierarchy(self):
+    """
+    Provides a list with all of our parents, up to the root.
+    """
+    
+    myHierarchy = [self]
+    while myHierarchy[-1].getParent():
+      myHierarchy.append(myHierarchy[-1].getParent())
+    
+    myHierarchy.reverse()
+    return myHierarchy
+  
+  def getRoot(self):
+    """
+    Provides the base submenu we belong to.
+    """
+    
+    if self._parent: return self._parent.getRoot()
+    else: return self
+  
+  def select(self):
+    """
+    Performs the callback for the menu item, returning true if we should close
+    the menu and false otherwise.
+    """
+    
+    if self._callback: return self._callback()
+    else: return False
+  
+  def next(self):
+    """
+    Provides the next option for the submenu we're in, raising a ValueError
+    if we don't have a parent.
+    """
+    
+    return self._getSibling(1)
+  
+  def prev(self):
+    """
+    Provides the previous option for the submenu we're in, raising a ValueError
+    if we don't have a parent.
+    """
+    
+    return self._getSibling(-1)
+  
+  def _getSibling(self, offset):
+    """
+    Provides our sibling with a given index offset from us, raising a
+    ValueError if we don't have a parent.
+    
+    Arguments:
+      offset - index offset for the sibling to be returned
+    """
+    
+    if self._parent:
+      mySiblings = self._parent.getChildren()
+      
+      try:
+        myIndex = mySiblings.index(self)
+        return mySiblings[(myIndex + offset) % len(mySiblings)]
+      except ValueError:
+        # We expect a bidirectional references between submenus and their
+        # children. If we don't have this then our menu's screwed up.
+        
+        msg = "The '%s' submenu doesn't contain '%s' (children: '%s')" % (self, self._parent, "', '".join(mySiblings))
+        raise ValueError(msg)
+    else: raise ValueError("Menu option '%s' doesn't have a parent" % self)
+  
+  def __str__(self):
+    return self._label
+
+class Submenu(MenuItem):
+  """
+  Menu item that lists other menu options.
+  """
+  
+  def __init__(self, label):
+    MenuItem.__init__(self, label, None)
+    self._children = []
+  
+  def getLabel(self):
+    """
+    Provides our label with a ">" suffix to indicate that we have suboptions.
+    """
+    
+    myLabel = MenuItem.getLabel(self)[1]
+    return ("", myLabel, " >")
+  
+  def add(self, menuItem):
+    """
+    Adds the given menu item to our listing. This raises a ValueError if the
+    item already has a parent.
+    
+    Arguments:
+      menuItem - menu option to be added
+    """
+    
+    if menuItem.getParent():
+      raise ValueError("Menu option '%s' already has a parent" % menuItem)
+    else:
+      menuItem._parent = self
+      self._children.append(menuItem)
+  
+  def getChildren(self):
+    """
+    Provides the menu and submenus we contain.
+    """
+    
+    return list(self._children)
+  
+  def isEmpty(self):
+    """
+    True if we have no children, false otherwise.
+    """
+    
+    return not bool(self._children)
+
diff --git a/src/cli/menu/menu.py b/src/cli/menu/menu.py
new file mode 100644
index 0000000..02f87ee
--- /dev/null
+++ b/src/cli/menu/menu.py
@@ -0,0 +1,222 @@
+
+import curses
+
+import cli.popups
+import cli.controller
+import cli.menu.item
+
+from util import uiTools
+
+def makeMenu():
+  """
+  Constructs the base menu and all of its contents.
+  """
+  
+  baseMenu = cli.menu.item.Submenu("")
+  
+  fileMenu = cli.menu.item.Submenu("File")
+  fileMenu.add(cli.menu.item.MenuItem("Exit", None))
+  baseMenu.add(fileMenu)
+  
+  logsMenu = cli.menu.item.Submenu("Logs")
+  logsMenu.add(cli.menu.item.MenuItem("Events", None))
+  logsMenu.add(cli.menu.item.MenuItem("Clear", None))
+  logsMenu.add(cli.menu.item.MenuItem("Save", None))
+  logsMenu.add(cli.menu.item.MenuItem("Filter", None))
+  
+  duplicatesSubmenu = cli.menu.item.Submenu("Duplicates")
+  duplicatesSubmenu.add(cli.menu.item.MenuItem("Hidden", None))
+  duplicatesSubmenu.add(cli.menu.item.MenuItem("Visible", None))
+  logsMenu.add(duplicatesSubmenu)
+  baseMenu.add(logsMenu)
+  
+  viewMenu = cli.menu.item.Submenu("View")
+  viewMenu.add(cli.menu.item.MenuItem("Graph", None))
+  viewMenu.add(cli.menu.item.MenuItem("Connections", None))
+  viewMenu.add(cli.menu.item.MenuItem("Configuration", None))
+  viewMenu.add(cli.menu.item.MenuItem("Configuration File", None))
+  baseMenu.add(viewMenu)
+  
+  graphMenu = cli.menu.item.Submenu("Graph")
+  graphMenu.add(cli.menu.item.MenuItem("Stats", None))
+  
+  sizeSubmenu = cli.menu.item.Submenu("Size")
+  sizeSubmenu.add(cli.menu.item.MenuItem("Increase", None))
+  sizeSubmenu.add(cli.menu.item.MenuItem("Decrease", None))
+  graphMenu.add(sizeSubmenu)
+  
+  graphMenu.add(cli.menu.item.MenuItem("Update Interval", None))
+  
+  boundsSubmenu = cli.menu.item.Submenu("Bounds")
+  boundsSubmenu.add(cli.menu.item.MenuItem("Local Max", None))
+  boundsSubmenu.add(cli.menu.item.MenuItem("Global Max", None))
+  boundsSubmenu.add(cli.menu.item.MenuItem("Tight", None))
+  graphMenu.add(boundsSubmenu)
+  baseMenu.add(graphMenu)
+  
+  connectionsMenu = cli.menu.item.Submenu("Connections")
+  connectionsMenu.add(cli.menu.item.MenuItem("Identity", None))
+  connectionsMenu.add(cli.menu.item.MenuItem("Resolver", None))
+  connectionsMenu.add(cli.menu.item.MenuItem("Sort Order", None))
+  baseMenu.add(connectionsMenu)
+  
+  configurationMenu = cli.menu.item.Submenu("Configuration")
+  
+  commentsSubmenu = cli.menu.item.Submenu("Comments")
+  commentsSubmenu.add(cli.menu.item.MenuItem("Hidden", None))
+  commentsSubmenu.add(cli.menu.item.MenuItem("Visible", None))
+  configurationMenu.add(commentsSubmenu)
+  
+  configurationMenu.add(cli.menu.item.MenuItem("Reload", None))
+  configurationMenu.add(cli.menu.item.MenuItem("Reset Tor", None))
+  baseMenu.add(configurationMenu)
+  
+  return baseMenu
+
+class MenuCursor:
+  """
+  Tracks selection and key handling in the menu.
+  """
+  
+  def __init__(self, initialSelection):
+    self._selection = initialSelection
+    self._isDone = False
+  
+  def isDone(self):
+    """
+    Provides true if a selection has indicated that we should close the menu.
+    False otherwise.
+    """
+    
+    return self._isDone
+  
+  def getSelection(self):
+    """
+    Provides the currently selected menu item.
+    """
+    
+    return self._selection
+  
+  def handleKey(self, key):
+    isSelectionSubmenu = isinstance(self._selection, cli.menu.item.Submenu)
+    selectionHierarchy = self._selection.getHierarchy()
+    
+    if uiTools.isSelectionKey(key):
+      if isSelectionSubmenu:
+        if not self._selection.isEmpty():
+          self._selection = self._selection.getChildren()[0]
+      else: self._isDone = self._selection.select()
+    elif key == curses.KEY_UP:
+      self._selection = self._selection.prev()
+    elif key == curses.KEY_DOWN:
+      self._selection = self._selection.next()
+    elif key == curses.KEY_LEFT:
+      if len(selectionHierarchy) <= 3:
+        # shift to the previous main submenu
+        prevSubmenu = selectionHierarchy[1].prev()
+        self._selection = prevSubmenu.getChildren()[0]
+      else:
+        # go up a submenu level
+        self._selection = self._selection.getParent()
+    elif key == curses.KEY_RIGHT:
+      if isSelectionSubmenu:
+        # open submenu (same as making a selection)
+        if not self._selection.isEmpty():
+          self._selection = self._selection.getChildren()[0]
+      else:
+        # shift to the next main submenu
+        nextSubmenu = selectionHierarchy[1].next()
+        self._selection = nextSubmenu.getChildren()[0]
+    elif key in (27, ord('m'), ord('M')):
+      # close menu
+      self._isDone = True
+
+def showMenu():
+  popup, _, _ = cli.popups.init(1, belowStatic = False)
+  if not popup: return
+  control = cli.controller.getController()
+  
+  try:
+    # generates the menu and uses the initial selection of the first item in
+    # the file menu
+    menu = makeMenu()
+    cursor = MenuCursor(menu.getChildren()[0].getChildren()[0])
+    
+    while not cursor.isDone():
+      # sets the background color
+      popup.win.clear()
+      popup.win.bkgd(' ', curses.A_STANDOUT | uiTools.getColor("red"))
+      selectionHierarchy = cursor.getSelection().getHierarchy()
+      
+      # renders the menu bar, noting where the open submenu is positioned
+      drawLeft, selectionLeft = 0, 0
+      
+      for topLevelItem in menu.getChildren():
+        drawFormat = curses.A_BOLD
+        if topLevelItem == selectionHierarchy[1]:
+          drawFormat |= curses.A_UNDERLINE
+          selectionLeft = drawLeft
+        
+        drawLabel = " %s " % topLevelItem.getLabel()[1]
+        popup.addstr(0, drawLeft, drawLabel, drawFormat)
+        popup.addch(0, drawLeft + len(drawLabel), curses.ACS_VLINE)
+        
+        drawLeft += len(drawLabel) + 1
+      
+      # recursively shows opened submenus
+      _drawSubmenu(cursor, 1, 1, selectionLeft)
+      
+      popup.win.refresh()
+      
+      key = control.getScreen().getch()
+      cursor.handleKey(key)
+      
+      # redraws the rest of the interface if we're rendering on it again
+      if not cursor.isDone():
+        for panelImpl in control.getDisplayPanels():
+          panelImpl.redraw(True)
+  finally: cli.popups.finalize()
+
+def _drawSubmenu(cursor, level, top, left):
+  selectionHierarchy = cursor.getSelection().getHierarchy()
+  
+  # checks if there's nothing to display
+  if len(selectionHierarchy) < level + 2: return
+  
+  # fetches the submenu and selection we're displaying
+  submenu = selectionHierarchy[level]
+  selection = selectionHierarchy[level + 1]
+  
+  # gets the size of the prefix, middle, and suffix columns
+  allLabelSets = [entry.getLabel() for entry in submenu.getChildren()]
+  prefixColSize = max([len(entry[0]) for entry in allLabelSets])
+  middleColSize = max([len(entry[1]) for entry in allLabelSets])
+  suffixColSize = max([len(entry[2]) for entry in allLabelSets])
+  
+  # formatted string so we can display aligned menu entries
+  labelFormat = " %%-%is%%-%is%%-%is " % (prefixColSize, middleColSize, suffixColSize)
+  menuWidth = len(labelFormat % ("", "", ""))
+  
+  popup, _, _ = cli.popups.init(len(submenu.getChildren()), menuWidth, top, left, belowStatic = False)
+  if not popup: return
+  
+  try:
+    # sets the background color
+    popup.win.bkgd(' ', curses.A_STANDOUT | uiTools.getColor("red"))
+    
+    drawTop, selectionTop = 0, 0
+    for menuItem in submenu.getChildren():
+      if menuItem == selection:
+        drawFormat = curses.A_BOLD | uiTools.getColor("white")
+        selectionTop = drawTop
+      else: drawFormat = curses.A_NORMAL
+      
+      popup.addstr(drawTop, 0, labelFormat % menuItem.getLabel(), drawFormat)
+      drawTop += 1
+    
+    popup.win.refresh()
+    
+    # shows the next submenu
+    _drawSubmenu(cursor, level + 1, top + selectionTop, left + menuWidth)
+  finally: cli.popups.finalize()
+  
diff --git a/src/cli/popups.py b/src/cli/popups.py
index 0e50aa1..b90f95d 100644
--- a/src/cli/popups.py
+++ b/src/cli/popups.py
@@ -8,7 +8,7 @@ import cli.controller
 
 from util import panel, uiTools
 
-def init(height = -1, width = -1, top = 0, left = 0):
+def init(height = -1, width = -1, top = 0, left = 0, belowStatic = True):
   """
   Preparation for displaying a popup. This creates a popup with a valid
   subwindow instance. If that's successful then the curses lock is acquired
@@ -17,14 +17,17 @@ def init(height = -1, width = -1, top = 0, left = 0):
   Otherwise this leaves curses unlocked and returns None.
   
   Arguments:
-    height - maximum height of the popup
-    width  - maximum width of the popup
-    top    - top position, relative to the sticky content
-    left   - left position from the screen
+    height      - maximum height of the popup
+    width       - maximum width of the popup
+    top         - top position, relative to the sticky content
+    left        - left position from the screen
+    belowStatic - positions popup below static content if true
   """
   
   control = cli.controller.getController()
-  stickyHeight = sum(stickyPanel.getHeight() for stickyPanel in control.getStickyPanels())
+  if belowStatic:
+    stickyHeight = sum(stickyPanel.getHeight() for stickyPanel in control.getStickyPanels())
+  else: stickyHeight = 0
   
   popup = panel.Panel(control.getScreen(), "popup", top + stickyHeight, left, height, width)
   popup.setVisible(True)
diff --git a/src/util/panel.py b/src/util/panel.py
index 93b44c2..6fae341 100644
--- a/src/util/panel.py
+++ b/src/util/panel.py
@@ -484,7 +484,7 @@ class Panel():
     # direction) from actual content to prevent crash when shrank
     if self.win and self.maxX > x and self.maxY > y:
       try:
-        self.win.addstr(y, x, msg[:self.maxX - x - 1], attr)
+        self.win.addstr(y, x, msg[:self.maxX - x], attr)
       except:
         # this might produce a _curses.error during edge cases, for instance
         # when resizing with visible popups





More information about the tor-commits mailing list