Commit a8d781d7 authored by Richer Maximilien's avatar Richer Maximilien

Merge branch '205_demo_gui_simple' into 'master'

205 demo gui simple

This merge request adds many features to the graphic demonstrator.

It is now possible to monitor promises and signatures with a predefined quantum / speed.

You can test with the attached file: [test.json](/uploads/8fe76b891df09188adab5b9c4633f1c2/test.json)

See merge request !47
parents 415f8766 11dcecc3
Pipeline #567 passed with stages
<RCC>
<qresource prefix="/">
<file>widget.ui</file>
<file>images/server_key.png</file>
<file>images/server_connect.png</file>
<file>images/node_magnifier.png</file>
<file>images/control_play.png</file>
<file>images/control_play_blue.png</file>
<file>images/control_pause.png</file>
<file>images/control_pause_blue.png</file>
<file>images/control_rewind_blue.png</file>
</qresource>
</RCC>
package gui
// This file stores useful colors
import "github.com/visualfc/goqt/ui"
var colors = map[string]uint32{
"red": 0x00ff0000,
"green": 0x0000aa00,
"blue": 0x000000ff,
"black": 0x00000000,
}
var pen_black = ui.NewPenWithColor(ui.NewColorWithGlobalcolor(ui.Qt_black))
var pen_gray = ui.NewPenWithColor(ui.NewColorWithGlobalcolor(ui.Qt_gray))
var brush_none = ui.NewBrushWithGlobalcolorBrushstyle(ui.Qt_transparent, ui.Qt_SolidPattern)
var brush_black = ui.NewBrushWithGlobalcolorBrushstyle(ui.Qt_black, ui.Qt_SolidPattern)
package gui
// This file handles event timers and imports.
import (
"fmt"
"math"
"time"
"dfss/dfssd/api"
"github.com/visualfc/goqt/ui"
)
// TEMPORARY
const quantum = 100 // discretization argument for events (ns)
const speed = 1000 // duration of a quantum (ms)
// AddEvent interprets an incoming event into a graphic one.
// Expected format:
//
// Timestamp: unix nano timestamp
// Identifier: either "platform", "ttp" or "<email>"
// Log: one of the following
// "sent promise to <email>"
// "sent signature to <email>"
//
// Other messages are currently ignored.
func (w *Window) AddEvent(e *api.Log) {
event := Event{
Sender: w.scene.identifierToIndex(e.Identifier),
Date: time.Unix(0, e.Timestamp),
}
w.Log(fmt.Sprint(e.Identifier, " ", e.Log))
var receiver string
if n, _ := fmt.Sscanf(e.Log, "sent promise to %s", &receiver); n > 0 {
event.Type = PROMISE
event.Receiver = w.scene.identifierToIndex(receiver)
} else if n, _ := fmt.Sscanf(e.Log, "sent signature to %s", &receiver); n > 0 {
event.Type = SIGNATURE
event.Receiver = w.scene.identifierToIndex(receiver)
}
if receiver != "" {
w.scene.Events = append(w.scene.Events, event)
}
}
// DrawEvent triggers the appropriate draw action for a spectific event.
func (w *Window) DrawEvent(e *Event) {
xa, ya := w.GetClientPosition(e.Sender)
xb, yb := w.GetClientPosition(e.Receiver)
var color string
switch e.Type {
case PROMISE:
color = "blue"
case SIGNATURE:
color = "green"
default:
color = "black"
}
w.DrawArrow(xa, ya, xb, yb, colors[color])
}
// PrintQuantumInformation triggers the update of the "x / y" quantum information.
func (w *Window) PrintQuantumInformation() {
if len(w.scene.Events) == 0 {
w.progress.SetText("No event")
return
}
beginning := w.scene.Events[0].Date.UnixNano()
totalDuration := w.scene.Events[len(w.scene.Events)-1].Date.UnixNano() - beginning
nbQuantum := math.Max(1, math.Ceil(float64(totalDuration)/quantum))
durationFromBeginning := w.scene.currentTime.UnixNano() - beginning
currentQuantum := math.Ceil(float64(durationFromBeginning)/quantum) + 1
if w.scene.currentEvent == 0 {
currentQuantum = 0
}
w.progress.SetText(fmt.Sprint(currentQuantum, " / ", nbQuantum))
}
// initTimer is called during window initialization. It initializes the timeout signal called for each refresh.
func (w *Window) initTimer() {
w.timer = ui.NewTimerWithParent(w)
lastNbOfClients := len(w.scene.Clients)
w.timer.OnTimeout(func() {
nbEvents := len(w.scene.Events)
if w.scene.currentEvent >= nbEvents {
w.replayButton.Click()
return
}
// Remove arrows from last tick
w.RemoveArrows()
// Check that we have a least one event to read
if nbEvents == 0 {
return
}
// Check if need to redraw everything
if lastNbOfClients != len(w.scene.Clients) {
w.initScene()
}
// Init first time
if w.scene.currentEvent == 0 {
w.scene.currentTime = w.scene.Events[0].Date
}
endOfQuantum := w.scene.currentTime.Add(quantum * time.Nanosecond)
for i := w.scene.currentEvent; i < nbEvents; i++ {
e := w.scene.Events[i]
if e.Date.After(endOfQuantum) || e.Date.Equal(endOfQuantum) {
break
}
w.DrawEvent(&e)
w.scene.currentEvent++
}
w.PrintQuantumInformation()
w.scene.currentTime = endOfQuantum
})
}
// identifierToIndex is used to retrieve a client index from its name, inserting a new client if needed.
func (s *Scene) identifierToIndex(identifier string) int {
if identifier == "platform" {
return -1
}
if identifier == "ttp" {
return -2
}
for i, c := range s.Clients {
if c.Name == identifier {
return i
}
}
s.Clients = append(s.Clients, Client{Name: identifier})
return len(s.Clients) - 1
}
package gui
// This file handles complex graphic primitives for the demonstrator.
import (
"math"
"github.com/visualfc/goqt/ui"
)
// These two constants are used to configure arrows
const ARROW_T = math.Pi / 6 // angle
const ARROW_L = 15 // side length
// DrawClients draws the different clients in a circle.
func (w *Window) DrawClients() {
scene := w.graphics.Scene()
for i, c := range w.scene.Clients {
x, y := w.GetClientPosition(i)
// Add ellipse
scene.AddEllipseFWithXYWidthHeightPenBrush(x-10, y-10, 20, 20, pen_black, brush_black)
// Add text
t := scene.AddSimpleText(c.Name)
r := t.BoundingRect()
t.SetX(x - r.Width()/2)
t.SetY(y + 10)
}
}
// GetClientPosition translates a client index into its cartesian coordinates.
func (w *Window) GetClientPosition(i int) (x, y float64) {
if i < 0 {
return w.GetServerPosition(i == -1)
}
nbClients := float64(len(w.scene.Clients))
angle := 2 * math.Pi * float64(i) / nbClients
return math.Cos(angle) * (w.circleSize / 2), math.Sin(angle) * (w.circleSize / 2)
}
// GetServerPosition translates a server into its cartesian coordinates.
func (w *Window) GetServerPosition(platform bool) (x, y float64) {
x = w.circleSize/2 + 150
y = 0
if !platform {
x *= -1
}
return
}
// DrawServers draws the DFSS main servers (ttp and platform)
func (w *Window) DrawServers() {
scene := w.graphics.Scene()
ttp := scene.AddPixmap(w.pixmaps["ttp"])
x, y := w.GetServerPosition(false)
ttp.SetPosFWithXY(x-32, y-16) // we are shifting here a bit for better arrow display
ttp.SetToolTip("TTP")
platform := scene.AddPixmap(w.pixmaps["platform"])
x, y = w.GetServerPosition(true)
platform.SetPosFWithXY(x, y-16)
platform.SetToolTip("Platform")
}
// DrawArrow is the graphic primitive for drawing an arrow between A and B points
func (w *Window) DrawArrow(xa, ya, xb, yb float64, rgb uint32) {
scene := w.graphics.Scene()
path := ui.NewPainterPath()
path.MoveToFWithXY(xa, ya)
path.LineToFWithXY(xb, yb)
v := ui.NewVector2DWithXposYpos(xa-xb, ya-yb)
l := v.Length()
// from http://math.stackexchange.com/a/1314050
xc := xb + ARROW_L/l*(v.X()*math.Cos(ARROW_T)+v.Y()*math.Sin(ARROW_T))
yc := yb + ARROW_L/l*(v.Y()*math.Cos(ARROW_T)-v.X()*math.Sin(ARROW_T))
xd := xb + ARROW_L/l*(v.X()*math.Cos(ARROW_T)-v.Y()*math.Sin(ARROW_T))
yd := yb + ARROW_L/l*(v.Y()*math.Cos(ARROW_T)+v.X()*math.Sin(ARROW_T))
path.LineToFWithXY(xc, yc)
path.LineToFWithXY(xd, yd)
path.LineToFWithXY(xb, yb)
path.SetFillRule(ui.Qt_WindingFill)
color := ui.NewColorWithRgb(rgb)
color.SetAlpha(200)
pen := ui.NewPenWithColor(color)
pen.SetWidth(3)
pen.SetJoinStyle(ui.Qt_RoundJoin)
brush := ui.NewBrush()
brush.SetColor(color)
brush.SetStyle(ui.Qt_SolidPattern)
arrow := scene.AddPathWithPathPenBrush(path, pen, brush)
w.currentArrows = append(w.currentArrows, arrow)
}
// RemoveArrows remove every arrow present in the graphic area, and delete them for better memory management.
func (w *Window) RemoveArrows() {
scene := w.graphics.Scene()
for _, arrow := range w.currentArrows {
scene.RemoveItem(&arrow.QGraphicsItem)
defer arrow.Delete()
}
w.currentArrows = nil
}
package gui
// This file handles open/save feature.
import (
"encoding/json"
"io/ioutil"
"strconv"
)
func (w *Window) Save(filename string) {
data, err := json.Marshal(w.scene)
if err != nil {
w.StatusBar().ShowMessage(err.Error())
return
}
err = ioutil.WriteFile(filename, data, 0600)
if err != nil {
w.StatusBar().ShowMessage(err.Error())
return
}
w.StatusBar().ShowMessage("Saved file as " + filename)
}
func (w *Window) Open(filename string) {
data, err := ioutil.ReadFile(filename)
if err != nil {
w.StatusBar().ShowMessage(err.Error())
return
}
newScene := &Scene{}
err = json.Unmarshal(data, newScene)
if err != nil {
w.StatusBar().ShowMessage(err.Error())
return
}
w.scene = newScene
w.StatusBar().ShowMessage("Imported file from " + filename + " (" + strconv.Itoa(len(w.scene.Events)) + " events)")
w.initScene()
}
package gui
// This file stores strucutures used in GUI for fast documentation.
import (
"time"
"github.com/visualfc/goqt/ui"
)
// Window contains all information used to make the demonstrator works.
// It extends QMainWindow and cache several graphic informations.
// Do not attempt to instantiante it directly, use `NewWindow` function instead.
type Window struct {
*ui.QMainWindow
logField *ui.QTextEdit
graphics *ui.QGraphicsView
progress *ui.QLabel
playButton *ui.QPushButton
stopButton *ui.QPushButton
replayButton *ui.QPushButton
scene *Scene
circleSize float64
pixmaps map[string]*ui.QPixmap
currentArrows []*ui.QGraphicsPathItem
timer *ui.QTimer
}
// Client represents a DFSSC instance
type Client struct {
Name string
}
// EventType is used as an enum for event types, to differenciate promises, signatures...
type EventType int
const (
PROMISE EventType = iota
SIGNATURE
OTHER
)
// Event represents a single signature event
type Event struct {
Type EventType
Sender int
Receiver int
Date time.Time
}
// Scene holds the global scene for registered clients and signature events
type Scene struct {
Clients []Client
Events []Event
currentTime time.Time
currentEvent int
}
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CalculatorForm</class>
<widget class="QWidget" name="CalculatorForm">
<class>Demonstrator</class>
<widget class="QWidget" name="Demonstrator">
<property name="enabled">
<bool>true</bool>
</property>
......@@ -39,6 +39,15 @@
<height>300</height>
</size>
</property>
<property name="backgroundBrush">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</property>
</widget>
</item>
<item>
......@@ -57,10 +66,32 @@
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Play</string>
<property name="maximumSize">
<size>
<width>40</width>
<height>40</height>
</size>
</property>
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="stopButton">
<property name="maximumSize">
<size>
<width>40</width>
<height>40</height>
</size>
</property>
<property name="default">
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
......@@ -73,8 +104,17 @@
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Replay</string>
<property name="maximumSize">
<size>
<width>40</width>
<height>40</height>
</size>
</property>
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
......@@ -99,6 +139,9 @@
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="focusPolicy">
<enum>Qt::TabFocus</enum>
</property>
<property name="suffix">
<string> us</string>
</property>
......@@ -132,11 +175,14 @@
<item>
<widget class="QSlider" name="speedSlider">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="focusPolicy">
<enum>Qt::TabFocus</enum>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
......@@ -148,6 +194,22 @@
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="progressLabel">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
......
// Package gui is the graphic part of the dfssd program.
package gui
// This file is the entry point of the gui package.
// It handles window instantiation and basic operations on it.
import (
"math"
"time"
"dfss"
"github.com/visualfc/goqt/ui"
)
type Window struct {
*ui.QMainWindow
logField *ui.QTextEdit
}
// NewWindow creates and initialiaze a new dfssd main window.
func NewWindow() *Window {
file := ui.NewFileWithName(":/widget.ui")
loader := ui.NewUiLoader()
......@@ -23,32 +25,138 @@ func NewWindow() *Window {
w := &Window{
QMainWindow: window,
scene: &Scene{},
}
w.InstallEventFilter(w)
// Load dynamic elements from driver
w.logField = ui.NewTextEditFromDriver(widget.FindChild("logField"))
w.graphics = ui.NewGraphicsViewFromDriver(widget.FindChild("graphicsView"))
w.progress = ui.NewLabelFromDriver(widget.FindChild("progressLabel"))
w.playButton = ui.NewPushButtonFromDriver(widget.FindChild("playButton"))
w.stopButton = ui.NewPushButtonFromDriver(widget.FindChild("stopButton"))
w.replayButton = ui.NewPushButtonFromDriver(widget.FindChild("replayButton"))
// Load pixmaps
w.pixmaps = map[string]*ui.QPixmap{
"ttp": ui.NewPixmapWithFilenameFormatFlags(":/images/server_key.png", "", ui.Qt_AutoColor),
"platform": ui.NewPixmapWithFilenameFormatFlags(":/images/server_connect.png", "", ui.Qt_AutoColor),
}
// Load icons
w.addIcons()
// Add actions
w.addActions()
w.initScene()
w.initTimer()
w.StatusBar().ShowMessage("Ready")
w.PrintQuantumInformation()
return w
}
// OnResizeEvent is called by Qt each time an user tries to resize the window.
// We have to redraw the whole scene to adapt.
func (w *Window) OnResizeEvent(ev *ui.QResizeEvent) bool {
w.initScene()
return true