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_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
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()
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()