Skip to content
Snippets Groups Projects
Commit 62c897e0 authored by Vincent Texier's avatar Vincent Texier
Browse files

Wot view integrated in the community tab

browsing and "me" button are working
todo: search function and arc color based on currency certifications config
parent 47085a10
Branches
Tags
No related merge requests found
......@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
<width>421</width>
<height>369</height>
</rect>
</property>
<property name="windowTitle">
......@@ -15,14 +15,17 @@
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Members</string>
<widget class="QTabWidget" name="tabs_community">
<property name="currentIndex">
<number>1</number>
</property>
</widget>
</item>
<widget class="QWidget" name="tab_members">
<attribute name="title">
<string>Members</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="QListView" name="list_community_members"/>
</item>
......@@ -52,8 +55,15 @@
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout"/>
</layout>
</widget>
<widget class="QWidget" name="tab_wot">
<attribute name="title">
<string>Wot</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2"/>
</widget>
</widget>
</item>
</layout>
</widget>
......
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>522</width>
<height>442</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QComboBox" name="comboBoxSearch">
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="pushButtonReset">
<property name="maximumSize">
<size>
<width>85</width>
<height>27</height>
</size>
</property>
<property name="text">
<string>Me</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="View" name="graphicsView">
<property name="viewportUpdateMode">
<enum>QGraphicsView::BoundingRectViewportUpdate</enum>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>View</class>
<extends>QGraphicsView</extends>
<header>cutecoin.wot.qt.view.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>pushButtonReset</sender>
<signal>clicked()</signal>
<receiver>Form</receiver>
<slot>reset()</slot>
<hints>
<hint type="sourcelabel">
<x>516</x>
<y>23</y>
</hint>
<hint type="destinationlabel">
<x>284</x>
<y>198</y>
</hint>
</hints>
</connection>
<connection>
<sender>comboBoxSearch</sender>
<signal>currentIndexChanged(int)</signal>
<receiver>Form</receiver>
<slot>select_node()</slot>
<hints>
<hint type="sourcelabel">
<x>215</x>
<y>22</y>
</hint>
<hint type="destinationlabel">
<x>260</x>
<y>220</y>
</hint>
</hints>
</connection>
</connections>
<slots>
<slot>reset()</slot>
<slot>search()</slot>
<slot>select_node()</slot>
</slots>
</ui>
......@@ -9,6 +9,7 @@ from PyQt5.QtWidgets import QWidget, QErrorMessage
from cutecoin.models.members import MembersListModel
from cutecoin.gen_resources.communityTabWidget_uic import Ui_CommunityTabWidget
from cutecoin.gui.addContactDialog import AddContactDialog
from cutecoin.wot.qt.form import Form
class CommunityTabWidget(QWidget, Ui_CommunityTabWidget):
......@@ -34,6 +35,9 @@ class CommunityTabWidget(QWidget, Ui_CommunityTabWidget):
self.button_membership.setText("Send membership demand")
self.button_membership.clicked.connect(self.send_membership_demand)
# create wot widget
self.verticalLayout_2.addWidget(Form(account, community))
def add_member_as_contact(self, index):
members_model = self.list_community_members.model()
members = members_model.members
......
# -*- coding: utf-8 -*-
import time
import datetime
import hashlib
from PyQt5.QtWidgets import QWidget
from cutecoin.gen_resources.wot_form_uic import Ui_Form
import cutecoin.wot.mapi as mapi
from cutecoin.wot.qt.view import NODE_STATUS_HIGHLIGHTED, NODE_STATUS_SELECTED, ARC_STATUS_STRONG, ARC_STATUS_WEAK
from ucoinpy.api import bma
class Form(QWidget, Ui_Form):
def __init__(self, account, community, parent=None):
"""
:param cutecoin.core.account.Account account:
:param cutecoin.core.community.Community community:
:param parent:
:return:
"""
super(Form, self).__init__(parent)
# construct from qtDesigner
self.setupUi(self)
# add combobox events
self.comboBoxSearch.lineEdit().textEdited.connect(self.search)
self.comboBoxSearch.lineEdit().returnPressed.connect(self.combobox_return_pressed)
# add scene events
self.graphicsView.scene().node_signed.connect(self.sign_node)
self.graphicsView.scene().node_clicked.connect(self.draw_graph)
self.account = account
self.community = community
self.nodes = []
self.graph = {}
self.signature_validity = 86400 * 365
self.ARC_STATUS_STRONG_time = self.signature_validity - (86400 * 165)
self.draw_graph(self.account.pubkey)
def draw_graph(self, public_key):
"""
Draw community graph centered on public_key identity
:param public_key: Public key of the identity
"""
self.graph = dict()
# add wallet node
node_status = (NODE_STATUS_HIGHLIGHTED and (public_key == self.account.pubkey)) or 0
node_status += NODE_STATUS_SELECTED
certifiers = self.community.request(bma.wot.CertifiersOf, {'search': public_key})
self.graph[public_key] = {'arcs': [], 'text': certifiers['uid'], 'tooltip': public_key, 'status': node_status}
# add certifiers of uid
#for certifier in self.community.request(mapi.get_sig_to(public_key):
for certifier in certifiers['certifications']:
if certifier['pubkey'] in self.graph.keys():
continue
if (time.time() - certifier['cert_time']['medianTime']) > self.ARC_STATUS_STRONG_time:
arc_status = ARC_STATUS_WEAK
else:
arc_status = ARC_STATUS_STRONG
arc = {
'id': public_key,
'status': arc_status,
'tooltip': datetime.datetime.fromtimestamp(
certifier['cert_time']['medianTime'] + self.signature_validity
).strftime("%Y/%m/%d")
}
node_status = (NODE_STATUS_HIGHLIGHTED and (certifier['pubkey'] == self.account.pubkey)) or 0
self.graph[certifier['pubkey']] = {
'arcs': [arc],
'text': certifier['uid'],
'tooltip': public_key,
'status': node_status
}
# add certified by uid
#for certified in mapi.get_sig_from(public_key):
for certified in self.community.request(bma.wot.CertifiedBy, {'search': public_key})['certifications']:
if certified['pubkey'] in self.graph.keys():
continue
if (time.time() - certified['cert_time']['medianTime']) > self.ARC_STATUS_STRONG_time:
arc_status = ARC_STATUS_WEAK
else:
arc_status = ARC_STATUS_STRONG
arc = {
'id': certified['pubkey'],
'status': arc_status,
'tooltip': datetime.datetime.fromtimestamp(
certified['cert_time']['medianTime'] + self.signature_validity
).strftime("%Y/%m/%d")
}
node_status = (NODE_STATUS_HIGHLIGHTED and (certified['pubkey'] == self.account.pubkey)) or 0
self.graph[certified['pubkey']] = {
'arcs': [],
'text': certified['uid'],
'tooltip': public_key,
'status': node_status
}
self.graph[public_key]['arcs'].append(arc)
# draw graph in qt scene
self.graphicsView.scene().update_wot(self.graph)
def reset(self):
"""
Reset graph scene to wallet identity
"""
self.draw_graph(
self.account.pubkey
)
def combobox_return_pressed(self):
"""
Search nodes when return is pressed in combobox lineEdit
"""
self.search(self.comboBoxSearch.lineEdit().text())
def search(self, text):
"""
Search nodes when text is edited in combobox lineEdit
"""
if len(text) < 2:
return False
self.nodes = mapi.search(text)
if self.nodes:
self.comboBoxSearch.clear()
self.comboBoxSearch.lineEdit().setText(text)
for node in self.nodes:
self.comboBoxSearch.addItem(node['uid'])
self.comboBoxSearch.showPopup()
def select_node(self, index):
"""
Select node in graph when item is selected in combobox
"""
if index < 0 or index >= len(self.nodes):
return False
node = self.nodes[index]
self.draw_graph(
node['pubkey']
)
def sign_node(self, public_key):
print('sign node {} not implemented'.format(public_key))
# -*- coding: utf-8 -*-
import math
from PyQt5.QtGui import QPainter, QBrush, QPen, QPolygonF, QColor, QRadialGradient,\
QPainterPath, QMouseEvent, QWheelEvent, QTransform
from PyQt5.QtCore import Qt, QRectF, QLineF, QPoint, QPointF, QSizeF, qFuzzyCompare, pyqtSignal
from PyQt5.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsEllipseItem,\
QGraphicsSimpleTextItem, QGraphicsLineItem, QMenu, QAction, QGraphicsSceneHoverEvent,\
QGraphicsSceneContextMenuEvent
NODE_STATUS_HIGHLIGHTED = 1
NODE_STATUS_SELECTED = 2
ARC_STATUS_STRONG = 1
ARC_STATUS_WEAK = 2
class View(QGraphicsView):
def __init__(self, parent=None):
"""
Create View to display scene
:param parent: [Optional, default=None] Parent widget
"""
super(View, self).__init__(parent)
self.setScene(Scene(self))
self.setCacheMode(QGraphicsView.CacheBackground)
self.setViewportUpdateMode(QGraphicsView.BoundingRectViewportUpdate)
self.setRenderHint(QPainter.Antialiasing)
self.setRenderHint(QPainter.SmoothPixmapTransform)
def wheelEvent(self, event: QWheelEvent):
"""
Zoom in/out on the mouse cursor
"""
# zoom only when CTRL key pressed
if (event.modifiers() & Qt.ControlModifier) == Qt.ControlModifier:
steps = event.angleDelta().y() / 15 / 8
if steps == 0:
event.ignore()
return
# scale factor 1.25
sc = pow(1.25, steps)
self.scale(sc, sc)
self.centerOn(self.mapToScene(event.pos()))
event.accept()
# act normally on scrollbar
else:
# transmit event to parent class wheelevent
super(QGraphicsView, self).wheelEvent(event)
class Scene(QGraphicsScene):
# This defines a signal called 'nodeSigned' that takes on string argument
node_signed = pyqtSignal(str, name='nodeSigned')
node_clicked = pyqtSignal(str, name='nodeClicked')
def __init__(self, parent=None):
"""
Create scene of the graph
:param parent: [Optional, default=None] Parent view
"""
super(Scene, self).__init__(parent)
self.lastDragPos = QPoint()
self.setItemIndexMethod(QGraphicsScene.NoIndex)
# axis of the scene for debug purpose
# self.addLine(-100, 0, 100, 0)
# self.addLine(0, -100, 0, 100)
def add_node(self, _id, node, pos):
"""
Add a node item in the graph
:param str _id: Node id
:param dict node: Node data
:param tuple pos: Position (x,y) of the node
:return: Node
"""
node = Node(_id, node, pos)
self.addItem(node)
return node
def add_arc(self, source_node, destination_node, arc):
"""
Add an arc between two nodes
:param Node source_node: Source node of the arc
:param Node destination_node: Destination node of the arc
:param dict arc: Arc data
:return: Arc
"""
arc = Arc(source_node, destination_node, arc)
self.addItem(arc)
return arc
def update_wot(self, graph):
"""
draw community graph
:param dict graph: graph to draw
"""
# clear scene
self.clear()
# capture selected node (to draw it in the center)
for _id, node in graph.items():
if node['status'] & NODE_STATUS_SELECTED:
selected_id = _id
selected_node = node
root_node = self.add_node(selected_id, selected_node, (0, 0))
done = [selected_id]
# add certified by selected node
y = 0
x = 200
# capture nodes for sorting by text
nodes = dict()
for arc in selected_node['arcs']:
nodes[arc['id']] = {'node': graph[arc['id']], 'arc': arc}
# sort by text
nodes = ((k, v) for (k, v) in sorted(nodes.items(), key=lambda kv: kv[1]['node']['text'].lower()))
# add nodes and arcs
for _id, items in nodes:
node = self.add_node(_id, items['node'], (x, y))
self.add_arc(root_node, node, items['arc'])
graph.pop(_id)
y += 50
done.append(_id)
# add certifiers of selected node
y = 0
x = -200
# capture nodes for sorting by text
nodes = dict()
for _id, certifier_node in graph.items():
nodes[_id] = certifier_node
# sort by text
nodes = ((k, v) for (k, v) in sorted(nodes.items(), key=lambda kv: kv[1]['text'].lower()))
# add nodes and arcs
for _id, certifier_node in nodes:
if _id in done:
continue
node = self.add_node(_id, certifier_node, (x, y))
for arc in certifier_node['arcs']:
if arc['id'] == selected_id:
self.add_arc(node, root_node, arc)
y += 50
self.update()
class Node(QGraphicsEllipseItem):
def __init__(self, _id, data, x_y):
"""
Create node in the graph scene
:param dict data: Node data
:param x_y: Position of the node
"""
# unpack tuple
x, y = x_y
super(Node, self).__init__()
self.id = _id
self.status_wallet = data['status'] & NODE_STATUS_HIGHLIGHTED
self.text = data['text']
self.setToolTip(data['tooltip'])
self.arcs = []
self.menu = None
self.action_sign = None
# color around ellipse
outline_color = QColor('grey')
if self.status_wallet:
outline_color = QColor('black')
self.setPen(QPen(outline_color))
# text inside ellipse
self.text_item = QGraphicsSimpleTextItem(self)
self.text_item.setText(self.text)
text_color = QColor('grey')
if self.status_wallet == NODE_STATUS_HIGHLIGHTED:
text_color = QColor('black')
self.text_item.setBrush(QBrush(text_color))
# center ellipse around text
self.setRect(
0,
0,
self.text_item.boundingRect().width() * 2,
self.text_item.boundingRect().height() * 2
)
# set anchor to the center
self.setTransform(QTransform().translate(-self.boundingRect().width()/2.0, -self.boundingRect().height()/2.0))
self.setPos(x, y)
#print(x, y)
# center text in ellipse
self.text_item.setPos(self.boundingRect().width() / 4.0, self.boundingRect().height() / 4.0)
# create gradient inside the ellipse
gradient = QRadialGradient(QPointF(0, self.boundingRect().height() / 4), self.boundingRect().width())
gradient.setColorAt(0, QColor('white'))
gradient.setColorAt(1, QColor('darkgrey'))
self.setBrush(QBrush(gradient))
# cursor change on hover
self.setAcceptHoverEvents(True)
self.setZValue(1)
def mousePressEvent(self, event: QMouseEvent):
"""
Click on mouse button
:param event: mouse event
"""
if event.button() == Qt.LeftButton:
# trigger scene signal
self.scene().node_clicked.emit(self.id)
def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent):
"""
Mouse enter on node zone
:param event: scene hover event
"""
self.setCursor(Qt.ArrowCursor)
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent):
"""
Right click on node to show node menu
:param event: scene context menu event
"""
# create node context menus
self.menu = QMenu()
# action sign identity
self.action_sign = QAction('Sign identity', self.scene())
self.menu.addAction(self.action_sign)
self.action_sign.triggered.connect(self.sign_action)
self.menu.exec(event.screenPos())
def add_arc(self, arc):
"""
Add arc to the arc list
:param arc: Arc
"""
self.arcs.append(arc)
def sign_action(self):
"""
Sign identity node
"""
# trigger scene signal
self.scene().node_signed.emit(self.id)
class Arc(QGraphicsLineItem):
def __init__(self, source_node, destination_node, data):
"""
Create an arc between two nodes
:param Node source_node: Source node of the arc
:param Node destination_node: Destination node of the arc
:param dict data: Arc data
"""
super(Arc, self).__init__()
self.source = source_node
self.destination = destination_node
self.setToolTip(data['tooltip'])
self.source.add_arc(self)
self.status = data['status']
self.source_point = None
self.destination_point = None
self.arrow_size = 5.0
self.setAcceptedMouseButtons(Qt.NoButton)
# cursor change on hover
self.setAcceptHoverEvents(True)
self.adjust()
self.setZValue(0)
def adjust(self):
"""
Draw the arc line
"""
if not self.source or not self.destination:
return
line = QLineF(
self.mapFromItem(
self.source,
self.source.boundingRect().width() - (self.source.boundingRect().width() / 2.0),
self.source.boundingRect().height() / 2.0
),
self.mapFromItem(
self.destination,
self.destination.boundingRect().width() / 2.0,
self.destination.boundingRect().height() / 2.0
)
)
self.prepareGeometryChange()
self.source_point = line.p1()
self.destination_point = line.p2()
# mouse over on line only
self.setLine(line)
# virtual function require subclassing
def boundingRect(self):
"""
Return the bounding rectangle size
:return: QRectF
"""
if not self.source or not self.destination:
return QRectF()
pen_width = 1.0
extra = (pen_width + self.arrow_size) / 2.0
return QRectF(
self.source_point, QSizeF(
self.destination_point.x() - self.source_point.x(),
self.destination_point.y() - self.source_point.y()
)
).normalized().adjusted(
-extra,
-extra,
extra,
extra
)
def paint(self, painter, option, widget):
"""
Customize line adding an arrow head
:param QPainter painter: Painter instance of the item
:param option: Painter option of the item
:param widget: Widget instance
"""
if not self.source or not self.destination:
return
line = QLineF(self.source_point, self.destination_point)
if qFuzzyCompare(line.length(), 0):
return
# Draw the line itself
color = QColor()
style = Qt.SolidLine
if self.status == ARC_STATUS_STRONG:
color.setNamedColor('blue')
if self.status == ARC_STATUS_WEAK:
color.setNamedColor('salmon')
style = Qt.DashLine
painter.setPen(QPen(color, 1, style, Qt.RoundCap, Qt.RoundJoin))
painter.drawLine(line)
painter.setPen(QPen(color, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
# Draw the arrows
angle = math.acos(line.dx() / line.length())
if line.dy() >= 0:
angle = (2.0 * math.pi) - angle
# arrow in the middle of the arc
hpx = (line.p2().x() + line.p1().x()) / 2.0
hpy = (line.p2().y() - line.p1().y()) / 2.0
if line.dy() < 0:
hpy = -hpy
head_point = QPointF(hpx, hpy)
painter.setPen(QPen(color, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
destination_arrow_p1 = head_point + QPointF(
math.sin(angle - math.pi / 3) * self.arrow_size,
math.cos(angle - math.pi / 3) * self.arrow_size)
destination_arrow_p2 = head_point + QPointF(
math.sin(angle - math.pi + math.pi / 3) * self.arrow_size,
math.cos(angle - math.pi + math.pi / 3) * self.arrow_size)
painter.setBrush(color)
painter.drawPolygon(QPolygonF([head_point, destination_arrow_p1, destination_arrow_p2]))
def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent):
"""
Mouse enter on arc zone
:param event: scene hover event
"""
self.setCursor(Qt.ArrowCursor)
def shape(self):
"""
Return real shape of the item to detect collision or hover accurately
:return: QPainterPath
"""
# detection mouse hover on arc path
path = QPainterPath()
path.addPolygon(QPolygonF([self.line().p1(), self.line().p2()]))
# add handles at the start and end of arc
path.addRect(QRectF(
self.line().p1().x()-5,
self.line().p1().y()-5,
self.line().p1().x()+5,
self.line().p1().y()+5
))
path.addRect(QRectF(
self.line().p2().x()-5,
self.line().p2().y()-5,
self.line().p2().x()+5,
self.line().p2().y()+5
))
return path
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment