Use new PCN format for settings config file

This commit is contained in:
miruka 2020-10-07 20:12:32 -04:00
parent 6ce3059322
commit db12036372
52 changed files with 1305 additions and 409 deletions

View File

@ -1,5 +1,6 @@
# TODO # TODO
- login page password spinner
- Encrypted rooms don't show invites in member list after Mirage restart - Encrypted rooms don't show invites in member list after Mirage restart
- Room display name not updated when someone removes theirs - Room display name not updated when someone removes theirs
- Fix right margin of own `<image url>\n<image url>` messages - Fix right margin of own `<image url>\n<image url>` messages

View File

@ -109,7 +109,7 @@ class Backend:
self.settings = Settings(self) self.settings = Settings(self)
self.ui_state = UIState(self) self.ui_state = UIState(self)
self.history = History(self) self.history = History(self)
self.theme = Theme(self, self.settings["theme"]) self.theme = Theme(self, self.settings.General.theme)
self.clients: Dict[str, MatrixClient] = {} self.clients: Dict[str, MatrixClient] = {}
@ -427,13 +427,13 @@ class Backend:
return path return path
async def get_settings(self) -> Tuple[Settings, UIState, History, str]: async def get_settings(self) -> Tuple[dict, UIState, History, str]:
"""Return parsed user config files for QML.""" """Return parsed user config files for QML."""
return ( return (
self.settings.data, self.settings.qml_data,
self.ui_state.data, self.ui_state.qml_data,
self.history.data, self.history.qml_data,
self.theme.data, self.theme.qml_data,
) )

View File

@ -493,7 +493,7 @@ class MatrixClient(nio.AsyncClient):
utils.dict_update_recursive(first, self.low_limit_filter) utils.dict_update_recursive(first, self.low_limit_filter)
if self.backend.settings["hideUnknownEvents"]: if not self.backend.settings.Chat.show_unknown_events:
first["room"]["timeline"]["not_types"].extend( first["room"]["timeline"]["not_types"].extend(
self.no_unknown_events_filter self.no_unknown_events_filter
["room"]["timeline"]["not_types"], ["room"]["timeline"]["not_types"],
@ -1146,14 +1146,22 @@ class MatrixClient(nio.AsyncClient):
room = self.models[self.user_id, "rooms"][room_id] room = self.models[self.user_id, "rooms"][room_id]
room.bookmarked = not room.bookmarked room.bookmarked = not room.bookmarked
settings = self.backend.ui_settings settings = self.backend.settings
bookmarks = settings["roomBookmarkIDs"].setdefault(self.user_id, []) bookmarks = settings.RoomList.bookmarks
if room.bookmarked and room_id not in bookmarks: user_bookmarks = bookmarks.setdefault(self.user_id, [])
bookmarks.append(room_id)
elif not room.bookmarked and room_id in bookmarks:
bookmarks.remove(room_id)
await self.backend.ui_settings.write(self.backend.ui_settings._data) if room.bookmarked and room_id not in user_bookmarks:
user_bookmarks.append(room_id)
while not room.bookmarked and room_id in user_bookmarks:
user_bookmarks.remove(room_id)
# Changes inside dicts/lists aren't monitored, need to reassign
settings.RoomList.bookmarks = {
**bookmarks, self.user_id: user_bookmarks,
}
self.backend.settings.save()
async def room_forget(self, room_id: str) -> None: async def room_forget(self, room_id: str) -> None:
"""Leave a joined room (or decline an invite) and forget its history. """Leave a joined room (or decline an invite) and forget its history.
@ -1866,7 +1874,7 @@ class MatrixClient(nio.AsyncClient):
) )
unverified_devices = registered.unverified_devices unverified_devices = registered.unverified_devices
bookmarks = self.backend.ui_settings["roomBookmarkIDs"] bookmarks = self.backend.settings.RoomList.bookmarks
room_item = Room( room_item = Room(
id = room.room_id, id = room.room_id,
for_account = self.user_id, for_account = self.user_id,
@ -1912,7 +1920,7 @@ class MatrixClient(nio.AsyncClient):
local_unreads = local_unreads, local_unreads = local_unreads,
local_highlights = local_highlights, local_highlights = local_highlights,
lexical_sorting = self.backend.settings["lexicalRoomSorting"], lexical_sorting = self.backend.settings.RoomList.lexical_sort,
bookmarked = room.room_id in bookmarks.get(self.user_id, {}), bookmarked = room.room_id in bookmarks.get(self.user_id, {}),
) )

View File

@ -498,7 +498,7 @@ class NioCallbacks:
# Membership changes # Membership changes
if not prev or membership != prev_membership: if not prev or membership != prev_membership:
if self.client.backend.settings["hideMembershipEvents"]: if not self.client.backend.settings.Chat.show_membership_events:
return None return None
reason = escape( reason = escape(
@ -564,7 +564,7 @@ class NioCallbacks:
avatar_url = now.get("avatar_url") or "", avatar_url = now.get("avatar_url") or "",
) )
if self.client.backend.settings["hideProfileChangeEvents"]: if not self.client.backend.settings.Chat.show_profile_changes:
return None return None
return ( return (
@ -682,7 +682,7 @@ class NioCallbacks:
async def onUnknownEvent( async def onUnknownEvent(
self, room: nio.MatrixRoom, ev: nio.UnknownEvent, self, room: nio.MatrixRoom, ev: nio.UnknownEvent,
) -> None: ) -> None:
if self.client.backend.settings["hideUnknownEvents"]: if not self.client.backend.settings.Chat.show_unknown_events:
await self.client.register_nio_room(room) await self.client.register_nio_room(room)
return return

View File

@ -0,0 +1,4 @@
# Copyright Mirage authors & contributors <https://github.com/mirukana/mirage>
# SPDX-License-Identifier: LGPL-3.0-or-later
"""Parse and operate on PCN (Python Config Notation) files."""

View File

@ -0,0 +1,37 @@
from collections import UserDict
from typing import TYPE_CHECKING, Any, Dict, Iterator
if TYPE_CHECKING:
from .section import Section
PCN_GLOBALS: Dict[str, Any] = {}
class GlobalsDict(UserDict):
def __init__(self, section: "Section") -> None:
super().__init__()
self.section = section
@property
def full_dict(self) -> Dict[str, Any]:
return {
**PCN_GLOBALS,
**(self.section.root if self.section.root else {}),
**(self.section.root.globals if self.section.root else {}),
"self": self.section,
"parent": self.section.parent,
"root": self.section.parent,
**self.data,
}
def __getitem__(self, key: str) -> Any:
return self.full_dict[key]
def __iter__(self) -> Iterator[str]:
return iter(self.full_dict)
def __len__(self) -> int:
return len(self.full_dict)
def __repr__(self) -> str:
return repr(self.full_dict)

View File

@ -0,0 +1,52 @@
import re
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Callable, Dict, Type
if TYPE_CHECKING:
from .section import Section
TYPE_PROCESSORS: Dict[str, Callable[[Any], Any]] = {
"tuple": lambda v: tuple(v),
"set": lambda v: set(v),
}
class Unset:
pass
@dataclass
class Property:
name: str = field()
annotation: str = field()
expression: str = field()
section: "Section" = field()
value_override: Any = Unset
def __get__(self, obj: "Section", objtype: Type["Section"]) -> Any:
if not obj:
return self
if self.value_override is not Unset:
return self.value_override
env = obj.globals
result = eval(self.expression, dict(env), env) # nosec
return process_value(self.annotation, result)
def __set__(self, obj: "Section", value: Any) -> None:
self.value_override = value
obj._edited[self.name] = value
def process_value(annotation: str, value: Any) -> Any:
annotation = re.sub(r"\[.*\]$", "", annotation)
if annotation in TYPE_PROCESSORS:
return TYPE_PROCESSORS[annotation](value)
if annotation.lower() in TYPE_PROCESSORS:
return TYPE_PROCESSORS[annotation.lower()](value)
return value

395
src/backend/pcn/section.py Normal file
View File

@ -0,0 +1,395 @@
import textwrap
from collections import OrderedDict
from collections.abc import MutableMapping
from dataclasses import dataclass, field
from operator import attrgetter
from pathlib import Path
from typing import (
Any, Callable, ClassVar, Dict, Generator, List, Optional, Set, Tuple, Type,
Union,
)
import redbaron as red
from .globals_dict import GlobalsDict
from .property import Property, process_value
# TODO: docstrings, error handling, support @property, non-section classes
BUILTINS_DIR: Path = Path(__file__).parent.parent.parent.resolve() # src dir
@dataclass(repr=False, eq=False)
class Section(MutableMapping):
sections: ClassVar[Set[str]] = set()
methods: ClassVar[Set[str]] = set()
properties: ClassVar[Set[str]] = set()
order: ClassVar[Dict[str, None]] = OrderedDict()
source_path: Optional[Path] = None
root: Optional["Section"] = None
parent: Optional["Section"] = None
builtins_path: Path = BUILTINS_DIR
globals: GlobalsDict = field(init=False)
_edited: Dict[str, Any] = field(init=False, default_factory=dict)
def __init_subclass__(cls, **kwargs) -> None:
# Make these attributes not shared between Section and its subclasses
cls.sections = set()
cls.methods = set()
cls.properties = set()
cls.order = OrderedDict()
for parent_class in cls.__bases__:
if not issubclass(parent_class, Section):
continue
cls.sections |= parent_class.sections # union operator
cls.methods |= parent_class.methods
cls.properties |= parent_class.properties
cls.order.update(parent_class.order)
super().__init_subclass__(**kwargs) # type: ignore
def __post_init__(self) -> None:
self.globals = GlobalsDict(self)
def __getattr__(self, name: str) -> Union["Section", Any]:
# This method signature tells mypy about the dynamic attribute types
# we can access. The body is run for attributes that aren't found.
return super().__getattribute__(name)
def __setattr__(self, name: str, value: Any) -> None:
# This method tells mypy about the dynamic attribute types we can set.
# The body is also run when setting an existing or new attribute.
if name in self.__dataclass_fields__:
super().__setattr__(name, value)
return
if name in self.properties:
value = process_value(getattr(type(self), name).annotation, value)
if self[name] == value:
return
getattr(type(self), name).value_override = value
self._edited[name] = value
return
if name in self.properties:
return
if name in self.sections:
raise NotImplementedError(f"cannot overwrite section {name!r}")
if name in self.methods:
raise NotImplementedError(f"cannot overwrite method {name!r}")
raise NotImplementedError(f"cannot add new attribute {name!r}")
def __delattr__(self, name: str) -> None:
raise NotImplementedError(f"cannot delete existing attribute {name!r}")
def __getitem__(self, key: str) -> Any:
try:
return getattr(self, key)
except AttributeError as err:
raise KeyError(str(err))
def __setitem__(self, key: str, value: Union["Section", str]) -> None:
setattr(self, key, value)
def __delitem__(self, key: str) -> None:
delattr(self, key)
def __iter__(self) -> Generator[str, None, None]:
for attr_name in self.order:
yield attr_name
def __len__(self) -> int:
return len(self.order)
def __eq__(self, obj: Any) -> bool:
if not isinstance(obj, Section):
return False
if self.globals.data != obj.globals.data or self.order != obj.order:
return False
return not any(self[attr] != obj[attr] for attr in self.order)
def __repr__(self) -> str:
name: str = type(self).__name__
children: List[str] = []
content: str = ""
newline: bool = False
for attr_name in self.order:
value = getattr(self, attr_name)
if attr_name in self.sections:
before = "\n" if children else ""
newline = True
try:
children.append(f"{before}{value!r},")
except RecursionError as err:
name = type(value).__name__
children.append(f"{before}{name}(\n {err!r}\n),")
pass
elif attr_name in self.methods:
before = "\n" if children else ""
newline = True
children.append(f"{before}def {value.__name__}(…),")
elif attr_name in self.properties:
before = "\n" if newline else ""
newline = False
try:
children.append(f"{before}{attr_name} = {value!r},")
except RecursionError as err:
children.append(f"{before}{attr_name} = {err!r},")
else:
newline = False
if children:
content = "\n%s\n" % textwrap.indent("\n".join(children), " " * 4)
return f"{name}({content})"
@classmethod
def _register_set_attr(cls, name: str, add_to_set_name: str) -> None:
cls.methods.discard(name)
cls.properties.discard(name)
cls.sections.discard(name)
getattr(cls, add_to_set_name).add(name)
cls.order[name] = None
for subclass in cls.__subclasses__():
subclass._register_set_attr(name, add_to_set_name)
def _set_section(self, section: "Section") -> None:
name = type(section).__name__
if hasattr(self, name) and name not in self.order:
raise AttributeError(f"{name!r}: forbidden name")
if name in self.sections:
self[name].deep_merge(section)
return
self._register_set_attr(name, "sections")
setattr(type(self), name, section)
def _set_method(self, name: str, method: Callable) -> None:
if hasattr(self, name) and name not in self.order:
raise AttributeError(f"{name!r}: forbidden name")
self._register_set_attr(name, "methods")
setattr(type(self), name, method)
def _set_property(
self, name: str, annotation: str, expression: str,
) -> None:
if hasattr(self, name) and name not in self.order:
raise AttributeError(f"{name!r}: forbidden name")
prop = Property(name, annotation, expression, self)
self._register_set_attr(name, "properties")
setattr(type(self), name, prop)
def deep_merge(self, section2: "Section") -> None:
for key in section2:
if key in self.sections and key in section2.sections:
self.globals.data.update(section2.globals.data)
self[key].deep_merge(section2[key])
elif key in section2.sections:
self.globals.data.update(section2.globals.data)
new_type = type(key, (Section,), {})
instance = new_type(
source_path = self.source_path,
root = self.root or self,
parent = self,
builtins_path = self.builtins_path,
)
self._set_section(instance)
instance.deep_merge(section2[key])
elif key in section2.methods:
self._set_method(key, section2[key])
else:
prop2 = getattr(type(section2), key)
self._set_property(key, prop2.annotation, prop2.expression)
def include_file(self, path: Union[Path, str]) -> None:
if not Path(path).is_absolute() and self.source_path:
path = self.source_path.parent / path
self.deep_merge(Section.from_file(path))
def include_builtin(self, relative_path: Union[Path, str]) -> None:
self.deep_merge(Section.from_file(self.builtins_path / relative_path))
def as_dict(self, _section: Optional["Section"] = None) -> Dict[str, Any]:
dct = {}
section = self if _section is None else _section
for key, value in section.items():
if isinstance(value, Section):
dct[key] = self.as_dict(value)
else:
dct[key] = value
return dct
def edits_as_dict(
self, _section: Optional["Section"] = None,
) -> Dict[str, Any]:
warning = (
"This file is generated when settings are changed from the GUI, "
"and properties in it override the ones in the corresponding "
"PCN user config file. "
"If a property is gets changed in the PCN file, any corresponding "
"property override here is removed."
)
if _section is None:
section = self
dct = {"__comment": warning, "set": section._edited.copy()}
add_to = dct["set"]
else:
section = _section
dct = {
prop_name: (
getattr(type(section), prop_name).expression,
value_override,
)
for prop_name, value_override in section._edited.items()
}
add_to = dct
for name in section.sections:
edits = section.edits_as_dict(section[name])
if edits:
add_to[name] = edits # type: ignore
return dct
def deep_merge_edits(
self, edits: Dict[str, Any], has_expressions: bool = True,
) -> None:
if not self.parent: # this is Root
edits = edits.get("set", {})
for name, value in edits.items():
if name not in self:
continue
if isinstance(self[name], Section) and isinstance(value, dict):
self[name].deep_merge_edits(value, has_expressions)
elif not has_expressions:
self[name] = value
elif isinstance(value, (tuple, list)):
user_expression, gui_value = value
if getattr(type(self), name).expression == user_expression:
self[name] = gui_value
@classmethod
def from_source_code(
cls,
code: str,
path: Optional[Path] = None,
builtins: Optional[Path] = None,
*,
inherit: Tuple[Type["Section"], ...] = (),
node: Union[None, red.RedBaron, red.ClassNode] = None,
name: str = "Root",
root: Optional["Section"] = None,
parent: Optional["Section"] = None,
) -> "Section":
builtins = builtins or BUILTINS_DIR
section: Type["Section"] = type(name, inherit or (Section,), {})
instance: Section = section(path, root, parent, builtins)
node = node or red.RedBaron(code)
for child in node.node_list:
if isinstance(child, red.ClassNode):
root_arg = instance if root is None else root
child_inherit = []
for name in child.inherit_from.dumps().split(","):
name = name.strip()
if root_arg is not None and name:
child_inherit.append(type(attrgetter(name)(root_arg)))
instance._set_section(section.from_source_code(
code = code,
path = path,
builtins = builtins,
inherit = tuple(child_inherit),
node = child,
name = child.name,
root = root_arg,
parent = instance,
))
elif isinstance(child, red.AssignmentNode):
instance._set_property(
child.target.value,
child.annotation.dumps() if child.annotation else "",
child.value.dumps(),
)
else:
env = instance.globals
exec(child.dumps(), dict(env), env) # nosec
if isinstance(child, red.DefNode):
instance._set_method(child.name, env[child.name])
return instance
@classmethod
def from_file(
cls, path: Union[str, Path], builtins: Union[str, Path] = BUILTINS_DIR,
) -> "Section":
path = Path(path)
return Section.from_source_code(path.read_text(), path, Path(builtins))

View File

@ -6,20 +6,23 @@
import asyncio import asyncio
import json import json
import os import os
import platform import traceback
from collections.abc import MutableMapping from collections.abc import MutableMapping
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Any, ClassVar, Iterator, Optional, Tuple from typing import (
TYPE_CHECKING, Any, ClassVar, Dict, Iterator, Optional, Tuple,
)
import aiofiles import aiofiles
from watchgod import Change, awatch from watchgod import Change, awatch
import pyotherside import pyotherside
from .pyotherside_events import UserFileChanged from .pcn.section import Section
from .pyotherside_events import LoopException, UserFileChanged
from .theme_parser import convert_to_qml from .theme_parser import convert_to_qml
from .utils import atomic_write, dict_update_recursive from .utils import atomic_write, deep_serialize_for_qml, dict_update_recursive
if TYPE_CHECKING: if TYPE_CHECKING:
from .backend import Backend from .backend import Backend
@ -34,20 +37,21 @@ class UserFile:
backend: "Backend" = field(repr=False) backend: "Backend" = field(repr=False)
filename: str = field() filename: str = field()
data: Any = field(init=False, default_factory=dict) data: Any = field(init=False, default_factory=dict)
_need_write: bool = field(init=False, default=False) _need_write: bool = field(init=False, default=False)
_wrote: bool = field(init=False, default=False) _mtime: Optional[float] = field(init=False, default=None)
_reader: Optional[asyncio.Future] = field(init=False, default=None) _reader: Optional[asyncio.Future] = field(init=False, default=None)
_writer: Optional[asyncio.Future] = field(init=False, default=None) _writer: Optional[asyncio.Future] = field(init=False, default=None)
def __post_init__(self) -> None: def __post_init__(self) -> None:
try: try:
self.data, save = self.deserialized(self.path.read_text()) text_data = self.path.read_text()
except FileNotFoundError: except FileNotFoundError:
self.data = self.default_data self.data = self.default_data
self._need_write = self.create_missing self._need_write = self.create_missing
else: else:
self.data, save = self.deserialized(text_data)
if save: if save:
self.save() self.save()
@ -56,14 +60,24 @@ class UserFile:
@property @property
def path(self) -> Path: def path(self) -> Path:
"""Full path of the file, can exist or not exist.""" """Full path of the file to read, can exist or not exist."""
raise NotImplementedError() raise NotImplementedError()
@property
def write_path(self) -> Path:
"""Full path of the file to write, can exist or not exist."""
return self.path
@property @property
def default_data(self) -> Any: def default_data(self) -> Any:
"""Default deserialized content to use if the file doesn't exist.""" """Default deserialized content to use if the file doesn't exist."""
raise NotImplementedError() raise NotImplementedError()
@property
def qml_data(self) -> Any:
"""Data converted for usage in QML."""
return self.data
def deserialized(self, data: str) -> Tuple[Any, bool]: def deserialized(self, data: str) -> Tuple[Any, bool]:
"""Return parsed data from file text and whether to call `save()`.""" """Return parsed data from file text and whether to call `save()`."""
return (data, False) return (data, False)
@ -76,6 +90,15 @@ class UserFile:
"""Inform the disk writer coroutine that the data has changed.""" """Inform the disk writer coroutine that the data has changed."""
self._need_write = True self._need_write = True
def stop_watching(self) -> None:
"""Stop watching the on-disk file for changes."""
if self._reader:
self._reader.cancel()
if self._writer:
self._writer.cancel()
async def set_data(self, data: Any) -> None: async def set_data(self, data: Any) -> None:
"""Set `data` and call `save()`, conveniance method for QML.""" """Set `data` and call `save()`, conveniance method for QML."""
self.data = data self.data = data
@ -88,45 +111,57 @@ class UserFile:
await asyncio.sleep(1) await asyncio.sleep(1)
async for changes in awatch(self.path): async for changes in awatch(self.path):
ignored = 0 try:
ignored = 0
for change in changes: for change in changes:
if change[0] in (Change.added, Change.modified): mtime = self.path.stat().st_mtime
if self._need_write or self._wrote:
self._wrote = False
ignored += 1
continue
async with aiofiles.open(self.path) as file: if change[0] in (Change.added, Change.modified):
self.data, save = self.deserialized(await file.read()) if mtime == self._mtime:
ignored += 1
continue
if save: async with aiofiles.open(self.path) as file:
self.save() text = await file.read()
self.data, save = self.deserialized(text)
elif change[0] == Change.deleted: if save:
self._wrote = False self.save()
self.data = self.default_data
self._need_write = self.create_missing
if changes and ignored < len(changes): elif change[0] == Change.deleted:
UserFileChanged(type(self), self.data) self.data = self.default_data
self._need_write = self.create_missing
self._mtime = mtime
if changes and ignored < len(changes):
UserFileChanged(type(self), self.qml_data)
except Exception as err:
LoopException(str(err), err, traceback.format_exc().rstrip())
async def _start_writer(self) -> None: async def _start_writer(self) -> None:
"""Disk writer coroutine, update the file with a 1 second cooldown.""" """Disk writer coroutine, update the file with a 1 second cooldown."""
self.path.parent.mkdir(parents=True, exist_ok=True) self.write_path.parent.mkdir(parents=True, exist_ok=True)
while True: while True:
await asyncio.sleep(1) await asyncio.sleep(1)
if self._need_write: try:
async with atomic_write(self.path) as (new, done): if self._need_write:
await new.write(self.serialized()) async with atomic_write(self.write_path) as (new, done):
done() await new.write(self.serialized())
done()
self._need_write = False
self._mtime = self.write_path.stat().st_mtime
except Exception as err:
self._need_write = False self._need_write = False
self._wrote = True LoopException(str(err), err, traceback.format_exc().rstrip())
@dataclass @dataclass
@ -156,6 +191,7 @@ class UserDataFile(UserFile):
@dataclass @dataclass
class MappingFile(MutableMapping, UserFile): class MappingFile(MutableMapping, UserFile):
"""A file manipulable like a dict. `data` must be a mutable mapping.""" """A file manipulable like a dict. `data` must be a mutable mapping."""
def __getitem__(self, key: Any) -> Any: def __getitem__(self, key: Any) -> Any:
return self.data[key] return self.data[key]
@ -171,6 +207,22 @@ class MappingFile(MutableMapping, UserFile):
def __len__(self) -> int: def __len__(self) -> int:
return len(self.data) return len(self.data)
def __getattr__(self, key: Any) -> Any:
try:
return self.data[key]
except KeyError:
return super().__getattribute__(key)
def __setattr__(self, key: Any, value: Any) -> None:
if key in self.__dataclass_fields__:
super().__setattr__(key, value)
return
self.data[key] = value
def __delattr__(self, key: Any) -> None:
del self.data[key]
@dataclass @dataclass
class JSONFile(MappingFile): class JSONFile(MappingFile):
@ -180,7 +232,6 @@ class JSONFile(MappingFile):
def default_data(self) -> dict: def default_data(self) -> dict:
return {} return {}
def deserialized(self, data: str) -> Tuple[dict, bool]: def deserialized(self, data: str) -> Tuple[dict, bool]:
"""Return parsed data from file text and whether to call `save()`. """Return parsed data from file text and whether to call `save()`.
@ -193,12 +244,41 @@ class JSONFile(MappingFile):
dict_update_recursive(all_data, loaded) dict_update_recursive(all_data, loaded)
return (all_data, loaded != all_data) return (all_data, loaded != all_data)
def serialized(self) -> str: def serialized(self) -> str:
data = self.data data = self.data
return json.dumps(data, indent=4, ensure_ascii=False, sort_keys=True) return json.dumps(data, indent=4, ensure_ascii=False, sort_keys=True)
@dataclass
class PCNFile(MappingFile):
"""File stored in the PCN format, with machine edits in a separate JSON."""
create_missing = False
@property
def write_path(self) -> Path:
"""Full path of file where programatically-done edits are stored."""
return self.path.with_suffix(".gui.json")
@property
def qml_data(self) -> Dict[str, Any]:
return deep_serialize_for_qml(self.data.as_dict()) # type: ignore
def deserialized(self, data: str) -> Tuple[Section, bool]:
root = Section.from_source_code(data, self.path)
edits = self.write_path.read_text() if self.write_path.exists() else ""
root.deep_merge_edits(json.loads(edits))
return (root, False)
def serialized(self) -> str:
edits = self.data.edits_as_dict()
return json.dumps(edits, indent=4, ensure_ascii=False)
async def set_data(self, data: Dict[str, Any]) -> None:
self.data.deep_merge_edits({"set": data}, has_expressions=False)
self.save()
@dataclass @dataclass
class Accounts(ConfigFile, JSONFile): class Accounts(ConfigFile, JSONFile):
"""Config file for saved matrix accounts: user ID, access tokens, etc""" """Config file for saved matrix accounts: user ID, access tokens, etc"""
@ -209,7 +289,6 @@ class Accounts(ConfigFile, JSONFile):
"""Return for QML whether there are any accounts saved on disk.""" """Return for QML whether there are any accounts saved on disk."""
return bool(self.data) return bool(self.data)
async def add(self, user_id: str) -> None: async def add(self, user_id: str) -> None:
"""Add an account to the config and write it on disk. """Add an account to the config and write it on disk.
@ -233,7 +312,6 @@ class Accounts(ConfigFile, JSONFile):
}) })
self.save() self.save()
async def set( async def set(
self, self,
user_id: str, user_id: str,
@ -261,7 +339,6 @@ class Accounts(ConfigFile, JSONFile):
self.save() self.save()
async def forget(self, user_id: str) -> None: async def forget(self, user_id: str) -> None:
"""Delete an account from the config and write it on disk.""" """Delete an account from the config and write it on disk."""
@ -270,187 +347,33 @@ class Accounts(ConfigFile, JSONFile):
@dataclass @dataclass
class Settings(ConfigFile, JSONFile): class Settings(ConfigFile, PCNFile):
"""General config file for UI and backend settings""" """General config file for UI and backend settings"""
filename: str = "settings.json" filename: str = "settings.py"
@property @property
def default_data(self) -> dict: def default_data(self) -> Section:
def ctrl_or_osx_ctrl() -> str: root = Section.from_file("src/config/settings.py")
# Meta in Qt corresponds to Ctrl on OSX edits = "{}"
return "Meta" if platform.system() == "Darwin" else "Ctrl"
def alt_or_cmd() -> str: if self.write_path.exists():
# Ctrl in Qt corresponds to Cmd on OSX edits = self.write_path.read_text()
return "Ctrl" if platform.system() == "Darwin" else "Alt"
return { root.deep_merge_edits(json.loads(edits))
"alertOnMentionForMsec": -1, return root
"alertOnMessageForMsec": 0,
"alwaysCenterRoomHeader": False,
# "autoHideScrollBarsAfterMsec": 2000,
"beUnavailableAfterSecondsIdle": 60 * 10,
"centerRoomListOnClick": False,
"compactMode": False,
"clearRoomFilterOnEnter": True,
"clearRoomFilterOnEscape": True,
"clearMemberFilterOnEscape": True,
"closeMinimizesToTray": False,
"collapseSidePanesUnderWindowWidth": 450,
"enableKineticScrolling": True,
"hideProfileChangeEvents": True,
"hideMembershipEvents": False,
"hideUnknownEvents": True,
"kineticScrollingMaxSpeed": 2500,
"kineticScrollingDeceleration": 1500,
"lexicalRoomSorting": False,
"markRoomReadMsecDelay": 200,
"maxMessageCharactersPerLine": 65,
"nonKineticScrollingSpeed": 1.0,
"ownMessagesOnLeftAboveWidth": 895,
"theme": "Midnight.qpl",
"writeAliases": {},
"zoom": 1.0,
"roomBookmarkIDs": {},
"media": { def deserialized(self, data: str) -> Tuple[Section, bool]:
"autoLoad": True, section, save = super().deserialized(data)
"autoPlay": False,
"autoPlayGIF": True,
"autoHideOSDAfterMsec": 3000,
"defaultVolume": 100,
"openExternallyOnClick": False,
"startMuted": False,
},
"keys": {
"startPythonDebugger": ["Alt+Shift+D"],
"toggleDebugConsole": ["Alt+Shift+C", "F1"],
"zoomIn": ["Ctrl++"], if self and self.General.theme != section.General.theme:
"zoomOut": ["Ctrl+-"], self.backend.theme.stop_watching()
"zoomReset": ["Ctrl+="], self.backend.theme = Theme(
"toggleCompactMode": ["Ctrl+Alt+C"], self.backend, section.General.theme, # type: ignore
"toggleHideRoomPane": ["Ctrl+Alt+R"], )
"scrollUp": ["Alt+Up", "Alt+K"],
"scrollDown": ["Alt+Down", "Alt+J"],
"scrollPageUp": ["Alt+Ctrl+Up", "Alt+Ctrl+K", "PgUp"],
"scrollPageDown": ["Alt+Ctrl+Down", "Alt+Ctrl+J", "PgDown"],
"scrollToTop":
["Alt+Ctrl+Shift+Up", "Alt+Ctrl+Shift+K", "Home"],
"scrollToBottom":
["Alt+Ctrl+Shift+Down", "Alt+Ctrl+Shift+J", "End"],
"previousTab": ["Alt+Shift+Left", "Alt+Shift+H"],
"nextTab": ["Alt+Shift+Right", "Alt+Shift+L"],
"addNewAccount": ["Alt+Shift+A"],
"accountSettings": ["Alt+A"],
"addNewChat": ["Alt+C"],
"toggleFocusMainPane": ["Alt+F"],
"clearRoomFilter": ["Alt+Shift+F"],
"toggleCollapseAccount": ["Alt+O"],
"openPresenceMenu": ["Alt+P"],
"togglePresenceUnavailable": ["Alt+Ctrl+U", "Alt+Ctrl+A"],
"togglePresenceInvisible": ["Alt+Ctrl+I"],
"togglePresenceOffline": ["Alt+Ctrl+O"],
"goToLastPage": ["Ctrl+Tab"],
"goToPreviousAccount": ["Alt+Shift+N"],
"goToNextAccount": ["Alt+N"],
"goToPreviousRoom": ["Alt+Shift+Up", "Alt+Shift+K"],
"goToNextRoom": ["Alt+Shift+Down", "Alt+Shift+J"],
"goToPreviousUnreadRoom": ["Alt+Shift+U"],
"goToNextUnreadRoom": ["Alt+U"],
"goToPreviousMentionedRoom": ["Alt+Shift+M"],
"goToNextMentionedRoom": ["Alt+M"],
"focusAccountAtIndex": {
"01": f"{ctrl_or_osx_ctrl()}+1",
"02": f"{ctrl_or_osx_ctrl()}+2",
"03": f"{ctrl_or_osx_ctrl()}+3",
"04": f"{ctrl_or_osx_ctrl()}+4",
"05": f"{ctrl_or_osx_ctrl()}+5",
"06": f"{ctrl_or_osx_ctrl()}+6",
"07": f"{ctrl_or_osx_ctrl()}+7",
"08": f"{ctrl_or_osx_ctrl()}+8",
"09": f"{ctrl_or_osx_ctrl()}+9",
"10": f"{ctrl_or_osx_ctrl()}+0",
},
# On OSX, alt+numbers if used for symbols, use cmd instead
"focusRoomAtIndex": {
"01": f"{alt_or_cmd()}+1",
"02": f"{alt_or_cmd()}+2",
"03": f"{alt_or_cmd()}+3",
"04": f"{alt_or_cmd()}+4",
"05": f"{alt_or_cmd()}+5",
"06": f"{alt_or_cmd()}+6",
"07": f"{alt_or_cmd()}+7",
"08": f"{alt_or_cmd()}+8",
"09": f"{alt_or_cmd()}+9",
"10": f"{alt_or_cmd()}+0",
},
"unfocusOrDeselectAllMessages": ["Ctrl+D"],
"focusPreviousMessage": ["Ctrl+Up", "Ctrl+K"],
"focusNextMessage": ["Ctrl+Down", "Ctrl+J"],
"toggleSelectMessage": ["Ctrl+Space"],
"selectMessagesUntilHere": ["Ctrl+Shift+Space"],
"removeFocusedOrSelectedMessages": ["Ctrl+R", "Alt+Del"],
"replyToFocusedOrLastMessage": ["Ctrl+Q"], # Q → Quote
"debugFocusedMessage": ["Ctrl+Shift+D"],
"openMessagesLinksOrFiles": ["Ctrl+O"],
"openMessagesLinksOrFilesExternally": ["Ctrl+Shift+O"],
"copyFilesLocalPath": ["Ctrl+Shift+C"],
"clearRoomMessages": ["Ctrl+L"],
"sendFile": ["Alt+S"],
"sendFileFromPathInClipboard": ["Alt+Shift+S"],
"inviteToRoom": ["Alt+I"],
"leaveRoom": ["Alt+Escape"],
"forgetRoom": ["Alt+Shift+Escape"],
"toggleFocusRoomPane": ["Alt+R"],
"refreshDevices": ["Alt+R", "F5"],
"signOutCheckedOrAllDevices": ["Alt+S", "Delete"],
"imageViewer": {
"panLeft": ["H", "Left", "Alt+H", "Alt+Left"],
"panDown": ["J", "Down", "Alt+J", "Alt+Down"],
"panUp": ["K", "Up", "Alt+K", "Alt+Up"],
"panRight": ["L", "Right", "Alt+L", "Alt+Right"],
"zoomReset": ["Alt+Z", "=", "Ctrl+="],
"zoomOut": ["Shift+Z", "-", "Ctrl+-"],
"zoomIn": ["Z", "+", "Ctrl++"],
"rotateReset": ["Alt+R"],
"rotateLeft": ["Shift+R"],
"rotateRight": ["R"],
"resetSpeed": ["Alt+S"],
"previousSpeed": ["Shift+S"],
"nextSpeed": ["S"],
"pause": ["Space"],
"expand": ["E"],
"fullScreen": ["F", "F11", "Alt+Return", "Alt+Enter"],
"close": ["X", "Q"],
},
},
}
def deserialized(self, data: str) -> Tuple[dict, bool]:
dict_data, save = super().deserialized(data)
if "theme" in self and self["theme"] != dict_data["theme"]:
self.backend.theme = Theme(self.backend, dict_data["theme"])
UserFileChanged(Theme, self.backend.theme.data) UserFileChanged(Theme, self.backend.theme.data)
return (dict_data, save) return (section, save)
@dataclass @dataclass

View File

@ -12,14 +12,16 @@ import json
import sys import sys
import xml.etree.cElementTree as xml_etree # FIXME: bandit warning import xml.etree.cElementTree as xml_etree # FIXME: bandit warning
from concurrent.futures import ProcessPoolExecutor from concurrent.futures import ProcessPoolExecutor
from datetime import datetime, timedelta from contextlib import suppress
from datetime import date, datetime, time, timedelta
from enum import Enum from enum import Enum
from enum import auto as autostr from enum import auto as autostr
from pathlib import Path from pathlib import Path
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from types import ModuleType from types import ModuleType
from typing import ( from typing import (
Any, AsyncIterator, Callable, Dict, Mapping, Sequence, Tuple, Type, Union, Any, AsyncIterator, Callable, Dict, Iterable, Mapping, Sequence, Tuple,
Type, Union,
) )
from uuid import UUID from uuid import UUID
@ -27,10 +29,9 @@ import aiofiles
import filetype import filetype
from aiofiles.threadpool.binary import AsyncBufferedReader from aiofiles.threadpool.binary import AsyncBufferedReader
from aiofiles.threadpool.text import AsyncTextIOWrapper from aiofiles.threadpool.text import AsyncTextIOWrapper
from PIL import Image as PILImage
from nio.crypto import AsyncDataT as File from nio.crypto import AsyncDataT as File
from nio.crypto import async_generator_from_data from nio.crypto import async_generator_from_data
from PIL import Image as PILImage
if sys.version_info >= (3, 7): if sys.version_info >= (3, 7):
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
@ -145,14 +146,17 @@ def plain2html(text: str) -> str:
.replace("\t", "&nbsp;" * 4) .replace("\t", "&nbsp;" * 4)
def serialize_value_for_qml(value: Any, json_list_dicts: bool = False) -> Any: def serialize_value_for_qml(
value: Any, json_list_dicts: bool = False, reject_unknown: bool = False,
) -> Any:
"""Convert a value to make it easier to use from QML. """Convert a value to make it easier to use from QML.
Returns: Returns:
- For `int`, `float`, `bool`, `str` and `datetime`: the unchanged value - For `bool`, `int`, `float`, `bytes`, `str`, `datetime`, `date`, `time`:
the unchanged value (PyOtherSide handles these)
- For `Sequence` and `Mapping` subclasses (includes `list` and `dict`): - For `Iterable` objects (includes `list` and `dict`):
a JSON dump if `json_list_dicts` is `True`, else the unchanged value a JSON dump if `json_list_dicts` is `True`, else the unchanged value
- If the value is an instancied object and has a `serialized` attribute or - If the value is an instancied object and has a `serialized` attribute or
@ -168,10 +172,11 @@ def serialize_value_for_qml(value: Any, json_list_dicts: bool = False) -> Any:
- For class types: the class `__name__` - For class types: the class `__name__`
- For anything else: the unchanged value - For anything else: raise a `TypeError` if `reject_unknown` is `True`,
else return the unchanged value.
""" """
if isinstance(value, (int, float, bool, str, datetime)): if isinstance(value, (bool, int, float, bytes, str, datetime, date, time)):
return value return value
if json_list_dicts and isinstance(value, (Sequence, Mapping)): if json_list_dicts and isinstance(value, (Sequence, Mapping)):
@ -180,6 +185,9 @@ def serialize_value_for_qml(value: Any, json_list_dicts: bool = False) -> Any:
if not inspect.isclass(value) and hasattr(value, "serialized"): if not inspect.isclass(value) and hasattr(value, "serialized"):
return value.serialized return value.serialized
if isinstance(value, Iterable):
return value
if hasattr(value, "__class__") and issubclass(value.__class__, Enum): if hasattr(value, "__class__") and issubclass(value.__class__, Enum):
return value.value return value.value
@ -195,9 +203,43 @@ def serialize_value_for_qml(value: Any, json_list_dicts: bool = False) -> Any:
if inspect.isclass(value): if inspect.isclass(value):
return value.__name__ return value.__name__
if reject_unknown:
raise TypeError("Unknown type reject")
return value return value
def deep_serialize_for_qml(obj: Iterable) -> Union[list, dict]:
"""Recursively serialize lists and dict values for QML."""
if isinstance(obj, Mapping):
dct = {}
for key, value in obj.items():
if isinstance(value, Iterable) and not isinstance(value, str):
# PyOtherSide only accept dicts with string keys
dct[str(key)] = deep_serialize_for_qml(value)
continue
with suppress(TypeError):
dct[str(key)] = \
serialize_value_for_qml(value, reject_unknown=True)
return dct
lst = []
for value in obj:
if isinstance(value, Iterable) and not isinstance(value, str):
lst.append(deep_serialize_for_qml(value))
continue
with suppress(TypeError):
lst.append(serialize_value_for_qml(value, reject_unknown=True))
return lst
def classes_defined_in(module: ModuleType) -> Dict[str, Type]: def classes_defined_in(module: ModuleType) -> Dict[str, Type]:
"""Return a `{name: class}` dict of all the classes a module defines.""" """Return a `{name: class}` dict of all the classes a module defines."""

430
src/config/settings.py Normal file
View File

@ -0,0 +1,430 @@
# pylint: skip-file
# flake8: noqa
# mypy: ignore-errors
class General:
# When closing the window, minimize the application to system tray instead
# of quitting the application.
# A click on the tray icon reveals the window, middle click fully quits it
# and right click opens a menu with these options.
close_to_tray: bool = False
# Show rooms, members and messages in way that takes less vertical space.
compact: bool = False
# When the window width is less than this number of pixels, switch to a
# mobile-like mode where only the left main pane, center page/chat or
# right room pane is visible at a time.
hide_side_panes_under: int = 450
# How many seconds the cursor must hover on buttons and other elements
# to show tooltips.
tooltips_delay: float = 0.5
# Application theme to use.
# Can be the name of a built-in theme (Mirage.qpl or Glass.qpl), or
# the name (including extension) of a file in the user theme folder, which
# is "$XDG_DATA_HOME/mirage/themes" or "~/.local/share/mirage/themes".
# For Flatpak, it is
# "~/.var/app/io.github.mirukana.mirage/data/mirage/themes".
theme: str = "Midnight.qpl"
# Interface scale multiplier, e.g. 0.5 makes everything half-size.
zoom: float = 1.0
class Presence:
# Automatically set your presence to unavailable after this number of
# seconds without any mouse or keyboard activity.
# This currently only works on Linux X11.
auto_away_after: int = 60 * 10
class Notifications:
# How long in seconds window alerts will last when a new message
# is posted in a room. On most desktops, this highlights or flashes the
# application in the taskbar or dock.
# Can be set to 0 for no alerts.
# Can be set to -1 for alerts that last until the window is focused.
alert_time: float = 0
# Same as alert_time for urgent messages, e.g. when you are mentioned,
# replied to, or the message contains a keyword.
urgent_alert_time: float = -1
class Scrolling:
# Use velocity-based kinetic scrolling.
# Can cause problems on laptop touchpads and some special mouse wheels.
kinetic: bool = True
# Maximum allowed velocity when kinetic scrolling is used.
kinetic_max_speed: int = 2500
# When kinetic scrolling is used, how fast the view slows down when you
# stop physically scrolling.
kinetic_deceleration: int = 1500
# Multiplier for the scrolling speed when kinetic scrolling is
# disabled, e.g. 1.5 is 1.5x faster than the default speed.
non_kinetic_speed: float = 1.0
class RoomList:
# Prevent resizing the pane below this width in pixels.
min_width: int = 144
# Sort rooms in alphabetical order instead of recent activity.
# The application must be restarted to apply changes to this setting.
lexical_sort: bool = False
# Mapping of account user ID to list of room ID to always keep on top.
# You can copy a room's ID by right clicking on it in the room list.
# Example: {"@alice:example.org": ["!aBc@example.org", "!123:example.org"]}
bookmarks: Dict[str, List[str]] = {}
# When clicking on a room, recenter the room list on that room.
click_centers: bool = False
# When pressing enter in the room filter field, clear the field's text,
# in addition to activating the keyboard-focused room.
enter_clears_filter: bool = True
# When pressing escape in the room filter field, clear the field's text.
# in addition to focusing the current page or chat composer.
escape_clears_filter: bool = True
class Chat:
# Center the chat header (room avatar, name and topic) even when sidepanes
# aren't hidden (see comment for the hide_sidepanes_under setting).
always_center_header: bool = False
# When the chat timeline is larger than this pixel number,
# Align your own messages to the left of the timeline instead of right.
# Can be 0 to always show your messages on the left.
own_messages_on_left_above: int = 895
# Maximum number of characters in a message line before wrapping the text
# to a new line. Ignores messages containing code blocks or tables.
max_messages_line_length: int = 65
# Show membership events in the timeline: when someone is invited to the
# room, joins, leaves, is kicked, banned or unbanned.
show_membership_events: bool = True
# Show room member display name and avatar change events in the timeline.
show_profile_changes: bool = False
# Show a notice in the timeline for events types that aren't recognized.
show_unknown_events: bool = False
# In a chat with unread messages, the messages will be marked as read
# after this number of seconds.
# Focusing another window or chat resets the timer.
mark_read_delay: float = 0.2
class Composer:
# Mapping of account user ID to alias.
# From any chat, start a message with an alias followed by a space
# to type and send as this associated account.
# The account must have permission to talk in the room.
# To ignore an alias when typing, prepend it with a space.
# Example: {"@alice:example.org": "al", "@bob:example.org": "b"}
aliases: Dict[str, str] = {}
class Files:
# Minimum width of the file name/size box for files without previews.
min_file_width: int = 256
# Minimum (width, height) for image thumbnails.
min_thumbnail_size: Tuple[int, int] = (256, 256)
# How much of the chat height image thumbnails can take at most,
# e.g. 0.4 for 40% of the chat or 1 for 100%.
max_thumbnail_height_ratio: float = 0.4
# Automatically play animated GIF images in the timeline.
auto_play_gif: bool = True
# When clicking on a file in the timeline, open it in an external
# programing instead of displaying it using Mirage's interface.
# On Linux, the xdg-open command is used.
click_opens_externally: bool = False
# In the full image viewer, if the image is large enough to cover the
# info bar or buttons, they will automatically hide after this number
# of seconds.
# Hovering on the top/bottom with a mouse or tapping on a touch screen
# reveals the hidden controls.
autohide_image_controls_after: float = 2.0
class Keys:
# All keybind settings, unless their comment says otherwise, are list of
# the possible shortcuts for an action, e.g. ["Ctrl+A", "Alt+Shift+A"].
#
# The available modifiers are Ctrl, Shift, Alt and Meta.
# On macOS, Ctrl corresponds to Cmd and Meta corresponds to Control.
# On other systems, Meta corresponds to the Windows/Super/mod4 key.
#
# https://doc.qt.io/qt-5/qt.html#Key-enum lists the names of special
# keys, e.g. for "Qt::Key_Space", you would use "Space" in this config.
#
# The Escape key by itself should not be bound, as it would conflict with
# closing popups and various other actions.
#
# Key chords can be defined by having up to four shortcuts
# separated by commas in a string, e.g. for ["Ctrl+A,B"], Ctrl+A then B
# would need to be pressed.
# Helper functions
import platform
def os_ctrl(self) -> str:
# Return Meta on macOS, which corresponds to Ctrl, and Ctrl on others.
return "Meta" if platform.system() == "Darwin" else "Ctrl"
def alt_or_cmd(self) -> str:
# Return Ctrl on macOS, which corresponds to Cmd, and Alt on others.
return "Ctrl" if platform.system() == "Darwin" else "Alt"
# Toggle compact interface mode. See the compact setting comment.
compact = ["Alt+Ctrl+C"]
# Control the interface scale.
zoom_in = ["Ctrl++"]
zoom_out = ["Ctrl+-"]
reset_zoom = ["Ctrl+="]
# Switch to the previous/next tab in pages. In chats, this controls what
# the right room pane shows, e.g. member list or room settings.
previous_tab = ["Alt+Shift+Left", "Alt+Shift+H"]
next_tab = ["Alt+Shift+Right", "Alt+Shift+L"]
# Switch to the last opened page/chat, similar to Alt+Tab on most desktops.
last_page = ["Ctrl+Tab"]
# Toggle the QML developer console. Type ". help" in it for more info.
qml_console = ["F1"]
# Start the Python backend debugger. Unless the "remote-pdb" pip package is
# installed, Mirage must be started from a terminal for this to work.
python_debugger = ["Shift+F1"]
class Scrolling:
# Pages and chat timeline scrolling
up = ["Alt+Up", "Alt+K"]
down = ["Alt+Down", "Alt+J"]
page_up = ["Alt+Ctrl+Up", "Alt+Ctrl+K", "PgUp"]
page_down = ["Alt+Ctrl+Down", "Alt+Ctrl+J", "PgDown"]
top = ["Alt+Ctrl+Shift+Up", "Alt+Ctrl+Shift+K", "Home"]
bottom = ["Alt+Ctrl+Shift+Down", "Alt+Ctrl+Shift+J", "End"]
class Accounts:
# The current account is the account under which a page or chat is
# opened, or the keyboard-focused one when using the room filter field.
# Add a new account
add = ["Alt+Shift+A"]
# Collapse the current account
collapse = ["Alt+O"]
# Open the current account settings
settings = ["Alt+A"]
# Open the current account context menu
menu = ["Alt+P"]
# Toggle current account presence between this status and online
unavailable = ["Alt+Ctrl+U", "Alt+Ctrl+A"]
invisible = ["Alt+Ctrl+I"]
offline = ["Alt+Ctrl+O"]
# Switch to first room of the previous/next account in the room list.
previous = ["Alt+Shift+N"]
next = ["Alt+N"]
# Switch to the first room of the account number X in the list.
# This is a mapping of account number to keybind, e.g.
# {1: "Ctrl+1"} would bind Ctrl+1 to the switch to the first account.
at_index: Dict[int, str] = {
"1": f"{parent.os_ctrl()}+1",
"2": f"{parent.os_ctrl()}+2",
"3": f"{parent.os_ctrl()}+3",
"4": f"{parent.os_ctrl()}+4",
"5": f"{parent.os_ctrl()}+5",
"6": f"{parent.os_ctrl()}+6",
"7": f"{parent.os_ctrl()}+7",
"8": f"{parent.os_ctrl()}+8",
"9": f"{parent.os_ctrl()}+9",
"10": f"{parent.os_ctrl()}+0",
}
class Rooms:
# Add a new room (direct chat, join or create a group).
add = ["Alt+C"]
# Focus or clear the text of the left main pane's room filter field.
# When focusing the field, use Tab/Shift+Tab or the arrows to navigate
# the list, Enter to switch to focused account/room, Escape to cancel,
# Menu to open the context menu.
focus_filter = ["Alt+F"]
clear_filter = ["Alt+Shift+F"]
# Switch to the previous/next room in the list.
previous = ["Alt+Shift+Up", "Alt+Shift+K"]
next = ["Alt+Shift+Down", "Alt+Shift+J"]
# Switch to the previous/next room with unread messages in the list.
previous_unread = ["Alt+Shift+U"]
next_unread = ["Alt+U"]
# Switch to the previous/next room with urgent messages in the list,
# e.g. messages mentioning your name, replies to you or keywords.
previous_urgent = ["Alt+Shift+M"]
next_urgent = ["Alt+M"]
# Switch to room number X in the current account.
# This is a mapping of room number to keybind, e.g.
# {1: "Alt+1"} would bind Alt+1 to switch to the first room.
at_index: Dict[int, str] = {
"1": f"{parent.alt_or_cmd()}+1",
"2": f"{parent.alt_or_cmd()}+2",
"3": f"{parent.alt_or_cmd()}+3",
"4": f"{parent.alt_or_cmd()}+4",
"5": f"{parent.alt_or_cmd()}+5",
"6": f"{parent.alt_or_cmd()}+6",
"7": f"{parent.alt_or_cmd()}+7",
"8": f"{parent.alt_or_cmd()}+8",
"9": f"{parent.alt_or_cmd()}+9",
"10": f"{parent.alt_or_cmd()}+0",
}
class Chat:
# Keybinds specific to the current chat page.
# Focus the right room pane. If the pane is currently showing the
# room member list, the corresponding filter field is focused.
# When focusing the field, use Tab/Shift+Tab or the arrows to navigate
# the list, Enter to see the focused member's profile, Escape to cancel,
# Menu to open the context menu.
focus_room_pane = ["Alt+R"]
# Toggle hiding the right pane.
# Can also be done by clicking on current tab button at the top right.
hide_room_pane = ["Alt+Ctrl+R"]
# Invite new members, leave or forget the current chat.
invite = ["Alt+I"]
leave = ["Alt+Escape"]
forget = ["Alt+Shift+Escape"]
# Open the file picker to upload files in the current chat.
send_file = ["Alt+S"]
# If your clipboard contains a file path, upload that file.
send_clipboard_path = ["Alt+Shift+S"]
class Messages:
# Focus the previous/next message in the timeline.
# Keybinds defined below in this section affect the focused message.
# The Menu key can open the context menu for a focused message.
previous = ["Ctrl+Up", "Ctrl+K"]
next = ["Ctrl+Down", "Ctrl+J"]
# Select the currently focused message, same as clicking on it.
# When there are selected messages, some right click menu options
# and keybinds defined below will affect these messages instead of
# the focused (for keybinds) or mouse-targeted (right click menu) one.
# The Menu key can open the context menu for selected messages.
select = ["Ctrl+Space"]
# Select all messages from point A to point B.
# If used when no messages are already selected, all the messages
# from the most recent in the timeline to the focused one are selected.
# Otherwise, messages from the last selected to focused are selected.
select_until_here = ["Ctrl+Shift+Space"]
# Clear the message keyboard focus.
# If no message is focused but some are selected, clear the selection.
unfocus_or_deselect = ["Ctrl+D"]
# Remove the selected messages if any, else the focused message if any,
# else the last message you posted.
remove = ["Ctrl+R", "Alt+Del"]
# Reply/cancel reply to the focused message if any,
# else the last message posted by someone else.
# Replying can also be cancelled by pressing Escape.
reply = ["Ctrl+Q"]
# Open the QML developer console for the focused message if any,
# and display the event source.
debug = ["Ctrl+Shift+D"]
# Open the files and links in selected messages if any, else the
# file/links of the focused message if any, else the last
# files/link in the timeline.
open_links_files = ["Ctrl+O"]
# Like open_links_files, but files open in external programs instead.
# On Linux, this uses the xdg-open command.
open_links_files_externally = ["Ctrl+Shift+O"]
# Copy the downloaded files path in selected messages if any,
# else the file path for the focused message if any, else the
# path for the last downloaded file in the timeline.
copy_files_path = ["Ctrl+Shift+C"]
# Clear all messages from the chat.
# This does not remove anything for other users.
clear_all = ["Ctrl+L"]
class ImageViewer:
# Close the image viewer
close = ["X", "Q"]
# Toggle alternate image scaling mode: if the original image size is
# smaller than the window, upscale it to fit the window.
# If it is bigger than the window, show it as its real size.
expand = ["E"]
# Toggle fullscreen mode.
fullscreen = ["F", "F11", "Alt+Return", "Alt+Enter"]
# Pan/scroll the image.
pan_left = ["H", "Left", "Alt+H", "Alt+Left"]
pan_down = ["J", "Down", "Alt+J", "Alt+Down"]
pan_up = ["K", "Up", "Alt+K", "Alt+Up"]
pan_right = ["L", "Right", "Alt+L", "Alt+Right"]
# Control the image's zoom. Ctrl+wheel can also be used.
zoom_in = ["Z", "+", "Ctrl++"]
zoom_out = ["Shift+Z", "-", "Ctrl+-"]
reset_zoom = ["Alt+Z", "=", "Ctrl+="]
# Control the image's rotation.
rotate_right = ["R"]
rotate_left = ["Shift+R"]
reset_rotation = ["Alt+R"]
# Control the speed for animated GIF images.
speed_up = ["S"]
slow_down = ["Shift+S"]
reset_speed = ["Alt+S"]
# Toggle pausing for animated GIF images.
pause = ["Space"]
class Sessions:
# These keybinds affect the session list in your account settings.
#
# Currently unchangable keys:
# Tab/Shift+Tab or the arrow keys to navigate the list,
# Space to check/uncheck focused session,
# Menu to open the focused session's context menu.
# Refresh the list of sessions.
refresh = ["Alt+R", "F5"]
# Sign out checked sessions if any, else sign out all sessions.
sign_out_checked_or_all = ["Alt+S", "Delete"]

View File

@ -37,7 +37,7 @@ Drawer {
property bool collapse: property bool collapse:
(horizontal ? window.width : window.height) < (horizontal ? window.width : window.height) <
window.settings.collapseSidePanesUnderWindowWidth * theme.uiScale window.settings.General.hide_side_panes_under * theme.uiScale
property int peekSizeWhileCollapsed: property int peekSizeWhileCollapsed:
(horizontal ? referenceSizeParent.width : referenceSizeParent.height) * (horizontal ? referenceSizeParent.width : referenceSizeParent.height) *

View File

@ -6,8 +6,8 @@ import QtQuick.Controls 2.12
Flickable { Flickable {
id: flickable id: flickable
maximumFlickVelocity: window.settings.kineticScrollingMaxSpeed maximumFlickVelocity: window.settings.Scrolling.kinetic_max_speed
flickDeceleration: window.settings.kineticScrollingDeceleration flickDeceleration: window.settings.Scrolling.kinetic_deceleration
ScrollBar.vertical: HScrollBar { ScrollBar.vertical: HScrollBar {
visible: parent.interactive visible: parent.interactive

View File

@ -81,8 +81,8 @@ GridView {
preferredHighlightBegin: height / 2 - currentItemHeight / 2 preferredHighlightBegin: height / 2 - currentItemHeight / 2
preferredHighlightEnd: height / 2 + currentItemHeight / 2 preferredHighlightEnd: height / 2 + currentItemHeight / 2
maximumFlickVelocity: window.settings.kineticScrollingMaxSpeed maximumFlickVelocity: window.settings.Scrolling.kinetic_max_speed
flickDeceleration: window.settings.kineticScrollingDeceleration flickDeceleration: window.settings.Scrolling.kinetic_deceleration
highlight: Rectangle { highlight: Rectangle {

View File

@ -18,7 +18,7 @@ Image {
property alias radius: roundMask.radius property alias radius: roundMask.radius
property alias showProgressBar: progressBarLoader.active property alias showProgressBar: progressBarLoader.active
property bool showPauseButton: true property bool showPauseButton: true
property bool pause: ! window.settings.media.autoPlayGIF property bool pause: ! window.settings.Chat.Files.auto_play_gif
property bool forcePause: false property bool forcePause: false
property real speed: 1 property real speed: 1

View File

@ -16,7 +16,7 @@ MouseArea {
const speedMultiply = const speedMultiply =
Qt.styleHints.wheelScrollLines * Qt.styleHints.wheelScrollLines *
window.settings.nonKineticScrollingSpeed window.settings.Scrolling.non_kinetic_speed
const pixelDelta = { const pixelDelta = {
x: wheel.pixelDelta.x || wheel.angleDelta.x / 8 * speedMultiply, x: wheel.pixelDelta.x || wheel.angleDelta.x / 8 * speedMultiply,
@ -64,7 +64,7 @@ MouseArea {
} }
enabled: ! window.settings.enableKineticScrolling enabled: ! window.settings.Scrolling.kinetic
propagateComposedEvents: true propagateComposedEvents: true
acceptedButtons: Qt.NoButton acceptedButtons: Qt.NoButton
@ -84,7 +84,8 @@ MouseArea {
Binding { Binding {
target: flickable target: flickable
property: "maximumFlickVelocity" property: "maximumFlickVelocity"
value: mouseArea.enabled ? 0 : window.settings.kineticScrollingMaxSpeed value:
mouseArea.enabled ? 0 : window.settings.Scrolling.kinetic_max_speed
} }
Binding { Binding {
@ -93,6 +94,6 @@ MouseArea {
value: value:
mouseArea.enabled ? mouseArea.enabled ?
0 : 0 :
window.settings.kineticScrollingDeceleration window.settings.Scrolling.kinetic_deceleration
} }
} }

View File

@ -89,8 +89,8 @@ ListView {
preferredHighlightBegin: height / 2 - currentItemHeight / 2 preferredHighlightBegin: height / 2 - currentItemHeight / 2
preferredHighlightEnd: height / 2 + currentItemHeight / 2 preferredHighlightEnd: height / 2 + currentItemHeight / 2
maximumFlickVelocity: window.settings.kineticScrollingMaxSpeed maximumFlickVelocity: window.settings.Scrolling.kinetic_max_speed
flickDeceleration: window.settings.kineticScrollingDeceleration flickDeceleration: window.settings.Scrolling.kinetic_deceleration
highlight: Rectangle { highlight: Rectangle {
color: theme.controls.listView.highlight color: theme.controls.listView.highlight

View File

@ -7,7 +7,7 @@ import ".."
HButton { HButton {
id: tile id: tile
property bool compact: window.settings.compactMode property bool compact: window.settings.General.compact
property real contentOpacity: 1 property real contentOpacity: 1
property Component contextMenu: null property Component contextMenu: null
property HMenu openedMenu: null property HMenu openedMenu: null

View File

@ -26,7 +26,7 @@ ToolTip {
} }
delay: instant ? 0 : theme.controls.toolTip.delay delay: instant ? 0 : window.settings.General.tooltips_delay * 1000
padding: background.border.width padding: background.border.width
background: Rectangle { background: Rectangle {

View File

@ -55,7 +55,7 @@ HColumnLayout {
Timer { Timer {
id: osdHideTimer id: osdHideTimer
interval: window.settings.media.autoHideOSDAfterMsec interval: window.settings.Chat.Files.autohide_image_controls_after
onTriggered: osd.showup = false onTriggered: osd.showup = false
} }

View File

@ -8,7 +8,7 @@ Rectangle {
implicitWidth: implicitWidth:
window.settings.compactMode ? window.settings.General.compact ?
theme.controls.presence.radius * 2 : theme.controls.presence.radius * 2 :
theme.controls.presence.radius * 2.5 theme.controls.presence.radius * 2.5

View File

@ -25,7 +25,7 @@ HDrawer {
property string selectedOutputText: "" property string selectedOutputText: ""
property string pythonDebugKeybind: property string pythonDebugKeybind:
window.settings.keys.startPythonDebugger[0] window.settings.Keys.python_debugger[0]
property string help: qsTr( property string help: qsTr(
`Interact with the QML code using JavaScript ES6 syntax. `Interact with the QML code using JavaScript ES6 syntax.
@ -71,7 +71,7 @@ HDrawer {
if (addToHistory && history.slice(-1)[0] !== input) { if (addToHistory && history.slice(-1)[0] !== input) {
history.push(input) history.push(input)
while (history.length > maxHistoryLength) history.shift() while (history.length > maxHistoryLength) history.shift()
window.historyChanged() window.saveHistory()
} }
let output = "" let output = ""
@ -154,7 +154,7 @@ HDrawer {
} }
HShortcut { HShortcut {
sequences: settings.keys.toggleDebugConsole sequences: settings.Keys.qml_console
onActivated: debugConsole.toggle() onActivated: debugConsole.toggle()
} }

View File

@ -17,7 +17,7 @@ Timer {
interval: 1000 interval: 1000
repeat: true repeat: true
running: running:
window.settings.beUnavailableAfterSecondsIdle > 0 && window.settings.Presence.auto_away_after > 0 &&
CppUtils.idleMilliseconds() !== -1 CppUtils.idleMilliseconds() !== -1
onTriggered: { onTriggered: {
@ -25,7 +25,7 @@ Timer {
const beUnavailable = const beUnavailable =
CppUtils.idleMilliseconds() / 1000 >= CppUtils.idleMilliseconds() / 1000 >=
window.settings.beUnavailableAfterSecondsIdle window.settings.Presence.auto_away_after
for (let i = 0; i < accounts.count; i++) { for (let i = 0; i < accounts.count; i++) {
const account = accounts.get(i) const account = accounts.get(i)

View File

@ -81,7 +81,7 @@ Rectangle {
HShortcut { HShortcut {
sequences: window.settings.keys.goToPreviousAccount sequences: window.settings.Keys.Accounts.previous
onActivated: { onActivated: {
accountList.moveCurrentIndexLeft() accountList.moveCurrentIndexLeft()
accountList.currentItem.leftClicked() accountList.currentItem.leftClicked()
@ -89,7 +89,7 @@ Rectangle {
} }
HShortcut { HShortcut {
sequences: window.settings.keys.goToNextAccount sequences: window.settings.Keys.Accounts.next
onActivated: { onActivated: {
accountList.moveCurrentIndexRight() accountList.moveCurrentIndexRight()
accountList.currentItem.leftClicked() accountList.currentItem.leftClicked()

View File

@ -26,7 +26,7 @@ HTile {
function setCollapse(collapse) { function setCollapse(collapse) {
window.uiState.collapseAccounts[model.id] = collapse window.uiState.collapseAccounts[model.id] = collapse
window.uiStateChanged() window.saveUIState()
py.callCoro("set_account_collapse", [model.id, collapse]) py.callCoro("set_account_collapse", [model.id, collapse])
} }
@ -162,7 +162,7 @@ HTile {
HShortcut { HShortcut {
enabled: enableKeybinds enabled: enableKeybinds
sequences: window.settings.keys.addNewChat sequences: window.settings.Keys.Rooms.add
onActivated: addChat.clicked() onActivated: addChat.clicked()
} }
} }
@ -210,37 +210,37 @@ HTile {
HShortcut { HShortcut {
enabled: enableKeybinds enabled: enableKeybinds
sequences: window.settings.keys.accountSettings sequences: window.settings.Keys.Accounts.settings
onActivated: leftClicked() onActivated: leftClicked()
} }
HShortcut { HShortcut {
enabled: enableKeybinds enabled: enableKeybinds
sequences: window.settings.keys.toggleCollapseAccount sequences: window.settings.Keys.Accounts.collapse
onActivated: toggleCollapse() onActivated: toggleCollapse()
} }
HShortcut { HShortcut {
enabled: enableKeybinds enabled: enableKeybinds
sequences: window.settings.keys.openPresenceMenu sequences: window.settings.Keys.Accounts.menu
onActivated: account.doRightClick(false) onActivated: account.doRightClick(false)
} }
HShortcut { HShortcut {
enabled: enableKeybinds enabled: enableKeybinds
sequences: window.settings.keys.togglePresenceUnavailable sequences: window.settings.Keys.Accounts.unavailable
onActivated: account.togglePresence("unavailable") onActivated: account.togglePresence("unavailable")
} }
HShortcut { HShortcut {
enabled: enableKeybinds enabled: enableKeybinds
sequences: window.settings.keys.togglePresenceInvisible sequences: window.settings.Keys.Accounts.invisible
onActivated: account.togglePresence("invisible") onActivated: account.togglePresence("invisible")
} }
HShortcut { HShortcut {
enabled: enableKeybinds enabled: enableKeybinds
sequences: window.settings.keys.togglePresenceOffline sequences: window.settings.Keys.Accounts.offline
onActivated: account.togglePresence("offline") onActivated: account.togglePresence("offline")
} }

View File

@ -32,7 +32,7 @@ Rectangle {
Layout.fillHeight: true Layout.fillHeight: true
HShortcut { HShortcut {
sequences: window.settings.keys.addNewAccount sequences: window.settings.Keys.Accounts.add
onActivated: addAccountButton.clicked() onActivated: addAccountButton.clicked()
} }
} }
@ -57,7 +57,7 @@ Rectangle {
Keys.onEnterPressed: Keys.onReturnPressed(event) Keys.onEnterPressed: Keys.onReturnPressed(event)
Keys.onReturnPressed: { Keys.onReturnPressed: {
roomList.showItemAtIndex() roomList.showItemAtIndex()
if (window.settings.clearRoomFilterOnEnter) text = "" if (window.settings.RoomList.enter_clears_filter) text = ""
} }
Keys.onMenuPressed: Keys.onMenuPressed:
@ -66,19 +66,19 @@ Rectangle {
Keys.onEscapePressed: { Keys.onEscapePressed: {
mainPane.toggleFocus() mainPane.toggleFocus()
if (window.settings.clearRoomFilterOnEscape) text = "" if (window.settings.RoomList.escape_clears_filter) text = ""
} }
Behavior on opacity { HNumberAnimation {} } Behavior on opacity { HNumberAnimation {} }
HShortcut { HShortcut {
sequences: window.settings.keys.clearRoomFilter sequences: window.settings.Keys.Rooms.clear_filter
onActivated: filterField.text = "" onActivated: filterField.text = ""
} }
HShortcut { HShortcut {
sequences: window.settings.keys.toggleFocusMainPane sequences: window.settings.Keys.Rooms.focus_filter
onActivated: mainPane.toggleFocus() onActivated: mainPane.toggleFocus()
} }
} }

View File

@ -25,8 +25,9 @@ HDrawer {
saveName: "mainPane" saveName: "mainPane"
background: Rectangle { color: theme.mainPane.background } background: Rectangle { color: theme.mainPane.background }
minimumSize: theme.mainPane.minimumSize
requireDefaultSize: bottomBar.filterField.activeFocus requireDefaultSize: bottomBar.filterField.activeFocus
minimumSize:
window.settings.RoomList.min_width * window.settings.General.zoom
Behavior on opacity { HNumberAnimation {} } Behavior on opacity { HNumberAnimation {} }

View File

@ -73,12 +73,12 @@ HListView {
) : ) :
pageLoader.showRoom(item.for_account, item.id) pageLoader.showRoom(item.for_account, item.id)
if (fromClick && ! window.settings.centerRoomListOnClick) if (fromClick && ! window.settings.RoomList.click_centers)
keepListCentered = false keepListCentered = false
currentIndex = index currentIndex = index
if (fromClick && ! window.settings.centerRoomListOnClick) if (fromClick && ! window.settings.RoomList.click_centers)
keepListCentered = true keepListCentered = true
} }
@ -245,54 +245,53 @@ HListView {
} }
HShortcut { HShortcut {
sequences: window.settings.keys.goToPreviousRoom sequences: window.settings.Keys.Rooms.previous
onActivated: { decrementCurrentIndex(); showItemLimiter.restart() } onActivated: { decrementCurrentIndex(); showItemLimiter.restart() }
} }
HShortcut { HShortcut {
sequences: window.settings.keys.goToNextRoom sequences: window.settings.Keys.Rooms.next
onActivated: { incrementCurrentIndex(); showItemLimiter.restart() } onActivated: { incrementCurrentIndex(); showItemLimiter.restart() }
} }
HShortcut { HShortcut {
sequences: window.settings.keys.goToPreviousUnreadRoom sequences: window.settings.Keys.Rooms.previous_unread
onActivated: { cycleUnreadRooms(false) && showItemLimiter.restart() } onActivated: { cycleUnreadRooms(false) && showItemLimiter.restart() }
} }
HShortcut { HShortcut {
sequences: window.settings.keys.goToNextUnreadRoom sequences: window.settings.Keys.Rooms.next_unread
onActivated: { cycleUnreadRooms(true) && showItemLimiter.restart() } onActivated: { cycleUnreadRooms(true) && showItemLimiter.restart() }
} }
HShortcut { HShortcut {
sequences: window.settings.keys.goToPreviousMentionedRoom sequences: window.settings.Keys.Rooms.previous_urgent
onActivated: cycleUnreadRooms(false, true) && showItemLimiter.restart() onActivated: cycleUnreadRooms(false, true) && showItemLimiter.restart()
} }
HShortcut { HShortcut {
sequences: window.settings.keys.goToNextMentionedRoom sequences: window.settings.Keys.Rooms.next_urgent
onActivated: cycleUnreadRooms(true, true) && showItemLimiter.restart() onActivated: cycleUnreadRooms(true, true) && showItemLimiter.restart()
} }
Repeater { Repeater {
model: Object.keys(window.settings.keys.focusAccountAtIndex) model: Object.keys(window.settings.Keys.Accounts.at_index)
Item { Item {
HShortcut { HShortcut {
sequence: window.settings.keys.focusAccountAtIndex[modelData] sequence: window.settings.Keys.Accounts.at_index[modelData]
onActivated: goToAccountNumber(parseInt(modelData - 1, 10)) onActivated: goToAccountNumber(parseInt(modelData, 10) - 1)
} }
} }
} }
Repeater { Repeater {
model: Object.keys(window.settings.keys.focusRoomAtIndex) model: Object.keys(window.settings.Keys.Rooms.at_index)
Item { Item {
HShortcut { HShortcut {
sequence: window.settings.keys.focusRoomAtIndex[modelData] sequence: window.settings.Keys.Rooms.at_index[modelData]
onActivated: onActivated: showAccountRoomAtIndex(parseInt(modelData,10) - 1)
showAccountRoomAtIndex(parseInt(modelData - 1, 10))
} }
} }
} }

View File

@ -44,7 +44,7 @@ HLoader {
} }
window.uiState.pageProperties = properties window.uiState.pageProperties = properties
window.uiStateChanged() window.saveUIState()
} }
function showRoom(userId, roomId) { function showRoom(userId, roomId) {
@ -92,7 +92,7 @@ HLoader {
} }
HShortcut { HShortcut {
sequences: window.settings.keys.goToLastPage sequences: window.settings.Keys.last_page
onActivated: showPrevious() onActivated: showPrevious()
} }
} }

View File

@ -34,8 +34,10 @@ HFlickableColumnPage {
} }
if (aliasFieldItem.changed) { if (aliasFieldItem.changed) {
window.settings.writeAliases[userId] = aliasFieldItem.text window.settings.Chat.Composer.aliases[userId] =
window.settingsChanged() aliasFieldItem.text
window.saveSettings()
} }
if (avatar.changed) { if (avatar.changed) {
@ -249,7 +251,7 @@ HFlickableColumnPage {
HLabeledItem { HLabeledItem {
id: aliasField id: aliasField
readonly property var aliases: window.settings.writeAliases readonly property var aliases: window.settings.Chat.Composer.aliases
readonly property string currentAlias: aliases[userId] || "" readonly property string currentAlias: aliases[userId] || ""
readonly property bool hasWhiteSpace: /\s/.test(item.text) readonly property bool hasWhiteSpace: /\s/.test(item.text)

View File

@ -164,12 +164,12 @@ HColumnPage {
Keys.onMenuPressed: Keys.onEnterPressed(event) Keys.onMenuPressed: Keys.onEnterPressed(event)
HShortcut { HShortcut {
sequences: window.settings.keys.refreshDevices sequences: window.settings.Keys.Sessions.refresh
onActivated: refreshButton.clicked() onActivated: refreshButton.clicked()
} }
HShortcut { HShortcut {
sequences: window.settings.keys.signOutCheckedOrAllDevices sequences: window.settings.Keys.Sessions.sign_out_checked_or_all
onActivated: signOutCheckedButton.clicked() onActivated: signOutCheckedButton.clicked()
} }

View File

@ -51,7 +51,7 @@ Item {
onReadyChanged: longLoading = false onReadyChanged: longLoading = false
HShortcut { HShortcut {
sequences: window.settings.keys.leaveRoom sequences: window.settings.Keys.Chat.leave
active: userInfo && userInfo.presence !== "offline" active: userInfo && userInfo.presence !== "offline"
onActivated: window.makePopup( onActivated: window.makePopup(
"Popups/LeaveRoomPopup.qml", "Popups/LeaveRoomPopup.qml",
@ -60,7 +60,7 @@ Item {
} }
HShortcut { HShortcut {
sequences: window.settings.keys.forgetRoom sequences: window.settings.Keys.Chat.forget
active: userInfo && userInfo.presence !== "offline" active: userInfo && userInfo.presence !== "offline"
onActivated: window.makePopup( onActivated: window.makePopup(
"Popups/ForgetRoomPopup.qml", "Popups/ForgetRoomPopup.qml",

View File

@ -21,7 +21,7 @@ HTextArea {
readonly property var usableAliases: { readonly property var usableAliases: {
const obj = {} const obj = {}
const aliases = window.settings.writeAliases const aliases = window.settings.Chat.Composer.aliases
// Get accounts that are members of this room with permission to talk // Get accounts that are members of this room with permission to talk
for (const [id, alias] of Object.entries(aliases)) { for (const [id, alias] of Object.entries(aliases)) {

View File

@ -21,7 +21,7 @@ HButton {
onClicked: sendFilePicker.dialog.open() onClicked: sendFilePicker.dialog.open()
HShortcut { HShortcut {
sequences: window.settings.keys.sendFileFromPathInClipboard sequences: window.settings.Keys.Chat.send_clipboard_path
onActivated: window.makePopup( onActivated: window.makePopup(
"Popups/ConfirmUploadPopup.qml", "Popups/ConfirmUploadPopup.qml",
{ {
@ -43,7 +43,7 @@ HButton {
onReplied: chat.clearReplyTo() onReplied: chat.clearReplyTo()
HShortcut { HShortcut {
sequences: window.settings.keys.sendFile sequences: window.settings.Keys.Chat.send_file
onActivated: sendFilePicker.dialog.open() onActivated: sendFilePicker.dialog.open()
} }
} }

View File

@ -14,7 +14,7 @@ Rectangle {
(chat.roomPane.collapse || chat.roomPane.forceCollapse) (chat.roomPane.collapse || chat.roomPane.forceCollapse)
readonly property bool center: readonly property bool center:
showLeftButton || window.settings.alwaysCenterRoomHeader showLeftButton || window.settings.Chat.always_center_header
implicitHeight: theme.baseElementsHeight implicitHeight: theme.baseElementsHeight

View File

@ -139,7 +139,9 @@ HColumnLayout {
stackView.currentItem.currentIndex = -1 stackView.currentItem.currentIndex = -1
roomPane.toggleFocus() roomPane.toggleFocus()
if (window.settings.clearMemberFilterOnEscape) text = ""
if (window.settings.RoomList.escape_clears_filter)
text = ""
} }
Behavior on opacity { HNumberAnimation {} } Behavior on opacity { HNumberAnimation {} }
@ -174,7 +176,7 @@ HColumnLayout {
Layout.preferredHeight: filterField.implicitHeight Layout.preferredHeight: filterField.implicitHeight
HShortcut { HShortcut {
sequences: window.settings.keys.inviteToRoom sequences: window.settings.Keys.Chat.invite
onActivated: onActivated:
if (inviteButton.enabled) inviteButton.clicked() if (inviteButton.enabled) inviteButton.clicked()
} }

View File

@ -105,12 +105,12 @@ MultiviewPane {
} }
HShortcut { HShortcut {
sequences: window.settings.keys.toggleFocusRoomPane sequences: window.settings.Keys.Chat.focus_room_pane
onActivated: roomPane.toggleFocus() onActivated: roomPane.toggleFocus()
} }
HShortcut { HShortcut {
sequences: window.settings.keys.toggleHideRoomPane sequences: window.settings.Keys.Chat.hide_room_pane
onActivated: roomPane.forceCollapse = ! roomPane.forceCollapse onActivated: roomPane.forceCollapse = ! roomPane.forceCollapse
} }
} }

View File

@ -75,11 +75,11 @@ HRowLayout {
readonly property int maxMessageWidth: readonly property int maxMessageWidth:
contentText.includes("<pre>") || contentText.includes("<table>") ? contentText.includes("<pre>") || contentText.includes("<table>") ?
-1 : -1 :
window.settings.maxMessageCharactersPerLine < 0 ? window.settings.Chat.max_messages_line_length < 0 ?
-1 : -1 :
Math.ceil( Math.ceil(
mainUI.fontMetrics.averageCharacterWidth * mainUI.fontMetrics.averageCharacterWidth *
window.settings.maxMessageCharactersPerLine window.settings.Chat.max_messages_line_length
) )
readonly property alias selectedText: contentLabel.selectedPlainText readonly property alias selectedText: contentLabel.selectedPlainText

View File

@ -17,7 +17,7 @@ HColumnLayout {
readonly property var nextModel: eventList.model.get(model.index - 1) readonly property var nextModel: eventList.model.get(model.index - 1)
readonly property QtObject currentModel: model readonly property QtObject currentModel: model
readonly property bool compact: window.settings.compactMode readonly property bool compact: window.settings.General.compact
readonly property bool checked: model.id in eventList.checked readonly property bool checked: model.id in eventList.checked
readonly property bool isOwn: chat.userId === model.sender_id readonly property bool isOwn: chat.userId === model.sender_id
readonly property bool isRedacted: model.event_type === "RedactedEvent" readonly property bool isRedacted: model.event_type === "RedactedEvent"

View File

@ -17,7 +17,12 @@ HTile {
width: Math.min( width: Math.min(
eventDelegate.width, eventDelegate.width,
eventContent.maxMessageWidth, eventContent.maxMessageWidth,
Math.max(theme.chat.message.fileMinWidth, implicitWidth), Math.max(
window.settings.Chat.Files.min_file_width *
window.settings.General.zoom,
implicitWidth,
),
) )
height: Math.max(theme.chat.message.avatarSize, implicitHeight) height: Math.max(theme.chat.message.avatarSize, implicitHeight)

View File

@ -10,13 +10,16 @@ HMxcImage {
property EventMediaLoader loader property EventMediaLoader loader
readonly property real zoom: window.settings.General.zoom
readonly property real maxHeight: readonly property real maxHeight:
eventList.height * theme.chat.message.thumbnailMaxHeightRatio eventList.height *
window.settings.Chat.Files.max_thumbnail_height_ratio * zoom
readonly property size fitSize: utils.fitSize( readonly property size fitSize: utils.fitSize(
// Minimum display size // Minimum display size
theme.chat.message.thumbnailMinSize.width, window.settings.Chat.Files.min_thumbnail_size[0] * zoom,
theme.chat.message.thumbnailMinSize.height, window.settings.Chat.Files.min_thumbnail_size[1] * zoom,
// Real size // Real size
( (
@ -35,12 +38,18 @@ HMxcImage {
// Maximum display size // Maximum display size
Math.min( Math.min(
Math.max(maxHeight, theme.chat.message.thumbnailMinSize.width), Math.max(
maxHeight,
window.settings.Chat.Files.min_thumbnail_size[0] * zoom,
),
pureMedia ? Infinity : eventContent.maxMessageWidth, pureMedia ? Infinity : eventContent.maxMessageWidth,
eventDelegate.width - eventContent.spacing - avatarWrapper.width - eventDelegate.width - eventContent.spacing - avatarWrapper.width -
eventContent.spacing * 2, // padding eventContent.spacing * 2, // padding
), ),
Math.max(maxHeight, theme.chat.message.thumbnailMinSize.height), Math.max(
maxHeight,
window.settings.Chat.Files.min_thumbnail_size[1] * zoom,
),
) )
readonly property bool hovered: hover.hovered readonly property bool hovered: hover.hovered
@ -92,7 +101,7 @@ HMxcImage {
return return
} }
window.settings.media.openExternallyOnClick ? window.settings.Chat.Files.click_opens_externally ?
image.openExternally() : image.openExternally() :
image.openInternally() image.openInternally()
} }
@ -103,7 +112,7 @@ HMxcImage {
acceptedModifiers: Qt.NoModifier acceptedModifiers: Qt.NoModifier
gesturePolicy: TapHandler.ReleaseWithinBounds gesturePolicy: TapHandler.ReleaseWithinBounds
onTapped: onTapped:
window.settings.media.openExternallyOnClick ? window.settings.Chat.Files.click_opens_externally ?
image.openInternally() : image.openInternally() :
image.openExternally() image.openExternally()
} }

View File

@ -21,7 +21,7 @@ Rectangle {
color: theme.chat.eventList.background color: theme.chat.eventList.background
HShortcut { HShortcut {
sequences: window.settings.keys.unfocusOrDeselectAllMessages sequences: window.settings.Keys.Messages.unfocus_or_deselect
onActivated: { onActivated: {
eventList.selectedCount ? eventList.selectedCount ?
eventList.checked = {} : eventList.checked = {} :
@ -30,24 +30,24 @@ Rectangle {
} }
HShortcut { HShortcut {
sequences: window.settings.keys.focusPreviousMessage sequences: window.settings.Keys.Messages.previous
onActivated: eventList.focusPreviousMessage() onActivated: eventList.focusPreviousMessage()
} }
HShortcut { HShortcut {
sequences: window.settings.keys.focusNextMessage sequences: window.settings.Keys.Messages.next
onActivated: eventList.focusNextMessage() onActivated: eventList.focusNextMessage()
} }
HShortcut { HShortcut {
active: eventList.currentItem active: eventList.currentItem
sequences: window.settings.keys.toggleSelectMessage sequences: window.settings.Keys.Messages.select
onActivated: eventList.toggleCheck(eventList.currentIndex) onActivated: eventList.toggleCheck(eventList.currentIndex)
} }
HShortcut { HShortcut {
active: eventList.currentItem active: eventList.currentItem
sequences: window.settings.keys.selectMessagesUntilHere sequences: window.settings.Keys.Messages.select_until_here
onActivated: onActivated:
eventList.checkFromLastToHere(eventList.currentIndex) eventList.checkFromLastToHere(eventList.currentIndex)
} }
@ -75,7 +75,7 @@ Rectangle {
} }
enabled: (events && events.length > 0) || events === null enabled: (events && events.length > 0) || events === null
sequences: window.settings.keys.removeFocusedOrSelectedMessages sequences: window.settings.Keys.Messages.remove
onActivated: window.makePopup( onActivated: window.makePopup(
"Popups/RedactPopup.qml", "Popups/RedactPopup.qml",
{ {
@ -98,7 +98,7 @@ Rectangle {
} }
HShortcut { HShortcut {
sequences: window.settings.keys.replyToFocusedOrLastMessage sequences: window.settings.Keys.Messages.reply
onActivated: { onActivated: {
let event = eventList.model.get(0) let event = eventList.model.get(0)
@ -132,7 +132,7 @@ Rectangle {
} }
HShortcut { HShortcut {
sequences: window.settings.keys.openMessagesLinksOrFiles sequences: window.settings.Keys.Messages.open_links_files
onActivated: { onActivated: {
const indice = const indice =
eventList.getFocusedOrSelectedOrLastMediaEvents(true) eventList.getFocusedOrSelectedOrLastMediaEvents(true)
@ -158,7 +158,7 @@ Rectangle {
} }
HShortcut { HShortcut {
sequences: window.settings.keys.openMessagesLinksOrFilesExternally sequences: window.settings.Keys.Messages.open_links_files_externally
onActivated: { onActivated: {
const indice = const indice =
eventList.getFocusedOrSelectedOrLastMediaEvents(true) eventList.getFocusedOrSelectedOrLastMediaEvents(true)
@ -178,7 +178,7 @@ Rectangle {
} }
HShortcut { HShortcut {
sequences: window.settings.keys.copyFilesLocalPath sequences: window.settings.Keys.Messages.copy_files_path
onActivated: { onActivated: {
const paths = [] const paths = []
const indice = const indice =
@ -199,14 +199,14 @@ Rectangle {
HShortcut { HShortcut {
active: eventList.currentItem active: eventList.currentItem
sequences: window.settings.keys.debugFocusedMessage sequences: window.settings.Keys.Messages.debug
onActivated: mainUI.debugConsole.toggle( onActivated: mainUI.debugConsole.toggle(
eventList.currentItem.eventContent, "t.parent.json()", eventList.currentItem.eventContent, "t.parent.json()",
) )
} }
HShortcut { HShortcut {
sequences: window.settings.keys.clearRoomMessages sequences: window.settings.Keys.Messages.clear_all
onActivated: window.makePopup( onActivated: window.makePopup(
"Popups/ClearMessagesPopup.qml", "Popups/ClearMessagesPopup.qml",
{ {
@ -231,9 +231,10 @@ Rectangle {
property bool moreToLoad: true property bool moreToLoad: true
property bool ownEventsOnLeft: property bool ownEventsOnLeft:
window.settings.ownMessagesOnLeftAboveWidth < 0 ? window.settings.Chat.own_messages_on_left_above < 0 ?
false : false :
width > window.settings.ownMessagesOnLeftAboveWidth * theme.uiScale width >
window.settings.Chat.own_messages_on_left_above * theme.uiScale
property string delegateWithSelectedText: "" property string delegateWithSelectedText: ""
property string selectedText: "" property string selectedText: ""
@ -616,7 +617,7 @@ Rectangle {
} }
Timer { Timer {
interval: Math.max(100, window.settings.markRoomReadMsecDelay) interval: Math.max(100, window.settings.Chat.mark_read_delay * 1000)
running: running:
! eventList.updateMarkerFutureId && ! eventList.updateMarkerFutureId &&

View File

@ -123,7 +123,7 @@ HPopup {
Timer { Timer {
id: autoHideTimer id: autoHideTimer
interval: window.settings.media.autoHideOSDAfterMsec interval: window.settings.Chat.Files.autohide_image_controls_after
} }
ViewerInfo { ViewerInfo {

View File

@ -29,7 +29,7 @@ HFlow {
visible: viewer.isAnimated visible: viewer.isAnimated
HPopupShortcut { HPopupShortcut {
sequences: window.settings.keys.imageViewer.pause sequences: window.settings.Keys.ImageViewer.pause
onActivated: pause.clicked() onActivated: pause.clicked()
} }
} }
@ -46,7 +46,7 @@ HFlow {
visible: viewer.isAnimated visible: viewer.isAnimated
HPopupShortcut { HPopupShortcut {
sequences: window.settings.keys.imageViewer.previousSpeed sequences: window.settings.Keys.ImageViewer.slow_down
onActivated: viewer.imagesSpeed = viewer.availableSpeeds[Math.min( onActivated: viewer.imagesSpeed = viewer.availableSpeeds[Math.min(
viewer.availableSpeeds.indexOf(viewer.imagesSpeed) + 1, viewer.availableSpeeds.indexOf(viewer.imagesSpeed) + 1,
viewer.availableSpeeds.length - 1, viewer.availableSpeeds.length - 1,
@ -54,14 +54,14 @@ HFlow {
} }
HPopupShortcut { HPopupShortcut {
sequences: window.settings.keys.imageViewer.nextSpeed sequences: window.settings.Keys.ImageViewer.speed_up
onActivated: viewer.imagesSpeed = viewer.availableSpeeds[Math.max( onActivated: viewer.imagesSpeed = viewer.availableSpeeds[Math.max(
viewer.availableSpeeds.indexOf(viewer.imagesSpeed) - 1, 0, viewer.availableSpeeds.indexOf(viewer.imagesSpeed) - 1, 0,
)] )]
} }
HPopupShortcut { HPopupShortcut {
sequences: window.settings.keys.imageViewer.resetSpeed sequences: window.settings.Keys.ImageViewer.reset_speed
onActivated: viewer.imagesSpeed = 1 onActivated: viewer.imagesSpeed = 1
} }
} }
@ -77,7 +77,7 @@ HFlow {
onPressed: viewer.animatedRotationTarget -= 45 onPressed: viewer.animatedRotationTarget -= 45
HPopupShortcut { HPopupShortcut {
sequences: window.settings.keys.imageViewer.rotateLeft sequences: window.settings.Keys.ImageViewer.rotate_left
onActivated: viewer.animatedRotationTarget -= 45 onActivated: viewer.animatedRotationTarget -= 45
} }
} }
@ -93,13 +93,13 @@ HFlow {
onPressed: viewer.animatedRotationTarget += 45 onPressed: viewer.animatedRotationTarget += 45
HPopupShortcut { HPopupShortcut {
sequences: window.settings.keys.imageViewer.rotateRight sequences: window.settings.Keys.ImageViewer.rotate_right
onActivated: viewer.animatedRotationTarget += 45 onActivated: viewer.animatedRotationTarget += 45
} }
} }
HPopupShortcut { HPopupShortcut {
sequences: window.settings.keys.imageViewer.rotateReset sequences: window.settings.Keys.ImageViewer.reset_rotation
onActivated: viewer.animatedRotationTarget = 0 onActivated: viewer.animatedRotationTarget = 0
} }
@ -116,7 +116,7 @@ HFlow {
onClicked: viewer.alternateScaling = ! viewer.alternateScaling onClicked: viewer.alternateScaling = ! viewer.alternateScaling
HPopupShortcut { HPopupShortcut {
sequences: window.settings.keys.imageViewer.expand sequences: window.settings.Keys.ImageViewer.expand
onActivated: expand.clicked() onActivated: expand.clicked()
} }
} }
@ -131,7 +131,7 @@ HFlow {
visible: Qt.application.supportsMultipleWindows visible: Qt.application.supportsMultipleWindows
HPopupShortcut { HPopupShortcut {
sequences: window.settings.keys.imageViewer.fullScreen sequences: window.settings.Keys.ImageViewer.fullscreen
onActivated: fullScreen.clicked() onActivated: fullScreen.clicked()
} }
} }
@ -144,7 +144,7 @@ HFlow {
onClicked: viewer.close() onClicked: viewer.close()
HPopupShortcut { HPopupShortcut {
sequences: window.settings.keys.imageViewer.close sequences: window.settings.Keys.ImageViewer.close
onActivated: close.clicked() onActivated: close.clicked()
} }
} }

View File

@ -53,37 +53,37 @@ HFlickable {
} }
HPopupShortcut { HPopupShortcut {
sequences: window.settings.keys.imageViewer.panLeft sequences: window.settings.Keys.ImageViewer.pan_left
onActivated: utils.flickPages(flickable, -0.2, true, 5) onActivated: utils.flickPages(flickable, -0.2, true, 5)
} }
HPopupShortcut { HPopupShortcut {
sequences: window.settings.keys.imageViewer.panRight sequences: window.settings.Keys.ImageViewer.pan_right
onActivated: utils.flickPages(flickable, 0.2, true, 5) onActivated: utils.flickPages(flickable, 0.2, true, 5)
} }
HPopupShortcut { HPopupShortcut {
sequences: window.settings.keys.imageViewer.panUp sequences: window.settings.Keys.ImageViewer.pan_up
onActivated: utils.flickPages(flickable, -0.2, false, 5) onActivated: utils.flickPages(flickable, -0.2, false, 5)
} }
HPopupShortcut { HPopupShortcut {
sequences: window.settings.keys.imageViewer.panDown sequences: window.settings.Keys.ImageViewer.pan_down
onActivated: utils.flickPages(flickable, 0.2, false, 5) onActivated: utils.flickPages(flickable, 0.2, false, 5)
} }
HPopupShortcut { HPopupShortcut {
sequences: window.settings.keys.imageViewer.zoomOut sequences: window.settings.Keys.ImageViewer.zoom_out
onActivated: thumbnail.scale = Math.max(0.1, thumbnail.scale - 0.2) onActivated: thumbnail.scale = Math.max(0.1, thumbnail.scale - 0.2)
} }
HPopupShortcut { HPopupShortcut {
sequences: window.settings.keys.imageViewer.zoomIn sequences: window.settings.Keys.ImageViewer.zoom_in
onActivated: thumbnail.scale = Math.min(10, thumbnail.scale + 0.2) onActivated: thumbnail.scale = Math.min(10, thumbnail.scale + 0.2)
} }
HPopupShortcut { HPopupShortcut {
sequences: window.settings.keys.imageViewer.zoomReset sequences: window.settings.Keys.ImageViewer.reset_zoom
onActivated: resetScaleAnimation.start() onActivated: resetScaleAnimation.start()
} }

View File

@ -28,8 +28,8 @@ QtObject {
const msec = const msec =
highImportance ? highImportance ?
window.settings.alertOnMentionForMsec : window.settings.Notifications.urgent_alert_time * 1000 :
window.settings.alertOnMessageForMsec window.settings.Notifications.alert_time * 1000
if (msec) window.alert(msec === -1 ? 0 : msec) // -1 0 = no time out if (msec) window.alert(msec === -1 ? 0 : msec) // -1 0 = no time out
} }

View File

@ -13,37 +13,37 @@ HQtObject {
HShortcut { HShortcut {
active: root.active active: root.active
sequences: window.settings.keys.scrollUp sequences: window.settings.Keys.Scrolling.up
onActivated: utils.flickPages(flickable, -1 / 10) onActivated: utils.flickPages(flickable, -1 / 10)
} }
HShortcut { HShortcut {
active: root.active active: root.active
sequences: window.settings.keys.scrollDown sequences: window.settings.Keys.Scrolling.down
onActivated: utils.flickPages(flickable, 1 / 10) onActivated: utils.flickPages(flickable, 1 / 10)
} }
HShortcut { HShortcut {
active: root.active active: root.active
sequences: window.settings.keys.scrollPageUp sequences: window.settings.Keys.Scrolling.page_up
onActivated: utils.flickPages(flickable, -1) onActivated: utils.flickPages(flickable, -1)
} }
HShortcut { HShortcut {
active: root.active active: root.active
sequences: window.settings.keys.scrollPageDown sequences: window.settings.Keys.Scrolling.page_down
onActivated: utils.flickPages(flickable, 1) onActivated: utils.flickPages(flickable, 1)
} }
HShortcut { HShortcut {
active: root.active active: root.active
sequences: window.settings.keys.scrollToTop sequences: window.settings.Keys.Scrolling.top
onActivated: utils.flickToTop(flickable) onActivated: utils.flickToTop(flickable)
} }
HShortcut { HShortcut {
active: root.active active: root.active
sequences: window.settings.keys.scrollToBottom sequences: window.settings.Keys.Scrolling.bottom
onActivated: utils.flickToBottom(flickable) onActivated: utils.flickToBottom(flickable)
} }
} }

View File

@ -13,7 +13,7 @@ HQtObject {
HShortcut { HShortcut {
active: root.active active: root.active
sequences: window.settings.keys.previousTab sequences: window.settings.Keys.previous_tab
onActivated: container.setCurrentIndex( onActivated: container.setCurrentIndex(
utils.numberWrapAt(container.currentIndex - 1, container.count), utils.numberWrapAt(container.currentIndex - 1, container.count),
) )
@ -21,7 +21,7 @@ HQtObject {
HShortcut { HShortcut {
active: root.active active: root.active
sequences: window.settings.keys.nextTab sequences: window.settings.Keys.next_tab
onActivated: container.setCurrentIndex( onActivated: container.setCurrentIndex(
utils.numberWrapAt(container.currentIndex + 1, container.count), utils.numberWrapAt(container.currentIndex + 1, container.count),
) )

View File

@ -36,39 +36,41 @@ Item {
Component.onCompleted: window.mainUI = mainUI Component.onCompleted: window.mainUI = mainUI
HShortcut { HShortcut {
sequences: window.settings.keys.startPythonDebugger sequences: window.settings.Keys.python_debugger
onActivated: py.call("BRIDGE.pdb") onActivated: py.call("BRIDGE.pdb")
} }
HShortcut { HShortcut {
sequences: window.settings.keys.zoomIn sequences: window.settings.Keys.zoom_in
onActivated: { onActivated: {
window.settings.zoom += 0.1 window.settings.General.zoom += 0.1
window.settingsChanged() window.saveSettings()
} }
} }
HShortcut { HShortcut {
sequences: window.settings.keys.zoomOut sequences: window.settings.Keys.zoom_out
onActivated: { onActivated: {
window.settings.zoom = Math.max(0.1, window.settings.zoom - 0.1) window.settings.General.zoom =
window.settingsChanged() Math.max(0.1, window.settings.General.zoom - 0.1)
window.saveSettings()
} }
} }
HShortcut { HShortcut {
sequences: window.settings.keys.zoomReset sequences: window.settings.Keys.reset_zoom
onActivated: { onActivated: {
window.settings.zoom = 1 window.settings.General.zoom = 1
window.settingsChanged() window.saveSettings()
} }
} }
HShortcut { HShortcut {
sequences: window.settings.keys.toggleCompactMode sequences: window.settings.Keys.compact
onActivated: { onActivated: {
settings.compactMode = ! settings.compactMode window.settings.General.compact = ! window.settings.General.compact
settingsChanged() windowsaveSettings()
} }
} }

View File

@ -36,6 +36,20 @@ ApplicationWindow {
readonly property bool anyPopup: Object.keys(visiblePopups).length > 0 readonly property bool anyPopup: Object.keys(visiblePopups).length > 0
readonly property bool anyPopupOrMenu: anyMenu || anyPopup readonly property bool anyPopupOrMenu: anyMenu || anyPopup
function saveSettings() {
settingsChanged()
py.saveConfig("settings", settings)
}
function saveUIState() {
uiStateChanged()
py.saveConfig("ui_state", uiState)
}
function saveHistory() {
historyChanged()
py.saveConfig("history", history)
}
function saveState(obj) { function saveState(obj) {
if (! obj.saveName || ! obj.saveProperties || if (! obj.saveName || ! obj.saveProperties ||
@ -51,7 +65,7 @@ ApplicationWindow {
[obj.saveName]: { [obj.saveId || "ALL"]: propertyValues }, [obj.saveName]: { [obj.saveId || "ALL"]: propertyValues },
}) })
uiStateChanged() saveUIState()
} }
function getState(obj, property, defaultValue=undefined) { function getState(obj, property, defaultValue=undefined) {
@ -86,15 +100,9 @@ ApplicationWindow {
visible: true visible: true
color: "transparent" color: "transparent"
// NOTE: For JS object variables, the corresponding method to notify
// key/value changes must be called manually, e.g. settingsChanged().
onSettingsChanged: py.saveConfig("settings", settings)
onUiStateChanged: py.saveConfig("ui_state", uiState)
onHistoryChanged: py.saveConfig("history", history)
onClosing: { onClosing: {
close.accepted = ! settings.closeMinimizesToTray close.accepted = ! settings.General.close_to_tray
settings.closeMinimizesToTray ? hide() : Qt.quit() settings.General.close_to_tray ? hide() : Qt.quit()
} }
PythonRootBridge { id: py } PythonRootBridge { id: py }

View File

@ -2,7 +2,7 @@
// Base variables // Base variables
real uiScale: window.settings.zoom real uiScale: window.settings.General.zoom
int minimumSupportedWidth: 240 * uiScale int minimumSupportedWidth: 240 * uiScale
int minimumSupportedHeight: 120 * uiScale int minimumSupportedHeight: 120 * uiScale
@ -211,7 +211,6 @@ controls:
color placeholderText: controls.textField.placeholderText color placeholderText: controls.textField.placeholderText
toolTip: toolTip:
int delay: 500
color background: colors.strongBackground color background: colors.strongBackground
color text: colors.text color text: colors.text
color border: "black" color border: "black"
@ -296,7 +295,6 @@ ui:
mainPane: mainPane:
int minimumSize: 144 * uiScale
color background: "transparent" color background: "transparent"
topBar: topBar:
@ -465,17 +463,6 @@ chat:
string styleInclude: string styleInclude:
'<style type"text/css">\n' + styleSheet + '\n</style>\n' '<style type"text/css">\n' + styleSheet + '\n</style>\n'
// Prefered minimum width of file messages
int fileMinWidth: 256 * uiScale
// Don't scale down thumbnails below this size in pixels, if
// the becomes too small to show it at normal size.
size thumbnailMinSize: Qt.size(256 * uiScale, 256 * uiScale)
// How much of the chat height thumbnails can take at most,
// by default 0.4 for 40%.
real thumbnailMaxHeightRatio: 0.4 * Math.min(1, uiScale)
real thumbnailCheckedOverlayOpacity: 0.4 real thumbnailCheckedOverlayOpacity: 0.4
daybreak: daybreak:

View File

@ -2,7 +2,7 @@
// Base variables // Base variables
real uiScale: window.settings.zoom real uiScale: window.settings.General.zoom
int minimumSupportedWidth: 240 * uiScale int minimumSupportedWidth: 240 * uiScale
int minimumSupportedHeight: 120 * uiScale int minimumSupportedHeight: 120 * uiScale
@ -217,7 +217,6 @@ controls:
color placeholderText: controls.textField.placeholderText color placeholderText: controls.textField.placeholderText
toolTip: toolTip:
int delay: 500
color background: colors.strongBackground color background: colors.strongBackground
color text: colors.text color text: colors.text
color border: "black" color border: "black"
@ -309,7 +308,6 @@ ui:
mainPane: mainPane:
int minimumSize: 144 * uiScale
color background: "transparent" color background: "transparent"
topBar: topBar:
@ -474,17 +472,6 @@ chat:
string styleInclude: string styleInclude:
'<style type"text/css">\n' + styleSheet + '\n</style>\n' '<style type"text/css">\n' + styleSheet + '\n</style>\n'
// Prefered minimum width of file messages
int fileMinWidth: 256 * uiScale
// Don't scale down thumbnails below this size in pixels, if
// the becomes too small to show it at normal size.
size thumbnailMinSize: Qt.size(256 * uiScale, 256 * uiScale)
// How much of the chat height thumbnails can take at most,
// by default 0.4 for 40%.
real thumbnailMaxHeightRatio: 0.4 * Math.min(1, uiScale)
real thumbnailCheckedOverlayOpacity: 0.4 real thumbnailCheckedOverlayOpacity: 0.4
daybreak: daybreak: