Revision control

++ // Copyright (C) 2018 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
++
++ #include "mainwindow.h"
++ #include "ui_mainwindow.h"
++ #include "distancefieldmodel.h"
++
++ #include <QtCore/qdir.h>
++ #include <QtCore/qdatastream.h>
++ #include <QtCore/qmath.h>
++ #include <QtCore/qendian.h>
++ #include <QtCore/qbuffer.h>
++ #include <QtGui/qdesktopservices.h>
++ #include <QtGui/qrawfont.h>
++ #include <QtWidgets/qmessagebox.h>
++ #include <QtWidgets/qlabel.h>
++ #include <QtWidgets/qprogressbar.h>
++ #include <QtWidgets/qfiledialog.h>
++ #include <QtWidgets/qinputdialog.h>
++
++ #include <QtCore/private/qunicodetables_p.h>
++ #include <QtGui/private/qdistancefield_p.h>
++ #include <QtQuick/private/qsgareaallocator_p.h>
++ #include <QtQuick/private/qsgadaptationlayer_p.h>
++
++ QT_BEGIN_NAMESPACE
++
++ static void openHelp()
++ {
++ const int qtVersion = QT_VERSION;
++ QString url;
++ QTextStream(&url) << "https://doc.qt.io/qt-" << (qtVersion >> 16) << "/qtdistancefieldgenerator-index.html";
++ QDesktopServices::openUrl(QUrl(url));
++ }
++
++ MainWindow::MainWindow(QWidget *parent)
++ : QMainWindow(parent)
++ , ui(new Ui::MainWindow)
++ , m_settings(qApp->organizationName(), qApp->applicationName())
++ , m_model(new DistanceFieldModel(this))
++ , m_statusBarLabel(nullptr)
++ , m_statusBarProgressBar(nullptr)
++ {
++ ui->setupUi(this);
++ ui->lvGlyphs->setModel(m_model);
++
++ ui->actionHelp->setShortcut(QKeySequence::HelpContents);
++
++ m_statusBarLabel = new QLabel(this);
++ m_statusBarLabel->setText(tr("Ready"));
++ ui->statusbar->addPermanentWidget(m_statusBarLabel);
++
++ m_statusBarProgressBar = new QProgressBar(this);
++ ui->statusbar->addPermanentWidget(m_statusBarProgressBar);
++ m_statusBarProgressBar->setVisible(false);
++
++ if (m_settings.contains(QStringLiteral("fontDirectory")))
++ m_fontDir = m_settings.value(QStringLiteral("fontDirectory")).toString();
++ else
++ m_fontDir = QDir::currentPath();
++
++ qRegisterMetaType<glyph_t>("glyph_t");
++ qRegisterMetaType<QPainterPath>("QPainterPath");
++
++ restoreGeometry(m_settings.value(QStringLiteral("geometry")).toByteArray());
++
++ setupConnections();
++ }
++
++ MainWindow::~MainWindow()
++ {
++ delete ui;
++ }
++
++ void MainWindow::open(const QString &path)
++ {
++ m_fileName.clear();
++ m_fontFile = path;
++ m_fontDir = QFileInfo(path).absolutePath();
++ m_settings.setValue(QStringLiteral("fontDirectory"), m_fontDir);
++
++ ui->lwUnicodeRanges->clear();
++ ui->lwUnicodeRanges->setDisabled(true);
++ ui->action_Save->setDisabled(true);
++ ui->action_Save_as->setDisabled(true);
++ ui->tbSave->setDisabled(true);
++ ui->action_Open->setDisabled(true);
++ m_model->setFont(path);
++ }
++
++ void MainWindow::closeEvent(QCloseEvent * /*event*/)
++ {
++ m_settings.setValue(QStringLiteral("geometry"), saveGeometry());
++ }
++
++ void MainWindow::setupConnections()
++ {
++ connect(ui->action_Open, &QAction::triggered, this, &MainWindow::openFont);
++ connect(ui->actionE_xit, &QAction::triggered, qApp, &QApplication::quit);
++ connect(ui->action_Save, &QAction::triggered, this, &MainWindow::save);
++ connect(ui->action_Save_as, &QAction::triggered, this, &MainWindow::saveAs);
++ connect(ui->tbSave, &QToolButton::clicked, this, &MainWindow::save);
++ connect(ui->tbSelectAll, &QToolButton::clicked, this, &MainWindow::selectAll);
++ connect(ui->actionSelect_all, &QAction::triggered, this, &MainWindow::selectAll);
++ connect(ui->actionSelect_string, &QAction::triggered, this, &MainWindow::selectString);
++ connect(ui->actionHelp, &QAction::triggered, this, openHelp);
++ connect(ui->actionAbout_App, &QAction::triggered, this, &MainWindow::about);
++ connect(ui->actionAbout_Qt, &QAction::triggered, this, [this]() {
++ QMessageBox::aboutQt(this);
++ });
++ connect(ui->lwUnicodeRanges, &QListWidget::itemSelectionChanged, this, &MainWindow::updateUnicodeRanges);
++
++ connect(ui->lvGlyphs->selectionModel(),
++ &QItemSelectionModel::selectionChanged,
++ this,
++ &MainWindow::updateSelection);
++ connect(m_model, &DistanceFieldModel::startGeneration, this, &MainWindow::startProgressBar);
++ connect(m_model, &DistanceFieldModel::stopGeneration, this, &MainWindow::stopProgressBar);
++ connect(m_model, &DistanceFieldModel::distanceFieldGenerated, this, &MainWindow::updateProgressBar);
++ connect(m_model, &DistanceFieldModel::stopGeneration, this, &MainWindow::populateUnicodeRanges);
++ connect(m_model, &DistanceFieldModel::error, this, &MainWindow::displayError);
++ }
++
++ void MainWindow::saveAs()
++ {
++ QString fileName = QFileDialog::getSaveFileName(this,
++ tr("Save distance field-enriched file"),
++ m_fontDir,
++ tr("Font files (*.ttf *.otf);;All files (*)"));
++ if (!fileName.isEmpty()) {
++ m_fileName = fileName;
++ m_fontDir = QFileInfo(m_fileName).absolutePath();
++ m_settings.setValue(QStringLiteral("fontDirectory"), m_fontDir);
++ save();
++ }
++ }
++
++
++ # pragma pack(1)
++ struct FontDirectoryHeader
++ {
++ quint32 sfntVersion;
++ quint16 numTables;
++ quint16 searchRange;
++ quint16 entrySelector;
++ quint16 rangeShift;
++ };
++
++ struct TableRecord
++ {
++ quint32 tag;
++ quint32 checkSum;
++ quint32 offset;
++ quint32 length;
++ };
++
++ struct QtdfHeader
++ {
++ quint8 majorVersion;
++ quint8 minorVersion;
++ quint16 pixelSize;
++ quint32 textureSize;
++ quint8 flags;
++ quint8 padding;
++ quint32 numGlyphs;
++ };
++
++ struct QtdfGlyphRecord
++ {
++ quint32 glyphIndex;
++ quint32 textureOffsetX;
++ quint32 textureOffsetY;
++ quint32 textureWidth;
++ quint32 textureHeight;
++ quint32 xMargin;
++ quint32 yMargin;
++ qint32 boundingRectX;
++ qint32 boundingRectY;
++ quint32 boundingRectWidth;
++ quint32 boundingRectHeight;
++ quint16 textureIndex;
++ };
++
++ struct QtdfTextureRecord
++ {
++ quint32 allocatedX;
++ quint32 allocatedY;
++ quint32 allocatedWidth;
++ quint32 allocatedHeight;
++ quint8 padding;
++ };
++
++ struct Head
++ {
++ quint16 majorVersion;
++ quint16 minorVersion;
++ quint32 fontRevision;
++ quint32 checkSumAdjustment;
++ };
++ # pragma pack()
++
++ #define PAD_BUFFER(buffer, size) \
++ { \
++ int paddingNeed = size % 4; \
++ if (paddingNeed > 0) { \
++ const char padding[3] = { 0, 0, 0 }; \
++ buffer.write(padding, 4 - paddingNeed); \
++ } \
++ }
++
++ #define ALIGN_OFFSET(offset) \
++ { \
++ int paddingNeed = offset % 4; \
++ if (paddingNeed > 0) \
++ offset += 4 - paddingNeed; \
++ }
++
++ #define TO_FIXED_POINT(value) \
++ ((int)(value*qreal(65536)))
++
++ void MainWindow::save()
++ {
++ QModelIndexList list = ui->lvGlyphs->selectionModel()->selectedIndexes();
++ if (list.isEmpty()) {
++ QMessageBox::warning(this,
++ tr("Nothing to save"),
++ tr("No glyphs selected for saving."),
++ QMessageBox::Ok);
++ return;
++ }
++
++ if (m_fileName.isEmpty()) {
++ saveAs();
++ return;
++ }
++
++ QFile inFile(m_fontFile);
++ if (!inFile.open(QIODevice::ReadOnly)) {
++ QMessageBox::warning(this,
++ tr("Can't read original font"),
++ tr("Cannot open '%s' for reading. The original font file must remain in place until the new file has been saved.").arg(m_fontFile),
++ QMessageBox::Ok);
++ return;
++ }
++
++ QByteArray output;
++ quint32 headOffset = 0;
++
++ {
++ QBuffer outBuffer(&output);
++ outBuffer.open(QIODevice::WriteOnly);
++
++ uchar *inData = inFile.map(0, inFile.size());
++ if (inData == nullptr) {
++ QMessageBox::warning(this,
++ tr("Can't map input file"),
++ tr("Unable to memory map input file '%s'.").arg(m_fontFile));
++ return;
++ }
++
++ uchar *end = inData + inFile.size();
++ if (inData + sizeof(FontDirectoryHeader) > end) {
++ QMessageBox::warning(this,
++ tr("Can't read font directory"),
++ tr("Input file seems to be invalid or corrupt."),
++ QMessageBox::Ok);
++ return;
++ }
++
++ FontDirectoryHeader fontDirectoryHeader;
++ memcpy(&fontDirectoryHeader, inData, sizeof(FontDirectoryHeader));
++ quint16 numTables = qFromBigEndian(fontDirectoryHeader.numTables) + 1;
++ fontDirectoryHeader.numTables = qToBigEndian(numTables);
++ {
++ quint16 searchRange = qFromBigEndian(fontDirectoryHeader.searchRange);
++ if (searchRange / 16 < numTables) {
++ quint16 pot = (searchRange / 16) * 2;
++ searchRange = pot * 16;
++ fontDirectoryHeader.searchRange = qToBigEndian(searchRange);
++ fontDirectoryHeader.rangeShift = qToBigEndian(numTables * 16 - searchRange);
++
++ quint16 entrySelector = 0;
++ while (pot > 1) {
++ pot >>= 1;
++ entrySelector++;
++ }
++ fontDirectoryHeader.entrySelector = qToBigEndian(entrySelector);
++ }
++ }
++
++ outBuffer.write(reinterpret_cast<char *>(&fontDirectoryHeader),
++ sizeof(FontDirectoryHeader));
++
++ QVarLengthArray<std::pair<quint32, quint32>> offsetLengthPairs;
++ offsetLengthPairs.reserve(numTables - 1);
++
++ // Copy the offset table, updating offsets
++ TableRecord *offsetTable = reinterpret_cast<TableRecord *>(inData + sizeof(FontDirectoryHeader));
++ quint32 currentOffset = sizeof(FontDirectoryHeader) + sizeof(TableRecord) * numTables;
++ for (int i = 0; i < numTables - 1; ++i) {
++ ALIGN_OFFSET(currentOffset)
++
++ quint32 originalOffset = qFromBigEndian(offsetTable->offset);
++ quint32 length = qFromBigEndian(offsetTable->length);
++ offsetLengthPairs.append({originalOffset, length});
++ if (offsetTable->tag == qFromBigEndian(QFont::Tag("head").value()))
++ headOffset = currentOffset;
++
++ TableRecord newTableRecord;
++ memcpy(&newTableRecord, offsetTable, sizeof(TableRecord));
++ newTableRecord.offset = qToBigEndian(currentOffset);
++ outBuffer.write(reinterpret_cast<char *>(&newTableRecord), sizeof(TableRecord));
++
++ offsetTable++;
++ currentOffset += length;
++ }
++
++ if (headOffset == 0) {
++ QMessageBox::warning(this,
++ tr("Invalid font file"),
++ tr("Font file does not have 'head' table."),
++ QMessageBox::Ok);
++ return;
++ }
++
++ QByteArray qtdf = createSfntTable();
++ if (qtdf.isEmpty())
++ return;
++
++ {
++ ALIGN_OFFSET(currentOffset)
++
++ TableRecord qtdfRecord;
++ qtdfRecord.offset = qToBigEndian(currentOffset);
++ qtdfRecord.length = qToBigEndian(quint32(qtdf.size()));
++ qtdfRecord.tag = qFromBigEndian(QFont::Tag("qtdf").value());
++ quint32 checkSum = 0;
++ const quint32 *start = reinterpret_cast<const quint32 *>(qtdf.constData());
++ const quint32 *end = reinterpret_cast<const quint32 *>(qtdf.constData() + qtdf.size());
++ while (start < end)
++ checkSum += *(start++);
++ qtdfRecord.checkSum = qToBigEndian(checkSum);
++
++ outBuffer.write(reinterpret_cast<char *>(&qtdfRecord),
++ sizeof(TableRecord));
++ }
++
++ // Copy all font tables
++ for (const std::pair<quint32, quint32> &offsetLengthPair : offsetLengthPairs) {
++ PAD_BUFFER(outBuffer, output.size())
++ outBuffer.write(reinterpret_cast<char *>(inData + offsetLengthPair.first),
++ offsetLengthPair.second);
++ }
++
++ PAD_BUFFER(outBuffer, output.size())
++ outBuffer.write(qtdf);
++ }
++
++ // Clear 'head' checksum and calculate new check sum adjustment
++ Head *head = reinterpret_cast<Head *>(output.data() + headOffset);
++ head->checkSumAdjustment = 0;
++
++ quint32 checkSum = 0;
++ const quint32 *start = reinterpret_cast<const quint32 *>(output.constData());
++ const quint32 *end = reinterpret_cast<const quint32 *>(output.constData() + output.size());
++ while (start < end)
++ checkSum += *(start++);
++
++ head->checkSumAdjustment = qToBigEndian(0xB1B0AFBA - checkSum);
++
++ QFile outFile(m_fileName);
++ if (!outFile.open(QIODevice::WriteOnly)) {
++ QMessageBox::warning(this,
++ tr("Can't write to file"),
++ tr("Cannot open the file '%s' for writing").arg(m_fileName),
++ QMessageBox::Ok);
++ return;
++ }
++
++ outFile.write(output);
++ }
++
++ QByteArray MainWindow::createSfntTable()
++ {
++ QModelIndexList list = ui->lvGlyphs->selectionModel()->selectedIndexes();
++ Q_ASSERT(!list.isEmpty());
++
++ QByteArray ret;
++ {
++ QBuffer buffer(&ret);
++ buffer.open(QIODevice::WriteOnly);
++
++ QtdfHeader header;
++ header.majorVersion = 5;
++ header.minorVersion = 12;
++ header.pixelSize = qToBigEndian(quint16(qRound(m_model->pixelSize())));
++
++ const quint8 padding = 2;
++ qreal scaleFactor = qreal(1) / QT_DISTANCEFIELD_SCALE(m_model->doubleGlyphResolution());
++ const int radius = QT_DISTANCEFIELD_RADIUS(m_model->doubleGlyphResolution())
++ / QT_DISTANCEFIELD_SCALE(m_model->doubleGlyphResolution());
++
++ quint32 textureSize = ui->sbMaximumTextureSize->value();
++
++ // Since we are using a single area allocator that spans all textures, we need
++ // to split the textures one row before the actual maximum size, otherwise
++ // glyphs that fall on the edge between two textures will expand the texture
++ // they are assigned to, and this will end up being larger than the max.
++ textureSize -= quint32(qCeil(m_model->pixelSize() * scaleFactor) + radius * 2 + padding * 2);
++ header.textureSize = qToBigEndian(textureSize);
++
++ header.padding = padding;
++ header.flags = m_model->doubleGlyphResolution() ? 1 : 0;
++ header.numGlyphs = qToBigEndian(quint32(list.size()));
++ buffer.write(reinterpret_cast<char *>(&header),
++ sizeof(QtdfHeader));
++
++ // Maximum height allocator to find optimal number of textures
++ QList<QRect> allocatedAreaPerTexture;
++
++ struct GlyphData {
++ QSGDistanceFieldGlyphCache::TexCoord texCoord;
++ QRectF boundingRect;
++ QSize glyphSize;
++ int textureIndex;
++ };
++ QList<GlyphData> glyphDatas;
++ glyphDatas.resize(m_model->rowCount());
++
++ int textureCount = 0;
++
++ {
++ QTransform scaleDown;
++ scaleDown.scale(scaleFactor, scaleFactor);
++
++ {
++ bool foundOptimalSize = false;
++ while (!foundOptimalSize) {
++ allocatedAreaPerTexture.clear();
++
++ QSGAreaAllocator allocator(QSize(textureSize, textureSize * (++textureCount)));
++
++ int i;
++ for (i = 0; i < list.size(); ++i) {
++ int glyphIndex = list.at(i).row();
++ GlyphData &glyphData = glyphDatas[glyphIndex];
++
++ QPainterPath path = m_model->path(glyphIndex);
++ glyphData.boundingRect = scaleDown.mapRect(path.boundingRect());
++ int glyphWidth = qCeil(glyphData.boundingRect.width()) + radius * 2;
++ int glyphHeight = qCeil(glyphData.boundingRect.height()) + radius * 2;
++
++ glyphData.glyphSize = QSize(glyphWidth + padding * 2, glyphHeight + padding * 2);
++
++ if (glyphData.glyphSize.width() > qint32(textureSize)
++ || glyphData.glyphSize.height() > qint32(textureSize)) {
++ QMessageBox::warning(this,
++ tr("Glyph too large for texture"),
++ tr("Glyph %1 is too large to fit in texture of size %2.")
++ .arg(glyphIndex).arg(textureSize));
++ return QByteArray();
++ }
++
++ QRect rect = allocator.allocate(glyphData.glyphSize);
++ if (rect.isNull())
++ break;
++
++ glyphData.textureIndex = rect.y() / textureSize;
++ while (glyphData.textureIndex >= allocatedAreaPerTexture.size())
++ allocatedAreaPerTexture.append(QRect(0, 0, 1, 1));
++
++ allocatedAreaPerTexture[glyphData.textureIndex] |= QRect(rect.x(),
++ rect.y() % textureSize,
++ rect.width(),
++ rect.height());
++
++ glyphData.texCoord.xMargin = QT_DISTANCEFIELD_RADIUS(m_model->doubleGlyphResolution()) / qreal(QT_DISTANCEFIELD_SCALE(m_model->doubleGlyphResolution()));
++ glyphData.texCoord.yMargin = QT_DISTANCEFIELD_RADIUS(m_model->doubleGlyphResolution()) / qreal(QT_DISTANCEFIELD_SCALE(m_model->doubleGlyphResolution()));
++ glyphData.texCoord.x = rect.x() + padding;
++ glyphData.texCoord.y = rect.y() % textureSize + padding;
++ glyphData.texCoord.width = glyphData.boundingRect.width();
++ glyphData.texCoord.height = glyphData.boundingRect.height();
++
++ glyphDatas.append(glyphData);
++ }
++
++ foundOptimalSize = i == list.size();
++ if (foundOptimalSize)
++ buffer.write(allocator.serialize());
++ }
++ }
++ }
++
++ QList<QDistanceField> textures;
++ textures.resize(textureCount);
++
++ for (int textureIndex = 0; textureIndex < textureCount; ++textureIndex) {
++ textures[textureIndex] = QDistanceField(allocatedAreaPerTexture.at(textureIndex).width(),
++ allocatedAreaPerTexture.at(textureIndex).height());
++
++ QRect rect = allocatedAreaPerTexture.at(textureIndex);
++
++ QtdfTextureRecord record;
++ record.allocatedX = qToBigEndian(rect.x());
++ record.allocatedY = qToBigEndian(rect.y());
++ record.allocatedWidth = qToBigEndian(rect.width());
++ record.allocatedHeight = qToBigEndian(rect.height());
++ record.padding = padding;
++ buffer.write(reinterpret_cast<char *>(&record),
++ sizeof(QtdfTextureRecord));
++ }
++
++ {
++ for (int i = 0; i < list.size(); ++i) {
++ int glyphIndex = list.at(i).row();
++ QImage image = m_model->distanceField(glyphIndex);
++
++ const GlyphData &glyphData = glyphDatas.at(glyphIndex);
++
++ QtdfGlyphRecord glyphRecord;
++ glyphRecord.glyphIndex = qToBigEndian(glyphIndex);
++ glyphRecord.textureOffsetX = qToBigEndian(TO_FIXED_POINT(glyphData.texCoord.x));
++ glyphRecord.textureOffsetY = qToBigEndian(TO_FIXED_POINT(glyphData.texCoord.y));
++ glyphRecord.textureWidth = qToBigEndian(TO_FIXED_POINT(glyphData.texCoord.width));
++ glyphRecord.textureHeight = qToBigEndian(TO_FIXED_POINT(glyphData.texCoord.height));
++ glyphRecord.xMargin = qToBigEndian(TO_FIXED_POINT(glyphData.texCoord.xMargin));
++ glyphRecord.yMargin = qToBigEndian(TO_FIXED_POINT(glyphData.texCoord.yMargin));
++ glyphRecord.boundingRectX = qToBigEndian(TO_FIXED_POINT(glyphData.boundingRect.x()));
++ glyphRecord.boundingRectY = qToBigEndian(TO_FIXED_POINT(glyphData.boundingRect.y()));
++ glyphRecord.boundingRectWidth = qToBigEndian(TO_FIXED_POINT(glyphData.boundingRect.width()));
++ glyphRecord.boundingRectHeight = qToBigEndian(TO_FIXED_POINT(glyphData.boundingRect.height()));
++ glyphRecord.textureIndex = qToBigEndian(quint16(glyphData.textureIndex));
++ buffer.write(reinterpret_cast<char *>(&glyphRecord), sizeof(QtdfGlyphRecord));
++
++ int expectedWidth = qCeil(glyphData.texCoord.width + glyphData.texCoord.xMargin * 2);
++ image = image.copy(-padding, -padding,
++ expectedWidth + padding * 2,
++ image.height() + padding * 2);
++
++ uchar *inBits = image.scanLine(0);
++ uchar *outBits = textures[glyphData.textureIndex].scanLine(int(glyphData.texCoord.y) - padding)
++ + int(glyphData.texCoord.x) - padding;
++ for (int y = 0; y < image.height(); ++y) {
++ memcpy(outBits, inBits, image.width());
++ inBits += image.bytesPerLine();
++ outBits += textures[glyphData.textureIndex].width();
++ }
++ }
++ }
++
++ for (int i = 0; i < textures.size(); ++i) {
++ const QDistanceField &texture = textures.at(i);
++ const QRect &allocatedArea = allocatedAreaPerTexture.at(i);
++ buffer.write(reinterpret_cast<const char *>(texture.constBits()),
++ allocatedArea.width() * allocatedArea.height());
++ }
++
++ PAD_BUFFER(buffer, ret.size())
++ }
++
++ return ret;
++ }
++
++ void MainWindow::writeFile()
++ {
++ Q_ASSERT(!m_fileName.isEmpty());
++
++ QFile file(m_fileName);
++ if (file.open(QIODevice::WriteOnly)) {
++
++ } else {
++ QMessageBox::warning(this,
++ tr("Can't open file for writing"),
++ tr("Unable to open file '%1' for writing").arg(m_fileName),
++ QMessageBox::Ok);
++ }
++ }
++
++ void MainWindow::openFont()
++ {
++ QString fileName = QFileDialog::getOpenFileName(this,
++ tr("Open font file"),
++ m_fontDir,
++ tr("Fonts (*.ttf *.otf);;All files (*)"));
++ if (!fileName.isEmpty())
++ open(fileName);
++ }
++
++ void MainWindow::updateProgressBar()
++ {
++ m_statusBarProgressBar->setValue(m_statusBarProgressBar->value() + 1);
++ updateSelection();
++ }
++
++ void MainWindow::startProgressBar(quint16 glyphCount)
++ {
++ ui->action_Open->setDisabled(false);
++ m_statusBarLabel->setText(tr("Generating"));
++ m_statusBarProgressBar->setMaximum(glyphCount);
++ m_statusBarProgressBar->setMinimum(0);
++ m_statusBarProgressBar->setValue(0);
++ m_statusBarProgressBar->setVisible(true);
++ }
++
++ void MainWindow::stopProgressBar()
++ {
++ m_statusBarLabel->setText(tr("Ready"));
++ m_statusBarProgressBar->setVisible(false);
++ }
++
++ void MainWindow::selectAll()
++ {
++ QModelIndexList list = ui->lvGlyphs->selectionModel()->selectedIndexes();
++ if (list.size() == ui->lvGlyphs->model()->rowCount())
++ ui->lvGlyphs->clearSelection();
++ else
++ ui->lvGlyphs->selectAll();
++ }
++
++ void MainWindow::updateSelection()
++ {
++ QModelIndexList list = ui->lvGlyphs->selectionModel()->selectedIndexes();
++ QString label;
++ if (list.size() == ui->lvGlyphs->model()->rowCount())
++ label = tr("Deselect &All");
++ else
++ label = tr("Select &All");
++
++ ui->tbSelectAll->setText(label);
++ ui->actionSelect_all->setText(label);
++
++ if (m_model != nullptr && ui->lwUnicodeRanges->count() > 0) {
++ // Ignore selection changes until we are done
++ disconnect(ui->lwUnicodeRanges, &QListWidget::itemSelectionChanged, this, &MainWindow::updateUnicodeRanges);
++
++ QSet<int> selectedGlyphIndexes;
++ for (const QModelIndex &modelIndex : list)
++ selectedGlyphIndexes.insert(modelIndex.row());
++
++ QList<DistanceFieldModel::UnicodeRange> unicodeRanges = m_model->unicodeRanges();
++ std::sort(unicodeRanges.begin(), unicodeRanges.end());
++
++ Q_ASSERT(ui->lwUnicodeRanges->count() == unicodeRanges.size());
++ for (int i = 0; i < unicodeRanges.size(); ++i) {
++ DistanceFieldModel::UnicodeRange unicodeRange = unicodeRanges.at(i);
++ QListWidgetItem *item = ui->lwUnicodeRanges->item(i);
++
++ QList<glyph_t> glyphIndexes = m_model->glyphIndexesForUnicodeRange(unicodeRange);
++ Q_ASSERT(!glyphIndexes.isEmpty());
++
++ item->setSelected(true);
++ for (glyph_t glyphIndex : glyphIndexes) {
++ if (!selectedGlyphIndexes.contains(glyphIndex)) {
++ item->setSelected(false);
++ break;
++ }
++ }
++ }
++
++ connect(ui->lwUnicodeRanges, &QListWidget::itemSelectionChanged, this, &MainWindow::updateUnicodeRanges);
++ }
++ }
++
++ void MainWindow::updateUnicodeRanges()
++ {
++ if (m_model == nullptr)
++ return;
++
++ disconnect(ui->lvGlyphs->selectionModel(),
++ &QItemSelectionModel::selectionChanged,
++ this,
++ &MainWindow::updateSelection);
++
++ QItemSelection selectedItems;
++
++ for (int i = 0; i < ui->lwUnicodeRanges->count(); ++i) {
++ QListWidgetItem *item = ui->lwUnicodeRanges->item(i);
++ if (item->isSelected()) {
++ DistanceFieldModel::UnicodeRange unicodeRange = item->data(Qt::UserRole).value<DistanceFieldModel::UnicodeRange>();
++ QList<glyph_t> glyphIndexes = m_model->glyphIndexesForUnicodeRange(unicodeRange);
++
++ for (glyph_t glyphIndex : glyphIndexes) {
++ QModelIndex index = m_model->index(glyphIndex);
++ selectedItems.select(index, index);
++ }
++ }
++ }
++
++ ui->lvGlyphs->selectionModel()->clearSelection();
++ if (!selectedItems.isEmpty())
++ ui->lvGlyphs->selectionModel()->select(selectedItems, QItemSelectionModel::Select);
++
++ connect(ui->lvGlyphs->selectionModel(),
++ &QItemSelectionModel::selectionChanged,
++ this,
++ &MainWindow::updateSelection);
++ }
++
++ void MainWindow::populateUnicodeRanges()
++ {
++ QList<DistanceFieldModel::UnicodeRange> unicodeRanges = m_model->unicodeRanges();
++ std::sort(unicodeRanges.begin(), unicodeRanges.end());
++
++ for (DistanceFieldModel::UnicodeRange unicodeRange : unicodeRanges) {
++ QString name = m_model->nameForUnicodeRange(unicodeRange);
++ QListWidgetItem *item = new QListWidgetItem(name, ui->lwUnicodeRanges);
++ item->setData(Qt::UserRole, unicodeRange);
++ }
++
++ ui->lwUnicodeRanges->setDisabled(false);
++ ui->action_Save->setDisabled(false);
++ ui->action_Save_as->setDisabled(false);
++ ui->tbSave->setDisabled(false);
++ }
++
++ void MainWindow::displayError(const QString &errorString)
++ {
++ QMessageBox::warning(this, tr("Error when parsing font file"), errorString, QMessageBox::Ok);
++ }
++
++ void MainWindow::selectString()
++ {
++ QString s = QInputDialog::getText(this,
++ tr("Select glyphs for string"),
++ tr("String to parse:"));
++ if (!s.isEmpty()) {
++ QList<uint> ucs4String = s.toUcs4();
++ for (uint ucs4 : ucs4String) {
++ glyph_t glyph = m_model->glyphIndexForUcs4(ucs4);
++ if (glyph != 0) {
++ ui->lvGlyphs->selectionModel()->select(m_model->index(glyph),
++ QItemSelectionModel::Select);
++ }
++ }
++ }
++ }
++
++ void MainWindow::about()
++ {
++ QMessageBox *msgBox = new QMessageBox(this);
++ msgBox->setAttribute(Qt::WA_DeleteOnClose);
++ msgBox->setWindowTitle(tr("About Qt Distance Field Generator"));
++ msgBox->setText(tr("<h3>Qt Distance Field Generator</h3>"
++ "<p>Version %1.<br/>"
++ "The Qt Distance Field Generator tool allows "
++ "to prepare a font cache for Qt applications.</p>"
++ "<p>Copyright (C) The Qt Company Ltd. and other contributors.</p>")
++ .arg(QLatin1String(QT_VERSION_STR)));
++ msgBox->show();
++ }
++
++ QT_END_NAMESPACE