mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-11 06:34:14 +02:00
Add cable management: drawing, cores, numbering, cross-references and cable list
Cables are drawn line objects (toolbar button next to the auto break/reconnect button, click-move-click, right click cancels), not components. The type comes from a CSV catalog following the material list pattern (designation, cores, core colours, editable through the entry dialog with "Nouvelle entrée" / "Modifier l'entrée"). Each core is bound to the wire its colour label stands on, and the cable reference is generated per core and written into the Conductor entry via Conductor::setCableReference(label, cable uuid, slot). The conductor entry is where it has to live because the terminal strip plan (Klemmenplan) consumes it in the next step. Numbering: "Cables" rule under Programmeinstellungen/Neues Projekt/Nummerierung auto (mirrored in project properties), literal "W" fallback without a rule, French gate dialog when no rule exists, Nummerierung menu with the renumber dialog (preferred axis, per-folio counter, rule and counter state written back, hand-typed names ask, whole-project scope). Also included: label block and under-line texts with per-text font and alignment plus the material-list cell margins, free-core placement and right-click removal, claim questions for wires of other cables, multi-selection drag as one undo step, type change with confirmation, cross-references for type "Cable" (%f-%l%c, own font, clickable links in exported PDFs), report propagation of the cable definition, Listes menu (table of contents, material list, terminal strip manager, terminal generator plugin, cable list), cable list with freely selectable columns (installation/localisation of cable, start and end, Blatt start/end, length, used cores...), CSV export dialog with column pick and preview in the Projekt menu, script API (addTable kind "cable_list", exportCableList) and a unit test for the type catalog.
This commit is contained in:
+356
-24
@@ -23,6 +23,9 @@
|
||||
#include "autoNum/numerotationcontext.h"
|
||||
#include "autoNum/numerotationcontextcommands.h"
|
||||
#include "autoNum/renumberelementscommand.h"
|
||||
#include "cable/cable.h"
|
||||
#include "cable/cablemanager.h"
|
||||
#include "cable/cablepart.h"
|
||||
#include "diagram.h"
|
||||
#include "qetgraphicsitem/element.h"
|
||||
#include "qetapp.h"
|
||||
@@ -43,27 +46,6 @@
|
||||
#include <QTimer>
|
||||
#include <QtConcurrentRun>
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* @brief Reset numeric fields of a NumerotationContext so renumbering starts at 1.
|
||||
* Keeps non-numeric parts (string/idfolio/folio/plant/locmach/elementline/elementcolumn/elementprefix) unchanged.
|
||||
*/
|
||||
NumerotationContext resetContextForRenumber(const NumerotationContext &tmpl)
|
||||
{
|
||||
NumerotationContext out = tmpl;
|
||||
for (int i = 0; i < out.size(); ++i) {
|
||||
const QStringList parts = out.itemAt(i);
|
||||
if (parts.isEmpty()) continue;
|
||||
const QString type = parts.at(0);
|
||||
if (out.keyIsNumber(type)) {
|
||||
out.replaceValue(i, QStringLiteral("1"));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
#include <QtDebug>
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
@@ -131,6 +113,17 @@ m_project_properties_handler{this}
|
||||
for (auto it = folioData.first.constBegin(); it != folioData.first.constEnd(); ++it) {
|
||||
addFolioAutoNum(it.key(), it.value());
|
||||
}
|
||||
|
||||
//The cable numbering rule a project starts with, so a project
|
||||
//made from these defaults numbers its cables the way the
|
||||
//global settings say without having to be opened first.
|
||||
auto cableData = NumerotationContext::loadFromSettings(settings, QStringLiteral("autonum/cable"));
|
||||
for (auto it = cableData.first.constBegin(); it != cableData.first.constEnd(); ++it) {
|
||||
addCableAutoNum(it.key(), it.value());
|
||||
}
|
||||
if (!cableData.second.isEmpty()) {
|
||||
setCurrentCableAutoNum(cableData.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -840,6 +833,16 @@ QHash <QString, NumerotationContext> QETProject::elementAutoNum() const
|
||||
return m_element_autonum;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETProject::cableAutoNum
|
||||
@return the cable numerotation stored in the project -- one entry at
|
||||
most, since cables are numbered by a single rule
|
||||
*/
|
||||
QHash <QString, NumerotationContext> QETProject::cableAutoNum() const
|
||||
{
|
||||
return m_cable_autonum;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETProject::elementAutoNumFormula
|
||||
@param key : autonum title
|
||||
@@ -880,6 +883,83 @@ void QETProject::setCurrrentElementAutonum(QString autoNum) {
|
||||
m_current_element_autonum = std::move(autoNum);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETProject::cableAutoNumFormula
|
||||
@param key : autonum title
|
||||
@return Formula of the cable numerotation stored with key
|
||||
*/
|
||||
QString QETProject::cableAutoNumFormula(const QString& key) const
|
||||
{
|
||||
if (m_cable_autonum.contains(key)) {
|
||||
return autonum::numerotationContextToFormula(m_cable_autonum.value(key));
|
||||
}
|
||||
|
||||
return QString();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETProject::hasCableAutoNum
|
||||
@return true when a numbering rule for cables is set: a context
|
||||
stored under the name which is in use. A project which has never been
|
||||
given one answers false, and that is what makes the cable tool ask
|
||||
whether one should be defined (see QETDiagramEditor).
|
||||
*/
|
||||
bool QETProject::hasCableAutoNum() const
|
||||
{
|
||||
if (m_current_cable_autonum.isEmpty()) return false;
|
||||
return !cableAutoNum(m_current_cable_autonum).isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETProject::cableCurrentAutoNum
|
||||
@return title of the cable numerotation in use (one rule only)
|
||||
*/
|
||||
QString QETProject::cableCurrentAutoNum() const
|
||||
{
|
||||
return m_current_cable_autonum;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETProject::setCurrentCableAutoNum
|
||||
@param autoNum : the rule which from now on numbers the cables
|
||||
*/
|
||||
void QETProject::setCurrentCableAutoNum(QString autoNum)
|
||||
{
|
||||
m_current_cable_autonum = std::move(autoNum);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETProject::cableAutoNumRuleName
|
||||
@return the name the one and only cable numbering rule is kept under
|
||||
*/
|
||||
QString QETProject::cableAutoNumRuleName()
|
||||
{
|
||||
return QStringLiteral("Cable");
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETProject::cableXAxisFirst
|
||||
@return true when the cables are numbered along the X axis (the
|
||||
leftmost first), false when they are numbered along the Y axis (the
|
||||
topmost first)
|
||||
*/
|
||||
bool QETProject::cableXAxisFirst() const
|
||||
{
|
||||
return m_cable_axis_x_first;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETProject::setCableXAxisFirst
|
||||
Which axis the whole project numbers its cables along. Stored with
|
||||
the rule rather than on its own, since it only means anything while
|
||||
a rule is being applied to every cable at once.
|
||||
@param x_axis_first
|
||||
*/
|
||||
void QETProject::setCableXAxisFirst(bool x_axis_first)
|
||||
{
|
||||
m_cable_axis_x_first = x_axis_first;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETProject::renumberElementsBySchemeTitle
|
||||
Renumber existing elements by element autonumbering scheme title.
|
||||
@@ -942,7 +1022,7 @@ void QETProject::renumberElementsBySchemeTitle(const QString &scheme_title)
|
||||
std::sort(elements.begin(), elements.end(), [](Element *a, Element *b){ return comparPos(a, b); });
|
||||
|
||||
NumerotationContext base_tmpl = m_element_autonum.value(key);
|
||||
NumerotationContext nc = resetContextForRenumber(base_tmpl);
|
||||
NumerotationContext nc = autonum::resetContextCounters(base_tmpl);
|
||||
NumerotationContextCommands ncc(nc);
|
||||
|
||||
for (Element *el : elements) {
|
||||
@@ -1060,6 +1140,32 @@ void QETProject::addFolioAutoNum(const QString& key, const NumerotationContext&
|
||||
emit autoNumContextUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETProject::addCableAutoNum
|
||||
Store the cable numbering rule. There is room for exactly one: saving
|
||||
it under a name drops every other entry, so a project can never end up
|
||||
with a list of cable rules to choose from.
|
||||
@param key
|
||||
@param context
|
||||
*/
|
||||
void QETProject::addCableAutoNum(const QString& key, const NumerotationContext& context)
|
||||
{
|
||||
if (!key.isEmpty()) {
|
||||
const QStringList others = m_cable_autonum.keys();
|
||||
for (const QString &other : others) {
|
||||
if (other != key) m_cable_autonum.remove(other);
|
||||
}
|
||||
}
|
||||
m_cable_autonum.insert(key, context);
|
||||
//The rule in use must always be one which exists: after a save
|
||||
//under another name the old entry is gone, so follow the new one.
|
||||
if (!key.isEmpty() && (m_current_cable_autonum.isEmpty()
|
||||
|| !m_cable_autonum.contains(m_current_cable_autonum))) {
|
||||
m_current_cable_autonum = key;
|
||||
}
|
||||
emit autoNumContextUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETProject::removeConductorAutoNum
|
||||
Remove Conductor Numerotation Context stored with key
|
||||
@@ -1089,6 +1195,21 @@ void QETProject::removeFolioAutoNum(const QString& key) {
|
||||
m_folio_autonum.remove(key);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETProject::removeCableAutoNum
|
||||
Forget the cable numbering rule stored with key
|
||||
@param key
|
||||
*/
|
||||
void QETProject::removeCableAutoNum(const QString& key)
|
||||
{
|
||||
m_cable_autonum.remove(key);
|
||||
if (m_current_cable_autonum == key) {
|
||||
m_current_cable_autonum = m_cable_autonum.isEmpty()
|
||||
? QString() : m_cable_autonum.keys().first();
|
||||
}
|
||||
emit autoNumContextUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETProject::conductorAutoNum
|
||||
Return conductor numerotation context stored with key.
|
||||
@@ -1124,6 +1245,18 @@ NumerotationContext QETProject::folioAutoNum (const QString &key) const
|
||||
else return NumerotationContext();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETProject::cableAutoNum
|
||||
Return the cable numerotation context stored with key.
|
||||
If key is not found, return an empty numerotation context
|
||||
@param key
|
||||
*/
|
||||
NumerotationContext QETProject::cableAutoNum(const QString &key) const
|
||||
{
|
||||
if (m_cable_autonum.contains(key)) return m_cable_autonum.value(key);
|
||||
else return NumerotationContext();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETProject::freezeExistentConductorLabel
|
||||
Freeze Existent Conductors in the selected folios
|
||||
@@ -1402,6 +1535,18 @@ QDomDocument QETProject::toXml()
|
||||
project_root.appendChild(xml_strip);
|
||||
}
|
||||
|
||||
//Write the cables to xml. Their sections are written here too,
|
||||
//beside the cable they belong to, so a cable and everything drawn
|
||||
//for it are read back together and can never come apart.
|
||||
if (m_cables.count())
|
||||
{
|
||||
auto xml_cables = xml_doc.createElement(QStringLiteral("cables"));
|
||||
for (auto &cable : m_cables) {
|
||||
xml_cables.appendChild(cable->toXml(xml_doc));
|
||||
}
|
||||
project_root.appendChild(xml_cables);
|
||||
}
|
||||
|
||||
// Write the elements collection.
|
||||
project_root.appendChild(m_elements_collection->root().cloneNode(true));
|
||||
|
||||
@@ -1905,10 +2050,23 @@ void QETProject::readProjectXml(QDomDocument &xml_project)
|
||||
readTerminalStripXml(xml_project);
|
||||
const qint64 strips_ms = phase_timer.restart();
|
||||
|
||||
//Load the cables, after the diagrams their sections are drawn on
|
||||
readCableXml(xml_project);
|
||||
const qint64 cables_ms = phase_timer.restart();
|
||||
|
||||
//Now that all are loaded we refresh content of the project.
|
||||
refresh();
|
||||
const qint64 refresh_ms = phase_timer.restart();
|
||||
|
||||
//refresh() above resolved the folio report links for the first
|
||||
//time (refreshContents() -> initLink). The cable label pass at
|
||||
//the end of readCableXml() ran before that, while no element
|
||||
//knew its partners yet, so a core running through a report could
|
||||
//not be carried over there -- it was even cleared off the far
|
||||
//wire again. Run the pass once more now that the links exist:
|
||||
//it recomputes everything from the project as it now stands.
|
||||
CableManager::refreshLabels(this);
|
||||
|
||||
m_data_base.blockSignals(false);
|
||||
m_data_base.setUpdateBlocked(false);
|
||||
m_data_base.updateDB(xml_project);
|
||||
@@ -1916,11 +2074,12 @@ void QETProject::readProjectXml(QDomDocument &xml_project)
|
||||
|
||||
qInfo().nospace()
|
||||
<< "Project content built in "
|
||||
<< (elements_ms + diagrams_ms + strips_ms
|
||||
<< (elements_ms + diagrams_ms + strips_ms + cables_ms
|
||||
+ refresh_ms + database_ms) / 1000.0
|
||||
<< " seconds (elements collection " << elements_ms / 1000.0
|
||||
<< ", diagrams " << diagrams_ms / 1000.0
|
||||
<< ", terminal strips " << strips_ms / 1000.0
|
||||
<< ", cables " << cables_ms / 1000.0
|
||||
<< ", refresh " << refresh_ms / 1000.0
|
||||
<< ", database " << database_ms / 1000.0 << ")";
|
||||
|
||||
@@ -2092,7 +2251,7 @@ void QETProject::readDefaultPropertiesXml(QDomDocument &xml_project)
|
||||
m_default_xref_properties = XRefProperties:: defaultProperties();
|
||||
|
||||
//Read values indicate in project
|
||||
QDomElement border_elmt, titleblock_elmt, conductors_elmt, report_elmt, xref_elmt, conds_autonums, folio_autonums, element_autonums, guides_elmt;
|
||||
QDomElement border_elmt, titleblock_elmt, conductors_elmt, report_elmt, xref_elmt, conds_autonums, folio_autonums, element_autonums, cable_autonums, guides_elmt;
|
||||
|
||||
for (QDomNode child = newdiagrams_elmt.firstChild() ; !child.isNull() ; child = child.nextSibling())
|
||||
{
|
||||
@@ -2115,6 +2274,8 @@ void QETProject::readDefaultPropertiesXml(QDomDocument &xml_project)
|
||||
folio_autonums = child_elmt;
|
||||
else if (child_elmt.tagName()== QLatin1String("element_autonums"))
|
||||
element_autonums = child_elmt;
|
||||
else if (child_elmt.tagName()== QLatin1String("cable_autonums"))
|
||||
cable_autonums = child_elmt;
|
||||
else if (child_elmt.tagName() == QLatin1String("guides"))
|
||||
guides_elmt = child_elmt;
|
||||
}
|
||||
@@ -2165,6 +2326,30 @@ void QETProject::readDefaultPropertiesXml(QDomDocument &xml_project)
|
||||
m_element_autonum.insert(elmt.attribute(QStringLiteral("title")), nc);
|
||||
}
|
||||
}
|
||||
if (!cable_autonums.isNull())
|
||||
{
|
||||
m_current_cable_autonum = cable_autonums.attribute(QStringLiteral("current_autonum"));
|
||||
//Which axis the cables are numbered along. A file written
|
||||
//before this existed has no say in it and stays with the
|
||||
//axis the terminals have always started from.
|
||||
m_cable_axis_x_first = cable_autonums.attribute(
|
||||
QStringLiteral("axis_priority"), QStringLiteral("x"))
|
||||
!= QLatin1String("y");
|
||||
for (auto elmt : QET::findInDomElement(cable_autonums, QStringLiteral("cable_autonum")))
|
||||
{
|
||||
NumerotationContext nc;
|
||||
nc.fromXml(elmt);
|
||||
if (elmt.attribute(QStringLiteral("title")).isEmpty()) continue;
|
||||
m_cable_autonum.insert(elmt.attribute(QStringLiteral("title")), nc);
|
||||
}
|
||||
//A file written by hand, or written before this rule existed,
|
||||
//may name a rule which is not in the list: the numbering must
|
||||
//never be left pointing at something which is not there.
|
||||
if (!m_cable_autonum.contains(m_current_cable_autonum)) {
|
||||
m_current_cable_autonum = m_cable_autonum.isEmpty()
|
||||
? QString() : m_cable_autonum.keys().first();
|
||||
}
|
||||
}
|
||||
// Read guides from XML (if missing, e.g. in old projects, list stays empty)
|
||||
m_default_guides.clear();
|
||||
|
||||
@@ -2199,6 +2384,55 @@ void QETProject::readTerminalStripXml(const QDomDocument &xml_project)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief QETProject::readCableXml
|
||||
* Read the cables of the project, and draw a section for each of them on
|
||||
* the folio it belongs to.
|
||||
*
|
||||
* The diagrams have already been read at this point, which is what makes
|
||||
* it possible to put the sections back where they were: a folio is
|
||||
* identified by its uuid, so reordering or renumbering pages never moves
|
||||
* a cable onto the wrong one.
|
||||
* @param xml_project
|
||||
*/
|
||||
void QETProject::readCableXml(const QDomDocument &xml_project)
|
||||
{
|
||||
auto xml_elmt = xml_project.documentElement();
|
||||
auto xml_cables = xml_elmt.firstChildElement(QStringLiteral("cables"));
|
||||
if (xml_cables.isNull()) return;
|
||||
|
||||
for (auto xml_cable = xml_cables.firstChildElement(QStringLiteral("cable"));
|
||||
!xml_cable.isNull();
|
||||
xml_cable = xml_cable.nextSiblingElement(QStringLiteral("cable")))
|
||||
{
|
||||
auto cable = new Cable(this);
|
||||
cable->fromXml(xml_cable);
|
||||
addCable(cable);
|
||||
|
||||
for (const CablePartData &part_data : cable->parts())
|
||||
{
|
||||
Diagram *diagram = diagramByUuid(part_data.diagram);
|
||||
if (!diagram)
|
||||
{
|
||||
//The folio this section was drawn on has been removed:
|
||||
//the section goes with it, and so do the cores which
|
||||
//were only ever shown on it.
|
||||
cable->removePart(part_data.uuid);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto part = new CablePart(part_data);
|
||||
part->setCable(cable);
|
||||
diagram->addItem(part);
|
||||
}
|
||||
}
|
||||
|
||||
//The file holds the generated cable field of every conductor as
|
||||
//well as the cables themselves; working it out again here makes
|
||||
//sure the two agree even if a file was edited by hand.
|
||||
CableManager::refreshLabels(this);
|
||||
}
|
||||
|
||||
/**
|
||||
Export project properties under the \a xml_element XML element.
|
||||
*/
|
||||
@@ -2331,6 +2565,24 @@ void QETProject::writeDefaultPropertiesXml(QDomElement &xml_element)
|
||||
}
|
||||
xml_element.appendChild(element_autonums);
|
||||
|
||||
//Export Cable Autonum -- one rule, but stored exactly like the others
|
||||
//so it travels with the project and survives a re-save untouched.
|
||||
QDomElement cable_autonums = xml_document.createElement("cable_autonums");
|
||||
cable_autonums.setAttribute("current_autonum", m_current_cable_autonum);
|
||||
cable_autonums.setAttribute("axis_priority",
|
||||
m_cable_axis_x_first ? "x" : "y");
|
||||
QStringList cable_autonum_keys = cableAutoNum().keys();
|
||||
cable_autonum_keys.sort();
|
||||
for (const QString &key : std::as_const(cable_autonum_keys)) {
|
||||
QDomElement cable_autonum = cableAutoNum(key).toXml(xml_document, "cable_autonum");
|
||||
if (key != "" && cableAutoNumFormula(key) != "") {
|
||||
cable_autonum.setAttribute("title", key);
|
||||
cable_autonum.setAttribute("formula", cableAutoNumFormula(key));
|
||||
cable_autonums.appendChild(cable_autonum);
|
||||
}
|
||||
}
|
||||
xml_element.appendChild(cable_autonums);
|
||||
|
||||
// Export default guides
|
||||
QDomElement guides_elmt = xml_document.createElement("guides");
|
||||
for (const auto &g : m_default_guides) {
|
||||
@@ -2516,6 +2768,86 @@ bool QETProject::removeTerminalStrip(TerminalStrip *strip) {
|
||||
return m_terminal_strip_vector.removeOne(strip);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief QETProject::cables
|
||||
* @return every cable of this project
|
||||
*/
|
||||
QVector<Cable *> QETProject::cables() const {
|
||||
return m_cables;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief QETProject::newCable
|
||||
* Create a cable owned by this project and watch it: whenever it changes,
|
||||
* the cable field of every conductor it feeds is worked out again.
|
||||
* @return the new cable
|
||||
*/
|
||||
Cable *QETProject::newCable()
|
||||
{
|
||||
auto cable = new Cable(this);
|
||||
addCable(cable);
|
||||
return cable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief QETProject::addCable
|
||||
* @param cable
|
||||
* @return true when the cable is now held by this project
|
||||
*/
|
||||
bool QETProject::addCable(Cable *cable)
|
||||
{
|
||||
if (!cable || cable->parent() != this)
|
||||
return false;
|
||||
|
||||
if (m_cables.contains(cable))
|
||||
return true;
|
||||
|
||||
m_cables.append(cable);
|
||||
connect(cable, &Cable::changed, this, [this]() {
|
||||
CableManager::refreshLabels(this);
|
||||
setModified(true);
|
||||
});
|
||||
emit cableAdded(cable);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief QETProject::removeCable
|
||||
* Cable is removed from the list but not deleted, so an undo command can
|
||||
* put it back.
|
||||
* @param cable
|
||||
* @return true when the cable was in the list
|
||||
*/
|
||||
bool QETProject::removeCable(Cable *cable) {
|
||||
if (!m_cables.removeOne(cable))
|
||||
return false;
|
||||
|
||||
disconnect(cable, nullptr, this, nullptr);
|
||||
|
||||
//A cable which is no longer one of ours stops writing its labels
|
||||
//into the conductors it used to feed.
|
||||
CableManager::refreshLabels(this);
|
||||
setModified(true);
|
||||
emit cableRemoved(cable);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief QETProject::diagramByUuid
|
||||
* @param uuid
|
||||
* @return the folio with that identity, or nullptr. A cable section says
|
||||
* which folio it is drawn on by identity, since folios are reordered and
|
||||
* renumbered freely.
|
||||
*/
|
||||
Diagram *QETProject::diagramByUuid(const QUuid &uuid)
|
||||
{
|
||||
if (uuid.isNull()) return nullptr;
|
||||
for (Diagram *diagram : m_diagrams_list) {
|
||||
if (diagram && diagram->uuid() == uuid) return diagram;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
Cette methode sert a reperer un projet vide, c-a-d un projet identique a ce
|
||||
que l'on obtient en faisant Fichier > Nouveau.
|
||||
|
||||
Reference in New Issue
Block a user