[vlc-commits] [Git][videolan/vlc][master] 34 commits: qml: fix focus management when clear button gets invisible in `SearchBox`

Steve Lhomme (@robUx4) gitlab at videolan.org
Mon Aug 3 14:59:51 UTC 2026



Steve Lhomme pushed to branch master at VideoLAN / VLC


Commits:
f256735b by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qml: fix focus management when clear button gets invisible in `SearchBox`

- - - - -
86533cd6 by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qml: make use of `Popup` in `SearchBox`

- - - - -
b79e46da by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qml: compress search pattern changes by default in `SearchBox`

- - - - -
3a67fb03 by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qml: add property `popBelow` in `SearchBox`

- - - - -
890317bf by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qml: expose the toggle button in `SearchBox`

- - - - -
53177ff4 by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qml: expose the text field in `SearchBox`

- - - - -
f60adff8 by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qml: expose the state in `SearchBox`

- - - - -
0fbdbb7f by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qml: add function `retract()` in `SearchBox`

- - - - -
7d0fd333 by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qml: adjust size of clear button depending on text field height in `SearchBox`

- - - - -
a7d14d9c by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qt: introduce icons `ic_fluent_text_change_case_24_regular` and `regex`

Also re-generates `VLCIcons.ttf`.

`regex` icon is taken from the bootstrap project.

- - - - -
425bf656 by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qml: introduce regular expression toggle button in `SearchBox`

- - - - -
c8a58b16 by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qml: introduce case sensitive toggle button in `SearchBox`

- - - - -
0a7648cf by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qml: do not use `OpacityAnimator` in add transition in `ListViewExt`

- - - - -
89ed376f by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qml: do not use the same `Transition` for multiple transitions in `ListViewExt`

- - - - -
6809dcf4 by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qt: register `QSortFilterProxyModel` as a qml type

- - - - -
421d097b by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qt: make `TitleRole` the display role in `PlaylistListModel`

- - - - -
baf35705 by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qt: register `PlaylistListModel::Roles` in meta-type system

So that we can refer the enumeration in QML.

- - - - -
fc09d5c4 by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qt: register `QAbstractProxyModel` as a qml type

- - - - -
b5f163ed by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qt: invalidate cache on model change in `ListSelectionModel`

- - - - -
86bdd751 by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qml: use the indexes provided in input item request handler in playlist drag item

- - - - -
30c77df8 by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qml: map the index if the view model is a proxy model in `PlaylistDelegate`

- - - - -
cf47cf55 by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qml: add required property `index` in `ListViewExt::VerticalDropAreaLayout`

- - - - -
f6f1419b by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qml: map the index if the view model is a proxy model in `ListViewExt::VerticalDropAreaLayout`

- - - - -
e03bffdd by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qml: map the index if the view model is a proxy model in footer drop area in `ListViewExt`

- - - - -
519b162e by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qt: add static method `VLCDuration::sum()`

Useful in QML where operators are not
applicable.

- - - - -
e0dd2bdb by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qml: use `ListView::count` instead of model count in `PlaylistPane`

`count` is a custom property and not part of abstract model api,
so it only works with custom models. In this case we could use
`rowCount()`, but that is not a property, so instead we just
use `ItemView::count` property (which also has notify signal,
similar to our custom `count`).

- - - - -
22a4e46c by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qml: add `SearchBox` to `PlaylistToolbar`

- - - - -
5ae2111b by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qml: use proxy model and enable filtering in `PlaylistPane`

The proxy model is instantiated and used once filtering is
active, and it is not unused afterward if filtering is no
longer active.

- - - - -
5af7a6d1 by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qml: map the selected indexes if the view model is a proxy model in `PlaylistPane`

- - - - -
570e8851 by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qt: register `QMessageBox` as an uncreatable qml type

- - - - -
d0963256 by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qt: add `DialogsProvider::messageDialog()`

- - - - -
6f3de0cc by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qt: add "Discard the filtered out items" action in playlist context menu

Only relevant when the view uses a proxy model.

- - - - -
ae61d611 by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qt: introduce `FilterRole` in `PlaylistListModel`

- - - - -
7ef76718 by Fatih Uzunoglu at 2026-08-03T14:09:40+00:00
qml: use `FilterRole` for filtering in `PlaylistPane`

... rather than the display role, which only considers
the title.

- - - - -


23 changed files:

- modules/gui/qt/Makefile.am
- modules/gui/qt/dialogs/dialogs_provider.cpp
- modules/gui/qt/dialogs/dialogs_provider.hpp
- modules/gui/qt/maininterface/mainui.cpp
- modules/gui/qt/maininterface/mainui.hpp
- modules/gui/qt/menus/qml_menu_wrapper.cpp
- modules/gui/qt/menus/qml_menu_wrapper.hpp
- modules/gui/qt/pixmaps/LICENCE
- modules/gui/qt/pixmaps/VLCIcons.json
- modules/gui/qt/pixmaps/VLCIcons.ttf
- + modules/gui/qt/pixmaps/icons/ic_fluent_text_change_case_24_regular.svg
- + modules/gui/qt/pixmaps/icons/regex.svg
- modules/gui/qt/playlist/playlist_model.cpp
- modules/gui/qt/playlist/playlist_model.hpp
- modules/gui/qt/playlist/qml/PlaylistDelegate.qml
- modules/gui/qt/playlist/qml/PlaylistPane.qml
- modules/gui/qt/playlist/qml/PlaylistToolbar.qml
- modules/gui/qt/style/vlcicons.hpp
- modules/gui/qt/util/list_selection_model.cpp
- modules/gui/qt/util/vlctick.hpp
- modules/gui/qt/widgets/qml/ListViewExt.qml
- modules/gui/qt/widgets/qml/SearchBox.qml
- modules/gui/qt/widgets/qml/TableViewDelegateExt.qml


Changes:

=====================================
modules/gui/qt/Makefile.am
=====================================
@@ -892,6 +892,8 @@ libqt_plugin_la_RES = \
 	pixmaps/icons/window_restore.svg \
 	pixmaps/icons/ic_fluent_arrow_download_24_regular.svg \
 	pixmaps/icons/ic_fluent_arrow_sync_24_regular.svg \
+	pixmaps/icons/regex.svg \
+	pixmaps/icons/ic_fluent_text_change_case_24_regular.svg \
 	pixmaps/menu/add.svg \
 	pixmaps/menu/capture-card.svg \
 	pixmaps/menu/clear.svg \


=====================================
modules/gui/qt/dialogs/dialogs_provider.cpp
=====================================
@@ -494,6 +494,33 @@ void DialogsProvider::mediaCodecDialog()
         m_mediaInfoDialog->hide();
 }
 
+QMessageBox::StandardButton DialogsProvider::messageDialog(const QString &text,
+                                                           const QString &title,
+                                                           const QMessageBox::StandardButtons buttons,
+                                                           const QMessageBox::StandardButton defaultButton,
+                                                           const QMessageBox::Icon icon)
+{
+    QMessageBox messageBox;
+
+    messageBox.setText(text);
+
+    if (!title.isEmpty())
+        messageBox.setWindowTitle(title);
+
+    if (icon != QMessageBox::NoIcon)
+        messageBox.setIcon(icon);
+
+    if (buttons != QMessageBox::NoButton)
+        messageBox.setStandardButtons(buttons);
+
+    if (defaultButton != QMessageBox::NoButton)
+        messageBox.setDefaultButton(defaultButton);
+
+    QVLCDialog::setWindowTransientParent(&messageBox, nullptr, p_intf);
+
+    return static_cast<QMessageBox::StandardButton>(messageBox.exec());
+}
+
 void DialogsProvider::playlistsDialog( const QVariantList & medias, MLPlaylistListModel::PlaylistType type )
 {
     const auto dialog = new PlaylistsDialog(p_intf, medias, type);


=====================================
modules/gui/qt/dialogs/dialogs_provider.hpp
=====================================
@@ -44,6 +44,7 @@
 
 #include <QObject>
 #include <QStringList>
+#include <QMessageBox>
 
 #include <vlc_es.h>
 
@@ -209,6 +210,11 @@ public slots:
     void mediaInfoDialog( const MLItemId& itemId );
     void mediaCodecDialog();
     bool questionDialog(const QString& text, const QString& title = {}) const;
+    QMessageBox::StandardButton messageDialog(const QString& text,
+                                              const QString& title = {},
+                                              const QMessageBox::StandardButtons buttons = QMessageBox::NoButton,
+                                              const QMessageBox::StandardButton defaultButton = QMessageBox::NoButton,
+                                              const QMessageBox::Icon icon = QMessageBox::NoIcon);
     void prefsDialog();
     void firstRunDialog();
     void extendedDialog();


=====================================
modules/gui/qt/maininterface/mainui.cpp
=====================================
@@ -93,6 +93,8 @@
 #include "mainctx_submodels.hpp"
 
 #include <QScreen>
+#include <QAbstractProxyModel>
+#include <QMessageBox>
 
 static bool g_qmlTypesAreRegistered = false;
 
@@ -240,6 +242,7 @@ void MainUI::registerQMLTypes()
         qmlRegisterSingletonInstance<UINotifier>(uri, versionMajor, versionMinor, "UINotifier", new UINotifier(m_mainCtx, m_mainCtx));
         qmlRegisterSingletonInstance<NavigationHistory>(uri, versionMajor, versionMinor, "History", new NavigationHistory(this));
         qmlRegisterUncreatableType<QAbstractItemModel>(uri, versionMajor, versionMinor, "QtAbstractItemModel", "");
+        qmlRegisterUncreatableType<QAbstractProxyModel>(uri, versionMajor, versionMinor, "QtAbstractProxyModel", "");
         qmlRegisterUncreatableType<QWindow>(uri, versionMajor, versionMinor, "QtWindow", "");
         qmlRegisterUncreatableType<QScreen>(uri, versionMajor, versionMinor, "QtScreen", "");
         qmlRegisterType<VideoSurface>(uri, versionMajor, versionMinor, "VideoSurface");
@@ -250,6 +253,8 @@ void MainUI::registerQMLTypes()
         qmlRegisterTypesAndRevisions<CSDMenu>( uri, versionMajor);
         qmlRegisterUncreatableType<NavigationAttached>( uri, versionMajor, versionMinor, "Navigation", "Navigation is only available via attached properties");
         qmlRegisterTypesAndRevisions<AboutModel>( uri, versionMajor );
+        qmlRegisterTypesAndRevisions<QSortFilterProxyModelForeign>( uri, versionMajor );
+        qmlRegisterUncreatableType<QMessageBox>( uri, versionMajor, versionMinor, "QtMessageBox", "QtMessageBox is uncreatable, use DialogsProvider instead." );
 #ifdef UPDATE_CHECK
         qmlRegisterSingletonInstance<UpdateModel>( uri, versionMajor, versionMinor, "UpdateModel", m_mainCtx->getUpdateModel() );
 #endif


=====================================
modules/gui/qt/maininterface/mainui.hpp
=====================================
@@ -11,6 +11,7 @@
 #include <QQmlEngine>
 #include <QQmlError>
 #include <QQuickItem>
+#include <QSortFilterProxyModel>
 
 class VLCVarChoiceModel;
 
@@ -43,4 +44,11 @@ private:
     QPointer<QQmlEngine> m_engineBound;
 };
 
+struct QSortFilterProxyModelForeign
+{
+    Q_GADGET
+    QML_FOREIGN(QSortFilterProxyModel)
+    QML_NAMED_ELEMENT(QtSortFilterProxyModel)
+};
+
 #endif // MAINUI_HPP


=====================================
modules/gui/qt/menus/qml_menu_wrapper.cpp
=====================================
@@ -34,6 +34,7 @@
 #include <QSignalMapper>
 #include <QScreen>
 #include <QActionGroup>
+#include <QAbstractProxyModel>
 
 namespace
 {
@@ -1196,6 +1197,50 @@ void PlaylistContextMenu::popup(int selectedIndex, QPoint pos )
             m_controler->shuffle();
         });
 
+        if (m_proxyModel)
+        {
+            if (m_proxyModel->rowCount() != m_model->rowCount())
+            {
+                m_menu->addSeparator();
+
+                // Actions that are available when the view uses a proxy model:
+
+                action = m_menu->addAction( qtr("Discard the filtered out items"));
+                connect(action, &QAction::triggered, [this]( ) {
+                    bool proceed = true;
+
+                    if (const auto dp = DialogsProvider::getInstance())
+                    {
+                        proceed = dp->questionDialog(QStringLiteral("Discard the filtered out items"),
+                                                     QStringLiteral("Do you really want to discard the items that are not displayed at the moment?"));
+                    }
+
+                    const auto count = m_model->rowCount() - m_proxyModel->rowCount();
+
+                    if (proceed && (count > 0))
+                    {
+                        QList<int> items;
+                        items.reserve(count);
+
+                        for (int i = 0; i < m_model->rowCount(); ++i)
+                        {
+                            const auto mappedIndex = m_proxyModel->mapFromSource(m_model->index(i, 0));
+
+                            if (!mappedIndex.isValid() || !(m_proxyModel->checkIndex(mappedIndex, QAbstractItemModel::CheckIndexOption::IndexIsValid)))
+                            {
+                                items.push_back(i);
+                            }
+                        }
+
+                        if (items.count() > 0)
+                        {
+                            m_model->removeItems(items);
+                            emit discardedFilteredOutItems();
+                        }
+                    }
+                });
+            }
+        }
     }
 
     m_menu->popup(pos);


=====================================
modules/gui/qt/menus/qml_menu_wrapper.hpp
=====================================
@@ -34,6 +34,9 @@ Q_MOC_INCLUDE("network/networkdevicemodel.hpp")
 Q_MOC_INCLUDE("network/networkmediamodel.hpp")
 Q_MOC_INCLUDE("medialibrary/mlplaylistlistmodel.hpp")
 Q_MOC_INCLUDE("medialibrary/mlplaylistmodel.hpp")
+Q_MOC_INCLUDE(<QAbstractProxyModel>)
+
+class QAbstractProxyModel;
 
 class MediaLib;
 class MLPlaylistListModel;
@@ -463,6 +466,7 @@ public slots:
 class PlaylistContextMenu : public QObject {
     Q_OBJECT
     SIMPLE_MENU_PROPERTY(vlc::playlist::PlaylistListModel*, model, nullptr)
+    SIMPLE_MENU_PROPERTY(QAbstractProxyModel*, proxyModel, nullptr)
     SIMPLE_MENU_PROPERTY(vlc::playlist::PlaylistController*, controler, nullptr)
     SIMPLE_MENU_PROPERTY(ListSelectionModel*, selectionModel, nullptr)
     SIMPLE_MENU_PROPERTY(MainCtx *, ctx, nullptr)
@@ -471,6 +475,7 @@ public:
 
 signals:
     void jumpToCurrentPlaying();
+    void discardedFilteredOutItems();
 
 public slots:
     void popup(int currentIndex, QPoint pos );


=====================================
modules/gui/qt/pixmaps/LICENCE
=====================================
@@ -1,6 +1,7 @@
 MIT License
 
 Copyright (c) 2020 Microsoft Corporation
+Copyright (c) 2019-2024 The Bootstrap Authors
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal


=====================================
modules/gui/qt/pixmaps/VLCIcons.json
=====================================
@@ -98,6 +98,8 @@
         {"key":"ic_fluent_arrow_download_24_regular", "path": "./icons/ic_fluent_arrow_download_24_regular.svg"},
         {"key":"ic_fluent_arrow_sync_24_regular", "path": "./icons/ic_fluent_arrow_sync_24_regular.svg"},
         {"key":"medialibrary_add", "path": "./icons/ic_medialibrary_add.svg"},
-        {"key":"medialibrary_remove", "path": "./icons/ic_medialibrary_remove.svg"}
+        {"key":"medialibrary_remove", "path": "./icons/ic_medialibrary_remove.svg"},
+        {"key":"regex", "path": "./icons/regex.svg"},
+        {"key":"ic_fluent_text_change_case_24_regular", "path": "./icons/ic_fluent_text_change_case_24_regular.svg"}
     ]
 }


=====================================
modules/gui/qt/pixmaps/VLCIcons.ttf
=====================================
Binary files a/modules/gui/qt/pixmaps/VLCIcons.ttf and b/modules/gui/qt/pixmaps/VLCIcons.ttf differ


=====================================
modules/gui/qt/pixmaps/icons/ic_fluent_text_change_case_24_regular.svg
=====================================
@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M16.5106 3.00008C16.8242 3.00452 17.1019 3.20362 17.2068 3.49919L22.7068 18.9992C22.8453 19.3896 22.6411 19.8183 22.2508 19.9568C21.8604 20.0953 21.4317 19.8912 21.2931 19.5008L19.8735 15.5L12.7558 15.5L11.1994 19.5207C11.0499 19.907 10.6155 20.099 10.2292 19.9494C9.84293 19.7999 9.651 19.3655 9.80053 18.9793L15.8005 3.47925C15.9137 3.18678 16.197 2.99563 16.5106 3.00008ZM16.4694 5.90659L13.3364 14L19.3412 14L16.4694 5.90659ZM5.50005 10.5014L5.78725 10.5113C7.74783 10.6088 8.91466 11.7373 8.9955 13.5555L9 13.7604V19.2604C9 19.6401 8.71785 19.9539 8.35177 20.0035L8.25 20.0104C7.8703 20.0104 7.55651 19.7282 7.50685 19.3621L7.5 19.2604L7.49865 19.156C6.51108 19.7214 5.59755 20.0104 4.75 20.0104C2.91158 20.0104 1.5 18.7159 1.5 16.7604C1.5 15.036 2.68843 13.7549 4.6597 13.5158C5.592 13.4028 6.53953 13.4736 7.49853 13.7255C7.4887 12.6184 6.94255 12.0706 5.71275 12.0095C4.75051 11.9616 4.07429 12.0967 3.67825 12.3744C3.33911 12.6123 2.87139 12.5301 2.63357 12.191C2.39576 11.8518 2.47789 11.3841 2.81703 11.1463C3.47484 10.685 4.37072 10.4807 5.50005 10.5014ZM7.499 15.3241L7.19613 15.2338C6.38875 15.0117 5.60483 14.9486 4.8403 15.0413C3.60791 15.1908 3 15.8461 3 16.7967C3 17.8934 3.7124 18.5467 4.75 18.5467C5.43032 18.5467 6.26848 18.2324 7.25129 17.583L7.499 17.4141V15.3241Z" fill="#212121"/>
+</svg>


=====================================
modules/gui/qt/pixmaps/icons/regex.svg
=====================================
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-regex" viewBox="0 0 16 16">
+  <path fill-rule="evenodd" d="M3.05 3.05a7 7 0 0 0 0 9.9.5.5 0 0 1-.707.707 8 8 0 0 1 0-11.314.5.5 0 1 1 .707.707m9.9-.707a.5.5 0 0 1 .707 0 8 8 0 0 1 0 11.314.5.5 0 0 1-.707-.707 7 7 0 0 0 0-9.9.5.5 0 0 1 0-.707M6 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0m5-6.5a.5.5 0 0 0-1 0v2.117L8.257 5.57a.5.5 0 0 0-.514.858L9.528 7.5 7.743 8.571a.5.5 0 1 0 .514.858L10 8.383V10.5a.5.5 0 1 0 1 0V8.383l1.743 1.046a.5.5 0 0 0 .514-.858L11.472 7.5l1.785-1.071a.5.5 0 1 0-.514-.858L11 6.617z"/>
+</svg>
\ No newline at end of file


=====================================
modules/gui/qt/playlist/playlist_model.cpp
=====================================
@@ -286,6 +286,7 @@ QHash<int, QByteArray>
 PlaylistListModel::roleNames() const
 {
     return {
+        { FilterRole, QByteArrayLiteral("filter") },
         { PreparsedRole, QByteArrayLiteral("preparsed") },
         { TitleRole, QByteArrayLiteral("title") },
         { DurationRole, QByteArrayLiteral("duration") },
@@ -484,6 +485,12 @@ PlaylistListModel::data(const QModelIndex &index, int role) const
 
     switch (role)
     {
+    case FilterRole:
+        return QVariant::fromValue<QString>(data(index, TitleRole).toString() +
+                                            QStringLiteral("\n") +
+                                            data(index, ArtistRole).toString() +
+                                            QStringLiteral("\n") +
+                                            data(index, AlbumRole).toString());
     case TitleRole:
         return d->m_items[row].getTitle();
     case IsCurrentRole:


=====================================
modules/gui/qt/playlist/playlist_model.hpp
=====================================
@@ -44,7 +44,8 @@ class PlaylistListModel : public QAbstractListModel
 public:
     enum Roles
     {
-        TitleRole = Qt::UserRole,
+        TitleRole = Qt::DisplayRole,
+        FilterRole = Qt::UserRole,
         DurationRole,
         IsCurrentRole,
         ArtistRole,
@@ -53,6 +54,7 @@ public:
         UrlRole,
         PreparsedRole
     };
+    Q_ENUM(Roles);
 
     PlaylistListModel(QObject *parent = nullptr);
     PlaylistListModel(vlc_playlist_t *playlist, QObject *parent = nullptr);


=====================================
modules/gui/qt/playlist/qml/PlaylistDelegate.qml
=====================================
@@ -109,12 +109,19 @@ T.Control {
 
     Keys.onMenuPressed: {
         if (contextMenu) {
-            contextMenu.popup(index, delegate.mapToGlobal(0, delegate.height))
+            contextMenu.popup(delegate.mappedIndex(), delegate.mapToGlobal(0, delegate.height))
         }
     }
 
     // Functions
 
+    function mappedIndex() {
+        if (delegate.view?.model instanceof QtAbstractProxyModel)
+            return delegate.view.model.mapToSource(delegate.view.model.index(delegate.index, 0))?.row
+        else
+            return delegate.index
+    }
+
     // Childs
 
     readonly property ColorContext colorContext: ColorContext {
@@ -324,12 +331,13 @@ T.Control {
                 }
 
                 if (contextMenu && button === Qt.RightButton)
-                    contextMenu.popup(index, eventPoint.globalPosition)
+                    contextMenu.popup(delegate.mappedIndex(), eventPoint.globalPosition)
             }
 
             onDoubleTapped: (eventPoint, button) => {
-                if (button !== Qt.RightButton)
-                    MainPlaylistController.goTo(index, true)
+                if (button !== Qt.RightButton) {
+                    MainPlaylistController.goTo(delegate.mappedIndex(), true)
+                }
             }
 
             Component.onCompleted: {
@@ -358,7 +366,23 @@ T.Control {
                             view.selectionModel.select(index, ItemSelectionModel.ClearAndSelect)
                         }
 
-                        dragItem.indexes = view.selectionModel.selectedIndexesFlat
+                        let items
+
+                        const selectedIndexes = view.selectionModel.selectedIndexesFlat
+                        if (view.model instanceof QtAbstractProxyModel) {
+                            items = []
+                            for (let i = 0; i < selectedIndexes.length; ++i) {
+                                const mappedIndex = view.model.mapToSource(view.model.index(selectedIndexes[i], 0))
+                                items.push(mappedIndex.row)
+                            }
+                        } else {
+                            items = selectedIndexes
+                        }
+
+                        if (items.length <= 0)
+                            return
+
+                        dragItem.indexes = items
                         dragItem.indexesFlat = true
                         dragItem.Drag.active = true
                     } else {
@@ -381,7 +405,7 @@ T.Control {
             }
 
             onContextMenuRequested: (index, globalPoint) => {
-                delegate.contextMenu.popup(index, globalPoint)
+                delegate.contextMenu.popup(delegate.mappedIndex(), globalPoint)
             }
         }
     }
@@ -390,6 +414,7 @@ T.Control {
         id: dropAreaLayout
         anchors.fill: parent
 
+        index: delegate.index
         view: delegate.view
     }
 }


=====================================
modules/gui/qt/playlist/qml/PlaylistPane.qml
=====================================
@@ -89,13 +89,15 @@ T.Pane {
         }
 
         onRequestInputItems: (indexes, data, resolve, reject) => {
-            resolve(root.model.getItemsForIndexes(root.selectionModel.selectedIndexesFlat))
+            resolve(root.model.getItemsForIndexes((indexes?.length > 0) ? indexes : root.selectionModel.selectedIndexesFlat))
         }
     }
 
     PlaylistContextMenu {
         id: contextMenu
         model: root.model
+        proxyModel: (root.contentItem?.listView?.model instanceof QtAbstractProxyModel) ? root.contentItem.listView.model
+                                                                                        : null
         selectionModel: root.selectionModel
         controler: MainPlaylistController
         ctx: MainCtx
@@ -131,8 +133,29 @@ T.Pane {
 
             Widgets.CaptionLabel {
                 color: theme.fg.secondary
-                visible: model.count !== 0
-                text: qsTr("%1 elements, %2").arg(model.count).arg(model.duration.formatLong())
+                visible: listView.count !== 0
+                text: {
+                    if (listView.count <= 0)
+                        return "" // Not visible otherwise
+
+                    let duration
+                    const model = listView.model
+                    if (model instanceof PlaylistListModel) {
+                        duration = model.duration
+                    } else {
+                        // Proxy model, we need to use `QAbstractItemModel` api:
+
+                        duration = []
+                        for (let i = 0; i < listView.count; ++i) {
+                            const itemDuration = model.data(model.index(i, 0), PlaylistListModel.DurationRole)
+                            console.assert(itemDuration !== undefined && itemDuration !== null) // typeof `vlcDuration`
+                            duration.push(itemDuration)
+                        }
+                        duration = duration[0].sum(duration)
+                    }
+
+                    return qsTr("%1 elements, %2").arg(listView.count).arg(duration.formatLong())
+                }
             }
         }
 
@@ -143,7 +166,7 @@ T.Pane {
         }
 
         RowLayout {
-            visible: model.count !== 0
+            visible: listView.count !== 0
 
             Layout.fillHeight: false
             Layout.leftMargin: VLCStyle.margin_normal
@@ -220,14 +243,69 @@ T.Pane {
 
             clip: !fadingEdge.implicitClipping && (height < contentHeight)
 
-            model: root.model
+            model: proxyModel ?? root.model
+
+            onWidthChanged: {
+                if (toolbar.searchBox.expanded) {
+                    if (width < toolbar.searchBox.textField.width)
+                        toolbar.searchBox.retract()
+                }
+            }
+
+            property QtSortFilterProxyModel proxyModel
+
+            function adjustFiltering() {
+                // Once text changes, we switch to the proxy model.
+                // We do not switch back to the source model after that.
+                if (!proxyModel) {
+                    proxyModel = proxyModelComponent.createObject(listView)
+                }
+
+                if (toolbar.searchBox.regexButtonToggled)
+                    proxyModel.setFilterRegularExpression(toolbar.searchBox.searchPattern)
+                else
+                    proxyModel.setFilterFixedString(toolbar.searchBox.searchPattern)
+            }
+
+            Component {
+                id: proxyModelComponent
+
+                QtSortFilterProxyModel {
+                    filterRole: PlaylistListModel.FilterRole
+
+                    sourceModel: root.model
+
+                    filterCaseSensitivity: toolbar.searchBox.caseSensitiveButtonToggled ? Qt.CaseSensitive
+                                                                                        : Qt.CaseInsensitive
+                }
+            }
 
             fadingEdge.backgroundColor: (root.background && (root.background.color.a >= 1.0)) ? root.background.color
                                                                                               : Qt.alpha(root.background.color, 0.0)
 
+            function mappedSelectedIndexes() : var {
+                let mappedSelectedIndexes
+
+                if (!listView.selectionModel)
+                    return [] // unlikely
+
+                const selectedIndexes = listView.selectionModel.selectedIndexesFlat
+                if (listView.proxyModel && listView.model === listView.proxyModel) {
+                    mappedSelectedIndexes = []
+                    for (let i = 0; i < selectedIndexes.length; ++i) {
+                        const mappedIndex = listView.model.mapToSource(listView.model.index(selectedIndexes[i], 0))
+                        mappedSelectedIndexes.push(mappedIndex.row)
+                    }
+                } else {
+                    mappedSelectedIndexes = selectedIndexes
+                }
+
+                return mappedSelectedIndexes
+            }
+
             isDropAcceptableFunc: function(drop, index) {
                 if (drop.source === dragItem)
-                    return Helpers.itemsMovable(selectionModel.sortedSelectedIndexesFlat, index)
+                    return Helpers.itemsMovable(listView.mappedSelectedIndexes(), index)
                 else if (Helpers.isValidInstanceOf(drop.source, Widgets.DragItem))
                     return true
                 else if (drop.hasUrls)
@@ -241,7 +319,7 @@ T.Pane {
 
                 // NOTE: Move implementation.
                 if (dragItem === item) {
-                    model.moveItemsPre(root.selectionModel.sortedSelectedIndexesFlat, index);
+                    root.model.moveItemsPre(listView.mappedSelectedIndexes(), index);
                     listView.forceActiveFocus();
                 // NOTE: Dropping medialibrary content into the queue.
                 } else if (Helpers.isValidInstanceOf(item, Widgets.DragItem)) {
@@ -305,6 +383,9 @@ T.Pane {
                         contentYBehavior.enabled = false
                     }
                 })
+
+                toolbar.searchBox.searchPatternChanged.connect(listView, listView.adjustFiltering)
+                toolbar.searchBox.regexButtonToggledChanged.connect(listView, listView.adjustFiltering)
             }
 
             Connections {
@@ -316,7 +397,7 @@ T.Pane {
                 }
 
                 function onModelReset() {
-                    if (listView.currentIndex === -1 && root.model.count > 0)
+                    if (listView.currentIndex === -1 && listView.count > 0)
                         listView.currentIndex = 0
                 }
             }
@@ -344,7 +425,23 @@ T.Pane {
                 }
             }
 
-            Keys.onDeletePressed: model.removeItems(selectionModel.selectedIndexesFlat)
+            Keys.onDeletePressed: {
+                let items
+
+                const selectedIndexes = selectionModel.selectedIndexesFlat
+                if (listView.proxyModel && listView.model === listView.proxyModel) {
+                    items = []
+                    for (let i = 0; i < selectedIndexes.length; ++i) {
+                        const mappedIndex = listView.proxyModel.mapToSource(listView.proxyModel.index(selectedIndexes[i], 0))
+                        items.push(mappedIndex.row)
+                    }
+                } else {
+                    items = selectedIndexes
+                }
+
+                if (items.length > 0)
+                    root.model.removeItems(items)
+            }
 
             Navigation.parentItem: root
 
@@ -367,7 +464,7 @@ T.Pane {
 
                 Binding on visible {
                     delayed: true
-                    value: (listView.model.count === 0 && !listView.footerItem.firstItemIndicatorVisible)
+                    value: (listView.count === 0 && !listView.footerItem.firstItemIndicatorVisible)
                 }
 
                 Widgets.IconLabel {
@@ -393,7 +490,10 @@ T.Pane {
                     horizontalAlignment: Text.AlignHCenter
                     verticalAlignment: Text.AlignVCenter
 
-                    text: qsTr("No content yet")
+                    text: (listView.proxyModel &&
+                           listView.model === listView.proxyModel &&
+                           listView.count !== root.model.count) ? qsTr("No results")
+                                                                : qsTr("No content yet")
 
                     color: label.color
 
@@ -407,6 +507,8 @@ T.Pane {
                     verticalAlignment: Text.AlignVCenter
 
                     text: qsTr("Drag & Drop some content here!")
+                    visible: (listView.proxyModel && listView.model === listView.proxyModel) ? (listView.count === root.model.count)
+                                                                                             : true
 
                     color: label.color
 
@@ -423,6 +525,17 @@ T.Pane {
             Layout.fillWidth: true
             Layout.leftMargin: VLCStyle.margin_normal
             Layout.rightMargin: VLCStyle.margin_normal
+
+            searchBox.displayRegexToggleButton: true
+            searchBox.displayCaseSensitiveToggleButton: true
+
+            Connections {
+                target: contextMenu
+
+                function onDiscardedFilteredOutItems() {
+                    toolbar.searchBox.retract()
+                }
+            }
         }
     }
 


=====================================
modules/gui/qt/playlist/qml/PlaylistToolbar.qml
=====================================
@@ -34,6 +34,8 @@ RowLayout {
 
     spacing: VLCStyle.margin_normal
 
+    property alias searchBox: searchBox
+
     Accessible.role: Accessible.ToolBar
 
     Item {
@@ -162,4 +164,27 @@ RowLayout {
             onClicked: MainPlaylistController.clear()
         }
     }
+
+    Item {
+        Layout.fillWidth: true
+        Layout.fillHeight: true
+
+        implicitWidth: searchBox.implicitWidth
+        implicitHeight: searchBox.implicitHeight
+
+        Widgets.SearchBox {
+            id: searchBox
+
+            anchors.centerIn: parent
+
+            enabled: MainPlaylistController.count > 1
+
+            toggleButton.font.pixelSize: VLCStyle.icon_playlist
+
+            popBelow: false
+
+            popup.width: Math.max(popup.implicitWidth, rowLayout.width * 0.6)
+            popup.bottomPadding: VLCStyle.margin_xxsmall
+        }
+    }
 }


=====================================
modules/gui/qt/style/vlcicons.hpp
=====================================
@@ -108,6 +108,8 @@ class VLCIcons : public QObject {
     Q_PROPERTY(QChar ic_fluent_arrow_sync_24_regular MEMBER ic_fluent_arrow_sync_24_regular CONSTANT FINAL)
     Q_PROPERTY(QChar medialibrary_add MEMBER medialibrary_add CONSTANT FINAL)
     Q_PROPERTY(QChar medialibrary_remove MEMBER medialibrary_remove CONSTANT FINAL)
+    Q_PROPERTY(QChar regex MEMBER regex CONSTANT FINAL)
+    Q_PROPERTY(QChar ic_fluent_text_change_case_24_regular MEMBER ic_fluent_text_change_case_24_regular CONSTANT FINAL)
 
 
 public:
@@ -212,6 +214,8 @@ public:
     static constexpr inline QChar ic_fluent_arrow_sync_24_regular{ u'\ue05b' };
     static constexpr inline QChar medialibrary_add{ u'\ue05c' };
     static constexpr inline QChar medialibrary_remove{ u'\ue05d' };
+    static constexpr inline QChar regex{ u'\ue05e' };
+    static constexpr inline QChar ic_fluent_text_change_case_24_regular{ u'\ue05f' };
 
 };
 


=====================================
modules/gui/qt/util/list_selection_model.cpp
=====================================
@@ -40,6 +40,8 @@ ListSelectionModel::ListSelectionModel(QAbstractItemModel *model, QObject *paren
             connect(model, &QAbstractItemModel::layoutChanged, this, &ListSelectionModel::invalidateCache);
             connect(model, &QAbstractItemModel::modelReset, this, &ListSelectionModel::invalidateCache);
         }
+
+        invalidateCache();
     });
 
     connect(this, &QItemSelectionModel::selectionChanged, this, &ListSelectionModel::invalidateCache);


=====================================
modules/gui/qt/util/vlctick.hpp
=====================================
@@ -103,6 +103,11 @@ public:
     VLCDuration();
     VLCDuration(vlc_tick_t);
 
+    Q_INVOKABLE static VLCDuration sum(const QList<VLCDuration>& durations)
+    {
+        return std::reduce(durations.constBegin(), durations.constEnd());
+    }
+
     VLCDuration operator*(double) const;
     VLCDuration operator+(const VLCDuration &) const;
     VLCDuration& operator+=(const VLCDuration &);


=====================================
modules/gui/qt/widgets/qml/ListViewExt.qml
=====================================
@@ -126,8 +126,12 @@ ListView {
     }
 
     component VerticalDropAreaLayout : ColumnLayout {
+        id: verticalDropAreaLayout
+
         spacing: 0
 
+        required property int index
+
         property alias higherDropArea: higherDropArea
         property alias lowerDropArea: lowerDropArea
 
@@ -163,6 +167,13 @@ ListView {
             }
         }
 
+        function mappedIndex() {
+            if (view?.model instanceof QtAbstractProxyModel)
+                return view.model.mapToSource(view.model.index(index, 0)).row
+            else
+                return index
+        }
+
         // NOTE: Nested inline components are not supported in QML as of Qt 6.8
 
         DropArea {
@@ -177,7 +188,7 @@ ListView {
                     return
                 }
 
-                if (isDropAcceptable && !isDropAcceptable(drag, index)) {
+                if (isDropAcceptable && !isDropAcceptable(drag, verticalDropAreaLayout.mappedIndex())) {
                     drag.accepted = false
                     return
                 }
@@ -185,7 +196,7 @@ ListView {
 
             onDropped: (drop) => {
                 console.assert(acceptDrop)
-                commonDrop(index, drop)
+                commonDrop(verticalDropAreaLayout.mappedIndex(), drop)
             }
         }
 
@@ -201,7 +212,7 @@ ListView {
                     return
                 }
 
-                if (isDropAcceptable && !isDropAcceptable(drag, index + 1)) {
+                if (isDropAcceptable && !isDropAcceptable(drag, verticalDropAreaLayout.mappedIndex() + 1)) {
                     drag.accepted = false
                     return
                 }
@@ -209,7 +220,7 @@ ListView {
 
             onDropped: (drop) => {
                 console.assert(acceptDrop)
-                commonDrop(index + 1, drop)
+                commonDrop(verticalDropAreaLayout.mappedIndex() + 1, drop)
             }
         }
     }
@@ -230,7 +241,7 @@ ListView {
             readonly property bool bottomContainsDrag: false
 
             onContainsDragChanged: {
-                if (root.model.count > 0) {
+                if (root.count > 0) {
                     root.updateItemContainsDrag(this, containsDrag)
                 } else if (!containsDrag && root.itemContainsDrag === this) {
                     // In case model count is changed somehow while
@@ -253,7 +264,7 @@ ListView {
 
                 color: "transparent"
 
-                visible: (root.model.count === 0 && (dropArea.containsDrag || dropArea.dropOperationOngoing))
+                visible: (root.count === 0 && (dropArea.containsDrag || dropArea.dropOperationOngoing))
 
                 opacity: 0.8
 
@@ -295,7 +306,12 @@ ListView {
                 onDropped: function(drop) {
                     console.assert(!!root.acceptDropFunc)
                     dropOperationOngoing = true
-                    root.acceptDropFunc(root.model.count, drop)
+                    let targetIndex
+                    if (root.model instanceof QtAbstractProxyModel)
+                        targetIndex = (root.model.mapToSource(root.model.index(root.count - 1, 0))?.row ?? 0) + 1
+                    else
+                        targetIndex = root.count
+                    root.acceptDropFunc(targetIndex, drop)
                         .then(() => { dropOperationOngoing = false })
                 }
             }
@@ -310,9 +326,9 @@ ListView {
         // does not have a "target" property, and wants a valid parent.
         enabled: !!root.acceptDropFunc
 
-        OpacityAnimator {
-            from: 0.0 // QTBUG-66475
-            to: 1.0
+        // WARNING: Do not use `OpacityAnimator` here, it is bugged.
+        NumberAnimation {
+            property: "opacity"
             duration: VLCStyle.duration_long
             easing.type: Easing.OutSine
         }
@@ -342,21 +358,29 @@ ListView {
     //          remove displaced seemingly are not affected from that
     //          issue. See QTBUG-131106, QTBUG-89158, ...
 
-    moveDisplaced: Transition {
+    component DefaultDisplacedTransition : Transition {
         // Transition is relevant when drag and drop is feasible.
         // Component approach can not be used here because `Transition`
         // does not have a "target" property, and wants a valid parent.
-        enabled: !!root.acceptDropFunc
+        enabled: !!view.acceptDropFunc
+
+        required property ListViewExt view
 
         NumberAnimation {
             // TODO: Use YAnimator >= Qt 6.0 (QTBUG-66475)
-            property: (root.orientation === ListView.Vertical) ? "y" : "x"
+            property: (view.orientation === ListView.Vertical) ? "y" : "x"
             duration: VLCStyle.duration_long
             easing.type: Easing.OutSine
         }
     }
 
-    removeDisplaced: moveDisplaced
+    moveDisplaced: DefaultDisplacedTransition {
+        view: root
+    }
+
+    removeDisplaced: DefaultDisplacedTransition {
+        view: root
+    }
 
     // Events
 


=====================================
modules/gui/qt/widgets/qml/SearchBox.qml
=====================================
@@ -17,6 +17,7 @@
  *****************************************************************************/
 import QtQuick
 import QtQuick.Controls
+import QtQuick.Templates as T
 import QtQuick.Layouts
 
 
@@ -30,9 +31,28 @@ FocusScope {
     implicitWidth: iconButton.implicitWidth
     implicitHeight: iconButton.implicitHeight
 
+    property alias popup: popup
     property real maxSearchFieldWidth: Number.MAX_VALUE
     property alias buttonWidth: iconButton.implicitWidth
-    property alias searchPattern: textField.text
+    property string searchPattern
+    property alias regexButtonToggled: regexToggleButton.checked
+    property alias caseSensitiveButtonToggled: caseSensitiveToggleButton.checked
+    property alias compressSearchPatternChanges: searchPatternBinding.delayed
+    compressSearchPatternChanges: true
+    property alias displayRegexToggleButton: regexToggleButton.visible
+    displayRegexToggleButton: false
+    property alias displayCaseSensitiveToggleButton: caseSensitiveToggleButton.visible
+    displayCaseSensitiveToggleButton: false
+
+    Binding on searchPattern {
+        id: searchPatternBinding
+        value: textField.text
+    }
+
+    property bool popBelow: true
+    property alias toggleButton: iconButton
+    property alias textField: textField
+    readonly property bool expanded: (expandedState.state === "expanded")
 
     // public functions
 
@@ -41,6 +61,10 @@ FocusScope {
         textField.forceActiveFocus(Qt.ShortcutFocusReason)
     }
 
+    function retract() {
+        expandedState.state = ""
+    }
+
     StateGroup {
         id: expandedState
 
@@ -51,8 +75,9 @@ FocusScope {
                 name: "expanded"
 
                 PropertyChanges {
-                    target: textField
-                    height:  textField.implicitHeight
+                    target: popup
+                    explicit: true
+                    height: popup.implicitHeight
                 }
 
                 PropertyChanges {
@@ -66,6 +91,10 @@ FocusScope {
                 PropertyChanges {
                     target: textField
                     text: ""
+                }
+
+                PropertyChanges {
+                    target: popup
                     height: 0.0
                 }
 
@@ -78,6 +107,8 @@ FocusScope {
         ]
 
         transitions: Transition {
+            id: transition
+
             from: ""; to: "expanded"
             reversible: true
 
@@ -109,107 +140,188 @@ FocusScope {
         focus: true
 
         Navigation.parentItem: root
-        Navigation.downItem: textField
+        Navigation.downItem: root.popBelow ? textField : null
+        Navigation.upItem: root.popBelow ? null : textField
 
         onClicked: {
             if (expandedState.state == "")
                 expandAndFocus()
             else
-                expandedState.state = ""
+                root.retract()
         }
-    }
 
-    TextFieldExt {
-        id: textField
+        T.Popup {
+            id: popup
 
-        property bool _keyPressed: false
+            x: -width + parent.width
+            y: root.popBelow ? parent.height : -height
 
-        anchors.top: iconButton.bottom
-        anchors.right: iconButton.right
+            implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
+                                    implicitContentWidth + leftPadding + rightPadding)
+            implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
+                                     implicitContentHeight + topPadding + bottomPadding)
 
-        implicitWidth: VLCStyle.widthSearchInput
-        height: 0
+            closePolicy: Popup.NoAutoClose
 
-        visible: (height > 0)
+            height: 0
+            visible: (height > 0)
 
-        padding: VLCStyle.dp(6)
-        leftPadding: padding + VLCStyle.dp(4)
-        rightPadding: (textField.width - clearButton.x)
+            onOpened: {
+                textField.forceActiveFocus(iconButton.focusReason)
+            }
 
-        radius: clearButton.radius
+            onClosed: {
+                textField.focus = false
+                focus = false
+                iconButton.focus = true
+            }
 
-        selectByMouse: true
+            contentItem: TextFieldExt {
+                id: textField
 
-        placeholderText: qsTr("filter")
+                property bool _keyPressed: false
 
-        Navigation.parentItem: root
-        Navigation.upItem: iconButton
-        Navigation.rightItem: clearButton.visible ? clearButton : null
-        Navigation.cancelAction: function() {
-            expandedState.state = ""
-            iconButton.focusReason = Qt.ShortcutFocusReason
-        }
+                colorContext.palette: theme.palette
 
-        Accessible.searchEdit: true
+                implicitWidth: VLCStyle.widthSearchInput
 
-        //ideally we should use Keys.onShortcutOverride but it doesn't
-        //work with TextField before 5.13 see QTBUG-68711
-        onActiveFocusChanged: {
-            if (activeFocus)
-                MainCtx.useGlobalShortcuts = false
-            else
-                MainCtx.useGlobalShortcuts = true
-        }
+                padding: VLCStyle.dp(6)
+                leftPadding: padding + VLCStyle.dp(4)
+                rightPadding: leftPadding + (insideRow.visible ? insideRow.width : 0.0)
 
-        Keys.priority: Keys.AfterItem
+                radius: clearButton.radius
 
-        Keys.onPressed: (event) => {
-            _keyPressed = true
+                selectByMouse: true
 
-            //we don't want Navigation.cancelAction to match Backspace
-            if (event.matches(StandardKey.Backspace))
-                event.accepted = true
+                placeholderText: qsTr("filter")
 
-            Navigation.defaultKeyAction(event)
-        }
+                Navigation.parentItem: root
+                Navigation.upItem: root.popBelow ? iconButton : null
+                Navigation.downItem: root.popBelow ? null : iconButton
+                Navigation.rightItem: caseSensitiveToggleButton.visible ? caseSensitiveToggleButton
+                                                                        : (regexToggleButton.visible ? regexToggleButton
+                                                                                                     : (clearButton.visible ? clearButton : null))
+                Navigation.cancelAction: function() {
+                    root.retract()
+                    iconButton.focusReason = Qt.ShortcutFocusReason
+                }
 
-        Keys.onReleased: (event) => {
-            if (_keyPressed === false)
-                return
+                Accessible.searchEdit: true
 
-            _keyPressed = false
+                //ideally we should use Keys.onShortcutOverride but it doesn't
+                //work with TextField before 5.13 see QTBUG-68711
+                onActiveFocusChanged: {
+                    if (activeFocus)
+                        MainCtx.useGlobalShortcuts = false
+                    else
+                        MainCtx.useGlobalShortcuts = true
+                }
 
-            //we don't want Navigation.cancelAction to match Backspace
-            if (event.matches(StandardKey.Backspace))
-                event.accepted = true
+                Keys.priority: Keys.AfterItem
 
-            Navigation.defaultKeyReleaseAction(event)
-        }
+                Keys.onPressed: (event) => {
+                    _keyPressed = true
 
-        Widgets.IconToolButton {
-            id: clearButton
+                    //we don't want Navigation.cancelAction to match Backspace
+                    if (event.matches(StandardKey.Backspace))
+                        event.accepted = true
 
-            anchors.verticalCenter: parent.verticalCenter
-            anchors.right: parent.right
-            anchors.rightMargin: VLCStyle.margin_xxsmall
+                    Navigation.defaultKeyAction(event)
+                }
 
-            font.pixelSize: VLCStyle.icon_banner
-            text: VLCIcons.close
+                Keys.onReleased: (event) => {
+                    if (_keyPressed === false)
+                        return
 
-            description: qsTr("Clear")
+                    _keyPressed = false
 
-            visible: (textField.text.length > 0)
+                    //we don't want Navigation.cancelAction to match Backspace
+                    if (event.matches(StandardKey.Backspace))
+                        event.accepted = true
 
-            onVisibleChanged: {
-                if (!visible && parent.visible) {
-                    parent.focus = true
+                    Navigation.defaultKeyReleaseAction(event)
                 }
-            }
-            onClicked: textField.clear()
 
-            Navigation.parentItem: textField
-            Navigation.leftItem: textField
+                Row {
+                    id: insideRow
+
+                    anchors.verticalCenter: parent.verticalCenter
+                    anchors.right: parent.right
+                    anchors.rightMargin: VLCStyle.margin_xxsmall
+
+                    spacing: VLCStyle.margin_xxsmall
+
+                    visible: parent.height > height
+
+                    Widgets.IconToolButton {
+                        id: caseSensitiveToggleButton
+
+                        anchors.verticalCenter: parent.verticalCenter
+
+                        // Animating font properties with native rendering is probably not a good idea:
+                        font.pixelSize: 0.6 * (contentItem.renderType === Text.QtRendering ? textField.height
+                                                                                           : textField.implicitHeight)
+                        text: VLCIcons.ic_fluent_text_change_case_24_regular
+
+                        description: qsTr("Case sensitive")
+
+                        onClicked: {
+                            checked = !checked
+                        }
+
+                        Navigation.parentItem: textField
+                        Navigation.leftItem: textField
+                        Navigation.rightItem: regexToggleButton
+                    }
+
+                    Widgets.IconToolButton {
+                        id: regexToggleButton
+
+                        anchors.verticalCenter: parent.verticalCenter
+
+                        // Animating font properties with native rendering is probably not a good idea:
+                        font.pixelSize: 0.6 * (contentItem.renderType === Text.QtRendering ? textField.height
+                                                                                           : textField.implicitHeight)
+                        text: VLCIcons.regex
+
+                        description: qsTr("Use regular expression")
+
+                        onClicked: {
+                            checked = !checked
+                        }
+
+                        Navigation.parentItem: textField
+                        Navigation.leftItem: caseSensitiveToggleButton
+                        Navigation.rightItem: clearButton
+                    }
+
+                    Widgets.IconToolButton {
+                        id: clearButton
+
+                        anchors.verticalCenter: parent.verticalCenter
+
+                        // Animating font properties with native rendering is probably not a good idea:
+                        font.pixelSize: 0.6 * (contentItem.renderType === Text.QtRendering ? textField.height
+                                                                                           : textField.implicitHeight)
+                        text: VLCIcons.close
+
+                        description: qsTr("Clear")
+
+                        visible: (textField.text.length > 0)
+
+                        onVisibleChanged: {
+                            if (activeFocus && !visible && parent.visible) {
+                                textField.focus = true
+                                textField.focusReason = focusReason
+                            }
+                        }
+                        onClicked: textField.clear()
+
+                        Navigation.parentItem: textField
+                        Navigation.leftItem: regexToggleButton
+                    }
+                }
+            }
         }
     }
-
 }


=====================================
modules/gui/qt/widgets/qml/TableViewDelegateExt.qml
=====================================
@@ -342,6 +342,7 @@ T.Control {
         id: dropAreaLayout
         anchors.fill: parent
 
+        index: delegate.index
         view: delegate.ListView.view
     }
 }



View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/bf0a50ef21b4c83450b31741320660e20725be7c...7ef767180dfa39c7e12c9bb5611bde5c89bc7e76

-- 
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/bf0a50ef21b4c83450b31741320660e20725be7c...7ef767180dfa39c7e12c9bb5611bde5c89bc7e76
You're receiving this email because of your account on code.videolan.org. Manage all notifications: https://code.videolan.org/-/profile/notifications | Help: https://code.videolan.org/help




More information about the vlc-commits mailing list