1 /*
2  *  Copyright (c) 2012 Sven Langkamp <sven.langkamp@gmail.com>
3  *
4  *  This library is free software; you can redistribute it and/or modify
5  *  it under the terms of the GNU Lesser General Public License as published by
6  *  the Free Software Foundation; version 2 of the License, or
7  *  (at your option) any later version.
8  *
9  *  This library is distributed in the hope that it will be useful,
10  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
11  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  *  GNU Lesser General Public License for more details.
13  *
14  *  You should have received a copy of the GNU Lesser General Public License
15  *  along with this program; if not, write to the Free Software
16  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17  */
18 
19 #include "compositiondocker_dock.h"
20 
21 #include <QGridLayout>
22 #include <QListView>
23 #include <QHeaderView>
24 #include <QStyledItemDelegate>
25 #include <QPainter>
26 #include <QInputDialog>
27 #include <QThread>
28 #include <QAction>
29 #include <QStandardPaths>
30 #include <QMenu>
31 #include <QAction>
32 
33 #include <klocalizedstring.h>
34 #include <kactioncollection.h>
35 
36 #include <kis_icon.h>
37 #include <KoCanvasBase.h>
38 #include <KoFileDialog.h>
39 
40 #include <KisPart.h>
41 #include <KisViewManager.h>
42 #include <kis_canvas2.h>
43 #include <KisKineticScroller.h>
44 
45 #include <KisDocument.h>
46 #include <kis_group_layer.h>
47 #include <kis_painter.h>
48 #include <kis_paint_layer.h>
49 #include <kis_action.h>
50 #include <kis_action_manager.h>
51 #include <kis_action_registry.h>
52 
53 #include <dialogs/KisAsyncAnimationFramesSaveDialog.h>
54 #include <animation/KisAnimationRenderingOptions.h>
55 #include <animation/KisAnimationRender.h>
56 #include <kis_image_animation_interface.h>
57 #include <kis_time_range.h>
58 #include <KisMimeDatabase.h>
59 
60 
61 #include "compositionmodel.h"
62 
63 
CompositionDockerDock()64 CompositionDockerDock::CompositionDockerDock( )
65     : QDockWidget(i18n("Compositions"))
66     , m_canvas(0)
67 {
68     QWidget* widget = new QWidget(this);
69     setupUi(widget);
70     m_model = new CompositionModel(this);
71     compositionView->setModel(m_model);
72     compositionView->installEventFilter(this);
73     deleteButton->setIcon(KisIconUtils::loadIcon("edit-delete"));
74     saveButton->setIcon(KisIconUtils::loadIcon("list-add"));
75     moveUpButton->setIcon(KisIconUtils::loadIcon("arrow-up"));
76     moveDownButton->setIcon(KisIconUtils::loadIcon("arrow-down"));
77 
78     deleteButton->setToolTip(i18n("Delete Composition"));
79     saveButton->setToolTip(i18n("New Composition"));
80     exportCompositions->setToolTip(i18n("Export Composition"));
81     moveUpButton->setToolTip(i18n("Move Composition Up"));
82     moveDownButton->setToolTip(i18n("Move Composition Down"));
83 
84     setWidget(widget);
85 
86     connect( compositionView, SIGNAL(doubleClicked(QModelIndex)),
87              this, SLOT(activated(QModelIndex)) );
88 
89     compositionView->setContextMenuPolicy(Qt::CustomContextMenu);
90     connect( compositionView, SIGNAL(customContextMenuRequested(QPoint)),
91              this, SLOT(customContextMenuRequested(QPoint)));
92 
93     connect( deleteButton, SIGNAL(clicked(bool)), this, SLOT(deleteClicked()));
94     connect( saveButton, SIGNAL(clicked(bool)), this, SLOT(saveClicked()));
95     connect( moveUpButton, SIGNAL(clicked(bool)), this, SLOT(moveCompositionUp()));
96     connect( moveDownButton, SIGNAL(clicked(bool)), this, SLOT(moveCompositionDown()));
97 
98     QAction* imageAction = new QAction(KisIconUtils::loadIcon("document-export"), i18n("Export Images"), this);
99     connect(imageAction, SIGNAL(triggered(bool)), this, SLOT(exportImageClicked()));
100 
101     QAction* animationAction = new QAction(KisIconUtils::loadIcon("addblankframe"), i18n("Export Animations"), this);
102     connect(animationAction, SIGNAL(triggered(bool)), this, SLOT(exportAnimationClicked()));
103 
104     exportCompositions->setDefaultAction(imageAction);
105 
106     QMenu* exportMenu = new QMenu(this);
107     exportMenu->addAction(imageAction);
108     exportMenu->addAction(animationAction);
109 
110     exportCompositions->setMenu(exportMenu);
111 
112     connect(exportMenu, &QMenu::triggered, [this](QAction* triggered){
113         exportCompositions->setDefaultAction(triggered);
114     });
115 
116     saveNameEdit->setPlaceholderText(i18n("Insert Name"));
117 
118     QScroller *scroller = KisKineticScroller::createPreconfiguredScroller(compositionView);
119     if (scroller) {
120         connect(scroller, SIGNAL(stateChanged(QScroller::State)), this, SLOT(slotScrollerStateChanged(QScroller::State)));
121     }
122 
123 }
124 
~CompositionDockerDock()125 CompositionDockerDock::~CompositionDockerDock()
126 {
127 
128 }
129 
setCanvas(KoCanvasBase * canvas)130 void CompositionDockerDock::setCanvas(KoCanvasBase * canvas)
131 {
132     if (m_canvas && m_canvas->viewManager()) {
133         Q_FOREACH (KisAction *action, m_actions) {
134             m_canvas->viewManager()->actionManager()->takeAction(action);
135         }
136     }
137 
138     unsetCanvas();
139     setEnabled(canvas != 0);
140 
141     m_canvas = dynamic_cast<KisCanvas2*>(canvas);
142     if (m_canvas && m_canvas->viewManager()) {
143         if (m_actions.isEmpty()) {
144             KisAction *updateAction = m_canvas->viewManager()->actionManager()->createAction("update_composition");
145             connect(updateAction, SIGNAL(triggered()), this, SLOT(updateComposition()));
146             m_actions.append(updateAction);
147 
148             KisAction *renameAction = m_canvas->viewManager()->actionManager()->createAction("rename_composition");
149             connect(renameAction, SIGNAL(triggered()), this, SLOT(renameComposition()));
150             m_actions.append(renameAction);
151 
152         } else {
153             Q_FOREACH (KisAction *action, m_actions) {
154                 m_canvas->viewManager()->actionManager()->addAction(action->objectName(), action);
155             }
156         }
157         updateModel();
158     }
159 }
160 
unsetCanvas()161 void CompositionDockerDock::unsetCanvas()
162 {
163     setEnabled(false);
164     m_canvas = 0;
165     m_model->setCompositions(QList<KisLayerCompositionSP>());
166 }
167 
activated(const QModelIndex & index)168 void CompositionDockerDock::activated(const QModelIndex& index)
169 {
170     KisLayerCompositionSP composition = m_model->compositionFromIndex(index);
171     composition->apply();
172 }
173 
deleteClicked()174 void CompositionDockerDock::deleteClicked()
175 {
176     QModelIndex index = compositionView->currentIndex();
177     if (m_canvas && m_canvas->viewManager() && m_canvas->viewManager()->image() && index.isValid()) {
178         KisLayerCompositionSP composition = m_model->compositionFromIndex(index);
179         m_canvas->viewManager()->image()->removeComposition(composition);
180         updateModel();
181     }
182 }
183 
saveClicked()184 void CompositionDockerDock::saveClicked()
185 {
186     KisImageWSP image = m_canvas->viewManager()->image();
187     if (!image) return;
188 
189     // format as 001, 002 ...
190     QString name = saveNameEdit->text();
191     if (name.isEmpty()) {
192         bool found = false;
193         int i = 1;
194         do {
195             name = QString("%1").arg(i, 3, 10, QChar('0'));
196             found = false;
197             Q_FOREACH (KisLayerCompositionSP composition, m_canvas->viewManager()->image()->compositions()) {
198                 if (composition->name() == name) {
199                     found = true;
200                     break;
201                 }
202             }
203             i++;
204         } while(found && i < 1000);
205     }
206     KisLayerCompositionSP composition(new KisLayerComposition(image, name));
207     composition->store();
208     image->addComposition(composition);
209     saveNameEdit->clear();
210     updateModel();
211     compositionView->setCurrentIndex(m_model->index(image->compositions().count()-1, 0));
212     image->setModified();
213 }
214 
moveCompositionUp()215 void CompositionDockerDock::moveCompositionUp()
216 {
217     QModelIndex index = compositionView->currentIndex();
218     if (m_canvas && m_canvas->viewManager() && m_canvas->viewManager()->image() && index.isValid()) {
219         KisLayerCompositionSP composition = m_model->compositionFromIndex(index);
220         m_canvas->viewManager()->image()->moveCompositionUp(composition);
221         updateModel();
222         compositionView->setCurrentIndex(m_model->index(m_canvas->viewManager()->image()->compositions().indexOf(composition),0));
223     }
224 }
225 
moveCompositionDown()226 void CompositionDockerDock::moveCompositionDown()
227 {
228     QModelIndex index = compositionView->currentIndex();
229     if (m_canvas && m_canvas->viewManager() && m_canvas->viewManager()->image() && index.isValid()) {
230         KisLayerCompositionSP composition = m_model->compositionFromIndex(index);
231         m_canvas->viewManager()->image()->moveCompositionDown(composition);
232         updateModel();
233         compositionView->setCurrentIndex(m_model->index(m_canvas->viewManager()->image()->compositions().indexOf(composition),0));
234     }
235 }
236 
updateModel()237 void CompositionDockerDock::updateModel()
238 {
239     if (m_model && m_canvas && m_canvas->viewManager() && m_canvas->viewManager()->image()) {
240         m_model->setCompositions(m_canvas->viewManager()->image()->compositions());
241     }
242 }
243 
exportImageClicked()244 void CompositionDockerDock::exportImageClicked()
245 {
246     if (m_canvas && m_canvas->viewManager() && m_canvas->viewManager()->image()) {
247         QString path;
248 
249         KoFileDialog dialog(0, KoFileDialog::OpenDirectory, "compositiondockerdock");
250         dialog.setCaption(i18n("Select a Directory"));
251         dialog.setDefaultDir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation));
252         path = dialog.filename();
253 
254 
255         if (path.isNull()) return;
256 
257         if (!path.endsWith('/')) {
258             path.append('/');
259         }
260 
261         KisImageSP image = m_canvas->viewManager()->image();
262         QString filename = m_canvas->viewManager()->document()->localFilePath();
263         if (!filename.isEmpty()) {
264             QFileInfo info(filename);
265             path += info.completeBaseName() + '_';
266         }
267 
268         KisLayerCompositionSP currentComposition = toQShared(new KisLayerComposition(image, "temp"));
269         currentComposition->store();
270 
271         Q_FOREACH (KisLayerCompositionSP composition, image->compositions()) {
272             if (!composition->isExportEnabled()) {
273                 continue;
274             }
275 
276             composition->apply();
277             image->waitForDone();
278             image->refreshGraph();
279 
280             QRect r = image->bounds();
281 
282             KisDocument *d = KisPart::instance()->createDocument();
283 
284             KisImageSP dst = new KisImage(d->createUndoStore(), r.width(), r.height(), image->colorSpace(), composition->name());
285             dst->setResolution(image->xRes(), image->yRes());
286             d->setCurrentImage(dst);
287             KisPaintLayer* paintLayer = new KisPaintLayer(dst, "projection", OPACITY_OPAQUE_U8);
288             KisPainter gc(paintLayer->paintDevice());
289             gc.bitBlt(QPoint(0, 0), image->rootLayer()->projection(), r);
290             dst->addNode(paintLayer, dst->rootLayer(), KisLayerSP(0));
291 
292             dst->refreshGraph();
293             dst->waitForDone();
294 
295             d->setFileBatchMode(true);
296 
297             d->exportDocumentSync(QUrl::fromLocalFile(path + composition->name() + ".png"), "image/png");
298             d->deleteLater();
299         }
300 
301         currentComposition->apply();
302         image->waitForDone();
303         image->refreshGraph();
304     }
305 
306 }
307 
exportAnimationClicked()308 void CompositionDockerDock::exportAnimationClicked()
309 {
310     KisConfig cfg(true);
311     KisPropertiesConfigurationSP settings = cfg.exportConfiguration("ANIMATION_EXPORT");
312     KisAnimationRenderingOptions exportOptions;
313     exportOptions.fromProperties(settings);
314 
315     if (m_canvas &&
316         m_canvas->viewManager() &&
317         m_canvas->viewManager()->image() &&
318         m_canvas->viewManager()->image()->animationInterface() &&
319         m_canvas->viewManager()->document()) {
320 
321         QString path;
322 
323         KoFileDialog dialog(0, KoFileDialog::OpenDirectory, "compositiondockerdock");
324         dialog.setCaption(i18n("Select a Directory"));
325         dialog.setDefaultDir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation));
326         path = dialog.filename();
327 
328 
329         if (path.isNull()) return;
330 
331         if (!path.endsWith('/')) {
332             path.append('/');
333         }
334 
335         KisImageSP image = m_canvas->viewManager()->image();
336         QString filename = m_canvas->viewManager()->document()->localFilePath();
337         if (!filename.isEmpty()) {
338             QFileInfo info(filename);
339             path += info.completeBaseName();
340         }
341 
342         KisLayerCompositionSP currentComposition = toQShared(new KisLayerComposition(image, "temp"));
343         currentComposition->store();
344 
345         const QString frameMimeType = settings->getPropertyLazy("frame_mimetype", frameMimeType);
346         const QString imageExtension = KisMimeDatabase::suffixesForMimeType(frameMimeType).first();
347         const QString videoExtension = KisMimeDatabase::suffixesForMimeType(exportOptions.videoMimeType).first();
348 
349         Q_FOREACH (KisLayerCompositionSP composition, image->compositions()) {
350             if(!composition->isExportEnabled())
351                 continue;
352 
353             composition->apply();
354             image->waitForDone();
355             image->refreshGraph();
356 
357             KisTimeRange range = image->animationInterface()->fullClipRange();
358 
359             exportOptions.firstFrame = range.start();
360             exportOptions.lastFrame = range.end();
361             exportOptions.width = image->width();
362             exportOptions.height = image->height();
363             exportOptions.videoFileName = QString("%1/%2/video.%3").arg(path).arg(composition->name()).arg(videoExtension);
364             exportOptions.directory = QString("%1/%2").arg(path).arg(composition->name());
365             exportOptions.basename = QString("frame");
366             exportOptions.wantsOnlyUniqueFrameSequence = true;
367 
368             KisAnimationRender::render(m_canvas->viewManager()->document(), m_canvas->viewManager(), exportOptions);
369         }
370 
371         currentComposition->apply();
372         image->waitForDone();
373         image->refreshGraph();
374     }
375 }
376 
eventFilter(QObject * obj,QEvent * event)377 bool CompositionDockerDock::eventFilter(QObject* obj, QEvent* event)
378 {
379     if (event->type() == QEvent::KeyPress ) {
380         QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
381         if (keyEvent->key() == Qt::Key_Up || keyEvent->key() == Qt::Key_Down) {
382             // new index will be set after the method is called
383             QTimer::singleShot(0, this, SLOT(activateCurrentIndex()));
384         }
385         return false;
386     } else {
387         return QObject::eventFilter(obj, event);
388     }
389 }
390 
activateCurrentIndex()391 void CompositionDockerDock::activateCurrentIndex()
392 {
393     QModelIndex index = compositionView->currentIndex();
394     if (index.isValid()) {
395         activated(index);
396     }
397 }
398 
customContextMenuRequested(QPoint pos)399 void CompositionDockerDock::customContextMenuRequested(QPoint pos)
400 {
401     if (m_actions.isEmpty()) return;
402 
403     QMenu menu;
404     Q_FOREACH (KisAction *action, m_actions) {
405         menu.addAction(action);
406 
407     }
408     menu.exec(compositionView->mapToGlobal(pos));
409 }
410 
updateComposition()411 void CompositionDockerDock::updateComposition()
412 {
413     QModelIndex index = compositionView->currentIndex();
414     if (m_canvas && m_canvas->viewManager() && m_canvas->viewManager()->image() && index.isValid()) {
415         KisLayerCompositionSP composition = m_model->compositionFromIndex(index);
416         composition->store();
417         m_canvas->image()->setModified();
418     }
419 }
420 
renameComposition()421 void CompositionDockerDock::renameComposition()
422 {
423     dbgKrita << "rename";
424     QModelIndex index = compositionView->currentIndex();
425     if (m_canvas && m_canvas->viewManager() && m_canvas->viewManager()->image() && index.isValid()) {
426         KisLayerCompositionSP composition = m_model->compositionFromIndex(index);
427         bool ok;
428         QString name = QInputDialog::getText(this, i18n("Rename Composition"),
429                                              i18n("New Name:"), QLineEdit::Normal,
430                                              composition->name(), &ok);
431         if (ok && !name.isEmpty()) {
432             composition->setName(name);
433             m_canvas->image()->setModified();
434         }
435     }
436 }
437 
438 
439