def setupTabs(self):
        """ Setup the various tabs in the AddressWidget. """
        groups = ["ABC", "DEF", "GHI", "JKL", "MNO", "PQR", "STU", "VW", "XYZ"]

        for group in groups:
            proxyModel = QSortFilterProxyModel(self)
            proxyModel.setSourceModel(self.tableModel)
            proxyModel.setDynamicSortFilter(True)

            tableView = QTableView()
            tableView.setModel(proxyModel)
            tableView.setSortingEnabled(True)
            tableView.setSelectionBehavior(QAbstractItemView.SelectRows)
            tableView.horizontalHeader().setStretchLastSection(True)
            tableView.verticalHeader().hide()
            tableView.setEditTriggers(QAbstractItemView.NoEditTriggers)
            tableView.setSelectionMode(QAbstractItemView.SingleSelection)

            # This here be the magic: we use the group name (e.g. "ABC") to
            # build the regex for the QSortFilterProxyModel for the group's
            # tab. The regex will end up looking like "^[ABC].*", only
            # allowing this tab to display items where the name starts with
            # "A", "B", or "C". Notice that we set it to be case-insensitive.
            reFilter = "^[%s].*" % group

            proxyModel.setFilterRegExp(QRegExp(reFilter, Qt.CaseInsensitive))
            proxyModel.setFilterKeyColumn(0) # Filter on the "name" column
            proxyModel.sort(0, Qt.AscendingOrder)

            # This prevents an application crash (see: http://www.qtcentre.org/threads/58874-QListView-SelectionModel-selectionChanged-Crash)
            viewselectionmodel = tableView.selectionModel()
            tableView.selectionModel().selectionChanged.connect(self.selectionChanged)

            self.addTab(tableView, group)
    def setupTabs(self):
        """ Setup the various tabs in the AddressWidget. """
        groups = ["ABC", "DEF", "GHI", "JKL", "MNO", "PQR", "STU", "VW", "XYZ"]

        for group in groups:
            proxyModel = QSortFilterProxyModel(self)
            proxyModel.setSourceModel(self.tableModel)
            proxyModel.setDynamicSortFilter(True)

            tableView = QTableView()
            tableView.setModel(proxyModel)
            tableView.setSortingEnabled(True)
            tableView.setSelectionBehavior(QAbstractItemView.SelectRows)
            tableView.horizontalHeader().setStretchLastSection(True)
            tableView.verticalHeader().hide()
            tableView.setEditTriggers(QAbstractItemView.NoEditTriggers)
            tableView.setSelectionMode(QAbstractItemView.SingleSelection)

            # This here be the magic: we use the group name (e.g. "ABC") to
            # build the regex for the QSortFilterProxyModel for the group's
            # tab. The regex will end up looking like "^[ABC].*", only
            # allowing this tab to display items where the name starts with
            # "A", "B", or "C". Notice that we set it to be case-insensitive.
            reFilter = "^[%s].*" % group

            proxyModel.setFilterRegExp(QRegExp(reFilter, Qt.CaseInsensitive))
            proxyModel.setFilterKeyColumn(0)  # Filter on the "name" column
            proxyModel.sort(0, Qt.AscendingOrder)

            # This prevents an application crash (see: http://www.qtcentre.org/threads/58874-QListView-SelectionModel-selectionChanged-Crash)
            viewselectionmodel = tableView.selectionModel()
            tableView.selectionModel().selectionChanged.connect(
                self.selectionChanged)

            self.addTab(tableView, group)
class AddressWidget(QDialog):

    selectionChanged = Signal(QItemSelection)

    def __init__(self, parent=None):
        super(AddressWidget, self).__init__(parent)

        self.tableModel = TableModel()
        self.tableView = QTableView()
        self.setupTable()

        statusLabel = QLabel("Tabular data view demo")

        layout = QVBoxLayout()
        layout.addWidget(self.tableView)
        layout.addWidget(statusLabel)
        self.setLayout(layout)
        self.setWindowTitle("Address Book")
        self.resize(800,500)

        # add test data
        self.populateTestData()

    def setupTable(self):
        proxyModel = QSortFilterProxyModel(self)
        proxyModel.setSourceModel(self.tableModel)
        proxyModel.setDynamicSortFilter(True)

        self.tableView.setModel(proxyModel)
        self.tableView.setSortingEnabled(True)
        self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.tableView.horizontalHeader().setStretchLastSection(True)
        self.tableView.verticalHeader().hide()
        self.tableView.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.tableView.setSelectionMode(QAbstractItemView.SingleSelection)

        proxyModel.setFilterKeyColumn(0)  # Filter on the "name" column
        proxyModel.sort(0, Qt.AscendingOrder)
        viewselectionmodel = self.tableView.selectionModel()
        self.tableView.selectionModel().selectionChanged.connect(self.selectionChanged)



    def populateTestData(self):
        addresses = [{"name": "John Doe", "address": "Alameda"},
                     {"name": "Alan Turing", "address": "San Deigo"},
                     {"name": "Bjarne Stroutsup", "address": "Columbia"},
                     {"name": "Herb Sutter", "address": "Seattle"},
                     {"name": "Micheal Konin", "address": "Colorado"}]

        for i in range(len(addresses)):
            self.tableModel.insertRows(0)
            ix = self.tableModel.index(0, 0, QModelIndex())
            self.tableModel.setData(ix, addresses[i]["name"], Qt.EditRole)

            ix = self.tableModel.index(0, 1, QModelIndex())
            self.tableModel.setData(ix, addresses[i]["address"], Qt.EditRole)

            self.tableView.resizeRowToContents(ix.row())
Exemple #4
0
class _ExtractSeriesWidget(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        # {series name: {frame name: [ (Attribute, QPersistentModelIndex) ]
        self.seriesOptions: Dict[str, Dict[str, List[Tuple[
            int, QPersistentModelIndex]]]] = dict()
        # {frame name: attribute model}
        self.models: Dict[str, CustomProxyAttributeModel] = dict()
        self.seriesView = CustomSignalView(parent=self)
        self.seriesModel = CustomStringListModel(self)
        self.seriesModel.setHeaderLabel('Series name')
        self.addSeriesButton = QPushButton('Add', self)
        self.removeSeriesButton = QPushButton('Remove', self)
        self.addSeriesButton.clicked.connect(self.addSeries)
        self.removeSeriesButton.clicked.connect(self.removeSeries)
        self.seriesView.setModel(self.seriesModel)
        self.seriesView.setDragDropMode(QTableView.InternalMove)
        self.seriesView.setDragDropOverwriteMode(False)
        self.seriesView.horizontalHeader().setSectionResizeMode(
            QHeaderView.Stretch)
        self.seriesView.verticalHeader().hide()

        # Connect selection to change
        self.seriesView.selectedRowChanged[str, str].connect(
            self.onSeriesSelectionChanged)
        # When a series is added it should be immediately edited
        self.seriesModel.rowAppended.connect(self.editSeriesName)
        self.seriesModel.rowsInserted.connect(self.checkNoSeries)
        self.seriesModel.rowsRemoved.connect(self.checkNoSeries)

        self.workbenchView = WorkbenchView(self, editable=False)
        self.workbench: WorkbenchModel = None
        self.workbenchView.selectedRowChanged[str, str].connect(
            self.onFrameSelectionChanged)

        self.attributesView = SearchableAttributeTableWidget(
            self, True, False, False, [Types.Numeric, Types.Ordinal])

        firstRowLayout = QHBoxLayout()
        firstRowLayout.setSpacing(5)

        selectionGroup = QGroupBox(
            title=
            'Select a time series. Then select the columns to add from the current '
            'datasets',
            parent=self)
        firstRowLayout.addWidget(self.seriesView)
        buttonLayout = QVBoxLayout()
        buttonLayout.addWidget(self.addSeriesButton)
        buttonLayout.addWidget(self.removeSeriesButton)
        firstRowLayout.addLayout(buttonLayout)
        firstRowLayout.addSpacing(30)
        firstRowLayout.addWidget(self.workbenchView)
        firstRowLayout.addSpacing(30)
        firstRowLayout.addWidget(self.attributesView)
        selectionGroup.setLayout(firstRowLayout)

        # Time axis labels model with add/remove buttons
        self.timeAxisModel = CustomStringListModel(self)
        self.timeAxisModel.setHeaderLabel('Time labels')
        self.timeAxisView = CustomSignalView(self)
        self.timeAxisView.setModel(self.timeAxisModel)
        self.addTimeButton = QPushButton('Add', self)
        self.removeTimeButton = QPushButton('Remove', self)
        self.addTimeButton.clicked.connect(self.addTimeLabel)
        self.removeTimeButton.clicked.connect(self.removeTimeLabel)
        self.timeAxisView.setDragDropMode(QTableView.InternalMove)
        self.timeAxisView.setDragDropOverwriteMode(False)
        self.timeAxisView.horizontalHeader().setSectionResizeMode(
            QHeaderView.Stretch)
        self.timeAxisView.verticalHeader().hide()
        self.timeAxisModel.rowAppended.connect(self.editTimeLabelName)

        # Concatenation model
        self.timeSeriesDataModel = ConcatenatedModel(self)
        self.timeSeriesDataView = QTableView(self)
        self.timeSeriesDataView.setSelectionMode(QTableView.NoSelection)
        self.timeSeriesDataView.setItemDelegateForColumn(
            1, ComboBoxDelegate(self.timeAxisModel, self.timeSeriesDataView))
        self.timeSeriesDataView.setEditTriggers(QTableView.CurrentChanged
                                                | QTableView.DoubleClicked)
        self.timeSeriesDataView.verticalHeader().hide()
        # Update the label column when some label changes in the label table
        self.timeAxisModel.dataChanged.connect(
            self.timeSeriesDataModel.timeAxisLabelChanged)

        groupTime = QGroupBox(
            title=
            'Add the time points (ordered) and set the correspondence to every selected column',
            parent=self)
        secondRowLayout = QHBoxLayout()
        secondRowLayout.setSpacing(5)
        # labelLayout = QVBoxLayout()
        # lab = QLabel('Here you should define every time point, in the correct order. After adding '
        #              'double-click a row to edit the point name and drag rows to reorder them', self)
        # lab.setWordWrap(True)
        # labelLayout.addWidget(lab)
        # labelLayout.addWidget(self.timeAxisView)
        secondRowLayout.addWidget(self.timeAxisView)
        timeButtonLayout = QVBoxLayout()
        timeButtonLayout.addWidget(self.addTimeButton)
        timeButtonLayout.addWidget(self.removeTimeButton)
        secondRowLayout.addLayout(timeButtonLayout)
        secondRowLayout.addSpacing(30)
        # labelLayout = QVBoxLayout()
        # lab = QLabel('Every selected column for the current series will be listed here. Click the right '
        #              'column of the table to set the time label associated with every original column',
        #              self)
        # lab.setWordWrap(True)
        # labelLayout.addWidget(lab)
        secondRowLayout.addWidget(self.timeSeriesDataView)
        # secondRowLayout.addLayout(labelLayout)
        groupTime.setLayout(secondRowLayout)

        self.outputName = QLineEdit(self)
        self.warningLabel = MessageLabel(text='',
                                         color='orange',
                                         icon=QMessageBox.Warning,
                                         parent=self)
        lastRowLayout = QFormLayout()
        lastRowLayout.addRow('Output variable name:', self.outputName)
        self.outputName.setPlaceholderText('Output name')
        lastRowLayout.setVerticalSpacing(0)
        lastRowLayout.addRow('', self.warningLabel)
        lastRowLayout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
        self.warningLabel.hide()
        self.outputName.textChanged.connect(self.checkOutputName)

        layout = QVBoxLayout(self)
        layout.addWidget(selectionGroup)
        layout.addWidget(groupTime)
        layout.addLayout(lastRowLayout)
        self.checkNoSeries()

    def setWorkbench(self, w: WorkbenchModel) -> None:
        """
        Sets the workbench and initialises every attribute model (one for each frame)
        """
        self.workbench = w
        self.workbenchView.setModel(w)
        # Set a default name for output
        if self.workbench:
            name = 'time_series_{:d}'
            n = 1
            name_n = name.format(n)
            while name_n in self.workbench.names:
                n += 1
                name_n = name.format(n)
            self.outputName.setText(name_n)

    def addSourceFrameModel(self, frameName: str) -> None:
        if self.workbench:
            dfModel = self.workbench.getDataframeModelByName(frameName)
            # Create an attribute model with checkboxes
            standardModel = AttributeTableModel(self,
                                                checkable=True,
                                                editable=False,
                                                showTypes=True)
            standardModel.setFrameModel(dfModel)
            # Create a proxy to filter data in the concatenation
            customProxy = CustomProxyAttributeModel(self)
            customProxy.setSourceModel(standardModel)
            # Add proxy to the list of models
            self.models[frameName] = customProxy
            # Add proxy as source model
            self.timeSeriesDataModel.addSourceModel(customProxy)

    @Slot()
    def checkNoSeries(self) -> None:
        if not self.seriesModel.rowCount():
            self.workbenchView.setEnabled(False)
            self.attributesView.setEnabled(False)
            self.timeAxisView.setEnabled(False)
            self.addTimeButton.setEnabled(False)
            self.removeTimeButton.setEnabled(False)
            self.timeSeriesDataView.setEnabled(False)
        else:
            self.workbenchView.setEnabled(True)
            self.attributesView.setEnabled(True)
            self.timeAxisView.setEnabled(True)
            self.addTimeButton.setEnabled(True)
            self.removeTimeButton.setEnabled(True)
            self.timeSeriesDataView.setEnabled(True)

    def persistOptionsSetForSeries(self, seriesName: str) -> None:
        if seriesName:
            seriesValues: Dict[str,
                               List[Tuple[int,
                                          QPersistentModelIndex]]] = dict()
            for r in range(self.timeSeriesDataModel.rowCount()):
                column0Index: QModelIndex = self.timeSeriesDataModel.index(
                    r, 0, QModelIndex())
                column1Index: QModelIndex = self.timeSeriesDataModel.index(
                    r, 1, QModelIndex())
                sourceIndex: QModelIndex = self.timeSeriesDataModel.mapToSource(
                    column0Index)
                proxy: CustomProxyAttributeModel = sourceIndex.model()
                frameName: str = proxy.sourceModel().frameModel().name
                attrIndexInFrame: int = proxy.mapToSource(sourceIndex).row()
                timeLabelIndex: QPersistentModelIndex = column1Index.data(
                    Qt.DisplayRole)
                if seriesValues.get(frameName, None):
                    seriesValues[frameName].append(
                        (attrIndexInFrame, timeLabelIndex))
                else:
                    seriesValues[frameName] = [(attrIndexInFrame,
                                                timeLabelIndex)]
            self.seriesOptions[seriesName] = seriesValues

    @Slot(str, str)
    def onSeriesSelectionChanged(self, new: str, old: str) -> None:
        # Save current set options
        self.persistOptionsSetForSeries(old)
        if new:
            # Get options of new selection
            newOptions: Dict[str, List[Tuple[int, QPersistentModelIndex]]] = \
                self.seriesOptions.get(new, dict())
            for frameName, proxyModel in self.models.items():
                frameOptions = newOptions.get(frameName, None)
                self.setOptionsForFrame(frameName, frameOptions)
                # Update proxy view on the time label columns
                proxyModel.dataChanged.emit(
                    proxyModel.index(0, 1, QModelIndex()),
                    proxyModel.index(proxyModel.rowCount() - 1, 1,
                                     QModelIndex()),
                    [Qt.DisplayRole, Qt.EditRole])
        # Every time series change clear frame selection in workbench
        self.workbenchView.clearSelection()

    @Slot(str, str)
    def onFrameSelectionChanged(self, newFrame: str, _: str) -> None:
        if not newFrame:
            # Nothing is selected
            return self.attributesView.setAttributeModel(
                AttributeTableModel(self))
        # Check if frame is already in the source models
        if newFrame not in self.models.keys():
            # Create a new proxy and add it to source models
            self.addSourceFrameModel(newFrame)
            if len(self.models) == 1:
                # If it is the first model added then set up the view
                self.timeSeriesDataView.setModel(self.timeSeriesDataModel)
                self.timeSeriesDataView.horizontalHeader(
                ).setSectionResizeMode(0, QHeaderView.Stretch)
                self.timeSeriesDataView.horizontalHeader(
                ).setSectionResizeMode(1, QHeaderView.Stretch)
        # Update the attribute table
        self.attributesView.setAttributeModel(
            self.models[newFrame].sourceModel())

    def setOptionsForFrame(
            self, frameName: str,
            options: Optional[List[Tuple[int,
                                         QPersistentModelIndex]]]) -> None:
        customProxyModel = self.models[frameName]
        attributeTableModel = customProxyModel.sourceModel()
        attributeTableModel.setAllChecked(False)
        if options:
            proxySelection: Dict[int, QPersistentModelIndex] = {
                i: pmi
                for i, pmi in options
            }
            customProxyModel.attributes = proxySelection
            attributeTableModel.setChecked(list(proxySelection.keys()),
                                           value=True)
        else:
            customProxyModel.attributes = dict()

    @Slot()
    def addSeries(self) -> None:
        # Append new row
        self.seriesModel.appendEmptyRow()
        # In oder to avoid copying previous options
        for frameName, proxyModel in self.models.items():
            self.setOptionsForFrame(frameName, None)

    @Slot()
    def removeSeries(self) -> None:
        selected: List[QModelIndex] = self.seriesView.selectedIndexes()
        if selected:
            seriesName: str = selected[0].data(Qt.DisplayRole)
            # Remove row
            self.seriesModel.removeRow(selected[0].row())
            # Remove options for series if they exists
            self.seriesOptions.pop(seriesName, None)

    @Slot()
    def addTimeLabel(self) -> None:
        self.timeAxisModel.appendEmptyRow()

    @Slot()
    def removeTimeLabel(self) -> None:
        selected: List[QModelIndex] = self.timeAxisView.selectedIndexes()
        if selected:
            self.timeAxisModel.removeRow(selected[0].row())
            # Update model
            self.timeSeriesDataModel.dataChanged.emit(
                self.timeSeriesDataModel.index(0, 1, QModelIndex()),
                self.timeSeriesDataModel.index(
                    self.timeSeriesDataModel.rowCount() - 1, 1, QModelIndex()),
                [Qt.DisplayRole, Qt.EditRole])

    @Slot()
    def editSeriesName(self) -> None:
        index = self.seriesModel.index(self.seriesModel.rowCount() - 1, 0,
                                       QModelIndex())
        self.seriesView.setCurrentIndex(index)
        self.seriesView.edit(index)

    @Slot()
    def editTimeLabelName(self) -> None:
        index = self.timeAxisModel.index(self.timeAxisModel.rowCount() - 1, 0,
                                         QModelIndex())
        self.timeAxisView.setCurrentIndex(index)
        self.timeAxisView.edit(index)

    @Slot(str)
    def checkOutputName(self, text: str) -> None:
        if self.workbench and text in self.workbench.names:
            self.warningLabel.setText(
                'Variable {:s} will be overwritten'.format(text))
            self.warningLabel.show()
        else:
            self.warningLabel.hide()
Exemple #5
0
 def _create_table(self, ) -> QTableView:
     widget = QTableView(self)
     widget.horizontalHeader().hide()
     widget.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
     widget.setEditTriggers(QAbstractItemView.AllEditTriggers, )
     return widget
class WTableParameterEdit(QWidget):

    dataChanged = Signal()

    def __init__(self, obj):
        super(WTableParameterEdit, self).__init__()
        self._obj = obj

        self._model = WTableParameterModel(self._obj)

        self._view = QTableView()
        self._view.setModel(self._model)

        self.setupUi()
        self.setNotEditableHidden(True)
        self.resizeToContent()

        self._model.dataChanged.connect(self.dataChanged.emit)

    def resizeToContent(self):
        """
        Resize all the columns (but the last) to fit the content or the minimum size.
        """
        rootIndex = self._view.rootIndex()
        for colId in range(self._model.columnCount() - 1):
            self._view.resizeColumnToContents(colId)

    def setNotEditableHidden(self, hidden):
        """Set the hide state of all rows that are not editable."""
        rootIndex = self._view.rootIndex()
        for rowId in range(self._model.rowCount()):
            isEditableRow = False
            for colId in range(self._model.columnCount()):
                flag = self._model.flags(
                    self._model.index(rowId, colId, rootIndex))
                if flag & Qt.ItemIsEditable:
                    isEditableRow = True

            self._view.setRowHidden(rowId, not isEditableRow and hidden)

    def setupUi(self):
        header = QLabel(f"{type(self._obj).__name__}")
        font = header.font()
        font.setBold(True)
        header.setFont(font)

        self.mainLayout = QVBoxLayout()
        self.mainLayout.addWidget(header)

        if isinstance(self._obj, (list, dict)):
            label = f"The {type(self._obj).__name__} has {len(self._obj)} element"
            if len(self._obj) != 1:
                label += "s"
            self.mainLayout.addWidget(QLabel(label + "."))
            self.mainLayout.addStretch()
        elif self._obj is None:
            self.mainLayout.addWidget(QLabel("Not implemented yet."))
            self.mainLayout.addStretch()
        else:
            self.mainLayout.addWidget(self._view)

        self.setLayout(self.mainLayout)

        # === table settings ===
        # self._view.resizeColumnsToContents()
        self._view.verticalHeader().setVisible(False)
        self._view.setFocusPolicy(Qt.NoFocus)
        self._view.setSelectionMode(QAbstractItemView.SingleSelection)
        self._view.setEditTriggers(QAbstractItemView.AllEditTriggers)
        self._view.horizontalHeader().setStretchLastSection(True)
        self._view.horizontalHeader().setSectionsClickable(False)
        # self._view.setMinimumWidth(650)

        self.setMinimumWidth(600)
class AddressWidget(QDialog):

    selectionChanged = Signal(QItemSelection)

    def __init__(self, parent=None):
        super(AddressWidget, self).__init__(parent)

        self.tableModel = TableModel()
        self.tableView = QTableView()
        self.setupTable()

        statusLabel = QLabel("Tabular data view demo")

        layout = QVBoxLayout()
        layout.addWidget(self.tableView)
        layout.addWidget(statusLabel)
        self.setLayout(layout)
        self.setWindowTitle("Address Book")
        self.resize(800, 500)

        # add test data
        self.populateTestData()

    def setupTable(self):
        proxyModel = QSortFilterProxyModel(self)
        proxyModel.setSourceModel(self.tableModel)
        proxyModel.setDynamicSortFilter(True)

        self.tableView.setModel(proxyModel)
        self.tableView.setSortingEnabled(True)
        self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.tableView.horizontalHeader().setStretchLastSection(True)
        self.tableView.verticalHeader().hide()
        self.tableView.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.tableView.setSelectionMode(QAbstractItemView.SingleSelection)

        proxyModel.setFilterKeyColumn(0)  # Filter on the "name" column
        proxyModel.sort(0, Qt.AscendingOrder)
        viewselectionmodel = self.tableView.selectionModel()
        self.tableView.selectionModel().selectionChanged.connect(
            self.selectionChanged)

    def populateTestData(self):
        addresses = [{
            "name": "John Doe",
            "address": "Alameda"
        }, {
            "name": "Alan Turing",
            "address": "San Deigo"
        }, {
            "name": "Bjarne Stroutsup",
            "address": "Columbia"
        }, {
            "name": "Herb Sutter",
            "address": "Seattle"
        }, {
            "name": "Micheal Konin",
            "address": "Colorado"
        }]

        for i in range(len(addresses)):
            self.tableModel.insertRows(0)
            ix = self.tableModel.index(0, 0, QModelIndex())
            self.tableModel.setData(ix, addresses[i]["name"], Qt.EditRole)

            ix = self.tableModel.index(0, 1, QModelIndex())
            self.tableModel.setData(ix, addresses[i]["address"], Qt.EditRole)

            self.tableView.resizeRowToContents(ix.row())
class AttributesTab(QWidget):

    def __init__(self):
        """
        Widget containing the table summary of all attributes
        """
        QWidget.__init__(self)

        # Data
        self.attributes = {}  # Attributes names to ids -> {attr_name: attr_id, ...}
        self.attr_idx = {}  # Attributes ids to indexes -> {attr_id: attr_idx, ...}
        self.students = {}  # Students names to ids -> {std_name: std_id, ...}
        self.std_idx = {}  # Students ids to indexes -> {std_id: std_idx, ...}
        self.data = {}  # Attributes and students ids to cell data -> {(attr_id, std_id): cell_data, ...}

        self.dm: AttributesTableModel = None

        # Widget
        self.table_attributes = QTableView()
        self.table_attributes.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.table_attributes.setSelectionMode(QAbstractItemView.SingleSelection)
        self.table_attributes.verticalHeader().setSectionResizeMode(QHeaderView.Fixed)
        self.table_attributes.verticalHeader().setFixedWidth(150)

        # Signals
        self.table_attributes.clicked.connect(self.on_cell_clicked)
        self.sig_cell_clicked: Signal = None

        # Layout
        self.__set_layout()

        self.set_data([], [], {})

    def __set_layout(self) -> None:
        layout = QHBoxLayout()
        layout.setSpacing(0)

        layout.addWidget(self.table_attributes)

        self.setLayout(layout)

    def set_data(self, attributes_data: list, students_data: list, data: dict) -> None:
        """
        Sets the data inside the table

        :param attributes_data: Attributes list (needs to be ordered) = [(attr_id, attr_name), ...]
        :param students_data: Students list (needs to be ordered) = [(std_id, std_name), ...]
        :param data: Table's data {(attr_id, std_id}: cell_data, ...}
        """
        self.attributes = {}
        self.attr_idx = {}
        i = 0
        for attr_id, attr_name in attributes_data:
            self.attributes[attr_name] = attr_id
            self.attr_idx[attr_id] = i
            i += 1

        self.students = {}
        self.std_idx = {}
        i = 0
        for std_id, std_name in students_data:
            self.students[std_name] = std_id
            self.std_idx[std_id] = i
            i += 1

        self.data = data
        attr_header = [a[1] for a in attributes_data]
        std_header = [s[1] for s in students_data]

        data_for_model = []
        for _ in range(len(std_header)):
            data_for_model.append([None] * len(attr_header))

        for attr_id, std_id in data:
            data_for_model[self.std_idx[std_id]][self.attr_idx[attr_id]] = data[(attr_id, std_id)]

        self.dm = AttributesTableModel(self.table_attributes, data_for_model, attr_header, std_header)

    def on_cell_clicked(self, item) -> None:
        """
        Triggered when a cell is clicked. Emits a signal with the attribute and student ids
        """
        attr_id = None
        for a in self.attr_idx:
            if self.attr_idx[a] == item.column():
                attr_id = a

        std_id = None
        for s in self.std_idx:
            if self.std_idx[s] == item.row():
                std_id = s

        self.sig_cell_clicked.emit(attr_id, std_id)
class VStdAttributesDialog(QDialog):
    def __init__(self, parent, sig_attribute_edition: Signal, student: Student,
                 attributes: list):
        """
        Confirm dialog for dangerous actions

        :param parent: gui's main window
        :param sig_attribute_edition: Signal to emit in order to edit the selected attribute
        :param student: Current student
        :param attributes: List of attributes
        """
        QDialog.__init__(self, parent)

        self.setFixedSize(QSize(350, 350))
        self.setWindowFlag(Qt.WindowStaysOnTopHint)

        self.student = student
        self.attributes = attributes

        # Widgets
        self.std_info = QLabel(f"{student.firstname} {student.lastname}")

        self.table_attributes = QTableView()
        self.table_attributes.setFixedWidth(300)
        self.table_attributes.horizontalHeader().setStretchLastSection(True)
        self.table_attributes.horizontalHeader().hide()
        self.table_attributes.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.table_attributes.setSelectionMode(
            QAbstractItemView.SingleSelection)
        self.table_attributes.verticalHeader().setSectionResizeMode(
            QHeaderView.Fixed)
        self.table_attributes.verticalHeader().setFixedWidth(150)

        self.dm: AttributesTableModel = None
        self.attr_idx = {
        }  # Attributes ids to indexes -> {attr_id: attr_idx, ...}

        # Close button
        self.ok_btn = QPushButton("Ok")
        self.ok_btn.clicked.connect(self.accept)
        self.ok_btn.setFixedSize(QSize(60, 33))

        # Signals
        self.sig_attribute_edition = sig_attribute_edition
        self.table_attributes.clicked.connect(self.on_attr_clicked)

        # Layout
        layout = QVBoxLayout()
        layout.addWidget(self.std_info)
        layout.setAlignment(self.std_info, Qt.AlignCenter)
        layout.addWidget(self.table_attributes)
        layout.addWidget(self.ok_btn)
        layout.setAlignment(self.ok_btn, Qt.AlignCenter)
        self.setLayout(layout)

        self.setStyleSheet(get_stylesheet("dialog"))

        # Initialization with the given attributes
        self.attributes_updated(self.attributes)

    def attributes_updated(self, attributes: list) -> None:
        """
        Called when an attribute changed and needs to be updated in the tableview

        :param attributes: List of attributes
        """
        self.attributes = attributes

        header = [a[1] for a in attributes]  # Table Header
        data_for_model = [a[2] for a in attributes]  # Table data

        # Save indexes
        self.attr_idx = {}
        i = 0
        for attr_id, _, _ in attributes:
            self.attr_idx[attr_id] = i
            i += 1

        # Create the table model
        self.dm = AttributesTableModel(self.table_attributes, data_for_model,
                                       header)

    def on_attr_clicked(self, item) -> None:
        """
        Triggered when a cell is clicked. Emits a signal with the attribute and student ids
        """
        attr_id = None
        for a in self.attr_idx:
            if self.attr_idx[a] == item.row():
                attr_id = a

        self.sig_attribute_edition.emit(attr_id, self.student.id)