styleui.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. """
  2. Copyright (c) Contributors to the Open 3D Engine Project.
  3. For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. SPDX-License-Identifier: Apache-2.0 OR MIT
  5. """
  6. import argparse
  7. import os
  8. import tempfile
  9. import json
  10. import xml.etree.ElementTree as ET
  11. import subprocess
  12. from threading import Thread
  13. import time
  14. import re
  15. def main():
  16. args = parse_args()
  17. (ui_copy_file_path, inserted_variables, inserted_qss) = make_copy(args)
  18. monitor_thread = start_monitoring_for_changes(ui_copy_file_path, args, inserted_variables, inserted_qss)
  19. run_designer(args.designer_file_path, ui_copy_file_path)
  20. stop_monitor_for_changes(monitor_thread)
  21. delete_copy(ui_copy_file_path)
  22. def parse_args():
  23. script_directory_path = os.path.dirname(os.path.realpath(__file__))
  24. dev_directory_path = os.path.join(script_directory_path, '../..')
  25. editor_styles_directory_path = os.path.join(dev_directory_path, 'Editor/Styles')
  26. third_party_directory_path = os.path.join(dev_directory_path, '../3rdParty')
  27. if not os.path.isdir(third_party_directory_path):
  28. third_party_directory_path = os.path.join(dev_directory_path, '../../3rdParty')
  29. if not os.path.isdir(third_party_directory_path):
  30. third_party_directory_path = os.path.join(dev_directory_path, '../../../3rdParty')
  31. if not os.path.isdir(third_party_directory_path):
  32. raise RuntimeError('Could not find 3rdParty directory.')
  33. default_qss_file_path = os.path.normpath(os.path.join(editor_styles_directory_path, 'EditorStylesheet.qss'))
  34. default_variables_file_path = os.path.normpath(os.path.join(editor_styles_directory_path, 'EditorStylesheetVariables_Dark.json'))
  35. default_designer_file_path = os.path.normpath(os.path.join(third_party_directory_path, 'Qt/5.3.2/msvc2013_64/bin/designer.exe'))
  36. parser = argparse.ArgumentParser(
  37. prog='syleui',
  38. description='Inserts styles from an qss into an ui file for use in QT Designer.',
  39. formatter_class=argparse.RawDescriptionHelpFormatter,
  40. epilog='''
  41. This program creates a temporary copy of an ui file and starts QT Designer
  42. on that copy.
  43. The ui file provided to QT Designer will have the contents of the qss file
  44. inserted into the root widget's stylesheet (right click on the root widget
  45. and select "Change styleSheet..." to view the stylesheet).
  46. Whenever the ui file is saved in QT Designer, the inserted qss content is
  47. removed and the original ui file is updated.
  48. The qss file may contain variable references of the form @VARIABLE_NAME.
  49. Values for these variable are taken from a json format file with the following
  50. structure:
  51. {
  52. "StylesheetVariables" : {
  53. "VARIABLE_NAME_1" : "VARIABLE_VALUE_1",
  54. "VARIABLE_NAME_2" : "VARIABLE_VALUE_2",
  55. ...
  56. "VARIABLE_NAME_N" : "VARIABLE_VALUE_N"
  57. }
  58. }
  59. Variable references in the qss file will be replaced with a comment containing
  60. the variable name and the variable value (not in a comment). For example, @Foo
  61. is replaced with /*@Foo*/VALUE/*@*/. The trailing /*@*/ marks the end of the
  62. inserted value (it is used when restoring the variable names as described
  63. below).
  64. The variables file content is saved as a comment in the stylesheet. For
  65. example:
  66. /*** START INSERTED VARIABLES
  67. {
  68. "StylesheetVariables" : {
  69. "WindowBackgroundColor" : "#393a3c",
  70. "PanelBackgroundColor" : "#303030",
  71. ...
  72. }
  73. }
  74. END INSERTED VARIABLES ***/
  75. You can edit this content while in QT Designer. Changes you make will be saved
  76. back to the original variables file.
  77. The qss file content is saved between two comments in the stylesheet. For
  78. example:
  79. /*** START INSERTED STYLES ***/
  80. Foo { color: #000000 }
  81. /*** END INSERTED STYLES ***/
  82. You can edit this content while in QT Designer. Changtes you make will be
  83. saved back to the original qss file, with the variable replacements changed
  84. back to variable references. YOU MUST CHANNGE VARIABLE VALUES IN THE VARIABLES
  85. SECTION FOR THOSE CHANGES TO BE SAVED. You can change the name of a variable
  86. in comment to cause the qss to reference a different variable. If you add a
  87. new variable reference, be sure to include the /*@VARIABLE_NAME*/ and /*@*/
  88. comments before and after the variable's temporary value, respectively.
  89. Be sure not to modify the START and END comments in the stylesheet or the
  90. changes you make may not be saved property.
  91. ''')
  92. parser.add_argument('ui_file_path', metavar='UI_FILE_PATH', help='path and name of ui file')
  93. parser.add_argument('--qss', default=default_qss_file_path, dest='qss_file_path', help='Path and name of qss file. Default is: ' + default_qss_file_path)
  94. parser.add_argument('--variables', default=default_variables_file_path, dest='variables_file_path', help='Path and name of json file containing variable definitions. Default is: ' + default_variables_file_path)
  95. parser.add_argument('--designer', default=default_designer_file_path, dest='designer_file_path', help='Path and name of QT Designer executable file. Default is: ' + default_designer_file_path)
  96. args = parser.parse_args()
  97. return args
  98. def make_copy(args):
  99. qss_content = read_qss_file(args.qss_file_path)
  100. variables_content = read_variables_file(args.variables_file_path)
  101. qss_content = replace_variables(qss_content, variables_content)
  102. ui_content = read_ui_file(args.ui_file_path)
  103. (inserted_variables, inserted_qss) = insert_styles_into_ui(qss_content, variables_content, ui_content)
  104. ui_copy_file_path = write_ui_copy(ui_content)
  105. print 'copied', args.ui_file_path, 'to', ui_copy_file_path, 'with styles from', args.qss_file_path
  106. return (ui_copy_file_path, inserted_variables, inserted_qss)
  107. def read_qss_file(qss_file_path):
  108. with open(qss_file_path, 'r') as qss_file:
  109. qss_content = qss_file.read()
  110. #print 'qss_content', qss_content
  111. return qss_content
  112. def read_variables_file(variables_file_path):
  113. with open(variables_file_path, 'r') as variables_file:
  114. variables_content = json.load(variables_file)
  115. #print 'variables_content', variables_content
  116. return variables_content
  117. def replace_variables(qss_content, variables_content):
  118. for name, value in variables_content.get('StylesheetVariables', {}).iteritems():
  119. qss_content = qss_content.replace('@' + name, '/*@' + name + '*/' + value + '/*@*/')
  120. #print 'replace_variables', qss_content
  121. return qss_content
  122. def read_ui_file(ui_file_path):
  123. ui_content = ET.parse(ui_file_path)
  124. #print 'ui_content', ui_content
  125. return ui_content
  126. def insert_styles_into_ui(qss_content, variables_content, ui_content):
  127. property_value_element = ui_content.find("./widget/property[@name='styleSheet']/string")
  128. if property_value_element is None:
  129. property_element = ET.SubElement(ui_content.find("./widget"), 'property')
  130. property_element.set('name', 'styleSheet')
  131. property_value_element = ET.SubElement(property_element, 'string')
  132. property_value_element.set('notr', 'true')
  133. property_value_element.text = ''
  134. value = property_value_element.text
  135. (value, removed_variables) = remove_variables_from_property_value(value)
  136. (value, removed_qss) = remove_qss_from_property_value(value)
  137. (value, inserted_variables) = insert_variables_into_property_value(value, variables_content)
  138. (value, inserted_qss) = insert_qss_into_property_value(value, qss_content)
  139. property_value_element.text = value
  140. return (inserted_variables, inserted_qss)
  141. START_VARIABLES_MARKER = '\n/*** START INSERTED VARIABLES\n'
  142. END_VARIABLES_MARKER = '\nEND INSERTED VARIABLES ***/\n'
  143. def remove_variables_from_property_value(property_value):
  144. removed_variables = None
  145. start_index = property_value.find(START_VARIABLES_MARKER)
  146. if start_index != -1:
  147. end_index = property_value.find(END_VARIABLES_MARKER, start_index + len(START_VARIABLES_MARKER))
  148. if end_index != -1:
  149. removed_variables = property_value[start_index + len(START_VARIABLES_MARKER):end_index]
  150. property_value = property_value[:start_index] + property_value[end_index + len(END_VARIABLES_MARKER):]
  151. #print 'removed', property_value
  152. return (property_value, removed_variables)
  153. def insert_variables_into_property_value(property_value, variables_content):
  154. inserted_variables = json.dumps(variables_content, sort_keys=True, indent=4)
  155. property_value = property_value + START_VARIABLES_MARKER + inserted_variables + END_VARIABLES_MARKER
  156. #print 'inserted', property_value
  157. return (property_value, inserted_variables)
  158. START_QSS_MARKER = '\n/*** START INSERTED STYLES ***/\n'
  159. END_QSS_MARKER = '\n/*** END INSERTED STYLES ***/\n'
  160. def remove_qss_from_property_value(property_value):
  161. removed_qss = None
  162. start_index = property_value.find(START_QSS_MARKER)
  163. if start_index != -1:
  164. end_index = property_value.find(END_QSS_MARKER, start_index + len(START_QSS_MARKER))
  165. if end_index != -1:
  166. removed_qss = property_value[start_index + len(START_QSS_MARKER):end_index]
  167. property_value = property_value[:start_index] + property_value[end_index + len(END_QSS_MARKER):]
  168. #print 'removed', property_value
  169. return (property_value, removed_qss)
  170. def insert_qss_into_property_value(property_value, qss_content):
  171. property_value = property_value + START_QSS_MARKER + qss_content + END_QSS_MARKER
  172. #print 'inserted', property_value
  173. return (property_value, qss_content)
  174. def write_ui_copy(ui_content):
  175. (ui_copy_file, ui_copy_file_path) = tempfile.mkstemp(suffix='.ui', text=True)
  176. #print 'ui_copy_file_path', ui_copy_file_path
  177. ui_content.write(os.fdopen(ui_copy_file, 'w'))
  178. #os.close(ui_copy_file) ElementTree.write must be closing... fails if called
  179. return ui_copy_file_path
  180. def start_monitoring_for_changes(ui_copy_file_path, args, inserted_variables, inserted_qss):
  181. print 'monitoring', ui_copy_file_path, 'for changes'
  182. monitor_thread = Thread(target = monitor_for_changes, args=(ui_copy_file_path, args, inserted_variables, inserted_qss))
  183. monitor_thread.start()
  184. return monitor_thread
  185. continue_monitoring = True
  186. def monitor_for_changes(ui_copy_file_path, args, inserted_variables, inserted_qss):
  187. # lets see if simple polling works ok... otherwise maybe use https://pythonhosted.org/watchdog/
  188. last_mtime = os.path.getmtime(ui_copy_file_path)
  189. global continue_monitoring
  190. while continue_monitoring:
  191. time.sleep(2) # seconds
  192. current_mtime = os.path.getmtime(ui_copy_file_path)
  193. if(last_mtime != current_mtime):
  194. last_mtime = current_mtime
  195. update_files(ui_copy_file_path, args, inserted_variables, inserted_qss)
  196. def update_files(ui_copy_file_path, args, inserted_variables, inserted_qss):
  197. ui_copy_content = read_ui_file(ui_copy_file_path)
  198. (removed_variables, removed_qss) = remove_styles_from_ui(ui_copy_content)
  199. update_ui(ui_copy_content, args.ui_file_path)
  200. if inserted_variables != removed_variables:
  201. update_variables(removed_variables, args.variables_file_path)
  202. if inserted_qss != removed_qss:
  203. update_qss(removed_qss, args.qss_file_path)
  204. def remove_styles_from_ui(ui_content):
  205. removed_variables = None
  206. removed_qss = None
  207. property_value_element = ui_content.find("./widget/property[@name='styleSheet']/string")
  208. if property_value_element is not None:
  209. value = property_value_element.text
  210. (value, removed_variables) = remove_variables_from_property_value(value)
  211. (value, removed_qss) = remove_qss_from_property_value(value)
  212. property_value_element.text = value
  213. return (removed_variables, removed_qss)
  214. def update_ui(ui_content, ui_file_path):
  215. print 'updating', ui_file_path, 'with changes'
  216. try:
  217. ui_content.write(ui_file_path)
  218. except Exception as e:
  219. print '\n*** WRITE FAILED', e
  220. parts = os.path.splitext(ui_file_path)
  221. temp_path = parts[0] + '_BACKUP' + parts[1]
  222. print '*** saving to', temp_path, 'instead\n'
  223. ui_content.write(temp_path)
  224. def update_variables(removed_variables, variables_file_path):
  225. print 'updating', variables_file_path, 'with changes'
  226. try:
  227. with open(variables_file_path, "w") as variables_file:
  228. variables_file.write(removed_variables)
  229. except Exception as e:
  230. print '\n*** WRITE FAILED', e
  231. parts = os.path.splitext(variables_file_path)
  232. temp_path = parts[0] + '_BACKUP' + parts[1]
  233. print '*** saving to', temp_path, 'instead\n'
  234. with open(temp_path, "w") as variables_file:
  235. variables_file.write(removed_variables)
  236. def update_qss(removed_qss, qss_file_path):
  237. print 'updating', qss_file_path, 'with changes'
  238. removed_qss = re.sub(r'/\*@(\w+)\*/.*/\*@\*/', '@\g<1>', removed_qss)
  239. try:
  240. with open(qss_file_path, "w") as qss_file:
  241. qss_file.write(removed_qss)
  242. except Exception as e:
  243. print '\n*** WRITE FAILED', e
  244. parts = os.path.splitext(qss_file_path)
  245. temp_path = parts[0] + '_BACKUP' + parts[1]
  246. print '*** saving to', temp_path, 'instead\n'
  247. with open(temp_path, "w") as qss_file:
  248. qss_file.write(removed_qss)
  249. def stop_monitor_for_changes(monitor_thread):
  250. print 'stopping change monitor'
  251. global continue_monitoring
  252. continue_monitoring = False
  253. monitor_thread.join()
  254. def run_designer(designer_file_path, ui_copy_file_path):
  255. print 'starting designer with', ui_copy_file_path
  256. subprocess.call([designer_file_path, ui_copy_file_path])
  257. print 'designer exited'
  258. def delete_copy(ui_copy_file_path):
  259. print 'deleting', ui_copy_file_path
  260. os.remove(ui_copy_file_path)
  261. main()