Compare commits

...

1 Commits

Author SHA1 Message Date
Tobias Fella
050f480955 Fix emojis in a different way
Apparently the existing fix is difficult to ship for distros. This is an alternative way, that manually goes over every text and sets the font family appropriately.
Unfortunately, we need a textDocument for that, which labels don't have, meaning we have to use TextEdits instead...
2023-05-30 18:37:15 +02:00
15 changed files with 367 additions and 128 deletions

View File

@@ -44,10 +44,6 @@ if (NOT ANDROID)
include(KDEClangFormat)
endif()
if(NEOCHAT_FLATPAK)
include(cmake/Flatpak.cmake)
endif()
ecm_setup_version(${PROJECT_VERSION}
VARIABLE_PREFIX NEOCHAT
VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h
@@ -75,6 +71,13 @@ set_package_properties(Qt${QT_MAJOR_VERSION}Keychain PROPERTIES
PURPOSE "Secure storage of account secrets"
)
find_package(ICU 61.0 COMPONENTS uc)
set_package_properties(ICU PROPERTIES
TYPE REQUIRED
PURPOSE "Unicode library"
)
if(ANDROID)
find_package(OpenSSL)
set_package_properties(OpenSSL PROPERTIES

View File

@@ -1,14 +0,0 @@
# SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
# SPDX-License-Identifier: BSD-2-Clause
include(GNUInstallDirs)
# Include FontConfig config which uses the Emoji One font from the
# KDE Flatpak SDK.
install(
FILES
${CMAKE_CURRENT_SOURCE_DIR}/cmake/Flatpak/99-noto-mono-color-emoji.conf
DESTINATION
${CMAKE_INSTALL_SYSCONFDIR}/fonts/local.conf
)

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<alias>
<family>serif</family>
<prefer>
<family>Noto Color Emoji</family>
</prefer>
</alias>
<alias>
<family>sans-serif</family>
<prefer>
<family>Noto Color Emoji</family>
</prefer>
</alias>
<alias>
<family>monospace</family>
<prefer>
<family>Noto Color Emoji</family>
</prefer>
</alias>
</fontconfig>

View File

@@ -55,6 +55,7 @@ add_library(neochat STATIC
events/joinrulesevent.cpp
events/stickerevent.cpp
models/reactionmodel.cpp
emojifixer.cpp
)
ecm_qt_declare_logging_category(neochat
@@ -109,7 +110,7 @@ else()
endif()
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR})
target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia Qt::Network Qt::QuickControls2 KF${QT_MAJOR_VERSION}::I18n KF${QT_MAJOR_VERSION}::Kirigami2 KF${QT_MAJOR_VERSION}::Notifications KF${QT_MAJOR_VERSION}::ConfigCore KF${QT_MAJOR_VERSION}::ConfigGui KF${QT_MAJOR_VERSION}::CoreAddons KF${QT_MAJOR_VERSION}::SonnetCore KF${QT_MAJOR_VERSION}::ItemModels Quotient${QUOTIENT_SUFFIX} cmark::cmark ${QTKEYCHAIN_LIBRARIES} QCoro::Core)
target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia Qt::Network Qt::QuickControls2 KF${QT_MAJOR_VERSION}::I18n KF${QT_MAJOR_VERSION}::Kirigami2 KF${QT_MAJOR_VERSION}::Notifications KF${QT_MAJOR_VERSION}::ConfigCore KF${QT_MAJOR_VERSION}::ConfigGui KF${QT_MAJOR_VERSION}::CoreAddons KF${QT_MAJOR_VERSION}::SonnetCore KF${QT_MAJOR_VERSION}::ItemModels Quotient${QUOTIENT_SUFFIX} cmark::cmark ${QTKEYCHAIN_LIBRARIES} QCoro::Core ICU::uc)
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
if(NEOCHAT_FLATPAK)

View File

@@ -14,6 +14,7 @@
#include <Sonnet/BackgroundChecker>
#include <Sonnet/Settings>
#include "emojifixer.h"
#include "neochatroom.h"
class SyntaxHighlighter : public QSyntaxHighlighter
@@ -183,6 +184,8 @@ void ChatDocumentHandler::setDocument(QQuickTextDocument *document)
m_document->textDocument()->disconnect(this);
}
m_document = document;
static EmojiFixer emojiFixer;
emojiFixer.addTextDocument(document);
Q_EMIT documentChanged();
}

72
src/emojifixer.cpp Normal file
View File

@@ -0,0 +1,72 @@
// SPDX-FileCopyrightText: Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QGuiApplication>
#include <QPalette>
#include <QQuickTextDocument>
#include <QTextBoundaryFinder>
#include <QTextCharFormat>
#include <QTextCursor>
#include <KLocalizedString>
#include <unicode/uchar.h>
#include <unicode/urename.h>
#include "emojifixer.h"
bool isEmoji(const QString &text)
{
QTextBoundaryFinder finder(QTextBoundaryFinder::Grapheme, text);
int from = 0;
while (finder.toNextBoundary() != -1) {
auto to = finder.position();
if (text[from].isSpace()) {
from = to;
continue;
}
auto first = text.mid(from, to - from).toUcs4()[0];
if (!u_hasBinaryProperty(first, UCHAR_EMOJI_PRESENTATION)) {
return false;
}
from = to;
}
return true;
}
void EmojiFixer::addTextDocument(QQuickTextDocument *document)
{
if (!document) {
return;
}
fix(document->textDocument());
}
void EmojiFixer::fix(QTextDocument *document)
{
disconnect(document, nullptr, this, nullptr);
QTextCursor curs(document);
QTextCharFormat format;
auto font = QGuiApplication::font();
font.setFamily("emoji");
format.setFont(font);
QTextBoundaryFinder finder(QTextBoundaryFinder::Grapheme, document->toRawText());
int from = 0;
while (finder.toNextBoundary() != -1) {
auto to = finder.position();
auto first = document->toRawText().mid(from, to - from).toUcs4()[0];
if (u_hasBinaryProperty(first, UCHAR_EMOJI_PRESENTATION)) {
curs.setPosition(from, QTextCursor::MoveAnchor);
curs.setPosition(to, QTextCursor::KeepAnchor);
curs.setCharFormat(format);
}
from = to;
}
connect(document, &QTextDocument::contentsChanged, this, [this, document]() {
fix(document);
});
}

20
src/emojifixer.h Normal file
View File

@@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QObject>
class QQuickTextDocument;
class QTextDocument;
class EmojiFixer : public QObject
{
Q_OBJECT
public:
Q_INVOKABLE void addTextDocument(QQuickTextDocument *doc);
private:
void fix(QTextDocument *doc);
};

View File

@@ -88,6 +88,7 @@
#ifdef HAVE_COLORSCHEME
#include "colorschemer.h"
#endif
#include "emojifixer.h"
#include "models/completionmodel.h"
#include "models/statemodel.h"
#include "neochatuser.h"
@@ -188,12 +189,6 @@ int main(int argc, char *argv[])
initLogging();
#ifdef NEOCHAT_FLATPAK
// Copy over the included FontConfig configuration to the
// app's config dir:
QFile::copy("/app/etc/fonts/conf.d/99-noto-mono-color-emoji.conf", "/var/config/fontconfig/conf.d/99-noto-mono-color-emoji.conf");
#endif
Clipboard clipboard;
auto config = NeoChatConfig::self();
FileTypeSingleton fileTypeSingleton;
@@ -246,6 +241,7 @@ int main(int argc, char *argv[])
qmlRegisterType<StateModel>("org.kde.neochat", 1, 0, "StateModel");
qmlRegisterType<StateFilterModel>("org.kde.neochat", 1, 0, "StateFilterModel");
qmlRegisterType<SearchModel>("org.kde.neochat", 1, 0, "SearchModel");
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "EmojiFixer", new EmojiFixer);
#ifdef QUOTIENT_07
qmlRegisterType<PollHandler>("org.kde.neochat", 1, 0, "PollHandler");
#endif

View File

@@ -165,7 +165,7 @@ ColumnLayout {
Component {
id: emojiDelegate
Kirigami.NavigationTabButton {
NeoChatTabButton {
width: root.categoryIconSize
height: width
checked: categories.currentIndex === model.index

View File

@@ -0,0 +1,92 @@
// SPDX-FileCopyrightText: 2021 Devin Lin <espidev@gmail.com>
// SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Templates 2.15 as T
import org.kde.kirigami 2.19 as Kirigami
import org.kde.neochat 1.0
T.TabButton {
id: control
property color foregroundColor: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.85)
property color highlightForegroundColor: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.85)
property color highlightBarColor: Kirigami.Theme.highlightColor
property color pressedColor: Qt.rgba(highlightBarColor.r, highlightBarColor.g, highlightBarColor.b, 0.3)
property color hoverSelectColor: Qt.rgba(highlightBarColor.r, highlightBarColor.g, highlightBarColor.b, 0.2)
property color checkedBorderColor: Qt.rgba(highlightBarColor.r, highlightBarColor.g, highlightBarColor.b, 0.7)
property color pressedBorderColor: Qt.rgba(highlightBarColor.r, highlightBarColor.g, highlightBarColor.b, 0.9)
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
implicitContentWidth + leftPadding + rightPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
implicitContentHeight + topPadding + bottomPadding)
display: T.AbstractButton.TextUnderIcon
Kirigami.Theme.colorSet: Kirigami.Theme.Window
Kirigami.Theme.inherit: false
// not using the hover handler built into control, since it seems to misbehave and
// permanently report hovered after a touch event
HoverHandler {
id: hoverHandler
}
padding: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.smallSpacing
icon.height: control.display === T.AbstractButton.TextBesideIcon ? Kirigami.Units.iconSizes.small : Kirigami.Units.iconSizes.smallMedium
icon.width: control.display === T.AbstractButton.TextBesideIcon ? Kirigami.Units.iconSizes.small : Kirigami.Units.iconSizes.smallMedium
icon.color: control.checked ? control.highlightForegroundColor : control.foregroundColor
background: Rectangle {
Kirigami.Theme.colorSet: Kirigami.Theme.Button
Kirigami.Theme.inherit: false
implicitHeight: Kirigami.Units.gridUnit * 3 + Kirigami.Units.smallSpacing * 2
color: "transparent"
Rectangle {
width: parent.width - Kirigami.Units.largeSpacing
height: parent.height - Kirigami.Units.largeSpacing
anchors.centerIn: parent
radius: Kirigami.Units.smallSpacing
color: control.down ? pressedColor : (control.checked || hoverHandler.hovered ? hoverSelectColor : "transparent")
border.color: control.checked ? checkedBorderColor : (control.down ? pressedBorderColor : color)
border.width: 1
Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration } }
Behavior on border.color { ColorAnimation { duration: Kirigami.Units.shortDuration } }
}
}
contentItem: QQC2.Label {
id: label
Kirigami.MnemonicData.enabled: control.enabled && control.visible
Kirigami.MnemonicData.controlType: Kirigami.MnemonicData.MenuItem
Kirigami.MnemonicData.label: control.text
text: Kirigami.MnemonicData.richTextLabel
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
elide: Text.ElideMiddle
color: control.checked ? control.highlightForegroundColor : control.foregroundColor
font.bold: control.checked
font.family: "emoji"
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 1.20
Behavior on color { ColorAnimation {} }
Behavior on opacity { NumberAnimation {} }
}
}

View File

@@ -6,6 +6,7 @@ import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
Flow {
id: root
@@ -28,15 +29,18 @@ Flow {
id: reactionRepeater
model: root.model
delegate: QQC2.AbstractButton {
delegate: QQC2.Control {
width: Math.max(reactionTextMetrics.advanceWidth + Kirigami.Units.smallSpacing * 4, height)
contentItem: QQC2.Label {
contentItem: TextEdit {
id: reactionLabel
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: model.text
readOnly: true
Component.onCompleted: EmojiFixer.addTextDocument(reactionLabel.textDocument)
color: Kirigami.Theme.textColor
selectByMouse: false
TextMetrics {
id: reactionTextMetrics
text: reactionLabel.text
@@ -56,12 +60,16 @@ Flow {
border.width: 1
}
onClicked: reactionClicked(model.reaction)
MouseArea {
anchors.fill: parent
onClicked: reactionClicked(model.reaction)
hoverEnabled: true
}
hoverEnabled: true
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: model.toolTip
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
}

View File

@@ -43,9 +43,12 @@ TextEdit {
persistentSelection: true
// Work around QTBUG 93281
Component.onCompleted: if (text.includes("<img")) {
Controller.forceRefreshTextDocument(root.textDocument, root)
Component.onCompleted: {
EmojiFixer.addTextDocument(root.textDocument)
// Work around QTBUG 93281
if (text.includes("<img")) {
Controller.forceRefreshTextDocument(root.textDocument, root)
}
}
text: "<style>

View File

@@ -13,7 +13,7 @@ import org.kde.neochat 1.0
import './' as RoomList
Kirigami.BasicListItem {
Kirigami.AbstractListItem {
id: root
required property int index
@@ -26,7 +26,6 @@ Kirigami.BasicListItem {
required property string subtitleText
required property string displayName
readonly property bool hasNotifications: notificationCount > 0
topPadding: Kirigami.Units.largeSpacing
@@ -35,16 +34,154 @@ Kirigami.BasicListItem {
visible: root.categoryVisible || root.filterText.length > 0 || Config.mergeRoomList
highlighted: ListView.view.currentIndex === index
focus: true
icon: undefined
bold: root.hasNotifications
property bool bold: root.hasNotifications
label: root.displayName
labelItem.textFormat: Text.PlainText
contentItem: Item {
id: contItem
subtitle: root.subtitleText
subtitleItem {
textFormat: Text.PlainText
visible: !Config.compactRoomList
implicitWidth: layout.implicitWidth
Binding on implicitHeight {
value: Math.max(iconItem.size, (!subtitleItem.visible && root.reserveSpaceForSubtitle ? (labelItem.implicitHeight + labelColumn.spacing + subtitleItem.implicitHeight): labelColumn.implicitHeight) )
delayed: true
}
RowLayout {
id: layout
spacing: LayoutMirroring.enabled ? root.rightPadding : root.leftPadding
anchors.left: contItem.left
anchors.leftMargin: root.leading ? root.leadingPadding : 0
anchors.right: contItem.right
anchors.rightMargin: root.trailing ? root.trailingPadding : 0
anchors.verticalCenter: parent.verticalCenter
Kirigami.Avatar {
source: root.avatar ? `image://mxc/${root.avatar}` : ""
name: root.displayName
implicitWidth: visible ? height : 0
visible: Config.showAvatarInRoomDrawer
Layout.preferredWidth: sourceSize.width
Layout.preferredHeight: sourceSize.height
sourceSize {
width: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
height: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
}
}
Kirigami.Icon {
id: iconItem
source: root.icon.name !== "" ? root.icon.name : root.icon.source
property int size: subtitleItem.visible || reserveSpaceForSubtitle ? Kirigami.Units.iconSizes.medium : Kirigami.Units.iconSizes.smallMedium
Layout.minimumHeight: size
Layout.maximumHeight: size
Layout.minimumWidth: size
Layout.maximumWidth: size
selected: (root.highlighted || root.checked || root.down)
opacity: root.fadeContent ? 0.6 : 1.0
visible: source.toString() !== ""
}
ColumnLayout {
id: labelColumn
spacing: 0
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
TextEdit {
id: labelItem
text: labelMetrics.elidedText
Layout.fillWidth: true
Layout.alignment: subtitleItem.visible ? Qt.AlignLeft | Qt.AlignBottom : Qt.AlignLeft | Qt.AlignVCenter
color: (root.highlighted || root.checked || root.down) ? root.activeTextColor : root.textColor
font.weight: root.bold ? Font.Bold : Font.Normal
opacity: root.fadeContent ? 0.6 : 1.0
readOnly: true
Component.onCompleted: EmojiFixer.addTextDocument(labelItem.textDocument)
TextMetrics {
id: labelMetrics
font: labelItem.font
text: root.displayName
elideWidth: labelItem.width
elide: Qt.ElideRight
}
MouseArea {
anchors.fill: parent
onClicked: RoomManager.enterRoom(root.currentRoom)
onPressAndHold: createRoomListContextMenu()
}
}
TextEdit {
id: subtitleItem
Layout.fillWidth: true
Layout.alignment: subtitleItem.visible ? Qt.AlignLeft | Qt.AlignTop : Qt.AlignLeft | Qt.AlignVCenter
color: (root.highlighted || root.checked || root.down) ? root.activeTextColor : root.textColor
//elide: Text.ElideRight
font: Kirigami.Theme.smallFont
opacity: root.fadeContent ? 0.6 : (root.bold ? 0.9 : 0.7)
text: subtitleMetrics.elidedText
textFormat: Text.PlainText
visible: !Config.compactRoomList
readOnly: true
Component.onCompleted: EmojiFixer.addTextDocument(subtitleItem.textDocument)
TextMetrics {
id: subtitleMetrics
font: subtitleItem.font
text: root.subtitleText
elideWidth: subtitleItem.width
elide: Qt.ElideRight
}
MouseArea {
anchors.fill: parent
onClicked: RoomManager.enterRoom(root.currentRoom)
onPressAndHold: createRoomListContextMenu()
}
}
}
Kirigami.Icon {
source: "notifications-disabled"
enabled: false
implicitWidth: Kirigami.Units.iconSizes.smallMedium
implicitHeight: Kirigami.Units.iconSizes.smallMedium
visible: currentRoom.pushNotificationState === PushNotificationState.Mute && !configButton.visible && !hasNotifications
Accessible.name: i18n("Muted room")
Layout.rightMargin: Kirigami.Units.smallSpacing
}
QQC2.Label {
id: notificationCountLabel
text: notificationCount
visible: hasNotifications
color: Kirigami.Theme.textColor
horizontalAlignment: Text.AlignHCenter
background: Rectangle {
visible: notificationCount > 0
Kirigami.Theme.colorSet: Kirigami.Theme.Button
color: highlightCount > 0 ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.disabledTextColor
opacity: highlightCount > 0 ? 1 : 0.3
radius: height / 2
}
Layout.rightMargin: Kirigami.Units.smallSpacing
Layout.minimumHeight: Kirigami.Units.iconSizes.smallMedium
Layout.minimumWidth: Math.max(notificationCountTextMetrics.advanceWidth + Kirigami.Units.smallSpacing * 2, height)
TextMetrics {
id: notificationCountTextMetrics
text: notificationCountLabel.text
}
}
QQC2.Button {
id: configButton
visible: root.hovered && !Kirigami.Settings.isMobile && !Config.compactRoomList
text: i18n("Configure room")
display: QQC2.Button.IconOnly
icon.name: "configure"
onClicked: createRoomListContextMenu()
}
TapHandler {
acceptedButtons: Qt.RightButton
acceptedDevices: PointerDevice.Mouse
onTapped: createRoomListContextMenu()
}
}
}
onClicked: RoomManager.enterRoom(root.currentRoom)
@@ -53,67 +190,6 @@ Kirigami.BasicListItem {
Keys.onEnterPressed: RoomManager.enterRoom(root.currentRoom)
Keys.onReturnPressed: RoomManager.enterRoom(root.currentRoom)
TapHandler {
acceptedButtons: Qt.RightButton
acceptedDevices: PointerDevice.Mouse
onTapped: createRoomListContextMenu()
}
leading: Kirigami.Avatar {
source: root.avatar ? `image://mxc/${root.avatar}` : ""
name: root.displayName
implicitWidth: visible ? height : 0
visible: Config.showAvatarInRoomDrawer
sourceSize {
width: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
height: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
}
}
trailing: RowLayout {
Kirigami.Icon {
source: "notifications-disabled"
enabled: false
implicitWidth: Kirigami.Units.iconSizes.smallMedium
implicitHeight: Kirigami.Units.iconSizes.smallMedium
visible: currentRoom.pushNotificationState === PushNotificationState.Mute && !configButton.visible && !hasNotifications
Accessible.name: i18n("Muted room")
Layout.rightMargin: Kirigami.Units.smallSpacing
}
QQC2.Label {
id: notificationCountLabel
text: notificationCount
visible: hasNotifications
color: Kirigami.Theme.textColor
horizontalAlignment: Text.AlignHCenter
background: Rectangle {
visible: notificationCount > 0
Kirigami.Theme.colorSet: Kirigami.Theme.Button
color: highlightCount > 0 ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.disabledTextColor
opacity: highlightCount > 0 ? 1 : 0.3
radius: height / 2
}
Layout.rightMargin: Kirigami.Units.smallSpacing
Layout.minimumHeight: Kirigami.Units.iconSizes.smallMedium
Layout.minimumWidth: Math.max(notificationCountTextMetrics.advanceWidth + Kirigami.Units.smallSpacing * 2, height)
TextMetrics {
id: notificationCountTextMetrics
text: notificationCountLabel.text
}
}
QQC2.Button {
id: configButton
visible: root.hovered && !Kirigami.Settings.isMobile && !Config.compactRoomList
text: i18n("Configure room")
display: QQC2.Button.IconOnly
icon.name: "configure"
onClicked: createRoomListContextMenu()
}
}
function createRoomListContextMenu() {
const component = Qt.createComponent(Qt.resolvedUrl("./ContextMenu.qml"))
const menu = component.createObject(root, {

View File

@@ -92,6 +92,7 @@ ColumnLayout {
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
}
background: Item {}
Component.onCompleted: EmojiFixer.addTextDocument(topicText.textDocument)
}
}

View File

@@ -124,6 +124,7 @@
<file alias="TimelineView.qml">qml/Component/TimelineView.qml</file>
<file alias="InvitationView.qml">qml/Component/InvitationView.qml</file>
<file alias="AvatarTabButton.qml">qml/Component/AvatarTabButton.qml</file>
<file alias="NeoChatTabButton.qml">qml/Component/NeoChatTabButton.qml</file>
<file alias="SpaceDrawer.qml">qml/Page/RoomList/SpaceDrawer.qml</file>
</qresource>
</RCC>