about summary refs log blame commit diff stats
path: root/ranger/gui/widgets/browsercolumn.py
blob: ca5710ac77256467b9d053fc50f7660eb608f28a (plain) (tree)
1
2
3
4
5
6
7
8
9
                        
                                                             
                                                                        
 
                                                                            
             
           

                     
                    
                        
                                    
                                            
 
                           


























































































































































































































































































































































































                                                                                 
# -*- encoding: utf8 -*-
# Copyright (C) 2009-2013  Roman Zimbelmann <hut@lavabit.com>
# This software is distributed under the terms of the GNU GPL version 3.

"""The BrowserColumn widget displays the contents of a directory or file."""
import curses
import stat
from time import time

from . import Widget
from .pager import Pager
from ranger.fsobject import BAD_INFO
from ranger.ext.widestring import WideString

class BrowserColumn(Pager):
    main_column = False
    display_infostring = False
    scroll_begin = 0
    target = None
    last_redraw_time = -1
    ellipsis = { False: '~', True: '…' }

    old_dir = None
    old_thisfile = None

    def __init__(self, win, level):
        """
        win = the curses window object of the BrowserView
        level = what to display?

        level >0 => previews
        level 0 => current file/directory
        level <0 => parent directories
        """
        Pager.__init__(self, win)
        Widget.__init__(self, win)
        self.level = level
        self.original_level = level

        self.settings.signal_bind('setopt.display_size_in_main_column',
                self.request_redraw, weak=True)

    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()
        if not (event.pressed(1) or event.pressed(3) or direction):
            return False

        if self.target is None:
            pass

        elif self.target.is_directory:
            if self.target.accessible and self.target.content_loaded:
                index = self.scroll_begin + event.y - self.y

                if direction:
                    if self.level == -1:
                        self.fm.move_parent(direction)
                    else:
                        return False
                elif event.pressed(1):
                    if not self.main_column:
                        self.fm.enter_dir(self.target.path)

                    if index < len(self.target):
                        self.fm.move(to=index)
                elif event.pressed(3):
                    try:
                        clicked_file = self.target.files[index]
                        if clicked_file.is_directory:
                            self.fm.enter_dir(clicked_file.path)
                        elif self.level == 0:
                            self.fm.thisdir.move_to_obj(clicked_file)
                            self.fm.execute_file(clicked_file)
                    except:
                        pass

        else:
            if self.level > 0 and not direction:
                self.fm.move(right=0)

        return True

    def execute_curses_batch(self, line, commands):
        """
        Executes a list of "commands" which can be easily cached.

        "commands" is a list of lists.  Each element contains
        a text and an attribute.  First, the attribute will be
        set with attrset, then the text is printed.

        Example:
        execute_curses_batch(0, [["hello ", 0], ["world", curses.A_BOLD]])
        """
        try:
            self.win.move(line, 0)
        except:
            return
        for entry in commands:
            text, attr = entry
            self.addstr(text, attr)

    def has_preview(self):
        if self.target is None:
            return False

        if self.target.is_file:
            if not self.target.has_preview():
                return False

        if self.target.is_directory:
            if self.level > 0 and not self.settings.preview_directories:
                return False

        return True

    def level_shift(self, amount):
        self.level = self.original_level + amount

    def level_restore(self):
        self.level = self.original_level

    def poke(self):
        Widget.poke(self)
        self.target = self.fm.thistab.at_level(self.level)

    def draw(self):
        """Call either _draw_file() or _draw_directory()"""
        if self.target != self.old_dir:
            self.need_redraw = True
            self.old_dir = self.target

        if self.target:  # don't garbage collect this directory please
            self.target.use()

        if self.target and self.target.is_directory \
                and (self.level <= 0 or self.settings.preview_directories):
            if self.target.pointed_obj != self.old_thisfile:
                self.need_redraw = True
                self.old_thisfile = self.target.pointed_obj

            if self.target.load_content_if_outdated() \
            or self.target.sort_if_outdated() \
            or self.last_redraw_time < self.target.last_update_time:
                self.need_redraw = True

        if self.need_redraw:
            self.win.erase()
            if self.target is None:
                pass
            elif self.target.is_file:
                Pager.open(self)
                self._draw_file()
            elif self.target.is_directory:
                self._draw_directory()
                Widget.draw(self)
            self.need_redraw = False
            self.last_redraw_time = time()

    def _draw_file(self):
        """Draw a preview of the file, if the settings allow it"""
        self.win.move(0, 0)
        if not self.target.accessible:
            self.addnstr("not accessible", self.wid)
            Pager.close(self)
            return

        if self.target is None or not self.target.has_preview():
            Pager.close(self)
            return

        if self.fm.settings.preview_images and self.target.image:
            self.set_image(self.target.realpath)
            Pager.draw(self)
        else:
            f = self.target.get_preview_source(self.wid, self.hei)
            if f is None:
                Pager.close(self)
            else:
                self.set_source(f)
                Pager.draw(self)

    def _draw_directory(self):
        """Draw the contents of a directory"""
        if self.image:
            self.image = None
            self.need_clear_image = True
            Pager.clear_image(self)

        if self.level > 0 and not self.settings.preview_directories:
            return

        base_color = ['in_browser']

        self.win.move(0, 0)

        if not self.target.content_loaded:
            self.color(tuple(base_color))
            self.addnstr("...", self.wid)
            self.color_reset()
            return

        if self.main_column:
            base_color.append('main_column')

        if not self.target.accessible:
            self.color(tuple(base_color + ['error']))
            self.addnstr("not accessible", self.wid)
            self.color_reset()
            return

        if self.target.empty():
            self.color(tuple(base_color + ['empty']))
            self.addnstr("empty", self.wid)
            self.color_reset()
            return

        self._set_scroll_begin()

        copied = [f.path for f in self.fm.copy_buffer]
        ellipsis = self.ellipsis[self.settings.unicode_ellipsis]

        selected_i = self.target.pointer
        for line in range(self.hei):
            i = line + self.scroll_begin
            if line > self.hei:
                break

            try:
                drawn = self.target.files[i]
            except IndexError:
                break

            tagged = self.fm.tags and drawn.realpath in self.fm.tags
            if tagged:
                tagged_marker = self.fm.tags.marker(drawn.realpath)
            else:
                tagged_marker = " "

            key = (self.wid, selected_i == i, drawn.marked, self.main_column,
                    drawn.path in copied, tagged_marker, drawn.infostring,
                    self.fm.do_cut)

            if key in drawn.display_data:
                self.execute_curses_batch(line, drawn.display_data[key])
                self.color_reset()
                continue

            display_data = []
            drawn.display_data[key] = display_data

            if self.display_infostring and drawn.infostring \
                    and self.settings.display_size_in_main_column:
                infostring = str(drawn.infostring) + " "
            else:
                infostring = ""

            this_color = base_color + list(drawn.mimetype_tuple)
            text = drawn.basename

            space = self.wid - len(infostring)
            if self.main_column:
                space -= 2
            elif self.settings.display_tags_in_all_columns:
                space -= 1

            if i == selected_i:
                this_color.append('selected')

            if drawn.marked:
                this_color.append('marked')
                if self.main_column or self.settings.display_tags_in_all_columns:
                    text = " " + text

            if tagged:
                this_color.append('tagged')

            if drawn.is_directory:
                this_color.append('directory')
            else:
                this_color.append('file')

            if drawn.stat:
                mode = drawn.stat.st_mode
                if mode & stat.S_IXUSR:
                    this_color.append('executable')
                if stat.S_ISFIFO(mode):
                    this_color.append('fifo')
                if stat.S_ISSOCK(mode):
                    this_color.append('socket')
                if drawn.is_device:
                    this_color.append('device')

            if drawn.path in copied:
                this_color.append('cut' if self.fm.do_cut else 'copied')

            if drawn.is_link:
                this_color.append('link')
                this_color.append(drawn.exists and 'good' or 'bad')

            attr = self.settings.colorscheme.get_attr(*this_color)

            if (self.main_column or self.settings.display_tags_in_all_columns) \
                    and tagged and self.wid > 2:
                this_color.append('tag_marker')
                tag_attr = self.settings.colorscheme.get_attr(*this_color)
                display_data.append([tagged_marker, tag_attr])
            else:
                text = " " + text
                space += 1

            wtext = WideString(text)
            if len(wtext) > space:
                wtext = wtext[:max(0, space - 1)] + ellipsis
            text = str(wtext)

            display_data.append([text, attr])

            padding = self.wid - len(wtext)
            if tagged and (self.main_column or \
                    self.settings.display_tags_in_all_columns):
                padding -= 1
            if infostring:
                if len(wtext) + 1 + len(infostring) > self.wid:
                    pass
                else:
                    padding -= len(infostring)
                    padding = max(0, padding)
                    infostring = (" " * padding) + infostring
                    display_data.append([infostring, attr])
            else:
                display_data.append([" " * max(0, padding), attr])

            self.execute_curses_batch(line, display_data)
            self.color_reset()

    def _get_scroll_begin(self):
        """Determines scroll_begin (the position of the first displayed file)"""
        offset = self.settings.scroll_offset
        dirsize = len(self.target)
        winsize = self.hei
        halfwinsize = winsize // 2
        index = self.target.pointer or 0
        original = self.target.scroll_begin
        projected = index - original

        upper_limit = winsize - 1 - offset
        lower_limit = offset

        if original < 0:
            return 0

        if dirsize < winsize:
            return 0

        if halfwinsize < offset:
            return min( dirsize - winsize, max( 0, index - halfwinsize ))

        if original > dirsize - winsize:
            self.target.scroll_begin = dirsize - winsize
            return self._get_scroll_begin()

        if projected < upper_limit and projected > lower_limit:
            return original

        if projected > upper_limit:
            return min( dirsize - winsize,
                    original + (projected - upper_limit))

        if projected < upper_limit:
            return max( 0,
                    original - (lower_limit - projected))

        return original

    def _set_scroll_begin(self):
        """Updates the scroll_begin value"""
        self.scroll_begin = self._get_scroll_begin()
        self.target.scroll_begin = self.scroll_begin

    def scroll(self, n):
        """scroll down by n lines"""
        self.need_redraw = True
        self.target.move(down=n)
        self.target.scroll_begin += 3 * n

    def __str__(self):
        return self.__class__.__name__ + ' at level ' + str(self.level)