OpenShot Video Editor  2.0.0
title_editor.py
Go to the documentation of this file.
1 ##
2 #
3 # @file
4 # @brief This file loads the title editor dialog (i.e SVG creator)
5 # @author Jonathan Thomas <jonathan@openshot.org>
6 # @author Andy Finch <andy@openshot.org>
7 #
8 # @section LICENSE
9 #
10 # Copyright (c) 2008-2018 OpenShot Studios, LLC
11 # (http://www.openshotstudios.com). This file is part of
12 # OpenShot Video Editor (http://www.openshot.org), an open-source project
13 # dedicated to delivering high quality video editing and animation solutions
14 # to the world.
15 #
16 # OpenShot Video Editor is free software: you can redistribute it and/or modify
17 # it under the terms of the GNU General Public License as published by
18 # the Free Software Foundation, either version 3 of the License, or
19 # (at your option) any later version.
20 #
21 # OpenShot Video Editor is distributed in the hope that it will be useful,
22 # but WITHOUT ANY WARRANTY; without even the implied warranty of
23 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 # GNU General Public License for more details.
25 #
26 # You should have received a copy of the GNU General Public License
27 # along with OpenShot Library. If not, see <http://www.gnu.org/licenses/>.
28 #
29 
30 import sys
31 import os
32 import shutil
33 import functools
34 import subprocess
35 from xml.dom import minidom
36 
37 from PyQt5.QtCore import *
38 from PyQt5.QtGui import QIcon, QStandardItemModel, QStandardItem, QFont
39 from PyQt5.QtWidgets import *
40 from PyQt5 import uic, QtSvg, QtGui
41 from PyQt5.QtWebKitWidgets import QWebView
42 import openshot
43 
44 from classes import info, ui_util, settings, qt_types, updates
45 from classes.logger import log
46 from classes.app import get_app
47 from classes.query import File
48 from classes.metrics import *
49 from windows.views.titles_listview import TitlesListView
50 
51 try:
52  import json
53 except ImportError:
54  import simplejson as json
55 
56 
57 ##
58 # Title Editor Dialog
59 class TitleEditor(QDialog):
60 
61  # Path to ui file
62  ui_path = os.path.join(info.PATH, 'windows', 'ui', 'title-editor.ui')
63 
64  def __init__(self, edit_file_path=None, duplicate=False):
65 
66  # Create dialog class
67  QDialog.__init__(self)
68 
69  self.app = get_app()
70  self.project = self.app.project
71  self.edit_file_path = edit_file_path
72  self.duplicate = duplicate
73 
74  # Get translation object
75  _ = self.app._tr
76 
77  # Load UI from designer
78  ui_util.load_ui(self, self.ui_path)
79 
80  # Init UI
81  ui_util.init_ui(self)
82 
83  # Track metrics
84  track_metric_screen("title-screen")
85 
86  # Initialize variables
87  self.template_name = ""
88  imp = minidom.getDOMImplementation()
89  self.xmldoc = imp.createDocument(None, "any", None)
90 
91  self.bg_color_code = ""
92  self.font_color_code = "#ffffff"
93 
94  self.bg_style_string = ""
97 
98  self.font_weight = 'normal'
99  self.font_style = 'normal'
100 
101  self.new_title_text = ""
102  self.sub_title_text = ""
103  self.subTitle = False
104 
105  self.display_name = ""
106  self.font_family = "Bitstream Vera Sans"
107  self.tspan_node = None
108 
109  # Add titles list view
110  self.titlesTreeView = TitlesListView(self)
111  self.verticalLayout.addWidget(self.titlesTreeView)
112 
113  # If editing existing title svg file
114  if self.edit_file_path:
115  # Hide list of templates
116  self.widget.setVisible(False)
117 
118  # Create temp version of title
120 
121  # Add all widgets for editing
122  self.load_svg_template()
123 
124  # Display image (slight delay to allow screen to be shown first)
125  QTimer.singleShot(50, self.display_svg)
126 
127  def txtLine_changed(self, txtWidget):
128 
129  # Loop through child widgets (and remove them)
130  text_list = []
131  for child in self.settingsContainer.children():
132  if type(child) == QTextEdit and child.objectName() != "txtFileName":
133  text_list.append(child.toPlainText())
134 
135  # Update text values in the SVG
136  for i in range(0, self.text_fields):
137  if len(self.tspan_node[i].childNodes) > 0 and i <= (len(text_list) - 1):
138  new_text_node = self.xmldoc.createTextNode(text_list[i])
139  old_text_node = self.tspan_node[i].childNodes[0]
140  self.tspan_node[i].removeChild(old_text_node)
141  # add new text node
142  self.tspan_node[i].appendChild(new_text_node)
143 
144  # Something changed, so update temp SVG
145  self.writeToFile(self.xmldoc)
146 
147  # Display SVG again
148  self.display_svg()
149 
150  def display_svg(self):
151  scene = QGraphicsScene(self)
152  view = self.graphicsView
153  svg = QtGui.QPixmap(self.filename)
154  svg_scaled = svg.scaled(self.graphicsView.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
155  scene.addPixmap(svg_scaled)
156  view.setScene(scene)
157  view.show()
158 
159  def create_temp_title(self, template_path):
160 
161  # Set temp file path
162  self.filename = os.path.join(info.TITLE_PATH, "temp.svg")
163 
164  # Copy template to temp file
165  shutil.copy(template_path, self.filename)
166 
167  # return temp path
168  return self.filename
169 
170  ##
171  # Load an SVG title and init all textboxes and controls
172  def load_svg_template(self):
173 
174  # Get translation object
175  _ = get_app()._tr
176 
177  # parse the svg object
178  self.xmldoc = minidom.parse(self.filename)
179  # get the text elements
180  self.tspan_node = self.xmldoc.getElementsByTagName('tspan')
181  self.text_fields = len(self.tspan_node)
182 
183  # Loop through child widgets (and remove them)
184  for child in self.settingsContainer.children():
185  try:
186  self.settingsContainer.layout().removeWidget(child)
187  child.deleteLater()
188  except:
189  pass
190 
191  # Get text nodes and rect nodes
192  self.text_node = self.xmldoc.getElementsByTagName('text')
193  self.rect_node = self.xmldoc.getElementsByTagName('rect')
194 
195  # Create Label
196  label = QLabel()
197  label_line_text = _("File Name:")
198  label.setText(label_line_text)
199  label.setToolTip(label_line_text)
200 
201  # create text editor for file name
202  self.txtFileName = QTextEdit()
203  self.txtFileName.setObjectName("txtFileName")
204 
205  # If edit mode, set file name
206  if self.edit_file_path and not self.duplicate:
207  # Use existing name (and prevent editing name)
208  self.txtFileName.setText(os.path.split(self.edit_file_path)[1])
209  self.txtFileName.setEnabled(False)
210  else:
211  # Find an unused file name
212  for i in range(1, 1000):
213  possible_path = os.path.join(info.ASSETS_PATH, "%s.svg" % _("TitleFileName-%d") % i)
214  if not os.path.exists(possible_path):
215  self.txtFileName.setText(_("TitleFileName-%d") % i)
216  break
217  self.txtFileName.setFixedHeight(28)
218  self.settingsContainer.layout().addRow(label, self.txtFileName)
219 
220  # Get text values
221  title_text = []
222  for i in range(0, self.text_fields):
223  if len(self.tspan_node[i].childNodes) > 0:
224  text = self.tspan_node[i].childNodes[0].data
225  title_text.append(text)
226 
227  # Create Label
228  label = QLabel()
229  label_line_text = _("Line %s:") % str(i + 1)
230  label.setText(label_line_text)
231  label.setToolTip(label_line_text)
232 
233  # create text editor for each text element in title
234  widget = QTextEdit()
235  widget.setText(_(text))
236  widget.setFixedHeight(28)
237  widget.textChanged.connect(functools.partial(self.txtLine_changed, widget))
238  self.settingsContainer.layout().addRow(label, widget)
239 
240 
241  # Add Font button
242  label = QLabel()
243  label.setText(_("Font:"))
244  label.setToolTip(_("Font:"))
245  self.btnFont = QPushButton()
246  self.btnFont.setText(_("Change Font"))
247  self.settingsContainer.layout().addRow(label, self.btnFont)
248  self.btnFont.clicked.connect(self.btnFont_clicked)
249 
250  # Add Text color button
251  label = QLabel()
252  label.setText(_("Text:"))
253  label.setToolTip(_("Text:"))
254  self.btnFontColor = QPushButton()
255  self.btnFontColor.setText(_("Text Color"))
256  self.settingsContainer.layout().addRow(label, self.btnFontColor)
257  self.btnFontColor.clicked.connect(self.btnFontColor_clicked)
258 
259  # Add Background color button
260  label = QLabel()
261  label.setText(_("Background:"))
262  label.setToolTip(_("Background:"))
263  self.btnBackgroundColor = QPushButton()
264  self.btnBackgroundColor.setText(_("Background Color"))
265  self.settingsContainer.layout().addRow(label, self.btnBackgroundColor)
266  self.btnBackgroundColor.clicked.connect(self.btnBackgroundColor_clicked)
267 
268  # Add Advanced Editor button
269  label = QLabel()
270  label.setText(_("Advanced:"))
271  label.setToolTip(_("Advanced:"))
272  self.btnAdvanced = QPushButton()
273  self.btnAdvanced.setText(_("Use Advanced Editor"))
274  self.settingsContainer.layout().addRow(label, self.btnAdvanced)
275  self.btnAdvanced.clicked.connect(self.btnAdvanced_clicked)
276 
277  # Update color buttons
280 
281  # Enable / Disable buttons based on # of text nodes
282  if len(title_text) >= 1:
283  self.btnFont.setEnabled(True)
284  self.btnFontColor.setEnabled(True)
285  self.btnBackgroundColor.setEnabled(True)
286  self.btnAdvanced.setEnabled(True)
287  else:
288  self.btnFont.setEnabled(False)
289  self.btnFontColor.setEnabled(False)
290 
291  ##
292  # writes a new svg file containing the user edited data
293  def writeToFile(self, xmldoc):
294 
295  if not self.filename.endswith("svg"):
296  self.filename = self.filename + ".svg"
297  try:
298  file = open(self.filename.encode('UTF-8'), "wb") # wb needed for windows support
299  file.write(bytes(xmldoc.toxml(), 'UTF-8'))
300  file.close()
301  except IOError as inst:
302  log.error("Error writing SVG title")
303 
305  app = get_app()
306  _ = app._tr
307 
308  # Get color from user
309  col = QColorDialog.getColor(Qt.white, self, _("Select a Color"),
310  QColorDialog.DontUseNativeDialog | QColorDialog.ShowAlphaChannel)
311 
312  # Update SVG colors
313  if col.isValid():
314  self.btnFontColor.setStyleSheet("background-color: %s" % col.name())
315  self.set_font_color_elements(col.name(), col.alphaF())
316 
317  # Something changed, so update temp SVG
318  self.writeToFile(self.xmldoc)
319 
320  # Display SVG again
321  self.display_svg()
322 
324  app = get_app()
325  _ = app._tr
326 
327  # Get color from user
328  col = QColorDialog.getColor(Qt.white, self, _("Select a Color"),
329  QColorDialog.DontUseNativeDialog | QColorDialog.ShowAlphaChannel)
330 
331  # Update SVG colors
332  if col.isValid():
333  self.btnBackgroundColor.setStyleSheet("background-color: %s" % col.name())
334  self.set_bg_style(col.name(), col.alphaF())
335 
336  # Something changed, so update temp SVG
337  self.writeToFile(self.xmldoc)
338 
339  # Display SVG again
340  self.display_svg()
341 
342  def btnFont_clicked(self):
343  app = get_app()
344  _ = app._tr
345 
346  # Get font from user
347  font, ok = QFontDialog.getFont(QFont(), caption=_("Change Font"))
348 
349  # Update SVG font
350  if ok:
351  fontinfo = QtGui.QFontInfo(font)
352  self.font_family = fontinfo.family()
353  self.font_style = fontinfo.styleName()
354  self.font_weight = fontinfo.weight()
355  self.set_font_style()
356 
357  # Something changed, so update temp SVG
358  self.writeToFile(self.xmldoc)
359 
360  # Display SVG again
361  self.display_svg()
362 
363  ##
364  # when passed a partial value, function will return the list index
365  def find_in_list(self, l, value):
366  for item in l:
367  if item.startswith(value):
368  return l.index(item)
369 
370  ##
371  # Updates the color shown on the font color button
373 
374  # Loop through each TEXT element
375  for node in self.text_node:
376 
377  # Get the value in the style attribute
378  s = node.attributes["style"].value
379 
380  # split the node so we can access each part
381  ar = s.split(";")
382  color = self.find_in_list(ar, "fill:")
383 
384  try:
385  # Parse the result
386  txt = ar[color]
387  color = txt[5:]
388  except:
389  # If the color was in an invalid format, try the next text element
390  continue
391 
392  opacity = self.find_in_list(ar, "opacity:")
393 
394  try:
395  # Parse the result
396  txt = ar[opacity]
397  opacity = float(txt[8:])
398  except:
399  pass
400 
401  # Default the font color to white if non-existing
402  if color == None:
403  color = "#FFFFFF"
404 
405  # Default the opacity to fully visible if non-existing
406  if opacity == None:
407  opacity = 1.0
408 
409  color = QtGui.QColor(color)
410  # Convert the opacity into the alpha value
411  alpha = int(opacity * 65535.0)
412  self.btnFontColor.setStyleSheet("background-color: %s; opacity %s" % (color.name(), alpha))
413 
414  ##
415  # Updates the color shown on the background color button
417 
418  if self.rect_node:
419 
420  # All backgrounds should be the first (index 0) rect tag in the svg
421  s = self.rect_node[0].attributes["style"].value
422 
423  # split the node so we can access each part
424  ar = s.split(";")
425 
426  color = self.find_in_list(ar, "fill:")
427 
428  try:
429  # Parse the result
430  txt = ar[color]
431  color = txt[5:]
432  except ValueError:
433  pass
434 
435  opacity = self.find_in_list(ar, "opacity:")
436 
437  try:
438  # Parse the result
439  txt = ar[opacity]
440  opacity = float(txt[8:])
441  except ValueError:
442  pass
443  except TypeError:
444  pass
445 
446  # Default the background color to black if non-existing
447  if color == None:
448  color = "#000000"
449 
450  # Default opacity to fully visible if non-existing
451  if opacity == None:
452  opacity = 1.0
453 
454  color = QtGui.QColor(color)
455  # Convert the opacity into the alpha value
456  alpha = int(opacity * 65535.0)
457  # Set the alpha value of the button
458  self.btnBackgroundColor.setStyleSheet("background-color: %s; opacity %s" % (color.name(), alpha))
459 
460  ##
461  # sets the font properties
462  def set_font_style(self):
463 
464  # Loop through each TEXT element
465  for text_child in self.text_node:
466  # set the style elements for the main text node
467  s = text_child.attributes["style"].value
468  # split the text node so we can access each part
469  ar = s.split(";")
470  # we need to find each element that we are changing, shouldn't assume
471  # they are in the same position in any given template.
472 
473  # ignoring font-weight, as not sure what it represents in Qt.
474  fs = self.find_in_list(ar, "font-style:")
475  ff = self.find_in_list(ar, "font-family:")
476  if fs:
477  ar[fs] = "font-style:" + self.font_style
478  if ff:
479  ar[ff] = "font-family:" + self.font_family
480  # rejoin the modified parts
481  t = ";"
482  self.title_style_string = t.join(ar)
483 
484  # set the text node
485  text_child.setAttribute("style", self.title_style_string)
486 
487  # Loop through each TSPAN
488  for tspan_child in self.tspan_node:
489  # set the style elements for the main text node
490  s = tspan_child.attributes["style"].value
491  # split the text node so we can access each part
492  ar = s.split(";")
493  # we need to find each element that we are changing, shouldn't assume
494  # they are in the same position in any given template.
495 
496  # ignoring font-weight, as not sure what it represents in Qt.
497  fs = self.find_in_list(ar, "font-style:")
498  ff = self.find_in_list(ar, "font-family:")
499  if fs:
500  ar[fs] = "font-style:" + self.font_style
501  if ff:
502  ar[ff] = "font-family:" + self.font_family
503  # rejoin the modified parts
504  t = ";"
505  self.title_style_string = t.join(ar)
506 
507  # set the text node
508  tspan_child.setAttribute("style", self.title_style_string)
509 
510  ##
511  # sets the background color
512  def set_bg_style(self, color, alpha):
513 
514  if self.rect_node:
515  # split the node so we can access each part
516  s = self.rect_node[0].attributes["style"].value
517  ar = s.split(";")
518  fill = self.find_in_list(ar, "fill:")
519  if fill == None:
520  ar.append("fill:" + color)
521  else:
522  ar[fill] = "fill:" + color
523 
524  opacity = self.find_in_list(ar, "opacity:")
525  if opacity == None:
526  ar.append("opacity:" + str(alpha))
527  else:
528  ar[opacity] = "opacity:" + str(alpha)
529 
530  # rejoin the modified parts
531  t = ";"
532  self.bg_style_string = t.join(ar)
533  # set the node in the xml doc
534  self.rect_node[0].setAttribute("style", self.bg_style_string)
535 
536  def set_font_color_elements(self, color, alpha):
537 
538  # Loop through each TEXT element
539  for text_child in self.text_node:
540 
541  # SET TEXT PROPERTIES
542  s = text_child.attributes["style"].value
543  # split the text node so we can access each part
544  ar = s.split(";")
545  fill = self.find_in_list(ar, "fill:")
546  if fill == None:
547  ar.append("fill:" + color)
548  else:
549  ar[fill] = "fill:" + color
550 
551  opacity = self.find_in_list(ar, "opacity:")
552  if opacity == None:
553  ar.append("opacity:" + str(alpha))
554  else:
555  ar[opacity] = "opacity:" + str(alpha)
556 
557  t = ";"
558  text_child.setAttribute("style", t.join(ar))
559 
560 
561  # Loop through each TSPAN
562  for tspan_child in self.tspan_node:
563 
564  # SET TSPAN PROPERTIES
565  s = tspan_child.attributes["style"].value
566  # split the text node so we can access each part
567  ar = s.split(";")
568  fill = self.find_in_list(ar, "fill:")
569  if fill == None:
570  ar.append("fill:" + color)
571  else:
572  ar[fill] = "fill:" + color
573  t = ";"
574  tspan_child.setAttribute("style", t.join(ar))
575 
576  def accept(self):
577  app = get_app()
578  _ = app._tr
579 
580  # If editing file, just update the existing file
581  if self.edit_file_path and not self.duplicate:
582  # Update filename
583  self.filename = self.edit_file_path
584 
585  # Overwrite title svg file
586  self.writeToFile(self.xmldoc)
587 
588  else:
589  # Create new title (with unique name)
590  file_name = "%s.svg" % self.txtFileName.toPlainText().strip()
591  file_path = os.path.join(info.ASSETS_PATH, file_name)
592 
593  if self.txtFileName.toPlainText().strip():
594  # Do we have unsaved changes?
595  if os.path.exists(file_path) and not self.edit_file_path:
596  ret = QMessageBox.question(self, _("Title Editor"), _("%s already exists.\nDo you want to replace it?") % file_name,
597  QMessageBox.No | QMessageBox.Yes)
598  if ret == QMessageBox.No:
599  # Do nothing
600  return
601 
602  # Update filename
603  self.filename = file_path
604 
605  # Save title
606  self.writeToFile(self.xmldoc)
607 
608  # Add file to project
609  self.add_file(self.filename)
610 
611  # Close window
612  super(TitleEditor, self).accept()
613 
614  def add_file(self, filepath):
615  path, filename = os.path.split(filepath)
616 
617  # Add file into project
618  app = get_app()
619  _ = get_app()._tr
620 
621  # Check for this path in our existing project data
622  file = File.get(path=filepath)
623 
624  # If this file is already found, exit
625  if file:
626  return
627 
628  # Load filepath in libopenshot clip object (which will try multiple readers to open it)
629  clip = openshot.Clip(filepath)
630 
631  # Get the JSON for the clip's internal reader
632  try:
633  reader = clip.Reader()
634  file_data = json.loads(reader.Json())
635 
636  # Set media type
637  file_data["media_type"] = "image"
638 
639  # Save new file to the project data
640  file = File()
641  file.data = file_data
642  file.save()
643  return True
644 
645  except:
646  # Handle exception
647  msg = QMessageBox()
648  msg.setText(_("{} is not a valid video, audio, or image file.".format(filename)))
649  msg.exec_()
650  return False
651 
653  _ = self.app._tr
654  # use an external editor to edit the image
655  try:
656  # Get settings
658 
659  # get the title editor executable path
660  prog = s.get("title_editor")
661 
662  # launch advanced title editor
663  # debug info
664  log.info("Advanced title editor command: {} {} ".format(prog, self.filename))
665 
666  p = subprocess.Popen([prog, self.filename])
667 
668  # wait for process to finish (so we can update the preview)
669  p.communicate()
670 
671  # update image preview
672  self.load_svg_template()
673  self.display_svg()
674 
675  except OSError:
676  msg = QMessageBox()
677  msg.setText(_("Please install {} to use this function").format(prog.capitalize()))
678  msg.exec_()
Title Editor Dialog.
Definition: title_editor.py:59
def set_bg_style
sets the background color
def track_metric_screen
Track a GUI screen being shown.
Definition: metrics.py:96
def get_app
Returns the current QApplication instance of OpenShot.
Definition: app.py:55
def set_font_style
sets the font properties
def load_svg_template
Load an SVG title and init all textboxes and controls.
def find_in_list
when passed a partial value, function will return the list index
def update_background_color_button
Updates the color shown on the background color button.
def load_ui
Load a Qt *.ui file, and also load an XML parsed version.
Definition: ui_util.py:66
def update_font_color_button
Updates the color shown on the font color button.
def init_ui
Initialize all child widgets and action of a window or dialog.
Definition: ui_util.py:220
def writeToFile
writes a new svg file containing the user edited data
def get_settings
Get the current QApplication's settings instance.
Definition: settings.py:44