Parse theme from a custom simpler format

This commit is contained in:
miruka 2019-07-23 03:14:02 -04:00
parent cb1b95766c
commit 9397687122
9 changed files with 308 additions and 221 deletions

View File

@ -2,6 +2,7 @@
- `QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling)` - `QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling)`
- Refactoring - Refactoring
- Sendbox
- Use .mjs modules - Use .mjs modules
- SignIn/RememberAccount screens - SignIn/RememberAccount screens
- SignIn must be in a flickable - SignIn must be in a flickable
@ -14,8 +15,7 @@
- When qml syntax highlighting supports string interpolation, use them - When qml syntax highlighting supports string interpolation, use them
- Fixes - Fixes
- Wrong typing notification sending before alias completed - Message position after daybreak delegate
- Message after daybreak delegate
- Keyboard flicking against top/bottom edge - Keyboard flicking against top/bottom edge
- Don't strip user spacing in html - Don't strip user spacing in html
- Past events loading (limit 100) freezes the GUI - need to move upsert func - Past events loading (limit 100) freezes the GUI - need to move upsert func

View File

@ -3,7 +3,7 @@
import asyncio import asyncio
import random import random
from typing import Any, Dict, Optional, Set, Tuple from typing import Dict, Optional, Set, Tuple
from .app import App from .app import App
from .events import users from .events import users
@ -104,8 +104,13 @@ class Backend:
# General functions # General functions
async def load_settings(self) -> Tuple[Dict[str, Any], ...]: async def load_settings(self) -> tuple:
return (await self.ui_settings.read(), await self.ui_state.read()) from .config_files import Theme
settings = await self.ui_settings.read()
ui_state = await self.ui_state.read()
theme = await Theme(self, settings["theme"]).read()
return (settings, ui_state, theme)
async def request_user_update_event(self, user_id: str) -> None: async def request_user_update_event(self, user_id: str) -> None:

View File

@ -10,6 +10,7 @@ import aiofiles
from dataclasses import dataclass, field from dataclasses import dataclass, field
from .backend import Backend from .backend import Backend
from .theme_parser import convert_to_qml
JsonData = Dict[str, Any] JsonData = Dict[str, Any]
@ -20,14 +21,30 @@ WRITE_LOCK = asyncio.Lock()
class ConfigFile: class ConfigFile:
backend: Backend = field(repr=False) backend: Backend = field(repr=False)
filename: str = field() filename: str = field()
use_data_dir: bool = False
@property @property
def path(self) -> Path: def path(self) -> Path:
# pylint: disable=no-member # pylint: disable=no-member
dirs = self.backend.app.appdirs return Path(self.backend.app.appdirs.user_config_dir) / self.filename
to = dirs.user_data_dir if self.use_data_dir else dirs.user_config_dir
return Path(to) / self.filename
async def default_data(self):
return ""
async def read(self):
try:
return self.path.read_text()
except FileNotFoundError:
return await self.default_data()
async def write(self, data) -> None:
async with WRITE_LOCK:
self.path.parent.mkdir(parents=True, exist_ok=True)
async with aiofiles.open(self.path, "w") as new:
await new.write(data)
@dataclass @dataclass
@ -38,8 +55,8 @@ class JSONConfigFile(ConfigFile):
async def read(self) -> JsonData: async def read(self) -> JsonData:
try: try:
data = json.loads(self.path.read_text()) data = json.loads(await super().read())
except (json.JSONDecodeError, FileNotFoundError): except json.JSONDecodeError:
data = {} data = {}
return {**await self.default_data(), **data} return {**await self.default_data(), **data}
@ -47,12 +64,7 @@ class JSONConfigFile(ConfigFile):
async def write(self, data: JsonData) -> None: async def write(self, data: JsonData) -> None:
js = json.dumps(data, indent=4, ensure_ascii=False, sort_keys=True) js = json.dumps(data, indent=4, ensure_ascii=False, sort_keys=True)
await super().write(js)
async with WRITE_LOCK:
self.path.parent.mkdir(parents=True, exist_ok=True)
async with aiofiles.open(self.path, "w") as new:
await new.write(js)
@dataclass @dataclass
@ -90,14 +102,20 @@ class UISettings(JSONConfigFile):
async def default_data(self) -> JsonData: async def default_data(self) -> JsonData:
return { return {
"writeAliases": {} "theme": "Default.qpl",
"writeAliases": {},
} }
@dataclass @dataclass
class UIState(JSONConfigFile): class UIState(JSONConfigFile):
filename: str = "ui-state.json" filename: str = "ui-state.json"
use_data_dir: bool = True
@property
def path(self) -> Path:
# pylint: disable=no-member
return Path(self.backend.app.appdirs.user_data_dir) / self.filename
async def default_data(self) -> JsonData: async def default_data(self) -> JsonData:
return { return {
@ -107,3 +125,30 @@ class UIState(JSONConfigFile):
"pageProperties": {}, "pageProperties": {},
"sidePaneManualWidth": None, "sidePaneManualWidth": None,
} }
@dataclass
class Theme(ConfigFile):
@property
def path(self) -> Path:
# pylint: disable=no-member
data_dir = Path(self.backend.app.appdirs.user_data_dir)
user_file = data_dir / "themes" / self.filename
if user_file.exists():
return user_file
return Path("src") / "themes" / self.filename
async def default_data(self) -> str:
async with aiofiles.open("src/themes/Default.qpl", "r") as file:
return file.read()
async def read(self) -> str:
return convert_to_qml(await super().read())
async def write(self, data: str) -> None:
raise NotImplementedError()

View File

@ -0,0 +1,69 @@
# Copyright 2019 miruka
# This file is part of harmonyqml, licensed under LGPLv3.
import re
from typing import Generator
PROPERTY_TYPES = {"bool", "double", "int", "list", "real", "string", "url",
"var", "date", "point", "rect", "size", "color"}
def _add_property(line: str) -> str:
if re.match(r"^\s*[a-zA-Z0-9_]+\s*:$", line):
return re.sub(r"^(\s*)(\S*\s*):$",
r"\1readonly property QtObject \2: QtObject",
line)
types = "|".join(PROPERTY_TYPES)
if re.match(fr"^\s*({types}) [a-zA-Z\d_]+\s*:", line):
return re.sub(r"^(\s*)(\S*)", r"\1readonly property \2", line)
return line
def _process_lines(content: str) -> Generator[str, None, None]:
skip = False
indent = " " * 4
current_indent = 0
for line in content.split("\n"):
line = line.rstrip()
if not line.strip() or line.strip().startswith("//"):
continue
start_space_list = re.findall(r"^ +", line)
start_space = start_space_list[0] if start_space_list else ""
line_indents = len(re.findall(indent, start_space))
if not skip:
if line_indents > current_indent:
yield "%s{" % (indent * current_indent)
current_indent = line_indents
while line_indents < current_indent:
current_indent -= 1
yield "%s}" % (indent * current_indent)
line = _add_property(line)
yield line
skip = any((line.endswith(e) for e in "([{+\\,?:"))
while current_indent:
current_indent -= 1
yield "%s}" % (indent * current_indent)
def convert_to_qml(theme_content: str) -> str:
lines = [
"import QtQuick 2.12",
'import "utils.js" as Ut',
"QtObject {",
" id: theme",
]
lines += [f" {line}" for line in _process_lines(theme_content)]
lines += ["}"]
return "\n".join(lines)

View File

@ -105,7 +105,7 @@ HRectangle {
vals.reduce((a, b) => a.length > b.length ? a: b) vals.reduce((a, b) => a.length > b.length ? a: b)
let textNotStartsWithAnyAlias = let textNotStartsWithAnyAlias =
! vals.some(a => text.startsWith(a)) ! vals.some(a => a.startsWith(text))
let textContainsCharNotInAnyAlias = let textContainsCharNotInAnyAlias =
vals.every(a => text.split("").some(c => ! a.includes(c))) vals.every(a => text.split("").some(c => ! a.includes(c)))

View File

@ -53,9 +53,10 @@ Python {
call("APP.is_debug_on", [Qt.application.arguments], on => { call("APP.is_debug_on", [Qt.application.arguments], on => {
window.debug = on window.debug = on
callCoro("load_settings", [], ([settings, uiState]) => { callCoro("load_settings", [], ([settings, uiState, theme]) => {
window.settings = settings window.settings = settings
window.uiState = uiState window.uiState = uiState
window.theme = Qt.createQmlObject(theme, window, "theme")
callCoro("saved_accounts.any_saved", [], any => { callCoro("saved_accounts.any_saved", [], any => {
py.ready = true py.ready = true

View File

@ -1,194 +0,0 @@
// Copyright 2019 miruka
// This file is part of harmonyqml, licensed under LGPLv3.
import QtQuick 2.12
import "utils.js" as Ut
QtObject {
id: theme
property int minimumSupportedWidth: 240
property int minimumSupportedHeight: 120
property int contentIsWideAbove: 439
property int minimumSupportedWidthPlusSpacing: 240 + spacing * 2
property int minimumSupportedHeightPlusSpacing: 120 + spacing * 2
property int baseElementsHeight: 36
property int spacing: 8
property int animationDuration: 100
property QtObject fontSize: QtObject {
property int smallest: 6
property int smaller: 8
property int small: 13
property int normal: 16
property int big: 22
property int bigger: 32
property int biggest: 48
}
property QtObject fontFamily: QtObject {
property string sans: "SFNS Display"
property string serif: "Roboto Slab"
property string mono: "Hack"
}
property int radius: 5
property QtObject colors: QtObject {
property color background0: Ut.hsla(0, 0, 90, 0.5)
property color background1: Ut.hsla(0, 0, 90, 0.6)
property color background2: Ut.hsla(0, 0, 90, 0.7)
property color foreground: "black"
property color foregroundDim: Ut.hsl(0, 0, 20)
property color foregroundDim2: Ut.hsl(0, 0, 30)
property color foregroundError: Ut.hsl(342, 64, 32)
property color textBorder: Ut.hsla(0, 0, 0, 0.07)
property color accent: Ut.hsl(25, 60, 50)
property color accentDarker: Ut.hsl(25, 60, 35)
}
property QtObject controls: QtObject {
property QtObject button: QtObject {
property color background: colors.background2
}
property QtObject interactiveRectangle: QtObject {
property color background: "transparent"
property color hoveredBackground: Ut.hsla(0, 0, 0, 0.2)
property color pressedBackground: Ut.hsla(0, 0, 0, 0.4)
property color checkedBackground: Ut.hsla(0, 0, 0, 0.4)
}
property QtObject textField: QtObject {
property color background: colors.background2
property color border: "transparent"
property color focusedBackground: background
property color focusedBorder: colors.accent
property int borderWidth: 1
}
property QtObject textArea: QtObject {
property color background: colors.background2
}
}
property QtObject sidePane: QtObject {
property real autoWidthRatio: 0.33
property int maximumAutoWidth: 320
property int autoCollapseBelowWidth: 128
property int collapsedWidth: avatar.size
property int autoReduceBelowWindowWidth:
minimumSupportedWidthPlusSpacing + collapsedWidth
property color background: colors.background2
property QtObject account: QtObject {
property color background: Qt.lighter(colors.background2, 1.05)
}
property QtObject settingsButton: QtObject {
property color background: colors.background2
}
property QtObject filterRooms: QtObject {
property color background: colors.background2
}
}
property QtObject chat: QtObject {
property QtObject selectViewBar: QtObject {
property color background: colors.background2
}
property QtObject roomHeader: QtObject {
property color background: colors.background2
}
property QtObject eventList: QtObject {
property int ownEventsOnRightUnderWidth: 768
property color background: "transparent"
}
property QtObject message: QtObject {
property color ownBackground: Ut.hsla(25, 40, 82, 0.7)
property color background: colors.background2
property color body: colors.foreground
property color date: colors.foregroundDim
property color link: colors.accentDarker
// property color code: Ut.hsl(0, 0, 80)
// property color codeBackground: Ut.hsl(0, 0, 10)
property color code: Ut.hsl(265, 60, 35)
property color greenText: Ut.hsl(80, 60, 25)
property string styleSheet:
"a { color: " + link + " }" +
"code { font-family: " + fontFamily.mono + "; " +
"color: " + code + " }" +
"h1, h2 { font-weight: normal }" +
"h6 { font-size: small }" +
".greentext { color: " + greenText + " }"
property string styleInclude:
'<style type"text/css">\n' + styleSheet + '\n</style>\n'
}
property QtObject daybreak: QtObject {
property color background: colors.background2
property color foreground: colors.foreground
property int radius: theme.radius
}
property QtObject inviteBanner: QtObject {
property color background: colors.background2
}
property QtObject leftBanner: QtObject {
property color background: colors.background2
}
property QtObject unknownDevices: QtObject {
property color background: colors.background2
}
property QtObject typingMembers: QtObject {
property color background: colors.background1
}
property QtObject sendBox: QtObject {
property color background: colors.background2
}
}
property color pageHeadersBackground: colors.background2
property QtObject box: QtObject {
property color background: colors.background0
property int radius: theme.radius
}
property QtObject avatar: QtObject {
property int size: baseElementsHeight
property int radius: theme.radius
property color letter: "white"
property QtObject background: QtObject {
property real saturation: 0.22
property real lightness: 0.5
property real alpha: 1
property color unknown: Ut.hsl(0, 0, 22)
}
}
property QtObject displayName: QtObject {
property real saturation: 0.32
property real lightness: 0.3
}
}

View File

@ -8,8 +8,8 @@ import "Models"
ApplicationWindow { ApplicationWindow {
id: window id: window
minimumWidth: theme.minimumSupportedWidth minimumWidth: theme ? theme.minimumSupportedWidth : 240
minimumHeight: theme.minimumSupportedHeight minimumHeight: theme ? theme.minimumSupportedHeight : 120
width: 640 width: 640
height: 480 height: 480
visible: true visible: true
@ -34,7 +34,8 @@ ApplicationWindow {
property var uiState: ({}) property var uiState: ({})
onUiStateChanged: py.saveConfig("ui_state", uiState) onUiStateChanged: py.saveConfig("ui_state", uiState)
Theme { id: theme } property var theme: null
Shortcuts { id: shortcuts} Shortcuts { id: shortcuts}
Python { id: py } Python { id: py }

160
src/themes/Default.qpl Normal file
View File

@ -0,0 +1,160 @@
// Copyright 2019 miruka
// This file is part of harmonyqml, licensed under LGPLv3.
// vim: syntax=qml
int minimumSupportedWidth: 240
int minimumSupportedHeight: 120
int contentIsWideAbove: 439
int minimumSupportedWidthPlusSpacing: 240 + spacing * 2
int minimumSupportedHeightPlusSpacing: 120 + spacing * 2
int baseElementsHeight: 36
int spacing: 8
int radius: 5
int animationDuration: 100
color pageHeadersBackground: colors.background2
fontSize:
int smallest: 6
int smaller: 8
int small: 13
int normal: 16
int big: 22
int bigger: 32
int biggest: 48
fontFamily:
string sans: "SFNS Display"
string serif: "Roboto Slab"
string mono: "Hack"
colors:
color background0: Ut.hsla(0, 0, 90, 0.5)
color background1: Ut.hsla(0, 0, 90, 0.6)
color background2: Ut.hsla(0, 0, 90, 0.7)
color foreground: "black"
color foregroundDim: Ut.hsl(0, 0, 20)
color foregroundDim2: Ut.hsl(0, 0, 30)
color foregroundError: Ut.hsl(342, 64, 32)
color textBorder: Ut.hsla(0, 0, 0, 0.07)
color accent: Ut.hsl(25, 60, 50)
color accentDarker: Ut.hsl(25, 60, 35)
controls:
button:
color background: colors.background2
interactiveRectangle:
color background: "transparent"
color hoveredBackground: Ut.hsla(0, 0, 0, 0.2)
color pressedBackground: Ut.hsla(0, 0, 0, 0.4)
color checkedBackground: Ut.hsla(0, 0, 0, 0.4)
textField:
color background: colors.background2
color border: "transparent"
color focusedBackground: background
color focusedBorder: colors.accent
int borderWidth: 1
textArea:
color background: colors.background2
sidePane:
real autoWidthRatio: 0.33
int maximumAutoWidth: 320
int autoCollapseBelowWidth: 128
int collapsedWidth: avatar.size
int autoReduceBelowWindowWidth:
minimumSupportedWidthPlusSpacing + collapsedWidth
color background: colors.background2
account:
color background: Qt.lighter(colors.background2, 1.05)
settingsButton:
color background: colors.background2
filterRooms:
color background: colors.background2
chat:
selectViewBar:
color background: colors.background2
roomHeader:
color background: colors.background2
eventList:
int ownEventsOnRightUnderWidth: 768
color background: "transparent"
message:
color ownBackground: Ut.hsla(25, 40, 82, 0.7)
color background: colors.background2
color body: colors.foreground
color date: colors.foregroundDim
color link: colors.accentDarker
// color code: Ut.hsl(0, 0, 80)
// color codeBackground: Ut.hsl(0, 0, 10)
color code: Ut.hsl(265, 60, 35)
color greenText: Ut.hsl(80, 60, 25)
string styleSheet:
"a { color: " + link + " }" +
"code { font-family: " + fontFamily.mono + "; " +
"color: " + code + " }" +
"h1, h2 { font-weight: normal }" +
"h6 { font-size: small }" +
".greentext { color: " + greenText + " }"
string styleInclude:
'<style type"text/css">\n' + styleSheet + '\n</style>\n'
daybreak:
color background: colors.background2
color foreground: colors.foreground
int radius: theme.radius
inviteBanner:
color background: colors.background2
leftBanner:
color background: colors.background2
unknownDevices:
color background: colors.background2
typingMembers:
color background: colors.background1
sendBox:
color background: colors.background2
box:
color background: colors.background0
int radius: theme.radius
avatar:
int size: baseElementsHeight
int radius: theme.radius
color letter: "white"
background:
real saturation: 0.22
real lightness: 0.5
real alpha: 1
color unknown: Ut.hsl(0, 0, 22)
displayName:
real saturation: 0.32
real lightness: 0.3