# HG changeset patch # User Radek Brich # Date 1313531634 -7200 # Node ID f3a1b9792cc9c3cb150e440c5752d5d8782722ca # Parent 2fcc8ef0b97d29d38bd59de7ca6fd7f46f065cf1 Added pgconsole. It is my older project, a GUI query console. It uses GTK+ and asynchronous queries. diff -r 2fcc8ef0b97d -r f3a1b9792cc9 data/Throbber.gif Binary file data/Throbber.gif has changed diff -r 2fcc8ef0b97d -r f3a1b9792cc9 pgconsole.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pgconsole.py Tue Aug 16 23:53:54 2011 +0200 @@ -0,0 +1,500 @@ +#!/usr/bin/env python + +import time +import gobject, gtk, pango, cairo +import gtksourceview2 as gtksourceview + +import psycopg2 +import psycopg2.extensions +import psycopg2.extras + +from pgconsole.config import cfg +from pgconsole.editor import Editor +from pgconsole.dataview import DataView +from pgconsole.database import Database, BadConnectionError, DatabaseError +from pgconsole.settings import Settings +from pgconsole.panedext import HPanedExt, VPanedExt + + +class PgConsoleApp: + def __init__(self): + self.db = Database() + self.conn = None + + win = gtk.Window(gtk.WINDOW_TOPLEVEL) + self.win = win + win.set_title('PostgreSQL Console') + win.set_size_request(300, 200) # minimal size + win.set_default_size(800, 600) + self.restore_window_size() + win.connect("destroy", self.destroy) + win.connect("key_press_event", self.keypress) + win.connect('configure-event', self.on_configure) + + # toolbar + toolbar = gtk.Toolbar() + toolbar.set_style(gtk.TOOLBAR_ICONS) + toolbar.set_property("icon-size", gtk.ICON_SIZE_SMALL_TOOLBAR) + + tb = gtk.ToolButton(gtk.STOCK_PREFERENCES) + tb.set_label('Settings') + tb.set_tooltip_text('Settings') + tb.connect('clicked', self.settings) + self.tb_settings = tb + toolbar.add(tb) + + self.cb_server = gtk.combo_box_entry_new_text() + self.cb_server.set_tooltip_text('Server') + self.cb_server.get_child().set_property("editable", False) + self.cb_server.set_property("add_tearoffs", True) + #self.cb_server.set_property("focus-on-click", False) + self.cb_server.set_property("can-focus", True) + self.cb_server.connect('changed', self.on_server_changed) + self.cb_server.connect("key_press_event", self.toolbar_server_keypress) + ti = gtk.ToolItem() + ti.add(self.cb_server) + toolbar.add(ti) + + self.cb_dbname = gtk.combo_box_entry_new_text() + self.cb_dbname.set_tooltip_text('Database') + self.cb_dbname.get_child().set_property("editable", False) + self.cb_dbname.set_property("add_tearoffs", True) + #self.cb_dbname.set_property("focus-on-click", False) + self.cb_dbname.set_property("can-focus", True) + ti = gtk.ToolItem() + ti.add(self.cb_dbname) + toolbar.add(ti) + + tb = gtk.ToolButton(gtk.STOCK_CONNECT) + tb.set_label('Connect') + tb.set_tooltip_text('Connect') + tb.connect('clicked', self.connect) + toolbar.add(tb) + self.tb_connect = tb + + sep = gtk.SeparatorToolItem() + toolbar.add(sep) + + tb = gtk.ToolButton(gtk.STOCK_EXECUTE) + tb.set_label('Execute') + tb.set_tooltip_text('Execute') + tb.connect('clicked', self.execute) + toolbar.add(tb) + tb.set_sensitive(False) + self.tb_execute = tb + tb = gtk.ToolButton(gtk.STOCK_NEW) + tb.set_label('Begin transaction') + tb.set_tooltip_text('Begin transaction') + tb.connect('clicked', self.begin) + tb.set_sensitive(False) + toolbar.add(tb) + self.tb_begin = tb + tb = gtk.ToolButton(gtk.STOCK_APPLY) + tb.set_label('Commit') + tb.set_tooltip_text('Commit') + tb.connect('clicked', self.commit) + tb.set_sensitive(False) + self.tb_commit = tb + toolbar.add(tb) + tb = gtk.ToolButton(gtk.STOCK_CANCEL) + tb.set_label('Rollback') + tb.set_tooltip_text('Rollback') + tb.connect('clicked', self.rollback) + tb.set_sensitive(False) + self.tb_rollback = tb + toolbar.add(tb) + + sep = gtk.SeparatorToolItem() + toolbar.add(sep) + + # editor + self.editor = Editor() + + # data view + self.dataview = DataView() + + + vbox = gtk.VBox(False, 2) + + sep = gtk.SeparatorToolItem() + sep.set_expand(True) + sep.set_draw(False) + toolbar.add(sep) + + self.throbber_anim = gtk.gdk.PixbufAnimation('data/Throbber.gif') + self.throbber = gtk.Image() + tb = gtk.ToolItem() + tb.add(self.throbber) + toolbar.add(tb) + sep = gtk.SeparatorToolItem() + sep.set_draw(False) + toolbar.add(sep) + + vbox.pack_start(toolbar, False, False, 0) + + vpaned = VPanedExt() + vpaned.set_border_width(5) + + hpaned = HPanedExt() + hpaned.set_border_width(0) + hpaned.add1(self.editor) + hpaned.child_set_property(self.editor, 'shrink', False) + hpaned.set_snap2(80) + hpaned.set_property('position', 500) + + vpaned.add1(hpaned) + vpaned.set_snap1(80) + vpaned.set_property('position', 300) + + vpaned.add2(self.dataview) + vpaned.set_snap2(80) + + self.vpaned = vpaned + self.hpaned = hpaned + + # log + self.logbuf = gtk.TextBuffer() + view = gtk.TextView(self.logbuf) + view.set_editable(False) + font_desc = pango.FontDescription('monospace') + if font_desc: + view.modify_font(font_desc) + + sw = gtk.ScrolledWindow() + sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + sw.set_shadow_type(gtk.SHADOW_ETCHED_IN) + sw.add(view) + hpaned.add2(sw) + + vbox.pack_start(vpaned, padding=0) + + win.add(vbox) + + self.editor.view.grab_focus() + + self.reload_server_list(0) + + win.show_all() + + self.restore_win_state() + + + def main(self): + gtk.main() + + + def destroy(self, widget, data=None): + self.save_win_state() + cfg.save() + gtk.main_quit() + + + def on_configure(self, w, ev): + cfg.root.window.size.width = ev.width + cfg.root.window.size.height = ev.height + + + def restore_window_size(self): + self.win.resize(cfg.root.window.size.width, cfg.root.window.size.height) + + + def save_win_state(self): + cfg.root.window.dividers.verticaldivider = self.vpaned.get_position() + cfg.root.window.dividers.horizontaldivider = self.hpaned.get_position() + cfg.root.window.dividers.editordivider = self.editor.get_position() + + + def restore_win_state(self): + pos = cfg.root.window.dividers.verticaldivider + if pos >= 0: + self.vpaned.set_position(pos) + pos = cfg.root.window.dividers.horizontaldivider + if pos >= 0: + self.hpaned.set_position(pos) + pos = cfg.root.window.dividers.editordivider + if pos >= 0: + self.editor.set_position(pos) + + + def get_typename(self, oid, size): + self.curs.execute('SELECT typname FROM pg_type WHERE oid=%s', [oid]) + psycopg2.extras.wait_select(self.curs.connection) + row = self.curs.fetchone() + + typname = None + if row: + typname = row[0] + if typname == 'int4': + return 'integer' + if typname in ('timestamp', 'interval', 'date'): + return typname + if size and size > 0 and size < 65535: + typname += '(%s)' % size + return typname + + + def get_conninfo(self, nodb=False): + sel = self.cb_server.get_active() + srv = cfg.servers.server[sel] + if nodb: + dbname = 'postgres' + else: + dbname = self.cb_dbname.get_active_text() + conninfo = 'host=%s port=%s dbname=%s user=%s password=%s' \ + % (srv.host, srv.port, dbname, srv.user, srv.password) + return conninfo + + + def connect(self, w): + conninfo = self.get_conninfo() + if self.conn: + # disconnect + self.db.put_conn(conninfo, self.conn) + self.conn = None + self.cb_server.set_sensitive(True) + self.cb_dbname.set_sensitive(True) + self.tb_connect.set_stock_id(gtk.STOCK_CONNECT) + self.tb_connect.set_label('Connect') + self.tb_connect.set_tooltip_text('Connect') + self.tb_execute.set_sensitive(False) + self.tb_begin.set_sensitive(False) + else: + # connect + self.logbuf.insert(self.logbuf.get_end_iter(), 'Connect %s\n' % conninfo) + try: + self.conn = self.db.get_conn(conninfo) + except DatabaseError, e: + self.logbuf.insert(self.logbuf.get_end_iter(), 'Error:\n%s\n' % e) + return + self.cb_server.set_sensitive(False) + self.cb_dbname.set_sensitive(False) + self.tb_connect.set_stock_id(gtk.STOCK_DISCONNECT) + self.tb_connect.set_label('Disconnect') + self.tb_connect.set_tooltip_text('Disconnect') + self.tb_execute.set_sensitive(True) + self.tb_begin.set_sensitive(True) + + + def begin(self, w): + self.logbuf.insert(self.logbuf.get_end_iter(), 'Begin transaction\n') + curs = self.conn.cursor() + curs.execute('BEGIN') + psycopg2.extras.wait_select(curs.connection) + + self.tb_connect.set_sensitive(False) + self.tb_begin.set_sensitive(False) + self.tb_commit.set_sensitive(True) + self.tb_rollback.set_sensitive(True) + + + def commit(self, w): + self.logbuf.insert(self.logbuf.get_end_iter(), 'Commit\n') + curs = self.conn.cursor() + curs.execute('COMMIT') + psycopg2.extras.wait_select(curs.connection) + + self.tb_connect.set_sensitive(True) + self.tb_begin.set_sensitive(True) + self.tb_commit.set_sensitive(False) + self.tb_rollback.set_sensitive(False) + + + def rollback(self, w): + self.logbuf.insert(self.logbuf.get_end_iter(), 'Rollback\n') + curs = self.conn.cursor() + curs.execute('ROLLBACK') + psycopg2.extras.wait_select(curs.connection) + + self.tb_connect.set_sensitive(True) + self.tb_begin.set_sensitive(True) + self.tb_commit.set_sensitive(False) + self.tb_rollback.set_sensitive(False) + + + def execute(self, widget): + query = self.editor.get_selection() or self.editor.get_text() + + self.tb_connect.set_sensitive(False) + self.tb_execute.set_sensitive(False) + self.tb_begin.set_sensitive(False) + self.tb_commit.set_sensitive(False) + self.tb_rollback.set_sensitive(False) + self.throbber.set_from_animation(self.throbber_anim) + + self.curs = self.conn.cursor() + + self.t1 = time.time() + try: + self.curs.execute(query) + except (psycopg2.OperationalError, psycopg2.DatabaseError), e: + self.logbuf.insert(self.logbuf.get_end_iter(), 'Error:\n' + str(e)) + return + + self.execute_poll() + + + def execute_poll(self, source=None, cond=None): + try: + state = self.conn.poll() + except (psycopg2.OperationalError, psycopg2.DatabaseError), e: + self.logbuf.insert(self.logbuf.get_end_iter(), 'Error:\n' + str(e)) + return + + if state == psycopg2.extensions.POLL_OK: + self.execute_finish() + elif state == psycopg2.extensions.POLL_WRITE: + gobject.io_add_watch(self.conn.fileno(), gobject.IO_OUT, self.execute_poll) + elif state == psycopg2.extensions.POLL_READ: + gobject.io_add_watch(self.conn.fileno(), gobject.IO_IN, self.execute_poll) + else: + self.logbuf.insert(self.logbuf.get_end_iter(), "poll() returned %s" % state) + return + + + def execute_finish(self): + t2 = time.time() + t = (t2 - self.t1)*1000 + + self.throbber.clear() + + self.logbuf.insert(self.logbuf.get_end_iter(), + 'Query successful (%d ms, %d rows)\n' % (t, self.curs.rowcount)) + + # notices + for n in self.conn.notices: + self.logbuf.insert(self.logbuf.get_end_iter(), n) + + if self.curs.rowcount >= 0: + rows = self.curs.fetchall() + + names = [] + for c in self.curs.description: + name = c[0] + typename = self.get_typename(c[1], c[3]) + names += [(name, typename)] + + self.dataview.load_data(names, rows) + + self.tb_execute.set_sensitive(True) + if self.conn.get_transaction_status() == psycopg2.extensions.TRANSACTION_STATUS_INTRANS: + self.tb_commit.set_sensitive(True) + self.tb_rollback.set_sensitive(True) + else: + self.tb_connect.set_sensitive(True) + self.tb_begin.set_sensitive(True) + + + def simulate_click(self, tb): + if tb.get_property('sensitive'): + tb.get_child().activate() + + + def keypress(self, w, event): + keyname = gtk.gdk.keyval_name(event.keyval) + if keyname == 'F1': + self.simulate_click(self.tb_settings) + return True + if keyname == 'F2': + print self.cb_server.popup() + return True + if keyname == 'F3': + print self.cb_dbname.popup() + return True + if keyname == 'F4': + self.simulate_click(self.tb_connect) + return True + if keyname == 'F5': + self.simulate_click(self.tb_execute) + return True + if keyname == 'F6': + self.simulate_click(self.tb_begin) + return True + if keyname == 'F7': + self.simulate_click(self.tb_commit) + return True + if keyname == 'F8': + self.simulate_click(self.tb_rollback) + return True + + return False + + + def toolbar_server_keypress(self, w, event): + keyname = gtk.gdk.keyval_name(event.keyval) + if keyname == 'Tab': + self.populate_db_list() + self.cb_dbname.grab_focus() + return True + + + def on_server_changed(self, w): + if self.cb_server.get_active() != -1: + self.populate_db_list() + + + def populate_db_list(self): + conninfo = self.get_conninfo(True) + try: + conn = self.db.get_conn(conninfo) + except DatabaseError, e: + self.logbuf.insert(self.logbuf.get_end_iter(), 'Error:\n%s\n' % e) + return + + curs = conn.cursor() + curs.execute('SELECT * FROM pg_catalog.pg_database WHERE NOT datistemplate ORDER BY 1') + psycopg2.extras.wait_select(conn) + rows = curs.fetchall() + + self.db.put_conn(conninfo, conn) + + for i in range(self.cb_dbname.get_model().iter_n_children(None)): + self.cb_dbname.remove_text(0) + + for row in rows: + self.cb_dbname.append_text(row[0]) + + self.cb_dbname.set_active(0) + + + def reload_server_list(self, sel=None): + # clean + for i in range(self.cb_server.get_model().iter_n_children(None)): + self.cb_server.remove_text(0) + self.cb_server.get_child().set_text('') + + try: + # populate + for server in cfg.servers.server: + if str(server.name): + title = '%s (%s)' % (str(server.name), str(server.host)) + else: + title = str(server.host) + self.cb_server.append_text(title) + except AttributeError: + pass + + if not sel is None: + self.cb_server.set_active(sel) + + + def settings(self, w): + Settings(self) + + + +if __name__ == '__main__': + try: + cfg.load('pgconsole.xml.gz') + except IOError: + cfg.new('pgconsole.xml.gz') + + try: + gtkrc = cfg.root.style.gtkrc.text + except AttributeError: + pass + else: + gtk.rc_parse_string(gtkrc) + + app = PgConsoleApp() + app.main() + diff -r 2fcc8ef0b97d -r f3a1b9792cc9 pgconsole/__init__.py diff -r 2fcc8ef0b97d -r f3a1b9792cc9 pgconsole/config.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pgconsole/config.py Tue Aug 16 23:53:54 2011 +0200 @@ -0,0 +1,73 @@ +from lxml import etree +from lxml import objectify + +class Config: + def __init__(self): + self.tree = None + self.fname = None + + + def new(self, fname=None): + E = objectify.E + self.root = E.psqlconsole( + E.servers, + E.editor( + E.nodes( + E.node('-- Type your query here', name='Untitled', type='text'), + selected='Untitled' + ) + ), + E.window( + E.size( + E.width(800), + E.height(600) + ), + E.dividers( + E.verticaldivider(300), + E.horizontaldivider(500), + E.editordivider(-1) + ) + ) + ) + self.tree = etree.ElementTree(self.root) + self.servers = self.root.servers + self.fname = fname + + + def load(self, fname=None): + if fname: + self.fname = fname + self.tree = objectify.parse(self.fname) + self.root = self.tree.getroot() + self.servers = self.root.servers + + + def save(self, fname=None): + if fname: + self.fname = fname + self.tree.write( + self.fname, + encoding='utf-8', + xml_declaration=True, + pretty_print=True, + compression=6) + + + def add_server(self, name, host, port, user, password): + e = etree.SubElement(self.servers, 'server') + e.name = name + e.host = host + e.port = port + e.user = user + e.password = password + return e + + +cfg = Config() + + +if __name__ == '__main__': + cfg.load('../psqlconsole.xml.gz') + print cfg.root.servers.server[0].host.text + cfg.save() + diff -r 2fcc8ef0b97d -r f3a1b9792cc9 pgconsole/database.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pgconsole/database.py Tue Aug 16 23:53:54 2011 +0200 @@ -0,0 +1,101 @@ +import psycopg2 +import psycopg2.extensions +import psycopg2.extras + + +class DatabaseError(Exception): + def __init__(self, msg, query=None): + self.query = query + Exception.__init__(self, msg) + + +class BadConnectionError(Exception): + pass + + +class Row(dict): + def __getattr__(self, key): + return self[key] + + +class Database: + def __init__(self): + # pool of database connections + # indexed by conninfo, items are lists of connections + self.pool = {} + # number of unused connections per conninfo to keep open + self.pool_keep_open = 1 + + + def __del__(self): + for conninfo in self.pool.keys(): + for conn in self.pool[conninfo]: + conn.close() + + + def connect(self, conninfo): + try: + conn = psycopg2.connect(conninfo, async=1) + psycopg2.extras.wait_select(conn) + except psycopg2.DatabaseError, e: + raise DatabaseError(str(e)) + return conn + + + def get_conn(self, conninfo): + if not conninfo in self.pool: + self.pool[conninfo] = [] + return self.connect(conninfo) + else: + conn = None + while len(self.pool[conninfo]) and conn is None: + conn = self.pool[conninfo].pop() + if conn.closed: + conn = None + if conn is None: + return self.connect(conninfo) + return conn + + + def put_conn(self, conninfo, conn): + if len(self.pool[conninfo]) >= self.pool_keep_open: + conn.close() + else: + self.pool[conninfo].append(conn) + + + def execute(self, q, args=[]): + conn = self.get_conn() + try: + curs = conn.cursor() + curs.execute(q, args) + psycopg2.extras.wait_select(curs.connection) +# conn.commit() + except psycopg2.OperationalError, e: + # disconnected? +# conn.rollback() + conn.close() + raise BadConnectionError(str(e)) + except psycopg2.DatabaseError, e: +# conn.rollback() + raise DatabaseError(str(e), curs.query) + return curs + + + def finish(self, curs): + self.put_conn(curs.connection) + + + def row(self, curs, row): + return Row(zip([x[0] for x in curs.description], row)) + + + def fetchone(self, curs): + return self.row(curs, curs.fetchone()) + + + def fetchall(self, curs): + rows = curs.fetchall() + return [self.row(curs, row) for row in rows] + + diff -r 2fcc8ef0b97d -r f3a1b9792cc9 pgconsole/dataview.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pgconsole/dataview.py Tue Aug 16 23:53:54 2011 +0200 @@ -0,0 +1,139 @@ +import gtk, gobject, pango +from cgi import escape + + +class DataView(gtk.ScrolledWindow): + def __init__(self): + super(DataView, self).__init__() + + sw = self + sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + sw.set_shadow_type(gtk.SHADOW_ETCHED_IN) + self.treeview = gtk.TreeView(gtk.ListStore(str)) + self.treeview.get_selection().set_mode(gtk.SELECTION_MULTIPLE) + self.treeview.set_property("rubber-banding", True) + sw.add(self.treeview) + + + def load_data(self, names, rows): + x = [str] * (len(names) + 1) + liststore = gtk.ListStore(*x) + + for c in self.treeview.get_columns(): + self.treeview.remove_column(c) + + tvcolumn = gtk.TreeViewColumn() + cell = DataViewCellRenderer() + cell.set_property('head', True) + tvcolumn.pack_start(cell, True) + tvcolumn.set_property('resizable', True) + #tvcolumn.set_property('sizing', gtk.TREE_VIEW_COLUMN_FIXED) + tvcolumn.set_attributes(cell, text=0) + tvcolumn.set_min_width(2) + self.treeview.append_column(tvcolumn) + + i = 0 + for c in names: + i += 1 + typename = c[1] + title = '%s' % escape(c[0]) + if typename: + title += '\n%s' % typename + + tvcolumn = gtk.TreeViewColumn() + cell = DataViewCellRenderer() + + lab = gtk.Label() + lab.set_use_underline(False) + lab.set_markup(title) + lab.show() + tvcolumn.set_widget(lab) + tvcolumn.set_property('resizable', True) + tvcolumn.pack_start(cell, True) + tvcolumn.set_attributes(cell, text=i) + + self.treeview.append_column(tvcolumn) + + self.treeview.set_model(liststore) + + i = 0 + for row in rows: + i += 1 + liststore.append([i]+list(row)) + + + +class DataViewCellRenderer(gtk.GenericCellRenderer): + __gtype_name__ = 'DataViewCellRenderer' + __gproperties__ = { + 'text': (gobject.TYPE_STRING, None, None, '', gobject.PARAM_READWRITE), + 'head': (gobject.TYPE_BOOLEAN, None, None, False, gobject.PARAM_READWRITE)} + + + def __init__(self): + gtk.GenericCellRenderer.__init__(self) + self._props = {'text' : '', 'head' : False} + + + def do_set_property(self, pspec, value): + if not pspec.name in self._props: + raise AttributeError, 'Unknown property: %s' % pspec.name + self._props[pspec.name] = value + + + def do_get_property(self, pspec): + return self._props[pspec.name] + + + def on_get_size(self, widget, cell_area): + if cell_area == None: + pangoctx = widget.get_pango_context() + layout = pango.Layout(pangoctx) + layout.set_width(-1) + layout.set_text(self.get_property('text') or 'NULL') + w,h = layout.get_pixel_size() + return (0, 0, w+5, 20) + x = cell_area.x + y = cell_area.x + w = cell_area.width + h = cell_area.height + + return (x,y,w,h) + + + def on_render(self, window, widget, background_area, cell_area, expose_area, flags): + x = background_area.x + y = background_area.y + w = background_area.width + h = background_area.height + + ctx = window.cairo_create() + ctx.set_line_width(0.4) + ctx.set_source_rgb(0, 0, 0) + ctx.move_to(x+w-1.5, y-0.5) + ctx.line_to(x+w-1.5, y+h-0.5) + ctx.line_to(x-0.5, y+h-0.5) + ctx.stroke() + ctx.set_source_rgb(1, 1, 1) + ctx.move_to(x+w-0.5, y-0.5) + ctx.line_to(x+w-0.5, y+h-0.5) + ctx.stroke() + + pangoctx = widget.get_pango_context() + layout = pango.Layout(pangoctx) + text = self.get_property('text') + head = self.get_property('head') + + if head: + layout.set_markup('%s' % text) + else: + if text is None: + layout.set_markup('NULL') + else: + layout.set_text(text) + + widget.style.paint_layout(window, gtk.STATE_NORMAL, True, + cell_area, widget, '', + cell_area.x, cell_area.y, + layout) + diff -r 2fcc8ef0b97d -r f3a1b9792cc9 pgconsole/editor.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pgconsole/editor.py Tue Aug 16 23:53:54 2011 +0200 @@ -0,0 +1,455 @@ +import gtk, pango +import gtksourceview2 as gtksourceview +from lxml import etree + +from panedext import HPanedExt +from config import cfg + + +class Editor(HPanedExt): + def __init__(self): + super(Editor, self).__init__() + + self.view = gtksourceview.View() + self.view.connect('toggle-overwrite', self.on_toggle_overwrite) + + vbox = gtk.VBox() + self.vbox1 = vbox + self.add1(vbox) + + sw = gtk.ScrolledWindow() + sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + sw.set_shadow_type(gtk.SHADOW_IN) + vbox.pack_start(sw) + tree = gtk.TreeView() + tree.set_headers_visible(False) + tree.get_selection().set_mode(gtk.SELECTION_BROWSE) + sw.add(tree) + + model = gtk.ListStore(str, str, object, bool) # title, filename, buffer, modified + tree.set_model(model) + cell = gtk.CellRendererPixbuf() + cell.set_property('stock-id', gtk.STOCK_SAVE) + column = gtk.TreeViewColumn("File", cell, visible=3) + tree.append_column(column) + column = gtk.TreeViewColumn("File", gtk.CellRendererText(), text=0) + tree.append_column(column) + tree.get_selection().connect('changed', self.item_change) + tree.set_property('can-focus', False) + self.filelist = tree + + + hbox = gtk.HBox() + + img = gtk.Image() + img.set_from_stock(gtk.STOCK_NEW, gtk.ICON_SIZE_SMALL_TOOLBAR) + btn = gtk.Button() + btn.set_relief(gtk.RELIEF_NONE) + btn.set_focus_on_click(False) + btn.set_image(img) + btn.connect('clicked', self.item_new) + hbox.pack_start(btn, expand=False) + + img = gtk.Image() + img.set_from_stock(gtk.STOCK_OPEN, gtk.ICON_SIZE_SMALL_TOOLBAR) + btn = gtk.Button() + btn.set_relief(gtk.RELIEF_NONE) + btn.set_focus_on_click(False) + btn.set_image(img) + btn.connect('clicked', self.item_open) + hbox.pack_start(btn, expand=False) + + img = gtk.Image() + img.set_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_SMALL_TOOLBAR) + btn = gtk.Button() + btn.set_relief(gtk.RELIEF_NONE) + btn.set_focus_on_click(False) + btn.set_image(img) + btn.connect('clicked', self.item_close) + hbox.pack_start(btn, expand=False) + hbox.connect('size-request', self.leftbuttons_size_request) + + vbox.pack_start(hbox, expand=False) + + vbox = gtk.VBox() + vbox.set_property("width-request", 200) + self.add2(vbox) + self.child_set_property(vbox, 'shrink', False) + + + # scroll + sw = gtk.ScrolledWindow() + sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + sw.set_shadow_type(gtk.SHADOW_ETCHED_IN) + sw.add(self.view) + vbox.pack_start(sw) + + + self.view.set_show_line_numbers(True) + self.view.set_smart_home_end(True) + #self.view.set_insert_spaces_instead_of_tabs(True) + self.view.set_property("tab-width", 4) + self.view.set_auto_indent(True) + + # font + font_desc = pango.FontDescription('monospace') + if font_desc: + self.view.modify_font(font_desc) + + # status bar + hbox = gtk.HBox() + + self.st_file, fr1 = self.construct_status('SQL snippet') + self.st_file.set_ellipsize(pango.ELLIPSIZE_START) + self.st_insovr, fr2 = self.construct_status('INS') + self.st_linecol, fr3 = self.construct_status('Line: 0 Col: 0') + + + img = gtk.Image() + img.set_from_stock(gtk.STOCK_SAVE, gtk.ICON_SIZE_SMALL_TOOLBAR) + #save = img + save = gtk.Button() + save.set_relief(gtk.RELIEF_NONE) + save.set_focus_on_click(False) + save.set_image(img) + save.connect('clicked', self.item_save) + hbox.pack_start(save, expand=False) + + hbox.pack_start(fr1, expand=True) + hbox.pack_start(fr2, expand=False) + hbox.pack_start(fr3, expand=False) + + #sep = gtk.HSeparator() + #vbox.pack_start(sep, expand=False, fill=False, padding=0) + + #align = gtk.Alignment() + #align.set_property("bottom-padding", 3) + #align.set_property("xscale", 1.0) + #align.add(hbox) + + #frame = gtk.Frame() + #frame.add(hbox) + #frame.set_shadow_type(gtk.SHADOW_ETCHED_IN) + vbox.pack_start(hbox, expand=False, padding=0) + + self.load_nodes() + self.build_context_menu() + + + def build_context_menu(self): + menu = gtk.Menu() + item = gtk.ImageMenuItem(gtk.STOCK_SAVE, "Save") + item.connect("activate", self.item_save) + item.show() + menu.append(item) + + item = gtk.ImageMenuItem(gtk.STOCK_SAVE_AS, "Save as") + item.connect("activate", self.item_save_as) + item.show() + menu.append(item) + + item = gtk.ImageMenuItem(gtk.STOCK_CLOSE, "Close") + item.connect("activate", self.item_close) + item.show() + menu.append(item) + + item = gtk.SeparatorMenuItem() + item.show() + menu.append(item) + + item = gtk.ImageMenuItem(gtk.STOCK_GO_UP, "Move up") + item.show() + menu.append(item) + + item = gtk.ImageMenuItem(gtk.STOCK_GO_DOWN, "Move down") + item.show() + menu.append(item) + + self.filelist_menu = menu + + self.filelist.connect_object("button-press-event", self.on_filelist_button_press_event, menu) + + + def on_filelist_button_press_event(self, w, event): + if event.button == 3: + x = int(event.x) + y = int(event.y) + pathinfo = self.filelist.get_path_at_pos(x, y) + if pathinfo is not None: + path, col, cellx, celly = pathinfo + self.filelist.grab_focus() + self.filelist.set_cursor(path, col, 0) + self.filelist_menu.popup(None, None, None, event.button, event.time) + return True + + + + def make_buffer(self): + buffer = gtksourceview.Buffer() + buffer.connect('mark-set', self.buffer_mark_set) + buffer.connect('changed', self.buffer_changed) + + # style + mgr = gtksourceview.style_scheme_manager_get_default() + style_scheme = mgr.get_scheme('kate') + if style_scheme: + buffer.set_style_scheme(style_scheme) + + # syntax + lngman = gtksourceview.language_manager_get_default() + langsql = lngman.get_language('sql') + buffer.set_language(langsql) + buffer.set_highlight_syntax(True) + + return buffer + + + def load_nodes(self): + model = self.filelist.get_model() + sel = cfg.root.editor.nodes.get('selected') + for node in cfg.root.editor.nodes.node: + buffer = self.make_buffer() + name = node.get('name') + type = node.get('type') + filename = None + if type == 'text' and node.text: + buffer.set_text(node.text) + if type == 'file': + filename = node.text + content = open(filename).read() + buffer.set_text(content) + iter = model.append([name, filename, buffer, False]) + if sel == name: + self.filelist.get_selection().select_iter(iter) + + + def set_text(self, text): + self.buffer.set_text(text) + + + def get_text(self): + start, end = self.buffer.get_bounds() + return self.buffer.get_text(start, end) + + + def get_selection(self): + bounds = self.buffer.get_selection_bounds() + if not bounds: + return None + return self.buffer.get_text(*bounds) + + + def construct_status(self, text): + st = gtk.Label(text) + st.set_property("single-line-mode", True) + st.set_property("xpad", 3) + st.set_alignment(0.0, 0.5) + fr = gtk.Frame() + fr.set_shadow_type(gtk.SHADOW_ETCHED_OUT) + fr.add(st) + return st, fr + + + def on_toggle_overwrite(self, w): + if not w.get_overwrite(): + self.st_insovr.set_label('OVR') + else: + self.st_insovr.set_label('INS') + + + def buffer_mark_set(self, w, iter, textmark): + if textmark == w.get_insert(): + line = iter.get_line() + col = iter.get_visible_line_offset() + self.st_linecol.set_label('Line: %d Col: %d' % (line + 1, col + 1)) + + + def buffer_changed(self, w): + iter = w.get_iter_at_mark(w.get_insert()) + line = iter.get_line() + col = iter.get_visible_line_offset() + self.st_linecol.set_label('Line: %d Col: %d' % (line + 1, col + 1)) + + + def item_change(self, w): + model, sel = self.filelist.get_selection().get_selected_rows() + if not sel: + return + iter = model.get_iter(sel[0]) + title, filename, self.buffer = model.get(iter, 0, 1, 2) + self.view.set_buffer(self.buffer) + + if filename: + self.st_file.set_text(filename) + else: + self.st_file.set_text('SQL snippet') + + # update config + cfg.root.editor.nodes.set('selected', title) + + + def probe_title(self, model, title): + iter = model.get_iter_first() + i = 1 + while iter: + if model.get_value(iter, 0).split(' /')[0] == title: + new = title + if i > 1: + new += ' /%d' % i + model.set_value(iter, 0, new) + i += 1 + iter = model.iter_next(iter) + if i > 1: + title = '%s /%d' % (title, i) + return title + + + def item_new(self, w): + model = self.filelist.get_model() + title = 'Untitled' + title = self.probe_title(model, title) + buffer = self.make_buffer() + iter = model.append([title, None, buffer, False]) + self.filelist.get_selection().select_iter(iter) + + # update config + etree.SubElement(cfg.root.editor.nodes, 'node', type='text', name=title) + cfg.root.editor.nodes.set('selected', title) + cfg.save() + + + def item_open(self, w): + dialog = gtk.FileChooserDialog(title='Open file', action=gtk.FILE_CHOOSER_ACTION_OPEN, + buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN,gtk.RESPONSE_OK)) + dialog.set_default_response(gtk.RESPONSE_OK) + dialog.set_select_multiple(True) + + filter = gtk.FileFilter() + filter.set_name("SQL files") + filter.add_pattern("*.sql") + dialog.add_filter(filter) + + filter = gtk.FileFilter() + filter.set_name("All files") + filter.add_pattern("*") + dialog.add_filter(filter) + + response = dialog.run() + if response == gtk.RESPONSE_OK: + filenames = dialog.get_filenames() + for fname in filenames: + self.open_file(fname) + + dialog.destroy() + + + def open_file(self, filename): + title = filename.rsplit('/', 1)[1] + + model = self.filelist.get_model() + iter = model.get_iter_first() + i = 1 + while iter: + if model.get_value(iter, 1) == filename: + # file already opened, select it + self.filelist.get_selection().select_iter(iter) + return + iter = model.iter_next(iter) + + title = self.probe_title(model, title) + + # add item + buffer = self.make_buffer() + buffer.set_text(open(filename).read()) + iter = model.append([title, filename, buffer, False]) + self.filelist.get_selection().select_iter(iter) + + # update config + el = etree.SubElement(cfg.root.editor.nodes, 'node', type='file', name=title) + el._setText(filename) + cfg.root.editor.nodes.set('selected', title) + cfg.save() + + + def item_save(self, w): + model, sel = self.filelist.get_selection().get_selected_rows() + if not sel: + return + iter = model.get_iter(sel[0]) + + filename, buffer = model.get(iter, 1, 2) + + data = buffer.get_text(*buffer.get_bounds()) + open(filename, 'w').write(data) + + + def item_save_as(self, w): + model, sel = self.filelist.get_selection().get_selected_rows() + if not sel: + return + iter = model.get_iter(sel[0]) + + filename, buffer = model.get(iter, 1, 2) + + path, file = filename.rsplit('/',1) + + dialog = gtk.FileChooserDialog(title='Save as', action=gtk.FILE_CHOOSER_ACTION_SAVE, + buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_SAVE,gtk.RESPONSE_OK)) + dialog.set_default_response(gtk.RESPONSE_OK) + dialog.set_current_folder(path) + dialog.set_current_name(file) + + filter = gtk.FileFilter() + filter.set_name("SQL files") + filter.add_pattern("*.sql") + dialog.add_filter(filter) + + filter = gtk.FileFilter() + filter.set_name("All files") + filter.add_pattern("*") + dialog.add_filter(filter) + + response = dialog.run() + if response == gtk.RESPONSE_OK: + filename = dialog.get_filename() + data = buffer.get_text(*buffer.get_bounds()) + open(filename, 'w').write(data) + + title = filename.rsplit('/',1)[1] + title = self.probe_title(model, title) + model.set(iter, 0, title) + model.set(iter, 1, filename) + model.set(iter, 3, False) # modified + + dialog.destroy() + + + def item_close(self, w): + model, sel = self.filelist.get_selection().get_selected_rows() + if not sel: + return + iter = model.get_iter(sel[0]) + newiter = model.iter_next(iter) + if newiter is None and sel[0][0] > 0: + newiter = model.get_iter((sel[0][0]-1,)) + + title, buffer = model.get(iter, 0, 2) + #buffer.destroy() + + model.remove(iter) + + if newiter is None: + self.item_new(None) + else: + self.filelist.get_selection().select_iter(newiter) + + # update config + el = cfg.root.editor.nodes.find('node[@name="%s"]' % title) + el.getparent().remove(el) + cfg.save() + + + def leftbuttons_size_request(self, w, request): + self.set_snap1(request[0]) + return True + diff -r 2fcc8ef0b97d -r f3a1b9792cc9 pgconsole/panedext.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pgconsole/panedext.py Tue Aug 16 23:53:54 2011 +0200 @@ -0,0 +1,61 @@ +import gtk + + +class PanedExt(gtk.Paned): + """Extended gtk.Paned (abstract) + + set_snap1(int), set_snap2(int) + set minimum size of child widget + if the handle is moved to the edge, child widget is hidden + """ + def __init__(self): + self.connect('notify::position', self.on_position_change) + self.min1 = 0 + self.min2 = 0 + + + def set_snap1(self, minpos): + self.min1 = minpos + + + def set_snap2(self, minpos): + self.min2 = minpos + + + def on_position_change(self, w, scrolltype): + if self.allocation[0] == -1: + return False + + pos = self.get_position() + maxpos = self.get_property('max-position') + + if self.min1: + if pos > 0 and pos < self.min1: + if pos < self.min1 / 2: + self.set_position(0) + else: + self.set_position(self.min1) + return True + + if self.min2: + if pos > maxpos - self.min2 and pos < maxpos: + if pos > maxpos - self.min2 / 2: + self.set_position(maxpos) + else: + self.set_position(maxpos - self.min2) + return True + + return False + + +class HPanedExt(gtk.HPaned, PanedExt): + def __init__(self): + gtk.HPaned.__init__(self) + PanedExt.__init__(self) + + +class VPanedExt(gtk.VPaned, PanedExt): + def __init__(self): + gtk.VPaned.__init__(self) + PanedExt.__init__(self) + diff -r 2fcc8ef0b97d -r f3a1b9792cc9 pgconsole/settings.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pgconsole/settings.py Tue Aug 16 23:53:54 2011 +0200 @@ -0,0 +1,198 @@ +import gtk + +from config import cfg + + +class Settings(gtk.Window): + def __init__(self, parent): + super(Settings, self).__init__(gtk.WINDOW_TOPLEVEL) + + self._parent = parent + + self.set_title('Settings') + self.set_modal(True) + self.set_transient_for(parent.win) + self.set_position(gtk.WIN_POS_CENTER) + #self.set_border_width(10) + self.connect("key_press_event", self.on_keypress) + self.connect("destroy", self.on_destroy) + + self.tabs = gtk.Notebook() + self.add(self.tabs) + + ### Servers + vbox = gtk.VBox(spacing=10) + vbox.set_border_width(10) + self.tabs.append_page(vbox, gtk.Label('Servers')) + + hbox = gtk.HBox(spacing=10) + vbox.pack_start(hbox) + sw = gtk.ScrolledWindow() + sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + sw.set_shadow_type(gtk.SHADOW_IN) + hbox.pack_start(sw) + tree = gtk.TreeView() + tree.set_headers_visible(False) + sw.add(tree) + tree.get_selection().set_mode(gtk.SELECTION_BROWSE) + + model = gtk.ListStore(str, object) + tree.set_model(model) + column = gtk.TreeViewColumn("Server", gtk.CellRendererText(), text=0) + tree.append_column(column) + tree.get_selection().connect('changed', self.item_change) + tree.set_size_request(100, -1) + + try: + for server in cfg.servers.server: + if str(server.name): + title = '%s (%s)' % (str(server.name), str(server.host)) + else: + title = str(server.host) + model.append([title, server]) + except AttributeError: + pass + + self.itemtree = tree + + vbox = gtk.VBox(spacing=10) + hbox.pack_start(vbox) + + table = gtk.Table(5, 2) + table.set_row_spacings(4) + table.set_col_spacings(10) + vbox.pack_start(table) + + label = gtk.Label("Name:") + label.set_alignment(0.0, 0.5) + table.attach(label, 0, 1, 0, 1) + entry = gtk.Entry() + table.attach(entry, 1, 2, 0, 1) + self.entry_name = entry + + label = gtk.Label("Host:") + label.set_alignment(0.0, 0.5) + table.attach(label, 0, 1, 1, 2) + entry = gtk.Entry() + entry.set_text('127.0.0.1') + table.attach(entry, 1, 2, 1, 2) + self.entry_host = entry + + label = gtk.Label("Port:") + label.set_alignment(0.0, 0.5) + table.attach(label, 0, 1, 2, 3) + entry = gtk.Entry() + entry.set_text('5432') + table.attach(entry, 1, 2, 2, 3) + self.entry_port = entry + + label = gtk.Label("User:") + label.set_alignment(0.0, 0.5) + table.attach(label, 0, 1, 3, 4) + entry = gtk.Entry() + entry.set_text('postgres') + table.attach(entry, 1, 2, 3, 4) + self.entry_user = entry + + label = gtk.Label("Password:") + label.set_alignment(0.0, 0.5) + table.attach(label, 0, 1, 4, 5) + entry = gtk.Entry() + table.attach(entry, 1, 2, 4, 5) + self.entry_password = entry + + hbox = gtk.HBox() + vbox.pack_start(hbox) + + btn = gtk.Button('Add') + btn.connect('clicked', self.item_add) + hbox.pack_start(btn) + btn = gtk.Button('Save') + btn.connect('clicked', self.item_save) + hbox.pack_start(btn) + btn = gtk.Button('Remove') + btn.connect('clicked', self.item_remove) + hbox.pack_start(btn) + + ### Editor + vbox = gtk.VBox(spacing=10) + vbox.set_border_width(10) + self.tabs.append_page(vbox, gtk.Label('Editor')) + + self.show_all() + self.itemtree.grab_focus() + + + + def item_change(self, w): + model, sel = self.itemtree.get_selection().get_selected_rows() + if not sel: + return + el, = model.get(model.get_iter(sel[0]), 1) + + self.entry_name.set_text(str(el.name)) + self.entry_host.set_text(str(el.host)) + self.entry_port.set_text(str(el.port)) + self.entry_user.set_text(str(el.user)) + self.entry_password.set_text(str(el.password)) + + + def item_add(self, w): + el = cfg.add_server( + self.entry_name.get_text(), + self.entry_host.get_text(), + self.entry_port.get_text(), + self.entry_user.get_text(), + self.entry_password.get_text()) + if str(el.name): + title = '%s (%s)' % (str(el.name), str(el.host)) + else: + title = str(el.host) + iter = self.itemtree.get_model().append([title, el]) + self.itemtree.get_selection().select_iter(iter) + cfg.save() + + + def item_save(self, w): + model, sel = self.itemtree.get_selection().get_selected_rows() + el = cfg.servers.server[sel[0][0]] + el.name = self.entry_name.get_text() + el.host = self.entry_host.get_text() + el.port = self.entry_port.get_text() + el.user = self.entry_user.get_text() + el.password = self.entry_password.get_text() + cfg.save() + + if str(el.name): + title = '%s (%s)' % (str(el.name), str(el.host)) + else: + title = str(el.host) + model.set(model.get_iter(sel[0]), 0, title) + + + def item_remove(self, w): + model, sel = self.itemtree.get_selection().get_selected_rows() + model.remove(model.get_iter(sel[0])) + if model.get_iter_first(): + self.itemtree.get_selection().select_iter(model.get_iter_first()) + del cfg.servers.server[sel[0][0]] + cfg.save() + + + def on_keypress(self, w, event): + keyname = gtk.gdk.keyval_name(event.keyval) + + if keyname == 'Escape': + self.destroy() + return True + return False + + + def on_destroy(self, w): + model, sel = self.itemtree.get_selection().get_selected_rows() + if sel: + sel = sel[0][0] + else: + sel = None + self._parent.reload_server_list(sel) + diff -r 2fcc8ef0b97d -r f3a1b9792cc9 pgtoolkit/config.py --- a/pgtoolkit/config.py Tue Aug 16 16:03:46 2011 +0200 +++ b/pgtoolkit/config.py Tue Aug 16 23:53:54 2011 +0200 @@ -5,7 +5,7 @@ def __init__(self): self.args = {} # config file arguments self.registered_args = {} - self.log = logging.getLogger('config') + self.log = logging.getLogger('config') def add_argument(self, name, type=str, default=None): self.registered_args[name] = {'type':type, 'default':default} diff -r 2fcc8ef0b97d -r f3a1b9792cc9 tests/testprettysize.py --- a/tests/testprettysize.py Tue Aug 16 16:03:46 2011 +0200 +++ b/tests/testprettysize.py Tue Aug 16 23:53:54 2011 +0200 @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import unittest -import prettysize +from pgtoolkit import prettysize class TestHumansize(unittest.TestCase): def test_humansize(self):