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