You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
311 lines
14 KiB
Python
311 lines
14 KiB
Python
"""
|
|
Copyright (c) Contributors to the Open 3D Engine Project.
|
|
For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
|
|
|
SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import tempfile
|
|
import json
|
|
import xml.etree.ElementTree as ET
|
|
import subprocess
|
|
from threading import Thread
|
|
import time
|
|
import re
|
|
|
|
def main():
|
|
args = parse_args()
|
|
(ui_copy_file_path, inserted_variables, inserted_qss) = make_copy(args)
|
|
monitor_thread = start_monitoring_for_changes(ui_copy_file_path, args, inserted_variables, inserted_qss)
|
|
run_designer(args.designer_file_path, ui_copy_file_path)
|
|
stop_monitor_for_changes(monitor_thread)
|
|
delete_copy(ui_copy_file_path)
|
|
|
|
def parse_args():
|
|
|
|
script_directory_path = os.path.dirname(os.path.realpath(__file__))
|
|
dev_directory_path = os.path.join(script_directory_path, '../..')
|
|
editor_styles_directory_path = os.path.join(dev_directory_path, 'Editor/Styles')
|
|
|
|
third_party_directory_path = os.path.join(dev_directory_path, '../3rdParty')
|
|
if not os.path.isdir(third_party_directory_path):
|
|
third_party_directory_path = os.path.join(dev_directory_path, '../../3rdParty')
|
|
if not os.path.isdir(third_party_directory_path):
|
|
third_party_directory_path = os.path.join(dev_directory_path, '../../../3rdParty')
|
|
if not os.path.isdir(third_party_directory_path):
|
|
raise RuntimeError('Could not find 3rdParty directory.')
|
|
|
|
default_qss_file_path = os.path.normpath(os.path.join(editor_styles_directory_path, 'EditorStylesheet.qss'))
|
|
default_variables_file_path = os.path.normpath(os.path.join(editor_styles_directory_path, 'EditorStylesheetVariables_Dark.json'))
|
|
default_designer_file_path = os.path.normpath(os.path.join(third_party_directory_path, 'Qt/5.3.2/msvc2013_64/bin/designer.exe'))
|
|
|
|
parser = argparse.ArgumentParser(
|
|
prog='syleui',
|
|
description='Inserts styles from an qss into an ui file for use in QT Designer.',
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog='''
|
|
This program creates a temporary copy of an ui file and starts QT Designer
|
|
on that copy.
|
|
|
|
The ui file provided to QT Designer will have the contents of the qss file
|
|
inserted into the root widget's stylesheet (right click on the root widget
|
|
and select "Change styleSheet..." to view the stylesheet).
|
|
|
|
Whenever the ui file is saved in QT Designer, the inserted qss content is
|
|
removed and the original ui file is updated.
|
|
|
|
The qss file may contain variable references of the form @VARIABLE_NAME.
|
|
Values for these variable are taken from a json format file with the following
|
|
structure:
|
|
|
|
{
|
|
"StylesheetVariables" : {
|
|
"VARIABLE_NAME_1" : "VARIABLE_VALUE_1",
|
|
"VARIABLE_NAME_2" : "VARIABLE_VALUE_2",
|
|
...
|
|
"VARIABLE_NAME_N" : "VARIABLE_VALUE_N"
|
|
}
|
|
}
|
|
|
|
Variable references in the qss file will be replaced with a comment containing
|
|
the variable name and the variable value (not in a comment). For example, @Foo
|
|
is replaced with /*@Foo*/VALUE/*@*/. The trailing /*@*/ marks the end of the
|
|
inserted value (it is used when restoring the variable names as described
|
|
below).
|
|
|
|
The variables file content is saved as a comment in the stylesheet. For
|
|
example:
|
|
|
|
/*** START INSERTED VARIABLES
|
|
{
|
|
"StylesheetVariables" : {
|
|
"WindowBackgroundColor" : "#393a3c",
|
|
"PanelBackgroundColor" : "#303030",
|
|
...
|
|
}
|
|
}
|
|
END INSERTED VARIABLES ***/
|
|
|
|
You can edit this content while in QT Designer. Changes you make will be saved
|
|
back to the original variables file.
|
|
|
|
The qss file content is saved between two comments in the stylesheet. For
|
|
example:
|
|
|
|
/*** START INSERTED STYLES ***/
|
|
Foo { color: #000000 }
|
|
/*** END INSERTED STYLES ***/
|
|
|
|
You can edit this content while in QT Designer. Changtes you make will be
|
|
saved back to the original qss file, with the variable replacements changed
|
|
back to variable references. YOU MUST CHANNGE VARIABLE VALUES IN THE VARIABLES
|
|
SECTION FOR THOSE CHANGES TO BE SAVED. You can change the name of a variable
|
|
in comment to cause the qss to reference a different variable. If you add a
|
|
new variable reference, be sure to include the /*@VARIABLE_NAME*/ and /*@*/
|
|
comments before and after the variable's temporary value, respectively.
|
|
|
|
Be sure not to modify the START and END comments in the stylesheet or the
|
|
changes you make may not be saved property.
|
|
''')
|
|
|
|
parser.add_argument('ui_file_path', metavar='UI_FILE_PATH', help='path and name of ui file')
|
|
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)
|
|
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)
|
|
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)
|
|
|
|
args = parser.parse_args()
|
|
|
|
return args
|
|
|
|
def make_copy(args):
|
|
qss_content = read_qss_file(args.qss_file_path)
|
|
variables_content = read_variables_file(args.variables_file_path)
|
|
qss_content = replace_variables(qss_content, variables_content)
|
|
ui_content = read_ui_file(args.ui_file_path)
|
|
(inserted_variables, inserted_qss) = insert_styles_into_ui(qss_content, variables_content, ui_content)
|
|
ui_copy_file_path = write_ui_copy(ui_content)
|
|
print 'copied', args.ui_file_path, 'to', ui_copy_file_path, 'with styles from', args.qss_file_path
|
|
return (ui_copy_file_path, inserted_variables, inserted_qss)
|
|
|
|
def read_qss_file(qss_file_path):
|
|
with open(qss_file_path, 'r') as qss_file:
|
|
qss_content = qss_file.read()
|
|
#print 'qss_content', qss_content
|
|
return qss_content
|
|
|
|
def read_variables_file(variables_file_path):
|
|
with open(variables_file_path, 'r') as variables_file:
|
|
variables_content = json.load(variables_file)
|
|
#print 'variables_content', variables_content
|
|
return variables_content
|
|
|
|
def replace_variables(qss_content, variables_content):
|
|
for name, value in variables_content.get('StylesheetVariables', {}).iteritems():
|
|
qss_content = qss_content.replace('@' + name, '/*@' + name + '*/' + value + '/*@*/')
|
|
#print 'replace_variables', qss_content
|
|
return qss_content
|
|
|
|
def read_ui_file(ui_file_path):
|
|
ui_content = ET.parse(ui_file_path)
|
|
#print 'ui_content', ui_content
|
|
return ui_content
|
|
|
|
def insert_styles_into_ui(qss_content, variables_content, ui_content):
|
|
property_value_element = ui_content.find("./widget/property[@name='styleSheet']/string")
|
|
if property_value_element is None:
|
|
property_element = ET.SubElement(ui_content.find("./widget"), 'property')
|
|
property_element.set('name', 'styleSheet')
|
|
property_value_element = ET.SubElement(property_element, 'string')
|
|
property_value_element.set('notr', 'true')
|
|
property_value_element.text = ''
|
|
value = property_value_element.text
|
|
(value, removed_variables) = remove_variables_from_property_value(value)
|
|
(value, removed_qss) = remove_qss_from_property_value(value)
|
|
(value, inserted_variables) = insert_variables_into_property_value(value, variables_content)
|
|
(value, inserted_qss) = insert_qss_into_property_value(value, qss_content)
|
|
property_value_element.text = value
|
|
return (inserted_variables, inserted_qss)
|
|
|
|
START_VARIABLES_MARKER = '\n/*** START INSERTED VARIABLES\n'
|
|
END_VARIABLES_MARKER = '\nEND INSERTED VARIABLES ***/\n'
|
|
|
|
def remove_variables_from_property_value(property_value):
|
|
removed_variables = None
|
|
start_index = property_value.find(START_VARIABLES_MARKER)
|
|
if start_index != -1:
|
|
end_index = property_value.find(END_VARIABLES_MARKER, start_index + len(START_VARIABLES_MARKER))
|
|
if end_index != -1:
|
|
removed_variables = property_value[start_index + len(START_VARIABLES_MARKER):end_index]
|
|
property_value = property_value[:start_index] + property_value[end_index + len(END_VARIABLES_MARKER):]
|
|
#print 'removed', property_value
|
|
return (property_value, removed_variables)
|
|
|
|
def insert_variables_into_property_value(property_value, variables_content):
|
|
inserted_variables = json.dumps(variables_content, sort_keys=True, indent=4)
|
|
property_value = property_value + START_VARIABLES_MARKER + inserted_variables + END_VARIABLES_MARKER
|
|
#print 'inserted', property_value
|
|
return (property_value, inserted_variables)
|
|
|
|
START_QSS_MARKER = '\n/*** START INSERTED STYLES ***/\n'
|
|
END_QSS_MARKER = '\n/*** END INSERTED STYLES ***/\n'
|
|
|
|
def remove_qss_from_property_value(property_value):
|
|
removed_qss = None
|
|
start_index = property_value.find(START_QSS_MARKER)
|
|
if start_index != -1:
|
|
end_index = property_value.find(END_QSS_MARKER, start_index + len(START_QSS_MARKER))
|
|
if end_index != -1:
|
|
removed_qss = property_value[start_index + len(START_QSS_MARKER):end_index]
|
|
property_value = property_value[:start_index] + property_value[end_index + len(END_QSS_MARKER):]
|
|
#print 'removed', property_value
|
|
return (property_value, removed_qss)
|
|
|
|
def insert_qss_into_property_value(property_value, qss_content):
|
|
property_value = property_value + START_QSS_MARKER + qss_content + END_QSS_MARKER
|
|
#print 'inserted', property_value
|
|
return (property_value, qss_content)
|
|
|
|
def write_ui_copy(ui_content):
|
|
(ui_copy_file, ui_copy_file_path) = tempfile.mkstemp(suffix='.ui', text=True)
|
|
#print 'ui_copy_file_path', ui_copy_file_path
|
|
ui_content.write(os.fdopen(ui_copy_file, 'w'))
|
|
#os.close(ui_copy_file) ElementTree.write must be closing... fails if called
|
|
return ui_copy_file_path
|
|
|
|
def start_monitoring_for_changes(ui_copy_file_path, args, inserted_variables, inserted_qss):
|
|
print 'monitoring', ui_copy_file_path, 'for changes'
|
|
monitor_thread = Thread(target = monitor_for_changes, args=(ui_copy_file_path, args, inserted_variables, inserted_qss))
|
|
monitor_thread.start()
|
|
return monitor_thread
|
|
|
|
continue_monitoring = True
|
|
|
|
def monitor_for_changes(ui_copy_file_path, args, inserted_variables, inserted_qss):
|
|
# lets see if simple polling works ok... otherwise maybe use https://pythonhosted.org/watchdog/
|
|
last_mtime = os.path.getmtime(ui_copy_file_path)
|
|
global continue_monitoring
|
|
while continue_monitoring:
|
|
time.sleep(2) # seconds
|
|
current_mtime = os.path.getmtime(ui_copy_file_path)
|
|
if(last_mtime != current_mtime):
|
|
last_mtime = current_mtime
|
|
update_files(ui_copy_file_path, args, inserted_variables, inserted_qss)
|
|
|
|
def update_files(ui_copy_file_path, args, inserted_variables, inserted_qss):
|
|
ui_copy_content = read_ui_file(ui_copy_file_path)
|
|
(removed_variables, removed_qss) = remove_styles_from_ui(ui_copy_content)
|
|
update_ui(ui_copy_content, args.ui_file_path)
|
|
if inserted_variables != removed_variables:
|
|
update_variables(removed_variables, args.variables_file_path)
|
|
if inserted_qss != removed_qss:
|
|
update_qss(removed_qss, args.qss_file_path)
|
|
|
|
def remove_styles_from_ui(ui_content):
|
|
removed_variables = None
|
|
removed_qss = None
|
|
property_value_element = ui_content.find("./widget/property[@name='styleSheet']/string")
|
|
if property_value_element is not None:
|
|
value = property_value_element.text
|
|
(value, removed_variables) = remove_variables_from_property_value(value)
|
|
(value, removed_qss) = remove_qss_from_property_value(value)
|
|
property_value_element.text = value
|
|
return (removed_variables, removed_qss)
|
|
|
|
def update_ui(ui_content, ui_file_path):
|
|
print 'updating', ui_file_path, 'with changes'
|
|
try:
|
|
ui_content.write(ui_file_path)
|
|
except Exception as e:
|
|
print '\n*** WRITE FAILED', e
|
|
parts = os.path.splitext(ui_file_path)
|
|
temp_path = parts[0] + '_BACKUP' + parts[1]
|
|
print '*** saving to', temp_path, 'instead\n'
|
|
ui_content.write(temp_path)
|
|
|
|
def update_variables(removed_variables, variables_file_path):
|
|
print 'updating', variables_file_path, 'with changes'
|
|
try:
|
|
with open(variables_file_path, "w") as variables_file:
|
|
variables_file.write(removed_variables)
|
|
except Exception as e:
|
|
print '\n*** WRITE FAILED', e
|
|
parts = os.path.splitext(variables_file_path)
|
|
temp_path = parts[0] + '_BACKUP' + parts[1]
|
|
print '*** saving to', temp_path, 'instead\n'
|
|
with open(temp_path, "w") as variables_file:
|
|
variables_file.write(removed_variables)
|
|
|
|
def update_qss(removed_qss, qss_file_path):
|
|
print 'updating', qss_file_path, 'with changes'
|
|
removed_qss = re.sub(r'/\*@(\w+)\*/.*/\*@\*/', '@\g<1>', removed_qss)
|
|
try:
|
|
with open(qss_file_path, "w") as qss_file:
|
|
qss_file.write(removed_qss)
|
|
except Exception as e:
|
|
print '\n*** WRITE FAILED', e
|
|
parts = os.path.splitext(qss_file_path)
|
|
temp_path = parts[0] + '_BACKUP' + parts[1]
|
|
print '*** saving to', temp_path, 'instead\n'
|
|
with open(temp_path, "w") as qss_file:
|
|
qss_file.write(removed_qss)
|
|
|
|
def stop_monitor_for_changes(monitor_thread):
|
|
print 'stopping change monitor'
|
|
global continue_monitoring
|
|
continue_monitoring = False
|
|
monitor_thread.join()
|
|
|
|
def run_designer(designer_file_path, ui_copy_file_path):
|
|
print 'starting designer with', ui_copy_file_path
|
|
subprocess.call([designer_file_path, ui_copy_file_path])
|
|
print 'designer exited'
|
|
|
|
def delete_copy(ui_copy_file_path):
|
|
print 'deleting', ui_copy_file_path
|
|
os.remove(ui_copy_file_path)
|
|
|
|
main()
|
|
|