1#!/usr/bin/python2
2# SPDX-License-Identifier: GPL-2.0
3# exported-sql-viewer.py: view data from sql database
4# Copyright (c) 2014-2018, Intel Corporation.
5
6# To use this script you will need to have exported data using either the
7# export-to-sqlite.py or the export-to-postgresql.py script.  Refer to those
8# scripts for details.
9#
10# Following on from the example in the export scripts, a
11# call-graph can be displayed for the pt_example database like this:
12#
13#	python tools/perf/scripts/python/exported-sql-viewer.py pt_example
14#
15# Note that for PostgreSQL, this script supports connecting to remote databases
16# by setting hostname, port, username, password, and dbname e.g.
17#
18#	python tools/perf/scripts/python/exported-sql-viewer.py "hostname=myhost username=myuser password=mypassword dbname=pt_example"
19#
20# The result is a GUI window with a tree representing a context-sensitive
21# call-graph.  Expanding a couple of levels of the tree and adjusting column
22# widths to suit will display something like:
23#
24#                                         Call Graph: pt_example
25# Call Path                          Object      Count   Time(ns)  Time(%)  Branch Count   Branch Count(%)
26# v- ls
27#     v- 2638:2638
28#         v- _start                  ld-2.19.so    1     10074071   100.0         211135            100.0
29#           |- unknown               unknown       1        13198     0.1              1              0.0
30#           >- _dl_start             ld-2.19.so    1      1400980    13.9          19637              9.3
31#           >- _d_linit_internal     ld-2.19.so    1       448152     4.4          11094              5.3
32#           v-__libc_start_main@plt  ls            1      8211741    81.5         180397             85.4
33#              >- _dl_fixup          ld-2.19.so    1         7607     0.1            108              0.1
34#              >- __cxa_atexit       libc-2.19.so  1        11737     0.1             10              0.0
35#              >- __libc_csu_init    ls            1        10354     0.1             10              0.0
36#              |- _setjmp            libc-2.19.so  1            0     0.0              4              0.0
37#              v- main               ls            1      8182043    99.6         180254             99.9
38#
39# Points to note:
40#	The top level is a command name (comm)
41#	The next level is a thread (pid:tid)
42#	Subsequent levels are functions
43#	'Count' is the number of calls
44#	'Time' is the elapsed time until the function returns
45#	Percentages are relative to the level above
46#	'Branch Count' is the total number of branches for that function and all
47#       functions that it calls
48
49import sys
50import weakref
51import threading
52import string
53import cPickle
54import re
55import os
56from PySide.QtCore import *
57from PySide.QtGui import *
58from PySide.QtSql import *
59from decimal import *
60from ctypes import *
61from multiprocessing import Process, Array, Value, Event
62
63# Data formatting helpers
64
65def dsoname(name):
66	if name == "[kernel.kallsyms]":
67		return "[kernel]"
68	return name
69
70# Percent to one decimal place
71
72def PercentToOneDP(n, d):
73	if not d:
74		return "0.0"
75	x = (n * Decimal(100)) / d
76	return str(x.quantize(Decimal(".1"), rounding=ROUND_HALF_UP))
77
78# Helper for queries that must not fail
79
80def QueryExec(query, stmt):
81	ret = query.exec_(stmt)
82	if not ret:
83		raise Exception("Query failed: " + query.lastError().text())
84
85# Background thread
86
87class Thread(QThread):
88
89	done = Signal(object)
90
91	def __init__(self, task, param=None, parent=None):
92		super(Thread, self).__init__(parent)
93		self.task = task
94		self.param = param
95
96	def run(self):
97		while True:
98			if self.param is None:
99				done, result = self.task()
100			else:
101				done, result = self.task(self.param)
102			self.done.emit(result)
103			if done:
104				break
105
106# Tree data model
107
108class TreeModel(QAbstractItemModel):
109
110	def __init__(self, root, parent=None):
111		super(TreeModel, self).__init__(parent)
112		self.root = root
113		self.last_row_read = 0
114
115	def Item(self, parent):
116		if parent.isValid():
117			return parent.internalPointer()
118		else:
119			return self.root
120
121	def rowCount(self, parent):
122		result = self.Item(parent).childCount()
123		if result < 0:
124			result = 0
125			self.dataChanged.emit(parent, parent)
126		return result
127
128	def hasChildren(self, parent):
129		return self.Item(parent).hasChildren()
130
131	def headerData(self, section, orientation, role):
132		if role == Qt.TextAlignmentRole:
133			return self.columnAlignment(section)
134		if role != Qt.DisplayRole:
135			return None
136		if orientation != Qt.Horizontal:
137			return None
138		return self.columnHeader(section)
139
140	def parent(self, child):
141		child_item = child.internalPointer()
142		if child_item is self.root:
143			return QModelIndex()
144		parent_item = child_item.getParentItem()
145		return self.createIndex(parent_item.getRow(), 0, parent_item)
146
147	def index(self, row, column, parent):
148		child_item = self.Item(parent).getChildItem(row)
149		return self.createIndex(row, column, child_item)
150
151	def DisplayData(self, item, index):
152		return item.getData(index.column())
153
154	def FetchIfNeeded(self, row):
155		if row > self.last_row_read:
156			self.last_row_read = row
157			if row + 10 >= self.root.child_count:
158				self.fetcher.Fetch(glb_chunk_sz)
159
160	def columnAlignment(self, column):
161		return Qt.AlignLeft
162
163	def columnFont(self, column):
164		return None
165
166	def data(self, index, role):
167		if role == Qt.TextAlignmentRole:
168			return self.columnAlignment(index.column())
169		if role == Qt.FontRole:
170			return self.columnFont(index.column())
171		if role != Qt.DisplayRole:
172			return None
173		item = index.internalPointer()
174		return self.DisplayData(item, index)
175
176# Table data model
177
178class TableModel(QAbstractTableModel):
179
180	def __init__(self, parent=None):
181		super(TableModel, self).__init__(parent)
182		self.child_count = 0
183		self.child_items = []
184		self.last_row_read = 0
185
186	def Item(self, parent):
187		if parent.isValid():
188			return parent.internalPointer()
189		else:
190			return self
191
192	def rowCount(self, parent):
193		return self.child_count
194
195	def headerData(self, section, orientation, role):
196		if role == Qt.TextAlignmentRole:
197			return self.columnAlignment(section)
198		if role != Qt.DisplayRole:
199			return None
200		if orientation != Qt.Horizontal:
201			return None
202		return self.columnHeader(section)
203
204	def index(self, row, column, parent):
205		return self.createIndex(row, column, self.child_items[row])
206
207	def DisplayData(self, item, index):
208		return item.getData(index.column())
209
210	def FetchIfNeeded(self, row):
211		if row > self.last_row_read:
212			self.last_row_read = row
213			if row + 10 >= self.child_count:
214				self.fetcher.Fetch(glb_chunk_sz)
215
216	def columnAlignment(self, column):
217		return Qt.AlignLeft
218
219	def columnFont(self, column):
220		return None
221
222	def data(self, index, role):
223		if role == Qt.TextAlignmentRole:
224			return self.columnAlignment(index.column())
225		if role == Qt.FontRole:
226			return self.columnFont(index.column())
227		if role != Qt.DisplayRole:
228			return None
229		item = index.internalPointer()
230		return self.DisplayData(item, index)
231
232# Model cache
233
234model_cache = weakref.WeakValueDictionary()
235model_cache_lock = threading.Lock()
236
237def LookupCreateModel(model_name, create_fn):
238	model_cache_lock.acquire()
239	try:
240		model = model_cache[model_name]
241	except:
242		model = None
243	if model is None:
244		model = create_fn()
245		model_cache[model_name] = model
246	model_cache_lock.release()
247	return model
248
249# Find bar
250
251class FindBar():
252
253	def __init__(self, parent, finder, is_reg_expr=False):
254		self.finder = finder
255		self.context = []
256		self.last_value = None
257		self.last_pattern = None
258
259		label = QLabel("Find:")
260		label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
261
262		self.textbox = QComboBox()
263		self.textbox.setEditable(True)
264		self.textbox.currentIndexChanged.connect(self.ValueChanged)
265
266		self.progress = QProgressBar()
267		self.progress.setRange(0, 0)
268		self.progress.hide()
269
270		if is_reg_expr:
271			self.pattern = QCheckBox("Regular Expression")
272		else:
273			self.pattern = QCheckBox("Pattern")
274		self.pattern.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
275
276		self.next_button = QToolButton()
277		self.next_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowDown))
278		self.next_button.released.connect(lambda: self.NextPrev(1))
279
280		self.prev_button = QToolButton()
281		self.prev_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowUp))
282		self.prev_button.released.connect(lambda: self.NextPrev(-1))
283
284		self.close_button = QToolButton()
285		self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
286		self.close_button.released.connect(self.Deactivate)
287
288		self.hbox = QHBoxLayout()
289		self.hbox.setContentsMargins(0, 0, 0, 0)
290
291		self.hbox.addWidget(label)
292		self.hbox.addWidget(self.textbox)
293		self.hbox.addWidget(self.progress)
294		self.hbox.addWidget(self.pattern)
295		self.hbox.addWidget(self.next_button)
296		self.hbox.addWidget(self.prev_button)
297		self.hbox.addWidget(self.close_button)
298
299		self.bar = QWidget()
300		self.bar.setLayout(self.hbox);
301		self.bar.hide()
302
303	def Widget(self):
304		return self.bar
305
306	def Activate(self):
307		self.bar.show()
308		self.textbox.setFocus()
309
310	def Deactivate(self):
311		self.bar.hide()
312
313	def Busy(self):
314		self.textbox.setEnabled(False)
315		self.pattern.hide()
316		self.next_button.hide()
317		self.prev_button.hide()
318		self.progress.show()
319
320	def Idle(self):
321		self.textbox.setEnabled(True)
322		self.progress.hide()
323		self.pattern.show()
324		self.next_button.show()
325		self.prev_button.show()
326
327	def Find(self, direction):
328		value = self.textbox.currentText()
329		pattern = self.pattern.isChecked()
330		self.last_value = value
331		self.last_pattern = pattern
332		self.finder.Find(value, direction, pattern, self.context)
333
334	def ValueChanged(self):
335		value = self.textbox.currentText()
336		pattern = self.pattern.isChecked()
337		index = self.textbox.currentIndex()
338		data = self.textbox.itemData(index)
339		# Store the pattern in the combo box to keep it with the text value
340		if data == None:
341			self.textbox.setItemData(index, pattern)
342		else:
343			self.pattern.setChecked(data)
344		self.Find(0)
345
346	def NextPrev(self, direction):
347		value = self.textbox.currentText()
348		pattern = self.pattern.isChecked()
349		if value != self.last_value:
350			index = self.textbox.findText(value)
351			# Allow for a button press before the value has been added to the combo box
352			if index < 0:
353				index = self.textbox.count()
354				self.textbox.addItem(value, pattern)
355				self.textbox.setCurrentIndex(index)
356				return
357			else:
358				self.textbox.setItemData(index, pattern)
359		elif pattern != self.last_pattern:
360			# Keep the pattern recorded in the combo box up to date
361			index = self.textbox.currentIndex()
362			self.textbox.setItemData(index, pattern)
363		self.Find(direction)
364
365	def NotFound(self):
366		QMessageBox.information(self.bar, "Find", "'" + self.textbox.currentText() + "' not found")
367
368# Context-sensitive call graph data model item base
369
370class CallGraphLevelItemBase(object):
371
372	def __init__(self, glb, row, parent_item):
373		self.glb = glb
374		self.row = row
375		self.parent_item = parent_item
376		self.query_done = False;
377		self.child_count = 0
378		self.child_items = []
379
380	def getChildItem(self, row):
381		return self.child_items[row]
382
383	def getParentItem(self):
384		return self.parent_item
385
386	def getRow(self):
387		return self.row
388
389	def childCount(self):
390		if not self.query_done:
391			self.Select()
392			if not self.child_count:
393				return -1
394		return self.child_count
395
396	def hasChildren(self):
397		if not self.query_done:
398			return True
399		return self.child_count > 0
400
401	def getData(self, column):
402		return self.data[column]
403
404# Context-sensitive call graph data model level 2+ item base
405
406class CallGraphLevelTwoPlusItemBase(CallGraphLevelItemBase):
407
408	def __init__(self, glb, row, comm_id, thread_id, call_path_id, time, branch_count, parent_item):
409		super(CallGraphLevelTwoPlusItemBase, self).__init__(glb, row, parent_item)
410		self.comm_id = comm_id
411		self.thread_id = thread_id
412		self.call_path_id = call_path_id
413		self.branch_count = branch_count
414		self.time = time
415
416	def Select(self):
417		self.query_done = True;
418		query = QSqlQuery(self.glb.db)
419		QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time), SUM(branch_count)"
420					" FROM calls"
421					" INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
422					" INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
423					" INNER JOIN dsos ON symbols.dso_id = dsos.id"
424					" WHERE parent_call_path_id = " + str(self.call_path_id) +
425					" AND comm_id = " + str(self.comm_id) +
426					" AND thread_id = " + str(self.thread_id) +
427					" GROUP BY call_path_id, name, short_name"
428					" ORDER BY call_path_id")
429		while query.next():
430			child_item = CallGraphLevelThreeItem(self.glb, self.child_count, self.comm_id, self.thread_id, query.value(0), query.value(1), query.value(2), query.value(3), int(query.value(4)), int(query.value(5)), self)
431			self.child_items.append(child_item)
432			self.child_count += 1
433
434# Context-sensitive call graph data model level three item
435
436class CallGraphLevelThreeItem(CallGraphLevelTwoPlusItemBase):
437
438	def __init__(self, glb, row, comm_id, thread_id, call_path_id, name, dso, count, time, branch_count, parent_item):
439		super(CallGraphLevelThreeItem, self).__init__(glb, row, comm_id, thread_id, call_path_id, time, branch_count, parent_item)
440		dso = dsoname(dso)
441		self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
442		self.dbid = call_path_id
443
444# Context-sensitive call graph data model level two item
445
446class CallGraphLevelTwoItem(CallGraphLevelTwoPlusItemBase):
447
448	def __init__(self, glb, row, comm_id, thread_id, pid, tid, parent_item):
449		super(CallGraphLevelTwoItem, self).__init__(glb, row, comm_id, thread_id, 1, 0, 0, parent_item)
450		self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
451		self.dbid = thread_id
452
453	def Select(self):
454		super(CallGraphLevelTwoItem, self).Select()
455		for child_item in self.child_items:
456			self.time += child_item.time
457			self.branch_count += child_item.branch_count
458		for child_item in self.child_items:
459			child_item.data[4] = PercentToOneDP(child_item.time, self.time)
460			child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
461
462# Context-sensitive call graph data model level one item
463
464class CallGraphLevelOneItem(CallGraphLevelItemBase):
465
466	def __init__(self, glb, row, comm_id, comm, parent_item):
467		super(CallGraphLevelOneItem, self).__init__(glb, row, parent_item)
468		self.data = [comm, "", "", "", "", "", ""]
469		self.dbid = comm_id
470
471	def Select(self):
472		self.query_done = True;
473		query = QSqlQuery(self.glb.db)
474		QueryExec(query, "SELECT thread_id, pid, tid"
475					" FROM comm_threads"
476					" INNER JOIN threads ON thread_id = threads.id"
477					" WHERE comm_id = " + str(self.dbid))
478		while query.next():
479			child_item = CallGraphLevelTwoItem(self.glb, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
480			self.child_items.append(child_item)
481			self.child_count += 1
482
483# Context-sensitive call graph data model root item
484
485class CallGraphRootItem(CallGraphLevelItemBase):
486
487	def __init__(self, glb):
488		super(CallGraphRootItem, self).__init__(glb, 0, None)
489		self.dbid = 0
490		self.query_done = True;
491		query = QSqlQuery(glb.db)
492		QueryExec(query, "SELECT id, comm FROM comms")
493		while query.next():
494			if not query.value(0):
495				continue
496			child_item = CallGraphLevelOneItem(glb, self.child_count, query.value(0), query.value(1), self)
497			self.child_items.append(child_item)
498			self.child_count += 1
499
500# Context-sensitive call graph data model
501
502class CallGraphModel(TreeModel):
503
504	def __init__(self, glb, parent=None):
505		super(CallGraphModel, self).__init__(CallGraphRootItem(glb), parent)
506		self.glb = glb
507
508	def columnCount(self, parent=None):
509		return 7
510
511	def columnHeader(self, column):
512		headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
513		return headers[column]
514
515	def columnAlignment(self, column):
516		alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
517		return alignment[column]
518
519	def FindSelect(self, value, pattern, query):
520		if pattern:
521			# postgresql and sqlite pattern patching differences:
522			#   postgresql LIKE is case sensitive but sqlite LIKE is not
523			#   postgresql LIKE allows % and _ to be escaped with \ but sqlite LIKE does not
524			#   postgresql supports ILIKE which is case insensitive
525			#   sqlite supports GLOB (text only) which uses * and ? and is case sensitive
526			if not self.glb.dbref.is_sqlite3:
527				# Escape % and _
528				s = value.replace("%", "\%")
529				s = s.replace("_", "\_")
530				# Translate * and ? into SQL LIKE pattern characters % and _
531				trans = string.maketrans("*?", "%_")
532				match = " LIKE '" + str(s).translate(trans) + "'"
533			else:
534				match = " GLOB '" + str(value) + "'"
535		else:
536			match = " = '" + str(value) + "'"
537		QueryExec(query, "SELECT call_path_id, comm_id, thread_id"
538						" FROM calls"
539						" INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
540						" INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
541						" WHERE symbols.name" + match +
542						" GROUP BY comm_id, thread_id, call_path_id"
543						" ORDER BY comm_id, thread_id, call_path_id")
544
545	def FindPath(self, query):
546		# Turn the query result into a list of ids that the tree view can walk
547		# to open the tree at the right place.
548		ids = []
549		parent_id = query.value(0)
550		while parent_id:
551			ids.insert(0, parent_id)
552			q2 = QSqlQuery(self.glb.db)
553			QueryExec(q2, "SELECT parent_id"
554					" FROM call_paths"
555					" WHERE id = " + str(parent_id))
556			if not q2.next():
557				break
558			parent_id = q2.value(0)
559		# The call path root is not used
560		if ids[0] == 1:
561			del ids[0]
562		ids.insert(0, query.value(2))
563		ids.insert(0, query.value(1))
564		return ids
565
566	def Found(self, query, found):
567		if found:
568			return self.FindPath(query)
569		return []
570
571	def FindValue(self, value, pattern, query, last_value, last_pattern):
572		if last_value == value and pattern == last_pattern:
573			found = query.first()
574		else:
575			self.FindSelect(value, pattern, query)
576			found = query.next()
577		return self.Found(query, found)
578
579	def FindNext(self, query):
580		found = query.next()
581		if not found:
582			found = query.first()
583		return self.Found(query, found)
584
585	def FindPrev(self, query):
586		found = query.previous()
587		if not found:
588			found = query.last()
589		return self.Found(query, found)
590
591	def FindThread(self, c):
592		if c.direction == 0 or c.value != c.last_value or c.pattern != c.last_pattern:
593			ids = self.FindValue(c.value, c.pattern, c.query, c.last_value, c.last_pattern)
594		elif c.direction > 0:
595			ids = self.FindNext(c.query)
596		else:
597			ids = self.FindPrev(c.query)
598		return (True, ids)
599
600	def Find(self, value, direction, pattern, context, callback):
601		class Context():
602			def __init__(self, *x):
603				self.value, self.direction, self.pattern, self.query, self.last_value, self.last_pattern = x
604			def Update(self, *x):
605				self.value, self.direction, self.pattern, self.last_value, self.last_pattern = x + (self.value, self.pattern)
606		if len(context):
607			context[0].Update(value, direction, pattern)
608		else:
609			context.append(Context(value, direction, pattern, QSqlQuery(self.glb.db), None, None))
610		# Use a thread so the UI is not blocked during the SELECT
611		thread = Thread(self.FindThread, context[0])
612		thread.done.connect(lambda ids, t=thread, c=callback: self.FindDone(t, c, ids), Qt.QueuedConnection)
613		thread.start()
614
615	def FindDone(self, thread, callback, ids):
616		callback(ids)
617
618# Vertical widget layout
619
620class VBox():
621
622	def __init__(self, w1, w2, w3=None):
623		self.vbox = QWidget()
624		self.vbox.setLayout(QVBoxLayout());
625
626		self.vbox.layout().setContentsMargins(0, 0, 0, 0)
627
628		self.vbox.layout().addWidget(w1)
629		self.vbox.layout().addWidget(w2)
630		if w3:
631			self.vbox.layout().addWidget(w3)
632
633	def Widget(self):
634		return self.vbox
635
636# Context-sensitive call graph window
637
638class CallGraphWindow(QMdiSubWindow):
639
640	def __init__(self, glb, parent=None):
641		super(CallGraphWindow, self).__init__(parent)
642
643		self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x))
644
645		self.view = QTreeView()
646		self.view.setModel(self.model)
647
648		for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)):
649			self.view.setColumnWidth(c, w)
650
651		self.find_bar = FindBar(self, self)
652
653		self.vbox = VBox(self.view, self.find_bar.Widget())
654
655		self.setWidget(self.vbox.Widget())
656
657		AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph")
658
659	def DisplayFound(self, ids):
660		if not len(ids):
661			return False
662		parent = QModelIndex()
663		for dbid in ids:
664			found = False
665			n = self.model.rowCount(parent)
666			for row in xrange(n):
667				child = self.model.index(row, 0, parent)
668				if child.internalPointer().dbid == dbid:
669					found = True
670					self.view.setCurrentIndex(child)
671					parent = child
672					break
673			if not found:
674				break
675		return found
676
677	def Find(self, value, direction, pattern, context):
678		self.view.setFocus()
679		self.find_bar.Busy()
680		self.model.Find(value, direction, pattern, context, self.FindDone)
681
682	def FindDone(self, ids):
683		found = True
684		if not self.DisplayFound(ids):
685			found = False
686		self.find_bar.Idle()
687		if not found:
688			self.find_bar.NotFound()
689
690# Child data item  finder
691
692class ChildDataItemFinder():
693
694	def __init__(self, root):
695		self.root = root
696		self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (None,) * 5
697		self.rows = []
698		self.pos = 0
699
700	def FindSelect(self):
701		self.rows = []
702		if self.pattern:
703			pattern = re.compile(self.value)
704			for child in self.root.child_items:
705				for column_data in child.data:
706					if re.search(pattern, str(column_data)) is not None:
707						self.rows.append(child.row)
708						break
709		else:
710			for child in self.root.child_items:
711				for column_data in child.data:
712					if self.value in str(column_data):
713						self.rows.append(child.row)
714						break
715
716	def FindValue(self):
717		self.pos = 0
718		if self.last_value != self.value or self.pattern != self.last_pattern:
719			self.FindSelect()
720		if not len(self.rows):
721			return -1
722		return self.rows[self.pos]
723
724	def FindThread(self):
725		if self.direction == 0 or self.value != self.last_value or self.pattern != self.last_pattern:
726			row = self.FindValue()
727		elif len(self.rows):
728			if self.direction > 0:
729				self.pos += 1
730				if self.pos >= len(self.rows):
731					self.pos = 0
732			else:
733				self.pos -= 1
734				if self.pos < 0:
735					self.pos = len(self.rows) - 1
736			row = self.rows[self.pos]
737		else:
738			row = -1
739		return (True, row)
740
741	def Find(self, value, direction, pattern, context, callback):
742		self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (value, direction,pattern, self.value, self.pattern)
743		# Use a thread so the UI is not blocked
744		thread = Thread(self.FindThread)
745		thread.done.connect(lambda row, t=thread, c=callback: self.FindDone(t, c, row), Qt.QueuedConnection)
746		thread.start()
747
748	def FindDone(self, thread, callback, row):
749		callback(row)
750
751# Number of database records to fetch in one go
752
753glb_chunk_sz = 10000
754
755# size of pickled integer big enough for record size
756
757glb_nsz = 8
758
759# Background process for SQL data fetcher
760
761class SQLFetcherProcess():
762
763	def __init__(self, dbref, sql, buffer, head, tail, fetch_count, fetching_done, process_target, wait_event, fetched_event, prep):
764		# Need a unique connection name
765		conn_name = "SQLFetcher" + str(os.getpid())
766		self.db, dbname = dbref.Open(conn_name)
767		self.sql = sql
768		self.buffer = buffer
769		self.head = head
770		self.tail = tail
771		self.fetch_count = fetch_count
772		self.fetching_done = fetching_done
773		self.process_target = process_target
774		self.wait_event = wait_event
775		self.fetched_event = fetched_event
776		self.prep = prep
777		self.query = QSqlQuery(self.db)
778		self.query_limit = 0 if "$$last_id$$" in sql else 2
779		self.last_id = -1
780		self.fetched = 0
781		self.more = True
782		self.local_head = self.head.value
783		self.local_tail = self.tail.value
784
785	def Select(self):
786		if self.query_limit:
787			if self.query_limit == 1:
788				return
789			self.query_limit -= 1
790		stmt = self.sql.replace("$$last_id$$", str(self.last_id))
791		QueryExec(self.query, stmt)
792
793	def Next(self):
794		if not self.query.next():
795			self.Select()
796			if not self.query.next():
797				return None
798		self.last_id = self.query.value(0)
799		return self.prep(self.query)
800
801	def WaitForTarget(self):
802		while True:
803			self.wait_event.clear()
804			target = self.process_target.value
805			if target > self.fetched or target < 0:
806				break
807			self.wait_event.wait()
808		return target
809
810	def HasSpace(self, sz):
811		if self.local_tail <= self.local_head:
812			space = len(self.buffer) - self.local_head
813			if space > sz:
814				return True
815			if space >= glb_nsz:
816				# Use 0 (or space < glb_nsz) to mean there is no more at the top of the buffer
817				nd = cPickle.dumps(0, cPickle.HIGHEST_PROTOCOL)
818				self.buffer[self.local_head : self.local_head + len(nd)] = nd
819			self.local_head = 0
820		if self.local_tail - self.local_head > sz:
821			return True
822		return False
823
824	def WaitForSpace(self, sz):
825		if self.HasSpace(sz):
826			return
827		while True:
828			self.wait_event.clear()
829			self.local_tail = self.tail.value
830			if self.HasSpace(sz):
831				return
832			self.wait_event.wait()
833
834	def AddToBuffer(self, obj):
835		d = cPickle.dumps(obj, cPickle.HIGHEST_PROTOCOL)
836		n = len(d)
837		nd = cPickle.dumps(n, cPickle.HIGHEST_PROTOCOL)
838		sz = n + glb_nsz
839		self.WaitForSpace(sz)
840		pos = self.local_head
841		self.buffer[pos : pos + len(nd)] = nd
842		self.buffer[pos + glb_nsz : pos + sz] = d
843		self.local_head += sz
844
845	def FetchBatch(self, batch_size):
846		fetched = 0
847		while batch_size > fetched:
848			obj = self.Next()
849			if obj is None:
850				self.more = False
851				break
852			self.AddToBuffer(obj)
853			fetched += 1
854		if fetched:
855			self.fetched += fetched
856			with self.fetch_count.get_lock():
857				self.fetch_count.value += fetched
858			self.head.value = self.local_head
859			self.fetched_event.set()
860
861	def Run(self):
862		while self.more:
863			target = self.WaitForTarget()
864			if target < 0:
865				break
866			batch_size = min(glb_chunk_sz, target - self.fetched)
867			self.FetchBatch(batch_size)
868		self.fetching_done.value = True
869		self.fetched_event.set()
870
871def SQLFetcherFn(*x):
872	process = SQLFetcherProcess(*x)
873	process.Run()
874
875# SQL data fetcher
876
877class SQLFetcher(QObject):
878
879	done = Signal(object)
880
881	def __init__(self, glb, sql, prep, process_data, parent=None):
882		super(SQLFetcher, self).__init__(parent)
883		self.process_data = process_data
884		self.more = True
885		self.target = 0
886		self.last_target = 0
887		self.fetched = 0
888		self.buffer_size = 16 * 1024 * 1024
889		self.buffer = Array(c_char, self.buffer_size, lock=False)
890		self.head = Value(c_longlong)
891		self.tail = Value(c_longlong)
892		self.local_tail = 0
893		self.fetch_count = Value(c_longlong)
894		self.fetching_done = Value(c_bool)
895		self.last_count = 0
896		self.process_target = Value(c_longlong)
897		self.wait_event = Event()
898		self.fetched_event = Event()
899		glb.AddInstanceToShutdownOnExit(self)
900		self.process = Process(target=SQLFetcherFn, args=(glb.dbref, sql, self.buffer, self.head, self.tail, self.fetch_count, self.fetching_done, self.process_target, self.wait_event, self.fetched_event, prep))
901		self.process.start()
902		self.thread = Thread(self.Thread)
903		self.thread.done.connect(self.ProcessData, Qt.QueuedConnection)
904		self.thread.start()
905
906	def Shutdown(self):
907		# Tell the thread and process to exit
908		self.process_target.value = -1
909		self.wait_event.set()
910		self.more = False
911		self.fetching_done.value = True
912		self.fetched_event.set()
913
914	def Thread(self):
915		if not self.more:
916			return True, 0
917		while True:
918			self.fetched_event.clear()
919			fetch_count = self.fetch_count.value
920			if fetch_count != self.last_count:
921				break
922			if self.fetching_done.value:
923				self.more = False
924				return True, 0
925			self.fetched_event.wait()
926		count = fetch_count - self.last_count
927		self.last_count = fetch_count
928		self.fetched += count
929		return False, count
930
931	def Fetch(self, nr):
932		if not self.more:
933			# -1 inidcates there are no more
934			return -1
935		result = self.fetched
936		extra = result + nr - self.target
937		if extra > 0:
938			self.target += extra
939			# process_target < 0 indicates shutting down
940			if self.process_target.value >= 0:
941				self.process_target.value = self.target
942			self.wait_event.set()
943		return result
944
945	def RemoveFromBuffer(self):
946		pos = self.local_tail
947		if len(self.buffer) - pos < glb_nsz:
948			pos = 0
949		n = cPickle.loads(self.buffer[pos : pos + glb_nsz])
950		if n == 0:
951			pos = 0
952			n = cPickle.loads(self.buffer[0 : glb_nsz])
953		pos += glb_nsz
954		obj = cPickle.loads(self.buffer[pos : pos + n])
955		self.local_tail = pos + n
956		return obj
957
958	def ProcessData(self, count):
959		for i in xrange(count):
960			obj = self.RemoveFromBuffer()
961			self.process_data(obj)
962		self.tail.value = self.local_tail
963		self.wait_event.set()
964		self.done.emit(count)
965
966# Fetch more records bar
967
968class FetchMoreRecordsBar():
969
970	def __init__(self, model, parent):
971		self.model = model
972
973		self.label = QLabel("Number of records (x " + "{:,}".format(glb_chunk_sz) + ") to fetch:")
974		self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
975
976		self.fetch_count = QSpinBox()
977		self.fetch_count.setRange(1, 1000000)
978		self.fetch_count.setValue(10)
979		self.fetch_count.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
980
981		self.fetch = QPushButton("Go!")
982		self.fetch.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
983		self.fetch.released.connect(self.FetchMoreRecords)
984
985		self.progress = QProgressBar()
986		self.progress.setRange(0, 100)
987		self.progress.hide()
988
989		self.done_label = QLabel("All records fetched")
990		self.done_label.hide()
991
992		self.spacer = QLabel("")
993
994		self.close_button = QToolButton()
995		self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
996		self.close_button.released.connect(self.Deactivate)
997
998		self.hbox = QHBoxLayout()
999		self.hbox.setContentsMargins(0, 0, 0, 0)
1000
1001		self.hbox.addWidget(self.label)
1002		self.hbox.addWidget(self.fetch_count)
1003		self.hbox.addWidget(self.fetch)
1004		self.hbox.addWidget(self.spacer)
1005		self.hbox.addWidget(self.progress)
1006		self.hbox.addWidget(self.done_label)
1007		self.hbox.addWidget(self.close_button)
1008
1009		self.bar = QWidget()
1010		self.bar.setLayout(self.hbox);
1011		self.bar.show()
1012
1013		self.in_progress = False
1014		self.model.progress.connect(self.Progress)
1015
1016		self.done = False
1017
1018		if not model.HasMoreRecords():
1019			self.Done()
1020
1021	def Widget(self):
1022		return self.bar
1023
1024	def Activate(self):
1025		self.bar.show()
1026		self.fetch.setFocus()
1027
1028	def Deactivate(self):
1029		self.bar.hide()
1030
1031	def Enable(self, enable):
1032		self.fetch.setEnabled(enable)
1033		self.fetch_count.setEnabled(enable)
1034
1035	def Busy(self):
1036		self.Enable(False)
1037		self.fetch.hide()
1038		self.spacer.hide()
1039		self.progress.show()
1040
1041	def Idle(self):
1042		self.in_progress = False
1043		self.Enable(True)
1044		self.progress.hide()
1045		self.fetch.show()
1046		self.spacer.show()
1047
1048	def Target(self):
1049		return self.fetch_count.value() * glb_chunk_sz
1050
1051	def Done(self):
1052		self.done = True
1053		self.Idle()
1054		self.label.hide()
1055		self.fetch_count.hide()
1056		self.fetch.hide()
1057		self.spacer.hide()
1058		self.done_label.show()
1059
1060	def Progress(self, count):
1061		if self.in_progress:
1062			if count:
1063				percent = ((count - self.start) * 100) / self.Target()
1064				if percent >= 100:
1065					self.Idle()
1066				else:
1067					self.progress.setValue(percent)
1068		if not count:
1069			# Count value of zero means no more records
1070			self.Done()
1071
1072	def FetchMoreRecords(self):
1073		if self.done:
1074			return
1075		self.progress.setValue(0)
1076		self.Busy()
1077		self.in_progress = True
1078		self.start = self.model.FetchMoreRecords(self.Target())
1079
1080# SQL data preparation
1081
1082def SQLTableDataPrep(query, count):
1083	data = []
1084	for i in xrange(count):
1085		data.append(query.value(i))
1086	return data
1087
1088# SQL table data model item
1089
1090class SQLTableItem():
1091
1092	def __init__(self, row, data):
1093		self.row = row
1094		self.data = data
1095
1096	def getData(self, column):
1097		return self.data[column]
1098
1099# SQL table data model
1100
1101class SQLTableModel(TableModel):
1102
1103	progress = Signal(object)
1104
1105	def __init__(self, glb, sql, column_count, parent=None):
1106		super(SQLTableModel, self).__init__(parent)
1107		self.glb = glb
1108		self.more = True
1109		self.populated = 0
1110		self.fetcher = SQLFetcher(glb, sql, lambda x, y=column_count: SQLTableDataPrep(x, y), self.AddSample)
1111		self.fetcher.done.connect(self.Update)
1112		self.fetcher.Fetch(glb_chunk_sz)
1113
1114	def DisplayData(self, item, index):
1115		self.FetchIfNeeded(item.row)
1116		return item.getData(index.column())
1117
1118	def AddSample(self, data):
1119		child = SQLTableItem(self.populated, data)
1120		self.child_items.append(child)
1121		self.populated += 1
1122
1123	def Update(self, fetched):
1124		if not fetched:
1125			self.more = False
1126			self.progress.emit(0)
1127		child_count = self.child_count
1128		count = self.populated - child_count
1129		if count > 0:
1130			parent = QModelIndex()
1131			self.beginInsertRows(parent, child_count, child_count + count - 1)
1132			self.insertRows(child_count, count, parent)
1133			self.child_count += count
1134			self.endInsertRows()
1135			self.progress.emit(self.child_count)
1136
1137	def FetchMoreRecords(self, count):
1138		current = self.child_count
1139		if self.more:
1140			self.fetcher.Fetch(count)
1141		else:
1142			self.progress.emit(0)
1143		return current
1144
1145	def HasMoreRecords(self):
1146		return self.more
1147
1148# SQL automatic table data model
1149
1150class SQLAutoTableModel(SQLTableModel):
1151
1152	def __init__(self, glb, table_name, parent=None):
1153		sql = "SELECT * FROM " + table_name + " WHERE id > $$last_id$$ ORDER BY id LIMIT " + str(glb_chunk_sz)
1154		if table_name == "comm_threads_view":
1155			# For now, comm_threads_view has no id column
1156			sql = "SELECT * FROM " + table_name + " WHERE comm_id > $$last_id$$ ORDER BY comm_id LIMIT " + str(glb_chunk_sz)
1157		self.column_headers = []
1158		query = QSqlQuery(glb.db)
1159		if glb.dbref.is_sqlite3:
1160			QueryExec(query, "PRAGMA table_info(" + table_name + ")")
1161			while query.next():
1162				self.column_headers.append(query.value(1))
1163			if table_name == "sqlite_master":
1164				sql = "SELECT * FROM " + table_name
1165		else:
1166			if table_name[:19] == "information_schema.":
1167				sql = "SELECT * FROM " + table_name
1168				select_table_name = table_name[19:]
1169				schema = "information_schema"
1170			else:
1171				select_table_name = table_name
1172				schema = "public"
1173			QueryExec(query, "SELECT column_name FROM information_schema.columns WHERE table_schema = '" + schema + "' and table_name = '" + select_table_name + "'")
1174			while query.next():
1175				self.column_headers.append(query.value(0))
1176		super(SQLAutoTableModel, self).__init__(glb, sql, len(self.column_headers), parent)
1177
1178	def columnCount(self, parent=None):
1179		return len(self.column_headers)
1180
1181	def columnHeader(self, column):
1182		return self.column_headers[column]
1183
1184# Base class for custom ResizeColumnsToContents
1185
1186class ResizeColumnsToContentsBase(QObject):
1187
1188	def __init__(self, parent=None):
1189		super(ResizeColumnsToContentsBase, self).__init__(parent)
1190
1191	def ResizeColumnToContents(self, column, n):
1192		# Using the view's resizeColumnToContents() here is extrememly slow
1193		# so implement a crude alternative
1194		font = self.view.font()
1195		metrics = QFontMetrics(font)
1196		max = 0
1197		for row in xrange(n):
1198			val = self.data_model.child_items[row].data[column]
1199			len = metrics.width(str(val) + "MM")
1200			max = len if len > max else max
1201		val = self.data_model.columnHeader(column)
1202		len = metrics.width(str(val) + "MM")
1203		max = len if len > max else max
1204		self.view.setColumnWidth(column, max)
1205
1206	def ResizeColumnsToContents(self):
1207		n = min(self.data_model.child_count, 100)
1208		if n < 1:
1209			# No data yet, so connect a signal to notify when there is
1210			self.data_model.rowsInserted.connect(self.UpdateColumnWidths)
1211			return
1212		columns = self.data_model.columnCount()
1213		for i in xrange(columns):
1214			self.ResizeColumnToContents(i, n)
1215
1216	def UpdateColumnWidths(self, *x):
1217		# This only needs to be done once, so disconnect the signal now
1218		self.data_model.rowsInserted.disconnect(self.UpdateColumnWidths)
1219		self.ResizeColumnsToContents()
1220
1221# Table window
1222
1223class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
1224
1225	def __init__(self, glb, table_name, parent=None):
1226		super(TableWindow, self).__init__(parent)
1227
1228		self.data_model = LookupCreateModel(table_name + " Table", lambda: SQLAutoTableModel(glb, table_name))
1229
1230		self.model = QSortFilterProxyModel()
1231		self.model.setSourceModel(self.data_model)
1232
1233		self.view = QTableView()
1234		self.view.setModel(self.model)
1235		self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
1236		self.view.verticalHeader().setVisible(False)
1237		self.view.sortByColumn(-1, Qt.AscendingOrder)
1238		self.view.setSortingEnabled(True)
1239
1240		self.ResizeColumnsToContents()
1241
1242		self.find_bar = FindBar(self, self, True)
1243
1244		self.finder = ChildDataItemFinder(self.data_model)
1245
1246		self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
1247
1248		self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
1249
1250		self.setWidget(self.vbox.Widget())
1251
1252		AddSubWindow(glb.mainwindow.mdi_area, self, table_name + " Table")
1253
1254	def Find(self, value, direction, pattern, context):
1255		self.view.setFocus()
1256		self.find_bar.Busy()
1257		self.finder.Find(value, direction, pattern, context, self.FindDone)
1258
1259	def FindDone(self, row):
1260		self.find_bar.Idle()
1261		if row >= 0:
1262			self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
1263		else:
1264			self.find_bar.NotFound()
1265
1266# Table list
1267
1268def GetTableList(glb):
1269	tables = []
1270	query = QSqlQuery(glb.db)
1271	if glb.dbref.is_sqlite3:
1272		QueryExec(query, "SELECT name FROM sqlite_master WHERE type IN ( 'table' , 'view' ) ORDER BY name")
1273	else:
1274		QueryExec(query, "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type IN ( 'BASE TABLE' , 'VIEW' ) ORDER BY table_name")
1275	while query.next():
1276		tables.append(query.value(0))
1277	if glb.dbref.is_sqlite3:
1278		tables.append("sqlite_master")
1279	else:
1280		tables.append("information_schema.tables")
1281		tables.append("information_schema.views")
1282		tables.append("information_schema.columns")
1283	return tables
1284
1285# Action Definition
1286
1287def CreateAction(label, tip, callback, parent=None, shortcut=None):
1288	action = QAction(label, parent)
1289	if shortcut != None:
1290		action.setShortcuts(shortcut)
1291	action.setStatusTip(tip)
1292	action.triggered.connect(callback)
1293	return action
1294
1295# Typical application actions
1296
1297def CreateExitAction(app, parent=None):
1298	return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit)
1299
1300# Typical MDI actions
1301
1302def CreateCloseActiveWindowAction(mdi_area):
1303	return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area)
1304
1305def CreateCloseAllWindowsAction(mdi_area):
1306	return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area)
1307
1308def CreateTileWindowsAction(mdi_area):
1309	return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area)
1310
1311def CreateCascadeWindowsAction(mdi_area):
1312	return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area)
1313
1314def CreateNextWindowAction(mdi_area):
1315	return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild)
1316
1317def CreatePreviousWindowAction(mdi_area):
1318	return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild)
1319
1320# Typical MDI window menu
1321
1322class WindowMenu():
1323
1324	def __init__(self, mdi_area, menu):
1325		self.mdi_area = mdi_area
1326		self.window_menu = menu.addMenu("&Windows")
1327		self.close_active_window = CreateCloseActiveWindowAction(mdi_area)
1328		self.close_all_windows = CreateCloseAllWindowsAction(mdi_area)
1329		self.tile_windows = CreateTileWindowsAction(mdi_area)
1330		self.cascade_windows = CreateCascadeWindowsAction(mdi_area)
1331		self.next_window = CreateNextWindowAction(mdi_area)
1332		self.previous_window = CreatePreviousWindowAction(mdi_area)
1333		self.window_menu.aboutToShow.connect(self.Update)
1334
1335	def Update(self):
1336		self.window_menu.clear()
1337		sub_window_count = len(self.mdi_area.subWindowList())
1338		have_sub_windows = sub_window_count != 0
1339		self.close_active_window.setEnabled(have_sub_windows)
1340		self.close_all_windows.setEnabled(have_sub_windows)
1341		self.tile_windows.setEnabled(have_sub_windows)
1342		self.cascade_windows.setEnabled(have_sub_windows)
1343		self.next_window.setEnabled(have_sub_windows)
1344		self.previous_window.setEnabled(have_sub_windows)
1345		self.window_menu.addAction(self.close_active_window)
1346		self.window_menu.addAction(self.close_all_windows)
1347		self.window_menu.addSeparator()
1348		self.window_menu.addAction(self.tile_windows)
1349		self.window_menu.addAction(self.cascade_windows)
1350		self.window_menu.addSeparator()
1351		self.window_menu.addAction(self.next_window)
1352		self.window_menu.addAction(self.previous_window)
1353		if sub_window_count == 0:
1354			return
1355		self.window_menu.addSeparator()
1356		nr = 1
1357		for sub_window in self.mdi_area.subWindowList():
1358			label = str(nr) + " " + sub_window.name
1359			if nr < 10:
1360				label = "&" + label
1361			action = self.window_menu.addAction(label)
1362			action.setCheckable(True)
1363			action.setChecked(sub_window == self.mdi_area.activeSubWindow())
1364			action.triggered.connect(lambda x=nr: self.setActiveSubWindow(x))
1365			self.window_menu.addAction(action)
1366			nr += 1
1367
1368	def setActiveSubWindow(self, nr):
1369		self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1])
1370
1371# Font resize
1372
1373def ResizeFont(widget, diff):
1374	font = widget.font()
1375	sz = font.pointSize()
1376	font.setPointSize(sz + diff)
1377	widget.setFont(font)
1378
1379def ShrinkFont(widget):
1380	ResizeFont(widget, -1)
1381
1382def EnlargeFont(widget):
1383	ResizeFont(widget, 1)
1384
1385# Unique name for sub-windows
1386
1387def NumberedWindowName(name, nr):
1388	if nr > 1:
1389		name += " <" + str(nr) + ">"
1390	return name
1391
1392def UniqueSubWindowName(mdi_area, name):
1393	nr = 1
1394	while True:
1395		unique_name = NumberedWindowName(name, nr)
1396		ok = True
1397		for sub_window in mdi_area.subWindowList():
1398			if sub_window.name == unique_name:
1399				ok = False
1400				break
1401		if ok:
1402			return unique_name
1403		nr += 1
1404
1405# Add a sub-window
1406
1407def AddSubWindow(mdi_area, sub_window, name):
1408	unique_name = UniqueSubWindowName(mdi_area, name)
1409	sub_window.setMinimumSize(200, 100)
1410	sub_window.resize(800, 600)
1411	sub_window.setWindowTitle(unique_name)
1412	sub_window.setAttribute(Qt.WA_DeleteOnClose)
1413	sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon))
1414	sub_window.name = unique_name
1415	mdi_area.addSubWindow(sub_window)
1416	sub_window.show()
1417
1418# Main window
1419
1420class MainWindow(QMainWindow):
1421
1422	def __init__(self, glb, parent=None):
1423		super(MainWindow, self).__init__(parent)
1424
1425		self.glb = glb
1426
1427		self.setWindowTitle("Exported SQL Viewer: " + glb.dbname)
1428		self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon))
1429		self.setMinimumSize(200, 100)
1430
1431		self.mdi_area = QMdiArea()
1432		self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
1433		self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
1434
1435		self.setCentralWidget(self.mdi_area)
1436
1437		menu = self.menuBar()
1438
1439		file_menu = menu.addMenu("&File")
1440		file_menu.addAction(CreateExitAction(glb.app, self))
1441
1442		edit_menu = menu.addMenu("&Edit")
1443		edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find))
1444		edit_menu.addAction(CreateAction("Fetch &more records...", "Fetch more records", self.FetchMoreRecords, self, [QKeySequence(Qt.Key_F8)]))
1445		edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")]))
1446		edit_menu.addAction(CreateAction("&Enlarge Font", "Make text bigger", self.EnlargeFont, self, [QKeySequence("Ctrl++")]))
1447
1448		reports_menu = menu.addMenu("&Reports")
1449		reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self))
1450
1451		self.TableMenu(GetTableList(glb), menu)
1452
1453		self.window_menu = WindowMenu(self.mdi_area, menu)
1454
1455	def Find(self):
1456		win = self.mdi_area.activeSubWindow()
1457		if win:
1458			try:
1459				win.find_bar.Activate()
1460			except:
1461				pass
1462
1463	def FetchMoreRecords(self):
1464		win = self.mdi_area.activeSubWindow()
1465		if win:
1466			try:
1467				win.fetch_bar.Activate()
1468			except:
1469				pass
1470
1471	def ShrinkFont(self):
1472		win = self.mdi_area.activeSubWindow()
1473		ShrinkFont(win.view)
1474
1475	def EnlargeFont(self):
1476		win = self.mdi_area.activeSubWindow()
1477		EnlargeFont(win.view)
1478
1479	def TableMenu(self, tables, menu):
1480		table_menu = menu.addMenu("&Tables")
1481		for table in tables:
1482			table_menu.addAction(CreateAction(table, "Create a new window containing a table view", lambda t=table: self.NewTableView(t), self))
1483
1484	def NewCallGraph(self):
1485		CallGraphWindow(self.glb, self)
1486
1487	def NewTableView(self, table_name):
1488		TableWindow(self.glb, table_name, self)
1489
1490# Global data
1491
1492class Glb():
1493
1494	def __init__(self, dbref, db, dbname):
1495		self.dbref = dbref
1496		self.db = db
1497		self.dbname = dbname
1498		self.app = None
1499		self.mainwindow = None
1500		self.instances_to_shutdown_on_exit = weakref.WeakSet()
1501
1502	def AddInstanceToShutdownOnExit(self, instance):
1503		self.instances_to_shutdown_on_exit.add(instance)
1504
1505	# Shutdown any background processes or threads
1506	def ShutdownInstances(self):
1507		for x in self.instances_to_shutdown_on_exit:
1508			try:
1509				x.Shutdown()
1510			except:
1511				pass
1512
1513# Database reference
1514
1515class DBRef():
1516
1517	def __init__(self, is_sqlite3, dbname):
1518		self.is_sqlite3 = is_sqlite3
1519		self.dbname = dbname
1520
1521	def Open(self, connection_name):
1522		dbname = self.dbname
1523		if self.is_sqlite3:
1524			db = QSqlDatabase.addDatabase("QSQLITE", connection_name)
1525		else:
1526			db = QSqlDatabase.addDatabase("QPSQL", connection_name)
1527			opts = dbname.split()
1528			for opt in opts:
1529				if "=" in opt:
1530					opt = opt.split("=")
1531					if opt[0] == "hostname":
1532						db.setHostName(opt[1])
1533					elif opt[0] == "port":
1534						db.setPort(int(opt[1]))
1535					elif opt[0] == "username":
1536						db.setUserName(opt[1])
1537					elif opt[0] == "password":
1538						db.setPassword(opt[1])
1539					elif opt[0] == "dbname":
1540						dbname = opt[1]
1541				else:
1542					dbname = opt
1543
1544		db.setDatabaseName(dbname)
1545		if not db.open():
1546			raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text())
1547		return db, dbname
1548
1549# Main
1550
1551def Main():
1552	if (len(sys.argv) < 2):
1553		print >> sys.stderr, "Usage is: exported-sql-viewer.py <database name>"
1554		raise Exception("Too few arguments")
1555
1556	dbname = sys.argv[1]
1557
1558	is_sqlite3 = False
1559	try:
1560		f = open(dbname)
1561		if f.read(15) == "SQLite format 3":
1562			is_sqlite3 = True
1563		f.close()
1564	except:
1565		pass
1566
1567	dbref = DBRef(is_sqlite3, dbname)
1568	db, dbname = dbref.Open("main")
1569	glb = Glb(dbref, db, dbname)
1570	app = QApplication(sys.argv)
1571	glb.app = app
1572	mainwindow = MainWindow(glb)
1573	glb.mainwindow = mainwindow
1574	mainwindow.show()
1575	err = app.exec_()
1576	glb.ShutdownInstances()
1577	db.close()
1578	sys.exit(err)
1579
1580if __name__ == "__main__":
1581	Main()
1582