pgconsole/app.py
changeset 11 bc69eca59041
child 76 3a41b351b122
equal deleted inserted replaced
10:f3a1b9792cc9 11:bc69eca59041
       
     1 import time
       
     2 import gobject, gtk, pango, cairo
       
     3 import gtksourceview2 as gtksourceview
       
     4 
       
     5 import psycopg2
       
     6 import psycopg2.extensions
       
     7 import psycopg2.extras
       
     8 
       
     9 from pgconsole.config import cfg
       
    10 from pgconsole.editor import Editor
       
    11 from pgconsole.dataview import DataView
       
    12 from pgconsole.database import Database, BadConnectionError, DatabaseError
       
    13 from pgconsole.settings import Settings
       
    14 from pgconsole.panedext import HPanedExt, VPanedExt
       
    15 
       
    16 
       
    17 class PgConsoleApp:
       
    18     def __init__(self):
       
    19         self.db = Database()
       
    20         self.conn = None
       
    21 
       
    22         win = gtk.Window(gtk.WINDOW_TOPLEVEL)
       
    23         self.win = win
       
    24         win.set_title('PostgreSQL Console')
       
    25         win.set_size_request(300, 200)  # minimal size
       
    26         win.set_default_size(800, 600)
       
    27         self.restore_window_size()
       
    28         win.connect("destroy", self.destroy)
       
    29         win.connect("key_press_event", self.keypress)
       
    30         win.connect('configure-event', self.on_configure)
       
    31 
       
    32         # toolbar
       
    33         toolbar = gtk.Toolbar()
       
    34         toolbar.set_style(gtk.TOOLBAR_ICONS)
       
    35         toolbar.set_property("icon-size", gtk.ICON_SIZE_SMALL_TOOLBAR)
       
    36 
       
    37         tb = gtk.ToolButton(gtk.STOCK_PREFERENCES)
       
    38         tb.set_label('Settings')
       
    39         tb.set_tooltip_text('Settings')
       
    40         tb.connect('clicked', self.settings)
       
    41         self.tb_settings = tb
       
    42         toolbar.add(tb)
       
    43 
       
    44         self.cb_server = gtk.combo_box_entry_new_text()
       
    45         self.cb_server.set_tooltip_text('Server')
       
    46         self.cb_server.get_child().set_property("editable", False)
       
    47         self.cb_server.set_property("add_tearoffs", True)
       
    48         #self.cb_server.set_property("focus-on-click", False)
       
    49         self.cb_server.set_property("can-focus", True)
       
    50         self.cb_server.connect('changed', self.on_server_changed)
       
    51         self.cb_server.connect("key_press_event", self.toolbar_server_keypress)
       
    52         ti = gtk.ToolItem()
       
    53         ti.add(self.cb_server)
       
    54         toolbar.add(ti)
       
    55 
       
    56         self.cb_dbname = gtk.combo_box_entry_new_text()
       
    57         self.cb_dbname.set_tooltip_text('Database')
       
    58         self.cb_dbname.get_child().set_property("editable", False)
       
    59         self.cb_dbname.set_property("add_tearoffs", True)
       
    60         #self.cb_dbname.set_property("focus-on-click", False)
       
    61         self.cb_dbname.set_property("can-focus", True)
       
    62         ti = gtk.ToolItem()
       
    63         ti.add(self.cb_dbname)
       
    64         toolbar.add(ti)
       
    65 
       
    66         tb = gtk.ToolButton(gtk.STOCK_CONNECT)
       
    67         tb.set_label('Connect')
       
    68         tb.set_tooltip_text('Connect')
       
    69         tb.connect('clicked', self.connect)
       
    70         toolbar.add(tb)
       
    71         self.tb_connect = tb
       
    72 
       
    73         sep = gtk.SeparatorToolItem()
       
    74         toolbar.add(sep)
       
    75 
       
    76         tb = gtk.ToolButton(gtk.STOCK_EXECUTE)
       
    77         tb.set_label('Execute')
       
    78         tb.set_tooltip_text('Execute')
       
    79         tb.connect('clicked', self.execute)
       
    80         toolbar.add(tb)
       
    81         tb.set_sensitive(False)
       
    82         self.tb_execute = tb
       
    83         tb = gtk.ToolButton(gtk.STOCK_NEW)
       
    84         tb.set_label('Begin transaction')
       
    85         tb.set_tooltip_text('Begin transaction')
       
    86         tb.connect('clicked', self.begin)
       
    87         tb.set_sensitive(False)
       
    88         toolbar.add(tb)
       
    89         self.tb_begin = tb
       
    90         tb = gtk.ToolButton(gtk.STOCK_APPLY)
       
    91         tb.set_label('Commit')
       
    92         tb.set_tooltip_text('Commit')
       
    93         tb.connect('clicked', self.commit)
       
    94         tb.set_sensitive(False)
       
    95         self.tb_commit = tb
       
    96         toolbar.add(tb)
       
    97         tb = gtk.ToolButton(gtk.STOCK_CANCEL)
       
    98         tb.set_label('Rollback')
       
    99         tb.set_tooltip_text('Rollback')
       
   100         tb.connect('clicked', self.rollback)
       
   101         tb.set_sensitive(False)
       
   102         self.tb_rollback = tb
       
   103         toolbar.add(tb)
       
   104 
       
   105         sep = gtk.SeparatorToolItem()
       
   106         toolbar.add(sep)
       
   107 
       
   108         # editor
       
   109         self.editor = Editor()
       
   110 
       
   111         # data view
       
   112         self.dataview = DataView()
       
   113 
       
   114 
       
   115         vbox = gtk.VBox(False, 2)
       
   116 
       
   117         sep = gtk.SeparatorToolItem()
       
   118         sep.set_expand(True)
       
   119         sep.set_draw(False)
       
   120         toolbar.add(sep)
       
   121 
       
   122         self.throbber_anim = gtk.gdk.PixbufAnimation('data/Throbber.gif')
       
   123         self.throbber = gtk.Image()
       
   124         tb = gtk.ToolItem()
       
   125         tb.add(self.throbber)
       
   126         toolbar.add(tb)
       
   127         sep = gtk.SeparatorToolItem()
       
   128         sep.set_draw(False)
       
   129         toolbar.add(sep)
       
   130 
       
   131         vbox.pack_start(toolbar, False, False, 0)
       
   132 
       
   133         vpaned = VPanedExt()
       
   134         vpaned.set_border_width(5)
       
   135 
       
   136         hpaned = HPanedExt()
       
   137         hpaned.set_border_width(0)
       
   138         hpaned.add1(self.editor)
       
   139         hpaned.child_set_property(self.editor, 'shrink', False)
       
   140         hpaned.set_snap2(80)
       
   141         hpaned.set_property('position', 500)
       
   142 
       
   143         vpaned.add1(hpaned)
       
   144         vpaned.set_snap1(80)
       
   145         vpaned.set_property('position', 300)
       
   146 
       
   147         vpaned.add2(self.dataview)
       
   148         vpaned.set_snap2(80)
       
   149 
       
   150         self.vpaned = vpaned
       
   151         self.hpaned = hpaned
       
   152 
       
   153         # log
       
   154         self.logbuf = gtk.TextBuffer()
       
   155         view = gtk.TextView(self.logbuf)
       
   156         view.set_editable(False)
       
   157         font_desc = pango.FontDescription('monospace')
       
   158         if font_desc:
       
   159             view.modify_font(font_desc)
       
   160 
       
   161         sw = gtk.ScrolledWindow()
       
   162         sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
       
   163         sw.set_shadow_type(gtk.SHADOW_ETCHED_IN)
       
   164         sw.add(view)
       
   165         hpaned.add2(sw)
       
   166 
       
   167         vbox.pack_start(vpaned, padding=0)
       
   168 
       
   169         win.add(vbox)
       
   170 
       
   171         self.editor.view.grab_focus()
       
   172 
       
   173         self.reload_server_list(0)
       
   174 
       
   175         win.show_all()
       
   176 
       
   177         self.restore_win_state()
       
   178 
       
   179 
       
   180     def main(self):
       
   181         gtk.main()
       
   182 
       
   183 
       
   184     def destroy(self, widget, data=None):
       
   185         self.save_win_state()
       
   186         cfg.save()
       
   187         gtk.main_quit()
       
   188 
       
   189 
       
   190     def on_configure(self, w, ev):
       
   191         cfg.root.window.size.width = ev.width
       
   192         cfg.root.window.size.height = ev.height
       
   193 
       
   194 
       
   195     def restore_window_size(self):
       
   196         self.win.resize(cfg.root.window.size.width, cfg.root.window.size.height)
       
   197 
       
   198 
       
   199     def save_win_state(self):
       
   200         cfg.root.window.dividers.verticaldivider = self.vpaned.get_position()
       
   201         cfg.root.window.dividers.horizontaldivider = self.hpaned.get_position()
       
   202         cfg.root.window.dividers.editordivider = self.editor.get_position()
       
   203 
       
   204 
       
   205     def restore_win_state(self):
       
   206         pos = cfg.root.window.dividers.verticaldivider
       
   207         if pos >= 0:
       
   208             self.vpaned.set_position(pos)
       
   209         pos = cfg.root.window.dividers.horizontaldivider
       
   210         if pos >= 0:
       
   211             self.hpaned.set_position(pos)
       
   212         pos = cfg.root.window.dividers.editordivider
       
   213         if pos >= 0:
       
   214             self.editor.set_position(pos)
       
   215 
       
   216 
       
   217     def get_typename(self, oid, size):
       
   218         self.curs.execute('SELECT typname FROM pg_type WHERE oid=%s', [oid])
       
   219         psycopg2.extras.wait_select(self.curs.connection)
       
   220         row = self.curs.fetchone()
       
   221 
       
   222         typname = None
       
   223         if row:
       
   224             typname = row[0]
       
   225             if typname == 'int4':
       
   226                 return 'integer'
       
   227             if typname in ('timestamp', 'interval', 'date'):
       
   228                 return typname
       
   229             if size and size > 0 and size < 65535:
       
   230                 typname += '(%s)' % size
       
   231         return typname
       
   232 
       
   233 
       
   234     def get_conninfo(self, nodb=False):
       
   235         sel = self.cb_server.get_active()
       
   236         srv = cfg.servers.server[sel]
       
   237         if nodb:
       
   238             dbname = 'postgres'
       
   239         else:
       
   240             dbname = self.cb_dbname.get_active_text()
       
   241         conninfo = 'host=%s port=%s dbname=%s user=%s password=%s' \
       
   242             % (srv.host, srv.port, dbname, srv.user, srv.password)
       
   243         return conninfo
       
   244 
       
   245 
       
   246     def connect(self, w):
       
   247         conninfo = self.get_conninfo()
       
   248         if self.conn:
       
   249             # disconnect
       
   250             self.db.put_conn(conninfo, self.conn)
       
   251             self.conn = None
       
   252             self.cb_server.set_sensitive(True)
       
   253             self.cb_dbname.set_sensitive(True)
       
   254             self.tb_connect.set_stock_id(gtk.STOCK_CONNECT)
       
   255             self.tb_connect.set_label('Connect')
       
   256             self.tb_connect.set_tooltip_text('Connect')
       
   257             self.tb_execute.set_sensitive(False)
       
   258             self.tb_begin.set_sensitive(False)
       
   259         else:
       
   260             # connect
       
   261             self.logbuf.insert(self.logbuf.get_end_iter(), 'Connect %s\n' % conninfo)
       
   262             try:
       
   263                 self.conn = self.db.get_conn(conninfo)
       
   264             except DatabaseError as e:
       
   265                 self.logbuf.insert(self.logbuf.get_end_iter(), 'Error:\n%s\n' % e)
       
   266                 return
       
   267             self.cb_server.set_sensitive(False)
       
   268             self.cb_dbname.set_sensitive(False)
       
   269             self.tb_connect.set_stock_id(gtk.STOCK_DISCONNECT)
       
   270             self.tb_connect.set_label('Disconnect')
       
   271             self.tb_connect.set_tooltip_text('Disconnect')
       
   272             self.tb_execute.set_sensitive(True)
       
   273             self.tb_begin.set_sensitive(True)
       
   274 
       
   275 
       
   276     def begin(self, w):
       
   277         self.logbuf.insert(self.logbuf.get_end_iter(), 'Begin transaction\n')
       
   278         curs = self.conn.cursor()
       
   279         curs.execute('BEGIN')
       
   280         psycopg2.extras.wait_select(curs.connection)
       
   281 
       
   282         self.tb_connect.set_sensitive(False)
       
   283         self.tb_begin.set_sensitive(False)
       
   284         self.tb_commit.set_sensitive(True)
       
   285         self.tb_rollback.set_sensitive(True)
       
   286 
       
   287 
       
   288     def commit(self, w):
       
   289         self.logbuf.insert(self.logbuf.get_end_iter(), 'Commit\n')
       
   290         curs = self.conn.cursor()
       
   291         curs.execute('COMMIT')
       
   292         psycopg2.extras.wait_select(curs.connection)
       
   293 
       
   294         self.tb_connect.set_sensitive(True)
       
   295         self.tb_begin.set_sensitive(True)
       
   296         self.tb_commit.set_sensitive(False)
       
   297         self.tb_rollback.set_sensitive(False)
       
   298 
       
   299 
       
   300     def rollback(self, w):
       
   301         self.logbuf.insert(self.logbuf.get_end_iter(), 'Rollback\n')
       
   302         curs = self.conn.cursor()
       
   303         curs.execute('ROLLBACK')
       
   304         psycopg2.extras.wait_select(curs.connection)
       
   305 
       
   306         self.tb_connect.set_sensitive(True)
       
   307         self.tb_begin.set_sensitive(True)
       
   308         self.tb_commit.set_sensitive(False)
       
   309         self.tb_rollback.set_sensitive(False)
       
   310 
       
   311 
       
   312     def execute(self, widget):
       
   313         query = self.editor.get_selection() or self.editor.get_text()
       
   314 
       
   315         self.tb_connect.set_sensitive(False)
       
   316         self.tb_execute.set_sensitive(False)
       
   317         self.tb_begin.set_sensitive(False)
       
   318         self.tb_commit.set_sensitive(False)
       
   319         self.tb_rollback.set_sensitive(False)
       
   320         self.throbber.set_from_animation(self.throbber_anim)
       
   321 
       
   322         self.curs = self.conn.cursor()
       
   323 
       
   324         self.t1 = time.time()
       
   325         try:
       
   326             self.curs.execute(query)
       
   327         except (psycopg2.OperationalError, psycopg2.DatabaseError) as e:
       
   328             self.logbuf.insert(self.logbuf.get_end_iter(), 'Error:\n' + str(e))
       
   329             return
       
   330 
       
   331         self.execute_poll()
       
   332 
       
   333 
       
   334     def execute_poll(self, source=None, cond=None):
       
   335         try:
       
   336             state = self.conn.poll()
       
   337         except (psycopg2.OperationalError, psycopg2.DatabaseError) as e:
       
   338             self.logbuf.insert(self.logbuf.get_end_iter(), 'Error:\n' + str(e))
       
   339             return
       
   340 
       
   341         if state == psycopg2.extensions.POLL_OK:
       
   342             self.execute_finish()
       
   343         elif state == psycopg2.extensions.POLL_WRITE:
       
   344             gobject.io_add_watch(self.conn.fileno(), gobject.IO_OUT, self.execute_poll)
       
   345         elif state == psycopg2.extensions.POLL_READ:
       
   346             gobject.io_add_watch(self.conn.fileno(), gobject.IO_IN, self.execute_poll)
       
   347         else:
       
   348             self.logbuf.insert(self.logbuf.get_end_iter(), "poll() returned %s" % state)
       
   349             return
       
   350 
       
   351 
       
   352     def execute_finish(self):
       
   353         t2 = time.time()
       
   354         t = (t2 - self.t1)*1000
       
   355 
       
   356         self.throbber.clear()
       
   357 
       
   358         self.logbuf.insert(self.logbuf.get_end_iter(),
       
   359             'Query successful (%d ms, %d rows)\n' % (t, self.curs.rowcount))
       
   360 
       
   361         # notices
       
   362         for n in self.conn.notices:
       
   363             self.logbuf.insert(self.logbuf.get_end_iter(), n)
       
   364 
       
   365         if self.curs.rowcount >= 0:
       
   366             rows = self.curs.fetchall()
       
   367 
       
   368             names = []
       
   369             for c in self.curs.description:
       
   370                 name = c[0]
       
   371                 typename = self.get_typename(c[1], c[3])
       
   372                 names += [(name, typename)]
       
   373 
       
   374             self.dataview.load_data(names, rows)
       
   375 
       
   376         self.tb_execute.set_sensitive(True)
       
   377         if self.conn.get_transaction_status() == psycopg2.extensions.TRANSACTION_STATUS_INTRANS:
       
   378             self.tb_commit.set_sensitive(True)
       
   379             self.tb_rollback.set_sensitive(True)
       
   380         else:
       
   381             self.tb_connect.set_sensitive(True)
       
   382             self.tb_begin.set_sensitive(True)
       
   383 
       
   384 
       
   385     def simulate_click(self, tb):
       
   386         if tb.get_property('sensitive'):
       
   387             tb.get_child().activate()
       
   388 
       
   389 
       
   390     def keypress(self, w, event):
       
   391         keyname = gtk.gdk.keyval_name(event.keyval)
       
   392         if keyname == 'F1':
       
   393             self.simulate_click(self.tb_settings)
       
   394             return True
       
   395         if keyname == 'F2':
       
   396             print(self.cb_server.popup())
       
   397             return True
       
   398         if keyname == 'F3':
       
   399             print(self.cb_dbname.popup())
       
   400             return True
       
   401         if keyname == 'F4':
       
   402             self.simulate_click(self.tb_connect)
       
   403             return True
       
   404         if keyname == 'F5':
       
   405             self.simulate_click(self.tb_execute)
       
   406             return True
       
   407         if keyname == 'F6':
       
   408             self.simulate_click(self.tb_begin)
       
   409             return True
       
   410         if keyname == 'F7':
       
   411             self.simulate_click(self.tb_commit)
       
   412             return True
       
   413         if keyname == 'F8':
       
   414             self.simulate_click(self.tb_rollback)
       
   415             return True
       
   416 
       
   417         return False
       
   418 
       
   419 
       
   420     def toolbar_server_keypress(self, w, event):
       
   421         keyname = gtk.gdk.keyval_name(event.keyval)
       
   422         if keyname == 'Tab':
       
   423             self.populate_db_list()
       
   424             self.cb_dbname.grab_focus()
       
   425             return True
       
   426 
       
   427 
       
   428     def on_server_changed(self, w):
       
   429         if self.cb_server.get_active() != -1:
       
   430             self.populate_db_list()
       
   431 
       
   432 
       
   433     def populate_db_list(self):
       
   434         conninfo = self.get_conninfo(True)
       
   435         try:
       
   436             conn = self.db.get_conn(conninfo)
       
   437         except DatabaseError as e:
       
   438             self.logbuf.insert(self.logbuf.get_end_iter(), 'Error:\n%s\n' % e)
       
   439             return
       
   440 
       
   441         curs = conn.cursor()
       
   442         curs.execute('SELECT * FROM pg_catalog.pg_database WHERE NOT datistemplate ORDER BY 1')
       
   443         psycopg2.extras.wait_select(conn)
       
   444         rows = curs.fetchall()
       
   445 
       
   446         self.db.put_conn(conninfo, conn)
       
   447 
       
   448         for i in range(self.cb_dbname.get_model().iter_n_children(None)):
       
   449             self.cb_dbname.remove_text(0)
       
   450 
       
   451         for row in rows:
       
   452             self.cb_dbname.append_text(row[0])
       
   453 
       
   454         self.cb_dbname.set_active(0)
       
   455 
       
   456 
       
   457     def reload_server_list(self, sel=None):
       
   458         # clean
       
   459         for i in range(self.cb_server.get_model().iter_n_children(None)):
       
   460             self.cb_server.remove_text(0)
       
   461         self.cb_server.get_child().set_text('')
       
   462 
       
   463         try:
       
   464             # populate
       
   465             for server in cfg.servers.server:
       
   466                 if str(server.name):
       
   467                     title = '%s (%s)' % (str(server.name), str(server.host))
       
   468                 else:
       
   469                     title = str(server.host)
       
   470                 self.cb_server.append_text(title)
       
   471         except AttributeError:
       
   472             pass
       
   473 
       
   474         if not sel is None:
       
   475             self.cb_server.set_active(sel)
       
   476 
       
   477 
       
   478     def settings(self, w):
       
   479         Settings(self)
       
   480