9 Commits

Author SHA1 Message Date
G.K.MacGregor
06e0b401ca Tag version 0.5.5-alpha 2022-10-23 15:24:43 +01:00
G.K.MacGregor
bc8780608c Force refresh when switching decode Levels
This should fix palettes not updating when switching in and out of
Level 3.5
2022-10-23 12:43:27 +01:00
G.K.MacGregor
536c231941 Fix detect-on-load of Level 3.5 objects and DRCS mode 2022-10-23 12:30:50 +01:00
G.K.MacGregor
4faed597c0 Disable triplet widgets when no triplet is selected
This fixes a crash that occured when the last triplet in the X/26 list
is deleted.
2022-10-04 21:22:02 +01:00
G.K.MacGregor
75816e7750 Fix reserved data detection in DRCS character triplet 2022-09-22 21:28:29 +01:00
G.K.MacGregor
8b655afb2d Highlight reserved mode and data in X/26 triplet list 2022-08-30 21:07:14 +01:00
G.K.MacGregor
abf649d2ab Add reload from disk
This is typically mapped to the F5 key, so the "secret force refresh"
key has been temporarily moved to F6.
2022-07-17 15:16:53 +01:00
G.K.MacGregor
a8f2152c92 Try fixing the "unknown keypress types a block character" 2022-06-20 18:30:53 +01:00
G.K.MacGregor
9d05126e8f Fix non-flashing when page is force-refreshed 2022-06-18 17:55:19 +01:00
15 changed files with 206 additions and 34 deletions

View File

@@ -14,13 +14,15 @@ Features
- Configurable zoom.
- View teletext pages in 4:3, 16:9 pillar-box and 16:9 stretch aspect ratios.
Although designed on and developed for Linux, the Qt 5 libraries are cross platform so a Windows executable can be built. A Windows executable can be found within the "Releases" link, compiled on a Linux host using [MXE](https://github.com/mxe/mxe) based on [these instructions](https://blog.8bitbuddhism.com/2018/08/22/cross-compiling-windows-applications-with-mxe/). After MXE is installed `make qtbase` should be enough to build QTeletextMaker.
Although designed on and developed for Linux, the Qt 5 libraries are cross platform so a Windows executable can be built. A Windows executable can be found within the "Releases" link, compiled on a Linux host using [MXE](https://github.com/mxe/mxe) based on [these instructions](https://blog.8bitbuddhism.com/2018/08/22/cross-compiling-windows-applications-with-mxe/). After MXE is installed `make qtbase` should build and install the required dependencies to build QTeletextMaker.
## Building
### Linux
Install the QtCore, QtGui and QtWidgets libraries and build headers, along with the qmake tool. Then type `qmake5 && make -j3` in a terminal, you can replace -j3 with the number of processor cores used for the compile process.
Install the QtCore, QtGui and QtWidgets libraries and build headers, along with the qmake tool. Depending on how qmake is installed, type `qmake && make -j3` or `qmake5 && make -j3` in a terminal to build, you can replace -j3 with the number of processor cores used for the compile process.
The above will place the qteletextmaker executable in the same directory as the source, type `./qteletextmaker` in the terminal to launch. Optionally, type `make install` afterwards to place the executable into /usr/local/bin.
The above should place the qteletextmaker executable in the same directory as the source, type `./qteletextmaker` in the terminal to launch. Some Qt installs may place the executable into a "release" directory.
Optionally, type `make install` afterwards to place the executable into /usr/local/bin.
## Current limitations
The following X/26 enhancement triplets are not rendered by the editor, although the list is fully aware of them.

View File

@@ -68,7 +68,13 @@ void TeletextPageDecode::setLevel(int level)
{
if (level == m_level)
return;
m_level = level;
for (int r=0; r<25; r++)
for (int c=0; c<72; c++)
m_refresh[r][c] = true;
decodePage();
}

View File

@@ -94,6 +94,25 @@ bool TeletextDocument::isEmpty() const
return true;
}
void TeletextDocument::clear()
{
LevelOnePage *blankSubPage = new LevelOnePage;
m_subPages.insert(m_subPages.begin(), blankSubPage);
emit aboutToChangeSubPage();
m_currentSubPageIndex = 0;
m_clutModel->setSubPage(m_subPages[0]);
emit subPageSelected();
cancelSelection();
m_undoStack->clear();
for (int i=m_subPages.size()-1; i>0; i--) {
delete(m_subPages[i]);
m_subPages.erase(m_subPages.begin()+i);
}
}
/*
void TeletextDocument::setPageFunction(PageFunctionEnum newPageFunction)
{

View File

@@ -56,6 +56,7 @@ public:
~TeletextDocument();
bool isEmpty() const;
void clear();
PageFunctionEnum pageFunction() const { return m_pageFunction; }
// void setPageFunction(PageFunctionEnum);

View File

@@ -25,6 +25,8 @@
#include "levelonepage.h"
#include "x26triplets.h"
LevelOnePage::LevelOnePage()
{
m_enhancements.reserve(maxEnhancements());
@@ -403,17 +405,21 @@ bool LevelOnePage::isPaletteDefault(int fromColour, int toColour) const
int LevelOnePage::levelRequired() const
{
// X/28/4 present i.e. CLUTs 0 or 1 redefined - Level 3.5
if (!isPaletteDefault(0, 15))
return 3;
// TODO Check for X/28/1 for DCLUT for mode 1-3 DRCS characters - return 3
// Assume Level 2.5 if any X/28 page enhancements are present, otherwise assume Level 1
int levelSeen = (!isPaletteDefault(16,31) || m_leftSidePanelDisplayed || m_rightSidePanelDisplayed || m_defaultScreenColour !=0 || m_defaultRowColour !=0 || m_blackBackgroundSubst || m_colourTableRemap !=0 || m_defaultCharSet != 0 || m_secondCharSet != 0xf) ? 2 : 0;
// If there's no X/26 triplets, exit here as Level 1 or 2.5
if (m_enhancements.isEmpty())
return levelSeen;
for (int i=0; i<m_enhancements.size(); i++) {
// Font style - Level 3.5 only triplet
if (m_enhancements.at(i).modeExt() == 0x2e) // Font style
return 3;
@@ -426,9 +432,10 @@ int LevelOnePage::levelRequired() const
case 0x22: // G3 character @ Level 1.5
case 0x2f: // G2 character
case 0x30 ... 0x3f: // G0 character with diacritical
levelSeen = qMax(levelSeen, 1);
levelSeen = 1;
break;
}
if (levelSeen < 2)
switch (m_enhancements.at(i).modeExt()) {
// Check for Level 2.5 triplets
@@ -436,25 +443,26 @@ int LevelOnePage::levelRequired() const
case 0x01: // Full row colour
case 0x10 ... 0x13: // Origin Modifer and Object Invocation
case 0x15 ... 0x17: // Object Definition
// Check if Object Definition is required only at Level 3.5
if ((m_enhancements.at(i).address() & 0x18) == 0x10)
return 3;
else
levelSeen = qMax(levelSeen, 2);
break;
case 0x18: // DRCS Mode
// Check if DRCS is required only at Level 3.5
if ((m_enhancements.at(i).data() & 0x30) == 0x20)
return 3;
else
levelSeen = qMax(levelSeen, 2);
break;
case 0x20: // Foreground colour
case 0x21: // G1 character
case 0x23: // Background colour
case 0x27 ... 0x29: // Flash functions, G0 and G2 charset designation, G0 character @ Level 2.5
case 0x2b ... 0x2d: // G3 character @ Level 2.5, display attributes, DRCS character
levelSeen = qMax(levelSeen, 2);
levelSeen = 2;
break;
}
if (levelSeen == 2)
switch (m_enhancements.at(i).modeExt()) {
// Check for triplets with "required at Level 3.5 only" parameters
case 0x15 ... 0x17: // Object Definition
if ((m_enhancements.at(i).address() & 0x18) == 0x10)
return 3;
break;
case 0x18: // DRCS Mode
if ((m_enhancements.at(i).data() & 0x30) == 0x20)
return 3;
break;
}
}

View File

@@ -30,7 +30,7 @@ int main(int argc, char *argv[])
QApplication::setApplicationDisplayName(QApplication::applicationName());
QApplication::setOrganizationName("gkmac.co.uk");
QApplication::setOrganizationDomain("gkmac.co.uk");
QApplication::setApplicationVersion("0.5.4-alpha");
QApplication::setApplicationVersion("0.5.5-alpha");
QCommandLineParser parser;
parser.setApplicationDescription(QApplication::applicationName());
parser.addHelpOption();

View File

@@ -250,8 +250,8 @@ void TeletextWidget::keyPressEvent(QKeyEvent *event)
if (event->key() < 0x01000000) {
// A character-typing key was pressed
// Try to keymap it, if not keymapped then plain ASCII code (may be) returned
char mappedKeyPress = keymapping[m_pageDecode.level1CharSet(m_teletextDocument->cursorRow(), m_teletextDocument->cursorColumn())].value(event->text().at(0), *qPrintable(event->text()));
if (mappedKeyPress < 0x20)
char mappedKeyPress = keymapping[m_pageDecode.level1CharSet(m_teletextDocument->cursorRow(), m_teletextDocument->cursorColumn())].value(event->text().at(0), *qPrintable(event->text().at(0)));
if (mappedKeyPress >= 0x00 && mappedKeyPress <= 0x1f)
return;
// If outside ASCII map then the character can't be represented by current Level 1 character set
// Map it to block character so it doesn't need to be inserted-between later on
@@ -386,7 +386,7 @@ void TeletextWidget::keyPressEvent(QKeyEvent *event)
case Qt::Key_PageDown:
m_teletextDocument->selectSubPagePrevious();
break;
case Qt::Key_F5:
case Qt::Key_F6:
m_pageDecode.decodePage();
m_pageRender.renderPage(true);
update();

View File

@@ -149,6 +149,34 @@ bool MainWindow::saveAs()
return saveFile(fileName);
}
void MainWindow::reload()
{
if (m_isUntitled)
return;
if (!m_textWidget->document()->undoStack()->isClean()) {
const QMessageBox::StandardButton ret = QMessageBox::warning(this, QApplication::applicationDisplayName(), tr("The document \"%1\" has been modified.\nDo you want to discard your changes?").arg(QFileInfo(m_curFile).fileName()), QMessageBox::Discard | QMessageBox::Cancel);
if (ret != QMessageBox::Discard)
return;
} else {
const QMessageBox::StandardButton ret = QMessageBox::warning(this, QApplication::applicationDisplayName(), tr("Do you want to reload the document \"%1\" from disk?").arg(QFileInfo(m_curFile).fileName()), QMessageBox::Yes | QMessageBox::No);
if (ret != QMessageBox::Yes)
return;
}
int subPageIndex = m_textWidget->document()->currentSubPageIndex();
m_textWidget->document()->clear();
loadFile(m_curFile);
if (subPageIndex >= m_textWidget->document()->numberOfSubPages())
subPageIndex = m_textWidget->document()->numberOfSubPages()-1;
m_textWidget->document()->selectSubPageIndex(subPageIndex, true);
}
void MainWindow::exportPNG()
{
QString exportFileName = QFileDialog::getSaveFileName(this, tr("Export PNG"), QString(), "PNG image (*.png)");
@@ -328,6 +356,11 @@ void MainWindow::createActions()
saveAsAct->setShortcuts(QKeySequence::SaveAs);
saveAsAct->setStatusTip(tr("Save the document under a new name"));
const QIcon reloadIcon = QIcon::fromTheme("document-revert");
QAction *reloadAct = fileMenu->addAction(reloadIcon, tr("Reload"), this, &MainWindow::reload);
reloadAct->setShortcuts(QKeySequence::Refresh);
reloadAct->setStatusTip(tr("Reload the document from disk"));
fileMenu->addSeparator();
QMenu *recentMenu = fileMenu->addMenu(tr("Recent"));

View File

@@ -59,6 +59,7 @@ private slots:
void open();
bool save();
bool saveAs();
void reload();
void exportT42();
void exportZXNet();
void exportEditTF();

View File

@@ -354,7 +354,9 @@ void TeletextPageRender::renderPage(bool force)
}
}
if (!force || m_decoder->cellFlashMode(r, c) == 0)
if (force && m_decoder->cellFlashMode(r, c) != 0)
m_decoder->setRefresh(r, c, true);
else
m_decoder->setRefresh(r, c, false);
}
}

View File

@@ -623,6 +623,8 @@ X26DockWidget::X26DockWidget(TeletextWidget *parent): QDockWidget(parent)
x26Layout->addLayout(insertDeleteLayout);
disableTripletWidgets();
x26Widget->setLayout(x26Layout);
this->setWidget(x26Widget);
@@ -674,14 +676,48 @@ void X26DockWidget::rowSelected(const QModelIndex &current, const QModelIndex &p
{
Q_UNUSED(previous);
updateAllRawTripletSpinBoxes(current);
updateAllCookedTripletWidgets(current);
if (current.isValid()) {
updateAllRawTripletSpinBoxes(current);
updateAllCookedTripletWidgets(current);
} else
disableTripletWidgets();
}
void X26DockWidget::disableTripletWidgets()
{
m_rawTripletAddressSpinBox->setEnabled(false);
m_rawTripletDataSpinBox->setEnabled(false);
m_rawTripletModeSpinBox->setEnabled(false);
m_rawTripletAddressSpinBox->blockSignals(true);
m_rawTripletModeSpinBox->blockSignals(true);
m_rawTripletDataSpinBox->blockSignals(true);
m_rawTripletAddressSpinBox->setValue(0);
m_rawTripletModeSpinBox->setValue(0);
m_rawTripletDataSpinBox->setValue(0);
m_rawTripletAddressSpinBox->blockSignals(false);
m_rawTripletModeSpinBox->blockSignals(false);
m_rawTripletDataSpinBox->blockSignals(false);
m_cookedRowSpinBox->setEnabled(false);
m_cookedColumnSpinBox->setEnabled(false);
m_cookedRowSpinBox->blockSignals(true);
m_cookedColumnSpinBox->blockSignals(true);
m_cookedRowSpinBox->setValue(1);
m_cookedColumnSpinBox->setValue(0);
m_cookedRowSpinBox->blockSignals(false);
m_cookedColumnSpinBox->blockSignals(false);
m_cookedModePushButton->setEnabled(false);
m_cookedModePushButton->setText(QString());
m_tripletParameterStackedLayout->setCurrentIndex(0);
}
void X26DockWidget::updateAllCookedTripletWidgets(const QModelIndex &index)
{
const int modeExt = index.model()->data(index.model()->index(index.row(), 2), Qt::EditRole).toInt();
m_cookedModePushButton->setEnabled(true);
m_cookedModePushButton->setText(m_x26Model->modeTripletName(modeExt));
switch (modeExt) {

View File

@@ -117,6 +117,9 @@ private:
QPushButton *m_insertBeforePushButton, *m_insertAfterPushButton, *m_insertCopyPushButton, *m_deletePushButton;
TeletextWidget *m_parentMainWidget;
void disableTripletWidgets();
};
#endif

View File

@@ -65,11 +65,19 @@ QVariant X26Model::data(const QModelIndex &index, int role) const
}
// Error colours from KDE Plasma Breeze (light) theme
if (role == Qt::ForegroundRole && triplet.error() != X26Triplet::NoError && index.column() == m_tripletErrors[triplet.error()].columnHighlight)
return QColor(252, 252, 252);
if (role == Qt::ForegroundRole) {
if (triplet.error() != X26Triplet::NoError && index.column() == m_tripletErrors[triplet.error()].columnHighlight)
return QColor(252, 252, 252);
else if ((index.column() == 2 && triplet.reservedMode()) || (index.column() == 3 && triplet.reservedData()))
return QColor(35, 38, 39);
}
if (role == Qt::BackgroundRole && triplet.error() != X26Triplet::NoError && index.column() == m_tripletErrors[triplet.error()].columnHighlight)
return QColor(218, 68, 63);
if (role == Qt::BackgroundRole) {
if (triplet.error() != X26Triplet::NoError && index.column() == m_tripletErrors[triplet.error()].columnHighlight)
return QColor(218, 68, 63);
else if ((index.column() == 2 && triplet.reservedMode()) || (index.column() == 3 && triplet.reservedData()))
return QColor(246, 116, 0);
}
if (role == Qt::ToolTipRole && triplet.error() != X26Triplet::NoError)
return m_tripletErrors[triplet.error()].message;

View File

@@ -77,30 +77,45 @@ void X26TripletList::updateInternalData()
for (int i=0; i < m_list.size(); i++) {
triplet = &m_list[i];
triplet->m_error = X26Triplet::NoError;
triplet->m_reservedMode = false;
triplet->m_reservedData = false;
if (triplet->isRowTriplet()) {
switch (triplet->modeExt()) {
case 0x00: // Full screen colour
if (activePosition.isDeployed())
// TODO more specific error needed
triplet->m_error = X26Triplet::ActivePositionMovedUp;
if (triplet->m_data & 0x60)
triplet->m_reservedData = true;
break;
case 0x01: // Full row colour
if (!activePosition.setRow(triplet->addressRow()))
triplet->m_error = X26Triplet::ActivePositionMovedUp;
if ((triplet->m_data & 0x60) != 0x00 && (triplet->m_data & 0x60) != 0x60)
triplet->m_reservedData = true;
break;
case 0x04: // Set Active Position;
if (!activePosition.setRow(triplet->addressRow()))
triplet->m_error = X26Triplet::ActivePositionMovedUp;
else if (triplet->data() >= 40 || !activePosition.setColumn(triplet->data()))
else if (triplet->data() >= 40)
// FIXME data column highlighted?
triplet->m_reservedData = true;
else if (!activePosition.setColumn(triplet->data()))
triplet->m_error = X26Triplet::ActivePositionMovedLeft;
break;
case 0x07: // Address row 0
if (activePosition.isDeployed())
if (triplet->m_address != 63)
// FIXME data column highlighted?
triplet->m_reservedData = true;
else if (activePosition.isDeployed())
triplet->m_error = X26Triplet::ActivePositionMovedUp;
else {
activePosition.setRow(0);
activePosition.setColumn(8);
}
if ((triplet->m_data & 0x60) != 0x00 && (triplet->m_data & 0x60) != 0x60)
triplet->m_reservedData = true;
break;
case 0x10: // Origin Modifier
if (i == m_list.size()-1 ||
@@ -125,11 +140,45 @@ void X26TripletList::updateInternalData()
// otherwise the object won't appear
triplet->setObjectLocalIndex(i);
break;
case 0x18: // DRCS mode
if ((triplet->m_data & 0x30) == 0x00)
triplet->m_reservedData = true;
case 0x1f: // Termination marker
case 0x08 ... 0x0d: // PDC
break;
default:
triplet->m_reservedMode = true;
};
// Column triplet: make sure that PDC and reserved triplets don't affect the Active Position
} else if (triplet->modeExt() != 0x24 && triplet->modeExt() != 0x25 && triplet->modeExt() != 0x26 && triplet->modeExt() != 0x2a)
if (!activePosition.setColumn(triplet->addressColumn()))
triplet->m_error = X26Triplet::ActivePositionMovedLeft;
// Column triplet: all triplets modes except PDC and reserved move the Active Position
} else if (triplet->modeExt() == 0x24 || triplet->modeExt() == 0x25 || triplet->modeExt() == 0x2a)
triplet->m_reservedMode = true;
else if (triplet->modeExt() != 0x26 && !activePosition.setColumn(triplet->addressColumn()))
triplet->m_error = X26Triplet::ActivePositionMovedLeft;
else
switch (triplet->modeExt()) {
case 0x20: // Foreground colour
case 0x23: // Foreground colour
if (triplet->m_data & 0x60)
triplet->m_reservedData = true;
break;
case 0x21: // G1 mosaic character
case 0x22: // G3 mosaic character at level 1.5
case 0x29: // G0 character
case 0x2b: // G3 mosaic character at level >=2.5
case 0x2f ... 0x3f: // G2 character or G0 diacritical mark
if (triplet->m_data < 0x20)
triplet->m_reservedData = true;
break;
case 0x27: // Additional flash functions
if (triplet->m_data >= 0x18)
triplet->m_reservedData = true;
break;
case 0x2d: // DRCS character
if ((triplet->m_data & 0x3f) >= 48)
// Should really be an error?
triplet->m_reservedData = true;
}
triplet->m_activePositionRow = activePosition.row();
triplet->m_activePositionColumn = activePosition.column();
}

View File

@@ -62,6 +62,8 @@ public:
int activePositionRow() const { return m_activePositionRow; }
int activePositionColumn() const { return m_activePositionColumn; }
X26TripletError error() const { return m_error; }
bool reservedMode() const { return m_reservedMode; }
bool reservedData() const { return m_reservedData; }
friend class X26TripletList;
@@ -70,6 +72,8 @@ private:
int m_activePositionRow = -1;
int m_activePositionColumn = -1;
X26TripletError m_error = NoError;
bool m_reservedMode = false;
bool m_reservedData = false;
};
class X26TripletList