Merge remote-tracking branch 'origin/master' into reactions

This commit is contained in:
Joseph Donofry 2020-05-16 15:10:49 -04:00
commit a5778bdf40
No known key found for this signature in database
GPG Key ID: E8A1D78EF044B0CB
15 changed files with 86 additions and 67 deletions

View File

@ -137,11 +137,11 @@ before_install:
# Use TRAVIS_TAG if defined, or the short commit SHA otherwise # Use TRAVIS_TAG if defined, or the short commit SHA otherwise
- export VERSION=${TRAVIS_TAG:-$(git rev-parse --short HEAD)} - export VERSION=${TRAVIS_TAG:-$(git rev-parse --short HEAD)}
install: install:
- ./.ci/install.sh - travis_wait ./.ci/install.sh
- export PATH=/usr/local/bin:${PATH} - export PATH=/usr/local/bin:${PATH}
script: script:
- ./.ci/script.sh - travis_wait ./.ci/script.sh
- sed -i -e "s/VERSION_NAME_VALUE/${VERSION}/g" ./.ci/bintray-release.json || true - sed -i -e "s/VERSION_NAME_VALUE/${VERSION}/g" ./.ci/bintray-release.json || true
- cp ./.ci/bintray-release.json . - cp ./.ci/bintray-release.json .
deploy: deploy:

View File

@ -35,8 +35,6 @@ option(USE_BUNDLED_OPENSSL "Use the bundled version of OpenSSL."
option(USE_BUNDLED_MTXCLIENT "Use the bundled version of the Matrix Client library." ${HUNTER_ENABLED}) option(USE_BUNDLED_MTXCLIENT "Use the bundled version of the Matrix Client library." ${HUNTER_ENABLED})
option(USE_BUNDLED_SODIUM "Use the bundled version of libsodium." option(USE_BUNDLED_SODIUM "Use the bundled version of libsodium."
${HUNTER_ENABLED}) ${HUNTER_ENABLED})
option(USE_BUNDLED_ZLIB "Use the bundled version of zlib."
${HUNTER_ENABLED})
option(USE_BUNDLED_LMDB "Use the bundled version of lmdb." option(USE_BUNDLED_LMDB "Use the bundled version of lmdb."
${HUNTER_ENABLED}) ${HUNTER_ENABLED})
option(USE_BUNDLED_LMDBXX "Use the bundled version of lmdb++." option(USE_BUNDLED_LMDBXX "Use the bundled version of lmdb++."
@ -323,10 +321,7 @@ find_package(Boost 1.70 REQUIRED
COMPONENTS iostreams COMPONENTS iostreams
system system
thread) thread)
if(USE_BUNDLED_ZLIB)
hunter_add_package(ZLIB)
endif()
find_package(ZLIB REQUIRED)
if(USE_BUNDLED_OPENSSL) if(USE_BUNDLED_OPENSSL)
hunter_add_package(OpenSSL) hunter_add_package(OpenSSL)
endif() endif()
@ -338,7 +333,7 @@ if(USE_BUNDLED_MTXCLIENT)
FetchContent_Declare( FetchContent_Declare(
MatrixClient MatrixClient
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
GIT_TAG 71bd56b66cf634341ffef804f07d33f01fd57c25 GIT_TAG 1018c0822b80cdfc5d6b589fe94d1fd759113ef6
) )
FetchContent_MakeAvailable(MatrixClient) FetchContent_MakeAvailable(MatrixClient)
else() else()

View File

@ -97,13 +97,6 @@ guix install nheko
#### macOS (10.14 and above) #### macOS (10.14 and above)
with [macports](https://www.macports.org/):
```sh
sudo port install nheko
```
with [homebrew](https://brew.sh/): with [homebrew](https://brew.sh/):
```sh ```sh
@ -120,7 +113,7 @@ brew cask install nheko
- [LMDB](https://symas.com/lightning-memory-mapped-database/) - [LMDB](https://symas.com/lightning-memory-mapped-database/)
- [cmark](https://github.com/commonmark/cmark) 0.29 or greater. - [cmark](https://github.com/commonmark/cmark) 0.29 or greater.
- Boost 1.70 or greater. - Boost 1.70 or greater.
- [libolm](https://git.matrix.org/git/olm) - [libolm](https://gitlab.matrix.org/matrix-org/olm)
- [libsodium](https://github.com/jedisct1/libsodium) - [libsodium](https://github.com/jedisct1/libsodium)
- [spdlog](https://github.com/gabime/spdlog) - [spdlog](https://github.com/gabime/spdlog)
- A compiler that supports C++ 17: - A compiler that supports C++ 17:
@ -210,7 +203,7 @@ guix environment nheko
```bash ```bash
brew update brew update
brew install qt5 lmdb cmake llvm libsodium spdlog boost cmark brew install qt5 lmdb cmake llvm libsodium spdlog boost cmark libolm
``` ```
##### Windows ##### Windows

View File

@ -146,9 +146,9 @@
"name": "mtxclient", "name": "mtxclient",
"sources": [ "sources": [
{ {
"sha256": "7055f1459a43a12f27f949564624f13cc593ac894e445e6de0e6563ad38ebc3e", "sha256": "537f4e6b280f351ad950cd6598c2407505a55f0d6c856b4ff97a9c59fc6fdb7a",
"type": "archive", "type": "archive",
"url": "https://github.com/Nheko-Reborn/mtxclient/archive/71bd56b66cf634341ffef804f07d33f01fd57c25.tar.gz" "url": "https://github.com/Nheko-Reborn/mtxclient/archive/1018c0822b80cdfc5d6b589fe94d1fd759113ef6.tar.gz"
} }
] ]
}, },

1
resources/langs/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.qm

View File

@ -245,7 +245,7 @@
<message> <message>
<location line="+2"/> <location line="+2"/>
<source>This message is not encrypted!</source> <source>This message is not encrypted!</source>
<translation type="unfinished"></translation> <translation>Diese Nachricht ist nicht verschlüsselt!</translation>
</message> </message>
</context> </context>
<context> <context>
@ -673,7 +673,7 @@ Beispiel: https://mein.server:8787</translation>
<message> <message>
<location line="+2"/> <location line="+2"/>
<source>%1 made this room require and invitation to join.</source> <source>%1 made this room require and invitation to join.</source>
<translation type="unfinished"></translation> <translation>%1 hat eingestellt, dass dieser Raum eine Einladung benötigt um beizutreten.</translation>
</message> </message>
<message> <message>
<location line="+23"/> <location line="+23"/>
@ -1037,7 +1037,7 @@ Beispiel: https://mein.server:8787</translation>
<message> <message>
<location line="+25"/> <location line="+25"/>
<source>INTERFACE</source> <source>INTERFACE</source>
<translation type="unfinished"></translation> <translation>OBERFLÄCHE</translation>
</message> </message>
<message> <message>
<location line="+108"/> <location line="+108"/>

Binary file not shown.

View File

@ -14,6 +14,7 @@ MouseArea {
height: row.height height: row.height
propagateComposedEvents: true propagateComposedEvents: true
preventStealing: true preventStealing: true
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: { onClicked: {
@ -24,7 +25,10 @@ MouseArea {
if (mouse.source === Qt.MouseEventNotSynthesized) if (mouse.source === Qt.MouseEventNotSynthesized)
messageContextMenu.show(model.id, model.type, model.isEncrypted, row) messageContextMenu.show(model.id, model.type, model.isEncrypted, row)
} }
Rectangle {
color: (timelineSettings.message_hover_highlight && parent.containsMouse) ? colors.base : "transparent"
anchors.fill: row
}
RowLayout { RowLayout {
id: row id: row

View File

@ -34,6 +34,7 @@ Page {
id: timelineSettings id: timelineSettings
category: "user/timeline" category: "user/timeline"
property bool buttons: true property bool buttons: true
property bool message_hover_highlight: false
} }
EmojiPicker { EmojiPicker {
@ -113,6 +114,7 @@ Page {
} }
BusyIndicator { BusyIndicator {
visible: running
anchors.centerIn: parent anchors.centerIn: parent
running: timelineManager.isInitialSync running: timelineManager.isInitialSync
height: 200 height: 200

View File

@ -1017,7 +1017,7 @@ ChatPage::trySync()
// TODO: fine grained error handling // TODO: fine grained error handling
try { try {
cache::saveState(res); cache::saveState(res);
olm::handle_to_device_messages(res.to_device); olm::handle_to_device_messages(res.to_device.events);
auto updates = cache::roomUpdates(res); auto updates = cache::roomUpdates(res);
@ -1240,7 +1240,7 @@ ChatPage::initialSyncHandler(const mtx::responses::Sync &res, mtx::http::Request
try { try {
cache::saveState(res); cache::saveState(res);
olm::handle_to_device_messages(res.to_device); olm::handle_to_device_messages(res.to_device.events);
emit initializeViews(std::move(res.rooms)); emit initializeViews(std::move(res.rooms));
emit initializeRoomList(cache::roomInfo()); emit initializeRoomList(cache::roomInfo());

View File

@ -23,52 +23,55 @@ client()
} }
void void
handle_to_device_messages(const std::vector<nlohmann::json> &msgs) handle_to_device_messages(const std::vector<mtx::events::collections::DeviceEvents> &msgs)
{ {
if (msgs.empty()) if (msgs.empty())
return; return;
nhlog::crypto()->info("received {} to_device messages", msgs.size()); nhlog::crypto()->info("received {} to_device messages", msgs.size());
nlohmann::json j_msg;
for (const auto &msg : msgs) { for (const auto &msg : msgs) {
if (msg.count("type") == 0) { j_msg = std::visit([](auto &e) { return json(e); }, std::move(msg));
if (j_msg.count("type") == 0) {
nhlog::crypto()->warn("received message with no type field: {}", nhlog::crypto()->warn("received message with no type field: {}",
msg.dump(2)); j_msg.dump(2));
continue; continue;
} }
std::string msg_type = msg.at("type"); std::string msg_type = j_msg.at("type");
if (msg_type == to_string(mtx::events::EventType::RoomEncrypted)) { if (msg_type == to_string(mtx::events::EventType::RoomEncrypted)) {
try { try {
OlmMessage olm_msg = msg; OlmMessage olm_msg = j_msg;
handle_olm_message(std::move(olm_msg)); handle_olm_message(std::move(olm_msg));
} catch (const nlohmann::json::exception &e) { } catch (const nlohmann::json::exception &e) {
nhlog::crypto()->warn( nhlog::crypto()->warn(
"parsing error for olm message: {} {}", e.what(), msg.dump(2)); "parsing error for olm message: {} {}", e.what(), j_msg.dump(2));
} catch (const std::invalid_argument &e) { } catch (const std::invalid_argument &e) {
nhlog::crypto()->warn( nhlog::crypto()->warn("validation error for olm message: {} {}",
"validation error for olm message: {} {}", e.what(), msg.dump(2)); e.what(),
j_msg.dump(2));
} }
} else if (msg_type == to_string(mtx::events::EventType::RoomKeyRequest)) { } else if (msg_type == to_string(mtx::events::EventType::RoomKeyRequest)) {
nhlog::crypto()->warn("handling key request event: {}", msg.dump(2)); nhlog::crypto()->warn("handling key request event: {}", j_msg.dump(2));
try { try {
mtx::events::msg::KeyRequest req = msg; mtx::events::DeviceEvent<mtx::events::msg::KeyRequest> req = j_msg;
if (req.action == mtx::events::msg::RequestAction::Request) if (req.content.action == mtx::events::msg::RequestAction::Request)
handle_key_request_message(std::move(req)); handle_key_request_message(req);
else else
nhlog::crypto()->warn( nhlog::crypto()->warn(
"ignore key request (unhandled action): {}", "ignore key request (unhandled action): {}",
req.request_id); req.content.request_id);
} catch (const nlohmann::json::exception &e) { } catch (const nlohmann::json::exception &e) {
nhlog::crypto()->warn( nhlog::crypto()->warn(
"parsing error for key_request message: {} {}", "parsing error for key_request message: {} {}",
e.what(), e.what(),
msg.dump(2)); j_msg.dump(2));
} }
} else { } else {
nhlog::crypto()->warn("unhandled event: {}", msg.dump(2)); nhlog::crypto()->warn("unhandled event: {}", j_msg.dump(2));
} }
} }
} }
@ -341,51 +344,53 @@ send_key_request_for(const std::string &room_id,
} }
void void
handle_key_request_message(const mtx::events::msg::KeyRequest &req) handle_key_request_message(const mtx::events::DeviceEvent<mtx::events::msg::KeyRequest> &req)
{ {
if (req.algorithm != MEGOLM_ALGO) { if (req.content.algorithm != MEGOLM_ALGO) {
nhlog::crypto()->debug("ignoring key request {} with invalid algorithm: {}", nhlog::crypto()->debug("ignoring key request {} with invalid algorithm: {}",
req.request_id, req.content.request_id,
req.algorithm); req.content.algorithm);
return; return;
} }
// Check if we were the sender of the session being requested. // Check if we were the sender of the session being requested.
if (req.sender_key != olm::client()->identity_keys().curve25519) { if (req.content.sender_key != olm::client()->identity_keys().curve25519) {
nhlog::crypto()->debug("ignoring key request {} because we were not the sender: " nhlog::crypto()->debug("ignoring key request {} because we were not the sender: "
"\nrequested({}) ours({})", "\nrequested({}) ours({})",
req.request_id, req.content.request_id,
req.sender_key, req.content.sender_key,
olm::client()->identity_keys().curve25519); olm::client()->identity_keys().curve25519);
return; return;
} }
// Check if we have the keys for the requested session. // Check if we have the keys for the requested session.
if (!cache::outboundMegolmSessionExists(req.room_id)) { if (!cache::outboundMegolmSessionExists(req.content.room_id)) {
nhlog::crypto()->warn("requested session not found in room: {}", req.room_id); nhlog::crypto()->warn("requested session not found in room: {}",
req.content.room_id);
return; return;
} }
// Check that the requested session_id and the one we have saved match. // Check that the requested session_id and the one we have saved match.
const auto session = cache::getOutboundMegolmSession(req.room_id); const auto session = cache::getOutboundMegolmSession(req.content.room_id);
if (req.session_id != session.data.session_id) { if (req.content.session_id != session.data.session_id) {
nhlog::crypto()->warn("session id of retrieved session doesn't match the request: " nhlog::crypto()->warn("session id of retrieved session doesn't match the request: "
"requested({}), ours({})", "requested({}), ours({})",
req.session_id, req.content.session_id,
session.data.session_id); session.data.session_id);
return; return;
} }
if (!cache::isRoomMember(req.sender, req.room_id)) { if (!cache::isRoomMember(req.sender, req.content.room_id)) {
nhlog::crypto()->warn( nhlog::crypto()->warn(
"user {} that requested the session key is not member of the room {}", "user {} that requested the session key is not member of the room {}",
req.sender, req.sender,
req.room_id); req.content.room_id);
return; return;
} }
if (!utils::respondsToKeyRequests(req.room_id)) { if (!utils::respondsToKeyRequests(req.content.room_id)) {
nhlog::crypto()->debug("ignoring all key requests for room {}", req.room_id); nhlog::crypto()->debug("ignoring all key requests for room {}",
req.content.room_id);
return; return;
} }
@ -393,11 +398,11 @@ handle_key_request_message(const mtx::events::msg::KeyRequest &req)
// Prepare the m.room_key event. // Prepare the m.room_key event.
// //
auto payload = json{{"algorithm", "m.megolm.v1.aes-sha2"}, auto payload = json{{"algorithm", "m.megolm.v1.aes-sha2"},
{"room_id", req.room_id}, {"room_id", req.content.room_id},
{"session_id", req.session_id}, {"session_id", req.content.session_id},
{"session_key", session.data.session_key}}; {"session_key", session.data.session_key}};
send_megolm_key_to_device(req.sender, req.requesting_device_id, payload); send_megolm_key_to_device(req.sender, req.content.requesting_device_id, payload);
} }
void void

View File

@ -40,7 +40,7 @@ mtx::crypto::OlmClient *
client(); client();
void void
handle_to_device_messages(const std::vector<nlohmann::json> &msgs); handle_to_device_messages(const std::vector<mtx::events::collections::DeviceEvents> &msgs);
nlohmann::json nlohmann::json
try_olm_decryption(const std::string &sender_key, try_olm_decryption(const std::string &sender_key,
@ -77,7 +77,7 @@ send_key_request_for(const std::string &room_id,
const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &); const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &);
void void
handle_key_request_message(const mtx::events::msg::KeyRequest &); handle_key_request_message(const mtx::events::DeviceEvent<mtx::events::msg::KeyRequest> &);
void void
send_megolm_key_to_device(const std::string &user_id, send_megolm_key_to_device(const std::string &user_id,

View File

@ -51,11 +51,13 @@ void
UserSettings::load() UserSettings::load()
{ {
QSettings settings; QSettings settings;
isTrayEnabled_ = settings.value("user/window/tray", false).toBool(); isTrayEnabled_ = settings.value("user/window/tray", false).toBool();
hasDesktopNotifications_ = settings.value("user/desktop_notifications", true).toBool(); hasDesktopNotifications_ = settings.value("user/desktop_notifications", true).toBool();
isStartInTrayEnabled_ = settings.value("user/window/start_in_tray", false).toBool(); isStartInTrayEnabled_ = settings.value("user/window/start_in_tray", false).toBool();
isGroupViewEnabled_ = settings.value("user/group_view", true).toBool(); isGroupViewEnabled_ = settings.value("user/group_view", true).toBool();
isButtonsInTimelineEnabled_ = settings.value("user/timeline/buttons", true).toBool(); isButtonsInTimelineEnabled_ = settings.value("user/timeline/buttons", true).toBool();
isMessageHoverHighlightEnabled_ =
settings.value("user/timeline/message_hover_highlight", false).toBool();
isMarkdownEnabled_ = settings.value("user/markdown_enabled", true).toBool(); isMarkdownEnabled_ = settings.value("user/markdown_enabled", true).toBool();
isTypingNotificationsEnabled_ = settings.value("user/typing_notifications", true).toBool(); isTypingNotificationsEnabled_ = settings.value("user/typing_notifications", true).toBool();
sortByImportance_ = settings.value("user/sort_by_unread", true).toBool(); sortByImportance_ = settings.value("user/sort_by_unread", true).toBool();
@ -165,6 +167,7 @@ UserSettings::save()
settings.beginGroup("timeline"); settings.beginGroup("timeline");
settings.setValue("buttons", isButtonsInTimelineEnabled_); settings.setValue("buttons", isButtonsInTimelineEnabled_);
settings.setValue("message_hover_highlight", isMessageHoverHighlightEnabled_);
settings.endGroup(); settings.endGroup();
settings.setValue("avatar_circles", avatarCircles_); settings.setValue("avatar_circles", avatarCircles_);
@ -235,6 +238,7 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
groupViewToggle_ = new Toggle{this}; groupViewToggle_ = new Toggle{this};
timelineButtonsToggle_ = new Toggle{this}; timelineButtonsToggle_ = new Toggle{this};
typingNotifications_ = new Toggle{this}; typingNotifications_ = new Toggle{this};
messageHoverHighlight_ = new Toggle{this};
sortByImportance_ = new Toggle{this}; sortByImportance_ = new Toggle{this};
readReceipts_ = new Toggle{this}; readReceipts_ = new Toggle{this};
markdownEnabled_ = new Toggle{this}; markdownEnabled_ = new Toggle{this};
@ -345,6 +349,7 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
boxWrap(tr("Read receipts"), readReceipts_); boxWrap(tr("Read receipts"), readReceipts_);
boxWrap(tr("Send messages as Markdown"), markdownEnabled_); boxWrap(tr("Send messages as Markdown"), markdownEnabled_);
boxWrap(tr("Desktop notifications"), desktopNotifications_); boxWrap(tr("Desktop notifications"), desktopNotifications_);
boxWrap(tr("Highlight message on hover"), messageHoverHighlight_);
formLayout_->addRow(uiLabel_); formLayout_->addRow(uiLabel_);
formLayout_->addRow(new HorizontalLine{this}); formLayout_->addRow(new HorizontalLine{this});
@ -463,6 +468,10 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
settings_->setDesktopNotifications(!isDisabled); settings_->setDesktopNotifications(!isDisabled);
}); });
connect(messageHoverHighlight_, &Toggle::toggled, this, [this](bool isDisabled) {
settings_->setMessageHoverHighlight(!isDisabled);
});
connect( connect(
sessionKeysImportBtn, &QPushButton::clicked, this, &UserSettingsPage::importSessionKeys); sessionKeysImportBtn, &QPushButton::clicked, this, &UserSettingsPage::importSessionKeys);
@ -495,6 +504,7 @@ UserSettingsPage::showEvent(QShowEvent *)
readReceipts_->setState(!settings_->isReadReceiptsEnabled()); readReceipts_->setState(!settings_->isReadReceiptsEnabled());
markdownEnabled_->setState(!settings_->isMarkdownEnabled()); markdownEnabled_->setState(!settings_->isMarkdownEnabled());
desktopNotifications_->setState(!settings_->hasDesktopNotifications()); desktopNotifications_->setState(!settings_->hasDesktopNotifications());
messageHoverHighlight_->setState(!settings_->isMessageHoverHighlightEnabled());
deviceIdValue_->setText(QString::fromStdString(http::client()->device_id())); deviceIdValue_->setText(QString::fromStdString(http::client()->device_id()));
deviceFingerprintValue_->setText( deviceFingerprintValue_->setText(

View File

@ -44,6 +44,11 @@ public:
void load(); void load();
void applyTheme(); void applyTheme();
void setTheme(QString theme); void setTheme(QString theme);
void setMessageHoverHighlight(bool state)
{
isMessageHoverHighlightEnabled_ = state;
save();
}
void setTray(bool state) void setTray(bool state)
{ {
isTrayEnabled_ = state; isTrayEnabled_ = state;
@ -118,6 +123,7 @@ public:
} }
QString theme() const { return !theme_.isEmpty() ? theme_ : defaultTheme_; } QString theme() const { return !theme_.isEmpty() ? theme_ : defaultTheme_; }
bool isMessageHoverHighlightEnabled() const { return isMessageHoverHighlightEnabled_; }
bool isTrayEnabled() const { return isTrayEnabled_; } bool isTrayEnabled() const { return isTrayEnabled_; }
bool isStartInTrayEnabled() const { return isStartInTrayEnabled_; } bool isStartInTrayEnabled() const { return isStartInTrayEnabled_; }
bool isGroupViewEnabled() const { return isGroupViewEnabled_; } bool isGroupViewEnabled() const { return isGroupViewEnabled_; }
@ -144,6 +150,7 @@ private:
? "light" ? "light"
: "system"; : "system";
QString theme_; QString theme_;
bool isMessageHoverHighlightEnabled_;
bool isTrayEnabled_; bool isTrayEnabled_;
bool isStartInTrayEnabled_; bool isStartInTrayEnabled_;
bool isGroupViewEnabled_; bool isGroupViewEnabled_;
@ -203,6 +210,7 @@ private:
Toggle *groupViewToggle_; Toggle *groupViewToggle_;
Toggle *timelineButtonsToggle_; Toggle *timelineButtonsToggle_;
Toggle *typingNotifications_; Toggle *typingNotifications_;
Toggle *messageHoverHighlight_;
Toggle *sortByImportance_; Toggle *sortByImportance_;
Toggle *readReceipts_; Toggle *readReceipts_;
Toggle *markdownEnabled_; Toggle *markdownEnabled_;

View File

@ -116,6 +116,7 @@ main(int argc, char *argv[])
QCoreApplication::setApplicationName("nheko"); QCoreApplication::setApplicationName("nheko");
QCoreApplication::setApplicationVersion(nheko::version); QCoreApplication::setApplicationVersion(nheko::version);
QCoreApplication::setOrganizationName("nheko"); QCoreApplication::setOrganizationName("nheko");
QCoreApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings);
QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
SingleApplication app(argc, SingleApplication app(argc,