diff --git a/build-aux/windows/QET64.nsi b/build-aux/windows/QET64.nsi
index fc79b780a..529efce82 100644
--- a/build-aux/windows/QET64.nsi
+++ b/build-aux/windows/QET64.nsi
@@ -338,6 +338,9 @@ Section ""
WriteRegStr HKEY_CLASSES_ROOT "qet_diagram_file\DefaultIcon" "" "$final_project_ico"
WriteRegStr HKEY_CLASSES_ROOT "qet_diagram_file\shell\open\command" "" '"$final_qet_exe" "%1"'
+ ; File associations - .qetz, the zipped project: the same file type
+ WriteRegStr HKEY_CLASSES_ROOT ".qetz" "" "qet_diagram_file"
+
; File associations - .elmt
WriteRegStr HKEY_CLASSES_ROOT ".elmt" "" "qet_element_file"
WriteRegStr HKEY_CLASSES_ROOT "qet_element_file" "" "Element QET"
@@ -486,6 +489,7 @@ Section "Uninstall"
; Remove file associations registry keys
DeleteRegKey HKEY_CLASSES_ROOT "Applications\qelectrotech.exe"
DeleteRegKey HKEY_CLASSES_ROOT ".qet"
+ DeleteRegKey HKEY_CLASSES_ROOT ".qetz"
DeleteRegKey HKEY_CLASSES_ROOT "qet_diagram_file"
DeleteRegKey HKEY_CLASSES_ROOT ".elmt"
DeleteRegKey HKEY_CLASSES_ROOT "qet_element_file"
diff --git a/misc/Info.plist b/misc/Info.plist
index 3d138a781..c41a2f03f 100644
--- a/misc/Info.plist
+++ b/misc/Info.plist
@@ -32,6 +32,7 @@
CFBundleTypeExtensions
qet
+ qetz
CFBundleTypeRole
Editor
diff --git a/misc/org.qelectrotech.qelectrotech.desktop b/misc/org.qelectrotech.qelectrotech.desktop
index d76e23964..86876992c 100644
--- a/misc/org.qelectrotech.qelectrotech.desktop
+++ b/misc/org.qelectrotech.qelectrotech.desktop
@@ -6,7 +6,7 @@ Exec=qelectrotech %F
Icon=qelectrotech
Terminal=false
Type=Application
-MimeType=application/x-qet-project;application/x-qet-element;application/x-qet-titleblock;
+MimeType=application/x-qet-project;application/x-qelectrotech-project+zip;application/x-qet-element;application/x-qet-titleblock;
Categories=Graphics;
Keywords=Graphics;Science;Electricity;Engineering;
Comment=Edit electrical diagrams.
diff --git a/misc/qelectrotech.xml b/misc/qelectrotech.xml
index 9c59b116b..cefe25eb5 100644
--- a/misc/qelectrotech.xml
+++ b/misc/qelectrotech.xml
@@ -34,6 +34,14 @@
QElectroTech 项目文件
+
+
+ QElectroTech zipped project file
+
+
+
+
+
QElectroTech element file
diff --git a/sources/projectview.cpp b/sources/projectview.cpp
index 47bdc0719..ab62a3591 100644
--- a/sources/projectview.cpp
+++ b/sources/projectview.cpp
@@ -341,13 +341,15 @@ QString ProjectView::askUserForFilePath(bool assign) {
// ask the user for a filepath in order to save the project
const QString qet_filter = tr("Project QElectroTech (*.qet)", "filetypes allowed when saving a project file");
const QString qetz_filter = tr("Zipped project QElectroTech (*.qetz)", "filetypes allowed when saving a project file");
- QString selected_filter = QETProject::isContainerPath(m_project->filePath()) ? qetz_filter : qet_filter;
+ //A project is saved as a .qetz unless a .qet is chosen
+ //(stage D1)
+ QString selected_filter = qetz_filter;
QString filepath = QFileDialog::getSaveFileName(
this,
tr("Save as", "dialog title"),
m_project -> currentDir() + "/" + tr("untitled")
+ (selected_filter == qetz_filter ? ".qetz" : ".qet"),
- qet_filter + ";;" + qetz_filter,
+ qetz_filter + ";;" + qet_filter,
&selected_filter
);
@@ -725,6 +727,32 @@ QETResult ProjectView::saveAs()
return(doSave());
}
+/**
+ @brief ProjectView::saveCopyForOlderVersions
+ Ask for a .qet path and write the project there, for a version of
+ QElectroTech that cannot open a .qetz. The project keeps its file.
+ @return a QETResult object reflecting the situation; a valid one if
+ the operation was cancelled
+*/
+QETResult ProjectView::saveCopyForOlderVersions()
+{
+ if (!m_project) return(noProjectResult());
+
+ QString base = m_project->filePath().isEmpty()
+ ? m_project->currentDir() + "/" + tr("untitled")
+ : m_project->filePath();
+ for (const QString &suffix : {QStringLiteral(".qetz"), QStringLiteral(".qet")})
+ if (base.endsWith(suffix, Qt::CaseInsensitive)) base.chop(suffix.size());
+ QString path = QFileDialog::getSaveFileName(
+ this,
+ tr("Save a copy for an older QElectroTech", "dialog title"),
+ base + ".qet",
+ tr("Project QElectroTech (*.qet)", "filetypes allowed when saving a project file"));
+ if (path.isEmpty()) return(QETResult());
+ if (!path.endsWith(".qet", Qt::CaseInsensitive)) path += ".qet";
+ return(m_project->writeCopyForOlderVersions(path));
+}
+
/**
Save project content, then write the project file. May
call saveAs if no filepath was provided before.
@@ -741,6 +769,31 @@ QETResult ProjectView::doSave()
return(saveAs());
}
+ //A project read from a .qet is saved as a .qetz beside it: say so,
+ //and ask before replacing a .qetz already there
+ if (m_project->savesBesideOpenedFile()) {
+ const QString beside = m_project->besideContainerPath();
+ const QString message = tr("This project was opened from %1. It is saved as %2, "
+ "beside it, in the zipped project format; %1 stays as "
+ "it is.\n\nTo write a file for a version of "
+ "QElectroTech that cannot open %2, use File > Save a "
+ "copy for an older QElectroTech.")
+ .arg(QFileInfo(m_project->filePath()).fileName(), QFileInfo(beside).fileName());
+ if (QFileInfo::exists(beside)) {
+ if (QET::QetMessageBox::question(
+ this, tr("Save as a zipped project", "message box title"),
+ message + QStringLiteral("\n\n")
+ + tr("%1 already exists. Replace it?").arg(QFileInfo(beside).fileName()),
+ QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Cancel)
+ != QMessageBox::Yes) {
+ return(QETResult());
+ }
+ } else {
+ QET::QetMessageBox::information(
+ this, tr("Save as a zipped project", "message box title"), message);
+ }
+ }
+
// write to file
QETResult result = m_project -> write();
updateWindowTitle();
diff --git a/sources/projectview.h b/sources/projectview.h
index dfbd03987..eec92dcc7 100644
--- a/sources/projectview.h
+++ b/sources/projectview.h
@@ -131,6 +131,7 @@ class ProjectView : public QWidget
QETResult save();
QETResult saveAs();
QETResult doSave();
+ QETResult saveCopyForOlderVersions();
int cleanProject();
void updateWindowTitle();
void updateTabTitle(DiagramView *);
diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp
index 8c9924606..ab25c619a 100644
--- a/sources/qetdiagrameditor.cpp
+++ b/sources/qetdiagrameditor.cpp
@@ -978,6 +978,8 @@ void QETDiagramEditor::setUpActions()
QAction *open_file = m_file_actions_group.addAction(QET::Icons::DocumentOpen, tr("&Open"));
m_save_file = m_file_actions_group.addAction(QET::Icons::DocumentSave, tr("&Save"));
m_save_file_as = m_file_actions_group.addAction(QET::Icons::DocumentSaveAs, tr("Save as"));
+ m_save_copy_for_older = m_file_actions_group.addAction(QET::Icons::DocumentSaveAs,
+ tr("Save a copy for an older QElectroTech"));
m_close_file = m_file_actions_group.addAction(QET::Icons::ProjectClose, tr("&Close"));
ShortcutManager::instance().registerAction(new_file, "diagrameditor.new_file", tr("Diagram editors"), QKeySequence::New);
@@ -991,6 +993,19 @@ void QETDiagramEditor::setUpActions()
m_close_file ->setStatusTip( tr("Closes the current project", "status bar tip") );
m_save_file ->setStatusTip( tr("Saves the current project and all its sheets", "status bar tip") );
m_save_file_as ->setStatusTip( tr("Saves the current project with a different file name", "status bar tip") );
+ m_save_copy_for_older->setStatusTip(tr("Writes a copy of the current project as a .qet file, which "
+ "versions of QElectroTech that cannot open a .qetz open",
+ "status bar tip"));
+ ShortcutManager::instance().registerAction(m_save_copy_for_older, "diagrameditor.save_copy_for_older",
+ tr("Diagram editors"), QKeySequence());
+ connect(m_save_copy_for_older, &QAction::triggered, this, [this]() {
+ if (ProjectView *project_view = currentProjectView()) {
+ const QETResult result = project_view->saveCopyForOlderVersions();
+ if (!result.isOk()) {
+ showError(result);
+ }
+ }
+ });
connect(m_save_file_as, &QAction::triggered, this, &QETDiagramEditor::saveAs);
connect(m_save_file, &QAction::triggered, this, &QETDiagramEditor::save);
@@ -2750,6 +2765,7 @@ void QETDiagramEditor::slot_updateActions()
m_close_file-> setEnabled(opened_project);
m_save_file-> setEnabled(opened_project);
m_save_file_as-> setEnabled(opened_project);
+ m_save_copy_for_older-> setEnabled(opened_project);
m_rotate_texts-> setEnabled(editable_project);
m_export_to_images-> setEnabled(opened_diagram);
m_print-> setEnabled(opened_diagram);
diff --git a/sources/qetdiagrameditor.h b/sources/qetdiagrameditor.h
index 2ff471b3f..f9287cf07 100644
--- a/sources/qetdiagrameditor.h
+++ b/sources/qetdiagrameditor.h
@@ -301,6 +301,7 @@ class QETDiagramEditor : public QETMainWindow
*m_close_file, ///< Close current project file
*m_save_file, ///< Save current project
*m_save_file_as, ///< Save current project as a specific file
+ *m_save_copy_for_older, ///< Write a .qet copy for an older QElectroTech
*m_find = nullptr,
*m_jump_to_element = nullptr; ///< Open the "jump to element" quick-open popup
diff --git a/sources/qetproject.cpp b/sources/qetproject.cpp
index 7cdc79970..92aeefc6c 100644
--- a/sources/qetproject.cpp
+++ b/sources/qetproject.cpp
@@ -192,6 +192,8 @@ QETProject::QETProject(KAutoSaveFile *backup, QObject *parent) :
}
//Set the real path, instead of the path of the backup.
setFilePath(backup->managedFile().path());
+ //A project read from a .qet is saved as a .qetz beside it
+ m_opened_from_qet = !isContainerPath(m_file_path);
delete backup;
//Set the project to read only mode if the file it.
@@ -437,8 +439,12 @@ QETProject::ProjectState QETProject::openFile(QFile *file)
//Extract the content of the xml. The raw bytes are kept : a project
//file without a persisted uuid derives its uuid from them.
const QByteArray content = file->readAll();
- //Each folio's text is kept while it is not built (readDiagramsXml())
- m_opened_content = content;
+ //Each folio's text is kept while it is not built (readDiagramsXml()):
+ //from a .qet only, a .qetz is a zip of parts
+ if (!content.startsWith("PK\x03\x04"))
+ m_opened_content = content;
+ //A project read from a .qet is saved as a .qetz beside it
+ m_opened_from_qet = !content.startsWith("PK\x03\x04");
QDomDocument xml_project;
// PreserveSpacingOnlyNodes: without it, a text node that is entirely
// whitespace -- e.g. a title-block custom variable deliberately set to
@@ -1007,6 +1013,8 @@ void QETProject::setFilePath(const QString &filepath)
}
m_next_backup_slot = 0;
m_file_path = filepath;
+ //A path chosen is saved to as it is
+ m_opened_from_qet = false;
QFileInfo fi(m_file_path);
if (fi.isWritable()) {
@@ -2472,12 +2480,48 @@ bool QETProject::writeContainer(const QDomDocument &project, const QString &path
return !entries.isEmpty() && QetZip::write(path, entries, error);
}
+/**
+ @brief QETProject::besideContainerPath
+ @return the .qetz a project read from a .qet is saved to: the same
+ name, beside it
+*/
+QString QETProject::besideContainerPath() const
+{
+ QString path = m_file_path;
+ if (path.endsWith(QStringLiteral(".qet"), Qt::CaseInsensitive)) {
+ path.chop(4);
+ }
+ return path + QStringLiteral(".qetz");
+}
+
+/**
+ @brief QETProject::writeCopyForOlderVersions
+ Write this project as a .qet at @p path, which a QElectroTech that
+ reads no .qetz opens. The project keeps its own file and state.
+*/
+QETResult QETProject::writeCopyForOlderVersions(const QString &path)
+{
+ QHash kept_folios;
+ QDomDocument xml_project(toXml(&kept_folios));
+ QString error_message;
+ if (!QET::writeXmlText(spliceKeptFolios(xml_project.toString(4), kept_folios),
+ path, &error_message))
+ return(error_message);
+ return(QETResult());
+}
+
QETResult QETProject::write()
{
// this operation requires a filepath
if (m_file_path.isEmpty())
return(QString("unable to save project to file: no filepath was specified"));
+ //A project read from a .qet is saved as a .qetz beside it; the
+ //.qet stays as it was (stage D1)
+ if (m_opened_from_qet) {
+ setFilePath(besideContainerPath());
+ }
+
// If the project was opened read-only, only refuse when the target
// really can't be written: an existing file that is not writable, or a
// new file (e.g. "Save As" to another location) whose directory is not
diff --git a/sources/qetproject.h b/sources/qetproject.h
index 97987d1a9..07d4abd51 100644
--- a/sources/qetproject.h
+++ b/sources/qetproject.h
@@ -436,6 +436,12 @@ class QETProject : public QObject
ProjectState openFile(QFile *file);
public:
static bool isContainerPath(const QString &path);
+ /// Saving writes a .qetz: one read from a .qet is saved as
+ /// a .qetz beside it, the .qet left as it is
+ /// (stage D1)
+ bool savesBesideOpenedFile() const {return m_opened_from_qet;}
+ QString besideContainerPath() const;
+ QETResult writeCopyForOlderVersions(const QString &path);
static bool writeContainer(const QDomDocument &project, const QString &path,
QString *error = nullptr);
private:
@@ -520,6 +526,8 @@ class QETProject : public QObject
/// database rows came from it, and nothing project-wide that
/// a folio's saved items depend on has changed since
bool m_kept_folios_trusted = false;
+ /// Read from a .qet whose path nobody chose since: saved beside it
+ bool m_opened_from_qet = false;
bool m_kept_folios_stale = false;
/// Something was edited since the project was opened: a table
/// filled from the database may have changed
diff --git a/tests/qttest/tst_resaveunchanged.cpp b/tests/qttest/tst_resaveunchanged.cpp
index c36ac8127..c7d7bc409 100644
--- a/tests/qttest/tst_resaveunchanged.cpp
+++ b/tests/qttest/tst_resaveunchanged.cpp
@@ -479,6 +479,40 @@ private slots:
QCOMPARE(rowsOf(edited), rowsOf(reopened));
}
+ // A project read from a .qet and saved where it is, is saved as a .qetz
+ // beside it, and the .qet is left as it was (stage D1).
+ // What the .qetz holds is what a plain save of the .qet holds.
+ void qetSavedAsQetzBeside()
+ {
+#ifndef QET_HAS_SCRIPTING
+ QSKIP("needs --run: this QElectroTech is built without Qt Qml");
+#endif
+ const QString qet = m_dir.filePath(QStringLiteral("beside.qet"));
+ QFile::remove(qet);
+ QVERIFY(QFile::copy(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"), qet));
+ const QByteArray before = read(qet);
+ const QString script = m_dir.filePath(QStringLiteral("beside.js"));
+ QFile js(script);
+ QVERIFY(js.open(QIODevice::WriteOnly));
+ js.write("qet.log('SAVED ' + qet.save(''));\n");
+ js.close();
+ const QString log = runChecked({QStringLiteral("--run"), script, qet});
+ QVERIFY2(log.contains(QStringLiteral("SAVED true")), qPrintable(log.right(400)));
+
+ QCOMPARE(read(qet), before);
+ const QString qetz = m_dir.filePath(QStringLiteral("beside.qetz"));
+ QVERIFY2(read(qetz).startsWith("PK\x03\x04"), "no .qetz beside the .qet");
+
+ auto without_save_stamps = [](QByteArray xml) {
+ static const QRegularExpression stamp(QStringLiteral(
+ "\\s*]*name=\"saved(filename|filepath|filenamedir|date|date-us|date-eu|time)\"[^>]*>[^<]*"));
+ return QString::fromUtf8(xml).remove(stamp);
+ };
+ const QString straight = resave(qet);
+ const QString back = resave(qetz);
+ QVERIFY(!straight.isEmpty() && !back.isEmpty());
+ QCOMPARE(without_save_stamps(read(back)), without_save_stamps(read(straight)));
+ }
// A title-block value that is a single space is kept through two saves
// (#973), and a value with accents comes back as it went in.