308 lines
9.3 KiB
C++
308 lines
9.3 KiB
C++
/****************************************************************************
|
|
**
|
|
** Copyright (C) 2023 The Qt Company Ltd.
|
|
** Contact: http://www.qt.io/licensing/
|
|
**
|
|
** This file is part of the Qt Quick Dialogs module of the Qt Toolkit.
|
|
**
|
|
** $QT_BEGIN_LICENSE:LGPL3$
|
|
** Commercial License Usage
|
|
** Licensees holding valid commercial Qt licenses may use this file in
|
|
** accordance with the commercial license agreement provided with the
|
|
** Software or, alternatively, in accordance with the terms contained in
|
|
** a written agreement between you and The Qt Company. For licensing terms
|
|
** and conditions see http://www.qt.io/terms-conditions. For further
|
|
** information use the contact form at http://www.qt.io/contact-us.
|
|
**
|
|
** GNU Lesser General Public License Usage
|
|
** Alternatively, this file may be used under the terms of the GNU Lesser
|
|
** General Public License version 3 as published by the Free Software
|
|
** Foundation and appearing in the file LICENSE.LGPLv3 included in the
|
|
** packaging of this file. Please review the following information to
|
|
** ensure the GNU Lesser General Public License version 3 requirements
|
|
** will be met: https://www.gnu.org/licenses/lgpl.html.
|
|
**
|
|
** GNU General Public License Usage
|
|
** Alternatively, this file may be used under the terms of the GNU
|
|
** General Public License version 2.0 or later as published by the Free
|
|
** Software Foundation and appearing in the file LICENSE.GPL included in
|
|
** the packaging of this file. Please review the following information to
|
|
** ensure the GNU General Public License version 2.0 requirements will be
|
|
** met: http://www.gnu.org/licenses/gpl-2.0.html.
|
|
**
|
|
** $QT_END_LICENSE$
|
|
**
|
|
****************************************************************************/
|
|
|
|
#include "quickstudiocsvtablemodel.h"
|
|
|
|
#include <QColor>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QFileSystemWatcher>
|
|
#include <QLoggingCategory>
|
|
#include <QRegularExpression>
|
|
#include <QTextStream>
|
|
|
|
static inline QColor fromString(const QString &colorName)
|
|
{
|
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)
|
|
return QColor::fromString(colorName);
|
|
#else
|
|
return colorName;
|
|
#endif // >= Qt 6.4
|
|
}
|
|
|
|
static inline bool isValidColorName(const QString &colorName)
|
|
{
|
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)
|
|
return QColor::isValidColorName(colorName);
|
|
#else
|
|
constexpr QStringView colorPattern(
|
|
u"(?<color>^(?:#(?:(?:[0-9a-fA-F]{2}){3,4}|(?:[0-9a-fA-F]){3,4}))$)");
|
|
static QRegularExpression colorRegex(colorPattern.toString());
|
|
return colorRegex.match(colorName).hasMatch();
|
|
#endif // >= Qt 6.4
|
|
}
|
|
|
|
static QVariant stringToVariant(const QString &value)
|
|
{
|
|
constexpr QStringView typesPattern{u"(?<boolean>^(?:true|false)$)|"
|
|
u"(?<number>^(?:-?(?:0|[1-9]\\d*)?(?:\\.\\d*)?(?<=\\d|\\.)"
|
|
u"(?:e-?(?:0|[1-9]\\d*))?|0x[0-9a-f]+)$)|"
|
|
u"(?<color>^(?:#(?:(?:[0-9a-fA-F]{2}){3,4}|"
|
|
u"(?:[0-9a-fA-F]){3,4}))$)"};
|
|
|
|
static QRegularExpression validator(typesPattern.toString());
|
|
const QString trimmedValue = value.trimmed();
|
|
QRegularExpressionMatch match = validator.match(trimmedValue);
|
|
|
|
if (!match.hasMatch())
|
|
return value;
|
|
|
|
if (!match.captured(u"boolean").isEmpty())
|
|
return QVariant::fromValue<bool>(trimmedValue.at(0).toLower() == u't');
|
|
|
|
if (!match.captured(u"number").isEmpty())
|
|
return trimmedValue.toDouble();
|
|
|
|
if (!match.captured(u"color").isEmpty())
|
|
return ::fromString(trimmedValue);
|
|
|
|
return value;
|
|
}
|
|
|
|
static QVariant stringToVariant(const QString &value, QMetaType::Type type, bool *ok = nullptr)
|
|
{
|
|
if (type == QMetaType::Bool) {
|
|
const QString lowerValue = value.toLower().trimmed();
|
|
bool conversionOk = true;
|
|
bool booleanValue = false;
|
|
|
|
if (lowerValue == u"true")
|
|
booleanValue = true;
|
|
else if (lowerValue == u"false")
|
|
booleanValue = false;
|
|
else
|
|
conversionOk = false;
|
|
|
|
if (ok)
|
|
*ok = conversionOk;
|
|
|
|
if (conversionOk)
|
|
return booleanValue;
|
|
}
|
|
|
|
if (type == QMetaType::Double) {
|
|
bool conversionOk = false;
|
|
double numericValue = value.toDouble(&conversionOk);
|
|
if (ok)
|
|
*ok = conversionOk;
|
|
|
|
if (conversionOk)
|
|
return numericValue;
|
|
}
|
|
|
|
if (type == QMetaType::QColor) {
|
|
bool conversionOk = ::isValidColorName(value);
|
|
if (ok)
|
|
*ok = conversionOk;
|
|
|
|
if (conversionOk)
|
|
return ::fromString(value);
|
|
}
|
|
|
|
if (type == QMetaType::QString) {
|
|
if (ok)
|
|
*ok = !value.isEmpty();
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
static QString urlToLocalPath(const QUrl &url)
|
|
{
|
|
QString localPath;
|
|
|
|
if (url.isLocalFile())
|
|
localPath = url.toLocalFile();
|
|
|
|
if (url.scheme() == QLatin1String("qrc")) {
|
|
const QString &path = url.path();
|
|
localPath = QStringLiteral(":") + path;
|
|
}
|
|
|
|
return localPath;
|
|
}
|
|
|
|
#ifndef Q_STATIC_LOGGING_CATEGORY // introduced with Qt 6.9
|
|
static Q_LOGGING_CATEGORY(quickStudioCsvTableModelDebug, "qt.StudioCsvTableModel.debug", QtDebugMsg)
|
|
#else
|
|
Q_STATIC_LOGGING_CATEGORY(quickStudioCsvTableModelDebug, "qt.StudioCsvTableModel.debug", QtDebugMsg)
|
|
#endif
|
|
|
|
|
|
QuickStudioCsvTableModel::QuickStudioCsvTableModel(QObject *parent)
|
|
: QAbstractTableModel(parent)
|
|
, m_fileWatcher(new QFileSystemWatcher(this))
|
|
{
|
|
connect(m_fileWatcher,
|
|
&QFileSystemWatcher::fileChanged,
|
|
this,
|
|
&QuickStudioCsvTableModel::checkPathAndReload);
|
|
}
|
|
|
|
int QuickStudioCsvTableModel::rowCount([[maybe_unused]] const QModelIndex &parent) const
|
|
{
|
|
return m_rows.size();
|
|
}
|
|
|
|
int QuickStudioCsvTableModel::columnCount([[maybe_unused]] const QModelIndex &parent) const
|
|
{
|
|
return m_headers.size();
|
|
}
|
|
|
|
QVariant QuickStudioCsvTableModel::data(const QModelIndex &index, int role) const
|
|
{
|
|
if (!index.isValid())
|
|
return {};
|
|
|
|
const QHash<int, QVariant> &recordData = m_rows.at(index.row());
|
|
|
|
if (role == Qt::DisplayRole)
|
|
return recordData.value(index.column()).toString();
|
|
|
|
return recordData.value(index.column());
|
|
}
|
|
|
|
QVariant QuickStudioCsvTableModel::headerData(int section,
|
|
Qt::Orientation orientation,
|
|
[[maybe_unused]] int role) const
|
|
{
|
|
if (orientation == Qt::Horizontal) {
|
|
if (section > -1 && section < m_headers.size())
|
|
return m_headers.at(section);
|
|
} else if (orientation == Qt::Vertical) {
|
|
if (section > -1 && section < m_rows.size())
|
|
return section;
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
QUrl QuickStudioCsvTableModel::source() const
|
|
{
|
|
return m_source;
|
|
}
|
|
|
|
void QuickStudioCsvTableModel::setSource(const QUrl &newSource)
|
|
{
|
|
if (m_source == newSource)
|
|
return;
|
|
|
|
m_source = newSource;
|
|
emit this->sourceChanged(m_source);
|
|
|
|
startWatchingSource();
|
|
reloadModel();
|
|
}
|
|
|
|
void QuickStudioCsvTableModel::reloadModel()
|
|
{
|
|
beginResetModel();
|
|
m_headers.clear();
|
|
m_rows.clear();
|
|
m_types.clear();
|
|
m_columnIsClean.clear();
|
|
|
|
QString filePath = ::urlToLocalPath(source());
|
|
QFile sourceFile(filePath);
|
|
|
|
if (!sourceFile.open(QFile::ReadOnly)) {
|
|
qWarning() << "File cannot be opened:" << sourceFile.errorString();
|
|
endResetModel();
|
|
return;
|
|
}
|
|
|
|
QTextStream stream(&sourceFile);
|
|
|
|
if (!stream.atEnd())
|
|
m_headers = stream.readLine().split(u',', Qt::KeepEmptyParts);
|
|
|
|
m_types.insert(0, m_headers.size(), QMetaType::UnknownType);
|
|
m_columnIsClean.insert(0, m_headers.size(), true);
|
|
|
|
if (!m_headers.isEmpty()) {
|
|
while (!stream.atEnd()) {
|
|
const QStringList recordDataList = stream.readLine().split(u',', Qt::KeepEmptyParts);
|
|
int column = -1;
|
|
QHash<int, QVariant> recordData;
|
|
for (const QString &cellString : recordDataList) {
|
|
if (++column == m_headers.size())
|
|
break;
|
|
|
|
if (!cellString.size())
|
|
continue;
|
|
|
|
const QMetaType::Type &type = m_types.at(column);
|
|
|
|
QVariant cellData;
|
|
if (type == QMetaType::UnknownType) {
|
|
cellData = stringToVariant(cellString);
|
|
m_types.replace(column, QMetaType::Type(cellData.typeId()));
|
|
} else {
|
|
bool columnIsClean = m_columnIsClean.at(column);
|
|
bool *ok = columnIsClean ? &columnIsClean : nullptr;
|
|
cellData = stringToVariant(cellString, type, ok);
|
|
if (ok)
|
|
m_columnIsClean.replace(column, *ok);
|
|
}
|
|
recordData.insert(column, cellData);
|
|
}
|
|
m_rows.append(recordData);
|
|
}
|
|
}
|
|
|
|
endResetModel();
|
|
}
|
|
|
|
void QuickStudioCsvTableModel::checkPathAndReload(const QString &path)
|
|
{
|
|
QString sourceLocalPath = ::urlToLocalPath(source());
|
|
if (path == sourceLocalPath)
|
|
reloadModel();
|
|
}
|
|
|
|
void QuickStudioCsvTableModel::startWatchingSource()
|
|
{
|
|
qCDebug(quickStudioCsvTableModelDebug) << Q_FUNC_INFO << "Load file: " << source();
|
|
|
|
const QStringList oldWatchingFiles = m_fileWatcher->files();
|
|
if (oldWatchingFiles.size())
|
|
m_fileWatcher->removePaths(oldWatchingFiles);
|
|
|
|
QString localPath = ::urlToLocalPath(source());
|
|
if (QFileInfo(localPath).isFile())
|
|
m_fileWatcher->addPath(localPath);
|
|
}
|