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, 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 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 
52  # Paint blue rounded rectangle
53  path = QPainterPath()
54  path.addRoundedRect(QRectF(31, 0, self.width()-31, self.height()), 10, 10)
55  painter.setPen(Qt.NoPen)
56  painter.fillPath(path, QColor("#53a0ed"))
57  painter.drawPath(path)
58 
59  # Paint gray rounded rectangle
60  path = QPainterPath()
61  path.addRoundedRect(QRectF(32, 1, self.width()-33, self.height()-2), 10, 10)
62  painter.setPen(Qt.NoPen)
63  painter.fillPath(path, QColor("#424242"))
64  painter.drawPath(path)
65 
66  # Paint blue triangle (if needed)
67  if self.arrow:
68  arrow_height = 20
69  path = QPainterPath()
70  path.moveTo (0, 35)
71  path.lineTo (31, 35 - arrow_height)
72  path.lineTo (31, (35 - arrow_height) + (arrow_height * 2))
73  path.lineTo (0, 35)
74  painter.fillPath(path, QColor("#53a0ed"))
75  painter.drawPath(path)
76 
77  def eventFilter(self, object, e):
78  if e.type() == QEvent.WindowActivate:
79  # Raise parent window, and then this tutorial
80  get_app().window.showNormal()
81  get_app().window.raise_()
82  self.raise_()
83 
84  return False
85 
86  ##
87  # Move widget next to its position widget
88  def moveWidget(self):
89  x = self.position_widget.mapToGlobal(self.position_widget.pos()).x()
90  y = self.position_widget.mapToGlobal(self.position_widget.pos()).y()
91  self.move(QPoint(x + self.x_offset, y + self.y_offset))
92 
93  ##
94  # Callback for error and anonymous usage checkbox
95  def checkbox_metrics_callback(self, state):
96  s = get_settings()
97  if state == Qt.Checked:
98  # Enabling metrics sending
99  s.set("send_metrics", True)
100 
101  # Opt-in for metrics tracking
102  track_metric_screen("metrics-opt-in")
103  else:
104  # Opt-out for metrics tracking
105  track_metric_screen("metrics-opt-out")
106 
107  # Disable metric sending
108  s.set("send_metrics", False)
109 
110  def __init__(self, id, text, position_widget, x_offset, y_offset, arrow, *args):
111  # Invoke parent init
112  QWidget.__init__(self, *args)
113 
114  # get translations
115  app = get_app()
116  _ = app._tr
117 
118  # Keep track of widget to position next to
119  self.id = id
120  self.position_widget = position_widget
121  self.x_offset = x_offset
122  self.y_offset = y_offset
123  self.arrow = arrow
124 
125  # Create vertical box
126  vbox = QVBoxLayout()
127  vbox.setContentsMargins(32,10,10,10)
128 
129  # Add label
130  self.label = QLabel(self)
131  self.label.setText(text)
132  self.label.setTextFormat(Qt.RichText)
133  self.label.setWordWrap(True)
134  self.label.setStyleSheet("margin-left: 20px;")
135  vbox.addWidget(self.label)
136 
137  # Add error and anonymous metrics checkbox (for ID=0) tooltip
138  # This is a bit of a hack, but since it's the only exception, it's
139  # probably okay for now.
140  if self.id == "0":
141  # Get settings
142  s = get_settings()
143 
144  # create spinner
145  checkbox_metrics = QCheckBox()
146  checkbox_metrics.setText(_("Yes, I would like to improve OpenShot!"))
147  checkbox_metrics.setStyleSheet("margin-left: 25px; margin-bottom: 5px;")
148  if s.get("send_metrics"):
149  checkbox_metrics.setCheckState(Qt.Checked)
150  else:
151  checkbox_metrics.setCheckState(Qt.Unchecked)
152  checkbox_metrics.stateChanged.connect(functools.partial(self.checkbox_metrics_callback))
153  vbox.addWidget(checkbox_metrics)
154 
155  # Add button box
156  hbox = QHBoxLayout()
157  hbox.setContentsMargins(20,10,0,0)
158 
159  # Create buttons
160  self.btn_close_tips = QPushButton(self)
161  self.btn_close_tips.setText(_("Hide Tutorial"))
162  self.btn_next_tip = QPushButton(self)
163  self.btn_next_tip.setText(_("Next"))
164  self.btn_next_tip.setStyleSheet("font-weight:bold;")
165  hbox.addWidget(self.btn_close_tips)
166  hbox.addWidget(self.btn_next_tip)
167  vbox.addLayout(hbox)
168 
169  # Set layout
170  self.setLayout(vbox)
171 
172  # Set size
173  self.setMinimumWidth(350)
174  self.setMinimumHeight(100)
175 
176  # Make it's own window
177  self.setWindowTitle("Tutorial")
178  self.setWindowFlags(Qt.Window | Qt.FramelessWindowHint)
179  self.setAttribute(Qt.WA_TranslucentBackground, True)
180  self.setFocusPolicy(Qt.NoFocus)
181 
182  # Position window next to other widget
183  self.moveWidget()
184 
185  # Install event filter
186  self.installEventFilter(self)
187 
188 
189 ##
190 # Manage and present a list of tutorial dialogs
192 
193  ##
194  # Process and show the first non-completed tutorial
195  def process(self, parent_name=None):
196  log.info("process tutorial dialogs")
197 
198  # Do nothing if a tutorial is already visible
199  if self.current_dialog:
200  self.re_show_dialog()
201  return
202 
203  # Loop through and add each tutorial dialog
204  for tutorial_details in self.tutorial_objects:
205  # Get details
206  tutorial_id = tutorial_details["id"]
207  tutorial_object_id = tutorial_details["object_id"]
208  tutorial_text = tutorial_details["text"]
209  tutorial_x_offset = tutorial_details["x"]
210  tutorial_y_offset = tutorial_details["y"]
211  turorial_arrow = tutorial_details["arrow"]
212 
213  # Get QWidget
214  tutorial_object = self.get_object(tutorial_object_id)
215 
216  # Skip completed tutorials (and invisible widgets)
217  if tutorial_object.visibleRegion().isEmpty() or tutorial_id in self.tutorial_ids or not self.tutorial_enabled:
218  continue
219 
220  # Create tutorial
221  tutorial_dialog = TutorialDialog(tutorial_id, tutorial_text, tutorial_object, tutorial_x_offset, tutorial_y_offset, turorial_arrow)
222 
223  # Connect signals
224  tutorial_dialog.btn_next_tip.clicked.connect(functools.partial(self.next_tip, tutorial_id))
225  tutorial_dialog.btn_close_tips.clicked.connect(functools.partial(self.hide_tips, tutorial_id, True))
226 
227  # Show dialog
228  self.current_dialog = tutorial_dialog
229  self.current_dialog.show()
230  break
231 
232  ##
233  # Get an object from the main window by object id
234  def get_object(self, object_id):
235  if object_id == "filesTreeView":
236  return self.win.filesTreeView
237  elif object_id == "timeline":
238  return self.win.timeline
239  elif object_id == "dockVideoContents":
240  return self.win.dockVideoContents
241  elif object_id == "propertyTableView":
242  return self.win.propertyTableView
243  elif object_id == "transitionsTreeView":
244  return self.win.transitionsTreeView
245  elif object_id == "effectsTreeView":
246  return self.win.effectsTreeView
247  elif object_id == "export_button":
248  # Find export toolbar button on main window
249  export_button = None
250  for toolbutton in self.win.toolBar.children():
251  if type(toolbutton) == QToolButton and toolbutton.defaultAction() and toolbutton.defaultAction().objectName() == "actionExportVideo":
252  return toolbutton
253 
254  ##
255  # Mark the current tip completed, and show the next one
256  def next_tip(self, tid):
257  log.info("next_tip")
258 
259  # Hide matching tutorial
260  self.hide_tips(tid)
261 
262  # Process the next type
263  self.process()
264 
265  ##
266  # Hide the current tip, and don't show anymore
267  def hide_tips(self, tid, user_clicked=False):
268  log.info("hide_tips")
269  s = get_settings()
270 
271  # Loop through and find current tid
272  for tutorial_object in self.tutorial_objects:
273  # Get details
274  tutorial_id = tutorial_object["id"]
275  if tutorial_id == tid:
276  # Hide dialog
277  self.close_dialogs()
278  # Update settings that this tutorial is completed
279  if tid not in self.tutorial_ids:
280  self.tutorial_ids.append(str(tid))
281  s.set("tutorial_ids", ",".join(self.tutorial_ids))
282 
283  # Mark tutorial as completed (if settings)
284  if user_clicked:
285  # Disable all tutorials
286  self.tutorial_enabled = False
287  s.set("tutorial_enabled", False)
288 
289  ##
290  # Close any open tutorial dialogs
291  def close_dialogs(self):
292  if self.current_dialog:
293  self.current_dialog.hide()
294  self.current_dialog = None
295 
296  ##
297  # Disconnect from all signals, and shutdown tutorial manager
298  def exit_manager(self):
299  try:
300  self.win.dockFiles.visibilityChanged.disconnect()
301  self.win.dockTransitions.visibilityChanged.disconnect()
302  self.win.dockEffects.visibilityChanged.disconnect()
303  self.win.dockProperties.visibilityChanged.disconnect()
304  self.win.dockVideo.visibilityChanged.disconnect()
305  except:
306  # Ignore errors from this
307  pass
308 
309  # Close dialog window
310  self.close_dialogs()
311 
312  ##
313  # Re show an active dialog
314  def re_show_dialog(self):
315  if self.current_dialog:
316  self.current_dialog.showNormal()
317  self.current_dialog.raise_()
318 
319  ##
320  # Reposition a tutorial dialog next to another widget
322  if self.current_dialog:
323  self.current_dialog.moveWidget()
324 
325  ##
326  # Minimize any visible tutorial dialog
327  def minimize(self):
328  log.info("minimize tutorial")
329  if self.current_dialog:
330  self.current_dialog.showMinimized()
331 
332  ##
333  # Constructor
334  def __init__(self, win):
335  self.tutorials = []
336  self.win = win
337  self.current_dialog = None
338 
339  # get translations
340  app = get_app()
341  _ = app._tr
342 
343  # get settings
344  s = get_settings()
345  self.tutorial_enabled = s.get("tutorial_enabled")
346  self.tutorial_ids = s.get("tutorial_ids").split(",")
347 
348  # Add all possible tutorials
349  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},
350  {"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},
351  {"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},
352  {"id":"3", "x":150, "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},
353  {"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},
354  {"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},
355  {"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},
356  {"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}
357  ]
358 
359  # Connect to dock widgets
360  self.win.dockFiles.visibilityChanged.connect(functools.partial(self.process, "dockFiles"))
361  self.win.dockTransitions.visibilityChanged.connect(functools.partial(self.process, "dockTransitions"))
362  self.win.dockEffects.visibilityChanged.connect(functools.partial(self.process, "dockEffects"))
363  self.win.dockProperties.visibilityChanged.connect(functools.partial(self.process, "dockProperties"))
364  self.win.dockVideo.visibilityChanged.connect(functools.partial(self.process, "dockVideo"))
365 
366  # Process tutorials (1 by 1)
367  if self.tutorial_enabled:
368  self.process()
def re_position_dialog
Reposition a tutorial dialog next to another widget.
Definition: tutorial.py:321
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:256
def exit_manager
Disconnect from all signals, and shutdown tutorial manager.
Definition: tutorial.py:298
Manage and present a list of tutorial dialogs.
Definition: tutorial.py:191
def __init__
Constructor.
Definition: tutorial.py:334
A QWidget used to instruct a user how to use a certain feature.
Definition: tutorial.py:43
def moveWidget
Move widget next to its position widget.
Definition: tutorial.py:88
def checkbox_metrics_callback
Callback for error and anonymous usage checkbox.
Definition: tutorial.py:95
def minimize
Minimize any visible tutorial dialog.
Definition: tutorial.py:327
def re_show_dialog
Re show an active dialog.
Definition: tutorial.py:314
def hide_tips
Hide the current tip, and don't show anymore.
Definition: tutorial.py:267
def get_object
Get an object from the main window by object id.
Definition: tutorial.py:234
def close_dialogs
Close any open tutorial dialogs.
Definition: tutorial.py:291
def process
Process and show the first non-completed tutorial.
Definition: tutorial.py:195
def get_settings
Get the current QApplication's settings instance.
Definition: settings.py:44