• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jjgomera / pychemqt / 7b61bc20-5463-49e6-bbc6-8270b48fb6e9

07 Feb 2026 05:51PM UTC coverage: 73.36%. Remained the same
7b61bc20-5463-49e6-bbc6-8270b48fb6e9

push

circleci

jjgomera
Merge branch 'master' of https://github.com/jjgomera/pychemqt

2 of 7 new or added lines in 1 file covered. (28.57%)

379 existing lines in 1 file now uncovered.

29145 of 39729 relevant lines covered (73.36%)

0.73 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

13.37
/UI/widgets.py
1
#!/usr/bin/python3
2
# -*- coding: utf-8 -*-
3

4
'''Pychemqt, Chemical Engineering Process simulator
5
Copyright (C) 2009-2025, Juan José Gómez Romera <jjgomera@gmail.com>
6

7
This program is free software: you can redistribute it and/or modify
8
it under the terms of the GNU General Public License as published by
9
the Free Software Foundation, either version 3 of the License, or
10
(at your option) any later version.
11

12
This program is distributed in the hope that it will be useful,
13
but WITHOUT ANY WARRANTY; without even the implied warranty of
14
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
GNU General Public License for more details.
16

17
You should have received a copy of the GNU General Public License
18
along with this program.  If not, see <http://www.gnu.org/licenses/>.
19

20

21
Module to define common graphics widget
22

23
  * :class:`SimpleStatus`: Status with simple format and functionality
24
  * :class:`Status`: Label with status (for equipment, stream)
25
  * :class:`Entrada_con_unidades`: Composite widget for unit values for input/view
26
  * :class:`Tabla`: Custom tablewidget tablewidget with added functionality
27
  * :class:`ClickableLabel`: Label with custom clicked signal
28
  * :class:`ColorSelector`: Composite widget for colour definition
29
  * :class:`DragButton`: Button with drag & drop support
30
  * :class:`PathConfig`: Custom widget for file path show and configure
31
  * :class:`LineConfig`: Custom QGroupbox with all matplotlib Line configuration
32
  * :class:`GridConfig`: Extended version of LineConfig for grid specific line
33
  * :class:`CustomCombo`: General custom QComboBox
34
  * :class:`LineStyleCombo`: Custom QComboBox for select matplotlib line styles
35
  * :class:`PFDLineCombo`: Custom QComboBox for select PFD line styles for stream
36
  * :class:`MarkerCombo`: Custom QComboBox for select matplotlib line marker
37
  * :class:`NumericFactor`: Numeric format configuration dialog
38
  * :class:`InputFont`: Custom widget to edit a text input with font and color support
39
  * :class:`Table_Graphics`: Custom widget to implement a popup in PFD
40
  * :func: `mathTex2QPixmap`: Convert a latex text to a QPixmap
41
  * :class:`QLabelMath`: Customized QLabel to show a pixmap from a latex code
42

43
  * :func: `createAction`
44
  * :func: `okToContinue`: Function to ask user if any unsaved change
45
'''
46

47

48
from configparser import ConfigParser
1✔
49
from math import pi
1✔
50
import os
1✔
51
import sys
1✔
52

53
from matplotlib.figure import Figure
1✔
54
from matplotlib.backends.backend_agg import FigureCanvasAgg
1✔
55

56
from lib.config import conf_dir, IMAGE_PATH
1✔
57
from lib.corriente import Corriente
1✔
58
from lib.utilities import representacion
1✔
59
from tools.qt import QtCore, QtGui, QtWidgets, translate
1✔
60
from tools.UI_unitConverter import UI_conversorUnidades, moneda
1✔
61
from UI.delegate import CellEditor
1✔
62

63

64
class SimpleStatus(QtWidgets.QLabel):
1✔
65
    """QLabel to show status info"""
66
    def setState(self, entity):
1✔
67
        """Change the state"""
NEW
68
        self.setText(entity.msg)
×
NEW
69
        if entity.status in (0, 5):
×
NEW
70
            color = "red"
×
71
        else:
NEW
72
            color = "black"
×
NEW
73
        self.setStyleSheet(f"QLabel {{color: {color}}}")
×
74

75

76
class Status(QtWidgets.QLabel):
1✔
77
    """Widget with status of dialog, equipment, stream, project, ..."""
78
    status = (
1✔
79
        (0, translate("widgets", "Underspecified"), "yellow"),
80
        (1, translate("widgets", "Solved"), "green"),
81
        (2, translate("widgets", "Ignored"), "Light gray"),
82
        (3, translate("widgets", "Warning"), "green"),
83
        (4, translate("widgets", "Calculating..."), "Cyan"),
84
        (5, translate("widgets", "Error"), "red"))
85

86
    def __init__(self, state=0, text="", parent=None):
1✔
87
        """
88
        state:
89
            0   -   Not solved
90
            1   -   OK
91
            2   -   Ignore
92
            3   -   Warning (Recommend: Use text parameter to explain)
93
            4   -   Calculating
94
            5   -   Error
95
        """
UNCOV
96
        super().__init__(parent)
×
UNCOV
97
        self.setState(state, text)
×
UNCOV
98
        self.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
×
UNCOV
99
        self.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)
×
UNCOV
100
        self.setFrameShape(QtWidgets.QFrame.Shape.Panel)
×
UNCOV
101
        self.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding,
×
102
                           QtWidgets.QSizePolicy.Policy.Preferred)
UNCOV
103
        self.state = 0
×
UNCOV
104
        self.oldState = 0
×
UNCOV
105
        self.oldText = ""
×
106

107
    def setState(self, state, text=""):
1✔
108
        """Change the state"""
109

110
        if state == 2:
×
111
            self.oldState = self.state
×
112
            oldtext = self.text().split(": ")
×
113
            if len(oldtext) == 1:
×
114
                self.oldText = ""
×
115
            else:
116
                self.oldText = oldtext[1:].join(": ")
×
117
        if text:
×
118
            self.setText(self.status[state][1]+": "+text)
×
119
        else:
UNCOV
120
            self.setText(self.status[state][1])
×
121

UNCOV
122
        color = self.status[state][2]
×
123
        self.setStyleSheet(f"QLabel {{background-color: {color}}}")
×
124
        QtWidgets.QApplication.processEvents()
×
125
        self.state = state
×
126

127
    def restaurar(self):
1✔
128
        """Restore old state"""
129
        self.setState(self.oldState, self.oldText)
×
130

131

132
class Entrada_con_unidades(QtWidgets.QWidget):
1✔
133
    """Customized widget with unit functionality"""
134

135
    valueChanged = QtCore.pyqtSignal(float)
1✔
136

137
    def __init__(self, unidad, UIconfig=None, retornar=True, readOnly=False,
1✔
138
                 boton=True, texto=True, textounidad="", title="", value=None,
139
                 start=0, max=float("inf"), min=0, decimales=4, tolerancia=4,
140
                 parent=None, width=85, resaltado=False, spinbox=False,
141
                 suffix="", step=0.01, colorReadOnly=None, colorResaltado=None,
142
                 frame=True, showNull=False):
143
        """
144
        Units:
145
            unidad: The unit (lib/unidades class) to use, mandatory
146
            UIconfig: Magnitud necessary if the main unit have several meaning
147
            title: Update unit title property
148
            retornar: Boolean to let or avoid the conversion window update
149
                the value of widget
150
            value: Inicial value of widget
151
            max: Maximum value for widget
152
            min: Minimum value for widget
153
            decimales: Decimal number count to show of value
154
            tolerancia: Value of exponent over than to use exponential notation
155
        UI:
156
            readOnly: Boolean, set widget readOnly property
157
            frame: Boolean, show the frame of widget or not
158
            width: Width of value widget
159
            boton: Boolean, show or not the button for unit conversion dialog
160
            texto: Boolean, show the unit text at right of value
161
            textounidad: Alternate text to show as unit text
162
            suffix: Text added to value in value representation
163
            showNull: Boolean, show value if it's 0
164
            resaltado: Boolean, use base color in widget
165
            colorResaltado: Color to use as base color if value
166
            colorReadOnly: Color to use is the widget is readOnly
167
        Spinbox functionality:
168
            spinbox: boolean to specified a QSpinbox use, with mouse response
169
            start: initial value for spinbox mouse interaction
170
            step: value of step at mouse spingox interaction
171
        """
UNCOV
172
        super().__init__(parent)
×
UNCOV
173
        self.resize(self.minimumSize())
×
UNCOV
174
        self.unidad = unidad
×
175

UNCOV
176
        if title:
×
UNCOV
177
            self.unidad.__title__ = title
×
UNCOV
178
        if unidad in (float, int):
×
UNCOV
179
            self.magnitud = None
×
180
        else:
UNCOV
181
            self.magnitud = unidad.__name__
×
UNCOV
182
        if unidad == int and spinbox and step == 0.01:
×
UNCOV
183
            step = 1
×
UNCOV
184
        self.decimales = decimales
×
185
        self.tolerancia = tolerancia
×
186
        self.step = step
×
187
        self.spinbox = spinbox
×
UNCOV
188
        self.max = max
×
189
        self.suffix = suffix
×
190
        self.min = min
×
191
        self.start = start
×
192
        self.textounidad = textounidad
×
UNCOV
193
        self.boton = boton
×
194
        self.resaltado = resaltado
×
195
        self.showNull = showNull
×
196

197
        # Set color on input widget
198
        Config = ConfigParser()
×
199
        Config.read(conf_dir+"pychemqtrc")
×
200
        if colorReadOnly:
×
201
            self.colorReadOnly = colorReadOnly
×
202
        else:
203
            self.colorReadOnly = QtGui.QColor(
×
204
                Config.get("General", 'Color_ReadOnly'))
205
        if colorResaltado:
×
206
            self.colorResaltado = colorResaltado
×
207
        else:
208
            self.colorResaltado = QtGui.QColor(
×
209
                Config.get("General", 'Color_Resaltado'))
210

211
        if UIconfig:
×
212
            self.UIconfig = UIconfig
×
213
        else:
214
            self.UIconfig = self.magnitud
×
UNCOV
215
        self.retornar = retornar
×
216
        layout = QtWidgets.QGridLayout(self)
×
UNCOV
217
        layout.setContentsMargins(0, 0, 0, 0)
×
218
        layout.setSpacing(0)
×
219
        self.entrada = QtWidgets.QLineEdit()
×
UNCOV
220
        self.entrada.setFixedSize(width, 24)
×
221
        self.entrada.editingFinished.connect(self.entrada_editingFinished)
×
UNCOV
222
        self.entrada.setAlignment(
×
223
            QtCore.Qt.AlignmentFlag.AlignRight
224
            | QtCore.Qt.AlignmentFlag.AlignVCenter)
225
        if unidad == int:
×
UNCOV
226
            if max == float("inf"):
×
227
                max = 1000000000
×
228
            validator = QtGui.QIntValidator(min, max, self)
×
229
        else:
230
            validator = QtGui.QDoubleValidator(min, max, decimales, self)
×
231
            locale = QtCore.QLocale("en")
×
232
            validator.setLocale(locale)
×
233
        self.entrada.setValidator(validator)
×
234
        self.setReadOnly(readOnly)
×
235
        self.setRetornar(self.retornar)
×
UNCOV
236
        self.setFrame(frame)
×
UNCOV
237
        layout.addWidget(self.entrada, 0, 1, 1, 3)
×
238

239
        if value is None:
×
240
            self.value = self.unidad(0)
×
241
        else:
UNCOV
242
            self.setValue(value)
×
243
        if self.magnitud:
×
244
            if boton:
×
245
                self.unidades = QtWidgets.QPushButton(".")
×
246
                self.unidades.setFixedSize(12, 24)
×
247
                self.unidades.setVisible(False)
×
248
                self.unidades.clicked.connect(self.unidades_clicked)
×
249
                layout.addWidget(self.unidades, 0, 1)
×
250

UNCOV
251
        if boton:
×
252
            self.botonClear = QtWidgets.QPushButton(QtGui.QIcon(QtGui.QPixmap(
×
253
                os.path.join(IMAGE_PATH, "button", "editDelete.png"))), "")
UNCOV
254
            self.botonClear.setFixedSize(12, 24)
×
255
            self.botonClear.setVisible(False)
×
256
            self.botonClear.clicked.connect(self.clear)
×
257
            layout.addWidget(self.botonClear, 0, 3)
×
258

259
        if texto:
×
260
            self.texto = QtWidgets.QLabel()
×
261
            self.texto.setAlignment(QtCore.Qt.AlignmentFlag.AlignVCenter)
×
262
            self.texto.setIndent(5)
×
UNCOV
263
            txt = ""
×
264
            if self.UIconfig:
×
265
                txt += self.value.text(self.UIconfig)
×
UNCOV
266
            if textounidad:
×
267
                txt += textounidad
×
268
            self.texto.setText(txt)
×
269
            layout.addWidget(self.texto, 0, 4)
×
270

UNCOV
271
        layout.addItem(QtWidgets.QSpacerItem(
×
272
            0, 0, QtWidgets.QSizePolicy.Policy.Expanding,
273
            QtWidgets.QSizePolicy.Policy.Fixed), 0, 5)
274
        self.setResaltado(resaltado)
×
275

276
    def unidades_clicked(self):
1✔
277
        """Show the unit converter dialog"""
278
        if self.magnitud == "Currency":
×
279
            dialog = moneda(self.value)
×
280
        else:
281
            dialog = UI_conversorUnidades(self.unidad, self.value)
×
282

UNCOV
283
        if dialog.exec() and self.retornar:
×
284
            # Change the value if change and retornar if active
UNCOV
285
            self.entrada.setText(
×
286
                representacion(dialog.value.config(self.UIconfig))+self.suffix)
287
            oldvalue = self.value
×
UNCOV
288
            self.value = dialog.value
×
UNCOV
289
            if oldvalue != self.value:
×
UNCOV
290
                self.valueChanged.emit(self.value)
×
291

292
    def entrada_editingFinished(self):
1✔
293
        """Change the value at finish of edit"""
294
        if not self.readOnly:
×
295
            # Filter suffix and fix bad numeric , interpretation
296
            if self.suffix:
×
UNCOV
297
                txt = self.entrada.text().split(self.suffix).replace(',', '.')
×
298
            else:
UNCOV
299
                txt = self.entrada.text().replace(',', '.')
×
300
            if self.unidad != int:
×
301
                self.entrada.setText(
×
302
                    representacion(float(txt), decimales=self.decimales,
303
                                   tol=self.tolerancia)+self.suffix)
UNCOV
304
            oldvalue = self.value
×
UNCOV
305
            if self.magnitud:
×
UNCOV
306
                self.value = self.unidad(
×
307
                    float(txt), "conf", magnitud=self.UIconfig)
308
            else:
309
                self.value = self.unidad(txt)
×
310
            if self.value != oldvalue or self.value == 0:
×
UNCOV
311
                self.valueChanged.emit(self.value)
×
312
                self.setToolTip()
×
313

314
    def clear(self):
1✔
315
        """Clear value"""
UNCOV
316
        self.entrada.setText("")
×
317
        self.value = None
×
318

319
    def setResaltado(self, boolean):
1✔
320
        """Set resalted background color for input widget"""
UNCOV
321
        self.resaltado = boolean
×
322
        paleta = QtGui.QPalette()
×
323
        if boolean:
×
324
            paleta.setColor(QtGui.QPalette.ColorRole.Base,
×
325
                            QtGui.QColor(self.colorResaltado))
UNCOV
326
        elif self.readOnly:
×
UNCOV
327
            paleta.setColor(QtGui.QPalette.ColorRole.Base,
×
328
                            QtGui.QColor(self.colorReadOnly))
329
        else:
330
            paleta.setColor(QtGui.QPalette.ColorRole.Base,
×
331
                            QtGui.QColor("white"))
UNCOV
332
        self.entrada.setPalette(paleta)
×
333

334
    def setReadOnly(self, readOnly):
1✔
335
        """Set read only state for widget"""
336
        self.entrada.setReadOnly(readOnly)
×
337
        self.readOnly = readOnly
×
UNCOV
338
        self.setResaltado(self.resaltado)
×
339

340
    def setNotReadOnly(self, editable):
1✔
341
        """Inverse readonly slot"""
UNCOV
342
        self.setReadOnly(not editable)
×
343

344
    def setRetornar(self, retornar):
1✔
345
        """Update value of widget from unit values list dialog"""
UNCOV
346
        self.retornar = retornar
×
347

348
    def setValue(self, value):
1✔
349
        """Set value of widget"""
350
        if value is None:
×
351
            value = 0
×
UNCOV
352
        self.value = self.unidad(value)
×
UNCOV
353
        if value or self.showNull:
×
UNCOV
354
            if self.magnitud:
×
355
                self.entrada.setText(
×
356
                    self.value.format(magnitud=self.UIconfig)+self.suffix)
UNCOV
357
            elif self.unidad == float:
×
UNCOV
358
                self.entrada.setText(
×
359
                    representacion(self.value, decimales=self.decimales,
360
                                   tol=self.tolerancia)+self.suffix)
361
            else:
UNCOV
362
                self.entrada.setText(str(self.value)+self.suffix)
×
363
            self.setToolTip()
×
364

365
    def setFrame(self, frame):
1✔
366
        """Set frame of widget"""
367
        self.entrada.setFrame(frame)
×
368
        self.frame = frame
×
369

370
    def setToolTip(self):
1✔
371
        """Define the tooltip with the values in confguration"""
UNCOV
372
        Preferences = ConfigParser()
×
UNCOV
373
        Preferences.read(conf_dir+"pychemqtrc")
×
UNCOV
374
        if Preferences.getboolean("Tooltip", "Show"):
×
375
            Config = ConfigParser()
×
376
            Config.read(conf_dir+"pychemqtrc")
×
UNCOV
377
            try:
×
UNCOV
378
                lst = map(int, Config.get("Tooltip", self.magnitud).split(","))
×
UNCOV
379
            except AttributeError:
×
380
                lst = []
×
381
            if lst:
×
UNCOV
382
                valores = []
×
UNCOV
383
                for i in lst:
×
UNCOV
384
                    valores.append(representacion(
×
385
                        getattr(self.value, self.value.__units__[i]),
386
                        self.decimales, self.tolerancia) + " " +
387
                        self.value.__text__[i])
388
                self.entrada.setToolTip(os.linesep.join(valores))
×
389

390
    def keyPressEvent(self, e):
1✔
391
        """Manage the key press to emulate a QSpinbox"""
392
        if not self.readOnly:
×
393
            if e.key() in [QtCore.Qt.Key.Key_Insert, QtCore.Qt.Key.Key_Backspace]:
×
394
                self.clear()
×
395
            if self.spinbox:
×
396
                if not self.value:
×
397
                    self.value = self.start
×
UNCOV
398
                if e.key() == QtCore.Qt.Key.Key_Up:
×
UNCOV
399
                    valor = self.value+self.step
×
UNCOV
400
                    if valor > self.max:
×
401
                        self.setValue(self.max)
×
402
                    else:
UNCOV
403
                        self.setValue(valor)
×
UNCOV
404
                elif e.key() == QtCore.Qt.Key.Key_Down:
×
405
                    valor = self.value-self.step
×
406
                    if valor < self.min:
×
407
                        self.setValue(self.min)
×
408
                    else:
409
                        self.setValue(valor)
×
410
                self.valueChanged.emit(self.value)
×
411

412
    def enterEvent(self, event):
1✔
413
        """When mouse enter in widget show the unidades and clear button, and
414
        add margin to let space to clear button"""
UNCOV
415
        if self.magnitud and self.boton:
×
416
            self.unidades.setVisible(True)
×
417
        if self.value and self.boton and not self.readOnly:
×
418
            self.botonClear.setVisible(True)
×
419
            self.entrada.setTextMargins(0, 0, 10, 0)
×
420

421
    def leaveEvent(self, event):
1✔
422
        """When mouse leave the widget undo the enterEvent actions"""
423
        if self.magnitud and self.boton:
×
UNCOV
424
            self.unidades.setVisible(False)
×
UNCOV
425
        if self.value and self.boton and not self.readOnly:
×
UNCOV
426
            self.botonClear.setVisible(False)
×
UNCOV
427
            self.entrada.setTextMargins(0, 0, 0, 0)
×
428

429

430
class Tabla(QtWidgets.QTableWidget):
1✔
431
    """QTableWidget with custom functionality"""
432
    editingFinished = QtCore.pyqtSignal()
1✔
433
    rowFinished = QtCore.pyqtSignal(list)
1✔
434

435
    def __init__(self, columnas=0, filas=0, stretch=True, dinamica=False,
1✔
436
                 readOnly=False, columnReadOnly=None,
437
                 horizontalHeader=None, verticalHeader=True,
438
                 verticalHeaderLabels=None, verticalHeaderModel="",
439
                 verticalOffset=0, orientacion=QtCore.Qt.AlignmentFlag.AlignRight,
440
                 delegate=CellEditor, delegateforRow=None,
441
                 parent=None):
442
        """
443
        columnas: Column count of widget
444
        filas: Row count, this value is initial and can be changed
445
        stretch: Boolean, stretch the last column to fill all space available
446
        dinamica: Boolean, let user fill the data adding new row when last
447
            are filled
448
        readOnly: Boolean, set the readOnly state of widget
449
        columnReadOnly: Array with boolean for column readOnly state, used when
450
            the readOnly state is different for each column
451

452
        horizontalHeader: Array with text for top header,
453
            Null don't show the top header
454
        verticalHeader: Boolean, to show or hide the right header
455
        verticalHeaderLabels: Array with text for right header
456
        verticalHeaderModel: If the table is dinamica and the right header is
457
            show, this are the text model to generate new vertical header label
458

459
        verticalOffset: Index, row don't included in fill data, used for show
460
            info, widget...
461
        orientation: Horizontal text alignment general for cell in tables,
462
            default is right as normal for numbers
463

464
        delegate: QItemDelegate subclass to configure cell editor
465
            default CellEditor with float appropiate functionality
466
        delegateforRow: delegate with specific values for row
467
        """
UNCOV
468
        super().__init__(parent)
×
469

470
        # Dimensions
UNCOV
471
        self.columnas = columnas
×
UNCOV
472
        self.filas = filas+verticalOffset
×
UNCOV
473
        self.verticalOffset = verticalOffset
×
UNCOV
474
        self.setColumnCount(self.columnas)
×
475

476
        # Header labels
UNCOV
477
        if not verticalHeader:
×
UNCOV
478
            self.verticalHeader().hide()
×
UNCOV
479
        if not horizontalHeader:
×
UNCOV
480
            self.horizontalHeader().hide()
×
481
        else:
UNCOV
482
            self.setHorizontalHeaderLabels(horizontalHeader)
×
483

484
        self.horizontalHeaderLabel = horizontalHeader
×
485
        self.verticalHeaderLabel = verticalHeaderLabels
×
486
        self.verticalHeaderBool = verticalHeader
×
487
        self.verticalHeaderModel = verticalHeaderModel
×
UNCOV
488
        self.horizontalHeader().setStretchLastSection(stretch)
×
489

490
        # readOnly state
491
        self.readOnly = readOnly
×
492
        if readOnly:
×
493
            self.setEditTriggers(
×
494
                QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers)
495
        else:
UNCOV
496
            self.setEditTriggers(
×
497
                QtWidgets.QAbstractItemView.EditTrigger.AllEditTriggers)
498
        if columnReadOnly is None:
×
499
            self.columnReadOnly = [self.readOnly]*self.columnas
×
500
        else:
501
            self.columnReadOnly = columnReadOnly
×
UNCOV
502
            for i in range(self.columnCount()):
×
UNCOV
503
                self.setColumnReadOnly(i, columnReadOnly[i])
×
504

505
        # Delegate functionality
506
        if delegate:
×
UNCOV
507
            self.setItemDelegate(delegate(self))
×
UNCOV
508
        self.delegateforRow = delegateforRow
×
509

UNCOV
510
        if dinamica:
×
511
            self.cellChanged.connect(self.tabla_cellChanged)
×
512
        self.dinamica = dinamica
×
513

514
        # self.setAlternatingRowColors(True)
515
        self.setGridStyle(QtCore.Qt.PenStyle.DotLine)
×
516
        self.orientacion = orientacion
×
UNCOV
517
        for i in range(filas):
×
UNCOV
518
            self.addRow()
×
519

520
    def setConnected(self):
1✔
521
        """The dynamic state can be defined at start or call this procedure"""
UNCOV
522
        self.cellChanged.connect(self.tabla_cellChanged)
×
523
        self.dinamica = True
×
524
        if self.rowCount() == 0:
×
525
            self.addRow()
×
526

527
    def addRow(self, data=None, index=None):
1✔
528
        """Add row to widget
529
        data: Array with data to fill new row
530
        index: Index to add row, default add row at endo of table"""
531
        if not data:
×
UNCOV
532
            data = [""]*self.columnas
×
533
        else:
UNCOV
534
            data = [representacion(i) for i in data]
×
535
        if index is not None:
×
536
            row = index
×
537
        else:
538
            row = self.rowCount()
×
UNCOV
539
        self.insertRow(row)
×
UNCOV
540
        self.setRowHeight(row, 22)
×
541

UNCOV
542
        if self.delegateforRow:
×
UNCOV
543
            delegate = self.delegateforRow(self.parent())
×
544
            self.setItemDelegateForRow(row, delegate)
×
545

UNCOV
546
        Config = ConfigParser()
×
547
        Config.read(conf_dir+"pychemqtrc")
×
548
        inactivo = QtGui.QColor(Config.get("General", 'Color_ReadOnly'))
×
549
        for j in range(self.columnCount()):
×
UNCOV
550
            self.setItem(row, j, QtWidgets.QTableWidgetItem(data[j]))
×
551
            self.item(row, j).setTextAlignment(
×
552
                self.orientacion | QtCore.Qt.AlignmentFlag.AlignVCenter)
553

UNCOV
554
            if self.columnReadOnly[j]:
×
555
                flags = QtCore.Qt.ItemFlag.ItemIsEnabled \
×
556
                    | QtCore.Qt.ItemFlag.ItemIsSelectable
557
                self.item(row, j).setBackground(inactivo)
×
558
            else:
559
                flags = QtCore.Qt.ItemFlag.ItemIsEditable \
×
560
                    | QtCore.Qt.ItemFlag.ItemIsEnabled \
561
                    | QtCore.Qt.ItemFlag.ItemIsSelectable
562
            self.item(row, j).setFlags(flags)
×
563

564
        self.setVHeader(row)
×
565

566
        # Set focus to first editable cell in new row
567
        if self.dinamica and self.rowCount() > 1:
×
568
            columna = self.columnReadOnly.index(False)
×
UNCOV
569
            self.setCurrentCell(row, columna)
×
570

571
    def setVHeader(self, row):
1✔
572
        """Set vertical header text"""
UNCOV
573
        if self.verticalHeaderBool:
×
UNCOV
574
            if self.verticalHeaderLabel:
×
575
                txt = self.verticalHeaderLabel[row]
×
UNCOV
576
            elif self.verticalHeaderModel:
×
577
                txt = self.verticalHeaderModel+str(row)
×
578
            else:
UNCOV
579
                txt = str(row+1)
×
580
            self.setVerticalHeaderItem(row, QtWidgets.QTableWidgetItem(txt))
×
581

582
    def tabla_cellChanged(self, i, j):
1✔
583
        """When edit a cell, check status to add new row"""
UNCOV
584
        new_line = True
×
UNCOV
585
        col = 0
×
586
        while col < self.columnas:
×
587
            if self.item(i, col).text() != "" or self.columnReadOnly[col]:
×
588
                col += 1
×
589
            else:
590
                new_line = False
×
UNCOV
591
                break
×
592
        if new_line and i == self.rowCount()-1:
×
593
            self.addRow()
×
UNCOV
594
            fila = self.getRow(i)
×
UNCOV
595
            self.rowFinished.emit(fila)
×
596

597
    def getValue(self, row, column):
1✔
598
        """Get value from cell in row and column"""
599
        txt = self.item(row, column).text()
×
600
        try:
×
601
            value = float(txt)
×
UNCOV
602
        except ValueError:
×
603
            value = txt
×
604
        return value
×
605

606
    def setValue(self, row, column, value, **fmt):
1✔
607
        """Set value for cell in row and column with text formating"""
608
        if isinstance(value, (float, int)):
×
UNCOV
609
            value = representacion(value, **fmt)
×
UNCOV
610
        self.item(row, column).setText(value)
×
UNCOV
611
        self.item(row, column).setTextAlignment(
×
612
            self.orientacion | QtCore.Qt.AlignmentFlag.AlignVCenter)
613

614
    def setColumn(self, column, data, **fmt):
1✔
615
        """Set data for a complete column"""
616
        while len(data) > self.rowCount()-self.verticalOffset:
×
617
            self.addRow()
×
UNCOV
618
        for row, dato in enumerate(data):
×
UNCOV
619
            self.setValue(row, column, dato, **fmt)
×
620

621
    def getColumn(self, column):
1✔
622
        """Get column data as array"""
623
        lst = []
×
624
        for row in range(self.verticalOffset, self.rowCount()):
×
UNCOV
625
            value = self.getValue(row, column)
×
UNCOV
626
            if isinstance(value, float):
×
UNCOV
627
                lst.append(value)
×
UNCOV
628
        return lst
×
629

630
    def getRow(self, row):
1✔
631
        """Get row data as array"""
632
        lst = []
×
UNCOV
633
        for column in range(self.columnCount()):
×
UNCOV
634
            lst.append(self.getValue(row, column))
×
UNCOV
635
        return lst
×
636

637
    def getData(self):
1✔
638
        """Get all table data as array"""
639
        matriz = []
×
640
        for row in range(self.verticalOffset, self.rowCount()-1):
×
641
            lst = self.getRow(row)
×
UNCOV
642
            matriz.append(lst)
×
UNCOV
643
        return matriz
×
644

645
    def setData(self, matriz):
1✔
646
        """Set table data"""
647
        for i in range(self.rowCount(), len(matriz)+self.verticalOffset):
×
648
            self.addRow()
×
UNCOV
649
        for fila in range(self.rowCount()-self.verticalOffset):
×
UNCOV
650
            for columna, dato in enumerate(matriz[fila]):
×
651
                # self.setVerticalHeaderItem(fila, QtWidgets.QTableWidgetItem(
652
                    # self.verticalHeaderModel+str(fila)))
653
                self.item(fila+self.verticalOffset, columna).setText(str(dato))
×
654
                self.item(fila+self.verticalOffset, columna).setTextAlignment(
×
655
                    self.orientacion | QtCore.Qt.AlignmentFlag.AlignVCenter)
656
        for i in range(self.verticalOffset, self.rowCount()):
×
UNCOV
657
            self.setRowHeight(i+self.verticalOffset, 20)
×
658

659
    def clear(self, size=True):
1✔
660
        """Clear table, remove all data and optionally remove all row"""
661
        if size:
×
662
            self.setRowCount(1+self.verticalOffset)
×
663
        for fila in range(self.rowCount()):
×
UNCOV
664
            for columna in range(self.columnas):
×
UNCOV
665
                self.item(fila+self.verticalOffset, columna).setText("")
×
666

667
    def setColumnReadOnly(self, column, boolean):
1✔
668
        """Set readonly estate per column"""
669
        if boolean:
×
670
            flags = QtCore.Qt.ItemFlag.ItemIsEnabled \
×
671
                | QtCore.Qt.ItemFlag.ItemIsSelectable
672
        else:
UNCOV
673
            flags = QtCore.Qt.ItemFlag.ItemIsEditable \
×
674
                | QtCore.Qt.ItemFlag.ItemIsEnabled \
675
                | QtCore.Qt.ItemFlag.ItemIsSelectable
676

677
        for row in range(self.rowCount()):
×
678
            self.item(row, column).setFlags(flags)
×
679

680
    def leaveEvent(self, event):
1✔
681
        """Emit editingFinished when focus leave widget"""
682
        if self.isEnabled():
×
683
            self.editingFinished.emit()
×
684

685

686
class ClickableLabel(QtWidgets.QLabel):
1✔
687
    """Custom QLabel with clicked functionality"""
688
    clicked = QtCore.pyqtSignal()
1✔
689

690
    def mousePressEvent(self, event):
1✔
691
        """Use mouse press event to simulate clicked signal"""
UNCOV
692
        self.clicked.emit()
×
693

694

695
class ColorSelector(QtWidgets.QWidget):
1✔
696
    """Color selector widget"""
697
    valueChanged = QtCore.pyqtSignal('QString')
1✔
698

699
    def __init__(self, color="#ffffff", alpha=255, isAlpha=False, parent=None):
1✔
UNCOV
700
        super().__init__(parent)
×
701

UNCOV
702
        lyt = QtWidgets.QHBoxLayout(self)
×
UNCOV
703
        lyt.setContentsMargins(0, 0, 0, 0)
×
UNCOV
704
        lyt.setSpacing(0)
×
705

UNCOV
706
        self.RGB = QtWidgets.QLineEdit()
×
UNCOV
707
        self.RGB.editingFinished.connect(self.rgbChanged)
×
UNCOV
708
        self.RGB.setFixedSize(80, 24)
×
UNCOV
709
        lyt.addWidget(self.RGB)
×
UNCOV
710
        self.button = QtWidgets.QToolButton()
×
UNCOV
711
        self.button.setFixedSize(24, 24)
×
UNCOV
712
        self.button.clicked.connect(self.ColorButtonClicked)
×
713
        lyt.addWidget(self.button)
×
UNCOV
714
        lyt.addItem(QtWidgets.QSpacerItem(
×
715
            20, 20, QtWidgets.QSizePolicy.Policy.Expanding,
716
            QtWidgets.QSizePolicy.Policy.Fixed))
717

UNCOV
718
        self.isAlpha = isAlpha
×
719
        if isAlpha:
×
720
            self.name = QtGui.QColor.NameFormat.HexArgb
×
721
        else:
722
            self.name = QtGui.QColor.NameFormat.HexRgb
×
723

724
        r = int(color[1:3], 16)
×
725
        g = int(color[3:5], 16)
×
726
        b = int(color[5:7], 16)
×
727
        color = QtGui.QColor(r, g, b, alpha)
×
UNCOV
728
        self.setColor(color)
×
729

730
    def setColor(self, color, alpha=255):
1✔
731
        """Set new color value and update text and button color"""
732
        # Accept color args as a #rgb string too
733
        if isinstance(color, str):
×
UNCOV
734
            color = QtGui.QColor(color)
×
735
            color.setAlpha(alpha)
×
UNCOV
736
        self.color = color
×
737
        self.button.setStyleSheet(f"background: {color.name(self.name)};")
×
738
        self.RGB.setText(color.name(self.name))
×
739

740
    def ColorButtonClicked(self):
1✔
741
        """Show the QColorDialog to let user choose new color"""
UNCOV
742
        dlg = QtWidgets.QColorDialog(self.color, self)
×
UNCOV
743
        if self.isAlpha:
×
UNCOV
744
            dlg.setOption(
×
745
                QtWidgets.QColorDialog.ColorDialogOption.ShowAlphaChannel)
746
        if dlg.exec():
×
747
            self.setColor(dlg.currentColor())
×
748
            self.valueChanged.emit(dlg.currentColor().name())
×
749

750
    def rgbChanged(self):
1✔
751
        """Let user define the color manually"""
UNCOV
752
        txt = self.RGB.text()
×
753

754
        # Avoid the editing finished with no changes
755
        if txt == self.color.name(self.name):
×
756
            return
×
757

758
        # Define the new color from text
759
        # From Qt 6.4 qcolor can read a color by name including the SVG color
760
        # keyword names
761
        # https://www.w3.org/TR/SVG11/types.html#ColorKeywords
762
        # Anyway it's retain this manual reading code for qt5 compatibility
UNCOV
763
        if self.isAlpha:
×
UNCOV
764
            alpha = int(txt[1:3], 16)
×
765
            r = int(txt[3:5], 16)
×
UNCOV
766
            g = int(txt[5:7], 16)
×
UNCOV
767
            b = int(txt[7:9], 16)
×
768
            color = QtGui.QColor(r, g, b, alpha)
×
769
        else:
UNCOV
770
            color = QtGui.QColor(txt)
×
771

772
        # Only accept new value if it's valid
UNCOV
773
        if color.isValid():
×
UNCOV
774
            self.setColor(color)
×
UNCOV
775
            self.valueChanged.emit(color.name(self.name))
×
776

777

778
class DragButton(QtWidgets.QToolButton):
1✔
779
    """Class to define a special button with drag/drop supoort"""
780

781
    def __init__(self, parent=None):
1✔
UNCOV
782
        super().__init__(parent)
×
783
        self.dragStartPosition = QtCore.QPointF()
×
784

785
    def mousePressEvent(self, event):
1✔
786
        """Register the start position to do the drag action and enable the
787
        normal clicking action"""
788
        if event.button() == QtCore.Qt.MouseButton.LeftButton:
×
UNCOV
789
            self.dragStartPosition = event.pos()
×
UNCOV
790
        QtWidgets.QToolButton.mousePressEvent(self, event)
×
791

792
    def mouseMoveEvent(self, event):
1✔
793
        """Do the drag & drop actions"""
794

795
        if (event.pos()-self.dragStartPosition).manhattanLength() \
×
796
                < QtWidgets.QApplication.startDragDistance():
UNCOV
797
            return
×
798

799
        # Disable drag action if button is the stream button
UNCOV
800
        if self.isCheckable():
×
801
            return
×
802

803
        data = QtCore.QByteArray()
×
UNCOV
804
        mimeData = QtCore.QMimeData()
×
UNCOV
805
        mimeData.setData("application/x-equipment", data)
×
UNCOV
806
        drag = QtGui.QDrag(self)
×
UNCOV
807
        drag.setMimeData(mimeData)
×
808
        pixmap = self.icon().pixmap(60, 60)
×
UNCOV
809
        drag.setHotSpot(QtCore.QPoint(0, 0))
×
810
        drag.setPixmap(pixmap)
×
UNCOV
811
        drag.exec(QtCore.Qt.DropAction.CopyAction)
×
812

813

814
class PathConfig(QtWidgets.QWidget):
1✔
815
    """Custom widget for a file path show and configure functionality"""
816
    valueChanged = QtCore.pyqtSignal('QString')
1✔
817

818
    def __init__(self, title="", path="", patron="", msg="", folder=False,
1✔
819
                 save=False, parent=None):
820
        """
821
        title: Optional text an right of widget
822
        path: Inicial value for file path
823
        patron: File format to filter in file search dialog
824
        msg: Title of dialog file
825
        """
UNCOV
826
        super().__init__(parent)
×
827

UNCOV
828
        self.folder = folder
×
UNCOV
829
        self.save = save
×
830

UNCOV
831
        layout = QtWidgets.QHBoxLayout(self)
×
UNCOV
832
        layout.setContentsMargins(0, 0, 0, 0)
×
UNCOV
833
        layout.setSpacing(0)
×
834

UNCOV
835
        if title:
×
UNCOV
836
            layout.addWidget(QtWidgets.QLabel(title))
×
UNCOV
837
            layout.addItem(QtWidgets.QSpacerItem(
×
838
                10, 10, QtWidgets.QSizePolicy.Policy.Fixed,
839
                QtWidgets.QSizePolicy.Policy.Fixed))
840

841
        self.path = QtWidgets.QLineEdit()
×
842
        self.path.setFixedHeight(24)
×
UNCOV
843
        self.path.textEdited.connect(self.pathEdited)
×
844
        layout.addWidget(self.path)
×
845
        self.boton = QtWidgets.QPushButton(self.tr("Browse"))
×
846
        self.boton.setFixedHeight(24)
×
UNCOV
847
        self.boton.clicked.connect(self.select)
×
848
        layout.addWidget(self.boton)
×
849

850
        # Define default values for parameters don't defined
UNCOV
851
        if not patron:
×
UNCOV
852
            patron = self.tr("All files") + "(*)"
×
UNCOV
853
        elif patron == "exe":
×
854
            if sys.platform == "win32":
×
855
                patron = self.tr("Executable files") + "(*.exe *.bat)"
×
856
            else:
857
                patron = self.tr("All files") + "(*)"
×
858
        self.patron = patron
×
859

860
        if not msg:
×
861
            msg = self.tr("Select path of file")
×
UNCOV
862
        self.msg = msg
×
UNCOV
863
        self.setText(path)
×
864

865
    def text(self):
1✔
866
        return self.path.text()
×
867

868
    def setText(self, text):
1✔
UNCOV
869
        self.path.setText(text)
×
870

871
    def select(self):
1✔
872
        """Open the QFileDialog to select the file path"""
873
        folder = os.path.dirname(str(self.path.text()))
×
874
        if self.save:
×
875
            ruta = QtWidgets.QFileDialog.getSaveFileName(
×
876
                self, self.msg, folder, self.patron)[0]
UNCOV
877
        elif self.folder:
×
UNCOV
878
            ruta = QtWidgets.QFileDialog.getExistingDirectory(
×
879
                self, self.msg, folder)
880
        else:
UNCOV
881
            ruta = QtWidgets.QFileDialog.getOpenFileName(
×
882
                self, self.msg, folder, self.patron)[0]
UNCOV
883
        if ruta:
×
UNCOV
884
            self.path.setText(ruta)
×
UNCOV
885
            self.valueChanged.emit(ruta)
×
886

887
    def pathEdited(self, path):
1✔
888
        """Emit valueChanged signals when path is changed"""
UNCOV
889
        if os.path.isfile(path):
×
890
            self.valueChanged.emit(path)
×
891

892

893
class LineConfig(QtWidgets.QGroupBox):
1✔
894
    """Custom QGroupbox with all matplotlib Line configuration"""
895

896
    def __init__(self, confSection, title, parent=None):
1✔
897
        """
898
        confSection: Name key to identify the line
899
        title: Title to use in QGroupbox
900
        """
UNCOV
901
        super().__init__(title, parent)
×
902
        self.conf = confSection
×
903

UNCOV
904
        layout = QtWidgets.QVBoxLayout(self)
×
UNCOV
905
        lyt1 = QtWidgets.QHBoxLayout()
×
UNCOV
906
        self.Width = QtWidgets.QDoubleSpinBox()
×
UNCOV
907
        self.Width.setFixedWidth(60)
×
UNCOV
908
        self.Width.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight)
×
UNCOV
909
        self.Width.setRange(0.1, 5)
×
UNCOV
910
        self.Width.setDecimals(1)
×
UNCOV
911
        self.Width.setSingleStep(0.1)
×
UNCOV
912
        self.Width.setToolTip(self.tr("Line width"))
×
UNCOV
913
        lyt1.addWidget(self.Width)
×
914
        self.Style = LineStyleCombo()
×
915
        self.Style.setToolTip(self.tr("Line style"))
×
UNCOV
916
        lyt1.addWidget(self.Style)
×
917
        self.Color = ColorSelector(isAlpha=True)
×
918
        self.Color.setToolTip(self.tr("Line color"))
×
919
        lyt1.addWidget(self.Color)
×
920
        self.Marker = MarkerCombo()
×
921
        self.Marker.setToolTip(self.tr("Line marker"))
×
922
        self.Marker.currentIndexChanged.connect(self.changeMarker)
×
923
        lyt1.addWidget(self.Marker)
×
924
        lyt1.addItem(QtWidgets.QSpacerItem(
×
925
            10, 10, QtWidgets.QSizePolicy.Policy.Expanding,
926
            QtWidgets.QSizePolicy.Policy.Fixed))
927
        layout.addLayout(lyt1)
×
928

929
        lyt2 = QtWidgets.QHBoxLayout()
×
930
        self.MarkerSize = QtWidgets.QDoubleSpinBox()
×
931
        self.MarkerSize.setFixedWidth(60)
×
932
        self.MarkerSize.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight)
×
933
        self.MarkerSize.setRange(0.1, 5)
×
934
        self.MarkerSize.setDecimals(1)
×
935
        self.MarkerSize.setSingleStep(0.1)
×
936
        self.MarkerSize.setToolTip(self.tr("Marker size"))
×
937
        lyt2.addWidget(self.MarkerSize)
×
UNCOV
938
        self.MarkerColor = ColorSelector()
×
UNCOV
939
        self.MarkerColor.setToolTip(self.tr("Marker face color"))
×
940
        lyt2.addWidget(self.MarkerColor)
×
UNCOV
941
        self.EdgeSize = QtWidgets.QDoubleSpinBox()
×
942
        self.EdgeSize.setFixedWidth(60)
×
943
        self.EdgeSize.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight)
×
944
        self.EdgeSize.setRange(0.1, 5)
×
945
        self.EdgeSize.setDecimals(1)
×
946
        self.EdgeSize.setSingleStep(0.1)
×
947
        self.EdgeSize.setToolTip(self.tr("Marker edge width"))
×
948
        lyt2.addWidget(self.EdgeSize)
×
949
        self.EdgeColor = ColorSelector()
×
950
        self.EdgeColor.setToolTip(self.tr("Marker edge color"))
×
951
        lyt2.addWidget(self.EdgeColor)
×
952
        layout.addLayout(lyt2)
×
953

954
        self.changeMarker(0)
×
955

956
    def changeMarker(self, index):
1✔
957
        """Update visibility of marker properties widget when any marker is
958
        selected"""
959
        self.MarkerSize.setVisible(index)
×
960
        self.MarkerColor.setVisible(index)
×
961
        self.EdgeSize.setVisible(index)
×
962
        self.EdgeColor.setVisible(index)
×
963

964
    def setConfig(self, config, section="MEOS"):
1✔
965
        """Set widget values from config"""
UNCOV
966
        alfa = config.getint(section, self.conf+"alpha")
×
967
        self.Color.setColor(config.get(section, self.conf+'Color'), alfa)
×
UNCOV
968
        self.Width.setValue(config.getfloat(section, self.conf+'lineWidth'))
×
UNCOV
969
        self.Style.setCurrentValue(config.get(section, self.conf+'lineStyle'))
×
UNCOV
970
        if self.Marker.isEnabled():
×
UNCOV
971
            self.Marker.setCurrentValue(config.get(section, self.conf+'marker'))
×
972
            self.MarkerSize.setValue(
×
973
                config.getfloat(section, self.conf+'markersize'))
974
            self.MarkerColor.setColor(
×
975
                config.get(section, self.conf+'markerfacecolor'), alfa)
UNCOV
976
            self.EdgeSize.setValue(
×
977
                config.getfloat(section, self.conf+'markeredgewidth'))
UNCOV
978
            self.EdgeColor.setColor(
×
979
                config.get(section, self.conf+'markeredgecolor'), alfa)
980

981
    def value(self, config, section="MEOS"):
1✔
982
        """Update ConfigParser instance with the config"""
983
        config.set(section, self.conf+"Color", self.Color.color.name())
×
984
        config.set(section, self.conf+"alpha", str(self.Color.color.alpha()))
×
985
        config.set(section, self.conf+"lineWidth", str(self.Width.value()))
×
UNCOV
986
        config.set(section, self.conf+"lineStyle", self.Style.currentValue())
×
987
        if self.Marker.isEnabled():
×
UNCOV
988
            config.set(section, self.conf+"marker", self.Marker.currentValue())
×
989
            config.set(section, self.conf+"markersize",
×
990
                       str(self.MarkerSize.value()))
991
            config.set(section, self.conf+"markerfacecolor",
×
992
                       self.MarkerColor.color.name())
UNCOV
993
            config.set(section, self.conf+"markeredgewidth",
×
994
                       str(self.EdgeSize.value()))
UNCOV
995
            config.set(section, self.conf+"markeredgecolor",
×
996
                       self.EdgeColor.color.name())
997

998
        return config
×
999

1000

1001
class GridConfig(LineConfig):
1✔
1002
    """Extended version of LineConfig for grid specific line"""
1003

1004
    WHICH = ["both", "major", "minor"]
1✔
1005
    AXIS = ["both", "x", "y"]
1✔
1006

1007
    def __init__(self, confSection, title, parent=None):
1✔
1008
        """
1009
        confSection: Name key to identify the line
1010
        title: Title to use in QGroupbox
1011
        """
UNCOV
1012
        super().__init__(confSection, title, parent)
×
1013

1014
        # Disable marker functionality for grid line
1015
        # Raise a error by duplicate marker kwarg in plot, anyway marker in
1016
        # grid line are useless
UNCOV
1017
        self.Marker.setEnabled(False)
×
1018

UNCOV
1019
        self.grid = QtWidgets.QCheckBox(self.tr("Show grid"))
×
UNCOV
1020
        self.layout().insertWidget(0, self.grid)
×
1021

UNCOV
1022
        lyt = QtWidgets.QHBoxLayout()
×
UNCOV
1023
        lyt.addWidget(QtWidgets.QLabel(self.tr("Which") + ":"))
×
UNCOV
1024
        self.gridWhich = QtWidgets.QComboBox()
×
1025
        for name in self.WHICH:
×
UNCOV
1026
            self.gridWhich.addItem(name)
×
UNCOV
1027
        lyt.addWidget(self.gridWhich)
×
UNCOV
1028
        lyt.addItem(QtWidgets.QSpacerItem(
×
1029
            10, 10, QtWidgets.QSizePolicy.Policy.Expanding,
1030
            QtWidgets.QSizePolicy.Policy.Fixed))
UNCOV
1031
        self.layout().insertLayout(1, lyt)
×
1032

1033
        lyt = QtWidgets.QHBoxLayout()
×
UNCOV
1034
        lyt.addWidget(QtWidgets.QLabel(self.tr("Axis") + ":"))
×
1035
        self.gridAxis = QtWidgets.QComboBox()
×
1036
        for name in self.AXIS:
×
1037
            self.gridAxis.addItem(name)
×
1038
        lyt.addWidget(self.gridAxis)
×
1039
        lyt.addItem(QtWidgets.QSpacerItem(
×
1040
            10, 10, QtWidgets.QSizePolicy.Policy.Expanding,
1041
            QtWidgets.QSizePolicy.Policy.Fixed))
UNCOV
1042
        self.layout().insertLayout(2, lyt)
×
1043

1044
    def setConfig(self, config, section):
1✔
1045
        """Set widget values from config"""
1046
        LineConfig.setConfig(self, config, section)
×
1047
        self.grid.setChecked(config.getboolean(section, 'grid'))
×
1048
        txt = config.get(section, 'gridwhich')
×
1049
        self.gridWhich.setCurrentIndex(self.WHICH.index(txt))
×
1050
        txt = config.get(section, 'gridaxis')
×
1051
        self.gridAxis.setCurrentIndex(self.AXIS.index(txt))
×
1052

1053
    def value(self, config, section):
1✔
1054
        """Update ConfigParser instance with the config"""
1055
        LineConfig.value(self, config, section)
×
UNCOV
1056
        config.set(section, "grid", str(self.grid.isChecked()))
×
UNCOV
1057
        config.set(section, "gridwhich", self.gridWhich.currentText())
×
UNCOV
1058
        config.set(section, "gridaxis", self.gridAxis.currentText())
×
1059
        return config
×
1060

1061

1062
class CustomCombo(QtWidgets.QComboBox):
1✔
1063
    """General custom QComboBox"""
1064
    valueChanged = QtCore.pyqtSignal("QString")
1✔
1065

1066
    def __init__(self, parent=None):
1✔
UNCOV
1067
        super().__init__(parent)
×
1068
        self.setIconSize(QtCore.QSize(35, 18))
×
1069
        self.currentIndexChanged.connect(self.emit)
×
1070
        self._populate()
×
1071

1072
    def setCurrentValue(self, value):
1✔
1073
        """Overload QComboBox setCurrentValue to let text input from keymap"""
UNCOV
1074
        ind = self.key.index(value)
×
UNCOV
1075
        self.setCurrentIndex(ind)
×
1076

1077
    def currentValue(self):
1✔
1078
        """Convert index in value from keymap"""
UNCOV
1079
        return self.key[self.currentIndex()]
×
1080

1081
    def emit(self, ind):
1✔
1082
        """Emit signal change integer for keymap value"""
1083
        self.valueChanged.emit(self.key[ind])
×
1084

1085

1086
class LineStyleCombo(CustomCombo):
1✔
1087
    """Custom QComboBox for select matplotlib line styles"""
1088
    key = ["None", "-", "--", ":", "-."]
1✔
1089
    image = {
1✔
1090
        "None": "",
1091
        "-": os.path.join("images", "button", "solid_line.png"),
1092
        "--": os.path.join("images", "button", "dash_line.png"),
1093
        ":": os.path.join("images", "button", "dot_line.png"),
1094
        "-.": os.path.join("images", "button", "dash_dot_line.png")}
1095

1096
    def _populate(self):
1✔
UNCOV
1097
        for key in self.key:
×
UNCOV
1098
            self.addItem(QtGui.QIcon(QtGui.QPixmap(
×
1099
                os.environ["pychemqt"] + self.image[key])), "")
1100

1101

1102
class PFDLineCombo(LineStyleCombo):
1✔
1103
    """Custom QComboBox for select PFD line styles for stream"""
1104
    key = [QtCore.Qt.PenStyle.SolidLine, QtCore.Qt.PenStyle.DashLine,
1✔
1105
           QtCore.Qt.PenStyle.DotLine, QtCore.Qt.PenStyle.DashDotLine,
1106
           QtCore.Qt.PenStyle.DashDotDotLine]
1107
    image = {
1✔
1108
        key[0]: os.path.join("images", "button", "solid_line.png"),
1109
        key[1]: os.path.join("images", "button", "dash_line.png"),
1110
        key[2]: os.path.join("images", "button", "dot_line.png"),
1111
        key[3]: os.path.join("images", "button", "dash_dot_line.png"),
1112
        key[4]: os.path.join("images", "button", "dash_dot_dot_line.png")}
1113

1114
    valueChanged = QtCore.pyqtSignal(int)
1✔
1115

1116

1117
class MarkerCombo(CustomCombo):
1✔
1118
    """Custom QComboBox for select matplotlib line marker"""
1119
    key = ["None", ".", ",", "o", "v", "^", "<", ">", "1", "2", "3", "4", "8",
1✔
1120
           "s", "p", "*", "h", "H", "+", "x", "D", "d", "|", "_"]
1121
    text = {"None": "", ".": "point", ",": "pixel", "o": "circle",
1✔
1122
            "v": "triangle_down", "^": "triangle_up", "<": "triangle_left",
1123
            ">": "triangle_right", "1": "tri_down", "2": "tri_up",
1124
            "3": "tri_left", "4": "tri_right", "8": "octagon", "s": "square",
1125
            "p": "pentagon", "*": "star", "h": "hexagon1", "H": "hexagon2",
1126
            "+": "plus", "x": "x", "D": "diamond", "d": "thin_diamond",
1127
            "|": "vline", "_": "hline"}
1128

1129
    def _populate(self):
1✔
UNCOV
1130
        for key in self.key:
×
UNCOV
1131
            txt = self.text[key]
×
UNCOV
1132
            if txt:
×
UNCOV
1133
                image = os.environ["pychemqt"] + \
×
1134
                    os.path.join("images", "marker", f"{txt}.png")
UNCOV
1135
                self.addItem(QtGui.QIcon(QtGui.QPixmap(image)), self.text[key])
×
1136
            else:
UNCOV
1137
                self.addItem(self.text[key])
×
1138

1139

1140
class NumericFactor(QtWidgets.QDialog):
1✔
1141
    """Numeric format configuration dialog"""
1142
    def __init__(self, config, unit=None, order=0, parent=None):
1✔
1143
        super().__init__(parent)
×
1144
        self.setWindowTitle(self.tr("Format"))
×
1145
        layout = QtWidgets.QGridLayout(self)
×
1146
        self.checkFixed = QtWidgets.QRadioButton(self.tr("Fixed decimal point"))
×
UNCOV
1147
        layout.addWidget(self.checkFixed, 1, 1, 1, 3)
×
1148
        layout.addWidget(QtWidgets.QLabel(self.tr("Total digits")), 2, 2)
×
UNCOV
1149
        self.TotalDigits = Entrada_con_unidades(
×
1150
            int, width=45, value=0, boton=False, spinbox=True, min=0, max=12,
1151
            showNull=True)
UNCOV
1152
        layout.addWidget(self.TotalDigits, 2, 3)
×
UNCOV
1153
        layout.addWidget(QtWidgets.QLabel(self.tr("Decimal digits")), 3, 2)
×
UNCOV
1154
        self.DecimalDigits = Entrada_con_unidades(
×
1155
            int, width=45, value=4, boton=False, spinbox=True, min=1, max=12)
1156
        layout.addWidget(self.DecimalDigits, 3, 3)
×
1157
        self.checkSignificant = QtWidgets.QRadioButton(
×
1158
            self.tr("Significant figures"))
1159
        layout.addWidget(self.checkSignificant, 4, 1, 1, 3)
×
1160
        layout.addWidget(QtWidgets.QLabel(self.tr("Figures")), 5, 2)
×
1161
        self.FiguresSignificatives = Entrada_con_unidades(
×
1162
            int, width=45, value=5, boton=False, spinbox=True, min=1, max=12)
UNCOV
1163
        layout.addWidget(self.FiguresSignificatives, 5, 3)
×
UNCOV
1164
        self.checkExp = QtWidgets.QRadioButton(
×
1165
            self.tr("Exponential preferred"))
1166
        layout.addWidget(self.checkExp, 6, 1, 1, 3)
×
1167
        layout.addWidget(QtWidgets.QLabel(self.tr("Figures")), 7, 2)
×
UNCOV
1168
        self.FiguresExponential = Entrada_con_unidades(
×
1169
            int, width=45, value=5, boton=False, spinbox=True, min=1, max=12)
1170
        layout.addWidget(self.FiguresExponential, 7, 3)
×
UNCOV
1171
        layout.addItem(QtWidgets.QSpacerItem(
×
1172
            30, 20, QtWidgets.QSizePolicy.Policy.Fixed,
1173
            QtWidgets.QSizePolicy.Policy.Fixed), 8, 1)
1174
        self.checkExpVariable = QtWidgets.QCheckBox(
×
1175
            self.tr("Exponential for big/small values"))
1176
        layout.addWidget(self.checkExpVariable, 9, 1, 1, 3)
×
1177
        self.labelTolerancia = QtWidgets.QLabel(self.tr("Tolerance"))
×
UNCOV
1178
        layout.addWidget(self.labelTolerancia, 10, 2)
×
1179
        self.Tolerance = Entrada_con_unidades(
×
1180
            int, width=45, value=4, boton=False, spinbox=True, min=0, max=12)
1181
        layout.addWidget(self.Tolerance, 10, 3)
×
UNCOV
1182
        self.checkSign = QtWidgets.QCheckBox(
×
1183
            self.tr("Show sign in positive values"))
1184
        layout.addWidget(self.checkSign, 11, 1, 1, 3)
×
UNCOV
1185
        self.checkThousand = QtWidgets.QCheckBox(
×
1186
            self.tr("Show thousand separator"))
1187
        layout.addWidget(self.checkThousand, 12, 1, 1, 3)
×
1188

1189
        self.checkFixed.toggled.connect(self.TotalDigits.setNotReadOnly)
×
1190
        self.checkFixed.toggled.connect(self.DecimalDigits.setNotReadOnly)
×
1191
        self.checkSignificant.toggled.connect(
×
1192
            self.FiguresSignificatives.setNotReadOnly)
UNCOV
1193
        self.checkExp.toggled.connect(self.ExpToggled)
×
1194
        self.checkExp.toggled.connect(self.FiguresExponential.setNotReadOnly)
×
1195
        self.checkExpVariable.toggled.connect(self.Tolerance.setNotReadOnly)
×
UNCOV
1196
        self.checkExpVariable.toggled.connect(self.labelTolerancia.setEnabled)
×
1197

1198
        layout.addItem(QtWidgets.QSpacerItem(
×
1199
            20, 10, QtWidgets.QSizePolicy.Policy.Fixed,
1200
            QtWidgets.QSizePolicy.Policy.Fixed), 13, 1)
UNCOV
1201
        self.muestra = QtWidgets.QLabel()
×
1202
        layout.addWidget(self.muestra, 14, 1, 1, 3)
×
1203

1204
        buttonBox = QtWidgets.QDialogButtonBox()
×
UNCOV
1205
        buttonBox.setStandardButtons(
×
1206
            QtWidgets.QDialogButtonBox.StandardButton.Cancel
1207
            | QtWidgets.QDialogButtonBox.StandardButton.Ok)
1208
        buttonBox.accepted.connect(self.accept)
×
1209
        buttonBox.rejected.connect(self.reject)
×
UNCOV
1210
        layout.addWidget(buttonBox, 20, 1, 1, 3)
×
1211

UNCOV
1212
        self.checkFixed.setChecked(config["fmt"] == 0)
×
UNCOV
1213
        self.TotalDigits.setReadOnly(config["fmt"] != 0)
×
1214
        self.DecimalDigits.setReadOnly(config["fmt"] != 0)
×
1215
        self.checkSignificant.setChecked(config["fmt"] == 1)
×
UNCOV
1216
        self.FiguresSignificatives.setReadOnly(config["fmt"] != 1)
×
1217
        self.checkExp.setChecked(config["fmt"] == 2)
×
1218
        self.FiguresExponential.setReadOnly(config["fmt"] != 2)
×
UNCOV
1219
        if config["fmt"] == 0:
×
UNCOV
1220
            self.DecimalDigits.setValue(config["decimales"])
×
1221
        elif config["fmt"] == 1:
×
1222
            self.FiguresSignificatives.setValue(config["decimales"])
×
1223
        else:
UNCOV
1224
            self.FiguresExponential.setValue(config["decimales"])
×
1225
        if "total" in config:
×
1226
            self.TotalDigits.setValue(config["total"])
×
1227
        if "exp" in config:
×
1228
            self.checkExpVariable.setChecked(config["exp"])
×
1229
        if "tol" in config:
×
1230
            self.Tolerance.setValue(config["tol"])
×
1231
        self.Tolerance.setReadOnly(not config.get("exp", False))
×
1232
        if "signo" in config:
×
1233
            self.checkSign.setChecked(config["signo"])
×
1234
        if "thousand" in config:
×
1235
            self.checkThousand.setChecked(config["thousand"])
×
1236

1237
        self.updateMuestra()
×
1238
        self.checkFixed.toggled.connect(self.updateMuestra)
×
1239
        self.checkSignificant.toggled.connect(self.updateMuestra)
×
1240
        self.checkExp.toggled.connect(self.updateMuestra)
×
1241
        self.checkExpVariable.toggled.connect(self.updateMuestra)
×
1242
        self.TotalDigits.valueChanged.connect(self.updateMuestra)
×
1243
        self.DecimalDigits.valueChanged.connect(self.updateMuestra)
×
1244
        self.FiguresSignificatives.valueChanged.connect(self.updateMuestra)
×
1245
        self.FiguresExponential.valueChanged.connect(self.updateMuestra)
×
1246
        self.Tolerance.valueChanged.connect(self.updateMuestra)
×
1247
        self.checkSign.toggled.connect(self.updateMuestra)
×
1248
        self.checkThousand.toggled.connect(self.updateMuestra)
×
1249

1250
        if unit and unit.__text__:
×
1251
            layout.addItem(QtWidgets.QSpacerItem(
×
1252
                20, 10, QtWidgets.QSizePolicy.Policy.Fixed,
1253
                QtWidgets.QSizePolicy.Policy.Fixed), 15, 1, 1, 3)
1254
            self.muestra = QtWidgets.QLabel()
×
1255
            layout.addWidget(QtWidgets.QLabel(self.tr("Convert units")), 16, 1)
×
1256
            self.unit = QtWidgets.QComboBox()
×
1257
            for txt in unit.__text__:
×
1258
                self.unit.addItem(txt)
×
1259
            self.unit.setCurrentIndex(order)
×
1260
            layout.addWidget(self.unit, 16, 2, 1, 2)
×
1261

1262
    def ExpToggled(self, boolean):
1✔
1263
        """Exponential checkbox toggled"""
1264
        self.FiguresExponential.setNotReadOnly(boolean)
×
UNCOV
1265
        self.checkExpVariable.setDisabled(boolean)
×
UNCOV
1266
        if self.checkExpVariable.isChecked():
×
1267
            self.labelTolerancia.setDisabled(False)
×
1268
            self.Tolerance.setReadOnly(True)
×
1269

1270
    def updateMuestra(self):
1✔
1271
        """Update numeric sample"""
1272
        kwargs = self.args()
×
1273
        txt = self.tr("Sample") + ": " + representacion(pi*1e4, **kwargs)
×
UNCOV
1274
        self.muestra.setText(txt)
×
1275

1276
    def args(self):
1✔
1277
        """Return a dict with the selected numeric representation parameters"""
1278
        kwarg = {}
×
1279
        if self.checkFixed.isChecked():
×
1280
            kwarg["fmt"] = 0
×
1281
            kwarg["total"] = self.TotalDigits.value
×
UNCOV
1282
            kwarg["decimales"] = self.DecimalDigits.value
×
UNCOV
1283
        elif self.checkSignificant.isChecked():
×
UNCOV
1284
            kwarg["fmt"] = 1
×
1285
            kwarg["decimales"] = self.FiguresSignificatives.value
×
1286
        else:
1287
            kwarg["fmt"] = 2
×
UNCOV
1288
            kwarg["decimales"] = self.FiguresExponential.value
×
UNCOV
1289
        kwarg["eng"] = self.checkExpVariable.isEnabled() and \
×
1290
            self.checkExpVariable.isChecked()
1291
        kwarg["tol"] = self.Tolerance.value
×
1292
        kwarg["signo"] = self.checkSign.isChecked()
×
1293
        kwarg["thousand"] = self.checkThousand.isChecked()
×
1294
        return kwarg
×
1295

1296

1297
class InputFont(QtWidgets.QWidget):
1✔
1298
    """Custom widget to edit a text input with font and color support"""
1299
    textChanged = QtCore.pyqtSignal("QString")
1✔
1300
    fontChanged = QtCore.pyqtSignal("QFont")
1✔
1301
    colorChanged = QtCore.pyqtSignal("QString")
1✔
1302

1303
    def __init__(self, text="", font=None, color="#000000", parent=None):
1✔
1304
        """
1305
        text: Initial txt for widget
1306
        font: QFont instance to initialize widget
1307
        color: Inicial color to widget, in code #rrggbb
1308
        """
UNCOV
1309
        super().__init__(parent)
×
1310

UNCOV
1311
        layout = QtWidgets.QHBoxLayout(self)
×
UNCOV
1312
        layout.setContentsMargins(0, 0, 0, 0)
×
UNCOV
1313
        layout.setSpacing(0)
×
1314

UNCOV
1315
        self.lineEdit = QtWidgets.QLineEdit()
×
UNCOV
1316
        self.lineEdit.setFixedHeight(24)
×
UNCOV
1317
        layout.addWidget(self.lineEdit)
×
UNCOV
1318
        self.fontButton = QtWidgets.QPushButton(QtGui.QIcon(QtGui.QPixmap(
×
1319
            os.path.join(IMAGE_PATH, "button", "font.png"))), "")
UNCOV
1320
        self.fontButton.setAutoDefault(False)
×
UNCOV
1321
        self.fontButton.setDefault(False)
×
1322
        self.fontButton.setFixedSize(24, 24)
×
UNCOV
1323
        self.fontButton.setIconSize(QtCore.QSize(24, 24))
×
1324
        self.fontButton.clicked.connect(self.fontButtonClicked)
×
1325
        layout.addWidget(self.fontButton)
×
1326
        self.colorButton = QtWidgets.QToolButton()
×
UNCOV
1327
        self.colorButton.setFixedSize(24, 24)
×
1328
        self.colorButton.clicked.connect(self.colorButtonClicked)
×
1329
        layout.addWidget(self.colorButton)
×
1330

1331
        self.font = font
×
UNCOV
1332
        self.color = color
×
1333
        self.setRGB(color)
×
1334
        self.setText(text)
×
1335
        self.lineEdit.textChanged.connect(self.textChanged.emit)
×
1336

1337
    def setText(self, txt):
1✔
1338
        """Set text for QLineEdit"""
1339
        self.txt = txt
×
1340
        self.lineEdit.setText(txt)
×
1341

1342
    def setRGB(self, rgb):
1✔
1343
        """Wrap method to set color with a #rrggbb code"""
1344
        color = QtGui.QColor(rgb)
×
1345
        if color.isValid():
×
1346
            self.setColor(color)
×
1347

1348
    def setColor(self, color):
1✔
1349
        """Set color for widget"""
UNCOV
1350
        self.colorButton.setPalette(QtGui.QPalette(color))
×
UNCOV
1351
        paleta = QtGui.QPalette()
×
1352
        paleta.setColor(QtGui.QPalette.ColorRole.Text, color)
×
1353
        self.lineEdit.setPalette(paleta)
×
UNCOV
1354
        self.colorChanged.emit(color.name())
×
UNCOV
1355
        self.color = color
×
1356

1357
    def setFont(self, font):
1✔
1358
        """Set font for widget"""
1359
        self.font = font
×
UNCOV
1360
        self.lineEdit.setFont(font)
×
UNCOV
1361
        self.fontChanged.emit(font)
×
1362

1363
    def colorButtonClicked(self):
1✔
1364
        """Show QColorDialog to change the color"""
1365
        dlg = QtWidgets.QColorDialog(self.color, self)
×
1366
        if dlg.exec():
×
1367
            self.setColor(dlg.currentColor())
×
1368

1369
    def fontButtonClicked(self):
1✔
1370
        """Show QFontDialog to choose the font"""
UNCOV
1371
        dlg = QtWidgets.QFontDialog(self.lineEdit.font())
×
1372
        if dlg.exec():
×
1373
            self.setFont(dlg.currentFont())
×
1374

1375

1376
class Table_Graphics(QtWidgets.QWidget):
1✔
1377
    """Custom widget to implement as popup in PFD when mouse over stream and
1378
    equipment graphic item, to show the status of entity and the properties
1379
    desired if availables"""
1380
    def __init__(self, entity=None, idx=None, preferences=None, parent=None):
1✔
UNCOV
1381
        super().__init__(parent)
×
UNCOV
1382
        self.setWindowFlags(QtCore.Qt.WindowType.Popup)
×
UNCOV
1383
        QtWidgets.QVBoxLayout(self)
×
1384

1385
        if entity and idx and preferences:
×
1386
            self.populate(entity, idx, preferences)
×
1387

1388
    def clear(self):
1✔
1389
        """Clear content of widget"""
UNCOV
1390
        for i in reversed(range(self.layout().count())):
×
UNCOV
1391
            self.layout().itemAt(i).widget().setParent(None)
×
1392

1393
        # Reduce size of proxy widget to force fit content to new entity
1394
        self.graphicsProxyWidget().resize(0, 0)
×
1395

1396
    def populate(self, entity, idx, preferences):
1✔
1397
        """Populate the widget with the new values"""
1398

1399
        self.clear()
×
1400

UNCOV
1401
        if isinstance(entity, Corriente):
×
UNCOV
1402
            title = f"Stream {idx}"
×
1403
        else:
1404
            title = f"Equipment {idx}"
×
UNCOV
1405
        label = QtWidgets.QLabel(title)
×
UNCOV
1406
        label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
×
1407
        self.layout().addWidget(label)
×
UNCOV
1408
        line = QtWidgets.QFrame()
×
UNCOV
1409
        line.setFrameShape(QtWidgets.QFrame.Shape.HLine)
×
UNCOV
1410
        line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)
×
UNCOV
1411
        self.layout().addWidget(line)
×
1412
        if entity:
×
UNCOV
1413
            if entity.status:
×
1414
                textos = entity.popup(preferences)
×
1415
                for txt, tooltip, j in textos:
×
UNCOV
1416
                    label = QtWidgets.QLabel(txt)
×
1417
                    label.setToolTip(tooltip)
×
1418
                    if j:
×
1419
                        label.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight)
×
1420
                    self.layout().addWidget(label)
×
1421
            else:
1422
                self.layout().addWidget(QtWidgets.QLabel(entity.msg))
×
1423
        else:
1424
            self.layout().addWidget(QtWidgets.QLabel(self.tr("Undefined")))
×
1425

1426

1427
def createAction(text, **kw):
1✔
1428
    """Create a QAction and a QToolButton if its requested
1429

1430
    Parameters
1431
    ----------
1432
    text : str
1433
        Text to define action
1434
    button : boolean
1435
        Return too a QToolButton with same properties
1436
    icon : str
1437
        Local path of icon in pychemqt images folder
1438
    slot : obj
1439
        Function to use as slot when action is triggered
1440
    shortcut : QtGui.QKeySequence
1441
        Shortcut keyboard to associate with action
1442
    tip : str
1443
        Text to use as tooltip
1444
    checkable : boolean
1445
        Set the created action checkable
1446
    parent : QtCore.QObject
1447
        Define object parent of created action and button
1448
    """
UNCOV
1449
    slot = kw.get("slot", None)
×
UNCOV
1450
    shortcut = kw.get("shortcut", None)
×
UNCOV
1451
    icon = kw.get("icon", None)
×
UNCOV
1452
    checkable = kw.get("checkable", False)
×
UNCOV
1453
    button = kw.get("button", False)
×
UNCOV
1454
    parent = kw.get("parent", None)
×
1455

1456
    # Use text as default tip if no avialable
UNCOV
1457
    tip = kw.get("tip", text)
×
1458

1459
    # Define action and its properties as requested
UNCOV
1460
    action = QtGui.QAction(text, parent)
×
UNCOV
1461
    if icon:
×
1462
        action.setIcon(QtGui.QIcon(IMAGE_PATH + icon))
×
1463
    if shortcut:
×
1464
        action.setShortcut(shortcut)
×
1465
    action.setToolTip(tip)
×
1466
    action.setStatusTip(tip)
×
1467
    if slot:
×
UNCOV
1468
        action.triggered.connect(slot)
×
UNCOV
1469
    if checkable:
×
1470
        action.setCheckable(True)
×
1471

1472
    # Create too the button if its requested
1473
    if button:
×
1474
        boton = DragButton(parent)
×
1475

1476
        boton.setIcon(QtGui.QIcon(IMAGE_PATH + icon))
×
1477
        boton.setToolTip(tip)
×
1478
        boton.setStatusTip(tip)
×
1479
        if slot:
×
1480
            boton.clicked.connect(slot)
×
1481
        boton.setCheckable(checkable)
×
1482
        boton.setIconSize(QtCore.QSize(36, 36))
×
1483
        boton.setFixedSize(QtCore.QSize(36, 36))
×
UNCOV
1484
        return action, boton
×
1485

1486
    return action
×
1487

1488

1489
def okToContinue(parent, dirty, func, *parameters):
1✔
1490
    """Function to ask user if any unsaved change
1491

1492
    Parameters
1493
    ----------
1494
    parent : QtWidgets.QWidget
1495
        Parent widget to dialog
1496
    dirty: boolean
1497
        Indicates if there are unsaved changes
1498
    func: object
1499
        function to run if user want to save changes
1500
    parameters: dict
1501
        parameter of func
1502
    """
UNCOV
1503
    if not dirty:
×
UNCOV
1504
        return True
×
1505

UNCOV
1506
    dialog = QtWidgets.QMessageBox.question(
×
1507
        parent,
1508
        translate("widgets", "Unsaved changes"),
1509
        translate("widgets", "Save unsaved changes?"),
1510
        QtWidgets.QMessageBox.StandardButton.Yes
1511
        | QtWidgets.QMessageBox.StandardButton.No
1512
        | QtWidgets.QMessageBox.StandardButton.Cancel,
1513
        QtWidgets.QMessageBox.StandardButton.Yes)
1514

UNCOV
1515
    if dialog == QtWidgets.QMessageBox.StandardButton.Cancel:
×
1516
        return False
×
1517

UNCOV
1518
    if dialog == QtWidgets.QMessageBox.StandardButton.No:
×
1519
        return True
×
1520

UNCOV
1521
    if dialog == QtWidgets.QMessageBox.StandardButton.Yes:
×
UNCOV
1522
        func(*parameters)
×
UNCOV
1523
        return True
×
1524

UNCOV
1525
    return None
×
1526

1527

1528
def mathTex2QPixmap(mathTex, fs):
1✔
1529
    """Convert a latex text to a QPixmap to display in any qt widget. Code
1530
    snippet from https://stackoverflow.com/questions/32035251
1531

1532
    Parameters
1533
    ----------
1534
    mathTex : str
1535
        Latex text of expression to show
1536
    fs : int
1537
        Font size of text to show
1538

1539
    Returns
1540
    -------
1541
    qpixmap : :class:`QtGui.QPixmap`
1542
        QPixmap ready to show in any other qt widget
1543
    """
1544

1545
    # set up a mpl figure instance
UNCOV
1546
    fig = Figure()
×
UNCOV
1547
    fig.patch.set_facecolor('none')
×
UNCOV
1548
    fig.set_canvas(FigureCanvasAgg(fig))
×
UNCOV
1549
    renderer = fig.canvas.get_renderer()
×
1550

1551
    # plot the mathTex expression
UNCOV
1552
    ax = fig.add_axes([0, 0, 1, 1])
×
UNCOV
1553
    ax.axis('off')
×
UNCOV
1554
    ax.patch.set_facecolor('none')
×
UNCOV
1555
    t = ax.text(0, 0, mathTex, ha='left', va='bottom', fontsize=fs)
×
1556

1557
    # fit figure size to text artist
UNCOV
1558
    fwidth, fheight = fig.get_size_inches()
×
1559
    fig_bbox = fig.get_window_extent(renderer)
×
1560

1561
    text_bbox = t.get_window_extent(renderer)
×
1562

UNCOV
1563
    tight_fwidth = text_bbox.width * fwidth / fig_bbox.width
×
UNCOV
1564
    tight_fheight = text_bbox.height * fheight / fig_bbox.height
×
1565

1566
    fig.set_size_inches(tight_fwidth, tight_fheight)
×
1567

1568
    # convert mpl figure to QPixmap
UNCOV
1569
    buf, size = fig.canvas.print_to_buffer()
×
UNCOV
1570
    qimage = QtGui.QImage(
×
1571
        buf, size[0], size[1], QtGui.QImage.Format.Format_ARGB32)
1572
    qpixmap = QtGui.QPixmap(qimage)
×
1573

1574
    return qpixmap
×
1575

1576

1577
class QLabelMath(QtWidgets.QLabel):
1✔
1578
    """Customized QLabel to show a pixmap with a mathematical formulae"""
1579
    def __init__(self, tex, *args, fs=12, **kw):
1✔
1580
        """
1581
        Parameters
1582
        ----------
1583
        tex : str
1584
            Latex code text of mathematical expresion to show
1585
        fs : int
1586
            Font size used in image
1587
        """
UNCOV
1588
        super().__init__(*args, **kw)
×
UNCOV
1589
        self.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
×
UNCOV
1590
        self.setFrameShape(QtWidgets.QFrame.Shape.NoFrame)
×
UNCOV
1591
        self.setFrameStyle(QtWidgets.QFrame.Shadow.Plain)
×
UNCOV
1592
        self.fs = fs
×
UNCOV
1593
        if tex:
×
UNCOV
1594
            pixmap = mathTex2QPixmap(tex, fs)
×
UNCOV
1595
            self.setPixmap(pixmap)
×
1596

1597
    def setTex(self, tex):
1✔
1598
        """Set the image to show from a latex text source"""
UNCOV
1599
        pixmap = mathTex2QPixmap(tex, self.fs)
×
UNCOV
1600
        self.setPixmap(pixmap)
×
1601

1602

1603
def demo():
1✔
1604
    """Show a demo widget with implemented custom widget"""
1605

1606
    from lib import unidades
×
1607

1608
    app = QtWidgets.QApplication(sys.argv)
×
1609

UNCOV
1610
    ui = QtWidgets.QDialog()
×
UNCOV
1611
    layout = QtWidgets.QVBoxLayout(ui)
×
1612

1613
    w = Entrada_con_unidades(unidades.Pressure)
×
UNCOV
1614
    layout.addWidget(w)
×
UNCOV
1615
    w2 = ColorSelector(isAlpha=True)
×
UNCOV
1616
    layout.addWidget(w2)
×
UNCOV
1617
    w3 = PathConfig()
×
UNCOV
1618
    layout.addWidget(w3)
×
1619
    w4 = LineConfig("saturation", "Line Style")
×
UNCOV
1620
    layout.addWidget(w4)
×
1621
    w5 = GridConfig("grid", "Grid Line Style")
×
UNCOV
1622
    layout.addWidget(w5)
×
1623
    w6 = LineStyleCombo()
×
1624
    layout.addWidget(w6)
×
UNCOV
1625
    w7 = InputFont(text="foo bar", color="#0000ff")
×
1626
    layout.addWidget(w7)
×
1627
    w8 = Tabla(columnas=1, filas=1, verticalHeaderModel="C", dinamica=True)
×
1628
    layout.addWidget(w8)
×
1629
    w9 = QLabelMath(r"$\frac{\pi r^2}{2\epsilon \mu}$", fs=15)
×
1630
    layout.addWidget(w9)
×
1631
    w10 = Status()
×
1632
    layout.addWidget(w10)
×
1633

1634
    ui.show()
×
1635
    sys.exit(app.exec())
×
1636

1637

1638
if __name__ == "__main__":
1✔
1639
    demo()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc