123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348 |
- """
- Copyright (c) Contributors to the Open 3D Engine Project.
- For complete copyright and license terms please see the LICENSE at the root of this distribution.
- SPDX-License-Identifier: Apache-2.0 OR MIT
- """
- import azlmbr
- from shiboken2 import wrapInstance, getCppPointer
- from PySide2 import QtCore, QtWidgets, QtGui
- from PySide2.QtCore import QEvent, Qt
- from PySide2.QtWidgets import QAction, QDialog, QHeaderView, QLabel, QLineEdit, QPushButton, QSplitter, QTreeWidget, QTreeWidgetItem, QWidget, QAbstractButton
- class OverlayWidget(QWidget):
- def __init__(self, parent=None):
- super(OverlayWidget, self).__init__(parent)
-
- self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.Tool | QtCore.Qt.WindowTransparentForInput | QtCore.Qt.WindowDoesNotAcceptFocus)
- self.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
-
- object_name = "OverlayWidget"
- self.setObjectName(object_name)
- self.setStyleSheet("#{name} {{ background-color: rgba(100, 65, 164, 128); }}".format(name=object_name))
-
- def update_widget(self, hovered_widget):
- self.resize(hovered_widget.size())
- self.window().move(hovered_widget.mapToGlobal(QtCore.QPoint(0, 0)))
- class InspectPopup(QWidget):
- def __init__(self, parent=None):
- super(InspectPopup, self).__init__(parent)
- self.setWindowFlags(Qt.Popup)
- object_name = "InspectPopup"
- self.setObjectName(object_name)
- self.setStyleSheet("#{name} {{ background-color: #6441A4; }}".format(name=object_name))
- self.setContentsMargins(10, 10, 10, 10)
- layout = QtWidgets.QGridLayout()
- self.name_label = QLabel("Name:")
- self.name_value = QLabel("")
- layout.addWidget(self.name_label, 0, 0)
- layout.addWidget(self.name_value, 0, 1)
- self.type_label = QLabel("Type:")
- self.type_value = QLabel("")
- layout.addWidget(self.type_label, 1, 0)
- layout.addWidget(self.type_value, 1, 1)
- self.geometry_label = QLabel("Geometry:")
- self.geometry_value = QLabel("")
- layout.addWidget(self.geometry_label, 2, 0)
- layout.addWidget(self.geometry_value, 2, 1)
- self.setLayout(layout)
- def update_widget(self, new_widget):
- name = "(None)"
- type_str = "(Unknown)"
- geometry_str = "(Unknown)"
- if new_widget:
- type_str = str(type(new_widget))
- geometry_rect = new_widget.geometry()
- geometry_str = "x: {x}, y: {y}, width: {width}, height: {height}".format(x=geometry_rect.x(), y=geometry_rect.y(), width=geometry_rect.width(), height=geometry_rect.height())
- # Not all of our widgets have their objectName set
- if new_widget.objectName():
- name = new_widget.objectName()
- self.name_value.setText(name)
- self.type_value.setText(type_str)
- self.geometry_value.setText(geometry_str)
- class ObjectTreeDialog(QDialog):
- def __init__(self, parent=None, root_object=None):
- super(ObjectTreeDialog, self).__init__(parent)
- self.setWindowTitle("Object Tree")
- layout = QtWidgets.QVBoxLayout()
- # Tree widget for displaying our object hierarchy
- self.tree_widget = QTreeWidget()
- self.tree_widget_columns = [
- "TYPE",
- "OBJECT NAME",
- "TEXT",
- "ICONTEXT",
- "TITLE",
- "WINDOW_TITLE",
- "CLASSES",
- "POINTER_ADDRESS",
- "GEOMETRY"
- ]
- self.tree_widget.setColumnCount(len(self.tree_widget_columns))
- self.tree_widget.setHeaderLabels(self.tree_widget_columns)
- # Only show our type and object name columns. The others we only use to store data so that
- # we can use the built-in QTreeWidget.findItems to query.
- for column_name in self.tree_widget_columns:
- if column_name == "TYPE" or column_name == "OBJECT NAME":
- continue
-
- column_index = self.tree_widget_columns.index(column_name)
- self.tree_widget.setColumnHidden(column_index, True)
- header = self.tree_widget.header()
- header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
- header.setSectionResizeMode(1, QHeaderView.ResizeToContents)
- # Populate our object tree widget
- # If a root object wasn't specified, then use the Editor main window
- if not root_object:
- params = azlmbr.qt.QtForPythonRequestBus(azlmbr.bus.Broadcast, "GetQtBootstrapParameters")
- editor_id = QtWidgets.QWidget.find(params.mainWindowId)
- editor_main_window = wrapInstance(int(getCppPointer(editor_id)[0]), QtWidgets.QMainWindow)
- root_object = editor_main_window
- self.build_tree(root_object, self.tree_widget)
- # Listen for when the tree widget selection changes so we can update
- # selected item properties
- self.tree_widget.itemSelectionChanged.connect(self.on_tree_widget_selection_changed)
- # Split our tree widget with a properties view for showing more information about
- # a selected item. We also use a stacked layout for the properties view so that
- # when nothing has been selected yet, we can show a message informing the user
- # that something needs to be selected.
- splitter = QSplitter()
- splitter.addWidget(self.tree_widget)
- self.widget_properties = QWidget(self)
- self.stacked_layout = QtWidgets.QStackedLayout()
- self.widget_info = QWidget()
- form_layout = QtWidgets.QFormLayout()
- self.name_value = QLineEdit("")
- self.name_value.setReadOnly(True)
- self.type_value = QLabel("")
- self.geometry_value = QLabel("")
- self.text_value = QLabel("")
- self.icon_text_value = QLabel("")
- self.title_value = QLabel("")
- self.window_title_value = QLabel("")
- self.classes_value = QLabel("")
- form_layout.addRow("Name:", self.name_value)
- form_layout.addRow("Type:", self.type_value)
- form_layout.addRow("Geometry:", self.geometry_value)
- form_layout.addRow("Text:", self.text_value)
- form_layout.addRow("Icon Text:", self.icon_text_value)
- form_layout.addRow("Title:", self.title_value)
- form_layout.addRow("Window Title:", self.window_title_value)
- form_layout.addRow("Classes:", self.classes_value)
- self.widget_info.setLayout(form_layout)
- self.widget_properties.setLayout(self.stacked_layout)
- self.stacked_layout.addWidget(QLabel("Select an object to view its properties"))
- self.stacked_layout.addWidget(self.widget_info)
- splitter.addWidget(self.widget_properties)
- # Give our splitter stretch factor of 1 so it will expand to take more room over
- # the footer
- layout.addWidget(splitter, 1)
- # Create our popup widget for showing information when hovering over widgets
- self.hovered_widget = None
- self.inspect_mode = False
- self.inspect_popup = InspectPopup()
- self.inspect_popup.resize(100, 50)
- self.inspect_popup.hide()
-
- # Create a widget to highlight the extent of the hovered widget
- self.hover_extent_widget = OverlayWidget()
- self.hover_extent_widget.hide()
- # Add a footer with a button to switch to widget inspect mode
- self.footer = QWidget()
- footer_layout = QtWidgets.QHBoxLayout()
- self.inspect_button = QPushButton("Pick widget to inspect")
- self.inspect_button.clicked.connect(self.on_inspect_clicked)
- footer_layout.addStretch(1)
- footer_layout.addWidget(self.inspect_button)
- self.footer.setLayout(footer_layout)
- layout.addWidget(self.footer)
- self.setLayout(layout)
- # Delete ourselves when the dialog is closed, so that we don't stay living in the background
- # since we install an event filter on the application
- self.setAttribute(Qt.WA_DeleteOnClose, True)
- # Listen to events at the application level so we can know when the mouse is moving
- app = QtWidgets.QApplication.instance()
- app.installEventFilter(self)
- def eventFilter(self, obj, event):
- # Look for mouse movement events so we can see what widget the mouse is hovered over
- event_type = event.type()
- if event_type == QEvent.MouseMove:
- global_pos = event.globalPos()
- # Make our popup follow the mouse, but we need to offset it by 1, 1 otherwise
- # the QApplication.widgetAt will always return our popup instead of the Editor
- # widget since it is on top
- self.inspect_popup.move(global_pos + QtCore.QPoint(1, 1))
- # Find out which widget is under our current mouse position
- hovered_widget = QtWidgets.QApplication.widgetAt(global_pos)
- if self.hovered_widget:
- # Bail out, this is the same widget we are already hovered on
- if self.hovered_widget is hovered_widget:
- return False
- # Update our hovered widget and label
- self.hovered_widget = hovered_widget
- self.update_hovered_widget_popup()
- elif event_type == QEvent.KeyRelease:
- if event.key() == Qt.Key_Escape:
- # Cancel the inspect mode if the Escape key is pressed
- # We don't need to actually hide the inspect popup here because
- # it will be hidden already by the Escape action
- self.inspect_mode = False
- elif event_type == QEvent.MouseButtonPress or event_type == QEvent.MouseButtonRelease:
- # Trigger inspecting the currently hovered widget when the left mouse button is clicked
- # Don't continue processing this event
- if self.inspect_mode and event.button() == Qt.LeftButton:
- # Only trigger the inspect on the click release, but we want to also eat the press
- # event so that the widget we clicked on isn't stuck in a weird state (e.g. thinks its being dragged)
- # Also hide the inspect popup since it won't be hidden automatically by the mouse click since we are
- # consuming the event
- if event_type == event_type == QEvent.MouseButtonRelease:
- self.inspect_popup.hide()
- self.hover_extent_widget.hide()
- self.inspect_widget()
- return True
- # Pass every event through
- return False
- def build_tree(self, obj, parent_tree):
- if len(obj.children()) == 0:
- return
- for child in obj.children():
- object_type = type(child).__name__
- if child.metaObject().className() != object_type:
- object_type = f"{child.metaObject().className()} ({object_type})"
- object_name = child.objectName()
- text = icon_text = title = window_title = geometry_str = classes = "(N/A)"
- if isinstance(child, QtGui.QWindow):
- title = child.title()
- if isinstance(child, QAction):
- text = child.text()
- icon_text = child.iconText()
- if isinstance(child, QWidget):
- window_title = child.windowTitle()
- if not (child.property("class") == ""):
- classes = child.property("class")
- if isinstance(child, QAbstractButton):
- text = child.text()
-
- # Keep track of the pointer address for this object so we can search for it later
- pointer_address = str(int(getCppPointer(child)[0]))
- # Some objects might not have a geometry (e.g. actions, generic qobjects)
- if hasattr(child, 'geometry'):
- geometry_rect = child.geometry()
- geometry_str = "x: {x}, y: {y}, width: {width}, height: {height}".format(x=geometry_rect.x(), y=geometry_rect.y(), width=geometry_rect.width(), height=geometry_rect.height())
- child_tree = QTreeWidgetItem([object_type, object_name, text, icon_text, title, window_title, classes, pointer_address, geometry_str])
- if isinstance(parent_tree, QTreeWidget):
- parent_tree.addTopLevelItem(child_tree)
- else:
- parent_tree.addChild(child_tree)
- self.build_tree(child, child_tree)
- def update_hovered_widget_popup(self):
- if self.inspect_mode and self.hovered_widget:
- if not self.inspect_popup.isVisible():
- self.hover_extent_widget.show()
- self.inspect_popup.show()
- self.hover_extent_widget.update_widget(self.hovered_widget)
- self.inspect_popup.update_widget(self.hovered_widget)
- else:
- self.inspect_popup.hide()
- self.hover_extent_widget.hide()
- def on_inspect_clicked(self):
- self.inspect_mode = True
- self.update_hovered_widget_popup()
- def on_tree_widget_selection_changed(self):
- selected_items = self.tree_widget.selectedItems()
- # If nothing is selected, then switch the stacked layout back to 0
- # to show the message
- if not selected_items:
- self.stacked_layout.setCurrentIndex(0)
- return
- # Update the selected widget properties and switch to the 1 index in
- # the stacked layout so that all the rows will be visible
- item = selected_items[0]
- self.name_value.setText(item.text(self.tree_widget_columns.index("OBJECT NAME")))
- self.type_value.setText(item.text(self.tree_widget_columns.index("TYPE")))
- self.geometry_value.setText(item.text(self.tree_widget_columns.index("GEOMETRY")))
- self.text_value.setText(item.text(self.tree_widget_columns.index("TEXT")))
- self.icon_text_value.setText(item.text(self.tree_widget_columns.index("ICONTEXT")))
- self.title_value.setText(item.text(self.tree_widget_columns.index("TITLE")))
- self.window_title_value.setText(item.text(self.tree_widget_columns.index("WINDOW_TITLE")))
- self.classes_value.setText(item.text(self.tree_widget_columns.index("CLASSES")))
- self.stacked_layout.setCurrentIndex(1)
- def inspect_widget(self):
- self.inspect_mode = False
- # Find the tree widget item that matches our hovered widget, and then set it as the current item
- # so that the tree widget will scroll to it, expand it, and select it
- widget_pointer_address = str(int(getCppPointer(self.hovered_widget)[0]))
- pointer_address_column = self.tree_widget_columns.index("POINTER_ADDRESS")
- items = self.tree_widget.findItems(widget_pointer_address, Qt.MatchFixedString | Qt.MatchRecursive, pointer_address_column)
- if items:
- item = items[0]
- self.tree_widget.clearSelection()
- self.tree_widget.setCurrentItem(item)
- else:
- print("Unable to find widget")
- def get_object_tree(parent, obj=None):
- """
- Returns the parent/child hierarchy for the given obj (QObject)
- parent: Parent for the dialog that is created
- obj: Root object for the tree to be built.
- returns: QTreeWidget object starting with the root element obj.
- """
- w = ObjectTreeDialog(parent, obj)
- w.resize(1000, 500)
- return w
-
- if __name__ == "__main__":
- # Get our Editor main window
- params = azlmbr.qt.QtForPythonRequestBus(azlmbr.bus.Broadcast, "GetQtBootstrapParameters")
- editor_id = QtWidgets.QWidget.find(params.mainWindowId)
- editor_main_window = wrapInstance(int(getCppPointer(editor_id)[0]), QtWidgets.QMainWindow)
- dock_main_window = editor_main_window.findChild(QtWidgets.QMainWindow)
- # Show our object tree visualizer
- object_tree = get_object_tree(dock_main_window)
- object_tree.show()
|