Source code for qtpyvcp.widgets.input_widgets.offset_table

#   Copyright (c) 2018 Kurt Jacobson
#      <kurtcjacobson@gmail.com>
#
#   This file is part of QtPyVCP.
#
#   QtPyVCP is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 2 of the License, or
#   (at your option) any later version.
#
#   QtPyVCP is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with QtPyVCP.  If not, see <http://www.gnu.org/licenses/>.

import os

from PySide6.QtCore import Qt, Slot, Property, QModelIndex, QSortFilterProxyModel
from PySide6.QtGui import QStandardItemModel, QColor, QBrush
from PySide6.QtWidgets import (QTableView, QHeaderView, QStyledItemDelegate,
                               QDoubleSpinBox, QMessageBox, QMenu)

from qtpyvcp.utilities.logger import getLogger
from qtpyvcp.plugins import getPlugin
from qtpyvcp.utilities.settings import connectSetting, getSetting

IN_DESIGNER = os.getenv('DESIGNER', False)
if not IN_DESIGNER:
    STATUS = getPlugin('status')
LOG = getLogger(__name__)
IN_DESIGNER = os.getenv('DESIGNER', False)


[docs] class ItemDelegate(QStyledItemDelegate): def __init__(self, columns): super(ItemDelegate, self).__init__() self._columns = columns self._padding = ' ' * 2 def setColumns(self, columns): self._columns = columns def displayText(self, value, locale): if type(value) == float: return "{0:.4f}".format(value) return "{}{}".format(self._padding, value) def createEditor(self, parent, option, index): # ToDo: set dec placed for IN and MM machines col = self._columns[index.column()] if col in 'XYZABCUVWR': editor = QDoubleSpinBox(parent) editor.setFrame(False) editor.setAlignment(Qt.AlignmentFlag.AlignCenter) editor.setDecimals(4) # editor.setStepType(QSpinBox.AdaptiveDecimalStepType) editor.setProperty('stepType', 1) # stepType was added in 5.12 try: min_range = getSetting('offset_table.min_range').value max_range = getSetting('offset_table.max_range').value if min_range and max_range: editor.setRange(min_range, max_range) else: editor.setRange(-1000, 1000) except: # In designer mode or when settings aren't available editor.setRange(-1000, 1000) return editor return None
[docs] class OffsetModel(QStandardItemModel): # persistent_data_manager key for the visible-columns set -- one entry, # scoped by the plugin's own per-config-folder persistence file, so no # per-machine namespacing needed here. VISIBLE_COLUMNS_KEY = 'offset_table_visible_columns' def __init__(self, parent=None): super(OffsetModel, self).__init__(parent) self.ot = getPlugin('offsettable') self.current_row_color = QColor(Qt.darkGreen) self.current_row_bg = None # Add this line if IN_DESIGNER: # In designer mode, set up dummy data to show the table structure self._columns = [c for c in 'XYZABC'] # Default columns for designer self._visible_columns = list(self._columns) self._rows = list(range(1, 11)) # 10 rows for designer preview self.setColumnCount(len(self._columns)) self.setRowCount(len(self._rows)) # Set headers for i, col in enumerate(self._columns): self.setHeaderData(i, Qt.Horizontal, col, Qt.DisplayRole) for i, row in enumerate(self._rows): self.setHeaderData(i, Qt.Vertical, f"G5{i+1}", Qt.DisplayRole) return self._columns = self.ot.columns self._rows = self.ot.rows self._column_labels = self.ot.COLUMN_LABELS self._row_labels = self.ot.ROW_LABELS self._offset_table = self.ot.getOffsetTable() # Column *visibility* is a pure display-layer concern, independent # of self._columns above (the full set the ini's OFFSET_COLUMNS # configures, and the positional index space the underlying # offset_table array/save-to-LinuxCNC path is keyed on -- see # data()/setData()/saveOffsetTable() below, none of which are # affected by which columns are currently visible). Persisted # per config folder (same mechanism .vcp_persistent_data.pickle # already uses), not per axis-letter default -- a fresh/never- # toggled config shows every configured column, same as before # this feature existed. self._data_manager = getPlugin('persistent_data_manager') persisted = self._filterToKnownColumns( self._data_manager.getData(self.VISIBLE_COLUMNS_KEY, None)) self._visible_columns = persisted or list(self._columns) self.setColumnCount(len(self._columns)) self.setRowCount(len(self._rows)) # (self.rowCount()) self.ot.offset_table_changed.connect(self.updateModel) def refreshModel(self): # refresh model so current row gets highlighted self.beginResetModel() self.endResetModel() def updateModel(self, offset_table): # update model with new data if len(offset_table) == 0: LOG.debug("Offset Table update is zero length - skip it") return self.beginResetModel() self._offset_table = offset_table self.endResetModel() def setColumns(self, columns): self._columns = columns self.setColumnCount(len(columns)) # ------------------------------------------------------ visibility
[docs] def allColumns(self): """Every column this machine is configured for (OFFSET_COLUMNS), regardless of current visibility -- the toggle menu's universe.""" return list(self._columns)
def visibleColumns(self): return list(self._visible_columns) def _filterToKnownColumns(self, columns): """Drop any letter not in self._columns (e.g. a persisted set from before OFFSET_COLUMNS changed) -- never surface a column the underlying offset_table array has no slot for. Also normalizes to self._columns' own canonical order regardless of what order `columns` arrives in, so a column that gets hidden and re-shown always lands back in its original position instead of at the end of whatever order it was toggled in (setVisibleColumns() callers naturally build the list in toggle order, not display order). Returns None (not an empty list) if nothing valid remains, so callers can fall back to a sane default instead of rendering zero columns.""" if not columns: return None wanted = set(columns) ordered = [c for c in self._columns if c in wanted] return ordered or None def setVisibleColumns(self, columns): visible = self._filterToKnownColumns(columns) or list(self._columns) self.beginResetModel() self._visible_columns = visible self.endResetModel() if not IN_DESIGNER: self._data_manager.setData(self.VISIBLE_COLUMNS_KEY, visible) def _realColumnIndex(self, visible_col): """Map a QTableView column position (an index into self._visible_columns) to its real position in self._columns -- the fixed index space self._offset_table rows and the plugin's column.index(letter) lookups (loadOffsetTable/saveOffsetTable) are keyed on. Hiding a column never changes that mapping, only which positions the view exposes -- see data()/setData() below.""" return self._columns.index(self._visible_columns[visible_col]) # --------------------------------------------------------------- def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole): if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: return self._visible_columns[section] elif role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Vertical: return self._row_labels[section] return QStandardItemModel.headerData(self, section, orientation, role) def columnCount(self, parent=None): if IN_DESIGNER: return 0 return len(self._visible_columns) def rowCount(self, parent=None): if IN_DESIGNER: return 0 return len(self._rows) def flags(self, index): return Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEditable def data(self, index, role=Qt.ItemDataRole.DisplayRole): if (role == Qt.ItemDataRole.DisplayRole or role == Qt.ItemDataRole.EditRole) and len(self._offset_table) > 0: columns_index = self._realColumnIndex(index.column()) rows_index = index.row() # column_index = self._columns[index.column()] # index_column = self._column_labels.index(column_index) return self._offset_table[rows_index][columns_index] elif role == Qt.ItemDataRole.TextAlignmentRole: return Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignRight elif role == Qt.ItemDataRole.ForegroundRole: offset = index.row() + 1 if self.ot.current_index == offset: return QBrush(self.current_row_color) else: return QStandardItemModel.data(self, index, role) elif role == Qt.ItemDataRole.BackgroundRole and self.current_row_bg is not None: # Add this block offset = index.row() + 1 if self.ot.current_index == offset: return QBrush(self.current_row_bg) else: return QStandardItemModel.data(self, index, role) return QStandardItemModel.data(self, index, role) def setData(self, index, value, role): columns_index = self._realColumnIndex(index.column()) rows_index = index.row() # column_index = self._columns[index.column()] # index_column = self._column_labels.index(column_index) self._offset_table[rows_index][columns_index] = value return True def clearRow(self, row): for col in range(len(self._columns)): # index_column = self._column_labels.index(self._columns[col]) self._offset_table[row][col] = 0.0 self.refreshModel() def clearRows(self): for row in range(len(self._rows)): for col in range(len(self._columns)): # index_column = self._column_labels.index(self._columns[col]) self._offset_table[row][col] = 0.0 self.refreshModel() def offsetDataFromRow(self, row): o_num = sorted(self._offset_table)[row] return self._offset_table[o_num] def saveOffsetTable(self): self.ot.saveOffsetTable(self._offset_table, columns=self._columns) return True def loadOffsetTable(self): # the tooltable plugin will emit the tool_table_changed signal # so we don't need to do any more here self.ot.loadOffsetTable() return True
[docs] class OffsetTable(QTableView): def __init__(self, parent=None): super(OffsetTable, self).__init__(parent) self.setEnabled(False) self.offset_model = OffsetModel(self) # Properties self._columns = [c for c in 'XYZABCUVWR'] # Default columns self._current_row_color = QColor('sage') self._current_row_bg = None # Add this line self._confirm_actions = False # Initialize confirm_actions property self.proxy_model = QSortFilterProxyModel() self.proxy_model.setFilterKeyColumn(0) self.proxy_model.setSourceModel(self.offset_model) if not IN_DESIGNER: self.item_delegate = ItemDelegate(columns=self._columns) self.setItemDelegate(self.item_delegate) self.setModel(self.proxy_model) if not IN_DESIGNER: # keep highlight/selection in sync with active work offset self.offset_model.ot.active_offset_changed.connect(self._onActiveOffsetChanged) # initial selection self._onActiveOffsetChanged(self.offset_model.ot.current_index) # Appearance/Behaviour settings self.setSortingEnabled(False) self.setAlternatingRowColors(True) self.setSelectionBehavior(QTableView.SelectRows) self.setSelectionMode(QTableView.SingleSelection) self.horizontalHeader().setStretchLastSection(False) self.horizontalHeader().setSortIndicator(0, Qt.AscendingOrder) self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) if not IN_DESIGNER: # per-column visibility menu, same UX as the tool table's header = self.horizontalHeader() header.setContextMenuPolicy(Qt.CustomContextMenu) header.customContextMenuRequested.connect(self._onHeaderContextMenu) if IN_DESIGNER: return STATUS.all_axes_homed.notify(self.handle_home_signal) def handle_home_signal(self, all_axes): if all_axes: self.setEnabled(True) else: self.setEnabled(False) # ------------------------------------------------------- column menu def _onHeaderContextMenu(self, position): header = self.horizontalHeader() menu = QMenu(self) show_all = menu.addAction('Show All Columns') menu.addSeparator() visible = set(self.offset_model.visibleColumns()) toggles = {} for key in self.offset_model.allColumns(): action = menu.addAction(key) action.setCheckable(True) action.setChecked(key in visible) toggles[action] = key selected = menu.exec(header.mapToGlobal(position)) if selected is None: return if selected == show_all: self.offset_model.setVisibleColumns(self.offset_model.allColumns()) elif selected in toggles: key = toggles[selected] cols = self.offset_model.visibleColumns() if selected.isChecked(): if key not in cols: cols.append(key) else: cols = [c for c in cols if c != key] if not cols: QMessageBox.warning(self, 'Offset Table', 'At least one column must remain visible.') return self.offset_model.setVisibleColumns(cols) @Slot(int) def _onActiveOffsetChanged(self, offset_num): # offset_num is 1-based (G54->1). Update row highlight and selection. self.offset_model.refreshModel() row = max(0, offset_num - 1) self.selectRow(row) @Slot() def saveOffsetTable(self): if self.isEnabled(): if not self.confirmAction("Do you want to save changes and\n" "load offset table into LinuxCNC?"): return self.offset_model.saveOffsetTable() @Slot() def loadOffsetTable(self): if not self.confirmAction("Do you want to re-load the offset table?\n" "All unsaved changes will be lost."): return self.offset_model.loadOffsetTable()
[docs] @Slot() def deleteSelectedOffset(self): """Delete the currently selected item""" current_row = self.selectedRow() if current_row == -1: # no row selected return if not self.confirmAction("Are you sure you want to delete offset {}?".format(current_row)): return self.offset_model.clearRow(current_row)
# @Slot() # def selectPrevious(self): # """Select the previous item in the view.""" # self.selectRow(self.selectedRow() - 1) # return True # @Slot() # def selectNext(self): # """Select the next item in the view.""" # self.selectRow(self.selectedRow() + 1) # return True
[docs] @Slot() def clearOffsetTable(self, confirm=True): """Remove all items from the model""" if confirm: if not self.confirmAction("Do you want to delete the whole offsets table?"): return self.offset_model.clearRows()
[docs] def selectedRow(self): """Returns the row number of the currently selected row, or 0""" return self.selectionModel().currentIndex().row()
def confirmAction(self, message): if not self._confirm_actions: return True box = QMessageBox.question(self, 'Confirm Action', message, QMessageBox.StandardButton.Yes, QMessageBox.StandardButton.No) if box == QMessageBox.StandardButton.Yes: return True else: return False @Property(int) def currentRow(self): return self.selectedRow() @currentRow.setter def currentRow(self, row): self.selectRow(row) @Property(bool) def confirmActions(self): return self._confirm_actions @confirmActions.setter def confirmActions(self, confirm): self._confirm_actions = confirm @Property(QColor) def currentRowColor(self): return self.offset_model.current_row_color @currentRowColor.setter def currentRowColor(self, color): self.offset_model.current_row_color = color @Property(QColor) def currentRowBackground(self): return self.offset_model.current_row_bg or QColor() @currentRowBackground.setter def currentRowBackground(self, color): self.offset_model.current_row_bg = color