The Problem:
By default, the System Tray widget’s children are presented to you in a fixed order. If your panel is horizontal, the primary System Tray button is fixed at the far right of the tray and each new icon emerges to the left of it. The tray behaves the same way on vertically oriented panels, with the icons emerging in a “bottom to top” order. This is unnecessarily rigid, and creates high friction when customizing the desktop.
Proposed solution:
This was tested in KDE Plasma 6 on an up-to-date Arch Linux install
Add a basic toggle in “Configure System Tray… > General” labeled “Reverse icon order” which does exactly that. When the tray is placed on a horizontal panel and the toggle is enabled, the tray’s “arrow” will start on the left side instead of the default right side, and all of it’s children will emerge on it’s right side. The same toggle will also invert the orientation if the tray is on a vertically oriented panel, so that the arrow moves to the top and new items emerge below it.
Try it yourself:
- Copy the tray from
/usr/shareto~/.local/share:
cp -r /usr/share/plasma/plasmoids/org.kde.plasma.systemtray ~/.local/share/plasma/plasmoids
- Open
~/.local/share/plasma/plasmoids/org.kde.plasma.systemtray/contents/config/main.xmlin your preferred text editor and replace the contents with this:
<?xml version="1.0" encoding="UTF-8"?>
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
<kcfgfile name=""/>
<group name="General">
<entry name="extraItems" type="StringList">
<label>All plasmoid items that are explicitly enabled in the systray. It's a comma-separated string list of plasmoid plugin ids.</label>
<default></default>
</entry>
<entry name="disabledStatusNotifiers" type="StringList">
<label>All StatusNotifier items that shouldn't be visible at all. It's a comma-separated string list of StatusNotifier ids.</label>
<default></default>
</entry>
<entry name="hiddenItems" type="StringList">
<label>All items that are hidden, forced always in the popup. It's a comma-separated string list of unique identifiers that are either plasmoid plugin ids or StatusNotifier ids.</label>
<default></default>
</entry>
<entry name="shownItems" type="StringList">
<label>All items that are shown. It's a comma-separated string list of unique identifiers that are either plasmoid plugin ids or StatusNotifier ids.</label>
<default></default>
</entry>
<entry name="showAllItems" type="bool">
<label>If true, all systray entries will be always in the main area, outside the popup.</label>
<default>false</default>
</entry>
<entry name="knownItems" type="StringList" hidden="true">
<default></default>
</entry>
<entry name="reverseIconOrder" type="bool">
<label>Whether to reverse the order of icons in the panel</label>
<default>false</default>
</entry>
<entry name="scaleIconsToFit" type="bool">
<label>Whether to automatically scale System Tray icons to fix the available thickness of the panel. If false, tray icons will be capped at the smallMedium size (22px) and become a two-row/column layout when the panel is thick.</label>
<default>false</default>
</entry>
<entry name="iconSpacing" type="int">
<label>spacing between icons, determined by this number multiplied by Kirigami.Units.smallSpacing</label>
<default>2</default>
</entry>
<entry name="pin" type="Bool">
<label>Whether the popup should remain open when another window is activated</label>
<default>false</default>
</entry>
</group>
</kcfg>
- Open
~/.local/share/plasma/plasmoids/org.kde.plasma.systemtray/contents/ui/ConfigGeneral.qmlin your preferred text editor and replace the contents with this:
/*
SPDX-FileCopyrightText: 2020 Konrad Materka <materka@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kcmutils as KCMUtils
import org.kde.kirigami as Kirigami
import org.kde.plasma.core as PlasmaCore
import org.kde.plasma.plasmoid
KCMUtils.SimpleKCM {
property bool cfg_reverseIconOrder
property bool cfg_scaleIconsToFit
property int cfg_iconSpacing
Kirigami.FormLayout {
Layout.fillHeight: true
QQC2.CheckBox {
Kirigami.FormData.label: i18n("Panel icon layout")
text: i18n("Reverse icon order")
checked: cfg_reverseIconOrder
onToggled: cfg_reverseIconOrder = checked
}
QQC2.RadioButton {
Kirigami.FormData.label: i18nc("The arrangement of system tray icons in the Panel", "Panel icon size:")
enabled: !Kirigami.Settings.tabletMode
text: i18n("Small")
checked: !cfg_scaleIconsToFit && !Kirigami.Settings.tabletMode
onToggled: cfg_scaleIconsToFit = !checked
}
QQC2.RadioButton {
id: automaticRadioButton
enabled: !Kirigami.Settings.tabletMode
text: Plasmoid.formFactor === PlasmaCore.Types.Horizontal ? i18n("Scale with Panel height")
: i18n("Scale with Panel width")
checked: cfg_scaleIconsToFit || Kirigami.Settings.tabletMode
onToggled: cfg_scaleIconsToFit = checked
}
QQC2.Label {
visible: Kirigami.Settings.tabletMode
text: i18n("Automatically enabled when in Touch Mode")
textFormat: Text.PlainText
font: Kirigami.Theme.smallFont
}
Item {
Kirigami.FormData.isSection: true
}
QQC2.ComboBox {
Kirigami.FormData.label: i18nc("@label:listbox The spacing between system tray icons in the Panel", "Panel icon spacing:")
model: [
{
"label": i18nc("@item:inlistbox Icon spacing", "Small"),
"spacing": 1
},
{
"label": i18nc("@item:inlistbox Icon spacing", "Normal"),
"spacing": 2
},
{
"label": i18nc("@item:inlistbox Icon spacing", "Large"),
"spacing": 6
}
]
textRole: "label"
enabled: !Kirigami.Settings.tabletMode
currentIndex: {
if (Kirigami.Settings.tabletMode) {
return 2; // Large
}
switch (cfg_iconSpacing) {
case 1: return 0; // Small
case 2: return 1; // Normal
case 6: return 2; // Large
}
}
onActivated: index => {
cfg_iconSpacing = model[currentIndex]["spacing"];
}
}
QQC2.Label {
visible: Kirigami.Settings.tabletMode
text: i18nc("@info:usagetip under a combobox when Touch Mode is on", "Automatically set to Large when in Touch Mode")
textFormat: Text.PlainText
font: Kirigami.Theme.smallFont
}
}
}
- Open
~/.local/share/plasma/plasmoids/org.kde.plasma.systemtray/contents/ui/main.qmlin your preferred text editor and replace the contents with this:
/*
SPDX-FileCopyrightText: 2011 Marco Martin <mart@kde.org>
SPDX-FileCopyrightText: 2020 Konrad Materka <materka@gmail.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick
import QtQuick.Layouts
import org.kde.draganddrop as DnD
import org.kde.kirigami as Kirigami
import org.kde.kitemmodels as KItemModels
import org.kde.ksvg as KSvg
import org.kde.plasma.core as PlasmaCore
import org.kde.plasma.plasmoid
import "items" as Items
ContainmentItem {
id: root
readonly property bool vertical: Plasmoid.formFactor === PlasmaCore.Types.Vertical
readonly property bool reverseLayout: Plasmoid.configuration.reverseIconOrder
Layout.minimumWidth: vertical ? Kirigami.Units.iconSizes.small : mainLayout.implicitWidth + Kirigami.Units.smallSpacing
Layout.minimumHeight: vertical ? mainLayout.implicitHeight + Kirigami.Units.smallSpacing : Kirigami.Units.iconSizes.small
LayoutMirroring.enabled: !vertical && ((Qt.application.layoutDirection === Qt.RightToLeft) !== reverseLayout)
LayoutMirroring.childrenInherit: true
readonly property alias systemTrayState: systemTrayState
readonly property alias itemSize: tasksGrid.itemSize
readonly property alias visibleLayout: tasksGrid
readonly property alias hiddenLayout: expandedRepresentation.hiddenLayout
readonly property bool oneRowOrColumn: tasksGrid.rowsOrColumns === 1
readonly property alias hiddenModel: hiddenModel
Component.onCompleted: {
// We need all the plasmoiditems to be there for correct working of shortcuts.
// Instantiators create the plasmoiditems: ensure this is done after
// this containmentitem actually exists so they can be immediately parented properly
// set active and not the model, as this wil lcause an assert deep in Qt
activeInstantiator.active = true;
hiddenInstantiator.active = true;
}
Connections {
target: Plasmoid
function onActivated() {
systemTrayState.expanded = !systemTrayState.expanded;
}
}
KItemModels.KSortFilterProxyModel {
id: activeModel
filterRoleName: "effectiveStatus"
filterRowCallback: (sourceRow, sourceParent) => {
let value = sourceModel.data(sourceModel.index(sourceRow, 0, sourceParent), filterRole);
return value === PlasmaCore.Types.ActiveStatus;
}
Component.onCompleted: sourceModel = Plasmoid.systemTrayModel // avoid unnecessary binding, it causes loops
}
KItemModels.KSortFilterProxyModel {
id: hiddenModel
filterRoleName: "effectiveStatus"
filterRowCallback: (sourceRow, sourceParent) => {
let value = sourceModel.data(sourceModel.index(sourceRow, 0, sourceParent), filterRole);
return value === PlasmaCore.Types.PassiveStatus
}
Component.onCompleted: sourceModel = Plasmoid.systemTrayModel // avoid unnecessary binding, it causes loops
}
Instantiator {
id: hiddenInstantiator
// It's important that those are inactive at creation time
// to not create plasmoiditems too soon
active: false
model: hiddenModel
delegate: Connections {
required property QtObject applet
required property int row
target: applet
function onExpandedChanged(expanded: bool) {
if (expanded) {
systemTrayState.setActiveApplet(applet, row)
}
}
}
}
Instantiator {
id: activeInstantiator
active: false
model:activeModel
delegate: Connections {
required property QtObject applet
required property int row
target: applet
function onExpandedChanged(expanded: bool) {
if (expanded) {
systemTrayState.setActiveApplet(applet, row)
}
}
}
}
MouseArea {
anchors.fill: parent
onWheel: wheel => {
// Don't propagate unhandled wheel events
wheel.accepted = true;
}
SystemTrayState {
id: systemTrayState
}
//being there forces the items to fully load, and they will be reparented in the popup one by one, this item is *never* visible
Item {
id: preloadedStorage
visible: false
}
CurrentItemHighLight {
location: Plasmoid.location
parent: root
}
DnD.DropArea {
anchors.fill: parent
preventStealing: true
/** Extracts the name of the system tray applet in the drag data if present
* otherwise returns null*/
function systemTrayAppletName(event) {
if (event.mimeData.formats.indexOf("text/x-plasmoidservicename") < 0) {
return null;
}
const plasmoidId = event.mimeData.getDataAsByteArray("text/x-plasmoidservicename");
if (!Plasmoid.isSystemTrayApplet(plasmoidId)) {
return null;
}
return plasmoidId;
}
onDragEnter: event => {
if (!systemTrayAppletName(event)) {
event.ignore();
}
}
onDrop: event => {
const plasmoidId = systemTrayAppletName(event);
if (!plasmoidId) {
event.ignore();
return;
}
if (Plasmoid.configuration.extraItems.indexOf(plasmoidId) < 0) {
const extraItems = Plasmoid.configuration.extraItems;
extraItems.push(plasmoidId);
Plasmoid.configuration.extraItems = extraItems;
}
}
}
//Main Layout
GridLayout {
id: mainLayout
rowSpacing: 0
columnSpacing: 0
anchors.fill: parent
flow: vertical ? GridLayout.TopToBottom : GridLayout.LeftToRight
transform: Scale {
origin.y: mainLayout.height / 2
yScale: (root.vertical && root.reverseLayout) ? -1 : 1
}
GridView {
id: tasksGrid
Layout.alignment: Qt.AlignCenter
interactive: false //disable features we don't need
flow: vertical ? GridView.LeftToRight : GridView.TopToBottom
// The icon size to display when not using the auto-scaling setting
readonly property int smallIconSize: Kirigami.Units.iconSizes.smallMedium
// Automatically use autoSize setting when in tablet mode, if it's
// not already being used
readonly property bool autoSize: Plasmoid.configuration.scaleIconsToFit || Kirigami.Settings.tabletMode
readonly property int gridThickness: root.vertical ? root.width : root.height
// Should change to 2 rows/columns on a 56px panel (in standard DPI)
readonly property int rowsOrColumns: autoSize ? 1 : Math.max(1, Math.min(count, Math.floor(gridThickness / (smallIconSize + Kirigami.Units.smallSpacing))))
// Add margins only if the panel is larger than a small icon (to avoid large gaps between tiny icons)
readonly property int cellSpacing: Kirigami.Units.smallSpacing * (Kirigami.Settings.tabletMode ? 6 : Plasmoid.configuration.iconSpacing)
readonly property int smallSizeCellLength: gridThickness < smallIconSize ? smallIconSize : smallIconSize + cellSpacing
cellHeight: {
if (root.vertical) {
return autoSize ? itemSize + (gridThickness < itemSize ? 0 : cellSpacing) : smallSizeCellLength
} else {
return autoSize ? root.height : Math.floor(root.height / rowsOrColumns)
}
}
cellWidth: {
if (root.vertical) {
return autoSize ? root.width : Math.floor(root.width / rowsOrColumns)
} else {
return autoSize ? itemSize + (gridThickness < itemSize ? 0 : cellSpacing) : smallSizeCellLength
}
}
//depending on the form factor, we are calculating only one dimension, second is always the same as root/parent
implicitHeight: root.vertical ? cellHeight * Math.ceil(count / rowsOrColumns) : root.height
implicitWidth: !root.vertical ? cellWidth * Math.ceil(count / rowsOrColumns) : root.width
readonly property int itemSize: {
if (autoSize) {
return Kirigami.Units.iconSizes.roundedIconSize(Math.min(Math.min(root.width, root.height) / rowsOrColumns, Kirigami.Units.iconSizes.enormous))
} else {
return smallIconSize
}
}
model: activeModel
delegate: Items.ItemLoader {
id: delegate
width: tasksGrid.cellWidth
height: tasksGrid.cellHeight
transform: Scale {
origin.y: height / 2
yScale: (root.vertical && root.reverseLayout) ? -1 : 1
}
// We need to recalculate the stacking order of the z values due to how keyboard navigation works
// the tab order depends exclusively from this, so we redo it as the position in the list
// ensuring tab navigation focuses the expected items
Component.onCompleted: {
let item = tasksGrid.itemAtIndex(index - 1);
if (item) {
Plasmoid.stackItemBefore(delegate, item)
} else {
item = tasksGrid.itemAtIndex(index + 1);
}
if (item) {
Plasmoid.stackItemAfter(delegate, item)
}
}
}
}
ExpanderArrow {
id: expander
Layout.fillWidth: vertical
Layout.fillHeight: !vertical
Layout.alignment: vertical ? Qt.AlignVCenter : Qt.AlignHCenter
iconSize: tasksGrid.itemSize
visible: root.hiddenLayout.itemCount > 0
}
}
Timer {
id: expandedSync
interval: 100
onTriggered: systemTrayState.expanded = dialog.visible;
}
//Main popup
PlasmaCore.AppletPopup {
id: dialog
objectName: "popupWindow"
visualParent: root
popupDirection: switch (Plasmoid.location) {
case PlasmaCore.Types.TopEdge:
return Qt.BottomEdge
case PlasmaCore.Types.LeftEdge:
return Qt.RightEdge
case PlasmaCore.Types.RightEdge:
return Qt.LeftEdge
default:
return Qt.TopEdge
}
margin: (Plasmoid.containmentDisplayHints & PlasmaCore.Types.ContainmentPrefersFloatingApplets) ? Kirigami.Units.largeSpacing : 0
Behavior on margin {
NumberAnimation {
// Since the panel animation won't be perfectly in sync,
// using a duration larger than the panel animation results
// in a better-looking animation.
duration: Kirigami.Units.veryLongDuration
easing.type: Easing.OutCubic
}
}
floating: Plasmoid.location == PlasmaCore.Desktop
removeBorderStrategy: Plasmoid.location === PlasmaCore.Types.Floating
? PlasmaCore.AppletPopup.AtScreenEdges
: PlasmaCore.AppletPopup.AtScreenEdges | PlasmaCore.AppletPopup.AtPanelEdges
hideOnWindowDeactivate: !Plasmoid.configuration.pin
visible: systemTrayState.expanded
appletInterface: root
backgroundHints: (Plasmoid.containmentDisplayHints & PlasmaCore.Types.ContainmentPrefersOpaqueBackground) ? PlasmaCore.AppletPopup.SolidBackground : PlasmaCore.AppletPopup.StandardBackground
onVisibleChanged: {
if (!visible) {
expandedSync.restart();
} else {
dialog.requestActivate();
if (expandedRepresentation.plasmoidContainer.visible) {
expandedRepresentation.plasmoidContainer.forceActiveFocus();
} else if (expandedRepresentation.hiddenLayout.visible) {
expandedRepresentation.hiddenLayout.forceActiveFocus();
}
}
}
mainItem: ExpandedRepresentation {
id: expandedRepresentation
Keys.onEscapePressed: event => {
systemTrayState.expanded = false
}
// Draws a line between the applet dialog and the panel
KSvg.SvgItem {
id: separator
// Only draw for popups of panel applets, not desktop applets
visible: [PlasmaCore.Types.TopEdge, PlasmaCore.Types.LeftEdge, PlasmaCore.Types.RightEdge, PlasmaCore.Types.BottomEdge]
.includes(Plasmoid.location) && !dialog.margin
anchors {
topMargin: -dialog.topPadding
leftMargin: -dialog.leftPadding
rightMargin: -dialog.rightPadding
bottomMargin: -dialog.bottomPadding
}
z: 999 /* Draw the line on top of the applet */
elementId: (Plasmoid.location === PlasmaCore.Types.TopEdge || Plasmoid.location === PlasmaCore.Types.BottomEdge) ? "horizontal-line" : "vertical-line"
imagePath: "widgets/line"
// QTBUG-120464: Use AnchorChanges instead of bindings as it's officially supported: https://doc.qt.io/qt-6/qtquick-positioning-anchors.html#changing-anchors
states: [
State {
when: Plasmoid.location === PlasmaCore.Types.TopEdge
AnchorChanges {
target: separator
anchors {
top: separator.parent.top
left: separator.parent.left
right: separator.parent.right
}
}
PropertyChanges {
target: separator
height: 1
}
},
State {
when: Plasmoid.location === PlasmaCore.Types.LeftEdge
AnchorChanges {
target: separator
anchors {
left: separator.parent.left
top: separator.parent.top
bottom: separator.parent.bottom
}
}
PropertyChanges {
target: separator
width: 1
}
},
State {
when: Plasmoid.location === PlasmaCore.Types.RightEdge
AnchorChanges {
target: separator
anchors {
top: separator.parent.top
right: separator.parent.right
bottom: separator.parent.bottom
}
}
PropertyChanges {
target: separator
width: 1
}
},
State {
when: Plasmoid.location === PlasmaCore.Types.BottomEdge
AnchorChanges {
target: separator
anchors {
left: separator.parent.left
right: separator.parent.right
bottom: separator.parent.bottom
}
}
PropertyChanges {
target: separator
height: 1
}
}
]
}
LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft
LayoutMirroring.childrenInherit: true
}
}
}
}
- Restart plasmashell:
systemctl --user restart plasma-plasmashell
Reverting back
If you want the official tray back, just do an rm -rf on the ~/.local/share path we copied the plasmoid to and then restart the plasmashell:
rm -rf ~/.local/share/plasma/plasmoids/org.kde.plasma.systemtray && systemctl --user restart plasma-plasmashell