Skip to content

Commit ae80263

Browse files
authored
Merge pull request #5676 from OpenShot/improved-tutorial-style
Improved Tutorial Styles + Fix for Window Titles
2 parents 6b583c3 + 61be0e1 commit ae80263

File tree

3 files changed

+150
-64
lines changed

3 files changed

+150
-64
lines changed

src/themes/cosmic/theme.py

+17
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,23 @@ def __init__(self, app):
5555
padding: 20px;
5656
}
5757
58+
QLabel#lblTutorialText {
59+
font-size: 14px;
60+
}
61+
62+
QCheckBox#checkboxMetrics {
63+
font-size: 14px;
64+
}
65+
66+
QWidget#tutorial QPushButton#NextTip {
67+
background-color: #283241;
68+
font-size: 12px;
69+
}
70+
71+
QWidget#tutorial QPushButton#HideTutorial {
72+
font-size: 12px;
73+
}
74+
5875
5976
QDialog {
6077
background-color: #192332;

src/windows/main_window.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2506,6 +2506,7 @@ def actionTutorial_trigger(self):
25062506
if self.tutorial_manager:
25072507
self.tutorial_manager.exit_manager()
25082508
self.tutorial_manager = TutorialManager(self)
2509+
self.tutorial_manager.process_visibility()
25092510

25102511
def actionInsertTimestamp_trigger(self, event):
25112512
"""Insert the current timestamp into the caption editor
@@ -3848,4 +3849,3 @@ def __init__(self, *args):
38483849

38493850
# Init all Keyboard shortcuts
38503851
self.initShortcuts()
3851-

src/windows/views/tutorial.py

+132-63
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
import functools
2929

30-
from PyQt5.QtCore import Qt, QPoint, QRectF, QTimer, QObject
30+
from PyQt5.QtCore import Qt, QPoint, QRectF, QTimer, QObject, QRect
3131
from PyQt5.QtGui import (
3232
QColor, QPalette, QPen, QPainter, QPainterPath, QKeySequence,
3333
)
@@ -44,32 +44,70 @@
4444
class TutorialDialog(QWidget):
4545
""" A customized QWidget used to instruct a user how to use a certain feature """
4646

47-
def paintEvent(self, event, *args):
47+
def paintEvent(self, event):
4848
""" Custom paint event """
49-
# Paint custom frame image on QWidget
5049
painter = QPainter(self)
5150
painter.setRenderHint(QPainter.Antialiasing)
52-
frameColor = QColor("#53a0ed")
5351

54-
painter.setPen(QPen(frameColor, 2))
52+
# Set correct margins based on left/right arrow
53+
arrow_width = 15
54+
if not self.draw_arrow_on_right:
55+
self.vbox.setContentsMargins(45, 10, 20, 10)
56+
else:
57+
self.vbox.setContentsMargins(20, 10, 45, 10)
58+
59+
# Define rounded rectangle geometry
60+
corner_radius = 10
61+
if self.draw_arrow_on_right:
62+
# Rectangle starts at left edge; arrow is on the right
63+
rounded_rect = QRectF(0, 0, self.width() - arrow_width, self.height())
64+
else:
65+
# Rectangle shifted to the right; arrow is on the left
66+
rounded_rect = QRectF(arrow_width, 0, self.width() - arrow_width, self.height())
67+
68+
# Clip to the rounded rectangle
69+
path = QPainterPath()
70+
path.addRoundedRect(rounded_rect, corner_radius, corner_radius)
71+
painter.setClipPath(path)
72+
73+
# Fill background
74+
frameColor = QColor("#53a0ed")
75+
painter.setPen(QPen(frameColor, 1.2))
5576
painter.setBrush(self.palette().color(QPalette.Window))
56-
painter.drawRoundedRect(
57-
QRectF(31, 0,
58-
self.width() - 31,
59-
self.height()
60-
),
61-
10, 10)
62-
63-
# Paint blue triangle (if needed)
77+
painter.drawRoundedRect(rounded_rect, corner_radius, corner_radius)
78+
79+
# Disable clipping temporarily for the arrow
80+
painter.setClipping(False)
81+
82+
# Draw arrow if needed
6483
if self.arrow:
65-
arrow_height = 20
84+
arrow_height = 15
85+
arrow_offset = 35
86+
87+
if self.draw_arrow_on_right:
88+
# Arrow on the right side
89+
arrow_point = rounded_rect.topRight().toPoint() + QPoint(arrow_width, arrow_offset)
90+
arrow_top_corner = rounded_rect.topRight().toPoint() + QPoint(-1, arrow_offset - arrow_height)
91+
arrow_bottom_corner = rounded_rect.topRight().toPoint() + QPoint(-1, arrow_offset + arrow_height)
92+
else:
93+
# Arrow on the left side
94+
arrow_point = rounded_rect.topLeft().toPoint() + QPoint(-arrow_width, arrow_offset)
95+
arrow_top_corner = rounded_rect.topLeft().toPoint() + QPoint(1, arrow_offset - arrow_height)
96+
arrow_bottom_corner = rounded_rect.topLeft().toPoint() + QPoint(1, arrow_offset + arrow_height)
97+
98+
# Draw triangle (filled with the same background color as the window)
6699
path = QPainterPath()
67-
path.moveTo(0, 35)
68-
path.lineTo(31, 35 - arrow_height)
69-
path.lineTo(
70-
31, int((35 - arrow_height) + (arrow_height * 2)))
71-
path.lineTo(0, 35)
72-
painter.fillPath(path, frameColor)
100+
path.moveTo(arrow_point) # Arrow tip
101+
path.lineTo(arrow_top_corner) # Top corner of the triangle
102+
path.lineTo(arrow_bottom_corner) # Bottom corner of the triangle
103+
path.closeSubpath()
104+
painter.fillPath(path, self.palette().color(QPalette.Window))
105+
106+
# Draw the triangle's borders
107+
border_pen = QPen(frameColor, 1)
108+
painter.setPen(border_pen)
109+
painter.drawLine(arrow_point, arrow_top_corner) # Top triangle border
110+
painter.drawLine(arrow_point, arrow_bottom_corner) # Bottom triangle border
73111

74112
def checkbox_metrics_callback(self, state):
75113
""" Callback for error and anonymous usage checkbox"""
@@ -96,8 +134,13 @@ def mouseReleaseEvent(self, event):
96134
self.manager.next_tip(self.widget_id)
97135

98136
def __init__(self, widget_id, text, arrow, manager, *args):
99-
# Invoke parent init
100-
QWidget.__init__(self, *args)
137+
super().__init__(*args)
138+
139+
# Ensure frameless, floating behavior
140+
self.setWindowFlags(Qt.FramelessWindowHint | Qt.Tool | Qt.WindowStaysOnTopHint)
141+
self.setAttribute(Qt.WA_NoSystemBackground, True)
142+
self.setAttribute(Qt.WA_TranslucentBackground, True)
143+
self.setAttribute(Qt.WA_DeleteOnClose, True)
101144

102145
# get translations
103146
app = get_app()
@@ -107,19 +150,20 @@ def __init__(self, widget_id, text, arrow, manager, *args):
107150
self.widget_id = widget_id
108151
self.arrow = arrow
109152
self.manager = manager
153+
self.draw_arrow_on_right = False
110154

111155
# Create vertical box
112-
vbox = QVBoxLayout()
113-
vbox.setContentsMargins(32, 10, 10, 10)
156+
self.vbox = QVBoxLayout()
114157

115158
# Add label
116159
self.label = QLabel(self)
160+
self.label.setObjectName("lblTutorialText")
117161
self.label.setText(text)
118162
self.label.setTextFormat(Qt.RichText)
119163
self.label.setWordWrap(True)
120-
self.label.setStyleSheet("margin-left: 20px;")
164+
self.label.setStyleSheet("")
121165
self.label.setAttribute(Qt.WA_TransparentForMouseEvents)
122-
vbox.addWidget(self.label)
166+
self.vbox.addWidget(self.label)
123167

124168
# Add error and anonymous metrics checkbox (for ID=0) tooltip
125169
# This is a bit of a hack, but since it's the only exception, it's
@@ -130,18 +174,18 @@ def __init__(self, widget_id, text, arrow, manager, *args):
130174

131175
# create spinner
132176
checkbox_metrics = QCheckBox()
177+
checkbox_metrics.setObjectName("checkboxMetrics")
133178
checkbox_metrics.setText(_("Yes, I would like to improve OpenShot!"))
134-
checkbox_metrics.setStyleSheet("margin-left: 25px; margin-bottom: 5px;")
135179
if s.get("send_metrics"):
136180
checkbox_metrics.setCheckState(Qt.Checked)
137181
else:
138182
checkbox_metrics.setCheckState(Qt.Unchecked)
139183
checkbox_metrics.stateChanged.connect(functools.partial(self.checkbox_metrics_callback))
140-
vbox.addWidget(checkbox_metrics)
184+
self.vbox.addWidget(checkbox_metrics)
141185

142186
# Add button box
143187
hbox = QHBoxLayout()
144-
hbox.setContentsMargins(20, 10, 0, 0)
188+
hbox.setContentsMargins(0, 5, 0, 5)
145189

146190
# Close action
147191
self.close_action = QAction(_("Hide Tutorial"), self)
@@ -151,28 +195,25 @@ def __init__(self, widget_id, text, arrow, manager, *args):
151195
# Create buttons
152196
self.btn_close_tips = QPushButton(self)
153197
self.btn_close_tips.setText(_("Hide Tutorial"))
198+
self.btn_close_tips.setObjectName("HideTutorial")
154199
self.btn_close_tips.addAction(self.close_action)
155200

156201
self.btn_next_tip = QPushButton(self)
202+
self.btn_next_tip.setObjectName("NextTip")
157203
self.btn_next_tip.setText(_("Next"))
158204
self.btn_next_tip.setStyleSheet("font-weight:bold;")
159205

160206
hbox.addWidget(self.btn_close_tips)
161207
hbox.addWidget(self.btn_next_tip)
162-
vbox.addLayout(hbox)
208+
self.vbox.addLayout(hbox)
163209

164210
# Set layout, cursor, and size
165-
self.setLayout(vbox)
211+
self.setLayout(self.vbox)
166212
self.setCursor(Qt.ArrowCursor)
167213
self.setMinimumWidth(350)
168214
self.setMinimumHeight(100)
169215
self.setFocusPolicy(Qt.ClickFocus)
170216

171-
# Make transparent
172-
self.setAttribute(Qt.WA_NoSystemBackground, True)
173-
self.setAttribute(Qt.WA_TranslucentBackground, True)
174-
self.setAttribute(Qt.WA_DeleteOnClose, True)
175-
176217
# Connect close action signal
177218
self.close_action.triggered.connect(
178219
functools.partial(self.manager.hide_tips, self.widget_id, True))
@@ -253,12 +294,15 @@ def get_object(self, object_id):
253294
elif object_id == "actionPlay":
254295
# Find play/pause button on transport controls toolbar
255296
for w in self.win.actionPlay.associatedWidgets():
256-
if isinstance(w, QToolButton):
297+
if isinstance(w, QToolButton) and w.isVisible():
298+
return w
299+
for w in self.win.actionPause.associatedWidgets():
300+
if isinstance(w, QToolButton) and w.isVisible():
257301
return w
258302
elif object_id == "export_button":
259303
# Find export toolbar button on main window
260-
for w in self.win.actionExportVideo.associatedWidgets():
261-
if isinstance(w, QToolButton):
304+
for w in reversed(self.win.actionExportVideo.associatedWidgets()):
305+
if isinstance(w, QToolButton) and w.isVisible() and w.parent() == self.win.toolBar:
262306
return w
263307

264308
def next_tip(self, tid):
@@ -319,6 +363,7 @@ def exit_manager(self):
319363
def re_show_dialog(self):
320364
""" Re show an active dialog """
321365
if self.current_dialog:
366+
self.dock.update()
322367
self.dock.raise_()
323368
self.dock.show()
324369

@@ -328,30 +373,57 @@ def hide_dialog(self):
328373
self.dock.hide()
329374

330375
def re_position_dialog(self):
331-
""" Reposition a tutorial dialog next to another widget """
332-
if self.current_dialog:
333-
# Check if target is visible
334-
if self.position_widget.isHidden() or self.position_widget.visibleRegion().isEmpty():
335-
self.hide_dialog()
336-
return
376+
""" Reposition the tutorial dialog next to self.position_widget. """
377+
# Bail if no dialog or target widget hidden
378+
if not self.current_dialog:
379+
return
380+
if self.position_widget.isHidden() or self.position_widget.visibleRegion().isEmpty():
381+
self.hide_dialog()
382+
return
337383

338-
# Locate tutorial popup relative to its "target" widget
339-
pos_rect = self.position_widget.rect()
384+
# Compute the reference rect of the target widget
385+
pos_rect = self.position_widget.rect()
386+
# “float” the popup 1/4 size away from top-left corner
387+
pos_rect.setSize(pos_rect.size() / 4)
388+
pos_rect.translate(self.offset)
389+
390+
# Compute both possible positions (arrow on left vs. arrow on right)
391+
# NOTE: We do this BEFORE we actually move the dialog!
392+
position_arrow_left = self.position_widget.mapToGlobal(pos_rect.bottomRight())
393+
position_arrow_right = self.position_widget.mapToGlobal(pos_rect.bottomLeft()) - QPoint(
394+
self.current_dialog.width(), 0)
395+
396+
# Decide which side is viable. For example, we can see if arrow-on-left
397+
# would run off the right side of the screen. If it does, pick arrow-on-right.
398+
screen_rect = get_app().primaryScreen().availableGeometry()
399+
monitor_width = screen_rect.width()
400+
401+
# If placing “arrow on left” means we’d exceed monitor width, we must switch to arrow on right
402+
would_exceed_right_edge = (position_arrow_left.x() + self.current_dialog.width()) > monitor_width
403+
if would_exceed_right_edge:
404+
final_position = position_arrow_right
405+
arrow_on_right = True
406+
else:
407+
final_position = position_arrow_left
408+
arrow_on_right = False
409+
410+
# Update the dialog’s internal state (so paintEvent() knows how to draw it).
411+
self.current_dialog.draw_arrow_on_right = arrow_on_right
340412

341-
# Start with a 1/4-size offset rectangle, so the tutorial dialog
342-
# floats a bit, then apply any custom offset defined for this popup.
343-
pos_rect.setSize(pos_rect.size() / 4)
344-
pos_rect.translate(self.offset)
345-
# Map the new rectangle's bottom-right corner to global coords
346-
position = self.position_widget.mapToGlobal(pos_rect.bottomRight())
413+
# Update margins ONE time here, so geometry only changes once
414+
if arrow_on_right:
415+
self.current_dialog.vbox.setContentsMargins(20, 10, 45, 10)
416+
else:
417+
self.current_dialog.vbox.setContentsMargins(45, 10, 20, 10)
347418

348-
# Move tutorial widget to the correct position
349-
self.dock.move(position)
350-
self.re_show_dialog()
419+
# Move the dock exactly once, and raise it
420+
self.dock.move(final_position)
421+
self.re_show_dialog()
351422

352423
def process_visibility(self):
353424
"""Handle callbacks when widget visibility changes"""
354-
self.tutorial_timer.start()
425+
if self.tutorial_enabled:
426+
self.tutorial_timer.start()
355427

356428
def __init__(self, win, *args):
357429
# Init QObject superclass
@@ -361,6 +433,7 @@ def __init__(self, win, *args):
361433
self.win = win
362434
self.dock = win.dockTutorial
363435
self.current_dialog = None
436+
self.dock.setParent(None)
364437

365438
# get translations
366439
app = get_app()
@@ -396,7 +469,7 @@ def __init__(self, win, *args):
396469
},
397470
{"id": "3",
398471
"x": 10,
399-
"y": -27,
472+
"y": -42,
400473
"object_id": "actionPlay",
401474
"text": _("<b>Video Preview:</b> Watch your timeline video preview here. Use the buttons (play, rewind, fast-forward) to control the video playback."),
402475
"arrow": True},
@@ -441,7 +514,7 @@ def __init__(self, win, *args):
441514
self.dock.setTitleBarWidget(QWidget()) # Prevents window decoration
442515
self.dock.setAttribute(Qt.WA_NoSystemBackground, True)
443516
self.dock.setAttribute(Qt.WA_TranslucentBackground, True)
444-
self.dock.setWindowFlags(Qt.FramelessWindowHint)
517+
self.dock.setWindowFlags(Qt.FramelessWindowHint | Qt.Tool | Qt.WindowStaysOnTopHint)
445518
self.dock.setFloating(True)
446519

447520
# Timer for processing new tutorials
@@ -457,7 +530,3 @@ def __init__(self, win, *args):
457530
self.win.dockProperties.visibilityChanged.connect(self.process_visibility)
458531
self.win.dockVideo.visibilityChanged.connect(self.process_visibility)
459532
self.win.dockEmojis.visibilityChanged.connect(self.process_visibility)
460-
461-
# Process tutorials (1 by 1)
462-
if self.tutorial_enabled:
463-
self.process_visibility()

0 commit comments

Comments
 (0)