From bc6d571a50c543d44cc8ebc7e1364e76f907f21e Mon Sep 17 00:00:00 2001 From: Jeza Date: Wed, 8 Apr 2026 20:16:01 +0800 Subject: [PATCH 1/4] [Feature] Add real-time settings sync from server to client via CMD_SETTINGS_CHANGED Previously, client-relevant settings (e.g. highlight overlay color) were only transmitted via CMD_ENABLE_INSPECT extra data, which was cleared on disable_inspect(). This caused the highlight color to revert to default when selecting widgets from control tree/hierarchy bar after inspect ended, or when settings were changed without an active inspect session. This commit introduces CMD_SETTINGS_CHANGED (1018) to immediately broadcast client-relevant settings to all connected clients when the user saves settings. Also stores highlight_color as a dedicated field on PyDB that persists across inspect sessions. Co-Authored-By: Claude Opus 4.6 (1M context) --- PyQtInspect/_pqi_bundle/pqi_comm.py | 8 +- PyQtInspect/_pqi_bundle/pqi_comm_constants.py | 3 + PyQtInspect/pqi.py | 10 ++- .../pqi_gui/windows/settings_window.py | 73 +++++++++++++++++++ PyQtInspect/pqi_gui/workers/dispatcher.py | 3 + PyQtInspect/pqi_gui/workers/pqy_worker.py | 4 + PyQtInspect/pqi_server_gui.py | 19 ++++- 7 files changed, 115 insertions(+), 5 deletions(-) diff --git a/PyQtInspect/_pqi_bundle/pqi_comm.py b/PyQtInspect/_pqi_bundle/pqi_comm.py index ea000c5..48352db 100644 --- a/PyQtInspect/_pqi_bundle/pqi_comm.py +++ b/PyQtInspect/_pqi_bundle/pqi_comm.py @@ -40,7 +40,7 @@ ID_TO_MEANING, CMD_EXIT, CMD_WIDGET_INFO, CMD_ENABLE_INSPECT, CMD_DISABLE_INSPECT, CMD_INSPECT_FINISHED, CMD_EXEC_CODE, CMD_EXEC_CODE_ERROR, CMD_EXEC_CODE_RESULT, CMD_SET_WIDGET_HIGHLIGHT, CMD_SELECT_WIDGET, CMD_REQ_WIDGET_INFO, CMD_REQ_CHILDREN_INFO, CMD_CHILDREN_INFO, - CMD_REQ_CONTROL_TREE, CMD_CONTROL_TREE, CMD_REQ_WIDGET_PROPS, CMD_WIDGET_PROPS, + CMD_REQ_CONTROL_TREE, CMD_CONTROL_TREE, CMD_REQ_WIDGET_PROPS, CMD_WIDGET_PROPS, CMD_SETTINGS_CHANGED, # Keys TreeViewResultKeys ) @@ -229,6 +229,9 @@ def process_net_command(self, global_dbg, cmd_id, seq, text): elif cmd_id == CMD_REQ_WIDGET_PROPS: widget_id = int(unquote(text)) global_dbg.notify_widget_props(widget_id) + elif cmd_id == CMD_SETTINGS_CHANGED: + settings = json.loads(unquote(text)) + global_dbg.on_settings_changed(settings) @@ -533,6 +536,9 @@ def make_req_widget_props_message(self, widget_id: int): def make_widget_props_message(self, widget_props: typing.List[typing.Dict]): return NetCommand(CMD_WIDGET_PROPS, 0, self._dump_json(widget_props)) + def make_settings_changed_message(self, settings: dict): + return NetCommand(CMD_SETTINGS_CHANGED, 0, self._dump_json(settings)) + def make_exit_message(self): return NetCommand(CMD_EXIT, 0, '') diff --git a/PyQtInspect/_pqi_bundle/pqi_comm_constants.py b/PyQtInspect/_pqi_bundle/pqi_comm_constants.py index d7eed83..b0b1f7d 100644 --- a/PyQtInspect/_pqi_bundle/pqi_comm_constants.py +++ b/PyQtInspect/_pqi_bundle/pqi_comm_constants.py @@ -33,6 +33,8 @@ # === WIDGET PROPS === CMD_REQ_WIDGET_PROPS = 1016 CMD_WIDGET_PROPS = 1017 +# === SETTINGS SYNC === +CMD_SETTINGS_CHANGED = 1018 ID_TO_MEANING = { '129': 'CMD_EXIT', @@ -54,6 +56,7 @@ '1015': 'CMD_CONTROL_TREE', '1016': 'CMD_REQ_WIDGET_PROPS', '1017': 'CMD_WIDGET_PROPS', + '1018': 'CMD_SETTINGS_CHANGED', } # === Tree Views === diff --git a/PyQtInspect/pqi.py b/PyQtInspect/pqi.py index cb42724..a271d1f 100644 --- a/PyQtInspect/pqi.py +++ b/PyQtInspect/pqi.py @@ -285,6 +285,7 @@ def __init__(self, set_as_global=True): self.inspect_enabled = False self._inspect_extra_data = {} + self._highlight_color = DEFAULT_HIGHLIGHT_COLOR self._selected_widget = None # Mapping from QWidget object's ID to QWidget object @@ -524,6 +525,9 @@ def enable_inspect(self, extra_data: OptionalDict = None): self.inspect_enabled = True self._inspect_extra_data = extra_data + if 'highlight_color' in extra_data: + self._highlight_color = extra_data['highlight_color'] + def disable_inspect(self): self.inspect_enabled = False self._inspect_extra_data = {} @@ -534,7 +538,11 @@ def mock_left_button_down(self) -> bool: @property def highlight_color(self) -> str: - return self._inspect_extra_data.get('highlight_color', DEFAULT_HIGHLIGHT_COLOR) + return self._highlight_color + + def on_settings_changed(self, settings: dict): + if 'highlight_color' in settings: + self._highlight_color = settings['highlight_color'] def notify_inspect_finished(self, widget): self.select_widget(widget) diff --git a/PyQtInspect/pqi_gui/windows/settings_window.py b/PyQtInspect/pqi_gui/windows/settings_window.py index 62c1690..2d8aa82 100644 --- a/PyQtInspect/pqi_gui/windows/settings_window.py +++ b/PyQtInspect/pqi_gui/windows/settings_window.py @@ -216,8 +216,24 @@ def __init__(self, parent): self._mainLayout.addWidget(self._colorWidget) + # Preview area: hover to see the overlay effect + self._previewLabel = QtWidgets.QLabel("Preview:", self) + self._previewLabel.setFixedWidth(100) + + self._previewArea = _HighlightPreviewWidget(self) + self._previewArea.setFixedHeight(80) + + self._previewWidget = QtWidgets.QWidget(self) + self._previewLayout = QtWidgets.QHBoxLayout(self._previewWidget) + self._previewLayout.setSpacing(10) + self._previewLayout.addWidget(self._previewLabel, 0, QtCore.Qt.AlignTop) + self._previewLayout.addWidget(self._previewArea) + + self._mainLayout.addWidget(self._previewWidget) + self._color = self._DEFAULT_COLOR self._updateButtonPreview() + self._updateOverlayPreview() @property def _qcolor(self) -> QtGui.QColor: @@ -234,6 +250,9 @@ def _updateButtonPreview(self): border: 1px solid #888; }}""") + def _updateOverlayPreview(self): + self._previewArea.setOverlayColor(self._qcolor) + def _selectColor(self): initial = self._qcolor color = QtWidgets.QColorDialog.getColor( @@ -243,6 +262,7 @@ def _selectColor(self): if color.isValid(): self._color = f"{color.red()},{color.green()},{color.blue()},{color.alpha()}" self._updateButtonPreview() + self._updateOverlayPreview() def getColor(self) -> str: return self._color @@ -259,9 +279,61 @@ def setColor(self, colorStr: str): pqi_log.warning(f"Invalid color string: {colorStr}. Resetting to default. Error: {e}") self._color = self._DEFAULT_COLOR self._updateButtonPreview() + self._updateOverlayPreview() + + +class _HighlightPreviewWidget(QtWidgets.QWidget): + """ A preview widget that shows a colored overlay when the mouse hovers over it. """ + + def __init__(self, parent=None): + super().__init__(parent) + self._highlightColor = QtGui.QColor(255, 0, 0, 51) + + self.setStyleSheet("_HighlightPreviewWidget { border: 1px solid #aaa; }") + self.setMouseTracking(True) + + contentLayout = QtWidgets.QHBoxLayout(self) + contentLayout.setContentsMargins(20, 20, 20, 20) + + self._sampleLabel = QtWidgets.QLabel("Hover here to preview the highlight overlay effect", self) + self._sampleLabel.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) + contentLayout.addWidget(self._sampleLabel) + + # Highlight overlay (hidden by default, shown on hover) + self._highlightOverlay = QtWidgets.QWidget(self) + self._highlightOverlay.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) + self._highlightOverlay.hide() + self._updateHighlightStylesheet() + + def setOverlayColor(self, color: QtGui.QColor): + self._highlightColor = color + self._updateHighlightStylesheet() + + def _updateHighlightStylesheet(self): + c = self._highlightColor + self._highlightOverlay.setStyleSheet( + f"background: transparent; background-color: rgba({c.red()},{c.green()},{c.blue()},{c.alpha()});" + ) + + def enterEvent(self, event): + self._highlightOverlay.setGeometry(self.rect()) + self._highlightOverlay.show() + self._highlightOverlay.raise_() + super().enterEvent(event) + + def leaveEvent(self, event): + self._highlightOverlay.hide() + super().leaveEvent(event) + + def resizeEvent(self, event): + if self._highlightOverlay.isVisible(): + self._highlightOverlay.setGeometry(self.rect()) + super().resizeEvent(event) class SettingWindow(QtWidgets.QDialog): + sigSettingsSaved = QtCore.pyqtSignal() + def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Settings") @@ -350,6 +422,7 @@ def saveSettings(self): pqi_log.info(f"Settings saved: IDE Type={ideType}, IDE Path={idePath}, Parameters={ideParameters}," f" Highlight Color={highlightColor}") + self.sigSettingsSaved.emit() self.close() def showEvent(self, ev): diff --git a/PyQtInspect/pqi_gui/workers/dispatcher.py b/PyQtInspect/pqi_gui/workers/dispatcher.py index 8ee0fdf..4841901 100644 --- a/PyQtInspect/pqi_gui/workers/dispatcher.py +++ b/PyQtInspect/pqi_gui/workers/dispatcher.py @@ -107,5 +107,8 @@ def sendRequestControlTreeInfoEvent(self, extra: OptionalDict = None): def sendRequestWidgetPropsEvent(self, widgetId: int): self.writer.add_command(self.net_command_factory.make_req_widget_props_message(widgetId)) + def sendSettingsChanged(self, settings: dict): + self.writer.add_command(self.net_command_factory.make_settings_changed_message(settings)) + def notifyDelete(self): self.close() diff --git a/PyQtInspect/pqi_gui/workers/pqy_worker.py b/PyQtInspect/pqi_gui/workers/pqy_worker.py index 5679ef6..d26c57b 100644 --- a/PyQtInspect/pqi_gui/workers/pqy_worker.py +++ b/PyQtInspect/pqi_gui/workers/pqy_worker.py @@ -99,6 +99,10 @@ def sendDisableInspect(self): for dispatcher in self.dispatchers: dispatcher.sendDisableInspect() + def sendSettingsChanged(self, settings: dict): + for dispatcher in self.dispatchers: + dispatcher.sendSettingsChanged(settings) + def sendExecCodeEvent(self, dispatcherId: int, code: str): dispatcher = self.idToDispatcher.get(dispatcherId) if dispatcher: diff --git a/PyQtInspect/pqi_server_gui.py b/PyQtInspect/pqi_server_gui.py index 10a7fbd..e7b8792 100644 --- a/PyQtInspect/pqi_server_gui.py +++ b/PyQtInspect/pqi_server_gui.py @@ -587,10 +587,16 @@ def _selectWidget(self, widgetId: int): # | # * Close the server / Stop Serving -> Disable Inspect # ---------------------------------------------------------------------------------------- + def _buildClientSettings(self) -> dict: + """ Build a dict of settings that need to be synced to the client. """ + return { + 'highlight_color': SettingsController.instance().highlightColor, + } + def _buildInspectExtraData(self) -> dict: return { 'mock_left_button_down': self._isMockLeftButtonDownAction.isChecked(), - 'highlight_color': SettingsController.instance().highlightColor, + **self._buildClientSettings(), } def _beginInspect(self): @@ -631,8 +637,14 @@ def _disableInspect(self): def _openSettingWindow(self): if self._settingWindow is None: self._settingWindow = SettingWindow(self) + self._settingWindow.sigSettingsSaved.connect(self._onSettingsSaved) self._settingWindow.show() + def _onSettingsSaved(self): + worker = self._getWorker() + if worker: + worker.sendSettingsChanged(self._buildClientSettings()) + # endregion # region -- code exec -- @@ -805,16 +817,17 @@ def _openLogDir(self): def _clearLogs(self): """ Clear the logs in the console and file. """ + _title = "Clear Logs" reply = QtWidgets.QMessageBox.question( self, - self._getWindowTitle(), + _title, "Are you sure you want to delete all logs?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No ) if reply == QtWidgets.QMessageBox.Yes: pqi_log.clear_logs() - QtWidgets.QMessageBox.information(self, self._getWindowTitle(), + QtWidgets.QMessageBox.information(self, _title, "All logs have been deleted.") # endregion From 6ca2e2b73ddd2de994e303acf2346c2245af67b8 Mon Sep 17 00:00:00 2001 From: Jeza Date: Wed, 8 Apr 2026 20:27:18 +0800 Subject: [PATCH 2/4] [Fix] Address PR review: fix preview selector, add logging, sync settings on connect - Use #objectName selector for preview widget border to avoid unreliable Python class name matching - Add pqi_log entries for server-side settings broadcast and client-side settings application - Send client settings to newly connected clients on CMD_QT_PATCH_SUCCESS so they start with the correct highlight color before any inspect/save Co-Authored-By: Claude Opus 4.6 (1M context) --- PyQtInspect/pqi.py | 1 + PyQtInspect/pqi_gui/windows/settings_window.py | 3 ++- PyQtInspect/pqi_gui/workers/pqy_worker.py | 5 +++++ PyQtInspect/pqi_server_gui.py | 7 ++++++- 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/PyQtInspect/pqi.py b/PyQtInspect/pqi.py index a271d1f..bc3f32a 100644 --- a/PyQtInspect/pqi.py +++ b/PyQtInspect/pqi.py @@ -541,6 +541,7 @@ def highlight_color(self) -> str: return self._highlight_color def on_settings_changed(self, settings: dict): + pqi_log.info(f"Settings changed from server: {settings}") if 'highlight_color' in settings: self._highlight_color = settings['highlight_color'] diff --git a/PyQtInspect/pqi_gui/windows/settings_window.py b/PyQtInspect/pqi_gui/windows/settings_window.py index 2d8aa82..8b96579 100644 --- a/PyQtInspect/pqi_gui/windows/settings_window.py +++ b/PyQtInspect/pqi_gui/windows/settings_window.py @@ -289,7 +289,8 @@ def __init__(self, parent=None): super().__init__(parent) self._highlightColor = QtGui.QColor(255, 0, 0, 51) - self.setStyleSheet("_HighlightPreviewWidget { border: 1px solid #aaa; }") + self.setObjectName("highlightPreview") + self.setStyleSheet("#highlightPreview { border: 1px solid #aaa; }") self.setMouseTracking(True) contentLayout = QtWidgets.QHBoxLayout(self) diff --git a/PyQtInspect/pqi_gui/workers/pqy_worker.py b/PyQtInspect/pqi_gui/workers/pqy_worker.py index d26c57b..f60b4ff 100644 --- a/PyQtInspect/pqi_gui/workers/pqy_worker.py +++ b/PyQtInspect/pqi_gui/workers/pqy_worker.py @@ -103,6 +103,11 @@ def sendSettingsChanged(self, settings: dict): for dispatcher in self.dispatchers: dispatcher.sendSettingsChanged(settings) + def sendSettingsChangedToDispatcher(self, dispatcherId: int, settings: dict): + dispatcher = self.idToDispatcher.get(dispatcherId) + if dispatcher: + dispatcher.sendSettingsChanged(settings) + def sendExecCodeEvent(self, dispatcherId: int, code: str): dispatcher = self.idToDispatcher.get(dispatcherId) if dispatcher: diff --git a/PyQtInspect/pqi_server_gui.py b/PyQtInspect/pqi_server_gui.py index e7b8792..605cbb8 100644 --- a/PyQtInspect/pqi_server_gui.py +++ b/PyQtInspect/pqi_server_gui.py @@ -462,6 +462,9 @@ def onWidgetInfoRecv(self, dispatcherId: int, info: dict): pid = int(text) pqi_log.info(f"Qt patched successfully, pid: {pid}") + # Sync client-relevant settings to the newly connected client. + self._getWorker().sendSettingsChangedToDispatcher(dispatcherId, self._buildClientSettings()) + # If inspection is enabled, enable it for the new process. if self._selectButton.isChecked(): self._getWorker().sendEnableInspectToDispatcher( @@ -643,7 +646,9 @@ def _openSettingWindow(self): def _onSettingsSaved(self): worker = self._getWorker() if worker: - worker.sendSettingsChanged(self._buildClientSettings()) + settings = self._buildClientSettings() + pqi_log.info(f"Broadcasting settings change to all clients: {settings}") + worker.sendSettingsChanged(settings) # endregion From 9ec799229b8daf4e86a495c9be9d0b9af04970ac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 12:37:50 +0000 Subject: [PATCH 3/4] Fix stylesheet selector: use direct border style instead of class/objectName selector in _HighlightPreviewWidget Agent-Logs-Url: https://github.com/JezaChen/PyQtInspect-Open/sessions/ab36e99d-e962-4cf1-8c96-591e997106fc Co-authored-by: JezaChen <31545780+JezaChen@users.noreply.github.com> --- PyQtInspect/pqi_gui/windows/settings_window.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/PyQtInspect/pqi_gui/windows/settings_window.py b/PyQtInspect/pqi_gui/windows/settings_window.py index 8b96579..7c9adf9 100644 --- a/PyQtInspect/pqi_gui/windows/settings_window.py +++ b/PyQtInspect/pqi_gui/windows/settings_window.py @@ -289,8 +289,7 @@ def __init__(self, parent=None): super().__init__(parent) self._highlightColor = QtGui.QColor(255, 0, 0, 51) - self.setObjectName("highlightPreview") - self.setStyleSheet("#highlightPreview { border: 1px solid #aaa; }") + self.setStyleSheet("border: 1px solid #aaa;") self.setMouseTracking(True) contentLayout = QtWidgets.QHBoxLayout(self) From 82afb5f72c026ade7beb346add4bb52556143c9a Mon Sep 17 00:00:00 2001 From: Jeza Date: Wed, 8 Apr 2026 21:23:18 +0800 Subject: [PATCH 4/4] [Enhancement] Improve highlight preview widget styling - Add WA_StyledBackground comment explaining why it's needed - Center-align the preview label text - Add border-radius to preview widget border Co-Authored-By: Claude Opus 4.6 (1M context) --- PyQtInspect/pqi_gui/windows/settings_window.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/PyQtInspect/pqi_gui/windows/settings_window.py b/PyQtInspect/pqi_gui/windows/settings_window.py index 7c9adf9..4e383e0 100644 --- a/PyQtInspect/pqi_gui/windows/settings_window.py +++ b/PyQtInspect/pqi_gui/windows/settings_window.py @@ -289,13 +289,17 @@ def __init__(self, parent=None): super().__init__(parent) self._highlightColor = QtGui.QColor(255, 0, 0, 51) - self.setStyleSheet("border: 1px solid #aaa;") + # Enable stylesheet-driven painting (border, background) for plain QWidget subclass. + self.setAttribute(QtCore.Qt.WA_StyledBackground, True) + self.setObjectName("highlightPreview") + self.setStyleSheet("#highlightPreview { border: 1px solid #aaa; border-radius: 4px; }") self.setMouseTracking(True) contentLayout = QtWidgets.QHBoxLayout(self) contentLayout.setContentsMargins(20, 20, 20, 20) self._sampleLabel = QtWidgets.QLabel("Hover here to preview the highlight overlay effect", self) + self._sampleLabel.setAlignment(QtCore.Qt.AlignCenter) self._sampleLabel.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) contentLayout.addWidget(self._sampleLabel)