Ejemplo n.º 1
0
 def _setup_textview(self):
     self.textview = SQLView(self.win, self)
     sw = self.builder.get_object("sw_editor_textview")
     sw.add(self.textview)
     buffer_ = self.textview.buffer
     tag = buffer_.create_tag('error', underline=pango.UNDERLINE_ERROR)
     self.textview.buffer.connect('changed', self.on_buffer_changed)
Ejemplo n.º 2
0
 def setup_startup_commands(self):
     sw = self.builder.get_object('sw_startup_commands')
     editor = SQLView(self)
     editor.set_show_line_numbers(False)
     sw.add(editor)
     editor.show()
     self.widget_startup_commands = editor
Ejemplo n.º 3
0
 def _setup_textview(self):
     self.textview = SQLView(self.win, self)
     sw = self.builder.get_object("sw_editor_textview")
     sw.add(self.textview)
     buffer_ = self.textview.buffer
     tag = buffer_.create_tag('error', underline=pango.UNDERLINE_ERROR)
     self.textview.buffer.connect('changed', self.on_buffer_changed)
Ejemplo n.º 4
0
 def setup_startup_commands(self):
     sw = self.builder.get_object('sw_startup_commands')
     editor = SQLView(self)
     editor.set_show_line_numbers(False)
     sw.add(editor)
     editor.show()
     self.widget_startup_commands = editor
Ejemplo n.º 5
0
class Editor(gobject.GObject, PaneItem):
    """SQL editor widget.

    :Signals:

    connection-changed
      ``def callback(editor, connection)``

      Emitted when a connection was assigned to this editor.
    """

    name = _(u'Editor')
    icon = gtk.STOCK_EDIT
    detachable = True

    __gsignals__ = {
        "connection-changed" : (gobject.SIGNAL_RUN_LAST,
                                gobject.TYPE_NONE,
                                (gobject.TYPE_PYOBJECT,))
    }

    __gproperties__ = {
        'buffer-dirty': (gobject.TYPE_BOOLEAN,
                         'dirty flag',
                         'is the buffer dirty?',
                         False,
                         gobject.PARAM_READWRITE),
        }

    def __init__(self, win):
        """Constructor.

        :param win: A MainWindow instance.
        """
        self.__gobject_init__()
        PaneItem.__init__(self, win.app)
        self.app = win.app
        self.win = win
        self.connection = None
        self._buffer_dirty = False
        self.__conn_close_tag = None
        self._query_timer = None
        self._filename = None
        self._filecontent_read = ""
        self.builder = gtk.Builder()
        self.builder.set_translation_domain('crunchyfrog')
        self.builder.add_from_file(self.app.get_glade_file('editor.glade'))
        self.widget = self.builder.get_object('box_editor')
        self._setup_widget()
        self._setup_connections()
        self.on_messages_copy = self.results.on_messages_copy
        self.on_messages_clear = self.results.on_messages_clear
        self.on_copy_data = self.results.on_copy_data
        self.on_export_data = self.results.on_export_data
        self.builder.connect_signals(self)
        self.set_data("win", None)
        self.win.emit('editor-created', self)
        self.show_all()
        if self.app.config.get('editor.hide_results_pane'):
            gobject.idle_add(self.toggle_results_pane)

    def show_all(self):
        self.widget.show_all()

    def show(self):
        self.widget.show()

    def destroy(self):
        self.widget.destroy()

    def get_widget(self):
        return self.widget

    def do_get_property(self, param):
        if param.name == 'buffer-dirty':
            return self._buffer_dirty

    def do_set_property(self, param, value):
        if param.name == 'buffer-dirty':
            self._buffer_dirty = value

    # Widget setup

    def _setup_widget(self):
        self._setup_textview()
        self._setup_resultsgrid()

    def _setup_textview(self):
        self.textview = SQLView(self.win, self)
        sw = self.builder.get_object("sw_editor_textview")
        sw.add(self.textview)
        buffer_ = self.textview.buffer
        tag = buffer_.create_tag('error', underline=pango.UNDERLINE_ERROR)
        self.textview.buffer.connect('changed', self.on_buffer_changed)

    def _setup_resultsgrid(self):
        self.results = ResultsView(self.win, self.builder)

    def _setup_connections(self):
        self.textview.connect("populate-popup", self.on_populate_popup)

    # Callbacks

    def on_buffer_changed(self, buffer):
        self.props.buffer_dirty = self.contents_changed()
        iter1 = buffer.get_iter_at_mark(buffer.get_insert())
        iter1.set_line_offset(0)
        iter2 = iter1.copy()
        iter2.forward_to_line_end()
        buffer.remove_tag_by_name('error', iter1, iter2)

    def on_close(self, *args):
        self.close()

    def on_connection_closed(self, connection):
        if connection == self.connection and self.__conn_close_tag:
            connection.disconnect(self.__conn_close_tag)
        self.set_connection(None)

    def on_explain(self, *args):
        self.explain()

    def on_populate_popup(self, textview, popup):
        cfg = self.app.config
        sep = gtk.SeparatorMenuItem()
        sep.show()
        popup.append(sep)
        item = gtk.CheckMenuItem(_(u"Split statements"))
        item.set_active(cfg.get("sqlparse.enabled"))
        item.connect("toggled", lambda x: cfg.set("sqlparse.enabled",
                                                  x.get_active()))
        item.show()
        popup.append(item)
        item = gtk.ImageMenuItem("gtk-close")
        item.show()
        item.connect("activate", self.on_close)
        popup.append(item)

    def on_query_started(self, query):
        start = time.time()
        # Note: The higher the time out value, the longer the query takes.
        #    50 is a reasonable value anyway.
        #    The start time is for the UI only. The real execution time
        #    is calculated in the Query class.
        self.win.statusbar.pop(1)
        if self._query_timer is not None:
            gobject.source_remove(self._query_timer)
        self.results.add_separator()
        self.results.add_message(query.statement, type_='query')
        query.path_status = self.results.add_message("")
        self._query_timer = gobject.timeout_add(50, self.update_exectime,
                                                start, query)

    def on_query_finished(self, query, tag_notice):
        if self._query_timer:
            gobject.source_remove(self._query_timer)
            self._query_timer = None
        self.results.set_query(query)
        if query.failed:
            msg = _(u'Query failed (%(sec).3f seconds)')
            msg = msg % {"sec": query.execution_time}
            type_ = 'error'
            if query.error_position:
                line, offset = query.error_position
                line += query.get_data('editor_start_line')
                self._mark_error(line, offset)
        elif query.description:
            msg = (_(u"Query finished (%(sec).3f seconds, %(num)d rows)")
                   % {"sec": query.execution_time,
                      "num": query.rowcount})
            type_ = 'info'
        else:
            msg = _(u"Query finished (%(sec).3f seconds, "
                    u"%(num)d affected rows)")
            msg = msg % {"sec": query.execution_time,
                         "num": query.rowcount}
            type_ = 'info'
        self.results.add_message(msg, type_, query.path_status)
        self.win.statusbar.push(1, msg)
        if self.connection.handler_is_connected(tag_notice):
            self.connection.disconnect(tag_notice)
        self.textview.grab_focus()

    def on_show_in_main_window(self, *args):
        gobject.idle_add(self.show_in_main_window)

    def on_show_in_separate_window(self, *args):
        gobject.idle_add(self.show_in_separate_window)

    # Public methods

    def _mark_error(self, line, offset):
        line = line - 2
        offset = offset - 1
        if line < 0 or offset < 0:
            # Backend reported a bad error position. Simply ignore it.
            return
        buffer_ = self.textview.buffer
#        buffer_.create_source_mark(
#            None, 'error',
#            buffer_.get_iter_at_line_offset(line-2, offset))
        tag_table = buffer_.get_tag_table()
        tag = tag_table.lookup('error')
        iter_err = buffer_.get_iter_at_line_offset(line, offset)
        iter1 = iter_err.copy()
        if not iter1.starts_word():
            iter1.backward_word_start()
        iter2 = iter1.copy()
        iter2.forward_word_end()
#        tag = gtk.TextTag()
#        tag.props.underline = pango.UNDERLINE_SINGLE
        buffer_.apply_tag(tag, iter1, iter2)
        buffer_.place_cursor(iter_err)
        self.textview.move_mark_onscreen(buffer_.get_insert())
#        self.textview.set_mark_category_background('error',
#                                                   gtk.gdk.color_parse('red'))
#        it = gtk.icon_theme_get_default()
#        pb = it.load_icon ("gtk-dialog-error", gtk.ICON_SIZE_MENU,
#                           gtk.ICON_LOOKUP_USE_BUILTIN)
#        self.textview.set_mark_category_pixbuf('error', pb)
#        self.textview.set_show_line_marks(True)

    def get_focus_child(self):
        return self.get_child1().get_children()[0].grab_focus()

    def close(self, force=False):
        """Close editor, displays a confirmation dialog for unsaved files.

        Args:
          force: If True, the method doesn't check for changed contents.
        """
        if self.contents_changed() and not force:
            dlg = ConfirmSaveDialog(self.win, [self])
            resp = dlg.run()
            if resp == 1:
                ret = dlg.save_files()
            elif resp == 2:
                ret = True
            else:
                ret = False
            dlg.destroy()
        else:
            ret = True
        if ret:
            if self.get_data("win"):
                self.get_data("win").destroy()
            else:
                self.destroy()
            self.win.set_editor_active(self, False)
            self.win.editor_remove(self)
            return True

    def commit(self):
        """Commit current transaction, if any."""
        if not self.connection: return
        self.connection.commit()
        self.results.add_message('COMMIT', 'info')

    def rollback(self):
        """Commit current transaction, if any."""
        if not self.connection: return
        self.connection.rollback()
        self.results.add_message('ROLLBACK', 'info')

    def begin_transaction(self):
        """Begin transaction."""
        if not self.connection: return
        self.connection.begin()
        self.results.add_message('BEGIN TRANSACTION', 'info')

    def execute_query(self, statement_at_cursor=False):
        # TODO(andi): This method needs some refactoring:
        #   - the actual execution code is doubled
        #   - that affects offset counting too
        self.results.assure_visible()
        def exec_threaded(statement, start_line):
            if self.app.config.get("sqlparse.enabled", True):
                stmts = sqlparse.split(statement)
            else:
                stmts = [statement]
            for stmt in stmts:
                add_offset = len(stmt.splitlines())
                if not stmt.strip():
                    start_line += add_offset
                    continue
                query = Query(stmt, self.connection)
#                query.coding_hint = self.connection.coding_hint
                gtk.gdk.threads_enter()
                query.set_data('editor_start_line', start_line)
                query.connect("started", self.on_query_started)
                query.connect("finished",
                              self.on_query_finished,
                              tag_notice)
                gtk.gdk.threads_leave()
                query.execute(True)
                start_line += add_offset
                if query.failed:
                    # hmpf, doesn't work that way... so just return here...
                    return
#                    gtk.gdk.threads_enter()
#                    dlg = gtk.MessageDialog(None,
#                                            gtk.DIALOG_MODAL|
#                                            gtk.DIALOG_DESTROY_WITH_PARENT,
#                                            gtk.MESSAGE_ERROR,
#                                            gtk.BUTTONS_YES_NO,
#                                            _(u"An error occurred. Continue?"))
#                    if dlg.run() == gtk.RESPONSE_NO:
#                        leave = True
#                    else:
#                        leave = False
#                    dlg.destroy()
#                    gtk.gdk.threads_leave()
#                    if leave:
#                        return
        buffer = self.textview.get_buffer()
        self.results.reset()
        if not statement_at_cursor:
            bounds = buffer.get_selection_bounds()
            if not bounds:
                bounds = buffer.get_bounds()
        else:
            bounds = self.textview.get_current_statement()
            if bounds is None:
                return
        buffer.remove_tag_by_name('error', *bounds)
        statement = buffer.get_text(*bounds)
        if self.app.config.get("editor.replace_variables"):
            tpl = string.Template(statement)
            tpl_search = tpl.pattern.search(tpl.template)
            if tpl_search and tpl_search.groupdict().get("named"):
                dlg = StatementVariablesDialog(tpl)
                if dlg.run() == gtk.RESPONSE_OK:
                    statement = dlg.get_statement()
                else:
                    statement = None
                dlg.destroy()
                if not statement:
                    return
        def foo(connection, msg):
            self.results.add_message(msg)
        tag_notice = self.connection.connect("notice", foo)
        if self.connection.threadsafety >= 2:
            start_line = bounds[0].get_line()+1
            thread.start_new_thread(exec_threaded, (statement, start_line))
        else:
            start_line = bounds[0].get_line()+1
            line_offset = start_line
            if self.app.config.get("sqlparse.enabled", True):
                stmts = sqlparse.split(statement)
            else:
                stmts = [statement]
            for stmt in stmts:
                add_offset = len(stmt.splitlines())
                if not stmt.strip():
                    line_offset += add_offset
                    continue
                query = Query(stmt, self.connection)
                query.set_data('editor_start_line', line_offset)
#                query.coding_hint = self.connection.coding_hint
                query.connect("started", self.on_query_started)
                query.connect("finished", self.on_query_finished, tag_notice)
                query.execute()
                line_offset += add_offset

    def explain(self):
        self.results.assure_visible()
        buf = self.textview.get_buffer()
        bounds = buf.get_selection_bounds()
        if not bounds:
            bounds = buf.get_bounds()
        statement = buf.get_text(*bounds)
        if len(sqlparse.split(statement)) > 1:
            dialogs.error(_(u"Select a single statement to explain."))
            return
        if not self.connection:
            return
        queries = [Query(stmt, self.connection)
                   for stmt in self.connection.explain_statements(statement)]
        def _execute_next(last, queries):
            if last is not None and last.failed:
                self.results.set_explain_results(last)
                return
            q = queries.pop(0)
            if len(queries) == 0:
                q.connect('finished',
                          lambda x: self.results.set_explain_results(x))
            else:
                q.connect('finished',
                          lambda x: _execute_next(x, queries))
            q.execute()
        _execute_next(None, queries)

    def set_connection(self, conn):
        if self.connection and self.__conn_close_tag:
            if self.connection.handler_is_connected(self.__conn_close_tag):
                self.connection.disconnect(self.__conn_close_tag)
            self.__conn_close_tag = None
        self.connection = conn
        if conn:
            self.__conn_close_tag = self.connection.connect("closed",
                                                            self.on_connection_closed)
        else:
            self._conn_close_tag = None
        self.emit("connection-changed", conn)

    def get_connection(self):
        """Returns the connection assigned to the editor."""
        return self.connection

    def set_filename(self, filename):
        """Opens filename.

        Returns ``True`` if the file was successfully opened.
        Otherwise ``False``.
        """
        msg = None
        if not os.path.isfile(filename):
            msg = _(u'No such file: %(name)s')
        elif not os.access(filename, os.R_OK):
            msg = _(u'File is not readable: %(name)s')
        if msg is not None:
            dialogs.error(_(u"Failed to open file"), msg % {'name': filename})
            return False
        self._filename = filename
        if filename:
            f = open(self._filename)
            a = f.read()
            f.close()
        else:
            a = ""
        self._filecontent_read = a
        self.set_text(a)
        self.app.recent_manager.add_item(to_uri(filename))
        return True

    def get_filename(self):
        return self._filename

    def file_contents_changed(self):
        if self._filename:
            buffer = self.textview.get_buffer()
            return buffer.get_text(*buffer.get_bounds()) != self._filecontent_read
        return False

    def contents_changed(self):
        if self._filename:
            return self.file_contents_changed()
        elif len(self.get_text()) == 0:
            return False
        return True

    def file_confirm_save(self):
        dlg = dialogs.yesno(_(u"Save file %(name)s before closing the editor?") % {"name":os.path.basename(self._filename)})
        if dlg == gtk.RESPONSE_YES:
            return self.save_file_as()
        return True

    def save_file(self, parent=None, default_name=None):
        if not self._filename:
            return self.save_file_as(parent=parent, default_name=default_name)
        buffer = self.get_buffer()
        a = buffer.get_text(*buffer.get_bounds())
        f = open(self._filename, "w")
        f.write(a)
        f.close()
        self.app.recent_manager.add_item(to_uri(self._filename))
        self._filecontent_read = a
        gobject.idle_add(buffer.emit, "changed")
        return True

    def save_file_as(self, parent=None, default_name=None):
        if not parent:
            parent = self.win
        dlg = gtk.FileChooserDialog(_(u"Save file"),
                            parent,
                            gtk.FILE_CHOOSER_ACTION_SAVE,
                            (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
                             gtk.STOCK_SAVE, gtk.RESPONSE_OK))
        if self._filename:
            dlg.set_filename(self._filename)
        else:
            dlg.set_current_folder(self.app.config.get("editor.recent_folder", ""))
            if default_name:
                dlg.set_current_name(default_name)
        filter = gtk.FileFilter()
        filter.set_name(_(u"All files (*)"))
        filter.add_pattern("*")
        dlg.add_filter(filter)
        filter = gtk.FileFilter()
        filter.set_name(_(u"SQL files (*.sql)"))
        filter.add_pattern("*.sql")
        dlg.add_filter(filter)
        dlg.set_filter(filter)
        if dlg.run() == gtk.RESPONSE_OK:
            self._filename = dlg.get_filename()
            self.save_file()
            self.app.config.set("editor.recent_folder", dlg.get_current_folder())
            ret = True
        else:
            ret = False
        dlg.destroy()
        return ret

    def get_buffer(self):
        return self.textview.get_buffer()

    def get_text(self):
        buffer = self.get_buffer()
        return buffer.get_text(*buffer.get_bounds())

    def set_text(self, txt):
        buffer_ = self.get_buffer()
        buffer_.begin_user_action()
        buffer_.set_text(txt)
        buffer_.end_user_action()  # starts tagging of statements too

    def show_in_separate_window(self):
        instance = self.app.new_instance(show=False)
        instance.editor_append(self)
        instance.show()
    detach = show_in_separate_window  # make it compatible with PaneItem

    def show_in_main_window(self):
        self.win.queries.attach(self)
        win = self.get_data("win")
        if win:
            win.destroy()
        self.set_data("win", None)

    def update_exectime(self, start, query):
        lbl = _("Query running... (%.3f seconds)" % (time.time()-start))
        self.results.add_message(lbl, path=query.path_status)
        if query.executed:
            if self._query_timer is not None:
                gobject.source_remove(self._query_timer)
            self._query_timer = None
            return False
        else:
            return True

    # Printing

    def on_print_paginate(self, operation, context, compositor):
        if compositor.paginate(context):
            n_pages = compositor.get_n_pages()
            operation.set_n_pages(n_pages)
            return True
        return False

    def on_print_draw_page(self, operation, context, page_no, compositor):
        compositor.draw_page(context, page_no)

    def on_end_page(self, operation, context, compositor):
        pass

    def print_contents(self, preview=False):
        """Send content of editor to printer."""
        view = self.textview
        compositor = gtksourceview2.print_compositor_new_from_view(view)
        operation = gtk.PrintOperation()
        operation.connect('paginate', self.on_print_paginate, compositor)
        operation.connect('draw-page', self.on_print_draw_page, compositor)
        if preview:
            action = gtk.PRINT_OPERATION_ACTION_PREVIEW
        else:
            action = gtk.PRINT_OPERATION_ACTION_PRINT_DIALOG
        operation.run(action)

    # Clipboard functions

    def clipboard_copy(self, clipboard):
        """Copies selected data to clipboard.

        This is either the selected text from the editor or the selected
        cells from the results grid.
        """
        if self.textview.is_focus():
            buffer_ = self.textview.get_buffer()
            buffer_.copy_clipboard(clipboard)
        elif self.results.grid.grid.is_focus():
            self.results.clipboard_copy(clipboard)

    def clipboard_cut(self, clipboard):
        """Cuts selected text from editor."""
        buffer_ = self.textview.get_buffer()
        buffer_.cut_clipboard(clipboard, True)

    def clipboard_paste(self, clipboard):
        """Pastes clipboard data to editor."""
        buffer_ = self.textview.get_buffer()
        buffer_.paste_clipboard(clipboard, None, True)

    # Statement navigation and formatting

    def rjump_to_statement(self, offset):
        """Jumps to a statement relative to current.

        :param offset: Position relative to current statement as integer.
        """
        idx_current = 0
        positions = []
        curr = self.textview.get_current_statement()
        buffer_ = self.textview.get_buffer()
        if curr is not None:
            # We're using the presence of an end iter as a flag if we're
            # inside a statement. If not, just jump to the next/prev statement
            # without respecting the offset.
            cstart, inside_statement = curr
        else:
            cstart = buffer_.get_iter_at_mark(buffer_.get_insert())
            cstart.set_line_offset(0)
            inside_statement = None
        for start, end in self.textview.get_statements():
            positions.append((start, end))
            if not inside_statement:
                if offset > 0 and start.get_line() > cstart.get_line():
                    buffer_.place_cursor(start)
                    return
                elif offset < 0 and start.get_line() > cstart.get_line():
                    if len(positions) > 2:
                        buffer_.place_cursor(positions[-2][0])
                    elif positions:  # there's just one statement buffered
                        buffer_.place_cursor(positions[0][0])
                    return
            elif start.equal(cstart):
                idx_current = len(positions)-1
        max_idx = len(positions)-1
        if offset > 0:
            new_pos = min(idx_current+offset, max_idx)
        else:
            new_pos = max(idx_current+offset, 0)
        buffer_.place_cursor(positions[new_pos][0])

    def selected_lines_toggle_comment(self):
        """Comments/uncomments selected lines."""
        buffer_ = self.textview.get_buffer()
        res = buffer_.get_selection_bounds()
        if not res:
            start = buffer_.get_iter_at_mark(buffer_.get_insert())
            lno_start = lno_end = start.get_line()
        else:
            start, end = res
            lno_start = start.get_line()
            lno_end = end.get_line()
        buffer_.begin_user_action()
        for line_no in xrange(lno_start, lno_end+1):
            lstart = buffer_.get_iter_at_line(line_no)
            lend = lstart.copy()
            lend.forward_to_line_end()
            line = buffer_.get_text(lstart, lend)
            if not line.startswith('-- '):
                line = '-- %s' % line
            else:
                line = re.sub(r'^\s*-- ', '', line)
            buffer_.delete(lstart, lend)
            buffer_.insert(lstart, line)
        buffer_.end_user_action()

    def selected_lines_quick_format(self, **options):
        """Runs format without any other options than the default ones."""
        if not options:
            options = FORMATTER_DEFAULT_OPTIONS
        buffer_ = self.textview.get_buffer()
        res = buffer_.get_selection_bounds()
        if not res:
            start = end = None
            if self.app.config.get('editor.format_statement_at_cursor', False):
                curr = self.textview.get_current_statement()
                if curr:
                    start, end = curr
                    select_range = False
            if start is None:
                start, end = buffer_.get_bounds()
                select_range = False
        else:
            start, end = res
            select_range = True
        # Count chars excluding whitespaces
        insert_mark = buffer_.get_insert()
        insert_iter = buffer_.get_iter_at_mark(insert_mark)
        if insert_iter.in_range(start, end):
            char_offset = len(re.sub(r'\s', '',
                                     buffer_.get_text(start, insert_iter)))
            start_offset = start.get_offset()
        else:
            char_offset = start_offset = None
        orig = buffer_.get_text(start, end)
        formatted = sqlparse.format(orig, **options)
        # Modify buffer
        buffer_.begin_user_action()
        buffer_.delete(start, end)
        buffer_.insert(start, formatted)
        # Set selection again
        if select_range:
            end = start.copy()
            end.backward_chars(len(formatted))
            buffer_.select_range(end, start)
        buffer_.end_user_action()
        # TODO: place cursor - but how to do this... :)
        if char_offset is not None:
            num = 0
            idx = 0
            iter_ = buffer_.get_iter_at_offset(start_offset)
            for i in range(len(formatted)):
                if formatted[i] not in '\r\n\t ':
                    num += 1
                if num == char_offset:
                    iter_.forward_chars(i+1)
                    buffer_.place_cursor(iter_)
                    self.textview.scroll_to_mark(buffer_.get_insert(), 0.25)
                    break

    def toggle_results_pane(self):
        """Show or hide the result pane."""
        pane = self.widget.get_child2()
        if pane.get_property('visible'):
            pane.hide()
        else:
            pane.show()
Ejemplo n.º 6
0
class Editor(gobject.GObject, PaneItem):
    """SQL editor widget.

    :Signals:

    connection-changed
      ``def callback(editor, connection)``

      Emitted when a connection was assigned to this editor.
    """

    name = _(u'Editor')
    icon = gtk.STOCK_EDIT
    detachable = True

    __gsignals__ = {
        "connection-changed":
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, ))
    }

    __gproperties__ = {
        'buffer-dirty':
        (gobject.TYPE_BOOLEAN, 'dirty flag', 'is the buffer dirty?', False,
         gobject.PARAM_READWRITE),
    }

    def __init__(self, win):
        """Constructor.

        :param win: A MainWindow instance.
        """
        self.__gobject_init__()
        PaneItem.__init__(self, win.app)
        self.app = win.app
        self.win = win
        self.connection = None
        self._buffer_dirty = False
        self.__conn_close_tag = None
        self._query_timer = None
        self._filename = None
        self._filecontent_read = ""
        self.builder = gtk.Builder()
        self.builder.set_translation_domain('crunchyfrog')
        self.builder.add_from_file(self.app.get_glade_file('editor.glade'))
        self.widget = self.builder.get_object('box_editor')
        self._setup_widget()
        self._setup_connections()
        self.on_messages_copy = self.results.on_messages_copy
        self.on_messages_clear = self.results.on_messages_clear
        self.on_copy_data = self.results.on_copy_data
        self.on_export_data = self.results.on_export_data
        self.builder.connect_signals(self)
        self.set_data("win", None)
        self.win.emit('editor-created', self)
        self.show_all()
        if self.app.config.get('editor.hide_results_pane'):
            gobject.idle_add(self.toggle_results_pane)

    def show_all(self):
        self.widget.show_all()

    def show(self):
        self.widget.show()

    def destroy(self):
        self.widget.destroy()

    def get_widget(self):
        return self.widget

    def do_get_property(self, param):
        if param.name == 'buffer-dirty':
            return self._buffer_dirty

    def do_set_property(self, param, value):
        if param.name == 'buffer-dirty':
            self._buffer_dirty = value

    # Widget setup

    def _setup_widget(self):
        self._setup_textview()
        self._setup_resultsgrid()

    def _setup_textview(self):
        self.textview = SQLView(self.win, self)
        sw = self.builder.get_object("sw_editor_textview")
        sw.add(self.textview)
        buffer_ = self.textview.buffer
        tag = buffer_.create_tag('error', underline=pango.UNDERLINE_ERROR)
        self.textview.buffer.connect('changed', self.on_buffer_changed)

    def _setup_resultsgrid(self):
        self.results = ResultsView(self.win, self.builder)

    def _setup_connections(self):
        self.textview.connect("populate-popup", self.on_populate_popup)

    # Callbacks

    def on_buffer_changed(self, buffer):
        self.props.buffer_dirty = self.contents_changed()
        iter1 = buffer.get_iter_at_mark(buffer.get_insert())
        iter1.set_line_offset(0)
        iter2 = iter1.copy()
        iter2.forward_to_line_end()
        buffer.remove_tag_by_name('error', iter1, iter2)

    def on_close(self, *args):
        self.close()

    def on_connection_closed(self, connection):
        if connection == self.connection and self.__conn_close_tag:
            connection.disconnect(self.__conn_close_tag)
        self.set_connection(None)

    def on_explain(self, *args):
        self.explain()

    def on_populate_popup(self, textview, popup):
        cfg = self.app.config
        sep = gtk.SeparatorMenuItem()
        sep.show()
        popup.append(sep)
        item = gtk.CheckMenuItem(_(u"Split statements"))
        item.set_active(cfg.get("sqlparse.enabled"))
        item.connect("toggled",
                     lambda x: cfg.set("sqlparse.enabled", x.get_active()))
        item.show()
        popup.append(item)
        item = gtk.ImageMenuItem("gtk-close")
        item.show()
        item.connect("activate", self.on_close)
        popup.append(item)

    def on_query_started(self, query):
        start = time.time()
        # Note: The higher the time out value, the longer the query takes.
        #    50 is a reasonable value anyway.
        #    The start time is for the UI only. The real execution time
        #    is calculated in the Query class.
        self.win.statusbar.pop(1)
        if self._query_timer is not None:
            gobject.source_remove(self._query_timer)
        self.results.add_separator()
        self.results.add_message(query.statement, type_='query')
        query.path_status = self.results.add_message("")
        self._query_timer = gobject.timeout_add(50, self.update_exectime,
                                                start, query)

    def on_query_finished(self, query, tag_notice):
        if self._query_timer:
            gobject.source_remove(self._query_timer)
            self._query_timer = None
        self.results.set_query(query)
        if query.failed:
            msg = _(u'Query failed (%(sec).3f seconds)')
            msg = msg % {"sec": query.execution_time}
            type_ = 'error'
            if query.error_position:
                line, offset = query.error_position
                line += query.get_data('editor_start_line')
                self._mark_error(line, offset)
        elif query.description:
            msg = (_(u"Query finished (%(sec).3f seconds, %(num)d rows)") % {
                "sec": query.execution_time,
                "num": query.rowcount
            })
            type_ = 'info'
        else:
            msg = _(u"Query finished (%(sec).3f seconds, "
                    u"%(num)d affected rows)")
            msg = msg % {"sec": query.execution_time, "num": query.rowcount}
            type_ = 'info'
        self.results.add_message(msg, type_, query.path_status)
        self.win.statusbar.push(1, msg)
        if self.connection.handler_is_connected(tag_notice):
            self.connection.disconnect(tag_notice)
        self.textview.grab_focus()

    def on_show_in_main_window(self, *args):
        gobject.idle_add(self.show_in_main_window)

    def on_show_in_separate_window(self, *args):
        gobject.idle_add(self.show_in_separate_window)

    # Public methods

    def _mark_error(self, line, offset):
        line = line - 2
        offset = offset - 1
        if line < 0 or offset < 0:
            # Backend reported a bad error position. Simply ignore it.
            return
        buffer_ = self.textview.buffer
        #        buffer_.create_source_mark(
        #            None, 'error',
        #            buffer_.get_iter_at_line_offset(line-2, offset))
        tag_table = buffer_.get_tag_table()
        tag = tag_table.lookup('error')
        iter_err = buffer_.get_iter_at_line_offset(line, offset)
        iter1 = iter_err.copy()
        if not iter1.starts_word():
            iter1.backward_word_start()
        iter2 = iter1.copy()
        iter2.forward_word_end()
        #        tag = gtk.TextTag()
        #        tag.props.underline = pango.UNDERLINE_SINGLE
        buffer_.apply_tag(tag, iter1, iter2)
        buffer_.place_cursor(iter_err)
        self.textview.move_mark_onscreen(buffer_.get_insert())
#        self.textview.set_mark_category_background('error',
#                                                   gtk.gdk.color_parse('red'))
#        it = gtk.icon_theme_get_default()
#        pb = it.load_icon ("gtk-dialog-error", gtk.ICON_SIZE_MENU,
#                           gtk.ICON_LOOKUP_USE_BUILTIN)
#        self.textview.set_mark_category_pixbuf('error', pb)
#        self.textview.set_show_line_marks(True)

    def get_focus_child(self):
        return self.get_child1().get_children()[0].grab_focus()

    def close(self, force=False):
        """Close editor, displays a confirmation dialog for unsaved files.

        Args:
          force: If True, the method doesn't check for changed contents.
        """
        if self.contents_changed() and not force:
            dlg = ConfirmSaveDialog(self.win, [self])
            resp = dlg.run()
            if resp == 1:
                ret = dlg.save_files()
            elif resp == 2:
                ret = True
            else:
                ret = False
            dlg.destroy()
        else:
            ret = True
        if ret:
            if self.get_data("win"):
                self.get_data("win").destroy()
            else:
                self.destroy()
            self.win.set_editor_active(self, False)
            self.win.editor_remove(self)
            return True

    def commit(self):
        """Commit current transaction, if any."""
        if not self.connection: return
        self.connection.commit()
        self.results.add_message('COMMIT', 'info')

    def rollback(self):
        """Commit current transaction, if any."""
        if not self.connection: return
        self.connection.rollback()
        self.results.add_message('ROLLBACK', 'info')

    def begin_transaction(self):
        """Begin transaction."""
        if not self.connection: return
        self.connection.begin()
        self.results.add_message('BEGIN TRANSACTION', 'info')

    def execute_query(self, statement_at_cursor=False):
        # TODO(andi): This method needs some refactoring:
        #   - the actual execution code is doubled
        #   - that affects offset counting too
        self.results.assure_visible()

        def exec_threaded(statement, start_line):
            if self.app.config.get("sqlparse.enabled", True):
                stmts = sqlparse.split(statement)
            else:
                stmts = [statement]
            for stmt in stmts:
                add_offset = len(stmt.splitlines())
                if not stmt.strip():
                    start_line += add_offset
                    continue
                query = Query(stmt, self.connection)
                #                query.coding_hint = self.connection.coding_hint
                gtk.gdk.threads_enter()
                query.set_data('editor_start_line', start_line)
                query.connect("started", self.on_query_started)
                query.connect("finished", self.on_query_finished, tag_notice)
                gtk.gdk.threads_leave()
                query.execute(True)
                start_line += add_offset
                if query.failed:
                    # hmpf, doesn't work that way... so just return here...
                    return


#                    gtk.gdk.threads_enter()
#                    dlg = gtk.MessageDialog(None,
#                                            gtk.DIALOG_MODAL|
#                                            gtk.DIALOG_DESTROY_WITH_PARENT,
#                                            gtk.MESSAGE_ERROR,
#                                            gtk.BUTTONS_YES_NO,
#                                            _(u"An error occurred. Continue?"))
#                    if dlg.run() == gtk.RESPONSE_NO:
#                        leave = True
#                    else:
#                        leave = False
#                    dlg.destroy()
#                    gtk.gdk.threads_leave()
#                    if leave:
#                        return

        buffer = self.textview.get_buffer()
        self.results.reset()
        if not statement_at_cursor:
            bounds = buffer.get_selection_bounds()
            if not bounds:
                bounds = buffer.get_bounds()
        else:
            bounds = self.textview.get_current_statement()
            if bounds is None:
                return
        buffer.remove_tag_by_name('error', *bounds)
        statement = buffer.get_text(*bounds)
        if self.app.config.get("editor.replace_variables"):
            tpl = string.Template(statement)
            tpl_search = tpl.pattern.search(tpl.template)
            if tpl_search and tpl_search.groupdict().get("named"):
                dlg = StatementVariablesDialog(tpl)
                if dlg.run() == gtk.RESPONSE_OK:
                    statement = dlg.get_statement()
                else:
                    statement = None
                dlg.destroy()
                if not statement:
                    return

        def foo(connection, msg):
            self.results.add_message(msg)

        tag_notice = self.connection.connect("notice", foo)
        if self.connection.threadsafety >= 2:
            start_line = bounds[0].get_line() + 1
            thread.start_new_thread(exec_threaded, (statement, start_line))
        else:
            start_line = bounds[0].get_line() + 1
            line_offset = start_line
            if self.app.config.get("sqlparse.enabled", True):
                stmts = sqlparse.split(statement)
            else:
                stmts = [statement]
            for stmt in stmts:
                add_offset = len(stmt.splitlines())
                if not stmt.strip():
                    line_offset += add_offset
                    continue
                query = Query(stmt, self.connection)
                query.set_data('editor_start_line', line_offset)
                #                query.coding_hint = self.connection.coding_hint
                query.connect("started", self.on_query_started)
                query.connect("finished", self.on_query_finished, tag_notice)
                query.execute()
                line_offset += add_offset

    def explain(self):
        self.results.assure_visible()
        buf = self.textview.get_buffer()
        bounds = buf.get_selection_bounds()
        if not bounds:
            bounds = buf.get_bounds()
        statement = buf.get_text(*bounds)
        if len(sqlparse.split(statement)) > 1:
            dialogs.error(_(u"Select a single statement to explain."))
            return
        if not self.connection:
            return
        queries = [
            Query(stmt, self.connection)
            for stmt in self.connection.explain_statements(statement)
        ]

        def _execute_next(last, queries):
            if last is not None and last.failed:
                self.results.set_explain_results(last)
                return
            q = queries.pop(0)
            if len(queries) == 0:
                q.connect('finished',
                          lambda x: self.results.set_explain_results(x))
            else:
                q.connect('finished', lambda x: _execute_next(x, queries))
            q.execute()

        _execute_next(None, queries)

    def set_connection(self, conn):
        if self.connection and self.__conn_close_tag:
            if self.connection.handler_is_connected(self.__conn_close_tag):
                self.connection.disconnect(self.__conn_close_tag)
            self.__conn_close_tag = None
        self.connection = conn
        if conn:
            self.__conn_close_tag = self.connection.connect(
                "closed", self.on_connection_closed)
        else:
            self._conn_close_tag = None
        self.emit("connection-changed", conn)

    def get_connection(self):
        """Returns the connection assigned to the editor."""
        return self.connection

    def set_filename(self, filename):
        """Opens filename.

        Returns ``True`` if the file was successfully opened.
        Otherwise ``False``.
        """
        msg = None
        if not os.path.isfile(filename):
            msg = _(u'No such file: %(name)s')
        elif not os.access(filename, os.R_OK):
            msg = _(u'File is not readable: %(name)s')
        if msg is not None:
            dialogs.error(_(u"Failed to open file"), msg % {'name': filename})
            return False
        self._filename = filename
        if filename:
            f = open(self._filename)
            a = f.read()
            f.close()
        else:
            a = ""
        self._filecontent_read = a
        self.set_text(a)
        self.app.recent_manager.add_item(to_uri(filename))
        return True

    def get_filename(self):
        return self._filename

    def file_contents_changed(self):
        if self._filename:
            buffer = self.textview.get_buffer()
            return buffer.get_text(
                *buffer.get_bounds()) != self._filecontent_read
        return False

    def contents_changed(self):
        if self._filename:
            return self.file_contents_changed()
        elif len(self.get_text()) == 0:
            return False
        return True

    def file_confirm_save(self):
        dlg = dialogs.yesno(
            _(u"Save file %(name)s before closing the editor?") %
            {"name": os.path.basename(self._filename)})
        if dlg == gtk.RESPONSE_YES:
            return self.save_file_as()
        return True

    def save_file(self, parent=None, default_name=None):
        if not self._filename:
            return self.save_file_as(parent=parent, default_name=default_name)
        buffer = self.get_buffer()
        a = buffer.get_text(*buffer.get_bounds())
        f = open(self._filename, "w")
        f.write(a)
        f.close()
        self.app.recent_manager.add_item(to_uri(self._filename))
        self._filecontent_read = a
        gobject.idle_add(buffer.emit, "changed")
        return True

    def save_file_as(self, parent=None, default_name=None):
        if not parent:
            parent = self.win
        dlg = gtk.FileChooserDialog(_(u"Save file"), parent,
                                    gtk.FILE_CHOOSER_ACTION_SAVE,
                                    (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
                                     gtk.STOCK_SAVE, gtk.RESPONSE_OK))
        if self._filename:
            dlg.set_filename(self._filename)
        else:
            dlg.set_current_folder(
                self.app.config.get("editor.recent_folder", ""))
            if default_name:
                dlg.set_current_name(default_name)
        filter = gtk.FileFilter()
        filter.set_name(_(u"All files (*)"))
        filter.add_pattern("*")
        dlg.add_filter(filter)
        filter = gtk.FileFilter()
        filter.set_name(_(u"SQL files (*.sql)"))
        filter.add_pattern("*.sql")
        dlg.add_filter(filter)
        dlg.set_filter(filter)
        if dlg.run() == gtk.RESPONSE_OK:
            self._filename = dlg.get_filename()
            self.save_file()
            self.app.config.set("editor.recent_folder",
                                dlg.get_current_folder())
            ret = True
        else:
            ret = False
        dlg.destroy()
        return ret

    def get_buffer(self):
        return self.textview.get_buffer()

    def get_text(self):
        buffer = self.get_buffer()
        return buffer.get_text(*buffer.get_bounds())

    def set_text(self, txt):
        buffer_ = self.get_buffer()
        buffer_.begin_user_action()
        buffer_.set_text(txt)
        buffer_.end_user_action()  # starts tagging of statements too

    def show_in_separate_window(self):
        instance = self.app.new_instance(show=False)
        instance.editor_append(self)
        instance.show()

    detach = show_in_separate_window  # make it compatible with PaneItem

    def show_in_main_window(self):
        self.win.queries.attach(self)
        win = self.get_data("win")
        if win:
            win.destroy()
        self.set_data("win", None)

    def update_exectime(self, start, query):
        lbl = _("Query running... (%.3f seconds)" % (time.time() - start))
        self.results.add_message(lbl, path=query.path_status)
        if query.executed:
            if self._query_timer is not None:
                gobject.source_remove(self._query_timer)
            self._query_timer = None
            return False
        else:
            return True

    # Printing

    def on_print_paginate(self, operation, context, compositor):
        if compositor.paginate(context):
            n_pages = compositor.get_n_pages()
            operation.set_n_pages(n_pages)
            return True
        return False

    def on_print_draw_page(self, operation, context, page_no, compositor):
        compositor.draw_page(context, page_no)

    def on_end_page(self, operation, context, compositor):
        pass

    def print_contents(self, preview=False):
        """Send content of editor to printer."""
        view = self.textview
        compositor = gtksourceview2.print_compositor_new_from_view(view)
        operation = gtk.PrintOperation()
        operation.connect('paginate', self.on_print_paginate, compositor)
        operation.connect('draw-page', self.on_print_draw_page, compositor)
        if preview:
            action = gtk.PRINT_OPERATION_ACTION_PREVIEW
        else:
            action = gtk.PRINT_OPERATION_ACTION_PRINT_DIALOG
        operation.run(action)

    # Clipboard functions

    def clipboard_copy(self, clipboard):
        """Copies selected data to clipboard.

        This is either the selected text from the editor or the selected
        cells from the results grid.
        """
        if self.textview.is_focus():
            buffer_ = self.textview.get_buffer()
            buffer_.copy_clipboard(clipboard)
        elif self.results.grid.grid.is_focus():
            self.results.clipboard_copy(clipboard)

    def clipboard_cut(self, clipboard):
        """Cuts selected text from editor."""
        buffer_ = self.textview.get_buffer()
        buffer_.cut_clipboard(clipboard, True)

    def clipboard_paste(self, clipboard):
        """Pastes clipboard data to editor."""
        buffer_ = self.textview.get_buffer()
        buffer_.paste_clipboard(clipboard, None, True)

    # Statement navigation and formatting

    def rjump_to_statement(self, offset):
        """Jumps to a statement relative to current.

        :param offset: Position relative to current statement as integer.
        """
        idx_current = 0
        positions = []
        curr = self.textview.get_current_statement()
        buffer_ = self.textview.get_buffer()
        if curr is not None:
            # We're using the presence of an end iter as a flag if we're
            # inside a statement. If not, just jump to the next/prev statement
            # without respecting the offset.
            cstart, inside_statement = curr
        else:
            cstart = buffer_.get_iter_at_mark(buffer_.get_insert())
            cstart.set_line_offset(0)
            inside_statement = None
        for start, end in self.textview.get_statements():
            positions.append((start, end))
            if not inside_statement:
                if offset > 0 and start.get_line() > cstart.get_line():
                    buffer_.place_cursor(start)
                    return
                elif offset < 0 and start.get_line() > cstart.get_line():
                    if len(positions) > 2:
                        buffer_.place_cursor(positions[-2][0])
                    elif positions:  # there's just one statement buffered
                        buffer_.place_cursor(positions[0][0])
                    return
            elif start.equal(cstart):
                idx_current = len(positions) - 1
        max_idx = len(positions) - 1
        if offset > 0:
            new_pos = min(idx_current + offset, max_idx)
        else:
            new_pos = max(idx_current + offset, 0)
        buffer_.place_cursor(positions[new_pos][0])

    def selected_lines_toggle_comment(self):
        """Comments/uncomments selected lines."""
        buffer_ = self.textview.get_buffer()
        res = buffer_.get_selection_bounds()
        if not res:
            start = buffer_.get_iter_at_mark(buffer_.get_insert())
            lno_start = lno_end = start.get_line()
        else:
            start, end = res
            lno_start = start.get_line()
            lno_end = end.get_line()
        buffer_.begin_user_action()
        for line_no in xrange(lno_start, lno_end + 1):
            lstart = buffer_.get_iter_at_line(line_no)
            lend = lstart.copy()
            lend.forward_to_line_end()
            line = buffer_.get_text(lstart, lend)
            if not line.startswith('-- '):
                line = '-- %s' % line
            else:
                line = re.sub(r'^\s*-- ', '', line)
            buffer_.delete(lstart, lend)
            buffer_.insert(lstart, line)
        buffer_.end_user_action()

    def selected_lines_quick_format(self, **options):
        """Runs format without any other options than the default ones."""
        if not options:
            options = FORMATTER_DEFAULT_OPTIONS
        buffer_ = self.textview.get_buffer()
        res = buffer_.get_selection_bounds()
        if not res:
            start = end = None
            if self.app.config.get('editor.format_statement_at_cursor', False):
                curr = self.textview.get_current_statement()
                if curr:
                    start, end = curr
                    select_range = False
            if start is None:
                start, end = buffer_.get_bounds()
                select_range = False
        else:
            start, end = res
            select_range = True
        # Count chars excluding whitespaces
        insert_mark = buffer_.get_insert()
        insert_iter = buffer_.get_iter_at_mark(insert_mark)
        if insert_iter.in_range(start, end):
            char_offset = len(
                re.sub(r'\s', '', buffer_.get_text(start, insert_iter)))
            start_offset = start.get_offset()
        else:
            char_offset = start_offset = None
        orig = buffer_.get_text(start, end)
        formatted = sqlparse.format(orig, **options)
        # Modify buffer
        buffer_.begin_user_action()
        buffer_.delete(start, end)
        buffer_.insert(start, formatted)
        # Set selection again
        if select_range:
            end = start.copy()
            end.backward_chars(len(formatted))
            buffer_.select_range(end, start)
        buffer_.end_user_action()
        # TODO: place cursor - but how to do this... :)
        if char_offset is not None:
            num = 0
            idx = 0
            iter_ = buffer_.get_iter_at_offset(start_offset)
            for i in range(len(formatted)):
                if formatted[i] not in '\r\n\t ':
                    num += 1
                if num == char_offset:
                    iter_.forward_chars(i + 1)
                    buffer_.place_cursor(iter_)
                    self.textview.scroll_to_mark(buffer_.get_insert(), 0.25)
                    break

    def toggle_results_pane(self):
        """Show or hide the result pane."""
        pane = self.widget.get_child2()
        if pane.get_property('visible'):
            pane.hide()
        else:
            pane.show()