/* Copyright 2006-2026 The QElectroTech Team This file is part of QElectroTech. QElectroTech is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. QElectroTech is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with QElectroTech. If not, see . */ #include "qetutils.h" #include #include #include #include "../qetapp.h" #include "../qetdiagrameditor.h" /** @brief QETUtils::marginsToString @param margins @return QString str "left;top;right;bottom" */ QString QETUtils::marginsToString(const QMargins &margins) { QString str; str += QString::number(margins.left()); str += ";"; str += QString::number(margins.top()); str += ";"; str += QString::number(margins.right()); str += ";"; str += QString::number(margins.bottom()); return str; } /** @brief QETUtils::marginsFromString @param string @return QMargins margins */ QMargins QETUtils::marginsFromString(const QString &string) { QMargins margins; auto split = string.split(";"); if (split.size() != 4) return margins; margins.setLeft (split.at(0).toInt()); margins.setTop (split.at(1).toInt()); margins.setRight (split.at(2).toInt()); margins.setBottom(split.at(3).toInt()); return margins; } /** * @brief QETUtils::graphicsHandlerSize * @param item * @return Return the handler size to use in the QGraphicsScene of @a item. * If size can't be found, return 10 by default. */ qreal QETUtils::graphicsHandlerSize(QGraphicsItem *item) { if (const auto scene_ = item->scene()) { if (!scene_->views().isEmpty()) { if (const auto editor_ = QETApp::instance()->diagramEditorAncestorOf(scene_->views().at(0))) { const auto variant_ = editor_->property("graphics_handler_size"); //If variant_ can't be converted to qreal, the returned qreal is 0.0 //it's sufficient for us to check if value is set or not. if (const auto value_ = variant_.toReal()) return value_; } } } //Default value return 10; } /** * @brief QETUtils::sortBeginIntString * Sort the string @a str_a and @a str_b and take in * count if string begin with an int to sort it * as int and not as string in this case. * For example if we have to sort the string : * "3str", 10str", "100str", "2str", "20str". * The default behavior when sorting QString with the comparison operator will be: * "10str" "100str" "2str", "20str", "3str" * When sorting with this function, the result will be : * "10str", "2str", "3str", "20str", "100str" * @param str_a * @param str_b * @return */ bool QETUtils::sortBeginIntString(const QString &str_a, const QString &str_b) { const QRegularExpression rx(QStringLiteral("^\\d+")); int int_a =-1; int int_b =-1; auto match_a = rx.match(str_a); if (match_a.hasMatch()) { int_a = match_a.captured(0).toInt(); } auto match_b = rx.match(str_b); if (match_b.hasMatch()) { int_b = match_b.captured(0).toInt(); } //Sort as numbers if both string //start at least by a digit and //the number of each string are different. //Else sort as string if (int_a >= 0 && int_b >= 0 && int_a != int_b) { return int_a -1) { return; } auto px = font.pointSizeF()/72 * QFontMetrics{font}.fontDpi(); font.setPixelSize(qRound(px)); } namespace { /* Counters for fontFromString(), reset per project load so the editor * can report how many stored font descriptions needed salvaging or were * unreadable. Font parsing only happens on the main thread. */ int salvaged_font_count = 0; int unreadable_font_count = 0; /** * Legacy (Qt 5) weight <- OpenType weight, closest match, * same table Qt uses when parsing a 10/11 field string. */ int legacyFontWeight(const int opentype_weight) { static const int weight_map[9][2] = { {0, 100}, {12, 200}, {25, 300}, {50, 400}, {57, 500}, {63, 600}, {75, 700}, {81, 800}, {87, 900} }; int legacy_weight = weight_map[0][0]; int closest_diff = qAbs(opentype_weight - weight_map[0][1]); for (int i = 1 ; i < 9 ; ++i) { const int diff = qAbs(opentype_weight - weight_map[i][1]); if (diff < closest_diff) { closest_diff = diff; legacy_weight = weight_map[i][0]; } } return legacy_weight; } } /** * @brief QETUtils::fontToString * Serialize a font to the 10/11 field description format written by Qt 5, * to be used instead of QFont::toString() everywhere a font description is * stored in a project, element or settings file. * The format of QFont::toString() is not stable across Qt versions : Qt 6.11 * switched to a 19 field format carrying OpenType weights, which * QFont::fromString() of Qt 5.x and Qt <= 6.10 rejects, leaving a broken font. * The 10/11 field form is parsed correctly by every Qt version (they convert * the legacy weight scale as needed), so composing it ourselves keeps files * readable by every QET build in circulation. * See @link https://github.com/qelectrotech/qelectrotech-source-mirror/issues/553 @endlink * @param font * @return the font description string */ QString QETUtils::fontToString(const QFont &font) { #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) return font.toString(); #else const int legacy_weight = legacyFontWeight(font.weight()); const QChar comma(QLatin1Char(',')); QString description = font.family() + comma + QString::number(font.pointSizeF()) + comma + QString::number(font.pixelSize()) + comma + QString::number(int(font.styleHint())) + comma + QString::number(legacy_weight) + comma + QString::number(int(font.style())) + comma + QString::number(int(font.underline())) + comma + QString::number(int(font.strikeOut())) + comma + QString::number(int(font.fixedPitch())) + comma + QString::number(0); if (!font.styleName().isEmpty()) { description += QChar(',') + font.styleName(); } return description; #endif } /** * @brief QETUtils::fontFromString * Restore a font from a description string, tolerating descriptions written * by any Qt version. QFont::fromString() of Qt 5.x and Qt <= 6.10 rejects the * >= 19 field format QFont::toString() emits since Qt 6.11 (16 base fields * plus style name, font-feature count and variable-axis count), leaving a * broken font behind. When the native parser fails, salvage such a * description by re-composing the 10/11 field form (OpenType weight mapped * back to the legacy scale) and parsing that instead, so no font information * stored in existing files is lost. * See @link https://github.com/qelectrotech/qelectrotech-source-mirror/issues/553 @endlink * @param font : font to restore into; left untouched on failure, so the * caller's default keeps applying. * @param description : font description string * @return true if the description could be parsed */ bool QETUtils::fontFromString(QFont &font, const QString &description) { if (description.trimmed().isEmpty()) { return false; } QFont parsed(font); if (parsed.fromString(description)) { font = parsed; return true; } const QStringList l = description.split(QLatin1Char(',')); const int count = l.size(); const QChar comma(QLatin1Char(',')); //16 base fields + style name + feature count + axis count; a family //name containing commas adds leading fields. Descriptions actually //using font features or variable axes append value groups whose //layout we cannot anchor reliably, so only salvage the plain //"...,0,0" case (the only one QET-edited files can contain). if (count >= 19 && l.at(count - 1).trimmed() == QLatin1String("0") && l.at(count - 2).trimmed() == QLatin1String("0")) { QString legacy = QStringList(l.mid(0, count - 18)).join(comma) + comma + // family l.at(count - 18) + comma + // pointSizeF l.at(count - 17) + comma + // pixelSize l.at(count - 16) + comma + // styleHint QString::number(legacyFontWeight(l.at(count - 15).toInt())) + comma + l.at(count - 14) + comma + // style l.at(count - 13) + comma + // underline l.at(count - 12) + comma + // strikeOut l.at(count - 11) + comma + // fixedPitch QString::number(0); const QString style_name = l.at(count - 3); if (!style_name.isEmpty()) { legacy += comma + style_name; } if (parsed.fromString(legacy)) { font = parsed; ++salvaged_font_count; return true; } ++unreadable_font_count; return false; } //Some historical builds double-serialized fonts, embedding a complete //legacy description as the family name of a second one (21 fields: //"Caladea,9,-1,5,75,1,0,0,0,0,Bold Italic,9,-1,0,50,0,0,0,0,0,Regular"). //The embedded leading description carries the real font, and taking it //matches what the lenient parser of Qt 6.11+ resolves such strings to. if (count > 11 && parsed.fromString(QStringList(l.mid(0, 11)).join(comma))) { font = parsed; ++salvaged_font_count; return true; } ++unreadable_font_count; return false; } /** * @brief QETUtils::FontRestorationScope::FontRestorationScope * Open a fresh counting window: the enclosing window's counts are kept * aside and restored by the destructor, so a project load nested inside * another one (through the event loop) reports its own numbers only. */ QETUtils::FontRestorationScope::FontRestorationScope() : m_outer_salvaged(salvaged_font_count), m_outer_unreadable(unreadable_font_count) { salvaged_font_count = 0; unreadable_font_count = 0; } QETUtils::FontRestorationScope::~FontRestorationScope() { salvaged_font_count = m_outer_salvaged; unreadable_font_count = m_outer_unreadable; } /** * @brief QETUtils::FontRestorationScope::salvaged * @return How many font descriptions fontFromString() restored from a * foreign or corrupt format since this window was opened. Such descriptions * are rewritten in the stable format on the next save. */ int QETUtils::FontRestorationScope::salvaged() const { return salvaged_font_count; } /** * @brief QETUtils::FontRestorationScope::unreadable * @return How many font descriptions fontFromString() could not restore at * all since this window was opened (the caller's default font applies). */ int QETUtils::FontRestorationScope::unreadable() const { return unreadable_font_count; }