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.
o3de/Tools/styleui/styleui.py

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()