27
27
28
28
import functools
29
29
30
- from PyQt5 .QtCore import Qt , QPoint , QRectF , QTimer , QObject
30
+ from PyQt5 .QtCore import Qt , QPoint , QRectF , QTimer , QObject , QRect
31
31
from PyQt5 .QtGui import (
32
32
QColor , QPalette , QPen , QPainter , QPainterPath , QKeySequence ,
33
33
)
44
44
class TutorialDialog (QWidget ):
45
45
""" A customized QWidget used to instruct a user how to use a certain feature """
46
46
47
- def paintEvent (self , event , * args ):
47
+ def paintEvent (self , event ):
48
48
""" Custom paint event """
49
- # Paint custom frame image on QWidget
50
49
painter = QPainter (self )
51
50
painter .setRenderHint (QPainter .Antialiasing )
52
- frameColor = QColor ("#53a0ed" )
53
51
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 ))
55
76
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
64
83
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)
66
99
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
73
111
74
112
def checkbox_metrics_callback (self , state ):
75
113
""" Callback for error and anonymous usage checkbox"""
@@ -96,8 +134,13 @@ def mouseReleaseEvent(self, event):
96
134
self .manager .next_tip (self .widget_id )
97
135
98
136
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 )
101
144
102
145
# get translations
103
146
app = get_app ()
@@ -107,19 +150,20 @@ def __init__(self, widget_id, text, arrow, manager, *args):
107
150
self .widget_id = widget_id
108
151
self .arrow = arrow
109
152
self .manager = manager
153
+ self .draw_arrow_on_right = False
110
154
111
155
# Create vertical box
112
- vbox = QVBoxLayout ()
113
- vbox .setContentsMargins (32 , 10 , 10 , 10 )
156
+ self .vbox = QVBoxLayout ()
114
157
115
158
# Add label
116
159
self .label = QLabel (self )
160
+ self .label .setObjectName ("lblTutorialText" )
117
161
self .label .setText (text )
118
162
self .label .setTextFormat (Qt .RichText )
119
163
self .label .setWordWrap (True )
120
- self .label .setStyleSheet ("margin-left: 20px; " )
164
+ self .label .setStyleSheet ("" )
121
165
self .label .setAttribute (Qt .WA_TransparentForMouseEvents )
122
- vbox .addWidget (self .label )
166
+ self . vbox .addWidget (self .label )
123
167
124
168
# Add error and anonymous metrics checkbox (for ID=0) tooltip
125
169
# 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):
130
174
131
175
# create spinner
132
176
checkbox_metrics = QCheckBox ()
177
+ checkbox_metrics .setObjectName ("checkboxMetrics" )
133
178
checkbox_metrics .setText (_ ("Yes, I would like to improve OpenShot!" ))
134
- checkbox_metrics .setStyleSheet ("margin-left: 25px; margin-bottom: 5px;" )
135
179
if s .get ("send_metrics" ):
136
180
checkbox_metrics .setCheckState (Qt .Checked )
137
181
else :
138
182
checkbox_metrics .setCheckState (Qt .Unchecked )
139
183
checkbox_metrics .stateChanged .connect (functools .partial (self .checkbox_metrics_callback ))
140
- vbox .addWidget (checkbox_metrics )
184
+ self . vbox .addWidget (checkbox_metrics )
141
185
142
186
# Add button box
143
187
hbox = QHBoxLayout ()
144
- hbox .setContentsMargins (20 , 10 , 0 , 0 )
188
+ hbox .setContentsMargins (0 , 5 , 0 , 5 )
145
189
146
190
# Close action
147
191
self .close_action = QAction (_ ("Hide Tutorial" ), self )
@@ -151,28 +195,25 @@ def __init__(self, widget_id, text, arrow, manager, *args):
151
195
# Create buttons
152
196
self .btn_close_tips = QPushButton (self )
153
197
self .btn_close_tips .setText (_ ("Hide Tutorial" ))
198
+ self .btn_close_tips .setObjectName ("HideTutorial" )
154
199
self .btn_close_tips .addAction (self .close_action )
155
200
156
201
self .btn_next_tip = QPushButton (self )
202
+ self .btn_next_tip .setObjectName ("NextTip" )
157
203
self .btn_next_tip .setText (_ ("Next" ))
158
204
self .btn_next_tip .setStyleSheet ("font-weight:bold;" )
159
205
160
206
hbox .addWidget (self .btn_close_tips )
161
207
hbox .addWidget (self .btn_next_tip )
162
- vbox .addLayout (hbox )
208
+ self . vbox .addLayout (hbox )
163
209
164
210
# Set layout, cursor, and size
165
- self .setLayout (vbox )
211
+ self .setLayout (self . vbox )
166
212
self .setCursor (Qt .ArrowCursor )
167
213
self .setMinimumWidth (350 )
168
214
self .setMinimumHeight (100 )
169
215
self .setFocusPolicy (Qt .ClickFocus )
170
216
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
-
176
217
# Connect close action signal
177
218
self .close_action .triggered .connect (
178
219
functools .partial (self .manager .hide_tips , self .widget_id , True ))
@@ -253,12 +294,15 @@ def get_object(self, object_id):
253
294
elif object_id == "actionPlay" :
254
295
# Find play/pause button on transport controls toolbar
255
296
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 ():
257
301
return w
258
302
elif object_id == "export_button" :
259
303
# 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 :
262
306
return w
263
307
264
308
def next_tip (self , tid ):
@@ -319,6 +363,7 @@ def exit_manager(self):
319
363
def re_show_dialog (self ):
320
364
""" Re show an active dialog """
321
365
if self .current_dialog :
366
+ self .dock .update ()
322
367
self .dock .raise_ ()
323
368
self .dock .show ()
324
369
@@ -328,30 +373,57 @@ def hide_dialog(self):
328
373
self .dock .hide ()
329
374
330
375
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
337
383
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
340
412
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 )
347
418
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 ()
351
422
352
423
def process_visibility (self ):
353
424
"""Handle callbacks when widget visibility changes"""
354
- self .tutorial_timer .start ()
425
+ if self .tutorial_enabled :
426
+ self .tutorial_timer .start ()
355
427
356
428
def __init__ (self , win , * args ):
357
429
# Init QObject superclass
@@ -361,6 +433,7 @@ def __init__(self, win, *args):
361
433
self .win = win
362
434
self .dock = win .dockTutorial
363
435
self .current_dialog = None
436
+ self .dock .setParent (None )
364
437
365
438
# get translations
366
439
app = get_app ()
@@ -396,7 +469,7 @@ def __init__(self, win, *args):
396
469
},
397
470
{"id" : "3" ,
398
471
"x" : 10 ,
399
- "y" : - 27 ,
472
+ "y" : - 42 ,
400
473
"object_id" : "actionPlay" ,
401
474
"text" : _ ("<b>Video Preview:</b> Watch your timeline video preview here. Use the buttons (play, rewind, fast-forward) to control the video playback." ),
402
475
"arrow" : True },
@@ -441,7 +514,7 @@ def __init__(self, win, *args):
441
514
self .dock .setTitleBarWidget (QWidget ()) # Prevents window decoration
442
515
self .dock .setAttribute (Qt .WA_NoSystemBackground , True )
443
516
self .dock .setAttribute (Qt .WA_TranslucentBackground , True )
444
- self .dock .setWindowFlags (Qt .FramelessWindowHint )
517
+ self .dock .setWindowFlags (Qt .FramelessWindowHint | Qt . Tool | Qt . WindowStaysOnTopHint )
445
518
self .dock .setFloating (True )
446
519
447
520
# Timer for processing new tutorials
@@ -457,7 +530,3 @@ def __init__(self, win, *args):
457
530
self .win .dockProperties .visibilityChanged .connect (self .process_visibility )
458
531
self .win .dockVideo .visibilityChanged .connect (self .process_visibility )
459
532
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