#!/usr/bin/python3
# -*- coding: utf-8 -*-
# pylint: disable=too-many-lines
'''Pychemqt, Chemical Engineering Process simulator
Copyright (C) 2009-2025, Juan José Gómez Romera <jjgomera@gmail.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Module for graphics elements in PFD, defining the main qt graphics class:
* :class:`GraphicsView`: PFD viewer
* :class:`GraphicsScene`: PFD scene to manage the graphical elements
and the different graphical items:
* :class:`GeometricItem`: Common functionality for geometric PFD elements
* :class:`RectItem`: Class to plot a rectangular item
* :class:`EllipseItem`: Class to plot a circle or oval item
* :class:`TextItem`: Class to plot a text item
* :class:`GraphicsEntity`: Common functionality for Entity in PFD
* :class:`StreamItem`: Class to plot a mass stream
* :class:`EquipmentItem`: Class to plot equipment item
Dialog related defined:
* :class:`SelectStreamProject`: Dialog to select a stream from a project
* :class:`TextItemDlg`: Dialog to edit texts in PFD
'''
from ast import literal_eval
from copy import deepcopy
from datetime import datetime
from functools import partial
import json
import os
from random import uniform
import subprocess
import tempfile
from xml.dom import minidom
try:
import ezodf
except ImportError:
pass
from lib import unidades
from lib.config import Preferences, IMAGE_PATH
from lib.corriente import Corriente
from lib.EoS import K, H
from lib.project import Project
from lib.thread import WaitforClick
from UI import texteditor, UI_corriente
from UI.plots import Plot_Distribucion
from UI.prefPFD import ConfLine, ConfLineDialog, Dialog, BrushCombo
from UI.widgets import createAction, Table_Graphics, PathConfig, ClickableLabel
from equipment import flux, spreadsheet, UI_equipments
from equipment.parents import equipment
from tools import UI_confResolution, UI_confThermo
from tools.qt import QtCore, QtGui, QtSvgWidgets, QtWidgets, translate
[docs]
class GraphicsView(QtWidgets.QGraphicsView):
"""Class for PFD representation"""
mouseMove = QtCore.pyqtSignal(QtCore.QPointF)
zoomChanged = QtCore.pyqtSignal(int)
[docs]
def __init__(self, PFD=True, parent=None):
"""
Parameters
----------
PFD : boolean
Set if this GraphicsView is the PFD of project, in that case the
windows can't be closed
"""
super().__init__(parent)
self.PFD = PFD
self.setDragMode(QtWidgets.QGraphicsView.DragMode.RubberBandDrag)
self.setRenderHints(QtGui.QPainter.RenderHint.Antialiasing
| QtGui.QPainter.RenderHint.TextAntialiasing)
brushColor = Preferences.get("PFD", "brushColor")
brushPattern = BrushCombo.BRUSH[Preferences.getint("PFD", "brush")]
self.setBackgroundBrush(
QtGui.QBrush(QtGui.QColor(brushColor), brushPattern))
self.setMouseTracking(True)
self.setAcceptDrops(True)
# Widgets to show in the statusbar of mainwindow
self.statusWidget = []
self.statusPosition = ClickableLabel()
self.statusPosition.setFrameShape(QtWidgets.QFrame.Shape.WinPanel)
self.statusPosition.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)
self.statusWidget.append(self.statusPosition)
self.statusThermo = ClickableLabel()
self.statusThermo.setFrameShape(QtWidgets.QFrame.Shape.WinPanel)
self.statusThermo.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)
self.statusWidget.append(self.statusThermo)
self.statusPosition.clicked.connect(
partial(parent.dialogConfig, UI_confResolution))
self.statusThermo.clicked.connect(
partial(parent.dialogConfig, UI_confThermo))
self.mouseMove.connect(self.updatePosition)
self.leaveEvent()
[docs]
def mouseMoveEvent(self, event):
"""Reimplemented to print the mouse position on statusbar"""
QtWidgets.QGraphicsView.mouseMoveEvent(self, event)
self.mouseMove.emit(self.mapToScene(event.pos()))
[docs]
def mousePressEvent(self, event):
"""Reimplement to use in a overview window the mouse click position as
the center position in the real PFD window"""
QtWidgets.QGraphicsView.mousePressEvent(self, event)
if not self.PFD:
self.scene().views()[0].centerOn(self.mapToScene(event.pos()))
[docs]
def wheelEvent(self, event):
"""Change zoom of window, only work with PFD window, not in overview"""
if self.PFD:
ratio = 1.41 ** (-event.angleDelta().y() / 240.0)
self.scale(ratio, ratio)
self.zoomChanged.emit(int(self.transform().m11()*100))
[docs]
def leaveEvent(self, event=None):
"""Reimplement to set value of status position to the total size"""
x = Preferences.getint("PFD", "x")
y = Preferences.getint("PFD", "y")
self.statusPosition.setText(f"{x}, {y}")
QtWidgets.QGraphicsView.leaveEvent(self, event)
[docs]
def zoom(self, value):
"""Apply zoom value to a pfd view"""
value /= 100.0
self.resetTransform()
self.scale(value, value)
[docs]
def dragMoveEvent(self, event):
"""Acept drag event"""
if event.mimeData().hasFormat("application/x-equipment"):
event.setDropAction(QtCore.Qt.DropAction.CopyAction)
event.accept()
else:
event.ignore()
[docs]
def dropEvent(self, event):
"""Do drop event simulating a click in the button and using the drop
point"""
if event.mimeData().hasFormat("application/x-equipment"):
event.source().clicked.emit()
self.scene().Pos.append(self.mapToScene(event.position().toPoint()))
event.source().setDown(False)
else:
event.ignore()
[docs]
def updatePosition(self, event):
"""Update text with mouse position"""
self.statusPosition.setText(f"{int(event.x())}, {int(event.y())}")
[docs]
def changeStatusThermo(self, config):
"""Show thermodynamic option in statusbar"""
if config.has_section("Thermo") and \
config.has_section("Components"):
components = literal_eval(config.get("Components", "components"))
if config.getboolean("Thermo", "iapws") and \
config.getboolean("Thermo", "freesteam") and \
len(components) == 1 and components[0] == 62:
txt = "Freesteam"
elif config.getboolean("Thermo", "iapws") and \
len(components) == 1 and components[0] == 62:
txt = "iapws97"
elif config.getboolean("Thermo", "meos") and \
config.getboolean("Thermo", "refprop"):
txt = "Refprop"
elif config.getboolean("Thermo", "meos") and \
config.getboolean("Thermo", "coolprop"):
txt = "CoolProp"
elif config.getboolean("Thermo", "meos"):
txt = "MEoS"
else:
txt = f"K: {K[config.getint('Thermo', 'K')].__status__}"
txt += f"H: {H[config.getint('Thermo', 'H')].__status__}"
self.statusThermo.setText(txt)
[docs]
class GraphicsScene(QtWidgets.QGraphicsScene):
"""Graphics PFD scene too manage the graphical elements"""
copiedItem = QtCore.QByteArray()
pasteOffset = 5
points = []
addObj = False
addType = ""
project = Project()
Pos = []
obj = None
objects = {"txt": [], "square": [], "ellipse": [], "stream": {}, "in": {},
"out": {}, "equip": {}}
[docs]
def __init__(self, parent=None):
super().__init__(parent)
self.popup = Table_Graphics()
proxy = QtWidgets.QGraphicsProxyWidget()
proxy.setWidget(self.popup)
self.addItem(proxy)
self.popup.hide()
[docs]
def mousePressEvent(self, event):
"""Save event position to locate item when add"""
QtWidgets.QGraphicsScene.mousePressEvent(self, event)
if self.addObj and self.addType != "stream":
self.Pos.append(event.scenePos())
[docs]
def addActions(self, menu, pos=None):
"""Define actions and state of its to add to context menu"""
actionCut, actionCopy, actionPaste, actionDelete = self.defineShortcut(pos)
menu.addAction(self.tr("Redraw"), self.update)
menu.addAction(
QtGui.QIcon(os.path.join(IMAGE_PATH, "button", "configure.png")),
self.tr("Configure"),
self.configure)
menu.addSeparator()
menu.addAction(createAction(
self.tr("Select All"),
slot=self.selectAll,
shortcut=QtGui.QKeySequence.StandardKey.SelectAll,
icon=os.path.join("button", "selectAll.png"), parent=self))
menu.addSeparator()
menu.addAction(actionCut)
menu.addAction(actionCopy)
menu.addAction(actionPaste)
menu.addAction(actionDelete)
actionDeleteAll = createAction(
self.tr("Delete All"),
slot=self.delete,
shortcut=QtGui.QKeySequence.StandardKey.Delete,
icon=os.path.join("button", "editDelete.png"), parent=self)
menu.addAction(actionDeleteAll)
menu.addSeparator()
if self.copiedItem.isEmpty():
actionPaste.setEnabled(False)
items = self.selectedItems()
if not items:
actionCut.setEnabled(False)
actionCopy.setEnabled(False)
actionDelete.setEnabled(False)
actionDeleteAll.setEnabled(False)
for item in items:
menuEl = item.contextMenu()
menu.addAction(menuEl.menuAction())
menu.addSeparator()
return menu
[docs]
def defineShortcut(self, pos=None):
"""Add actions to QGraphicsView to enable keyboard shortcut"""
# Delete actions of last run
for action in self.views()[0].actions():
self.views()[0].removeAction(action)
# Add actions to QGraphicsView to enable key shortcut
actionCut = createAction(
self.tr("Cut"),
slot=self.cut,
shortcut=QtGui.QKeySequence.StandardKey.Cut,
icon=os.path.join("button", "editCut.png"), parent=self)
actionCopy = createAction(
self.tr("Copy"),
slot=self.copy,
shortcut=QtGui.QKeySequence.StandardKey.Copy,
icon=os.path.join("button", "editCopy.png"), parent=self)
actionPaste = createAction(
self.tr("Paste"),
slot=partial(self.paste, pos),
shortcut=QtGui.QKeySequence.StandardKey.Paste,
icon=os.path.join("button", "editPaste.png"), parent=self)
actionDelete = createAction(
self.tr("Delete"),
slot=self.delete,
shortcut=QtGui.QKeySequence.StandardKey.Delete,
icon=os.path.join("button", "remove.png"), parent=self)
self.views()[0].addAction(actionCopy)
self.views()[0].addAction(actionCut)
self.views()[0].addAction(actionPaste)
self.views()[0].addAction(actionDelete)
return actionCut, actionCopy, actionPaste, actionDelete
[docs]
def selectAll(self):
"""Select all itemin scene"""
for item in list(self.items()):
item.setSelected(True)
[docs]
def copy(self, items=None):
"""Copy selected items to internal clickboard, StreamItem is not
supported"""
if not items:
items = self.selectedItems()
self.copiedItem.clear()
self.pasteOffset = 50
st = QtCore.QDataStream(
self.copiedItem, QtCore.QIODevice.OpenModeFlag.WriteOnly)
st.writeInt32(len(items))
for item in items:
self.writeItemToStream(st, item)
[docs]
def cut(self, items=None):
"""Copy selected items to internal clickboard and delete of scene"""
if not items:
items = self.selectedItems()
self.copy(items)
self.delete(items)
[docs]
def paste(self, pos=None):
"""Paste item saved in internal clipboard to the scene"""
st = QtCore.QDataStream(
self.copiedItem, QtCore.QIODevice.OpenModeFlag.ReadOnly)
count = st.readInt32()
for it in range(count):
item = self.readItemFromStream(st)
# Discard stream item
if isinstance(item, StreamItem):
continue
if pos:
item.setPos(pos)
else:
print(item.pos().x())
offset = QtCore.QPointF(
uniform(-item.pos().x(), item.pos().x()),
uniform(-item.pos().y(), item.pos().y()))
item.setPos(item.pos() + offset)
self.pasteOffset += 5
self.addItem(item)
# Add new equipment to project
if item.tipo == "e":
if item.name == "in":
self.project.copyInput(item.id-1)
elif item.name == "out":
self.project.setOutput(item.id, Corriente())
else:
self.project.copyItem(item.id-1)
[docs]
def delete(self, items=None):
"""Delete items to the PFD"""
if not items:
items = self.selectedItems()
if not isinstance(items, list):
items = [items]
for item in items:
tipo = item.tipo
# FIXME: Unify name convention to avoid use this annoying map
mapa = {"e": "equip",
"i": "in",
"o": "out",
"stream": "stream"}
if tipo in ["stream", "e", "i", "o"]:
del self.objects[mapa[tipo]][item.id]
item.postDelete()
else:
self.objects[tipo].remove(item)
self.removeItem(item)
self.update()
self.parent().list.updateList(self.objects)
[docs]
def waitClick(self, numClick, tipo, obj):
"""Init wait for click instance to get click mouse user interaction"""
self.Pos = []
self.obj = obj
self.addType = tipo
self.addObj = True
self.views()[0].viewport().setCursor(
QtGui.QCursor(QtCore.Qt.CursorShape.CrossCursor))
self.parent().statusBar().showMessage(
self.tr("Click in desire text position in screen"))
clickCollector = WaitforClick(numClick, self)
clickCollector.finished.connect(self.click)
clickCollector.start()
[docs]
def click(self):
"""Actions to do when user finished a mouse interaction action"""
if self.addType in ["equip", "in", "out", "txt"]:
self.obj.setPos(self.Pos[0])
elif self.addType == "stream":
self.obj.up = self.up
self.obj.down = self.down
self.up.down = self.up.down + [self.obj]
self.down.up = self.down.up + [self.obj]
self.up.down_used += 1
self.down.up_used += 1
self.obj.Ang_entrada = self.points[0].direction
self.obj.Ang_salida = self.points[1].direction
self.obj.redraw(self.Pos[0], self.Pos[1])
# Unchecked stream button in toolbox
self.parent().botonCorriente.setChecked(False)
elif self.addType in ["square", "ellipse"]:
rect = QtCore.QRectF(self.Pos[0], self.Pos[1])
self.obj.setRect(rect)
self.addItem(self.obj)
if self.addType == "equip":
self.project.addItem(
f"e{self.obj.id}", self.obj.dialogo.Equipment)
elif self.addType == "in":
self.project.addItem(
f"i{self.obj.id}", self.obj.dialogo.corriente)
elif self.addType == "out":
self.project.addItem(
f"o{self.obj.id}", self.obj.dialogo.corriente)
elif self.addType == "stream":
self.project.addStream(
self.obj.id, f"{self.up.tipo}{self.up.id}",
f"{self.down.tipo}{self.down.id}", Corriente(),
self.up.down_used - 1, self.down.up_used - 1)
self.parent().dirty[self.parent().idTab] = True
self.parent().saveControl()
self.update()
self.obj.setSelected(True)
self.parent().statusBar().clearMessage()
self.addObj = False
if self.addType in ("txt", "square", "ellipse"):
self.objects[self.addType].append(self.obj)
else:
idx = self.obj.id
self.objects[self.addType][idx] = self.obj
self.parent().list.updateList(self.objects)
self.views()[0].viewport().setCursor(
QtGui.QCursor(QtCore.Qt.CursorShape.ArrowCursor))
# for item in self.items():
# if isinstance(item, QtSvg.QGraphicsSvgItem):
# item.hoverLeaveEvent(None)
# item.setAcceptHoverEvents(False)
[docs]
def writeItemToStream(self, stream, item):
"""Save item as a QDataStream in memory to paste in PFD is requested"""
stream.writeQString(item.tipo)
stream << item.transform() << item.pos()
if isinstance(item, TextItem):
stream.writeQString(item.toHtml())
elif isinstance(item, (RectItem, EllipseItem)):
stream << item.rect() << item.pen()
elif isinstance(item, EquipmentItem):
stream.writeQString(item.name)
if item.tipo == "e":
stream.writeInt32(item.dialogoId)
[docs]
def readItemFromStream(self, stream):
"""Read item from internal clickboard and create item"""
tipo = stream.readQString()
matrix = QtGui.QTransform()
stream >> matrix
pos = QtCore.QPointF()
stream >> pos
if tipo == "txt":
txt = stream.readQString()
item = TextItem(txt)
elif tipo == "square":
rect = QtCore.QRectF()
pen = QtGui.QPen()
stream >> rect >> pen
item = RectItem()
item.setRect(rect)
item.setPen(pen)
elif tipo == "ellipse":
rect = QtCore.QRectF()
pen = QtGui.QPen()
stream >> rect >> pen
item = EllipseItem()
item.setRect(rect)
item.setPen(pen)
elif tipo in ("e", "i", "o"):
name = stream.readQString()
if tipo == "e":
dialogoid = stream.readInt32()
else:
dialogoid = None
item = EquipmentItem(name, dialogoid)
elif tipo == "stream":
item = StreamItem()
item.setTransform(matrix)
item.setPos(pos)
return item
[docs]
def readFromJSON(self, data):
"""Read scene object from project file"""
self.objects = deepcopy(GraphicsScene.objects)
for text in data["PFD"]["txt"].values():
txt = text["txt"]
s = TextItem(txt)
x = text["x"]
y = text["y"]
pos = QtCore.QPoint(x, y)
s.setPos(pos)
self.objects["txt"].append(s)
self.addItem(s)
for obj in data["PFD"]["square"].values():
s = RectItem()
x = obj["x"]
y = obj["y"]
width = obj["width"]
height = obj["height"]
rect = QtCore.QRect(x, y, width, height)
s.setRect(rect)
pen = QtGui.QPen(QtGui.QColor(obj["color"]))
pen.setWidthF(obj["width"])
pen.setJoinStyle(ConfLine.join[obj["joinStyle"]])
pen.setMiterLimit(obj["miterLimit"])
pen.setCapStyle(ConfLine.cap[obj["capStyle"]])
pen.setStyle(ConfLine.line[obj["style"]])
pen.setDashOffset(obj["dashOffset"])
s.setPen(pen)
self.objects["square"].append(s)
self.addItem(s)
for obj in data["PFD"]["ellipse"].values():
s = EllipseItem()
x = obj["x"]
y = obj["y"]
width = obj["width"]
height = obj["height"]
rect = QtCore.QRect(x, y, width, height)
s.setRect(rect)
pen = QtGui.QPen(QtGui.QColor(obj["color"]))
pen.setWidthF(obj["width"])
pen.setJoinStyle(ConfLine.join[obj["joinStyle"]])
pen.setMiterLimit(obj["miterLimit"])
pen.setCapStyle(ConfLine.cap[obj["capStyle"]])
pen.setStyle(ConfLine.line[obj["style"]])
pen.setDashOffset(obj["dashOffset"])
s.setPen(pen)
self.objects["ellipse"].append(s)
self.addItem(s)
id_stream = []
up_stream = {}
down_stream = {}
for idx, obj in data["PFD"]["stream"].items():
idx = int(idx)
s = StreamItem()
in_x = obj["input_x"]
in_y = obj["input_y"]
entrada = QtCore.QPointF(in_x, in_y)
out_x = obj["output_x"]
out_y = obj["output_y"]
salida = QtCore.QPointF(out_x, out_y)
pen = QtGui.QPen(QtGui.QColor(obj["pen"]["color"]))
pen.setWidthF(obj["pen"]["width"])
pen.setJoinStyle(ConfLine.join[obj["pen"]["joinStyle"]])
pen.setMiterLimit(obj["pen"]["miterLimit"])
pen.setCapStyle(ConfLine.cap[obj["pen"]["capStyle"]])
pen.setStyle(ConfLine.line[obj["pen"]["style"]])
pen.setDashOffset(obj["pen"]["dashOffset"])
s.setPen(pen)
up_type = obj["up_type"]
down_type = obj["down_type"]
up_id = obj["up_id"]
down_id = obj["down_id"]
id_stream.append(idx)
up_stream[idx] = up_type, up_id
down_stream[idx] = down_type, down_id
s.id = idx
s.entrada = entrada
s.salida = salida
s.Ang_entrada = obj["input_angle"]
s.Ang_salida = obj["output_angle"]
self.objects["stream"][id_stream[-1]] = s
self.addItem(s)
txt = obj["label"]
x = obj["label_x"]
y = obj["label_y"]
pos = QtCore.QPointF(x, y)
s.idLabel.setPos(pos)
s.idLabel.setHtml(txt)
visible = obj["label_visible"]
s.idLabel.setVisible(visible)
angle_in = {}
for idx, obj in data["PFD"]["in"].items():
idx = int(idx)
s = EquipmentItem("in", None)
x = obj["x"]
y = obj["y"]
pos = QtCore.QPointF(x, y)
s.setPos(pos)
angle_in[idx] = obj["angle"]
down = [self.objects["stream"][obj["down_id"]]]
s.down = down
self.objects["in"][idx] = s
self.addItem(s)
angle_out = {}
for idx, obj in data["PFD"]["out"].items():
idx = int(idx)
s = EquipmentItem("out", None)
x = obj["x"]
y = obj["y"]
pos = QtCore.QPointF(x, y)
s.setPos(pos)
angle_out[idx] = obj["angle"]
up = [self.objects["stream"][obj["up_id"]]]
s.up = up
self.objects["out"][idx] = s
self.addItem(s)
angle_equip = {}
for idx, obj in data["PFD"]["equip"].items():
idx = int(idx)
name = obj["name"]
dialogoId = obj["dialogo_id"]
s = EquipmentItem(name, dialogoId)
s.id = idx
x = obj["x"]
y = obj["y"]
pos = QtCore.QPointF(x, y)
s.setPos(pos)
angle_equip[idx] = obj["angle"]
up = [self.objects["stream"][i] for i in obj["up_id"]]
s.up = up
down = [self.objects["stream"][i] for i in obj["down_id"]]
s.down = down
self.objects["equip"][idx] = s
self.addItem(s)
txt = obj["label"]
x = obj["label_x"]
y = obj["label_y"]
pos = QtCore.QPointF(x, y)
s.idLabel.setPos(pos)
s.idLabel.setHtml(txt)
visible = obj["label_visible"]
s.idLabel.setVisible(visible)
for idx in id_stream:
tipo, i = up_stream[idx]
self.objects["stream"][idx].up = self.getObject(tipo, i)
tipo, i = down_stream[idx]
self.objects["stream"][idx].down = self.getObject(tipo, i)
self.objects["stream"][idx].redraw()
for idx, angle in angle_in.items():
self.objects["in"][idx].rotate(angle)
for idx, angle in angle_out.items():
self.objects["out"][idx].rotate(angle)
for idx, angle in angle_equip.items():
self.objects["equip"][idx].rotate(angle)
[docs]
def writeToJSON(self, data):
"""Save json format to write to file"""
txts = {}
for i, obj in enumerate(self.objects["txt"]):
txt = {}
txt["txt"] = obj.toHtml()
txt["x"] = obj.pos().x()
txt["y"] = obj.pos().y()
txts[i] = txt
data["txt"] = txts
squares = {}
for i, obj in enumerate(self.objects["square"]):
square = {}
square["x"] = obj.rect().x()
square["y"] = obj.rect().y()
square["width"] = obj.rect().width()
square["height"] = obj.rect().height()
square["pen"] = obj.getPen()
squares[i] = square
data["square"] = squares
ellipses = {}
for i, obj in enumerate(self.objects["ellipse"]):
ellipse = {}
ellipse["x"] = obj.rect().x()
ellipse["y"] = obj.rect().y()
ellipse["width"] = obj.rect().width()
ellipse["height"] = obj.rect().height()
ellipse["pen"] = obj.getPen()
ellipses[i] = ellipse
data["ellipse"] = ellipses
streams = {}
for idx, obj in self.objects["stream"].items():
stream = {}
stream["input_x"] = obj.entrada.x()
stream["input_y"] = obj.entrada.y()
stream["output_x"] = obj.salida.x()
stream["output_y"] = obj.salida.y()
stream["pen"] = obj.getPen()
stream["up_id"] = obj.up.id
stream["up_type"] = obj.up.tipo
stream["down_id"] = obj.down.id
stream["down_type"] = obj.down.tipo
stream["input_angle"] = obj.Ang_entrada
stream["output_angle"] = obj.Ang_salida
stream["label"] = obj.idLabel.toHtml()
stream["label_x"] = obj.idLabel.pos().x()
stream["label_y"] = obj.idLabel.pos().y()
stream["label_visible"] = int(obj.idLabel.isVisible())
streams[idx] = stream
data["stream"] = streams
ins = {}
for idx, obj in self.objects["in"].items():
in_ = {}
in_["x"] = obj.x()
in_["y"] = obj.y()
in_["angle"] = obj.angle
if obj.down:
in_["down_id"] = obj.down[0].id
else:
in_["down_id"] = None
ins[idx] = in_
data["in"] = ins
outs = {}
for idx, obj in self.objects["out"].items():
out = {}
out["x"] = obj.x()
out["y"] = obj.y()
out["angle"] = obj.angle
if obj.up:
out["up_id"] = obj.up[0].id
else:
out["up_id"] = None
outs[idx] = out
data["out"] = outs
equipments = {}
for idx, obj in self.objects["equip"].items():
equip = {}
equip["name"] = obj.name
equip["dialogo_id"] = obj.dialogoId
equip["x"] = obj.pos().x()
equip["y"] = obj.pos().y()
equip["angle"] = obj.angle
ups = []
for up in obj.up:
ups.append(up.id)
equip["up_id"] = ups
downs = []
for down in obj.down:
downs.append(down.id)
equip["down_id"] = downs
equip["label"] = obj.idLabel.toHtml()
equip["label_x"] = obj.idLabel.pos().x()
equip["label_y"] = obj.idLabel.pos().y()
equip["label_visible"] = int(obj.idLabel.isVisible())
equipments[idx] = equip
data["equip"] = equipments
[docs]
def getObject(self, tipo, idx):
"""Return the object of project"""
if tipo == "e":
lista = self.objects["equip"]
elif tipo == "i":
lista = self.objects["in"]
elif tipo == "o":
lista = self.objects["out"]
elif tipo == "s":
lista = self.objects["stream"]
else:
raise ValueError
return lista[idx]
[docs]
class GeometricItem(QtWidgets.QAbstractGraphicsShapeItem):
"""Generic class with common functionality for geometric PFD elements"""
tipo = ""
icon = None
[docs]
def __init__(self, parent=None):
super().__init__(parent)
self.setPen(self._pen())
self.setFlags(
QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable)
self.setZValue(-1)
[docs]
def _pen(self):
"""Load pen properties from preferences"""
pen = QtGui.QPen(QtGui.QColor(Preferences.get("PFD", 'Color_Stream')))
pen.setWidthF(Preferences.getfloat("PFD", 'Width'))
pen.setJoinStyle(ConfLine.join[Preferences.getint("PFD", 'Union')])
pen.setMiterLimit(Preferences.getfloat("PFD", 'Miter_limit'))
pen.setCapStyle(ConfLine.cap[Preferences.getint("PFD", 'Punta')])
pen.setStyle(ConfLine.line[Preferences.getint("PFD", 'Guion')])
pen.setDashOffset(Preferences.getfloat("PFD", 'Dash_offset'))
return pen
[docs]
def getPen(self):
"""Get item pen properties in a json serializable format"""
pen = {}
pen["color"] = self.pen().color().name()
pen["width"] = self.pen().widthF()
pen["joinStyle"] = ConfLine.join.index(self.pen().joinStyle())
pen["miterLimit"] = self.pen().miterLimit()
pen["capStyle"] = ConfLine.cap.index(self.pen().capStyle())
pen["style"] = ConfLine.line.index(self.pen().style())
pen["dashOffset"] = self.pen().dashOffset()
return pen
[docs]
def delete(self):
"""Delete item from scene"""
self.scene().delete(self)
[docs]
def itemChange(self, key, value):
"""Set mainwindow current project modified at any change of item"""
if self.scene() and key == \
QtWidgets.QGraphicsItem.GraphicsItemChange.ItemPositionChange:
mainwindow = self.scene().parent()
if not mainwindow.dirty[mainwindow.idTab]:
mainwindow.dirty[mainwindow.idTab] = True
mainwindow.activeControl(True)
mainwindow.tabModified(mainwindow.idTab)
return QtWidgets.QGraphicsItem.itemChange(self, key, value)
[docs]
def keyPressEvent(self, event):
"""Do action when arrows keys are pressed"""
factor = Preferences.getint("PFD", 'move_factor')
if event.modifiers() & QtCore.Qt.KeyboardModifier.ShiftModifier:
if event.key() == QtCore.Qt.Key.Key_Up:
rect = self.rect()
rect.setBottom(self.rect().bottom() - factor)
self.setRect(rect)
elif event.key() == QtCore.Qt.Key.Key_Down:
rect = self.rect()
rect.setBottom(self.rect().bottom() + factor)
self.setRect(rect)
elif event.key() == QtCore.Qt.Key.Key_Left:
rect = self.rect()
rect.setRight(self.rect().right() - factor)
self.setRect(rect)
elif event.key() == QtCore.Qt.Key.Key_Right:
rect = self.rect()
rect.setRight(self.rect().right() + factor)
self.setRect(rect)
else:
if event.key() == QtCore.Qt.Key.Key_Delete or \
event.key() == QtCore.Qt.Key.Key_Backspace:
self.delete()
elif event.key() == QtCore.Qt.Key.Key_Escape:
self.setSelected(False)
elif event.key() == QtCore.Qt.Key.Key_Return or \
event.key() == QtCore.Qt.Key.Key_Enter:
self.setCurrentCell(self.currentRow()-1, self.currentColumn())
elif event.key() == QtCore.Qt.Key.Key_Up:
rect = self.rect()
rect.moveTop(self.rect().y() - factor)
self.setRect(rect)
elif event.key() == QtCore.Qt.Key.Key_Down:
rect = self.rect()
rect.moveTop(self.rect().y() + factor)
self.setRect(rect)
elif event.key() == QtCore.Qt.Key.Key_Left:
rect = self.rect()
rect.moveLeft(self.rect().x() - factor)
self.setRect(rect)
elif event.key() == QtCore.Qt.Key.Key_Right:
rect = self.rect()
rect.moveLeft(self.rect().x() + factor)
self.setRect(rect)
[docs]
class RectItem(QtWidgets.QGraphicsRectItem, GeometricItem):
"""Class to plot a rectangular item"""
tipo = "square"
icon = os.path.join(IMAGE_PATH, "equipment", "square.png")
[docs]
class EllipseItem(QtWidgets.QGraphicsEllipseItem, GeometricItem):
"""Class to plot a circle or oval item"""
tipo = "ellipse"
icon = os.path.join(IMAGE_PATH, "equipment", "cirle.png")
[docs]
class TextItem(QtWidgets.QGraphicsTextItem):
"""Class to plot a text item"""
tipo = "txt"
[docs]
def __init__(self, text, parent=None, position=QtCore.QPointF(0, 0),
transform=QtGui.QTransform(), selectable=True):
super().__init__(parent=parent)
if selectable:
self.setFlags(
QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable)
else:
self.setFlags(
QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable)
self.setHtml(text)
self.setPos(position)
self.setTransform(transform)
self.selectable = selectable
[docs]
def delete(self):
"""Delete item from scene"""
self.scene().delete(self)
[docs]
def mouseDoubleClickEvent(self, event=None):
"""Show dialog to change text"""
dialog = TextItemDlg(self.toHtml())
if dialog.exec():
self.setHtml(dialog.editor.text)
[docs]
def itemChange(self, key, value):
"""Set mainwindow current project modified at any change of item"""
if self.scene() and key == \
QtWidgets.QGraphicsItem.GraphicsItemChange.ItemPositionChange:
mainwindow = self.scene().parent()
if mainwindow.dirty[mainwindow.idTab] is False:
mainwindow.dirty[mainwindow.idTab] = True
mainwindow.activeControl(True)
mainwindow.tabModified(mainwindow.idTab)
return QtWidgets.QGraphicsItem.itemChange(self, key, value)
[docs]
def keyPressEvent(self, event):
"""Do action when arrows keys are pressed"""
factor = Preferences.getint("PFD", 'move_factor')
if event.key() == QtCore.Qt.Key.Key_Delete or \
event.key() == QtCore.Qt.Key.Key_Backspace:
self.delete()
elif event.key() == QtCore.Qt.Key.Key_Escape:
self.setSelected(False)
elif event.key() == QtCore.Qt.Key.Key_Return or \
event.key() == QtCore.Qt.Key.Key_Enter:
self.mouseDoubleClickEvent()
elif event.key() == QtCore.Qt.Key.Key_Up:
self.setPos(QtCore.QPointF(self.pos().x(), self.pos().y()-factor))
elif event.key() == QtCore.Qt.Key.Key_Down:
self.setPos(QtCore.QPointF(self.pos().x(), self.pos().y()+factor))
elif event.key() == QtCore.Qt.Key.Key_Left:
self.setPos(QtCore.QPointF(self.pos().x()-factor, self.pos().y()))
elif event.key() == QtCore.Qt.Key.Key_Right:
self.setPos(QtCore.QPointF(self.pos().x()+factor, self.pos().y()))
[docs]
class GraphicsEntity(QtCore.QObject):
"""Class with common functionality for Entity in PFD"""
[docs]
def view(self):
"""Generate text report with properties calculated of entity"""
with tempfile.NamedTemporaryFile(
"w", delete=False, suffix=".txt", encoding="utf-8") as temp:
title = self.tr("Project Name") + ": "
title += self.scene().parent().currentFilename
temp.write(title + os.linesep)
if isinstance(self.entity, Corriente):
temp.write(self.tr("Stream Id"))
else:
temp.write(self.tr("Equipment Id"))
temp.write(f": {self.id}" + os.linesep)
now = datetime.today()
temp.write(self.tr("Report generated at"))
temp.write(now.strftime("%H:%M:%S - %d/%m/%Y") + os.linesep)
temp.write(self.entity.txt())
subprocess.Popen(
[Preferences.get("Applications", 'TextViewer'), temp.name])
[docs]
def exportExcel(self):
"""Export data to spreadsheet file"""
msg = self.tr("Select Spreadsheet")
ext = []
if os.environ["ezodf"]:
ext.append(
self.tr("Libreoffice spreadsheet files") + " (*.ods)")
if os.environ["xlwt"]:
ext.append(
self.tr("Microsoft Excel 97/2000/XP/2003 XMLL")
+ " (*.xls)")
if os.environ["openpyxl"]:
ext.append(
self.tr("Microsoft Excel 2007/2010 XML") + " (*.xlsx)")
patron = ";;".join(ext)
folder = os.path.dirname(str(self.scene().parent().currentFilename))
ruta = QtWidgets.QFileDialog.getSaveFileName(
self.scene().parent(), msg, folder, patron)[0]
if ruta:
name, ext = os.path.splitext(ruta)
if not ext or ext not in (".ods", ".xlsx", ".xls"):
ruta += "." + str(ext[0]).split(".")[-1][:-1]
if ruta[-3:] == "ods":
templatefile = os.path.join(
os.environ["pychemqt"], "dat", "templates",
self.entity.__class__.__name__.lower()) + ".ots"
if os.path.isfile(templatefile):
spreadfile = ezodf.newdoc("ods", ruta, templatefile)
sheet = spreadfile.sheets[0]
for attr, tipo, cell in self.entity.datamap2xls():
prop = self.entity._prop(attr)
if tipo == "value":
value = prop.config()
else:
value = prop.text()
sheet[cell].set_value(value)
else:
spreadfile = ezodf.newdoc("ods", ruta)
sheets = spreadfile.sheets
sheet = ezodf.Table(f'pychemqt - s{self.id}')
sheets += sheet
propiedades = self.entity.properties()
sheet.reset(size=(len(propiedades) + 1, 10))
for i, (name, attr, unit) in enumerate(
self.entity.propertiesNames()):
value = self.entity._prop(attr)
txt = ""
if isinstance(value, list):
print(value)
# txt = value[0].text()
elif unit in unidades._all:
txt = value.text()
value = value.config()
sheet[f"B{i+2}"].set_value(name)
sheet[f"C{i+2}"].set_value(value)
sheet[f"D{i+2}"].set_value(txt)
spreadfile.save()
elif ruta[-4:] == "xlsx":
# TODO:
print(ruta, "is xlsx")
elif ruta[-3:] == "xls":
print(ruta, "is xls")
[docs]
class StreamItem(QtWidgets.QGraphicsPathItem, GeometricItem, GraphicsEntity):
"""Class to plot a mass stream"""
up = None
down = None
id = 0
free_id = []
tipo = "stream"
[docs]
def __init__(self, parent=None):
super().__init__()
self.parent = parent
self.setPen(self._pen())
qp = QtGui.QPainterPath()
self.setPath(qp)
self.setFlags(
QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable)
if StreamItem.free_id:
self.id = StreamItem.free_id.pop(0)
else:
self.id = StreamItem.id + 1
StreamItem.id += 1
self.idLabel = TextItem(f"S{self.id}", self, selectable=False)
self.idLabel.setZValue(2)
self.setAcceptHoverEvents(True)
self.tr = partial(translate, "StreamItem")
@property
def corriente(self):
"""Property with Corriente instance"""
return self.scene().project.getStream(self.id)
@property
def entity(self):
"""Property with Corriente instance"""
return self.corriente
[docs]
def setCorriente(self, corriente):
"""Set Corriente instance"""
self.scene().project.setStream(self.id, corriente)
kwargs = {"entrada": corriente}
if isinstance(self.scene().project.getDownToStream(self.id), flux.Mixer):
kwargs["id_entrada"] = self.scene().project.streams[self.id][3] + 1
equip = self.scene().project.getDownToStream(self.id)
if isinstance(equip, equipment):
equip(**kwargs)
# Set color of stream to show fully calculated versus undefined streams
pen = self.pen()
if corriente.status == 1:
pen.setColor(QtGui.QColor("blue"))
else:
pen.setColor(QtGui.QColor("red"))
self.setPen(pen)
[docs]
def mouseDoubleClickEvent(self, event=None):
"""Show Corriente dialog when double click object"""
dialog = UI_corriente.Corriente_Dialog(self.corriente)
if dialog.exec():
self.setCorriente(dialog.corriente)
[docs]
def copyFromProject(self):
"""Show dialog to select any stream from other pychemqt project to copy
it in this object"""
dialog = SelectStreamProject()
if dialog.exec():
indice = dialog.stream.currentText()
data = dialog.project["stream"][indice]
corriente = Corriente()
corriente.readFromJSON(data)
self.setCorriente(corriente)
[docs]
def keyPressEvent(self, event):
"""Do action when keys are pressed"""
if event.key() == QtCore.Qt.Key.Key_Delete or \
event.key() == QtCore.Qt.Key.Key_Backspace:
self.delete()
elif event.key() == QtCore.Qt.Key.Key_Escape:
self.setSelected(False)
elif event.key() == QtCore.Qt.Key.Key_Return or \
event.key() == QtCore.Qt.Key.Key_Enter:
self.mouseDoubleClickEvent()
[docs]
def hoverEnterEvent(self, event):
"""Show popup dialog with info when the mouse stay over the object"""
if not (self.scene().addObj and self.scene().addType == "stream"):
self.scene().popup.show()
self.scene().popup.populate(self.corriente, self.id, Preferences)
point = self.mapToParent(event.scenePos())
self.scene().popup.move(int(point.x()), int(point.y()))
[docs]
def hoverLeaveEvent(self, event):
"""Hide popup dialog when mouse leave object"""
self.scene().popup.hide()
[docs]
def hoverMoveEvent(self, event):
"""Move popup dialog when the mouse move over the object"""
point = self.mapToParent(event.scenePos())
self.scene().popup.show()
self.scene().popup.move(int(point.x()), int(point.y()))
[docs]
def redraw(self, entrada=None, salida=None):
"""Recalcule the stream path in screen"""
if entrada:
self.entrada = entrada
if salida:
self.salida = salida
max_height = max(self.up.boundingRect().height(),
self.down.boundingRect().height())
max_width = max(self.up.boundingRect().width(),
self.down.boundingRect().width())
y_up = min(self.up.pos().y(), self.down.pos().y())
y_down = max(self.up.pos().y(), self.down.pos().y())
if self.up.pos().y() == y_up:
height_sup = self.up.boundingRect().height()
else:
height_sup = self.down.boundingRect().height()
x_mean = (self.entrada.x() + self.salida.x()) / 2.
Xdist_entrada = 0
Ydist_entrada = 0
Xdist_salida = 0
Ydist_salida = 0
if self.Ang_entrada == 0:
Xdist_entrada = 20
elif self.Ang_entrada == 180:
Xdist_entrada = -20
elif self.Ang_entrada == 90:
Ydist_entrada = 20
elif self.Ang_entrada == 360:
Ydist_entrada = -20
if self.Ang_salida == 0:
Xdist_salida = -20
elif self.Ang_salida == 180:
Xdist_salida = 20
qp = QtGui.QPainterPath()
qp.moveTo(self.entrada)
if self.Ang_entrada == self.Ang_salida:
if self.salida.x() > self.entrada.x() + 10:
qp.lineTo(QtCore.QPointF(x_mean, self.entrada.y()))
qp.lineTo(QtCore.QPointF(x_mean, self.salida.y()))
else:
if abs(self.entrada.y() - self.salida.y()) > max_height:
# cabe la linea entre ambos equipos
y_mean = (y_up + y_down + height_sup) / 2.
qp.lineTo(QtCore.QPointF(
self.entrada.x() + Xdist_entrada,
self.entrada.y() + Ydist_entrada))
qp.lineTo(QtCore.QPointF(
self.entrada.x() + Xdist_entrada, y_mean))
qp.lineTo(QtCore.QPointF(
self.salida.x() + Xdist_salida, y_mean))
qp.lineTo(QtCore.QPointF(
self.salida.x() + Xdist_salida,
self.salida.y() + Ydist_entrada))
else:
# sacamos la linea por encima de los equipos
y_mean = y_up - 20
qp.lineTo(QtCore.QPointF(
self.entrada.x() + Xdist_entrada,
self.entrada.y() + Ydist_entrada))
qp.lineTo(QtCore.QPointF(
self.entrada.x() + Xdist_entrada, y_mean))
qp.lineTo(QtCore.QPointF(
self.salida.x() + Xdist_salida, y_mean))
qp.lineTo(QtCore.QPointF(
self.salida.x() + Xdist_salida,
self.salida.y() + Ydist_salida))
else:
x_mean = max(self.entrada.x(), self.salida.x()) + Xdist_salida
qp.lineTo(QtCore.QPointF(x_mean, self.entrada.y()))
qp.lineTo(QtCore.QPointF(x_mean, self.salida.y()))
qp.lineTo(self.salida)
self.prepareGeometryChange()
self.setPath(qp)
if abs(self.entrada.y() - self.salida.y()) <= 30:
self.idLabel.setPos(x_mean, max(self.entrada.y(), self.salida.y()))
else:
self.idLabel.setPos(x_mean, (self.entrada.y()+self.salida.y())/2-10)
[docs]
def postDelete(self):
"""Do actions necessary when remove a stream object"""
StreamItem.free_id.append(self.id)
self.up.down_used -= 1
self.down.up_used -= 1
if self in self.up.down:
self.up.down.remove(self)
if self in self.down.up:
self.down.up.remove(self)
[docs]
def toggleLabel(self):
"""Toggle label visibility"""
self.idLabel.setVisible(not self.idLabel.isVisible())
[docs]
def solidFit(self):
"""Show a dialog to fit solid particle size distribution"""
if self.corriente.solido:
dialog = Plot_Distribucion(self.id, self.corriente.solido)
self.scene().parent().currentMdi.addSubWindow(dialog)
dialog.show()
[docs]
class EquipmentItem(QtSvgWidgets.QGraphicsSvgItem, GraphicsEntity):
"""Class to plot equipment item"""
up = []
down = []
up_used = 0
down_used = 0
id = 0
id_in = 0
id_out = 0
tipo = "equip"
[docs]
def __init__(self, name, dialogoId, parent=None):
self.name = name
imagen = os.path.join(IMAGE_PATH, "equipment", f"{name}.svg")
super().__init__(imagen, parent=parent)
self.dialogoId = dialogoId
self.setFlags(
QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable)
self.imagen = imagen
self.angle = 0
self.setAcceptHoverEvents(True)
if dialogoId is not None:
self.dialogo = UI_equipments[dialogoId].UI_equipment
EquipmentItem.id += 1
self.id = EquipmentItem.id
self.tipo = "e"
self.idLabel = TextItem(f"E{self.id}", self, selectable=False)
self.idLabel.setPos(self.boundingRect().width() / 3., -20)
else:
self.dialogo = UI_corriente.Corriente_Dialog
if name == "in":
EquipmentItem.id_in += 1
self.id = EquipmentItem.id_in
self.tipo = "i"
else:
EquipmentItem.id_out += 1
self.id = EquipmentItem.id_out
self.tipo = "o"
output = []
inpt = []
doc = minidom.parse(imagen)
for entrada in doc.getElementsByTagName("ins")[0].childNodes:
if isinstance(entrada, minidom.Element):
if entrada.tagName == "in":
x = float(entrada.getAttribute("x"))
y = float(entrada.getAttribute("y"))
d = float(entrada.getAttribute("d"))
inpt.append([x, y, d])
for salida in doc.getElementsByTagName("outs")[0].childNodes:
if isinstance(salida, minidom.Element):
if salida.tagName == "out":
x = float(salida.getAttribute("x"))
y = float(salida.getAttribute("y"))
d = float(salida.getAttribute("d"))
output.append([x, y, d])
doc.unlink()
self.input = []
if inpt:
for entrada in inpt:
obj = QtWidgets.QGraphicsEllipseItem(self)
obj.setRect(entrada[0] * self.boundingRect().width() - 5,
entrada[1] * self.boundingRect().height() - 5,
10, 10)
obj.direction = int(entrada[2])
obj.setPen(QtGui.QColor(255, 255, 255))
obj.setBrush(QtGui.QColor(
Preferences.get("PFD", 'Color_Entrada')))
self.input.append(obj)
self.output = []
if output:
for salida in output:
obj = QtWidgets.QGraphicsEllipseItem(self)
obj.setRect(salida[0] * self.boundingRect().width() - 5,
salida[1] * self.boundingRect().height() - 5,
10, 10)
obj.direction = int(salida[2])
obj.setPen(QtGui.QColor(255, 255, 255))
obj.setBrush(QtGui.QColor(
Preferences.get("PFD", 'Color_Salida')))
self.output.append(obj)
self.showInput(False)
@property
def equipment(self):
"""Property with Equipment instance"""
return self.scene().project.getItem(self.id)
@property
def entity(self):
"""Property with Equipment instance"""
return self.equipment
[docs]
def mouseDoubleClickEvent(self, event=None):
"""Show equipment dialog when double click object"""
if self.dialogoId is not None:
kwarg = {"equipment": self.equipment}
# Aditional parameters for selected equipment
if isinstance(self.equipment, flux.Divider):
# Divider
if not self.down:
return
kwarg["salidas"] = len(self.down)
elif isinstance(self.equipment, flux.Mixer):
# mixer
if not self.up:
return
kwarg["entradas"] = len(self.up)
elif isinstance(self.equipment, spreadsheet.Spreadsheet):
# spreadsheet
self.equipment(project=self.scene().project)
kwarg["project"] = self.scene().project
dialog = self.dialogo(**kwarg)
if dialog.exec():
self.scene().project.setItem(self.id, dialog.Equipment)
# self.up[0].setCorriente(dialog.Equipment.entrada)
for i, corriente in enumerate(dialog.Equipment.salida):
self.down[i].setCorriente(corriente)
else:
if self.output:
self.down[0].mouseDoubleClickEvent()
else:
self.up[0].mouseDoubleClickEvent()
[docs]
def mousePressEvent(self, event):
"""If any user interaction is waiting manage that mouse click"""
QtSvgWidgets.QGraphicsSvgItem.mousePressEvent(self, event)
if self.scene().addObj:
if self.scene().addType == "stream":
if len(self.scene().Pos) == 0:
punto = self.output
self.scene().up = self
x = self.down_used
else:
punto = self.input
self.scene().down = self
x = self.up_used
self.scene().Pos.append(
self.mapToScene(punto[x].rect().center()))
self.scene().points.append(punto[x])
else:
self.scene().Pos.append(event.pos())
[docs]
def mouseMoveEvent(self, event=None):
"""Move object in scene and redraw the streams associated to it"""
if event:
QtWidgets.QGraphicsPixmapItem.mouseMoveEvent(self, event)
for i, corriente in enumerate(self.up):
corriente.redraw(
salida=self.mapToScene(self.input[i].rect().center()))
for i, corriente in enumerate(self.down):
corriente.redraw(
entrada=self.mapToScene(self.output[i].rect().center()))
[docs]
def hoverEnterEvent(self, event):
"""Do actions when mouse hover over the object, show input/output point
when it's waiting user interaction or popup info in other case"""
if self.scene().addObj and self.scene().addType == "stream":
self.showInput(True)
else:
if self.dialogoId is not None:
self.scene().popup.populate(self.equipment, self.id, Preferences)
else:
if self.output:
self.scene().popup.populate(
self.down[0].corriente, self.down[0].id, Preferences)
else:
self.scene().popup.populate(
self.up[0].corriente, self.up[0].id, Preferences)
point = event.scenePos()
self.scene().popup.show()
self.scene().popup.move(int(point.x()), int(point.y()))
self.scene().popup.graphicsProxyWidget().resize(0, 0)
[docs]
def hoverLeaveEvent(self, event):
"""Hide object shown at hoverEnterEvent"""
self.showInput(False)
self.scene().popup.hide()
[docs]
def hoverMoveEvent(self, event):
"""Move popup with mouse movement"""
point = event.scenePos()
self.scene().popup.move(int(point.x()), int(point.y()))
[docs]
def keyPressEvent(self, event):
"""Do action when arrows keys are pressed"""
factor = Preferences.getint("PFD", 'move_factor')
if event.key() == QtCore.Qt.Key.Key_Delete or \
event.key() == QtCore.Qt.Key.Key_Backspace:
self.delete()
elif event.key() == QtCore.Qt.Key.Key_Escape:
self.setSelected(False)
elif event.key() == QtCore.Qt.Key.Key_Return or \
event.key() == QtCore.Qt.Key.Key_Enter:
self.mouseDoubleClickEvent()
elif event.key() == QtCore.Qt.Key.Key_Up:
self.setPos(QtCore.QPointF(self.pos().x(), self.pos().y()-factor))
self.mouseMoveEvent()
elif event.key() == QtCore.Qt.Key.Key_Down:
self.setPos(QtCore.QPointF(self.pos().x(), self.pos().y()+factor))
self.mouseMoveEvent()
elif event.key() == QtCore.Qt.Key.Key_Left:
self.setPos(QtCore.QPointF(self.pos().x()-factor, self.pos().y()))
self.mouseMoveEvent()
elif event.key() == QtCore.Qt.Key.Key_Right:
self.setPos(QtCore.QPointF(self.pos().x()+factor, self.pos().y()))
self.mouseMoveEvent()
[docs]
def itemChange(self, key, value):
"""Set mainwindow current project modified at any change of item"""
if self.scene() and key == \
QtWidgets.QGraphicsItem.GraphicsItemChange.ItemPositionChange:
mainwindow = self.scene().parent()
if mainwindow.dirty[mainwindow.idTab] is False:
mainwindow.dirty[mainwindow.idTab] = True
mainwindow.activeControl(True)
mainwindow.tabModified(mainwindow.idTab)
return QtWidgets.QGraphicsItem.itemChange(self, key, value)
[docs]
def delete(self):
"""Delete item from scene"""
self.scene().delete(self)
# TODO: Add configuration support for appearance of equipment
[docs]
def postDelete(self):
"""Do actions necessary when remove a equipment object"""
while self.down:
stream = self.down.pop()
self.scene().delete(stream)
while self.up:
stream = self.up.pop()
self.scene().delete(stream)
[docs]
def toggleLabel(self):
"""Toggle label visibility"""
self.idLabel.setVisible(not self.idLabel.isVisible())
[docs]
def rotate(self, angle):
"""Rotate equipment image"""
self.angle = angle
transform = self.transform()
transform.rotate(angle)
self.setTransform(transform)
self.mouseMoveEvent()
for i, entrada in enumerate(self.up):
new_angle = (self.input[i].direction + angle) % 360
self.input[i].direction = new_angle
entrada.Ang_salida = new_angle
entrada.redraw()
for i, salida in enumerate(self.down):
new_angle = (self.output[i].direction + angle) % 360
self.output[i].direction = new_angle
salida.Ang_entrada = new_angle
salida.redraw()
[docs]
class SelectStreamProject(QtWidgets.QDialog):
"""Dialog to select a stream from any external project"""
project = None
[docs]
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle(self.tr("Select stream from file"))
layout = QtWidgets.QVBoxLayout(self)
label = self.tr("Project path")
msg = self.tr("Select pychemqt project file")
patrones = []
patrones.append(self.tr("pychemqt project file") + " (*.pcq)")
patron = ";;".join(patrones)
self.filename = PathConfig(label + ":", msg=msg, patron=patron)
self.filename.valueChanged.connect(self.changeproject)
layout.addWidget(self.filename)
lyt1 = QtWidgets.QHBoxLayout()
lyt1.addWidget(QtWidgets.QLabel(self.tr("Streams")))
self.stream = QtWidgets.QComboBox()
lyt1.addWidget(self.stream)
lyt1.addItem(QtWidgets.QSpacerItem(
10, 10, QtWidgets.QSizePolicy.Policy.Expanding,
QtWidgets.QSizePolicy.Policy.Fixed))
layout.addLayout(lyt1)
layout.addItem(QtWidgets.QSpacerItem(
10, 10, QtWidgets.QSizePolicy.Policy.Expanding,
QtWidgets.QSizePolicy.Policy.Expanding))
lyt2 = QtWidgets.QHBoxLayout()
self.status = QtWidgets.QLabel()
lyt2.addWidget(self.status)
self.buttonBox = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.StandardButton.Ok
| QtWidgets.QDialogButtonBox.StandardButton.Cancel)
self.buttonBox.button(
QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled(False)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
lyt2.addWidget(self.buttonBox)
layout.addLayout(lyt2)
[docs]
def changeproject(self, path):
"""Upgrade dialog with the streams of loaded file"""
self.stream.clear()
st = self.tr("Loading project...")
self.status.setText(st)
QtWidgets.QApplication.processEvents()
try:
with open(path, "r") as file:
self.project = json.load(file)
except Exception as e:
self.status.setText(self.tr("Failed to loading project..."))
raise e
self.buttonBox.button(
QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled(True)
self.status.setText(self.tr("Project loaded succesfully"))
for stream in sorted(self.project["stream"].keys()):
self.stream.addItem(stream)
[docs]
class TextItemDlg(QtWidgets.QDialog):
"""Dialog to edit texts in PFD"""
[docs]
def __init__(self, text=None, parent=None):
super().__init__(parent)
layout = QtWidgets.QGridLayout(self)
self.editor = texteditor.TextEditor()
self.editor.notes.textChanged.connect(self.updateUi)
layout.addWidget(self.editor, 1, 1, 1, 1)
self.buttonBox = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.StandardButton.Ok
| QtWidgets.QDialogButtonBox.StandardButton.Cancel)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
layout.addWidget(self.buttonBox, 2, 1, 1, 1)
self.editor.notes.setFocus()
if text:
self.editor.setText(text)
self.setWindowTitle(self.tr("Edit text"))
self.updateUi()
[docs]
def updateUi(self):
"""Set enable/disable OK button"""
self.buttonBox.button(
QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled(
bool(self.editor.notes.toPlainText()))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
# dialogo = ConfLineDialog()
dialogo = SelectStreamProject()
dialogo.show()
sys.exit(app.exec())