📄 listbox.py
字号:
#!/usr/bin/python## Urwid listbox class# Copyright (C) 2004-2007 Ian Ward## This library is free software; you can redistribute it and/or# modify it under the terms of the GNU Lesser General Public# License as published by the Free Software Foundation; either# version 2.1 of the License, or (at your option) any later version.## This library is distributed in the hope that it will be useful,# but WITHOUT ANY WARRANTY; without even the implied warranty of# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU# Lesser General Public License for more details.## You should have received a copy of the GNU Lesser General Public# License along with this library; if not, write to the Free Software# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA## Urwid web site: http://excess.org/urwid/from util import *from canvas import *from widget import *class ListWalkerError(Exception): passclass ListWalker(object): __metaclass__ = MetaSignals signals = ["modified"] def __hash__(self): return id(self) def _modified(self): Signals.emit(self, "modified") class PollingListWalker(object): # NOT ListWalker subclass def __init__(self, contents): """ contents -- list to poll for changes """ self.contents = contents if not type(contents) == type([]) and not hasattr( contents, '__getitem__' ): raise ListWalkerError, "SimpleListWalker expecting list like object, got: "+`contents` self.focus = 0 def _clamp_focus(self): if self.focus >= len(self.contents): self.focus = len(self.contents)-1 def get_focus(self): """Return (focus widget, focus position).""" if len(self.contents) == 0: return None, None self._clamp_focus() return self.contents[self.focus], self.focus def set_focus(self, position): """Set focus position.""" assert type(position) == type(1) self.focus = position def get_next(self, start_from): """ Return (widget after start_from, position after start_from). """ pos = start_from + 1 if len(self.contents) <= pos: return None, None return self.contents[pos],pos def get_prev(self, start_from): """ Return (widget before start_from, position before start_from). """ pos = start_from - 1 if pos < 0: return None, None return self.contents[pos],posclass SimpleListWalker(MonitoredList, ListWalker): def __init__(self, contents): """ contents -- list to copy into this object Changes made to this object (when it is treated as a list) are detected automatically and will cause ListBox objects using this list walker to be updated. """ if not type(contents) == type([]) and not hasattr(contents, '__getitem__'): raise ListWalkerError, "SimpleListWalker expecting list like object, got: "+`contents` MonitoredList.__init__(self, contents) self.focus = 0 def __hash__(self): return id(self) def _get_contents(self): """ Return self. Provides compatibility with old SimpleListWalker class. """ return self contents = property(_get_contents) def _modified(self): if self.focus >= len(self): self.focus = max(0, len(self)-1) ListWalker._modified(self) def set_modified_callback(self, callback): """ This function inherited from MonitoredList is not implemented in SimleListWalker. Use Signals.connect(list_walker, "modified", ...) instead. """ raise NotImplementedError('Use Signals.connect(' 'list_walker, "modified", ...) instead.') def get_focus(self): """Return (focus widget, focus position).""" if len(self) == 0: return None, None return self[self.focus], self.focus def set_focus(self, position): """Set focus position.""" assert type(position) == type(1) self.focus = position self._modified() def get_next(self, start_from): """ Return (widget after start_from, position after start_from). """ pos = start_from + 1 if len(self) <= pos: return None, None return self[pos],pos def get_prev(self, start_from): """ Return (widget before start_from, position before start_from). """ pos = start_from - 1 if pos < 0: return None, None return self[pos],pos class ListBoxError(Exception): passclass ListBox(BoxWidget): def __init__(self, body): """ body -- a ListWalker-like object that contains widgets to be displayed inside the list box """ if hasattr(body,'get_focus'): self.body = body else: self.body = PollingListWalker(body) try: Signals.connect(self.body, "modified", self._invalidate) except NameError: # our list walker has no modified signal so we must not # cache our canvases because we don't know when our # content has changed self.render = nocache_widget_render_instance(self) # offset_rows is the number of rows between the top of the view # and the top of the focused item self.offset_rows = 0 # inset_fraction is used when the focused widget is off the # top of the view. it is the fraction of the widget cut off # at the top. (numerator, denominator) self.inset_fraction = (0,1) # pref_col is the preferred column for the cursor when moving # between widgets that use the cursor (edit boxes etc.) self.pref_col = 'left' # variable for delayed focus change used by set_focus self.set_focus_pending = 'first selectable' # variable for delayed valign change used by set_focus_valign self.set_focus_valign_pending = None def calculate_visible(self, (maxcol, maxrow), focus=False ): """ Return (middle,top,bottom) or None,None,None. middle -- ( row offset(when +ve) or inset(when -ve), focus widget, focus position, focus rows, cursor coords or None ) top -- ( # lines to trim off top, list of (widget, position, rows) tuples above focus in order from bottom to top ) bottom -- ( # lines to trim off bottom, list of (widget, position, rows) tuples below focus in order from top to bottom ) """ # 0. set the focus if a change is pending if self.set_focus_pending or self.set_focus_valign_pending: self._set_focus_complete( (maxcol, maxrow), focus ) # 1. start with the focus widget focus_widget, focus_pos = self.body.get_focus() if focus_widget is None: #list box is empty? return None,None,None top_pos = bottom_pos = focus_pos offset_rows, inset_rows = self.get_focus_offset_inset( (maxcol,maxrow)) # force at least one line of focus to be visible if maxrow and offset_rows >= maxrow: offset_rows = maxrow -1 # adjust position so cursor remains visible cursor = None if maxrow and focus_widget.selectable() and focus: if hasattr(focus_widget,'get_cursor_coords'): cursor=focus_widget.get_cursor_coords((maxcol,)) if cursor is not None: cx, cy = cursor effective_cy = cy + offset_rows - inset_rows if effective_cy < 0: # cursor above top? inset_rows = cy elif effective_cy >= maxrow: # cursor below bottom? offset_rows = maxrow - cy -1 # set trim_top by focus trimmimg trim_top = inset_rows focus_rows = focus_widget.rows((maxcol,),True) # 2. collect the widgets above the focus pos = focus_pos fill_lines = offset_rows fill_above = [] top_pos = pos while fill_lines > 0: prev, pos = self.body.get_prev( pos ) if prev is None: # run out of widgets above? offset_rows -= fill_lines break top_pos = pos p_rows = prev.rows( (maxcol,) ) fill_above.append( (prev, pos, p_rows) ) if p_rows > fill_lines: # crosses top edge? trim_top = p_rows-fill_lines break fill_lines -= p_rows trim_bottom = focus_rows + offset_rows - inset_rows - maxrow if trim_bottom < 0: trim_bottom = 0 # 3. collect the widgets below the focus pos = focus_pos fill_lines = maxrow - focus_rows - offset_rows + inset_rows fill_below = [] while fill_lines > 0: next, pos = self.body.get_next( pos ) if next is None: # run out of widgets below? break bottom_pos = pos n_rows = next.rows( (maxcol,) ) fill_below.append( (next, pos, n_rows) ) if n_rows > fill_lines: # crosses bottom edge? trim_bottom = n_rows-fill_lines fill_lines -= n_rows break fill_lines -= n_rows # 4. fill from top again if necessary & possible fill_lines = max(0, fill_lines) if fill_lines >0 and trim_top >0: if fill_lines <= trim_top: trim_top -= fill_lines offset_rows += fill_lines fill_lines = 0 else: fill_lines -= trim_top offset_rows += trim_top trim_top = 0 pos = top_pos while fill_lines > 0: prev, pos = self.body.get_prev( pos ) if prev is None: break p_rows = prev.rows( (maxcol,) ) fill_above.append( (prev, pos, p_rows) ) if p_rows > fill_lines: # more than required trim_top = p_rows-fill_lines offset_rows += fill_lines break fill_lines -= p_rows offset_rows += p_rows # 5. return the interesting bits return ((offset_rows - inset_rows, focus_widget, focus_pos, focus_rows, cursor ), (trim_top, fill_above), (trim_bottom, fill_below)) def render(self, (maxcol, maxrow), focus=False ): """ Render listbox and return canvas. """ middle, top, bottom = self.calculate_visible( (maxcol, maxrow), focus=focus) if middle is None: return SolidCanvas(" ", maxcol, maxrow) _ignore, focus_widget, focus_pos, focus_rows, cursor = middle trim_top, fill_above = top trim_bottom, fill_below = bottom combinelist = [] rows = 0 fill_above.reverse() # fill_above is in bottom-up order for widget,w_pos,w_rows in fill_above: canvas = widget.render((maxcol,)) if w_rows != canvas.rows(): raise ListBoxError, "Widget %s at position %s within listbox calculated %d rows but rendered %d!"% (`widget`,`w_pos`,w_rows, canvas.rows()) rows += w_rows combinelist.append((canvas, w_pos, False)) focus_canvas = focus_widget.render((maxcol,), focus=focus) if focus_canvas.rows() != focus_rows: raise ListBoxError, "Focus Widget %s at position %s within listbox calculated %d rows but rendered %d!"% (`focus_widget`,`focus_pos`,focus_rows, focus_canvas.rows()) c_cursor = focus_canvas.cursor if cursor != c_cursor: raise ListBoxError, "Focus Widget %s at position %s within listbox calculated cursor coords %s but rendered cursor coords %s!" %(`focus_widget`,`focus_pos`,`cursor`,`c_cursor`) rows += focus_rows combinelist.append((focus_canvas, focus_pos, True)) for widget,w_pos,w_rows in fill_below: canvas = widget.render((maxcol,)) if w_rows != canvas.rows(): raise ListBoxError, "Widget %s at position %s within listbox calculated %d rows but rendered %d!"% (`widget`,`w_pos`,w_rows, canvas.rows()) rows += w_rows combinelist.append((canvas, w_pos, False)) final_canvas = CanvasCombine(combinelist) if trim_top: final_canvas.trim(trim_top) rows -= trim_top if trim_bottom: final_canvas.trim_end(trim_bottom) rows -= trim_bottom assert rows <= maxrow, "Listbox contents too long! Probably urwid's fault (please report): %s" % `top,middle,bottom` if rows < maxrow: bottom_pos = focus_pos if fill_below: bottom_pos = fill_below[-1][1] assert trim_bottom==0 and self.body.get_next(bottom_pos) == (None,None), "Listbox contents too short! Probably urwid's fault (please report): %s" % `top,middle,bottom` final_canvas.pad_trim_top_bottom(0, maxrow - rows) return final_canvas def set_focus_valign(self, valign): """Set the focus widget's display offset and inset. valign -- one of: 'top', 'middle', 'bottom' ('fixed top', rows) ('fixed bottom', rows) ('relative', percentage 0=top 100=bottom) """ vt,va,ht,ha=decompose_valign_height(valign,None,ListBoxError) self.set_focus_valign_pending = vt,va def set_focus(self, position, coming_from=None): """ Set the focus position and try to keep the old focus in view. position -- a position compatible with self.body.set_focus coming_from -- set to 'above' or 'below' if you know that old position is above or below the new position. """ assert coming_from in ('above', 'below', None) focus_widget, focus_pos = self.body.get_focus() self.set_focus_pending = coming_from, focus_widget, focus_pos self.body.set_focus( position ) def get_focus(self): """ Return a (focus widget, focus position) tuple. """ return self.body.get_focus() def _set_focus_valign_complete(self, (maxcol, maxrow), focus): """ Finish setting the offset and inset now that we have have a maxcol & maxrow. """ vt,va = self.set_focus_valign_pending self.set_focus_valign_pending = None self.set_focus_pending = None focus_widget, focus_pos = self.body.get_focus() if focus_widget is None: return rows = focus_widget.rows((maxcol,), focus) rtop, rbot = calculate_filler( vt, va, 'fixed', rows, None, maxrow ) self.shift_focus((maxcol, maxrow), rtop) def _set_focus_first_selectable(self, (maxcol, maxrow), focus): """ Choose the first visible, selectable widget below the current focus as the focus widget. """ self.set_focus_valign_pending = None self.set_focus_pending = None middle, top, bottom = self.calculate_visible( (maxcol, maxrow), focus=focus) if middle is None: return row_offset, focus_widget, focus_pos, focus_rows, cursor = middle
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -