[tor-commits] [arm/master] Moving menus fromt he controller to panels

atagar at torproject.org atagar at torproject.org
Mon May 9 16:59:19 UTC 2011


commit a2c89f6f8261a892cc02aee5325fc0ffff7671d9
Author: Damian Johnson <atagar at torproject.org>
Date:   Mon May 9 09:56:27 2011 -0700

    Moving menus fromt he controller to panels
    
    Moving all of the option menus from a huge if/else block in the controller to
    the panels they're being used for. This included a couple minor changes to the
    panel api to better accommodate them.
---
 src/cli/configPanel.py           |   15 ++-
 src/cli/connections/connPanel.py |   39 ++++++-
 src/cli/controller.py            |  242 ++------------------------------------
 src/cli/graphing/graphPanel.py   |   36 ++++++-
 src/cli/logPanel.py              |   59 +++++++++-
 src/cli/popups.py                |   97 +++++++++++++++-
 src/cli/torrcPanel.py            |    6 +-
 src/util/panel.py                |   27 ++++
 8 files changed, 272 insertions(+), 249 deletions(-)

diff --git a/src/cli/configPanel.py b/src/cli/configPanel.py
index 6aaafc2..31a78ab 100644
--- a/src/cli/configPanel.py
+++ b/src/cli/configPanel.py
@@ -247,6 +247,7 @@ class ConfigPanel(panel.Panel):
   
   def handleKey(self, key):
     self.valsLock.acquire()
+    isKeystrokeConsumed = True
     if uiTools.isScrollKey(key):
       pageHeight = self.getPreferredSize()[0] - 1
       detailPanelHeight = self._config["features.config.selectionDetails.height"]
@@ -270,8 +271,10 @@ class ConfigPanel(panel.Panel):
         # converts labels back to enums
         resultEnums = [getFieldFromLabel(label) for label in results]
         self.setSortOrder(resultEnums)
+    else: isKeystrokeConsumed = False
     
     self.valsLock.release()
+    return isKeystrokeConsumed
   
   def getHelp(self):
     options = []
@@ -288,10 +291,6 @@ class ConfigPanel(panel.Panel):
   def draw(self, width, height):
     self.valsLock.acquire()
     
-    # draws the top label
-    configType = "Tor" if self.configType == State.TOR else "Arm"
-    hiddenMsg = "press 'a' to hide most options" if self.showAll else "press 'a' to show all options"
-    
     # panel with details for the current selection
     detailPanelHeight = self._config["features.config.selectionDetails.height"]
     isScrollbarVisible = False
@@ -311,8 +310,12 @@ class ConfigPanel(panel.Panel):
       
       self._drawSelectionPanel(cursorSelection, width, detailPanelHeight, isScrollbarVisible)
     
-    titleLabel = "%s Configuration (%s):" % (configType, hiddenMsg)
-    self.addstr(0, 0, titleLabel, curses.A_STANDOUT)
+    # draws the top label
+    if self.isTitleVisible():
+      configType = "Tor" if self.configType == State.TOR else "Arm"
+      hiddenMsg = "press 'a' to hide most options" if self.showAll else "press 'a' to show all options"
+      titleLabel = "%s Configuration (%s):" % (configType, hiddenMsg)
+      self.addstr(0, 0, titleLabel, curses.A_STANDOUT)
     
     # draws left-hand scroll bar if content's longer than the height
     scrollOffset = 1
diff --git a/src/cli/connections/connPanel.py b/src/cli/connections/connPanel.py
index 8ad41d5..4d89d5c 100644
--- a/src/cli/connections/connPanel.py
+++ b/src/cli/connections/connPanel.py
@@ -131,6 +131,8 @@ class ConnectionPanel(panel.Panel, threading.Thread):
       listingType - Listing instance for the primary information to be shown
     """
     
+    if self._listingType == listingType: return
+    
     self.valsLock.acquire()
     self._listingType = listingType
     
@@ -143,6 +145,7 @@ class ConnectionPanel(panel.Panel, threading.Thread):
   def handleKey(self, key):
     self.valsLock.acquire()
     
+    isKeystrokeConsumed = True
     if uiTools.isScrollKey(key):
       pageHeight = self.getPreferredSize()[0] - 1
       if self._showDetails: pageHeight -= (DETAILS_HEIGHT + 1)
@@ -159,8 +162,39 @@ class ConnectionPanel(panel.Panel, threading.Thread):
       optionColors = dict([(attr, entries.SORT_COLORS[attr]) for attr in options])
       results = cli.popups.showSortDialog(titleLabel, options, oldSelection, optionColors)
       if results: self.setSortOrder(results)
+    elif key == ord('u') or key == ord('U'):
+      # provides a menu to pick the connection resolver
+      title = "Resolver Util:"
+      options = ["auto"] + connections.Resolver.values()
+      connResolver = connections.getResolver("tor")
+      
+      currentOverwrite = connResolver.overwriteResolver
+      if currentOverwrite == None: oldSelection = 0
+      else: oldSelection = options.index(currentOverwrite)
+      
+      selection = cli.popups.showMenu(title, options, oldSelection)
+      
+      # applies new setting
+      if selection != -1:
+        selectedOption = options[selection] if selection != 0 else None
+        connResolver.overwriteResolver = selectedOption
+    elif key == ord('l') or key == ord('L'):
+      # provides a menu to pick the primary information we list connections by
+      title = "List By:"
+      options = entries.ListingType.values()
+      
+      # dropping the HOSTNAME listing type until we support displaying that content
+      options.remove(cli.connections.entries.ListingType.HOSTNAME)
+      
+      oldSelection = options.index(self._listingType)
+      selection = cli.popups.showMenu(title, options, oldSelection)
+      
+      # applies new setting
+      if selection != -1: self.setListingType(options[selection])
+    else: isKeystrokeConsumed = False
     
     self.valsLock.release()
+    return isKeystrokeConsumed
   
   def run(self):
     """
@@ -233,8 +267,9 @@ class ConnectionPanel(panel.Panel, threading.Thread):
         drawEntries[i].render(self, 1 + i, 2)
     
     # title label with connection counts
-    title = "Connection Details:" if self._showDetails else self._title
-    self.addstr(0, 0, title, curses.A_STANDOUT)
+    if self.isTitleVisible():
+      title = "Connection Details:" if self._showDetails else self._title
+      self.addstr(0, 0, title, curses.A_STANDOUT)
     
     scrollOffset = 1
     if isScrollbarVisible:
diff --git a/src/cli/controller.py b/src/cli/controller.py
index a195c15..6f4b70d 100644
--- a/src/cli/controller.py
+++ b/src/cli/controller.py
@@ -42,6 +42,7 @@ def refresh():
 # new panel params and accessors (this is part of the new controller apis)
 PANELS = {}
 STDSCR = None
+IS_PAUSED = False
 
 def getScreen():
   return STDSCR
@@ -83,7 +84,6 @@ def getPanels(page = None):
 
 CONFIRM_QUIT = True
 REFRESH_RATE = 5        # seconds between redrawing screen
-MAX_REGEX_FILTERS = 5   # maximum number of previous regex filters that'll be remembered
 
 # enums for message in control label
 CTL_HELP, CTL_PAUSED = range(2)
@@ -129,6 +129,9 @@ class ControlPanel(panel.Panel):
     self.msgText = msgText
     self.msgAttr = msgAttr
   
+  def revertMsg(self):
+    self.setMsg(CTL_PAUSED if IS_PAUSED else CTL_HELP)
+  
   def draw(self, width, height):
     msgText = self.msgText
     msgAttr = self.msgAttr
@@ -275,57 +278,6 @@ def setPauseState(panels, monitorIsPaused, currentPage, overwrite=False):
   
   for key in allPanels: panels[key].setPaused(overwrite or monitorIsPaused or (key not in PAGES[currentPage] and key not in PAGE_S))
 
-def showMenu(stdscr, popup, title, options, initialSelection):
-  """
-  Provides menu with options laid out in a single column. User can cancel
-  selection with the escape key, in which case this proives -1. Otherwise this
-  returns the index of the selection. If initialSelection is -1 then the first
-  option is used and the carrot indicating past selection is ommitted.
-  """
-  
-  selection = initialSelection if initialSelection != -1 else 0
-  
-  if popup.win:
-    if not panel.CURSES_LOCK.acquire(False): return -1
-    try:
-      # TODO: should pause interface (to avoid event accumilation)
-      curses.cbreak() # wait indefinitely for key presses (no timeout)
-      
-      # uses smaller dimentions more fitting for small content
-      popup.height = len(options) + 2
-      
-      newWidth = max([len(label) for label in options]) + 9
-      popup.recreate(stdscr, newWidth)
-      
-      key = 0
-      while not uiTools.isSelectionKey(key):
-        popup.clear()
-        popup.win.box()
-        popup.addstr(0, 0, title, curses.A_STANDOUT)
-        
-        for i in range(len(options)):
-          label = options[i]
-          format = curses.A_STANDOUT if i == selection else curses.A_NORMAL
-          tab = "> " if i == initialSelection else "  "
-          popup.addstr(i + 1, 2, tab)
-          popup.addstr(i + 1, 4, " %s " % label, format)
-        
-        popup.refresh()
-        key = stdscr.getch()
-        if key == curses.KEY_UP: selection = max(0, selection - 1)
-        elif key == curses.KEY_DOWN: selection = min(len(options) - 1, selection + 1)
-        elif key == 27: selection, key = -1, curses.KEY_ENTER # esc - cancel
-      
-      # reverts popup dimensions and conn panel label
-      popup.height = 9
-      popup.recreate(stdscr, 80)
-      
-      curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
-    finally:
-      panel.CURSES_LOCK.release()
-  
-  return selection
-
 def setEventListening(selectedEvents, isBlindMode):
   # creates a local copy, note that a suspected python bug causes *very*
   # puzzling results otherwise when trying to discard entries (silently
@@ -381,7 +333,7 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
     otherwise unrecognized events)
   """
   
-  global PANELS, STDSCR, REFRESH_FLAG, PAGE
+  global PANELS, STDSCR, REFRESH_FLAG, PAGE, IS_PAUSED
   STDSCR = stdscr
   
   # loads config for various interface components
@@ -592,7 +544,6 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
   isPaused = False          # if true updates are frozen
   overrideKey = None        # immediately runs with this input rather than waiting for the user if set
   page = 0
-  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
@@ -846,6 +797,7 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
       panel.CURSES_LOCK.acquire()
       try:
         isPaused = not isPaused
+        IS_PAUSED = isPaused
         setPauseState(panels, isPaused, page)
         panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
       finally:
@@ -883,69 +835,6 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
         panel.CURSES_LOCK.release()
     elif key == ord('h') or key == ord('H'):
       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()]
-      options = ["None"]
-      
-      # appends stats labels with first letters of each word capitalized
-      initialSelection, i = -1, 1
-      if not panels["graph"].currentDisplay: initialSelection = 0
-      graphLabels = panels["graph"].stats.keys()
-      graphLabels.sort()
-      for label in graphLabels:
-        if label == panels["graph"].currentDisplay: initialSelection = i
-        words = label.split()
-        options.append(" ".join(word[0].upper() + word[1:] for word in words))
-        i += 1
-      
-      # hides top label of the graph panel and pauses panels
-      if panels["graph"].currentDisplay:
-        panels["graph"].showLabel = False
-        panels["graph"].redraw(True)
-      setPauseState(panels, isPaused, page, True)
-      
-      selection = showMenu(stdscr, panels["popup"], "Graphed Stats:", options, initialSelection)
-      
-      # reverts changes made for popup
-      panels["graph"].showLabel = True
-      setPauseState(panels, isPaused, page)
-      
-      # applies new setting
-      if selection != -1 and selection != initialSelection:
-        if selection == 0: panels["graph"].setStats(None)
-        else: panels["graph"].setStats(options[selection].lower())
-      
-      selectiveRefresh(panels, page)
-      
-      # TODO: this shouldn't be necessary with the above refresh, but doesn't seem responsive otherwise...
-      panels["graph"].redraw(True)
-    elif page == 0 and (key == ord('i') or key == ord('I')):
-      # provides menu to pick graph panel update interval
-      options = [label for (label, intervalTime) in graphing.graphPanel.UPDATE_INTERVALS]
-      
-      initialSelection = panels["graph"].updateInterval
-      
-      #initialSelection = -1
-      #for i in range(len(options)):
-      #  if options[i] == panels["graph"].updateInterval: initialSelection = i
-      
-      # hides top label of the graph panel and pauses panels
-      if panels["graph"].currentDisplay:
-        panels["graph"].showLabel = False
-        panels["graph"].redraw(True)
-      setPauseState(panels, isPaused, page, True)
-      
-      selection = showMenu(stdscr, panels["popup"], "Update Interval:", options, initialSelection)
-      
-      # reverts changes made for popup
-      panels["graph"].showLabel = True
-      setPauseState(panels, isPaused, page)
-      
-      # applies new setting
-      if selection != -1: panels["graph"].updateInterval = selection
-      
-      selectiveRefresh(panels, page)
     elif page == 0 and (key == ord('b') or key == ord('B')):
       # uses the next boundary type for graph
       panels["graph"].bounds = graphing.graphPanel.Bounds.next(panels["graph"].bounds)
@@ -1031,64 +920,6 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
         panel.CURSES_LOCK.release()
       
       panels["graph"].redraw(True)
-    elif page == 0 and (key == ord('f') or key == ord('F')):
-      # provides menu to pick previous regular expression filters or to add a new one
-      # for syntax see: http://docs.python.org/library/re.html#regular-expression-syntax
-      options = ["None"] + regexFilters + ["New..."]
-      initialSelection = 0 if not panels["log"].regexFilter else 1
-      
-      # hides top label of the graph panel and pauses panels
-      if panels["graph"].currentDisplay:
-        panels["graph"].showLabel = False
-        panels["graph"].redraw(True)
-      setPauseState(panels, isPaused, page, True)
-      
-      selection = showMenu(stdscr, panels["popup"], "Log Filter:", options, initialSelection)
-      
-      # applies new setting
-      if selection == 0:
-        panels["log"].setFilter(None)
-      elif selection == len(options) - 1:
-        # selected 'New...' option - prompt user to input regular expression
-        panel.CURSES_LOCK.acquire()
-        try:
-          # provides prompt
-          panels["control"].setMsg("Regular expression: ")
-          panels["control"].redraw(True)
-          
-          # gets user input (this blocks monitor updates)
-          regexInput = panels["control"].getstr(0, 20)
-          
-          if regexInput:
-            try:
-              panels["log"].setFilter(re.compile(regexInput))
-              if regexInput in regexFilters: regexFilters.remove(regexInput)
-              regexFilters = [regexInput] + regexFilters
-            except re.error, exc:
-              panels["control"].setMsg("Unable to compile expression: %s" % str(exc), curses.A_STANDOUT)
-              panels["control"].redraw(True)
-              time.sleep(2)
-          panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
-        finally:
-          panel.CURSES_LOCK.release()
-      elif selection != -1:
-        try:
-          panels["log"].setFilter(re.compile(regexFilters[selection - 1]))
-          
-          # move selection to top
-          regexFilters = [regexFilters[selection - 1]] + regexFilters
-          del regexFilters[selection]
-        except re.error, exc:
-          # shouldn't happen since we've already checked validity
-          log.log(log.WARN, "Invalid regular expression ('%s': %s) - removing from listing" % (regexFilters[selection - 1], str(exc)))
-          del regexFilters[selection - 1]
-      
-      if len(regexFilters) > MAX_REGEX_FILTERS: del regexFilters[MAX_REGEX_FILTERS:]
-      
-      # reverts changes made for popup
-      panels["graph"].showLabel = True
-      setPauseState(panels, isPaused, page)
-      panels["graph"].redraw(True)
     elif page == 0 and key in (ord('n'), ord('N'), ord('m'), ord('M')):
       # Unfortunately modifier keys don't work with the up/down arrows (sending
       # multiple keycodes. The only exception to this is shift + left/right,
@@ -1124,30 +955,6 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
         setPauseState(panels, isPaused, page)
       finally:
         panel.CURSES_LOCK.release()
-    elif page == 1 and (key == ord('u') or key == ord('U')):
-      # provides menu to pick identification resolving utility
-      options = ["auto"] + connections.Resolver.values()
-      
-      currentOverwrite = connections.getResolver("tor").overwriteResolver # enums correspond to indices
-      if currentOverwrite == None: initialSelection = 0
-      else: initialSelection = options.index(currentOverwrite)
-      
-      # hides top label of conn panel and pauses panels
-      panelTitle = panels["conn"]._title
-      panels["conn"]._title = ""
-      panels["conn"].redraw(True)
-      setPauseState(panels, isPaused, page, True)
-      
-      selection = showMenu(stdscr, panels["popup"], "Resolver Util:", options, initialSelection)
-      selectedOption = options[selection] if selection != 0 else None
-      
-      # reverts changes made for popup
-      panels["conn"]._title = panelTitle
-      setPauseState(panels, isPaused, page)
-      
-      # applies new setting
-      if selection != -1 and selectedOption != connections.getResolver("tor").overwriteResolver:
-        connections.getResolver("tor").overwriteResolver = selectedOption
     elif page == 1 and key in (ord('d'), ord('D')):
       # presents popup for raw consensus data
       panel.CURSES_LOCK.acquire()
@@ -1165,31 +972,6 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
         curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
       finally:
         panel.CURSES_LOCK.release()
-    elif page == 1 and (key == ord('l') or key == ord('L')):
-      # provides a menu to pick the primary information we list connections by
-      options = cli.connections.entries.ListingType.values()
-      
-      # dropping the HOSTNAME listing type until we support displaying that content
-      options.remove(cli.connections.entries.ListingType.HOSTNAME)
-      
-      initialSelection = options.index(panels["conn"]._listingType)
-      
-      # hides top label of connection panel and pauses the display
-      panelTitle = panels["conn"]._title
-      panels["conn"]._title = ""
-      panels["conn"].redraw(True)
-      setPauseState(panels, isPaused, page, True)
-      
-      selection = showMenu(stdscr, panels["popup"], "List By:", options, initialSelection)
-      
-      # reverts changes made for popup
-      panels["conn"]._title = panelTitle
-      setPauseState(panels, isPaused, page)
-      
-      # applies new setting
-      if selection != -1 and options[selection] != panels["conn"]._listingType:
-        panels["conn"].setListingType(options[selection])
-        panels["conn"].redraw(True)
     elif page == 2 and (key == ord('w') or key == ord('W')):
       # display a popup for saving the current configuration
       panel.CURSES_LOCK.acquire()
@@ -1400,14 +1182,10 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
       time.sleep(1)
       
       panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
-    elif page == 0:
-      panels["log"].handleKey(key)
-    elif page == 1:
-      panels["conn"].handleKey(key)
-    elif page == 2:
-      panels["config"].handleKey(key)
-    elif page == 3:
-      panels["torrc"].handleKey(key)
+    else:
+      for pagePanel in getPanels(page + 1):
+        isKeystrokeConsumed = pagePanel.handleKey(key)
+        if isKeystrokeConsumed: break
     
     if REFRESH_FLAG:
       REFRESH_FLAG = False
diff --git a/src/cli/graphing/graphPanel.py b/src/cli/graphing/graphPanel.py
index d8808b3..0c52f6f 100644
--- a/src/cli/graphing/graphPanel.py
+++ b/src/cli/graphing/graphPanel.py
@@ -20,6 +20,8 @@ import copy
 import curses
 from TorCtl import TorCtl
 
+import cli.popups
+
 from util import enum, panel, uiTools
 
 # time intervals at which graphs can be updated
@@ -229,7 +231,6 @@ class GraphPanel(panel.Panel):
     self.graphHeight = CONFIG["features.graph.height"]
     self.currentDisplay = None    # label of the stats currently being displayed
     self.stats = {}               # available stats (mappings of label -> instance)
-    self.showLabel = True         # shows top label if true, hides otherwise
     self.setPauseAttr("stats")
   
   def getHeight(self):
@@ -253,6 +254,37 @@ class GraphPanel(panel.Panel):
     
     self.graphHeight = max(MIN_GRAPH_HEIGHT, newGraphHeight)
   
+  def handleKey(self, key):
+    isKeystrokeConsumed = True
+    if key == ord('s') or key == ord('S'):
+      # provides a menu to pick the graphed stats
+      availableStats = self.stats.keys()
+      availableStats.sort()
+      
+      # uses sorted, camel cased labels for the options
+      options = ["None"]
+      for label in availableStats:
+        words = label.split()
+        options.append(" ".join(word[0].upper() + word[1:] for word in words))
+      
+      if self.currentDisplay:
+        initialSelection = availableStats.index(self.currentDisplay) + 1
+      else: initialSelection = 0
+      
+      selection = cli.popups.showMenu("Graphed Stats:", options, initialSelection)
+      
+      # applies new setting
+      if selection == 0: self.setStats(None)
+      elif selection != -1: self.setStats(options[selection].lower())
+    elif key == ord('i') or key == ord('I'):
+      # provides menu to pick graph panel update interval
+      options = [label for (label, _) in UPDATE_INTERVALS]
+      selection = cli.popups.showMenu("Update Interval:", options, self.updateInterval)
+      if selection != -1: self.updateInterval = selection
+    else: isKeystrokeConsumed = False
+    
+    return isKeystrokeConsumed
+  
   def getHelp(self):
     if self.currentDisplay: graphedStats = self.currentDisplay
     else: graphedStats = "none"
@@ -275,7 +307,7 @@ class GraphPanel(panel.Panel):
       primaryColor = uiTools.getColor(param.getColor(True))
       secondaryColor = uiTools.getColor(param.getColor(False))
       
-      if self.showLabel: self.addstr(0, 0, param.getTitle(width), curses.A_STANDOUT)
+      if self.isTitleVisible(): self.addstr(0, 0, param.getTitle(width), curses.A_STANDOUT)
       
       # top labels
       left, right = param.getHeaderLabel(width / 2, True), param.getHeaderLabel(width / 2, False)
diff --git a/src/cli/logPanel.py b/src/cli/logPanel.py
index b12715e..d34b640 100644
--- a/src/cli/logPanel.py
+++ b/src/cli/logPanel.py
@@ -4,13 +4,15 @@ for. This provides prepopulation from the log file and supports filtering by
 regular expressions.
 """
 
-import time
+import re
 import os
+import time
 import curses
 import threading
 
 from TorCtl import TorCtl
 
+import popups
 from version import VERSION
 from util import conf, log, panel, sysTools, torTools, uiTools
 
@@ -75,6 +77,9 @@ CACHED_DUPLICATES_RESULT = None
 # duration we'll wait for the deduplication function before giving up (in ms)
 DEDUPLICATION_TIMEOUT = 100
 
+# maximum number of regex filters we'll remember
+MAX_REGEX_FILTERS = 5
+
 def daysSince(timestamp=None):
   """
   Provides the number of days since the epoch converted to local time (rounded
@@ -551,6 +556,7 @@ class LogPanel(panel.Panel, threading.Thread):
     self.msgLog = []                    # log entries, sorted by the timestamp
     self.loggedEvents = loggedEvents    # events we're listening to
     self.regexFilter = None             # filter for presented log events (no filtering if None)
+    self.filterOptions = []             # filters the user has input
     self.lastContentHeight = 0          # height of the rendered content when last drawn
     self.logFile = None                 # file log messages are saved to (skipped if None)
     self.scroll = 0
@@ -746,6 +752,7 @@ class LogPanel(panel.Panel, threading.Thread):
       raise exc
   
   def handleKey(self, key):
+    isKeystrokeConsumed = True
     if uiTools.isScrollKey(key):
       pageHeight = self.getPreferredSize()[0] - 1
       newScroll = uiTools.getScrollPosition(key, self.scroll, pageHeight, self.lastContentHeight)
@@ -760,6 +767,53 @@ class LogPanel(panel.Panel, threading.Thread):
       self.showDuplicates = not self.showDuplicates
       self.redraw(True)
       self.valsLock.release()
+    elif key == ord('f') or key == ord('F'):
+      # Provides menu to pick regular expression filters or adding new ones:
+      # for syntax see: http://docs.python.org/library/re.html#regular-expression-syntax
+      options = ["None"] + self.filterOptions + ["New..."]
+      oldSelection = 0 if not self.regexFilter else 1
+      
+      # does all activity under a curses lock to prevent redraws when adding
+      # new filters
+      panel.CURSES_LOCK.acquire()
+      try:
+        selection = popups.showMenu("Log Filter:", options, oldSelection)
+        
+        # applies new setting
+        if selection == 0:
+          self.setFilter(None)
+        elif selection == len(options) - 1:
+          # selected 'New...' option - prompt user to input regular expression
+          regexInput = popups.inputPrompt("Regular expression: ")
+          
+          if regexInput:
+            try:
+              self.setFilter(re.compile(regexInput))
+              if regexInput in self.filterOptions: self.filterOptions.remove(regexInput)
+              self.filterOptions.insert(0, regexInput)
+            except re.error, exc:
+              popups.showMsg("Unable to compile expression: %s" % exc, 2)
+        elif selection != -1:
+          selectedOption = self.filterOptions[selection - 1]
+          
+          try:
+            self.setFilter(re.compile(selectedOption))
+            
+            # move selection to top
+            self.filterOptions.remove(selectedOption)
+            self.filterOptions.insert(0, selectedOption)
+          except re.error, exc:
+            # shouldn't happen since we've already checked validity
+            msg = "Invalid regular expression ('%s': %s) - removing from listing" % (selectedOption, exc)
+            log.log(log.WARN, msg)
+            self.filterOptions.remove(selectedOption)
+      finally:
+        panel.CURSES_LOCK.release()
+      
+      if len(self.filterOptions) > MAX_REGEX_FILTERS: del self.filterOptions[MAX_REGEX_FILTERS:]
+    else: isKeystrokeConsumed = False
+    
+    return isKeystrokeConsumed
   
   def getHelp(self):
     options = []
@@ -784,7 +838,8 @@ class LogPanel(panel.Panel, threading.Thread):
     self._lastLoggedEvents, self._lastUpdate = list(currentLog), time.time()
     
     # draws the top label
-    self.addstr(0, 0, self._getTitle(width), curses.A_STANDOUT)
+    if self.isTitleVisible():
+      self.addstr(0, 0, self._getTitle(width), curses.A_STANDOUT)
     
     # restricts scroll location to valid bounds
     self.scroll = max(0, min(self.scroll, self.lastContentHeight - height + 1))
diff --git a/src/cli/popups.py b/src/cli/popups.py
index d5cf76c..81b4589 100644
--- a/src/cli/popups.py
+++ b/src/cli/popups.py
@@ -44,6 +44,46 @@ def finalize():
   controller.refresh()
   panel.CURSES_LOCK.release()
 
+def inputPrompt(msg):
+  """
+  Prompts the user to enter a string on the control line (which usually
+  displays the page number and basic controls).
+  
+  Arguments:
+    msg - message to prompt the user for input with
+  """
+  
+  panel.CURSES_LOCK.acquire()
+  controlPanel = controller.getPanel("control")
+  controlPanel.setMsg(msg)
+  controlPanel.redraw(True)
+  userInput = controlPanel.getstr(0, len(msg))
+  controlPanel.revertMsg()
+  panel.CURSES_LOCK.release()
+  return userInput
+
+def showMsg(msg, maxWait, attr = curses.A_STANDOUT):
+  """
+  Displays a single line message on the control line for a set time. Pressing
+  any key will end the message.
+  
+  Arguments:
+    msg     - message to be displayed to the user
+    maxWait - time to show the message
+    attr    - attributes with which to draw the message
+  """
+  
+  panel.CURSES_LOCK.acquire()
+  controlPanel = controller.getPanel("control")
+  controlPanel.setMsg(msg, attr)
+  controlPanel.redraw(True)
+  
+  curses.halfdelay(maxWait * 10)
+  controller.getScreen().getch()
+  controlPanel.revertMsg()
+  curses.halfdelay(controller.REFRESH_RATE * 10)
+  panel.CURSES_LOCK.release()
+
 def showHelpPopup():
   """
   Presents a popup with instructions for the current page's hotkeys. This
@@ -108,7 +148,7 @@ def showHelpPopup():
     return exitKey
   else: return None
 
-def showSortDialog(titleLabel, options, oldSelection, optionColors):
+def showSortDialog(title, options, oldSelection, optionColors):
   """
   Displays a sorting dialog of the form:
   
@@ -122,7 +162,7 @@ def showSortDialog(titleLabel, options, oldSelection, optionColors):
   then this returns None. Otherwise, the new ordering is provided.
   
   Arguments:
-    titleLabel   - title displayed for the popup window
+    title   - title displayed for the popup window
     options      - ordered listing of option labels
     oldSelection - current ordering
     optionColors - mappings of options to their color
@@ -142,7 +182,7 @@ def showSortDialog(titleLabel, options, oldSelection, optionColors):
     while len(newSelections) < len(oldSelection):
       popup.win.erase()
       popup.win.box()
-      popup.addstr(0, 0, titleLabel, curses.A_STANDOUT)
+      popup.addstr(0, 0, title, curses.A_STANDOUT)
       
       _drawSortSelection(popup, 1, 2, "Current Order: ", oldSelection, optionColors)
       _drawSortSelection(popup, 2, 2, "New Order: ", newSelections, optionColors)
@@ -214,3 +254,54 @@ def _drawSortSelection(popup, y, x, prefix, options, optionColors):
       popup.addstr(y, x, ", ", curses.A_BOLD)
       x += 2
 
+def showMenu(title, options, oldSelection):
+  """
+  Provides menu with options laid out in a single column. User can cancel
+  selection with the escape key, in which case this proives -1. Otherwise this
+  returns the index of the selection.
+  
+  Arguments:
+    title        - title displayed for the popup window
+    options      - ordered listing of options to display
+    oldSelection - index of the initially selected option (uses the first
+                   selection without a carrot if -1)
+  """
+  
+  maxWidth = max([len(label) for label in options]) + 9
+  popup, width, height = init(len(options) + 2, maxWidth)
+  if not popup: return
+  key, selection = 0, oldSelection if oldSelection != -1 else 0
+  
+  try:
+    # hides the title of the first panel on the page
+    topPanel = controller.getPanels(controller.getPage())[0]
+    topPanel.setTitleVisible(False)
+    topPanel.redraw(True)
+    
+    curses.cbreak()   # wait indefinitely for key presses (no timeout)
+    
+    while not uiTools.isSelectionKey(key):
+      popup.win.erase()
+      popup.win.box()
+      popup.addstr(0, 0, title, curses.A_STANDOUT)
+      
+      for i in range(len(options)):
+        label = options[i]
+        format = curses.A_STANDOUT if i == selection else curses.A_NORMAL
+        tab = "> " if i == oldSelection else "  "
+        popup.addstr(i + 1, 2, tab)
+        popup.addstr(i + 1, 4, " %s " % label, format)
+      
+      popup.win.refresh()
+      
+      key = controller.getScreen().getch()
+      if key == curses.KEY_UP: selection = max(0, selection - 1)
+      elif key == curses.KEY_DOWN: selection = min(len(options) - 1, selection + 1)
+      elif key == 27: selection, key = -1, curses.KEY_ENTER # esc - cancel
+      
+    topPanel.setTitleVisible(True)
+    curses.halfdelay(controller.REFRESH_RATE * 10) # reset normal pausing behavior
+  finally: finalize()
+  
+  return selection
+
diff --git a/src/cli/torrcPanel.py b/src/cli/torrcPanel.py
index 6d7156d..5ca839f 100644
--- a/src/cli/torrcPanel.py
+++ b/src/cli/torrcPanel.py
@@ -31,7 +31,6 @@ class TorrcPanel(panel.Panel):
     self.valsLock = threading.RLock()
     self.configType = configType
     self.scroll = 0
-    self.showLabel = True       # shows top label (hides otherwise)
     self.showLineNum = True     # shows left aligned line numbers
     self.stripComments = False  # drops comments and extra whitespace
     
@@ -42,6 +41,7 @@ class TorrcPanel(panel.Panel):
   
   def handleKey(self, key):
     self.valsLock.acquire()
+    isKeystrokeConsumed = True
     if uiTools.isScrollKey(key):
       pageHeight = self.getPreferredSize()[0] - 1
       newScroll = uiTools.getScrollPosition(key, self.scroll, pageHeight, self._lastContentHeight)
@@ -57,8 +57,10 @@ class TorrcPanel(panel.Panel):
       self.stripComments = not self.stripComments
       self._lastContentHeightArgs = None
       self.redraw(True)
+    else: isKeystrokeConsumed = False
     
     self.valsLock.release()
+    return isKeystrokeConsumed
   
   def getHelp(self):
     options = []
@@ -120,7 +122,7 @@ class TorrcPanel(panel.Panel):
     displayLine = -self.scroll + 1 # line we're drawing on
     
     # draws the top label
-    if self.showLabel:
+    if self.isTitleVisible():
       sourceLabel = "Tor" if self.configType == Config.TORRC else "Arm"
       locationLabel = " (%s)" % confLocation if confLocation else ""
       self.addstr(0, 0, "%s Configuration File%s:" % (sourceLabel, locationLabel), curses.A_STANDOUT)
diff --git a/src/util/panel.py b/src/util/panel.py
index 7387833..06c8649 100644
--- a/src/util/panel.py
+++ b/src/util/panel.py
@@ -61,6 +61,7 @@ class Panel():
     self.panelName = name
     self.parent = parent
     self.visible = False
+    self.titleVisible = True
     
     # Attributes for pausing. The pauseAttr contains variables our getAttr
     # method is tracking, and the pause buffer has copies of the values from
@@ -93,6 +94,21 @@ class Panel():
     
     return self.panelName
   
+  def isTitleVisible(self):
+    """
+    True if the title is configured to be visible, False otherwise.
+    """
+    
+    return self.titleVisible
+  
+  def setTitleVisible(self, isVisible):
+    """
+    Configures the panel's title to be visible or not when it's next redrawn.
+    This is not guarenteed to be respected (not all panels have a title).
+    """
+    
+    self.titleVisible = isVisible
+  
   def getParent(self):
     """
     Provides the parent used to create subwindows.
@@ -290,6 +306,17 @@ class Panel():
     if setWidth != -1: newWidth = min(newWidth, setWidth)
     return (newHeight, newWidth)
   
+  def handleKey(self, key):
+    """
+    Handler for user input. This returns true if the key press was consumed,
+    false otherwise.
+    
+    Arguments:
+      key - keycode for the key pressed
+    """
+    
+    return False
+  
   def getHelp(self):
     """
     Provides help information for the controls this page provides. This is a



More information about the tor-commits mailing list