Package libxyz :: Package ui :: Module panel

[hide private]
[frames] | no frames]

# Source Code for Module libxyz.ui.panel

       1  #-*- coding: utf8 -*
       2  #
       3  # Max E. Kuznecov ~syhpoon <syhpoon@syhpoon.name> 2008
       4  #
       5  # This file is part of XYZCommander.
       6  # XYZCommander is free software: you can redistribute it and/or modify
       7  # it under the terms of the GNU Lesser Public License as published by
       8  # the Free Software Foundation, either version 3 of the License, or
       9  # (at your option) any later version.
      10  # XYZCommander is distributed in the hope that it will be useful,
      11  # but WITHOUT ANY WARRANTY; without even the implied warranty of
      12  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
      13  # GNU Lesser Public License for more details.
      14  # You should have received a copy of the GNU Lesser Public License
      15  # along with XYZCommander. If not, see <http://www.gnu.org/licenses/>.
      16
      17  import os
      18  import traceback
      19
      20  import libxyz.ui
      21  import libxyz.core
      22  import libxyz.const
      23  import libxyz.exceptions
      24
      25  from libxyz.core.utils import ustring, bstring, is_func
      26  from libxyz.ui import lowui
      27  from libxyz.ui import align
      28  from libxyz.ui import Shortcut
      29  from libxyz.ui.utils import refresh
      30  from libxyz.ui.utils import truncate
      31  from libxyz.vfs.types import VFSTypeFile
      32  from libxyz.vfs.local import LocalVFSObject
      33
      34 -class Panel(lowui.WidgetWrap):
      35      """
      36      Panel is used to display filesystem hierarchy
      37      """
      38
      39      resolution = (u"panel", u"widget")
      40      context = u":sys:panel"
      41
      42      EVENT_SHUTDOWN = u"event:shutdown"
      43      EVENT_BEFORE_SWITCH_TAB = u"event:sys:panel:before_switch_tab"
      44      EVENT_SWITCH_TAB = u"event:sys:panel:switch_tab"
      45      EVENT_NEW_TAB = u"event:sys:panel:new_tab"
      46      EVENT_DEL_TAB = u"event:sys:panel:del_tab"
      47
      48 -    def __init__(self, xyz):
      49          self.xyz = xyz
      50          self.conf = self.xyz.conf[u"plugins"][u":sys:panel"]
      51
      52          self._keys = libxyz.ui.Keys()
      53
      54          _size = self.xyz.screen.get_cols_rows()
      55          _blocksize = libxyz.ui.Size(rows=_size[1] - 1, cols=_size[0] / 2 - 2)
      56          self._enc = xyzenc
      57          self._stop = False
      58          self._resize = False
      59
      60          self._set_plugins()
      61          self._cmd = libxyz.ui.Cmd(xyz)
      62          _cwd = os.getcwd()
      63
      64          self.filters = self._build_filters()
      65          self.xyz.hm.register("event:conf_update", self._update_conf_hook)
      66
      67          self.block1 = Block(xyz, _blocksize, _cwd, self._enc, active=True)
      68          self.block2 = Block(xyz, _blocksize, _cwd, self._enc)
      69          self._compose()
      70
      71          super(Panel, self).__init__(self._widget)
      72
      73      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      74
      75 -    def _update_conf_hook(self, var, val, sect):
      76          """
      77          Hook for update conf event
      78          """
      79
      80          # Not ours
      81          if sect != "plugins" or var != ":sys:panel":
      82              return
      83
      84          if "filters" in val or "filters_enabled" in val:
      85              self.filters = self._build_filters()
      86
      87      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      88
      89 -    def _compose(self):
      90          """
      91          Compose widgets
      92          """
      93
      94          columns = lowui.Columns([self.block1, self.block2], 0)
      95          self._widget = lowui.Pile([columns, self._cmd])
      96
      97      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      98
      99 -    def _build_filters(self):
     100          """
     101          Compile filters
     102          """
     103
     104          filters = []
     105
     106          # No need to compile
     107          if not self.conf[u"filters_enabled"]:
     108              return filters
     109
     110          for f in self.conf[u"filters"]:
     111              try:
     112                  rule = libxyz.core.FSRule(ustring(f))
     113              except libxyz.exceptions.ParseError, e:
     114                  xyzlog.error(_(u"Error compiling filter: %s" %
     115                                 ustring(str(e))))
     116                  continue
     117              else:
     118                  filters.append(rule)
     119
     120          return filters
     121
     122      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     123
     124      @property
     125 -    def active(self):
     126          if self.block1.active:
     127              return self.block1
     128          else:
     129              return self.block2
     130
     131      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     132
     133      @property
     134 -    def inactive(self):
     135          if self.block1.active:
     136              return self.block2
     137          else:
     138              return self.block1
     139
     140      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     141
     142 -    def loop(self):
     143          """
     144          Start working loop
     145          """
     146
     147          _dim = self.xyz.screen.get_cols_rows()
     148
     149          while True:
     150              if self._stop:
     151                  break
     152
     153              canv = self.xyz.top.render(_dim, True)
     154              self.xyz.screen.draw_screen(_dim, canv)
     155
     156              _input = self.xyz.input.get()
     157
     158              if _input:
     159                  try:
     160                      self._cmd.keypress(_dim, _input)
     161                  except Exception, e:
     162                      xyzlog.error(_(u"Error executing bind (%s): %s") %
     163                                   (Shortcut(_input), ustring(str(e))))
     164                      xyzlog.debug(ustring(traceback.format_exc(),
     165                                           self._enc))
     166
     167                  if self.xyz.input.resized:
     168                      self._resize = True
     169
     170                  if self._resize:
     171                      self._resize = False
     172                      _dim = self.xyz.screen.get_cols_rows()
     173                      _bsize = libxyz.ui.Size(rows=_dim[1] - 1,
     174                                              cols=_dim[0] / 2 - 2)
     175
     176                      self.block1.size = _bsize
     177                      self.block2.size = _bsize
     178                      self._cmd._invalidate()
     179                      self.block1._invalidate()
     180                      self.block2._invalidate()
     181
     182      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     183
     184 -    def _set_plugins(self):
     185          """
     186          Set virtual plugins
     187          """
     188
     189          # :sys:run
     190          _run_plugin = libxyz.core.plugins.VirtualPlugin(self.xyz, u"run")
     191          _run_plugin.export(self.shutdown)
     192          _run_plugin.export(self.repaint)
     193
     194          _run_plugin.VERSION = u"0.1"
     195          _run_plugin.AUTHOR = u"Max E. Kuznecov <syhpoon@syhpoon.name>"
     196          _run_plugin.BRIEF_DESCRIPTION = u"Run plugin"
     197          _run_plugin.HOMEPAGE = u"xyzcmd.syhpoon.name"
     198
     199          # :sys:panel
     200          _panel_plugin = libxyz.core.plugins.VirtualPlugin(self.xyz, u"panel")
     201          _panel_plugin.export(self.entry_next)
     202          _panel_plugin.export(self.entry_prev)
     203          _panel_plugin.export(self.entry_top)
     204          _panel_plugin.export(self.entry_bottom)
     205          _panel_plugin.export(self.block_next)
     206          _panel_plugin.export(self.block_prev)
     207          _panel_plugin.export(self.switch_active)
     208          _panel_plugin.export(self.get_selected)
     209          _panel_plugin.export(self.get_tagged)
     210          _panel_plugin.export(self.get_untagged)
     211          _panel_plugin.export(self.get_current)
     212          _panel_plugin.export(self.get_active)
     213          _panel_plugin.export(self.toggle_tag)
     214          _panel_plugin.export(self.tag_all)
     215          _panel_plugin.export(self.untag_all)
     216          _panel_plugin.export(self.tag_invert)
     217          _panel_plugin.export(self.tag_rule)
     218          _panel_plugin.export(self.untag_rule)
     219          _panel_plugin.export(self.swap_blocks)
     220          _panel_plugin.export(self.reload)
     221          _panel_plugin.export(self.reload_all)
     222          _panel_plugin.export(self.action)
     223          _panel_plugin.export(self.chdir)
     224          _panel_plugin.export(self.search_forward)
     225          _panel_plugin.export(self.search_backward)
     226          _panel_plugin.export(self.search_cycle)
     227          _panel_plugin.export(self.show_tagged)
     228          _panel_plugin.export(self.select)
     229          _panel_plugin.export(self.cwd)
     230          _panel_plugin.export(self.vfs_driver)
     231          _panel_plugin.export(self.filter)
     232          _panel_plugin.export(self.sort)
     233          _panel_plugin.export(self.new_tab)
     234          _panel_plugin.export(self.del_tab)
     235          _panel_plugin.export(self.switch_tab)
     236          _panel_plugin.export(self.next_tab)
     237          _panel_plugin.export(self.prev_tab)
     238          _panel_plugin.export(self.get_tabs)
     239          _panel_plugin.export(self.active_tab)
     240
     241          _panel_plugin.VERSION = u"0.1"
     242          _panel_plugin.AUTHOR = u"Max E. Kuznecov <syhpoon@syhpoon.name>"
     243          _panel_plugin.BRIEF_DESCRIPTION = u"Panel plugin"
     244          _panel_plugin.HOMEPAGE = u"xyzcmd.syhpoon.name"
     245          _panel_plugin.DOC = u"""\
     246  Configuration variables:
     247  filters_enabled - Enable permanent filters. Default - False
     248  filters_policy - Filters policy.
     249   If True - filter out objects which matching the rule.
     250   If False - filter out objects which do not match the rule. Default - True
     251  filters - List of permanent filters.
     252   Filters applied in defined order sequentially. Default - []
     253  sorting_policy - Active sorting policy name or None. Default - None
     254  sorting - Defined sorting policies. Each key corresponds to a policy name
     255   and value is either a function with two arguments (VFSObject) behaving
     256   like cmp() or a list of those functions. If value is a list, each function
     257   applied sequentially. Default - []"""
     258
     259          _panel_plugin.EVENTS = [
     260              ("before_switch_tab",
     261               "Fires before switching to another tab. "\
     262               "Arguments: Block instance and old tab index."),
     263
     264              ("switch_tab",
     265               "Fires when switching to another tab. "\
     266               "Arguments: Block instance and new tab index."),
     267
     268              ("new_tab",
     269               "Fires when new tab is added. "\
     270               "Arguments: Block instance and new tab index."),
     271
     272              ("del_tab",
     273               "Fires when tab is delete. "\
     274               "Arguments: Block instance and deleted tab index."),
     275              ]
     276
     277          self.xyz.pm.register(_run_plugin)
     278          self.xyz.pm.register(_panel_plugin)
     279
     280      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     281
     282 -    def shutdown(self):
     283          """
     284          Quit program
     285          """
     286
     287          _q = _(u"Really quit %s?") % libxyz.const.PROG
     288          _title = libxyz.const.PROG
     289
     290          if libxyz.ui.YesNoBox(self.xyz, self.xyz.top, _q, _title).show():
     291              self._stop = True
     292              self.xyz.hm.dispatch(self.EVENT_SHUTDOWN)
     293
     294      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     295
     296 -    def repaint(self):
     297          """
     298          Reparint screen
     299          """
     300
     301          self._resize = True
     302          self.xyz.screen.clear()
     303
     304      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     305
     306 -    def entry_next(self):
     307          """
     308          Next entry
     309          """
     310
     311          return self.active.next()
     312
     313      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     314
     315 -    def entry_prev(self):
     316          """
     317          Previous entry
     318          """
     319
     320          return self.active.prev()
     321
     322      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     323
     324 -    def entry_top(self):
     325          """
     326          Top entry
     327          """
     328
     329          return self.active.top()
     330
     331      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     332
     333 -    def entry_bottom(self):
     334          """
     335          Bottom entry
     336          """
     337
     338          return self.active.bottom()
     339
     340      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     341
     342 -    def switch_active(self):
     343          """
     344          Switch active block
     345          """
     346
     347          if self.block1.active:
     348              self.block1.deactivate()
     349              self.block2.activate()
     350          else:
     351              self.block2.deactivate()
     352              self.block1.activate()
     353
     354      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     355
     356 -    def block_next(self):
     357          """
     358          Next block
     359          """
     360
     361          return self.active.block_next()
     362
     363      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     364
     365 -    def block_prev(self):
     366          """
     367          Previous block
     368          """
     369
     370          return self.active.block_prev()
     371
     372      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     373
     374 -    def get_selected(self, active=True):
     375          """
     376          Get selected VFSObject instance
     377          """
     378
     379          if active:
     380              obj = self.active
     381          else:
     382              obj = self.inactive
     383
     384          return obj.get_selected()
     385
     386      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     387
     388 -    def get_tagged(self, active=True):
     389          """
     390          Return list of tagged VFSObject instances
     391          """
     392
     393          if active:
     394              obj = self.active
     395          else:
     396              obj = self.inactive
     397
     398          return obj.get_tagged()
     399
     400      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     401
     402 -    def get_untagged(self, active=True):
     403          """
     404          Return list of not tagged VFSObject instances
     405          """
     406
     407          if active:
     408              obj = self.active
     409          else:
     410              obj = self.inactive
     411
     412          return obj.get_untagged()
     413
     414      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     415
     416 -    def get_current(self, active=True):
     417          """
     418          Return VFSObject instance of current VFSObject
     419          """
     420
     421          if active:
     422              obj = self.active
     423          else:
     424              obj = self.inactive
     425
     426          return obj.get_current()
     427
     428      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     429
     430 -    def get_active(self):
     431          """
     432          Return list of tagged VFSObject instances or list of single selected
     433          object if none tagged
     434          """
     435
     436          return self.get_tagged() or [self.get_selected()]
     437
     438      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     439
     440 -    def toggle_tag(self, active=True):
     441          """
     442          Tag selected file
     443          """
     444
     445          if active:
     446              obj = self.active
     447          else:
     448              obj = self.inactive
     449
     450          return obj.toggle_tag()
     451
     452      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     453
     454 -    def tag_all(self, active=True):
     455          """
     456          Tag every single object in current dir
     457          """
     458
     459          if active:
     460              obj = self.active
     461          else:
     462              obj = self.inactive
     463
     464          return obj.tag_all()
     465
     466      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     467
     468 -    def untag_all(self, active=True):
     469          """
     470          Untag every single object in current dir
     471          """
     472
     473          if active:
     474              obj = self.active
     475          else:
     476              obj = self.inactive
     477
     478          return obj.untag_all()
     479
     480      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     481
     482 -    def tag_invert(self, active=True):
     483          """
     484          Invert currently tagged files
     485          """
     486
     487          if active:
     488              obj = self.active
     489          else:
     490              obj = self.inactive
     491
     492          return obj.tag_invert()
     493
     494      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     495
     496 -    def tag_rule(self, active=True):
     497          """
     498          Tag files by combined rule
     499          """
     500
     501          if active:
     502              obj = self.active
     503          else:
     504              obj = self.inactive
     505
     506          return obj.tag_rule()
     507
     508      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     509
     510 -    def untag_rule(self, active=True):
     511          """
     512          Untag files by combined rules
     513          """
     514
     515          if active:
     516              obj = self.active
     517          else:
     518              obj = self.inactive
     519
     520          return obj.untag_rule()
     521
     522      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     523
     524 -    def swap_blocks(self):
     525          """
     526          Swap panel blocks
     527          """
     528
     529          self.block1, self.block2 = self.block2, self.block1
     530          self._compose()
     531
     532          if hasattr(self, "set_w"):
     533              self.set_w(self._widget)
     534          else:
     535              self._w = self._widget
     536
     537      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     538
     539 -    def reload(self, active=True):
     540          """
     541          Reload contents
     542          """
     543
     544          if active:
     545              obj = self.active
     546          else:
     547              obj = self.inactive
     548
     549          return obj.reload()
     550
     551      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     552
     553 -    def reload_all(self):
     554          """
     555          Reload both panels
     556          """
     557
     558          self.active.reload()
     559          self.inactive.reload()
     560
     561      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     562
     563 -    def action(self, active=True):
     564          """
     565          Perfrom action on selected object
     566          """
     567
     568          if active:
     569              obj = self.active
     570          else:
     571              obj = self.inactive
     572
     573          return obj.action()
     574
     575      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     576
     577 -    def chdir(self, path, active=True):
     578          """
     579          Change directory
     580          """
     581
     582          if active:
     583              obj = self.active
     584          else:
     585              obj = self.inactive
     586
     587          return obj.chdir(path, active=active)
     588
     589      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     590
     591 -    def search_forward(self):
     592          """
     593          Enable forward search-when-you-type mode
     594          """
     595
     596          self.active.search_forward()
     597
     598      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     599
     600 -    def search_backward(self):
     601          """
     602          Enable backward search-when-you-type mode
     603          """
     604
     605          self.active.search_backward()
     606
     607      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     608
     609 -    def search_cycle(self):
     610          """
     611          Enable cyclic search-when-you-type mode
     612          """
     613
     614          self.active.search_cycle()
     615
     616      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     617
     618 -    def show_tagged(self, active=True):
     619          """
     620          Show only tagged entries
     621          """
     622
     623          if active:
     624              obj = self.active
     625          else:
     626              obj = self.inactive
     627
     628          return obj.show_tagged()
     629
     630      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     631
     632 -    def select(self, name, active=True):
     633          """
     634          Select VFS object by given name in current directory
     635          """
     636
     637          if active:
     638              obj = self.active
     639          else:
     640              obj = self.inactive
     641
     642          return obj.select(name)
     643
     644      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     645
     646 -    def cwd(self, active=True):
     647          """
     648          Get current working directory
     649          """
     650
     651          if active:
     652              obj = self.active
     653          else:
     654              obj = self.inactive
     655
     656          return obj.cwd
     657
     658      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     659
     660 -    def vfs_driver(self, active=True):
     661          """
     662          Return vfs driver used by object. None stands for LocalVFS
     663          """
     664
     665          if active:
     666              obj = self.active
     667          else:
     668              obj = self.inactive
     669
     670          return obj.vfs_driver
     671
     672      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     673
     674 -    def filter(self, objects):
     675          """
     676          Filter objects
     677          """
     678
     679          if not self.conf["filters_enabled"]:
     680              return objects
     681
     682          policy = self.conf["filters_policy"]
     683
     684          def policyf(res):
     685              if policy == True:
     686                  result = not res
     687              else:
     688                  result = res
     689
     690              return result
     691
     692          for f in self.filters:
     693              objects = [x for x in objects if policyf(f.match(x))]
     694
     695          return objects
     696
     697      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     698
     699 -    def sort(self, objects):
     700          """
     701          Sort objects
     702          """
     703
     704          policy = self.conf["sorting_policy"]
     705
     706          if policy is None:
     707              return objects
     708
     709          if policy not in self.conf["sorting"]:
     710              xyzlog.warning(_(u"Unable to find `%s` sorting policy" %
     711                               ustring(policy)))
     712              return objects
     713
     714          policy_data = self.conf["sorting"][policy]
     715
     716          if is_func(policy_data):
     717              objects.sort(cmp=policy_data)
     718          elif isinstance(policy_data, list):
     719              for f in policy_data:
     720                  objects.sort(cmp=f)
     721
     722          return objects
     723
     724      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     725
     726 -    def new_tab(self, tabname=None, active=True):
     727          """
     728          Create new tab
     729          """
     730
     731          if active:
     732              obj = self.active
     733          else:
     734              obj = self.inactive
     735
     736          return obj.tab_bar.new_tab(tabname)
     737
     738      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     739
     740 -    def del_tab(self, index=None, active=True):
     741          """
     742          Delete tab. If index is None - delete current tab
     743          """
     744
     745          if active:
     746              obj = self.active
     747          else:
     748              obj = self.inactive
     749
     750          return obj.tab_bar.del_tab(index)
     751
     752      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     753
     754 -    def switch_tab(self, index, active=True):
     755          """
     756          Switch to tab
     757          """
     758
     759          if active:
     760              obj = self.active
     761          else:
     762              obj = self.inactive
     763
     764          return obj.tab_bar.switch_tab(index)
     765
     766      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     767
     768 -    def next_tab(self, active=True):
     769          """
     770          Switch to the next tab
     771          """
     772
     773          if active:
     774              obj = self.active
     775          else:
     776              obj = self.inactive
     777
     778          return obj.tab_bar.next_tab()
     779
     780      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     781
     782 -    def prev_tab(self, active=True):
     783          """
     784          Switch to the previous  tab
     785          """
     786
     787          if active:
     788              obj = self.active
     789          else:
     790              obj = self.inactive
     791
     792          return obj.tab_bar.prev_tab()
     793
     794      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     795
     796 -    def get_tabs(self, active=True):
     797          """
     798          Return list of tabs in format:
     799          [(path, selected_name)]
     800          """
     801
     802          if active:
     803              obj = self.active
     804          else:
     805              obj = self.inactive
     806
     807          return obj.get_tabs()
     808
     809      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     810
     811 -    def active_tab(self, active=True):
     812          """
     813          Get active tab index
     814          """
     815
     816          if active:
     817              obj = self.active
     818          else:
     819              obj = self.inactive
     820
     821          return obj.tab_bar.active_tab
     822
     823  #++++++++++++++++++++++++++++++++++++++++++++++++
     824
     825 -class Block(lowui.FlowWidget):
     826      """
     827      Single panel block
     828      """
     829
     830 -    def __init__(self, xyz, size, path, enc, active=False):
     831          """
     832          @param xyz: XYZData instance
     833          @param size: Block widget size
     834          @type size: L{libxyz.ui.Size}
     835          @param enc: Local encoding
     836          @param active: Boolean flag, True if block is active
     837
     838          Required resources: cwdtitle, cwdtitleinact, panel, cursor, info
     839                              border, tagged
     840          """
     841
     842          self.xyz = xyz
     843          self.size = size
     844          self.attr = lambda x: self.xyz.skin.attr(Panel.resolution, x)
     845          # Length of the string in terms of terminal columns
     846          self.term_width = lambda x: lowui.util.calc_width(x, 0, len(x))
     847
     848          self.active = active
     849          self.selected = 0
     850          self.cwd = path
     851
     852          self._display = []
     853          self._vindex = 0
     854          self._from = 0
     855          self._to = 0
     856          self._force_reload = False
     857          self.entries = []
     858          self._dir = None
     859          self._len = 0
     860          self._palettes = []
     861          self._vfsobj = None
     862          self._title = u""
     863          self._tagged = []
     864          self._tab_data = []
     865
     866          self._cursor_attr = None
     867          self._custom_info = None
     868          self._keys = libxyz.ui.Keys()
     869          self._cmd = self.xyz.pm.load(":sys:cmd")
     870          self._filter = self.xyz.pm.from_load(":sys:panel", "filter")
     871          self._sort = self.xyz.pm.from_load(":sys:panel", "sort")
     872
     873          self._pending = libxyz.core.Queue(20)
     874          self._re_raw = r".*"
     875          self._rule_raw = ""
     876          self._enc = enc
     877          self.vfs_driver = None
     878
     879          self.tab_bar = TabBar(self.xyz, self.attr, self)
     880
     881          self._winfo = lowui.Text(u"")
     882          self._sep = libxyz.ui.Separator()
     883
     884          _info = self._make_info()
     885          _title_attr = self._get_title_attr()
     886
     887          self.frame = lowui.Frame(lowui.Filler(lowui.Text("")), footer=_info)
     888
     889          self.border = libxyz.ui.Border(self.frame, self._title,
     890                                         _title_attr, self.attr(u"border"))
     891          self.block = lowui.Frame(
     892              lowui.AttrWrap(self.border, self.attr(u"panel")),
     893              header=self.tab_bar)
     894
     895          self.xyz.hm.register(Panel.EVENT_BEFORE_SWITCH_TAB,
     896                               self._before_switch_tab_hook)
     897          self.xyz.hm.register(Panel.EVENT_SWITCH_TAB, self._switch_tab_hook)
     898          self.xyz.hm.register(Panel.EVENT_NEW_TAB, self._new_tab_hook)
     899          self.xyz.hm.register(Panel.EVENT_DEL_TAB, self._del_tab_hook)
     900
     901          self.tab_bar.new_tab()
     902
     903          super(Block, self).__init__()
     904
     905      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     906
     907 -    def rows(self, (maxcol,), focus=False):
     908          # TODO: cache
     909          w = self.display_widget((maxcol,), focus)
     910          return w.rows((maxcol,), focus)
     911
     912      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     913
     914 -    def display_widget(self, (maxcol,), focus):
     915          return lowui.BoxAdapter(self.block, self.size.rows)
     916
     917      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     918
     919 -    def _setup(self, vfsobj):
     920          _parent, _dir, _dirs, _files = vfsobj.walk()
     921
     922          self._dir = _dir
     923
     924          _entries = []
     925          _entries.extend(_dirs)
     926          _entries.extend(_files)
     927          _entries = self._filter(_entries)
     928          _entries = self._sort(_entries)
     929          _entries.insert(0, _parent)
     930
     931          self._title = truncate(_dir.full_path, self.size.cols - 4,
     932                                 self._enc, True)
     933
     934          if hasattr(self, "border"):
     935              self.border.set_title(self._title)
     936
     937          self._tagged = []
     938
     939          self.entries = _entries
     940          self._len = len(self.entries)
     941          self._palettes = self._process_skin_rulesets()
     942          self._vfsobj = vfsobj
     943          self.vfs_driver = vfsobj.driver
     944
     945          self._force_reload = True
     946
     947      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     948
     949 -    def selectable(self):
     950          return True
     951
     952      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     953
     954 -    def render(self, (maxcol,), focus=False):
     955          """
     956          Render block
     957          """
     958
     959          w = self.display_widget((maxcol,), focus)
     960          maxrow = w.rows((maxcol,), focus)
     961
     962          # Reduce original sizes in order to fit into overlay
     963          maxcol_orig, maxcol = maxcol, maxcol - 2
     964          maxrow_orig, maxrow = maxrow, maxrow - 5
     965
     966          # Search for pending action
     967          while True:
     968              try:
     969                  _act = self._pending.pop()
     970              except IndexError:
     971                  break
     972              else:
     973                  _act(maxcol, maxrow)
     974
     975          if self._custom_info is not None:
     976              self._set_custom_info(self._custom_info, maxcol)
     977          else:
     978              self._set_info(self.entries[self.selected], maxcol)
     979
     980          _tlen = len(self._tagged)
     981
     982          if _tlen > 0:
     983              _text = _(u"%s bytes (%d)") % (
     984                  self._make_number_readable(
     985                      reduce(lambda x, y: x + y,
     986                             [self.entries[x].size for x in self._tagged
     987                              if isinstance(self.entries[x].ftype, VFSTypeFile)
     988                              ], 0)), _tlen)
     989
     990              self._sep.set_text(bstring(_text, self._enc),
     991                                 self.attr(u"tagged"))
     992          else:
     993              self._sep.clear_text()
     994
     995          self._display = self._get_visible(maxrow, maxcol, self._force_reload)
     996          self._force_reload = False
     997
     998          _len = len(self._display)
     999
    1000          canvases = []
    1001
    1002          for i in xrange(0, _len):
    1003              _text = self._display[i]
    1004              _own_attr = None
    1005              _abs_i = self._from + i
    1006
    1007              if self._cursor_attr is not None and i == self._vindex:
    1008                  _own_attr = self._cursor_attr
    1009              elif self.active and i == self._vindex:
    1010                  _own_attr = self.attr(u"cursor")
    1011              elif _abs_i in self._tagged:
    1012                  _own_attr = self.attr(u"tagged")
    1013              elif _abs_i in self._palettes:
    1014                  _own_attr = self._palettes[_abs_i]
    1015
    1016              if _own_attr is not None:
    1017                  x = lowui.AttrWrap(lowui.Text(bstring(_text, self._enc)),
    1018                                     _own_attr).render((maxcol,))
    1019                  canvases.append((x, i, False))
    1020              else:
    1021                  canvases.append((lowui.Text(_text).render((maxcol,)),
    1022                                   i, False))
    1023
    1024          if _len < maxrow:
    1025              _pad = lowui.AttrWrap(lowui.Text(" "), self.attr(u"panel"))
    1026              canvases.append((_pad.render((maxcol,), focus), 0, False))
    1027
    1028          _info = self._make_info()
    1029          self.frame.set_footer(_info)
    1030
    1031          combined = lowui.CanvasCombine(canvases)
    1032          border = self.block.render((maxcol_orig, maxrow_orig), focus)
    1033
    1034          if _len > maxrow:
    1035              combined.trim_end(_len - maxrow)
    1036
    1037          return lowui.CanvasOverlay(combined, border, 1, 2)
    1038
    1039      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1040
    1041 -    def _make_info(self):
    1042          _info = lowui.Padding(self._winfo, align.LEFT, self.size.cols)
    1043          _info = lowui.AttrWrap(_info, self.attr(u"info"))
    1044          _info = lowui.Pile([self._sep, _info])
    1045          return _info
    1046
    1047      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1048
    1049 -    def _make_number_readable(self, num):
    1050          _res = []
    1051
    1052          i = 0
    1053          _sep = False
    1054
    1055          for x in reversed(unicode(num)):
    1056              if _sep:
    1057                  _res.append(u"_")
    1058                  _sep = False
    1059
    1060              _res.append(x)
    1061
    1062              if i > 0 and (i + 1) % 3 == 0:
    1063                  _sep = True
    1064
    1065              i += 1
    1066
    1067          _res.reverse()
    1068
    1069          return u"".join(_res)
    1070
    1071      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1072
    1073 -    def _get_visible(self, rows, cols, reload=False):
    1074          """
    1075          Get currently visible piece of entries
    1076          """
    1077
    1078          _len = self._len
    1079          _from, _to, self._vindex = self._update_vindex(rows)
    1080
    1081          if reload or ((_from, _to) != (self._from, self._to)):
    1082              self._from, self._to = _from, _to
    1083              self._display = []
    1084
    1085              for _obj in self.entries[self._from:self._to]:
    1086                  _text = "%s%s "% (_obj.vtype, _obj.name)
    1087                  _text = truncate(_text, cols, self._enc)
    1088                  self._display.append(_text)
    1089
    1090          return self._display
    1091
    1092      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1093
    1094 -    def _process_skin_rulesets(self):
    1095          """
    1096          Process defined fs.* rulesets
    1097          """
    1098
    1099          _result = {}
    1100
    1101          try:
    1102              _rules = self.xyz.skin[u"fs.rules"]
    1103          except KeyError:
    1104              return _result
    1105
    1106          for i in xrange(self._len):
    1107              for _exp, _attr in _rules.iteritems():
    1108                  if _exp.match(self.entries[i]):
    1109                      _result[i] = _attr.name
    1110                      break
    1111
    1112          return _result
    1113
    1114      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1115
    1116 -    def _get_title_attr(self):
    1117          """
    1118          Return title attr
    1119          """
    1120
    1121          if self.active:
    1122              return self.attr(u"cwdtitle")
    1123          else:
    1124              return self.attr(u"cwdtitleinact")
    1125
    1126      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1127
    1128 -    def _set_info(self, vfsobj, cols):
    1129          """
    1130          Set info text
    1131          """
    1132
    1133          _part2 = vfsobj.info
    1134          _part1 = truncate(vfsobj.visual, cols - len(_part2) - 2, self._enc)
    1135
    1136          _text = u"%s%s%s" % (_part1,
    1137                               u" " * (cols - (self.term_width(_part1) +
    1138                                               self.term_width(_part2)) -
    1139                                       1), _part2)
    1140
    1141          self._winfo.set_text(bstring(_text, self._enc))
    1142
    1143      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1144
    1145 -    def _set_custom_info(self, custom_text, cols):
    1146          """
    1147          Set custom info text
    1148          """
    1149
    1150          _text = truncate(custom_text, cols, self._enc, True)
    1151          self._winfo.set_text(bstring(_text, self._enc))
    1152
    1153      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1154
    1155 -    def _update_vindex(self, rows):
    1156          """
    1157          Calculate vindex according to selected position
    1158          """
    1159
    1160          pos = self.selected
    1161
    1162          _from = pos / rows * rows
    1163          _to = _from + rows
    1164          _vindex = pos - (rows * (pos / rows))
    1165
    1166          return (_from, _to, _vindex)
    1167
    1168      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1169
    1170      @refresh
    1171 -    def deactivate(self):
    1172          """
    1173          Deactivate block
    1174          """
    1175
    1176          self.active = False
    1177          self.border.set_title_attr(self._get_title_attr())
    1178
    1179      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1180
    1181      @refresh
    1182 -    def activate(self):
    1183          """
    1184          Activate block
    1185          """
    1186
    1187          self.active = True
    1188          self.border.set_title_attr(self._get_title_attr())
    1189          self.chdir(self._dir.path, reload=False)
    1190
    1191      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1192
    1193      @refresh
    1194 -    def next(self):
    1195          """
    1196          Next entry
    1197          """
    1198
    1199          if self.selected < self._len - 1:
    1200              self.selected += 1
    1201
    1202      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1203
    1204      @refresh
    1205 -    def prev(self):
    1206          """
    1207          Previous entry
    1208          """
    1209
    1210          if self.selected > 0:
    1211              self.selected -= 1
    1212
    1213      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1214
    1215      @refresh
    1216 -    def top(self):
    1217          """
    1218          Top entry
    1219          """
    1220
    1221          self.selected = 0
    1222
    1223      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1224
    1225      @refresh
    1226 -    def bottom(self):
    1227          """
    1228          Bottom entry
    1229          """
    1230
    1231          self.selected = self._len - 1
    1232
    1233      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1234
    1235      @refresh
    1236 -    def block_next(self):
    1237          """
    1238          One block down
    1239          """
    1240
    1241          def _do_next_block(cols, rows):
    1242              if self.selected + rows >= self._len:
    1243                  return self.bottom()
    1244              else:
    1245                  self.selected += rows
    1246
    1247          #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1248
    1249          # As we aren't aware of how many rows are in a single
    1250          # block at this moment, postpone jumping until render is called
    1251
    1252          self._pending.push(_do_next_block)
    1253
    1254      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1255
    1256      @refresh
    1257 -    def block_prev(self):
    1258          """
    1259          One block up
    1260          """
    1261
    1262          def _do_prev_block(cols, rows):
    1263              if self.selected - rows < 0:
    1264                  return self.top()
    1265              else:
    1266                  self.selected -= rows
    1267
    1268          #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1269
    1270          self._pending.push(_do_prev_block)
    1271
    1272      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1273
    1274 -    def get_selected(self):
    1275          """
    1276          Get selected VFSObject instance
    1277          """
    1278
    1279          return self.entries[self.selected]
    1280
    1281      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1282
    1283 -    def get_current(self):
    1284          """
    1285          Get current VFSObject instance
    1286          """
    1287
    1288          return self._vfsobj
    1289
    1290      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1291
    1292 -    def get_tagged(self):
    1293          """
    1294          Return list of tagged VFSObject instances
    1295          """
    1296
    1297          return [self.entries[x] for x in self._tagged]
    1298
    1299      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1300
    1301 -    def get_untagged(self):
    1302          """
    1303          Return list of not tagged VFSObject instances
    1304          """
    1305
    1306          return [self.entries[x] for x in xrange(self._len)
    1307                  if x not in self._tagged]
    1308
    1309      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1310
    1311 -    def toggle_tag(self):
    1312          """
    1313          Toggle tagged selected file
    1314          """
    1315
    1316          if self.selected in self._tagged:
    1317              self._tagged.remove(self.selected)
    1318          else:
    1319              self._tagged.append(self.selected)
    1320
    1321          self.next()
    1322
    1323      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1324
    1325      @refresh
    1326 -    def tag_rule(self):
    1327          """
    1328          Tag files by combined rule
    1329          """
    1330
    1331          self._tag_rule(tag=True)
    1332
    1333      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1334
    1335      @refresh
    1336 -    def untag_rule(self):
    1337          """
    1338          Untag files by combined rule
    1339          """
    1340
    1341          self._tag_rule(tag=False)
    1342
    1343      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1344
    1345 -    def _tag_rule(self, tag=True):
    1346          """
    1347          Tag engine
    1348          """
    1349
    1350          if tag:
    1351              _title = _(u"Tag group")
    1352          else:
    1353              _title = _(u"Untag group")
    1354
    1355          _input = libxyz.ui.InputBox(self.xyz, self.xyz.top,
    1356                                      _("Type FS Rule"),
    1357                                      title=_title, text=self._rule_raw)
    1358
    1359          _raw = _input.show()
    1360
    1361          if _raw is None:
    1362              return
    1363          else:
    1364              self._rule_raw = _raw
    1365
    1366          try:
    1367              _rule = libxyz.core.FSRule(ustring(_raw, self._enc))
    1368          except libxyz.exceptions.ParseError, e:
    1369              xyzlog.error(ustring(str(e)))
    1370              return
    1371
    1372          try:
    1373              if tag:
    1374                  self._tagged = [i for i in xrange(self._len) if
    1375                                  _rule.match(self.entries[i])]
    1376              else:
    1377                  self._tagged = [i for i in self._tagged if not
    1378                                 _rule.match(self.entries[i])]
    1379          except libxyz.exceptions.FSRuleError, e:
    1380              self._tagged = []
    1381
    1382              xyzlog.error(ustring(str(e)))
    1383              return
    1384
    1385      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1386
    1387      @refresh
    1388 -    def tag_invert(self):
    1389          """
    1390          Invert currently tagged files
    1391          """
    1392
    1393          self._tagged = [i for i in xrange(self._len)
    1394                          if i not in self._tagged]
    1395
    1396      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1397
    1398      @refresh
    1399 -    def tag_all(self):
    1400          """
    1401          Tag every single object in current dir
    1402          """
    1403
    1404          self._tagged = [i for i in xrange(self._len) if
    1405                          self.entries[i].name != ".."]
    1406
    1407      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1408
    1409      @refresh
    1410 -    def untag_all(self):
    1411          """
    1412          Untag every single object in current dir
    1413          """
    1414
    1415          self._tagged = []
    1416
    1417      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1418
    1419      @refresh
    1420 -    def reload(self):
    1421          """
    1422          Reload contents
    1423          """
    1424
    1425          _selected = self.entries[self.selected]
    1426
    1427          self._setup(self._vfsobj)
    1428
    1429          if self.selected >= self._len:
    1430              self.selected = self._len - 1
    1431
    1432          # Try to find previously selected object
    1433          if self.entries[self.selected].name != _selected.name:
    1434              self.select(_selected.name)
    1435
    1436      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1437
    1438      @refresh
    1439 -    def select(self, name):
    1440          """
    1441          Select VFS object by given name in current directory
    1442          """
    1443
    1444          for i in xrange(self._len):
    1445              if self.entries[i].name == name:
    1446                  self.selected = i
    1447                  break
    1448
    1449      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1450
    1451 -    def action(self):
    1452          """
    1453          Perform action on selected file
    1454          """
    1455
    1456          _selected = self.entries[self.selected]
    1457
    1458          _action = self.xyz.am.match(_selected)
    1459
    1460          if _action is not None:
    1461              try:
    1462                  _action(_selected)
    1463              except Exception, e:
    1464                  xyzlog.error(_(u"Action error: %s") % (ustring(str(e))))
    1465
    1466      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1467
    1468      @refresh
    1469 -    def chdir(self, path, reload=True, active=True):
    1470          """
    1471          Change directory
    1472          If reload is not True only execute os.chdir, without reloading
    1473          directory contents
    1474          If active is False do not call os.chdir
    1475          """
    1476
    1477          try:
    1478              old_selected = self.entries[self.selected].name
    1479          except IndexError:
    1480              old_selected = None
    1481
    1482          if reload:
    1483              _path = os.path.normpath(path)
    1484              _parent = None
    1485              _old_vfs = None
    1486
    1487              if self.entries:
    1488                  _parent = os.path.normpath(self.entries[0].full_path)
    1489                  _old = self._dir.name
    1490                  _old_vfs = self._vfsobj
    1491
    1492              try:
    1493                  _vfsobj = self.xyz.vfs.dispatch(path, self._enc)
    1494              except libxyz.exceptions.VFSError, e:
    1495                  xyzlog.error(_(u"Unable to chdir to %s: %s") %
    1496                               (ustring(path), ustring(e)))
    1497                  return
    1498
    1499              try:
    1500                  self._setup(_vfsobj)
    1501              except libxyz.exceptions.XYZRuntimeError, e:
    1502                  xyzlog.info(_(u"Unable to chdir to %s: %s") %
    1503                              (ustring(path), ustring(e)))
    1504                  return
    1505
    1506              self.selected = 0
    1507
    1508              # We've just stepped out from dir, try to focus on it
    1509              if _parent == _path:
    1510                  for x in xrange(self._len):
    1511                      if self.entries[x].name == _old:
    1512                          self.selected = x
    1513                          break
    1514
    1515              # TODO: 2-3 level cache
    1516              if _old_vfs:
    1517                  del(_old_vfs)
    1518
    1519          self.cwd = path
    1520          self._tab_data[self.tab_bar.active_tab] = (path, old_selected)
    1521
    1522          if path == os.path.sep:
    1523              new_tab_name = path
    1524          else:
    1525              new_tab_name = os.path.basename(path)
    1526
    1527          self.tab_bar.rename_tab(self.tab_bar.active_tab,
    1528                                  truncate(new_tab_name, 15, self._enc))
    1529
    1530          # Call chdir only for local objects
    1531          if isinstance(self._vfsobj, LocalVFSObject) and active:
    1532              os.chdir(path)
    1533
    1534      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1535
    1536      @refresh
    1537 -    def search_forward(self):
    1538          """
    1539          Search forward for matching object while user types
    1540          """
    1541
    1542          return self._search_engine(lambda x: (xrange(x, self._len)))
    1543
    1544      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1545
    1546      @refresh
    1547 -    def search_backward(self):
    1548          """
    1549          Search backward for matching object while user types
    1550          """
    1551
    1552          return self._search_engine(lambda x: (xrange(x, 0, -1)))
    1553
    1554      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1555
    1556 -    def search_cycle(self):
    1557          """
    1558          Search from current position downwards and then from top to
    1559          currently selected
    1560          """
    1561
    1562          return self._search_engine(lambda x: range(x, self._len) +
    1563                                               range(0, x))
    1564
    1565      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1566
    1567      @refresh
    1568 -    def show_tagged(self):
    1569          """
    1570          Show only tagged entries
    1571          """
    1572
    1573          if not self._tagged:
    1574              return
    1575
    1576          self.entries = [self.entries[x] for x in self._tagged]
    1577          self._len = len(self.entries)
    1578          self.selected = 0
    1579          self._tagged = []
    1580          self._palettes = self._process_skin_rulesets()
    1581
    1582          _tagged = _(u"TAGGED")
    1583
    1584          if not self._title.endswith(_tagged):
    1585              self._title = truncate(u"%s:%s" % (self._title, _tagged),
    1586                                     self.size.cols - 4, self._enc, True)
    1587
    1588              if hasattr(self, "border"):
    1589                  self.border.set_title(self._title)
    1590
    1591          self._force_reload = True
    1592
    1593      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1594
    1595 -    def _search_engine(self, order, pattern=None):
    1596          """
    1597          Search for matching filenames while user types
    1598          @param order: A function that returns generator for search order
    1599          @param pattern: A search type pattern
    1600          """
    1601
    1602          self._cursor_attr = self.attr(u"search")
    1603
    1604          if pattern is None:
    1605              # Search everywhere in object name
    1606              pattern = lambda pat, obj: ustring(pat) in ustring(obj)
    1607              # Search from the beginning of object name
    1608              #pattern = lambda pat, obj: obj.startswith(pat)
    1609
    1610          _dim = self.xyz.screen.get_cols_rows()
    1611          _collected = []
    1612
    1613          _current_pos = self.selected
    1614          _current_pos_orig = self.selected
    1615          _skip = False
    1616
    1617          # Starting internal read loop
    1618          while True:
    1619              self._custom_info = u"".join(_collected)
    1620
    1621              self._invalidate()
    1622              self.xyz.screen.draw_screen(_dim, self.xyz.top.render(_dim, True))
    1623
    1624              try:
    1625                  _raw = self.xyz.input.get()
    1626
    1627                  if self.xyz.input.WIN_RESIZE in _raw:
    1628                      _dim = self.xyz.screen.get_cols_rows()
    1629                      continue
    1630
    1631                  if self._keys.ESCAPE in _raw or self._keys.ENTER in _raw:
    1632                      self._invalidate()
    1633                      break
    1634                  elif self._keys.BACKSPACE in _raw:
    1635                      _current_pos = _current_pos_orig
    1636                      if _collected:
    1637                          _collected.pop()
    1638                  # Continue search
    1639                  elif self._keys.DOWN in _raw:
    1640                      _skip = True
    1641
    1642                  _tmp = _collected[:]
    1643                  _tmp.extend([ustring(x, self._enc) for x in _raw
    1644                               if len(x) == 1])
    1645                  _pattern = u"".join(_tmp)
    1646              except Exception:
    1647                  break
    1648
    1649              # Search
    1650              for i in order(_current_pos):
    1651                  if pattern(_pattern, self.entries[i].name):
    1652                      if _skip:
    1653                          _skip = False
    1654                          _current_pos  = i + 1
    1655                          continue
    1656
    1657                      self.selected = i
    1658                      _collected = _tmp
    1659                      break
    1660
    1661          self._cursor_attr = None
    1662          self._custom_info = None
    1663
    1664      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1665
    1666 -    def _before_switch_tab_hook(self, block, index):
    1667          """
    1668          Before switch tab hook
    1669          """
    1670
    1671          # It's for other block
    1672          if block is not self:
    1673              return
    1674
    1675          try:
    1676              # Save position
    1677              path = self._tab_data[index][0]
    1678              self._tab_data[index] = (path, self.entries[self.selected].name)
    1679
    1680              self.chdir(path)
    1681          except IndexError:
    1682              pass
    1683
    1684      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1685
    1686 -    def _switch_tab_hook(self, block, index):
    1687          """
    1688          Switch tab hook
    1689          """
    1690
    1691          # It's for other block
    1692          if block is not self:
    1693              return
    1694
    1695          try:
    1696              path, name = self._tab_data[index]
    1697
    1698              self.chdir(path)
    1699
    1700              if name is not None:
    1701                  self.select(name)
    1702          except IndexError:
    1703              pass
    1704
    1705      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1706
    1707 -    def _new_tab_hook(self, block, _index):
    1708          """
    1709          New tab hook
    1710          """
    1711
    1712          # It's for other block
    1713          if block is not self:
    1714              return
    1715
    1716          try:
    1717              selected = self.entries[self.selected].name
    1718          except IndexError:
    1719              selected = None
    1720
    1721          self._tab_data.append((self.cwd, selected))
    1722
    1723      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1724
    1725 -    def _del_tab_hook(self, block, index):
    1726          """
    1727          Delete tab hook
    1728          """
    1729
    1730          # It's for other block
    1731          if block is not self:
    1732              return
    1733
    1734          try:
    1735              del(self._tab_data[index])
    1736          except IndexError:
    1737              pass
    1738
    1739      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1740
    1741 -    def get_tabs(self):
    1742          """
    1743          Return list of open tabs
    1744          """
    1745
    1746          return self._tab_data
    1747
    1748  #++++++++++++++++++++++++++++++++++++++++++++++++
    1749
    1750 -class TabBar(lowui.FlowWidget):
    1751      """
    1752      Tabs bar
    1753      """
    1754
    1755 -    def __init__(self, xyz, attr, block):
    1756          self.xyz = xyz
    1757          self.block = block
    1758          self._attr = attr
    1759
    1760          self._active_tab = 0
    1761          self._tabs = []
    1762
    1763          super(TabBar, self).__init__()
    1764
    1765      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1766
    1767      active_tab = property(lambda self: self._active_tab)
    1768
    1769      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1770
    1771      @refresh
    1772 -    def new_tab(self, tabname=None):
    1773          """
    1774          Add new tab
    1775          """
    1776
    1777          if tabname is None:
    1778              tabname = "Tab"
    1779
    1780          self._tabs.append(tabname)
    1781          newidx = len(self._tabs) - 1
    1782
    1783          self.xyz.hm.dispatch(Panel.EVENT_NEW_TAB, self.block, newidx)
    1784          self.switch_tab(newidx)
    1785
    1786      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1787
    1788      @refresh
    1789 -    def del_tab(self, index=None):
    1790          """
    1791          Delete tab by index
    1792          """
    1793
    1794          if index is None:
    1795              index = self._active_tab
    1796
    1797          _len = len(self._tabs)
    1798
    1799          if _len > 1 and index < _len:
    1800              del(self._tabs[index])
    1801
    1802              if self._active_tab >= len(self._tabs):
    1803                  self._active_tab -= 1
    1804
    1805              self.xyz.hm.dispatch(Panel.EVENT_DEL_TAB, self.block, index)
    1806
    1807              self.switch_tab(self._active_tab)
    1808
    1809      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1810
    1811      @refresh
    1812 -    def switch_tab(self, index):
    1813          """
    1814          Switch to tab by index
    1815          """
    1816
    1817          if index < len(self._tabs):
    1818              self.xyz.hm.dispatch(Panel.EVENT_BEFORE_SWITCH_TAB, self.block,
    1819                                   self._active_tab)
    1820              self._active_tab = index
    1821              self.xyz.hm.dispatch(Panel.EVENT_SWITCH_TAB, self.block,
    1822                                   self._active_tab)
    1823
    1824      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1825
    1826      @refresh
    1827 -    def next_tab(self):
    1828          """
    1829          Switch to the next tab
    1830          """
    1831
    1832          index = self._active_tab + 1
    1833
    1834          if index >= len(self._tabs):
    1835              index = 0
    1836
    1837          self.switch_tab(index)
    1838
    1839      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1840
    1841      @refresh
    1842 -    def prev_tab(self):
    1843          """
    1844          Switch to the previous  tab
    1845          """
    1846
    1847          index = self._active_tab - 1
    1848
    1849          if index < 0:
    1850              index = len(self._tabs) - 1
    1851
    1852          self.switch_tab(index)
    1853
    1854      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1855
    1856      @refresh
    1857 -    def rename_tab(self, index, new_name):
    1858          """
    1859          Rename tab at index
    1860          """
    1861
    1862          if index >= len(self._tabs):
    1863              return
    1864          else:
    1865              self._tabs[index] = new_name
    1866
    1867      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1868
    1869 -    def render(self, (maxcol,), focus=False):
    1870          """
    1871          Render the tab bar
    1872          """
    1873
    1874          make_c = lambda text, at: lowui.AttrWrap(
    1875              lowui.Text(text), self._attr(at)).render((maxcol,))
    1876
    1877          canvases = []
    1878
    1879          length = 0
    1880
    1881          for idx in xrange(len(self._tabs)):
    1882              tabname = self._gen_tab_name(self._tabs[idx], idx)
    1883              length += len(tabname)
    1884
    1885              if idx == self._active_tab:
    1886                  canv = make_c(tabname, "tabact")
    1887              else:
    1888                  canv = make_c(tabname, "tabbar")
    1889
    1890              canvases.append((canv, None, False, len(tabname)))
    1891
    1892
    1893          if length < maxcol:
    1894              canvases.append((make_c("", "tabbar"), None, False,
    1895                               maxcol - length))
    1896
    1897          combined = lowui.CanvasJoin(canvases)
    1898
    1899          if length > maxcol:
    1900              more = lowui.AttrWrap(
    1901                  lowui.Text(" >>"),
    1902                  self._attr("tabbar")).render((3,))
    1903
    1904              combined.pad_trim_left_right(0, -(length - maxcol))
    1905              combined.overlay(more, maxcol - 3, 0)
    1906
    1907          return combined
    1908
    1909      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1910
    1911 -    def rows(self, (maxcol,), focus=False):
    1912          """
    1913          Return the number of lines that will be rendered
    1914          """
    1915
    1916          return 1
    1917
    1918      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1919
    1920 -    def _gen_tab_name(self, tab, idx):
    1921          return "{%d %s} " % (idx, tab)
    1922