Merge pull request #893 from Nheko-Reborn/qml-root

Qml root
This commit is contained in:
DeepBlueV7.X 2022-01-30 19:18:32 +00:00 committed by GitHub
commit b706e272e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
78 changed files with 1755 additions and 4368 deletions

View File

@ -331,24 +331,13 @@ set(SRC_FILES
src/timeline/RoomlistModel.cpp
# UI components
src/ui/DropShadow.cpp
src/ui/FlatButton.cpp
src/ui/Label.cpp
src/ui/LoadingIndicator.cpp
src/ui/MxcAnimatedImage.cpp
src/ui/MxcMediaProxy.cpp
src/ui/NhekoCursorShape.cpp
src/ui/NhekoDropArea.cpp
src/ui/NhekoGlobalObject.cpp
src/ui/OverlayModal.cpp
src/ui/OverlayWidget.cpp
src/ui/RaisedButton.cpp
src/ui/Ripple.cpp
src/ui/RippleOverlay.cpp
src/ui/RoomSettings.cpp
src/ui/SnackBar.cpp
src/ui/TextField.cpp
src/ui/TextLabel.cpp
src/ui/Theme.cpp
src/ui/ThemeManager.cpp
src/ui/ToggleButton.cpp
@ -395,7 +384,6 @@ set(SRC_FILES
src/RoomDirectoryModel.cpp
src/RoomsModel.cpp
src/Utils.cpp
src/WelcomePage.cpp
src/main.cpp
third_party/blurhash/blurhash.cpp
@ -414,7 +402,7 @@ if(USE_BUNDLED_MTXCLIENT)
FetchContent_Declare(
MatrixClient
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
GIT_TAG 6a7eaa5006b1a18e132be7655e490d9819158dca
GIT_TAG 9781553b0186f2db9036d2abbde83c28828eb2b9
)
set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
@ -538,23 +526,13 @@ qt5_wrap_cpp(MOC_HEADERS
src/timeline/RoomlistModel.h
# UI components
src/ui/FlatButton.h
src/ui/Label.h
src/ui/LoadingIndicator.h
src/ui/MxcAnimatedImage.h
src/ui/MxcMediaProxy.h
src/ui/Menu.h
src/ui/NhekoCursorShape.h
src/ui/NhekoDropArea.h
src/ui/NhekoGlobalObject.h
src/ui/OverlayWidget.h
src/ui/RaisedButton.h
src/ui/Ripple.h
src/ui/RippleOverlay.h
src/ui/RoomSettings.h
src/ui/SnackBar.h
src/ui/TextField.h
src/ui/TextLabel.h
src/ui/Theme.h
src/ui/ThemeManager.h
src/ui/ToggleButton.h
@ -596,7 +574,6 @@ qt5_wrap_cpp(MOC_HEADERS
src/UsersModel.h
src/RoomDirectoryModel.h
src/RoomsModel.h
src/WelcomePage.h
src/ReadReceiptsModel.h
)

View File

@ -191,7 +191,7 @@ modules:
buildsystem: cmake-ninja
name: mtxclient
sources:
- commit: 6a7eaa5006b1a18e132be7655e490d9819158dca
- commit: 9781553b0186f2db9036d2abbde83c28828eb2b9
#tag: v0.6.1
type: git
url: https://github.com/Nheko-Reborn/mtxclient.git

View File

@ -319,7 +319,7 @@
<message>
<location line="+66"/>
<source>Failed to kick %1 from %2: %3</source>
<translation>Kontte %1 nicht aus %2 entfernen: %3</translation>
<translation>Konnte %1 nicht aus %2 entfernen: %3</translation>
</message>
</context>
<context>

View File

@ -97,6 +97,7 @@ Rectangle {
implicitHeight: chatPage.height
collapsed: parent.collapsed
anchors.fill: parent
}
Binding {

View File

@ -8,29 +8,139 @@ import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import im.nheko 1.0
TextField {
id: input
property alias backgroundColor: backgroundRect.color
ColumnLayout {
id: c
property color backgroundColor: Nheko.colors.base
property alias color: labelC.color
property alias textPadding: input.padding
property alias text: input.text
property alias label: labelC.text
property alias placeholderText: input.placeholderText
property alias font: input.font
property alias echoMode: input.echoMode
property alias selectByMouse: input.selectByMouse
palette: Nheko.colors
color: Nheko.colors.text
Timer {
id: timer
interval: 350
onTriggered: editingFinished()
}
onTextChanged: timer.restart()
signal textEdited
signal accepted
signal editingFinished
function forceActiveFocus() {
input.forceActiveFocus();
}
ToolTip.delay: Nheko.tooltipDelay
ToolTip.visible: hover.hovered
spacing: 0
Item {
Layout.fillWidth: true
Layout.preferredHeight: labelC.contentHeight
Layout.margins: input.padding
Layout.bottomMargin: Nheko.paddingSmall
visible: labelC.text
z: 1
Label {
id: labelC
y: contentHeight + input.padding + Nheko.paddingSmall
enabled: false
palette: Nheko.colors
color: Nheko.colors.text
font.pixelSize: input.font.pixelSize
font.weight: Font.DemiBold
font.letterSpacing: input.font.pixelSize * 0.02
width: parent.width
state: labelC.text && (input.activeFocus == true || input.text) ? "focused" : ""
states: State {
name: "focused"
PropertyChanges {
target: labelC
y: 0
}
PropertyChanges {
target: input
opacity: 1
}
}
transitions: Transition {
from: ""
to: "focused"
reversible: true
NumberAnimation {
target: labelC
properties: "y"
duration: 210
easing.type: Easing.InCubic
alwaysRunToEnd: true
}
NumberAnimation {
target: input
properties: "opacity"
duration: 210
easing.type: Easing.InCubic
alwaysRunToEnd: true
}
}
}
}
TextField {
id: input
Layout.fillWidth: true
palette: Nheko.colors
color: labelC.color
opacity: labelC.text ? 0 : 1
onTextEdited: c.textEdited()
onAccepted: c.accepted()
onEditingFinished: c.editingFinished()
background: Rectangle {
id: backgroundRect
color: labelC.text ? "transparent" : backgroundColor
}
}
Rectangle {
id: blueBar
anchors.top: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
Layout.fillWidth: true
color: Nheko.colors.highlight
height: 1
width: parent.width
Rectangle {
id: blackBar
anchors.verticalCenter: blueBar.verticalCenter
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
height: parent.height + 1
height: parent.height*2
width: 0
color: Nheko.colors.text
@ -50,11 +160,12 @@ TextField {
to: "focused"
reversible: true
NumberAnimation {
target: blackBar
properties: "width"
duration: 500
easing.type: Easing.InOutQuad
duration: 310
easing.type: Easing.InCubic
alwaysRunToEnd: true
}
@ -64,10 +175,8 @@ TextField {
}
background: Rectangle {
id: backgroundRect
color: Nheko.colors.base
HoverHandler {
id: hover
enabled: c.ToolTip.text
}
}

View File

@ -5,6 +5,7 @@
import QtGraphicalEffects 1.0
import QtQuick 2.12
import QtQuick.Window 2.2
import im.nheko 1.0
Item {
@ -15,7 +16,7 @@ Item {
Connections {
function onFocusChanged() {
if (TimelineManager.isWindowFocused) {
if (MainWindow.active) {
screenSaverTimer.stop();
screenSaver.state = "Invisible";
} else {
@ -32,7 +33,7 @@ Item {
id: screenSaverTimer
interval: screenTimeout * 1000
running: true
running: !MainWindow.active
onTriggered: {
screenSaver.state = "Visible";
}

View File

@ -11,7 +11,6 @@ Popup {
id: quickSwitcher
property int textHeight: Math.round(Qt.application.font.pixelSize * 2.4)
property int textMargin: Math.round(textHeight / 8)
background: null
width: Math.round(parent.width / 2)
@ -34,7 +33,6 @@ Popup {
anchors.fill: parent
font.pixelSize: Math.ceil(quickSwitcher.textHeight * 0.6)
padding: textMargin
color: Nheko.colors.text
onTextEdited: {
completerPopup.completer.searchString = text;
@ -60,7 +58,7 @@ Popup {
id: completerPopup
x: roomTextInput.x
y: roomTextInput.y + quickSwitcher.textHeight + quickSwitcher.textMargin
y: roomTextInput.y + quickSwitcher.textHeight
visible: roomTextInput.length > 0
width: parent.width
completerName: "room"

View File

@ -385,7 +385,7 @@ Page {
header: ColumnLayout {
spacing: 0
Rectangle {
Pane {
id: userInfoPanel
function openUserProfile() {
@ -396,12 +396,15 @@ Page {
userProfile.show();
}
color: Nheko.colors.window
Layout.fillWidth: true
Layout.alignment: Qt.AlignBottom
Layout.preferredHeight: userInfoGrid.implicitHeight + 2 * Nheko.paddingMedium
//Layout.preferredHeight: userInfoGrid.implicitHeight + 2 * Nheko.paddingMedium
padding: Nheko.paddingMedium
Layout.minimumHeight: 40
background: Rectangle {color: Nheko.colors.window}
InputDialog {
id: statusDialog
@ -442,14 +445,12 @@ Page {
gesturePolicy: TapHandler.ReleaseWithinBounds
}
RowLayout {
contentItem: RowLayout {
id: userInfoGrid
property var profile: Nheko.currentUser
spacing: Nheko.paddingMedium
anchors.fill: parent
anchors.margins: Nheko.paddingMedium
Avatar {
id: avatar
@ -614,19 +615,17 @@ Page {
Layout.fillWidth: true
}
Rectangle {
color: Nheko.colors.window
Pane {
Layout.fillWidth: true
Layout.alignment: Qt.AlignBottom
Layout.preferredHeight: buttonRow.implicitHeight
Layout.minimumHeight: 40
RowLayout {
id: buttonRow
horizontalPadding: Nheko.paddingMedium
verticalPadding: 0
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Nheko.paddingMedium
background: Rectangle {color: Nheko.colors.window}
contentItem: RowLayout {
id: buttonRow
ImageButton {
Layout.fillWidth: true

View File

@ -9,6 +9,7 @@ import "./dialogs"
import "./emoji"
import "./pages"
import "./voip"
import "./ui"
import Qt.labs.platform 1.1 as Platform
import QtQuick 2.15
import QtQuick.Controls 2.15
@ -17,10 +18,12 @@ import QtQuick.Window 2.15
import im.nheko 1.0
import im.nheko.EmojiModel 1.0
Page {
Pane {
id: timelineRoot
palette: Nheko.colors
background: null
padding: 0
FontMetrics {
id: fontMetrics
@ -153,11 +156,15 @@ Page {
}
Shortcut {
sequence: StandardKey.Quit
onActivated: Qt.quit()
}
Shortcut {
sequence: "Ctrl+K"
onActivated: {
var quickSwitch = quickSwitcherComponent.createObject(timelineRoot);
TimelineManager.focusTimeline();
quickSwitch.open();
}
}
@ -165,7 +172,6 @@ Page {
Shortcut {
// Add alternative shortcut, because sometimes Alt+A is stolen by the TextEdit
sequences: ["Alt+A", "Ctrl+Shift+A"]
context: Qt.ApplicationShortcut
onActivated: Rooms.nextRoomWithActivity()
}
@ -366,9 +372,51 @@ Page {
id: mainWindow
anchors.fill: parent
initialItem: ChatPage {
//anchors.fill: parent
initialItem: welcomePage
}
Component {
id: welcomePage
WelcomePage {
}
}
Component {
id: chatPage
ChatPage {
}
}
Component {
id: loginPage
LoginPage {
}
}
Component {
id: registerPage
RegisterPage {
}
}
Snackbar { id: snackbar }
Connections {
function onSwitchToChatPage() {
mainWindow.replace(null, chatPage);
}
function onSwitchToLoginPage(error) {
mainWindow.replace(welcomePage, {}, loginPage, {"error": error}, StackView.PopTransition);
}
function onShowNotification(msg) {
snackbar.showNotification(msg);
console.log("New snack: " + msg);
}
target: MainWindow
}
}

View File

@ -12,7 +12,7 @@ import im.nheko 1.0
import "./delegates"
Rectangle {
Pane {
id: topBar
property bool showBackButton: false
@ -28,7 +28,11 @@ Rectangle {
Layout.fillWidth: true
implicitHeight: topLayout.height + Nheko.paddingMedium * 2
z: 3
color: Nheko.colors.window
padding: 0
background: Rectangle {
color: Nheko.colors.window
}
TapHandler {
onSingleTapped: {
@ -65,248 +69,250 @@ Rectangle {
grabPermissions: PointerHandler.TakeOverForbidden | PointerHandler.CanTakeOverFromAnything
}
GridLayout {
id: topLayout
contentItem: Item {
GridLayout {
id: topLayout
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Nheko.paddingMedium
anchors.verticalCenter: parent.verticalCenter
columnSpacing: Nheko.paddingSmall
rowSpacing: Nheko.paddingSmall
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Nheko.paddingMedium
anchors.verticalCenter: parent.verticalCenter
columnSpacing: Nheko.paddingSmall
rowSpacing: Nheko.paddingSmall
ImageButton {
id: backToRoomsButton
ImageButton {
id: backToRoomsButton
Layout.column: 0
Layout.row: 0
Layout.rowSpan: 2
Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
visible: showBackButton
image: ":/icons/icons/ui/angle-arrow-left.svg"
ToolTip.visible: hovered
ToolTip.text: qsTr("Back to room list")
onClicked: Rooms.resetCurrentRoom()
}
Avatar {
Layout.column: 1
Layout.row: 0
Layout.rowSpan: 2
Layout.alignment: Qt.AlignVCenter
width: Nheko.avatarSize
height: Nheko.avatarSize
url: avatarUrl.replace("mxc://", "image://MxcImage/")
roomid: roomId
userid: isDirect ? directChatOtherUserId : ""
displayName: roomName
enabled: false
}
Label {
Layout.fillWidth: true
Layout.column: 2
Layout.row: 0
color: Nheko.colors.text
font.pointSize: fontMetrics.font.pointSize * 1.1
text: roomName
maximumLineCount: 1
elide: Text.ElideRight
textFormat: Text.RichText
}
MatrixText {
id: roomTopicC
Layout.fillWidth: true
Layout.column: 2
Layout.row: 1
Layout.maximumHeight: fontMetrics.lineSpacing * 2 // show 2 lines
selectByMouse: false
enabled: false
clip: true
text: roomTopic
}
EncryptionIndicator {
Layout.column: 3
Layout.row: 0
Layout.rowSpan: 2
Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
sourceSize.height: Layout.preferredHeight * Screen.devicePixelRatio
sourceSize.width: Layout.preferredWidth * Screen.devicePixelRatio
visible: isEncrypted
encrypted: isEncrypted
trust: trustlevel
ToolTip.text: {
if (!encrypted)
return qsTr("This room is not encrypted!");
switch (trust) {
case Crypto.Verified:
return qsTr("This room contains only verified devices.");
case Crypto.TOFU:
return qsTr("This room contains verified devices and devices which have never changed their master key.");
default:
return qsTr("This room contains unverified devices!");
}
Layout.column: 0
Layout.row: 0
Layout.rowSpan: 2
Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
visible: showBackButton
image: ":/icons/icons/ui/angle-arrow-left.svg"
ToolTip.visible: hovered
ToolTip.text: qsTr("Back to room list")
onClicked: Rooms.resetCurrentRoom()
}
}
ImageButton {
id: pinButton
Avatar {
Layout.column: 1
Layout.row: 0
Layout.rowSpan: 2
Layout.alignment: Qt.AlignVCenter
width: Nheko.avatarSize
height: Nheko.avatarSize
url: avatarUrl.replace("mxc://", "image://MxcImage/")
roomid: roomId
userid: isDirect ? directChatOtherUserId : ""
displayName: roomName
enabled: false
}
property bool pinsShown: !Settings.hiddenPins.includes(roomId)
Label {
Layout.fillWidth: true
Layout.column: 2
Layout.row: 0
color: Nheko.colors.text
font.pointSize: fontMetrics.font.pointSize * 1.1
text: roomName
maximumLineCount: 1
elide: Text.ElideRight
textFormat: Text.RichText
}
visible: !!room && room.pinnedMessages.length > 0
Layout.column: 4
Layout.row: 0
Layout.rowSpan: 2
Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
image: pinsShown ? ":/icons/icons/ui/pin.svg" : ":/icons/icons/ui/pin-off.svg"
ToolTip.visible: hovered
ToolTip.text: qsTr("Show or hide pinned messages")
onClicked: {
var ps = Settings.hiddenPins;
if (pinsShown) {
ps.push(roomId);
} else {
const index = ps.indexOf(roomId);
if (index > -1) {
ps.splice(index, 1);
MatrixText {
id: roomTopicC
Layout.fillWidth: true
Layout.column: 2
Layout.row: 1
Layout.maximumHeight: fontMetrics.lineSpacing * 2 // show 2 lines
selectByMouse: false
enabled: false
clip: true
text: roomTopic
}
EncryptionIndicator {
Layout.column: 3
Layout.row: 0
Layout.rowSpan: 2
Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
sourceSize.height: Layout.preferredHeight * Screen.devicePixelRatio
sourceSize.width: Layout.preferredWidth * Screen.devicePixelRatio
visible: isEncrypted
encrypted: isEncrypted
trust: trustlevel
ToolTip.text: {
if (!encrypted)
return qsTr("This room is not encrypted!");
switch (trust) {
case Crypto.Verified:
return qsTr("This room contains only verified devices.");
case Crypto.TOFU:
return qsTr("This room contains verified devices and devices which have never changed their master key.");
default:
return qsTr("This room contains unverified devices!");
}
}
Settings.hiddenPins = ps;
}
}
ImageButton {
id: pinButton
ImageButton {
id: roomOptionsButton
property bool pinsShown: !Settings.hiddenPins.includes(roomId)
visible: !!room
Layout.column: 5
Layout.row: 0
Layout.rowSpan: 2
Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
image: ":/icons/icons/ui/options.svg"
ToolTip.visible: hovered
ToolTip.text: qsTr("Room options")
onClicked: roomOptionsMenu.open(roomOptionsButton)
Platform.Menu {
id: roomOptionsMenu
Platform.MenuItem {
visible: room ? room.permissions.canInvite() : false
text: qsTr("Invite users")
onTriggered: TimelineManager.openInviteUsers(roomId)
}
Platform.MenuItem {
text: qsTr("Members")
onTriggered: TimelineManager.openRoomMembers(room)
}
Platform.MenuItem {
text: qsTr("Leave room")
onTriggered: TimelineManager.openLeaveRoomDialog(roomId)
}
Platform.MenuItem {
text: qsTr("Settings")
onTriggered: TimelineManager.openRoomSettings(roomId)
visible: !!room && room.pinnedMessages.length > 0
Layout.column: 4
Layout.row: 0
Layout.rowSpan: 2
Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
image: pinsShown ? ":/icons/icons/ui/pin.svg" : ":/icons/icons/ui/pin-off.svg"
ToolTip.visible: hovered
ToolTip.text: qsTr("Show or hide pinned messages")
onClicked: {
var ps = Settings.hiddenPins;
if (pinsShown) {
ps.push(roomId);
} else {
const index = ps.indexOf(roomId);
if (index > -1) {
ps.splice(index, 1);
}
}
Settings.hiddenPins = ps;
}
}
}
ImageButton {
id: roomOptionsButton
ScrollView {
id: pinnedMessages
visible: !!room
Layout.column: 5
Layout.row: 0
Layout.rowSpan: 2
Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
image: ":/icons/icons/ui/options.svg"
ToolTip.visible: hovered
ToolTip.text: qsTr("Room options")
onClicked: roomOptionsMenu.open(roomOptionsButton)
Layout.row: 2
Layout.column: 2
Layout.columnSpan: 3
Platform.Menu {
id: roomOptionsMenu
Layout.fillWidth: true
Layout.preferredHeight: Math.min(contentHeight, Nheko.avatarSize * 4)
visible: !!room && room.pinnedMessages.length > 0 && !Settings.hiddenPins.includes(roomId)
clip: true
palette: Nheko.colors
ScrollBar.horizontal.visible: false
ListView {
spacing: Nheko.paddingSmall
model: room ? room.pinnedMessages : undefined
delegate: RowLayout {
required property string modelData
width: ListView.view.width
height: implicitHeight
Reply {
property var e: room ? room.getDump(modelData, "") : {}
Layout.fillWidth: true
Layout.preferredHeight: height
userColor: TimelineManager.userColor(e.userId, Nheko.colors.window)
blurhash: e.blurhash ?? ""
body: e.body ?? ""
formattedBody: e.formattedBody ?? ""
eventId: e.eventId ?? ""
filename: e.filename ?? ""
filesize: e.filesize ?? ""
proportionalHeight: e.proportionalHeight ?? 1
type: e.type ?? MtxEvent.UnknownMessage
typeString: e.typeString ?? ""
url: e.url ?? ""
originalWidth: e.originalWidth ?? 0
isOnlyEmoji: e.isOnlyEmoji ?? false
userId: e.userId ?? ""
userName: e.userName ?? ""
encryptionError: e.encryptionError ?? ""
Platform.MenuItem {
visible: room ? room.permissions.canInvite() : false
text: qsTr("Invite users")
onTriggered: TimelineManager.openInviteUsers(roomId)
}
ImageButton {
id: deletePinButton
Layout.preferredHeight: 16
Layout.preferredWidth: 16
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
visible: room.permissions.canChange(MtxEvent.PinnedEvents)
hoverEnabled: true
image: ":/icons/icons/ui/dismiss.svg"
ToolTip.visible: hovered
ToolTip.text: qsTr("Unpin")
onClicked: room.unpin(modelData)
Platform.MenuItem {
text: qsTr("Members")
onTriggered: TimelineManager.openRoomMembers(room)
}
Platform.MenuItem {
text: qsTr("Leave room")
onTriggered: TimelineManager.openLeaveRoomDialog(roomId)
}
Platform.MenuItem {
text: qsTr("Settings")
onTriggered: TimelineManager.openRoomSettings(roomId)
}
}
}
ScrollHelper {
flickable: parent
anchors.fill: parent
enabled: !Settings.mobileMode
ScrollView {
id: pinnedMessages
Layout.row: 2
Layout.column: 2
Layout.columnSpan: 3
Layout.fillWidth: true
Layout.preferredHeight: Math.min(contentHeight, Nheko.avatarSize * 4)
visible: !!room && room.pinnedMessages.length > 0 && !Settings.hiddenPins.includes(roomId)
clip: true
palette: Nheko.colors
ScrollBar.horizontal.visible: false
ListView {
spacing: Nheko.paddingSmall
model: room ? room.pinnedMessages : undefined
delegate: RowLayout {
required property string modelData
width: ListView.view.width
height: implicitHeight
Reply {
property var e: room ? room.getDump(modelData, "") : {}
Layout.fillWidth: true
Layout.preferredHeight: height
userColor: TimelineManager.userColor(e.userId, Nheko.colors.window)
blurhash: e.blurhash ?? ""
body: e.body ?? ""
formattedBody: e.formattedBody ?? ""
eventId: e.eventId ?? ""
filename: e.filename ?? ""
filesize: e.filesize ?? ""
proportionalHeight: e.proportionalHeight ?? 1
type: e.type ?? MtxEvent.UnknownMessage
typeString: e.typeString ?? ""
url: e.url ?? ""
originalWidth: e.originalWidth ?? 0
isOnlyEmoji: e.isOnlyEmoji ?? false
userId: e.userId ?? ""
userName: e.userName ?? ""
encryptionError: e.encryptionError ?? ""
}
ImageButton {
id: deletePinButton
Layout.preferredHeight: 16
Layout.preferredWidth: 16
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
visible: room.permissions.canChange(MtxEvent.PinnedEvents)
hoverEnabled: true
image: ":/icons/icons/ui/dismiss.svg"
ToolTip.visible: hovered
ToolTip.text: qsTr("Unpin")
onClicked: room.unpin(modelData)
}
}
ScrollHelper {
flickable: parent
anchors.fill: parent
enabled: !Settings.mobileMode
}
}
}
}
}
CursorShape {
anchors.fill: parent
anchors.bottomMargin: pinnedMessages.visible ? pinnedMessages.height : 0
cursorShape: Qt.PointingHandCursor
CursorShape {
anchors.fill: parent
anchors.bottomMargin: pinnedMessages.visible ? pinnedMessages.height : 0
cursorShape: Qt.PointingHandCursor
}
}
}

View File

@ -12,7 +12,7 @@ import im.nheko 1.0
Button {
id: control
implicitHeight: Math.ceil(control.contentItem.implicitHeight * 1.5)
implicitHeight: Math.ceil(control.contentItem.implicitHeight * 1.70)
implicitWidth: Math.ceil(control.contentItem.implicitWidth + control.contentItem.implicitHeight)
hoverEnabled: true
@ -42,7 +42,7 @@ Button {
background: Rectangle {
//height: control.contentItem.implicitHeight * 2
//width: control.contentItem.implicitWidth * 2
radius: height / 6
radius: height / 8
color: Qt.lighter(Nheko.colors.dark, control.down ? 1.4 : (control.hovered ? 1.2 : 1))
}

View File

@ -21,7 +21,6 @@ ApplicationWindow {
minimumHeight: stack.implicitHeight
width: stack.implicitWidth
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
Component.onCompleted: Nheko.reparent(dialog)
StackView {
id: stack

View File

@ -12,8 +12,6 @@ import QtQuick.Layouts 1.12
import im.nheko 1.0
ApplicationWindow {
//Component.onCompleted: Nheko.reparent(win)
id: win
property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3)
@ -175,39 +173,37 @@ ApplicationWindow {
}
}
MatrixText {
visible: imagePack.roomid
text: qsTr("State key")
}
MatrixTextField {
id: statekeyField
visible: imagePack.roomid
Layout.fillWidth: true
Layout.columnSpan: 2
label: qsTr("State key")
text: imagePack.statekey
onTextEdited: imagePack.statekey = text
}
MatrixText {
text: qsTr("Packname")
}
MatrixTextField {
Layout.fillWidth: true
Layout.columnSpan: 2
label: qsTr("Packname")
text: imagePack.packname
onTextEdited: imagePack.packname = text
}
MatrixText {
text: qsTr("Attribution")
}
MatrixTextField {
Layout.fillWidth: true
Layout.columnSpan: 2
label: qsTr("Attribution")
text: imagePack.attribution
onTextEdited: imagePack.attribution = text
}
MatrixText {
Layout.margins: statekeyField.textPadding
font.weight: Font.DemiBold
font.letterSpacing: font.pixelSize * 0.02
text: qsTr("Use as Emoji")
}
@ -218,6 +214,9 @@ ApplicationWindow {
}
MatrixText {
Layout.margins: statekeyField.textPadding
font.weight: Font.DemiBold
font.letterSpacing: font.pixelSize * 0.02
text: qsTr("Use as Sticker")
}
@ -253,27 +252,28 @@ ApplicationWindow {
Layout.alignment: Qt.AlignHCenter
}
MatrixText {
text: qsTr("Shortcode")
}
MatrixTextField {
Layout.fillWidth: true
Layout.columnSpan: 2
label: qsTr("Shortcode")
text: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.ShortCode)
onTextEdited: imagePack.setData(imagePack.index(currentImageIndex, 0), text, SingleImagePackModel.ShortCode)
}
MatrixText {
text: qsTr("Body")
}
MatrixTextField {
id: bodyField
Layout.fillWidth: true
Layout.columnSpan: 2
label: qsTr("Body")
text: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.Body)
onTextEdited: imagePack.setData(imagePack.index(currentImageIndex, 0), text, SingleImagePackModel.Body)
}
MatrixText {
Layout.margins: bodyField.textPadding
font.weight: Font.DemiBold
font.letterSpacing: font.pixelSize * 0.02
text: qsTr("Use as Emoji")
}
@ -284,6 +284,9 @@ ApplicationWindow {
}
MatrixText {
Layout.margins: bodyField.textPadding
font.weight: Font.DemiBold
font.letterSpacing: font.pixelSize * 0.02
text: qsTr("Use as Sticker")
}
@ -294,6 +297,9 @@ ApplicationWindow {
}
MatrixText {
Layout.margins: bodyField.textPadding
font.weight: Font.DemiBold
font.letterSpacing: font.pixelSize * 0.02
text: qsTr("Remove from pack")
}

View File

@ -28,7 +28,6 @@ ApplicationWindow {
color: Nheko.colors.base
modality: Qt.NonModal
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
Component.onCompleted: Nheko.reparent(win)
Component {
id: packEditor

View File

@ -18,7 +18,6 @@ ApplicationWindow {
modality: Qt.NonModal
flags: Qt.Dialog
Component.onCompleted: Nheko.reparent(inputDialog)
width: 350
height: fontMetrics.lineSpacing * 7

View File

@ -37,7 +37,6 @@ ApplicationWindow {
palette: Nheko.colors
color: Nheko.colors.window
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
Component.onCompleted: Nheko.reparent(inviteDialogRoot)
Shortcut {
sequence: "Ctrl+Enter"

View File

@ -17,7 +17,6 @@ ApplicationWindow {
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
palette: Nheko.colors
color: Nheko.colors.window
Component.onCompleted: Nheko.reparent(joinRoomRoot)
width: 350
height: fontMetrics.lineSpacing * 7

View File

@ -19,7 +19,6 @@ ApplicationWindow {
modality: Qt.NonModal
flags: Qt.Dialog
Component.onCompleted: Nheko.reparent(inputDialog)
width: 350
height: fontMetrics.lineSpacing * 7

View File

@ -17,7 +17,6 @@ ApplicationWindow {
palette: Nheko.colors
color: Nheko.colors.window
flags: Qt.Tool | Qt.WindowStaysOnTopHint | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
Component.onCompleted: Nheko.reparent(rawMessageRoot)
Shortcut {
sequence: StandardKey.Cancel

View File

@ -22,7 +22,6 @@ ApplicationWindow {
palette: Nheko.colors
color: Nheko.colors.window
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
Component.onCompleted: Nheko.reparent(readReceiptsRoot)
Shortcut {
sequence: StandardKey.Cancel

View File

@ -22,7 +22,6 @@ ApplicationWindow {
color: Nheko.colors.window
modality: Qt.WindowModal
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
Component.onCompleted: Nheko.reparent(roomDirectoryWindow)
title: qsTr("Explore Public Rooms")
Shortcut {
@ -189,7 +188,6 @@ ApplicationWindow {
Layout.fillWidth: true
selectByMouse: true
font.pixelSize: fontMetrics.font.pixelSize
padding: Nheko.paddingMedium
color: Nheko.colors.text
placeholderText: qsTr("Search for public rooms")
onTextChanged: searchTimer.restart()
@ -200,7 +198,6 @@ ApplicationWindow {
Layout.minimumWidth: 0.3 * header.width
Layout.maximumWidth: 0.3 * header.width
padding: Nheko.paddingMedium
color: Nheko.colors.text
placeholderText: qsTr("Choose custom homeserver")
onTextChanged: publicRooms.setMatrixServer(text)

View File

@ -24,7 +24,6 @@ ApplicationWindow {
palette: Nheko.colors
color: Nheko.colors.window
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
Component.onCompleted: Nheko.reparent(roomMembersRoot)
Shortcut {
sequence: StandardKey.Cancel

View File

@ -23,7 +23,6 @@ ApplicationWindow {
color: Nheko.colors.window
modality: Qt.NonModal
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
Component.onCompleted: Nheko.reparent(roomSettingsDialog)
title: qsTr("Room Settings")
Shortcut {

View File

@ -13,9 +13,6 @@ import QtQuick.Window 2.13
import im.nheko 1.0
ApplicationWindow {
// this does not work in ApplicationWindow, just in Window
//transientParent: Nheko.mainwindow()
id: userProfileDialog
property var profile
@ -29,7 +26,6 @@ ApplicationWindow {
title: profile.isGlobalUserProfile ? qsTr("Global User Profile") : qsTr("Room User Profile")
modality: Qt.NonModal
flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
Component.onCompleted: Nheko.reparent(userProfileDialog)
Shortcut {
sequence: StandardKey.Cancel

View File

@ -0,0 +1,182 @@
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.2
import QtQuick.Window 2.15
import im.nheko 1.0
import "../components/"
import "../ui/"
import "../"
Item {
id: loginPage
property int maxExpansion: 400
property string error: login.error
Login {
id: login
}
ScrollView {
id: scroll
clip: false
palette: Nheko.colors
ScrollBar.horizontal.visible: false
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
height: Math.min(loginPage.height, col.implicitHeight)
anchors.margins: Nheko.paddingLarge
contentWidth: availableWidth
ColumnLayout {
id: col
spacing: Nheko.paddingMedium
anchors.horizontalCenter: parent.horizontalCenter
width: Math.min(loginPage.maxExpansion, scroll.width- Nheko.paddingLarge*2)
Image {
Layout.alignment: Qt.AlignHCenter
source: "qrc:/logos/login.png"
height: 128
width: 128
}
RowLayout {
spacing: Nheko.paddingLarge
Layout.fillWidth: true
MatrixTextField {
id: matrixIdLabel
label: qsTr("Matrix ID")
placeholderText: qsTr("e.g @joe:matrix.org")
onEditingFinished: login.mxid = text
ToolTip.text: qsTr("Your login name. A mxid should start with @ followed by the user id. After the user id you need to include your server name after a :.\nYou can also put your homeserver address there, if your server doesn't support .well-known lookup.\nExample: @user:server.my\nIf Nheko fails to discover your homeserver, it will show you a field to enter the server manually.")
Keys.forwardTo: [pwBtn, ssoBtn]
}
Spinner {
height: matrixIdLabel.height/2
Layout.alignment: Qt.AlignBottom
visible: running
running: login.lookingUpHs
foreground: Nheko.colors.mid
}
}
MatrixText {
textFormat: Text.PlainText
color: Nheko.theme.error
text: login.mxidError
visible: text
}
MatrixTextField {
id: passwordLabel
Layout.fillWidth: true
label: qsTr("Password")
echoMode: TextInput.Password
ToolTip.text: qsTr("Your password.")
visible: login.passwordSupported
Keys.forwardTo: [pwBtn, ssoBtn]
}
MatrixTextField {
id: deviceNameLabel
Layout.fillWidth: true
label: qsTr("Device name")
placeholderText: login.initialDeviceName()
ToolTip.text: qsTr("A name for this device, which will be shown to others, when verifying your devices. If none is provided a default is used.")
Keys.forwardTo: [pwBtn, ssoBtn]
}
MatrixTextField {
id: hsLabel
enabled: visible
visible: login.homeserverNeeded
Layout.fillWidth: true
label: qsTr("Homeserver address")
placeholderText: qsTr("server.my:8787")
text: login.homeserver
onEditingFinished: login.homeserver = text
ToolTip.text: qsTr("The address that can be used to contact you homeservers client API.\nExample: https://server.my:8787")
Keys.forwardTo: [pwBtn, ssoBtn]
}
Item {
height: Nheko.avatarSize
Layout.fillWidth: true
Spinner {
height: parent.height
anchors.centerIn: parent
visible: running
running: login.loggingIn
foreground: Nheko.colors.mid
}
}
MatrixText {
textFormat: Text.PlainText
color: Nheko.theme.error
text: loginPage.error
visible: text
}
FlatButton {
id: pwBtn
visible: login.passwordSupported
enabled: login.homeserverValid && matrixIdLabel.text == login.mxid && login.homeserver == hsLabel.text
Layout.alignment: Qt.AlignHCenter
text: qsTr("LOGIN")
function pwLogin() {
login.onLoginButtonClicked(Login.Password, matrixIdLabel.text, passwordLabel.text, deviceNameLabel.text)
}
onClicked: pwBtn.pwLogin()
Keys.onEnterPressed: pwBtn.pwLogin()
Keys.onReturnPressed: pwBtn.pwLogin()
Keys.enabled: pwBtn.enabled && login.passwordSupported
}
FlatButton {
id: ssoBtn
visible: login.ssoSupported
enabled: login.homeserverValid && matrixIdLabel.text == login.mxid && login.homeserver == hsLabel.text
Layout.alignment: Qt.AlignHCenter
text: qsTr("SSO LOGIN")
function ssoLogin() {
login.onLoginButtonClicked(Login.SSO, matrixIdLabel.text, passwordLabel.text, deviceNameLabel.text)
}
onClicked: ssoBtn.ssoLogin()
Keys.onEnterPressed: ssoBtn.ssoLogin()
Keys.onReturnPressed: ssoBtn.ssoLogin()
Keys.enabled: ssoBtn.enabled && !login.passwordSupported
}
}
}
ImageButton {
anchors.top: parent.top
anchors.left: parent.left
anchors.margins: Nheko.paddingMedium
width: Nheko.avatarSize
height: Nheko.avatarSize
image: ":/icons/icons/ui/angle-arrow-left.svg"
ToolTip.visible: hovered
ToolTip.text: qsTr("Back")
onClicked: mainWindow.pop()
}
}

View File

@ -0,0 +1,215 @@
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.2
import QtQuick.Window 2.15
import im.nheko 1.0
import "../components/"
import "../ui/"
import "../"
Item {
id: registrationPage
property int maxExpansion: 400
property string error: regis.error
Registration {
id: regis
}
ScrollView {
id: scroll
clip: false
palette: Nheko.colors
ScrollBar.horizontal.visible: false
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
height: Math.min(registrationPage.height, col.implicitHeight)
anchors.margins: Nheko.paddingLarge
contentWidth: availableWidth
ColumnLayout {
id: col
spacing: Nheko.paddingMedium
anchors.horizontalCenter: parent.horizontalCenter
width: Math.min(registrationPage.maxExpansion, scroll.width- Nheko.paddingLarge*2)
Image {
Layout.alignment: Qt.AlignHCenter
source: "qrc:/logos/login.png"
height: 128
width: 128
}
RowLayout {
spacing: Nheko.paddingLarge
Layout.fillWidth: true
MatrixTextField {
id: hsLabel
label: qsTr("Homeserver")
placeholderText: qsTr("your.server")
onEditingFinished: regis.setServer(text)
ToolTip.text: qsTr("A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own.")
}
Spinner {
height: hsLabel.height/2
Layout.alignment: Qt.AlignBottom
visible: running
running: regis.lookingUpHs
foreground: Nheko.colors.mid
}
}
MatrixText {
textFormat: Text.PlainText
color: Nheko.theme.error
text: regis.hsError
visible: text
}
RowLayout {
spacing: Nheko.paddingLarge
visible: regis.supported
Layout.fillWidth: true
MatrixTextField {
id: usernameLabel
Layout.fillWidth: true
label: qsTr("Username")
ToolTip.text: qsTr("The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /.")
onEditingFinished: regis.checkUsername(text)
}
Spinner {
height: usernameLabel.height/2
Layout.alignment: Qt.AlignBottom
visible: running
running: regis.lookingUpUsername
foreground: Nheko.colors.mid
}
Image {
width: usernameLabel.height/2
height: width
Layout.preferredHeight: usernameLabel.height/2
Layout.preferredWidth: usernameLabel.height/2
Layout.alignment: Qt.AlignBottom
source: regis.usernameAvailable ? ("image://colorimage/:/icons/icons/ui/checkmark.svg?green") : ("image://colorimage/:/icons/icons/ui/dismiss.svg?"+Nheko.theme.error)
visible: regis.usernameAvailable || regis.usernameUnavailable
ToolTip.visible: ma.hovered
ToolTip.text: qsTr("Back")
sourceSize.height: height * Screen.devicePixelRatio
sourceSize.width: width * Screen.devicePixelRatio
HoverHandler {
id: ma
}
}
}
MatrixText {
textFormat: Text.PlainText
color: Nheko.theme.error
text: regis.usernameError
visible: text
}
MatrixTextField {
visible: regis.supported
id: passwordLabel
Layout.fillWidth: true
label: qsTr("Password")
echoMode: TextInput.Password
ToolTip.text: qsTr("Please choose a secure password. The exact requirements for password strength may depend on your server.")
}
MatrixTextField {
visible: regis.supported
id: passwordConfirmationLabel
Layout.fillWidth: true
label: qsTr("Password confirmation")
echoMode: TextInput.Password
}
MatrixText {
visible: regis.supported
textFormat: Text.PlainText
color: Nheko.theme.error
text: passwordLabel.text != passwordConfirmationLabel.text ? qsTr("Your passwords do not match!") : ""
}
MatrixTextField {
visible: regis.supported
id: deviceNameLabel
Layout.fillWidth: true
label: qsTr("Device name")
placeholderText: regis.initialDeviceName()
ToolTip.text: qsTr("A name for this device, which will be shown to others, when verifying your devices. If none is provided a default is used.")
}
Item {
height: Nheko.avatarSize
Layout.fillWidth: true
Spinner {
height: parent.height
anchors.centerIn: parent
visible: running
running: regis.registering
foreground: Nheko.colors.mid
}
}
MatrixText {
textFormat: Text.PlainText
color: Nheko.theme.error
text: registrationPage.error
visible: text
}
FlatButton {
id: regisBtn
visible: regis.supported
enabled: usernameLabel.text && passwordLabel.text && passwordLabel.text == passwordConfirmationLabel.text
Layout.alignment: Qt.AlignHCenter
text: qsTr("REGISTER")
function register() {
regis.startRegistration(usernameLabel.text, passwordLabel.text, deviceNameLabel.text)
}
onClicked: regisBtn.register()
Keys.onEnterPressed: regisBtn.register()
Keys.onReturnPressed: regisBtn.register()
Keys.enabled: regisBtn.enabled && regis.supported
}
}
}
ImageButton {
anchors.top: parent.top
anchors.left: parent.left
anchors.margins: Nheko.paddingMedium
width: Nheko.avatarSize
height: Nheko.avatarSize
image: ":/icons/icons/ui/angle-arrow-left.svg"
ToolTip.visible: hovered
ToolTip.text: qsTr("Back")
onClicked: mainWindow.pop()
}
}

View File

@ -0,0 +1,73 @@
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.2
import QtQuick.Window 2.15
import im.nheko 1.0
import "../components/"
ColumnLayout {
Item {
Layout.fillHeight: true
}
Image {
Layout.alignment: Qt.AlignHCenter
source: "qrc:/logos/splash.png"
height: 256
width: 256
}
Label {
Layout.margins: Nheko.paddingLarge
Layout.bottomMargin: 0
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
text: qsTr("Welcome to nheko! The desktop client for the Matrix protocol.")
color: Nheko.colors.text
font.pointSize: fontMetrics.font.pointSize*2
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
}
Label {
Layout.margins: Nheko.paddingLarge
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
text: qsTr("Enjoy your stay!")
color: Nheko.colors.text
font.pointSize: fontMetrics.font.pointSize*1.5
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
}
RowLayout {
Item {
Layout.fillWidth: true
}
FlatButton {
Layout.margins: Nheko.paddingLarge
Layout.alignment: Qt.AlignHCenter
text: qsTr("REGISTER")
onClicked: {
mainWindow.push(registerPage);
}
}
FlatButton {
Layout.margins: Nheko.paddingLarge
Layout.alignment: Qt.AlignHCenter
text: qsTr("LOGIN")
onClicked: {
mainWindow.push(loginPage);
}
}
Item {
Layout.fillWidth: true
}
}
Item {
Layout.fillHeight: true
}
}

View File

@ -0,0 +1,98 @@
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15
import im.nheko 1.0
Popup {
id: snackbar
property var messages: []
property string currentMessage: ""
function showNotification(msg) {
messages.push(msg);
currentMessage = messages[0];
if (!visible) {
open();
dismissTimer.start();
}
}
Timer {
id: dismissTimer
interval: 10000
onTriggered: snackbar.close()
}
onAboutToHide: {
messages.shift();
}
onClosed: {
if (messages.length > 0) {
currentMessage = messages[0];
open();
dismissTimer.restart();
}
}
parent: Overlay.overlay
opacity: 0
y: -100
x: (parent.width - width)/2
padding: Nheko.paddingLarge
contentItem: Label {
color: Nheko.colors.light
width: Math.max(Overlay.overlay? Overlay.overlay.width/2 : 0, 400)
text: snackbar.currentMessage
font.bold: true
}
background: Rectangle {
radius: Nheko.paddingLarge
color: Nheko.colors.dark
opacity: 0.8
}
enter: Transition {
NumberAnimation {
target: snackbar
property: "opacity"
from: 0.0
to: 1.0
duration: 200
easing.type: Easing.OutCubic
}
NumberAnimation {
target: snackbar
properties: "y"
from: -100
to: 100
duration: 1000
easing.type: Easing.OutCubic
}
}
exit: Transition {
NumberAnimation {
target: snackbar
property: "opacity"
from: 1.0
to: 0.0
duration: 300
easing.type: Easing.InCubic
}
NumberAnimation {
target: snackbar
properties: "y"
to: -100
from: 100
duration: 300
easing.type: Easing.InCubic
}
}
}

View File

@ -110,6 +110,9 @@
<file>qml/TypingIndicator.qml</file>
<file>qml/NotificationWarning.qml</file>
<file>qml/pages/UserSettingsPage.qml</file>
<file>qml/pages/WelcomePage.qml</file>
<file>qml/pages/LoginPage.qml</file>
<file>qml/pages/RegisterPage.qml</file>
<file>qml/components/AdaptiveLayout.qml</file>
<file>qml/components/AdaptiveLayoutElement.qml</file>
<file>qml/components/AvatarListTile.qml</file>
@ -154,6 +157,7 @@
<file>qml/ui/NhekoSlider.qml</file>
<file>qml/ui/Ripple.qml</file>
<file>qml/ui/Spinner.qml</file>
<file>qml/ui/Snackbar.qml</file>
<file>qml/ui/animations/BlinkAnimation.qml</file>
<file>qml/ui/media/MediaControls.qml</file>
<file>qml/voip/ActiveCallBar.qml</file>

View File

@ -325,7 +325,7 @@ static void
fatalSecretError()
{
QMessageBox::critical(
ChatPage::instance(),
nullptr,
QCoreApplication::translate("SecretStorage", "Failed to connect to secret storage"),
QCoreApplication::translate(
"SecretStorage",
@ -391,6 +391,7 @@ Cache::loadSecrets(std::vector<std::pair<std::string, bool>> toLoad)
&QKeychain::ReadPasswordJob::finished,
this,
[this, name, toLoad, job](QKeychain::Job *) mutable {
nhlog::db()->debug("Finished reading '{}'", toLoad.begin()->first);
const QString secret = job->textData();
if (job->error() && job->error() != QKeychain::Error::EntryNotFound) {
nhlog::db()->error("Restoring secret '{}' failed ({}): {}",
@ -413,6 +414,7 @@ Cache::loadSecrets(std::vector<std::pair<std::string, bool>> toLoad)
// You can't start a job from the finish signal of a job.
QTimer::singleShot(0, this, [this, toLoad] { loadSecrets(toLoad); });
});
nhlog::db()->debug("Reading '{}'", name_);
job->start();
}

View File

@ -22,7 +22,6 @@
#include "Utils.h"
#include "encryption/DeviceVerificationFlow.h"
#include "encryption/Olm.h"
#include "ui/OverlayModal.h"
#include "ui/Theme.h"
#include "ui/UserProfile.h"
#include "voip/CallManager.h"
@ -44,8 +43,8 @@ Q_DECLARE_METATYPE(mtx::presence::PresenceState)
Q_DECLARE_METATYPE(mtx::secret_storage::AesHmacSha2KeyDescription)
Q_DECLARE_METATYPE(SecretsToDecrypt)
ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
: QWidget(parent)
ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QObject *parent)
: QObject(parent)
, isConnected_(true)
, userSettings_{userSettings}
, notificationsManager(this)
@ -61,14 +60,8 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
qRegisterMetaType<mtx::secret_storage::AesHmacSha2KeyDescription>();
qRegisterMetaType<SecretsToDecrypt>();
topLayout_ = new QHBoxLayout(this);
topLayout_->setSpacing(0);
topLayout_->setContentsMargins(0, 0, 0, 0);
view_manager_ = new TimelineViewManager(callManager_, this);
topLayout_->addWidget(view_manager_->getWidget());
connect(this,
&ChatPage::downloadedSecrets,
this,
@ -154,7 +147,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
[this](const QString &roomid, const QString &eventid) {
Q_UNUSED(eventid)
view_manager_->rooms()->setCurrentRoom(roomid);
activateWindow();
MainWindow::instance()->requestActivate();
});
connect(&notificationsManager,
&NotificationsManager::sendNotificationReply,
@ -162,17 +155,9 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
[this](const QString &roomid, const QString &eventid, const QString &body) {
view_manager_->rooms()->setCurrentRoom(roomid);
view_manager_->queueReply(roomid, eventid, body);
activateWindow();
MainWindow::instance()->requestActivate();
});
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
// ensure the qml context is shutdown before we destroy all other singletons
// Otherwise Qml will try to access the room list or settings, after they have been
// destroyed
topLayout_->removeWidget(view_manager_->getWidget());
delete view_manager_->getWidget();
});
connect(
this,
&ChatPage::initializeViews,
@ -183,8 +168,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
&ChatPage::initializeEmptyViews,
view_manager_,
&TimelineViewManager::initializeRoomlist);
connect(
this, &ChatPage::chatFocusChanged, view_manager_, &TimelineViewManager::chatFocusChanged);
connect(this, &ChatPage::syncUI, this, [this](const mtx::responses::Sync &sync) {
view_manager_->sync(sync);
@ -201,7 +184,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
// TODO: Replace this once we have proper pushrules support. This is a horrible hack
if (prevNotificationCount < notificationCount) {
if (userSettings_->hasAlertOnNotification())
QApplication::alert(this);
MainWindow::instance()->alert(0);
}
prevNotificationCount = notificationCount;
@ -331,7 +314,7 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
} else if (cacheVersion == cache::CacheVersion::Older) {
if (!cache::runMigrations()) {
QMessageBox::critical(
this,
nullptr,
tr("Cache migration failed!"),
tr("Migrating the cache to the current version failed. "
"This can have different reasons. Please open an "
@ -344,7 +327,7 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
return;
} else if (cacheVersion == cache::CacheVersion::Newer) {
QMessageBox::critical(
this,
nullptr,
tr("Incompatible cache version"),
tr("The cache on your disk is newer than this version of Nheko "
"supports. Please update Nheko or clear your cache."));
@ -690,7 +673,7 @@ ChatPage::joinRoomVia(const std::string &room_id,
if (promptForConfirmation &&
QMessageBox::Yes !=
QMessageBox::question(
this,
nullptr,
tr("Confirm join"),
tr("Do you really want to join %1?").arg(QString::fromStdString(room_id))))
return;
@ -776,7 +759,7 @@ ChatPage::inviteUser(QString userid, QString reason)
{
auto room = currentRoom();
if (QMessageBox::question(this,
if (QMessageBox::question(nullptr,
tr("Confirm invite"),
tr("Do you really want to invite %1 (%2)?")
.arg(cache::displayName(room, userid), userid)) != QMessageBox::Yes)
@ -787,6 +770,8 @@ ChatPage::inviteUser(QString userid, QString reason)
userid.toStdString(),
[this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) {
if (err) {
nhlog::net()->error(
"Failed to invite {} to {}: {}", userid.toStdString(), room.toStdString(), *err);
emit showNotification(
tr("Failed to invite %1 to %2: %3")
.arg(userid, room, QString::fromStdString(err->matrix_error.error)));
@ -800,7 +785,7 @@ ChatPage::kickUser(QString userid, QString reason)
{
auto room = currentRoom();
if (QMessageBox::question(this,
if (QMessageBox::question(nullptr,
tr("Confirm kick"),
tr("Do you really want to kick %1 (%2)?")
.arg(cache::displayName(room, userid), userid)) != QMessageBox::Yes)
@ -825,7 +810,7 @@ ChatPage::banUser(QString userid, QString reason)
auto room = currentRoom();
if (QMessageBox::question(
this,
nullptr,
tr("Confirm ban"),
tr("Do you really want to ban %1 (%2)?").arg(cache::displayName(room, userid), userid)) !=
QMessageBox::Yes)
@ -849,7 +834,7 @@ ChatPage::unbanUser(QString userid, QString reason)
{
auto room = currentRoom();
if (QMessageBox::question(this,
if (QMessageBox::question(nullptr,
tr("Confirm unban"),
tr("Do you really want to unban %1 (%2)?")
.arg(cache::displayName(room, userid), userid)) != QMessageBox::Yes)
@ -1064,8 +1049,6 @@ ChatPage::initiateLogout()
emit loggedOut();
});
emit showOverlayProgressBar();
}
template<typename T>
@ -1083,7 +1066,7 @@ ChatPage::decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescriptio
const SecretsToDecrypt &secrets)
{
QString text = QInputDialog::getText(
ChatPage::instance(),
nullptr,
QCoreApplication::translate("CrossSigningSecrets", "Decrypt secrets"),
keyDesc.name.empty()
? QCoreApplication::translate(
@ -1115,7 +1098,7 @@ ChatPage::decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescriptio
if (!decryptionKey) {
QMessageBox::information(
ChatPage::instance(),
nullptr,
QCoreApplication::translate("CrossSigningSecrets", "Decryption failed"),
QCoreApplication::translate("CrossSigningSecrets",
"Failed to decrypt secrets with the "
@ -1209,7 +1192,7 @@ ChatPage::startChat(QString userid)
if (QMessageBox::Yes !=
QMessageBox::question(
this,
nullptr,
tr("Confirm invite"),
tr("Do you really want to start a private chat with %1?").arg(userid)))
return;
@ -1395,7 +1378,7 @@ ChatPage::handleMatrixUri(const QUrl &uri)
bool
ChatPage::isRoomActive(const QString &room_id)
{
return isActiveWindow() && currentRoom() == room_id;
return MainWindow::instance()->isActive() && currentRoom() == room_id;
}
QString

View File

@ -8,7 +8,6 @@
#include <atomic>
#include <optional>
#include <stack>
#include <variant>
#include <mtx/common.hpp>
@ -18,17 +17,15 @@
#include <mtx/events/presence.hpp>
#include <mtx/secret_storage.hpp>
#include <QHBoxLayout>
#include <QMap>
#include <QPoint>
#include <QSharedPointer>
#include <QTimer>
#include <QWidget>
#include "CacheCryptoStructs.h"
#include "CacheStructs.h"
#include "notifications/Manager.h"
class OverlayModal;
class TimelineViewManager;
class UserSettings;
class NotificationsManager;
@ -51,12 +48,12 @@ struct Rooms;
using SecretsToDecrypt = std::map<std::string, mtx::secret_storage::AesHmacSha2EncryptedData>;
class ChatPage : public QWidget
class ChatPage : public QObject
{
Q_OBJECT
public:
ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent = nullptr);
ChatPage(QSharedPointer<UserSettings> userSettings, QObject *parent = nullptr);
// Initialize all the components of the UI.
void bootstrap(QString userid, QString homeserver, QString token);
@ -112,7 +109,6 @@ signals:
void showNotification(const QString &msg);
void showLoginPage(const QString &msg);
void showUserSettingsPage();
void showOverlayProgressBar();
void ownProfileOk();
void setUserDisplayName(const QString &name);
@ -143,7 +139,6 @@ signals:
void retrievedPresence(const QString &statusMsg, mtx::presence::PresenceState state);
void themeChanged();
void decryptSidebarChanged();
void chatFocusChanged(const bool focused);
//! Signals for device verificaiton
void receivedDeviceVerificationAccept(const mtx::events::msg::KeyVerificationAccept &message);
@ -201,8 +196,6 @@ private:
template<typename T>
void connectCallMessage();
QHBoxLayout *topLayout_;
TimelineViewManager *view_manager_;
QTimer connectivityTimer_;

View File

@ -5,11 +5,6 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QDesktopServices>
#include <QFontMetrics>
#include <QLabel>
#include <QPainter>
#include <QStyleOption>
#include <QtMath>
#include <mtx/identifiers.hpp>
#include <mtx/requests.hpp>
@ -18,247 +13,94 @@
#include "Config.h"
#include "Logging.h"
#include "LoginPage.h"
#include "MainWindow.h"
#include "MatrixClient.h"
#include "SSOHandler.h"
#include "UserSettingsPage.h"
#include "ui/FlatButton.h"
#include "ui/LoadingIndicator.h"
#include "ui/OverlayModal.h"
#include "ui/RaisedButton.h"
#include "ui/TextField.h"
Q_DECLARE_METATYPE(LoginPage::LoginMethod)
using namespace mtx::identifiers;
LoginPage::LoginPage(QWidget *parent)
: QWidget(parent)
LoginPage::LoginPage(QObject *parent)
: QObject(parent)
, inferredServerAddress_()
{
qRegisterMetaType<LoginPage::LoginMethod>("LoginPage::LoginMethod");
top_layout_ = new QVBoxLayout();
top_bar_layout_ = new QHBoxLayout();
top_bar_layout_->setSpacing(0);
top_bar_layout_->setContentsMargins(0, 0, 0, 0);
back_button_ = new FlatButton(this);
back_button_->setMinimumSize(QSize(30, 30));
top_bar_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter);
top_bar_layout_->addStretch(1);
QIcon icon;
icon.addFile(QStringLiteral(":/icons/icons/ui/angle-arrow-left.svg"));
back_button_->setIcon(icon);
back_button_->setIconSize(QSize(32, 32));
QIcon logo;
logo.addFile(QStringLiteral(":/logos/login.png"));
logo_ = new QLabel(this);
logo_->setPixmap(logo.pixmap(128));
logo_layout_ = new QHBoxLayout();
logo_layout_->setContentsMargins(0, 0, 0, 20);
logo_layout_->addWidget(logo_, 0, Qt::AlignHCenter);
form_wrapper_ = new QHBoxLayout();
form_widget_ = new QWidget();
form_widget_->setMinimumSize(QSize(350, 200));
form_layout_ = new QVBoxLayout();
form_layout_->setSpacing(20);
form_layout_->setContentsMargins(0, 0, 0, 30);
form_widget_->setLayout(form_layout_);
form_wrapper_->addStretch(1);
form_wrapper_->addWidget(form_widget_);
form_wrapper_->addStretch(1);
matrixid_input_ = new TextField(this);
matrixid_input_->setLabel(tr("Matrix ID"));
matrixid_input_->setRegexp(QRegularExpression(QStringLiteral("@.+?:.{3,}")));
matrixid_input_->setPlaceholderText(tr("e.g @joe:matrix.org"));
matrixid_input_->setToolTip(
tr("Your login name. A mxid should start with @ followed by the user id. After the user "
"id you need to include your server name after a :.\nYou can also put your homeserver "
"address there, if your server doesn't support .well-known lookup.\nExample: "
"@user:server.my\nIf Nheko fails to discover your homeserver, it will show you a "
"field to enter the server manually."));
spinner_ = new LoadingIndicator(this);
spinner_->setFixedHeight(40);
spinner_->setFixedWidth(40);
spinner_->hide();
errorIcon_ = new QLabel(this);
errorIcon_->setPixmap(QPixmap(QStringLiteral(":/icons/icons/error.png")));
errorIcon_->hide();
matrixidLayout_ = new QHBoxLayout();
matrixidLayout_->addWidget(matrixid_input_, 0, Qt::AlignVCenter);
QFont font;
error_matrixid_label_ = new QLabel(this);
error_matrixid_label_->setFont(font);
error_matrixid_label_->setWordWrap(true);
password_input_ = new TextField(this);
password_input_->setLabel(tr("Password"));
password_input_->setEchoMode(QLineEdit::Password);
password_input_->setToolTip(tr("Your password."));
deviceName_ = new TextField(this);
deviceName_->setLabel(tr("Device name"));
deviceName_->setToolTip(
tr("A name for this device, which will be shown to others, when verifying your devices. "
"If none is provided a default is used."));
serverInput_ = new TextField(this);
serverInput_->setLabel(tr("Homeserver address"));
serverInput_->setPlaceholderText(tr("server.my:8787"));
serverInput_->setToolTip(tr("The address that can be used to contact you homeservers "
"client API.\nExample: https://server.my:8787"));
serverInput_->hide();
serverLayout_ = new QHBoxLayout();
serverLayout_->addWidget(serverInput_, 0, Qt::AlignVCenter);
form_layout_->addLayout(matrixidLayout_);
form_layout_->addWidget(error_matrixid_label_, 0, Qt::AlignHCenter);
form_layout_->addWidget(password_input_);
form_layout_->addWidget(deviceName_, Qt::AlignHCenter);
form_layout_->addLayout(serverLayout_);
error_matrixid_label_->hide();
button_layout_ = new QHBoxLayout();
button_layout_->setSpacing(20);
button_layout_->setContentsMargins(0, 0, 0, 30);
login_button_ = new RaisedButton(tr("LOGIN"), this);
login_button_->setMinimumSize(150, 65);
login_button_->setFontSize(20);
login_button_->setCornerRadius(3);
sso_login_button_ = new RaisedButton(tr("SSO LOGIN"), this);
sso_login_button_->setMinimumSize(150, 65);
sso_login_button_->setFontSize(20);
sso_login_button_->setCornerRadius(3);
sso_login_button_->setVisible(false);
button_layout_->addStretch(1);
button_layout_->addWidget(login_button_);
button_layout_->addWidget(sso_login_button_);
button_layout_->addStretch(1);
error_label_ = new QLabel(this);
error_label_->setFont(font);
error_label_->setWordWrap(true);
top_layout_->addLayout(top_bar_layout_);
top_layout_->addStretch(1);
top_layout_->addLayout(logo_layout_);
top_layout_->addLayout(form_wrapper_);
top_layout_->addStretch(1);
top_layout_->addLayout(button_layout_);
top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter);
top_layout_->addStretch(1);
setLayout(top_layout_);
[[maybe_unused]] static auto ignored =
qRegisterMetaType<LoginPage::LoginMethod>("LoginPage::LoginMethod");
connect(this, &LoginPage::versionOkCb, this, &LoginPage::versionOk, Qt::QueuedConnection);
connect(this, &LoginPage::versionErrorCb, this, &LoginPage::versionError, Qt::QueuedConnection);
connect(
this,
&LoginPage::loginOk,
this,
[this](const mtx::responses::Login &res) {
loggingIn_ = false;
emit loggingInChanged();
connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked()));
connect(login_button_, &RaisedButton::clicked, this, [this]() {
onLoginButtonClicked(passwordSupported ? LoginMethod::Password : LoginMethod::SSO);
});
connect(sso_login_button_, &RaisedButton::clicked, this, [this]() {
onLoginButtonClicked(LoginMethod::SSO);
});
connect(this,
&LoginPage::showErrorMessage,
this,
static_cast<void (LoginPage::*)(QLabel *, const QString &)>(&LoginPage::showError),
Qt::QueuedConnection);
connect(matrixid_input_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
connect(password_input_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
connect(deviceName_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
connect(serverInput_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
connect(matrixid_input_, SIGNAL(editingFinished()), this, SLOT(onMatrixIdEntered()));
connect(serverInput_, SIGNAL(editingFinished()), this, SLOT(onServerAddressEntered()));
http::client()->set_user(res.user_id);
MainWindow::instance()->showChatPage();
},
Qt::QueuedConnection);
}
void
LoginPage::showError(const QString &msg)
{
auto rect = QFontMetrics(font()).boundingRect(msg);
int width = rect.width();
int height = rect.height();
error_label_->setFixedHeight((int)qCeil(width / 200.0) * height);
error_label_->setText(msg);
loggingIn_ = false;
emit loggingInChanged();
error_ = msg;
emit errorOccurred();
}
void
LoginPage::showError(QLabel *label, const QString &msg)
LoginPage::setHomeserver(QString hs)
{
auto rect = QFontMetrics(font()).boundingRect(msg);
int width = rect.width();
int height = rect.height();
label->setFixedHeight((int)qCeil(width / 200.0) * height);
label->setText(msg);
if (hs != homeserver_) {
homeserver_ = hs;
homeserverValid_ = false;
emit homeserverChanged();
http::client()->set_server(hs.toStdString());
checkHomeserverVersion();
}
}
void
LoginPage::onMatrixIdEntered()
{
error_label_->setText(QLatin1String(""));
clearErrors();
homeserverValid_ = false;
emit homeserverChanged();
User user;
try {
user = parse<User>(mxid_.toStdString());
} catch (const std::exception &) {
mxidError_ = tr("You have entered an invalid Matrix ID e.g @joe:matrix.org");
emit mxidErrorChanged();
return;
}
if (!matrixid_input_->isValid()) {
error_matrixid_label_->show();
showError(error_matrixid_label_,
tr("You have entered an invalid Matrix ID e.g @joe:matrix.org"));
if (user.hostname().empty() || user.localpart().empty()) {
mxidError_ = tr("You have entered an invalid Matrix ID e.g @joe:matrix.org");
emit mxidErrorChanged();
return;
} else {
error_matrixid_label_->setText(QLatin1String(""));
error_matrixid_label_->hide();
nhlog::net()->debug("hostname: {}", user.hostname());
}
try {
user = parse<User>(matrixid_input_->text().toStdString());
} catch (const std::exception &) {
showError(error_matrixid_label_,
tr("You have entered an invalid Matrix ID e.g @joe:matrix.org"));
return;
}
QString homeServer = QString::fromStdString(user.hostname());
if (homeServer != inferredServerAddress_) {
serverInput_->hide();
serverLayout_->removeWidget(errorIcon_);
errorIcon_->hide();
if (serverInput_->isVisible()) {
matrixidLayout_->removeWidget(spinner_);
serverLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight);
spinner_->start();
} else {
serverLayout_->removeWidget(spinner_);
matrixidLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight);
spinner_->start();
}
inferredServerAddress_ = homeServer;
serverInput_->setText(homeServer);
if (user.hostname() != inferredServerAddress_.toStdString()) {
homeserverNeeded_ = false;
lookingUpHs_ = true;
emit lookingUpHsChanged();
http::client()->set_server(user.hostname());
http::client()->verify_certificates(
!UserSettings::instance()->disableCertificateValidation());
homeserver_ = QString::fromStdString(user.hostname());
emit homeserverChanged();
http::client()->well_known(
[this](const mtx::responses::WellKnown &res, mtx::http::RequestErr err) {
@ -286,6 +128,7 @@ LoginPage::onMatrixIdEntered()
nhlog::net()->info("Autodiscovery: Discovered '" + res.homeserver.base_url + "'");
http::client()->set_server(res.homeserver.base_url);
emit homeserverChanged();
checkHomeserverVersion();
});
}
@ -294,6 +137,16 @@ LoginPage::onMatrixIdEntered()
void
LoginPage::checkHomeserverVersion()
{
clearErrors();
try {
User user = parse<User>(mxid_.toStdString());
} catch (const std::exception &) {
mxidError_ = tr("You have entered an invalid Matrix ID e.g @joe:matrix.org");
emit mxidErrorChanged();
return;
}
http::client()->versions([this](const mtx::responses::Versions &, mtx::http::RequestErr err) {
if (err) {
if (err->status_code == 404) {
@ -318,107 +171,78 @@ LoginPage::checkHomeserverVersion()
if (err || flows.flows.empty())
emit versionOkCb(true, false);
bool ssoSupported_ = false;
bool passwordSupported_ = false;
bool ssoSupported = false;
bool passwordSupported = false;
for (const auto &flow : flows.flows) {
if (flow.type == mtx::user_interactive::auth_types::sso) {
ssoSupported_ = true;
ssoSupported = true;
} else if (flow.type == mtx::user_interactive::auth_types::password) {
passwordSupported_ = true;
passwordSupported = true;
}
}
emit versionOkCb(passwordSupported_, ssoSupported_);
emit versionOkCb(passwordSupported, ssoSupported);
});
});
}
void
LoginPage::onServerAddressEntered()
{
error_label_->setText(QLatin1String(""));
http::client()->verify_certificates(!UserSettings::instance()->disableCertificateValidation());
http::client()->set_server(serverInput_->text().toStdString());
checkHomeserverVersion();
serverLayout_->removeWidget(errorIcon_);
errorIcon_->hide();
serverLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight);
spinner_->start();
}
void
LoginPage::versionError(const QString &error)
{
showError(error_label_, error);
serverInput_->show();
showError(error);
spinner_->stop();
serverLayout_->removeWidget(spinner_);
serverLayout_->addWidget(errorIcon_, 0, Qt::AlignVCenter | Qt::AlignRight);
errorIcon_->show();
matrixidLayout_->removeWidget(spinner_);
homeserverNeeded_ = true;
lookingUpHs_ = false;
homeserverValid_ = false;
emit lookingUpHsChanged();
emit versionLookedUp();
}
void
LoginPage::versionOk(bool passwordSupported_, bool ssoSupported_)
LoginPage::versionOk(bool passwordSupported, bool ssoSupported)
{
passwordSupported = passwordSupported_;
ssoSupported = ssoSupported_;
passwordSupported_ = passwordSupported;
ssoSupported_ = ssoSupported;
serverLayout_->removeWidget(spinner_);
matrixidLayout_->removeWidget(spinner_);
spinner_->stop();
password_input_->setVisible(passwordSupported);
password_input_->setEnabled(passwordSupported);
sso_login_button_->setVisible(ssoSupported);
login_button_->setVisible(passwordSupported);
if (serverInput_->isVisible())
serverInput_->hide();
lookingUpHs_ = false;
homeserverValid_ = true;
emit homeserverChanged();
emit lookingUpHsChanged();
emit versionLookedUp();
}
void
LoginPage::onLoginButtonClicked(LoginMethod loginMethod)
LoginPage::onLoginButtonClicked(LoginMethod loginMethod,
QString userid,
QString password,
QString deviceName)
{
error_label_->setText(QLatin1String(""));
clearErrors();
User user;
if (!matrixid_input_->isValid()) {
error_matrixid_label_->show();
showError(error_matrixid_label_,
tr("You have entered an invalid Matrix ID e.g @joe:matrix.org"));
return;
} else {
error_matrixid_label_->setText(QLatin1String(""));
error_matrixid_label_->hide();
}
try {
user = parse<User>(matrixid_input_->text().toStdString());
user = parse<User>(userid.toStdString());
} catch (const std::exception &) {
showError(error_matrixid_label_,
tr("You have entered an invalid Matrix ID e.g @joe:matrix.org"));
mxidError_ = tr("You have entered an invalid Matrix ID e.g @joe:matrix.org");
emit mxidErrorChanged();
return;
}
if (loginMethod == LoginMethod::Password) {
if (password_input_->text().isEmpty())
return showError(error_label_, tr("Empty password"));
if (password.isEmpty())
return showError(tr("Empty password"));
http::client()->login(
user.localpart(),
password_input_->text().toStdString(),
deviceName_->text().trimmed().isEmpty() ? initialDeviceName()
: deviceName_->text().toStdString(),
password.toStdString(),
deviceName.trimmed().isEmpty() ? initialDeviceName_() : deviceName.toStdString(),
[this](const mtx::responses::Login &res, mtx::http::RequestErr err) {
if (err) {
auto error = err->matrix_error.error;
if (error.empty())
error = err->parse_error;
showErrorMessage(error_label_, QString::fromStdString(error));
emit errorOccurred();
showError(QString::fromStdString(error));
return;
}
@ -432,34 +256,33 @@ LoginPage::onLoginButtonClicked(LoginMethod loginMethod)
});
} else {
auto sso = new SSOHandler();
connect(sso, &SSOHandler::ssoSuccess, this, [this, sso](std::string token) {
mtx::requests::Login req{};
req.token = token;
req.type = mtx::user_interactive::auth_types::token;
req.device_id = deviceName_->text().trimmed().isEmpty()
? initialDeviceName()
: deviceName_->text().toStdString();
http::client()->login(
req, [this](const mtx::responses::Login &res, mtx::http::RequestErr err) {
if (err) {
showErrorMessage(error_label_,
QString::fromStdString(err->matrix_error.error));
emit errorOccurred();
return;
}
connect(
sso, &SSOHandler::ssoSuccess, this, [this, sso, userid, deviceName](std::string token) {
mtx::requests::Login req{};
req.token = token;
req.type = mtx::user_interactive::auth_types::token;
req.device_id =
deviceName.trimmed().isEmpty() ? initialDeviceName_() : deviceName.toStdString();
http::client()->login(
req, [this](const mtx::responses::Login &res, mtx::http::RequestErr err) {
if (err) {
showError(QString::fromStdString(err->matrix_error.error));
emit errorOccurred();
return;
}
if (res.well_known) {
http::client()->set_server(res.well_known->homeserver.base_url);
nhlog::net()->info("Login requested to user server: " +
res.well_known->homeserver.base_url);
}
if (res.well_known) {
http::client()->set_server(res.well_known->homeserver.base_url);
nhlog::net()->info("Login requested to user server: " +
res.well_known->homeserver.base_url);
}
emit loginOk(res);
});
sso->deleteLater();
});
emit loginOk(res);
});
sso->deleteLater();
});
connect(sso, &SSOHandler::ssoFailed, this, [this, sso]() {
showErrorMessage(error_label_, tr("SSO login failed"));
showError(tr("SSO login failed"));
emit errorOccurred();
sso->deleteLater();
});
@ -468,37 +291,6 @@ LoginPage::onLoginButtonClicked(LoginMethod loginMethod)
QString::fromStdString(http::client()->login_sso_redirect(sso->url())));
}
emit loggingIn();
}
void
LoginPage::reset()
{
matrixid_input_->clear();
password_input_->clear();
password_input_->show();
serverInput_->clear();
spinner_->stop();
errorIcon_->hide();
serverLayout_->removeWidget(spinner_);
serverLayout_->removeWidget(errorIcon_);
matrixidLayout_->removeWidget(spinner_);
inferredServerAddress_.clear();
}
void
LoginPage::onBackButtonClicked()
{
emit backButtonClicked();
}
void
LoginPage::paintEvent(QPaintEvent *)
{
QStyleOption opt;
opt.initFrom(this);
QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
loggingIn_ = true;
emit loggingInChanged();
}

View File

@ -6,16 +6,7 @@
#pragma once
#include <QWidget>
class FlatButton;
class LoadingIndicator;
class OverlayModal;
class RaisedButton;
class TextField;
class QLabel;
class QVBoxLayout;
class QHBoxLayout;
#include <QObject>
namespace mtx {
namespace responses {
@ -23,62 +14,61 @@ struct Login;
}
}
class LoginPage : public QWidget
class LoginPage : public QObject
{
Q_OBJECT
Q_PROPERTY(QString mxid READ mxid WRITE setMxid NOTIFY matrixIdChanged)
Q_PROPERTY(QString homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged)
Q_PROPERTY(QString mxidError READ mxidError NOTIFY mxidErrorChanged)
Q_PROPERTY(QString error READ error NOTIFY errorOccurred)
Q_PROPERTY(bool lookingUpHs READ lookingUpHs NOTIFY lookingUpHsChanged)
Q_PROPERTY(bool homeserverValid READ homeserverValid NOTIFY lookingUpHsChanged)
Q_PROPERTY(bool loggingIn READ loggingIn NOTIFY loggingInChanged)
Q_PROPERTY(bool passwordSupported READ passwordSupported NOTIFY versionLookedUp)
Q_PROPERTY(bool ssoSupported READ ssoSupported NOTIFY versionLookedUp)
Q_PROPERTY(bool homeserverNeeded READ homeserverNeeded NOTIFY versionLookedUp)
public:
enum class LoginMethod
{
Password,
SSO,
};
Q_ENUM(LoginMethod)
LoginPage(QWidget *parent = nullptr);
LoginPage(QObject *parent = nullptr);
void reset();
Q_INVOKABLE QString initialDeviceName() const
{
return QString::fromStdString(initialDeviceName_());
}
signals:
void backButtonClicked();
void loggingIn();
void errorOccurred();
bool lookingUpHs() const { return lookingUpHs_; }
bool loggingIn() const { return loggingIn_; }
bool passwordSupported() const { return passwordSupported_; }
bool ssoSupported() const { return ssoSupported_; }
bool homeserverNeeded() const { return homeserverNeeded_; }
bool homeserverValid() const { return homeserverValid_; }
//! Used to trigger the corresponding slot outside of the main thread.
void versionErrorCb(const QString &err);
void versionOkCb(bool passwordSupported, bool ssoSupported);
QString homeserver() { return homeserver_; }
QString mxid() { return mxid_; }
void loginOk(const mtx::responses::Login &res);
void showErrorMessage(QLabel *label, const QString &msg);
QString error() { return error_; }
QString mxidError() { return mxidError_; }
protected:
void paintEvent(QPaintEvent *event) override;
void setHomeserver(QString hs);
void setMxid(QString id)
{
if (id != mxid_) {
mxid_ = id;
emit matrixIdChanged();
onMatrixIdEntered();
}
}
public slots:
// Displays errors produced during the login.
void showError(const QString &msg);
void showError(QLabel *label, const QString &msg);
private slots:
// Callback for the back button.
void onBackButtonClicked();
// Callback for the login button.
void onLoginButtonClicked(LoginMethod loginMethod);
// Callback for probing the server found in the mxid
void onMatrixIdEntered();
// Callback for probing the manually entered server
void onServerAddressEntered();
// Callback for errors produced during server probing
void versionError(const QString &error_message);
// Callback for successful server probing
void versionOk(bool passwordSupported, bool ssoSupported);
private:
void checkHomeserverVersion();
std::string initialDeviceName()
static std::string initialDeviceName_()
{
#if defined(Q_OS_MAC)
return "Nheko on macOS";
@ -93,33 +83,65 @@ private:
#endif
}
QVBoxLayout *top_layout_;
signals:
void loggingInChanged();
void errorOccurred();
QHBoxLayout *top_bar_layout_;
QHBoxLayout *logo_layout_;
QHBoxLayout *button_layout_;
//! Used to trigger the corresponding slot outside of the main thread.
void versionErrorCb(const QString &err);
void versionOkCb(bool passwordSupported, bool ssoSupported);
QLabel *logo_;
QLabel *error_label_;
QLabel *error_matrixid_label_;
void loginOk(const mtx::responses::Login &res);
void onServerAddressEntered();
void matrixIdChanged();
void homeserverChanged();
void mxidErrorChanged();
void lookingUpHsChanged();
void versionLookedUp();
void versionLookupFinished();
public slots:
// Displays errors produced during the login.
void showError(const QString &msg);
// Callback for the login button.
void onLoginButtonClicked(LoginMethod loginMethod,
QString userid,
QString password,
QString deviceName);
// Callback for errors produced during server probing
void versionError(const QString &error_message);
// Callback for successful server probing
void versionOk(bool passwordSupported, bool ssoSupported);
private:
void checkHomeserverVersion();
void onMatrixIdEntered();
void clearErrors()
{
error_.clear();
mxidError_.clear();
emit errorOccurred();
emit mxidErrorChanged();
}
QHBoxLayout *serverLayout_;
QHBoxLayout *matrixidLayout_;
LoadingIndicator *spinner_;
QLabel *errorIcon_;
QString inferredServerAddress_;
FlatButton *back_button_;
RaisedButton *login_button_, *sso_login_button_;
QString mxid_;
QString homeserver_;
QWidget *form_widget_;
QHBoxLayout *form_wrapper_;
QVBoxLayout *form_layout_;
QString mxidError_;
QString error_;
TextField *matrixid_input_;
TextField *password_input_;
TextField *deviceName_;
TextField *serverInput_;
bool passwordSupported = true;
bool ssoSupported = false;
bool passwordSupported_ = true;
bool ssoSupported_ = false;
bool lookingUpHs_ = false;
bool loggingIn_ = false;
bool homeserverNeeded_ = false;
bool homeserverValid_ = false;
};

View File

@ -5,91 +5,87 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QApplication>
#include <QLayout>
#include <QMessageBox>
#include <QPluginLoader>
#include <QShortcut>
#include <mtx/requests.hpp>
#include <mtx/responses/login.hpp>
#include "BlurhashProvider.h"
#include "Cache.h"
#include "Cache_p.h"
#include "ChatPage.h"
#include "Clipboard.h"
#include "ColorImageProvider.h"
#include "CombinedImagePackModel.h"
#include "CompletionProxyModel.h"
#include "Config.h"
#include "EventAccessors.h"
#include "ImagePackListModel.h"
#include "InviteesModel.h"
#include "JdenticonProvider.h"
#include "Logging.h"
#include "LoginPage.h"
#include "MainWindow.h"
#include "MatrixClient.h"
#include "MemberList.h"
#include "MxcImageProvider.h"
#include "ReadReceiptsModel.h"
#include "RegisterPage.h"
#include "RoomDirectoryModel.h"
#include "RoomsModel.h"
#include "SingleImagePackModel.h"
#include "TrayIcon.h"
#include "UserSettingsPage.h"
#include "UsersModel.h"
#include "Utils.h"
#include "WelcomePage.h"
#include "ui/LoadingIndicator.h"
#include "ui/OverlayModal.h"
#include "ui/SnackBar.h"
#include "emoji/EmojiModel.h"
#include "emoji/Provider.h"
#include "encryption/DeviceVerificationFlow.h"
#include "encryption/SelfVerificationStatus.h"
#include "timeline/DelegateChooser.h"
#include "timeline/TimelineViewManager.h"
#include "ui/MxcAnimatedImage.h"
#include "ui/MxcMediaProxy.h"
#include "ui/NhekoCursorShape.h"
#include "ui/NhekoDropArea.h"
#include "ui/NhekoGlobalObject.h"
#include "ui/UIA.h"
#include "voip/WebRTCSession.h"
#include "dialogs/CreateRoom.h"
Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents)
Q_DECLARE_METATYPE(std::vector<DeviceInfo>)
Q_DECLARE_METATYPE(std::vector<mtx::responses::PublicRoomsChunk>)
MainWindow *MainWindow::instance_ = nullptr;
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
MainWindow::MainWindow(QWindow *parent)
: QQuickView(parent)
, userSettings_{UserSettings::instance()}
{
instance_ = this;
QMainWindow::setWindowTitle(0);
MainWindow::setWindowTitle(0);
setObjectName(QStringLiteral("MainWindow"));
modal_ = new OverlayModal(this);
setResizeMode(QQuickView::SizeRootObjectToView);
setMinimumHeight(400);
setMinimumWidth(400);
restoreWindowSize();
QFont font;
font.setStyleStrategy(QFont::PreferAntialias);
setFont(font);
chat_page_ = new ChatPage(userSettings_, this);
registerQmlTypes();
setColor(Theme::paletteFromTheme(userSettings_->theme()).window().color());
setSource(QUrl(QStringLiteral("qrc:///qml/Root.qml")));
trayIcon_ = new TrayIcon(QStringLiteral(":/logos/nheko.svg"), this);
welcome_page_ = new WelcomePage(this);
login_page_ = new LoginPage(this);
register_page_ = new RegisterPage(this);
chat_page_ = new ChatPage(userSettings_, this);
// Initialize sliding widget manager.
pageStack_ = new QStackedWidget(this);
pageStack_->addWidget(welcome_page_);
pageStack_->addWidget(login_page_);
pageStack_->addWidget(register_page_);
pageStack_->addWidget(chat_page_);
setCentralWidget(pageStack_);
connect(welcome_page_, SIGNAL(userLogin()), this, SLOT(showLoginPage()));
connect(welcome_page_, SIGNAL(userRegister()), this, SLOT(showRegisterPage()));
connect(login_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage()));
connect(login_page_, &LoginPage::loggingIn, this, &MainWindow::showOverlayProgressBar);
connect(register_page_, &RegisterPage::registering, this, &MainWindow::showOverlayProgressBar);
connect(login_page_, &LoginPage::errorOccurred, this, [this]() { removeOverlayProgressBar(); });
connect(
register_page_, &RegisterPage::errorOccurred, this, [this]() { removeOverlayProgressBar(); });
connect(register_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage()));
connect(chat_page_, &ChatPage::closing, this, &MainWindow::showWelcomePage);
connect(
chat_page_, &ChatPage::showOverlayProgressBar, this, &MainWindow::showOverlayProgressBar);
connect(chat_page_, &ChatPage::closing, this, [this] { switchToLoginPage(""); });
connect(chat_page_, &ChatPage::unreadMessages, this, &MainWindow::setWindowTitle);
connect(chat_page_, SIGNAL(unreadMessages(int)), trayIcon_, SLOT(setUnreadCount(int)));
connect(chat_page_, &ChatPage::showLoginPage, this, [this](const QString &msg) {
login_page_->showError(msg);
showLoginPage();
});
connect(chat_page_, &ChatPage::showLoginPage, this, &MainWindow::switchToLoginPage);
connect(chat_page_, &ChatPage::showNotification, this, &MainWindow::showNotification);
connect(userSettings_.get(), &UserSettings::trayChanged, trayIcon_, &TrayIcon::setVisible);
connect(trayIcon_,
@ -97,20 +93,6 @@ MainWindow::MainWindow(QWidget *parent)
this,
SLOT(iconActivated(QSystemTrayIcon::ActivationReason)));
connect(chat_page_, SIGNAL(contentLoaded()), this, SLOT(removeOverlayProgressBar()));
connect(this, &MainWindow::focusChanged, chat_page_, &ChatPage::chatFocusChanged);
connect(login_page_, &LoginPage::loginOk, this, [this](const mtx::responses::Login &res) {
http::client()->set_user(res.user_id);
showChatPage();
});
connect(register_page_, &RegisterPage::registerOk, this, &MainWindow::showChatPage);
QShortcut *quitShortcut = new QShortcut(QKeySequence::Quit, this);
connect(quitShortcut, &QShortcut::activated, this, QApplication::quit);
trayIcon_->setVisible(userSettings_->tray());
// load cache on event loop
@ -133,11 +115,171 @@ MainWindow::MainWindow(QWidget *parent)
user_id.toStdString());
}
nhlog::ui()->info("User already signed in, showing chat page");
showChatPage();
}
});
}
void
MainWindow::registerQmlTypes()
{
qRegisterMetaType<mtx::events::msg::KeyVerificationAccept>();
qRegisterMetaType<mtx::events::msg::KeyVerificationCancel>();
qRegisterMetaType<mtx::events::msg::KeyVerificationDone>();
qRegisterMetaType<mtx::events::msg::KeyVerificationKey>();
qRegisterMetaType<mtx::events::msg::KeyVerificationMac>();
qRegisterMetaType<mtx::events::msg::KeyVerificationReady>();
qRegisterMetaType<mtx::events::msg::KeyVerificationRequest>();
qRegisterMetaType<mtx::events::msg::KeyVerificationStart>();
qRegisterMetaType<CombinedImagePackModel *>();
qRegisterMetaType<mtx::events::collections::TimelineEvents>();
qRegisterMetaType<std::vector<DeviceInfo>>();
qRegisterMetaType<std::vector<mtx::responses::PublicRoomsChunk>>();
qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject,
"im.nheko",
1,
0,
"MtxEvent",
QStringLiteral("Can't instantiate enum!"));
qmlRegisterUncreatableMetaObject(
olm::staticMetaObject, "im.nheko", 1, 0, "Olm", QStringLiteral("Can't instantiate enum!"));
qmlRegisterUncreatableMetaObject(crypto::staticMetaObject,
"im.nheko",
1,
0,
"Crypto",
QStringLiteral("Can't instantiate enum!"));
qmlRegisterUncreatableMetaObject(verification::staticMetaObject,
"im.nheko",
1,
0,
"VerificationStatus",
QStringLiteral("Can't instantiate enum!"));
qmlRegisterType<DelegateChoice>("im.nheko", 1, 0, "DelegateChoice");
qmlRegisterType<DelegateChooser>("im.nheko", 1, 0, "DelegateChooser");
qmlRegisterType<NhekoDropArea>("im.nheko", 1, 0, "NhekoDropArea");
qmlRegisterType<NhekoCursorShape>("im.nheko", 1, 0, "CursorShape");
qmlRegisterType<MxcAnimatedImage>("im.nheko", 1, 0, "MxcAnimatedImage");
qmlRegisterType<MxcMediaProxy>("im.nheko", 1, 0, "MxcMedia");
qmlRegisterType<RoomDirectoryModel>("im.nheko", 1, 0, "RoomDirectoryModel");
qmlRegisterType<LoginPage>("im.nheko", 1, 0, "Login");
qmlRegisterType<RegisterPage>("im.nheko", 1, 0, "Registration");
qmlRegisterUncreatableType<DeviceVerificationFlow>(
"im.nheko",
1,
0,
"DeviceVerificationFlow",
QStringLiteral("Can't create verification flow from QML!"));
qmlRegisterUncreatableType<UserProfile>(
"im.nheko",
1,
0,
"UserProfileModel",
QStringLiteral("UserProfile needs to be instantiated on the C++ side"));
qmlRegisterUncreatableType<MemberList>(
"im.nheko",
1,
0,
"MemberList",
QStringLiteral("MemberList needs to be instantiated on the C++ side"));
qmlRegisterUncreatableType<RoomSettings>(
"im.nheko",
1,
0,
"RoomSettingsModel",
QStringLiteral("Room Settings needs to be instantiated on the C++ side"));
qmlRegisterUncreatableType<TimelineModel>(
"im.nheko", 1, 0, "Room", QStringLiteral("Room needs to be instantiated on the C++ side"));
qmlRegisterUncreatableType<ImagePackListModel>(
"im.nheko",
1,
0,
"ImagePackListModel",
QStringLiteral("ImagePackListModel needs to be instantiated on the C++ side"));
qmlRegisterUncreatableType<SingleImagePackModel>(
"im.nheko",
1,
0,
"SingleImagePackModel",
QStringLiteral("SingleImagePackModel needs to be instantiated on the C++ side"));
qmlRegisterUncreatableType<InviteesModel>(
"im.nheko",
1,
0,
"InviteesModel",
QStringLiteral("InviteesModel needs to be instantiated on the C++ side"));
qmlRegisterUncreatableType<ReadReceiptsProxy>(
"im.nheko",
1,
0,
"ReadReceiptsProxy",
QStringLiteral("ReadReceiptsProxy needs to be instantiated on the C++ side"));
qmlRegisterSingletonType<Clipboard>(
"im.nheko", 1, 0, "Clipboard", [](QQmlEngine *, QJSEngine *) -> QObject * {
return new Clipboard();
});
qmlRegisterSingletonType<Nheko>(
"im.nheko", 1, 0, "Nheko", [](QQmlEngine *, QJSEngine *) -> QObject * {
return new Nheko();
});
qmlRegisterSingletonType<UserSettingsModel>(
"im.nheko", 1, 0, "UserSettingsModel", [](QQmlEngine *, QJSEngine *) -> QObject * {
return new UserSettingsModel();
});
qmlRegisterSingletonInstance("im.nheko", 1, 0, "Settings", userSettings_.data());
qRegisterMetaType<mtx::events::collections::TimelineEvents>();
qRegisterMetaType<std::vector<DeviceInfo>>();
qmlRegisterUncreatableType<FilteredCommunitiesModel>(
"im.nheko",
1,
0,
"FilteredCommunitiesModel",
QStringLiteral("Use Communities.filtered() to create a FilteredCommunitiesModel"));
qmlRegisterType<emoji::EmojiModel>("im.nheko.EmojiModel", 1, 0, "EmojiModel");
qmlRegisterUncreatableType<emoji::Emoji>(
"im.nheko.EmojiModel", 1, 0, "Emoji", QStringLiteral("Used by emoji models"));
qmlRegisterUncreatableMetaObject(emoji::staticMetaObject,
"im.nheko.EmojiModel",
1,
0,
"EmojiCategory",
QStringLiteral("Error: Only enums"));
qmlRegisterType<RoomDirectoryModel>("im.nheko", 1, 0, "RoomDirectoryModel");
qmlRegisterSingletonType<SelfVerificationStatus>(
"im.nheko", 1, 0, "SelfVerificationStatus", [](QQmlEngine *, QJSEngine *) -> QObject * {
auto ptr = new SelfVerificationStatus();
QObject::connect(ChatPage::instance(),
&ChatPage::initializeEmptyViews,
ptr,
&SelfVerificationStatus::invalidate);
return ptr;
});
qmlRegisterSingletonInstance("im.nheko", 1, 0, "MainWindow", this);
qmlRegisterSingletonInstance("im.nheko", 1, 0, "UIA", UIA::instance());
qmlRegisterSingletonInstance(
"im.nheko", 1, 0, "CallManager", ChatPage::instance()->callManager());
imgProvider = new MxcImageProvider();
engine()->addImageProvider(QStringLiteral("MxcImage"), imgProvider);
engine()->addImageProvider(QStringLiteral("colorimage"), new ColorImageProvider());
engine()->addImageProvider(QStringLiteral("blurhash"), new BlurhashProvider());
if (JdenticonProvider::isAvailable())
engine()->addImageProvider(QStringLiteral("jdenticon"), new JdenticonProvider());
QObject::connect(engine(), &QQmlEngine::quit, &QGuiApplication::quit);
}
void
MainWindow::setWindowTitle(int notificationCount)
{
@ -148,20 +290,19 @@ MainWindow::setWindowTitle(int notificationCount)
if (notificationCount > 0) {
name.append(QString{QStringLiteral(" (%1)")}.arg(notificationCount));
}
QMainWindow::setWindowTitle(name);
QQuickView::setTitle(name);
}
bool
MainWindow::event(QEvent *event)
{
auto type = event->type();
if (type == QEvent::WindowActivate) {
emit focusChanged(true);
} else if (type == QEvent::WindowDeactivate) {
emit focusChanged(false);
if (type == QEvent::Close) {
closeEvent(static_cast<QCloseEvent *>(event));
}
return QMainWindow::event(event);
return QQuickView::event(event);
}
void
@ -188,31 +329,6 @@ MainWindow::saveCurrentWindowSize()
settings->setValue(QStringLiteral("window/height"), current.height());
}
void
MainWindow::removeOverlayProgressBar()
{
QTimer *timer = new QTimer(this);
timer->setSingleShot(true);
connect(timer, &QTimer::timeout, this, [this, timer]() {
timer->deleteLater();
if (modal_)
modal_->hide();
if (spinner_)
spinner_->stop();
});
// FIXME: Snackbar doesn't work if it's initialized in the constructor.
QTimer::singleShot(0, this, [this]() {
snackBar_ = new SnackBar(this);
connect(chat_page_, &ChatPage::showNotification, snackBar_, &SnackBar::showMessage);
});
timer->start(50);
}
void
MainWindow::showChatPage()
{
@ -227,19 +343,13 @@ MainWindow::showChatPage()
userSettings_.data()->setDeviceId(device_id);
userSettings_.data()->setHomeserver(homeserver);
showOverlayProgressBar();
pageStack_->setCurrentWidget(chat_page_);
pageStack_->removeWidget(welcome_page_);
pageStack_->removeWidget(login_page_);
pageStack_->removeWidget(register_page_);
login_page_->reset();
chat_page_->bootstrap(userid, homeserver, token);
connect(cache::client(), &Cache::databaseReady, this, &MainWindow::secretsChanged);
connect(cache::client(), &Cache::secretChanged, this, &MainWindow::secretsChanged);
emit reload();
nhlog::ui()->info("Switching to chat page");
emit switchToChatPage();
}
void
@ -247,7 +357,7 @@ MainWindow::closeEvent(QCloseEvent *event)
{
if (WebRTCSession::instance().state() != webrtc::State::DISCONNECTED) {
if (QMessageBox::question(
this, QStringLiteral("nheko"), QStringLiteral("A call is in progress. Quit?")) !=
nullptr, QStringLiteral("nheko"), QStringLiteral("A call is in progress. Quit?")) !=
QMessageBox::Yes) {
event->ignore();
return;
@ -289,23 +399,11 @@ MainWindow::hasActiveUser()
settings->contains(prefix + "auth/user_id");
}
void
MainWindow::showOverlayProgressBar()
{
spinner_ = new LoadingIndicator(this);
spinner_->setFixedHeight(100);
spinner_->setFixedWidth(100);
spinner_->setObjectName(QStringLiteral("ChatPageLoadSpinner"));
spinner_->start();
showSolidOverlayModal(spinner_);
}
void
MainWindow::openCreateRoomDialog(
std::function<void(const mtx::requests::CreateRoom &request)> callback)
{
auto dialog = new dialogs::CreateRoom(this);
auto dialog = new dialogs::CreateRoom(nullptr);
connect(dialog,
&dialogs::CreateRoom::createRoom,
this,
@ -314,76 +412,19 @@ MainWindow::openCreateRoomDialog(
showDialog(dialog);
}
void
MainWindow::showTransparentOverlayModal(QWidget *content, QFlags<Qt::AlignmentFlag> flags)
{
modal_->setWidget(content);
modal_->setColor(QColor(30, 30, 30, 150));
modal_->setDismissible(true);
modal_->setContentAlignment(flags);
modal_->raise();
modal_->show();
}
void
MainWindow::showSolidOverlayModal(QWidget *content, QFlags<Qt::AlignmentFlag> flags)
{
modal_->setWidget(content);
modal_->setColor(QColor(30, 30, 30));
modal_->setDismissible(false);
modal_->setContentAlignment(flags);
modal_->raise();
modal_->show();
}
bool
MainWindow::hasActiveDialogs() const
{
return modal_ && modal_->isVisible();
}
bool
MainWindow::pageSupportsTray() const
{
return !welcome_page_->isVisible() && !login_page_->isVisible() && !register_page_->isVisible();
}
void
MainWindow::hideOverlay()
{
if (modal_)
modal_->hide();
return !http::client()->access_token().empty();
}
inline void
MainWindow::showDialog(QWidget *dialog)
{
utils::centerWidget(dialog, this);
dialog->setWindowFlags(Qt::WindowType::Dialog | Qt::WindowType::WindowCloseButtonHint |
Qt::WindowType::WindowTitleHint);
dialog->raise();
dialog->show();
}
void
MainWindow::showWelcomePage()
{
removeOverlayProgressBar();
pageStack_->addWidget(welcome_page_);
pageStack_->setCurrentWidget(welcome_page_);
}
void
MainWindow::showLoginPage()
{
if (modal_)
modal_->hide();
pageStack_->addWidget(login_page_);
pageStack_->setCurrentWidget(login_page_);
}
void
MainWindow::showRegisterPage()
{
pageStack_->addWidget(register_page_);
pageStack_->setCurrentWidget(register_page_);
utils::centerWidget(dialog, this);
dialog->window()->windowHandle()->setTransientParent(this);
}

View File

@ -8,26 +8,21 @@
#include <functional>
#include <QMainWindow>
#include <QQuickView>
#include <QSharedPointer>
#include <QStackedWidget>
#include <QSystemTrayIcon>
#include "UserSettingsPage.h"
#include "ui/OverlayModal.h"
#include "jdenticoninterface.h"
class ChatPage;
class RegisterPage;
class LoginPage;
class WelcomePage;
class LoadingIndicator;
class OverlayModal;
class SnackBar;
class TrayIcon;
class UserSettings;
class MxcImageProvider;
namespace mtx {
namespace requests {
@ -42,17 +37,12 @@ class MemberList;
class ReCaptcha;
}
class MainWindow : public QMainWindow
class MainWindow : public QQuickView
{
Q_OBJECT
Q_PROPERTY(int x READ x CONSTANT)
Q_PROPERTY(int y READ y CONSTANT)
Q_PROPERTY(int width READ width CONSTANT)
Q_PROPERTY(int height READ height CONSTANT)
public:
explicit MainWindow(QWidget *parent = nullptr);
explicit MainWindow(QWindow *parent = nullptr);
static MainWindow *instance() { return instance_; }
void saveCurrentWindowSize();
@ -61,69 +51,51 @@ public:
openCreateRoomDialog(std::function<void(const mtx::requests::CreateRoom &request)> callback);
void openJoinRoomDialog(std::function<void(const QString &room_id)> callback);
void hideOverlay();
void showSolidOverlayModal(QWidget *content, QFlags<Qt::AlignmentFlag> flags = Qt::AlignCenter);
void
showTransparentOverlayModal(QWidget *content,
QFlags<Qt::AlignmentFlag> flags = Qt::AlignTop | Qt::AlignHCenter);
MxcImageProvider *imageProvider() { return imgProvider; }
//! Show the chat page and start communicating with the given access token.
void showChatPage();
protected:
void closeEvent(QCloseEvent *event) override;
void closeEvent(QCloseEvent *event);
bool event(QEvent *event) override;
private slots:
//! Handle interaction with the tray icon.
void iconActivated(QSystemTrayIcon::ActivationReason reason);
//! Show the welcome page in the main window.
void showWelcomePage();
//! Show the login page in the main window.
void showLoginPage();
//! Show the register page in the main window.
void showRegisterPage();
//! Show the chat page and start communicating with the given access token.
void showChatPage();
void showOverlayProgressBar();
void removeOverlayProgressBar();
virtual void setWindowTitle(int notificationCount);
signals:
void focusChanged(const bool focused);
void reload();
void secretsChanged();
void showNotification(QString msg);
void switchToChatPage();
void switchToWelcomePage();
void switchToLoginPage(QString error);
private:
void showDialog(QWidget *dialog);
bool hasActiveUser();
void restoreWindowSize();
//! Check if there is an open dialog.
bool hasActiveDialogs() const;
//! Check if the current page supports the "minimize to tray" functionality.
bool pageSupportsTray() const;
void registerQmlTypes();
static MainWindow *instance_;
//! The initial welcome screen.
WelcomePage *welcome_page_;
//! The login screen.
LoginPage *login_page_;
//! The register page.
RegisterPage *register_page_;
//! A stacked widget that handles the transitions between widgets.
QStackedWidget *pageStack_;
//! The main chat area.
ChatPage *chat_page_;
QSharedPointer<UserSettings> userSettings_;
//! Tray icon that shows the unread message count.
TrayIcon *trayIcon_;
//! Notifications display.
SnackBar *snackBar_ = nullptr;
//! Overlay modal used to project other widgets.
OverlayModal *modal_ = nullptr;
LoadingIndicator *spinner_ = nullptr;
MxcImageProvider *imgProvider = nullptr;
};

View File

@ -4,312 +4,83 @@
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QInputDialog>
#include <QLabel>
#include <QMetaType>
#include <QPainter>
#include <QStyleOption>
#include <QTimer>
#include <QtMath>
#include <mtx/responses/common.hpp>
#include <mtx/responses/register.hpp>
#include <mtx/responses/well-known.hpp>
#include <mtxclient/http/client.hpp>
#include "Config.h"
#include "Logging.h"
#include "LoginPage.h"
#include "MainWindow.h"
#include "MatrixClient.h"
#include "RegisterPage.h"
#include "ui/FlatButton.h"
#include "ui/RaisedButton.h"
#include "ui/TextField.h"
#include "ui/UIA.h"
#include "dialogs/FallbackAuth.h"
#include "dialogs/ReCaptcha.h"
Q_DECLARE_METATYPE(mtx::user_interactive::Unauthorized)
Q_DECLARE_METATYPE(mtx::user_interactive::Auth)
RegisterPage::RegisterPage(QWidget *parent)
: QWidget(parent)
RegisterPage::RegisterPage(QObject *parent)
: QObject(parent)
{
qRegisterMetaType<mtx::user_interactive::Unauthorized>();
qRegisterMetaType<mtx::user_interactive::Auth>();
top_layout_ = new QVBoxLayout();
back_layout_ = new QHBoxLayout();
back_layout_->setSpacing(0);
back_layout_->setContentsMargins(5, 5, -1, -1);
back_button_ = new FlatButton(this);
back_button_->setMinimumSize(QSize(30, 30));
QIcon icon;
icon.addFile(QStringLiteral(":/icons/icons/ui/angle-arrow-left.svg"));
back_button_->setIcon(icon);
back_button_->setIconSize(QSize(32, 32));
back_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter);
back_layout_->addStretch(1);
QIcon logo;
logo.addFile(QStringLiteral(":/logos/register.png"));
logo_ = new QLabel(this);
logo_->setPixmap(logo.pixmap(128));
logo_layout_ = new QHBoxLayout();
logo_layout_->setContentsMargins(0, 0, 0, 0);
logo_layout_->addWidget(logo_, 0, Qt::AlignHCenter);
form_wrapper_ = new QHBoxLayout();
form_widget_ = new QWidget();
form_widget_->setMinimumSize(QSize(350, 300));
form_layout_ = new QVBoxLayout();
form_layout_->setSpacing(20);
form_layout_->setContentsMargins(0, 0, 0, 40);
form_widget_->setLayout(form_layout_);
form_wrapper_->addStretch(1);
form_wrapper_->addWidget(form_widget_);
form_wrapper_->addStretch(1);
username_input_ = new TextField();
username_input_->setLabel(tr("Username"));
username_input_->setRegexp(QRegularExpression(QStringLiteral("[a-z0-9._=/-]+")));
username_input_->setToolTip(tr("The username must not be empty, and must contain only the "
"characters a-z, 0-9, ., _, =, -, and /."));
password_input_ = new TextField();
password_input_->setLabel(tr("Password"));
password_input_->setRegexp(QRegularExpression(QStringLiteral("^.{8,}$")));
password_input_->setEchoMode(QLineEdit::Password);
password_input_->setToolTip(tr("Please choose a secure password. The exact requirements "
"for password strength may depend on your server."));
password_confirmation_ = new TextField();
password_confirmation_->setLabel(tr("Password confirmation"));
password_confirmation_->setEchoMode(QLineEdit::Password);
server_input_ = new TextField();
server_input_->setLabel(tr("Homeserver"));
server_input_->setRegexp(QRegularExpression(QStringLiteral(".+")));
server_input_->setToolTip(
tr("A server that allows registration. Since matrix is decentralized, you need to first "
"find a server you can register on or host your own."));
error_username_label_ = new QLabel(this);
error_username_label_->setWordWrap(true);
error_username_label_->hide();
error_password_label_ = new QLabel(this);
error_password_label_->setWordWrap(true);
error_password_label_->hide();
error_password_confirmation_label_ = new QLabel(this);
error_password_confirmation_label_->setWordWrap(true);
error_password_confirmation_label_->hide();
error_server_label_ = new QLabel(this);
error_server_label_->setWordWrap(true);
error_server_label_->hide();
form_layout_->addWidget(username_input_, Qt::AlignHCenter);
form_layout_->addWidget(error_username_label_, Qt::AlignHCenter);
form_layout_->addWidget(password_input_, Qt::AlignHCenter);
form_layout_->addWidget(error_password_label_, Qt::AlignHCenter);
form_layout_->addWidget(password_confirmation_, Qt::AlignHCenter);
form_layout_->addWidget(error_password_confirmation_label_, Qt::AlignHCenter);
form_layout_->addWidget(server_input_, Qt::AlignHCenter);
form_layout_->addWidget(error_server_label_, Qt::AlignHCenter);
button_layout_ = new QHBoxLayout();
button_layout_->setSpacing(0);
button_layout_->setContentsMargins(0, 0, 0, 0);
error_label_ = new QLabel(this);
error_label_->setWordWrap(true);
register_button_ = new RaisedButton(tr("REGISTER"), this);
register_button_->setMinimumSize(350, 65);
register_button_->setFontSize(conf::btn::fontSize);
register_button_->setCornerRadius(conf::btn::cornerRadius);
button_layout_->addStretch(1);
button_layout_->addWidget(register_button_);
button_layout_->addStretch(1);
top_layout_->addLayout(back_layout_);
top_layout_->addLayout(logo_layout_);
top_layout_->addLayout(form_wrapper_);
top_layout_->addStretch(1);
top_layout_->addLayout(button_layout_);
top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter);
top_layout_->addStretch(1);
setLayout(top_layout_);
connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked()));
connect(register_button_, SIGNAL(clicked()), this, SLOT(onRegisterButtonClicked()));
connect(username_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
connect(username_input_, &TextField::editingFinished, this, &RegisterPage::checkUsername);
connect(password_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
connect(password_input_, &TextField::editingFinished, this, &RegisterPage::checkPassword);
connect(password_confirmation_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
connect(password_confirmation_,
&TextField::editingFinished,
this,
&RegisterPage::checkPasswordConfirmation);
connect(server_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
connect(server_input_, &TextField::editingFinished, this, &RegisterPage::checkServer);
connect(
this,
&RegisterPage::serverError,
this,
[this](const QString &msg) {
server_input_->setValid(false);
showError(error_server_label_, msg);
},
Qt::QueuedConnection);
connect(this, &RegisterPage::wellKnownLookup, this, &RegisterPage::doWellKnownLookup);
connect(this, &RegisterPage::versionsCheck, this, &RegisterPage::doVersionsCheck);
connect(this, &RegisterPage::registration, this, &RegisterPage::doRegistration);
connect(this, &RegisterPage::registerOk, this, [] { MainWindow::instance()->showChatPage(); });
}
void
RegisterPage::onBackButtonClicked()
RegisterPage::setError(QString err)
{
emit backButtonClicked();
registrationError_ = err;
emit errorChanged();
registering_ = false;
emit registeringChanged();
}
void
RegisterPage::setHsError(QString err)
{
hsError_ = err;
emit hsErrorChanged();
lookingUpHs_ = false;
emit lookingUpHsChanged();
}
QString
RegisterPage::initialDeviceName() const
{
return QString::fromStdString(LoginPage::initialDeviceName_());
}
void
RegisterPage::showError(const QString &msg)
RegisterPage::setServer(QString server)
{
emit errorOccurred();
auto rect = QFontMetrics(font()).boundingRect(msg);
int width = rect.width();
int height = rect.height();
error_label_->setFixedHeight(qCeil(width / 200.0) * height);
error_label_->setText(msg);
}
if (server == lastServer)
return;
void
RegisterPage::showError(QLabel *label, const QString &msg)
{
emit errorOccurred();
auto rect = QFontMetrics(font()).boundingRect(msg);
int width = rect.width();
int height = rect.height();
label->setFixedHeight((int)qCeil(width / 200.0) * height);
label->setText(msg);
label->show();
}
lastServer = server;
bool
RegisterPage::checkOneField(QLabel *label, const TextField *t_field, const QString &msg)
{
if (t_field->isValid()) {
label->hide();
return true;
} else {
showError(label, msg);
return false;
}
}
http::client()->set_server(server.toStdString());
http::client()->verify_certificates(!UserSettings::instance()->disableCertificateValidation());
bool
RegisterPage::checkUsername()
{
return checkOneField(error_username_label_,
username_input_,
tr("The username must not be empty, and must contain only the "
"characters a-z, 0-9, ., _, =, -, and /."));
}
hsError_.clear();
emit hsErrorChanged();
supported_ = false;
lookingUpHs_ = true;
emit lookingUpHsChanged();
bool
RegisterPage::checkPassword()
{
return checkOneField(
error_password_label_, password_input_, tr("Password is not long enough (min 8 chars)"));
}
bool
RegisterPage::checkPasswordConfirmation()
{
if (password_input_->text() == password_confirmation_->text()) {
error_password_confirmation_label_->hide();
password_confirmation_->setValid(true);
return true;
} else {
showError(error_password_confirmation_label_, tr("Passwords don't match"));
password_confirmation_->setValid(false);
return false;
}
}
bool
RegisterPage::checkServer()
{
// This doesn't check that the server is reachable,
// just that the input is not obviously wrong.
return checkOneField(error_server_label_, server_input_, tr("Invalid server name"));
}
void
RegisterPage::onRegisterButtonClicked()
{
if (checkUsername() && checkPassword() && checkPasswordConfirmation() && checkServer()) {
auto server = server_input_->text().toStdString();
http::client()->set_server(server);
http::client()->verify_certificates(
!UserSettings::instance()->disableCertificateValidation());
// This starts a chain of `emit`s which ends up doing the
// registration. Signals are used rather than normal function
// calls so that the dialogs used in UIA work correctly.
//
// The sequence of events looks something like this:
//
// doKnownLookup
// v
// doVersionsCheck
// v
// doRegistration -> loops the UIAHandler until complete
emit wellKnownLookup();
emit registering();
}
}
void
RegisterPage::doWellKnownLookup()
{
http::client()->well_known(
[this](const mtx::responses::WellKnown &res, mtx::http::RequestErr err) {
if (err) {
if (err->status_code == 404) {
nhlog::net()->info("Autodiscovery: No .well-known.");
// Check that the homeserver can be reached
emit versionsCheck();
versionsCheck();
return;
}
if (!err->parse_error.empty()) {
emit serverError(tr("Autodiscovery failed. Received malformed response."));
setHsError(tr("Autodiscovery failed. Received malformed response."));
nhlog::net()->error("Autodiscovery failed. Received malformed response.");
emit hsErrorChanged();
return;
}
emit serverError(tr("Autodiscovery failed. Unknown error when "
"requesting .well-known."));
setHsError(tr("Autodiscovery failed. Unknown error when requesting .well-known."));
nhlog::net()->error("Autodiscovery failed. Unknown error when "
"requesting .well-known. {} {}",
err->status_code,
@ -319,98 +90,140 @@ RegisterPage::doWellKnownLookup()
nhlog::net()->info("Autodiscovery: Discovered '" + res.homeserver.base_url + "'");
http::client()->set_server(res.homeserver.base_url);
emit hsErrorChanged();
// Check that the homeserver can be reached
emit versionsCheck();
versionsCheck();
});
}
void
RegisterPage::doVersionsCheck()
RegisterPage::versionsCheck()
{
// Make a request to /_matrix/client/versions to check the address
// given is a Matrix homeserver.
http::client()->versions([this](const mtx::responses::Versions &, mtx::http::RequestErr err) {
if (err) {
if (err->status_code == 404) {
emit serverError(tr("The required endpoints were not found. Possibly "
"not a Matrix server."));
setHsError(
tr("The required endpoints were not found. Possibly not a Matrix server."));
emit hsErrorChanged();
return;
}
if (!err->parse_error.empty()) {
emit serverError(tr("Received malformed response. Make sure the homeserver "
"domain is valid."));
setHsError(
tr("Received malformed response. Make sure the homeserver domain is valid."));
emit hsErrorChanged();
return;
}
emit serverError(tr("An unknown error occured. Make sure the "
"homeserver domain is valid."));
setHsError(tr("An unknown error occured. Make sure the homeserver domain is valid."));
emit hsErrorChanged();
return;
}
// Attempt registration without an `auth` dict
emit registration();
http::client()->registration(
[this](const mtx::responses::Register &, mtx::http::RequestErr e) {
nhlog::net()->debug("Registration check: {}", e);
if (!e) {
setHsError(tr("Server does not support querying registration flows!"));
emit hsErrorChanged();
return;
}
if (e->status_code != 401) {
setHsError(tr("Server does not support registration."));
emit hsErrorChanged();
return;
}
supported_ = true;
lookingUpHs_ = false;
emit lookingUpHsChanged();
});
});
}
void
RegisterPage::doRegistration()
RegisterPage::checkUsername(QString name)
{
// These inputs should still be alright, but check just in case
if (checkUsername() && checkPassword() && checkPasswordConfirmation()) {
auto username = username_input_->text().toStdString();
auto password = password_input_->text().toStdString();
connect(UIA::instance(), &UIA::error, this, [this](QString msg) {
showError(msg);
disconnect(UIA::instance(), &UIA::error, this, nullptr);
});
http::client()->registration(
username,
password,
::UIA::instance()->genericHandler(QStringLiteral("Registration")),
registrationCb());
}
}
usernameAvailable_ = usernameUnavailable_ = false;
usernameError_.clear();
lookingUpUsername_ = true;
emit lookingUpUsernameChanged();
mtx::http::Callback<mtx::responses::Register>
RegisterPage::registrationCb()
{
// Return a function to be used as the callback when an attempt at
// registration is made.
return [this](const mtx::responses::Register &res, mtx::http::RequestErr err) {
if (!err) {
http::client()->set_user(res.user_id);
http::client()->set_access_token(res.access_token);
emit registerOk();
disconnect(UIA::instance(), &UIA::error, this, nullptr);
return;
}
http::client()->register_username_available(
name.toStdString(),
[this](const mtx::responses::Available &available, mtx::http::RequestErr e) {
if (e) {
if (e->matrix_error.errcode == mtx::errors::ErrorCode::M_INVALID_USERNAME) {
usernameError_ = tr("Invalid username.");
} else if (e->matrix_error.errcode == mtx::errors::ErrorCode::M_USER_IN_USE) {
usernameError_ = tr("Name already in use.");
} else if (e->matrix_error.errcode == mtx::errors::ErrorCode::M_EXCLUSIVE) {
usernameError_ = tr("Part of the reserved namespace.");
} else {
}
// The server requires registration flows.
if (err->status_code == 401) {
if (err->matrix_error.unauthorized.flows.empty()) {
nhlog::net()->warn("failed to retrieve registration flows: "
"status_code({}), matrix_error({}) ",
static_cast<int>(err->status_code),
err->matrix_error.error);
showError(QString::fromStdString(err->matrix_error.error));
}
return;
}
nhlog::net()->error("failed to register: status_code ({}), matrix_error({})",
static_cast<int>(err->status_code),
err->matrix_error.error);
showError(QString::fromStdString(err->matrix_error.error));
};
usernameAvailable_ = false;
usernameUnavailable_ = true;
} else {
usernameAvailable_ = available.available;
usernameUnavailable_ = !available.available;
}
lookingUpUsername_ = false;
emit lookingUpUsernameChanged();
});
}
void
RegisterPage::paintEvent(QPaintEvent *)
RegisterPage::startRegistration(QString username, QString password, QString devicename)
{
QStyleOption opt;
opt.initFrom(this);
QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
// These inputs should still be alright, but check just in case
if (!username.isEmpty() && !password.isEmpty() && usernameAvailable_ && supported_) {
registrationError_.clear();
emit errorChanged();
registering_ = true;
emit registeringChanged();
connect(UIA::instance(), &UIA::error, this, [this](QString msg) {
setError(msg);
disconnect(UIA::instance(), &UIA::error, this, nullptr);
});
http::client()->registration(
username.toStdString(),
password.toStdString(),
::UIA::instance()->genericHandler(QStringLiteral("Registration")),
[this](const mtx::responses::Register &res, mtx::http::RequestErr err) {
registering_ = false;
emit registeringChanged();
if (!err) {
http::client()->set_user(res.user_id);
http::client()->set_access_token(res.access_token);
emit registerOk();
disconnect(UIA::instance(), &UIA::error, this, nullptr);
return;
}
// The server requires registration flows.
if (err->status_code == 401 && err->matrix_error.unauthorized.flows.empty()) {
nhlog::net()->warn("failed to retrieve registration flows: "
"status_code({}), matrix_error({}) ",
static_cast<int>(err->status_code),
err->matrix_error.error);
setError(QString::fromStdString(err->matrix_error.error));
disconnect(UIA::instance(), &UIA::error, this, nullptr);
return;
}
nhlog::net()->error("failed to register: status_code ({}), matrix_error({})",
static_cast<int>(err->status_code),
err->matrix_error.error);
setError(QString::fromStdString(err->matrix_error.error));
disconnect(UIA::instance(), &UIA::error, this, nullptr);
},
devicename.isEmpty() ? LoginPage::initialDeviceName_() : devicename.toStdString());
}
}

View File

@ -6,88 +6,69 @@
#pragma once
#include <QWidget>
#include <memory>
#include <QObject>
#include <QString>
#include <mtx/user_interactive.hpp>
#include <mtxclient/http/client.hpp>
class FlatButton;
class RaisedButton;
class TextField;
class QLabel;
class QVBoxLayout;
class QHBoxLayout;
class RegisterPage : public QWidget
class RegisterPage : public QObject
{
Q_OBJECT
public:
RegisterPage(QWidget *parent = nullptr);
Q_PROPERTY(QString error READ error NOTIFY errorChanged)
Q_PROPERTY(QString hsError READ hsError NOTIFY hsErrorChanged)
Q_PROPERTY(QString usernameError READ usernameError NOTIFY lookingUpUsernameChanged)
Q_PROPERTY(bool registering READ registering NOTIFY registeringChanged)
Q_PROPERTY(bool supported READ supported NOTIFY lookingUpHsChanged)
Q_PROPERTY(bool lookingUpHs READ lookingUpHs NOTIFY lookingUpHsChanged)
Q_PROPERTY(bool lookingUpUsername READ lookingUpUsername NOTIFY lookingUpUsernameChanged)
Q_PROPERTY(bool usernameAvailable READ usernameAvailable NOTIFY lookingUpUsernameChanged)
Q_PROPERTY(bool usernameUnavailable READ usernameUnavailable NOTIFY lookingUpUsernameChanged)
protected:
void paintEvent(QPaintEvent *event) override;
public:
RegisterPage(QObject *parent = nullptr);
Q_INVOKABLE void setServer(QString server);
Q_INVOKABLE void checkUsername(QString name);
Q_INVOKABLE void startRegistration(QString username, QString password, QString deviceName);
Q_INVOKABLE QString initialDeviceName() const;
bool registering() const { return registering_; }
bool supported() const { return supported_; }
bool lookingUpHs() const { return lookingUpHs_; }
bool lookingUpUsername() const { return lookingUpUsername_; }
bool usernameAvailable() const { return usernameAvailable_; }
bool usernameUnavailable() const { return usernameUnavailable_; }
QString error() const { return registrationError_; }
QString usernameError() const { return usernameError_; }
QString hsError() const { return hsError_; }
signals:
void backButtonClicked();
void errorOccurred();
void errorChanged();
void hsErrorChanged();
//! Used to trigger the corresponding slot outside of the main thread.
void serverError(const QString &err);
void registeringChanged();
void lookingUpHsChanged();
void lookingUpUsernameChanged();
void wellKnownLookup();
void versionsCheck();
void registration();
void registering();
void registerOk();
private slots:
void onBackButtonClicked();
void onRegisterButtonClicked();
// function for showing different errors
void showError(const QString &msg);
void showError(QLabel *label, const QString &msg);
bool checkOneField(QLabel *label, const TextField *t_field, const QString &msg);
bool checkUsername();
bool checkPassword();
bool checkPasswordConfirmation();
bool checkServer();
void doWellKnownLookup();
void doVersionsCheck();
void doRegistration();
mtx::http::Callback<mtx::responses::Register> registrationCb();
private:
QVBoxLayout *top_layout_;
void versionsCheck();
QHBoxLayout *back_layout_;
QHBoxLayout *logo_layout_;
QHBoxLayout *button_layout_;
void setHsError(QString err);
void setError(QString err);
QLabel *logo_;
QLabel *error_label_;
QLabel *error_username_label_;
QLabel *error_password_label_;
QLabel *error_password_confirmation_label_;
QLabel *error_server_label_;
QLabel *error_registration_token_label_;
QString registrationError_, hsError_, usernameError_;
FlatButton *back_button_;
RaisedButton *register_button_;
bool registering_;
bool supported_;
bool lookingUpHs_;
bool lookingUpUsername_;
bool usernameAvailable_;
bool usernameUnavailable_;
QWidget *form_widget_;
QHBoxLayout *form_wrapper_;
QVBoxLayout *form_layout_;
TextField *username_input_;
TextField *password_input_;
TextField *password_confirmation_;
TextField *server_input_;
TextField *registration_token_input_;
QString lastServer;
};

View File

@ -10,6 +10,7 @@
#include <QMenu>
#include <QPainter>
#include <QTimer>
#include <QWindow>
#include "TrayIcon.h"
@ -100,7 +101,7 @@ MsgCountComposedIcon::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State s
return result;
}
TrayIcon::TrayIcon(const QString &filename, QWidget *parent)
TrayIcon::TrayIcon(const QString &filename, QWindow *parent)
: QSystemTrayIcon(parent)
{
#if defined(Q_OS_MAC) || defined(Q_OS_WIN)
@ -110,13 +111,13 @@ TrayIcon::TrayIcon(const QString &filename, QWidget *parent)
setIcon(QIcon(icon_));
#endif
QMenu *menu = new QMenu(parent);
QMenu *menu = new QMenu();
setContextMenu(menu);
viewAction_ = new QAction(tr("Show"), this);
quitAction_ = new QAction(tr("Quit"), this);
connect(viewAction_, SIGNAL(triggered()), parent, SLOT(show()));
connect(viewAction_, &QAction::triggered, parent, &QWindow::show);
connect(quitAction_, &QAction::triggered, this, QApplication::quit);
menu->addAction(viewAction_);

View File

@ -40,7 +40,7 @@ class TrayIcon : public QSystemTrayIcon
{
Q_OBJECT
public:
TrayIcon(const QString &filename, QWidget *parent);
TrayIcon(const QString &filename, QWindow *parent);
public slots:
void setUnreadCount(int count);

View File

@ -5,25 +5,14 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QApplication>
#include <QComboBox>
#include <QCoreApplication>
#include <QFileDialog>
#include <QFontComboBox>
#include <QFormLayout>
#include <QFontDatabase>
#include <QInputDialog>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPainter>
#include <QPushButton>
#include <QResizeEvent>
#include <QScrollArea>
#include <QScroller>
#include <QSpinBox>
#include <QStandardPaths>
#include <QString>
#include <QTextStream>
#include <QtQml>
#include <mtx/secret_storage.hpp>
#include "Cache.h"
@ -33,8 +22,7 @@
#include "UserSettingsPage.h"
#include "Utils.h"
#include "encryption/Olm.h"
#include "ui/FlatButton.h"
#include "ui/ToggleButton.h"
#include "ui/Theme.h"
#include "voip/CallDevices.h"
#include "config/nheko.h"
@ -1518,7 +1506,7 @@ UserSettingsModel::setData(const QModelIndex &index, const QVariant &value, int
QString homeFolder =
QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
auto filepath = QFileDialog::getOpenFileName(
MainWindow::instance(), tr("Select a file"), homeFolder, tr("All Files (*)"));
nullptr, tr("Select a file"), homeFolder, tr("All Files (*)"));
if (!filepath.isEmpty()) {
i->setRingtone(filepath);
i->setRingtone(filepath);
@ -1600,11 +1588,11 @@ UserSettingsModel::importSessionKeys()
{
const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
const QString fileName = QFileDialog::getOpenFileName(
MainWindow::instance(), tr("Open Sessions File"), homeFolder, QLatin1String(""));
nullptr, tr("Open Sessions File"), homeFolder, QLatin1String(""));
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly)) {
QMessageBox::warning(MainWindow::instance(), tr("Error"), file.errorString());
QMessageBox::warning(nullptr, tr("Error"), file.errorString());
return;
}
@ -1612,7 +1600,7 @@ UserSettingsModel::importSessionKeys()
auto payload = std::string(bin.data(), bin.size());
bool ok;
auto password = QInputDialog::getText(MainWindow::instance(),
auto password = QInputDialog::getText(nullptr,
tr("File Password"),
tr("Enter the passphrase to decrypt the file:"),
QLineEdit::Password,
@ -1622,8 +1610,7 @@ UserSettingsModel::importSessionKeys()
return;
if (password.isEmpty()) {
QMessageBox::warning(
MainWindow::instance(), tr("Error"), tr("The password cannot be empty"));
QMessageBox::warning(nullptr, tr("Error"), tr("The password cannot be empty"));
return;
}
@ -1631,7 +1618,7 @@ UserSettingsModel::importSessionKeys()
auto sessions = mtx::crypto::decrypt_exported_sessions(payload, password.toStdString());
cache::importSessionKeys(std::move(sessions));
} catch (const std::exception &e) {
QMessageBox::warning(MainWindow::instance(), tr("Error"), e.what());
QMessageBox::warning(nullptr, tr("Error"), e.what());
}
}
void
@ -1639,7 +1626,7 @@ UserSettingsModel::exportSessionKeys()
{
// Open password dialog.
bool ok;
auto password = QInputDialog::getText(MainWindow::instance(),
auto password = QInputDialog::getText(nullptr,
tr("File Password"),
tr("Enter passphrase to encrypt your session keys:"),
QLineEdit::Password,
@ -1649,19 +1636,18 @@ UserSettingsModel::exportSessionKeys()
return;
if (password.isEmpty()) {
QMessageBox::warning(
MainWindow::instance(), tr("Error"), tr("The password cannot be empty"));
QMessageBox::warning(nullptr, tr("Error"), tr("The password cannot be empty"));
return;
}
// Open file dialog to save the file.
const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
const QString fileName = QFileDialog::getSaveFileName(
MainWindow::instance(), tr("File to save the exported session keys"), homeFolder);
nullptr, tr("File to save the exported session keys"), homeFolder);
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::warning(MainWindow::instance(), tr("Error"), file.errorString());
QMessageBox::warning(nullptr, tr("Error"), file.errorString());
return;
}
@ -1679,7 +1665,7 @@ UserSettingsModel::exportSessionKeys()
out << prefix << newline << b64 << newline << suffix << newline;
file.close();
} catch (const std::exception &e) {
QMessageBox::warning(MainWindow::instance(), tr("Error"), e.what());
QMessageBox::warning(nullptr, tr("Error"), e.what());
}
}
void

View File

@ -7,12 +7,9 @@
#pragma once
#include <QAbstractListModel>
#include <QFontDatabase>
#include <QFrame>
#include <QProcessEnvironment>
#include <QSettings>
#include <QSharedPointer>
#include <QWidget>
#include "JdenticonProvider.h"
#include <optional>

View File

@ -17,6 +17,7 @@
#include <QStringBuilder>
#include <QTextBoundaryFinder>
#include <QTextDocument>
#include <QWindow>
#include <QXmlStreamReader>
#include <array>
@ -770,20 +771,17 @@ utils::luminance(const QColor &col)
}
void
utils::centerWidget(QWidget *widget, QWidget *parent)
utils::centerWidget(QWidget *widget, QWindow *parent)
{
if (parent) {
widget->window()->windowHandle()->setTransientParent(parent);
return;
}
auto findCenter = [childRect = widget->rect()](QRect hostRect) -> QPoint {
return QPoint(hostRect.center().x() - (childRect.width() * 0.5),
hostRect.center().y() - (childRect.height() * 0.5));
};
if (parent) {
widget->move(parent->window()->frameGeometry().topLeft() +
parent->window()->rect().center() - widget->rect().center());
return;
}
// Deprecated in 5.13: widget->move(findCenter(QApplication::desktop()->screenGeometry()));
widget->move(findCenter(QGuiApplication::primaryScreen()->geometry()));
}

View File

@ -290,7 +290,7 @@ luminance(const QColor &col);
//! Center a widget in relation to another widget.
void
centerWidget(QWidget *widget, QWidget *parent);
centerWidget(QWidget *widget, QWindow *parent);
void
restoreCombobox(QComboBox *combo, const QString &value);

View File

@ -1,88 +0,0 @@
// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr>
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QLabel>
#include <QLayout>
#include <QPainter>
#include <QStyleOption>
#include "Config.h"
#include "WelcomePage.h"
#include "ui/RaisedButton.h"
#include "ui/TextLabel.h"
WelcomePage::WelcomePage(QWidget *parent)
: QWidget(parent)
{
auto topLayout_ = new QVBoxLayout(this);
topLayout_->setSpacing(20);
topLayout_->setAlignment(Qt::AlignCenter);
QFont headingFont;
headingFont.setPointSizeF(headingFont.pointSizeF() * 2);
QFont subTitleFont;
subTitleFont.setPointSizeF(subTitleFont.pointSizeF() * 1.5);
QIcon icon{QIcon::fromTheme("nheko", QIcon{":/logos/splash.png"})};
auto logo_ = new QLabel(this);
logo_->setPixmap(icon.pixmap(256));
logo_->setAlignment(Qt::AlignCenter);
QString heading(tr("Welcome to nheko! The desktop client for the Matrix protocol."));
QString main(tr("Enjoy your stay!"));
auto intoTxt_ = new TextLabel(heading, this);
intoTxt_->setFont(headingFont);
intoTxt_->setAlignment(Qt::AlignCenter);
auto subTitle = new TextLabel(main, this);
subTitle->setFont(subTitleFont);
subTitle->setAlignment(Qt::AlignCenter);
topLayout_->addStretch(1);
topLayout_->addWidget(logo_);
topLayout_->addWidget(intoTxt_);
topLayout_->addWidget(subTitle);
auto btnLayout_ = new QHBoxLayout();
btnLayout_->setSpacing(20);
btnLayout_->setContentsMargins(0, 20, 0, 20);
const int fontHeight = QFontMetrics{subTitleFont}.height();
const int buttonHeight = fontHeight * 2.5;
const int buttonWidth = fontHeight * 8;
auto registerBtn = new RaisedButton(tr("REGISTER"), this);
registerBtn->setMinimumSize(buttonWidth, buttonHeight);
registerBtn->setFontSize(subTitleFont.pointSizeF());
registerBtn->setCornerRadius(conf::btn::cornerRadius);
auto loginBtn = new RaisedButton(tr("LOGIN"), this);
loginBtn->setMinimumSize(buttonWidth, buttonHeight);
loginBtn->setFontSize(subTitleFont.pointSizeF());
loginBtn->setCornerRadius(conf::btn::cornerRadius);
btnLayout_->addStretch(1);
btnLayout_->addWidget(registerBtn);
btnLayout_->addWidget(loginBtn);
btnLayout_->addStretch(1);
topLayout_->addLayout(btnLayout_);
topLayout_->addStretch(1);
connect(registerBtn, &QPushButton::clicked, this, &WelcomePage::userRegister);
connect(loginBtn, &QPushButton::clicked, this, &WelcomePage::userLogin);
}
void
WelcomePage::paintEvent(QPaintEvent *)
{
QStyleOption opt;
opt.initFrom(this);
QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
}

View File

@ -1,26 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QWidget>
class WelcomePage : public QWidget
{
Q_OBJECT
public:
explicit WelcomePage(QWidget *parent = nullptr);
protected:
void paintEvent(QPaintEvent *) override;
signals:
// Notify that the user wants to login in.
void userLogin();
// Notify that the user wants to register.
void userRegister();
};

View File

@ -17,6 +17,7 @@
#include <QLibraryInfo>
#include <QMessageBox>
#include <QPoint>
#include <QQuickView>
#include <QScreen>
#include <QStandardPaths>
#include <QTranslator>
@ -279,6 +280,7 @@ main(int argc, char *argv[])
font.setPointSizeF(settings.lock()->fontSize());
app.setFont(font);
settings.lock()->applyTheme();
if (QLocale().language() == QLocale::C)
QLocale::setDefault(QLocale(QLocale::English, QLocale::UnitedKingdom));
@ -296,9 +298,10 @@ main(int argc, char *argv[])
app.installTranslator(&appTranslator);
MainWindow w;
// QQuickView w;
// Move the MainWindow to the center
w.move(screenCenter(w.width(), w.height()));
// w.move(screenCenter(w.width(), w.height()));
if (!(settings.lock()->startInTray() && settings.lock()->tray()))
w.show();
@ -314,7 +317,7 @@ main(int argc, char *argv[])
QObject::connect(&app, &SingleApplication::instanceStarted, &w, [&w]() {
w.show();
w.raise();
w.activateWindow();
w.requestActivate();
});
// It seems like handling the message in a blocking manner is a no-go. I have no idea how to

View File

@ -266,8 +266,8 @@ void
InputBar::openFileSelection()
{
const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
const auto fileName = QFileDialog::getOpenFileName(
ChatPage::instance(), tr("Select a file"), homeFolder, tr("All Files (*)"));
const auto fileName =
QFileDialog::getOpenFileName(nullptr, tr("Select a file"), homeFolder, tr("All Files (*)"));
if (fileName.isEmpty())
return;
@ -659,7 +659,7 @@ InputBar::command(const QString &command, QString args)
void
InputBar::showPreview(const QMimeData &source, const QString &path, const QStringList &formats)
{
auto *previewDialog_ = new dialogs::PreviewUploadOverlay(ChatPage::instance());
auto *previewDialog_ = new dialogs::PreviewUploadOverlay(nullptr);
previewDialog_->setAttribute(Qt::WA_DeleteOnClose);
// Force SVG to _not_ be handled as an image, but as raw data

View File

@ -8,6 +8,7 @@
#include "Cache_p.h"
#include "ChatPage.h"
#include "Logging.h"
#include "MainWindow.h"
#include "MatrixClient.h"
#include "MxcImageProvider.h"
#include "TimelineModel.h"
@ -275,7 +276,7 @@ RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification)
connect(newRoom.data(),
&TimelineModel::newEncryptedImage,
manager->imageProvider(),
MainWindow::instance()->imageProvider(),
&MxcImageProvider::addEncryptionInfo);
connect(newRoom.data(),
&TimelineModel::forwardToRoom,
@ -509,7 +510,7 @@ RoomlistModel::sync(const mtx::responses::Sync &sync_)
// room_model->addEvents(room.timeline);
connect(room_model.data(),
&TimelineModel::newCallEvent,
manager->callManager(),
ChatPage::instance()->callManager(),
&CallManager::syncEvent,
Qt::UniqueConnection);

View File

@ -1031,7 +1031,7 @@ TimelineModel::setCurrentIndex(int index)
if (index != oldIndex)
emit currentIndexChanged(index);
if (!ChatPage::instance()->isActiveWindow())
if (MainWindow::instance() != QGuiApplication::focusWindow())
return;
if (!currentId.startsWith('m')) {
@ -1495,7 +1495,7 @@ TimelineModel::saveMedia(const QString &eventId) const
const QString openLocation = downloadsFolder + "/" + originalFilename;
const QString filename =
QFileDialog::getSaveFileName(manager_->getWidget(), dialogTitle, openLocation, filterString);
QFileDialog::getSaveFileName(nullptr, dialogTitle, openLocation, filterString);
if (filename.isEmpty())
return false;

View File

@ -5,10 +5,10 @@
#include "TimelineViewManager.h"
#include <QApplication>
#include <QDropEvent>
#include <QFileDialog>
#include <QMetaType>
#include <QPalette>
#include <QQmlContext>
#include <QQmlEngine>
#include <QStandardPaths>
@ -45,10 +45,6 @@
#include "ui/NhekoGlobalObject.h"
#include "ui/UIA.h"
Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents)
Q_DECLARE_METATYPE(std::vector<DeviceInfo>)
Q_DECLARE_METATYPE(std::vector<mtx::responses::PublicRoomsChunk>)
namespace msgs = mtx::events::msg;
namespace {
@ -102,19 +98,6 @@ void
TimelineViewManager::updateColorPalette()
{
userColors.clear();
if (ChatPage::instance()->userSettings()->theme() == QLatin1String("light")) {
view->rootContext()->setContextProperty(QStringLiteral("currentActivePalette"), QPalette());
view->rootContext()->setContextProperty(QStringLiteral("currentInactivePalette"),
QPalette());
} else if (ChatPage::instance()->userSettings()->theme() == QLatin1String("dark")) {
view->rootContext()->setContextProperty(QStringLiteral("currentActivePalette"), QPalette());
view->rootContext()->setContextProperty(QStringLiteral("currentInactivePalette"),
QPalette());
} else {
view->rootContext()->setContextProperty(QStringLiteral("currentActivePalette"), QPalette());
view->rootContext()->setContextProperty(QStringLiteral("currentInactivePalette"), nullptr);
}
}
QColor
@ -126,112 +109,15 @@ TimelineViewManager::userColor(QString id, QColor background)
return userColors.value(idx);
}
TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *parent)
TimelineViewManager::TimelineViewManager(CallManager *, ChatPage *parent)
: QObject(parent)
, imgProvider(new MxcImageProvider())
, colorImgProvider(new ColorImageProvider())
, blurhashProvider(new BlurhashProvider())
, jdenticonProvider(new JdenticonProvider())
, rooms_(new RoomlistModel(this))
, communities_(new CommunitiesModel(this))
, callManager_(callManager)
, verificationManager_(new VerificationManager(this))
, presenceEmitter(new PresenceEmitter(this))
{
qRegisterMetaType<mtx::events::msg::KeyVerificationAccept>();
qRegisterMetaType<mtx::events::msg::KeyVerificationCancel>();
qRegisterMetaType<mtx::events::msg::KeyVerificationDone>();
qRegisterMetaType<mtx::events::msg::KeyVerificationKey>();
qRegisterMetaType<mtx::events::msg::KeyVerificationMac>();
qRegisterMetaType<mtx::events::msg::KeyVerificationReady>();
qRegisterMetaType<mtx::events::msg::KeyVerificationRequest>();
qRegisterMetaType<mtx::events::msg::KeyVerificationStart>();
qRegisterMetaType<CombinedImagePackModel *>();
qRegisterMetaType<std::vector<mtx::responses::PublicRoomsChunk>>();
qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject,
"im.nheko",
1,
0,
"MtxEvent",
QStringLiteral("Can't instantiate enum!"));
qmlRegisterUncreatableMetaObject(
olm::staticMetaObject, "im.nheko", 1, 0, "Olm", QStringLiteral("Can't instantiate enum!"));
qmlRegisterUncreatableMetaObject(crypto::staticMetaObject,
"im.nheko",
1,
0,
"Crypto",
QStringLiteral("Can't instantiate enum!"));
qmlRegisterUncreatableMetaObject(verification::staticMetaObject,
"im.nheko",
1,
0,
"VerificationStatus",
QStringLiteral("Can't instantiate enum!"));
qmlRegisterType<DelegateChoice>("im.nheko", 1, 0, "DelegateChoice");
qmlRegisterType<DelegateChooser>("im.nheko", 1, 0, "DelegateChooser");
qmlRegisterType<NhekoDropArea>("im.nheko", 1, 0, "NhekoDropArea");
qmlRegisterType<NhekoCursorShape>("im.nheko", 1, 0, "CursorShape");
qmlRegisterType<MxcAnimatedImage>("im.nheko", 1, 0, "MxcAnimatedImage");
qmlRegisterType<MxcMediaProxy>("im.nheko", 1, 0, "MxcMedia");
qmlRegisterUncreatableType<DeviceVerificationFlow>(
"im.nheko",
1,
0,
"DeviceVerificationFlow",
QStringLiteral("Can't create verification flow from QML!"));
qmlRegisterUncreatableType<UserProfile>(
"im.nheko",
1,
0,
"UserProfileModel",
QStringLiteral("UserProfile needs to be instantiated on the C++ side"));
qmlRegisterUncreatableType<MemberList>(
"im.nheko",
1,
0,
"MemberList",
QStringLiteral("MemberList needs to be instantiated on the C++ side"));
qmlRegisterUncreatableType<RoomSettings>(
"im.nheko",
1,
0,
"RoomSettingsModel",
QStringLiteral("Room Settings needs to be instantiated on the C++ side"));
qmlRegisterUncreatableType<TimelineModel>(
"im.nheko", 1, 0, "Room", QStringLiteral("Room needs to be instantiated on the C++ side"));
qmlRegisterUncreatableType<ImagePackListModel>(
"im.nheko",
1,
0,
"ImagePackListModel",
QStringLiteral("ImagePackListModel needs to be instantiated on the C++ side"));
qmlRegisterUncreatableType<SingleImagePackModel>(
"im.nheko",
1,
0,
"SingleImagePackModel",
QStringLiteral("SingleImagePackModel needs to be instantiated on the C++ side"));
qmlRegisterUncreatableType<InviteesModel>(
"im.nheko",
1,
0,
"InviteesModel",
QStringLiteral("InviteesModel needs to be instantiated on the C++ side"));
qmlRegisterUncreatableType<ReadReceiptsProxy>(
"im.nheko",
1,
0,
"ReadReceiptsProxy",
QStringLiteral("ReadReceiptsProxy needs to be instantiated on the C++ side"));
static auto self = this;
qmlRegisterSingletonInstance("im.nheko", 1, 0, "MainWindow", MainWindow::instance());
qmlRegisterSingletonInstance("im.nheko", 1, 0, "TimelineManager", self);
qmlRegisterSingletonInstance("im.nheko", 1, 0, "UIA", UIA::instance());
qmlRegisterSingletonType<RoomlistModel>(
"im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * {
auto ptr = new FilteredRoomlistModel(self->rooms_);
@ -247,79 +133,15 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
return ptr;
});
qmlRegisterSingletonInstance("im.nheko", 1, 0, "Communities", self->communities_);
qmlRegisterSingletonInstance(
"im.nheko", 1, 0, "Settings", ChatPage::instance()->userSettings().data());
qmlRegisterSingletonInstance(
"im.nheko", 1, 0, "CallManager", ChatPage::instance()->callManager());
qmlRegisterSingletonType<Clipboard>(
"im.nheko", 1, 0, "Clipboard", [](QQmlEngine *, QJSEngine *) -> QObject * {
return new Clipboard();
});
qmlRegisterSingletonType<Nheko>(
"im.nheko", 1, 0, "Nheko", [](QQmlEngine *, QJSEngine *) -> QObject * {
return new Nheko();
});
qmlRegisterSingletonType<UserSettingsModel>(
"im.nheko", 1, 0, "UserSettingsModel", [](QQmlEngine *, QJSEngine *) -> QObject * {
return new UserSettingsModel();
});
qmlRegisterSingletonInstance("im.nheko", 1, 0, "VerificationManager", verificationManager_);
qmlRegisterSingletonInstance("im.nheko", 1, 0, "Presence", presenceEmitter);
qmlRegisterSingletonType<SelfVerificationStatus>(
"im.nheko", 1, 0, "SelfVerificationStatus", [](QQmlEngine *, QJSEngine *) -> QObject * {
auto ptr = new SelfVerificationStatus();
QObject::connect(ChatPage::instance(),
&ChatPage::initializeEmptyViews,
ptr,
&SelfVerificationStatus::invalidate);
return ptr;
});
qRegisterMetaType<mtx::events::collections::TimelineEvents>();
qRegisterMetaType<std::vector<DeviceInfo>>();
qmlRegisterUncreatableType<FilteredCommunitiesModel>(
"im.nheko",
1,
0,
"FilteredCommunitiesModel",
QStringLiteral("Use Communities.filtered() to create a FilteredCommunitiesModel"));
qmlRegisterType<emoji::EmojiModel>("im.nheko.EmojiModel", 1, 0, "EmojiModel");
qmlRegisterUncreatableType<emoji::Emoji>(
"im.nheko.EmojiModel", 1, 0, "Emoji", QStringLiteral("Used by emoji models"));
qmlRegisterUncreatableMetaObject(emoji::staticMetaObject,
"im.nheko.EmojiModel",
1,
0,
"EmojiCategory",
QStringLiteral("Error: Only enums"));
qmlRegisterType<RoomDirectoryModel>("im.nheko", 1, 0, "RoomDirectoryModel");
#ifdef USE_QUICK_VIEW
view = new QQuickView(parent);
container = QWidget::createWindowContainer(view, parent);
#else
view = new QQuickWidget(parent);
container = view;
view->setResizeMode(QQuickWidget::SizeRootObjectToView);
container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
connect(view, &QQuickWidget::statusChanged, this, [](QQuickWidget::Status status) {
nhlog::ui()->debug("Status changed to {}", status);
});
#endif
container->setMinimumSize(200, 200);
updateColorPalette();
view->engine()->addImageProvider(QStringLiteral("MxcImage"), imgProvider);
view->engine()->addImageProvider(QStringLiteral("colorimage"), colorImgProvider);
view->engine()->addImageProvider(QStringLiteral("blurhash"), blurhashProvider);
if (JdenticonProvider::isAvailable())
view->engine()->addImageProvider(QStringLiteral("jdenticon"), jdenticonProvider);
view->setSource(QUrl(QStringLiteral("qrc:///qml/Root.qml")));
connect(parent, &ChatPage::themeChanged, this, &TimelineViewManager::updateColorPalette);
connect(UserSettings::instance().get(),
&UserSettings::themeChanged,
this,
&TimelineViewManager::updateColorPalette);
connect(parent,
&ChatPage::receivedRoomDeviceVerificationRequest,
verificationManager_,
@ -336,6 +158,16 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
isInitialSync_ = true;
emit initialSyncChanged(true);
});
connect(qobject_cast<QApplication *>(QApplication::instance()),
&QApplication::focusWindowChanged,
this,
&TimelineViewManager::focusChanged);
}
bool
TimelineViewManager::isWindowFocused() const
{
return MainWindow::instance() == QApplication::focusWindow();
}
void
@ -379,7 +211,8 @@ void
TimelineViewManager::setVideoCallItem()
{
WebRTCSession::instance().setVideoItem(
view->rootObject()->findChild<QQuickItem *>(QStringLiteral("videoCallItem")));
MainWindow::instance()->rootObject()->findChild<QQuickItem *>(
QStringLiteral("videoCallItem")));
}
void
@ -401,7 +234,7 @@ TimelineViewManager::showEvent(const QString &room_id, const QString &event_id)
if (auto room = rooms_->getRoomById(room_id)) {
if (rooms_->currentRoom() != room) {
rooms_->setCurrentRoom(room_id);
container->setFocus();
MainWindow::instance()->requestActivate();
nhlog::ui()->info("Activated room {}", room_id.toStdString());
}
@ -439,7 +272,7 @@ TimelineViewManager::saveMedia(QString mxcUrl)
QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
const QString openLocation = downloadsFolder + "/" + mxcUrl.splitRef(u'/').constLast();
const QString filename = QFileDialog::getSaveFileName(getWidget(), {}, openLocation);
const QString filename = QFileDialog::getSaveFileName(nullptr, {}, openLocation);
if (filename.isEmpty())
return;
@ -590,12 +423,6 @@ TimelineViewManager::completerFor(QString completerName, QString roomId)
return nullptr;
}
void
TimelineViewManager::focusTimeline()
{
getWidget()->setFocus();
}
void
TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEvents *e,
QString roomId)

View File

@ -8,8 +8,6 @@
#include <QHash>
#include <QQuickItem>
#include <QQuickTextDocument>
#include <QQuickView>
#include <QQuickWidget>
#include <QWidget>
#include <mtx/common.hpp>
@ -43,23 +41,19 @@ class TimelineViewManager : public QObject
Q_PROPERTY(
bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged)
Q_PROPERTY(
bool isWindowFocused MEMBER isWindowFocused_ READ isWindowFocused NOTIFY focusChanged)
Q_PROPERTY(bool isWindowFocused READ isWindowFocused NOTIFY focusChanged)
public:
TimelineViewManager(CallManager *callManager, ChatPage *parent = nullptr);
QWidget *getWidget() const { return container; }
void sync(const mtx::responses::Sync &sync_);
MxcImageProvider *imageProvider() { return imgProvider; }
CallManager *callManager() { return callManager_; }
VerificationManager *verificationManager() { return verificationManager_; }
void clearAll() { rooms_->clear(); }
Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; }
bool isWindowFocused() const { return isWindowFocused_; }
bool isWindowFocused() const;
Q_INVOKABLE void openImageOverlay(TimelineModel *room, QString mxcUrl, QString eventId);
Q_INVOKABLE void openImagePackSettings(QString roomid);
Q_INVOKABLE void saveMedia(QString mxcUrl);
@ -98,14 +92,8 @@ public slots:
void updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
void receivedSessionKey(const std::string &room_id, const std::string &session_id);
void initializeRoomlist();
void chatFocusChanged(bool focused)
{
isWindowFocused_ = focused;
emit focusChanged();
}
void showEvent(const QString &room_id, const QString &event_id);
void focusTimeline();
void updateColorPalette();
void queueReply(const QString &roomid, const QString &repliedToEvent, const QString &replyBody);
@ -122,26 +110,12 @@ public slots:
RoomlistModel *rooms() { return rooms_; }
private:
#ifdef USE_QUICK_VIEW
QQuickView *view;
#else
QQuickWidget *view;
#endif
QWidget *container;
MxcImageProvider *imgProvider;
ColorImageProvider *colorImgProvider;
BlurhashProvider *blurhashProvider;
JdenticonProvider *jdenticonProvider;
bool isInitialSync_ = true;
bool isWindowFocused_ = false;
bool isInitialSync_ = true;
RoomlistModel *rooms_ = nullptr;
CommunitiesModel *communities_ = nullptr;
// don't move this above the rooms_
CallManager *callManager_ = nullptr;
VerificationManager *verificationManager_ = nullptr;
PresenceEmitter *presenceEmitter = nullptr;

View File

@ -1,108 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "DropShadow.h"
#include <QLinearGradient>
#include <QPainter>
void
DropShadow::draw(QPainter &painter,
qint16 margin,
qreal radius,
QColor start,
QColor end,
qreal startPosition,
qreal endPosition0,
qreal endPosition1,
qreal width,
qreal height)
{
painter.setPen(Qt::NoPen);
QLinearGradient gradient;
gradient.setColorAt(startPosition, start);
gradient.setColorAt(endPosition0, end);
// Right
QPointF right0(width - margin, height / 2);
QPointF right1(width, height / 2);
gradient.setStart(right0);
gradient.setFinalStop(right1);
painter.setBrush(QBrush(gradient));
// Deprecated in 5.13: painter.drawRoundRect(
// QRectF(QPointF(width - margin * radius, margin), QPointF(width, height -
// margin)), 0.0, 0.0);
painter.drawRoundedRect(
QRectF(QPointF(width - margin * radius, margin), QPointF(width, height - margin)), 0.0, 0.0);
// Left
QPointF left0(margin, height / 2);
QPointF left1(0, height / 2);
gradient.setStart(left0);
gradient.setFinalStop(left1);
painter.setBrush(QBrush(gradient));
painter.drawRoundedRect(
QRectF(QPointF(margin * radius, margin), QPointF(0, height - margin)), 0.0, 0.0);
// Top
QPointF top0(width / 2, margin);
QPointF top1(width / 2, 0);
gradient.setStart(top0);
gradient.setFinalStop(top1);
painter.setBrush(QBrush(gradient));
painter.drawRoundedRect(QRectF(QPointF(width - margin, 0), QPointF(margin, margin)), 0.0, 0.0);
// Bottom
QPointF bottom0(width / 2, height - margin);
QPointF bottom1(width / 2, height);
gradient.setStart(bottom0);
gradient.setFinalStop(bottom1);
painter.setBrush(QBrush(gradient));
painter.drawRoundedRect(
QRectF(QPointF(margin, height - margin), QPointF(width - margin, height)), 0.0, 0.0);
// BottomRight
QPointF bottomright0(width - margin, height - margin);
QPointF bottomright1(width, height);
gradient.setStart(bottomright0);
gradient.setFinalStop(bottomright1);
gradient.setColorAt(endPosition1, end);
painter.setBrush(QBrush(gradient));
painter.drawRoundedRect(QRectF(bottomright0, bottomright1), 0.0, 0.0);
// BottomLeft
QPointF bottomleft0(margin, height - margin);
QPointF bottomleft1(0, height);
gradient.setStart(bottomleft0);
gradient.setFinalStop(bottomleft1);
gradient.setColorAt(endPosition1, end);
painter.setBrush(QBrush(gradient));
painter.drawRoundedRect(QRectF(bottomleft0, bottomleft1), 0.0, 0.0);
// TopLeft
QPointF topleft0(margin, margin);
QPointF topleft1(0, 0);
gradient.setStart(topleft0);
gradient.setFinalStop(topleft1);
gradient.setColorAt(endPosition1, end);
painter.setBrush(QBrush(gradient));
painter.drawRoundedRect(QRectF(topleft0, topleft1), 0.0, 0.0);
// TopRight
QPointF topright0(width - margin, margin);
QPointF topright1(width, 0);
gradient.setStart(topright0);
gradient.setFinalStop(topright1);
gradient.setColorAt(endPosition1, end);
painter.setBrush(QBrush(gradient));
painter.drawRoundedRect(QRectF(topright0, topright1), 0.0, 0.0);
// Widget
painter.setBrush(QBrush(QColor(0xff, 0xff, 0xff)));
painter.setRenderHint(QPainter::Antialiasing);
painter.drawRoundedRect(
QRectF(QPointF(margin, margin), QPointF(width - margin, height - margin)), radius, radius);
}

View File

@ -1,25 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QColor>
class QPainter;
class DropShadow
{
public:
static void draw(QPainter &painter,
qint16 margin,
qreal radius,
QColor start,
QColor end,
qreal startPosition,
qreal endPosition0,
qreal endPosition1,
qreal width,
qreal height);
};

View File

@ -1,730 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QEventTransition>
#include <QFontDatabase>
#include <QIcon>
#include <QMouseEvent>
#include <QPaintEvent>
#include <QPainter>
#include <QPainterPath>
#include <QResizeEvent>
#include <QSignalTransition>
#include "FlatButton.h"
#include "Ripple.h"
#include "RippleOverlay.h"
#include "ThemeManager.h"
// The ampersand is automatically set in QPushButton or QCheckbx
// by KDEPlatformTheme plugin in Qt5.
// [https://bugs.kde.org/show_bug.cgi?id=337491]
//
// A workaroud is to add
//
// [Development]
// AutoCheckAccelerators=false
//
// to ~/.config/kdeglobals
static QString
removeKDEAccelerators(QString text)
{
return text.remove(QChar('&'));
}
void
FlatButton::init()
{
ripple_overlay_ = new RippleOverlay(this);
state_machine_ = new FlatButtonStateMachine(this);
role_ = ui::Role::Default;
ripple_style_ = ui::RippleStyle::PositionedRipple;
icon_placement_ = ui::ButtonIconPlacement::LeftIcon;
overlay_style_ = ui::OverlayStyle::GrayOverlay;
bg_mode_ = Qt::TransparentMode;
fixed_ripple_radius_ = 64;
corner_radius_ = 3;
base_opacity_ = 0.13;
font_size_ = 10; // 10.5;
use_fixed_ripple_radius_ = false;
setStyle(&ThemeManager::instance());
setAttribute(Qt::WA_Hover);
setMouseTracking(true);
setCursor(QCursor(Qt::PointingHandCursor));
QPainterPath path;
path.addRoundedRect(rect(), corner_radius_, corner_radius_);
ripple_overlay_->setClipPath(path);
ripple_overlay_->setClipping(true);
state_machine_->setupProperties();
state_machine_->startAnimations();
}
FlatButton::FlatButton(QWidget *parent, ui::ButtonPreset preset)
: QPushButton(parent)
{
init();
applyPreset(preset);
}
FlatButton::FlatButton(const QString &text, QWidget *parent, ui::ButtonPreset preset)
: QPushButton(text, parent)
{
init();
applyPreset(preset);
}
FlatButton::FlatButton(const QString &text, ui::Role role, QWidget *parent, ui::ButtonPreset preset)
: QPushButton(text, parent)
{
init();
applyPreset(preset);
setRole(role);
}
void
FlatButton::applyPreset(ui::ButtonPreset preset)
{
switch (preset) {
case ui::ButtonPreset::FlatPreset:
setOverlayStyle(ui::OverlayStyle::NoOverlay);
break;
case ui::ButtonPreset::CheckablePreset:
setOverlayStyle(ui::OverlayStyle::NoOverlay);
setCheckable(true);
break;
default:
break;
}
}
void
FlatButton::setRole(ui::Role role)
{
role_ = role;
state_machine_->setupProperties();
}
ui::Role
FlatButton::role() const
{
return role_;
}
void
FlatButton::setForegroundColor(const QColor &color)
{
foreground_color_ = color;
emit foregroundColorChanged();
}
QColor
FlatButton::foregroundColor() const
{
if (!foreground_color_.isValid()) {
if (bg_mode_ == Qt::OpaqueMode) {
return ThemeManager::instance().themeColor(QStringLiteral("BrightWhite"));
}
switch (role_) {
case ui::Role::Primary:
return ThemeManager::instance().themeColor(QStringLiteral("Blue"));
case ui::Role::Secondary:
return ThemeManager::instance().themeColor(QStringLiteral("Gray"));
case ui::Role::Default:
default:
return ThemeManager::instance().themeColor(QStringLiteral("Black"));
}
}
return foreground_color_;
}
void
FlatButton::setBackgroundColor(const QColor &color)
{
background_color_ = color;
emit backgroundColorChanged();
}
QColor
FlatButton::backgroundColor() const
{
if (!background_color_.isValid()) {
switch (role_) {
case ui::Role::Primary:
return ThemeManager::instance().themeColor(QStringLiteral("Blue"));
case ui::Role::Secondary:
return ThemeManager::instance().themeColor(QStringLiteral("Gray"));
case ui::Role::Default:
default:
return ThemeManager::instance().themeColor(QStringLiteral("Black"));
}
}
return background_color_;
}
void
FlatButton::setOverlayColor(const QColor &color)
{
overlay_color_ = color;
setOverlayStyle(ui::OverlayStyle::TintedOverlay);
emit overlayColorChanged();
}
QColor
FlatButton::overlayColor() const
{
if (!overlay_color_.isValid()) {
return foregroundColor();
}
return overlay_color_;
}
void
FlatButton::setDisabledForegroundColor(const QColor &color)
{
disabled_color_ = color;
emit disabledForegroundColorChanged();
}
QColor
FlatButton::disabledForegroundColor() const
{
if (!disabled_color_.isValid()) {
return ThemeManager::instance().themeColor(QStringLiteral("FadedWhite"));
}
return disabled_color_;
}
void
FlatButton::setDisabledBackgroundColor(const QColor &color)
{
disabled_background_color_ = color;
emit disabledBackgroundColorChanged();
}
QColor
FlatButton::disabledBackgroundColor() const
{
if (!disabled_background_color_.isValid()) {
return ThemeManager::instance().themeColor(QStringLiteral("FadedWhite"));
}
return disabled_background_color_;
}
void
FlatButton::setFontSize(qreal size)
{
font_size_ = size;
QFont f(font());
f.setPointSizeF(size);
setFont(f);
emit fontSizeChanged();
update();
}
qreal
FlatButton::fontSize() const
{
return font_size_;
}
void
FlatButton::setOverlayStyle(ui::OverlayStyle style)
{
overlay_style_ = style;
update();
}
ui::OverlayStyle
FlatButton::overlayStyle() const
{
return overlay_style_;
}
void
FlatButton::setRippleStyle(ui::RippleStyle style)
{
ripple_style_ = style;
}
ui::RippleStyle
FlatButton::rippleStyle() const
{
return ripple_style_;
}
void
FlatButton::setIconPlacement(ui::ButtonIconPlacement placement)
{
icon_placement_ = placement;
update();
}
ui::ButtonIconPlacement
FlatButton::iconPlacement() const
{
return icon_placement_;
}
void
FlatButton::setCornerRadius(qreal radius)
{
corner_radius_ = radius;
updateClipPath();
update();
}
qreal
FlatButton::cornerRadius() const
{
return corner_radius_;
}
void
FlatButton::setBackgroundMode(Qt::BGMode mode)
{
bg_mode_ = mode;
state_machine_->setupProperties();
}
Qt::BGMode
FlatButton::backgroundMode() const
{
return bg_mode_;
}
void
FlatButton::setBaseOpacity(qreal opacity)
{
base_opacity_ = opacity;
state_machine_->setupProperties();
}
qreal
FlatButton::baseOpacity() const
{
return base_opacity_;
}
void
FlatButton::setCheckable(bool value)
{
state_machine_->updateCheckedStatus();
state_machine_->setCheckedOverlayProgress(0);
QPushButton::setCheckable(value);
}
void
FlatButton::setHasFixedRippleRadius(bool value)
{
use_fixed_ripple_radius_ = value;
}
bool
FlatButton::hasFixedRippleRadius() const
{
return use_fixed_ripple_radius_;
}
void
FlatButton::setFixedRippleRadius(qreal radius)
{
fixed_ripple_radius_ = radius;
setHasFixedRippleRadius(true);
}
QSize
FlatButton::sizeHint() const
{
ensurePolished();
QSize label(fontMetrics().size(Qt::TextSingleLine, removeKDEAccelerators(text())));
int w = 20 + label.width();
int h = label.height();
if (!icon().isNull()) {
w += iconSize().width() + FlatButton::IconPadding;
h = qMax(h, iconSize().height());
}
return QSize(w, 20 + h);
}
void
FlatButton::checkStateSet()
{
state_machine_->updateCheckedStatus();
QPushButton::checkStateSet();
}
void
FlatButton::mousePressEvent(QMouseEvent *event)
{
if (ui::RippleStyle::NoRipple != ripple_style_) {
QPoint pos;
qreal radiusEndValue;
if (ui::RippleStyle::CenteredRipple == ripple_style_) {
pos = rect().center();
} else {
pos = event->pos();
}
if (use_fixed_ripple_radius_) {
radiusEndValue = fixed_ripple_radius_;
} else {
radiusEndValue = static_cast<qreal>(width()) / 2;
}
Ripple *ripple = new Ripple(pos);
ripple->setRadiusEndValue(radiusEndValue);
ripple->setOpacityStartValue(0.35);
ripple->setColor(foregroundColor());
ripple->radiusAnimation()->setDuration(250);
ripple->opacityAnimation()->setDuration(250);
ripple_overlay_->addRipple(ripple);
}
QPushButton::mousePressEvent(event);
}
void
FlatButton::mouseReleaseEvent(QMouseEvent *event)
{
QPushButton::mouseReleaseEvent(event);
state_machine_->updateCheckedStatus();
}
void
FlatButton::resizeEvent(QResizeEvent *event)
{
QPushButton::resizeEvent(event);
updateClipPath();
}
void
FlatButton::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event)
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
const qreal cr = corner_radius_;
if (cr > 0) {
QPainterPath path;
path.addRoundedRect(rect(), cr, cr);
painter.setClipPath(path);
painter.setClipping(true);
}
paintBackground(&painter);
painter.setOpacity(1);
painter.setClipping(false);
paintForeground(&painter);
}
void
FlatButton::paintBackground(QPainter *painter)
{
const qreal overlayOpacity = state_machine_->overlayOpacity();
const qreal checkedProgress = state_machine_->checkedOverlayProgress();
if (Qt::OpaqueMode == bg_mode_) {
QBrush brush;
brush.setStyle(Qt::SolidPattern);
if (isEnabled()) {
brush.setColor(backgroundColor());
} else {
brush.setColor(disabledBackgroundColor());
}
painter->setOpacity(1);
painter->setBrush(brush);
painter->setPen(Qt::NoPen);
painter->drawRect(rect());
}
QBrush brush;
brush.setStyle(Qt::SolidPattern);
painter->setPen(Qt::NoPen);
if (!isEnabled()) {
return;
}
if ((ui::OverlayStyle::NoOverlay != overlay_style_) && (overlayOpacity > 0)) {
if (ui::OverlayStyle::TintedOverlay == overlay_style_) {
brush.setColor(overlayColor());
} else {
brush.setColor(Qt::gray);
}
painter->setOpacity(overlayOpacity);
painter->setBrush(brush);
painter->drawRect(rect());
}
if (isCheckable() && checkedProgress > 0) {
const qreal q = Qt::TransparentMode == bg_mode_ ? 0.45 : 0.7;
brush.setColor(foregroundColor());
painter->setOpacity(q * checkedProgress);
painter->setBrush(brush);
QRect r(rect());
r.setHeight(static_cast<qreal>(r.height()) * checkedProgress);
painter->drawRect(r);
}
}
#define COLOR_INTERPOLATE(CH) (1 - progress) * source.CH() + progress *dest.CH()
void
FlatButton::paintForeground(QPainter *painter)
{
if (isEnabled()) {
painter->setPen(foregroundColor());
const qreal progress = state_machine_->checkedOverlayProgress();
if (isCheckable() && progress > 0) {
QColor source = foregroundColor();
QColor dest = Qt::TransparentMode == bg_mode_ ? Qt::white : backgroundColor();
if (qFuzzyCompare(1, progress)) {
painter->setPen(dest);
} else {
painter->setPen(QColor(COLOR_INTERPOLATE(red),
COLOR_INTERPOLATE(green),
COLOR_INTERPOLATE(blue),
COLOR_INTERPOLATE(alpha)));
}
}
} else {
painter->setPen(disabledForegroundColor());
}
if (icon().isNull()) {
painter->drawText(rect(), Qt::AlignCenter, removeKDEAccelerators(text()));
return;
}
QSize textSize(fontMetrics().size(Qt::TextSingleLine, removeKDEAccelerators(text())));
QSize base(size() - textSize);
const int iw = iconSize().width() + IconPadding;
QPoint pos((base.width() - iw) / 2, 0);
QRect textGeometry(pos + QPoint(0, base.height() / 2), textSize);
QRect iconGeometry(pos + QPoint(0, (height() - iconSize().height()) / 2), iconSize());
/* if (ui::LeftIcon == icon_placement_) { */
/* textGeometry.translate(iw, 0); */
/* } else { */
/* iconGeometry.translate(textSize.width() + IconPadding, 0); */
/* } */
painter->drawText(textGeometry, Qt::AlignCenter, removeKDEAccelerators(text()));
QPixmap pixmap = icon().pixmap(iconSize());
QPainter icon(&pixmap);
icon.setCompositionMode(QPainter::CompositionMode_SourceIn);
icon.fillRect(pixmap.rect(), painter->pen().color());
painter->drawPixmap(iconGeometry, pixmap);
}
void
FlatButton::updateClipPath()
{
const qreal radius = corner_radius_;
QPainterPath path;
path.addRoundedRect(rect(), radius, radius);
ripple_overlay_->setClipPath(path);
}
FlatButtonStateMachine::FlatButtonStateMachine(FlatButton *parent)
: QStateMachine(parent)
, button_(parent)
, top_level_state_(new QState(QState::ParallelStates))
, config_state_(new QState(top_level_state_))
, checkable_state_(new QState(top_level_state_))
, checked_state_(new QState(checkable_state_))
, unchecked_state_(new QState(checkable_state_))
, neutral_state_(new QState(config_state_))
, neutral_focused_state_(new QState(config_state_))
, hovered_state_(new QState(config_state_))
, hovered_focused_state_(new QState(config_state_))
, pressed_state_(new QState(config_state_))
, overlay_opacity_(0)
, checked_overlay_progress_(parent->isChecked() ? 1 : 0)
, was_checked_(false)
{
Q_ASSERT(parent);
parent->installEventFilter(this);
config_state_->setInitialState(neutral_state_);
addState(top_level_state_);
setInitialState(top_level_state_);
checkable_state_->setInitialState(parent->isChecked() ? checked_state_ : unchecked_state_);
QSignalTransition *transition;
QPropertyAnimation *animation;
transition = new QSignalTransition(this, SIGNAL(buttonChecked()));
transition->setTargetState(checked_state_);
unchecked_state_->addTransition(transition);
animation = new QPropertyAnimation(this, "checkedOverlayProgress", this);
animation->setDuration(200);
transition->addAnimation(animation);
transition = new QSignalTransition(this, SIGNAL(buttonUnchecked()));
transition->setTargetState(unchecked_state_);
checked_state_->addTransition(transition);
animation = new QPropertyAnimation(this, "checkedOverlayProgress", this);
animation->setDuration(200);
transition->addAnimation(animation);
addTransition(button_, QEvent::FocusIn, neutral_state_, neutral_focused_state_);
addTransition(button_, QEvent::FocusOut, neutral_focused_state_, neutral_state_);
addTransition(button_, QEvent::Enter, neutral_state_, hovered_state_);
addTransition(button_, QEvent::Leave, hovered_state_, neutral_state_);
addTransition(button_, QEvent::Enter, neutral_focused_state_, hovered_focused_state_);
addTransition(button_, QEvent::Leave, hovered_focused_state_, neutral_focused_state_);
addTransition(button_, QEvent::FocusIn, hovered_state_, hovered_focused_state_);
addTransition(button_, QEvent::FocusOut, hovered_focused_state_, hovered_state_);
addTransition(this, SIGNAL(buttonPressed()), hovered_state_, pressed_state_);
addTransition(button_, QEvent::Leave, pressed_state_, neutral_focused_state_);
addTransition(button_, QEvent::FocusOut, pressed_state_, hovered_state_);
}
void
FlatButtonStateMachine::setOverlayOpacity(qreal opacity)
{
overlay_opacity_ = opacity;
emit overlayOpacityChanged();
button_->update();
}
void
FlatButtonStateMachine::setCheckedOverlayProgress(qreal opacity)
{
checked_overlay_progress_ = opacity;
emit checkedOverlayProgressChanged();
button_->update();
}
void
FlatButtonStateMachine::startAnimations()
{
start();
}
void
FlatButtonStateMachine::setupProperties()
{
QColor overlayColor;
if (Qt::TransparentMode == button_->backgroundMode()) {
overlayColor = button_->backgroundColor();
} else {
overlayColor = button_->foregroundColor();
}
const qreal baseOpacity = button_->baseOpacity();
neutral_state_->assignProperty(this, "overlayOpacity", 0);
neutral_focused_state_->assignProperty(this, "overlayOpacity", 0);
hovered_state_->assignProperty(this, "overlayOpacity", baseOpacity);
hovered_focused_state_->assignProperty(this, "overlayOpacity", baseOpacity);
pressed_state_->assignProperty(this, "overlayOpacity", baseOpacity);
checked_state_->assignProperty(this, "checkedOverlayProgress", 1);
unchecked_state_->assignProperty(this, "checkedOverlayProgress", 0);
button_->update();
}
void
FlatButtonStateMachine::updateCheckedStatus()
{
const bool checked = button_->isChecked();
if (was_checked_ != checked) {
was_checked_ = checked;
if (checked) {
emit buttonChecked();
} else {
emit buttonUnchecked();
}
}
}
bool
FlatButtonStateMachine::eventFilter(QObject *watched, QEvent *event)
{
if (QEvent::FocusIn == event->type()) {
QFocusEvent *focusEvent = static_cast<QFocusEvent *>(event);
if (focusEvent && Qt::MouseFocusReason == focusEvent->reason()) {
emit buttonPressed();
return true;
}
}
return QStateMachine::eventFilter(watched, event);
}
void
FlatButtonStateMachine::addTransition(QObject *object,
const char *signal,
QState *fromState,
QState *toState)
{
addTransition(new QSignalTransition(object, signal), fromState, toState);
}
void
FlatButtonStateMachine::addTransition(QObject *object,
QEvent::Type eventType,
QState *fromState,
QState *toState)
{
addTransition(new QEventTransition(object, eventType), fromState, toState);
}
void
FlatButtonStateMachine::addTransition(QAbstractTransition *transition,
QState *fromState,
QState *toState)
{
transition->setTargetState(toState);
QPropertyAnimation *animation;
animation = new QPropertyAnimation(this, "overlayOpacity", this);
animation->setDuration(150);
transition->addAnimation(animation);
fromState->addTransition(transition);
}

View File

@ -1,198 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QPushButton>
#include <QStateMachine>
#include "Theme.h"
class RippleOverlay;
class FlatButton;
class FlatButtonStateMachine : public QStateMachine
{
Q_OBJECT
Q_PROPERTY(
qreal overlayOpacity WRITE setOverlayOpacity READ overlayOpacity NOTIFY overlayOpacityChanged)
Q_PROPERTY(qreal checkedOverlayProgress WRITE setCheckedOverlayProgress READ
checkedOverlayProgress NOTIFY checkedOverlayProgressChanged)
public:
explicit FlatButtonStateMachine(FlatButton *parent);
void setOverlayOpacity(qreal opacity);
void setCheckedOverlayProgress(qreal opacity);
inline qreal overlayOpacity() const;
inline qreal checkedOverlayProgress() const;
void startAnimations();
void setupProperties();
void updateCheckedStatus();
signals:
void buttonPressed();
void buttonChecked();
void buttonUnchecked();
void overlayOpacityChanged();
void checkedOverlayProgressChanged();
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
private:
void addTransition(QObject *object, const char *signal, QState *fromState, QState *toState);
void addTransition(QObject *object, QEvent::Type eventType, QState *fromState, QState *toState);
void addTransition(QAbstractTransition *transition, QState *fromState, QState *toState);
FlatButton *const button_;
QState *const top_level_state_;
QState *const config_state_;
QState *const checkable_state_;
QState *const checked_state_;
QState *const unchecked_state_;
QState *const neutral_state_;
QState *const neutral_focused_state_;
QState *const hovered_state_;
QState *const hovered_focused_state_;
QState *const pressed_state_;
qreal overlay_opacity_;
qreal checked_overlay_progress_;
bool was_checked_;
};
inline qreal
FlatButtonStateMachine::overlayOpacity() const
{
return overlay_opacity_;
}
inline qreal
FlatButtonStateMachine::checkedOverlayProgress() const
{
return checked_overlay_progress_;
}
class FlatButton : public QPushButton
{
Q_OBJECT
Q_PROPERTY(QColor foregroundColor WRITE setForegroundColor READ foregroundColor NOTIFY
foregroundColorChanged)
Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor NOTIFY
backgroundColorChanged)
Q_PROPERTY(
QColor overlayColor WRITE setOverlayColor READ overlayColor NOTIFY overlayColorChanged)
Q_PROPERTY(QColor disabledForegroundColor WRITE setDisabledForegroundColor READ
disabledForegroundColor NOTIFY disabledForegroundColorChanged)
Q_PROPERTY(QColor disabledBackgroundColor WRITE setDisabledBackgroundColor READ
disabledBackgroundColor NOTIFY disabledBackgroundColorChanged)
Q_PROPERTY(qreal fontSize WRITE setFontSize READ fontSize NOTIFY fontSizeChanged)
public:
explicit FlatButton(QWidget *parent = nullptr,
ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset);
explicit FlatButton(const QString &text,
QWidget *parent = nullptr,
ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset);
FlatButton(const QString &text,
ui::Role role,
QWidget *parent = nullptr,
ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset);
void applyPreset(ui::ButtonPreset preset);
void setBackgroundColor(const QColor &color);
void setBackgroundMode(Qt::BGMode mode);
void setBaseOpacity(qreal opacity);
void setCheckable(bool value);
void setCornerRadius(qreal radius);
void setDisabledBackgroundColor(const QColor &color);
void setDisabledForegroundColor(const QColor &color);
void setFixedRippleRadius(qreal radius);
void setFontSize(qreal size);
void setForegroundColor(const QColor &color);
void setHasFixedRippleRadius(bool value);
void setIconPlacement(ui::ButtonIconPlacement placement);
void setOverlayColor(const QColor &color);
void setOverlayStyle(ui::OverlayStyle style);
void setRippleStyle(ui::RippleStyle style);
void setRole(ui::Role role);
QColor foregroundColor() const;
QColor backgroundColor() const;
QColor overlayColor() const;
QColor disabledForegroundColor() const;
QColor disabledBackgroundColor() const;
qreal fontSize() const;
qreal cornerRadius() const;
qreal baseOpacity() const;
bool hasFixedRippleRadius() const;
ui::Role role() const;
ui::OverlayStyle overlayStyle() const;
ui::RippleStyle rippleStyle() const;
ui::ButtonIconPlacement iconPlacement() const;
Qt::BGMode backgroundMode() const;
QSize sizeHint() const override;
protected:
int IconPadding = 0;
void checkStateSet() override;
void mousePressEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
void paintEvent(QPaintEvent *event) override;
virtual void paintBackground(QPainter *painter);
virtual void paintForeground(QPainter *painter);
virtual void updateClipPath();
void init();
signals:
void foregroundColorChanged();
void backgroundColorChanged();
void overlayColorChanged();
void disabledForegroundColorChanged();
void disabledBackgroundColorChanged();
void fontSizeChanged();
private:
RippleOverlay *ripple_overlay_;
FlatButtonStateMachine *state_machine_;
ui::Role role_;
ui::RippleStyle ripple_style_;
ui::ButtonIconPlacement icon_placement_;
ui::OverlayStyle overlay_style_;
Qt::BGMode bg_mode_;
QColor background_color_;
QColor foreground_color_;
QColor overlay_color_;
QColor disabled_color_;
QColor disabled_background_color_;
qreal fixed_ripple_radius_;
qreal corner_radius_;
qreal base_opacity_;
qreal font_size_;
bool use_fixed_ripple_radius_;
};

View File

@ -1,33 +0,0 @@
// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr>
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "Label.h"
#include <QMouseEvent>
Label::Label(QWidget *parent, Qt::WindowFlags f)
: QLabel(parent, f)
{}
Label::Label(const QString &text, QWidget *parent, Qt::WindowFlags f)
: QLabel(text, parent, f)
{}
void
Label::mousePressEvent(QMouseEvent *e)
{
pressPosition_ = e->pos();
emit pressed(e);
QLabel::mousePressEvent(e);
}
void
Label::mouseReleaseEvent(QMouseEvent *e)
{
emit released(e);
if (pressPosition_ == e->pos())
emit clicked(e);
QLabel::mouseReleaseEvent(e);
}

View File

@ -1,30 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QLabel>
class Label : public QLabel
{
Q_OBJECT
public:
explicit Label(QWidget *parent = Q_NULLPTR, Qt::WindowFlags f = Qt::WindowFlags());
explicit Label(const QString &text,
QWidget *parent = Q_NULLPTR,
Qt::WindowFlags f = Qt::WindowFlags());
signals:
void clicked(QMouseEvent *e);
void pressed(QMouseEvent *e);
void released(QMouseEvent *e);
protected:
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
QPoint pressPosition_;
};

View File

@ -1,84 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "LoadingIndicator.h"
#include <QPaintEvent>
#include <QPainter>
#include <QTimer>
LoadingIndicator::LoadingIndicator(QWidget *parent)
: QWidget(parent)
, interval_(70)
, angle_(0)
, color_(Qt::black)
{
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
setFocusPolicy(Qt::NoFocus);
timer_ = new QTimer(this);
connect(timer_, SIGNAL(timeout()), this, SLOT(onTimeout()));
}
void
LoadingIndicator::paintEvent(QPaintEvent *e)
{
Q_UNUSED(e)
if (!timer_->isActive())
return;
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
int width = qMin(this->width(), this->height());
int outerRadius = (width - 4) * 0.5f;
int innerRadius = outerRadius * 0.78f;
int capsuleRadius = (outerRadius - innerRadius) / 2;
for (int i = 0; i < 8; ++i) {
QColor color = color_;
color.setAlphaF(1.0f - (i / 8.0f));
painter.setPen(Qt::NoPen);
painter.setBrush(color);
qreal radius = capsuleRadius * (1.0f - (i / 16.0f));
painter.save();
painter.translate(rect().center());
painter.rotate(angle_ - i * 45.0f);
QPointF center = QPointF(-capsuleRadius, -innerRadius);
painter.drawEllipse(center, radius * 2, radius * 2);
painter.restore();
}
}
void
LoadingIndicator::start()
{
timer_->start(interval_);
show();
}
void
LoadingIndicator::stop()
{
timer_->stop();
hide();
}
void
LoadingIndicator::onTimeout()
{
angle_ = (angle_ + 45) % 360;
repaint();
}

View File

@ -1,49 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QColor>
#include <QWidget>
class QPainter;
class QTimer;
class QPaintEvent;
class LoadingIndicator : public QWidget
{
Q_OBJECT
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
public:
LoadingIndicator(QWidget *parent = nullptr);
void paintEvent(QPaintEvent *e) override;
void start();
void stop();
QColor color() { return color_; }
void setColor(QColor color)
{
color_ = color;
emit colorChanged();
}
int interval() { return interval_; }
void setInterval(int interval) { interval_ = interval; }
private slots:
void onTimeout();
signals:
void colorChanged();
private:
int interval_;
int angle_;
QColor color_;
QTimer *timer_;
};

View File

@ -1,26 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QMenu>
#include "Config.h"
class Menu : public QMenu
{
Q_OBJECT
public:
Menu(QWidget *parent = nullptr)
: QMenu(parent){};
protected:
void leaveEvent(QEvent *e) override
{
hide();
QMenu::leaveEvent(e);
}
};

View File

@ -139,5 +139,5 @@ Nheko::openCreateRoomDialog() const
void
Nheko::reparent(QWindow *win) const
{
win->setTransientParent(MainWindow::instance()->windowHandle());
win->setTransientParent(MainWindow::instance());
}

View File

@ -1,62 +0,0 @@
// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr>
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QPainter>
#include <QVBoxLayout>
#include "OverlayModal.h"
OverlayModal::OverlayModal(QWidget *parent)
: OverlayWidget(parent)
, color_{QColor(30, 30, 30, 170)}
{
layout_ = new QVBoxLayout(this);
layout_->setSpacing(0);
layout_->setContentsMargins(10, 40, 10, 20);
setContentAlignment(Qt::AlignCenter);
}
void
OverlayModal::setWidget(QWidget *widget)
{
// Delete the previous widget
if (layout_->count() > 0) {
QLayoutItem *item;
while ((item = layout_->takeAt(0)) != nullptr) {
delete item->widget();
delete item;
}
}
layout_->addWidget(widget);
content_ = widget;
content_->setFocus();
}
void
OverlayModal::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
QPainter painter(this);
painter.fillRect(rect(), color_);
}
void
OverlayModal::mousePressEvent(QMouseEvent *e)
{
if (isDismissible_ && content_ && !content_->geometry().contains(e->pos()))
hide();
}
void
OverlayModal::keyPressEvent(QKeyEvent *event)
{
if (event->key() == Qt::Key_Escape) {
event->accept();
hide();
}
}

View File

@ -1,40 +0,0 @@
// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr>
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QKeyEvent>
#include <QMouseEvent>
#include <QPaintEvent>
#include <QVBoxLayout>
#include "OverlayWidget.h"
class OverlayModal : public OverlayWidget
{
public:
OverlayModal(QWidget *parent);
void setColor(QColor color) { color_ = color; }
void setDismissible(bool state) { isDismissible_ = state; }
void setContentAlignment(QFlags<Qt::AlignmentFlag> flag) { layout_->setAlignment(flag); }
void setWidget(QWidget *widget);
protected:
void paintEvent(QPaintEvent *event) override;
void keyPressEvent(QKeyEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
private:
QWidget *content_;
QVBoxLayout *layout_;
QColor color_;
//! Decides whether or not the modal can be removed by clicking into it.
bool isDismissible_ = true;
};

View File

@ -1,79 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "OverlayWidget.h"
#include <QPainter>
#include <QStyleOption>
OverlayWidget::OverlayWidget(QWidget *parent)
: QWidget(parent)
{
if (parent) {
parent->installEventFilter(this);
setGeometry(overlayGeometry());
raise();
}
}
bool
OverlayWidget::event(QEvent *event)
{
if (!parent())
return QWidget::event(event);
switch (event->type()) {
case QEvent::ParentChange: {
parent()->installEventFilter(this);
setGeometry(overlayGeometry());
break;
}
case QEvent::ParentAboutToChange: {
parent()->removeEventFilter(this);
break;
}
default:
break;
}
return QWidget::event(event);
}
bool
OverlayWidget::eventFilter(QObject *obj, QEvent *event)
{
switch (event->type()) {
case QEvent::Move:
case QEvent::Resize:
setGeometry(overlayGeometry());
break;
default:
break;
}
return QWidget::eventFilter(obj, event);
}
QRect
OverlayWidget::overlayGeometry() const
{
QWidget *widget = parentWidget();
if (!widget)
return QRect();
return widget->rect();
}
void
OverlayWidget::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
QStyleOption opt;
opt.initFrom(this);
QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
}

View File

@ -1,26 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QEvent>
#include <QWidget>
class QPainter;
class OverlayWidget : public QWidget
{
Q_OBJECT
public:
explicit OverlayWidget(QWidget *parent = nullptr);
protected:
bool event(QEvent *event) override;
bool eventFilter(QObject *obj, QEvent *event) override;
QRect overlayGeometry() const;
void paintEvent(QPaintEvent *event) override;
};

View File

@ -1,154 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QFontMetrics>
#include <QPaintDevice>
#include <QPainter>
#include <QPainterPath>
#include <QtGlobal>
class Painter : public QPainter
{
public:
explicit Painter(QPaintDevice *device)
: QPainter(device)
{}
void drawTextLeft(int x, int y, const QString &text)
{
QFontMetrics m(fontMetrics());
drawText(x, y + m.ascent(), text);
}
void drawTextRight(int x, int y, int outerw, const QString &text, int textWidth = -1)
{
QFontMetrics m(fontMetrics());
if (textWidth < 0) {
textWidth = m.horizontalAdvance(text);
}
drawText((outerw - x - textWidth), y + m.ascent(), text);
}
void drawPixmapLeft(int x, int y, const QPixmap &pix, const QRect &from)
{
drawPixmap(QPoint(x, y), pix, from);
}
void drawPixmapLeft(const QPoint &p, const QPixmap &pix, const QRect &from)
{
return drawPixmapLeft(p.x(), p.y(), pix, from);
}
void drawPixmapLeft(int x, int y, int w, int h, const QPixmap &pix, const QRect &from)
{
drawPixmap(QRect(x, y, w, h), pix, from);
}
void drawPixmapLeft(const QRect &r, const QPixmap &pix, const QRect &from)
{
return drawPixmapLeft(r.x(), r.y(), r.width(), r.height(), pix, from);
}
void drawPixmapLeft(int x, int y, int outerw, const QPixmap &pix)
{
Q_UNUSED(outerw);
drawPixmap(QPoint(x, y), pix);
}
void drawPixmapLeft(const QPoint &p, int outerw, const QPixmap &pix)
{
return drawPixmapLeft(p.x(), p.y(), outerw, pix);
}
void drawPixmapRight(int x, int y, int outerw, const QPixmap &pix, const QRect &from)
{
drawPixmap(QPoint((outerw - x - (from.width() / pix.devicePixelRatio())), y), pix, from);
}
void drawPixmapRight(const QPoint &p, int outerw, const QPixmap &pix, const QRect &from)
{
return drawPixmapRight(p.x(), p.y(), outerw, pix, from);
}
void
drawPixmapRight(int x, int y, int w, int h, int outerw, const QPixmap &pix, const QRect &from)
{
drawPixmap(QRect((outerw - x - w), y, w, h), pix, from);
}
void drawPixmapRight(const QRect &r, int outerw, const QPixmap &pix, const QRect &from)
{
return drawPixmapRight(r.x(), r.y(), r.width(), r.height(), outerw, pix, from);
}
void drawPixmapRight(int x, int y, int outerw, const QPixmap &pix)
{
drawPixmap(QPoint((outerw - x - (pix.width() / pix.devicePixelRatio())), y), pix);
}
void drawPixmapRight(const QPoint &p, int outerw, const QPixmap &pix)
{
return drawPixmapRight(p.x(), p.y(), outerw, pix);
}
void drawAvatar(const QPixmap &pix, int w, int h, int d)
{
QPainterPath pp;
pp.addEllipse((w - d) / 2, (h - d) / 2, d, d);
QRect region((w - d) / 2, (h - d) / 2, d, d);
setClipPath(pp);
drawPixmap(region, pix);
}
void drawLetterAvatar(const QString &c,
const QColor &penColor,
const QColor &brushColor,
int w,
int h,
int d)
{
QRect region((w - d) / 2, (h - d) / 2, d, d);
setPen(Qt::NoPen);
setBrush(brushColor);
drawEllipse(region.center(), d / 2, d / 2);
setBrush(Qt::NoBrush);
drawEllipse(region.center(), d / 2, d / 2);
setPen(penColor);
drawText(region.translated(0, -1), Qt::AlignCenter, c);
}
};
class PainterHighQualityEnabler
{
public:
PainterHighQualityEnabler(Painter &p)
: _painter(p)
{
hints_ =
QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing;
_painter.setRenderHints(hints_);
}
~PainterHighQualityEnabler()
{
if (hints_)
_painter.setRenderHints(hints_, false);
}
PainterHighQualityEnabler(const PainterHighQualityEnabler &other) = delete;
PainterHighQualityEnabler &operator=(const PainterHighQualityEnabler &other) = delete;
private:
Painter &_painter;
QPainter::RenderHints hints_ = {};
};

View File

@ -1,92 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QEventTransition>
#include <QPropertyAnimation>
#include "RaisedButton.h"
void
RaisedButton::init()
{
shadow_state_machine_ = new QStateMachine(this);
normal_state_ = new QState;
pressed_state_ = new QState;
effect_ = new QGraphicsDropShadowEffect;
effect_->setBlurRadius(7);
effect_->setOffset(QPointF(0, 2));
effect_->setColor(QColor(0, 0, 0, 75));
setBackgroundMode(Qt::OpaqueMode);
setMinimumHeight(42);
setGraphicsEffect(effect_);
setBaseOpacity(0.3);
shadow_state_machine_->addState(normal_state_);
shadow_state_machine_->addState(pressed_state_);
normal_state_->assignProperty(effect_, "offset", QPointF(0, 2));
normal_state_->assignProperty(effect_, "blurRadius", 7);
pressed_state_->assignProperty(effect_, "offset", QPointF(0, 5));
pressed_state_->assignProperty(effect_, "blurRadius", 29);
QAbstractTransition *transition;
transition = new QEventTransition(this, QEvent::MouseButtonPress);
transition->setTargetState(pressed_state_);
normal_state_->addTransition(transition);
transition = new QEventTransition(this, QEvent::MouseButtonDblClick);
transition->setTargetState(pressed_state_);
normal_state_->addTransition(transition);
transition = new QEventTransition(this, QEvent::MouseButtonRelease);
transition->setTargetState(normal_state_);
pressed_state_->addTransition(transition);
QPropertyAnimation *animation;
animation = new QPropertyAnimation(effect_, "offset", this);
animation->setDuration(100);
shadow_state_machine_->addDefaultAnimation(animation);
animation = new QPropertyAnimation(effect_, "blurRadius", this);
animation->setDuration(100);
shadow_state_machine_->addDefaultAnimation(animation);
shadow_state_machine_->setInitialState(normal_state_);
shadow_state_machine_->start();
}
RaisedButton::RaisedButton(QWidget *parent)
: FlatButton(parent)
{
init();
}
RaisedButton::RaisedButton(const QString &text, QWidget *parent)
: FlatButton(parent)
{
init();
setText(text);
}
bool
RaisedButton::event(QEvent *event)
{
if (QEvent::EnabledChange == event->type()) {
if (isEnabled()) {
shadow_state_machine_->start();
effect_->setEnabled(true);
} else {
shadow_state_machine_->stop();
effect_->setEnabled(false);
}
}
return FlatButton::event(event);
}

View File

@ -1,32 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QGraphicsDropShadowEffect>
#include <QState>
#include <QStateMachine>
#include "FlatButton.h"
class RaisedButton : public FlatButton
{
Q_OBJECT
public:
explicit RaisedButton(QWidget *parent = nullptr);
explicit RaisedButton(const QString &text, QWidget *parent = nullptr);
protected:
bool event(QEvent *event) override;
private:
void init();
QStateMachine *shadow_state_machine_;
QState *normal_state_;
QState *pressed_state_;
QGraphicsDropShadowEffect *effect_;
};

View File

@ -1,116 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "Ripple.h"
#include "RippleOverlay.h"
Ripple::Ripple(const QPoint &center, QObject *parent)
: QParallelAnimationGroup(parent)
, overlay_(nullptr)
, radius_anim_(animate("radius"))
, opacity_anim_(animate("opacity"))
, radius_(0)
, opacity_(0)
, center_(center)
{
init();
}
Ripple::Ripple(const QPoint &center, RippleOverlay *overlay, QObject *parent)
: QParallelAnimationGroup(parent)
, overlay_(overlay)
, radius_anim_(animate("radius"))
, opacity_anim_(animate("opacity"))
, radius_(0)
, opacity_(0)
, center_(center)
{
init();
}
void
Ripple::setRadius(qreal radius)
{
Q_ASSERT(overlay_);
if (radius_ == radius)
return;
radius_ = radius;
overlay_->update();
emit radiusChanged();
}
void
Ripple::setOpacity(qreal opacity)
{
Q_ASSERT(overlay_);
if (opacity_ == opacity)
return;
opacity_ = opacity;
overlay_->update();
emit opacityChanged();
}
void
Ripple::setColor(const QColor &color)
{
if (brush_.color() == color)
return;
brush_.setColor(color);
if (overlay_)
overlay_->update();
}
void
Ripple::setBrush(const QBrush &brush)
{
brush_ = brush;
if (overlay_)
overlay_->update();
}
void
Ripple::destroy()
{
Q_ASSERT(overlay_);
overlay_->removeRipple(this);
}
QPropertyAnimation *
Ripple::animate(const QByteArray &property, const QEasingCurve &easing, int duration)
{
QPropertyAnimation *animation = new QPropertyAnimation;
animation->setTargetObject(this);
animation->setPropertyName(property);
animation->setEasingCurve(easing);
animation->setDuration(duration);
addAnimation(animation);
return animation;
}
void
Ripple::init()
{
setOpacityStartValue(0.5);
setOpacityEndValue(0);
setRadiusStartValue(0);
setRadiusEndValue(300);
brush_.setColor(Qt::black);
brush_.setStyle(Qt::SolidPattern);
connect(this, SIGNAL(finished()), this, SLOT(destroy()));
}

View File

@ -1,154 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QBrush>
#include <QEasingCurve>
#include <QParallelAnimationGroup>
#include <QPoint>
#include <QPropertyAnimation>
class RippleOverlay;
class Ripple : public QParallelAnimationGroup
{
Q_OBJECT
Q_PROPERTY(qreal radius WRITE setRadius READ radius NOTIFY radiusChanged)
Q_PROPERTY(qreal opacity WRITE setOpacity READ opacity NOTIFY opacityChanged)
public:
explicit Ripple(const QPoint &center, QObject *parent = nullptr);
Ripple(const QPoint &center, RippleOverlay *overlay, QObject *parent = nullptr);
inline void setOverlay(RippleOverlay *overlay);
void setRadius(qreal radius);
void setOpacity(qreal opacity);
void setColor(const QColor &color);
void setBrush(const QBrush &brush);
inline qreal radius() const;
inline qreal opacity() const;
inline QColor color() const;
inline QBrush brush() const;
inline QPoint center() const;
inline QPropertyAnimation *radiusAnimation() const;
inline QPropertyAnimation *opacityAnimation() const;
inline void setOpacityStartValue(qreal value);
inline void setOpacityEndValue(qreal value);
inline void setRadiusStartValue(qreal value);
inline void setRadiusEndValue(qreal value);
inline void setDuration(int msecs);
protected slots:
void destroy();
signals:
void radiusChanged();
void opacityChanged();
private:
Q_DISABLE_COPY(Ripple)
QPropertyAnimation *animate(const QByteArray &property,
const QEasingCurve &easing = QEasingCurve::OutQuad,
int duration = 800);
void init();
RippleOverlay *overlay_;
QPropertyAnimation *const radius_anim_;
QPropertyAnimation *const opacity_anim_;
qreal radius_;
qreal opacity_;
QPoint center_;
QBrush brush_;
};
inline void
Ripple::setOverlay(RippleOverlay *overlay)
{
overlay_ = overlay;
}
inline qreal
Ripple::radius() const
{
return radius_;
}
inline qreal
Ripple::opacity() const
{
return opacity_;
}
inline QColor
Ripple::color() const
{
return brush_.color();
}
inline QBrush
Ripple::brush() const
{
return brush_;
}
inline QPoint
Ripple::center() const
{
return center_;
}
inline QPropertyAnimation *
Ripple::radiusAnimation() const
{
return radius_anim_;
}
inline QPropertyAnimation *
Ripple::opacityAnimation() const
{
return opacity_anim_;
}
inline void
Ripple::setOpacityStartValue(qreal value)
{
opacity_anim_->setStartValue(value);
}
inline void
Ripple::setOpacityEndValue(qreal value)
{
opacity_anim_->setEndValue(value);
}
inline void
Ripple::setRadiusStartValue(qreal value)
{
radius_anim_->setStartValue(value);
}
inline void
Ripple::setRadiusEndValue(qreal value)
{
radius_anim_->setEndValue(value);
}
inline void
Ripple::setDuration(int msecs)
{
radius_anim_->setDuration(msecs);
opacity_anim_->setDuration(msecs);
}

View File

@ -1,67 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QPainter>
#include "Ripple.h"
#include "RippleOverlay.h"
RippleOverlay::RippleOverlay(QWidget *parent)
: OverlayWidget(parent)
, use_clip_(false)
{
setAttribute(Qt::WA_TransparentForMouseEvents);
setAttribute(Qt::WA_NoSystemBackground);
}
void
RippleOverlay::addRipple(Ripple *ripple)
{
ripple->setOverlay(this);
ripples_.push_back(ripple);
ripple->start();
}
void
RippleOverlay::addRipple(const QPoint &position, qreal radius)
{
Ripple *ripple = new Ripple(position);
ripple->setRadiusEndValue(radius);
addRipple(ripple);
}
void
RippleOverlay::removeRipple(Ripple *ripple)
{
if (ripples_.removeOne(ripple))
delete ripple;
}
void
RippleOverlay::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event)
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.setPen(Qt::NoPen);
if (use_clip_)
painter.setClipPath(clip_path_);
for (auto it = ripples_.constBegin(); it != ripples_.constEnd(); ++it)
paintRipple(&painter, *it);
}
void
RippleOverlay::paintRipple(QPainter *painter, Ripple *ripple)
{
const qreal radius = ripple->radius();
const QPointF center = ripple->center();
painter->setOpacity(ripple->opacity());
painter->setBrush(ripple->brush());
painter->drawEllipse(center, radius, radius);
}

View File

@ -1,62 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QPainterPath>
#include "OverlayWidget.h"
class Ripple;
class RippleOverlay : public OverlayWidget
{
Q_OBJECT
public:
explicit RippleOverlay(QWidget *parent = nullptr);
void addRipple(Ripple *ripple);
void addRipple(const QPoint &position, qreal radius = 300);
void removeRipple(Ripple *ripple);
inline void setClipping(bool enable);
inline bool hasClipping() const;
inline void setClipPath(const QPainterPath &path);
protected:
void paintEvent(QPaintEvent *event) Q_DECL_OVERRIDE;
private:
Q_DISABLE_COPY(RippleOverlay)
void paintRipple(QPainter *painter, Ripple *ripple);
QList<Ripple *> ripples_;
QPainterPath clip_path_;
bool use_clip_;
};
inline void
RippleOverlay::setClipping(bool enable)
{
use_clip_ = enable;
update();
}
inline bool
RippleOverlay::hasClipping() const
{
return use_clip_;
}
inline void
RippleOverlay::setClipPath(const QPainterPath &path)
{
clip_path_ = path;
update();
}

View File

@ -1,136 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QPainter>
#include "SnackBar.h"
constexpr int STARTING_OFFSET = 1;
constexpr int BOX_PADDING = 10;
constexpr double MIN_WIDTH = 400.0;
constexpr double MIN_WIDTH_PERCENTAGE = 0.3;
SnackBar::SnackBar(QWidget *parent)
: OverlayWidget(parent)
, offset_anim(this, "offset", this)
{
QFont font;
font.setPointSizeF(font.pointSizeF() * 1.2);
font.setWeight(QFont::Weight::Thin);
setFont(font);
boxHeight_ = QFontMetrics(font).height() * 2;
offset_ = STARTING_OFFSET;
position_ = SnackBarPosition::Top;
hideTimer_.setSingleShot(true);
offset_anim.setStartValue(1.0);
offset_anim.setEndValue(0.0);
offset_anim.setDuration(100);
offset_anim.setEasingCurve(QEasingCurve::OutCubic);
connect(this, &SnackBar::offsetChanged, this, [this]() mutable { repaint(); });
connect(
&offset_anim, &QPropertyAnimation::finished, this, [this]() { hideTimer_.start(10000); });
connect(&hideTimer_, SIGNAL(timeout()), this, SLOT(hideMessage()));
hide();
}
void
SnackBar::start()
{
if (messages_.empty())
return;
show();
raise();
offset_anim.start();
}
void
SnackBar::hideMessage()
{
stopTimers();
hide();
if (!messages_.empty())
// Moving on to the next message.
messages_.pop_front();
// Resetting the starting position of the widget.
offset_ = STARTING_OFFSET;
if (!messages_.empty())
start();
}
void
SnackBar::stopTimers()
{
hideTimer_.stop();
}
void
SnackBar::showMessage(const QString &msg)
{
messages_.push_back(msg);
// There is already an active message.
if (isVisible())
return;
start();
}
void
SnackBar::mousePressEvent(QMouseEvent *)
{
hideMessage();
}
void
SnackBar::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event)
if (messages_.empty())
return;
auto message_ = messages_.front();
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
QBrush brush;
brush.setStyle(Qt::SolidPattern);
brush.setColor(bgColor_);
p.setBrush(brush);
QRect r(0, 0, std::max(MIN_WIDTH, width() * MIN_WIDTH_PERCENTAGE), boxHeight_);
p.setPen(Qt::white);
QRect br = p.boundingRect(r, Qt::AlignHCenter | Qt::AlignTop | Qt::TextWordWrap, message_);
p.setPen(Qt::NoPen);
r = br.united(r).adjusted(-BOX_PADDING, -BOX_PADDING, BOX_PADDING, BOX_PADDING);
const qreal s = 1 - offset_;
if (position_ == SnackBarPosition::Bottom)
p.translate((width() - (r.width() - 2 * BOX_PADDING)) / 2,
height() - BOX_PADDING - s * (r.height()));
else
p.translate((width() - (r.width() - 2 * BOX_PADDING)) / 2,
s * (r.height()) - 2 * BOX_PADDING);
br.moveCenter(r.center());
p.drawRoundedRect(r.adjusted(0, 0, 0, 4), 4, 4);
p.setPen(textColor_);
p.drawText(br, Qt::AlignHCenter | Qt::AlignTop | Qt::TextWordWrap, message_);
}

View File

@ -1,98 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QCoreApplication>
#include <QPaintEvent>
#include <QPropertyAnimation>
#include <QTimer>
#include <deque>
#include "OverlayWidget.h"
enum class SnackBarPosition
{
Bottom,
Top,
};
class SnackBar : public OverlayWidget
{
Q_OBJECT
Q_PROPERTY(
QColor bgColor READ backgroundColor WRITE setBackgroundColor NOTIFY backgroundColorChanged)
Q_PROPERTY(QColor textColor READ textColor WRITE setTextColor NOTIFY textColorChanged)
Q_PROPERTY(double offset READ offset WRITE setOffset NOTIFY offsetChanged)
public:
explicit SnackBar(QWidget *parent);
QColor backgroundColor() const { return bgColor_; }
void setBackgroundColor(const QColor &color)
{
bgColor_ = color;
update();
emit backgroundColorChanged();
}
QColor textColor() const { return textColor_; }
void setTextColor(const QColor &color)
{
textColor_ = color;
update();
emit textColorChanged();
}
void setPosition(SnackBarPosition pos)
{
position_ = pos;
update();
}
double offset() { return offset_; }
void setOffset(double offset)
{
if (offset != offset_) {
offset_ = offset;
emit offsetChanged();
}
}
public slots:
void showMessage(const QString &msg);
signals:
void offsetChanged();
void backgroundColorChanged();
void textColorChanged();
protected:
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
private slots:
void hideMessage();
private:
void stopTimers();
void start();
QColor bgColor_;
QColor textColor_;
qreal bgOpacity_;
qreal offset_;
std::deque<QString> messages_;
QTimer hideTimer_;
double boxHeight_;
QPropertyAnimation offset_anim;
SnackBarPosition position_;
};

View File

@ -1,123 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ui/TextLabel.h"
#include <QAbstractTextDocumentLayout>
#include <QDesktopServices>
#include <QEvent>
#include <QWheelEvent>
#include "Utils.h"
bool
ContextMenuFilter::eventFilter(QObject *obj, QEvent *event)
{
if (event->type() == QEvent::MouseButtonPress) {
emit contextMenuIsOpening();
return true;
}
return QObject::eventFilter(obj, event);
}
TextLabel::TextLabel(QWidget *parent)
: TextLabel(QString(), parent)
{}
TextLabel::TextLabel(const QString &text, QWidget *parent)
: QTextBrowser(parent)
{
document()->setDefaultStyleSheet(QStringLiteral("a {color: %1; }").arg(utils::linkColor()));
setText(text);
setOpenExternalLinks(true);
// Make it look and feel like an ordinary label.
setReadOnly(true);
setFrameStyle(QFrame::NoFrame);
QPalette pal = palette();
pal.setColor(QPalette::Base, Qt::transparent);
setPalette(pal);
// Wrap anywhere but prefer words, adjust minimum height on the fly.
setLineWrapMode(QTextEdit::WidgetWidth);
setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
connect(document()->documentLayout(),
&QAbstractTextDocumentLayout::documentSizeChanged,
this,
&TextLabel::adjustHeight);
document()->setDocumentMargin(0);
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
setFixedHeight(0);
connect(this, &TextLabel::linkActivated, this, &TextLabel::handleLinkActivation);
auto filter = new ContextMenuFilter(this);
installEventFilter(filter);
connect(filter, &ContextMenuFilter::contextMenuIsOpening, this, [this]() {
contextMenuRequested_ = true;
});
}
void
TextLabel::focusOutEvent(QFocusEvent *e)
{
QTextBrowser::focusOutEvent(e);
// We keep the selection available for the context menu.
if (contextMenuRequested_) {
contextMenuRequested_ = false;
return;
}
QTextCursor cursor = textCursor();
cursor.clearSelection();
setTextCursor(cursor);
}
void
TextLabel::mousePressEvent(QMouseEvent *e)
{
link_ = (e->button() & Qt::LeftButton) ? anchorAt(e->pos()) : QString();
QTextBrowser::mousePressEvent(e);
}
void
TextLabel::mouseReleaseEvent(QMouseEvent *e)
{
if (e->button() & Qt::LeftButton && !link_.isEmpty() && anchorAt(e->pos()) == link_) {
emit linkActivated(link_);
return;
}
QTextBrowser::mouseReleaseEvent(e);
}
void
TextLabel::wheelEvent(QWheelEvent *event)
{
event->ignore();
}
void
TextLabel::handleLinkActivation(const QUrl &url)
{
auto parts = url.toString().split('/');
auto defaultHandler = [](const QUrl &url) { QDesktopServices::openUrl(url); };
if (url.host() != QLatin1String("matrix.to") || parts.isEmpty())
return defaultHandler(url);
try {
using namespace mtx::identifiers;
parse<User>(parts.last().toStdString());
} catch (const std::exception &) {
return defaultHandler(url);
}
emit userProfileTriggered(parts.last());
}

View File

@ -1,60 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QSize>
#include <QString>
#include <QTextBrowser>
#include <QUrl>
class QMouseEvent;
class QFocusEvent;
class QWheelEvent;
class ContextMenuFilter : public QObject
{
Q_OBJECT
public:
explicit ContextMenuFilter(QWidget *parent)
: QObject(parent)
{}
signals:
void contextMenuIsOpening();
protected:
bool eventFilter(QObject *obj, QEvent *event) override;
};
class TextLabel : public QTextBrowser
{
Q_OBJECT
public:
TextLabel(const QString &text, QWidget *parent = nullptr);
TextLabel(QWidget *parent = nullptr);
void wheelEvent(QWheelEvent *event) override;
void clearLinks() { link_.clear(); }
protected:
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void focusOutEvent(QFocusEvent *e) override;
private slots:
void adjustHeight(const QSizeF &size) { setFixedHeight(size.height()); }
void handleLinkActivation(const QUrl &link);
signals:
void userProfileTriggered(const QString &user_id);
void linkActivated(const QUrl &link);
private:
QString link_;
bool contextMenuRequested_ = false;
};

View File

@ -13,7 +13,6 @@
#include <mtx/responses/common.hpp>
#include "Logging.h"
#include "MainWindow.h"
#include "dialogs/FallbackAuth.h"
#include "dialogs/ReCaptcha.h"
@ -71,7 +70,7 @@ UIA::genericHandler(QString context)
emit phoneNumber();
} else if (current_stage == mtx::user_interactive::auth_types::recaptcha) {
auto captchaDialog =
new dialogs::ReCaptcha(QString::fromStdString(u.session), MainWindow::instance());
new dialogs::ReCaptcha(QString::fromStdString(u.session), nullptr);
captchaDialog->setWindowTitle(context);
connect(
@ -95,7 +94,7 @@ UIA::genericHandler(QString context)
} else if (current_stage == mtx::user_interactive::auth_types::registration_token) {
bool ok;
QString token =
QInputDialog::getText(MainWindow::instance(),
QInputDialog::getText(nullptr,
context,
tr("Please enter a valid registration token."),
QLineEdit::Normal,
@ -113,7 +112,7 @@ UIA::genericHandler(QString context)
// use fallback
auto dialog = new dialogs::FallbackAuth(QString::fromStdString(current_stage),
QString::fromStdString(u.session),
MainWindow::instance());
nullptr);
dialog->setWindowTitle(context);
connect(dialog, &dialogs::FallbackAuth::confirmation, this, [h, u, dialog]() {