File indexing completed on 2024-04-28 04:20:52
0001 // SPDX-FileCopyrightText: 2014-2020 Tobias Leupold <tl@stonemx.de> 0002 // SPDX-FileCopyrightText: 2014-2023 Johannes Zarl-Zierl <johannes@zarl-zierl.at> 0003 // SPDX-FileCopyrightText: 2017 Jonathan Riddell <jr@jriddell.org> 0004 // SPDX-FileCopyrightText: 2022 Jesper K. Pedersen <jesper.pedersen@kdab.com> 0005 // 0006 // SPDX-License-Identifier: GPL-2.0-or-later 0007 0008 // The basic resizable QFrame has been shamelessly stolen from 0009 // http://qt-project.org/forums/viewthread/24104 0010 // Big thanks to Mr. kripton :-) 0011 0012 #include "ResizableFrame.h" 0013 0014 // Local includes 0015 #include "AreaTagSelectDialog.h" 0016 #include "ImagePreview.h" 0017 #include "ImagePreviewWidget.h" 0018 #include "Logging.h" 0019 0020 // Qt includes 0021 #include <QApplication> 0022 #include <QDockWidget> 0023 #include <QList> 0024 #include <QLoggingCategory> 0025 #include <QMenu> 0026 #include <QMouseEvent> 0027 #include <QTimer> 0028 0029 // KDE includes 0030 #include <KLocalizedString> 0031 #include <KMessageBox> 0032 0033 namespace 0034 { 0035 constexpr int SCALE_TOP = 0b00000001; 0036 constexpr int SCALE_BOTTOM = 0b00000010; 0037 constexpr int SCALE_RIGHT = 0b00000100; 0038 constexpr int SCALE_LEFT = 0b00001000; 0039 constexpr int MOVE = 0b10000000; 0040 } 0041 0042 AnnotationDialog::ResizableFrame::ResizableFrame(QWidget *parent) 0043 : QFrame(parent) 0044 , m_dialog(nullptr) 0045 { 0046 m_preview = dynamic_cast<ImagePreview *>(parent); 0047 Q_ASSERT(m_preview); 0048 m_previewWidget = dynamic_cast<ImagePreviewWidget *>(m_preview->parentWidget()); 0049 Q_ASSERT(m_previewWidget); 0050 0051 setFrameShape(QFrame::Box); 0052 setMouseTracking(true); 0053 0054 m_removeAct = new QAction( 0055 i18nc("area of an image; rectangle that is overlayed upon the image", 0056 "Remove area"), 0057 this); 0058 connect(m_removeAct, &QAction::triggered, this, &ResizableFrame::remove); 0059 0060 m_removeTagAct = new QAction(this); 0061 connect(m_removeTagAct, &QAction::triggered, this, &ResizableFrame::removeTag); 0062 } 0063 0064 AnnotationDialog::ResizableFrame::~ResizableFrame() 0065 { 0066 } 0067 0068 void AnnotationDialog::ResizableFrame::setActualCoordinates(QRect actualCoordinates) 0069 { 0070 m_actualCoordinates = actualCoordinates; 0071 } 0072 0073 QRect AnnotationDialog::ResizableFrame::actualCoordinates() const 0074 { 0075 return m_actualCoordinates; 0076 } 0077 0078 void AnnotationDialog::ResizableFrame::getMinMaxCoordinates() 0079 { 0080 // Get the maximal area to drag or resize the frame 0081 m_minMaxCoordinates = m_preview->minMaxAreaPreview(); 0082 // Add one pixel (width of the frame) 0083 m_minMaxCoordinates.setWidth(m_minMaxCoordinates.width() + 1); 0084 m_minMaxCoordinates.setHeight(m_minMaxCoordinates.height() + 1); 0085 } 0086 0087 void AnnotationDialog::ResizableFrame::mousePressEvent(QMouseEvent *event) 0088 { 0089 if (event->button() == Qt::LeftButton) { 0090 m_dragStartPosition = event->pos(); 0091 m_dragStartGeometry = geometry(); 0092 0093 // Just in case this will be a drag/resize and not just a click 0094 getMinMaxCoordinates(); 0095 } 0096 } 0097 0098 void AnnotationDialog::ResizableFrame::mouseMoveEvent(QMouseEvent *event) 0099 { 0100 static int moveAction = 0; 0101 if (!(event->buttons() & Qt::LeftButton)) { 0102 // No drag, just change the cursor and return 0103 if (event->x() <= 3 && event->y() <= 3) { 0104 moveAction = SCALE_TOP | SCALE_LEFT; 0105 setCursor(Qt::SizeFDiagCursor); 0106 } else if (event->x() <= 3 && event->y() >= height() - 3) { 0107 moveAction = SCALE_BOTTOM | SCALE_LEFT; 0108 setCursor(Qt::SizeBDiagCursor); 0109 } else if (event->x() >= width() - 3 && event->y() <= 3) { 0110 moveAction = SCALE_TOP | SCALE_RIGHT; 0111 setCursor(Qt::SizeBDiagCursor); 0112 } else if (event->x() >= width() - 3 && event->y() >= height() - 3) { 0113 moveAction = SCALE_BOTTOM | SCALE_RIGHT; 0114 setCursor(Qt::SizeFDiagCursor); 0115 } else if (event->x() <= 3) { 0116 moveAction = SCALE_LEFT; 0117 setCursor(Qt::SizeHorCursor); 0118 } else if (event->x() >= width() - 3) { 0119 moveAction = SCALE_RIGHT; 0120 setCursor(Qt::SizeHorCursor); 0121 } else if (event->y() <= 3) { 0122 moveAction = SCALE_TOP; 0123 setCursor(Qt::SizeVerCursor); 0124 } else if (event->y() >= height() - 3) { 0125 moveAction = SCALE_BOTTOM; 0126 setCursor(Qt::SizeVerCursor); 0127 } else { 0128 moveAction = MOVE; 0129 setCursor(Qt::SizeAllCursor); 0130 } 0131 return; 0132 } 0133 0134 int x; 0135 int y; 0136 int w; 0137 int h; 0138 0139 h = height(); 0140 0141 if (moveAction & MOVE) { 0142 x = m_dragStartGeometry.left() - (m_dragStartPosition.x() - event->x()); 0143 y = m_dragStartGeometry.top() - (m_dragStartPosition.y() - event->y()); 0144 w = width(); 0145 0146 // Be sure not to move out of the preview 0147 if (x < m_minMaxCoordinates.left()) { 0148 x = m_minMaxCoordinates.left(); 0149 } 0150 if (y < m_minMaxCoordinates.top()) { 0151 y = m_minMaxCoordinates.top(); 0152 } 0153 if (x + w > m_minMaxCoordinates.width()) { 0154 x = m_minMaxCoordinates.width() - w; 0155 } 0156 if (y + h > m_minMaxCoordinates.height()) { 0157 y = m_minMaxCoordinates.height() - h; 0158 } 0159 } else { 0160 // initialize with the "missing" values when only one direction is manipulated: 0161 x = m_dragStartGeometry.left(); 0162 y = m_dragStartGeometry.top(); 0163 w = m_dragStartGeometry.width(); 0164 0165 if (moveAction & SCALE_TOP) { 0166 y = m_dragStartGeometry.top() - (m_dragStartPosition.y() - event->y()); 0167 0168 if (y >= geometry().y() + geometry().height()) { 0169 y = m_dragStartGeometry.top() + m_dragStartGeometry.height(); 0170 moveAction ^= SCALE_BOTTOM | SCALE_TOP; 0171 } 0172 0173 if (y < m_minMaxCoordinates.top()) { 0174 y = m_minMaxCoordinates.top(); 0175 h = m_dragStartGeometry.top() + m_dragStartGeometry.height() - m_minMaxCoordinates.y(); 0176 } else { 0177 h = height() + (m_dragStartPosition.y() - event->y()); 0178 } 0179 } else if (moveAction & SCALE_BOTTOM) { 0180 y = m_dragStartGeometry.top(); 0181 h = event->y(); 0182 0183 if (h <= 0) { 0184 h = 0; 0185 m_dragStartPosition.setY(0); 0186 moveAction ^= SCALE_BOTTOM | SCALE_TOP; 0187 } 0188 0189 if (y + h > m_minMaxCoordinates.height()) { 0190 h = m_minMaxCoordinates.height() - y; 0191 } 0192 } 0193 0194 if (moveAction & SCALE_RIGHT) { 0195 x = m_dragStartGeometry.left(); 0196 w = event->x(); 0197 0198 if (w <= 0) { 0199 w = 0; 0200 m_dragStartPosition.setX(0); 0201 moveAction ^= SCALE_RIGHT | SCALE_LEFT; 0202 } 0203 0204 if (x + w > m_minMaxCoordinates.width()) { 0205 w = m_minMaxCoordinates.width() - x; 0206 } 0207 } else if (moveAction & SCALE_LEFT) { 0208 x = m_dragStartGeometry.left() - (m_dragStartPosition.x() - event->x()); 0209 0210 if (x >= geometry().left() + geometry().width()) { 0211 x = m_dragStartGeometry.left() + m_dragStartGeometry.width(); 0212 moveAction ^= SCALE_RIGHT | SCALE_LEFT; 0213 } 0214 0215 if (x < m_minMaxCoordinates.left()) { 0216 x = m_minMaxCoordinates.left(); 0217 w = m_dragStartGeometry.x() + m_dragStartGeometry.width() - m_minMaxCoordinates.x(); 0218 } else { 0219 w = m_dragStartGeometry.width() + (m_dragStartPosition.x() - event->x()); 0220 } 0221 } 0222 } 0223 setGeometry(x, y, w, h); 0224 m_dragStartGeometry = geometry(); 0225 0226 if (!m_tagData.first.isEmpty()) { 0227 // If we change an area with an associated tag: 0228 // tell the Dialog we made a change that should be saved (set the dirty marker) 0229 m_dialog->areaChanged(); 0230 } 0231 } 0232 0233 void AnnotationDialog::ResizableFrame::checkGeometry() 0234 { 0235 // If this is called when the area is created, we don't have it yet 0236 getMinMaxCoordinates(); 0237 0238 // First cache the current geometry 0239 int x; 0240 int y; 0241 int w; 0242 int h; 0243 x = geometry().x(); 0244 y = geometry().y(); 0245 w = geometry().width(); 0246 h = geometry().height(); 0247 0248 // Be sure no non-visible area is created by resizing to a height or width of 0 0249 // A height and width of 3 is the minimum to have more than a line 0250 if (geometry().height() < 3) { 0251 y = y - 1; 0252 h = 3; 0253 } 0254 if (geometry().width() < 3) { 0255 x = x - 1; 0256 w = 3; 0257 } 0258 0259 // Probably, the above tweaking moved the area out of the preview area 0260 if (x < m_minMaxCoordinates.left()) { 0261 x = m_minMaxCoordinates.left(); 0262 } 0263 if (y < m_minMaxCoordinates.top()) { 0264 y = m_minMaxCoordinates.top(); 0265 } 0266 if (x + w > m_minMaxCoordinates.width()) { 0267 x = m_minMaxCoordinates.width() - w; 0268 } 0269 if (y + h > m_minMaxCoordinates.height()) { 0270 y = m_minMaxCoordinates.height() - h; 0271 } 0272 0273 // If anything has been changed, set the updated geometry 0274 if (geometry().x() != x 0275 || geometry().y() != y 0276 || geometry().width() != w 0277 || geometry().height() != h) { 0278 0279 setGeometry(x, y, w, h); 0280 } 0281 } 0282 0283 void AnnotationDialog::ResizableFrame::mouseReleaseEvent(QMouseEvent *event) 0284 { 0285 if (event->button() == Qt::LeftButton) { 0286 checkGeometry(); 0287 setActualCoordinates(m_preview->areaPreviewToActual(geometry())); 0288 } 0289 } 0290 0291 void AnnotationDialog::ResizableFrame::contextMenuEvent(QContextMenuEvent *) 0292 { 0293 showContextMenu(); 0294 } 0295 0296 void AnnotationDialog::ResizableFrame::repolish() 0297 { 0298 style()->unpolish(this); 0299 style()->polish(this); 0300 update(); 0301 } 0302 0303 QAction *AnnotationDialog::ResizableFrame::createAssociateTagAction( 0304 const QPair<QString, QString> &tag, 0305 QString prefix) 0306 { 0307 QString actionText; 0308 if (!prefix.isEmpty()) { 0309 actionText = i18nc("%1 is a prefix like 'Associate with', " 0310 "%2 is the tag name and %3 is the tag's category", 0311 "%1 %2 (%3)", 0312 prefix, tag.second, tag.first); 0313 } else { 0314 actionText = i18nc("%1 is the tag name and %2 is the tag's category", 0315 "%1 (%2)", 0316 tag.second, tag.first); 0317 } 0318 0319 QAction *action = new QAction(actionText, this); 0320 QStringList data; 0321 data << tag.first << tag.second; 0322 action->setData(data); 0323 0324 return action; 0325 } 0326 0327 bool AnnotationDialog::ResizableFrame::associated() const 0328 { 0329 return !m_tagData.first.isEmpty(); 0330 } 0331 0332 void AnnotationDialog::ResizableFrame::associateTag() 0333 { 0334 QAction *action = qobject_cast<QAction *>(sender()); 0335 Q_ASSERT(action != nullptr); 0336 associateTag(action); 0337 } 0338 0339 void AnnotationDialog::ResizableFrame::associateTag(QAction *action) 0340 { 0341 setTagData(action->data().toStringList()[0], action->data().toStringList()[1]); 0342 } 0343 0344 void AnnotationDialog::ResizableFrame::setTagData(QString category, QString tag, ChangeOrigin changeOrigin) 0345 { 0346 QPair<QString, QString> selectedData = QPair<QString, QString>(category, tag); 0347 0348 // check existing areas for consistency 0349 const auto areas = m_dialog->areas(); 0350 for (ResizableFrame *area : areas) { 0351 if (area->isTidied()) { 0352 continue; 0353 } 0354 0355 if (area->tagData() == selectedData) { 0356 if (KMessageBox::Cancel == KMessageBox::warningContinueCancel(m_preview, i18n("<p>%1 has already been tagged in another area on this image.</p>" 0357 "<p>If you continue, the previous tag will be removed...</p>", 0358 tag), 0359 i18n("Replace existing area?"))) { 0360 // don't execute setTagData 0361 return; 0362 } 0363 // replace existing tag 0364 area->removeTagData(); 0365 } 0366 } 0367 // Add the data to this area 0368 m_tagData = selectedData; 0369 0370 // Update the tool tip 0371 setToolTip(tag + QString::fromUtf8(" (") + category + QString::fromUtf8(")")); 0372 0373 repolish(); 0374 qCDebug(AnnotationDialogLog) << "ResizeableFrame is now associated to" << m_tagData; 0375 0376 // Remove the associated tag from the tag candidate list 0377 m_dialog->removeTagFromCandidateList(m_tagData.first, m_tagData.second); 0378 0379 if (changeOrigin != AutomatedChange) { 0380 // Tell the dialog an area has been changed 0381 m_dialog->areaChanged(); 0382 } 0383 } 0384 0385 void AnnotationDialog::ResizableFrame::removeTag() 0386 { 0387 // Deselect the tag 0388 m_dialog->listSelectForCategory(m_tagData.first)->deselectTag(m_tagData.second); 0389 // Delete the tag data from this area 0390 removeTagData(); 0391 } 0392 0393 void AnnotationDialog::ResizableFrame::removeTagData() 0394 { 0395 // Delete the data 0396 m_tagData.first.clear(); 0397 m_tagData.second.clear(); 0398 setToolTip(QString()); 0399 repolish(); 0400 qCDebug(AnnotationDialogLog) << "ResizeableFrame is now unassociated"; 0401 0402 // Tell the dialog an area has been changed 0403 m_dialog->areaChanged(); 0404 } 0405 0406 void AnnotationDialog::ResizableFrame::remove() 0407 { 0408 if (!m_tagData.first.isEmpty()) { 0409 // Deselect the tag 0410 m_dialog->listSelectForCategory(m_tagData.first)->deselectTag(m_tagData.second); 0411 } 0412 0413 // Delete the area 0414 this->deleteLater(); 0415 } 0416 0417 void AnnotationDialog::ResizableFrame::showContextMenu() 0418 { 0419 // Display a dialog where a tag can be selected directly 0420 QString category = m_previewWidget->defaultPositionableCategory(); 0421 // this is not a memory leak: AreaTagSelectDialog is a regular parented dialog 0422 AreaTagSelectDialog *tagMenu = new AreaTagSelectDialog( 0423 this, 0424 m_dialog->listSelectForCategory(category), 0425 m_preview->grabAreaImage(geometry()), 0426 m_dialog); 0427 0428 tagMenu->show(); 0429 tagMenu->moveToArea(mapToGlobal(QPoint(0, 0))); 0430 tagMenu->exec(); 0431 } 0432 0433 void AnnotationDialog::ResizableFrame::setDialog(Dialog *dialog) 0434 { 0435 m_dialog = dialog; 0436 } 0437 0438 QPair<QString, QString> AnnotationDialog::ResizableFrame::tagData() const 0439 { 0440 return m_tagData; 0441 } 0442 0443 QPair<QString, QString> AnnotationDialog::ResizableFrame::proposedTagData() const 0444 { 0445 return m_proposedTagData; 0446 } 0447 0448 void AnnotationDialog::ResizableFrame::removeProposedTagData() 0449 { 0450 m_proposedTagData = QPair<QString, QString>(); 0451 setToolTip(QString()); 0452 } 0453 0454 void AnnotationDialog::ResizableFrame::addTagActions(QMenu *menu) 0455 { 0456 // Let's see if we already have an associated tag 0457 if (!m_tagData.first.isEmpty()) { 0458 m_removeTagAct->setText( 0459 i18nc("As in: remove tag %1 in category %2 [from this marked area of the image]", 0460 "Remove tag %1 (%2)", 0461 m_tagData.second, 0462 m_tagData.first)); 0463 menu->addAction(m_removeTagAct); 0464 0465 } else { 0466 // Handle the last selected positionable tag (if we have one) 0467 QPair<QString, QString> lastSelectedPositionableTag = m_dialog->lastSelectedPositionableTag(); 0468 if (!lastSelectedPositionableTag.first.isEmpty()) { 0469 QAction *associateLastSelectedTagAction = createAssociateTagAction( 0470 lastSelectedPositionableTag, 0471 i18n("Associate with")); 0472 connect(associateLastSelectedTagAction, &QAction::triggered, 0473 this, QOverload<>::of(&ResizableFrame::associateTag)); 0474 menu->addAction(associateLastSelectedTagAction); 0475 } 0476 0477 // Handle all positionable tag candidates 0478 0479 QList<QPair<QString, QString>> positionableTagCandidates = m_dialog->positionableTagCandidates(); 0480 // If we have a last selected positionable tag: remove it 0481 const auto lastIndex = positionableTagCandidates.indexOf(lastSelectedPositionableTag); 0482 if (lastIndex >= 0) 0483 positionableTagCandidates.removeAt(lastIndex); 0484 0485 // If we still have candidates: 0486 if (positionableTagCandidates.length() > 0) { 0487 if (positionableTagCandidates.length() == 1 0488 && lastSelectedPositionableTag.first.isEmpty()) { 0489 0490 // Add a single action 0491 QAction *associateOnlyCandidateAction = createAssociateTagAction( 0492 positionableTagCandidates[0], 0493 i18nc("As in: associate [this marked area of the image] with one of the " 0494 "following choices/menu items", 0495 "Associate with")); 0496 connect(associateOnlyCandidateAction, &QAction::triggered, 0497 this, QOverload<>::of(&ResizableFrame::associateTag)); 0498 menu->addAction(associateOnlyCandidateAction); 0499 } else { 0500 // Create a new menu for all other tags 0501 QMenu *submenu = menu->addMenu( 0502 i18nc("As in: associate [this marked area of the image] with one of the " 0503 "following choices/menu items", 0504 "Associate with")); 0505 0506 for (const QPair<QString, QString> &tag : positionableTagCandidates) { 0507 submenu->addAction(createAssociateTagAction(tag)); 0508 } 0509 0510 connect(submenu, &QMenu::triggered, this, QOverload<QAction *>::of(&ResizableFrame::associateTag)); 0511 } 0512 } 0513 } 0514 0515 QAction *sep = menu->addSeparator(); 0516 // clicking the separator should not dismiss the menu: 0517 sep->setEnabled(false); 0518 0519 // Append the "Remove area" action 0520 menu->addAction(m_removeAct); 0521 } 0522 0523 void AnnotationDialog::ResizableFrame::markTidied() 0524 { 0525 m_tidied = true; 0526 } 0527 0528 bool AnnotationDialog::ResizableFrame::isTidied() const 0529 { 0530 return m_tidied; 0531 } 0532 0533 // vi:expandtab:tabstop=4 shiftwidth=4: 0534 0535 #include "moc_ResizableFrame.cpp"