This commit is contained in:
Ваше Имя
2025-06-30 23:19:29 +03:00
parent 12137d7e0d
commit ed9ba99412
195 changed files with 25445 additions and 1 deletions

View File

@@ -0,0 +1,142 @@
import QtQuick
import QtQuick.Controls
Canvas {
id: avatar
signal clicked
signal clickedOutside
property bool active: false
property string source: ""
property string shape: Config.avatarShape
property int squareRadius: Config.avatarBorderRadius === 0 ? 1 : Config.avatarBorderRadius // min: 1
property bool drawStroke: (active && Config.avatarActiveBorderSize > 0) || (!active && Config.avatarInactiveBorderSize > 0)
property color strokeColor: active ? Config.avatarActiveBorderColor : Config.avatarInactiveBorderColor
property int strokeSize: active ? Config.avatarActiveBorderSize : Config.avatarInactiveBorderSize
property string tooltipText: ""
property bool showTooltip: false
onSourceChanged: delayPaintTimer.running = true
onPaint: {
// FIX: Canvas zero dimension protection
if (width <= 0 || height <= 0)
return;
var ctx = getContext("2d");
ctx.reset(); // Clear previous drawing
ctx.beginPath();
if (shape === "square") {
// Squircle, actually
var r = width * squareRadius / 100;
ctx.moveTo(width - r, 0);
ctx.arcTo(width, 0, width, height, r);
ctx.arcTo(width, height, 0, height, r);
ctx.arcTo(0, height, 0, 0, r);
ctx.arcTo(0, 0, width, 0, r);
ctx.closePath();
} else {
// Circle
ctx.ellipse(0, 0, width, height);
}
ctx.clip();
if (source === "")
source = "../icons/user-default.png";
ctx.drawImage(source, 0, 0, width, height);
// Border
if (drawStroke) {
ctx.strokeStyle = strokeColor;
ctx.lineWidth = strokeSize;
ctx.stroke();
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.ArrowCursor
function isCursorInsideAvatar() {
if (!mouseArea.containsMouse)
return false;
if (avatar.shape === "square")
return true;
// Ellipse center and radius
var centerX = width / 2;
var centerY = height / 2;
var radiusX = centerX;
var radiusY = centerY;
// Distance from center
var dx = (mouseArea.mouseX - centerX) / radiusX;
var dy = (mouseArea.mouseY - centerY) / radiusY;
// Check if pointer is inside the ellipse
return (dx * dx + dy * dy) <= 1.0;
}
onReleased: function (mouse) {
var isInside = isCursorInsideAvatar();
if (isInside) {
avatar.clicked();
} else {
avatar.clickedOutside();
}
mouse.accepted = isInside;
}
function updateHover() {
if (isCursorInsideAvatar()) {
cursorShape = Qt.PointingHandCursor;
} else {
cursorShape = Qt.ArrowCursor;
}
}
onMouseXChanged: updateHover()
onMouseYChanged: updateHover()
ToolTip {
parent: mouseArea
enabled: Config.tooltipsEnable && !Config.tooltipsDisableUser
property bool shouldShow: enabled && avatar.showTooltip || (enabled && mouseArea.isCursorInsideAvatar() && avatar.tooltipText !== "")
visible: shouldShow
delay: 300
contentItem: Text {
font.family: Config.tooltipsFontFamily
font.pixelSize: Config.tooltipsFontSize
text: avatar.tooltipText
color: Config.tooltipsContentColor
}
background: Rectangle {
color: Config.tooltipsBackgroundColor
opacity: Config.tooltipsBackgroundOpacity
border.width: 0
radius: Config.tooltipsBorderRadius
}
}
}
// FIX: paint() not affect event if source is not empty in initialization
Timer {
id: delayPaintTimer
repeat: false
interval: 150
onTriggered: avatar.requestPaint()
running: true
}
// FIX: Critical timer memory leak prevention
// Overkill, but fine...
Component.onDestruction: {
if (delayPaintTimer) {
delayPaintTimer.running = false;
delayPaintTimer.stop();
}
}
}

View File

@@ -0,0 +1,311 @@
pragma Singleton
import QtQuick
/*
`config["option"]` is used in some places instead of `config.boolValue("option")` so we can default to `true`.
https://github.com/sddm/sddm/wiki/Theming#new-explicitly-typed-api-since-sddm-020
*/
QtObject {
// [General]
property bool enableAnimations: config['enable-animations'] === "false" ? false : true // @desc:Enable or disable all animations.
property string animatedBackgroundPlaceholder: config.stringValue("animated-background-placeholder") // @possible:File in `backgrounds/` @desc:An image file to be used as a placeholder for the animated background while it loads.
// [LockScreen]
property bool lockScreenDisplay: config['LockScreen/display'] === "false" ? false : true // @desc:Whether or not to display the lock screen. If false, the theme will load straight to the login screen.
property int lockScreenPaddingTop: config.intValue("LockScreen/padding-top") // @desc:Top padding of the lock screen. <br/>See also: <a href="#clockposition">Clock/position</a>, <a href="#lockmessageposition">Message/position</a>.
property int lockScreenPaddingRight: config.intValue("LockScreen/padding-right") // @desc:Right padding of the lock screen. <br/>See also: <a href="#clockposition">Clock/position</a>, <a href="#lockmessageposition">Message/position</a>.
property int lockScreenPaddingBottom: config.intValue("LockScreen/padding-bottom") // @desc:Bottom padding of the lock screen. <br/>See also: <a href="#clockposition">Clock/position</a>, <a href="#lockmessageposition">Message/position</a>.
property int lockScreenPaddingLeft: config.intValue("LockScreen/padding-left") // @desc:Left padding of the lock screen. <br/>See also: <a href="#clockposition">Clock/position</a>, <a href="#lockmessageposition">Message/position</a>.
property string lockScreenBackground: config.stringValue("LockScreen/background") || "default.jpg" // @possible:File in `backgrounds/` @desc:Background of the lock screen.<br/>Supported formats: jpg, png, avi, mp4, mov, mkv, m4v, webm. <strong>.gifs are not supported as they may cause SDDM to crash.</strong> <br/>See also: <a href="#animatedbackgroundplaceholder">animated-background-placeholder</a>
property bool lockScreenUseBackgroundColor: config.boolValue('LockScreen/use-background-color') // @desc:Whether or not to use a plain color as background of the lock screen instead of an image/video file.
property color lockScreenBackgroundColor: config.stringValue("LockScreen/background-color") || "#000000" // @desc:The color to be used as background of the lock screen. <br/>See also: <a href="#lockscreenusebackgroundcolor">use-background-color<a>
property int lockScreenBlur: config.intValue("LockScreen/blur") // @desc:Amount of blur to be applied to the background of the lock screen. 0 means no blur.
property real lockScreenBrightness: config.realValue("LockScreen/brightness") // @possible:-1.0 ≤ R ≤ 1.0 @desc:Brightness of the background of the lock screen. 0.0 leaves unchanged, -1.0 makes it black and 1.0 white.
// [LockScreen.Clock]
property bool clockDisplay: config['LockScreen.Clock/display'] === "false" ? false : true // @desc:Whether or not to display the clock in the lock screen.
property string clockPosition: config.stringValue("LockScreen.Clock/position") || "top-center" // @possible:'top-left' | 'top-center' | 'top-right' | 'center-left' | 'center' | 'center-right' | 'bottom-left' | 'bottom-center' | 'bottom-right' @desc:Position of the clock and date in the lock screen. <br />See also: <a href="#lockscreenpaddingtop">LockScreen/padding-top</a>
property string clockAlign: config.stringValue("LockScreen.Clock/align") || "center" // @possible:'left' | 'center' | 'right' @desc:Relative alignment of the clock and date.
property string clockFormat: config.stringValue("LockScreen.Clock/format") || "hh:mm" // @desc:Format string used for the clock.
property string clockFontFamily: config.stringValue("LockScreen.Clock/font-family") || "RedHatDisplay" // @desc:Font family used for the clock.
property int clockFontSize: config.intValue("LockScreen.Clock/font-size") || 70 // @desc:Font size of the clock.
property int clockFontWeight: config.intValue("LockScreen.Clock/font-weight") || 900 // @desc:Font weight of the clock. 400 = regular, 600 = bold, 800 = black
property color clockColor: config.stringValue("LockScreen.Clock/color") || "#FFFFFF" // @desc:Color of the clock.
// [LockScreen.Date]
property bool dateDisplay: config['LockScreen.Date/display'] === "false" ? false : true // @desc:Whether or not to display the date in the lock screen.
property string dateFormat: config.stringValue("LockScreen.Date/format") || "dddd, MMMM dd, yyyy" // @desc:Format string used for the date.
property string dateFontFamily: config.stringValue("LockScreen.Date/font-family") || "RedHatDisplay" // @desc:Font family used for the date.
property int dateFontSize: config.intValue("LockScreen.Date/font-size") || 14 // @desc:Font size of the date.
property int dateFontWeight: config.intValue("LockScreen.Date/font-weight") || 400 // @desc:Font weight of the date. 400 = regular, 600 = bold, 800 = black
property color dateColor: config.stringValue("LockScreen.Date/color") || "#FFFFFF" // @desc:Color of the date.
property int dateMarginTop: config.intValue("LockScreen.Date/margin-top") // @desc:Top margin from the clock
// [LockScreen.Message]
property bool lockMessageDisplay: config['LockScreen.Message/display'] === "false" ? false : true // @desc:Whether or not to display the custom message in the lock screen.
property string lockMessagePosition: config.stringValue("LockScreen.Message/position") || "bottom-center" // @possible:'top-left' | 'top-center' | 'top-right' | 'center-left' | 'center' | 'center-right' | 'bottom-left' | 'bottom-center' | 'bottom-right' @desc:Position of the custom message in the lock screen. <br />See also: <a href="#lockscreenpaddingtop">LockScreen/padding-top</a>
property string lockMessageAlign: config.stringValue("LockScreen.Message/align") || "center" // @possible:'left' | 'center' | 'right' @desc:Relative alignment of the custom message and its icon.
property string lockMessageText: config.stringValue("LockScreen.Message/text") || "Press any key" // @desc:Text of the custom message.
property string lockMessageFontFamily: config.stringValue("LockScreen.Message/font-family") || "RedHatDisplay" // @desc:Font family used for the custom message.
property int lockMessageFontSize: config.intValue("LockScreen.Message/font-size") || 12 // @desc:Font size of the custom message.
property int lockMessageFontWeight: config.intValue("LockScreen.Message/font-weight") || 400 // @desc:Font weight of the date. 400 = regular, 600 = bold, 800 = black
property bool lockMessageDisplayIcon: config['LockScreen.Message/display-icon'] === "false" ? false : true // @desc:Show or hide the icon above the message.
property string lockMessageIcon: config.stringValue("LockScreen.Message/icon") || "enter.svg" // @possible:File in `icons/` @desc:Icon above the custom message.
property int lockMessageIconSize: config.intValue("LockScreen.Message/icon-size") || 16 // @desc:Size of the custom message's icon.
property color lockMessageColor: config.stringValue("LockScreen.Message/color") || "#FFFFFF" // @desc:Color of the custom message.
property bool lockMessagePaintIcon: config['LockScreen.Message/paint-icon'] === "false" ? false : true // @desc:Whether or not to paint the icon with the same color as the text.
property int lockMessageSpacing: config.intValue("LockScreen.Message/spacing") // @desc:Spacing between the icon and the text in the custom message.
// [LoginScreen]
property string loginScreenBackground: config.stringValue("LoginScreen/background") || "default.jpg" // @possible:File in `backgrounds/` @desc:Background of the login screen.<br/>Supported formats: jpg, png, avi, mp4, mov, mkv, m4v, webm. <strong>.gifs are not supported as they may cause SDDM to crash.</strong> <br/>See also: <a href="#animatedbackgroundplaceholder">animated-background-placeholder</a>
property bool loginScreenUseBackgroundColor: config.boolValue('LoginScreen/use-background-color') // @desc:Whether or not to use a plain color as background of the login screen instead of an image/video file.
property color loginScreenBackgroundColor: config.stringValue("LoginScreen/background-color") || "#000000" // @desc:The color to be used as background of the login screen. <br/>See also: <a href="#loginscreenusebackgroundcolor">use-background-color<a>
property int loginScreenBlur: config.intValue("LoginScreen/blur") // @desc:Amount of blur to be applied to the background of the login screen. 0 means no blur.
property real loginScreenBrightness: config.realValue("LoginScreen/brightness") // @possible:-1.0 ≤ R ≤ 1.0 @desc:Brightness of the background of the login screen. 0.0 leaves unchanged, -1.0 makes it black and 1.0 white.
// [LoginScreen.LoginArea]
property string loginAreaPosition: config.stringValue("LoginScreen.LoginArea/position") || "center" // @possible:'left' | 'center' | 'right' @desc:Position of the login area.
property int loginAreaMargin: config.intValue("LoginScreen.LoginArea/margin") // @desc:Margin of the login area relative to its anchor point.<br/>If position is set to `center`, this option specifies the top margin, left/right margin otherwise.<br/><br/><strong>Set this option to `-1` to center the login area.</strong>
// [LoginScreen.LoginArea.Avatar]
property string avatarShape: config.stringValue("LoginScreen.LoginArea.Avatar/shape") || "circle" // @possible:'circle' || 'square' @desc:Shape of the avatar. <br/>See also: <a href="#avatarborderradius">border-radius<a>
property int avatarBorderRadius: config.intValue("LoginScreen.LoginArea.Avatar/border-radius") // @desc:Border radius of the 'square' avatar. <br/>See also: <a href="#avatarshape">shape<a>
property int avatarActiveSize: config.intValue("LoginScreen.LoginArea.Avatar/active-size") || 120 // @desc:Size of the selected user's avatar.
property int avatarInactiveSize: config.intValue("LoginScreen.LoginArea.Avatar/inactive-size") || 80 // @desc:Size of the non-selected user avatars.
property real avatarInactiveOpacity: config.realValue("LoginScreen.LoginArea.Avatar/inactive-opacity") || 0.35 // @possible:0.0 ≤ R ≤ 1.0 @desc:Opacity of the non-selected avatars.
property int avatarActiveBorderSize: config.intValue("LoginScreen.LoginArea.Avatar/active-border-size") // @desc:Border size of the selected user's avatar.
property int avatarInactiveBorderSize: config.intValue("LoginScreen.LoginArea.Avatar/inactive-border-size") // @desc:Border size of the non-selected avatars.
property color avatarActiveBorderColor: config.stringValue("LoginScreen.LoginArea.Avatar/active-border-color") || "#FFFFFF" // @desc:Border color of the selected user's avatar.
property color avatarInactiveBorderColor: config.stringValue("LoginScreen.LoginArea.Avatar/inactive-border-color") || "#FFFFFF" // @desc:Border color of the non-selected avatars.
// [LoginScreen.LoginArea.Username]
property string usernameFontFamily: config.stringValue("LoginScreen.LoginArea.Username/font-family") || "RedHatDisplay" // @desc:Font family used for the username.
property int usernameFontSize: config.intValue("LoginScreen.LoginArea.Username/font-size") || 16 // @desc:Font size of the username.
property int usernameFontWeight: config.intValue("LoginScreen.LoginArea.Username/font-weight") || 900 // @desc:Font weight of the username. 400 = regular, 600 = bold, 800 = black
property color usernameColor: config.stringValue("LoginScreen.LoginArea.Username/color") || "#FFFFFF" // @desc:Color of the username.
property int usernameMargin: config.intValue("LoginScreen.LoginArea.Username/margin") // @desc:Distance of the username from the avatar.
// [LoginScreen.LoginArea.PasswordInput]
property int passwordInputWidth: config.intValue("LoginScreen.LoginArea.PasswordInput/width") || 200 // @desc:Width of the password field.
property int passwordInputHeight: config.intValue("LoginScreen.LoginArea.PasswordInput/height") || 30 // @desc:Height of the password field. This option also defines the size of the login button.
property bool passwordInputDisplayIcon: config['LoginScreen.LoginArea.PasswordInput/display-icon'] === "false" ? false : true // @desc:Whether or not to display the icon in the password field.
property string passwordInputFontFamily: config.stringValue("LoginScreen.LoginArea.PasswordInput/font-family") || "RedHatDisplay" // @desc:Font family of the password field.
property int passwordInputFontSize: config.intValue("LoginScreen.LoginArea.PasswordInput/font-size") || 12 // @desc:Font size of the password field.
property string passwordInputIcon: config.stringValue("LoginScreen.LoginArea.PasswordInput/icon") || "password.svg" // @possible:File in `icons/` @desc:Icon in the password field.
property int passwordInputIconSize: config.intValue("LoginScreen.LoginArea.PasswordInput/icon-size") || 16 // @desc:Size of the icon inside the password field.
property color passwordInputContentColor: config.stringValue("LoginScreen.LoginArea.PasswordInput/content-color") || "#FFFFFF" // @desc:Color of text/icon in the password field.
property color passwordInputBackgroundColor: config.stringValue("LoginScreen.LoginArea.PasswordInput/background-color") || "#FFFFFF" // @desc:Background color of the password field.
property real passwordInputBackgroundOpacity: config.realValue("LoginScreen.LoginArea.PasswordInput/background-opacity") // @possible:0.0 ≤ R ≤ 1.0 @desc:Opacity of the background of the password field.
property int passwordInputBorderSize: config.intValue("LoginScreen.LoginArea.PasswordInput/border-size") // @desc:Size of the border of the password field.
property color passwordInputBorderColor: config.stringValue("LoginScreen.LoginArea.PasswordInput/border-color") || "#FFFFFF" // @desc:Color of the border of the password field.
property int passwordInputBorderRadiusLeft: config.intValue("LoginScreen.LoginArea.PasswordInput/border-radius-left") // @desc:Left border radius of the password field.
property int passwordInputBorderRadiusRight: config.intValue("LoginScreen.LoginArea.PasswordInput/border-radius-right") // @desc:Right border radius of the password field.
property int passwordInputMarginTop: config.intValue("LoginScreen.LoginArea.PasswordInput/margin-top") // @desc:Distance of the password field/login button from the username.
// [LoginScreen.LoginArea.LoginButton]
property color loginButtonBackgroundColor: config.stringValue("LoginScreen.LoginArea.LoginButton/background-color") || "#FFFFFF" // @desc:Background color of the login button.
property real loginButtonBackgroundOpacity: config.realValue("LoginScreen.LoginArea.LoginButton/background-opacity") // @possible:0.0 ≤ R ≤ 1.0 @desc:Opacity of the background of the login button.
property color loginButtonActiveBackgroundColor: config.stringValue("LoginScreen.LoginArea.LoginButton/active-background-color") || "#FFFFFF" // @desc:Background color of the login button when hovered/focused.
property real loginButtonActiveBackgroundOpacity: config.realValue("LoginScreen.LoginArea.LoginButton/active-background-opacity") // @possible:0.0 ≤ R ≤ 1.0 @desc:Opacity of the background of the login button when hovered/focused.
property string loginButtonIcon: config.stringValue("LoginScreen.LoginArea.LoginButton/icon") || "arrow-right.svg" // @possible:File in `icons/` @desc:Icon in the login button
property int loginButtonIconSize: config.intValue("LoginScreen.LoginArea.LoginButton/icon-size") || 18 // @desc:Size of the icon in the login button.
property color loginButtonContentColor: config.stringValue("LoginScreen.LoginArea.LoginButton/content-color") || "#FFFFFF" // @desc:Color of the icon/text in the login button.
property color loginButtonActiveContentColor: config.stringValue("LoginScreen.LoginArea.LoginButton/active-content-color") || "#FFFFFF" // @desc:Color of the icon/text in the login button when hovered/focused.
property int loginButtonBorderSize: config.intValue("LoginScreen.LoginArea.LoginButton/border-size") // @desc:Border size of the login button.
property color loginButtonBorderColor: config.stringValue("LoginScreen.LoginArea.LoginButton/border-color") || "#FFFFFF" // @desc:Border color of the login button.
property int loginButtonBorderRadiusLeft: config.intValue("LoginScreen.LoginArea.LoginButton/border-radius-left") // @desc:Left border radius of the login button.
property int loginButtonBorderRadiusRight: config.intValue("LoginScreen.LoginArea.LoginButton/border-radius-right") // @desc:Right border radius of the login button.
property int loginButtonMarginLeft: config.intValue("LoginScreen.LoginArea.LoginButton/margin-left") // @desc:Distance of the login button from the password field.
property bool loginButtonShowTextIfNoPassword: config['LoginScreen.LoginArea.LoginButton/show-text-if-no-password'] === "false" ? false : true // @desc:Whether or not to show a label in the login button when the password field is not visible.
property bool loginButtonHideIfNotNeeded: config.boolValue("LoginScreen.LoginArea.LoginButton/hide-if-not-needed") // @desc:Whether or not to hide the login button if the password field is visible. You can still log-in with [enter].
property string loginButtonFontFamily: config.stringValue("LoginScreen.LoginArea.LoginButton/font-family") || "RedHatDisplay" // @desc:Font family of the label of the login button/
property int loginButtonFontSize: config.intValue("LoginScreen.LoginArea.LoginButton/font-size") || 12 // @desc:Font size of the label of the login button.
property int loginButtonFontWeight: config.intValue("LoginScreen.LoginArea.LoginButton/font-weight") || 600 // @desc:Font weight of the label of the login button. 400 = regular, 600 = bold, 800 = black
// [LoginScreen.LoginArea.Spinner]
property bool spinnerDisplayText: config['LoginScreen.LoginArea.Spinner/display-text'] === "false" ? false : true // @desc:Whether or not to display the text with the spinning icon.
property string spinnerText: config.stringValue("LoginScreen.LoginArea.Spinner/text") || "Logging in" // @desc:Text to be displayed with the spinning icon.
property string spinnerFontFamily: config.stringValue("LoginScreen.LoginArea.Spinner/font-family") || "RedHatDisplay" // @desc:Font family of the text to be displayed with the spinning icon.
property int spinnerFontWeight: config.intValue("LoginScreen.LoginArea.Spinner/font-weight") || 600 // @desc:Font weight of the text to be displayed with the spinning icon. 400 = regular, 600 = bold, 800 = black
property int spinnerFontSize: config.intValue("LoginScreen.LoginArea.Spinner/font-size") || 12 // @desc:Font size of the spinner's text.
property int spinnerIconSize: config.intValue("LoginScreen.LoginArea.Spinner/icon-size") || 32 // @desc:Size of the spinning icon.
property string spinnerIcon: config.stringValue("LoginScreen.LoginArea.Spinner/icon") || "spinner.svg" // @possible:File in `icons/` @desc:Spinning icon.
property color spinnerColor: config.stringValue("LoginScreen.LoginArea.Spinner/color") || "#FFFFFF" // @desc:Color of the spinning icon and its text.
property int spinnerSpacing: config.intValue("LoginScreen.LoginArea.Spinner/spacing") // @desc:Spacing between the spinning icon and its text.
// [LoginScreen.LoginArea.WarningMessage]
property string warningMessageFontFamily: config.stringValue("LoginScreen.LoginArea.WarningMessage/font-family") || "RedHatDisplay" // @desc:Font family of the warning message.
property int warningMessageFontSize: config.intValue("LoginScreen.LoginArea.WarningMessage/font-size") || 11 // @desc:Font size of the warning message.
property int warningMessageFontWeight: config.intValue("LoginScreen.LoginArea.WarningMessage/font-weight") || 400 // @desc:Font weight of the warning message. 400 = regular, 600 = bold, 800 = black
property color warningMessageNormalColor: config.stringValue("LoginScreen.LoginArea.WarningMessage/normal-color") || "#FFFFFF" // @desc:Color of the warning message for normal messages.
property color warningMessageWarningColor: config.stringValue("LoginScreen.LoginArea.WarningMessage/warning-color") || "#FFFFFF" // @desc:Color of the warning message for warnings.
property color warningMessageErrorColor: config.stringValue("LoginScreen.LoginArea.WarningMessage/error-color") || "#FFFFFF" // @desc:Color of the warning message for error messages.
property int warningMessageMarginTop: config.intValue("LoginScreen.LoginArea.WarningMessage/margin-top") // @desc:Distance of the warning message from the password field/login button.
// [LoginScreen.MenuArea.Buttons]
property int menuAreaButtonsMarginTop: config.intValue("LoginScreen.MenuArea.Buttons/margin-top") // @desc:Top margin of the menu buttons.
property int menuAreaButtonsMarginRight: config.intValue("LoginScreen.MenuArea.Buttons/margin-right") // @desc:Right margin of the menu buttons.
property int menuAreaButtonsMarginBottom: config.intValue("LoginScreen.MenuArea.Buttons/margin-bottom") // @desc:Bottom margin of the menu buttons.
property int menuAreaButtonsMarginLeft: config.intValue("LoginScreen.MenuArea.Buttons/margin-left") // @desc:Left margin of the menu buttons.
property int menuAreaButtonsSize: config.intValue("LoginScreen.MenuArea.Buttons/size") || 30 // @desc:Size of the menu buttons.
property int menuAreaButtonsBorderRadius: config.intValue("LoginScreen.MenuArea.Buttons/border-radius") // @desc:Border radius of the menu buttons.
property int menuAreaButtonsSpacing: config.intValue("LoginScreen.MenuArea.Buttons/spacing") // @desc:Spacing between menu buttons placed in the same position.
property string menuAreaButtonsFontFamily: config.stringValue("LoginScreen.MenuArea.Buttons/font-family") || "RedHatDisplay" // @desc:Font family of the menu buttons.
// [LoginScreen.MenuArea.Popups]
property int menuAreaPopupsMaxHeight: config.intValue("LoginScreen.MenuArea.Popups/max-height") || 300 // @desc:Max height of the popups.
property int menuAreaPopupsItemHeight: config.intValue("LoginScreen.MenuArea.Popups/item-height") || 30 // @desc:Height of the items inside a popup.
property int menuAreaPopupsSpacing: config.intValue("LoginScreen.MenuArea.Popups/item-spacing") // @desc:Spacing between items inside a popup.
property int menuAreaPopupsPadding: config.intValue("LoginScreen.MenuArea.Popups/padding") // @desc:Padding of the popups.
property bool menuAreaPopupsDisplayScrollbar: config["LoginScreen.MenuArea.Popups/display-scrollbar"] === "false" ? false : true // @desc:Whether or not to display a scrollbar in the popups if its items don't fit.
property int menuAreaPopupsMargin: config.intValue("LoginScreen.MenuArea.Popups/margin") // @desc:Distance of the popup from its button.
property color menuAreaPopupsBackgroundColor: config.stringValue("LoginScreen.MenuArea.Popups/background-color") || "#FFFFFF" // @desc:Background color of the popups.
property real menuAreaPopupsBackgroundOpacity: config.realValue("LoginScreen.MenuArea.Popups/background-opacity") // @possible:0.0 ≤ R ≤ 1.0 @desc:Opacity of the background of the popups.
property color menuAreaPopupsActiveOptionBackgroundColor: config.stringValue("LoginScreen.MenuArea.Popups/active-option-background-color") || "#FFFFFF" // @desc:Background color of the hovered/focused item in the popup.
property real menuAreaPopupsActiveOptionBackgroundOpacity: config.realValue("LoginScreen.MenuArea.Popups/active-option-background-opacity") // @possible:0.0 ≤ R ≤ 1.0 @desc:Opacity of the background of the hovered/focused item in the popup.
property color menuAreaPopupsContentColor: config.stringValue("LoginScreen.MenuArea.Popups/content-color") || "#FFFFFF" // @desc:Color of the text of the non-selected items in the popup.
property color menuAreaPopupsActiveContentColor: config.stringValue("LoginScreen.MenuArea.Popups/active-content-color") || "#FFFFFF" // @desc:Color of the text of the hovered/focused item in the popup.
property string menuAreaPopupsFontFamily: config.stringValue("LoginScreen.MenuArea.Popups/font-family") || "RedHatDisplay" // @desc:Font family of the popups.
property int menuAreaPopupsBorderSize: config.intValue("LoginScreen.MenuArea.Popups/border-size") // @desc:Size of the border of the popups.
property color menuAreaPopupsBorderColor: config.stringValue("LoginScreen.MenuArea.Popups/border-color") || "#FFFFFF" // @desc:Color of the border of the popups.
property int menuAreaPopupsFontSize: config.intValue("LoginScreen.MenuArea.Popups/font-size") || 11 // @desc:Font size of the popups.
property int menuAreaPopupsIconSize: config.intValue("LoginScreen.MenuArea.Popups/icon-size") || 16 // @desc:Size of the icons in the popups.
// [LoginScreen.MenuArea.Session]
property bool sessionDisplay: config["LoginScreen.MenuArea.Session/display"] === "false" ? false : true // @desc:Whether or not to display the session button.
property string sessionPosition: config.stringValue("LoginScreen.MenuArea.Session/position") // @possible:'top-left' | 'top-center' | 'top-right' | 'center-left' | 'center-right' | 'bottom-left' | 'bottom-center' | 'bottom-right' @default:bottom-left @desc:Position of the session button.
property int sessionIndex: config.intValue("LoginScreen.MenuArea.Session/index") // @default:0 @desc:This number is used to sort menu buttons placed in the same position.
property string sessionPopupDirection: config.stringValue("LoginScreen.MenuArea.Session/popup-direction") || "up" // @possible:'up' | 'down' | 'left' | 'right' @desc:Which direction to open the session popup to.
property string sessionPopupAlign: config.stringValue("LoginScreen.MenuArea.Session/popup-align") || "center" // @possible:'start' | 'center' | 'end' // @desc:Alignment of the session popup.
property bool sessionDisplaySessionName: config['LoginScreen.MenuArea.Session/display-session-name'] === "false" ? false : true // @desc:Whether or not to display the name of the current session in the session button.
property int sessionButtonWidth: config.intValue("LoginScreen.MenuArea.Session/button-width") || 200 // @desc:Width of the session button. Set this to '-1' to make it the same as its contents. <br/>This option is not applied if 'display-session-name' is set to true.
property int sessionPopupWidth: config.intValue("LoginScreen.MenuArea.Session/popup-width") || 200 // @desc:Width of the session popup.
property color sessionBackgroundColor: config.stringValue("LoginScreen.MenuArea.Session/background-color") || "#FFFFFF" // @desc:Background color of the session button.
property real sessionBackgroundOpacity: config.realValue("LoginScreen.MenuArea.Session/background-opacity") // @possible:0.0 ≤ R ≤ 1.0 @desc:Opacity of the background of the session button.
property real sessionActiveBackgroundOpacity: config.realValue("LoginScreen.MenuArea.Session/active-background-opacity") // @possible:0.0 ≤ R ≤ 1.0 @desc:Opacity of the background of the session button when hovered/focused.
property color sessionContentColor: config.stringValue("LoginScreen.MenuArea.Session/content-color") || "#FFFFFF" // @desc:Color of the icon and text in the session button.
property color sessionActiveContentColor: config.stringValue("LoginScreen.MenuArea.Session/active-content-color") || "#FFFFFF" // @desc:Color of the icon and text in the session button when hovered/focused.
property int sessionBorderSize: config.intValue("LoginScreen.MenuArea.Session/border-size") // @desc:Size of the border of the session button. The color of the border is defined by 'content-color' and 'active-content-color'.
property int sessionFontSize: config.intValue("LoginScreen.MenuArea.Session/font-size") || 10 // @desc:Font size of the session button.
property int sessionIconSize: config.intValue("LoginScreen.MenuArea.Session/icon-size") || 16 // @desc:Size of the icon in the session button.
// [LoginScreen.MenuArea.Layout]
property bool layoutDisplay: config["LoginScreen.MenuArea.Layout/display"] === "false" ? false : true // @desc:Whether or not to display the layout button.
property string layoutPosition: config.stringValue("LoginScreen.MenuArea.Layout/position") // @possible:'top-left' | 'top-center' | 'top-right' | 'center-left' | 'center-right' | 'bottom-left' | 'bottom-center' | 'bottom-right' @default:bottom-right @desc:Position of the layout button.
property int layoutIndex: config.intValue("LoginScreen.MenuArea.Layout/index") // @default:1 @desc:This number is used to sort menu buttons placed in the same position.
property string layoutPopupDirection: config.stringValue("LoginScreen.MenuArea.Layout/popup-direction") || "up" // @possible:'up' | 'down' | 'left' | 'right' @desc:Which direction to open the layout popup to.
property string layoutPopupAlign: config.stringValue("LoginScreen.MenuArea.Layout/popup-align") || "center" // @possible:'start' | 'center' | 'end' @desc:Alignment of the session popup.
property int layoutPopupWidth: config.intValue("LoginScreen.MenuArea.Layout/popup-width") || 180 // @desc:Width of the layout popup.
property bool layoutDisplayLayoutName: config['LoginScreen.MenuArea.Layout/display-layout-name'] === "false" ? false : true // @desc:Whether or not to display the country code of the current layout in the layout button.
property color layoutBackgroundColor: config.stringValue("LoginScreen.MenuArea.Layout/background-color") || "#FFFFFF" // @desc:Background color of the layout button.
property real layoutBackgroundOpacity: config.realValue("LoginScreen.MenuArea.Layout/background-opacity") // @possible:0.0 ≤ R ≤ 1.0 @desc:Opacity of the background of the layout button.
property real layoutActiveBackgroundOpacity: config.realValue("LoginScreen.MenuArea.Layout/active-background-opacity") // @possible:0.0 ≤ R ≤ 1.0 @desc:Opacity of the background of the layout button when hovered/focused.
property color layoutContentColor: config.stringValue("LoginScreen.MenuArea.Layout/content-color") || "#FFFFFF" // @desc:Color of the icon and text in the layout button.
property color layoutActiveContentColor: config.stringValue("LoginScreen.MenuArea.Layout/active-content-color") || "#FFFFFF" // @desc:Color of the icon and text in the layout button when hovered/focused.
property int layoutBorderSize: config.intValue("LoginScreen.MenuArea.Layout/border-size") // @desc:Size of the border of the layouts button. The color of the border is defined by 'content-color' and 'active-content-color'.
property int layoutFontSize: config.intValue("LoginScreen.MenuArea.Layout/font-size") || 10 // @desc:Font size of the layout button.
property string layoutIcon: config.stringValue("LoginScreen.MenuArea.Layout/icon") || "language.svg" // @possible:File in `icons/` @desc:Icon in the layout button.
property int layoutIconSize: config.intValue("LoginScreen.MenuArea.Layout/icon-size") || 16 // @desc:Size of the icon in the layout button.
// [LoginScreen.MenuArea.Keyboard]
property bool keyboardDisplay: config["LoginScreen.MenuArea.Keyboard/display"] === "false" ? false : true // @desc:Whether or not to display the virtual keyboard toggle button.
property string keyboardPosition: config.stringValue("LoginScreen.MenuArea.Keyboard/position") // @possible:'top-left' | 'top-center' | 'top-right' | 'center-left' | 'center-right' | 'bottom-left' | 'bottom-center' | 'bottom-right' @default:bottom-right @desc:Position of the virtual keyboard toggle button.
property int keyboardIndex: config.intValue("LoginScreen.MenuArea.Keyboard/index") // @default:2 @desc:This number is used to sort menu buttons placed in the same position.
property color keyboardBackgroundColor: config.stringValue("LoginScreen.MenuArea.Keyboard/background-color") || "#FFFFFF" // @desc:Background color of the virtual keyboard toggle button.
property real keyboardBackgroundOpacity: config.realValue("LoginScreen.MenuArea.Keyboard/background-opacity") // @possible:0.0 ≤ R ≤ 1.0 @desc:Opacity of the background of the virtual keyboard toggle button
property real keyboardActiveBackgroundOpacity: config.realValue("LoginScreen.MenuArea.Keyboard/active-background-opacity") // @possible:0.0 ≤ R ≤ 1.0 @desc:Opacity of the background of the virtual keyboard toggle button when hovered/focused.
property color keyboardContentColor: config.stringValue("LoginScreen.MenuArea.Keyboard/content-color") || "#FFFFFF" // @desc:Color of the icon in the virtual keyboard toggle button.
property color keyboardActiveContentColor: config.stringValue("LoginScreen.MenuArea.Keyboard/active-content-color") || "#FFFFFF" // @desc:Color of the icon in the virtual keyboard toggle button when hovered/focused.
property int keyboardBorderSize: config.intValue("LoginScreen.MenuArea.Keyboard/border-size") // @desc:Border size of the virtual keyboard toggle button. The color of the border is defined by 'content-color' and 'active-content-color'.
property string keyboardIcon: config.stringValue("LoginScreen.MenuArea.Keyboard/icon") || "keyboard.svg" // @possible:File in `icons/` @desc:Icon in the virtual keyboard toggle button.
property int keyboardIconSize: config.intValue("LoginScreen.MenuArea.Keyboard/icon-size") || 16 // @desc:Size of the icon in the virtual keyboard toggle button.
// [LoginScreen.MenuArea.Power]
property bool powerDisplay: config["LoginScreen.MenuArea.Power/display"] === "false" ? false : true // @desc:Whether or not to display the power button.
property string powerPosition: config.stringValue("LoginScreen.MenuArea.Power/position") // @possible:'top-left' | 'top-center' | 'top-right' | 'center-left' | 'center-right' | 'bottom-left' | 'bottom-center' | 'bottom-right' @default:bottom-right @desc:Position of the power button.
property int powerIndex: config.intValue("LoginScreen.MenuArea.Power/index") // @default:3 @desc:This number is used to sort menu buttons placed in the same position.
property string powerPopupDirection: config.stringValue("LoginScreen.MenuArea.Power/popup-direction") || "up" // @possible:'up' | 'down' | 'left' | 'right' @desc:Which direction to open the power popup to.
property string powerPopupAlign: config.stringValue("LoginScreen.MenuArea.Power/popup-align") || "center" // @possible:'start' | 'center' | 'end' @Alignment of the power popup.
property int powerPopupWidth: config.intValue("LoginScreen.MenuArea.Power/popup-width") || 90 // @desc:Width of the power popup.
property color powerBackgroundColor: config.stringValue("LoginScreen.MenuArea.Power/background-color") || "#FFFFFF" // @desc:Background color of the power button.
property real powerBackgroundOpacity: config.realValue("LoginScreen.MenuArea.Power/background-opacity") // @possible:0.0 ≤ R ≤ 1.0 @desc:Opacity of the background of the power button.
property real powerActiveBackgroundOpacity: config.realValue("LoginScreen.MenuArea.Power/active-background-opacity") // @possible:0.0 ≤ R ≤ 1.0 @desc:Opacity of the background of the power button when hovered/focused.
property color powerContentColor: config.stringValue("LoginScreen.MenuArea.Power/content-color") || "#FFFFFF" // @desc:Color of the icon in the power button.
property color powerActiveContentColor: config.stringValue("LoginScreen.MenuArea.Power/active-content-color") || "#FFFFFF" // @desc:Color of the icon in the power button when hovered/focused.
property int powerBorderSize: config.intValue("LoginScreen.MenuArea.Power/border-size") // @desc:Border size of the power button. The color of the border is defined by 'content-color' and 'active-content-color'.
property string powerIcon: config.stringValue("LoginScreen.MenuArea.Power/icon") || "power.svg" // @possible:File in `icons/` @desc:Icon in the power button.
property int powerIconSize: config.intValue("LoginScreen.MenuArea.Power/icon-size") || 16 // @desc:Size of the icon in the power button.
// [LoginScreen.VirtualKeyboard]
property int virtualKeyboardScale: config.realValue("LoginScreen.VirtualKeyboard/scale") || 1.0 // @desc:Scale of the virtual keyboard.
property string virtualKeyboardPosition: config.stringValue("LoginScreen.VirtualKeyboard/position") || "login" // @possible: 'login' | 'top' | 'bottom' | 'left' | 'right' @desc:Initial position of the virtual keyboard. You can drag the keyboard using the middle mouse button.
property bool virtualKeyboardStartHidden: config['LoginScreen.VirtualKeyboard/start-hidden'] === "false" ? false : true // @desc:Whether or not the virtual keyboard should start hidden.
property color virtualKeyboardBackgroundColor: config.stringValue("LoginScreen.VirtualKeyboard/background-color") || "#FFFFFF" // @desc:Color of the background of the virtual keyboard.
property real virtualKeyboardBackgroundOpacity: config.realValue("LoginScreen.VirtualKeyboard/background-opacity") // @possible:0.0 ≤ R ≤ 1.0 @desc:Opacity of the background of the virtual keyboard.
property color virtualKeyboardKeyContentColor: config.stringValue("LoginScreen.VirtualKeyboard/key-content-color") || "#FFFFFF" // @desc:Color of the keys' text/icon in the virtual keyboard.
property color virtualKeyboardKeyColor: config.stringValue("LoginScreen.VirtualKeyboard/key-color") || "#FFFFFF" // @desc:Color of the background of the keys in the virtual keyboard.
property real virtualKeyboardKeyOpacity: config.realValue("LoginScreen.VirtualKeyboard/key-opacity") // @possible:0.0 ≤ R ≤ 1.0 @desc:Opacity of the background of the keys in the virtual keybaord.
property color virtualKeyboardKeyActiveBackgroundColor: config.stringValue("LoginScreen.VirtualKeyboard/key-active-background-color") || "#FFFFFF" // @desc:Color of the background of the special keys in the virtual keyboard.
property real virtualKeyboardKeyActiveOpacity: config.realValue("LoginScreen.VirtualKeyboard/key-active-opacity") // @possible:0.0 ≤ R ≤ 1.0 @desc:Opacity of the background of the special keys in the virtual keyboard.
property color virtualKeyboardSelectionBackgroundColor: config.stringValue("LoginScreen.VirtualKeyboard/selection-background-color") || "#CCCCCC" // @desc:Color of the background of the selected character in the virtual keyboard.
property color virtualKeyboardSelectionContentColor: config.stringValue("LoginScreen.VirtualKeyboard/selection-content-color") || "#FFFFFF" // @desc:Color of the text of the selected character in the virtual keyboard.
property color virtualKeyboardPrimaryColor: config.stringValue("LoginScreen.VirtualKeyboard/primary-color") || "#000000" // @desc:Color of the icon/text in special keys when they're active.
property int virtualKeyboardBorderSize: config.intValue("LoginScreen.VirtualKeyboard/border-size") // @desc:Border size of the virtual keyboard and its keys.
property color virtualKeyboardBorderColor: config.stringValue("LoginScreen.VirtualKeyboard/border-color") || "#000000" // @desc:Color of the border of the virtual keyboard and its keys.
// [Tooltips]
property bool tooltipsEnable: config['Tooltips/enable'] === "false" ? false : true // @desc:Whether or not to show tooltips when hovering over buttons.
property string tooltipsFontFamily: config.stringValue("Tooltips/font-family") || "RedHatDisplay" // @desc:Font family of the tooltips.
property int tooltipsFontSize: config.intValue("Tooltips/font-size") || 11 // @desc:Font size of the tooltips.
property color tooltipsContentColor: config.stringValue("Tooltips/content-color") || "#FFFFFF" // @desc:Color of the text in tooltips.
property color tooltipsBackgroundColor: config.stringValue("Tooltips/background-color") || "#FFFFFF" // @desc:Color of the background of the tooltips.
property real tooltipsBackgroundOpacity: config.realValue("Tooltips/background-opacity") // @possible:0.0 ≤ R ≤ 1.0 @desc:Opacity of the background of the tooltips.
property int tooltipsBorderRadius: config.intValue("Tooltips/border-radius") || 5 // @desc:Border radius of the tooltips.
property bool tooltipsDisableUser: config.boolValue("Tooltips/disable-user") // @desc:If false, disables only the tooltip for the user selector.
property bool tooltipsDisableLoginButton: config.boolValue("Tooltips/disable-login-button") // @desc:If false, disabled only the tooltip for the login button.
function sortMenuButtons() {
var menus = [];
var available_positions = ["top-left", "top-center", "top-right", "center-left", "center-right", "bottom-left", "bottom-center", "bottom-right"];
if (sessionDisplay)
menus.push({
name: "session",
index: sessionIndex,
def_index: 0,
position: available_positions.includes(sessionPosition) ? sessionPosition : "bottom-left"
});
if (layoutDisplay)
menus.push({
name: "layout",
index: layoutIndex,
def_index: 1,
position: available_positions.includes(layoutPosition) ? layoutPosition : "bottom-right"
});
if (keyboardDisplay)
menus.push({
name: "keyboard",
index: keyboardIndex,
def_index: 2,
position: available_positions.includes(keyboardPosition) ? keyboardPosition : "bottom-right"
});
if (powerDisplay)
menus.push({
name: "power",
index: powerIndex,
def_index: 3,
position: available_positions.includes(powerPosition) ? powerPosition : "bottom-right"
});
// Sort by index or default index if 0
return menus.sort((c, n) => c.index - n.index || c.def_index - n.def_index);
}
function getIcon(iconName) {
return `../icons/${iconName}`;
}
}

View File

@@ -0,0 +1,193 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
Item {
id: iconButton
signal clicked
property bool active: false
readonly property bool isActive: active || focus || mouseArea.pressed || mouseArea.containsMouse
property string icon: ""
property int iconSize: 16
property color contentColor: "#FFFFFF"
property color activeContentColor: "#FFFFFF"
property string label: ""
property bool showLabel: true
property string fontFamily: "RedHatDisplay"
property int fontWeight: 400
property int fontSize: 12
property color backgroundColor: "#FFFFFF"
property double backgroundOpacity: 0.0
property color activeBackgroundColor: "#FFFFFF"
property double activeBackgroundOpacity: 0.15
property string tooltipText: ""
property int borderRadius: 10
property int borderRadiusLeft: borderRadius
property int borderRadiusRight: borderRadius
property int borderSize: 0
property color borderColor: isActive ? iconButton.activeContentColor : iconButton.contentColor
property int preferredWidth: -1
width: preferredWidth !== -1 ? preferredWidth : buttonContentRow.width
height: iconSize * 2
Rectangle {
id: buttonBackground
anchors.fill: parent
color: iconButton.isActive ? iconButton.activeBackgroundColor : iconButton.backgroundColor
opacity: iconButton.isActive ? iconButton.activeBackgroundOpacity : iconButton.backgroundOpacity
topLeftRadius: iconButton.borderRadiusLeft
topRightRadius: iconButton.borderRadiusRight
bottomLeftRadius: iconButton.borderRadiusLeft
bottomRightRadius: iconButton.borderRadiusRight
Behavior on opacity {
enabled: Config.enableAnimations
NumberAnimation {
duration: 250
}
}
Behavior on color {
enabled: Config.enableAnimations
ColorAnimation {
duration: 250
}
}
}
Rectangle {
id: buttonBorder
color: "transparent"
topLeftRadius: iconButton.borderRadiusLeft
topRightRadius: iconButton.borderRadiusRight
bottomLeftRadius: iconButton.borderRadiusLeft
bottomRightRadius: iconButton.borderRadiusRight
anchors.fill: parent
visible: iconButton.borderSize > 0 || iconButton.focus
border {
color: iconButton.borderColor
width: iconButton.focus ? iconButton.borderSize || 2 : (iconButton.borderSize > 0 ? iconButton.borderSize : 0)
}
}
RowLayout {
id: buttonContentRow
height: parent.height
spacing: 0
Item {
id: iconContainer
Layout.preferredWidth: parent.height
Layout.preferredHeight: parent.height
// Скрытая иконка-источник для MultiEffect
Image {
id: iconSource
source: iconButton.icon
anchors.centerIn: parent
width: iconButton.iconSize
height: width
sourceSize: Qt.size(width, height)
fillMode: Image.PreserveAspectFit
visible: false // Скрываем оригинал
}
// Применяем эффект к скрытому источнику
MultiEffect {
id: iconEffect
source: iconSource
anchors.fill: iconSource
colorization: 1.0
colorizationColor: iconButton.isActive ? iconButton.activeContentColor : iconButton.contentColor
opacity: iconButton.enabled ? 1.0 : 0.3
// Добавляем анимацию для плавного перехода цвета
Behavior on colorizationColor {
enabled: Config.enableAnimations
ColorAnimation {
duration: 200
}
}
Behavior on opacity {
enabled: Config.enableAnimations
NumberAnimation {
duration: 250
}
}
}
}
Text {
id: buttonLabel
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
Layout.fillWidth: true
elide: Text.ElideRight
text: iconButton.label
visible: iconButton.showLabel && text !== ""
font.family: iconButton.fontFamily
font.pixelSize: iconButton.fontSize
font.weight: iconButton.fontWeight
rightPadding: 10
color: iconButton.isActive ? iconButton.activeContentColor : iconButton.contentColor
opacity: iconButton.enabled ? 1.0 : 0.5
Behavior on color {
enabled: Config.enableAnimations
ColorAnimation {
duration: 200
}
}
Behavior on opacity {
enabled: Config.enableAnimations
NumberAnimation {
duration: 250
}
}
Component.onCompleted: {
if (iconButton.preferredWidth !== -1) {
Layout.preferredWidth = iconButton.width - iconContainer.width;
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: parent.enabled
onClicked: iconButton.clicked()
cursorShape: Qt.PointingHandCursor
ToolTip {
parent: mouseArea
enabled: Config.tooltipsEnable
property bool shouldShow: enabled && mouseArea.containsMouse && iconButton.tooltipText !== "" || enabled && iconButton.focus && iconButton.tooltipText !== ""
visible: shouldShow
delay: 300
contentItem: Text {
font.family: Config.tooltipsFontFamily
font.pixelSize: Config.tooltipsFontSize
text: iconButton.tooltipText
color: Config.tooltipsContentColor
}
background: Rectangle {
color: Config.tooltipsBackgroundColor
opacity: Config.tooltipsBackgroundOpacity
border.width: 0
radius: Config.tooltipsBorderRadius
}
}
}
Keys.onPressed: function (event) {
if (event.key == Qt.Key_Return || event.key == Qt.Key_Enter || event.key === Qt.Key_Space) {
iconButton.clicked();
}
}
}

View File

@@ -0,0 +1,241 @@
/*
Map country codes to IETF language codes, so we can use the system's keyboard layouts with the virtual keyboard
The keyboard layout isn't detected under Wayland, so I'm still not sure if that's a good idea.
Avaialble layouts -> https://doc.qt.io/qt-6/qtvirtualkeyboard-layouts.html
Language labels -> https://en.wikipedia.org/wiki/IETF_language_tag
https://github.com/qt/qtvirtualkeyboard/blob/16fbddbbc03777e0a006daa998eda14624d62268/src/virtualkeyboard/configure.cmake
https://github.com/qt/qtvirtualkeyboard/blob/16fbddbbc03777e0a006daa998eda14624d62268/tests/manual/x11vkbtest/testlanguagechange.cpp
https://github.com/qt/qtvirtualkeyboard/blob/16fbddbbc03777e0a006daa998eda14624d62268/cmake/QtVirtualKeyboardSetup.cmake
*/
pragma Singleton
import QtQuick
QtObject {
property var layouts: {
"ar": {
// Arabic
"label": "العربية",
"kb_code": "ar_AR"
},
"bg": {
// Bulgarian
"label": "български",
"kb_code": "bg_BG"
},
"cz": {
// Czech
"label": "Čeština ",
"kb_code": "cs_CZ"
},
"dk": {
// Danish
"label": "Dansk",
"kb_code": "da_DK"
},
"de": {
// German
"label": "Deutsch",
"kb_code": "de_DE"
},
"gr": {
// Greek
"label": "Ελληνικά",
"kb_code": "el_GR"
},
"gb": {
"label": "British English",
"kb_code": "en_GB"
},
"us": {
"label": "American English",
"kb_code": "en_US"
},
"es": {
// Spanish
"label": "Español",
"kb_code": "es_ES"
},
"mx": {
// Mexican spanish
"label": "Español (México)",
"kb_code": "es_MX"
},
"ee": {
// Estonian
"label": "Eesti",
"kb_code": "et_EE"
},
"fa": {
// Persian (Farsi)
"label": "فارسى",
"kb_code": "fa_FA"
},
"fi": {
// Finnish
"label": "Suomi",
"kb_code": "fi_FI"
},
"ca": {
// French Canada
"label": "Français (Canada)",
"kb_code": "fr_CA"
},
"fr": {
// French
"label": "Français",
"kb_code": "fr_FR"
},
"il": {
// Hebrew
"label": "עברית",
"kb_code": "he_IL"
},
"in": {
// Hindi
"label": "हिंदी",
"kb_code": "hi_IN"
},
"hr": {
// Croatian
"label": "Hrvatski ",
"kb_code": "hr_HR"
},
"hu": {
// Hungarian
"label": "Magyar ",
"kb_code": "hu_HU"
},
"id": {
// Indonesian
"label": "Bahasa Indonesia",
"kb_code": "id_ID"
},
"it": {
// Italian
"label": "Italiano",
"kb_code": "it_IT"
},
"lv": {
// Latvian
"label": "latviešu ",
"kb_code": "lv_LV"
},
"jp": {
// Japanese
"label": "日本語",
"kb_code": "ja_JP"
},
"kr": {
// Korean
"label": "한국어",
"kb_code": "ko_KR"
},
"my": {
// Malay
"label": "Bahasa Malaysia",
"kb_code": "ms_MY"
},
"no": {
// Norwegian
"label": "Norsk ",
"kb_code": "nb_NO"
},
"nl": {
// Dutch
"label": "Nederlands",
"kb_code": "nl_NL"
},
"pl": {
// Polish
"label": "Polski",
"kb_code": "pl_PL"
},
"br": {
// Portuguese (Brazil)
"label": "Português (Brasil)",
"kb_code": "pt_BR"
},
"pt": {
// Portuguese (Europe)
"label": "Português (Portugal)",
"kb_code": "pt_PT"
},
"ro": {
// Romanian
"label": "Română",
"kb_code": "ro_RO"
},
"ru": {
// Russian
"label": "Русский",
"kb_code": "ru_RU"
},
"sk": {
// Slovak
"label": "Slovenčina",
"kb_code": "sk_SK"
},
"si": {
// Slovenian
"label": "Slovenski",
"kb_code": "sl_SI"
},
"al": {
// Albanian
"label": "Shqip",
"kb_code": "sq_AL"
},
"sp": {
// Serbian
"label": "Srpski/Српски",
"kb_code": "sr_SP"
},
"se": {
// Swedish
"label": "Svenska",
"kb_code": "sv_SE"
},
"th": {
// Thai
"label": "ไทย",
"kb_code": "th_TH"
},
"tr": {
// Turkish
"label": "Türkçe",
"kb_code": "tr_TR"
},
"ua": {
// Ukrainian
"label": "Українська",
"kb_code": "uk_UA"
},
"vn": {
// Vietnamese
"label": "Tiếng Việt",
"kb_code": "vi_VN"
},
"cn": {
// Simplified Chinese
"label": "简体中文",
"kb_code": "zh_CN"
},
"tw": {
// Traditional Chinese
"label": "繁體中文",
"kb_code": "zh_TW"
}
// FIXME: Missing layout for "zh_HK" (HongKong Chinese). This might be yet another SDDM bug.
}
function getKBCodeFor(country) {
return country && layouts[country] ? layouts[country]["kb_code"] : "";
}
function getLabelFor(country) {
return country && layouts[country] ? layouts[country]["label"] : "";
}
}

View File

@@ -0,0 +1,241 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
ColumnLayout {
id: selector
width: Config.layoutPopupWidth - (Config.menuAreaPopupsPadding * 2)
signal layoutChanged(layoutIndex: int)
signal close
property int currentLayoutIndex: (keyboard && keyboard.layouts && keyboard.layouts.length > 0) ? keyboard.currentLayout : 0
property string layoutName: ""
property string layoutShortName: ""
// FIX: Добавляем функцию принудительного обновления
function forceUpdate() {
console.log("LayoutSelector: Force updating layouts");
if (keyboard && keyboard.layouts) {
console.log("LayoutSelector: Found", keyboard.layouts.length, "layouts");
// Принудительно обновляем модель
layoutList.model = null;
layoutList.model = keyboard.layouts;
// Обновляем текущий индекс
selector.currentLayoutIndex = keyboard.currentLayout;
updateLayout();
} else {
console.log("LayoutSelector: No keyboard or layouts found");
}
}
function updateLayout() {
if (keyboard && keyboard.layouts && selector.currentLayoutIndex >= 0 && selector.currentLayoutIndex < keyboard.layouts.length) {
keyboard.currentLayout = selector.currentLayoutIndex;
selector.layoutName = keyboard.layouts[selector.currentLayoutIndex].longName;
selector.layoutShortName = keyboard.layouts[selector.currentLayoutIndex].shortName;
console.log("LayoutSelector: Updated to layout", selector.currentLayoutIndex, ":", selector.layoutShortName);
}
selector.layoutChanged(selector.currentLayoutIndex);
}
Component.onCompleted: {
console.log("LayoutSelector: Component completed");
// FIX: Принудительное обновление при создании компонента
Qt.callLater(function() {
forceUpdate();
});
selector.layoutName = keyboard && keyboard.layouts && keyboard.layouts.length > 0 ? keyboard.layouts[selector.currentLayoutIndex].longName : "";
selector.layoutShortName = keyboard && keyboard.layouts && keyboard.layouts.length > 0 ? keyboard.layouts[selector.currentLayoutIndex].shortName : "";
selector.layoutChanged(selector.currentLayoutIndex);
}
// FIX: Добавляем связь для отслеживания изменений раскладок
Connections {
target: keyboard
function onLayoutsChanged() {
console.log("LayoutSelector: Keyboard layouts changed");
forceUpdate();
}
function onCurrentLayoutChanged() {
console.log("LayoutSelector: Current layout changed to", keyboard.currentLayout);
selector.currentLayoutIndex = keyboard.currentLayout;
updateLayout();
}
}
Text {
id: noLayoutMessage
Layout.preferredWidth: parent.width - 5
text: "No keyboard layout could be found. This is a known issue with Wayland."
visible: keyboard == undefined || !keyboard.layouts || keyboard.layouts.length === 0
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
color: Config.menuAreaPopupsContentColor
font.pixelSize: Config.menuAreaPopupsFontSize
font.family: Config.menuAreaPopupsFontFamily
padding: 10
}
ListView {
id: layoutList
visible: !noLayoutMessage.visible
Layout.preferredWidth: parent.width
Layout.preferredHeight: Math.min((keyboard && keyboard.layouts ? keyboard.layouts.length : 0) * (Config.menuAreaPopupsItemHeight + 5 + spacing) - spacing, Config.menuAreaPopupsMaxHeight)
orientation: ListView.Vertical
interactive: true
clip: true
boundsBehavior: Flickable.StopAtBounds
spacing: Config.menuAreaPopupsSpacing
highlightFollowsCurrentItem: true
highlightMoveDuration: 0
contentHeight: (keyboard && keyboard.layouts ? keyboard.layouts.length : 0) * (Config.menuAreaPopupsItemHeight + 5 + spacing) - spacing
ScrollBar.vertical: ScrollBar {
id: scrollbar
policy: Config.menuAreaPopupsDisplayScrollbar && layoutList.contentHeight > layoutList.height ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff
contentItem: Rectangle {
implicitWidth: 5
radius: 5
color: Config.menuAreaPopupsContentColor
opacity: Config.menuAreaPopupsActiveOptionBackgroundOpacity
}
}
model: keyboard && keyboard.layouts ? keyboard.layouts : []
delegate: Rectangle {
width: scrollbar.visible ? selector.width - Config.menuAreaPopupsPadding - scrollbar.width : selector.width
height: childrenRect.height
color: "transparent"
Rectangle {
anchors.fill: parent
color: Config.menuAreaPopupsActiveOptionBackgroundColor
opacity: index === currentLayoutIndex ? Config.menuAreaPopupsActiveOptionBackgroundOpacity : (mouseArea.containsMouse ? Config.menuAreaPopupsActiveOptionBackgroundOpacity : 0.0)
radius: 5
}
RowLayout {
width: parent.width
height: Config.menuAreaPopupsItemHeight + 5
spacing: 0
Rectangle {
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
Layout.preferredHeight: parent.height
Layout.preferredWidth: Layout.preferredHeight
color: "transparent"
Image {
anchors.centerIn: parent
// FIX: Улучшаем путь к флагам и добавляем fallback
source: {
var flagPath = `/usr/share/sddm/flags/${shortName}.png`;
// Альтернативные пути для флагов
if (!Qt.resolvedUrl(flagPath)) {
flagPath = `/usr/share/flags/${shortName}.png`;
}
if (!Qt.resolvedUrl(flagPath)) {
flagPath = `/usr/share/pixmaps/flags/${shortName}.png`;
}
return flagPath;
}
width: Config.menuAreaPopupsIconSize
height: width
sourceSize: Qt.size(width, height)
fillMode: Image.PreserveAspectFit
// FIX: Добавляем fallback для случая, когда флаг не найден
onStatusChanged: {
if (status === Image.Error) {
// Используем текстовую замену, если изображение не найдено
visible = false;
}
}
}
// FIX: Текстовая замена для флага, если изображение не найдено
Text {
anchors.centerIn: parent
text: shortName ? shortName.toUpperCase() : ""
visible: parent.children[0].status === Image.Error
color: index === currentLayoutIndex || mouseArea.containsMouse ? Config.menuAreaPopupsActiveContentColor : Config.menuAreaPopupsContentColor
font.pixelSize: Config.menuAreaPopupsFontSize - 4
font.family: Config.menuAreaPopupsFontFamily
font.weight: Font.Bold
}
}
Column {
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
Layout.fillWidth: true
Text {
width: parent.width
// FIX: Улучшаем получение имени языка
text: {
var label = Languages.getLabelFor(shortName);
if (!label || label.length === 0) {
// Fallback к короткому имени, если нет перевода
return shortName ? shortName.toUpperCase() : "";
}
return label;
}
visible: text && text.length > 0
color: index === currentLayoutIndex || mouseArea.containsMouse ? Config.menuAreaPopupsActiveContentColor : Config.menuAreaPopupsContentColor
font.pixelSize: Config.menuAreaPopupsFontSize
font.family: Config.menuAreaPopupsFontFamily
elide: Text.ElideRight
rightPadding: 10
}
Text {
width: parent.width
text: longName || shortName || "Unknown Layout"
color: index === currentLayoutIndex || mouseArea.containsMouse ? Config.menuAreaPopupsActiveContentColor : Config.menuAreaPopupsContentColor
opacity: 0.75
font.pixelSize: Config.menuAreaPopupsFontSize - 2
font.family: Config.menuAreaPopupsFontFamily
elide: Text.ElideRight
rightPadding: 10
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
enabled: index !== selector.currentLayoutIndex
hoverEnabled: index !== selector.currentLayoutIndex
z: 2
cursorShape: hoverEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
console.log("LayoutSelector: Clicked layout", index, ":", shortName);
selector.currentLayoutIndex = index;
selector.updateLayout();
}
}
}
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Down) {
if (keyboard && keyboard.layouts && keyboard.layouts.length > 0) {
selector.currentLayoutIndex = (selector.currentLayoutIndex + keyboard.layouts.length + 1) % keyboard.layouts.length;
selector.updateLayout();
}
} else if (event.key === Qt.Key_Up) {
if (keyboard && keyboard.layouts && keyboard.layouts.length > 0) {
selector.currentLayoutIndex = (selector.currentLayoutIndex + keyboard.layouts.length - 1) % keyboard.layouts.length;
selector.updateLayout();
}
} else if (event.key == Qt.Key_Return || event.key == Qt.Key_Enter || event.key === Qt.Key_Space) {
selector.close();
} else if (event.key === Qt.Key_CapsLock) {
root.capsLockOn = !root.capsLockOn;
}
}
}

View File

@@ -0,0 +1,176 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import QtQuick.Controls
Item {
id: lockScreen
signal loginRequested
// TODO: Support for weather info?
ColumnLayout {
id: timePositioner
spacing: Config.dateMarginTop
Text {
id: time
visible: Config.clockDisplay
font.pixelSize: Config.clockFontSize
font.weight: Config.clockFontWeight
font.family: Config.clockFontFamily
color: Config.clockColor
Layout.alignment: Config.clockAlign === "left" ? Qt.AlignLeft : (Config.clockAlign === "right" ? Qt.AlignRight : Qt.AlignHCenter)
function updateTime() {
text = new Date().toLocaleString(Qt.locale(""), Config.clockFormat);
}
}
Text {
id: date
Layout.alignment: Config.clockAlign === "left" ? Qt.AlignLeft : (Config.clockAlign === "right" ? Qt.AlignRight : Qt.AlignHCenter)
visible: Config.dateDisplay
font.pixelSize: Config.dateFontSize
font.family: Config.dateFontFamily
font.weight: Config.dateFontWeight
color: Config.dateColor
function updateDate() {
text = new Date().toLocaleString(Qt.locale("ru_RU"), Config.dateFormat);
}
}
Timer {
id: clockTimer
interval: 1000
repeat: true
running: true
onTriggered: {
time.updateTime();
date.updateDate();
}
}
Component.onDestruction: {
if (clockTimer) {
clockTimer.stop();
}
}
anchors {
// FIX: Height calculation fixes - protect against zero division
topMargin: Config.lockScreenPaddingTop || (lockScreen.height > 0 ? lockScreen.height / 10 : 50)
rightMargin: Config.lockScreenPaddingRight || (lockScreen.height > 0 ? lockScreen.height / 10 : 50)
bottomMargin: Config.lockScreenPaddingBottom || (lockScreen.height > 0 ? lockScreen.height / 10 : 50)
leftMargin: Config.lockScreenPaddingLeft || (lockScreen.height > 0 ? lockScreen.height / 10 : 50)
}
Component.onCompleted: {
lockScreen.alignItem(timePositioner, Config.clockPosition);
time.updateTime();
date.updateDate();
}
}
ColumnLayout {
id: messagePositioner
visible: Config.lockMessageDisplay
spacing: Config.lockMessageSpacing
Image {
id: lockIcon
source: Config.getIcon(Config.lockMessageIcon)
Layout.alignment: Config.lockMessageAlign === "left" ? Qt.AlignLeft : (Config.lockMessageAlign === "right" ? Qt.AlignRight : Qt.AlignHCenter)
visible: Config.lockMessageDisplayIcon
Layout.preferredWidth: Config.lockMessageIconSize
Layout.preferredHeight: Config.lockMessageIconSize
sourceSize: Qt.size(width, height)
fillMode: Image.PreserveAspectFit
MultiEffect {
source: lockIcon
anchors.fill: lockIcon
colorization: Config.lockMessagePaintIcon ? 1 : 0
colorizationColor: Config.lockMessageColor
}
}
Text {
id: lockMessage
Layout.alignment: Config.lockMessageAlign === "left" ? Qt.AlignLeft : (Config.lockMessageAlign === "right" ? Qt.AlignRight : Qt.AlignHCenter)
font.pixelSize: Config.lockMessageFontSize
font.family: Config.lockMessageFontFamily
font.weight: Config.lockMessageFontWeight
color: Config.lockMessageColor
text: Config.lockMessageText
}
anchors {
// FIX: Height calculation fixes - protect against zero division
topMargin: Config.lockScreenPaddingTop || (lockScreen.height > 0 ? lockScreen.height / 10 : 50)
rightMargin: Config.lockScreenPaddingRight || (lockScreen.height > 0 ? lockScreen.height / 10 : 50)
bottomMargin: Config.lockScreenPaddingBottom || (lockScreen.height > 0 ? lockScreen.height / 10 : 50)
leftMargin: Config.lockScreenPaddingLeft || (lockScreen.height > 0 ? lockScreen.height / 10 : 50)
}
Component.onCompleted: lockScreen.alignItem(messagePositioner, Config.lockMessagePosition)
}
function alignItem(item, pos) {
switch (pos) {
case "top-left":
item.anchors.top = lockScreen.top;
item.anchors.left = lockScreen.left;
break;
case "top-center":
item.anchors.top = lockScreen.top;
item.anchors.horizontalCenter = lockScreen.horizontalCenter;
break;
case "top-right":
item.anchors.top = lockScreen.top;
item.anchors.right = lockScreen.right;
break;
case "center-left":
item.anchors.verticalCenter = lockScreen.verticalCenter;
item.anchors.left = lockScreen.left;
break;
case "center":
item.anchors.verticalCenter = lockScreen.verticalCenter;
item.anchors.horizontalCenter = lockScreen.horizontalCenter;
break;
case "center-right":
item.anchors.verticalCenter = lockScreen.verticalCenter;
item.anchors.right = lockScreen.right;
break;
case "bottom-left":
item.anchors.bottom = lockScreen.bottom;
item.anchors.left = lockScreen.left;
break;
case "bottom-center":
item.anchors.bottom = lockScreen.bottom;
item.anchors.horizontalCenter = lockScreen.horizontalCenter;
break;
default:
item.anchors.bottom = lockScreen.bottom;
item.anchors.right = lockScreen.right;
}
}
MouseArea {
id: lockScreenMouseArea
hoverEnabled: true
z: -1
anchors.fill: lockScreen
onClicked: lockScreen.loginRequested()
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_CapsLock) {
root.capsLockOn = !root.capsLockOn;
}
if (event.key === Qt.Key_Escape) {
event.accepted = false;
return;
} else {
lockScreen.loginRequested();
}
event.accepted = true;
}
}

View File

@@ -0,0 +1,379 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import SddmComponents
Item {
id: loginScreen
signal close
signal toggleLayoutPopup
state: "normal"
property bool stateChanging: false
function safeStateChange(newState) { // This is probably overkill, but whatever
if (!stateChanging) {
stateChanging = true;
state = newState;
stateChanging = false;
}
}
onStateChanged: {
if (state === "normal") {
resetFocus();
}
}
readonly property alias password: password
readonly property alias loginButton: loginButton
readonly property alias loginContainer: loginContainer
property bool showKeyboard: !Config.virtualKeyboardStartHidden
// Login info
property int sessionIndex: 0
property int userIndex: 0
property string userName: ""
property string userRealName: ""
property string userIcon: ""
property bool userNeedsPassword: false
function login() {
if (password.text.length > 0 || !userNeedsPassword) {
safeStateChange("authenticating");
sddm.login(userName, password.text, sessionIndex);
}
}
Connections {
function onLoginSucceeded() {
loginContainer.scale = 0.0;
}
function onLoginFailed() {
safeStateChange("normal");
loginMessage.warn(textConstants.loginFailed || "Login failed", "error");
password.text = "";
}
function onInformationMessage(message) {
loginMessage.warn(message, "error");
}
target: sddm
}
// FIX: Critical connections memory leak prevention?
Component.onDestruction: {
if (typeof connections !== 'undefined') {
connections.target = null;
}
}
function updateCapsLock() {
if (root.capsLockOn && loginScreen.state !== "authenticating") {
loginMessage.warn(textConstants.capslockWarning || "Caps Lock is on", "warning");
} else {
loginMessage.clear();
}
}
function resetFocus() {
if (loginScreen.userNeedsPassword) {
password.input.forceActiveFocus();
} else {
loginButton.forceActiveFocus();
}
}
Item {
id: loginContainer
width: Config.loginAreaPosition === "left" || Config.loginAreaPosition === "right" ? (Config.avatarActiveSize + Config.usernameMargin + loginArea.width) : userSelector.width
height: childrenRect.height
scale: 0.5 // Initial animation
Behavior on scale {
enabled: Config.enableAnimations
NumberAnimation {
duration: 200
}
}
// LoginArea position
Component.onCompleted: {
if (Config.loginAreaPosition === "left") {
anchors.verticalCenter = parent.verticalCenter;
if (Config.loginAreaMargin === -1) {
anchors.horizontalCenter = parent.horizontalCenter;
} else {
anchors.left = parent.left;
anchors.leftMargin = Config.loginAreaMargin;
}
} else if (Config.loginAreaPosition === "right") {
anchors.verticalCenter = parent.verticalCenter;
if (Config.loginAreaMargin === -1) {
anchors.horizontalCenter = parent.horizontalCenter;
} else {
anchors.right = parent.right;
anchors.rightMargin = Config.loginAreaMargin;
}
} else {
anchors.horizontalCenter = parent.horizontalCenter;
if (Config.loginAreaMargin === -1) {
anchors.verticalCenter = parent.verticalCenter;
} else {
anchors.top = parent.top;
anchors.topMargin = Config.loginAreaMargin;
}
}
}
UserSelector {
id: userSelector
listUsers: loginScreen.state === "selectingUser"
enabled: loginScreen.state !== "authenticating"
activeFocusOnTab: true
orientation: Config.loginAreaPosition === "left" || Config.loginAreaPosition === "right" ? "vertical" : "horizontal"
width: orientation === "horizontal" ? loginScreen.width - Config.loginAreaMargin * 2 : Config.avatarActiveSize
height: orientation === "horizontal" ? Config.avatarActiveSize : loginScreen.height - Config.loginAreaMargin * 2
onOpenUserList: {
safeStateChange("selectingUser");
}
onCloseUserList: {
safeStateChange("normal");
loginScreen.resetFocus(); // resetFocus with escape even if the selector is not open
}
onUserChanged: (index, name, realName, icon, needsPassword) => {
loginScreen.userIndex = index;
loginScreen.userName = name;
loginScreen.userRealName = realName;
loginScreen.userIcon = icon;
loginScreen.userNeedsPassword = needsPassword;
}
Component.onCompleted: {
anchors.top = parent.top;
if (Config.loginAreaPosition === "left") {
anchors.left = parent.left;
} else if (Config.loginAreaPosition === "right") {
anchors.right = parent.right;
}
}
}
Item {
id: loginLayout
height: activeUserName.height + Config.passwordInputMarginTop + loginArea.height
width: loginArea.width > activeUserName.width ? loginArea.width : activeUserName.width
// LoginArea alignment
Component.onCompleted: {
if (Config.loginAreaPosition === "left") {
anchors.verticalCenter = parent.verticalCenter;
anchors.left = userSelector.right;
anchors.leftMargin = Config.usernameMargin;
} else if (Config.loginAreaPosition === "right") {
anchors.verticalCenter = parent.verticalCenter;
anchors.right = userSelector.left;
anchors.rightMargin = Config.usernameMargin;
} else {
anchors.top = userSelector.bottom;
anchors.topMargin = Config.usernameMargin;
anchors.horizontalCenter = parent.horizontalCenter;
}
}
Text {
id: activeUserName
font.family: Config.usernameFontFamily
font.weight: Config.usernameFontWeight
font.pixelSize: Config.usernameFontSize
color: Config.usernameColor
text: loginScreen.userRealName || loginScreen.userName || ""
Component.onCompleted: {
anchors.top = parent.top;
if (Config.loginAreaPosition === "left") {
anchors.left = parent.left;
} else if (Config.loginAreaPosition === "right") {
anchors.right = parent.right;
} else {
anchors.horizontalCenter = parent.horizontalCenter;
}
}
}
RowLayout {
id: loginArea
height: Config.passwordInputHeight
spacing: Config.loginButtonMarginLeft
visible: loginScreen.state !== "authenticating"
Component.onCompleted: {
anchors.top = activeUserName.bottom;
anchors.topMargin = Config.passwordInputMarginTop;
if (Config.loginAreaPosition === "left") {
anchors.left = parent.left;
} else if (Config.loginAreaPosition === "right") {
anchors.right = parent.right;
} else {
anchors.horizontalCenter = parent.horizontalCenter;
}
}
PasswordInput {
id: password
Layout.alignment: Qt.AlignHCenter
enabled: loginScreen.state !== "selectingUser" && loginScreen.state !== "authenticating" && loginScreen.state === "normal"
visible: loginScreen.userNeedsPassword
onAccepted: {
loginScreen.login();
}
}
IconButton {
id: loginButton
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: width // Fix button not resizing when label updates
height: password.height
visible: !Config.loginButtonHideIfNotNeeded || !loginScreen.userNeedsPassword
enabled: loginScreen.state !== "selectingUser" && loginScreen.state !== "authenticating"
activeFocusOnTab: true
icon: Config.getIcon(Config.loginButtonIcon)
label: textConstants.login ? textConstants.login.toUpperCase() : "LOGIN"
showLabel: Config.loginButtonShowTextIfNoPassword && !loginScreen.userNeedsPassword
tooltipText: !Config.tooltipsDisableLoginButton && (!Config.loginButtonShowTextIfNoPassword || loginScreen.userNeedsPassword) ? (textConstants.login || "Login") : ""
iconSize: Config.loginButtonIconSize
fontFamily: Config.loginButtonFontFamily
fontSize: Config.loginButtonFontSize
fontWeight: Config.loginButtonFontWeight
contentColor: Config.loginButtonContentColor
activeContentColor: Config.loginButtonActiveContentColor
backgroundColor: Config.loginButtonBackgroundColor
backgroundOpacity: Config.loginButtonBackgroundOpacity
activeBackgroundColor: Config.loginButtonActiveBackgroundColor
activeBackgroundOpacity: Config.loginButtonActiveBackgroundOpacity
borderSize: Config.loginButtonBorderSize
borderColor: Config.loginButtonBorderColor
borderRadiusLeft: password.visible ? Config.loginButtonBorderRadiusLeft : Config.loginButtonBorderRadiusRight
borderRadiusRight: Config.loginButtonBorderRadiusRight
onClicked: {
loginScreen.login();
}
Behavior on x {
enabled: Config.enableAnimations
NumberAnimation {
duration: 150
}
}
}
}
Spinner {
id: spinner
visible: loginScreen.state === "authenticating"
opacity: visible ? 1.0 : 0.0
Component.onCompleted: {
anchors.top = activeUserName.bottom;
anchors.topMargin = Config.passwordInputMarginTop;
if (Config.loginAreaPosition === "left") {
anchors.left = parent.left;
} else if (Config.loginAreaPosition === "right") {
anchors.right = parent.right;
} else {
anchors.horizontalCenter = parent.horizontalCenter;
}
}
}
Text {
id: loginMessage
property bool capslockWarning: false
font.pixelSize: Config.warningMessageFontSize
font.family: Config.warningMessageFontFamily
font.weight: Config.warningMessageFontWeight
color: Config.warningMessageNormalColor
visible: text !== "" && loginScreen.state !== "authenticating" && (capslockWarning ? loginScreen.userNeedsPassword : true)
opacity: visible ? 1.0 : 0.0
anchors.top: loginArea.bottom
anchors.topMargin: visible ? Config.warningMessageMarginTop : 0
Component.onCompleted: {
if (root.capsLockOn)
loginMessage.warn(textConstants.capslockWarning || "Caps Lock is on", "warning");
if (Config.loginAreaPosition === "left") {
anchors.left = parent.left;
} else if (Config.loginAreaPosition === "right") {
anchors.right = parent.right;
} else {
anchors.horizontalCenter = parent.horizontalCenter;
}
}
Behavior on anchors.topMargin {
enabled: Config.enableAnimations
NumberAnimation {
duration: 150
}
}
Behavior on opacity {
enabled: Config.enableAnimations
NumberAnimation {
duration: 150
}
}
function warn(message, type) {
clear();
text = message;
color = type === "error" ? Config.warningMessageErrorColor : (type === "warning" ? Config.warningMessageWarningColor : Config.warningMessageNormalColor);
if (message === (textConstants.capslockWarning || "Caps Lock is on"))
capslockWarning = true;
}
function clear() {
text = "";
capslockWarning = false;
}
}
}
}
MenuArea {}
VirtualKeyboard {}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) {
if (loginScreen.state === "authenticating") {
event.accepted = false;
return;
}
if (Config.lockScreenDisplay) {
loginScreen.close();
}
password.text = "";
} else if (event.key === Qt.Key_CapsLock) {
root.capsLockOn = !root.capsLockOn;
}
event.accepted = true;
}
MouseArea {
id: closeUserSelectorMouseArea
z: -1
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (loginScreen.state === "selectingUser") {
safeStateChange("normal");
}
}
onWheel: event => {
if (loginScreen.state === "selectingUser") {
if (event.angleDelta.y < 0) {
userSelector.nextUser();
} else {
userSelector.prevUser();
}
}
}
}
}

View File

@@ -0,0 +1,575 @@
import QtQuick
import QtQuick.Controls
import QtQuick.VirtualKeyboard.Settings
Item {
id: menuArea
anchors.fill: parent
Component {
id: sessionMenuComponent
IconButton {
id: sessionButton
property bool showLabel: Config.sessionDisplaySessionName
preferredWidth: showLabel ? (Config.sessionButtonWidth === -1 ? undefined : Config.sessionButtonWidth) : Config.menuAreaButtonsSize
height: Config.menuAreaButtonsSize
iconSize: Config.sessionIconSize
fontSize: Config.sessionFontSize
enabled: loginScreen.state === "normal" || popup.visible
active: popup.visible
contentColor: Config.sessionContentColor
activeContentColor: Config.sessionActiveContentColor
borderRadius: Config.menuAreaButtonsBorderRadius
borderSize: Config.sessionBorderSize
backgroundColor: Config.sessionBackgroundColor
backgroundOpacity: Config.sessionBackgroundOpacity
activeBackgroundColor: Config.sessionBackgroundColor
activeBackgroundOpacity: Config.sessionActiveBackgroundOpacity
fontFamily: Config.menuAreaButtonsFontFamily
activeFocusOnTab: true
focus: false
onClicked: {
if (loginScreen.isSelectingUser) {
loginScreen.isSelectingUser = false;
} else {
popup.open();
}
}
tooltipText: "Change session"
Popup {
id: popup
parent: sessionButton
padding: Config.menuAreaPopupsPadding
z: 1000
background: Rectangle {
color: Config.menuAreaPopupsBackgroundColor
opacity: Config.menuAreaPopupsBackgroundOpacity
radius: Config.menuAreaButtonsBorderRadius
Rectangle {
anchors.fill: parent
visible: Config.menuAreaPopupsBorderSize > 0
radius: parent.radius
color: "transparent"
border {
color: Config.menuAreaPopupsBorderColor
width: Config.menuAreaPopupsBorderSize
}
}
}
dim: true
Overlay.modal: Rectangle {
color: "transparent"
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: function (event) {
popup.close();
event.accepted = true;
}
}
}
onOpened: {
loginScreen.safeStateChange("popup");
[x, y] = menuArea.calculatePopupPos(Config.sessionPopupDirection, Config.sessionPopupAlign, popup, sessionButton);
}
onClosed: loginScreen.safeStateChange("normal")
modal: true
popupType: Popup.Item
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
focus: visible
SessionSelector {
focus: popup.focus
onSessionChanged: function (newSessionIndex, sessionIcon, sessionLabel) {
loginScreen.sessionIndex = newSessionIndex;
sessionButton.icon = sessionIcon;
sessionButton.label = sessionButton.showLabel ? sessionLabel : "";
}
onClose: {
popup.close();
}
}
}
}
}
Component {
id: layoutMenuComponent
IconButton {
id: layoutButton
property bool showLabel: Config.layoutDisplayLayoutName
height: Config.menuAreaButtonsSize
icon: Config.getIcon(Config.layoutIcon)
active: popup.visible
borderRadius: Config.menuAreaButtonsBorderRadius
borderSize: Config.layoutBorderSize
iconSize: Config.layoutIconSize
fontSize: Config.layoutFontSize
backgroundColor: Config.layoutBackgroundColor
backgroundOpacity: Config.layoutBackgroundOpacity
activeBackgroundColor: Config.layoutBackgroundColor
activeBackgroundOpacity: Config.layoutActiveBackgroundOpacity
contentColor: Config.layoutContentColor
activeContentColor: Config.layoutActiveContentColor
fontFamily: Config.menuAreaButtonsFontFamily
activeFocusOnTab: true
enabled: loginScreen.state === "normal" || popup.visible
focus: false
onClicked: {
if (loginScreen.isSelectingUser) {
loginScreen.isSelectingUser = false;
} else {
if (keyboard && keyboard.layouts) {
console.log("Available layouts:", keyboard.layouts.length);
for (var i = 0; i < keyboard.layouts.length; i++) {
console.log("Layout", i, ":", keyboard.layouts[i].shortName, keyboard.layouts[i].longName);
}
}
popup.open();
}
}
tooltipText: "Change keyboard layout"
label: {
if (!showLabel) return "";
if (!keyboard || !keyboard.layouts || keyboard.layouts.length === 0) return "";
if (keyboard.currentLayout < 0 || keyboard.currentLayout >= keyboard.layouts.length) return "";
return keyboard.layouts[keyboard.currentLayout].shortName.toUpperCase();
}
Connections {
target: loginScreen
function onToggleLayoutPopup() {
if (popup.visible) {
popup.close();
} else {
popup.open();
}
}
}
Component.onDestruction: {
if (typeof connections !== 'undefined') {
connections.target = null;
}
}
Popup {
id: popup
parent: layoutButton
padding: Config.menuAreaPopupsPadding
z: 1000
background: Rectangle {
color: Config.menuAreaPopupsBackgroundColor
opacity: Config.menuAreaPopupsBackgroundOpacity
radius: Config.menuAreaButtonsBorderRadius
Rectangle {
anchors.fill: parent
visible: Config.menuAreaPopupsBorderSize > 0
radius: parent.radius
color: "transparent"
border {
color: Config.menuAreaPopupsBorderColor
width: Config.menuAreaPopupsBorderSize
}
}
}
focus: visible
dim: true
Overlay.modal: Rectangle {
color: "transparent"
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: function (event) {
popup.close();
event.accepted = true;
}
}
}
onOpened: {
loginScreen.safeStateChange("popup");
if (layoutSelector) {
layoutSelector.forceUpdate();
}
[x, y] = menuArea.calculatePopupPos(Config.layoutPopupDirection, Config.layoutPopupAlign, popup, layoutButton);
}
onImplicitHeightChanged: {
if (visible) {
[x, y] = menuArea.calculatePopupPos(Config.layoutPopupDirection, Config.layoutPopupAlign, popup, layoutButton);
}
}
onClosed: loginScreen.safeStateChange("normal")
modal: true
popupType: Popup.Item
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
LayoutSelector {
id: layoutSelector
focus: popup.focus
onLayoutChanged: function (index) {
if (keyboard && keyboard.layouts && index >= 0 && index < keyboard.layouts.length) {
layoutButton.label = showLabel ? keyboard.layouts[index].shortName.toUpperCase() : "";
VirtualKeyboardSettings.locale = Languages.getKBCodeFor(keyboard.layouts[index].shortName);
}
}
onClose: {
popup.close();
}
}
}
}
}
Component {
id: keyboardMenuComponent
IconButton {
id: keyboardButton
height: Config.menuAreaButtonsSize
width: Config.menuAreaButtonsSize
icon: Config.getIcon(Config.keyboardIcon)
iconSize: Config.keyboardIconSize
backgroundColor: Config.keyboardBackgroundColor
backgroundOpacity: Config.keyboardBackgroundOpacity
activeBackgroundColor: Config.keyboardBackgroundColor
activeBackgroundOpacity: Config.keyboardActiveBackgroundOpacity
contentColor: Config.keyboardContentColor
activeContentColor: Config.keyboardActiveContentColor
active: showKeyboard
fontFamily: Config.menuAreaButtonsFontFamily
borderRadius: Config.menuAreaButtonsBorderRadius
borderSize: Config.keyboardBorderSize
enabled: loginScreen.showKeyboard || loginScreen.state === "normal"
activeFocusOnTab: true
focus: false
onClicked: {
loginScreen.showKeyboard = !loginScreen.showKeyboard;
}
tooltipText: "Toggle virtual keyboard"
}
}
Component {
id: powerMenuComponent
IconButton {
id: powerButton
height: Config.menuAreaButtonsSize
width: Config.menuAreaButtonsSize
icon: Config.getIcon(Config.powerIcon)
iconSize: Config.powerIconSize
contentColor: Config.powerContentColor
activeContentColor: Config.powerActiveContentColor
fontFamily: Config.menuAreaButtonsFontFamily
active: popup.visible
borderRadius: Config.menuAreaButtonsBorderRadius
borderSize: Config.powerBorderSize
backgroundColor: Config.powerBackgroundColor
backgroundOpacity: Config.powerBackgroundOpacity
activeBackgroundColor: Config.powerBackgroundColor
activeBackgroundOpacity: Config.powerActiveBackgroundOpacity
enabled: loginScreen.state === "normal" || popup.visible
activeFocusOnTab: true
focus: false
onClicked: {
popup.open();
}
tooltipText: "Power options"
Popup {
id: popup
parent: powerButton
z: 1000
background: Rectangle {
color: Config.menuAreaPopupsBackgroundColor
opacity: Config.menuAreaPopupsBackgroundOpacity
radius: Config.menuAreaButtonsBorderRadius
Rectangle {
anchors.fill: parent
visible: Config.menuAreaPopupsBorderSize > 0
radius: parent.radius
color: "transparent"
border {
color: Config.menuAreaPopupsBorderColor
width: Config.menuAreaPopupsBorderSize
}
}
}
dim: true
padding: Config.menuAreaPopupsPadding
Overlay.modal: Rectangle {
color: "transparent"
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: function (event) {
popup.close();
event.accepted = true;
}
}
}
onOpened: {
loginScreen.safeStateChange("popup");
[x, y] = menuArea.calculatePopupPos(Config.powerPopupDirection, Config.powerPopupAlign, popup, powerButton);
}
onClosed: loginScreen.safeStateChange("normal")
modal: true
popupType: Popup.Item
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
focus: visible
PowerMenu {
focus: popup.focus
onClose: {
popup.close();
}
}
}
}
}
Row {
// top_left
id: topLeftButtons
height: childrenRect.height
width: childrenRect.width
spacing: Config.menuAreaButtonsSpacing // 10
anchors {
top: parent.top
left: parent.left
topMargin: Config.menuAreaButtonsMarginTop
leftMargin: Config.menuAreaButtonsMarginLeft
}
}
Row {
// top_center
id: topCenterButtons
height: childrenRect.height
width: childrenRect.width
spacing: Config.menuAreaButtonsSpacing // 10
anchors {
top: parent.top
horizontalCenter: parent.horizontalCenter
topMargin: Config.menuAreaButtonsMarginTop
}
}
Row {
// top_right
id: topRightButtons
height: childrenRect.height
width: childrenRect.width
spacing: Config.menuAreaButtonsSpacing // 10
anchors {
top: parent.top
right: parent.right
topMargin: Config.menuAreaButtonsMarginTop
rightMargin: Config.menuAreaButtonsMarginRight
}
}
Column {
// center_left
id: centerLeftButtons
height: childrenRect.height
width: childrenRect.width
spacing: Config.menuAreaButtonsSpacing // 10
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
leftMargin: Config.menuAreaButtonsMarginLeft
}
}
Column {
// center_right
id: centerRightButtons
height: childrenRect.height
width: childrenRect.width
spacing: Config.menuAreaButtonsSpacing // 10
anchors {
right: parent.right
verticalCenter: parent.verticalCenter
rightMargin: Config.menuAreaButtonsMarginRight
}
}
Row {
// bottom_left
id: bottomLeftButtons
height: childrenRect.height
width: childrenRect.width
spacing: Config.menuAreaButtonsSpacing // 10
anchors {
bottom: parent.bottom
left: parent.left
bottomMargin: Config.menuAreaButtonsMarginBottom
leftMargin: Config.menuAreaButtonsMarginLeft
}
}
Row {
// bottom_center
id: bottomCenterButtons
height: childrenRect.height
width: childrenRect.width
spacing: Config.menuAreaButtonsSpacing // 10
anchors {
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
bottomMargin: Config.menuAreaButtonsMarginBottom
}
}
Row {
// bottom_right
id: bottomRightButtons
height: childrenRect.height
width: childrenRect.width
spacing: Config.menuAreaButtonsSpacing // 10
anchors {
bottom: parent.bottom
right: parent.right
bottomMargin: Config.menuAreaButtonsMarginBottom
rightMargin: Config.menuAreaButtonsMarginRight
}
}
property var createdObjects: []
Component.onCompleted: {
var menus = Config.sortMenuButtons();
for (var i = 0; i < menus.length; i++) {
var pos;
switch (menus[i].position) {
case "top-left":
pos = topLeftButtons;
break;
case "top-center":
pos = topCenterButtons;
break;
case "top-right":
pos = topRightButtons;
break;
case "center-left":
pos = centerLeftButtons;
break;
case "center-right":
pos = centerRightButtons;
break;
case "bottom-left":
pos = bottomLeftButtons;
break;
case "bottom-center":
pos = bottomCenterButtons;
break;
case "bottom-right":
pos = bottomRightButtons;
break;
}
var createdObject;
if (menus[i].name === "session")
createdObject = sessionMenuComponent.createObject(pos, {});
else if (menus[i].name === "layout")
createdObject = layoutMenuComponent.createObject(pos, {});
else if (menus[i].name === "keyboard")
createdObject = keyboardMenuComponent.createObject(pos, {});
else if (menus[i].name === "power")
createdObject = powerMenuComponent.createObject(pos, {});
if (createdObject) {
createdObjects.push(createdObject);
}
}
}
Component.onDestruction: {
for (var i = 0; i < createdObjects.length; i++) {
if (createdObjects[i]) {
createdObjects[i].destroy();
}
}
createdObjects = [];
}
function calculatePopupPos(direction, align, popup, button) {
var popupMargin = Config.menuAreaPopupsMargin;
var x = 0, y = 0;
if (direction === "up") {
y = -popup.height - popupMargin;
if (align === "start") {
x = 0;
} else if (align === "end") {
x = -popup.width + button.width;
} else {
x = (button.width - popup.width) / 2;
}
} else if (direction === "down") {
y = button.height + popupMargin;
if (align === "start") {
x = 0;
} else if (align === "end") {
x = -popup.width + button.width;
} else {
x = (button.width - popup.width) / 2;
}
} else if (direction === "left") {
x = -popup.width - popupMargin;
if (align === "start") {
y = 0;
} else if (align === "end") {
y = -popup.height + button.height;
} else {
y = (button.height - popup.height) / 2;
}
} else {
x = button.width + popupMargin;
if (align === "start") {
y = 0;
} else if (align === "end") {
y = -popup.height + button.height;
} else {
y = (button.height - popup.height) / 2;
}
}
return [x, y];
}
}

View File

@@ -0,0 +1,108 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Effects
Item {
id: passwordInput
signal accepted
property alias input: textField
property alias text: textField.text
property bool enabled: true
width: Config.passwordInputWidth
height: Config.passwordInputHeight
TextField {
id: textField
anchors.fill: parent
color: Config.passwordInputContentColor
enabled: passwordInput.enabled
echoMode: TextInput.Password
activeFocusOnTab: true
selectByMouse: true
verticalAlignment: TextField.AlignVCenter
font.family: Config.passwordInputFontFamily
font.pixelSize: Math.max(8, Config.passwordInputFontSize || 12)
background: Rectangle {
anchors.fill: parent
color: Config.passwordInputBackgroundColor
opacity: Config.passwordInputBackgroundOpacity
topLeftRadius: Config.passwordInputBorderRadiusLeft
bottomLeftRadius: Config.passwordInputBorderRadiusLeft
topRightRadius: Config.passwordInputBorderRadiusRight
bottomRightRadius: Config.passwordInputBorderRadiusRight
}
leftPadding: placeholderLabel.x
rightPadding: 10
onAccepted: passwordInput.accepted()
Rectangle {
anchors.fill: parent
border.width: Config.passwordInputBorderSize
border.color: Config.passwordInputBorderColor
color: "transparent"
topLeftRadius: Config.passwordInputBorderRadiusLeft
bottomLeftRadius: Config.passwordInputBorderRadiusLeft
topRightRadius: Config.passwordInputBorderRadiusRight
bottomRightRadius: Config.passwordInputBorderRadiusRight
}
Row {
anchors.fill: parent
spacing: 0
leftPadding: Config.passwordInputDisplayIcon ? 2 : 10
Rectangle {
id: iconContainer
color: "transparent"
visible: Config.passwordInputDisplayIcon
height: parent.height
width: height
Image {
id: icon
source: Config.getIcon(Config.passwordInputIcon)
anchors.centerIn: parent
// FIX: Icon size safety
width: Math.max(1, Config.passwordInputIconSize || 16)
height: width
sourceSize: Qt.size(width, height)
fillMode: Image.PreserveAspectFit
opacity: passwordInput.enabled ? 1.0 : 0.3
Behavior on opacity {
enabled: Config.enableAnimations
NumberAnimation {
duration: 250
}
}
MultiEffect {
source: parent
anchors.fill: parent
colorization: 1
colorizationColor: textField.color
}
}
}
Text {
id: placeholderLabel
anchors {
verticalCenter: parent.verticalCenter
}
padding: 0
visible: textField.text.length === 0 && (!textField.preeditText || textField.preeditText.length === 0)
text: (textConstants && textConstants.password) ? textConstants.password : "Password"
color: textField.color
font.pixelSize: Math.max(8, textField.font.pixelSize || 12)
font.family: textField.font.family || "sans-serif"
horizontalAlignment: Text.AlignLeft
verticalAlignment: textField.verticalAlignment
font.italic: true
}
}
}
}

View File

@@ -0,0 +1,100 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
ColumnLayout {
id: selector
width: Config.powerPopupWidth
spacing: 2
signal close
KeyNavigation.up: shutdownButton
KeyNavigation.down: suspendButton
IconButton {
id: suspendButton
Layout.preferredHeight: Config.menuAreaPopupsItemHeight
Layout.preferredWidth: Config.powerPopupWidth
focus: selector.visible
width: Layout.preferredWidth
enabled: sddm.canSuspend
icon: Config.getIcon("power-suspend.svg")
contentColor: Config.menuAreaPopupsContentColor
activeContentColor: Config.menuAreaPopupsActiveContentColor
fontFamily: Config.menuAreaPopupsFontFamily
backgroundColor: "transparent"
activeBackgroundColor: Config.menuAreaPopupsActiveOptionBackgroundColor
activeBackgroundOpacity: Config.menuAreaPopupsActiveOptionBackgroundOpacity
iconSize: Config.menuAreaPopupsIconSize
fontSize: Config.menuAreaPopupsFontSize
onClicked: {
selector.close();
sddm.suspend();
}
label: textConstants.suspend
KeyNavigation.up: shutdownButton
KeyNavigation.down: rebootButton
}
IconButton {
id: rebootButton
Layout.preferredHeight: Config.menuAreaPopupsItemHeight
Layout.preferredWidth: Config.powerPopupWidth
focus: selector.visible
width: Layout.preferredWidth
enabled: sddm.canReboot
icon: Config.getIcon("power-reboot.svg")
contentColor: Config.menuAreaPopupsContentColor
activeContentColor: Config.menuAreaPopupsActiveContentColor
fontFamily: Config.menuAreaPopupsFontFamily
backgroundColor: "transparent"
activeBackgroundColor: Config.menuAreaPopupsActiveOptionBackgroundColor
activeBackgroundOpacity: Config.menuAreaPopupsActiveOptionBackgroundOpacity
iconSize: Config.menuAreaPopupsIconSize
fontSize: Config.menuAreaPopupsFontSize
onClicked: {
selector.close();
sddm.reboot();
}
label: textConstants.reboot
KeyNavigation.up: suspendButton
KeyNavigation.down: shutdownButton
}
IconButton {
id: shutdownButton
Layout.preferredHeight: Config.menuAreaPopupsItemHeight
Layout.preferredWidth: Config.powerPopupWidth
focus: selector.visible
width: Layout.preferredWidth
enabled: sddm.canPowerOff
icon: Config.getIcon("power.svg")
contentColor: Config.menuAreaPopupsContentColor
activeContentColor: Config.menuAreaPopupsActiveContentColor
fontFamily: Config.menuAreaPopupsFontFamily
backgroundColor: "transparent"
activeBackgroundColor: Config.menuAreaPopupsActiveOptionBackgroundColor
activeBackgroundOpacity: Config.menuAreaPopupsActiveOptionBackgroundOpacity
iconSize: Config.menuAreaPopupsIconSize
fontSize: Config.menuAreaPopupsFontSize
onClicked: {
selector.close();
sddm.powerOff();
}
label: textConstants.shutdown
KeyNavigation.up: rebootButton
KeyNavigation.down: suspendButton
}
Keys.onPressed: function (event) {
if (event.key == Qt.Key_Return || event.key == Qt.Key_Enter || event.key === Qt.Key_Space) {
selector.close();
} else if (event.key === Qt.Key_CapsLock) {
root.capsLockOn = !root.capsLockOn;
}
}
}

View File

@@ -0,0 +1 @@
Style style.qml

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,144 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Effects
ColumnLayout {
id: selector
width: Config.sessionPopupWidth - (Config.menuAreaPopupsPadding * 2)
signal sessionChanged(sessionIndex: int, iconPath: string, label: string)
signal close
property int currentSessionIndex: (sessionModel && sessionModel.lastIndex >= 0) ? sessionModel.lastIndex : 0
property string sessionName: ""
property string sessionIconPath: ""
function getSessionIcon(name) {
var available_session_icons = ["hyprland", "kde", "gnome", "ubuntu", "sway", "awesome", "qtile", "i3", "bspwm", "dwm", "xfce", "cinnamon", "niri"];
for (var i = 0; i < available_session_icons.length; i++) {
if (name && name.toLowerCase().includes(available_session_icons[i]))
return "../icons/sessions/" + available_session_icons[i] + ".svg";
}
return "../icons/sessions/default.svg";
}
ListView {
id: sessionList
Layout.preferredWidth: parent.width
Layout.preferredHeight: Math.min((sessionModel ? sessionModel.rowCount() : 0) * (Config.menuAreaPopupsItemHeight + spacing), Config.menuAreaPopupsMaxHeight)
orientation: ListView.Vertical
interactive: true
clip: true
boundsBehavior: Flickable.StopAtBounds
spacing: Config.menuAreaPopupsSpacing
highlightFollowsCurrentItem: true
highlightMoveDuration: 0
contentHeight: sessionModel.rowCount() * (Config.menuAreaPopupsItemHeight + spacing)
ScrollBar.vertical: ScrollBar {
id: scrollbar
policy: Config.menuAreaPopupsDisplayScrollbar && sessionList.contentHeight > sessionList.height ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff
contentItem: Rectangle {
id: scrollbarBackground
implicitWidth: 5
radius: 5
color: Config.menuAreaPopupsContentColor
opacity: Config.menuAreaPopupsActiveOptionBackgroundOpacity
}
}
model: sessionModel
currentIndex: selector.currentSessionIndex
onCurrentIndexChanged: {
var session_name = sessionModel.data(sessionModel.index(currentIndex, 0), 260);
selector.currentSessionIndex = currentIndex;
selector.sessionName = session_name;
selector.sessionChanged(selector.currentSessionIndex, getSessionIcon(session_name), session_name);
}
delegate: Rectangle {
width: scrollbar.visible ? parent.width - Config.menuAreaPopupsPadding - scrollbar.width : parent.width
height: Config.menuAreaPopupsItemHeight
color: "transparent"
radius: Config.menuAreaButtonsBorderRadius
Rectangle {
anchors.fill: parent
color: Config.menuAreaPopupsActiveOptionBackgroundColor
opacity: index === selector.currentSessionIndex ? Config.menuAreaPopupsActiveOptionBackgroundOpacity : (itemMouseArea.containsMouse ? Config.menuAreaPopupsActiveOptionBackgroundOpacity : 0.0)
radius: Config.menuAreaButtonsBorderRadius
}
RowLayout {
anchors.fill: parent
Rectangle {
Layout.preferredWidth: parent.height
Layout.preferredHeight: parent.height
Layout.alignment: Qt.AlignVCenter
color: "transparent"
Image {
anchors.centerIn: parent
source: selector.getSessionIcon(name)
width: Config.menuAreaPopupsIconSize
height: Config.menuAreaPopupsIconSize
sourceSize: Qt.size(width, height)
fillMode: Image.PreserveAspectFit
MultiEffect {
source: parent
anchors.fill: parent
colorization: 1
colorizationColor: index === selector.currentSessionIndex || itemMouseArea.containsMouse ? Config.menuAreaPopupsActiveContentColor : Config.menuAreaPopupsContentColor
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: parent.height
Layout.alignment: Qt.AlignVCenter
color: "transparent"
Text {
anchors.verticalCenter: parent.verticalCenter
// text: (name.length > 25) ? name.slice(0, 24) + '...' : name
text: name
color: index === selector.currentSessionIndex || itemMouseArea.containsMouse ? Config.menuAreaPopupsActiveContentColor : Config.menuAreaPopupsContentColor
font.pixelSize: Config.menuAreaPopupsFontSize
font.family: Config.menuAreaPopupsFontFamily
}
}
}
MouseArea {
id: itemMouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onClicked: {
sessionList.currentIndex = index;
}
}
}
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Down) {
if (sessionModel.rowCount() > 0) {
sessionList.currentIndex = (sessionList.currentIndex + sessionModel.rowCount() + 1) % sessionModel.rowCount();
}
} else if (event.key === Qt.Key_Up) {
if (sessionModel.rowCount() > 0) {
sessionList.currentIndex = (sessionList.currentIndex + sessionModel.rowCount() - 1) % sessionModel.rowCount();
}
} else if (event.key == Qt.Key_Return || event.key == Qt.Key_Enter || event.key === Qt.Key_Space) {
selector.close();
} else if (event.key === Qt.Key_CapsLock) {
root.capsLockOn = !root.capsLockOn;
}
}
}

View File

@@ -0,0 +1,150 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
Item {
id: spinnerContainer
width: spinner.width + Config.spinnerSpacing + spinnerText.width
height: childrenRect.height
Behavior on opacity {
enabled: Config.enableAnimations
NumberAnimation {
duration: 150
}
}
Behavior on visible {
enabled: Config.enableAnimations && Config.spinnerDisplayText
ParallelAnimation {
running: spinnerContainer.visible && Config.spinnerDisplayText
NumberAnimation {
target: spinnerText
property: Config.loginAreaPosition === "left" ? "anchors.leftMargin" : (Config.loginAreaPosition === "right" ? "anchors.rightMargin" : "anchors.topMargin")
from: -spinner.height
to: Config.spinnerSpacing
duration: 300
easing.type: Easing.OutQuart
}
NumberAnimation {
target: spinner
property: "opacity"
from: 0.0
to: 1.0
duration: 200
}
}
}
Image {
id: spinner
source: Config.getIcon(Config.spinnerIcon)
width: Config.spinnerIconSize
height: width
sourceSize.width: width
sourceSize.height: height
opacity: Config.spinnerDisplayText ? 0.0 : 1.0
RotationAnimation {
target: spinner
running: spinnerContainer.visible && Config.enableAnimations
from: 0
to: 360
loops: Animation.Infinite
duration: 1200
}
MultiEffect {
source: spinner
anchors.fill: spinner
colorization: 1
colorizationColor: Config.spinnerColor
}
Component.onCompleted: {
if (Config.loginAreaPosition === "left") {
anchors.left = parent.left;
anchors.verticalCenter = parent.verticalCenter;
} else if (Config.loginAreaPosition === "right") {
anchors.right = parent.right;
anchors.verticalCenter = parent.verticalCenter;
} else {
anchors.top = parent.top;
anchors.horizontalCenter = parent.horizontalCenter;
}
}
}
Text {
id: spinnerText
visible: Config.spinnerDisplayText
text: Config.spinnerText
color: Config.spinnerColor
font.pixelSize: Config.spinnerFontSize
font.weight: Config.spinnerFontWeight
font.family: Config.spinnerFontFamily
Component.onCompleted: {
if (Config.loginAreaPosition === "left") {
anchors.left = spinner.right;
anchors.leftMargin = Config.spinnerSpacing;
anchors.verticalCenter = parent.verticalCenter;
} else if (Config.loginAreaPosition === "right") {
anchors.right = spinner.left;
anchors.rightMargin = Config.spinnerSpacing;
anchors.verticalCenter = parent.verticalCenter;
} else {
anchors.top = spinner.bottom;
anchors.topMargin = Config.spinnerSpacing;
anchors.horizontalCenter = parent.horizontalCenter;
}
}
onVisibleChanged: {
if (visible && Config.enableAnimations && Config.spinnerDisplayText) {
spinnerTextInterval.running = true;
} else {
spinnerTextAnimation.running = false;
spinnerTextInterval.running = false;
}
}
SequentialAnimation on scale {
id: spinnerTextAnimation
running: false
loops: Animation.Infinite
NumberAnimation {
from: 1.0
to: 1.05
duration: 900
easing.type: Easing.InOutQuad
}
NumberAnimation {
from: 1.05
to: 1.0
duration: 900
easing.type: Easing.InOutQuad
}
}
}
Timer {
id: spinnerTextInterval
interval: 3500
repeat: false
running: false
onTriggered: {
spinnerTextAnimation.running = true;
}
}
Component.onDestruction: {
if (spinnerTextInterval) {
spinnerTextInterval.running = false;
spinnerTextInterval.stop();
}
if (spinnerTextAnimation) {
spinnerTextAnimation.running = false;
spinnerTextAnimation.stop();
}
}
}

View File

@@ -0,0 +1,165 @@
import QtQuick
import QtQuick.Controls
import SddmComponents
Item {
id: selector
signal openUserList
signal closeUserList
signal userChanged(userIndex: int, username: string, userRealName: string, userIcon: string, needsPassword: bool)
property bool listUsers: false
property string orientation: ""
property bool isDragging: false
function prevUser() {
userList.decrementCurrentIndex();
}
function nextUser() {
userList.incrementCurrentIndex();
}
ListView {
id: userList
anchors.fill: parent
orientation: selector.orientation === "horizontal" ? ListView.Horizontal : ListView.Vertical
spacing: 10
interactive: false
boundsBehavior: Flickable.StopAtBounds
// Center the active avatar
preferredHighlightBegin: selector.orientation === "horizontal" ? (width - Config.avatarActiveSize) / 2 : (height - Config.avatarActiveSize) / 2
preferredHighlightEnd: preferredHighlightBegin
highlightRangeMode: ListView.StrictlyEnforceRange
// Padding for centering
leftMargin: selector.orientation === "horizontal" ? preferredHighlightBegin : 0
rightMargin: leftMargin
topMargin: selector.orientation === "horizontal" ? 0 : preferredHighlightBegin
bottomMargin: topMargin
// Animation properties
highlightMoveDuration: 200
highlightResizeDuration: 200
highlightMoveVelocity: -1
highlightFollowsCurrentItem: true
model: userModel
currentIndex: userModel.lastIndex
onCurrentIndexChanged: {
var username = userModel.data(userModel.index(currentIndex, 0), 257);
var userRealName = userModel.data(userModel.index(currentIndex, 0), 258);
var userIcon = userModel.data(userModel.index(currentIndex, 0), 260);
var needsPasswd = userModel.data(userModel.index(currentIndex, 0), 261);
sddm.currentUser = username;
selector.userChanged(currentIndex, username, userRealName, userIcon, needsPasswd);
}
delegate: Rectangle {
width: index === userList.currentIndex ? Config.avatarActiveSize : Config.avatarInactiveSize
height: index === userList.currentIndex ? Config.avatarActiveSize : Config.avatarInactiveSize
anchors {
verticalCenter: selector.orientation === "horizontal" ? parent.verticalCenter : undefined
horizontalCenter: selector.orientation === "horizontal" ? undefined : parent.horizontalCenter
}
color: "transparent"
visible: selector.listUsers || index === userList.currentIndex
Behavior on width {
enabled: Config.enableAnimations
NumberAnimation {
duration: 200
easing.type: Easing.OutQuad
}
}
Behavior on height {
enabled: Config.enableAnimations
NumberAnimation {
duration: 200
easing.type: Easing.OutQuad
}
}
opacity: selector.listUsers || index === userList.currentIndex ? 1.0 : 0.0
Behavior on opacity {
enabled: Config.enableAnimations
NumberAnimation {
duration: 200
}
}
Avatar {
width: parent.width
height: parent.height
source: model.icon
active: index === userList.currentIndex
opacity: active ? 1.0 : Config.avatarInactiveOpacity
enabled: userModel.rowCount() > 1 // No need to open the selector if there's only one user
tooltipText: active && selector.listUsers ? "Close user selection" : (active && !listUsers ? "Select user" : `Select user ${model.name}`)
showTooltip: selector.focus && !listUsers && active
Behavior on opacity {
enabled: Config.enableAnimations
NumberAnimation {
duration: 200
}
}
onClicked: {
if (!selector.listUsers) {
// Open selector
selector.openUserList();
selector.focus = true;
userList.model.reset();
} else {
// Collapse the list if the selected user gets another click
if (index === userList.currentIndex) {
selector.closeUserList();
selector.focus = false;
}
userList.currentIndex = index;
}
}
onClickedOutside: {
selector.closeUserList();
selector.focus = false;
}
}
}
}
Keys.onPressed: function (event) {
if (event.key == Qt.Key_Return || event.key == Qt.Key_Enter || event.key === Qt.Key_Space) {
if (selector.listUsers) {
selector.closeUserList();
selector.focus = false;
} else {
selector.openUserList();
selector.focus = true;
}
event.accepted = true;
} else if (event.key == Qt.Key_Escape) {
selector.closeUserList();
selector.focus = false;
event.accepted = true;
} else if ((selector.orientation === "horizontal" && event.key == Qt.Key_Left) || (selector.orientation === "vertical" && event.key == Qt.Key_Up)) {
if (userModel.rowCount() > 0) {
userList.currentIndex = (userList.currentIndex + userModel.rowCount() - 1) % userModel.rowCount();
}
selector.focus = true;
event.accepted = true;
} else if ((selector.orientation === "horizontal" && event.key == Qt.Key_Right) || (selector.orientation === "vertical" && event.key == Qt.Key_Down)) {
if (userModel.rowCount() > 0) {
userList.currentIndex = (userList.currentIndex + userModel.rowCount() + 1) % userModel.rowCount();
}
selector.focus = true;
event.accepted = true;
} else if (event.key === Qt.Key_CapsLock) {
root.capsLockOn = !root.capsLockOn;
event.accepted = true;
} else {
// Do not steal other keys
event.accepted = false;
}
}
}

View File

@@ -0,0 +1,103 @@
import QtQuick
import QtQuick.VirtualKeyboard
import QtQuick.VirtualKeyboard.Settings
InputPanel {
id: inputPanel
width: Math.min(loginScreen && loginScreen.width ? loginScreen.width / 2 : 800, 600) * Config.virtualKeyboardScale
active: Qt.inputMethod.visible
visible: loginScreen && loginScreen.showKeyboard && loginScreen.state !== "selectingUser" && loginScreen.state !== "authenticating"
opacity: visible ? 1.0 : 0.0
externalLanguageSwitchEnabled: true
onExternalLanguageSwitch: {
if (loginScreen && loginScreen.toggleLayoutPopup) {
loginScreen.toggleLayoutPopup();
}
}
Component.onCompleted: {
VirtualKeyboardSettings.styleName = "vkeyboardStyle";
VirtualKeyboardSettings.layout = "symbols";
}
property string pos: Config.virtualKeyboardPosition
property point loginLayoutPosition: loginContainer && loginLayout ? loginContainer.mapToGlobal(loginLayout.x, loginLayout.y) : Qt.point(0, 0)
property bool vKeyboardMoved: false
x: {
if (pos === "top" || pos === "bottom") {
return (parent.width - inputPanel.width) / 2;
} else if (pos === "left") {
return Config.menuAreaButtonsMarginLeft;
} else if (pos === "right") {
return parent.width - inputPanel.width - Config.menuAreaButtonsMarginRight;
} else {
// pos === "login"
if (Config.loginAreaPosition === "left" && Config.loginAreaMargin !== -1) {
return Config.loginAreaMargin;
} else if (Config.loginAreaPosition === "right" && Config.loginAreaMargin !== -1) {
return parent.width - inputPanel.width - Config.loginAreaMargin;
} else {
return (parent.width - inputPanel.width) / 2;
}
}
}
y: {
if (pos === "top") {
return Config.menuAreaButtonsMarginTop;
} else if (pos === "bottom") {
return parent.height - inputPanel.height - Config.menuAreaButtonsMarginBottom;
} else if (pos === "right" || pos === "left") {
return (parent.height - inputPanel.height) / 2;
} else {
// pos === "login"
if (!vKeyboardMoved) {
if (loginMessage && loginMessage.visible && Config.loginAreaPosition !== "right" && Config.loginAreaPosition !== "left") {
return loginLayoutPosition.y + (loginLayout ? loginLayout.height : 0) + (loginMessage ? loginMessage.height * 2 : 0) + Config.warningMessageMarginTop + Config.warningMessageMarginTop;
} else {
return loginLayoutPosition.y + (loginLayout ? loginLayout.height : 0) + (loginMessage ? loginMessage.height * 2 : 0) + Config.warningMessageMarginTop;
}
}
return y;
}
}
Behavior on y {
enabled: Config.enableAnimations
NumberAnimation {
duration: 150
}
}
Behavior on x {
enabled: Config.enableAnimations
NumberAnimation {
duration: 150
}
}
Behavior on opacity {
enabled: Config.enableAnimations
NumberAnimation {
duration: 250
}
}
MouseArea {
id: vKeyboardDragArea
property point initialPosition: Qt.point(-1, -1)
anchors.fill: parent
hoverEnabled: true
cursorShape: loginScreen && loginScreen.userNeedsPassword ? Qt.ArrowCursor : Qt.ForbiddenCursor
drag.target: inputPanel
acceptedButtons: loginScreen && loginScreen.userNeedsPassword ? Qt.MiddleButton : Qt.MiddleButton
onPressed: function (event) {
cursorShape = Qt.ClosedHandCursor;
initialPosition = Qt.point(event.x, event.y);
}
onReleased: function (event) {
cursorShape = loginScreen && loginScreen.userNeedsPassword ? Qt.ArrowCursor : Qt.ForbiddenCursor;
if (initialPosition !== Qt.point(event.x, event.y) && !inputPanel.vKeyboardMoved) {
inputPanel.vKeyboardMoved = true;
}
}
}
}

View File

@@ -0,0 +1,2 @@
singleton Config Config.qml
singleton Languages Languages.qml