[tor-commits] [arm/release] Alternative menu implementation
atagar at torproject.org
atagar at torproject.org
Sun Jul 17 06:08:22 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