From 156ec35246e5c280b390fb6a9c727c49883629f6 Mon Sep 17 00:00:00 2001 From: Anantharaman R <90021026+CrazyCyberbug@users.noreply.github.com> Date: Fri, 6 Feb 2026 23:59:58 +0530 Subject: [PATCH] Added copy-to-clipboard button in recording transcribe widget (#1370) --- buzz/widgets/recording_transcriber_widget.py | 57 +++++++++++++ .../recording_transcriber_widget_test.py | 83 +++++++++++++++++++ 2 files changed, 140 insertions(+) diff --git a/buzz/widgets/recording_transcriber_widget.py b/buzz/widgets/recording_transcriber_widget.py index 7a35a06c..1df5f149 100644 --- a/buzz/widgets/recording_transcriber_widget.py +++ b/buzz/widgets/recording_transcriber_widget.py @@ -16,6 +16,7 @@ from PyQt6.QtWidgets import ( QFormLayout, QHBoxLayout, QMessageBox, + QApplication, QPushButton, QComboBox, QLabel, @@ -209,6 +210,9 @@ class RecordingTranscriberWidget(QWidget): self.presentation_options_bar = self.create_presentation_options_bar() layout.insertWidget(3, self.presentation_options_bar) self.presentation_options_bar.hide() + self.copy_actions_bar = self.create_copy_actions_bar() + layout.addWidget(self.copy_actions_bar) # Add at the bottom + self.copy_actions_bar.hide() def create_presentation_options_bar(self) -> QWidget: """Crete the presentation options bar widget""" @@ -286,6 +290,56 @@ class RecordingTranscriberWidget(QWidget): return bar + def create_copy_actions_bar(self) -> QWidget: + """Create the copy actions bar widget""" + bar = QWidget(self) + layout = QHBoxLayout(bar) + layout.setContentsMargins(5, 5, 5, 5) + layout.setSpacing(10) + + layout.addStretch() # Push button to the right + + self.copy_transcript_button = QPushButton(_("Copy"), bar) + self.copy_transcript_button.setToolTip(_("Copy transcription to clipboard")) + self.copy_transcript_button.clicked.connect(self.on_copy_transcript_clicked) + layout.addWidget(self.copy_transcript_button) + + return bar + + def on_copy_transcript_clicked(self): + """Handle copy transcript button click""" + transcript_text = self.transcription_text_box.toPlainText().strip() + + if not transcript_text: + self.copy_transcript_button.setText(_("Nothing to copy!")) + QTimer.singleShot(1500, lambda: self.copy_transcript_button.setText(_("Copy"))) + return + + app = QApplication.instance() + if app is None: + logging.warning("QApplication instance not available; clipboard disabled") + self.copy_transcript_button.setText(_("Copy failed")) + QTimer.singleShot(1500, lambda: self.copy_transcript_button.setText(_("Copy"))) + return + + clipboard = app.clipboard() + if clipboard is None: + logging.warning("Clipboard not available") + self.copy_transcript_button.setText(_("Copy failed")) + QTimer.singleShot(1500, lambda: self.copy_transcript_button.setText(_("Copy"))) + return + + try: + clipboard.setText(transcript_text) + except Exception as e: + logging.warning("Clipboard error: %s", e) + self.copy_transcript_button.setText(_("Copy failed")) + QTimer.singleShot(1500, lambda: self.copy_transcript_button.setText(_("Copy"))) + return + + self.copy_transcript_button.setText(_("Copied!")) + QTimer.singleShot(2000, lambda: self.copy_transcript_button.setText(_("Copy"))) + def on_show_presentation_clicked(self): """Handle click on 'Show in new window' button""" if self.presentation_window is None or not self.presentation_window.isVisible(): @@ -464,6 +518,8 @@ class RecordingTranscriberWidget(QWidget): self.transcription_options_group_box.setEnabled(False) self.audio_devices_combo_box.setEnabled(False) self.presentation_options_bar.show() + self.copy_actions_bar.hide() + else: # RecordingStatus.RECORDING self.stop_recording() self.set_recording_status_stopped() @@ -574,6 +630,7 @@ class RecordingTranscriberWidget(QWidget): self.transcription_options_group_box.setEnabled(True) self.audio_devices_combo_box.setEnabled(True) self.presentation_options_bar.hide() + self.copy_actions_bar.show() #added this here def on_download_model_error(self, error: str): self.reset_model_download() diff --git a/tests/widgets/recording_transcriber_widget_test.py b/tests/widgets/recording_transcriber_widget_test.py index 42baa148..4e43357b 100644 --- a/tests/widgets/recording_transcriber_widget_test.py +++ b/tests/widgets/recording_transcriber_widget_test.py @@ -470,6 +470,89 @@ class TestRecordingTranscriberWidgetPresentation: time.sleep(0.5) widget.close() + @pytest.mark.timeout(60) + def test_on_copy_transcript_clicked_with_text(self, qtbot: QtBot): + with ( + patch("sounddevice.InputStream", side_effect=MockInputStream), + patch("sounddevice.check_input_settings"), + patch( + "buzz.transcriber.recording_transcriber.RecordingTranscriber.get_device_sample_rate", + return_value=16_000, + ), + ): + mock_clipboard = MagicMock() + mock_app = MagicMock() + mock_app.clipboard.return_value = mock_clipboard + + widget = RecordingTranscriberWidget(custom_sounddevice=MockSoundDevice()) + qtbot.add_widget(widget) + + widget.transcription_text_box.setPlainText("Hello world") + widget.copy_actions_bar.show() + + with patch("buzz.widgets.recording_transcriber_widget.QApplication.instance", + return_value=mock_app): + widget.on_copy_transcript_clicked() + + mock_clipboard.setText.assert_called_once_with("Hello world") + assert widget.copy_transcript_button.text() == _("Copied!") + + time.sleep(0.5) + widget.close() + + @pytest.mark.timeout(60) + def test_on_copy_transcript_clicked_without_text(self, qtbot: QtBot): + """Test that copy button handles empty transcript gracefully""" + with ( + patch("sounddevice.InputStream", side_effect=MockInputStream), + patch("sounddevice.check_input_settings"), + patch("buzz.transcriber.recording_transcriber.RecordingTranscriber.get_device_sample_rate", + return_value=16_000), + ): + widget = RecordingTranscriberWidget( + custom_sounddevice=MockSoundDevice() + ) + qtbot.add_widget(widget) + + widget.transcription_text_box.setPlainText("") + widget.copy_actions_bar.show() + + widget.on_copy_transcript_clicked() + + assert widget.copy_transcript_button.text() == _("Nothing to copy!") + + time.sleep(0.5) + widget.close() + + @pytest.mark.timeout(60) + def test_copy_actions_bar_hidden_when_recording_starts(self, qtbot: QtBot): + """Test that copy actions bar hides when recording starts""" + with ( + patch("sounddevice.InputStream", side_effect=MockInputStream), + patch("sounddevice.check_input_settings"), + patch("buzz.transcriber.recording_transcriber.RecordingTranscriber.get_device_sample_rate", + return_value=16_000), + ): + widget = RecordingTranscriberWidget( + custom_sounddevice=MockSoundDevice() + ) + widget.device_sample_rate = 16_000 + qtbot.add_widget(widget) + + widget.copy_actions_bar.show() + assert not widget.copy_actions_bar.isHidden() + + # Mock start_recording to prevent actual recording threads from starting + widget.current_status = widget.RecordingStatus.STOPPED + with patch.object(widget, 'start_recording'): + widget.on_record_button_clicked() + + assert widget.copy_actions_bar.isHidden() + + time.sleep(0.5) + widget.close() + + @pytest.mark.timeout(60) def test_on_bg_color_clicked(self, qtbot: QtBot): """Test that background color button opens color dialog and saves selection"""