Initial Commit
This commit is contained in:
commit
4dbdc7d793
18 changed files with 2384 additions and 0 deletions
34
pyqt6_scaffold/__init__.py
Normal file
34
pyqt6_scaffold/__init__.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
from .core.composer import Composer
|
||||
from .core.logging import setup_logger
|
||||
from .core.database import CursorContext, AbstractDatabase
|
||||
from .core.windows import BaseWindow
|
||||
from .core.objects import (
|
||||
BaseUser,
|
||||
NavigationContext,
|
||||
NavigateRequest
|
||||
)
|
||||
from .core.models import (
|
||||
DataMixin,
|
||||
BaseTableModel,
|
||||
BaseListModel,
|
||||
BaseCardModel
|
||||
)
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
||||
__all__ = [
|
||||
"BaseUser",
|
||||
"NavigationContext",
|
||||
"NavigateRequest",
|
||||
"DataMixin",
|
||||
"BaseTableModel",
|
||||
"BaseListModel",
|
||||
"BaseCardModel",
|
||||
"BaseWindow",
|
||||
"AbstractDatabase",
|
||||
"CursorContext",
|
||||
"setup_logger",
|
||||
"Composer"
|
||||
]
|
||||
15
pyqt6_scaffold/contrib/__init__.py
Normal file
15
pyqt6_scaffold/contrib/__init__.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
from .backends import (
|
||||
MysqlDatabase,
|
||||
SqliteDatabase,
|
||||
PostgresqlDatabase,
|
||||
)
|
||||
from . import auth
|
||||
|
||||
__all__ = [
|
||||
"auth",
|
||||
"MysqlDatabase",
|
||||
"SqliteDatabase",
|
||||
"PostgresqlDatabase"
|
||||
]
|
||||
10
pyqt6_scaffold/contrib/auth/__init__.py
Normal file
10
pyqt6_scaffold/contrib/auth/__init__.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
from .role import Role, RoleLevel
|
||||
from .database import RBACMixin
|
||||
|
||||
__all__ = [
|
||||
"Role",
|
||||
"RoleLevel",
|
||||
"RBACMixin"
|
||||
]
|
||||
45
pyqt6_scaffold/contrib/auth/database.py
Normal file
45
pyqt6_scaffold/contrib/auth/database.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
from pyqt6_scaffold.core.objects import BaseUser
|
||||
|
||||
class RBACMixin:
|
||||
"""
|
||||
Role-Based Access Control mixin for AbstractDatabase subclasses.
|
||||
|
||||
Provides a can() method that checks user permissions against
|
||||
a permission table in the database. Table and column names
|
||||
are configurable as class attributes.
|
||||
|
||||
Expected table schema:
|
||||
permission_table (permission_column, level_column)
|
||||
|
||||
Example:
|
||||
permission_map (perm VARCHAR, min_level INT)
|
||||
"""
|
||||
permission_table: str = "permission_map"
|
||||
permission_column: str = "perm"
|
||||
level_column: str = "min_level"
|
||||
|
||||
def can(self, user: BaseUser, permission: str) -> bool:
|
||||
"""
|
||||
Check whether a user has the required permission level.
|
||||
|
||||
Args:
|
||||
user: A BaseUser instance with a role.level attribute.
|
||||
permission: Permission identifier to look up in the database.
|
||||
|
||||
Returns:
|
||||
True if user.role.level >= required level, False otherwise.
|
||||
"""
|
||||
with self.execute(
|
||||
f"""
|
||||
SELECT {self.level_column}
|
||||
FROM {self.permission_table}
|
||||
WHERE {self.permission_column} = {self.placeholder}
|
||||
""",
|
||||
(permission,)
|
||||
) as cursor:
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
return False
|
||||
return user.role.level >= row[0]
|
||||
29
pyqt6_scaffold/contrib/auth/role.py
Normal file
29
pyqt6_scaffold/contrib/auth/role.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class Role:
|
||||
"""
|
||||
Represents a user role with a hierarchical access level.
|
||||
|
||||
Attributes:
|
||||
name: Human-readable role name.
|
||||
level: Numeric access level used for permission checks.
|
||||
"""
|
||||
name: str
|
||||
level: int
|
||||
|
||||
class RoleLevel(Enum):
|
||||
"""
|
||||
Predefined access levels for common roles.
|
||||
|
||||
Use these constants when populating the permission table
|
||||
or comparing role levels in application logic.
|
||||
"""
|
||||
GUEST = 0
|
||||
CLIENT = 25
|
||||
EMPLOYEE = 50
|
||||
MANAGER = 75
|
||||
ADMIN = 100
|
||||
87
pyqt6_scaffold/contrib/backends.py
Normal file
87
pyqt6_scaffold/contrib/backends.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
import os
|
||||
from pyqt6_scaffold.core.logging import setup_logger
|
||||
from pyqt6_scaffold.core.database import AbstractDatabase
|
||||
|
||||
log = setup_logger(__name__)
|
||||
|
||||
class PostgresqlDatabase(AbstractDatabase):
|
||||
"""
|
||||
AbstractDatabase implementation for PostgreSQL via psycopg2.
|
||||
|
||||
Configuration is read from environment variables:
|
||||
PG_HOST, PG_PORT, PG_USER, PG_DATABASE, PG_PASSWORD
|
||||
|
||||
Requires:
|
||||
pip install pyqt6-scaffold[postgres]
|
||||
"""
|
||||
@property
|
||||
def placeholder(self) -> str:
|
||||
return "%s"
|
||||
|
||||
def _connect(self):
|
||||
try:
|
||||
import psycopg2 as pg
|
||||
except ImportError:
|
||||
log.error("Ошибка импорта psycopg2")
|
||||
raise
|
||||
|
||||
DB_CONFIG = {
|
||||
"host": os.getenv("PG_HOST", "127.0.0.1"),
|
||||
"port": int(os.getenv("PG_PORT", 5432)),
|
||||
"user": os.getenv("PG_USER", "postgres"),
|
||||
"database": os.getenv("PG_DATABASE", "postgres"),
|
||||
"password": os.getenv("PG_PASSWORD", "postgres")
|
||||
}
|
||||
|
||||
return pg.connect(**DB_CONFIG)
|
||||
|
||||
class MysqlDatabase(AbstractDatabase):
|
||||
"""
|
||||
AbstractDatabase implementation for MySQL via pymysql.
|
||||
|
||||
Configuration is read from environment variables:
|
||||
MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_DATABASE, MYSQL_PASSWORD
|
||||
|
||||
Requires:
|
||||
pip install pyqt6-scaffold[mysql]
|
||||
"""
|
||||
@property
|
||||
def placeholder(self) -> str:
|
||||
return "%s"
|
||||
|
||||
def _connect(self):
|
||||
try:
|
||||
import pymysql as pms
|
||||
except ImportError:
|
||||
log.error("Ошибка импорта pymysql")
|
||||
raise
|
||||
|
||||
DB_CONFIG = {
|
||||
"host": os.getenv("MYSQL_HOST", "localhost"),
|
||||
"port": int(os.getenv("MYSQL_PORT", 3306)),
|
||||
"user": os.getenv("MYSQL_USER", "root"),
|
||||
"database": os.getenv("MYSQL_DATABASE", "root"),
|
||||
"password": os.getenv("MYSQL_PASSWORD", "root")
|
||||
}
|
||||
|
||||
return pms.connect(**DB_CONFIG)
|
||||
|
||||
class SqliteDatabase(AbstractDatabase):
|
||||
"""
|
||||
AbstractDatabase implementation for SQLite via the stdlib sqlite3 module.
|
||||
|
||||
Configuration is read from environment variables:
|
||||
SQLITE_PATH (default: app.db)
|
||||
|
||||
No additional dependencies required.
|
||||
"""
|
||||
@property
|
||||
def placeholder(self) -> str:
|
||||
return "?"
|
||||
|
||||
def _connect(self):
|
||||
import sqlite3
|
||||
path = os.getenv("SQLITE_PATH", "app.db")
|
||||
return sqlite3.connect(path)
|
||||
1
pyqt6_scaffold/core/__init__.py
Normal file
1
pyqt6_scaffold/core/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
127
pyqt6_scaffold/core/composer.py
Normal file
127
pyqt6_scaffold/core/composer.py
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
# SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
from .objects import NavigateRequest
|
||||
from .windows import BaseWindow
|
||||
from .logging import setup_logger
|
||||
from .database import AbstractDatabase
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot
|
||||
from typing import Dict
|
||||
|
||||
log = setup_logger(__name__)
|
||||
|
||||
class Composer(QObject):
|
||||
"""
|
||||
Application router and window lifecycle manager.
|
||||
|
||||
Manages window registration, navigation between windows,
|
||||
and holds the active database connection. Acts as the
|
||||
central hub of the application.
|
||||
|
||||
Args:
|
||||
app: QApplication instance.
|
||||
db: Connected AbstractDatabase instance.
|
||||
"""
|
||||
navigate_request = pyqtSignal(NavigateRequest)
|
||||
|
||||
def __init__(self, app: QApplication, db: AbstractDatabase):
|
||||
super().__init__()
|
||||
self._db = db
|
||||
self._current = None
|
||||
self._app = app
|
||||
self._registry: Dict[str, Dict[str, object]] = dict()
|
||||
self._current_ctx: Dict[str, Dict[str, object]] = dict()
|
||||
|
||||
self.navigate_request.connect(self.navigate)
|
||||
|
||||
@property
|
||||
def context(self):
|
||||
"""
|
||||
Return the current navigation context.
|
||||
"""
|
||||
return self._current_ctx
|
||||
|
||||
def register(self, name: str,
|
||||
window: type[BaseWindow],
|
||||
lazy: bool = True):
|
||||
"""
|
||||
Register a window class under a given name.
|
||||
|
||||
Args:
|
||||
name: Unique string identifier for the window.
|
||||
window: A subclass of BaseWindow.
|
||||
lazy: If True, the window is instantiated on first navigation.
|
||||
If False, the window is instantiated immediately.
|
||||
|
||||
Raises:
|
||||
TypeError: If window is not a subclass of BaseWindow.
|
||||
"""
|
||||
if not issubclass(window, BaseWindow):
|
||||
raise TypeError(f"{window} is not a descendant of BaseWindow")
|
||||
|
||||
entry = {
|
||||
"class": window,
|
||||
"instance": None,
|
||||
"lazy": lazy
|
||||
}
|
||||
|
||||
if not lazy:
|
||||
entry["instance"] = window(self, self._db)
|
||||
|
||||
self._registry[name] = entry
|
||||
|
||||
def _switch_window(self, window):
|
||||
"""
|
||||
Close the current window and show the new one.
|
||||
"""
|
||||
if self._current:
|
||||
self._current.close()
|
||||
self._current = window
|
||||
self._current.show()
|
||||
|
||||
def _create_window(self, name: str) -> BaseWindow:
|
||||
"""
|
||||
Instantiate or retrieve the window registered under name.
|
||||
|
||||
For lazy windows, creates a new instance on every call.
|
||||
For eager windows, returns the existing instance.
|
||||
"""
|
||||
entry = self._registry[name]
|
||||
|
||||
if not entry["lazy"]:
|
||||
return entry["instance"]
|
||||
|
||||
instance = entry["class"](self, self._db)
|
||||
return instance
|
||||
|
||||
@pyqtSlot(NavigateRequest)
|
||||
def navigate(self, request: NavigateRequest):
|
||||
"""
|
||||
Handle a NavigateRequest signal and switch to the target window.
|
||||
|
||||
Raises:
|
||||
KeyError: If the target window is not registered.
|
||||
"""
|
||||
if request.target not in self._registry:
|
||||
raise KeyError("Window is not registered")
|
||||
|
||||
self._current_ctx = request.context
|
||||
window = self._create_window(request.target)
|
||||
self._switch_window(window)
|
||||
|
||||
def run(self, start: str):
|
||||
"""
|
||||
Start the application from the given window.
|
||||
|
||||
Args:
|
||||
start: Name of the window to show first.
|
||||
|
||||
Raises:
|
||||
KeyError: If the start window is not registered.
|
||||
"""
|
||||
if start not in self._registry:
|
||||
raise KeyError("Window is not registered")
|
||||
|
||||
window = self._create_window(start)
|
||||
self._switch_window(window)
|
||||
return self._app.exec()
|
||||
171
pyqt6_scaffold/core/database.py
Normal file
171
pyqt6_scaffold/core/database.py
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
# SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
from .logging import setup_logger
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
log = setup_logger(__name__)
|
||||
|
||||
class CursorContext:
|
||||
"""
|
||||
Proxy wrapper around a database cursor.
|
||||
Manages cursor lifecycle as a context manager and delegates
|
||||
all attribute access to the underlying cursor object.
|
||||
"""
|
||||
def __init__(self, cursor):
|
||||
self._cursor = cursor
|
||||
|
||||
def __enter__(self):
|
||||
"""
|
||||
Return the underlying cursor.
|
||||
"""
|
||||
return self._cursor
|
||||
|
||||
def __exit__(self, *args):
|
||||
"""
|
||||
Close the cursor on exit.
|
||||
"""
|
||||
self._cursor.close()
|
||||
return False
|
||||
|
||||
def __getattr__(self, item):
|
||||
return getattr(self._cursor, item)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._cursor)
|
||||
|
||||
def __next__(self):
|
||||
return next(self._cursor)
|
||||
|
||||
class AbstractDatabase(ABC):
|
||||
"""
|
||||
Abstract base class for database connections.
|
||||
|
||||
Provides a unified interface for connecting, executing queries,
|
||||
and managing transactions. Subclasses must implement _connect()
|
||||
and placeholder.
|
||||
|
||||
Args:
|
||||
strict: If True, raises RuntimeError when connect() is called
|
||||
on an already open connection. If False, closes the
|
||||
existing connection silently.
|
||||
"""
|
||||
def __init__(self, strict: bool = True):
|
||||
self._conn = None
|
||||
self._strict = strict
|
||||
|
||||
@abstractmethod
|
||||
def _connect(self):
|
||||
"""
|
||||
Establish and return a database connection.
|
||||
|
||||
Must be implemented by subclasses. Read configuration from
|
||||
environment variables or any other source, then return a
|
||||
DB-API 2.0 compatible connection object.
|
||||
|
||||
Returns:
|
||||
A database connection object.
|
||||
"""
|
||||
pass
|
||||
|
||||
def connect(self, *args, **kwargs):
|
||||
"""
|
||||
Open the database connection.
|
||||
|
||||
Calls _connect() internally. Should not be overridden.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If connection is already open and strict=True.
|
||||
RuntimeError: If _connect() returns None.
|
||||
"""
|
||||
if self._conn is not None:
|
||||
if self._strict:
|
||||
raise RuntimeError("Database connection already open")
|
||||
else:
|
||||
try:
|
||||
self._conn.close()
|
||||
except Exception as e:
|
||||
log.error(f"Query execution failed: {e}")
|
||||
return None
|
||||
|
||||
conn = self._connect(*args, **kwargs)
|
||||
|
||||
if conn is None:
|
||||
raise RuntimeError("_connect() must return a connection object")
|
||||
|
||||
self._conn = conn
|
||||
return conn
|
||||
|
||||
def disconnect(self):
|
||||
"""
|
||||
Close the database connection and release resources.
|
||||
"""
|
||||
if self._conn is not None:
|
||||
try:
|
||||
self._conn.close()
|
||||
finally:
|
||||
self._conn = None
|
||||
|
||||
@property
|
||||
def connection(self):
|
||||
"""
|
||||
Return the active connection object.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the database is not connected.
|
||||
"""
|
||||
if self._conn is None:
|
||||
raise RuntimeError("Database not connected")
|
||||
return self._conn
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def placeholder(self) -> str:
|
||||
"""
|
||||
SQL parameter placeholder for the target dialect.
|
||||
Must be implemented by subclasses.
|
||||
"""
|
||||
pass
|
||||
|
||||
def execute(self, sql: str, params: tuple = (), autocommit: bool = False) -> CursorContext | None:
|
||||
"""
|
||||
Execute a SQL query and return a CursorContext.
|
||||
|
||||
Args:
|
||||
sql: SQL query string with placeholders.
|
||||
params: Query parameters as a tuple.
|
||||
autocommit: If True, commits the transaction after execution.
|
||||
|
||||
Returns:
|
||||
CursorContext wrapping the cursor, or raises on failure.
|
||||
|
||||
Raises:
|
||||
Exception: Re-raises any database error after rollback.
|
||||
"""
|
||||
cursor = self.connection.cursor()
|
||||
|
||||
try:
|
||||
cursor.execute(sql, params)
|
||||
|
||||
if autocommit:
|
||||
self.connection.commit()
|
||||
|
||||
return CursorContext(cursor)
|
||||
|
||||
except Exception:
|
||||
self.connection.rollback()
|
||||
log.exception("Query execution failed")
|
||||
raise
|
||||
|
||||
def __enter__(self):
|
||||
"""
|
||||
Support use as a context manager for connection lifecycle.
|
||||
Calls disconnect() on exit.
|
||||
"""
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""
|
||||
Support use as a context manager for connection lifecycle.
|
||||
Calls disconnect() on exit.
|
||||
"""
|
||||
self.disconnect()
|
||||
32
pyqt6_scaffold/core/logging.py
Normal file
32
pyqt6_scaffold/core/logging.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
import logging
|
||||
|
||||
def setup_logger(name: str = "scaffold"):
|
||||
"""
|
||||
Create and configure a logger with console output.
|
||||
|
||||
Returns an existing logger if one with the given name
|
||||
already exists to avoid duplicate handlers.
|
||||
|
||||
Args:
|
||||
name: Logger name, typically __name__.
|
||||
|
||||
Returns:
|
||||
Configured logging.Logger instance.
|
||||
"""
|
||||
logger = logging.getLogger(name)
|
||||
|
||||
if logger.handlers:
|
||||
return logger
|
||||
|
||||
formatter = logging.Formatter(
|
||||
fmt="[%(asctime)s] %(levelname)-8s %(name)s: %(message)s",
|
||||
datefmt="%H:%M:%S"
|
||||
)
|
||||
|
||||
console = logging.StreamHandler()
|
||||
console.setFormatter(formatter)
|
||||
|
||||
logger.addHandler(console)
|
||||
return logger
|
||||
248
pyqt6_scaffold/core/models.py
Normal file
248
pyqt6_scaffold/core/models.py
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
# SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
from abc import ABC
|
||||
from typing import List, Sequence, Any
|
||||
from PyQt6.QtGui import QColor, QBrush, QFont
|
||||
from PyQt6.QtCore import (
|
||||
Qt,
|
||||
QAbstractTableModel,
|
||||
QAbstractListModel,
|
||||
QModelIndex,
|
||||
QVariant
|
||||
)
|
||||
|
||||
class DataMixin(ABC):
|
||||
"""
|
||||
Mixin for Qt data models providing common data storage and display logic.
|
||||
|
||||
Intended to be used alongside QAbstractTableModel or QAbstractListModel.
|
||||
The refresh() method relies on beginResetModel() and endResetModel()
|
||||
provided by the Qt base class.
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._data: List[Sequence[Any]] = list()
|
||||
|
||||
def row_background(self, data: tuple) -> QColor | None:
|
||||
"""
|
||||
Return a background color for the given row, or None for default.
|
||||
|
||||
Override in subclass to apply conditional row highlighting.
|
||||
|
||||
Args:
|
||||
data: The full row tuple from the dataset.
|
||||
"""
|
||||
pass
|
||||
|
||||
def row_foreground(self, data: tuple) -> QColor | None:
|
||||
"""
|
||||
Return a foreground (text) color for the given row, or None for default.
|
||||
|
||||
Override in subclass to apply conditional text coloring.
|
||||
|
||||
Args:
|
||||
data: The full row tuple from the dataset.
|
||||
"""
|
||||
pass
|
||||
|
||||
def row_font(self, data: tuple) -> QFont | None:
|
||||
"""
|
||||
Return a QFont for the given row, or None for default.
|
||||
|
||||
Override in subclass to apply conditional font styling,
|
||||
such as strikethrough or bold.
|
||||
|
||||
Args:
|
||||
data: The full row tuple from the dataset.
|
||||
"""
|
||||
pass
|
||||
|
||||
def refresh(self, data: List[Sequence[Any]]):
|
||||
"""
|
||||
Replace the model data and notify the view.
|
||||
|
||||
Args:
|
||||
data: New dataset as a list of sequences.
|
||||
"""
|
||||
self.beginResetModel()
|
||||
self._data = data
|
||||
self.endResetModel()
|
||||
|
||||
def row_data(self, row: int) -> tuple | None:
|
||||
"""
|
||||
Return the raw row tuple at the given index, or None if out of range.
|
||||
"""
|
||||
if 0 <= row < len(self._data):
|
||||
return self._data[row]
|
||||
return None
|
||||
|
||||
def row_display(self, data: Sequence[Any]):
|
||||
"""
|
||||
Return the string representation of a row for DisplayRole.
|
||||
|
||||
Override in subclass to customize how rows appear in the view.
|
||||
|
||||
Args:
|
||||
data: The full row tuple from the dataset.
|
||||
"""
|
||||
return str(data[0]) if data else ""
|
||||
|
||||
class BaseTableModel(DataMixin, QAbstractTableModel):
|
||||
"""
|
||||
Table model backed by a list of row tuples.
|
||||
|
||||
Subclasses must define the headers class attribute.
|
||||
Override row_display(), row_background(), row_foreground(),
|
||||
and row_font() to customize cell appearance.
|
||||
|
||||
Attributes:
|
||||
headers: Column header labels. Must be defined in subclass.
|
||||
"""
|
||||
# Column header labels displayed in the table view.
|
||||
# Define this in your subclass: headers = ["ID", "Name", "Price"]
|
||||
headers: List[str] = []
|
||||
|
||||
def rowCount(self, parent = QModelIndex()) -> int:
|
||||
return len(self._data)
|
||||
|
||||
def columnCount(self, parent = QModelIndex()) -> int:
|
||||
return len(self.headers)
|
||||
|
||||
def row_display(self, data):
|
||||
"""
|
||||
Return the string representation of a single cell value.
|
||||
|
||||
Args:
|
||||
data: A single cell value from the dataset.
|
||||
"""
|
||||
return str(data) if data else ""
|
||||
|
||||
def data(self, index: QModelIndex, role = Qt.ItemDataRole.DisplayRole):
|
||||
if not index.isValid():
|
||||
return QVariant()
|
||||
|
||||
match role:
|
||||
case Qt.ItemDataRole.DisplayRole:
|
||||
value = self._data[index.row()][index.column()]
|
||||
if value is not None:
|
||||
return self.row_display(value)
|
||||
else:
|
||||
return ""
|
||||
|
||||
case Qt.ItemDataRole.UserRole:
|
||||
return self._data[index.row()]
|
||||
|
||||
case Qt.ItemDataRole.BackgroundRole:
|
||||
color = self.row_background(self._data[index.row()])
|
||||
if color is None:
|
||||
return QVariant()
|
||||
brush = QBrush(color)
|
||||
return brush
|
||||
|
||||
case Qt.ItemDataRole.ForegroundRole:
|
||||
color = self.row_foreground(self._data[index.row()])
|
||||
if color is None:
|
||||
return QVariant()
|
||||
brush = QBrush(color)
|
||||
return brush
|
||||
|
||||
case Qt.ItemDataRole.FontRole:
|
||||
font = self.row_font(self._data[index.row()])
|
||||
if font is None:
|
||||
return QVariant()
|
||||
return font
|
||||
|
||||
return QVariant()
|
||||
|
||||
class BaseListModel(DataMixin, QAbstractListModel):
|
||||
"""
|
||||
List model backed by a list of row tuples.
|
||||
|
||||
Each row is treated as a single list item. DisplayRole
|
||||
is rendered via row_display() which returns the first
|
||||
element by default.
|
||||
|
||||
Override row_display() to customize how items appear in the view.
|
||||
"""
|
||||
def rowCount(self, parent = QModelIndex()) -> int:
|
||||
return len(self._data)
|
||||
|
||||
def data(self, index: QModelIndex, role = Qt.ItemDataRole.DisplayRole):
|
||||
if not index.isValid():
|
||||
return QVariant()
|
||||
|
||||
match role:
|
||||
case Qt.ItemDataRole.DisplayRole:
|
||||
value = self._data[index.row()]
|
||||
if value is not None:
|
||||
return self.row_display(value)
|
||||
return ""
|
||||
|
||||
case Qt.ItemDataRole.UserRole:
|
||||
return self._data[index.row()]
|
||||
|
||||
case Qt.ItemDataRole.BackgroundRole:
|
||||
color = self.row_background(self._data[index.row()])
|
||||
if color is None:
|
||||
return QVariant()
|
||||
brush = QBrush(color)
|
||||
return brush
|
||||
|
||||
case Qt.ItemDataRole.ForegroundRole:
|
||||
color = self.row_foreground(self._data[index.row()])
|
||||
if color is None:
|
||||
return QVariant()
|
||||
brush = QBrush(color)
|
||||
return brush
|
||||
|
||||
case Qt.ItemDataRole.FontRole:
|
||||
font = self.row_font(self._data[index.row()])
|
||||
if font is None:
|
||||
return QVariant()
|
||||
return font
|
||||
|
||||
return QVariant()
|
||||
|
||||
class BaseCardModel(DataMixin, QAbstractListModel):
|
||||
"""
|
||||
List model for card-based views using a custom QStyledItemDelegate.
|
||||
|
||||
Does not implement DisplayRole — visual rendering is handled
|
||||
entirely by a delegate. Raw row data is available via UserRole.
|
||||
|
||||
Note:
|
||||
You must provide a QStyledItemDelegate subclass to render items.
|
||||
Without a delegate, the view will appear empty.
|
||||
"""
|
||||
def rowCount(self, parent = QModelIndex()) -> int:
|
||||
return len(self._data)
|
||||
|
||||
def data(self, index: QModelIndex, role = Qt.ItemDataRole.DisplayRole):
|
||||
if not index.isValid():
|
||||
return QVariant()
|
||||
|
||||
match role:
|
||||
case Qt.ItemDataRole.UserRole:
|
||||
return self._data[index.row()]
|
||||
|
||||
case Qt.ItemDataRole.BackgroundRole:
|
||||
color = self.row_background(self._data[index.row()])
|
||||
if color is None:
|
||||
return QVariant()
|
||||
brush = QBrush(color)
|
||||
return brush
|
||||
|
||||
case Qt.ItemDataRole.ForegroundRole:
|
||||
color = self.row_foreground(self._data[index.row()])
|
||||
if color is None:
|
||||
return QVariant()
|
||||
brush = QBrush(color)
|
||||
return brush
|
||||
|
||||
case Qt.ItemDataRole.FontRole:
|
||||
font = self.row_font(self._data[index.row()])
|
||||
if font is None:
|
||||
return QVariant()
|
||||
return font
|
||||
|
||||
return QVariant()
|
||||
35
pyqt6_scaffold/core/objects.py
Normal file
35
pyqt6_scaffold/core/objects.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
from typing import Any, Dict
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
@dataclass
|
||||
class BaseUser:
|
||||
"""
|
||||
Minimal user representation.
|
||||
|
||||
id and role are typed as Any to support different
|
||||
authentication schemes (integer PK, UUID, custom role objects).
|
||||
"""
|
||||
id: Any
|
||||
name: str
|
||||
role: Any
|
||||
|
||||
@dataclass
|
||||
class NavigationContext:
|
||||
"""
|
||||
Container for data passed between windows during navigation.
|
||||
"""
|
||||
data: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@dataclass
|
||||
class NavigateRequest:
|
||||
"""
|
||||
Navigation event emitted by windows to request a screen transition.
|
||||
|
||||
Attributes:
|
||||
target: Registered name of the destination window.
|
||||
context: Data to pass to the destination window.
|
||||
"""
|
||||
target: str
|
||||
context: NavigationContext
|
||||
32
pyqt6_scaffold/core/windows.py
Normal file
32
pyqt6_scaffold/core/windows.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
from PyQt6.QtWidgets import QMainWindow
|
||||
|
||||
class BaseWindow(QMainWindow):
|
||||
"""
|
||||
Base class for all application windows.
|
||||
|
||||
Provides a structured initialization sequence through four
|
||||
template methods that subclasses should override as needed.
|
||||
The composer and database are available via self._composer
|
||||
and self._db respectively.
|
||||
|
||||
_define_widgets: Create and initialize all widgets.
|
||||
_tune_layouts: Arrange widgets into layouts.
|
||||
_connect_slots: Connect signals to slots.
|
||||
_apply_windows_settings: Set window title, size, icons, and other properties.
|
||||
"""
|
||||
def __init__(self, composer, db):
|
||||
super().__init__()
|
||||
self._db = db
|
||||
self._composer = composer
|
||||
|
||||
self._define_widgets()
|
||||
self._tune_layouts()
|
||||
self._connect_slots()
|
||||
self._apply_windows_settings()
|
||||
|
||||
def _define_widgets(self): pass
|
||||
def _tune_layouts(self): pass
|
||||
def _connect_slots(self): pass
|
||||
def _apply_windows_settings(self): pass
|
||||
Loading…
Add table
Add a link
Reference in a new issue