From e18188514aab93cdc71105c4ab44508de90aa5da Mon Sep 17 00:00:00 2001 From: Pascal Precht Date: Fri, 24 Jul 2020 13:27:26 +0200 Subject: [PATCH] feat(profile): implement contact management This introduces the ability to: - list search existing contacts - block contacts - unblock contacts - list blocked contacts - remove contacts - search and add contacts Closes #608 --- src/app/profile/core.nim | 10 +- src/app/profile/view.nim | 92 ++++++- src/app/profile/views/contact_list.nim | 6 +- src/status/contacts.nim | 9 +- ui/app/AppLayouts/Profile/ProfileLayout.qml | 4 +- .../Profile/Sections/Contacts/Contact.qml | 122 ++++++++-- .../Profile/Sections/Contacts/ContactList.qml | 3 + .../Profile/Sections/ContactsContainer.qml | 230 +++++++++++++----- ui/app/img/block-icon.svg | 3 + ui/app/img/dots-icon.svg | 5 + ui/app/img/remove-contact.svg | 5 + ui/nim-status-client.pro | 1 + ui/shared/AddButton.qml | 79 +----- ui/shared/IconButton.qml | 83 +++++++ ui/shared/Input.qml | 2 + ui/shared/PopupMenu.qml | 4 + 16 files changed, 490 insertions(+), 168 deletions(-) create mode 100644 ui/app/img/block-icon.svg create mode 100644 ui/app/img/dots-icon.svg create mode 100644 ui/app/img/remove-contact.svg create mode 100644 ui/shared/IconButton.qml diff --git a/src/app/profile/core.nim b/src/app/profile/core.nim index 72cd10deec..74ea6b4c0e 100644 --- a/src/app/profile/core.nim +++ b/src/app/profile/core.nim @@ -1,4 +1,4 @@ -import NimQml, json, eventemitter, strutils +import NimQml, json, eventemitter, strutils, sugar, sequtils import json_serialization import ../../status/libstatus/mailservers as status_mailservers import ../../signals/types @@ -62,6 +62,14 @@ proc init*(self: ProfileController, account: Account) = let contacts = self.status.contacts.getContacts() self.view.setContactList(contacts) + self.status.events.on("contactBlocked") do(e: Args): + let contacts = self.status.contacts.getContacts() + self.view.setContactList(contacts) + + self.status.events.on("contactUnblocked") do(e: Args): + let contacts = self.status.contacts.getContacts() + self.view.setContactList(contacts) + self.status.events.on("contactRemoved") do(e: Args): let contacts = self.status.contacts.getContacts() self.view.setContactList(contacts) diff --git a/src/app/profile/view.nim b/src/app/profile/view.nim index 68e1ccf07e..063bbe2177 100644 --- a/src/app/profile/view.nim +++ b/src/app/profile/view.nim @@ -1,4 +1,4 @@ -import NimQml, sequtils +import NimQml, sequtils, strutils, sugar import views/[mailservers_list, contact_list, profile_info, device_list] import ../../status/profile/[mailserver, profile, devices] import ../../status/profile as status_profile @@ -6,6 +6,7 @@ import ../../status/contacts as status_contacts import ../../status/accounts as status_accounts import ../../status/status import ../../status/devices as status_devices +import ../../status/ens as status_ens import ../../status/chat/chat import ../../status/libstatus/types import qrcode/qrcode @@ -15,12 +16,15 @@ QtObject: profile*: ProfileInfoView mailserversList*: MailServersList contactList*: ContactList + addedContacts*: ContactList + blockedContacts*: ContactList deviceList*: DeviceList mnemonic: string network: string status*: Status isDeviceSetup: bool changeLanguage*: proc(locale: string) + contactToAdd*: Profile proc setup(self: ProfileView) = self.QObject.setup @@ -28,6 +32,8 @@ QtObject: proc delete*(self: ProfileView) = if not self.mailserversList.isNil: self.mailserversList.delete if not self.contactList.isNil: self.contactList.delete + if not self.addedContacts.isNil: self.addedContacts.delete + if not self.blockedContacts.isNil: self.blockedContacts.delete if not self.deviceList.isNil: self.deviceList.delete if not self.profile.isNil: self.profile.delete self.QObject.delete @@ -38,12 +44,19 @@ QtObject: result.profile = newProfileInfoView() result.mailserversList = newMailServersList() result.contactList = newContactList() + result.addedContacts = newContactList() + result.blockedContacts = newContactList() result.deviceList = newDeviceList() result.mnemonic = "" result.network = "" result.status = status result.isDeviceSetup = false result.changeLanguage = changeLanguage + result.contactToAdd = Profile( + username: "", + alias: "", + ensName: "" + ) result.setup proc addMailServerToList*(self: ProfileView, mailserver: MailServer) = @@ -58,6 +71,10 @@ QtObject: proc updateContactList*(self: ProfileView, contacts: seq[Profile]) = for contact in contacts: self.contactList.updateContact(contact) + if contact.systemTags.contains(":contact/added"): + self.addedContacts.updateContact(contact) + if contact.systemTags.contains(":contact/blocked"): + self.blockedContacts.updateContact(contact) proc contactListChanged*(self: ProfileView) {.signal.} @@ -66,6 +83,8 @@ QtObject: proc setContactList*(self: ProfileView, contactList: seq[Profile]) = self.contactList.setNewData(contactList) + self.addedContacts.setNewData(contactList.filter(c => c.systemTags.contains(":contact/added"))) + self.blockedContacts.setNewData(contactList.filter(c => c.systemTags.contains(":contact/blocked"))) self.contactListChanged() QtProperty[QVariant] contactList: @@ -73,6 +92,20 @@ QtObject: write = setContactList notify = contactListChanged + proc getAddedContacts(self: ProfileView): QVariant {.slot.} = + return newQVariant(self.addedContacts) + + QtProperty[QVariant] addedContacts: + read = getAddedContacts + notify = contactListChanged + + proc getBlockedContacts(self: ProfileView): QVariant {.slot.} = + return newQVariant(self.blockedContacts) + + QtProperty[QVariant] blockedContacts: + read = getBlockedContacts + notify = contactListChanged + proc mnemonicChanged*(self: ProfileView) {.signal.} proc getMnemonic*(self: ProfileView): QVariant {.slot.} = @@ -116,6 +149,27 @@ QtObject: QtProperty[QVariant] profile: read = getProfile + proc contactToAddChanged*(self: ProfileView) {.signal.} + + proc getContactToAddUsername(self: ProfileView): QVariant {.slot.} = + var username = self.contactToAdd.alias; + + if self.contactToAdd.ensVerified and self.contactToAdd.ensName != "": + username = self.contactToAdd.ensName + + return newQVariant(username) + + QtProperty[QVariant] contactToAddUsername: + read = getContactToAddUsername + notify = contactToAddChanged + + proc getContactToAddPubKey(self: ProfileView): QVariant {.slot.} = + return newQVariant(self.contactToAdd.address) + + QtProperty[QVariant] contactToAddPubKey: + read = getContactToAddPubKey + notify = contactToAddChanged + proc logout*(self: ProfileView) {.slot.} = self.status.profile.logout() @@ -173,4 +227,38 @@ QtObject: if enable: status_devices.enable(installationId) else: - status_devices.disable(installationId) \ No newline at end of file + status_devices.disable(installationId) + + proc lookupContact*(self: ProfileView, value: string) {.slot.} = + if value == "": + return + + var id = value + + if not id.startsWith("0x"): + id = status_ens.pubkey(id) + + let contact = self.status.contacts.getContactByID(id) + + if contact != nil: + self.contactToAdd = contact + else: + self.contactToAdd = Profile( + username: "", + alias: "", + ensName: "", + ensVerified: false + ) + self.contactToAddChanged() + + proc addContact*(self: ProfileView, pk: string) {.slot.} = + discard self.status.contacts.addContact(pk) + + proc unblockContact*(self: ProfileView, id: string) {.slot.} = + discard self.status.contacts.unblockContact(id) + + proc blockContact*(self: ProfileView, id: string) {.slot.} = + discard self.status.contacts.blockContact(id) + + proc removeContact*(self: ProfileView, id: string) {.slot.} = + self.status.contacts.removeContact(id) diff --git a/src/app/profile/views/contact_list.nim b/src/app/profile/views/contact_list.nim index 914767c153..d6bc04ffad 100644 --- a/src/app/profile/views/contact_list.nim +++ b/src/app/profile/views/contact_list.nim @@ -10,6 +10,7 @@ type Address = UserRole + 3 Identicon = UserRole + 4 IsContact = UserRole + 5 + IsBlocked = UserRole + 6 QtObject: type ContactList* = ref object of QAbstractListModel @@ -46,6 +47,7 @@ QtObject: of "identicon": result = contact.identicon of "pubKey": result = contact.id of "isContact": result = $contact.isContact() + of "isBlocked": result = $contact.isBlocked() method data(self: ContactList, index: QModelIndex, role: int): QVariant = if not index.isValid: @@ -59,6 +61,7 @@ QtObject: of ContactRoles.Identicon: result = newQVariant(contact.identicon) of ContactRoles.PubKey: result = newQVariant(contact.id) of ContactRoles.IsContact: result = newQVariant(contact.isContact()) + of ContactRoles.IsBlocked: result = newQVariant(contact.isBlocked()) method roleNames(self: ContactList): Table[int, string] = { @@ -66,7 +69,8 @@ QtObject: ContactRoles.Address.int:"address", ContactRoles.Identicon.int:"identicon", ContactRoles.PubKey.int:"pubKey", - ContactRoles.IsContact.int:"isContact" + ContactRoles.IsContact.int:"isContact", + ContactRoles.IsBlocked.int:"isBlocked" }.toTable proc addContactToList*(self: ContactList, contact: Profile) = diff --git a/src/status/contacts.nim b/src/status/contacts.nim index 9dc6dd56da..6382f8560e 100644 --- a/src/status/contacts.nim +++ b/src/status/contacts.nim @@ -26,7 +26,14 @@ proc getContactByID*(self: ContactModel, id: string): Profile = proc blockContact*(self: ContactModel, id: string): string = var contact = self.getContactByID(id) contact.systemTags.add(":contact/blocked") - status_contacts.blockContact(contact) + discard status_contacts.blockContact(contact) + self.events.emit("contactBlocked", Args()) + +proc unblockContact*(self: ContactModel, id: string): string = + var contact = self.getContactByID(id) + contact.systemTags.delete(contact.systemTags.find(":contact/blocked")) + discard status_contacts.saveContact(contact.id, contact.ensVerified, contact.ensName, contact.ensVerifiedAt, contact.ensVerificationRetries, contact.alias, contact.identicon, contact.systemTags) + self.events.emit("contactUnblocked", Args()) proc getContacts*(self: ContactModel): seq[Profile] = result = map(status_contacts.getContacts().getElems(), proc(x: JsonNode): Profile = x.toProfileModel()) diff --git a/ui/app/AppLayouts/Profile/ProfileLayout.qml b/ui/app/AppLayouts/Profile/ProfileLayout.qml index 8b9846a981..a59901fff9 100644 --- a/ui/app/AppLayouts/Profile/ProfileLayout.qml +++ b/ui/app/AppLayouts/Profile/ProfileLayout.qml @@ -32,9 +32,9 @@ SplitView { anchors.bottom: parent.bottom anchors.bottomMargin: 0 anchors.right: parent.right - anchors.rightMargin: 0 + anchors.rightMargin: 113 anchors.left: leftTab.right - anchors.leftMargin: 0 + anchors.leftMargin: 113 currentIndex: leftTab.currentTab // This list needs to match LeftTab/constants.js diff --git a/ui/app/AppLayouts/Profile/Sections/Contacts/Contact.qml b/ui/app/AppLayouts/Profile/Sections/Contacts/Contact.qml index d4deb7e7af..73fe5d6d9b 100644 --- a/ui/app/AppLayouts/Profile/Sections/Contacts/Contact.qml +++ b/ui/app/AppLayouts/Profile/Sections/Contacts/Contact.qml @@ -11,8 +11,11 @@ Rectangle { property bool selectable: false property var profileClick: function() {} property bool isContact: true + property bool isBlocked: false + property string searchStr: "" + id: container - visible: isContact + visible: isContact && (searchStr == "" || name.includes(searchStr)) height: visible ? 64 : 0 anchors.right: parent.right anchors.left: parent.left @@ -26,6 +29,7 @@ Rectangle { anchors.verticalCenter: parent.verticalCenter source: identicon } + StyledText { id: usernameText text: name @@ -34,22 +38,106 @@ Rectangle { anchors.rightMargin: Style.current.padding font.pixelSize: 17 anchors.top: accountImage.top + anchors.topMargin: Style.current.smallPadding anchors.left: accountImage.right anchors.leftMargin: Style.current.padding } - StyledText { - id: addressText - width: 108 - font.family: Style.current.fontHexRegular.name - text: address - elide: Text.ElideMiddle - anchors.bottom: accountImage.bottom - anchors.bottomMargin: 0 - anchors.left: usernameText.left - anchors.leftMargin: 0 - font.pixelSize: 15 - color: Style.current.darkGrey + + Rectangle { + property int iconSize: 14 + id: menuButton + height: 32 + width: 32 + anchors.top: usernameText.top + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + radius: 8 + + SVGImage { + source: "../../../../img/dots-icon.svg" + width: 18 + height: 4 + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + + MouseArea { + id: mouseArea + property bool menuOpened: false + cursorShape: Qt.PointingHandCursor + anchors.fill: parent + hoverEnabled: true + onExited: { + menuButton.color = Style.current.white + } + onEntered: { + menuButton.color = Style.current.grey + } + onClicked: { + menuOpened = true + contactContextMenu.popup() + } + + PopupMenu { + id: contactContextMenu + hasArrow: false + onClosed: { + mouseArea.menuOpened = false + } + Action { + icon.source: "../../../../img/profileActive.svg" + icon.width: menuButton.iconSize + icon.height: menuButton.iconSize + text: qsTrId("view-profile") + onTriggered: profileClick(name, address, identicon) + enabled: true + } + Action { + icon.source: "../../../../img/message.svg" + icon.width: menuButton.iconSize + icon.height: menuButton.iconSize + text: qsTrId("send-message") + onTriggered: { + tabBar.currentIndex = 0 + chatsModel.joinChat(address, Constants.chatTypeOneToOne) + } + enabled: !container.isBlocked + } + Action { + icon.source: "../../../../img/block-icon.svg" + icon.width: menuButton.iconSize + icon.height: menuButton.iconSize + text: qsTrId("block-user") + enabled: !container.isBlocked + onTriggered: { + profileModel.blockContact(address) + } + } + Action { + icon.source: "../../../../img/remove-contact.svg" + icon.width: menuButton.iconSize + icon.height: menuButton.iconSize + icon.color: Style.current.red + text: qsTrId("remove-contact") + enabled: container.isContact + onTriggered: profileModel.removeContact(address) + } + Action { + icon.source: "../../../../img/block-icon.svg" + icon.width: menuButton.iconSize + icon.height: menuButton.iconSize + icon.color: Style.current.red + text: qsTrId("unblock-user") + enabled: container.isBlocked + onTriggered: { + profileModel.unblockContact(address) + contactContextMenu.close() + } + } + } + } } + RadioButton { visible: selectable anchors.top: parent.top @@ -57,12 +145,4 @@ Rectangle { anchors.right: parent.right ButtonGroup.group: contactGroup } - MouseArea { - enabled: !selectable - cursorShape: Qt.PointingHandCursor - anchors.fill: parent - onClicked: { - profileClick(name, address, identicon) - } - } } diff --git a/ui/app/AppLayouts/Profile/Sections/Contacts/ContactList.qml b/ui/app/AppLayouts/Profile/Sections/Contacts/ContactList.qml index 6576475e23..d7714fc1d8 100644 --- a/ui/app/AppLayouts/Profile/Sections/Contacts/ContactList.qml +++ b/ui/app/AppLayouts/Profile/Sections/Contacts/ContactList.qml @@ -11,6 +11,7 @@ ListView { id: contactList property var contacts: ContactsData {} property var selectable: true + property string searchStr: "" property alias selectedContact: contactGroup.checkedButton property string searchString: "" property string lowerCaseSearchString: searchString.toLowerCase() @@ -23,6 +24,7 @@ ListView { address: model.address identicon: model.identicon isContact: model.isContact + isBlocked: model.isBlocked selectable: contactList.selectable profileClick: profilePopup.openPopup.bind(profilePopup) visible: searchString === "" || @@ -37,6 +39,7 @@ ListView { ButtonGroup { id: contactGroup } + } /*##^## Designer { diff --git a/ui/app/AppLayouts/Profile/Sections/ContactsContainer.qml b/ui/app/AppLayouts/Profile/Sections/ContactsContainer.qml index 5242842093..1f1030c131 100644 --- a/ui/app/AppLayouts/Profile/Sections/ContactsContainer.qml +++ b/ui/app/AppLayouts/Profile/Sections/ContactsContainer.qml @@ -9,93 +9,193 @@ import "./Contacts" Item { id: contactsContainer Layout.fillHeight: true - Layout.fillWidth: true + property alias searchStr: searchBox.text - Item { + SearchBox { + id: searchBox anchors.top: parent.top anchors.topMargin: 32 - anchors.right: parent.right - anchors.rightMargin: contentMargin - anchors.left: parent.left - anchors.leftMargin: contentMargin - anchors.bottom: parent.bottom + fontPixelSize: 15 + } - SearchBox { - id: searchBox + Item { + id: addNewContact + anchors.top: searchBox.bottom + anchors.topMargin: Style.current.bigPadding + width: addButton.width + usernameText.width + Style.current.padding + height: addButton.height + + AddButton { + id: addButton + clickable: false + anchors.verticalCenter: parent.verticalCenter + width: 40 + height: 40 } - Item { - id: addNewContact - anchors.top: searchBox.bottom - anchors.topMargin: Style.current.bigPadding - width: parent.width - height: addButton.height + StyledText { + id: usernameText + text: qsTr("Add new contact") + color: Style.current.blue + anchors.left: addButton.right + anchors.leftMargin: Style.current.padding + anchors.verticalCenter: addButton.verticalCenter + font.pixelSize: 15 + } - AddButton { - id: addButton - clickable: false - anchors.verticalCenter: parent.verticalCenter - } - StyledText { - id: usernameText - text: qsTr("Add new contact") - color: Style.current.blue - anchors.left: addButton.right - anchors.leftMargin: Style.current.padding - anchors.verticalCenter: addButton.verticalCenter - font.pixelSize: 15 - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - // TODO implement adding a contact - console.log('Add a contact') - } + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + addContactModal.open() } } + } + + Item { + id: blockedContactsButton + anchors.top: addNewContact.bottom + anchors.topMargin: Style.current.bigPadding + width: blockButton.width + blockButtonLabel.width + Style.current.padding + height: addButton.height + + IconButton { + id: blockButton + clickable: false + anchors.verticalCenter: parent.verticalCenter + width: 40 + height: 40 + iconName: "block-icon" + color: Style.current.lightBlue + } + + StyledText { + id: blockButtonLabel + text: qsTr("Blocked contacts") + color: Style.current.blue + anchors.left: blockButton.right + anchors.leftMargin: Style.current.padding + anchors.verticalCenter: blockButton.verticalCenter + font.pixelSize: 15 + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + blockedContactsModal.open() + } + } + } + + ModalPopup { + id: blockedContactsModal + title: qsTr("Blocked contacts") ContactList { - id: contactListView - anchors.top: addNewContact.bottom - anchors.topMargin: Style.current.bigPadding + anchors.top: parent.top anchors.bottom: parent.bottom - contacts: profileModel.contactList + contacts: profileModel.blockedContacts selectable: false - searchString: searchBox.text + } + } + + ModalPopup { + id: addContactModal + title: qsTr("Add contact") + + Input { + id: addContactSearchInput + placeholderText: qsTrId("Enter ENS username or chat key") + customHeight: 44 + fontPixelSize: 15 + onEditingFinished: { + profileModel.lookupContact(inputValue) + } } Item { - id: element - visible: profileModel.contactList.rowCount() === 0 - anchors.fill: parent + id: contactToAddInfo + anchors.top: addContactSearchInput.bottom + anchors.topMargin: Style.current.padding + anchors.horizontalCenter: parent.horizontalCenter + height: contactUsername.height + width: contactUsername.width + contactPubKey.width + visible: profileModel.contactToAddPubKey !== "" - StyledText { - id: noFriendsText - text: qsTr("You don’t have any contacts yet") - anchors.verticalCenterOffset: -Style.current.bigPadding - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - font.pixelSize: 15 - color: Style.current.darkGrey - } + StyledText { + id: contactUsername + text: profileModel.contactToAddUsername + " • " + anchors.top: addContactSearchInput.bottom + anchors.topMargin: Style.current.padding + font.pixelSize: 12 + color: Style.current.darkGrey + } - StyledButton { - anchors.horizontalCenter: noFriendsText.horizontalCenter - anchors.top: noFriendsText.bottom - anchors.topMargin: Style.current.bigPadding - label: qsTr("Invite firends") - onClicked: function () { - inviteFriendsPopup.open() - } - } + StyledText { + id: contactPubKey + text: profileModel.contactToAddPubKey + anchors.left: contactUsername.right + width: 100 + font.pixelSize: 12 + elide: Text.ElideMiddle + color: Style.current.darkGrey + } - InviteFriendsPopup { - id: inviteFriendsPopup + } + footer: StyledButton { + anchors.right: parent.right + anchors.leftMargin: Style.current.padding + //% "Send Message" + label: qsTr("Add contact") + disabled: !contactToAddInfo.visible + anchors.bottom: parent.bottom + onClicked: { + profileModel.addContact(profileModel.contactToAddPubKey); + addContactModal.close() } } } + + ContactList { + id: contactListView + anchors.top: blockedContactsButton.bottom + anchors.topMargin: Style.current.bigPadding + anchors.bottom: parent.bottom + contacts: profileModel.addedContacts + selectable: false + searchString: searchBox.text + } + + Item { + id: element + visible: profileModel.contactList.rowCount() === 0 + anchors.fill: parent + + StyledText { + id: noFriendsText + text: qsTr("You don’t have any contacts yet") + anchors.verticalCenterOffset: -Style.current.bigPadding + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + font.pixelSize: 15 + color: Style.current.darkGrey + } + + StyledButton { + anchors.horizontalCenter: noFriendsText.horizontalCenter + anchors.top: noFriendsText.bottom + anchors.topMargin: Style.current.bigPadding + label: qsTr("Invite firends") + onClicked: function () { + inviteFriendsPopup.open() + } + } + + InviteFriendsPopup { + id: inviteFriendsPopup + } + } } /*##^## diff --git a/ui/app/img/block-icon.svg b/ui/app/img/block-icon.svg new file mode 100644 index 0000000000..c1ce6b4901 --- /dev/null +++ b/ui/app/img/block-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/app/img/dots-icon.svg b/ui/app/img/dots-icon.svg new file mode 100644 index 0000000000..b3f89c55b9 --- /dev/null +++ b/ui/app/img/dots-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/app/img/remove-contact.svg b/ui/app/img/remove-contact.svg new file mode 100644 index 0000000000..4aaaca41b4 --- /dev/null +++ b/ui/app/img/remove-contact.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/nim-status-client.pro b/ui/nim-status-client.pro index 0c00b30530..39c55ad804 100644 --- a/ui/nim-status-client.pro +++ b/ui/nim-status-client.pro @@ -308,6 +308,7 @@ DISTFILES += \ onboarding/img/wallet@3x.jpg \ onboarding/qmldir \ shared/AddButton.qml \ + shared/IconButton.qml \ shared/Input.qml \ shared/ModalPopup.qml \ shared/NotificationWindow.qml \ diff --git a/ui/shared/AddButton.qml b/ui/shared/AddButton.qml index f0c2849a93..c86ea186dc 100644 --- a/ui/shared/AddButton.qml +++ b/ui/shared/AddButton.qml @@ -3,80 +3,9 @@ import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 import Qt.labs.platform 1.1 import "../imports" +import "./" -Rectangle { - signal clicked - property int iconWidth: 14 - property int iconHeight: 14 - property alias icon: imgIcon - property bool clickable: true - - id: btnAddContainer - width: 36 - height: 36 - color: Style.current.blue - radius: width / 2 - - - Image { - id: imgIcon - fillMode: Image.PreserveAspectFit - source: "../app/img/plusSign.svg" - width: iconWidth - height: iconHeight - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - - state: "default" - rotation: 0 - states: [ - State { - name: "default" - PropertyChanges { - target: imgIcon - rotation: 0 - } - }, - State { - name: "rotated" - PropertyChanges { - target: imgIcon - rotation: 45 - } - } - ] - - transitions: [ - Transition { - from: "default" - to: "rotated" - RotationAnimation { - duration: 150 - direction: RotationAnimation.Clockwise - easing.type: Easing.InCubic - } - }, - Transition { - from: "rotated" - to: "default" - RotationAnimation { - duration: 150 - direction: RotationAnimation.Counterclockwise - easing.type: Easing.OutCubic - } - } - ] - } - - MouseArea { - id: mouseArea - visible: btnAddContainer.clickable - anchors.fill: parent - acceptedButtons: Qt.LeftButton | Qt.RightButton - cursorShape: Qt.PointingHandCursor - onClicked: { - imgIcon.state = "rotated" - btnAddContainer.clicked() - } - } +IconButton { + id: iconButton + iconName: "plusSign" } diff --git a/ui/shared/IconButton.qml b/ui/shared/IconButton.qml new file mode 100644 index 0000000000..e1e0293247 --- /dev/null +++ b/ui/shared/IconButton.qml @@ -0,0 +1,83 @@ +import QtQuick 2.3 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.3 +import Qt.labs.platform 1.1 +import "../imports" + +Rectangle { + signal clicked + property int iconWidth: 14 + property int iconHeight: 14 + property alias icon: imgIcon + property bool clickable: true + property string iconName: "plusSign" + + id: btnAddContainer + width: 36 + height: 36 + color: Style.current.blue + radius: width / 2 + + Image { + id: imgIcon + fillMode: Image.PreserveAspectFit + source: "../app/img/" + parent.iconName + ".svg" + width: iconWidth + height: iconHeight + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + + state: "default" + rotation: 0 + states: [ + State { + name: "default" + PropertyChanges { + target: imgIcon + rotation: 0 + } + }, + State { + name: "rotated" + PropertyChanges { + target: imgIcon + rotation: 45 + } + } + ] + + transitions: [ + Transition { + from: "default" + to: "rotated" + RotationAnimation { + duration: 150 + direction: RotationAnimation.Clockwise + easing.type: Easing.InCubic + } + }, + Transition { + from: "rotated" + to: "default" + RotationAnimation { + duration: 150 + direction: RotationAnimation.Counterclockwise + easing.type: Easing.OutCubic + } + } + ] + } + + MouseArea { + id: mouseArea + visible: btnAddContainer.clickable + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + cursorShape: Qt.PointingHandCursor + onClicked: { + imgIcon.state = "rotated" + btnAddContainer.clicked() + } + } +} + diff --git a/ui/shared/Input.qml b/ui/shared/Input.qml index 64cfc799bf..ca0e7de0ca 100644 --- a/ui/shared/Input.qml +++ b/ui/shared/Input.qml @@ -21,6 +21,7 @@ Item { readonly property int labelMargin: 7 property int customHeight: 44 property int fontPixelSize: 15 + signal editingFinished(string inputValue) id: inputBox height: inputRectangle.height + (hasLabel ? inputLabel.height + labelMargin : 0) + (!!validationError ? validationErrorText.height : 0) @@ -70,6 +71,7 @@ Item { background: Rectangle { color: Style.current.transparent } + onEditingFinished: inputBox.editingFinished(inputBox.text) } SVGImage { diff --git a/ui/shared/PopupMenu.qml b/ui/shared/PopupMenu.qml index f2e3faa3e9..197c4f6c1c 100644 --- a/ui/shared/PopupMenu.qml +++ b/ui/shared/PopupMenu.qml @@ -7,6 +7,7 @@ import "../shared" Menu { property alias arrowX: bgPopupMenuTopArrow.x property int paddingSize: 8 + property bool hasArrow: true closePolicy: Popup.CloseOnPressOutside | Popup.CloseOnReleaseOutside | Popup.CloseOnEscape id: popupMenu topPadding: bgPopupMenuTopArrow.height + paddingSize @@ -18,6 +19,8 @@ Menu { implicitHeight: 34 font.pixelSize: 13 icon.color: popupMenuItem.action.icon.color != "#00000000" ? popupMenuItem.action.icon.color : Style.current.blue + visible: popupMenuItem.action.enabled + height: popupMenuItem.action.enabled ? popupMenuItem.implicitHeight : 0 contentItem: Item { id: menuItemContent @@ -66,6 +69,7 @@ Menu { color: "transparent" Rectangle { id: bgPopupMenuTopArrow + visible: popupMenu.hasArrow color: Style.current.modalBackground height: 14 width: 14