home *** CD-ROM | disk | FTP | other *** search
/ Clickx 115 / Clickx 115.iso / software / tools / windows / tails-i386-0.16.iso / live / filesystem.squashfs / usr / share / arm / util / panel.py < prev    next >
Encoding:
Python Source  |  2012-05-18  |  24.0 KB  |  734 lines

  1. """
  2. Wrapper for safely working with curses subwindows.
  3. """
  4.  
  5. import copy
  6. import time
  7. import curses
  8. import curses.ascii
  9. import curses.textpad
  10. from threading import RLock
  11.  
  12. from util import log, textInput, uiTools
  13.  
  14. # global ui lock governing all panel instances (curses isn't thread save and 
  15. # concurrency bugs produce especially sinister glitches)
  16. CURSES_LOCK = RLock()
  17.  
  18. # tags used by addfstr - this maps to functor/argument combinations since the
  19. # actual values (in the case of color attributes) might not yet be initialized
  20. def _noOp(arg): return arg
  21. FORMAT_TAGS = {"<b>": (_noOp, curses.A_BOLD),
  22.                "<u>": (_noOp, curses.A_UNDERLINE),
  23.                "<h>": (_noOp, curses.A_STANDOUT)}
  24. for colorLabel in uiTools.COLOR_LIST: FORMAT_TAGS["<%s>" % colorLabel] = (uiTools.getColor, colorLabel)
  25.  
  26. CONFIG = {"log.panelRecreated": log.DEBUG}
  27.  
  28. # prevents curses redraws if set
  29. HALT_ACTIVITY = False
  30.  
  31. def loadConfig(config):
  32.   config.update(CONFIG)
  33.  
  34. class Panel():
  35.   """
  36.   Wrapper for curses subwindows. This hides most of the ugliness in common
  37.   curses operations including:
  38.     - locking when concurrently drawing to multiple windows
  39.     - gracefully handle terminal resizing
  40.     - clip text that falls outside the panel
  41.     - convenience methods for word wrap, in-line formatting, etc
  42.   
  43.   This uses a design akin to Swing where panel instances provide their display
  44.   implementation by overwriting the draw() method, and are redrawn with
  45.   redraw().
  46.   """
  47.   
  48.   def __init__(self, parent, name, top, left=0, height=-1, width=-1):
  49.     """
  50.     Creates a durable wrapper for a curses subwindow in the given parent.
  51.     
  52.     Arguments:
  53.       parent - parent curses window
  54.       name   - identifier for the panel
  55.       top    - positioning of top within parent
  56.       left   - positioning of the left edge within the parent
  57.       height - maximum height of panel (uses all available space if -1)
  58.       width  - maximum width of panel (uses all available space if -1)
  59.     """
  60.     
  61.     # The not-so-pythonic getters for these parameters are because some
  62.     # implementations aren't entirely deterministic (for instance panels
  63.     # might chose their height based on its parent's current width).
  64.     
  65.     self.panelName = name
  66.     self.parent = parent
  67.     self.visible = False
  68.     self.titleVisible = True
  69.     
  70.     # Attributes for pausing. The pauseAttr contains variables our getAttr
  71.     # method is tracking, and the pause buffer has copies of the values from
  72.     # when we were last unpaused (unused unless we're paused).
  73.     
  74.     self.paused = False
  75.     self.pauseAttr = []
  76.     self.pauseBuffer = {}
  77.     self.pauseTime = -1
  78.     
  79.     self.top = top
  80.     self.left = left
  81.     self.height = height
  82.     self.width = width
  83.     
  84.     # The panel's subwindow instance. This is made available to implementors
  85.     # via their draw method and shouldn't be accessed directly.
  86.     # 
  87.     # This is None if either the subwindow failed to be created or needs to be
  88.     # remade before it's used. The later could be for a couple reasons:
  89.     # - The subwindow was never initialized.
  90.     # - Any of the parameters used for subwindow initialization have changed.
  91.     self.win = None
  92.     
  93.     self.maxY, self.maxX = -1, -1 # subwindow dimensions when last redrawn
  94.   
  95.   def getName(self):
  96.     """
  97.     Provides panel's identifier.
  98.     """
  99.     
  100.     return self.panelName
  101.   
  102.   def isTitleVisible(self):
  103.     """
  104.     True if the title is configured to be visible, False otherwise.
  105.     """
  106.     
  107.     return self.titleVisible
  108.   
  109.   def setTitleVisible(self, isVisible):
  110.     """
  111.     Configures the panel's title to be visible or not when it's next redrawn.
  112.     This is not guarenteed to be respected (not all panels have a title).
  113.     """
  114.     
  115.     self.titleVisible = isVisible
  116.   
  117.   def getParent(self):
  118.     """
  119.     Provides the parent used to create subwindows.
  120.     """
  121.     
  122.     return self.parent
  123.   
  124.   def setParent(self, parent):
  125.     """
  126.     Changes the parent used to create subwindows.
  127.     
  128.     Arguments:
  129.       parent - parent curses window
  130.     """
  131.     
  132.     if self.parent != parent:
  133.       self.parent = parent
  134.       self.win = None
  135.   
  136.   def isVisible(self):
  137.     """
  138.     Provides if the panel's configured to be visible or not.
  139.     """
  140.     
  141.     return self.visible
  142.   
  143.   def setVisible(self, isVisible):
  144.     """
  145.     Toggles if the panel is visible or not.
  146.     
  147.     Arguments:
  148.       isVisible - panel is redrawn when requested if true, skipped otherwise
  149.     """
  150.     
  151.     self.visible = isVisible
  152.   
  153.   def isPaused(self):
  154.     """
  155.     Provides if the panel's configured to be paused or not.
  156.     """
  157.     
  158.     return self.paused
  159.   
  160.   def setPauseAttr(self, attr):
  161.     """
  162.     Configures the panel to track the given attribute so that getAttr provides
  163.     the value when it was last unpaused (or its current value if we're
  164.     currently unpaused). For instance...
  165.     
  166.     > self.setPauseAttr("myVar")
  167.     > self.myVar = 5
  168.     > self.myVar = 6 # self.getAttr("myVar") -> 6
  169.     > self.setPaused(True)
  170.     > self.myVar = 7 # self.getAttr("myVar") -> 6
  171.     > self.setPaused(False)
  172.     > self.myVar = 7 # self.getAttr("myVar") -> 7
  173.     
  174.     Arguments:
  175.       attr - parameter to be tracked for getAttr
  176.     """
  177.     
  178.     self.pauseAttr.append(attr)
  179.     self.pauseBuffer[attr] = self.copyAttr(attr)
  180.   
  181.   def getAttr(self, attr):
  182.     """
  183.     Provides the value of the given attribute when we were last unpaused. If
  184.     we're currently unpaused then this is the current value. If untracked this
  185.     returns None.
  186.     
  187.     Arguments:
  188.       attr - local variable to be returned
  189.     """
  190.     
  191.     if not attr in self.pauseAttr: return None
  192.     elif self.paused: return self.pauseBuffer[attr]
  193.     else: return self.__dict__.get(attr)
  194.   
  195.   def copyAttr(self, attr):
  196.     """
  197.     Provides a duplicate of the given configuration value, suitable for the
  198.     pause buffer.
  199.     
  200.     Arguments:
  201.       attr - parameter to be provided back
  202.     """
  203.     
  204.     currentValue = self.__dict__.get(attr)
  205.     return copy.copy(currentValue)
  206.   
  207.   def setPaused(self, isPause, suppressRedraw = False):
  208.     """
  209.     Toggles if the panel is paused or not. This causes the panel to be redrawn
  210.     when toggling is pause state unless told to do otherwise. This is
  211.     important when pausing since otherwise the panel's display could change
  212.     when redrawn for other reasons.
  213.     
  214.     This returns True if the panel's pause state was changed, False otherwise.
  215.     
  216.     Arguments:
  217.       isPause        - freezes the state of the pause attributes if true, makes
  218.                        them editable otherwise
  219.       suppressRedraw - if true then this will never redraw the panel
  220.     """
  221.     
  222.     if isPause != self.paused:
  223.       if isPause: self.pauseTime = time.time()
  224.       self.paused = isPause
  225.       
  226.       if isPause:
  227.         # copies tracked attributes so we know what they were before pausing
  228.         for attr in self.pauseAttr:
  229.           self.pauseBuffer[attr] = self.copyAttr(attr)
  230.       
  231.       if not suppressRedraw: self.redraw(True)
  232.       return True
  233.     else: return False
  234.   
  235.   def getPauseTime(self):
  236.     """
  237.     Provides the time that we were last paused, returning -1 if we've never
  238.     been paused.
  239.     """
  240.     
  241.     return self.pauseTime
  242.   
  243.   def getTop(self):
  244.     """
  245.     Provides the position subwindows are placed at within its parent.
  246.     """
  247.     
  248.     return self.top
  249.   
  250.   def setTop(self, top):
  251.     """
  252.     Changes the position where subwindows are placed within its parent.
  253.     
  254.     Arguments:
  255.       top - positioning of top within parent
  256.     """
  257.     
  258.     if self.top != top:
  259.       self.top = top
  260.       self.win = None
  261.   
  262.   def getLeft(self):
  263.     """
  264.     Provides the left position where this subwindow is placed within its
  265.     parent.
  266.     """
  267.     
  268.     return self.left
  269.   
  270.   def setLeft(self, left):
  271.     """
  272.     Changes the left position where this subwindow is placed within its parent.
  273.     
  274.     Arguments:
  275.       left - positioning of top within parent
  276.     """
  277.     
  278.     if self.left != left:
  279.       self.left = left
  280.       self.win = None
  281.   
  282.   def getHeight(self):
  283.     """
  284.     Provides the height used for subwindows (-1 if it isn't limited).
  285.     """
  286.     
  287.     return self.height
  288.   
  289.   def setHeight(self, height):
  290.     """
  291.     Changes the height used for subwindows. This uses all available space if -1.
  292.     
  293.     Arguments:
  294.       height - maximum height of panel (uses all available space if -1)
  295.     """
  296.     
  297.     if self.height != height:
  298.       self.height = height
  299.       self.win = None
  300.   
  301.   def getWidth(self):
  302.     """
  303.     Provides the width used for subwindows (-1 if it isn't limited).
  304.     """
  305.     
  306.     return self.width
  307.   
  308.   def setWidth(self, width):
  309.     """
  310.     Changes the width used for subwindows. This uses all available space if -1.
  311.     
  312.     Arguments:
  313.       width - maximum width of panel (uses all available space if -1)
  314.     """
  315.     
  316.     if self.width != width:
  317.       self.width = width
  318.       self.win = None
  319.   
  320.   def getPreferredSize(self):
  321.     """
  322.     Provides the dimensions the subwindow would use when next redrawn, given
  323.     that none of the properties of the panel or parent change before then. This
  324.     returns a tuple of (height, width).
  325.     """
  326.     
  327.     newHeight, newWidth = self.parent.getmaxyx()
  328.     setHeight, setWidth = self.getHeight(), self.getWidth()
  329.     newHeight = max(0, newHeight - self.top)
  330.     newWidth = max(0, newWidth - self.left)
  331.     if setHeight != -1: newHeight = min(newHeight, setHeight)
  332.     if setWidth != -1: newWidth = min(newWidth, setWidth)
  333.     return (newHeight, newWidth)
  334.   
  335.   def handleKey(self, key):
  336.     """
  337.     Handler for user input. This returns true if the key press was consumed,
  338.     false otherwise.
  339.     
  340.     Arguments:
  341.       key - keycode for the key pressed
  342.     """
  343.     
  344.     return False
  345.   
  346.   def getHelp(self):
  347.     """
  348.     Provides help information for the controls this page provides. This is a
  349.     list of tuples of the form...
  350.     (control, description, status)
  351.     """
  352.     
  353.     return []
  354.   
  355.   def draw(self, width, height):
  356.     """
  357.     Draws display's content. This is meant to be overwritten by 
  358.     implementations and not called directly (use redraw() instead). The
  359.     dimensions provided are the drawable dimensions, which in terms of width is
  360.     a column less than the actual space.
  361.     
  362.     Arguments:
  363.       width  - horizontal space available for content
  364.       height - vertical space available for content
  365.     """
  366.     
  367.     pass
  368.   
  369.   def redraw(self, forceRedraw=False, block=False):
  370.     """
  371.     Clears display and redraws its content. This can skip redrawing content if
  372.     able (ie, the subwindow's unchanged), instead just refreshing the display.
  373.     
  374.     Arguments:
  375.       forceRedraw - forces the content to be cleared and redrawn if true
  376.       block       - if drawing concurrently with other panels this determines
  377.                     if the request is willing to wait its turn or should be
  378.                     abandoned
  379.     """
  380.     
  381.     # skipped if not currently visible or activity has been halted
  382.     if not self.isVisible() or HALT_ACTIVITY: return
  383.     
  384.     # if the panel's completely outside its parent then this is a no-op
  385.     newHeight, newWidth = self.getPreferredSize()
  386.     if newHeight == 0 or newWidth == 0:
  387.       self.win = None
  388.       return
  389.     
  390.     # recreates the subwindow if necessary
  391.     isNewWindow = self._resetSubwindow()
  392.     
  393.     # The reset argument is disregarded in a couple of situations:
  394.     # - The subwindow's been recreated (obviously it then doesn't have the old
  395.     #   content to refresh).
  396.     # - The subwindow's dimensions have changed since last drawn (this will
  397.     #   likely change the content's layout)
  398.     
  399.     subwinMaxY, subwinMaxX = self.win.getmaxyx()
  400.     if isNewWindow or subwinMaxY != self.maxY or subwinMaxX != self.maxX:
  401.       forceRedraw = True
  402.     
  403.     self.maxY, self.maxX = subwinMaxY, subwinMaxX
  404.     if not CURSES_LOCK.acquire(block): return
  405.     try:
  406.       if forceRedraw:
  407.         self.win.erase() # clears any old contents
  408.         self.draw(self.maxX, self.maxY)
  409.       self.win.refresh()
  410.     finally:
  411.       CURSES_LOCK.release()
  412.   
  413.   def hline(self, y, x, length, attr=curses.A_NORMAL):
  414.     """
  415.     Draws a horizontal line. This should only be called from the context of a
  416.     panel's draw method.
  417.     
  418.     Arguments:
  419.       y      - vertical location
  420.       x      - horizontal location
  421.       length - length the line spans
  422.       attr   - text attributes
  423.     """
  424.     
  425.     if self.win and self.maxX > x and self.maxY > y:
  426.       try:
  427.         drawLength = min(length, self.maxX - x)
  428.         self.win.hline(y, x, curses.ACS_HLINE | attr, drawLength)
  429.       except:
  430.         # in edge cases drawing could cause a _curses.error
  431.         pass
  432.   
  433.   def vline(self, y, x, length, attr=curses.A_NORMAL):
  434.     """
  435.     Draws a vertical line. This should only be called from the context of a
  436.     panel's draw method.
  437.     
  438.     Arguments:
  439.       y      - vertical location
  440.       x      - horizontal location
  441.       length - length the line spans
  442.       attr   - text attributes
  443.     """
  444.     
  445.     if self.win and self.maxX > x and self.maxY > y:
  446.       try:
  447.         drawLength = min(length, self.maxY - y)
  448.         self.win.vline(y, x, curses.ACS_VLINE | attr, drawLength)
  449.       except:
  450.         # in edge cases drawing could cause a _curses.error
  451.         pass
  452.   
  453.   def addch(self, y, x, char, attr=curses.A_NORMAL):
  454.     """
  455.     Draws a single character. This should only be called from the context of a
  456.     panel's draw method.
  457.     
  458.     Arguments:
  459.       y    - vertical location
  460.       x    - horizontal location
  461.       char - character to be drawn
  462.       attr - text attributes
  463.     """
  464.     
  465.     if self.win and self.maxX > x and self.maxY > y:
  466.       try:
  467.         self.win.addch(y, x, char, attr)
  468.       except:
  469.         # in edge cases drawing could cause a _curses.error
  470.         pass
  471.   
  472.   def addstr(self, y, x, msg, attr=curses.A_NORMAL):
  473.     """
  474.     Writes string to subwindow if able. This takes into account screen bounds
  475.     to avoid making curses upset. This should only be called from the context
  476.     of a panel's draw method.
  477.     
  478.     Arguments:
  479.       y    - vertical location
  480.       x    - horizontal location
  481.       msg  - text to be added
  482.       attr - text attributes
  483.     """
  484.     
  485.     # subwindows need a single character buffer (either in the x or y 
  486.     # direction) from actual content to prevent crash when shrank
  487.     if self.win and self.maxX > x and self.maxY > y:
  488.       try:
  489.         self.win.addstr(y, x, msg[:self.maxX - x], attr)
  490.       except:
  491.         # this might produce a _curses.error during edge cases, for instance
  492.         # when resizing with visible popups
  493.         pass
  494.   
  495.   def addfstr(self, y, x, msg):
  496.     """
  497.     Writes string to subwindow. The message can contain xhtml-style tags for
  498.     formatting, including:
  499.     <b>text</b>               bold
  500.     <u>text</u>               underline
  501.     <h>text</h>               highlight
  502.     <[color]>text</[color]>   use color (see uiTools.getColor() for constants)
  503.     
  504.     Tag nesting is supported and tag closing is strictly enforced (raising an
  505.     exception for invalid formatting). Unrecognized tags are treated as normal
  506.     text. This should only be called from the context of a panel's draw method.
  507.     
  508.     Text in multiple color tags (for instance "<blue><red>hello</red></blue>")
  509.     uses the bitwise OR of those flags (hint: that's probably not what you
  510.     want).
  511.     
  512.     Arguments:
  513.       y    - vertical location
  514.       x    - horizontal location
  515.       msg  - formatted text to be added
  516.     """
  517.     
  518.     if self.win and self.maxY > y:
  519.       formatting = [curses.A_NORMAL]
  520.       expectedCloseTags = []
  521.       unusedMsg = msg
  522.       
  523.       while self.maxX > x and len(unusedMsg) > 0:
  524.         # finds next consumeable tag (left as None if there aren't any left)
  525.         nextTag, tagStart, tagEnd = None, -1, -1
  526.         
  527.         tmpChecked = 0 # portion of the message cleared for having any valid tags
  528.         expectedTags = FORMAT_TAGS.keys() + expectedCloseTags
  529.         while nextTag == None:
  530.           tagStart = unusedMsg.find("<", tmpChecked)
  531.           tagEnd = unusedMsg.find(">", tagStart) + 1 if tagStart != -1 else -1
  532.           
  533.           if tagStart == -1 or tagEnd == -1: break # no more tags to consume
  534.           else:
  535.             # check if the tag we've found matches anything being expected
  536.             if unusedMsg[tagStart:tagEnd] in expectedTags:
  537.               nextTag = unusedMsg[tagStart:tagEnd]
  538.               break # found a tag to use
  539.             else:
  540.               # not a valid tag - narrow search to everything after it
  541.               tmpChecked = tagEnd
  542.         
  543.         # splits into text before and after tag
  544.         if nextTag:
  545.           msgSegment = unusedMsg[:tagStart]
  546.           unusedMsg = unusedMsg[tagEnd:]
  547.         else:
  548.           msgSegment = unusedMsg
  549.           unusedMsg = ""
  550.         
  551.         # adds text before tag with current formatting
  552.         attr = 0
  553.         for format in formatting: attr |= format
  554.         self.win.addstr(y, x, msgSegment[:self.maxX - x - 1], attr)
  555.         x += len(msgSegment)
  556.         
  557.         # applies tag attributes for future text
  558.         if nextTag:
  559.           formatTag = "<" + nextTag[2:] if nextTag.startswith("</") else nextTag
  560.           formatMatch = FORMAT_TAGS[formatTag][0](FORMAT_TAGS[formatTag][1])
  561.           
  562.           if not nextTag.startswith("</"):
  563.             # open tag - add formatting
  564.             expectedCloseTags.append("</" + nextTag[1:])
  565.             formatting.append(formatMatch)
  566.           else:
  567.             # close tag - remove formatting
  568.             expectedCloseTags.remove(nextTag)
  569.             formatting.remove(formatMatch)
  570.       
  571.       # only check for unclosed tags if we processed the whole message (if we
  572.       # stopped processing prematurely it might still be valid)
  573.       if expectedCloseTags and not unusedMsg:
  574.         # if we're done then raise an exception for any unclosed tags (tisk, tisk)
  575.         baseMsg = "Unclosed formatting tag%s:" % ("s" if len(expectedCloseTags) > 1 else "")
  576.         raise ValueError("%s: '%s'\n  \"%s\"" % (baseMsg, "', '".join(expectedCloseTags), msg))
  577.   
  578.   def getstr(self, y, x, initialText = "", format = None, maxWidth = None, validator = None):
  579.     """
  580.     Provides a text field where the user can input a string, blocking until
  581.     they've done so and returning the result. If the user presses escape then
  582.     this terminates and provides back None. This should only be called from
  583.     the context of a panel's draw method.
  584.     
  585.     This blanks any content within the space that the input field is rendered
  586.     (otherwise stray characters would be interpreted as part of the initial
  587.     input).
  588.     
  589.     Arguments:
  590.       y           - vertical location
  591.       x           - horizontal location
  592.       initialText - starting text in this field
  593.       format      - format used for the text
  594.       maxWidth    - maximum width for the text field
  595.       validator   - custom TextInputValidator for handling keybindings
  596.     """
  597.     
  598.     if not format: format = curses.A_NORMAL
  599.     
  600.     # makes cursor visible
  601.     try: previousCursorState = curses.curs_set(1)
  602.     except curses.error: previousCursorState = 0
  603.     
  604.     # temporary subwindow for user input
  605.     displayWidth = self.getPreferredSize()[1]
  606.     if maxWidth: displayWidth = min(displayWidth, maxWidth + x)
  607.     inputSubwindow = self.parent.subwin(1, displayWidth - x, self.top + y, self.left + x)
  608.     
  609.     # blanks the field's area, filling it with the font in case it's hilighting
  610.     inputSubwindow.clear()
  611.     inputSubwindow.bkgd(' ', format)
  612.     
  613.     # prepopulates the initial text
  614.     if initialText:
  615.       inputSubwindow.addstr(0, 0, initialText[:displayWidth - x - 1], format)
  616.     
  617.     # Displays the text field, blocking until the user's done. This closes the
  618.     # text panel and returns userInput to the initial text if the user presses
  619.     # escape.
  620.     
  621.     textbox = curses.textpad.Textbox(inputSubwindow)
  622.     
  623.     if not validator:
  624.       validator = textInput.BasicValidator()
  625.     
  626.     textbox.win.attron(format)
  627.     userInput = textbox.edit(lambda key: validator.validate(key, textbox)).strip()
  628.     textbox.win.attroff(format)
  629.     if textbox.lastcmd == curses.ascii.BEL: userInput = None
  630.     
  631.     # reverts visability settings
  632.     try: curses.curs_set(previousCursorState)
  633.     except curses.error: pass
  634.     
  635.     return userInput
  636.   
  637.   def addScrollBar(self, top, bottom, size, drawTop = 0, drawBottom = -1, drawLeft = 0):
  638.     """
  639.     Draws a left justified scroll bar reflecting position within a vertical
  640.     listing. This is shorted if necessary, and left undrawn if no space is
  641.     available. The bottom is squared off, having a layout like:
  642.      | 
  643.     *|
  644.     *|
  645.     *|
  646.      |
  647.     -+
  648.     
  649.     This should only be called from the context of a panel's draw method.
  650.     
  651.     Arguments:
  652.       top        - list index for the top-most visible element
  653.       bottom     - list index for the bottom-most visible element
  654.       size       - size of the list in which the listed elements are contained
  655.       drawTop    - starting row where the scroll bar should be drawn
  656.       drawBottom - ending row where the scroll bar should end, -1 if it should
  657.                    span to the bottom of the panel
  658.       drawLeft   - left offset at which to draw the scroll bar
  659.     """
  660.     
  661.     if (self.maxY - drawTop) < 2: return # not enough room
  662.     
  663.     # sets drawBottom to be the actual row on which the scrollbar should end
  664.     if drawBottom == -1: drawBottom = self.maxY - 1
  665.     else: drawBottom = min(drawBottom, self.maxY - 1)
  666.     
  667.     # determines scrollbar dimensions
  668.     scrollbarHeight = drawBottom - drawTop
  669.     sliderTop = scrollbarHeight * top / size
  670.     sliderSize = scrollbarHeight * (bottom - top) / size
  671.     
  672.     # ensures slider isn't at top or bottom unless really at those extreme bounds
  673.     if top > 0: sliderTop = max(sliderTop, 1)
  674.     if bottom != size: sliderTop = min(sliderTop, scrollbarHeight - sliderSize - 2)
  675.     
  676.     # avoids a rounding error that causes the scrollbar to be too low when at
  677.     # the bottom
  678.     if bottom == size: sliderTop = scrollbarHeight - sliderSize - 1
  679.     
  680.     # draws scrollbar slider
  681.     for i in range(scrollbarHeight):
  682.       if i >= sliderTop and i <= sliderTop + sliderSize:
  683.         self.addstr(i + drawTop, drawLeft, " ", curses.A_STANDOUT)
  684.       else:
  685.         self.addstr(i + drawTop, drawLeft, " ")
  686.     
  687.     # draws box around the scroll bar
  688.     self.vline(drawTop, drawLeft + 1, drawBottom - 1)
  689.     self.addch(drawBottom, drawLeft + 1, curses.ACS_LRCORNER)
  690.     self.addch(drawBottom, drawLeft, curses.ACS_HLINE)
  691.   
  692.   def _resetSubwindow(self):
  693.     """
  694.     Create a new subwindow instance for the panel if:
  695.     - Panel currently doesn't have a subwindow (was uninitialized or
  696.       invalidated).
  697.     - There's room for the panel to grow vertically (curses automatically
  698.       lets subwindows regrow horizontally, but not vertically).
  699.     - The subwindow has been displaced. This is a curses display bug that
  700.       manifests if the terminal's shrank then re-expanded. Displaced
  701.       subwindows are never restored to their proper position, resulting in
  702.       graphical glitches if we draw to them.
  703.     - The preferred size is smaller than the actual size (should shrink).
  704.     
  705.     This returns True if a new subwindow instance was created, False otherwise.
  706.     """
  707.     
  708.     newHeight, newWidth = self.getPreferredSize()
  709.     if newHeight == 0: return False # subwindow would be outside its parent
  710.     
  711.     # determines if a new subwindow should be recreated
  712.     recreate = self.win == None
  713.     if self.win:
  714.       subwinMaxY, subwinMaxX = self.win.getmaxyx()
  715.       recreate |= subwinMaxY < newHeight              # check for vertical growth
  716.       recreate |= self.top > self.win.getparyx()[0]   # check for displacement
  717.       recreate |= subwinMaxX > newWidth or subwinMaxY > newHeight # shrinking
  718.     
  719.     # I'm not sure if recreating subwindows is some sort of memory leak but the
  720.     # Python curses bindings seem to lack all of the following:
  721.     # - subwindow deletion (to tell curses to free the memory)
  722.     # - subwindow moving/resizing (to restore the displaced windows)
  723.     # so this is the only option (besides removing subwindows entirely which 
  724.     # would mean far more complicated code and no more selective refreshing)
  725.     
  726.     if recreate:
  727.       self.win = self.parent.subwin(newHeight, newWidth, self.top, self.left)
  728.       
  729.       # note: doing this log before setting win produces an infinite loop
  730.       msg = "recreating panel '%s' with the dimensions of %i/%i" % (self.getName(), newHeight, newWidth)
  731.       log.log(CONFIG["log.panelRecreated"], msg)
  732.     return recreate
  733.  
  734.