diff --git a/.eslintrc.json b/.eslintrc.json index 7d2927c69ba..5b871fcb9b9 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -84,6 +84,7 @@ "object-curly-spacing": "warn", "prefer-const": "warn", "prefer-regex-literals": "warn", + "prefer-template": "warn", "quotes": [ "warn", "double" ], "require-atomic-updates": "error", "semi": "warn", diff --git a/.github/labeler.yml b/.github/labeler.yml index 8f181d7af8a..195c1d8bf5f 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -102,6 +102,12 @@ preferences: - any-glob-to-any-file: - src/preferences/** +qml: + - changed-files: + - any-glob-to-any-file: + - res/qml/** + - src/qml/** + skins: - changed-files: - any-glob-to-any-file: diff --git a/.github/workflows/download_cleanup.yml b/.github/workflows/download_cleanup.yml new file mode 100644 index 00000000000..397cffeb48e --- /dev/null +++ b/.github/workflows/download_cleanup.yml @@ -0,0 +1,34 @@ +name: Clean up downloads + +on: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-22.04 + + steps: + - name: "Set up SSH Agent" + if: env.SSH_PRIVATE_KEY != null + env: + SSH_AUTH_SOCK: /tmp/ssh_agent.sock + SSH_PRIVATE_KEY: ${{ secrets.DOWNLOADS_HOSTGATOR_DOT_MIXXX_DOT_ORG_KEY }} + SSH_HOST: downloads-hostgator.mixxx.org + run: | + ssh-agent -a $SSH_AUTH_SOCK > /dev/null + ssh-add - <<< "${SSH_PRIVATE_KEY}" + mkdir -p "${HOME}/.ssh" + ssh-keyscan "${SSH_HOST}" >> "${HOME}/.ssh/known_hosts" + echo "SSH_AUTH_SOCK=${SSH_AUTH_SOCK}" >> "${GITHUB_ENV}" + + - name: Delete obsolete files + if: env.SSH_AUTH_SOCK != null + run: | + mkdir empty_folder + echo build-checks-fix/ >> include_file.txt + echo build-checks-fix/* >> include_file.txt + rsync --verbose --archive --times --recursive --delete --include-from=include_file.txt --exclude=* "empty_folder/" "${SSH_USER}@${SSH_HOST}:${DESTDIR}/" + env: + DESTDIR: public_html/downloads/snapshots + SSH_HOST: downloads-hostgator.mixxx.org + SSH_USER: mixxx diff --git a/CHANGELOG.md b/CHANGELOG.md index 8aac5d4913a..dbca711c83f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,6 @@ ### Features -* Sync Lock: End of track checking is not needed [#11035](https://github.com/mixxxdj/mixxx/pull/11035) * BaseExternalLibraryFeature: Add ability to import external playlists as crates [#11852](https://github.com/mixxxdj/mixxx/pull/11852) * Logging: Include timestamps in messages by default [#11861](https://github.com/mixxxdj/mixxx/pull/11861) * DlgPrefSound: Fix fetching of sample rate [#11951](https://github.com/mixxxdj/mixxx/pull/11951) [11949](https://github.com/mixxxdj/mixxx/issues/11949) @@ -14,16 +13,23 @@ * Playlist feature: add 'Shuffle playlist' sidebar action [#12498](https://github.com/mixxxdj/mixxx/pull/12498) * New built-in effect: Glitch [#11329](https://github.com/mixxxdj/mixxx/pull/11329) * Fullscreen toggle rework [#11566](https://github.com/mixxxdj/mixxx/pull/11566) +* Playlists: Update of playlist labels after adding tracks [#12866](https://github.com/mixxxdj/mixxx/pull/12866) [#12761](https://github.com/mixxxdj/mixxx/issues/12761) +* Tracks: Custom text color for played tracks (qss) [#12744](https://github.com/mixxxdj/mixxx/pull/12744) [#5911](https://github.com/mixxxdj/mixxx/issues/5911) [#12912](https://github.com/mixxxdj/mixxx/pull/12912) +* History: Show track count and duration in sidebar [#12811](https://github.com/mixxxdj/mixxx/pull/12811) [#12788](https://github.com/mixxxdj/mixxx/issues/12788) +* fixes around cratetablemodel, remove tracks + don't allow pasting tracks into locked playlists/crates or History [#12926](https://github.com/mixxxdj/mixxx/pull/12926) +* Tootips: Improve `rate_up/down` tooltips, pitch vs. speed [#12590](https://github.com/mixxxdj/mixxx/pull/12590) +* Shortkeys for track list management [#12020](https://github.com/mixxxdj/mixxx/pull/12020) +* Track menu: Rephrase "Reset" to "Clear" [#12955](https://github.com/mixxxdj/mixxx/pull/12955) ### Controller Mappings -* Traktor Kontrol S2 MK3: Add colored hotcues [#4637](https://github.com/mixxxdj/mixxx/pull/4637) * Pioneer DDJ-FLX4: mapping improvements [#12842](https://github.com/mixxxdj/mixxx/pull/12842) ### Controller Backend * Send sysex to all handlers [#12827](https://github.com/mixxxdj/mixxx/pull/12827) * Add control for showing a deck's track menu [#10825](https://github.com/mixxxdj/mixxx/pull/10825) +* Removed old examples HID keyboard and HID trackpad [#12977](https://github.com/mixxxdj/mixxx/pull/12977) ### Experimental QML Skin @@ -34,6 +40,8 @@ * fix(qml): Improve knobs by applying selective 4xMSAA on the Arc shape [#12541](https://github.com/mixxxdj/mixxx/pull/12541) * Add QML interceptor to auto reload on file change [#12795](https://github.com/mixxxdj/mixxx/pull/12795) * Add multi-sampling settings for QML [#12546](https://github.com/mixxxdj/mixxx/pull/12546) [#12794](https://github.com/mixxxdj/mixxx/pull/12794) [#12536](https://github.com/mixxxdj/mixxx/issues/12536) +* Install qml module on Windows [#12604](https://github.com/mixxxdj/mixxx/pull/12604) +* Add scrolling waveforms [#3967](https://github.com/mixxxdj/mixxx/pull/3967) ### Update to Qt6 @@ -59,6 +67,7 @@ * Fix track color background with Qt6 [#12380](https://github.com/mixxxdj/mixxx/pull/12380) * multi-line delegate: fix bg color, Qt6 on Linux [#12478](https://github.com/mixxxdj/mixxx/pull/12478) * Revert "BaseTrackPlayer: Remove references to WaveformWidgetRenderer when using Qt6" [#12342](https://github.com/mixxxdj/mixxx/pull/12342) +* Fix Tango waveform splitter [#12939](https://github.com/mixxxdj/mixxx/pull/12939) ### Experimental iOs support @@ -78,6 +87,20 @@ * MacOsVersion: Rename to AppleOsVersion [#12662](https://github.com/mixxxdj/mixxx/pull/12662) * macOS: Add more `Q_OS_MACOS` cond-compiles where appropriate [#12663](https://github.com/mixxxdj/mixxx/pull/12663) +### Experimental WebAssembly support + +* CMakeLists: Add support for targeting Emscripten/WebAssembly [#12918](https://github.com/mixxxdj/mixxx/pull/12918) +* CMakeLists: Emit better errors for exotic target platforms [#12910](https://github.com/mixxxdj/mixxx/pull/12910) +* Build: Add `PORTMIDI` flag for compiling with(out) PortMidi [#12913](https://github.com/mixxxdj/mixxx/pull/12913) +* DesktopHelper: Compile out process-spawning on WASM too [#12916](https://github.com/mixxxdj/mixxx/pull/12916) +* MixxxApplication: Use `QWasmIntegrationPlugin` when targeting WebAssembly [#12915](https://github.com/mixxxdj/mixxx/pull/12915) +* CMakeLists: Enable asyncify when targeting WASM [#12921](https://github.com/mixxxdj/mixxx/pull/12921) +* Resources: Bundle resources for preloading when targeting Emscripten/WASM [#12922](https://github.com/mixxxdj/mixxx/pull/12922) +* CMakeLists: Add `WASM_ASSERTIONS` option [#12931](https://github.com/mixxxdj/mixxx/pull/12931) +* VersionStore: Recognize Emscripten/WebAssembly [#12940](https://github.com/mixxxdj/mixxx/pull/12940) +* OpenGLWindow: Fix sizing on Wasm by setting `Qt::FramelessWindowHint` [#12945](https://github.com/mixxxdj/mixxx/pull/12945) +* CMakeLists: Require WebGL 2.0 when building for Wasm [#12952](https://github.com/mixxxdj/mixxx/pull/12952) + ### Misc Refactorings * Add missing `` include in `defs.h` [#11348](https://github.com/mixxxdj/mixxx/pull/11348) @@ -111,10 +134,21 @@ * fix/History: remove obsolete placeholder playlists [#12494](https://github.com/mixxxdj/mixxx/pull/12494) * Add missing Taglib dependency [#12830](https://github.com/mixxxdj/mixxx/pull/12830) * fix: typo ;) [#12726](https://github.com/mixxxdj/mixxx/pull/12726) +* refactor: Avoid temporary qlist allocation on midi sysex receive [#12843](https://github.com/mixxxdj/mixxx/pull/12843) +* SoundSourceMP3: fix unused function warning [#12847](https://github.com/mixxxdj/mixxx/pull/12847) +* update libkeyfinder to 2.2.8 [#12853](https://github.com/mixxxdj/mixxx/pull/12853) +* feat(ci): Add eslint rule prefer-template [#12889](https://github.com/mixxxdj/mixxx/pull/12889) +* Labeler: Add `qml` to labeler config [#12911](https://github.com/mixxxdj/mixxx/pull/12911) +* WTrackMenu: Add missing wcoverartlabel.h include [#12924](https://github.com/mixxxdj/mixxx/pull/12924) +* Fix clazy complains and naming [#12935](https://github.com/mixxxdj/mixxx/pull/12935) +* src/library: Sort files into sub-directories [#12956](https://github.com/mixxxdj/mixxx/pull/12956) +* CMakeLists: Fix deduplication trap with `--preload-file` [#12944](https://github.com/mixxxdj/mixxx/pull/12944) +* Add CI runner that allows cleaning up the download server [#12957](https://github.com/mixxxdj/mixxx/pull/12957) * Update to latest vcpkg dependencies [#11649](https://github.com/mixxxdj/mixxx/pull/11649) [#12512](https://github.com/mixxxdj/mixxx/pull/12512) [#12067](https://github.com/mixxxdj/mixxx/pull/12067) + [#12898](https://github.com/mixxxdj/mixxx/pull/12898) * GitHub actions updates [#11544](https://github.com/mixxxdj/mixxx/pull/11544) [#11508](https://github.com/mixxxdj/mixxx/pull/11508) @@ -148,7 +182,33 @@ [#12626](https://github.com/mixxxdj/mixxx/pull/12626) [#12577](https://github.com/mixxxdj/mixxx/pull/12577) -## [2.4.0](https://launchpad.net/mixxx/+milestone/2.4.0) (2024-02-16) +## [2.4.1](https://github.com/mixxxdj/mixxx/milestone/41?closed=1) (unreleased) + +### Controller Mappings + +* Hercules Inpulse 200: Configure shift-browser knob to scroll the library (quick) [#12932](https://github.com/mixxxdj/mixxx/pull/12932) +* Pioneer DDJ-FLX4: Add waveform zoom and other mapping improvements + [#12896](https://github.com/mixxxdj/mixxx/pull/12896) + [#12842](https://github.com/mixxxdj/mixxx/pull/12842) +* Traktor Kontrol F1: Fixes for hid-parser and related script [#12876](https://github.com/mixxxdj/mixxx/pull/12876) +* Traktor S3: Fix mapping crash on macOS [#12840](https://github.com/mixxxdj/mixxx/pull/12840) + +### Target Support + +* Fix various minor build issues + [#12853](https://github.com/mixxxdj/mixxx/pull/12853) + [#12847](https://github.com/mixxxdj/mixxx/pull/12847) + [#12822](https://github.com/mixxxdj/mixxx/pull/12822) + [#12892](https://github.com/mixxxdj/mixxx/pull/12892) + +### Miscellaneous + +* Remove unnecessary unpolish operation of the style, before polish the new style [#12445](https://github.com/mixxxdj/mixxx/pull/12445) +* Developer Tools: Initially sort controls by group name, ascending [#12884](https://github.com/mixxxdj/mixxx/pull/12884) +* History: Show track count and duration in sidebar [#12811](https://github.com/mixxxdj/mixxx/pull/12811) +* Prevent removing tracks from locked playlists [#12927](https://github.com/mixxxdj/mixxx/pull/12927) + +## [2.4.0](https://github.com/mixxxdj/mixxx/milestone/15?closed=1) (2024-02-16) ### Music Library: Tracks Table & Track Menu @@ -587,7 +647,7 @@ * Log warning if deprecated control is used [#11972](https://github.com/mixxxdj/mixxx/pull/11972) * ControlObject alias improvements [#11973](https://github.com/mixxxdj/mixxx/pull/11973) * Keyboard mapping: Repeat certain control actions if key is held [#12474](https://github.com/mixxxdj/mixxx/pull/12474) -* Keyboard mapping: Return triggers doubleclick, move Preview functions to P / Shift+P [#12639](https://github.com/mixxxdj/mixxx/pull/12639) +* Keyboard mapping: Return triggers double-click, move Preview functions to P / Shift+P [#12639](https://github.com/mixxxdj/mixxx/pull/12639) * Keyboard mapping: Various fixes [#12730](https://github.com/mixxxdj/mixxx/pull/12730) * Update keyboard sheet [#12578](https://github.com/mixxxdj/mixxx/pull/12578) * Logging: Add support for `QT_MESSAGE_PATTERN` environment variable @@ -643,7 +703,7 @@ [#11975](https://github.com/mixxxdj/mixxx/pull/11975) [#11957](https://github.com/mixxxdj/mixxx/issues/11957) * Fix 500ms blocking of the whole event loop, when holding mouse down on title bar on Windows [#12359](https://github.com/mixxxdj/mixxx/pull/12359) [#12358](https://github.com/mixxxdj/mixxx/issues/12358) [#12433](https://github.com/mixxxdj/mixxx/pull/12433) [#12458](https://github.com/mixxxdj/mixxx/pull/12458) -* change SKIN_WARNING to show the skin file:line first, then c++ context [#12253](https://github.com/mixxxdj/mixxx/pull/12253) +* Change SKIN_WARNING to show the skin file and line first, then c++ context [#12253](https://github.com/mixxxdj/mixxx/pull/12253) * Fix style of selected QComboBox items on Windows [#12339](https://github.com/mixxxdj/mixxx/pull/12339) [#12323](https://github.com/mixxxdj/mixxx/issues/12323) * Fix reading the Spinny cover on Windows [#12103](https://github.com/mixxxdj/mixxx/pull/12103) [#11131](https://github.com/mixxxdj/mixxx/issues/11131) * Fix inconsistent/wrong musical keys in the UI [#12051](https://github.com/mixxxdj/mixxx/pull/12051) [#12044](https://github.com/mixxxdj/mixxx/issues/12044) diff --git a/CMakeLists.txt b/CMakeLists.txt index cf44b174833..2530efeff70 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,14 +54,18 @@ function(FATAL_ERROR_MISSING_ENV) else() message(FATAL_ERROR "Did you download the Mixxx build environment using `${CMAKE_SOURCE_DIR}/tools/windows_release_buildenv.bat` or `${CMAKE_SOURCE_DIR}/tools/windows_buildenv.bat`(includes Debug)?") endif() - elseif(APPLE) + elseif(APPLE AND NOT IOS) if(CMAKE_BUILD_TYPE MATCHES "Debug") message(FATAL_ERROR "Did you download the Mixxx build environment using `${CMAKE_SOURCE_DIR}/tools/macos_buildenv.sh`") else() message(FATAL_ERROR "Did you download the Mixxx build environment using `${CMAKE_SOURCE_DIR}/tools/macos_release_buildenv.sh` or `${CMAKE_SOURCE_DIR}/tools/macos_buildenv.sh`(includes Debug)?") endif() - else() + elseif(LINUX) message(FATAL_ERROR "Did you install the Debian dev packages via `${CMAKE_SOURCE_DIR}/tools/debian_buildenv.sh` or the equivalent packages using your package manager?") + elseif(DEFINED VCPKG_TARGET_TRIPLET) + message(FATAL_ERROR "You are targeting ${VCPKG_TARGET_TRIPLET}, which does not have a prebuilt environment. Please make sure that -DMIXXX_VCPKG_ROOT points to a vcpkg environment containing installed dependencies for ${VCPKG_TARGET_TRIPLET}!") + else() + message(FATAL_ERROR "You are building for an unknown platform and are missing a build environment. Please set -DVCPKG_TARGET_TRIPLET and make sure that -DMIXXX_VCPKG_ROOT points to a vcpkg environment containing installed dependencies for your target platform!") endif() endfunction() @@ -147,7 +151,22 @@ if(QML) add_compile_definitions(MIXXX_USE_QML) endif() -if(APPLE) +if(VCPKG_TARGET_TRIPLET MATCHES "^wasm(32|64)-emscripten") + message(STATUS "Targeting Emscripten (${VCPKG_TARGET_TRIPLET})") + if(DEFINED ENV{EMSDK}) + message(STATUS "Found EMSDK at $ENV{EMSDK}") + else() + message(FATAL_ERROR "Please make sure emsdk is installed and the environment variable EMSDK is set (see https://emscripten.org/docs/getting_started/downloads.html)") + endif() + if(NOT DEFINED VCPKG_CHAINLOAD_TOOLCHAIN_FILE) + set(VCPKG_CHAINLOAD_TOOLCHAIN_FILE "$ENV{EMSDK}/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake" CACHE STRING "") + endif() + # Enabling this causes Qt's FindWrapRt C++ compile check to fail as it tries + # to run `clang-scan-deps` (because we set the C++ standard to 20). Emscripten + # does not ship this binary yet, though. Potentially relevant upstream bug: + # https://github.com/emscripten-core/emscripten/issues/21042 + set(CMAKE_CXX_SCAN_FOR_MODULES OFF) +elseif(APPLE) # Check if xcode-select is installed execute_process(COMMAND xcode-select -v RESULT_VARIABLE XCODE_SELECT_RESULT @@ -443,10 +462,17 @@ elseif(GNU_GCC OR LLVM_CLANG) # The following optimisation flags makes the engine code ~3 times # faster, measured on a Atom CPU. add_compile_options( - -O3 -ffast-math -funroll-loops ) + if(EMSCRIPTEN) + # Optimize for size + speed when targeting Emscripten/WebAssembly + # This is recommended as we use asyncify: + # See https://doc.qt.io/qt-6/wasm.html#asyncify + add_compile_options(-Os) + else() + add_compile_options(-O3) + endif() # set -fomit-frame-pointer when we don't profile and are not using # Clang sanitizers. # Note: It is only included in -O on machines where it does not @@ -459,15 +485,20 @@ elseif(GNU_GCC OR LLVM_CLANG) # portable: sse2 CPU (>= Pentium 4) if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(i[3456]86|x86|x64|x86_64|AMD64)$") message(STATUS "Enabling SSE2 CPU optimizations (>= Pentium 4)") - add_compile_options(-mtune=generic) + if(NOT EMSCRIPTEN) + add_compile_options(-mtune=generic) + endif() # -mtune=generic picks the most common, but compatible options. # on arm platforms equivalent to -march=arch if(NOT CMAKE_SIZEOF_VOID_P EQUAL 8) # the sse flags are not set by default on 32 bit builds # but are not supported on arm builds - add_compile_options( - -msse2 - -mfpmath=sse) + add_compile_options(-msse2) + if(EMSCRIPTEN) + add_compile_options(-msimd128) + else() + add_compile_options(-mfpmath=sse) + endif() endif() # TODO(rryan): macOS can use SSE3, and possibly SSE 4.1 once # we require macOS 10.12. @@ -700,10 +731,14 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/controllers/controllerlearningeventfilter.cpp src/controllers/controllermanager.cpp src/controllers/controllermappinginfo.cpp + src/controllers/legacycontrollersettings.cpp + src/controllers/legacycontrollersettingslayout.cpp src/controllers/controllermappinginfoenumerator.cpp src/controllers/controllermappingtablemodel.cpp src/controllers/controlleroutputmappingtablemodel.cpp src/controllers/controlpickermenu.cpp + src/controllers/legacycontrollermappingfilehandler.cpp + src/controllers/legacycontrollermapping.cpp src/controllers/delegates/controldelegate.cpp src/controllers/delegates/midibytedelegate.cpp src/controllers/delegates/midichanneldelegate.cpp @@ -725,8 +760,6 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/controllers/midi/midimessage.cpp src/controllers/midi/midioutputhandler.cpp src/controllers/midi/midiutils.cpp - src/controllers/midi/portmidicontroller.cpp - src/controllers/midi/portmidienumerator.cpp src/controllers/scripting/colormapper.cpp src/controllers/scripting/colormapperjsproxy.cpp src/controllers/scripting/controllerscriptenginebase.cpp @@ -859,8 +892,10 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/engine/sync/internalclock.cpp src/engine/sync/synccontrol.cpp src/errordialoghandler.cpp - src/library/analysisfeature.cpp - src/library/analysislibrarytablemodel.cpp + src/library/analysis/analysisfeature.cpp + src/library/analysis/analysislibrarytablemodel.cpp + src/library/analysis/dlganalysis.cpp + src/library/analysis/dlganalysis.ui src/library/autodj/autodjfeature.cpp src/library/autodj/autodjprocessor.cpp src/library/autodj/dlgautodj.cpp @@ -874,16 +909,13 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/library/basesqltablemodel.cpp src/library/basetrackcache.cpp src/library/basetracktablemodel.cpp - src/library/bpmdelegate.cpp src/library/browse/browsefeature.cpp src/library/browse/browsetablemodel.cpp src/library/browse/browsethread.cpp src/library/browse/foldertreemodel.cpp - src/library/colordelegate.cpp src/library/columncache.cpp src/library/coverart.cpp src/library/coverartcache.cpp - src/library/coverartdelegate.cpp src/library/coverartutils.cpp src/library/dao/analysisdao.cpp src/library/dao/autodjcratesdao.cpp @@ -894,14 +926,8 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/library/dao/settingsdao.cpp src/library/dao/trackdao.cpp src/library/dao/trackschema.cpp - src/library/dlganalysis.cpp - src/library/dlganalysis.ui src/library/dlgcoverartfullsize.cpp src/library/dlgcoverartfullsize.ui - src/library/dlghidden.cpp - src/library/dlghidden.ui - src/library/dlgmissing.cpp - src/library/dlgmissing.ui src/library/dlgtagfetcher.cpp src/library/dlgtagfetcher.ui src/library/dlgtrackinfo.cpp @@ -913,7 +939,6 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/library/export/trackexportwizard.cpp src/library/export/trackexportworker.cpp src/library/externaltrackcollection.cpp - src/library/hiddentablemodel.cpp src/library/itunes/itunesdao.cpp src/library/itunes/itunesfeature.cpp src/library/itunes/itunesimporter.cpp @@ -924,17 +949,18 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/library/librarycontrol.cpp src/library/libraryfeature.cpp src/library/librarytablemodel.cpp - src/library/locationdelegate.cpp - src/library/missingtablemodel.cpp + src/library/missing_hidden/dlghidden.cpp + src/library/missing_hidden/dlghidden.ui + src/library/missing_hidden/dlgmissing.cpp + src/library/missing_hidden/dlgmissing.ui + src/library/missing_hidden/hiddentablemodel.cpp + src/library/missing_hidden/missingtablemodel.cpp src/library/mixxxlibraryfeature.cpp - src/library/multilineeditdelegate.cpp src/library/parser.cpp src/library/parsercsv.cpp src/library/parserm3u.cpp src/library/parserpls.cpp - src/library/playcountdelegate.cpp src/library/playlisttablemodel.cpp - src/library/previewbuttondelegate.cpp src/library/proxytrackmodel.cpp src/library/recording/dlgrecording.cpp src/library/recording/dlgrecording.ui @@ -953,10 +979,17 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/library/serato/seratofeature.cpp src/library/serato/seratoplaylistmodel.cpp src/library/sidebarmodel.cpp - src/library/stardelegate.cpp - src/library/stareditor.cpp src/library/starrating.cpp - src/library/tableitemdelegate.cpp + src/library/tabledelegates/bpmdelegate.cpp + src/library/tabledelegates/colordelegate.cpp + src/library/tabledelegates/coverartdelegate.cpp + src/library/tabledelegates/locationdelegate.cpp + src/library/tabledelegates/multilineeditdelegate.cpp + src/library/tabledelegates/playcountdelegate.cpp + src/library/tabledelegates/previewbuttondelegate.cpp + src/library/tabledelegates/stardelegate.cpp + src/library/tabledelegates/stareditor.cpp + src/library/tabledelegates/tableitemdelegate.cpp src/library/trackcollection.cpp src/library/trackcollectioniterator.cpp src/library/trackcollectionmanager.cpp @@ -1112,6 +1145,7 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/track/trackref.cpp src/util/battery/battery.cpp src/util/cache.cpp + src/util/clipboard.cpp src/util/cmdlineargs.cpp src/util/color/color.cpp src/util/color/colorpalette.cpp @@ -1563,6 +1597,13 @@ if(APPLE) endif() endif() +if(EMSCRIPTEN) + # We need asyncify to support asynchronous calls (e.g. QDialog::exec) + # when targeting Emscripten/WebAssembly. + # See https://doc.qt.io/qt-6/wasm.html#asyncify + target_link_options(mixxx-lib PUBLIC -sASYNCIFY) +endif() + # QML Debugging if(CMAKE_BUILD_TYPE STREQUAL "Debug") target_compile_definitions(mixxx-lib PUBLIC QT_QML_DEBUG) @@ -1789,6 +1830,14 @@ if (APPLE) endif() endif() +if(EMSCRIPTEN) + # Package resources for the web using preloading. + # This will generate a mixxx.data file containing all the resources. + # See https://emscripten.org/docs/porting/files/packaging_files.html + # TODO: Strip this down by only including what we need (i.e. no macOS/Linux packaging, ...) + target_link_options(mixxx-lib PUBLIC "--preload-file=${CMAKE_CURRENT_SOURCE_DIR}/res@/res") +endif() + if(WIN32) set(CMAKE_INSTALL_SYSTEM_RUNTIME_DESTINATION "${MIXXX_INSTALL_BINDIR}") if(MSVC AND CMAKE_BUILD_TYPE STREQUAL "Debug") @@ -2025,6 +2074,8 @@ add_executable(mixxx-test src/test/colorpalette_test.cpp src/test/configobject_test.cpp src/test/controller_mapping_validation_test.cpp + src/test/controller_mapping_settings_test.cpp + src/test/controllers/controller_columnid_regression_test.cpp src/test/controllerscriptenginelegacy_test.cpp src/test/controlobjecttest.cpp src/test/controlobjectaliastest.cpp @@ -2297,7 +2348,7 @@ option(ENGINEPRIME "Support for library export to Denon Engine Prime" ON) if(ENGINEPRIME) # libdjinterop does not currently have a stable ABI, so we fetch sources for a specific tag, build here, and link # statically. This situation should be reviewed once libdjinterop hits version 1.x. - set(LIBDJINTEROP_VERSION 0.20.1) + set(LIBDJINTEROP_VERSION 0.20.2) # Look whether an existing installation of libdjinterop matches the required version. find_package(DjInterop ${LIBDJINTEROP_VERSION} EXACT CONFIG) if(NOT DjInterop_FOUND) @@ -2333,7 +2384,7 @@ if(ENGINEPRIME) URL "https://github.com/xsco/libdjinterop/archive/refs/tags/${LIBDJINTEROP_VERSION}.tar.gz" "https://launchpad.net/~xsco/+archive/ubuntu/djinterop/+sourcefiles/libdjinterop/${LIBDJINTEROP_VERSION}-0ubuntu1/libdjinterop_${LIBDJINTEROP_VERSION}.orig.tar.gz" - URL_HASH SHA256=69bdbd0e68f12858b79795a76a6023962f93f819ca36ea56a9d4680901865d13 + URL_HASH SHA256=3024b8b49bc0bd673a7f032e7da7b73ce61144951e810683ec89650fedd45b85 DOWNLOAD_DIR "${CMAKE_CURRENT_BINARY_DIR}/downloads" DOWNLOAD_NAME "libdjinterop-${LIBDJINTEROP_VERSION}.tar.gz" INSTALL_DIR ${DJINTEROP_INSTALL_DIR} @@ -2551,7 +2602,17 @@ if(IOS) else() set(OpenGL_GL_PREFERENCE "GLVND") find_package(OpenGL REQUIRED) - target_link_libraries(mixxx-lib PRIVATE OpenGL::GL) + if(EMSCRIPTEN) + # Emscripten's FindOpenGL.cmake does not create OpenGL::GL + target_link_libraries(mixxx-lib PRIVATE ${OPENGL_gl_LIBRARY}) + target_compile_definitions(mixxx-lib PUBLIC QT_OPENGL_ES_2) + # Require WebGL 2.0 (for a WebGL-friendly subset of OpenGL ES 3.0) and + # enable full OpenGL ES 2.0 emulation as per + # https://emscripten.org/docs/porting/multimedia_and_graphics/OpenGL-support.html + target_link_options(mixxx-lib PUBLIC -sMIN_WEBGL_VERSION=2 -sMAX_WEBGL_VERSION=2 -sFULL_ES2=1) + else() + target_link_libraries(mixxx-lib PRIVATE OpenGL::GL) + endif() endif() # Ogg @@ -2574,9 +2635,17 @@ target_include_directories(mixxx-lib SYSTEM PUBLIC lib/portaudio) target_link_libraries(mixxx-lib PRIVATE PortAudioRingBuffer) # PortMidi -find_package(PortMidi REQUIRED) -target_include_directories(mixxx-lib SYSTEM PUBLIC ${PortMidi_INCLUDE_DIRS}) -target_link_libraries(mixxx-lib PRIVATE ${PortMidi_LIBRARIES}) +option(PORTMIDI "Enable the PortMidi backend for MIDI controllers" ON) +if(PORTMIDI) + target_compile_definitions(mixxx-lib PUBLIC __PORTMIDI__) + find_package(PortMidi REQUIRED) + target_include_directories(mixxx-lib SYSTEM PUBLIC ${PortMidi_INCLUDE_DIRS}) + target_link_libraries(mixxx-lib PRIVATE ${PortMidi_LIBRARIES}) + target_sources(mixxx-lib PRIVATE + src/controllers/midi/portmidicontroller.cpp + src/controllers/midi/portmidienumerator.cpp + ) +endif() # Protobuf add_subdirectory(src/proto) @@ -2605,6 +2674,7 @@ set( set(QT_EXTRA_COMPONENTS "") if(QT6) find_package(QT 6.2 NAMES Qt6 COMPONENTS Core REQUIRED) + list(APPEND QT_EXTRA_COMPONENTS "ShaderTools") list(APPEND QT_EXTRA_COMPONENTS "SvgWidgets") list(APPEND QT_EXTRA_COMPONENTS "Core5Compat") else() @@ -2639,6 +2709,8 @@ if(QT_EXTRA_COMPONENTS) endif() if(QML) + add_subdirectory(res/shaders) + set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml) qt_add_library(mixxx-qml-lib STATIC) foreach(COMPONENT ${QT_COMPONENTS}) @@ -2698,6 +2770,8 @@ if(QML) src/qml/asyncimageprovider.cpp src/qml/qmlapplication.cpp src/qml/qmlautoreload.cpp + src/qml/qmlbeatsmodel.cpp + src/qml/qmlcuesmodel.cpp src/qml/qmlcontrolproxy.cpp src/qml/qmlconfigproxy.cpp src/qml/qmldlgpreferencesproxy.cpp @@ -2739,6 +2813,13 @@ if(DEBUG_ASSERTIONS_FATAL) endif() endif() +if(EMSCRIPTEN) + option(WASM_ASSERTIONS "Enable additional checks when targeting Emscripten/WebAssembly" OFF) + if(WASM_ASSERTIONS) + target_link_options(mixxx-lib PUBLIC -sASSERTIONS) + endif() +endif() + target_compile_definitions(mixxx-lib PUBLIC QT_TABLET_SUPPORT QT_USE_QSTRINGBUILDER) is_static_library(Qt_IS_STATIC Qt${QT_VERSION_MAJOR}::Core) if(Qt_IS_STATIC) @@ -2750,10 +2831,6 @@ if(Qt_IS_STATIC) # into Qt .libs by default. target_link_libraries(mixxx-lib PRIVATE - # platform plugins - Qt${QT_VERSION_MAJOR}::QOffscreenIntegrationPlugin - Qt${QT_VERSION_MAJOR}::QMinimalIntegrationPlugin - # imageformats plugins Qt${QT_VERSION_MAJOR}::QGifPlugin Qt${QT_VERSION_MAJOR}::QICOPlugin @@ -2764,6 +2841,18 @@ if(Qt_IS_STATIC) Qt${QT_VERSION_MAJOR}::QSQLiteDriverPlugin ) + if(EMSCRIPTEN) + target_link_libraries(mixxx-lib PRIVATE + Qt${QT_VERSION_MAJOR}::QWasmIntegrationPlugin + ) + else() + target_link_libraries(mixxx-lib PRIVATE + # platform plugins + Qt${QT_VERSION_MAJOR}::QOffscreenIntegrationPlugin + Qt${QT_VERSION_MAJOR}::QMinimalIntegrationPlugin + ) + endif() + if(WIN32) target_link_libraries(mixxx-lib PRIVATE Qt${QT_VERSION_MAJOR}::QWindowsIntegrationPlugin @@ -2964,7 +3053,7 @@ if(APPLE) "-weak_framework IOKit" ) endif() -elseif(UNIX AND NOT APPLE) +elseif(UNIX AND NOT APPLE AND NOT EMSCRIPTEN) if(QT6) find_package(X11) else() @@ -3195,6 +3284,9 @@ if(BATTERY) target_sources(mixxx-lib PRIVATE src/util/battery/batterymac.cpp) endif() elseif(UNIX) + if(EMSCRIPTEN) + message(FATAL_ERROR "Battery support is not implemented for Emscripten (WebAssembly)") + endif() find_package(Upower REQUIRED) find_package(GLIB COMPONENTS gobject REQUIRED) target_include_directories(mixxx-lib SYSTEM PUBLIC ${GLIB_INCLUDE_DIRS}) diff --git a/README.md b/README.md index 78277832a91..fba0236afa8 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Have a bug or feature request? [File a bug on Github][fileabug]. Want to get involved in Mixxx development? Assign yourself a bug from the [easy bug list][easybugs] and get started! +Read [CONTRIBUTING](CONTRIBUTING.md) for more information. ## Building Mixxx diff --git a/cmake/modules/FindTagLib.cmake b/cmake/modules/FindTagLib.cmake index 63aa6d8c25e..7e907f6794b 100644 --- a/cmake/modules/FindTagLib.cmake +++ b/cmake/modules/FindTagLib.cmake @@ -48,8 +48,8 @@ include(IsStaticLibrary) find_package(PkgConfig QUIET) if(PkgConfig_FOUND) if(UNIX AND NOT APPLE) - # priorize the taglib1 package introduced in https://aur.archlinux.org/packages/taglib1 - set(ENV{PKG_CONFIG_PATH} "/usr/lib/taglib1/lib/pkgconfig/:$ENV{PKG_CONFIG_PATH}") + # priorize the taglib1 package introduced in https://www.archlinux.de/packages/extra/x86_64/taglib1 + set(ENV{PKG_CONFIG_PATH} "/usr/lib/taglib1/pkgconfig/:$ENV{PKG_CONFIG_PATH}") endif() pkg_check_modules(PC_TagLib QUIET taglib) endif() diff --git a/res/Mixxx-Keyboard-Shortcuts.pdf b/res/Mixxx-Keyboard-Shortcuts.pdf index 38277941820..312a1810125 100644 Binary files a/res/Mixxx-Keyboard-Shortcuts.pdf and b/res/Mixxx-Keyboard-Shortcuts.pdf differ diff --git a/res/Mixxx-Keyboard-Shortcuts.png b/res/Mixxx-Keyboard-Shortcuts.png deleted file mode 100644 index a757fadfb12..00000000000 Binary files a/res/Mixxx-Keyboard-Shortcuts.png and /dev/null differ diff --git a/res/controllers/Behringer-Extension-scripts.js b/res/controllers/Behringer-Extension-scripts.js index f83d6dd45d4..cc912e64d3a 100644 --- a/res/controllers/Behringer-Extension-scripts.js +++ b/res/controllers/Behringer-Extension-scripts.js @@ -239,12 +239,12 @@ isEnabled: function() { return this.id !== 0; }, start: function() { this.reset(); - this.id = engine.beginTimer(this.timeout, () => { + this.id = engine.beginTimer(this.timeout, function() { if (this.oneShot) { this.disable(); } this.action.call(this.owner); - }, this.oneShot); + }.bind(this), this.oneShot); // .bind(this) is required instead of arrow function for Qt < 6.2.4 due to QTBUG-95677 }, reset: function() { if (this.isEnabled()) { diff --git a/res/controllers/HID Keyboard.hid.xml.example b/res/controllers/HID Keyboard.hid.xml.example deleted file mode 100644 index c4be603bff7..00000000000 --- a/res/controllers/HID Keyboard.hid.xml.example +++ /dev/null @@ -1,18 +0,0 @@ - - - - Apple Bluetooth Keyboard - Ilkka Tuohela - HID example for Apple Bluetooth Keyboard - - - - - - - - - - - - diff --git a/res/controllers/HID Trackpad.hid.xml.example b/res/controllers/HID Trackpad.hid.xml.example deleted file mode 100644 index 89689f3a7d2..00000000000 --- a/res/controllers/HID Trackpad.hid.xml.example +++ /dev/null @@ -1,18 +0,0 @@ - - - - Apple Bluetooth Trackpad - Ilkka Tuohela - HID example for Apple Bluetooth Trackpad - - - - - - - - - - - - diff --git a/res/controllers/HID-Keyboard.js b/res/controllers/HID-Keyboard.js deleted file mode 100644 index 29efbc51838..00000000000 --- a/res/controllers/HID-Keyboard.js +++ /dev/null @@ -1,29 +0,0 @@ -// -// Demo script to print events from apple bluetooth keyboard on OS/X -// Copyright (C) 2012, Ilkka Tuohela -// Feel free to use whatever way wish1 -// - -HIDKeyboard = new HIDKeyboardDevice(); - -HIDKeyboard.init = function(id) { - HIDKeyboard.id = id; - HIDKeyboard.registerInputPackets(); - HIDKeyboard.registerOutputPackets(); - HIDKeyboard.registerScalers(); - HIDKeyboard.registerCallbacks(); - HIDDebug("HID Keyboard Initialized: " + HIDKeyboard.id); -} - -HIDKeyboard.shutdown = function() { - HIDDebug("HID Keyboard Shutdown: " + HIDKeyboard.id); -} - -HIDKeyboard.incomingData = function(data,length) { - var controller = HIDKeyboard.controller; - if (controller==undefined) { - HIDDebug("Error in script initialization: controller not found"); - return; - } - controller.parsePacket(data,length); -} diff --git a/res/controllers/HID-Trackpad.js b/res/controllers/HID-Trackpad.js deleted file mode 100644 index 2a0c48f0e11..00000000000 --- a/res/controllers/HID-Trackpad.js +++ /dev/null @@ -1,31 +0,0 @@ -// -// Demo script to print events from apple bluetooth trackpad on OS/X -// NOTE: the trackpad doesn't seem to actually send events to us. This -// is just a silly example anyway -// Copyright (C) 2012, Ilkka Tuohela -// Feel free to use whatever way wish1 -// - -HIDTrackpad = new HIDTrackpadDevice(); - -HIDTrackpad.init = function(id) { - HIDTrackpad.id = id; - HIDTrackpad.registerInputPackets(); - HIDTrackpad.registerOutputPackets(); - HIDTrackpad.registerScalers(); - HIDTrackpad.registerCallbacks(); - HIDDebug("HID Trackpad Initialized: " + HIDTrackpad.id); -} - -HIDTrackpad.shutdown = function() { - HIDDebug("HID Trackpad Shutdown: " + HIDTrackpad.id); -} - -HIDTrackpad.incomingData = function(data,length) { - var controller = HIDTrackpad.controller; - if (controller==undefined) { - HIDDebug("Error in script initialization: controller not found"); - return; - } - controller.parsePacket(data,length); -} diff --git a/res/controllers/Hercules_DJControl_Inpulse_200.midi.xml b/res/controllers/Hercules_DJControl_Inpulse_200.midi.xml index 5ce8c9ae3a1..b9d10873f59 100644 --- a/res/controllers/Hercules_DJControl_Inpulse_200.midi.xml +++ b/res/controllers/Hercules_DJControl_Inpulse_200.midi.xml @@ -1107,6 +1107,18 @@ + + + + [Library] + ScrollVertical + Scroll Vertical (SHIFT + Browser Knob) + 0xB3 + 0x01 + + + + diff --git a/res/controllers/Pioneer-DDJ-FLX4-script.js b/res/controllers/Pioneer-DDJ-FLX4-script.js index 3514f52421e..3c1db2b4c97 100644 --- a/res/controllers/Pioneer-DDJ-FLX4-script.js +++ b/res/controllers/Pioneer-DDJ-FLX4-script.js @@ -276,6 +276,18 @@ PioneerDDJFLX4.init = function() { PioneerDDJFLX4.sendKeepAlive(); // the query seems to double as a keep alive message }; +// +// Waveform zoom +// + +PioneerDDJFLX4.waveformZoom = function(midichan, control, value, status, group) { + if (value === 0x7f) { + script.triggerControl(group, "waveform_zoom_up", 100); + } else { + script.triggerControl(group, "waveform_zoom_down", 100); + } +}; + // // Channel level lights // diff --git a/res/controllers/Pioneer-DDJ-FLX4.midi.xml b/res/controllers/Pioneer-DDJ-FLX4.midi.xml index 1e99c3d0cbd..12875bbd5a4 100644 --- a/res/controllers/Pioneer-DDJ-FLX4.midi.xml +++ b/res/controllers/Pioneer-DDJ-FLX4.midi.xml @@ -25,6 +25,16 @@ + + BROWSE +SHIFT - Zoom waveform + [Channel1] + PioneerDDJFLX4.waveformZoom + 0xB6 + 0x64 + + + + BROWSE - press - Move cursor between track list and tree view [Library] diff --git a/res/controllers/Traktor Kontrol S4 MK3.hid.xml b/res/controllers/Traktor Kontrol S4 MK3.hid.xml index 02b9b5b9363..0c6f835c972 100644 --- a/res/controllers/Traktor Kontrol S4 MK3.hid.xml +++ b/res/controllers/Traktor Kontrol S4 MK3.hid.xml @@ -9,6 +9,620 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/controllers/Traktor-Kontrol-F1-scripts.js b/res/controllers/Traktor-Kontrol-F1-scripts.js index c02d2ca2172..7c004cf3b4d 100644 --- a/res/controllers/Traktor-Kontrol-F1-scripts.js +++ b/res/controllers/Traktor-Kontrol-F1-scripts.js @@ -75,94 +75,94 @@ function KontrolF1Controller() { this.registerOutputPackets = function() { var packet = new HIDPacket("lights", 0x80); // Right 7-segment element - 0x0 off, 0x40 on - packet.addControl("hid", "right_segment_dp", 0,"B"); - packet.addControl("hid", "right_segment_1", 1,"B"); - packet.addControl("hid", "right_segment_2", 2,"B"); - packet.addControl("hid", "right_segment_3", 3,"B"); - packet.addControl("hid", "right_segment_4", 4,"B"); - packet.addControl("hid", "right_segment_5", 5,"B"); - packet.addControl("hid", "right_segment_6", 6,"B"); - packet.addControl("hid", "right_segment_7", 7,"B"); + packet.addOutput("hid", "right_segment_dp", 1, "B"); + packet.addOutput("hid", "right_segment_1", 2, "B"); + packet.addOutput("hid", "right_segment_2", 3, "B"); + packet.addOutput("hid", "right_segment_3", 4, "B"); + packet.addOutput("hid", "right_segment_4", 5, "B"); + packet.addOutput("hid", "right_segment_5", 6, "B"); + packet.addOutput("hid", "right_segment_6", 7, "B"); + packet.addOutput("hid", "right_segment_7", 8, "B"); // Left 7-segment element - 0x0 off, 0x40 on - packet.addControl("hid", "left_segment_dp", 8,"B"); - packet.addControl("hid", "left_segment_1", 9,"B"); - packet.addControl("hid", "left_segment_2", 10,"B"); - packet.addControl("hid", "left_segment_3", 11,"B"); - packet.addControl("hid", "left_segment_4", 12,"B"); - packet.addControl("hid", "left_segment_5", 13,"B"); - packet.addControl("hid", "left_segment_6", 14,"B"); - packet.addControl("hid", "left_segment_7", 15,"B"); + packet.addOutput("hid", "left_segment_dp", 9, "B"); + packet.addOutput("hid", "left_segment_1", 10, "B"); + packet.addOutput("hid", "left_segment_2", 11, "B"); + packet.addOutput("hid", "left_segment_3", 12, "B"); + packet.addOutput("hid", "left_segment_4", 13, "B"); + packet.addOutput("hid", "left_segment_5", 14, "B"); + packet.addOutput("hid", "left_segment_6", 15, "B"); + packet.addOutput("hid", "left_segment_7", 16, "B"); // Button led brightness, 0-0xff - packet.addControl("hid", "browse_brightness", 16,"B"); - packet.addControl("hid", "size_brightness", 17,"B"); - packet.addControl("hid", "type_brightness", 18,"B"); - packet.addControl("hid", "reverse_brightness", 19,"B"); - packet.addControl("hid", "shift_brightness", 20,"B"); - packet.addControl("hid", "capture_brightness", 21,"B"); - packet.addControl("hid", "quant_brightness", 22,"B"); - packet.addControl("hid", "sync_brightness", 23,"B"); + packet.addOutput("hid", "browse_brightness", 17, "B"); + packet.addOutput("hid", "size_brightness", 18, "B"); + packet.addOutput("hid", "type_brightness", 19, "B"); + packet.addOutput("hid", "reverse_brightness", 20, "B"); + packet.addOutput("hid", "shift_brightness", 21, "B"); + packet.addOutput("hid", "capture_brightness", 22, "B"); + packet.addOutput("hid", "quant_brightness", 23, "B"); + packet.addOutput("hid", "sync_brightness", 24, "B"); // Pad RGB color button controls, 3 bytes per pad - packet.addControl("hid", "grid_1_blue", 24,"B") - packet.addControl("hid", "grid_1_red", 25,"B") - packet.addControl("hid", "grid_1_green", 26,"B") - packet.addControl("hid", "grid_2_blue", 27,"B") - packet.addControl("hid", "grid_2_red", 28,"B") - packet.addControl("hid", "grid_2_green", 29,"B") - packet.addControl("hid", "grid_3_blue", 30,"B") - packet.addControl("hid", "grid_3_red", 31,"B") - packet.addControl("hid", "grid_3_green", 32,"B") - packet.addControl("hid", "grid_4_blue", 33,"B") - packet.addControl("hid", "grid_4_red", 34,"B") - packet.addControl("hid", "grid_4_green", 35,"B") - packet.addControl("hid", "grid_5_blue", 36,"B") - packet.addControl("hid", "grid_5_red", 37,"B") - packet.addControl("hid", "grid_5_green", 38,"B") - packet.addControl("hid", "grid_6_blue", 39,"B") - packet.addControl("hid", "grid_6_red", 40,"B") - packet.addControl("hid", "grid_6_green", 41,"B") - packet.addControl("hid", "grid_7_blue", 42,"B") - packet.addControl("hid", "grid_7_red", 43,"B") - packet.addControl("hid", "grid_7_green", 44,"B") - packet.addControl("hid", "grid_8_blue", 45,"B") - packet.addControl("hid", "grid_8_red", 46,"B") - packet.addControl("hid", "grid_8_green", 47,"B") - packet.addControl("hid", "grid_9_blue", 48,"B") - packet.addControl("hid", "grid_9_red", 49,"B") - packet.addControl("hid", "grid_9_green", 50,"B") - packet.addControl("hid", "grid_10_blue", 51,"B") - packet.addControl("hid", "grid_10_red", 52,"B") - packet.addControl("hid", "grid_10_green", 53,"B") - packet.addControl("hid", "grid_11_blue", 54,"B") - packet.addControl("hid", "grid_11_red", 55,"B") - packet.addControl("hid", "grid_11_green", 56,"B") - packet.addControl("hid", "grid_12_blue", 57,"B") - packet.addControl("hid", "grid_12_red", 58,"B") - packet.addControl("hid", "grid_12_green", 59,"B") - packet.addControl("hid", "grid_13_blue", 60,"B") - packet.addControl("hid", "grid_13_red", 61,"B") - packet.addControl("hid", "grid_13_green", 62,"B") - packet.addControl("hid", "grid_14_blue", 63,"B") - packet.addControl("hid", "grid_14_red", 64,"B") - packet.addControl("hid", "grid_14_green", 65,"B") - packet.addControl("hid", "grid_15_blue", 66,"B") - packet.addControl("hid", "grid_15_red", 67,"B") - packet.addControl("hid", "grid_15_green", 68,"B") - packet.addControl("hid", "grid_16_blue", 69,"B") - packet.addControl("hid", "grid_16_red", 70,"B") - packet.addControl("hid", "grid_16_green", 71,"B") + packet.addOutput("hid", "grid_1_blue", 25, "B"); + packet.addOutput("hid", "grid_1_red", 26, "B"); + packet.addOutput("hid", "grid_1_green", 27, "B"); + packet.addOutput("hid", "grid_2_blue", 28, "B"); + packet.addOutput("hid", "grid_2_red", 29, "B"); + packet.addOutput("hid", "grid_2_green", 30, "B"); + packet.addOutput("hid", "grid_3_blue", 31, "B"); + packet.addOutput("hid", "grid_3_red", 32, "B"); + packet.addOutput("hid", "grid_3_green", 33, "B"); + packet.addOutput("hid", "grid_4_blue", 34, "B"); + packet.addOutput("hid", "grid_4_red", 35, "B"); + packet.addOutput("hid", "grid_4_green", 36, "B"); + packet.addOutput("hid", "grid_5_blue", 37, "B"); + packet.addOutput("hid", "grid_5_red", 38, "B"); + packet.addOutput("hid", "grid_5_green", 39, "B"); + packet.addOutput("hid", "grid_6_blue", 40, "B"); + packet.addOutput("hid", "grid_6_red", 41, "B"); + packet.addOutput("hid", "grid_6_green", 42, "B"); + packet.addOutput("hid", "grid_7_blue", 43, "B"); + packet.addOutput("hid", "grid_7_red", 44, "B"); + packet.addOutput("hid", "grid_7_green", 45, "B"); + packet.addOutput("hid", "grid_8_blue", 46, "B"); + packet.addOutput("hid", "grid_8_red", 47, "B"); + packet.addOutput("hid", "grid_8_green", 48, "B"); + packet.addOutput("hid", "grid_9_blue", 49, "B"); + packet.addOutput("hid", "grid_9_red", 50, "B"); + packet.addOutput("hid", "grid_9_green", 51, "B"); + packet.addOutput("hid", "grid_10_blue", 52, "B"); + packet.addOutput("hid", "grid_10_red", 53, "B"); + packet.addOutput("hid", "grid_10_green", 54, "B"); + packet.addOutput("hid", "grid_11_blue", 55, "B"); + packet.addOutput("hid", "grid_11_red", 56, "B"); + packet.addOutput("hid", "grid_11_green", 57, "B"); + packet.addOutput("hid", "grid_12_blue", 58, "B"); + packet.addOutput("hid", "grid_12_red", 59, "B"); + packet.addOutput("hid", "grid_12_green", 60, "B"); + packet.addOutput("hid", "grid_13_blue", 61, "B"); + packet.addOutput("hid", "grid_13_red", 62, "B"); + packet.addOutput("hid", "grid_13_green", 63, "B"); + packet.addOutput("hid", "grid_14_blue", 64, "B"); + packet.addOutput("hid", "grid_14_red", 65, "B"); + packet.addOutput("hid", "grid_14_green", 66, "B"); + packet.addOutput("hid", "grid_15_blue", 67, "B"); + packet.addOutput("hid", "grid_15_red", 68, "B"); + packet.addOutput("hid", "grid_15_green", 69, "B"); + packet.addOutput("hid", "grid_16_blue", 70, "B"); + packet.addOutput("hid", "grid_16_red", 71, "B"); + packet.addOutput("hid", "grid_16_green", 72, "B"); // Play key brightness control, 0-0xff - packet.addControl("hid", "play_4_1_brightness", 72,"B"); - packet.addControl("hid", "play_4_2_brightness", 73,"B"); - packet.addControl("hid", "play_3_1_brightness", 74,"B"); - packet.addControl("hid", "play_3_2_brightness", 75,"B"); - packet.addControl("hid", "play_2_1_brightness", 76,"B"); - packet.addControl("hid", "play_2_2_brightness", 77,"B"); - packet.addControl("hid", "play_1_1_brightness", 78,"B"); - packet.addControl("hid", "play_1_2_brightness", 79,"B"); + packet.addOutput("hid", "play_4_1_brightness", 73, "B"); + packet.addOutput("hid", "play_4_2_brightness", 74, "B"); + packet.addOutput("hid", "play_3_1_brightness", 75, "B"); + packet.addOutput("hid", "play_3_2_brightness", 76, "B"); + packet.addOutput("hid", "play_2_1_brightness", 77, "B"); + packet.addOutput("hid", "play_2_2_brightness", 78, "B"); + packet.addOutput("hid", "play_1_1_brightness", 79, "B"); + packet.addOutput("hid", "play_1_2_brightness", 80, "B"); this.controller.registerOutputPacket(packet); diff --git a/res/controllers/Traktor-Kontrol-S4-MK3.js b/res/controllers/Traktor-Kontrol-S4-MK3.js index 857ff9ff35d..7f167085065 100644 --- a/res/controllers/Traktor-Kontrol-S4-MK3.js +++ b/res/controllers/Traktor-Kontrol-S4-MK3.js @@ -40,95 +40,102 @@ const KeyboardColors = [ /* * USER CONFIGURABLE SETTINGS - * Adjust these to your liking + * Change settings in the preferences */ const DeckColors = [ - LedColors.red, - LedColors.blue, - LedColors.yellow, - LedColors.purple, + LedColors[engine.getSetting("deckA")] || LedColors.red, + LedColors[engine.getSetting("deckB")] || LedColors.blue, + LedColors[engine.getSetting("deckC")] || LedColors.yellow, + LedColors[engine.getSetting("deckD")] || LedColors.purple, ]; const LibrarySortableColumns = [ - script.LIBRARY_COLUMNS.ARTIST, - script.LIBRARY_COLUMNS.TITLE, - script.LIBRARY_COLUMNS.BPM, - script.LIBRARY_COLUMNS.KEY, - script.LIBRARY_COLUMNS.DATETIME_ADDED, -]; + engine.getSetting("librarySortableColumns1Value"), + engine.getSetting("librarySortableColumns2Value"), + engine.getSetting("librarySortableColumns3Value"), + engine.getSetting("librarySortableColumns4Value"), + engine.getSetting("librarySortableColumns5Value"), + engine.getSetting("librarySortableColumns6Value"), +].map(c => parseInt(c)).filter(c => c); // Filter '0' column, equivalent to '---' value in the UI or disabled -const LoopWheelMoveFactor = 50; -const LoopEncoderMoveFactor = 500; -const LoopEncoderShiftmoveFactor = 2500; +const LoopWheelMoveFactor = engine.getSetting("loopWheelMoveFactor") || 50; +const LoopEncoderMoveFactor = engine.getSetting("loopEncoderMoveFactor") || 500; +const LoopEncoderShiftMoveFactor = engine.getSetting("loopEncoderShiftMoveFactor") || 2500; -const TempoFaderSoftTakeoverColorLow = LedColors.white; -const TempoFaderSoftTakeoverColorHigh = LedColors.green; +const TempoFaderSoftTakeoverColorLow = LedColors[engine.getSetting("tempoFaderSoftTakeoverColorLow")] || LedColors.white; +const TempoFaderSoftTakeoverColorHigh = LedColors[engine.getSetting("tempoFaderSoftTakeoverColorHigh")] || LedColors.green; // Define whether or not to keep LED that have only one color (reverse, flux, play, shift) dimmed if they are inactive. // 'true' will keep them dimmed, 'false' will turn them off. Default: true -const KeepLEDWithOneColorDimedWhenInactive = true; +const InactiveLightsAlwaysBacklit = !!engine.getSetting("inactiveLightsAlwaysBacklit"); // Keep both deck select buttons backlit and do not fully turn off the inactive deck button. -// 'true' will keep the unseclected deck dimmed, 'false' to fully turn it off. Default: true -const KeepDeckSelectDimmed = true; +// 'true' will keep the unselected deck dimmed, 'false' to fully turn it off. Default: true +const DeckSelectAlwaysBacklit = !!engine.getSetting("deckSelectAlwaysBacklit"); // Define whether the keylock is mapped when doing "shift+master" (on press) or "shift+sync" (on release since long push copies the key)". // 'true' will use "sync+master", 'false' will use "shift+sync". Default: false -const UseKeylockOnMaster = false; +const UseKeylockOnMaster = !!engine.getSetting("useKeylockOnMaster"); -// Define whether the grid button would blink when the playback is going over a detcted beat. Can help to adjust beat grid. +// Define whether the grid button would blink when the playback is going over a detected beat. Can help to adjust beat grid. // Default: false -const GridButtonBlinkOverBeat = false; +const GridButtonBlinkOverBeat = !!engine.getSetting("gridButtonBlinkOverBeat"); // Wheel led blinking if reaching the end of track warning (default 30 seconds, can be changed in the settings, under "Waveforms" > "End of track warning"). // Default: true -const WheelLedBlinkOnTrackEnd = true; +const WheelLedBlinkOnTrackEnd = !!engine.getSetting("wheelLedBlinkOnTrackEnd"); // When shifting either decks, the mixer will control microphones or auxiliary lines. If there is both a mic and an configure on the same channel, the mixer will control the auxiliary. // Default: false -const MixerControlsMixAuxOnShift = false; +const MixerControlsMixAuxOnShift = !!engine.getSetting("mixerControlsMicAuxOnShift"); // Define how many wheel moves are sampled to compute the speed. The more you have, the more the speed is accurate, but the // less responsive it gets in Mixxx. Default: 5 -const WheelSpeedSample = 3; +const WheelSpeedSample = engine.getSetting("wheelSpeedSample") || 5; // Make the sampler tab a beatlooproll tab instead // Default: false -const UseBeatloopRollInsteadOfSampler = false; +const UseBeatloopRollInsteadOfSampler = !!engine.getSetting("useBeatloopRollInsteadOfSampler"); // Predefined beatlooproll sizes. Note that if you use AddLoopHalveAndDoubleOnBeatloopRollTab, the first and // last size will be ignored -const BeatLoopRolls = [1/16, 1/8, 1/4, 1/2, 1, 2, 4, 8]; +const BeatLoopRolls = [ + engine.getSetting("beatLoopRollsSize1") || 1/8, + engine.getSetting("beatLoopRollsSize2") || 1/4, + engine.getSetting("beatLoopRollsSize3") || 1/2, + engine.getSetting("beatLoopRollsSize4") || 1, + engine.getSetting("beatLoopRollsSize5") || 2, + engine.getSetting("beatLoopRollsSize6") || 4, + engine.getSetting("beatLoopRollsSize7") || "half", + engine.getSetting("beatLoopRollsSize8") || "double" +]; -// Make the two last button on the beatlooproll pad halve or double the loop size. This will take away the 1/16 and 8 loop size. -// Default: true -const AddLoopHalveAndDoubleOnBeatloopRollTab = true; // Define the speed of the jogwheel. This will impact the speed of the LED playback indicator, the sratch, and the speed of // the motor if enable. Recommended value are 33 + 1/3 or 45. // Default: 33 + 1/3 -const BaseRevolutionsPerMinute = 33 + 1/3; +const BaseRevolutionsPerMinute = engine.getSetting("baseRevolutionsPerMinute") || 33 + 1/3; // Define whether or not to use motors. // This is a BETA feature! Please use at your own risk. Setting this off means that below settings are inactive // Default: false -const UseMotors = false; +const UseMotors = !!engine.getSetting("useMotors"); // Define how many wheel moves are sampled to compute the speed when using the motor. This is helpful to mitigate delay that // occurs in communication as well as Mixxx limitation to 20ms latency. // The more you have, the more the speed is accurate. // less responsive it gets in Mixxx. Default: 20 -const TurnTableSpeedSample = 20; +const TurnTableSpeedSample = engine.getSetting("turnTableSpeedSample") || 20; // Define how much the wheel will resist. It is a similar setting that the Grid+Wheel in Tracktor // Value must defined between 0 to 1. 0 is very tight, 1 is very loose. // Default: 0.5 -const TightnessFactor = 0.5; +const TightnessFactor = engine.getSetting("tightnessFactor") || 0.5; // Define how much force can the motor use. This defines how much the wheel will "fight" you when you block it in TT mode // This will also affect how quick the wheel starts spinning when enabling motor mode, or starting a deck with motor mode on -const MaxWheelForce = 25000; // Traktor seems to cap the max value at 60000, which just sounds insane +const MaxWheelForce = engine.getSetting("maxWheelForce") || 25000; // Traktor seems to cap the max value at 60000, which just sounds insane @@ -699,7 +706,7 @@ class HotcueButton extends PushButton { if (this.number === undefined || !Number.isInteger(this.number) || this.number < 1 || this.number > 32) { throw Error("HotcueButton must have a number property of an integer between 1 and 32"); } - this.outKey = `hotcue_${this.number}_enabled`; + this.outKey = `hotcue_${this.number}_status`; this.colorKey = `hotcue_${this.number}_color`; this.outConnect(); } @@ -815,8 +822,16 @@ class BeatLoopRollButton extends TriggerButton { if (options.number === undefined || !Number.isInteger(options.number) || options.number < 0 || options.number > 7) { throw Error("BeatLoopRollButton must have a number property of an integer between 0 and 7"); } - if (options.number <= 5 || !AddLoopHalveAndDoubleOnBeatloopRollTab) { - options.key = "beatlooproll_"+BeatLoopRolls[AddLoopHalveAndDoubleOnBeatloopRollTab ? options.number + 1 : options.number]+"_activate"; + if (BeatLoopRolls[options.number] === "half") { + options.key = "loop_halve"; + } else if (BeatLoopRolls[options.number] === "double") { + options.key = "loop_double"; + } else { + const size = parseFloat(BeatLoopRolls[options.number]); + if (isNaN(size)) { + throw Error(`BeatLoopRollButton ${options.number}'s size "${BeatLoopRolls[options.number]}" is invalid. Must be a float, or the literal 'half' or 'double'`); + } + options.key = `beatlooproll_${size}_activate`; options.onShortPress = function() { if (!this.deck.beatloopSize) { this.deck.beatloopSize = engine.getValue(this.group, "beatloop_size"); @@ -830,10 +845,6 @@ class BeatLoopRollButton extends TriggerButton { this.deck.beatloopSize = undefined; } }; - } else if (options.number === 6) { - options.key = "loop_halve"; - } else { - options.key = "loop_double"; } super(options); if (this.deck === undefined) { @@ -843,7 +854,7 @@ class BeatLoopRollButton extends TriggerButton { this.outConnect(); } output(value) { - if (this.number <= 5 || !AddLoopHalveAndDoubleOnBeatloopRollTab) { + if (this.key.startsWith("beatlooproll_")) { this.send(LedColors.white + (value ? this.brightnessOn : this.brightnessOff)); } else { this.send(this.color); @@ -1522,7 +1533,7 @@ class S4Mk3Deck extends Deck { super(decks, colors); this.playButton = new PlayButton({ - output: KeepLEDWithOneColorDimedWhenInactive ? undefined : Button.prototype.uncoloredOutput + output: InactiveLightsAlwaysBacklit ? undefined : Button.prototype.uncoloredOutput }); this.cueButton = new CueButton({ @@ -1624,7 +1635,7 @@ class S4Mk3Deck extends Deck { shift: function() { this.setKey("loop_enabled"); }, - output: KeepLEDWithOneColorDimedWhenInactive ? undefined : Button.prototype.uncoloredOutput, + output: InactiveLightsAlwaysBacklit ? undefined : Button.prototype.uncoloredOutput, onShortRelease: function() { if (!this.shifted) { engine.setValue(this.group, this.key, false); @@ -1708,7 +1719,7 @@ class S4Mk3Deck extends Deck { } } }, - output: KeepLEDWithOneColorDimedWhenInactive ? undefined : Button.prototype.uncoloredOutput, + output: InactiveLightsAlwaysBacklit ? undefined : Button.prototype.uncoloredOutput, onShortRelease: function() { if (!this.shifted) { engine.setValue(this.group, this.key, false); @@ -1820,7 +1831,7 @@ class S4Mk3Deck extends Deck { this.deck.switchDeck(Deck.groupForNumber(decks[0])); this.outReport.data[io.deckButtonOutputByteOffset] = colors[0] + this.brightnessOn; // turn off the other deck selection button's LED - this.outReport.data[io.deckButtonOutputByteOffset + 1] = KeepDeckSelectDimmed ? colors[1] + this.brightnessOff : 0; + this.outReport.data[io.deckButtonOutputByteOffset + 1] = DeckSelectAlwaysBacklit ? colors[1] + this.brightnessOff : 0; this.outReport.send(); } }, @@ -1831,7 +1842,7 @@ class S4Mk3Deck extends Deck { if (value) { this.deck.switchDeck(Deck.groupForNumber(decks[1])); // turn off the other deck selection button's LED - this.outReport.data[io.deckButtonOutputByteOffset] = KeepDeckSelectDimmed ? colors[0] + this.brightnessOff : 0; + this.outReport.data[io.deckButtonOutputByteOffset] = DeckSelectAlwaysBacklit ? colors[0] + this.brightnessOff : 0; this.outReport.data[io.deckButtonOutputByteOffset + 1] = colors[1] + this.brightnessOn; this.outReport.send(); } @@ -1840,12 +1851,12 @@ class S4Mk3Deck extends Deck { // set deck selection button LEDs outReport.data[io.deckButtonOutputByteOffset] = colors[0] + Button.prototype.brightnessOn; - outReport.data[io.deckButtonOutputByteOffset + 1] = KeepDeckSelectDimmed ? colors[1] + Button.prototype.brightnessOff : 0; + outReport.data[io.deckButtonOutputByteOffset + 1] = DeckSelectAlwaysBacklit ? colors[1] + Button.prototype.brightnessOff : 0; outReport.send(); this.shiftButton = new PushButton({ deck: this, - output: KeepLEDWithOneColorDimedWhenInactive ? undefined : Button.prototype.uncoloredOutput, + output: InactiveLightsAlwaysBacklit ? undefined : Button.prototype.uncoloredOutput, unshift: function() { this.output(false); }, @@ -1924,7 +1935,7 @@ class S4Mk3Deck extends Deck { deck: this, onChange: function(right) { if (this.deck.wheelMode === wheelModes.loopIn || this.deck.wheelMode === wheelModes.loopOut) { - const moveFactor = this.shifted ? LoopEncoderShiftmoveFactor : LoopEncoderMoveFactor; + const moveFactor = this.shifted ? LoopEncoderShiftMoveFactor : LoopEncoderMoveFactor; const valueIn = engine.getValue(this.group, "loop_start_position") + (right ? moveFactor : -moveFactor); const valueOut = engine.getValue(this.group, "loop_end_position") + (right ? moveFactor : -moveFactor); engine.setValue(this.group, "loop_start_position", valueIn); diff --git a/res/controllers/common-hid-devices.js b/res/controllers/common-hid-devices.js deleted file mode 100644 index 6d4e917bf2b..00000000000 --- a/res/controllers/common-hid-devices.js +++ /dev/null @@ -1,76 +0,0 @@ -// Generic HID trackpad implementation -HIDTrackpadDevice = function() { - this.controller = new HIDController() - - this.registerInputPackets = function() { - // Example how to register a callback directly on a packet - packet = new HIDPacket("control", 0, this.mouseInput) - packet.addControl("hid", "byte_1", 3, "B") - packet.addControl("hid", "byte_2", 4, "B") - packet.addControl("hid", "byte_3", 5, "B") - packet.addControl("hid", "byte_4", 6, "B") - this.controller.registerInputPacket(packet) - } - - // HID trackpads have no output controls - this.registerOutputPackets = function() { } - - // No need to do scaling for keyboard presses - this.registerScalers = function() { } - - // No need for callbacks here, we bound whole input packet to mouseInput - this.registerCallbacks = function() { } - - // Example to process the mouse input packet all yourself - this.mouseInput = function(packet, delta) { - HIDDebug("Trackpad INPUT " + packet.name) - if (!delta.length) { - HIDDebug("No changed data received in HID packet") - return - } - for (var field_name in delta) { - var field = delta[field_name] - HIDDebug("FIELD " + field.id + " VALUE " + value) - } - } -} - -// Generic HID keyboard implementation -HIDKeyboardDevice = function() { - this.controller = new HIDController() - - this.registerInputPackets = function() { - packet = new HIDPacket("control", 0x1) - packet.addControl("hid", "keycode_1", 3, "B") - packet.addControl("hid", "keycode_2", 4, "B") - packet.addControl("hid", "keycode_3", 5, "B") - packet.addControl("hid", "keycode_4", 6, "B") - packet.addControl("hid", "keycode_5", 7, "B") - packet.addControl("hid", "keycode_6", 8, "B") - this.controller.registerInputPacket(packet) - } - - // HID keyboards have no output controls - this.registerOutputPackets = function() { } - - // No need to do scaling for keyboard presses - this.registerScalers = function() { } - - // Example to bind the bytes to a callback - this.registerCallbacks = function() { - this.controller.setCallback("control", "hid", "keycode_1", this.keyPress) - this.controller.setCallback("control", "hid", "keycode_2", this.keyPress) - this.controller.setCallback("control", "hid", "keycode_3", this.keyPress) - this.controller.setCallback("control", "hid", "keycode_4", this.keyPress) - this.controller.setCallback("control", "hid", "keycode_5", this.keyPress) - this.controller.setCallback("control", "hid", "keycode_6", this.keyPress) - } - - // Example to do something with the keycodes received - this.keyPress = function(field) { - if (field.value != 0) - HIDDebug("KEY PRESS " + field.id + " CODE " + field.value) - else - HIDDebug("KEY RELEASE " + field.id) - } -} diff --git a/res/controllers/common-hid-packet-parser.js b/res/controllers/common-hid-packet-parser.js index 2b66d00fade..5547af90050 100644 --- a/res/controllers/common-hid-packet-parser.js +++ b/res/controllers/common-hid-packet-parser.js @@ -696,14 +696,14 @@ class HIDPacket { field.auto_repeat = undefined; field.auto_repeat_interval = undefined; - const packet_max_value = Math.pow(2, this.packSizes[field.pack] * 8); + const packet_max_value = Math.pow(2, this.packSizes[field.pack] * 8) - 1; const signed = this.signedPackFormats.includes(field.pack); if (signed) { - field.min = 0 - (packet_max_value / 2) + 1; - field.max = (packet_max_value / 2) - 1; + field.min = 0 - ((packet_max_value + 1) / 2) + 1; + field.max = ((packet_max_value + 1) / 2) - 1; } else { field.min = 0; - field.max = packet_max_value - 1; + field.max = packet_max_value; } if (bitmask === undefined || bitmask === packet_max_value) { @@ -1679,7 +1679,7 @@ class HIDController { */ processIncomingPacket(packet, delta) { - HIDController.fastForIn(delta, (field_name) => { + HIDController.fastForIn(delta, (function(field_name) { // @ts-ignore ignoredControlChanges should be defined in the users mapping // see EKS-Otus.js for an example if (this.ignoredControlChanges !== undefined && @@ -1697,8 +1697,7 @@ class HIDController { } else { console.warn(`HIDController.processIncomingPacket - Unknown field ${field.name} type ${field.type}`); } - } - ); + }).bind(this)); // Qt < 6.2.4 : .bind(this) needed because of QTBUG-95677 } /** * Get active group for this field diff --git a/res/controllers/engine-api.d.ts b/res/controllers/engine-api.d.ts index 4a2b935cd29..e40f67db0d2 100644 --- a/res/controllers/engine-api.d.ts +++ b/res/controllers/engine-api.d.ts @@ -24,6 +24,16 @@ declare interface ScriptConnection { /** ControllerScriptInterfaceLegacy */ declare namespace engine { + type SettingValue = string | number | boolean; + /** + * Gets the value of a controller setting + * The value is either set in the preferences dialog, + * or got restored from file. + * @param name Name of the setting (as specified in the XML file of the mapping) + * @returns Value of the setting, or undefined in failure case + */ + function getSetting(name: string): SettingValue | undefined; + /** * Gets the control value * diff --git a/res/images/templates/ic_template_keyboard_mapping_sheet.svg b/res/images/templates/ic_template_keyboard_mapping_sheet.svg index 948f5dfa139..8d3316c89b1 100644 --- a/res/images/templates/ic_template_keyboard_mapping_sheet.svg +++ b/res/images/templates/ic_template_keyboard_mapping_sheet.svg @@ -7,7 +7,7 @@ id="svg1180" sodipodi:docname="ic_template_keyboard_mapping_sheet.svg" inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)" - inkscape:export-filename="../../Mixxx-Keyboard-Shortcuts.png" + inkscape:export-filename="../../Mixxx-Keyboard-Shortcuts.pdf" inkscape:export-xdpi="148.322" inkscape:export-ydpi="148.322" xml:space="preserve" @@ -28,14 +28,14 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:zoom="4.7711688" - inkscape:cx="514.23459" - inkscape:cy="159.18531" + inkscape:cx="366.89123" + inkscape:cy="166.52104" inkscape:window-width="1920" inkscape:window-height="1056" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" - inkscape:current-layer="layer2" + inkscape:current-layer="layer1" showguides="true" />Mixxx Keyboard mapping sheet templateEach control inside Mixxx is identified by a unique string. These strings are used in the keyboard mappings, the MIDI mappings, and inside Mixxx to gain access to the controls. The following is a list of controls that can be used in any of the above contexts. * for de-de keyboard layout - für Deutsche Tastatur * for en-us keyboard layout * for en-us keyboard layoutMixxx Keyboard mapping sheet templateAutoDJAutoDJdouble + id="tspan4">double clickclickstopload+ playstopload+ - +

Features

    -
  • - Sync Lock: End of track checking is not needed - #11035 -
  • BaseExternalLibraryFeature: Add ability to import external playlists as crates #11852 @@ -143,15 +139,45 @@ Fullscreen toggle rework #11566
  • +
  • + Playlists: Update of playlist labels after adding tracks + #12866 + #12761 +
  • +
  • + Tracks: Custom text color for played tracks (qss) + #12744 + #5911 + #12912 +
  • +
  • + History: Show track count and duration in sidebar + #12811 + #12788 +
  • +
  • + fixes around cratetablemodel, remove tracks + don't allow pasting tracks into locked playlists/crates or History + #12926 +
  • +
  • + Tootips: Improve + rate_up/down + tooltips, pitch vs. speed + #12590 +
  • +
  • + Shortkeys for track list management + #12020 +
  • +
  • + Track menu: Rephrase "Reset" to "Clear" + #12955 +

Controller Mappings

    -
  • - Traktor Kontrol S2 MK3: Add colored hotcues - #4637 -
  • Pioneer DDJ-FLX4: mapping improvements #12842 @@ -169,6 +195,10 @@ Add control for showing a deck's track menu #10825
  • +
  • + Removed old examples HID keyboard and HID trackpad + #12977 +

Experimental QML Skin @@ -207,6 +237,14 @@ #12794 #12536 +

  • + Install qml module on Windows + #12604 +
  • +
  • + Add scrolling waveforms + #3967 +
  • Update to Qt6 @@ -300,6 +338,10 @@ Revert "BaseTrackPlayer: Remove references to WaveformWidgetRenderer when using Qt6" #12342 +

  • + Fix Tango waveform splitter + #12939 +
  • Experimental iOs support @@ -370,6 +412,62 @@ #12663 +

    + Experimental WebAssembly support +

    +
      +
    • + CMakeLists: Add support for targeting Emscripten/WebAssembly + #12918 +
    • +
    • + CMakeLists: Emit better errors for exotic target platforms + #12910 +
    • +
    • + Build: Add + PORTMIDI + flag for compiling with(out) PortMidi + #12913 +
    • +
    • + DesktopHelper: Compile out process-spawning on WASM too + #12916 +
    • +
    • + MixxxApplication: Use + QWasmIntegrationPlugin + when targeting WebAssembly + #12915 +
    • +
    • + CMakeLists: Enable asyncify when targeting WASM + #12921 +
    • +
    • + Resources: Bundle resources for preloading when targeting Emscripten/WASM + #12922 +
    • +
    • + CMakeLists: Add + WASM_ASSERTIONS + option + #12931 +
    • +
    • + VersionStore: Recognize Emscripten/WebAssembly + #12940 +
    • +
    • + OpenGLWindow: Fix sizing on Wasm by setting + Qt::FramelessWindowHint + #12945 +
    • +
    • + CMakeLists: Require WebGL 2.0 when building for Wasm + #12952 +
    • +

    Misc Refactorings

    @@ -519,11 +617,55 @@ fix: typo ;) #12726 +
  • + refactor: Avoid temporary qlist allocation on midi sysex receive + #12843 +
  • +
  • + SoundSourceMP3: fix unused function warning + #12847 +
  • +
  • + update libkeyfinder to 2.2.8 + #12853 +
  • +
  • + feat(ci): Add eslint rule prefer-template + #12889 +
  • +
  • + Labeler: Add + qml + to labeler config + #12911 +
  • +
  • + WTrackMenu: Add missing wcoverartlabel.h include + #12924 +
  • +
  • + Fix clazy complains and naming + #12935 +
  • +
  • + src/library: Sort files into sub-directories + #12956 +
  • +
  • + CMakeLists: Fix deduplication trap with + --preload-file + #12944 +
  • +
  • + Add CI runner that allows cleaning up the download server + #12957 +
  • Update to latest vcpkg dependencies #11649 #12512 #12067 + #12898
  • GitHub actions updates @@ -560,6 +702,65 @@ #12577
  • +
    +
    + + +

    + Controller Mappings +

    +
      +
    • + Hercules Inpulse 200: Configure shift-browser knob to scroll the library (quick) + #12932 +
    • +
    • + Pioneer DDJ-FLX4: Add waveform zoom and other mapping improvements + #12896 + #12842 +
    • +
    • + Traktor Kontrol F1: Fixes for hid-parser and related script + #12876 +
    • +
    • + Traktor S3: Fix mapping crash on macOS + #12840 +
    • +
    +

    + Target Support +

    +
      +
    • + Fix various minor build issues + #12853 + #12847 + #12822 + #12892 +
    • +
    +

    + Miscellaneous +

    +
      +
    • + Remove unnecessary unpolish operation of the style, before polish the new style + #12445 +
    • +
    • + Developer Tools: Initially sort controls by group name, ascending + #12884 +
    • +
    • + History: Show track count and duration in sidebar + #12811 +
    • +
    • + Prevent removing tracks from locked playlists + #12927 +
    • +
    @@ -1665,7 +1866,7 @@ #12474
  • - Keyboard mapping: Return triggers doubleclick, move Preview functions to P / Shift+P + Keyboard mapping: Return triggers double-click, move Preview functions to P / Shift+P #12639
  • @@ -1849,7 +2050,7 @@ #12458
  • - change SKIN_WARNING to show the skin file:line first, then c++ context + Change SKIN_WARNING to show the skin file and line first, then c++ context #12253
  • diff --git a/res/qml/Theme/Theme.qml b/res/qml/Theme/Theme.qml index 23301140a0b..ca1b4d498e5 100644 --- a/res/qml/Theme/Theme.qml +++ b/res/qml/Theme/Theme.qml @@ -36,6 +36,15 @@ QtObject { property color buttonNormalColor: midGray property color textColor: lightGray2 property color toolbarActiveColor: white + property color waveformPrerollColor: midGray + property color waveformPostrollColor: midGray + property color waveformBeatColor: lightGray + property color waveformCursorColor: white + property color waveformMarkerDefault: '#ff7a01' + property color waveformMarkerLabel: Qt.rgba(255, 255, 255, 0.8) + property color waveformMarkerIntroOutroColor: '#2c5c9a' + property color waveformMarkerLoopColor: '#00b400' + property color waveformMarkerLoopColorDisabled: '#FFFFFF' property string fontFamily: "Open Sans" property int textFontPixelSize: 14 property int buttonFontPixelSize: 10 diff --git a/res/qml/WaveformCue.qml b/res/qml/WaveformCue.qml new file mode 100644 index 00000000000..74ba5dff246 --- /dev/null +++ b/res/qml/WaveformCue.qml @@ -0,0 +1,71 @@ +import Mixxx 1.0 as Mixxx +import QtQuick 2.15 +import QtQuick.Shapes 1.4 +import QtQuick.Window 2.15 + +import QtQuick.Controls 2.15 + +import "Theme" + +Item { + id: root + + property color color: Theme.waveformMarkerDefault + + property real markerHeight: root.height + property color labelColor: Theme.waveformMarkerLabel + property real radiusSize: 4 + property string cueLabel: qsTr("CUE") + + FontMetrics { + id: fontMetrics + font.family: Theme.fontFamily + } + + property rect contentRect: fontMetrics.tightBoundingRect(cueLabel) + + Shape { + ShapePath { + strokeWidth: 0 + strokeColor: 'transparent' + fillColor: color + strokeStyle: ShapePath.SolidLine + startX: -1; startY: 0 + + PathLine { x: 8; y: 0 } + PathLine { x: 8 - radiusSize + contentRect.width; y: 0 } + PathArc { + x: 8 + contentRect.width + y: radiusSize + radiusX: radiusSize; radiusY: radiusSize + direction: PathArc.Clockwise + } + PathLine { x: 8 + contentRect.width; y: 16 - radiusSize } + PathArc { + x: 8 - radiusSize + contentRect.width + y: 16 + radiusX: radiusSize; radiusY: radiusSize + direction: PathArc.Clockwise + } + PathLine { x: 8; y: 16 } + PathLine { x: 2; y: 16 } + PathLine { x: 2; y: markerHeight } + PathLine { x: -1; y: markerHeight } + PathLine { x: -1; y: 0 } + } + } + Shape { + ShapePath { + fillColor: labelColor + strokeColor: labelColor + PathText { + x: 3 + y: 3 + font.family: Theme.fontFamily + font.pixelSize: 13 + font.weight: Font.Bold + text: cueLabel + } + } + } +} diff --git a/res/qml/WaveformHotcue.qml b/res/qml/WaveformHotcue.qml new file mode 100644 index 00000000000..0c010e42cb0 --- /dev/null +++ b/res/qml/WaveformHotcue.qml @@ -0,0 +1,100 @@ +import "." as Skin +import Mixxx 1.0 as Mixxx +import QtQuick 2.15 +import QtQuick.Shapes 1.4 +import QtQuick.Window 2.15 + +import QtQuick.Controls 2.15 + +import "Theme" + +Item { + id: root + + required property int hotcueNumber + required property string group + required property string label + property bool isLoop: false + + Skin.Hotcue { + id: hotcue + + group: root.group + hotcueNumber: root.hotcueNumber + } + + property real markerHeight: root.height + property color labelColor: Theme.waveformMarkerLabel + property real radiusSize: 4 + property string hotcueLabel: label != "" ? `${hotcueNumber}: ${label}` : `${hotcueNumber}` + + FontMetrics { + id: fontMetrics + font.family: Theme.fontFamily + } + + property rect contentRect: fontMetrics.tightBoundingRect(hotcueLabel) + + Rectangle { + visible: root.isLoop + anchors.fill: parent + color: Qt.alpha(hotcue.color, 0.3) + } + + Shape { + ShapePath { + strokeWidth: 0 + strokeColor: 'transparent' + fillColor: hotcue.color + strokeStyle: ShapePath.SolidLine + startX: -1; startY: 0 + + PathLine { x: 1; y: 0 } + PathLine { x: 1; y: markerHeight - 16 } + PathLine { x: 8 - radiusSize + contentRect.width; y: markerHeight - 16 } + PathArc { + x: 8 + contentRect.width + y: markerHeight - 16 + radiusSize + radiusX: radiusSize; radiusY: radiusSize + direction: PathArc.Clockwise + } + PathLine { x: 8 + contentRect.width; y: markerHeight - radiusSize } + PathArc { + x: 8 - radiusSize + contentRect.width + y: markerHeight + radiusX: radiusSize; radiusY: radiusSize + direction: PathArc.Clockwise + } + PathLine { x: -1; y: markerHeight } + PathLine { x: -1; y: 0 } + } + } + Shape { + visible: root.isLoop + ShapePath { + strokeWidth: 0 + strokeColor: 'transparent' + fillColor: hotcue.color + strokeStyle: ShapePath.SolidLine + startX: root.width - 1; startY: 0 + + PathLine { x: root.width - 1; y: markerHeight } + PathLine { x: root.width + 1; y: markerHeight } + PathLine { x: root.width + 1; y: 0 } + } + } + Shape { + ShapePath { + fillColor: labelColor + strokeColor: labelColor + PathText { + x: 3 + y: markerHeight - 13 + font.family: Theme.fontFamily + font.pixelSize: 13 + font.weight: Font.Medium + text: hotcueLabel + } + } + } +} diff --git a/res/qml/WaveformIntroOutro.qml b/res/qml/WaveformIntroOutro.qml new file mode 100644 index 00000000000..ba6779cd692 --- /dev/null +++ b/res/qml/WaveformIntroOutro.qml @@ -0,0 +1,95 @@ +import Mixxx 1.0 as Mixxx +import QtQuick 2.15 +import QtQuick.Shapes 1.4 +import QtQuick.Window 2.15 + +import QtQuick.Controls 2.15 + +import "Theme" + +Item { + id: root + + property color mainColor: Theme.waveformMarkerIntroOutroColor + property bool isIntro: true + + property real markerHeight: root.height + property real radiusSize: 4 + + Rectangle { + anchors.fill: parent + color: Qt.alpha(root.mainColor, 0.1) + } + + Shape { + ShapePath { + strokeWidth: 0 + strokeColor: 'transparent' + fillColor: root.mainColor + strokeStyle: ShapePath.SolidLine + startX: -1; startY: 0 + + PathLine { x: 16 - radiusSize; y: 0 } + PathArc { + x: 16 + y: radiusSize + radiusX: radiusSize; radiusY: radiusSize + direction: PathArc.Clockwise + } + PathLine { x: 16; y: 16 - radiusSize } + PathArc { + x: 16 - radiusSize + y: 16 + radiusX: radiusSize; radiusY: radiusSize + direction: PathArc.Clockwise + } + PathLine { x: 1; y: 16 } + PathLine { x: 1; y: markerHeight } + PathLine { x: -1; y: markerHeight } + } + } + Shape { + visible: root.width != 0 + ShapePath { + strokeWidth: 0 + strokeColor: 'transparent' + fillColor: root.mainColor + strokeStyle: ShapePath.SolidLine + startX: root.width - 1; startY: 0 + + PathLine { x: root.width - 16 + radiusSize; y: 0 } + PathArc { + x: root.width - 16 + y: radiusSize + radiusX: radiusSize; radiusY: radiusSize + direction: PathArc.Counterclockwise + } + PathLine { x: root.width - 16; y: 16 - radiusSize } + PathArc { + x: root.width - 16 + radiusSize + y: 16 + radiusX: radiusSize; radiusY: radiusSize + direction: PathArc.Counterclockwise + } + PathLine { x: root.width -1; y: 16 } + PathLine { x: root.width -1; y: markerHeight } + PathLine { x: root.width + 1; y: markerHeight } + PathLine { x: root.width + 1; y: 0 } + } + } + Image { + x: 2 + y: 2 + width: 12 + height: 12 + source: `images/mark_${(root.isIntro ? 'intro' : 'outro')}.svg` + } + Image { + visible: root.width != 0 + x: root.width - 14 + y: 2 + width: 12 + height: 12 + source: `images/mark_${(root.isIntro ? 'intro' : 'outro')}.svg` + } +} diff --git a/res/qml/WaveformLoop.qml b/res/qml/WaveformLoop.qml new file mode 100644 index 00000000000..d05e1bbe26c --- /dev/null +++ b/res/qml/WaveformLoop.qml @@ -0,0 +1,76 @@ +import Mixxx 1.0 as Mixxx +import QtQuick 2.15 +import QtQuick.Shapes 1.4 +import QtQuick.Window 2.15 + +import QtQuick.Controls 2.15 + +import "Theme" + +Item { + id: root + + property color mainColor: Theme.waveformMarkerLoopColor + property color disabledColor: Theme.waveformMarkerLoopColorDisabled + property real enabledOpacity: 0.8 + property real disabledOpacity: 0.5 + + property real markerHeight: root.height + property real radiusSize: 4 + property bool enabled: true + + Rectangle { + anchors.fill: parent + color: Qt.alpha(root.enabled ? root.mainColor : root.disabledColor, root.enabled ? root.enabledOpacity : root.disabledOpacity) + } + + Shape { + ShapePath { + strokeWidth: 0 + strokeColor: 'transparent' + fillColor: root.mainColor + strokeStyle: ShapePath.SolidLine + startX: -1; startY: 0 + + PathLine { x: -16 + radiusSize; y: 0 } + PathArc { + x: -16 + y: radiusSize + radiusX: radiusSize; radiusY: radiusSize + direction: PathArc.Counterclockwise + } + PathLine { x: -16; y: 16 - radiusSize } + PathArc { + x: -16 + radiusSize + y: 16 + radiusX: radiusSize; radiusY: radiusSize + direction: PathArc.Counterclockwise + } + PathLine { x: -1; y: 16 } + PathLine { x: -1; y: markerHeight } + PathLine { x: 1; y: markerHeight } + PathLine { x: 1; y: 0 } + } + } + Shape { + + ShapePath { + strokeWidth: 0 + strokeColor: 'transparent' + fillColor: root.mainColor + strokeStyle: ShapePath.SolidLine + startX: root.width - 1; startY: 0 + + PathLine { x: root.width - 1; y: markerHeight } + PathLine { x: root.width + 1; y: markerHeight } + PathLine { x: root.width + 1; y: 0 } + } + } + Image { + x: -14 + y: 2 + width: 12 + height: 12 + source: "images/mark_loop.svg" + } +} diff --git a/res/qml/WaveformRow.qml b/res/qml/WaveformRow.qml new file mode 100644 index 00000000000..be129fc42bf --- /dev/null +++ b/res/qml/WaveformRow.qml @@ -0,0 +1,380 @@ +import "." as Skin +import Mixxx 1.0 as Mixxx +import QtQuick 2.14 +import QtQuick.Shapes 1.12 +import "Theme" + +Item { + id: root + + enum MouseStatus { + Normal, + Bending, + Scratching + } + + property string group // required + property var deckPlayer: Mixxx.PlayerManager.getPlayer(group) + + Item { + id: waveformContainer + + property real duration: samplesControl.value / sampleRateControl.value + + anchors.fill: parent + clip: true + + Mixxx.ControlProxy { + id: samplesControl + + group: root.group + key: "track_samples" + } + + Mixxx.ControlProxy { + id: sampleRateControl + + group: root.group + key: "track_samplerate" + } + + Mixxx.ControlProxy { + id: playPositionControl + + group: root.group + key: "playposition" + } + + Mixxx.ControlProxy { + id: rateRatioControl + + group: root.group + key: "rate_ratio" + } + + Mixxx.ControlProxy { + id: zoomControl + + group: root.group + key: "waveform_zoom" + } + + Mixxx.ControlProxy { + id: introStartPosition + + group: root.group + key: "intro_start_position" + } + + Mixxx.ControlProxy { + id: introEndPosition + + group: root.group + key: "intro_end_position" + } + + Mixxx.ControlProxy { + id: outroStartPosition + + group: root.group + key: "outro_start_position" + } + + Mixxx.ControlProxy { + id: outroEndPosition + + group: root.group + key: "outro_end_position" + } + + Mixxx.ControlProxy { + id: loopStartPosition + + group: root.group + key: "loop_start_position" + } + + Mixxx.ControlProxy { + id: loopEndPosition + + group: root.group + key: "loop_end_position" + } + + Mixxx.ControlProxy { + id: loopEnabled + + group: root.group + key: "loop_enabled" + } + + Mixxx.ControlProxy { + id: mainCuePosition + + group: root.group + key: "cue_point" + } + + Item { + id: waveform + + property real effectiveZoomFactor: (1 / rateRatioControl.value) * (100 / zoomControl.value) + + width: waveformContainer.duration * effectiveZoomFactor + height: parent.height + x: playMarker.screenPosition * waveformContainer.width - playPositionControl.value * width + visible: root.deckPlayer.isLoaded + + WaveformShader { + group: root.group + anchors.fill: parent + } + + Shape { + id: preroll + + property real triangleHeight: waveform.height + property real triangleWidth: 0.25 * waveform.effectiveZoomFactor + property int numTriangles: Math.ceil(width / triangleWidth) + + anchors.top: waveform.top + anchors.right: waveform.left + width: Math.max(0, waveform.x) + height: waveform.height + + ShapePath { + strokeColor: Theme.waveformPrerollColor + strokeWidth: 1 + fillColor: "transparent" + + PathMultiline { + paths: { + let p = []; + for (let i = 0; i < preroll.numTriangles; i++) { + p.push([ + Qt.point(preroll.width - i * preroll.triangleWidth, preroll.triangleHeight / 2), + Qt.point(preroll.width - (i + 1) * preroll.triangleWidth, 0), + Qt.point(preroll.width - (i + 1) * preroll.triangleWidth, preroll.triangleHeight), + Qt.point(preroll.width - i * preroll.triangleWidth, preroll.triangleHeight / 2), + ]); + } + return p; + } + } + } + } + + Shape { + id: postroll + + property real triangleHeight: waveform.height + property real triangleWidth: 0.25 * waveform.effectiveZoomFactor + property int numTriangles: Math.ceil(width / triangleWidth) + + anchors.top: waveform.top + anchors.left: waveform.right + width: waveformContainer.width / 2 + height: waveform.height + + ShapePath { + strokeColor: Theme.waveformPostrollColor + strokeWidth: 1 + fillColor: "transparent" + + PathMultiline { + paths: { + let p = []; + for (let i = 0; i < postroll.numTriangles; i++) { + p.push([ + Qt.point(i * postroll.triangleWidth, postroll.triangleHeight / 2), + Qt.point((i + 1) * postroll.triangleWidth, 0), + Qt.point((i + 1) * postroll.triangleWidth, postroll.triangleHeight), + Qt.point(i * postroll.triangleWidth, postroll.triangleHeight / 2), + ]); + } + return p; + } + } + } + } + + Repeater { + model: root.deckPlayer.beatsModel + + Rectangle { + property real alpha: 0.9 // TODO: Make this configurable (i.e., "[Waveform],beatGridAlpha" config option) + + width: 1 + height: waveform.height + x: (framePosition * 2 / samplesControl.value) * waveform.width + color: Theme.waveformBeatColor + } + } + + Skin.WaveformIntroOutro { + id: intro + + visible: introStartPosition.value != -1 || introEndPosition.value != -1 + + height: waveform.height + x: ((introStartPosition.value != -1 ? introStartPosition.value : introEndPosition.value) / samplesControl.value) * waveform.width + width: introEndPosition.value == -1 ? 0 : ((introEndPosition.value - introStartPosition.value) / samplesControl.value) * waveform.width + } + + Skin.WaveformIntroOutro { + id: outro + + visible: outroStartPosition.value != -1 || outroEndPosition.value != -1 + isIntro: false + + height: waveform.height + x: ((outroStartPosition.value != -1 ? outroStartPosition.value : outroEndPosition.value) / samplesControl.value) * waveform.width + width: outroEndPosition.value == -1 || outroStartPosition.value == -1 ? 0 : ((outroEndPosition.value - outroStartPosition.value) / samplesControl.value) * waveform.width + } + + Skin.WaveformLoop { + id: loop + + visible: loopStartPosition.value != -1 && loopEndPosition.value != -1 + + height: waveform.height + x: (loopStartPosition.value / samplesControl.value) * waveform.width + width: ((loopEndPosition.value - loopStartPosition.value) / samplesControl.value) * waveform.width + enabled: loopEnabled.value + } + + Repeater { + model: root.deckPlayer.hotcuesModel + + Item { + id: cue + + required property int startPosition + required property int endPosition + required property string label + required property bool isLoop + required property int hotcueNumber + + Skin.WaveformHotcue { + group: root.group + hotcueNumber: cue.hotcueNumber + 1 + label: cue.label + isLoop: cue.isLoop + + x: (startPosition * 2 / samplesControl.value) * waveform.width + width: cue.isLoop ? ((endPosition - startPosition) * 2 / samplesControl.value) * waveform.width : null + height: waveform.height + } + } + } + + Skin.WaveformCue { + id: maincue + + height: waveform.height + x: (mainCuePosition.value / samplesControl.value) * waveform.width + } + } + } + + Shape { + id: playMarkerShape + + anchors.fill: parent + + ShapePath { + id: playMarker + + property real screenPosition: 0.5 + + startX: playMarkerShape.width * playMarker.screenPosition + startY: 0 + strokeColor: Theme.waveformCursorColor + strokeWidth: 1 + + PathLine { + id: marker + + x: playMarkerShape.width * playMarker.screenPosition + y: playMarkerShape.height + } + } + } + + Mixxx.ControlProxy { + id: scratchPositionEnableControl + + group: root.group + key: "scratch_position_enable" + } + + Mixxx.ControlProxy { + id: scratchPositionControl + + group: root.group + key: "scratch_position" + } + + Mixxx.ControlProxy { + id: wheelControl + + group: root.group + key: "wheel" + } + + MouseArea { + property int mouseStatus: WaveformRow.MouseStatus.Normal + property point mouseAnchor: Qt.point(0, 0) + + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + onPressed: { + mouseAnchor = Qt.point(mouse.x, mouse.y); + if (mouse.button == Qt.LeftButton) { + if (mouseStatus == WaveformRow.MouseStatus.Bending) + wheelControl.parameter = 0.5; + + mouseStatus = WaveformRow.MouseStatus.Scratching; + scratchPositionEnableControl.value = 1; + // TODO: Calculate position properly + scratchPositionControl.value = -mouse.x * waveform.effectiveZoomFactor * 2; + console.log(mouse.x); + } else { + if (mouseStatus == WaveformRow.MouseStatus.Scratching) + scratchPositionEnableControl.value = 0; + + wheelControl.parameter = 0.5; + mouseStatus = WaveformRow.MouseStatus.Bending; + } + } + onPositionChanged: { + switch (mouseStatus) { + case WaveformRow.MouseStatus.Bending: { + const diff = mouse.x - mouseAnchor.x; + // Start at the middle of [0.0, 1.0], and emit values based on how far + // the mouse has traveled horizontally. Note, for legacy (MIDI) reasons, + // this is tuned to 127. + const v = 0.5 + (diff / 1270); + // clamp to [0.0, 1.0] + wheelControl.parameter = Mixxx.MathUtils.clamp(v, 0, 1); + break; + }; + case WaveformRow.MouseStatus.Scratching: + // TODO: Calculate position properly + scratchPositionControl.value = -mouse.x * waveform.effectiveZoomFactor * 2; + break; + } + } + onReleased: { + switch (mouseStatus) { + case WaveformRow.MouseStatus.Bending: + wheelControl.parameter = 0.5; + break; + case WaveformRow.MouseStatus.Scratching: + scratchPositionEnableControl.value = 0; + break; + } + mouseStatus = WaveformRow.MouseStatus.Normal; + } + } +} diff --git a/res/qml/WaveformShader.qml b/res/qml/WaveformShader.qml new file mode 100644 index 00000000000..33de4acde0c --- /dev/null +++ b/res/qml/WaveformShader.qml @@ -0,0 +1,88 @@ +import Mixxx 1.0 as Mixxx +import QtQuick 2.12 + +ShaderEffect { + id: root + + property string group // required + property var deckPlayer: Mixxx.PlayerManager.getPlayer(group) + property size framebufferSize: Qt.size(width, height) + property int waveformLength: root.deckPlayer.waveformLength + property int textureSize: root.deckPlayer.waveformTextureSize + property int textureStride: root.deckPlayer.waveformTextureStride + property real firstVisualIndex: 1 + property real lastVisualIndex: root.deckPlayer.waveformLength / 2 + property color axesColor: "#FFFFFF" + property color highColor: "#0000FF" + property color midColor: "#00FF00" + property color lowColor: "#FF0000" + property real highGain: filterWaveformEnableControl.value ? (filterHighKillControl.value ? 0 : filterHighControl.value) : 1 + property real midGain: filterWaveformEnableControl.value ? (filterMidKillControl.value ? 0 : filterMidControl.value) : 1 + property real lowGain: filterWaveformEnableControl.value ? (filterLowKillControl.value ? 0 : filterLowControl.value) : 1 + property real allGain: pregainControl.value + property Image waveformTexture + + fragmentShader: "qrc:/shaders/rgbsignal_qml.frag.qsb" + + Mixxx.ControlProxy { + id: pregainControl + + group: root.group + key: "pregain" + } + + Mixxx.ControlProxy { + id: filterWaveformEnableControl + + group: root.group + key: "filterWaveformEnable" + } + + Mixxx.ControlProxy { + id: filterHighControl + + group: "[EqualizerRack1_" + root.group + "_Effect1]" + key: "parameter3" + } + + Mixxx.ControlProxy { + id: filterHighKillControl + + group: "[EqualizerRack1_" + root.group + "_Effect1]" + key: "button_parameter3" + } + + Mixxx.ControlProxy { + id: filterMidControl + + group: "[EqualizerRack1_" + root.group + "_Effect1]" + key: "parameter2" + } + + Mixxx.ControlProxy { + id: filterMidKillControl + + group: "[EqualizerRack1_" + root.group + "_Effect1]" + key: "button_parameter2" + } + + Mixxx.ControlProxy { + id: filterLowControl + + group: "[EqualizerRack1_" + root.group + "_Effect1]" + key: "parameter1" + } + + Mixxx.ControlProxy { + id: filterLowKillControl + + group: "[EqualizerRack1_" + root.group + "_Effect1]" + key: "button_parameter1" + } + + waveformTexture: Image { + visible: false + layer.enabled: false + source: root.deckPlayer.waveformTexture + } +} diff --git a/res/qml/images/mark_intro.svg b/res/qml/images/mark_intro.svg new file mode 100644 index 00000000000..96dd7bcd702 --- /dev/null +++ b/res/qml/images/mark_intro.svg @@ -0,0 +1,18 @@ + + + + + + + diff --git a/res/qml/images/mark_loop.svg b/res/qml/images/mark_loop.svg new file mode 100644 index 00000000000..3bcb6feff1c --- /dev/null +++ b/res/qml/images/mark_loop.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + diff --git a/res/qml/images/mark_outro.svg b/res/qml/images/mark_outro.svg new file mode 100644 index 00000000000..ad9c98f174b --- /dev/null +++ b/res/qml/images/mark_outro.svg @@ -0,0 +1,18 @@ + + + + + + + diff --git a/res/qml/main.qml b/res/qml/main.qml index 73804bfd66b..15578d303cb 100644 --- a/res/qml/main.qml +++ b/res/qml/main.qml @@ -98,6 +98,58 @@ ApplicationWindow { } } + WaveformRow { + id: deck3waveform + + group: "[Channel3]" + width: root.width + height: 60 + visible: root.show4decks && !root.maximizeLibrary + + FadeBehavior on visible { + fadeTarget: deck3waveform + } + } + + WaveformRow { + id: deck1waveform + + group: "[Channel1]" + width: root.width + height: 60 + visible: !root.maximizeLibrary + + FadeBehavior on visible { + fadeTarget: deck1waveform + } + } + + WaveformRow { + id: deck2waveform + + group: "[Channel2]" + width: root.width + height: 60 + visible: !root.maximizeLibrary + + FadeBehavior on visible { + fadeTarget: deck2waveform + } + } + + WaveformRow { + id: deck4waveform + + group: "[Channel4]" + width: root.width + height: 60 + visible: root.show4decks && !root.maximizeLibrary + + FadeBehavior on visible { + fadeTarget: deck4waveform + } + } + Skin.DeckRow { id: decks12 diff --git a/res/shaders/CMakeLists.txt b/res/shaders/CMakeLists.txt new file mode 100644 index 00000000000..c54470accf9 --- /dev/null +++ b/res/shaders/CMakeLists.txt @@ -0,0 +1,5 @@ +qt_add_shaders(mixxx-lib "waveform_shaders" + PREFIX "/shaders" + FILES + "rgbsignal_qml.frag" +) diff --git a/res/shaders/rgbsignal_qml.frag b/res/shaders/rgbsignal_qml.frag new file mode 100644 index 00000000000..602f858ba71 --- /dev/null +++ b/res/shaders/rgbsignal_qml.frag @@ -0,0 +1,144 @@ +#version 450 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + + vec2 framebufferSize; + vec4 axesColor; + vec4 lowColor; + vec4 midColor; + vec4 highColor; + + int waveformLength; + int textureSize; + int textureStride; + + float allGain; + float lowGain; + float midGain; + float highGain; + float firstVisualIndex; + float lastVisualIndex; +}; + +layout(binding = 1) uniform sampler2D waveformTexture; + +vec4 getWaveformData(float index) { + vec2 uv_data; + uv_data.y = floor(index / float(textureStride)); + uv_data.x = floor(index - uv_data.y * float(textureStride)); + // Divide again to convert to normalized UV coordinates. + return texture(waveformTexture, uv_data / float(textureStride)); +} + +void main(void) { + vec2 uv = qt_TexCoord0.st; + vec4 pixel = gl_FragCoord; + fragColor = vec4(1, 0, 0, 1); + + float new_currentIndex = + floor(firstVisualIndex + + uv.x * (lastVisualIndex - firstVisualIndex)) * + 2; + + // Texture coordinates put (0,0) at the bottom left, so show the right + // channel if we are in the bottom half. + if (uv.y < 0.5) { + new_currentIndex += 1; + } + + vec4 outputColor = vec4(0.0, 0.0, 0.0, 0.0); + bool showing = false; + bool showingUnscaled = false; + vec4 showingColor = vec4(0.0, 0.0, 0.0, 0.0); + vec4 showingUnscaledColor = vec4(0.0, 0.0, 0.0, 0.0); + + // We don't exit early if the waveform data is not valid because we may want + // to show other things (e.g. the axes lines) even when we are on a pixel + // that does not have valid waveform data. + if (new_currentIndex >= 0 && new_currentIndex <= waveformLength - 1) { + vec4 new_currentDataUnscaled = getWaveformData(new_currentIndex) * allGain; + + vec4 new_currentData = new_currentDataUnscaled; + new_currentData.x *= lowGain; + new_currentData.y *= midGain; + new_currentData.z *= highGain; + + //(vrince) debug see pre-computed signal + // gl_FragColor = new_currentData; + // return; + + // Represents the [-1, 1] distance of this pixel. Subtracting this from + // the signal data in new_currentData, we can tell if a signal band should + // show in this pixel if the component is > 0. + float ourDistance = abs((uv.y - 0.5) * 2.0); + + // Since the magnitude of the (low, mid, high) vector is used as the + // waveform height, re-scale the maximum height to 1. + const float scaleFactor = 1.0 / sqrt(3.0); + + float signalDistance = sqrt(new_currentData.x * new_currentData.x + + new_currentData.y * new_currentData.y + + new_currentData.z * new_currentData.z) * + scaleFactor; + showing = (signalDistance - ourDistance) >= 0.0; + + // Linearly combine the low, mid, and high colors according to the low, + // mid, and high components. + showingColor = lowColor * new_currentData.x + + midColor * new_currentData.y + + highColor * new_currentData.z; + + // Re-scale the color by the maximum component. + float showingMax = max(showingColor.x, max(showingColor.y, showingColor.z)); + showingColor = showingColor / showingMax; + showingColor.w = 1.0; + + // Now do it all over again for the unscaled version of the waveform, + // which we will draw at very low opacity. + float signalDistanceUnscaled = + sqrt(new_currentDataUnscaled.x * new_currentDataUnscaled.x + + new_currentDataUnscaled.y * new_currentDataUnscaled.y + + new_currentDataUnscaled.z * new_currentDataUnscaled.z) * + scaleFactor; + showingUnscaled = (signalDistanceUnscaled - ourDistance) >= 0.0; + + // Linearly combine the low, mid, and high colors according to the + // original low, mid, and high components. + showingUnscaledColor = lowColor * new_currentDataUnscaled.x + + midColor * new_currentDataUnscaled.y + + highColor * new_currentDataUnscaled.z; + + // Re-scale the color by the maximum component. + float showingUnscaledMax = max(showingUnscaledColor.x, + max(showingUnscaledColor.y, showingUnscaledColor.z)); + showingUnscaledColor = showingUnscaledColor / showingUnscaledMax; + showingUnscaledColor.w = 1.0; + } + + // Draw the axes color as the lowest item on the screen. + // TODO(owilliams): The 4 in this line makes sure the axis gets + // rendered even when the waveform is fairly short. Really this + // value should be based on the size of the widget. + if (abs(framebufferSize.y / 2 - pixel.y) <= 4) { + outputColor.xyz = mix(outputColor.xyz, axesColor.xyz, axesColor.w); + outputColor.w = 1.0; + } + + if (showingUnscaled) { + float alpha = 0.4; + outputColor.xyz = mix(outputColor.xyz, showingUnscaledColor.xyz, alpha); + outputColor.w = 1.0; + } + + if (showing) { + float alpha = 0.8; + outputColor.xyz = mix(outputColor.xyz, showingColor.xyz, alpha); + outputColor.w = 1.0; + } + fragColor = outputColor; +} diff --git a/res/skins/Deere/preview_deck.xml b/res/skins/Deere/preview_deck.xml index b12ea427f23..1806cb669f5 100644 --- a/res/skins/Deere/preview_deck.xml +++ b/res/skins/Deere/preview_deck.xml @@ -33,7 +33,7 @@ text me,min - titleInfo + info right diff --git a/res/skins/Deere/sampler_row.xml b/res/skins/Deere/sampler_row.xml index 7fa3f36c71f..020d8ef1458 100644 --- a/res/skins/Deere/sampler_row.xml +++ b/res/skins/Deere/sampler_row.xml @@ -45,6 +45,7 @@ diff --git a/res/skins/Deere/style.qss b/res/skins/Deere/style.qss index ed067cd3ae0..e04992ef587 100644 --- a/res/skins/Deere/style.qss +++ b/res/skins/Deere/style.qss @@ -254,11 +254,14 @@ WTrackTableView, WTrackTableView QCheckBox:focus { outline: 1px solid #D6D6D6; } -/* This uses a custom qproperty to set the focus border - for Color and Cover Art cells, 1px solid, sharp corners. - See src/library/tableitemdelegate.cpp */ WTrackTableView { + /* This uses a custom qproperty to set the focus border + for Color and Cover Art cells, 1px solid, sharp corners. + See src/library/tableitemdelegate.cpp */ qproperty-focusBorderColor: #D6D6D6; + /* This is the color used to paint the text of played tracks. + BaseTrackTableModel::data() uses this to override Qt::ForegroundRole (QBrush). */ + qproperty-playedInactiveColor: #555; } WTrackTableView:focus, diff --git a/res/skins/LateNight/decks/preview_deck.xml b/res/skins/LateNight/decks/preview_deck.xml index a5773193a5b..0a0841f3109 100644 --- a/res/skins/LateNight/decks/preview_deck.xml +++ b/res/skins/LateNight/decks/preview_deck.xml @@ -42,7 +42,7 @@ 0me,20f right - titleInfo + info PreviewBPM diff --git a/res/skins/LateNight/samplers/sampler_expand_button.xml b/res/skins/LateNight/samplers/sampler_expand_button.xml index 41bb90fd38f..f9ba47abbcf 100644 --- a/res/skins/LateNight/samplers/sampler_expand_button.xml +++ b/res/skins/LateNight/samplers/sampler_expand_button.xml @@ -9,7 +9,7 @@ stacked - show_samplers + expand_samplers SamplerExpandOverlay 16f,36me 2 diff --git a/res/skins/LateNight/style_classic.qss b/res/skins/LateNight/style_classic.qss index ac80e15ecde..4f3067c9c9d 100644 --- a/res/skins/LateNight/style_classic.qss +++ b/res/skins/LateNight/style_classic.qss @@ -1354,7 +1354,7 @@ WPushButton#PlayIndicator, WPushButton#CueDeck, #PlayCueMini WPushButton, WPushButton#LoopActivate, -WPushButton#RateControls WPushButton, +#RateControls WPushButton, WPushButton#SyncSampler, #MixerContainer WPushButton, #FxUnitContainer WPushButton, @@ -2121,7 +2121,13 @@ WTrackTableView QCheckBox:focus { for Color and Cover Art cells, 1px solid, sharp corners. See src/library/tableitemdelegate.cpp */ WTrackTableView { + /* This uses a custom qproperty to set the focus border + for Color and Cover Art cells, 1px solid, sharp corners. + See src/library/tableitemdelegate.cpp */ qproperty-focusBorderColor: #fff; + /* This is the color used to paint the text of played tracks. + BaseTrackTableModel::data() uses this to override Qt::ForegroundRole (QBrush). */ + qproperty-playedInactiveColor: #555; } WLibrarySidebar { diff --git a/res/skins/LateNight/style_palemoon.qss b/res/skins/LateNight/style_palemoon.qss index 0014b4b065a..5dd1b3233bf 100644 --- a/res/skins/LateNight/style_palemoon.qss +++ b/res/skins/LateNight/style_palemoon.qss @@ -2587,11 +2587,14 @@ WTrackTableView, WTrackTableView QCheckBox:focus { outline: 1px solid #fff; } -/* This uses a custom qproperty to set the focus border - for Color and Cover Art cells, 1px solid, sharp corners. - See src/library/tableitemdelegate.cpp */ WTrackTableView { + /* This uses a custom qproperty to set the focus border + for Color and Cover Art cells, 1px solid, sharp corners. + See src/library/tableitemdelegate.cpp */ qproperty-focusBorderColor: #fff; + /* This is the color used to paint the text of played tracks. + BaseTrackTableModel::data() uses this to override Qt::ForegroundRole (QBrush). */ + qproperty-playedInactiveColor: #555; } /* Table cell in edit mode */ diff --git a/res/skins/Shade/preview_deck.xml b/res/skins/Shade/preview_deck.xml index 5588b3ad84b..aa132122d68 100644 --- a/res/skins/Shade/preview_deck.xml +++ b/res/skins/Shade/preview_deck.xml @@ -53,7 +53,7 @@ padding-top: 2px;} [PreviewDeck1] - titleInfo + info me,min right diff --git a/res/skins/Shade/samplerrow.xml b/res/skins/Shade/samplerrow.xml index bc4db361a22..335280539c9 100644 --- a/res/skins/Shade/samplerrow.xml +++ b/res/skins/Shade/samplerrow.xml @@ -31,7 +31,7 @@ horizontal - show_samplers + expand_samplers 2 0 @@ -158,7 +158,7 @@ style/style_bg_sampler_right.png - show_samplers + expand_samplers 2 0 diff --git a/res/skins/Shade/style.qss b/res/skins/Shade/style.qss index 269aa35a60f..b9eebbb2f2b 100644 --- a/res/skins/Shade/style.qss +++ b/res/skins/Shade/style.qss @@ -526,7 +526,13 @@ WTrackTableView QCheckBox:focus { for Color and Cover Art cells, 1px solid, sharp corners. See src/library/tableitemdelegate.cpp */ WTrackTableView { + /* This uses a custom qproperty to set the focus border + for Color and Cover Art cells, 1px solid, sharp corners. + See src/library/tableitemdelegate.cpp */ qproperty-focusBorderColor: #c9c9c9; + /* This is the color used to paint the text of played tracks. + BaseTrackTableModel::data() uses this to override Qt::ForegroundRole (QBrush). */ + qproperty-playedInactiveColor: #555; } /* Table cell in edit mode */ diff --git a/res/skins/Shade/style_dark.qss b/res/skins/Shade/style_dark.qss index 4a4f7803797..a20d798cfc5 100644 --- a/res/skins/Shade/style_dark.qss +++ b/res/skins/Shade/style_dark.qss @@ -198,7 +198,13 @@ WTrackTableView QCheckBox:focus { for Color and Cover Art cells, 1px solid, sharp corners. See src/library/tableitemdelegate.cpp */ WTrackTableView { + /* This uses a custom qproperty to set the focus border + for Color and Cover Art cells, 1px solid, sharp corners. + See src/library/tableitemdelegate.cpp */ qproperty-focusBorderColor: #ccc; + /* This is the color used to paint the text of played tracks. + BaseTrackTableModel::data() uses this to override Qt::ForegroundRole (QBrush). */ + qproperty-playedInactiveColor: #555; } /* Table cell in edit mode */ diff --git a/res/skins/Tango (64 Samplers)/skin.xml b/res/skins/Tango (64 Samplers)/skin.xml index 4e326b24256..e2415c2bc35 100644 --- a/res/skins/Tango (64 Samplers)/skin.xml +++ b/res/skins/Tango (64 Samplers)/skin.xml @@ -422,9 +422,9 @@ [Library] --> WaveformSplitter - me,me vertical - 100,10000 + me,me + 100,550 [Skin],stackedWaveforms_splitSize 0,0 @@ -436,7 +436,7 @@ vertical - me,max + me,me Waveforms_Singleton diff --git a/res/skins/Tango/decks/preview_deck.xml b/res/skins/Tango/decks/preview_deck.xml index 9afe526893d..f31cd98321f 100644 --- a/res/skins/Tango/decks/preview_deck.xml +++ b/res/skins/Tango/decks/preview_deck.xml @@ -61,7 +61,7 @@ Variables: text 1me,17f - titleInfo + info right diff --git a/res/skins/Tango/mic_aux_sampler/sampler_row.xml b/res/skins/Tango/mic_aux_sampler/sampler_row.xml index cde6f91a68f..730fff1f119 100644 --- a/res/skins/Tango/mic_aux_sampler/sampler_row.xml +++ b/res/skins/Tango/mic_aux_sampler/sampler_row.xml @@ -47,6 +47,7 @@ Variables: 1min,3f diff --git a/res/skins/Tango/skin.xml b/res/skins/Tango/skin.xml index 97edbf271b1..58397ca66c4 100644 --- a/res/skins/Tango/skin.xml +++ b/res/skins/Tango/skin.xml @@ -416,9 +416,9 @@ [Library] --> WaveformSplitter - me,me vertical - 100,10000 + me,me + 100,550 [Skin],stackedWaveforms_splitSize 0,0 @@ -430,7 +430,7 @@ vertical - me,max + me,me Waveforms_Singleton diff --git a/res/skins/Tango/style.qss b/res/skins/Tango/style.qss index 17c903921ba..a2b0649ce4e 100644 --- a/res/skins/Tango/style.qss +++ b/res/skins/Tango/style.qss @@ -2615,7 +2615,13 @@ WTrackTableView QCheckBox:focus { for Color and Cover Art cells, 1px solid, sharp corners. See src/library/tableitemdelegate.cpp */ WTrackTableView { + /* This uses a custom qproperty to set the focus border + for Color and Cover Art cells, 1px solid, sharp corners. + See src/library/tableitemdelegate.cpp */ qproperty-focusBorderColor: #fff; + /* This is the color used to paint the text of played tracks. + BaseTrackTableModel::data() uses this to override Qt::ForegroundRole (QBrush). */ + qproperty-playedInactiveColor: #555; } /* Table cell in edit mode */ diff --git a/src/controllers/bulk/bulkcontroller.cpp b/src/controllers/bulk/bulkcontroller.cpp index 4dd814b7e52..0e44462299f 100644 --- a/src/controllers/bulk/bulkcontroller.cpp +++ b/src/controllers/bulk/bulkcontroller.cpp @@ -172,6 +172,12 @@ int BulkController::open() { if (m_pReader != nullptr) { qCWarning(m_logBase) << "BulkReader already present for" << getName(); + } else if (m_pMapping && + !(m_pMapping->getDeviceDirection() & + LegacyControllerMapping::DeviceDirection::Incoming)) { + qDebug() << "The mapping for the bulk device" << getName() + << "doesn't require reading the data. Ignoring BulkReader " + "setup."; } else { m_pReader = new BulkReader(m_phandle, in_epaddr); m_pReader->setObjectName(QString("BulkReader %1").arg(getName())); @@ -195,10 +201,12 @@ int BulkController::close() { qCInfo(m_logBase) << "Shutting down USB Bulk device" << getName(); // Stop the reading thread - if (m_pReader == nullptr) { + if (m_pReader == nullptr && + m_pMapping->getDeviceDirection() & + LegacyControllerMapping::DeviceDirection::Incoming) { qCWarning(m_logBase) << "BulkReader not present for" << getName() << "yet the device is open!"; - } else { + } else if (m_pReader) { disconnect(m_pReader, &BulkReader::incomingData, this, &BulkController::receive); m_pReader->stop(); qCInfo(m_logBase) << " Waiting on reader to finish"; @@ -230,6 +238,14 @@ void BulkController::send(const QList& data, unsigned int length) { } void BulkController::sendBytes(const QByteArray& data) { + VERIFY_OR_DEBUG_ASSERT(!m_pMapping || + m_pMapping->getDeviceDirection() & + LegacyControllerMapping::DeviceDirection::Outgoing) { + qDebug() << "The mapping for the bulk device" << getName() + << "doesn't require sending data. Ignoring sending request."; + return; + } + int ret; int transferred; diff --git a/src/controllers/controller.cpp b/src/controllers/controller.cpp index c9cb45c7e5e..e37933858d1 100644 --- a/src/controllers/controller.cpp +++ b/src/controllers/controller.cpp @@ -76,6 +76,8 @@ bool Controller::applyMapping() { } m_pScriptEngineLegacy->setScriptFiles(scriptFiles); + + m_pScriptEngineLegacy->setSettings(pMapping->getSettings()); return m_pScriptEngineLegacy->initialize(); } diff --git a/src/controllers/controllermanager.cpp b/src/controllers/controllermanager.cpp index 971c9dc634b..7dbac102ef3 100644 --- a/src/controllers/controllermanager.cpp +++ b/src/controllers/controllermanager.cpp @@ -7,12 +7,16 @@ #include "controllers/controllerlearningeventfilter.h" #include "controllers/controllermappinginfoenumerator.h" #include "controllers/defs_controllers.h" -#include "controllers/midi/portmidienumerator.h" #include "moc_controllermanager.cpp" #include "util/cmdlineargs.h" #include "util/compatibility/qmutex.h" #include "util/duration.h" #include "util/time.h" + +#ifdef __PORTMIDI__ +#include "controllers/midi/portmidienumerator.h" +#endif + #ifdef __HSS1394__ #include "controllers/midi/hss1394enumerator.h" #endif @@ -148,7 +152,9 @@ void ControllerManager::slotInitialize() { // Instantiate all enumerators. Enumerators can take a long time to // construct since they interact with host MIDI APIs. +#ifdef __PORTMIDI__ m_enumerators.append(new PortMidiEnumerator()); +#endif #ifdef __HSS1394__ m_enumerators.append(new Hss1394Enumerator(m_pConfig)); #endif @@ -276,6 +282,7 @@ void ControllerManager::slotSetUpDevices() { if (!pMapping) { continue; } + pMapping->loadSettings(m_pConfig, pController->getName()); // This runs on the main thread but LegacyControllerMapping is not thread safe, so clone it. pController->setMapping(pMapping->clone()); diff --git a/src/controllers/controllermanager.h b/src/controllers/controllermanager.h index a41b015c020..cfa040d496a 100644 --- a/src/controllers/controllermanager.h +++ b/src/controllers/controllermanager.h @@ -5,6 +5,7 @@ #include #include +#include "controllers/controllerenumerator.h" #include "preferences/usersettings.h" #include "util/duration.h" diff --git a/src/controllers/dlgprefcontroller.cpp b/src/controllers/dlgprefcontroller.cpp index 2c6f09dee85..49bf0788393 100644 --- a/src/controllers/dlgprefcontroller.cpp +++ b/src/controllers/dlgprefcontroller.cpp @@ -21,6 +21,7 @@ #include "moc_dlgprefcontroller.cpp" #include "preferences/usersettings.h" #include "util/desktophelper.h" +#include "util/parented_ptr.h" #include "util/string.h" namespace { @@ -410,6 +411,7 @@ QString DlgPrefController::mappingFileLinks( void DlgPrefController::enumerateMappings(const QString& selectedMappingPath) { m_ui.comboBoxMapping->blockSignals(true); + QString currentMappingFilePath = mappingFilePathFromIndex(m_ui.comboBoxMapping->currentIndex()); m_ui.comboBoxMapping->clear(); // qDebug() << "Enumerating mappings for controller" << m_pController->getName(); @@ -461,14 +463,18 @@ void DlgPrefController::enumerateMappings(const QString& selectedMappingPath) { } else if (match.isValid()) { index = m_ui.comboBoxMapping->findText(match.getName()); } + QString newMappingFilePath = mappingFilePathFromIndex(index); if (index == -1) { m_ui.chkEnabledDevice->setEnabled(false); + m_ui.groupBoxSettings->setVisible(false); } else { m_ui.comboBoxMapping->setCurrentIndex(index); m_ui.chkEnabledDevice->setEnabled(true); } m_ui.comboBoxMapping->blockSignals(false); - slotMappingSelected(m_ui.comboBoxMapping->currentIndex()); + if (newMappingFilePath != currentMappingFilePath) { + slotMappingSelected(index); + } } MappingInfo DlgPrefController::enumerateMappingsFromEnumerator( @@ -498,6 +504,8 @@ MappingInfo DlgPrefController::enumerateMappingsFromEnumerator( void DlgPrefController::slotUpdate() { enumerateMappings(m_pControllerManager->getConfiguredMappingFileForDevice( m_pController->getName())); + // Force updating the controller settings + slotMappingSelected(m_ui.comboBoxMapping->currentIndex()); // enumeratePresets calls slotPresetSelected which will check the m_ui.chkEnabledDevice // checkbox if there is a valid mapping saved in the mixxx.cfg file. However, the @@ -514,9 +522,10 @@ void DlgPrefController::slotUpdate() { } void DlgPrefController::slotResetToDefaults() { - m_ui.chkEnabledDevice->setChecked(false); + if (m_pMapping) { + m_pMapping->resetSettings(); + } enumerateMappings(QString()); - slotMappingSelected(m_ui.comboBoxMapping->currentIndex()); } void DlgPrefController::applyMappingChanges() { @@ -557,9 +566,13 @@ void DlgPrefController::slotApply() { return; } - QString mappingPath = mappingPathFromIndex(m_ui.comboBoxMapping->currentIndex()); - m_pMapping = LegacyControllerMappingFileHandler::loadMapping( - QFileInfo(mappingPath), QDir(resourceMappingsPath(m_pConfig))); + // If there is currently a mapping loaded, we save the new settings for it. + // Note that `m_pMapping`, `mappingFileInfo` and the setting on the screen + // will always match as the settings displayed are updated depending of the + // currently selected mapping in `slotMappingSelected` + if (m_pMapping) { + m_pMapping->saveSettings(m_pConfig, m_pController->getName()); + } // Load the resulting mapping (which has been mutated by the input/output // table models). The controller clones the mapping so we aren't touching @@ -593,7 +606,7 @@ void DlgPrefController::enableWizardAndIOTabs(bool enable) { m_ui.outputMappingsTab->setEnabled(enable); } -QString DlgPrefController::mappingPathFromIndex(int index) const { +QString DlgPrefController::mappingFilePathFromIndex(int index) const { if (index == 0) { // "No Mapping" item return QString(); @@ -603,8 +616,8 @@ QString DlgPrefController::mappingPathFromIndex(int index) const { } void DlgPrefController::slotMappingSelected(int chosenIndex) { - QString mappingPath = mappingPathFromIndex(chosenIndex); - if (mappingPath.isEmpty()) { // User picked "No Mapping" item + QString mappingFilePath = mappingFilePathFromIndex(chosenIndex); + if (mappingFilePath.isEmpty()) { // User picked "No Mapping" item m_ui.chkEnabledDevice->setEnabled(false); if (m_ui.chkEnabledDevice->isChecked()) { @@ -614,6 +627,8 @@ void DlgPrefController::slotMappingSelected(int chosenIndex) { } enableWizardAndIOTabs(false); } + + m_ui.groupBoxSettings->setVisible(false); } else { // User picked a mapping m_ui.chkEnabledDevice->setEnabled(true); @@ -628,7 +643,7 @@ void DlgPrefController::slotMappingSelected(int chosenIndex) { // Check if the mapping is different from the configured mapping if (m_GuiInitialized && m_pControllerManager->getConfiguredMappingFileForDevice( - m_pController->getName()) != mappingPath) { + m_pController->getName()) != mappingFilePath) { setDirty(true); } @@ -643,9 +658,10 @@ void DlgPrefController::slotMappingSelected(int chosenIndex) { } } + auto mappingFileInfo = QFileInfo(mappingFilePath); std::shared_ptr pMapping = LegacyControllerMappingFileHandler::loadMapping( - QFileInfo(mappingPath), QDir(resourceMappingsPath(m_pConfig))); + mappingFileInfo, QDir(resourceMappingsPath(m_pConfig))); if (pMapping) { DEBUG_ASSERT(!pMapping->isDirty()); @@ -654,7 +670,7 @@ void DlgPrefController::slotMappingSelected(int chosenIndex) { if (previousMappingSaved) { // We might have saved the previous preset with a new name, so update // the preset combobox. - enumerateMappings(mappingPath); + enumerateMappings(mappingFilePath); } else { slotShowMapping(pMapping); } @@ -823,10 +839,41 @@ void DlgPrefController::slotShowMapping(std::shared_ptr m_ui.labelLoadedMappingSupportLinks->setText(mappingSupportLinks(pMapping)); m_ui.labelLoadedMappingScriptFileLinks->setText(mappingFileLinks(pMapping)); - // We mutate this mapping so keep a reference to it while we are using it. - // TODO(rryan): Clone it? Technically a waste since nothing else uses this - // copy but if someone did they might not expect it to change. - m_pMapping = pMapping; + if (pMapping) { + pMapping->loadSettings(m_pConfig, m_pController->getName()); + auto settings = pMapping->getSettings(); + auto* pLayout = pMapping->getSettingsLayout(); + + QLayoutItem* pItem; + while ((pItem = m_ui.groupBoxSettings->layout()->takeAt(0)) != nullptr) { + delete pItem->widget(); + delete pItem; + } + + if (pLayout != nullptr && !settings.isEmpty()) { + m_ui.groupBoxSettings->layout()->addWidget(pLayout->build(m_ui.groupBoxSettings)); + + for (const auto& setting : std::as_const(settings)) { + connect(setting.get(), + &AbstractLegacyControllerSetting::changed, + this, + [this] { setDirty(true); }); + } + } + + m_ui.groupBoxSettings->setVisible(!settings.isEmpty()); + } + + // If there is still settings that may be saved and no new mapping selected + // (e.g restored default), we keep the the dirty mapping live so it can be + // saved in apply slot. If there is a new mapping, then setting changes are + // discarded + if (pMapping || (m_pMapping && !m_pMapping->hasDirtySettings())) { + // We mutate this mapping so keep a reference to it while we are using it. + // TODO(rryan): Clone it? Technically a waste since nothing else uses this + // copy but if someone did they might not expect it to change. + m_pMapping = pMapping; + } // Inputs tab ControllerInputMappingTableModel* pInputModel = diff --git a/src/controllers/dlgprefcontroller.h b/src/controllers/dlgprefcontroller.h index eccd070f22b..9a3fbd7e6ed 100644 --- a/src/controllers/dlgprefcontroller.h +++ b/src/controllers/dlgprefcontroller.h @@ -81,7 +81,7 @@ class DlgPrefController : public DlgPreferencePage { QString mappingDescription(const std::shared_ptr pMapping) const; QString mappingSupportLinks(const std::shared_ptr pMapping) const; QString mappingFileLinks(const std::shared_ptr pMapping) const; - QString mappingPathFromIndex(int index) const; + QString mappingFilePathFromIndex(int index) const; QString askForMappingName(const QString& prefilledName = QString()) const; void applyMappingChanges(); bool saveMapping(); diff --git a/src/controllers/dlgprefcontrollerdlg.ui b/src/controllers/dlgprefcontrollerdlg.ui index 1d391eb06aa..9b296011d89 100644 --- a/src/controllers/dlgprefcontrollerdlg.ui +++ b/src/controllers/dlgprefcontrollerdlg.ui @@ -29,110 +29,8 @@ Controller Setup - - - 0 - 0 - - - - - - - true - - - - 0 - 0 - - - - - 14 - 75 - true - - - - Controller Name - - - - - - - true - - - - 0 - 0 - - - - - - - (device category goes here) - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Enabled - - - - - - - Click to start the Controller Learning wizard. - - - - - - Learning Wizard (MIDI Only) - - - false - - - false - - - - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Load Mapping: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - comboBoxMapping - - - - + + @@ -191,6 +89,53 @@ + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Load Mapping: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + comboBoxMapping + + + + + + + true + + + + 0 + 0 + + + + + + + (device category goes here) + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + @@ -207,6 +152,71 @@ + + + + Click to start the Controller Learning wizard. + + + + + + Learning Wizard (MIDI Only) + + + false + + + false + + + + + + + true + + + + 0 + 0 + + + + + 14 + 75 + true + + + + Controller Name + + + + + + + Enabled + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 20 + 40 + + + + @@ -421,18 +431,22 @@ - - - - Qt::Vertical + + + + + 0 + 0 + - - - 20 - 40 - + + Mapping settings - + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + @@ -525,7 +539,6 @@ - Output Mappings diff --git a/src/controllers/hid/hidiooutputreport.cpp b/src/controllers/hid/hidiooutputreport.cpp index d573eb79b44..26fdc4ef79a 100644 --- a/src/controllers/hid/hidiooutputreport.cpp +++ b/src/controllers/hid/hidiooutputreport.cpp @@ -2,6 +2,7 @@ #include +#include "util/cmdlineargs.h" #include "util/compatibility/qbytearray.h" #include "util/runtimeloggingcategory.h" #include "util/string.h" @@ -34,7 +35,9 @@ void HidIoOutputReport::updateCachedData(const QByteArray& data, m_lastCachedDataSize = data.size(); } else { - if (m_possiblyUnsentDataCached && !useNonSkippingFIFO) { + if (CmdlineArgs::Instance() + .getControllerDebug() && + m_possiblyUnsentDataCached && !useNonSkippingFIFO) { qCDebug(logOutput) << "t:" << mixxx::Time::elapsed().formatMillisWithUnit() << "skipped superseded OutputReport data for ReportID" << m_reportId; @@ -95,9 +98,13 @@ bool HidIoOutputReport::sendCachedData(QMutex* pHidDeviceAndPollMutex, cacheLock.unlock(); - qCDebug(logOutput) << "t:" << startOfHidWrite.formatMillisWithUnit() - << "Skipped sending identical OutputReport data from cache for ReportID" - << m_reportId; + if (CmdlineArgs::Instance() + .getControllerDebug()) { + qCDebug(logOutput) << "t:" << startOfHidWrite.formatMillisWithUnit() + << "Skipped sending identical OutputReport data " + "from cache for ReportID" + << m_reportId; + } // Return with false, to signal the caller, that no time consuming IO operation was necessary return false; @@ -144,11 +151,14 @@ bool HidIoOutputReport::sendCachedData(QMutex* pHidDeviceAndPollMutex, return true; } - qCDebug(logOutput) << "t:" << startOfHidWrite.formatMillisWithUnit() << " " - << result << "bytes ( including ReportID of" - << static_cast(m_reportId) - << ") sent from skipping cache - Needed:" - << (mixxx::Time::elapsed() - startOfHidWrite).formatMicrosWithUnit(); + if (CmdlineArgs::Instance() + .getControllerDebug()) { + qCDebug(logOutput) << "t:" << startOfHidWrite.formatMillisWithUnit() << " " + << result << "bytes ( including ReportID of" + << static_cast(m_reportId) + << ") sent from skipping cache - Needed:" + << (mixxx::Time::elapsed() - startOfHidWrite).formatMicrosWithUnit(); + } // Return with true, to signal the caller, that the time consuming hid_write operation was executed return true; diff --git a/src/controllers/hid/legacyhidcontrollermappingfilehandler.cpp b/src/controllers/hid/legacyhidcontrollermappingfilehandler.cpp index 1b715ea8cd6..d33506d567e 100644 --- a/src/controllers/hid/legacyhidcontrollermappingfilehandler.cpp +++ b/src/controllers/hid/legacyhidcontrollermappingfilehandler.cpp @@ -24,6 +24,7 @@ LegacyHidControllerMappingFileHandler::load(const QDomElement& root, auto pMapping = std::make_shared(); pMapping->setFilePath(filePath); parseMappingInfo(root, pMapping); + parseMappingSettings(root, pMapping.get()); addScriptFilesToMapping(controller, pMapping, systemMappingsPath); return pMapping; } diff --git a/src/controllers/legacycontrollermapping.cpp b/src/controllers/legacycontrollermapping.cpp new file mode 100644 index 00000000000..198487fa2f7 --- /dev/null +++ b/src/controllers/legacycontrollermapping.cpp @@ -0,0 +1,93 @@ +#include "controllers/legacycontrollermapping.h" + +#include + +namespace { +const QString kControllerSettingsPreferenceGroupKey = QStringLiteral("[ControllerSettings_%1_%2]"); +const QString kControllerSettingsSettingPathSubst = QStringLiteral("%SETTING_PATH"); +const QString kControllerSettingsResourcePathSubst = QStringLiteral("%RESOURCE_PATH"); +} // anonymous namespace + +void LegacyControllerMapping::loadSettings(UserSettingsPointer pConfig, + const QString& controllerName) const { + auto mappingFile = QFileInfo(m_filePath); + DEBUG_ASSERT(mappingFile.exists()); + QString controllerPath = + mappingFile.absoluteFilePath() + .replace(pConfig->getSettingsPath(), + kControllerSettingsSettingPathSubst) + .replace(pConfig->getResourcePath(), + kControllerSettingsResourcePathSubst); + + QString controllerKey = QString(kControllerSettingsPreferenceGroupKey) + .arg(controllerName, controllerPath); + + auto availableSettings = getSettings(); + QList definedSettings = pConfig->getKeysWithGroup(controllerKey); + + QList availableSettingKeys; + for (const auto& pSetting : std::as_const(availableSettings)) { + availableSettingKeys.append(pSetting->variableName()); + } + + bool ok; + for (const auto& key : definedSettings) { + if (!availableSettingKeys.contains(key.item)) { + qDebug() << "The setting" << key.item + << "does not seem to exist in the mapping" << mappingFile.absoluteFilePath() + << ". It may be invalid or may have been removed."; + pConfig->remove(key); + continue; + } + const auto& pSetting = availableSettings.at(availableSettingKeys.indexOf(key.item)); + QString value = pConfig->getValueString(key); + if (!pSetting->valid()) { + qWarning() << "The setting" << pSetting->variableName() + << "for the mapping" << mappingFile.absoluteFilePath() + << "appears to be invalid. Its saved value won't be restored."; + } + pSetting->parse(value, &ok); + if (!ok || !pSetting->valid()) { + qWarning() << "The setting" << pSetting->variableName() + << "for the mapping" << mappingFile.absoluteFilePath() + << "could not be restore. Removing and resetting the setting default value."; + pConfig->remove(key); + pSetting->reset(); + } + } +} + +void LegacyControllerMapping::resetSettings() { + for (auto setting : getSettings()) { + setting->reset(); + } +} + +void LegacyControllerMapping::saveSettings(UserSettingsPointer pConfig, + const QString& controllerName) const { + auto mappingFile = QFileInfo(m_filePath); + DEBUG_ASSERT(mappingFile.exists()); + QString controllerPath = + mappingFile.absoluteFilePath() + .replace(pConfig->getSettingsPath(), kControllerSettingsSettingPathSubst) + .replace(pConfig->getResourcePath(), kControllerSettingsResourcePathSubst); + QString controllerKey = QString(kControllerSettingsPreferenceGroupKey) + .arg(controllerName, controllerPath); + for (auto setting : getSettings()) { + if (!setting->isDirty()) { + continue; + } + setting->save(); + if (!setting->valid()) { + qWarning() << "Setting" << setting->variableName() + << "for controller" << controllerName + << "is invalid. Its value will not be saved."; + continue; + } + if (setting->isDefault()) { + pConfig->remove(ConfigKey(controllerKey, setting->variableName())); + } else { + pConfig->set(ConfigKey(controllerKey, setting->variableName()), setting->stringify()); + } + } +} diff --git a/src/controllers/legacycontrollermapping.h b/src/controllers/legacycontrollermapping.h index ca0e369b96c..45fe89df260 100644 --- a/src/controllers/legacycontrollermapping.h +++ b/src/controllers/legacycontrollermapping.h @@ -1,21 +1,41 @@ #pragma once -#include +#include + #include -#include -#include -#include -#include -#include +#include +#include "controllers/legacycontrollersettings.h" +#include "controllers/legacycontrollersettingslayout.h" #include "defs_urls.h" +#include "preferences/usersettings.h" /// This class represents a controller mapping, containing the data elements that /// make it up. class LegacyControllerMapping { public: LegacyControllerMapping() - : m_bDirty(false) { + : m_bDirty(false), + m_deviceDirection(DeviceDirection::Bidirectionnal) { + } + LegacyControllerMapping(const LegacyControllerMapping& other) + : m_productMatches(other.m_productMatches), + m_bDirty(other.m_bDirty), + m_deviceId(other.m_deviceId), + m_filePath(other.m_filePath), + m_name(other.m_name), + m_author(other.m_author), + m_description(other.m_description), + m_forumlink(other.m_forumlink), + m_manualPage(other.m_manualPage), + m_wikilink(other.m_wikilink), + m_schemaVersion(other.m_schemaVersion), + m_mixxxVersion(other.m_mixxxVersion), + m_settings(other.m_settings), + m_settingsLayout(other.m_settingsLayout.get() != nullptr + ? other.m_settingsLayout->clone() + : nullptr), + m_scripts(other.m_scripts) { } virtual ~LegacyControllerMapping() = default; @@ -32,6 +52,19 @@ class LegacyControllerMapping { bool builtin; }; + // TODO (xxx): this is a temporary solution to address devices that don't + // support and need bidirectional communication and lead to + // polling/performance issues. The proper solution would involve refactoring + // the bulk integration to perform a better endpoint capability discovery + // and let Mixxx decide communication direction depending of the hardware + // capabilities + enum class DeviceDirection : uint8_t { + Outgoing = 0x1, + Incoming = 0x2, + Bidirectionnal = 0x3 + }; + Q_DECLARE_FLAGS(DeviceDirections, DeviceDirection) + /// Adds a script file to the list of controller scripts for this mapping. /// @param filename Name of the script file to add /// @param functionprefix The script's function prefix (or empty string) @@ -50,50 +83,106 @@ class LegacyControllerMapping { setDirty(true); } + /// Adds a setting option to the list of setting option for this mapping. + /// The option added must be a valid option. + /// @param option The option to add + /// @return whether or not the setting was added successfully. + bool addSetting(std::shared_ptr option) { + VERIFY_OR_DEBUG_ASSERT(option->valid()) { + return false; + } + for (const auto& setting : std::as_const(m_settings)) { + if (*setting == *option) { + qWarning() << "Mapping setting duplication detected for " + "setting with name" + << option->variableName() + << ". Keeping the first occurrence."; + return false; + } + } + m_settings.append(option); + return true; + } + + /// @brief Set a setting layout as they should be perceived when edited in + /// the preference dialog. + /// @param layout The layout root element + void setSettingLayout(std::unique_ptr&& layout) { + VERIFY_OR_DEBUG_ASSERT(layout.get()) { + return; + } + m_settingsLayout = std::move(layout); + } + const QList& getScriptFiles() const { return m_scripts; } - inline void setDirty(bool bDirty) { + const QList>& getSettings() const { + return m_settings; + } + + bool hasDirtySettings() const { + for (const auto& setting : m_settings) { + if (setting->isDirty()) { + return true; + } + } + return false; + } + + LegacyControllerSettingsLayoutElement* getSettingsLayout() { + return m_settingsLayout.get(); + } + + void setDeviceDirection(DeviceDirections aDeviceDirection) { + m_deviceDirection = aDeviceDirection; + } + + DeviceDirections getDeviceDirection() const { + return m_deviceDirection; + } + + void setDirty(bool bDirty) { m_bDirty = bDirty; } - inline bool isDirty() const { + bool isDirty() const { return m_bDirty; } - inline void setDeviceId(const QString& id) { + void setDeviceId(const QString& id) { m_deviceId = id; setDirty(true); } - inline QString deviceId() const { + QString deviceId() const { return m_deviceId; } - inline void setFilePath(const QString& filePath) { + void setFilePath(const QString& filePath) { m_filePath = filePath; setDirty(true); } - inline QString filePath() const { + QString filePath() const { return m_filePath; } - inline QDir dirPath() const { + QDir dirPath() const { return QFileInfo(filePath()).absoluteDir(); } - inline void setName(const QString& name) { + void setName(const QString& name) { m_name = name; setDirty(true); } - inline QString name() const { + QString name() const { return m_name; } - inline void setAuthor(const QString& author) { + void setAuthor(const QString& author) { m_author = author; setDirty(true); } @@ -174,6 +263,12 @@ class LegacyControllerMapping { virtual bool isMappable() const = 0; + void loadSettings(UserSettingsPointer pConfig, + const QString& controllerName) const; + void saveSettings(UserSettingsPointer pConfig, + const QString& controllerName) const; + void resetSettings(); + // Optional list of controller device match details QList> m_productMatches; @@ -191,5 +286,8 @@ class LegacyControllerMapping { QString m_schemaVersion; QString m_mixxxVersion; + QList> m_settings; + std::unique_ptr m_settingsLayout; QList m_scripts; + DeviceDirections m_deviceDirection; }; diff --git a/src/controllers/legacycontrollermappingfilehandler.cpp b/src/controllers/legacycontrollermappingfilehandler.cpp index 754c972ba78..b8c6ab94321 100644 --- a/src/controllers/legacycontrollermappingfilehandler.cpp +++ b/src/controllers/legacycontrollermappingfilehandler.cpp @@ -45,20 +45,20 @@ std::shared_ptr LegacyControllerMappingFileHandler::loa return nullptr; } - LegacyControllerMappingFileHandler* pHandler = nullptr; + std::unique_ptr pHandler; if (mappingFile.fileName().endsWith( MIDI_MAPPING_EXTENSION, Qt::CaseInsensitive)) { - pHandler = new LegacyMidiControllerMappingFileHandler(); + pHandler = std::make_unique(); } else if (mappingFile.fileName().endsWith( HID_MAPPING_EXTENSION, Qt::CaseInsensitive) || mappingFile.fileName().endsWith( BULK_MAPPING_EXTENSION, Qt::CaseInsensitive)) { #ifdef __HID__ - pHandler = new LegacyHidControllerMappingFileHandler(); + pHandler = std::make_unique(); #endif } - if (pHandler == nullptr) { + if (!pHandler) { qDebug() << "Mapping" << mappingFile.absoluteFilePath() << "has an unrecognized extension."; return nullptr; @@ -66,6 +66,7 @@ std::shared_ptr LegacyControllerMappingFileHandler::loa std::shared_ptr pMapping = pHandler->load( mappingFile.absoluteFilePath(), systemMappingsPath); + if (pMapping) { pMapping->setDirty(false); } @@ -108,6 +109,85 @@ void LegacyControllerMappingFileHandler::parseMappingInfo( mapping->setWikiLink(wiki.isNull() ? "" : wiki.text()); } +void LegacyControllerMappingFileHandler::parseMappingSettings( + const QDomElement& root, LegacyControllerMapping* mapping) const { + if (root.isNull() || !mapping) { + return; + } + + QDomElement settings = root.firstChildElement("settings"); + if (settings.isNull()) { + return; + } + + std::unique_ptr settingLayout = + std::make_unique( + LegacyControllerSettingsLayoutContainer::Disposition:: + VERTICAL); + parseMappingSettingsElement(settings, mapping, settingLayout.get()); + mapping->setSettingLayout(std::move(settingLayout)); +} + +void LegacyControllerMappingFileHandler::parseMappingSettingsElement( + const QDomElement& current, + LegacyControllerMapping* pMapping, + LegacyControllerSettingsLayoutContainer* pLayout) + const { + for (QDomElement element = current.firstChildElement(); + !element.isNull(); + element = element.nextSiblingElement()) { + const QString& tagName = element.tagName().toLower(); + if (tagName == "option") { + std::shared_ptr pSetting( + LegacyControllerSettingBuilder::build(element)); + if (pSetting.get() == nullptr) { + qDebug() << "Ignoring unsupported controller setting in file" + << pMapping->filePath() << "at line" + << element.lineNumber() << "."; + continue; + } + if (!pSetting->valid()) { + qDebug() << "The parsed setting in file" << pMapping->filePath() + << "at line" << element.lineNumber() + << "appears to be invalid. It will be ignored."; + continue; + } + if (pMapping->addSetting(pSetting)) { + pLayout->addItem(pSetting); + } else { + qDebug() << "The parsed setting in file" << pMapping->filePath() + << "at line" << element.lineNumber() + << "couldn't be added. Its layout information will also be ignored."; + continue; + } + } else if (tagName == "row") { + LegacyControllerSettingsLayoutContainer::Disposition orientation = + element.attribute("orientation").trimmed().toLower() == + "vertical" + ? LegacyControllerSettingsLayoutContainer::VERTICAL + : LegacyControllerSettingsLayoutContainer::HORIZONTAL; + std::unique_ptr row = + std::make_unique( + LegacyControllerSettingsLayoutContainer::HORIZONTAL, + orientation); + parseMappingSettingsElement(element, pMapping, row.get()); + pLayout->addItem(std::move(row)); + } else if (tagName == "group") { + std::unique_ptr group = + std::make_unique( + element.attribute("label")); + parseMappingSettingsElement(element, pMapping, group.get()); + pLayout->addItem(std::move(group)); + } else { + qDebug() << "Ignoring unsupported tag" << tagName + << "in file" << pMapping->filePath() + << "on line" << element.lineNumber() + << "for controller layout settings. Check the documentation supported tags."; + continue; + } + } +} + QDomElement LegacyControllerMappingFileHandler::getControllerNode( const QDomElement& root) { if (root.isNull()) { @@ -131,6 +211,17 @@ void LegacyControllerMappingFileHandler::addScriptFilesToMapping( QString deviceId = controller.attribute("id", ""); mapping->setDeviceId(deviceId); + // See TODO in LegacyControllerMapping::DeviceDirection - `direction` should + // only be used as a workaround till the bulk integration gets refactored + QString deviceDirection = controller.attribute("direction", "").toLower(); + if (deviceDirection == "in") { + mapping->setDeviceDirection(LegacyControllerMapping::DeviceDirection::Incoming); + } else if (deviceDirection == "out") { + mapping->setDeviceDirection(LegacyControllerMapping::DeviceDirection::Outgoing); + } else { + mapping->setDeviceDirection(LegacyControllerMapping::DeviceDirection::Bidirectionnal); + } + // Build a list of script files to load QDomElement scriptFile = controller.firstChildElement("scriptfiles") .firstChildElement("file"); @@ -193,7 +284,7 @@ QDomDocument LegacyControllerMappingFileHandler::buildRootWithScripts( QString blank = "\n" "\n" - "\n"; + "\n"; doc.setContent(blank); QDomElement rootNode = doc.documentElement(); diff --git a/src/controllers/legacycontrollermappingfilehandler.h b/src/controllers/legacycontrollermappingfilehandler.h index c7bc6377b6d..73e5eea584c 100644 --- a/src/controllers/legacycontrollermappingfilehandler.h +++ b/src/controllers/legacycontrollermappingfilehandler.h @@ -7,6 +7,7 @@ class QFileInfo; class QDir; class LegacyControllerMapping; +class LegacyControllerSettingsLayoutContainer; /// The LegacyControllerMappingFileHandler is used for serializing/deserializing the /// LegacyControllerMapping objects to/from XML files and is also responsible @@ -41,6 +42,12 @@ class LegacyControllerMappingFileHandler { void parseMappingInfo(const QDomElement& root, std::shared_ptr mapping) const; + /// @brief Parse the setting definition block from the root node if any. + /// @param root The root node (MixxxControllerPreset) + /// @param mapping The mapping object to populate with the gathered data + void parseMappingSettings(const QDomElement& root, + LegacyControllerMapping* mapping) const; + /// Adds script files from XML to the LegacyControllerMapping. /// /// This function parses the supplied QDomElement structure, finds the @@ -61,8 +68,21 @@ class LegacyControllerMappingFileHandler { bool writeDocument(const QDomDocument& root, const QString& fileName) const; private: + /// @brief Recursively parse setting definition and layout information + /// within a setting node + /// @param current The setting node (MixxxControllerPreset.settings) or any + /// children nodes + /// @param mapping The mapping object to populate with the gathered data + /// @param layout The currently active layout, on which new setting item + /// (leaf) should be attached + void parseMappingSettingsElement(const QDomElement& current, + LegacyControllerMapping* pMapping, + LegacyControllerSettingsLayoutContainer* pLayout) const; + // Sub-classes implement this. virtual std::shared_ptr load(const QDomElement& root, const QString& filePath, const QDir& systemMappingPath) = 0; + + friend class LegacyControllerMappingSettingsTest_parseSettingBlock_Test; }; diff --git a/src/controllers/legacycontrollersettings.cpp b/src/controllers/legacycontrollersettings.cpp new file mode 100644 index 00000000000..918e5ea9c99 --- /dev/null +++ b/src/controllers/legacycontrollersettings.cpp @@ -0,0 +1,250 @@ +#include "controllers/legacycontrollersettings.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include "moc_legacycontrollersettings.cpp" + +LegacyControllerSettingBuilder* LegacyControllerSettingBuilder::instance() { + static LegacyControllerSettingBuilder* s_self = nullptr; + + if (s_self == nullptr) { + s_self = new LegacyControllerSettingBuilder(); + } + + return s_self; +} + +LegacyControllerSettingBuilder::LegacyControllerSettingBuilder() { + // Each possible setting types must be added there. This will allow the + // builder to know each type of supported setting + registerType(); + registerType(); + registerType(); + registerType(); +} + +AbstractLegacyControllerSetting::AbstractLegacyControllerSetting(const QDomElement& element) { + m_variableName = element.attribute("variable").trimmed(); + m_label = element.attribute("label", m_variableName).trimmed(); + + QDomElement description = element.firstChildElement("description"); + if (!description.isNull()) { + m_description = description.text().trimmed(); + } +} + +QWidget* AbstractLegacyControllerSetting::buildWidget(QWidget* pParent, + LegacyControllerSettingsLayoutContainer::Disposition orientation) { + auto pRoot = make_parented(pParent); + QBoxLayout* pLayout = new QBoxLayout(QBoxLayout::LeftToRight); + + pLayout->setContentsMargins(0, 0, 0, 0); + + if (orientation == LegacyControllerSettingsLayoutContainer::VERTICAL) { + auto* pSettingsContainer = dynamic_cast(pParent); + if (pSettingsContainer) { + connect(pSettingsContainer, + &WLegacyControllerSettingsContainer::orientationChanged, + this, + [pLayout, pParent]( + LegacyControllerSettingsLayoutContainer::Disposition + disposition) { + pLayout->setDirection(disposition == + LegacyControllerSettingsLayoutContainer:: + HORIZONTAL + ? QBoxLayout::TopToBottom + : QBoxLayout::LeftToRight); + pParent->layout()->invalidate(); + }); + } + } + + auto pLabelWidget = make_parented(pRoot); + pLabelWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); + pLabelWidget->setText(label()); + + if (!description().isEmpty()) { + pRoot->setToolTip(QString("

    %1

    ").arg(description())); + } + + pLayout->addWidget(pLabelWidget); + pLayout->addWidget(buildInputWidget(pRoot)); + + pLayout->setStretch(0, 3); + pLayout->setStretch(1, 1); + + pRoot->setLayout(pLayout); + + return pRoot; +} + +LegacyControllerBooleanSetting::LegacyControllerBooleanSetting( + const QDomElement& element) + : AbstractLegacyControllerSetting(element) { + m_defaultValue = parseValue(element.attribute("default")); + m_savedValue = m_defaultValue; + m_editedValue = m_defaultValue; +} + +QWidget* LegacyControllerBooleanSetting::buildWidget( + QWidget* pParent, LegacyControllerSettingsLayoutContainer::Disposition) { + return buildInputWidget(pParent); +} + +QWidget* LegacyControllerBooleanSetting::buildInputWidget(QWidget* pParent) { + auto pWidget = make_parented(pParent); + + auto* pCheckBox = new QCheckBox(pWidget); + pCheckBox->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed); + if (m_editedValue) { + pCheckBox->setCheckState(Qt::Checked); + } + + if (!description().isEmpty()) { + pCheckBox->setToolTip(QString("

    %1

    ").arg(description())); + } + + connect(this, &AbstractLegacyControllerSetting::valueReset, pCheckBox, [this, &pCheckBox]() { + pCheckBox->setCheckState(m_editedValue ? Qt::Checked : Qt::Unchecked); + }); + + connect(pCheckBox, &QCheckBox::stateChanged, this, [this](int state) { + m_editedValue = state == Qt::Checked; + emit changed(); + }); + + auto pLabelWidget = make_parented(pWidget); + pLabelWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); + pLabelWidget->setText(label()); + + QBoxLayout* pLayout = new QHBoxLayout(); + + pLayout->addWidget(pCheckBox); + pLayout->addWidget(pLabelWidget); + + pLayout->setStretch(0, 3); + pLayout->setStretch(1, 1); + + pWidget->setLayout(pLayout); + + return pWidget; +} + +bool LegacyControllerBooleanSetting::match(const QDomElement& element) { + return element.hasAttribute("type") && + QString::compare(element.attribute("type"), + "boolean", + Qt::CaseInsensitive) == 0; +} + +template ValueSerializer, + Deserializer ValueDeserializer, + class InputWidget> +QWidget* LegacyControllerNumberSetting::buildInputWidget(QWidget* pParent) { + auto* pSpinBox = new InputWidget(pParent); + pSpinBox->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); + + pSpinBox->setRange(m_minValue, m_maxValue); + pSpinBox->setSingleStep(m_stepValue); + pSpinBox->setValue(m_editedValue); + + connect(this, &AbstractLegacyControllerSetting::valueReset, pSpinBox, [this, pSpinBox]() { + pSpinBox->setValue(m_editedValue); + }); + + connect(pSpinBox, + QOverload::of(&InputWidget::valueChanged), + this, + [this](SettingType value) { + m_editedValue = value; + emit changed(); + }); + + return pSpinBox; +} + +QWidget* LegacyControllerRealSetting::buildInputWidget(QWidget* pParent) { + QDoubleSpinBox* spinBox = dynamic_cast( + LegacyControllerNumberSetting::buildInputWidget(pParent)); + VERIFY_OR_DEBUG_ASSERT(spinBox != nullptr) { + qWarning() << "Unable to set precision on the input widget " + "input. It does not appear to be a valid QDoubleSpinBox"; + return spinBox; + } + spinBox->setDecimals(m_precisionValue); + + return spinBox; +} + +LegacyControllerEnumSetting::LegacyControllerEnumSetting( + const QDomElement& element) + : AbstractLegacyControllerSetting(element), m_options(), m_defaultValue(0) { + size_t pos = 0; + for (QDomElement value = element.firstChildElement("value"); + !value.isNull(); + value = value.nextSiblingElement("value")) { + QString val = value.text(); + m_options.append(std::tuple(val, value.attribute("label", val))); + if (value.hasAttribute("default")) { + m_defaultValue = pos; + } + pos++; + } + reset(); + save(); +} + +void LegacyControllerEnumSetting::parse(const QString& in, bool* ok) { + if (ok != nullptr) { + *ok = false; + } + reset(); + save(); + + size_t pos = 0; + for (const auto& value : std::as_const(m_options)) { + if (std::get<0>(value) == in) { + if (ok != nullptr) { + *ok = true; + } + m_savedValue = pos; + m_editedValue = m_savedValue; + return; + } + pos++; + } +} + +QWidget* LegacyControllerEnumSetting::buildInputWidget(QWidget* pParent) { + auto* pComboBox = new QComboBox(pParent); + + for (const auto& value : std::as_const(m_options)) { + pComboBox->addItem(std::get<1>(value)); + } + pComboBox->setCurrentIndex(static_cast(m_editedValue)); + + connect(this, &AbstractLegacyControllerSetting::valueReset, pComboBox, [this, &pComboBox]() { + pComboBox->setCurrentIndex(static_cast(m_editedValue)); + }); + + connect(pComboBox, + QOverload::of(&QComboBox::currentIndexChanged), + this, + [this](int selected) { + m_editedValue = selected; + emit changed(); + }); + + return pComboBox; +} diff --git a/src/controllers/legacycontrollersettings.h b/src/controllers/legacycontrollersettings.h new file mode 100644 index 00000000000..b7d2f378d9c --- /dev/null +++ b/src/controllers/legacycontrollersettings.h @@ -0,0 +1,457 @@ +#pragma once + +#include + +#include "controllers/legacycontrollersettingsfactory.h" +#include "controllers/legacycontrollersettingslayout.h" +#include "util/parented_ptr.h" + +class QSpinBox; +class QDoubleSpinBox; + +/// @brief The abstract controller setting. Any type of setting will have to +/// implement this base class +class AbstractLegacyControllerSetting : public QObject { + Q_OBJECT + public: + ~AbstractLegacyControllerSetting() override = default; + + /// @brief Build a widget that can be used to interact with this setting. It + /// shouldn't mutate the state of the setting. + /// @param parent The parent widget for which this widget is being created. + /// The parent widget will own the newly created widget + /// @return a new widget + virtual QWidget* buildWidget(QWidget* parent, + LegacyControllerSettingsLayoutContainer::Disposition orientation = + LegacyControllerSettingsLayoutContainer::HORIZONTAL); + + /// @brief Build a JSValue with the current setting value. The JSValue + /// variant will use the appropriate type + /// @return A QJSValue with the current value + virtual QJSValue value() const = 0; + + /// @brief Serialize the current value in a string format + /// @return A String with current setting value + virtual QString stringify() const = 0; + + /// @brief Parse a string that contains the value in a compatible format. + /// @param in The string containing the data + /// @param ok A pointer to a boolean in which the result of the + /// deserialisation will be stored (true means the operation was successful) + virtual void parse(const QString&, bool*) = 0; + + /// @brief Indicate if the setting is currently not using a user-specified value + /// @return Whether or not the setting is currently set to its default value + virtual bool isDefault() const = 0; + + /// @brief Indicate if the setting is currently being mutated and if the + /// edited value is different than its its currently known value. This would + /// indicate that the user may need to save the changes or acknowledge + /// otherwise. + /// @return Whether or not the setting value is dirty + virtual bool isDirty() const = 0; + + /// @brief Commit the the edited value to become the currently known value. + /// Note that if `isDirty() == false`, this have no effect + virtual void save() = 0; + + /// @brief Reset the current value, as well as the editing value to use the + /// default, as specified in the spec + virtual void reset() = 0; + + /// @brief Whether of not this setting definition is valid. Validity scope + /// includes things like default value within range' for example. + /// @return true if valid + virtual bool valid() const { + return !m_variableName.isEmpty(); + } + + /// @brief The variable name as perceived within the mapping definition. + /// @return a string + QString variableName() const { + return m_variableName; + } + + /// @brief The user-friendly label to be display in the UI + /// @return a string + const QString& label() const { + return m_label; + } + + /// @brief A description of what this setting does + /// @return a string + const QString& description() const { + return m_description; + } + + bool operator==(const AbstractLegacyControllerSetting& other) const noexcept { + return variableName() == other.variableName(); + } + + protected: + AbstractLegacyControllerSetting(const QString& variableName, + const QString& label, + const QString& description) + : m_variableName(variableName), + m_label(label), + m_description(description) { + } + AbstractLegacyControllerSetting(const QDomElement& element); + + virtual QWidget* buildInputWidget(QWidget* parent) = 0; + + signals: + /// This signal will be emitted when the user has interacted with the + /// setting and changed its value + void changed(); + /// This signal will be emitted when the user has requested a value reset + void valueReset(); + + private: + QString m_variableName; + QString m_label; + QString m_description; +}; + +class LegacyControllerBooleanSetting + : public LegacyControllerSettingFactory, + public AbstractLegacyControllerSetting { + public: + LegacyControllerBooleanSetting(const QDomElement& element); + + virtual ~LegacyControllerBooleanSetting() = default; + + QWidget* buildWidget(QWidget* parent, + LegacyControllerSettingsLayoutContainer::Disposition orientation = + LegacyControllerSettingsLayoutContainer::HORIZONTAL) + override; + + QJSValue value() const override { + return QJSValue(m_savedValue); + } + + QString stringify() const override { + return m_savedValue ? "true" : "false"; + } + void parse(const QString& in, bool* ok = nullptr) override { + if (ok != nullptr) + *ok = true; + m_savedValue = parseValue(in); + m_editedValue = m_savedValue; + } + bool isDefault() const override { + return m_savedValue == m_defaultValue; + } + bool isDirty() const override { + return m_savedValue != m_editedValue; + } + + virtual void save() override { + m_savedValue = m_editedValue; + } + + virtual void reset() override { + m_editedValue = m_defaultValue; + emit valueReset(); + } + + static AbstractLegacyControllerSetting* createFrom(const QDomElement& element) { + return new LegacyControllerBooleanSetting(element); + } + static bool match(const QDomElement& element); + + protected: + LegacyControllerBooleanSetting(const QDomElement& element, + bool currentValue, + bool defaultValue) + : AbstractLegacyControllerSetting(element), + m_savedValue(currentValue), + m_defaultValue(defaultValue) { + } + + bool parseValue(const QString& in) { + return QString::compare(in, "true", Qt::CaseInsensitive) == 0 || in == "1"; + } + + virtual QWidget* buildInputWidget(QWidget* parent) override; + + private: + bool m_savedValue; + bool m_defaultValue; + bool m_editedValue; + + friend class LegacyControllerMappingSettingsTest_booleanSettingEditing_Test; +}; + +template +using Serializer = QString (*)(const SettingType&); + +template +using Deserializer = SettingType (*)(const QString&, bool*); + +template ValueSerializer, + Deserializer ValueDeserializer, + class InputWidget> +class LegacyControllerNumberSetting + : public LegacyControllerSettingFactory< + LegacyControllerNumberSetting>, + public AbstractLegacyControllerSetting { + public: + LegacyControllerNumberSetting(const QDomElement& element) + : AbstractLegacyControllerSetting(element) { + bool isOk = false; + m_minValue = ValueDeserializer(element.attribute("min"), &isOk); + if (!isOk) { + m_minValue = std::numeric_limits::min(); + } + m_maxValue = ValueDeserializer(element.attribute("max"), &isOk); + if (!isOk) { + m_maxValue = std::numeric_limits::max(); + } + m_stepValue = ValueDeserializer(element.attribute("step"), &isOk); + if (!isOk) { + m_stepValue = 1; + } + m_defaultValue = ValueDeserializer(element.attribute("default"), &isOk); + if (!isOk) { + m_defaultValue = 0; + } + reset(); + save(); + } + + virtual ~LegacyControllerNumberSetting() = default; + + QJSValue value() const override { + return QJSValue(m_savedValue); + } + + QString stringify() const override { + return ValueSerializer(m_savedValue); + } + void parse(const QString& in, bool* ok) override { + m_savedValue = ValueDeserializer(in, ok); + m_editedValue = m_savedValue; + } + + bool isDefault() const override { + return m_savedValue == m_defaultValue; + } + bool isDirty() const override { + return m_savedValue != m_editedValue; + } + + virtual void save() override { + m_savedValue = m_editedValue; + } + + virtual void reset() override { + m_editedValue = m_defaultValue; + emit valueReset(); + } + + /// @brief Whether of not this setting definition and its current state are + /// valid. Validity scope includes default/current/dirty value within range + /// and a strictly positive step, strictly less than max.. + /// @return true if valid + bool valid() const override { + return AbstractLegacyControllerSetting::valid() && + m_defaultValue >= m_minValue && m_savedValue >= m_minValue && + m_editedValue >= m_minValue && m_defaultValue <= m_maxValue && + m_savedValue <= m_maxValue && m_editedValue <= m_maxValue && + m_stepValue > 0 && m_stepValue < m_maxValue; + } + + static AbstractLegacyControllerSetting* createFrom(const QDomElement& element) { + return new LegacyControllerNumberSetting(element); + } + static bool match(const QDomElement& element); + + protected: + LegacyControllerNumberSetting(const QDomElement& element, + SettingType currentValue, + SettingType defaultValue, + SettingType minValue, + SettingType maxValue, + SettingType stepValue) + : AbstractLegacyControllerSetting(element), + m_savedValue(currentValue), + m_defaultValue(defaultValue), + m_minValue(minValue), + m_maxValue(maxValue), + m_stepValue(stepValue) { + } + + virtual QWidget* buildInputWidget(QWidget* parent) override; + + private: + SettingType m_savedValue; + SettingType m_defaultValue; + SettingType m_minValue; + SettingType m_maxValue; + SettingType m_stepValue; + + SettingType m_editedValue; + + friend class LegacyControllerMappingSettingsTest_integerSettingEditing_Test; + friend class LegacyControllerMappingSettingsTest_doubleSettingEditing_Test; +}; + +template +inline bool matchSetting(const QDomElement& element); + +inline int extractSettingIntegerValue(const QString& str, bool* ok = nullptr) { + return str.toInt(ok); +} +inline double extractSettingDoubleValue(const QString& str, bool* ok = nullptr) { + return str.toDouble(ok); +} + +inline QString packSettingIntegerValue(const int& in) { + return QString::number(in); +} +inline QString packSettingDoubleValue(const double& in) { + return QString::number(in); +} + +using LegacyControllerIntegerSetting = LegacyControllerNumberSetting; + +class LegacyControllerRealSetting : public LegacyControllerNumberSetting { + public: + LegacyControllerRealSetting(const QDomElement& element) + : LegacyControllerNumberSetting(element) { + bool isOk = false; + m_precisionValue = element.attribute("precision").toInt(&isOk); + if (!isOk) { + m_precisionValue = 2; + } + } + + static AbstractLegacyControllerSetting* createFrom(const QDomElement& element) { + return new LegacyControllerRealSetting(element); + } + + QWidget* buildInputWidget(QWidget* parent) override; + + private: + int m_precisionValue; +}; + +class LegacyControllerEnumSetting + : public LegacyControllerSettingFactory, + public AbstractLegacyControllerSetting { + public: + LegacyControllerEnumSetting(const QDomElement& element); + + virtual ~LegacyControllerEnumSetting() = default; + + QJSValue value() const override { + return QJSValue(stringify()); + } + + const QList>& options() const { + return m_options; + } + + QString stringify() const override { + return std::get<0>(m_options.value(static_cast(m_savedValue))); + } + void parse(const QString& in, bool* ok) override; + bool isDefault() const override { + return m_savedValue == m_defaultValue; + } + bool isDirty() const override { + return m_savedValue != m_editedValue; + } + + virtual void save() override { + m_savedValue = m_editedValue; + } + + virtual void reset() override { + m_editedValue = m_defaultValue; + emit valueReset(); + } + + /// @brief Whether or not this setting definition and its current state are + /// valid. Validity scope includes a known default/current/dirty option. + /// @return true if valid + bool valid() const override { + return AbstractLegacyControllerSetting::valid() && + static_cast(m_defaultValue) < m_options.size() && + static_cast(m_savedValue) < m_options.size() && + static_cast(m_editedValue) < m_options.size(); + } + + static AbstractLegacyControllerSetting* createFrom(const QDomElement& element) { + return new LegacyControllerEnumSetting(element); + } + static inline bool match(const QDomElement& element); + + protected: + LegacyControllerEnumSetting(const QDomElement& element, + const QList>& options, + size_t currentValue, + size_t defaultValue) + : AbstractLegacyControllerSetting(element), + m_options(options), + m_savedValue(currentValue), + m_defaultValue(defaultValue) { + } + + virtual QWidget* buildInputWidget(QWidget* parent) override; + + private: + // We use a QList instead of QHash here because we want to keep the natural order + QList> m_options; + size_t m_savedValue; + size_t m_defaultValue; + + size_t m_editedValue; + + friend class LegacyControllerMappingSettingsTest_enumSettingEditing_Test; + friend class ControllerS4MK3SettingTest_ensureLibrarySettingValueAndEnumEquals; +}; + +template<> +inline bool matchSetting(const QDomElement& element) { + return element.hasAttribute("type") && + QString::compare(element.attribute("type"), + "integer", + Qt::CaseInsensitive) == 0; +} +template<> +inline bool matchSetting(const QDomElement& element) { + return element.hasAttribute("type") && + QString::compare(element.attribute("type"), + "real", + Qt::CaseInsensitive) == 0; +} + +template ValueSerializer, + Deserializer ValueDeserializer, + class InputWidget> +inline bool LegacyControllerNumberSetting::match(const QDomElement& element) { + return matchSetting(element); +} + +inline bool LegacyControllerEnumSetting::match(const QDomElement& element) { + return element.hasAttribute("type") && + QString::compare(element.attribute("type"), + "enum", + Qt::CaseInsensitive) == 0; +} diff --git a/src/controllers/legacycontrollersettingsfactory.h b/src/controllers/legacycontrollersettingsfactory.h new file mode 100644 index 00000000000..433a8b7495d --- /dev/null +++ b/src/controllers/legacycontrollersettingsfactory.h @@ -0,0 +1,65 @@ +#pragma once + +#include + +#include "controllers/legacycontrollersettingsfactory.h" + +class AbstractLegacyControllerSetting; + +/// @brief This class defines an interface that a controller setting type must +/// implement so it can be used properly by the builder +/// @tparam T The class implementing this interface +template +class LegacyControllerSettingFactory { + inline static LegacyControllerSettingFactory* createFrom(const QDomElement& element) { + return new T(element); + } + inline static bool match(const QDomElement& element) { + return T::match(element); + } +}; + +/// @brief This class is used to dynamically instantiate a controller setting based on its type +class LegacyControllerSettingBuilder { + public: + static LegacyControllerSettingBuilder* instance(); + + /// @brief Register a new type of setting. This method is used by the + /// REGISTER macro, it shouldn't be used directly + /// @param match the match function of the new setting + /// @param creator the creator function of the new setting + /// @return Always true + template + bool registerType() { + m_supportedSettings.append(SupportedSetting{ + &T::match, + &T::createFrom}); + return true; + } + + /// @brief instantiate a new setting from a an XML definition if any valid + /// setting was found. The caller is the owner of the instance + /// @param element The XML element to parse to build the new setting + /// @return an instance if a a supported setting has been found, null + /// otherwise + static AbstractLegacyControllerSetting* build(const QDomElement& element) { + for (const auto& settingType : std::as_const(instance()->m_supportedSettings)) { + if (settingType.matcher(element)) { + return settingType.builder(element); + } + } + + return nullptr; + } + + private: + struct SupportedSetting { + bool (*matcher)(const QDomElement&); + AbstractLegacyControllerSetting* (*builder)(const QDomElement&); + }; + + LegacyControllerSettingBuilder(); + + QList + m_supportedSettings; +}; diff --git a/src/controllers/legacycontrollersettingslayout.cpp b/src/controllers/legacycontrollersettingslayout.cpp new file mode 100644 index 00000000000..483e765ba1f --- /dev/null +++ b/src/controllers/legacycontrollersettingslayout.cpp @@ -0,0 +1,95 @@ + +#include "controllers/legacycontrollersettingslayout.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "controllers/legacycontrollersettings.h" +#include "moc_legacycontrollersettingslayout.cpp" +#include "util/parented_ptr.h" + +namespace { +constexpr int kMinScreenSizeForControllerSettingRow = 960; +} // anonymous namespace + +void LegacyControllerSettingsLayoutContainer::addItem( + std::shared_ptr setting) { + m_elements.push_back(std::make_unique( + setting, m_widgetOrientation)); +} + +QBoxLayout* LegacyControllerSettingsLayoutContainer::buildLayout(QWidget* pParent) const { + auto pLayout = make_parented(QBoxLayout::TopToBottom); + + pParent->setLayout(pLayout); + + return pLayout; +} + +QWidget* LegacyControllerSettingsLayoutContainer::build(QWidget* pParent) { + auto pContainer = make_parented(m_disposition, pParent); + QBoxLayout* pLayout = buildLayout(pContainer); + + pLayout->setContentsMargins(0, 0, 0, 0); + + auto& lastElement = m_elements.back(); + for (auto& element : m_elements) { + auto* pWidget = element->build(pContainer); + pWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + pLayout->addWidget(pWidget); + if (element != lastElement) { + pLayout->addItem(new QSpacerItem( + 10, 10, QSizePolicy::Expanding, QSizePolicy::Fixed)); + } + } + + return pContainer; +} + +QWidget* LegacyControllerSettingsGroup::build(QWidget* pParent) { + auto pContainer = make_parented(m_label, pParent); + QBoxLayout* pLayout = buildLayout(pContainer); + + for (auto& element : m_elements) { + pLayout->addWidget(element->build(pContainer)); + } + + return pContainer; +} + +QWidget* LegacyControllerSettingsLayoutItem::build(QWidget* parent) { + VERIFY_OR_DEBUG_ASSERT(m_setting.get() != nullptr) { + return nullptr; + } + return m_setting->buildWidget(parent, m_preferredOrientation); +} + +void WLegacyControllerSettingsContainer::resizeEvent(QResizeEvent* event) { + if (m_preferredOrientation == LegacyControllerSettingsLayoutContainer::VERTICAL) { + return; + } + + auto* pLayout = dynamic_cast(layout()); + if (pLayout == nullptr) { + return; + } + + if (event->size().width() < kMinScreenSizeForControllerSettingRow && + pLayout->direction() == QBoxLayout::LeftToRight) { + pLayout->setDirection(QBoxLayout::TopToBottom); + pLayout->setSpacing(6); + emit orientationChanged(LegacyControllerSettingsLayoutContainer::VERTICAL); + } else if (event->size().width() >= + kMinScreenSizeForControllerSettingRow && + pLayout->direction() == QBoxLayout::TopToBottom) { + pLayout->setDirection(QBoxLayout::LeftToRight); + pLayout->setSpacing(16); + emit orientationChanged(LegacyControllerSettingsLayoutContainer::HORIZONTAL); + } +} diff --git a/src/controllers/legacycontrollersettingslayout.h b/src/controllers/legacycontrollersettingslayout.h new file mode 100644 index 00000000000..f9b07fb40dc --- /dev/null +++ b/src/controllers/legacycontrollersettingslayout.h @@ -0,0 +1,133 @@ +#pragma once + +#include "defs_urls.h" +#include "preferences/usersettings.h" + +class AbstractLegacyControllerSetting; +class QBoxLayout; + +/// @brief Layout information used for controller setting when rendered in the Preference Dialog +class LegacyControllerSettingsLayoutElement { + public: + LegacyControllerSettingsLayoutElement() { + } + virtual ~LegacyControllerSettingsLayoutElement() = default; + + virtual std::unique_ptr clone() const = 0; + + virtual QWidget* build(QWidget* parent) = 0; +}; + +/// @brief This layout element can hold others element. It is also the one used +/// to represent a `row` in the settings +class LegacyControllerSettingsLayoutContainer : public LegacyControllerSettingsLayoutElement { + public: + /// @brief This is a simplified representation of disposition orientation. This used to + /// define how a container orients its children. This is also used by layout + /// items to decide how the label should be rendered alongside the input + /// widget + enum Disposition { + HORIZONTAL = 0, + VERTICAL, + }; + + LegacyControllerSettingsLayoutContainer( + Disposition disposition = HORIZONTAL, + Disposition widgetOrientation = HORIZONTAL) + : LegacyControllerSettingsLayoutElement(), + m_disposition(disposition), + m_widgetOrientation(widgetOrientation) { + } + LegacyControllerSettingsLayoutContainer(const LegacyControllerSettingsLayoutContainer& other) { + m_elements.reserve(other.m_elements.size()); + for (const auto& e : other.m_elements) + m_elements.push_back(e->clone()); + } + virtual ~LegacyControllerSettingsLayoutContainer() = default; + + virtual std::unique_ptr clone() const override { + return std::make_unique(*this); + } + + /// @brief This helper method allows to add a LegacyControllerSetting + /// directly, without to have it to wrap within an item object. This is + /// helpful as the item that will be create to wrap will be initialised with + /// the right parameters + /// @param setting The controller setting to add to the layout container + void addItem(std::shared_ptr setting); + void addItem(std::unique_ptr&& container) { + m_elements.push_back(std::move(container)); + } + + virtual QWidget* build(QWidget* parent) override; + + protected: + QBoxLayout* buildLayout(QWidget* parent) const; + + Disposition m_disposition; + Disposition m_widgetOrientation; + std::vector> m_elements; +}; + +class LegacyControllerSettingsGroup : public LegacyControllerSettingsLayoutContainer { + public: + LegacyControllerSettingsGroup(const QString& label, + LegacyControllerSettingsLayoutContainer::Disposition disposition = + VERTICAL) + : LegacyControllerSettingsLayoutContainer(disposition), + m_label(label) { + } + virtual ~LegacyControllerSettingsGroup() = default; + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } + + QWidget* build(QWidget* parent) override; + + private: + QString m_label; +}; + +class LegacyControllerSettingsLayoutItem : public LegacyControllerSettingsLayoutElement { + public: + LegacyControllerSettingsLayoutItem( + std::shared_ptr setting, + LegacyControllerSettingsLayoutContainer::Disposition orientation = + LegacyControllerSettingsGroup::HORIZONTAL) + : LegacyControllerSettingsLayoutElement(), + m_setting(setting), + m_preferredOrientation(orientation) { + } + virtual ~LegacyControllerSettingsLayoutItem() = default; + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } + + QWidget* build(QWidget* parent) override; + + private: + std::shared_ptr m_setting; + LegacyControllerSettingsLayoutContainer::Disposition m_preferredOrientation; +}; + +class WLegacyControllerSettingsContainer : public QWidget { + Q_OBJECT + public: + WLegacyControllerSettingsContainer( + LegacyControllerSettingsLayoutContainer::Disposition + preferredOrientation, + QWidget* parent) + : QWidget(parent), m_preferredOrientation(preferredOrientation) { + } + + protected: + void resizeEvent(QResizeEvent* event); + + signals: + void orientationChanged(LegacyControllerSettingsLayoutContainer::Disposition); + + private: + LegacyControllerSettingsLayoutContainer::Disposition m_preferredOrientation; +}; diff --git a/src/controllers/midi/legacymidicontrollermappingfilehandler.cpp b/src/controllers/midi/legacymidicontrollermappingfilehandler.cpp index e0fcea473cf..4735a6a3255 100644 --- a/src/controllers/midi/legacymidicontrollermappingfilehandler.cpp +++ b/src/controllers/midi/legacymidicontrollermappingfilehandler.cpp @@ -11,6 +11,7 @@ std::shared_ptr LegacyMidiControllerMappingFileHandler::load(const QDomElement& root, const QString& filePath, const QDir& systemMappingsPath) { + // TODO (XXX): support for controller settings if (root.isNull()) { return nullptr; } diff --git a/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp b/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp index a5886204ec6..66cb55b4a06 100644 --- a/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp +++ b/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp @@ -99,6 +99,18 @@ void ControllerScriptEngineLegacy::setScriptFiles( m_scriptFiles = scripts; } +void ControllerScriptEngineLegacy::setSettings( + const QList>& settings) { + m_settings.clear(); + for (const auto& pSetting : std::as_const(settings)) { + QString name = pSetting->variableName(); + VERIFY_OR_DEBUG_ASSERT(!name.isEmpty()) { + continue; + } + m_settings[name] = pSetting->value(); + } +} + bool ControllerScriptEngineLegacy::initialize() { if (!ControllerScriptEngineBase::initialize()) { return false; @@ -123,6 +135,7 @@ bool ControllerScriptEngineLegacy::initialize() { QJSValue engineGlobalObject = m_pJSEngine->globalObject(); ControllerScriptInterfaceLegacy* legacyScriptInterface = new ControllerScriptInterfaceLegacy(this, m_logger); + engineGlobalObject.setProperty( "engine", m_pJSEngine->newQObject(legacyScriptInterface)); diff --git a/src/controllers/scripting/legacy/controllerscriptenginelegacy.h b/src/controllers/scripting/legacy/controllerscriptenginelegacy.h index 70dea6a392e..9bf379dc0cf 100644 --- a/src/controllers/scripting/legacy/controllerscriptenginelegacy.h +++ b/src/controllers/scripting/legacy/controllerscriptenginelegacy.h @@ -29,7 +29,21 @@ class ControllerScriptEngineLegacy : public ControllerScriptEngineBase { public slots: void setScriptFiles(const QList& scripts); + /// @brief Set the list of customizable settings and their currently set + /// value, ready to be used. This method will generate a JSValue from their + /// current state, meaning that any later mutation won't be used, and this + /// method should be called again + /// @param settings The list of settings in a valid state (initialized and + /// restored) + void setSettings( + const QList>& settings); + private: + struct Setting { + QString name; + QJSValue value; + }; + bool evaluateScriptFile(const QFileInfo& scriptFile); void shutdown() override; @@ -44,6 +58,7 @@ class ControllerScriptEngineLegacy : public ControllerScriptEngineBase { QList m_incomingDataFunctions; QHash m_scriptWrappedFunctionCache; QList m_scriptFiles; + QHash m_settings; QFileSystemWatcher m_fileWatcher; diff --git a/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.cpp b/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.cpp index 33a60e3cae0..3ee8d09a280 100644 --- a/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.cpp +++ b/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.cpp @@ -108,6 +108,29 @@ ControlObjectScript* ControllerScriptInterfaceLegacy::getControlObjectScript( return coScript; } +QJSValue ControllerScriptInterfaceLegacy::getSetting(const QString& name) { + VERIFY_OR_DEBUG_ASSERT(m_pScriptEngineLegacy) { + return QJSValue::UndefinedValue; + } + if (name.isEmpty()) { + m_pScriptEngineLegacy->logOrThrowError( + QStringLiteral("getSetting called with empty name " + "string, returning undefined") + .arg(name)); + return QJSValue::UndefinedValue; + } + + const auto it = m_pScriptEngineLegacy->m_settings.constFind(name); + if (it != m_pScriptEngineLegacy->m_settings.constEnd()) { + return it.value(); + } else { + m_pScriptEngineLegacy->logOrThrowError( + QStringLiteral("Unknown controllerSetting (%1) returning undefined") + .arg(name)); + return QJSValue::UndefinedValue; + } +} + double ControllerScriptInterfaceLegacy::getValue(const QString& group, const QString& name) { ControlObjectScript* coScript = getControlObjectScript(group, name); if (coScript == nullptr) { diff --git a/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.h b/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.h index be8589f4d1a..4d3e280a6ff 100644 --- a/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.h +++ b/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.h @@ -22,6 +22,7 @@ class ControllerScriptInterfaceLegacy : public QObject { virtual ~ControllerScriptInterfaceLegacy(); + Q_INVOKABLE QJSValue getSetting(const QString& name); Q_INVOKABLE double getValue(const QString& group, const QString& name); Q_INVOKABLE void setValue(const QString& group, const QString& name, double newValue); Q_INVOKABLE double getParameter(const QString& group, const QString& name); diff --git a/src/coreservices.cpp b/src/coreservices.cpp index c0e1ccc10fe..3a66ab5decc 100644 --- a/src/coreservices.cpp +++ b/src/coreservices.cpp @@ -30,6 +30,7 @@ #include "skin/skincontrols.h" #include "soundio/soundmanager.h" #include "sources/soundsourceproxy.h" +#include "util/clipboard.h" #include "util/db/dbconnectionpooled.h" #include "util/font.h" #include "util/logger.h" @@ -335,6 +336,7 @@ void CoreServices::initialize(QApplication* pApp) { emit initializationProgressUpdate(50, tr("library")); CoverArtCache::createInstance(); + Clipboard::createInstance(); m_pTrackCollectionManager = std::make_shared( this, @@ -580,6 +582,8 @@ void CoreServices::finalize() { // CoverArtCache is fairly independent of everything else. CoverArtCache::destroy(); + Clipboard::destroy(); + // PlayerManager depends on Engine, SoundManager, VinylControlManager, and Config // The player manager has to be deleted before the library to ensure // that all modified track metadata of loaded tracks is saved. diff --git a/src/dialog/dlgdevelopertools.cpp b/src/dialog/dlgdevelopertools.cpp index 4e9af0206cd..6d9c34a3b2f 100644 --- a/src/dialog/dlgdevelopertools.cpp +++ b/src/dialog/dlgdevelopertools.cpp @@ -18,6 +18,7 @@ DlgDeveloperTools::DlgDeveloperTools(QWidget* pParent, controlsTable->hideColumn(ControlModel::CONTROL_COLUMN_TITLE); controlsTable->hideColumn(ControlModel::CONTROL_COLUMN_DESCRIPTION); controlsTable->hideColumn(ControlModel::CONTROL_COLUMN_FILTER); + m_controlProxyModel.sort(0, Qt::AscendingOrder); StatsManager* pManager = StatsManager::instance(); if (pManager) { diff --git a/src/effects/backends/effectsbackendmanager.cpp b/src/effects/backends/effectsbackendmanager.cpp index 1762a17d468..abc464b5f87 100644 --- a/src/effects/backends/effectsbackendmanager.cpp +++ b/src/effects/backends/effectsbackendmanager.cpp @@ -2,6 +2,7 @@ #include "control/controlobject.h" #include "effects/backends/builtin/builtinbackend.h" +#include "effects/backends/effectmanifest.h" #include "effects/backends/effectprocessor.h" #ifdef __LILV__ #include "effects/backends/lv2/lv2backend.h" diff --git a/src/library/analysisfeature.cpp b/src/library/analysis/analysisfeature.cpp similarity index 98% rename from src/library/analysisfeature.cpp rename to src/library/analysis/analysisfeature.cpp index ebe954c551d..a713f57ddfc 100644 --- a/src/library/analysisfeature.cpp +++ b/src/library/analysis/analysisfeature.cpp @@ -1,11 +1,11 @@ -#include "library/analysisfeature.h" +#include "library/analysis/analysisfeature.h" #include #include #include "analyzer/analyzerscheduledtrack.h" #include "controllers/keyboard/keyboardeventfilter.h" -#include "library/dlganalysis.h" +#include "library/analysis/dlganalysis.h" #include "library/library.h" #include "library/trackcollectionmanager.h" #include "moc_analysisfeature.cpp" diff --git a/src/library/analysisfeature.h b/src/library/analysis/analysisfeature.h similarity index 100% rename from src/library/analysisfeature.h rename to src/library/analysis/analysisfeature.h diff --git a/src/library/analysislibrarytablemodel.cpp b/src/library/analysis/analysislibrarytablemodel.cpp similarity index 93% rename from src/library/analysislibrarytablemodel.cpp rename to src/library/analysis/analysislibrarytablemodel.cpp index 4eeccbf5e1b..574c1545b98 100644 --- a/src/library/analysislibrarytablemodel.cpp +++ b/src/library/analysis/analysislibrarytablemodel.cpp @@ -1,4 +1,4 @@ -#include "library/analysislibrarytablemodel.h" +#include "library/analysis/analysislibrarytablemodel.h" #include "moc_analysislibrarytablemodel.cpp" diff --git a/src/library/analysislibrarytablemodel.h b/src/library/analysis/analysislibrarytablemodel.h similarity index 100% rename from src/library/analysislibrarytablemodel.h rename to src/library/analysis/analysislibrarytablemodel.h diff --git a/src/library/dlganalysis.cpp b/src/library/analysis/dlganalysis.cpp similarity index 98% rename from src/library/dlganalysis.cpp rename to src/library/analysis/dlganalysis.cpp index 36bb12953b6..3f67fe0bbd8 100644 --- a/src/library/dlganalysis.cpp +++ b/src/library/analysis/dlganalysis.cpp @@ -1,7 +1,8 @@ -#include "library/dlganalysis.h" +#include "library/analysis/dlganalysis.h" #include "analyzer/analyzerprogress.h" #include "analyzer/analyzerscheduledtrack.h" +#include "library/analysis/ui_dlganalysis.h" #include "library/dao/trackschema.h" #include "library/library.h" #include "moc_dlganalysis.cpp" diff --git a/src/library/dlganalysis.h b/src/library/analysis/dlganalysis.h similarity index 95% rename from src/library/dlganalysis.h rename to src/library/analysis/dlganalysis.h index c4d90c56521..ca32cc02f84 100644 --- a/src/library/dlganalysis.h +++ b/src/library/analysis/dlganalysis.h @@ -4,13 +4,13 @@ #include "analyzer/analyzerprogress.h" #include "analyzer/analyzerscheduledtrack.h" -#include "library/analysislibrarytablemodel.h" +#include "library/analysis/analysislibrarytablemodel.h" +#include "library/analysis/ui_dlganalysis.h" #include "library/libraryview.h" -#include "library/ui_dlganalysis.h" #include "preferences/usersettings.h" -class WAnalysisLibraryTableView; class Library; +class WAnalysisLibraryTableView; class WLibrary; class QItemSelection; diff --git a/src/library/dlganalysis.ui b/src/library/analysis/dlganalysis.ui similarity index 100% rename from src/library/dlganalysis.ui rename to src/library/analysis/dlganalysis.ui diff --git a/src/library/autodj/autodjfeature.cpp b/src/library/autodj/autodjfeature.cpp index 59525f24a38..cdca01516af 100644 --- a/src/library/autodj/autodjfeature.cpp +++ b/src/library/autodj/autodjfeature.cpp @@ -14,6 +14,8 @@ #include "moc_autodjfeature.cpp" #include "sources/soundsourceproxy.h" #include "track/track.h" +#include "util/clipboard.h" +#include "util/dnd.h" #include "widget/wlibrary.h" #include "widget/wlibrarysidebar.h" @@ -161,6 +163,14 @@ void AutoDJFeature::activate() { emit enableCoverArtDisplay(true); } +void AutoDJFeature::clear() { + m_playlistDao.clearAutoDJQueue(); +} + +void AutoDJFeature::paste() { + emit pasteFromSidebar(); +} + bool AutoDJFeature::dropAccept(const QList& urls, QObject* pSource) { // If a track is dropped onto the Auto DJ tree node, but the track isn't in the // library, then add the track to the library before adding it to the diff --git a/src/library/autodj/autodjfeature.h b/src/library/autodj/autodjfeature.h index 8a9a9d2a4fd..70451a49d7a 100644 --- a/src/library/autodj/autodjfeature.h +++ b/src/library/autodj/autodjfeature.h @@ -32,6 +32,9 @@ class AutoDJFeature : public LibraryFeature { QVariant title() override; + void clear() override; + void paste() override; + bool dropAccept(const QList& urls, QObject* pSource) override; bool dragMoveAccept(const QUrl& url) override; diff --git a/src/library/autodj/autodjprocessor.cpp b/src/library/autodj/autodjprocessor.cpp index d5485ccaae8..0910d6f9b89 100644 --- a/src/library/autodj/autodjprocessor.cpp +++ b/src/library/autodj/autodjprocessor.cpp @@ -401,6 +401,7 @@ AutoDJProcessor::AutoDJError AutoDJProcessor::toggleAutoDJ(bool enable) { for (int i = 2; i < m_decks.length(); ++i) { if (m_decks[i] && m_decks[i]->isPlaying()) { // Keep the current state. + emitAutoDJStateChanged(m_eState); emit autoDJError(ADJ_DECKS_3_4_PLAYING); return ADJ_DECKS_3_4_PLAYING; } diff --git a/src/library/autodj/dlgautodj.cpp b/src/library/autodj/dlgautodj.cpp index 4c8e3ced149..43c0dbb94fe 100644 --- a/src/library/autodj/dlgautodj.cpp +++ b/src/library/autodj/dlgautodj.cpp @@ -390,6 +390,10 @@ void DlgAutoDJ::setFocus() { m_pTrackTableView->setFocus(); } +void DlgAutoDJ::pasteFromSidebar() { + m_pTrackTableView->pasteFromSidebar(); +} + void DlgAutoDJ::keyPressEvent(QKeyEvent* pEvent) { // If we receive key events either the mode selector or the spinbox are focused. // Return, Enter and Escape move focus back to the previously focused diff --git a/src/library/autodj/dlgautodj.h b/src/library/autodj/dlgautodj.h index a9cc19a8f75..a2d976dc774 100644 --- a/src/library/autodj/dlgautodj.h +++ b/src/library/autodj/dlgautodj.h @@ -28,6 +28,7 @@ class DlgAutoDJ : public QWidget, public Ui::DlgAutoDJ, public LibraryView { void onShow() override; bool hasFocus() const override; void setFocus() override; + void pasteFromSidebar() override; void onSearch(const QString& text) override; void activateSelectedTrack() override; void loadSelectedTrackToGroup(const QString& group, bool play) override; diff --git a/src/library/basetracktablemodel.cpp b/src/library/basetracktablemodel.cpp index 6256cb114d4..b90bab1aea3 100644 --- a/src/library/basetracktablemodel.cpp +++ b/src/library/basetracktablemodel.cpp @@ -3,17 +3,17 @@ #include #include -#include "library/bpmdelegate.h" -#include "library/colordelegate.h" #include "library/coverartcache.h" -#include "library/coverartdelegate.h" #include "library/dao/trackschema.h" -#include "library/locationdelegate.h" -#include "library/multilineeditdelegate.h" -#include "library/playcountdelegate.h" -#include "library/previewbuttondelegate.h" -#include "library/stardelegate.h" #include "library/starrating.h" +#include "library/tabledelegates/bpmdelegate.h" +#include "library/tabledelegates/colordelegate.h" +#include "library/tabledelegates/coverartdelegate.h" +#include "library/tabledelegates/locationdelegate.h" +#include "library/tabledelegates/multilineeditdelegate.h" +#include "library/tabledelegates/playcountdelegate.h" +#include "library/tabledelegates/previewbuttondelegate.h" +#include "library/tabledelegates/stardelegate.h" #include "library/trackcollection.h" #include "library/trackcollectionmanager.h" #include "mixer/playerinfo.h" @@ -21,6 +21,7 @@ #include "moc_basetracktablemodel.cpp" #include "track/track.h" #include "util/assert.h" +#include "util/clipboard.h" #include "util/datetime.h" #include "util/db/sqlite.h" #include "util/logger.h" @@ -112,6 +113,14 @@ void BaseTrackTableModel::setBpmColumnPrecision(int precision) { } s_bpmColumnPrecision = precision; } + +bool BaseTrackTableModel::s_bApplyPlayedTrackColor = + kApplyPlayedTrackColorDefault; + +void BaseTrackTableModel::setApplyPlayedTrackColor(bool apply) { + s_bApplyPlayedTrackColor = apply; +} + //static QStringList BaseTrackTableModel::defaultTableColumns() { return kDefaultTableColumns; @@ -125,7 +134,8 @@ BaseTrackTableModel::BaseTrackTableModel( TrackModel(cloneDatabase(pTrackCollectionManager), settingsNamespace), m_pTrackCollectionManager(pTrackCollectionManager), m_previewDeckGroup(PlayerManager::groupForPreviewDeck(0)), - m_backgroundColorOpacity(WLibrary::kDefaultTrackTableBackgroundColorOpacity) { + m_backgroundColorOpacity(WLibrary::kDefaultTrackTableBackgroundColorOpacity), + m_playedInactiveColor(QColor::fromRgb(WTrackTableView::kDefaultPlayedInactiveColorHex)) { connect(&pTrackCollectionManager->internalCollection()->getTrackDAO(), &TrackDAO::forceModelUpdate, this, @@ -371,6 +381,59 @@ int BaseTrackTableModel::columnCount(const QModelIndex& parent) const { return countValidColumnHeaders(); } +void BaseTrackTableModel::cutTracks(const QModelIndexList& indices) { + copyTracks(indices); + removeTracks(indices); +} + +void BaseTrackTableModel::copyTracks(const QModelIndexList& indices) const { + Clipboard::start(); + for (const QModelIndex& index : indices) { + if (index.isValid()) { + Clipboard::add(QUrl::fromLocalFile(getTrackLocation(index))); + } + } + Clipboard::finish(); +} + +QList BaseTrackTableModel::pasteTracks(const QModelIndex& insertionIndex) { + // Don't paste into locked playlists and crates or into into History + if (isLocked() || !hasCapabilities(TrackModel::Capability::ReceiveDrops)) { + return QList{}; + } + + int insertionPos = 0; + const QList urls = Clipboard::urls(); + const QList trackIds = m_pTrackCollectionManager->resolveTrackIdsFromUrls(urls, true); + if (!trackIds.isEmpty()) { + addTracksWithTrackIds(insertionIndex, trackIds, &insertionPos); + } + + QList rows; + for (const auto& trackId : trackIds) { + const auto trackRows = getTrackRows(trackId); + for (int trackRow : trackRows) { + if (insertionPos == 0) { + rows.append(trackRow); + } else { + int pos = + index( + trackRow, + fieldIndex(ColumnCache:: + COLUMN_PLAYLISTTRACKSTABLE_POSITION)) + .data() + .toInt(); + // trackRows includes all instances in the table of the pasted + // tracks. We only want to select the ones we just inserted + if (pos >= insertionPos && pos < insertionPos + trackIds.size()) { + rows.append(trackRow); + } + } + } + } + return rows; +} + bool BaseTrackTableModel::isColumnHiddenByDefault( int column) { return column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUMARTIST) || @@ -396,6 +459,15 @@ QAbstractItemDelegate* BaseTrackTableModel::delegateForColumn( return nullptr; } m_backgroundColorOpacity = pTableView->getBackgroundColorOpacity(); + // This is the color used for the text of played tracks. + // data() uses this to compose the ForegroundRole QBrush if 'played' is checked. + m_playedInactiveColor = pTableView->getPlayedInactiveColor(); + connect(pTableView, + &WTrackTableView::playedInactiveColorChanged, + this, + [this](QColor col) { + m_playedInactiveColor = col; + }); if (index == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_RATING)) { return new StarDelegate(pTableView); } else if (index == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM)) { @@ -450,6 +522,16 @@ QVariant BaseTrackTableModel::data( DEBUG_ASSERT(m_backgroundColorOpacity <= 1.0); bgColor.setAlphaF(static_cast(m_backgroundColorOpacity)); return QBrush(bgColor); + } else if (role == Qt::ForegroundRole && s_bApplyPlayedTrackColor) { + // Custom text color for played tracks + auto playedRaw = rawSiblingValue( + index, + ColumnCache::COLUMN_LIBRARYTABLE_PLAYED); + if (!playedRaw.isNull() && + playedRaw.canConvert() && + playedRaw.toBool()) { + return QVariant::fromValue(m_playedInactiveColor); + } } // Only retrieve a value for supported roles diff --git a/src/library/basetracktablemodel.h b/src/library/basetracktablemodel.h index d8daf44ba22..e46cc2569c8 100644 --- a/src/library/basetracktablemodel.h +++ b/src/library/basetracktablemodel.h @@ -81,6 +81,10 @@ class BaseTrackTableModel : public QAbstractTableModel, public TrackModel { return m_columnCache.fieldIndex(fieldName); } + void cutTracks(const QModelIndexList& indices) override; + void copyTracks(const QModelIndexList& indices) const override; + QList pasteTracks(const QModelIndex& index) override; + bool isColumnHiddenByDefault( int column) override; @@ -101,6 +105,9 @@ class BaseTrackTableModel : public QAbstractTableModel, public TrackModel { static constexpr int kBpmColumnPrecisionMaximum = 10; static void setBpmColumnPrecision(int precision); + static constexpr bool kApplyPlayedTrackColorDefault = true; + static void setApplyPlayedTrackColor(bool apply); + protected: static constexpr int defaultColumnWidth() { return 50; @@ -268,6 +275,7 @@ class BaseTrackTableModel : public QAbstractTableModel, public TrackModel { const QString m_previewDeckGroup; double m_backgroundColorOpacity; + QColor m_playedInactiveColor; ColumnCache m_columnCache; @@ -284,4 +292,6 @@ class BaseTrackTableModel : public QAbstractTableModel, public TrackModel { mutable QModelIndex m_toolTipIndex; static int s_bpmColumnPrecision; + + static bool s_bApplyPlayedTrackColor; }; diff --git a/src/library/browse/browsetablemodel.cpp b/src/library/browse/browsetablemodel.cpp index 561b2a3ecac..f6245926421 100644 --- a/src/library/browse/browsetablemodel.cpp +++ b/src/library/browse/browsetablemodel.cpp @@ -6,7 +6,7 @@ #include "library/browse/browsetablemodel.h" #include "library/browse/browsethread.h" -#include "library/previewbuttondelegate.h" +#include "library/tabledelegates/previewbuttondelegate.h" #include "library/trackcollection.h" #include "library/trackcollectionmanager.h" #include "mixer/playerinfo.h" @@ -14,6 +14,7 @@ #include "moc_browsetablemodel.cpp" #include "recording/recordingmanager.h" #include "track/track.h" +#include "util/clipboard.h" #include "widget/wlibrarytableview.h" namespace { @@ -314,6 +315,20 @@ bool BrowseTableModel::isColumnHiddenByDefault(int column) { void BrowseTableModel::moveTrack(const QModelIndex&, const QModelIndex&) { } +void BrowseTableModel::copyTracks(const QModelIndexList& indices) const { + Clipboard::start(); + for (const QModelIndex& index : indices) { + if (index.isValid()) { + Clipboard::add(QUrl::fromLocalFile(getTrackLocation(index))); + } + } + Clipboard::finish(); + + // TODO Investigate if we can also implement cut and paste (via QFile + // operations) so mixxx could manage files in the filesystem, rather than + // having to go switch between mixxx and the system file browser. +} + void BrowseTableModel::removeTracks(const QModelIndexList&) { } diff --git a/src/library/browse/browsetablemodel.h b/src/library/browse/browsetablemodel.h index fb4b8dbc7f8..e770096eee5 100644 --- a/src/library/browse/browsetablemodel.h +++ b/src/library/browse/browsetablemodel.h @@ -71,6 +71,7 @@ class BrowseTableModel final : public QStandardItemModel, public virtual TrackMo const QString currentSearch() const override; bool isColumnInternal(int) override; void moveTrack(const QModelIndex&, const QModelIndex&) override; + void copyTracks(const QModelIndexList& indices) const override; bool isLocked() override { return false; } bool isColumnHiddenByDefault(int column) override; const QList& searchColumns() const override; diff --git a/src/library/dao/playlistdao.cpp b/src/library/dao/playlistdao.cpp index 192c0002ae4..ede2f4eb569 100644 --- a/src/library/dao/playlistdao.cpp +++ b/src/library/dao/playlistdao.cpp @@ -796,6 +796,16 @@ int PlaylistDAO::insertTracksIntoPlaylist(const QList& trackIds, return tracksAdded; } +void PlaylistDAO::clearAutoDJQueue() { + const int iAutoDJPlaylistId = getPlaylistIdFromName(AUTODJ_TABLE); + // If the first track is already loaded to the player, + // alter the playlist only below the first track + const int position = + (m_pAutoDJProcessor && m_pAutoDJProcessor->nextTrackLoaded()) ? 2 : 1; + + removeTracksFromPlaylist(iAutoDJPlaylistId, position); +} + void PlaylistDAO::addPlaylistToAutoDJQueue(const int playlistId, AutoDJSendLoc loc) { //qDebug() << "Adding tracks from playlist " << playlistId << " to the Auto-DJ Queue"; diff --git a/src/library/dao/playlistdao.h b/src/library/dao/playlistdao.h index 58660626c27..3eb6def5ec2 100644 --- a/src/library/dao/playlistdao.h +++ b/src/library/dao/playlistdao.h @@ -107,6 +107,8 @@ class PlaylistDAO : public QObject, public virtual DAO { bool insertTrackIntoPlaylist(TrackId trackId, int playlistId, int position); // Inserts a list of tracks into playlist int insertTracksIntoPlaylist(const QList& trackIds, const int playlistId, int position); + // Remove all tracks from the Auto-DJ Queue + void clearAutoDJQueue(); // Add a playlist to the Auto-DJ Queue void addPlaylistToAutoDJQueue(const int playlistId, AutoDJSendLoc loc); // Add a list of tracks to the Auto-DJ Queue diff --git a/src/library/dlgtrackinfo.cpp b/src/library/dlgtrackinfo.cpp index e85aa0f9a2c..2366ec88399 100644 --- a/src/library/dlgtrackinfo.cpp +++ b/src/library/dlgtrackinfo.cpp @@ -452,8 +452,8 @@ void DlgTrackInfo::reloadTrackBeats(const Track& track) { updateSpinBpmFromBeats(); m_trackHasBeatMap = m_pBeatsClone && !m_pBeatsClone->hasConstantTempo(); bpmConst->setChecked(!m_trackHasBeatMap); - bpmConst->setEnabled(m_trackHasBeatMap); // We cannot make turn a BeatGrid to a BeatMap - spinBpm->setEnabled(!m_trackHasBeatMap); // We cannot change bpm continuously or tab them + bpmConst->setEnabled(m_trackHasBeatMap); // We cannot turn a BeatGrid to a BeatMap + spinBpm->setEnabled(!m_trackHasBeatMap); // We cannot change bpm continuously or tap them bpmTap->setEnabled(!m_trackHasBeatMap); // when we have a beatmap if (track.isBpmLocked()) { @@ -614,8 +614,13 @@ void DlgTrackInfo::saveTrack() { static_cast(updateKeyText()); // discard result // Update the cached track - // The dialog is updated and repopulated by the Track::changed() signal. - m_pLoadedTrack->replaceRecord(std::move(m_trackRecord), std::move(m_pBeatsClone)); + // + // If replaceRecord() returns true then both m_trackRecord and m_pBeatsClone + // will be updated by the subsequent Track::changed() signal to keep them + // synchronized with the track. Otherwise the track has not been modified and + // both members must remain valid. Do not use std::move() for passing arguments! + // Else triggering apply twice in quick succession might clear the metadata. + m_pLoadedTrack->replaceRecord(m_trackRecord, m_pBeatsClone); } void DlgTrackInfo::clear() { @@ -693,28 +698,28 @@ void DlgTrackInfo::slotSpinBpmValueChanged(double value) { return; } - if (!m_pBeatsClone) { - mixxx::audio::FramePos cuePosition = m_pLoadedTrack->getMainCuePosition(); - // This should never happen, but we cannot be sure - VERIFY_OR_DEBUG_ASSERT(cuePosition.isValid()) { - cuePosition = mixxx::audio::kStartFramePos; - } - m_pBeatsClone = mixxx::Beats::fromConstTempo( - m_pLoadedTrack->getSampleRate(), - // Cue positions might be fractional, i.e. not on frame boundaries! - cuePosition.toNearestFrameBoundary(), - bpm); - } - - if (m_pLoadedTrack && m_pBeatsClone) { - const auto trackEndPosition = mixxx::audio::FramePos{ - m_pLoadedTrack->getDuration() * m_pBeatsClone->getSampleRate()}; - const mixxx::Bpm oldBpm = m_pBeatsClone->getBpmInRange( - mixxx::audio::kStartFramePos, trackEndPosition); - if (oldBpm == bpm) { - return; + if (m_pLoadedTrack) { + if (m_pBeatsClone) { + const auto trackEndPosition = mixxx::audio::FramePos{ + m_pLoadedTrack->getDuration() * m_pBeatsClone->getSampleRate()}; + const mixxx::Bpm oldBpm = m_pBeatsClone->getBpmInRange( + mixxx::audio::kStartFramePos, trackEndPosition); + if (oldBpm == bpm) { + return; + } + m_pBeatsClone = m_pBeatsClone->trySetBpm(bpm).value_or(m_pBeatsClone); + } else { + mixxx::audio::FramePos cuePosition = m_pLoadedTrack->getMainCuePosition(); + // This should never happen, but we cannot be sure + VERIFY_OR_DEBUG_ASSERT(cuePosition.isValid()) { + cuePosition = mixxx::audio::kStartFramePos; + } + m_pBeatsClone = mixxx::Beats::fromConstTempo( + m_pLoadedTrack->getSampleRate(), + // Cue positions might be fractional, i.e. not on frame boundaries! + cuePosition.toNearestFrameBoundary(), + bpm); } - m_pBeatsClone = m_pBeatsClone->trySetBpm(bpm).value_or(m_pBeatsClone); } updateSpinBpmFromBeats(); diff --git a/src/library/library.cpp b/src/library/library.cpp index 77bd315e1b4..d1f73fee806 100644 --- a/src/library/library.cpp +++ b/src/library/library.cpp @@ -6,7 +6,7 @@ #include "control/controlobject.h" #include "controllers/keyboard/keyboardeventfilter.h" -#include "library/analysisfeature.h" +#include "library/analysis/analysisfeature.h" #include "library/autodj/autodjfeature.h" #include "library/banshee/bansheefeature.h" #include "library/browse/browsefeature.h" @@ -389,6 +389,10 @@ void Library::bindLibraryWidget( &Library::showTrackModel, pTrackTableView, &WTrackTableView::loadTrackModel); + connect(this, + &Library::pasteFromSidebar, + m_pLibraryWidget, + &WLibrary::pasteFromSidebar); connect(pTrackTableView, &WTrackTableView::loadTrack, this, @@ -399,6 +403,10 @@ void Library::bindLibraryWidget( &Library::slotLoadTrackToPlayer); m_pLibraryWidget->registerView(m_sTrackViewName, pTrackTableView); + connect(m_pLibraryWidget, + &WLibrary::setLibraryFocus, + m_pLibraryControl, + &LibraryControl::setLibraryFocus); connect(this, &Library::switchToView, m_pLibraryWidget, @@ -461,6 +469,10 @@ void Library::addFeature(LibraryFeature* feature) { } m_features.push_back(feature); m_pSidebarModel->addLibraryFeature(feature); + connect(feature, + &LibraryFeature::pasteFromSidebar, + this, + &Library::pasteFromSidebar); connect(feature, &LibraryFeature::showTrackModel, this, @@ -517,7 +529,7 @@ void Library::onPlayerManagerTrackAnalyzerIdle() { } void Library::slotShowTrackModel(QAbstractItemModel* model) { - //qDebug() << "Library::slotShowTrackModel" << model; + // qDebug() << "Library::slotShowTrackModel" << model; TrackModel* trackModel = dynamic_cast(model); VERIFY_OR_DEBUG_ASSERT(trackModel) { return; @@ -528,7 +540,7 @@ void Library::slotShowTrackModel(QAbstractItemModel* model) { } void Library::slotSwitchToView(const QString& view) { - //qDebug() << "Library::slotSwitchToView" << view; + // qDebug() << "Library::slotSwitchToView" << view; emit switchToView(view); } diff --git a/src/library/library.h b/src/library/library.h index fe9bf0056c1..3cd08cc23e5 100644 --- a/src/library/library.h +++ b/src/library/library.h @@ -124,6 +124,7 @@ class Library: public QObject { void restoreSearch(const QString&); void search(const QString& text); void disableSearch(); + void pasteFromSidebar(); // emit this signal to enable/disable the cover art widget void enableCoverArtDisplay(bool); void selectTrack(const TrackId&); diff --git a/src/library/library_prefs.cpp b/src/library/library_prefs.cpp index a10136763c8..b0583611e0c 100644 --- a/src/library/library_prefs.cpp +++ b/src/library/library_prefs.cpp @@ -63,6 +63,11 @@ const ConfigKey mixxx::library::prefs::kBpmColumnPrecisionConfigKey = mixxx::library::prefs::kConfigGroup, QStringLiteral("BpmColumnPrecision")}; +const ConfigKey mixxx::library::prefs::kApplyPlayedTrackColorConfigKey = + ConfigKey{ + mixxx::library::prefs::kConfigGroup, + QStringLiteral("ApplyPlayedTrackColor")}; + // The "Export" suffix in the key is kept for backward compatibility const ConfigKey mixxx::library::prefs::kSyncTrackMetadataConfigKey = ConfigKey{ diff --git a/src/library/library_prefs.h b/src/library/library_prefs.h index 6750582b6d3..ce45b709aa0 100644 --- a/src/library/library_prefs.h +++ b/src/library/library_prefs.h @@ -26,6 +26,8 @@ extern const ConfigKey kEnableSearchHistoryShortcutsConfigKey; extern const ConfigKey kBpmColumnPrecisionConfigKey; +extern const ConfigKey kApplyPlayedTrackColorConfigKey; + extern const ConfigKey kEditMetadataSelectedClickConfigKey; extern const ConfigKey kHistoryMinTracksToKeepConfigKey; diff --git a/src/library/libraryfeature.h b/src/library/libraryfeature.h index 5ba3ffa7da2..61972192dcd 100644 --- a/src/library/libraryfeature.h +++ b/src/library/libraryfeature.h @@ -68,6 +68,13 @@ class LibraryFeature : public QObject { return false; } + virtual void clear() { + } + virtual void paste() { + } + virtual void pasteChild(const QModelIndex& index) { + Q_UNUSED(index); + } // Reimplement this to register custom views with the library widget. virtual void bindLibraryWidget(WLibrary* /* libraryWidget */, KeyboardEventFilter* /* keyboard */) {} @@ -135,6 +142,7 @@ class LibraryFeature : public QObject { void restoreModelState(); void restoreSearch(const QString&); void disableSearch(); + void pasteFromSidebar(); // emit this signal before you parse a large music collection, e.g., iTunes, Traktor. // The second arg indicates if the feature should be "selected" when loading starts void featureIsLoading(LibraryFeature*, bool selectFeature); diff --git a/src/library/libraryview.h b/src/library/libraryview.h index eec640e636a..ac6061b5ac8 100644 --- a/src/library/libraryview.h +++ b/src/library/libraryview.h @@ -22,6 +22,9 @@ class LibraryView { /// Reimplement if LibraryView should be able to search virtual void onSearch(const QString& text) {Q_UNUSED(text);} + virtual void pasteFromSidebar() { + } + /// If applicable, requests that the LibraryView load the selected /// track. Does nothing otherwise. virtual void activateSelectedTrack() { diff --git a/src/library/dlghidden.cpp b/src/library/missing_hidden/dlghidden.cpp similarity index 97% rename from src/library/dlghidden.cpp rename to src/library/missing_hidden/dlghidden.cpp index a43bd67ce10..0943a182eb3 100644 --- a/src/library/dlghidden.cpp +++ b/src/library/missing_hidden/dlghidden.cpp @@ -1,10 +1,10 @@ -#include "library/dlghidden.h" +#include "library/missing_hidden/dlghidden.h" #include #include "controllers/keyboard/keyboardeventfilter.h" -#include "library/hiddentablemodel.h" #include "library/library.h" +#include "library/missing_hidden/hiddentablemodel.h" #include "moc_dlghidden.cpp" #include "util/assert.h" #include "widget/wlibrary.h" diff --git a/src/library/dlghidden.h b/src/library/missing_hidden/dlghidden.h similarity index 100% rename from src/library/dlghidden.h rename to src/library/missing_hidden/dlghidden.h diff --git a/src/library/dlghidden.ui b/src/library/missing_hidden/dlghidden.ui similarity index 100% rename from src/library/dlghidden.ui rename to src/library/missing_hidden/dlghidden.ui diff --git a/src/library/dlgmissing.cpp b/src/library/missing_hidden/dlgmissing.cpp similarity index 97% rename from src/library/dlgmissing.cpp rename to src/library/missing_hidden/dlgmissing.cpp index 24b4b3a42ee..736fb4842cd 100644 --- a/src/library/dlgmissing.cpp +++ b/src/library/missing_hidden/dlgmissing.cpp @@ -1,10 +1,10 @@ -#include "library/dlgmissing.h" +#include "library/missing_hidden/dlgmissing.h" #include #include "controllers/keyboard/keyboardeventfilter.h" #include "library/library.h" -#include "library/missingtablemodel.h" +#include "library/missing_hidden/missingtablemodel.h" #include "moc_dlgmissing.cpp" #include "util/assert.h" #include "widget/wlibrary.h" diff --git a/src/library/dlgmissing.h b/src/library/missing_hidden/dlgmissing.h similarity index 100% rename from src/library/dlgmissing.h rename to src/library/missing_hidden/dlgmissing.h diff --git a/src/library/dlgmissing.ui b/src/library/missing_hidden/dlgmissing.ui similarity index 100% rename from src/library/dlgmissing.ui rename to src/library/missing_hidden/dlgmissing.ui diff --git a/src/library/hiddentablemodel.cpp b/src/library/missing_hidden/hiddentablemodel.cpp similarity index 98% rename from src/library/hiddentablemodel.cpp rename to src/library/missing_hidden/hiddentablemodel.cpp index f54fc17ca0e..ff87e84387d 100644 --- a/src/library/hiddentablemodel.cpp +++ b/src/library/missing_hidden/hiddentablemodel.cpp @@ -1,4 +1,4 @@ -#include "library/hiddentablemodel.h" +#include "library/missing_hidden/hiddentablemodel.h" #include "library/dao/trackschema.h" #include "library/trackcollection.h" diff --git a/src/library/hiddentablemodel.h b/src/library/missing_hidden/hiddentablemodel.h similarity index 100% rename from src/library/hiddentablemodel.h rename to src/library/missing_hidden/hiddentablemodel.h diff --git a/src/library/missingtablemodel.cpp b/src/library/missing_hidden/missingtablemodel.cpp similarity index 98% rename from src/library/missingtablemodel.cpp rename to src/library/missing_hidden/missingtablemodel.cpp index 5906196c3b1..1b6c077e509 100644 --- a/src/library/missingtablemodel.cpp +++ b/src/library/missing_hidden/missingtablemodel.cpp @@ -1,4 +1,4 @@ -#include "library/missingtablemodel.h" +#include "library/missing_hidden/missingtablemodel.h" #include "library/dao/trackschema.h" #include "library/trackcollection.h" diff --git a/src/library/missingtablemodel.h b/src/library/missing_hidden/missingtablemodel.h similarity index 100% rename from src/library/missingtablemodel.h rename to src/library/missing_hidden/missingtablemodel.h diff --git a/src/library/mixxxlibraryfeature.cpp b/src/library/mixxxlibraryfeature.cpp index 84400b7a939..4ea52b1e6a1 100644 --- a/src/library/mixxxlibraryfeature.cpp +++ b/src/library/mixxxlibraryfeature.cpp @@ -7,10 +7,10 @@ #include "library/basetrackcache.h" #include "library/dao/trackschema.h" -#include "library/dlghidden.h" -#include "library/dlgmissing.h" #include "library/library.h" #include "library/librarytablemodel.h" +#include "library/missing_hidden/dlghidden.h" +#include "library/missing_hidden/dlgmissing.h" #include "library/parser.h" #include "library/queryutil.h" #include "library/trackcollection.h" diff --git a/src/library/playlisttablemodel.cpp b/src/library/playlisttablemodel.cpp index 3c563657d8d..56e512d6053 100644 --- a/src/library/playlisttablemodel.cpp +++ b/src/library/playlisttablemodel.cpp @@ -16,10 +16,10 @@ const QString kModelName = "playlist:"; PlaylistTableModel::PlaylistTableModel(QObject* parent, TrackCollectionManager* pTrackCollectionManager, const char* settingsNamespace, - bool keepDeletedTracks) + bool keepHiddenTracks) : TrackSetTableModel(parent, pTrackCollectionManager, settingsNamespace), m_iPlaylistId(kInvalidPlaylistId), - m_keepDeletedTracks(keepDeletedTracks) { + m_keepHiddenTracks(keepHiddenTracks) { connect(&m_pTrackCollectionManager->internalCollection()->getPlaylistDAO(), &PlaylistDAO::tracksChanged, this, @@ -139,7 +139,7 @@ void PlaylistTableModel::selectPlaylist(int playlistId) { m_iPlaylistId = playlistId; - if (!m_keepDeletedTracks) { + if (!m_keepHiddenTracks) { // From Mixxx 2.1 we drop tracks that have been explicitly deleted // in the library (mixxx_deleted = 0) from playlists. // These invisible tracks, consuming a playlist position number were @@ -189,30 +189,35 @@ void PlaylistTableModel::selectPlaylist(int playlistId) { setSort(defaultSortColumn(), defaultSortOrder()); } -int PlaylistTableModel::addTracks(const QModelIndex& index, - const QList& locations) { - if (locations.isEmpty()) { +int PlaylistTableModel::addTracksWithTrackIds(const QModelIndex& insertionIndex, + const QList& trackIds, + int* pOutInsertionPos) { + if (trackIds.isEmpty()) { return 0; } - QList trackIds = m_pTrackCollectionManager->resolveTrackIdsFromLocations( - locations); - const int positionColumn = fieldIndex(ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_POSITION); - int position = index.sibling(index.row(), positionColumn).data().toInt(); + int position = index(insertionIndex.row(), positionColumn).data().toInt(); // Handle weird cases like a drag and drop to an invalid index if (position <= 0) { position = rowCount() + 1; } + if (pOutInsertionPos) { + *pOutInsertionPos = position; + } + int tracksAdded = m_pTrackCollectionManager->internalCollection()->getPlaylistDAO().insertTracksIntoPlaylist( trackIds, m_iPlaylistId, position); - if (locations.size() - tracksAdded > 0) { + if (trackIds.size() - tracksAdded > 0) { + QString playlistName = m_pTrackCollectionManager->internalCollection() + ->getPlaylistDAO() + .getPlaylistName(m_iPlaylistId); qDebug() << "PlaylistTableModel::addTracks could not add" - << locations.size() - tracksAdded - << "to playlist" << m_iPlaylistId; + << trackIds.size() - tracksAdded + << "to playlist id" << m_iPlaylistId << "name" << playlistName; } return tracksAdded; } diff --git a/src/library/playlisttablemodel.h b/src/library/playlisttablemodel.h index 0443bd53577..1f8efddb69a 100644 --- a/src/library/playlisttablemodel.h +++ b/src/library/playlisttablemodel.h @@ -7,7 +7,10 @@ class PlaylistTableModel final : public TrackSetTableModel { Q_OBJECT public: - PlaylistTableModel(QObject* parent, TrackCollectionManager* pTrackCollectionManager, const char* settingsNamespace, bool keepDeletedTracks = false); + PlaylistTableModel(QObject* parent, + TrackCollectionManager* pTrackCollectionManager, + const char* settingsNamespace, + bool keepHiddenTracks = false); ~PlaylistTableModel() final = default; void selectPlaylist(int playlistId = -1 /* kInvalidPlaylistId */); @@ -25,7 +28,9 @@ class PlaylistTableModel final : public TrackSetTableModel { /// This function should only be used by AUTODJ void removeTracks(const QModelIndexList& indices) final; /// Returns the number of successful additions. - int addTracks(const QModelIndex& index, const QList& locations) final; + int addTracksWithTrackIds(const QModelIndex& index, + const QList& trackIds, + int* pOutInsertionPos) final; bool isLocked() final; /// Get the total duration of all tracks referenced by the given model indices @@ -42,6 +47,6 @@ class PlaylistTableModel final : public TrackSetTableModel { void initSortColumnMapping() override; int m_iPlaylistId; - bool m_keepDeletedTracks; + bool m_keepHiddenTracks; QHash m_searchTexts; }; diff --git a/src/library/proxytrackmodel.cpp b/src/library/proxytrackmodel.cpp index 73c374e2838..1d0398c6ffe 100644 --- a/src/library/proxytrackmodel.cpp +++ b/src/library/proxytrackmodel.cpp @@ -113,6 +113,17 @@ void ProxyTrackModel::removeTracks(const QModelIndexList& indices) { } } +void ProxyTrackModel::copyTracks(const QModelIndexList& indices) const { + QModelIndexList translatedList; + foreach (QModelIndex index, indices) { + QModelIndex indexSource = mapToSource(index); + translatedList.append(indexSource); + } + if (m_pTrackModel) { + m_pTrackModel->copyTracks(translatedList); + } +} + void ProxyTrackModel::moveTrack(const QModelIndex& sourceIndex, const QModelIndex& destIndex) { QModelIndex sourceIndexSource = mapToSource(sourceIndex); diff --git a/src/library/proxytrackmodel.h b/src/library/proxytrackmodel.h index d56ef77eeb5..561449a31a8 100644 --- a/src/library/proxytrackmodel.h +++ b/src/library/proxytrackmodel.h @@ -36,6 +36,7 @@ class ProxyTrackModel : public QSortFilterProxyModel, public TrackModel { bool isColumnInternal(int column) final; bool isColumnHiddenByDefault(int column) final; void removeTracks(const QModelIndexList& indices) final; + void copyTracks(const QModelIndexList& indices) const final; void moveTrack(const QModelIndex& sourceIndex, const QModelIndex& destIndex) final; QAbstractItemDelegate* delegateForColumn(const int i, QObject* pParent) final; QString getModelSetting(const QString& name) final; diff --git a/src/library/sidebarmodel.cpp b/src/library/sidebarmodel.cpp index 1dd2786fbc4..49a13dd9bce 100644 --- a/src/library/sidebarmodel.cpp +++ b/src/library/sidebarmodel.cpp @@ -148,6 +148,24 @@ QModelIndex SidebarModel::getFeatureRootIndex(LibraryFeature* pFeature) { return ind; } +void SidebarModel::clear(const QModelIndex& index) { + if (index.internalPointer() == this) { + m_sFeatures[index.row()]->clear(); + } +} + +void SidebarModel::paste(const QModelIndex& index) { + if (index.internalPointer() == this) { + m_sFeatures[index.row()]->paste(); + } else { + TreeItem* pTreeItem = (TreeItem*)index.internalPointer(); + if (pTreeItem) { + LibraryFeature* feature = pTreeItem->feature(); + feature->pasteChild(index); + } + } +} + QModelIndex SidebarModel::parent(const QModelIndex& index) const { //qDebug() << "SidebarModel::parent index=" << index.getData(); if (index.isValid()) { @@ -475,7 +493,7 @@ QModelIndex SidebarModel::translateIndex( } void SidebarModel::slotDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { - //qDebug() << "slotDataChanged topLeft:" << topLeft << "bottomRight:" << bottomRight; + // qDebug() << "slotDataChanged topLeft:" << topLeft << "bottomRight:" << bottomRight; QModelIndex topLeftTranslated = translateSourceIndex(topLeft); QModelIndex bottomRightTranslated = translateSourceIndex(bottomRight); emit dataChanged(topLeftTranslated, bottomRightTranslated); @@ -499,8 +517,8 @@ void SidebarModel::slotRowsInserted(const QModelIndex& parent, int start, int en Q_UNUSED(parent); Q_UNUSED(start); Q_UNUSED(end); - //qDebug() << "slotRowsInserted" << parent << start << end; - //QModelIndex newParent = translateSourceIndex(parent); + // qDebug() << "slotRowsInserted" << parent << start << end; + // QModelIndex newParent = translateSourceIndex(parent); endInsertRows(); } diff --git a/src/library/sidebarmodel.h b/src/library/sidebarmodel.h index c0d7a6ca063..c987dff72c7 100644 --- a/src/library/sidebarmodel.h +++ b/src/library/sidebarmodel.h @@ -47,6 +47,8 @@ class SidebarModel : public QAbstractItemModel { } QModelIndex getFeatureRootIndex(LibraryFeature* pFeature); + void clear(const QModelIndex& index); + void paste(const QModelIndex& index); public slots: void pressed(const QModelIndex& index); void clicked(const QModelIndex& index); diff --git a/src/library/bpmdelegate.cpp b/src/library/tabledelegates/bpmdelegate.cpp similarity index 77% rename from src/library/bpmdelegate.cpp rename to src/library/tabledelegates/bpmdelegate.cpp index 4f6b1b3b9a8..9b54cde6027 100644 --- a/src/library/bpmdelegate.cpp +++ b/src/library/tabledelegates/bpmdelegate.cpp @@ -1,4 +1,4 @@ -#include "library/bpmdelegate.h" +#include "library/tabledelegates/bpmdelegate.h" #include #include @@ -92,6 +92,28 @@ void BPMDelegate::paintItem(QPainter* painter,const QStyleOptionViewItem &option QStyleOptionViewItem opt = option; initStyleOption(&opt, index); + // The checkbox uses the QTableView's qss style, therefore it's not picking + // up the 'played' text color via ForegroundRole from BaseTrackTableModel::data(). + // Enforce it with an explicit stylesheet. Note: the stylesheet persists so + // we need to reset it to normal/highlighted. + QColor textColor; + auto dat = index.data(Qt::ForegroundRole); + if (dat.canConvert()) { + textColor = dat.value(); + } else { + if (option.state & QStyle::State_Selected) { + textColor = option.palette.color(QPalette::Normal, QPalette::HighlightedText); + } else { + textColor = option.palette.color(QPalette::Normal, QPalette::Text); + // qWarning() << " !! BPM col:" << textColor.name(QColor::HexRgb); + } + } + if (textColor.isValid()) { + m_pCheckBox->setStyleSheet(QStringLiteral( + "#LibraryBPMButton::item { color: %1; }") + .arg(textColor.name(QColor::HexRgb))); + } + QStyle* style = m_pTableView->style(); if (style != nullptr) { style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, m_pCheckBox); diff --git a/src/library/bpmdelegate.h b/src/library/tabledelegates/bpmdelegate.h similarity index 88% rename from src/library/bpmdelegate.h rename to src/library/tabledelegates/bpmdelegate.h index f0251199255..38a398898dc 100644 --- a/src/library/bpmdelegate.h +++ b/src/library/tabledelegates/bpmdelegate.h @@ -1,6 +1,6 @@ #pragma once -#include "library/tableitemdelegate.h" +#include "library/tabledelegates/tableitemdelegate.h" class QCheckBox; diff --git a/src/library/colordelegate.cpp b/src/library/tabledelegates/colordelegate.cpp similarity index 88% rename from src/library/colordelegate.cpp rename to src/library/tabledelegates/colordelegate.cpp index e36e26d56bd..76400bd6ee8 100644 --- a/src/library/colordelegate.cpp +++ b/src/library/tabledelegates/colordelegate.cpp @@ -1,4 +1,4 @@ -#include "library/colordelegate.h" +#include "library/tabledelegates/colordelegate.h" #include #include @@ -28,6 +28,6 @@ void ColorDelegate::paintItem( // Draw a border if the color cell has focus if (option.state & QStyle::State_HasFocus) { - drawBorder(painter, m_pFocusBorderColor, option.rect); + drawBorder(painter, m_focusBorderColor, option.rect); } } diff --git a/src/library/colordelegate.h b/src/library/tabledelegates/colordelegate.h similarity index 86% rename from src/library/colordelegate.h rename to src/library/tabledelegates/colordelegate.h index e0b0b9889e1..7309b560a0e 100644 --- a/src/library/colordelegate.h +++ b/src/library/tabledelegates/colordelegate.h @@ -1,6 +1,6 @@ #pragma once -#include "library/tableitemdelegate.h" +#include "library/tabledelegates/tableitemdelegate.h" class QModelIndex; class QPainter; diff --git a/src/library/coverartdelegate.cpp b/src/library/tabledelegates/coverartdelegate.cpp similarity index 98% rename from src/library/coverartdelegate.cpp rename to src/library/tabledelegates/coverartdelegate.cpp index 972587d3978..781dbc48292 100644 --- a/src/library/coverartdelegate.cpp +++ b/src/library/tabledelegates/coverartdelegate.cpp @@ -1,4 +1,4 @@ -#include "library/coverartdelegate.h" +#include "library/tabledelegates/coverartdelegate.h" #include #include @@ -168,6 +168,6 @@ void CoverArtDelegate::paintItem( // Draw a border if the cover art cell has focus if (option.state & QStyle::State_HasFocus) { - drawBorder(painter, m_pFocusBorderColor, option.rect); + drawBorder(painter, m_focusBorderColor, option.rect); } } diff --git a/src/library/coverartdelegate.h b/src/library/tabledelegates/coverartdelegate.h similarity index 97% rename from src/library/coverartdelegate.h rename to src/library/tabledelegates/coverartdelegate.h index 84c69c5604a..f2e5b338915 100644 --- a/src/library/coverartdelegate.h +++ b/src/library/tabledelegates/coverartdelegate.h @@ -3,7 +3,7 @@ #include #include "library/coverart.h" -#include "library/tableitemdelegate.h" +#include "library/tabledelegates/tableitemdelegate.h" #include "track/track_decl.h" #include "util/cache.h" diff --git a/src/library/locationdelegate.cpp b/src/library/tabledelegates/locationdelegate.cpp similarity index 94% rename from src/library/locationdelegate.cpp rename to src/library/tabledelegates/locationdelegate.cpp index 43438d58e6e..9fa79339c8e 100644 --- a/src/library/locationdelegate.cpp +++ b/src/library/tabledelegates/locationdelegate.cpp @@ -1,4 +1,4 @@ -#include "library/locationdelegate.h" +#include "library/tabledelegates/locationdelegate.h" #include diff --git a/src/library/locationdelegate.h b/src/library/tabledelegates/locationdelegate.h similarity index 86% rename from src/library/locationdelegate.h rename to src/library/tabledelegates/locationdelegate.h index 9fb698dab21..43fd03cc79d 100644 --- a/src/library/locationdelegate.h +++ b/src/library/tabledelegates/locationdelegate.h @@ -1,7 +1,6 @@ #pragma once -#include "library/tableitemdelegate.h" - +#include "library/tabledelegates/tableitemdelegate.h" class LocationDelegate : public TableItemDelegate { Q_OBJECT diff --git a/src/library/multilineeditdelegate.cpp b/src/library/tabledelegates/multilineeditdelegate.cpp similarity index 99% rename from src/library/multilineeditdelegate.cpp rename to src/library/tabledelegates/multilineeditdelegate.cpp index e7c0fa7ae10..3dd6904fc8d 100644 --- a/src/library/multilineeditdelegate.cpp +++ b/src/library/tabledelegates/multilineeditdelegate.cpp @@ -1,4 +1,4 @@ -#include "library/multilineeditdelegate.h" +#include "library/tabledelegates/multilineeditdelegate.h" #include #include diff --git a/src/library/multilineeditdelegate.h b/src/library/tabledelegates/multilineeditdelegate.h similarity index 96% rename from src/library/multilineeditdelegate.h rename to src/library/tabledelegates/multilineeditdelegate.h index 692598cc422..d46bd048c26 100644 --- a/src/library/multilineeditdelegate.h +++ b/src/library/tabledelegates/multilineeditdelegate.h @@ -2,7 +2,7 @@ #include -#include "library/tableitemdelegate.h" +#include "library/tabledelegates/tableitemdelegate.h" /// A QPlainTextEdit to show all content lines in a scrollable view. /// * finish editing with Return key, like QLineEdit used for other text columns diff --git a/src/library/playcountdelegate.cpp b/src/library/tabledelegates/playcountdelegate.cpp similarity index 95% rename from src/library/playcountdelegate.cpp rename to src/library/tabledelegates/playcountdelegate.cpp index 4f25aaf8833..53bac452dc1 100644 --- a/src/library/playcountdelegate.cpp +++ b/src/library/tabledelegates/playcountdelegate.cpp @@ -1,4 +1,4 @@ -#include "library/playcountdelegate.h" +#include "library/tabledelegates/playcountdelegate.h" #include #include diff --git a/src/library/playcountdelegate.h b/src/library/tabledelegates/playcountdelegate.h similarity index 87% rename from src/library/playcountdelegate.h rename to src/library/tabledelegates/playcountdelegate.h index 87f3705ed57..ce791f733c2 100644 --- a/src/library/playcountdelegate.h +++ b/src/library/tabledelegates/playcountdelegate.h @@ -1,6 +1,6 @@ #pragma once -#include "library/tableitemdelegate.h" +#include "library/tabledelegates/tableitemdelegate.h" class QCheckBox; diff --git a/src/library/previewbuttondelegate.cpp b/src/library/tabledelegates/previewbuttondelegate.cpp similarity index 99% rename from src/library/previewbuttondelegate.cpp rename to src/library/tabledelegates/previewbuttondelegate.cpp index ca565cb185b..110b84d69fa 100644 --- a/src/library/previewbuttondelegate.cpp +++ b/src/library/tabledelegates/previewbuttondelegate.cpp @@ -1,4 +1,4 @@ -#include "library/previewbuttondelegate.h" +#include "library/tabledelegates/previewbuttondelegate.h" #include #include diff --git a/src/library/previewbuttondelegate.h b/src/library/tabledelegates/previewbuttondelegate.h similarity index 97% rename from src/library/previewbuttondelegate.h rename to src/library/tabledelegates/previewbuttondelegate.h index fdd1f0f06d2..6bb37e514f9 100644 --- a/src/library/previewbuttondelegate.h +++ b/src/library/tabledelegates/previewbuttondelegate.h @@ -2,7 +2,7 @@ #include -#include "library/tableitemdelegate.h" +#include "library/tabledelegates/tableitemdelegate.h" #include "track/track_decl.h" #include "util/parented_ptr.h" diff --git a/src/library/stardelegate.cpp b/src/library/tabledelegates/stardelegate.cpp similarity index 94% rename from src/library/stardelegate.cpp rename to src/library/tabledelegates/stardelegate.cpp index e8506f4dc56..62743016af8 100644 --- a/src/library/stardelegate.cpp +++ b/src/library/tabledelegates/stardelegate.cpp @@ -1,10 +1,10 @@ -#include "library/stardelegate.h" +#include "library/tabledelegates/stardelegate.h" #include -#include "library/stareditor.h" #include "library/starrating.h" -#include "library/tableitemdelegate.h" +#include "library/tabledelegates/stareditor.h" +#include "library/tabledelegates/tableitemdelegate.h" #include "moc_stardelegate.cpp" StarDelegate::StarDelegate(QTableView* pTableView) @@ -44,7 +44,7 @@ QWidget* StarDelegate::createEditor(QWidget* parent, initStyleOption(&newOption, index); StarEditor* editor = - new StarEditor(parent, m_pTableView, index, newOption, m_pFocusBorderColor); + new StarEditor(parent, m_pTableView, index, newOption, m_focusBorderColor); connect(editor, &StarEditor::editingFinished, this, diff --git a/src/library/stardelegate.h b/src/library/tabledelegates/stardelegate.h similarity index 95% rename from src/library/stardelegate.h rename to src/library/tabledelegates/stardelegate.h index 3ac6bd7fd3a..39d0f1457d9 100644 --- a/src/library/stardelegate.h +++ b/src/library/tabledelegates/stardelegate.h @@ -1,6 +1,6 @@ #pragma once -#include "library/tableitemdelegate.h" +#include "library/tabledelegates/tableitemdelegate.h" class StarDelegate : public TableItemDelegate { Q_OBJECT diff --git a/src/library/stareditor.cpp b/src/library/tabledelegates/stareditor.cpp similarity index 95% rename from src/library/stareditor.cpp rename to src/library/tabledelegates/stareditor.cpp index 890e960f2ed..a4e1ba78a6c 100644 --- a/src/library/stareditor.cpp +++ b/src/library/tabledelegates/stareditor.cpp @@ -1,4 +1,4 @@ -#include "library/stareditor.h" +#include "library/tabledelegates/stareditor.h" #include #include @@ -6,7 +6,7 @@ #include #include "library/starrating.h" -#include "library/tableitemdelegate.h" +#include "library/tabledelegates/tableitemdelegate.h" #include "moc_stareditor.cpp" // We enable mouse tracking on the widget so we can follow the cursor even @@ -28,7 +28,7 @@ StarEditor::StarEditor(QWidget* parent, m_pTableView(pTableView), m_index(index), m_styleOption(option), - m_pFocusBorderColor(focusBorderColor), + m_focusBorderColor(focusBorderColor), m_starCount(StarRating::kMinStarCount) { DEBUG_ASSERT(m_pTableView); setMouseTracking(true); @@ -93,7 +93,7 @@ void StarEditor::paintEvent(QPaintEvent*) { // Draw a border if the color cell is selected if (m_styleOption.state & QStyle::State_HasFocus) { - TableItemDelegate::drawBorder(&painter, m_pFocusBorderColor, m_styleOption.rect); + TableItemDelegate::drawBorder(&painter, m_focusBorderColor, m_styleOption.rect); } } diff --git a/src/library/stareditor.h b/src/library/tabledelegates/stareditor.h similarity index 97% rename from src/library/stareditor.h rename to src/library/tabledelegates/stareditor.h index 306e1ca76c7..88753d02bcb 100644 --- a/src/library/stareditor.h +++ b/src/library/tabledelegates/stareditor.h @@ -46,7 +46,7 @@ class StarEditor : public QWidget { QTableView* m_pTableView; QModelIndex m_index; QStyleOptionViewItem m_styleOption; - QColor m_pFocusBorderColor; + QColor m_focusBorderColor; StarRating m_starRating; int m_starCount; }; diff --git a/src/library/tableitemdelegate.cpp b/src/library/tabledelegates/tableitemdelegate.cpp similarity index 71% rename from src/library/tableitemdelegate.cpp rename to src/library/tabledelegates/tableitemdelegate.cpp index 30c44b679f8..229b08caa81 100644 --- a/src/library/tableitemdelegate.cpp +++ b/src/library/tabledelegates/tableitemdelegate.cpp @@ -1,4 +1,4 @@ -#include "library/tableitemdelegate.h" +#include "library/tabledelegates/tableitemdelegate.h" #include @@ -16,7 +16,16 @@ TableItemDelegate::TableItemDelegate(QTableView* pTableView) // WTrackTableView { // qproperty-focusBorderColor: red; // } - m_pFocusBorderColor = pTrackTableView->getFocusBorderColor(); + m_focusBorderColor = pTrackTableView->getFocusBorderColor(); + // For some reason the color is not initialized from the stylesheet for + // some WTrackTableViews (in DlgAutoDJ, DlgAnalysis, ...) + // Listen to the property changed signal. + connect(pTrackTableView, + &WTrackTableView::focusBorderColorChanged, + this, + [this](QColor col) { + m_focusBorderColor = col; + }); } } @@ -39,7 +48,20 @@ void TableItemDelegate::paint( if (option.state & QStyle::State_Selected) { painter->setBrush(option.palette.color(cg, QPalette::HighlightedText)); } else { - painter->setBrush(option.palette.color(cg, QPalette::Text)); + // This gets the custom 'played' text color from BaseTrackTableModel + // depending on check state of the 'played' column. + // Note that we need to do this again in BPMDelegate which uses the + // style of the TableView. + auto playedColorData = index.data(Qt::ForegroundRole); + if (playedColorData.canConvert()) { + QColor playedColor = playedColorData.value(); + // for the star rating polygons + painter->setBrush(playedColor); + // for the 'location' text + painter->setPen(playedColor); + } else { + painter->setBrush(option.palette.color(cg, QPalette::Text)); + } } QStyle* style = m_pTableView->style(); diff --git a/src/library/tableitemdelegate.h b/src/library/tabledelegates/tableitemdelegate.h similarity index 97% rename from src/library/tableitemdelegate.h rename to src/library/tabledelegates/tableitemdelegate.h index bf47645cafa..36973761c7c 100644 --- a/src/library/tableitemdelegate.h +++ b/src/library/tabledelegates/tableitemdelegate.h @@ -36,6 +36,6 @@ class TableItemDelegate : public QStyledItemDelegate { // Having this here avoids including QTableView there. int columnWidth(const QModelIndex &index) const; - QColor m_pFocusBorderColor; + QColor m_focusBorderColor; QTableView* m_pTableView; }; diff --git a/src/library/trackmodel.h b/src/library/trackmodel.h index c811114709a..53bf53ddbc9 100644 --- a/src/library/trackmodel.h +++ b/src/library/trackmodel.h @@ -140,6 +140,16 @@ class TrackModel { virtual void removeTracks(const QModelIndexList& indices) { Q_UNUSED(indices); } + virtual void cutTracks(const QModelIndexList& indices) { + Q_UNUSED(indices); + } + virtual void copyTracks(const QModelIndexList& indices) const { + Q_UNUSED(indices); + } + virtual QList pasteTracks(const QModelIndex& index) { + Q_UNUSED(index); + return QList(); + } virtual void hideTracks(const QModelIndexList& indices) { Q_UNUSED(indices); } @@ -154,6 +164,14 @@ class TrackModel { Q_UNUSED(locations); return 0; } + virtual int addTracksWithTrackIds(const QModelIndex& index, + const QList& tracks, + int* pOutInsertionPos) { + Q_UNUSED(index); + Q_UNUSED(tracks); + Q_UNUSED(pOutInsertionPos); + return 0; + } virtual void moveTrack(const QModelIndex& sourceIndex, const QModelIndex& destIndex) { Q_UNUSED(sourceIndex); diff --git a/src/library/trackset/baseplaylistfeature.cpp b/src/library/trackset/baseplaylistfeature.cpp index ac81372e687..5221873712e 100644 --- a/src/library/trackset/baseplaylistfeature.cpp +++ b/src/library/trackset/baseplaylistfeature.cpp @@ -39,12 +39,14 @@ BasePlaylistFeature::BasePlaylistFeature( UserSettingsPointer pConfig, PlaylistTableModel* pModel, const QString& rootViewName, - const QString& iconName) + const QString& iconName, + bool keepHiddenTracks) : BaseTrackSetFeature(pLibrary, pConfig, rootViewName, iconName), m_playlistDao(pLibrary->trackCollectionManager() ->internalCollection() ->getPlaylistDAO()), - m_pPlaylistTableModel(pModel) { + m_pPlaylistTableModel(pModel), + m_keepHiddenTracks(keepHiddenTracks) { pModel->setParent(this); initActions(); @@ -165,7 +167,7 @@ void BasePlaylistFeature::connectPlaylistDAO() { &BasePlaylistFeature::slotPlaylistTableRenamed); } -int BasePlaylistFeature::playlistIdFromIndex(const QModelIndex& index) { +int BasePlaylistFeature::playlistIdFromIndex(const QModelIndex& index) const { TreeItem* item = static_cast(index.internalPointer()); if (item == nullptr) { return kInvalidPlaylistId; @@ -568,7 +570,8 @@ void BasePlaylistFeature::slotExportPlaylist() { std::unique_ptr pPlaylistTableModel = std::make_unique(this, m_pLibrary->trackCollectionManager(), - "mixxx.db.model.playlist_export"); + "mixxx.db.model.playlist_export", + m_keepHiddenTracks); emit saveModelState(); pPlaylistTableModel->selectPlaylist(playlistId); @@ -804,6 +807,16 @@ void BasePlaylistFeature::markTreeItem(TreeItem* pTreeItem) { } } +QString BasePlaylistFeature::createPlaylistLabel(const QString& name, + int count, + int duration) const { + return QStringLiteral("%1 (%2) %3") + .arg(name, + QString::number(count), + mixxx::Duration::formatTime( + duration, mixxx::Duration::Precision::SECONDS)); +} + void BasePlaylistFeature::slotResetSelectedTrack() { slotTrackSelected(TrackId{}); } diff --git a/src/library/trackset/baseplaylistfeature.h b/src/library/trackset/baseplaylistfeature.h index 761f94305be..dc20c588c71 100644 --- a/src/library/trackset/baseplaylistfeature.h +++ b/src/library/trackset/baseplaylistfeature.h @@ -25,7 +25,8 @@ class BasePlaylistFeature : public BaseTrackSetFeature { UserSettingsPointer pConfig, PlaylistTableModel* pModel, const QString& rootViewName, - const QString& iconName); + const QString& iconName, + bool keepHiddenTracks = false); ~BasePlaylistFeature() override = default; TreeItemModel* sidebarModel() const override; @@ -86,12 +87,14 @@ class BasePlaylistFeature : public BaseTrackSetFeature { virtual void decorateChild(TreeItem* pChild, int playlistId) = 0; virtual void addToAutoDJ(PlaylistDAO::AutoDJSendLoc loc); - int playlistIdFromIndex(const QModelIndex& index); + int playlistIdFromIndex(const QModelIndex& index) const; // Get the QModelIndex of a playlist based on its id. Returns QModelIndex() // on failure. QModelIndex indexFromPlaylistId(int playlistId); bool isChildIndexSelectedInSidebar(const QModelIndex& index); + QString createPlaylistLabel(const QString& name, int count, int duration) const; + PlaylistDAO& m_playlistDao; QModelIndex m_lastClickedIndex; QModelIndex m_lastRightClickedIndex; @@ -126,4 +129,6 @@ class BasePlaylistFeature : public BaseTrackSetFeature { void markTreeItem(TreeItem* pTreeItem); TrackId m_selectedTrackId; + + const bool m_keepHiddenTracks; }; diff --git a/src/library/trackset/basetracksetfeature.cpp b/src/library/trackset/basetracksetfeature.cpp index a50b9b94019..51f18f1f436 100644 --- a/src/library/trackset/basetracksetfeature.cpp +++ b/src/library/trackset/basetracksetfeature.cpp @@ -13,6 +13,10 @@ BaseTrackSetFeature::BaseTrackSetFeature( m_pSidebarModel(make_parented(this)) { } +void BaseTrackSetFeature::pasteChild(const QModelIndex&) { + emit pasteFromSidebar(); +} + void BaseTrackSetFeature::activate() { emit switchToView(m_rootViewName); emit disableSearch(); diff --git a/src/library/trackset/basetracksetfeature.h b/src/library/trackset/basetracksetfeature.h index 6869d97ac46..c647f9d046f 100644 --- a/src/library/trackset/basetracksetfeature.h +++ b/src/library/trackset/basetracksetfeature.h @@ -14,6 +14,7 @@ class BaseTrackSetFeature : public LibraryFeature { const QString& rootViewName, const QString& iconName); + void pasteChild(const QModelIndex& index) override; signals: void analyzeTracks(const QList&); diff --git a/src/library/trackset/crate/cratetablemodel.cpp b/src/library/trackset/crate/cratetablemodel.cpp index 60eb14eed34..d574f90eb35 100644 --- a/src/library/trackset/crate/cratetablemodel.cpp +++ b/src/library/trackset/crate/cratetablemodel.cpp @@ -150,17 +150,22 @@ TrackModel::Capabilities CrateTableModel::getCapabilities() const { return caps; } -int CrateTableModel::addTracks( - const QModelIndex& index, const QList& locations) { +int CrateTableModel::addTracksWithTrackIds( + const QModelIndex& index, const QList& trackIds, int* pOutInsertionPos) { Q_UNUSED(index); + + if (pOutInsertionPos != nullptr) { + // crate insertion is not done by position, and no duplicates will be added,. + // 0 indicates this to the caller. + *pOutInsertionPos = 0; + } + // If a track is dropped but it isn't in the library, then add it because // the user probably dropped a file from outside Mixxx into this crate. - QList trackIds = - m_pTrackCollectionManager->resolveTrackIdsFromLocations(locations); if (!m_pTrackCollectionManager->internalCollection()->addCrateTracks( m_selectedCrate, trackIds)) { qWarning() << "CrateTableModel::addTracks could not add" - << locations.size() << "tracks to crate" << m_selectedCrate; + << trackIds.size() << "tracks to crate" << m_selectedCrate; return 0; } @@ -168,6 +173,17 @@ int CrateTableModel::addTracks( return trackIds.size(); } +bool CrateTableModel::isLocked() { + Crate crate; + if (!m_pTrackCollectionManager->internalCollection() + ->crates() + .readCrateById(m_selectedCrate, &crate)) { + qWarning() << "Failed to read create" << m_selectedCrate; + return false; + } + return crate.isLocked(); +} + void CrateTableModel::removeTracks(const QModelIndexList& indices) { VERIFY_OR_DEBUG_ASSERT(m_selectedCrate.isValid()) { return; diff --git a/src/library/trackset/crate/cratetablemodel.h b/src/library/trackset/crate/cratetablemodel.h index 32760e32fb4..6e977b4a1d8 100644 --- a/src/library/trackset/crate/cratetablemodel.h +++ b/src/library/trackset/crate/cratetablemodel.h @@ -19,7 +19,10 @@ class CrateTableModel final : public TrackSetTableModel { void removeTracks(const QModelIndexList& indices) final; /// Returns the number of unsuccessful additions. - int addTracks(const QModelIndex& index, const QList& locations) final; + int addTracksWithTrackIds(const QModelIndex& index, + const QList& tracks, + int* pOutInsertionPos) final; + bool isLocked() final; Capabilities getCapabilities() const final; QString modelKey(bool noSearch) const override; diff --git a/src/library/trackset/playlistfeature.cpp b/src/library/trackset/playlistfeature.cpp index d76e0923a83..4f00f80f07b 100644 --- a/src/library/trackset/playlistfeature.cpp +++ b/src/library/trackset/playlistfeature.cpp @@ -18,21 +18,6 @@ #include "widget/wlibrarysidebar.h" #include "widget/wtracktableview.h" -namespace { - -QString createPlaylistLabel( - const QString& name, - int count, - int duration) { - return QStringLiteral("%1 (%2) %3") - .arg(name, - QString::number(count), - mixxx::Duration::formatTime( - duration, mixxx::Duration::Precision::SECONDS)); -} - -} // anonymous namespace - PlaylistFeature::PlaylistFeature(Library* pLibrary, UserSettingsPointer pConfig) : BasePlaylistFeature(pLibrary, pConfig, @@ -152,7 +137,7 @@ QList PlaylistFeature::createPlaylistLabels() { " ON PlaylistTracks.playlist_id = Playlists.id " "LEFT JOIN library " " ON PlaylistTracks.track_id = library.id " - " WHERE Playlists.hidden = 0 " + " WHERE Playlists.hidden = 0 " // PlaylistDAO::HiddenType::PLHT_NOT_HIDDEN " GROUP BY Playlists.id"); queryString.append( mixxx::DbConnection::collateLexicographically( diff --git a/src/library/trackset/setlogfeature.cpp b/src/library/trackset/setlogfeature.cpp index 74ca557f2b6..6aeafb53fee 100644 --- a/src/library/trackset/setlogfeature.cpp +++ b/src/library/trackset/setlogfeature.cpp @@ -34,9 +34,10 @@ SetlogFeature::SetlogFeature( nullptr, pLibrary->trackCollectionManager(), "mixxx.db.model.setlog", - /*keep deleted tracks*/ true), + /*keep hidden tracks*/ true), QStringLiteral("SETLOGHOME"), - QStringLiteral("history")), + QStringLiteral("history"), + /*keep hidden tracks*/ true), m_currentPlaylistId(kInvalidPlaylistId), m_yearNodeId(kInvalidPlaylistId), m_pLibrary(pLibrary), @@ -226,14 +227,42 @@ void SetlogFeature::onRightClickChild(const QPoint& globalPos, const QModelIndex /// Use a custom model in the history for grouping by year /// @param selectedId row which should be selected QModelIndex SetlogFeature::constructChildModel(int selectedId) { - // qDebug() << "SetlogFeature::constructChildModel() id:" << selectedId; + // qDebug() << "SetlogFeature::constructChildModel() selected:" << selectedId; // Setup the sidebar playlist model - QSqlTableModel playlistTableModel(this, - m_pLibrary->trackCollectionManager()->internalCollection()->database()); - playlistTableModel.setTable("Playlists"); - playlistTableModel.setFilter("hidden=" + QString::number(PlaylistDAO::PLHT_SET_LOG)); - playlistTableModel.setSort( - playlistTableModel.fieldIndex("id"), Qt::DescendingOrder); + QSqlDatabase database = + m_pLibrary->trackCollectionManager()->internalCollection()->database(); + + QString queryString = QStringLiteral( + "CREATE TEMPORARY VIEW IF NOT EXISTS SetlogCountsDurations " + "AS SELECT " + " Playlists.id AS id, " + " Playlists.name AS name, " + " Playlists.date_created AS date_created, " + " LOWER(Playlists.name) AS sort_name, " + " COUNT(case library.mixxx_deleted when 0 then 1 else null end) " + " AS count, " + " SUM(case library.mixxx_deleted " + " when 0 then library.duration else 0 end) AS durationSeconds " + "FROM Playlists " + "LEFT JOIN PlaylistTracks " + " ON PlaylistTracks.playlist_id = Playlists.id " + "LEFT JOIN library " + " ON PlaylistTracks.track_id = library.id " + " WHERE Playlists.hidden = %1 " + " GROUP BY Playlists.id") + .arg(QString::number(PlaylistDAO::HiddenType::PLHT_SET_LOG)); + queryString.append( + mixxx::DbConnection::collateLexicographically( + " ORDER BY sort_name")); + QSqlQuery query(database); + if (!query.exec(queryString)) { + LOG_FAILED_QUERY(query); + } + + // Setup the sidebar playlist model + QSqlTableModel playlistTableModel(this, database); + playlistTableModel.setTable("SetlogCountsDurations"); + playlistTableModel.setSort(playlistTableModel.fieldIndex("id"), Qt::DescendingOrder); playlistTableModel.select(); while (playlistTableModel.canFetchMore()) { playlistTableModel.fetchMore(); @@ -242,6 +271,8 @@ QModelIndex SetlogFeature::constructChildModel(int selectedId) { int nameColumn = record.indexOf("name"); int idColumn = record.indexOf("id"); int createdColumn = record.indexOf("date_created"); + int countColumn = record.indexOf("count"); + int durationColumn = record.indexOf("durationSeconds"); // Nice to have: restore previous expanded/collapsed state of YEAR items clearChildModel(); @@ -263,8 +294,16 @@ QModelIndex SetlogFeature::constructChildModel(int selectedId) { playlistTableModel .data(playlistTableModel.index(row, createdColumn)) .toDateTime(); + int count = playlistTableModel + .data(playlistTableModel.index(row, countColumn)) + .toInt(); + int duration = + playlistTableModel + .data(playlistTableModel.index(row, durationColumn)) + .toInt(); + QString label = createPlaylistLabel(name, count, duration); - // Create the TreeItem whose parent is the invisible root item + // Create the TreeItem whose parent is the invisible root item. // Show only [kNumToplevelHistoryEntries] recent playlists at the top level // before grouping them by year. if (row >= kNumToplevelHistoryEntries) { @@ -286,12 +325,12 @@ QModelIndex SetlogFeature::constructChildModel(int selectedId) { itemList.push_back(std::move(pNewGroupItem)); } - TreeItem* pItem = pGroupItem->appendChild(name, id); + TreeItem* pItem = pGroupItem->appendChild(label, id); pItem->setBold(m_playlistIdsOfSelectedTrack.contains(id)); decorateChild(pItem, id); } else { // add most recent top-level playlist - auto pItem = std::make_unique(name, id); + auto pItem = std::make_unique(label, id); pItem->setBold(m_playlistIdsOfSelectedTrack.contains(id)); decorateChild(pItem.get(), id); diff --git a/src/library/trackset/tracksettablemodel.cpp b/src/library/trackset/tracksettablemodel.cpp index f27bd25efae..1e53c73ca60 100644 --- a/src/library/trackset/tracksettablemodel.cpp +++ b/src/library/trackset/tracksettablemodel.cpp @@ -1,5 +1,6 @@ #include "library/trackset/tracksettablemodel.h" +#include "library/trackcollectionmanager.h" #include "mixer/playermanager.h" #include "moc_tracksettablemodel.cpp" @@ -26,3 +27,15 @@ bool TrackSetTableModel::isColumnInternal(int column) { column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COVERART_DIGEST) || column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COVERART_HASH); } + +int TrackSetTableModel::addTracks(const QModelIndex& index, + const QList& locations) { + if (locations.isEmpty()) { + return 0; + } + + QList trackIds = m_pTrackCollectionManager->resolveTrackIdsFromLocations( + locations); + + return addTracksWithTrackIds(index, trackIds, nullptr); +} diff --git a/src/library/trackset/tracksettablemodel.h b/src/library/trackset/tracksettablemodel.h index 649d33ef325..94202c9a853 100644 --- a/src/library/trackset/tracksettablemodel.h +++ b/src/library/trackset/tracksettablemodel.h @@ -11,4 +11,6 @@ class TrackSetTableModel : public BaseSqlTableModel { const char* settingsNamespace); bool isColumnInternal(int column) override; + + int addTracks(const QModelIndex& index, const QList& locations) final; }; diff --git a/src/library/traktor/traktorfeature.cpp b/src/library/traktor/traktorfeature.cpp index 0cb61d2dd6d..d6773cf5fc3 100644 --- a/src/library/traktor/traktorfeature.cpp +++ b/src/library/traktor/traktorfeature.cpp @@ -10,7 +10,7 @@ #include "library/library.h" #include "library/librarytablemodel.h" -#include "library/missingtablemodel.h" +#include "library/missing_hidden/missingtablemodel.h" #include "library/queryutil.h" #include "library/trackcollection.h" #include "library/trackcollectionmanager.h" diff --git a/src/mixer/basetrackplayer.cpp b/src/mixer/basetrackplayer.cpp index dbdb1416012..0f5cf8a80a6 100644 --- a/src/mixer/basetrackplayer.cpp +++ b/src/mixer/basetrackplayer.cpp @@ -326,6 +326,12 @@ void BaseTrackPlayerImpl::slotEjectTrack(double v) { return; } + // Don't allow eject while playing a track. We don't need to lock to + // call ControlObject::get() so this is fine. + if (m_pPlay->toBool()) { + return; + } + mixxx::Duration elapsed = m_ejectTimer.restart(); // Double-click always restores the last replaced track, i.e. un-eject the second @@ -347,11 +353,6 @@ void BaseTrackPlayerImpl::slotEjectTrack(double v) { return; } - // Don't allow rejections while playing a track. We don't need to lock to - // call ControlObject::get() so this is fine. - if (m_pPlay->toBool()) { - return; - } m_pChannel->getEngineBuffer()->ejectTrack(); } diff --git a/src/mixer/playermanager.cpp b/src/mixer/playermanager.cpp index 552873426aa..a4f0f1ea15e 100644 --- a/src/mixer/playermanager.cpp +++ b/src/mixer/playermanager.cpp @@ -696,6 +696,16 @@ void PlayerManager::slotLoadTrackToPlayer(TrackPointer pTrack, const QString& gr // so clone another playing deck instead of loading the selected track clone = true; } + } else if (isPreviewDeckGroup(group) && play) { + // This extends/overrides the behaviour of [PreviewDeckN],LoadSelectedTrackAndPlay: + // if the track is already loaded, toggle play/pause. + if (pTrack == pPlayer->getLoadedTrack()) { + auto* pPlay = + ControlObject::getControl(ConfigKey(group, QStringLiteral("play"))); + double newPlay = pPlay->toBool() ? 0.0 : 1.0; + pPlay->set(newPlay); + return; + } } if (clone) { diff --git a/src/mixxxapplication.cpp b/src/mixxxapplication.cpp index eaae3838013..6f11149942a 100644 --- a/src/mixxxapplication.cpp +++ b/src/mixxxapplication.cpp @@ -23,7 +23,9 @@ // https://doc.qt.io/qt-5/plugins-howto.html#details-of-linking-static-plugins #ifdef QT_STATIC #include -#if defined(Q_OS_WIN) +#if defined(Q_OS_WASM) +Q_IMPORT_PLUGIN(QWasmIntegrationPlugin) +#elif defined(Q_OS_WIN) Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin) Q_IMPORT_PLUGIN(QWindowsVistaStylePlugin) #elif defined(Q_OS_IOS) @@ -36,8 +38,10 @@ Q_IMPORT_PLUGIN(QXcbIntegrationPlugin) #else #error "Q_IMPORT_PLUGIN() for the current patform is missing" #endif +#if !defined(Q_OS_WASM) Q_IMPORT_PLUGIN(QOffscreenIntegrationPlugin) Q_IMPORT_PLUGIN(QMinimalIntegrationPlugin) +#endif Q_IMPORT_PLUGIN(QSQLiteDriverPlugin) Q_IMPORT_PLUGIN(QSvgPlugin) diff --git a/src/mixxxmainwindow.cpp b/src/mixxxmainwindow.cpp index b6bc0889a7e..5c16b18f251 100644 --- a/src/mixxxmainwindow.cpp +++ b/src/mixxxmainwindow.cpp @@ -398,6 +398,12 @@ void MixxxMainWindow::initialize() { &PlayerInfo::currentPlayingTrackChanged, this, &MixxxMainWindow::slotUpdateWindowTitle); + + // Start Auto DJ if the cmdline arg is passed. + if (CmdlineArgs::Instance().getStartAutoDJ()) { + qDebug("Enabling Auto DJ from CLI flag."); + ControlObject::set(ConfigKey("[AutoDJ]", "enabled"), 1.0); + } } MixxxMainWindow::~MixxxMainWindow() { diff --git a/src/preferences/configobject.cpp b/src/preferences/configobject.cpp index aa0315636ff..92b596b09ab 100644 --- a/src/preferences/configobject.cpp +++ b/src/preferences/configobject.cpp @@ -22,6 +22,12 @@ QString computeResourcePathImpl() { QString qResourcePath = CmdlineArgs::Instance().getResourcePath(); if (qResourcePath.isEmpty()) { +#ifdef __EMSCRIPTEN__ + // When targeting Emscripten/WebAssembly, we have a virtual file system + // that is populated by our preloaded resources located at /res. See + // also https://emscripten.org/docs/porting/files/packaging_files.html + qResourcePath = "/res"; +#else QDir mixxxDir = QCoreApplication::applicationDirPath(); // We used to support using the mixxx.cfg's [Config],Path setting but @@ -76,6 +82,7 @@ QString computeResourcePathImpl() { // TODO(rryan): What should we do here? } #endif +#endif // !defined(__EMSCRIPTEN__) } else { //qDebug() << "Setting qResourcePath from location in resourcePath commandline arg:" << qResourcePath; } diff --git a/src/preferences/dialog/dlgpreflibrary.cpp b/src/preferences/dialog/dlgpreflibrary.cpp index 5d0c2ef3232..1cc4f523222 100644 --- a/src/preferences/dialog/dlgpreflibrary.cpp +++ b/src/preferences/dialog/dlgpreflibrary.cpp @@ -211,6 +211,8 @@ void DlgPrefLibrary::slotResetToDefaults() { checkBoxEditMetadataSelectedClicked->setChecked(kEditMetadataSelectedClickDefault); radioButton_dbclick_deck->setChecked(true); spinbox_bpm_precision->setValue(BaseTrackTableModel::kBpmColumnPrecisionDefault); + checkbox_played_track_color->setChecked( + BaseTrackTableModel::kApplyPlayedTrackColorDefault); radioButton_cover_art_fetcher_medium->setChecked(true); @@ -325,6 +327,12 @@ void DlgPrefLibrary::slotUpdate() { kBpmColumnPrecisionConfigKey, BaseTrackTableModel::kBpmColumnPrecisionDefault); spinbox_bpm_precision->setValue(bpmColumnPrecision); + + const auto applyPlayedTrackColor = + m_pConfig->getValue( + mixxx::library::prefs::kApplyPlayedTrackColorConfigKey, + BaseTrackTableModel::kApplyPlayedTrackColorDefault); + checkbox_played_track_color->setChecked(applyPlayedTrackColor); } void DlgPrefLibrary::slotCancel() { @@ -523,6 +531,12 @@ void DlgPrefLibrary::slotApply() { ConfigValue(rowHeight)); } + BaseTrackTableModel::setApplyPlayedTrackColor( + checkbox_played_track_color->isChecked()); + m_pConfig->set( + mixxx::library::prefs::kApplyPlayedTrackColorConfigKey, + ConfigValue(checkbox_played_track_color->isChecked())); + // TODO(rryan): Don't save here. m_pConfig->save(); } diff --git a/src/preferences/dialog/dlgpreflibrarydlg.ui b/src/preferences/dialog/dlgpreflibrarydlg.ui index ec528bb00ae..2b670f8d4d8 100644 --- a/src/preferences/dialog/dlgpreflibrarydlg.ui +++ b/src/preferences/dialog/dlgpreflibrarydlg.ui @@ -219,6 +219,14 @@
    + + + + Grey out played tracks + + + + @@ -636,6 +644,7 @@ radioButton_dbclick_top radioButton_dbclick_ignore spinbox_bpm_precision + checkbox_played_track_color spinbox_history_track_duplicate_distance spinbox_history_min_tracks_to_keep checkBox_use_relative_path diff --git a/src/proto/CMakeLists.txt b/src/proto/CMakeLists.txt index 4c365331ecf..4f2995e685a 100644 --- a/src/proto/CMakeLists.txt +++ b/src/proto/CMakeLists.txt @@ -17,6 +17,14 @@ else() message(FATAL_ERROR "Protobuf or Protobuf-lite libraries are required to compile Mixxx.") endif() +if(EMSCRIPTEN) + # If we try linking in a proto lib built without -pthread we get + # wasm-ld: error: --shared-memory is disallowed by keys.pb.cc.o + # because it was not compiled with 'atomics' or 'bulk-memory' features. + # See https://groups.google.com/g/emscripten-discuss/c/G4nwFprZFYo + target_compile_options(mixxx-proto PRIVATE -pthread) +endif() + protobuf_generate( LANGUAGE cpp TARGET mixxx-proto diff --git a/src/qml/qmlbeatsmodel.cpp b/src/qml/qmlbeatsmodel.cpp new file mode 100644 index 00000000000..09b84f90712 --- /dev/null +++ b/src/qml/qmlbeatsmodel.cpp @@ -0,0 +1,73 @@ +#include "qml/qmlbeatsmodel.h" + +#include + +namespace mixxx { +namespace qml { +namespace { +const QHash kRoleNames = { + {QmlBeatsModel::FramePositionRole, "framePosition"}, +}; +} + +QmlBeatsModel::QmlBeatsModel( + QObject* parent) + : QAbstractListModel(parent), m_pBeats(nullptr), m_numBeats(0) { +} + +void QmlBeatsModel::setBeats(const BeatsPointer pBeats, audio::FramePos trackEndPosition) { + beginResetModel(); + m_numBeats = 0; + m_pBeats = pBeats; + if (pBeats != nullptr) { + m_numBeats = pBeats->numBeatsInRange(audio::kStartFramePos, trackEndPosition); + } + endResetModel(); +} + +QVariant QmlBeatsModel::data(const QModelIndex& index, int role) const { + if (index.row() < 0 || index.row() >= m_numBeats) { + return QVariant(); + } + + const BeatsPointer pBeats = m_pBeats; + if (pBeats == nullptr) { + return QVariant(); + } + + auto it = pBeats->iteratorFrom(audio::kStartFramePos) + index.row(); + VERIFY_OR_DEBUG_ASSERT(it != pBeats->cend()) { + return QVariant(); + } + + switch (role) { + case QmlBeatsModel::FramePositionRole: + return (*it).value(); + default: + return QVariant(); + } +} + +int QmlBeatsModel::rowCount(const QModelIndex& parent) const { + if (parent.isValid()) { + return 0; + } + + return m_numBeats; +} + +QHash QmlBeatsModel::roleNames() const { + return kRoleNames; +} + +QVariant QmlBeatsModel::get(int row) const { + QModelIndex idx = index(row, 0); + QVariantMap dataMap; + for (auto it = kRoleNames.constBegin(); it != kRoleNames.constEnd(); it++) { + dataMap.insert(it.value(), data(idx, it.key())); + } + return dataMap; +} + +} // namespace qml +} // namespace mixxx diff --git a/src/qml/qmlbeatsmodel.h b/src/qml/qmlbeatsmodel.h new file mode 100644 index 00000000000..0ce8590db55 --- /dev/null +++ b/src/qml/qmlbeatsmodel.h @@ -0,0 +1,33 @@ +#pragma once +#include +#include + +#include "track/beats.h" + +namespace mixxx { +namespace qml { + +class QmlBeatsModel : public QAbstractListModel { + Q_OBJECT + public: + enum Roles { + FramePositionRole = Qt::UserRole + 1, + }; + Q_ENUM(Roles) + + explicit QmlBeatsModel(QObject* parent = nullptr); + + void setBeats(const BeatsPointer pBeats, audio::FramePos trackEndPosition); + + QVariant data(const QModelIndex& index, int role) const override; + int rowCount(const QModelIndex& parent) const override; + QHash roleNames() const override; + Q_INVOKABLE QVariant get(int row) const; + + private: + BeatsPointer m_pBeats; + int m_numBeats; +}; + +} // namespace qml +} // namespace mixxx diff --git a/src/qml/qmlcuesmodel.cpp b/src/qml/qmlcuesmodel.cpp new file mode 100644 index 00000000000..78908836558 --- /dev/null +++ b/src/qml/qmlcuesmodel.cpp @@ -0,0 +1,83 @@ +#include "qml/qmlcuesmodel.h" + +#include + +#include "moc_qmlcuesmodel.cpp" +#include "track/cue.h" + +namespace mixxx { +namespace qml { +namespace { +const QHash kRoleNames = { + {QmlCuesModel::StartPositionRole, "startPosition"}, + {QmlCuesModel::EndPositionRole, "endPosition"}, + {QmlCuesModel::LabelRole, "label"}, + {QmlCuesModel::IsLoopRole, "isLoop"}, + {QmlCuesModel::HotcueNumberRole, "hotcueNumber"}, +}; +} + +QmlCuesModel::QmlCuesModel( + QObject* pParent) + : QAbstractListModel(pParent) { +} + +void QmlCuesModel::setCues(QList cues) { + beginResetModel(); + m_cues = QList(std::move(cues)); + endResetModel(); +} + +QVariant QmlCuesModel::data(const QModelIndex& index, int role) const { + if (index.row() < 0 || index.row() >= m_cues.size()) { + return QVariant(); + } + + const CuePointer& pCue = m_cues.at(index.row()); + VERIFY_OR_DEBUG_ASSERT(pCue.get()) { + return QVariant(); + } + + switch (role) { + case QmlCuesModel::StartPositionRole: { + const auto position = pCue->getPosition(); + return position.isValid() ? position.value() : QVariant(); + } + case QmlCuesModel::EndPositionRole: { + const auto position = pCue->getEndPosition(); + return position.isValid() ? position.value() : QVariant(); + } + case QmlCuesModel::LabelRole: + return pCue->getLabel(); + case QmlCuesModel::IsLoopRole: + return pCue->getType() == CueType::Loop; + case QmlCuesModel::HotcueNumberRole: + return pCue->getHotCue(); + default: + return QVariant(); + } +} + +int QmlCuesModel::rowCount(const QModelIndex& parent) const { + if (parent.isValid()) { + return 0; + } + + return m_cues.size(); +} + +QHash QmlCuesModel::roleNames() const { + return kRoleNames; +} + +QVariant QmlCuesModel::get(int row) const { + QModelIndex idx = index(row, 0); + QVariantMap dataMap; + for (auto it = kRoleNames.constBegin(); it != kRoleNames.constEnd(); it++) { + dataMap.insert(it.value(), data(idx, it.key())); + } + return dataMap; +} + +} // namespace qml +} // namespace mixxx diff --git a/src/qml/qmlcuesmodel.h b/src/qml/qmlcuesmodel.h new file mode 100644 index 00000000000..1c864f6d2d1 --- /dev/null +++ b/src/qml/qmlcuesmodel.h @@ -0,0 +1,36 @@ +#pragma once +#include +#include + +class CuePointer; + +namespace mixxx { +namespace qml { + +class QmlCuesModel : public QAbstractListModel { + Q_OBJECT + public: + enum Roles { + StartPositionRole = Qt::UserRole + 1, + EndPositionRole, + LabelRole, + IsLoopRole, + HotcueNumberRole, + }; + Q_ENUM(Roles) + + explicit QmlCuesModel(QObject* pParent = nullptr); + + void setCues(QList cues); + + QVariant data(const QModelIndex& index, int role) const override; + int rowCount(const QModelIndex& parent) const override; + QHash roleNames() const override; + Q_INVOKABLE QVariant get(int row) const; + + private: + QList m_cues; +}; + +} // namespace qml +} // namespace mixxx diff --git a/src/qml/qmlplayerproxy.cpp b/src/qml/qmlplayerproxy.cpp index e35a9fc9946..f15a1197489 100644 --- a/src/qml/qmlplayerproxy.cpp +++ b/src/qml/qmlplayerproxy.cpp @@ -1,5 +1,7 @@ #include "qml/qmlplayerproxy.h" +#include + #include "mixer/basetrackplayer.h" #include "moc_qmlplayerproxy.cpp" #include "qml/asyncimageprovider.h" @@ -26,7 +28,10 @@ namespace mixxx { namespace qml { QmlPlayerProxy::QmlPlayerProxy(BaseTrackPlayer* pTrackPlayer, QObject* parent) - : QObject(parent), m_pTrackPlayer(pTrackPlayer) { + : QObject(parent), + m_pTrackPlayer(pTrackPlayer), + m_pBeatsModel(new QmlBeatsModel(this)), + m_pHotcuesModel(new QmlCuesModel(this)) { connect(m_pTrackPlayer, &BaseTrackPlayer::loadingTrack, this, @@ -112,6 +117,20 @@ void QmlPlayerProxy::slotTrackLoaded(TrackPointer pTrack) { &Track::colorUpdated, this, &QmlPlayerProxy::colorChanged); + connect(pTrack.get(), + &Track::waveformUpdated, + this, + &QmlPlayerProxy::slotWaveformChanged); + connect(pTrack.get(), + &Track::beatsUpdated, + this, + &QmlPlayerProxy::slotBeatsChanged); + connect(pTrack.get(), + &Track::cuesUpdated, + this, + &QmlPlayerProxy::slotHotcuesChanged); + slotBeatsChanged(); + slotHotcuesChanged(); } emit trackChanged(); emit trackLoaded(); @@ -135,6 +154,7 @@ void QmlPlayerProxy::slotLoadingTrack(TrackPointer pNewTrack, TrackPointer pOldT } m_pCurrentTrack.reset(); m_pCurrentTrack = pNewTrack; + m_waveformTexture = QImage(); emit trackChanged(); emit trackLoading(); } @@ -155,6 +175,121 @@ void QmlPlayerProxy::slotTrackChanged() { emit colorChanged(); emit coverArtUrlChanged(); emit trackLocationUrlChanged(); + + emit waveformLengthChanged(); + emit waveformTextureChanged(); + emit waveformTextureSizeChanged(); + emit waveformTextureStrideChanged(); +} + +void QmlPlayerProxy::slotWaveformChanged() { + emit waveformLengthChanged(); + emit waveformTextureSizeChanged(); + emit waveformTextureStrideChanged(); + + const TrackPointer pTrack = m_pCurrentTrack; + if (!pTrack) { + return; + } + const ConstWaveformPointer pWaveform = + pTrack->getWaveform(); + if (!pWaveform) { + return; + } + const int textureWidth = pWaveform->getTextureStride(); + const int textureHeight = pWaveform->getTextureSize() / pWaveform->getTextureStride(); + const uchar* data = reinterpret_cast(pWaveform->data()); + m_waveformTexture = QImage(data, textureWidth, textureHeight, QImage::Format_RGBA8888); + emit waveformTextureChanged(); +} + +void QmlPlayerProxy::slotBeatsChanged() { + VERIFY_OR_DEBUG_ASSERT(m_pBeatsModel != nullptr) { + return; + } + + const TrackPointer pTrack = m_pCurrentTrack; + if (pTrack) { + const auto trackEndPosition = mixxx::audio::FramePos{ + pTrack->getDuration() * pTrack->getSampleRate()}; + const auto pBeats = pTrack->getBeats(); + m_pBeatsModel->setBeats(pBeats, trackEndPosition); + } else { + m_pBeatsModel->setBeats(nullptr, audio::kStartFramePos); + } +} + +void QmlPlayerProxy::slotHotcuesChanged() { + VERIFY_OR_DEBUG_ASSERT(m_pHotcuesModel != nullptr) { + return; + } + + QList hotcues; + + const TrackPointer pTrack = m_pCurrentTrack; + if (pTrack) { + for (const auto& cuePoint : pTrack->getCuePoints()) { + if (cuePoint->getHotCue() == Cue::kNoHotCue) + continue; + hotcues.append(cuePoint); + } + } + m_pHotcuesModel->setCues(hotcues); + emit cuesChanged(); +} + +int QmlPlayerProxy::getWaveformLength() const { + const TrackPointer pTrack = m_pCurrentTrack; + if (pTrack) { + const ConstWaveformPointer pWaveform = pTrack->getWaveform(); + if (pWaveform) { + return pWaveform->getDataSize(); + } + } + return 0; +} + +QString QmlPlayerProxy::getWaveformTexture() const { + if (m_waveformTexture.isNull()) { + return QString(); + } + QByteArray byteArray; + QBuffer buffer(&byteArray); + buffer.open(QIODevice::WriteOnly); + m_waveformTexture.save(&buffer, "png"); + + QString imageData = QString::fromLatin1(byteArray.toBase64().data()); + if (imageData.isEmpty()) { + return QString(); + } + + return QStringLiteral("data:image/png;base64,") + imageData; +} + +int QmlPlayerProxy::getWaveformTextureSize() const { + const TrackPointer pTrack = m_pCurrentTrack; + if (pTrack) { + const ConstWaveformPointer pWaveform = pTrack->getWaveform(); + if (pWaveform) { + return pWaveform->getTextureSize(); + } + } + return 0; +} + +int QmlPlayerProxy::getWaveformTextureStride() const { + const TrackPointer pTrack = m_pCurrentTrack; + if (pTrack) { + const ConstWaveformPointer pWaveform = pTrack->getWaveform(); + if (pWaveform) { + return pWaveform->getTextureStride(); + } + } + return 0; +} + +bool QmlPlayerProxy::isLoaded() const { + return m_pCurrentTrack != nullptr; } PROPERTY_IMPL(QString, artist, getArtist, setArtist) diff --git a/src/qml/qmlplayerproxy.h b/src/qml/qmlplayerproxy.h index 3ca284ab6ec..7bcc20fa558 100644 --- a/src/qml/qmlplayerproxy.h +++ b/src/qml/qmlplayerproxy.h @@ -7,6 +7,9 @@ #include #include "mixer/basetrackplayer.h" +#include "qml/qmlbeatsmodel.h" +#include "qml/qmlcuesmodel.h" +#include "track/cueinfo.h" #include "track/track.h" namespace mixxx { @@ -14,6 +17,7 @@ namespace qml { class QmlPlayerProxy : public QObject { Q_OBJECT + Q_PROPERTY(bool isLoaded READ isLoaded NOTIFY trackChanged) Q_PROPERTY(QString artist READ getArtist WRITE setArtist NOTIFY artistChanged) Q_PROPERTY(QString title READ getTitle WRITE setTitle NOTIFY titleChanged) Q_PROPERTY(QString album READ getAlbum WRITE setAlbum NOTIFY albumChanged) @@ -34,9 +38,20 @@ class QmlPlayerProxy : public QObject { QML_NAMED_ELEMENT(Player) QML_UNCREATABLE("Only accessible via Mixxx.PlayerManager.getPlayer(group)") + Q_PROPERTY(int waveformLength READ getWaveformLength NOTIFY waveformLengthChanged) + Q_PROPERTY(QString waveformTexture READ getWaveformTexture NOTIFY waveformTextureChanged) + Q_PROPERTY(int waveformTextureSize READ getWaveformTextureSize NOTIFY + waveformTextureSizeChanged) + Q_PROPERTY(int waveformTextureStride READ getWaveformTextureStride NOTIFY + waveformTextureStrideChanged) + + Q_PROPERTY(mixxx::qml::QmlBeatsModel* beatsModel MEMBER m_pBeatsModel CONSTANT); + Q_PROPERTY(mixxx::qml::QmlCuesModel* hotcuesModel MEMBER m_pHotcuesModel CONSTANT); + public: explicit QmlPlayerProxy(BaseTrackPlayer* pTrackPlayer, QObject* parent = nullptr); + bool isLoaded() const; QString getTrack() const; QString getTitle() const; QString getArtist() const; @@ -54,6 +69,11 @@ class QmlPlayerProxy : public QObject { QUrl getCoverArtUrl() const; QUrl getTrackLocationUrl() const; + int getWaveformLength() const; + QString getWaveformTexture() const; + int getWaveformTextureSize() const; + int getWaveformTextureStride() const; + /// Needed for interacting with the raw track player object. BaseTrackPlayer* internalTrackPlayer() const { return m_pTrackPlayer; @@ -66,6 +86,9 @@ class QmlPlayerProxy : public QObject { void slotTrackLoaded(TrackPointer pTrack); void slotLoadingTrack(TrackPointer pNewTrack, TrackPointer pOldTrack); void slotTrackChanged(); + void slotWaveformChanged(); + void slotBeatsChanged(); + void slotHotcuesChanged(); void setArtist(const QString& artist); void setTitle(const QString& title); @@ -102,12 +125,21 @@ class QmlPlayerProxy : public QObject { void colorChanged(); void coverArtUrlChanged(); void trackLocationUrlChanged(); + void cuesChanged(); void loadTrackFromLocationRequested(const QString& trackLocation, bool play); + void waveformLengthChanged(); + void waveformTextureChanged(); + void waveformTextureSizeChanged(); + void waveformTextureStrideChanged(); + private: + QImage m_waveformTexture; QPointer m_pTrackPlayer; TrackPointer m_pCurrentTrack; + QmlBeatsModel* m_pBeatsModel; + QmlCuesModel* m_pHotcuesModel; }; } // namespace qml diff --git a/src/skin/legacy/legacyskinparser.cpp b/src/skin/legacy/legacyskinparser.cpp index 6d67ccbb702..10f53326ab5 100644 --- a/src/skin/legacy/legacyskinparser.cpp +++ b/src/skin/legacy/legacyskinparser.cpp @@ -1081,17 +1081,20 @@ QWidget* LegacySkinParser::parseTrackProperty(const QDomElement& node) { return nullptr; } + bool isMainDeck = PlayerManager::isDeckGroup(group); WTrackProperty* pTrackProperty = new WTrackProperty( m_pParent, m_pConfig, m_pLibrary, - group); + group, + isMainDeck); setupLabelWidget(node, pTrackProperty); // Ensure 'show_track_menu' control is created for each main deck and // valueChangeRequest hook is set up. - // Only the first WTrackProperty that is created connects the signals. - if (PlayerManager::isDeckGroup(group)) { + if (isMainDeck) { + // Only the first WTrackProperty that is created connects the signals, + // for later attempts this returns false. if (pPlayer->isTrackMenuControlAvailable()) { connect(pPlayer, &BaseTrackPlayer::trackMenuChangeRequest, @@ -1143,7 +1146,8 @@ QWidget* LegacySkinParser::parseTrackWidgetGroup(const QDomElement& node) { m_pParent, m_pConfig, m_pLibrary, - group); + group, + PlayerManager::isDeckGroup(group)); commonWidgetSetup(node, pGroup); pGroup->setup(node, *m_pContext); pGroup->Init(); @@ -1565,6 +1569,12 @@ QWidget* LegacySkinParser::parseLibrary(const QDomElement& node) { BaseTrackTableModel::kBpmColumnPrecisionDefault); BaseTrackTableModel::setBpmColumnPrecision(bpmColumnPrecision); + const auto applyPlayedTrackColor = + m_pConfig->getValue( + mixxx::library::prefs::kApplyPlayedTrackColorConfigKey, + BaseTrackTableModel::kApplyPlayedTrackColorDefault); + BaseTrackTableModel::setApplyPlayedTrackColor(applyPlayedTrackColor); + // Connect Library search signals to the WLibrary connect(m_pLibrary, &Library::search, diff --git a/src/skin/legacy/tooltips.cpp b/src/skin/legacy/tooltips.cpp index 388f8b49da8..3ab8fe371ed 100644 --- a/src/skin/legacy/tooltips.cpp +++ b/src/skin/legacy/tooltips.cpp @@ -324,28 +324,44 @@ void Tooltips::addStandardTooltips() { << tr("Manual: Sets how much to reduce the music volume, when talkover is activated regardless of volume of microphone inputs."); QString changeAmount = tr("Change the step-size in the Preferences -> Decks menu."); + QString pitchAffected = tr("If keylock is disabled, pitch is also affected."); + add("rate_perm_up_rate_perm_up_small") - << tr("Raise Pitch") - << QString("%1: %2").arg(leftClick, tr("Sets the pitch higher.")) - << QString("%1: %2").arg(rightClick, tr("Sets the pitch higher in small steps.")) + << tr("Speed Up") + << QString("%1: %2").arg(leftClick, + tr("Raises the track playback speed (tempo).")) + << pitchAffected + << QString("%1: %2").arg( + rightClick, tr("Raises playback speed in small steps.")) << changeAmount; add("rate_perm_down_rate_perm_down_small") - << tr("Lower Pitch") - << QString("%1: %2").arg(leftClick, tr("Sets the pitch lower.")) - << QString("%1: %2").arg(rightClick, tr("Sets the pitch lower in small steps.")) + << tr("Slow Down") + << QString("%1: %2").arg(leftClick, + tr("Lowers the track playback speed (tempo).")) + << pitchAffected + << QString("%1: %2").arg( + rightClick, tr("Lowers playback speed in small steps.")) << changeAmount; add("rate_temp_up_rate_temp_up_small") - << tr("Raise Pitch Temporary (Nudge)") - << QString("%1: %2").arg(leftClick, tr("Holds the pitch higher while active.")) - << QString("%1: %2").arg(rightClick, tr("Holds the pitch higher (small amount) while active.")) + << tr("Speed Up Temporarily (Nudge)") + << QString("%1: %2").arg(leftClick, + tr("Holds playback speed higher while active (tempo).")) + << pitchAffected + << QString("%1: %2").arg(rightClick, + tr("Holds playback speed higher (small amount) while " + "active.")) << changeAmount; add("rate_temp_down_rate_temp_down_small") - << tr("Lower Pitch Temporary (Nudge)") - << QString("%1: %2").arg(leftClick, tr("Holds the pitch lower while active.")) - << QString("%1: %2").arg(rightClick, tr("Holds the pitch lower (small amount) while active.")) + << tr("Slow Down Temporarily (Nudge)") + << QString("%1: %2").arg(leftClick, + tr("Holds playback speed lower while active (tempo).")) + << pitchAffected + << QString("%1: %2").arg(rightClick, + tr("Holds playback speed lower (small amount) while " + "active.")) << changeAmount; add("filterLow") @@ -648,6 +664,10 @@ void Tooltips::addStandardTooltips() { << tr("Record Mix") << tr("Toggle mix recording."); + add("expand_samplers") + << tr("Expand/Collapse Samplers") + << tr("Toggle expanded samplers view."); + // Status displays and toggle buttons add("recording_duration") << tr("Recording Duration") diff --git a/src/test/controller_mapping_settings_test.cpp b/src/test/controller_mapping_settings_test.cpp new file mode 100644 index 00000000000..3719fcefaf4 --- /dev/null +++ b/src/test/controller_mapping_settings_test.cpp @@ -0,0 +1,513 @@ + +#include + +#include + +#include "controllers/legacycontrollermapping.h" +#include "controllers/legacycontrollermappingfilehandler.h" +#include "controllers/legacycontrollersettings.h" +#include "test/mixxxtest.h" + +class LegacyControllerMappingSettingsTest : public MixxxTest { +}; + +const char* const kValidBoolean = + ""; + +const char* const kValidInteger = + ""; + +// This setting has purposfully no custom "label" and description +const char* const kValidDouble = + ""; +const char* const kValidEnumOption = "%2"; + +TEST_F(LegacyControllerMappingSettingsTest, booleanSettingParsing) { + QDomDocument doc; + doc.setContent(QString(kValidBoolean).arg("false").toLatin1()); + + EXPECT_TRUE(LegacyControllerBooleanSetting::match(doc.documentElement())); + LegacyControllerBooleanSetting* setting = (LegacyControllerBooleanSetting*) + LegacyControllerBooleanSetting::createFrom(doc.documentElement()); + EXPECT_TRUE(setting->valid()) << "Unable to create a boolean setting"; + + EXPECT_EQ(setting->variableName(), "myToggle1"); + EXPECT_EQ(setting->label(), "Test label"); + EXPECT_EQ(setting->description(), "Test description"); + + EXPECT_FALSE(setting->isDirty()); + EXPECT_TRUE(setting->isDefault()); + EXPECT_EQ(setting->stringify(), "false"); + EXPECT_TRUE(setting->valid()); + + delete setting; + + doc.setContent(QString(kValidBoolean).arg("true").toLatin1()); + + setting = (LegacyControllerBooleanSetting*) + LegacyControllerBooleanSetting::createFrom(doc.documentElement()); + EXPECT_TRUE(setting->valid()) << "Unable to create a boolean setting"; + + EXPECT_EQ(setting->stringify(), "true"); + + delete setting; +} + +TEST_F(LegacyControllerMappingSettingsTest, booleanSettingEditing) { + QDomDocument doc; + doc.setContent( + QByteArray("