show_object_tree.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. """
  2. Copyright (c) Contributors to the Open 3D Engine Project.
  3. For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. SPDX-License-Identifier: Apache-2.0 OR MIT
  5. """
  6. import azlmbr
  7. from shiboken2 import wrapInstance, getCppPointer
  8. from PySide2 import QtCore, QtWidgets, QtGui
  9. from PySide2.QtCore import QEvent, Qt
  10. from PySide2.QtWidgets import QAction, QDialog, QHeaderView, QLabel, QLineEdit, QPushButton, QSplitter, QTreeWidget, QTreeWidgetItem, QWidget, QAbstractButton
  11. class OverlayWidget(QWidget):
  12. def __init__(self, parent=None):
  13. super(OverlayWidget, self).__init__(parent)
  14. self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.Tool | QtCore.Qt.WindowTransparentForInput | QtCore.Qt.WindowDoesNotAcceptFocus)
  15. self.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
  16. object_name = "OverlayWidget"
  17. self.setObjectName(object_name)
  18. self.setStyleSheet("#{name} {{ background-color: rgba(100, 65, 164, 128); }}".format(name=object_name))
  19. def update_widget(self, hovered_widget):
  20. self.resize(hovered_widget.size())
  21. self.window().move(hovered_widget.mapToGlobal(QtCore.QPoint(0, 0)))
  22. class InspectPopup(QWidget):
  23. def __init__(self, parent=None):
  24. super(InspectPopup, self).__init__(parent)
  25. self.setWindowFlags(Qt.Popup)
  26. object_name = "InspectPopup"
  27. self.setObjectName(object_name)
  28. self.setStyleSheet("#{name} {{ background-color: #6441A4; }}".format(name=object_name))
  29. self.setContentsMargins(10, 10, 10, 10)
  30. layout = QtWidgets.QGridLayout()
  31. self.name_label = QLabel("Name:")
  32. self.name_value = QLabel("")
  33. layout.addWidget(self.name_label, 0, 0)
  34. layout.addWidget(self.name_value, 0, 1)
  35. self.type_label = QLabel("Type:")
  36. self.type_value = QLabel("")
  37. layout.addWidget(self.type_label, 1, 0)
  38. layout.addWidget(self.type_value, 1, 1)
  39. self.geometry_label = QLabel("Geometry:")
  40. self.geometry_value = QLabel("")
  41. layout.addWidget(self.geometry_label, 2, 0)
  42. layout.addWidget(self.geometry_value, 2, 1)
  43. self.setLayout(layout)
  44. def update_widget(self, new_widget):
  45. name = "(None)"
  46. type_str = "(Unknown)"
  47. geometry_str = "(Unknown)"
  48. if new_widget:
  49. type_str = str(type(new_widget))
  50. geometry_rect = new_widget.geometry()
  51. 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())
  52. # Not all of our widgets have their objectName set
  53. if new_widget.objectName():
  54. name = new_widget.objectName()
  55. self.name_value.setText(name)
  56. self.type_value.setText(type_str)
  57. self.geometry_value.setText(geometry_str)
  58. class ObjectTreeDialog(QDialog):
  59. def __init__(self, parent=None, root_object=None):
  60. super(ObjectTreeDialog, self).__init__(parent)
  61. self.setWindowTitle("Object Tree")
  62. layout = QtWidgets.QVBoxLayout()
  63. # Tree widget for displaying our object hierarchy
  64. self.tree_widget = QTreeWidget()
  65. self.tree_widget_columns = [
  66. "TYPE",
  67. "OBJECT NAME",
  68. "TEXT",
  69. "ICONTEXT",
  70. "TITLE",
  71. "WINDOW_TITLE",
  72. "CLASSES",
  73. "POINTER_ADDRESS",
  74. "GEOMETRY"
  75. ]
  76. self.tree_widget.setColumnCount(len(self.tree_widget_columns))
  77. self.tree_widget.setHeaderLabels(self.tree_widget_columns)
  78. # Only show our type and object name columns. The others we only use to store data so that
  79. # we can use the built-in QTreeWidget.findItems to query.
  80. for column_name in self.tree_widget_columns:
  81. if column_name == "TYPE" or column_name == "OBJECT NAME":
  82. continue
  83. column_index = self.tree_widget_columns.index(column_name)
  84. self.tree_widget.setColumnHidden(column_index, True)
  85. header = self.tree_widget.header()
  86. header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
  87. header.setSectionResizeMode(1, QHeaderView.ResizeToContents)
  88. # Populate our object tree widget
  89. # If a root object wasn't specified, then use the Editor main window
  90. if not root_object:
  91. params = azlmbr.qt.QtForPythonRequestBus(azlmbr.bus.Broadcast, "GetQtBootstrapParameters")
  92. editor_id = QtWidgets.QWidget.find(params.mainWindowId)
  93. editor_main_window = wrapInstance(int(getCppPointer(editor_id)[0]), QtWidgets.QMainWindow)
  94. root_object = editor_main_window
  95. self.build_tree(root_object, self.tree_widget)
  96. # Listen for when the tree widget selection changes so we can update
  97. # selected item properties
  98. self.tree_widget.itemSelectionChanged.connect(self.on_tree_widget_selection_changed)
  99. # Split our tree widget with a properties view for showing more information about
  100. # a selected item. We also use a stacked layout for the properties view so that
  101. # when nothing has been selected yet, we can show a message informing the user
  102. # that something needs to be selected.
  103. splitter = QSplitter()
  104. splitter.addWidget(self.tree_widget)
  105. self.widget_properties = QWidget(self)
  106. self.stacked_layout = QtWidgets.QStackedLayout()
  107. self.widget_info = QWidget()
  108. form_layout = QtWidgets.QFormLayout()
  109. self.name_value = QLineEdit("")
  110. self.name_value.setReadOnly(True)
  111. self.type_value = QLabel("")
  112. self.geometry_value = QLabel("")
  113. self.text_value = QLabel("")
  114. self.icon_text_value = QLabel("")
  115. self.title_value = QLabel("")
  116. self.window_title_value = QLabel("")
  117. self.classes_value = QLabel("")
  118. form_layout.addRow("Name:", self.name_value)
  119. form_layout.addRow("Type:", self.type_value)
  120. form_layout.addRow("Geometry:", self.geometry_value)
  121. form_layout.addRow("Text:", self.text_value)
  122. form_layout.addRow("Icon Text:", self.icon_text_value)
  123. form_layout.addRow("Title:", self.title_value)
  124. form_layout.addRow("Window Title:", self.window_title_value)
  125. form_layout.addRow("Classes:", self.classes_value)
  126. self.widget_info.setLayout(form_layout)
  127. self.widget_properties.setLayout(self.stacked_layout)
  128. self.stacked_layout.addWidget(QLabel("Select an object to view its properties"))
  129. self.stacked_layout.addWidget(self.widget_info)
  130. splitter.addWidget(self.widget_properties)
  131. # Give our splitter stretch factor of 1 so it will expand to take more room over
  132. # the footer
  133. layout.addWidget(splitter, 1)
  134. # Create our popup widget for showing information when hovering over widgets
  135. self.hovered_widget = None
  136. self.inspect_mode = False
  137. self.inspect_popup = InspectPopup()
  138. self.inspect_popup.resize(100, 50)
  139. self.inspect_popup.hide()
  140. # Create a widget to highlight the extent of the hovered widget
  141. self.hover_extent_widget = OverlayWidget()
  142. self.hover_extent_widget.hide()
  143. # Add a footer with a button to switch to widget inspect mode
  144. self.footer = QWidget()
  145. footer_layout = QtWidgets.QHBoxLayout()
  146. self.inspect_button = QPushButton("Pick widget to inspect")
  147. self.inspect_button.clicked.connect(self.on_inspect_clicked)
  148. footer_layout.addStretch(1)
  149. footer_layout.addWidget(self.inspect_button)
  150. self.footer.setLayout(footer_layout)
  151. layout.addWidget(self.footer)
  152. self.setLayout(layout)
  153. # Delete ourselves when the dialog is closed, so that we don't stay living in the background
  154. # since we install an event filter on the application
  155. self.setAttribute(Qt.WA_DeleteOnClose, True)
  156. # Listen to events at the application level so we can know when the mouse is moving
  157. app = QtWidgets.QApplication.instance()
  158. app.installEventFilter(self)
  159. def eventFilter(self, obj, event):
  160. # Look for mouse movement events so we can see what widget the mouse is hovered over
  161. event_type = event.type()
  162. if event_type == QEvent.MouseMove:
  163. global_pos = event.globalPos()
  164. # Make our popup follow the mouse, but we need to offset it by 1, 1 otherwise
  165. # the QApplication.widgetAt will always return our popup instead of the Editor
  166. # widget since it is on top
  167. self.inspect_popup.move(global_pos + QtCore.QPoint(1, 1))
  168. # Find out which widget is under our current mouse position
  169. hovered_widget = QtWidgets.QApplication.widgetAt(global_pos)
  170. if self.hovered_widget:
  171. # Bail out, this is the same widget we are already hovered on
  172. if self.hovered_widget is hovered_widget:
  173. return False
  174. # Update our hovered widget and label
  175. self.hovered_widget = hovered_widget
  176. self.update_hovered_widget_popup()
  177. elif event_type == QEvent.KeyRelease:
  178. if event.key() == Qt.Key_Escape:
  179. # Cancel the inspect mode if the Escape key is pressed
  180. # We don't need to actually hide the inspect popup here because
  181. # it will be hidden already by the Escape action
  182. self.inspect_mode = False
  183. elif event_type == QEvent.MouseButtonPress or event_type == QEvent.MouseButtonRelease:
  184. # Trigger inspecting the currently hovered widget when the left mouse button is clicked
  185. # Don't continue processing this event
  186. if self.inspect_mode and event.button() == Qt.LeftButton:
  187. # Only trigger the inspect on the click release, but we want to also eat the press
  188. # event so that the widget we clicked on isn't stuck in a weird state (e.g. thinks its being dragged)
  189. # Also hide the inspect popup since it won't be hidden automatically by the mouse click since we are
  190. # consuming the event
  191. if event_type == event_type == QEvent.MouseButtonRelease:
  192. self.inspect_popup.hide()
  193. self.hover_extent_widget.hide()
  194. self.inspect_widget()
  195. return True
  196. # Pass every event through
  197. return False
  198. def build_tree(self, obj, parent_tree):
  199. if len(obj.children()) == 0:
  200. return
  201. for child in obj.children():
  202. object_type = type(child).__name__
  203. if child.metaObject().className() != object_type:
  204. object_type = f"{child.metaObject().className()} ({object_type})"
  205. object_name = child.objectName()
  206. text = icon_text = title = window_title = geometry_str = classes = "(N/A)"
  207. if isinstance(child, QtGui.QWindow):
  208. title = child.title()
  209. if isinstance(child, QAction):
  210. text = child.text()
  211. icon_text = child.iconText()
  212. if isinstance(child, QWidget):
  213. window_title = child.windowTitle()
  214. if not (child.property("class") == ""):
  215. classes = child.property("class")
  216. if isinstance(child, QAbstractButton):
  217. text = child.text()
  218. # Keep track of the pointer address for this object so we can search for it later
  219. pointer_address = str(int(getCppPointer(child)[0]))
  220. # Some objects might not have a geometry (e.g. actions, generic qobjects)
  221. if hasattr(child, 'geometry'):
  222. geometry_rect = child.geometry()
  223. 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())
  224. child_tree = QTreeWidgetItem([object_type, object_name, text, icon_text, title, window_title, classes, pointer_address, geometry_str])
  225. if isinstance(parent_tree, QTreeWidget):
  226. parent_tree.addTopLevelItem(child_tree)
  227. else:
  228. parent_tree.addChild(child_tree)
  229. self.build_tree(child, child_tree)
  230. def update_hovered_widget_popup(self):
  231. if self.inspect_mode and self.hovered_widget:
  232. if not self.inspect_popup.isVisible():
  233. self.hover_extent_widget.show()
  234. self.inspect_popup.show()
  235. self.hover_extent_widget.update_widget(self.hovered_widget)
  236. self.inspect_popup.update_widget(self.hovered_widget)
  237. else:
  238. self.inspect_popup.hide()
  239. self.hover_extent_widget.hide()
  240. def on_inspect_clicked(self):
  241. self.inspect_mode = True
  242. self.update_hovered_widget_popup()
  243. def on_tree_widget_selection_changed(self):
  244. selected_items = self.tree_widget.selectedItems()
  245. # If nothing is selected, then switch the stacked layout back to 0
  246. # to show the message
  247. if not selected_items:
  248. self.stacked_layout.setCurrentIndex(0)
  249. return
  250. # Update the selected widget properties and switch to the 1 index in
  251. # the stacked layout so that all the rows will be visible
  252. item = selected_items[0]
  253. self.name_value.setText(item.text(self.tree_widget_columns.index("OBJECT NAME")))
  254. self.type_value.setText(item.text(self.tree_widget_columns.index("TYPE")))
  255. self.geometry_value.setText(item.text(self.tree_widget_columns.index("GEOMETRY")))
  256. self.text_value.setText(item.text(self.tree_widget_columns.index("TEXT")))
  257. self.icon_text_value.setText(item.text(self.tree_widget_columns.index("ICONTEXT")))
  258. self.title_value.setText(item.text(self.tree_widget_columns.index("TITLE")))
  259. self.window_title_value.setText(item.text(self.tree_widget_columns.index("WINDOW_TITLE")))
  260. self.classes_value.setText(item.text(self.tree_widget_columns.index("CLASSES")))
  261. self.stacked_layout.setCurrentIndex(1)
  262. def inspect_widget(self):
  263. self.inspect_mode = False
  264. # Find the tree widget item that matches our hovered widget, and then set it as the current item
  265. # so that the tree widget will scroll to it, expand it, and select it
  266. widget_pointer_address = str(int(getCppPointer(self.hovered_widget)[0]))
  267. pointer_address_column = self.tree_widget_columns.index("POINTER_ADDRESS")
  268. items = self.tree_widget.findItems(widget_pointer_address, Qt.MatchFixedString | Qt.MatchRecursive, pointer_address_column)
  269. if items:
  270. item = items[0]
  271. self.tree_widget.clearSelection()
  272. self.tree_widget.setCurrentItem(item)
  273. else:
  274. print("Unable to find widget")
  275. def get_object_tree(parent, obj=None):
  276. """
  277. Returns the parent/child hierarchy for the given obj (QObject)
  278. parent: Parent for the dialog that is created
  279. obj: Root object for the tree to be built.
  280. returns: QTreeWidget object starting with the root element obj.
  281. """
  282. w = ObjectTreeDialog(parent, obj)
  283. w.resize(1000, 500)
  284. return w
  285. if __name__ == "__main__":
  286. # Get our Editor main window
  287. params = azlmbr.qt.QtForPythonRequestBus(azlmbr.bus.Broadcast, "GetQtBootstrapParameters")
  288. editor_id = QtWidgets.QWidget.find(params.mainWindowId)
  289. editor_main_window = wrapInstance(int(getCppPointer(editor_id)[0]), QtWidgets.QMainWindow)
  290. dock_main_window = editor_main_window.findChild(QtWidgets.QMainWindow)
  291. # Show our object tree visualizer
  292. object_tree = get_object_tree(dock_main_window)
  293. object_tree.show()