Parse theme from a custom simpler format
This commit is contained in:
parent
cb1b95766c
commit
9397687122
4
TODO.md
4
TODO.md
|
@ -2,6 +2,7 @@
|
|||
- `QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling)`
|
||||
|
||||
- Refactoring
|
||||
- Sendbox
|
||||
- Use .mjs modules
|
||||
- SignIn/RememberAccount screens
|
||||
- SignIn must be in a flickable
|
||||
|
@ -14,8 +15,7 @@
|
|||
- When qml syntax highlighting supports string interpolation, use them
|
||||
|
||||
- Fixes
|
||||
- Wrong typing notification sending before alias completed
|
||||
- Message after daybreak delegate
|
||||
- Message position after daybreak delegate
|
||||
- Keyboard flicking against top/bottom edge
|
||||
- Don't strip user spacing in html
|
||||
- Past events loading (limit 100) freezes the GUI - need to move upsert func
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import asyncio
|
||||
import random
|
||||
from typing import Any, Dict, Optional, Set, Tuple
|
||||
from typing import Dict, Optional, Set, Tuple
|
||||
|
||||
from .app import App
|
||||
from .events import users
|
||||
|
@ -104,8 +104,13 @@ class Backend:
|
|||
|
||||
# General functions
|
||||
|
||||
async def load_settings(self) -> Tuple[Dict[str, Any], ...]:
|
||||
return (await self.ui_settings.read(), await self.ui_state.read())
|
||||
async def load_settings(self) -> tuple:
|
||||
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:
|
||||
|
|
|
@ -10,6 +10,7 @@ import aiofiles
|
|||
from dataclasses import dataclass, field
|
||||
|
||||
from .backend import Backend
|
||||
from .theme_parser import convert_to_qml
|
||||
|
||||
JsonData = Dict[str, Any]
|
||||
|
||||
|
@ -20,14 +21,30 @@ WRITE_LOCK = asyncio.Lock()
|
|||
class ConfigFile:
|
||||
backend: Backend = field(repr=False)
|
||||
filename: str = field()
|
||||
use_data_dir: bool = False
|
||||
|
||||
@property
|
||||
def path(self) -> Path:
|
||||
# pylint: disable=no-member
|
||||
dirs = self.backend.app.appdirs
|
||||
to = dirs.user_data_dir if self.use_data_dir else dirs.user_config_dir
|
||||
return Path(to) / self.filename
|
||||
return Path(self.backend.app.appdirs.user_config_dir) / 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
|
||||
|
@ -38,8 +55,8 @@ class JSONConfigFile(ConfigFile):
|
|||
|
||||
async def read(self) -> JsonData:
|
||||
try:
|
||||
data = json.loads(self.path.read_text())
|
||||
except (json.JSONDecodeError, FileNotFoundError):
|
||||
data = json.loads(await super().read())
|
||||
except json.JSONDecodeError:
|
||||
data = {}
|
||||
|
||||
return {**await self.default_data(), **data}
|
||||
|
@ -47,12 +64,7 @@ class JSONConfigFile(ConfigFile):
|
|||
|
||||
async def write(self, data: JsonData) -> None:
|
||||
js = json.dumps(data, indent=4, ensure_ascii=False, sort_keys=True)
|
||||
|
||||
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)
|
||||
await super().write(js)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -90,14 +102,20 @@ class UISettings(JSONConfigFile):
|
|||
|
||||
async def default_data(self) -> JsonData:
|
||||
return {
|
||||
"writeAliases": {}
|
||||
"theme": "Default.qpl",
|
||||
"writeAliases": {},
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class UIState(JSONConfigFile):
|
||||
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:
|
||||
return {
|
||||
|
@ -107,3 +125,30 @@ class UIState(JSONConfigFile):
|
|||
"pageProperties": {},
|
||||
"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()
|
||||
|
|
69
src/python/theme_parser.py
Normal file
69
src/python/theme_parser.py
Normal 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)
|
|
@ -105,7 +105,7 @@ HRectangle {
|
|||
vals.reduce((a, b) => a.length > b.length ? a: b)
|
||||
|
||||
let textNotStartsWithAnyAlias =
|
||||
! vals.some(a => text.startsWith(a))
|
||||
! vals.some(a => a.startsWith(text))
|
||||
|
||||
let textContainsCharNotInAnyAlias =
|
||||
vals.every(a => text.split("").some(c => ! a.includes(c)))
|
||||
|
|
|
@ -53,9 +53,10 @@ Python {
|
|||
call("APP.is_debug_on", [Qt.application.arguments], on => {
|
||||
window.debug = on
|
||||
|
||||
callCoro("load_settings", [], ([settings, uiState]) => {
|
||||
callCoro("load_settings", [], ([settings, uiState, theme]) => {
|
||||
window.settings = settings
|
||||
window.uiState = uiState
|
||||
window.theme = Qt.createQmlObject(theme, window, "theme")
|
||||
|
||||
callCoro("saved_accounts.any_saved", [], any => {
|
||||
py.ready = true
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -8,8 +8,8 @@ import "Models"
|
|||
|
||||
ApplicationWindow {
|
||||
id: window
|
||||
minimumWidth: theme.minimumSupportedWidth
|
||||
minimumHeight: theme.minimumSupportedHeight
|
||||
minimumWidth: theme ? theme.minimumSupportedWidth : 240
|
||||
minimumHeight: theme ? theme.minimumSupportedHeight : 120
|
||||
width: 640
|
||||
height: 480
|
||||
visible: true
|
||||
|
@ -34,7 +34,8 @@ ApplicationWindow {
|
|||
property var uiState: ({})
|
||||
onUiStateChanged: py.saveConfig("ui_state", uiState)
|
||||
|
||||
Theme { id: theme }
|
||||
property var theme: null
|
||||
|
||||
Shortcuts { id: shortcuts}
|
||||
Python { id: py }
|
||||
|
||||
|
|
160
src/themes/Default.qpl
Normal file
160
src/themes/Default.qpl
Normal 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
|
Loading…
Reference in New Issue
Block a user