SetHeaderData no longer requires crazy introspection.
[versaplex.git] / veranda / Main.py
blob965532c81efd047bfa4ea113072edba43bc155cd
1 #!/usr/bin/python
3 #------------------------------------------------------------------------------
4 # Veranda
5 # ~--------------~
7 # Original Author: Andrei "Garoth" Thorp <garoth@gmail.com>
9 # Description: This program provides a graphical frontend to using the
10 # Versaplex software. The user may provide SQL commands in the
11 # editor, and this program will forward those commands over DBus
12 # to Versaplexd. Versaplexd will then apply the commands to
13 # whatever database it is configured to use under the surface.
14 # As such, this program eventually should be capable of providing
15 # a graphical interface to many databases, using Versaplex for
16 # abstraction.
18 # Notes:
19 # Indentation: I use tabs only, 4 spaces per tab.
21 # Todo:
22 # Add version numbers for imports
23 #------------------------------------------------------------------------------
24 import sys
25 import pygtk
26 import gtk
27 import gtk.glade
28 import gtksourceview2 as gtksourceview
29 import dbus
30 import re
31 import pango
32 from Parser import Parser
33 from Resulter import Resulter
34 from Searcher import Searcher
35 #------------------------------------------------------------------------------
36 class MainUI:
37 #------------------------------------------------------------------------------
38 """ Generates the GUI for Veranda"""
39 #------------------
40 def __init__(self):
41 #------------------
42 """Initialize the program & ui"""
43 # Define some instance variables
44 self.name = "Veranda" # App name
45 self.version = "0.1.0" # Version
46 self.newNumbers = [] # for naming new tabs
47 self.database = DBusSql() # SQL Access driver
48 self.bottomState = False # False: notebookBottom closed
49 self.getObject = "get object" # Versaplex command for get object
50 self.listAll = "list all" # Versaplex command for list all
51 self.searcher = "" # Becomes a searcher object later
52 self.exposeEventID = 0 # Used to disconnect a signal
53 self.bindings = gtk.AccelGroup()# Keyboard bindings group
55 # Import Glade's XML and load it
56 self.gladeTree = gtk.glade.XML("ui.glade")
57 dic = {"on_exit":(gtk.mainquit)}
58 self.gladeTree.signal_autoconnect(dic)
60 # Grab some of the widgets for easy access
61 self.sidebar = ""
62 self.resulter = Resulter()
63 self.window = self.gladeTree.get_widget("window")
64 self.vboxMain = self.gladeTree.get_widget("vbox-main")
65 self.vpanedEditor = self.gladeTree.get_widget("vpaned-editor")
66 self.vpanedPanel = self.gladeTree.get_widget("vpaned-panel")
67 self.notebookTop = self.gladeTree.get_widget("notebook-top")
68 self.notebookBottom = self.gladeTree.get_widget("notebook-bottom")
69 self.buttonRun = self.gladeTree.get_widget("button-run")
70 self.buttonNewTab = self.gladeTree.get_widget("button-newtab")
71 self.buttonClose = self.gladeTree.get_widget("button-closetab")
72 self.buttonNext = self.gladeTree.get_widget("button-nexttab")
73 self.buttonPrevious = self.gladeTree.get_widget("button-lasttab")
74 self.entrySearch = self.gladeTree.get_widget("entry-search")
75 self.statusbar = self.gladeTree.get_widget("statusbar")
76 # Statusbar context ids: * "sidebar"
77 # * "run query"
78 # * "error"
79 # * "success"
81 # Misc Initializations
82 hbox = gtk.HBox()
83 hbox.show()
84 runImage = gtk.Image()
85 runImage.set_from_file("run.svg")
86 runImage.show()
87 hbox.pack_start(runImage)
88 label = gtk.Label(" Run")
89 label.show()
90 hbox.pack_start(label)
91 self.buttonRun.add(hbox)
93 hbox = gtk.HBox()
94 hbox.show()
95 newTabImage = gtk.Image()
96 newTabImage.set_from_file("new.svg")
97 newTabImage.show()
98 hbox.pack_start(newTabImage)
99 label = gtk.Label(" New Tab")
100 label.show()
101 hbox.pack_start(label)
102 self.buttonNewTab.add(hbox)
104 hbox = gtk.HBox()
105 hbox.show()
106 newTabImage = gtk.Image()
107 newTabImage.set_from_file("close.svg")
108 newTabImage.show()
109 hbox.pack_start(newTabImage)
110 label = gtk.Label(" Close Current Tab")
111 label.show()
112 hbox.pack_start(label)
113 self.buttonClose.add(hbox)
115 hbox = gtk.HBox()
116 hbox.show()
117 newTabImage = gtk.Image()
118 newTabImage.set_from_file("next.svg")
119 newTabImage.show()
120 hbox.pack_start(newTabImage)
121 label = gtk.Label(" Next Tab")
122 label.show()
123 hbox.pack_start(label)
124 self.buttonNext.add(hbox)
126 hbox = gtk.HBox()
127 hbox.show()
128 newTabImage = gtk.Image()
129 newTabImage.set_from_file("previous.svg")
130 newTabImage.show()
131 hbox.pack_start(newTabImage)
132 label = gtk.Label(" Previous Tab")
133 label.show()
134 hbox.pack_start(label)
135 self.buttonPrevious.add(hbox)
137 # Open a first tab (comes with configured editor)
138 self.newTab()
140 # Connect events & key strokes
141 self.window.connect("delete_event", gtk.main_quit)
142 self.buttonRun.connect("clicked", self.runQuery)
143 self.buttonNewTab.connect("clicked", self.newTab)
144 self.buttonClose.connect("clicked", self.closeCurrentTab)
145 self.buttonNext.connect("clicked", self.nextTab)
146 self.buttonPrevious.connect("clicked", self.lastTab)
147 self.entrySearch.connect("key-release-event", self.search)
148 self.exposeEventID = self.window.connect("expose-event",
149 self.postStartInit)
151 self.window.add_accel_group(self.bindings)
152 self.buttonRun.add_accelerator("clicked", self.bindings,
153 ord("r"), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
154 self.buttonNewTab.add_accelerator("clicked", self.bindings,
155 ord("t"), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
156 self.buttonNext.add_accelerator("clicked", self.bindings,
157 ord("n"), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
158 self.buttonPrevious.add_accelerator("clicked", self.bindings,
159 ord("p"), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
160 self.buttonClose.add_accelerator("clicked", self.bindings,
161 ord("w"), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
162 self.buttonClose.add_accelerator("clicked", self.bindings,
163 ord("c"), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
165 # Show things
166 self.window.show()
168 #---------------------
169 def initSidebar(self):
170 #---------------------
171 """ Initializes the sidebar with the tables list and configures it"""
172 toList = ["table", "view", "procedure",
173 "trigger", "scalarfunction", "tablefunction"]
175 statusID = self.statusbar.get_context_id("sidebar")
176 self.statusbar.push(statusID, "Initializing Sidebar")
178 scrolls = gtk.ScrolledWindow(gtk.Adjustment(), gtk.Adjustment())
179 scrolls.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
181 treestore = gtk.TreeStore(str)
182 self.sidebar = gtk.TreeView(treestore)
183 cell = gtk.CellRendererText()
184 column = gtk.TreeViewColumn("Database Objects", cell, text=0)
185 self.sidebar.append_column(column)
187 masterTable = []
189 for item in toList:
190 result = self.database.query(self.listAll+" "+item)
191 if "Error" not in result:
192 statusID = self.statusbar.get_context_id("success")
193 self.statusbar.push(statusID, "Success.")
194 parser = Parser(result)
195 table = parser.getTable()[:] #the [:] makes a clone
196 table.insert(0, [item])
197 masterTable.append(table)
198 rows = parser.getTableIterator()
199 iter = treestore.append(None, [item.title()])
200 while rows.hasNext():
201 treestore.append(iter, [str(rows.getNext()[0])])
202 else:
203 statusID = self.statusbar.get_context_id("error")
204 self.statusbar.push(statusID, result)
206 self.searcher = Searcher(masterTable)
208 self.sidebar.connect("row-activated", self.rowClicked, masterTable)
210 scrolls.add(self.sidebar)
211 self.vpanedPanel.add(scrolls)
212 scrolls.show()
213 self.sidebar.show()
215 self.statusbar.push(statusID, "Sidebar Loaded")
217 #-----------------------
218 def getNewNumber(self):
219 #-----------------------
220 """ Get a unique number to number a tab """
221 x = 0
222 while (True):
223 if x in self.newNumbers:
224 x = x+1
225 else:
226 self.newNumbers.append(x)
227 return r" "+str(x)+r" "
229 #----------------------------------------
230 def removeNumber(self, editor, notebook):
231 #----------------------------------------
232 """ If a given page has a label with an automatic
233 number, remove that number from the list of numbers so that
234 it can be reassigned to a new fresh tab in the future"""
235 label = self.getLabelText(editor, notebook)
236 label = label.split(" ")
237 self.newNumbers.remove(int(label[0]))
239 #---------------------------------------------
240 def configureEditor(self, editor, textbuffer):
241 #---------------------------------------------
242 """Sets up a gtksourceview with the common options I want."""
243 languagemanager = gtksourceview.LanguageManager()
244 textbuffer.set_language(languagemanager.get_language("sql"))
245 textbuffer.set_highlight_syntax(True)
246 editor.set_show_line_numbers(True)
247 editor.set_wrap_mode(gtk.WRAP_WORD_CHAR)
248 editor.modify_font(pango.FontDescription("monospace 10"))
250 #---------------------------------------------
251 def makeBottomTabMenu(self, label, resulter):
252 #---------------------------------------------
253 """Returns an hbox with the title, change button, and close button
254 to be put in a tab"""
255 hbox = gtk.HBox()
256 label = gtk.Label(r" "+str(label)+r" ")
257 hbox.pack_start(label)
259 changeIcon = gtk.Image()
260 changeIcon.set_from_file("cycle.svg")
261 buttonMode = gtk.Button(None)
262 buttonMode.add(changeIcon)
263 hbox.pack_start(buttonMode, False, False, 1)
265 closeIcon = gtk.Image()
266 closeIcon.set_from_file("close.svg")
267 buttonClose = gtk.Button(None)
268 buttonClose.add(closeIcon)
269 hbox.pack_start(buttonClose, False, False, 1)
271 buttonClose.connect("clicked", self.closeTab, resulter)
272 buttonMode.connect("clicked", self.changeMode, resulter)
274 changeIcon.show()
275 closeIcon.show()
276 buttonMode.show()
277 label.show()
278 buttonClose.show()
279 hbox.show()
281 return hbox
283 #---------------------------------------
284 def showOutput(self, topEditor, result):
285 #---------------------------------------
286 parser = Parser(result)
288 if self.bottomState == False:
289 self.resulter.update(parser)
290 self.notebookBottom.show()
291 hbox = self.makeBottomTabMenu("Results", self.resulter)
292 self.newTabBottom(self.resulter.getCurrentView(), hbox)
293 self.bottomState = True
295 else :
296 index = self.notebookBottom.page_num(self.resulter.getCurrentView())
297 hbox = self.notebookBottom.get_tab_label(
298 self.resulter.getCurrentView())
299 self.resulter.update(parser)
300 self.notebookBottom.remove_page(index)
301 self.notebookBottom.insert_page(self.resulter.getCurrentView(),
302 hbox, index)
303 self.notebookBottom.set_tab_reorderable(
304 self.resulter.getCurrentView(), True)
305 self.notebookBottom.set_current_page(index)
307 #------------------------------------
308 def newTabBottom(self, widget, hbox):
309 #------------------------------------
310 """Creates a new tab on the bottom notebook, with "widget" in the tab
311 and "hbox" as the label (not actually a gtk label)"""
312 self.notebookBottom.append_page(widget, hbox)
314 #----------------------------------------
315 def getLabelText(self, editor, notebook):
316 #----------------------------------------
317 """Retrieves the label number from notebook with a page which contains
318 the given editor"""
319 hbox = notebook.get_tab_label(editor)
320 children = hbox.get_children()
321 labelText = children[0].get_text()
322 labelText = labelText.strip(' ')
323 return str(labelText)
325 #---------------------------------
326 def expandSidebar(self, sidebarList):
327 #---------------------------------
328 """Will expand some of the sidebar elements to make better use
329 of space"""
330 expandMax = 18
331 usedSoFar = 0
332 for section in sidebarList:
333 if len(section) + usedSoFar > expandMax:
334 break
335 else:
336 usedSoFar += len(section)
337 self.sidebar.expand_to_path((sidebarList.index(section),1))
339 #------------------------------------
340 def updateSidebar(self, sidebarList):
341 #------------------------------------
342 """Given a new list, this will change the contents of the sidebar"""
343 treestore = gtk.TreeStore(str)
345 for section in sidebarList:
346 iter = treestore.append(None,[section[0][0]])
347 for element in section[1:]:
348 treestore.append(iter,[element[0]])
350 self.sidebar.set_model(treestore)
352 self.expandSidebar(sidebarList)
354 #----------------------#
355 #-- CALLBACK METHODS --#
356 #----------------------#
358 #------------------------------------------
359 def postStartInit(self, widget, data=None):
360 #------------------------------------------
361 """ Initializes all the stuff that should only happen after the window
362 is already on screen"""
363 self.initSidebar()
364 widget.disconnect(self.exposeEventID)
366 #-------------------------------------
367 def runQuery(self, widget, data=None):
368 #-------------------------------------
369 """Uses the database abstraction (initially Dbus)
370 To send the query that is in the current window"""
371 scrolls = self.notebookTop.get_nth_page(self.notebookTop.
372 get_current_page())
373 if scrolls != None:
374 editor = scrolls.get_children()[0]
375 buffer = editor.get_buffer()
376 #get all text, not including hidden chars
377 query = buffer.get_text(buffer.get_start_iter(),
378 buffer.get_end_iter(), False)
380 contextID = self.statusbar.get_context_id("run query")
381 self.statusbar.push(contextID, "Ran query: "+query)
383 result = self.database.query(query)
384 if "Error" not in result:
385 self.showOutput(editor, result)
386 statusID = self.statusbar.get_context_id("success")
387 self.statusbar.push(statusID, "Success.")
388 else:
389 statusID = self.statusbar.get_context_id("error")
390 self.statusbar.push(statusID, result)
391 else:
392 contextID = self.statusbar.get_context_id("error")
393 self.statusbar.push(contextID, "No query to run.")
395 #-----------------------------------
396 def search(self, widget, data=None):
397 #-----------------------------------
398 """Incremental search callback. As the user types, this method
399 notices and modifies the sidebar"""
400 text = widget.get_text()
401 sidebarList = self.searcher.find(text)
402 self.updateSidebar(sidebarList)
404 #--------------------------------------
405 def newTab(self, widget=None, data=None):
406 #--------------------------------------
407 """Open a new editor tab (top). Data is an optional title for the tab."""
409 scrolls = gtk.ScrolledWindow(gtk.Adjustment(), gtk.Adjustment())
410 scrolls.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
412 textbuffer = gtksourceview.Buffer()
413 editor = gtksourceview.View(textbuffer)
415 self.configureEditor(editor, textbuffer)
417 hbox = gtk.HBox()
418 if data == None:
419 label = gtk.Label(self.getNewNumber())
420 else:
421 label = gtk.Label(self.getNewNumber()+str(data)+" ")
422 hbox.pack_start(label)
424 closeIcon = gtk.Image()
425 closeIcon.set_from_file("close.svg")
426 buttonClose = gtk.Button(None)
427 buttonClose.add(closeIcon)
428 hbox.pack_start(buttonClose, False, False, 1)
430 buttonClose.connect("clicked", self.closeTab, scrolls)
432 scrolls.add(editor)
433 self.notebookTop.append_page(scrolls, hbox)
434 self.notebookTop.set_tab_reorderable(scrolls, True)
436 scrolls.show()
437 closeIcon.show()
438 label.show()
439 buttonClose.show()
440 hbox.show()
441 editor.show()
443 # KEEP THIS LINE AT THE END OR ELSE! (hours of frustration...)
444 self.notebookTop.set_current_page(-1)
446 return editor
448 #--------------------------------------------
449 def closeTab(self, sourceWidget, targetWidget):
450 #--------------------------------------------
451 """Close a tab. targetWidget points to the contents of the notebook
452 tab that you want closed."""
454 index = -1
455 try:
456 index = self.notebookTop.page_num(targetWidget)
457 except TypeError:
458 pass
459 if index != -1:
460 self.removeNumber(targetWidget, self.notebookTop)
461 self.notebookTop.remove_page(index)
462 return
464 index = self.notebookBottom.page_num(targetWidget.getCurrentView())
465 if index != -1:
466 self.notebookBottom.remove_page(index)
467 self.bottomState = False
468 self.notebookBottom.queue_resize()
469 self.notebookTop.queue_resize()
470 return
472 if index == -1:
473 print "Worse Than Failure: Lost The Tab!"
475 #--------------------------------------------
476 def closeCurrentTab(self, widget, data=None):
477 #--------------------------------------------
478 """Closes the current tab in the top editor section"""
479 index = self.notebookTop.get_current_page()
480 self.notebookTop.remove_page(index)
482 #------------------------------------
483 def nextTab(self, widget, data=None):
484 #------------------------------------
485 """Changes to the previous tab"""
486 index = self.notebookTop.get_current_page()
487 self.notebookTop.set_current_page((index+1) % \
488 self.notebookTop.get_n_pages())
490 #------------------------------------
491 def lastTab(self, widget, data=None):
492 #------------------------------------
493 """Changes to the next tab"""
494 index = self.notebookTop.get_current_page()
495 self.notebookTop.set_current_page(index-1)
497 #--------------------------------------
498 def changeMode(self, widget, resulter):
499 #--------------------------------------
500 """After a change button is clicked, this makes the notebook tab
501 osscroll through the different view modes in a fixed pattern"""
502 pageIndex = self.notebookBottom.page_num(resulter.getCurrentView())
503 hbox = self.notebookBottom.get_tab_label(resulter.getCurrentView())
504 self.notebookBottom.remove_page(pageIndex)
505 self.notebookBottom.insert_page(resulter.getNextView(), hbox, pageIndex)
506 self.notebookBottom.set_tab_reorderable(resulter.getCurrentView(), True)
508 #-------------------------------------------------------------
509 def rowClicked(self, treeview, position, column, masterTable):
510 #-------------------------------------------------------------
511 """
512 Given the position coordinates and the master table (a
513 list of all data that is in the sidebar), this method opens
514 a new editor tab which has code in it. The code is the source
515 code to the object that was double clicked on in the sidebar.
516 If the item is a table, the code is just a select statement.
518 try:
519 type = masterTable[position[0]][0][0]
520 name = masterTable[position[0]][position[1]+1][0]
521 except IndexError:
522 contextID = self.statusbar.get_context_id("error")
523 self.statusbar.push(contextID,
524 "Can't do anything with a category title")
526 print "Can't do anything when a category title is clicked"
527 return
529 if type == "table":
530 query = "select top 100 * from [%s]" % name
532 contextID = self.statusbar.get_context_id("run query")
533 self.statusbar.push(contextID, "Ran query: "+query)
535 result = self.database.query(query)
537 if "Error" not in result:
538 editor = self.newTab(None, name)
539 buffer = editor.get_buffer()
540 buffer.set_text(query)
541 statusID = self.statusbar.get_context_id("success")
542 self.statusbar.push(statusID, "Success.")
544 self.showOutput(editor, result)
545 else:
546 statusID = self.statusbar.get_context_id("error")
547 self.statusbar.push(statusID, result)
549 else:
550 query = self.getObject + " " + type + " " + name
552 contextID = self.statusbar.get_context_id("run query")
553 self.statusbar.push(contextID, "Ran query: "+query)
555 result = self.database.query(query)
557 if "Error" in result:
558 statusID = self.statusbar.get_context_id("error")
559 self.statusbar.push(statusID, result)
560 return
561 else:
562 statusID = self.statusbar.get_context_id("success")
563 self.statusbar.push(statusID, "Success.")
565 parser = Parser(result)
566 data = parser.getTable()
567 commands = data[0][0]
569 com2 = commands[:]
570 pattern1 = re.compile(r"^create", re.I)
571 commands = re.sub(pattern1, r"ALTER", commands, 1)
572 if commands == com2:
573 pattern2 = re.compile(r"\ncreate", re.I)
574 commands = re.sub(pattern2, r"\nALTER", commands, 1)
576 editor = self.newTab(None, name)
577 buffer = editor.get_buffer()
578 buffer.set_text(commands)
580 #--------------------------#
581 #-- END CALLBACK METHODS --#
582 #--------------------------#
584 #------------------------------------------------------------------------------
585 class DBusSql:
586 #------------------------------------------------------------------------------
587 """ Provides abstraction for connecting to an SQL database via
588 DBus and the Versaplex software """
589 #TODO let dbus shut down nicely
590 #------------------
591 def __init__(self):
592 #------------------
593 """ Connects to DBus and connects to versaplex"""
594 print "~-------------------------------------------~"
595 print "| Setting up DBus Connection |"
596 print "| If you're using a non-standard bus, |"
597 print "| export DBUS_SESSION_BUS_ADDRESS |"
598 print "| to listen to it. For testing with WvDBus, |"
599 print "| it would be something like |"
600 print "| 'tcp:host=localhost,port=5432' |"
601 print "~-------------------------------------------~"
603 # FIXME Rewrite to use a config file instead of
604 # DBUS_SESSION_BUS_ADDRESS
605 self.bus = dbus.SessionBus()
606 # FIXME put most of this stuff in a config file
607 self.versaplex = self.bus.get_object("vx.versaplexd",
608 "/db")
609 self.versaplexI = dbus.Interface(self.versaplex, dbus_interface=
610 "vx.db")
612 #----------------------
613 def query(self, query):
614 #----------------------
615 """ Runs given query over dbus """
617 if query.lower().strip() == "test":
618 print "Running a test..."
619 result = self.versaplexI.Test()
620 print "Done."
621 return result
623 print "Ran Query:", query
624 try:
625 result = self.versaplexI.ExecRecordset(query)
626 except dbus.exceptions.DBusException:
627 # the string Error will be parsed to recognize the error.
628 result = "Error: " + str(sys.exc_info()[1])
629 print result
631 print "Done."
632 return result
634 mainUI=MainUI()
635 gtk.main()