diff options
| author | kat <kat@vector35.com> | 2024-03-31 19:15:05 -0400 |
|---|---|---|
| committer | kat <kat@vector35.com> | 2024-04-12 14:49:00 -0400 |
| commit | e812c40e8b1f879e2ffbd4053b8be0c3a65886e5 (patch) | |
| tree | 6e17179f08a5669c5fdb6ae22431f8829cd670a5 /python/examples | |
| parent | f49ff0910df22c53cee2240e0922554bc639fda0 (diff) | |
Update the Typelib Explorer API Example to support new Sidebar orientations + Pane usage
Diffstat (limited to 'python/examples')
| -rw-r--r-- | python/examples/typelibexplorer.py | 355 |
1 files changed, 314 insertions, 41 deletions
diff --git a/python/examples/typelibexplorer.py b/python/examples/typelibexplorer.py index 8d1d863b..28899003 100644 --- a/python/examples/typelibexplorer.py +++ b/python/examples/typelibexplorer.py @@ -21,21 +21,45 @@ # This is an example UI plugin which demonstrates how to add sidebar widgets to Binary Ninja. # See .../api/ui/sidebar.h for interface details. -from binaryninjaui import SidebarWidget, SidebarWidgetType, Sidebar, UIActionHandler -from PySide6.QtCore import Qt, QRectF -from PySide6.QtWidgets import QVBoxLayout, QLabel, QComboBox, QTableWidget, QTableWidgetItem, QTextEdit, QApplication, QLineEdit, QHBoxLayout -from PySide6.QtGui import QImage, QPainter, QFont, QColor -from binaryninja import Platform, BinaryView, TypeLibrary, log +from binaryninjaui import SidebarWidget, SidebarWidgetType, Sidebar, UIActionHandler, FilterEdit, FilteredView, \ + FilterTarget, DockableTabWidget, GlobalAreaTabStyle, DockableTabCollection, View, ViewType, ViewFrame, ViewPane +from PySide6.QtCore import Qt, QRectF, QModelIndex +from PySide6.QtWidgets import QVBoxLayout, QLabel, QComboBox, QTableWidget, QTableWidgetItem, QTextEdit, QApplication, \ + QLineEdit, QHBoxLayout, QWidget, QAbstractItemView, QFrame +from PySide6.QtGui import QImage, QPainter, QFont, QColor, QPalette +from binaryninja import Platform, BinaryView, TypeLibrary, log, QualifiedName from typing import Optional from re import search instance_id = 0 -class TypelibTypeTableWidget(QTableWidget): +g_typelib_explorer_viewtype = None + + +class TypelibTypeTableWidget(QTableWidget, FilterTarget): def __init__(self, parent): QTableWidget.__init__(self, parent) + FilterTarget.__init__(self) self.typelib = None + def setFilter(self, filter_text: str): + self.setFilterRegExp(filter_text) + + def scrollToFirstItem(self): + self.scrollToTop() + + def scrollToCurrentItem(self): + self.scrollTo(self.currentIndex()) + + def selectFirstItem(self): + self.setCurrentIndex(self.model().index(0, 0, QModelIndex())) + + def activateFirstItem(self): + self.setCurrentIndex(self.model().index(0, 0, QModelIndex())) + + def closeFilter(self): + self.setFocus(Qt.OtherFocusReason) + def setFilterRegExp(self, pattern): if pattern == "": for i in range(self.rowCount()): @@ -65,16 +89,35 @@ class TypelibTypeTableWidget(QTableWidget): if data is None: self.setItem(i, 2, QTableWidgetItem(str(type))) else: - lines = type.get_lines(data, name) + lines = type.get_lines(data, str(name)) self.setItem(i, 2, QTableWidgetItem("".join([str(l) for l in lines]))) -class TypelibObjectTableWidget(QTableWidget): +class TypelibObjectTableWidget(QTableWidget, FilterTarget): def __init__(self, parent): QTableWidget.__init__(self, parent) + FilterTarget.__init__(self) self.typelib = None self.ordinals = None + def setFilter(self, filterText): + self.setFilterRegExp(filterText) + + def scrollToFirstItem(self): + self.scrollToTop() + + def scrollToCurrentItem(self): + self.scrollTo(self.currentIndex()) + + def selectFirstItem(self): + self.setCurrentIndex(self.model().index(0, 0, QModelIndex())) + + def activateFirstItem(self): + self.setCurrentIndex(self.model().index(0, 0, QModelIndex())) + + def closeFilter(self): + self.setFocus(Qt.OtherFocusReason) + def lookup_ordinal(self, name: str) -> str: assert self.typelib is not None if md := self.typelib.query_metadata("ordinals"): @@ -137,19 +180,33 @@ class TypelibObjectTableWidget(QTableWidget): # Sidebar widgets must derive from SidebarWidget, not QWidget. SidebarWidget is a QWidget but # provides callbacks for sidebar events, and must be created with a title. -class TypelibExplorerWidget(SidebarWidget): +class TypelibExplorerWidget(SidebarWidget, FilterTarget): def __init__(self, name, frame, data): - global instance_id SidebarWidget.__init__(self, name) + FilterTarget.__init__(self) + self.setBackgroundRole(QPalette.ColorRole.Window) + self.setAutoFillBackground(True) + self.setObjectName("TypelibExplorerWidget") + self.actionHandler = UIActionHandler() self.actionHandler.setupActionHandler(self) - layout = QVBoxLayout() + self.previous_typelib_index = -1 self.previous_platform_index = -1 + self.orientation = None self.data = None self.platform = None self.typelib = None + self.setLayout(QVBoxLayout()) + self.layout().setContentsMargins(0, 0, 0, 0) + self.primary_wrapper = QWidget() + self.primary_wrapper.setLayout(QVBoxLayout()) + self.primary_wrapper.setBackgroundRole(QPalette.ColorRole.Window) + self.primary_wrapper.setAutoFillBackground(True) + + self.layout().addWidget(self.primary_wrapper) + # platform selector self.platform_selector = QComboBox(self) self.platform_selector.setEditable(True) @@ -158,69 +215,151 @@ class TypelibExplorerWidget(SidebarWidget): self.platform_selector.addItem(platform.name, platform) if data is not None: self.platform_selector.setCurrentIndex(self.platform_selector.findText(data.platform.name)) - layout.addWidget(self.platform_selector) # typelib selector self.typelib_selector = QComboBox(self) self.typelib_selector.setEditable(True) self.typelib_selector.setInsertPolicy(QComboBox.InsertPolicy.NoInsert) - layout.addWidget(self.typelib_selector) # guid - layout.addWidget(QLabel("GUID", self)) self.guid = QLineEdit(self) self.guid.setReadOnly(True) - layout.addWidget(self.guid) # alternate names - layout.addWidget(QLabel("Alternate Names", self)) self.alternate_names = QTextEdit(self) self.alternate_names.setReadOnly(True) - self.alternate_names.setMaximumHeight(64) - layout.addWidget(self.alternate_names) # title and table of objects + self.object_table = TypelibObjectTableWidget(self) + self.object_table.verticalHeader().hide() + self.object_table.horizontalHeader().setStretchLastSection(True) + self.object_table.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel) self.object_label = QLabel("Objects", self) - self.object_filter = QLineEdit(self) + self.object_filter = FilterEdit(self.object_table) self.object_filter.setPlaceholderText("Filter (regex)...") - object_layout = QHBoxLayout() - object_layout.addWidget(self.object_label) - object_layout.addWidget(self.object_filter) - layout.addLayout(object_layout) + self.object_layout = QHBoxLayout() + self.object_layout.addWidget(self.object_label) + self.object_layout.addWidget(self.object_filter) + self.object_layout_widget = QWidget() + self.object_layout_widget.setLayout(self.object_layout) - self.object_table = TypelibObjectTableWidget(self) - self.object_table.verticalHeader().hide() self.object_table.setSortingEnabled(True) - layout.addWidget(self.object_table) + self.object_filter.textChanged.connect(self.object_table.setFilter) # title and table of types self.table_label = QLabel("Types", self) - self.types_filter = QLineEdit(self) - self.types_filter.setPlaceholderText("Filter (regex)...") - type_layout = QHBoxLayout() - type_layout.addWidget(self.table_label) - type_layout.addWidget(self.types_filter) - layout.addLayout(type_layout) self.type_table = TypelibTypeTableWidget(self) + self.type_table.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel) self.type_table.verticalHeader().hide() self.type_table.setSortingEnabled(True) - layout.addWidget(self.type_table) - self.setLayout(layout) - - # initialize - instance_id += 1 + self.type_table.horizontalHeader().setStretchLastSection(True) + self.types_filter = FilterEdit(self.type_table) + self.types_filter.setPlaceholderText("Filter (regex)...") + self.types_filter.textChanged.connect(self.type_table.setFilter) + self.type_layout = QHBoxLayout() + self.type_layout.addWidget(self.table_label) + self.type_layout.addWidget(self.types_filter) + self.type_layout_widget = QWidget() + self.type_layout_widget.setLayout(self.type_layout) self.typelib_selector.currentIndexChanged.connect(self.on_typelib_selector_changed) self.platform_selector.currentIndexChanged.connect(self.on_platform_selector_changed) self.object_filter.textChanged.connect(self.object_table.setFilterRegExp) - self.types_filter.textChanged.connect(self.type_table.setFilterRegExp) - self.initialize_data(data) + + # Whenever we're in Horizontal mode, we hide the two table-specific filters and show a singular one to the right + # of tabs. + # We'll set the filter target to this class, and manually forward the signals to the appropriate FilterEdit. + # This approach ensures that whenever we transition back to Vertical mode, the FilterEdits maintain their + # respective contents, and allows us to easily swap out contents when tab changes. + self.shared_filter = FilterEdit(self) + self.shared_filter.setPlaceholderText("Filter (regex)...") + self.shared_filter.textChanged.connect(self.setFilter) + shared_filter_container = QWidget() + shared_filter_layout = QHBoxLayout() + shared_filter_layout.setContentsMargins(0, 2, 0, 2) + shared_filter_layout.addWidget(self.shared_filter) + shared_filter_container.setLayout(shared_filter_layout) + + # We *must* hold a reference to the DockableTabCollection as long as this widget is alive + self.tab_collection = DockableTabCollection() + self.horizontal_tabs = DockableTabWidget(self.tab_collection) + self.horizontal_tabs.setCornerWidget(shared_filter_container, Qt.TopRightCorner, True) + # Whenever we swap tabs, we want to pull the filter text from the currently active tab's FilterEdit + self.horizontal_tabs.currentChanged.connect(self.resetSharedFilterForTabIdx) + + self.horizontal_tabs.setTabStyle(GlobalAreaTabStyle()) + + # We're setting up wrapper widgets for the contents of each tab, as we want to set up our tab system + # before we know if we actually want to put the widgets in it. + self.tab_object_container = QWidget() + self.tab_object_layout = QVBoxLayout() + self.tab_object_layout.setContentsMargins(0, 0, 0, 0) + self.tab_object_container.setLayout(self.tab_object_layout) + self.horizontal_tabs.addTab(self.tab_object_container, "Objects") + self.tab_type_container = QWidget() + self.tab_type_layout = QVBoxLayout() + self.tab_type_layout.setContentsMargins(0, 0, 0, 0) + self.tab_type_container.setLayout(self.tab_type_layout) + self.horizontal_tabs.addTab(self.tab_type_container, "Types") + + self.horizontal_tabs.setCanSplit(False) + self.horizontal_tabs.setCanCloseTab(0, False) + self.horizontal_tabs.setCanCloseTab(1, False) + self.horizontal_tabs.setCanCreateNewWindow(False) + + # initialize + global instance_id + instance_id += 1 + self.initialize_data(self.data) + + def resetSharedFilterForTabIdx(self, idx): + if idx == 0: + self.shared_filter.setText(self.object_filter.text()) + else: + self.shared_filter.setText(self.types_filter.text()) + + def setFilter(self, filterText): + if self.horizontal_tabs.currentIndex() == 0: + self.object_filter.setText(filterText) + else: + self.types_filter.setText(filterText) + + def scrollToFirstItem(self): + if self.horizontal_tabs.currentIndex() == 0: + self.object_table.scrollToFirstItem() + else: + self.type_table.scrollToFirstItem() + + def scrollToCurrentItem(self): + if self.horizontal_tabs.currentIndex() == 0: + self.object_table.scrollToCurrentItem() + else: + self.type_table.scrollToCurrentItem() + + def selectFirstItem(self): + if self.horizontal_tabs.currentIndex() == 0: + self.object_table.selectFirstItem() + else: + self.type_table.selectFirstItem() + + def activateFirstItem(self): + if self.horizontal_tabs.currentIndex() == 0: + self.object_table.activateFirstItem() + else: + self.type_table.activateFirstItem() + + def closeFilter(self): + if self.horizontal_tabs.currentIndex() == 0: + self.object_table.closeFilter() + else: + self.type_table.closeFilter() def setDisabled(self, disabled): self.typelib_selector.setDisabled(disabled) self.object_table.setDisabled(disabled) self.type_table.setDisabled(disabled) - def initialize_data(self, data: Optional[BinaryView], platform: Optional[Platform]=None, typelib: Optional[TypeLibrary]=None): + def initialize_data(self, data: Optional[BinaryView], platform: Optional[Platform] = None, + typelib: Optional[TypeLibrary] = None): # first ensure we have valid platform and typelib data_changed = data != self.data platform_changed = platform != self.platform @@ -319,12 +458,133 @@ class TypelibExplorerWidget(SidebarWidget): self.initialize_data(data, data.platform) def notifyViewChanged(self, view_frame): - data = view_frame.getCurrentViewInterface().getData() if view_frame is not None else None + data = None + if view_frame is not None: + data = view_frame.getCurrentViewInterface().getData() self.on_binaryview_changed(data) def contextMenuEvent(self, event): self.m_contextMenuManager.show(self.m_menu, self.actionHandler) + def updateLayout(self): + self.layout().removeWidget(self.primary_wrapper) + + # Deparent all of our top level widgets before deleting the primary_wrapper so that they don't get deleted, + # and we can reparent them to a new wrapper. + self.horizontal_tabs.setParent(None) + self.platform_selector.setParent(None) + self.typelib_selector.setParent(None) + self.guid.setParent(None) + self.alternate_names.setParent(None) + self.object_layout_widget.setParent(None) + self.object_table.setParent(None) + self.type_layout_widget.setParent(None) + self.type_table.setParent(None) + + # Recreate our primary wrapper so we can use a new layout. + self.primary_wrapper.setParent(None) + self.primary_wrapper.deleteLater() + self.primary_wrapper = QWidget() + self.primary_wrapper.setBackgroundRole(QPalette.ColorRole.Window) + self.primary_wrapper.setAutoFillBackground(True) + + if self.orientation == Qt.Orientation.Vertical: + self.alternate_names.setMaximumHeight(64) + layout = QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.platform_selector) + layout.addWidget(self.typelib_selector) + layout.addWidget(QLabel("GUID", self)) + layout.addWidget(self.guid) + layout.addWidget(QLabel("Alternate Names", self)) + layout.addWidget(self.alternate_names) + layout.addWidget(self.object_layout_widget) + layout.addWidget(self.object_table) + layout.addWidget(self.type_layout_widget) + layout.addWidget(self.type_table) + self.primary_wrapper.setLayout(layout) + else: # Horizontal + self.alternate_names.setMaximumHeight(2048) + layout = QHBoxLayout() + left_hand_layout = QVBoxLayout() + left_hand_layout.setContentsMargins(0, 0, 0, 0) + left_hand_layout.addWidget(self.platform_selector) + left_hand_layout.addWidget(self.typelib_selector) + left_hand_layout.addWidget(QLabel("GUID", self)) + left_hand_layout.addWidget(self.guid) + left_hand_layout.addWidget(QLabel("Alternate Names", self)) + left_hand_layout.addWidget(self.alternate_names, 1) + layout.addLayout(left_hand_layout) + right_hand_layout = QVBoxLayout() + right_hand_layout.setContentsMargins(0, 0, 0, 0) + right_hand_layout.addWidget(self.horizontal_tabs) + active_tab = self.horizontal_tabs.currentIndex() + filter_text = self.object_filter.text() if active_tab == 0 else self.types_filter.text() + self.shared_filter.setText(filter_text) + self.tab_object_layout.addWidget(self.object_table) + self.tab_type_layout.addWidget(self.type_table) + layout.addLayout(right_hand_layout) + self.primary_wrapper.setLayout(layout) + + self.layout().addWidget(self.primary_wrapper) + + def setPrimaryOrientation(self, orientation): + if orientation == self.orientation: + return + self.orientation = orientation + self.updateLayout() + + +class TypelibExplorerView(QFrame, View): + def __init__(self, parent, data): + self.data = data + QFrame.__init__(self) + View.__init__(self) + View.setBinaryDataNavigable(self, False) + self.setupView(self) + + self.setParent(parent) + self.setObjectName("TypelibExplorerView") + + self.typelib_explorer_widget = TypelibExplorerWidget("Typelib Explorer", self, data) + self.typelib_explorer_widget.initialize_data(data, data.platform) + self.typelib_explorer_widget.setPrimaryOrientation(Qt.Orientation.Vertical) + self.setLayout(QVBoxLayout()) + self.layout().addWidget(self.typelib_explorer_widget) + + def getData(self): + try: + return self.data + except AttributeError: + return None + + def getCurrentOffset(self): + return 0 + + def navigate(self, offset): + return False + + def resizeEvent(self, event) -> None: + self.typelib_explorer_widget.setPrimaryOrientation(Qt.Orientation.Horizontal if self.width() > self.height() else Qt.Orientation.Vertical) + QFrame.resizeEvent(self, event) + + +class TypelibExplorerViewType(ViewType): + def __init__(self): + ViewType.__init__(self, "Typelib Explorer", "Typelib Explorer") + + def getPriority(self, data, filename): + return 1 + + def create(self, data: 'BinaryView', view_frame: ViewFrame): + return TypelibExplorerView(view_frame, data) + + @classmethod + def init(cls): + global g_typelib_explorer_viewtype + g_typelib_explorer_viewtype = cls() + ViewType.registerViewType(g_typelib_explorer_viewtype) + class TypelibExplorerWidgetType(SidebarWidgetType): def __init__(self): @@ -350,7 +610,20 @@ class TypelibExplorerWidgetType(SidebarWidgetType): # widget is visible and the BinaryView becomes active. return TypelibExplorerWidget("Typelib Explorer", frame, data) + def canUseAsPane(self, split_pane_widget: 'binaryninjaui.SplitPaneWidget', data: 'BinaryView'): + return True + + def createPane(self, split_pane_widget: 'binaryninjaui.SplitPaneWidget', data: 'BinaryView') -> 'binaryninjaui.Pane': + # We've already registered the View Type, so we request that here by name. + _type = "Typelib Explorer:" + data.view_type + frame = ViewFrame(split_pane_widget, split_pane_widget.fileContext(), _type) + if not frame.getCurrentBinaryView(): + del frame + return None + return ViewPane(frame) + # Register the sidebar widget type with Binary Ninja. This will make it appear as an icon in the # sidebar and the `createWidget` method will be called when a widget is required. Sidebar.addSidebarWidgetType(TypelibExplorerWidgetType()) +TypelibExplorerViewType.init() |
