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-2016 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 fnmatch
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 
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):
65 
66  # Create dialog class
67  QDialog.__init__(self)
68 
69  self.app = get_app()
70  self.project = self.app.project
71 
72  # Get translation object
73  _ = self.app._tr
74 
75  # Load UI from designer
76  ui_util.load_ui(self, self.ui_path)
77 
78  # Init UI
79  ui_util.init_ui(self)
80 
81  # Track metrics
82  track_metric_screen("title-screen")
83 
84  # Initialize variables
85  self.template_name = ""
86  imp = minidom.getDOMImplementation()
87  self.xmldoc = imp.createDocument(None, "any", None)
88 
89  self.bg_color_code = ""
90  self.font_color_code = "#ffffff"
91 
92  self.bg_style_string = ""
95 
96  self.font_weight = 'normal'
97  self.font_style = 'normal'
98 
99  self.new_title_text = ""
100  self.sub_title_text = ""
101  self.subTitle = False
102 
103  self.display_name = ""
104  self.font_family = "Bitstream Vera Sans"
105  self.tspan_node = None
106 
107  # Hide all textboxes
108  self.hide_textboxes()
109 
110  # Remove temp file (if found)
111  temp_svg_path = os.path.join(info.TITLE_PATH, "temp.svg")
112  if os.path.exists(temp_svg_path):
113  os.remove(temp_svg_path)
114 
115  # load the template files
116  self.cmbTemplate.addItem(_("<Select a template>"))
117 
118  # Add user-defined titles (if any)
119  for file in sorted(os.listdir(info.TITLE_PATH)):
120  # pretty up the filename for display purposes
121  if fnmatch.fnmatch(file, '*.svg'):
122  (fileName, fileExtension) = os.path.splitext(file)
123  self.cmbTemplate.addItem(fileName.replace("_", " "), os.path.join(info.TITLE_PATH, file))
124 
125  # Add built-in titles
126  self.template_dir = os.path.join(info.PATH, 'titles')
127  for file in sorted(os.listdir(self.template_dir)):
128  # pretty up the filename for display purposes
129  if fnmatch.fnmatch(file, '*.svg'):
130  (fileName, fileExtension) = os.path.splitext(file)
131  self.cmbTemplate.addItem(fileName.replace("_", " "), os.path.join(self.template_dir, file))
132 
133  # set event handlers
134  self.cmbTemplate.activated.connect(functools.partial(self.cmbTemplate_activated))
135  self.btnFontColor.clicked.connect(functools.partial(self.btnFontColor_clicked))
136  self.btnBackgroundColor.clicked.connect(functools.partial(self.btnBackgroundColor_clicked))
137  self.btnFont.clicked.connect(functools.partial(self.btnFont_clicked))
138  self.btnAdvanced.clicked.connect(functools.partial(self.btnAdvanced_clicked))
139  self.txtLine1.textChanged.connect(functools.partial(self.txtLine_changed))
140  self.txtLine2.textChanged.connect(functools.partial(self.txtLine_changed))
141  self.txtLine3.textChanged.connect(functools.partial(self.txtLine_changed))
142  self.txtLine4.textChanged.connect(functools.partial(self.txtLine_changed))
143  self.txtLine5.textChanged.connect(functools.partial(self.txtLine_changed))
144  self.txtLine6.textChanged.connect(functools.partial(self.txtLine_changed))
145 
146  def txtLine_changed(self):
147 
148  # Update text values in the SVG
149  text_list = []
150  text_list.append(self.txtLine1.toPlainText())
151  text_list.append(self.txtLine2.toPlainText())
152  text_list.append(self.txtLine3.toPlainText())
153  text_list.append(self.txtLine4.toPlainText())
154  text_list.append(self.txtLine5.toPlainText())
155  text_list.append(self.txtLine6.toPlainText())
156  for i in range(0, self.text_fields):
157  if len(self.tspan_node[i].childNodes) > 0 and i <= (len(text_list) - 1):
158  new_text_node = self.xmldoc.createTextNode(text_list[i])
159  old_text_node = self.tspan_node[i].childNodes[0]
160  self.tspan_node[i].removeChild(old_text_node)
161  # add new text node
162  self.tspan_node[i].appendChild(new_text_node)
163 
164  # Something changed, so update temp SVG
165  self.writeToFile(self.xmldoc)
166 
167  # Display SVG again
168  self.display_svg()
169 
170  ##
171  # Hide all text inputs
172  def hide_textboxes(self):
173  self.txtLine1.setVisible(False)
174  self.lblLine1.setVisible(False)
175  self.txtLine2.setVisible(False)
176  self.lblLine2.setVisible(False)
177  self.txtLine3.setVisible(False)
178  self.lblLine3.setVisible(False)
179  self.txtLine4.setVisible(False)
180  self.lblLine4.setVisible(False)
181  self.txtLine5.setVisible(False)
182  self.lblLine5.setVisible(False)
183  self.txtLine6.setVisible(False)
184  self.lblLine6.setVisible(False)
185 
186  ##
187  # Only show a certain number of text inputs
188  def show_textboxes(self, num_fields):
189 
190  if num_fields >= 1:
191  self.txtLine1.setEnabled(True)
192  self.txtLine1.setVisible(True)
193  self.lblLine1.setVisible(True)
194  if num_fields >= 2:
195  self.txtLine2.setEnabled(True)
196  self.txtLine2.setVisible(True)
197  self.lblLine2.setVisible(True)
198  if num_fields >= 3:
199  self.txtLine3.setEnabled(True)
200  self.txtLine3.setVisible(True)
201  self.lblLine3.setVisible(True)
202  if num_fields >= 4:
203  self.txtLine4.setEnabled(True)
204  self.txtLine4.setVisible(True)
205  self.lblLine4.setVisible(True)
206  if num_fields >= 5:
207  self.txtLine5.setEnabled(True)
208  self.txtLine5.setVisible(True)
209  self.lblLine5.setVisible(True)
210  if num_fields >= 6:
211  self.txtLine6.setEnabled(True)
212  self.txtLine6.setVisible(True)
213  self.lblLine6.setVisible(True)
214 
215  def display_svg(self):
216  scene = QGraphicsScene(self)
217  view = self.graphicsView
218  svg = QtGui.QPixmap(self.filename)
219  svg_scaled = svg.scaled(svg.width() / 4, svg.height() / 4, Qt.KeepAspectRatio)
220  scene.addPixmap(svg_scaled)
221  view.setScene(scene)
222  view.show()
223 
225  # reconstruct the filename from the modified display name
226  if self.cmbTemplate.currentIndex() > 0:
227  # ignore the 'select a template entry'
228  template = self.cmbTemplate.currentText()
229  template_path = self.cmbTemplate.itemData(self.cmbTemplate.currentIndex())
230 
231  # Create temp version of SVG title
232  self.filename = self.create_temp_title(template_path)
233 
234  # Load temp title
235  self.load_svg_template()
236 
237  # Display tmp title
238  self.display_svg()
239 
240  def create_temp_title(self, template_path):
241 
242  # Set temp file path
243  self.filename = os.path.join(info.TITLE_PATH, "temp.svg")
244 
245  # Copy template to temp file
246  shutil.copy(template_path, self.filename)
247 
248  # return temp path
249  return self.filename
250 
251  ##
252  # Load an SVG title and init all textboxes and controls
253  def load_svg_template(self):
254 
255  # parse the svg object
256  self.xmldoc = minidom.parse(self.filename)
257  # get the text elements
258  self.tspan_node = self.xmldoc.getElementsByTagName('tspan')
259  self.text_fields = len(self.tspan_node)
260 
261  # Hide all text inputs
262  self.hide_textboxes()
263  # Show the correct number of text inputs
264  self.show_textboxes(self.text_fields)
265 
266  # Get text nodes and rect nodes
267  self.text_node = self.xmldoc.getElementsByTagName('text')
268  self.rect_node = self.xmldoc.getElementsByTagName('rect')
269 
270  # Get text values
271  title_text = []
272  for i in range(0, self.text_fields):
273  if len(self.tspan_node[i].childNodes) > 0:
274  title_text.append(self.tspan_node[i].childNodes[0].data)
275 
276  # Set textbox values
277  num_fields = len(title_text)
278  if num_fields >= 1:
279  self.txtLine1.setText("")
280  self.txtLine1.setText(title_text[0])
281  if num_fields >= 2:
282  self.txtLine2.setText("")
283  self.txtLine2.setText(title_text[1])
284  if num_fields >= 3:
285  self.txtLine3.setText("")
286  self.txtLine3.setText(title_text[2])
287  if num_fields >= 4:
288  self.txtLine4.setText("")
289  self.txtLine4.setText(title_text[3])
290  if num_fields >= 5:
291  self.txtLine5.setText("")
292  self.txtLine5.setText(title_text[4])
293  if num_fields >= 6:
294  self.txtLine6.setText("")
295  self.txtLine6.setText(title_text[5])
296 
297  # Update color buttons
300 
301  # Enable / Disable buttons based on # of text nodes
302  if num_fields >= 1:
303  self.btnFont.setEnabled(True)
304  self.btnFontColor.setEnabled(True)
305  self.btnBackgroundColor.setEnabled(True)
306  self.btnAdvanced.setEnabled(True)
307  else:
308  self.btnFont.setEnabled(False)
309  self.btnFontColor.setEnabled(False)
310 
311  ##
312  # writes a new svg file containing the user edited data
313  def writeToFile(self, xmldoc):
314 
315  if not self.filename.endswith("svg"):
316  self.filename = self.filename + ".svg"
317  try:
318  file = open(self.filename.encode('UTF-8'), "wb") # wb needed for windows support
319  file.write(bytes(xmldoc.toxml(), 'UTF-8'))
320  file.close()
321  except IOError as inst:
322  log.error("Error writing SVG title")
323 
325  app = get_app()
326  _ = app._tr
327 
328  # Get color from user
329  col = QColorDialog.getColor(Qt.white, self, _("Select a Color"),
330  QColorDialog.DontUseNativeDialog | QColorDialog.ShowAlphaChannel)
331 
332  # Update SVG colors
333  if col.isValid():
334  self.btnFontColor.setStyleSheet("background-color: %s" % col.name())
335  self.set_font_color_elements(col.name(), col.alphaF())
336 
337  # Something changed, so update temp SVG
338  self.writeToFile(self.xmldoc)
339 
340  # Display SVG again
341  self.display_svg()
342 
344  app = get_app()
345  _ = app._tr
346 
347  # Get color from user
348  col = QColorDialog.getColor(Qt.white, self, _("Select a Color"),
349  QColorDialog.DontUseNativeDialog | QColorDialog.ShowAlphaChannel)
350 
351  # Update SVG colors
352  if col.isValid():
353  self.btnBackgroundColor.setStyleSheet("background-color: %s" % col.name())
354  self.set_bg_style(col.name(), col.alphaF())
355 
356  # Something changed, so update temp SVG
357  self.writeToFile(self.xmldoc)
358 
359  # Display SVG again
360  self.display_svg()
361 
362  def btnFont_clicked(self):
363  app = get_app()
364  _ = app._tr
365 
366  # Get font from user
367  font, ok = QFontDialog.getFont(QFont(), caption=_("Change Font"))
368 
369  # Update SVG font
370  if ok:
371  fontinfo = QtGui.QFontInfo(font)
372  self.font_family = fontinfo.family()
373  self.font_style = fontinfo.styleName()
374  self.font_weight = fontinfo.weight()
375  self.set_font_style()
376 
377  # Something changed, so update temp SVG
378  self.writeToFile(self.xmldoc)
379 
380  # Display SVG again
381  self.display_svg()
382 
383  ##
384  # when passed a partial value, function will return the list index
385  def find_in_list(self, l, value):
386  for item in l:
387  if item.startswith(value):
388  return l.index(item)
389 
390  ##
391  # Updates the color shown on the font color button
393 
394  # Loop through each TEXT element
395  for node in self.text_node:
396 
397  # Get the value in the style attribute
398  s = node.attributes["style"].value
399 
400  # split the node so we can access each part
401  ar = s.split(";")
402  color = self.find_in_list(ar, "fill:")
403 
404  try:
405  # Parse the result
406  txt = ar[color]
407  color = txt[5:]
408  except:
409  # If the color was in an invalid format, try the next text element
410  continue
411 
412  opacity = self.find_in_list(ar, "opacity:")
413 
414  try:
415  # Parse the result
416  txt = ar[opacity]
417  opacity = float(txt[8:])
418  except:
419  pass
420 
421  # Default the font color to white if non-existing
422  if color == None:
423  color = "#FFFFFF"
424 
425  # Default the opacity to fully visible if non-existing
426  if opacity == None:
427  opacity = 1.0
428 
429  color = QtGui.QColor(color)
430  # Convert the opacity into the alpha value
431  alpha = int(opacity * 65535.0)
432  self.btnFontColor.setStyleSheet("background-color: %s; opacity %s" % (color.name(), alpha))
433 
434  ##
435  # Updates the color shown on the background color button
437 
438  if self.rect_node:
439 
440  # All backgrounds should be the first (index 0) rect tag in the svg
441  s = self.rect_node[0].attributes["style"].value
442 
443  # split the node so we can access each part
444  ar = s.split(";")
445 
446  color = self.find_in_list(ar, "fill:")
447 
448  try:
449  # Parse the result
450  txt = ar[color]
451  color = txt[5:]
452  except ValueError:
453  pass
454 
455  opacity = self.find_in_list(ar, "opacity:")
456 
457  try:
458  # Parse the result
459  txt = ar[opacity]
460  opacity = float(txt[8:])
461  except ValueError:
462  pass
463  except TypeError:
464  pass
465 
466  # Default the background color to black if non-existing
467  if color == None:
468  color = "#000000"
469 
470  # Default opacity to fully visible if non-existing
471  if opacity == None:
472  opacity = 1.0
473 
474  color = QtGui.QColor(color)
475  # Convert the opacity into the alpha value
476  alpha = int(opacity * 65535.0)
477  # Set the alpha value of the button
478  self.btnBackgroundColor.setStyleSheet("background-color: %s; opacity %s" % (color.name(), alpha))
479 
480  ##
481  # sets the font properties
482  def set_font_style(self):
483 
484  # Loop through each TEXT element
485  for text_child in self.text_node:
486  # set the style elements for the main text node
487  s = text_child.attributes["style"].value
488  # split the text node so we can access each part
489  ar = s.split(";")
490  # we need to find each element that we are changing, shouldn't assume
491  # they are in the same position in any given template.
492 
493  # ignoring font-weight, as not sure what it represents in Qt.
494  fs = self.find_in_list(ar, "font-style:")
495  ff = self.find_in_list(ar, "font-family:")
496  ar[fs] = "font-style:" + self.font_style
497  ar[ff] = "font-family:" + self.font_family
498  # rejoin the modified parts
499  t = ";"
500  self.title_style_string = t.join(ar)
501 
502  # set the text node
503  text_child.setAttribute("style", self.title_style_string)
504 
505  # Loop through each TSPAN
506  for tspan_child in self.tspan_node:
507  # set the style elements for the main text node
508  s = tspan_child.attributes["style"].value
509  # split the text node so we can access each part
510  ar = s.split(";")
511  # we need to find each element that we are changing, shouldn't assume
512  # they are in the same position in any given template.
513 
514  # ignoring font-weight, as not sure what it represents in Qt.
515  fs = self.find_in_list(ar, "font-style:")
516  ff = self.find_in_list(ar, "font-family:")
517  ar[fs] = "font-style:" + self.font_style
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 modifed 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  # Get current project folder (if any)
597  project_path = get_app().project.current_filepath
598  default_folder = info.HOME_PATH
599  if project_path:
600  default_folder = os.path.dirname(project_path)
601 
602  # Init file path for new title
603  title_path = os.path.join(default_folder, "%s.svg" % _("New Title"))
604 
605  # Get file path for SVG title
606  file_path, file_type = QFileDialog.getSaveFileName(self, _("Save Title As..."), title_path, _("Scalable Vector Graphics (*.svg)"))
607 
608  if file_path:
609  # Append .svg (if not already there)
610  if not file_path.endswith("svg"):
611  file_path = file_path + ".svg"
612 
613  # Update filename
614  self.filename = file_path
615 
616  # Save title
617  self.writeToFile(self.xmldoc)
618 
619  # Add file to project
620  self.add_file(self.filename)
621 
622  # Close window
623  super(TitleEditor, self).accept()
624 
625  def add_file(self, filepath):
626  path, filename = os.path.split(filepath)
627 
628  # Add file into project
629  app = get_app()
630  _ = get_app()._tr
631 
632  # Check for this path in our existing project data
633  file = File.get(path=filepath)
634 
635  # If this file is already found, exit
636  if file:
637  return
638 
639  # Load filepath in libopenshot clip object (which will try multiple readers to open it)
640  clip = openshot.Clip(filepath)
641 
642  # Get the JSON for the clip's internal reader
643  try:
644  reader = clip.Reader()
645  file_data = json.loads(reader.Json())
646 
647  # Set media type
648  file_data["media_type"] = "image"
649 
650  # Save new file to the project data
651  file = File()
652  file.data = file_data
653  file.save()
654  return True
655 
656  except:
657  # Handle exception
658  msg = QMessageBox()
659  msg.setText(_("{} is not a valid video, audio, or image file.".format(filename)))
660  msg.exec_()
661  return False
662 
664  _ = self.app._tr
665  # use an external editor to edit the image
666  try:
667  # Get settings
669 
670  # get the title editor executable path
671  prog = s.get("title_editor")
672 
673  # launch advanced title editor
674  # debug info
675  log.info("Advanced title editor command: {} {} ".format(prog, self.filename))
676 
677  p = subprocess.Popen([prog, self.filename])
678 
679  # wait for process to finish (so we can update the preview)
680  p.communicate()
681 
682  # update image preview
683  self.load_svg_template()
684  self.display_svg()
685 
686  except OSError:
687  msg = QMessageBox()
688  msg.setText(_("Please install {} to use this function").format(prog.capitalize()))
689  msg.exec_()
Title Editor Dialog.
Definition: title_editor.py:59
def get_app()
Returns the current QApplication instance of OpenShot.
Definition: app.py:54
def update_background_color_button(self)
Updates the color shown on the background color button.
def create_temp_title(self, template_path)
def load_svg_template(self)
Load an SVG title and init all textboxes and controls.
def btnBackgroundColor_clicked(self)
def hide_textboxes(self)
Hide all text inputs.
def set_font_color_elements(self, color, alpha)
def find_in_list(self, l, value)
when passed a partial value, function will return the list index
def writeToFile(self, xmldoc)
writes a new svg file containing the user edited data
def get_settings()
Get the current QApplication&#39;s settings instance.
Definition: settings.py:43
def init_ui(window)
Initialize all child widgets and action of a window or dialog.
Definition: ui_util.py:200
def set_bg_style(self, color, alpha)
sets the background color
def track_metric_screen(screen_name)
Track a GUI screen being shown.
Definition: metrics.py:94
def load_ui(window, path)
Load a Qt *.ui file, and also load an XML parsed version.
Definition: ui_util.py:65
def show_textboxes(self, num_fields)
Only show a certain number of text inputs.
def update_font_color_button(self)
Updates the color shown on the font color button.
def add_file(self, filepath)
def set_font_style(self)
sets the font properties