From 30244afacf25debc8c58117c72e4056965cd4145 Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Thu, 26 Dec 2024 06:58:43 -0800 Subject: [PATCH] add View > Files (#1613) * add View > Files * make Files media type resolution multi-threaded * improve Files thumbnail performance * improve Files media type performance * implement Files > Update Thumbnails * add Files panel to default layouts * update style of Files media type buttons * enable sorting in details mode * details view improvements * minor improvements to view modes * add `files/viewMode` to settings * display size in tiles view * more media type filename extensions * add sub-folder navigation * fix cosmetic regression in playlist icons view * add Locations combo * refactor to FilesDock::changeDirectory() * add Show In Files * add Files > Show In File Manager * add Open With * fix compile error on macOS & Linux * fix Enter to open on macOS * remove a verbose log line * fix layout on Qt 6.4/Linux * fix View > Files shortcut * mov is too generic --- src/CMakeLists.txt | 2 + src/defaultlayouts.h | 12 +- src/docks/filesdock.cpp | 1266 ++++++++++++++++++++++++ src/docks/filesdock.h | 102 ++ src/docks/filesdock.ui | 208 ++++ src/jobs/encodejob.cpp | 7 +- src/jobs/meltjob.cpp | 10 + src/jobs/meltjob.h | 1 + src/jobs/videoqualityjob.cpp | 7 +- src/mainwindow.cpp | 36 +- src/mainwindow.h | 4 + src/mainwindow.ui | 12 +- src/models/playlistmodel.h | 3 +- src/settings.cpp | 84 ++ src/settings.h | 12 + src/widgets/avformatproducerwidget.cpp | 7 + src/widgets/avformatproducerwidget.h | 2 + src/widgets/avformatproducerwidget.ui | 7 +- src/widgets/imageproducerwidget.cpp | 9 +- src/widgets/imageproducerwidget.h | 6 +- src/widgets/imageproducerwidget.ui | 7 +- src/widgets/playlisticonview.cpp | 41 +- src/widgets/playlisticonview.h | 4 +- 23 files changed, 1820 insertions(+), 29 deletions(-) create mode 100644 src/docks/filesdock.cpp create mode 100644 src/docks/filesdock.h create mode 100644 src/docks/filesdock.ui diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 551f0d1e30..5550158588 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -42,6 +42,8 @@ add_executable(shotcut WIN32 MACOSX_BUNDLE dialogs/unlinkedfilesdialog.ui docks/encodedock.cpp docks/encodedock.h docks/encodedock.ui + docks/filesdock.cpp docks/filesdock.h + docks/filesdock.ui docks/filtersdock.cpp docks/filtersdock.h docks/jobsdock.cpp docks/jobsdock.h docks/jobsdock.ui diff --git a/src/defaultlayouts.h b/src/defaultlayouts.h index 9ccae74376..1d0a69ea6e 100644 --- a/src/defaultlayouts.h +++ b/src/defaultlayouts.h @@ -4,21 +4,21 @@ #include static const auto kLayoutLoggingDefault = - QByteArray::fromBase64("AAAA/wAAAAD9AAAAAwAAAAAAAAHgAAADoPwCAAAAAvsAAAAYAFAAbABhAHkAbABpAHMAdABEAG8AYwBrAQAAAFEAAAOgAAAAewD////7AAAAEgBOAG8AdABlAHMARABvAGMAawAAAAAA/////wAAAFgA////AAAAAQAAAeAAAAOg/AIAAAAE/AAAAFEAAAOgAAAAWAD////6AAAAAAIAAAAG+wAAABwAcAByAG8AcABlAHIAdABpAGUAcwBEAG8AYwBrAQAAAAD/////AAAAWAD////7AAAAFgBGAGkAbAB0AGUAcgBzAEQAbwBjAGsAAAAAAP////8AAAEsAP////sAAAAaAFMAdQBiAHQAaQB0AGwAZQBzAEQAbwBjAGsAAAAAAP////8AAABWAP////sAAAAWAGgAaQBzAHQAbwByAHkARABvAGMAawAAAAAA/////wAAAFgA////+wAAABQARQBuAGMAbwBkAGUARABvAGMAawAAAAAA/////wAAAJgA////+wAAABAASgBvAGIAcwBEAG8AYwBrAAAAAAD/////AAAAbgD////8AAACUAAAAegAAAAAAP////oAAAACAQAAAAv7AAAAHgBWAGkAZABlAG8AVgBlAGMAdABvAHIARABvAGMAawAAAAAA/////wAAAEQA////+wAAABoAVgBpAGQAZQBvAFoAbwBvAG0ARABvAGMAawAAAAAA/////wAAAEQA////+wAAABoAVgBpAGQAZQBvAFoAbwBvAG0ARABvAGMAawAAAAAA/////wAAAAAAAAAA+wAAABoAVgBpAGQAZQBvAFoAbwBvAG0ARABvAGMAawAAAAAA/////wAAAAAAAAAA+wAAAB4AUgBnAGIAVwBhAHYAZQBmAG8AcgBtAEQAbwBjAGsAAAAAAP////8AAABEAP////sAAAAaAFIAZwBiAFAAYQByAGEAZABlAEQAbwBjAGsAAAAAAP////8AAABEAP////sAAAAkAFYAaQBkAGUAbwBIAGkAcwB0AG8AZwByAGEAbQBEAG8AYwBrAAAAAAD/////AAAARAD////7AAAAIgBBAHUAZABpAG8AVwBhAHYAZQBmAG8AcgBtAEQAbwBjAGsAAAAAAP////8AAABEAP////sAAAAiAEEAdQBkAGkAbwBTAHAAZQBjAHQAcgB1AG0ARABvAGMAawAAAAAA/////wAAAEQA////+wAAACwAQQB1AGQAaQBvAEwAbwB1AGQAbgBlAHMAcwBNAGUAdABlAHIARABvAGMAawAAAAV5AAAB0gAAAEQA////+wAAACIAVgBpAGQAZQBvAFcAYQB2AGUAZgBvAHIAbQBEAG8AYwBrAAAAAAD/////AAAARAD////7AAAAIgBBAHUAZABpAG8AUwB1AHIAcgBvAHUAbgBkAEQAbwBjAGsAAAAAAP////8AAABWAP////sAAAAeAEEAdQBkAGkAbwBWAGUAYwB0AG8AcgBEAG8AYwBrAAAAAAD/////AAAAVgD///8AAAADAAADrAAAAS78AQAAAAP8AAAB6gAAA6wAAACWAP////oAAAAAAQAAAAP7AAAAFABSAGUAYwBlAG4AdABEAG8AYwBrAQAAAAD/////AAAAlgD////7AAAAGABUAGkAbQBlAGwAaQBuAGUARABvAGMAawAAAAAA/////wAAAMgA////+wAAABoASwBlAHkAZgByAGEAbQBlAHMARABvAGMAawAAAAAA/////wAAAMgA////+wAAACQAQQB1AGQAaQBvAFAAZQBhAGsATQBlAHQAZQByAEQAbwBjAGsAAAAFVwAAAEQAAABEAP////sAAAAWAE0AYQByAGsAZQByAHMARABvAGMAawAAAAMwAAAAgwAAAEQA////AAADrAAAAmgAAAABAAAAAgAAAAEAAAAC/AAAAAEAAAACAAAAAQAAABYAbQBhAGkAbgBUAG8AbwBsAEIAYQByAQAAAAD/////AAAAAAAAAAA="); + QByteArray::fromBase64("AAAA/wAAAAD9AAAAAwAAAAAAAAHgAAADuPwCAAAAAvsAAAAYAFAAbABhAHkAbABpAHMAdABEAG8AYwBrAQAAADsAAAO4AAAAnQD////7AAAAEgBOAG8AdABlAHMARABvAGMAawAAAAAA/////wAAAFkA////AAAAAQAAAeAAAAO4/AIAAAAE/AAAADsAAAO4AAAAWQD////6AAAAAAIAAAAG+wAAABwAcAByAG8AcABlAHIAdABpAGUAcwBEAG8AYwBrAQAAAAD/////AAAAWQD////7AAAAFgBGAGkAbAB0AGUAcgBzAEQAbwBjAGsAAAAAAP////8AAAEsAP////sAAAAaAFMAdQBiAHQAaQB0AGwAZQBzAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAWAGgAaQBzAHQAbwByAHkARABvAGMAawAAAAAA/////wAAAFkA////+wAAABQARQBuAGMAbwBkAGUARABvAGMAawAAAAAA/////wAAAJcA////+wAAABAASgBvAGIAcwBEAG8AYwBrAAAAAAD/////AAAAbwD////8AAACUAAAAegAAAAAAP////oAAAACAQAAAAv7AAAAHgBWAGkAZABlAG8AVgBlAGMAdABvAHIARABvAGMAawAAAAAA/////wAAAEQA////+wAAABoAVgBpAGQAZQBvAFoAbwBvAG0ARABvAGMAawAAAAAA/////wAAAEQA////+wAAABoAVgBpAGQAZQBvAFoAbwBvAG0ARABvAGMAawAAAAAA/////wAAAAAAAAAA+wAAABoAVgBpAGQAZQBvAFoAbwBvAG0ARABvAGMAawAAAAAA/////wAAAAAAAAAA+wAAAB4AUgBnAGIAVwBhAHYAZQBmAG8AcgBtAEQAbwBjAGsAAAAAAP////8AAABEAP////sAAAAaAFIAZwBiAFAAYQByAGEAZABlAEQAbwBjAGsAAAAAAP////8AAABEAP////sAAAAkAFYAaQBkAGUAbwBIAGkAcwB0AG8AZwByAGEAbQBEAG8AYwBrAAAAAAD/////AAAARAD////7AAAAIgBBAHUAZABpAG8AVwBhAHYAZQBmAG8AcgBtAEQAbwBjAGsAAAAAAP////8AAABEAP////sAAAAiAEEAdQBkAGkAbwBTAHAAZQBjAHQAcgB1AG0ARABvAGMAawAAAAAA/////wAAAEQA////+wAAACwAQQB1AGQAaQBvAEwAbwB1AGQAbgBlAHMAcwBNAGUAdABlAHIARABvAGMAawAAAAV5AAAB0gAAAEQA////+wAAACIAVgBpAGQAZQBvAFcAYQB2AGUAZgBvAHIAbQBEAG8AYwBrAAAAAAD/////AAAARAD////7AAAAIgBBAHUAZABpAG8AUwB1AHIAcgBvAHUAbgBkAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAeAEEAdQBkAGkAbwBWAGUAYwB0AG8AcgBEAG8AYwBrAAAAAAD/////AAAAVwD///8AAAADAAADrAAAAUP8AQAAAAX8AAAB6gAAAtYAAAAAAP////r/////AQAAAAL7AAAAGABUAGkAbQBlAGwAaQBuAGUARABvAGMAawAAAAAA/////wAAAMgA////+wAAABoASwBlAHkAZgByAGEAbQBlAHMARABvAGMAawAAAAAA/////wAAAMgA////+wAAABIARgBpAGwAZQBzAEQAbwBjAGsBAAAB6gAAAqYAAACZAP////sAAAAUAFIAZQBjAGUAbgB0AEQAbwBjAGsBAAAEmgAAAPwAAACWAP////sAAAAkAEEAdQBkAGkAbwBQAGUAYQBrAE0AZQB0AGUAcgBEAG8AYwBrAAAABVcAAABEAAAARAD////7AAAAFgBNAGEAcgBrAGUAcgBzAEQAbwBjAGsAAAADMAAAAIMAAABEAP///wAAA6wAAAJrAAAAAQAAAAIAAAABAAAAAvwAAAABAAAAAgAAAAEAAAAWAG0AYQBpAG4AVABvAG8AbABCAGEAcgEAAAAA/////wAAAAAAAAAA"); static const auto kLayoutEditingDefault = - QByteArray::fromBase64("AAAA/wAAAAD9AAAAAwAAAAAAAAJkAAACifwCAAAAAfwAAABRAAACiQAAAUgBAAAb+gAAAAICAAAABvsAAAAYAFAAbABhAHkAbABpAHMAdABEAG8AYwBrAQAAAAD/////AAAAewD////7AAAAFgBGAGkAbAB0AGUAcgBzAEQAbwBjAGsBAAAAAP////8AAAEsAP////sAAAAcAHAAcgBvAHAAZQByAHQAaQBlAHMARABvAGMAawEAAABGAAADGgAAAFgA////+wAAABQARQBuAGMAbwBkAGUARABvAGMAawAAAAAA/////wAAAJgA////+wAAABoAUwB1AGIAdABpAHQAbABlAHMARABvAGMAawAAAAAA/////wAAAFYA////+wAAABIATgBvAHQAZQBzAEQAbwBjAGsAAAAAAP////8AAABYAP///wAAAAEAAAFGAAACifwCAAAAA/wAAABRAAACiQAAAI4A/////AEAAAAC+wAAACQAQQB1AGQAaQBvAFAAZQBhAGsATQBlAHQAZQByAEQAbwBjAGsBAAAGOgAAAEQAAABEAP////wAAAaIAAAA+AAAAJYA////+gAAAAACAAAAA/sAAAAUAFIAZQBjAGUAbgB0AEQAbwBjAGsBAAAAAP////8AAAByAP////sAAAAWAGgAaQBzAHQAbwByAHkARABvAGMAawEAAAAA/////wAAAFgA////+wAAABAASgBvAGIAcwBEAG8AYwBrAAAAAEYAAABvAAAAbgD////7AAAAIgBBAHUAZABpAG8AUwB1AHIAcgBvAHUAbgBkAEQAbwBjAGsAAAAAAP////8AAABWAP////sAAAAeAEEAdQBkAGkAbwBWAGUAYwB0AG8AcgBEAG8AYwBrAAAAAAD/////AAAAVgD///8AAAADAAAHgAAAAQ38AQAAAAP8AAAAAAAAB4AAAADIAP////oAAAABAgAAAAL7AAAAGgBLAGUAeQBmAHIAYQBtAGUAcwBEAG8AYwBrAQAAAAD/////AAAAMwD////7AAAAGABUAGkAbQBlAGwAaQBuAGUARABvAGMAawEAAAJwAAAByAAAAMgA/////AAABf8AAAF0AAAAAAD////6AAAAAwIAAAAL+wAAAB4AVgBpAGQAZQBvAFYAZQBjAHQAbwByAEQAbwBjAGsAAAAAAP////8AAABWAP////sAAAAiAFYAaQBkAGUAbwBXAGEAdgBlAGYAbwByAG0ARABvAGMAawAAAAAA/////wAAAFYA////+wAAABoAVgBpAGQAZQBvAFoAbwBvAG0ARABvAGMAawAAAAAA/////wAAAFYA////+wAAABoAVgBpAGQAZQBvAFoAbwBvAG0ARABvAGMAawAAAAAA/////wAAAAAAAAAA+wAAABoAVgBpAGQAZQBvAFoAbwBvAG0ARABvAGMAawAAAAAA/////wAAAAAAAAAA+wAAAB4AUgBnAGIAVwBhAHYAZQBmAG8AcgBtAEQAbwBjAGsAAAAAAP////8AAABWAP////sAAAAaAFIAZwBiAFAAYQByAGEAZABlAEQAbwBjAGsAAAAAAP////8AAABWAP////sAAAAkAFYAaQBkAGUAbwBIAGkAcwB0AG8AZwByAGEAbQBEAG8AYwBrAAAAAAD/////AAAAVgD////7AAAAIgBBAHUAZABpAG8AVwBhAHYAZQBmAG8AcgBtAEQAbwBjAGsAAAAAAP////8AAABWAP////sAAAAiAEEAdQBkAGkAbwBTAHAAZQBjAHQAcgB1AG0ARABvAGMAawAAAAAA/////wAAAFYA////+wAAACwAQQB1AGQAaQBvAEwAbwB1AGQAbgBlAHMAcwBNAGUAdABlAHIARABvAGMAawAAAAJmAAAByAAAAFYA////+wAAABYATQBhAHIAawBlAHIAcwBEAG8AYwBrAAAAA/gAAAD+AAAARAD///8AAAPCAAACiQAAAAEAAAACAAAACAAAAAj8AAAAAQAAAAIAAAABAAAAFgBtAGEAaQBuAFQAbwBvAGwAQgBhAHIBAAAAAP////8AAAAAAAAAAA=="); + QByteArray::fromBase64("AAAA/wAAAAD9AAAAAwAAAAAAAAJkAAACofwCAAAAAvsAAAASAEYAaQBsAGUAcwBEAG8AYwBrAAAAADsAAAE+AAAAnQD////8AAAAOwAAAqEAAAFGAQAAGfoAAAAAAgAAAAb7AAAAGABQAGwAYQB5AGwAaQBzAHQARABvAGMAawEAAAAA/////wAAAJ0A////+wAAABYARgBpAGwAdABlAHIAcwBEAG8AYwBrAQAAAAD/////AAABLAD////7AAAAHABwAHIAbwBwAGUAcgB0AGkAZQBzAEQAbwBjAGsBAAAARgAAAxoAAABZAP////sAAAAUAEUAbgBjAG8AZABlAEQAbwBjAGsAAAAAAP////8AAACXAP////sAAAAaAFMAdQBiAHQAaQB0AGwAZQBzAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAASAE4AbwB0AGUAcwBEAG8AYwBrAAAAAAD/////AAAAWQD///8AAAABAAABhwAAAqH8AgAAAAP8AAAAOwAAAqEAAACMAP////wBAAAAAvsAAAAkAEEAdQBkAGkAbwBQAGUAYQBrAE0AZQB0AGUAcgBEAG8AYwBrAQAABfkAAAB3AAAARAD////8AAAGegAAAQYAAACWAP////oAAAAAAgAAAAP7AAAAFABSAGUAYwBlAG4AdABEAG8AYwBrAQAAAAD/////AAAAcgD////7AAAAFgBoAGkAcwB0AG8AcgB5AEQAbwBjAGsBAAAAAP////8AAABZAP////sAAAAQAEoAbwBiAHMARABvAGMAawAAAABGAAAAbwAAAG8A////+wAAACIAQQB1AGQAaQBvAFMAdQByAHIAbwB1AG4AZABEAG8AYwBrAAAAAAD/////AAAAVwD////7AAAAHgBBAHUAZABpAG8AVgBlAGMAdABvAHIARABvAGMAawAAAAAA/////wAAAFcA////AAAAAwAAB4AAAAEN/AEAAAAD/AAAAAAAAAeAAAAAyAD////6AAAAAQIAAAAC+wAAABoASwBlAHkAZgByAGEAbQBlAHMARABvAGMAawEAAAAA/////wAAADQA////+wAAABgAVABpAG0AZQBsAGkAbgBlAEQAbwBjAGsBAAACcAAAAcgAAADIAP////wAAAX/AAABdAAAAAAA////+gAAAAMCAAAAC/sAAAAeAFYAaQBkAGUAbwBWAGUAYwB0AG8AcgBEAG8AYwBrAAAAAAD/////AAAAVwD////7AAAAIgBWAGkAZABlAG8AVwBhAHYAZQBmAG8AcgBtAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAaAFYAaQBkAGUAbwBaAG8AbwBtAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAaAFYAaQBkAGUAbwBaAG8AbwBtAEQAbwBjAGsAAAAAAP////8AAAAAAAAAAPsAAAAaAFYAaQBkAGUAbwBaAG8AbwBtAEQAbwBjAGsAAAAAAP////8AAAAAAAAAAPsAAAAeAFIAZwBiAFcAYQB2AGUAZgBvAHIAbQBEAG8AYwBrAAAAAAD/////AAAAVwD////7AAAAGgBSAGcAYgBQAGEAcgBhAGQAZQBEAG8AYwBrAAAAAAD/////AAAAVwD////7AAAAJABWAGkAZABlAG8ASABpAHMAdABvAGcAcgBhAG0ARABvAGMAawAAAAAA/////wAAAFcA////+wAAACIAQQB1AGQAaQBvAFcAYQB2AGUAZgBvAHIAbQBEAG8AYwBrAAAAAAD/////AAAAVwD////7AAAAIgBBAHUAZABpAG8AUwBwAGUAYwB0AHIAdQBtAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAsAEEAdQBkAGkAbwBMAG8AdQBkAG4AZQBzAHMATQBlAHQAZQByAEQAbwBjAGsAAAACZgAAAcgAAABXAP////sAAAAWAE0AYQByAGsAZQByAHMARABvAGMAawAAAAP4AAAA/gAAAEQA////AAADgQAAAqEAAAABAAAAAgAAAAgAAAAI/AAAAAEAAAACAAAAAQAAABYAbQBhAGkAbgBUAG8AbwBsAEIAYQByAQAAAAD/////AAAAAAAAAAA="); static const auto kLayoutEffectsDefault = - QByteArray::fromBase64("AAAA/wAAAAD9AAAAAwAAAAAAAANmAAACbPwCAAAAAfwAAABRAAACbAAAASwA/////AEAAAAD+wAAABYARgBpAGwAdABlAHIAcwBEAG8AYwBrAQAAAAAAAAHfAAABkAD////7AAAAGgBLAGUAeQBmAHIAYQBtAGUAcwBEAG8AYwBrAQAAAekAAAF9AAAAyAD////7AAAAEgBOAG8AdABlAHMARABvAGMAawAAAAAA/////wAAAEYA////AAAAAQAAAEQAAAJs/AIAAAAD+wAAACQAQQB1AGQAaQBvAFAAZQBhAGsATQBlAHQAZQByAEQAbwBjAGsBAAAAUQAAAmwAAABWAP////sAAAAiAEEAdQBkAGkAbwBTAHUAcgByAG8AdQBuAGQARABvAGMAawAAAAAA/////wAAAFYA////+wAAAB4AQQB1AGQAaQBvAFYAZQBjAHQAbwByAEQAbwBjAGsAAAAAAP////8AAABWAP///wAAAAMAAAeAAAABKvwBAAAABPwAAAAAAAACMgAAAEYA////+gAAAAACAAAAB/sAAAAYAFAAbABhAHkAbABpAHMAdABEAG8AYwBrAQAAAmYAAAHIAAAAewD////7AAAAGgBTAHUAYgB0AGkAdABsAGUAcwBEAG8AYwBrAAAAAAD/////AAAAVgD////7AAAAHABwAHIAbwBwAGUAcgB0AGkAZQBzAEQAbwBjAGsAAAAAAP////8AAABYAP////sAAAAUAFIAZQBjAGUAbgB0AEQAbwBjAGsAAAAAAP////8AAAByAP////sAAAAWAGgAaQBzAHQAbwByAHkARABvAGMAawAAAAAA/////wAAAFgA////+wAAABQARQBuAGMAbwBkAGUARABvAGMAawAAAAAA/////wAAAJgA////+wAAABAASgBvAGIAcwBEAG8AYwBrAAAAAAD/////AAAAbgD////7AAAAGABUAGkAbQBlAGwAaQBuAGUARABvAGMAawEAAAI8AAAFRAAAAMgA/////AAABiQAAAFPAAAAAAD////6AAAAAwIAAAAL+wAAACIAVgBpAGQAZQBvAFcAYQB2AGUAZgBvAHIAbQBEAG8AYwBrAAAAAAD/////AAAAVgD////7AAAAHgBWAGkAZABlAG8AVgBlAGMAdABvAHIARABvAGMAawAAAAAA/////wAAAFYA////+wAAABoAVgBpAGQAZQBvAFoAbwBvAG0ARABvAGMAawAAAAAA/////wAAAFYA////+wAAABoAVgBpAGQAZQBvAFoAbwBvAG0ARABvAGMAawAAAAAA/////wAAAAAAAAAA+wAAABoAVgBpAGQAZQBvAFoAbwBvAG0ARABvAGMAawAAAAAA/////wAAAAAAAAAA+wAAAB4AUgBnAGIAVwBhAHYAZQBmAG8AcgBtAEQAbwBjAGsAAAAAAP////8AAABWAP////sAAAAaAFIAZwBiAFAAYQByAGEAZABlAEQAbwBjAGsAAAAAAP////8AAABWAP////sAAAAkAFYAaQBkAGUAbwBIAGkAcwB0AG8AZwByAGEAbQBEAG8AYwBrAAAAAAD/////AAAAVgD////7AAAAIgBBAHUAZABpAG8AVwBhAHYAZQBmAG8AcgBtAEQAbwBjAGsAAAAAAP////8AAABWAP////sAAAAiAEEAdQBkAGkAbwBTAHAAZQBjAHQAcgB1AG0ARABvAGMAawAAAAAA/////wAAAFYA////+wAAACwAQQB1AGQAaQBvAEwAbwB1AGQAbgBlAHMAcwBNAGUAdABlAHIARABvAGMAawAAAAJmAAAByAAAAFYA////+wAAABYATQBhAHIAawBlAHIAcwBEAG8AYwBrAAAAA+oAAAEMAAAARAD///8AAAPCAAACbAAAAAEAAAACAAAACAAAAAj8AAAAAQAAAAIAAAABAAAAFgBtAGEAaQBuAFQAbwBvAGwAQgBhAHIBAAAAAP////8AAAAAAAAAAA=="); + QByteArray::fromBase64("AAAA/wAAAAD9AAAAAwAAAAAAAANmAAAChPwCAAAAAfwAAAA7AAAChAAAASwA/////AEAAAAD/AAAAAAAAAHfAAABkAD////6AAAAAQIAAAAC+wAAABIARgBpAGwAZQBzAEQAbwBjAGsAAAAAAP////8AAACdAP////sAAAAWAEYAaQBsAHQAZQByAHMARABvAGMAawEAAAA7AAACgAAAASwA////+wAAABoASwBlAHkAZgByAGEAbQBlAHMARABvAGMAawEAAAHpAAABfQAAAMgA////+wAAABIATgBvAHQAZQBzAEQAbwBjAGsAAAAAAP////8AAABGAP///wAAAAEAAACZAAAChPwCAAAAA/sAAAAkAEEAdQBkAGkAbwBQAGUAYQBrAE0AZQB0AGUAcgBEAG8AYwBrAQAAADsAAAKEAAAAVwD////7AAAAIgBBAHUAZABpAG8AUwB1AHIAcgBvAHUAbgBkAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAeAEEAdQBkAGkAbwBWAGUAYwB0AG8AcgBEAG8AYwBrAAAAAAD/////AAAAVwD///8AAAADAAAHgAAAASr8AQAAAAT8AAAAAAAAAjIAAACZAP////oAAAAAAgAAAAf7AAAAGABQAGwAYQB5AGwAaQBzAHQARABvAGMAawEAAAJmAAAByAAAAJ0A////+wAAABoAUwB1AGIAdABpAHQAbABlAHMARABvAGMAawAAAAAA/////wAAAFcA////+wAAABwAcAByAG8AcABlAHIAdABpAGUAcwBEAG8AYwBrAAAAAAD/////AAAAWQD////7AAAAFABSAGUAYwBlAG4AdABEAG8AYwBrAAAAAAD/////AAAAcgD////7AAAAFgBoAGkAcwB0AG8AcgB5AEQAbwBjAGsAAAAAAP////8AAABZAP////sAAAAUAEUAbgBjAG8AZABlAEQAbwBjAGsAAAAAAP////8AAACXAP////sAAAAQAEoAbwBiAHMARABvAGMAawAAAAAA/////wAAAG8A////+wAAABgAVABpAG0AZQBsAGkAbgBlAEQAbwBjAGsBAAACPAAABUQAAADIAP////wAAAYkAAABTwAAAAAA////+gAAAAMCAAAAC/sAAAAiAFYAaQBkAGUAbwBXAGEAdgBlAGYAbwByAG0ARABvAGMAawAAAAAA/////wAAAFcA////+wAAAB4AVgBpAGQAZQBvAFYAZQBjAHQAbwByAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAaAFYAaQBkAGUAbwBaAG8AbwBtAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAaAFYAaQBkAGUAbwBaAG8AbwBtAEQAbwBjAGsAAAAAAP////8AAAAAAAAAAPsAAAAaAFYAaQBkAGUAbwBaAG8AbwBtAEQAbwBjAGsAAAAAAP////8AAAAAAAAAAPsAAAAeAFIAZwBiAFcAYQB2AGUAZgBvAHIAbQBEAG8AYwBrAAAAAAD/////AAAAVwD////7AAAAGgBSAGcAYgBQAGEAcgBhAGQAZQBEAG8AYwBrAAAAAAD/////AAAAVwD////7AAAAJABWAGkAZABlAG8ASABpAHMAdABvAGcAcgBhAG0ARABvAGMAawAAAAAA/////wAAAFcA////+wAAACIAQQB1AGQAaQBvAFcAYQB2AGUAZgBvAHIAbQBEAG8AYwBrAAAAAAD/////AAAAVwD////7AAAAIgBBAHUAZABpAG8AUwBwAGUAYwB0AHIAdQBtAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAsAEEAdQBkAGkAbwBMAG8AdQBkAG4AZQBzAHMATQBlAHQAZQByAEQAbwBjAGsAAAACZgAAAcgAAABXAP////sAAAAWAE0AYQByAGsAZQByAHMARABvAGMAawAAAAPqAAABDAAAAEQA////AAADbQAAAoQAAAABAAAAAgAAAAgAAAAI/AAAAAEAAAACAAAAAQAAABYAbQBhAGkAbgBUAG8AbwBsAEIAYQByAQAAAAD/////AAAAAAAAAAA="); static const auto kLayoutColorDefault = - QByteArray::fromBase64("AAAA/wAAAAD9AAAAAwAAAAAAAAJMAAACbPwCAAAAAfwAAABRAAACbAAAASwA////+gAAAAEBAAAABPsAAAAUAEUAbgBjAG8AZABlAEQAbwBjAGsAAAAAAP////8AAAFWAP////sAAAAWAEYAaQBsAHQAZQByAHMARABvAGMAawEAAAAAAAADEAAAAZAA////+wAAABoAUwB1AGIAdABpAHQAbABlAHMARABvAGMAawAAAAAA/////wAAAEQA////+wAAABIATgBvAHQAZQBzAEQAbwBjAGsAAAAAAP////8AAABGAP///wAAAAEAAAFeAAADoPwCAAAAA/wAAABRAAABDAAAAHIA/////AEAAAAC+wAAACQAQQB1AGQAaQBvAFAAZQBhAGsATQBlAHQAZQByAEQAbwBjAGsAAAAF+QAAAEQAAABEAP////wAAAYiAAABXgAAAHsA////+gAAAAACAAAAAvsAAAAeAFYAaQBkAGUAbwBWAGUAYwB0AG8AcgBEAG8AYwBrAQAAAHYAAADiAAAAVgD////7AAAAGgBWAGkAZABlAG8AWgBvAG8AbQBEAG8AYwBrAQAAAAD/////AAAAVgD////8AAABZwAAAVgAAAByAQAAG/oAAAAAAQAAAAb7AAAAGgBSAGcAYgBQAGEAcgBhAGQAZQBEAG8AYwBrAQAAAAD/////AAAARAD////7AAAAGgBWAGkAZABlAG8AWgBvAG8AbQBEAG8AYwBrAQAABfAAAAF6AAAAAAAAAAD7AAAAHgBSAGcAYgBXAGEAdgBlAGYAbwByAG0ARABvAGMAawEAAAAA/////wAAAEQA////+wAAACIAVgBpAGQAZQBvAFcAYQB2AGUAZgBvAHIAbQBEAG8AYwBrAQAAAAD/////AAAARAD////7AAAAIgBBAHUAZABpAG8AUwB1AHIAcgBvAHUAbgBkAEQAbwBjAGsAAAAAAP////8AAABEAP////sAAAAeAEEAdQBkAGkAbwBWAGUAYwB0AG8AcgBEAG8AYwBrAAAAAAD/////AAAARAD////8AAACyQAAASgAAABWAP////oAAAAAAQAAAAL7AAAAJABWAGkAZABlAG8ASABpAHMAdABvAGcAcgBhAG0ARABvAGMAawEAAAAA/////wAAAEQA////+wAAABoAVgBpAGQAZQBvAFoAbwBvAG0ARABvAGMAawEAAAXwAAABegAAAAAAAAAAAAAAAwAABhgAAAEq/AEAAAAE/AAAAAAAAAFWAAAAAAD////6/////wIAAAAF+wAAABgAUABsAGEAeQBsAGkAcwB0AEQAbwBjAGsAAAAAAP////8AAAB7AP////sAAAAUAFIAZQBjAGUAbgB0AEQAbwBjAGsAAAAAAP////8AAAByAP////sAAAAcAHAAcgBvAHAAZQByAHQAaQBlAHMARABvAGMAawAAAAAA/////wAAAFgA////+wAAABYAaABpAHMAdABvAHIAeQBEAG8AYwBrAAAAAAD/////AAAAWAD////7AAAAEABKAG8AYgBzAEQAbwBjAGsAAAAAAP////8AAABuAP////wAAAAAAAAGGAAAAMgA////+gAAAAACAAAAAvsAAAAYAFQAaQBtAGUAbABpAG4AZQBEAG8AYwBrAQAAAioAAAIEAAAAyAD////7AAAAGgBLAGUAeQBmAHIAYQBtAGUAcwBEAG8AYwBrAAAAAAD/////AAAAMwD////8AAAE6QAAAQEAAAAAAP////r/////AgAAAAP7AAAAIgBBAHUAZABpAG8AVwBhAHYAZQBmAG8AcgBtAEQAbwBjAGsAAAAAAP////8AAABWAP////sAAAAiAEEAdQBkAGkAbwBTAHAAZQBjAHQAcgB1AG0ARABvAGMAawAAAAAA/////wAAAFYA////+wAAACwAQQB1AGQAaQBvAEwAbwB1AGQAbgBlAHMAcwBNAGUAdABlAHIARABvAGMAawAAAAJmAAAByAAAAFYA////+wAAABYATQBhAHIAawBlAHIAcwBEAG8AYwBrAAAAAt8AAACzAAAARAD///8AAAPCAAACbAAAAAEAAAACAAAACAAAAAL8AAAAAQAAAAIAAAABAAAAFgBtAGEAaQBuAFQAbwBvAGwAQgBhAHIBAAAAAP////8AAAAAAAAAAA=="); + QByteArray::fromBase64("AAAA/wAAAAD9AAAAAwAAAAAAAAJMAAACWPwCAAAAAfwAAAA7AAACWAAAASwA////+gAAAAIBAAAABfsAAAASAEYAaQBsAGUAcwBEAG8AYwBrAAAAAAD/////AAAAmQD////7AAAAFABFAG4AYwBvAGQAZQBEAG8AYwBrAAAAAAD/////AAABVgD////7AAAAFgBGAGkAbAB0AGUAcgBzAEQAbwBjAGsBAAAAAAAAAxAAAAGQAP////sAAAAaAFMAdQBiAHQAaQB0AGwAZQBzAEQAbwBjAGsAAAAAAP////8AAABEAP////sAAAASAE4AbwB0AGUAcwBEAG8AYwBrAAAAAAD/////AAAARgD///8AAAABAAABXgAAA7j8AgAAAAP8AAAAOwAAARMAAABxAP////wBAAAAAvsAAAAkAEEAdQBkAGkAbwBQAGUAYQBrAE0AZQB0AGUAcgBEAG8AYwBrAAAABfkAAABEAAAARAD////8AAAGIgAAAV4AAAB7AP////oAAAAAAgAAAAL7AAAAHgBWAGkAZABlAG8AVgBlAGMAdABvAHIARABvAGMAawEAAAB2AAAA4gAAAFcA////+wAAABoAVgBpAGQAZQBvAFoAbwBvAG0ARABvAGMAawEAAAAA/////wAAAFcA/////AAAAVgAAAFhAAAAcQEAABn6AAAAAAEAAAAG+wAAABoAUgBnAGIAUABhAHIAYQBkAGUARABvAGMAawEAAAAA/////wAAAEQA////+wAAABoAVgBpAGQAZQBvAFoAbwBvAG0ARABvAGMAawEAAAXwAAABegAAAAAAAAAA+wAAAB4AUgBnAGIAVwBhAHYAZQBmAG8AcgBtAEQAbwBjAGsBAAAAAP////8AAABEAP////sAAAAiAFYAaQBkAGUAbwBXAGEAdgBlAGYAbwByAG0ARABvAGMAawEAAAAA/////wAAAEQA////+wAAACIAQQB1AGQAaQBvAFMAdQByAHIAbwB1AG4AZABEAG8AYwBrAAAAAAD/////AAAARAD////7AAAAHgBBAHUAZABpAG8AVgBlAGMAdABvAHIARABvAGMAawAAAAAA/////wAAAEQA/////AAAAsMAAAEwAAAAVwD////6AAAAAAEAAAAC+wAAACQAVgBpAGQAZQBvAEgAaQBzAHQAbwBnAHIAYQBtAEQAbwBjAGsBAAAAAP////8AAABEAP////sAAAAaAFYAaQBkAGUAbwBaAG8AbwBtAEQAbwBjAGsBAAAF8AAAAXoAAAAAAAAAAAAAAAMAAAYYAAABVvwBAAAABPwAAAAAAAAByQAAAJkA////+gAAAAACAAAABfsAAAAYAFAAbABhAHkAbABpAHMAdABEAG8AYwBrAQAAAAD/////AAAAnQD////7AAAAFABSAGUAYwBlAG4AdABEAG8AYwBrAAAAAAD/////AAAAcgD////7AAAAHABwAHIAbwBwAGUAcgB0AGkAZQBzAEQAbwBjAGsAAAAAAP////8AAABZAP////sAAAAWAGgAaQBzAHQAbwByAHkARABvAGMAawAAAAAA/////wAAAFkA////+wAAABAASgBvAGIAcwBEAG8AYwBrAAAAAAD/////AAAAbwD////8AAAB0wAABEUAAADIAP////oAAAAAAgAAAAL7AAAAGABUAGkAbQBlAGwAaQBuAGUARABvAGMAawEAAAIqAAACBAAAAMgA////+wAAABoASwBlAHkAZgByAGEAbQBlAHMARABvAGMAawAAAAAA/////wAAADQA/////AAABOkAAAEBAAAAAAD////6/////wIAAAAD+wAAACIAQQB1AGQAaQBvAFcAYQB2AGUAZgBvAHIAbQBEAG8AYwBrAAAAAAD/////AAAAVwD////7AAAAIgBBAHUAZABpAG8AUwBwAGUAYwB0AHIAdQBtAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAsAEEAdQBkAGkAbwBMAG8AdQBkAG4AZQBzAHMATQBlAHQAZQByAEQAbwBjAGsAAAACZgAAAcgAAABXAP////sAAAAWAE0AYQByAGsAZQByAHMARABvAGMAawAAAALfAAAAswAAAEQA////AAADwgAAAlgAAAABAAAAAgAAAAgAAAAC/AAAAAEAAAACAAAAAQAAABYAbQBhAGkAbgBUAG8AbwBsAEIAYQByAQAAAAD/////AAAAAAAAAAA="); static const auto kLayoutAudioDefault = - QByteArray::fromBase64("AAAA/wAAAAD9AAAAAwAAAAAAAAJsAAACbPwCAAAAAfwAAABRAAACbAAAASwA////+gAAAAEBAAAABPsAAAAUAEUAbgBjAG8AZABlAEQAbwBjAGsAAAAAAP////8AAAFWAP////sAAAAWAEYAaQBsAHQAZQByAHMARABvAGMAawEAAAAAAAADEAAAAZAA////+wAAABoAUwB1AGIAdABpAHQAbABlAHMARABvAGMAawAAAAAA/////wAAAEQA////+wAAABIATgBvAHQAZQBzAEQAbwBjAGsAAAAAAP////8AAABGAP///wAAAAEAAAE+AAADoPwCAAAAA/wAAABRAAABoQAAAFYA/////AEAAAAC+wAAACQAQQB1AGQAaQBvAFAAZQBhAGsATQBlAHQAZQByAEQAbwBjAGsBAAAGQgAAAFcAAABEAP////wAAAajAAAA3QAAAEQA////+gAAAAACAAAAA/sAAAAsAEEAdQBkAGkAbwBMAG8AdQBkAG4AZQBzAHMATQBlAHQAZQByAEQAbwBjAGsBAAAAAP////8AAABWAP////sAAAAaAFYAaQBkAGUAbwBaAG8AbwBtAEQAbwBjAGsAAAAARgAAAP4AAABWAP////sAAAAeAFYAaQBkAGUAbwBWAGUAYwB0AG8AcgBEAG8AYwBrAAAAAAD/////AAAAVgD////8AAAB/AAAANEAAABWAP////oAAAAAAQAAAAf7AAAAIgBBAHUAZABpAG8AUwBwAGUAYwB0AHIAdQBtAEQAbwBjAGsBAAAAAP////8AAABEAP////sAAAAaAFIAZwBiAFAAYQByAGEAZABlAEQAbwBjAGsAAAAAAP////8AAABEAP////sAAAAaAFYAaQBkAGUAbwBaAG8AbwBtAEQAbwBjAGsAAAAF8AAAAXoAAAAAAAAAAPsAAAAeAFIAZwBiAFcAYQB2AGUAZgBvAHIAbQBEAG8AYwBrAAAAAAD/////AAAARAD////7AAAAIgBWAGkAZABlAG8AVwBhAHYAZQBmAG8AcgBtAEQAbwBjAGsAAAAAAP////8AAABEAP////sAAAAiAEEAdQBkAGkAbwBTAHUAcgByAG8AdQBuAGQARABvAGMAawAAAAAA/////wAAAEQA////+wAAAB4AQQB1AGQAaQBvAFYAZQBjAHQAbwByAEQAbwBjAGsAAAAAAP////8AAABEAP////wAAALXAAABGgAAAFYA////+gAAAAABAAAAA/sAAAAiAEEAdQBkAGkAbwBXAGEAdgBlAGYAbwByAG0ARABvAGMAawEAAAAA/////wAAAEQA////+wAAACQAVgBpAGQAZQBvAEgAaQBzAHQAbwBnAHIAYQBtAEQAbwBjAGsAAAAAAP////8AAABEAP////sAAAAaAFYAaQBkAGUAbwBaAG8AbwBtAEQAbwBjAGsAAAAF8AAAAXoAAAAAAAAAAAAAAAMAAAY4AAABKvwBAAAAA/wAAAAAAAABVgAAAAAA////+v////8CAAAABfsAAAAYAFAAbABhAHkAbABpAHMAdABEAG8AYwBrAAAAAAD/////AAAAewD////7AAAAFABSAGUAYwBlAG4AdABEAG8AYwBrAAAAAAD/////AAAAcgD////7AAAAHABwAHIAbwBwAGUAcgB0AGkAZQBzAEQAbwBjAGsAAAAAAP////8AAABYAP////sAAAAWAGgAaQBzAHQAbwByAHkARABvAGMAawAAAAAA/////wAAAFgA////+wAAABAASgBvAGIAcwBEAG8AYwBrAAAAAAD/////AAAAbgD////8AAAAAAAABjgAAADIAP////oAAAAAAgAAAAL7AAAAGABUAGkAbQBlAGwAaQBuAGUARABvAGMAawEAAAIqAAACBAAAAMgA////+wAAABoASwBlAHkAZgByAGEAbQBlAHMARABvAGMAawEAAAAA/////wAAADMA////+wAAABYATQBhAHIAawBlAHIAcwBEAG8AYwBrAAAAAtQAAADeAAAARAD///8AAAPCAAACbAAAAAEAAAACAAAACAAAAAL8AAAAAQAAAAIAAAABAAAAFgBtAGEAaQBuAFQAbwBvAGwAQgBhAHIBAAAAAP////8AAAAAAAAAAA=="); + QByteArray::fromBase64("AAAA/wAAAAD9AAAAAwAAAAAAAAJsAAACP/wCAAAAAfwAAAA7AAACPwAAASwA////+gAAAAIBAAAABfsAAAASAEYAaQBsAGUAcwBEAG8AYwBrAAAAAAD/////AAAAmQD////7AAAAFABFAG4AYwBvAGQAZQBEAG8AYwBrAAAAAAD/////AAABVgD////7AAAAFgBGAGkAbAB0AGUAcgBzAEQAbwBjAGsBAAAAAAAAAxAAAAGQAP////sAAAAaAFMAdQBiAHQAaQB0AGwAZQBzAEQAbwBjAGsAAAAAAP////8AAABEAP////sAAAASAE4AbwB0AGUAcwBEAG8AYwBrAAAAAAD/////AAAARgD///8AAAABAAABPgAAA7j8AgAAAAP8AAAAOwAAAawAAABXAP////wBAAAAAvsAAAAkAEEAdQBkAGkAbwBQAGUAYQBrAE0AZQB0AGUAcgBEAG8AYwBrAQAABkIAAABXAAAARAD////8AAAGowAAAN0AAABEAP////oAAAAAAgAAAAP7AAAALABBAHUAZABpAG8ATABvAHUAZABuAGUAcwBzAE0AZQB0AGUAcgBEAG8AYwBrAQAAAAD/////AAAAVwD////7AAAAGgBWAGkAZABlAG8AWgBvAG8AbQBEAG8AYwBrAAAAAEYAAAD+AAAAVwD////7AAAAHgBWAGkAZABlAG8AVgBlAGMAdABvAHIARABvAGMAawAAAAAA/////wAAAFcA/////AAAAfEAAADWAAAAVwD////6AAAAAAEAAAAH+wAAACIAQQB1AGQAaQBvAFMAcABlAGMAdAByAHUAbQBEAG8AYwBrAQAAAAD/////AAAARAD////7AAAAGgBSAGcAYgBQAGEAcgBhAGQAZQBEAG8AYwBrAAAAAAD/////AAAARAD////7AAAAGgBWAGkAZABlAG8AWgBvAG8AbQBEAG8AYwBrAAAABfAAAAF6AAAAAAAAAAD7AAAAHgBSAGcAYgBXAGEAdgBlAGYAbwByAG0ARABvAGMAawAAAAAA/////wAAAEQA////+wAAACIAVgBpAGQAZQBvAFcAYQB2AGUAZgBvAHIAbQBEAG8AYwBrAAAAAAD/////AAAARAD////7AAAAIgBBAHUAZABpAG8AUwB1AHIAcgBvAHUAbgBkAEQAbwBjAGsAAAAAAP////8AAABEAP////sAAAAeAEEAdQBkAGkAbwBWAGUAYwB0AG8AcgBEAG8AYwBrAAAAAAD/////AAAARAD////8AAAC0QAAASIAAABXAP////oAAAAAAQAAAAP7AAAAIgBBAHUAZABpAG8AVwBhAHYAZQBmAG8AcgBtAEQAbwBjAGsBAAAAAP////8AAABEAP////sAAAAkAFYAaQBkAGUAbwBIAGkAcwB0AG8AZwByAGEAbQBEAG8AYwBrAAAAAAD/////AAAARAD////7AAAAGgBWAGkAZABlAG8AWgBvAG8AbQBEAG8AYwBrAAAABfAAAAF6AAAAAAAAAAAAAAADAAAGOAAAAW/8AQAAAAP8AAAAAAAAAXwAAACZAP////oAAAAAAgAAAAX7AAAAGABQAGwAYQB5AGwAaQBzAHQARABvAGMAawEAAAAA/////wAAAJ0A////+wAAABQAUgBlAGMAZQBuAHQARABvAGMAawAAAAAA/////wAAAHIA////+wAAABwAcAByAG8AcABlAHIAdABpAGUAcwBEAG8AYwBrAAAAAAD/////AAAAWQD////7AAAAFgBoAGkAcwB0AG8AcgB5AEQAbwBjAGsAAAAAAP////8AAABZAP////sAAAAQAEoAbwBiAHMARABvAGMAawAAAAAA/////wAAAG8A/////AAAAYYAAASyAAAAyAD////6AAAAAAIAAAAC+wAAABgAVABpAG0AZQBsAGkAbgBlAEQAbwBjAGsBAAACKgAAAgQAAADIAP////sAAAAaAEsAZQB5AGYAcgBhAG0AZQBzAEQAbwBjAGsBAAAAAP////8AAAA0AP////sAAAAWAE0AYQByAGsAZQByAHMARABvAGMAawAAAALUAAAA3gAAAEQA////AAADwgAAAj8AAAABAAAAAgAAAAgAAAAC/AAAAAEAAAACAAAAAQAAABYAbQBhAGkAbgBUAG8AbwBsAEIAYQByAQAAAAD/////AAAAAAAAAAA="); static const auto kLayoutPlayerDefault = - QByteArray::fromBase64("AAAA/wAAAAD9AAAAAwAAAAAAAAJFAAADoPwCAAAAAfwAAABRAAADoAAAAAAA/////AEAAAAD+wAAABgAUABsAGEAeQBsAGkAcwB0AEQAbwBjAGsAAAAAAAAAAyIAAABGAP////wAAAAAAAACRQAAAAAA////+v////8CAAAABPsAAAAcAHAAcgBvAHAAZQByAHQAaQBlAHMARABvAGMAawAAAABGAAADGgAAAFgA////+wAAABoAUwB1AGIAdABpAHQAbABlAHMARABvAGMAawAAAAAA/////wAAAFYA////+wAAABYARgBpAGwAdABlAHIAcwBEAG8AYwBrAAAAAAD/////AAABLAD////7AAAAFABFAG4AYwBvAGQAZQBEAG8AYwBrAAAAAAD/////AAAAmAD////7AAAAEgBOAG8AdABlAHMARABvAGMAawAAAAAA/////wAAAEYA////AAAAAQAAAYsAAAI3/AIAAAAD/AAAADMAAAI3AAAAAAD////8AQAAAAL7AAAAJABBAHUAZABpAG8AUABlAGEAawBNAGUAdABlAHIARABvAGMAawAAAAXfAAABiwAAAEQA/////AAABegAAAGLAAAAAAD////6/////wIAAAAD+wAAABQAUgBlAGMAZQBuAHQARABvAGMAawAAAAAA/////wAAAHIA////+wAAABYAaABpAHMAdABvAHIAeQBEAG8AYwBrAAAAAAD/////AAAAWAD////7AAAAEABKAG8AYgBzAEQAbwBjAGsAAAAARgAAAG8AAABuAP////sAAAAiAEEAdQBkAGkAbwBTAHUAcgByAG8AdQBuAGQARABvAGMAawAAAAAA/////wAAAFYA////+wAAAB4AQQB1AGQAaQBvAFYAZQBjAHQAbwByAEQAbwBjAGsAAAAAAP////8AAABWAP///wAAAAMAAAT2AAABL/wBAAAAA/wAAAAAAAAD5QAAAAAA////+v////8CAAAAAvsAAAAYAFQAaQBtAGUAbABpAG4AZQBEAG8AYwBrAAAAAAD/////AAAAyAD////7AAAAGgBLAGUAeQBmAHIAYQBtAGUAcwBEAG8AYwBrAAAAA2YAAADIAAAAMwD////8AAAAAAAAB3MAAAAAAP////oAAAADAgAAAAv7AAAAIgBWAGkAZABlAG8AVwBhAHYAZQBmAG8AcgBtAEQAbwBjAGsAAAAAAP////8AAABWAP////sAAAAeAFYAaQBkAGUAbwBWAGUAYwB0AG8AcgBEAG8AYwBrAAAAAAD/////AAAAVgD////7AAAAGgBWAGkAZABlAG8AWgBvAG8AbQBEAG8AYwBrAAAAAAD/////AAAAVgD////7AAAAGgBWAGkAZABlAG8AWgBvAG8AbQBEAG8AYwBrAAAAAAD/////AAAAAAAAAAD7AAAAGgBWAGkAZABlAG8AWgBvAG8AbQBEAG8AYwBrAAAAAAD/////AAAAAAAAAAD7AAAAHgBSAGcAYgBXAGEAdgBlAGYAbwByAG0ARABvAGMAawAAAAAA/////wAAAFYA////+wAAABoAUgBnAGIAUABhAHIAYQBkAGUARABvAGMAawAAAAAA/////wAAAFYA////+wAAACQAVgBpAGQAZQBvAEgAaQBzAHQAbwBnAHIAYQBtAEQAbwBjAGsAAAAAAP////8AAABWAP////sAAAAiAEEAdQBkAGkAbwBXAGEAdgBlAGYAbwByAG0ARABvAGMAawAAAAAA/////wAAAFYA////+wAAACIAQQB1AGQAaQBvAFMAcABlAGMAdAByAHUAbQBEAG8AYwBrAAAAAAD/////AAAAVgD////7AAAALABBAHUAZABpAG8ATABvAHUAZABuAGUAcwBzAE0AZQB0AGUAcgBEAG8AYwBrAAAAAmYAAAHIAAAAVgD////7AAAAFgBNAGEAcgBrAGUAcgBzAEQAbwBjAGsAAAAAAAAABPYAAABEAP///wAAB4AAAAOgAAAAAQAAAAIAAAAIAAAACPwAAAABAAAAAgAAAAEAAAAWAG0AYQBpAG4AVABvAG8AbABCAGEAcgEAAAAA/////wAAAAAAAAAA"); + QByteArray::fromBase64("AAAA/wAAAAD9AAAAAwAAAAAAAAJFAAADuPwCAAAAAfwAAAA7AAADuAAAAAAA/////AEAAAAD/AAAAAAAAAJFAAAAAAD////6/////wIAAAAC+wAAABIARgBpAGwAZQBzAEQAbwBjAGsAAAAAAP////8AAACdAP////sAAAAYAFAAbABhAHkAbABpAHMAdABEAG8AYwBrAAAAADsAAAO4AAAAnQD////8AAAAAAAAAkUAAAAAAP////r/////AgAAAAT7AAAAHABwAHIAbwBwAGUAcgB0AGkAZQBzAEQAbwBjAGsAAAAARgAAAxoAAABZAP////sAAAAaAFMAdQBiAHQAaQB0AGwAZQBzAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAWAEYAaQBsAHQAZQByAHMARABvAGMAawAAAAAA/////wAAASwA////+wAAABQARQBuAGMAbwBkAGUARABvAGMAawAAAAAA/////wAAAJcA////+wAAABIATgBvAHQAZQBzAEQAbwBjAGsAAAAAAP////8AAABGAP///wAAAAEAAAGLAAADuPwCAAAAA/wAAAA7AAADuAAAAAAA/////AEAAAAC+wAAACQAQQB1AGQAaQBvAFAAZQBhAGsATQBlAHQAZQByAEQAbwBjAGsAAAAF3wAAAYsAAABEAP////wAAAX1AAABiwAAAAAA////+v////8CAAAAA/sAAAAUAFIAZQBjAGUAbgB0AEQAbwBjAGsAAAAAAP////8AAAByAP////sAAAAWAGgAaQBzAHQAbwByAHkARABvAGMAawAAAAAA/////wAAAFkA////+wAAABAASgBvAGIAcwBEAG8AYwBrAAAAAEYAAABvAAAAbwD////7AAAAIgBBAHUAZABpAG8AUwB1AHIAcgBvAHUAbgBkAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAeAEEAdQBkAGkAbwBWAGUAYwB0AG8AcgBEAG8AYwBrAAAAAAD/////AAAAVwD///8AAAADAAAE9gAAAS/8AQAAAAP8AAAAAAAAA+UAAAAAAP////r/////AgAAAAL7AAAAGABUAGkAbQBlAGwAaQBuAGUARABvAGMAawAAAAAA/////wAAAMgA////+wAAABoASwBlAHkAZgByAGEAbQBlAHMARABvAGMAawAAAANmAAAAyAAAADQA/////AAAAAAAAAdzAAAAAAD////6AAAAAwIAAAAL+wAAACIAVgBpAGQAZQBvAFcAYQB2AGUAZgBvAHIAbQBEAG8AYwBrAAAAAAD/////AAAAVwD////7AAAAHgBWAGkAZABlAG8AVgBlAGMAdABvAHIARABvAGMAawAAAAAA/////wAAAFcA////+wAAABoAVgBpAGQAZQBvAFoAbwBvAG0ARABvAGMAawAAAAAA/////wAAAFcA////+wAAABoAVgBpAGQAZQBvAFoAbwBvAG0ARABvAGMAawAAAAAA/////wAAAAAAAAAA+wAAABoAVgBpAGQAZQBvAFoAbwBvAG0ARABvAGMAawAAAAAA/////wAAAAAAAAAA+wAAAB4AUgBnAGIAVwBhAHYAZQBmAG8AcgBtAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAaAFIAZwBiAFAAYQByAGEAZABlAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAkAFYAaQBkAGUAbwBIAGkAcwB0AG8AZwByAGEAbQBEAG8AYwBrAAAAAAD/////AAAAVwD////7AAAAIgBBAHUAZABpAG8AVwBhAHYAZQBmAG8AcgBtAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAiAEEAdQBkAGkAbwBTAHAAZQBjAHQAcgB1AG0ARABvAGMAawAAAAAA/////wAAAFcA////+wAAACwAQQB1AGQAaQBvAEwAbwB1AGQAbgBlAHMAcwBNAGUAdABlAHIARABvAGMAawAAAAJmAAAByAAAAFcA////+wAAABYATQBhAHIAawBlAHIAcwBEAG8AYwBrAAAAAAAAAAT2AAAARAD///8AAAeAAAADuAAAAAEAAAACAAAACAAAAAj8AAAAAQAAAAIAAAABAAAAFgBtAGEAaQBuAFQAbwBvAGwAQgBhAHIBAAAAAP////8AAAAAAAAAAA=="); #endif // DEFAULTLAYOUTS_H diff --git a/src/docks/filesdock.cpp b/src/docks/filesdock.cpp new file mode 100644 index 0000000000..822d3e08a2 --- /dev/null +++ b/src/docks/filesdock.cpp @@ -0,0 +1,1266 @@ +/* + * Copyright (c) 2024 Meltytech, LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "filesdock.h" +#include "ui_filesdock.h" +#include "actions.h" +#include "mainwindow.h" +#include "settings.h" +#include "widgets/docktoolbar.h" +#include "widgets/playlisticonview.h" +#include "widgets/playlisttable.h" +#include "widgets/playlistlistview.h" +#include "util.h" +#include "qmltypes/qmlapplication.h" +#include "widgets/lineeditclear.h" +#include "models/playlistmodel.h" +#include "database.h" +#include "dialogs/listselectiondialog.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const auto kTilePaddingPx = 10; +static const auto kDetailedMode = QLatin1String("detailed"); +static const auto kIconsMode = QLatin1String("icons"); +static const auto kTiledMode = QLatin1String("tiled"); +static const QSet kAudioExtensions { + QLatin1String("m4a"), + QLatin1String("wav"), + QLatin1String("mp3"), + QLatin1String("ac3"), + QLatin1String("flac"), + QLatin1String("oga"), + QLatin1String("opus"), + QLatin1String("wma"), + QLatin1String("mka"), +}; +static const QSet kImageExtensions { + QLatin1String("jpg"), + QLatin1String("jpeg"), + QLatin1String("png"), + QLatin1String("bmp"), + QLatin1String("tif"), + QLatin1String("tiff"), + QLatin1String("svg"), + QLatin1String("webp"), + QLatin1String("gif"), + QLatin1String("tga"), +}; +static const QSet kOtherExtensions { + QLatin1String("mlt"), + QLatin1String("xml"), + QLatin1String("txt"), + QLatin1String("pdf"), + QLatin1String("doc"), + QLatin1String("gpx"), + QLatin1String("rawr"), + QLatin1String("stab"), + QLatin1String("srt"), + QLatin1String("so"), + QLatin1String("dll"), + QLatin1String("exe"), + QLatin1String("zip"), + QLatin1String("edl"), + QLatin1String("kdenlive"), + QLatin1String("osp"), + QLatin1String("blend"), + QLatin1String("swf"), + QLatin1String("cube"), + QLatin1String("json"), +}; +static const QSet kVideoExtensions { + QLatin1String("mp4"), + QLatin1String("m4v"), + QLatin1String("avi"), + QLatin1String("mpg"), + QLatin1String("mpeg"), + QLatin1String("ts"), + QLatin1String("mts"), + QLatin1String("m2ts"), + QLatin1String("mkv"), + QLatin1String("ogv"), + QLatin1String("webm"), + QLatin1String("dv"), + QLatin1String("lrv"), + QLatin1String("360"), + QLatin1String("flv"), + QLatin1String("wmv"), +}; + +static void cacheMediaType(FilesModel *model, const QString &filePath, int mediaType, + const QModelIndex &index); +static void cacheThumbnail(FilesModel *model, const QString &filePath, const QImage &image, + const QModelIndex &index); + +class FilesMediaTypeTask : public QRunnable +{ + FilesModel *m_model; + QString m_filePath; + QModelIndex m_index; + +public: + FilesMediaTypeTask(FilesModel *model, const QString &filePath, const QModelIndex &index) + : QRunnable() + , m_model(model) + , m_filePath(filePath) + , m_index(index) + { + } + +public: + void run() + { + static Mlt::Profile profile {"atsc_720p_60"}; + Mlt::Producer producer(profile, m_filePath.toUtf8().constData()); + auto mediaType = PlaylistModel::Other; + if (producer.is_valid()) { + if (MLT.isImageProducer(&producer)) { + mediaType = PlaylistModel::Image; + } else { + auto service = QString::fromLatin1(producer.get("mlt_service")); + if (service.startsWith(QLatin1String("avformat"))) { + if (producer.get_int("video_index") > -1 + && Util::getSuggestedFrameRate(&producer) != 90000) + mediaType = PlaylistModel::Video; + else if (producer.get_int("audio_index") > -1) + mediaType = PlaylistModel::Audio; + } + } + } + LOG_DEBUG() << "Mlt::Producer" << m_filePath << mediaType; + cacheMediaType(m_model, m_filePath, mediaType, m_index); + } +}; + +class FilesThumbnailTask : public QRunnable +{ + FilesModel *m_model; + QString m_filePath; + QModelIndex m_index; + +public: + FilesThumbnailTask(FilesModel *model, const QString &filePath, const QModelIndex &index) + : QRunnable() + , m_model(model) + , m_filePath(filePath) + , m_index(index) + { + } + + static QString cacheKey(const QString &filePath) + { + QCryptographicHash hash(QCryptographicHash::Sha1); + hash.addData(filePath.toUtf8()); + return hash.result().toHex(); + } + +private: + bool isValidService(Mlt::Producer &producer) const + { + if (producer.is_valid()) { + auto service = QString::fromLatin1(producer.get("mlt_service")); + return (service.startsWith("avformat") || + service == "qimage" || + service == "pixbuf" || + service == "glaxnimate"); + } + return false; + } + +public: + void run() + { + LOG_DEBUG() << "Mlt::Producer" << m_filePath; + QImage image; + static Mlt::Profile profile {"atsc_720p_60"}; + Mlt::Producer producer(profile, "abnormal", m_filePath.toUtf8().constData()); + if (isValidService(producer)) { + Mlt::Filter scaler(profile, "swscale"); + Mlt::Filter padder(profile, "resize"); + Mlt::Filter converter(profile, "avcolor_space"); + producer.attach(scaler); + producer.attach(padder); + producer.attach(converter); + + auto width = PlaylistModel::THUMBNAIL_WIDTH * 2; + auto height = PlaylistModel::THUMBNAIL_HEIGHT * 2; + image = MLT.image(producer, 0, width, height); + } + cacheThumbnail(m_model, m_filePath, image, m_index); + } +}; + +class FilesModel : public QFileSystemModel +{ +public: + enum Roles { + DateRole = QFileSystemModel::FilePermissions + 1, + MediaTypeRole, + MediaTypeStringRole, + ThumbnailRole, + }; + + explicit FilesModel(FilesDock *parent = nullptr) + : QFileSystemModel(parent) + , m_dock(parent) + { + } + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override + { + const auto isDir = fileInfo(index).isDir(); + if (MediaTypeStringRole == role || (index.column() == 2 && Qt::DisplayRole == role)) { + QString names[] = { + tr("Video"), + tr("Image"), + tr("Audio"), + tr("Other"), + QStringLiteral(""), + }; + auto i = isDir ? 4 : mediaType(index); + return names[i]; + } + switch (role) { + case Qt::ToolTipRole: + return QDir::toNativeSeparators(filePath(index)); + case DateRole: + return fileInfo(index).lastModified(); + case MediaTypeRole: + return isDir ? PlaylistModel::Other : mediaType(index); + case ThumbnailRole: { + if (isDir) { + QImage image(64, 64, QImage::Format_ARGB32); + QPainter painter(&image); + const auto icon = QIcon::fromTheme("folder", + QIcon(":/icons/oxygen/32x32/places/folder.png")); + image.fill(Qt::transparent); + icon.paint(&painter, image.rect()); + return image; + } + QImage image; + const auto path = filePath(index); + const auto thumbnailKey = FilesThumbnailTask::cacheKey(path); + image = DB.getThumbnail(thumbnailKey); + if (image.isNull()) { + ::cacheThumbnail(const_cast(this), path, image, QModelIndex()); + if (!path.endsWith(QStringLiteral(".mlt"), Qt::CaseInsensitive)) + QThreadPool::globalInstance()->start( + new FilesThumbnailTask(const_cast(this), path, index)); + } + return image; + } + default: + break; + } + + return QFileSystemModel::data(index, role); + } + + void updateThumbnails(const QModelIndex &index) + { + const auto path = filePath(index); + if (!path.endsWith(QStringLiteral(".mlt"), Qt::CaseInsensitive)) + QThreadPool::globalInstance()->start( + new FilesThumbnailTask(const_cast(this), path, index)); + } + +private: + FilesDock *m_dock; + + int mediaType(const QModelIndex &index) const + { + auto path = filePath(index); + auto mediaType = m_dock->getCacheMediaType(path); + + if (mediaType < 0) { + const auto info = QFileInfo(path); + const auto ext = info.suffix().toLower(); + if (info.isDir() || kOtherExtensions.contains(ext)) { + m_dock->setCacheMediaType(path, mediaType); + return PlaylistModel::Other; + } + if (kAudioExtensions.contains(ext)) { + m_dock->setCacheMediaType(path, mediaType); + return PlaylistModel::Audio; + } + if (kImageExtensions.contains(ext)) { + m_dock->setCacheMediaType(path, mediaType); + return PlaylistModel::Image; + } + if (kVideoExtensions.contains(ext)) { + m_dock->setCacheMediaType(path, mediaType); + return PlaylistModel::Video; + } + mediaType = PlaylistModel::Pending; + m_dock->setCacheMediaType(path, mediaType); + QThreadPool::globalInstance()->start( + new FilesMediaTypeTask(const_cast(this), path, index)); + } + + return mediaType; + } + +public: + void cacheMediaType(const QString &filePath, int mediaType, const QModelIndex &index) + { + m_dock->setCacheMediaType(filePath, mediaType); + emit dataChanged(index, index); + } + + void cacheThumbnail(const QString &filePath, QImage image, const QModelIndex &index) + { + if (image.isNull()) { + image = QImage(PlaylistModel::THUMBNAIL_WIDTH, PlaylistModel::THUMBNAIL_WIDTH, + QImage::Format_ARGB32); + image.fill(Qt::transparent); + } + auto key = FilesThumbnailTask::cacheKey(filePath); + DB.putThumbnail(key, image); + if (index.isValid()) + emit dataChanged(index, index); + } +}; + +static void cacheMediaType(FilesModel *model, const QString &filePath, int mediaType, + const QModelIndex &index) +{ + model->cacheMediaType(filePath, mediaType, index); +} + +static void cacheThumbnail(FilesModel *model, const QString &filePath, const QImage &image, + const QModelIndex &index) +{ + model->cacheThumbnail(filePath, image, index); +} + + +class FilesTileDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + FilesTileDelegate(QAbstractItemView *view, QWidget *parent = nullptr) + : QStyledItemDelegate(parent), + m_view(view) + { + connect(&Settings, SIGNAL(playlistThumbnailsChanged()), + SLOT(emitSizeHintChanged())); + } + + void paint(QPainter *painter, + const QStyleOptionViewItem &option, const QModelIndex &index) const + { + const QImage thumb = index.data(FilesModel::ThumbnailRole).value(); + const int lineHeight = painter->fontMetrics().height(); + const auto fileInfo = QFileInfo(index.data(Qt::ToolTipRole).toString()); + const QFont oldFont = painter->font(); + QFont boldFont(oldFont); + boldFont.setBold(true); + + if (option.state & QStyle::State_Selected) { + painter->fillRect(option.rect, option.palette.highlight().color()); + } else { + if (option.features & QStyleOptionViewItem::Alternate) + painter->fillRect(option.rect, option.palette.alternateBase()); + } + + auto thumbRect = thumb.rect(); + const float width = qRound(16.f / 9.f * option.rect.height()); + thumbRect = QRect(0, 0, width, qRound(width * thumbRect.height() / thumbRect.width())); + thumbRect.moveCenter(option.rect.center()); + thumbRect.moveLeft(0); + painter->drawImage(thumbRect, thumb); + auto textRect = option.rect; + textRect.setHeight(lineHeight * 3 + kTilePaddingPx); + textRect.moveCenter(option.rect.center()); + textRect.setLeft(thumbRect.width() + kTilePaddingPx); + + QPoint textPoint = textRect.topLeft(); + textPoint.setY(textPoint.y() + lineHeight); + painter->setFont(boldFont); + painter->drawText(textPoint, + painter->fontMetrics().elidedText(fileInfo.fileName(), Qt::ElideMiddle, textRect.width())); + painter->setFont(oldFont); + + textPoint.setY(textPoint.y() + lineHeight); + painter->drawText(textPoint, tr("Date: %1").arg( + index.data(FilesModel::DateRole).toDateTime().toString("yyyy-MM-dd HH:mm:ss"))); + textPoint.setY(textPoint.y() + lineHeight); + if (!fileInfo.isDir()) { + // Get the text of the second (size) column + auto myindex = index.model()->index(index.row(), 1, index.parent()); + auto mediaType = myindex.data(Qt::DisplayRole).toString(); + painter->drawText(textPoint, tr("Size: %1").arg(mediaType)); + } + } + + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const + { + Q_UNUSED(option); + Q_UNUSED(index); + return QSize(m_view->viewport()->width(), PlaylistModel::THUMBNAIL_HEIGHT + kTilePaddingPx); + } + +private slots: + void emitSizeHintChanged() + { + emit sizeHintChanged(QModelIndex()); + } + +private: + QAbstractItemView *m_view; + +}; + +class FilesProxyModel : public QSortFilterProxyModel +{ +public: + explicit FilesProxyModel(QObject *parent = nullptr) + : QSortFilterProxyModel(parent) + { + } + + void setMediaTypes(QList types) + { + m_mediaTypes = types; + invalidateFilter(); + } + +protected: + bool filterAcceptsRow(int row, const QModelIndex &parent) const override + { + const auto index = sourceModel()->index(row, 0, parent); + const auto model = qobject_cast(sourceModel()); + + // Media types + if (m_mediaTypes.size() > 0 && m_mediaTypes.size() < 4 && !model->isDir(index)) { + if (!m_mediaTypes.contains(index.data(FilesModel::MediaTypeRole))) + return false; + } + + // Text search + return index.data(QFileSystemModel::FileNameRole).toString().contains(filterRegularExpression()); + } + + bool lessThan(const QModelIndex &left, const QModelIndex &right) const override + { + const auto model = qobject_cast(sourceModel()); + if (model->isDir(left) && model->isDir(right)) { + return left.data().toString().toLower() < right.data().toString().toLower(); + } else if (model->isDir(left) || model->isDir(right)) { + return model->isDir(left); + } + return QSortFilterProxyModel::lessThan(left, right); + } + +private: + QList m_mediaTypes { + PlaylistModel::Video, PlaylistModel::Audio, PlaylistModel::Image, PlaylistModel::Other}; +}; + +FilesDock::FilesDock(QWidget *parent) + : QDockWidget(parent) + , ui(new Ui::FilesDock) +{ + LOG_DEBUG() << "begin"; + ui->setupUi(this); + toggleViewAction()->setIcon(windowIcon()); + + const auto ls = QStandardPaths::standardLocations(QStandardPaths::HomeLocation); + const auto home = ls.first(); + ui->locationsCombo->addItem(tr("Home", "The user's home folder in the file system"), home); + ui->locationsCombo->addItem(tr("Current Project"), ""); + ui->locationsCombo->addItem(tr("Documents"), + QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).first()); +#if defined(Q_OS_MAC) + ui->locationsCombo->addItem(tr("Movies", + "The system-provided videos folder called Movies on macOS"), + QStandardPaths::standardLocations(QStandardPaths::MoviesLocation).first()); +#else + ui->locationsCombo->addItem(tr("Videos"), + QStandardPaths::standardLocations(QStandardPaths::MoviesLocation).first()); +#endif + ui->locationsCombo->addItem(tr("Music"), + QStandardPaths::standardLocations(QStandardPaths::MusicLocation).first()); + ui->locationsCombo->addItem(tr("Pictures", "The system-provided photos folder"), + QStandardPaths::standardLocations(QStandardPaths::PicturesLocation).first()); + ui->removeLocationButton->setDisabled(true); + connect(ui->locationsCombo, &QComboBox::currentIndexChanged, this, [ = ](int index) { + ui->removeLocationButton->setEnabled(index > 5); + }); + + // Add from Settings + auto locations = Settings.filesLocations(); + for (const auto &name : locations) { + auto path = Settings.filesLocationPath(name); + ui->locationsCombo->addItem(name, path); + } + + m_filesModel = new FilesModel(this); + m_filesModel->setOption(QFileSystemModel::DontUseCustomDirectoryIcons); + m_filesModel->setFilter(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot); + m_filesModel->setReadOnly(true); + m_filesModel->setRootPath(home); + m_filesProxyModel = new FilesProxyModel(this); + m_filesProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + m_filesProxyModel->setSourceModel(m_filesModel); + m_filesProxyModel->setFilterRole(QFileSystemModel::FileNameRole); + m_filesProxyModel->setSortCaseSensitivity(Qt::CaseInsensitive); + m_filesProxyModel->setRecursiveFilteringEnabled(true); + m_selectionModel = new QItemSelectionModel(m_filesProxyModel, this); + connect(m_selectionModel, &QItemSelectionModel::selectionChanged, this, + &FilesDock::selectionChanged); + + m_dirsModel.setReadOnly(true); + m_dirsModel.setRootPath(QString()); + m_dirsModel.setOption(QFileSystemModel::DontUseCustomDirectoryIcons); + m_dirsModel.setOption(QFileSystemModel::DontWatchForChanges); + m_dirsModel.setFilter(QDir::Drives | QDir::Dirs | QDir::NoDotAndDotDot); + + int width = qRound(150 * devicePixelRatio()); + ui->splitter->setSizes({width, this->width() - width}); + ui->splitter->setStretchFactor(0, 0); + ui->splitter->setStretchFactor(1, 1); + ui->treeView->setModel(&m_dirsModel); + ui->treeView->setSelectionMode(QAbstractItemView::SingleSelection); + for (int i = 1; i < m_dirsModel.columnCount(); ++i) + ui->treeView->hideColumn(i); + ui->treeView->setContextMenuPolicy(Qt::CustomContextMenu); + const auto homeIndex = m_dirsModel.index(home); + ui->treeView->setExpanded(homeIndex, true); + ui->treeView->scrollTo(homeIndex); + ui->treeView->setCurrentIndex(homeIndex); + connect(ui->treeView, &QWidget::customContextMenuRequested, this, [ = ](const QPoint & pos) { + QMenu menu(this); + menu.exec(mapToGlobal(pos)); + }); + connect(ui->treeView, &QAbstractItemView::clicked, this, [ = ](const QModelIndex & index) { + auto filePath = m_dirsModel.filePath(index); + LOG_DEBUG() << "clicked" << filePath; + auto sourceIndex = m_filesModel->setRootPath(filePath); + m_view->setRootIndex(m_filesProxyModel->mapFromSource(sourceIndex)); + m_view->scrollToTop(); + }); + + setupActions(); + + m_mainMenu = new QMenu(tr("Files"), this); + m_mainMenu->addAction(Actions["filesOpenAction"]); + m_mainMenu->addAction(Actions["filesGoUp"]); + m_mainMenu->addAction(Actions["filesOpenPreviousAction"]); + m_mainMenu->addAction(Actions["filesOpenNextAction"]); + m_mainMenu->addAction(Actions["filesUpdateThumbnailsAction"]); + m_mainMenu->addAction(Actions["filesShowInFolder"]); + m_mainMenu->addSeparator(); + QMenu *selectMenu = m_mainMenu->addMenu(tr("Select")); + selectMenu->addAction(Actions["filesSelectAllAction"]); + selectMenu->addAction(Actions["filesSelectNoneAction"]); + Actions.loadFromMenu(m_mainMenu); + + DockToolBar *toolbar = new DockToolBar(tr("Files Controls")); + toolbar->setAreaHint(Qt::BottomToolBarArea); + QToolButton *menuButton = new QToolButton(); + menuButton->setIcon(QIcon::fromTheme("show-menu", + QIcon(":/icons/oxygen/32x32/actions/show-menu.png"))); + menuButton->setToolTip(tr("Files Menu")); + menuButton->setAutoRaise(true); + menuButton->setPopupMode(QToolButton::QToolButton::InstantPopup); + menuButton->setMenu(m_mainMenu); + toolbar->addWidget(menuButton); + toolbar->addAction(Actions["filesGoUp"]); + toolbar->addSeparator(); + toolbar->addAction(Actions["filesViewDetailsAction"]); + toolbar->addAction(Actions["filesViewTilesAction"]); + toolbar->addAction(Actions["filesViewIconsAction"]); + toolbar->addSeparator(); + ui->verticalLayout->addWidget(toolbar); + ui->verticalLayout->addSpacing(2); + + toolbar = new DockToolBar(tr("Files Filters")); + toolbar->addAction(Actions["filesFoldersView"]); + ui->filtersLayout->addWidget(toolbar); + + auto toolbar2 = new QToolBar(tr("Files Filters")); + QString styleSheet = QStringLiteral( + "QToolButton {" + " background-color: palette(background);" + " border-style: solid;" + " border-width: 1px;" + " border-radius: 3px;" + " border-color: palette(shadow);" + " color: palette(button-text);" + "}" + "QToolButton:checked {" + " color:palette(highlighted-text);" + " background-color:palette(highlight);" + " border-color: palette(highlight);" + "}" + ); + toolbar2->setStyleSheet(styleSheet); + ui->filtersLayout->addItem(new QSpacerItem(20, 20, QSizePolicy::Expanding, QSizePolicy::Minimum)); + toolbar2->addActions({Actions["filesFiltersVideo"], Actions["filesFiltersAudio"], Actions["filesFiltersImage"], Actions["filesFiltersOther"]}); + ui->filtersLayout->addWidget(toolbar2); + auto lineEdit = new LineEditClear(this); + lineEdit->setToolTip(tr("Only show files whose name contains some text")); + lineEdit->setPlaceholderText(tr("search")); + connect(lineEdit, &QLineEdit::textChanged, this, [ = ](const QString & search) { + m_filesProxyModel->setFilterFixedString(search); + if (search.isEmpty()) { + m_view->setRootIndex(m_filesProxyModel->mapFromSource(m_filesModel->index( + m_filesModel->rootPath()))); + } + m_view->scrollToTop(); + }); + ui->filtersLayout->addWidget(lineEdit, 1); + + m_iconsView = new PlaylistIconView(this); + ui->listView->parentWidget()->layout()->addWidget(m_iconsView); + m_iconsView->setIconRole(FilesModel::ThumbnailRole); + ui->tableView->setModel(m_filesProxyModel); + ui->listView->setModel(m_filesProxyModel); + m_iconsView->setModel(m_filesProxyModel); + ui->tableView->setSelectionModel(m_selectionModel); + ui->listView->setSelectionModel(m_selectionModel); + m_iconsView->setSelectionModel(m_selectionModel); + ui->tableView->setColumnHidden(2, true); + ui->tableView->sortByColumn(0, Qt::AscendingOrder); + ui->tableView->horizontalHeader()->setSectionsMovable(true); + ui->tableView->setColumnWidth(1, 100); + connect(ui->tableView, &QAbstractItemView::activated, this, [ = ] (const QModelIndex & index) { + auto sourceIndex = m_filesProxyModel->mapToSource(index); + auto filePath = m_filesModel->filePath(sourceIndex); + + LOG_DEBUG() << "activated" << filePath; + if (m_filesModel->isDir(sourceIndex)) { + m_filesModel->setRootPath(filePath); + m_view->setRootIndex(index); + m_view->scrollToTop(); + const auto dirsIndex = m_dirsModel.index(filePath); + ui->treeView->setExpanded(dirsIndex, true); + ui->treeView->scrollTo(dirsIndex); + ui->treeView->setCurrentIndex(dirsIndex); + return; + } + m_view->setCurrentIndex(index); + emit clipOpened(filePath); + }); + connect(ui->tableView->horizontalHeader(), &QHeaderView::sortIndicatorChanged, + this, [ = ](int column, Qt::SortOrder order) { + ui->tableView->sortByColumn(column, order); + }); + connect(ui->listView, &QAbstractItemView::activated, ui->tableView, &QAbstractItemView::activated); + connect(m_iconsView, &QAbstractItemView::activated, ui->tableView, &QAbstractItemView::activated); + + QList views; + views << ui->tableView; + views << ui->listView; + views << m_iconsView; + for (auto view : views) { + view->setDragDropMode(QAbstractItemView::DragOnly); + view->setAcceptDrops(false); + view->setAlternatingRowColors(true); + connect(view, SIGNAL(customContextMenuRequested(QPoint)), + SLOT(viewCustomContextMenuRequested(QPoint))); + } + + if (Settings.viewMode() == kDetailedMode) { + Actions["filesViewDetailsAction"]->trigger(); + } else if (Settings.viewMode() == kTiledMode) { + Actions["filesViewTilesAction"]->trigger(); + } else { /* if (Settings.viewMode() == kIconsMode) */ + Actions["filesViewIconsAction"]->trigger(); + } + addOpenWithMenu(m_mainMenu); + + LOG_DEBUG() << "end"; +} + +FilesDock::~FilesDock() +{ + delete ui; +} + +int FilesDock::getCacheMediaType(const QString &key) +{ + QMutexLocker m_lock(&m_cacheMutex); + auto x = m_cache.find(key); + if (x == m_cache.end()) + return -1; + return x.value().mediaType; +} + +void FilesDock::setCacheMediaType(const QString &key, int mediaType) +{ + QMutexLocker m_lock(&m_cacheMutex); + m_cache[key].mediaType = mediaType; +} + +void FilesDock::setupActions() +{ + QIcon icon; + QAction *action; + QActionGroup *modeGroup = new QActionGroup(this); + modeGroup->setExclusive(true); + + action = new QAction(tr("Tiles"), this); + action->setToolTip(tr("View as tiles")); + icon = QIcon::fromTheme("view-list-details", + QIcon(":/icons/oxygen/32x32/actions/view-list-details.png")); + action->setIcon(icon); + action->setCheckable(true); + connect(action, &QAction::triggered, this, [&]() { + Settings.setFilesViewMode(kTiledMode); + updateViewMode(); + }); + modeGroup->addAction(action); + Actions.add("filesViewTilesAction", action); + + action = new QAction(tr("Icons"), this); + action->setToolTip(tr("View as icons")); + icon = QIcon::fromTheme("view-list-icons", + QIcon(":/icons/oxygen/32x32/actions/view-list-icons.png")); + action->setIcon(icon); + action->setCheckable(true); + connect(action, &QAction::triggered, this, [&]() { + Settings.setFilesViewMode(kIconsMode); + updateViewMode(); + }); + modeGroup->addAction(action); + Actions.add("filesViewIconsAction", action); + + action = new QAction(tr("Details"), this); + action->setToolTip(tr("View as details")); + icon = QIcon::fromTheme("view-list-text", + QIcon(":/icons/oxygen/32x32/actions/view-list-text.png")); + action->setIcon(icon); + action->setCheckable(true); + connect(action, &QAction::triggered, this, [&]() { + Settings.setFilesViewMode(kDetailedMode); + updateViewMode(); + }); + modeGroup->addAction(action); + Actions.add("filesViewDetailsAction", action); + + action = new QAction(tr("Open In Shotcut"), this); + action->setToolTip(tr("Open the clip in the Source player")); + action->setEnabled(false); + connect(action, &QAction::triggered, this, &FilesDock::onOpenActionTriggered); + connect(this, &FilesDock::selectionChanged, action, [ = ]() { + action->setEnabled(m_view->currentIndex().isValid()); + }); + Actions.add("filesOpenAction", action); + + action = new QAction(tr("System Default"), this); + action->setEnabled(false); + connect(action, &QAction::triggered, this, [ = ]() { + auto filePath = firstSelectedFilePath(); + if (filePath.isEmpty()) + filePath = m_filesModel->rootPath(); + LOG_DEBUG() << filePath; +#if defined(Q_OS_WIN) + const auto scheme = QLatin1String("file:///"); +#else + const auto scheme = QLatin1String("file://"); +#endif + QDesktopServices::openUrl({scheme + filePath, QUrl::TolerantMode}); + }); + connect(this, &FilesDock::selectionChanged, action, [ = ]() { + action->setEnabled(m_view->currentIndex().isValid()); + }); + Actions.add("filesOpenDefaultAction", action); + + action = new QAction(tr("Other..."), this); + action->setEnabled(false); + connect(this, &FilesDock::selectionChanged, action, [ = ]() { + action->setEnabled(m_view->currentIndex().isValid()); + }); + connect(action, &QAction::triggered, this, &FilesDock::onOpenOtherAdd); + Actions.add("filesOpenWithOtherAction", action); + + action = new QAction(tr("Remove..."), this); + action->setEnabled(false); + connect(this, &FilesDock::selectionChanged, action, [ = ]() { + action->setEnabled(m_view->currentIndex().isValid()); + }); + connect(action, &QAction::triggered, this, &FilesDock::onOpenOtherRemove); + Actions.add("filesOpenWithRemoveAction", action); + + action = new QAction(tr("Show In File Manager"), this); + connect(action, &QAction::triggered, this, [ = ]() { + auto filePath = firstSelectedFilePath(); + if (filePath.isEmpty()) + filePath = m_filesModel->rootPath(); + LOG_DEBUG() << filePath; + Util::showInFolder(filePath); + }); + Actions.add("filesShowInFolder", action); + + action = new QAction(tr("Update Thumbnails"), this); + action->setEnabled(false); + connect(action, &QAction::triggered, this, &FilesDock::onUpdateThumbnailsActionTriggered); + connect(this, &FilesDock::selectionChanged, action, [ = ]() { + action->setEnabled(m_filesProxyModel->rowCount() > 0); + }); + Actions.add("filesUpdateThumbnailsAction", action); + + action = new QAction(tr("Select All"), this); + // action->setShortcut(QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_A)); + connect(action, &QAction::triggered, this, &FilesDock::onSelectAllActionTriggered); + action->setEnabled(m_filesProxyModel->rowCount() > 0); + Actions.add("filesSelectAllAction", action); + + action = new QAction(tr("Select None"), this); + // action->setShortcut(QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_D)); + connect(action, &QAction::triggered, this, [ = ]() { + m_view->setCurrentIndex({}); + m_selectionModel->clearSelection(); + }); + connect(this, &FilesDock::selectionChanged, action, [ = ]() { + action->setEnabled(m_selectionModel->selection().size() > 0); + }); + Actions.add("filesSelectNoneAction", action); + + action = new QAction(tr("Open Previous"), this); + // action->setShortcut(QKeySequence(Qt::ALT | Qt::Key_Up)); + action->setEnabled(false); + connect(action, &QAction::triggered, this, [ = ]() { + raise(); + incrementIndex(-1); + onOpenActionTriggered(); + }); + connect(this, &FilesDock::selectionChanged, action, [ = ]() { + action->setEnabled(m_view->currentIndex().isValid()); + }); + Actions.add("filesOpenPreviousAction", action); + + action = new QAction(tr("Open Next"), this); + // action->setShortcut(QKeySequence(Qt::ALT | Qt::Key_Down)); + action->setEnabled(false); + connect(action, &QAction::triggered, this, [ = ]() { + raise(); + incrementIndex(1); + onOpenActionTriggered(); + }); + connect(this, &FilesDock::selectionChanged, action, [ = ]() { + action->setEnabled(m_view->currentIndex().isValid()); + }); + Actions.add("filesOpenNextAction", action); + + action = new QAction(tr("Video"), this); + action->setToolTip(tr("Show or hide video files")); + action->setCheckable(true); + action->setChecked(true); + connect(action, &QAction::triggered, this, &FilesDock::onMediaTypeClicked); + Actions.add("filesFiltersVideo", action, this->windowTitle()); + + action = new QAction(tr("Audio"), this); + action->setToolTip(tr("Show or hide audio files")); + action->setCheckable(true); + action->setChecked(true); + connect(action, &QAction::triggered, this, &FilesDock::onMediaTypeClicked); + Actions.add("filesFiltersAudio", action, this->windowTitle()); + + action = new QAction(tr("Image"), this); + action->setToolTip(tr("Show or hide image files")); + action->setCheckable(true); + action->setChecked(true); + connect(action, &QAction::triggered, this, &FilesDock::onMediaTypeClicked); + Actions.add("filesFiltersImage", action, this->windowTitle()); + + action = new QAction(tr("Other"), this); + action->setToolTip(tr("Show or hide other kinds of files")); + action->setCheckable(true); + action->setChecked(true); + connect(action, &QAction::triggered, this, &FilesDock::onMediaTypeClicked); + Actions.add("filesFiltersOther", action, this->windowTitle()); + + action = new QAction(tr("Folders"), this); + action->setToolTip(tr("Hide or show the list of folders")); + icon = QIcon::fromTheme("view-choose", + QIcon(":/icons/oxygen/32x32/actions/view-choose.png")); + action->setIcon(icon); + action->setCheckable(true); + action->setChecked(true); + connect(action, &QAction::triggered, this, [ = ](bool checked) { + ui->treeView->setVisible(checked); + }); + Actions.add("filesFoldersView", action, windowTitle()); + + action = new QAction(tr("Go Up"), this); + action->setToolTip(tr("Show the parent folder")); + action->setShortcut({Qt::ALT | Qt::Key_Backspace}); + icon = QIcon::fromTheme("lift", + QIcon(":/icons/oxygen/32x32/actions/lift.png")); + action->setIcon(icon); + connect(action, &QAction::triggered, this, [ = ]() { + auto dir = QDir(m_filesModel->rootPath()); + dir.cdUp(); + const auto filePath = dir.absolutePath(); + const auto index = m_filesModel->setRootPath(filePath); + m_view->setRootIndex(m_filesProxyModel->mapFromSource(index)); + m_view->scrollToTop(); + const auto dirsIndex = m_dirsModel.index(filePath); + ui->treeView->setExpanded(dirsIndex, true); + ui->treeView->scrollTo(dirsIndex); + ui->treeView->setCurrentIndex(dirsIndex); + }); + Actions.add("filesGoUp", action); +} + +void FilesDock::incrementIndex(int step) +{ + QModelIndex index = m_view->currentIndex(); + if (!index.isValid()) + index = m_filesModel->index(0, 0); + if (index.isValid()) { + auto row = qBound(0, index.row() + step, m_filesProxyModel->rowCount(index.parent()) - 1); + index = m_filesProxyModel->index(row, index.column(), index.parent()); + m_view->setCurrentIndex(index); + } +} + +void FilesDock::addOpenWithMenu(QMenu *menu) +{ + auto subMenu = menu->addMenu(tr("Open With")); + subMenu->addAction(Actions["filesOpenDefaultAction"]); + subMenu->addSeparator(); + // custom options + auto programs = Settings.filesOpenOther(firstSelectedMediaType()); + for (const auto &program : programs) { + auto action = subMenu->addAction(QFileInfo(program).baseName(), this, [ = ]() { + const auto filePath = firstSelectedFilePath(); + LOG_DEBUG() << program << filePath; + QProcess::startDetached(program, {QDir::toNativeSeparators(filePath)}); + }); + action->setObjectName(program); + } + subMenu->addSeparator(); + // custom options actions + subMenu->addAction(Actions["filesOpenWithOtherAction"]); + subMenu->addAction(Actions["filesOpenWithRemoveAction"]); +} + +QString FilesDock::firstSelectedFilePath() +{ + QString result; + if (!m_view->selectionModel()->selectedIndexes().isEmpty()) { + const auto index = m_view->selectionModel()->selectedIndexes().first(); + if (index.isValid()) { + auto sourceIndex = m_filesProxyModel->mapToSource(index); + result = m_filesModel->filePath(sourceIndex); + } + } + return result; +} + +QString FilesDock::firstSelectedMediaType() +{ + QString result; + if (!m_view->selectionModel()->selectedIndexes().isEmpty()) { + const auto index = m_view->selectionModel()->selectedIndexes().first(); + if (index.isValid()) { + auto sourceIndex = m_filesProxyModel->mapToSource(index); + switch (sourceIndex.data(FilesModel::MediaTypeRole).toInt()) { + case PlaylistModel::Audio: + result = QLatin1String("audio"); + break; + case PlaylistModel::Image: + result = QLatin1String("image"); + break; + case PlaylistModel::Video: + result = QLatin1String("video"); + break; + default: + result = QLatin1String("other"); + break; + } + } + } + return result; +} + +void FilesDock::onOpenActionTriggered() +{ + const auto filePath = firstSelectedFilePath(); + if (!filePath.isEmpty()) { + const auto index = m_filesProxyModel->mapFromSource(m_filesModel->index(filePath)); + m_view->setCurrentIndex(index); + emit clipOpened(filePath); + } +} + +void FilesDock::changeDirectory(const QString &filePath) +{ + LOG_DEBUG() << filePath; + QFileInfo info(filePath); + auto path = info.isDir() ? filePath : info.path(); + auto index = m_dirsModel.index(path); + ui->treeView->setExpanded(index, true); + ui->treeView->scrollTo(index); + ui->treeView->setCurrentIndex(index); + index = m_filesModel->setRootPath(path); + m_view->setRootIndex(m_filesProxyModel->mapFromSource(index)); + if (info.isDir()) { + m_view->scrollToTop(); + } else { + QTimer::singleShot(2000, this, [ = ]() { + const auto index = m_filesProxyModel->mapFromSource(m_filesModel->index(filePath)); + m_view->scrollTo(index); + m_view->setCurrentIndex(index); + }); + } +} + +void FilesDock::viewCustomContextMenuRequested(const QPoint &pos) +{ + QModelIndex index = m_view->currentIndex(); + if (index.isValid()) { + QMenu menu(this); + menu.addAction(Actions["filesOpenAction"]); + menu.addAction(Actions["filesUpdateThumbnailsAction"]); + menu.addAction(Actions["filesShowInFolder"]); + addOpenWithMenu(&menu); + menu.exec(mapToGlobal(pos)); + } +} + +void FilesDock::updateViewMode() +{ + ui->listView->hide(); + ui->tableView->hide(); + m_iconsView->hide(); + + if (ui->listView->itemDelegate()) { + QAbstractItemDelegate *delegate = ui->listView->itemDelegate(); + ui->listView->setItemDelegate(nullptr); + delete delegate; + } + + QString mode = Settings.filesViewMode(); + if (mode == kDetailedMode) { + m_view = ui->tableView; + } else if (mode == kTiledMode) { + m_view = ui->listView; + if (!ui->listView->itemDelegate()) + ui->listView->setItemDelegate(new FilesTileDelegate(ui->listView)); + } else { + m_view = m_iconsView; + } + m_view->setRootIndex(m_filesProxyModel->mapFromSource(m_filesModel->index( + m_filesModel->rootPath()))); + m_view->show(); +} + +void FilesDock::keyPressEvent(QKeyEvent *event) +{ +#if defined(Q_OS_MAC) + if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) { + if (!m_view->selectionModel()->selectedIndexes().isEmpty()) { + const auto index = m_view->selectionModel()->selectedIndexes().first(); + if (index.isValid()) { + const auto sourceIndex = m_filesProxyModel->mapToSource(index); + const auto filePath = m_filesModel->filePath(sourceIndex); + if (m_filesModel->isDir(sourceIndex)) { + m_filesModel->setRootPath(filePath); + m_view->setRootIndex(index); + m_view->scrollToTop(); + const auto dirsIndex = m_dirsModel.index(filePath); + ui->treeView->setExpanded(dirsIndex, true); + ui->treeView->scrollTo(dirsIndex); + ui->treeView->setCurrentIndex(dirsIndex); + return; + } + emit clipOpened(filePath); + } + } + event->accept(); + return; + } +#endif + QDockWidget::keyPressEvent(event); + if (event->key() == Qt::Key_Up || event->key() == Qt::Key_Down) + event->accept(); + if (!event->isAccepted()) + MAIN.keyPressEvent(event); +} + +void FilesDock::keyReleaseEvent(QKeyEvent *event) +{ + QDockWidget::keyReleaseEvent(event); + if (!event->isAccepted()) + MAIN.keyReleaseEvent(event); +} + +void FilesDock::onSelectAllActionTriggered() +{ + show(); + raise(); + m_view->selectionModel()->clearSelection(); + for (int i = 0; i < m_filesProxyModel->rowCount(m_view->rootIndex()); i++) { + m_view->selectionModel()->select(m_filesProxyModel->index(i, 0, m_view->rootIndex()), + QItemSelectionModel::Select | QItemSelectionModel::Rows); + } +} + +void FilesDock::onUpdateThumbnailsActionTriggered() +{ + for (const auto &index : m_view->selectionModel()->selectedIndexes()) { + auto sourceIndex = m_filesProxyModel->mapToSource(index); + if (sourceIndex.isValid()) + m_filesModel->updateThumbnails(sourceIndex); + } +} + +void FilesDock::onMediaTypeClicked() +{ + QList types; + if (Actions["filesFiltersVideo"]->isChecked()) + types << PlaylistModel::Video; + if (Actions["filesFiltersAudio"]->isChecked()) + types << PlaylistModel::Audio; + if (Actions["filesFiltersImage"]->isChecked()) + types << PlaylistModel::Image; + if (Actions["filesFiltersOther"]->isChecked()) + types << PlaylistModel::Other; + m_filesProxyModel->setMediaTypes(types); + m_view->scrollToTop(); +} + +void FilesDock::onOpenOtherAdd() +{ + LOG_DEBUG(); + const auto filePath = firstSelectedFilePath(); + if (filePath.isEmpty()) return; + + QString dir("/usr/bin"); + QString filter; +#if defined(Q_OS_WIN) + dir = QStringLiteral("C:/Program Files"); + filter = tr("Executable Files (*.exe);;All Files (*)"); +#elif defined(Q_OS_MAC) + dir = QStringLiteral("/Applications"); +#endif + const auto program = QFileDialog::getOpenFileName(MAIN.window(), tr("Choose Executable"), dir, + filter, + nullptr, Util::getFileDialogOptions()); + if (!program.isEmpty()) { + if (QProcess::startDetached(program, {QDir::toNativeSeparators(filePath)})) { + Settings.setFilesOpenOther(firstSelectedMediaType(), program); + } + } +} + +void FilesDock::onOpenOtherRemove() +{ + const auto mediaType = firstSelectedMediaType(); + LOG_DEBUG() << mediaType; + auto ls = Settings.filesOpenOther(mediaType); + ls.sort(Qt::CaseInsensitive); + QStringList programs; + std::for_each(ls.begin(), ls.end(), [&](const QString & s) { + programs << QDir::toNativeSeparators(s); + }); + ListSelectionDialog dialog(programs, this); + dialog.setWindowModality(QmlApplication::dialogModality()); + dialog.setWindowTitle(tr("Remove From Open Other")); + if (QDialog::Accepted == dialog.exec()) { + for (auto program : dialog.selection()) { + program = QDir::fromNativeSeparators(program); + Settings.removeFilesOpenOther(mediaType, program); + // Remove menu item + delete m_mainMenu->findChild(program); + } + } +} + +void FilesDock::on_locationsCombo_activated(int) +{ + auto path = ui->locationsCombo->currentData().toString(); + if (path.isEmpty() && !MAIN.fileName().isEmpty()) + path = QFileInfo(MAIN.fileName()).absolutePath(); + if (path.isEmpty()) + return; +#if defined(Q_OS_WIN) + if (QLatin1String("/") == path) + path = QStringLiteral("C:/"); +#endif + changeDirectory(path); +} + +void FilesDock::on_addLocationButton_clicked() +{ + const auto path = m_filesProxyModel->mapToSource(m_view->rootIndex()).data( + QFileSystemModel::FilePathRole).toString(); + if (path.isEmpty()) + return; + QInputDialog dialog(this); + dialog.setInputMode(QInputDialog::TextInput); + dialog.setWindowTitle(tr("Add Location")); + dialog.setLabelText(tr("Name") + QStringLiteral(" ").repeated(80)); + dialog.setWindowModality(QmlApplication::dialogModality()); + dialog.setTextValue(QDir::toNativeSeparators(path)); + if (QDialog::Accepted == dialog.exec()) { + auto name = dialog.textValue(); + if (name.isEmpty()) + name = path; + Settings.setFilesLocation(name, path); + ui->locationsCombo->addItem(name, path); + } +} + +void FilesDock::on_removeLocationButton_clicked() +{ + const auto &location = ui->locationsCombo->currentText(); + if (location.isEmpty()) + return; + QMessageBox dialog(QMessageBox::Question, + tr("Delete Location"), + tr("Are you sure you want to remove %1?").arg(location), + QMessageBox::No | QMessageBox::Yes, + this); + dialog.setDefaultButton(QMessageBox::Yes); + dialog.setEscapeButton(QMessageBox::No); + dialog.setWindowModality(QmlApplication::dialogModality()); + if (QMessageBox::Yes == dialog.exec()) { + Settings.removeFilesLocation(location); + const auto index = ui->locationsCombo->findText(location); + if (index > -1) + ui->locationsCombo->removeItem(index); + } +} + +#include "filesdock.moc" diff --git a/src/docks/filesdock.h b/src/docks/filesdock.h new file mode 100644 index 0000000000..ae41a54390 --- /dev/null +++ b/src/docks/filesdock.h @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2024 Meltytech, LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef FILESDOCK_H +#define FILESDOCK_H + +#include +#include +#include +#include +#include +#include + +namespace Ui { +class FilesDock; +} + +class QAbstractItemView; +class QItemSelectionModel; +class QMenu; +class PlaylistIconView; +class FilesModel; +class FilesProxyModel; +class QSortFilterProxyModel; + +class FilesDock : public QDockWidget +{ + Q_OBJECT + +public: + explicit FilesDock(QWidget *parent = 0); + ~FilesDock(); + + struct CacheItem { + int mediaType {-1}; // -1 = unknown + }; + + int getCacheMediaType(const QString &key); + void setCacheMediaType(const QString &key, int mediaType); + +signals: + void clipOpened(QString); + void selectionChanged(); + +public slots: + void onOpenActionTriggered(); + void changeDirectory(const QString &path); + +private slots: + void viewCustomContextMenuRequested(const QPoint &pos); + void onMediaTypeClicked(); + void onOpenOtherAdd(); + void onOpenOtherRemove(); + + void on_locationsCombo_activated(int index); + + void on_addLocationButton_clicked(); + + void on_removeLocationButton_clicked(); + +protected: + void keyPressEvent(QKeyEvent *event); + void keyReleaseEvent(QKeyEvent *event); + +private: + void setupActions(); + void emitDataChanged(const QVector &roles); + void updateViewMode(); + void onUpdateThumbnailsActionTriggered(); + void onSelectAllActionTriggered(); + void incrementIndex(int step); + void addOpenWithMenu(QMenu *menu); + QString firstSelectedFilePath(); + QString firstSelectedMediaType(); + + Ui::FilesDock *ui; + QAbstractItemView *m_view; + PlaylistIconView *m_iconsView; + QFileSystemModel m_dirsModel; + FilesModel *m_filesModel; + QItemSelectionModel *m_selectionModel; + QMenu *m_mainMenu; + FilesProxyModel *m_filesProxyModel; + QHash m_cache; + QMutex m_cacheMutex; +}; + +#endif // FILESDOCK_H diff --git a/src/docks/filesdock.ui b/src/docks/filesdock.ui new file mode 100644 index 0000000000..17d93dde2f --- /dev/null +++ b/src/docks/filesdock.ui @@ -0,0 +1,208 @@ + + + FilesDock + + + + 0 + 0 + 460 + 278 + + + + + :/icons/oxygen/32x32/apps/system-file-manager.png:/icons/oxygen/32x32/apps/system-file-manager.png + + + Files + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 5 + + + + + Qt::Orientation::Horizontal + + + QSizePolicy::Policy::Fixed + + + + 5 + 20 + + + + + + + + Locations + + + + + + + + 0 + 0 + + + + + + + + + 20 + 20 + + + + Add the current folder to the saved locations + + + + + + + :/icons/oxygen/32x32/actions/list-add.png:/icons/oxygen/32x32/actions/list-add.png + + + + + + + + 20 + 20 + + + + Remove the selected location + + + + + + + :/icons/oxygen/32x32/actions/list-remove.png:/icons/oxygen/32x32/actions/list-remove.png + + + + + + + + + + + + Qt::Orientation::Horizontal + + + + true + + + true + + + true + + + + + + + + Qt::ContextMenuPolicy::CustomContextMenu + + + + + + false + + + true + + + QAbstractItemView::SelectionMode::ExtendedSelection + + + QAbstractItemView::SelectionBehavior::SelectRows + + + QAbstractItemView::ScrollMode::ScrollPerPixel + + + 200 + + + true + + + false + + + + + + + Qt::ContextMenuPolicy::CustomContextMenu + + + QAbstractItemView::SelectionMode::ExtendedSelection + + + QListView::Movement::Static + + + + + + + + + + + + + PlaylistTable + QTableView +
widgets/playlisttable.h
+
+ + PlaylistListView + QListView +
widgets/playlistlistview.h
+
+
+ + + + +
diff --git a/src/jobs/encodejob.cpp b/src/jobs/encodejob.cpp index 175b32483c..df53f18041 100644 --- a/src/jobs/encodejob.cpp +++ b/src/jobs/encodejob.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2022 Meltytech, LLC + * Copyright (c) 2012-2024 Meltytech, LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -44,6 +44,11 @@ EncodeJob::EncodeJob(const QString &name, const QString &xml, int frameRateNum, connect(action, SIGNAL(triggered()), this, SLOT(onOpenTiggered())); m_successActions << action; + action = new QAction(tr("Show In Files"), this); + action->setToolTip(tr("Show In Files")); + connect(action, SIGNAL(triggered()), this, SLOT(onShowInFilesTriggered())); + m_successActions << action; + action = new QAction(tr("Show In Folder"), this); action->setToolTip(tr("Show In Folder")); connect(action, SIGNAL(triggered()), this, SLOT(onShowFolderTriggered())); diff --git a/src/jobs/meltjob.cpp b/src/jobs/meltjob.cpp index 020c0b6cc9..52ba082e0c 100644 --- a/src/jobs/meltjob.cpp +++ b/src/jobs/meltjob.cpp @@ -55,6 +55,11 @@ MeltJob::MeltJob(const QString &name, const QString &xml, int frameRateNum, int connect(action, SIGNAL(triggered()), this, SLOT(onOpenTiggered())); m_successActions << action; + action = new QAction(tr("Show In Folder"), this); + action->setToolTip(tr("Show In Files")); + connect(action, SIGNAL(triggered()), this, SLOT(onShowInFilesTriggered())); + m_successActions << action; + action = new QAction(tr("Show In Folder"), this); action->setToolTip(tr("Show In Folder")); connect(action, SIGNAL(triggered()), this, SLOT(onShowFolderTriggered())); @@ -74,6 +79,11 @@ void MeltJob::onShowFolderTriggered() Util::showInFolder(objectName()); } +void MeltJob::onShowInFilesTriggered() +{ + MAIN.showInFiles(objectName()); +} + MeltJob::MeltJob(const QString &name, const QString &xml, const QStringList &args, int frameRateNum, int frameRateDen) : MeltJob(name, xml, frameRateNum, frameRateDen) diff --git a/src/jobs/meltjob.h b/src/jobs/meltjob.h index ed85c1edc0..b45646c890 100644 --- a/src/jobs/meltjob.h +++ b/src/jobs/meltjob.h @@ -49,6 +49,7 @@ protected slots: virtual void onOpenTiggered(); virtual void onFinished(int exitCode, QProcess::ExitStatus exitStatus); void onShowFolderTriggered(); + void onShowInFilesTriggered(); void onReadyRead(); protected: diff --git a/src/jobs/videoqualityjob.cpp b/src/jobs/videoqualityjob.cpp index 0e35852b61..b2100b1e2c 100644 --- a/src/jobs/videoqualityjob.cpp +++ b/src/jobs/videoqualityjob.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2022 Meltytech, LLC + * Copyright (c) 2012-2024 Meltytech, LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -25,7 +25,6 @@ #include #include "mainwindow.h" #include "dialogs/textviewerdialog.h" -#include "util.h" VideoQualityJob::VideoQualityJob(const QString &name, const QString &xml, const QString &reportPath, int frameRateNum, int frameRateDen) @@ -42,6 +41,10 @@ VideoQualityJob::VideoQualityJob(const QString &name, const QString &xml, connect(action, SIGNAL(triggered()), this, SLOT(onViewReportTriggered())); m_successActions << action; + action = new QAction(tr("Show In Files"), this); + connect(action, SIGNAL(triggered()), this, SLOT(onShowInFilesTriggered())); + m_successActions << action; + action = new QAction(tr("Show In Folder"), this); connect(action, SIGNAL(triggered()), this, SLOT(onShowFolderTriggered())); m_successActions << action; diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 088f2beeb3..72a2503e76 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -72,6 +72,7 @@ #include "widgets/timelinepropertieswidget.h" #include "dialogs/unlinkedfilesdialog.h" #include "docks/keyframesdock.h" +#include "docks/filesdock.h" #include "docks/markersdock.h" #include "docks/notesdock.h" #include "docks/subtitlesdock.h" @@ -230,6 +231,7 @@ MainWindow::MainWindow() QThreadPool::globalInstance()->setMaxThreadCount(qMin(4, QThreadPool::globalInstance()->maxThreadCount())); + QThreadPool::globalInstance()->setThreadPriority(QThread::LowPriority); QImageReader::setAllocationLimit(1024); ProxyManager::removePending(); @@ -458,6 +460,18 @@ void MainWindow::setupAndConnectDocks() subMenu->addAction(Actions["playlistThumbnailsInOnlyLargeAction"]); ui->menuPlaylist->addAction(Actions["playlistPlayAfterOpenAction"]); + m_filesDock = new FilesDock(this); + m_filesDock->hide(); + m_filesDock->toggleViewAction()->setShortcuts({QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_4), QKeySequence(Qt::Key_F10)}); + ui->menuView->addAction(m_filesDock->toggleViewAction()); + // connect(m_filesDock, SIGNAL(itemActivated(QString)), this, SLOT(open(QString))); + connect(m_filesDock->toggleViewAction(), SIGNAL(triggered(bool)), this, + SLOT(onFilesDockTriggered(bool))); + connect(ui->actionFiles, SIGNAL(triggered()), this, SLOT(onFilesDockTriggered())); + connect(m_filesDock, &FilesDock::clipOpened, this, [ = ] (const QString & url) { + open(url); + }); + m_timelineDock = new TimelineDock(this); m_timelineDock->hide(); m_timelineDock->toggleViewAction()->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_5)); @@ -715,6 +729,7 @@ void MainWindow::setupAndConnectDocks() addDockWidget(Qt::RightDockWidgetArea, m_jobsDock); addDockWidget(Qt::LeftDockWidgetArea, m_notesDock); addDockWidget(Qt::LeftDockWidgetArea, m_subtitlesDock); + addDockWidget(Qt::RightDockWidgetArea, m_filesDock); splitDockWidget(m_timelineDock, m_markersDock, Qt::Horizontal); tabifyDockWidget(m_propertiesDock, m_playlistDock); tabifyDockWidget(m_playlistDock, m_filtersDock); @@ -723,7 +738,8 @@ void MainWindow::setupAndConnectDocks() tabifyDockWidget(m_notesDock, m_subtitlesDock); splitDockWidget(m_recentDock, findChild("AudioWaveformDock"), Qt::Vertical); splitDockWidget(audioMeterDock, m_recentDock, Qt::Horizontal); - tabifyDockWidget(m_recentDock, m_historyDock); + tabifyDockWidget(m_recentDock, m_filesDock); + tabifyDockWidget(m_filesDock, m_historyDock); tabifyDockWidget(m_historyDock, m_jobsDock); tabifyDockWidget(m_keyframesDock, m_timelineDock); m_recentDock->raise(); @@ -2907,6 +2923,12 @@ Mlt::Playlist *MainWindow::binPlaylist() return m_playlistDock->binPlaylist(); } +void MainWindow::showInFiles(const QString &filePath) +{ + onFilesDockTriggered(); + m_filesDock->changeDirectory(filePath); +} + bool MainWindow::continueModified() { if (isWindowModified()) { @@ -3077,6 +3099,14 @@ void MainWindow::onSubtitlesDockTriggered(bool checked) } } +void MainWindow::onFilesDockTriggered(bool checked) +{ + if (checked) { + m_filesDock->show(); + m_filesDock->raise(); + } +} + void MainWindow::onPlaylistCreated() { if (!playlist() || playlist()->count() == 0) return; @@ -3530,6 +3560,10 @@ QWidget *MainWindow::loadProducerWidget(Mlt::Producer *producer) connect(w, SIGNAL(modified()), m_keyframesDock, SLOT(onProducerModified())); connect(w, SIGNAL(modified()), m_filterController, SLOT(onProducerChanged())); } + if (-1 != w->metaObject()->indexOfSignal("showInFiles(QString)")) { + connect(w, SIGNAL(showInFiles(QString)), this, SLOT(onFilesDockTriggered())); + connect(w, SIGNAL(showInFiles(QString)), m_filesDock, SLOT(changeDirectory(QString))); + } if (-1 != w->metaObject()->indexOfSlot("updateDuration()")) { connect(m_timelineDock, SIGNAL(durationChanged()), w, SLOT(updateDuration())); } diff --git a/src/mainwindow.h b/src/mainwindow.h index bc56884dfd..b82f2e08ad 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -44,6 +44,7 @@ class QUndoStack; class QActionGroup; class FilterController; class ScopeController; +class FilesDock; class FiltersDock; class TimelineDock; class AutoSaveFile; @@ -135,6 +136,7 @@ class MainWindow : public QMainWindow void getMarkerRange(int position, int *start, int *end); void getSelectionRange(int *start, int *end); Mlt::Playlist *binPlaylist(); + void showInFiles(const QString &filePath); signals: void audioChannelsChanged(); @@ -234,6 +236,7 @@ class MainWindow : public QMainWindow NotesDock *m_notesDock; SubtitlesDock *m_subtitlesDock; std::unique_ptr m_producerWidget; + FilesDock *m_filesDock; public slots: bool isCompatibleWithGpuMode(MltXmlChecker &checker); @@ -280,6 +283,7 @@ private slots: void onMarkersDockTriggered(bool = true); void onNotesDockTriggered(bool = true); void onSubtitlesDockTriggered(bool = true); + void onFilesDockTriggered(bool = true); void onPlaylistCreated(); void onPlaylistLoaded(); void onPlaylistCleared(); diff --git a/src/mainwindow.ui b/src/mainwindow.ui index 1c403f07c5..d8914e37ed 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -343,11 +343,10 @@ - + - @@ -1421,6 +1420,15 @@ Pause After Seek + + + + :/icons/oxygen/32x32/apps/system-file-manager.png:/icons/oxygen/32x32/apps/system-file-manager.png + + + Files + + diff --git a/src/models/playlistmodel.h b/src/models/playlistmodel.h index 278d42d8fa..c31bbfd4b3 100644 --- a/src/models/playlistmodel.h +++ b/src/models/playlistmodel.h @@ -40,7 +40,8 @@ class PlaylistModel : public QAbstractTableModel Video, Image, Audio, - Other + Other, + Pending }; enum Columns { diff --git a/src/settings.cpp b/src/settings.cpp index 0bffbff983..4cfbf65063 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -323,6 +323,90 @@ void ShotcutSettings::setViewMode(const QString &viewMode) emit viewModeChanged(); } +QString ShotcutSettings::filesViewMode() const +{ + return settings.value("files/viewMode", QLatin1String("tiled")).toString(); +} + +void ShotcutSettings::setFilesViewMode(const QString &viewMode) +{ + settings.setValue("files/viewMode", viewMode); + emit filesViewModeChanged(); +} + +QStringList ShotcutSettings::filesLocations() const +{ + QStringList result; + for (const auto &s : settings.value("files/locations").toStringList()) { + if (!s.startsWith("__")) + result << s; + } + return result; +} + +QString ShotcutSettings::filesLocationPath(const QString &name) const +{ + QString key = QStringLiteral("files/location/%1").arg(name); + return settings.value(key).toString(); +} + +bool ShotcutSettings::setFilesLocation(const QString &name, const QString &path) +{ + bool isNew = false; + QStringList locations = filesLocations(); + if (!locations.contains(name)) { + isNew = true; + locations.append(name); + settings.setValue("files/locations", locations); + } + settings.setValue("files/location/" + name, path); + return isNew; +} + +bool ShotcutSettings::removeFilesLocation(const QString &name) +{ + QStringList list = filesLocations(); + int index = list.indexOf(name); + if (index > -1) { + list.removeAt(index); + if (list.isEmpty()) + settings.remove("files/locations"); + else + settings.setValue("files/locations", list); + settings.remove("files/location/" + name); + return true; + } + return false; +} + +QStringList ShotcutSettings::filesOpenOther(const QString &type) const +{ + return settings.value("files/openOther/" + type).toStringList(); +} + +void ShotcutSettings::setFilesOpenOther(const QString &type, const QString &filePath) +{ + QStringList filePaths = filesOpenOther(type); + filePaths.removeAll(filePath); + filePaths.append(filePath); + settings.setValue("files/openOther/" + type, filePaths); +} + +bool ShotcutSettings::removeFilesOpenOther(const QString &type, const QString &filePath) +{ + QStringList list = filesOpenOther(type); + int index = list.indexOf(filePath); + if (index > -1) { + list.removeAt(index); + if (list.isEmpty()) + settings.remove("files/openOther/" + type); + else + settings.setValue("files/openOther/" + type, list); + return true; + } + return false; +} + QString ShotcutSettings::exportFrameSuffix() const { return settings.value("exportFrameSuffix", ".png").toString(); diff --git a/src/settings.h b/src/settings.h index f2daebba75..cb502ae05d 100644 --- a/src/settings.h +++ b/src/settings.h @@ -347,6 +347,17 @@ class ShotcutSettings : public QObject void setNotesZoom(int zoom); int notesZoom() const; + // Files + QString filesViewMode() const; + void setFilesViewMode(const QString &viewMode); + QStringList filesLocations() const; + QString filesLocationPath(const QString &name) const; + bool setFilesLocation(const QString &name, const QString &path); + bool removeFilesLocation(const QString &name); + QStringList filesOpenOther(const QString &type) const; + void setFilesOpenOther(const QString &type, const QString &filePath); + bool removeFilesOpenOther(const QString &type, const QString &filePath); + public slots: void reset(); @@ -370,6 +381,7 @@ public slots: void videoOutDurationChanged(); void playlistThumbnailsChanged(); void viewModeChanged(); + void filesViewModeChanged(); void smallIconsChanged(); void askOutputFilterChanged(); void timelineScrollingChanged(); diff --git a/src/widgets/avformatproducerwidget.cpp b/src/widgets/avformatproducerwidget.cpp index c9bd12092b..e77bc3eede 100644 --- a/src/widgets/avformatproducerwidget.cpp +++ b/src/widgets/avformatproducerwidget.cpp @@ -678,6 +678,8 @@ void AvformatProducerWidget::on_menuButton_clicked() { QMenu menu; menu.addAction(ui->actionReset); + if (!MLT.resource().contains("://")) // not a network stream + menu.addAction(ui->actionShowInFiles); if (!MLT.resource().contains("://")) // not a network stream menu.addAction(ui->actionOpenFolder); menu.addAction(ui->actionCopyFullFilePath); @@ -1420,3 +1422,8 @@ void AvformatProducerWidget::on_actionBitrateViewer_triggered() job->setLabel(tr("Bitrate %1").arg(Util::baseName(args.last()))); JOBS.add(job); } + +void AvformatProducerWidget::on_actionShowInFiles_triggered() +{ + emit showInFiles(Util::GetFilenameFromProducer(producer())); +} diff --git a/src/widgets/avformatproducerwidget.h b/src/widgets/avformatproducerwidget.h index 5e1c5f6433..8dbe0f6a95 100644 --- a/src/widgets/avformatproducerwidget.h +++ b/src/widgets/avformatproducerwidget.h @@ -48,6 +48,7 @@ public slots: void producerChanged(Mlt::Producer *); void producerReopened(bool play); void modified(); + void showInFiles(QString); protected: void keyPressEvent(QKeyEvent *event); @@ -141,6 +142,7 @@ private slots: private slots: void reloadProducerValues(); void on_actionBitrateViewer_triggered(); + void on_actionShowInFiles_triggered(); }; class ProbeTask : public QObject, public QRunnable diff --git a/src/widgets/avformatproducerwidget.ui b/src/widgets/avformatproducerwidget.ui index 34b351dab8..1c2b425b86 100644 --- a/src/widgets/avformatproducerwidget.ui +++ b/src/widgets/avformatproducerwidget.ui @@ -1029,7 +1029,7 @@ - Show in Folder + Show In Folder @@ -1124,6 +1124,11 @@ View Bitrate + + + Show In Files + + diff --git a/src/widgets/imageproducerwidget.cpp b/src/widgets/imageproducerwidget.cpp index 69a230eeba..25e5efd235 100644 --- a/src/widgets/imageproducerwidget.cpp +++ b/src/widgets/imageproducerwidget.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2023 Meltytech, LLC + * Copyright (c) 2012-2024 Meltytech, LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -340,6 +340,8 @@ void ImageProducerWidget::on_notesTextEdit_textChanged() void ImageProducerWidget::on_menuButton_clicked() { QMenu menu; + if (!MLT.resource().contains("://")) // not a network stream + menu.addAction(ui->actionShowInFiles); if (!MLT.resource().contains("://")) // not a network stream menu.addAction(ui->actionOpenFolder); menu.addAction(ui->actionCopyFullFilePath); @@ -469,3 +471,8 @@ void ImageProducerWidget::on_proxyButton_clicked() ui->actionDisableProxy->setChecked(proxyDisabled); menu.exec(ui->proxyButton->mapToGlobal(QPoint(0, 0))); } + +void ImageProducerWidget::on_actionShowInFiles_triggered() +{ + emit showInFiles(GetFilenameFromProducer(producer())); +} diff --git a/src/widgets/imageproducerwidget.h b/src/widgets/imageproducerwidget.h index e7b54c57b2..3276a1eb9d 100644 --- a/src/widgets/imageproducerwidget.h +++ b/src/widgets/imageproducerwidget.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2021 Meltytech, LLC + * Copyright (c) 2012-2024 Meltytech, LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,7 +20,6 @@ #include #include "abstractproducerwidget.h" -#include "mltcontroller.h" namespace Ui { class ImageProducerWidget; @@ -42,6 +41,7 @@ class ImageProducerWidget : public QWidget, public AbstractProducerWidget void producerChanged(Mlt::Producer *); void producerReopened(bool play); void modified(); + void showInFiles(QString); public slots: void updateDuration(); @@ -84,6 +84,8 @@ private slots: void on_proxyButton_clicked(); + void on_actionShowInFiles_triggered(); + private: Ui::ImageProducerWidget *ui; int m_defaultDuration; diff --git a/src/widgets/imageproducerwidget.ui b/src/widgets/imageproducerwidget.ui index c2cc8e9eda..fa0daf3ec0 100644 --- a/src/widgets/imageproducerwidget.ui +++ b/src/widgets/imageproducerwidget.ui @@ -305,7 +305,7 @@ - Show in Folder + Show In Folder @@ -336,6 +336,11 @@ Copy Hash Code + + + Show In Files + + diff --git a/src/widgets/playlisticonview.cpp b/src/widgets/playlisticonview.cpp index 959cf8b97a..da2f8a8de6 100644 --- a/src/widgets/playlisticonview.cpp +++ b/src/widgets/playlisticonview.cpp @@ -28,11 +28,15 @@ #include #include +static const auto kPaddingPx = 10; +static const auto kFilesSizeFactor = 1.5f; + PlaylistIconView::PlaylistIconView(QWidget *parent) : QAbstractItemView(parent) , m_gridSize(170, 100) , m_draggingOverPos(QPoint()) , m_itemsPerRow(3) + , m_iconRole(Qt::DecorationRole) { verticalScrollBar()->setSingleStep(100); verticalScrollBar()->setPageStep(400); @@ -92,7 +96,7 @@ QModelIndex PlaylistIconView::indexAt(const QPoint &point) const int row = (point.y() + verticalScrollBar()->value()) / m_gridSize.height(); int col = (point.x() / m_gridSize.width()) % m_itemsPerRow; - return model()->index(row * m_itemsPerRow + col, 0); + return model()->index(row * m_itemsPerRow + col, 0, rootIndex()); } QModelIndex PlaylistIconView::moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers) @@ -161,6 +165,8 @@ void PlaylistIconView::paintEvent(QPaintEvent *) const auto proxy = tr("P", "The first letter or symbol of \"proxy\""); const auto oldFont = painter.font(); auto boldFont(oldFont); + + painter.setRenderHints(QPainter::TextAntialiasing | QPainter::SmoothPixmapTransform); boldFont.setBold(true); painter.fillRect(rect(), pal.base()); @@ -170,11 +176,11 @@ void PlaylistIconView::paintEvent(QPaintEvent *) auto proxyModel = static_cast(model()); QRect dragIndicator; - for (int row = 0; row <= proxyModel->rowCount() / m_itemsPerRow; row++) { + for (int row = 0; row <= proxyModel->rowCount(rootIndex()) / m_itemsPerRow; row++) { for (int col = 0; col < m_itemsPerRow; col++) { const int rowIdx = row * m_itemsPerRow + col; - QModelIndex idx = proxyModel->index(rowIdx, 0); + QModelIndex idx = proxyModel->index(rowIdx, 0, rootIndex()); if (!idx.isValid()) break; @@ -185,11 +191,17 @@ void PlaylistIconView::paintEvent(QPaintEvent *) continue; const bool selected = selectedIndexes().contains(idx); - const QImage thumb = proxyModel->mapToSource(idx).data(Qt::DecorationRole).value(); + QImage thumb = proxyModel->mapToSource(idx).data(m_iconRole).value(); + + if (m_iconRole != Qt::DecorationRole) { // Files + thumb = thumb.scaled(PlaylistModel::THUMBNAIL_WIDTH * kFilesSizeFactor, + PlaylistModel::THUMBNAIL_HEIGHT * kFilesSizeFactor, + Qt::KeepAspectRatio, Qt::SmoothTransformation); + } QRect imageBoundingRect = itemRect; imageBoundingRect.setHeight(0.7 * imageBoundingRect.height()); - imageBoundingRect.adjust(0, 10, 0, 0); + imageBoundingRect.adjust(0, kPaddingPx, 0, 0); QRect imageRect(QPoint(), thumb.size()); imageRect.moveCenter(imageBoundingRect.center()); @@ -282,7 +294,7 @@ void PlaylistIconView::dropEvent(QDropEvent *event) index = index.sibling(index.row() + 1, index.column()); const Qt::DropAction action = event->dropAction(); - int row = (index.row() != -1) ? index.row() : model()->rowCount(); + int row = (index.row() != -1) ? index.row() : model()->rowCount(rootIndex()); if (model()->dropMimeData(event->mimeData(), action, row, index.column(), index)) event->acceptProposedAction(); @@ -330,13 +342,16 @@ QAbstractItemView::DropIndicatorPosition PlaylistIconView::position(const QPoint void PlaylistIconView::updateSizes() { - if (!model() || !model()->rowCount()) { + if (!model() || !model()->rowCount(rootIndex())) { verticalScrollBar()->setRange(0, 0); return; } QSize size; - if (Settings.playlistThumbnails() == "tall") + if (m_iconRole != Qt::DecorationRole) // Files + size = QSize(PlaylistModel::THUMBNAIL_WIDTH * kFilesSizeFactor, + PlaylistModel::THUMBNAIL_HEIGHT * kFilesSizeFactor); + else if (Settings.playlistThumbnails() == "tall") size = QSize(PlaylistModel::THUMBNAIL_WIDTH, PlaylistModel::THUMBNAIL_HEIGHT * 2); else if (Settings.playlistThumbnails() == "large") size = QSize(PlaylistModel::THUMBNAIL_WIDTH * 2, PlaylistModel::THUMBNAIL_HEIGHT * 2); @@ -345,7 +360,7 @@ void PlaylistIconView::updateSizes() else size = QSize(PlaylistModel::THUMBNAIL_WIDTH, PlaylistModel::THUMBNAIL_HEIGHT); - size.setWidth(size.width() + 10); + size.setWidth(size.width() + kPaddingPx); m_itemsPerRow = qMax(1, viewport()->width() / size.width()); m_gridSize = QSize(viewport()->width() / m_itemsPerRow, size.height() + 40); @@ -354,7 +369,8 @@ void PlaylistIconView::updateSizes() return; verticalScrollBar()->setRange(0, - m_gridSize.height() * model()->rowCount() / m_itemsPerRow - height() + m_gridSize.height()); + m_gridSize.height() * model()->rowCount(rootIndex()) / m_itemsPerRow - height() + + m_gridSize.height()); viewport()->update(); } @@ -363,3 +379,8 @@ void PlaylistIconView::resetMultiSelect() m_isToggleSelect = false; m_isRangeSelect = false; } + +void PlaylistIconView::setIconRole(int role) +{ + m_iconRole = role; +} diff --git a/src/widgets/playlisticonview.h b/src/widgets/playlisticonview.h index 97fa1057d5..8f208df9bf 100644 --- a/src/widgets/playlisticonview.h +++ b/src/widgets/playlisticonview.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 Meltytech, LLC + * Copyright (c) 2016-2024 Meltytech, LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -26,6 +26,7 @@ class PlaylistIconView : public QAbstractItemView public: PlaylistIconView(QWidget *parent); void resetMultiSelect(); + void setIconRole(int role); QRect visualRect(const QModelIndex &index) const Q_DECL_OVERRIDE; void scrollTo(const QModelIndex &index, ScrollHint hint = EnsureVisible) Q_DECL_OVERRIDE; @@ -71,6 +72,7 @@ private slots: bool m_isToggleSelect {false}; bool m_isRangeSelect {false}; QModelIndex m_pendingSelect; + int m_iconRole; }; #endif