diff --git a/bec_widgets/widgets/editors/bec_console/bec_console.py b/bec_widgets/widgets/editors/bec_console/bec_console.py index 59fa8def..77102394 100644 --- a/bec_widgets/widgets/editors/bec_console/bec_console.py +++ b/bec_widgets/widgets/editors/bec_console/bec_console.py @@ -50,6 +50,7 @@ class _TerminalOwnerInfo: terminal_id: str = "" initialized: bool = False persist_session: bool = False + zoom_level: int = 0 fallback_holder: QWidget | None = None @@ -75,20 +76,32 @@ class BecConsoleRegistry: return app.aboutToQuit.connect(self.clear, Qt.ConnectionType.UniqueConnection) + @staticmethod + def _apply_zoom_level(term: BecTerminal, zoom_level: int) -> None: + if zoom_level > 0: + for _ in range(zoom_level): + term.zoom_in() + elif zoom_level < 0: + for _ in range(-zoom_level): + term.zoom_out() + @staticmethod def _new_terminal_info(console: BecConsole) -> _TerminalOwnerInfo: term = _BecTermClass() + BecConsoleRegistry._apply_zoom_level(term, console.default_zoom_level) return _TerminalOwnerInfo( registered_console_ids={console.console_id}, owner_console_id=console.console_id, instance=term, terminal_id=console.terminal_id, persist_session=console.persist_terminal_session, + zoom_level=console.default_zoom_level, ) @staticmethod def _replace_terminal(info: _TerminalOwnerInfo, console: BecConsole) -> None: info.instance = _BecTermClass() + BecConsoleRegistry._apply_zoom_level(info.instance, info.zoom_level) info.initialized = False info.owner_console_id = console.console_id info.registered_console_ids.add(console.console_id) @@ -334,6 +347,27 @@ class BecConsoleRegistry: return None return info.instance + def change_zoom(self, term_id: str, delta: int) -> int | None: + """Apply a relative zoom change to the tracked terminal and return the resulting level.""" + info = self._terminal_registry.get(term_id) + if info is None or not self._is_valid_qobject(info.instance) or delta == 0: + return None + + if delta > 0: + for _ in range(delta): + info.instance.zoom_in() + else: + for _ in range(-delta): + info.instance.zoom_out() + info.zoom_level += delta + return info.zoom_level + + def zoom_level(self, term_id: str) -> int: + info = self._terminal_registry.get(term_id) + if info is None: + return 0 + return info.zoom_level + def owner_is_visible(self, term_id: str) -> bool: """ Check if the owner of an instance is currently visible. @@ -381,6 +415,7 @@ class BecConsole(BECWidget, QWidget): PLUGIN = True ICON_NAME = "terminal" persist_terminal_session = False + default_zoom_level = 1 def __init__( self, @@ -497,6 +532,18 @@ class BecConsole(BECWidget, QWidget): if term: term.write(data, send_return) + @property + def zoom_level(self) -> int: + return _bec_console_registry.zoom_level(self.terminal_id) + + def zoom_in(self) -> int | None: + """Increase the tracked zoom level for the shared terminal session.""" + return _bec_console_registry.change_zoom(self.terminal_id, 1) + + def zoom_out(self) -> int | None: + """Decrease the tracked zoom level for the shared terminal session.""" + return _bec_console_registry.change_zoom(self.terminal_id, -1) + def _ensure_startup_started(self): if not self.startup_cmd or not _bec_console_registry.should_initialize(self): return diff --git a/tests/unit_tests/test_bec_console.py b/tests/unit_tests/test_bec_console.py index d887c973..9ea51d25 100644 --- a/tests/unit_tests/test_bec_console.py +++ b/tests/unit_tests/test_bec_console.py @@ -54,6 +54,7 @@ def test_bec_console_initialization(console_widget: BecConsole): assert console_widget.terminal_id == "test_terminal" assert console_widget._mode == ConsoleMode.ACTIVE assert console_widget.term is not None + assert console_widget.zoom_level == 1 assert console_widget._overlay.isHidden() console_widget.show() assert console_widget.isVisible() @@ -126,6 +127,7 @@ def test_bec_shell_initialization(qtbot): assert widget.console_id == "bec_shell" assert widget.terminal_id == "bec_shell" assert widget.startup_cmd is not None + assert widget.zoom_level == 1 def test_bec_console_write(console_widget): @@ -152,6 +154,89 @@ def test_bec_console_write_can_target_shared_terminal_without_ownership(qtbot): mock_write.assert_called_once_with("test command", True) +def test_bec_console_zoom_tracks_shared_terminal_without_ownership(qtbot): + owner = BecConsole(client=mocked_client, gui_id="zoom_owner", terminal_id="shared_zoom") + follower = BecConsole(client=mocked_client, gui_id="zoom_follower", terminal_id="shared_zoom") + qtbot.addWidget(owner) + qtbot.addWidget(follower) + + owner.take_terminal_ownership() + assert owner.term is not None + + with ( + mock.patch.object(owner.term, "zoom_in") as mock_zoom_in, + mock.patch.object(owner.term, "zoom_out") as mock_zoom_out, + ): + assert follower.zoom_in() == 2 + assert follower.zoom_level == 2 + mock_zoom_in.assert_called_once_with() + + assert follower.zoom_out() == 1 + assert follower.zoom_level == 1 + mock_zoom_out.assert_called_once_with() + + +def test_bec_console_reapplies_zoom_level_when_terminal_is_recreated(qtbot, monkeypatch): + class RecordingTerminal(QWidget): + zoom_in_calls = 0 + zoom_out_calls = 0 + + def write(self, text: str, add_newline: bool = True): + return None + + def zoom_in(self): + type(self).zoom_in_calls += 1 + + def zoom_out(self): + type(self).zoom_out_calls += 1 + + def send_ctrl_c(self): + return None + + monkeypatch.setattr(bec_console_module, "_BecTermClass", RecordingTerminal) + + widget = BecConsole(client=mocked_client, gui_id="zoom_recreate", terminal_id="zoom_recreate") + qtbot.addWidget(widget) + + widget.zoom_in() + widget.zoom_in() + assert widget.zoom_level == 3 + assert RecordingTerminal.zoom_in_calls == 3 + + _bec_console_registry._terminal_registry[widget.terminal_id].instance = None + widget.take_terminal_ownership() + + assert widget.zoom_level == 3 + assert RecordingTerminal.zoom_in_calls == 6 + + +def test_bec_console_starts_with_default_zoom_level(qtbot, monkeypatch): + class RecordingTerminal(QWidget): + zoom_in_calls = 0 + zoom_out_calls = 0 + + def write(self, text: str, add_newline: bool = True): + return None + + def zoom_in(self): + type(self).zoom_in_calls += 1 + + def zoom_out(self): + type(self).zoom_out_calls += 1 + + def send_ctrl_c(self): + return None + + monkeypatch.setattr(bec_console_module, "_BecTermClass", RecordingTerminal) + + widget = BecConsole(client=mocked_client, gui_id="console_zoom_default", terminal_id="zoom_default") + qtbot.addWidget(widget) + + assert widget.zoom_level == 1 + assert RecordingTerminal.zoom_in_calls == 1 + assert RecordingTerminal.zoom_out_calls == 0 + + def test_is_owner(console_widget: BecConsole): assert _bec_console_registry.is_owner(console_widget) mock_console = mock.MagicMock() diff --git a/tests/unit_tests/test_developer_view.py b/tests/unit_tests/test_developer_view.py index 803858c6..cfc77ca7 100644 --- a/tests/unit_tests/test_developer_view.py +++ b/tests/unit_tests/test_developer_view.py @@ -104,7 +104,13 @@ class TestDeveloperViewInitialization: # Check for expected toolbar actions toolbar_components = developer_view.toolbar.components - expected_actions = ["save", "save_as", "run", "stop", "vim"] + expected_actions = [ + "save", + "save_as", + "run", + "stop", + "vim", + ] for action_name in expected_actions: assert toolbar_components.exists(action_name)