1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2025-07-05 16:15:26 +02:00

jamiidentifier: fix some UI issues

Introduces some new, more composable base components in an effort
to reduce synthetic "editing" states and have a more natural focus
change response.

Change-Id: I35a51f93ad6f92c0154fd0c40e944af6f54cbba2
This commit is contained in:
Andreas Traczyk 2022-10-03 17:01:54 -04:00
parent d6ed9adf32
commit b2c7fc0414
9 changed files with 506 additions and 181 deletions

View file

@ -204,5 +204,8 @@
<file>src/app/mainview/components/BackupTipBox.qml</file>
<file>src/app/mainview/components/InformativeTipBox.qml</file>
<file>src/app/commoncomponents/TimestampInfo.qml</file>
<file>src/app/commoncomponents/MaterialTextField.qml</file>
<file>src/app/commoncomponents/ModalTextEdit.qml</file>
<file>src/app/commoncomponents/UsernameTextEdit.qml</file>
</qresource>
</RCC>

View file

@ -0,0 +1,159 @@
/*
* Copyright (C) 2022 Savoir-faire Linux Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import net.jami.Constants 1.1
TextField {
id: root
// We need to remove focus when another widget takes activeFocus,
// except the context menu.
property bool isActive: activeFocus || contextMenu.active
onActiveFocusChanged: {
if (!activeFocus && !contextMenu.active) {
root.focus = false
}
}
property bool inputIsValid: true
property string prefixIconSrc
property alias prefixIconColor: prefixIcon.color
property string suffixIconSrc
property alias suffixIconColor: suffixIcon.color
property color accent: isActive
? prefixIconColor
: JamiTheme.buttonTintedBlue
property color baseColor: JamiTheme.primaryForegroundColor
color: JamiTheme.textColor
placeholderTextColor: !isActive
? JamiTheme.transparentColor
: JamiTheme.placeholderTextColor
property alias infoTipText: infoTip.text
wrapMode: Text.Wrap
font.pointSize: JamiTheme.materialLineEditPointSize
font.kerning: true
selectByMouse: true
mouseSelectionMode: TextInput.SelectCharacters
height: implicitHeight
leftPadding: readOnly || prefixIconSrc === '' ? 0 : 32
rightPadding: readOnly || suffixIconSrc === '' ? 0 : 32
bottomPadding: 20
topPadding: 2
onIsActiveChanged: if (!isActive && !readOnly) text = ''
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Enter
|| event.key === Qt.Key_Return) {
if (inputIsValid) {
root.accepted()
}
event.accepted = true
}
}
// Context menu.
LineEditContextMenu {
id: contextMenu
lineEditObj: root
selectOnly: readOnly
}
onReleased: function (event) {
if (event.button === Qt.RightButton)
contextMenu.openMenuAt(event)
}
// The centered placeholder that appears in the design specs.
Label {
id: overBaseLineLabel
font.pointSize: root.font.pointSize
anchors.baseline: root.baseline
anchors.horizontalCenter: root.horizontalCenter
text: root.placeholderText
color: root.baseColor
visible: !root.isActive && !readOnly
}
Rectangle {
id: baselineLine
width: parent.width
height: 1
anchors.top: root.baseline
anchors.topMargin: root.font.pointSize
color: root.accent
visible: !readOnly
}
component TextFieldIcon: ResponsiveImage {
property real size: 18
width: visible ? size : 0
height: size
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -root.bottomPadding / 2
opacity: root.isActive && !readOnly && source.toString() !== ''
visible: opacity
HoverHandler { cursorShape: Qt.ArrowCursor }
Behavior on opacity {
NumberAnimation { duration: JamiTheme.longFadeDuration }
}
}
TextFieldIcon {
id: prefixIcon
anchors.left: parent.left
color: prefixIconColor
source: prefixIconSrc
}
Label {
id: underBaseLineLabel
font.pointSize: root.font.pointSize - 3
anchors.top: baselineLine.bottom
anchors.topMargin: 2
text: root.placeholderText
color: root.baseColor
// Show the alternate placeholder while the user types.
visible: root.text.toString() !== '' && !readOnly
}
TextFieldIcon {
id: suffixIcon
size: 20
anchors.right: parent.right
color: suffixIconColor
source: suffixIconSrc
MaterialToolTip {
id: infoTip
textColor: JamiTheme.blackColor
backGroundColor: JamiTheme.whiteColor
visible: parent.hovered && infoTipText.toString() !== ''
delay: Qt.styleHints.mousePressAndHoldInterval
}
}
background: null
}

View file

@ -0,0 +1,88 @@
/*
* Copyright (C) 2022 Savoir-faire Linux Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick
import net.jami.Constants 1.1
// This component is used to display and edit a value.
Loader {
id: root
property string prefixIconSrc
property color prefixIconColor
property string suffixIconSrc
property color suffixIconColor
required property string placeholderText
required property string staticText
property string dynamicText
property bool inputIsValid: true
property string infoTipText
property variant validator
property real fontPointSize: JamiTheme.materialLineEditPointSize
// Always start with the static text component displayed first.
property bool editMode: false
// Emitted when the editor has been accepted.
signal accepted
// Always give up focus when accepted.
onAccepted: focus = false
// This is used when the user is not editing the text.
Component {
id: usernameDisplayComp
MaterialTextField {
font.pointSize: root.fontPointSize
readOnly: true
text: staticText
horizontalAlignment: TextEdit.AlignHCenter
}
}
// This is used when the user is editing the text.
Component {
id: usernameEditComp
MaterialTextField {
focus: true
infoTipText: root.infoTipText
prefixIconSrc: root.prefixIconSrc
prefixIconColor: root.prefixIconColor
suffixIconSrc: root.suffixIconSrc
suffixIconColor: root.suffixIconColor
font.pointSize: root.fontPointSize
placeholderText: root.placeholderText
validator: root.validator
onAccepted: root.accepted()
onTextChanged: dynamicText = text
inputIsValid: root.inputIsValid
onFocusChanged: if (!focus) root.editMode = false
}
}
// We use a loader to switch between the two components depending on the
// editMode property.
sourceComponent: {
editMode
? usernameEditComp
: usernameDisplayComp
}
}

View file

@ -0,0 +1,126 @@
/*
* Copyright (C) 2022 Savoir-faire Linux Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick
import net.jami.Adapters 1.1
import net.jami.Constants 1.1
import net.jami.Models 1.1
ModalTextEdit {
id: root
prefixIconSrc: {
switch(nameRegistrationState){
case UsernameLineEdit.NameRegistrationState.FREE:
return JamiResources.circled_green_check_svg
case UsernameLineEdit.NameRegistrationState.INVALID:
case UsernameLineEdit.NameRegistrationState.TAKEN:
return JamiResources.circled_red_cross_svg
case UsernameLineEdit.NameRegistrationState.BLANK:
default:
return JamiResources.person_24dp_svg
}
}
prefixIconColor: {
switch(nameRegistrationState){
case UsernameLineEdit.NameRegistrationState.FREE:
return "#009980"
case UsernameLineEdit.NameRegistrationState.INVALID:
case UsernameLineEdit.NameRegistrationState.TAKEN:
return "#CC0022"
case UsernameLineEdit.NameRegistrationState.BLANK:
default:
return JamiTheme.editLineColor
}
}
suffixIconSrc: JamiResources.outline_info_24dp_svg
suffixIconColor: JamiTheme.buttonTintedBlue
property string infohash: CurrentAccount.uri
property string registeredName: CurrentAccount.registeredName
property bool hasRegisteredName: registeredName !== ''
infoTipText: JamiStrings.usernameToolTip
placeholderText: JamiStrings.chooseAUsername
staticText: hasRegisteredName ? registeredName : infohash
enum NameRegistrationState { BLANK, INVALID, TAKEN, FREE, SEARCHING }
property int nameRegistrationState: UsernameLineEdit.NameRegistrationState.BLANK
validator: RegularExpressionValidator { regularExpression: /[A-z0-9_]{0,32}/ }
inputIsValid: dynamicText.length === 0
|| nameRegistrationState === UsernameLineEdit.NameRegistrationState.FREE
Connections {
target: CurrentAccount
function onRegisteredNameChanged() {
root.editMode = false
}
}
Connections {
id: registeredNameFoundConnection
target: NameDirectory
enabled: dynamicText.length !== 0
function onRegisteredNameFound(status, address, name) {
if (dynamicText === name) {
switch(status) {
case NameDirectory.LookupStatus.NOT_FOUND:
nameRegistrationState = UsernameLineEdit.NameRegistrationState.FREE
break
case NameDirectory.LookupStatus.ERROR:
case NameDirectory.LookupStatus.INVALID_NAME:
case NameDirectory.LookupStatus.INVALID:
nameRegistrationState = UsernameLineEdit.NameRegistrationState.INVALID
break
case NameDirectory.LookupStatus.SUCCESS:
nameRegistrationState = UsernameLineEdit.NameRegistrationState.TAKEN
break
}
}
}
}
Timer {
id: lookupTimer
repeat: false
interval: JamiTheme.usernameLineEditlookupInterval
onTriggered: {
if (dynamicText.length !== 0) {
nameRegistrationState = UsernameLineEdit.NameRegistrationState.SEARCHING
NameDirectory.lookupName(CurrentAccount.id, dynamicText)
} else {
nameRegistrationState = UsernameLineEdit.NameRegistrationState.BLANK
}
}
}
onDynamicTextChanged: lookupTimer.restart()
function startEditing() {
if (!hasRegisteredName) {
root.editMode = true
forceActiveFocus()
nameRegistrationState = UsernameLineEdit.NameRegistrationState.BLANK
}
}
}

View file

@ -30,6 +30,8 @@ Loader {
property int contextMenuItemPreferredHeight: 0
property int contextMenuSeparatorPreferredHeight: 0
active: false
function openMenu() {
root.active = true
root.sourceComponent = menuComponent

View file

@ -1,6 +1,5 @@
/*
* Copyright (C) 2022 Savoir-faire Linux Inc.
* Author: Fadi Shehadeh <fadi.shehadeh@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -19,6 +18,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import net.jami.Models 1.1
import net.jami.Adapters 1.1
@ -27,182 +27,134 @@ import net.jami.Constants 1.1
import "../../commoncomponents"
import "../../settingsview/components"
Rectangle {
Item {
id: root
NameRegistrationDialog {
id : nameRegistrationDialog
onAccepted: jamiRegisteredNameText.nameRegistrationState =
onAccepted: usernameTextEdit.nameRegistrationState =
UsernameLineEdit.NameRegistrationState.BLANK
}
property bool editable: false
property bool editing: false
width: childrenRect.width
height: controlsLayout.height + usernameTextEdit.height
+ 2 * JamiTheme.preferredMarginSize
radius: 20
Layout.bottomMargin: JamiTheme.jamiIdMargins
Layout.leftMargin: JamiTheme.jamiIdMargins
property var minWidth: mainRectangle.width + secondLine.implicitWidth
width: Math.max(minWidth, jamiRegisteredNameText.width + 2 * JamiTheme.preferredMarginSize)
height: firstLine.implicitHeight + jamiRegisteredNameText.height + 12
color: JamiTheme.secondaryBackgroundColor
// Background rounded rectangle.
Rectangle {
id: outerRect
anchors.fill: parent; radius: 20
color: JamiTheme.secondaryBackgroundColor
}
ColumnLayout {
RowLayout {
id: firstLine
Layout.alignment: Qt.AlignTop
Layout.preferredWidth: root.width
// Logo masked by outerRect.
Item {
anchors.fill: outerRect
layer.enabled: true; layer.effect: OpacityMask { maskSource: outerRect }
Rectangle {
id: mainRectangle
Rectangle {
id: logoRect
width: 97 + radius
height: 40
color: JamiTheme.mainColor
radius: 20
anchors.top: parent.top
anchors.left: parent.left
anchors.leftMargin: -radius
width: 97
height: 40
color: JamiTheme.mainColor
radius: 20
Rectangle {
id: rectForRadius
anchors.bottom: parent.bottom
width: 20
height: 20
color: JamiTheme.mainColor
}
ResponsiveImage {
id: jamiIdLogo
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
width: JamiTheme.jamiIdLogoWidth
height: JamiTheme.jamiIdLogoHeight
opacity: 1
source: JamiResources.jamiid_svg
}
}
RowLayout {
id: secondLine
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
PushButton {
id: btnEdit
preferredSize : 30
imageContainerWidth: JamiTheme.pushButtonSize
imageContainerHeight: JamiTheme.pushButtonSize
Layout.topMargin: JamiTheme.pushButtonMargin
imageColor: enabled ? JamiTheme.buttonTintedBlue : JamiTheme.buttonTintedBlack
normalColor: JamiTheme.transparentColor
hoveredColor: JamiTheme.transparentColor
visible: editable && CurrentAccount.registeredName === ""
border.color: enabled ? JamiTheme.buttonTintedBlue : JamiTheme.buttonTintedBlack
enabled: {
switch(jamiRegisteredNameText.nameRegistrationState) {
case UsernameLineEdit.NameRegistrationState.BLANK:
case UsernameLineEdit.NameRegistrationState.FREE:
return true
case UsernameLineEdit.NameRegistrationState.SEARCHING:
case UsernameLineEdit.NameRegistrationState.INVALID:
case UsernameLineEdit.NameRegistrationState.TAKEN:
return false
}
}
source: JamiResources.round_edit_24dp_svg
toolTipText: JamiStrings.chooseUsername
onClicked: {
if (!root.editing) {
root.editing = !root.editing
source = JamiResources.check_black_24dp_svg
jamiRegisteredNameText.text = ""
jamiRegisteredNameText.forceActiveFocus()
} else {
root.editing = !root.editing
source = JamiResources.round_edit_24dp_svg
jamiRegisteredNameText.accepted()
jamiRegisteredNameText.focus = false
}
}
}
PushButton {
id: btnCopy
imageColor: JamiTheme.buttonTintedBlue
normalColor: JamiTheme.transparentColor
hoveredColor: JamiTheme.transparentColor
Layout.topMargin: JamiTheme.pushButtonMargin
preferredSize : 30
imageContainerWidth: JamiTheme.pushButtonSize
imageContainerHeight: JamiTheme.pushButtonSize
border.color: JamiTheme.tintedBlue
source: JamiResources.content_copy_24dp_svg
toolTipText: JamiStrings.copy
onClicked: UtilsAdapter.setClipboardText(CurrentAccount.bestId)
}
PushButton {
id: btnShare
imageColor: JamiTheme.buttonTintedBlue
normalColor: JamiTheme.transparentColor
hoveredColor: JamiTheme.transparentColor
Layout.topMargin: JamiTheme.pushButtonMargin
Layout.rightMargin: JamiTheme.pushButtonMargin
preferredSize : 30
imageContainerWidth: JamiTheme.pushButtonSize
imageContainerHeight: JamiTheme.pushButtonSize
border.color: JamiTheme.buttonTintedBlue
source: JamiResources.share_24dp_svg
toolTipText: JamiStrings.share
onClicked: qrDialog.open()
}
}
}
UsernameLineEdit {
id: jamiRegisteredNameText
readOnly: !root.editing
Layout.preferredWidth: 330
horizontalAlignment: Qt.AlignHCenter
Layout.leftMargin: JamiTheme.preferredMarginSize
Layout.rightMargin: JamiTheme.preferredMarginSize
backgroundColor: JamiTheme.secondaryBackgroundColor
font.pointSize: JamiTheme.textFontSize + 1
text: CurrentAccount.bestId
color: JamiTheme.textColor
onAccepted: {
if (!btnEdit.enabled)
return
if (text.length === 0) {
text = CurrentAccount.bestId
} else {
nameRegistrationDialog.openNameRegistrationDialog(text)
}
ResponsiveImage {
id: jamiIdLogo
anchors.horizontalCenter: parent.horizontalCenter
// Adjust offset for parent masking margin.
anchors.horizontalCenterOffset: parent.radius / 2
anchors.verticalCenter: parent.verticalCenter
width: JamiTheme.jamiIdLogoWidth
height: JamiTheme.jamiIdLogoHeight
source: JamiResources.jamiid_svg
}
}
}
}
ColumnLayout {
id: columnLayout
spacing: JamiTheme.preferredMarginSize
RowLayout {
id: controlsLayout
Layout.alignment: Qt.AlignTop | Qt.AlignRight
Layout.topMargin: JamiTheme.pushButtonMargin
Layout.rightMargin: JamiTheme.pushButtonMargin
Layout.preferredHeight: childrenRect.height
component JamiIdControlButton: PushButton {
preferredSize : 30
normalColor: JamiTheme.transparentColor
hoveredColor: JamiTheme.transparentColor
imageContainerWidth: JamiTheme.pushButtonSize
imageContainerHeight: JamiTheme.pushButtonSize
border.color: JamiTheme.tintedBlue
imageColor: JamiTheme.buttonTintedBlue
}
JamiIdControlButton {
id: btnEdit
visible: CurrentAccount.registeredName === ""
border.color: enabled ? JamiTheme.buttonTintedBlue : JamiTheme.buttonTintedBlack
imageColor: enabled ? JamiTheme.buttonTintedBlue : JamiTheme.buttonTintedBlack
enabled: {
if (!usernameTextEdit.editMode)
return true
switch(usernameTextEdit.nameRegistrationState) {
case UsernameLineEdit.NameRegistrationState.BLANK:
case UsernameLineEdit.NameRegistrationState.FREE:
return true
case UsernameLineEdit.NameRegistrationState.SEARCHING:
case UsernameLineEdit.NameRegistrationState.INVALID:
case UsernameLineEdit.NameRegistrationState.TAKEN:
return false
}
}
source: usernameTextEdit.editMode
? JamiResources.check_black_24dp_svg
: JamiResources.round_edit_24dp_svg
toolTipText: JamiStrings.chooseUsername
onClicked: {
if (!usernameTextEdit.editMode) {
usernameTextEdit.startEditing()
} else {
usernameTextEdit.accepted()
}
}
}
JamiIdControlButton {
id: btnCopy
source: JamiResources.content_copy_24dp_svg
toolTipText: JamiStrings.copy
onClicked: UtilsAdapter.setClipboardText(CurrentAccount.bestId)
}
JamiIdControlButton {
id: btnShare
source: JamiResources.share_24dp_svg
toolTipText: JamiStrings.share
onClicked: qrDialog.open()
}
}
UsernameTextEdit {
id: usernameTextEdit
Layout.preferredWidth: 330
Layout.preferredHeight: implicitHeight + JamiTheme.preferredMarginSize
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
Layout.leftMargin: JamiTheme.preferredMarginSize
Layout.rightMargin: JamiTheme.preferredMarginSize
fontPointSize: JamiTheme.textFontSize + 1
onAccepted: nameRegistrationDialog.openNameRegistrationDialog(dynamicText)
}
}
}

View file

@ -31,13 +31,13 @@ import "../../commoncomponents"
Item {
id: root
property var title: ""
property var description: ""
property string title: ""
property string description: ""
property int tipId: 0
property string type : ""
property bool hovered: false
property bool clicked : false
property bool opened: false
property bool opened: activeFocus
property string customizeTip:"CustomizeTipBox {}"
@ -89,7 +89,7 @@ Item {
TapHandler {
target: rect
onTapped: opened = !opened
onTapped: opened ? focus = false : root.forceActiveFocus()
}
DropShadow {

View file

@ -1,6 +1,5 @@
/*
* Copyright (C) 2020-2022 Savoir-faire Linux Inc.
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
* Copyright (C) 2022 Savoir-faire Linux Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -39,11 +38,7 @@ Rectangle {
MouseArea {
anchors.fill: parent
enabled: visible
onClicked: {
for (var c in flow.children) {
flow.children[c].opened = false
}
}
onClicked: welcomeView.forceActiveFocus()
}
anchors.fill: root
@ -67,7 +62,8 @@ Rectangle {
color: JamiTheme.rectColor
anchors.topMargin: 25
anchors.horizontalCenter: parent.horizontalCenter
width: identifier.width + 2 * JamiTheme.preferredMarginSize + (welcomeLogo.visible ? welcomeLogo.width : 0)
width: identifier.width + 2 * JamiTheme.preferredMarginSize
+ (welcomeLogo.visible ? welcomeLogo.width : 0)
height: childrenRect.height
opacity:1
@ -133,9 +129,8 @@ Rectangle {
JamiIdentifier {
id: identifier
editable: true
visible: CurrentAccount.type !== Profile.Type.SIP
visible: CurrentAccount.type !== Profile.Type.SIP
anchors.top: identifierDescription.bottom
anchors.left: parent.left
anchors.margins: JamiTheme.preferredMarginSize
@ -179,6 +174,7 @@ Rectangle {
spacing: 13
Repeater {
id: tipsRepeater
model: TipsModel
Layout.alignment: Qt.AlignCenter
@ -259,6 +255,4 @@ Rectangle {
bBorderwidth: 0
borderColor: JamiTheme.tabbarBorderColor
}
}

View file

@ -35,8 +35,9 @@ BaseModalDialog {
signal accepted
function openNameRegistrationDialog(registerNameIn) {
if (registerNameIn === '')
return
registerdName = registerNameIn
open()
}