Untangle saving TTI files into new file

The saving code is an attempt to be page function and page coding
agnostic so the same code can save (G)POP, (G)DRCS and MOT pages in
the future.

loadsave will be the home of page loading, URL exporting and also for
importing and exporting of other teletext file formats.
This commit is contained in:
G.K.MacGregor
2020-11-24 19:01:25 +00:00
parent cf00cff59d
commit 4ce1b027b0
10 changed files with 230 additions and 99 deletions

View File

@@ -147,34 +147,6 @@ void TeletextDocument::loadDocument(QFile *inFile)
subPageSelected(); subPageSelected();
} }
void TeletextDocument::saveDocument(QTextStream *outStream)
{
if (!m_description.isEmpty())
*outStream << "DE," << m_description << endl;
//TODO DS and SP commands
int subPageNumber = m_subPages.size()>1;
for (auto &subPage : m_subPages) {
subPage->savePage(outStream, m_pageNumber, subPageNumber++);
if ((subPage->fastTextLinkPageNumber(0) & 0x0ff) != 0x0ff) {
*outStream << "FL,";
for (int i=0; i<6; i++) {
// Stored as page link with relative magazine number, convert to absolute page number for display
int absoluteLinkPageNumber = subPage->fastTextLinkPageNumber(i) ^ (m_pageNumber & 0x700);
// Fix magazine 0 to 8
if ((absoluteLinkPageNumber & 0x700) == 0x000)
absoluteLinkPageNumber |= 0x800;
*outStream << QString("%1").arg(absoluteLinkPageNumber, 3, 16, QChar('0'));
if (i<5)
*outStream << ',';
}
*outStream << endl;
}
}
}
void TeletextDocument::selectSubPageIndex(int newSubPageIndex, bool forceRefresh) void TeletextDocument::selectSubPageIndex(int newSubPageIndex, bool forceRefresh)
{ {
// forceRefresh overrides "beyond the last subpage" check, so inserting a subpage after the last one still shows - dangerous workaround? // forceRefresh overrides "beyond the last subpage" check, so inserting a subpage after the last one still shows - dangerous workaround?

View File

@@ -48,8 +48,8 @@ public:
// void setPacketCoding(PacketCodingEnum); // void setPacketCoding(PacketCodingEnum);
void loadDocument(QFile *); void loadDocument(QFile *);
void saveDocument(QTextStream *);
int numberOfSubPages() const { return m_subPages.size(); } int numberOfSubPages() const { return m_subPages.size(); }
LevelOnePage* subPage(int p) const { return m_subPages[p]; }
LevelOnePage* currentSubPage() const { return m_subPages[m_currentSubPageIndex]; } LevelOnePage* currentSubPage() const { return m_subPages[m_currentSubPageIndex]; }
int currentSubPageIndex() const { return m_currentSubPageIndex; } int currentSubPageIndex() const { return m_currentSubPageIndex; }
void selectSubPageIndex(int, bool=false); void selectSubPageIndex(int, bool=false);

View File

@@ -42,7 +42,7 @@ LevelOnePage::LevelOnePage(const PageBase &other)
localEnhance.reserve(208); localEnhance.reserve(208);
clearPage(); clearPage();
for (int i=PageBase::C4ErasePage; i<=PageBase::C11SerialMagazine; i++) for (int i=PageBase::C4ErasePage; i<=PageBase::C14NOS; i++)
setControlBit(i, other.controlBit(i)); setControlBit(i, other.controlBit(i));
for (int i=0; i<90; i++) for (int i=0; i<90; i++)
if (other.packetNeededArrayIndex(i)) if (other.packetNeededArrayIndex(i))
@@ -55,12 +55,12 @@ void LevelOnePage::clearPage()
for (int r=0; r<25; r++) for (int r=0; r<25; r++)
for (int c=0; c<40; c++) for (int c=0; c<40; c++)
m_level1Page[r][c] = 0x20; m_level1Page[r][c] = 0x20;
for (int i=0; i<8; i++) { for (int i=C4ErasePage; i<=C14NOS; i++)
setControlBit(i, false); setControlBit(i, false);
for (int i=0; i<8; i++)
m_composeLink[i] = { (i<4) ? i : 0, false, i>=4, 0x0ff, 0x0000 }; m_composeLink[i] = { (i<4) ? i : 0, false, i>=4, 0x0ff, 0x0000 };
}
for (int i=0; i<6; i++) for (int i=0; i<6; i++)
m_fastTextLink[i] = { 0x0ff, 0x37f7 }; m_fastTextLink[i] = { 0x0ff, 0x3f7f };
/* m_subPageNumber = 0x0000; */ /* m_subPageNumber = 0x0000; */
m_cycleValue = 8; m_cycleValue = 8;
@@ -256,7 +256,8 @@ bool LevelOnePage::setPacket(int packetNumber, int designationCode, QByteArray p
int CLUToffset = (designationCode == 0) ? 16 : 0; int CLUToffset = (designationCode == 0) ? 16 : 0;
m_defaultCharSet = ((packetContents.at(2) >> 4) & 0x3) | ((packetContents.at(3) << 2) & 0xc); m_defaultCharSet = ((packetContents.at(2) >> 4) & 0x3) | ((packetContents.at(3) << 2) & 0xc);
m_defaultNOS = (packetContents.at(2) >> 1) & 0x7; // Don't set m_defaultNOS directly as we need to keep control bits in subclass in sync
setDefaultNOS((packetContents.at(2) >> 1) & 0x7);
m_secondCharSet = ((packetContents.at(3) >> 5) & 0x1) | ((packetContents.at(4) << 1) & 0xe); m_secondCharSet = ((packetContents.at(3) >> 5) & 0x1) | ((packetContents.at(4) << 1) & 0xe);
m_secondNOS = (packetContents.at(3) >> 2) & 0x7; m_secondNOS = (packetContents.at(3) >> 2) & 0x7;
@@ -292,18 +293,13 @@ bool LevelOnePage::packetNeeded(int packetNumber, int designationCode) const
if (packetNumber == 26) if (packetNumber == 26)
return ((localEnhance.size()+12) / 13) > designationCode; return ((localEnhance.size()+12) / 13) > designationCode;
// FIXME don't save this raw packet yet as TeletextDocument::savePage currently uses fastTextLinkPageNumber if (packetNumber == 27 && designationCode == 0) {
// to put the FL commands into the .tti file
// When we separate out loading and saving into its own cpp file, that will then become responsible for
// converting this packet into an FL command itself
/* if (packetNumber == 27 && designationCode == 0) {
for (int i=0; i<6; i++) for (int i=0; i<6; i++)
if ((m_fastTextLink[i].pageNumber & 0x0ff) != 0xff) if ((m_fastTextLink[i].pageNumber & 0x0ff) != 0xff)
return true; return true;
return false; return false;
}*/ }
if (packetNumber == 27 && (designationCode == 4 || designationCode == 5)) { if (packetNumber == 27 && (designationCode == 4 || designationCode == 5)) {
for (int i=0; i<(designationCode == 4 ? 6 : 2); i++) { for (int i=0; i<(designationCode == 4 ? 6 : 2); i++) {
@@ -363,7 +359,7 @@ void LevelOnePage::loadPagePacket(QByteArray &inLine)
setPacket(lineNumber, inLine); setPacket(lineNumber, inLine);
} else { } else {
int designationCode = inLine.at(0) & 0x3f; int designationCode = inLine.at(0) & 0x3f;
if (inLine.size() < 40) if (inLine.size() < 40) {
// OL is too short! // OL is too short!
if (lineNumber == 26) { if (lineNumber == 26) {
// For a too-short enhancement triplets OL, first trim the line down to nearest whole triplet // For a too-short enhancement triplets OL, first trim the line down to nearest whole triplet
@@ -374,6 +370,7 @@ void LevelOnePage::loadPagePacket(QByteArray &inLine)
} else } else
// For other triplet OLs and Hamming 8/4 OLs, just pad with zero data // For other triplet OLs and Hamming 8/4 OLs, just pad with zero data
inLine.leftJustified(40, '@'); inLine.leftJustified(40, '@');
}
for (int i=1; i<=39; i++) for (int i=1; i<=39; i++)
inLine[i] = inLine.at(i) & 0x3f; inLine[i] = inLine.at(i) & 0x3f;
setPacket(lineNumber, designationCode, inLine); setPacket(lineNumber, designationCode, inLine);
@@ -381,48 +378,6 @@ void LevelOnePage::loadPagePacket(QByteArray &inLine)
} }
} }
void LevelOnePage::savePage(QTextStream *outStream, int pageNumber, int subPageNumber)
{
auto writePacketsWithDesignationCodes=[&](int packetNumber)
{
for (int i=0; i<=16; i++)
if (packetNeeded(packetNumber, i)) {
QByteArray outLine = packet(packetNumber, i);
*outStream << QString("OL,%1,").arg(packetNumber);
outLine[0] = i | 0x40;
for (int c=1; c<outLine.size(); c++)
outLine[c] = outLine.at(c) | 0x40;
*outStream << outLine << endl;
}
};
*outStream << QString("PN,%1%2").arg(pageNumber, 3, 16, QChar('0')).arg(subPageNumber & 0xff, 2, 16, QChar('0')) << endl;
*outStream << QString("SC,%1").arg(subPageNumber, 4, 16, QChar('0')) << endl;
*outStream << QString("PS,%1").arg(0x8000 | controlBitsToPS(), 4, 16, QChar('0')) << endl;
*outStream << QString("CT,%1,%2").arg(m_cycleValue).arg(m_cycleType==CTcycles ? 'C' : 'T') << endl;
// TODO RE and maybe FLOF?
// BUG FL commands may clash with X/27/0 packets that specify links manually (e.g. with subcodes)
writePacketsWithDesignationCodes(27);
writePacketsWithDesignationCodes(28);
writePacketsWithDesignationCodes(26);
for (int r=1; r<25; r++)
if (packetNeeded(r)) {
QByteArray outLine = packet(r);
*outStream << QString("OL,%1,").arg(r);
for (int c=0; c<outLine.size(); c++)
if (outLine.at(c) < 0x20) {
outLine[c] = outLine.at(c) | 0x40;
outLine.insert(c, 0x1b);
c++;
}
*outStream << outLine << endl;
}
}
int LevelOnePage::controlBitsToPS() const int LevelOnePage::controlBitsToPS() const
{ {
//TODO map page language for regions other than 0 //TODO map page language for regions other than 0
@@ -492,7 +447,14 @@ QString LevelOnePage::exportURLHash(QString pageHash)
void LevelOnePage::setCycleValue(int newValue) { m_cycleValue = newValue; }; void LevelOnePage::setCycleValue(int newValue) { m_cycleValue = newValue; };
void LevelOnePage::setCycleType(CycleTypeEnum newType) { m_cycleType = newType; } void LevelOnePage::setCycleType(CycleTypeEnum newType) { m_cycleType = newType; }
void LevelOnePage::setDefaultCharSet(int newDefaultCharSet) { m_defaultCharSet = newDefaultCharSet; } void LevelOnePage::setDefaultCharSet(int newDefaultCharSet) { m_defaultCharSet = newDefaultCharSet; }
void LevelOnePage::setDefaultNOS(int newDefaultNOS) { m_defaultNOS = newDefaultNOS; }
void LevelOnePage::setDefaultNOS(int defaultNOS)
{
m_defaultNOS = defaultNOS;
setControlBit(C12NOS, defaultNOS & 1);
setControlBit(C13NOS, defaultNOS & 2);
setControlBit(C14NOS, defaultNOS & 4);
}
void LevelOnePage::setSecondCharSet(int newSecondCharSet) void LevelOnePage::setSecondCharSet(int newSecondCharSet)
{ {

View File

@@ -50,7 +50,6 @@ public:
void clearPage(); void clearPage();
void loadPagePacket(QByteArray &); void loadPagePacket(QByteArray &);
void savePage(QTextStream *, int, int);
QString exportURLHash(QString); QString exportURLHash(QString);
/* void setSubPageNumber(int); */ /* void setSubPageNumber(int); */

158
loadsave.cpp Normal file
View File

@@ -0,0 +1,158 @@
/*
* Copyright (C) 2020 Gavin MacGregor
*
* This file is part of QTeletextMaker.
*
* QTeletextMaker 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 3 of the License, or
* (at your option) any later version.
*
* QTeletextMaker 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 QTeletextMaker. If not, see <https://www.gnu.org/licenses/>.
*/
#include "loadsave.h"
#include <QSaveFile>
#include "document.h"
#include "levelonepage.h"
#include "pagebase.h"
// Used by TTI and hashstring
int controlBitsToPS(PageBase *subPage)
{
// C4 Erase page is stored in bit 14
int pageStatus = 0x8000 | (subPage->controlBit(PageBase::C4ErasePage) << 14);
// C5 to C11 stored in order from bits 1 to 6
for (int i=PageBase::C5Newsflash; i<=PageBase::C11SerialMagazine; i++)
pageStatus |= subPage->controlBit(i) << (i-1);
// Apparently the TTI format stores the NOS bits backwards
pageStatus |= subPage->controlBit(PageBase::C12NOS) << 9;
pageStatus |= subPage->controlBit(PageBase::C13NOS) << 8;
pageStatus |= subPage->controlBit(PageBase::C14NOS) << 7;
return pageStatus;
}
void saveTTI(QSaveFile &file, const TeletextDocument &document)
{
int p;
QTextStream outStream(&file);
auto write7bitPacket=[&](int packetNumber)
{
if (document.subPage(p)->packetNeeded(packetNumber)) {
QByteArray outLine = document.subPage(p)->packet(packetNumber);
outStream << QString("OL,%1,").arg(packetNumber);
for (int c=0; c<outLine.size(); c++)
if (outLine.at(c) < 0x20) {
// TTI files are plain text, so put in escape followed by control code with bit 6 set
outLine[c] = outLine.at(c) | 0x40;
outLine.insert(c, 0x1b);
c++;
}
outStream << outLine << Qt::endl;
}
};
auto writeHammingPacket=[&](int packetNumber, int designationCode=0)
{
if (document.subPage(p)->packetNeeded(packetNumber, designationCode)) {
QByteArray outLine = document.subPage(p)->packet(packetNumber, designationCode);
outStream << QString("OL,%1,").arg(packetNumber);
// TTI stores raw values with bit 7 set, doesn't do Hamming encoding
outLine[0] = designationCode | 0x40;
for (int c=1; c<outLine.size(); c++)
outLine[c] = outLine.at(c) | 0x40;
outStream << outLine << Qt::endl;
}
};
outStream.setCodec("ISO-8859-1");
if (!document.description().isEmpty())
outStream << "DE," << document.description() << Qt::endl;
// TODO DS and SP commands
// If there's just one subpage then we save it with a subcode of 0000
// otherwise start with a subcode of 0001
int subPageNumber = document.numberOfSubPages() > 1;
for (p=0; p<document.numberOfSubPages(); p++) {
outStream << QString("PN,%1%2").arg(document.pageNumber(), 3, 16, QChar('0')).arg(subPageNumber & 0xff, 2, 16, QChar('0')) << Qt::endl;
// Magazine Organisation Table and Magazine Inventory Page don't have subpages
if (document.pageFunction() != TeletextDocument::PFMOT && document.pageFunction() != TeletextDocument::PFMIP)
outStream << QString("SC,%1").arg(subPageNumber, 4, 16, QChar('0')) << Qt::endl;
outStream << QString("PS,%1").arg(0x8000 | controlBitsToPS(document.subPage(p)), 4, 16, QChar('0')) << Qt::endl;
if (document.pageFunction() == TeletextDocument::PFLevelOnePage)
// Assume that only Level One Pages have configurable cycle times
outStream << QString("CT,%1,%2").arg(document.subPage(p)->cycleValue()).arg(document.subPage(p)->cycleType()==LevelOnePage::CTcycles ? 'C' : 'T') << Qt::endl;
else
// X/28/0 specifies page function and coding but the PF command
// should make it obvious to a human that this isn't a Level One Page
outStream << QString("PF,%1,%2").arg(document.pageFunction()).arg(document.packetCoding()) << Qt::endl;
bool writeFLCommand = false;
if (document.pageFunction() == TeletextDocument::PFLevelOnePage && document.subPage(p)->packetNeeded(27,0)) {
// Subpage has FastText links - if any link to a specific subpage, we need to write X/27/0 as raw
// otherwise we write the links as a human-readable FL command later on
writeFLCommand = true;
// TODO uncomment this when we can edit FastText subpage links
/*for (int i=0; i<6; i++)
if (document.subPage(p)->fastTextLinkSubPageNumber(i) != 0x3f7f) {
writeFLCommand = false;
break;
}*/
}
// X27 then X28 always come first
for (int i=(writeFLCommand ? 1 : 0); i<16; i++)
writeHammingPacket(27, i);
for (int i=0; i<16; i++)
writeHammingPacket(28, i);
if (document.packetCoding() == TeletextDocument::Coding7bit) {
// For 7 bit coding i.e. Level One Pages, X/26 are written before X/1 to X/25
for (int i=0; i<16; i++)
writeHammingPacket(26, i);
for (int i=1; i<=24; i++)
write7bitPacket(i);
} else {
// For others (especially (G)POP pages) X/1 to X/25 are written before X/26
for (int i=1; i<=25; i++)
writeHammingPacket(i);
for (int i=0; i<16; i++)
writeHammingPacket(26, i);
}
if (writeFLCommand) {
outStream << "FL,";
for (int i=0; i<6; i++) {
// Stored as page link with relative magazine number, convert to absolute page number for display
int absoluteLinkPageNumber = document.subPage(p)->fastTextLinkPageNumber(i) ^ (document.pageNumber() & 0x700);
// Fix magazine 0 to 8
if ((absoluteLinkPageNumber & 0x700) == 0x000)
absoluteLinkPageNumber |= 0x800;
outStream << QString("%1").arg(absoluteLinkPageNumber, 3, 16, QChar('0'));
if (i<5)
outStream << ',';
}
outStream << Qt::endl;
}
subPageNumber++;
}
}

31
loadsave.h Normal file
View File

@@ -0,0 +1,31 @@
/*
* Copyright (C) 2020 Gavin MacGregor
*
* This file is part of QTeletextMaker.
*
* QTeletextMaker 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 3 of the License, or
* (at your option) any later version.
*
* QTeletextMaker 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 QTeletextMaker. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef LOADSAVE_H
#define LOADSAVE_H
#include <QSaveFile>
#include "document.h"
#include "pagebase.h"
int controlBitsToPS(PageBase *);
void saveTTI(QSaveFile &, const TeletextDocument &);
#endif

View File

@@ -24,6 +24,7 @@
#include <QMenuBar> #include <QMenuBar>
#include <QMessageBox> #include <QMessageBox>
#include <QRadioButton> #include <QRadioButton>
#include <QSaveFile>
#include <QScreen> #include <QScreen>
#include <QSettings> #include <QSettings>
#include <QStatusBar> #include <QStatusBar>
@@ -33,6 +34,7 @@
#include "mainwindow.h" #include "mainwindow.h"
#include "levelonecommands.h" #include "levelonecommands.h"
#include "loadsave.h"
#include "mainwidget.h" #include "mainwidget.h"
#include "pageenhancementsdockwidget.h" #include "pageenhancementsdockwidget.h"
#include "pageoptionsdockwidget.h" #include "pageoptionsdockwidget.h"
@@ -762,18 +764,23 @@ void MainWindow::openRecentFile()
bool MainWindow::saveFile(const QString &fileName) bool MainWindow::saveFile(const QString &fileName)
{ {
QFile file(fileName); QString errorMessage;
if (!file.open(QFile::WriteOnly | QFile::Text)) {
QMessageBox::warning(this, tr("QTeletextMaker"), tr("Cannot write file %1:\n%2.").arg(QDir::toNativeSeparators(fileName), file.errorString())); QApplication::setOverrideCursor(Qt::WaitCursor);
QSaveFile file(fileName);
if (file.open(QFile::WriteOnly | QFile::Text)) {
saveTTI(file, *m_textWidget->document());
if (!file.commit())
errorMessage = tr("Cannot write file %1:\n%2.") .arg(QDir::toNativeSeparators(fileName), file.errorString());
} else
errorMessage = tr("Cannot open file %1 for writing:\n%2.").arg(QDir::toNativeSeparators(fileName), file.errorString());
QApplication::restoreOverrideCursor();
if (!errorMessage.isEmpty()) {
QMessageBox::warning(this, tr("QTeletextMaker"), errorMessage);
return false; return false;
} }
QTextStream out(&file);
out.setCodec("ISO-8859-1");
QApplication::setOverrideCursor(Qt::WaitCursor);
m_textWidget->document()->saveDocument(&out);
QApplication::restoreOverrideCursor();
setCurrentFile(fileName); setCurrentFile(fileName);
statusBar()->showMessage(tr("File saved"), 2000); statusBar()->showMessage(tr("File saved"), 2000);
return true; return true;

View File

@@ -26,13 +26,13 @@ PageBase::PageBase()
// We use nullptrs to keep track of allocated packets, so initialise them this way // We use nullptrs to keep track of allocated packets, so initialise them this way
for (int i=0; i<90; i++) for (int i=0; i<90; i++)
m_packets[i] = nullptr; m_packets[i] = nullptr;
for (int i=PageBase::C4ErasePage; i<=PageBase::C11SerialMagazine; i++) for (int i=PageBase::C4ErasePage; i<=PageBase::C14NOS; i++)
m_controlBits[i] = false; m_controlBits[i] = false;
} }
PageBase::PageBase(const PageBase &other) PageBase::PageBase(const PageBase &other)
{ {
for (int i=PageBase::C4ErasePage; i<=PageBase::C11SerialMagazine; i++) for (int i=PageBase::C4ErasePage; i<=PageBase::C14NOS; i++)
setControlBit(i, other.controlBit(i)); setControlBit(i, other.controlBit(i));
for (int i=0; i<90; i++) for (int i=0; i<90; i++)
if (other.packetNeededArrayIndex(i)) if (other.packetNeededArrayIndex(i))

View File

@@ -28,7 +28,7 @@ class PageBase //: public QObject
//Q_OBJECT //Q_OBJECT
public: public:
enum ControlBitsEnum { C4ErasePage, C5Newsflash, C6Subtitle, C7SuppressHeader, C8Update, C9InterruptedSequence, C10InhibitDisplay, C11SerialMagazine }; enum ControlBitsEnum { C4ErasePage, C5Newsflash, C6Subtitle, C7SuppressHeader, C8Update, C9InterruptedSequence, C10InhibitDisplay, C11SerialMagazine, C12NOS, C13NOS, C14NOS };
PageBase(); PageBase();
PageBase(const PageBase &); PageBase(const PageBase &);
@@ -49,7 +49,7 @@ public:
bool setControlBit(int, bool); bool setControlBit(int, bool);
private: private:
bool m_controlBits[8]; bool m_controlBits[11];
QByteArray *m_packets[90]; // X/0 to X/25, plus 16 packets for X/26, another 16 for X/27, for X28 and for X/29 QByteArray *m_packets[90]; // X/0 to X/25, plus 16 packets for X/26, another 16 for X/27, for X28 and for X/29
}; };

View File

@@ -4,6 +4,7 @@ requires(qtConfig(filedialog))
HEADERS = document.h \ HEADERS = document.h \
levelonecommands.h \ levelonecommands.h \
levelonepage.h \ levelonepage.h \
loadsave.h \
mainwidget.h \ mainwidget.h \
mainwindow.h \ mainwindow.h \
pagebase.h \ pagebase.h \
@@ -17,6 +18,7 @@ HEADERS = document.h \
SOURCES = document.cpp \ SOURCES = document.cpp \
levelonecommands.cpp \ levelonecommands.cpp \
levelonepage.cpp \ levelonepage.cpp \
loadsave.cpp \
main.cpp \ main.cpp \
mainwidget.cpp \ mainwidget.cpp \
mainwindow.cpp \ mainwindow.cpp \