curve_plot_property.py 32.5 KB
Newer Older
1 2 3 4 5 6 7
# -*- coding: utf-8 -*-
#
# This file is part of the bliss project
#
# Copyright (c) 2015-2019 Beamline Control Unit, ESRF
# Distributed under the GNU LGPLv3. See LICENSE for more info.

Valentin Valls's avatar
Valentin Valls committed
8 9
from __future__ import annotations
from typing import Union
10
from typing import List
Valentin Valls's avatar
Valentin Valls committed
11
from typing import Dict
12
from typing import Optional
Valentin Valls's avatar
Valentin Valls committed
13

Valentin Valls's avatar
Valentin Valls committed
14 15
import logging

16
from silx.gui import qt
Valentin Valls's avatar
Valentin Valls committed
17
from silx.gui import icons
18 19 20

from bliss.flint.model import flint_model
from bliss.flint.model import plot_model
21
from bliss.flint.model import plot_item_model
22
from bliss.flint.model import plot_state_model
Valentin Valls's avatar
Valentin Valls committed
23
from bliss.flint.model import scan_model
24
from bliss.flint.helper import model_helper
25
from . import delegates
26
from . import _property_tree_helper
27 28


Valentin Valls's avatar
Valentin Valls committed
29 30
_logger = logging.getLogger(__name__)

31

Valentin Valls's avatar
Valentin Valls committed
32
class YAxesEditor(qt.QWidget):
Valentin Valls's avatar
Valentin Valls committed
33 34 35

    valueChanged = qt.Signal()

Valentin Valls's avatar
Valentin Valls committed
36 37
    def __init__(self, parent=None):
        qt.QWidget.__init__(self, parent=parent)
Valentin Valls's avatar
Valentin Valls committed
38
        self.setContentsMargins(1, 1, 1, 1)
Valentin Valls's avatar
Valentin Valls committed
39 40
        self.__plotItem = None
        layout = qt.QHBoxLayout(self)
Valentin Valls's avatar
Valentin Valls committed
41
        layout.setContentsMargins(0, 0, 0, 0)
Valentin Valls's avatar
Valentin Valls committed
42

Valentin Valls's avatar
Valentin Valls committed
43 44 45
        self.__group = qt.QButtonGroup(self)

        y1Check = qt.QRadioButton(self)
Valentin Valls's avatar
Valentin Valls committed
46
        y1Check.setObjectName("y1")
Valentin Valls's avatar
Valentin Valls committed
47
        y2Check = qt.QRadioButton(self)
Valentin Valls's avatar
Valentin Valls committed
48 49
        y2Check.setObjectName("y2")

Valentin Valls's avatar
Valentin Valls committed
50 51 52 53 54
        self.__group.addButton(y1Check)
        self.__group.addButton(y2Check)
        self.__group.setExclusive(True)
        self.__group.buttonClicked[qt.QAbstractButton].connect(self.__checkedChanged)

Valentin Valls's avatar
Valentin Valls committed
55 56 57
        layout.addWidget(y1Check)
        layout.addWidget(y2Check)

Valentin Valls's avatar
Valentin Valls committed
58 59 60 61 62 63
    def __getY1Axis(self):
        return self.findChildren(qt.QRadioButton, "y1")[0]

    def __getY2Axis(self):
        return self.findChildren(qt.QRadioButton, "y2")[0]

Valentin Valls's avatar
Valentin Valls committed
64 65 66 67 68 69 70
    def yAxis(self) -> str:
        if self.__getY1Axis().isChecked():
            return "left"
        elif self.__getY2Axis().isChecked():
            return "right"
        return ""

Valentin Valls's avatar
Valentin Valls committed
71 72 73 74 75 76 77 78
    def setPlotItem(self, plotItem):
        if self.__plotItem is not None:
            self.__plotItem.valueChanged.disconnect(self.__plotItemChanged)
        self.__plotItem = plotItem
        if self.__plotItem is not None:
            self.__plotItem.valueChanged.connect(self.__plotItemChanged)
            self.__plotItemYAxisChanged()

Valentin Valls's avatar
Valentin Valls committed
79
        isReadOnly = self.__isReadOnly()
Valentin Valls's avatar
Valentin Valls committed
80

Valentin Valls's avatar
Valentin Valls committed
81 82 83 84 85 86 87
        w = self.__getY1Axis()
        w.setEnabled(not isReadOnly)

        w = self.__getY2Axis()
        w.setEnabled(not isReadOnly)

        self.__updateToolTips()
Valentin Valls's avatar
Valentin Valls committed
88

Valentin Valls's avatar
Valentin Valls committed
89
    def __isReadOnly(self):
Valentin Valls's avatar
Valentin Valls committed
90 91
        if self.__plotItem is None:
            return False
92
        return not isinstance(self.__plotItem, plot_item_model.CurveMixIn)
Valentin Valls's avatar
Valentin Valls committed
93

Valentin Valls's avatar
Valentin Valls committed
94
    def __updateToolTips(self):
Valentin Valls's avatar
Valentin Valls committed
95
        isReadOnly = self.__isReadOnly()
Valentin Valls's avatar
Valentin Valls committed
96 97

        w = self.__getY1Axis()
Valentin Valls's avatar
Valentin Valls committed
98
        if w.isChecked():
Valentin Valls's avatar
Valentin Valls committed
99 100 101 102
            w.setToolTip("Displayed within the Y1 axis")
        elif isReadOnly:
            w.setToolTip("")
        else:
Valentin Valls's avatar
Valentin Valls committed
103
            w.setToolTip("Display it within the Y1 axis")
Valentin Valls's avatar
Valentin Valls committed
104 105

        w = self.__getY2Axis()
Valentin Valls's avatar
Valentin Valls committed
106
        if w.isChecked():
Valentin Valls's avatar
Valentin Valls committed
107 108 109 110
            w.setToolTip("Displayed within the Y2 axis")
        elif isReadOnly:
            w.setToolTip("")
        else:
Valentin Valls's avatar
Valentin Valls committed
111
            w.setToolTip("Display it within the Y2 axis")
Valentin Valls's avatar
Valentin Valls committed
112 113 114 115 116 117 118 119 120 121

    def __checkedChanged(self, button: qt.QRadioButton):
        yAxis1 = self.__getY1Axis()
        yAxis2 = self.__getY2Axis()
        if button is yAxis1:
            axis = "left"
        elif button is yAxis2:
            axis = "right"
        else:
            assert False
Valentin Valls's avatar
Valentin Valls committed
122 123
        if self.__plotItem is not None:
            self.__plotItem.setYAxis(axis)
Valentin Valls's avatar
Valentin Valls committed
124
        self.valueChanged.emit()
Valentin Valls's avatar
Valentin Valls committed
125 126 127 128 129 130 131 132

    def __plotItemChanged(self, eventType):
        if eventType == plot_model.ChangeEventType.YAXIS:
            self.__plotItemYAxisChanged()

    def __plotItemYAxisChanged(self):
        try:
            axis = self.__plotItem.yAxis()
Valentin Valls's avatar
Valentin Valls committed
133 134 135 136
        except Exception:
            _logger.error(
                "Error while reaching y-axis from %s", self.__plotItem, exc_info=True
            )
Valentin Valls's avatar
Valentin Valls committed
137 138
            axis = None

Valentin Valls's avatar
Valentin Valls committed
139
        y1Axis = self.__getY1Axis()
Valentin Valls's avatar
Valentin Valls committed
140 141 142 143
        old = y1Axis.blockSignals(True)
        y1Axis.setChecked(axis == "left")
        y1Axis.blockSignals(old)

Valentin Valls's avatar
Valentin Valls committed
144
        y2Axis = self.__getY2Axis()
Valentin Valls's avatar
Valentin Valls committed
145 146 147 148
        old = y2Axis.blockSignals(True)
        y2Axis.setChecked(axis == "right")
        y2Axis.blockSignals(old)

Valentin Valls's avatar
Valentin Valls committed
149 150
        self.__updateToolTips()

Valentin Valls's avatar
Valentin Valls committed
151

Valentin Valls's avatar
Valentin Valls committed
152
class YAxesPropertyItemDelegate(qt.QStyledItemDelegate):
Valentin Valls's avatar
Valentin Valls committed
153 154 155

    YAxesRole = qt.Qt.UserRole + 2

156 157 158 159 160
    def __init__(self, parent):
        qt.QStyledItemDelegate.__init__(self, parent=parent)

    def createEditor(self, parent, option, index):
        if not index.isValid():
Valentin Valls's avatar
Valentin Valls committed
161
            return super(YAxesPropertyItemDelegate, self).createEditor(
162 163 164
                parent, option, index
            )

Valentin Valls's avatar
Valentin Valls committed
165
        editor = YAxesEditor(parent=parent)
166 167
        plotItem = self.getPlotItem(index)
        editor.setPlotItem(plotItem)
Valentin Valls's avatar
Valentin Valls committed
168 169
        if plotItem is None:
            editor.valueChanged.connect(self.__editorsChanged)
170 171 172 173 174 175

        editor.setMinimumSize(editor.sizeHint())
        editor.setMaximumSize(editor.sizeHint())
        editor.setSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Fixed)
        return editor

Valentin Valls's avatar
Valentin Valls committed
176 177 178 179
    def __editorsChanged(self):
        editor = self.sender()
        self.commitData.emit(editor)

Valentin Valls's avatar
Valentin Valls committed
180
    def getPlotItem(self, index) -> Union[None, plot_model.Item]:
181
        plotItem = index.data(delegates.PlotItemRole)
182 183 184 185 186 187 188 189 190
        if not isinstance(plotItem, plot_model.Item):
            return None
        return plotItem

    def setEditorData(self, editor, index):
        plotItem = self.getPlotItem(index)
        editor.setPlotItem(plotItem)

    def setModelData(self, editor, model, index):
Valentin Valls's avatar
Valentin Valls committed
191 192 193 194 195 196 197 198
        plotItem = self.getPlotItem(index)
        if plotItem is None:
            yAxis = editor.yAxis()
            model.setData(index, yAxis, role=self.YAxesRole)
        else:
            # Already up-to-date
            # From signals from plot items
            pass
199

Valentin Valls's avatar
Valentin Valls committed
200 201 202 203 204 205 206 207
    def updateEditorGeometry(self, editor, option, index):
        # Center the widget to the cell
        size = editor.sizeHint()
        half = size / 2
        halfPoint = qt.QPoint(half.width(), half.height() - 1)
        pos = option.rect.center() - halfPoint
        editor.move(pos)

Valentin Valls's avatar
Valentin Valls committed
208

209
class _AddItemAction(qt.QWidgetAction):
210 211
    def __init__(self, parent: qt.QObject):
        assert isinstance(parent, CurvePlotPropertyWidget)
212
        super(_AddItemAction, self).__init__(parent)
213
        parent.plotItemSelected.connect(self.__selectionChanged)
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232

        widget = qt.QToolButton(parent)
        icon = icons.getQIcon("flint:icons/add-item")
        widget.setIcon(icon)
        widget.setAutoRaise(True)
        widget.setToolTip("CReate new items in the plot")
        widget.setPopupMode(qt.QToolButton.InstantPopup)
        widget.setEnabled(False)
        widget.setText("Create items")
        self.setDefaultWidget(widget)

        menu = qt.QMenu(parent)
        menu.aboutToShow.connect(self.__aboutToShow)
        widget.setMenu(menu)

    def __aboutToShow(self):
        menu: qt.QMenu = self.sender()
        menu.clear()

233
        item = self.parent().selectedPlotItem()
234 235 236 237 238 239 240 241 242 243
        if isinstance(item, plot_item_model.CurveMixIn):
            menu.addSection("Statistics")

            action = qt.QAction(self)
            action.setText("Max marker")
            icon = icons.getQIcon("flint:icons/item-stats")
            action.setIcon(icon)
            action.triggered.connect(self.__createMax)
            menu.addAction(action)

Valentin Valls's avatar
Valentin Valls committed
244 245 246 247 248 249 250
            action = qt.QAction(self)
            action.setText("Min marker")
            icon = icons.getQIcon("flint:icons/item-stats")
            action.setIcon(icon)
            action.triggered.connect(self.__createMin)
            menu.addAction(action)

251 252 253 254 255 256 257 258
            menu.addSection("Functions")

            action = qt.QAction(self)
            action.setText("Derivative function")
            icon = icons.getQIcon("flint:icons/item-stats")
            action.setIcon(icon)
            action.triggered.connect(self.__createDerivative)
            menu.addAction(action)
Valentin Valls's avatar
Valentin Valls committed
259 260 261 262 263 264 265

            action = qt.QAction(self)
            action.setText("Gaussian fit")
            icon = icons.getQIcon("flint:icons/item-stats")
            action.setIcon(icon)
            action.triggered.connect(self.__createGaussianFit)
            menu.addAction(action)
266 267 268 269 270 271
        else:
            action = qt.QAction(self)
            action.setText("No available items")
            action.setEnabled(False)
            menu.addAction(action)

272 273
    def __selectionChanged(self, current: plot_model.Item):
        self.defaultWidget().setEnabled(current is not None)
274 275

    def __createMax(self):
276
        parentItem = self.parent().selectedPlotItem()
277 278 279 280 281 282 283
        if parentItem is not None:
            plot = parentItem.plot()
            newItem = plot_state_model.MaxCurveItem(plot)
            newItem.setSource(parentItem)
            with plot.transaction():
                plot.addItem(newItem)

Valentin Valls's avatar
Valentin Valls committed
284
    def __createMin(self):
285
        parentItem = self.parent().selectedPlotItem()
Valentin Valls's avatar
Valentin Valls committed
286 287 288 289 290 291 292
        if parentItem is not None:
            plot = parentItem.plot()
            newItem = plot_state_model.MinCurveItem(plot)
            newItem.setSource(parentItem)
            with plot.transaction():
                plot.addItem(newItem)

293
    def __createDerivative(self):
294
        parentItem = self.parent().selectedPlotItem()
295 296 297 298 299
        if parentItem is not None:
            plot = parentItem.plot()
            newItem = plot_state_model.DerivativeItem(plot)
            newItem.setSource(parentItem)
            with plot.transaction():
Valentin Valls's avatar
Valentin Valls committed
300 301 302
                plot.addItem(newItem)

    def __createGaussianFit(self):
303
        parentItem = self.parent().selectedPlotItem()
Valentin Valls's avatar
Valentin Valls committed
304 305 306 307 308
        if parentItem is not None:
            plot = parentItem.plot()
            newItem = plot_state_model.GaussianFitItem(plot)
            newItem.setSource(parentItem)
            with plot.transaction():
309 310 311
                plot.addItem(newItem)


Valentin Valls's avatar
Valentin Valls committed
312
class _DataItem(_property_tree_helper.ScanRowItem):
313 314 315
    def __init__(self):
        super(_DataItem, self).__init__()
        qt.QStandardItem.__init__(self)
316 317 318
        self.__xaxis = delegates.HookedStandardItem("")
        self.__yaxes = delegates.HookedStandardItem("")
        self.__displayed = delegates.HookedStandardItem("")
319
        self.__style = qt.QStandardItem("")
Valentin Valls's avatar
Valentin Valls committed
320
        self.__remove = qt.QStandardItem("")
321
        self.__error = qt.QStandardItem("")
322
        self.__xAxisSelected = False
Valentin Valls's avatar
Valentin Valls committed
323

Valentin Valls's avatar
Valentin Valls committed
324 325 326
        self.__plotModel: Optional[plot_model.Plot] = None
        self.__plotItem: Optional[plot_model.Item] = None
        self.__channel: Optional[scan_model.Channel] = None
327 328 329
        self.__treeView: Optional[qt.QTreeView] = None
        self.__flintModel: Optional[flint_model.FlintState] = None

330 331 332 333 334 335 336 337 338
        self.setOtherRowItems(
            self.__xaxis,
            self.__yaxes,
            self.__displayed,
            self.__style,
            self.__remove,
            self.__error,
        )

Valentin Valls's avatar
Valentin Valls committed
339 340 341
    def __hash__(self):
        return hash(id(self))

342 343 344
    def channel(self) -> Optional[scan_model.Channel]:
        return self.__channel

345 346 347 348 349
    def setEnvironment(
        self, treeView: qt.QTreeView, flintState: flint_model.FlintState
    ):
        self.__treeView = treeView
        self.__flintModel = flintState
350

Valentin Valls's avatar
Valentin Valls committed
351 352
    def setPlotModel(self, plotModel: plot_model.Plot):
        self.__plotModel = plotModel
353

354 355 356
    def plotModel(self) -> Optional[plot_model.Plot]:
        return self.__plotModel

357
    def axesItem(self) -> qt.QStandardItem:
Valentin Valls's avatar
Valentin Valls committed
358
        return self.__yaxes
359 360 361 362

    def styleItem(self) -> qt.QStandardItem:
        return self.__style

363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380
    def updateError(self):
        scan = self.__flintModel.currentScan()
        if scan is None or self.__plotItem is None:
            # No message to reach
            self.__error.setText(None)
            self.__error.setIcon(qt.QIcon())
            return
        result = self.__plotItem.getErrorMessage(scan)
        if result is None:
            # Ths item is valid
            self.__error.setText(None)
            self.__error.setIcon(qt.QIcon())
            return

        self.__error.setText(result)
        icon = icons.getQIcon("flint:icons/warning")
        self.__error.setIcon(icon)

Valentin Valls's avatar
Typo  
Valentin Valls committed
381
    def __yAxisChanged(self, item: qt.QStandardItem):
Valentin Valls's avatar
Valentin Valls committed
382 383 384 385 386 387 388 389 390 391
        if self.__plotItem is not None:
            # There is a plot item already
            return
        else:
            assert self.__channel is not None
            assert self.__plotModel is not None
            plot = self.__plotModel
            yAxis = item.data(role=YAxesPropertyItemDelegate.YAxesRole)
            assert yAxis in ["left", "right"]

392 393 394 395 396 397
            curve, wasUpdated = model_helper.createCurveItem(
                plot, self.__channel, yAxis
            )
            if wasUpdated:
                # It's now an item with a value
                self.setPlotItem(curve)
Valentin Valls's avatar
Valentin Valls committed
398

399 400
    def __visibilityViewChanged(self, item: qt.QStandardItem):
        if self.__plotItem is not None:
401
            state = item.data(delegates.VisibilityRole)
402 403
            self.__plotItem.setVisible(state == qt.Qt.Checked)

Valentin Valls's avatar
Valentin Valls committed
404
    def setSelectedXAxis(self):
405 406 407 408
        if self.__xAxisSelected:
            return
        self.__xAxisSelected = True

Valentin Valls's avatar
Valentin Valls committed
409 410 411
        old = self.__xaxis.modelUpdated
        self.__xaxis.modelUpdated = None
        try:
412
            self.__xaxis.setData(qt.Qt.Checked, role=delegates.RadioRole)
Valentin Valls's avatar
Valentin Valls committed
413 414
        finally:
            self.__xaxis.modelUpdated = old
415 416 417
        # It have to be closed to be refreshed. Sounds like a bug.
        self.__treeView.closePersistentEditor(self.__xaxis.index())
        self.__treeView.openPersistentEditor(self.__xaxis.index())
Valentin Valls's avatar
Valentin Valls committed
418 419 420 421 422 423 424 425 426 427

    def __xAxisChanged(self, item: qt.QStandardItem):
        assert self.__channel is not None
        assert self.__plotModel is not None

        # Reach the top master
        topMaster = self.__channel.device().topMaster()
        scan = topMaster.scan()

        # Reach all plot items from this top master
428 429 430
        curves = model_helper.reachAllCurveItemFromDevice(
            self.__plotModel, scan, topMaster
        )
Valentin Valls's avatar
Valentin Valls committed
431

432 433 434 435
        if len(curves) == 0:
            # Create an item to store the x-value
            plot = self.__plotModel
            channelName = self.__channel.name()
436
            newItem = plot_item_model.CurveItem(plot)
437 438 439 440 441 442 443 444 445
            newItem.setXChannel(plot_model.ChannelRef(plot, channelName))
            plot.addItem(newItem)
        else:
            # Update the x-channel of all this curves
            with self.__plotModel.transaction():
                xChannelName = self.__channel.name()
                for curve in curves:
                    xChannel = plot_model.ChannelRef(curve, xChannelName)
                    curve.setXChannel(xChannel)
Valentin Valls's avatar
Valentin Valls committed
446

Valentin Valls's avatar
Valentin Valls committed
447
    def setDevice(self, device: scan_model.Device):
Valentin Valls's avatar
Valentin Valls committed
448
        self.setDeviceLookAndFeel(device)
449 450 451 452 453 454 455 456 457 458 459 460 461
        self.__updateXAxisStyle(True, None)

    def __rootRow(self) -> int:
        item = self
        while item is not None:
            parent = item.parent()
            if parent is None:
                break
            item = parent
        return item.row()

    def __updateXAxisStyle(self, setAxisValue: bool, radioValue=None):
        # FIXME: avoid hard coded style
Valentin Valls's avatar
Valentin Valls committed
462
        cellColors = [qt.QColor(0xE8, 0xE8, 0xE8), qt.QColor(0xF5, 0xF5, 0xF5)]
463 464 465 466 467 468 469
        old = self.__xaxis.modelUpdated
        self.__xaxis.modelUpdated = None
        if setAxisValue:
            self.__xaxis.setData(radioValue, role=delegates.RadioRole)
        i = self.__rootRow()
        self.__xaxis.setBackground(cellColors[i % 2])
        self.__xaxis.modelUpdated = old
Valentin Valls's avatar
Valentin Valls committed
470

Valentin Valls's avatar
Typo  
Valentin Valls committed
471
    def setChannel(self, channel: scan_model.Channel):
472
        assert self.__treeView is not None
Valentin Valls's avatar
Valentin Valls committed
473
        self.__channel = channel
Valentin Valls's avatar
Valentin Valls committed
474
        self.setChannelLookAndFeel(channel)
475
        self.__updateXAxisStyle(True, qt.Qt.Unchecked)
Valentin Valls's avatar
Valentin Valls committed
476
        self.__xaxis.modelUpdated = self.__xAxisChanged
Valentin Valls's avatar
Typo  
Valentin Valls committed
477
        self.__yaxes.modelUpdated = self.__yAxisChanged
Valentin Valls's avatar
Valentin Valls committed
478

479
        self.__treeView.openPersistentEditor(self.__xaxis.index())
Valentin Valls's avatar
Typo  
Valentin Valls committed
480
        self.__treeView.openPersistentEditor(self.__yaxes.index())
Valentin Valls's avatar
Valentin Valls committed
481

Cyril Guilloud's avatar
Cyril Guilloud committed
482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507
    def data(self, role=qt.Qt.DisplayRole):
        if role == qt.Qt.ToolTipRole:
            return self.toolTip()
        return _property_tree_helper.ScanRowItem.data(self, role)

    def toolTip(self):
        if self.__channel is not None:
            data = self.__channel.data()
            if data is not None:
                array = data.array()
            else:
                array = None
            if array is None:
                shape = "No data"
            elif array is tuple():
                shape = "Scalar"
            else:
                shape = " × ".join([str(s) for s in array.shape])
            name = self.__channel.name()
            return f"""<html><ul>
            <li><b>Channel name:</b> {name}</li>
            <li><b>Data shape:</b> {shape}</li>
            </ul></html>"""

        return None

508 509 510
    def plotItem(self) -> Optional[plot_model.Item]:
        return self.__plotItem

511
    def setPlotItem(self, plotItem):
512
        self.__plotItem = plotItem
Valentin Valls's avatar
Valentin Valls committed
513

514 515 516
        self.__yaxes.setData(plotItem, role=delegates.PlotItemRole)
        self.__style.setData(plotItem, role=delegates.PlotItemRole)
        self.__remove.setData(plotItem, role=delegates.PlotItemRole)
Valentin Valls's avatar
Valentin Valls committed
517

Valentin Valls's avatar
Typo  
Valentin Valls committed
518
        self.__yaxes.modelUpdated = self.__yAxisChanged
519

Valentin Valls's avatar
Valentin Valls committed
520 521 522
        if plotItem is not None:
            isVisible = plotItem.isVisible()
            state = qt.Qt.Checked if isVisible else qt.Qt.Unchecked
523
            self.__displayed.setData(state, role=delegates.VisibilityRole)
Valentin Valls's avatar
Valentin Valls committed
524 525
            self.__displayed.modelUpdated = self.__visibilityViewChanged
        else:
526
            self.__displayed.setData(None, role=delegates.VisibilityRole)
Valentin Valls's avatar
Valentin Valls committed
527
            self.__displayed.modelUpdated = None
528

529 530 531
        if self.__channel is None:
            self.setPlotItemLookAndFeel(plotItem)

532
        if isinstance(plotItem, plot_item_model.CurveItem):
533 534
            self.__xaxis.modelUpdated = self.__xAxisChanged
            useXAxis = True
535
        elif isinstance(plotItem, plot_item_model.CurveMixIn):
536 537 538
            # self.__updateXAxisStyle(False, None)
            useXAxis = False
            self.__updateXAxisStyle(False)
Valentin Valls's avatar
Valentin Valls committed
539
        elif isinstance(plotItem, plot_state_model.CurveStatisticItem):
540 541
            useXAxis = False
            self.__updateXAxisStyle(False)
Valentin Valls's avatar
Valentin Valls committed
542

543
        # FIXME: It have to be converted into delegate
544 545
        if useXAxis:
            self.__treeView.openPersistentEditor(self.__xaxis.index())
Valentin Valls's avatar
Valentin Valls committed
546 547
        # FIXME: close/open is needed, sometime the item is not updated
        self.__treeView.closePersistentEditor(self.__yaxes.index())
548 549 550
        self.__treeView.openPersistentEditor(self.__yaxes.index())
        self.__treeView.openPersistentEditor(self.__displayed.index())
        self.__treeView.openPersistentEditor(self.__remove.index())
551
        widget = delegates.StylePropertyWidget(self.__treeView)
552
        widget.setPlotItem(self.__plotItem)
553 554
        widget.setFlintModel(self.__flintModel)
        self.__treeView.setIndexWidget(self.__style.index(), widget)
555

556 557
        self.updateError()

558

559
class CurvePlotPropertyWidget(qt.QWidget):
Valentin Valls's avatar
Valentin Valls committed
560 561 562 563 564 565 566 567

    NameColumn = 0
    XAxisColumn = 1
    YAxesColumn = 2
    VisibleColumn = 3
    StyleColumn = 4
    RemoveColumn = 5

568 569
    plotItemSelected = qt.Signal(object)

570 571
    def __init__(self, parent=None):
        super(CurvePlotPropertyWidget, self).__init__(parent=parent)
572
        self.__scan: Optional[scan_model.Scan] = None
Valentin Valls's avatar
Valentin Valls committed
573 574
        self.__flintModel: Union[None, flint_model.FlintState] = None
        self.__plotModel: Union[None, plot_model.Plot] = None
575 576 577 578
        self.__tree = qt.QTreeView(self)
        self.__tree.setEditTriggers(qt.QAbstractItemView.NoEditTriggers)
        self.__tree.setUniformRowHeights(True)

Valentin Valls's avatar
Valentin Valls committed
579
        self.__xAxisInvalidated: bool = False
580
        self.__xAxisDelegate = delegates.RadioPropertyItemDelegate(self)
Valentin Valls's avatar
Valentin Valls committed
581
        self.__yAxesDelegate = YAxesPropertyItemDelegate(self)
582 583
        self.__visibilityDelegate = delegates.VisibilityPropertyItemDelegate(self)
        self.__removeDelegate = delegates.RemovePropertyItemDelegate(self)
584 585 586

        model = qt.QStandardItemModel(self)
        self.__tree.setModel(model)
587 588 589
        selectionModel = self.__tree.selectionModel()
        selectionModel.currentChanged.connect(self.__selectionChanged)

590 591 592
        self.__scan = None
        self.__focusWidget = None

593 594
        toolBar = self.__createToolBar()

595
        layout = qt.QVBoxLayout(self)
596 597
        layout.setSpacing(0)
        layout.addWidget(toolBar)
598 599
        layout.addWidget(self.__tree)

600 601 602
    def __createToolBar(self):
        toolBar = qt.QToolBar(self)
        toolBar.setMovable(False)
603
        action = _AddItemAction(self)
604 605 606
        toolBar.addAction(action)
        return toolBar

607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624
    def __selectionChanged(self, current: qt.QModelIndex, previous: qt.QModelIndex):
        item = self.selectedPlotItem()
        self.plotItemSelected.emit(item)

    def selectedPlotItem(self) -> Optional[plot_model.Item]:
        index = self.__tree.currentIndex()
        if not index.isValid():
            self.setEnabled(False)
            return
        model = self.__tree.model()
        index = model.index(index.row(), 0, index.parent())
        model = self.__tree.model()
        item = model.itemFromIndex(index)
        if isinstance(item, _DataItem):
            plotItem = item.plotItem()
            return plotItem
        return None

625 626 627 628 629 630 631 632 633
    def setFlintModel(self, flintModel: flint_model.FlintState = None):
        if self.__flintModel is not None:
            self.__flintModel.currentScanChanged.disconnect(self.__currentScanChanged)
            self.__setScan(None)
        self.__flintModel = flintModel
        if self.__flintModel is not None:
            self.__flintModel.currentScanChanged.connect(self.__currentScanChanged)
            self.__setScan(self.__flintModel.currentScan())

Valentin Valls's avatar
Valentin Valls committed
634 635 636
    def focusWidget(self):
        return self.__focusWidget

637 638 639 640 641 642
    def setFocusWidget(self, widget):
        if self.__focusWidget is not None:
            widget.plotModelUpdated.disconnect(self.__plotModelUpdated)
        self.__focusWidget = widget
        if self.__focusWidget is not None:
            widget.plotModelUpdated.connect(self.__plotModelUpdated)
Valentin Valls's avatar
Valentin Valls committed
643 644 645 646
            plotModel = widget.plotModel()
        else:
            plotModel = None
        self.__plotModelUpdated(plotModel)
647 648 649 650 651 652 653

    def __plotModelUpdated(self, plotModel):
        self.setPlotModel(plotModel)

    def setPlotModel(self, plotModel: plot_model.Plot):
        if self.__plotModel is not None:
            self.__plotModel.structureChanged.disconnect(self.__structureChanged)
Valentin Valls's avatar
Valentin Valls committed
654 655
            self.__plotModel.itemValueChanged.disconnect(self.__itemValueChanged)
            self.__plotModel.transactionFinished.disconnect(self.__transactionFinished)
656 657 658
        self.__plotModel = plotModel
        if self.__plotModel is not None:
            self.__plotModel.structureChanged.connect(self.__structureChanged)
Valentin Valls's avatar
Valentin Valls committed
659 660
            self.__plotModel.itemValueChanged.connect(self.__itemValueChanged)
            self.__plotModel.transactionFinished.connect(self.__transactionFinished)
661 662 663 664 665 666
        self.__updateTree()

    def __currentScanChanged(self):
        self.__setScan(self.__flintModel.currentScan())

    def __structureChanged(self):
Valentin Valls's avatar
Valentin Valls committed
667
        self.__updateTree()
668

Valentin Valls's avatar
Valentin Valls committed
669 670 671 672 673 674 675 676 677 678 679 680 681 682 683
    def __itemValueChanged(
        self, item: plot_model.Item, eventType: plot_model.ChangeEventType
    ):
        assert self.__plotModel is not None
        if eventType == plot_model.ChangeEventType.X_CHANNEL:
            if self.__plotModel.isInTransaction():
                self.__xAxisInvalidated = True
            else:
                self.__updateTree()

    def __transactionFinished(self):
        if self.__xAxisInvalidated:
            self.__xAxisInvalidated = False
            self.__updateTree()

Valentin Valls's avatar
Valentin Valls committed
684
    def plotModel(self) -> Union[None, plot_model.Plot]:
685 686
        return self.__plotModel

687 688 689 690
    def __setScan(self, scan: Optional[scan_model.Scan]):
        if self.__scan is scan:
            return
        if self.__scan is not None:
691
            self.__scan.scanDataUpdated[object].disconnect(self.__scanDataUpdated)
692
        self.__scan = scan
693
        if self.__scan is not None:
694
            self.__scan.scanDataUpdated[object].connect(self.__scanDataUpdated)
695 696
        self.__updateTree()

697
    def __scanDataUpdated(self, event: scan_model.ScanDataUpdateEvent):
698 699 700
        model = self.__tree.model()
        flags = qt.Qt.MatchWildcard | qt.Qt.MatchRecursive
        items = model.findItems("*", flags)
701
        channels = set(event.iterUpdatedChannels())
702 703 704
        # FIXME: This loop could be optimized
        for item in items:
            if isinstance(item, _DataItem):
705 706
                if item.channel() in channels:
                    item.updateError()
707

Valentin Valls's avatar
Valentin Valls committed
708 709 710 711 712 713
    def __genScanTree(
        self,
        model: qt.QStandardItemModel,
        scan: scan_model.Scan,
        channelFilter: scan_model.ChannelType,
    ) -> Dict[str, _DataItem]:
Valentin Valls's avatar
Valentin Valls committed
714 715 716 717 718
        """Feed the provided model with a tree of scan concepts (devices,
        channels).

        Returns a map from channel name to Qt items (`_DataItem`)
        """
Valentin Valls's avatar
Valentin Valls committed
719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744
        assert self.__tree is not None
        assert self.__flintModel is not None
        assert self.__plotModel is not None
        scanTree = {}
        channelItems: Dict[str, _DataItem] = {}

        devices: List[qt.QStandardItem] = []
        channelsPerDevices: Dict[qt.QStandardItem, int] = {}

        for device in scan.devices():
            item = _DataItem()
            item.setEnvironment(self.__tree, self.__flintModel)
            scanTree[device] = item

            master = device.master()
            if master is None:
                # Root device
                parent = model
            else:
                itemMaster = scanTree.get(master, None)
                if itemMaster is None:
                    parent = model
                    _logger.warning("Device list is not well ordered")
                else:
                    parent = itemMaster

745
            parent.appendRow(item.rowItems())
Valentin Valls's avatar
Valentin Valls committed
746 747 748 749 750 751 752 753 754 755 756 757 758
            # It have to be done when model index are initialized
            item.setDevice(device)
            devices.append(item)

            channels = []
            for channel in device.channels():
                if channel.type() != channelFilter:
                    continue
                channels.append(channel)

            for channel in channels:
                channelItem = _DataItem()
                channelItem.setEnvironment(self.__tree, self.__flintModel)
759
                item.appendRow(channelItem.rowItems())
Valentin Valls's avatar
Valentin Valls committed
760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787
                # It have to be done when model index are initialized
                channelItem.setChannel(channel)
                channelItem.setPlotModel(self.__plotModel)
                channelItems[channel.name()] = channelItem

            # Update channel use
            parent = item
            channelsPerDevices[parent] = 0
            while parent is not None:
                if parent in channelsPerDevices:
                    channelsPerDevices[parent] += len(channels)
                parent = parent.parent()
                if parent is None:
                    break

        # Clean up unused devices
        for device in reversed(devices):
            if device not in channelsPerDevices:
                continue
            if channelsPerDevices[device] > 0:
                continue
            parent = device.parent()
            if parent is None:
                parent = model
            parent.removeRows(device.row(), 1)

        return channelItems

788
    def __updateTree(self):
Valentin Valls's avatar
Valentin Valls committed
789
        collapsed = _property_tree_helper.getPathFromCollapsedNodes(self.__tree)
790 791 792 793 794 795 796 797
        model = self.__tree.model()
        model.clear()

        if self.__plotModel is None:
            foo = qt.QStandardItem("Empty")
            model.appendRow(foo)
            return

Valentin Valls's avatar
Valentin Valls committed
798
        model.setHorizontalHeaderLabels(
799
            ["Name", "X", "Y1/Y2", "Displayed", "Style", "Remove", "Message"]
Valentin Valls's avatar
Valentin Valls committed
800
        )
801
        self.__tree.setItemDelegateForColumn(self.XAxisColumn, self.__xAxisDelegate)
Valentin Valls's avatar
Valentin Valls committed
802
        self.__tree.setItemDelegateForColumn(self.YAxesColumn, self.__yAxesDelegate)
Valentin Valls's avatar
Valentin Valls committed
803 804 805
        self.__tree.setItemDelegateForColumn(
            self.VisibleColumn, self.__visibilityDelegate
        )
Valentin Valls's avatar
Valentin Valls committed
806
        self.__tree.setItemDelegateForColumn(self.RemoveColumn, self.__removeDelegate)
Valentin Valls's avatar
Valentin Valls committed
807
        header = self.__tree.header()
Valentin Valls's avatar
Valentin Valls committed
808 809 810 811 812 813
        header.setSectionResizeMode(self.NameColumn, qt.QHeaderView.ResizeToContents)
        header.setSectionResizeMode(self.XAxisColumn, qt.QHeaderView.ResizeToContents)
        header.setSectionResizeMode(self.YAxesColumn, qt.QHeaderView.ResizeToContents)
        header.setSectionResizeMode(self.VisibleColumn, qt.QHeaderView.ResizeToContents)
        header.setSectionResizeMode(self.StyleColumn, qt.QHeaderView.ResizeToContents)
        header.setSectionResizeMode(self.RemoveColumn, qt.QHeaderView.ResizeToContents)
814

Valentin Valls's avatar
Valentin Valls committed
815
        sourceTree: Dict[plot_model.Item, qt.QStandardItem] = {}
Valentin Valls's avatar
Valentin Valls committed
816
        scan = self.__scan
Valentin Valls's avatar
Valentin Valls committed
817 818 819 820 821 822
        if scan is not None:
            channelItems = self.__genScanTree(
                model, scan, scan_model.ChannelType.COUNTER
            )
        else:
            channelItems = {}
823

Valentin Valls's avatar
Valentin Valls committed
824
        itemWithoutLocation = qt.QStandardItem("Not linked to this scan")
825
        itemWithoutMaster = qt.QStandardItem("Not linked to a master")
826
        model.appendRow(itemWithoutLocation)
827
        model.appendRow(itemWithoutMaster)
828

829 830 831
        xChannelPerMasters = model_helper.getMostUsedXChannelPerMasters(
            scan, self.__plotModel
        )
Valentin Valls's avatar
Valentin Valls committed
832

833
        for plotItem in self.__plotModel.items():
834
            parentChannel = None
835

836
            if isinstance(plotItem, plot_item_model.ScanItem):
Valentin Valls's avatar
Valentin Valls committed
837
                continue
838 839
            if isinstance(plotItem, plot_item_model.MotorPositionMarker):
                continue
Valentin Valls's avatar
Valentin Valls committed
840

Valentin Valls's avatar
Valentin Valls committed
841
            if isinstance(plotItem, plot_model.ComputableMixIn):
842 843 844 845 846 847
                source = plotItem.source()
                if source is None:
                    parent = itemWithoutLocation
                else:
                    itemSource = sourceTree.get(source, None)
                    if itemSource is None:
848
                        parent = itemWithoutMaster
Valentin Valls's avatar
Valentin Valls committed
849
                        _logger.warning("Item list is not well ordered")
850 851 852
                    else:
                        parent = itemSource
            else:
Valentin Valls's avatar
Valentin Valls committed
853 854 855
                if scan is None:
                    parent = itemWithoutLocation
                else:
856
                    if isinstance(plotItem, plot_item_model.CurveItem):
857 858 859 860 861
                        xChannel = plotItem.xChannel()
                        if xChannel is None:
                            yChannel = plotItem.yChannel()
                            if yChannel is not None:
                                yChannelName = yChannel.name()
Valentin Valls's avatar
Valentin Valls committed
862 863 864 865
                                _logger.error(yChannelName)
                                parentChannel = channelItems.get(yChannelName, None)
                                if parentChannel is None:
                                    parent = itemWithoutLocation
866 867
                            else:
                                # item with bad content
868
                                continue
Valentin Valls's avatar
Valentin Valls committed
869
                        else:
870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889
                            topMaster = model_helper.getConsistentTopMaster(
                                scan, plotItem
                            )
                            xChannelName = xChannel.name()
                            if (
                                topMaster is not None
                                and xChannelPerMasters[topMaster] == xChannelName
                            ):
                                # The x-channel is what it is expected then we can link the y-channel
                                yChannel = plotItem.yChannel()
                                if yChannel is not None:
                                    yChannelName = yChannel.name()
                                    parentChannel = channelItems[yChannelName]
                                xAxisItem = channelItems[xChannelName]
                                xAxisItem.setSelectedXAxis()
                                if yChannel is None:
                                    # This item must not be displayed
                                    continue
                            else:
                                parent = itemWithoutLocation
890

891
            if parentChannel is not None:
892
                parentChannel.setPlotItem(plotItem)
893 894
                sourceTree[plotItem] = parentChannel
            else:
895
                item = _DataItem()
896
                item.setEnvironment(self.__tree, self.__flintModel)
897
                parent.appendRow(item.rowItems())
Valentin Valls's avatar
Valentin Valls committed
898
                # It have to be done when model index are initialized
899
                item.setPlotItem(plotItem)
900
                sourceTree[plotItem] = item
901

Valentin Valls's avatar
Valentin Valls committed
902 903 904 905 906
        if itemWithoutLocation.rowCount() == 0:
            model.removeRows(itemWithoutLocation.row(), 1)
        if itemWithoutMaster.rowCount() == 0:
            model.removeRows(itemWithoutMaster.row(), 1)

907
        self.__tree.expandAll()
Valentin Valls's avatar
Valentin Valls committed
908
        _property_tree_helper.collapseNodesFromPaths(self.__tree, collapsed)