File indexing completed on 2024-04-21 03:50:56

0001 /*************************************************************************
0002  *  Copyright (C) 2020 by Caio Jordão Carvalho <caiojcarvalho@gmail.com> *
0003  *                                                                       *
0004  *  This program is free software; you can redistribute it and/or        *
0005  *  modify it under the terms of the GNU General Public License as       *
0006  *  published by the Free Software Foundation; either version 3 of       *
0007  *  the License, or (at your option) any later version.                  *
0008  *                                                                       *
0009  *  This program is distributed in the hope that it will be useful,      *
0010  *  but WITHOUT ANY WARRANTY; without even the implied warranty of       *
0011  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the        *
0012  *  GNU General Public License for more details.                         *
0013  *                                                                       *
0014  *  You should have received a copy of the GNU General Public License    *
0015  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.*
0016  *************************************************************************/
0017 
0018 #include "image/imagepainter.h"
0019 #include "ui/container.h"
0020 #include "ui/mark.h"
0021 #include "ui/ui_mark.h"
0022 #include "util/fileutils.h"
0023 
0024 #include <QAction>
0025 #include <QActionGroup>
0026 #include <QColorDialog>
0027 #include <QDir>
0028 #include <QFileDialog>
0029 #include <QFontMetrics>
0030 #include <QGraphicsItem>
0031 #include <QGraphicsPixmapItem>
0032 #include <QKeySequence>
0033 #include <QMessageBox>
0034 #include <QRandomGenerator>
0035 #include <QShortcut>
0036 #include <QToolButton>
0037 #include <QtGlobal>
0038 #include <QtConcurrent/QtConcurrentRun>
0039 
0040 #include <KActionCollection>
0041 #include <QFutureWatcher>
0042 
0043 static QDir markTempDirectory()
0044 {
0045     return QDir(QDir::tempPath().append("/mark"));
0046 }
0047 
0048 marK::marK(QWidget *parent) :
0049     KXmlGuiWindow(parent),
0050     m_ui(new Ui::marK),
0051     m_watcher(new QFileSystemWatcher(this)),
0052     m_currentDirectory(QDir::currentPath()),
0053     m_filepath(""),
0054     m_autoSaveType(Serializer::OutputType::None)
0055 {
0056     m_ui->setupUi(this);
0057     m_ui->listLabel->setText(m_currentDirectory);
0058     m_ui->listLabel->setToolTip(m_currentDirectory);
0059     m_ui->containerWidget->setMinimumSize(860, 650);
0060 
0061     setupActions();
0062     setupConnections();
0063     setupGUI(Default);
0064 
0065     updateFiles();
0066     addNewClass();
0067 
0068     if (!markTempDirectory().exists())
0069         markTempDirectory().mkpath(".");
0070 }
0071 
0072 void marK::setupActions()
0073 {
0074     QAction *openDirAction = new QAction(this);
0075     openDirAction->setText("Open Directory");
0076     openDirAction->setIcon(QIcon::fromTheme("document-open"));
0077     actionCollection()->setDefaultShortcut(openDirAction, Qt::Modifier::CTRL + Qt::Key::Key_O);
0078     actionCollection()->addAction("openDirectory", openDirAction);
0079     connect(openDirAction, &QAction::triggered, this, &marK::changeDirectory);
0080 
0081     QAction *importData = new QAction(this);
0082     importData->setText("Import");
0083     actionCollection()->setDefaultShortcut(importData, Qt::Modifier::CTRL + Qt::Key::Key_I);
0084     actionCollection()->addAction("importData", importData);
0085     connect(importData, &QAction::triggered, this, &marK::importData);
0086 
0087     QAction *toXML = new QAction(this);
0088     toXML->setText("XML");
0089     actionCollection()->addAction("toXML", toXML);
0090     connect(toXML, &QAction::triggered, [&](){ saveObjects(Serializer::OutputType::XML); });
0091 
0092     QAction *toJson = new QAction(this);
0093     toJson->setText("JSON");
0094     actionCollection()->addAction("toJSON", toJson);
0095     connect(toJson, &QAction::triggered, [&](){ saveObjects(Serializer::OutputType::JSON); });
0096 
0097     QAction *undoAction = new QAction(this);
0098     undoAction->setText("Undo");
0099     actionCollection()->setDefaultShortcut(undoAction, Qt::Modifier::CTRL + Qt::Key::Key_Z);
0100     actionCollection()->addAction("undo", undoAction);
0101     connect(undoAction, &QAction::triggered, m_ui->containerWidget, &Container::undo);
0102 
0103     QAction *deleteAction  = new QAction(this);
0104     deleteAction->setText("Delete");
0105     actionCollection()->setDefaultShortcut(deleteAction, Qt::Modifier::CTRL + Qt::Key::Key_D);
0106     actionCollection()->addAction("delete", deleteAction);
0107     connect(deleteAction, &QAction::triggered, m_ui->containerWidget, &Container::deleteObject);
0108 
0109     QActionGroup *autoSaveActionGroup = new QActionGroup(this);
0110 
0111     QAction *autoSaveJsonButton = new QAction(this);
0112     autoSaveJsonButton->setText("JSON");
0113     autoSaveJsonButton->setCheckable(true);
0114     autoSaveJsonButton->setActionGroup(autoSaveActionGroup);
0115     actionCollection()->addAction("autosaveJSON", autoSaveJsonButton);
0116     connect(autoSaveJsonButton, &QAction::triggered, this, &marK::toggleAutoSave);
0117 
0118     QAction *autoSaveXmlButton = new QAction(this);
0119     autoSaveXmlButton->setText("XML");
0120     autoSaveXmlButton->setCheckable(true);
0121     autoSaveXmlButton->setActionGroup(autoSaveActionGroup);
0122     actionCollection()->addAction("autosaveXML", autoSaveXmlButton);
0123     connect(autoSaveXmlButton, &QAction::triggered, this, &marK::toggleAutoSave);
0124 
0125     QAction *autoSaveDisabledButton = new QAction(this);
0126     autoSaveDisabledButton->setText("Disabled");
0127     autoSaveDisabledButton->setCheckable(true);
0128     autoSaveDisabledButton->setChecked(true);
0129     autoSaveDisabledButton->setActionGroup(autoSaveActionGroup);
0130     actionCollection()->addAction("autosaveDisabled", autoSaveDisabledButton);
0131     connect(autoSaveDisabledButton, &QAction::triggered, this, &marK::toggleAutoSave);
0132 
0133     QShortcut *nextItemShortcut = new QShortcut(this);
0134     nextItemShortcut->setKey(Qt::Key_Down);
0135     connect(nextItemShortcut, &QShortcut::activated, [&](){ changeIndex(1); });
0136 
0137     QShortcut *previousItemShortcut = new QShortcut(this);
0138     previousItemShortcut->setKey(Qt::Key_Up);
0139     connect(previousItemShortcut, &QShortcut::activated, [&]() { changeIndex(-1); });
0140 
0141     QShortcut *zoomInShortcut = new QShortcut(this);
0142     zoomInShortcut->setKey(Qt::Modifier::CTRL + Qt::Key::Key_Equal);
0143     connect(zoomInShortcut, &QShortcut::activated, [&]() {
0144         ImagePainter *painter = dynamic_cast<ImagePainter*>(m_ui->containerWidget->painter());
0145         if (painter != nullptr)
0146             painter->zoomIn();
0147     });
0148 
0149     QShortcut *zoomOutShortcut = new QShortcut(this);
0150     zoomOutShortcut->setKey(Qt::Modifier::CTRL + Qt::Key::Key_Minus);
0151     connect(zoomOutShortcut, &QShortcut::activated, [&]() {
0152         ImagePainter *painter = dynamic_cast<ImagePainter*>(m_ui->containerWidget->painter());
0153         if (painter != nullptr)
0154             painter->zoomOut();
0155     });
0156 }
0157 
0158 void marK::setupConnections()
0159 {
0160     m_ui->newClassButton->setEnabled(false);
0161     m_ui->undoButton->setEnabled(false);
0162     m_ui->deleteButton->setEnabled(false);
0163     m_ui->resetButton->setEnabled(false);
0164     m_ui->comboBox->setEnabled(false);
0165     m_ui->selectClassColorButton->setEnabled(false);
0166     m_ui->polygonButton->setEnabled(false);
0167     m_ui->rectButton->setEnabled(false);
0168 
0169     KStandardAction::quit(qApp, SLOT(quit()), actionCollection());
0170 
0171     connect(m_ui->listWidget, &QListWidget::currentItemChanged, this,
0172             qOverload<QListWidgetItem*>(&marK::changeItem));
0173 
0174     connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &marK::updateFiles);
0175 
0176     connect(m_ui->newClassButton, &QPushButton::clicked, this, qOverload<>(&marK::addNewClass));
0177 
0178     connect(m_ui->undoButton, &QPushButton::clicked, m_ui->containerWidget, &Container::undo);
0179     connect(m_ui->deleteButton, &QPushButton::clicked, m_ui->containerWidget, &Container::deleteObject);
0180     connect(m_ui->resetButton, &QPushButton::clicked, m_ui->containerWidget, &Container::reset);
0181 
0182     connect(m_ui->comboBox, &QComboBox::editTextChanged, this, 
0183         [&](const QString & text) {
0184             if (m_ui->comboBox->count() == 0) return;
0185 
0186             m_ui->comboBox->setItemText(m_ui->comboBox->currentIndex(), text);
0187             m_objClasses[m_ui->comboBox->currentIndex()]->setName(text);
0188         }
0189     );
0190 
0191     connect(m_ui->comboBox, QOverload<int>::of(&QComboBox::activated), this, 
0192         [&](int index) {
0193             m_ui->containerWidget->setObjClass(m_objClasses[index]);
0194         }
0195     );
0196 
0197     connect(m_ui->selectClassColorButton, &QPushButton::clicked, this, &marK::selectClassColor);
0198 
0199     connect(m_ui->polygonButton, &QPushButton::clicked, this,
0200         [&](bool checked) {
0201         // probably temporary, made this so Shape can be in imagecontainer
0202             auto polygonShape = ImagePainter::Shape::Polygon;
0203             ImagePainter* imgPainter = dynamic_cast<ImagePainter*>(m_ui->containerWidget->painter());
0204             if (imgPainter != nullptr)
0205                 imgPainter->setShape(polygonShape);
0206         }
0207     );
0208 
0209     connect(m_ui->rectButton, &QPushButton::clicked, this,
0210         [&](bool checked) {
0211             auto rectangleShape = ImagePainter::Shape::Rectangle;
0212             ImagePainter* imgPainter = dynamic_cast<ImagePainter*>(m_ui->containerWidget->painter());
0213             if (imgPainter != nullptr)
0214                 imgPainter->setShape(rectangleShape);
0215         }
0216     );
0217 
0218     connect(m_ui->containerWidget, &Container::painterChanged, this,
0219         [&](Container::PainterType painterType) {
0220             m_objClasses.clear();
0221             m_ui->comboBox->clear();
0222             addNewClass();
0223             bool isFileLoaded = painterType != Container::PainterType::None;
0224             m_ui->newClassButton->setEnabled(isFileLoaded);
0225             m_ui->undoButton->setEnabled(isFileLoaded);
0226             m_ui->resetButton->setEnabled(isFileLoaded);
0227             m_ui->comboBox->setEnabled(isFileLoaded);
0228             m_ui->selectClassColorButton->setEnabled(isFileLoaded);
0229             m_ui->polygonButton->setEnabled(isFileLoaded);
0230             m_ui->rectButton->setEnabled(isFileLoaded);
0231             m_ui->deleteButton->setEnabled(isFileLoaded);
0232             if (painterType == Container::PainterType::Image || !isFileLoaded)
0233                 m_ui->groupBox_2->setVisible(true);
0234             else
0235                 m_ui->groupBox_2->setVisible(false);
0236         }
0237     );
0238 
0239     connect(m_ui->containerWidget, &Container::savedObjectsChanged, this, &marK::autoSave);
0240 }
0241 
0242 void marK::updateFiles()
0243 {
0244     int index = m_ui->listWidget->currentRow();
0245     m_ui->listWidget->clear();
0246 
0247     QDir resDirectory(m_currentDirectory);
0248     QStringList items = resDirectory.entryList(QDir::Files);
0249     // removing json and xml files that are resulted from autosave
0250     for (const QString &item : qAsConst(items)) {
0251         items.removeOne(FileUtils::placeSuffix(item, Serializer::OutputType::JSON));
0252         items.removeOne(FileUtils::placeSuffix(item, Serializer::OutputType::XML));
0253     }
0254 
0255     for (const QString &item : qAsConst(items)) {
0256         if (FileUtils::isTextFile(item) || FileUtils::isImageFile(item)) {
0257             QPixmap item_pix = (FileUtils::isTextFile(item)) ?
0258                 QIcon::fromTheme("document-edit-sign").pixmap(20, 20) :
0259                 QPixmap(resDirectory.filePath(item));
0260 
0261             item_pix = item_pix.scaledToWidth(20);
0262 
0263             QListWidgetItem *itemW = new QListWidgetItem(item_pix, item);
0264             itemW->setToolTip(item);
0265 
0266             m_ui->listWidget->addItem(itemW);
0267         }
0268     }
0269 
0270     m_ui->listWidget->setGridSize(QSize(
0271             m_ui->listWidget->sizeHintForColumn(0),
0272             m_ui->listWidget->sizeHintForRow(0)));
0273 
0274     m_ui->listWidget->setCurrentRow(index);
0275 }
0276 
0277 void marK::changeIndex(const int count)
0278 {
0279     int newIndex = m_ui->listWidget->currentRow() + count;
0280     if (newIndex >= m_ui->listWidget->count())
0281         newIndex = 0;
0282 
0283     else if (newIndex < 0)
0284         newIndex = m_ui->listWidget->count() - 1;
0285 
0286     m_ui->listWidget->setCurrentRow(newIndex);
0287     QListWidgetItem *currentItem = m_ui->listWidget->item(newIndex);
0288     changeItem(currentItem);
0289 }
0290 
0291 void marK::changeItem(QListWidgetItem *item)
0292 {
0293     if (item != nullptr) {
0294         QString itemPath = QDir(m_currentDirectory).filePath(item->text());
0295 
0296         if (itemPath != m_filepath) {
0297             makeTempFile();
0298             m_filepath = itemPath;
0299             m_ui->containerWidget->changeItem(itemPath);
0300             retrieveTempFile();
0301         }
0302     }
0303 }
0304 
0305 void marK::changeDirectory()
0306 {
0307     QString newDirectoryPath = QFileDialog::getExistingDirectory(this, "Select Directory", QDir::homePath(),
0308                                                      QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
0309 
0310     if (m_currentDirectory == newDirectoryPath)
0311         return;
0312 
0313     if (!newDirectoryPath.isEmpty()) {
0314         if (m_currentDirectory != "")
0315             m_watcher->removePath(m_currentDirectory);
0316 
0317         m_currentDirectory = newDirectoryPath;
0318         m_watcher->addPath(m_currentDirectory);
0319         m_ui->listWidget->clear();
0320         m_ui->containerWidget->clear();
0321         m_filepath.clear();
0322         updateFiles();
0323 
0324         QFontMetrics metrics(m_ui->listLabel->font());
0325         QString elidedText = metrics.elidedText(m_currentDirectory, Qt::ElideMiddle,
0326                                                 m_ui->listLabel->maximumWidth() - int(m_ui->listLabel->maximumWidth() * 0.05));
0327 
0328         m_ui->listLabel->setText(elidedText);
0329         m_ui->listLabel->setToolTip(m_currentDirectory);
0330     }
0331 }
0332 
0333 void marK::addNewClass()
0334 {
0335     auto classSize = QString::number(m_objClasses.size() + 1);
0336     MarkedClass* newClass = new MarkedClass(classSize);
0337     m_objClasses << newClass;
0338     
0339     QPixmap colorPix(70, 45);
0340     colorPix.fill(newClass->color());
0341 
0342     m_ui->comboBox->addItem(QIcon(colorPix), newClass->name());
0343     m_ui->comboBox->setCurrentIndex(m_objClasses.size() - 1);
0344 
0345     m_ui->containerWidget->setObjClass(newClass);
0346 }
0347 
0348 void marK::updateComboBox()
0349 {
0350     m_ui->comboBox->clear();
0351 
0352     for (const auto& markedClass : m_objClasses) {
0353         QPixmap colorPix(70, 45);
0354         colorPix.fill(markedClass->color());
0355         m_ui->comboBox->addItem(QIcon(colorPix), markedClass->name());
0356     }
0357 
0358     m_ui->comboBox->setCurrentIndex(m_objClasses.size() - 1);
0359     MarkedClass* currentClass = m_objClasses[m_ui->comboBox->currentIndex()];
0360     m_ui->containerWidget->setObjClass(currentClass);
0361 }
0362 
0363 void marK::selectClassColor()
0364 {
0365     auto rand = QRandomGenerator().global();
0366     QColorDialog colorDialog(QColor(rand->bounded(0, 256), rand->bounded(0, 256), rand->bounded(0, 256)), this);
0367 
0368     if (colorDialog.exec() == QDialog::DialogCode::Accepted) {
0369         QPixmap colorPix(70, 45);
0370         colorPix.fill(colorDialog.selectedColor());
0371         m_ui->comboBox->setItemIcon(m_ui->comboBox->currentIndex(), QIcon(colorPix));
0372 
0373         m_objClasses[m_ui->comboBox->currentIndex()]->setColor(colorDialog.selectedColor());
0374     }
0375 
0376     m_ui->containerWidget->repaint();
0377 }
0378 
0379 void marK::saveObjects(Serializer::OutputType outputType)
0380 {
0381     QString filepath = QFileDialog::getSaveFileName(this, tr("Save File"),
0382                            m_currentDirectory,
0383                            tr(FileUtils::filterString(outputType)));
0384 
0385     if (filepath.isEmpty())
0386         return;
0387 
0388     bool success = Serializer::write(filepath, m_ui->containerWidget->savedObjects(), outputType);
0389 
0390     if (!success) {
0391         QMessageBox msgBox;
0392         msgBox.setText("failed to save annotation");
0393         msgBox.setIcon(QMessageBox::Warning);
0394         msgBox.exec();
0395     }
0396 }
0397 
0398 void marK::importData()
0399 {
0400     if (m_filepath.isEmpty()) return; //exiting because this is no image loaded
0401 
0402     QString filepath = QFileDialog::getOpenFileName(this, "Select File", m_currentDirectory,
0403                                                      "JSON and XML files (*.json *.xml)");
0404 
0405     if (filepath.isEmpty())
0406         return;
0407 
0408     QVector<MarkedObject*> objects = Serializer::read(filepath);
0409 
0410     bool success = m_ui->containerWidget->importObjects(objects);
0411 
0412     if (!success) {
0413         QMessageBox msgBox;
0414         msgBox.setText("failed to load annotation");
0415         msgBox.setIcon(QMessageBox::Warning);
0416         msgBox.exec();
0417         return;
0418     }
0419 
0420     m_objClasses.clear();
0421     for (MarkedObject *obj : objects) {
0422         if (!m_objClasses.contains(obj->objClass()))
0423             m_objClasses << obj->objClass();
0424     }
0425     updateComboBox();
0426 }
0427 
0428 void marK::retrieveTempFile()
0429 {
0430     QString tempFilePath = markTempDirectory().filePath(QString(m_filepath).replace("/", "_"));
0431     tempFilePath = FileUtils::placeSuffix(tempFilePath, Serializer::OutputType::JSON);
0432 
0433     QVector<MarkedObject*> objects = Serializer::read(tempFilePath);
0434 
0435     bool success = m_ui->containerWidget->importObjects(objects);
0436 
0437     if (!success)
0438         return;
0439 
0440     m_objClasses.clear();
0441     for (MarkedObject *obj : objects) {
0442         if (!m_objClasses.contains(obj->objClass()))
0443             m_objClasses << obj->objClass();
0444     }
0445 
0446     updateComboBox();
0447 
0448     m_ui->containerWidget->repaint();
0449 }
0450 
0451 void marK::makeTempFile()
0452 {
0453     QString tempFilePath = markTempDirectory().filePath(QString(m_filepath).replace("/", "_"));
0454 
0455     Serializer::write(tempFilePath, m_ui->containerWidget->savedObjects(), Serializer::OutputType::JSON);
0456 }
0457 
0458 void marK::toggleAutoSave()
0459 {
0460     QAction *button = qobject_cast<QAction*>(sender());
0461     QString type = button->text();
0462 
0463     if (type == "&Disabled")
0464         m_autoSaveType = Serializer::OutputType::None;
0465 
0466     else if (type == "&XML")
0467         m_autoSaveType = Serializer::OutputType::XML;
0468 
0469     else if (type == "&JSON")
0470         m_autoSaveType = Serializer::OutputType::JSON;
0471 }
0472 
0473 void marK::autoSave()
0474 {
0475     if (m_autoSaveType == Serializer::OutputType::None)
0476         return;
0477 
0478     auto *watcher = new QFutureWatcher<bool>();
0479     connect(watcher, &QFutureWatcher<bool>::finished, this, [watcher]() {
0480         bool success = watcher->result();
0481         delete watcher;
0482 
0483         if (!success) {
0484             QMessageBox msgBox;
0485             msgBox.setText("Failed to auto-save annotation.");
0486             msgBox.setIcon(QMessageBox::Warning);
0487             msgBox.exec();
0488         }
0489     });
0490 
0491     QFuture<bool> future = QtConcurrent::run(
0492         Serializer::write, m_filepath, m_ui->containerWidget->savedObjects(), m_autoSaveType
0493     );
0494     watcher->setFuture(future);
0495 }
0496 
0497 marK::~marK()
0498 {
0499     markTempDirectory().removeRecursively();
0500 }
0501 
0502 #include "moc_mark.cpp"