/* Copyright 2006-2020 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 "qet.h" #include "qeticons.h" #include #include #include #include #include #include /** Indique si deux orientations de Borne sont sur le meme axe (Vertical / Horizontal). @param a La premiere orientation de Borne @param b La seconde orientation de Borne @return Un booleen a true si les deux orientations de bornes sont sur le meme axe */ bool Qet::surLeMemeAxe(Qet::Orientation a, Qet::Orientation b) { if ((a == Qet::North || a == Qet::South) && (b == Qet::North || b == Qet::South)) return(true); else if ((a == Qet::East || a == Qet::West) && (b == Qet::East || b == Qet::West)) return(true); else return(false); } /** * @brief Qet::isOpposed * @param a * @param b * @return true if a and b is opposed, else false; */ bool Qet::isOpposed(Qet::Orientation a, Qet::Orientation b) { bool result = false; switch (a) { case Qet::North: if (b == Qet::South) result = true; break; case Qet::East: if (b == Qet::West) result = true; break; case Qet::South: if (b == Qet::North) result = true; break; case Qet::West: if (b == Qet::East) result = true; break; default: break; } return result; } /** * @brief Qet::isHorizontal * @param a * @return true if @a is horizontal, else false. */ bool Qet::isHorizontal(Qet::Orientation a) { return(a == Qet::East || a == Qet::West); } /** * @brief Qet::isVertical * @param a * @return true if @a is vertical, else false. */ bool Qet::isVertical(Qet::Orientation a) { return(a == Qet::North || a == Qet::South); } /** Permet de connaitre l'orientation suivante apres celle donnee en parametre. Les orientations sont generalement presentees dans l'ordre suivant : Nord, Est, Sud, Ouest. @param o une orientation @return l'orientation suivante */ Qet::Orientation Qet::nextOrientation(Qet::Orientation o) { if (o < 0 || o > 2) return(Qet::North); return((Qet::Orientation)(o + 1)); } /** Permet de connaitre l'orientation precedant celle donnee en parametre. Les orientations sont generalement presentees dans l'ordre suivant : Nord, Est, Sud, Ouest. @param o une orientation @return l'orientation precedente */ Qet::Orientation Qet::previousOrientation(Qet::Orientation o) { if (o < 0 || o > 3) return(Qet::North); if (o == Qet::North) return(Qet::West); return((Qet::Orientation)(o - 1)); } /** @param line Un segment de droite @param point Un point @return true si le point appartient au segment de droite, false sinon */ bool QET::lineContainsPoint(const QLineF &line, const QPointF &point) { if (point == line.p1()) return(true); QLineF point_line(line.p1(), point); if (point_line.unitVector() != line.unitVector()) return(false); return(point_line.length() <= line.length()); } /** @param point Un point donne @param line Un segment de droite donnee @param intersection si ce pointeur est different de 0, le QPointF ainsi designe contiendra les coordonnees du projete orthogonal, meme si celui-ci n'appartient pas au segment de droite @return true si le projete orthogonal du point sur la droite appartient au segment de droite. */ bool QET::orthogonalProjection(const QPointF &point, const QLineF &line, QPointF *intersection) { // recupere le vecteur normal de `line' QLineF line_normal_vector(line.normalVector()); QPointF normal_vector(line_normal_vector.dx(), line_normal_vector.dy()); // cree une droite perpendiculaire a `line' passant par `point' QLineF perpendicular_line(point, point + normal_vector); // determine le point d'intersection des deux droites = le projete orthogonal QPointF intersection_point; #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) QLineF::IntersectType it = line.intersect(perpendicular_line, &intersection_point); // ### Qt 6: remove #else QLineF::IntersectType it = line.intersects(perpendicular_line, &intersection_point); #endif // ne devrait pas arriver (mais bon...) if (it == QLineF::NoIntersection) return(false); // fournit le point d'intersection a l'appelant si necessaire if (intersection) { *intersection = intersection_point; } // determine si le point d'intersection appartient au segment de droite if (QET::lineContainsPoint(line, intersection_point)) { return(true); } return(false); } /** Permet de savoir si l'attribut nom_attribut d'un element XML e est bien un entier. Si oui, sa valeur est copiee dans entier. @param e Element XML @param nom_attribut Nom de l'attribut a analyser @param entier Pointeur facultatif vers un entier @return true si l'attribut est bien un entier, false sinon */ bool QET::attributeIsAnInteger(const QDomElement &e, const QString& nom_attribut, int *entier) { // verifie la presence de l'attribut if (!e.hasAttribute(nom_attribut)) return(false); // verifie la validite de l'attribut bool ok; int tmp = e.attribute(nom_attribut).toInt(&ok); if (!ok) return(false); if (entier != nullptr) *entier = tmp; return(true); } /** Permet de savoir si l'attribut nom_attribut d'un element XML e est bien un reel. Si oui, sa valeur est copiee dans reel. @param e Element XML @param nom_attribut Nom de l'attribut a analyser @param reel Pointeur facultatif vers un double @return true si l'attribut est bien un reel, false sinon */ bool QET::attributeIsAReal(const QDomElement &e, const QString& nom_attribut, qreal *reel) { // verifie la presence de l'attribut if (!e.hasAttribute(nom_attribut)) return(false); // verifie la validite de l'attribut bool ok; qreal tmp = e.attribute(nom_attribut).toDouble(&ok); if (!ok) return(false); if (reel != nullptr) *reel = tmp; return(true); } /** Permet de composer rapidement la proposition "x elements et y conducteurs" ou encore "x elements, y conducteurs et z champs de texte". @param elements_count nombre d'elements @param conductors_count nombre de conducteurs @param texts_count nombre de champs de texte @param images_count nombre d'images @return la proposition decrivant le nombre d'elements, de conducteurs et de textes */ QString QET::ElementsAndConductorsSentence(int elements_count, int conductors_count, int texts_count, int images_count, int shapes_count, int element_text_count, int tables_count) { QString text; if (elements_count) { text += QObject::tr( "%n élément(s)", "part of a sentence listing the content of a diagram", elements_count ); } if (conductors_count) { if (!text.isEmpty()) text += ", "; text += QObject::tr( "%n conducteur(s)", "part of a sentence listing the content of a diagram", conductors_count ); } if (texts_count) { if (!text.isEmpty()) text += ", "; text += QObject::tr( "%n champ(s) de texte", "part of a sentence listing the content of a diagram", texts_count ); } if (images_count) { if (!text.isEmpty()) text += ", "; text += QObject::tr( "%n image(s)", "part of a sentence listing the content of a diagram", images_count ); } if (shapes_count) { if (!text.isEmpty()) text += ", "; text += QObject::tr( "%n forme(s)", "part of a sentence listing the content of a diagram", shapes_count ); } if (element_text_count) { if (!text.isEmpty()) text += ", "; text += QObject::tr( "%n texte(s) d'élément", "part of a sentence listing the content of a diagram", element_text_count); } if (tables_count) { if (!text.isEmpty()) text += ", "; text += QObject::tr( "%n tableau(s)", "part of a sentence listing the content of diagram", tables_count); } return(text); } /** @return the list of \a tag_name elements directly under the \a e XML element. */ QList QET::findInDomElement(const QDomElement &e, const QString &tag_name) { QList return_list; for (QDomNode node = e.firstChild() ; !node.isNull() ; node = node.nextSibling()) { if (!node.isElement()) continue; QDomElement element = node.toElement(); if (element.isNull() || element.tagName() != tag_name) continue; return_list << element; } return(return_list); } /** Etant donne un element XML e, renvoie la liste de tous les elements children imbriques dans les elements parent, eux-memes enfants de l'elememt e @param e Element XML a explorer @param parent tag XML intermediaire @param children tag XML a rechercher @return La liste des elements XML children */ QList QET::findInDomElement(const QDomElement &e, const QString &parent, const QString &children) { QList return_list; // parcours des elements parents for (QDomNode enfant = e.firstChild() ; !enfant.isNull() ; enfant = enfant.nextSibling()) { // on s'interesse a l'element XML "parent" QDomElement parents = enfant.toElement(); if (parents.isNull() || parents.tagName() != parent) continue; // parcours des enfants de l'element XML "parent" for (QDomNode node_children = parents.firstChild() ; !node_children.isNull() ; node_children = node_children.nextSibling()) { // on s'interesse a l'element XML "children" QDomElement n_children = node_children.toElement(); if (!n_children.isNull() && n_children.tagName() == children) return_list.append(n_children); } } return(return_list); } /// @return le texte de la licence de QElectroTech (GNU/GPL) QString QET::license() { // Recuperation du texte de la GNU/GPL dans un fichier integre a l'application QFile *file_license = new QFile(":/LICENSE"); QString txt_license; // verifie que le fichier existe if (!file_license -> exists()) { txt_license = QString(QObject::tr("Le fichier texte contenant la licence GNU/GPL est introuvable - bon bah de toute façon, vous la connaissez par coeur non ?")); } else { // ouvre le fichier en mode texte et en lecture seule if (!file_license -> open(QIODevice::ReadOnly | QIODevice::Text)) { txt_license = QString(QObject::tr("Le fichier texte contenant la licence GNU/GPL existe mais n'a pas pu être ouvert - bon bah de toute façon, vous la connaissez par coeur non ?")); } else { // charge le contenu du fichier dans une QString QTextStream in(file_license); txt_license = QString(""); while (!in.atEnd()) txt_license += in.readLine()+"\n"; // ferme le fichier file_license -> close(); } } return(txt_license); }; /** @return la liste des caracteres interdits dans les noms de fichiers sous Windows */ QList QET::forbiddenCharacters() { return(QList() << '\\' << '/' << ':' << '*' << '?' << '"' << '<' << '>' << '|'); } /** Cette fonction transforme une chaine de caracteres (typiquement : un nom de schema, de projet, d'element) en un nom de fichier potable. Par nom de fichier potable, on entend un nom : * ne comprenant pas de caracteres interdits sous Windows * ne comprenant pas d'espace @param name Chaine de caractere a transformer en nom de fichier potable @todo virer les caracteres accentues ? */ QString QET::stringToFileName(const QString &name) { QString file_name(name.toLower()); // remplace les caracteres interdits par des tirets foreach(QChar c, QET::forbiddenCharacters()) { file_name.replace(c, '-'); } // remplace les espaces par des underscores file_name.replace(' ', '_'); return(file_name); } /** @param string une chaine de caracteres @return la meme chaine de caracteres, mais avec les espaces et backslashes echappes */ QString QET::escapeSpaces(const QString &string) { return(QString(string).replace('\\', "\\\\").replace(' ', "\\ ")); } /** @param string une chaine de caracteres @return la meme chaine de caracteres, mais avec les espaces et backslashes non echappes */ QString QET::unescapeSpaces(const QString &string) { return(QString(string).replace("\\\\", "\\").replace("\\ ", " ")); } /** Assemble une liste de chaines en une seule. Un espace separe chaque chaine. Les espaces et backslashes des chaines sont echappes. @param string_list une liste de chaine @return l'assemblage des chaines */ QString QET::joinWithSpaces(const QStringList &string_list) { QString returned_string; for (int i = 0 ; i < string_list.count() ; ++ i) { returned_string += QET::escapeSpaces(string_list.at(i)); if (i != string_list.count() - 1) returned_string += " "; } return(returned_string); } /** @param string Une chaine de caracteres contenant des sous-chaines a extraire separees par des espaces non echappes. Les espaces des sous-chaines sont echappes. @return La liste des sous-chaines, sans echappement. */ QStringList QET::splitWithSpaces(const QString &string) { // les chaines sont separees par des espaces non echappes = avec un nombre nul ou pair de backslashes devant #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove QStringList escaped_strings = string.split(QRegExp("[^\\]?(?:\\\\)* "), QString::SkipEmptyParts); #else QStringList escaped_strings = string.split(QRegExp("[^\\]?(?:\\\\)* "), Qt::SkipEmptyParts); #endif QStringList returned_list; foreach(QString escaped_string, escaped_strings) { returned_list << QET::unescapeSpaces(escaped_string); } return(returned_list); } /** @param end_type un type d'extremite @return une chaine representant le type d'extremite */ QString Qet::endTypeToString(const Qet::EndType &end_type) { switch(end_type) { case Qet::Simple: return("simple"); case Qet::Triangle: return("triangle"); case Qet::Circle: return("circle"); case Qet::Diamond: return("diamond"); case Qet::None: default: return("none"); } } /** @param string une chaine representant un type d'extremite @return le type d'extremite correspondant ; si la chaine est invalide, QET::None est retourne. */ Qet::EndType Qet::endTypeFromString(const QString &string) { if (string == "simple") return(Qet::Simple); else if (string == "triangle") return(Qet::Triangle); else if (string == "circle") return(Qet::Circle); else if (string == "diamond") return(Qet::Diamond); else return(Qet::None); } /** @param diagram_area un type de zone de schema @return une chaine representant le type de zone de schema */ QString QET::diagramAreaToString(const QET::DiagramArea &diagram_area) { if (diagram_area == ElementsArea) return("elements"); else return("border"); } /** @param string une chaine representant un type de zone de schema @return le type de zone de schema correspondant ; si la chaine est invalide, QET::ElementsArea est retourne. */ QET::DiagramArea QET::diagramAreaFromString(const QString &string) { if (!string.compare("border", Qt::CaseInsensitive)) return(QET::BorderArea); else return(QET::ElementsArea); } /** Round \a x to the nearest multiple of the invert of \a epsilon. For instance, epsilon = 10 will round to 1/10 = 0.1 */ qreal QET::round(qreal x, qreal epsilon) { return(int(x * epsilon) / epsilon); } /** @param angle Un angle quelconque @return l'angle passe en parametre, mais ramene entre -360.0 + 360.0 degres */ qreal QET::correctAngle(const qreal &angle) { // ramene l'angle demande entre -360.0 et +360.0 degres qreal corrected_angle = angle; while (corrected_angle <= -360.0) corrected_angle += 360.0; while (corrected_angle >= 360.0) corrected_angle -= 360.0; return(corrected_angle); } /** @param first Un premier chemin vers un fichier @param second Un second chemin vers un fichier @return true si les deux chemins existent existent et sont identiques lorsqu'ils sont exprimes sous forme canonique */ bool QET::compareCanonicalFilePaths(const QString &first, const QString &second) { QString first_canonical_path = QFileInfo(first).canonicalFilePath(); if (first_canonical_path.isEmpty()) return(false); QString second_canonical_path = QFileInfo(second).canonicalFilePath(); if (second_canonical_path.isEmpty()) return(false); #ifdef Q_OS_WIN // sous Windows, on ramene les chemins en minuscules first_canonical_path = first_canonical_path.toLower(); second_canonical_path = second_canonical_path.toLower(); #endif return(first_canonical_path == second_canonical_path); } /** Export an XML document to an UTF-8 text file indented with 4 spaces, with LF end of lines and no BOM. @param xml_doc An XML document to be exported @param filepath Path to the file to be written @param error_message If non-zero, will contain an error message explaining what happened when this function returns false. @return false if an error occurred, true otherwise */ bool QET::writeXmlFile(QDomDocument &xml_doc, const QString &filepath, QString *error_message) { QSaveFile file(filepath); // Note: we do not set QIODevice::Text to avoid generating CRLF end of lines bool file_opening = file.open(QIODevice::WriteOnly); if (!file_opening) { if (error_message) { *error_message = QString(QObject::tr("Impossible d'ouvrir le fichier %1 en écriture, erreur %2 rencontrée.", "error message when attempting to write an XML file")).arg(filepath).arg(file.error()); } return(false); } QTextStream out(&file); out.setCodec("UTF-8"); out.setGenerateByteOrderMark(false); out << xml_doc.toString(4); if (!file.commit()) { if (error_message) { *error_message = QString(QObject::tr("Une erreur est survenue lors de l'écriture du fichier %1, erreur %2 rencontrée.", "error message when attempting to write an XML file")).arg(filepath).arg(file.error()); } return false; } return(true); } /** * @brief QET::eachStrIsEqual * @param qsl list of string to compare * @return true if every string is identical, else false; * The list must not be empty * If the list can be empty, call isEmpty() before calling this function */ bool QET::eachStrIsEqual(const QStringList &qsl) { if (qsl.size() == 1) return true; foreach (const QString t, qsl) { if (qsl.at(0) != t) return false; } return true; } /** * @brief QET::qetCollectionToString * @param c QetCollection value to convert * @return The QetCollection enum value converted to a QString */ QString QET::qetCollectionToString(const QET::QetCollection &c) { switch (c) { case Common : return "common"; case Custom : return "custom"; case Embedded : return "embedded"; default: return "common"; } } /** * @brief QET::qetCollectionFromString * @param str string to convert * @return The corresponding QetCollection value from a string. * If the string don't match anything, we return the failsafe value QetCollection::Common */ QET::QetCollection QET::qetCollectionFromString(const QString &str) { if (str == "common") return QetCollection::Common; else if (str == "custom") return QetCollection::Custom; else if (str == "embedded") return QetCollection::Embedded; else return QetCollection::Common; } /** * @brief QET::depthActionGroup * @param parent * @return an action group which contain 4 actions (forward, raise, lower, backward) * already made with icon, shortcut and data (see QET::DepthOption) */ QActionGroup *QET::depthActionGroup(QObject *parent) { QActionGroup *action_group = new QActionGroup(parent); QAction *edit_forward = new QAction(QET::Icons::BringForward, QObject::tr("Amener au premier plan"), action_group); QAction *edit_raise = new QAction(QET::Icons::Raise, QObject::tr("Rapprocher"), action_group); QAction *edit_lower = new QAction(QET::Icons::Lower, QObject::tr("Éloigner"), action_group); QAction *edit_backward = new QAction(QET::Icons::SendBackward, QObject::tr("Envoyer au fond"), action_group); edit_forward ->setStatusTip(QObject::tr("Ramène la ou les sélections au premier plan")); edit_raise ->setStatusTip(QObject::tr("Rapproche la ou les sélections")); edit_lower ->setStatusTip(QObject::tr("Éloigne la ou les sélections")); edit_backward->setStatusTip(QObject::tr("Envoie en arrière plan la ou les sélections")); edit_raise ->setShortcut(QKeySequence(QObject::tr("Ctrl+Shift+Up"))); edit_lower ->setShortcut(QKeySequence(QObject::tr("Ctrl+Shift+Down"))); edit_backward->setShortcut(QKeySequence(QObject::tr("Ctrl+Shift+End"))); edit_forward ->setShortcut(QKeySequence(QObject::tr("Ctrl+Shift+Home"))); edit_forward ->setData(QET::BringForward); edit_raise ->setData(QET::Raise); edit_lower ->setData(QET::Lower); edit_backward->setData(QET::SendBackward); return action_group; } bool QET::writeToFile(QDomDocument &xml_doc, QFile *file, QString *error_message) { bool opened_here = file->isOpen() ? false : true; if (!file->isOpen()) { bool open_ = file->open(QIODevice::WriteOnly); if (!open_) { if (error_message) { QFileInfo info_(*file); *error_message = QString( QObject::tr("Impossible d'ouvrir le fichier %1 en écriture, erreur %2 rencontrée.", "error message when attempting to write an XML file") ).arg(info_.absoluteFilePath()).arg(file->error()); } return false; } } QTextStream out(file); out.seek(0); out.setCodec("UTF-8"); out.setGenerateByteOrderMark(false); out << xml_doc.toString(4); if (opened_here) { file->close(); } return(true); }