Use Qt MDI (multiple document interface) to support multiple sub-windows. Put the data model in a cache so that each sub-window can share the same data. This allows mutiple views of the call-graph at the same time and paves the way to add more reports. Committer testing: Starts with a "File Reports Windows" main menu, from the "Reports" I can get what was available up to now, the "Context-Sensitivi Call Graph" option.
Showing the changes to tools/perf/scripts/python (1 of 1 file). Show all
Unified · Split
| @@ -47,6 +47,8 @@ · 46 unmodified lines | ||
| 47 | 47 | # functions that it calls |
| 48 | 48 | |
| 49 | 49 | import sys |
| 50 | +import weakref | |
| 51 | +import threading | |
| 50 | 52 | from PySide.QtCore import * |
| 51 | 53 | from PySide.QtGui import * |
| 52 | 54 | from PySide.QtSql import * |
| @@ -138,6 +140,23 @@ class TreeModel(QAbstractItemModel): · 85 unmodified lines | ||
| 138 | 140 | item = index.internalPointer() |
| 139 | 141 | return self.DisplayData(item, index) |
| 140 | 142 | |
| 143 | +# Model cache | |
| 144 | + | |
| 145 | +model_cache = weakref.WeakValueDictionary() | |
| 146 | +model_cache_lock = threading.Lock() | |
| 147 | + | |
| 148 | +def LookupCreateModel(model_name, create_fn): | |
| 149 | + model_cache_lock.acquire() | |
| 150 | + try: | |
| 151 | + model = model_cache[model_name] | |
| 152 | + except: | |
| 153 | + model = None | |
| 154 | + if model is None: | |
| 155 | + model = create_fn() | |
| 156 | + model_cache[model_name] = model | |
| 157 | + model_cache_lock.release() | |
| 158 | + return model | |
| 159 | + | |
| 141 | 160 | # Context-sensitive call graph data model item base |
| 142 | 161 | |
| 143 | 162 | class CallGraphLevelItemBase(object): |
| @@ -289,6 +308,144 @@ class CallGraphModel(TreeModel): · 145 unmodified lines | ||
| 289 | 308 | alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ] |
| 290 | 309 | return alignment[column] |
| 291 | 310 | |
| 311 | +# Context-sensitive call graph window | |
| 312 | + | |
| 313 | +class CallGraphWindow(QMdiSubWindow): | |
| 314 | + | |
| 315 | + def __init__(self, glb, parent=None): | |
| 316 | + super(CallGraphWindow, self).__init__(parent) | |
| 317 | + | |
| 318 | + self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x)) | |
| 319 | + | |
| 320 | + self.view = QTreeView() | |
| 321 | + self.view.setModel(self.model) | |
| 322 | + | |
| 323 | + for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)): | |
| 324 | + self.view.setColumnWidth(c, w) | |
| 325 | + | |
| 326 | + self.setWidget(self.view) | |
| 327 | + | |
| 328 | + AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph") | |
| 329 | + | |
| 330 | +# Action Definition | |
| 331 | + | |
| 332 | +def CreateAction(label, tip, callback, parent=None, shortcut=None): | |
| 333 | + action = QAction(label, parent) | |
| 334 | + if shortcut != None: | |
| 335 | + action.setShortcuts(shortcut) | |
| 336 | + action.setStatusTip(tip) | |
| 337 | + action.triggered.connect(callback) | |
| 338 | + return action | |
| 339 | + | |
| 340 | +# Typical application actions | |
| 341 | + | |
| 342 | +def CreateExitAction(app, parent=None): | |
| 343 | + return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit) | |
| 344 | + | |
| 345 | +# Typical MDI actions | |
| 346 | + | |
| 347 | +def CreateCloseActiveWindowAction(mdi_area): | |
| 348 | + return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area) | |
| 349 | + | |
| 350 | +def CreateCloseAllWindowsAction(mdi_area): | |
| 351 | + return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area) | |
| 352 | + | |
| 353 | +def CreateTileWindowsAction(mdi_area): | |
| 354 | + return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area) | |
| 355 | + | |
| 356 | +def CreateCascadeWindowsAction(mdi_area): | |
| 357 | + return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area) | |
| 358 | + | |
| 359 | +def CreateNextWindowAction(mdi_area): | |
| 360 | + return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild) | |
| 361 | + | |
| 362 | +def CreatePreviousWindowAction(mdi_area): | |
| 363 | + return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild) | |
| 364 | + | |
| 365 | +# Typical MDI window menu | |
| 366 | + | |
| 367 | +class WindowMenu(): | |
| 368 | + | |
| 369 | + def __init__(self, mdi_area, menu): | |
| 370 | + self.mdi_area = mdi_area | |
| 371 | + self.window_menu = menu.addMenu("&Windows") | |
| 372 | + self.close_active_window = CreateCloseActiveWindowAction(mdi_area) | |
| 373 | + self.close_all_windows = CreateCloseAllWindowsAction(mdi_area) | |
| 374 | + self.tile_windows = CreateTileWindowsAction(mdi_area) | |
| 375 | + self.cascade_windows = CreateCascadeWindowsAction(mdi_area) | |
| 376 | + self.next_window = CreateNextWindowAction(mdi_area) | |
| 377 | + self.previous_window = CreatePreviousWindowAction(mdi_area) | |
| 378 | + self.window_menu.aboutToShow.connect(self.Update) | |
| 379 | + | |
| 380 | + def Update(self): | |
| 381 | + self.window_menu.clear() | |
| 382 | + sub_window_count = len(self.mdi_area.subWindowList()) | |
| 383 | + have_sub_windows = sub_window_count != 0 | |
| 384 | + self.close_active_window.setEnabled(have_sub_windows) | |
| 385 | + self.close_all_windows.setEnabled(have_sub_windows) | |
| 386 | + self.tile_windows.setEnabled(have_sub_windows) | |
| 387 | + self.cascade_windows.setEnabled(have_sub_windows) | |
| 388 | + self.next_window.setEnabled(have_sub_windows) | |
| 389 | + self.previous_window.setEnabled(have_sub_windows) | |
| 390 | + self.window_menu.addAction(self.close_active_window) | |
| 391 | + self.window_menu.addAction(self.close_all_windows) | |
| 392 | + self.window_menu.addSeparator() | |
| 393 | + self.window_menu.addAction(self.tile_windows) | |
| 394 | + self.window_menu.addAction(self.cascade_windows) | |
| 395 | + self.window_menu.addSeparator() | |
| 396 | + self.window_menu.addAction(self.next_window) | |
| 397 | + self.window_menu.addAction(self.previous_window) | |
| 398 | + if sub_window_count == 0: | |
| 399 | + return | |
| 400 | + self.window_menu.addSeparator() | |
| 401 | + nr = 1 | |
| 402 | + for sub_window in self.mdi_area.subWindowList(): | |
| 403 | + label = str(nr) + " " + sub_window.name | |
| 404 | + if nr < 10: | |
| 405 | + label = "&" + label | |
| 406 | + action = self.window_menu.addAction(label) | |
| 407 | + action.setCheckable(True) | |
| 408 | + action.setChecked(sub_window == self.mdi_area.activeSubWindow()) | |
| 409 | + action.triggered.connect(lambda x=nr: self.setActiveSubWindow(x)) | |
| 410 | + self.window_menu.addAction(action) | |
| 411 | + nr += 1 | |
| 412 | + | |
| 413 | + def setActiveSubWindow(self, nr): | |
| 414 | + self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1]) | |
| 415 | + | |
| 416 | +# Unique name for sub-windows | |
| 417 | + | |
| 418 | +def NumberedWindowName(name, nr): | |
| 419 | + if nr > 1: | |
| 420 | + name += " <" + str(nr) + ">" | |
| 421 | + return name | |
| 422 | + | |
| 423 | +def UniqueSubWindowName(mdi_area, name): | |
| 424 | + nr = 1 | |
| 425 | + while True: | |
| 426 | + unique_name = NumberedWindowName(name, nr) | |
| 427 | + ok = True | |
| 428 | + for sub_window in mdi_area.subWindowList(): | |
| 429 | + if sub_window.name == unique_name: | |
| 430 | + ok = False | |
| 431 | + break | |
| 432 | + if ok: | |
| 433 | + return unique_name | |
| 434 | + nr += 1 | |
| 435 | + | |
| 436 | +# Add a sub-window | |
| 437 | + | |
| 438 | +def AddSubWindow(mdi_area, sub_window, name): | |
| 439 | + unique_name = UniqueSubWindowName(mdi_area, name) | |
| 440 | + sub_window.setMinimumSize(200, 100) | |
| 441 | + sub_window.resize(800, 600) | |
| 442 | + sub_window.setWindowTitle(unique_name) | |
| 443 | + sub_window.setAttribute(Qt.WA_DeleteOnClose) | |
| 444 | + sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon)) | |
| 445 | + sub_window.name = unique_name | |
| 446 | + mdi_area.addSubWindow(sub_window) | |
| 447 | + sub_window.show() | |
| 448 | + | |
| 292 | 449 | # Main window |
| 293 | 450 | |
| 294 | 451 | class MainWindow(QMainWindow): |
| @@ -298,21 +455,28 @@ class MainWindow(QMainWindow): · 3 unmodified lines | ||
| 298 | 455 | |
| 299 | 456 | self.glb = glb |
| 300 | 457 | |
| 301 | − self.setWindowTitle("Call Graph: " + glb.dbname) | |
| 302 | − self.move(100, 100) | |
| 303 | − self.resize(800, 600) | |
| 458 | + self.setWindowTitle("Exported SQL Viewer: " + glb.dbname) | |
| 304 | 459 | self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon)) |
| 305 | 460 | self.setMinimumSize(200, 100) |
| 306 | 461 | |
| 307 | − self.model = CallGraphModel(glb) | |
| 462 | + self.mdi_area = QMdiArea() | |
| 463 | + self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) | |
| 464 | + self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) | |
| 308 | 465 | |
| 309 | − self.view = QTreeView() | |
| 310 | − self.view.setModel(self.model) | |
| 466 | + self.setCentralWidget(self.mdi_area) | |
| 311 | 467 | |
| 312 | − for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)): | |
| 313 | − self.view.setColumnWidth(c, w) | |
| 468 | + menu = self.menuBar() | |
| 469 | + | |
| 470 | + file_menu = menu.addMenu("&File") | |
| 471 | + file_menu.addAction(CreateExitAction(glb.app, self)) | |
| 472 | + | |
| 473 | + reports_menu = menu.addMenu("&Reports") | |
| 474 | + reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self)) | |
| 475 | + | |
| 476 | + self.window_menu = WindowMenu(self.mdi_area, menu) | |
| 314 | 477 | |
| 315 | − self.setCentralWidget(self.view) | |
| 478 | + def NewCallGraph(self): | |
| 479 | + CallGraphWindow(self.glb, self) | |
| 316 | 480 | |
| 317 | 481 | # Global data |
| 318 | 482 | |