3 #------------------------------------------------------------------------------
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
19 # Indentation: I use tabs only, 4 spaces per tab.
22 # Add version numbers for imports
23 #------------------------------------------------------------------------------
28 import gtksourceview2
as gtksourceview
32 from Parser
import Parser
33 from Resulter
import Resulter
34 from Searcher
import Searcher
35 #------------------------------------------------------------------------------
37 #------------------------------------------------------------------------------
38 """ Generates the GUI for Veranda"""
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
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"
81 # Misc Initializations
84 runImage
= gtk
.Image()
85 runImage
.set_from_file("run.svg")
87 hbox
.pack_start(runImage
)
88 label
= gtk
.Label(" Run")
90 hbox
.pack_start(label
)
91 self
.buttonRun
.add(hbox
)
95 newTabImage
= gtk
.Image()
96 newTabImage
.set_from_file("new.svg")
98 hbox
.pack_start(newTabImage
)
99 label
= gtk
.Label(" New Tab")
101 hbox
.pack_start(label
)
102 self
.buttonNewTab
.add(hbox
)
106 newTabImage
= gtk
.Image()
107 newTabImage
.set_from_file("close.svg")
109 hbox
.pack_start(newTabImage
)
110 label
= gtk
.Label(" Close Current Tab")
112 hbox
.pack_start(label
)
113 self
.buttonClose
.add(hbox
)
117 newTabImage
= gtk
.Image()
118 newTabImage
.set_from_file("next.svg")
120 hbox
.pack_start(newTabImage
)
121 label
= gtk
.Label(" Next Tab")
123 hbox
.pack_start(label
)
124 self
.buttonNext
.add(hbox
)
128 newTabImage
= gtk
.Image()
129 newTabImage
.set_from_file("previous.svg")
131 hbox
.pack_start(newTabImage
)
132 label
= gtk
.Label(" Previous Tab")
134 hbox
.pack_start(label
)
135 self
.buttonPrevious
.add(hbox
)
137 # Open a first tab (comes with configured editor)
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",
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
)
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
)
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])])
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
)
215 self
.statusbar
.push(statusID
, "Sidebar Loaded")
217 #-----------------------
218 def getNewNumber(self
):
219 #-----------------------
220 """ Get a unique number to number a tab """
223 if x
in self
.newNumbers
:
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"""
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
)
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
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(),
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
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
332 for section
in sidebarList
:
333 if len(section
) + usedSoFar
> expandMax
:
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"""
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
.
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.")
389 statusID
= self
.statusbar
.get_context_id("error")
390 self
.statusbar
.push(statusID
, result
)
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
)
419 label
= gtk
.Label(self
.getNewNumber())
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
)
433 self
.notebookTop
.append_page(scrolls
, hbox
)
434 self
.notebookTop
.set_tab_reorderable(scrolls
, True)
443 # KEEP THIS LINE AT THE END OR ELSE! (hours of frustration...)
444 self
.notebookTop
.set_current_page(-1)
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."""
456 index
= self
.notebookTop
.page_num(targetWidget
)
460 self
.removeNumber(targetWidget
, self
.notebookTop
)
461 self
.notebookTop
.remove_page(index
)
464 index
= self
.notebookBottom
.page_num(targetWidget
.getCurrentView())
466 self
.notebookBottom
.remove_page(index
)
467 self
.bottomState
= False
468 self
.notebookBottom
.queue_resize()
469 self
.notebookTop
.queue_resize()
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 #-------------------------------------------------------------
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.
519 type = masterTable
[position
[0]][0][0]
520 name
= masterTable
[position
[0]][position
[1]+1][0]
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"
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
)
546 statusID
= self
.statusbar
.get_context_id("error")
547 self
.statusbar
.push(statusID
, result
)
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
)
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]
570 pattern1
= re
.compile(r
"^create", re
.I
)
571 commands
= re
.sub(pattern1
, r
"ALTER", commands
, 1)
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 #------------------------------------------------------------------------------
586 #------------------------------------------------------------------------------
587 """ Provides abstraction for connecting to an SQL database via
588 DBus and the Versaplex software """
589 #TODO let dbus shut down nicely
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",
609 self
.versaplexI
= dbus
.Interface(self
.versaplex
, dbus_interface
=
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()
623 print "Ran Query:", query
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])