Skip to content

Commit bb23bba

Browse files
committed
feat: add feedback dialog
1 parent 6cf075c commit bb23bba

4 files changed

Lines changed: 577 additions & 1 deletion

File tree

bec_widgets/widgets/containers/main_window/main_window.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import os
44

5-
from bec_lib import bec_logger
5+
from bec_lib import bec_logger, messages
66
from bec_lib.endpoints import MessageEndpoints
77
from qtpy.QtCore import QSize, Qt, QTimer
88
from qtpy.QtGui import QAction, QActionGroup, QIcon
@@ -36,6 +36,7 @@
3636
from bec_widgets.widgets.containers.main_window.addons.scroll_label import ScrollLabel
3737
from bec_widgets.widgets.containers.main_window.addons.web_links import BECWebLinksMixin
3838
from bec_widgets.widgets.progress.scan_progressbar.scan_progressbar import ScanProgressBar
39+
from bec_widgets.widgets.utility.feedback_dialog.feedback_dialog import FeedbackDialog
3940
from bec_widgets.widgets.utility.widget_hierarchy_tree.widget_hierarchy_tree import (
4041
WidgetHierarchyDialog,
4142
)
@@ -384,6 +385,16 @@ def _setup_menu_bar(self):
384385

385386
help_menu.addAction(bec_docs)
386387
help_menu.addAction(bug_report)
388+
389+
# Feedback action
390+
feedback_icon = QApplication.style().standardIcon(
391+
QStyle.StandardPixmap.SP_MessageBoxQuestion
392+
)
393+
feedback_action = QAction("Submit Feedback", self)
394+
feedback_action.setIcon(feedback_icon)
395+
feedback_action.triggered.connect(self._show_feedback_dialog)
396+
help_menu.addAction(feedback_action)
397+
387398
help_menu.addSeparator()
388399

389400
self._app_id_action = QAction(self)
@@ -401,6 +412,19 @@ def _copy_app_id_to_clipboard(self):
401412
clipboard = QApplication.clipboard()
402413
clipboard.setText(cli_server.gui_id)
403414

415+
def _show_feedback_dialog(self):
416+
"""Show the feedback dialog and handle the submitted feedback."""
417+
dialog = FeedbackDialog(self)
418+
419+
def on_feedback_submitted(rating: int, comment: str, email: str):
420+
rating = max(1, min(rating, 5)) # Ensure rating is between 1 and 5
421+
422+
message = messages.FeedbackMessage(feedback=comment, rating=rating, contact=email)
423+
self.bec_dispatcher.client.connector.send(MessageEndpoints.user_feedback(), message)
424+
425+
dialog.feedback_submitted.connect(on_feedback_submitted)
426+
dialog.exec()
427+
404428
################################################################################
405429
# Status Bar Addons
406430
################################################################################

bec_widgets/widgets/utility/feedback_dialog/__init__.py

Whitespace-only changes.
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
from qtpy.QtCore import Qt, Signal
2+
from qtpy.QtGui import QColor, QFont
3+
from qtpy.QtWidgets import (
4+
QApplication,
5+
QDialog,
6+
QHBoxLayout,
7+
QLabel,
8+
QLineEdit,
9+
QPushButton,
10+
QTextEdit,
11+
QVBoxLayout,
12+
QWidget,
13+
)
14+
15+
from bec_widgets.utils.error_popups import SafeConnect, SafeSlot
16+
17+
18+
class StarRating(QWidget):
19+
"""
20+
A star rating widget that allows users to rate from 1 to 5 stars.
21+
"""
22+
23+
rating_changed = Signal(int)
24+
25+
def __init__(self, parent=None):
26+
super().__init__(parent)
27+
self._rating = 0
28+
self._hovered_star = 0
29+
self._star_buttons = []
30+
31+
# Get theme colors
32+
theme = getattr(QApplication.instance(), "theme", None)
33+
if theme:
34+
SafeConnect(self, theme.theme_changed, self._update_theme_colors)
35+
self._update_theme_colors()
36+
37+
# Enable mouse tracking to handle hover across the entire widget
38+
self.setMouseTracking(True)
39+
40+
layout = QHBoxLayout()
41+
layout.setContentsMargins(0, 0, 0, 0)
42+
layout.setSpacing(5)
43+
44+
for i in range(5):
45+
btn = QPushButton("★")
46+
btn.setFixedSize(30, 30)
47+
btn.setFlat(True)
48+
btn.setCursor(Qt.CursorShape.PointingHandCursor)
49+
btn.clicked.connect(lambda checked=False, idx=i + 1: self._set_rating(idx))
50+
layout.addWidget(btn)
51+
self._star_buttons.append(btn)
52+
53+
self.setLayout(layout)
54+
self._update_display()
55+
56+
@SafeSlot(str)
57+
def _update_theme_colors(self, _theme: str | None = None):
58+
"""Update colors based on theme."""
59+
theme = getattr(QApplication.instance(), "theme", None)
60+
colors = theme.colors if theme else {}
61+
62+
self._inactive_color = colors.get("SEPARATOR", QColor(200, 200, 200))
63+
self._active_color = colors.get("ACCENT_WARNING", QColor(255, 193, 7))
64+
65+
# Update display if already initialized
66+
if hasattr(self, "_star_buttons") and self._star_buttons:
67+
self._update_display()
68+
69+
def _set_rating(self, rating: int):
70+
"""Set the rating and emit the signal."""
71+
if self._rating != rating:
72+
self._rating = rating
73+
self.rating_changed.emit(rating)
74+
self._update_display()
75+
76+
def mouseMoveEvent(self, event):
77+
"""Handle mouse movement to update hovered star."""
78+
# Calculate which star is being hovered based on mouse position
79+
x_pos = event.pos().x()
80+
star_idx = 0
81+
82+
# Find which star region we're in (including gaps between stars)
83+
for i, btn in enumerate(self._star_buttons):
84+
btn_geometry = btn.geometry()
85+
# If we're to the right of this button's left edge, this is the current star
86+
# (including the gap before the next button)
87+
if x_pos >= btn_geometry.left():
88+
star_idx = i + 1
89+
else:
90+
break
91+
92+
if star_idx != self._hovered_star:
93+
self._hovered_star = star_idx
94+
self._update_display()
95+
96+
super().mouseMoveEvent(event)
97+
98+
def leaveEvent(self, event):
99+
"""Handle mouse leaving the widget."""
100+
self._hovered_star = 0
101+
self._update_display()
102+
super().leaveEvent(event)
103+
104+
def _update_display(self):
105+
"""Update the visual display of stars."""
106+
display_rating = self._hovered_star if self._hovered_star > 0 else self._rating
107+
inactive_color_name = self._inactive_color.name()
108+
active_color_name = self._active_color.name()
109+
110+
for i, btn in enumerate(self._star_buttons):
111+
if i < display_rating:
112+
btn.setStyleSheet(f"""
113+
QPushButton {{
114+
border: none;
115+
background: transparent;
116+
font-size: 24px;
117+
color: {active_color_name};
118+
}}
119+
""")
120+
else:
121+
btn.setStyleSheet(f"""
122+
QPushButton {{
123+
border: none;
124+
background: transparent;
125+
font-size: 24px;
126+
color: {inactive_color_name};
127+
}}
128+
QPushButton:hover {{
129+
color: {active_color_name};
130+
}}
131+
""")
132+
133+
def rating(self) -> int:
134+
"""Get the current rating."""
135+
return self._rating
136+
137+
def set_rating(self, rating: int):
138+
"""Set the rating programmatically."""
139+
if 0 <= rating <= 5:
140+
self._set_rating(rating)
141+
142+
143+
class FeedbackDialog(QDialog):
144+
"""
145+
A feedback dialog widget containing a comment field, star rating, and optional email field.
146+
147+
Signals:
148+
feedbackSubmitted: Emitted when feedback is submitted (rating: int, comment: str, email: str)
149+
"""
150+
151+
feedback_submitted = Signal(int, str, str)
152+
ICON_NAME = "feedback"
153+
PLUGIN = True
154+
155+
def __init__(self, parent=None):
156+
super().__init__(parent)
157+
self.setWindowTitle("Feedback")
158+
self.setModal(True)
159+
self.setMinimumWidth(400)
160+
self.setMinimumHeight(300)
161+
162+
self._setup_ui()
163+
164+
def _setup_ui(self):
165+
"""Set up the user interface."""
166+
layout = QVBoxLayout()
167+
layout.setSpacing(15)
168+
169+
# Title
170+
title_label = QLabel("We'd love to hear your feedback!")
171+
title_font = QFont()
172+
title_font.setPointSize(12)
173+
title_font.setBold(True)
174+
title_label.setFont(title_font)
175+
layout.addWidget(title_label)
176+
177+
# Star rating section
178+
rating_layout = QVBoxLayout()
179+
rating_label = QLabel("Rating:")
180+
rating_layout.addWidget(rating_label)
181+
182+
self._star_rating = StarRating()
183+
rating_layout.addWidget(self._star_rating)
184+
layout.addLayout(rating_layout)
185+
186+
# Comment section
187+
comment_label = QLabel("Comments:")
188+
layout.addWidget(comment_label)
189+
190+
self._comment_field = QTextEdit()
191+
self._comment_field.setPlaceholderText("Please share your thoughts...")
192+
self._comment_field.setMaximumHeight(150)
193+
layout.addWidget(self._comment_field)
194+
195+
# Email section (optional)
196+
email_label = QLabel("Email (optional, for follow-up):")
197+
layout.addWidget(email_label)
198+
199+
self._email_field = QLineEdit()
200+
self._email_field.setPlaceholderText("your.email@example.com")
201+
layout.addWidget(self._email_field)
202+
203+
# Buttons
204+
button_layout = QHBoxLayout()
205+
button_layout.addStretch()
206+
207+
self._cancel_button = QPushButton("Cancel")
208+
self._cancel_button.clicked.connect(self.reject)
209+
button_layout.addWidget(self._cancel_button)
210+
211+
self._submit_button = QPushButton("Submit")
212+
self._submit_button.setDefault(True)
213+
self._submit_button.clicked.connect(self._on_submit)
214+
button_layout.addWidget(self._submit_button)
215+
216+
layout.addLayout(button_layout)
217+
218+
self.setLayout(layout)
219+
220+
def _on_submit(self):
221+
"""Handle submit button click."""
222+
rating = self._star_rating.rating()
223+
comment = self._comment_field.toPlainText().strip()
224+
email = self._email_field.text().strip()
225+
226+
# Emit the feedback signal
227+
self.feedback_submitted.emit(rating, comment, email)
228+
229+
# Accept the dialog
230+
self.accept()
231+
232+
def get_feedback(self) -> tuple[int, str, str]:
233+
"""
234+
Get the current feedback values.
235+
236+
Returns:
237+
tuple: (rating, comment, email)
238+
"""
239+
return (
240+
self._star_rating.rating(),
241+
self._comment_field.toPlainText().strip(),
242+
self._email_field.text().strip(),
243+
)
244+
245+
def set_rating(self, rating: int):
246+
"""Set the star rating."""
247+
self._star_rating.set_rating(rating)
248+
249+
def set_comment(self, comment: str):
250+
"""Set the comment text."""
251+
self._comment_field.setPlainText(comment)
252+
253+
def set_email(self, email: str):
254+
"""Set the email text."""
255+
self._email_field.setText(email)
256+
257+
@staticmethod
258+
def show_feedback_dialog(parent=None) -> tuple[int, str, str] | None:
259+
"""
260+
Show the feedback dialog and return the feedback if submitted.
261+
262+
Args:
263+
parent: Parent widget
264+
265+
Returns:
266+
tuple: (rating, comment, email) if submitted, None if cancelled
267+
"""
268+
dialog = FeedbackDialog(parent)
269+
if dialog.exec() == QDialog.DialogCode.Accepted:
270+
return dialog.get_feedback()
271+
return None
272+
273+
274+
if __name__ == "__main__": # pragma: no cover
275+
import sys
276+
277+
from bec_widgets.utils.colors import apply_theme
278+
279+
app = QApplication(sys.argv)
280+
apply_theme("dark")
281+
dialog = FeedbackDialog()
282+
283+
def on_feedback(rating, comment, email):
284+
print(f"Rating: {rating}")
285+
print(f"Comment: {comment}")
286+
print(f"Email: {email}")
287+
288+
dialog.feedback_submitted.connect(on_feedback)
289+
dialog.exec()
290+
sys.exit(app.exec())

0 commit comments

Comments
 (0)