diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 2be53dc6ac..eb779370d6 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -296,6 +296,7 @@ if (KWIN_BUILD_TABBOX)
tabbox/x11_filter.cpp
)
target_link_libraries(kwin Qt::GuiPrivate)
+ add_subdirectory(tabbox/switchers)
endif()
qt_generate_dbus_interface(virtualkeyboard_dbus.h org.kde.kwin.VirtualKeyboard.xml OPTIONS -A)
diff --git a/src/kwin.kcfg b/src/kwin.kcfg
index 46bac99242..b014e87fd1 100644
--- a/src/kwin.kcfg
+++ b/src/kwin.kcfg
@@ -318,7 +318,7 @@
true
- thumbnails
+ thumbnail_grid
diff --git a/src/tabbox/switchers/CMakeLists.txt b/src/tabbox/switchers/CMakeLists.txt
new file mode 100644
index 0000000000..d2d4569504
--- /dev/null
+++ b/src/tabbox/switchers/CMakeLists.txt
@@ -0,0 +1,6 @@
+# SPDX-FileCopyrightText: 2023 Nate Graham
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+set(TABBOX_DIR ${KDE_INSTALL_DATADIR}/kwin/tabbox)
+install(DIRECTORY thumbnail_grid DESTINATION ${TABBOX_DIR})
diff --git a/src/tabbox/switchers/thumbnail_grid/contents/ui/main.qml b/src/tabbox/switchers/thumbnail_grid/contents/ui/main.qml
new file mode 100644
index 0000000000..525d0f5c14
--- /dev/null
+++ b/src/tabbox/switchers/thumbnail_grid/contents/ui/main.qml
@@ -0,0 +1,225 @@
+/*
+ KWin - the KDE window manager
+ This file is part of the KDE project.
+
+ SPDX-FileCopyrightText: 2020 Chris Holland
+ SPDX-FileCopyrightText: 2023 Nate Graham
+
+ SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+import QtQuick 2.0
+import QtQuick.Layouts 1.1
+import org.kde.plasma.core 2.0 as PlasmaCore
+import org.kde.plasma.components 3.0 as PlasmaComponents3
+import org.kde.kwin 3.0 as KWin
+import org.kde.kirigami 2.20 as Kirigami
+
+KWin.TabBoxSwitcher {
+ id: tabBox
+ currentIndex: thumbnailGridView.currentIndex
+
+ PlasmaCore.Dialog {
+ location: PlasmaCore.Types.Floating
+ visible: tabBox.visible
+ flags: Qt.X11BypassWindowManagerHint
+ x: tabBox.screenGeometry.x + tabBox.screenGeometry.width * 0.5 - dialogMainItem.width * 0.5
+ y: tabBox.screenGeometry.y + tabBox.screenGeometry.height * 0.5 - dialogMainItem.height * 0.5
+
+ mainItem: Item {
+ id: dialogMainItem
+
+ focus: true
+
+ property int maxWidth: tabBox.screenGeometry.width * 0.9
+ property int maxHeight: tabBox.screenGeometry.height * 0.7
+ property real screenFactor: tabBox.screenGeometry.width / tabBox.screenGeometry.height
+ property int maxGridColumnsByWidth: Math.floor(maxWidth / thumbnailGridView.cellWidth)
+
+ property int gridColumns: { // Simple greedy algorithm
+ // respect screenGeometry
+ const c = Math.min(thumbnailGridView.count, maxGridColumnsByWidth);
+ const residue = thumbnailGridView.count % c;
+ if (residue == 0) {
+ return c;
+ }
+ // start greedy recursion
+ return columnCountRecursion(c, c, c - residue);
+ }
+
+ property int gridRows: Math.ceil(thumbnailGridView.count / gridColumns)
+ property int optimalWidth: thumbnailGridView.cellWidth * gridColumns
+ property int optimalHeight: thumbnailGridView.cellHeight * gridRows
+ width: Math.min(Math.max(thumbnailGridView.cellWidth, optimalWidth), maxWidth)
+ height: Math.min(Math.max(thumbnailGridView.cellHeight, optimalHeight), maxHeight)
+
+ clip: true
+
+ // Step for greedy algorithm
+ function columnCountRecursion(prevC, prevBestC, prevDiff) {
+ const c = prevC - 1;
+
+ // don't increase vertical extent more than horizontal
+ // and don't exceed maxHeight
+ if (prevC * prevC <= thumbnailGridView.count + prevDiff ||
+ maxHeight < Math.ceil(thumbnailGridView.count / c) * thumbnailGridView.cellHeight) {
+ return prevBestC;
+ }
+ const residue = thumbnailGridView.count % c;
+ // halts algorithm at some point
+ if (residue == 0) {
+ return c;
+ }
+ // empty slots
+ const diff = c - residue;
+
+ // compare it to previous count of empty slots
+ if (diff < prevDiff) {
+ return columnCountRecursion(c, c, diff);
+ } else if (diff == prevDiff) {
+ // when it's the same try again, we'll stop early enough thanks to the landscape mode condition
+ return columnCountRecursion(c, prevBestC, diff);
+ }
+ // when we've found a local minimum choose this one (greedy)
+ return columnCountRecursion(c, prevBestC, diff);
+ }
+
+ // Just to get the margin sizes
+ PlasmaCore.FrameSvgItem {
+ id: hoverItem
+ imagePath: "widgets/viewitem"
+ prefix: "hover"
+ visible: false
+ }
+
+ GridView {
+ id: thumbnailGridView
+ anchors.fill: parent
+
+ model: tabBox.model
+
+ readonly property int iconSize: PlasmaCore.Units.iconSizes.huge
+ readonly property int captionRowHeight: PlasmaCore.Units.gridUnit * 2
+ readonly property int columnSpacing: PlasmaCore.Units.largeSpacing
+ readonly property int thumbnailWidth: PlasmaCore.Units.gridUnit * 16
+ readonly property int thumbnailHeight: thumbnailWidth * (1.0/dialogMainItem.screenFactor)
+ cellWidth: hoverItem.margins.left + thumbnailWidth + hoverItem.margins.right
+ cellHeight: hoverItem.margins.top + captionRowHeight + thumbnailHeight + hoverItem.margins.bottom
+
+ keyNavigationWraps: true
+ highlightMoveDuration: 0
+
+ delegate: Item {
+ id: thumbnailGridItem
+ width: thumbnailGridView.cellWidth
+ height: thumbnailGridView.cellHeight
+ readonly property bool isCurrentItem: GridView.isCurrentItem
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ onClicked: {
+ thumbnailGridItem.select();
+ }
+ }
+ function select() {
+ thumbnailGridView.currentIndex = index;
+ thumbnailGridView.currentIndexChanged(thumbnailGridView.currentIndex);
+ }
+
+ ColumnLayout {
+ id: columnLayout
+ z: 0
+ spacing: thumbnailGridView.columnSpacing
+ anchors.fill: parent
+ anchors.leftMargin: hoverItem.margins.left * 2
+ anchors.topMargin: hoverItem.margins.top * 2
+ anchors.rightMargin: hoverItem.margins.right * 2
+ anchors.bottomMargin: hoverItem.margins.bottom * 2
+
+
+ // KWin.WindowThumbnail needs a container
+ // otherwise it will be drawn the same size as the parent ColumnLayout
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ KWin.WindowThumbnail {
+ anchors.fill: parent
+ wId: windowId
+ }
+
+ PlasmaCore.IconItem {
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.bottom
+ anchors.verticalCenterOffset: Math.round(-columnLayout.spacing/2)
+ width: thumbnailGridView.iconSize
+ height: thumbnailGridView.iconSize
+
+ source: model.icon
+ usesPlasmaTheme: false
+ }
+
+ PlasmaComponents3.Button {
+ id: closeButton
+ anchors {
+ right: parent.right
+ top: parent.top
+ }
+ visible: model.closeable && typeof tabBox.model.close !== 'undefined' &&
+ (mouseArea.containsMouse
+ || closeButton.hovered
+ || thumbnailGridItem.isCurrentItem
+ || Kirigami.Settings.tabletMode
+ || Kirigami.Settings.hasTransientTouchInput
+ )
+ icon.name: 'window-close-symbolic'
+ onClicked: {
+ tabBox.model.close(index);
+ }
+ }
+ }
+
+ PlasmaComponents3.Label {
+ Layout.fillWidth: true
+ text: model.caption
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ textFormat: Text.PlainText
+ elide: Text.ElideRight
+ }
+ }
+ } // GridView.delegate
+
+ highlight: PlasmaCore.FrameSvgItem {
+ imagePath: "widgets/viewitem"
+ prefix: "hover"
+ }
+
+ Connections {
+ target: tabBox
+ function onCurrentIndexChanged() {
+ thumbnailGridView.currentIndex = tabBox.currentIndex;
+ }
+ }
+ } // GridView
+
+ Keys.onPressed: {
+ if (event.key == Qt.Key_Left) {
+ thumbnailGridView.moveCurrentIndexLeft();
+ } else if (event.key == Qt.Key_Right) {
+ thumbnailGridView.moveCurrentIndexRight();
+ } else if (event.key == Qt.Key_Up) {
+ thumbnailGridView.moveCurrentIndexUp();
+ } else if (event.key == Qt.Key_Down) {
+ thumbnailGridView.moveCurrentIndexDown();
+ } else {
+ return;
+ }
+
+ thumbnailGridView.currentIndexChanged(thumbnailGridView.currentIndex);
+ }
+ } // Dialog.mainItem
+ } // Dialog
+}
diff --git a/src/tabbox/switchers/thumbnail_grid/metadata.json b/src/tabbox/switchers/thumbnail_grid/metadata.json
new file mode 100644
index 0000000000..71a273bb86
--- /dev/null
+++ b/src/tabbox/switchers/thumbnail_grid/metadata.json
@@ -0,0 +1,132 @@
+{
+ "KPackageStructure": "KWin/WindowSwitcher",
+ "KPlugin": {
+ "Authors": [
+ {
+ "Email": "zrenfire@gmail.com",
+ "Name": "Chris Holland",
+ "Name[ar]": "Chris Holland",
+ "Name[az]": "Chris Holland",
+ "Name[bg]": "Chris Holland",
+ "Name[ca@valencia]": "Chris Holland",
+ "Name[ca]": "Chris Holland",
+ "Name[cs]": "Chris Holland",
+ "Name[de]": "Chris Holland",
+ "Name[en_GB]": "Chris Holland",
+ "Name[eo]": "Chris Holland",
+ "Name[es]": "Chris Holland",
+ "Name[eu]": "Chris Holland",
+ "Name[fi]": "Chris Holland",
+ "Name[fr]": "Chris Holland",
+ "Name[gl]": "Chris Holland",
+ "Name[ia]": "Chris Holland",
+ "Name[id]": "Chris Holland",
+ "Name[is]": "Chris Holland",
+ "Name[it]": "Chris Holland",
+ "Name[ja]": "Chris Holland",
+ "Name[ka]": "Chris Holland",
+ "Name[ko]": "Chris Holland",
+ "Name[lt]": "Chris Holland",
+ "Name[nl]": "Chris Holland",
+ "Name[nn]": "Chris Holland",
+ "Name[pl]": "Chris Holland",
+ "Name[pt]": "Chris Holland",
+ "Name[pt_BR]": "Chris Holland",
+ "Name[ro]": "Chris Holland",
+ "Name[ru]": "Chris Holland",
+ "Name[sk]": "Chris Holland",
+ "Name[sl]": "Chris Holland",
+ "Name[sv]": "Chris Holland",
+ "Name[tr]": "Chris Holland",
+ "Name[uk]": "Chris Holland",
+ "Name[vi]": "Chris Holland",
+ "Name[x-test]": "xxChris Hollandxx",
+ "Name[zh_CN]": "Chris Holland",
+ "Name[zh_TW]": "Chris Holland"
+ }
+ ],
+ "Description": "A window switcher layout using a small grid of thumbnails",
+ "Description[ar]": "مخطط مبدل نوافذ يستخدم شبكة صغيرة من المصغرات",
+ "Description[az]": "Kiçik miniatürlər toru istifadə edən pəncərə dəyişdirici maketi",
+ "Description[bg]": "Оформление на програмата за превключване на прозорци с използване на редица от миниатюри за представяне на прозорците",
+ "Description[ca@valencia]": "Una disposició del commutador de finestres que utilitza una quadrícula xicoteta de miniatures",
+ "Description[ca]": "Una disposició del commutador de finestres que utilitza una quadrícula petita de miniatures",
+ "Description[cs]": "Přepínač oken používající malou mřížku náhledů",
+ "Description[de]": "Ein Fensterwechsler-Layout, das ein Raster von kleinen Vorschaubildern verwendet",
+ "Description[en_GB]": "A window switcher layout using a small grid of thumbnails",
+ "Description[eo]": "Fenestra ŝaltilo-aranĝo uzante malgrandan kradon de bildetoj",
+ "Description[es]": "Un esquema de selección de ventanas que usa una pequeña cuadrícula de miniaturas",
+ "Description[eu]": "Koadro-txikien sareta txiki bat erabiltzen duen leiho-trukatzaile antolamendu bat",
+ "Description[fi]": "Ikkunavaihtoasettelu, joka käyttää pientä pienoiskuvaruudukkoa",
+ "Description[fr]": "Une disposition du sélecteur de fenêtres utilisant une petite grille de vignettes",
+ "Description[gl]": "Unha disposición do selector de xanelas que usa unha pequena grade de miniaturas",
+ "Description[ia]": "Un disposition de commutator de fenestra usante un parve grillia de miniaturas",
+ "Description[id]": "Sebuah tataletak pengalih jendela yang menggunakan sebuah kisi kecil ber-thumbnail",
+ "Description[it]": "Una disposizione del selettore delle finestre che utilizza una piccola griglia di miniature",
+ "Description[ja]": "サムネイルの小さいグリッドを使用するウィンドウスイッチャー",
+ "Description[ka]": "ფანჯრების განლაგების გადართვა მინიატურების პატარა ბადის ჩვენების საშუალებით",
+ "Description[ko]": "작은 축소판 격자를 사용하는 창 전환기 레이아웃",
+ "Description[lt]": "Langų perjungiklio išdėstymas, naudojantis mažą miniatiūrų tinklelį",
+ "Description[nl]": "Een indeling van de vensterwisselaar met een klein raster van miniaturen",
+ "Description[nn]": "Vindaugsbytar som viser eit rutenett med miniatyrbilete",
+ "Description[pl]": "Układ przełączania okien, używający małej siatki miniatur",
+ "Description[pt]": "Uma disposição de mudança de janelas que mostra uma pequena grelha de miniaturas",
+ "Description[pt_BR]": "Um layout do seletor de janelas que usa uma pequena grade de miniaturas",
+ "Description[ro]": "Aranjament pentru comutatorul de ferestre folosind o grilă mică de pictograme",
+ "Description[ru]": "Переключатель окон в виде сетки миниатюр окон",
+ "Description[sk]": "Rozloženie prepínača okien používajúce malú mriežku miniatúr",
+ "Description[sl]": "Razporeditev preklopa oken, ki uporablja malo mrežo predoglednih sličic",
+ "Description[sv]": "En layout för fönsterbyte som använder ett litet miniatyrbildsrutnät",
+ "Description[tr]": "Küçük görsellerin bir ızgarasını gösteren pencere değiştirici düzeni",
+ "Description[uk]": "Компонування засобу перемикання вікон з малою таблицею мініатюр поточного стану вікон",
+ "Description[vi]": "Bố cục trình chuyển cửa sổ dùng một lưới nhỏ các hình nhỏ",
+ "Description[x-test]": "xxA window switcher layout using a small grid of thumbnailsxx",
+ "Description[zh_CN]": "将窗口缩略图按小型表格显示的窗口切换器布局",
+ "Description[zh_TW]": "使用小縮圖格線的視窗切換器佈局",
+ "Icon": "preferences-system-windows-switcher-thumbnail-grid",
+ "Id": "thumbnail_grid",
+ "License": "GPL",
+ "Name": "Thumbnail Grid",
+ "Name[ar]": "شبكة مصغرات",
+ "Name[az]": "Miniatürlər cədvəli",
+ "Name[bg]": "Решетка с миниатюри",
+ "Name[ca@valencia]": "Quadrícula de miniatures",
+ "Name[ca]": "Quadrícula de miniatures",
+ "Name[cs]": "Mřížka náhledů",
+ "Name[de]": "Vorschaubilder-Raster",
+ "Name[en_GB]": "Thumbnail Grid",
+ "Name[eo]": "Bildeto-Krado",
+ "Name[es]": "Cuadrícula de miniaturas",
+ "Name[eu]": "Koadro txikien sareta",
+ "Name[fi]": "Pienoiskuvaruudukko",
+ "Name[fr]": "Grille de vignettes",
+ "Name[gl]": "Grade de miniaturas",
+ "Name[ia]": "Grillia de Miniatura",
+ "Name[id]": "Kisi Thumbnail",
+ "Name[is]": "Raðaðar smámyndir",
+ "Name[it]": "Griglia delle miniature",
+ "Name[ja]": "サムネイルグリッド",
+ "Name[ka]": "მინიატურის ბადე",
+ "Name[ko]": "축소판 격자",
+ "Name[lt]": "Miniatiūrų tinklelis",
+ "Name[nl]": "Raster voor miniaturen",
+ "Name[nn]": "Rutenett med miniatyrbilete",
+ "Name[pl]": "Siatka miniatur",
+ "Name[pt]": "Grelha de Miniaturas",
+ "Name[pt_BR]": "Grade de miniaturas",
+ "Name[ro]": "Grilă de miniaturi",
+ "Name[ru]": "Сетка миниатюр",
+ "Name[sk]": "Mriežka miniatúr",
+ "Name[sl]": "Mreža predoglednih sličic",
+ "Name[sv]": "Miniatyrbildsrutnät",
+ "Name[tr]": "Küçük Görsel Izgarası",
+ "Name[uk]": "Таблиця мініатюр",
+ "Name[vi]": "Lưới hình nhỏ",
+ "Name[x-test]": "xxThumbnail Gridxx",
+ "Name[zh_CN]": "缩略图网格",
+ "Name[zh_TW]": "縮圖格線",
+ "Version": "6"
+ },
+ "X-Plasma-API": "declarativeappletscript",
+ "X-Plasma-MainScript": "ui/main.qml"
+}
diff --git a/src/tabbox/tabboxconfig.h b/src/tabbox/tabboxconfig.h
index c9a80c33e3..36082ce4a3 100644
--- a/src/tabbox/tabboxconfig.h
+++ b/src/tabbox/tabboxconfig.h
@@ -279,7 +279,7 @@ public:
}
static QString defaultLayoutName()
{
- return QStringLiteral("org.kde.breeze.desktop");
+ return QStringLiteral("thumbnail_grid");
}
private:
diff --git a/src/tabbox/tabboxhandler.cpp b/src/tabbox/tabboxhandler.cpp
index 0d445b88dd..a5fd18bced 100644
--- a/src/tabbox/tabboxhandler.cpp
+++ b/src/tabbox/tabboxhandler.cpp
@@ -235,7 +235,7 @@ QObject *TabBoxHandlerPrivate::createSwitcherItem()
// load default
offers = KPackage::PackageLoader::self()->findPackages(type, folderName,
[](const KPluginMetaData &data) {
- return data.pluginId().compare(QStringLiteral("compact"), Qt::CaseInsensitive) == 0;
+ return data.pluginId().compare(QStringLiteral("thumbnail_grid"), Qt::CaseInsensitive) == 0;
});
if (offers.isEmpty()) {
qCDebug(KWIN_TABBOX) << "could not find default window switcher layout";