617 lines
19 KiB
C++
617 lines
19 KiB
C++
/*
|
|
* Copyright (C) 2020-2022 Roy Qu (royqh1979@gmail.com)
|
|
*
|
|
* This program 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.
|
|
*
|
|
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
#include "ojproblemsetmodel.h"
|
|
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QIcon>
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QMimeData>
|
|
#include "../utils.h"
|
|
#include "../iconsmanager.h"
|
|
#include "../systemconsts.h"
|
|
#include "../settings.h"
|
|
|
|
OJProblemSetModel::OJProblemSetModel(QObject *parent) : QAbstractListModel(parent)
|
|
{
|
|
|
|
}
|
|
|
|
void OJProblemSetModel::clear()
|
|
{
|
|
beginResetModel();
|
|
mProblemSet.problems.clear();
|
|
mProblemSet.exportFilename.clear();
|
|
endResetModel();
|
|
}
|
|
|
|
int OJProblemSetModel::count()
|
|
{
|
|
return mProblemSet.problems.count();
|
|
}
|
|
|
|
void OJProblemSetModel::create(const QString& name)
|
|
{
|
|
mProblemSet.name = name;
|
|
clear();
|
|
}
|
|
|
|
void OJProblemSetModel::rename(const QString &newName)
|
|
{
|
|
if (mProblemSet.name!=newName)
|
|
mProblemSet.name = newName;
|
|
}
|
|
|
|
QString OJProblemSetModel::name() const
|
|
{
|
|
return mProblemSet.name;
|
|
}
|
|
|
|
QString OJProblemSetModel::exportFilename() const
|
|
{
|
|
return mProblemSet.exportFilename;
|
|
}
|
|
|
|
void OJProblemSetModel::addProblem(const POJProblem& problem)
|
|
{
|
|
beginInsertRows(QModelIndex(), mProblemSet.problems.count(), mProblemSet.problems.count());
|
|
mProblemSet.problems.append(problem);
|
|
endInsertRows();
|
|
}
|
|
|
|
void OJProblemSetModel::addProblems(const QList<POJProblem> &problems)
|
|
{
|
|
if (problems.isEmpty())
|
|
return;
|
|
beginInsertRows(QModelIndex(), mProblemSet.problems.count(), mProblemSet.problems.count()+problems.count()-1);
|
|
foreach( const POJProblem& p, problems)
|
|
mProblemSet.problems.append(p);
|
|
endInsertRows();
|
|
}
|
|
|
|
POJProblem OJProblemSetModel::problem(int index)
|
|
{
|
|
return mProblemSet.problems[index];
|
|
}
|
|
|
|
void OJProblemSetModel::removeProblem(int index)
|
|
{
|
|
Q_ASSERT(index>=0 && index < mProblemSet.problems.count());
|
|
beginRemoveRows(QModelIndex(),index,index);
|
|
mProblemSet.problems.removeAt(index);
|
|
endRemoveRows();
|
|
}
|
|
|
|
bool OJProblemSetModel::problemNameUsed(const QString &name)
|
|
{
|
|
foreach (const POJProblem& problem, mProblemSet.problems) {
|
|
if (name == problem->name)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void OJProblemSetModel::removeAllProblems()
|
|
{
|
|
clear();
|
|
}
|
|
|
|
void OJProblemSetModel::saveToFile(const QString &fileName, int currentIndex)
|
|
{
|
|
QFile file(fileName);
|
|
if (file.open(QFile::WriteOnly | QFile::Truncate)) {
|
|
QJsonObject obj;
|
|
mProblemSet.exportFilename=fileName;
|
|
obj["name"]=mProblemSet.name;
|
|
QJsonArray problemsArray;
|
|
foreach (const POJProblem& problem, mProblemSet.problems) {
|
|
QJsonObject problemObj;
|
|
problemObj["name"]=problem->name;
|
|
problemObj["url"]=problem->url;
|
|
problemObj["description"]=problem->description;
|
|
problemObj["time_limit"]=(int)problem->timeLimit;
|
|
problemObj["memory_limit"]=(int)problem->memoryLimit;
|
|
problemObj["time_limit_unit"]=(int)problem->timeLimitUnit;
|
|
problemObj["memory_limit_unit"]=(int)problem->memoryLimitUnit;
|
|
if (fileExists(problem->answerProgram))
|
|
problemObj["answer_program"] = problem->answerProgram;
|
|
QJsonArray cases;
|
|
foreach (const POJProblemCase& problemCase, problem->cases) {
|
|
QJsonObject caseObj;
|
|
caseObj["name"]=problemCase->name;
|
|
caseObj["input"]=problemCase->input;
|
|
QString path = problemCase->inputFileName;
|
|
QString prefix = includeTrailingPathDelimiter(extractFileDir(fileName));
|
|
if (path.startsWith(prefix, PATH_SENSITIVITY)) {
|
|
path = "%ProblemSetPath%/"+ path.mid(prefix.length());
|
|
}
|
|
caseObj["input_filename"]=path;
|
|
path = problemCase->expectedOutputFileName;
|
|
if (path.startsWith(prefix, PATH_SENSITIVITY)) {
|
|
path = "%ProblemSetPath%/"+ path.mid(prefix.length());
|
|
}
|
|
caseObj["expected_output_filename"]=path;
|
|
caseObj["expected"]=problemCase->expected;
|
|
cases.append(caseObj);
|
|
}
|
|
problemObj["cases"]=cases;
|
|
problemsArray.append(problemObj);
|
|
}
|
|
obj["problems"]=problemsArray;
|
|
obj["current_index"]=currentIndex;
|
|
QJsonDocument doc;
|
|
doc.setObject(obj);
|
|
file.write(doc.toJson());
|
|
file.close();
|
|
} else {
|
|
throw FileError(QObject::tr("Can't open file '%1' for read.")
|
|
.arg(fileName));
|
|
}
|
|
}
|
|
|
|
void OJProblemSetModel::loadFromFile(const QString &fileName, int& currentIndex)
|
|
{
|
|
QFile file(fileName);
|
|
if (file.open(QFile::ReadOnly)) {
|
|
QByteArray content = file.readAll();
|
|
QJsonParseError error;
|
|
QJsonDocument doc(QJsonDocument::fromJson(content,&error));
|
|
if (error.error!=QJsonParseError::NoError) {
|
|
throw FileError(QObject::tr("Can't parse problem set file '%1':%2")
|
|
.arg(fileName)
|
|
.arg(error.errorString()));
|
|
}
|
|
beginResetModel();
|
|
QJsonObject obj = doc.object();
|
|
mProblemSet.name = obj["name"].toString();
|
|
currentIndex = obj["current_index"].toInt(-1);
|
|
mProblemSet.problems.clear();
|
|
QJsonArray problemsArray = obj["problems"].toArray();
|
|
foreach (const QJsonValue& problemVal, problemsArray) {
|
|
QJsonObject problemObj = problemVal.toObject();
|
|
POJProblem problem = std::make_shared<OJProblem>();
|
|
problem->name = problemObj["name"].toString();
|
|
problem->url = problemObj["url"].toString();
|
|
problem->timeLimit = problemObj["time_limit"].toInt();
|
|
problem->memoryLimit = problemObj["memory_limit"].toInt();
|
|
problem->timeLimitUnit = (ProblemTimeLimitUnit)problemObj["time_limit_unit"].toInt();
|
|
problem->memoryLimitUnit = (ProblemMemoryLimitUnit)problemObj["memory_limit_unit"].toInt();
|
|
|
|
problem->description = problemObj["description"].toString();
|
|
problem->answerProgram = problemObj["answer_program"].toString();
|
|
QJsonArray casesArray = problemObj["cases"].toArray();
|
|
foreach (const QJsonValue& caseVal, casesArray) {
|
|
QJsonObject caseObj = caseVal.toObject();
|
|
POJProblemCase problemCase = std::make_shared<OJProblemCase>();
|
|
problemCase->name = caseObj["name"].toString();
|
|
problemCase->input = caseObj["input"].toString();
|
|
problemCase->expected = caseObj["expected"].toString();
|
|
QString path = caseObj["input_filename"].toString();
|
|
if (path.startsWith("%ProblemSetPath%/")) {
|
|
path = includeTrailingPathDelimiter(extractFileDir(fileName))+
|
|
path.mid(QLatin1String("%ProblemSetPath%/").size());
|
|
}
|
|
problemCase->inputFileName=path;
|
|
path = caseObj["expected_output_filename"].toString();
|
|
if (path.startsWith("%ProblemSetPath%/")) {
|
|
path = includeTrailingPathDelimiter(extractFileDir(fileName))+
|
|
path.mid(QLatin1String("%ProblemSetPath%/").size());
|
|
}
|
|
problemCase->expectedOutputFileName=path;
|
|
problemCase->testState = ProblemCaseTestState::NotTested;
|
|
problem->cases.append(problemCase);
|
|
}
|
|
mProblemSet.problems.append(problem);
|
|
}
|
|
endResetModel();
|
|
} else {
|
|
throw FileError(QObject::tr("Can't open file '%1' for read.")
|
|
.arg(fileName));
|
|
}
|
|
}
|
|
|
|
void OJProblemSetModel::load(int ¤tIndex)
|
|
{
|
|
QDir dir(pSettings->dirs().config());
|
|
QString filename=dir.filePath(DEV_PROBLEM_SET_FILE);
|
|
if (fileExists(filename))
|
|
loadFromFile(filename,currentIndex);
|
|
}
|
|
|
|
void OJProblemSetModel::save(int currentIndex)
|
|
{
|
|
QDir dir(pSettings->dirs().config());
|
|
QString filename=dir.filePath(DEV_PROBLEM_SET_FILE);
|
|
saveToFile(filename,currentIndex);
|
|
}
|
|
|
|
void OJProblemSetModel::updateProblemAnswerFilename(const QString &oldFilename, const QString &newFilename)
|
|
{
|
|
foreach (POJProblem problem, mProblemSet.problems) {
|
|
if (QString::compare(problem->answerProgram,oldFilename,PATH_SENSITIVITY)==0) {
|
|
problem->answerProgram = newFilename;
|
|
}
|
|
}
|
|
}
|
|
|
|
int OJProblemSetModel::rowCount(const QModelIndex &) const
|
|
{
|
|
return mProblemSet.problems.count();
|
|
}
|
|
|
|
QVariant OJProblemSetModel::data(const QModelIndex &index, int role) const
|
|
{
|
|
if (!index.isValid())
|
|
return QVariant();
|
|
if (role == Qt::DisplayRole || role == Qt::EditRole) {
|
|
return mProblemSet.problems[index.row()]->name;
|
|
} else if (role == Qt::ToolTipRole) {
|
|
POJProblem problem = mProblemSet.problems[index.row()];
|
|
|
|
QString s;
|
|
s=QString("<h3>%1</h3>").arg(problem->name);
|
|
if (!problem->description.isEmpty())
|
|
s+=problem->description;
|
|
|
|
return s;
|
|
}
|
|
return QVariant();
|
|
}
|
|
|
|
bool OJProblemSetModel::setData(const QModelIndex &index, const QVariant &value, int role)
|
|
{
|
|
if (!index.isValid())
|
|
return false;
|
|
if (role == Qt::EditRole) {
|
|
QString s = value.toString();
|
|
if (!s.isEmpty()) {
|
|
mProblemSet.problems[index.row()]->name = s;
|
|
emit problemNameChanged(index.row());
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Qt::ItemFlags OJProblemSetModel::flags(const QModelIndex &index) const
|
|
{
|
|
Qt::ItemFlags flags = Qt::NoItemFlags;
|
|
if (index.isValid()) {
|
|
flags = Qt::ItemIsEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsDropEnabled;
|
|
} else if (index.row() == -1) {
|
|
// -1 means it's a drop target?
|
|
flags = Qt::ItemIsDropEnabled;
|
|
}
|
|
return flags ;
|
|
}
|
|
|
|
Qt::DropActions OJProblemSetModel::supportedDropActions() const
|
|
{
|
|
return Qt::DropAction::MoveAction;
|
|
}
|
|
|
|
bool OJProblemSetModel::moveRows(const QModelIndex &/*sourceParent*/, int sourceRow, int count, const QModelIndex &/*destinationParent*/, int destinationChild)
|
|
{
|
|
if (sourceRow < 0
|
|
|| sourceRow + count - 1 >= mProblemSet.problems.count()
|
|
|| destinationChild < 0
|
|
|| destinationChild > mProblemSet.problems.count()
|
|
|| sourceRow == destinationChild
|
|
|| count <= 0) {
|
|
return false;
|
|
}
|
|
if (!beginMoveRows(QModelIndex(), sourceRow, sourceRow + count - 1, QModelIndex(), destinationChild))
|
|
return false;
|
|
|
|
int fromRow = sourceRow;
|
|
if (destinationChild < sourceRow)
|
|
fromRow += count - 1;
|
|
else
|
|
destinationChild--;
|
|
while (count--)
|
|
mProblemSet.problems.move(fromRow, destinationChild);
|
|
endMoveRows();
|
|
return true;
|
|
}
|
|
|
|
OJProblemModel::OJProblemModel(QObject *parent): QAbstractTableModel(parent)
|
|
{
|
|
|
|
}
|
|
|
|
const POJProblem &OJProblemModel::problem() const
|
|
{
|
|
return mProblem;
|
|
}
|
|
|
|
void OJProblemModel::setProblem(const POJProblem &newProblem)
|
|
{
|
|
if (newProblem!=mProblem) {
|
|
beginResetModel();
|
|
mProblem = newProblem;
|
|
endResetModel();
|
|
}
|
|
}
|
|
|
|
void OJProblemModel::addCase(POJProblemCase problemCase)
|
|
{
|
|
if (mProblem==nullptr)
|
|
return;
|
|
beginInsertRows(QModelIndex(),mProblem->cases.count(),mProblem->cases.count());
|
|
mProblem->cases.append(problemCase);
|
|
endInsertRows();
|
|
}
|
|
|
|
void OJProblemModel::removeCase(int index)
|
|
{
|
|
if (mProblem==nullptr)
|
|
return;
|
|
Q_ASSERT(index >= 0 && index < mProblem->cases.count());
|
|
beginRemoveRows(QModelIndex(),index,index);
|
|
mProblem->cases.removeAt(index);
|
|
endRemoveRows();
|
|
}
|
|
|
|
void OJProblemModel::removeCases()
|
|
{
|
|
beginRemoveRows(QModelIndex(),0,mProblem->cases.count());
|
|
mProblem->cases.clear();
|
|
endRemoveRows();
|
|
}
|
|
|
|
POJProblemCase OJProblemModel::getCase(int index)
|
|
{
|
|
if (mProblem==nullptr)
|
|
return POJProblemCase();
|
|
return mProblem->cases[index];
|
|
}
|
|
|
|
POJProblemCase OJProblemModel::getCaseById(const QString& id)
|
|
{
|
|
if (mProblem==nullptr)
|
|
return POJProblemCase();
|
|
foreach (const POJProblemCase& problemCase, mProblem->cases){
|
|
if (problemCase->getId() == id)
|
|
return problemCase;
|
|
}
|
|
return POJProblemCase();
|
|
}
|
|
|
|
int OJProblemModel::getCaseIndexById(const QString &id)
|
|
{
|
|
if (mProblem==nullptr)
|
|
return -1;
|
|
for (int i=0;i<mProblem->cases.size();i++) {
|
|
const POJProblemCase& problemCase = mProblem->cases[i];
|
|
if (problemCase->getId() == id)
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
void OJProblemModel::clear()
|
|
{
|
|
if (mProblem==nullptr)
|
|
return;
|
|
beginResetModel();
|
|
mProblem->cases.clear();
|
|
endResetModel();
|
|
}
|
|
|
|
int OJProblemModel::count()
|
|
{
|
|
if (mProblem == nullptr)
|
|
return 0;
|
|
return mProblem->cases.count();
|
|
}
|
|
|
|
void OJProblemModel::update(int row)
|
|
{
|
|
emit dataChanged(index(row,0),index(row,0));
|
|
}
|
|
|
|
QString OJProblemModel::getTitle()
|
|
{
|
|
if (!mProblem)
|
|
return "";
|
|
int total = mProblem->cases.count();
|
|
int passed = 0;
|
|
foreach (const POJProblemCase& problemCase, mProblem->cases) {
|
|
if (problemCase->testState == ProblemCaseTestState::Passed)
|
|
passed ++ ;
|
|
}
|
|
QString title = QString("%1 (%2/%3)").arg(mProblem->name)
|
|
.arg(passed).arg(total);
|
|
if (!mProblem->url.isEmpty()) {
|
|
title = QString("<a href=\"%1\">%2</a>").arg(mProblem->url,title);
|
|
}
|
|
return title;
|
|
}
|
|
|
|
QString OJProblemModel::getTooltip()
|
|
{
|
|
if (!mProblem)
|
|
return "";
|
|
QString s;
|
|
s=QString("<h3>%1</h3>").arg(mProblem->name);
|
|
if (!mProblem->description.isEmpty())
|
|
s+=QString("<p>%1</p>")
|
|
.arg(mProblem->description);
|
|
return s;
|
|
}
|
|
|
|
int OJProblemModel::rowCount(const QModelIndex &) const
|
|
{
|
|
if (mProblem==nullptr)
|
|
return 0;
|
|
return mProblem->cases.count();
|
|
}
|
|
|
|
QVariant OJProblemModel::data(const QModelIndex &index, int role) const
|
|
{
|
|
if (!index.isValid())
|
|
return QVariant();
|
|
if (mProblem==nullptr)
|
|
return QVariant();
|
|
switch (index.column()) {
|
|
case 0:
|
|
if (role == Qt::DisplayRole || role == Qt::EditRole) {
|
|
POJProblemCase problemCase = mProblem->cases[index.row()];
|
|
return problemCase->name;
|
|
} else if (role == Qt::DecorationRole) {
|
|
switch (mProblem->cases[index.row()]->testState) {
|
|
case ProblemCaseTestState::Failed:
|
|
return pIconsManager->getIcon(IconsManager::ACTION_PROBLEM_FALIED);
|
|
case ProblemCaseTestState::Passed:
|
|
return pIconsManager->getIcon(IconsManager::ACTION_PROBLEM_PASSED);
|
|
case ProblemCaseTestState::Testing:
|
|
return pIconsManager->getIcon(IconsManager::ACTION_PROBLEM_TESTING);
|
|
default:
|
|
return QVariant();
|
|
}
|
|
}
|
|
break;
|
|
case 1:
|
|
if (role == Qt::DisplayRole) {
|
|
POJProblemCase problemCase = mProblem->cases[index.row()];
|
|
if (problemCase->testState == ProblemCaseTestState::Passed
|
|
|| problemCase->testState == ProblemCaseTestState::Failed)
|
|
return problemCase->runningTime;
|
|
else
|
|
return "";
|
|
}
|
|
break;
|
|
#ifdef Q_OS_WIN
|
|
case 2:
|
|
if (role == Qt::DisplayRole) {
|
|
POJProblemCase problemCase = mProblem->cases[index.row()];
|
|
if (problemCase->testState == ProblemCaseTestState::Passed
|
|
|| problemCase->testState == ProblemCaseTestState::Failed)
|
|
return problemCase->runningMemory/1024;
|
|
else
|
|
return "";
|
|
}
|
|
break;
|
|
#endif
|
|
}
|
|
|
|
return QVariant();
|
|
}
|
|
|
|
bool OJProblemModel::setData(const QModelIndex &index, const QVariant &value, int role)
|
|
{
|
|
if (!index.isValid())
|
|
return false;
|
|
if (index.column()!=0)
|
|
return false;
|
|
if (mProblem==nullptr)
|
|
return false;
|
|
if (role == Qt::EditRole ) {
|
|
QString s = value.toString();
|
|
if (!s.isEmpty()) {
|
|
mProblem->cases[index.row()]->name = s;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Qt::ItemFlags OJProblemModel::flags(const QModelIndex &idx) const
|
|
{
|
|
Qt::ItemFlags flags=Qt::ItemIsEnabled | Qt::ItemIsSelectable;
|
|
if (idx.column()==0)
|
|
flags |= Qt::ItemIsEditable ;
|
|
if (idx.isValid())
|
|
flags |= Qt::ItemIsDragEnabled;
|
|
flags |= Qt::ItemIsDropEnabled;
|
|
return flags;
|
|
}
|
|
|
|
int OJProblemModel::columnCount(const QModelIndex &/*parent*/) const
|
|
{
|
|
#ifdef Q_OS_WIN
|
|
return 3;
|
|
#else
|
|
return 2;
|
|
#endif
|
|
}
|
|
|
|
QVariant OJProblemModel::headerData(int section, Qt::Orientation orientation, int role) const
|
|
{
|
|
if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
|
|
switch (section) {
|
|
case 0:
|
|
return tr("Name");
|
|
case 1:
|
|
return tr("Time(ms)");
|
|
#ifdef Q_OS_WIN
|
|
case 2:
|
|
return tr("Memory(kb)");
|
|
#endif
|
|
}
|
|
}
|
|
return QVariant();
|
|
}
|
|
|
|
Qt::DropActions OJProblemModel::supportedDropActions() const
|
|
{
|
|
return Qt::DropAction::MoveAction;
|
|
}
|
|
|
|
bool OJProblemModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int /* column */, const QModelIndex &parent)
|
|
{
|
|
mMoveTargetRow=row;
|
|
if (mMoveTargetRow==-1)
|
|
mMoveTargetRow=mProblem->cases.length();
|
|
return QAbstractTableModel::dropMimeData(data,action,row,0,parent);
|
|
}
|
|
|
|
bool OJProblemModel::insertRows(int /* row */, int /*count*/, const QModelIndex &/*parent*/)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
bool OJProblemModel::removeRows(int row, int count, const QModelIndex &/*parent*/)
|
|
{
|
|
int sourceRow = row;
|
|
int destinationChild = mMoveTargetRow;
|
|
mMoveTargetRow=-1;
|
|
if (sourceRow < 0
|
|
|| sourceRow + count - 1 >= mProblem->cases.count()
|
|
|| destinationChild < 0
|
|
|| destinationChild > mProblem->cases.count()
|
|
|| sourceRow == destinationChild
|
|
|| count <= 0) {
|
|
return false;
|
|
}
|
|
if (!beginMoveRows(QModelIndex(), sourceRow, sourceRow + count - 1, QModelIndex(), destinationChild))
|
|
return false;
|
|
|
|
int fromRow = sourceRow;
|
|
if (destinationChild < sourceRow)
|
|
fromRow += count - 1;
|
|
else
|
|
destinationChild--;
|
|
while (count--)
|
|
mProblem->cases.move(fromRow, destinationChild);
|
|
endMoveRows();
|
|
return true;
|
|
}
|
|
|