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>&nbsp;" + 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