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
This commit is contained in:
Pascal Precht 2020-07-24 13:27:26 +02:00 committed by Iuri Matias
parent 9604faff08
commit e18188514a
16 changed files with 490 additions and 168 deletions

View File

@ -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)

View File

@ -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)
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)

View File

@ -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) =

View File

@ -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())

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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 {

View File

@ -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 dont 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 dont 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
}
}
}
/*##^##

View File

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.99967 13.6668C3.31778 13.6668 0.333008 10.6821 0.333008 7.00016C0.333008 3.31826 3.31778 0.333496 6.99967 0.333496C10.6816 0.333496 13.6663 3.31826 13.6663 7.00016C13.6663 10.6821 10.6816 13.6668 6.99967 13.6668ZM2.91208 10.3806C2.77375 10.519 2.54581 10.5094 2.42998 10.3518C1.74037 9.41312 1.33301 8.25421 1.33301 7.00016C1.33301 3.87055 3.87006 1.3335 6.99967 1.3335C8.25373 1.3335 9.41263 1.74086 10.3513 2.43047C10.509 2.5463 10.5185 2.77424 10.3802 2.91257L2.91208 10.3806ZM3.61919 11.0878C3.48086 11.2261 3.4904 11.454 3.64806 11.5699C4.58672 12.2595 5.74562 12.6668 6.99967 12.6668C10.1293 12.6668 12.6663 10.1298 12.6663 7.00016C12.6663 5.74611 12.259 4.5872 11.5694 3.64855C11.4535 3.49089 11.2256 3.48135 11.0873 3.61968L3.61919 11.0878Z" fill="#4360DF"/>
</svg>

After

Width:  |  Height:  |  Size: 922 B

5
ui/app/img/dots-icon.svg Normal file
View File

@ -0,0 +1,5 @@
<svg width="18" height="4" viewBox="0 0 18 4" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 2C4 3.10457 3.10457 4 2 4C0.895431 4 0 3.10457 0 2C0 0.895431 0.895431 0 2 0C3.10457 0 4 0.895431 4 2Z" fill="#939BA1"/>
<path d="M11 2C11 3.10457 10.1046 4 9 4C7.89543 4 7 3.10457 7 2C7 0.895431 7.89543 0 9 0C10.1046 0 11 0.895431 11 2Z" fill="#939BA1"/>
<path d="M16 4C17.1046 4 18 3.10457 18 2C18 0.895431 17.1046 0 16 0C14.8954 0 14 0.895431 14 2C14 3.10457 14.8954 4 16 4Z" fill="#939BA1"/>
</svg>

After

Width:  |  Height:  |  Size: 509 B

View File

@ -0,0 +1,5 @@
<svg width="13" height="12" viewBox="0 0 13 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.00033 6.66667C7.84127 6.66667 9.33366 5.17428 9.33366 3.33333C9.33366 1.49238 7.84127 0 6.00033 0C4.15938 0 2.66699 1.49238 2.66699 3.33333C2.66699 5.17428 4.15938 6.66667 6.00033 6.66667ZM6.00033 5.66667C7.28899 5.66667 8.33366 4.622 8.33366 3.33333C8.33366 2.04467 7.28899 1 6.00033 1C4.71166 1 3.66699 2.04467 3.66699 3.33333C3.66699 4.622 4.71166 5.66667 6.00033 5.66667Z" fill="#FF2D55"/>
<path d="M6.62336 9.18929C6.60016 9.46766 6.35289 9.67067 6.07357 9.66713C6.04916 9.66682 6.02471 9.66667 6.00022 9.66667C4.30462 9.66667 2.78296 10.4114 1.74449 11.5916C1.54282 11.8208 1.18548 11.8519 0.969607 11.636C0.795787 11.4622 0.774868 11.1856 0.934774 10.9989C2.15749 9.57132 3.97321 8.66667 6.00022 8.66667C6.05818 8.66667 6.11597 8.66741 6.17357 8.66888C6.44624 8.67584 6.64601 8.91747 6.62336 9.18929Z" fill="#FF2D55"/>
<path d="M8.66699 8.66667C8.39085 8.66667 8.16699 8.89052 8.16699 9.16667C8.16699 9.44281 8.39085 9.66667 8.66699 9.66667H12.0003C12.2765 9.66667 12.5003 9.44281 12.5003 9.16667C12.5003 8.89052 12.2765 8.66667 12.0003 8.66667H8.66699Z" fill="#FF2D55"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -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 \

View File

@ -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"
}

83
ui/shared/IconButton.qml Normal file
View File

@ -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()
}
}
}

View File

@ -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 {

View File

@ -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