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
119 {
120 RecorderConfig config(true);
122#ifdef Q_OS_ANDROID
123 fixInternalSnapshotDirectory();
124#endif
126 format = config.format();
127 quality = config.quality();
128 compression = config.compression();
129 resolution = config.resolution();
133 q->exportSettings->lockFps = true;
135 }
138
140 }
141
143 {
144 RecorderExportConfig config(true);
145 q->exportSettings->fps = config.fps();
146 }
147
149 int index = 0;
150 QString title;
151 QString hint;
152 int minValue = 0;
153 int maxValue = 0;
154 QString suffix;
155 int factor = 0;
156 switch (format) {
158 index = 0;
159 title = i18nc("Title for label. JPEG Quality level", "Quality:");
160 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.");
161 minValue = 1;
162 maxValue = 100;
163 suffix = "%";
164 factor = quality;
165 break;
167 index = 1;
168 title = i18nc("Title for label. PNG Compression level", "Compression:");
169 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.");
170 minValue = 0;
171 maxValue = 5;
172 suffix = "";
173 factor = compression;
174 break;
175 }
176
177 ui->comboFormat->setCurrentIndex(index);
178 ui->labelQuality->setText(title);
179 ui->spinQuality->setToolTip(hint);
180 QSignalBlocker blocker(ui->spinQuality);
181 ui->spinQuality->setMinimum(minValue);
182 ui->spinQuality->setMaximum(maxValue);
183 ui->spinQuality->setValue(factor);
184 ui->spinQuality->setSuffix(suffix);
185 }
186
188 QString title;
189 double minValue = 0;
190 double maxValue = 0;
191 double value = 0;
192 int decimals = 0;
193 QString suffix;
194 QSignalBlocker blocker(ui->spinRate);
195
197 title = i18nc("Title for label. Video frames per second", "Video FPS:");
198 minValue = 1;
199 maxValue = 60;
200 decimals = 0;
202 suffix = "";
203 disconnect(ui->spinRate, SIGNAL(valueChanged(double)), q, SLOT(onCaptureIntervalChanged(double)));
204 connect(ui->spinRate, SIGNAL(valueChanged(double)), q, SLOT(onVideoFPSChanged(double)));
205 } else {
206 title = i18nc("Title for label. Capture rate", "Capture interval:");
207 minValue = 0.10;
208 maxValue = 100.0;
209 decimals = 1;
211 suffix = " sec.";
212 disconnect(ui->spinRate, SIGNAL(valueChanged(double)), q, SLOT(onVideoFPSChanged(double)));
213 connect(ui->spinRate, SIGNAL(valueChanged(double)), q, SLOT(onCaptureIntervalChanged(double)));
214 }
215
216 ui->labelRate->setText(title);
217 ui->spinRate->setDecimals(decimals);
218 ui->spinRate->setMinimum(minValue);
219 ui->spinRate->setMaximum(maxValue);
220 ui->spinRate->setSuffix(suffix);
221 ui->spinRate->setValue(value);
222 }
223
225 {
226 outputDirectory = snapshotDirectory % QDir::separator() % prefix % QDir::separator();
227 writer.setup({
229 format,
230 quality,
236 }
237
238 QString getPrefix()
239 {
240 return !canvas ? ""
241 : canvas->imageView()->document()->documentInfo()->aboutInfo("creation-date").remove(QRegularExpression("[^0-9]"));
242 }
243
244 void updateComboResolution(quint32 width, quint32 height)
245 {
246 const QStringList titles = {
247 i18nc("Use original resolution for the frames when recording the canvas", "Original"),
248 i18nc("Use the resolution two times smaller than the original resolution for the frames when recording the canvas", "Half"),
249 i18nc("Use the resolution four times smaller than the original resolution for the frames when recording the canvas", "Quarter")
250 };
251
252 QStringList items;
253 for (int index = 0, len = titles.length(); index < len; ++index) {
254 int divider = 1 << index;
255 items += QString("%1 (%2x%3)").arg(titles[index])
256 .arg((width / divider) & ~1)
257 .arg((height / divider) & ~1);
258 }
259 QSignalBlocker blocker(ui->comboResolution);
260 const int currentIndex = ui->comboResolution->currentIndex();
261 ui->comboResolution->clear();
262 ui->comboResolution->addItems(items);
263 ui->comboResolution->setCurrentIndex(currentIndex);
264 }
265
266 void updateRecordStatus(bool isRecording)
267 {
268 recordToggleAction->setChecked(isRecording);
269 recordToggleAction->setEnabled(true);
270
271 QSignalBlocker blocker(ui->buttonRecordToggle);
272 ui->buttonRecordToggle->setChecked(isRecording);
273 ui->buttonRecordToggle->setIcon(KisIconUtils::loadIcon(isRecording ? "media-playback-stop" : "media-record"));
274 ui->buttonRecordToggle->setText(isRecording ? i18nc("Stop recording the canvas", "Stop")
275 : i18nc("Start recording the canvas", "Record"));
276 ui->buttonRecordToggle->setEnabled(true);
277
278 ui->widgetSettings->setEnabled(!isRecording);
279
280 statusBarLabel->setVisible(isRecording);
281
282 if (!canvas)
283 return;
284
285 KisStatusBar *statusBar = canvas->viewManager()->statusBar();
286 if (isRecording) {
288 statusBar->addExtraWidget(statusBarLabel);
290 } else {
293 }
294 }
295
297 {
298 auto threads = writer.recorderThreads.get();
299 auto threadsInUse = writer.recorderThreads.getUsed();
300 QString label("<font style='letter-spacing:-4px'>");
301 QString activeColor;
302 QString inactiveColor;
303 for (unsigned int threadNr = 1; threadNr <= ThreadSystemValue::MaxThreadCount ; threadNr++)
304 {
305 if (threadNr > threads) {
306 activeColor = inactiveColorGray;
307 inactiveColor = inactiveColorGray;
308 } else if (threadNr > ThreadSystemValue::MaxRecordThreadCount) {
309 activeColor = activeColorRed;
310 inactiveColor = inactiveColorRed;
311 } else if (threadNr > ThreadSystemValue::IdealRecordThreadCount) {
312 activeColor = activeColorOrange;
313 inactiveColor = inactiveColorOrange;
314 } else {
315 activeColor = activeColorGreen;
316 inactiveColor = inactiveColorGreen;
317 }
318 label.append(QString("<font%1>▍</font>")
319 .arg(threadNr <= threadsInUse ? activeColor : inactiveColor));
320 }
321 // don't remove empty <font></font> tag else label will jump a few pixels around
322 label.append(QString("</font><font> %1 </font><font%2>●</font>")
323 .arg(i18nc("Recording symbol", "REC"))
324 .arg(paused ? "" : activeColorRed));
325 statusBarLabel->setText(label);
326 statusBarLabel->setToolTip(paused ? i18n("Recorder is paused") : QString(i18n("Active recording with %1 of %2 available threads")).arg(threadsInUse).arg(threads));
327 }
328
329 void showWarning(const QString &hint) {
330 if (statusBarWarningLabel->isHidden()) {
331 statusBarWarningLabel->setToolTip(hint);
332 statusBarWarningLabel->show();
333 warningTimer.start();
334 }
335 }
336
338 {
339 QString toolTipText;
340 auto threads = writer.recorderThreads.get();
342 // Number of threads exceeds ideal thread count
343 // -> switch color of threads slider and spin wheel to red
344 QPalette pal;
345 pal.setColor(QPalette::Text, textColorRed);
346 pal.setColor(QPalette::Button, buttonColorRed);
347 ui->spinThreads->setPalette(pal);
348 ui->sliderThreads->setPalette(pal);
349 toolTipText = QString(
350 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.")
352 } else if (threads > ThreadSystemValue::IdealRecordThreadCount) {
353 // Number of threads exceeds ideal recorder thread count
354 // -> switch color of threads slider and spin wheel to orange
355 QPalette pal;
356 pal.setColor(QPalette::Text, textColorOrange);
357 pal.setColor(QPalette::Button, buttonColorOrange);
358 ui->spinThreads->setPalette(pal);
359 ui->sliderThreads->setPalette(pal);
360 toolTipText = QString(
361 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.")
363 } else {
364 ui->spinThreads->setPalette(threadsSpinPalette);
365 ui->sliderThreads->setPalette(threadsSliderPalette);
366 toolTipText = i18n("Set the number of threads to be used for recording.");
367 }
368 ui->spinThreads->setToolTip(toolTipText);
369 ui->sliderThreads->setToolTip(toolTipText);
370 }
371
372#ifdef Q_OS_ANDROID
373 void fixInternalSnapshotDirectory()
374 {
375 // Older versions of Krita used an internal directory as the snapshots
376 // directory by default, which is a bogus place to save stuff to because
377 // the user can't access it. That means the files stored there are stuck
378 // inaccessible and once the user picks a "real" directory, they can no
379 // longer even delete the files. So here we're rectifying the situation.
380 if (snapshotDirectory == RecorderConfig::defaultInternalSnapshotDirectory()) {
381 // Internal path got persisted to settings. Clear that out, replace
382 // it with the default of nothing.
383 snapshotDirectory = QString();
384 } else {
385 q->moveFilesFromInternalSnapshotDirectory();
386 }
387 }
388#endif
389};
390
392 : QDockWidget(i18nc("Title of the docker", "Recorder"))
393 , exportSettings(new RecorderExportSettings())
394 , d(new Private(*exportSettings, this))
395{
396 QWidget* page = new QWidget(this);
397 d->ui->setupUi(page);
398
399 d->ui->buttonManageRecordings->setIcon(KisIconUtils::loadIcon("configure-thicker"));
400 d->ui->buttonBrowse->setIcon(KisIconUtils::loadIcon("folder"));
401 d->ui->buttonRecordToggle->setIcon(KisIconUtils::loadIcon("media-record"));
402 d->ui->buttonExport->setIcon(KisIconUtils::loadIcon("document-export-16"));
403 d->ui->sliderThreads->setTickPosition(QSlider::TickPosition::TicksBelow);
404 d->ui->sliderThreads->setMinimum(1);
405 d->ui->sliderThreads->setMaximum(ThreadSystemValue::MaxThreadCount);
406 d->ui->spinThreads->setMinimum(1);
407 d->ui->spinThreads->setMaximum(ThreadSystemValue::MaxThreadCount);
408 d->threadsSpinPalette = d->ui->spinThreads->palette();
409 d->threadsSliderPalette = d->ui->sliderThreads->palette();
410
411 d->loadSettings();
413 d->updateThreadUi();
414
415 d->ui->editDirectory->setText(d->snapshotDirectory);
416 d->ui->spinQuality->setValue(d->quality);
417 d->ui->spinThreads->setValue(d->writer.recorderThreads.get());
418 d->ui->comboResolution->setCurrentIndex(d->resolution);
419 d->ui->checkBoxRealTimeCaptureMode->setChecked(d->realTimeCaptureMode);
420 d->ui->checkBoxRecordIsolateMode->setChecked(d->recordIsolateLayerMode);
421 d->ui->checkBoxAutoRecord->setChecked(d->recordAutomatically);
422
424 d->recordToggleAction = actionRegistry->makeQAction(keyActionRecordToggle, this);
425 d->exportAction = actionRegistry->makeQAction(keyActionExport, this);
426
427 connect(d->recordToggleAction, SIGNAL(toggled(bool)), d->ui->buttonRecordToggle, SLOT(setChecked(bool)));
428 connect(d->exportAction, SIGNAL(triggered()), d->ui->buttonExport, SIGNAL(clicked()));
429 connect(d->ui->buttonRecordToggle, SIGNAL(toggled(bool)), d->ui->buttonExport, SLOT(setDisabled(bool)));
431 d->ui->buttonExport->setDisabled(true);
432
433 // Need to register toolbar actions before attaching canvas else it wont appear after restart.
434 // Is there any better way to do this?
435 connect(KisPart::instance(), SIGNAL(sigMainWindowIsBeingCreated(KisMainWindow *)),
437
438 connect(d->ui->buttonManageRecordings, SIGNAL(clicked()), this, SLOT(onManageRecordingsButtonClicked()));
439 connect(d->ui->buttonBrowse, SIGNAL(clicked()), this, SLOT(slotSelectSnapshotDirectory()));
440 connect(d->ui->comboFormat, SIGNAL(currentIndexChanged(int)), this, SLOT(onFormatChanged(int)));
441 connect(d->ui->spinQuality, SIGNAL(valueChanged(int)), this, SLOT(onQualityChanged(int)));
442 connect(d->ui->spinThreads, SIGNAL(valueChanged(int)), this, SLOT(onThreadsChanged(int)));
443 connect(d->ui->comboResolution, SIGNAL(currentIndexChanged(int)), this, SLOT(onResolutionChanged(int)));
444 connect(d->ui->checkBoxRealTimeCaptureMode, SIGNAL(toggled(bool)), this, SLOT(onRealTimeCaptureModeToggled(bool)));
445 connect(d->ui->checkBoxRecordIsolateMode, SIGNAL(toggled(bool)), this, SLOT(onRecordIsolateLayerModeToggled(bool)));
446 connect(d->ui->checkBoxAutoRecord, SIGNAL(toggled(bool)), this, SLOT(onAutoRecordToggled(bool)));
447 connect(d->ui->buttonRecordToggle, SIGNAL(toggled(bool)), this, SLOT(onRecordButtonToggled(bool)));
448 connect(d->ui->buttonExport, SIGNAL(clicked()), this, SLOT(onExportButtonClicked()));
449
450 connect(&d->writer.recorderThreads, SIGNAL(notifyInUseChange(bool)), this, SLOT(onActiveRecording(bool)));
451 connect(&d->writer.recorderThreads, SIGNAL(notifyInUseChange(bool)), this, SLOT(onUpdateRecIndicator()));
452 connect(&d->writer, SIGNAL(started()), this, SLOT(onWriterStarted()));
453 connect(&d->writer, SIGNAL(stopped()), this, SLOT(onWriterStopped()));
454 connect(&d->writer, SIGNAL(frameWriteFailed()), this, SLOT(onWriterFrameWriteFailed()));
455 connect(&d->writer, SIGNAL(recorderStopWarning()), this, SLOT(onRecorderStopWarning()));
456 connect(&d->writer, SIGNAL(lowPerformanceWarning()), this, SLOT(onLowPerformanceWarning()));
457
458
459 QScroller *scroller = KisKineticScroller::createPreconfiguredScroller(d->ui->scrollArea);
460 if (scroller) {
461 connect(scroller, SIGNAL(stateChanged(QScroller::State)),
462 this, SLOT(slotScrollerStateChanged(QScroller::State)));
463 }
464
465 // The system is not efficient enough for the RealTime Recording Feature
467 {
468 d->ui->checkBoxRealTimeCaptureMode->setCheckState(Qt::Unchecked);
469 d->ui->checkBoxRealTimeCaptureMode->setDisabled(true);
470 d->ui->checkBoxRealTimeCaptureMode->setToolTip(
471 i18n("Your system is not efficient enough for this feature"));
472 }
473
474 setWidget(page);
475}
476
478{
479 delete d;
480 delete exportSettings;
481}
482
484{
485 setEnabled(canvas != nullptr);
486
487 if (d->canvas == canvas)
488 return;
489
490 d->canvas = dynamic_cast<KisCanvas2*>(canvas);
492
493 if (!d->canvas)
494 return;
495
496 KisDocument *document = d->canvas->imageView()->document();
497 d->updateComboResolution(document->image()->width(), document->image()->height());
498
499 d->prefix = d->getPrefix();
500 bool wasToggled = false;
501 if (d->recordAutomatically && !d->snapshotDirectory.isEmpty()
502 && !d->enabledIds.contains(document->linkedResourcesStorageId())) {
503 wasToggled = onRecordButtonToggled(true);
504 }
505 if (!wasToggled) { // onRecordButtonToggled(true) may call these, don't call them twice.
507 d->updateUiFormat();
508 }
510
511 bool enabled = d->enabledIds.value(document->linkedResourcesStorageId(), false);
512 d->writer.setEnabled(enabled);
513 d->updateRecordStatus(enabled);
514}
515
517{
518 d->updateRecordStatus(false);
519 d->recordToggleAction->setChecked(false);
520 setEnabled(false);
521 d->writer.stop();
522 d->writer.setCanvas(nullptr);
523 d->canvas = nullptr;
524 d->enabledIds.clear();
525}
526
528{
529 KisKActionCollection *actionCollection = window->viewManager()->actionCollection();
530 actionCollection->addAction(keyActionRecordToggle, d->recordToggleAction);
531 actionCollection->addAction(keyActionExport, d->exportAction);
532}
533
535{
536 QSignalBlocker blocker(d->ui->buttonRecordToggle);
537
538 // Ask the user to pick a directory if we don't have one. This should only
539 // happen on Android, other operating systems have a non-empty default that
540 // the user is not able to clear out via the user interface.
541 if (checked && d->snapshotDirectory.isEmpty()) {
543 if (d->snapshotDirectory.isEmpty()) {
544 d->ui->buttonRecordToggle->setChecked(false);
545 d->recordToggleAction->setChecked(false);
546 return false;
547 }
548 }
549
550 d->recordToggleAction->setChecked(checked);
551
552 if (!d->canvas)
553 return false;
554
555 const QString &id = d->canvas->imageView()->document()->linkedResourcesStorageId();
556
557 bool wasEmpty = !d->enabledIds.values().contains(true);
558
559 d->enabledIds[id] = checked;
560
561 bool isEmpty = !d->enabledIds.values().contains(true);
562
563 d->writer.setEnabled(checked);
564
565 if (isEmpty == wasEmpty) {
566 d->updateRecordStatus(checked);
567 return false;
568 }
569
570
571 d->ui->buttonRecordToggle->setEnabled(false);
572
573 if (checked) {
575 d->updateUiFormat();
576 d->writer.start();
577
578 // Calculate Rec symbol activity timeout depending on the capture interval
579 // The pausedTimer interval is set to a slightly greater value than the capture interval
580 // to avoid flickering for ongoing painting. This is also the reason for the min and max
581 // values 305 and 2005 (instead of 300 and 2000, respectively).
582 if (d->realTimeCaptureMode) {
583 d->pausedTimer.setInterval(qBound(305, static_cast<int>(1000.0/static_cast<double>(exportSettings->fps)) + 5,2005));
584 } else {
585 d->pausedTimer.setInterval(qBound(305, static_cast<int>(qMax(d->captureInterval, .1) * 1000.0) + 5, 2005));
586 }
587 } else {
588 d->writer.stop();
589 d->warningTimer.stop();
590 d->pausedTimer.stop();
591 d->statusBarWarningLabel->hide();
592 d->paused = true;
593 }
594
595 return true;
596}
597
599{
600 if (!d->canvas)
601 return;
602
603 KisDocument *document = d->canvas->imageView()->document();
604
605 exportSettings->videoFileName = QFileInfo(document->caption().trimmed()).completeBaseName();
609
610 RecorderExport exportDialog(exportSettings, this);
611 exportDialog.setup();
612 exportDialog.exec();
613
615 d->ui->spinRate->setValue(exportSettings->fps);
616}
617
619{
620 RecorderSnapshotsManager snapshotsManager(this);
621 snapshotsManager.execFor(d->snapshotDirectory);
622}
623
625{
626 KoFileDialog dialog(this, KoFileDialog::OpenDirectory, "SelectRecordingsDirectory");
627 dialog.setCaption(i18n("Select a Directory for Recordings"));
628 dialog.setDefaultDir(d->ui->editDirectory->text());
629 QString directory = dialog.filename();
630 if (!directory.isEmpty()) {
631 d->ui->editDirectory->setText(directory);
632 RecorderConfig(false).setSnapshotDirectory(directory);
633 d->loadSettings();
634 }
635}
636
643
645{
646 d->recordAutomatically = checked;
648 d->loadSettings();
649}
650
662
664{
665 d->captureInterval = interval;
666 RecorderConfig(false).setCaptureInterval(interval);
667 d->loadSettings();
668}
675
677{
678 switch (d->format) {
680 d->quality = value;
682 d->loadSettings();
683 break;
687 d->loadSettings();
688 break;
689 }
690}
691
693{
694 d->format = static_cast<RecorderFormat>(format);
695 d->updateUiFormat();
696
698 d->loadSettings();
699}
700
702{
703 d->resolution = resolution;
704 RecorderConfig(false).setResolution(resolution);
705 d->loadSettings();
706}
707
709{
710 d->writer.recorderThreads.set(threads);
711 RecorderConfig(false).setThreads(threads);
712 d->loadSettings();
713 d->updateThreadUi();
714}
715
720
725
730
731void RecorderDockerDock::onActiveRecording(bool valueWasIncreased)
732{
733 if (!valueWasIncreased)
734 return;
735
736 d->paused = false;
737 d->pausedTimer.start();
738}
739
745
747{
748 QMessageBox::warning(this, i18nc("@title:window", "Recorder"),
749 i18n("The recorder has been stopped due to failure while writing a frame. Please check free disk space and start the recorder again."));
750}
751
753{
754 QMessageBox::warning(this, i18nc("@title:window", "Recorder"),
755 i18n("Krita was unable to stop the recorder probably. Please try to restart Krita."));
756}
758{
759 if (d->realTimeCaptureMode) {
760 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."));
761 } else {
762 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."));
763 }
764}
765
770
772{
774}
775
776#ifdef Q_OS_ANDROID
777void RecorderDockerDock::moveFilesFromInternalSnapshotDirectory()
778{
779 if (!d->internalMoveInProgress) {
780 const QString &internalPath = RecorderConfig::defaultInternalSnapshotDirectory();
781 if (!d->snapshotDirectory.isEmpty() && QFileInfo::exists(internalPath)) {
782 // The user has picked a directory to record to, but the nonsense
783 // internal directory is present and may have stuff inside that
784 // would become effectively inaccessible. To fix that, we move the
785 // files over to the selected directory. Of course moving files on
786 // Android is gobsmackingly slow, so we'll have to do it in the
787 // background to not lock the UI for ages. The moving should be
788 // re-entrant, so getting interrupted and continuing later is fine.
789 qWarning().nospace() << "Moving recordings stuck in internal directory '" << internalPath
790 << "' to selected directory '" << d->snapshotDirectory << "'";
791 RecorderDockerInternalSnapshotsMover *mover =
792 new RecorderDockerInternalSnapshotsMover(internalPath, d->snapshotDirectory);
793 connect(mover,
794 &RecorderDockerInternalSnapshotsMover::sigMoveFinished,
795 this,
796 &RecorderDockerDock::slotInternalSnapshotMoveFinished,
797 Qt::QueuedConnection);
798 d->internalMoveInProgress = true;
799 QThreadPool::globalInstance()->start(mover);
800 }
801 }
802}
803
804void RecorderDockerDock::slotInternalSnapshotMoveFinished(const QString &srcRoot)
805{
806 d->internalMoveInProgress = false;
807 if (srcRoot != d->snapshotDirectory) {
808 // Directory changed meanwhile, trigger another move.
809 moveFilesFromInternalSnapshotDirectory();
810 }
811}
812
813RecorderDockerInternalSnapshotsMover::RecorderDockerInternalSnapshotsMover(const QString &srcRoot,
814 const QString &dstRoot)
815 : m_srcRoot(srcRoot)
816 , m_dstRoot(dstRoot)
817{
818}
819
820void RecorderDockerInternalSnapshotsMover::run()
821{
822 moveFromInternalSnapshotDirectory(QDir(m_srcRoot), QDir(m_dstRoot));
823 if (!QDir().rmdir(m_srcRoot)) {
824 qWarning().nospace() << "Failed to remove root directory '" << m_srcRoot << "'";
825 }
826 Q_EMIT sigMoveFinished(m_srcRoot);
827}
828
829void RecorderDockerInternalSnapshotsMover::moveFromInternalSnapshotDirectory(const QDir &src, const QDir &dst)
830{
831 for (const QFileInfo &srcInfo : src.entryInfoList(FILTERS)) {
832 QString srcName = srcInfo.fileName();
833 QString dstPath = dst.filePath(srcName);
834
835 if (srcInfo.isDir()) {
836 // Move the directory over recursively.
837 if (dst.mkpath(dstPath)) {
838 moveFromInternalSnapshotDirectory(QDir(srcInfo.filePath()), QDir(dstPath));
839 } else {
840 qWarning().nospace() << "Failed to create directory '" << dstPath << "' in '" << dst.path() << "'";
841 }
842
843 // Removal will fail if the directory is non-empty, so we
844 // can just attempt it unconditionally.
845 if (!src.rmdir(srcName)) {
846 qWarning().nospace() << "Failed to remove directory '" << srcName << "' in '" << src.path() << "'";
847 }
848
849 } else {
850 QFile srcFile(srcInfo.filePath());
851 // Rename refuses to replace files in the destination, so
852 // try to remove that first. The only reason it should
853 // already exist is if a previous attempt to move the file
854 // partially copied it and then got interrupted.
855 QFile::remove(dstPath);
856 if (!srcFile.rename(dstPath)) {
857 qWarning().nospace() << "Error " << srcFile.error() << " moving '" << srcFile.fileName() << "' to '"
858 << dstPath << "': " << srcFile.errorString();
859 }
860 }
861 }
862}
863#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:131
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