682 lines
21 KiB
C++

/*
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 <http://www.gnu.org/licenses/>.
*/
#include "qet.h"
#include "qeticons.h"
#include <limits>
#include <QGraphicsSceneContextMenuEvent>
#include <QAction>
#include <QFileInfo>
#include <QSaveFile>
#include <QTextStream>
/**
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<QDomElement> QET::findInDomElement(const QDomElement &e, const QString &tag_name) {
QList<QDomElement> 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<QDomElement> QET::findInDomElement(const QDomElement &e, const QString &parent, const QString &children) {
QList<QDomElement> 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<QChar> QET::forbiddenCharacters() {
return(QList<QChar>() << '\\' << '/' << ':' << '*' << '?' << '"' << '<' << '>' << '|');
}
/**
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);
}