1/*
2 * Copyright (C) 2020-2022 Roy Qu (royqh1979@gmail.com)
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17#include "ojproblemsetmodel.h"
18
19#include <QFile>
20#include <QIcon>
21#include <QJsonArray>
22#include <QJsonDocument>
23#include <QJsonObject>
24#include <QMimeData>
25#include "../utils.h"
26#include "../iconsmanager.h"
27#include "../systemconsts.h"
28
29OJProblemSetModel::OJProblemSetModel(QObject *parent) : QAbstractListModel(parent)
30{
31
32}
33
34void OJProblemSetModel::clear()
35{
36 beginResetModel();
37 mProblemSet.problems.clear();
38 mProblemSet.exportFilename.clear();
39 endResetModel();
40}
41
42int OJProblemSetModel::count()
43{
44 return mProblemSet.problems.count();
45}
46
47void OJProblemSetModel::create(const QString& name)
48{
49 mProblemSet.name = name;
50 clear();
51}
52
53void OJProblemSetModel::rename(const QString &newName)
54{
55 if (mProblemSet.name!=newName)
56 mProblemSet.name = newName;
57}
58
59QString OJProblemSetModel::name() const
60{
61 return mProblemSet.name;
62}
63
64QString OJProblemSetModel::exportFilename() const
65{
66 return mProblemSet.exportFilename;
67}
68
69void OJProblemSetModel::addProblem(POJProblem problem)
70{
71 beginInsertRows(QModelIndex(), mProblemSet.problems.count(), mProblemSet.problems.count());
72 mProblemSet.problems.append(problem);
73 endInsertRows();
74}
75
76POJProblem OJProblemSetModel::problem(int index)
77{
78 return mProblemSet.problems[index];
79}
80
81void OJProblemSetModel::removeProblem(int index)
82{
83 Q_ASSERT(index>=0 && index < mProblemSet.problems.count());
84 beginRemoveRows(QModelIndex(),index,index);
85 mProblemSet.problems.removeAt(index);
86 endRemoveRows();
87}
88
89bool OJProblemSetModel::problemNameUsed(const QString &name)
90{
91 foreach (const POJProblem& problem, mProblemSet.problems) {
92 if (name == problem->name)
93 return true;
94 }
95 return false;
96}
97
98void OJProblemSetModel::removeAllProblems()
99{
100 clear();
101}
102
103void OJProblemSetModel::saveToFile(const QString &fileName)
104{
105 QFile file(fileName);
106 if (file.open(QFile::WriteOnly | QFile::Truncate)) {
107 QJsonObject obj;
108 mProblemSet.exportFilename=fileName;
109 obj["name"]=mProblemSet.name;
110 QJsonArray problemsArray;
111 foreach (const POJProblem& problem, mProblemSet.problems) {
112 QJsonObject problemObj;
113 problemObj["name"]=problem->name;
114 problemObj["url"]=problem->url;
115 problemObj["description"]=problem->description;
116 if (fileExists(problem->answerProgram))
117 problemObj["answer_program"] = problem->answerProgram;
118 QJsonArray cases;
119 foreach (const POJProblemCase& problemCase, problem->cases) {
120 QJsonObject caseObj;
121 caseObj["name"]=problemCase->name;
122 caseObj["input"]=problemCase->input;
123 QString path = problemCase->inputFileName;
124 QString prefix = includeTrailingPathDelimiter(extractFileDir(fileName));
125 if (path.startsWith(prefix, PATH_SENSITIVITY)) {
126 path = "%ProblemSetPath%/"+ path.mid(prefix.length());
127 }
128 caseObj["input_filename"]=path;
129 path = problemCase->expectedOutputFileName;
130 if (path.startsWith(prefix, PATH_SENSITIVITY)) {
131 path = "%ProblemSetPath%/"+ path.mid(prefix.length());
132 }
133 caseObj["expected_output_filename"]=path;
134 caseObj["expected"]=problemCase->expected;
135 cases.append(caseObj);
136 }
137 problemObj["cases"]=cases;
138 problemsArray.append(problemObj);
139 }
140 obj["problems"]=problemsArray;
141 QJsonDocument doc;
142 doc.setObject(obj);
143 file.write(doc.toJson());
144 file.close();
145 } else {
146 throw FileError(QObject::tr("Can't open file '%1' for read.")
147 .arg(fileName));
148 }
149}
150
151void OJProblemSetModel::loadFromFile(const QString &fileName)
152{
153 QFile file(fileName);
154 if (file.open(QFile::ReadOnly)) {
155 QByteArray content = file.readAll();
156 QJsonParseError error;
157 QJsonDocument doc(QJsonDocument::fromJson(content,&error));
158 if (error.error!=QJsonParseError::NoError) {
159 throw FileError(QObject::tr("Can't parse problem set file '%1':%2")
160 .arg(fileName)
161 .arg(error.errorString()));
162 }
163 beginResetModel();
164 QJsonObject obj = doc.object();
165 mProblemSet.name = obj["name"].toString();
166 mProblemSet.problems.clear();
167 QJsonArray problemsArray = obj["problems"].toArray();
168 foreach (const QJsonValue& problemVal, problemsArray) {
169 QJsonObject problemObj = problemVal.toObject();
170 POJProblem problem = std::make_shared<OJProblem>();
171 problem->name = problemObj["name"].toString();
172 problem->url = problemObj["url"].toString();
173 problem->description = problemObj["description"].toString();
174 problem->answerProgram = problemObj["answer_program"].toString();
175 QJsonArray casesArray = problemObj["cases"].toArray();
176 foreach (const QJsonValue& caseVal, casesArray) {
177 QJsonObject caseObj = caseVal.toObject();
178 POJProblemCase problemCase = std::make_shared<OJProblemCase>();
179 problemCase->name = caseObj["name"].toString();
180 problemCase->input = caseObj["input"].toString();
181 problemCase->expected = caseObj["expected"].toString();
182 QString path = caseObj["input_filename"].toString();
183 if (path.startsWith("%ProblemSetPath%/")) {
184 path = includeTrailingPathDelimiter(extractFileDir(fileName))+
185 path.mid(QLatin1String("%ProblemSetPath%/").size());
186 }
187 problemCase->inputFileName=path;
188 path = caseObj["expected_output_filename"].toString();
189 if (path.startsWith("%ProblemSetPath%/")) {
190 path = includeTrailingPathDelimiter(extractFileDir(fileName))+
191 path.mid(QLatin1String("%ProblemSetPath%/").size());
192 }
193 problemCase->expectedOutputFileName=path;
194 problemCase->testState = ProblemCaseTestState::NotTested;
195 problem->cases.append(problemCase);
196 }
197 mProblemSet.problems.append(problem);
198 }
199 endResetModel();
200 } else {
201 throw FileError(QObject::tr("Can't open file '%1' for read.")
202 .arg(fileName));
203 }
204}
205
206void OJProblemSetModel::updateProblemAnswerFilename(const QString &oldFilename, const QString &newFilename)
207{
208 foreach (POJProblem problem, mProblemSet.problems) {
209 if (QString::compare(problem->answerProgram,oldFilename,PATH_SENSITIVITY)==0) {
210 problem->answerProgram = newFilename;
211 }
212 }
213}
214
215int OJProblemSetModel::rowCount(const QModelIndex &) const
216{
217 return mProblemSet.problems.count();
218}
219
220QVariant OJProblemSetModel::data(const QModelIndex &index, int role) const
221{
222 if (!index.isValid())
223 return QVariant();
224 if (role == Qt::DisplayRole || role == Qt::EditRole) {
225 return mProblemSet.problems[index.row()]->name;
226 }
227 return QVariant();
228}
229
230bool OJProblemSetModel::setData(const QModelIndex &index, const QVariant &value, int role)
231{
232 if (!index.isValid())
233 return false;
234 if (role == Qt::EditRole) {
235 QString s = value.toString();
236 if (!s.isEmpty()) {
237 mProblemSet.problems[index.row()]->name = s;
238 emit problemNameChanged(index.row());
239 return true;
240 }
241 }
242 return false;
243}
244
245Qt::ItemFlags OJProblemSetModel::flags(const QModelIndex &index) const
246{
247 Qt::ItemFlags flags = Qt::NoItemFlags;
248 if (index.isValid()) {
249 flags = Qt::ItemIsEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsDropEnabled;
250 } else if (index.row() == -1) {
251 // -1 means it's a drop target?
252 flags = Qt::ItemIsDropEnabled;
253 }
254 return flags ;
255}
256
257Qt::DropActions OJProblemSetModel::supportedDropActions() const
258{
259 return Qt::DropAction::MoveAction;
260}
261
262bool OJProblemSetModel::moveRows(const QModelIndex &sourceParent, int sourceRow, int count, const QModelIndex &destinationParent, int destinationChild)
263{
264 if (sourceRow < 0
265 || sourceRow + count - 1 >= mProblemSet.problems.count()
266 || destinationChild < 0
267 || destinationChild > mProblemSet.problems.count()
268 || sourceRow == destinationChild
269 || count <= 0) {
270 return false;
271 }
272 if (!beginMoveRows(QModelIndex(), sourceRow, sourceRow + count - 1, QModelIndex(), destinationChild))
273 return false;
274
275 int fromRow = sourceRow;
276 if (destinationChild < sourceRow)
277 fromRow += count - 1;
278 else
279 destinationChild--;
280 while (count--)
281 mProblemSet.problems.move(fromRow, destinationChild);
282 endMoveRows();
283 return true;
284}
285
286OJProblemModel::OJProblemModel(QObject *parent): QAbstractTableModel(parent)
287{
288
289}
290
291const POJProblem &OJProblemModel::problem() const
292{
293 return mProblem;
294}
295
296void OJProblemModel::setProblem(const POJProblem &newProblem)
297{
298 if (newProblem!=mProblem) {
299 beginResetModel();
300 mProblem = newProblem;
301 endResetModel();
302 }
303}
304
305void OJProblemModel::addCase(POJProblemCase problemCase)
306{
307 if (mProblem==nullptr)
308 return;
309 beginInsertRows(QModelIndex(),mProblem->cases.count(),mProblem->cases.count());
310 mProblem->cases.append(problemCase);
311 endInsertRows();
312}
313
314void OJProblemModel::removeCase(int index)
315{
316 if (mProblem==nullptr)
317 return;
318 Q_ASSERT(index >= 0 && index < mProblem->cases.count());
319 beginRemoveRows(QModelIndex(),index,index);
320 mProblem->cases.removeAt(index);
321 endRemoveRows();
322}
323
324void OJProblemModel::removeCases()
325{
326 beginRemoveRows(QModelIndex(),0,mProblem->cases.count());
327 mProblem->cases.clear();
328 endRemoveRows();
329}
330
331POJProblemCase OJProblemModel::getCase(int index)
332{
333 if (mProblem==nullptr)
334 return POJProblemCase();
335 return mProblem->cases[index];
336}
337
338POJProblemCase OJProblemModel::getCaseById(const QString& id)
339{
340 if (mProblem==nullptr)
341 return POJProblemCase();
342 foreach (const POJProblemCase& problemCase, mProblem->cases){
343 if (problemCase->getId() == id)
344 return problemCase;
345 }
346 return POJProblemCase();
347}
348
349int OJProblemModel::getCaseIndexById(const QString &id)
350{
351 if (mProblem==nullptr)
352 return -1;
353 for (int i=0;i<mProblem->cases.size();i++) {
354 const POJProblemCase& problemCase = mProblem->cases[i];
355 if (problemCase->getId() == id)
356 return i;
357 }
358 return -1;
359}
360
361void OJProblemModel::clear()
362{
363 if (mProblem==nullptr)
364 return;
365 beginResetModel();
366 mProblem->cases.clear();
367 endResetModel();
368}
369
370int OJProblemModel::count()
371{
372 if (mProblem == nullptr)
373 return 0;
374 return mProblem->cases.count();
375}
376
377void OJProblemModel::update(int row)
378{
379 emit dataChanged(index(row,0),index(row,0));
380}
381
382QString OJProblemModel::getTitle()
383{
384 if (!mProblem)
385 return "";
386 int total = mProblem->cases.count();
387 int passed = 0;
388 foreach (const POJProblemCase& problemCase, mProblem->cases) {
389 if (problemCase->testState == ProblemCaseTestState::Passed)
390 passed ++ ;
391 }
392 QString title = QString("%1 (%2/%3)").arg(mProblem->name)
393 .arg(passed).arg(total);
394 if (!mProblem->url.isEmpty()) {
395 title = QString("<a href=\"%1\">%2</a>").arg(mProblem->url,title);
396 }
397 return title;
398}
399
400QString OJProblemModel::getTooltip()
401{
402 if (!mProblem)
403 return "";
404 QString s;
405 s=QString("<h3>%1</h3>").arg(mProblem->name);
406 if (!mProblem->description.isEmpty())
407 s+=QString("<p>%1</p>")
408 .arg(mProblem->description);
409 return s;
410}
411
412int OJProblemModel::rowCount(const QModelIndex &) const
413{
414 if (mProblem==nullptr)
415 return 0;
416 return mProblem->cases.count();
417}
418
419QVariant OJProblemModel::data(const QModelIndex &index, int role) const
420{
421 if (!index.isValid())
422 return QVariant();
423 if (mProblem==nullptr)
424 return QVariant();
425 switch (index.column()) {
426 case 0:
427 if (role == Qt::DisplayRole || role == Qt::EditRole) {
428 POJProblemCase problemCase = mProblem->cases[index.row()];
429 return problemCase->name;
430 } else if (role == Qt::DecorationRole) {
431 switch (mProblem->cases[index.row()]->testState) {
432 case ProblemCaseTestState::Failed:
433 return pIconsManager->getIcon(IconsManager::ACTION_PROBLEM_FALIED);
434 case ProblemCaseTestState::Passed:
435 return pIconsManager->getIcon(IconsManager::ACTION_PROBLEM_PASSED);
436 case ProblemCaseTestState::Testing:
437 return pIconsManager->getIcon(IconsManager::ACTION_PROBLEM_TESTING);
438 default:
439 return QVariant();
440 }
441 }
442 break;
443 case 1:
444 if (role == Qt::DisplayRole) {
445 POJProblemCase problemCase = mProblem->cases[index.row()];
446 if (problemCase->testState == ProblemCaseTestState::Passed
447 || problemCase->testState == ProblemCaseTestState::Failed)
448 return problemCase->runningTime;
449 else
450 return "";
451 }
452 break;
453 }
454
455 return QVariant();
456}
457
458bool OJProblemModel::setData(const QModelIndex &index, const QVariant &value, int role)
459{
460 if (!index.isValid())
461 return false;
462 if (index.column()!=0)
463 return false;
464 if (mProblem==nullptr)
465 return false;
466 if (role == Qt::EditRole ) {
467 QString s = value.toString();
468 if (!s.isEmpty()) {
469 mProblem->cases[index.row()]->name = s;
470 return true;
471 }
472 }
473 return false;
474}
475
476Qt::ItemFlags OJProblemModel::flags(const QModelIndex &idx) const
477{
478 Qt::ItemFlags flags=Qt::ItemIsEnabled | Qt::ItemIsSelectable;
479 if (idx.column()==0)
480 flags |= Qt::ItemIsEditable ;
481 if (idx.isValid())
482 flags |= Qt::ItemIsDragEnabled;
483 flags |= Qt::ItemIsDropEnabled;
484 return flags;
485}
486
487int OJProblemModel::columnCount(const QModelIndex &/*parent*/) const
488{
489 return 2;
490}
491
492QVariant OJProblemModel::headerData(int section, Qt::Orientation orientation, int role) const
493{
494 if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
495 switch (section) {
496 case 0:
497 return tr("Name");
498 case 1:
499 return tr("Time(ms)");
500 }
501 }
502 return QVariant();
503}
504
505Qt::DropActions OJProblemModel::supportedDropActions() const
506{
507 return Qt::DropAction::MoveAction;
508}
509
510bool OJProblemModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
511{
512 mMoveTargetRow=row;
513 if (mMoveTargetRow==-1)
514 mMoveTargetRow=mProblem->cases.length();
515 return QAbstractTableModel::dropMimeData(data,action,row,0,parent);
516}
517
518bool OJProblemModel::insertRows(int row, int count, const QModelIndex &parent)
519{
520 return true;
521}
522
523bool OJProblemModel::removeRows(int row, int count, const QModelIndex &parent)
524{
525 int sourceRow = row;
526 int destinationChild = mMoveTargetRow;
527 mMoveTargetRow=-1;
528 if (sourceRow < 0
529 || sourceRow + count - 1 >= mProblem->cases.count()
530 || destinationChild < 0
531 || destinationChild > mProblem->cases.count()
532 || sourceRow == destinationChild
533 || count <= 0) {
534 return false;
535 }
536 if (!beginMoveRows(QModelIndex(), sourceRow, sourceRow + count - 1, QModelIndex(), destinationChild))
537 return false;
538
539 int fromRow = sourceRow;
540 if (destinationChild < sourceRow)
541 fromRow += count - 1;
542 else
543 destinationChild--;
544 while (count--)
545 mProblem->cases.move(fromRow, destinationChild);
546 endMoveRows();
547 return true;
548}
549
550