File indexing completed on 2024-06-16 04:16:21
0001 /* 0002 * SPDX-FileCopyrightText: 2020 Saurabh Kumar <saurabhk660@gmail.com> 0003 * 0004 * SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "StoryboardDockerDock.h" 0008 #include "CommentDelegate.h" 0009 #include "CommentModel.h" 0010 #include "StoryboardModel.h" 0011 #include "StoryboardDelegate.h" 0012 #include "StoryboardView.h" 0013 #include "StoryboardUtils.h" 0014 #include "DlgExportStoryboard.h" 0015 #include "KisAddRemoveStoryboardCommand.h" 0016 0017 #include <QMenu> 0018 #include <QButtonGroup> 0019 #include <QDebug> 0020 #include <QStringListModel> 0021 #include <QListView> 0022 #include <QItemSelection> 0023 #include <QSize> 0024 #include <QPrinter> 0025 #include <QTextDocument> 0026 #include <QAbstractTextDocumentLayout> 0027 #include <QSvgGenerator> 0028 #include <QSvgRenderer> 0029 #include <QGraphicsSvgItem> 0030 #include <QMessageBox> 0031 #include <QSizePolicy> 0032 0033 #include <klocalizedstring.h> 0034 0035 #include <KisPart.h> 0036 #include <KisViewManager.h> 0037 #include <kis_node_manager.h> 0038 #include <KisDocument.h> 0039 #include <kis_icon.h> 0040 #include <kis_image_animation_interface.h> 0041 #include <kis_time_span.h> 0042 #include <kis_global.h> 0043 #include <KisCursorOverrideLock.h> 0044 0045 #include "ui_wdgstoryboarddock.h" 0046 #include "ui_wdgcommentmenu.h" 0047 #include "ui_wdgarrangemenu.h" 0048 0049 enum Mode { 0050 Column, 0051 Row, 0052 Grid 0053 }; 0054 0055 enum View { 0056 All, 0057 ThumbnailsOnly, 0058 CommentsOnly 0059 }; 0060 0061 class CommentMenu: public QMenu 0062 { 0063 Q_OBJECT 0064 public: 0065 CommentMenu(QWidget *parent, StoryboardCommentModel *m_model) 0066 : QMenu(parent) 0067 , m_menuUI(new Ui_WdgCommentMenu()) 0068 , model(m_model) 0069 , delegate(new CommentDelegate(this)) 0070 { 0071 QWidget* commentWidget = new QWidget(this); 0072 m_menuUI->setupUi(commentWidget); 0073 0074 m_menuUI->fieldListView->setDragEnabled(true); 0075 m_menuUI->fieldListView->setAcceptDrops(true); 0076 m_menuUI->fieldListView->setDropIndicatorShown(true); 0077 m_menuUI->fieldListView->setDragDropMode(QAbstractItemView::InternalMove); 0078 0079 m_menuUI->fieldListView->setModel(model); 0080 m_menuUI->fieldListView->setItemDelegate(delegate); 0081 0082 m_menuUI->fieldListView->setEditTriggers(QAbstractItemView::AnyKeyPressed | 0083 QAbstractItemView::DoubleClicked ); 0084 0085 m_menuUI->btnAddField->setIcon(KisIconUtils::loadIcon("list-add")); 0086 m_menuUI->btnDeleteField->setIcon(KisIconUtils::loadIcon("edit-delete")); 0087 m_menuUI->btnAddField->setIconSize(QSize(16, 16)); 0088 m_menuUI->btnDeleteField->setIconSize(QSize(16, 16)); 0089 connect(m_menuUI->btnAddField, SIGNAL(clicked()), this, SLOT(slotaddItem())); 0090 connect(m_menuUI->btnDeleteField, SIGNAL(clicked()), this, SLOT(slotdeleteItem())); 0091 0092 KisAction *commentAction = new KisAction(commentWidget); 0093 commentAction->setDefaultWidget(commentWidget); 0094 this->addAction(commentAction); 0095 } 0096 0097 private Q_SLOTS: 0098 void slotaddItem() 0099 { 0100 int row = m_menuUI->fieldListView->currentIndex().row()+1; 0101 model->insertRows(row, 1); 0102 0103 QModelIndex index = model->index(row); 0104 m_menuUI->fieldListView->setCurrentIndex(index); 0105 m_menuUI->fieldListView->edit(index); 0106 } 0107 0108 void slotdeleteItem() 0109 { 0110 model->removeRows(m_menuUI->fieldListView->currentIndex().row(), 1); 0111 } 0112 0113 private: 0114 QScopedPointer<Ui_WdgCommentMenu> m_menuUI; 0115 StoryboardCommentModel *model; 0116 CommentDelegate *delegate; 0117 }; 0118 0119 class ArrangeMenu: public QMenu 0120 { 0121 public: 0122 ArrangeMenu(QWidget *parent) 0123 : QMenu(parent) 0124 , m_menuUI(new Ui_WdgArrangeMenu()) 0125 , modeGroup(new QButtonGroup(this)) 0126 , viewGroup(new QButtonGroup(this)) 0127 { 0128 QWidget* arrangeWidget = new QWidget(this); 0129 m_menuUI->setupUi(arrangeWidget); 0130 0131 modeGroup->addButton(m_menuUI->btnColumnMode, Mode::Column); 0132 modeGroup->addButton(m_menuUI->btnRowMode, Mode::Row); 0133 modeGroup->addButton(m_menuUI->btnGridMode, Mode::Grid); 0134 0135 viewGroup->addButton(m_menuUI->btnAllView, View::All); 0136 viewGroup->addButton(m_menuUI->btnThumbnailsView, View::ThumbnailsOnly); 0137 viewGroup->addButton(m_menuUI->btnCommentsView, View::CommentsOnly); 0138 0139 KisAction *arrangeAction = new KisAction(arrangeWidget); 0140 arrangeAction->setDefaultWidget(arrangeWidget); 0141 this->addAction(arrangeAction); 0142 } 0143 0144 QButtonGroup* getModeGroup(){ return modeGroup;} 0145 QButtonGroup* getViewGroup(){ return viewGroup;} 0146 0147 private: 0148 QScopedPointer<Ui_WdgArrangeMenu> m_menuUI; 0149 QButtonGroup *modeGroup; 0150 QButtonGroup *viewGroup; 0151 }; 0152 0153 0154 inline QMap<QString, QDomNode> rootItemsInSvg(const QDomDocument &d){ 0155 QMap<QString, QDomNode> nodeMap; 0156 QDomNodeList svgs = d.elementsByTagName("svg"); 0157 KIS_ASSERT_RECOVER_RETURN_VALUE(svgs.size() > 0, nodeMap); 0158 QDomNode svg = svgs.at(0); 0159 QDomNodeList children = svg.toElement().childNodes(); 0160 for (int i = 0; i < children.count(); i++) { 0161 QString id = children.at(i).toElement().attribute("id"); 0162 if (id.isEmpty()) 0163 continue; 0164 0165 nodeMap.insert(id, children.at(i)); 0166 } 0167 return nodeMap; 0168 }; 0169 0170 0171 StoryboardDockerDock::StoryboardDockerDock( ) 0172 : QDockWidget(i18nc("Storyboard Docker", "Storyboard")) 0173 , m_canvas(0) 0174 , m_ui(new Ui_WdgStoryboardDock()) 0175 , m_exportMenu(new QMenu(this)) 0176 , m_commentModel(new StoryboardCommentModel(this)) 0177 , m_commentMenu(new CommentMenu(this, m_commentModel)) 0178 , m_arrangeMenu(new ArrangeMenu(this)) 0179 , m_storyboardModel(new StoryboardModel(this)) 0180 , m_storyboardDelegate(new StoryboardDelegate(this)) 0181 { 0182 QWidget* mainWidget = new QWidget(this); 0183 setWidget(mainWidget); 0184 m_ui->setupUi(mainWidget); 0185 0186 m_ui->btnExport->setMenu(m_exportMenu); 0187 m_ui->btnExport->setPopupMode(QToolButton::InstantPopup); 0188 0189 m_exportAsPdfAction = new KisAction(i18nc("Export storyboard as PDF", "Export as PDF"), m_exportMenu); 0190 m_exportMenu->addAction(m_exportAsPdfAction); 0191 0192 m_exportAsSvgAction = new KisAction(i18nc("Export storyboard as SVG", "Export as SVG"), m_exportMenu); 0193 m_exportMenu->addAction(m_exportAsSvgAction); 0194 connect(m_exportAsPdfAction, SIGNAL(triggered()), this, SLOT(slotExportAsPdf())); 0195 connect(m_exportAsSvgAction, SIGNAL(triggered()), this, SLOT(slotExportAsSvg())); 0196 0197 //Setup dynamic QListView Width Based on Comment Model Columns... 0198 connect(m_commentModel, &StoryboardCommentModel::sigCommentListChanged, this, &StoryboardDockerDock::slotUpdateMinimumWidth); 0199 connect(m_storyboardModel.data(), &StoryboardModel::rowsInserted, this, &StoryboardDockerDock::slotUpdateMinimumWidth); 0200 0201 connect(m_storyboardModel.data(), &StoryboardModel::rowsInserted, this, &StoryboardDockerDock::slotModelChanged); 0202 connect(m_storyboardModel.data(), &StoryboardModel::rowsRemoved, this, &StoryboardDockerDock::slotModelChanged); 0203 0204 m_ui->btnComment->setMenu(m_commentMenu); 0205 m_ui->btnComment->setPopupMode(QToolButton::InstantPopup); 0206 0207 m_lockAction = new KisAction(KisIconUtils::loadIcon("unlocked"), 0208 i18nc("Freeze keyframe positions and ignore storyboard adjustments", "Freeze Keyframe Data"), m_ui->btnLock); 0209 m_lockAction->setCheckable(true); 0210 m_ui->btnLock->setDefaultAction(m_lockAction); 0211 m_ui->btnLock->setIconSize(QSize(16, 16)); 0212 connect(m_lockAction, SIGNAL(toggled(bool)), this, SLOT(slotLockClicked(bool))); 0213 0214 m_ui->btnArrange->setMenu(m_arrangeMenu); 0215 m_ui->btnArrange->setPopupMode(QToolButton::InstantPopup); 0216 m_ui->btnArrange->setIcon(KisIconUtils::loadIcon("view-choose")); 0217 m_ui->btnArrange->setAutoRaise(true); 0218 m_ui->btnArrange->setIconSize(QSize(16, 16)); 0219 0220 m_modeGroup = m_arrangeMenu->getModeGroup(); 0221 m_viewGroup = m_arrangeMenu->getViewGroup(); 0222 0223 connect(m_modeGroup, SIGNAL(buttonClicked(QAbstractButton*)), this, SLOT(slotModeChanged(QAbstractButton*))); 0224 connect(m_viewGroup, SIGNAL(buttonClicked(QAbstractButton*)), this, SLOT(slotViewChanged(QAbstractButton*))); 0225 0226 m_storyboardDelegate->setView(m_ui->sceneView); 0227 m_storyboardModel->setView(m_ui->sceneView); 0228 m_ui->sceneView->setModel(m_storyboardModel.data()); 0229 m_ui->sceneView->setItemDelegate(m_storyboardDelegate); 0230 0231 m_storyboardModel->setCommentModel(m_commentModel); 0232 0233 m_modeGroup->button(Mode::Row)->click(); 0234 m_viewGroup->button(View::All)->click(); 0235 0236 { // Footer section... 0237 QAction* action = new QAction(i18nc("Add new scene as the last storyboard", "Add Scene"), this); 0238 connect(action, &QAction::triggered, this, [this](bool){ 0239 if (!m_canvas) return; 0240 0241 QModelIndex currentSelection = m_ui->sceneView->currentIndex(); 0242 if (currentSelection.parent().isValid()) { 0243 currentSelection = currentSelection.parent(); 0244 } 0245 0246 m_storyboardModel->insertItem(currentSelection, true); 0247 }); 0248 action->setIcon(KisIconUtils::loadIcon("list-add")); 0249 m_ui->btnCreateScene->setAutoRaise(true); 0250 m_ui->btnCreateScene->setIconSize(QSize(22,22)); 0251 m_ui->btnCreateScene->setDefaultAction(action); 0252 0253 action = new QAction(i18nc("Remove current scene from storyboards", "Remove Scene"), this); 0254 connect(action, &QAction::triggered, this, [this](bool){ 0255 if (!m_canvas) return; 0256 0257 QModelIndex currentSelection = m_ui->sceneView->currentIndex(); 0258 if (currentSelection.parent().isValid()) { 0259 currentSelection = currentSelection.parent(); 0260 } 0261 0262 if (currentSelection.isValid()) { 0263 int row = currentSelection.row(); 0264 KisRemoveStoryboardCommand *command = new KisRemoveStoryboardCommand(row, m_storyboardModel->getData().at(row), m_storyboardModel.data()); 0265 0266 m_storyboardModel->removeItem(currentSelection, command); 0267 m_storyboardModel->pushUndoCommand(command); 0268 } 0269 }); 0270 action->setIcon(KisIconUtils::loadIcon("edit-delete")); 0271 m_ui->btnDeleteScene->setAutoRaise(true); 0272 m_ui->btnDeleteScene->setIconSize(QSize(22,22)); 0273 m_ui->btnDeleteScene->setDefaultAction(action); 0274 } 0275 0276 setEnabled(false); 0277 } 0278 0279 StoryboardDockerDock::~StoryboardDockerDock() 0280 { 0281 delete m_commentModel; 0282 m_storyboardModel.reset(); 0283 delete m_storyboardDelegate; 0284 } 0285 0286 void StoryboardDockerDock::setCanvas(KoCanvasBase *canvas) 0287 { 0288 if (m_canvas == canvas) { 0289 return; 0290 } 0291 0292 if (m_canvas) { 0293 disconnect(m_storyboardModel.data(), SIGNAL(sigStoryboardItemListChanged()), this, SLOT(slotUpdateDocumentList())); 0294 disconnect(m_commentModel, SIGNAL(sigCommentListChanged()), this, SLOT(slotUpdateDocumentList())); 0295 disconnect(m_canvas->imageView()->document(), SIGNAL(sigStoryboardItemListChanged()), this, SLOT(slotUpdateStoryboardModelList())); 0296 disconnect(m_canvas->imageView()->document(), SIGNAL(sigStoryboardItemListChanged()), this, SLOT(slotUpdateCommentModelList())); 0297 0298 //update the lists in KisDocument and empty storyboardModel's list and commentModel's list 0299 slotUpdateDocumentList(); 0300 m_storyboardModel->resetData(StoryboardItemList()); 0301 m_commentModel->resetData(QVector<StoryboardComment>()); 0302 m_storyboardModel->slotSetActiveNode(nullptr); 0303 } 0304 0305 m_canvas = dynamic_cast<KisCanvas2*>(canvas); 0306 setEnabled(m_canvas != 0); 0307 0308 if (m_canvas && m_canvas->image()) { 0309 //sync data between KisDocument and models 0310 slotUpdateStoryboardModelList(); 0311 slotUpdateCommentModelList(); 0312 0313 connect(m_storyboardModel.data(), SIGNAL(sigStoryboardItemListChanged()), SLOT(slotUpdateDocumentList()), Qt::UniqueConnection); 0314 connect(m_commentModel, SIGNAL(sigCommentListChanged()), SLOT(slotUpdateDocumentList()), Qt::UniqueConnection); 0315 connect(m_canvas->imageView()->document(), SIGNAL(sigStoryboardItemListChanged()), this, SLOT(slotUpdateStoryboardModelList()), Qt::UniqueConnection); 0316 connect(m_canvas->imageView()->document(), SIGNAL(sigStoryboardCommentListChanged()), this, SLOT(slotUpdateCommentModelList()), Qt::UniqueConnection); 0317 0318 m_storyboardModel->setImage(m_canvas->image()); 0319 m_storyboardDelegate->setImageSize(m_canvas->image()->size()); 0320 connect(m_canvas->image(), SIGNAL(sigAboutToBeDeleted()), SLOT(notifyImageDeleted()), Qt::UniqueConnection); 0321 0322 if (m_nodeManager) { 0323 m_storyboardModel->slotSetActiveNode(m_nodeManager->activeNode()); 0324 } 0325 } 0326 0327 slotUpdateMinimumWidth(); 0328 slotModelChanged(); 0329 } 0330 0331 void StoryboardDockerDock::unsetCanvas() 0332 { 0333 setCanvas(0); 0334 } 0335 0336 void StoryboardDockerDock::setViewManager(KisViewManager* kisview) 0337 { 0338 m_nodeManager = kisview->nodeManager(); 0339 if (m_nodeManager) { 0340 connect(m_nodeManager, SIGNAL(sigNodeActivated(KisNodeSP)), m_storyboardModel.data(), SLOT(slotSetActiveNode(KisNodeSP))); 0341 } 0342 } 0343 0344 0345 void StoryboardDockerDock::notifyImageDeleted() 0346 { 0347 //if there is no image 0348 if (!m_canvas || !m_canvas->image()){ 0349 m_storyboardModel->setImage(0); 0350 } 0351 } 0352 0353 void StoryboardDockerDock::slotUpdateDocumentList() 0354 { 0355 m_canvas->imageView()->document()->setStoryboardItemList(m_storyboardModel->getData()); 0356 m_canvas->imageView()->document()->setStoryboardCommentList(m_commentModel->getData()); 0357 } 0358 0359 void StoryboardDockerDock::slotUpdateStoryboardModelList() 0360 { 0361 m_storyboardModel->resetData(m_canvas->imageView()->document()->getStoryboardItemList()); 0362 } 0363 0364 void StoryboardDockerDock::slotUpdateCommentModelList() 0365 { 0366 m_commentModel->resetData(m_canvas->imageView()->document()->getStoryboardCommentsList()); 0367 } 0368 0369 void StoryboardDockerDock::slotExportAsPdf() 0370 { 0371 slotExport(ExportFormat::PDF); 0372 } 0373 0374 void StoryboardDockerDock::slotExportAsSvg() 0375 { 0376 slotExport(ExportFormat::SVG); 0377 } 0378 0379 void StoryboardDockerDock::slotExport(ExportFormat format) 0380 { 0381 QFileInfo fileInfo(m_canvas->imageView()->document()->path()); 0382 const QString imageFileName = fileInfo.baseName(); 0383 const int storyboardCount = m_storyboardModel->rowCount(); 0384 KIS_SAFE_ASSERT_RECOVER_RETURN(storyboardCount > 0); 0385 DlgExportStoryboard dlg(format, m_storyboardModel); 0386 0387 if (dlg.exec() == QDialog::Accepted) { 0388 dlg.hide(); 0389 KisCursorOverrideLock cursorLock(Qt::WaitCursor); 0390 0391 ExportPage layoutPage; 0392 QPrinter printer(QPrinter::HighResolution); 0393 0394 // Setup export parameters... 0395 QPainter painter; 0396 QSvgGenerator generator; 0397 0398 QFont font = painter.font(); 0399 font.setPointSize(dlg.fontSize()); 0400 0401 StoryboardItemList storyboardList = m_storyboardModel->getData(); 0402 0403 // Setup per-element layout details... 0404 bool layoutSpecifiedBySvg = dlg.layoutSpecifiedBySvgFile(); 0405 if (layoutSpecifiedBySvg) { 0406 QString svgFileName = dlg.layoutSvgFile(); 0407 layoutPage = getPageLayout(svgFileName, &printer); 0408 } 0409 else { 0410 int rows = dlg.rows(); 0411 int columns = dlg.columns(); 0412 printer.setOutputFileName(dlg.saveFileName()); 0413 printer.setPageSize(dlg.pageSize()); 0414 printer.setPageOrientation(dlg.pageOrientation()); 0415 painter.begin(&printer); // We need to begin the painter temporarily for font metrics. 0416 painter.setFont(font); 0417 layoutPage = getPageLayout(rows, columns, QRect(0, 0, m_canvas->image()->width(), m_canvas->image()->height()), printer.pageRect(), painter.fontMetrics()); 0418 painter.end(); // End temporary painter begin. 0419 } 0420 0421 KIS_SAFE_ASSERT_RECOVER_RETURN(layoutPage.elements.length() > 0); 0422 0423 // Get a range of items to render. Used to be configurable in an older version but now simplified... 0424 QModelIndex firstIndex = m_storyboardModel->index(0,0); 0425 QModelIndex lastIndex = m_storyboardModel->index(m_storyboardModel->rowCount() - 1, 0); 0426 0427 if (!firstIndex.isValid() || !lastIndex.isValid()) { 0428 QMessageBox::warning((QWidget*)(&dlg), i18nc("@title:window", "Krita"), i18n("Please enter correct range. There are no panels in the range of frames provided.")); 0429 return; 0430 } 0431 0432 int firstItemRow = firstIndex.row(); 0433 int lastItemRow = lastIndex.row(); 0434 0435 int numBoards = lastItemRow - firstItemRow + 1; 0436 if (numBoards <= 0) { 0437 QMessageBox::warning((QWidget*)(&dlg), i18nc("@title:window", "Krita"), i18n("Please enter correct range. There are no panels in the range of frames provided.")); 0438 return; 0439 } 0440 0441 if (dlg.format() == ExportFormat::SVG) { 0442 generator.setFileName(dlg.saveFileName() + "/" + imageFileName + "0.svg"); 0443 QSize sz = printer.pageRect().size(); 0444 generator.setSize(sz); 0445 generator.setViewBox(QRect(0, 0, sz.width(), sz.height())); 0446 generator.setResolution(printer.resolution()); 0447 painter.begin(&generator); 0448 painter.setBrush(QBrush(QColor(255,255,255))); 0449 painter.drawRect(QRect(0,0, sz.width(), sz.height())); 0450 } 0451 else { 0452 printer.setOutputFileName(dlg.saveFileName()); 0453 printer.setOutputFormat(QPrinter::PdfFormat); 0454 painter.begin(&printer); 0455 painter.setFont(font); 0456 painter.setBackgroundMode(Qt::BGMode::OpaqueMode); 0457 } 0458 0459 // Paint boards 0460 int pageNumber = 1; 0461 int pageDurationInFrames = 0; 0462 0463 for (int currentBoard = 0; currentBoard < numBoards; currentBoard++) { 0464 if (currentBoard % layoutPage.elements.length() == 0) { 0465 if (dlg.format() == ExportFormat::SVG) { 0466 if (currentBoard != 0) { 0467 painter.end(); 0468 painter.eraseRect(printer.pageRect()); 0469 generator.setFileName(dlg.saveFileName() + "/" + imageFileName + QString::number(currentBoard / layoutPage.elements.length()) + ".svg"); 0470 QSize sz = printer.pageRect().size(); 0471 generator.setSize(sz); 0472 generator.setViewBox(QRect(0, 0, sz.width(), sz.height())); 0473 generator.setResolution(printer.resolution()); 0474 painter.begin(&generator); 0475 } 0476 } 0477 else { 0478 if (currentBoard != 0 ) { // New page! 0479 printer.newPage(); 0480 0481 pageNumber++; 0482 pageDurationInFrames = 0; 0483 } 0484 0485 if(layoutPage.svg) { 0486 QMap<QString, QDomNode> groups = rootItemsInSvg(layoutPage.svg.value()); 0487 if (groups.contains("overlay")) { 0488 QMapIterator<QString, QDomNode> iter(groups); 0489 while(iter.hasNext()) { 0490 iter.next(); 0491 if (iter.key() == "overlay" || iter.key() == "layout") { 0492 iter.value().toElement().setAttribute("display","none"); 0493 } else { 0494 iter.value().toElement().setAttribute("display","inline"); 0495 } 0496 } 0497 } 0498 QSvgRenderer renderer(layoutPage.svg->toByteArray()); 0499 renderer.render(&painter); 0500 } 0501 } 0502 } 0503 0504 ThumbnailData data = qvariant_cast<ThumbnailData>(storyboardList.at(currentBoard + firstItemRow)->child(StoryboardItem::FrameNumber)->data()); 0505 QPixmap pxmp = qvariant_cast<QPixmap>(data.pixmap); 0506 QVector<StoryboardComment> comments = m_commentModel->getData(); 0507 const int numComments = comments.size(); 0508 0509 const ExportPageShot* const layoutShot = &layoutPage.elements[currentBoard % layoutPage.elements.length()]; 0510 0511 QPen pen(QColor(1, 0, 0)); 0512 pen.setWidth(5); 0513 painter.setPen(pen); 0514 0515 // Draw image 0516 if (layoutShot->cutImageRect.has_value()) { 0517 QRectF imgRect = layoutShot->cutImageRect.value(); 0518 QSizeF resizedImage = QSizeF(pxmp.size()).scaled(layoutShot->cutImageRect->size(), Qt::KeepAspectRatio); 0519 const int MARGIN = -2; 0520 resizedImage = QSize(resizedImage.width() + MARGIN * 2, resizedImage.height() + MARGIN * 2); 0521 imgRect.setSize(resizedImage); 0522 imgRect.translate((layoutShot->cutImageRect.value().width() - imgRect.size().width()) / 2 - MARGIN, 0523 (layoutShot->cutImageRect->height() - imgRect.size().height()) / 2 - MARGIN); 0524 painter.drawPixmap(imgRect, pxmp, pxmp.rect()); 0525 painter.drawRect(layoutShot->cutImageRect.value()); 0526 } 0527 0528 { // Insert shot text elements... 0529 painter.save(); 0530 painter.setBackgroundMode(Qt::TransparentMode); 0531 0532 // Draw shot name 0533 if (layoutShot->cutNameRect.has_value()) { 0534 QString str = storyboardList.at(currentBoard + firstItemRow)->child(StoryboardItem::ItemName)->data().toString(); 0535 painter.drawText(layoutShot->cutNameRect.value().translated(painter.fontMetrics().averageCharWidth() / 2, 0), Qt::AlignLeft | Qt::AlignVCenter, str); 0536 0537 if (!layoutPage.svg) { 0538 painter.drawRect(layoutShot->cutNameRect.value()); 0539 } 0540 } 0541 0542 // Draw shot number 0543 if (layoutShot->cutNumberRect.has_value()) { 0544 painter.drawText(layoutShot->cutNumberRect.value(), Qt::AlignCenter, QString::number(currentBoard + firstItemRow)); 0545 0546 if (!layoutPage.svg) { 0547 painter.drawRect(layoutShot->cutNumberRect.value()); 0548 } 0549 } 0550 0551 QModelIndex boardIndex = m_storyboardModel->index(currentBoard + firstItemRow, 0); 0552 0553 const int boardDurationFrames = m_storyboardModel->data(boardIndex, StoryboardModel::TotalSceneDurationInFrames).toInt(); 0554 pageDurationInFrames += boardDurationFrames; 0555 0556 // Draw shot duration 0557 if (layoutShot->cutDurationRect.has_value()) { 0558 // Split shot duration into tuple of seconds + frames (remainder).. 0559 int durationSecondsPart = boardDurationFrames / m_storyboardModel->getFramesPerSecond(); 0560 int durationFramesPart = boardDurationFrames % m_storyboardModel->getFramesPerSecond(); 0561 0562 painter.drawText(layoutShot->cutDurationRect.value(), Qt::AlignCenter, 0563 buildDurationString(durationSecondsPart, durationFramesPart)); 0564 0565 if (!layoutPage.svg) { 0566 painter.drawRect(layoutShot->cutDurationRect.value()); 0567 } 0568 } 0569 0570 painter.restore(); 0571 } 0572 0573 0574 // Draw shot comments 0575 for (int commentIndex = 0; commentIndex < numComments; commentIndex++) { 0576 if (!layoutShot->commentRects.contains(comments[commentIndex].name)) 0577 continue; 0578 0579 const QString& commentName = comments[commentIndex].name; 0580 0581 QTextDocument doc; 0582 doc.setDocumentMargin(0); 0583 doc.setDefaultFont(painter.font()); 0584 QString comment; 0585 comment += "<p><b>" + commentName + "</b></p>"; // if arrange options are used check for visibility 0586 QString originalCommentText = qvariant_cast<CommentBox>(storyboardList.at(currentBoard + firstItemRow)->child(StoryboardItem::Comments + commentIndex)->data()).content.toString(); 0587 originalCommentText = originalCommentText.replace('\n', "</p><p>"); 0588 comment += "<p> " + originalCommentText + "</p>"; 0589 const int MARGIN = painter.fontMetrics().averageCharWidth() / 2; 0590 0591 doc.setHtml(comment); 0592 doc.setTextWidth(layoutShot->commentRects[commentName].width() - MARGIN * 2); 0593 0594 QAbstractTextDocumentLayout::PaintContext ctx; 0595 ctx.palette.setColor(QPalette::Text, painter.pen().color()); 0596 0597 //draw the comments 0598 painter.save(); 0599 painter.translate(layoutShot->commentRects[commentName].topLeft() + QPoint(MARGIN, MARGIN)); 0600 painter.setClipRegion(QRegion(0,0,layoutShot->commentRects[commentName].width(), layoutShot->commentRects[commentName].height() - painter.fontMetrics().height())); 0601 painter.setBackgroundMode(Qt::TransparentMode); 0602 doc.documentLayout()->draw(&painter, ctx); 0603 painter.restore(); 0604 0605 painter.drawRect(layoutShot->commentRects[commentName]); 0606 } 0607 0608 // Draw overlays after drawing last element on page or after drawing last element in general. 0609 if ((currentBoard % layoutPage.elements.length()) == (layoutPage.elements.length() - 1) || (currentBoard == numBoards - 1)) { 0610 0611 painter.save(); 0612 painter.setBackgroundMode(Qt::TransparentMode); 0613 0614 if (layoutPage.pageNumberRect) { 0615 painter.drawText(layoutPage.pageNumberRect.value(), Qt::AlignCenter, QString::number(pageNumber)); 0616 } 0617 0618 if (layoutPage.pageTimeRect) { 0619 int pageDurationSecondsPart = pageDurationInFrames / m_storyboardModel->getFramesPerSecond(); 0620 int pageDurationFramesPart = pageDurationInFrames % m_storyboardModel->getFramesPerSecond(); 0621 0622 painter.drawText(layoutPage.pageTimeRect.value(), Qt::AlignCenter, buildDurationString(pageDurationSecondsPart, pageDurationFramesPart)); 0623 } 0624 0625 if (layoutPage.svg) { 0626 QMap<QString, QDomNode> groups = rootItemsInSvg(layoutPage.svg.value()); 0627 if (groups.contains("overlay")) { 0628 QMapIterator<QString, QDomNode> iter(groups); 0629 while(iter.hasNext()) { 0630 iter.next(); 0631 if (iter.key() == "overlay") { 0632 iter.value().toElement().setAttribute("display","inline"); 0633 } else { 0634 iter.value().toElement().setAttribute("display","none"); 0635 } 0636 } 0637 } 0638 QSvgRenderer renderer(layoutPage.svg->toByteArray()); 0639 renderer.render(&painter); 0640 } 0641 0642 painter.restore(); 0643 } 0644 } 0645 painter.end(); 0646 } 0647 } 0648 0649 void StoryboardDockerDock::slotLockClicked(bool isLocked){ 0650 if (isLocked) { 0651 m_lockAction->setIcon(KisIconUtils::loadIcon("locked")); 0652 m_storyboardModel->setLocked(true); 0653 } 0654 else { 0655 m_lockAction->setIcon(KisIconUtils::loadIcon("unlocked")); 0656 m_storyboardModel->setLocked(false); 0657 } 0658 } 0659 0660 void StoryboardDockerDock::slotModeChanged(QAbstractButton* button) 0661 { 0662 int mode = m_modeGroup->id(button); 0663 if (mode == Mode::Column) { 0664 m_ui->sceneView->setFlow(QListView::LeftToRight); 0665 m_ui->sceneView->setWrapping(false); 0666 m_ui->sceneView->setItemOrientation(Qt::Vertical); 0667 m_viewGroup->button(View::CommentsOnly)->setEnabled(true); 0668 } 0669 else if (mode == Mode::Row) { 0670 m_ui->sceneView->setFlow(QListView::TopToBottom); 0671 m_ui->sceneView->setWrapping(false); 0672 m_ui->sceneView->setItemOrientation(Qt::Horizontal); 0673 m_viewGroup->button(View::CommentsOnly)->setEnabled(false); //disable the comments only view 0674 } 0675 else if (mode == Mode::Grid) { 0676 m_ui->sceneView->setFlow(QListView::LeftToRight); 0677 m_ui->sceneView->setWrapping(true); 0678 m_ui->sceneView->setItemOrientation(Qt::Vertical); 0679 m_viewGroup->button(View::CommentsOnly)->setEnabled(true); 0680 } 0681 m_storyboardModel->layoutChanged(); 0682 } 0683 0684 void StoryboardDockerDock::slotViewChanged(QAbstractButton* button) 0685 { 0686 int view = m_viewGroup->id(button); 0687 if (view == View::All) { 0688 m_ui->sceneView->setCommentVisibility(true); 0689 m_ui->sceneView->setThumbnailVisibility(true); 0690 m_modeGroup->button(Mode::Row)->setEnabled(true); 0691 } 0692 else if (view == View::ThumbnailsOnly) { 0693 m_ui->sceneView->setCommentVisibility(false); 0694 m_ui->sceneView->setThumbnailVisibility(true); 0695 m_modeGroup->button(Mode::Row)->setEnabled(true); 0696 } 0697 0698 else if (view == View::CommentsOnly) { 0699 m_ui->sceneView->setCommentVisibility(true); 0700 m_ui->sceneView->setThumbnailVisibility(false); 0701 m_modeGroup->button(Mode::Row)->setEnabled(false); //disable the row mode 0702 } 0703 m_storyboardModel->layoutChanged(); 0704 } 0705 0706 void StoryboardDockerDock::slotUpdateMinimumWidth() 0707 { 0708 m_ui->sceneView->setMinimumSize(m_ui->sceneView->sizeHint()); 0709 } 0710 0711 void StoryboardDockerDock::slotModelChanged() 0712 { 0713 if (m_storyboardModel) { 0714 m_ui->btnExport->setDisabled(m_storyboardModel->rowCount() == 0); 0715 } 0716 } 0717 0718 StoryboardDockerDock::ExportPage StoryboardDockerDock::getPageLayout(int rows, int columns, const QRect& imageSize, const QRect& pageRect, const QFontMetrics& fontMetrics) 0719 { 0720 QSizeF pageSize = pageRect.size(); 0721 QRectF border = pageRect; 0722 QSizeF cellSize(pageSize.width() / columns, pageSize.height() / rows); 0723 QVector<QRectF> rects; 0724 0725 #if QT_VERSION >= QT_VERSION_CHECK(5,11,0) 0726 int numericFontWidth = fontMetrics.horizontalAdvance("0"); 0727 #else 0728 int numericFontWidth = fontMetrics.width("0"); 0729 #endif 0730 0731 for (int row = 0; row < rows; row++) { 0732 0733 QRectF cellRect = border; 0734 cellRect.moveTop(border.top() + row * cellSize.height()); 0735 cellRect.setSize(cellSize - QSize(200,200)); 0736 for (int column = 0; column < columns; column++) { 0737 cellRect.moveLeft(border.left() + column * cellSize.width()); 0738 cellRect.setSize(cellSize * 0.9); 0739 rects.push_back(cellRect); 0740 } 0741 } 0742 0743 QVector<ExportPageShot> elements; 0744 0745 for (int i = 0; i < rects.length(); i++) { 0746 QRectF& cellRect = rects[i]; 0747 0748 const bool horizontal = cellRect.width() > cellRect.height(); // Determine general image / text flow orientation. 0749 ExportPageShot layout; 0750 0751 QVector<StoryboardComment> comments = m_commentModel->getData(); 0752 const int numComments = comments.size(); 0753 0754 if (horizontal) { 0755 QRectF sourceRect = cellRect; 0756 layout.cutDurationRect = kisTrimTop(fontMetrics.height() * 1.5, sourceRect); 0757 layout.cutNameRect = kisTrimLeft(layout.cutDurationRect.value().width() - numericFontWidth * 6, layout.cutDurationRect.value()); 0758 0759 const int imageWidth = sourceRect.height() * static_cast<qreal>(imageSize.width()) / static_cast<qreal>(imageSize.height()); 0760 layout.cutImageRect = kisTrimLeft(imageWidth, sourceRect); 0761 const float commentWidth = sourceRect.width() / numComments; 0762 if (commentWidth > 100) { 0763 for (int i = 0; i < numComments; i++) { 0764 QRectF rect = kisTrimLeft(commentWidth, sourceRect); 0765 layout.commentRects.insert(comments[i].name, rect); 0766 } 0767 } 0768 } else { 0769 QRectF sourceRect = cellRect; 0770 layout.cutDurationRect = kisTrimTop(fontMetrics.height() * 1.5, sourceRect); 0771 layout.cutNameRect = kisTrimLeft(layout.cutDurationRect.value().width() - numericFontWidth * 6, layout.cutDurationRect.value()); 0772 0773 const int imageHeight = sourceRect.width() * static_cast<qreal>(imageSize.height()) / static_cast<qreal>(imageSize.width()); 0774 layout.cutImageRect = kisTrimTop(imageHeight, sourceRect); 0775 const float commentHeight = sourceRect.height() / numComments; 0776 if (commentHeight > 200) { 0777 for (int i = 0; i < numComments; i++) { 0778 QRectF rect = kisTrimTop(commentHeight, sourceRect); 0779 layout.commentRects.insert(comments[i].name, rect); 0780 } 0781 } 0782 } 0783 0784 elements.push_back(layout); 0785 } 0786 0787 ExportPage layout; 0788 layout.elements = elements; 0789 return layout; 0790 } 0791 0792 StoryboardDockerDock::ExportPage StoryboardDockerDock::getPageLayout(QString layoutSvgFileName, QPrinter *printer) 0793 { 0794 QDomDocument svgDoc; 0795 ExportPage page; 0796 QVector<ExportPageShot> elements; 0797 0798 // Load DOM from file... 0799 QFile f(layoutSvgFileName); 0800 if (!f.open(QIODevice::ReadOnly )) 0801 { 0802 qDebug()<<"svg layout file didn't open"; 0803 return page; 0804 } 0805 0806 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(svgDoc.setContent(&f), page); 0807 f.close(); 0808 0809 QDomElement eroot = svgDoc.documentElement(); 0810 0811 QStringList lst = eroot.attribute("viewBox").split(" "); 0812 QSizeF sizeMM(lst.at(2).toDouble(), lst.at(3).toDouble()); 0813 printer->setPageSizeMM(sizeMM); 0814 QSizeF size = printer->pageRect().size(); 0815 QSizeF scaling = QSizeF( size.width() / sizeMM.width(), size.height() / sizeMM.height()); 0816 0817 QVector<QString> commentLayers; 0818 Q_FOREACH( StoryboardComment channel, m_commentModel->getData()) { 0819 commentLayers.push_back(channel.name); 0820 } 0821 0822 QMap<int, ExportPageShot> elementMap; 0823 0824 { // Go through all root-level svg data and preconfigure... 0825 QMap<QString, QDomNode> groupsMap = rootItemsInSvg(svgDoc); 0826 0827 KIS_ASSERT_RECOVER_RETURN_VALUE(groupsMap.contains("layout"), page); 0828 0829 groupsMap["layout"].toElement().setAttribute("display", "none"); 0830 if (groupsMap.contains("overlay")) { 0831 groupsMap["overlay"].toElement().setAttribute("display", "nonde"); 0832 } 0833 0834 QDomNodeList nodeList = groupsMap["layout"].toElement().elementsByTagName("rect"); 0835 for(int i = 0; i < nodeList.size(); i++) { 0836 QDomNode node = nodeList.at(i); 0837 QDomNamedNodeMap attrMap = node.attributes(); 0838 0839 for (int j = 0; j < attrMap.length(); j++) { 0840 QDomAttr attribute = attrMap.item(j).toAttr(); 0841 QString afterNamespace = attribute.name().split(":").last(); 0842 0843 auto isValidLabel = [&](QString label, int& index) -> bool { 0844 if (attribute.value().startsWith(label)) { 0845 if (attribute.value() == label) { 0846 index = 0; 0847 return true; 0848 } 0849 0850 QString indexString = attribute.value().remove(0, label.length()); 0851 bool ok = false; 0852 index = indexString.toInt(&ok); 0853 if (ok) { 0854 if (!elementMap.contains(index)) 0855 elementMap.insert(index, ExportPageShot()); 0856 return true; 0857 } 0858 } 0859 return false; 0860 }; 0861 0862 auto extractRect = [&](boost::optional<QRectF>& to) { 0863 double x = scaling.width() * attrMap.namedItem("x").nodeValue().toDouble(); 0864 double y = scaling.height() * attrMap.namedItem("y").nodeValue().toDouble(); 0865 double width = scaling.width() * attrMap.namedItem("width").nodeValue().toDouble(); 0866 double height = scaling.height() * attrMap.namedItem("height").nodeValue().toDouble(); 0867 to = QRectF(x,y, width, height); 0868 }; 0869 0870 if (afterNamespace == "label") { 0871 int index = 0; 0872 if (isValidLabel("image", index)) { 0873 extractRect(elementMap[index].cutImageRect); 0874 } else if (isValidLabel("time",index)) { 0875 extractRect(elementMap[index].cutDurationRect); 0876 } else if (isValidLabel("name", index)) { 0877 extractRect(elementMap[index].cutNameRect); 0878 } else if (isValidLabel("shot", index)) { 0879 extractRect(elementMap[index].cutNumberRect); 0880 } else if (isValidLabel("page-time", index)) { 0881 extractRect(page.pageTimeRect); 0882 } else if (isValidLabel("page-number", index)) { 0883 extractRect(page.pageNumberRect); 0884 } else { 0885 for(int commentIndex = 0; commentIndex < commentLayers.length(); commentIndex++) { 0886 const QString& comment = commentLayers[commentIndex]; 0887 if (isValidLabel(comment.toLower(), index)) { 0888 boost::optional<QRectF> rect; 0889 extractRect(rect); 0890 if (rect) { 0891 elementMap[index].commentRects.insert(comment, rect.value()); 0892 } 0893 } 0894 } 0895 } 0896 } 0897 } 0898 0899 } 0900 } 0901 0902 // Sort fetched elements and push to array to return... 0903 QList<int> indices = elementMap.keys(); 0904 std::sort(indices.begin(), indices.end(), [](const int& a, const int& b){ 0905 return a < b; 0906 }); 0907 0908 Q_FOREACH(const int& index, indices){ 0909 elements.push_back(elementMap[index]); 0910 } 0911 0912 page.svg = svgDoc; 0913 page.elements = elements; 0914 return page; 0915 } 0916 0917 QString StoryboardDockerDock::buildDurationString(int seconds, int frames) 0918 { 0919 QString durationString = QString::number(seconds); 0920 durationString += i18nc("suffix in spin box in storyboard that means 'seconds'", "s"); 0921 durationString += "+"; 0922 durationString += QString::number(frames); 0923 durationString += i18nc("suffix in spin box in storyboard that means 'frames'", "f"); 0924 return durationString; 0925 } 0926 0927 #include "StoryboardDockerDock.moc" 0928