You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
480 lines
17 KiB
480 lines
17 KiB
#!/usr/bin/env python3
|
|
"""
|
|
Command Table Widget - Reusable
|
|
================================
|
|
Auto-detecting command table with CRUD operations.
|
|
|
|
Features:
|
|
- Auto-detect columns from database
|
|
- Search + filter
|
|
- Add/Edit/Delete commands
|
|
- Gray out when disconnected
|
|
- Double-click to execute
|
|
- Works for both UART and I2C
|
|
|
|
Author: Kynsight
|
|
Version: 1.0.0
|
|
"""
|
|
|
|
from PyQt6.QtWidgets import (
|
|
QWidget, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem,
|
|
QPushButton, QLineEdit, QLabel, QComboBox, QHeaderView,
|
|
QAbstractItemView, QDialog, QFormLayout, QDialogButtonBox,
|
|
QMessageBox, QTextEdit
|
|
)
|
|
from PyQt6.QtCore import Qt, pyqtSignal
|
|
from PyQt6.QtGui import QColor
|
|
|
|
|
|
class CommandDialog(QDialog):
|
|
"""Dialog for adding/editing commands."""
|
|
|
|
def __init__(self, command_type, all_columns, values=None, parent=None):
|
|
super().__init__(parent)
|
|
|
|
self.command_type = command_type
|
|
self.all_columns = all_columns
|
|
self.fields = {}
|
|
|
|
self.setWindowTitle("Add Command" if values is None else "Edit Command")
|
|
self.setModal(True)
|
|
|
|
layout = QVBoxLayout()
|
|
self.setLayout(layout)
|
|
|
|
# Form
|
|
form_layout = QFormLayout()
|
|
|
|
for col in all_columns:
|
|
if col == 'command_id':
|
|
continue # Skip ID
|
|
|
|
# Create appropriate widget
|
|
if col == 'description':
|
|
widget = QTextEdit()
|
|
widget.setMaximumHeight(80)
|
|
if values and col in values:
|
|
widget.setPlainText(str(values[col]))
|
|
else:
|
|
widget = QLineEdit()
|
|
if values and col in values:
|
|
widget.setText(str(values[col]))
|
|
|
|
self.fields[col] = widget
|
|
form_layout.addRow(col.replace('_', ' ').title() + ":", widget)
|
|
|
|
layout.addLayout(form_layout)
|
|
|
|
# Buttons
|
|
button_box = QDialogButtonBox(
|
|
QDialogButtonBox.StandardButton.Ok |
|
|
QDialogButtonBox.StandardButton.Cancel
|
|
)
|
|
button_box.accepted.connect(self.accept)
|
|
button_box.rejected.connect(self.reject)
|
|
layout.addWidget(button_box)
|
|
|
|
def get_values(self):
|
|
"""Get values from form."""
|
|
values = {}
|
|
for col, widget in self.fields.items():
|
|
if isinstance(widget, QTextEdit):
|
|
values[col] = widget.toPlainText()
|
|
else:
|
|
values[col] = widget.text()
|
|
return values
|
|
|
|
|
|
class CommandTableWidget(QWidget):
|
|
"""
|
|
Reusable command table with auto-detection.
|
|
|
|
Signals:
|
|
command_selected: (command_id, command_data)
|
|
command_double_clicked: (command_id, command_data)
|
|
"""
|
|
|
|
command_selected = pyqtSignal(int, dict) # command_id, all column data
|
|
command_double_clicked = pyqtSignal(int, dict)
|
|
|
|
def __init__(self, db_connection, command_type='uart', visible_columns=None, parent=None):
|
|
"""
|
|
Args:
|
|
db_connection: SQLite connection
|
|
command_type: 'uart' or 'i2c'
|
|
visible_columns: List of columns to display (None = all)
|
|
"""
|
|
super().__init__(parent)
|
|
|
|
self.conn = db_connection
|
|
self.command_type = command_type
|
|
self.table_name = f"{command_type}_commands"
|
|
|
|
self.all_commands = []
|
|
self.all_columns = [] # All columns from DB
|
|
self.visible_columns = visible_columns # Columns to show
|
|
self.filter_column = None # For category/operation filter
|
|
|
|
self._init_ui()
|
|
self._detect_columns()
|
|
self._load_commands()
|
|
|
|
def _init_ui(self):
|
|
"""Initialize UI."""
|
|
layout = QVBoxLayout()
|
|
self.setLayout(layout)
|
|
|
|
# Search and filter
|
|
filter_layout = QHBoxLayout()
|
|
|
|
self.txt_search = QLineEdit()
|
|
self.txt_search.setPlaceholderText("Search commands...")
|
|
self.txt_search.textChanged.connect(self._filter_table)
|
|
filter_layout.addWidget(self.txt_search, 2)
|
|
|
|
self.combo_filter = QComboBox()
|
|
self.combo_filter.currentTextChanged.connect(self._filter_table)
|
|
filter_layout.addWidget(self.combo_filter, 1)
|
|
|
|
layout.addLayout(filter_layout)
|
|
|
|
# Table
|
|
self.table = QTableWidget()
|
|
self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
|
self.table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
|
self.table.setAlternatingRowColors(True)
|
|
self.table.verticalHeader().setVisible(False)
|
|
self.table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
|
|
|
self.table.itemSelectionChanged.connect(self._on_selection_changed)
|
|
self.table.cellDoubleClicked.connect(self._on_double_click)
|
|
|
|
layout.addWidget(self.table)
|
|
|
|
# CRUD buttons
|
|
btn_layout = QHBoxLayout()
|
|
|
|
self.btn_add = QPushButton("+ Add")
|
|
self.btn_add.clicked.connect(self._add_command)
|
|
btn_layout.addWidget(self.btn_add)
|
|
|
|
self.btn_edit = QPushButton("✎ Edit")
|
|
self.btn_edit.setEnabled(False)
|
|
self.btn_edit.clicked.connect(self._edit_command)
|
|
btn_layout.addWidget(self.btn_edit)
|
|
|
|
self.btn_delete = QPushButton("🗑 Delete")
|
|
self.btn_delete.setEnabled(False)
|
|
self.btn_delete.clicked.connect(self._delete_command)
|
|
btn_layout.addWidget(self.btn_delete)
|
|
|
|
btn_layout.addStretch()
|
|
|
|
self.lbl_count = QLabel()
|
|
btn_layout.addWidget(self.lbl_count)
|
|
|
|
layout.addLayout(btn_layout)
|
|
|
|
def _detect_columns(self):
|
|
"""Auto-detect columns from database."""
|
|
cursor = self.conn.execute(f"PRAGMA table_info({self.table_name})")
|
|
self.all_columns = [row[1] for row in cursor.fetchall()]
|
|
|
|
# If no visible_columns specified, show all
|
|
if self.visible_columns is None:
|
|
self.visible_columns = self.all_columns
|
|
|
|
# Setup table columns (only visible ones)
|
|
self.table.setColumnCount(len(self.visible_columns))
|
|
self.table.setHorizontalHeaderLabels([
|
|
col.replace('_', ' ').title() for col in self.visible_columns
|
|
])
|
|
|
|
# Make ALL columns user-resizable (Interactive mode)
|
|
header = self.table.horizontalHeader()
|
|
for i in range(len(self.visible_columns)):
|
|
header.setSectionResizeMode(i, QHeaderView.ResizeMode.Interactive)
|
|
|
|
# Detect filter column (category for UART, operation for I2C)
|
|
if 'category' in self.all_columns:
|
|
self.filter_column = 'category'
|
|
self.combo_filter.addItem("All Categories")
|
|
elif 'operation' in self.all_columns:
|
|
self.filter_column = 'operation'
|
|
self.combo_filter.addItem("All Operations")
|
|
else:
|
|
self.combo_filter.setVisible(False)
|
|
|
|
def _load_commands(self):
|
|
"""Load commands from database."""
|
|
cursor = self.conn.execute(f"""
|
|
SELECT * FROM {self.table_name}
|
|
WHERE is_active = 1
|
|
ORDER BY {self.all_columns[1]}
|
|
""")
|
|
|
|
self.all_commands = []
|
|
for row in cursor.fetchall():
|
|
cmd_dict = {self.all_columns[i]: row[i] for i in range(len(self.all_columns))}
|
|
self.all_commands.append(cmd_dict)
|
|
|
|
# Populate filter dropdown
|
|
if self.filter_column:
|
|
values = sorted(set(cmd[self.filter_column] for cmd in self.all_commands
|
|
if cmd[self.filter_column]))
|
|
self.combo_filter.clear()
|
|
self.combo_filter.addItem(f"All {self.filter_column.title()}s")
|
|
self.combo_filter.addItems(values)
|
|
|
|
self._display_commands(self.all_commands)
|
|
|
|
def _display_commands(self, commands):
|
|
"""Display commands in table (only visible columns)."""
|
|
self.table.setRowCount(len(commands))
|
|
|
|
for row_idx, cmd in enumerate(commands):
|
|
for col_idx, col_name in enumerate(self.visible_columns):
|
|
value = cmd.get(col_name, '')
|
|
|
|
item = QTableWidgetItem(str(value) if value is not None else '')
|
|
|
|
# Center-align ID
|
|
if col_name == 'command_id':
|
|
item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
|
|
self.table.setItem(row_idx, col_idx, item)
|
|
|
|
# Color by category/operation (dark theme compatible)
|
|
color = self._get_row_color(cmd)
|
|
if color:
|
|
item.setBackground(color)
|
|
|
|
self.lbl_count.setText(f"Count: {len(commands)}")
|
|
|
|
def _get_row_color(self, cmd):
|
|
"""Get row color based on category/operation (dark theme compatible)."""
|
|
if self.command_type == 'uart':
|
|
colors = {
|
|
"Door": QColor(70, 130, 180), # Steel blue
|
|
"Action": QColor(255, 165, 0), # Orange
|
|
"Error": QColor(220, 20, 60), # Crimson
|
|
"Status": QColor(60, 179, 113), # Medium sea green
|
|
"Motor": QColor(218, 165, 32), # Goldenrod
|
|
"Sensor": QColor(147, 112, 219), # Medium purple
|
|
"Power": QColor(138, 43, 226), # Blue violet
|
|
"Info": QColor(100, 149, 237), # Cornflower blue
|
|
}
|
|
return colors.get(cmd.get('category'))
|
|
else: # i2c
|
|
operation = cmd.get('operation')
|
|
if operation == 'read':
|
|
return QColor(60, 179, 113) # Medium sea green
|
|
elif operation == 'write':
|
|
return QColor(255, 140, 0) # Dark orange
|
|
return None
|
|
|
|
def _filter_table(self):
|
|
"""Filter table by search and dropdown."""
|
|
search_text = self.txt_search.text().lower()
|
|
filter_value = self.combo_filter.currentText()
|
|
|
|
# Filter commands
|
|
filtered = []
|
|
for cmd in self.all_commands:
|
|
# Filter by dropdown
|
|
if self.filter_column and not filter_value.startswith("All"):
|
|
if cmd.get(self.filter_column) != filter_value:
|
|
continue
|
|
|
|
# Filter by search text
|
|
if search_text:
|
|
found = False
|
|
for col_name, value in cmd.items():
|
|
if search_text in str(value).lower():
|
|
found = True
|
|
break
|
|
if not found:
|
|
continue
|
|
|
|
filtered.append(cmd)
|
|
|
|
self._display_commands(filtered)
|
|
|
|
def _on_selection_changed(self):
|
|
"""Handle selection change."""
|
|
selected = self.table.selectedItems()
|
|
has_selection = bool(selected)
|
|
|
|
# Only auto-enable edit/delete if they're not externally disabled
|
|
# (i.e., if Add button is enabled, we're in edit mode)
|
|
if self.btn_add.isEnabled():
|
|
self.btn_edit.setEnabled(has_selection)
|
|
self.btn_delete.setEnabled(has_selection)
|
|
|
|
if has_selection:
|
|
row = selected[0].row()
|
|
|
|
# Get command_id from visible columns
|
|
cmd_id_col_idx = self.visible_columns.index('command_id') if 'command_id' in self.visible_columns else 0
|
|
command_id = int(self.table.item(row, cmd_id_col_idx).text())
|
|
|
|
# Get ALL column data (from all_commands, not just visible)
|
|
cmd_data = next((cmd for cmd in self.all_commands
|
|
if cmd['command_id'] == command_id), {})
|
|
|
|
# Emit signal
|
|
self.command_selected.emit(command_id, cmd_data)
|
|
|
|
def _on_double_click(self, row, col):
|
|
"""Handle double-click."""
|
|
# Get command_id from visible columns
|
|
cmd_id_col_idx = self.visible_columns.index('command_id') if 'command_id' in self.visible_columns else 0
|
|
command_id = int(self.table.item(row, cmd_id_col_idx).text())
|
|
|
|
# Get ALL column data
|
|
cmd_data = next((cmd for cmd in self.all_commands
|
|
if cmd['command_id'] == command_id), {})
|
|
|
|
# Emit signal
|
|
self.command_double_clicked.emit(command_id, cmd_data)
|
|
|
|
def _add_command(self):
|
|
"""Add new command."""
|
|
dialog = CommandDialog(self.command_type, self.all_columns, parent=self)
|
|
|
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
values = dialog.get_values()
|
|
|
|
# Build INSERT query
|
|
cols = [col for col in self.all_columns if col != 'command_id']
|
|
placeholders = ', '.join(['?'] * len(cols))
|
|
col_names = ', '.join(cols)
|
|
|
|
try:
|
|
self.conn.execute(f"""
|
|
INSERT INTO {self.table_name} ({col_names})
|
|
VALUES ({placeholders})
|
|
""", [values.get(col, '') for col in cols])
|
|
|
|
self.conn.commit()
|
|
self._load_commands()
|
|
|
|
QMessageBox.information(self, "Success", "Command added successfully!")
|
|
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error", f"Failed to add command:\n{e}")
|
|
|
|
def _edit_command(self):
|
|
"""Edit selected command."""
|
|
selected = self.table.selectedItems()
|
|
if not selected:
|
|
return
|
|
|
|
row = selected[0].row()
|
|
|
|
# Get command_id from visible columns
|
|
cmd_id_col_idx = self.visible_columns.index('command_id') if 'command_id' in self.visible_columns else 0
|
|
command_id = int(self.table.item(row, cmd_id_col_idx).text())
|
|
|
|
# Get current values (all columns)
|
|
current_values = next((cmd for cmd in self.all_commands
|
|
if cmd['command_id'] == command_id), {})
|
|
|
|
dialog = CommandDialog(self.command_type, self.all_columns,
|
|
current_values, parent=self)
|
|
|
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
values = dialog.get_values()
|
|
|
|
# Build UPDATE query
|
|
set_clause = ', '.join([f"{col} = ?" for col in values.keys()])
|
|
|
|
try:
|
|
self.conn.execute(f"""
|
|
UPDATE {self.table_name}
|
|
SET {set_clause}
|
|
WHERE command_id = ?
|
|
""", list(values.values()) + [command_id])
|
|
|
|
self.conn.commit()
|
|
self._load_commands()
|
|
|
|
QMessageBox.information(self, "Success", "Command updated successfully!")
|
|
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error", f"Failed to update command:\n{e}")
|
|
|
|
def _delete_command(self):
|
|
"""Delete selected command."""
|
|
selected = self.table.selectedItems()
|
|
if not selected:
|
|
return
|
|
|
|
row = selected[0].row()
|
|
|
|
# Get command_id and name
|
|
cmd_id_col_idx = self.visible_columns.index('command_id') if 'command_id' in self.visible_columns else 0
|
|
cmd_name_col_idx = self.visible_columns.index('command_name') if 'command_name' in self.visible_columns else 1
|
|
|
|
command_id = int(self.table.item(row, cmd_id_col_idx).text())
|
|
name = self.table.item(row, cmd_name_col_idx).text()
|
|
|
|
reply = QMessageBox.question(
|
|
self,
|
|
"Delete Command",
|
|
f"Delete command '{name}'?\n\nThis will mark it as inactive.",
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
|
)
|
|
|
|
if reply == QMessageBox.StandardButton.Yes:
|
|
try:
|
|
# Soft delete (mark as inactive)
|
|
self.conn.execute(f"""
|
|
UPDATE {self.table_name}
|
|
SET is_active = 0
|
|
WHERE command_id = ?
|
|
""", (command_id,))
|
|
|
|
self.conn.commit()
|
|
self._load_commands()
|
|
|
|
QMessageBox.information(self, "Success", "Command deleted successfully!")
|
|
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error", f"Failed to delete command:\n{e}")
|
|
|
|
def set_enabled_state(self, enabled: bool):
|
|
"""Enable/disable table (gray out when disconnected)."""
|
|
self.table.setEnabled(enabled)
|
|
self.txt_search.setEnabled(enabled)
|
|
self.combo_filter.setEnabled(enabled)
|
|
# Keep CRUD buttons always enabled
|
|
|
|
def get_selected_command_id(self):
|
|
"""Get selected command ID."""
|
|
selected = self.table.selectedItems()
|
|
if selected:
|
|
return int(self.table.item(selected[0].row(), 0).text())
|
|
return None
|
|
|
|
def refresh(self):
|
|
"""Reload commands from database."""
|
|
self._load_commands()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
from PyQt6.QtWidgets import QApplication
|
|
import sqlite3
|
|
|
|
app = QApplication(sys.argv)
|
|
|
|
conn = sqlite3.connect("./database/ehinge.db")
|
|
|
|
widget = CommandTableWidget(conn, 'uart')
|
|
widget.setWindowTitle("Command Table - UART")
|
|
widget.resize(900, 600)
|
|
widget.show()
|
|
|
|
sys.exit(app.exec())
|