Use new PCN format for settings config file
This commit is contained in:
parent
6ce3059322
commit
db12036372
1
TODO.md
1
TODO.md
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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, {}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
4
src/backend/pcn/__init__.py
Normal file
4
src/backend/pcn/__init__.py
Normal 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."""
|
37
src/backend/pcn/globals_dict.py
Normal file
37
src/backend/pcn/globals_dict.py
Normal 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)
|
52
src/backend/pcn/property.py
Normal file
52
src/backend/pcn/property.py
Normal 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
395
src/backend/pcn/section.py
Normal 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))
|
|
@ -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
|
||||||
|
@ -36,18 +39,19 @@ class UserFile:
|
||||||
|
|
||||||
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):
|
||||||
|
try:
|
||||||
ignored = 0
|
ignored = 0
|
||||||
|
|
||||||
for change in changes:
|
for change in changes:
|
||||||
|
mtime = self.path.stat().st_mtime
|
||||||
|
|
||||||
if change[0] in (Change.added, Change.modified):
|
if change[0] in (Change.added, Change.modified):
|
||||||
if self._need_write or self._wrote:
|
if mtime == self._mtime:
|
||||||
self._wrote = False
|
|
||||||
ignored += 1
|
ignored += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
async with aiofiles.open(self.path) as file:
|
async with aiofiles.open(self.path) as file:
|
||||||
self.data, save = self.deserialized(await file.read())
|
text = await file.read()
|
||||||
|
self.data, save = self.deserialized(text)
|
||||||
|
|
||||||
if save:
|
if save:
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
elif change[0] == Change.deleted:
|
elif change[0] == Change.deleted:
|
||||||
self._wrote = False
|
|
||||||
self.data = self.default_data
|
self.data = self.default_data
|
||||||
self._need_write = self.create_missing
|
self._need_write = self.create_missing
|
||||||
|
|
||||||
|
self._mtime = mtime
|
||||||
|
|
||||||
if changes and ignored < len(changes):
|
if changes and ignored < len(changes):
|
||||||
UserFileChanged(type(self), self.data)
|
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)
|
||||||
|
|
||||||
|
try:
|
||||||
if self._need_write:
|
if self._need_write:
|
||||||
async with atomic_write(self.path) as (new, done):
|
async with atomic_write(self.write_path) as (new, done):
|
||||||
await new.write(self.serialized())
|
await new.write(self.serialized())
|
||||||
done()
|
done()
|
||||||
|
|
||||||
self._need_write = False
|
self._need_write = False
|
||||||
self._wrote = True
|
self._mtime = self.write_path.stat().st_mtime
|
||||||
|
|
||||||
|
except Exception as err:
|
||||||
|
self._need_write = False
|
||||||
|
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
|
||||||
|
|
|
@ -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", " " * 4)
|
.replace("\t", " " * 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
430
src/config/settings.py
Normal 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"]
|
|
@ -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) *
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {} }
|
||||||
|
|
||||||
|
|
|
@ -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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 &&
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue
Block a user