#!/usr/bin/env python
# ###########################################################################
#
# This file is part of Taurus
#
# http://taurus-scada.org
#
# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain
#
# Taurus is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Taurus is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Taurus.  If not, see <http://www.gnu.org/licenses/>.
#
# ###########################################################################
"""
itemsmodel Model and view for new CurveItem configuration
"""
# raise UnimplementedError('Under Construction!')
import copy
from taurus.external.qt import Qt
import taurus
from taurus.core.taurusbasetypes import TaurusElementType
from taurus.core.taurusexception import TaurusException
from taurus.qt.qtcore.mimetypes import TAURUS_MODEL_LIST_MIME_TYPE
from taurus.qt.qtcore.mimetypes import TAURUS_ATTR_MIME_TYPE
from taurus.qt.qtcore.mimetypes import TAURUS_MODEL_MIME_TYPE
from taurus.qt.qtgui.icon import getElementTypeIcon
# set some named constants
SRC_ROLE = Qt.Qt.UserRole + 1
[docs]
class TaurusModelItem(object):
    """An item object for :class:`TaurusModelModel`. Exposes `display` `icon`
    and `ok` attributes which are calculated and kept in synch with the
    property `src`
    """
    def __init__(self, src=None, display=None):
        self.icon = Qt.QIcon()
        self.ok = True
        self._src = None
        self.setSrc(src)
        if display is not None:
            self.display = display
    def __deepcopy__(self, memo):
        cls = self.__class__
        result = cls.__new__(cls)
        memo[id(self)] = result
        result.icon = Qt.QIcon(self.icon)
        result.ok = copy.deepcopy(self.ok, memo)
        result._src = copy.deepcopy(self._src, memo)
        result.display = copy.deepcopy(self.display, memo)
        return result
    def __repr__(self):
        ret = "TaurusModelItem('%s')" % (self.display)
        return ret
[docs]
    def getSrc(self):
        """getter for src."""
        return self._src 
[docs]
    def setSrc(self, src):
        """processes the src and sets the values of _src, display, icon and ok
        attributes"""
        if src is None:
            self._src, self.display, self.icon, self.ok = (
                "",
                "(Empty)",
                Qt.QIcon(),
                True,
            )
            return
        src = str(src).strip()
        if src == self._src:
            return
        self._src = src
        # empty
        if src == "":
            self.display, self.icon, self.ok = "(Empty)", Qt.QIcon(), True
            return
        # TODO tango-centric. For tango devices
        try:
            dev = taurus.Device(src)
            if dev.getDeviceProxy() is not None:
                self.display, self.icon, self.ok = (
                    dev.getSimpleName(),
                    getElementTypeIcon(TaurusElementType.Device),
                    True,
                )
                return
            else:
                self.display, self.icon, self.ok = (
                    src,
                    Qt.QIcon.fromTheme("network-error"),
                    False,
                )
                return
        except Exception:
            # for tango attributes
            try:
                attr = taurus.Attribute(src)
                dev = attr.getParentObj()
            except TaurusException:
                self.display, self.icon, self.ok = (
                    src,
                    Qt.QIcon.fromTheme("dialog-warning"),
                    False,
                )
                return
            except Exception:
                # @todo: this catchall except is here as an emergency bugfix,
                #        but should probably be narrowed to tango DevFailed.
                self.display, self.icon, self.ok = (
                    src,
                    Qt.QIcon.fromTheme("network-error"),
                    False,
                )
                return
            if (
                taurus.getSchemeFromName(attr.getFullName()) == "tango"
                and dev.getDeviceProxy() is None
            ):
                self.display, self.icon, self.ok = (
                    src,
                    Qt.QIcon.fromTheme("network-error"),
                    False,
                )
                return
            self.display, self.icon, self.ok = (
                attr.getSimpleName(),
                getElementTypeIcon(TaurusElementType.Attribute),
                True,
            ) 
    # properties
    src = property(getSrc, setSrc) 
[docs]
class TaurusModelModel(Qt.QAbstractListModel):
    """A Qt data model for describing taurus models"""
    def __init__(self, items=None):
        if items is None:
            items = []
        super(TaurusModelModel, self).__init__()
        self.items = items
[docs]
    def addItem(self, item):
        """appends an item to the internal list
        :param item:
        :type item: TaurusModelItem
        """
        self.items.append(item) 
[docs]
    def dumpData(self):
        """returns a deep copy of the internal item list representation"""
        return copy.deepcopy(self.items) 
[docs]
    def rowCount(self, index=Qt.QModelIndex()):
        """reimplemented from :class:`Qt.QAbstractListModel`"""
        return len(self.items) 
[docs]
    def data(self, index, role=Qt.Qt.DisplayRole):
        """reimplemented from :class:`Qt.QAbstractListModel`"""
        if not index.isValid() or not (0 <= index.row() < self.rowCount()):
            return None
        row = index.row()
        # Display Role
        if role == Qt.Qt.DisplayRole:
            return str(self.items[row].display)
        elif role == Qt.Qt.DecorationRole:
            return self.items[row].icon
        elif role == Qt.Qt.ForegroundRole:
            if not self.items[row].src:
                return Qt.QColor("gray")
            return Qt.QColor(self.items[row].ok and "green" or "red")
        elif role == SRC_ROLE:
            return str(self.items[row].src)
        elif role == Qt.Qt.ToolTipRole:
            return str(self.items[row].src)
        if role == Qt.Qt.EditRole:
            return str(self.items[row].src)
        return None 
[docs]
    def flags(self, index):
        """reimplemented from :class:`Qt.QAbstractListModel`"""
        if not index.isValid():
            return Qt.Qt.ItemIsEnabled | Qt.Qt.ItemIsDropEnabled
        return Qt.Qt.ItemFlags(
            Qt.Qt.ItemIsEnabled
            | Qt.Qt.ItemIsEditable
            | Qt.Qt.ItemIsDragEnabled
            | Qt.Qt.ItemIsSelectable
        ) 
[docs]
    def setData(self, index, value=None, role=Qt.Qt.EditRole):
        """reimplemented from :class:`Qt.QAbstractListModel`"""
        if index.isValid() and (0 <= index.row() < self.rowCount()):
            row = index.row()
            item = self.items[row]
            if role == Qt.Qt.EditRole:
                item.src = value
            elif role == Qt.Qt.DisplayRole:
                item.display = value
            self.dataChanged.emit(index, index)
            return True
        return False 
[docs]
    def insertRows(self, position=None, rows=1, parentindex=None, items=None):
        """reimplemented from :class:`Qt.QAbstractListModel`"""
        if position is None or position == -1:
            position = self.rowCount()
        if parentindex is None:
            parentindex = Qt.QModelIndex()
        if items is None:
            slice = [TaurusModelItem() for i in range(rows)]
        else:
            slice = list(items)
            # note that the rows parameter is ignored if items is passed
            rows = len(slice)
        self.beginInsertRows(parentindex, position, position + rows - 1)
        self.items = self.items[:position] + slice + self.items[position:]
        self.endInsertRows()
        return True 
[docs]
    def removeRows(self, position, rows=1, parentindex=None):
        """reimplemented from :class:`Qt.QAbstractListModel`"""
        if parentindex is None:
            parentindex = Qt.QModelIndex()
        self.beginResetModel()
        self.beginRemoveRows(parentindex, position, position + rows - 1)
        self.items = self.items[:position] + self.items[position + rows :]
        self.endRemoveRows()
        self.endResetModel()
        return True 
[docs]
    def clearAll(self):
        """clears all rows"""
        self.removeRows(0, self.rowCount()) 
[docs]
    def swapItems(self, index1, index2):
        """swap the items described by index1 and index2 in the list"""
        r1, r2 = index1.row(), index2.row()
        self.items[r1], self.items[r2] = self.items[r2], self.items[r1]
        self.dataChanged.emit(index1, index2) 
[docs]
    def mimeTypes(self):
        """reimplemented from :class:`Qt.QAbstractListModel`"""
        result = list(Qt.QAbstractItemModel.mimeTypes(self))
        result += [
            TAURUS_ATTR_MIME_TYPE,
            TAURUS_MODEL_MIME_TYPE,
            TAURUS_MODEL_LIST_MIME_TYPE,
            "text/plain",
        ]
        return result 
[docs]
    def dropMimeData(self, data, action, row, column, parent):
        """reimplemented from :class:`Qt.QAbstractListModel`"""
        if row == -1 and parent.isValid():
            row = parent.row()
        if data.hasFormat(TAURUS_ATTR_MIME_TYPE):
            items = [bytes(data.data(TAURUS_ATTR_MIME_TYPE)).decode("utf-8")]
        elif data.hasFormat(TAURUS_MODEL_MIME_TYPE):
            items = [bytes(data.data(TAURUS_MODEL_MIME_TYPE)).decode("utf-8")]
        elif data.hasFormat(TAURUS_MODEL_LIST_MIME_TYPE):
            items = (
                bytes(data.data(TAURUS_MODEL_LIST_MIME_TYPE))
                .decode("utf-8")
                .split()
            )
        elif data.hasText():
            items = [str(data.text())]
        else:
            return False
        self.insertItems(row, items)
        return True 
[docs]
    def insertItems(self, row, items):
        """convenience method to add new rows by passing a list of strings ()
        :param row: the row of the list at which the item insertion starts, if
            row==-1, items will be appended to the list
        :type row: int
        :param items: a sequence items to add to the list. The objects in the
            sequence can be either strings, :class:`TaurusModelItem` objects or
            tuples of valid arguments for initializing :class:`TaurusModelItem`
            objects
        :type items: seq
        """
        itemobjs = []
        for e in items:
            if isinstance(e, TaurusModelItem):
                itemobjs.append(e)
            elif isinstance(e, str):
                itemobjs.append(TaurusModelItem(src=e))
            else:
                # assuming it is a sequence of arguments that can be passed
                # to the constructor of TaurusModelItem
                itemobjs.append(TaurusModelItem(*e))
        self.insertRows(position=row, items=itemobjs) 
[docs]
    def mimeData(self, indexes):
        """reimplemented from :class:`Qt.QAbstractListModel`"""
        mimedata = Qt.QAbstractListModel.mimeData(self, indexes)
        if len(indexes) == 1:
            # mimedata.setData(TAURUS_ATTR_MIME_TYPE, self.data(indexes[0]))
            txt = self.data(indexes[0], role=SRC_ROLE)
            mimedata.setText(txt)
        return mimedata 
 
        # mimedata.setData()
[docs]
class TaurusModelList(Qt.QListView):
    """A list view widget to display and manage a list of models
    Tries to identify the type of model and show the state of the device/attr
    associated with it. It also allows drag and drop of models and sorting.
    """
    dataChangedSignal = Qt.pyqtSignal(list, name="dataChanged")
    def __init__(self, parent=None, items=None, designMode=False):
        super(TaurusModelList, self).__init__(parent)
        if items is None:
            items = []
        self._model = TaurusModelModel(items)
        self.setModel(self._model)
        self.setDragDropMode(self.DragDropMode.DragDrop)
        # self.setAcceptDrops(True)
        self.setSelectionMode(self.SelectionMode.ExtendedSelection)
        self._contextMenu = Qt.QMenu(self)
        self.addRowAction = Qt.QAction("Add new row", self)
        self.addRowAction.setIcon(Qt.QIcon.fromTheme("list-add"))
        self.addRowAction.triggered.connect(self.newRow)
        self.addRowAction.setShortcut(Qt.QKeySequence.New)
        self.removeSelectedAction = Qt.QAction("Remove Selected", self)
        self.removeSelectedAction.setIcon(Qt.QIcon.fromTheme("list-remove"))
        self.removeSelectedAction.triggered.connect(self.removeSelected)
        self.removeSelectedAction.setShortcut(Qt.QKeySequence.Delete)
        self.removeAllAction = Qt.QAction("Clear all", self)
        self.removeAllAction.setIcon(Qt.QIcon.fromTheme("edit-clear"))
        self.removeAllAction.triggered.connect(self.clear)
        self.removeAllAction.setShortcut(Qt.QKeySequence("Ctrl+Del"))
        self.moveUpAction = Qt.QAction("Move up in the list", self)
        self.moveUpAction.setIcon(Qt.QIcon.fromTheme("go-up"))
        self.moveUpAction.triggered.connect(self._onMoveUpAction)
        self.moveUpAction.setShortcut(Qt.QKeySequence("Alt+Up"))
        self.moveDownAction = Qt.QAction("Move down in the list", self)
        self.moveDownAction.setIcon(Qt.QIcon.fromTheme("go-down"))
        self.moveDownAction.triggered.connect(self._onMoveDownAction)
        self.moveDownAction.setShortcut(Qt.QKeySequence("Alt+Down"))
        self.editDisplayAction = Qt.QAction(
            "Edit the display (leave the source)", self
        )
        self.editDisplayAction.setIcon(
            Qt.QIcon("actions:format-text-italic.svg")
        )
        self.editDisplayAction.triggered.connect(self._onEditDisplay)
        self.editDisplayAction.setShortcut(Qt.QKeySequence("Alt+D"))
        self.addActions(
            [
                self.addRowAction,
                self.removeSelectedAction,
                self.removeAllAction,
                self.moveUpAction,
                self.moveDownAction,
                self.editDisplayAction,
            ]
        )
        # signal connections
        selectionmodel = self.selectionModel()
        selectionmodel.selectionChanged.connect(self._onSelectionChanged)
        self._model.dataChanged.connect(self._onDataChanged)
        self._model.rowsInserted.connect(self._onDataChanged)
        self._model.rowsRemoved.connect(self._onDataChanged)
        self._onSelectionChanged(Qt.QItemSelection(), Qt.QItemSelection())
[docs]
    def clear(self):
        """removes all items from the list"""
        self._model.clearAll() 
    def _onEditDisplay(self):
        selected = self.selectionModel().selectedIndexes()
        if len(selected) == 1:
            idx = selected[0]
        else:
            return
        value = self._model.data(idx, role=Qt.Qt.DisplayRole)
        src = self._model.data(idx, role=SRC_ROLE)
        value, ok = Qt.QInputDialog.getText(
            self,
            "Display Value",
            "Display value for %s?" % src,
            Qt.QLineEdit.Normal,
            value,
        )
        if not ok:
            return
        self._model.setData(idx, value, role=Qt.Qt.DisplayRole)
    def _onSelectionChanged(self, selected, deselected):
        """updates the status of the actions that depend on the selection"""
        selectedIndexes = self.selectionModel().selectedRows()
        self.removeSelectedAction.setEnabled(len(selectedIndexes) > 0)
        self.moveUpAction.setEnabled(
            len(selectedIndexes) == 1 and selectedIndexes[0].row() > 0
        )
        self.moveDownAction.setEnabled(
            len(selectedIndexes) == 1
            and (0 <= selectedIndexes[0].row() < self._model.rowCount() - 1)
        )
        self.editDisplayAction.setEnabled(len(selectedIndexes) > 0)
    def _onDataChanged(self, *args):
        """emits a signal containing the current data as a list of strings"""
        self.dataChangedSignal.emit(self.getModelItems())
    def _onMoveUpAction(self):
        """slot for move up action"""
        selected = self.selectionModel().selectedIndexes()
        if len(selected) != 1:
            return
        i1 = selected[0]
        i2 = self._model.index(i1.row() - 1)
        self._model.swapItems(i1, i2)
        self.selectionModel().select(i2, Qt.QItemSelectionModel.ClearAndSelect)
    def _onMoveDownAction(self):
        """slot for move down action"""
        selected = self.selectionModel().selectedIndexes()
        if len(selected) != 1:
            return
        i1 = selected[0]
        i2 = self._model.index(i1.row() + 1)
        self._model.swapItems(i1, i2)
        self.selectionModel().select(i2, Qt.QItemSelectionModel.ClearAndSelect)
[docs]
    def newRow(self, position=None):
        """adds an empty row *before* the given position
        :param position: position at which the new row will be added. If None
            passed, it will be added at the end.
        :type position: int or None
        """
        if position is None:
            selected = self.selectionModel().selectedIndexes()
            if len(selected) == 0:
                position = -1
            elif len(selected) == 1:
                position = selected[0].row()
            else:
                return
        self._model.insertItems(position, [""]) 
[docs]
    def removeSelected(self):
        """removes selected items from the list"""
        selected = sorted(
            [idx.row() for idx in self.selectionModel().selectedRows()],
            reverse=True,
        )
        for row in selected:  # we remove rows starting from the last one
            self._model.removeRows(row) 
[docs]
    def addModels(self, models):
        """adds models to the list
        :param models: sequence of model names to be added
        :type models: list<str>
        """
        self._model.insertItems(-1, models) 
[docs]
    def getModelItems(self):
        """returns the model item objects
        :return:
        :rtype: list<TaurusModelItem>
        .. seealso:: :meth:`getModelList`
        """
        return self._model.dumpData() 
[docs]
    def getModelList(self):
        """returns a the model names corresponding to the items in the list
        :return:
        :rtype: list<str>
        .. seealso:: :meth:`getModelItems`
        """
        return [str(s.src) for s in self.getModelItems()] 
[docs]
    @classmethod
    def getQtDesignerPluginInfo(cls):
        return {
            "group": "Taurus Input",
            "icon": "logos:taurus.png",
            "container": False,
            "module": "taurus.qt.qtgui.panel",
        } 
 
if __name__ == "__main__":
    from taurus.qt.qtgui.application import TaurusApplication
    import sys
    app = TaurusApplication(cmd_line_parser=None)
    w = TaurusModelList()
    w.addModels(
        ["item%i" % i for i in range(3)]
        + [TaurusModelItem(src="src1", display="d1")]
        + [("src2", "d2")]
    )
    w.show()
    sys.exit(app.exec_())