Exemple #1
0
class Entry(Gtk.EventBox):
    __gtype_name__ = "CommanderEntry"

    def __init__(self, view):
        Gtk.EventBox.__init__(self)
        self._view = view

        self.set_visible_window(False)

        hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 3)
        hbox.show()
        hbox.set_border_width(3)

        # context for the view
        self._entry = Gtk.Entry()
        self._entry.set_has_frame(False)
        self._entry.set_name('gedit-commander-entry')
        self._entry.show()

        css = Gtk.CssProvider()
        css.load_from_data(
            bytes(
                """
@binding-set terminal-like-bindings {
    unbind "<Control>A";

    bind "<Control>W" { "delete-from-cursor" (word-ends, -1) };
    bind "<Control>A" { "move-cursor" (buffer-ends, -1, 0) };
    bind "<Control>U" { "delete-from-cursor" (display-line-ends, -1) };
    bind "<Control>K" { "delete-from-cursor" (display-line-ends, 1) };
    bind "<Control>E" { "move-cursor" (buffer-ends, 1, 0) };
    bind "Escape" { "delete-from-cursor" (display-lines, 1) };
}

GtkEntry#gedit-commander-entry {
    gtk-key-bindings: terminal-like-bindings;

    /* Override background to anything. This is weird, but doing this we can
       then in code use widget.override_background to set the color dynamically
       to the same color as the gedit view */
    background: transparent;
    border-width: 0;
    box-shadow: 0 0 transparent;
    transition: none;
}
""", 'utf-8'))

        # FIXME: remove hardcopy of 600 (GTK_STYLE_PROVIDER_PRIORITY_APPLICATION)
        # https://bugzilla.gnome.org/show_bug.cgi?id=646860
        self._entry.get_style_context().add_provider(css, 600)

        self._prompt_label = Gtk.Label(label='<b>&gt;&gt;&gt;</b>',
                                       use_markup=True)
        self._prompt_label.show()

        self._entry.connect('focus-out-event', self.on_entry_focus_out)
        self._entry.connect('key-press-event', self.on_entry_key_press)

        self._history = History(
            os.path.join(GLib.get_user_config_dir(),
                         'gedit/commander/history'))
        self._prompt = None

        self._accel_group = None

        hbox.pack_start(self._prompt_label, False, False, 0)
        hbox.pack_start(self._entry, True, True, 0)

        self.copy_style_from_view()
        self.view_style_updated_id = self._view.connect(
            'style-updated', self.on_view_style_updated)

        self.add(hbox)
        self.attach()
        self._entry.grab_focus()

        self._wait_timeout = 0
        self._info_window = None

        self.connect('destroy', self.on_destroy)
        self.connect_after('size-allocate', self.on_size_allocate)
        self.view_draw_id = self._view.connect_after('draw', self.on_draw)

        self._history_prefix = None
        self._suspended = None
        self._handlers = [[0, Gdk.KEY_Up, self.on_history_move, -1],
                          [0, Gdk.KEY_Down, self.on_history_move, 1],
                          [None, Gdk.KEY_Return, self.on_execute, None],
                          [None, Gdk.KEY_KP_Enter, self.on_execute, None],
                          [0, Gdk.KEY_Tab, self.on_complete, None],
                          [0, Gdk.KEY_ISO_Left_Tab, self.on_complete, None]]

        self._re_complete = re.compile(
            '("((?:\\\\"|[^"])*)"?|\'((?:\\\\\'|[^\'])*)\'?|[^\s]+)')
        self._command_state = commands.Commands.State()

    def on_view_style_updated(self, widget):
        self.copy_style_from_view()

    def get_border_color(self):
        color = self.get_background_color().copy()
        color.red = 1 - color.red
        color.green = 1 - color.green
        color.blue = 1 - color.blue
        color.alpha = 0.5

        return color

    def get_background_color(self):
        context = self._view.get_style_context()
        return context.get_background_color(Gtk.StateFlags.NORMAL)

    def get_foreground_color(self):
        context = self._view.get_style_context()
        return context.get_color(Gtk.StateFlags.NORMAL)

    def get_font(self):
        context = self._view.get_style_context()
        return context.get_font(Gtk.StateFlags.NORMAL)

    def copy_style_from_view(self, widget=None):
        if widget != None:
            context = self._view.get_style_context()
            font = context.get_font(Gtk.StateFlags.NORMAL)

            widget.override_color(Gtk.StateFlags.NORMAL,
                                  self.get_foreground_color())
            widget.override_font(self.get_font())
        else:
            if self._entry:
                self.copy_style_from_view(self._entry)

            if self._prompt_label:
                self.copy_style_from_view(self._prompt_label)

    def view(self):
        return self._view

    def on_size_allocate(self, widget, alloc):
        alloc = self.get_allocation()
        self._view.set_border_window_size(Gtk.TextWindowType.BOTTOM,
                                          alloc.height)

        win = self._view.get_window(Gtk.TextWindowType.BOTTOM)
        self.set_size_request(win.get_width(), -1)

        # NOTE: we need to do this explicitly somehow, otherwise the window
        # size will not be updated unless something else happens, not exactly
        # sure what. This might be caused by the multi notebook, or custom
        # animation layouting?
        self._view.get_parent().resize_children()

    def attach(self):
        # Attach ourselves in the text view, and position just above the
        # text window
        win = self._view.get_window(Gtk.TextWindowType.TEXT)
        alloc = self.get_allocation()

        self._view.set_border_window_size(Gtk.TextWindowType.BOTTOM,
                                          max(alloc.height, 1))
        self._view.add_child_in_window(self, Gtk.TextWindowType.BOTTOM, 0, 0)

        win = self._view.get_window(Gtk.TextWindowType.BOTTOM)

        self.show()
        self.set_size_request(win.get_width(), -1)

    def on_entry_focus_out(self, widget, evnt):
        if self._entry.get_sensitive():
            self.destroy()

    def on_entry_key_press(self, widget, evnt):
        state = evnt.state & Gtk.accelerator_get_default_mod_mask()
        text = self._entry.get_text()

        if evnt.keyval == Gdk.KEY_Escape:
            if self._info_window:
                if self._suspended:
                    self._suspended.resume()

                if self._info_window:
                    self._info_window.destroy()

                self._entry.set_sensitive(True)
            elif self._accel_group:
                self._accel_group = self._accel_group.parent

                if not self._accel_group or not self._accel_group.parent:
                    self._entry.set_editable(True)
                    self._accel_group = None

                self.prompt()
            elif text:
                self._entry.set_text('')
            elif self._command_state:
                self._command_state.clear()
                self.prompt()
            else:
                self._view.grab_focus()
                self.destroy()

            return True

        if state or self._accel_group:
            # Check if it should be handled by the accel group
            group = self._accel_group

            if not self._accel_group:
                group = commands.Commands().accelerator_group()

            accel = group.activate(evnt.keyval, state)

            if isinstance(accel, commands.accel_group.AccelGroup):
                self._accel_group = accel
                self._entry.set_text('')
                self._entry.set_editable(False)
                self.prompt()

                return True
            elif isinstance(accel, commands.accel_group.AccelCallback):
                self._entry.set_editable(True)
                self.run_command(
                    lambda: accel.activate(self._command_state, self))
                return True

        if not self._entry.get_editable():
            return True

        for handler in self._handlers:
            if (handler[0] == None or handler[0]
                    == state) and evnt.keyval == handler[1] and handler[2](
                        handler[3], state):
                return True

        if self._info_window and self._info_window.empty():
            self._info_window.destroy()

        self._history_prefix = None
        return False

    def on_history_move(self, direction, modifier):
        pos = self._entry.get_position()

        self._history.update(self._entry.get_text())

        if self._history_prefix == None:
            if len(self._entry.get_text()) == pos:
                self._history_prefix = self._entry.get_chars(0, pos)
            else:
                self._history_prefix = ''

        if self._history_prefix == None:
            hist = ''
        else:
            hist = self._history_prefix

        next = self._history.move(direction, hist)

        if next != None:
            self._entry.set_text(next)
            self._entry.set_position(-1)

        return True

    def prompt(self, pr=''):
        self._prompt = pr

        if self._accel_group != None:
            pr = '<i>%s</i>' % (saxutils.escape(
                self._accel_group.full_name()), )

        if not pr:
            pr = ''
        else:
            pr = ' ' + pr

        self._prompt_label.set_markup('<b>&gt;&gt;&gt;</b>%s' % pr)

    def make_info(self):
        if self._info_window == None:
            self._info_window = Info(self)
            self._info_window.show()

            self._info_window.connect('destroy', self.on_info_window_destroy)

    def on_info_window_destroy(self, widget):
        self._info_window = None

    def info_show(self, text='', use_markup=False):
        self.make_info()
        self._info_window.add_lines(text, use_markup)

    def info_status(self, text):
        self.make_info()
        self._info_window.status(text)

    def info_add_action(self, stock, callback, data=None):
        self.make_info()
        return self._info_window.add_action(stock, callback, data)

    def command_history_done(self):
        self._history.add(self._entry.get_text())
        self._history_prefix = None
        self._entry.set_text('')

    def on_wait_cancel(self):
        if self._suspended:
            self._suspended.resume()

        if self._cancel_button:
            self._cancel_button.destroy()

        if self._info_window and self._info_window.empty():
            self._info_window.destroy()
            self._entry.grab_focus()
            self._entry.set_sensitive(True)

    def _show_wait_cancel(self):
        self._cancel_button = self.info_add_action(Gtk.STOCK_STOP,
                                                   self.on_wait_cancel)
        self.info_status('<i>Waiting to finish...</i>')

        self._wait_timeout = 0
        return False

    def _complete_word_match(self, match):
        for i in (3, 2, 0):
            if match.group(i) != None:
                return [match.group(i), match.start(i), match.end(i)]

    def on_suspend_resume(self):
        if self._wait_timeout:
            GLib.source_remove(self._wait_timeout)
            self._wait_timeout = 0
        else:
            self._cancel_button.destroy()
            self._cancel_button = None
            self.info_status(None)

        self._entry.set_sensitive(True)
        self.command_history_done()

        if self._entry.props.has_focus or (self._info_window
                                           and not self._info_window.empty()):
            self._entry.grab_focus()

        self.on_execute(None, 0)

    def ellipsize(self, s, size):
        if len(s) <= size:
            return s

        mid = (size - 4) / 2
        return s[:mid] + '...' + s[-mid:]

    def destroy(self):
        self.hide()
        Gtk.EventBox.destroy(self)

    def run_command(self, cb):
        self._suspended = None

        try:
            ret = cb()
        except Exception as e:
            self.command_history_done()
            self._command_state.clear()

            self.prompt()

            # Show error in info
            self.info_show(
                '<b><span color="#f66">Error:</span></b> ' +
                saxutils.escape(str(e)), True)

            if not isinstance(e, commands.exceptions.Execute):
                self.info_show(self.format_trace(), False)

            return None

        mod = sys.modules['commander.commands.result']

        if ret == mod.Result.SUSPEND:
            # Wait for it...
            self._suspended = ret
            ret.register(self.on_suspend_resume)

            self._wait_timeout = GLib.timeout_add(500, self._show_wait_cancel)
            self._entry.set_sensitive(False)
        else:
            self.command_history_done()
            self.prompt('')

            if ret == mod.Result.PROMPT:
                self.prompt(ret.prompt)
            elif (ret == None or ret == mod.HIDE) and not self._prompt and (
                    not self._info_window or self._info_window.empty()):
                self._command_state.clear()
                self._view.grab_focus()
                self.destroy()
            else:
                self._entry.grab_focus()

        return ret

    def format_trace(self):
        tp, val, tb = sys.exc_info()

        origtb = tb

        thisdir = os.path.dirname(__file__)

        # Skip frames up until after the last entry.py...
        while True:
            filename = tb.tb_frame.f_code.co_filename

            dname = os.path.dirname(filename)

            if not dname.startswith(thisdir):
                break

            tb = tb.tb_next

        msg = traceback.format_exception(tp, val, tb)
        r = ''.join(msg[0:-1])

        # This is done to prevent cyclic references, see python
        # documentation on sys.exc_info
        del origtb

        return r

    def on_execute(self, dummy, modifier):
        if self._info_window and not self._suspended:
            self._info_window.destroy()

        text = self._entry.get_text().strip()
        words = list(self._re_complete.finditer(text))
        wordsstr = []

        for word in words:
            spec = self._complete_word_match(word)
            wordsstr.append(spec[0])

        if not wordsstr and not self._command_state:
            self._entry.set_text('')
            return

        self.run_command(lambda: commands.Commands().execute(
            self._command_state, text, words, wordsstr, self, modifier))

        return True

    def on_complete(self, dummy, modifier):
        # First split all the text in words
        text = self._entry.get_text()
        pos = self._entry.get_position()

        words = list(self._re_complete.finditer(text))
        wordsstr = []

        for word in words:
            spec = self._complete_word_match(word)
            wordsstr.append(spec[0])

        # Find out at which word the cursor actually is
        # Examples:
        #  * hello world|
        #  * hello| world
        #  * |hello world
        #  * hello wor|ld
        #  * hello  |  world
        #  * "hello world|"
        posidx = None

        for idx in range(0, len(words)):
            spec = self._complete_word_match(words[idx])

            if words[idx].start(0) > pos:
                # Empty space, new completion
                wordsstr.insert(idx, '')
                words.insert(idx, None)
                posidx = idx
                break
            elif spec[2] == pos:
                # At end of word, resume completion
                posidx = idx
                break
            elif spec[1] <= pos and spec[2] > pos:
                # In middle of word, do not complete
                return True

        if posidx == None:
            wordsstr.append('')
            words.append(None)
            posidx = len(wordsstr) - 1

        # First word completes a command, if not in any special 'mode'
        # otherwise, relay completion to the command, or complete by advice
        # from the 'mode' (prompt)
        cmds = commands.Commands()

        if not self._command_state and posidx == 0:
            # Complete the first command
            ret = commands.completion.command(words=wordsstr, idx=posidx)
        else:
            complete = None
            realidx = posidx

            if not self._command_state:
                # Get the command first
                cmd = commands.completion.single_command(wordsstr, 0)
                realidx -= 1

                ww = wordsstr[1:]
            else:
                cmd = self._command_state.top()
                ww = wordsstr

            if cmd:
                complete = cmd.autocomplete_func()

            if not complete:
                return True

            # 'complete' contains a dict with arg -> func to do the completion
            # of the named argument the command (or stack item) expects
            args, varargs = cmd.args()

            # Remove system arguments
            s = ['argstr', 'args', 'entry', 'view']
            args = list(filter(lambda x: not x in s, args))

            if realidx < len(args):
                arg = args[realidx]
            elif varargs:
                arg = '*'
            else:
                return True

            if not arg in complete:
                return True

            func = complete[arg]

            try:
                spec = utils.getargspec(func)

                if not ww:
                    ww = ['']

                kwargs = {'words': ww, 'idx': realidx, 'view': self._view}

                if not spec.keywords:
                    for k in list(kwargs.keys()):
                        if not k in spec.args:
                            del kwargs[k]

                ret = func(**kwargs)
            except Exception as e:
                # Can be number of arguments, or return values or simply buggy
                # modules
                print(e)
                traceback.print_exc()
                return True

        if not ret or not ret[0]:
            return True

        res = ret[0]
        completed = ret[1]

        if len(ret) > 2:
            after = ret[2]
        else:
            after = ' '

        # Replace the word
        if words[posidx] == None:
            # At end of everything, just append
            spec = None

            self._entry.insert_text(completed, self._entry.get_text_length())
            self._entry.set_position(-1)
        else:
            spec = self._complete_word_match(words[posidx])

            self._entry.delete_text(spec[1], spec[2])
            self._entry.insert_text(completed, spec[1])
            self._entry.set_position(spec[1] + len(completed))

        if len(res) == 1:
            # Full completion
            lastpos = self._entry.get_position()

            if not isinstance(res[0],
                              commands.module.Module) or not res[0].commands():
                if words[posidx] and after == ' ' and (
                        words[posidx].group(2) != None
                        or words[posidx].group(3) != None):
                    lastpos = lastpos + 1

                self._entry.insert_text(after, lastpos)
                self._entry.set_position(lastpos + 1)
            elif completed == wordsstr[posidx] or not res[0].method:
                self._entry.insert_text('.', lastpos)
                self._entry.set_position(lastpos + 1)

            if self._info_window:
                self._info_window.destroy()
        else:
            # Show popup with completed items
            if self._info_window:
                self._info_window.clear()

            ret = []

            for x in res:
                if isinstance(x, commands.method.Method):
                    ret.append('<b>' + saxutils.escape(x.name) + '</b> (<i>' +
                               x.oneline_doc() + '</i>)')
                else:
                    ret.append(str(x))

            self.info_show("\n".join(ret), True)

        return True

    def on_draw(self, widget, ct):
        win = widget.get_window(Gtk.TextWindowType.BOTTOM)

        if not Gtk.cairo_should_draw_window(ct, win):
            return False

        Gtk.cairo_transform_to_window(ct, widget, win)

        color = self.get_border_color()
        width = win.get_width()

        ct.set_source_rgba(color.red, color.green, color.blue, color.alpha)
        ct.move_to(0, 0)
        ct.line_to(width, 0)
        ct.stroke()

        return False

    def on_destroy(self, widget):
        self._view.set_border_window_size(Gtk.TextWindowType.BOTTOM, 0)
        self._view.disconnect(self.view_style_updated_id)
        self._view.disconnect(self.view_draw_id)

        if self._info_window:
            self._info_window.destroy()

        self._history.save()
class Entry(gtk.EventBox):
    def __init__(self, view):
        gtk.EventBox.__init__(self)
        self._view = view

        hbox = gtk.HBox(False, 3)
        hbox.show()
        hbox.set_border_width(3)

        self._entry = gtk.Entry()
        self._entry.modify_font(self._view.style.font_desc)
        self._entry.set_has_frame(False)
        self._entry.set_name('command-bar')
        self._entry.modify_text(gtk.STATE_NORMAL,
                                self._view.style.text[gtk.STATE_NORMAL])
        self._entry.set_app_paintable(True)

        self._entry.connect('realize', self.on_realize)
        self._entry.connect('expose-event', self.on_entry_expose)

        self._entry.show()

        self._prompt_label = gtk.Label('<b>&gt;&gt;&gt;</b>')
        self._prompt_label.set_use_markup(True)
        self._prompt_label.modify_font(self._view.style.font_desc)
        self._prompt_label.show()
        self._prompt_label.modify_fg(gtk.STATE_NORMAL,
                                     self._view.style.text[gtk.STATE_NORMAL])

        self.modify_bg(gtk.STATE_NORMAL, self.background_gdk())
        self._entry.modify_base(gtk.STATE_NORMAL, self.background_gdk())

        self._entry.connect('focus-out-event', self.on_entry_focus_out)
        self._entry.connect('key-press-event', self.on_entry_key_press)

        self.connect_after('size-allocate', self.on_size_allocate)
        self.connect_after('expose-event', self.on_expose)
        self.connect_after('realize', self.on_realize)

        self._history = History(
            os.path.expanduser('~/.config/pluma/commander/history'))
        self._prompt = None

        self._accel_group = None

        hbox.pack_start(self._prompt_label, False, False, 0)
        hbox.pack_start(self._entry, True, True, 0)

        self.add(hbox)
        self.attach()

        self._entry.grab_focus()
        self._wait_timeout = 0
        self._info_window = None

        self.connect('destroy', self.on_destroy)

        self._history_prefix = None
        self._suspended = None
        self._handlers = [[0, gtk.keysyms.Up, self.on_history_move, -1],
                          [0, gtk.keysyms.Down, self.on_history_move, 1],
                          [None, gtk.keysyms.Return, self.on_execute, None],
                          [None, gtk.keysyms.KP_Enter, self.on_execute, None],
                          [0, gtk.keysyms.Tab, self.on_complete, None],
                          [
                              0, gtk.keysyms.ISO_Left_Tab, self.on_complete,
                              None
                          ]]

        self._re_complete = re.compile(
            '("((?:\\\\"|[^"])*)"?|\'((?:\\\\\'|[^\'])*)\'?|[^\s]+)')
        self._command_state = commands.Commands.State()

    def view(self):
        return self._view

    def on_realize(self, widget):
        widget.window.set_back_pixmap(None, False)

    def on_entry_expose(self, widget, evnt):
        ct = evnt.window.cairo_create()
        ct.rectangle(evnt.area.x, evnt.area.y, evnt.area.width,
                     evnt.area.height)

        bg = self.background_color()
        ct.set_source_rgb(bg[0], bg[1], bg[2])
        ct.fill()

        return False

    def on_expose(self, widget, evnt):
        ct = evnt.window.cairo_create()
        color = self.background_color()

        ct.rectangle(evnt.area.x, evnt.area.y, evnt.area.width,
                     evnt.area.height)
        ct.clip()

        # Draw separator line
        ct.move_to(0, 0)
        ct.set_line_width(1)
        ct.line_to(self.allocation.width, 0)

        ct.set_source_rgb(1 - color[0], 1 - color[1], 1 - color[2])
        ct.stroke()
        return False

    def on_size_allocate(self, widget, alloc):
        vwwnd = self._view.get_window(gtk.TEXT_WINDOW_BOTTOM).get_parent()
        size = vwwnd.get_size()
        position = vwwnd.get_position()

        self._view.set_border_window_size(gtk.TEXT_WINDOW_BOTTOM, alloc.height)

    def attach(self):
        # Attach ourselves in the text view, and position just above the
        # text window
        self._view.set_border_window_size(gtk.TEXT_WINDOW_BOTTOM, 1)
        alloc = self._view.allocation

        self.show()
        self._view.add_child_in_window(self, gtk.TEXT_WINDOW_BOTTOM, 0, 0)
        self.set_size_request(alloc.width, -1)

    def background_gdk(self):
        bg = self.background_color()

        bg = map(lambda x: int(x * 65535), bg)
        return gtk.gdk.Color(bg[0], bg[1], bg[2])

    def background_color(self):
        bg = self._view.get_style().base[self._view.state]

        vals = [bg.red, bg.green, bg.blue, 1]

        for i in range(3):
            val = vals[i] / 65535.0

            if val < 0.0001:
                vals[i] = 0.1
            elif val > 0.9999:
                vals[i] = 0.9
            elif val < 0.1:
                vals[i] = val * 1.2
            else:
                vals[i] = val * 0.8

        return vals

    def on_entry_focus_out(self, widget, evnt):
        if self._entry.flags() & gtk.SENSITIVE:
            self.destroy()

    def on_entry_key_press(self, widget, evnt):
        state = evnt.state & gtk.accelerator_get_default_mod_mask()
        text = self._entry.get_text()

        if evnt.keyval == gtk.keysyms.Escape:
            if self._info_window:
                if self._suspended:
                    self._suspended.resume()

                if self._info_window:
                    self._info_window.destroy()

                self._entry.set_sensitive(True)
            elif self._accel_group:
                self._accel_group = self._accel_group.parent

                if not self._accel_group or not self._accel_group.parent:
                    self._entry.set_editable(True)
                    self._accel_group = None

                self.prompt()
            elif text:
                self._entry.set_text('')
            elif self._command_state:
                self._command_state.clear()
                self.prompt()
            else:
                self._view.grab_focus()
                self.destroy()

            return True

        if state or self._accel_group:
            # Check if it should be handled by the accel group
            group = self._accel_group

            if not self._accel_group:
                group = commands.Commands().accelerator_group()

            accel = group.activate(evnt.keyval, state)

            if isinstance(accel, commands.accel_group.AccelGroup):
                self._accel_group = accel
                self._entry.set_text('')
                self._entry.set_editable(False)
                self.prompt()

                return True
            elif isinstance(accel, commands.accel_group.AccelCallback):
                self._entry.set_editable(True)
                self.run_command(
                    lambda: accel.activate(self._command_state, self))
                return True

        if not self._entry.get_editable():
            return True

        for handler in self._handlers:
            if (handler[0] == None or handler[0]
                    == state) and evnt.keyval == handler[1] and handler[2](
                        handler[3], state):
                return True

        if self._info_window and self._info_window.empty():
            self._info_window.destroy()

        self._history_prefix = None
        return False

    def on_history_move(self, direction, modifier):
        pos = self._entry.get_position()

        self._history.update(self._entry.get_text())

        if self._history_prefix == None:
            if len(self._entry.get_text()) == pos:
                self._history_prefix = self._entry.get_chars(0, pos)
            else:
                self._history_prefix = ''

        if self._history_prefix == None:
            hist = ''
        else:
            hist = self._history_prefix

        next = self._history.move(direction, hist)

        if next != None:
            self._entry.set_text(next)
            self._entry.set_position(-1)

        return True

    def prompt(self, pr=''):
        self._prompt = pr

        if self._accel_group != None:
            pr = '<i>%s</i>' % (saxutils.escape(
                self._accel_group.full_name()), )

        if not pr:
            pr = ''
        else:
            pr = ' ' + pr

        self._prompt_label.set_markup('<b>&gt;&gt;&gt;</b>%s' % pr)

    def make_info(self):
        if self._info_window == None:
            self._info_window = Info(self)
            self._info_window.show()

            self._info_window.connect('destroy', self.on_info_window_destroy)

    def on_info_window_destroy(self, widget):
        self._info_window = None

    def info_show(self, text='', use_markup=False):
        self.make_info()
        self._info_window.add_lines(text, use_markup)

    def info_status(self, text):
        self.make_info()
        self._info_window.status(text)

    def info_add_action(self, stock, callback, data=None):
        self.make_info()
        return self._info_window.add_action(stock, callback, data)

    def command_history_done(self):
        self._history.add(self._entry.get_text())
        self._history_prefix = None
        self._entry.set_text('')

    def on_wait_cancel(self):
        if self._suspended:
            self._suspended.resume()

        if self._cancel_button:
            self._cancel_button.destroy()

        if self._info_window and self._info_window.empty():
            self._info_window.destroy()
            self._entry.grab_focus()
            self._entry.set_sensitive(True)

    def _show_wait_cancel(self):
        self._cancel_button = self.info_add_action(gtk.STOCK_STOP,
                                                   self.on_wait_cancel)
        self.info_status('<i>Waiting to finish...</i>')

        self._wait_timeout = 0
        return False

    def _complete_word_match(self, match):
        for i in (3, 2, 0):
            if match.group(i) != None:
                return [match.group(i), match.start(i), match.end(i)]

    def on_suspend_resume(self):
        if self._wait_timeout:
            glib.source_remove(self._wait_timeout)
            self._wait_timeout = 0
        else:
            self._cancel_button.destroy()
            self._cancel_button = None
            self.info_status(None)

        self._entry.set_sensitive(True)
        self.command_history_done()

        if self._entry.props.has_focus or (self._info_window
                                           and not self._info_window.empty()):
            self._entry.grab_focus()

        self.on_execute(None, 0)

    def ellipsize(self, s, size):
        if len(s) <= size:
            return s

        mid = (size - 4) / 2
        return s[:mid] + '...' + s[-mid:]

    def destroy(self):
        self.hide()
        gtk.EventBox.destroy(self)

    def run_command(self, cb):
        self._suspended = None

        try:
            ret = cb()
        except Exception, e:
            self.command_history_done()
            self._command_state.clear()

            self.prompt()

            # Show error in info
            self.info_show(
                '<b><span color="#f66">Error:</span></b> ' +
                saxutils.escape(str(e)), True)

            if not isinstance(e, commands.exceptions.Execute):
                self.info_show(traceback.format_exc(), False)

            return None

        if ret == commands.result.Result.SUSPEND:
            # Wait for it...
            self._suspended = ret
            ret.register(self.on_suspend_resume)

            self._wait_timeout = glib.timeout_add(500, self._show_wait_cancel)
            self._entry.set_sensitive(False)
        else:
            self.command_history_done()
            self.prompt('')

            if ret == commands.result.Result.PROMPT:
                self.prompt(ret.prompt)
            elif (ret == None
                  or ret == commands.result.HIDE) and not self._prompt and (
                      not self._info_window or self._info_window.empty()):
                self._command_state.clear()
                self._view.grab_focus()
                self.destroy()
            else:
                self._entry.grab_focus()

        return ret
Exemple #3
0
class Entry(gtk.EventBox):
	def __init__(self, view):
		gtk.EventBox.__init__(self)
		self._view = view
		
		hbox = gtk.HBox(False, 3)
		hbox.show()
		hbox.set_border_width(3)
		
		self._entry = gtk.Entry()
		self._entry.modify_font(self._view.style.font_desc)
		self._entry.set_has_frame(False)
		self._entry.set_name('command-bar')
		self._entry.modify_text(gtk.STATE_NORMAL, self._view.style.text[gtk.STATE_NORMAL])
		self._entry.set_app_paintable(True)
		
		self._entry.connect('realize', self.on_realize)
		self._entry.connect('expose-event', self.on_entry_expose)

		self._entry.show()

		self._prompt_label = gtk.Label('<b>&gt;&gt;&gt;</b>')
		self._prompt_label.set_use_markup(True)
		self._prompt_label.modify_font(self._view.style.font_desc)
		self._prompt_label.show()
		self._prompt_label.modify_fg(gtk.STATE_NORMAL, self._view.style.text[gtk.STATE_NORMAL])
		
		self.modify_bg(gtk.STATE_NORMAL, self.background_gdk())
		self._entry.modify_base(gtk.STATE_NORMAL, self.background_gdk())
		
		self._entry.connect('focus-out-event', self.on_entry_focus_out)
		self._entry.connect('key-press-event', self.on_entry_key_press)
		
		self.connect_after('size-allocate', self.on_size_allocate)
		self.connect_after('expose-event', self.on_expose)
		self.connect_after('realize', self.on_realize)
		
		self._history = History(os.path.expanduser('~/.gnome2/gedit/commander/history'))
		self._prompt = None
		
		hbox.pack_start(self._prompt_label, False, False, 0)
		hbox.pack_start(self._entry, True, True, 0)
		
		self.add(hbox)
		self.attach()

		self._entry.grab_focus()
		self._wait_timeout = 0
		self._info_window = None
		
		self.connect('destroy', self.on_destroy)
		
		self._history_prefix = None		
		self._suspended = None
		self._handlers = [
			[0, gtk.keysyms.Up, self.on_history_move, -1],
			[0, gtk.keysyms.Down, self.on_history_move, 1],
			[None, gtk.keysyms.Return, self.on_execute, None],
			[None, gtk.keysyms.KP_Enter, self.on_execute, None],
			[0, gtk.keysyms.Tab, self.on_complete, None],
			[0, gtk.keysyms.ISO_Left_Tab, self.on_complete, None]
		]
		
		self._re_complete = re.compile('("((?:\\\\"|[^"])*)"?|\'((?:\\\\\'|[^\'])*)\'?|[^\s]+)')
		self._command_state = commands.Commands.State()
	
	def view(self):
		return self._view

	def on_realize(self, widget):
		widget.window.set_back_pixmap(None, False)
	
	def on_entry_expose(self, widget, evnt):
		ct = evnt.window.cairo_create()
		ct.rectangle(evnt.area.x, evnt.area.y, evnt.area.width, evnt.area.height)
		
		bg = self.background_color()
		ct.set_source_rgb(bg[0], bg[1], bg[1])
		ct.fill()
		return False

	def on_expose(self, widget, evnt):
		ct = evnt.window.cairo_create()
		color = self.background_color()
		
		ct.rectangle(evnt.area.x, evnt.area.y, evnt.area.width, evnt.area.height)
		ct.clip()
		
		# Draw separator line
		ct.move_to(0, 0)
		ct.set_line_width(1)
		ct.line_to(self.allocation.width, 0)

		ct.set_source_rgb(1 - color[0], 1 - color[1], 1 - color[2])
		ct.stroke()
		return False
		
	def on_size_allocate(self, widget, alloc):
		vwwnd = self._view.get_window(gtk.TEXT_WINDOW_BOTTOM).get_parent()
		size = vwwnd.get_size()
		position = vwwnd.get_position()

		self._view.set_border_window_size(gtk.TEXT_WINDOW_BOTTOM, alloc.height)
	
	def attach(self):
		# Attach ourselves in the text view, and position just above the 
		# text window
		self._view.set_border_window_size(gtk.TEXT_WINDOW_BOTTOM, 1)
		alloc = self._view.allocation
		
		self.show()
		self._view.add_child_in_window(self, gtk.TEXT_WINDOW_BOTTOM, 0, 0)
		self.set_size_request(alloc.width, -1)

	def background_gdk(self):
		bg = self.background_color()
		
		bg = map(lambda x: int(x * 65535), bg)
		return gtk.gdk.Color(bg[0], bg[1], bg[2])
		
	def background_color(self):
		bg = self._view.get_style().base[self._view.state]
		
		return [bg.red / 65535.0 * 1.1, bg.green / 65535.0 * 1.1, bg.blue / 65535.0 * 0.9, 0.8]

	def on_entry_focus_out(self, widget, evnt):
		if self._entry.flags() & gtk.SENSITIVE:
			self.destroy()
	
	def on_entry_key_press(self, widget, evnt):
		state = evnt.state & gtk.accelerator_get_default_mod_mask()
		text = self._entry.get_text()
		
		if evnt.keyval == gtk.keysyms.Escape and self._info_window:
			if self._suspended:
				self._suspended.resume()

			if self._info_window:
				self._info_window.destroy()

			self._entry.set_sensitive(True)
			return True

		if evnt.keyval == gtk.keysyms.Escape:
			if text:
				self._entry.set_text('')
			elif self._command_state:
				self._command_state.clear()
				self.prompt()
			else:
				self._view.grab_focus()
				self.destroy()

			return True

		for handler in self._handlers:
			if (handler[0] == None or handler[0] == state) and evnt.keyval == handler[1] and handler[2](handler[3], state):
				return True

		if self._info_window and self._info_window.empty():
			self._info_window.destroy()
		
		self._history_prefix = None
		return False
	
	def on_history_move(self, direction, modifier):
		pos = self._entry.get_position()
		
		self._history.update(self._entry.get_text())
		
		if self._history_prefix == None:
			if len(self._entry.get_text()) == pos:
				self._history_prefix = self._entry.get_chars(0, pos)
			else:
				self._history_prefix = ''
		
		if self._history_prefix == None:
			hist = ''
		else:
			hist = self._history_prefix
			
		next = self._history.move(direction, hist)
		
		if next != None:
			self._entry.set_text(next)
			self._entry.set_position(-1)
		
		return True
	
	def prompt(self, pr=''):
		self._prompt = pr

		if not pr:
			pr = ''
		else:
			pr = ' ' + pr
		
		self._prompt_label.set_markup('<b>&gt;&gt;&gt;</b>%s' % pr)
	
	def make_info(self):
		if self._info_window == None:
			self._info_window = Info(self)
			self._info_window.show()
			
			self._info_window.connect('destroy', self.on_info_window_destroy)
	
	def on_info_window_destroy(self, widget):
		self._info_window = None
	
	def info_show(self, text='', use_markup=False):
		self.make_info()
		self._info_window.add_lines(text, use_markup)
	
	def info_status(self, text):
		self.make_info()
		self._info_window.status(text)
	
	def info_add_action(self, stock, callback, data=None):
		self.make_info()
		return self._info_window.add_action(stock, callback, data)

	def command_history_done(self):
		self._history.update(self._entry.get_text())
		self._history.add()
		self._history_prefix = None
		self._entry.set_text('')
	
	def on_wait_cancel(self):
		if self._suspended:
			self._suspended.resume()
		
		if self._cancel_button:
			self._cancel_button.destroy()

		if self._info_window and self._info_window.empty():
			self._info_window.destroy()
			self._entry.grab_focus()
			self._entry.set_sensitive(True)			
	
	def _show_wait_cancel(self):
		self._cancel_button = self.info_add_action(gtk.STOCK_STOP, self.on_wait_cancel)
		self.info_status('<i>Waiting to finish...</i>')
		
		self._wait_timeout = 0
		return False

	def _complete_word_match(self, match):
		for i in (3, 2, 0):
			if match.group(i) != None:
				return [match.group(i), match.start(i), match.end(i)]

	def on_suspend_resume(self):
		if self._wait_timeout:
			glib.source_remove(self._wait_timeout)
			self._wait_timeout = 0
		else:
			self._cancel_button.destroy()
			self._cancel_button = None
			self.info_status(None)
		
		self._entry.set_sensitive(True)
		self.command_history_done()

		if self._entry.props.has_focus or (self._info_window and not self._info_window.empty()):
			self._entry.grab_focus()

		self.on_execute(None, 0)

	def ellipsize(self, s, size):
		if len(s) <= size:
			return s
		
		mid = (size - 4) / 2
		return s[:mid] + '...' + s[-mid:]

	def destroy(self):
		self.hide()
		gtk.EventBox.destroy(self)

	def on_execute(self, dummy, modifier):
		if self._info_window and not self._suspended:
			self._info_window.destroy()

		text = self._entry.get_text().strip()
		words = list(self._re_complete.finditer(text))
		wordsstr = []
		
		for word in words:
			spec = self._complete_word_match(word)
			wordsstr.append(spec[0])

		if not wordsstr and not self._command_state:
			self._entry.set_text('')
			return
		
		self._suspended = None

		try:
			ret = commands.Commands().execute(self._command_state, text, words, wordsstr, self, modifier)
		except Exception, e:
			self.command_history_done()
			self._command_state.clear()
			
			self.prompt()
			
			# Show error in info
			self.info_show('<b><span color="#f66">Error:</span></b> ' + saxutils.escape(str(e)), True)

			if not isinstance(e, commands.exceptions.Execute):
				self.info_show(traceback.format_exc(), False)

			return True

		if ret == commands.result.Result.SUSPEND:
			# Wait for it...
			self._suspended = ret
			ret.register(self.on_suspend_resume)

			self._wait_timeout = glib.timeout_add(500, self._show_wait_cancel)
			self._entry.set_sensitive(False)
		else:
			self.command_history_done()
			self.prompt('')
			
			if ret == commands.result.Result.PROMPT:
				self.prompt(ret.prompt)
			elif (ret == None or ret == commands.result.HIDE) and not self._prompt and (not self._info_window or self._info_window.empty()):
				self._command_state.clear()
				self._view.grab_focus()
				self.destroy()
			else:
				self._entry.grab_focus()

		return True
Exemple #4
0
class Entry(Gtk.EventBox):
    __gtype_name__ = "CommanderEntry"

    def __init__(self, view):
        Gtk.EventBox.__init__(self)
        self._view = view

        self.set_visible_window(False)

        hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 3)
        hbox.show()
        hbox.set_border_width(3)

        # context for the view
        self._entry = Gtk.Entry()
        self._entry.set_has_frame(False)
        self._entry.set_name("gedit-commander-entry")
        self._entry.show()

        css = Gtk.CssProvider()
        css.load_from_data(
            bytes(
                """
@binding-set terminal-like-bindings {
    unbind "<Control>A";

    bind "<Control>W" { "delete-from-cursor" (word-ends, -1) };
    bind "<Control>A" { "move-cursor" (buffer-ends, -1, 0) };
    bind "<Control>U" { "delete-from-cursor" (display-line-ends, -1) };
    bind "<Control>K" { "delete-from-cursor" (display-line-ends, 1) };
    bind "<Control>E" { "move-cursor" (buffer-ends, 1, 0) };
    bind "Escape" { "delete-from-cursor" (display-lines, 1) };
}

GtkEntry#gedit-commander-entry {
    gtk-key-bindings: terminal-like-bindings;

    /* Override background to anything. This is weird, but doing this we can
       then in code use widget.override_background to set the color dynamically
       to the same color as the gedit view */
    background: transparent;
    border-width: 0;
    box-shadow: 0 0 transparent;
    transition: none;
}
""",
                "utf-8",
            )
        )

        # FIXME: remove hardcopy of 600 (GTK_STYLE_PROVIDER_PRIORITY_APPLICATION)
        # https://bugzilla.gnome.org/show_bug.cgi?id=646860
        self._entry.get_style_context().add_provider(css, 600)

        self._prompt_label = Gtk.Label(label="<b>&gt;&gt;&gt;</b>", use_markup=True)
        self._prompt_label.show()

        self._entry.connect("focus-out-event", self.on_entry_focus_out)
        self._entry.connect("key-press-event", self.on_entry_key_press)

        self._history = History(os.path.join(GLib.get_user_config_dir(), "gedit/commander/history"))
        self._prompt = None

        self._accel_group = None

        hbox.pack_start(self._prompt_label, False, False, 0)
        hbox.pack_start(self._entry, True, True, 0)

        self.copy_style_from_view()
        self.view_style_updated_id = self._view.connect("style-updated", self.on_view_style_updated)

        self.add(hbox)
        self.attach()
        self._entry.grab_focus()

        self._wait_timeout = 0
        self._info_window = None

        self.connect("destroy", self.on_destroy)
        self.connect_after("size-allocate", self.on_size_allocate)
        self.view_draw_id = self._view.connect_after("draw", self.on_draw)

        self._history_prefix = None
        self._suspended = None
        self._handlers = [
            [0, Gdk.KEY_Up, self.on_history_move, -1],
            [0, Gdk.KEY_Down, self.on_history_move, 1],
            [None, Gdk.KEY_Return, self.on_execute, None],
            [None, Gdk.KEY_KP_Enter, self.on_execute, None],
            [0, Gdk.KEY_Tab, self.on_complete, None],
            [0, Gdk.KEY_ISO_Left_Tab, self.on_complete, None],
        ]

        self._re_complete = re.compile("(\"((?:\\\\\"|[^\"])*)\"?|'((?:\\\\'|[^'])*)'?|[^\s]+)")
        self._command_state = commands.Commands.State()

    def on_view_style_updated(self, widget):
        self.copy_style_from_view()

    def get_border_color(self):
        color = self.get_background_color().copy()
        color.red = 1 - color.red
        color.green = 1 - color.green
        color.blue = 1 - color.blue
        color.alpha = 0.5

        return color

    def get_background_color(self):
        context = self._view.get_style_context()
        return context.get_background_color(Gtk.StateFlags.NORMAL)

    def get_foreground_color(self):
        context = self._view.get_style_context()
        return context.get_color(Gtk.StateFlags.NORMAL)

    def get_font(self):
        context = self._view.get_style_context()
        return context.get_font(Gtk.StateFlags.NORMAL)

    def copy_style_from_view(self, widget=None):
        if widget != None:
            context = self._view.get_style_context()
            font = context.get_font(Gtk.StateFlags.NORMAL)

            widget.override_color(Gtk.StateFlags.NORMAL, self.get_foreground_color())
            widget.override_font(self.get_font())
        else:
            if self._entry:
                self.copy_style_from_view(self._entry)

            if self._prompt_label:
                self.copy_style_from_view(self._prompt_label)

    def view(self):
        return self._view

    def on_size_allocate(self, widget, alloc):
        alloc = self.get_allocation()
        self._view.set_border_window_size(Gtk.TextWindowType.BOTTOM, alloc.height)

        win = self._view.get_window(Gtk.TextWindowType.BOTTOM)
        self.set_size_request(win.get_width(), -1)

        # NOTE: we need to do this explicitly somehow, otherwise the window
        # size will not be updated unless something else happens, not exactly
        # sure what. This might be caused by the multi notebook, or custom
        # animation layouting?
        self._view.get_parent().resize_children()

    def attach(self):
        # Attach ourselves in the text view, and position just above the
        # text window
        win = self._view.get_window(Gtk.TextWindowType.TEXT)
        alloc = self.get_allocation()

        self._view.set_border_window_size(Gtk.TextWindowType.BOTTOM, max(alloc.height, 1))
        self._view.add_child_in_window(self, Gtk.TextWindowType.BOTTOM, 0, 0)

        win = self._view.get_window(Gtk.TextWindowType.BOTTOM)

        self.show()
        self.set_size_request(win.get_width(), -1)

    def on_entry_focus_out(self, widget, evnt):
        if self._entry.get_sensitive():
            self.destroy()

    def on_entry_key_press(self, widget, evnt):
        state = evnt.state & Gtk.accelerator_get_default_mod_mask()
        text = self._entry.get_text()

        if evnt.keyval == Gdk.KEY_Escape:
            if self._info_window:
                if self._suspended:
                    self._suspended.resume()

                if self._info_window:
                    self._info_window.destroy()

                self._entry.set_sensitive(True)
            elif self._accel_group:
                self._accel_group = self._accel_group.parent

                if not self._accel_group or not self._accel_group.parent:
                    self._entry.set_editable(True)
                    self._accel_group = None

                self.prompt()
            elif text:
                self._entry.set_text("")
            elif self._command_state:
                self._command_state.clear()
                self.prompt()
            else:
                self._view.grab_focus()
                self.destroy()

            return True

        if state or self._accel_group:
            # Check if it should be handled by the accel group
            group = self._accel_group

            if not self._accel_group:
                group = commands.Commands().accelerator_group()

            accel = group.activate(evnt.keyval, state)

            if isinstance(accel, commands.accel_group.AccelGroup):
                self._accel_group = accel
                self._entry.set_text("")
                self._entry.set_editable(False)
                self.prompt()

                return True
            elif isinstance(accel, commands.accel_group.AccelCallback):
                self._entry.set_editable(True)
                self.run_command(lambda: accel.activate(self._command_state, self))
                return True

        if not self._entry.get_editable():
            return True

        for handler in self._handlers:
            if (
                (handler[0] == None or handler[0] == state)
                and evnt.keyval == handler[1]
                and handler[2](handler[3], state)
            ):
                return True

        if self._info_window and self._info_window.empty():
            self._info_window.destroy()

        self._history_prefix = None
        return False

    def on_history_move(self, direction, modifier):
        pos = self._entry.get_position()

        self._history.update(self._entry.get_text())

        if self._history_prefix == None:
            if len(self._entry.get_text()) == pos:
                self._history_prefix = self._entry.get_chars(0, pos)
            else:
                self._history_prefix = ""

        if self._history_prefix == None:
            hist = ""
        else:
            hist = self._history_prefix

        next = self._history.move(direction, hist)

        if next != None:
            self._entry.set_text(next)
            self._entry.set_position(-1)

        return True

    def prompt(self, pr=""):
        self._prompt = pr

        if self._accel_group != None:
            pr = "<i>%s</i>" % (saxutils.escape(self._accel_group.full_name()),)

        if not pr:
            pr = ""
        else:
            pr = " " + pr

        self._prompt_label.set_markup("<b>&gt;&gt;&gt;</b>%s" % pr)

    def make_info(self):
        if self._info_window == None:
            self._info_window = Info(self)
            self._info_window.show()

            self._info_window.connect("destroy", self.on_info_window_destroy)

    def on_info_window_destroy(self, widget):
        self._info_window = None

    def info_show(self, text="", use_markup=False):
        self.make_info()
        self._info_window.add_lines(text, use_markup)

    def info_status(self, text):
        self.make_info()
        self._info_window.status(text)

    def info_add_action(self, stock, callback, data=None):
        self.make_info()
        return self._info_window.add_action(stock, callback, data)

    def command_history_done(self):
        self._history.add(self._entry.get_text())
        self._history_prefix = None
        self._entry.set_text("")

    def on_wait_cancel(self):
        if self._suspended:
            self._suspended.resume()

        if self._cancel_button:
            self._cancel_button.destroy()

        if self._info_window and self._info_window.empty():
            self._info_window.destroy()
            self._entry.grab_focus()
            self._entry.set_sensitive(True)

    def _show_wait_cancel(self):
        self._cancel_button = self.info_add_action(Gtk.STOCK_STOP, self.on_wait_cancel)
        self.info_status("<i>Waiting to finish...</i>")

        self._wait_timeout = 0
        return False

    def _complete_word_match(self, match):
        for i in (3, 2, 0):
            if match.group(i) != None:
                return [match.group(i), match.start(i), match.end(i)]

    def on_suspend_resume(self):
        if self._wait_timeout:
            GLib.source_remove(self._wait_timeout)
            self._wait_timeout = 0
        else:
            self._cancel_button.destroy()
            self._cancel_button = None
            self.info_status(None)

        self._entry.set_sensitive(True)
        self.command_history_done()

        if self._entry.props.has_focus or (self._info_window and not self._info_window.empty()):
            self._entry.grab_focus()

        self.on_execute(None, 0)

    def ellipsize(self, s, size):
        if len(s) <= size:
            return s

        mid = (size - 4) / 2
        return s[:mid] + "..." + s[-mid:]

    def destroy(self):
        self.hide()
        Gtk.EventBox.destroy(self)

    def run_command(self, cb):
        self._suspended = None

        try:
            ret = cb()
        except Exception as e:
            self.command_history_done()
            self._command_state.clear()

            self.prompt()

            # Show error in info
            self.info_show('<b><span color="#f66">Error:</span></b> ' + saxutils.escape(str(e)), True)

            if not isinstance(e, commands.exceptions.Execute):
                self.info_show(self.format_trace(), False)

            return None

        mod = sys.modules["commander.commands.result"]

        if ret == mod.Result.SUSPEND:
            # Wait for it...
            self._suspended = ret
            ret.register(self.on_suspend_resume)

            self._wait_timeout = GLib.timeout_add(500, self._show_wait_cancel)
            self._entry.set_sensitive(False)
        else:
            self.command_history_done()
            self.prompt("")

            if ret == mod.Result.PROMPT:
                self.prompt(ret.prompt)
            elif (
                (ret == None or ret == mod.HIDE)
                and not self._prompt
                and (not self._info_window or self._info_window.empty())
            ):
                self._command_state.clear()
                self._view.grab_focus()
                self.destroy()
            else:
                self._entry.grab_focus()

        return ret

    def format_trace(self):
        tp, val, tb = sys.exc_info()

        origtb = tb

        thisdir = os.path.dirname(__file__)

        # Skip frames up until after the last entry.py...
        while True:
            filename = tb.tb_frame.f_code.co_filename

            dname = os.path.dirname(filename)

            if not dname.startswith(thisdir):
                break

            tb = tb.tb_next

        msg = traceback.format_exception(tp, val, tb)
        r = "".join(msg[0:-1])

        # This is done to prevent cyclic references, see python
        # documentation on sys.exc_info
        del origtb

        return r

    def on_execute(self, dummy, modifier):
        if self._info_window and not self._suspended:
            self._info_window.destroy()

        text = self._entry.get_text().strip()
        words = list(self._re_complete.finditer(text))
        wordsstr = []

        for word in words:
            spec = self._complete_word_match(word)
            wordsstr.append(spec[0])

        if not wordsstr and not self._command_state:
            self._entry.set_text("")
            return

        self.run_command(
            lambda: commands.Commands().execute(self._command_state, text, words, wordsstr, self, modifier)
        )

        return True

    def on_complete(self, dummy, modifier):
        # First split all the text in words
        text = self._entry.get_text()
        pos = self._entry.get_position()

        words = list(self._re_complete.finditer(text))
        wordsstr = []

        for word in words:
            spec = self._complete_word_match(word)
            wordsstr.append(spec[0])

        # Find out at which word the cursor actually is
        # Examples:
        #  * hello world|
        #  * hello| world
        #  * |hello world
        #  * hello wor|ld
        #  * hello  |  world
        #  * "hello world|"
        posidx = None

        for idx in range(0, len(words)):
            spec = self._complete_word_match(words[idx])

            if words[idx].start(0) > pos:
                # Empty space, new completion
                wordsstr.insert(idx, "")
                words.insert(idx, None)
                posidx = idx
                break
            elif spec[2] == pos:
                # At end of word, resume completion
                posidx = idx
                break
            elif spec[1] <= pos and spec[2] > pos:
                # In middle of word, do not complete
                return True

        if posidx == None:
            wordsstr.append("")
            words.append(None)
            posidx = len(wordsstr) - 1

        # First word completes a command, if not in any special 'mode'
        # otherwise, relay completion to the command, or complete by advice
        # from the 'mode' (prompt)
        cmds = commands.Commands()

        if not self._command_state and posidx == 0:
            # Complete the first command
            ret = commands.completion.command(words=wordsstr, idx=posidx)
        else:
            complete = None
            realidx = posidx

            if not self._command_state:
                # Get the command first
                cmd = commands.completion.single_command(wordsstr, 0)
                realidx -= 1

                ww = wordsstr[1:]
            else:
                cmd = self._command_state.top()
                ww = wordsstr

            if cmd:
                complete = cmd.autocomplete_func()

            if not complete:
                return True

            # 'complete' contains a dict with arg -> func to do the completion
            # of the named argument the command (or stack item) expects
            args, varargs = cmd.args()

            # Remove system arguments
            s = ["argstr", "args", "entry", "view"]
            args = list(filter(lambda x: not x in s, args))

            if realidx < len(args):
                arg = args[realidx]
            elif varargs:
                arg = "*"
            else:
                return True

            if not arg in complete:
                return True

            func = complete[arg]

            try:
                spec = utils.getargspec(func)

                if not ww:
                    ww = [""]

                kwargs = {"words": ww, "idx": realidx, "view": self._view}

                if not spec.keywords:
                    for k in list(kwargs.keys()):
                        if not k in spec.args:
                            del kwargs[k]

                ret = func(**kwargs)
            except Exception as e:
                # Can be number of arguments, or return values or simply buggy
                # modules
                print(e)
                traceback.print_exc()
                return True

        if not ret or not ret[0]:
            return True

        res = ret[0]
        completed = ret[1]

        if len(ret) > 2:
            after = ret[2]
        else:
            after = " "

        # Replace the word
        if words[posidx] == None:
            # At end of everything, just append
            spec = None

            self._entry.insert_text(completed, self._entry.get_text_length())
            self._entry.set_position(-1)
        else:
            spec = self._complete_word_match(words[posidx])

            self._entry.delete_text(spec[1], spec[2])
            self._entry.insert_text(completed, spec[1])
            self._entry.set_position(spec[1] + len(completed))

        if len(res) == 1:
            # Full completion
            lastpos = self._entry.get_position()

            if not isinstance(res[0], commands.module.Module) or not res[0].commands():
                if (
                    words[posidx]
                    and after == " "
                    and (words[posidx].group(2) != None or words[posidx].group(3) != None)
                ):
                    lastpos = lastpos + 1

                self._entry.insert_text(after, lastpos)
                self._entry.set_position(lastpos + 1)
            elif completed == wordsstr[posidx] or not res[0].method:
                self._entry.insert_text(".", lastpos)
                self._entry.set_position(lastpos + 1)

            if self._info_window:
                self._info_window.destroy()
        else:
            # Show popup with completed items
            if self._info_window:
                self._info_window.clear()

            ret = []

            for x in res:
                if isinstance(x, commands.method.Method):
                    ret.append("<b>" + saxutils.escape(x.name) + "</b> (<i>" + x.oneline_doc() + "</i>)")
                else:
                    ret.append(str(x))

            self.info_show("\n".join(ret), True)

        return True

    def on_draw(self, widget, ct):
        win = widget.get_window(Gtk.TextWindowType.BOTTOM)

        if not Gtk.cairo_should_draw_window(ct, win):
            return False

        Gtk.cairo_transform_to_window(ct, widget, win)

        color = self.get_border_color()
        width = win.get_width()

        ct.set_source_rgba(color.red, color.green, color.blue, color.alpha)
        ct.move_to(0, 0)
        ct.line_to(width, 0)
        ct.stroke()

        return False

    def on_destroy(self, widget):
        self._view.set_border_window_size(Gtk.TextWindowType.BOTTOM, 0)
        self._view.disconnect(self.view_style_updated_id)
        self._view.disconnect(self.view_draw_id)

        if self._info_window:
            self._info_window.destroy()

        self._history.save()
Exemple #5
0
class Entry(Gtk.EventBox):
    __gtype_name__ = "CommanderEntry"

    def __init__(self, view):
        Gtk.EventBox.__init__(self)
        self._view = view

        self.set_visible_window(False)

        hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 3)
        hbox.show()
        hbox.set_border_width(3)

        # context for the view
        self._entry = Gtk.Entry()
        self._entry.set_has_frame(False)
        self._entry.set_name('gedit-commander-entry')
        self._entry.show()

        css = Gtk.CssProvider()
        css.load_from_data("""
@binding-set terminal-like-bindings {
    unbind "<Control>A";

    bind "<Control>W" { "delete-from-cursor" (word-ends, -1) };
    bind "<Control>A" { "move-cursor" (buffer-ends, -1, 0) };
    bind "<Control>U" { "delete-from-cursor" (display-line-ends, -1) };
    bind "<Control>K" { "delete-from-cursor" (display-line-ends, 1) };
    bind "<Control>E" { "move-cursor" (buffer-ends, 1, 0) };
    bind "Escape" { "delete-from-cursor" (display-lines, 1) };
}

GtkEntry#gedit-commander-entry {
    gtk-key-bindings: terminal-like-bindings;

    /* Override background to anything. This is weird, but doing this we can
       then in code use widget.override_background to set the color dynamically
       to the same color as the gedit view */
    background: transparent;
    border-width: 0;
    box-shadow: 0 0 transparent;
}
""")

        # FIXME: remove hardcopy of 600 (GTK_STYLE_PROVIDER_PRIORITY_APPLICATION)
        # https://bugzilla.gnome.org/show_bug.cgi?id=646860
        self._entry.get_style_context().add_provider(css, 600)

        self._prompt_label = Gtk.Label(label='<b>&gt;&gt;&gt;</b>',
                                       use_markup=True)
        self._prompt_label.show()

        self._entry.connect('focus-out-event', self.on_entry_focus_out)
        self._entry.connect('key-press-event', self.on_entry_key_press)

        self._history = History(
            os.path.join(GLib.get_user_config_dir(),
                         'gedit/commander/history'))
        self._prompt = None

        self._accel_group = None

        hbox.pack_start(self._prompt_label, False, False, 0)
        hbox.pack_start(self._entry, True, True, 0)

        self.copy_style_from_view()
        self.view_style_updated_id = self._view.connect(
            'style-updated', self.on_view_style_updated)

        self.add(hbox)
        self.attach()
        self._entry.grab_focus()

        self._wait_timeout = 0
        self._info_window = None

        self.connect('destroy', self.on_destroy)
        self.connect_after('size-allocate', self.on_size_allocate)
        self.view_draw_id = self._view.connect_after('draw', self.on_draw)

        self._history_prefix = None
        self._suspended = None
        self._handlers = [[0, Gdk.KEY_Up, self.on_history_move, -1],
                          [0, Gdk.KEY_Down, self.on_history_move, 1],
                          [None, Gdk.KEY_Return, self.on_execute, None],
                          [None, Gdk.KEY_KP_Enter, self.on_execute, None],
                          [0, Gdk.KEY_Tab, self.on_complete, None],
                          [0, Gdk.KEY_ISO_Left_Tab, self.on_complete, None]]

        self._re_complete = re.compile(
            '("((?:\\\\"|[^"])*)"?|\'((?:\\\\\'|[^\'])*)\'?|[^\s]+)')
        self._command_state = commands.Commands.State()

    def on_view_style_updated(self, widget):
        self.copy_style_from_view()

    def get_border_color(self):
        color = self.get_background_color().copy()
        color.red = 1 - color.red
        color.green = 1 - color.green
        color.blue = 1 - color.blue
        color.alpha = 0.5

        return color

    def get_background_color(self):
        context = self._view.get_style_context()
        return context.get_background_color(Gtk.StateFlags.NORMAL)

    def get_foreground_color(self):
        context = self._view.get_style_context()
        return context.get_color(Gtk.StateFlags.NORMAL)

    def get_font(self):
        context = self._view.get_style_context()
        return context.get_font(Gtk.StateFlags.NORMAL)

    def copy_style_from_view(self, widget=None):
        if widget != None:
            context = self._view.get_style_context()
            font = context.get_font(Gtk.StateFlags.NORMAL)

            widget.override_color(Gtk.StateFlags.NORMAL,
                                  self.get_foreground_color())
            widget.override_font(self.get_font())
        else:
            if self._entry:
                self.copy_style_from_view(self._entry)

            if self._prompt_label:
                self.copy_style_from_view(self._prompt_label)

    def view(self):
        return self._view

    def on_size_allocate(self, widget, alloc):
        alloc = self.get_allocation()
        self._view.set_border_window_size(Gtk.TextWindowType.BOTTOM,
                                          alloc.height)

        win = self._view.get_window(Gtk.TextWindowType.BOTTOM)
        self.set_size_request(win.get_width(), -1)

        # NOTE: we need to do this explicitly somehow, otherwise the window
        # size will not be updated unless something else happens, not exactly
        # sure what. This might be caused by the multi notebook, or custom
        # animation layouting?
        self._view.get_parent().resize_children()

    def attach(self):
        # Attach ourselves in the text view, and position just above the
        # text window
        win = self._view.get_window(Gtk.TextWindowType.TEXT)
        alloc = self.get_allocation()

        self._view.set_border_window_size(Gtk.TextWindowType.BOTTOM,
                                          max(alloc.height, 1))
        self._view.add_child_in_window(self, Gtk.TextWindowType.BOTTOM, 0, 0)

        win = self._view.get_window(Gtk.TextWindowType.BOTTOM)

        self.show()
        self.set_size_request(win.get_width(), -1)

    def on_entry_focus_out(self, widget, evnt):
        if self._entry.get_sensitive():
            self.destroy()

    def on_entry_key_press(self, widget, evnt):
        state = evnt.state & Gtk.accelerator_get_default_mod_mask()
        text = self._entry.get_text()

        if evnt.keyval == Gdk.KEY_Escape:
            if self._info_window:
                if self._suspended:
                    self._suspended.resume()

                if self._info_window:
                    self._info_window.destroy()

                self._entry.set_sensitive(True)
            elif self._accel_group:
                self._accel_group = self._accel_group.parent

                if not self._accel_group or not self._accel_group.parent:
                    self._entry.set_editable(True)
                    self._accel_group = None

                self.prompt()
            elif text:
                self._entry.set_text('')
            elif self._command_state:
                self._command_state.clear()
                self.prompt()
            else:
                self._view.grab_focus()
                self.destroy()

            return True

        if state or self._accel_group:
            # Check if it should be handled by the accel group
            group = self._accel_group

            if not self._accel_group:
                group = commands.Commands().accelerator_group()

            accel = group.activate(evnt.keyval, state)

            if isinstance(accel, commands.accel_group.AccelGroup):
                self._accel_group = accel
                self._entry.set_text('')
                self._entry.set_editable(False)
                self.prompt()

                return True
            elif isinstance(accel, commands.accel_group.AccelCallback):
                self._entry.set_editable(True)
                self.run_command(
                    lambda: accel.activate(self._command_state, self))
                return True

        if not self._entry.get_editable():
            return True

        for handler in self._handlers:
            if (handler[0] == None or handler[0]
                    == state) and evnt.keyval == handler[1] and handler[2](
                        handler[3], state):
                return True

        if self._info_window and self._info_window.empty():
            self._info_window.destroy()

        self._history_prefix = None
        return False

    def on_history_move(self, direction, modifier):
        pos = self._entry.get_position()

        self._history.update(self._entry.get_text())

        if self._history_prefix == None:
            if len(self._entry.get_text()) == pos:
                self._history_prefix = self._entry.get_chars(0, pos)
            else:
                self._history_prefix = ''

        if self._history_prefix == None:
            hist = ''
        else:
            hist = self._history_prefix

        next = self._history.move(direction, hist)

        if next != None:
            self._entry.set_text(next)
            self._entry.set_position(-1)

        return True

    def prompt(self, pr=''):
        self._prompt = pr

        if self._accel_group != None:
            pr = '<i>%s</i>' % (saxutils.escape(
                self._accel_group.full_name()), )

        if not pr:
            pr = ''
        else:
            pr = ' ' + pr

        self._prompt_label.set_markup('<b>&gt;&gt;&gt;</b>%s' % pr)

    def make_info(self):
        if self._info_window == None:
            self._info_window = Info(self)
            self._info_window.show()

            self._info_window.connect('destroy', self.on_info_window_destroy)

    def on_info_window_destroy(self, widget):
        self._info_window = None

    def info_show(self, text='', use_markup=False):
        self.make_info()
        self._info_window.add_lines(text, use_markup)

    def info_status(self, text):
        self.make_info()
        self._info_window.status(text)

    def info_add_action(self, stock, callback, data=None):
        self.make_info()
        return self._info_window.add_action(stock, callback, data)

    def command_history_done(self):
        self._history.add(self._entry.get_text())
        self._history_prefix = None
        self._entry.set_text('')

    def on_wait_cancel(self):
        if self._suspended:
            self._suspended.resume()

        if self._cancel_button:
            self._cancel_button.destroy()

        if self._info_window and self._info_window.empty():
            self._info_window.destroy()
            self._entry.grab_focus()
            self._entry.set_sensitive(True)

    def _show_wait_cancel(self):
        self._cancel_button = self.info_add_action(Gtk.STOCK_STOP,
                                                   self.on_wait_cancel)
        self.info_status('<i>Waiting to finish...</i>')

        self._wait_timeout = 0
        return False

    def _complete_word_match(self, match):
        for i in (3, 2, 0):
            if match.group(i) != None:
                return [match.group(i), match.start(i), match.end(i)]

    def on_suspend_resume(self):
        if self._wait_timeout:
            GObject.source_remove(self._wait_timeout)
            self._wait_timeout = 0
        else:
            self._cancel_button.destroy()
            self._cancel_button = None
            self.info_status(None)

        self._entry.set_sensitive(True)
        self.command_history_done()

        if self._entry.props.has_focus or (self._info_window
                                           and not self._info_window.empty()):
            self._entry.grab_focus()

        self.on_execute(None, 0)

    def ellipsize(self, s, size):
        if len(s) <= size:
            return s

        mid = (size - 4) / 2
        return s[:mid] + '...' + s[-mid:]

    def destroy(self):
        self.hide()
        Gtk.EventBox.destroy(self)

    def run_command(self, cb):
        self._suspended = None

        try:
            ret = cb()
        except Exception, e:
            self.command_history_done()
            self._command_state.clear()

            self.prompt()

            # Show error in info
            self.info_show(
                '<b><span color="#f66">Error:</span></b> ' +
                saxutils.escape(str(e)), True)

            if not isinstance(e, commands.exceptions.Execute):
                self.info_show(self.format_trace(), False)

            return None

        if ret == commands.result.Result.SUSPEND:
            # Wait for it...
            self._suspended = ret
            ret.register(self.on_suspend_resume)

            self._wait_timeout = GObject.timeout_add(500,
                                                     self._show_wait_cancel)
            self._entry.set_sensitive(False)
        else:
            self.command_history_done()
            self.prompt('')

            if ret == commands.result.Result.PROMPT:
                self.prompt(ret.prompt)
            elif (ret == None
                  or ret == commands.result.HIDE) and not self._prompt and (
                      not self._info_window or self._info_window.empty()):
                self._command_state.clear()
                self._view.grab_focus()
                self.destroy()
            else:
                self._entry.grab_focus()

        return ret
Exemple #6
0
class Entry(Gtk.Box):
    __gtype_name__ = "CommanderEntry"

    def _show(self):
        self._reveal.set_reveal_child(True)

    def _hide(self):
        self._reveal.set_reveal_child(False)

    def __init__(self, view):
        super(Entry, self).__init__()

        self._view = view
        view.connect("destroy", self._on_view_destroyed)

        self._history = History(os.path.join(GLib.get_user_config_dir(), 'gedit/commander/history'))
        self._history_prefix = None

        self._prompt_text = None
        self._accel_group = None

        self._wait_timeout = 0
        self._cancel_button = None
        self._info = None
        self._info_revealer = None

        self._suspended = None

        self._handlers = [
            [0, Gdk.KEY_Up, self._on_history_move, -1],
            [0, Gdk.KEY_Down, self._on_history_move, 1],
            [None, Gdk.KEY_Return, self._on_execute, None],
            [None, Gdk.KEY_KP_Enter, self._on_execute, None],
            [0, Gdk.KEY_Tab, self._on_complete, None],
            [0, Gdk.KEY_ISO_Left_Tab, self._on_complete, None]
        ]

        self._re_complete = re.compile('("((?:\\\\"|[^"])*)"?|\'((?:\\\\\'|[^\'])*)\'?|[^\s]+)')
        self._command_state = commands.Commands.State()

        self.connect('destroy', self._on_destroy)

        self._build_ui()
        self._setup_keybindings()

        self._attach()

    def view(self):
        return self._view

    def _setup_keybindings(self):
        css = Gtk.CssProvider()
        css.load_from_data(bytes("""
@binding-set terminal-like-bindings {
    unbind "<Control>A";

    bind "<Control>W" { "delete-from-cursor" (word-ends, -1) };
    bind "<Control>A" { "move-cursor" (buffer-ends, -1, 0) };
    bind "<Control>U" { "delete-from-cursor" (display-line-ends, -1) };
    bind "<Control>K" { "delete-from-cursor" (display-line-ends, 1) };
    bind "<Control>E" { "move-cursor" (buffer-ends, 1, 0) };
    bind "Escape" { "delete-from-cursor" (display-lines, 1) };
}

GtkEntry#gedit-commander-entry {
    gtk-key-bindings: terminal-like-bindings;

    background-image: none;
    box-shadow: 0 0;
    transition: none;
    border: 0;
}

""", 'utf-8'))

        self._entry.get_style_context().add_provider(css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)

    def _find_overlay(self, view):
        parent = view.get_parent()

        while not isinstance(parent, Gtk.Overlay):
            parent = parent.get_parent()

        return parent

    def _build_ui(self):
        self.set_orientation(Gtk.Orientation.VERTICAL)

        self._overlay = self._find_overlay(self._view)

        hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
        hbox.show()

        self.pack_end(hbox, False, False, 0)

        self._info_revealer = Gtk.Revealer()

        self._info_revealer.set_transition_type(Gtk.RevealerTransitionType.SLIDE_UP)
        self._info_revealer.set_transition_duration(150)

        self.pack_start(self._info_revealer, False, False, 0)
        self._info_revealer.connect('notify::child-revealed', self._on_info_revealer_child_revealed)

        self._prompt_label = Gtk.Label(label='<b>&gt;&gt;&gt;</b>', use_markup=True)
        self._prompt_label.set_margin_top(3)
        self._prompt_label.set_margin_bottom(3)
        self._prompt_label.set_margin_start(3)

        self._prompt_label.show()
        hbox.add(self._prompt_label)

        self._entry = Gtk.Entry()
        self._entry.set_has_frame(False)
        self._entry.set_name('gedit-commander-entry')
        self._entry.set_hexpand(True)
        self._entry.set_margin_top(3)
        self._entry.set_margin_bottom(3)
        self._entry.set_margin_end(3)

        self._entry.connect('key-press-event', self._on_entry_key_press)

        self._entry.show()
        hbox.add(self._entry)

        self._copy_style_from_view()
        self._view_style_updated_id = self._view.connect('style-updated', self._on_view_style_updated)

    def _on_view_destroyed (self, widget, user_data=None):
        self._view.disconnect(self._view_style_updated_id)
        self._view_style_updated_id = None

    def _on_view_style_updated(self, widget):
        self._copy_style_from_view()

    @property
    def _border_color(self):
        style = self._view.get_buffer().get_style_scheme().get_style('right-margin')

        if not style is None and style.props.foreground_set:
            color = Gdk.RGBA()
            color.parse(style.props.foreground)
        else:
            color = self._get_background_color(Gtk.StateFlags.NORMAL, 'bottom').copy()
            color.red = 1 - color.red
            color.green = 1 - color.green
            color.blue = 1 - color.blue

        color.alpha = 0.3
        return color

    def _get_background_color(self, state, cls=None):
        context = self._view.get_style_context()

        context.save()

        if not cls is None:
            context.add_class(cls)

        ret = context.get_background_color(state)

        context.restore()

        return ret

    def _get_foreground_color(self, state, cls=None):
        context = self._view.get_style_context()

        context.save()

        if not cls is None:
            context.add_class(cls)

        ret = context.get_color(state)

        context.restore()

        return ret

    def _get_font(self):
        context = self._view.get_style_context()
        return context.get_font(Gtk.StateFlags.NORMAL)

    def _styled_widgets(self):
        widgets = [self, self._entry, self._prompt_label]

        if not self._info is None:
            widgets.append(self._info)
            widgets.append(self._info.text_view)

        return widgets

    def _modify_bg(self, col, widget):
        if self._info is None or (self._info.text_view != widget and self._info != widget):
            return col

        d = 0.1

        h, l, s = colorsys.rgb_to_hls(col.red, col.green, col.blue)

        if l < 0.5:
            factor = 1 + d
        else:
            factor = 1 - d

        l = max(0, min(1, l * factor))
        s = max(0, min(1, s * factor))

        r, g, b = colorsys.hls_to_rgb(h, l, s)

        return Gdk.RGBA(r, g, b, col.alpha)

    def _copy_style_from_view(self, widgets=None):
        font = self._get_font()
        fg = self._get_foreground_color(Gtk.StateFlags.NORMAL, 'bottom')
        bg = self._get_background_color(Gtk.StateFlags.NORMAL, 'bottom')

        fgsel = self._get_foreground_color(Gtk.StateFlags.SELECTED)
        bgsel = self._get_background_color(Gtk.StateFlags.SELECTED)

        cursor = self._view.style_get_property('cursor-color')

        if not cursor is None:
            cursor = Gdk.RGBA.from_color(cursor)

        secondary_cursor = self._view.style_get_property('secondary-cursor-color')

        if not secondary_cursor is None:
            secondary_cursor = Gdk.RGBA.from_color(secondary_cursor)

        if widgets is None:
            widgets = self._styled_widgets()

        for widget in widgets:
            widget.override_color(Gtk.StateFlags.NORMAL, fg)
            widget.override_background_color(Gtk.StateFlags.NORMAL, self._modify_bg(bg, widget))

            widget.override_color(Gtk.StateFlags.SELECTED, fgsel)
            widget.override_background_color(Gtk.StateFlags.SELECTED, self._modify_bg(bgsel, widget))

            widget.override_font(font)
            widget.override_cursor(cursor, secondary_cursor)

    def _attach(self):
        reveal = Gtk.Revealer()
        reveal.add(self)
        self.show()

        reveal.set_transition_type(Gtk.RevealerTransitionType.SLIDE_UP)
        reveal.set_transition_duration(200)

        reveal.set_valign(Gtk.Align.END)
        reveal.set_halign(Gtk.Align.FILL)

        self._overlay.add_overlay(reveal)
        reveal.show()

        reveal.set_reveal_child(True)
        self._reveal = reveal

        self._entry.grab_focus()

    def grab_focus(self):
        self._entry.grab_focus()

    def _on_entry_key_press(self, widget, evnt):
        state = evnt.state & Gtk.accelerator_get_default_mod_mask()
        text = self._entry.get_text()

        if evnt.keyval == Gdk.KEY_Escape:
            if not self._info is None:
                if not self._suspended is None:
                    self._suspended.resume()

                if not self._info is None:
                    self._info_revealer.set_reveal_child(False)

                self._entry.set_sensitive(True)
            elif self._accel_group:
                self._accel_group = self._accel_group.parent

                if not self._accel_group or not self._accel_group.parent:
                    self._entry.set_editable(True)
                    self._accel_group = None

                self._prompt()
            elif text:
                self._entry.set_text('')
            elif self._command_state:
                self._command_state.clear()
                self._prompt()
            else:
                self._view.grab_focus()
                self._reveal.set_reveal_child(False)

            return True

        if state or self._accel_group:
            # Check if it should be handled by the accel group
            group = self._accel_group

            if not self._accel_group:
                group = commands.Commands().accelerator_group()

            accel = group.activate(evnt.keyval, state)

            if isinstance(accel, commands.accel_group.AccelGroup):
                self._accel_group = accel
                self._entry.set_text('')
                self._entry.set_editable(False)
                self._prompt()

                return True
            elif isinstance(accel, commands.accel_group.AccelCallback):
                self._entry.set_editable(True)
                self._run_command(lambda: accel.activate(self._command_state, self))
                return True

        if not self._entry.get_editable():
            return True

        for handler in self._handlers:
            if (handler[0] == None or handler[0] == state) and evnt.keyval == handler[1] and handler[2](handler[3], state):
                return True

        if not self._info is None and self._info.is_empty:
            self._info_revealer.set_reveal_child(False)

        self._history_prefix = None
        return False

    def _on_history_move(self, direction, modifier):
        pos = self._entry.get_position()

        self._history.update(self._entry.get_text())

        if self._history_prefix == None:
            if len(self._entry.get_text()) == pos:
                self._history_prefix = self._entry.get_chars(0, pos)
            else:
                self._history_prefix = ''

        if self._history_prefix == None:
            hist = ''
        else:
            hist = self._history_prefix

        next = self._history.move(direction, hist)

        if next != None:
            self._entry.set_text(next)
            self._entry.set_position(-1)

        return True

    def _prompt(self, pr=''):
        self._prompt_text = pr

        if self._accel_group != None:
            pr = '<i>%s</i>' % (saxutils.escape(self._accel_group.full_name()),)

        if not pr:
            pr = ''
        else:
            pr = ' ' + pr

        self._prompt_label.set_markup('<b>&gt;&gt;&gt;</b>%s' % pr)

    def _make_info(self):
        if self._info is None:
            self._info = Info()

            self._copy_style_from_view([self._info, self._info.text_view])

            self._info_revealer.add(self._info)
            self._info.show()

            self._info_revealer.show()
            self._info_revealer.set_reveal_child(True)

            self._info.connect('destroy', self._on_info_destroy)

    def _on_info_revealer_child_revealed(self, widget, pspec):
        if not self._info_revealer.get_child_revealed():
            self._info.destroy()
            self._info_revealer.hide()

    def _on_info_destroy(self, widget):
        self._info = None

    def info_show(self, text='', use_markup=False):
        self._make_info()
        self._info.add_lines(text, use_markup)

    def info_status(self, text):
        self._make_info()
        self._info.status(text)

    def _info_add_action(self, stock, callback, data=None):
        self._make_info()
        return self._info.add_action(stock, callback, data)

    def _command_history_done(self):
        self._history.add(self._entry.get_text())
        self._history_prefix = None
        self._entry.set_text('')

    def _on_wait_cancel(self):
        self._on_execute(None, 0)

    def _show_wait_cancel(self):
        self._cancel_button = self._info_add_action('process-stop-symbolic', self._on_wait_cancel)
        self.info_status('<i>Waiting to finish\u2026</i>')

        self._wait_timeout = 0
        return False

    def _complete_word_match(self, match):
        for i in (3, 2, 0):
            if match.group(i) != None:
                return [match.group(i), match.start(i), match.end(i)]

    def _on_suspend_resume(self):
        if self._wait_timeout:
            GLib.source_remove(self._wait_timeout)
            self._wait_timeout = 0
        else:
            if not self._cancel_button is None:
                self._cancel_button.destroy()
                self._cancel_button = None

            self.info_status(None)

        self._entry.set_sensitive(True)
        self._command_history_done()

        if self._entry.props.has_focus or (not self._info is None and not self._info.is_empty):
            self._entry.grab_focus()

    def _run_command(self, cb):
        self._suspended = None

        try:
            ret = cb()
        except Exception as e:
            sys.stderr.write(self._format_trace() + '\n')

            self._command_history_done()
            self._command_state.clear()

            self._prompt()

            # Show error in info
            self.info_show('<b><span color="#f66">Error:</span></b> ' + saxutils.escape(str(e)), True)

            if not isinstance(e, commands.exceptions.Execute):
                self.info_show(self._format_trace(), False)

            return None

        mod = sys.modules['commander.commands.result']

        if ret == mod.Result.SUSPEND:
            # Wait for it...
            self._suspended = ret
            ret.register(self._on_suspend_resume)

            self._wait_timeout = GLib.timeout_add(500, self._show_wait_cancel)
            self._entry.set_sensitive(False)
        else:
            self._command_history_done()
            self._prompt('')

            if ret == mod.Result.PROMPT:
                self._prompt(ret.prompt)
            elif (ret == None or ret == mod.HIDE) and not self._prompt_text and (self._info is None or self._info.is_empty):
                self._command_state.clear()
                self._view.grab_focus()
                self._reveal.set_reveal_child(False)
            else:
                self._entry.grab_focus()

        return ret

    def _format_trace(self):
        tp, val, tb = sys.exc_info()

        origtb = tb

        thisdir = os.path.dirname(__file__)

        # Skip frames up until after the last entry.py...
        while not tb is None:
            filename = tb.tb_frame.f_code.co_filename

            dname = os.path.dirname(filename)

            if not dname.startswith(thisdir):
                break

            tb = tb.tb_next

        msg = traceback.format_exception(tp, val, tb)
        r = ''.join(msg[0:-1])

        # This is done to prevent cyclic references, see python
        # documentation on sys.exc_info
        del origtb

        return r

    def _on_execute(self, dummy, modifier):
        if not self._info is None and not self._suspended:
            self._info_revealer.set_reveal_child(False)

        text = self._entry.get_text().strip()
        words = list(self._re_complete.finditer(text))
        wordsstr = []

        for word in words:
            spec = self._complete_word_match(word)
            wordsstr.append(spec[0])

        if not wordsstr and not self._command_state:
            self._entry.set_text('')
            return

        self._run_command(lambda: commands.Commands().execute(self._command_state, text, words, wordsstr, self, modifier))

        return True

    def _on_complete(self, dummy, modifier):
        # First split all the text in words
        text = self._entry.get_text()
        pos = self._entry.get_position()

        words = list(self._re_complete.finditer(text))
        wordsstr = []

        for word in words:
            spec = self._complete_word_match(word)
            wordsstr.append(spec[0])

        # Find out at which word the cursor actually is
        # Examples:
        #  * hello world|
        #  * hello| world
        #  * |hello world
        #  * hello wor|ld
        #  * hello  |  world
        #  * "hello world|"
        posidx = None

        for idx in range(0, len(words)):
            spec = self._complete_word_match(words[idx])

            if words[idx].start(0) > pos:
                # Empty space, new completion
                wordsstr.insert(idx, '')
                words.insert(idx, None)
                posidx = idx
                break
            elif spec[2] == pos:
                # At end of word, resume completion
                posidx = idx
                break
            elif spec[1] <= pos and spec[2] > pos:
                # In middle of word, do not complete
                return True

        if posidx == None:
            wordsstr.append('')
            words.append(None)
            posidx = len(wordsstr) - 1

        # First word completes a command, if not in any special 'mode'
        # otherwise, relay completion to the command, or complete by advice
        # from the 'mode' (prompt)
        cmds = commands.Commands()

        if not self._command_state and posidx == 0:
            # Complete the first command
            ret = commands.completion.command(words=wordsstr, idx=posidx)
        else:
            complete = None
            realidx = posidx

            if not self._command_state:
                # Get the command first
                cmd = commands.completion.single_command(wordsstr, 0)
                realidx -= 1

                ww = wordsstr[1:]
            else:
                cmd = self._command_state.top()
                ww = wordsstr

            if cmd:
                complete = cmd.autocomplete_func()

            if not complete:
                return True

            # 'complete' contains a dict with arg -> func to do the completion
            # of the named argument the command (or stack item) expects
            args, varargs = cmd.args()

            # Remove system arguments
            s = ['argstr', 'args', 'entry', 'view']
            args = list(filter(lambda x: not x in s, args))

            if realidx < len(args):
                arg = args[realidx]
            elif varargs:
                arg = '*'
            else:
                return True

            if not arg in complete:
                return True

            func = complete[arg]

            try:
                spec = utils.getargspec(func)

                if not ww:
                    ww = ['']

                kwargs = {
                    'words': ww,
                    'idx': realidx,
                    'view': self._view
                }

                if not spec.keywords:
                    for k in list(kwargs.keys()):
                        if not k in spec.args:
                            del kwargs[k]

                ret = func(**kwargs)
            except Exception as e:
                # Can be number of arguments, or return values or simply buggy
                # modules
                print(e)
                traceback.print_exc()
                return True

        if not ret or not ret[0]:
            return True

        res = ret[0]
        completed = ret[1]

        if len(ret) > 2:
            after = ret[2]
        else:
            after = ' '

        # Replace the word
        if words[posidx] == None:
            # At end of everything, just append
            spec = None

            self._entry.insert_text(completed, self._entry.get_text_length())
            self._entry.set_position(-1)
        else:
            spec = self._complete_word_match(words[posidx])

            self._entry.delete_text(spec[1], spec[2])
            self._entry.insert_text(completed, spec[1])
            self._entry.set_position(spec[1] + len(completed))

        if len(res) == 1:
            # Full completion
            lastpos = self._entry.get_position()

            if not isinstance(res[0], commands.module.Module) or not res[0].commands():
                if words[posidx] and after == ' ' and (words[posidx].group(2) != None or words[posidx].group(3) != None):
                    lastpos = lastpos + 1

                self._entry.insert_text(after, lastpos)
                self._entry.set_position(lastpos + 1)
            elif completed == wordsstr[posidx] or not res[0].method:
                self._entry.insert_text('.', lastpos)
                self._entry.set_position(lastpos + 1)

            if not self._info is None:
                self._info_revealer.set_reveal_child(False)
        else:
            # Show popup with completed items
            if not self._info is None:
                self._info.clear()

            ret = []

            for x in res:
                if isinstance(x, commands.method.Method):
                    ret.append('<b>' + saxutils.escape(x.name) + '</b> (<i>' + x.oneline_doc() + '</i>)')
                else:
                    ret.append(str(x))

            self.info_show("\n".join(ret), True)

        return True

    def do_draw(self, ctx):
        ret = Gtk.Box.do_draw(self, ctx)

        col = self._border_color

        ctx.set_line_width(1)
        ctx.set_source_rgba(col.red, col.green, col.blue, col.alpha)

        w = self.get_allocated_width()

        ctx.move_to(0, 0.5)
        ctx.line_to(w, 0.5)
        ctx.stroke()

        if not self._info is None:
            alloc = self._info_revealer.get_allocation()
            y = alloc.y + alloc.height + 0.5

            if y >= 3:
                ctx.move_to(0, y)
                ctx.line_to(w, y)
                ctx.stroke()

        return ret

    def _on_destroy(self, widget):
        # Note we do this not as an override because somehow something
        # goes wrong when finalizing in that case, maybe self is NULL
        # or something like that, and then gets some new empty instance?
        if self._view_style_updated_id:
            self._view.disconnect(self._view_style_updated_id)

        self._history.save()

        self._view = None
        self._view_style_updated_id = None
Exemple #7
0
class Entry(Gtk.Box):
    __gtype_name__ = "CommanderEntry"

    def _show(self):
        self._reveal.set_reveal_child(True)

    def _hide(self):
        self._reveal.set_reveal_child(False)

    def __init__(self, view):
        super(Entry, self).__init__()

        self._view = view
        view.connect("destroy", self._on_view_destroyed)

        self._history = History(
            os.path.join(GLib.get_user_config_dir(),
                         'gedit/commander/history'))
        self._history_prefix = None

        self._prompt_text = None
        self._accel_group = None

        self._wait_timeout = 0
        self._cancel_button = None
        self._info = None
        self._info_revealer = None

        self._suspended = None

        self._handlers = [[0, Gdk.KEY_Up, self._on_history_move, -1],
                          [0, Gdk.KEY_Down, self._on_history_move, 1],
                          [None, Gdk.KEY_Return, self._on_execute, None],
                          [None, Gdk.KEY_KP_Enter, self._on_execute, None],
                          [0, Gdk.KEY_Tab, self._on_complete, None],
                          [0, Gdk.KEY_ISO_Left_Tab, self._on_complete, None]]

        self._re_complete = re.compile(
            '("((?:\\\\"|[^"])*)"?|\'((?:\\\\\'|[^\'])*)\'?|[^\s]+)')
        self._command_state = commands.Commands.State()

        self.connect('destroy', self._on_destroy)

        self._build_ui()
        self._setup_keybindings()

        self._attach()

    def view(self):
        return self._view

    def _setup_keybindings(self):
        css = Gtk.CssProvider()
        css.load_from_data(
            bytes(
                """
@binding-set terminal-like-bindings {
    unbind "<Control>A";

    bind "<Control>W" { "delete-from-cursor" (word-ends, -1) };
    bind "<Control>A" { "move-cursor" (buffer-ends, -1, 0) };
    bind "<Control>U" { "delete-from-cursor" (display-line-ends, -1) };
    bind "<Control>K" { "delete-from-cursor" (display-line-ends, 1) };
    bind "<Control>E" { "move-cursor" (buffer-ends, 1, 0) };
    bind "Escape" { "delete-from-cursor" (display-lines, 1) };
}

GtkEntry#gedit-commander-entry {
    gtk-key-bindings: terminal-like-bindings;

    background-image: none;
    box-shadow: 0 0;
    transition: none;
    border: 0;
}

""", 'utf-8'))

        self._entry.get_style_context().add_provider(
            css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)

    def _find_overlay(self, view):
        parent = view.get_parent()

        while not isinstance(parent, Gtk.Overlay):
            parent = parent.get_parent()

        return parent

    def _build_ui(self):
        self.set_orientation(Gtk.Orientation.VERTICAL)

        self._overlay = self._find_overlay(self._view)

        hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
        hbox.show()

        self.pack_end(hbox, False, False, 0)

        self._info_revealer = Gtk.Revealer()

        self._info_revealer.set_transition_type(
            Gtk.RevealerTransitionType.SLIDE_UP)
        self._info_revealer.set_transition_duration(150)

        self.pack_start(self._info_revealer, False, False, 0)
        self._info_revealer.connect('notify::child-revealed',
                                    self._on_info_revealer_child_revealed)

        self._prompt_label = Gtk.Label(label='<b>&gt;&gt;&gt;</b>',
                                       use_markup=True)
        self._prompt_label.set_margin_top(3)
        self._prompt_label.set_margin_bottom(3)
        self._prompt_label.set_margin_start(3)

        self._prompt_label.show()
        hbox.add(self._prompt_label)

        self._entry = Gtk.Entry()
        self._entry.set_has_frame(False)
        self._entry.set_name('gedit-commander-entry')
        self._entry.set_hexpand(True)
        self._entry.set_margin_top(3)
        self._entry.set_margin_bottom(3)
        self._entry.set_margin_end(3)

        self._entry.connect('key-press-event', self._on_entry_key_press)

        self._entry.show()
        hbox.add(self._entry)

        self._copy_style_from_view()
        self._view_style_updated_id = self._view.connect(
            'style-updated', self._on_view_style_updated)

    def _on_view_destroyed(self, widget, user_data=None):
        self._view.disconnect(self._view_style_updated_id)
        self._view_style_updated_id = None

    def _on_view_style_updated(self, widget):
        self._copy_style_from_view()

    @property
    def _border_color(self):
        style = self._view.get_buffer().get_style_scheme().get_style(
            'right-margin')

        if not style is None and style.props.foreground_set:
            color = Gdk.RGBA()
            color.parse(style.props.foreground)
        else:
            color = self._get_background_color(Gtk.StateFlags.NORMAL,
                                               'bottom').copy()
            color.red = 1 - color.red
            color.green = 1 - color.green
            color.blue = 1 - color.blue

        color.alpha = 0.3
        return color

    def _get_background_color(self, state, cls=None):
        context = self._view.get_style_context()

        context.save()

        if not cls is None:
            context.add_class(cls)

        ret = context.get_background_color(state)

        context.restore()

        return ret

    def _get_foreground_color(self, state, cls=None):
        context = self._view.get_style_context()

        context.save()

        if not cls is None:
            context.add_class(cls)

        ret = context.get_color(state)

        context.restore()

        return ret

    def _get_font(self):
        context = self._view.get_style_context()
        return context.get_font(Gtk.StateFlags.NORMAL)

    def _styled_widgets(self):
        widgets = [self, self._entry, self._prompt_label]

        if not self._info is None:
            widgets.append(self._info)
            widgets.append(self._info.text_view)

        return widgets

    def _modify_bg(self, col, widget):
        if self._info is None or (self._info.text_view != widget
                                  and self._info != widget):
            return col

        d = 0.1

        h, l, s = colorsys.rgb_to_hls(col.red, col.green, col.blue)

        if l < 0.5:
            factor = 1 + d
        else:
            factor = 1 - d

        l = max(0, min(1, l * factor))
        s = max(0, min(1, s * factor))

        r, g, b = colorsys.hls_to_rgb(h, l, s)

        return Gdk.RGBA(r, g, b, col.alpha)

    def _copy_style_from_view(self, widgets=None):
        font = self._get_font()
        fg = self._get_foreground_color(Gtk.StateFlags.NORMAL, 'bottom')
        bg = self._get_background_color(Gtk.StateFlags.NORMAL, 'bottom')

        fgsel = self._get_foreground_color(Gtk.StateFlags.SELECTED)
        bgsel = self._get_background_color(Gtk.StateFlags.SELECTED)

        cursor = self._view.style_get_property('cursor-color')

        if not cursor is None:
            cursor = Gdk.RGBA.from_color(cursor)

        secondary_cursor = self._view.style_get_property(
            'secondary-cursor-color')

        if not secondary_cursor is None:
            secondary_cursor = Gdk.RGBA.from_color(secondary_cursor)

        if widgets is None:
            widgets = self._styled_widgets()

        for widget in widgets:
            widget.override_color(Gtk.StateFlags.NORMAL, fg)
            widget.override_background_color(Gtk.StateFlags.NORMAL,
                                             self._modify_bg(bg, widget))

            widget.override_color(Gtk.StateFlags.SELECTED, fgsel)
            widget.override_background_color(Gtk.StateFlags.SELECTED,
                                             self._modify_bg(bgsel, widget))

            widget.override_font(font)
            widget.override_cursor(cursor, secondary_cursor)

    def _attach(self):
        reveal = Gtk.Revealer()
        reveal.add(self)
        self.show()

        reveal.set_transition_type(Gtk.RevealerTransitionType.SLIDE_UP)
        reveal.set_transition_duration(200)

        reveal.set_valign(Gtk.Align.END)
        reveal.set_halign(Gtk.Align.FILL)

        self._overlay.add_overlay(reveal)
        reveal.show()

        reveal.set_reveal_child(True)
        self._reveal = reveal

        self._entry.grab_focus()

    def grab_focus(self):
        self._entry.grab_focus()

    def _on_entry_key_press(self, widget, evnt):
        state = evnt.state & Gtk.accelerator_get_default_mod_mask()
        text = self._entry.get_text()

        if evnt.keyval == Gdk.KEY_Escape:
            if not self._info is None:
                if not self._suspended is None:
                    self._suspended.resume()

                if not self._info is None:
                    self._info_revealer.set_reveal_child(False)

                self._entry.set_sensitive(True)
            elif self._accel_group:
                self._accel_group = self._accel_group.parent

                if not self._accel_group or not self._accel_group.parent:
                    self._entry.set_editable(True)
                    self._accel_group = None

                self._prompt()
            elif text:
                self._entry.set_text('')
            elif self._command_state:
                self._command_state.clear()
                self._prompt()
            else:
                self._view.grab_focus()
                self._reveal.set_reveal_child(False)

            return True

        if state or self._accel_group:
            # Check if it should be handled by the accel group
            group = self._accel_group

            if not self._accel_group:
                group = commands.Commands().accelerator_group()

            accel = group.activate(evnt.keyval, state)

            if isinstance(accel, commands.accel_group.AccelGroup):
                self._accel_group = accel
                self._entry.set_text('')
                self._entry.set_editable(False)
                self._prompt()

                return True
            elif isinstance(accel, commands.accel_group.AccelCallback):
                self._entry.set_editable(True)
                self._run_command(
                    lambda: accel.activate(self._command_state, self))
                return True

        if not self._entry.get_editable():
            return True

        for handler in self._handlers:
            if (handler[0] == None or handler[0]
                    == state) and evnt.keyval == handler[1] and handler[2](
                        handler[3], state):
                return True

        if not self._info is None and self._info.is_empty:
            self._info_revealer.set_reveal_child(False)

        self._history_prefix = None
        return False

    def _on_history_move(self, direction, modifier):
        pos = self._entry.get_position()

        self._history.update(self._entry.get_text())

        if self._history_prefix == None:
            if len(self._entry.get_text()) == pos:
                self._history_prefix = self._entry.get_chars(0, pos)
            else:
                self._history_prefix = ''

        if self._history_prefix == None:
            hist = ''
        else:
            hist = self._history_prefix

        next = self._history.move(direction, hist)

        if next != None:
            self._entry.set_text(next)
            self._entry.set_position(-1)

        return True

    def _prompt(self, pr=''):
        self._prompt_text = pr

        if self._accel_group != None:
            pr = '<i>%s</i>' % (saxutils.escape(
                self._accel_group.full_name()), )

        if not pr:
            pr = ''
        else:
            pr = ' ' + pr

        self._prompt_label.set_markup('<b>&gt;&gt;&gt;</b>%s' % pr)

    def _make_info(self):
        if self._info is None:
            self._info = Info()

            self._copy_style_from_view([self._info, self._info.text_view])

            self._info_revealer.add(self._info)
            self._info.show()

            self._info_revealer.show()
            self._info_revealer.set_reveal_child(True)

            self._info.connect('destroy', self._on_info_destroy)

    def _on_info_revealer_child_revealed(self, widget, pspec):
        if not self._info_revealer.get_child_revealed():
            self._info.destroy()
            self._info_revealer.hide()

    def _on_info_destroy(self, widget):
        self._info = None

    def info_show(self, text='', use_markup=False):
        self._make_info()
        self._info.add_lines(text, use_markup)

    def info_status(self, text):
        self._make_info()
        self._info.status(text)

    def _info_add_action(self, stock, callback, data=None):
        self._make_info()
        return self._info.add_action(stock, callback, data)

    def _command_history_done(self):
        self._history.add(self._entry.get_text())
        self._history_prefix = None
        self._entry.set_text('')

    def _on_wait_cancel(self):
        self._on_execute(None, 0)

    def _show_wait_cancel(self):
        self._cancel_button = self._info_add_action('process-stop-symbolic',
                                                    self._on_wait_cancel)
        self.info_status('<i>Waiting to finish\u2026</i>')

        self._wait_timeout = 0
        return False

    def _complete_word_match(self, match):
        for i in (3, 2, 0):
            if match.group(i) != None:
                return [match.group(i), match.start(i), match.end(i)]

    def _on_suspend_resume(self):
        if self._wait_timeout:
            GLib.source_remove(self._wait_timeout)
            self._wait_timeout = 0
        else:
            if not self._cancel_button is None:
                self._cancel_button.destroy()
                self._cancel_button = None

            self.info_status(None)

        self._entry.set_sensitive(True)
        self._command_history_done()

        if self._entry.props.has_focus or (not self._info is None
                                           and not self._info.is_empty):
            self._entry.grab_focus()

    def _run_command(self, cb):
        self._suspended = None

        try:
            ret = cb()
        except Exception as e:
            sys.stderr.write(self._format_trace() + '\n')

            self._command_history_done()
            self._command_state.clear()

            self._prompt()

            # Show error in info
            self.info_show(
                '<b><span color="#f66">Error:</span></b> ' +
                saxutils.escape(str(e)), True)

            if not isinstance(e, commands.exceptions.Execute):
                self.info_show(self._format_trace(), False)

            return None

        mod = sys.modules['commander.commands.result']

        if ret == mod.Result.SUSPEND:
            # Wait for it...
            self._suspended = ret
            ret.register(self._on_suspend_resume)

            self._wait_timeout = GLib.timeout_add(500, self._show_wait_cancel)
            self._entry.set_sensitive(False)
        else:
            self._command_history_done()
            self._prompt('')

            if ret == mod.Result.PROMPT:
                self._prompt(ret.prompt)
            elif (ret == None
                  or ret == mod.HIDE) and not self._prompt_text and (
                      self._info is None or self._info.is_empty):
                self._command_state.clear()
                self._view.grab_focus()
                self._reveal.set_reveal_child(False)
            else:
                self._entry.grab_focus()

        return ret

    def _format_trace(self):
        tp, val, tb = sys.exc_info()

        origtb = tb

        thisdir = os.path.dirname(__file__)

        # Skip frames up until after the last entry.py...
        while not tb is None:
            filename = tb.tb_frame.f_code.co_filename

            dname = os.path.dirname(filename)

            if not dname.startswith(thisdir):
                break

            tb = tb.tb_next

        msg = traceback.format_exception(tp, val, tb)
        r = ''.join(msg[0:-1])

        # This is done to prevent cyclic references, see python
        # documentation on sys.exc_info
        del origtb

        return r

    def _on_execute(self, dummy, modifier):
        if not self._info is None and not self._suspended:
            self._info_revealer.set_reveal_child(False)

        text = self._entry.get_text().strip()
        words = list(self._re_complete.finditer(text))
        wordsstr = []

        for word in words:
            spec = self._complete_word_match(word)
            wordsstr.append(spec[0])

        if not wordsstr and not self._command_state:
            self._entry.set_text('')
            return

        self._run_command(lambda: commands.Commands().execute(
            self._command_state, text, words, wordsstr, self, modifier))

        return True

    def _on_complete(self, dummy, modifier):
        # First split all the text in words
        text = self._entry.get_text()
        pos = self._entry.get_position()

        words = list(self._re_complete.finditer(text))
        wordsstr = []

        for word in words:
            spec = self._complete_word_match(word)
            wordsstr.append(spec[0])

        # Find out at which word the cursor actually is
        # Examples:
        #  * hello world|
        #  * hello| world
        #  * |hello world
        #  * hello wor|ld
        #  * hello  |  world
        #  * "hello world|"
        posidx = None

        for idx in range(0, len(words)):
            spec = self._complete_word_match(words[idx])

            if words[idx].start(0) > pos:
                # Empty space, new completion
                wordsstr.insert(idx, '')
                words.insert(idx, None)
                posidx = idx
                break
            elif spec[2] == pos:
                # At end of word, resume completion
                posidx = idx
                break
            elif spec[1] <= pos and spec[2] > pos:
                # In middle of word, do not complete
                return True

        if posidx == None:
            wordsstr.append('')
            words.append(None)
            posidx = len(wordsstr) - 1

        # First word completes a command, if not in any special 'mode'
        # otherwise, relay completion to the command, or complete by advice
        # from the 'mode' (prompt)
        cmds = commands.Commands()

        if not self._command_state and posidx == 0:
            # Complete the first command
            ret = commands.completion.command(words=wordsstr, idx=posidx)
        else:
            complete = None
            realidx = posidx

            if not self._command_state:
                # Get the command first
                cmd = commands.completion.single_command(wordsstr, 0)
                realidx -= 1

                ww = wordsstr[1:]
            else:
                cmd = self._command_state.top()
                ww = wordsstr

            if cmd:
                complete = cmd.autocomplete_func()

            if not complete:
                return True

            # 'complete' contains a dict with arg -> func to do the completion
            # of the named argument the command (or stack item) expects
            args, varargs = cmd.args()

            # Remove system arguments
            s = ['argstr', 'args', 'entry', 'view']
            args = list(filter(lambda x: not x in s, args))

            if realidx < len(args):
                arg = args[realidx]
            elif varargs:
                arg = '*'
            else:
                return True

            if not arg in complete:
                return True

            func = complete[arg]

            try:
                spec = utils.getargspec(func)

                if not ww:
                    ww = ['']

                kwargs = {'words': ww, 'idx': realidx, 'view': self._view}

                if not spec.keywords:
                    for k in list(kwargs.keys()):
                        if not k in spec.args:
                            del kwargs[k]

                ret = func(**kwargs)
            except Exception as e:
                # Can be number of arguments, or return values or simply buggy
                # modules
                print(e)
                traceback.print_exc()
                return True

        if not ret or not ret[0]:
            return True

        res = ret[0]
        completed = ret[1]

        if len(ret) > 2:
            after = ret[2]
        else:
            after = ' '

        # Replace the word
        if words[posidx] == None:
            # At end of everything, just append
            spec = None

            self._entry.insert_text(completed, self._entry.get_text_length())
            self._entry.set_position(-1)
        else:
            spec = self._complete_word_match(words[posidx])

            self._entry.delete_text(spec[1], spec[2])
            self._entry.insert_text(completed, spec[1])
            self._entry.set_position(spec[1] + len(completed))

        if len(res) == 1:
            # Full completion
            lastpos = self._entry.get_position()

            if not isinstance(res[0],
                              commands.module.Module) or not res[0].commands():
                if words[posidx] and after == ' ' and (
                        words[posidx].group(2) != None
                        or words[posidx].group(3) != None):
                    lastpos = lastpos + 1

                self._entry.insert_text(after, lastpos)
                self._entry.set_position(lastpos + 1)
            elif completed == wordsstr[posidx] or not res[0].method:
                self._entry.insert_text('.', lastpos)
                self._entry.set_position(lastpos + 1)

            if not self._info is None:
                self._info_revealer.set_reveal_child(False)
        else:
            # Show popup with completed items
            if not self._info is None:
                self._info.clear()

            ret = []

            for x in res:
                if isinstance(x, commands.method.Method):
                    ret.append('<b>' + saxutils.escape(x.name) + '</b> (<i>' +
                               x.oneline_doc() + '</i>)')
                else:
                    ret.append(str(x))

            self.info_show("\n".join(ret), True)

        return True

    def do_draw(self, ctx):
        ret = Gtk.Box.do_draw(self, ctx)

        col = self._border_color

        ctx.set_line_width(1)
        ctx.set_source_rgba(col.red, col.green, col.blue, col.alpha)

        w = self.get_allocated_width()

        ctx.move_to(0, 0.5)
        ctx.line_to(w, 0.5)
        ctx.stroke()

        if not self._info is None:
            alloc = self._info_revealer.get_allocation()
            y = alloc.y + alloc.height + 0.5

            if y >= 3:
                ctx.move_to(0, y)
                ctx.line_to(w, y)
                ctx.stroke()

        return ret

    def _on_destroy(self, widget):
        # Note we do this not as an override because somehow something
        # goes wrong when finalizing in that case, maybe self is NULL
        # or something like that, and then gets some new empty instance?
        if self._view_style_updated_id:
            self._view.disconnect(self._view_style_updated_id)

        self._history.save()

        self._view = None
        self._view_style_updated_id = None
Exemple #8
0
class Entry(Gtk.EventBox):
    __gtype_name__ = "CommanderEntry"

    def __init__(self, view):
        Gtk.EventBox.__init__(self)
        self._view = view

        self.set_visible_window(False)

        hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 3)
        hbox.show()
        hbox.set_border_width(3)

        # context for the view
        self._entry = Gtk.Entry()
        self._entry.set_has_frame(False)
        self._entry.set_name('gedit-commander-entry')
        self._entry.show()

        css = Gtk.CssProvider()
        css.load_from_data("""
@binding-set terminal-like-bindings {
    unbind "<Control>A";

    bind "<Control>W" { "delete-from-cursor" (word-ends, -1) };
    bind "<Control>A" { "move-cursor" (buffer-ends, -1, 0) };
    bind "<Control>U" { "delete-from-cursor" (display-line-ends, -1) };
    bind "<Control>K" { "delete-from-cursor" (display-line-ends, 1) };
    bind "<Control>E" { "move-cursor" (buffer-ends, 1, 0) };
    bind "Escape" { "delete-from-cursor" (display-lines, 1) };
}

GtkEntry#gedit-commander-entry {
    gtk-key-bindings: terminal-like-bindings;

    /* Override background to anything. This is weird, but doing this we can
       then in code use widget.override_background to set the color dynamically
       to the same color as the gedit view */
    background: transparent;
    border-width: 0;
    box-shadow: 0 0 transparent;
}
""")

        # FIXME: remove hardcopy of 600 (GTK_STYLE_PROVIDER_PRIORITY_APPLICATION)
        # https://bugzilla.gnome.org/show_bug.cgi?id=646860
        self._entry.get_style_context().add_provider(css, 600)

        self._prompt_label = Gtk.Label(label='<b>&gt;&gt;&gt;</b>', use_markup=True)
        self._prompt_label.show()

        self._entry.connect('focus-out-event', self.on_entry_focus_out)
        self._entry.connect('key-press-event', self.on_entry_key_press)

        self._history = History(os.path.join(GLib.get_user_config_dir(), 'gedit/commander/history'))
        self._prompt = None

        self._accel_group = None

        hbox.pack_start(self._prompt_label, False, False, 0)
        hbox.pack_start(self._entry, True, True, 0)

        self.copy_style_from_view()
        self.view_style_updated_id = self._view.connect('style-updated', self.on_view_style_updated)

        self.add(hbox)
        self.attach()
        self._entry.grab_focus()

        self._wait_timeout = 0
        self._info_window = None

        self.connect('destroy', self.on_destroy)
        self.connect_after('size-allocate', self.on_size_allocate)
        self.view_draw_id = self._view.connect_after('draw', self.on_draw)

        self._history_prefix = None
        self._suspended = None
        self._handlers = [
            [0, Gdk.KEY_Up, self.on_history_move, -1],
            [0, Gdk.KEY_Down, self.on_history_move, 1],
            [None, Gdk.KEY_Return, self.on_execute, None],
            [None, Gdk.KEY_KP_Enter, self.on_execute, None],
            [0, Gdk.KEY_Tab, self.on_complete, None],
            [0, Gdk.KEY_ISO_Left_Tab, self.on_complete, None]
        ]

        self._re_complete = re.compile('("((?:\\\\"|[^"])*)"?|\'((?:\\\\\'|[^\'])*)\'?|[^\s]+)')
        self._command_state = commands.Commands.State()

    def on_view_style_updated(self, widget):
        self.copy_style_from_view()

    def get_border_color(self):
        color = self.get_background_color().copy()
        color.red = 1 - color.red
        color.green = 1 - color.green
        color.blue = 1 - color.blue
        color.alpha = 0.5

        return color

    def get_background_color(self):
        context = self._view.get_style_context()
        return context.get_background_color(Gtk.StateFlags.NORMAL)
    
    def get_foreground_color(self):
        context = self._view.get_style_context()
        return context.get_color(Gtk.StateFlags.NORMAL)

    def get_font(self):
        context = self._view.get_style_context()
        return context.get_font(Gtk.StateFlags.NORMAL)

    def copy_style_from_view(self, widget=None):
        if widget != None:
            context = self._view.get_style_context()
            font = context.get_font(Gtk.StateFlags.NORMAL)

            widget.override_color(Gtk.StateFlags.NORMAL, self.get_foreground_color())
            widget.override_font(self.get_font())
        else:
            if self._entry:
                self.copy_style_from_view(self._entry)

            if self._prompt_label:
                self.copy_style_from_view(self._prompt_label)

    def view(self):
        return self._view

    def on_size_allocate(self, widget, alloc):
        alloc = self.get_allocation()
        self._view.set_border_window_size(Gtk.TextWindowType.BOTTOM, alloc.height)

        win = self._view.get_window(Gtk.TextWindowType.BOTTOM)
        self.set_size_request(win.get_width(), -1)

        # NOTE: we need to do this explicitly somehow, otherwise the window
        # size will not be updated unless something else happens, not exactly
        # sure what. This might be caused by the multi notebook, or custom
        # animation layouting?
        self._view.get_parent().resize_children()

    def attach(self):
        # Attach ourselves in the text view, and position just above the
        # text window
        win = self._view.get_window(Gtk.TextWindowType.TEXT)
        alloc = self.get_allocation()

        self._view.set_border_window_size(Gtk.TextWindowType.BOTTOM, max(alloc.height, 1))
        self._view.add_child_in_window(self, Gtk.TextWindowType.BOTTOM, 0, 0)

        win = self._view.get_window(Gtk.TextWindowType.BOTTOM)

        self.show()
        self.set_size_request(win.get_width(), -1)

    def on_entry_focus_out(self, widget, evnt):
        if self._entry.get_sensitive():
            self.destroy()

    def on_entry_key_press(self, widget, evnt):
        state = evnt.state & Gtk.accelerator_get_default_mod_mask()
        text = self._entry.get_text()

        if evnt.keyval == Gdk.KEY_Escape:
            if self._info_window:
                if self._suspended:
                    self._suspended.resume()

                if self._info_window:
                    self._info_window.destroy()

                self._entry.set_sensitive(True)
            elif self._accel_group:
                self._accel_group = self._accel_group.parent

                if not self._accel_group or not self._accel_group.parent:
                    self._entry.set_editable(True)
                    self._accel_group = None

                self.prompt()
            elif text:
                self._entry.set_text('')
            elif self._command_state:
                self._command_state.clear()
                self.prompt()
            else:
                self._view.grab_focus()
                self.destroy()

            return True

        if state or self._accel_group:
            # Check if it should be handled by the accel group
            group = self._accel_group

            if not self._accel_group:
                group = commands.Commands().accelerator_group()

            accel = group.activate(evnt.keyval, state)

            if isinstance(accel, commands.accel_group.AccelGroup):
                self._accel_group = accel
                self._entry.set_text('')
                self._entry.set_editable(False)
                self.prompt()

                return True
            elif isinstance(accel, commands.accel_group.AccelCallback):
                self._entry.set_editable(True)
                self.run_command(lambda: accel.activate(self._command_state, self))
                return True

        if not self._entry.get_editable():
            return True

        for handler in self._handlers:
            if (handler[0] == None or handler[0] == state) and evnt.keyval == handler[1] and handler[2](handler[3], state):
                return True

        if self._info_window and self._info_window.empty():
            self._info_window.destroy()

        self._history_prefix = None
        return False

    def on_history_move(self, direction, modifier):
        pos = self._entry.get_position()

        self._history.update(self._entry.get_text())

        if self._history_prefix == None:
            if len(self._entry.get_text()) == pos:
                self._history_prefix = self._entry.get_chars(0, pos)
            else:
                self._history_prefix = ''

        if self._history_prefix == None:
            hist = ''
        else:
            hist = self._history_prefix

        next = self._history.move(direction, hist)

        if next != None:
            self._entry.set_text(next)
            self._entry.set_position(-1)

        return True

    def prompt(self, pr=''):
        self._prompt = pr

        if self._accel_group != None:
            pr = '<i>%s</i>' % (saxutils.escape(self._accel_group.full_name()),)

        if not pr:
            pr = ''
        else:
            pr = ' ' + pr

        self._prompt_label.set_markup('<b>&gt;&gt;&gt;</b>%s' % pr)

    def make_info(self):
        if self._info_window == None:
            self._info_window = Info(self)
            self._info_window.show()

            self._info_window.connect('destroy', self.on_info_window_destroy)

    def on_info_window_destroy(self, widget):
        self._info_window = None

    def info_show(self, text='', use_markup=False):
        self.make_info()
        self._info_window.add_lines(text, use_markup)

    def info_status(self, text):
        self.make_info()
        self._info_window.status(text)

    def info_add_action(self, stock, callback, data=None):
        self.make_info()
        return self._info_window.add_action(stock, callback, data)

    def command_history_done(self):
        self._history.add(self._entry.get_text())
        self._history_prefix = None
        self._entry.set_text('')

    def on_wait_cancel(self):
        if self._suspended:
            self._suspended.resume()

        if self._cancel_button:
            self._cancel_button.destroy()

        if self._info_window and self._info_window.empty():
            self._info_window.destroy()
            self._entry.grab_focus()
            self._entry.set_sensitive(True)

    def _show_wait_cancel(self):
        self._cancel_button = self.info_add_action(Gtk.STOCK_STOP, self.on_wait_cancel)
        self.info_status('<i>Waiting to finish...</i>')

        self._wait_timeout = 0
        return False

    def _complete_word_match(self, match):
        for i in (3, 2, 0):
            if match.group(i) != None:
                return [match.group(i), match.start(i), match.end(i)]

    def on_suspend_resume(self):
        if self._wait_timeout:
            GObject.source_remove(self._wait_timeout)
            self._wait_timeout = 0
        else:
            self._cancel_button.destroy()
            self._cancel_button = None
            self.info_status(None)

        self._entry.set_sensitive(True)
        self.command_history_done()

        if self._entry.props.has_focus or (self._info_window and not self._info_window.empty()):
            self._entry.grab_focus()

        self.on_execute(None, 0)

    def ellipsize(self, s, size):
        if len(s) <= size:
            return s

        mid = (size - 4) / 2
        return s[:mid] + '...' + s[-mid:]

    def destroy(self):
        self.hide()
        Gtk.EventBox.destroy(self)

    def run_command(self, cb):
        self._suspended = None

        try:
            ret = cb()
        except Exception, e:
            self.command_history_done()
            self._command_state.clear()

            self.prompt()

            # Show error in info
            self.info_show('<b><span color="#f66">Error:</span></b> ' + saxutils.escape(str(e)), True)

            if not isinstance(e, commands.exceptions.Execute):
                self.info_show(self.format_trace(), False)

            return None

        if ret == commands.result.Result.SUSPEND:
            # Wait for it...
            self._suspended = ret
            ret.register(self.on_suspend_resume)

            self._wait_timeout = GObject.timeout_add(500, self._show_wait_cancel)
            self._entry.set_sensitive(False)
        else:
            self.command_history_done()
            self.prompt('')

            if ret == commands.result.Result.PROMPT:
                self.prompt(ret.prompt)
            elif (ret == None or ret == commands.result.HIDE) and not self._prompt and (not self._info_window or self._info_window.empty()):
                self._command_state.clear()
                self._view.grab_focus()
                self.destroy()
            else:
                self._entry.grab_focus()

        return ret