diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1dfbf357..25f1f6fe 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,414 +1,415 @@ include_directories(${elisa_BINARY_DIR}) set(elisaLib_SOURCES mediaplaylist.cpp musicalbum.cpp musicaudiotrack.cpp musicartist.cpp musicaudiogenre.cpp progressindicator.cpp databaseinterface.cpp musiclistenersmanager.cpp managemediaplayercontrol.cpp manageheaderbar.cpp manageaudioplayer.cpp trackslistener.cpp elisaapplication.cpp notificationitem.cpp topnotificationmanager.cpp elisautils.cpp datatype.cpp trackdatahelper.cpp modeldatacache.cpp abstractfile/abstractfilelistener.cpp abstractfile/abstractfilelisting.cpp filescanner.cpp file/filelistener.cpp file/localfilelisting.cpp models/albummodel.cpp models/allalbumsmodel.cpp models/allartistsmodel.cpp models/alltracksmodel.cpp models/allgenresmodel.cpp models/abstractmediaproxymodel.cpp models/allalbumsproxymodel.cpp models/allartistsproxymodel.cpp models/alltracksproxymodel.cpp models/singleartistproxymodel.cpp models/singlealbumproxymodel.cpp models/genericdatamodel.cpp ) if (LIBVLC_FOUND) set(elisaLib_SOURCES ${elisaLib_SOURCES} audiowrapper_libvlc.cpp ) else() set(elisaLib_SOURCES ${elisaLib_SOURCES} audiowrapper_qtmultimedia.cpp ) endif() if (KF5KIO_FOUND) set(elisaLib_SOURCES ${elisaLib_SOURCES} models/filebrowsermodel.cpp models/filebrowserproxymodel.cpp ) endif() if (KF5Baloo_FOUND) if (Qt5DBus_FOUND) set(elisaLib_SOURCES ${elisaLib_SOURCES} baloo/localbaloofilelisting.cpp baloo/baloolistener.cpp ) qt5_add_dbus_interface(elisaLib_SOURCES ${BALOO_DBUS_INTERFACES_DIR}/org.kde.baloo.main.xml baloo/main) qt5_add_dbus_interface(elisaLib_SOURCES ${BALOO_DBUS_INTERFACES_DIR}/org.kde.baloo.fileindexer.xml baloo/fileindexer) qt5_add_dbus_interface(elisaLib_SOURCES ${BALOO_DBUS_INTERFACES_DIR}/org.kde.baloo.scheduler.xml baloo/scheduler) qt5_add_dbus_adaptor(elisaLib_SOURCES ${BALOO_DBUS_INTERFACES_DIR}/org.kde.BalooWatcherApplication.xml baloo/localbaloofilelisting.h LocalBalooFileListing) endif() endif() if (Qt5DBus_FOUND) set(elisaLib_SOURCES ${elisaLib_SOURCES} mpris2/mpris2.cpp mpris2/mediaplayer2.cpp mpris2/mediaplayer2player.cpp ) endif() if (UPNPQT_FOUND) set(elisaLib_SOURCES ${elisaLib_SOURCES} upnp/upnpcontrolcontentdirectory.cpp upnp/upnpcontentdirectorymodel.cpp upnp/upnpcontrolconnectionmanager.cpp upnp/upnpcontrolmediaserver.cpp upnp/didlparser.cpp upnp/upnplistener.cpp upnp/upnpdiscoverallmusic.cpp ) endif() if (KF5Baloo_FOUND) if (Qt5DBus_FOUND) qt5_add_dbus_interface(elisaLib_SOURCES ${BALOO_DBUS_INTERFACES_DIR}/org.kde.baloo.fileindexer.xml baloo/fileindexer) qt5_add_dbus_interface(elisaLib_SOURCES ${BALOO_DBUS_INTERFACES_DIR}/org.kde.baloo.scheduler.xml baloo/scheduler) set(elisaLib_SOURCES ${elisaLib_SOURCES} ../src/baloo/baloolistener.cpp ../src/baloo/localbaloofilelisting.cpp ) endif() endif() kconfig_add_kcfg_files(elisaLib_SOURCES ../src/elisa_settings.kcfgc ) set(elisaLib_SOURCES ${elisaLib_SOURCES} ../src/elisa_core.kcfg ) add_library(elisaLib ${elisaLib_SOURCES}) target_link_libraries(elisaLib LINK_PUBLIC Qt5::Multimedia LINK_PRIVATE Qt5::Core Qt5::Sql Qt5::Widgets Qt5::Concurrent Qt5::Qml KF5::I18n KF5::CoreAddons KF5::ConfigCore KF5::ConfigGui) if (KF5FileMetaData_FOUND) target_link_libraries(elisaLib LINK_PRIVATE KF5::FileMetaData ) endif() if (KF5KIO_FOUND) target_link_libraries(elisaLib LINK_PUBLIC KF5::KIOCore KF5::KIOFileWidgets KF5::KIOWidgets ) endif() if (KF5XmlGui_FOUND) target_link_libraries(elisaLib LINK_PUBLIC KF5::XmlGui ) endif() if (KF5ConfigWidgets_FOUND) target_link_libraries(elisaLib LINK_PUBLIC KF5::ConfigWidgets ) endif() if (KF5KCMUtils_FOUND) target_link_libraries(elisaLib LINK_PUBLIC KF5::KCMUtils ) endif() if (KF5Baloo_FOUND) if (Qt5DBus_FOUND) target_link_libraries(elisaLib LINK_PUBLIC KF5::Baloo ) endif() endif() if (Qt5DBus_FOUND) target_link_libraries(elisaLib LINK_PUBLIC Qt5::DBus ) if (KF5DBusAddons_FOUND) target_link_libraries(elisaLib LINK_PUBLIC KF5::DBusAddons ) endif() endif() if (LIBVLC_FOUND) target_include_directories(elisaLib PRIVATE ${LIBVLC_INCLUDE_DIR} ) target_link_libraries(elisaLib LINK_PRIVATE ${LIBVLC_LIBRARY} ) endif() generate_export_header(elisaLib BASE_NAME ElisaLib EXPORT_FILE_NAME elisaLib_export.h) set_target_properties(elisaLib PROPERTIES VERSION 0.1 SOVERSION 0 EXPORT_NAME ElisaLib ) if (NOT APPLE AND NOT WIN32) install(TARGETS elisaLib LIBRARY DESTINATION ${KDE_INSTALL_FULL_LIBDIR}/elisa RUNTIME DESTINATION ${KDE_INSTALL_FULL_LIBDIR}/elisa BUNDLE DESTINATION ${KDE_INSTALL_FULL_LIBDIR}/elisa ) else() install(TARGETS elisaLib ${INSTALL_TARGETS_DEFAULT_ARGS}) endif() set(elisaqmlplugin_SOURCES elisaqmlplugin.cpp datatype.cpp elisautils.cpp ) add_library(elisaqmlplugin SHARED ${elisaqmlplugin_SOURCES}) target_link_libraries(elisaqmlplugin LINK_PRIVATE Qt5::Quick Qt5::Widgets KF5::ConfigCore KF5::ConfigGui elisaLib ) if (KF5FileMetaData_FOUND) target_link_libraries(elisaqmlplugin LINK_PRIVATE KF5::FileMetaData ) endif() set_target_properties(elisaqmlplugin PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/org/kde/elisa ) if (NOT APPLE AND NOT WIN32) set_target_properties(elisaqmlplugin PROPERTIES INSTALL_RPATH "${KDE_INSTALL_FULL_LIBDIR}/elisa;${CMAKE_INSTALL_RPATH}" ) endif() install(TARGETS elisaqmlplugin DESTINATION ${QML_INSTALL_DIR}/org/kde/elisa/) install(FILES qmldir DESTINATION ${QML_INSTALL_DIR}/org/kde/elisa) add_custom_target(copy) add_custom_target(copy2) file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/bin/org/kde/elisa) add_custom_command(TARGET copy PRE_BUILD COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/qmldir ${CMAKE_BINARY_DIR}/bin/org/kde/elisa/) add_custom_command(TARGET copy2 PRE_BUILD COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/plugins.qmltypes ${CMAKE_BINARY_DIR}/bin/org/kde/elisa/) add_dependencies(elisaqmlplugin copy copy2) if (Qt5Quick_FOUND AND Qt5Widgets_FOUND) set(elisa_SOURCES main.cpp windows/WindowsTheme.qml windows/PlatformIntegration.qml android/ElisaMainWindow.qml android/AndroidTheme.qml android/PlatformIntegration.qml qml/ElisaMainWindow.qml qml/ApplicationMenu.qml qml/BaseTheme.qml qml/Theme.qml qml/PlatformIntegration.qml qml/LabelWithToolTip.qml qml/RatingStar.qml qml/PlayListEntry.qml qml/MediaBrowser.qml qml/DraggableItem.qml qml/PassiveNotification.qml qml/TopNotification.qml qml/TopNotificationItem.qml qml/TrackImportNotification.qml qml/HeaderBar.qml qml/NavigationActionBar.qml qml/MediaPlayerControl.qml qml/ContextView.qml qml/ContentView.qml qml/ViewSelector.qml qml/ViewManager.qml qml/MediaPlayListView.qml qml/MediaTrackDelegate.qml qml/MediaAlbumTrackDelegate.qml qml/MediaTrackMetadataView.qml qml/GridBrowserView.qml qml/GridBrowserDelegate.qml qml/ListBrowserView.qml qml/FileBrowserDelegate.qml qml/FileBrowserView.qml qml/ScrollHelper.qml + qml/FlatButtonWithToolTip.qml ) qt5_add_resources(elisa_SOURCES resources.qrc) set_property(SOURCE qrc_resources.cpp PROPERTY SKIP_AUTOMOC ON) set(elisa_ICONS_PNG ../icons/128-apps-elisa.png ../icons/64-apps-elisa.png ../icons/48-apps-elisa.png ../icons/32-apps-elisa.png ../icons/22-apps-elisa.png ../icons/16-apps-elisa.png ) # add icons to application sources, to have them bundled ecm_add_app_icon(elisa_SOURCES ICONS ${elisa_ICONS_PNG}) add_executable(elisa ${elisa_SOURCES}) target_include_directories(elisa PRIVATE ${KDSoap_INCLUDE_DIRS}) target_link_libraries(elisa LINK_PRIVATE elisaLib Qt5::Widgets Qt5::QuickControls2 Qt5::Svg KF5::I18n KF5::CoreAddons KF5::ConfigCore KF5::ConfigGui ) if (ANDROID) target_link_libraries(elisa LINK_PRIVATE Qt5::AndroidExtras ) endif() if (KF5Crash_FOUND) target_link_libraries(elisa LINK_PRIVATE KF5::Crash ) endif() if (KF5Declarative_FOUND) target_link_libraries(elisa LINK_PRIVATE KF5::Declarative ) endif() if (NOT APPLE AND NOT WIN32) set_target_properties(elisa PROPERTIES INSTALL_RPATH "${KDE_INSTALL_FULL_LIBDIR}/elisa;${CMAKE_INSTALL_RPATH}" ) endif() install(TARGETS elisa ${INSTALL_TARGETS_DEFAULT_ARGS}) endif() if (KF5ConfigWidgets_FOUND AND KF5Declarative_FOUND) add_subdirectory(localFileConfiguration) endif() set(elisaImport_SOURCES elisaimport.cpp elisaimportapplication.cpp ) kconfig_add_kcfg_files(elisaImport_SOURCES ../src/elisa_settings.kcfgc ) set(elisaImport_SOURCES ${elisaImport_SOURCES} ../src/elisa_core.kcfg ) add_executable(elisaImport ${elisaImport_SOURCES}) target_link_libraries(elisaImport LINK_PRIVATE KF5::ConfigCore KF5::ConfigGui elisaLib ) if (KF5FileMetaData_FOUND) target_link_libraries(elisaImport LINK_PRIVATE KF5::FileMetaData ) endif() set(QML_IMPORT_PATH ${CMAKE_BINARY_DIR}/bin CACHE INTERNAL "qml import path" FORCE) diff --git a/src/qml/BaseTheme.qml b/src/qml/BaseTheme.qml index cc8fdfce..8bf44ccc 100644 --- a/src/qml/BaseTheme.qml +++ b/src/qml/BaseTheme.qml @@ -1,128 +1,116 @@ /* * Copyright 2017 Matthieu Gallien * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ import QtQuick 2.7 import QtQuick.Controls 2.2 Item { function dp(pixel) { // 96 - common, "base" DPI value return Math.round(pixel * logicalDpi / 96); } property string defaultAlbumImage: 'image://icon/media-optical-audio' property string defaultArtistImage: 'image://icon/view-media-artist' property string defaultBackgroundImage: 'qrc:///background.png' property string artistIcon: 'image://icon/view-media-artist' property string albumIcon: 'image://icon/view-media-album-cover' property string playlistIcon: 'image://icon/view-media-playlist' property string tracksIcon: 'image://icon/view-media-track' property string genresIcon: 'image://icon/view-media-genre' property string clearIcon: 'image://icon/edit-clear' - property string skipBackwardIcon: 'image://icon/media-skip-backward' - property string pauseIcon: 'image://icon/media-playback-pause' - property string playIcon: 'image://icon/media-playback-start' - property string skipForwardIcon: 'image://icon/media-skip-forward' property string pausedIndicatorIcon: 'image://icon/media-playback-paused' property string playingIndicatorIcon: 'image://icon/media-playback-playing' - property string playerVolumeMutedIcon: 'image://icon/player-volume-muted' - property string playerVolumeIcon: 'image://icon/player-volume' property string ratingIcon: 'image://icon/rating' property string ratingUnratedIcon: 'image://icon/rating-unrated' property string errorIcon: 'image://icon/error' - property string repeatIcon: 'image://icon/media-repeat-all' - property string shuffleIcon: 'image://icon/media-playlist-shuffle' - property string noRepeatIcon: 'image://icon/media-repeat-none' - property string noShuffleIcon: 'image://icon/media-playlist-normal' property string folderIcon: 'image://icon/document-open-folder' - property string maximizeIcon: 'image://icon/draw-arrow-down' - property string minimizeIcon: 'image://icon/draw-arrow-up' property int layoutHorizontalMargin: dp(8) property int layoutVerticalMargin: dp(6) property int delegateHeight: dp(28) FontMetrics { id: playListAuthorTextHeight font.weight: Font.Light } FontMetrics { id: playListAlbumTextHeight font.weight: Font.Bold font.pointSize: elisaTheme.defaultFontPointSize * 1.4 } FontMetrics { id: playListTrackTextHeight font.weight: Font.Bold } property int playListDelegateHeight: (playListTrackTextHeight.height > dp(28)) ? playListTrackTextHeight.height : dp(28) property int playListDelegateWithHeaderHeight: playListDelegateHeight + elisaTheme.layoutVerticalMargin * 5 + playListAuthorTextHeight.height + playListAlbumTextHeight.height property int trackDelegateHeight: dp(45) property int coverImageSize: dp(180) property int smallImageSize: dp(32) property int maximumMetadataWidth: dp(300) property int tooltipRadius: dp(3) property int shadowOffset: dp(2) property int delegateToolButtonSize: dp(34) property int smallDelegateToolButtonSize: dp(20) property int ratingStarSize: dp(15) property int mediaPlayerControlHeight: dp(42) property int mediaPlayerHorizontalMargin: dp(10) property real mediaPlayerControlOpacity: 0.6 property int smallControlButtonSize: dp(22) property int volumeSliderWidth: dp(100) property int dragDropPlaceholderHeight: dp(28) property int navigationBarHeight: dp(100) property int navigationBarFilterHeight: dp(44) property int gridDelegateHeight: dp(100) + layoutVerticalMargin + fontSize.height * 2 property int gridDelegateWidth: dp(100) property int viewSelectorDelegateHeight: dp(24) property int filterClearButtonMargin: layoutVerticalMargin property alias defaultFontPointSize: fontSize.font.pointSize TextMetrics { id: bigTextSize font.pointSize: defaultFontPointSize * 1.4 text: "Albums" } property int viewSelectorSmallSizeThreshold: 3 * layoutHorizontalMargin + viewSelectorDelegateHeight + bigTextSize.width Label { id: fontSize } } diff --git a/src/qml/ElisaMainWindow.qml b/src/qml/ElisaMainWindow.qml index 4cb25f8d..2f02a2ba 100644 --- a/src/qml/ElisaMainWindow.qml +++ b/src/qml/ElisaMainWindow.qml @@ -1,309 +1,306 @@ /* * Copyright 2016-2018 Matthieu Gallien * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ import QtQuick 2.7 -import QtQuick.Controls 2.2 -import QtQuick.Controls 1.4 as Controls1 +import QtQuick.Controls 2.3 import QtQuick.Layouts 1.1 import QtQuick.Window 2.2 import org.kde.elisa 1.0 import Qt.labs.settings 1.0 ApplicationWindow { id: mainWindow visible: true minimumWidth: contentView.showPlaylist ? 1100 : 700 minimumHeight: 600 LayoutMirroring.enabled: Qt.application.layoutDirection == Qt.RightToLeft LayoutMirroring.childrenInherit: true x: persistentSettings.x y: persistentSettings.y width: persistentSettings.width height: persistentSettings.height title: i18n("Elisa") property var goBackAction: elisa.action("go_back") - Controls1.Action { + Action { shortcut: goBackAction.shortcut onTriggered: contentView.goBack() } - Controls1.Action { - id: applicationMenuAction - text: i18nc("open application menu", "Application Menu") - iconName: "application-menu" - onTriggered: applicationMenu.popup() - } - ApplicationMenu { id: applicationMenu } SystemPalette { id: myPalette colorGroup: SystemPalette.Active } Theme { id: elisaTheme } Settings { id: persistentSettings property int x property int y property int width : 1100 property int height : 600 property var playListState property var audioPlayerState property double playControlItemVolume : 100.0 property bool playControlItemMuted : false property bool playControlItemRepeat : false property bool playControlItemShuffle : false property bool expandedFilterView: false property bool showPlaylist: true property bool headerBarIsMaximized: false - } + } + + Connections { + target: headerBar.playerControl + onOpenMenu: applicationMenu.popup() + } Connections { target: Qt.application onAboutToQuit: { persistentSettings.x = mainWindow.x; persistentSettings.y = mainWindow.y; persistentSettings.width = mainWindow.width; persistentSettings.height = mainWindow.height; persistentSettings.playListState = elisa.mediaPlayList.persistentState; persistentSettings.audioPlayerState = elisa.audioControl.persistentState persistentSettings.playControlItemVolume = headerBar.playerControl.volume persistentSettings.playControlItemMuted = headerBar.playerControl.muted persistentSettings.playControlItemRepeat = headerBar.playerControl.repeat persistentSettings.playControlItemShuffle = headerBar.playerControl.shuffle persistentSettings.showPlaylist = contentView.showPlaylist persistentSettings.headerBarIsMaximized = headerBar.isMaximized } } Loader { id: mprisloader active: false sourceComponent: PlatformIntegration { id: platformInterface playListModel: elisa.mediaPlayList audioPlayerManager: elisa.audioControl player: elisa.audioPlayer headerBarManager: elisa.manageHeaderBar manageMediaPlayerControl: elisa.playerControl onRaisePlayer: { mainWindow.show() mainWindow.raise() mainWindow.requestActivate() } } } Connections { target: elisa.audioPlayer onVolumeChanged: headerBar.playerControl.volume = elisa.audioPlayer.volume onMutedChanged: headerBar.playerControl.muted = elisa.audioPlayer.muted } Connections { target: elisa.mediaPlayList onPlayListLoadFailed: { messageNotification.showNotification(i18nc("message of passive notification when playlist load failed", "Load of playlist failed"), 3000) } } PassiveNotification { id: messageNotification } Rectangle { color: myPalette.base anchors.fill: parent ColumnLayout { anchors.fill: parent spacing: 0 Item { id: headerBarParent Layout.minimumHeight: mainWindow.height * 0.2 + elisaTheme.mediaPlayerControlHeight Layout.maximumHeight: mainWindow.height * 0.2 + elisaTheme.mediaPlayerControlHeight Layout.fillWidth: true HeaderBar { id: headerBar focus: true anchors.fill: parent tracksCount: elisa.manageHeaderBar.remainingTracks album: elisa.manageHeaderBar.album title: elisa.manageHeaderBar.title artist: elisa.manageHeaderBar.artist image: elisa.manageHeaderBar.image ratingVisible: false playerControl.duration: elisa.audioPlayer.duration playerControl.seekable: elisa.audioPlayer.seekable playerControl.volume: persistentSettings.playControlItemVolume playerControl.muted: persistentSettings.playControlItemMuted playerControl.position: elisa.audioPlayer.position playerControl.skipBackwardEnabled: elisa.playerControl.skipBackwardControlEnabled playerControl.skipForwardEnabled: elisa.playerControl.skipForwardControlEnabled playerControl.playEnabled: elisa.playerControl.playControlEnabled playerControl.isPlaying: elisa.playerControl.musicPlaying playerControl.repeat: persistentSettings.playControlItemRepeat playerControl.shuffle: persistentSettings.playControlItemShuffle playerControl.onSeek: elisa.audioPlayer.seek(position) playerControl.onPlay: elisa.audioControl.playPause() playerControl.onPause: elisa.audioControl.playPause() playerControl.onPlayPrevious: elisa.mediaPlayList.skipPreviousTrack() playerControl.onPlayNext: elisa.mediaPlayList.skipNextTrack() playerControl.isMaximized: persistentSettings.headerBarIsMaximized TrackImportNotification { id: importedTracksCountNotification anchors { right: headerBar.right top: headerBar.top rightMargin: elisaTheme.layoutHorizontalMargin * 1.75 topMargin: elisaTheme.layoutHorizontalMargin * 3 } } Binding { target: importedTracksCountNotification property: 'musicManager' value: elisa.musicManager when: elisa.musicManager !== undefined } Loader { sourceComponent: Binding { target: importedTracksCountNotification property: 'indexingRunning' value: elisa.musicManager.indexingRunning } active: elisa.musicManager !== undefined } Loader { sourceComponent: Binding { target: importedTracksCountNotification property: 'importedTracksCount' value: elisa.musicManager.importedTracksCount } active: elisa.musicManager !== undefined } } } ContentView { id: contentView Layout.fillHeight: true Layout.fillWidth: true showPlaylist: persistentSettings.showPlaylist } } } StateGroup { id: mainWindowState states: [ State { name: "headerBarIsMaximized" when: headerBar.isMaximized changes: [ PropertyChanges { target: mainWindow minimumHeight: 120 + elisaTheme.mediaPlayerControlHeight explicit: true }, PropertyChanges { target: headerBarParent Layout.minimumHeight: mainWindow.height Layout.maximumHeight: mainWindow.height }, PropertyChanges { target: contentView height: 0 visible: false } ] } ] } Component.onCompleted: { elisa.initialize() elisa.mediaPlayList.randomPlay = Qt.binding(function() { return headerBar.playerControl.shuffle }) elisa.mediaPlayList.repeatPlay = Qt.binding(function() { return headerBar.playerControl.repeat }) elisa.playerControl.randomOrContinuePlay = Qt.binding(function() { return headerBar.playerControl.shuffle || headerBar.playerControl.repeat}) if (persistentSettings.playListState) { elisa.mediaPlayList.persistentState = persistentSettings.playListState } if (persistentSettings.audioPlayerState) { elisa.audioControl.persistentState = persistentSettings.audioPlayerState } elisa.audioPlayer.muted = Qt.binding(function() { return headerBar.playerControl.muted }) elisa.audioPlayer.volume = Qt.binding(function() { return headerBar.playerControl.volume }) mprisloader.active = true } } diff --git a/src/qml/FlatButtonWithToolTip.qml b/src/qml/FlatButtonWithToolTip.qml new file mode 100644 index 00000000..28ae30bc --- /dev/null +++ b/src/qml/FlatButtonWithToolTip.qml @@ -0,0 +1,50 @@ +/* + * Copyright 2018 Alexander Stippich + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.7 +import QtQuick.Layouts 1.2 +import QtGraphicalEffects 1.0 +import QtQuick.Controls 2.3 +import org.kde.elisa 1.0 + +Button { + id: flatButtonWithToolTip + + contentItem: Image { + anchors.fill: parent + + source: flatButtonWithToolTip.action.icon.name != "" ? ('image://icon/' + flatButtonWithToolTip.action.icon.name) : Qt.resolvedUrl(flatButtonWithToolTip.action.icon.source) + + sourceSize.width: flatButtonWithToolTip.width + sourceSize.height: flatButtonWithToolTip.height + + fillMode: Image.PreserveAspectFit + opacity: flatButtonWithToolTip.action.enabled ? 1 : 0.6 + } + + background: Rectangle { + color: parent.pressed ? myPalette.highlight : "transparent" + border.color: (parent.hovered || parent.activeFocus) ? myPalette.highlight : "transparent" + border.width: 1 + } + + ToolTip.visible: hovered + ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval + ToolTip.text: flatButtonWithToolTip.action.text +} diff --git a/src/qml/MediaPlayerControl.qml b/src/qml/MediaPlayerControl.qml index 83645232..5a640d94 100644 --- a/src/qml/MediaPlayerControl.qml +++ b/src/qml/MediaPlayerControl.qml @@ -1,610 +1,427 @@ /* * Copyright 2016 Matthieu Gallien * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ import QtQuick 2.7 import QtQuick.Layouts 1.2 import QtGraphicalEffects 1.0 -import QtQuick.Controls 2.2 +import QtQuick.Controls 2.3 import org.kde.elisa 1.0 -import QtQuick.Controls 1.4 as Controls1 - FocusScope { property alias volume: volumeSlider.value property int position property int duration property bool muted property bool isPlaying property bool seekable property bool playEnabled property bool skipForwardEnabled property bool skipBackwardEnabled property bool isMaximized property bool shuffle property bool repeat signal play() signal pause() signal playPrevious() signal playNext() signal seek(int position) + signal openMenu() signal maximize() signal minimize() id: musicWidget SystemPalette { id: myPalette colorGroup: SystemPalette.Active } Theme { id: elisaTheme } + Action { + id: applicationMenuAction + text: i18nc("open application menu", "Application Menu") + icon.name: "application-menu" + onTriggered: openMenu() + } + + Action { + id: repeatAction + text: i18nc("toggle repeat mode for playlist", "Toggle Repeat") + icon.name: musicWidget.repeat ? "media-repeat-all" : "media-repeat-none" + onTriggered: musicWidget.repeat = !musicWidget.repeat + } + + Action { + id: shuffleAction + text: i18nc("toggle shuffle mode for playlist", "Toggle Shuffle") + icon.name: musicWidget.shuffle ? "media-playlist-shuffle" : "media-playlist-normal" + onTriggered: musicWidget.shuffle = !musicWidget.shuffle + } + + Action { + id: muteAction + text: i18nc("toggle mute mode for player", "Toggle Mute") + icon.name: musicWidget.muted ? "player-volume-muted" : "player-volume" + onTriggered: musicWidget.muted = !musicWidget.muted + } + + Action { + id: playPauseAction + text: i18nc("toggle play and pause for the audio player", "Toggle Play and Pause") + icon.name: musicWidget.isPlaying? "media-playback-pause" : "media-playback-start" + onTriggered: musicWidget.isPlaying ? musicWidget.pause() : musicWidget.play() + enabled: playEnabled + } + + Action { + id: skipBackwardAction + text: i18nc("skip backward in playlists", "Skip Backward") + icon.name: musicWidget.LayoutMirroring.enabled ? "media-skip-forward" : "media-skip-backward" + onTriggered: musicWidget.playPrevious() + enabled: skipBackwardEnabled + } + + Action { + id: skipForwardAction + text: i18nc("skip forward in playlists", "Skip Forward") + icon.name: musicWidget.LayoutMirroring.enabled ? "media-skip-backward" : "media-skip-forward" + onTriggered: musicWidget.playNext() + enabled: skipForwardEnabled + } + + Action { + id: minimizeMaximizeAction + text: i18nc("toggle between maximized and minimized ivre", "Toggle Maximize") + icon.name: musicWidget.isMaximized ? "draw-arrow-up" : "draw-arrow-down" + onTriggered: musicWidget.isMaximized = !musicWidget.isMaximized + } + Rectangle { anchors.fill: parent color: myPalette.midlight opacity: elisaTheme.mediaPlayerControlOpacity } RowLayout { anchors.fill: parent spacing: 5 - Button { + FlatButtonWithToolTip { + id: minimzeMaximizeAction + action: minimizeMaximizeAction + Layout.preferredWidth: elisaTheme.smallControlButtonSize Layout.preferredHeight: elisaTheme.smallControlButtonSize Layout.alignment: Qt.AlignVCenter Layout.maximumWidth: elisaTheme.smallControlButtonSize Layout.maximumHeight: elisaTheme.smallControlButtonSize Layout.minimumWidth: elisaTheme.smallControlButtonSize Layout.minimumHeight: elisaTheme.smallControlButtonSize Layout.rightMargin: LayoutMirroring.enabled ? elisaTheme.mediaPlayerHorizontalMargin : 0 Layout.leftMargin: !LayoutMirroring.enabled ? elisaTheme.mediaPlayerHorizontalMargin : 0 - - contentItem: Image { - anchors.fill: parent - source: Qt.resolvedUrl(musicWidget.isMaximized ? elisaTheme.minimizeIcon : elisaTheme.maximizeIcon) - - width: elisaTheme.smallControlButtonSize - height: elisaTheme.smallControlButtonSize - - sourceSize.width: elisaTheme.smallControlButtonSize - sourceSize.height: elisaTheme.smallControlButtonSize - - fillMode: Image.PreserveAspectFit - opacity: 1.0 - } - - background: Rectangle { - border.width: 0 - opacity: 0.0 - } - - onClicked: { - musicWidget.isMaximized = !musicWidget.isMaximized - } } - RoundButton { + FlatButtonWithToolTip { + id: skipBackwardButton + action: skipBackwardAction focus: skipBackwardEnabled Layout.preferredWidth: elisaTheme.smallControlButtonSize Layout.preferredHeight: elisaTheme.smallControlButtonSize Layout.alignment: Qt.AlignVCenter Layout.maximumWidth: elisaTheme.smallControlButtonSize Layout.maximumHeight: elisaTheme.smallControlButtonSize Layout.minimumWidth: elisaTheme.smallControlButtonSize Layout.minimumHeight: elisaTheme.smallControlButtonSize - Layout.rightMargin: LayoutMirroring.enabled ? elisaTheme.mediaPlayerHorizontalMargin : 0 - Layout.leftMargin: !LayoutMirroring.enabled ? elisaTheme.mediaPlayerHorizontalMargin : 0 - - enabled: skipBackwardEnabled - hoverEnabled: true - - onClicked: { - musicWidget.playPrevious() - } - - contentItem: Image { - anchors.fill: parent - - source: Qt.resolvedUrl(LayoutMirroring.enabled ? elisaTheme.skipForwardIcon : elisaTheme.skipBackwardIcon) - - width: elisaTheme.smallControlButtonSize - height: elisaTheme.smallControlButtonSize - - sourceSize.width: elisaTheme.smallControlButtonSize - sourceSize.height: elisaTheme.smallControlButtonSize - - fillMode: Image.PreserveAspectFit - - opacity: skipBackwardEnabled ? 1.0 : 0.6 - } - - background: Rectangle { - color: "transparent" - - border.color: (parent.hovered || parent.activeFocus) ? myPalette.highlight : "transparent" - border.width: 1 - - radius: elisaTheme.smallControlButtonSize - - Behavior on border.color { - ColorAnimation { - duration: 300 - } - } - } } - RoundButton { + FlatButtonWithToolTip { + id: playPauseButton + action: playPauseAction focus: playEnabled Layout.preferredWidth: elisaTheme.smallControlButtonSize Layout.preferredHeight: elisaTheme.smallControlButtonSize Layout.alignment: Qt.AlignVCenter Layout.maximumWidth: elisaTheme.smallControlButtonSize Layout.maximumHeight: elisaTheme.smallControlButtonSize Layout.minimumWidth: elisaTheme.smallControlButtonSize Layout.minimumHeight: elisaTheme.smallControlButtonSize - - enabled: playEnabled - hoverEnabled: true - - onClicked: { - if (musicWidget.isPlaying) { - musicWidget.pause() - } else { - musicWidget.play() - } - } - - contentItem: Image { - anchors.fill: parent - - source: { - if (musicWidget.isPlaying) - Qt.resolvedUrl(elisaTheme.pauseIcon) - else - Qt.resolvedUrl(elisaTheme.playIcon) - } - - width: elisaTheme.smallControlButtonSize - height: elisaTheme.smallControlButtonSize - - sourceSize.width: elisaTheme.smallControlButtonSize - sourceSize.height: elisaTheme.smallControlButtonSize - - fillMode: Image.PreserveAspectFit - mirror: LayoutMirroring.enabled - opacity: playEnabled ? 1.0 : 0.6 - } - - background: Rectangle { - color: "transparent" - - border.color: (parent.hovered || parent.activeFocus) ? myPalette.highlight : "transparent" - border.width: 1 - - radius: elisaTheme.smallControlButtonSize - - Behavior on border.color { - ColorAnimation { - duration: 300 - } - } - } } - RoundButton { + FlatButtonWithToolTip { + id: skipForwardButton + action: skipForwardAction focus: skipForwardEnabled Layout.preferredWidth: elisaTheme.smallControlButtonSize Layout.preferredHeight: elisaTheme.smallControlButtonSize Layout.alignment: Qt.AlignVCenter Layout.maximumWidth: elisaTheme.smallControlButtonSize Layout.maximumHeight: elisaTheme.smallControlButtonSize Layout.minimumWidth: elisaTheme.smallControlButtonSize Layout.minimumHeight: elisaTheme.smallControlButtonSize - - enabled: skipForwardEnabled - hoverEnabled: true - - onClicked: { - musicWidget.playNext() - } - - contentItem: Image { - anchors.fill: parent - - source: Qt.resolvedUrl(LayoutMirroring.enabled ? elisaTheme.skipBackwardIcon : elisaTheme.skipForwardIcon) - - width: elisaTheme.smallControlButtonSize - height: elisaTheme.smallControlButtonSize - - sourceSize.width: elisaTheme.smallControlButtonSize - sourceSize.height: elisaTheme.smallControlButtonSize - - fillMode: Image.PreserveAspectFit - - opacity: skipForwardEnabled ? 1.0 : 0.6 - } - - background: Rectangle { - color: "transparent" - - border.color: (parent.hovered || parent.activeFocus) ? myPalette.highlight : "transparent" - border.width: 1 - - radius: elisaTheme.smallControlButtonSize - - Behavior on border.color { - ColorAnimation { - duration: 300 - } - } - } } TextMetrics { id: durationTextMetrics text: i18nc("This is used to preserve a fixed width for the duration text.", "00:00:00") } LabelWithToolTip { id: positionLabel text: timeIndicator.progressDuration color: myPalette.text Layout.alignment: Qt.AlignVCenter Layout.fillHeight: true Layout.rightMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : elisaTheme.layoutHorizontalMargin * 2 Layout.leftMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : elisaTheme.layoutHorizontalMargin * 2 Layout.preferredWidth: durationTextMetrics.width+5 // be in the safe side verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignRight ProgressIndicator { id: timeIndicator position: musicWidget.position } } Slider { property bool seekStarted: false property int seekValue id: musicProgress from: 0 to: musicWidget.duration Layout.alignment: Qt.AlignVCenter Layout.fillWidth: true Layout.rightMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 Layout.leftMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 enabled: musicWidget.seekable && musicWidget.playEnabled live: true onValueChanged: { if (seekStarted) { seekValue = value } } onPressedChanged: { if (pressed) { seekStarted = true; seekValue = value } else { musicWidget.seek(seekValue) seekStarted = false; } } background: Rectangle { x: musicProgress.leftPadding y: musicProgress.topPadding + musicProgress.availableHeight / 2 - height / 2 implicitWidth: 200 implicitHeight: 6 width: musicProgress.availableWidth height: implicitHeight radius: 3 color: myPalette.dark Rectangle { x: (LayoutMirroring.enabled ? musicProgress.visualPosition * parent.width : 0) width: LayoutMirroring.enabled ? parent.width - musicProgress.visualPosition * parent.width: musicProgress.handle.x + radius height: parent.height color: myPalette.text radius: 3 } } handle: Rectangle { x: musicProgress.leftPadding + musicProgress.visualPosition * (musicProgress.availableWidth - width) y: musicProgress.topPadding + musicProgress.availableHeight / 2 - height / 2 implicitWidth: 18 implicitHeight: 18 radius: 9 color: myPalette.base border.width: 1 border.color: musicProgress.pressed ? myPalette.text : myPalette.dark } } LabelWithToolTip { id: durationLabel text: durationIndicator.progressDuration color: myPalette.text Layout.alignment: Qt.AlignVCenter Layout.fillHeight: true Layout.rightMargin: !LayoutMirroring.enabled ? (elisaTheme.layoutHorizontalMargin * 2) : 0 Layout.leftMargin: LayoutMirroring.enabled ? (elisaTheme.layoutHorizontalMargin * 2) : 0 Layout.preferredWidth: durationTextMetrics.width verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignLeft ProgressIndicator { id: durationIndicator position: musicWidget.duration } } - Image { - id: volumeIcon - - source: if (musicWidget.muted) - Qt.resolvedUrl(elisaTheme.playerVolumeMutedIcon) - else - Qt.resolvedUrl(elisaTheme.playerVolumeIcon) - - Layout.preferredWidth: elisaTheme.smallControlButtonSize - Layout.preferredHeight: elisaTheme.smallControlButtonSize - Layout.alignment: Qt.AlignVCenter - Layout.maximumWidth: elisaTheme.smallControlButtonSize - Layout.maximumHeight: elisaTheme.smallControlButtonSize - Layout.minimumWidth: elisaTheme.smallControlButtonSize - Layout.minimumHeight: elisaTheme.smallControlButtonSize - Layout.rightMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 - Layout.leftMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 - - sourceSize.width: elisaTheme.smallControlButtonSize - sourceSize.height: elisaTheme.smallControlButtonSize - - fillMode: Image.PreserveAspectFit - - visible: false - } - - - RoundButton { + FlatButtonWithToolTip { + id: muteButton + action: muteAction focus: true Layout.preferredWidth: elisaTheme.smallControlButtonSize Layout.preferredHeight: elisaTheme.smallControlButtonSize Layout.alignment: Qt.AlignVCenter Layout.maximumWidth: elisaTheme.smallControlButtonSize Layout.maximumHeight: elisaTheme.smallControlButtonSize Layout.minimumWidth: elisaTheme.smallControlButtonSize Layout.minimumHeight: elisaTheme.smallControlButtonSize - - hoverEnabled: true - - onClicked: musicWidget.muted = !musicWidget.muted - - contentItem: Image { - anchors.fill: parent - - source: if (musicWidget.muted) - Qt.resolvedUrl(elisaTheme.playerVolumeMutedIcon) - else - Qt.resolvedUrl(elisaTheme.playerVolumeIcon) - - width: elisaTheme.smallControlButtonSize - height: elisaTheme.smallControlButtonSize - - sourceSize.width: elisaTheme.smallControlButtonSize - sourceSize.height: elisaTheme.smallControlButtonSize - - fillMode: Image.PreserveAspectFit - } - - background: Rectangle { - color: "transparent" - - border.color: (parent.hovered || parent.activeFocus) ? myPalette.highlight : "transparent" - border.width: 1 - - radius: elisaTheme.smallControlButtonSize - - Behavior on border.color { - ColorAnimation { - duration: 300 - } - } - } } Slider { id: volumeSlider from: 0 to: 100 enabled: !muted Layout.alignment: Qt.AlignVCenter Layout.preferredWidth: elisaTheme.volumeSliderWidth Layout.maximumWidth: elisaTheme.volumeSliderWidth Layout.minimumWidth: elisaTheme.volumeSliderWidth Layout.rightMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin * 3 : 0 Layout.leftMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin * 3 : 0 width: elisaTheme.volumeSliderWidth background: Rectangle { x: volumeSlider.leftPadding y: volumeSlider.topPadding + volumeSlider.availableHeight / 2 - height / 2 implicitWidth: 200 implicitHeight: 6 width: volumeSlider.availableWidth height: implicitHeight radius: 3 color: myPalette.dark opacity: muted ? 0.5 : 1 Rectangle { x: (LayoutMirroring.enabled ? volumeSlider.visualPosition * parent.width : 0) width: (LayoutMirroring.enabled ? parent.width - volumeSlider.visualPosition * parent.width : volumeSlider.visualPosition * parent.width) height: parent.height color: myPalette.text radius: 3 opacity: muted ? 0.5 : 1 } } handle: Rectangle { x: volumeSlider.leftPadding + volumeSlider.visualPosition * (volumeSlider.availableWidth - width) y: volumeSlider.topPadding + volumeSlider.availableHeight / 2 - height / 2 implicitWidth: 18 implicitHeight: 18 radius: 9 color: myPalette.base border.width: 1 border.color: volumeSlider.pressed ? myPalette.text : (muted ? myPalette.mid : myPalette.dark) } } - RoundButton { + FlatButtonWithToolTip { focus: true + action: shuffleAction id: shuffleButton Layout.preferredWidth: elisaTheme.smallControlButtonSize Layout.preferredHeight: elisaTheme.smallControlButtonSize Layout.alignment: Qt.AlignCenter Layout.maximumWidth: elisaTheme.smallControlButtonSize Layout.maximumHeight: elisaTheme.smallControlButtonSize Layout.minimumWidth: elisaTheme.smallControlButtonSize Layout.minimumHeight: elisaTheme.smallControlButtonSize + } - hoverEnabled: true - onClicked: musicWidget.shuffle = !musicWidget.shuffle - - contentItem: Image { - anchors.fill: parent - - source: musicWidget.shuffle ? Qt.resolvedUrl(elisaTheme.shuffleIcon) : Qt.resolvedUrl(elisaTheme.noShuffleIcon) - - width: elisaTheme.smallControlButtonSize - height: elisaTheme.smallControlButtonSize - - sourceSize.width: elisaTheme.smallControlButtonSize - sourceSize.height: elisaTheme.smallControlButtonSize - - fillMode: Image.PreserveAspectFit - } - - background: Rectangle { - color: "transparent" - } - } - - - RoundButton { + FlatButtonWithToolTip { focus: true + action: repeatAction id: repeatButton Layout.preferredWidth: elisaTheme.smallControlButtonSize Layout.preferredHeight: elisaTheme.smallControlButtonSize Layout.alignment: Qt.AlignCenter Layout.maximumWidth: elisaTheme.smallControlButtonSize Layout.maximumHeight: elisaTheme.smallControlButtonSize Layout.minimumWidth: elisaTheme.smallControlButtonSize Layout.minimumHeight: elisaTheme.smallControlButtonSize - - hoverEnabled: true - onClicked: musicWidget.repeat = !musicWidget.repeat - - contentItem: Image { - anchors.fill: parent - - source: musicWidget.repeat ? Qt.resolvedUrl(elisaTheme.repeatIcon) : Qt.resolvedUrl(elisaTheme.noRepeatIcon) - - width: elisaTheme.smallControlButtonSize - height: elisaTheme.smallControlButtonSize - - sourceSize.width: elisaTheme.smallControlButtonSize - sourceSize.height: elisaTheme.smallControlButtonSize - - fillMode: Image.PreserveAspectFit - } - - background: Rectangle { - color: "transparent" - } - } - Controls1.ToolButton { + FlatButtonWithToolTip { id: menuButton action: applicationMenuAction + focus: true Layout.preferredWidth: elisaTheme.smallControlButtonSize Layout.preferredHeight: elisaTheme.smallControlButtonSize Layout.alignment: Qt.AlignVCenter Layout.maximumWidth: elisaTheme.smallControlButtonSize Layout.maximumHeight: elisaTheme.smallControlButtonSize Layout.minimumWidth: elisaTheme.smallControlButtonSize Layout.minimumHeight: elisaTheme.smallControlButtonSize Layout.rightMargin: !LayoutMirroring.enabled ? elisaTheme.mediaPlayerHorizontalMargin : elisaTheme.mediaPlayerHorizontalMargin * 1.5 Layout.leftMargin: LayoutMirroring.enabled ? elisaTheme.mediaPlayerHorizontalMargin : elisaTheme.mediaPlayerHorizontalMargin * 1.5 } } onPositionChanged: { if (!musicProgress.seekStarted) { musicProgress.value = position } } onIsMaximizedChanged: { if (musicWidget.isMaximized) { musicWidget.maximize() } else { musicWidget.minimize() } } } diff --git a/src/resources.qrc b/src/resources.qrc index bfd609de..891d9434 100644 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -1,47 +1,48 @@ qml/MediaPlayerControl.qml qml/RatingStar.qml qml/MediaPlayListView.qml qml/ElisaMainWindow.qml qml/ApplicationMenu.qml qml/HeaderBar.qml qml/ContextView.qml qml/ContentView.qml qml/DraggableItem.qml qml/PassiveNotification.qml qml/NavigationActionBar.qml qml/PlayListEntry.qml qml/MediaBrowser.qml qml/Theme.qml qml/PlatformIntegration.qml qml/LabelWithToolTip.qml qml/TopNotification.qml qml/TrackImportNotification.qml qml/TopNotificationItem.qml qml/ViewSelector.qml qml/MediaTrackDelegate.qml qml/MediaAlbumTrackDelegate.qml qml/MediaTrackMetadataView.qml qml/GridBrowserView.qml qml/GridBrowserDelegate.qml qml/ListBrowserView.qml qml/FileBrowserDelegate.qml qml/FileBrowserView.qml qtquickcontrols2.conf background.png qml/ViewManager.qml qml/BaseTheme.qml qml/ScrollHelper.qml + qml/FlatButtonWithToolTip.qml windows/WindowsTheme.qml windows/PlatformIntegration.qml windows/LabelWithToolTip.qml android/ElisaMainWindow.qml android/AndroidTheme.qml android/PlatformIntegration.qml