Open and save projects as .qetz (zipped, opt-in)

Third stage of the zipped project file (#1440). A project is saved as a
.qetz when its file name ends in .qetz -- Save As offers it as a second
file type, and --resave, --set-titleblock, the script API's save() and
the live-mode snapshot follow the name too. Saving a .qet project still
writes a .qet; nothing changes unless the user picks .qetz.

Opening recognises a .qetz by its content (a zip), not its name, and
reads it as bytes: text mode would alter it on Windows. The project
document is joined back and read by the same code as a .qet. Crash
backups stay plain XML. Drag and drop, the open dialog and the
numbering import accept .qetz; the database export names its file from
the project's name without assuming ".qet".

Parts are now written without added indentation (one line each): the
symbol definitions are kept as read, whitespace included, and parts
that gained indentation made the next save differ (five forum projects).

Tests: tst_resaveunchanged::containerSavesTheSameProject (four examples
saved as .qetz, then as .qet, equal the straight .qet; pictures checked
to fail without the fix), tst_qetcontainer::noWhitespaceAdded. Outside
ctest: 341 of 341 projects QET can open (examples, bugtracker, GitHub,
forum) give the same .qet through a .qetz; 181.6 MB of .qet became
36.9 MB of .qetz.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Wga9B92BNurDEtxxtCDxf
This commit is contained in:
ispyisail
2026-10-10 23:11:24 +13:00
parent a475383915
commit fac09253a0
14 changed files with 215 additions and 44 deletions
+50 -10
View File
@@ -17,6 +17,7 @@
*/
#include "qetproject.h"
#include "container/qetcontainer.h"
#include "ElementsCollection/xmlelementcollection.h"
#include "autoNum/assignvariables.h"
@@ -385,10 +386,20 @@ QETProject::ProjectState QETProject::openFile(QFile *file)
load_timer.start();
bool opened_here = file->isOpen() ? false : true;
if (!file->isOpen()
&& !file->open(QIODevice::ReadOnly
| QIODevice::Text)) {
return FileOpenFailed;
if (opened_here)
{
//A .qetz is a zip and is read as bytes: text mode would turn
//its CR LF pairs into LF on Windows. A .qet is read as text,
//as it always was.
if (!file->open(QIODevice::ReadOnly)) {
return FileOpenFailed;
}
if (!QetContainer::isZip(file->peek(4))) {
file->close();
if (!file->open(QIODevice::ReadOnly | QIODevice::Text)) {
return FileOpenFailed;
}
}
}
QFileInfo fi(*file);
setFilePath(fi.absoluteFilePath());
@@ -410,12 +421,12 @@ QETProject::ProjectState QETProject::openFile(QFile *file)
// call .text() on themselves -- which is exactly where the bug was.
// The option exists since Qt 6.5; older Qt always drops such nodes,
// so there an all-whitespace value still reloads as "".
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
if (!xml_project.setContent(content, QDomDocument::ParseOption::PreserveSpacingOnlyNodes))
#else
if (!xml_project.setContent(content))
#endif
// QetContainer::parse() does exactly that, for a .qet and for each part
// of a .qetz alike.
QString read_error;
if (!QetContainer::readDocument(content, &xml_project, &read_error))
{
qWarning().noquote() << "Unable to read" << fi.fileName() << ":" << read_error;
if(opened_here) {
file->close();
}
@@ -1617,6 +1628,30 @@ bool QETProject::close()
@see setFilePath()
@return true if the project was successfully saved, else false
*/
/**
@brief QETProject::isContainerPath
@return true if @p path names a .qetz file: write() saves it zipped
*/
bool QETProject::isContainerPath(const QString &path)
{
return path.endsWith(QLatin1String(".qetz"), Qt::CaseInsensitive);
}
/**
@brief QETProject::writeContainer
Write the project document @p project to @p path as a .qetz: the same
document, in parts, zipped (#1440). The file is replaced only once
the whole archive is written.
*/
bool QETProject::writeContainer(const QDomDocument &project, const QString &path, QString *error)
{
const QList<QetZip::Entry> entries = QetContainer::split(
project,
QStringLiteral("QElectroTech %1").arg(QetVersion::currentVersion().toString()),
error);
return !entries.isEmpty() && QetZip::write(path, entries, error);
}
QETResult QETProject::write()
{
// this operation requires a filepath
@@ -1639,7 +1674,12 @@ QETResult QETProject::write()
QDomDocument xml_project(toXml());
QString error_message;
if (!QET::writeXmlFile(xml_project, m_file_path, &error_message))
if (isContainerPath(m_file_path))
{
if (!writeContainer(xml_project, m_file_path, &error_message))
return(error_message);
}
else if (!QET::writeXmlFile(xml_project, m_file_path, &error_message))
return(error_message);
// The project has just been written to a writable file (e.g. saved to