OpenShot Video Editor  2.0.0
tutorial.py
Go to the documentation of this file.
1 ##
2 #
3 # @file
4 # @brief This file contains the tutorial dialogs, which are used to explain certain features to new users
5 # @author Jonathan Thomas <jonathan@openshot.org>
6 #
7 # @section LICENSE
8 #
9 # Copyright (c) 2008-2018 OpenShot Studios, LLC
10 # (http://www.openshotstudios.com). This file is part of
11 # OpenShot Video Editor (http://www.openshot.org), an open-source project
12 # dedicated to delivering high quality video editing and animation solutions
13 # to the world.
14 #
15 # OpenShot Video Editor is free software: you can redistribute it and/or modify
16 # it under the terms of the GNU General Public License as published by
17 # the Free Software Foundation, either version 3 of the License, or
18 # (at your option) any later version.
19 #
20 # OpenShot Video Editor is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
24 #
25 # You should have received a copy of the GNU General Public License
26 # along with OpenShot Library. If not, see <http://www.gnu.org/licenses/>.
27 #
28 
29 import functools
30 
31 from PyQt5.QtCore import Qt, QPoint, QRectF, QEvent
32 from PyQt5.QtGui import *
33 from PyQt5.QtWidgets import QLabel, QWidget, QDockWidget, QVBoxLayout, QHBoxLayout, QPushButton, QToolButton, QCheckBox
34 
35 from classes.logger import log
36 from classes.settings import get_settings
37 from classes.app import get_app
38 from classes.metrics import *
39 
40 
41 ##
42 # A customized QWidget used to instruct a user how to use a certain feature
43 class TutorialDialog(QWidget):
44 
45  ##
46  # Custom paint event
47  def paintEvent(self, event, *args):
48  # Paint custom frame image on QWidget
49  painter = QPainter(self)
50  painter.setRenderHint(QPainter.Antialiasing)
51  frameColor = QColor("#53a0ed")
52 
53  # Paint blue rounded rectangle
54  path = QPainterPath()
55  path.addRoundedRect(QRectF(31, 0, self.width()-31, self.height()), 10, 10)
56  painter.setPen(Qt.NoPen)
57  painter.fillPath(path, frameColor)
58  painter.drawPath(path)
59 
60  # Paint gray rounded rectangle
61  path = QPainterPath()
62  path.addRoundedRect(QRectF(32, 1, self.width()-33, self.height()-2), 10, 10)
63  painter.setPen(Qt.NoPen)
64  painter.fillPath(path, self.palette().color(QPalette.Window))
65  painter.drawPath(path)
66 
67  # Paint blue triangle (if needed)
68  if self.arrow:
69  arrow_height = 20
70  path = QPainterPath()
71  path.moveTo (0, 35)
72  path.lineTo (31, 35 - arrow_height)
73  path.lineTo (31, (35 - arrow_height) + (arrow_height * 2))
74  path.lineTo (0, 35)
75  painter.fillPath(path, frameColor)
76  painter.drawPath(path)
77 
78  ##
79  # Callback for error and anonymous usage checkbox
80  def checkbox_metrics_callback(self, state):
81  s = get_settings()
82  if state == Qt.Checked:
83  # Enabling metrics sending
84  s.set("send_metrics", True)
85 
86  # Opt-in for metrics tracking
87  track_metric_screen("metrics-opt-in")
88  else:
89  # Opt-out for metrics tracking
90  track_metric_screen("metrics-opt-out")
91 
92  # Disable metric sending
93  s.set("send_metrics", False)
94 
95  def __init__(self, id, text, arrow, *args):
96  # Invoke parent init
97  QWidget.__init__(self, *args)
98 
99  # get translations
100  app = get_app()
101  _ = app._tr
102 
103  # Keep track of widget to position next to
104  self.id = id
105  self.arrow = arrow
106 
107  # Create vertical box
108  vbox = QVBoxLayout()
109  vbox.setContentsMargins(32,10,10,10)
110 
111  # Add label
112  self.label = QLabel(self)
113  self.label.setText(text)
114  self.label.setTextFormat(Qt.RichText)
115  self.label.setWordWrap(True)
116  self.label.setStyleSheet("margin-left: 20px;")
117  vbox.addWidget(self.label)
118 
119  # Add error and anonymous metrics checkbox (for ID=0) tooltip
120  # This is a bit of a hack, but since it's the only exception, it's
121  # probably okay for now.
122  if self.id == "0":
123  # Get settings
124  s = get_settings()
125 
126  # create spinner
127  checkbox_metrics = QCheckBox()
128  checkbox_metrics.setText(_("Yes, I would like to improve OpenShot!"))
129  checkbox_metrics.setStyleSheet("margin-left: 25px; margin-bottom: 5px;")
130  if s.get("send_metrics"):
131  checkbox_metrics.setCheckState(Qt.Checked)
132  else:
133  checkbox_metrics.setCheckState(Qt.Unchecked)
134  checkbox_metrics.stateChanged.connect(functools.partial(self.checkbox_metrics_callback))
135  vbox.addWidget(checkbox_metrics)
136 
137  # Add button box
138  hbox = QHBoxLayout()
139  hbox.setContentsMargins(20,10,0,0)
140 
141  # Create buttons
142  self.btn_close_tips = QPushButton(self)
143  self.btn_close_tips.setText(_("Hide Tutorial"))
144  self.btn_next_tip = QPushButton(self)
145  self.btn_next_tip.setText(_("Next"))
146  self.btn_next_tip.setStyleSheet("font-weight:bold;")
147  hbox.addWidget(self.btn_close_tips)
148  hbox.addWidget(self.btn_next_tip)
149  vbox.addLayout(hbox)
150 
151  # Set layout
152  self.setLayout(vbox)
153 
154  # Set size
155  self.setMinimumWidth(350)
156  self.setMinimumHeight(100)
157 
158  # Make transparent
159  self.setAttribute(Qt.WA_NoSystemBackground, True)
160  self.setAttribute(Qt.WA_TranslucentBackground, True)
161  #self.setWindowFlags(Qt.FramelessWindowHint)
162 
163 
164 ##
165 # Manage and present a list of tutorial dialogs
167 
168  ##
169  # Process and show the first non-completed tutorial
170  def process(self, parent_name=None):
171 
172  # Do nothing if a tutorial is already visible
173  if self.current_dialog:
174  # XXX: Respond to possible dock floats/moves
175  self.dock.raise_()
176  self.re_position_dialog()
177  return
178 
179  # Loop through and add each tutorial dialog
180  for tutorial_details in self.tutorial_objects:
181  # Get details
182  tutorial_id = tutorial_details["id"]
183  tutorial_object_id = tutorial_details["object_id"]
184  tutorial_text = tutorial_details["text"]
185  tutorial_x_offset = tutorial_details["x"]
186  tutorial_y_offset = tutorial_details["y"]
187  turorial_arrow = tutorial_details["arrow"]
188 
189  # Get QWidget
190  tutorial_object = self.get_object(tutorial_object_id)
191 
192  # Skip completed tutorials (and invisible widgets)
193  if tutorial_object.visibleRegion().isEmpty() or tutorial_id in self.tutorial_ids or not self.tutorial_enabled:
194  continue
195 
196  # Create tutorial
197  self.position_widget = tutorial_object
198  self.x_offset = tutorial_x_offset
199  self.y_offset = tutorial_y_offset
200  tutorial_dialog = TutorialDialog(tutorial_id, tutorial_text, turorial_arrow)
201 
202  # Connect signals
203  tutorial_dialog.btn_next_tip.clicked.connect(functools.partial(self.next_tip, tutorial_id))
204  tutorial_dialog.btn_close_tips.clicked.connect(functools.partial(self.hide_tips, tutorial_id, True))
205 
206  # Insert into tutorial dock
207  self.dock.setWidget(tutorial_dialog)
208  self.current_dialog = tutorial_dialog
209 
210  # Show dialog
211  self.dock.adjustSize()
212  self.dock.setEnabled(True)
213  self.re_position_dialog()
214  self.dock.show()
215  break
216 
217  ##
218  # Get an object from the main window by object id
219  def get_object(self, object_id):
220  if object_id == "filesTreeView":
221  return self.win.filesTreeView
222  elif object_id == "timeline":
223  return self.win.timeline
224  elif object_id == "dockVideoContents":
225  return self.win.dockVideoContents
226  elif object_id == "propertyTableView":
227  return self.win.propertyTableView
228  elif object_id == "transitionsTreeView":
229  return self.win.transitionsTreeView
230  elif object_id == "effectsTreeView":
231  return self.win.effectsTreeView
232  elif object_id == "export_button":
233  # Find export toolbar button on main window
234  export_button = None
235  for toolbutton in self.win.toolBar.children():
236  if type(toolbutton) == QToolButton and toolbutton.defaultAction() and toolbutton.defaultAction().objectName() == "actionExportVideo":
237  return toolbutton
238 
239  ##
240  # Mark the current tip completed, and show the next one
241  def next_tip(self, tid):
242  # Hide matching tutorial
243  self.hide_tips(tid)
244 
245  # Advance to the next one
246  self.process()
247 
248  ##
249  # Hide the current tip, and don't show anymore
250  def hide_tips(self, tid, user_clicked=False):
251  s = get_settings()
252 
253  # Loop through and find current tid
254  for tutorial_object in self.tutorial_objects:
255  # Get details
256  tutorial_id = tutorial_object["id"]
257  if tutorial_id == tid:
258  # Hide dialog
259  self.close_dialogs()
260  # Update settings that this tutorial is completed
261  if tid not in self.tutorial_ids:
262  self.tutorial_ids.append(str(tid))
263  s.set("tutorial_ids", ",".join(self.tutorial_ids))
264 
265  # Mark tutorial as completed (if settings)
266  if user_clicked:
267  # Disable all tutorials
268  self.tutorial_enabled = False
269  s.set("tutorial_enabled", False)
270 
271  ##
272  # Close any open tutorial dialogs
273  def close_dialogs(self):
274  if self.current_dialog:
275  self.dock.hide()
276  self.dock.setEnabled(False)
277  self.current_dialog = None
278 
279  ##
280  # Disconnect from all signals, and shutdown tutorial manager
281  def exit_manager(self):
282  try:
283  self.win.dockFiles.visibilityChanged.disconnect()
284  self.win.dockTransitions.visibilityChanged.disconnect()
285  self.win.dockEffects.visibilityChanged.disconnect()
286  self.win.dockProperties.visibilityChanged.disconnect()
287  self.win.dockVideo.visibilityChanged.disconnect()
288  except:
289  # Ignore errors from this
290  pass
291 
292  # Close dialog window
293  self.close_dialogs()
294 
295  ##
296  # Re show an active dialog
297  def re_show_dialog(self):
298  if self.current_dialog:
299  self.dock.raise_()
300  self.dock.show()
301 
302  ##
303  # Reposition a tutorial dialog next to another widget
305  if self.current_dialog:
306  """ Move widget next to its position widget """
307  x = self.position_widget.mapToGlobal(self.position_widget.pos()).x()
308  y = self.position_widget.mapToGlobal(self.position_widget.pos()).y()
309  self.dock.move(QPoint(x + self.x_offset, y + self.y_offset))
310 
311  ##
312  # Constructor
313  def __init__(self, win):
314  self.win = win
315  self.dock = win.dockTutorial
316  self.current_dialog = None
317 
318  # get translations
319  app = get_app()
320  _ = app._tr
321 
322  # get settings
323  s = get_settings()
324  self.tutorial_enabled = s.get("tutorial_enabled")
325  self.tutorial_ids = s.get("tutorial_ids").split(",")
326 
327  # Add all possible tutorials
328  self.tutorial_objects = [ {"id":"0", "x":400, "y":0, "object_id":"filesTreeView", "text":_("<b>Welcome!</b> OpenShot Video Editor is an award-winning, open-source video editing application! This tutorial will walk you through the basics.<br><br>Would you like to automatically send errors and metrics to help improve OpenShot?"), "arrow":False},
329  {"id":"1", "x":20, "y":0, "object_id":"filesTreeView", "text":_("<b>Project Files:</b> Get started with your project by adding video, audio, and image files here. Drag and drop files from your file system."), "arrow":True},
330  {"id":"2", "x":200, "y":-15, "object_id":"timeline", "text":_("<b>Timeline:</b> Arrange your clips on the timeline here. Overlap clips to create automatic transitions. Access lots of fun presets and options by right-clicking on clips."), "arrow":True},
331  {"id":"3", "x":200, "y":100, "object_id":"dockVideoContents", "text":_("<b>Video Preview:</b> Watch your timeline video preview here. Use the buttons (play, rewind, fast-forward) to control the video playback."), "arrow":True},
332  {"id":"4", "x":20, "y":-35, "object_id":"propertyTableView", "text":_("<b>Properties:</b> View and change advanced properties of clips and effects here. Right-clicking on clips is usually faster than manually changing properties."), "arrow":True},
333  {"id":"5", "x":20, "y":10, "object_id":"transitionsTreeView", "text":_("<b>Transitions:</b> Create a gradual fade from one clip to another. Drag and drop a transition onto the timeline and position it on top of a clip (usually at the beginning or ending)."), "arrow":True},
334  {"id":"6", "x":20, "y":20, "object_id":"effectsTreeView", "text":_("<b>Effects:</b> Adjust brightness, contrast, saturation, and add exciting special effects. Drag and drop an effect onto the timeline and position it on top of a clip (or track)"), "arrow":True},
335  {"id":"7", "x":-265, "y":-22, "object_id":"export_button", "text":_("<b>Export Video:</b> When you are ready to create your finished video, click this button to export your timeline as a single video file."), "arrow":True}
336  ]
337 
338  # Configure tutorial frame
339  self.dock.setTitleBarWidget(QWidget()) # Prevents window decoration
340  self.dock.setAttribute(Qt.WA_NoSystemBackground, True)
341  self.dock.setAttribute(Qt.WA_TranslucentBackground, True)
342  self.dock.setWindowFlags(Qt.FramelessWindowHint)
343  self.dock.setFloating(True)
344 
345 
346  # Connect to interface dock widgets
347  self.win.dockFiles.visibilityChanged.connect(functools.partial(self.process, "dockFiles"))
348  self.win.dockTransitions.visibilityChanged.connect(functools.partial(self.process, "dockTransitions"))
349  self.win.dockEffects.visibilityChanged.connect(functools.partial(self.process, "dockEffects"))
350  self.win.dockProperties.visibilityChanged.connect(functools.partial(self.process, "dockProperties"))
351  self.win.dockVideo.visibilityChanged.connect(functools.partial(self.process, "dockVideo"))
352 
353  # Process tutorials (1 by 1)
354  if self.tutorial_enabled:
355  self.process()
def re_position_dialog
Reposition a tutorial dialog next to another widget.
Definition: tutorial.py:304
def track_metric_screen
Track a GUI screen being shown.
Definition: metrics.py:96
def paintEvent
Custom paint event.
Definition: tutorial.py:47
def get_app
Returns the current QApplication instance of OpenShot.
Definition: app.py:55
def next_tip
Mark the current tip completed, and show the next one.
Definition: tutorial.py:241
def exit_manager
Disconnect from all signals, and shutdown tutorial manager.
Definition: tutorial.py:281
Manage and present a list of tutorial dialogs.
Definition: tutorial.py:166
def __init__
Constructor.
Definition: tutorial.py:313
A customized QWidget used to instruct a user how to use a certain feature.
Definition: tutorial.py:43
def checkbox_metrics_callback
Callback for error and anonymous usage checkbox.
Definition: tutorial.py:80
def re_show_dialog
Re show an active dialog.
Definition: tutorial.py:297
def hide_tips
Hide the current tip, and don't show anymore.
Definition: tutorial.py:250
def get_object
Get an object from the main window by object id.
Definition: tutorial.py:219
def close_dialogs
Close any open tutorial dialogs.
Definition: tutorial.py:273
def process
Process and show the first non-completed tutorial.
Definition: tutorial.py:170
def get_settings
Get the current QApplication's settings instance.
Definition: settings.py:44