diff --git a/TODO.md b/TODO.md
index cfa3aea8..cb7c7f36 100644
--- a/TODO.md
+++ b/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
diff --git a/src/python/backend.py b/src/python/backend.py
index 4bc2dc9c..db4cd675 100644
--- a/src/python/backend.py
+++ b/src/python/backend.py
@@ -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:
diff --git a/src/python/config_files.py b/src/python/config_files.py
index 76ba650e..0fad5c43 100644
--- a/src/python/config_files.py
+++ b/src/python/config_files.py
@@ -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]
@@ -18,16 +19,32 @@ WRITE_LOCK = asyncio.Lock()
@dataclass
class ConfigFile:
- backend: Backend = field(repr=False)
- filename: str = field()
- use_data_dir: bool = False
+ backend: Backend = field(repr=False)
+ filename: str = field()
@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
+ filename: str = "ui-state.json"
+
+ @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()
diff --git a/src/python/theme_parser.py b/src/python/theme_parser.py
new file mode 100644
index 00000000..4f80d8ca
--- /dev/null
+++ b/src/python/theme_parser.py
@@ -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)
diff --git a/src/qml/Chat/SendBox.qml b/src/qml/Chat/SendBox.qml
index bd42a440..9f8fce72 100644
--- a/src/qml/Chat/SendBox.qml
+++ b/src/qml/Chat/SendBox.qml
@@ -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)))
diff --git a/src/qml/Python.qml b/src/qml/Python.qml
index 2d76bd7d..f2e082f1 100644
--- a/src/qml/Python.qml
+++ b/src/qml/Python.qml
@@ -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
diff --git a/src/qml/Theme.qml b/src/qml/Theme.qml
deleted file mode 100644
index 47d959f5..00000000
--- a/src/qml/Theme.qml
+++ /dev/null
@@ -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:
- '\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
- }
-}
diff --git a/src/qml/Window.qml b/src/qml/Window.qml
index 5c89fa84..8d8b6d68 100644
--- a/src/qml/Window.qml
+++ b/src/qml/Window.qml
@@ -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 }
diff --git a/src/themes/Default.qpl b/src/themes/Default.qpl
new file mode 100644
index 00000000..75f5cd53
--- /dev/null
+++ b/src/themes/Default.qpl
@@ -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:
+ '\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