Krita Source Code Documentation
Loading...
Searching...
No Matches
recorderdocker_dock.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2019 Shi Yan <billconan@gmail.net>
3 * SPDX-FileCopyrightText: 2020 Dmitrii Utkin <loentar@gmail.com>
4 *
5 * SPDX-License-Identifier: LGPL-2.1-only
6 */
7
9#include "recorder_config.h"
10#include "recorder_writer.h"
11#include "recorder_const.h"
12#include "ui_recorderdocker.h"
14#include "recorder_export.h"
17
18#include <klocalizedstring.h>
19#include <kis_action_registry.h>
20#include <kis_canvas2.h>
21#include <kis_icon_utils.h>
22#include <kis_statusbar.h>
23#include <KisDocument.h>
24#include <KisViewManager.h>
25#include <KoDocumentInfo.h>
26#include <kactioncollection.h>
27#include <KisPart.h>
28#include <KisKineticScroller.h>
29#include "KisMainWindow.h"
30#include "KoFileDialog.h"
31
32#include <QFileInfo>
33#include <QPointer>
34#include <QMessageBox>
35#include <QTimer>
36#include <QRegularExpression>
37
38#ifdef Q_OS_ANDROID
39#include <QDir>
40#include <QFile>
41#include <QThreadPool>
42#endif
43
44namespace
45{
46const QString keyActionRecordToggle = "recorder_record_toggle";
47const QString keyActionExport = "recorder_export";
48
49const QString activeColorGreen(" color='#5cab25'");
50const QString inactiveColorGreen(" color='#b4e196'");
51const QString activeColorOrange(" color='#ca8f14'");
52const QString inactiveColorOrange(" color='#ffe5af'");
53const QString activeColorRed(" color='#da4453'");
54const QString inactiveColorRed(" color='#f2c4c9'");
55const QString inactiveColorGray(" color='#3e3e3e'");
56
57const QColor textColorOrange(0xff, 0xe5, 0xaf);
58const QColor buttonColorOrange(0xca, 0x8f, 0x14);
59const QColor textColorRed(0xf2, 0xc4, 0xc9);
60const QColor buttonColorRed(0xda, 0x44, 0x53);
61
62}
63
64
66{
67public:
69 QScopedPointer<Ui::RecorderDocker> ui;
74
75 QAction *recordToggleAction = nullptr;
76 QAction *exportAction = nullptr;
77
79 QString prefix;
81 double captureInterval = 0.;
83 int quality = 0;
84 int compression = 0;
85 int resolution = 0;
86 bool realTimeCaptureMode = false;
88 bool recordAutomatically = false;
89 bool paused = true;
90#ifdef Q_OS_ANDROID
91 bool internalMoveInProgress{false};
92#endif
95
98
99 QMap<QString, bool> enabledIds;
100
102 : q(q_ptr)
103 , ui(new Ui::RecorderDocker())
104 , writer(es)
105 , statusBarLabel(new QLabel())
106 , statusBarWarningLabel(new QLabel())
107 {
109 statusBarWarningLabel->setPixmap(KisIconUtils::loadIcon("warning").pixmap(16, 16));
110 statusBarWarningLabel->hide();
111 warningTimer.setInterval(10000);
112 warningTimer.setSingleShot(true);
113 pausedTimer.setSingleShot(true);
114 connect(&warningTimer, SIGNAL(timeout()), q, SLOT(onWarningTimeout()));
115 connect(&pausedTimer, SIGNAL(timeout()), q, SLOT(onPausedTimeout()));
116 }
117
118 // For technical reasons recorder can not capture an image that is larger than 2^31 - 1 bytes in size, It is also
119 // kinda absurd that you'd even want to do it, so we just shouldn't support it
121 {
122 QSize imageSize = canvas->image()->size();
123
124 // We always convert to RGBA8, so can assume 4 bytes/pixel
125 quint64 totalSize = (quint64)imageSize.width() * (quint64)imageSize.height() * 4;
126
127 return totalSize <= 2147483647;
128 }
129
131 {
132 RecorderConfig config(true);
134#ifdef Q_OS_ANDROID
135 fixInternalSnapshotDirectory();
136#endif
138 format = config.format();
139 quality = config.quality();
140 compression = config.compression();
141 resolution = config.resolution();
145 q->exportSettings->lockFps = true;
147 }
150
152 }
153
155 {
156 RecorderExportConfig config(true);
157 q->exportSettings->fps = config.fps();
158 }
159
161 int index = 0;
162 QString title;
163 QString hint;
164 int minValue = 0;
165 int maxValue = 0;
166 QString suffix;
167 int factor = 0;
168 switch (format) {
170 index = 0;
171 title = i18nc("Title for label. JPEG Quality level", "Quality:");
172 hint = i18nc("@tooltip", "Greater value will produce a larger file and a better quality. Doesn't affect CPU consumption.\nValues lower than 50 are not recommended due to high artifacts.");
173 minValue = 1;
174 maxValue = 100;
175 suffix = "%";
176 factor = quality;
177 break;
179 index = 1;
180 title = i18nc("Title for label. PNG Compression level", "Compression:");
181 hint = i18nc("@tooltip", "Greater value will produce a smaller file but will require more from your CPU. Doesn't affect quality.\nCompression set to 0 is not recommended due to high disk space consumption.\nValues above 3 are not recommended due to high performance impact.");
182 minValue = 0;
183 maxValue = 5;
184 suffix = "";
185 factor = compression;
186 break;
187 }
188
189 ui->comboFormat->setCurrentIndex(index);
190 ui->labelQuality->setText(title);
191 ui->spinQuality->setToolTip(hint);
192 QSignalBlocker blocker(ui->spinQuality);
193 ui->spinQuality->setMinimum(minValue);
194 ui->spinQuality->setMaximum(maxValue);
195 ui->spinQuality->setValue(factor);
196 ui->spinQuality->setSuffix(suffix);
197 }
198
200 QString title;
201 double minValue = 0;
202 double maxValue = 0;
203 double value = 0;
204 int decimals = 0;
205 QString suffix;
206 QSignalBlocker blocker(ui->spinRate);
207
209 title = i18nc("Title for label. Video frames per second", "Video FPS:");
210 minValue = 1;
211 maxValue = 60;
212 decimals = 0;
214 suffix = "";
215 disconnect(ui->spinRate, SIGNAL(valueChanged(double)), q, SLOT(onCaptureIntervalChanged(double)));
216 connect(ui->spinRate, SIGNAL(valueChanged(double)), q, SLOT(onVideoFPSChanged(double)));
217 } else {
218 title = i18nc("Title for label. Capture rate", "Capture interval:");
219 minValue = 0.10;
220 maxValue = 100.0;
221 decimals = 1;
223 suffix = " sec.";
224 disconnect(ui->spinRate, SIGNAL(valueChanged(double)), q, SLOT(onVideoFPSChanged(double)));
225 connect(ui->spinRate, SIGNAL(valueChanged(double)), q, SLOT(onCaptureIntervalChanged(double)));
226 }
227
228 ui->labelRate->setText(title);
229 ui->spinRate->setDecimals(decimals);
230 ui->spinRate->setMinimum(minValue);
231 ui->spinRate->setMaximum(maxValue);
232 ui->spinRate->setSuffix(suffix);
233 ui->spinRate->setValue(value);
234 }
235
237 {
238 outputDirectory = snapshotDirectory % QDir::separator() % prefix % QDir::separator();
239 writer.setup({
241 format,
242 quality,
248 }
249
250 QString getPrefix()
251 {
252 return !canvas ? ""
253 : canvas->imageView()->document()->documentInfo()->aboutInfo("creation-date").remove(QRegularExpression("[^0-9]"));
254 }
255
256 void updateComboResolution(quint32 width, quint32 height)
257 {
258 const QStringList titles = {
259 i18nc("Use original resolution for the frames when recording the canvas", "Original"),
260 i18nc("Use the resolution two times smaller than the original resolution for the frames when recording the canvas", "Half"),
261 i18nc("Use the resolution four times smaller than the original resolution for the frames when recording the canvas", "Quarter")
262 };
263
264 QStringList items;
265 for (int index = 0, len = titles.length(); index < len; ++index) {
266 int divider = 1 << index;
267 items += QString("%1 (%2x%3)").arg(titles[index])
268 .arg((width / divider) & ~1)
269 .arg((height / divider) & ~1);
270 }
271 QSignalBlocker blocker(ui->comboResolution);
272 const int currentIndex = ui->comboResolution->currentIndex();
273 ui->comboResolution->clear();
274 ui->comboResolution->addItems(items);
275 ui->comboResolution->setCurrentIndex(currentIndex);
276 }
277
278 void updateRecordStatus(bool isRecording)
279 {
280 recordToggleAction->setChecked(isRecording);
281 recordToggleAction->setEnabled(true);
282
283 QSignalBlocker blocker(ui->buttonRecordToggle);
284 ui->buttonRecordToggle->setChecked(isRecording);
285 ui->buttonRecordToggle->setIcon(KisIconUtils::loadIcon(isRecording ? "media-playback-stop" : "media-record"));
286 ui->buttonRecordToggle->setText(isRecording ? i18nc("Stop recording the canvas", "Stop")
287 : i18nc("Start recording the canvas", "Record"));
288
289 if (canRecord()) {
290 ui->buttonRecordToggle->setEnabled(true);
291 ui->buttonRecordToggle->setToolTip("");
292 } else {
293 ui->buttonRecordToggle->setEnabled(false);
294 ui->buttonRecordToggle->setToolTip(i18n("Image too large to be recorded"));
295 }
296 ui->widgetSettings->setEnabled(!isRecording);
297
298 statusBarLabel->setVisible(isRecording);
299
300 if (!canvas)
301 return;
302
303 KisStatusBar *statusBar = canvas->viewManager()->statusBar();
304 if (isRecording) {
306 statusBar->addExtraWidget(statusBarLabel);
308 } else {
311 }
312 }
313
315 {
316 auto threads = writer.recorderThreads.get();
317 auto threadsInUse = writer.recorderThreads.getUsed();
318 QString label("<font style='letter-spacing:-4px'>");
319 QString activeColor;
320 QString inactiveColor;
321 for (unsigned int threadNr = 1; threadNr <= ThreadSystemValue::MaxThreadCount ; threadNr++)
322 {
323 if (threadNr > threads) {
324 activeColor = inactiveColorGray;
325 inactiveColor = inactiveColorGray;
326 } else if (threadNr > ThreadSystemValue::MaxRecordThreadCount) {
327 activeColor = activeColorRed;
328 inactiveColor = inactiveColorRed;
329 } else if (threadNr > ThreadSystemValue::IdealRecordThreadCount) {
330 activeColor = activeColorOrange;
331 inactiveColor = inactiveColorOrange;
332 } else {
333 activeColor = activeColorGreen;
334 inactiveColor = inactiveColorGreen;
335 }
336 label.append(QString("<font%1>▍</font>")
337 .arg(threadNr <= threadsInUse ? activeColor : inactiveColor));
338 }
339 // don't remove empty <font></font> tag else label will jump a few pixels around
340 label.append(QString("</font><font> %1 </font><font%2>●</font>")
341 .arg(i18nc("Recording symbol", "REC"))
342 .arg(paused ? "" : activeColorRed));
343 statusBarLabel->setText(label);
344 statusBarLabel->setToolTip(paused ? i18n("Recorder is paused") : QString(i18n("Active recording with %1 of %2 available threads")).arg(threadsInUse).arg(threads));
345 }
346
347 void showWarning(const QString &hint) {
348 if (statusBarWarningLabel->isHidden()) {
349 statusBarWarningLabel->setToolTip(hint);
350 statusBarWarningLabel->show();
351 warningTimer.start();
352 }
353 }
354
356 {
357 QString toolTipText;
358 auto threads = writer.recorderThreads.get();
360 // Number of threads exceeds ideal thread count
361 // -> switch color of threads slider and spin wheel to red
362 QPalette pal;
363 pal.setColor(QPalette::Text, textColorRed);
364 pal.setColor(QPalette::Button, buttonColorRed);
365 ui->spinThreads->setPalette(pal);
366 ui->sliderThreads->setPalette(pal);
367 toolTipText = QString(
368 i18n("Set the number of recording threads.\nThe number of threads exceeds the ideal max number of your hardware setup.\nPlease be aware, that a number greater than %1 probably won't give you any performance boost.")
370 } else if (threads > ThreadSystemValue::IdealRecordThreadCount) {
371 // Number of threads exceeds ideal recorder thread count
372 // -> switch color of threads slider and spin wheel to orange
373 QPalette pal;
374 pal.setColor(QPalette::Text, textColorOrange);
375 pal.setColor(QPalette::Button, buttonColorOrange);
376 ui->spinThreads->setPalette(pal);
377 ui->sliderThreads->setPalette(pal);
378 toolTipText = QString(
379 i18n("Set the number of recording threads.\nAccording to your hardware setup you should record with no more than %1 threads.\nYou can play around with one or two more threads, but keep an eye on your overall system performance.")
381 } else {
382 ui->spinThreads->setPalette(threadsSpinPalette);
383 ui->sliderThreads->setPalette(threadsSliderPalette);
384 toolTipText = i18n("Set the number of threads to be used for recording.");
385 }
386 ui->spinThreads->setToolTip(toolTipText);
387 ui->sliderThreads->setToolTip(toolTipText);
388 }
389
390#ifdef Q_OS_ANDROID
391 void fixInternalSnapshotDirectory()
392 {
393 // Older versions of Krita used an internal directory as the snapshots
394 // directory by default, which is a bogus place to save stuff to because
395 // the user can't access it. That means the files stored there are stuck
396 // inaccessible and once the user picks a "real" directory, they can no
397 // longer even delete the files. So here we're rectifying the situation.
398 if (snapshotDirectory == RecorderConfig::defaultInternalSnapshotDirectory()) {
399 // Internal path got persisted to settings. Clear that out, replace
400 // it with the default of nothing.
401 snapshotDirectory = QString();
402 } else {
403 q->moveFilesFromInternalSnapshotDirectory();
404 }
405 }
406#endif
407};
408
410 : QDockWidget(i18nc("Title of the docker", "Recorder"))
411 , exportSettings(new RecorderExportSettings())
412 , d(new Private(*exportSettings, this))
413{
414 QWidget* page = new QWidget(this);
415 d->ui->setupUi(page);
416
417 d->ui->buttonManageRecordings->setIcon(KisIconUtils::loadIcon("configure-thicker"));
418 d->ui->buttonBrowse->setIcon(KisIconUtils::loadIcon("folder"));
419 d->ui->buttonRecordToggle->setIcon(KisIconUtils::loadIcon("media-record"));
420 d->ui->buttonExport->setIcon(KisIconUtils::loadIcon("document-export-16"));
421 d->ui->sliderThreads->setTickPosition(QSlider::TickPosition::TicksBelow);
422 d->ui->sliderThreads->setMinimum(1);
423 d->ui->sliderThreads->setMaximum(ThreadSystemValue::MaxThreadCount);
424 d->ui->spinThreads->setMinimum(1);
425 d->ui->spinThreads->setMaximum(ThreadSystemValue::MaxThreadCount);
426 d->threadsSpinPalette = d->ui->spinThreads->palette();
427 d->threadsSliderPalette = d->ui->sliderThreads->palette();
428
429 d->loadSettings();
431 d->updateThreadUi();
432
433 d->ui->editDirectory->setText(d->snapshotDirectory);
434 d->ui->spinQuality->setValue(d->quality);
435 d->ui->spinThreads->setValue(d->writer.recorderThreads.get());
436 d->ui->comboResolution->setCurrentIndex(d->resolution);
437 d->ui->checkBoxRealTimeCaptureMode->setChecked(d->realTimeCaptureMode);
438 d->ui->checkBoxRecordIsolateMode->setChecked(d->recordIsolateLayerMode);
439 d->ui->checkBoxAutoRecord->setChecked(d->recordAutomatically);
440
442 d->recordToggleAction = actionRegistry->makeQAction(keyActionRecordToggle, this);
443 d->exportAction = actionRegistry->makeQAction(keyActionExport, this);
444
445 connect(d->recordToggleAction, SIGNAL(toggled(bool)), d->ui->buttonRecordToggle, SLOT(setChecked(bool)));
446 connect(d->exportAction, SIGNAL(triggered()), d->ui->buttonExport, SIGNAL(clicked()));
447 connect(d->ui->buttonRecordToggle, SIGNAL(toggled(bool)), d->ui->buttonExport, SLOT(setDisabled(bool)));
449 d->ui->buttonExport->setDisabled(true);
450
451 // Need to register toolbar actions before attaching canvas else it wont appear after restart.
452 // Is there any better way to do this?
453 connect(KisPart::instance(), SIGNAL(sigMainWindowIsBeingCreated(KisMainWindow *)),
455
456 connect(d->ui->buttonManageRecordings, SIGNAL(clicked()), this, SLOT(onManageRecordingsButtonClicked()));
457 connect(d->ui->buttonBrowse, SIGNAL(clicked()), this, SLOT(slotSelectSnapshotDirectory()));
458 connect(d->ui->comboFormat, SIGNAL(currentIndexChanged(int)), this, SLOT(onFormatChanged(int)));
459 connect(d->ui->spinQuality, SIGNAL(valueChanged(int)), this, SLOT(onQualityChanged(int)));
460 connect(d->ui->spinThreads, SIGNAL(valueChanged(int)), this, SLOT(onThreadsChanged(int)));
461 connect(d->ui->comboResolution, SIGNAL(currentIndexChanged(int)), this, SLOT(onResolutionChanged(int)));
462 connect(d->ui->checkBoxRealTimeCaptureMode, SIGNAL(toggled(bool)), this, SLOT(onRealTimeCaptureModeToggled(bool)));
463 connect(d->ui->checkBoxRecordIsolateMode, SIGNAL(toggled(bool)), this, SLOT(onRecordIsolateLayerModeToggled(bool)));
464 connect(d->ui->checkBoxAutoRecord, SIGNAL(toggled(bool)), this, SLOT(onAutoRecordToggled(bool)));
465 connect(d->ui->buttonRecordToggle, SIGNAL(toggled(bool)), this, SLOT(onRecordButtonToggled(bool)));
466 connect(d->ui->buttonExport, SIGNAL(clicked()), this, SLOT(onExportButtonClicked()));
467
468 connect(&d->writer.recorderThreads, SIGNAL(notifyInUseChange(bool)), this, SLOT(onActiveRecording(bool)));
469 connect(&d->writer.recorderThreads, SIGNAL(notifyInUseChange(bool)), this, SLOT(onUpdateRecIndicator()));
470 connect(&d->writer, SIGNAL(started()), this, SLOT(onWriterStarted()));
471 connect(&d->writer, SIGNAL(stopped()), this, SLOT(onWriterStopped()));
472 connect(&d->writer, SIGNAL(frameWriteFailed()), this, SLOT(onWriterFrameWriteFailed()));
473 connect(&d->writer, SIGNAL(recorderStopWarning()), this, SLOT(onRecorderStopWarning()));
474 connect(&d->writer, SIGNAL(lowPerformanceWarning()), this, SLOT(onLowPerformanceWarning()));
475
476
477 QScroller *scroller = KisKineticScroller::createPreconfiguredScroller(d->ui->scrollArea);
478 if (scroller) {
479 connect(scroller, SIGNAL(stateChanged(QScroller::State)),
480 this, SLOT(slotScrollerStateChanged(QScroller::State)));
481 }
482
483 // The system is not efficient enough for the RealTime Recording Feature
485 {
486 d->ui->checkBoxRealTimeCaptureMode->setCheckState(Qt::Unchecked);
487 d->ui->checkBoxRealTimeCaptureMode->setDisabled(true);
488 d->ui->checkBoxRealTimeCaptureMode->setToolTip(
489 i18n("Your system is not efficient enough for this feature"));
490 }
491
492 setWidget(page);
493}
494
496{
497 delete d;
498 delete exportSettings;
499}
500
502{
503 setEnabled(canvas != nullptr);
504
505 if (d->canvas == canvas)
506 return;
507
508 d->canvas = dynamic_cast<KisCanvas2*>(canvas);
510
511 if (!d->canvas)
512 return;
513
514 KisDocument *document = d->canvas->imageView()->document();
515 d->updateComboResolution(document->image()->width(), document->image()->height());
516
517 d->prefix = d->getPrefix();
518 bool wasToggled = false;
519 if (d->recordAutomatically && !d->snapshotDirectory.isEmpty()
520 && !d->enabledIds.contains(document->linkedResourcesStorageId()) && d->canRecord()) {
521 wasToggled = onRecordButtonToggled(true);
522 }
523 if (!wasToggled) { // onRecordButtonToggled(true) may call these, don't call them twice.
525 d->updateUiFormat();
526 }
528
529 bool enabled = d->enabledIds.value(document->linkedResourcesStorageId(), false);
530 d->writer.setEnabled(enabled);
531 d->updateRecordStatus(enabled);
532}
533
535{
536 d->updateRecordStatus(false);
537 d->recordToggleAction->setChecked(false);
538 setEnabled(false);
539 d->writer.stop();
540 d->writer.setCanvas(nullptr);
541 d->canvas = nullptr;
542 d->enabledIds.clear();
543}
544
546{
547 KisKActionCollection *actionCollection = window->viewManager()->actionCollection();
548 actionCollection->addAction(keyActionRecordToggle, d->recordToggleAction);
549 actionCollection->addAction(keyActionExport, d->exportAction);
550}
551
553{
554 QSignalBlocker blocker(d->ui->buttonRecordToggle);
555
556 // Ask the user to pick a directory if we don't have one. This should only
557 // happen on Android, other operating systems have a non-empty default that
558 // the user is not able to clear out via the user interface.
559 if (checked && d->snapshotDirectory.isEmpty()) {
561 if (d->snapshotDirectory.isEmpty()) {
562 d->ui->buttonRecordToggle->setChecked(false);
563 d->recordToggleAction->setChecked(false);
564 return false;
565 }
566 }
567
568 d->recordToggleAction->setChecked(checked);
569
570 if (!d->canvas)
571 return false;
572
573 const QString &id = d->canvas->imageView()->document()->linkedResourcesStorageId();
574
575 bool wasEmpty = !d->enabledIds.values().contains(true);
576
577 d->enabledIds[id] = checked;
578
579 bool isEmpty = !d->enabledIds.values().contains(true);
580
581 d->writer.setEnabled(checked);
582
583 if (isEmpty == wasEmpty) {
584 d->updateRecordStatus(checked);
585 return false;
586 }
587
588
589 d->ui->buttonRecordToggle->setEnabled(false);
590
591 if (checked) {
593 d->updateUiFormat();
594 d->writer.start();
595
596 // Calculate Rec symbol activity timeout depending on the capture interval
597 // The pausedTimer interval is set to a slightly greater value than the capture interval
598 // to avoid flickering for ongoing painting. This is also the reason for the min and max
599 // values 305 and 2005 (instead of 300 and 2000, respectively).
600 if (d->realTimeCaptureMode) {
601 d->pausedTimer.setInterval(qBound(305, static_cast<int>(1000.0/static_cast<double>(exportSettings->fps)) + 5,2005));
602 } else {
603 d->pausedTimer.setInterval(qBound(305, static_cast<int>(qMax(d->captureInterval, .1) * 1000.0) + 5, 2005));
604 }
605 } else {
606 d->writer.stop();
607 d->warningTimer.stop();
608 d->pausedTimer.stop();
609 d->statusBarWarningLabel->hide();
610 d->paused = true;
611 }
612
613 return true;
614}
615
617{
618 if (!d->canvas)
619 return;
620
621 KisDocument *document = d->canvas->imageView()->document();
622
623#ifndef Q_OS_ANDROID
624 exportSettings->videoFileName = QFileInfo(document->caption().trimmed()).completeBaseName();
625#endif
629
630 RecorderExport exportDialog(exportSettings, this);
631 exportDialog.setup();
632 exportDialog.exec();
633
635 d->ui->spinRate->setValue(exportSettings->fps);
636}
637
639{
640 RecorderSnapshotsManager snapshotsManager(this);
641 snapshotsManager.execFor(d->snapshotDirectory);
642}
643
645{
646 KoFileDialog dialog(this, KoFileDialog::OpenDirectory, "SelectRecordingsDirectory");
647 dialog.setCaption(i18n("Select a Directory for Recordings"));
648 dialog.setDefaultDir(d->ui->editDirectory->text());
649 QString directory = dialog.filename();
650 if (!directory.isEmpty()) {
651 d->ui->editDirectory->setText(directory);
652 RecorderConfig(false).setSnapshotDirectory(directory);
653 d->loadSettings();
654 }
655}
656
663
665{
666 d->recordAutomatically = checked;
668 d->loadSettings();
669}
670
682
684{
685 d->captureInterval = interval;
686 RecorderConfig(false).setCaptureInterval(interval);
687 d->loadSettings();
688}
695
697{
698 switch (d->format) {
700 d->quality = value;
702 d->loadSettings();
703 break;
707 d->loadSettings();
708 break;
709 }
710}
711
713{
714 d->format = static_cast<RecorderFormat>(format);
715 d->updateUiFormat();
716
718 d->loadSettings();
719}
720
722{
723 d->resolution = resolution;
724 RecorderConfig(false).setResolution(resolution);
725 d->loadSettings();
726}
727
729{
730 d->writer.recorderThreads.set(threads);
731 RecorderConfig(false).setThreads(threads);
732 d->loadSettings();
733 d->updateThreadUi();
734}
735
740
745
750
751void RecorderDockerDock::onActiveRecording(bool valueWasIncreased)
752{
753 if (!valueWasIncreased)
754 return;
755
756 d->paused = false;
757 d->pausedTimer.start();
758}
759
765
767{
768 QMessageBox::warning(this, i18nc("@title:window", "Recorder"),
769 i18n("The recorder has been stopped due to failure while writing a frame. Please check free disk space and start the recorder again."));
770}
771
773{
774 QMessageBox::warning(this, i18nc("@title:window", "Recorder"),
775 i18n("Krita was unable to stop the recorder probably. Please try to restart Krita."));
776}
778{
779 if (d->realTimeCaptureMode) {
780 d->showWarning(i18n("Low performance warning. The recorder is not able to write all the frames in time during Real Time Capture mode.\nTry to reduce the frame rate for the ffmpeg export or reduce the scaling filtering in the canvas acceleration settings."));
781 } else {
782 d->showWarning(i18n("Low performance warning. The recorder is not able to write all the frames in time.\nTry to increase the capture interval or reduce the scaling filtering in the canvas acceleration settings."));
783 }
784}
785
790
792{
794}
795
796#ifdef Q_OS_ANDROID
797void RecorderDockerDock::moveFilesFromInternalSnapshotDirectory()
798{
799 if (!d->internalMoveInProgress) {
800 const QString &internalPath = RecorderConfig::defaultInternalSnapshotDirectory();
801 if (!d->snapshotDirectory.isEmpty() && QFileInfo::exists(internalPath)) {
802 // The user has picked a directory to record to, but the nonsense
803 // internal directory is present and may have stuff inside that
804 // would become effectively inaccessible. To fix that, we move the
805 // files over to the selected directory. Of course moving files on
806 // Android is gobsmackingly slow, so we'll have to do it in the
807 // background to not lock the UI for ages. The moving should be
808 // re-entrant, so getting interrupted and continuing later is fine.
809 qWarning().nospace() << "Moving recordings stuck in internal directory '" << internalPath
810 << "' to selected directory '" << d->snapshotDirectory << "'";
811 RecorderDockerInternalSnapshotsMover *mover =
812 new RecorderDockerInternalSnapshotsMover(internalPath, d->snapshotDirectory);
813 connect(mover,
814 &RecorderDockerInternalSnapshotsMover::sigMoveFinished,
815 this,
816 &RecorderDockerDock::slotInternalSnapshotMoveFinished,
817 Qt::QueuedConnection);
818 d->internalMoveInProgress = true;
819 QThreadPool::globalInstance()->start(mover);
820 }
821 }
822}
823
824void RecorderDockerDock::slotInternalSnapshotMoveFinished(const QString &srcRoot)
825{
826 d->internalMoveInProgress = false;
827 if (srcRoot != d->snapshotDirectory) {
828 // Directory changed meanwhile, trigger another move.
829 moveFilesFromInternalSnapshotDirectory();
830 }
831}
832
833RecorderDockerInternalSnapshotsMover::RecorderDockerInternalSnapshotsMover(const QString &srcRoot,
834 const QString &dstRoot)
835 : m_srcRoot(srcRoot)
836 , m_dstRoot(dstRoot)
837{
838}
839
840void RecorderDockerInternalSnapshotsMover::run()
841{
842 moveFromInternalSnapshotDirectory(QDir(m_srcRoot), QDir(m_dstRoot));
843 if (!QDir().rmdir(m_srcRoot)) {
844 qWarning().nospace() << "Failed to remove root directory '" << m_srcRoot << "'";
845 }
846 Q_EMIT sigMoveFinished(m_srcRoot);
847}
848
849void RecorderDockerInternalSnapshotsMover::moveFromInternalSnapshotDirectory(const QDir &src, const QDir &dst)
850{
851 for (const QFileInfo &srcInfo : src.entryInfoList(FILTERS)) {
852 QString srcName = srcInfo.fileName();
853 QString dstPath = dst.filePath(srcName);
854
855 if (srcInfo.isDir()) {
856 // Move the directory over recursively.
857 if (dst.mkpath(dstPath)) {
858 moveFromInternalSnapshotDirectory(QDir(srcInfo.filePath()), QDir(dstPath));
859 } else {
860 qWarning().nospace() << "Failed to create directory '" << dstPath << "' in '" << dst.path() << "'";
861 }
862
863 // Removal will fail if the directory is non-empty, so we
864 // can just attempt it unconditionally.
865 if (!src.rmdir(srcName)) {
866 qWarning().nospace() << "Failed to remove directory '" << srcName << "' in '" << src.path() << "'";
867 }
868
869 } else {
870 QFile srcFile(srcInfo.filePath());
871 // Rename refuses to replace files in the destination, so
872 // try to remove that first. The only reason it should
873 // already exist is if a previous attempt to move the file
874 // partially copied it and then got interrupted.
875 QFile::remove(dstPath);
876 if (!srcFile.rename(dstPath)) {
877 qWarning().nospace() << "Error " << srcFile.error() << " moving '" << srcFile.fileName() << "' to '"
878 << dstPath << "': " << srcFile.errorString();
879 }
880 }
881 }
882}
883#endif
float value(const T *src, size_t ch)
QAction * makeQAction(const QString &name, QObject *parent=0)
static KisActionRegistry * instance()
A container for a set of QAction objects.
Q_INVOKABLE QAction * addAction(const QString &name, QAction *action)
Main window for Krita.
KisViewManager * viewManager
static KisPart * instance()
Definition KisPart.cpp:130
void removeExtraWidget(QWidget *widget)
void addExtraWidget(QWidget *widget)
virtual KisKActionCollection * actionCollection() const
double captureInterval() const
QString snapshotDirectory() const
void setFormat(RecorderFormat value)
void setRealTimeCaptureMode(bool value)
bool recordIsolateLayerMode() const
RecorderFormat format() const
int resolution() const
void setRecordAutomatically(bool value)
void setResolution(int value)
void setCaptureInterval(double value)
void setSnapshotDirectory(const QString &value)
void setRecordIsolateLayerMode(bool value)
void setCompression(int value)
bool recordAutomatically() const
void setQuality(int value)
bool realTimeCaptureMode() const
int compression() const
void setThreads(int value)
int quality() const
void showWarning(const QString &hint)
Private(const RecorderExportSettings &es, RecorderDockerDock *q_ptr)
QScopedPointer< Ui::RecorderDocker > ui
void updateRecordStatus(bool isRecording)
RecorderDockerDock *const q
void updateComboResolution(quint32 width, quint32 height)
void slotScrollerStateChanged(QScroller::State state)
RecorderExportSettings *const exportSettings
void onCaptureIntervalChanged(double interval)
void setCanvas(KoCanvasBase *canvas) override
void onThreadsChanged(int threads)
void onVideoFPSChanged(double interval)
void onQualityChanged(int value)
void onRecordIsolateLayerModeToggled(bool checked)
void onRealTimeCaptureModeToggled(bool checked)
void onResolutionChanged(int resolution)
bool onRecordButtonToggled(bool checked)
void onAutoRecordToggled(bool checked)
void onActiveRecording(bool valueWasIncreased)
void onMainWindowIsBeingCreated(KisMainWindow *window)
void onFormatChanged(int format)
void execFor(const QString &snapshotsDirectory)
void start(bool toggleEnabled=true)
void setCanvas(QPointer< KisCanvas2 > canvas)
bool stop(bool toggleEnabled=true)
void setEnabled(bool enabled)
void setup(const RecorderWriterSettings &settings)
ThreadCounter recorderThreads
unsigned int getUsed() const
unsigned int get() const
bool set(int value)
QIcon loadIcon(const QString &name)
KRITAWIDGETUTILS_EXPORT void updateCursor(QWidget *source, QScroller::State state)
KRITAWIDGETUTILS_EXPORT QScroller * createPreconfiguredScroller(QAbstractScrollArea *target)
const unsigned int IdealRecordThreadCount
const unsigned int MaxThreadCount
const unsigned int MaxRecordThreadCount
RecorderFormat