2021-07-14 06:14:04 +00:00
|
|
|
'''
|
|
|
|
configure
|
|
|
|
=========
|
|
|
|
|
2021-07-16 01:28:48 +00:00
|
|
|
Configure icons, stylesheets, and resource files.
|
2021-07-14 06:14:04 +00:00
|
|
|
'''
|
|
|
|
|
2021-07-16 19:30:46 +00:00
|
|
|
import argparse
|
2021-07-17 03:07:12 +00:00
|
|
|
import glob
|
2021-07-16 02:09:27 +00:00
|
|
|
import json
|
2021-07-16 19:30:46 +00:00
|
|
|
import re
|
2021-07-14 06:14:04 +00:00
|
|
|
import os
|
|
|
|
|
|
|
|
home = os.path.dirname(os.path.realpath(__file__))
|
2021-07-16 19:30:46 +00:00
|
|
|
|
|
|
|
# Create our arguments.
|
|
|
|
parser = argparse.ArgumentParser(description='Styles to configure for a Qt application.')
|
|
|
|
parser.add_argument(
|
|
|
|
'--styles',
|
2021-07-17 03:07:12 +00:00
|
|
|
help='''comma-separate list of styles to configure. pass `all` to build all themes''',
|
2021-07-16 19:30:46 +00:00
|
|
|
default='light,dark',
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
'--resource',
|
|
|
|
help='''output resource file name''',
|
|
|
|
default='custom.qrc',
|
|
|
|
)
|
2021-07-14 06:14:04 +00:00
|
|
|
|
2021-07-16 02:09:27 +00:00
|
|
|
# List of all icons to configure.
|
2021-07-16 01:28:48 +00:00
|
|
|
icons = {
|
2021-07-14 06:14:04 +00:00
|
|
|
# Arrows
|
|
|
|
'down_arrow': {
|
2021-07-16 19:30:46 +00:00
|
|
|
'default': ['foreground:hex', 'foreground:opacity'],
|
|
|
|
'hover': ['highlight:hex', 'highlight:opacity'],
|
|
|
|
'disabled': ['midtone:light:hex', 'midtone:light:opacity'],
|
2021-07-14 06:14:04 +00:00
|
|
|
},
|
|
|
|
'left_arrow': {
|
|
|
|
'default': ['foreground'],
|
2021-07-16 19:30:46 +00:00
|
|
|
'disabled': ['midtone:light'],
|
2021-07-14 06:14:04 +00:00
|
|
|
},
|
|
|
|
'right_arrow': {
|
|
|
|
'default': ['foreground'],
|
2021-07-16 19:30:46 +00:00
|
|
|
'disabled': ['midtone:light'],
|
2021-07-14 06:14:04 +00:00
|
|
|
},
|
|
|
|
'up_arrow': {
|
2021-07-16 19:30:46 +00:00
|
|
|
'default': ['foreground:hex', 'foreground:opacity'],
|
|
|
|
'hover': ['highlight:hex', 'highlight:opacity'],
|
|
|
|
'disabled': ['midtone:light:hex', 'midtone:light:opacity'],
|
2021-07-14 06:14:04 +00:00
|
|
|
},
|
|
|
|
# Abstract buttons.
|
|
|
|
'checkbox_checked': {
|
2021-07-16 19:30:46 +00:00
|
|
|
'default': ['checkbox:light'],
|
2021-07-16 01:28:48 +00:00
|
|
|
'disabled': ['checkbox:disabled'],
|
2021-07-14 06:14:04 +00:00
|
|
|
},
|
|
|
|
'checkbox_indeterminate': {
|
2021-07-16 19:30:46 +00:00
|
|
|
'default': ['checkbox:light'],
|
2021-07-16 01:28:48 +00:00
|
|
|
'disabled': ['checkbox:disabled'],
|
2021-07-14 06:14:04 +00:00
|
|
|
},
|
|
|
|
'checkbox_unchecked': {
|
2021-07-16 19:30:46 +00:00
|
|
|
'default': ['checkbox:light'],
|
2021-07-16 01:28:48 +00:00
|
|
|
'disabled': ['checkbox:disabled'],
|
2021-07-14 06:14:04 +00:00
|
|
|
},
|
|
|
|
'radio_checked': {
|
2021-07-16 19:30:46 +00:00
|
|
|
'default': ['checkbox:light'],
|
2021-07-16 01:28:48 +00:00
|
|
|
'disabled': ['checkbox:disabled'],
|
2021-07-14 06:14:04 +00:00
|
|
|
},
|
|
|
|
'radio_unchecked': {
|
2021-07-16 19:30:46 +00:00
|
|
|
'default': ['checkbox:light'],
|
2021-07-16 01:28:48 +00:00
|
|
|
'disabled': ['checkbox:disabled'],
|
2021-07-14 06:14:04 +00:00
|
|
|
},
|
|
|
|
# Dock/Tab widgets
|
|
|
|
'close': {
|
2021-07-16 19:30:46 +00:00
|
|
|
'default': ['midtone:dark:hex', 'midtone:dark:opacity'],
|
|
|
|
'hover': ['close:hover:hex', 'close:hover:opacity'],
|
|
|
|
'pressed': ['close:pressed:hex', 'close:pressed:opacity'],
|
2021-07-14 06:14:04 +00:00
|
|
|
},
|
|
|
|
'undock': {
|
2021-07-16 01:28:48 +00:00
|
|
|
'default': ['dock:float'],
|
2021-07-14 06:14:04 +00:00
|
|
|
},
|
|
|
|
'undock_hover': {
|
2021-07-16 01:28:48 +00:00
|
|
|
'default': ['dock:float', 'foreground'],
|
2021-07-14 06:14:04 +00:00
|
|
|
},
|
2021-07-14 21:57:20 +00:00
|
|
|
# Tree views.
|
|
|
|
'branch_open': {
|
2021-07-16 19:30:46 +00:00
|
|
|
'default': ['tree:hex', 'tree:opacity'],
|
|
|
|
'hover': ['highlight:hex', 'highlight:opacity'],
|
2021-07-14 21:57:20 +00:00
|
|
|
},
|
|
|
|
'branch_closed': {
|
2021-07-16 19:30:46 +00:00
|
|
|
'default': ['tree:hex', 'tree:opacity'],
|
|
|
|
'hover': ['highlight:hex', 'highlight:opacity'],
|
2021-07-14 21:57:20 +00:00
|
|
|
},
|
2021-07-15 01:22:33 +00:00
|
|
|
'branch_end': {
|
|
|
|
'default': ['tree'],
|
|
|
|
},
|
2021-07-14 21:57:20 +00:00
|
|
|
'branch_end_arrow': {
|
|
|
|
'default': ['tree'],
|
|
|
|
},
|
|
|
|
'branch_more': {
|
|
|
|
'default': ['tree'],
|
|
|
|
},
|
|
|
|
'branch_more_arrow': {
|
|
|
|
'default': ['tree'],
|
|
|
|
},
|
|
|
|
'vline': {
|
|
|
|
'default': ['tree'],
|
|
|
|
},
|
2021-07-15 21:44:59 +00:00
|
|
|
'calendar_next': {
|
|
|
|
'default': ['foreground'],
|
|
|
|
},
|
|
|
|
'calendar_previous': {
|
|
|
|
'default': ['foreground'],
|
|
|
|
},
|
2021-07-15 23:27:07 +00:00
|
|
|
'transparent': {
|
|
|
|
'default': [],
|
|
|
|
},
|
|
|
|
'hmovetoolbar': {
|
2021-07-16 19:30:46 +00:00
|
|
|
'default': ['midtone:light'],
|
2021-07-15 23:27:07 +00:00
|
|
|
},
|
|
|
|
'vmovetoolbar': {
|
2021-07-16 19:30:46 +00:00
|
|
|
'default': ['midtone:light'],
|
2021-07-15 23:27:07 +00:00
|
|
|
},
|
|
|
|
'hseptoolbar': {
|
2021-07-16 19:30:46 +00:00
|
|
|
'default': ['midtone:light'],
|
2021-07-15 23:27:07 +00:00
|
|
|
},
|
|
|
|
'vseptoolbar': {
|
2021-07-16 19:30:46 +00:00
|
|
|
'default': ['midtone:light'],
|
2021-07-15 23:27:07 +00:00
|
|
|
},
|
|
|
|
'sizegrip': {
|
2021-07-16 19:30:46 +00:00
|
|
|
'default': ['midtone:light'],
|
2021-07-17 03:07:12 +00:00
|
|
|
},
|
|
|
|
# Dialog icons
|
|
|
|
'dialog-cancel': {
|
|
|
|
'default': ['foreground'],
|
|
|
|
},
|
|
|
|
'dialog-close': {
|
|
|
|
'default': ['foreground'],
|
|
|
|
},
|
|
|
|
'dialog-ok': {
|
|
|
|
'default': ['foreground'],
|
|
|
|
},
|
|
|
|
'dialog-open': {
|
|
|
|
'default': ['foreground'],
|
|
|
|
},
|
|
|
|
'dialog-save': {
|
|
|
|
'default': ['foreground'],
|
|
|
|
},
|
|
|
|
'dialog-reset': {
|
|
|
|
'default': ['foreground'],
|
|
|
|
},
|
|
|
|
'dialog-help': {
|
|
|
|
'default': ['foreground'],
|
|
|
|
},
|
|
|
|
'dialog-no': {
|
|
|
|
'default': ['foreground'],
|
|
|
|
},
|
|
|
|
'dialog-discard': {
|
|
|
|
'default': ['foreground'],
|
|
|
|
},
|
|
|
|
# Message icons
|
|
|
|
'message-critical': {
|
|
|
|
'default': ['critical', 'foreground'],
|
|
|
|
},
|
|
|
|
'message-information': {
|
|
|
|
'default': ['information', 'foreground'],
|
|
|
|
},
|
|
|
|
'message-question': {
|
|
|
|
'default': ['question', 'foreground'],
|
|
|
|
},
|
|
|
|
'message-warning': {
|
|
|
|
'default': ['warning', 'foreground'],
|
|
|
|
},
|
2021-07-14 06:14:04 +00:00
|
|
|
}
|
|
|
|
|
2021-07-16 19:30:46 +00:00
|
|
|
def parse_hexcolor(color):
|
|
|
|
'''Parse a hexadecimal color.'''
|
|
|
|
|
|
|
|
# Have a hex color: can be 6 or 8 (non-standard) items.
|
|
|
|
color = color[1:]
|
|
|
|
if len(color) not in (6, 8):
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
red = int(color[:2], 16)
|
|
|
|
green = int(color[2:4], 16)
|
|
|
|
blue = int(color[4:6], 16)
|
|
|
|
alpha = 1.0
|
|
|
|
if len(color) == 8:
|
|
|
|
alpha = int(color[6:8], 16) / 100
|
|
|
|
return (red, green, blue, alpha)
|
|
|
|
|
|
|
|
def parse_rgba(color):
|
|
|
|
'''Parse an RGBA color.'''
|
|
|
|
|
|
|
|
# Match our rgba character. Note that this is
|
|
|
|
# First split the rgba components to get the inner stuff.
|
|
|
|
# Both rgb() and rgba() can have or omit an alpha layer.
|
|
|
|
rgba = re.match(r'^\s*rgba?\s*\((.*)\)\s*$', color).group(1)
|
|
|
|
split = re.split(r'(?:\s*,\s*)|\s+', rgba)
|
|
|
|
if len(split) not in (3, 4):
|
|
|
|
raise NotImplementedError
|
|
|
|
red = int(split[0])
|
|
|
|
green = int(split[1])
|
|
|
|
blue = int(split[2])
|
|
|
|
alpha = 1.0
|
|
|
|
if len(split) == 4:
|
|
|
|
alpha = float(split[3])
|
|
|
|
return (red, green, blue, alpha)
|
|
|
|
|
|
|
|
def parse_color(color):
|
|
|
|
'''Parse a color into the RGBA components.'''
|
|
|
|
|
|
|
|
if color.startswith('#'):
|
|
|
|
return parse_hexcolor(color)
|
|
|
|
elif color.startswith('rgb'):
|
|
|
|
return parse_rgba(color)
|
|
|
|
raise NotImplementedError
|
|
|
|
|
2021-07-14 06:14:04 +00:00
|
|
|
def replace(contents, colors, color_map):
|
|
|
|
'''Replace all template values.'''
|
|
|
|
|
2021-07-16 19:30:46 +00:00
|
|
|
for index, key in enumerate(colors):
|
2021-07-14 06:14:04 +00:00
|
|
|
sub = f'^{index}^'
|
2021-07-16 19:30:46 +00:00
|
|
|
# Need special handling if we have a hex or non:hex character.
|
|
|
|
if key.endswith(':hex'):
|
|
|
|
color = color_map[key[:-len(':hex')]]
|
|
|
|
rgb = [f"{i:02x}" for i in parse_color(color)[:3]]
|
|
|
|
value = f'#{"".join(rgb)}'
|
|
|
|
elif key.endswith(':opacity'):
|
|
|
|
color = color_map[key[:-len(':opacity')]]
|
|
|
|
value = str(parse_color(color)[3])
|
|
|
|
else:
|
|
|
|
value = color_map[key]
|
|
|
|
contents = contents.replace(sub, value)
|
2021-07-14 06:14:04 +00:00
|
|
|
return contents
|
|
|
|
|
2021-07-16 02:09:27 +00:00
|
|
|
def configure_icons(style, color_map):
|
2021-07-16 01:28:48 +00:00
|
|
|
'''Configure icons for a given style.'''
|
2021-07-14 06:14:04 +00:00
|
|
|
|
2021-07-16 01:28:48 +00:00
|
|
|
for icon, extensions in icons.items():
|
|
|
|
template = f'{home}/template/{icon}.svg.in'
|
2021-07-14 06:14:04 +00:00
|
|
|
template_contents = open(template).read()
|
|
|
|
for extension, colors in extensions.items():
|
|
|
|
contents = replace(template_contents, colors, color_map)
|
|
|
|
if extension == 'default':
|
2021-07-16 01:28:48 +00:00
|
|
|
filename = f'{home}/{style}/{icon}.svg'
|
2021-07-14 06:14:04 +00:00
|
|
|
else:
|
2021-07-16 01:28:48 +00:00
|
|
|
filename = f'{home}/{style}/{icon}_{extension}.svg'
|
2021-07-14 06:14:04 +00:00
|
|
|
with open(filename, 'w') as file:
|
|
|
|
file.write(contents)
|
|
|
|
|
2021-07-16 02:09:27 +00:00
|
|
|
def configure_stylesheet(style, color_map):
|
2021-07-16 01:28:48 +00:00
|
|
|
'''Configure the stylesheet for a given style.'''
|
|
|
|
|
|
|
|
contents = open(f'{home}/template/stylesheet.qss.in').read()
|
|
|
|
for key, color in color_map.items():
|
|
|
|
contents = contents.replace(f'^{key}^', color)
|
|
|
|
contents = contents.replace('^style^', style)
|
|
|
|
with open(f'{home}/{style}/stylesheet.qss', 'w') as file:
|
|
|
|
file.write(contents)
|
|
|
|
|
2021-07-16 02:09:27 +00:00
|
|
|
def configure_style(style, color_map):
|
2021-07-16 01:28:48 +00:00
|
|
|
'''Configure the icons and stylesheet for a given style.'''
|
|
|
|
|
|
|
|
os.makedirs(f'{home}/{style}', exist_ok=True)
|
2021-07-16 02:09:27 +00:00
|
|
|
configure_icons(style, color_map)
|
|
|
|
configure_stylesheet(style, color_map)
|
2021-07-16 01:28:48 +00:00
|
|
|
|
2021-07-16 19:30:46 +00:00
|
|
|
def write_xml(styles, path):
|
|
|
|
'''Simple QRC writer.'''
|
|
|
|
|
|
|
|
resources = []
|
|
|
|
for style in styles:
|
|
|
|
files = os.listdir(f'{home}/{style}')
|
|
|
|
resources += [f'{style}/{i}' for i in files]
|
|
|
|
with open(path, 'w') as file:
|
|
|
|
print('<RCC>', file=file)
|
|
|
|
print(' <qresource>', file=file)
|
|
|
|
for resource in sorted(resources):
|
|
|
|
print(f' <file>{resource}</file>', file=file)
|
|
|
|
print(' </qresource>', file=file)
|
|
|
|
print('</RCC>', file=file)
|
|
|
|
|
|
|
|
def configure(styles, path):
|
2021-07-16 01:28:48 +00:00
|
|
|
'''Configure all styles and write the files to a QRC file.'''
|
|
|
|
|
|
|
|
for style in styles:
|
2021-07-16 02:09:27 +00:00
|
|
|
# Note: we need comments for maintainability, so we
|
|
|
|
# can annotate what works and the rationale, but
|
|
|
|
# we don't want to prevent code from working without
|
|
|
|
# a complex parser, so we do something very simple:
|
|
|
|
# only remove lines starting with '//'.
|
2021-07-16 20:16:35 +00:00
|
|
|
with open(f'{home}/theme/{style}.json') as file:
|
2021-07-16 02:09:27 +00:00
|
|
|
lines = file.read().splitlines()
|
|
|
|
lines = [i for i in lines if not i.strip().startswith('//')]
|
|
|
|
color_map = json.loads('\n'.join(lines))
|
|
|
|
configure_style(style, color_map)
|
2021-07-16 01:28:48 +00:00
|
|
|
|
2021-07-16 19:30:46 +00:00
|
|
|
write_xml(styles, path)
|
|
|
|
|
2021-07-14 06:14:04 +00:00
|
|
|
if __name__ == '__main__':
|
2021-07-16 19:30:46 +00:00
|
|
|
args = parser.parse_args()
|
2021-07-17 03:07:12 +00:00
|
|
|
styles = args.styles.split(',')
|
|
|
|
if args.styles == 'all':
|
|
|
|
files = glob.glob(f'{home}/theme/*json')
|
|
|
|
styles = [os.path.splitext(os.path.basename(i))[0] for i in files]
|
|
|
|
configure(styles, args.resource)
|