diff --git a/modules/gui/qt/Makefile.am b/modules/gui/qt/Makefile.am index fcc8e3b9e9be284327b1be37f1f2897480cacc57..bfd2d79fb80ce18042566f8a1f8cb5c2797b07b5 100644 --- a/modules/gui/qt/Makefile.am +++ b/modules/gui/qt/Makefile.am @@ -334,6 +334,7 @@ libqt_plugin_la_SOURCES = \ util/vlcqtmessagehandler.hpp \ util/colorizedsvgicon.cpp \ util/colorizedsvgicon.hpp \ + util/filesystemwatcher.hpp \ widgets/native/animators.cpp \ widgets/native/animators.hpp \ widgets/native/customwidgets.cpp widgets/native/customwidgets.hpp \ @@ -487,6 +488,7 @@ nodist_libqt_plugin_la_SOURCES = \ util/variables.moc.cpp \ util/vlctick.moc.cpp \ util/dismiss_popup_event_filter.moc.cpp \ + util/filesystemwatcher.moc.cpp \ util/list_selection_model.moc.cpp \ util/vlchotkeyconverter.moc.cpp \ util/qsgtextureview.moc.cpp \ diff --git a/modules/gui/qt/dialogs/toolbar/qml/EditorDNDDelegate.qml b/modules/gui/qt/dialogs/toolbar/qml/EditorDNDDelegate.qml index c885c2729d58f1f6ff7d92a3a47b1a6f3e8821da..1caf1957f4eaecca3e44d5baa43412b757252c8e 100644 --- a/modules/gui/qt/dialogs/toolbar/qml/EditorDNDDelegate.qml +++ b/modules/gui/qt/dialogs/toolbar/qml/EditorDNDDelegate.qml @@ -158,7 +158,7 @@ T.Control { anchors.fill: (parent === control.contentItem) ? parent : undefined - source: PlayerControlbarControls.control(model.id).source + source: PlayerControlbarControls.controlList.control(model.id).source Drag.source: control diff --git a/modules/gui/qt/dialogs/toolbar/qml/ToolbarEditor.qml b/modules/gui/qt/dialogs/toolbar/qml/ToolbarEditor.qml index 0ae25ad93763d3c5d17e1e69aea8f02293c5a599..df2b2ad61b8b1782d0211b2b966d04ad37421d06 100644 --- a/modules/gui/qt/dialogs/toolbar/qml/ToolbarEditor.qml +++ b/modules/gui/qt/dialogs/toolbar/qml/ToolbarEditor.qml @@ -26,6 +26,7 @@ import VLC.Style import VLC.Widgets as Widgets import VLC.Util import VLC.PlayerControls +import VLC.Player // PlayerControlbarControls Item { id: root @@ -44,6 +45,55 @@ Item { colorSet: ColorContext.View } + FileSystemWatcher { + // This is added to automatically update the buttons list, which + // is useful if the user adds a new external control while the + // dialog is open. This could be added to the PlayerControlbarControls + // singleton so that updates occur when the toolbar editor dialog + // is not open, but I see little reason for that as the user should + // restart the application instead if they make changes while the + // dialog is not open. + + path: { + const dir = PlayerControlbarControls.externalControlPath + if (MainCtx.dirExists(dir)) + return dir + else { + // Prepare: + if (MainCtx.mkDir(dir)) { + MainCtx.mkFile(dir + "/README.txt", + qsTr("You may place external controls in this directory.\n" + + "The control file name must be <id>-<name>.qml, where\n" + + "<id> is an arbitrary integer between 0 and %1, and\n" + + "<name> is the name of the control. An example control\n" + + "is available below:\n\n" + + "```\n" + + "import QtQuick\n\n" + + "import VLC.Widgets as Widgets\n" + + "import VLC.Style\n" + + "import VLC.Player\n" + + "import VLC.Playlist\n\n" + + "Widgets.IconToolButton {\n" + + " id: stopBtn\n" + + " enabled: Player.isStarted\n" + + " text: VLCIcons.stop\n" + + " onClicked: MainPlaylistController.stop()\n" + + " description: qsTr(\"Stop\") \n" + + "}\n" + + "```\n").arg(ControlListModel.EXTERNAL_END - ControlListModel.EXTERNAL)) + return dir + } + } + + return "" + } + + onDirectoryChanged: { + // Files added or removed: + PlayerControlbarControls.refreshExternalControls() + } + } + ColumnLayout{ anchors.fill: parent spacing: 0 diff --git a/modules/gui/qt/dialogs/toolbar/qml/ToolbarEditorButtonList.qml b/modules/gui/qt/dialogs/toolbar/qml/ToolbarEditorButtonList.qml index 3d4c9e2c41edc53cf5ed8fb0dc0ebfa94e000c07..e1c93385afaf7b3197812628f97fa26a01e2e020 100644 --- a/modules/gui/qt/dialogs/toolbar/qml/ToolbarEditorButtonList.qml +++ b/modules/gui/qt/dialogs/toolbar/qml/ToolbarEditorButtonList.qml @@ -32,7 +32,7 @@ GridView { clip: true ScrollBar.vertical: ScrollBar { } - model: PlayerControlbarControls.controlList.length + model: PlayerControlbarControls.controlList.controls.length currentIndex: -1 highlightFollowsCurrentItem: false @@ -139,7 +139,7 @@ GridView { drag.smoothed: false - readonly property int mIndex: PlayerControlbarControls.controlList[model.index].id + readonly property int mIndex: PlayerControlbarControls.controlList.controls[model.index].id readonly property ColorContext colorContext: ColorContext { colorSet: ColorContext.Item @@ -149,7 +149,7 @@ GridView { if (drag.active) { root.controlDragStarted(mIndex) - buttonDragItem.text = PlayerControlbarControls.controlList[model.index].label + buttonDragItem.text = PlayerControlbarControls.controlList.controls[model.index].label buttonDragItem.Drag.source = this buttonDragItem.Drag.start() @@ -191,7 +191,7 @@ GridView { Layout.alignment: Qt.AlignHCenter color: colorContext.fg.primary - text: PlayerControlbarControls.controlList[model.index].label + text: PlayerControlbarControls.controlList.controls[model.index].label } Widgets.ListSubtitleLabel { @@ -201,7 +201,7 @@ GridView { color: colorContext.fg.secondary elide: Text.ElideNone fontSizeMode: Text.Fit - text: PlayerControlbarControls.controlList[model.index].text + text: PlayerControlbarControls.controlList.controls[model.index].text wrapMode: Text.WordWrap horizontalAlignment: Text.AlignHCenter } diff --git a/modules/gui/qt/dialogs/toolbar/qml/ToolbarEditorDialog.qml b/modules/gui/qt/dialogs/toolbar/qml/ToolbarEditorDialog.qml index ffff3c17cb82372ea145d28c3f647f273e2e2c57..2b55111d053a3caf4e326f265e4844c0986bc140 100644 --- a/modules/gui/qt/dialogs/toolbar/qml/ToolbarEditorDialog.qml +++ b/modules/gui/qt/dialogs/toolbar/qml/ToolbarEditorDialog.qml @@ -24,7 +24,7 @@ import VLC.MainInterface import VLC.Widgets as Widgets import VLC.Style import VLC.Dialogs - +import VLC.Player // PlayerControlbarControls WindowDialog { id: root @@ -181,6 +181,15 @@ WindowDialog { MainCtx.controlbarProfileModel.deleteSelectedProfile() } } + + Widgets.IconToolButton { + description: qsTr("Browse the external controls directory...") + text: VLCIcons.ellipsis + + onClicked: { + Qt.openUrlExternally(PlayerControlbarControls.externalControlPath) + } + } } // The main context of the toolbareditor dialog: diff --git a/modules/gui/qt/maininterface/mainctx.cpp b/modules/gui/qt/maininterface/mainctx.cpp index cfa81d5e2de2ddd7710641df0e4860e86fc18528..d0406f9d9d2b8715e40aeefd760508d4cb319950 100644 --- a/modules/gui/qt/maininterface/mainctx.cpp +++ b/modules/gui/qt/maininterface/mainctx.cpp @@ -771,6 +771,39 @@ QJSValue MainCtx::urlListToMimeData(const QJSValue &array) { return ret; } +QStringList MainCtx::getFilesInDirectory(const QString &path, int sortFlags) +{ + const QDir directory(QUrl(path).toLocalFile()); + return directory.entryList(QDir::Filter::Files, static_cast<QDir::SortFlags>(sortFlags)); +} + +bool MainCtx::dirExists(const QString &path) +{ + const QDir directory(QUrl(path).toLocalFile()); + return directory.exists(); +} + +bool MainCtx::mkDir(const QString &path) +{ + const QDir directory(QUrl(path).toLocalFile()); + if (directory.exists()) + return false; + return directory.mkpath(directory.path()); +} + +bool MainCtx::mkFile(const QString &path, const QString &content) +{ + QFile file(QUrl(path).toLocalFile()); + if (file.open(QIODevice::OpenModeFlag::WriteOnly)) + { + QTextStream stream(&file); + stream << content; + return (stream.status() == QTextStream::Ok); + } + + return false; +} + VideoSurfaceProvider* MainCtx::getVideoSurfaceProvider() const { return m_videoSurfaceProvider; diff --git a/modules/gui/qt/maininterface/mainctx.hpp b/modules/gui/qt/maininterface/mainctx.hpp index ba82c43bccef7dbdaf5a6a78570655ee136a6687..127b251c152ae7ceadf411c8152a9bb57ebdfd73 100644 --- a/modules/gui/qt/maininterface/mainctx.hpp +++ b/modules/gui/qt/maininterface/mainctx.hpp @@ -332,6 +332,12 @@ public: Q_INVOKABLE virtual bool platformHandlesTitleBarButtonsWithCSD() const { return false; }; Q_INVOKABLE virtual bool platformHandlesShadowsWithCSD() const { return false; }; + // Filesystem accessories: + Q_INVOKABLE static QStringList getFilesInDirectory(const QString& path, int sortFlags = -1); + Q_INVOKABLE static bool dirExists(const QString& path); + Q_INVOKABLE static bool mkDir(const QString& path); + Q_INVOKABLE static bool mkFile(const QString& path, const QString& content = {}); + /** * @brief ask for the application to terminate */ diff --git a/modules/gui/qt/maininterface/mainui.cpp b/modules/gui/qt/maininterface/mainui.cpp index f3f3450b02aa1b92635dc61f32a1e55f55a8b755..4d78ac7fa8985b55ef3eff3918a13127f06197ce 100644 --- a/modules/gui/qt/maininterface/mainui.cpp +++ b/modules/gui/qt/maininterface/mainui.cpp @@ -46,6 +46,7 @@ #include "util/csdbuttonmodel.hpp" #include "util/vlctick.hpp" #include "util/list_selection_model.hpp" +#include "util/filesystemwatcher.hpp" #include "dialogs/help/aboutmodel.hpp" #include "dialogs/dialogs_provider.hpp" @@ -337,6 +338,7 @@ void MainUI::registerQMLTypes() qmlRegisterType<FlickableScrollHandler>( uri, versionMajor, versionMinor, "FlickableScrollHandler" ); qmlRegisterType<ListSelectionModel>( uri, versionMajor, versionMinor, "ListSelectionModel" ); qmlRegisterType<DoubleClickIgnoringItem>( uri, versionMajor, versionMinor, "DoubleClickIgnoringItem" ); + qmlRegisterType<FileSystemWatcher>( uri, versionMajor, versionMinor, "FileSystemWatcher" ); qmlRegisterModule(uri, versionMajor, versionMinor); qmlProtectModule(uri, versionMajor); diff --git a/modules/gui/qt/maininterface/qml/MainInterface.qml b/modules/gui/qt/maininterface/qml/MainInterface.qml index 57897e53e7107815fbab2f94a19b2051f4b4cfa9..118b534cf6767a9bae74de618f76b9726decf8e8 100644 --- a/modules/gui/qt/maininterface/qml/MainInterface.qml +++ b/modules/gui/qt/maininterface/qml/MainInterface.qml @@ -18,6 +18,7 @@ // NOTE: All imports used throughout the interface // must be imported here as well: +import QtCore import QtQml import QtQml.Models import QtQuick diff --git a/modules/gui/qt/meson.build b/modules/gui/qt/meson.build index 3740f7ac1436660cf2151171abdc589fb50a5de3..c1d2c0e9116f741e809246cc2c294ecc2c928620 100644 --- a/modules/gui/qt/meson.build +++ b/modules/gui/qt/meson.build @@ -145,6 +145,7 @@ moc_headers = files( 'util/dismiss_popup_event_filter.hpp', 'util/list_selection_model.hpp', 'util/qsgtextureview.hpp', + 'util/filesystemwatcher.hpp', 'widgets/native/animators.hpp', 'widgets/native/csdthemeimage.hpp', 'widgets/native/customwidgets.hpp', @@ -478,6 +479,7 @@ some_sources = files( 'util/dismiss_popup_event_filter.hpp', 'util/list_selection_model.cpp', 'util/list_selection_model.hpp', + 'util/filesystemwatcher.hpp', 'util/model_recovery_agent.hpp', 'util/vlcqtmessagehandler.cpp', 'util/vlcqtmessagehandler.hpp', diff --git a/modules/gui/qt/player/control_list_model.hpp b/modules/gui/qt/player/control_list_model.hpp index 127cd9fea750a2482e26700985422a6da50a33b6..dafb88b24bf7d2fb6d28103b93afde85a3644f50 100644 --- a/modules/gui/qt/player/control_list_model.hpp +++ b/modules/gui/qt/player/control_list_model.hpp @@ -83,6 +83,9 @@ public: WIDGET_SPACER = 0x40, WIDGET_SPACER_EXTEND, WIDGET_MAX, + + EXTERNAL = 0x160, + EXTERNAL_END = 0x320 }; Q_ENUM(ControlType) diff --git a/modules/gui/qt/player/qml/ControlRepeater.qml b/modules/gui/qt/player/qml/ControlRepeater.qml index 127f02c9805876fb0337fc1bffcf98f2dc51ae70..a63e41c9480d2e01514884d6419370fe112a668d 100644 --- a/modules/gui/qt/player/qml/ControlRepeater.qml +++ b/modules/gui/qt/player/qml/ControlRepeater.qml @@ -51,7 +51,7 @@ Repeater { // Settings - source: PlayerControlbarControls.control(model.id).source + source: PlayerControlbarControls.controlList.control(model.id).source focus: (index === 0) diff --git a/modules/gui/qt/player/qml/PlayerControlbarControls.qml b/modules/gui/qt/player/qml/PlayerControlbarControls.qml index 9b4c78fe66053dfbb05672b0362a1487746ec6da..1b429dbbfb6c634c8f126bc8770b1771b0b1d766 100644 --- a/modules/gui/qt/player/qml/PlayerControlbarControls.qml +++ b/modules/gui/qt/player/qml/PlayerControlbarControls.qml @@ -18,6 +18,7 @@ pragma Singleton +import QtCore import QtQml @@ -29,57 +30,101 @@ import VLC.Style QtObject { readonly property string controlPath : "qrc:///qt/qml/VLC/PlayerControls/" - readonly property var controlList: [ - { id: ControlListModel.PLAY_BUTTON, file: "PlayButton.qml", label: VLCIcons.play_filled, text: qsTr("Play") }, - { id: ControlListModel.STOP_BUTTON, file: "StopButton.qml", label: VLCIcons.stop, text: qsTr("Stop") }, - { id: ControlListModel.OPEN_BUTTON, file: "OpenButton.qml", label: VLCIcons.eject, text: qsTr("Open") }, - { id: ControlListModel.PREVIOUS_BUTTON, file: "PreviousButton.qml", label: VLCIcons.previous, text: qsTr("Previous") }, - { id: ControlListModel.NEXT_BUTTON, file: "NextButton.qml", label: VLCIcons.next, text: qsTr("Next") }, - { id: ControlListModel.SLOWER_BUTTON, file: "SlowerButton.qml", label: VLCIcons.slower, text: qsTr("Slower") }, - { id: ControlListModel.FASTER_BUTTON, file: "FasterButton.qml", label: VLCIcons.faster, text: qsTr("Faster") }, - { id: ControlListModel.FULLSCREEN_BUTTON, file: "FullscreenButton.qml", label: VLCIcons.fullscreen, text: qsTr("Fullscreen") }, - { id: ControlListModel.EXTENDED_BUTTON, file: "ExtendedSettingsButton.qml", label: VLCIcons.effect_filter, text: qsTr("Extended panel") }, - { id: ControlListModel.PLAYLIST_BUTTON, file: "PlaylistButton.qml", label: VLCIcons.playlist, text: qsTr("Playlist") }, - { id: ControlListModel.SNAPSHOT_BUTTON, file: "SnapshotButton.qml", label: VLCIcons.snapshot, text: qsTr("Snapshot") }, - { id: ControlListModel.RECORD_BUTTON, file: "RecordButton.qml", label: VLCIcons.record, text: qsTr("Record") }, - { id: ControlListModel.ATOB_BUTTON, file: "AtoBButton.qml", label: VLCIcons.atob, text: qsTr("A-B Loop") }, - { id: ControlListModel.FRAME_BUTTON, file: "FrameButton.qml", label: VLCIcons.frame_by_frame, text: qsTr("Frame By Frame") }, - { id: ControlListModel.REVERSE_BUTTON, file: "ReverseButton.qml", label: VLCIcons.play_reverse, text: qsTr("Trickplay Reverse") }, - { id: ControlListModel.SKIP_BACK_BUTTON, file: "SkipBackButton.qml", label: VLCIcons.skip_back, text: qsTr("Step backward") }, - { id: ControlListModel.SKIP_FW_BUTTON, file: "SkipForwardButton.qml", label: VLCIcons.skip_for, text: qsTr("Step forward") }, - { id: ControlListModel.QUIT_BUTTON, file: "QuitButton.qml", label: VLCIcons.clear, text: qsTr("Quit") }, - { id: ControlListModel.RANDOM_BUTTON, file: "RandomButton.qml", label: VLCIcons.shuffle, text: qsTr("Random") }, - { id: ControlListModel.LOOP_BUTTON, file: "LoopButton.qml", label: VLCIcons.repeat_all, text: qsTr("Loop") }, - { id: ControlListModel.INFO_BUTTON, file: "InfoButton.qml", label: VLCIcons.info, text: qsTr("Information") }, - { id: ControlListModel.LANG_BUTTON, file: "LangButton.qml", label: VLCIcons.audiosub, text: qsTr("Open subtitles") }, - { id: ControlListModel.BOOKMARK_BUTTON, file: "BookmarkButton.qml", label: VLCIcons.bookmark, text: qsTr("Bookmark Button") }, - { id: ControlListModel.CHAPTER_PREVIOUS_BUTTON, file: "ChapterPreviousButton.qml", label: VLCIcons.dvd_prev, text: qsTr("Previous chapter") }, - { id: ControlListModel.CHAPTER_NEXT_BUTTON, file: "ChapterNextButton.qml", label: VLCIcons.dvd_next, text: qsTr("Next chapter") }, - { id: ControlListModel.VOLUME, file: "VolumeWidget.qml", label: VLCIcons.volume_high, text: qsTr("Volume Widget") }, - { id: ControlListModel.NAVIGATION_BOX, file: "NavigationBoxButton.qml", label: VLCIcons.ic_fluent_arrow_move, text: qsTr("Navigation Box") }, - { id: ControlListModel.NAVIGATION_BUTTONS, file: "NavigationWidget.qml", label: VLCIcons.dvd_menu, text: qsTr("Navigation") }, - { id: ControlListModel.DVD_MENUS_BUTTON, file: "DvdMenuButton.qml", label: VLCIcons.dvd_menu, text: qsTr("DVD menus") }, - { id: ControlListModel.PROGRAM_BUTTON, file: "ProgramButton.qml", label: VLCIcons.tv, text: qsTr("Program Button") }, - { id: ControlListModel.TELETEXT_BUTTONS, file: "TeletextButton.qml", label: VLCIcons.tvtelx, text: qsTr("Teletext") }, - { id: ControlListModel.RENDERER_BUTTON, file: "RendererButton.qml", label: VLCIcons.renderer, text: qsTr("Renderer Button") }, - { id: ControlListModel.ASPECT_RATIO_COMBOBOX, file: "AspectRatioWidget.qml", label: VLCIcons.aspect_ratio, text: qsTr("Aspect Ratio") }, - { id: ControlListModel.WIDGET_SPACER, file: "SpacerWidget.qml", label: VLCIcons.space, text: qsTr("Spacer") }, - { id: ControlListModel.WIDGET_SPACER_EXTEND, file: "ExpandingSpacerWidget.qml", label: VLCIcons.space, text: qsTr("Expanding Spacer") }, - { id: ControlListModel.PLAYER_SWITCH_BUTTON, file: "PlayerSwitchButton.qml", label: VLCIcons.fullscreen, text: qsTr("Switch Player") }, - { id: ControlListModel.ARTWORK_INFO, file: "ArtworkInfoWidget.qml", label: VLCIcons.info, text: qsTr("Artwork Info") }, - { id: ControlListModel.PLAYBACK_SPEED_BUTTON, file: "PlaybackSpeedButton.qml", label: "1x", text: qsTr("Playback Speed") }, - { id: ControlListModel.HIGH_RESOLUTION_TIME_WIDGET, file: "HighResolutionTimeWidget.qml", label: VLCIcons.info, text: qsTr("High Resolution Time") } - ] - - function control(id) { - const entry = controlList.find( function(e) { return ( e.id === id ) } ) + readonly property var controlList: ({ + control: _control /* retriever function */, + controls: [ + { id: ControlListModel.PLAY_BUTTON, file: "PlayButton.qml", label: VLCIcons.play_filled, text: qsTr("Play") }, + { id: ControlListModel.STOP_BUTTON, file: "StopButton.qml", label: VLCIcons.stop, text: qsTr("Stop") }, + { id: ControlListModel.OPEN_BUTTON, file: "OpenButton.qml", label: VLCIcons.eject, text: qsTr("Open") }, + { id: ControlListModel.PREVIOUS_BUTTON, file: "PreviousButton.qml", label: VLCIcons.previous, text: qsTr("Previous") }, + { id: ControlListModel.NEXT_BUTTON, file: "NextButton.qml", label: VLCIcons.next, text: qsTr("Next") }, + { id: ControlListModel.SLOWER_BUTTON, file: "SlowerButton.qml", label: VLCIcons.slower, text: qsTr("Slower") }, + { id: ControlListModel.FASTER_BUTTON, file: "FasterButton.qml", label: VLCIcons.faster, text: qsTr("Faster") }, + { id: ControlListModel.FULLSCREEN_BUTTON, file: "FullscreenButton.qml", label: VLCIcons.fullscreen, text: qsTr("Fullscreen") }, + { id: ControlListModel.EXTENDED_BUTTON, file: "ExtendedSettingsButton.qml", label: VLCIcons.effect_filter, text: qsTr("Extended panel") }, + { id: ControlListModel.PLAYLIST_BUTTON, file: "PlaylistButton.qml", label: VLCIcons.playlist, text: qsTr("Playlist") }, + { id: ControlListModel.SNAPSHOT_BUTTON, file: "SnapshotButton.qml", label: VLCIcons.snapshot, text: qsTr("Snapshot") }, + { id: ControlListModel.RECORD_BUTTON, file: "RecordButton.qml", label: VLCIcons.record, text: qsTr("Record") }, + { id: ControlListModel.ATOB_BUTTON, file: "AtoBButton.qml", label: VLCIcons.atob, text: qsTr("A-B Loop") }, + { id: ControlListModel.FRAME_BUTTON, file: "FrameButton.qml", label: VLCIcons.frame_by_frame, text: qsTr("Frame By Frame") }, + { id: ControlListModel.REVERSE_BUTTON, file: "ReverseButton.qml", label: VLCIcons.play_reverse, text: qsTr("Trickplay Reverse") }, + { id: ControlListModel.SKIP_BACK_BUTTON, file: "SkipBackButton.qml", label: VLCIcons.skip_back, text: qsTr("Step backward") }, + { id: ControlListModel.SKIP_FW_BUTTON, file: "SkipForwardButton.qml", label: VLCIcons.skip_for, text: qsTr("Step forward") }, + { id: ControlListModel.QUIT_BUTTON, file: "QuitButton.qml", label: VLCIcons.clear, text: qsTr("Quit") }, + { id: ControlListModel.RANDOM_BUTTON, file: "RandomButton.qml", label: VLCIcons.shuffle, text: qsTr("Random") }, + { id: ControlListModel.LOOP_BUTTON, file: "LoopButton.qml", label: VLCIcons.repeat_all, text: qsTr("Loop") }, + { id: ControlListModel.INFO_BUTTON, file: "InfoButton.qml", label: VLCIcons.info, text: qsTr("Information") }, + { id: ControlListModel.LANG_BUTTON, file: "LangButton.qml", label: VLCIcons.audiosub, text: qsTr("Open subtitles") }, + { id: ControlListModel.BOOKMARK_BUTTON, file: "BookmarkButton.qml", label: VLCIcons.bookmark, text: qsTr("Bookmark Button") }, + { id: ControlListModel.CHAPTER_PREVIOUS_BUTTON, file: "ChapterPreviousButton.qml", label: VLCIcons.dvd_prev, text: qsTr("Previous chapter") }, + { id: ControlListModel.CHAPTER_NEXT_BUTTON, file: "ChapterNextButton.qml", label: VLCIcons.dvd_next, text: qsTr("Next chapter") }, + { id: ControlListModel.VOLUME, file: "VolumeWidget.qml", label: VLCIcons.volume_high, text: qsTr("Volume Widget") }, + { id: ControlListModel.NAVIGATION_BOX, file: "NavigationBoxButton.qml", label: VLCIcons.ic_fluent_arrow_move, text: qsTr("Navigation Box") }, + { id: ControlListModel.NAVIGATION_BUTTONS, file: "NavigationWidget.qml", label: VLCIcons.dvd_menu, text: qsTr("Navigation") }, + { id: ControlListModel.DVD_MENUS_BUTTON, file: "DvdMenuButton.qml", label: VLCIcons.dvd_menu, text: qsTr("DVD menus") }, + { id: ControlListModel.PROGRAM_BUTTON, file: "ProgramButton.qml", label: VLCIcons.tv, text: qsTr("Program Button") }, + { id: ControlListModel.TELETEXT_BUTTONS, file: "TeletextButton.qml", label: VLCIcons.tvtelx, text: qsTr("Teletext") }, + { id: ControlListModel.RENDERER_BUTTON, file: "RendererButton.qml", label: VLCIcons.renderer, text: qsTr("Renderer Button") }, + { id: ControlListModel.ASPECT_RATIO_COMBOBOX, file: "AspectRatioWidget.qml", label: VLCIcons.aspect_ratio, text: qsTr("Aspect Ratio") }, + { id: ControlListModel.WIDGET_SPACER, file: "SpacerWidget.qml", label: VLCIcons.space, text: qsTr("Spacer") }, + { id: ControlListModel.WIDGET_SPACER_EXTEND, file: "ExpandingSpacerWidget.qml", label: VLCIcons.space, text: qsTr("Expanding Spacer") }, + { id: ControlListModel.PLAYER_SWITCH_BUTTON, file: "PlayerSwitchButton.qml", label: VLCIcons.fullscreen, text: qsTr("Switch Player") }, + { id: ControlListModel.ARTWORK_INFO, file: "ArtworkInfoWidget.qml", label: VLCIcons.info, text: qsTr("Artwork Info") }, + { id: ControlListModel.PLAYBACK_SPEED_BUTTON, file: "PlaybackSpeedButton.qml", label: "1x", text: qsTr("Playback Speed") }, + { id: ControlListModel.HIGH_RESOLUTION_TIME_WIDGET, file: "HighResolutionTimeWidget.qml", label: VLCIcons.info, text: qsTr("High Resolution Time") }, + ..._externalControls + ] + }) + + property var _externalControls: [] + + readonly property string externalControlPath: (StandardPaths.writableLocation(StandardPaths.AppConfigLocation) + "/qt-external-controls/") + + function refreshExternalControls() { + const files = MainCtx.getFilesInDirectory(externalControlPath) + let externalControls = [] + if (files.length > 0) { + for (const file of files) { + if (file.includes('-') && file.endsWith('.qml')) { + const split = file.split('-') + const id = Math.trunc(Number(split[0])) + if (id >= 0 && id <= (ControlListModel.EXTERNAL_END - ControlListModel.EXTERNAL)) { + const text = split[1].slice(0, -4) // slice the ".qml" extension + if (text.length > 0) { + const obj = { id: ControlListModel.EXTERNAL + id, file: file, label: VLCIcons.dropzone, text: text } + externalControls.push(obj) + } + } + } + } + } + _externalControls = externalControls + } + + + Component.onCompleted: { + refreshExternalControls() + } + + function _control(id) { + const entry = controlList.controls.find( function(e) { return ( e.id === id ) } ) + const isExternal = (id >= ControlListModel.EXTERNAL && id <= ControlListModel.EXTERNAL_END) if (entry === undefined) { - console.warn("control delegate id " + id + " doesn't exist") + if (isExternal) + console.warn("external control with id " + (id - ControlListModel.EXTERNAL) + " does not exist") + else + console.warn("internal control with id " + id + " doesn't exist") return { source: controlPath + "Fallback.qml" } } - entry.source = controlPath + entry.file + let dir + if (isExternal) + dir = externalControlPath + else + dir = controlPath + + entry.source = dir + entry.file return entry } diff --git a/modules/gui/qt/player/qml/controlbarcontrols/Fallback.qml b/modules/gui/qt/player/qml/controlbarcontrols/Fallback.qml index 6686e5b18586ec3dd42cb6b6c9a764ca870748c6..fab3ef262c24e0948d04b5a8ad2f1086abfbc872 100644 --- a/modules/gui/qt/player/qml/controlbarcontrols/Fallback.qml +++ b/modules/gui/qt/player/qml/controlbarcontrols/Fallback.qml @@ -47,5 +47,6 @@ Control { text: qsTr("WIDGET\nNOT\nFOUND") horizontalAlignment: Text.AlignHCenter color: theme.fg.primary + fontSizeMode: Text.Fit } } diff --git a/modules/gui/qt/plugins.hpp b/modules/gui/qt/plugins.hpp index 64c759c4712f8a73b6607d1aaefbd7b5f67cb3a9..d38f9282b9f9998c4c83c2cad3da502092cb334f 100644 --- a/modules/gui/qt/plugins.hpp +++ b/modules/gui/qt/plugins.hpp @@ -57,6 +57,7 @@ Q_IMPORT_QML_PLUGIN(QtGraphicalEffectsPlugin) Q_IMPORT_QML_PLUGIN(QtGraphicalEffectsPrivatePlugin) #endif + Q_IMPORT_QML_PLUGIN(QtQmlCorePlugin) Q_IMPORT_QML_PLUGIN(QtQmlModelsPlugin) Q_IMPORT_QML_PLUGIN(QtQmlPlugin) Q_IMPORT_QML_PLUGIN(QtQmlWorkerScriptPlugin) diff --git a/modules/gui/qt/util/filesystemwatcher.hpp b/modules/gui/qt/util/filesystemwatcher.hpp new file mode 100644 index 0000000000000000000000000000000000000000..998eb509ab24fb9cf3018ea1c7c4f64a90b712a8 --- /dev/null +++ b/modules/gui/qt/util/filesystemwatcher.hpp @@ -0,0 +1,70 @@ +/***************************************************************************** + * Copyright (C) 2025 VLC authors and VideoLAN + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * ( at your option ) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. + *****************************************************************************/ +#ifndef FILESYSTEMWATCHER_HPP +#define FILESYSTEMWATCHER_HPP + +#include <QFileSystemWatcher> +#include <QQmlEngine> + +class FileSystemWatcher : public QFileSystemWatcher +{ + Q_OBJECT + + Q_PROPERTY(QString path MEMBER m_path WRITE setPath NOTIFY pathChanged FINAL) + + QML_ELEMENT + +public: + explicit FileSystemWatcher(QObject *parent = nullptr) : QFileSystemWatcher(parent) + { } + + void setPath(const QString& path) + { + QString localFile = QUrl(path).toLocalFile(); + + if (localFile == m_path) + return; + + bool changed = false; + + if (!m_path.isEmpty()) + { + removePath(m_path); + m_path.clear(); + changed = true; + } + + if (!localFile.isEmpty() && addPath(localFile)) + { + m_path = localFile; + changed = true; + } + + if (changed) + emit pathChanged(); + + } + +signals: + void pathChanged(); + +private: + QString m_path; +}; + +#endif // FILESYSTEMWATCHER_HPP