summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorhut <hut@lepus.uberspace.de>2016-03-30 23:46:06 +0200
committerhut <hut@lepus.uberspace.de>2016-04-01 20:22:13 +0200
commit1134e3ab15c9ec763b446b9903bf8db9828f051c (patch)
treeeb57d658afe539245a5bceec3fcd71bebb703f9f
parentb6be66e8851f6b2b775eec2782e83af8340bfba0 (diff)
downloadranger-1134e3ab15c9ec763b446b9903bf8db9828f051c.tar.gz
Implement Midnight Commander-like mutlipane view
-rw-r--r--ranger/container/settings.py1
-rw-r--r--ranger/core/actions.py4
-rw-r--r--ranger/gui/context.py1
-rw-r--r--ranger/gui/displayable.py6
-rw-r--r--ranger/gui/ui.py59
-rw-r--r--ranger/gui/widgets/browsercolumn.py35
-rw-r--r--ranger/gui/widgets/console.py1
-rw-r--r--ranger/gui/widgets/pager.py1
-rw-r--r--ranger/gui/widgets/view_base.py159
-rw-r--r--ranger/gui/widgets/view_miller.py (renamed from ranger/gui/widgets/browserview.py)132
-rw-r--r--ranger/gui/widgets/view_multipane.py50
11 files changed, 312 insertions, 137 deletions
diff --git a/ranger/container/settings.py b/ranger/container/settings.py
index d7258d6d..36db03ad 100644
--- a/ranger/container/settings.py
+++ b/ranger/container/settings.py
@@ -56,6 +56,7 @@ ALLOWED_SETTINGS = {
     'update_title': bool,
     'update_tmux_title': bool,
     'use_preview_script': bool,
+    'viewmode': str,
     'vcs_aware': bool,
     'vcs_backend_bzr': str,
     'vcs_backend_git': str,
diff --git a/ranger/core/actions.py b/ranger/core/actions.py
index a168a095..825a1cc0 100644
--- a/ranger/core/actions.py
+++ b/ranger/core/actions.py
@@ -566,7 +566,7 @@ class Actions(FileManagerAware, SettingsAware):
     def pager_close(self):
         if self.ui.pager.visible:
             self.ui.close_pager()
-        if self.ui.browser.pager.visible:
+        if hasattr(self.ui.browser, 'pager') and self.ui.browser.pager.visible:
             self.ui.close_embedded_pager()
 
     def taskview_open(self):
@@ -1042,6 +1042,7 @@ class Actions(FileManagerAware, SettingsAware):
         if tab_has_changed:
             self.change_mode('normal')
             self.signal_emit('tab.change', old=previous_tab, new=self.thistab)
+            self.signal_emit('tab.layoutchange')
 
     def tab_close(self, name=None):
         if name is None:
@@ -1056,6 +1057,7 @@ class Actions(FileManagerAware, SettingsAware):
         if name in self.tabs:
             del self.tabs[name]
         self.restorable_tabs.append(tab)
+        self.signal_emit('tab.layoutchange')
 
     def tab_restore(self):
         # NOTE: The name of the tab is not restored.
diff --git a/ranger/gui/context.py b/ranger/gui/context.py
index ac597e5a..2d23d4f1 100644
--- a/ranger/gui/context.py
+++ b/ranger/gui/context.py
@@ -4,6 +4,7 @@
 CONTEXT_KEYS = ['reset', 'error', 'badinfo',
         'in_browser', 'in_statusbar', 'in_titlebar', 'in_console',
         'in_pager', 'in_taskview',
+        'active_pane', 'inactive_pane',
         'directory', 'file', 'hostname',
         'executable', 'media', 'link', 'fifo', 'socket', 'device',
         'video', 'audio', 'image', 'media', 'document', 'container',
diff --git a/ranger/gui/displayable.py b/ranger/gui/displayable.py
index d3adfe50..8415b82c 100644
--- a/ranger/gui/displayable.py
+++ b/ranger/gui/displayable.py
@@ -94,10 +94,8 @@ class Displayable(FileManagerAware, CursesShortcuts):
         """
 
     def destroy(self):
-        """Called when the object is destroyed.
-
-        Override this!
-        """
+        """Called when the object is destroyed."""
+        del self.win
 
     def contains_point(self, y, x):
         """Test whether the point lies inside this object.
diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py
index ad95d754..5d06ca13 100644
--- a/ranger/gui/ui.py
+++ b/ranger/gui/ui.py
@@ -11,6 +11,7 @@ from .displayable import DisplayableContainer
 from .mouse_event import MouseEvent
 from ranger.ext.keybinding_parser import KeyBuffer, KeyMaps, ALT_KEY
 from ranger.ext.lazy_property import lazy_property
+from ranger.ext.signals import Signal
 
 MOUSEMASK = curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION
 
@@ -34,6 +35,8 @@ def _setup_mouse(signal):
         curses.mousemask(0)
 
 class UI(DisplayableContainer):
+    ALLOWED_VIEWMODES = ('multipane', )
+
     is_set_up = False
     load_mode = False
     is_on = False
@@ -44,6 +47,7 @@ class UI(DisplayableContainer):
         self.keymaps = KeyMaps(self.keybuffer)
         self.redrawlock = threading.Event()
         self.redrawlock.set()
+        self._browser_viewmodes = dict()
 
         if fm is not None:
             self.fm = fm
@@ -87,6 +91,7 @@ class UI(DisplayableContainer):
             self.win.addstr("loading...")
             self.win.refresh()
             self._draw_title = curses.tigetflag('hs') # has_status_line
+
         self.update_size()
         self.is_on = True
 
@@ -223,7 +228,7 @@ class UI(DisplayableContainer):
 
     def setup(self):
         """Build up the UI by initializing widgets."""
-        from ranger.gui.widgets.browserview import BrowserView
+        from ranger.gui.widgets.view_miller import ViewMiller
         from ranger.gui.widgets.titlebar import TitleBar
         from ranger.gui.widgets.console import Console
         from ranger.gui.widgets.statusbar import StatusBar
@@ -235,9 +240,10 @@ class UI(DisplayableContainer):
         self.add_child(self.titlebar)
 
         # Create the browser view
-        self.browser = BrowserView(self.win, self.settings.column_ratios)
-        self.settings.signal_bind('setopt.column_ratios',
-                self.browser.change_ratios)
+        self.settings.signal_bind('setopt.viewmode', self._set_viewmode)
+        self._viewmode = None
+        self.viewmode = 'multipane'  # this line sets self.browser implicitly
+                                     # through the signal handler bound above
         self.add_child(self.browser)
 
         # Create the process manager
@@ -339,10 +345,11 @@ class UI(DisplayableContainer):
     def draw_images(self):
         if self.pager.visible:
             self.pager.draw_image()
-        elif self.browser.pager.visible:
-            self.browser.pager.draw_image()
-        else:
-            self.browser.columns[-1].draw_image()
+        elif hasattr(self.browser, 'pager'):
+            if self.browser.pager.visible:
+                self.browser.pager.draw_image()
+            else:
+                self.browser.columns[-1].draw_image()
 
     def close_pager(self):
         if self.console.visible:
@@ -411,7 +418,41 @@ class UI(DisplayableContainer):
         self.status.hint = text
 
     def get_pager(self):
-        if self.browser.pager.visible:
+        if hasattr(self.browser, 'pager') and self.browser.pager.visible:
             return self.browser.pager
         else:
             return self.pager
+
+    def _get_viewmode(self):
+        return self._viewmode
+
+    def _set_viewmode(self, value):
+        if isinstance(value, Signal):
+            value = value.value
+        if value == '':
+            value = self.ALLOWED_VIEWMODES[0]
+        if value in self.ALLOWED_VIEWMODES:
+            if self._viewmode != value:
+                self._viewmode = value
+                if hasattr(self, 'browser'):
+                    self.remove_child(self.browser)
+                if value in self._browser_viewmodes:
+                    self.browser = self._browser_viewmodes[value]
+                else:
+                    browser = self._viewmode_to_class(value)(self.win)
+                    self.browser = self._browser_viewmodes[value] = browser
+                    self.browser.resize(self.y, self.x, self.hei, self.wid)
+                self.add_child(self.browser)
+        else:
+            raise ValueError("Attempting to set invalid viewmode `%s`, should "
+                    "be one of `%s`." % (value, "`, `".join(self.ALLOWED_VIEWMODES)))
+
+    viewmode = property(_get_viewmode, _set_viewmode)
+
+    def _viewmode_to_class(self, viewmode):
+        if viewmode == 'miller':
+            from ranger.gui.widgets.view_miller import ViewMiller
+            return ViewMiller
+        if viewmode == 'multipane':
+            from ranger.gui.widgets.view_multipane import ViewMultipane
+            return ViewMultipane
diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py
index 07830b31..129d8486 100644
--- a/ranger/gui/widgets/browsercolumn.py
+++ b/ranger/gui/widgets/browsercolumn.py
@@ -27,7 +27,7 @@ class BrowserColumn(Pager):
     old_dir = None
     old_thisfile = None
 
-    def __init__(self, win, level):
+    def __init__(self, win, level, tab=None):
         """Initializes a Browser Column Widget
 
         win = the curses window object of the BrowserView
@@ -40,6 +40,7 @@ class BrowserColumn(Pager):
         Pager.__init__(self, win)
         Widget.__init__(self, win)
         self.level = level
+        self.tab = tab
         self.original_level = level
 
         self.settings.signal_bind('setopt.display_size_in_main_column',
@@ -48,9 +49,6 @@ class BrowserColumn(Pager):
     def request_redraw(self):
         self.need_redraw = True
 
-    def resize(self, y, x, hei, wid):
-        Widget.resize(self, y, x, hei, wid)
-
     def click(self, event):
         """Handle a MouseEvent"""
         direction = event.mouse_wheel_direction()
@@ -132,7 +130,11 @@ class BrowserColumn(Pager):
 
     def poke(self):
         Widget.poke(self)
-        self.target = self.fm.thistab.at_level(self.level)
+        if self.tab is None:
+            tab = self.fm.thistab
+        else:
+            tab = self.tab
+        self.target = tab.at_level(self.level)
 
     def draw(self):
         """Call either _draw_file() or _draw_directory()"""
@@ -201,6 +203,15 @@ class BrowserColumn(Pager):
 
         base_color = ['in_browser']
 
+        if self.fm.ui.viewmode == 'multipane' and self.tab is not None:
+            active_pane = self.tab == self.fm.thistab
+            if active_pane:
+                base_color.append('active_pane')
+            else:
+                base_color.append('inactive_pane')
+        else:
+            active_pane = False
+
         self.win.move(0, 0)
 
         if not self.target.content_loaded:
@@ -228,7 +239,7 @@ class BrowserColumn(Pager):
 
         copied = [f.path for f in self.fm.copy_buffer]
 
-        selected_i = self.target.pointer
+        selected_i = self._get_index_of_selected_file()
         for line in range(self.hei):
             i = line + self.scroll_begin
             if line > self.hei:
@@ -258,7 +269,7 @@ class BrowserColumn(Pager):
             key = (self.wid, selected_i == i, drawn.marked, self.main_column,
                    drawn.path in copied, tagged_marker, drawn.infostring,
                    drawn.vcsstatus, drawn.vcsremotestatus, self.target.has_vcschild,
-                   self.fm.do_cut, current_linemode.name, metakey)
+                   self.fm.do_cut, current_linemode.name, metakey, active_pane)
 
             if key in drawn.display_data:
                 self.execute_curses_batch(line, drawn.display_data[key])
@@ -336,6 +347,12 @@ class BrowserColumn(Pager):
             self.execute_curses_batch(line, display_data)
             self.color_reset()
 
+    def _get_index_of_selected_file(self):
+        if self.fm.ui.viewmode == 'multipane' and hasattr(self, 'tab'):
+            return self.tab.pointer
+        else:
+            return self.target.pointer
+
     def _total_len(self, predisplay):
         return sum([len(WideString(s)) for s, L in predisplay])
 
@@ -391,7 +408,7 @@ class BrowserColumn(Pager):
 
     def _draw_directory_color(self, i, drawn, copied):
         this_color = []
-        if i == self.target.pointer:
+        if i == self._get_index_of_selected_file():
             this_color.append('selected')
 
         if drawn.marked:
@@ -431,7 +448,7 @@ class BrowserColumn(Pager):
         dirsize = len(self.target)
         winsize = self.hei
         halfwinsize = winsize // 2
-        index = self.target.pointer or 0
+        index = self._get_index_of_selected_file() or 0
         original = self.target.scroll_begin
         projected = index - original
 
diff --git a/ranger/gui/widgets/console.py b/ranger/gui/widgets/console.py
index 159b8f1e..156298ab 100644
--- a/ranger/gui/widgets/console.py
+++ b/ranger/gui/widgets/console.py
@@ -76,6 +76,7 @@ class Console(Widget):
                     except UnicodeEncodeError:
                         pass
                 f.close()
+        Widget.destroy(self)
 
     def draw(self):
         self.win.erase()
diff --git a/ranger/gui/widgets/pager.py b/ranger/gui/widgets/pager.py
index ce3cc1bf..0d185f98 100644
--- a/ranger/gui/widgets/pager.py
+++ b/ranger/gui/widgets/pager.py
@@ -52,6 +52,7 @@ class Pager(Widget):
 
     def destroy(self):
         self.clear_image(force=True)
+        Widget.destroy(self)
 
     def finalize(self):
         self.fm.ui.win.move(self.y, self.x)
diff --git a/ranger/gui/widgets/view_base.py b/ranger/gui/widgets/view_base.py
new file mode 100644
index 00000000..9fbd9586
--- /dev/null
+++ b/ranger/gui/widgets/view_base.py
@@ -0,0 +1,159 @@
+# This file is part of ranger, the console file manager.
+# License: GNU GPL version 3, see the file "AUTHORS" for details.
+
+"""The base GUI element for views on the directory"""
+
+import curses, _curses
+from ranger.ext.keybinding_parser import key_to_string
+from . import Widget
+from ..displayable import DisplayableContainer
+
+class ViewBase(Widget, DisplayableContainer):
+    draw_bookmarks = False
+    need_clear = False
+    draw_hints = False
+    draw_info = False
+
+    def __init__(self, win):
+        DisplayableContainer.__init__(self, win)
+
+        self.fm.signal_bind('move', self.request_clear)
+        self.old_draw_borders = self.settings.draw_borders
+
+    def request_clear(self):
+        self.need_clear = True
+
+    def draw(self):
+        if self.need_clear:
+            self.win.erase()
+            self.need_redraw = True
+            self.need_clear = False
+        for tab in self.fm.tabs.values():
+            directory = tab.thisdir
+            if directory:
+                directory.load_content_if_outdated()
+                directory.use()
+        DisplayableContainer.draw(self)
+        if self.draw_bookmarks:
+            self._draw_bookmarks()
+        elif self.draw_hints:
+            self._draw_hints()
+        elif self.draw_info:
+            self._draw_info(self.draw_info)
+
+    def finalize(self):
+        if hasattr(self, 'pager') and self.pager.visible:
+            try:
+                self.fm.ui.win.move(self.main_column.y, self.main_column.x)
+            except:
+                pass
+        else:
+            try:
+                x = self.main_column.x
+                y = self.main_column.y + self.main_column.target.pointer\
+                        - self.main_column.scroll_begin
+                self.fm.ui.win.move(y, x)
+            except:
+                pass
+
+    def _draw_bookmarks(self):
+        self.columns[-1].clear_image(force=True)
+        self.fm.bookmarks.update_if_outdated()
+        self.color_reset()
+        self.need_clear = True
+
+        sorted_bookmarks = sorted((item for item in self.fm.bookmarks \
+            if self.fm.settings.show_hidden_bookmarks or \
+            '/.' not in item[1].path), key=lambda t: t[0].lower())
+
+        hei = min(self.hei - 1, len(sorted_bookmarks))
+        ystart = self.hei - hei
+
+        maxlen = self.wid
+        self.addnstr(ystart - 1, 0, "mark  path".ljust(self.wid), self.wid)
+
+        whitespace = " " * maxlen
+        for line, items in zip(range(self.hei-1), sorted_bookmarks):
+            key, mark = items
+            string = " " + key + "   " + mark.path
+            self.addstr(ystart + line, 0, whitespace)
+            self.addnstr(ystart + line, 0, string, self.wid)
+
+        self.win.chgat(ystart - 1, 0, curses.A_UNDERLINE)
+
+    def _draw_info(self, lines):
+        self.columns[-1].clear_image(force=True)
+        self.need_clear = True
+        hei = min(self.hei - 1, len(lines))
+        ystart = self.hei - hei
+        i = ystart
+        whitespace = " " * self.wid
+        for line in lines:
+            if i >= self.hei:
+                break
+            self.addstr(i, 0, whitespace)
+            self.addnstr(i, 0, line, self.wid)
+            i += 1
+
+    def _draw_hints(self):
+        self.columns[-1].clear_image(force=True)
+        self.need_clear = True
+        hints = []
+        for k, v in self.fm.ui.keybuffer.pointer.items():
+            k = key_to_string(k)
+            if isinstance(v, dict):
+                text = '...'
+            else:
+                text = v
+            if text.startswith('hint') or text.startswith('chain hint'):
+                continue
+            hints.append((k, text))
+        hints.sort(key=lambda t: t[1])
+
+        hei = min(self.hei - 1, len(hints))
+        ystart = self.hei - hei
+        self.addnstr(ystart - 1, 0, "key          command".ljust(self.wid),
+                self.wid)
+        try:
+            self.win.chgat(ystart - 1, 0, curses.A_UNDERLINE)
+        except:
+            pass
+        whitespace = " " * self.wid
+        i = ystart
+        for key, cmd in hints:
+            string = " " + key.ljust(11) + " " + cmd
+            self.addstr(i, 0, whitespace)
+            self.addnstr(i, 0, string, self.wid)
+            i += 1
+
+    def _collapse(self):
+        # Should the last column be cut off? (Because there is no preview)
+        if not self.settings.collapse_preview or not self.preview \
+                or not self.stretch_ratios:
+            return False
+        result = not self.columns[-1].has_preview()
+        target = self.columns[-1].target
+        if not result and target and target.is_file:
+            if self.fm.settings.preview_script and \
+                    self.fm.settings.use_preview_script:
+                try:
+                    result = not self.fm.previews[target.realpath]['foundpreview']
+                except:
+                    return self.old_collapse
+
+        self.old_collapse = result
+        return result
+
+    def click(self, event):
+        if DisplayableContainer.click(self, event):
+            return True
+        direction = event.mouse_wheel_direction()
+        if direction:
+            self.main_column.scroll(direction)
+        return False
+
+    def resize(self, y, x, hei, wid):
+        DisplayableContainer.resize(self, y, x, hei, wid)
+
+    def poke(self):
+        DisplayableContainer.poke(self)
diff --git a/ranger/gui/widgets/browserview.py b/ranger/gui/widgets/view_miller.py
index e9640208..b34cc579 100644
--- a/ranger/gui/widgets/browserview.py
+++ b/ranger/gui/widgets/view_miller.py
@@ -1,50 +1,49 @@
 # This file is part of ranger, the console file manager.
 # License: GNU GPL version 3, see the file "AUTHORS" for details.
 
-"""The BrowserView manages a set of BrowserColumns."""
+"""ViewMiller arranges the view in miller columns"""
 
 import curses, _curses
 from ranger.ext.signals import Signal
-from ranger.ext.keybinding_parser import key_to_string
-from . import Widget
 from .browsercolumn import BrowserColumn
 from .pager import Pager
 from ..displayable import DisplayableContainer
+from ranger.gui.widgets.view_base import ViewBase
 
-class BrowserView(Widget, DisplayableContainer):
+class ViewMiller(ViewBase):
     ratios = None
     preview = True
     is_collapsed = False
-    draw_bookmarks = False
     stretch_ratios = None
-    need_clear = False
     old_collapse = False
-    draw_hints = False
-    draw_info = False
 
-    def __init__(self, win, ratios, preview = True):
-        DisplayableContainer.__init__(self, win)
-        self.preview = preview
+    def __init__(self, win):
+        ViewBase.__init__(self, win)
+        self.preview = True
         self.columns = []
 
         self.pager = Pager(self.win, embedded=True)
         self.pager.visible = False
         self.add_child(self.pager)
 
-        self.change_ratios(ratios)
+        self.rebuild()
 
         for option in ('preview_directories', 'preview_files'):
             self.settings.signal_bind('setopt.' + option,
                     self._request_clear_if_has_borders, weak=True)
 
-        self.fm.signal_bind('move', self.request_clear)
         self.settings.signal_bind('setopt.column_ratios', self.request_clear)
+        self.settings.signal_bind('setopt.column_ratios', self.rebuild)
 
         self.old_draw_borders = self.settings.draw_borders
 
-    def change_ratios(self, ratios):
-        if isinstance(ratios, Signal):
-            ratios = ratios.value
+    def rebuild(self):
+        for child in self.container:
+            if isinstance(child, BrowserColumn):
+                self.remove_child(child)
+                child.destroy()
+
+        ratios = self.settings.column_ratios
 
         for column in self.columns:
             column.destroy()
@@ -82,9 +81,6 @@ class BrowserView(Widget, DisplayableContainer):
         if self.settings.draw_borders:
             self.request_clear()
 
-    def request_clear(self):
-        self.need_clear = True
-
     def draw(self):
         if self.need_clear:
             self.win.erase()
@@ -105,21 +101,6 @@ class BrowserView(Widget, DisplayableContainer):
         elif self.draw_info:
             self._draw_info(self.draw_info)
 
-    def finalize(self):
-        if self.pager.visible:
-            try:
-                self.fm.ui.win.move(self.main_column.y, self.main_column.x)
-            except:
-                pass
-        else:
-            try:
-                x = self.main_column.x
-                y = self.main_column.y + self.main_column.target.pointer\
-                        - self.main_column.scroll_begin
-                self.fm.ui.win.move(y, x)
-            except:
-                pass
-
     def _draw_borders(self):
         win = self.win
         self.color('in_browser', 'border')
@@ -180,76 +161,6 @@ class BrowserView(Widget, DisplayableContainer):
         self.addch(0, right_end, curses.ACS_URCORNER)
         self.addch(self.hei - 1, right_end, curses.ACS_LRCORNER)
 
-    def _draw_bookmarks(self):
-        self.columns[-1].clear_image(force=True)
-        self.fm.bookmarks.update_if_outdated()
-        self.color_reset()
-        self.need_clear = True
-
-        sorted_bookmarks = sorted((item for item in self.fm.bookmarks \
-            if self.fm.settings.show_hidden_bookmarks or \
-            '/.' not in item[1].path), key=lambda t: t[0].lower())
-
-        hei = min(self.hei - 1, len(sorted_bookmarks))
-        ystart = self.hei - hei
-
-        maxlen = self.wid
-        self.addnstr(ystart - 1, 0, "mark  path".ljust(self.wid), self.wid)
-
-        whitespace = " " * maxlen
-        for line, items in zip(range(self.hei-1), sorted_bookmarks):
-            key, mark = items
-            string = " " + key + "   " + mark.path
-            self.addstr(ystart + line, 0, whitespace)
-            self.addnstr(ystart + line, 0, string, self.wid)
-
-        self.win.chgat(ystart - 1, 0, curses.A_UNDERLINE)
-
-    def _draw_info(self, lines):
-        self.columns[-1].clear_image(force=True)
-        self.need_clear = True
-        hei = min(self.hei - 1, len(lines))
-        ystart = self.hei - hei
-        i = ystart
-        whitespace = " " * self.wid
-        for line in lines:
-            if i >= self.hei:
-                break
-            self.addstr(i, 0, whitespace)
-            self.addnstr(i, 0, line, self.wid)
-            i += 1
-
-    def _draw_hints(self):
-        self.columns[-1].clear_image(force=True)
-        self.need_clear = True
-        hints = []
-        for k, v in self.fm.ui.keybuffer.pointer.items():
-            k = key_to_string(k)
-            if isinstance(v, dict):
-                text = '...'
-            else:
-                text = v
-            if text.startswith('hint') or text.startswith('chain hint'):
-                continue
-            hints.append((k, text))
-        hints.sort(key=lambda t: t[1])
-
-        hei = min(self.hei - 1, len(hints))
-        ystart = self.hei - hei
-        self.addnstr(ystart - 1, 0, "key          command".ljust(self.wid),
-                self.wid)
-        try:
-            self.win.chgat(ystart - 1, 0, curses.A_UNDERLINE)
-        except:
-            pass
-        whitespace = " " * self.wid
-        i = ystart
-        for key, cmd in hints:
-            string = " " + key.ljust(11) + " " + cmd
-            self.addstr(i, 0, whitespace)
-            self.addnstr(i, 0, string, self.wid)
-            i += 1
-
     def _collapse(self):
         # Should the last column be cut off? (Because there is no preview)
         if not self.settings.collapse_preview or not self.preview \
@@ -270,7 +181,8 @@ class BrowserView(Widget, DisplayableContainer):
 
     def resize(self, y, x, hei, wid):
         """Resize all the columns according to the given ratio"""
-        DisplayableContainer.resize(self, y, x, hei, wid)
+        ViewBase.resize(self, y, x, hei, wid)
+
         borders = self.settings.draw_borders
         pad = 1 if borders else 0
         left = pad
@@ -312,14 +224,6 @@ class BrowserView(Widget, DisplayableContainer):
 
             left += wid
 
-    def click(self, event):
-        if DisplayableContainer.click(self, event):
-            return True
-        direction = event.mouse_wheel_direction()
-        if direction:
-            self.main_column.scroll(direction)
-        return False
-
     def open_pager(self):
         self.pager.visible = True
         self.pager.focused = True
@@ -343,7 +247,7 @@ class BrowserView(Widget, DisplayableContainer):
             pass
 
     def poke(self):
-        DisplayableContainer.poke(self)
+        ViewBase.poke(self)
 
         # Show the preview column when it has a preview but has
         # been hidden (e.g. because of padding_right = False)
diff --git a/ranger/gui/widgets/view_multipane.py b/ranger/gui/widgets/view_multipane.py
new file mode 100644
index 00000000..745a08dd
--- /dev/null
+++ b/ranger/gui/widgets/view_multipane.py
@@ -0,0 +1,50 @@
+# This file is part of ranger, the console file manager.
+# License: GNU GPL version 3, see the file "AUTHORS" for details.
+
+from ranger.gui.widgets.view_base import ViewBase
+from ranger.gui.widgets.browsercolumn import BrowserColumn
+
+class ViewMultipane(ViewBase):
+    def __init__(self, win):
+        ViewBase.__init__(self, win)
+
+        self.fm.signal_bind('tab.layoutchange', self._layoutchange_handler)
+        self.fm.signal_bind('tab.change', self._tabchange_handler)
+        self.rebuild()
+
+    def _layoutchange_handler(self):
+        if self.fm.ui.browser == self:
+            self.rebuild()
+
+    def _tabchange_handler(self, signal):
+        if self.fm.ui.browser == self:
+            if signal.old:
+                signal.old.need_redraw = True
+            if signal.new:
+                signal.new.need_redraw = True
+
+    def rebuild(self):
+        self.columns = []
+
+        for child in self.container:
+            self.remove_child(child)
+            child.destroy()
+        for name, tab in self.fm.tabs.items():
+            column = BrowserColumn(self.win, 0, tab=tab)
+            column.main_column = True
+            if name == self.fm.current_tab:
+                self.main_column = column
+            self.columns.append(column)
+            self.add_child(column)
+        self.resize(self.y, self.x, self.hei, self.wid)
+
+    def resize(self, y, x, hei, wid):
+        ViewBase.resize(self, y, x, hei, wid)
+        column_width = int(float(wid) / len(self.columns))
+        left = 0
+        top = 0
+        for i, column in enumerate(self.columns):
+            column.resize(top, left, hei, max(1, column_width - 1))
+            left += column_width
+            column.need_redraw = True
+        self.need_redraw = True