add noctalia and fuzzel
This commit is contained in:
@@ -0,0 +1,804 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Commons
|
||||
import qs.Modules.Panels.Settings
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
PopupWindow {
|
||||
id: root
|
||||
|
||||
property var toplevel: null
|
||||
property var appData: null
|
||||
property Item anchorItem: null
|
||||
property ShellScreen targetScreen: null
|
||||
|
||||
property string menuMode: "app" // "app" or "launcher"
|
||||
property string launcherWidgetSection: ""
|
||||
property int launcherWidgetIndex: -1
|
||||
property var launcherWidgetSettings: ({})
|
||||
|
||||
property bool hovered: menuHoverHandler.hovered
|
||||
property var onAppClosed: null // Callback function for when an app is closed
|
||||
property bool canAutoClose: false
|
||||
|
||||
// Track which menu item is hovered
|
||||
property int hoveredItem: -1 // -1: none, otherwise the index of the item in `items`
|
||||
|
||||
property var items: []
|
||||
|
||||
signal requestClose
|
||||
|
||||
property real menuContentWidth: 160
|
||||
property real menuMinWidth: 120
|
||||
property real menuMaxWidth: 360
|
||||
property real menuMaxHeight: Math.max(180, Math.min(420, Math.round((targetScreen ? targetScreen.height : 600) * 0.3)))
|
||||
property int separatorCompactHeight: Style.borderS + Style.margin2S
|
||||
property string forcedGroupMenuMode: ""
|
||||
readonly property int separatorIndex: {
|
||||
for (let i = 0; i < root.items.length; i++) {
|
||||
if (root.items[i] && root.items[i].separator === true)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
readonly property bool splitExtendedLayout: separatorIndex >= 0
|
||||
readonly property var scrollItems: splitExtendedLayout ? root.items.slice(0, separatorIndex) : root.items
|
||||
readonly property var fixedItems: splitExtendedLayout ? root.items.slice(separatorIndex + 1) : []
|
||||
readonly property real menuInnerHeight: Math.max(0, implicitHeight - Style.margin2M)
|
||||
readonly property real fixedActionsHeight: listHeight(fixedItems)
|
||||
readonly property real separatorBlockHeight: splitExtendedLayout ? separatorCompactHeight : 0
|
||||
readonly property real scrollAreaHeight: splitExtendedLayout ? Math.max(0, menuInnerHeight - fixedActionsHeight - separatorBlockHeight) : menuInnerHeight
|
||||
readonly property bool listOverflowing: menuFlick && menuFlick.contentHeight > menuFlick.height
|
||||
readonly property real menuBodyHeight: {
|
||||
if (splitExtendedLayout) {
|
||||
return listHeight(scrollItems) + separatorBlockHeight + fixedActionsHeight;
|
||||
}
|
||||
return listHeight(root.items);
|
||||
}
|
||||
|
||||
implicitWidth: menuContentWidth + Style.margin2M
|
||||
implicitHeight: Math.min(menuBodyHeight + Style.margin2M, menuMaxHeight)
|
||||
color: "transparent"
|
||||
visible: false
|
||||
|
||||
// Hidden text element for measuring text width
|
||||
NText {
|
||||
id: textMeasure
|
||||
visible: false
|
||||
pointSize: Style.fontSizeS
|
||||
family: "Sans Serif" // Match your NText font if different
|
||||
wrapMode: Text.NoWrap
|
||||
elide: Text.ElideNone
|
||||
}
|
||||
|
||||
// Calculate the maximum width needed for all menu items
|
||||
function calculateMenuWidth() {
|
||||
let maxWidth = 0; // Start with 0, we'll apply minimum later
|
||||
if (root.items && root.items.length > 0) {
|
||||
for (let i = 0; i < root.items.length; i++) {
|
||||
const item = root.items[i];
|
||||
if (item && item.text) {
|
||||
// Calculate width: margins + icon (if present) + spacing + text width
|
||||
let itemWidth = Style.margin2S; // left and right margins
|
||||
|
||||
if (item.icon && item.icon !== "") {
|
||||
itemWidth += Style.fontSizeL + Style.marginS; // icon + spacing
|
||||
}
|
||||
|
||||
// Measure actual text width
|
||||
textMeasure.text = item.text;
|
||||
const textWidth = textMeasure.contentWidth;
|
||||
itemWidth += textWidth;
|
||||
|
||||
if (itemWidth > maxWidth) {
|
||||
maxWidth = itemWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Keep menu readable without allowing extremely wide labels.
|
||||
menuContentWidth = Math.max(menuMinWidth, Math.min(menuMaxWidth, Math.ceil(maxWidth)));
|
||||
}
|
||||
|
||||
function getCurrentAppId() {
|
||||
return appData?.appId || toplevel?.appId || "";
|
||||
}
|
||||
|
||||
function getValidToplevels() {
|
||||
if (!ToplevelManager || !ToplevelManager.toplevels)
|
||||
return [];
|
||||
const source = appData?.toplevels && appData.toplevels.length > 0 ? appData.toplevels : (toplevel ? [toplevel] : []);
|
||||
const allToplevels = ToplevelManager.toplevels.values || [];
|
||||
return source.filter(window => window && allToplevels.includes(window));
|
||||
}
|
||||
|
||||
function getPrimaryToplevel() {
|
||||
const windows = getValidToplevels();
|
||||
if (windows.length === 0)
|
||||
return null;
|
||||
if (ToplevelManager && ToplevelManager.activeToplevel && windows.includes(ToplevelManager.activeToplevel))
|
||||
return ToplevelManager.activeToplevel;
|
||||
return windows[0];
|
||||
}
|
||||
|
||||
function isItemActionable(index) {
|
||||
if (index < 0 || index >= root.items.length)
|
||||
return false;
|
||||
const item = root.items[index];
|
||||
return item && typeof item.action === "function";
|
||||
}
|
||||
|
||||
function rowHeightForItem(item) {
|
||||
return item && item.separator === true ? 16 : 32;
|
||||
}
|
||||
|
||||
function listHeight(items) {
|
||||
let total = 0;
|
||||
if (!items)
|
||||
return total;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
total += rowHeightForItem(items[i]);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
function initItems() {
|
||||
if (menuMode === "launcher") {
|
||||
root.items = [
|
||||
{
|
||||
"icon": "adjustments",
|
||||
"text": I18n.tr("actions.dock-settings"),
|
||||
"action": function () {
|
||||
handleDockSettings();
|
||||
}
|
||||
},
|
||||
{
|
||||
"icon": "adjustments",
|
||||
"text": I18n.tr("actions.launcher-settings"),
|
||||
"action": function () {
|
||||
handleLauncherSettings();
|
||||
}
|
||||
}
|
||||
];
|
||||
calculateMenuWidth();
|
||||
return;
|
||||
}
|
||||
|
||||
const windows = getValidToplevels();
|
||||
const primaryToplevel = getPrimaryToplevel();
|
||||
const appId = getCurrentAppId();
|
||||
const isRunning = windows.length > 0;
|
||||
const isPinned = isAppPinned(appId);
|
||||
const grouped = Settings.data.dock.groupApps && windows.length > 1;
|
||||
const rawGroupMenuMode = forcedGroupMenuMode || Settings.data.dock.groupContextMenuMode || "extended";
|
||||
const menuModeForGroup = grouped ? ((rawGroupMenuMode === "list" || rawGroupMenuMode === "extended") ? rawGroupMenuMode : "extended") : "single";
|
||||
|
||||
var next = [];
|
||||
|
||||
if (!grouped || menuModeForGroup === "single") {
|
||||
if (isRunning) {
|
||||
next.push({
|
||||
"icon": "eye",
|
||||
"text": I18n.tr("common.focus"),
|
||||
"action": function () {
|
||||
handleFocus(primaryToplevel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
next.push({
|
||||
"icon": !isPinned ? "pin" : "unpin",
|
||||
"text": !isPinned ? I18n.tr("common.pin") : I18n.tr("common.unpin"),
|
||||
"action": function () {
|
||||
handlePin(appId);
|
||||
}
|
||||
});
|
||||
|
||||
if (isRunning) {
|
||||
next.push({
|
||||
"icon": "close",
|
||||
"text": I18n.tr("common.close"),
|
||||
"action": function () {
|
||||
handleClose(primaryToplevel);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
windows.forEach((window, index) => {
|
||||
const windowTitle = (window.title && window.title.trim() !== "") ? window.title : (appId || ("Window " + (index + 1)));
|
||||
next.push({
|
||||
"icon": window === ToplevelManager?.activeToplevel ? "circle-filled" : "square-rounded",
|
||||
"text": windowTitle,
|
||||
"action": function () {
|
||||
handleFocus(window);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (menuModeForGroup === "extended") {
|
||||
next.push({
|
||||
"separator": true
|
||||
});
|
||||
next.push({
|
||||
"icon": "eye",
|
||||
"text": I18n.tr("common.focus"),
|
||||
"action": function () {
|
||||
handleFocus(primaryToplevel);
|
||||
}
|
||||
});
|
||||
next.push({
|
||||
"icon": !isPinned ? "pin" : "unpin",
|
||||
"text": !isPinned ? I18n.tr("common.pin") : I18n.tr("common.unpin"),
|
||||
"action": function () {
|
||||
handlePin(appId);
|
||||
}
|
||||
});
|
||||
next.push({
|
||||
"icon": "close",
|
||||
"text": I18n.tr("common.close") + " All",
|
||||
"action": function () {
|
||||
handleCloseAll(windows);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Keep grouped list mode as a clean window switcher.
|
||||
const canAddDesktopActions = !grouped || menuModeForGroup === "extended";
|
||||
|
||||
// Create a menu entry for each app-specific action defined in its .desktop file
|
||||
if (canAddDesktopActions && typeof DesktopEntries !== 'undefined' && DesktopEntries.byId && appId) {
|
||||
const entry = (DesktopEntries.heuristicLookup) ? DesktopEntries.heuristicLookup(appId) : DesktopEntries.byId(appId);
|
||||
if (entry != null) {
|
||||
entry.actions.forEach(function (action) {
|
||||
next.push({
|
||||
"icon": "chevron-right",
|
||||
"text": action.name,
|
||||
"action": function () {
|
||||
if (action.command && action.command.length > 0) {
|
||||
Quickshell.execDetached(action.command);
|
||||
} else if (action.execute) {
|
||||
action.execute();
|
||||
}
|
||||
if (Settings.data.dock.dockType === "attached") {
|
||||
const panel = PanelService.getPanel("staticDockPanel", root.screen, false);
|
||||
if (panel)
|
||||
panel.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
root.items = next;
|
||||
// Force width recalculation when items change
|
||||
calculateMenuWidth();
|
||||
}
|
||||
|
||||
// Helper function to normalize app IDs for case-insensitive matching
|
||||
function normalizeAppId(appId) {
|
||||
if (!appId || typeof appId !== 'string')
|
||||
return "";
|
||||
return appId.toLowerCase().trim();
|
||||
}
|
||||
|
||||
// Helper function to get desktop entry ID from an app ID
|
||||
function getDesktopEntryId(appId) {
|
||||
if (!appId)
|
||||
return appId;
|
||||
|
||||
// Try to find the desktop entry using heuristic lookup
|
||||
if (typeof DesktopEntries !== 'undefined' && DesktopEntries.heuristicLookup) {
|
||||
try {
|
||||
const entry = DesktopEntries.heuristicLookup(appId);
|
||||
if (entry && entry.id) {
|
||||
return entry.id;
|
||||
}
|
||||
} catch (e)
|
||||
// Fall through to return original appId
|
||||
{}
|
||||
}
|
||||
|
||||
// Try direct lookup
|
||||
if (typeof DesktopEntries !== 'undefined' && DesktopEntries.byId) {
|
||||
try {
|
||||
const entry = DesktopEntries.byId(appId);
|
||||
if (entry && entry.id) {
|
||||
return entry.id;
|
||||
}
|
||||
} catch (e)
|
||||
// Fall through to return original appId
|
||||
{}
|
||||
}
|
||||
|
||||
// Return original appId if we can't find a desktop entry
|
||||
return appId;
|
||||
}
|
||||
|
||||
// Helper functions for pin/unpin functionality
|
||||
function isAppPinned(appId) {
|
||||
if (!appId)
|
||||
return false;
|
||||
const pinnedApps = Settings.data.dock.pinnedApps || [];
|
||||
const normalizedId = normalizeAppId(appId);
|
||||
return pinnedApps.some(pinnedId => normalizeAppId(pinnedId) === normalizedId);
|
||||
}
|
||||
|
||||
function toggleAppPin(appId) {
|
||||
if (!appId)
|
||||
return;
|
||||
|
||||
// Get the desktop entry ID for consistent pinning
|
||||
const desktopEntryId = getDesktopEntryId(appId);
|
||||
const normalizedId = normalizeAppId(desktopEntryId);
|
||||
|
||||
let pinnedApps = (Settings.data.dock.pinnedApps || []).slice(); // Create a copy
|
||||
|
||||
// Find existing pinned app with case-insensitive matching
|
||||
const existingIndex = pinnedApps.findIndex(pinnedId => normalizeAppId(pinnedId) === normalizedId);
|
||||
const isPinned = existingIndex >= 0;
|
||||
|
||||
if (isPinned) {
|
||||
// Unpin: remove from array
|
||||
pinnedApps.splice(existingIndex, 1);
|
||||
} else {
|
||||
// Pin: add desktop entry ID to array
|
||||
pinnedApps.push(desktopEntryId);
|
||||
}
|
||||
|
||||
// Update the settings
|
||||
Settings.data.dock.pinnedApps = pinnedApps;
|
||||
}
|
||||
|
||||
// Dock position for context menu placement
|
||||
property string dockPosition: "bottom"
|
||||
|
||||
anchor.item: anchorItem
|
||||
// Position menu on opposite side of dock with comfortable spacing
|
||||
anchor.rect.x: {
|
||||
if (!anchorItem)
|
||||
return 0;
|
||||
switch (dockPosition) {
|
||||
case "left":
|
||||
return anchorItem.width + Style.marginL; // Open to right of dock
|
||||
case "right":
|
||||
return -implicitWidth - Style.marginL; // Open to left of dock
|
||||
default:
|
||||
return (anchorItem.width - implicitWidth) / 2; // Center horizontally
|
||||
}
|
||||
}
|
||||
anchor.rect.y: {
|
||||
if (!anchorItem)
|
||||
return 0;
|
||||
switch (dockPosition) {
|
||||
case "top":
|
||||
return anchorItem.height + Style.marginL; // Open below dock
|
||||
case "bottom":
|
||||
return -implicitHeight - Style.marginL; // Open above dock (default)
|
||||
case "left":
|
||||
case "right":
|
||||
return (anchorItem.height - implicitHeight) / 2; // Center vertically
|
||||
default:
|
||||
return -implicitHeight - Style.marginL;
|
||||
}
|
||||
}
|
||||
|
||||
function show(item, toplevelData, screen, groupModeOverride) {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
// First hide completely
|
||||
visible = false;
|
||||
|
||||
// Then set up new data
|
||||
anchorItem = item;
|
||||
if (toplevelData && typeof toplevelData === "object" && (toplevelData.appId !== undefined || toplevelData.toplevels !== undefined)) {
|
||||
appData = toplevelData;
|
||||
toplevel = toplevelData.toplevel || null;
|
||||
} else {
|
||||
appData = toplevelData ? {
|
||||
"appId": toplevelData.appId,
|
||||
"toplevel": toplevelData,
|
||||
"toplevels": toplevelData ? [toplevelData] : []
|
||||
} : null;
|
||||
toplevel = toplevelData;
|
||||
}
|
||||
targetScreen = screen || null;
|
||||
forcedGroupMenuMode = groupModeOverride || "";
|
||||
initItems();
|
||||
|
||||
visible = true;
|
||||
canAutoClose = false;
|
||||
gracePeriodTimer.restart();
|
||||
}
|
||||
|
||||
// Helper function to determine which menu item is under the mouse
|
||||
function getHoveredItem(mouseY) {
|
||||
const startY = Style.marginM;
|
||||
const localY = mouseY - startY;
|
||||
|
||||
if (localY < 0)
|
||||
return -1;
|
||||
|
||||
function findIndexInList(items, relativeY, baseIndex) {
|
||||
let offset = 0;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const h = rowHeightForItem(items[i]);
|
||||
if (relativeY >= offset && relativeY < offset + h)
|
||||
return baseIndex + i;
|
||||
offset += h;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (splitExtendedLayout) {
|
||||
if (localY < scrollAreaHeight) {
|
||||
return findIndexInList(scrollItems, localY + (menuFlick ? menuFlick.contentY : 0), 0);
|
||||
}
|
||||
if (localY < scrollAreaHeight + separatorBlockHeight) {
|
||||
return -1;
|
||||
}
|
||||
return findIndexInList(fixedItems, localY - scrollAreaHeight - separatorBlockHeight, separatorIndex + 1);
|
||||
} else {
|
||||
return findIndexInList(scrollItems, localY + (menuFlick ? menuFlick.contentY : 0), 0);
|
||||
}
|
||||
}
|
||||
|
||||
function fixedItemGlobalIndex(localIndex) {
|
||||
if (!splitExtendedLayout)
|
||||
return localIndex;
|
||||
return separatorIndex + 1 + localIndex;
|
||||
}
|
||||
|
||||
function isScrollableHovered(mouseY) {
|
||||
const localY = mouseY - Style.marginM;
|
||||
return localY >= 0 && localY < scrollAreaHeight;
|
||||
}
|
||||
|
||||
function onWheelScroll(deltaY) {
|
||||
if (!menuFlick || menuFlick.contentHeight <= menuFlick.height)
|
||||
return;
|
||||
const nextY = menuFlick.contentY - deltaY;
|
||||
menuFlick.contentY = Math.max(0, Math.min(nextY, menuFlick.contentHeight - menuFlick.height));
|
||||
}
|
||||
|
||||
function resetMenuState() {
|
||||
root.items.length = 0;
|
||||
root.appData = null;
|
||||
root.toplevel = null;
|
||||
root.forcedGroupMenuMode = "";
|
||||
menuContentWidth = menuMinWidth;
|
||||
hoveredItem = -1;
|
||||
if (menuFlick)
|
||||
menuFlick.contentY = 0;
|
||||
}
|
||||
|
||||
function hide() {
|
||||
visible = false;
|
||||
resetMenuState();
|
||||
}
|
||||
|
||||
function hideWithoutReset() {
|
||||
visible = false;
|
||||
}
|
||||
|
||||
function closeAndReset() {
|
||||
hide();
|
||||
root.requestClose();
|
||||
}
|
||||
|
||||
function handleFocus(targetToplevel) {
|
||||
if (targetToplevel?.activate) {
|
||||
targetToplevel.activate();
|
||||
}
|
||||
closeAndReset();
|
||||
}
|
||||
|
||||
function handlePin(appId) {
|
||||
if (appId) {
|
||||
root.toggleAppPin(appId);
|
||||
}
|
||||
Qt.callLater(() => closeAndReset());
|
||||
}
|
||||
|
||||
function handleClose(targetToplevel) {
|
||||
const isValidToplevel = targetToplevel && ToplevelManager && ToplevelManager.toplevels.values.includes(targetToplevel);
|
||||
|
||||
if (isValidToplevel && targetToplevel.close) {
|
||||
targetToplevel.close();
|
||||
if (root.onAppClosed && typeof root.onAppClosed === "function") {
|
||||
Qt.callLater(root.onAppClosed);
|
||||
}
|
||||
}
|
||||
closeAndReset();
|
||||
}
|
||||
|
||||
function handleCloseAll(windows) {
|
||||
windows.forEach(window => {
|
||||
if (window && ToplevelManager && ToplevelManager.toplevels.values.includes(window) && window.close) {
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
if (root.onAppClosed && typeof root.onAppClosed === "function") {
|
||||
Qt.callLater(root.onAppClosed);
|
||||
}
|
||||
closeAndReset();
|
||||
}
|
||||
|
||||
function handleLauncherSettings() {
|
||||
if (targetScreen) {
|
||||
var panel = PanelService.getPanel("settingsPanel", targetScreen);
|
||||
panel.requestedTab = SettingsPanel.Tab.Launcher;
|
||||
panel.toggle();
|
||||
}
|
||||
closeAndReset();
|
||||
}
|
||||
|
||||
function handleDockSettings() {
|
||||
if (targetScreen) {
|
||||
var panel = PanelService.getPanel("settingsPanel", targetScreen);
|
||||
panel.requestedTab = SettingsPanel.Tab.Dock;
|
||||
panel.toggle();
|
||||
}
|
||||
closeAndReset();
|
||||
}
|
||||
|
||||
function handleLauncherWidgetSettings() {
|
||||
if (targetScreen && launcherWidgetSection && launcherWidgetIndex >= 0) {
|
||||
BarService.openWidgetSettings(targetScreen, launcherWidgetSection, launcherWidgetIndex, "Launcher", launcherWidgetSettings || {});
|
||||
}
|
||||
closeAndReset();
|
||||
}
|
||||
|
||||
// Short delay to ignore spurious events
|
||||
Timer {
|
||||
id: gracePeriodTimer
|
||||
interval: 1500
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
root.canAutoClose = true;
|
||||
if (!menuHoverHandler.hovered) {
|
||||
closeTimer.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: closeTimer
|
||||
interval: 500
|
||||
repeat: false
|
||||
running: false
|
||||
onTriggered: {
|
||||
root.hideWithoutReset();
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: border.width
|
||||
color: Color.mSurfaceVariant
|
||||
radius: Style.radiusS
|
||||
border.color: Color.mOutline
|
||||
border.width: Style.borderS
|
||||
|
||||
HoverHandler {
|
||||
id: menuHoverHandler
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
onHoveredChanged: {
|
||||
if (hovered) {
|
||||
closeTimer.stop();
|
||||
} else {
|
||||
root.hoveredItem = -1;
|
||||
if (root.canAutoClose) {
|
||||
closeTimer.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WheelHandler {
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
onWheel: event => {
|
||||
if (!root.isScrollableHovered(event.y))
|
||||
return;
|
||||
const delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y : event.angleDelta.y / 2;
|
||||
root.onWheelScroll(delta);
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: menuFlick
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.leftMargin: Style.marginM
|
||||
anchors.rightMargin: Style.marginM
|
||||
anchors.topMargin: Style.marginM
|
||||
height: root.scrollAreaHeight
|
||||
clip: true
|
||||
contentWidth: width
|
||||
contentHeight: scrollColumn.height
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
interactive: contentHeight > height
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
id: menuScrollBar
|
||||
policy: ScrollBar.AsNeeded
|
||||
visible: root.listOverflowing
|
||||
interactive: true
|
||||
hoverEnabled: true
|
||||
}
|
||||
|
||||
Column {
|
||||
id: scrollColumn
|
||||
width: menuFlick.width
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: root.scrollItems
|
||||
|
||||
Rectangle {
|
||||
readonly property bool isSeparator: modelData && modelData.separator === true
|
||||
width: scrollColumn.width
|
||||
height: root.rowHeightForItem(modelData)
|
||||
color: (!isSeparator && root.hoveredItem === index) ? Color.mHover : "transparent"
|
||||
radius: Style.radiusXS
|
||||
|
||||
Row {
|
||||
id: rowLayout
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Style.marginS
|
||||
anchors.rightMargin: Style.marginS
|
||||
spacing: Style.marginS
|
||||
visible: !isSeparator
|
||||
|
||||
NIcon {
|
||||
icon: modelData.icon
|
||||
pointSize: Style.fontSizeL
|
||||
color: root.hoveredItem === index ? Color.mOnHover : Color.mOnSurfaceVariant
|
||||
visible: icon !== ""
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: modelData.text
|
||||
pointSize: Style.fontSizeS
|
||||
color: root.hoveredItem === index ? Color.mOnHover : Color.mOnSurfaceVariant
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: rowLayout.width - ((modelData.icon && modelData.icon !== "") ? (Style.fontSizeL + Style.marginS) : 0)
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: !parent.isSeparator && root.isItemActionable(index)
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton
|
||||
|
||||
onEntered: {
|
||||
root.hoveredItem = index;
|
||||
}
|
||||
|
||||
onExited: {
|
||||
if (root.hoveredItem === index) {
|
||||
root.hoveredItem = -1;
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (root.isItemActionable(index)) {
|
||||
root.items[index].action.call();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: separator
|
||||
visible: root.splitExtendedLayout
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: menuFlick.bottom
|
||||
anchors.leftMargin: Style.marginS
|
||||
anchors.rightMargin: Style.marginS
|
||||
anchors.topMargin: Style.marginS
|
||||
height: Style.borderS
|
||||
color: Qt.alpha(Color.mOutline, 0.7)
|
||||
radius: Style.radiusXS
|
||||
}
|
||||
|
||||
Column {
|
||||
id: fixedColumn
|
||||
visible: root.splitExtendedLayout && root.fixedItems.length > 0
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.leftMargin: Style.marginM
|
||||
anchors.rightMargin: Style.marginM
|
||||
anchors.topMargin: Style.marginS
|
||||
anchors.bottomMargin: Style.marginM
|
||||
anchors.top: separator.bottom
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: root.fixedItems
|
||||
|
||||
Rectangle {
|
||||
id: fixedItemRect
|
||||
readonly property int globalIndex: root.fixedItemGlobalIndex(index)
|
||||
width: fixedColumn.width
|
||||
height: root.rowHeightForItem(modelData)
|
||||
color: root.hoveredItem === globalIndex ? Color.mHover : "transparent"
|
||||
radius: Style.radiusXS
|
||||
|
||||
Row {
|
||||
id: fixedRowLayout
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Style.marginS
|
||||
anchors.rightMargin: Style.marginS
|
||||
spacing: Style.marginS
|
||||
|
||||
NIcon {
|
||||
icon: modelData.icon
|
||||
pointSize: Style.fontSizeL
|
||||
color: root.hoveredItem === fixedItemRect.globalIndex ? Color.mOnHover : Color.mOnSurfaceVariant
|
||||
visible: icon !== ""
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: modelData.text
|
||||
pointSize: Style.fontSizeS
|
||||
color: root.hoveredItem === fixedItemRect.globalIndex ? Color.mOnHover : Color.mOnSurfaceVariant
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: fixedRowLayout.width - ((modelData.icon && modelData.icon !== "") ? (Style.fontSizeL + Style.marginS) : 0)
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.isItemActionable(parent.globalIndex)
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton
|
||||
|
||||
onEntered: {
|
||||
root.hoveredItem = parent.globalIndex;
|
||||
}
|
||||
|
||||
onExited: {
|
||||
if (root.hoveredItem === parent.globalIndex) {
|
||||
root.hoveredItem = -1;
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (root.isItemActionable(parent.globalIndex)) {
|
||||
root.items[parent.globalIndex].action.call();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user