1eb2869fa8
This may increase the db size by a factor of 1000 in the worst case and it will need some fixes, when we decide to not show some events in the timeline, but it should work for now.
653 lines
23 KiB
C++
653 lines
23 KiB
C++
#include "Utils.h"
|
|
|
|
#include <QApplication>
|
|
#include <QComboBox>
|
|
#include <QDesktopWidget>
|
|
#include <QGuiApplication>
|
|
#include <QProcessEnvironment>
|
|
#include <QScreen>
|
|
#include <QSettings>
|
|
#include <QTextDocument>
|
|
#include <QXmlStreamReader>
|
|
|
|
#include <cmath>
|
|
#include <variant>
|
|
|
|
#include <cmark.h>
|
|
|
|
#include "Cache.h"
|
|
#include "Config.h"
|
|
#include "MatrixClient.h"
|
|
|
|
using TimelineEvent = mtx::events::collections::TimelineEvents;
|
|
|
|
QHash<QString, QString> authorColors_;
|
|
|
|
template<class T, class Event>
|
|
static DescInfo
|
|
createDescriptionInfo(const Event &event, const QString &localUser, const QString &room_id)
|
|
{
|
|
const auto msg = std::get<T>(event);
|
|
const auto sender = QString::fromStdString(msg.sender);
|
|
|
|
const auto username = cache::displayName(room_id, sender);
|
|
const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts);
|
|
|
|
return DescInfo{
|
|
QString::fromStdString(msg.event_id),
|
|
sender,
|
|
utils::messageDescription<T>(
|
|
username, QString::fromStdString(msg.content.body).trimmed(), sender == localUser),
|
|
utils::descriptiveTime(ts),
|
|
ts};
|
|
}
|
|
|
|
QString
|
|
utils::localUser()
|
|
{
|
|
return QString::fromStdString(http::client()->user_id().to_string());
|
|
}
|
|
|
|
QString
|
|
utils::replaceEmoji(const QString &body)
|
|
{
|
|
QString fmtBody = "";
|
|
|
|
QVector<uint> utf32_string = body.toUcs4();
|
|
|
|
QSettings settings;
|
|
QString userFontFamily = settings.value("user/emoji_font_family", "emoji").toString();
|
|
|
|
bool insideFontBlock = false;
|
|
for (auto &code : utf32_string) {
|
|
// TODO: Be more precise here.
|
|
if ((code >= 0x2600 && code <= 0x27bf) || (code >= 0x1f300 && code <= 0x1f3ff) ||
|
|
(code >= 0x1f000 && code <= 0x1faff)) {
|
|
if (!insideFontBlock) {
|
|
fmtBody += QString("<font face=\"" + userFontFamily + "\">");
|
|
insideFontBlock = true;
|
|
}
|
|
|
|
} else {
|
|
if (insideFontBlock) {
|
|
fmtBody += "</font>";
|
|
insideFontBlock = false;
|
|
}
|
|
}
|
|
fmtBody += QString::fromUcs4(&code, 1);
|
|
}
|
|
if (insideFontBlock) {
|
|
fmtBody += "</font>";
|
|
}
|
|
|
|
return fmtBody;
|
|
}
|
|
|
|
void
|
|
utils::setScaleFactor(float factor)
|
|
{
|
|
if (factor < 1 || factor > 3)
|
|
return;
|
|
|
|
QSettings settings;
|
|
settings.setValue("settings/scale_factor", factor);
|
|
}
|
|
|
|
float
|
|
utils::scaleFactor()
|
|
{
|
|
QSettings settings;
|
|
return settings.value("settings/scale_factor", -1).toFloat();
|
|
}
|
|
|
|
bool
|
|
utils::respondsToKeyRequests(const std::string &roomId)
|
|
{
|
|
return respondsToKeyRequests(QString::fromStdString(roomId));
|
|
}
|
|
|
|
bool
|
|
utils::respondsToKeyRequests(const QString &roomId)
|
|
{
|
|
if (roomId.isEmpty())
|
|
return false;
|
|
|
|
QSettings settings;
|
|
return settings.value("rooms/respond_to_key_requests/" + roomId, false).toBool();
|
|
}
|
|
|
|
void
|
|
utils::setKeyRequestsPreference(QString roomId, bool value)
|
|
{
|
|
if (roomId.isEmpty())
|
|
return;
|
|
|
|
QSettings settings;
|
|
settings.setValue("rooms/respond_to_key_requests/" + roomId, value);
|
|
}
|
|
|
|
QString
|
|
utils::descriptiveTime(const QDateTime &then)
|
|
{
|
|
const auto now = QDateTime::currentDateTime();
|
|
const auto days = then.daysTo(now);
|
|
|
|
if (days == 0)
|
|
return then.time().toString(Qt::DefaultLocaleShortDate);
|
|
else if (days < 2)
|
|
return QString(QCoreApplication::translate("descriptiveTime", "Yesterday"));
|
|
else if (days < 7)
|
|
return then.toString("dddd");
|
|
|
|
return then.date().toString(Qt::DefaultLocaleShortDate);
|
|
}
|
|
|
|
DescInfo
|
|
utils::getMessageDescription(const TimelineEvent &event,
|
|
const QString &localUser,
|
|
const QString &room_id)
|
|
{
|
|
using Audio = mtx::events::RoomEvent<mtx::events::msg::Audio>;
|
|
using Emote = mtx::events::RoomEvent<mtx::events::msg::Emote>;
|
|
using File = mtx::events::RoomEvent<mtx::events::msg::File>;
|
|
using Image = mtx::events::RoomEvent<mtx::events::msg::Image>;
|
|
using Notice = mtx::events::RoomEvent<mtx::events::msg::Notice>;
|
|
using Text = mtx::events::RoomEvent<mtx::events::msg::Text>;
|
|
using Video = mtx::events::RoomEvent<mtx::events::msg::Video>;
|
|
using Encrypted = mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>;
|
|
|
|
if (std::holds_alternative<Audio>(event)) {
|
|
return createDescriptionInfo<Audio>(event, localUser, room_id);
|
|
} else if (std::holds_alternative<Emote>(event)) {
|
|
return createDescriptionInfo<Emote>(event, localUser, room_id);
|
|
} else if (std::holds_alternative<File>(event)) {
|
|
return createDescriptionInfo<File>(event, localUser, room_id);
|
|
} else if (std::holds_alternative<Image>(event)) {
|
|
return createDescriptionInfo<Image>(event, localUser, room_id);
|
|
} else if (std::holds_alternative<Notice>(event)) {
|
|
return createDescriptionInfo<Notice>(event, localUser, room_id);
|
|
} else if (std::holds_alternative<Text>(event)) {
|
|
return createDescriptionInfo<Text>(event, localUser, room_id);
|
|
} else if (std::holds_alternative<Video>(event)) {
|
|
return createDescriptionInfo<Video>(event, localUser, room_id);
|
|
} else if (std::holds_alternative<mtx::events::Sticker>(event)) {
|
|
return createDescriptionInfo<mtx::events::Sticker>(event, localUser, room_id);
|
|
} else if (auto msg = std::get_if<Encrypted>(&event); msg != nullptr) {
|
|
const auto sender = QString::fromStdString(msg->sender);
|
|
|
|
const auto username = cache::displayName(room_id, sender);
|
|
const auto ts = QDateTime::fromMSecsSinceEpoch(msg->origin_server_ts);
|
|
|
|
DescInfo info;
|
|
info.userid = sender;
|
|
info.body = QString(" %1").arg(messageDescription<Encrypted>());
|
|
info.timestamp = utils::descriptiveTime(ts);
|
|
info.event_id = QString::fromStdString(msg->event_id);
|
|
info.datetime = ts;
|
|
|
|
return info;
|
|
}
|
|
|
|
return DescInfo{};
|
|
}
|
|
|
|
QString
|
|
utils::firstChar(const QString &input)
|
|
{
|
|
if (input.isEmpty())
|
|
return input;
|
|
|
|
for (auto const &c : input.toUcs4()) {
|
|
if (QString::fromUcs4(&c, 1) != QString("#"))
|
|
return QString::fromUcs4(&c, 1).toUpper();
|
|
}
|
|
|
|
return QString::fromUcs4(&input.toUcs4().at(0), 1).toUpper();
|
|
}
|
|
|
|
QString
|
|
utils::humanReadableFileSize(uint64_t bytes)
|
|
{
|
|
constexpr static const char *units[] = {"B", "KiB", "MiB", "GiB", "TiB"};
|
|
constexpr static const int length = sizeof(units) / sizeof(units[0]);
|
|
|
|
int u = 0;
|
|
double size = static_cast<double>(bytes);
|
|
while (size >= 1024.0 && u < length) {
|
|
++u;
|
|
size /= 1024.0;
|
|
}
|
|
|
|
return QString::number(size, 'g', 4) + ' ' + units[u];
|
|
}
|
|
|
|
int
|
|
utils::levenshtein_distance(const std::string &s1, const std::string &s2)
|
|
{
|
|
const int nlen = s1.size();
|
|
const int hlen = s2.size();
|
|
|
|
if (hlen == 0)
|
|
return -1;
|
|
if (nlen == 1)
|
|
return s2.find(s1);
|
|
|
|
std::vector<int> row1(hlen + 1, 0);
|
|
|
|
for (int i = 0; i < nlen; ++i) {
|
|
std::vector<int> row2(1, i + 1);
|
|
|
|
for (int j = 0; j < hlen; ++j) {
|
|
const int cost = s1[i] != s2[j];
|
|
row2.push_back(
|
|
std::min(row1[j + 1] + 1, std::min(row2[j] + 1, row1[j] + cost)));
|
|
}
|
|
|
|
row1.swap(row2);
|
|
}
|
|
|
|
return *std::min_element(row1.begin(), row1.end());
|
|
}
|
|
|
|
QString
|
|
utils::event_body(const mtx::events::collections::TimelineEvents &e)
|
|
{
|
|
using namespace mtx::events;
|
|
if (auto ev = std::get_if<RoomEvent<msg::Audio>>(&e); ev != nullptr)
|
|
return QString::fromStdString(ev->content.body);
|
|
if (auto ev = std::get_if<RoomEvent<msg::Emote>>(&e); ev != nullptr)
|
|
return QString::fromStdString(ev->content.body);
|
|
if (auto ev = std::get_if<RoomEvent<msg::File>>(&e); ev != nullptr)
|
|
return QString::fromStdString(ev->content.body);
|
|
if (auto ev = std::get_if<RoomEvent<msg::Image>>(&e); ev != nullptr)
|
|
return QString::fromStdString(ev->content.body);
|
|
if (auto ev = std::get_if<RoomEvent<msg::Notice>>(&e); ev != nullptr)
|
|
return QString::fromStdString(ev->content.body);
|
|
if (auto ev = std::get_if<RoomEvent<msg::Text>>(&e); ev != nullptr)
|
|
return QString::fromStdString(ev->content.body);
|
|
if (auto ev = std::get_if<RoomEvent<msg::Video>>(&e); ev != nullptr)
|
|
return QString::fromStdString(ev->content.body);
|
|
|
|
return "";
|
|
}
|
|
|
|
QPixmap
|
|
utils::scaleImageToPixmap(const QImage &img, int size)
|
|
{
|
|
if (img.isNull())
|
|
return QPixmap();
|
|
|
|
// Deprecated in 5.13: const double sz =
|
|
// std::ceil(QApplication::desktop()->screen()->devicePixelRatioF() * (double)size);
|
|
const double sz =
|
|
std::ceil(QGuiApplication::primaryScreen()->devicePixelRatio() * (double)size);
|
|
return QPixmap::fromImage(
|
|
img.scaled(sz, sz, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
|
|
}
|
|
|
|
QPixmap
|
|
utils::scaleDown(uint64_t maxWidth, uint64_t maxHeight, const QPixmap &source)
|
|
{
|
|
if (source.isNull())
|
|
return QPixmap();
|
|
|
|
const double widthRatio = (double)maxWidth / (double)source.width();
|
|
const double heightRatio = (double)maxHeight / (double)source.height();
|
|
const double minAspectRatio = std::min(widthRatio, heightRatio);
|
|
|
|
// Size of the output image.
|
|
int w, h = 0;
|
|
|
|
if (minAspectRatio > 1) {
|
|
w = source.width();
|
|
h = source.height();
|
|
} else {
|
|
w = source.width() * minAspectRatio;
|
|
h = source.height() * minAspectRatio;
|
|
}
|
|
|
|
return source.scaled(w, h, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
|
}
|
|
|
|
QString
|
|
utils::mxcToHttp(const QUrl &url, const QString &server, int port)
|
|
{
|
|
auto mxcParts = mtx::client::utils::parse_mxc_url(url.toString().toStdString());
|
|
|
|
return QString("https://%1:%2/_matrix/media/r0/download/%3/%4")
|
|
.arg(server)
|
|
.arg(port)
|
|
.arg(QString::fromStdString(mxcParts.server))
|
|
.arg(QString::fromStdString(mxcParts.media_id));
|
|
}
|
|
|
|
QString
|
|
utils::humanReadableFingerprint(const std::string &ed25519)
|
|
{
|
|
return humanReadableFingerprint(QString::fromStdString(ed25519));
|
|
}
|
|
QString
|
|
utils::humanReadableFingerprint(const QString &ed25519)
|
|
{
|
|
QStringList fingerprintList;
|
|
for (int i = 0; i < ed25519.length(); i = i + 4) {
|
|
fingerprintList << ed25519.mid(i, 4);
|
|
}
|
|
return fingerprintList.join(" ");
|
|
}
|
|
|
|
QString
|
|
utils::linkifyMessage(const QString &body)
|
|
{
|
|
// Convert to valid XML.
|
|
auto doc = body;
|
|
doc.replace(conf::strings::url_regex, conf::strings::url_html);
|
|
|
|
return doc;
|
|
}
|
|
|
|
QString
|
|
utils::escapeBlacklistedHtml(const QString &rawStr)
|
|
{
|
|
static const std::array allowedTags = {
|
|
"font", "/font", "del", "/del", "h1", "/h1", "h2", "/h2",
|
|
"h3", "/h3", "h4", "/h4", "h5", "/h5", "h6", "/h6",
|
|
"blockquote", "/blockquote", "p", "/p", "a", "/a", "ul", "/ul",
|
|
"ol", "/ol", "sup", "/sup", "sub", "/sub", "li", "/li",
|
|
"b", "/b", "i", "/i", "u", "/u", "strong", "/strong",
|
|
"em", "/em", "strike", "/strike", "code", "/code", "hr", "/hr",
|
|
"br", "br/", "div", "/div", "table", "/table", "thead", "/thead",
|
|
"tbody", "/tbody", "tr", "/tr", "th", "/th", "td", "/td",
|
|
"caption", "/caption", "pre", "/pre", "span", "/span", "img", "/img"};
|
|
QByteArray data = rawStr.toUtf8();
|
|
QByteArray buffer;
|
|
const size_t length = data.size();
|
|
buffer.reserve(length);
|
|
bool escapingTag = false;
|
|
for (size_t pos = 0; pos != length; ++pos) {
|
|
switch (data.at(pos)) {
|
|
case '<': {
|
|
bool oneTagMatched = false;
|
|
size_t endPos = std::min(static_cast<size_t>(data.indexOf('>', pos)),
|
|
static_cast<size_t>(data.indexOf(' ', pos)));
|
|
|
|
auto mid = data.mid(pos + 1, endPos - pos - 1);
|
|
for (const auto &tag : allowedTags) {
|
|
// TODO: Check src and href attribute
|
|
if (mid.toLower() == tag) {
|
|
oneTagMatched = true;
|
|
}
|
|
}
|
|
if (oneTagMatched)
|
|
buffer.append('<');
|
|
else {
|
|
escapingTag = true;
|
|
buffer.append("<");
|
|
}
|
|
break;
|
|
}
|
|
case '>':
|
|
if (escapingTag) {
|
|
buffer.append(">");
|
|
escapingTag = false;
|
|
} else
|
|
buffer.append('>');
|
|
break;
|
|
default:
|
|
buffer.append(data.at(pos));
|
|
break;
|
|
}
|
|
}
|
|
return QString::fromUtf8(buffer);
|
|
}
|
|
|
|
QString
|
|
utils::markdownToHtml(const QString &text)
|
|
{
|
|
const auto str = text.toUtf8();
|
|
const char *tmp_buf = cmark_markdown_to_html(str.constData(), str.size(), CMARK_OPT_UNSAFE);
|
|
|
|
// Copy the null terminated output buffer.
|
|
std::string html(tmp_buf);
|
|
|
|
// The buffer is no longer needed.
|
|
free((char *)tmp_buf);
|
|
|
|
auto result = linkifyMessage(escapeBlacklistedHtml(QString::fromStdString(html))).trimmed();
|
|
|
|
if (result.count("<p>") == 1 && result.startsWith("<p>") && result.endsWith("</p>")) {
|
|
result = result.mid(3, result.size() - 3 - 4);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
QString
|
|
utils::getFormattedQuoteBody(const RelatedInfo &related, const QString &html)
|
|
{
|
|
auto getFormattedBody = [related]() -> QString {
|
|
using MsgType = mtx::events::MessageType;
|
|
|
|
switch (related.type) {
|
|
case MsgType::File: {
|
|
return "sent a file.";
|
|
}
|
|
case MsgType::Image: {
|
|
return "sent an image.";
|
|
}
|
|
case MsgType::Audio: {
|
|
return "sent an audio file.";
|
|
}
|
|
case MsgType::Video: {
|
|
return "sent a video";
|
|
}
|
|
default: {
|
|
return related.quoted_formatted_body;
|
|
}
|
|
}
|
|
};
|
|
return QString("<mx-reply><blockquote><a "
|
|
"href=\"https://matrix.to/#/%1/%2\">In reply "
|
|
"to</a> <a href=\"https://matrix.to/#/%3\">%4</a><br"
|
|
"/>%5</blockquote></mx-reply>")
|
|
.arg(related.room,
|
|
QString::fromStdString(related.related_event),
|
|
related.quoted_user,
|
|
related.quoted_user,
|
|
getFormattedBody()) +
|
|
html;
|
|
}
|
|
|
|
QString
|
|
utils::getQuoteBody(const RelatedInfo &related)
|
|
{
|
|
using MsgType = mtx::events::MessageType;
|
|
|
|
switch (related.type) {
|
|
case MsgType::File: {
|
|
return "sent a file.";
|
|
}
|
|
case MsgType::Image: {
|
|
return "sent an image.";
|
|
}
|
|
case MsgType::Audio: {
|
|
return "sent an audio file.";
|
|
}
|
|
case MsgType::Video: {
|
|
return "sent a video";
|
|
}
|
|
default: {
|
|
return related.quoted_body;
|
|
}
|
|
}
|
|
}
|
|
|
|
QString
|
|
utils::linkColor()
|
|
{
|
|
QSettings settings;
|
|
// Default to system theme if QT_QPA_PLATFORMTHEME var is set.
|
|
QString defaultTheme =
|
|
QProcessEnvironment::systemEnvironment().value("QT_QPA_PLATFORMTHEME", "").isEmpty()
|
|
? "light"
|
|
: "system";
|
|
const auto theme = settings.value("user/theme", defaultTheme).toString();
|
|
|
|
if (theme == "light") {
|
|
return "#0077b5";
|
|
} else if (theme == "dark") {
|
|
return "#38A3D8";
|
|
} else {
|
|
return QPalette().color(QPalette::Link).name();
|
|
}
|
|
}
|
|
|
|
uint32_t
|
|
utils::hashQString(const QString &input)
|
|
{
|
|
uint32_t hash = 0;
|
|
|
|
for (int i = 0; i < input.length(); i++) {
|
|
hash = input.at(i).digitValue() + ((hash << 5) - hash);
|
|
}
|
|
|
|
return hash;
|
|
}
|
|
|
|
QString
|
|
utils::generateContrastingHexColor(const QString &input, const QString &background)
|
|
{
|
|
const QColor backgroundCol(background);
|
|
const qreal backgroundLum = luminance(background);
|
|
|
|
// Create a color for the input
|
|
auto hash = hashQString(input);
|
|
// create a hue value based on the hash of the input.
|
|
auto userHue = static_cast<int>(qAbs(hash % 360));
|
|
// start with moderate saturation and lightness values.
|
|
auto sat = 220;
|
|
auto lightness = 125;
|
|
|
|
// converting to a QColor makes the luminance calc easier.
|
|
QColor inputColor = QColor::fromHsl(userHue, sat, lightness);
|
|
|
|
// calculate the initial luminance and contrast of the
|
|
// generated color. It's possible that no additional
|
|
// work will be necessary.
|
|
auto lum = luminance(inputColor);
|
|
auto contrast = computeContrast(lum, backgroundLum);
|
|
|
|
// If the contrast doesn't meet our criteria,
|
|
// try again and again until they do by modifying first
|
|
// the lightness and then the saturation of the color.
|
|
while (contrast < 5) {
|
|
// if our lightness is at it's bounds, try changing
|
|
// saturation instead.
|
|
if (lightness == 242 || lightness == 13) {
|
|
qreal newSat = qBound(26.0, sat * 1.25, 242.0);
|
|
|
|
inputColor.setHsl(userHue, qFloor(newSat), lightness);
|
|
auto tmpLum = luminance(inputColor);
|
|
auto higherContrast = computeContrast(tmpLum, backgroundLum);
|
|
if (higherContrast > contrast) {
|
|
contrast = higherContrast;
|
|
sat = newSat;
|
|
} else {
|
|
newSat = qBound(26.0, sat / 1.25, 242.0);
|
|
inputColor.setHsl(userHue, qFloor(newSat), lightness);
|
|
tmpLum = luminance(inputColor);
|
|
auto lowerContrast = computeContrast(tmpLum, backgroundLum);
|
|
if (lowerContrast > contrast) {
|
|
contrast = lowerContrast;
|
|
sat = newSat;
|
|
}
|
|
}
|
|
} else {
|
|
qreal newLightness = qBound(13.0, lightness * 1.25, 242.0);
|
|
|
|
inputColor.setHsl(userHue, sat, qFloor(newLightness));
|
|
|
|
auto tmpLum = luminance(inputColor);
|
|
auto higherContrast = computeContrast(tmpLum, backgroundLum);
|
|
|
|
// Check to make sure we have actually improved contrast
|
|
if (higherContrast > contrast) {
|
|
contrast = higherContrast;
|
|
lightness = newLightness;
|
|
// otherwise, try going the other way instead.
|
|
} else {
|
|
newLightness = qBound(13.0, lightness / 1.25, 242.0);
|
|
inputColor.setHsl(userHue, sat, qFloor(newLightness));
|
|
tmpLum = luminance(inputColor);
|
|
auto lowerContrast = computeContrast(tmpLum, backgroundLum);
|
|
if (lowerContrast > contrast) {
|
|
contrast = lowerContrast;
|
|
lightness = newLightness;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// get the hex value of the generated color.
|
|
auto colorHex = inputColor.name();
|
|
|
|
return colorHex;
|
|
}
|
|
|
|
qreal
|
|
utils::computeContrast(const qreal &one, const qreal &two)
|
|
{
|
|
auto ratio = (one + 0.05) / (two + 0.05);
|
|
|
|
if (two > one) {
|
|
ratio = 1 / ratio;
|
|
}
|
|
|
|
return ratio;
|
|
}
|
|
|
|
qreal
|
|
utils::luminance(const QColor &col)
|
|
{
|
|
int colRgb[3] = {col.red(), col.green(), col.blue()};
|
|
qreal lumRgb[3];
|
|
|
|
for (int i = 0; i < 3; i++) {
|
|
qreal v = colRgb[i] / 255.0;
|
|
v <= 0.03928 ? lumRgb[i] = v / 12.92 : lumRgb[i] = qPow((v + 0.055) / 1.055, 2.4);
|
|
}
|
|
|
|
auto lum = lumRgb[0] * 0.2126 + lumRgb[1] * 0.7152 + lumRgb[2] * 0.0722;
|
|
|
|
return lum;
|
|
}
|
|
|
|
void
|
|
utils::centerWidget(QWidget *widget, QWidget *parent)
|
|
{
|
|
auto findCenter = [childRect = widget->rect()](QRect hostRect) -> QPoint {
|
|
return QPoint(hostRect.center().x() - (childRect.width() * 0.5),
|
|
hostRect.center().y() - (childRect.height() * 0.5));
|
|
};
|
|
|
|
if (parent) {
|
|
widget->move(parent->window()->frameGeometry().topLeft() +
|
|
parent->window()->rect().center() - widget->rect().center());
|
|
return;
|
|
}
|
|
|
|
// Deprecated in 5.13: widget->move(findCenter(QApplication::desktop()->screenGeometry()));
|
|
widget->move(findCenter(QGuiApplication::primaryScreen()->geometry()));
|
|
}
|
|
|
|
void
|
|
utils::restoreCombobox(QComboBox *combo, const QString &value)
|
|
{
|
|
for (auto i = 0; i < combo->count(); ++i) {
|
|
if (value == combo->itemText(i)) {
|
|
combo->setCurrentIndex(i);
|
|
break;
|
|
}
|
|
}
|
|
}
|