From d88232a386dbc2153aa12bea7f30c9c53c414010 Mon Sep 17 00:00:00 2001 From: hut Date: Fri, 11 Dec 2009 22:30:07 +0100 Subject: Reorganization of gui/widget hierarchy and directory structure --- ranger/actions.py | 8 +- ranger/applications.py | 4 +- ranger/defaults/keys.py | 22 ++-- ranger/environment.py | 3 +- ranger/fm.py | 7 +- ranger/gui/color.py | 3 +- ranger/gui/defaultui.py | 36 +++---- ranger/gui/displayable.py | 167 ++++++++++++++++++++++++++++ ranger/gui/mouse_event.py | 16 +++ ranger/gui/ui.py | 140 +++++++++++------------- ranger/gui/wconsole.py | 239 ----------------------------------------- ranger/gui/wdisplay.py | 191 -------------------------------- ranger/gui/widget.py | 74 ------------- ranger/gui/widgets/__init__.py | 0 ranger/gui/widgets/console.py | 239 +++++++++++++++++++++++++++++++++++++++++ ranger/gui/widgets/filelist.py | 195 +++++++++++++++++++++++++++++++++ ranger/gui/widgets/titlebar.py | 38 +++++++ ranger/gui/wtitlebar.py | 45 -------- ranger/main.py | 6 +- test/tc_directory.py | 2 +- 20 files changed, 768 insertions(+), 667 deletions(-) create mode 100644 ranger/gui/displayable.py create mode 100644 ranger/gui/mouse_event.py delete mode 100644 ranger/gui/wconsole.py delete mode 100644 ranger/gui/wdisplay.py delete mode 100644 ranger/gui/widget.py create mode 100644 ranger/gui/widgets/__init__.py create mode 100644 ranger/gui/widgets/console.py create mode 100644 ranger/gui/widgets/filelist.py create mode 100644 ranger/gui/widgets/titlebar.py delete mode 100644 ranger/gui/wtitlebar.py diff --git a/ranger/actions.py b/ranger/actions.py index 54a88b11..a36b0c56 100644 --- a/ranger/actions.py +++ b/ranger/actions.py @@ -20,7 +20,7 @@ class Actions(EnvironmentAware, SettingsAware): raise SystemExit() def resize(self): - self.ui.resize() + self.ui.update_size() def exit(self): raise SystemExit() @@ -57,7 +57,7 @@ class Actions(EnvironmentAware, SettingsAware): self.env.history_go(relative) def handle_mouse(self): - self.ui.handle_mouse(self) + self.ui.handle_mouse() def execute_file(self, files, app = '', flags = '', mode = 0): if type(files) not in (list, tuple): @@ -78,7 +78,7 @@ class Actions(EnvironmentAware, SettingsAware): self.execute_file(self.env.cf, app = 'editor') def open_console(self, mode = ':'): - if self.ui.can('open_console'): + if hasattr(self.ui, 'open_console'): self.ui.open_console(mode) def move_pointer(self, relative = 0, absolute = None): @@ -89,7 +89,7 @@ class Actions(EnvironmentAware, SettingsAware): relative = int(relative * self.env.termsize[0])) def scroll(self, relative): - if self.ui.can('scroll'): + if hasattr(self.ui, 'scroll'): self.ui.scroll(relative) self.env.cf = self.env.pwd.pointed_file diff --git a/ranger/applications.py b/ranger/applications.py index 99a40e93..85dc50f5 100644 --- a/ranger/applications.py +++ b/ranger/applications.py @@ -50,8 +50,8 @@ def run(*args, **kw): return process else: - if fm.ui is not None: fm.ui.exit() + if fm.ui: fm.ui.destroy() p = Popen(args, **popen_kw) waitpid_no_intr(p.pid) - if fm.ui is not None: fm.ui.initialize() + if fm.ui: fm.ui.initialize() return p diff --git a/ranger/defaults/keys.py b/ranger/defaults/keys.py index 2f623bca..5edae929 100644 --- a/ranger/defaults/keys.py +++ b/ranger/defaults/keys.py @@ -90,7 +90,7 @@ def initialize_commands(command_list): def initialize_console_commands(command_list): from ranger.actions import Actions as do - from ranger.gui.wconsole import WConsole + from ranger.gui.widgets.console import Console def bind(fnc, *keys): command_list.bind(fnc, *keys) @@ -107,18 +107,18 @@ def initialize_console_commands(command_list): c = curry # movement - bind(c(WConsole.move, relative = -1), curses.KEY_LEFT, ctrl('b')) - bind(c(WConsole.move, relative = 1), curses.KEY_RIGHT, ctrl('f')) - bind(c(WConsole.move, absolute = 0), curses.KEY_HOME, ctrl('a')) - bind(c(WConsole.move, absolute = -1), curses.KEY_END, ctrl('e')) - bind(c(WConsole.delete, 0), curses.KEY_DC, ctrl('d')) - bind(c(WConsole.delete, -1), curses.KEY_BACKSPACE, 127, ctrl('h')) - bind(c(WConsole.delete_rest, -1), ctrl('U')) - bind(c(WConsole.delete_rest, 1), ctrl('K')) + bind(c(Console.move, relative = -1), curses.KEY_LEFT, ctrl('b')) + bind(c(Console.move, relative = 1), curses.KEY_RIGHT, ctrl('f')) + bind(c(Console.move, absolute = 0), curses.KEY_HOME, ctrl('a')) + bind(c(Console.move, absolute = -1), curses.KEY_END, ctrl('e')) + bind(c(Console.delete, 0), curses.KEY_DC, ctrl('d')) + bind(c(Console.delete, -1), curses.KEY_BACKSPACE, 127, ctrl('h')) + bind(c(Console.delete_rest, -1), ctrl('U')) + bind(c(Console.delete_rest, 1), ctrl('K')) # system functions - bind(c(WConsole.close), ESC, ctrl('C')) - bind(WConsole.execute, curses.KEY_ENTER, ctrl('j')) + bind(c(Console.close), ESC, ctrl('C')) + bind(Console.execute, curses.KEY_ENTER, ctrl('j')) bind(c_fm(do.redraw), ctrl('L')) bind(c_fm(do.resize), curses.KEY_RESIZE) diff --git a/ranger/environment.py b/ranger/environment.py index d7097fc8..9e2c2877 100644 --- a/ranger/environment.py +++ b/ranger/environment.py @@ -15,7 +15,8 @@ class Environment(SettingsAware): self.cf = None # current file self.keybuffer = KeyBuffer() self.copy = None - self.termsize = (24, 80) + self.termsize = None +# self.termsize = (24, 80) self.history = History(self.settings.max_history_size) from ranger.shared import EnvironmentAware diff --git a/ranger/fm.py b/ranger/fm.py index cdcfb8d7..dd74342f 100644 --- a/ranger/fm.py +++ b/ranger/fm.py @@ -3,6 +3,7 @@ from ranger.container import Bookmarks from ranger import __version__ USAGE = '''%s [options] [path/filename]''' +CTRL_C = 3 class FM(Actions): def __init__(self, ui = None, bookmarks = None): @@ -35,8 +36,10 @@ class FM(Actions): try: self.bookmarks.reload_if_outdated() self.ui.draw() + self.ui.finalize() + key = self.ui.get_next_key() - self.ui.press(key, self) + self.ui.handle_key(key) gc_tick += 1 if gc_tick > 10: @@ -44,4 +47,4 @@ class FM(Actions): self.env.garbage_collect() except KeyboardInterrupt: - self.ui.press(3, self) + self.ui.handle_key(CTRL_C, self) diff --git a/ranger/gui/color.py b/ranger/gui/color.py index 214383ee..1318ab6f 100644 --- a/ranger/gui/color.py +++ b/ranger/gui/color.py @@ -1,9 +1,10 @@ +"""Contains abbreviations to curses' color/attribute constants.""" import curses COLOR_PAIRS = {10: 0} def get_color(fg, bg): - import curses + """Returns the color pair for the given fg/bg combination.""" c = bg+2 + 9*(fg + 2) diff --git a/ranger/gui/defaultui.py b/ranger/gui/defaultui.py index 547f20ff..8d9f7ded 100644 --- a/ranger/gui/defaultui.py +++ b/ranger/gui/defaultui.py @@ -4,28 +4,29 @@ RATIO = ( 0.15, 0.15, 0.4, 0.3 ) from ranger.gui.ui import UI as SuperClass class DefaultUI(SuperClass): def setup(self): - from ranger.gui.wdisplay import WDisplay - from ranger.gui.wtitlebar import WTitleBar - from ranger.gui.wconsole import WConsole - self.titlebar = WTitleBar(self.win, self.colorscheme) - self.add_widget(self.titlebar) + from ranger.gui.widgets.filelist import FileList + from ranger.gui.widgets.titlebar import TitleBar + from ranger.gui.widgets.console import Console + self.titlebar = TitleBar(self.win) + self.add_obj(self.titlebar) self.displays = [ - WDisplay(self.win, self.colorscheme, -2), - WDisplay(self.win, self.colorscheme, -1), - WDisplay(self.win, self.colorscheme, 0), - WDisplay(self.win, self.colorscheme, 1) ] + FileList(self.win, -2), + FileList(self.win, -1), + FileList(self.win, 0), + FileList(self.win, 1) ] self.main_display = self.displays[2] self.displays[2].display_infostring = True self.displays[2].main_display = True for disp in self.displays: - self.add_widget(disp) + self.add_obj(disp) - self.console = WConsole(self.win, self.colorscheme) - self.add_widget(self.console) + self.console = Console(self.win) + self.add_obj(self.console) - def resize(self): - SuperClass.resize(self) + def update_size(self): + """resize all widgets""" + SuperClass.update_size(self) y, x = self.win.getmaxyx() leftborder = 0 @@ -34,16 +35,15 @@ class DefaultUI(SuperClass): for ratio in RATIO: wid = int(ratio * x) try: - self.displays[i].setdim(1, leftborder, y-2, wid - 1) + self.displays[i].resize(1, leftborder, y-2, wid - 1) except KeyError: pass leftborder += wid i += 1 - self.titlebar.setdim(0, 0, 1, x) - self.console.setdim(y-1, 0, 1, x) + self.titlebar.resize(0, 0, 1, x) + self.console.resize(y-1, 0, 1, x) - # ---specials--- def open_console(self, mode): if self.console.open(mode): self.console.on_close = self.close_console diff --git a/ranger/gui/displayable.py b/ranger/gui/displayable.py new file mode 100644 index 00000000..4ac8c3fc --- /dev/null +++ b/ranger/gui/displayable.py @@ -0,0 +1,167 @@ +from ranger.shared import FileManagerAware, EnvironmentAware, SettingsAware + +class Displayable(EnvironmentAware, FileManagerAware, SettingsAware): + focused = False + visible = True + win = None + colorscheme = None + + def __init__(self, win): + self.resize(0, 0, 0, 0) + self.colorscheme = self.env.settings.colorscheme + + if win is not None: + self.win = win + + def __nonzero__(self): + """Always True""" + return True + + def color(self, keylist = None, *keys): + """Change the colors from now on.""" + keys = combine(keylist, keys) + self.win.attrset(self.colorscheme.get_attr(*keys)) + + def color_at(self, y, x, wid, keylist = None, *keys): + """Change the colors at the specified position""" + keys = combine(keylist, keys) + self.win.chgat(y, x, wid, self.colorscheme.get_attr(*keys)) + + def color_reset(self): + """Change the colors to the default colors""" + Displayable.color(self, 'reset') + + def draw(self): + """Draw the object. Called on every main iteration. +Containers should call draw() on their contained objects here. +Override this!""" + + def destroy(self): + """Called when the object is destroyed. +Override this!""" + + def contains_point(self, y, x): + """Test if the point lies within the boundaries of this object""" + return (x >= self.x and x < self.x + self.wid) and \ + (y >= self.y and y < self.y + self.hei) + + def click(self, event): + """Called when a mouse key is pressed and self.focused is True. +Override this!""" + pass + + def press(self, key): + """Called when a key is pressed and self.focused is True. +Override this!""" + pass + + def draw(self): + """Draw displayable. Called on every main iteration. +Override this!""" + pass + + def finalize(self): + """Called after every displayable is done drawing. +Override this!""" + pass + + def resize(self, y, x, hei=None, wid=None): + """Resize the widget""" + try: + maxy, maxx = self.env.termsize + except TypeError: + pass + else: + wid = wid or maxx - x + hei = hei or maxy - y + + if x + wid > maxx and y + hei > maxy: + raise OutOfBoundsException("X and Y out of bounds!") + + if x + wid > maxx: + raise OutOfBoundsException("X out of bounds!") + + if y + hei > maxy: + raise OutOfBoundsException("Y out of bounds!") + + self.x = x + self.y = y + self.wid = wid + self.hei = hei + + +class DisplayableContainer(Displayable): + container = None + def __init__(self, win): + Displayable.__init__(self, win) + self.container = [] + + def draw(self): + """Recursively called on objects in container""" + for displayable in self.container: + if displayable.visible: + displayable.draw() + + def finalize(self): + """Recursively called on objects in container""" + for displayable in self.container: + if displayable.visible: + displayable.finalize() + + def get_focused_obj(self): + """Finds a focused displayable object in the container.""" + for displayable in self.container: + if displayable.focused: + return displayable + try: + obj = displayable.get_focused_obj() + except AttributeError: + pass + else: + if obj is not None: + return obj + return None + + def press(self, key): + """Recursively called on objects in container""" + focused_obj = self.get_focused_obj() + + if focused_obj: + focused_obj.press(key) + return True + return False + + def click(self, event): + """Recursively called on objects in container""" + focused_obj = self.get_focused_obj() + if focused_obj: + focused_obj.press(key) + return True + return False + + def add_obj(self, obj): + self.container.append(obj) + + def destroy(self): + """Recursively called on objects in container""" + for displayable in self.container: + displayable.destroy() + +# def resize(self): +# """Recursively called on objects in container""" +# for displayable in container: +# displayable.resize() + +class OutOfBoundsException(Exception): + pass + +def combine(seq, tup): + """Add seq and tup. Ensures that the result is a tuple.""" + try: + if isinstance(seq, str): raise TypeError + return tuple(tuple(seq) + tup) + except TypeError: + try: + return tuple((seq, ) + tup) + except: + return () diff --git a/ranger/gui/mouse_event.py b/ranger/gui/mouse_event.py new file mode 100644 index 00000000..fa25f5f0 --- /dev/null +++ b/ranger/gui/mouse_event.py @@ -0,0 +1,16 @@ +class MouseEvent(object): + import curses + PRESSED = [ 0, + curses.BUTTON1_PRESSED, + curses.BUTTON2_PRESSED, + curses.BUTTON3_PRESSED, + curses.BUTTON4_PRESSED ] + + def __init__(self, getmouse): + _, self.x, self.y, _, self.bstate = getmouse + + def pressed(self, n): + try: + return (self.bstate & MouseEvent.PRESSED[n]) != 0 + except: + return False diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py index c7c10cfe..8abd39df 100644 --- a/ranger/gui/ui.py +++ b/ranger/gui/ui.py @@ -1,26 +1,11 @@ import curses -class MouseEvent(object): - import curses - PRESSED = [ 0, - curses.BUTTON1_PRESSED, - curses.BUTTON2_PRESSED, - curses.BUTTON3_PRESSED, - curses.BUTTON4_PRESSED ] - - def __init__(self, getmouse): - _, self.x, self.y, _, self.bstate = getmouse - - def pressed(self, n): - try: - return (self.bstate & MouseEvent.PRESSED[n]) != 0 - except: - return False - -from ranger.shared import EnvironmentAware, SettingsAware +from .displayable import DisplayableContainer +from .mouse_event import MouseEvent from ranger.container import CommandList -class UI(EnvironmentAware, SettingsAware): +class UI(DisplayableContainer): + is_set_up = False def __init__(self, commandlist = None): import os os.environ['ESCDELAY'] = '25' # don't know a cleaner way @@ -30,13 +15,12 @@ class UI(EnvironmentAware, SettingsAware): self.settings.keys.initialize_commands(self.commandlist) else: self.commandlist = commandlist - self.colorscheme = self.env.settings.colorscheme - self.is_set_up = False self.win = curses.initscr() - self.widgets = [] + DisplayableContainer.__init__(self, None) def initialize(self): + """initialize curses, then call setup (at the first time) and resize.""" self.win.leaveok(0) self.win.keypad(1) @@ -46,6 +30,7 @@ class UI(EnvironmentAware, SettingsAware): curses.curs_set(0) curses.start_color() curses.use_default_colors() + curses.mouseinterval(0) mask = curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION avail, old = curses.mousemask(mask) @@ -54,63 +39,37 @@ class UI(EnvironmentAware, SettingsAware): if not self.is_set_up: self.is_set_up = True self.setup() - self.resize() - - def exit(self): - from ranger import log - log("exiting ui!") - self.win.keypad(0) - curses.nocbreak() - curses.echo() - curses.curs_set(1) - curses.mousemask(0) - curses.endwin() + self.update_size() - def handle_mouse(self, fm): + def handle_mouse(self): + """Handles mouse input""" try: event = MouseEvent(curses.getmouse()) except: return + from ranger import log + log(str(event.bstate)) + if event.pressed(1) or event.pressed(3): - for widg in self.widgets: - if widg.contains_point(event.y, event.x): - widg.click(event, fm) + for displayable in self.container: + if displayable.contains_point(event.y, event.x): + displayable.click(event) break - if event.pressed(4) or event.pressed(2) or event.bstate & 134217728: +# if event.pressed(4) or event.pressed(2) or event.bstate & 134217728: + if event.pressed(4) or event.pressed(2): if event.pressed(4): - fm.scroll(relative = -3) + self.fm.scroll(relative = -3) else: - fm.scroll(relative = 3) + self.fm.scroll(relative = 3) - def can(self, attr): - return hasattr(self, attr) - - def setup(self): - pass - - def resize(self): - self.env.termsize = self.win.getmaxyx() - - def redraw(self): - self.win.redrawwin() - self.win.refresh() - self.win.redrawwin() - - def add_widget(self, widg): - self.widgets.append(widg) - - def feed_env(self, env): - self.env = env - - def press(self, key, fm): + def handle_key(self, key): + """Handles key input""" self.env.key_append(key) - for widg in self.widgets: - if widg.focused: - widg.press(key, fm, self.env) - return + if DisplayableContainer.press(self, key): + return try: cmd = self.commandlist.paths[tuple(self.env.keybuffer)] @@ -121,21 +80,48 @@ class UI(EnvironmentAware, SettingsAware): if cmd == self.commandlist.dummy_object: return - cmd.execute(fm) + cmd.execute(self.fm) self.env.key_clear() - def draw(self): - self.win.erase() - for widg in self.widgets: - widg.feed_env(self.env) - if widg.visible: - widg.draw() - for widg in self.widgets: - if widg.visible: - widg.finalize() - self.win.refresh() - def get_next_key(self): + """Waits for key input and returns the pressed key""" key = self.win.getch() curses.flushinp() return key + + def setup(self): + """Called after an initialize() call. +Override this!""" + + def redraw(self): + """Redraw the window. This only calls self.win.redrawwin().""" + self.win.redrawwin() + self.win.refresh() + self.win.redrawwin() + + def update_size(self): + """Update self.env.termsize. +Extend this method to resize all widgets!""" + self.env.termsize = self.win.getmaxyx() + + def draw(self): + """Erase the window, then draw all objects in the container""" + self.win.erase() + DisplayableContainer.draw(self) + + def finalize(self): + """Finalize every object in container and refresh the window""" + DisplayableContainer.finalize(self) + self.win.refresh() + + def destroy(self): + """Destroy all widgets and turn off curses""" +# DisplayableContainer.destroy(self) + from ranger import log + log("exiting ui!") + self.win.keypad(0) + curses.nocbreak() + curses.echo() + curses.curs_set(1) +# curses.mousemask(0) + curses.endwin() diff --git a/ranger/gui/wconsole.py b/ranger/gui/wconsole.py deleted file mode 100644 index 581745b4..00000000 --- a/ranger/gui/wconsole.py +++ /dev/null @@ -1,239 +0,0 @@ -from ranger.gui.widget import Widget as SuperClass -import curses - -CONSOLE_MODES = tuple(':@/?>!') -CONSOLE_MODES_DICTIONARY = { '@': 'open with: ' } - -class WConsole(SuperClass): - def __init__(self, win, colorscheme): - from ranger.container import CommandList - SuperClass.__init__(self, win, colorscheme) - self.mode = None - self.visible = False - self.commandlist = CommandList() - self.settings.keys.initialize_console_commands(self.commandlist) - self.last_cursor_mode = 1 - self.clear() - self.prompt = None - self.execute_funcs = { - ':': WConsole.execute_command, - '@': WConsole.execute_openwith_quick, - '/': WConsole.execute_search, - '?': WConsole.execute_search, - '>': WConsole.execute_noreturn, - '!': WConsole.execute_openwith } - - def feed_env(self, env): - self.cf = env.cf - - def draw(self): - if self.mode is None: - return - self.win.addstr(self.y, self.x, self.prompt + self.line) - - def finalize(self): - try: - self.win.move(self.y, self.x + self.pos + len(self.prompt)) - except: - pass - - def open(self, mode): - if mode not in CONSOLE_MODES: - return False - - self.last_cursor_mode = curses.curs_set(1) - self.mode = mode - try: - self.prompt = CONSOLE_MODES_DICTIONARY[self.mode] - except KeyError: - self.prompt = self.mode - self.focused = True - self.visible = True - return True - - def close(self): - curses.curs_set(self.last_cursor_mode) - self.focused = False - self.visible = False - if hasattr(self, 'on_close'): - self.on_close() - - def clear(self): - self.pos = 0 - self.line = '' - - def press(self, key, fm, env): - from curses.ascii import ctrl, ESC -# from ranger.helper import log -# log(key) - - try: - cmd = self.commandlist.paths[env.keybuffer] - except KeyError: - env.key_clear() - return - - if cmd == self.commandlist.dummy_object: - return - - cmd.execute(self, fm) - env.key_clear() - - def type_key(self, key): - if isinstance(key, int): - key = chr(key) - - if self.pos == len(self.line): - self.line += key - else: - self.line = self.line[:self.pos] + key + self.line[self.pos:] - - self.pos += len(key) - - def move(self, relative = 0, absolute = None): - if absolute is not None: - if absolute < 0: - self.pos = len(self.line) + 1 + absolute - else: - self.pos = absolute - - self.pos = min(max(0, self.pos + relative), len(self.line)) - - def delete_rest(self, direction): - if direction > 0: - self.line = self.line[:self.pos] - else: - self.line = self.line[self.pos:] - self.pos = 0 - - def delete(self, mod): - if mod == -1 and len(self.line) == 0: - self.close() - pos = self.pos + mod - - self.line = self.line[0:pos] + self.line[pos+1:] - self.move(relative = mod) - - def execute(self, fm): - try: - self.execute_funcs[self.mode] (self, fm) - except KeyError: - pass - self.line = '' - self.pos = 0 - self.close() - - def execute_search(self, fm): - import re - if fm.env.pwd: -# try: - regexp = re.compile(self.line, re.L | re.U | re.I) - fm.env.last_search = regexp - if fm.env.pwd.search(regexp): - fm.env.cf = fm.env.pwd.pointed_file -# except: -# pass - - def execute_openwith(self, fm): - line = self.line - if line[0] == '!': - fm.execute_file(tuple(line[1:].split()) + (fm.env.cf.path, )) - else: - fm.execute_file(tuple(line.split()) + (fm.env.cf.path, ), background = True) - - def execute_openwith_quick(self, fm): - split = self.line.split() - app, flags, mode = get_app_flags_mode(self.line, fm) - fm.execute_file( - files = [self.cf], - app = app, - flags = flags, - mode = mode ) - - def execute_noreturn(self, fm): - pass - - def execute_command(self, fm): - pass - -def get_app_flags_mode(line, fm): - """ extracts the application, flags and mode from a string entered into the "openwith_quick" console. """ - # examples: - # "mplayer d 1" => ("mplayer", "d", 1) - # "aunpack 4" => ("aunpack", "", 4) - # "p" => ("", "p", 0) - # "" => None - - app = '' - flags = '' - mode = 0 - split = line.split() - - if len(split) == 0: - pass - - elif len(split) == 1: - part = split[0] - if is_app(part, fm): - app = part - elif is_flags(part): - flags = part - elif is_mode(part): - mode = part - - elif len(split) == 2: - part0 = split[0] - part1 = split[1] - - if is_app(part0, fm): - app = part0 - if is_flags(part1): - flags = part1 - elif is_mode(part1): - mode = part1 - elif is_flags(part0): - flags = part0 - if is_mode(part1): - mode = part1 - elif is_mode(part0): - mode = part0 - if is_flags(part1): - flags = part1 - - elif len(split) >= 3: - part0 = split[0] - part1 = split[1] - part2 = split[2] - - if is_app(part0, fm): - app = part0 - if is_flags(part1): - flags = part1 - if is_mode(part2): - mode = part2 - elif is_mode(part1): - mode = part1 - if is_flags(part2): - flags = part2 - elif is_flags(part0): - flags = part0 - if is_mode(part1): - mode = part1 - elif is_mode(part0): - mode = part0 - if is_flags(part1): - flags = part1 - - return app, flags, int(mode) - -def is_app(arg, fm): - return fm.apps.has(arg) - -def is_flags(arg): - from ranger.applications import ALLOWED_FLAGS - return all(x in ALLOWED_FLAGS for x in arg) - -def is_mode(arg): - return all(x in '0123456789' for x in arg) - - diff --git a/ranger/gui/wdisplay.py b/ranger/gui/wdisplay.py deleted file mode 100644 index c7e78b8e..00000000 --- a/ranger/gui/wdisplay.py +++ /dev/null @@ -1,191 +0,0 @@ -from ranger.gui.widget import Widget as SuperClass - -class WDisplay(SuperClass): - def __init__(self, win, colorscheme, level): - SuperClass.__init__(self, win, colorscheme) - self.level = level - self.main_display = False - self.display_infostring = False - self.scroll_begin = 0 - - def feed_env(self, env): - self.target = env.at_level(self.level) - - def click(self, event, fm): - from ranger.fsobject.fsobject import T_DIRECTORY - - if self.target is None: - pass - - elif self.target.type is T_DIRECTORY: - index = self.scroll_begin + event.y - self.y - - if event.pressed(1): - if not self.main_display: - fm.enter_dir(self.target.path) - - if index < len(self.target): - fm.move_pointer(absolute = index) - elif event.pressed(3): - try: - clicked_file = self.target[index] - fm.enter_dir(clicked_file.path) - except: - pass - - else: - if self.level > 0: - fm.move_right() - - def draw(self): - from ranger.fsobject.file import File - from ranger.fsobject.directory import Directory - - if self.target is None: - pass - elif type(self.target) == File: - self.draw_file() - elif type(self.target) == Directory: - self.draw_directory() - else: - self.win.addnstr(self.y, self.x, "unknown type.", self.wid) - - def draw_file(self): - if not self.target.accessible: - self.win.addnstr(self.y, self.x, "not accessible", self.wid) - return - - if self.settings.preview_files: - try: - if self.target.size < 1024 * 20: - f = open(self.target.path, 'r') - for line in range(self.hei): - read = f.readline().expandtabs() - self.win.addnstr(self.y + line, self.x, read, self.wid) - except: - pass - - def draw_directory(self): - from ranger.fsobject.directory import Directory - import curses - import stat - - self.target.use() - self.target.load_content_if_outdated() - self.target.sort_if_outdated() - - base_color = ['in_display'] - - if self.main_display: - base_color.append('maindisplay') - - if not self.target.accessible: - self.color(base_color, 'error') - self.win.addnstr(self.y, self.x, "not accessible", self.wid) - self.color_reset() - return - - if self.target.empty(): - self.color(base_color, 'empty') - self.win.addnstr(self.y, self.x, "empty", self.wid) - self.color_reset() - return - - self.set_scroll_begin() - - selected_i = self.target.pointed_index - for line in range(self.hei): - i = line + self.scroll_begin - - try: - drawed = self.target[i] - except IndexError: - break - - this_color = base_color + list(drawed.mimetype_tuple) - - if i == selected_i: - this_color.append('selected') - - if isinstance(drawed, Directory): - this_color.append('directory') - else: - this_color.append('file') - - if drawed.stat is not None and drawed.stat.st_mode & stat.S_IXUSR: - this_color.append('executable') - - if drawed.islink: - this_color.append('link') - - string = drawed.basename - if self.main_display: - self.win.addnstr(self.y + line, self.x+1, drawed.basename, self.wid-2) - else: - self.win.addnstr(self.y + line, self.x, drawed.basename, self.wid) - - if self.display_infostring and drawed.infostring: - info = drawed.infostring - x = self.x + self.wid - 1 - len(info) - if x > self.x: - self.win.addstr(self.y + line, x, str(info) + ' ') - - self.color_at(self.y + line, self.x, self.wid, this_color) - - self.color_reset() - - def get_scroll_begin(self): - offset = self.settings.scroll_offset - dirsize = len(self.target) - winsize = self.hei - halfwinsize = winsize // 2 - index = self.target.pointed_index 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): - self.scroll_begin = self.get_scroll_begin() - self.target.scroll_begin = self.scroll_begin - - # TODO: does not work if options.scroll_offset is high, - # relative > 1 and you scroll from scroll_begin = 1 to 0 - def scroll(self, relative): - self.set_scroll_begin() - old_value = self.target.scroll_begin - self.target.scroll_begin += relative - self.set_scroll_begin() - - if self.target.scroll_begin == old_value: - self.target.move_pointer(relative = relative) - self.target.scroll_begin += relative - - diff --git a/ranger/gui/widget.py b/ranger/gui/widget.py deleted file mode 100644 index 093eee14..00000000 --- a/ranger/gui/widget.py +++ /dev/null @@ -1,74 +0,0 @@ - -class OutOfBoundsException(Exception): - pass - -def combine(keylist, keys): - if type(keylist) in (list, tuple): - return tuple(tuple(keylist) + keys) - else: - return tuple((keylist, ) + keys) - -from ranger.shared import SettingsAware -class Widget(SettingsAware): - def __init__(self, win, _): - self.win = win - self.focused = False - self.colorscheme = self.settings.colorscheme - self.visible = True - self.setdim(0, 0, 0, 0) - - def color(self, keylist = None, *keys): - keys = combine(keylist, keys) - self.win.attrset(self.colorscheme.get_attr(*keys)) - - def color_at(self, y, x, wid, keylist = None, *keys): - keys = combine(keylist, keys) - self.win.chgat(y, x, wid, self.colorscheme.get_attr(*keys)) - - def color_reset(self): - Widget.color(self, 'reset') - - def setdim(self, y, x, hei=None, wid=None): - maxy, maxx = self.win.getmaxyx() - - wid = wid or maxx - x - hei = hei or maxy - y - - if x + wid > maxx and y + hei > maxy: - raise OutOfBoundsException("X and Y out of bounds!") - - if x + wid > maxx: - raise OutOfBoundsException("X out of bounds!") - - if y + hei > maxy: - raise OutOfBoundsException("Y out of bounds!") - - self.x = x - self.y = y - self.wid = wid - self.hei = hei - - def contains_point(self, y, x): - return (x >= self.x and x < self.x + self.wid) and \ - (y >= self.y and y < self.y + self.hei) - - def feed_env(self, env): - pass - - def feed(self): - pass - - def click(self, event, fm): - pass - - def press(self, key, fm): - pass - - def draw(self): - pass - - def finalize(self): - pass - - def destroy(self): - pass diff --git a/ranger/gui/widgets/__init__.py b/ranger/gui/widgets/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ranger/gui/widgets/console.py b/ranger/gui/widgets/console.py new file mode 100644 index 00000000..ab37d628 --- /dev/null +++ b/ranger/gui/widgets/console.py @@ -0,0 +1,239 @@ +"""The Console widget implements a vim-like console for entering commands, +searching and executing files.""" +from ..displayable import Displayable +import curses + +CONSOLE_MODES = tuple(':@/?>!') +CONSOLE_PROMPTS = { '@': 'open with: ' } + +class Console(Displayable): + def __init__(self, win): + from ranger.container import CommandList + Displayable.__init__(self, win) + self.mode = None + self.visible = False + self.commandlist = CommandList() + self.settings.keys.initialize_console_commands(self.commandlist) + self.last_cursor_mode = 1 + self.clear() + self.prompt = None + self.execute_funcs = { + ':': Console.execute_command, + '@': Console.execute_openwith_quick, + '/': Console.execute_search, + '?': Console.execute_search, + '>': Console.execute_noreturn, + '!': Console.execute_openwith } + + def feed_env(self, env): + self.cf = env.cf + + def draw(self): + if self.mode is None: + return + self.win.addstr(self.y, self.x, self.prompt + self.line) + + def finalize(self): + try: + self.win.move(self.y, self.x + self.pos + len(self.prompt)) + except: + pass + + def open(self, mode): + if mode not in CONSOLE_MODES: + return False + + self.last_cursor_mode = curses.curs_set(1) + self.mode = mode + try: + self.prompt = CONSOLE_PROMPTS[self.mode] + except KeyError: + self.prompt = self.mode + self.focused = True + self.visible = True + return True + + def close(self): + curses.curs_set(self.last_cursor_mode) + self.focused = False + self.visible = False + if hasattr(self, 'on_close'): + self.on_close() + + def clear(self): + self.pos = 0 + self.line = '' + + def press(self, key): + from curses.ascii import ctrl, ESC + + try: + cmd = self.commandlist.paths[self.env.keybuffer] + except KeyError: + self.env.key_clear() + return + + if cmd == self.commandlist.dummy_object: + return + + cmd.execute(self, self.fm) + self.env.key_clear() + + def type_key(self, key): + if isinstance(key, int): + key = chr(key) + + if self.pos == len(self.line): + self.line += key + else: + self.line = self.line[:self.pos] + key + self.line[self.pos:] + + self.pos += len(key) + + def move(self, relative = 0, absolute = None): + if absolute is not None: + if absolute < 0: + self.pos = len(self.line) + 1 + absolute + else: + self.pos = absolute + + self.pos = min(max(0, self.pos + relative), len(self.line)) + + def delete_rest(self, direction): + if direction > 0: + self.line = self.line[:self.pos] + else: + self.line = self.line[self.pos:] + self.pos = 0 + + def delete(self, mod): + if mod == -1 and len(self.line) == 0: + self.close() + pos = self.pos + mod + + self.line = self.line[0:pos] + self.line[pos+1:] + self.move(relative = mod) + + def execute(self, fm): + try: + self.execute_funcs[self.mode] (self, fm) + except KeyError: + pass + self.line = '' + self.pos = 0 + self.close() + + def execute_search(self, fm): + import re + if fm.env.pwd: +# try: + regexp = re.compile(self.line, re.L | re.U | re.I) + fm.env.last_search = regexp + if fm.env.pwd.search(regexp): + fm.env.cf = fm.env.pwd.pointed_file +# except: +# pass + + def execute_openwith(self, fm): + line = self.line + if line[0] == '!': + fm.execute_file(tuple(line[1:].split()) + (fm.env.cf.path, )) + else: + fm.execute_file(tuple(line.split()) + (fm.env.cf.path, ), background = True) + + def execute_openwith_quick(self, fm): + split = self.line.split() + app, flags, mode = get_app_flags_mode(self.line, fm) + fm.execute_file( + files = [self.cf], + app = app, + flags = flags, + mode = mode ) + + def execute_noreturn(self, fm): + pass + + def execute_command(self, fm): + pass + +def get_app_flags_mode(line, fm): + """ extracts the application, flags and mode from a string entered into the "openwith_quick" console. """ + # examples: + # "mplayer d 1" => ("mplayer", "d", 1) + # "aunpack 4" => ("aunpack", "", 4) + # "p" => ("", "p", 0) + # "" => None + + app = '' + flags = '' + mode = 0 + split = line.split() + + if len(split) == 0: + pass + + elif len(split) == 1: + part = split[0] + if is_app(part, fm): + app = part + elif is_flags(part): + flags = part + elif is_mode(part): + mode = part + + elif len(split) == 2: + part0 = split[0] + part1 = split[1] + + if is_app(part0, fm): + app = part0 + if is_flags(part1): + flags = part1 + elif is_mode(part1): + mode = part1 + elif is_flags(part0): + flags = part0 + if is_mode(part1): + mode = part1 + elif is_mode(part0): + mode = part0 + if is_flags(part1): + flags = part1 + + elif len(split) >= 3: + part0 = split[0] + part1 = split[1] + part2 = split[2] + + if is_app(part0, fm): + app = part0 + if is_flags(part1): + flags = part1 + if is_mode(part2): + mode = part2 + elif is_mode(part1): + mode = part1 + if is_flags(part2): + flags = part2 + elif is_flags(part0): + flags = part0 + if is_mode(part1): + mode = part1 + elif is_mode(part0): + mode = part0 + if is_flags(part1): + flags = part1 + + return app, flags, int(mode) + +def is_app(arg, fm): + return fm.apps.has(arg) + +def is_flags(arg): + from ranger.applications import ALLOWED_FLAGS + return all(x in ALLOWED_FLAGS for x in arg) + +def is_mode(arg): + return all(x in '0123456789' for x in arg) + + diff --git a/ranger/gui/widgets/filelist.py b/ranger/gui/widgets/filelist.py new file mode 100644 index 00000000..493dae24 --- /dev/null +++ b/ranger/gui/widgets/filelist.py @@ -0,0 +1,195 @@ +"""The FileList widget displays the contents of a directory or file.""" +from ..displayable import Displayable + +class FileList(Displayable): + main_display = False + display_infostring = False + scroll_begin = 0 + + def __init__(self, win, level): + Displayable.__init__(self, win) + self.level = level + + def click(self, event): + """Handle a MouseEvent""" + from ranger.fsobject.fsobject import T_DIRECTORY + + if self.target is None: + pass + + elif self.target.type is T_DIRECTORY: + index = self.scroll_begin + event.y - self.y + + if event.pressed(1): + if not self.main_display: + self.fm.enter_dir(self.target.path) + + if index < len(self.target): + self.fm.move_pointer(absolute = index) + elif event.pressed(3): + try: + clicked_file = self.target[index] + self.fm.enter_dir(clicked_file.path) + except: + pass + + else: + if self.level > 0: + self.fm.move_right() + + def draw(self): + """Call either draw_file() or draw_directory()""" + from ranger.fsobject.file import File + from ranger.fsobject.directory import Directory + + self.target = self.env.at_level(self.level) + + if self.target is None: + pass + elif type(self.target) == File: + self.draw_file() + elif type(self.target) == Directory: + self.draw_directory() + else: + self.win.addnstr(self.y, self.x, "unknown type.", self.wid) + + def draw_file(self): + """Draw a preview of the file, if the settings allow it""" + if not self.target.accessible: + self.win.addnstr(self.y, self.x, "not accessible", self.wid) + return + + if self.settings.preview_files: + try: + if self.target.size < 1024 * 20: + f = open(self.target.path, 'r') + for line in range(self.hei): + read = f.readline().expandtabs() + self.win.addnstr(self.y + line, self.x, read, self.wid) + except: + pass + + def draw_directory(self): + """Draw the contents of a directory""" + from ranger.fsobject.directory import Directory + import stat + + self.target.use() + self.target.load_content_if_outdated() + self.target.sort_if_outdated() + + base_color = ['in_display'] + + if self.main_display: + base_color.append('maindisplay') + + if not self.target.accessible: + self.color(base_color, 'error') + self.win.addnstr(self.y, self.x, "not accessible", self.wid) + self.color_reset() + return + + if self.target.empty(): + self.color(base_color, 'empty') + self.win.addnstr(self.y, self.x, "empty", self.wid) + self.color_reset() + return + + self.set_scroll_begin() + + selected_i = self.target.pointed_index + for line in range(self.hei): + i = line + self.scroll_begin + + try: + drawed = self.target[i] + except IndexError: + break + + this_color = base_color + list(drawed.mimetype_tuple) + + if i == selected_i: + this_color.append('selected') + + if isinstance(drawed, Directory): + this_color.append('directory') + else: + this_color.append('file') + + if drawed.stat is not None and drawed.stat.st_mode & stat.S_IXUSR: + this_color.append('executable') + + if drawed.islink: + this_color.append('link') + + string = drawed.basename + if self.main_display: + self.win.addnstr(self.y + line, self.x+1, drawed.basename, self.wid-2) + else: + self.win.addnstr(self.y + line, self.x, drawed.basename, self.wid) + + if self.display_infostring and drawed.infostring: + info = drawed.infostring + x = self.x + self.wid - 1 - len(info) + if x > self.x: + self.win.addstr(self.y + line, x, str(info) + ' ') + + self.color_at(self.y + line, self.x, self.wid, this_color) + + 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.pointed_index 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 + + # TODO: does not work if options.scroll_offset is high, + # relative > 1 and you scroll from scroll_begin = 1 to 0 + def scroll(self, relative): + self.set_scroll_begin() + old_value = self.target.scroll_begin + self.target.scroll_begin += relative + self.set_scroll_begin() + + if self.target.scroll_begin == old_value: + self.target.move_pointer(relative = relative) + self.target.scroll_begin += relative diff --git a/ranger/gui/widgets/titlebar.py b/ranger/gui/widgets/titlebar.py new file mode 100644 index 00000000..4ec48766 --- /dev/null +++ b/ranger/gui/widgets/titlebar.py @@ -0,0 +1,38 @@ +"""The TitleBar widget displays the current path and some other useful +information.""" + +from ..displayable import Displayable + +class TitleBar(Displayable): + def draw(self): + import curses, socket, os + self.win.move(self.y, self.x) + + self.color('in_titlebar', 'hostname') + string = os.getenv('LOGNAME') + '@' + socket.gethostname() + self.win.addnstr(string, self.wid) + + for path in self.env.pathway: + currentx = self.win.getyx()[1] + + if path.islink: + self.color('in_titlebar', 'link') + else: + self.color('in_titlebar', 'directory') + + self.win.addnstr(path.basename + '/', max(self.wid - currentx, 0)) + if self.env.cf is not None: + currentx = self.win.getyx()[1] + self.color('in_titlebar', 'file') + self.win.addnstr(self.env.cf.basename, max(self.wid - currentx, 0)) + + self.color('in_titlebar', 'keybuffer') + + kb = str(self.env.keybuffer) + if self.wid + self.x - currentx > len(kb): + self.win.addstr( + self.y, + self.x + self.wid - len(kb) - 2, + kb) + + self.color_reset() diff --git a/ranger/gui/wtitlebar.py b/ranger/gui/wtitlebar.py deleted file mode 100644 index 65ed2e33..00000000 --- a/ranger/gui/wtitlebar.py +++ /dev/null @@ -1,45 +0,0 @@ -from ranger.gui.widget import Widget as SuperClass - -class WTitleBar(SuperClass): - def feed_env(self, env): - self.pathway = env.pathway - self.cf = env.cf - self.keybuffer = env.keybuffer - - def draw(self): - import curses, socket, os - self.win.move(self.y, self.x) - - try: - self.color('in_titlebar', 'hostname') - string = os.getenv('LOGNAME') + '@' + socket.gethostname() - self.win.addnstr(string, self.wid) - except: - raise - pass - - for path in self.pathway: - currentx = self.win.getyx()[1] - - if path.islink: - self.color('in_titlebar', 'link') - else: - self.color('in_titlebar', 'directory') - - self.win.addnstr(path.basename + '/', max(self.wid - currentx, 0)) - if self.cf is not None: - currentx = self.win.getyx()[1] - self.color('in_titlebar', 'file') - self.win.addnstr(self.cf.basename, max(self.wid - currentx, 0)) - - self.color('in_titlebar', 'keybuffer') - - kb = str(self.keybuffer) - if self.wid + self.x - currentx > len(kb): - self.win.addstr( - self.y, - self.x + self.wid - len(kb) - 2, - kb) - - self.color_reset() - diff --git a/ranger/main.py b/ranger/main.py index 48297a7c..b6af145d 100644 --- a/ranger/main.py +++ b/ranger/main.py @@ -69,11 +69,15 @@ def main(): # Run the file manager my_ui.initialize() my_fm.loop() + +# except: +# from ranger import log +# log(str(sys.exc_info())) finally: # Finish, clean up if 'my_ui' in vars(): - my_ui.exit() + my_ui.destroy() if args.cd_after_exit: try: sys.__stderr__.write(env.pwd.path) diff --git a/test/tc_directory.py b/test/tc_directory.py index 92a4f01c..434d282f 100644 --- a/test/tc_directory.py +++ b/test/tc_directory.py @@ -10,7 +10,7 @@ from ranger.fsobject.directory import Directory from os.path import realpath, join, dirname TESTDIR = realpath(join(dirname(__file__), 'testdir')) TESTFILE = join(TESTDIR, 'testfile5234148') -NONEXISTANT_DIR = '/this/directory/will/most/certainly/not/exist' +NONEXISTANT_DIR = join(TESTDIR, 'nonexistant') import unittest class Test1(unittest.TestCase): -- cgit 1.4.1-2-gfad0